refactor(desktop): drop legacy subagent dashboard wiring

This commit is contained in:
Jiayuan Zhang 2026-02-17 00:07:23 +08:00
parent 909efb5dab
commit db0f8b3f7b
9 changed files with 7 additions and 392 deletions

View file

@ -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<SubagentRunInfo[]>
}
cron: {
cron: {
list: () => Promise<unknown[]>
toggle: (jobId: string) => Promise<{ ok: boolean }>
remove: (jobId: string) => Promise<{ ok: boolean }>

View file

@ -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()
}
/**

View file

@ -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)
})
}

View file

@ -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<SubagentRunInfo[]> =>
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),

View file

@ -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={
<SubagentStatusBar
runs={subagentRuns}
onViewClick={() => setDashboardOpen(true)}
/>
}
/>
{currentMeta && currentMeta.authMethod === 'api-key' && (
@ -154,12 +139,6 @@ export function LocalChat({ initialPrompt }: LocalChatProps) {
onSuccess={handleProviderConfigSuccess}
/>
)}
<SubagentDashboard
open={dashboardOpen}
onOpenChange={setDashboardOpen}
runs={subagentRuns}
/>
</>
)
}

View file

@ -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<SubagentRunInfo['status'], { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline' }> = {
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 (
<div className="rounded-lg border bg-card p-3 space-y-2">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">
{run.label || run.task.slice(0, 80)}
</p>
{run.label && (
<p className="text-xs text-muted-foreground truncate mt-0.5">
{run.task.slice(0, 120)}
</p>
)}
</div>
<Badge variant={config.variant} className="shrink-0">
{config.label}
</Badge>
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{run.startedAt && (
<span>{formatElapsed(run.startedAt, run.endedAt)}</span>
)}
{run.groupLabel && (
<span className="bg-muted px-1.5 py-0.5 rounded text-[10px]">
{run.groupLabel}
</span>
)}
</div>
{run.error && (
<p className="text-xs text-destructive bg-destructive/5 rounded px-2 py-1 font-mono">
{run.error}
</p>
)}
{run.findings && !run.error && (
<p className="text-xs text-muted-foreground bg-muted/50 rounded px-2 py-1.5 font-mono whitespace-pre-wrap line-clamp-4">
{run.findings.slice(0, 200)}
</p>
)}
</div>
)
}
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 (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right" className="sm:max-w-md">
<SheetHeader>
<SheetTitle>Subagents ({runs.length})</SheetTitle>
<SheetDescription>Child agents spawned by the current session</SheetDescription>
</SheetHeader>
<div className="flex-1 min-h-0 overflow-y-auto px-4 pb-4 space-y-2">
{sorted.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-8">
No subagents yet
</p>
) : (
sorted.map((run) => <RunCard key={run.runId} run={run} />)
)}
</div>
</SheetContent>
</Sheet>
)
}

View file

@ -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 (
<div className="container px-4">
<div className="flex items-center justify-between h-8 px-3 rounded-lg bg-muted/50 border text-xs text-muted-foreground">
<div className="flex items-center gap-2">
{running > 0 && (
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
</span>
)}
<span>{statusText}</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={onViewClick}
className="text-xs font-medium text-foreground/70 hover:text-foreground transition-colors"
>
View
</button>
{!hasActive && (
<button
onClick={() => setDismissed(true)}
className="text-xs text-muted-foreground/50 hover:text-muted-foreground transition-colors"
>
Dismiss
</button>
)}
</div>
</div>
</div>
)
}

View file

@ -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<ReturnType<typeof setInterval> | 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])
}

View file

@ -1,29 +0,0 @@
import { create } from 'zustand'
interface SubagentsStore {
runs: SubagentRunInfo[]
fetch: (requesterSessionId: string) => Promise<void>
}
export const useSubagentsStore = create<SubagentsStore>()((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')
}