From db0f8b3f7b14670fad2e77f8c4e5891aa8f6de5c Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Tue, 17 Feb 2026 00:07:23 +0800 Subject: [PATCH] refactor(desktop): drop legacy subagent dashboard wiring --- apps/desktop/src/main/electron-env.d.ts | 21 +-- apps/desktop/src/main/ipc/index.ts | 3 - apps/desktop/src/main/ipc/subagents.ts | 63 --------- apps/desktop/src/preload/index.ts | 30 +---- .../renderer/src/components/local-chat.tsx | 21 --- .../src/components/subagent-dashboard.tsx | 122 ------------------ .../src/components/subagent-status-bar.tsx | 77 ----------- .../src/hooks/use-subagent-polling.ts | 33 ----- .../src/renderer/src/stores/subagents.ts | 29 ----- 9 files changed, 7 insertions(+), 392 deletions(-) delete mode 100644 apps/desktop/src/main/ipc/subagents.ts delete mode 100644 apps/desktop/src/renderer/src/components/subagent-dashboard.tsx delete mode 100644 apps/desktop/src/renderer/src/components/subagent-status-bar.tsx delete mode 100644 apps/desktop/src/renderer/src/hooks/use-subagent-polling.ts delete mode 100644 apps/desktop/src/renderer/src/stores/subagents.ts diff --git a/apps/desktop/src/main/electron-env.d.ts b/apps/desktop/src/main/electron-env.d.ts index c4d13f11..70720780 100644 --- a/apps/desktop/src/main/electron-env.d.ts +++ b/apps/desktop/src/main/electron-env.d.ts @@ -162,21 +162,7 @@ interface InboundMessageEvent { timestamp: number } -interface SubagentRunInfo { - runId: string - label: string | undefined - task: string - status: 'queued' | 'running' | 'ok' | 'error' | 'timeout' | 'unknown' - groupId: string | undefined - groupLabel: string | undefined - startedAt: number | undefined - endedAt: number | undefined - createdAt: number - findings: string | undefined - error: string | undefined -} - -interface ElectronAPI { +interface ElectronAPI { app: { getFlags: () => Promise<{ forceOnboarding: boolean }> } @@ -251,10 +237,7 @@ interface ElectronAPI { stop: (channelId: string, accountId: string) => Promise<{ ok: boolean; error?: string }> start: (channelId: string, accountId: string) => Promise<{ ok: boolean; error?: string }> } - subagents: { - list: (requesterSessionId: string) => Promise - } - cron: { + cron: { list: () => Promise toggle: (jobId: string) => Promise<{ ok: boolean }> remove: (jobId: string) => Promise<{ ok: boolean }> diff --git a/apps/desktop/src/main/ipc/index.ts b/apps/desktop/src/main/ipc/index.ts index eed30e01..42abe57a 100644 --- a/apps/desktop/src/main/ipc/index.ts +++ b/apps/desktop/src/main/ipc/index.ts @@ -11,7 +11,6 @@ export { registerCronIpcHandlers } from './cron.js' export { registerHeartbeatIpcHandlers } from './heartbeat.js' export { registerAppStateIpcHandlers } from './app-state.js' export { registerAuthHandlers, setMainWindow as setAuthMainWindow, handleAuthDeepLink } from './auth.js' -export { registerSubagentsIpcHandlers } from './subagents.js' import { registerAgentIpcHandlers, cleanupAgent } from './agent.js' import { registerAuthHandlers } from './auth.js' @@ -23,7 +22,6 @@ import { registerChannelsIpcHandlers } from './channels.js' import { registerCronIpcHandlers } from './cron.js' import { registerHeartbeatIpcHandlers } from './heartbeat.js' import { registerAppStateIpcHandlers } from './app-state.js' -import { registerSubagentsIpcHandlers } from './subagents.js' /** * Register all IPC handlers. @@ -40,7 +38,6 @@ export function registerAllIpcHandlers(): void { registerHeartbeatIpcHandlers() registerAppStateIpcHandlers() registerAuthHandlers() - registerSubagentsIpcHandlers() } /** diff --git a/apps/desktop/src/main/ipc/subagents.ts b/apps/desktop/src/main/ipc/subagents.ts deleted file mode 100644 index ea901a91..00000000 --- a/apps/desktop/src/main/ipc/subagents.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Subagent IPC handlers for Electron main process. - * - * Exposes subagent registry data to the renderer process - * for the Subagent Dashboard UI. - */ -import { ipcMain } from 'electron' -import { listSubagentRuns, getSubagentGroup } from '@multica/core' -import type { SubagentRunRecord } from '@multica/core' - -/** Serializable DTO for renderer consumption */ -export interface SubagentRunInfo { - runId: string - label: string | undefined - task: string - status: 'queued' | 'running' | 'ok' | 'error' | 'timeout' | 'unknown' - groupId: string | undefined - groupLabel: string | undefined - startedAt: number | undefined - endedAt: number | undefined - createdAt: number - findings: string | undefined - error: string | undefined -} - -function deriveStatus(record: SubagentRunRecord): SubagentRunInfo['status'] { - if (!record.startedAt) return 'queued' - if (!record.endedAt) return 'running' - return record.outcome?.status ?? 'unknown' -} - -function toDTO(record: SubagentRunRecord): SubagentRunInfo { - const group = record.groupId ? getSubagentGroup(record.groupId) : undefined - return { - runId: record.runId, - label: record.label, - task: record.task, - status: deriveStatus(record), - groupId: record.groupId, - groupLabel: group?.label, - startedAt: record.startedAt, - endedAt: record.endedAt, - createdAt: record.createdAt, - findings: record.findings ? record.findings.slice(0, 500) : undefined, - error: record.outcome?.error, - } -} - -/** Hide completed runs after 5 minutes */ -const COMPLETED_RETENTION_MS = 5 * 60 * 1000 - -/** - * Register all Subagent-related IPC handlers. - */ -export function registerSubagentsIpcHandlers(): void { - ipcMain.handle('subagents:list', async (_event, requesterSessionId: string) => { - const now = Date.now() - const runs = listSubagentRuns(requesterSessionId) - return runs - .filter((r) => !r.endedAt || now - r.endedAt < COMPLETED_RETENTION_MS) - .map(toDTO) - }) -} diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index e317dc33..3e282bee 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -105,23 +105,9 @@ export interface LocalChatApproval { expiresAtMs: number } -export interface SubagentRunInfo { - runId: string - label: string | undefined - task: string - status: 'queued' | 'running' | 'ok' | 'error' | 'timeout' | 'unknown' - groupId: string | undefined - groupLabel: string | undefined - startedAt: number | undefined - endedAt: number | undefined - createdAt: number - findings: string | undefined - error: string | undefined -} - -// ============================================================================ -// Expose typed API to Renderer process -// ============================================================================ +// ============================================================================ +// Expose typed API to Renderer process +// ============================================================================ const electronAPI = { // App-level @@ -291,14 +277,8 @@ const electronAPI = { ipcRenderer.invoke('channels:start', channelId, accountId), }, - // Subagent dashboard - subagents: { - list: (requesterSessionId: string): Promise => - ipcRenderer.invoke('subagents:list', requesterSessionId), - }, - - // Cron jobs management - cron: { + // Cron jobs management + cron: { list: () => ipcRenderer.invoke('cron:list'), toggle: (jobId: string) => ipcRenderer.invoke('cron:toggle', jobId), remove: (jobId: string) => ipcRenderer.invoke('cron:remove', jobId), diff --git a/apps/desktop/src/renderer/src/components/local-chat.tsx b/apps/desktop/src/renderer/src/components/local-chat.tsx index 0f842a05..7cbded04 100644 --- a/apps/desktop/src/renderer/src/components/local-chat.tsx +++ b/apps/desktop/src/renderer/src/components/local-chat.tsx @@ -3,13 +3,9 @@ import { useNavigate } from 'react-router-dom' import { Loading } from '@multica/ui/components/ui/loading' import { ChatView } from '@multica/ui/components/chat-view' import { useLocalChat } from '../hooks/use-local-chat' -import { useSubagentPolling } from '../hooks/use-subagent-polling' -import { useSubagentsStore } from '../stores/subagents' import { useProviderStore } from '../stores/provider' import { ApiKeyDialog } from './api-key-dialog' import { OAuthDialog } from './oauth-dialog' -import { SubagentStatusBar } from './subagent-status-bar' -import { SubagentDashboard } from './subagent-dashboard' interface LocalChatProps { initialPrompt?: string @@ -37,11 +33,6 @@ export function LocalChat({ initialPrompt }: LocalChatProps) { const { providers, current, setProvider: switchProvider, refresh: refreshProviders } = useProviderStore() - // Subagent polling + dashboard - useSubagentPolling(agentId) - const subagentRuns = useSubagentsStore((s) => s.runs) - const [dashboardOpen, setDashboardOpen] = useState(false) - // Provider config dialog state const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false) const [oauthDialogOpen, setOauthDialogOpen] = useState(false) @@ -126,12 +117,6 @@ export function LocalChat({ initialPrompt }: LocalChatProps) { loadMore={loadMore} resolveApproval={resolveApproval} errorAction={errorAction} - bottomSlot={ - setDashboardOpen(true)} - /> - } /> {currentMeta && currentMeta.authMethod === 'api-key' && ( @@ -154,12 +139,6 @@ export function LocalChat({ initialPrompt }: LocalChatProps) { onSuccess={handleProviderConfigSuccess} /> )} - - ) } diff --git a/apps/desktop/src/renderer/src/components/subagent-dashboard.tsx b/apps/desktop/src/renderer/src/components/subagent-dashboard.tsx deleted file mode 100644 index bac29773..00000000 --- a/apps/desktop/src/renderer/src/components/subagent-dashboard.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useState, useEffect } from 'react' -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetDescription, -} from '@multica/ui/components/ui/sheet' -import { Badge } from '@multica/ui/components/ui/badge' - -interface SubagentDashboardProps { - open: boolean - onOpenChange: (open: boolean) => void - runs: SubagentRunInfo[] -} - -const STATUS_CONFIG: Record = { - running: { label: 'Running', variant: 'default' }, - queued: { label: 'Queued', variant: 'secondary' }, - ok: { label: 'Completed', variant: 'outline' }, - error: { label: 'Error', variant: 'destructive' }, - timeout: { label: 'Timeout', variant: 'destructive' }, - unknown: { label: 'Unknown', variant: 'secondary' }, -} - -function formatElapsed(startMs: number, endMs?: number): string { - const elapsed = (endMs ?? Date.now()) - startMs - const seconds = Math.floor(elapsed / 1000) - if (seconds < 60) return `${seconds}s` - const minutes = Math.floor(seconds / 60) - const remainSec = seconds % 60 - if (minutes < 60) return `${minutes}m ${remainSec}s` - const hours = Math.floor(minutes / 60) - const remainMin = minutes % 60 - return `${hours}h ${remainMin}m` -} - -function RunCard({ run }: { run: SubagentRunInfo }) { - const config = STATUS_CONFIG[run.status] - const isActive = run.status === 'running' || run.status === 'queued' - const [, setTick] = useState(0) - - // Tick every 1s for running agents to update elapsed time - useEffect(() => { - if (!isActive) return - const timer = setInterval(() => setTick((t) => t + 1), 1000) - return () => clearInterval(timer) - }, [isActive]) - - return ( -
-
-
-

- {run.label || run.task.slice(0, 80)} -

- {run.label && ( -

- {run.task.slice(0, 120)} -

- )} -
- - {config.label} - -
- -
- {run.startedAt && ( - {formatElapsed(run.startedAt, run.endedAt)} - )} - {run.groupLabel && ( - - {run.groupLabel} - - )} -
- - {run.error && ( -

- {run.error} -

- )} - - {run.findings && !run.error && ( -

- {run.findings.slice(0, 200)} -

- )} -
- ) -} - -export function SubagentDashboard({ open, onOpenChange, runs }: SubagentDashboardProps) { - // Sort: active first (running, queued), then by createdAt desc - const sorted = [...runs].sort((a, b) => { - const aActive = a.status === 'running' || a.status === 'queued' ? 0 : 1 - const bActive = b.status === 'running' || b.status === 'queued' ? 0 : 1 - if (aActive !== bActive) return aActive - bActive - return b.createdAt - a.createdAt - }) - - return ( - - - - Subagents ({runs.length}) - Child agents spawned by the current session - -
- {sorted.length === 0 ? ( -

- No subagents yet -

- ) : ( - sorted.map((run) => ) - )} -
-
-
- ) -} diff --git a/apps/desktop/src/renderer/src/components/subagent-status-bar.tsx b/apps/desktop/src/renderer/src/components/subagent-status-bar.tsx deleted file mode 100644 index 60e4125f..00000000 --- a/apps/desktop/src/renderer/src/components/subagent-status-bar.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useState, useEffect, useRef } from 'react' - -/** Auto-dismiss delay after all runs complete (ms) */ -const DISMISS_DELAY_MS = 30_000 - -interface SubagentStatusBarProps { - runs: SubagentRunInfo[] - onViewClick: () => void -} - -export function SubagentStatusBar({ runs, onViewClick }: SubagentStatusBarProps) { - const [dismissed, setDismissed] = useState(false) - const prevHadActiveRef = useRef(false) - - const running = runs.filter((r) => r.status === 'running' || r.status === 'queued').length - const completed = runs.filter((r) => r.status !== 'running' && r.status !== 'queued').length - const hasActive = running > 0 - - // Auto-dismiss after all runs complete - useEffect(() => { - if (hasActive) { - // Reset dismissed state when new active runs appear - prevHadActiveRef.current = true - setDismissed(false) - return - } - - // Only auto-dismiss if we previously had active runs (transition to all-complete) - if (!prevHadActiveRef.current || runs.length === 0) return - - const timer = setTimeout(() => setDismissed(true), DISMISS_DELAY_MS) - return () => clearTimeout(timer) - }, [hasActive, runs.length]) - - if (runs.length === 0 || dismissed) return null - - let statusText: string - if (running > 0 && completed > 0) { - statusText = `${running} running, ${completed} completed` - } else if (running > 0) { - statusText = `${running} subagent${running > 1 ? 's' : ''} running` - } else { - statusText = `${completed} completed` - } - - return ( -
-
-
- {running > 0 && ( - - - - - )} - {statusText} -
-
- - {!hasActive && ( - - )} -
-
-
- ) -} diff --git a/apps/desktop/src/renderer/src/hooks/use-subagent-polling.ts b/apps/desktop/src/renderer/src/hooks/use-subagent-polling.ts deleted file mode 100644 index 78f73d77..00000000 --- a/apps/desktop/src/renderer/src/hooks/use-subagent-polling.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect, useRef } from 'react' -import { useSubagentsStore, selectHasActiveRuns } from '../stores/subagents' - -const ACTIVE_INTERVAL_MS = 2_000 -const IDLE_INTERVAL_MS = 10_000 - -/** - * Polls for subagent runs at an adaptive interval. - * 2s when there are active (running/queued) runs, 10s otherwise. - */ -export function useSubagentPolling(agentId: string | null): void { - const fetch = useSubagentsStore((s) => s.fetch) - const runs = useSubagentsStore((s) => s.runs) - const hasActive = selectHasActiveRuns(runs) - const intervalRef = useRef | null>(null) - - useEffect(() => { - if (!agentId) return - - // Fetch immediately - fetch(agentId) - - const ms = hasActive ? ACTIVE_INTERVAL_MS : IDLE_INTERVAL_MS - intervalRef.current = setInterval(() => fetch(agentId), ms) - - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current) - intervalRef.current = null - } - } - }, [agentId, hasActive, fetch]) -} diff --git a/apps/desktop/src/renderer/src/stores/subagents.ts b/apps/desktop/src/renderer/src/stores/subagents.ts deleted file mode 100644 index 249ed321..00000000 --- a/apps/desktop/src/renderer/src/stores/subagents.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { create } from 'zustand' - -interface SubagentsStore { - runs: SubagentRunInfo[] - fetch: (requesterSessionId: string) => Promise -} - -export const useSubagentsStore = create()((set) => ({ - runs: [], - - fetch: async (requesterSessionId: string) => { - try { - const result = await window.electronAPI.subagents.list(requesterSessionId) - if (Array.isArray(result)) { - set({ runs: result }) - } - } catch (err) { - console.error('[SubagentsStore] Failed to fetch:', err) - } - }, -})) - -export function selectRunningCount(runs: SubagentRunInfo[]): number { - return runs.filter((r) => r.status === 'running' || r.status === 'queued').length -} - -export function selectHasActiveRuns(runs: SubagentRunInfo[]): boolean { - return runs.some((r) => r.status === 'running' || r.status === 'queued') -}