mirror of
https://github.com/different-ai/openwork
synced 2026-05-14 11:06:25 +02:00
feat(app): align UI with Codex DLS
Apply DLS tokens, dark mode variables, and refresh Skills/Automations layouts for mock parity.
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gray-12 text-gray-12">
|
||||
<body class="bg-dls-surface text-dls-text">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
|
||||
@@ -159,7 +159,7 @@ export default function App() {
|
||||
location.pathname.toLowerCase().startsWith("/proto-v1-ux")
|
||||
);
|
||||
|
||||
const [tab, setTabState] = createSignal<DashboardTab>("home");
|
||||
const [tab, setTabState] = createSignal<DashboardTab>("scheduled");
|
||||
const [settingsTab, setSettingsTab] = createSignal<SettingsTab>("general");
|
||||
|
||||
const goToDashboard = (nextTab: DashboardTab, options?: { replace?: boolean }) => {
|
||||
@@ -3720,6 +3720,7 @@ export default function App() {
|
||||
id: session.id,
|
||||
title: session.title,
|
||||
slug: session.slug,
|
||||
time: { updated: session.time.updated },
|
||||
workspaceLabel: workspaceLabelForDirectory(session.directory),
|
||||
})),
|
||||
selectSession: selectSession,
|
||||
@@ -3782,8 +3783,6 @@ export default function App() {
|
||||
});
|
||||
|
||||
const dashboardTabs = new Set<DashboardTab>([
|
||||
"home",
|
||||
"sessions",
|
||||
"scheduled",
|
||||
"skills",
|
||||
"plugins",
|
||||
@@ -3796,7 +3795,7 @@ export default function App() {
|
||||
if (dashboardTabs.has(normalized as DashboardTab)) {
|
||||
return normalized as DashboardTab;
|
||||
}
|
||||
return "home";
|
||||
return "scheduled";
|
||||
};
|
||||
|
||||
const initialRoute = () => {
|
||||
@@ -3846,14 +3845,14 @@ export default function App() {
|
||||
|
||||
if (path.startsWith("/proto-v1-ux")) {
|
||||
if (isTauriRuntime()) {
|
||||
navigate("/dashboard/home", { replace: true });
|
||||
navigate("/dashboard/scheduled", { replace: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.startsWith("/proto")) {
|
||||
if (isTauriRuntime()) {
|
||||
navigate("/dashboard/home", { replace: true });
|
||||
navigate("/dashboard/scheduled", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ export default function Button(props: ButtonProps) {
|
||||
const variant = () => local.variant ?? "primary";
|
||||
|
||||
const base =
|
||||
"inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-medium transition-all duration-200 active:scale-95 focus:outline-none focus:ring-2 focus:ring-gray-6/15 disabled:opacity-50 disabled:cursor-not-allowed";
|
||||
"inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors duration-150 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-[rgba(var(--dls-accent-rgb),0.2)] disabled:opacity-50 disabled:cursor-not-allowed";
|
||||
|
||||
const variants: Record<NonNullable<ButtonProps["variant"]>, string> = {
|
||||
primary: "bg-gray-12 text-gray-1 hover:bg-gray-11 shadow-lg shadow-gray-12/5",
|
||||
secondary: "bg-gray-4 text-gray-12 hover:bg-gray-5 border border-gray-7/50",
|
||||
ghost: "bg-transparent text-gray-11 hover:text-gray-12 hover:bg-gray-4/50",
|
||||
outline: "border border-gray-7 text-gray-11 hover:border-gray-7 bg-transparent",
|
||||
danger: "bg-red-7/10 text-red-11 hover:bg-red-7/20 border border-red-7/20",
|
||||
primary: "bg-dls-accent text-white hover:bg-[var(--dls-accent-hover)] border border-transparent shadow-[0_1px_2px_rgba(17,24,39,0.12)]",
|
||||
secondary: "bg-dls-surface text-dls-text hover:bg-dls-hover border border-dls-border",
|
||||
ghost: "bg-transparent text-dls-secondary hover:text-dls-text hover:bg-dls-hover",
|
||||
outline: "border border-dls-border text-dls-text hover:bg-dls-hover bg-transparent",
|
||||
danger: "bg-red-3 text-red-11 hover:bg-red-4 border border-red-6",
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,10 +8,10 @@ type CardProps = {
|
||||
|
||||
export default function Card(props: CardProps) {
|
||||
return (
|
||||
<div class="rounded-2xl bg-gray-2/50 border border-gray-6/60 backdrop-blur-xl shadow-[0_20px_80px_rgba(0,0,0,0.55)]">
|
||||
<div class="rounded-xl bg-dls-surface border border-dls-border shadow-[0_1px_2px_rgba(17,24,39,0.06)] transition-shadow hover:shadow-[0_8px_24px_rgba(17,24,39,0.08)]">
|
||||
{props.title || props.actions ? (
|
||||
<div class="flex items-center justify-between gap-3 border-b border-gray-6/70 px-5 py-4">
|
||||
<div class="text-sm font-semibold text-gray-12">{props.title}</div>
|
||||
<div class="flex items-center justify-between gap-3 border-b border-dls-border px-5 py-4">
|
||||
<div class="text-sm font-semibold text-dls-text">{props.title}</div>
|
||||
<div>{props.actions}</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -133,18 +133,18 @@ export default function ModelPickerModal(props: ModelPickerModalProps) {
|
||||
|
||||
<div class="mt-5">
|
||||
<div class="relative">
|
||||
<Search size={16} class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-10" />
|
||||
<Search size={16} class="absolute left-3 top-1/2 -translate-y-1/2 text-dls-secondary" />
|
||||
<input
|
||||
ref={(el) => (searchInputRef = el)}
|
||||
type="text"
|
||||
value={props.query}
|
||||
onInput={(e) => props.setQuery(e.currentTarget.value)}
|
||||
placeholder={translate("settings.search_models")}
|
||||
class="w-full bg-gray-1/40 border border-gray-6 rounded-xl py-2.5 pl-9 pr-3 text-sm text-gray-12 placeholder-gray-6 focus:outline-none focus:ring-1 focus:ring-gray-8 focus:border-gray-8"
|
||||
class="w-full bg-dls-surface border border-dls-border rounded-xl py-2.5 pl-9 pr-3 text-sm text-dls-text placeholder:text-dls-secondary focus:outline-none focus:ring-1 focus:ring-[rgba(var(--dls-accent-rgb),0.2)] focus:border-dls-accent"
|
||||
/>
|
||||
</div>
|
||||
<Show when={props.query.trim()}>
|
||||
<div class="mt-2 text-xs text-gray-10">
|
||||
<div class="mt-2 text-xs text-dls-secondary">
|
||||
{translate("settings.showing_models").replace("{count}", String(props.filteredOptions.length)).replace("{total}", String(props.options.length))}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -59,13 +59,13 @@ export default function OnboardingWorkspaceSelector(props: {
|
||||
<div class="ml-9">
|
||||
<div
|
||||
class={`w-full border border-dashed border-gray-6 bg-gray-1/40 rounded-xl p-4 text-left transition ${
|
||||
pickingFolder() ? "opacity-70" : "hover:border-zinc-500"
|
||||
pickingFolder() ? "opacity-70" : "hover:border-dls-active"
|
||||
}`.trim()}
|
||||
>
|
||||
<div class="flex items-center gap-3 text-gray-10">
|
||||
<FolderPlus size={20} class="text-gray-6" />
|
||||
<div class="flex items-center gap-3 text-dls-text">
|
||||
<FolderPlus size={20} class="text-dls-secondary" />
|
||||
<input
|
||||
class="flex-1 min-w-0 bg-transparent text-sm font-medium text-gray-10 placeholder-gray-600 focus:outline-none"
|
||||
class="flex-1 min-w-0 bg-transparent text-sm font-medium text-dls-text placeholder:text-dls-secondary focus:outline-none"
|
||||
value={selectedFolder()}
|
||||
onInput={(e) => setSelectedFolder(e.currentTarget.value)}
|
||||
placeholder={props.defaultPath}
|
||||
@@ -74,7 +74,7 @@ export default function OnboardingWorkspaceSelector(props: {
|
||||
type="button"
|
||||
onClick={handlePickFolder}
|
||||
disabled={pickingFolder()}
|
||||
class="text-xs text-gray-6 hover:text-gray-10 transition-colors"
|
||||
class="text-xs text-dls-secondary hover:text-dls-text transition-colors"
|
||||
>
|
||||
<Show
|
||||
when={pickingFolder()}
|
||||
|
||||
@@ -92,7 +92,7 @@ function createCustomRenderer(tone: "light" | "dark") {
|
||||
href="${safeHref}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline underline-offset-2 text-blue-600 hover:text-blue-700"
|
||||
class="underline underline-offset-2 text-dls-accent hover:text-[var(--dls-accent-hover)]"
|
||||
${safeTitle ? `title="${safeTitle}"` : ""}
|
||||
>
|
||||
${text}
|
||||
@@ -337,10 +337,10 @@ export default function PartView(props: Props) {
|
||||
[&_ul]:list-disc [&_ul]:pl-6 [&_ul]:my-3
|
||||
[&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:my-3
|
||||
[&_li]:my-1
|
||||
[&_blockquote]:border-l-4 [&_blockquote]:border-gray-300 [&_blockquote]:pl-4 [&_blockquote]:my-4 [&_blockquote]:italic
|
||||
[&_blockquote]:border-l-4 [&_blockquote]:border-dls-border [&_blockquote]:pl-4 [&_blockquote]:my-4 [&_blockquote]:italic
|
||||
[&_table]:w-full [&_table]:border-collapse [&_table]:my-4
|
||||
[&_th]:border [&_th]:border-gray-300 [&_th]:p-2 [&_th]:bg-gray-50
|
||||
[&_td]:border [&_td]:border-gray-300 [&_td]:p-2
|
||||
[&_th]:border [&_th]:border-dls-border [&_th]:p-2 [&_th]:bg-dls-hover
|
||||
[&_td]:border [&_td]:border-dls-border [&_td]:p-2
|
||||
`.trim()}
|
||||
innerHTML={renderedMarkdown()!}
|
||||
/>
|
||||
|
||||
@@ -183,15 +183,15 @@ export default function QuestionModal(props: QuestionModalProps) {
|
||||
</div>
|
||||
|
||||
<Show when={currentQuestion()!.custom}>
|
||||
<div class="mt-4 pt-4 border-t border-gray-6/30">
|
||||
<label class="block text-xs font-semibold text-gray-11 mb-2 uppercase tracking-wide">
|
||||
<div class="mt-4 pt-4 border-t border-dls-border">
|
||||
<label class="block text-xs font-semibold text-dls-secondary mb-2 uppercase tracking-wide">
|
||||
Or type a custom answer
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customInput()}
|
||||
onInput={(e) => setCustomInput(e.currentTarget.value)}
|
||||
class="w-full px-4 py-3 rounded-xl bg-gray-1 border border-gray-6 focus:border-blue-9/50 focus:ring-4 focus:ring-blue-9/10 focus:outline-none text-sm text-gray-12 placeholder-gray-9 transition-shadow"
|
||||
class="w-full px-4 py-3 rounded-xl bg-dls-surface border border-dls-border focus:border-dls-accent focus:ring-4 focus:ring-[rgba(var(--dls-accent-rgb),0.2)] focus:outline-none text-sm text-dls-text placeholder:text-dls-secondary transition-shadow"
|
||||
placeholder="Type your answer here..."
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
@@ -204,9 +204,9 @@ export default function QuestionModal(props: QuestionModalProps) {
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-gray-6/40 bg-gray-2/50 flex justify-between items-center">
|
||||
<div class="text-xs text-gray-10 flex items-center gap-2">
|
||||
<span class="px-1.5 py-0.5 rounded border border-gray-6 bg-gray-3 font-mono">↑↓</span>
|
||||
<div class="p-6 border-t border-dls-border bg-dls-hover flex justify-between items-center">
|
||||
<div class="text-xs text-dls-secondary flex items-center gap-2">
|
||||
<span class="px-1.5 py-0.5 rounded border border-dls-border bg-dls-active font-mono">↑↓</span>
|
||||
<span>navigate</span>
|
||||
<span class="px-1.5 py-0.5 rounded border border-gray-6 bg-gray-3 font-mono ml-2">↵</span>
|
||||
<span>select</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js";
|
||||
import type { Agent } from "@opencode-ai/sdk/v2/client";
|
||||
import { ArrowRight, AtSign, ChevronDown, File, Paperclip, X, Zap } from "lucide-solid";
|
||||
import { ArrowUp, AtSign, ChevronDown, File, History, Mic, Paperclip, X, Zap } from "lucide-solid";
|
||||
|
||||
import type { ComposerAttachment, ComposerDraft, ComposerPart, PromptMode } from "../../types";
|
||||
|
||||
@@ -95,7 +95,7 @@ const createMentionSpan = (part: Extract<ComposerPart, { type: "agent" | "file"
|
||||
span.dataset.mentionValue = part.type === "agent" ? part.name : part.path;
|
||||
span.dataset.mentionLabel = label;
|
||||
span.className =
|
||||
"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium bg-gray-12/10 text-gray-12 border border-gray-6/70";
|
||||
"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium bg-dls-active text-dls-text border border-dls-border";
|
||||
return span;
|
||||
};
|
||||
|
||||
@@ -761,13 +761,11 @@ export default function Composer(props: ComposerProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="p-4 border-t border-gray-6 bg-gray-1 sticky bottom-0 z-20">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="px-4 pb-4 pt-0 bg-dls-surface sticky bottom-0 z-20">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div
|
||||
class={`bg-gray-2 border border-gray-6 rounded-3xl overflow-visible transition-all shadow-2xl relative group/input ${
|
||||
mentionOpen()
|
||||
? "rounded-t-none border-t-transparent"
|
||||
: "focus-within:ring-1 focus-within:ring-gray-7"
|
||||
class={`bg-dls-surface border border-dls-border rounded-2xl shadow-xl overflow-visible transition-all relative group/input ${
|
||||
mentionOpen() ? "rounded-t-none border-t-transparent" : ""
|
||||
}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={(event: DragEvent) => {
|
||||
@@ -777,16 +775,16 @@ export default function Composer(props: ComposerProps) {
|
||||
>
|
||||
<Show when={mentionOpen()}>
|
||||
<div class="absolute bottom-full left-[-1px] right-[-1px] z-30">
|
||||
<div class="rounded-t-3xl border border-gray-6 border-b-0 bg-gray-2 shadow-2xl overflow-hidden">
|
||||
<div class="px-4 pt-3 pb-2 text-[10px] font-semibold uppercase tracking-[0.2em] text-gray-8 border-b border-gray-6/30 bg-gray-2 flex items-center gap-2">
|
||||
<div class="rounded-t-3xl border border-dls-border border-b-0 bg-dls-surface shadow-2xl overflow-hidden">
|
||||
<div class="px-4 pt-3 pb-2 text-[10px] font-semibold uppercase tracking-[0.2em] text-dls-secondary border-b border-dls-border bg-dls-surface flex items-center gap-2">
|
||||
<AtSign size={12} />
|
||||
Mentions
|
||||
</div>
|
||||
<div class="space-y-3 p-3 bg-gray-2 max-h-64 overflow-y-auto">
|
||||
<div class="space-y-3 p-3 bg-dls-surface max-h-64 overflow-y-auto">
|
||||
<Show
|
||||
when={mentionOptions().length}
|
||||
fallback={
|
||||
<div class="px-3 py-2 text-xs text-gray-9">
|
||||
<div class="px-3 py-2 text-xs text-dls-secondary">
|
||||
{searchLoading() ? "Searching files..." : "No matches found."}
|
||||
</div>
|
||||
}
|
||||
@@ -794,7 +792,7 @@ export default function Composer(props: ComposerProps) {
|
||||
<For each={mentionSections()}>
|
||||
{(section: MentionSection) => (
|
||||
<div>
|
||||
<div class="px-3 py-1 text-[10px] uppercase tracking-[0.2em] text-gray-8">
|
||||
<div class="px-3 py-1 text-[10px] uppercase tracking-[0.2em] text-dls-secondary">
|
||||
{section.title}
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
@@ -806,8 +804,8 @@ export default function Composer(props: ComposerProps) {
|
||||
type="button"
|
||||
class={`w-full flex items-start gap-3 rounded-xl px-3 py-2 text-left transition-colors ${
|
||||
optionIndex() === mentionIndex()
|
||||
? "bg-gray-12/10 text-gray-12"
|
||||
: "text-gray-11 hover:bg-gray-12/5"
|
||||
? "bg-dls-active text-dls-text"
|
||||
: "text-dls-text hover:bg-dls-hover"
|
||||
}`}
|
||||
onMouseDown={(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -815,11 +813,11 @@ export default function Composer(props: ComposerProps) {
|
||||
}}
|
||||
onMouseEnter={() => setMentionIndex(optionIndex())}
|
||||
>
|
||||
<div class="text-xs font-semibold text-gray-12">
|
||||
<div class="text-xs font-semibold text-dls-text">
|
||||
{option.kind === "agent" ? `@${option.label}` : option.label}
|
||||
</div>
|
||||
<Show when={option.detail}>
|
||||
<div class="text-[11px] text-gray-9">{option.detail}</div>
|
||||
<div class="text-[11px] text-dls-secondary">{option.detail}</div>
|
||||
</Show>
|
||||
</button>
|
||||
);
|
||||
@@ -835,7 +833,7 @@ export default function Composer(props: ComposerProps) {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="absolute top-3 left-4 flex items-center gap-3 text-[10px] font-bold text-gray-7 uppercase tracking-widest z-10">
|
||||
<div class="hidden">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 text-gray-7 hover:text-gray-11 transition-colors"
|
||||
@@ -890,7 +888,7 @@ export default function Composer(props: ComposerProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3 pt-8 pb-3 px-4">
|
||||
<div class="p-3 px-4">
|
||||
<Show when={props.showNotionBanner}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -906,24 +904,24 @@ export default function Composer(props: ComposerProps) {
|
||||
<div class="mb-3 flex flex-wrap gap-2">
|
||||
<For each={attachments()}>
|
||||
{(attachment: ComposerAttachment) => (
|
||||
<div class="flex items-center gap-2 rounded-2xl border border-gray-6 bg-gray-1/70 px-3 py-2 text-xs text-gray-11">
|
||||
<div class="flex items-center gap-2 rounded-2xl border border-dls-border bg-dls-hover px-3 py-2 text-xs text-dls-secondary">
|
||||
<Show
|
||||
when={attachment.kind === "image"}
|
||||
fallback={<File size={14} class="text-gray-9" />}
|
||||
fallback={<File size={14} class="text-dls-secondary" />}
|
||||
>
|
||||
<div class="h-10 w-10 rounded-xl bg-gray-2 overflow-hidden border border-gray-6">
|
||||
<div class="h-10 w-10 rounded-xl bg-dls-surface overflow-hidden border border-dls-border">
|
||||
<img src={attachment.dataUrl} alt={attachment.name} class="h-full w-full object-cover" />
|
||||
</div>
|
||||
</Show>
|
||||
<div class="max-w-[160px]">
|
||||
<div class="truncate text-gray-12">{attachment.name}</div>
|
||||
<div class="text-[10px] text-gray-9">
|
||||
<div class="truncate text-dls-text">{attachment.name}</div>
|
||||
<div class="text-[10px] text-dls-secondary">
|
||||
{attachment.kind === "image" ? "Image" : attachment.mimeType || "File"}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 rounded-full p-1 text-gray-9 hover:text-gray-12 hover:bg-gray-12/10"
|
||||
class="ml-1 rounded-full p-1 text-dls-secondary hover:text-dls-text hover:bg-dls-active"
|
||||
onClick={() => {
|
||||
setAttachments((current: ComposerAttachment[]) =>
|
||||
current.filter((item) => item.id !== attachment.id)
|
||||
@@ -941,7 +939,7 @@ export default function Composer(props: ComposerProps) {
|
||||
|
||||
<div class="relative min-h-[120px]">
|
||||
<Show when={props.toast}>
|
||||
<div class="absolute bottom-full right-0 mb-2 z-30 rounded-xl border border-gray-6 bg-gray-1/90 px-3 py-2 text-xs text-gray-11 shadow-lg backdrop-blur-md">
|
||||
<div class="absolute bottom-full right-0 mb-2 z-30 rounded-xl border border-dls-border bg-dls-surface px-3 py-2 text-xs text-dls-secondary shadow-lg backdrop-blur-md">
|
||||
{props.toast}
|
||||
</div>
|
||||
</Show>
|
||||
@@ -949,12 +947,12 @@ export default function Composer(props: ComposerProps) {
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<Show when={props.isRemoteWorkspace}>
|
||||
<div class="mb-2 text-[10px] uppercase tracking-wider text-gray-8">Remote workspace</div>
|
||||
<div class="mb-2 text-[10px] uppercase tracking-wider text-dls-secondary">Remote workspace</div>
|
||||
</Show>
|
||||
|
||||
<div class="relative">
|
||||
<Show when={!props.prompt.trim() && !attachments().length}>
|
||||
<div class="absolute left-0 top-0 text-gray-6 text-[15px] leading-relaxed pointer-events-none">
|
||||
<div class="absolute left-0 top-0 text-dls-secondary text-sm leading-relaxed pointer-events-none">
|
||||
Ask OpenWork...
|
||||
</div>
|
||||
</Show>
|
||||
@@ -971,141 +969,74 @@ export default function Composer(props: ComposerProps) {
|
||||
onKeyUp={updateMentionQuery}
|
||||
onClick={updateMentionQuery}
|
||||
onPaste={handlePaste}
|
||||
class="bg-transparent border-none p-0 pb-12 pr-20 text-gray-12 focus:ring-0 text-[15px] leading-relaxed resize-none min-h-[24px] outline-none relative z-10"
|
||||
class="bg-transparent border-none p-0 pb-8 pr-4 text-dls-text focus:ring-0 text-sm leading-relaxed resize-none min-h-[24px] outline-none relative z-10"
|
||||
/>
|
||||
|
||||
<div class="mt-3" ref={props.setAgentPickerRef}>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 pl-3 pr-2 py-1.5 bg-gray-1/70 border border-gray-6 rounded-lg hover:border-gray-7 hover:bg-gray-3 transition-all group"
|
||||
onClick={props.onToggleAgentPicker}
|
||||
aria-expanded={props.agentPickerOpen}
|
||||
>
|
||||
<div class="p-1 rounded bg-gray-4 text-gray-10">
|
||||
<AtSign size={14} />
|
||||
</div>
|
||||
<div class="flex flex-col items-start mr-2 min-w-0">
|
||||
<span class="text-xs font-medium text-gray-12 leading-none truncate max-w-[10rem]">
|
||||
{props.agentLabel}
|
||||
</span>
|
||||
<span class="text-[10px] text-gray-10 font-mono leading-none">
|
||||
{props.selectedAgent ? "Agent" : "Default"}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown size={14} class="text-gray-10 group-hover:text-gray-11" />
|
||||
</button>
|
||||
|
||||
<Show when={props.agentPickerOpen}>
|
||||
<div class="absolute left-0 bottom-full mb-2 w-72 rounded-2xl border border-gray-6 bg-gray-1/95 shadow-2xl backdrop-blur-md overflow-hidden">
|
||||
<div class="px-4 pt-3 pb-2 text-[10px] font-semibold uppercase tracking-[0.2em] text-gray-8 border-b border-gray-6/30">
|
||||
Session agent
|
||||
</div>
|
||||
<div class="max-h-64 overflow-auto p-2 space-y-1">
|
||||
<button
|
||||
type="button"
|
||||
class={`w-full flex items-center justify-between rounded-xl px-3 py-2 text-left text-xs transition-colors ${
|
||||
props.selectedAgent ? "text-gray-11 hover:bg-gray-12/5" : "bg-gray-12/10 text-gray-12"
|
||||
}`}
|
||||
onClick={() => props.onSelectAgent(null)}
|
||||
>
|
||||
<span>Default agent</span>
|
||||
<Show when={!props.selectedAgent}>
|
||||
<span class="text-[10px] uppercase tracking-wider text-gray-9">Active</span>
|
||||
</Show>
|
||||
</button>
|
||||
<Show
|
||||
when={!props.agentPickerBusy}
|
||||
fallback={<div class="px-3 py-2 text-xs text-gray-9">Loading agents...</div>}
|
||||
>
|
||||
<Show
|
||||
when={props.agentOptions.length}
|
||||
fallback={<div class="px-3 py-2 text-xs text-gray-9">No agents available.</div>}
|
||||
>
|
||||
<For each={props.agentOptions}>
|
||||
{(agent: Agent) => (
|
||||
<button
|
||||
type="button"
|
||||
class={`w-full flex items-center justify-between rounded-xl px-3 py-2 text-left text-xs transition-colors ${
|
||||
props.selectedAgent === agent.name
|
||||
? "bg-gray-12/10 text-gray-12"
|
||||
: "text-gray-11 hover:bg-gray-12/5"
|
||||
}`}
|
||||
onClick={() => props.onSelectAgent(agent.name)}
|
||||
>
|
||||
<span>{agent.name}</span>
|
||||
<Show when={props.selectedAgent === agent.name}>
|
||||
<span class="text-[10px] uppercase tracking-wider text-gray-9">Active</span>
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={props.agentPickerError}>
|
||||
<div class="px-3 py-2 text-xs text-red-11">{props.agentPickerError}</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="border-t border-gray-6/40 px-4 py-2 text-[10px] text-gray-9">
|
||||
Tip: use /agent-next or /agent-prev to cycle.
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={!props.prompt.trim() && !attachments().length}>
|
||||
<div class="mt-2 text-[10px] text-gray-8">
|
||||
Enter to send · Shift+Enter for newline
|
||||
<div class="mt-3 flex items-center justify-between px-2 pb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept={ACCEPTED_FILE_TYPES.join(",")}
|
||||
class="hidden"
|
||||
disabled={attachmentsDisabled()}
|
||||
onChange={(event: Event) => {
|
||||
const target = event.currentTarget as HTMLInputElement;
|
||||
const files = Array.from(target.files ?? []);
|
||||
if (files.length) void addAttachments(files);
|
||||
target.value = "";
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class={`p-1.5 hover:bg-dls-hover rounded-md text-dls-secondary transition-colors ${
|
||||
attachmentsDisabled() ? "cursor-not-allowed" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (attachmentsDisabled()) return;
|
||||
fileInputRef?.click();
|
||||
}}
|
||||
disabled={attachmentsDisabled()}
|
||||
title={
|
||||
attachmentsDisabled()
|
||||
? props.attachmentsDisabledReason ?? "Attachments are unavailable."
|
||||
: "Attach files"
|
||||
}
|
||||
>
|
||||
<Paperclip size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-2 py-1 hover:bg-dls-hover rounded-md text-xs font-medium text-dls-secondary hover:text-dls-text"
|
||||
onClick={props.onModelClick}
|
||||
disabled={props.busy}
|
||||
>
|
||||
{props.selectedModelLabel}
|
||||
<ChevronDown size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-dls-secondary">
|
||||
<button type="button" class="cursor-pointer hover:text-dls-text">
|
||||
<History size={18} />
|
||||
</button>
|
||||
<button type="button" class="cursor-pointer hover:text-dls-text">
|
||||
<Mic size={18} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!props.prompt.trim() && !attachments().length}
|
||||
onClick={sendDraft}
|
||||
class={`p-1.5 rounded-full ${
|
||||
!props.prompt.trim() && !attachments().length
|
||||
? "bg-dls-active text-dls-secondary"
|
||||
: "bg-dls-accent text-white"
|
||||
}`}
|
||||
title="Send"
|
||||
>
|
||||
<ArrowUp size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="absolute bottom-0 right-0 z-20 flex items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept={ACCEPTED_FILE_TYPES.join(",")}
|
||||
class="hidden"
|
||||
disabled={attachmentsDisabled()}
|
||||
onChange={(event: Event) => {
|
||||
const target = event.currentTarget as HTMLInputElement;
|
||||
const files = Array.from(target.files ?? []);
|
||||
if (files.length) void addAttachments(files);
|
||||
target.value = "";
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class={`p-2 rounded-xl border transition-colors ${
|
||||
attachmentsDisabled()
|
||||
? "border-gray-6 text-gray-7 cursor-not-allowed"
|
||||
: "border-gray-6 text-gray-10 hover:text-gray-12 hover:border-gray-7"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (attachmentsDisabled()) return;
|
||||
fileInputRef?.click();
|
||||
}}
|
||||
disabled={attachmentsDisabled()}
|
||||
title={
|
||||
attachmentsDisabled()
|
||||
? props.attachmentsDisabledReason ?? "Attachments are unavailable."
|
||||
: "Attach files"
|
||||
}
|
||||
>
|
||||
<Paperclip size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
disabled={!props.prompt.trim() && !attachments().length}
|
||||
onClick={sendDraft}
|
||||
class={`p-2 rounded-xl transition-all shadow-lg shrink-0 flex items-center justify-center ${
|
||||
!props.prompt.trim() && !attachments().length
|
||||
? "bg-gray-4 text-gray-8 cursor-not-allowed"
|
||||
: "bg-gray-12 text-gray-1 hover:scale-105 active:scale-95"
|
||||
}`}
|
||||
title="Run"
|
||||
>
|
||||
<ArrowRight size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -228,7 +228,7 @@ export default function MessageList(props: MessageListProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="max-w-3xl mx-auto space-y-6 pb-32 px-4">
|
||||
<div class="space-y-6 pb-32">
|
||||
<For each={messageBlocks()}>
|
||||
{(block) => {
|
||||
if (block.kind === "steps-cluster") {
|
||||
@@ -380,7 +380,7 @@ export default function MessageList(props: MessageListProps) {
|
||||
</For>
|
||||
<div class="absolute bottom-2 right-2 flex justify-end opacity-100 pointer-events-auto md:opacity-0 md:pointer-events-none md:group-hover:opacity-100 md:group-hover:pointer-events-auto md:group-focus-within:opacity-100 md:group-focus-within:pointer-events-auto transition-opacity select-none">
|
||||
<button
|
||||
class="text-gray-9 hover:text-gray-11 p-1 rounded hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="text-dls-secondary hover:text-dls-text p-1 rounded hover:bg-dls-hover transition-colors"
|
||||
title="Copy message"
|
||||
onClick={() => {
|
||||
const text = block.renderableParts
|
||||
|
||||
@@ -188,7 +188,7 @@ export default function StatusBar(props: StatusBarProps) {
|
||||
|
||||
return (
|
||||
<div class="border-t border-gray-6 bg-gray-1/90 backdrop-blur-md">
|
||||
<div class="mx-auto max-w-5xl px-4 py-2 flex flex-wrap items-center gap-3 text-xs">
|
||||
<div class="px-4 py-2 flex flex-wrap items-center gap-3 text-xs">
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
title={`OpenCode Engine: ${opencodeStatusMeta().label}`}
|
||||
@@ -213,20 +213,6 @@ export default function StatusBar(props: StatusBarProps) {
|
||||
</Show>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="h-7 px-2.5 py-0 text-xs"
|
||||
onClick={props.onOpenMessaging}
|
||||
title={messagingMeta().label}
|
||||
>
|
||||
<span class="relative">
|
||||
<MessageCircle class={`w-4 h-4 ${messagingMeta().text}`} />
|
||||
<span class={`absolute -right-1 -bottom-1 w-2 h-2 rounded-full ${messagingMeta().dot}`} />
|
||||
</span>
|
||||
<Show when={props.developerMode}>
|
||||
<span class="text-gray-11 font-medium">Messaging</span>
|
||||
</Show>
|
||||
</Button>
|
||||
<Show when={tipVisible() && activeTip()}>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -11,16 +11,16 @@ export default function TextInput(props: TextInputProps) {
|
||||
return (
|
||||
<label class="block">
|
||||
{local.label ? (
|
||||
<div class="mb-1 text-xs font-medium text-gray-11">{local.label}</div>
|
||||
<div class="mb-1 text-xs font-medium text-dls-secondary">{local.label}</div>
|
||||
) : null}
|
||||
<input
|
||||
{...rest}
|
||||
ref={local.ref}
|
||||
class={`w-full rounded-xl bg-gray-2/60 px-3 py-2 text-sm text-gray-12 placeholder:text-gray-10 shadow-[0_0_0_1px_rgba(255,255,255,0.08)] focus:outline-none focus:ring-2 focus:ring-gray-6/20 ${
|
||||
class={`w-full rounded-lg bg-dls-surface px-3 py-2 text-sm text-dls-text placeholder:text-dls-secondary border border-dls-border shadow-sm focus:outline-none focus:ring-2 focus:ring-[rgba(var(--dls-accent-rgb),0.2)] ${
|
||||
local.class ?? ""
|
||||
}`.trim()}
|
||||
/>
|
||||
{local.hint ? <div class="mt-1 text-xs text-gray-10">{local.hint}</div> : null}
|
||||
{local.hint ? <div class="mt-1 text-xs text-dls-secondary">{local.hint}</div> : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function LocalProvider(props: ParentProps) {
|
||||
Persist.global("local.ui", ["openwork.ui"]),
|
||||
createStore<LocalUIState>({
|
||||
view: "onboarding",
|
||||
tab: "home",
|
||||
tab: "scheduled",
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -876,7 +876,7 @@ export function createWorkspaceStore(options: {
|
||||
options.refreshSkills({ force: true }).catch(() => undefined);
|
||||
options.refreshPlugins().catch(() => undefined);
|
||||
if (!options.selectedSessionId()) {
|
||||
options.setTab("home");
|
||||
options.setTab("scheduled");
|
||||
options.setView("session");
|
||||
}
|
||||
|
||||
@@ -946,7 +946,7 @@ export function createWorkspaceStore(options: {
|
||||
}
|
||||
|
||||
setCreateWorkspaceOpen(false);
|
||||
options.setTab("home");
|
||||
options.setTab("scheduled");
|
||||
options.setView("dashboard");
|
||||
markOnboardingComplete();
|
||||
} catch (e) {
|
||||
@@ -1386,7 +1386,7 @@ export function createWorkspaceStore(options: {
|
||||
syncActiveWorkspaceId(ws.activeId);
|
||||
setCreateWorkspaceOpen(false);
|
||||
setCreateRemoteWorkspaceOpen(false);
|
||||
options.setTab("home");
|
||||
options.setTab("scheduled");
|
||||
options.setView("dashboard");
|
||||
markOnboardingComplete();
|
||||
|
||||
|
||||
@@ -5,10 +5,32 @@
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--dls-surface: #ffffff;
|
||||
--dls-sidebar: #f3f3f3;
|
||||
--dls-border: #f3f3f3;
|
||||
--dls-accent: #2563eb;
|
||||
--dls-accent-hover: #1d4ed8;
|
||||
--dls-accent-rgb: 37 99 235;
|
||||
--dls-text-primary: #111827;
|
||||
--dls-text-secondary: #6b7280;
|
||||
--dls-hover: #f9fafb;
|
||||
--dls-active: #e5e5e7;
|
||||
--dls-radius: 8px;
|
||||
--dls-radius-lg: 16px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--dls-surface: #121212;
|
||||
--dls-sidebar: #1a1a1a;
|
||||
--dls-border: #262626;
|
||||
--dls-accent: #3b82f6;
|
||||
--dls-accent-hover: #2563eb;
|
||||
--dls-accent-rgb: 59 130 246;
|
||||
--dls-text-primary: #ededed;
|
||||
--dls-text-secondary: #9ca3af;
|
||||
--dls-hover: #1f1f1f;
|
||||
--dls-active: #2d2d2d;
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -19,6 +41,7 @@ body {
|
||||
body {
|
||||
margin: 0;
|
||||
font-family:
|
||||
Inter,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
@@ -30,6 +53,10 @@ body {
|
||||
"Noto Sans",
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji";
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--dls-text-primary);
|
||||
background-color: var(--dls-surface);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import type {
|
||||
import type { EngineInfo, OpenwrkStatus, OpenworkServerInfo, OwpenbotInfo, WorkspaceInfo } from "../lib/tauri";
|
||||
|
||||
import Button from "../components/button";
|
||||
import OpenWorkLogo from "../components/openwork-logo";
|
||||
import McpView from "./mcp";
|
||||
import PluginsView from "./plugins";
|
||||
import ScheduledTasksView from "./scheduled";
|
||||
@@ -32,7 +31,17 @@ import SettingsView from "./settings";
|
||||
import SkillsView from "./skills";
|
||||
import StatusBar from "../components/status-bar";
|
||||
import ProviderAuthModal from "../components/provider-auth-modal";
|
||||
import { Command, Cpu, Calendar, Package, Play, Server } from "lucide-solid";
|
||||
import {
|
||||
Box,
|
||||
ChevronRight,
|
||||
Edit2,
|
||||
History,
|
||||
Layout,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
Settings,
|
||||
Zap,
|
||||
} from "lucide-solid";
|
||||
|
||||
export type DashboardViewProps = {
|
||||
tab: DashboardTab;
|
||||
@@ -213,30 +222,40 @@ export type DashboardViewProps = {
|
||||
export default function DashboardView(props: DashboardViewProps) {
|
||||
const title = createMemo(() => {
|
||||
switch (props.tab) {
|
||||
case "sessions":
|
||||
return "Sessions";
|
||||
case "scheduled":
|
||||
return "Scheduled Tasks";
|
||||
return "Automations";
|
||||
case "skills":
|
||||
return "Skills";
|
||||
case "plugins":
|
||||
return "Plugins";
|
||||
case "mcp":
|
||||
return "MCPs";
|
||||
return "Apps";
|
||||
case "settings":
|
||||
return "Settings";
|
||||
default:
|
||||
return "Dashboard";
|
||||
return "Automations";
|
||||
}
|
||||
});
|
||||
|
||||
const canExportWorkspace = createMemo(() => props.activeWorkspaceDisplay.workspaceType !== "remote");
|
||||
const workspaceStatus = createMemo(() => {
|
||||
switch (props.openworkServerStatus) {
|
||||
case "connected":
|
||||
return { label: "Live", className: "bg-emerald-3 text-emerald-11" };
|
||||
case "limited":
|
||||
return { label: "Limited", className: "bg-amber-3 text-amber-11" };
|
||||
case "disconnected":
|
||||
default:
|
||||
return { label: "Offline", className: "bg-red-3 text-red-11" };
|
||||
}
|
||||
});
|
||||
const workspaceTypeLabel = createMemo(() =>
|
||||
props.activeWorkspaceDisplay.workspaceType === "remote" ? "Remote" : "Local"
|
||||
);
|
||||
|
||||
const openSessionFromList = (sessionId: string) => {
|
||||
// Defer view switch to avoid click-through on the same event frame.
|
||||
window.setTimeout(() => {
|
||||
void props.selectSession(sessionId);
|
||||
props.setTab("sessions");
|
||||
props.setView("session", sessionId);
|
||||
}, 0);
|
||||
};
|
||||
@@ -244,21 +263,13 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
// Track last refreshed tab to avoid duplicate calls
|
||||
const [lastRefreshedTab, setLastRefreshedTab] = createSignal<string | null>(null);
|
||||
const [refreshInProgress, setRefreshInProgress] = createSignal(false);
|
||||
const [taskDraft, setTaskDraft] = createSignal("");
|
||||
const [providerAuthActionBusy, setProviderAuthActionBusy] = createSignal(false);
|
||||
|
||||
const canCreateTask = createMemo(
|
||||
() => !props.newTaskDisabled && taskDraft().trim().length > 0
|
||||
const [sessionsExpanded, setSessionsExpanded] = createSignal(true);
|
||||
const [showAllSessions, setShowAllSessions] = createSignal(false);
|
||||
const visibleSessions = createMemo(() =>
|
||||
showAllSessions() ? props.sessions : props.sessions.slice(0, 5)
|
||||
);
|
||||
|
||||
const startTask = () => {
|
||||
const value = taskDraft().trim();
|
||||
if (!value || props.newTaskDisabled) return;
|
||||
props.setPrompt(value);
|
||||
props.createSessionAndOpen();
|
||||
setTaskDraft("");
|
||||
};
|
||||
|
||||
const handleProviderAuthSelect = async (providerId: string) => {
|
||||
if (providerAuthActionBusy()) return;
|
||||
setProviderAuthActionBusy(true);
|
||||
@@ -318,13 +329,6 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
if (currentTab === "scheduled" && !cancelled) {
|
||||
await props.refreshScheduledJobs();
|
||||
}
|
||||
if (currentTab === "sessions" && !cancelled) {
|
||||
// Stagger these calls to avoid request stacking
|
||||
await props.refreshSkills();
|
||||
if (!cancelled) {
|
||||
await props.refreshPlugins("project");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during navigation
|
||||
} finally {
|
||||
@@ -346,10 +350,10 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
const active = () => props.tab === t;
|
||||
return (
|
||||
<button
|
||||
class={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-colors ${
|
||||
class={`w-full h-10 flex items-center gap-3 px-3 rounded-lg text-sm font-medium transition-colors ${
|
||||
active()
|
||||
? "bg-gray-2 text-gray-12"
|
||||
: "text-gray-10 hover:text-gray-12 hover:bg-gray-2/50"
|
||||
? "bg-dls-active text-dls-text"
|
||||
: "text-dls-secondary hover:text-dls-text hover:bg-dls-hover"
|
||||
}`}
|
||||
onClick={() => props.setTab(t)}
|
||||
>
|
||||
@@ -365,274 +369,186 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex h-screen bg-gray-1 text-gray-12 overflow-hidden">
|
||||
<aside class="w-64 border-r border-gray-6 p-6 hidden md:flex flex-col justify-between bg-gray-1">
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-10 px-2">
|
||||
<div class="">
|
||||
<OpenWorkLogo size={32} />
|
||||
</div>
|
||||
<span class="font-bold text-lg tracking-tight">OpenWork</span>
|
||||
</div>
|
||||
|
||||
<nav class="space-y-1">
|
||||
{navItem("home", "Dashboard", <Command size={18} />)}
|
||||
{navItem("sessions", "Sessions", <Play size={18} />)}
|
||||
{navItem("scheduled", "Scheduled Tasks", <Calendar size={18} />)}
|
||||
{navItem("skills", "Skills", <Package size={18} />)}
|
||||
{navItem("plugins", "Plugins", <Cpu size={18} />)}
|
||||
{navItem(
|
||||
"mcp",
|
||||
<span class="inline-flex items-center gap-2">
|
||||
MCPs
|
||||
<span class="text-[10px] uppercase tracking-wide px-2 py-0.5 rounded-full bg-amber-7/20 text-amber-12">
|
||||
Alpha
|
||||
</span>
|
||||
</span>,
|
||||
<Server size={18} />,
|
||||
)}
|
||||
</nav>
|
||||
<div class="flex h-screen w-full bg-dls-surface text-dls-text font-sans overflow-hidden">
|
||||
<aside class="w-64 hidden md:flex flex-col bg-dls-sidebar border-r border-dls-border p-4">
|
||||
<div class="space-y-0.5 mb-6 pt-2">
|
||||
{navItem("scheduled", "Automations", <History size={18} />)}
|
||||
{navItem("skills", "Skills", <Zap size={18} />)}
|
||||
{navItem("mcp", "Apps", <Box size={18} />)}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2 mb-6">
|
||||
<div class="flex items-center justify-between text-[11px] font-bold text-dls-secondary uppercase px-3 tracking-tight">
|
||||
<span>Workspace</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Workspace settings"
|
||||
onClick={() => openSettings("general")}
|
||||
class="text-dls-secondary hover:text-dls-text"
|
||||
>
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="rounded-lg border border-dls-border bg-dls-surface px-3 py-2">
|
||||
<div class="text-sm font-semibold text-dls-text truncate">
|
||||
{props.activeWorkspaceDisplay.name}
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-2 text-xs text-dls-secondary">
|
||||
<span>{workspaceTypeLabel()}</span>
|
||||
<span>{workspaceStatus().label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={
|
||||
props.activeWorkspaceDisplay.workspaceType === "remote" &&
|
||||
props.openworkServerStatus === "disconnected"
|
||||
}
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div class="flex items-center justify-between text-[11px] font-bold text-dls-secondary uppercase px-3 mb-3 tracking-tight">
|
||||
<span>Sessions</span>
|
||||
<div class="flex gap-2 text-dls-secondary">
|
||||
<button type="button" class="hover:text-dls-text" aria-label="Session layout">
|
||||
<Layout size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="hover:text-dls-text"
|
||||
aria-label="New session"
|
||||
onClick={props.createSessionAndOpen}
|
||||
disabled={props.newTaskDisabled}
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSessionsExpanded((current) => !current)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
setSessionsExpanded((current) => !current);
|
||||
}}
|
||||
class="group flex items-center justify-between h-8 px-3 rounded-lg cursor-pointer text-dls-text transition-colors hover:bg-dls-hover"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<ChevronRight
|
||||
size={14}
|
||||
class={`text-dls-secondary transition-transform ${
|
||||
sessionsExpanded() ? "rotate-90" : ""
|
||||
}`}
|
||||
/>
|
||||
<span class="text-sm font-medium">{props.activeWorkspaceDisplay.name}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-secondary transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Workspace options"
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={sessionsExpanded()}>
|
||||
<div class="mt-0.5 space-y-0.5 border-l border-dls-border ml-4">
|
||||
<Show
|
||||
when={props.sessions.length > 0}
|
||||
fallback={
|
||||
<div class="px-3 py-2 text-xs text-dls-secondary">
|
||||
No sessions yet.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={visibleSessions()}>
|
||||
{(session) => (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
class="group flex items-center justify-between h-8 px-3 hover:bg-dls-hover rounded-lg cursor-pointer relative overflow-hidden ml-5 w-[calc(100%-1.25rem)]"
|
||||
onClick={() => openSessionFromList(session.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
openSessionFromList(session.id);
|
||||
}}
|
||||
>
|
||||
<span class="text-sm text-dls-text truncate mr-2 font-medium group-hover:pr-14 transition-all">
|
||||
{session.title}
|
||||
</span>
|
||||
<span class="text-xs text-dls-secondary whitespace-nowrap group-hover:opacity-0 transition-opacity">
|
||||
{formatRelativeTime(session.time.updated)}
|
||||
</span>
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-text transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Session options"
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-text transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Rename session"
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={props.sessions.length > 5}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllSessions((current) => !current)}
|
||||
class="px-3 py-1.5 text-xs text-dls-secondary hover:text-dls-text font-medium"
|
||||
>
|
||||
{showAllSessions() ? "Show less" : "Show more"}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-dls-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openSettings("general")}
|
||||
class="flex items-center gap-3 px-3 py-2 rounded-lg text-dls-secondary hover:bg-dls-hover transition-colors"
|
||||
>
|
||||
<div class="text-[11px] text-gray-9 px-1">
|
||||
OpenWork server is offline — remote tasks still run.
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!props.clientConnected}>
|
||||
<div class="text-[11px] text-gray-9 px-1">
|
||||
Add a workspace from the Sessions sidebar to get started.
|
||||
</div>
|
||||
</Show>
|
||||
<Settings size={18} />
|
||||
<span class="text-sm font-medium">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 overflow-y-auto relative pb-24 md:pb-12">
|
||||
<header class="h-16 flex items-center justify-between px-6 md:px-10 border-b border-gray-6 sticky top-0 bg-gray-1/80 backdrop-blur-md z-10">
|
||||
<main class="flex-1 overflow-y-auto relative pb-24 md:pb-12 bg-dls-surface">
|
||||
<header class="h-14 flex items-center justify-between px-6 md:px-10 border-b border-dls-border sticky top-0 bg-dls-surface z-10">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="px-3 py-1.5 rounded-xl bg-gray-2 text-xs text-gray-11 font-medium">
|
||||
<div class="px-3 py-1.5 rounded-xl bg-dls-hover text-xs text-dls-secondary font-medium">
|
||||
{props.activeWorkspaceDisplay.name}
|
||||
</div>
|
||||
<h1 class="text-lg font-medium">{title()}</h1>
|
||||
<Show when={props.developerMode}>
|
||||
<span class="text-xs text-gray-7">{props.headerStatus}</span>
|
||||
<span class="text-xs text-dls-secondary">{props.headerStatus}</span>
|
||||
</Show>
|
||||
<Show when={props.busyHint}>
|
||||
<span class="text-xs text-gray-10">{props.busyHint}</span>
|
||||
<span class="text-xs text-dls-secondary">{props.busyHint}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={props.tab === "home" || props.tab === "sessions"}>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="text-xs h-9"
|
||||
onClick={props.exportWorkspaceConfig}
|
||||
disabled={!canExportWorkspace() || props.exportWorkspaceBusy}
|
||||
title={
|
||||
!canExportWorkspace()
|
||||
? "Export is only available for local workspaces"
|
||||
: "Export workspace config"
|
||||
}
|
||||
>
|
||||
Share config
|
||||
</Button>
|
||||
<Button
|
||||
onPointerDown={(e) => {
|
||||
e.currentTarget.setPointerCapture?.(e.pointerId);
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
console.log("[DEBUG] new task button pointerup");
|
||||
props.createSessionAndOpen();
|
||||
}}
|
||||
disabled={props.newTaskDisabled}
|
||||
title={props.newTaskDisabled ? props.busyHint ?? "Busy" : ""}
|
||||
>
|
||||
<Play size={16} />
|
||||
New Task
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
<div class="flex items-center gap-2" />
|
||||
</header>
|
||||
|
||||
<div class="p-6 md:p-10 max-w-5xl mx-auto space-y-10">
|
||||
<Switch>
|
||||
<Match when={props.tab === "home"}>
|
||||
<section>
|
||||
<div class="bg-gradient-to-r from-gray-2 to-gray-4 rounded-3xl p-1 ">
|
||||
<div class="bg-gray-1 rounded-[22px] p-6 md:p-8 flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
<div class="space-y-2 text-center md:text-left">
|
||||
<h2 class="text-2xl font-semibold text-gray-12">
|
||||
What should we do today?
|
||||
</h2>
|
||||
<p class="text-gray-11">
|
||||
Describe an outcome. OpenWork will run it and keep an
|
||||
audit trail.
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full md:w-[360px]">
|
||||
<div class="flex items-center gap-2 rounded-2xl border border-gray-6/60 bg-gray-2/50 px-4 py-3 shadow-lg shadow-gray-12/5 focus-within:border-gray-7 focus-within:bg-gray-2 transition-all">
|
||||
<input
|
||||
value={taskDraft()}
|
||||
onInput={(event) => setTaskDraft(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
startTask();
|
||||
}
|
||||
}}
|
||||
placeholder="Draft a task to run..."
|
||||
class="flex-1 bg-transparent border-none p-0 text-sm text-gray-12 placeholder-gray-7 focus:ring-0"
|
||||
aria-label="Describe a task"
|
||||
disabled={props.newTaskDisabled}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={startTask}
|
||||
disabled={!canCreateTask()}
|
||||
class="rounded-xl bg-gray-12 px-3 py-1.5 text-xs font-semibold text-gray-1 shadow-md transition-all hover:scale-[1.02] active:scale-95 disabled:opacity-40 disabled:hover:scale-100"
|
||||
title={
|
||||
props.newTaskDisabled ? props.busyHint ?? "Busy" : ""
|
||||
}
|
||||
>
|
||||
Run
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 text-[11px] text-gray-9 text-center md:text-left">
|
||||
Press Enter to start a new session.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-sm font-medium text-gray-11 uppercase tracking-wider mb-4">
|
||||
Recent Sessions
|
||||
</h3>
|
||||
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl overflow-hidden">
|
||||
<For each={props.sessions.slice(0, 3)}>
|
||||
{(s, idx) => (
|
||||
<button
|
||||
class={`w-full p-4 flex items-center justify-between hover:bg-gray-4/50 transition-colors text-left ${
|
||||
idx() !== Math.min(props.sessions.length, 3) - 1
|
||||
? "border-b border-gray-6/50"
|
||||
: ""
|
||||
}`}
|
||||
onPointerDown={(e) => {
|
||||
e.currentTarget.setPointerCapture?.(e.pointerId);
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
openSessionFromList(s.id);
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-8 h-8 rounded-full bg-gray-4 flex items-center justify-center text-xs text-gray-10 font-mono">
|
||||
#{s.slug?.slice(0, 2) ?? ".."}
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-sm text-gray-12">
|
||||
{s.title}
|
||||
</div>
|
||||
<div class="text-xs text-gray-10 flex items-center gap-2">
|
||||
<span class="flex items-center gap-1">
|
||||
{formatRelativeTime(s.time.updated)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-xs px-2 py-0.5 rounded-full border border-gray-7/60 text-gray-11 flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-current" />
|
||||
{props.sessionStatusById[s.id] ?? "idle"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={!props.sessions.length}>
|
||||
<div class="p-6 text-sm text-gray-10 space-y-3">
|
||||
<div>No sessions yet.</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="text-xs h-8"
|
||||
onClick={props.createSessionAndOpen}
|
||||
disabled={props.newTaskDisabled}
|
||||
>
|
||||
Start a task
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
</Match>
|
||||
|
||||
<Match when={props.tab === "sessions"}>
|
||||
<section>
|
||||
<h3 class="text-sm font-medium text-gray-11 uppercase tracking-wider mb-4">
|
||||
Sessions
|
||||
</h3>
|
||||
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl overflow-hidden">
|
||||
<For each={props.sessions}>
|
||||
{(s, idx) => (
|
||||
<button
|
||||
class={`w-full p-4 flex items-center justify-between hover:bg-gray-4/50 transition-colors text-left ${
|
||||
idx() !== props.sessions.length - 1
|
||||
? "border-b border-gray-6/50"
|
||||
: ""
|
||||
}`}
|
||||
onPointerDown={(e) => {
|
||||
e.currentTarget.setPointerCapture?.(e.pointerId);
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
openSessionFromList(s.id);
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-8 h-8 rounded-full bg-gray-4 flex items-center justify-center text-xs text-gray-10 font-mono">
|
||||
#{s.slug?.slice(0, 2) ?? ".."}
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-sm text-gray-12">
|
||||
{s.title}
|
||||
</div>
|
||||
<div class="text-xs text-gray-10 flex items-center gap-2">
|
||||
<span class="flex items-center gap-1">
|
||||
{formatRelativeTime(s.time.updated)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-xs px-2 py-0.5 rounded-full border border-gray-7/60 text-gray-11 flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-current" />
|
||||
{props.sessionStatusById[s.id] ?? "idle"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={!props.sessions.length}>
|
||||
<div class="p-6 text-sm text-gray-10">
|
||||
No sessions yet.
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
</Match>
|
||||
|
||||
<Match when={props.tab === "scheduled"}>
|
||||
<ScheduledTasksView
|
||||
jobs={props.scheduledJobs}
|
||||
@@ -843,34 +759,16 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
providerConnectedIds={props.providerConnectedIds}
|
||||
mcpStatuses={props.mcpStatuses}
|
||||
/>
|
||||
<nav class="md:hidden border-t border-gray-6 bg-gray-1/90 backdrop-blur-md">
|
||||
<div class="mx-auto max-w-5xl px-4 py-3 grid grid-cols-7 gap-2">
|
||||
<button
|
||||
class={`flex flex-col items-center gap-1 text-xs ${
|
||||
props.tab === "home" ? "text-gray-12" : "text-gray-10"
|
||||
}`}
|
||||
onClick={() => props.setTab("home")}
|
||||
>
|
||||
<Command size={18} />
|
||||
Home
|
||||
</button>
|
||||
<button
|
||||
class={`flex flex-col items-center gap-1 text-xs ${
|
||||
props.tab === "sessions" ? "text-gray-12" : "text-gray-10"
|
||||
}`}
|
||||
onClick={() => props.setTab("sessions")}
|
||||
>
|
||||
<Play size={18} />
|
||||
Runs
|
||||
</button>
|
||||
<nav class="md:hidden border-t border-dls-border bg-dls-surface">
|
||||
<div class="mx-auto max-w-5xl px-4 py-3 grid grid-cols-3 gap-2">
|
||||
<button
|
||||
class={`flex flex-col items-center gap-1 text-xs ${
|
||||
props.tab === "scheduled" ? "text-gray-12" : "text-gray-10"
|
||||
}`}
|
||||
onClick={() => props.setTab("scheduled")}
|
||||
>
|
||||
<Calendar size={18} />
|
||||
Schedule
|
||||
<History size={18} />
|
||||
Automations
|
||||
</button>
|
||||
<button
|
||||
class={`flex flex-col items-center gap-1 text-xs ${
|
||||
@@ -878,26 +776,17 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
}`}
|
||||
onClick={() => props.setTab("skills")}
|
||||
>
|
||||
<Package size={18} />
|
||||
<Zap size={18} />
|
||||
Skills
|
||||
</button>
|
||||
<button
|
||||
class={`flex flex-col items-center gap-1 text-xs ${
|
||||
props.tab === "plugins" ? "text-gray-12" : "text-gray-10"
|
||||
}`}
|
||||
onClick={() => props.setTab("plugins")}
|
||||
>
|
||||
<Cpu size={18} />
|
||||
Plugins
|
||||
</button>
|
||||
<button
|
||||
class={`flex flex-col items-center gap-1 text-xs ${
|
||||
props.tab === "mcp" ? "text-gray-12" : "text-gray-10"
|
||||
}`}
|
||||
onClick={() => props.setTab("mcp")}
|
||||
>
|
||||
<Server size={18} />
|
||||
MCPs
|
||||
<Box size={18} />
|
||||
Apps
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -43,16 +43,16 @@ export type McpViewProps = {
|
||||
const statusBadge = (status: "connected" | "needs_auth" | "needs_client_registration" | "failed" | "disabled" | "disconnected") => {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
return "bg-green-7/10 text-green-11 border-green-7/20";
|
||||
return "bg-green-3 text-green-11 border-green-6";
|
||||
case "needs_auth":
|
||||
case "needs_client_registration":
|
||||
return "bg-amber-7/10 text-amber-11 border-amber-7/20";
|
||||
return "bg-amber-3 text-amber-11 border-amber-6";
|
||||
case "disabled":
|
||||
return "bg-gray-4/60 text-gray-11 border-gray-7/50";
|
||||
return "bg-gray-3 text-gray-11 border-gray-6";
|
||||
case "disconnected":
|
||||
return "bg-gray-2/80 text-gray-12 border-gray-7/50";
|
||||
return "bg-gray-2 text-gray-11 border-gray-6";
|
||||
default:
|
||||
return "bg-red-7/10 text-red-11 border-red-7/20";
|
||||
return "bg-red-3 text-red-11 border-red-6";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -180,41 +180,37 @@ export default function McpView(props: McpViewProps) {
|
||||
const canConnect = () => !props.busy;
|
||||
|
||||
return (
|
||||
<section class="space-y-6">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-lg font-semibold text-gray-12">{translate("mcp.title")}</h2>
|
||||
<p class="text-sm text-gray-11">
|
||||
{translate("mcp.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[1.5fr_1fr] animate-in fade-in slide-in-from-top-11 duration-300">
|
||||
<div class="space-y-6">
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-12">{translate("mcp.mcps_title")}</div>
|
||||
<div class="text-xs text-gray-10">
|
||||
{translate("mcp.connect_mcp_hint")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-10 text-right">
|
||||
<div>{props.mcpServers.length} {translate("mcp.configured")}</div>
|
||||
<Show when={props.mcpLastUpdatedAt}>
|
||||
<div>{translate("mcp.updated")} {formatRelativeTime(props.mcpLastUpdatedAt ?? Date.now())}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={props.mcpStatus}>
|
||||
<div class="text-xs text-gray-10">{props.mcpStatus}</div>
|
||||
<section class="space-y-10">
|
||||
<div class="rounded-2xl border border-dls-border bg-dls-surface p-6 md:p-8 space-y-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-dls-text">{translate("mcp.title")}</h2>
|
||||
<p class="text-sm text-dls-secondary mt-1">
|
||||
{translate("mcp.description")}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-xs text-dls-secondary text-right">
|
||||
<div>{props.mcpServers.length} {translate("mcp.configured")}</div>
|
||||
<Show when={props.mcpLastUpdatedAt}>
|
||||
<div>{translate("mcp.updated")} {formatRelativeTime(props.mcpLastUpdatedAt ?? Date.now())}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={props.mcpStatus}>
|
||||
<div class="rounded-xl border border-dls-border bg-dls-hover px-4 py-3 text-xs text-dls-secondary">
|
||||
{props.mcpStatus}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-8 lg:grid-cols-[1.5fr_1fr] animate-in fade-in slide-in-from-top-11 duration-300">
|
||||
<div class="space-y-6">
|
||||
|
||||
<Show when={props.showMcpReloadBanner}>
|
||||
<div class="bg-gray-2/60 border border-gray-6/70 rounded-2xl px-4 py-3 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div class="bg-dls-hover border border-dls-border rounded-xl px-4 py-3 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-12">{translate("mcp.reload_banner_title")}</div>
|
||||
<div class="text-xs text-gray-10">
|
||||
<div class="text-sm font-semibold text-dls-text">{translate("mcp.reload_banner_title")}</div>
|
||||
<div class="text-xs text-dls-secondary">
|
||||
{props.reloadBlocked
|
||||
? translate("mcp.reload_banner_description_blocked")
|
||||
: translate("mcp.reload_banner_description")}
|
||||
@@ -231,20 +227,20 @@ export default function McpView(props: McpViewProps) {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4">
|
||||
<div class="rounded-2xl border border-dls-border bg-dls-surface p-6 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm font-medium text-gray-12">{translate("mcp.quick_connect_title")}</div>
|
||||
<div class="text-[11px] text-gray-10">{translate("mcp.oauth_only_label")}</div>
|
||||
<div class="text-sm font-semibold text-dls-text">{translate("mcp.quick_connect_title")}</div>
|
||||
<div class="text-[11px] text-dls-secondary">{translate("mcp.oauth_only_label")}</div>
|
||||
</div>
|
||||
<div class="grid gap-3">
|
||||
<For each={quickConnectList()}>
|
||||
{(entry) => (
|
||||
<div class="rounded-2xl border border-gray-6/70 bg-gray-1/40 p-4 space-y-3">
|
||||
<div class="rounded-xl border border-dls-border bg-dls-hover p-4 space-y-3">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-12">{entry.name}</div>
|
||||
<div class="text-xs text-gray-10 mt-1">{entry.description}</div>
|
||||
<div class="text-xs text-gray-7 font-mono mt-1">
|
||||
<div class="text-sm font-semibold text-dls-text">{entry.name}</div>
|
||||
<div class="text-xs text-dls-secondary mt-1">{entry.description}</div>
|
||||
<div class="text-xs text-dls-secondary font-mono mt-1">
|
||||
{entry.type === "local" ? entry.command?.join(" ") : entry.url}
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,7 +248,7 @@ export default function McpView(props: McpViewProps) {
|
||||
<Show
|
||||
when={!isQuickConnectConnected(entry.name)}
|
||||
fallback={
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-green-7/10 border border-green-7/20">
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-green-3 border border-green-6">
|
||||
<CheckCircle2 size={16} class="text-green-11" />
|
||||
<span class="text-sm text-green-11">{translate("mcp.connected_status")}</span>
|
||||
</div>
|
||||
@@ -287,22 +283,22 @@ export default function McpView(props: McpViewProps) {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[11px] text-gray-10">{translate("mcp.no_env_vars")}</div>
|
||||
<div class="text-[11px] text-dls-secondary">{translate("mcp.no_env_vars")}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4">
|
||||
<div class="bg-dls-surface border border-dls-border rounded-2xl p-5 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm font-medium text-gray-12">{translate("mcp.connected_title")}</div>
|
||||
<div class="text-[11px] text-gray-10">{translate("mcp.from_opencode_json")}</div>
|
||||
<div class="text-sm font-medium text-dls-text">{translate("mcp.connected_title")}</div>
|
||||
<div class="text-[11px] text-dls-secondary">{translate("mcp.from_opencode_json")}</div>
|
||||
</div>
|
||||
<Show
|
||||
when={props.mcpServers.length}
|
||||
fallback={
|
||||
<div class="rounded-xl border border-gray-6/60 bg-gray-1/40 p-4 text-sm text-gray-10">
|
||||
<div class="rounded-xl border border-dls-border bg-dls-hover p-4 text-sm text-dls-secondary">
|
||||
{translate("mcp.no_servers_yet")}
|
||||
</div>
|
||||
}
|
||||
@@ -320,17 +316,17 @@ export default function McpView(props: McpViewProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class={`text-left rounded-2xl border px-4 py-3 transition-all ${
|
||||
class={`text-left rounded-2xl border px-4 py-3 transition-colors ${
|
||||
props.selectedMcp === entry.name
|
||||
? "border-gray-8 bg-gray-2/70"
|
||||
: "border-gray-6/70 bg-gray-1/40 hover:border-gray-7"
|
||||
? "border-dls-border bg-dls-active"
|
||||
: "border-dls-border bg-dls-hover hover:bg-dls-active"
|
||||
}`}
|
||||
onClick={() => props.setSelectedMcp(entry.name)}
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-12">{entry.name}</div>
|
||||
<div class="text-xs text-gray-10 font-mono">
|
||||
<div class="text-sm font-medium text-dls-text">{entry.name}</div>
|
||||
<div class="text-xs text-dls-secondary font-mono">
|
||||
{entry.config.type === "remote" ? entry.config.url : entry.config.command?.join(" ")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -346,11 +342,11 @@ export default function McpView(props: McpViewProps) {
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4">
|
||||
<div class="bg-dls-surface border border-dls-border rounded-2xl p-5 space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-medium text-gray-12">{translate("mcp.edit_config_title")}</div>
|
||||
<div class="text-xs text-gray-10">
|
||||
<div class="text-sm font-medium text-dls-text">{translate("mcp.edit_config_title")}</div>
|
||||
<div class="text-xs text-dls-secondary">
|
||||
{translate("mcp.edit_config_description")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -358,7 +354,7 @@ export default function McpView(props: McpViewProps) {
|
||||
href="https://opencode.ai/docs/mcp-servers/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1.5 text-xs text-gray-10 hover:text-gray-12 underline decoration-gray-6/30 underline-offset-4 transition-colors"
|
||||
class="inline-flex items-center gap-1.5 text-xs text-dls-secondary hover:text-dls-text underline decoration-dls-border underline-offset-4 transition-colors"
|
||||
>
|
||||
<ExternalLink size={12} />
|
||||
{translate("mcp.docs_link")}
|
||||
@@ -369,8 +365,8 @@ export default function McpView(props: McpViewProps) {
|
||||
<button
|
||||
class={`px-3 py-1 rounded-full text-xs font-medium border transition-colors ${
|
||||
configScope() === "project"
|
||||
? "bg-gray-12/10 text-gray-12 border-gray-6/30"
|
||||
: "text-gray-10 border-gray-6 hover:text-gray-12"
|
||||
? "bg-dls-active text-dls-text border-dls-border"
|
||||
: "text-dls-secondary border-dls-border hover:text-dls-text hover:bg-dls-hover"
|
||||
}`}
|
||||
onClick={() => setConfigScope("project")}
|
||||
>
|
||||
@@ -379,8 +375,8 @@ export default function McpView(props: McpViewProps) {
|
||||
<button
|
||||
class={`px-3 py-1 rounded-full text-xs font-medium border transition-colors ${
|
||||
configScope() === "global"
|
||||
? "bg-gray-12/10 text-gray-12 border-gray-6/30"
|
||||
: "text-gray-10 border-gray-6 hover:text-gray-12"
|
||||
? "bg-dls-active text-dls-text border-dls-border"
|
||||
: "text-dls-secondary border-dls-border hover:text-dls-text hover:bg-dls-hover"
|
||||
}`}
|
||||
onClick={() => setConfigScope("global")}
|
||||
>
|
||||
@@ -388,9 +384,9 @@ export default function McpView(props: McpViewProps) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 text-xs text-gray-10">
|
||||
<div class="flex flex-col gap-1 text-xs text-dls-secondary">
|
||||
<div>{translate("mcp.config_label")}</div>
|
||||
<div class="text-gray-7 font-mono truncate">
|
||||
<div class="text-dls-secondary font-mono truncate">
|
||||
{activeConfig()?.path ?? translate("mcp.config_not_loaded")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -415,38 +411,38 @@ export default function McpView(props: McpViewProps) {
|
||||
</Show>
|
||||
</Button>
|
||||
<Show when={activeConfig() && activeConfig()!.exists === false}>
|
||||
<div class="text-[11px] text-zinc-600">{translate("mcp.file_not_found")}</div>
|
||||
<div class="text-[11px] text-dls-secondary">{translate("mcp.file_not_found")}</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={configError()}>
|
||||
<div class="text-xs text-red-300">{configError()}</div>
|
||||
<div class="text-xs text-red-11">{configError()}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4 lg:sticky lg:top-6 self-start">
|
||||
<div class="bg-dls-surface border border-dls-border rounded-2xl p-5 space-y-4 lg:sticky lg:top-6 self-start">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm font-medium text-gray-12">{translate("mcp.details_title")}</div>
|
||||
<div class="text-xs text-gray-10">{selectedEntry()?.name ?? translate("mcp.select_server_hint").split(" ").slice(0, 3).join(" ")}</div>
|
||||
<div class="text-sm font-medium text-dls-text">{translate("mcp.details_title")}</div>
|
||||
<div class="text-xs text-dls-secondary">{selectedEntry()?.name ?? translate("mcp.select_server_hint").split(" ").slice(0, 3).join(" ")}</div>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={selectedEntry()}
|
||||
fallback={
|
||||
<div class="rounded-xl border border-gray-6/60 bg-gray-1/40 p-4 text-sm text-gray-10">
|
||||
<div class="rounded-xl border border-dls-border bg-dls-hover p-4 text-sm text-dls-secondary">
|
||||
{translate("mcp.select_server_hint")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{(entry) => (
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border border-gray-6/70 bg-gray-1/40 p-4 space-y-2">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-12">
|
||||
<div class="rounded-xl border border-dls-border bg-dls-hover p-4 space-y-2">
|
||||
<div class="flex items-center gap-2 text-sm text-dls-text">
|
||||
<Settings size={16} />
|
||||
{entry().name}
|
||||
</div>
|
||||
<div class="text-xs text-gray-10 font-mono break-all">
|
||||
<div class="text-xs text-dls-secondary font-mono break-all">
|
||||
{entry().config.type === "remote" ? entry().config.url : entry().config.command?.join(" ")}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -467,28 +463,28 @@ export default function McpView(props: McpViewProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-6/70 bg-gray-1/40 p-4 space-y-2">
|
||||
<div class="text-xs text-gray-11 uppercase tracking-wider">{translate("mcp.capabilities_label")}</div>
|
||||
<div class="rounded-xl border border-dls-border bg-dls-hover p-4 space-y-2">
|
||||
<div class="text-xs text-dls-secondary uppercase tracking-wider">{translate("mcp.capabilities_label")}</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="text-[10px] uppercase tracking-wide bg-gray-4/70 text-gray-11 px-2 py-0.5 rounded-full">
|
||||
<span class="text-[10px] uppercase tracking-wide bg-dls-active text-dls-secondary px-2 py-0.5 rounded-full">
|
||||
{translate("mcp.tools_enabled_label")}
|
||||
</span>
|
||||
<span class="text-[10px] uppercase tracking-wide bg-gray-4/70 text-gray-11 px-2 py-0.5 rounded-full">
|
||||
<span class="text-[10px] uppercase tracking-wide bg-dls-active text-dls-secondary px-2 py-0.5 rounded-full">
|
||||
{translate("mcp.oauth_ready_label")}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-10">
|
||||
<div class="text-xs text-dls-secondary">
|
||||
{translate("mcp.usage_hint_text")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-6/70 bg-gray-1/40 p-4 space-y-2">
|
||||
<div class="text-xs text-gray-11 uppercase tracking-wider">{translate("mcp.next_steps_label")}</div>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-10">
|
||||
<div class="rounded-xl border border-dls-border bg-dls-hover p-4 space-y-2">
|
||||
<div class="text-xs text-dls-secondary uppercase tracking-wider">{translate("mcp.next_steps_label")}</div>
|
||||
<div class="flex items-center gap-2 text-xs text-dls-secondary">
|
||||
<CheckCircle2 size={14} />
|
||||
{translate("mcp.reload_step")}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-10">
|
||||
<div class="flex items-center gap-2 text-xs text-dls-secondary">
|
||||
<CircleAlert size={14} />
|
||||
{translate("mcp.auth_step")}
|
||||
</div>
|
||||
|
||||
@@ -248,7 +248,7 @@ export default function OnboardingView(props: OnboardingViewProps) {
|
||||
<div class="space-y-3">
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
class="w-full bg-gray-2/50 border border-gray-6 rounded-xl px-3 py-2 text-sm text-gray-12 placeholder-gray-7 focus:outline-none focus:ring-1 focus:ring-gray-8 focus:border-gray-8 transition-all"
|
||||
class="w-full bg-dls-surface border border-dls-border rounded-xl px-3 py-2 text-sm text-dls-text placeholder:text-dls-secondary focus:outline-none focus:ring-1 focus:ring-[rgba(var(--dls-accent-rgb),0.2)] focus:border-dls-accent transition-all"
|
||||
placeholder={translate("onboarding.add_folder_path")}
|
||||
value={props.newAuthorizedDir}
|
||||
onInput={(e) => props.onSetAuthorizedDir(e.currentTarget.value)}
|
||||
|
||||
@@ -95,7 +95,7 @@ const navItems: Array<{
|
||||
label: string;
|
||||
icon: any;
|
||||
}> = [
|
||||
{ id: "home", label: "Dashboard", icon: Command },
|
||||
{ id: "scheduled", label: "Schedule", icon: Command },
|
||||
{ id: "sessions", label: "Sessions", icon: Play },
|
||||
{ id: "skills", label: "Skills", icon: Folder },
|
||||
{ id: "settings", label: "Settings", icon: Settings },
|
||||
@@ -224,7 +224,7 @@ export default function ProtoWorkspacesView() {
|
||||
{(item) => (
|
||||
<button
|
||||
class={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-colors ${
|
||||
item.id === "home"
|
||||
item.id === "scheduled"
|
||||
? "bg-gray-2 text-gray-12"
|
||||
: "text-gray-10 hover:text-gray-12 hover:bg-gray-2/50"
|
||||
}`}
|
||||
@@ -277,7 +277,7 @@ export default function ProtoWorkspacesView() {
|
||||
<div class="flex items-center gap-2 rounded-2xl border border-gray-6/60 bg-gray-2/50 px-4 py-3">
|
||||
<input
|
||||
placeholder="Draft a task to run..."
|
||||
class="flex-1 bg-transparent border-none p-0 text-sm text-gray-12 placeholder-gray-7 focus:ring-0"
|
||||
class="flex-1 bg-transparent border-none p-0 text-sm text-dls-text placeholder:text-dls-secondary focus:ring-0"
|
||||
/>
|
||||
<button class="rounded-xl bg-gray-12 px-3 py-1.5 text-xs font-semibold text-gray-1">Run</button>
|
||||
</div>
|
||||
@@ -436,7 +436,7 @@ export default function ProtoWorkspacesView() {
|
||||
<div class="max-w-2xl mx-auto flex items-center gap-2 rounded-2xl border border-gray-6/60 bg-gray-2/40 px-4 py-3">
|
||||
<input
|
||||
placeholder="Describe a task..."
|
||||
class="flex-1 bg-transparent text-sm text-gray-12 placeholder-gray-7 focus:outline-none"
|
||||
class="flex-1 bg-transparent text-sm text-dls-text placeholder:text-dls-secondary focus:outline-none"
|
||||
/>
|
||||
<button class="rounded-xl bg-gray-12 px-3 py-1.5 text-xs font-semibold text-gray-1">Send</button>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ const statusTone = (status?: string | null) => {
|
||||
if (status === "success") return "border-emerald-7/50 bg-emerald-4/20 text-emerald-11";
|
||||
if (status === "failed") return "border-red-7/50 bg-red-4/20 text-red-11";
|
||||
if (status === "running") return "border-amber-7/50 bg-amber-4/20 text-amber-11";
|
||||
return "border-gray-6/60 bg-gray-2/40 text-gray-11";
|
||||
return "border-dls-border bg-dls-hover text-dls-secondary";
|
||||
};
|
||||
|
||||
export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
|
||||
@@ -120,87 +120,87 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<section class="space-y-8">
|
||||
<div class="bg-gradient-to-r from-gray-2 to-gray-4 rounded-3xl p-1">
|
||||
<div class="bg-gray-1 rounded-[22px] p-6 md:p-8 space-y-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-12">Scheduled Tasks</h3>
|
||||
<p class="text-sm text-gray-10 mt-1">
|
||||
{sourceDescription()}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => props.refreshJobs({ force: true })}
|
||||
disabled={!supported() || props.busy}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
{props.busy ? "Refreshing" : "Refresh"}
|
||||
</Button>
|
||||
</div>
|
||||
<section class="space-y-10">
|
||||
<div class="flex flex-wrap items-center justify-end gap-4 border-b border-dls-border pb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.refreshJobs({ force: true })}
|
||||
disabled={!supported() || props.busy}
|
||||
class={`flex items-center gap-1.5 text-xs font-medium transition-colors ${
|
||||
!supported() || props.busy
|
||||
? "text-dls-secondary"
|
||||
: "text-dls-secondary hover:text-dls-text"
|
||||
}`}
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
{props.busy ? "Refreshing" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-2xl border border-gray-6/60 bg-gray-2/40 p-4">
|
||||
<div class="text-[11px] uppercase tracking-wider text-gray-10">
|
||||
Scheduled Jobs
|
||||
</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-gray-12">
|
||||
{props.jobs.length}
|
||||
</div>
|
||||
<div class="text-xs text-gray-9 mt-1">Active schedules</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-gray-6/60 bg-gray-2/40 p-4">
|
||||
<div class="text-[11px] uppercase tracking-wider text-gray-10">
|
||||
Last Sync
|
||||
</div>
|
||||
<div class="mt-2 text-lg font-semibold text-gray-12">
|
||||
{supported() ? lastUpdatedLabel() : "Unavailable"}
|
||||
</div>
|
||||
<div class="text-xs text-gray-9 mt-1">{sourceLabel()}</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-gray-6/60 bg-gray-2/40 p-4">
|
||||
<div class="text-[11px] uppercase tracking-wider text-gray-10">Scheduler</div>
|
||||
<div class="mt-2 text-lg font-semibold text-gray-12">
|
||||
{supported() ? schedulerLabel() : "Unavailable"}
|
||||
</div>
|
||||
<div class="text-xs text-gray-9 mt-1">
|
||||
{supported() ? schedulerHint() : schedulerUnavailableHint()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-3xl font-bold text-dls-text">Automations</h2>
|
||||
<p class="text-sm text-dls-secondary">{sourceDescription()}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<div class="rounded-xl border border-dls-border bg-dls-surface p-4">
|
||||
<div class="text-[11px] font-bold uppercase tracking-widest text-dls-secondary">
|
||||
Automations
|
||||
</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-dls-text">
|
||||
{props.jobs.length}
|
||||
</div>
|
||||
<div class="text-xs text-dls-secondary mt-1">Active automations</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-dls-border bg-dls-surface p-4">
|
||||
<div class="text-[11px] font-bold uppercase tracking-widest text-dls-secondary">
|
||||
Last Sync
|
||||
</div>
|
||||
<div class="mt-2 text-lg font-semibold text-dls-text">
|
||||
{supported() ? lastUpdatedLabel() : "Unavailable"}
|
||||
</div>
|
||||
<div class="text-xs text-dls-secondary mt-1">{sourceLabel()}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-dls-border bg-dls-surface p-4">
|
||||
<div class="text-[11px] font-bold uppercase tracking-widest text-dls-secondary">Scheduler</div>
|
||||
<div class="mt-2 text-lg font-semibold text-dls-text">
|
||||
{supported() ? schedulerLabel() : "Unavailable"}
|
||||
</div>
|
||||
<div class="text-xs text-dls-secondary mt-1">
|
||||
{supported() ? schedulerHint() : schedulerUnavailableHint()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={supportNote()}>
|
||||
<div class="rounded-2xl border border-gray-6/60 bg-gray-2/40 px-5 py-4 text-sm text-gray-10">
|
||||
<div class="rounded-xl border border-dls-border bg-dls-hover px-5 py-4 text-sm text-dls-secondary">
|
||||
{supportNote()}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.status}>
|
||||
<div class="rounded-2xl border border-red-7/40 bg-red-4/10 px-5 py-4 text-sm text-red-11">
|
||||
<div class="rounded-xl border border-red-100 bg-red-50/40 px-5 py-4 text-sm text-red-600">
|
||||
{props.status}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={deleteError()}>
|
||||
<div class="rounded-2xl border border-red-7/40 bg-red-4/10 px-5 py-4 text-sm text-red-11">
|
||||
<div class="rounded-xl border border-red-100 bg-red-50/40 px-5 py-4 text-sm text-red-600">
|
||||
{deleteError()}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="rounded-2xl border border-gray-6/60 bg-gray-1/40 overflow-hidden">
|
||||
<div class="rounded-xl border border-dls-border bg-dls-surface overflow-hidden">
|
||||
<Show
|
||||
when={props.jobs.length}
|
||||
fallback={
|
||||
<div class="px-6 py-10 text-sm text-gray-10">
|
||||
No scheduled tasks yet. Add the opencode-scheduler plugin and create a job to
|
||||
see it here.
|
||||
<div class="px-6 py-10 text-sm text-dls-secondary">
|
||||
No automations yet. Add the opencode-scheduler plugin and create a job to see it
|
||||
here.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="divide-y divide-gray-6/60">
|
||||
<div class="divide-y divide-dls-border">
|
||||
<For each={props.jobs}>
|
||||
{(job) => {
|
||||
const summary = () => taskSummary(job);
|
||||
@@ -209,13 +209,13 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Calendar size={16} class="text-gray-11" />
|
||||
<div class="text-sm font-semibold text-gray-12">{job.name}</div>
|
||||
<Calendar size={16} class="text-dls-secondary" />
|
||||
<div class="text-sm font-semibold text-dls-text">{job.name}</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-10">
|
||||
Cron <span class="font-mono text-gray-12">{job.schedule}</span>
|
||||
<div class="text-xs text-dls-secondary">
|
||||
Cron <span class="font-mono text-dls-text">{job.schedule}</span>
|
||||
</div>
|
||||
<div class="text-[11px] text-gray-7 font-mono">{job.slug}</div>
|
||||
<div class="text-[11px] text-dls-secondary font-mono">{job.slug}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
@@ -238,43 +238,43 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div class="rounded-xl border border-gray-6/60 bg-gray-2/30 p-4 space-y-2">
|
||||
<div class="text-[10px] uppercase tracking-wide text-gray-10">
|
||||
<div class="rounded-xl border border-dls-border bg-dls-hover p-4 space-y-2">
|
||||
<div class="text-[10px] uppercase tracking-wide text-dls-secondary">
|
||||
{summary().label}
|
||||
</div>
|
||||
<div
|
||||
class={`text-sm text-gray-12 break-words ${
|
||||
class={`text-sm text-dls-text break-words ${
|
||||
summary().mono ? "font-mono" : ""
|
||||
}`}
|
||||
>
|
||||
{summary().value}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-6/60 bg-gray-2/30 p-4 space-y-2">
|
||||
<div class="text-[10px] uppercase tracking-wide text-gray-10">Run context</div>
|
||||
<div class="space-y-2 text-xs text-gray-10">
|
||||
<div class="rounded-xl border border-dls-border bg-dls-hover p-4 space-y-2">
|
||||
<div class="text-[10px] uppercase tracking-wide text-dls-secondary">Run context</div>
|
||||
<div class="space-y-2 text-xs text-dls-secondary">
|
||||
<div class="flex items-center gap-2">
|
||||
<FolderOpen size={14} class="text-gray-9" />
|
||||
<span class="font-mono text-gray-12 break-all">
|
||||
<FolderOpen size={14} class="text-dls-secondary" />
|
||||
<span class="font-mono text-dls-text break-all">
|
||||
{job.workdir ?? "Default"}
|
||||
</span>
|
||||
</div>
|
||||
<Show when={job.run?.attachUrl ?? job.attachUrl}>
|
||||
<div class="flex items-center gap-2">
|
||||
<Terminal size={14} class="text-gray-9" />
|
||||
<span class="font-mono text-gray-12 break-all">
|
||||
<Terminal size={14} class="text-dls-secondary" />
|
||||
<span class="font-mono text-dls-text break-all">
|
||||
{job.run?.attachUrl ?? job.attachUrl}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={job.source}>
|
||||
<div class="text-[11px] text-gray-9">Source: {job.source}</div>
|
||||
<div class="text-[11px] text-dls-secondary">Source: {job.source}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-4 text-xs text-gray-10">
|
||||
<div class="flex flex-wrap gap-4 text-xs text-dls-secondary">
|
||||
<div class="flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
Last run {toRelative(job.lastRunAt)}
|
||||
@@ -296,18 +296,18 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
|
||||
</div>
|
||||
|
||||
<Show when={deleteTarget()}>
|
||||
<div class="fixed inset-0 z-50 bg-gray-1/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="bg-gray-2 border border-gray-6/70 w-full max-w-md rounded-2xl shadow-2xl overflow-hidden">
|
||||
<div class="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="bg-dls-surface border border-dls-border w-full max-w-md rounded-2xl shadow-2xl overflow-hidden">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-12">Delete scheduled task?</h3>
|
||||
<p class="text-sm text-gray-11 mt-1">
|
||||
<h3 class="text-lg font-semibold text-dls-text">Delete scheduled task?</h3>
|
||||
<p class="text-sm text-dls-secondary mt-1">
|
||||
{deleteDescription()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl bg-gray-1/20 border border-gray-6 p-3 text-xs text-gray-11 font-mono break-all">
|
||||
<div class="rounded-xl bg-dls-hover border border-dls-border p-3 text-xs text-dls-secondary font-mono break-all">
|
||||
{deleteTarget()?.name}
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
|
||||
@@ -21,7 +21,20 @@ import type {
|
||||
|
||||
import type { WorkspaceInfo } from "../lib/tauri";
|
||||
|
||||
import { ArrowRight, ChevronDown, HardDrive, Shield, Zap } from "lucide-solid";
|
||||
import {
|
||||
Box,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Edit2,
|
||||
HardDrive,
|
||||
History,
|
||||
Layout,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
Settings,
|
||||
Shield,
|
||||
Zap,
|
||||
} from "lucide-solid";
|
||||
|
||||
import Button from "../components/button";
|
||||
import RenameSessionModal from "../components/rename-session-modal";
|
||||
@@ -29,12 +42,11 @@ import ProviderAuthModal from "../components/provider-auth-modal";
|
||||
import StatusBar from "../components/status-bar";
|
||||
import type { OpenworkServerStatus } from "../lib/openwork-server";
|
||||
import { join } from "@tauri-apps/api/path";
|
||||
import { isTauriRuntime } from "../utils";
|
||||
import { formatRelativeTime, isTauriRuntime } from "../utils";
|
||||
|
||||
import MessageList from "../components/session/message-list";
|
||||
import Composer from "../components/session/composer";
|
||||
import SessionSidebar, { type SidebarSectionState } from "../components/session/sidebar";
|
||||
import ContextPanel from "../components/session/context-panel";
|
||||
import type { SidebarSectionState } from "../components/session/sidebar";
|
||||
import FlyoutItem from "../components/flyout-item";
|
||||
import QuestionModal from "../components/question-modal";
|
||||
|
||||
@@ -65,7 +77,13 @@ export type SessionViewProps = {
|
||||
createSessionAndOpen: () => void;
|
||||
sendPromptAsync: (draft: ComposerDraft) => Promise<void>;
|
||||
newTaskDisabled: boolean;
|
||||
sessions: Array<{ id: string; title: string; slug?: string | null; workspaceLabel?: string | null }>;
|
||||
sessions: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
slug?: string | null;
|
||||
workspaceLabel?: string | null;
|
||||
time?: { updated: number };
|
||||
}>;
|
||||
selectSession: (sessionId: string) => Promise<void> | void;
|
||||
messages: MessageWithParts[];
|
||||
todos: TodoItem[];
|
||||
@@ -130,35 +148,6 @@ export type SessionViewProps = {
|
||||
deleteSession: (sessionId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
type SessionSummary = { id: string; title: string; slug?: string | null };
|
||||
|
||||
const WORKSPACE_ORDER_KEY = "openwork.workspace-order.v1";
|
||||
|
||||
const readWorkspaceOrder = (): string[] => {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const raw = window.localStorage.getItem(WORKSPACE_ORDER_KEY);
|
||||
if (!raw) return [];
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter((entry): entry is string => typeof entry === "string");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const writeWorkspaceOrder = (order: string[]) => {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.localStorage.setItem(WORKSPACE_ORDER_KEY, JSON.stringify(order));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const arraysEqual = (a: string[], b: string[]) =>
|
||||
a.length === b.length && a.every((value, index) => value === b[index]);
|
||||
|
||||
export default function SessionView(props: SessionViewProps) {
|
||||
let messagesEndEl: HTMLDivElement | undefined;
|
||||
let chatContainerEl: HTMLDivElement | undefined;
|
||||
@@ -169,19 +158,35 @@ export default function SessionView(props: SessionViewProps) {
|
||||
const [renameModalOpen, setRenameModalOpen] = createSignal(false);
|
||||
const [renameTitle, setRenameTitle] = createSignal("");
|
||||
const [renameBusy, setRenameBusy] = createSignal(false);
|
||||
const [deleteBusy, setDeleteBusy] = createSignal(false);
|
||||
const [agentPickerOpen, setAgentPickerOpen] = createSignal(false);
|
||||
const [agentPickerBusy, setAgentPickerBusy] = createSignal(false);
|
||||
const [agentPickerReady, setAgentPickerReady] = createSignal(false);
|
||||
const [agentPickerError, setAgentPickerError] = createSignal<string | null>(null);
|
||||
const [workspaceOrder, setWorkspaceOrder] = createSignal<string[]>(readWorkspaceOrder());
|
||||
const [sessionsByWorkspaceId, setSessionsByWorkspaceId] = createSignal<Record<string, SessionSummary[]>>({});
|
||||
const [agentOptions, setAgentOptions] = createSignal<Agent[]>([]);
|
||||
const [autoScrollEnabled, setAutoScrollEnabled] = createSignal(false);
|
||||
const [scrollOnNextUpdate, setScrollOnNextUpdate] = createSignal(false);
|
||||
const [unreadCount, setUnreadCount] = createSignal(0);
|
||||
|
||||
const agentLabel = createMemo(() => props.selectedSessionAgent ?? "Default agent");
|
||||
const workspaceStatus = createMemo(() => {
|
||||
switch (props.openworkServerStatus) {
|
||||
case "connected":
|
||||
return { label: "Live", className: "bg-emerald-3 text-emerald-11" };
|
||||
case "limited":
|
||||
return { label: "Limited", className: "bg-amber-3 text-amber-11" };
|
||||
case "disconnected":
|
||||
default:
|
||||
return { label: "Offline", className: "bg-red-3 text-red-11" };
|
||||
}
|
||||
});
|
||||
const workspaceTypeLabel = createMemo(() =>
|
||||
props.activeWorkspaceDisplay.workspaceType === "remote" ? "Remote" : "Local"
|
||||
);
|
||||
const [sessionsExpanded, setSessionsExpanded] = createSignal(true);
|
||||
const [showAllSessions, setShowAllSessions] = createSignal(false);
|
||||
const visibleSessions = createMemo(() =>
|
||||
showAllSessions() ? props.sessions : props.sessions.slice(0, 5)
|
||||
);
|
||||
const attachmentsEnabled = createMemo(() => {
|
||||
if (props.activeWorkspaceDisplay.workspaceType !== "remote") return true;
|
||||
return props.openworkServerStatus === "connected";
|
||||
@@ -606,57 +611,6 @@ export default function SessionView(props: SessionViewProps) {
|
||||
return props.sessions.find((session) => session.id === id)?.title ?? "";
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const ids = props.workspaces.map((workspace) => workspace.id);
|
||||
const base = workspaceOrder().length ? workspaceOrder() : readWorkspaceOrder();
|
||||
const filtered = base.filter((id) => ids.includes(id));
|
||||
const missing = ids.filter((id) => !filtered.includes(id));
|
||||
const next = [...filtered, ...missing];
|
||||
if (!arraysEqual(base, next)) {
|
||||
writeWorkspaceOrder(next);
|
||||
}
|
||||
if (!arraysEqual(workspaceOrder(), next)) {
|
||||
setWorkspaceOrder(next);
|
||||
}
|
||||
});
|
||||
|
||||
const orderedWorkspaces = createMemo(() => {
|
||||
const byId = new Map(props.workspaces.map((workspace) => [workspace.id, workspace]));
|
||||
const order = workspaceOrder();
|
||||
const list = order.map((id) => byId.get(id)).filter((workspace): workspace is WorkspaceInfo => Boolean(workspace));
|
||||
return list.length ? list : props.workspaces;
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const workspaceId = props.activeWorkspaceId;
|
||||
if (!workspaceId) return;
|
||||
const list = props.sessions.map((session) => ({
|
||||
id: session.id,
|
||||
title: session.title,
|
||||
slug: session.slug,
|
||||
}));
|
||||
setSessionsByWorkspaceId((prev) => ({
|
||||
...prev,
|
||||
[workspaceId]: list,
|
||||
}));
|
||||
});
|
||||
|
||||
const sessionWorkspaceGroups = createMemo(() => {
|
||||
const byWorkspace = sessionsByWorkspaceId();
|
||||
return orderedWorkspaces().map((workspace) => ({
|
||||
workspace,
|
||||
sessions: byWorkspace[workspace.id] ?? [],
|
||||
}));
|
||||
});
|
||||
|
||||
const pickFallbackSessionId = (targetId: string) => {
|
||||
const list = props.sessions.map((session) => session.id);
|
||||
if (list.length <= 1) return null;
|
||||
const index = list.indexOf(targetId);
|
||||
if (index === -1) return list[0] ?? null;
|
||||
return list[index + 1] ?? list[index - 1] ?? null;
|
||||
};
|
||||
|
||||
const renameCanSave = createMemo(() => {
|
||||
if (renameBusy()) return false;
|
||||
const next = renameTitle().trim();
|
||||
@@ -695,38 +649,6 @@ export default function SessionView(props: SessionViewProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSession = async (sessionId: string) => {
|
||||
if (deleteBusy()) return;
|
||||
const targetId = sessionId?.trim();
|
||||
if (!targetId) {
|
||||
setToastMessage("No session selected");
|
||||
return;
|
||||
}
|
||||
const targetTitle = props.sessions.find((session) => session.id === targetId)?.title ?? "this session";
|
||||
const confirmed = window.confirm(`Delete session "${targetTitle}"?`);
|
||||
if (!confirmed) return;
|
||||
const fallbackId = pickFallbackSessionId(targetId);
|
||||
setDeleteBusy(true);
|
||||
try {
|
||||
await props.deleteSession(targetId);
|
||||
setToastMessage("Session deleted");
|
||||
if (props.selectedSessionId !== targetId) return;
|
||||
if (fallbackId) {
|
||||
await Promise.resolve(props.selectSession(fallbackId));
|
||||
props.setView("session", fallbackId);
|
||||
props.setTab("sessions");
|
||||
return;
|
||||
}
|
||||
props.setView("dashboard");
|
||||
props.setTab("sessions");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : props.safeStringify(error);
|
||||
setToastMessage(message);
|
||||
} finally {
|
||||
setDeleteBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const requireSessionId = () => {
|
||||
const sessionId = props.selectedSessionId;
|
||||
if (!sessionId) {
|
||||
@@ -801,6 +723,12 @@ export default function SessionView(props: SessionViewProps) {
|
||||
props.setPrompt(draft.text);
|
||||
};
|
||||
|
||||
const openSessionFromList = (sessionId: string) => {
|
||||
if (!sessionId) return;
|
||||
void props.selectSession(sessionId);
|
||||
props.setView("session", sessionId);
|
||||
};
|
||||
|
||||
const openSettings = (tab: SettingsTab = "general") => {
|
||||
props.setSettingsTab(tab);
|
||||
props.setTab("settings");
|
||||
@@ -812,37 +740,6 @@ export default function SessionView(props: SessionViewProps) {
|
||||
props.setView("dashboard");
|
||||
};
|
||||
|
||||
const handleSelectSession = async (workspaceId: string, sessionId: string) => {
|
||||
const targetWorkspaceId = workspaceId?.trim();
|
||||
if (!targetWorkspaceId || !sessionId) return;
|
||||
if (props.connectingWorkspaceId && props.connectingWorkspaceId !== targetWorkspaceId) return;
|
||||
if (targetWorkspaceId !== props.activeWorkspaceId) {
|
||||
const result = await props.activateWorkspace(targetWorkspaceId);
|
||||
if (result === false) return;
|
||||
}
|
||||
await props.selectSession(sessionId);
|
||||
props.setView("session", sessionId);
|
||||
props.setTab("sessions");
|
||||
};
|
||||
|
||||
const handleReorderWorkspace = (fromId: string, toId: string | null) => {
|
||||
setWorkspaceOrder((current) => {
|
||||
const base = current.length ? current : props.workspaces.map((workspace) => workspace.id);
|
||||
if (!base.includes(fromId)) return current;
|
||||
const next = base.filter((id) => id !== fromId);
|
||||
if (toId) {
|
||||
const index = next.indexOf(toId);
|
||||
if (index === -1) return current;
|
||||
next.splice(index, 0, fromId);
|
||||
} else {
|
||||
next.push(fromId);
|
||||
}
|
||||
if (arraysEqual(base, next)) return current;
|
||||
writeWorkspaceOrder(next);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const openProviderAuth = () => {
|
||||
void props.openProviderAuthModal().catch((error) => {
|
||||
const message = error instanceof Error ? error.message : "Connect failed";
|
||||
@@ -857,33 +754,225 @@ export default function SessionView(props: SessionViewProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="h-screen flex flex-col bg-gray-1 text-gray-12 relative pb-16 md:pb-12">
|
||||
<header class="h-16 border-b border-gray-6 flex items-center justify-between px-6 bg-gray-1/80 backdrop-blur-md z-10 sticky top-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="!p-2 rounded-full md:!px-3 md:!py-2 md:rounded-xl"
|
||||
<div class="flex h-screen w-full bg-dls-surface text-dls-text font-sans overflow-hidden">
|
||||
<aside class="w-64 hidden md:flex flex-col bg-dls-sidebar border-r border-dls-border p-4">
|
||||
<div class="space-y-0.5 mb-6 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
class={`w-full h-10 flex items-center gap-3 px-3 rounded-lg text-sm font-medium transition-colors ${
|
||||
props.tab === "scheduled"
|
||||
? "bg-dls-active text-dls-text"
|
||||
: "text-dls-secondary hover:text-dls-text hover:bg-dls-hover"
|
||||
}`}
|
||||
onClick={() => {
|
||||
props.setTab("sessions");
|
||||
props.setTab("scheduled");
|
||||
props.setView("dashboard");
|
||||
}}
|
||||
title="Back to dashboard"
|
||||
>
|
||||
<ArrowRight class="rotate-180 w-5 h-5" />
|
||||
<span class="hidden md:inline text-xs">Back</span>
|
||||
</Button>
|
||||
<div class="px-3 py-1.5 rounded-xl bg-gray-2 text-xs text-gray-11 font-medium">
|
||||
{props.activeWorkspaceDisplay.name}
|
||||
</div>
|
||||
<Show when={props.developerMode}>
|
||||
<span class="text-xs text-gray-7">{props.headerStatus}</span>
|
||||
</Show>
|
||||
<Show when={props.busyHint}>
|
||||
<span class="text-xs text-gray-10">· {props.busyHint}</span>
|
||||
</Show>
|
||||
|
||||
<History size={18} />
|
||||
Automations
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`w-full h-10 flex items-center gap-3 px-3 rounded-lg text-sm font-medium transition-colors ${
|
||||
props.tab === "skills"
|
||||
? "bg-dls-active text-dls-text"
|
||||
: "text-dls-secondary hover:text-dls-text hover:bg-dls-hover"
|
||||
}`}
|
||||
onClick={() => {
|
||||
props.setTab("skills");
|
||||
props.setView("dashboard");
|
||||
}}
|
||||
>
|
||||
<Zap size={18} />
|
||||
Skills
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`w-full h-10 flex items-center gap-3 px-3 rounded-lg text-sm font-medium transition-colors ${
|
||||
props.tab === "mcp"
|
||||
? "bg-dls-active text-dls-text"
|
||||
: "text-dls-secondary hover:text-dls-text hover:bg-dls-hover"
|
||||
}`}
|
||||
onClick={() => {
|
||||
props.setTab("mcp");
|
||||
props.setView("dashboard");
|
||||
}}
|
||||
>
|
||||
<Box size={18} />
|
||||
Apps
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
<div class="flex items-center justify-between text-[11px] font-bold text-dls-secondary uppercase px-3 tracking-tight">
|
||||
<span>Workspace</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Workspace settings"
|
||||
onClick={() => openSettings("general")}
|
||||
class="text-dls-secondary hover:text-dls-text"
|
||||
>
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="rounded-lg border border-dls-border bg-dls-surface px-3 py-2">
|
||||
<div class="text-sm font-semibold text-dls-text truncate">
|
||||
{props.activeWorkspaceDisplay.name}
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-2 text-xs text-dls-secondary">
|
||||
<span>{workspaceTypeLabel()}</span>
|
||||
<span>{workspaceStatus().label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div class="flex items-center justify-between text-[11px] font-bold text-dls-secondary uppercase px-3 mb-3 tracking-tight">
|
||||
<span>Sessions</span>
|
||||
<div class="flex gap-2 text-dls-secondary">
|
||||
<button type="button" class="hover:text-dls-text" aria-label="Session layout">
|
||||
<Layout size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="hover:text-dls-text"
|
||||
aria-label="New session"
|
||||
onClick={props.createSessionAndOpen}
|
||||
disabled={props.newTaskDisabled}
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSessionsExpanded((current) => !current)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
setSessionsExpanded((current) => !current);
|
||||
}}
|
||||
class="group flex items-center justify-between h-8 px-3 rounded-lg cursor-pointer text-dls-text transition-colors hover:bg-dls-hover"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<ChevronRight
|
||||
size={14}
|
||||
class={`text-dls-secondary transition-transform ${
|
||||
sessionsExpanded() ? "rotate-90" : ""
|
||||
}`}
|
||||
/>
|
||||
<span class="text-sm font-medium">{props.activeWorkspaceDisplay.name}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-secondary transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Workspace options"
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={sessionsExpanded()}>
|
||||
<div class="mt-0.5 space-y-0.5 border-l border-dls-border ml-4">
|
||||
<Show
|
||||
when={props.sessions.length > 0}
|
||||
fallback={
|
||||
<div class="px-3 py-2 text-xs text-dls-secondary">
|
||||
No sessions yet.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={visibleSessions()}>
|
||||
{(session) => (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
class="group flex items-center justify-between h-8 px-3 hover:bg-dls-hover rounded-lg cursor-pointer relative overflow-hidden ml-5 w-[calc(100%-1.25rem)]"
|
||||
onClick={() => openSessionFromList(session.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
openSessionFromList(session.id);
|
||||
}}
|
||||
>
|
||||
<span class="text-sm text-dls-text truncate mr-2 font-medium group-hover:pr-14 transition-all">
|
||||
{session.title}
|
||||
</span>
|
||||
<Show when={session.time?.updated}>
|
||||
<span class="text-xs text-dls-secondary whitespace-nowrap group-hover:opacity-0 transition-opacity">
|
||||
{formatRelativeTime(session.time?.updated ?? Date.now())}
|
||||
</span>
|
||||
</Show>
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-text transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Session options"
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-text transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Rename session"
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={props.sessions.length > 5}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllSessions((current) => !current)}
|
||||
class="px-3 py-1.5 text-xs text-dls-secondary hover:text-dls-text font-medium"
|
||||
>
|
||||
{showAllSessions() ? "Show less" : "Show more"}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-dls-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openSettings("general")}
|
||||
class="flex items-center gap-3 px-3 py-2 rounded-lg text-dls-secondary hover:bg-dls-hover transition-colors"
|
||||
>
|
||||
<Settings size={18} />
|
||||
<span class="text-sm font-medium">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 flex flex-col relative pb-16 md:pb-12 bg-dls-surface">
|
||||
<header class="h-14 border-b border-dls-border flex items-center justify-between px-6 bg-dls-surface z-10 sticky top-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-sm font-semibold text-dls-text">
|
||||
{selectedSessionTitle() || "New thread"}
|
||||
</h1>
|
||||
<Show when={props.developerMode}>
|
||||
<span class="text-xs text-dls-secondary">{props.headerStatus}</span>
|
||||
</Show>
|
||||
<Show when={props.busyHint}>
|
||||
<span class="text-xs text-dls-secondary">· {props.busyHint}</span>
|
||||
</Show>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Show when={props.error}>
|
||||
<div class="mx-auto max-w-5xl w-full px-6 md:px-10 pt-4">
|
||||
@@ -894,47 +983,19 @@ export default function SessionView(props: SessionViewProps) {
|
||||
</Show>
|
||||
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<aside class="hidden lg:flex w-72 border-r border-gray-6 bg-gray-1 flex-col">
|
||||
<SessionSidebar
|
||||
todos={props.todos}
|
||||
expandedSections={props.expandedSidebarSections}
|
||||
onToggleSection={(section) => {
|
||||
props.setExpandedSidebarSections((curr) => ({ ...curr, [section]: !curr[section] }));
|
||||
}}
|
||||
workspaceGroups={sessionWorkspaceGroups()}
|
||||
activeWorkspaceId={props.activeWorkspaceId}
|
||||
connectingWorkspaceId={props.connectingWorkspaceId}
|
||||
workspaceConnectionStateById={props.workspaceConnectionStateById}
|
||||
onSelectWorkspace={props.activateWorkspace}
|
||||
onCreateWorkspace={props.openCreateWorkspace}
|
||||
onCreateRemoteWorkspace={props.openCreateRemoteWorkspace}
|
||||
onImportWorkspace={props.importWorkspaceConfig}
|
||||
importingWorkspaceConfig={props.importingWorkspaceConfig}
|
||||
onEditWorkspace={props.editWorkspaceConnection}
|
||||
onTestWorkspaceConnection={props.testWorkspaceConnection}
|
||||
onForgetWorkspace={props.forgetWorkspace}
|
||||
onReorderWorkspace={handleReorderWorkspace}
|
||||
onSelectSession={handleSelectSession}
|
||||
selectedSessionId={props.selectedSessionId}
|
||||
sessionStatusById={props.sessionStatusById}
|
||||
onCreateSession={props.createSessionAndOpen}
|
||||
onDeleteSession={handleDeleteSession}
|
||||
newTaskDisabled={props.newTaskDisabled}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<div
|
||||
class="flex-1 overflow-y-auto pt-6 md:pt-10 scroll-smooth relative"
|
||||
class="flex-1 overflow-y-auto px-12 py-10 scroll-smooth relative bg-dls-surface"
|
||||
ref={(el) => (chatContainerEl = el)}
|
||||
>
|
||||
<div class="max-w-5xl mx-auto w-full">
|
||||
<Show when={props.messages.length === 0}>
|
||||
<div class="text-center py-16 px-6 space-y-6">
|
||||
<div class="w-16 h-16 bg-gray-2 rounded-3xl mx-auto flex items-center justify-center border border-gray-6">
|
||||
<Zap class="text-gray-7" />
|
||||
<div class="w-16 h-16 bg-dls-hover rounded-3xl mx-auto flex items-center justify-center border border-dls-border">
|
||||
<Zap class="text-dls-secondary" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-xl font-medium">What do you want to do?</h3>
|
||||
<p class="text-gray-10 text-sm max-w-sm mx-auto">
|
||||
<p class="text-dls-secondary text-sm max-w-sm mx-auto">
|
||||
Pick a starting point or just type below.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1054,30 +1115,9 @@ export default function SessionView(props: SessionViewProps) {
|
||||
</Show>
|
||||
|
||||
<div ref={(el) => (messagesEndEl = el)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="hidden lg:flex w-72 border-l border-gray-6 bg-gray-1 flex-col">
|
||||
<ContextPanel
|
||||
activePlugins={props.activePlugins}
|
||||
activePluginStatus={props.activePluginStatus}
|
||||
mcpServers={props.mcpServers}
|
||||
mcpStatuses={props.mcpStatuses}
|
||||
mcpStatus={props.mcpStatus}
|
||||
skills={props.skills}
|
||||
skillsStatus={props.skillsStatus}
|
||||
authorizedDirs={props.authorizedDirs}
|
||||
workingFiles={props.workingFiles}
|
||||
workspaceRoot={props.activeWorkspaceRoot}
|
||||
expandedSections={props.expandedSidebarSections}
|
||||
onToggleSection={(section) =>
|
||||
props.setExpandedSidebarSections((curr) => ({
|
||||
...curr,
|
||||
[section]: !curr[section],
|
||||
}))
|
||||
}
|
||||
onFileClick={handleWorkingFileClick}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<Composer
|
||||
@@ -1133,6 +1173,8 @@ export default function SessionView(props: SessionViewProps) {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
</main>
|
||||
|
||||
<div class="fixed bottom-0 left-0 right-0">
|
||||
<StatusBar
|
||||
clientConnected={props.clientConnected}
|
||||
|
||||
@@ -585,7 +585,7 @@ function OwpenbotSettings(props: {
|
||||
}
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="flex justify-center p-4 bg-white rounded-lg">
|
||||
<div class="flex justify-center p-4 bg-dls-surface rounded-lg">
|
||||
<img
|
||||
src={`data:image/png;base64,${qrCode()}`}
|
||||
alt="WhatsApp QR Code"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { For, Show, createMemo, createSignal } from "solid-js";
|
||||
import type { SkillCard } from "../types";
|
||||
|
||||
import Button from "../components/button";
|
||||
import { FolderOpen, Package, Upload } from "lucide-solid";
|
||||
import { Edit2, FolderOpen, Package, Plus, RefreshCw, Search, Sparkles, Upload } from "lucide-solid";
|
||||
import { currentLocale, t } from "../../i18n";
|
||||
|
||||
export type SkillsViewProps = {
|
||||
@@ -30,138 +30,170 @@ export default function SkillsView(props: SkillsViewProps) {
|
||||
|
||||
const [uninstallTarget, setUninstallTarget] = createSignal<SkillCard | null>(null);
|
||||
const uninstallOpen = createMemo(() => uninstallTarget() != null);
|
||||
const [searchQuery, setSearchQuery] = createSignal("");
|
||||
|
||||
const filteredSkills = createMemo(() => {
|
||||
const query = searchQuery().trim().toLowerCase();
|
||||
if (!query) return props.skills;
|
||||
return props.skills.filter((skill) => {
|
||||
const description = skill.description ?? "";
|
||||
return (
|
||||
skill.name.toLowerCase().includes(query) ||
|
||||
description.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const recommendedSkills = createMemo(() => [
|
||||
{
|
||||
id: "skill-creator",
|
||||
title: translate("skills.install_skill_creator"),
|
||||
description: translate("skills.install_skill_creator_hint"),
|
||||
icon: Sparkles,
|
||||
onClick: () => props.installSkillCreator(),
|
||||
disabled: props.busy || skillCreatorInstalled() || !props.canInstallSkillCreator,
|
||||
},
|
||||
{
|
||||
id: "import-local",
|
||||
title: translate("skills.import_local"),
|
||||
description: translate("skills.import_local_hint"),
|
||||
icon: Upload,
|
||||
onClick: props.importLocalSkill,
|
||||
disabled: props.busy || !props.canUseDesktopTools,
|
||||
},
|
||||
{
|
||||
id: "reveal-folder",
|
||||
title: translate("skills.reveal_folder"),
|
||||
description: translate("skills.reveal_folder_hint"),
|
||||
icon: FolderOpen,
|
||||
onClick: props.revealSkillsFolder,
|
||||
disabled: props.busy || !props.canUseDesktopTools,
|
||||
},
|
||||
]);
|
||||
|
||||
const handleNewSkill = () => {
|
||||
if (props.busy) return;
|
||||
if (props.canInstallSkillCreator && !skillCreatorInstalled()) {
|
||||
props.installSkillCreator();
|
||||
return;
|
||||
}
|
||||
if (props.canUseDesktopTools) {
|
||||
props.revealSkillsFolder();
|
||||
}
|
||||
};
|
||||
|
||||
const newSkillDisabled = createMemo(
|
||||
() =>
|
||||
props.busy ||
|
||||
(!props.canUseDesktopTools &&
|
||||
(!props.canInstallSkillCreator || skillCreatorInstalled()))
|
||||
);
|
||||
|
||||
return (
|
||||
<section class="space-y-8">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-12">{translate("skills.title")}</h3>
|
||||
<p class="text-sm text-gray-10 mt-1">{translate("skills.subtitle")}</p>
|
||||
</div>
|
||||
<Button variant="secondary" onClick={() => props.refreshSkills({ force: true })} disabled={props.busy}>
|
||||
<section class="space-y-10">
|
||||
<div class="flex flex-wrap items-center justify-end gap-4 border-b border-dls-border pb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.refreshSkills({ force: true })}
|
||||
disabled={props.busy}
|
||||
class={`flex items-center gap-1.5 text-xs font-medium transition-colors ${
|
||||
props.busy
|
||||
? "text-dls-secondary"
|
||||
: "text-dls-secondary hover:text-dls-text"
|
||||
}`}
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
{translate("skills.refresh")}
|
||||
</Button>
|
||||
</button>
|
||||
<div class="relative">
|
||||
<Search size={14} class="absolute left-3 top-1/2 -translate-y-1/2 text-dls-secondary" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery()}
|
||||
onInput={(event) => setSearchQuery(event.currentTarget.value)}
|
||||
placeholder="Search skills"
|
||||
class="bg-dls-hover border border-dls-border rounded-lg py-1.5 pl-9 pr-4 text-xs w-48 focus:w-64 focus:outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNewSkill}
|
||||
disabled={newSkillDisabled()}
|
||||
class={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-colors ${
|
||||
newSkillDisabled()
|
||||
? "bg-dls-active text-dls-secondary"
|
||||
: "bg-dls-text text-dls-surface hover:opacity-90"
|
||||
}`}
|
||||
>
|
||||
<Plus size={14} />
|
||||
New skill
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-gray-6/60 bg-gray-1/40 overflow-hidden">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 px-5 py-4 border-b border-gray-6/60 bg-gray-2/40">
|
||||
<div>
|
||||
<div class="text-xs font-semibold text-gray-11 uppercase tracking-wider">{translate("skills.add_title")}</div>
|
||||
<div class="text-sm text-gray-10 mt-2">{translate("skills.add_description")}</div>
|
||||
</div>
|
||||
<Show when={props.accessHint}>
|
||||
<div class="text-xs text-gray-10">{props.accessHint}</div>
|
||||
</Show>
|
||||
<Show
|
||||
when={
|
||||
!props.accessHint &&
|
||||
!props.canInstallSkillCreator &&
|
||||
!props.canUseDesktopTools
|
||||
}
|
||||
>
|
||||
<div class="text-xs text-gray-10">{translate("skills.host_mode_only")}</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-gray-6/60">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 px-5 py-4">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-12">{translate("skills.install_skill_creator")}</div>
|
||||
<div class="text-xs text-gray-10 mt-1">{translate("skills.install_skill_creator_hint")}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={skillCreatorInstalled() ? "outline" : "secondary"}
|
||||
onClick={() => {
|
||||
if (skillCreatorInstalled()) return;
|
||||
props.installSkillCreator();
|
||||
}}
|
||||
disabled={props.busy || skillCreatorInstalled() || !props.canInstallSkillCreator}
|
||||
>
|
||||
<Package size={16} />
|
||||
{skillCreatorInstalled() ? translate("skills.installed_label") : translate("skills.install")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 px-5 py-4">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-12">{translate("skills.import_local")}</div>
|
||||
<div class="text-xs text-gray-10 mt-1">{translate("skills.import_local_hint")}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={props.importLocalSkill}
|
||||
disabled={props.busy || !props.canUseDesktopTools}
|
||||
>
|
||||
<Upload size={16} />
|
||||
{translate("skills.import")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 px-5 py-4">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-12">{translate("skills.reveal_folder")}</div>
|
||||
<div class="text-xs text-gray-10 mt-1">{translate("skills.reveal_folder_hint")}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={props.revealSkillsFolder}
|
||||
disabled={props.busy || !props.canUseDesktopTools}
|
||||
>
|
||||
<FolderOpen size={16} />
|
||||
{translate("skills.reveal_button")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={props.skillsStatus}>
|
||||
<div class="border-t border-gray-6/60 px-5 py-3 text-xs text-gray-11 whitespace-pre-wrap break-words">
|
||||
{props.skillsStatus}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-3xl font-bold text-dls-text">{translate("skills.title")}</h2>
|
||||
<p class="text-sm text-dls-secondary">
|
||||
{translate("skills.subtitle")} {" "}
|
||||
<button type="button" class="text-dls-accent hover:underline">
|
||||
Learn more
|
||||
</button>
|
||||
</p>
|
||||
<Show when={props.accessHint}>
|
||||
<div class="text-xs text-dls-secondary">{props.accessHint}</div>
|
||||
</Show>
|
||||
<Show
|
||||
when={!props.accessHint && !props.canInstallSkillCreator && !props.canUseDesktopTools}
|
||||
>
|
||||
<div class="text-xs text-dls-secondary">{translate("skills.host_mode_only")}</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-12">{translate("skills.installed")}</div>
|
||||
<div class="text-xs text-gray-10 mt-1">{translate("skills.installed_description")}</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-10">{props.skills.length}</div>
|
||||
<Show when={props.skillsStatus}>
|
||||
<div class="rounded-xl border border-dls-border bg-dls-hover px-4 py-3 text-xs text-dls-secondary whitespace-pre-wrap break-words">
|
||||
{props.skillsStatus}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-[11px] font-bold text-dls-secondary uppercase tracking-widest">
|
||||
{translate("skills.installed")}
|
||||
</h3>
|
||||
<Show
|
||||
when={props.skills.length}
|
||||
when={filteredSkills().length}
|
||||
fallback={
|
||||
<div class="rounded-2xl border border-gray-6/60 bg-gray-1/40 px-5 py-6 text-sm text-zinc-500">
|
||||
<div class="rounded-xl border border-dls-border bg-dls-surface px-5 py-6 text-sm text-dls-secondary">
|
||||
{translate("skills.no_skills")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="rounded-2xl border border-gray-6/60 bg-gray-1/40 divide-y divide-gray-6/60">
|
||||
<For each={props.skills}>
|
||||
{(s) => (
|
||||
<div class="px-5 py-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Package size={16} class="text-gray-11" />
|
||||
<div class="font-medium text-gray-12">{s.name}</div>
|
||||
</div>
|
||||
<Show when={s.description}>
|
||||
<div class="text-sm text-gray-10">{s.description}</div>
|
||||
</Show>
|
||||
<div class="text-xs text-gray-7 font-mono">{s.path}</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<For each={filteredSkills()}>
|
||||
{(skill) => (
|
||||
<div class="bg-dls-surface border border-dls-border rounded-xl p-4 flex items-start justify-between group hover:border-dls-border transition-all">
|
||||
<div class="flex gap-4">
|
||||
<div class="w-10 h-10 rounded-lg flex items-center justify-center shadow-sm border border-dls-border bg-dls-surface">
|
||||
<Package size={20} class="text-dls-secondary" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-0.5">
|
||||
<h4 class="text-sm font-semibold text-dls-text">{skill.name}</h4>
|
||||
</div>
|
||||
<Show when={skill.description}>
|
||||
<p class="text-xs text-dls-secondary line-clamp-1">
|
||||
{skill.description}
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
<Button
|
||||
variant="danger"
|
||||
class="!px-3 !py-2 text-xs"
|
||||
onClick={() => setUninstallTarget(s)}
|
||||
disabled={props.busy || !props.canUseDesktopTools}
|
||||
title={translate("skills.uninstall")}
|
||||
>
|
||||
{translate("skills.uninstall")}
|
||||
</Button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1.5 text-dls-secondary hover:text-dls-text hover:bg-dls-hover rounded-md transition-colors"
|
||||
onClick={() => setUninstallTarget(skill)}
|
||||
disabled={props.busy || !props.canUseDesktopTools}
|
||||
title={translate("skills.uninstall")}
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
@@ -169,20 +201,59 @@ export default function SkillsView(props: SkillsViewProps) {
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-[11px] font-bold text-dls-secondary uppercase tracking-widest">Recommended</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<For each={recommendedSkills()}>
|
||||
{(item) => (
|
||||
<div class="bg-dls-surface border border-dls-border rounded-xl p-4 flex items-start justify-between group hover:border-dls-border transition-all">
|
||||
<div class="flex gap-4">
|
||||
<div class="w-10 h-10 rounded-lg flex items-center justify-center shadow-sm border border-dls-border bg-dls-hover">
|
||||
<item.icon size={20} class="text-dls-secondary" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-0.5">
|
||||
<h4 class="text-sm font-semibold text-dls-text">{item.title}</h4>
|
||||
</div>
|
||||
<p class="text-xs text-dls-secondary line-clamp-1">{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class={`p-1.5 rounded-md transition-colors ${
|
||||
item.disabled
|
||||
? "text-dls-secondary opacity-40"
|
||||
: "text-dls-secondary hover:text-dls-text hover:bg-dls-hover"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (item.disabled) return;
|
||||
item.onClick();
|
||||
}}
|
||||
disabled={item.disabled}
|
||||
title={item.title}
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={uninstallOpen()}>
|
||||
<div class="fixed inset-0 z-50 bg-gray-1/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="bg-gray-2 border border-gray-6/70 w-full max-w-md rounded-2xl shadow-2xl overflow-hidden">
|
||||
<div class="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="bg-dls-surface border border-dls-border w-full max-w-md rounded-2xl shadow-2xl overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-12">{translate("skills.uninstall_title")}</h3>
|
||||
<p class="text-sm text-gray-11 mt-1">
|
||||
<h3 class="text-lg font-semibold text-dls-text">{translate("skills.uninstall_title")}</h3>
|
||||
<p class="text-sm text-dls-secondary mt-1">
|
||||
{translate("skills.uninstall_warning").replace("{name}", uninstallTarget()?.name ?? "")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 rounded-xl bg-gray-1/20 border border-gray-6 p-3 text-xs text-gray-11 font-mono break-all">
|
||||
<div class="mt-4 rounded-xl bg-dls-hover border border-dls-border p-3 text-xs text-dls-secondary font-mono break-all">
|
||||
{uninstallTarget()?.path}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -99,8 +99,6 @@ export type EngineRuntime = "direct" | "openwrk";
|
||||
export type OnboardingStep = "welcome" | "local" | "server" | "connecting";
|
||||
|
||||
export type DashboardTab =
|
||||
| "home"
|
||||
| "sessions"
|
||||
| "scheduled"
|
||||
| "skills"
|
||||
| "plugins"
|
||||
|
||||
@@ -9,6 +9,18 @@ safelist: [
|
||||
// OVERRIDE the base theme completely instead of extending it
|
||||
colors: {
|
||||
...radixColors,
|
||||
dls: {
|
||||
surface: "var(--dls-surface)",
|
||||
sidebar: "var(--dls-sidebar)",
|
||||
border: "var(--dls-border)",
|
||||
accent: "var(--dls-accent)",
|
||||
text: "var(--dls-text-primary)",
|
||||
secondary: "var(--dls-text-secondary)",
|
||||
hover: "var(--dls-hover)",
|
||||
active: "var(--dls-active)",
|
||||
},
|
||||
white: "#ffffff",
|
||||
black: "#000000",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
import os from "node:os";
|
||||
import { defineConfig } from "vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import solid from "vite-plugin-solid";
|
||||
|
||||
const portValue = Number.parseInt(process.env.PORT ?? "", 10);
|
||||
const devPort = Number.isFinite(portValue) && portValue > 0 ? portValue : 5173;
|
||||
const allowedHosts = new Set<string>();
|
||||
const envAllowedHosts = process.env.VITE_ALLOWED_HOSTS ?? "";
|
||||
|
||||
const addHost = (value?: string | null) => {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) return;
|
||||
allowedHosts.add(trimmed);
|
||||
};
|
||||
|
||||
envAllowedHosts.split(",").forEach(addHost);
|
||||
addHost(process.env.OPENWORK_PUBLIC_HOST ?? null);
|
||||
const hostname = os.hostname();
|
||||
addHost(hostname);
|
||||
const shortHostname = hostname.split(".")[0];
|
||||
if (shortHostname && shortHostname !== hostname) {
|
||||
addHost(shortHostname);
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), solid()],
|
||||
server: {
|
||||
port: devPort,
|
||||
strictPort: true,
|
||||
...(allowedHosts.size > 0 ? { allowedHosts: Array.from(allowedHosts) } : {}),
|
||||
},
|
||||
build: {
|
||||
target: "esnext",
|
||||
|
||||
Reference in New Issue
Block a user