-
{plan.title}
- {plan.steps.map(s => (
+
+
+ ●
+ {plan?.title ?? '当前任务'}
+ {done}/{total}
+
+ {steps.map(s => (
-
+
{s.text}
{s.agentRun && }
))}
+ {items.map(t => (
+
+
+ {t.text}
+
+ ))}
)
}
-function TodosTab({ todos }: { todos: { items: { id: string; text: string; status: string }[] } | null }) {
- if (!todos?.items?.length) return
- return <>{todos.items.map(t => (
-
-
- {t.text}
-
- ))}>
+export function StatusIcon({ status }: { status?: string }) {
+ if (status === 'done' || status === 'completed') return
●
+ if (status === 'in_progress' || status === 'running') return
◐
+ if (status === 'skipped') return
◌
+ return
○
}
-function ArtifactsTab() {
- const artifacts = useStore(s => s.artifacts)
- if (artifacts.length === 0) return
- return <>{artifacts.map(a => (
-
store.viewArtifact(a)}>
- {a.kind === 'image' ? '🖼' : a.kind === 'document' ? '📄' : '🗂'}
- {a.name}
- {a.kind}
-
- ))}>
-}
-
-function AgentsTab({ runs }: { runs: Run[] }) {
- const convRuns = runs.filter(r => r.kind !== 'tool')
- if (convRuns.length === 0) return
- // 任务树:顶层 run + 其子代理。
- const roots = convRuns.filter(r => !r.parentRunId)
- return
- {roots.map(r => (
-
-
- {convRuns.filter(c => c.parentRunId === r.id).map(c =>
)}
-
- ))}
-
-}
-
-function AgentRow({ run, depth }: { run: Run; depth: number }) {
+/** 代理行(工作区弹层复用;点击进 AgentTracker)。 */
+export function AgentRow({ run, depth }: { run: Run; depth: number }) {
const label = run.kind === 'subagent' ? `🤖 ${run.agentName || '子代理'}` : '💬 对话任务'
- const terminal = isTerminal(run.status)
return (
store.openTracker(run.id)}>
- {label}
- {terminal ? run.status : (run.phase || run.status)}
+ {label}
+ {run.status}
)
}
-
-function Empty({ text }: { text: string }) {
- return
{text}
-}
diff --git a/src/components/Workspaces.tsx b/src/components/Workspaces.tsx
new file mode 100644
index 0000000..edff518
--- /dev/null
+++ b/src/components/Workspaces.tsx
@@ -0,0 +1,92 @@
+// 工作区弹层(提案 T4/D25):工件/代理/插件面板/插件声明工作区统一经 OverlayHost 呈现;
+// ⌘1–9 快捷键与左栏快捷区共享同一清单(workspaceEntries)。
+import { store, useStore } from '../store'
+import type { Run } from '../contracts'
+import { OverlayHost, type DocBridge } from '@ui'
+import { docBridge } from './Workbench'
+import { AgentRow } from './Workbench'
+
+/** 工作区清单(顺序即 ⌘ 序号):内置在前,插件面板/声明工作区随后。 */
+export function workspaceEntries(): { key: string; title: string; icon: string; shortcut: string }[] {
+ const s = store.getState()
+ const out = [
+ { key: 'artifacts', title: '工件', icon: '📦', shortcut: '⌘1' },
+ { key: 'agents', title: '代理', icon: '🤖', shortcut: '⌘2' },
+ ...s.pluginPanels.map((p, i) => ({ key: `plugin:${p.id}`, title: p.title, icon: '🧩', shortcut: `⌘${i + 3}` })),
+ ...s.serverWorkspaces.map((w, i) => ({ key: `ws:${w.id}`, title: w.title, icon: w.icon || '🗂', shortcut: w.shortcut ? `⌘${w.shortcut}` : `⌘${i + 3 + s.pluginPanels.length}` })),
+ ]
+ return out.slice(0, 9)
+}
+
+export function Workspaces() {
+ const active = useStore(s => s.ui.activeWorkspace)
+ const artifacts = useStore(s => s.artifacts)
+ const runs = useStore(s => s.runs)
+ const pluginPanels = useStore(s => s.pluginPanels)
+ const serverWorkspaces = useStore(s => s.serverWorkspaces)
+ if (!active) return null
+ const entry = workspaceEntries().find(e => e.key === active)
+ const close = () => store.closeWorkspace()
+
+ if (active === 'artifacts') {
+ return
+
+
+ }
+ if (active === 'agents') {
+ const convRuns = runs.filter(r => r.kind !== 'tool')
+ return
+
+
+ }
+ if (active.startsWith('plugin:')) {
+ const panel = pluginPanels.find(p => `plugin:${p.id}` === active)
+ return
store.refreshPlugins()} />
+ }
+ if (active.startsWith('ws:')) {
+ const ws = serverWorkspaces.find(w => `ws:${w.id}` === active)
+ return store.refreshPlugins()} />
+ }
+ return null
+}
+
+/** 工件面板(自 Workbench tabs 迁入;点击进 DocViewer 深读)。 */
+export function ArtifactsPanel() {
+ const artifacts = useStore(s => s.artifacts)
+ if (artifacts.length === 0) return 暂无工件——上传附件或让模型产出文档(mock:#doc)
+ return <>{artifacts.map(a => (
+ store.viewArtifact(a)}>
+ {a.kind === 'image' ? '🖼' : a.kind === 'document' ? '📄' : '🗂'}
+ {a.name}
+ {a.kind}
+
+ ))}>
+}
+
+/** 代理面板(任务树;点击进 AgentTracker 深读)。 */
+export function AgentsPanel({ runs }: { runs: Run[] }) {
+ if (runs.length === 0) return 暂无运行——多代理任务会在此形成任务树(mock:#agents)
+ const roots = runs.filter(r => !r.parentRunId)
+ return
+ {roots.map(r => (
+
+
+ {runs.filter(c => c.parentRunId === r.id).map(c =>
)}
+
+ ))}
+
+}
+
+/** ⌘1–9 / Ctrl+1–9 全局快捷键(挂载于 App)。 */
+export function workspaceShortcuts(e: KeyboardEvent) {
+ if (!(e.metaKey || e.ctrlKey)) return
+ const n = parseInt(e.key, 10)
+ if (!Number.isInteger(n) || n < 1 || n > 9) return
+ const entries = workspaceEntries()
+ const hit = entries[n - 1]
+ if (!hit) return
+ e.preventDefault()
+ store.openWorkspace(hit.key)
+}
diff --git a/src/store.ts b/src/store.ts
index 004d0e4..9e16cd1 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -34,6 +34,7 @@ export interface UIState {
settingsOpen: boolean
trackerRunId: string | null
viewingArtifact: Artifact | null
+ activeWorkspace: string | null
}
interface AppState {
@@ -53,6 +54,7 @@ interface AppState {
cards: ActivityCard[]
profiles: AgentProfile[]
pluginPanels: PluginPanel[]
+ serverWorkspaces: { id: string; title: string; icon?: string; shortcut?: string; body?: any[] }[]
ui: UIState
connected: boolean
error: string | null
@@ -65,6 +67,7 @@ const DEFAULT_UI: UIState = {
rightWidth: 340,
rightCollapsed: false,
rightTab: 'inbox',
+ activeWorkspace: null,
theme: 'dark',
autoCollapseProcess: true,
settingsOpen: false,
@@ -85,6 +88,7 @@ let state: AppState = {
projects: [], currentProjectId: null, conversations: [], currentConvId: null,
messages: [], runs: [], activeHitl: [], lastSeq: 0, live: null,
plan: null, todos: null, artifacts: [], cards: [], profiles: [], pluginPanels: [],
+ serverWorkspaces: [],
ui: loadUI(), connected: false, error: null, toast: null,
}
@@ -391,6 +395,10 @@ export const store = {
const panels = list.flatMap(p => p.panels ?? [])
set({ pluginPanels: panels })
} catch { /* ignore */ }
+ try {
+ const ws = await api.listWorkspaces()
+ set({ serverWorkspaces: ws ?? [] })
+ } catch { /* ignore */ }
},
async uploadAttachment(file: File): Promise {
@@ -409,6 +417,8 @@ export const store = {
setUI,
toggleLeft() { setUI({ leftCollapsed: !state.ui.leftCollapsed }) },
toggleRight() { setUI({ rightCollapsed: !state.ui.rightCollapsed }) },
+ openWorkspace(id: string) { setUI({ activeWorkspace: id }) },
+ closeWorkspace() { setUI({ activeWorkspace: null }) },
openInbox(cardId?: string) {
setUI({ rightCollapsed: false, rightTab: 'inbox' })
if (cardId) setTimeout(() => {
diff --git a/src/styles.css b/src/styles.css
index 1038ba3..4d43a27 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -182,3 +182,17 @@ button { font-family: inherit; }
background: #3b82f6; color: #fff; font-size: 14px; cursor: pointer;
}
.login-err { margin-top: 10px; font-size: 12px; color: #f87171; }
+
+/* 工作区快捷区(T4/D26 左栏)与当前任务常驻卡(D25 R4) */
+.ws-quick{padding:6px 8px 8px;border-bottom:1px solid var(--border)}
+.ws-quick-list{display:flex;flex-direction:column;gap:2px;margin-top:4px}
+.ws-quick-item{display:flex;align-items:center;gap:8px;padding:4px 8px;border:none;background:transparent;color:inherit;cursor:pointer;border-radius:6px;text-align:left;font-size:12px}
+.ws-quick-item:hover{background:rgba(120,160,255,.12)}
+.ws-quick-item .sc{margin-left:auto;font-size:10px;color:var(--text-faint);border:1px solid var(--border);border-radius:3px;padding:0 4px}
+.current-task{border:1px solid var(--border);border-radius:8px;padding:8px 10px;margin-bottom:8px;background:rgba(120,160,255,.05)}
+.ct-head{display:flex;align-items:center;gap:8px;font-weight:600;margin-bottom:6px}
+.ct-title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.ct-progress{font-size:11px;color:var(--text-faint)}
+.project-badge{font-size:11px;padding:1px 8px;border-radius:8px;background:rgba(120,160,255,.15);color:var(--accent);white-space:nowrap}
+.tab-hint{font-size:10px;color:var(--text-faint);align-self:center}
+.empty-note{color:var(--text-faint);font-size:12px;padding:12px 4px}
diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo
index fe98e6f..00c22a1 100644
--- a/tsconfig.tsbuildinfo
+++ b/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/main.tsx","./src/store.ts","./src/components/AgentTracker.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/ProjectNav.tsx","./src/components/SettingsModal.tsx","./src/components/Workbench.tsx","./test/auth.test.ts","./test/contracts.test.ts"],"version":"5.9.3"}
\ No newline at end of file
+{"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/main.tsx","./src/store.ts","./src/components/AgentTracker.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/ProjectNav.tsx","./src/components/SettingsModal.tsx","./src/components/Workbench.tsx","./src/components/Workspaces.tsx","./test/auth.test.ts","./test/contracts.test.ts"],"version":"5.9.3"}
\ No newline at end of file