diff --git a/desktop/src/renderer/src/App.tsx b/desktop/src/renderer/src/App.tsx index 7ff7af9a..7ea2f076 100644 --- a/desktop/src/renderer/src/App.tsx +++ b/desktop/src/renderer/src/App.tsx @@ -1120,6 +1120,8 @@ export function App() { void restartInstalledOctopal()} onUpdateOctopal={() => void updateInstalledOctopal()} onUpdateDesktopApp={() => void updateDesktopApp()} + onLanguageChange={updateLanguage} + onThemeChange={setTheme} /> ) : null} diff --git a/desktop/src/renderer/src/components/Button.tsx b/desktop/src/renderer/src/components/Button.tsx index efe8c3e4..8a2ae292 100644 --- a/desktop/src/renderer/src/components/Button.tsx +++ b/desktop/src/renderer/src/components/Button.tsx @@ -1,21 +1 @@ -import { type ButtonHTMLAttributes, type ReactNode } from "react"; - -import { cn } from "../lib/cn"; - -type ButtonVariant = "primary" | "secondary" | "ghost" | "success" | "danger"; - -export function Button({ - children, - className, - variant = "primary", - ...props -}: ButtonHTMLAttributes & { - children: ReactNode; - variant?: ButtonVariant; -}) { - return ( - - ); -} +export { Button } from "./ui/button"; diff --git a/desktop/src/renderer/src/components/ChatView.tsx b/desktop/src/renderer/src/components/ChatView.tsx index f162098f..f7477edf 100644 --- a/desktop/src/renderer/src/components/ChatView.tsx +++ b/desktop/src/renderer/src/components/ChatView.tsx @@ -41,6 +41,9 @@ const initialStatus: DesktopChatConnectionStatus = { detail: "Chat is idle.", }; +const ACTIVITY_TIMEOUT_MS = 30_000; +const THINKING_STATUS_TEXT = "Octo is thinking"; + function stringValue(value: unknown, fallback = ""): string { return typeof value === "string" && value.trim() ? value.trim() : fallback; } @@ -154,42 +157,58 @@ function workerSnapshotText(worker: Record): string { const name = workerSnapshotName(worker); const status = stringValue(worker.status, "unknown").toLowerCase(); if (status === "running") { - return `${name} worker is running.`; + return `${name} worker is running`; } if (status === "waiting_for_children") { - return `${name} worker is waiting for child workers.`; + return `${name} worker is waiting for child workers`; } if (status === "awaiting_instruction") { - return `${name} worker is awaiting instruction.`; + return `${name} worker is awaiting instruction`; } if (["started", "completed", "failed", "stopped"].includes(status)) { - return `${name} worker ${status}.`; + return `${name} worker ${status}`; } - return `${name} worker status: ${status}.`; + return `${name} worker status: ${status}`; +} + +function activityStatusText(text: string): string { + return text.trim().replace(/\.+$/u, ""); } -function chatItemFromWorkerSnapshot( +function activityTextFromWorkerSnapshot( worker: Record, - index: number, -): ChatItem { - const createdAt = - stringValue(worker.updated_at) || - stringValue(worker.created_at) || - new Date().toISOString(); - const workerId = stringValue(worker.id, `worker-${index}`); - const status = stringValue(worker.status, "unknown"); - return { - id: `worker-${workerId}-${status}-${createdAt}`, - kind: "event", - type: "worker_snapshot", - role: "system", - direction: "event", - channel: "runtime", - text: workerSnapshotText(worker), - createdAt, - meta: worker, - technical: true, - }; +): string { + return workerSnapshotText(worker); +} + +function activityTextFromEvent(event: DesktopChatEvent): string { + const type = stringValue(event.type); + if (type === "progress") { + return eventText(event); + } + if (type === "worker_event") { + const payload = recordValue(event.payload); + return ( + stringValue(event.text) || + stringValue(event.message) || + stringValue(payload.message) || + stringValue(payload.summary) || + stringValue(event.event) + ); + } + if (type === "workers_snapshot") { + const activeWorker = recordArray(event.workers).find((worker) => { + const status = stringValue(worker.status).toLowerCase(); + return [ + "running", + "started", + "waiting_for_children", + "awaiting_instruction", + ].includes(status); + }); + return activeWorker ? activityTextFromWorkerSnapshot(activeWorker) : ""; + } + return ""; } function chatItemFromEvent( @@ -235,7 +254,11 @@ function chatItemFromEvent( }; } - if (["workers_snapshot", "pong", "typing", "worker_event"].includes(type)) { + if ( + ["workers_snapshot", "pong", "typing", "worker_event", "progress"].includes( + type, + ) + ) { return null; } @@ -243,7 +266,7 @@ function chatItemFromEvent( return null; } - if (["progress", "file", "warning", "error"].includes(type)) { + if (["file", "warning", "error"].includes(type)) { return { id: baseId, kind: "event", @@ -276,9 +299,7 @@ function chatItemsFromEvent( .filter((item): item is ChatItem => item !== null); } if (type === "workers_snapshot") { - return recordArray(event.workers).map((worker, workerIndex) => - chatItemFromWorkerSnapshot(worker, index + workerIndex), - ); + return []; } const item = chatItemFromEvent(event, index); return item ? [item] : []; @@ -377,8 +398,10 @@ export function ChatView({ active, installDir }: ChatViewProps) { const [sendError, setSendError] = useState(""); const [sending, setSending] = useState(false); const [thinking, setThinking] = useState(false); + const [activityText, setActivityText] = useState(""); const scrollRef = useRef(null); const eventCount = useRef(0); + const activityTimeoutRef = useRef(null); const connected = status.state === "connected"; const canSend = @@ -412,6 +435,37 @@ export function ChatView({ active, installDir }: ChatViewProps) { } }, [installDir]); + const clearActivityTimeout = useCallback(() => { + if (activityTimeoutRef.current !== null) { + window.clearTimeout(activityTimeoutRef.current); + activityTimeoutRef.current = null; + } + }, []); + + const clearActivity = useCallback(() => { + clearActivityTimeout(); + setThinking(false); + setActivityText(""); + }, [clearActivityTimeout]); + + const scheduleActivityTimeout = useCallback(() => { + clearActivityTimeout(); + activityTimeoutRef.current = window.setTimeout(() => { + setThinking(false); + setActivityText(""); + activityTimeoutRef.current = null; + }, ACTIVITY_TIMEOUT_MS); + }, [clearActivityTimeout]); + + const showActivity = useCallback( + (text: string) => { + setActivityText(activityStatusText(text) || THINKING_STATUS_TEXT); + setThinking(true); + scheduleActivityTimeout(); + }, + [scheduleActivityTimeout], + ); + useEffect(() => { if (!window.octopalDesktop || !installDir) { return; @@ -420,7 +474,13 @@ export function ChatView({ active, installDir }: ChatViewProps) { const unsubscribeStatus = window.octopalDesktop.onChatStatus(setStatus); const unsubscribeEvent = window.octopalDesktop.onChatEvent((event) => { if (stringValue(event.type) === "typing") { - setThinking(Boolean(event.active)); + if (event.active) { + setActivityText((current) => current || THINKING_STATUS_TEXT); + setThinking(true); + scheduleActivityTimeout(); + } else { + setThinking(false); + } return; } @@ -457,12 +517,20 @@ export function ChatView({ active, installDir }: ChatViewProps) { } eventCount.current += 1; + const activity = activityTextFromEvent(event); + if (activity) { + showActivity(activity); + } const nextItems = chatItemsFromEvent(event, eventCount.current); if (nextItems.length === 0) { return; } - if (nextItems.some((item) => item.role === "assistant" || item.type === "error")) { - setThinking(false); + if ( + nextItems.some( + (item) => item.role === "assistant" || item.type === "error", + ) + ) { + clearActivity(); } setItems((current) => { if ( @@ -487,8 +555,16 @@ export function ChatView({ active, installDir }: ChatViewProps) { return () => { unsubscribeStatus(); unsubscribeEvent(); + clearActivityTimeout(); }; - }, [connect, installDir]); + }, [ + clearActivity, + clearActivityTimeout, + connect, + installDir, + scheduleActivityTimeout, + showActivity, + ]); useEffect(() => { if (!active) { @@ -498,7 +574,7 @@ export function ChatView({ active, installDir }: ChatViewProps) { top: scrollRef.current.scrollHeight, behavior: "smooth", }); - }, [active, sortedItems.length, thinking]); + }, [active, sortedItems.length, thinking, activityText]); function appendAttachments(next: DesktopChatAttachment[]): void { setAttachments((current) => { @@ -580,7 +656,7 @@ export function ChatView({ active, installDir }: ChatViewProps) { ); setDraft(""); setAttachments([]); - setThinking(true); + showActivity(THINKING_STATUS_TEXT); } catch (error) { setThinking(false); setSendError( @@ -626,7 +702,7 @@ export function ChatView({ active, installDir }: ChatViewProps) { aria-label="Desktop chat" >
- {sortedItems.length === 0 ? ( + {sortedItems.length === 0 && !activityText && !thinking ? (

No chat events yet

Waiting for live activity.

@@ -698,18 +774,12 @@ export function ChatView({ active, installDir }: ChatViewProps) { ) : null} ))} - {thinking ? ( -
-
- Octo - thinking -
-
- - - -
-
+ {thinking || activityText ? ( +
+ + {activityText || THINKING_STATUS_TEXT} + +
) : null}
diff --git a/desktop/src/renderer/src/components/DashboardScreen.tsx b/desktop/src/renderer/src/components/DashboardScreen.tsx index 76671904..8c466226 100644 --- a/desktop/src/renderer/src/components/DashboardScreen.tsx +++ b/desktop/src/renderer/src/components/DashboardScreen.tsx @@ -3,6 +3,7 @@ import { AlertTriangle, CalendarDays, CheckCircle2, + ChevronDown, Clock, Download, ExternalLink, @@ -11,11 +12,16 @@ import { Folder, GitBranch, Github, + Globe2, Info, KeyRound, + LayoutDashboard, ListChecks, Mail, MessageCircle, + Moon, + PanelLeftClose, + PanelLeftOpen, Pencil, Play, Plus, @@ -25,6 +31,7 @@ import { RotateCw, Settings2, Square, + Sun, Trash2, Unplug, Wrench, @@ -32,12 +39,12 @@ import { } from "lucide-react"; import { motion } from "framer-motion"; import { useCallback, useEffect, useMemo, useState } from "react"; -import type { ReactNode } from "react"; import octoIdleSprite from "../../../../assets/octo-idle-sprite.png"; import octoThinkingSprite from "../../../../assets/octo-thinking-sprite.png"; import octoImage from "../../../../assets/octo.png"; -import type { CopyFn } from "../lib/appTypes"; +import type { CopyFn, Theme } from "../lib/appTypes"; +import { languages, type Language } from "../lib/i18n"; import { buildOctopalConfig, connectorProviders, @@ -45,9 +52,30 @@ import { isExistingSecret, type InstallForm, } from "../lib/install"; -import { Button } from "./Button"; +import { cn } from "../lib/cn"; import { ChatView } from "./ChatView"; +import { DashboardHeader } from "./dashboard/DashboardHeader"; import { Field as SetupField, Input } from "./Field"; +import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; +import { Badge, type BadgeVariant } from "./ui/badge"; +import { + Card, + CardAction, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "./ui/card"; +import { Button } from "./ui/button"; +import { + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogTitle, +} from "./ui/dialog"; +import { Table, TableCell, TableHead, TableRow } from "./ui/table"; type DashboardView = "chat" | "control" | "connectors" | "skills" | "workers" | "system"; @@ -80,6 +108,10 @@ type ConnectorStatus = { services?: string[]; }; +type DashboardMcpServer = NonNullable< + DesktopDashboardSnapshot["system"] +>["mcpServers"][number]; + const emptyTemplateForm: WorkerTemplateForm = { id: "", name: "", @@ -154,14 +186,42 @@ function skillStatusLabel(skill: DesktopSkill): string { return skill.status || "needs setup"; } -function skillStatusClass(skill: DesktopSkill): string { +function statusBadgeVariant(status?: string): BadgeVariant { + const value = String(status ?? "").toLowerCase(); + if (["running", "started", "thinking"].includes(value)) { + return "live"; + } + if (["completed", "ok", "connected", "ready"].includes(value)) { + return "success"; + } + if ( + [ + "warning", + "stopped", + "awaiting_instruction", + "waiting_for_children", + "needs_auth", + "needs_reauth", + "misconfigured", + "unsupported_service_configuration", + ].includes(value) + ) { + return "warning"; + } + if (["error", "failed", "critical"].includes(value)) { + return "danger"; + } + return "outline"; +} + +function skillStatusBadgeVariant(skill: DesktopSkill): BadgeVariant { if (!skill.enabled) { - return statusClass("stopped"); + return "warning"; } if (skill.ready) { - return statusClass("ok"); + return "success"; } - return statusClass("warning"); + return "warning"; } function skillSourceLabel(skill: DesktopSkill): string { @@ -278,18 +338,66 @@ function connectorStatusFor( : null; } -function connectorStatusClass(status?: string): string { - const value = String(status ?? "").toLowerCase(); - if (value === "ready") { - return "dashboard-status dashboard-status-good"; +function connectorServerId( + name: DesktopConnectorName, + serviceId: string, +): string { + if (name === "google") { + return `google-${serviceId}`; } - if (["needs_auth", "needs_reauth", "misconfigured", "unsupported_service_configuration"].includes(value)) { - return "dashboard-status dashboard-status-warn"; + return "github-core"; +} + +function connectorLabel(name: DesktopConnectorName): string { + return name === "google" ? "Google" : "GitHub"; +} + +function connectorStatusFromRuntimeServers( + name: DesktopConnectorName, + enabled: boolean, + services: string[], + servers: DashboardMcpServer[], +): ConnectorStatus | null { + if (!enabled) { + return { status: "disabled", message: `${connectorLabel(name)} connector is disabled.` }; } - if (["error", "failed"].includes(value)) { - return "dashboard-status dashboard-status-bad"; + + if (services.length === 0) { + return { + status: "misconfigured", + message: `${connectorLabel(name)} connector is enabled but no services are selected.`, + }; } - return "dashboard-status"; + + const connectedIds = new Set(servers.map((server) => server.id)); + const expectedIds = new Set( + services.map((service) => connectorServerId(name, service)), + ); + const matchingServers = + name === "github" + ? servers.filter((server) => server.id === "github-core" || server.id.startsWith("github-")) + : servers.filter((server) => expectedIds.has(server.id)); + + if (matchingServers.length === 0) { + return null; + } + + if ( + name === "github" || + Array.from(expectedIds).every((serverId) => connectedIds.has(serverId)) + ) { + return { + status: "ready", + message: `${connectorLabel(name)} connector is ready. ${matchingServers.length} runtime MCP server${matchingServers.length === 1 ? "" : "s"} connected.`, + services, + }; + } + + return { + status: "warning", + message: `${matchingServers.length} of ${expectedIds.size} ${connectorLabel(name)} runtime MCP servers connected.`, + services, + }; } function connectorServiceIcon(serviceId: string) { @@ -517,6 +625,8 @@ function Field({ export function DashboardScreen({ copy, + language, + theme, installDir, runtimeView, updateAvailable, @@ -530,8 +640,12 @@ export function DashboardScreen({ onRestart, onUpdateOctopal, onUpdateDesktopApp, + onLanguageChange, + onThemeChange, }: { copy: CopyFn; + language: Language; + theme: Theme; installDir: string; runtimeView: { state: string; title: string; detail: string }; updateAvailable: boolean; @@ -545,8 +659,11 @@ export function DashboardScreen({ onRestart: () => void; onUpdateOctopal: () => void; onUpdateDesktopApp: () => void; + onLanguageChange: (language: Language) => void; + onThemeChange: (theme: Theme) => void; }) { const [view, setView] = useState("control"); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [snapshot, setSnapshot] = useState( null, ); @@ -577,6 +694,8 @@ export function DashboardScreen({ useState(null); const [connectorBusy, setConnectorBusy] = useState(null); + const [expandedConnector, setExpandedConnector] = + useState(null); const [connectorError, setConnectorError] = useState(""); const [connectorNotice, setConnectorNotice] = useState(""); const [startedAt] = useState(() => Date.now()); @@ -675,7 +794,7 @@ export function DashboardScreen({ ]); setConnectorValues(formValuesFromOctopalConfig(config, installDir)); setConnectorStatus(status); - setConnectorError(status.ok ? "" : status.detail); + setConnectorError(""); } catch (error) { setConnectorError( error instanceof Error ? error.message : "Could not load connectors.", @@ -791,10 +910,6 @@ export function DashboardScreen({ ] .filter(Boolean) .join(" · "); - const systemTitle = attention ? attentionTitleText : runtimeView.title; - const systemDetail = attention - ? attentionDetail - : runtimeView.detail || copy("systemBody"); const services = snapshot?.system?.services ?? []; const connectedMcpServers = (snapshot?.system?.mcpServers ?? []).filter( (server) => String(server.status).toLowerCase() === "connected", @@ -823,6 +938,70 @@ export function DashboardScreen({ ) ?? null) : null; const animatedOcto = animatedOctoForState(displayOctoState); + const octoHeaderTitle = octoHeadline; + const octoHeaderDetail = latestAction || octoDetail; + const octoHeaderAvatar = animatedOcto ? ( + + ) : ( + Octopal mascot + ); + const dashboardNavItems: Array<{ + view: DashboardView; + label: string; + description: string; + icon: typeof Activity; + count?: number; + }> = [ + { + view: "control", + label: copy("control"), + description: copy("control"), + icon: Activity, + }, + { + view: "chat", + label: copy("chat"), + description: copy("chat"), + icon: MessageCircle, + }, + { + view: "workers", + label: copy("workers"), + description: copy("workers"), + icon: Wrench, + count: templates.length, + }, + { + view: "skills", + label: copy("skills"), + description: copy("skills"), + icon: Puzzle, + count: enabledSkillCount, + }, + { + view: "connectors", + label: copy("connectors"), + description: copy("connectors"), + icon: Mail, + }, + { + view: "system", + label: copy("systemView"), + description: copy("systemBody"), + icon: Settings2, + }, + ]; + const currentNavItem = + dashboardNavItems.find((item) => item.view === view) ?? dashboardNavItems[0]; function startCreateTemplate(): void { setEditingTemplateId(""); @@ -1219,110 +1398,49 @@ export function DashboardScreen({ } } - function renderDashboardHeader({ - title, - titleRaw = title, - detail, - detailRaw = detail, - latest, - latestRaw = latest, - actions, - }: { - title: string; - titleRaw?: string; - detail: string; - detailRaw?: string; - latest?: string; - latestRaw?: string; - actions?: ReactNode; - }) { - return ( -
-
- {animatedOcto ? ( - - ) : ( - Octopal mascot - )} - - {displayOctoState} - -
-
-

{title}

-

- {detail} -

- {latest ? ( -

- {copy("latestAction")}: {latest} -

- ) : null} -
- {actions ?
{actions}
: null} -
- ); - } - function renderControl() { return (
- {renderDashboardHeader({ - title: octoHeadline, - titleRaw: octoHeadlineRaw, - detail: octoDetail, - detailRaw: octoDetailRaw, - latest: latestAction, - latestRaw: latestActionRaw, - actions: - updateAvailable || desktopUpdateAvailable ? ( - - ) : null, - })} + {updateAvailable || desktopUpdateAvailable ? ( +
+
+ {copy("updateReady")} + {latestAction} +
+ +
+ ) : null} {attention || octoNeedsAttention || dashboardError ? ( -
+
{copy("runtimeStatusError")} -

{attentionTitleText}

-

{attentionDetail}

+ {attentionTitleText} + {attentionDetail} {attentionMeta ? {attentionMeta} : null}
-
+ ) : null} -
-
+ +
-

{copy("liveLoad")}

-

{copy("liveLoadBody")}

+ {copy("liveLoad")} + {copy("liveLoadBody")}
-
- {copy("lastSamples")} -
-
+ + {copy("lastSamples")} + +
{loadMetrics.map((metric) => (
@@ -1369,51 +1487,53 @@ export function DashboardScreen({ {dashboardError ? (

{dashboardError}

) : null} -
+ -
-
+ +
-

{copy("workerRuns")}

-

{copy("workerRunsBody")}

-
- -
-
-
- ID - {copy("status")} - {copy("template")} - {copy("task")} - {copy("updated")} - Details + {copy("workerRuns")} + {copy("workerRunsBody")}
+ + + + + + + + ID + {copy("status")} + {copy("template")} + {copy("task")} + {copy("updated")} + Details + {recentWorkers.length === 0 ? ( -
+
{copy("noRecentWorkers")}
) : ( recentWorkers.slice(0, 8).map((worker, index) => ( - + + )) )} -
- +
+
+
); } @@ -1446,12 +1567,6 @@ export function DashboardScreen({ function renderWorkers() { return (
- {renderDashboardHeader({ - title: copy("workerTemplates"), - detail: copy("workerTemplatesBody"), - latest: latestAction, - latestRaw: latestActionRaw, - })} {templateError ? (

{templateError}

) : null} @@ -1459,22 +1574,24 @@ export function DashboardScreen({

{templateNotice}

) : null}
-
-
+ +
-

{copy("templates")}

-

workspace/workers

+ {copy("templates")} + workspace/workers
- -
-
+ + + + + {templates.length === 0 ? (

{copy("noWorkerTemplates")} @@ -1496,8 +1613,8 @@ export function DashboardScreen({ )) )} -

-
+ +
); @@ -1510,13 +1627,6 @@ export function DashboardScreen({ return (
- {renderDashboardHeader({ - title: copy("skills"), - detail: copy("skillsBody"), - latest: latestAction, - latestRaw: latestActionRaw, - })} - {skillError ? (

{skillError}

) : null} @@ -1524,61 +1634,63 @@ export function DashboardScreen({

{skillNotice}

) : null} -
- - -
- - -
-
+ + + + +
+ + +
+
+
-
-
+ +
-

{copy("installedSkills")}

-

workspace/skills

-
-
- {skills.length} total - {enabledSkillCount} enabled - {readySkillCount} ready + {copy("installedSkills")} + workspace/skills
-
-
+ + {skills.length} total + {enabledSkillCount} enabled + {readySkillCount} ready + + + {skills.length === 0 ? (

{copy("noSkills")}

) : ( @@ -1600,9 +1712,9 @@ export function DashboardScreen({ {skill.name} - + {skillStatusLabel(skill)} - +

{skill.description || copy("noSkillDescription")}

{skillScopeLabel(skill.scope)} @@ -1612,10 +1724,10 @@ export function DashboardScreen({ )) )} -
-
+ + -
+ {selectedSkill ? ( <>
@@ -1665,15 +1777,15 @@ export function DashboardScreen({
- + {skillStatusLabel(selectedSkill)} - - + + {skillScopeLabel(selectedSkill.scope)} - - + + {skillOriginLabel(selectedSkill.origin)} - +
{selectedSkill.reasons.length > 0 ? ( @@ -1750,7 +1862,7 @@ export function DashboardScreen({ ) : (

{copy("selectSkill")}

)} -
+
); @@ -1760,9 +1872,9 @@ export function DashboardScreen({ function renderConnectorPanel(name: DesktopConnectorName) { if (!connectorValues) { return ( -
+

Loading connector settings...

-
+ ); } @@ -1774,20 +1886,64 @@ export function DashboardScreen({ const services = isGoogle ? connectorValues.googleConnectorServices : connectorValues.githubConnectorServices; - const status = isGoogle ? googleConnectorStatus : githubConnectorStatus; + const status = + (isGoogle ? googleConnectorStatus : githubConnectorStatus) ?? + connectorStatusFromRuntimeServers( + name, + enabled, + services, + connectedMcpServers, + ); + const expanded = expandedConnector === name; + const selectedServices = services.length + ? `${services.length} service${services.length === 1 ? "" : "s"} selected` + : "No services selected"; return ( -
-
-
-

{provider.label}

-

{isGoogle ? copy("googleConnectorBody") : copy("githubConnectorBody")}

+ +
-
+
+ {selectedServices} + + {status?.status ?? "unknown"} + +
+ + + {expanded ? ( +
-
-
+ + ) : null} + ); } @@ -1909,15 +2066,16 @@ export function DashboardScreen({ return (
- {renderDashboardHeader({ - title: copy("connectorsTitle"), - detail: - "Configure connector accounts and apply them to the running instance.", - latest: - connectorServers.length > 0 - ? `${connectorServers.length} connector MCP server${connectorServers.length === 1 ? "" : "s"} visible` - : "No connector MCP servers visible yet", - actions: ( +
+
+ + {connectorServers.length > 0 + ? `${connectorServers.length} connector MCP server${connectorServers.length === 1 ? "" : "s"} visible` + : "No connector MCP servers visible yet"} + + Configure connector accounts and apply them to the running instance. +
+
- ), - })} +
+
{connectorError ? (

{connectorError}

@@ -1942,8 +2100,11 @@ export function DashboardScreen({ {renderConnectorPanel("github")}
-
-

Runtime connector servers

+ + + Runtime connector servers + + {connectorServers.length === 0 ? (

No connector-backed MCP servers are currently connected.

) : ( @@ -1968,7 +2129,8 @@ export function DashboardScreen({ ))}
)} - + + ); } @@ -1976,34 +2138,32 @@ export function DashboardScreen({ function renderSystem() { return (
- {renderDashboardHeader({ - title: systemTitle, - detail: systemDetail, - latest: latestAction, - latestRaw: latestActionRaw, - })} {attention || dashboardError ? ( -
+
{copy("runtimeStatusError")} -

{attentionTitleText}

-

{attentionDetail}

+ {attentionTitleText} + {attentionDetail} {attentionMeta ? {attentionMeta} : null}
-
+ ) : null}
-
-

{copy("runtime")}

-

{copy("runtimeBody")}

-
+ + +
+ {copy("runtime")} + {copy("runtimeBody")} +
+
+ {runtimeView.state === "running" ? ( <> ) : null} -
-
+ + -
-

{copy("updates")}

-

{copy("updatesBody")}

-
+ + +
+ {copy("updates")} + {copy("updatesBody")} +
+
+
-
+ + -
-

{copy("services")}

-
+ + +
+ + {copy("language")} / {copy("theme")} + + + {copy("language")} · {copy("theme")} + +
+
+ + + + +
+ + + + {copy("services")} + + {services.length === 0 ? ( - + {copy("noDashboardData")} - + ) : ( services.map((service) => ( - {service.name} {service.status} - + )) )} -
-
- -
-

{copy("connectedMcpServers")}

+ + + + + + {copy("connectedMcpServers")} + + {connectedMcpServers.length === 0 ? (

{copy("noConnectedMcpServers")}

) : ( @@ -2114,10 +2342,14 @@ export function DashboardScreen({ ))}
)} -
- -
-

{copy("recentLogs")}

+ + + + + + {copy("recentLogs")} + + {logs.length === 0 ? (

{copy("noLogs")}

) : ( @@ -2130,7 +2362,8 @@ export function DashboardScreen({ ))}
)} - + +
); @@ -2174,21 +2407,21 @@ export function DashboardScreen({ "No result yet."; return ( -
-
+ -
+

Worker run

-

+ {selectedWorker.template_name ?? selectedWorker.template_id ?? shortId(selectedWorker.id)} -

+
{selectedWorkerTemplate ? ( @@ -2201,15 +2434,19 @@ export function DashboardScreen({ Edit template ) : null} - +
-
+
@@ -2253,7 +2490,7 @@ export function DashboardScreen({
-
+

Result

@@ -2275,9 +2512,9 @@ export function DashboardScreen({ {!selectedWorker.summary && !selectedWorker.error ? (

{preview}

) : null} -
+
-
+

Action timeline

@@ -2300,21 +2537,21 @@ export function DashboardScreen({ ))} -
+
{outputText ? ( -
+

Structured output

JSON
{outputText}
-
+ ) : null}
-
-
+ + ); } @@ -2459,117 +2696,121 @@ export function DashboardScreen({ exit={{ opacity: 0, y: -16 }} transition={{ duration: 0.24 }} > - - -
- - {view === "control" ? renderControl() : null} - {view === "workers" ? renderWorkers() : null} - {view === "skills" ? renderSkills() : null} - {view === "connectors" ? renderConnectors() : null} - {view === "system" ? renderSystem() : null} +
+ + +
+ + +
+ + {view === "control" ? renderControl() : null} + {view === "workers" ? renderWorkers() : null} + {view === "skills" ? renderSkills() : null} + {view === "connectors" ? renderConnectors() : null} + {view === "system" ? renderSystem() : null} +
+
{renderWorkerDetailModal()} {editingTemplateId !== null ? ( -
-
+ -
+
-

+ {isCreatingTemplate ? copy("newTemplate") : copy("editTemplate")} -

-

{copy("focusedWorkerEditor")}

+ + {copy("focusedWorkerEditor")}
- -
+ +
-
+ {!isCreatingTemplate ? (
-
-
+ + + ) : null} ); diff --git a/desktop/src/renderer/src/components/DesignPreview.tsx b/desktop/src/renderer/src/components/DesignPreview.tsx new file mode 100644 index 00000000..c08dcd24 --- /dev/null +++ b/desktop/src/renderer/src/components/DesignPreview.tsx @@ -0,0 +1,603 @@ +import { useEffect, useState } from "react"; + +import { AppShell } from "./AppShell"; +import { DashboardScreen } from "./DashboardScreen"; +import type { Theme } from "../lib/appTypes"; +import { t, type Language } from "../lib/i18n"; + +const now = new Date("2026-06-04T18:24:00.000Z"); + +const previewTemplates: DesktopWorkerTemplate[] = [ + { + id: "research", + name: "Research Analyst", + description: "Explores current context, summarizes evidence, and reports concise next steps.", + system_prompt: "You are a careful research worker.", + available_tools: ["web.search", "filesystem.read", "memory.query", "report.write"], + required_permissions: ["network", "workspace_read"], + model: "gpt-5.5", + max_thinking_steps: 12, + default_timeout_seconds: 420, + can_spawn_children: true, + allowed_child_templates: ["writer", "qa"], + }, + { + id: "qa", + name: "QA Runner", + description: "Runs checks, captures failures, and keeps reproducible notes.", + system_prompt: "You are a pragmatic QA worker.", + available_tools: ["shell", "browser", "screenshot", "logs"], + required_permissions: ["workspace_read", "workspace_write"], + model: "gpt-5.5", + max_thinking_steps: 10, + default_timeout_seconds: 360, + can_spawn_children: false, + allowed_child_templates: [], + }, +]; + +const longPreview = + "Running a cross-page interface QA pass. Checking desktop dashboard pages, scroll containment, worker detail modal overflow, connector forms, and dense table layouts after the shadcn visual pass."; + +const previewSnapshot: DesktopDashboardSnapshot = { + ok: true, + detail: "Preview dashboard is using mocked runtime data.", + generatedAt: now.toISOString(), + baseUrl: "http://127.0.0.1:8010", + dashboardEnabled: true, + load: { + activeWorkers: 3, + queueDepth: 5, + octoQueue: 2, + }, + octo: { + state: "thinking", + headline: "Octopal is running", + detail: "PID 4157 · uptime 10:24:33 · channel Telegram", + latestAction: longPreview, + }, + workers: { + recent: [ + { + id: "worker-qa-scroll-0001", + template_name: "QA Runner", + template_id: "qa", + status: "running", + task: longPreview, + created_at: new Date(now.getTime() - 13 * 60_000).toISOString(), + updated_at: now.toISOString(), + summary: "", + result_preview: "Inspecting worker detail scroll behavior and dense dashboard sections.", + tools_used: [ + "browser.open", + "browser.screenshot", + "dom.measure", + "css.inspect", + "browser.scroll", + "build.run", + "build.run", + "dom.measure", + "screenshot.compare", + ], + parent_worker_id: null, + lineage_id: "lineage-ui-qa-20260604", + spawn_depth: 0, + output: { + pages_checked: ["Control", "Workers", "Skills", "Connectors", "System"], + issues: [ + "Worker detail body must scroll independently.", + "Buttons should not overflow in compact panels.", + "Cards should stay inside dashboard content bounds.", + ], + long_note: + "This intentionally long structured output gives the modal enough content to prove that scrolling works in the main and side columns without pushing the dialog outside the viewport.", + }, + template_config: { + model: "gpt-5.5", + max_thinking_steps: 10, + default_timeout_seconds: 360, + available_tools: [ + "browser.open", + "browser.screenshot", + "dom.measure", + "css.inspect", + "browser.scroll", + "build.run", + "logs.read", + "shell.exec", + "artifact.capture", + "issue.report", + ], + can_spawn_children: false, + }, + audit_timeline: Array.from({ length: 18 }, (_, index) => ({ + id: `qa-event-${index + 1}`, + ts: new Date(now.getTime() - (18 - index) * 42_000).toISOString(), + level: index % 7 === 0 ? "warning" : "info", + event_type: index % 7 === 0 ? "layout_warning" : "dom_check", + data_preview: + index % 7 === 0 + ? "Detected a possible overflow condition; verifying scroll container metrics before reporting." + : `Checked layout segment ${index + 1}: cards, tables, dialogs, and action rows remain inside their containers.`, + })), + }, + { + id: "worker-research-0002", + template_name: "Research Analyst", + template_id: "research", + status: "completed", + task: "Summarize connector readiness and MCP server health.", + created_at: new Date(now.getTime() - 42 * 60_000).toISOString(), + updated_at: new Date(now.getTime() - 34 * 60_000).toISOString(), + summary: "Connectors are configured and runtime MCP servers are visible.", + tools_used: ["filesystem.read", "report.write"], + lineage_id: "lineage-connectors", + spawn_depth: 1, + }, + { + id: "worker-docs-0003", + template_name: "Documentation Writer", + template_id: "writer", + status: "awaiting_instruction", + task: "Prepare release notes draft for the desktop UI refresh.", + created_at: new Date(now.getTime() - 75 * 60_000).toISOString(), + updated_at: new Date(now.getTime() - 61 * 60_000).toISOString(), + result_preview: "Waiting for final QA findings.", + tools_used: ["report.write"], + lineage_id: "lineage-release-notes", + spawn_depth: 1, + }, + ], + }, + system: { + services: [ + { id: "octo", name: "Octo", status: "running", reason: "Runtime loop active." }, + { id: "gateway", name: "Gateway", status: "running", reason: "FastAPI gateway reachable." }, + { id: "workers", name: "Workers", status: "warning", reason: "One run is still active." }, + { id: "memory", name: "Memory", status: "ok", reason: "SQLite store is available." }, + ], + mcpServers: [ + { + id: "google-gmail", + name: "Gmail Connector", + status: "connected", + reason: "Gmail connector is ready.", + transport: "stdio", + toolCount: 21, + reconnectAttempts: 0, + }, + { + id: "google-calendar", + name: "Google Calendar Connector", + status: "connected", + reason: "Calendar connector is ready.", + transport: "stdio", + toolCount: 8, + reconnectAttempts: 0, + }, + { + id: "google-drive", + name: "Google Drive Connector", + status: "connected", + reason: "Drive connector is ready.", + transport: "stdio", + toolCount: 10, + reconnectAttempts: 0, + }, + { + id: "github-core", + name: "GitHub Connector", + status: "connected", + reason: "Repository tools are ready.", + transport: "stdio", + toolCount: 9, + reconnectAttempts: 0, + }, + ], + logs: Array.from({ length: 12 }, (_, index) => ({ + timestamp: new Date(now.getTime() - index * 90_000).toISOString(), + level: index % 5 === 0 ? "warning" : "info", + service: index % 5 === 0 ? "workers" : "gateway", + event: + index % 5 === 0 + ? "worker queue pressure changed" + : "dashboard snapshot emitted", + })), + }, +}; + +const previewSkills: DesktopSkillsResponse = { + contract_version: "1", + count: 3, + registry_path: "/preview/octopal/workspace/skills", + install: { + supported_sources: ["clawhub", "git", "path"], + default_clawhub_site: "https://clawhub.dev", + }, + skills: [ + { + id: "desktop-qa", + name: "Desktop QA", + description: "Checks UI surfaces, screenshots, overflow, and keyboard-accessible flows.", + scope: "both", + enabled: true, + ready: true, + status: "ready", + reasons: [], + origin: "installed", + source: { + kind: "path", + label: "/preview/skills/desktop-qa", + path: "/preview/skills/desktop-qa", + installer_managed: true, + auto_discovered: false, + }, + trust: { + trusted: true, + has_scripts: true, + scan_status: "clean", + scan_findings_count: 0, + }, + runtime: { + kind: "node", + required: true, + recommended: true, + prepared: true, + next_step: "", + }, + requirements: { missing_bins: [], missing_env: [], missing_config: [] }, + actions: { can_enable: false, can_disable: true, can_remove: true, can_install: false }, + }, + { + id: "release-notes", + name: "Release Notes", + description: "Drafts concise operator-facing release notes from checked changes.", + scope: "octo", + enabled: true, + ready: false, + status: "needs setup", + reasons: ["Missing RELEASE_CHANNEL"], + origin: "workspace", + source: { + kind: "path", + label: "/preview/skills/release-notes", + path: "/preview/skills/release-notes", + installer_managed: false, + auto_discovered: true, + }, + trust: { + trusted: true, + has_scripts: false, + scan_status: "clean", + scan_findings_count: 0, + }, + runtime: { + kind: "python", + required: false, + recommended: true, + prepared: false, + next_step: "Set RELEASE_CHANNEL to enable publishing checks.", + }, + requirements: { + missing_bins: [], + missing_env: ["RELEASE_CHANNEL"], + missing_config: [], + }, + actions: { can_enable: true, can_disable: true, can_remove: true, can_install: false }, + }, + ], +}; + +const previewChatHistory: DesktopChatEvent = { + type: "chat_history", + messages: [ + { + id: "preview-chat-user-1", + type: "chat_message", + role: "user", + direction: "outbound", + channel: "desktop", + created_at: new Date(now.getTime() - 5 * 60_000).toISOString(), + text: "Can we close the Mini Shai-Hulu investigation?", + }, + { + id: "preview-chat-assistant-1", + type: "chat_message", + role: "assistant", + direction: "inbound", + channel: "desktop", + created_at: new Date(now.getTime() - 3 * 60_000).toISOString(), + text: "Chrome extension manifests are missing or empty. I will check Brave separately.", + }, + { + id: "preview-chat-user-2", + type: "chat_message", + role: "user", + direction: "outbound", + channel: "desktop", + created_at: new Date(now.getTime() - 45_000).toISOString(), + text: "Find the current water temperature near Port Colborne.", + }, + ], +}; + +const previewChatActivities: DesktopChatEvent[] = [ + { + type: "progress", + state: "tool_start", + text: "Octo using web_research", + meta: { tool_name: "web_research" }, + }, + { + type: "progress", + state: "tool_start", + text: "Octo checking latest lake conditions", + meta: { tool_name: "get_worker_result" }, + }, +]; + +const previewChatReply: DesktopChatEvent = { + id: "preview-chat-assistant-2", + type: "chat_message", + role: "assistant", + direction: "inbound", + channel: "desktop", + created_at: new Date(now.getTime()).toISOString(), + text: "The latest nearby reading is from the Port Colborne shoreline station.\n\n| Location | Peak temp | Season |\n| --- | ---: | --- |\n| Erie (Port Colborne) | 24-26C | July-August |\n| Ontario | 22-25C | July-August |\n\nI can pull a fresher station if you want a narrower location.", +}; + +function installPreviewDesktopApi() { + if (window.octopalDesktop) { + return; + } + + window.octopalDesktop = { + loadSettings: async () => ({ language: "en", theme: "dark", installDir: "/preview/octopal" }), + saveSettings: async (settings) => settings, + chooseInstallDir: async () => "/preview/octopal", + closeWindow: async () => undefined, + minimizeWindow: async () => undefined, + toggleMaximizeWindow: async () => undefined, + checkPrerequisites: async () => [], + getInstallState: async () => ({ + installed: true, + installDir: "/preview/octopal", + configPath: "/preview/octopal/config.json", + planPath: "/preview/octopal/install-plan.json", + }), + loadOctopalConfig: async () => ({ + user_channel: "telegram", + gateway: { webapp_enabled: true, port: 8010, dashboard_token: "preview" }, + connectors: { + instances: { + google: { + enabled: true, + enabled_services: ["gmail", "calendar", "drive"], + credentials: { client_id: "preview-client", client_secret: "configured" }, + }, + github: { + enabled: true, + enabled_services: ["repos", "issues", "pull_requests"], + auth: { access_token: "configured" }, + }, + }, + }, + }), + saveOctopalConfig: async () => ({ + installed: true, + installDir: "/preview/octopal", + configPath: "/preview/octopal/config.json", + planPath: "/preview/octopal/install-plan.json", + }), + writeInstallPlan: async () => ({ planPath: "/preview/octopal/install-plan.json" }), + installOctopal: async () => ({ + installDir: "/preview/octopal", + releaseTag: "preview", + configPath: "/preview/octopal/config.json", + planPath: "/preview/octopal/install-plan.json", + }), + startOctopal: async () => ({ ok: true, installDir: "/preview/octopal", detail: "Started" }), + stopOctopal: async () => ({ ok: true, installDir: "/preview/octopal", detail: "Stopped" }), + getOctopalStatus: async () => ({ + ok: true, + state: "running", + title: "Octopal is running", + detail: "Preview runtime", + installDir: "/preview/octopal", + }), + checkOctopalUpdate: async () => ({ + ok: true, + status: "current", + updateAvailable: false, + canUpdate: true, + detail: "No update available.", + }), + updateOctopal: async () => ({ + ok: true, + installDir: "/preview/octopal", + detail: "Already current.", + }), + getDashboardSnapshot: async () => previewSnapshot, + openOctopalLogs: async () => true, + getWorkerTemplates: async () => previewTemplates, + getSkills: async () => previewSkills, + installSkill: async () => previewSkills.skills[0], + setSkillEnabled: async (_installDir, skillId, enabled) => ({ + ...(previewSkills.skills.find((skill) => skill.id === skillId) ?? previewSkills.skills[0]), + enabled, + }), + deleteSkill: async () => previewSkills, + connectChat: async () => ({ ok: true, state: "connected", detail: "Preview chat connected." }), + disconnectChat: async () => ({ ok: true, state: "disconnected", detail: "Preview chat disconnected." }), + chooseChatFiles: async () => [], + savePastedChatImage: async () => ({ + path: "/preview/image.png", + name: "image.png", + sizeBytes: 1024, + }), + sendChatMessage: async () => ({ ok: true }), + sendChatApprovalResponse: async () => ({ ok: true }), + pingChat: async () => ({ ok: true }), + saveWorkerTemplate: async (_installDir, template) => template, + deleteWorkerTemplate: async () => undefined, + getAppUpdateStatus: async () => ({ + ok: true, + status: "not-available", + currentVersion: "preview", + detail: "No app update available.", + canDownload: false, + canInstall: false, + isPackaged: false, + }), + checkAppUpdate: async () => ({ + ok: true, + status: "not-available", + currentVersion: "preview", + detail: "No app update available.", + canDownload: false, + canInstall: false, + isPackaged: false, + }), + downloadAppUpdate: async () => ({ + ok: true, + status: "not-available", + currentVersion: "preview", + detail: "No app update available.", + canDownload: false, + canInstall: false, + isPackaged: false, + }), + installAppUpdate: async () => ({ + ok: true, + status: "not-available", + currentVersion: "preview", + detail: "No app update available.", + canDownload: false, + canInstall: false, + isPackaged: false, + }), + getConnectorStatus: async () => ({ + ok: false, + detail: "Connector status command did not return JSON in preview.", + connectors: {}, + }), + authorizeConnector: async (_installDir, payload) => ({ + ok: true, + name: payload.name, + status: "ready", + message: "Authorized.", + detail: "Authorized.", + }), + disconnectConnector: async (_installDir, name) => ({ + ok: true, + name, + status: "disconnected", + message: "Disconnected.", + detail: "Disconnected.", + }), + applyConnectorRuntime: async (_installDir, name) => ({ + ok: true, + name, + status: "ready", + message: "Runtime updated.", + detail: "Runtime updated.", + }), + getCodexAuthStatus: async () => ({ available: true, connected: true, accountLabel: "Preview" }), + startCodexAuth: async () => ({ success: true }), + disconnectCodexAuth: async () => ({ success: true }), + listCodexModels: async () => ({ success: true, models: [] }), + startWhatsAppLink: async () => ({ + ok: true, + running: false, + connected: false, + linked: false, + qr: "", + terminal: "", + self: "", + detail: "Preview", + }), + getWhatsAppLinkStatus: async () => ({ + ok: true, + running: false, + connected: false, + linked: false, + qr: "", + terminal: "", + self: "", + detail: "Preview", + }), + stopWhatsAppLink: async () => ({ + ok: true, + running: false, + connected: false, + linked: false, + qr: "", + terminal: "", + self: "", + detail: "Preview", + }), + onInstallEvent: () => () => undefined, + onAppUpdateStatus: () => () => undefined, + onChatStatus: () => () => undefined, + onChatEvent: (callback) => { + window.setTimeout(() => callback(previewChatHistory), 0); + previewChatActivities.forEach((event, index) => { + window.setTimeout(() => callback(event), 450 + index * 900); + }); + window.setTimeout(() => callback(previewChatReply), 4200); + return () => undefined; + }, + onCodexAuthStatus: () => () => undefined, + onCodexAuthUpdated: () => () => undefined, + }; +} + +export function DesignPreview() { + const [previewLanguage, setPreviewLanguage] = useState("en"); + const [previewTheme, setPreviewTheme] = useState("dark"); + + installPreviewDesktopApi(); + + useEffect(() => { + document.documentElement.dataset.theme = + previewTheme === "system" + ? window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light" + : previewTheme; + }, [previewTheme]); + + return ( + undefined} + onMinimize={() => undefined} + onMaximize={() => undefined} + > + t(previewLanguage, key)} + language={previewLanguage} + theme={previewTheme} + installDir="/preview/octopal" + runtimeView={{ + state: "running", + title: "Octopal is running", + detail: "Local runtime is available and accepting work.", + }} + updateAvailable={false} + updateBlocked={false} + updateBusy={false} + desktopUpdateAvailable={false} + desktopUpdateReady={false} + desktopUpdateBusy={false} + onStart={() => undefined} + onStop={() => undefined} + onRestart={() => undefined} + onUpdateOctopal={() => undefined} + onUpdateDesktopApp={() => undefined} + onLanguageChange={setPreviewLanguage} + onThemeChange={setPreviewTheme} + /> + + ); +} diff --git a/desktop/src/renderer/src/components/dashboard/DashboardHeader.tsx b/desktop/src/renderer/src/components/dashboard/DashboardHeader.tsx new file mode 100644 index 00000000..f48fde39 --- /dev/null +++ b/desktop/src/renderer/src/components/dashboard/DashboardHeader.tsx @@ -0,0 +1,52 @@ +import type { ReactNode } from "react"; + +export function DashboardHeader({ + title, + description, + octo, +}: { + title: string; + description: string; + octo: { + avatar: ReactNode; + title: string; + detail: string; + state: string; + statusClassName: string; + }; +}) { + return ( +
+
+ {description} +

{title}

+
+ +
+ ); +} + +function OctoStatusWidget({ + avatar, + title, + detail, + state, + statusClassName, +}: { + avatar: ReactNode; + title: string; + detail: string; + state: string; + statusClassName: string; +}) { + return ( +
+ {avatar} +
+ {title} + {detail} +
+ {state} +
+ ); +} diff --git a/desktop/src/renderer/src/components/steps/ChannelStep.tsx b/desktop/src/renderer/src/components/steps/ChannelStep.tsx index a1b66b10..53de0981 100644 --- a/desktop/src/renderer/src/components/steps/ChannelStep.tsx +++ b/desktop/src/renderer/src/components/steps/ChannelStep.tsx @@ -1,4 +1,5 @@ import { motion } from "framer-motion"; +import { Monitor } from "lucide-react"; import type { FieldErrors, UseFormReturn } from "react-hook-form"; import { Field, Input, Select } from "../Field"; @@ -25,25 +26,62 @@ export function ChannelStep({ form: UseFormReturn; errors: FieldErrors; }) { + const desktopOnly = values.channel === "desktop"; + return ( -
- } - title={copy("telegram")} - body={channelCopy.telegram} - onClick={() => form.setValue("channel", "telegram")} - /> - } - title={copy("whatsapp")} - body={channelCopy.whatsapp} - onClick={() => form.setValue("channel", "whatsapp")} +
- {values.channel === "telegram" ? ( +
); } diff --git a/desktop/src/renderer/src/components/steps/ReviewStep.tsx b/desktop/src/renderer/src/components/steps/ReviewStep.tsx index 1dd836ff..53d233af 100644 --- a/desktop/src/renderer/src/components/steps/ReviewStep.tsx +++ b/desktop/src/renderer/src/components/steps/ReviewStep.tsx @@ -130,12 +130,17 @@ export function ReviewStep({ values.googleConnectorEnabled ? copy("googleConnector") : "", values.githubConnectorEnabled ? copy("githubConnector") : "", ].filter(Boolean); + const channelLabels: Record = { + desktop: copy("desktopChannel"), + telegram: copy("telegram"), + whatsapp: copy("whatsapp"), + }; return (
- + item.id === values.providerId)?.label ?? values.providerId} /> diff --git a/desktop/src/renderer/src/components/ui/alert.tsx b/desktop/src/renderer/src/components/ui/alert.tsx new file mode 100644 index 00000000..ae8b6bd3 --- /dev/null +++ b/desktop/src/renderer/src/components/ui/alert.tsx @@ -0,0 +1,54 @@ +import type { HTMLAttributes, ReactNode } from "react"; + +import { cn } from "../../lib/cn"; + +type AlertVariant = "default" | "warning" | "danger"; + +export function Alert({ + children, + className, + variant = "default", + ...props +}: HTMLAttributes & { + children: ReactNode; + variant?: AlertVariant; +}) { + return ( +
+ {children} +
+ ); +} + +export function AlertTitle({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function AlertDescription({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/desktop/src/renderer/src/components/ui/badge.tsx b/desktop/src/renderer/src/components/ui/badge.tsx new file mode 100644 index 00000000..8704f9d0 --- /dev/null +++ b/desktop/src/renderer/src/components/ui/badge.tsx @@ -0,0 +1,33 @@ +import type { HTMLAttributes, ReactNode } from "react"; + +import { cn } from "../../lib/cn"; + +export type BadgeVariant = + | "default" + | "secondary" + | "outline" + | "success" + | "warning" + | "danger" + | "live"; + +export function Badge({ + children, + className, + variant = "default", + ...props +}: HTMLAttributes & { + children: ReactNode; + variant?: BadgeVariant; +}) { + return ( + + {children} + + ); +} diff --git a/desktop/src/renderer/src/components/ui/button.tsx b/desktop/src/renderer/src/components/ui/button.tsx new file mode 100644 index 00000000..df6f25eb --- /dev/null +++ b/desktop/src/renderer/src/components/ui/button.tsx @@ -0,0 +1,37 @@ +import type { ButtonHTMLAttributes, ReactNode } from "react"; + +import { cn } from "../../lib/cn"; + +type ButtonVariant = + | "default" + | "primary" + | "secondary" + | "ghost" + | "outline" + | "success" + | "danger"; +type ButtonSize = "default" | "sm" | "lg" | "icon"; + +export function Button({ + children, + className, + size = "default", + variant = "default", + ...props +}: ButtonHTMLAttributes & { + children: ReactNode; + size?: ButtonSize; + variant?: ButtonVariant; +}) { + return ( + + ); +} diff --git a/desktop/src/renderer/src/components/ui/card.tsx b/desktop/src/renderer/src/components/ui/card.tsx new file mode 100644 index 00000000..fc6059cc --- /dev/null +++ b/desktop/src/renderer/src/components/ui/card.tsx @@ -0,0 +1,88 @@ +import type { HTMLAttributes, ReactNode } from "react"; + +import { cn } from "../../lib/cn"; + +export function Card({ + children, + className, + size = "default", + ...props +}: HTMLAttributes & { + children: ReactNode; + size?: "default" | "sm"; +}) { + return ( +
+ {children} +
+ ); +} + +export function CardHeader({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function CardTitle({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function CardDescription({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function CardAction({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function CardContent({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/desktop/src/renderer/src/components/ui/dialog.tsx b/desktop/src/renderer/src/components/ui/dialog.tsx new file mode 100644 index 00000000..25253ade --- /dev/null +++ b/desktop/src/renderer/src/components/ui/dialog.tsx @@ -0,0 +1,83 @@ +import type { HTMLAttributes, ReactNode } from "react"; + +import { cn } from "../../lib/cn"; + +export function DialogOverlay({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function DialogContent({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function DialogHeader({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function DialogFooter({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function DialogTitle({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +export function DialogDescription({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +

+ {children} +

+ ); +} diff --git a/desktop/src/renderer/src/components/ui/table.tsx b/desktop/src/renderer/src/components/ui/table.tsx new file mode 100644 index 00000000..d2f0de5a --- /dev/null +++ b/desktop/src/renderer/src/components/ui/table.tsx @@ -0,0 +1,75 @@ +import type { + ButtonHTMLAttributes, + HTMLAttributes, + ReactNode, +} from "react"; + +import { cn } from "../../lib/cn"; + +export function Table({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function TableRow({ + as = "div", + children, + className, + ...props +}: (HTMLAttributes | ButtonHTMLAttributes) & { + as?: "div" | "button"; + children: ReactNode; +}) { + if (as === "button") { + return ( + + ); + } + + return ( +
)} + > + {children} +
+ ); +} + +export function TableHead({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function TableCell({ + children, + className, + ...props +}: HTMLAttributes & { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/desktop/src/renderer/src/lib/i18n.ts b/desktop/src/renderer/src/lib/i18n.ts index 05a9b037..932f82ad 100644 --- a/desktop/src/renderer/src/lib/i18n.ts +++ b/desktop/src/renderer/src/lib/i18n.ts @@ -56,6 +56,10 @@ export const messages = { preflightFailed: "Could not check local requirements.", channelTitle: "How should Octopal talk to you?", channelBody: "Pick the first communication channel. You can change this later.", + desktopChannelOnly: "Use only Desktop chat", + desktopChannelOnlyBody: "Telegram and WhatsApp are optional when you use the desktop app.", + desktopChannel: "Desktop chat", + desktopChannelBody: "Octopal will talk to you through this app and will not require Telegram or WhatsApp.", telegram: "Telegram", whatsapp: "WhatsApp", telegramToken: "Telegram bot token", @@ -219,7 +223,11 @@ export const messages = { missing: "Missing", available: "Available", control: "Control", + chat: "Chat", workers: "Workers", + connectors: "Connectors", + collapseSidebar: "Collapse", + expandSidebar: "Expand", systemView: "System", openLogs: "Open logs", updateReady: "Update ready", @@ -363,6 +371,10 @@ export const messages = { preflightFailed: "Impossible de vérifier les prérequis locaux.", channelTitle: "Comment Octopal doit-il vous parler ?", channelBody: "Choisissez le premier canal. Vous pourrez le modifier plus tard.", + desktopChannelOnly: "Utiliser uniquement le chat Desktop", + desktopChannelOnlyBody: "Telegram et WhatsApp sont optionnels avec l'application desktop.", + desktopChannel: "Chat Desktop", + desktopChannelBody: "Octopal vous parlera via cette application et ne demandera ni Telegram ni WhatsApp.", telegram: "Telegram", whatsapp: "WhatsApp", telegramToken: "Token du bot Telegram", @@ -502,7 +514,11 @@ export const messages = { missing: "Manquant", available: "Disponible", control: "Contrôle", - workers: "Workers", + chat: "Chat", + workers: "Agents", + connectors: "Connecteurs", + collapseSidebar: "Réduire", + expandSidebar: "Déployer", systemView: "Système", openLogs: "Ouvrir les logs", updateReady: "Mise à jour prête", @@ -514,7 +530,7 @@ export const messages = { workerQueue: "File workers", octoQueue: "File Octo", collectingSamples: "Collecte des points", - workerRuns: "Workers", + workerRuns: "Agents", workerRunsBody: "Exécutions récentes avec statut, tâche et point d'entrée du résultat.", openWorkerStudio: "Ouvrir le studio workers", status: "Statut", @@ -548,7 +564,7 @@ export const messages = { templateSaveFailed: "Impossible d'enregistrer le template worker.", templateDeleteFailed: "Impossible de supprimer le template worker.", noWorkerTemplates: "Aucun template worker trouvé.", - runtime: "Runtime", + runtime: "Exécution", runtimeBody: "Processus Octopal, gateway et contrôles du canal.", restartOctopal: "Redémarrer Octopal", openDashboardUrl: "Ouvrir l'URL dashboard", @@ -567,7 +583,7 @@ export const messages = { octoLatestFallback: "Aucune action Octo récente.", systemBody: "Statut, boutons de cycle de vie, mises à jour, connecteurs et logs.", noLogs: "Aucun log dans la fenêtre actuelle.", - skills: "Skills", + skills: "Compétences", skillsBody: "Installer, activer, désactiver et supprimer les skills du runtime local.", skillsLoadFailed: "Impossible de charger les skills.", skillSourceRequired: "La source du skill est requise.", @@ -646,6 +662,10 @@ export const messages = { preflightFailed: "No se pudieron comprobar los requisitos locales.", channelTitle: "¿Cómo debe hablarte Octopal?", channelBody: "Elige el canal inicial. Podrás cambiarlo después.", + desktopChannelOnly: "Usar solo el chat Desktop", + desktopChannelOnlyBody: "Telegram y WhatsApp son opcionales cuando usas la app de escritorio.", + desktopChannel: "Chat Desktop", + desktopChannelBody: "Octopal hablará contigo mediante esta app y no requerirá Telegram ni WhatsApp.", telegram: "Telegram", whatsapp: "WhatsApp", telegramToken: "Token del bot de Telegram", @@ -785,7 +805,11 @@ export const messages = { missing: "Falta", available: "Disponible", control: "Control", - workers: "Workers", + chat: "Chat", + workers: "Trabajadores", + connectors: "Conectores", + collapseSidebar: "Contraer", + expandSidebar: "Expandir", systemView: "Sistema", openLogs: "Abrir logs", updateReady: "Actualización lista", @@ -797,7 +821,7 @@ export const messages = { workerQueue: "Cola workers", octoQueue: "Cola Octo", collectingSamples: "Recopilando muestras", - workerRuns: "Workers", + workerRuns: "Trabajadores", workerRunsBody: "Ejecuciones recientes con estado, tarea y entrada al resultado.", openWorkerStudio: "Abrir estudio de workers", status: "Estado", @@ -831,7 +855,7 @@ export const messages = { templateSaveFailed: "No se pudo guardar el template de worker.", templateDeleteFailed: "No se pudo eliminar el template de worker.", noWorkerTemplates: "No se encontraron templates de workers.", - runtime: "Runtime", + runtime: "Ejecución", runtimeBody: "Proceso Octopal, gateway y controles del canal.", restartOctopal: "Reiniciar Octopal", openDashboardUrl: "Abrir URL del dashboard", @@ -850,7 +874,7 @@ export const messages = { octoLatestFallback: "No hay acción reciente de Octo.", systemBody: "Estado, botones de ciclo de vida, actualizaciones, conectores y logs.", noLogs: "No hay logs en la ventana actual.", - skills: "Skills", + skills: "Habilidades", skillsBody: "Instala, activa, desactiva y elimina skills del runtime local.", skillsLoadFailed: "No se pudieron cargar los skills.", skillSourceRequired: "La fuente del skill es obligatoria.", @@ -929,6 +953,10 @@ export const messages = { preflightFailed: "无法检查本地依赖。", channelTitle: "Octopal 应该如何联系你?", channelBody: "选择初始沟通渠道。之后可以修改。", + desktopChannelOnly: "仅使用桌面聊天", + desktopChannelOnlyBody: "使用桌面应用时,Telegram 和 WhatsApp 都是可选的。", + desktopChannel: "桌面聊天", + desktopChannelBody: "Octopal 会通过此应用与你对话,不需要 Telegram 或 WhatsApp。", telegram: "Telegram", whatsapp: "WhatsApp", telegramToken: "Telegram 机器人 token", @@ -1068,7 +1096,11 @@ export const messages = { missing: "缺失", available: "可用", control: "控制", - workers: "Workers", + chat: "聊天", + workers: "工作器", + connectors: "连接器", + collapseSidebar: "收起", + expandSidebar: "展开", systemView: "系统", openLogs: "打开日志", updateReady: "更新就绪", @@ -1080,7 +1112,7 @@ export const messages = { workerQueue: "Worker 队列", octoQueue: "Octo 队列", collectingSamples: "正在收集样本", - workerRuns: "Workers", + workerRuns: "工作器", workerRunsBody: "最近运行,包含状态、任务和结果入口。", openWorkerStudio: "打开 worker studio", status: "状态", @@ -1114,7 +1146,7 @@ export const messages = { templateSaveFailed: "无法保存 worker 模板。", templateDeleteFailed: "无法删除 worker 模板。", noWorkerTemplates: "未找到 worker 模板。", - runtime: "Runtime", + runtime: "运行时", runtimeBody: "Octopal 进程、gateway 和渠道控制。", restartOctopal: "重启 Octopal", openDashboardUrl: "打开 dashboard URL", @@ -1133,7 +1165,7 @@ export const messages = { octoLatestFallback: "没有最近的 Octo 动作。", systemBody: "状态、生命周期按钮、更新、连接器检查和日志。", noLogs: "当前窗口没有日志。", - skills: "Skills", + skills: "技能", skillsBody: "安装、启用、停用并删除本地 runtime skills。", skillsLoadFailed: "无法加载 skills。", skillSourceRequired: "Skill source is required.", diff --git a/desktop/src/renderer/src/lib/install.ts b/desktop/src/renderer/src/lib/install.ts index 09d810a3..77a9bd96 100644 --- a/desktop/src/renderer/src/lib/install.ts +++ b/desktop/src/renderer/src/lib/install.ts @@ -72,7 +72,7 @@ const githubServiceSchema = z.enum(["repos", "issues", "pull_requests"]); export const installSchema = z .object({ installDir: z.string().trim().min(1), - channel: z.enum(["telegram", "whatsapp"]), + channel: z.enum(["desktop", "telegram", "whatsapp"]), telegramToken: z.string().optional(), allowedChatIds: z.string().optional(), whatsappMode: z.enum(["personal", "separate"]), @@ -179,7 +179,7 @@ function secretNullable(value: string | undefined): string | null { export const defaultInstallValues: InstallForm = { installDir: "", - channel: "telegram", + channel: "desktop", telegramToken: "", allowedChatIds: "", whatsappMode: "separate", @@ -345,7 +345,8 @@ export function formValuesFromOctopalConfig(config: unknown, installDir: string) return { ...defaultInstallValues, installDir, - channel: root.user_channel === "whatsapp" ? "whatsapp" : "telegram", + channel: + root.user_channel === "whatsapp" ? "whatsapp" : root.user_channel === "telegram" ? "telegram" : "desktop", telegramToken: stringValue(telegram.bot_token), allowedChatIds: listValue(telegram.allowed_chat_ids), whatsappMode: whatsapp.mode === "personal" ? "personal" : "separate", diff --git a/desktop/src/renderer/src/lib/wizard.ts b/desktop/src/renderer/src/lib/wizard.ts index 660b2566..d334bb6e 100644 --- a/desktop/src/renderer/src/lib/wizard.ts +++ b/desktop/src/renderer/src/lib/wizard.ts @@ -36,6 +36,9 @@ export function getValidationFields(step: StepId, values: InstallForm): Array - + , ); diff --git a/desktop/src/renderer/src/styles.css b/desktop/src/renderer/src/styles.css index 033952dc..20aa4668 100644 --- a/desktop/src/renderer/src/styles.css +++ b/desktop/src/renderer/src/styles.css @@ -8,41 +8,45 @@ BlinkMacSystemFont, "Segoe UI", sans-serif; - --background: #f7f9fd; - --foreground: #172033; + --background: #f8fafc; + --foreground: #101828; --muted: #667085; - --muted-strong: #4d5871; - --surface: rgba(255, 255, 255, 0.78); - --surface-strong: rgba(255, 255, 255, 0.94); - --border: rgba(111, 124, 152, 0.2); - --shadow: 0 24px 80px rgba(36, 49, 82, 0.14); - --shadow-soft: 0 14px 40px rgba(36, 49, 82, 0.1); - --primary: #4f74ff; - --primary-strong: #6548f5; + --muted-strong: #475467; + --surface: #ffffff; + --surface-strong: #ffffff; + --surface-muted: #f2f4f7; + --border: #d0d5dd; + --border-subtle: #eaecf0; + --shadow: 0 18px 40px rgba(16, 24, 40, 0.08); + --shadow-soft: 0 1px 2px rgba(16, 24, 40, 0.06); + --primary: #3451e2; + --primary-strong: #263bb5; --primary-foreground: #ffffff; - --accent: #23c3ff; - --success: #2fbf8f; - --danger: #ef5b6d; - --input: rgba(255, 255, 255, 0.88); - --radius: 18px; + --accent: #0ea5e9; + --success: #12b76a; + --danger: #f04438; + --input: #ffffff; + --radius: 8px; } :root[data-theme="dark"] { color-scheme: dark; - --background: #111522; - --foreground: #edf2ff; - --muted: #9aa6bd; - --muted-strong: #c0c8d8; - --surface: rgba(25, 31, 48, 0.76); - --surface-strong: rgba(29, 36, 56, 0.94); - --border: rgba(181, 194, 221, 0.18); - --shadow: 0 24px 80px rgba(0, 0, 0, 0.35); - --shadow-soft: 0 14px 44px rgba(0, 0, 0, 0.22); - --primary: #6ea0ff; - --primary-strong: #7658ff; + --background: #09090b; + --foreground: #f2f4f7; + --muted: #98a2b3; + --muted-strong: #d0d5dd; + --surface: #111113; + --surface-strong: #18181b; + --surface-muted: #1f1f23; + --border: #303036; + --border-subtle: #242428; + --shadow: 0 18px 38px rgba(0, 0, 0, 0.28); + --shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.22); + --primary: #5b7cfa; + --primary-strong: #4a63d9; --primary-foreground: #ffffff; - --accent: #49dcff; - --input: rgba(20, 26, 41, 0.92); + --accent: #38bdf8; + --input: #0f0f12; } * { @@ -120,6 +124,298 @@ button { cursor: pointer; } +.ui-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + min-height: 36px; + border: 1px solid transparent; + border-radius: 8px; + background: var(--primary); + color: var(--primary-foreground); + padding: 0 12px; + font-size: 13px; + font-weight: 650; + line-height: 1; + text-decoration: none; + white-space: nowrap; + transition: + border-color 0.14s ease, + background 0.14s ease, + color 0.14s ease; +} + +.ui-button:hover { + background: var(--primary-strong); +} + +.ui-button:focus-visible { + border-color: color-mix(in srgb, var(--primary) 64%, var(--border)); + outline: 0; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 16%, transparent); +} + +.ui-button:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.ui-button svg { + width: 16px; + height: 16px; +} + +.ui-button[data-size="sm"] { + min-height: 30px; + border-radius: 7px; + padding: 0 10px; + font-size: 12px; +} + +.ui-button[data-size="lg"] { + min-height: 40px; + padding: 0 14px; +} + +.ui-button[data-size="icon"] { + width: 36px; + min-width: 36px; + padding: 0; +} + +.ui-button[data-variant="secondary"], +.ui-button[data-variant="outline"], +.ui-button[data-variant="ghost"] { + border-color: var(--border); + background: var(--surface); + color: var(--foreground); +} + +.ui-button[data-variant="secondary"]:hover, +.ui-button[data-variant="outline"]:hover, +.ui-button[data-variant="ghost"]:hover { + background: var(--surface-muted); + color: var(--foreground); +} + +.ui-button[data-variant="outline"] { + background: transparent; +} + +.ui-button[data-variant="ghost"] { + border-color: transparent; + background: transparent; +} + +.ui-button[data-variant="success"] { + border-color: color-mix(in srgb, var(--success) 52%, var(--border)); + background: color-mix(in srgb, var(--success) 15%, var(--surface)); + color: color-mix(in srgb, var(--success) 78%, var(--foreground)); +} + +.ui-button[data-variant="danger"] { + border-color: color-mix(in srgb, var(--danger) 52%, var(--border)); + background: color-mix(in srgb, var(--danger) 13%, var(--surface)); + color: color-mix(in srgb, var(--danger) 82%, var(--foreground)); +} + +.ui-card { + min-width: 0; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + box-shadow: var(--shadow-soft); +} + +.ui-card[data-size="sm"] { + border-radius: 8px; +} + +.ui-card-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + border-bottom: 1px solid var(--border-subtle); + padding: 14px 16px; +} + +.ui-card-title { + margin: 0; + color: var(--foreground); + font-size: 14px; + font-weight: 650; + letter-spacing: 0; + line-height: 1.25; +} + +.ui-card-description { + margin-top: 4px; + color: var(--muted); + font-size: 13px; + line-height: 1.45; +} + +.ui-card-action { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + +.ui-card-content { + padding: 14px 16px 16px; +} + +.ui-alert { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + color: var(--foreground); +} + +.ui-alert[data-variant="warning"] { + border-color: color-mix(in srgb, #f4b84f 54%, var(--border)); + background: color-mix(in srgb, #f4b84f 9%, var(--surface-strong)); +} + +.ui-alert[data-variant="danger"] { + border-color: color-mix(in srgb, var(--danger) 62%, var(--border)); + background: color-mix(in srgb, var(--danger) 10%, var(--surface-strong)); +} + +.ui-alert-title { + margin: 0; + color: var(--foreground); + font-weight: 850; +} + +.ui-alert-description { + margin: 0; + color: var(--muted-strong); +} + +.ui-dialog-overlay, +.ui-dialog-content { + min-width: 0; +} + +.ui-dialog-title { + margin: 0; + color: var(--foreground); +} + +.ui-dialog-description { + margin: 0; + color: var(--muted); +} + +.ui-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 22px; + max-width: 100%; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--surface-muted); + color: var(--muted-strong); + padding: 0 8px; + font-size: 11px; + font-weight: 650; + line-height: 1; + white-space: nowrap; +} + +.ui-badge[data-variant="live"] { + border-color: color-mix(in srgb, var(--accent) 46%, var(--border)); + background: color-mix(in srgb, var(--accent) 10%, var(--surface)); + color: color-mix(in srgb, var(--accent) 72%, var(--foreground)); +} + +.ui-badge[data-variant="success"] { + border-color: color-mix(in srgb, var(--success) 50%, var(--border)); + background: color-mix(in srgb, var(--success) 10%, var(--surface)); + color: color-mix(in srgb, var(--success) 72%, var(--foreground)); +} + +.ui-badge[data-variant="warning"] { + border-color: color-mix(in srgb, #f4b84f 54%, var(--border)); + background: color-mix(in srgb, #f4b84f 10%, var(--surface)); + color: color-mix(in srgb, #f4b84f 74%, var(--foreground)); +} + +.ui-badge[data-variant="danger"] { + border-color: color-mix(in srgb, var(--danger) 58%, var(--border)); + background: color-mix(in srgb, var(--danger) 10%, var(--surface)); + color: color-mix(in srgb, var(--danger) 76%, var(--foreground)); +} + +.ui-badge[data-variant="outline"] { + background: transparent; +} + +.ui-table { + min-width: 0; + overflow-x: auto; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); +} + +.ui-table-row, +.ui-table-head { + display: grid; + width: 100%; + min-width: 760px; + grid-template-columns: 0.8fr 0.8fr 1.2fr minmax(180px, 2fr) 1fr 0.8fr; + align-items: center; + gap: 12px; + border-bottom: 1px solid var(--border-subtle); + padding: 10px 12px; +} + +.ui-table-row:last-child { + border-bottom: 0; +} + +.ui-table-head { + color: var(--muted); + font-size: 11px; + font-weight: 850; + text-transform: uppercase; +} + +.ui-table-row { + border-inline: 0; + background: transparent; + color: var(--foreground); + font-size: 13px; + text-align: left; +} + +.ui-table-row[as="button"], +button.ui-table-row { + cursor: pointer; +} + +button.ui-table-row:hover { + background: color-mix(in srgb, var(--surface-strong) 68%, transparent); +} + +.ui-table-cell { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .app-shell { height: 100vh; overflow: hidden; @@ -423,28 +719,6 @@ button { background: var(--danger); } -.button { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 10px; - min-height: 44px; - padding: 0 18px; - border: 1px solid transparent; - border-radius: 14px; - font-weight: 650; - transition: - transform 160ms ease, - box-shadow 160ms ease, - border-color 160ms ease, - background 160ms ease; -} - -.button:hover { - transform: translateY(-1px); -} - -.button:focus-visible, .input:focus, .step-item:focus-visible, .toggle-card:focus-visible { @@ -452,47 +726,6 @@ button { outline-offset: 2px; } -.button svg { - width: 20px; - height: 20px; -} - -.button-primary { - background: linear-gradient(135deg, var(--primary), var(--primary-strong)); - color: var(--primary-foreground); - box-shadow: 0 16px 30px color-mix(in srgb, var(--primary) 30%, transparent); -} - -.button-success { - background: linear-gradient(135deg, #2fbf8f, #17a56f); - color: #ffffff; - box-shadow: 0 16px 30px rgba(47, 191, 143, 0.24); -} - -.button-danger { - background: linear-gradient(135deg, #f06b7c, #d9465f); - color: #ffffff; - box-shadow: 0 16px 30px rgba(217, 70, 95, 0.22); -} - -.button:disabled { - cursor: default; - opacity: 0.68; - transform: none; -} - -.button-secondary, -.button-ghost { - color: var(--foreground); - background: var(--surface); - border-color: var(--border); -} - -.button-ghost { - min-height: 38px; - background: transparent; -} - .wizard-screen { min-height: calc(100vh - 58px); display: grid; @@ -506,7 +739,6 @@ button { border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow-soft); - backdrop-filter: blur(22px); } .wizard-rail { @@ -1393,7 +1625,7 @@ p { padding-top: 18px; } -.setup-footer .button { +.setup-footer .ui-button { min-width: 160px; } @@ -1480,7 +1712,7 @@ p { font-size: 15px; } -.setup-screen-connectors .connector-panel-head .button { +.setup-screen-connectors .connector-panel-head .ui-button { height: 42px; } @@ -1538,13 +1770,118 @@ p { margin: 0 auto; } -.search-grid { - max-width: 760px; - margin: 0 auto; - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.dashboard-form { +.desktop-channel-toggle { + position: relative; + display: grid; + grid-template-columns: 22px minmax(0, 1fr); + align-items: center; + gap: 12px; + width: min(760px, 100%); + margin: 0 auto 16px; + padding: 15px 16px; + border: 1px solid rgba(94, 119, 255, 0.34); + border-radius: 12px; + background: color-mix(in srgb, var(--surface) 78%, rgba(89, 119, 255, 0.18)); + cursor: pointer; +} + +.desktop-channel-toggle input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.desktop-channel-check { + width: 22px; + height: 22px; + border: 1px solid rgba(142, 157, 189, 0.5); + border-radius: 7px; + background: rgba(8, 10, 16, 0.46); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); +} + +.desktop-channel-toggle input:checked + .desktop-channel-check { + border-color: rgba(97, 128, 255, 0.94); + background: + linear-gradient(135deg, rgba(92, 121, 255, 0.96), rgba(61, 102, 214, 0.92)), + rgba(8, 10, 16, 0.46); +} + +.desktop-channel-toggle input:checked + .desktop-channel-check::after { + content: ""; + display: block; + width: 10px; + height: 6px; + margin: 5px 0 0 5px; + border-bottom: 2px solid #fff; + border-left: 2px solid #fff; + transform: rotate(-45deg); +} + +.desktop-channel-copy, +.desktop-channel-panel span:last-child { + display: flex; + min-width: 0; + flex-direction: column; + gap: 4px; +} + +.desktop-channel-copy strong, +.desktop-channel-panel strong { + color: var(--foreground); + font-size: 15px; + font-weight: 800; +} + +.desktop-channel-copy small, +.desktop-channel-panel small { + color: var(--muted); + font-size: 13px; + line-height: 1.35; +} + +.desktop-channel-panel { + display: grid; + grid-template-columns: 44px minmax(0, 1fr); + align-items: center; + gap: 14px; + width: min(760px, 100%); + padding: 17px 18px; + border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border)); + border-radius: 14px; + background: color-mix(in srgb, var(--primary) 7%, var(--surface)); + box-shadow: var(--shadow-soft); +} + +.desktop-channel-icon { + display: grid; + width: 44px; + height: 44px; + place-items: center; + border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border)); + border-radius: 12px; + color: var(--primary); + background: color-mix(in srgb, var(--primary) 10%, var(--surface)); +} + +:root[data-theme="dark"] .desktop-channel-panel { + border-color: rgba(94, 119, 255, 0.26); + background: rgba(17, 21, 34, 0.78); +} + +:root[data-theme="dark"] .desktop-channel-icon { + border-color: rgba(94, 119, 255, 0.35); + color: #8fa5ff; + background: rgba(10, 13, 24, 0.72); +} + +.search-grid { + max-width: 760px; + margin: 0 auto; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.dashboard-form { display: grid; grid-template-columns: minmax(160px, 245px) minmax(0, 1fr); gap: 16px; @@ -1773,7 +2110,7 @@ p { gap: 10px; } -.codex-auth-actions .button { +.codex-auth-actions .ui-button { min-height: 42px; } @@ -2062,53 +2399,415 @@ p { position: relative; height: calc(100vh - 58px); overflow: hidden; - padding: 20px 28px 28px; + padding: 12px 16px 16px; + background: var(--background); } -.dashboard-tabs { - position: fixed; - top: 8px; - right: 28px; - z-index: 30; +.dashboard-layout { + display: grid; + grid-template-columns: 248px minmax(0, 1fr); + gap: 12px; + height: 100%; + min-height: 0; +} + +.dashboard-layout-collapsed { + grid-template-columns: 64px minmax(0, 1fr); +} + +.dashboard-sidebar { display: flex; + min-width: 0; + min-height: 0; + flex-direction: column; + gap: 14px; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + padding: 10px; + box-shadow: var(--shadow-soft); +} + +.dashboard-sidebar-brand { + display: grid; + grid-template-columns: 36px minmax(0, 1fr); + align-items: center; gap: 10px; + min-height: 42px; +} + +.dashboard-sidebar-mark { + display: grid; + width: 36px; + height: 36px; + place-items: center; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface-muted); + color: var(--primary); +} + +.dashboard-sidebar-mark svg { + width: 19px; + height: 19px; +} + +.dashboard-sidebar-brand-copy { + min-width: 0; + transition: opacity 0.16s ease; +} + +.dashboard-sidebar-brand-copy strong, +.dashboard-sidebar-brand-copy span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dashboard-sidebar-brand-copy strong { + font-size: 14px; + font-weight: 650; +} + +.dashboard-sidebar-brand-copy span { + margin-top: 2px; + color: var(--muted); + font-size: 12px; + font-weight: 500; +} + +.dashboard-sidebar-nav { + display: grid; + gap: 4px; + min-height: 0; + overflow-y: auto; + padding: 2px 0; +} + +.dashboard-sidebar-item, +.dashboard-sidebar-collapse { + display: grid; + grid-template-columns: 20px minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + width: 100%; + min-height: 36px; + border: 1px solid transparent; + border-radius: 8px; + background: transparent; + color: var(--muted-strong); + padding: 0 10px; + font-size: 13px; + font-weight: 550; + text-align: left; +} + +.dashboard-sidebar-item:hover, +.dashboard-sidebar-collapse:hover, +.dashboard-sidebar-item-active { + border-color: var(--border-subtle); + background: var(--surface-muted); + color: var(--foreground); +} + +.dashboard-sidebar-item-active { + border-color: color-mix(in srgb, var(--primary) 45%, var(--border)); + background: color-mix(in srgb, var(--primary) 12%, var(--surface)); + color: var(--foreground); + box-shadow: inset 3px 0 0 var(--primary); +} + +.dashboard-sidebar-item svg, +.dashboard-sidebar-collapse svg { + width: 18px; + height: 18px; +} + +.dashboard-sidebar-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dashboard-sidebar-count { + display: inline-grid; + min-width: 22px; + height: 20px; + place-items: center; + border-radius: 6px; + background: var(--surface-muted); + color: color-mix(in srgb, var(--primary) 76%, var(--foreground)); + font-size: 11px; + font-variant-numeric: tabular-nums; +} + +.dashboard-sidebar-collapse { + margin-top: auto; -webkit-app-region: no-drag; } -.dashboard-tab { - display: inline-flex; +.dashboard-layout-collapsed .dashboard-sidebar { align-items: center; - gap: 7px; - min-height: 42px; - padding: 0 16px; + padding-inline: 8px; +} + +.dashboard-layout-collapsed .dashboard-sidebar-brand { + grid-template-columns: 36px; +} + +.dashboard-layout-collapsed .dashboard-sidebar-brand-copy, +.dashboard-layout-collapsed .dashboard-sidebar-label, +.dashboard-layout-collapsed .dashboard-sidebar-count { + display: none; +} + +.dashboard-layout-collapsed .dashboard-sidebar-item, +.dashboard-layout-collapsed .dashboard-sidebar-collapse { + grid-template-columns: 1fr; + justify-items: center; + width: 44px; + padding: 0; +} + +.dashboard-layout-collapsed .dashboard-sidebar-item-active { + box-shadow: inset 0 -3px 0 var(--primary); +} + +.dashboard-workspace { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 12px; + min-width: 0; + min-height: 0; +} + +.dashboard-workspace-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + min-height: 60px; border: 1px solid var(--border); - border-radius: 13px; + border-radius: 8px; background: var(--surface); - color: var(--muted-strong); - font-weight: 750; + padding: 10px 14px; + box-shadow: var(--shadow-soft); +} + +.dashboard-workspace-title { + min-width: 0; +} + +.dashboard-workspace-title span { + display: block; + overflow: hidden; + color: var(--muted); + font-size: 11px; + font-weight: 650; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; +} + +.dashboard-workspace-title h1 { + margin: 3px 0 0; + overflow: hidden; + font-size: 20px; + line-height: 1.16; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dashboard-workspace-status { + display: flex; + flex-shrink: 0; + align-items: center; + gap: 8px; + min-width: 0; +} + +.dashboard-header-octo { + display: grid; + grid-template-columns: 38px minmax(140px, 260px) auto; + align-items: center; + gap: 10px; + max-width: min(520px, 48vw); + min-height: 44px; + border: 1px solid var(--border-subtle); + border-radius: 8px; + background: var(--surface-muted); + padding: 4px 6px 4px 4px; +} + +.dashboard-header-octo-image { + display: block; + width: 38px; + height: 38px; + object-fit: contain; + filter: none; +} + +.dashboard-header-octo-copy { + min-width: 0; +} + +.dashboard-header-octo-copy strong, +.dashboard-header-octo-copy span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dashboard-header-octo-copy strong { + font-size: 13px; + font-weight: 650; + line-height: 1.2; +} + +.dashboard-header-octo-copy span { + margin-top: 3px; + color: var(--muted); + font-size: 12px; + font-weight: 500; + line-height: 1.2; +} + +.dashboard-content { + min-height: 0; + height: 100%; + overflow: auto; + padding-right: 4px; +} + +.dashboard-workspace > .dashboard-content { + border: 1px solid var(--border); + border-radius: 8px; + background: color-mix(in srgb, var(--surface-muted) 54%, var(--background)); + padding: 14px; + box-shadow: var(--shadow-soft); +} + +@media (max-width: 860px) { + .dashboard-layout { + grid-template-columns: 64px minmax(0, 1fr); + } + + .dashboard-sidebar { + align-items: center; + padding-inline: 8px; + } + + .dashboard-sidebar-brand { + grid-template-columns: 36px; + } + + .dashboard-sidebar-brand-copy, + .dashboard-sidebar-label, + .dashboard-sidebar-count { + display: none; + } + + .dashboard-sidebar-item, + .dashboard-sidebar-collapse { + grid-template-columns: 1fr; + justify-items: center; + width: 44px; + padding: 0; + } + + .dashboard-sidebar-item-active { + box-shadow: inset 0 -3px 0 var(--primary); + } +} + +.dashboard-control, +.dashboard-connectors-view, +.dashboard-skills-view, +.dashboard-workers-view, +.dashboard-system-view { + display: grid; + gap: 14px; + min-height: 100%; +} + +.dashboard-control-strip { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + min-width: 0; + border: 1px solid color-mix(in srgb, var(--primary) 42%, var(--border)); + border-radius: 8px; + background: color-mix(in srgb, var(--primary) 10%, var(--surface)); + padding: 10px 12px; +} + +.dashboard-control-strip > div { + min-width: 0; +} + +.dashboard-control-strip strong, +.dashboard-control-strip span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dashboard-control-strip strong { + font-size: 13px; + font-weight: 650; +} + +.dashboard-control-strip span { + margin-top: 3px; + color: var(--muted); + font-size: 12px; +} + +.dashboard-page-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + min-width: 0; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + padding: 10px 12px; } -.dashboard-tab-active { - border-color: color-mix(in srgb, var(--primary) 48%, var(--border)); - background: color-mix(in srgb, var(--primary) 16%, var(--surface)); - color: var(--foreground); +.dashboard-page-toolbar > div:first-child { + min-width: 0; } -.dashboard-content { - height: 100%; - overflow: auto; - padding-right: 4px; +.dashboard-page-toolbar strong, +.dashboard-page-toolbar span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.dashboard-control, -.dashboard-connectors-view, -.dashboard-skills-view, -.dashboard-workers-view, -.dashboard-system-view { - display: grid; - gap: 18px; - min-height: 100%; - padding-top: 4px; +.dashboard-page-toolbar strong { + font-size: 13px; + font-weight: 650; +} + +.dashboard-page-toolbar span { + margin-top: 3px; + color: var(--muted); + font-size: 12px; +} + +.dashboard-page-toolbar-actions { + display: flex; + flex-shrink: 0; + gap: 8px; } .chat-view { @@ -2127,17 +2826,10 @@ p { min-height: 0; overflow-y: auto; border: 1px solid var(--border); - border-radius: 22px; - background: - linear-gradient( - 180deg, - color-mix(in srgb, var(--surface-strong) 72%, transparent), - transparent 260px - ), - color-mix(in srgb, var(--surface) 78%, transparent); - padding: 18px; + border-radius: 8px; + background: var(--surface); + padding: 14px; box-shadow: var(--shadow-soft); - backdrop-filter: blur(22px); } .chat-empty { @@ -2163,11 +2855,11 @@ p { .chat-bubble, .chat-event { margin-bottom: 12px; - padding: 13px 15px; + padding: 11px 13px; border: 1px solid var(--border); - border-radius: 18px; - background: var(--surface-strong); - box-shadow: 0 12px 32px rgba(19, 27, 45, 0.08); + border-radius: 8px; + background: var(--surface); + box-shadow: none; } .chat-bubble { @@ -2175,10 +2867,15 @@ p { max-width: min(78%, 820px); } +.chat-bubble .chat-markdown { + color: color-mix(in srgb, var(--foreground) 96%, white 4%); + font-weight: 500; +} + .chat-bubble-user { margin-left: auto; border-color: color-mix(in srgb, var(--primary) 34%, var(--border)); - background: color-mix(in srgb, var(--primary) 15%, var(--surface-strong)); + background: color-mix(in srgb, var(--primary) 10%, var(--surface)); } .chat-bubble-assistant { @@ -2188,7 +2885,7 @@ p { .chat-event { width: min(88%, 900px); margin-right: auto; - background: color-mix(in srgb, var(--accent) 7%, var(--surface-strong)); + background: color-mix(in srgb, var(--accent) 6%, var(--surface)); } .chat-event-technical { @@ -2210,7 +2907,7 @@ p { .chat-event-error, .chat-event-warning { border-color: color-mix(in srgb, var(--danger) 38%, var(--border)); - background: color-mix(in srgb, var(--danger) 8%, var(--surface-strong)); + background: color-mix(in srgb, var(--danger) 8%, var(--surface)); } .chat-item-meta { @@ -2229,6 +2926,8 @@ p { } .chat-markdown { + max-width: 100%; + overflow-x: auto; color: var(--foreground); font-size: 14px; line-height: 1.5; @@ -2306,6 +3005,55 @@ p { overflow-wrap: anywhere; } +.chat-markdown table { + width: 100%; + min-width: min(520px, 100%); + margin: 10px 0; + overflow: hidden; + border: 1px solid var(--border); + border-collapse: separate; + border-spacing: 0; + border-radius: 8px; + background: color-mix(in srgb, var(--surface-muted) 38%, transparent); + font-size: 13px; + line-height: 1.42; +} + +.chat-markdown th, +.chat-markdown td { + padding: 8px 10px; + border-right: 1px solid var(--border-subtle); + border-bottom: 1px solid var(--border-subtle); + text-align: left; + vertical-align: top; +} + +.chat-markdown th:last-child, +.chat-markdown td:last-child { + border-right: 0; +} + +.chat-markdown tr:last-child td { + border-bottom: 0; +} + +.chat-markdown th { + background: color-mix(in srgb, var(--surface-muted) 72%, var(--surface)); + color: var(--foreground); + font-size: 12px; + font-weight: 750; + white-space: nowrap; +} + +.chat-markdown td { + color: var(--muted-strong); + font-weight: 560; +} + +.chat-bubble-assistant .chat-markdown td { + color: color-mix(in srgb, var(--foreground) 84%, var(--muted)); +} + .chat-image-previews { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 220px)); @@ -2323,45 +3071,45 @@ p { object-fit: cover; } -.chat-thinking { - width: fit-content; - min-width: 120px; -} - -.chat-thinking-dots { +.chat-activity { display: inline-flex; align-items: center; - gap: 5px; - min-height: 20px; -} - -.chat-thinking-dots span { - width: 7px; - height: 7px; - border-radius: 999px; - background: color-mix(in srgb, var(--muted-strong) 76%, var(--foreground)); - animation: chat-thinking-dot 1s ease-in-out infinite; -} - -.chat-thinking-dots span:nth-child(2) { - animation-delay: 0.14s; + max-width: min(78%, 760px); + min-height: 32px; + margin: 2px 0 14px 4px; + padding: 0 2px; + color: var(--muted-strong); + font-size: 13px; + font-weight: 650; } -.chat-thinking-dots span:nth-child(3) { - animation-delay: 0.28s; +.chat-activity-text { + overflow: hidden; + background: + linear-gradient( + 110deg, + var(--muted-strong) 0%, + var(--muted-strong) 34%, + var(--foreground) 48%, + var(--accent) 56%, + var(--muted-strong) 70%, + var(--muted-strong) 100% + ); + background-size: 260% 100%; + background-clip: text; + color: transparent; + text-overflow: ellipsis; + white-space: nowrap; + animation: chat-activity-shimmer 2.2s linear infinite; } -@keyframes chat-thinking-dot { - 0%, - 80%, - 100% { - opacity: 0.35; - transform: translateY(0); +@keyframes chat-activity-shimmer { + from { + background-position: 130% 0; } - 40% { - opacity: 1; - transform: translateY(-4px); + to { + background-position: -130% 0; } } @@ -2383,11 +3131,10 @@ p { display: grid; gap: 8px; border: 1px solid var(--border); - border-radius: 22px; + border-radius: 8px; background: var(--surface); - padding: 12px; + padding: 10px; box-shadow: var(--shadow-soft); - backdrop-filter: blur(22px); } .chat-composer-row { @@ -2398,8 +3145,8 @@ p { } .chat-attach-button { - width: 44px; - min-width: 44px; + width: 36px; + min-width: 36px; padding: 0; } @@ -2409,14 +3156,14 @@ p { } .chat-composer textarea { - min-height: 64px; + min-height: 58px; max-height: 150px; resize: vertical; border: 1px solid var(--border); - border-radius: 16px; + border-radius: 8px; background: var(--input); color: var(--foreground); - padding: 12px 13px; + padding: 10px 12px; line-height: 1.4; outline: none; } @@ -2438,12 +3185,12 @@ p { align-items: center; gap: 8px; border: 1px solid var(--border); - border-radius: 999px; - background: var(--surface-strong); + border-radius: 6px; + background: var(--surface-muted); color: var(--muted-strong); padding: 6px 8px 6px 11px; font-size: 12px; - font-weight: 760; + font-weight: 650; } .chat-attachment span { @@ -2491,6 +3238,13 @@ p { padding-right: 210px; } +.dashboard-assistant-head-compact { + display: flex; + align-items: stretch; + gap: 14px; + padding-right: 0; +} + .dashboard-octo { width: 92px; height: 92px; @@ -2523,8 +3277,8 @@ p { max-height: 260px; padding: 18px 24px; border: 1px solid var(--border); - border-radius: 22px; - background: var(--surface-strong); + border-radius: 8px; + background: var(--surface); box-shadow: var(--shadow-soft); overflow: hidden; } @@ -2543,6 +3297,19 @@ p { transform: rotate(45deg); } +.dashboard-bubble-compact { + flex: 1 1 auto; + min-width: 0; + width: 100%; + max-width: none; + max-height: none; + padding: 16px 18px; +} + +.dashboard-bubble-compact::before { + content: none; +} + .dashboard-bubble h1 { display: -webkit-box; overflow: hidden; @@ -2553,6 +3320,11 @@ p { -webkit-line-clamp: 3; } +.dashboard-bubble-compact h1 { + font-size: 20px; + -webkit-line-clamp: 2; +} + .dashboard-bubble p { display: -webkit-box; overflow: hidden; @@ -2568,6 +3340,10 @@ p { -webkit-line-clamp: 4; } +.dashboard-bubble-compact .dashboard-octo-detail { + -webkit-line-clamp: 2; +} + .dashboard-bubble .dashboard-latest { -webkit-line-clamp: 2; } @@ -2589,10 +3365,10 @@ p { grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14px; - padding: 16px 18px; + padding: 12px 14px; border: 1px solid color-mix(in srgb, var(--danger) 62%, var(--border)); - border-radius: 18px; - background: color-mix(in srgb, var(--danger) 10%, var(--surface-strong)); + border-radius: 8px; + background: color-mix(in srgb, var(--danger) 9%, var(--surface)); box-shadow: var(--shadow-soft); } @@ -2602,7 +3378,7 @@ p { color: color-mix(in srgb, var(--danger) 78%, var(--foreground)); } -.dashboard-attention-panel h2 { +.dashboard-attention-panel .ui-alert-title { margin: 2px 0 0; overflow-wrap: anywhere; color: var(--foreground); @@ -2610,7 +3386,7 @@ p { line-height: 1.25; } -.dashboard-attention-panel p { +.dashboard-attention-panel .ui-alert-description { display: -webkit-box; margin: 6px 0 0; overflow: hidden; @@ -2639,55 +3415,19 @@ p { text-transform: none; } -.dashboard-panel { - min-width: 0; - border: 1px solid var(--border); - border-radius: 22px; - background: var(--surface); - box-shadow: var(--shadow-soft); - backdrop-filter: blur(22px); -} - -.dashboard-panel-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; - padding: 18px 20px 15px; - border-bottom: 1px solid var(--border); -} - -.dashboard-panel-head h2, -.system-card h2 { - margin: 0; - font-size: 14px; - font-weight: 800; - letter-spacing: 0; - text-transform: uppercase; -} - -.dashboard-panel-head p, -.system-card p { - margin: 5px 0 0; - color: var(--muted); - font-size: 13px; - line-height: 1.45; -} - -.dashboard-pill, .dashboard-status { display: inline-flex; align-items: center; justify-content: center; - min-height: 34px; + min-height: 24px; max-width: 100%; - padding: 0 12px; + padding: 0 8px; border: 1px solid var(--border); - border-radius: 999px; - background: color-mix(in srgb, var(--surface-strong) 78%, transparent); + border-radius: 6px; + background: var(--surface-muted); color: var(--muted-strong); font-size: 12px; - font-weight: 750; + font-weight: 650; white-space: nowrap; } @@ -2700,35 +3440,35 @@ p { .dashboard-status-live { border-color: color-mix(in srgb, var(--accent) 46%, var(--border)); - background: color-mix(in srgb, var(--accent) 12%, var(--surface-strong)); + background: color-mix(in srgb, var(--accent) 10%, var(--surface)); color: color-mix(in srgb, var(--accent) 72%, var(--foreground)); } .dashboard-status-good { border-color: color-mix(in srgb, var(--success) 50%, var(--border)); - background: color-mix(in srgb, var(--success) 11%, var(--surface-strong)); + background: color-mix(in srgb, var(--success) 10%, var(--surface)); color: color-mix(in srgb, var(--success) 72%, var(--foreground)); } .dashboard-status-warn { border-color: color-mix(in srgb, #f4b84f 54%, var(--border)); - background: color-mix(in srgb, #f4b84f 10%, var(--surface-strong)); + background: color-mix(in srgb, #f4b84f 10%, var(--surface)); color: color-mix(in srgb, #f4b84f 74%, var(--foreground)); } .dashboard-status-bad { border-color: color-mix(in srgb, var(--danger) 58%, var(--border)); - background: color-mix(in srgb, var(--danger) 10%, var(--surface-strong)); + background: color-mix(in srgb, var(--danger) 10%, var(--surface)); color: color-mix(in srgb, var(--danger) 76%, var(--foreground)); } .dashboard-chart { position: relative; height: 220px; - margin: 10px 18px 16px; + margin: 10px 16px 14px; overflow: hidden; border: 1px solid var(--border); - border-radius: 18px; + border-radius: 8px; background: linear-gradient( color-mix(in srgb, var(--border) 52%, transparent) 1px, @@ -2739,7 +3479,7 @@ p { color-mix(in srgb, var(--border) 44%, transparent) 1px, transparent 1px ), - color-mix(in srgb, var(--surface-strong) 72%, transparent); + var(--surface); background-size: 100% 58px, 92px 100%, @@ -2758,15 +3498,15 @@ p { grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 9px; - min-height: 38px; + min-height: 32px; min-width: min(220px, 100%); padding: 0 12px; border: 1px solid var(--border); - border-radius: 999px; - background: color-mix(in srgb, var(--surface-strong) 72%, transparent); + border-radius: 8px; + background: var(--surface); color: var(--muted-strong); font-size: 12px; - font-weight: 750; + font-weight: 650; } .dashboard-load-chip strong { @@ -2821,49 +3561,151 @@ p { bottom: 10px; } -.dashboard-chart svg { - position: absolute; - inset: 18px; - width: calc(100% - 36px); - height: calc(100% - 36px); +.dashboard-chart svg { + position: absolute; + inset: 18px; + width: calc(100% - 36px); + height: calc(100% - 36px); +} + +.dashboard-inline-error, +.dashboard-inline-notice { + margin: 0; + padding: 12px 14px; + border: 1px solid color-mix(in srgb, var(--danger) 60%, var(--border)); + border-radius: 8px; + background: color-mix(in srgb, var(--danger) 8%, var(--surface)); + color: var(--foreground); + font-size: 13px; + line-height: 1.45; +} + +.dashboard-inline-error { + margin: 0 18px 18px; +} + +.dashboard-inline-notice { + border-color: color-mix(in srgb, var(--success) 48%, var(--border)); + background: color-mix(in srgb, var(--success) 8%, var(--surface)); +} + +.connector-management-grid { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 10px; +} + +.connector-management-panel { + align-self: start; + overflow: hidden; +} + +.connector-management-trigger { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 16px; + width: 100%; + min-height: 72px; + padding: 14px 16px; + border: 0; + background: transparent; + color: var(--foreground); + text-align: left; +} + +.connector-management-trigger:hover { + background: color-mix(in srgb, var(--surface-muted) 46%, transparent); +} + +.connector-management-title { + display: grid; + gap: 4px; + min-width: 0; +} + +.connector-management-title-row { + display: grid; + grid-template-columns: 40px minmax(0, 1fr); + align-items: center; + gap: 12px; + min-width: 0; +} + +.connector-provider-icon { + display: grid; + width: 40px; + height: 40px; + place-items: center; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface-muted); + color: var(--foreground); +} + +.connector-provider-icon svg { + width: 19px; + height: 19px; +} + +.connector-provider-icon-google { + border-color: color-mix(in srgb, #4285f4 42%, var(--border)); + background: + linear-gradient(var(--surface), var(--surface)) padding-box, + conic-gradient(from -45deg, #4285f4, #34a853, #fbbc05, #ea4335, #4285f4) border-box; + color: #4285f4; + font-size: 18px; + font-weight: 800; +} + +.connector-provider-icon-github { + background: color-mix(in srgb, var(--foreground) 8%, var(--surface-muted)); } -.dashboard-inline-error, -.dashboard-inline-notice { - margin: 0; - padding: 12px 14px; - border: 1px solid color-mix(in srgb, var(--danger) 60%, var(--border)); - border-radius: 14px; - background: color-mix(in srgb, var(--danger) 8%, var(--surface-strong)); +.connector-management-title strong { + overflow: hidden; color: var(--foreground); - font-size: 13px; - line-height: 1.45; + font-size: 15px; + font-weight: 650; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; } -.dashboard-inline-error { - margin: 0 18px 18px; +.connector-management-title span, +.connector-management-summary > span { + overflow: hidden; + color: var(--muted); + font-size: 13px; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; } -.dashboard-inline-notice { - border-color: color-mix(in srgb, var(--success) 48%, var(--border)); - background: color-mix(in srgb, var(--success) 8%, var(--surface-strong)); +.connector-management-summary { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + min-width: 0; } -.connector-management-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 18px; +.connector-management-summary svg { + width: 17px; + height: 17px; + color: var(--muted); + transition: transform 0.14s ease; } -.connector-management-panel { - align-self: start; - overflow: hidden; +.connector-management-panel-expanded .connector-management-summary svg { + transform: rotate(180deg); } .connector-management-body { display: grid; gap: 14px; - padding: 18px; + border-top: 1px solid var(--border-subtle); + padding: 14px; } .connector-enable-row { @@ -2871,10 +3713,10 @@ p { width: fit-content; align-items: center; gap: 10px; - min-height: 38px; + min-height: 34px; color: var(--muted-strong); font-size: 13px; - font-weight: 760; + font-weight: 550; } .connector-enable-row input { @@ -2888,11 +3730,11 @@ p { grid-template-columns: auto minmax(0, 1fr); align-items: start; gap: 10px; - min-height: 52px; - padding: 12px 13px; + min-height: 48px; + padding: 10px 12px; border: 1px solid var(--border); - border-radius: 14px; - background: color-mix(in srgb, var(--surface-strong) 76%, transparent); + border-radius: 8px; + background: var(--surface-muted); color: var(--muted-strong); font-size: 13px; line-height: 1.45; @@ -2915,8 +3757,8 @@ p { margin: 18px; overflow: auto; border: 1px solid var(--border); - border-radius: 18px; - background: color-mix(in srgb, var(--surface-strong) 74%, transparent); + border-radius: 8px; + background: var(--surface); } .dashboard-worker-row { @@ -3007,29 +3849,34 @@ p { z-index: 70; display: grid; padding: 18px; - background: color-mix(in srgb, var(--background) 84%, transparent); - backdrop-filter: blur(18px); + background: color-mix(in srgb, var(--background) 88%, transparent); } .worker-detail-modal { display: grid; grid-template-rows: auto auto minmax(0, 1fr); + align-self: center; + justify-self: center; + width: min(1120px, calc(100vw - 36px)); + height: min(760px, calc(100vh - 36px)); + max-width: 100%; + max-height: calc(100vh - 36px); min-width: 0; min-height: 0; overflow: hidden; border: 1px solid var(--border); - border-radius: 26px; - background: color-mix(in srgb, var(--surface-strong) 94%, var(--background)); - box-shadow: var(--shadow-strong); + border-radius: 8px; + background: var(--surface); + box-shadow: var(--shadow); } .worker-detail-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: flex-start; - gap: 18px; - padding: 22px 24px 18px; - border-bottom: 1px solid var(--border); + gap: 14px; + padding: 16px 18px; + border-bottom: 1px solid var(--border-subtle); } .worker-detail-header > div:first-child { @@ -3040,7 +3887,7 @@ p { margin: 0 0 8px; color: var(--accent); font-size: 11px; - font-weight: 850; + font-weight: 650; text-transform: uppercase; } @@ -3048,7 +3895,7 @@ p { max-width: 820px; margin: 0; overflow-wrap: anywhere; - font-size: 25px; + font-size: 20px; line-height: 1.15; } @@ -3062,7 +3909,7 @@ p { min-width: 0; } -.worker-detail-header-actions .button { +.worker-detail-header-actions .ui-button { min-width: 0; padding-right: 14px; padding-left: 14px; @@ -3077,9 +3924,9 @@ p { .worker-detail-summary { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); - gap: 10px; - padding: 14px 18px; - border-bottom: 1px solid var(--border); + gap: 8px; + padding: 12px 14px; + border-bottom: 1px solid var(--border-subtle); } .worker-detail-summary > div { @@ -3091,10 +3938,10 @@ p { column-gap: 8px; align-items: center; min-width: 0; - padding: 11px 12px; + padding: 10px 12px; border: 1px solid var(--border); - border-radius: 16px; - background: color-mix(in srgb, var(--surface) 78%, transparent); + border-radius: 8px; + background: var(--surface-muted); } .worker-detail-summary svg { @@ -3108,7 +3955,7 @@ p { grid-area: label; color: var(--muted); font-size: 11px; - font-weight: 800; + font-weight: 650; text-transform: uppercase; } @@ -3145,31 +3992,34 @@ p { .worker-detail-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 360px); - gap: 16px; + gap: 12px; min-height: 0; - padding: 18px; + padding: 14px; overflow: hidden; } .worker-detail-main, .worker-detail-side { - display: grid; - align-content: start; - gap: 14px; + display: flex; + flex-direction: column; + gap: 12px; min-height: 0; overflow: auto; padding-right: 4px; } .worker-detail-section { + flex: 0 0 auto; min-width: 0; - padding: 15px; + overflow: visible; + padding: 12px; border: 1px solid var(--border); - border-radius: 18px; - background: color-mix(in srgb, var(--surface) 82%, transparent); + border-radius: 8px; + background: var(--surface); } .worker-detail-section-result { + border-color: color-mix(in srgb, var(--primary) 32%, var(--border)); background: color-mix(in srgb, var(--primary) 7%, var(--surface)); } @@ -3184,8 +4034,7 @@ p { .worker-detail-section-head h3 { margin: 0; font-size: 13px; - font-weight: 850; - text-transform: uppercase; + font-weight: 650; } .worker-detail-section-head span, @@ -3217,7 +4066,7 @@ p { .worker-detail-error { padding: 12px; border: 1px solid color-mix(in srgb, var(--danger) 48%, var(--border)); - border-radius: 14px; + border-radius: 8px; background: color-mix(in srgb, var(--danger) 9%, transparent); color: color-mix(in srgb, var(--danger) 70%, var(--foreground)); } @@ -3310,8 +4159,8 @@ p { overflow: auto; padding: 13px; border: 1px solid var(--border); - border-radius: 14px; - background: color-mix(in srgb, var(--background) 42%, var(--surface-strong)); + border-radius: 8px; + background: var(--input); color: var(--muted-strong); font-size: 12px; line-height: 1.45; @@ -3358,16 +4207,16 @@ p { overflow-wrap: anywhere; padding: 7px 9px; border: 1px solid color-mix(in srgb, var(--accent) 38%, var(--border)); - border-radius: 999px; - background: color-mix(in srgb, var(--accent) 10%, transparent); + border-radius: 6px; + background: color-mix(in srgb, var(--accent) 10%, var(--surface)); color: color-mix(in srgb, var(--accent) 72%, var(--foreground)); font-size: 11px; - font-weight: 750; + font-weight: 650; } .worker-tool-cloud-muted span { border-color: var(--border); - background: color-mix(in srgb, var(--surface-strong) 76%, transparent); + background: var(--surface-muted); color: var(--muted-strong); } @@ -3393,7 +4242,7 @@ p { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; - padding: 14px; + padding: 12px; } .worker-template-card { @@ -3401,10 +4250,10 @@ p { display: grid; gap: 8px; width: 100%; - padding: 14px 44px 14px 14px; + padding: 12px 40px 12px 12px; border: 1px solid var(--border); - border-radius: 18px; - background: var(--surface-strong); + border-radius: 8px; + background: var(--surface); color: var(--foreground); text-align: left; } @@ -3450,11 +4299,14 @@ p { } .skill-install-panel { + overflow: visible; +} + +.skill-install-panel > .ui-card-content { display: grid; - grid-template-columns: minmax(0, 1.4fr) minmax(220px, 0.7fr) auto; + grid-template-columns: minmax(260px, 1.1fr) minmax(220px, 0.8fr) auto; align-items: end; gap: 12px; - padding: 16px; } .skill-install-actions { @@ -3466,22 +4318,17 @@ p { .skills-grid { display: grid; - gap: 16px; + gap: 14px; } .skill-list-panel { min-height: 480px; } -.skill-list-panel .dashboard-panel-head { - align-items: flex-start; - gap: 12px; -} - .skill-list { display: grid; gap: 10px; - padding: 14px; + padding: 12px; } .skill-card { @@ -3489,10 +4336,10 @@ p { grid-template-columns: minmax(0, 1fr) auto; gap: 8px 12px; width: 100%; - padding: 14px; + padding: 12px; border: 1px solid var(--border); - border-radius: 18px; - background: var(--surface-strong); + border-radius: 8px; + background: var(--surface); color: var(--foreground); text-align: left; } @@ -3500,6 +4347,7 @@ p { .skill-card:hover, .skill-card-active { border-color: color-mix(in srgb, var(--primary) 46%, var(--border)); + background: color-mix(in srgb, var(--primary) 6%, var(--surface)); } .skill-card strong { @@ -3525,13 +4373,13 @@ p { gap: 8px; color: var(--muted); font-size: 11px; - font-weight: 800; + font-weight: 650; text-transform: uppercase; } .skill-detail-panel { min-height: 480px; - padding: 20px; + padding: 16px; } .skill-detail-head { @@ -3545,14 +4393,14 @@ p { margin: 0 0 8px; color: var(--muted); font-size: 11px; - font-weight: 850; + font-weight: 650; letter-spacing: 0.14em; text-transform: uppercase; } .skill-detail-head h2 { margin: 0; - font-size: 24px; + font-size: 20px; letter-spacing: 0; overflow-wrap: anywhere; } @@ -3574,7 +4422,7 @@ p { color: var(--accent); cursor: pointer; font: inherit; - font-weight: 750; + font-weight: 650; text-align: left; } @@ -3614,8 +4462,8 @@ p { margin-top: 16px; padding: 14px; border: 1px solid color-mix(in srgb, #f4b84f 54%, var(--border)); - border-radius: 18px; - background: color-mix(in srgb, #f4b84f 9%, var(--surface-strong)); + border-radius: 8px; + background: color-mix(in srgb, #f4b84f 9%, var(--surface)); } .skill-attention svg { @@ -3648,14 +4496,14 @@ p { min-width: 0; padding: 12px; border: 1px solid var(--border); - border-radius: 16px; - background: var(--surface-strong); + border-radius: 8px; + background: var(--surface-muted); } .skill-facts dt { color: var(--muted); font-size: 11px; - font-weight: 850; + font-weight: 650; text-transform: uppercase; } @@ -3704,8 +4552,8 @@ p { margin: 16px 0 0; padding: 12px; border: 1px solid var(--border); - border-radius: 16px; - background: var(--surface-strong); + border-radius: 8px; + background: var(--surface-muted); color: var(--muted-strong); font-size: 13px; line-height: 1.45; @@ -3718,7 +4566,7 @@ p { } .system-card { - padding: 22px 20px; + padding: 16px; } .system-card { @@ -3730,13 +4578,76 @@ p { margin-top: 2px; } -.system-card .system-actions .button { +.system-card .system-actions .ui-button { min-height: 44px; } +.system-preferences { + display: grid; + gap: 10px; +} + +.system-preference-control { + display: grid; + grid-template-columns: 36px minmax(0, 1fr); + align-items: center; + gap: 12px; + min-width: 0; + min-height: 58px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface-muted); + padding: 10px 12px; +} + +.system-preference-icon { + display: grid; + width: 36px; + height: 36px; + place-items: center; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + color: var(--primary); +} + +.system-preference-icon svg { + width: 18px; + height: 18px; +} + +.system-preference-control > span:last-child { + display: grid; + gap: 5px; + min-width: 0; +} + +.system-preference-control strong { + color: var(--muted); + font-size: 11px; + font-weight: 750; + text-transform: uppercase; +} + +.system-preference-control select { + width: 100%; + min-width: 0; + border: 0; + background: transparent; + color: var(--foreground); + font-size: 14px; + font-weight: 650; + outline: 0; +} + +.system-preference-control:focus-within { + border-color: color-mix(in srgb, var(--primary) 58%, var(--border)); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 14%, transparent); +} + .system-grid { display: grid; - gap: 16px; + gap: 14px; } @media (min-width: 900px) { @@ -3747,6 +4658,10 @@ p { .system-card:not(.system-card-half) { grid-column: 1 / -1; } + + .system-preferences { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } .service-pills { @@ -3769,8 +4684,8 @@ p { gap: 14px; padding: 12px 14px; border: 1px solid color-mix(in srgb, var(--success) 34%, var(--border)); - border-radius: 14px; - background: color-mix(in srgb, var(--success) 8%, transparent); + border-radius: 8px; + background: color-mix(in srgb, var(--success) 7%, var(--surface)); } .mcp-server-card strong, @@ -3804,8 +4719,8 @@ p { min-width: 86px; padding: 8px 10px; border: 1px solid var(--border); - border-radius: 12px; - background: color-mix(in srgb, var(--surface-strong) 74%, transparent); + border-radius: 8px; + background: var(--surface-muted); } .mcp-server-card dt, @@ -3857,7 +4772,6 @@ p { place-items: center; padding: 28px; background: rgba(5, 8, 14, 0.42); - backdrop-filter: blur(14px); } .template-modal { @@ -3867,8 +4781,8 @@ p { flex-direction: column; overflow: hidden; border: 1px solid var(--border); - border-radius: 24px; - background: var(--surface-strong); + border-radius: 8px; + background: var(--surface); box-shadow: var(--shadow); } @@ -3877,12 +4791,12 @@ p { display: flex; align-items: center; gap: 12px; - padding: 18px 20px; - border-bottom: 1px solid var(--border); + padding: 16px 18px; + border-bottom: 1px solid var(--border-subtle); } .template-modal footer { - border-top: 1px solid var(--border); + border-top: 1px solid var(--border-subtle); border-bottom: 0; } @@ -3898,12 +4812,12 @@ p { .template-icon-button { display: grid; - width: 42px; - height: 42px; + width: 36px; + height: 36px; margin-left: auto; place-items: center; border: 1px solid var(--border); - border-radius: 13px; + border-radius: 8px; background: transparent; color: var(--foreground); } @@ -3916,8 +4830,8 @@ p { .template-modal-body { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; - padding: 18px 20px; + gap: 12px; + padding: 16px 18px; overflow: auto; } @@ -3930,17 +4844,17 @@ p { .template-check { color: var(--muted-strong); font-size: 12px; - font-weight: 750; + font-weight: 650; text-transform: uppercase; } .template-field input, .template-field textarea { width: 100%; - min-height: 44px; + min-height: 38px; padding: 10px 12px; border: 1px solid var(--border); - border-radius: 14px; + border-radius: 8px; background: var(--input); color: var(--foreground); outline: 0; @@ -3963,10 +4877,10 @@ p { display: flex; align-items: center; gap: 10px; - min-height: 44px; + min-height: 38px; padding: 0 12px; border: 1px solid var(--border); - border-radius: 14px; + border-radius: 8px; background: var(--input); } @@ -4200,7 +5114,7 @@ p { padding-top: 12px; } - .setup-footer .button { + .setup-footer .ui-button { min-width: 0; flex: 1; } @@ -4239,7 +5153,7 @@ p { grid-template-columns: auto minmax(0, 1fr); } - .chat-composer-row .button { + .chat-composer-row .ui-button { width: 100%; } @@ -4247,7 +5161,7 @@ p { width: 44px; } - .chat-composer-row .button:not(.chat-attach-button) { + .chat-composer-row .ui-button:not(.chat-attach-button) { grid-column: 1 / -1; } @@ -4264,12 +5178,24 @@ p { grid-column: 1 / -1; } - .connector-management-grid, .connector-form { grid-template-columns: minmax(0, 1fr); } - .connector-management-actions .button { + .connector-management-trigger { + grid-template-columns: minmax(0, 1fr); + gap: 10px; + } + + .connector-management-summary { + justify-content: flex-start; + } + + .connector-management-summary > span { + flex: 1 1 auto; + } + + .connector-management-actions .ui-button { flex: 1 1 160px; } @@ -4295,7 +5221,10 @@ p { } .worker-detail-modal { - border-radius: 20px; + width: calc(100vw - 20px); + height: calc(100vh - 20px); + max-height: calc(100vh - 20px); + border-radius: 8px; } .worker-detail-header, @@ -4328,7 +5257,7 @@ p { } .worker-studio-grid, - .skill-install-panel, + .skill-install-panel > .ui-card-content, .skill-facts, .template-modal-body { grid-template-columns: 1fr; @@ -4361,4 +5290,13 @@ p { .dashboard-octo-thinking { animation: none !important; } + + .chat-activity-text { + animation: none !important; + } + + .chat-activity-text { + background: none; + color: var(--muted-strong); + } } diff --git a/src/octopal/channels/__init__.py b/src/octopal/channels/__init__.py index 483bab31..f396b9b6 100644 --- a/src/octopal/channels/__init__.py +++ b/src/octopal/channels/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations DEFAULT_USER_CHANNEL = "telegram" -SUPPORTED_USER_CHANNELS = ("telegram", "whatsapp") +SUPPORTED_USER_CHANNELS = ("telegram", "whatsapp", "desktop") def normalize_user_channel(value: str | None) -> str: @@ -15,4 +15,6 @@ def user_channel_label(value: str | None) -> str: normalized = normalize_user_channel(value) if normalized == "whatsapp": return "WhatsApp" + if normalized == "desktop": + return "Desktop" return "Telegram" diff --git a/src/octopal/cli/configure.py b/src/octopal/cli/configure.py index f3511e6a..efd6c8b6 100644 --- a/src/octopal/cli/configure.py +++ b/src/octopal/cli/configure.py @@ -9,7 +9,7 @@ from rich.rule import Rule from rich.table import Table -from octopal.channels import normalize_user_channel +from octopal.channels import DEFAULT_USER_CHANNEL, normalize_user_channel from octopal.cli.branding import print_banner from octopal.cli.wizard import ( WizardConfirmParams, @@ -185,11 +185,14 @@ def configure_wizard() -> None: def _configure_user_channel(config: OctopalConfig, prompter) -> None: _print_section_header("Channel Access") + initial_channel = normalize_user_channel(config.user_channel) + if initial_channel == "desktop": + initial_channel = DEFAULT_USER_CHANNEL channel = prompter.select( WizardSelectParams( message="Primary communication channel", - initial_value=normalize_user_channel(config.user_channel), + initial_value=initial_channel, options=[ WizardSelectOption( value="telegram", diff --git a/src/octopal/cli/main.py b/src/octopal/cli/main.py index 1778d5c6..440c7663 100644 --- a/src/octopal/cli/main.py +++ b/src/octopal/cli/main.py @@ -805,6 +805,12 @@ async def run_all(): octo = await whatsapp_runtime.start() gateway_app = _build_gateway_app(settings, octo) gateway_app.state.whatsapp_runtime = whatsapp_runtime + elif selected_channel == "desktop": + from octopal.runtime.app import build_octo + + octo = build_octo(settings) + await octo.initialize_system(bot=None, allowed_chat_ids=[]) + gateway_app = _build_gateway_app(settings, octo) else: bot_instance = _build_telegram_bot(settings.telegram_bot_token) _dp, octo = build_dispatcher(settings, bot_instance) @@ -815,7 +821,7 @@ async def run_all(): server = uvicorn.Server(config) gateway_task = asyncio.create_task(server.serve()) try: - if selected_channel == "whatsapp": + if selected_channel in {"desktop", "whatsapp"}: await asyncio.Event().wait() else: await run_bot(settings, existing_octo=octo) @@ -823,9 +829,10 @@ async def run_all(): server.should_exit = True await gateway_task runtime = getattr(gateway_app.state, "whatsapp_runtime", None) - WhatsAppRuntime = _get_whatsapp_runtime_class() - if isinstance(runtime, WhatsAppRuntime): + if runtime is not None and isinstance(runtime, _get_whatsapp_runtime_class()): await runtime.stop() + elif selected_channel == "desktop": + await octo.stop_background_tasks() try: asyncio.run(run_all()) @@ -1106,6 +1113,8 @@ def status() -> None: "WhatsApp", f"[dim]mapped chats=[/dim]{whatsapp_metrics.get('chat_mappings', 0)} [dim]connected=[/dim]{whatsapp_metrics.get('connected', 0)}", ) + elif selected_channel == "desktop": + grid.add_row("Desktop Chat", "[dim]gateway=[/dim]websocket [dim]external bot=[/dim]disabled") else: grid.add_row("Telegram Chat", f"[dim]queues=[/dim]{telegram_metrics.get('chat_queues', 0)} [dim]tasks=[/dim]{telegram_metrics.get('send_tasks', 0)}") grid.add_row("Exec Sessions", f"[dim]running=[/dim]{exec_metrics.get('background_sessions_running', 0)} [dim]total=[/dim]{exec_metrics.get('background_sessions_total', 0)}") @@ -1459,6 +1468,8 @@ def _fmt_value(value: object, is_secret: bool) -> str: if _has_allowed_whatsapp_numbers(settings.allowed_whatsapp_numbers) else "[bright_red]SETUP NEEDED[/bright_red]" ) + elif selected_channel == "desktop": + profile_status = "[bright_green]READY[/bright_green]" else: profile_status = "[bright_green]READY[/bright_green]" if settings.telegram_bot_token.strip() else "[bright_red]SETUP NEEDED[/bright_red]" header.add_row("Profile", profile_status) @@ -1502,6 +1513,8 @@ def _fmt_value(value: object, is_secret: bool) -> str: if selected_channel == "whatsapp": if not _has_allowed_whatsapp_numbers(settings.allowed_whatsapp_numbers): checks.append("[bright_red]Set ALLOWED_WHATSAPP_NUMBERS for WhatsApp access[/bright_red]") + elif selected_channel == "desktop": + pass else: if not settings.telegram_bot_token.strip(): checks.append("[bright_red]Missing TELEGRAM_BOT_TOKEN[/bright_red]") diff --git a/src/octopal/gateway/dashboard.py b/src/octopal/gateway/dashboard.py index f9673e98..ef2a1598 100644 --- a/src/octopal/gateway/dashboard.py +++ b/src/octopal/gateway/dashboard.py @@ -2545,17 +2545,25 @@ def _select_active_channel_metrics( telegram_metrics: dict[str, Any], whatsapp_metrics: dict[str, Any], ) -> dict[str, Any]: - if active_channel == "whatsapp": - return { - "queue_depth": 0, - "send_tasks": None, - "connected": int(whatsapp_metrics.get("connected", 0) or 0), - "chat_mappings": int(whatsapp_metrics.get("chat_mappings", 0) or 0), - "updated_at": whatsapp_metrics.get("updated_at"), - } - return { - "queue_depth": int(telegram_metrics.get("chat_queues", 0) or 0), - "send_tasks": int(telegram_metrics.get("send_tasks", 0) or 0), + if active_channel == "whatsapp": + return { + "queue_depth": 0, + "send_tasks": None, + "connected": int(whatsapp_metrics.get("connected", 0) or 0), + "chat_mappings": int(whatsapp_metrics.get("chat_mappings", 0) or 0), + "updated_at": whatsapp_metrics.get("updated_at"), + } + if active_channel == "desktop": + return { + "queue_depth": 0, + "send_tasks": None, + "connected": None, + "chat_mappings": None, + "updated_at": None, + } + return { + "queue_depth": int(telegram_metrics.get("chat_queues", 0) or 0), + "send_tasks": int(telegram_metrics.get("send_tasks", 0) or 0), "connected": None, "chat_mappings": None, "updated_at": telegram_metrics.get("updated_at"), diff --git a/src/octopal/tools/catalog.py b/src/octopal/tools/catalog.py index 37adc03e..85931079 100644 --- a/src/octopal/tools/catalog.py +++ b/src/octopal/tools/catalog.py @@ -2094,7 +2094,12 @@ def _tool_gateway_status(args, ctx) -> str: exec_metrics = metrics.get("exec_run", {}) if isinstance(metrics, dict) else {} connectivity_metrics = metrics.get("connectivity", {}) if isinstance(metrics, dict) else {} scheduler_metrics = metrics.get("scheduler", {}) if isinstance(metrics, dict) else {} - active_channel_metrics = whatsapp_metrics if active_channel == "whatsapp" else telegram_metrics + if active_channel == "whatsapp": + active_channel_metrics = whatsapp_metrics + elif active_channel == "telegram": + active_channel_metrics = telegram_metrics + else: + active_channel_metrics = {} octo_status = build_octo_status(octo_metrics) last_heartbeat = status_data.get("last_internal_heartbeat_at") @@ -2353,10 +2358,12 @@ def _workspace_dir() -> Path: return Path(os.getenv("OCTOPAL_WORKSPACE_DIR", "workspace")).resolve() -def _gateway_channel_status(channel_id: str, channel_metrics: dict[str, object]) -> str: - if channel_id == "whatsapp": - connected = channel_metrics.get("connected") - if connected in {0}: +def _gateway_channel_status(channel_id: str, channel_metrics: dict[str, object]) -> str: + if channel_id == "desktop": + return "ok" + if channel_id == "whatsapp": + connected = channel_metrics.get("connected") + if connected in {0}: return "critical" return "ok" if connected in {1} else "warning" queue_depth = int(channel_metrics.get("chat_queues", 0) or 0) @@ -2367,9 +2374,11 @@ def _gateway_channel_status(channel_id: str, channel_metrics: dict[str, object]) return "ok" -def _gateway_channel_reason(channel_id: str, channel_metrics: dict[str, object]) -> str: - if channel_id == "whatsapp": - connected = channel_metrics.get("connected") +def _gateway_channel_reason(channel_id: str, channel_metrics: dict[str, object]) -> str: + if channel_id == "desktop": + return "desktop gateway" + if channel_id == "whatsapp": + connected = channel_metrics.get("connected") mappings = int(channel_metrics.get("chat_mappings", 0) or 0) if connected in {0}: return "bridge disconnected" diff --git a/tests/test_settings_config_sync.py b/tests/test_settings_config_sync.py index e5d8059c..3cd0544d 100644 --- a/tests/test_settings_config_sync.py +++ b/tests/test_settings_config_sync.py @@ -26,6 +26,18 @@ def test_load_settings_uses_user_channel_from_config_json(tmp_path, monkeypatch) assert settings.allowed_whatsapp_numbers == "+15551234567" +def test_load_settings_accepts_desktop_user_channel(tmp_path, monkeypatch) -> None: + (tmp_path / "config.json").write_text( + json.dumps({"user_channel": "desktop"}), + encoding="utf-8", + ) + monkeypatch.chdir(tmp_path) + + settings = load_settings() + + assert settings.user_channel == "desktop" + + def test_load_settings_defaults_to_empty_telegram_values_without_config_json( tmp_path, monkeypatch ) -> None: diff --git a/tests/test_state_active_channel.py b/tests/test_state_active_channel.py index 9e2cd03d..f16730a0 100644 --- a/tests/test_state_active_channel.py +++ b/tests/test_state_active_channel.py @@ -26,6 +26,14 @@ def test_write_start_status_persists_active_channel(tmp_path) -> None: assert payload["status_updated_at"] +def test_write_start_status_persists_desktop_active_channel(tmp_path) -> None: + settings = SimpleNamespace(state_dir=tmp_path, user_channel="desktop") + write_start_status(settings) + + payload = json.loads((tmp_path / "status.json").read_text(encoding="utf-8")) + assert payload["active_channel"] == "Desktop" + + def test_mark_runtime_running_updates_phase(tmp_path) -> None: settings = SimpleNamespace(state_dir=tmp_path, user_channel="telegram") write_start_status(settings)