Merge pull request 'Make the right rail pure inbox and move deep views into overlay workspaces (T4)' (#7) from feature/workspaces-r1 into main
This commit was merged in pull request #7.
This commit is contained in:
@@ -4,6 +4,7 @@ import { store, useStore } from './store'
|
||||
import { ProjectNav } from './components/ProjectNav'
|
||||
import { Conversation } from './components/Conversation'
|
||||
import { Workbench } from './components/Workbench'
|
||||
import { Workspaces, workspaceShortcuts } from './components/Workspaces'
|
||||
import { AgentTracker } from './components/AgentTracker'
|
||||
import { SettingsModal } from './components/SettingsModal'
|
||||
import { DocViewer } from './components/DocViewer'
|
||||
@@ -40,6 +41,12 @@ function LoginGate({ onSuccess }: { onSuccess: () => void }) {
|
||||
}
|
||||
|
||||
export function App() {
|
||||
useEffect(() => {
|
||||
const h = (e: KeyboardEvent) => workspaceShortcuts(e)
|
||||
window.addEventListener('keydown', h, true)
|
||||
return () => window.removeEventListener('keydown', h, true)
|
||||
}, [])
|
||||
|
||||
const ui = useStore(s => s.ui)
|
||||
const toast = useStore(s => s.toast)
|
||||
const trackerRunId = useStore(s => s.ui.trackerRunId)
|
||||
@@ -138,6 +145,7 @@ export function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Workspaces />
|
||||
{trackerRunId && <AgentTracker />}
|
||||
{ui.settingsOpen && <SettingsModal />}
|
||||
{viewingArtifact && <DocViewer artifact={viewingArtifact} />}
|
||||
|
||||
+2
-1
@@ -78,7 +78,8 @@ export const api = {
|
||||
listTools: () => auth.fetch(`${BASE}/api/tools`).then(r => r.json()),
|
||||
listPlugins: () => auth.fetch(`${BASE}/api/plugins`).then(r => r.json()),
|
||||
reloadPlugins: () => auth.fetch(`${BASE}/api/plugins/reload`, { method: 'POST' }).then(r => r.json()),
|
||||
listCommands: () => auth.fetch(`${BASE}/api/commands`).then(r => r.json()) as Promise<{ name: string; description: string }[]>,
|
||||
listCommands: () => auth.fetch(`${BASE}/api/commands`).then(r => r.json()) as Promise<{ name?: string; id?: string; title?: string; description?: string; danger?: string }[]>,
|
||||
listWorkspaces: () => auth.fetch(`${BASE}/api/workspaces`).then(r => r.json()) as Promise<{ id: string; title: string; icon?: string; shortcut?: string; badgeTool?: string; body?: any[]; refreshTool?: string }[]>,
|
||||
}
|
||||
|
||||
// ---- SSE 客户端:Last-Event-ID 游标续传;断线自动重连 ----
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '../api'
|
||||
import { store, useStore } from '../store'
|
||||
import { OverlayHost } from '@ui'
|
||||
import type { Message, Run, Usage } from '../contracts'
|
||||
|
||||
export function AgentTracker() {
|
||||
@@ -24,7 +25,7 @@ export function AgentTracker() {
|
||||
const focusMsgs = focus === runId ? messagesAll.filter(m => m.runId === runId) : subMessages[focus] ?? []
|
||||
|
||||
return (
|
||||
<div className="overlay" onClick={e => { if (e.target === e.currentTarget) store.setUI({ trackerRunId: null }) }}>
|
||||
<OverlayHost title="代理追踪" icon="🤖" onClose={() => store.setUI({ trackerRunId: null })}>
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
🤖 Sub-agent 追踪
|
||||
@@ -75,7 +76,7 @@ export function AgentTracker() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayHost>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@ export function Conversation() {
|
||||
const artifacts = useStore(s => s.artifacts)
|
||||
const profiles = useStore(s => s.profiles)
|
||||
const cards = useStore(s => s.cards)
|
||||
const projectName = useStore(s => s.projects.find(p => p.id === s.currentProjectId)?.name ?? '项目')
|
||||
|
||||
const [text, setText] = useState('')
|
||||
const [attachmentIds, setAttachmentIds] = useState<string[]>([])
|
||||
@@ -235,6 +236,7 @@ export function Conversation() {
|
||||
return (
|
||||
<div className="chat">
|
||||
<div className="chat-header">
|
||||
<span className="project-badge" title="当前项目">{projectName}</span>
|
||||
<div className="chat-title">{conversation?.title ?? '会话'}</div>
|
||||
<select className="effect-badge" value={conversation?.profileId || 'default'} title="切换会话画像"
|
||||
onChange={async e => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
|
||||
import { marked } from 'marked'
|
||||
import { api } from '../api'
|
||||
import { store } from '../store'
|
||||
import { OverlayHost } from '@ui'
|
||||
import type { Artifact } from '../contracts'
|
||||
|
||||
export function DocViewer({ artifact }: { artifact: Artifact }) {
|
||||
@@ -16,7 +17,7 @@ export function DocViewer({ artifact }: { artifact: Artifact }) {
|
||||
}, [artifact.id])
|
||||
|
||||
return (
|
||||
<div className="overlay" onClick={e => { if (e.target === e.currentTarget) store.viewArtifact(null) }}>
|
||||
<OverlayHost title={artifact.name} icon="📄" onClose={() => store.viewArtifact(null)}>
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
📄 {artifact.name}
|
||||
@@ -49,7 +50,7 @@ export function DocViewer({ artifact }: { artifact: Artifact }) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayHost>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 左栏:项目折叠组 → 会话列表;底部账户/设置入口(4.4)。
|
||||
import { useEffect, useState } from 'react'
|
||||
import { store, useStore } from '../store'
|
||||
import { workspaceEntries } from './Workspaces'
|
||||
|
||||
export function ProjectNav() {
|
||||
const projects = useStore(s => s.projects)
|
||||
@@ -19,6 +20,17 @@ export function ProjectNav() {
|
||||
<span className="nav-label">Agent-System</span>
|
||||
<button title="新建项目" onClick={() => setCreating(v => !v)}>✚</button>
|
||||
</div>
|
||||
<div className="ws-quick">
|
||||
<div className="nav-label">工作区</div>
|
||||
<div className="ws-quick-list">
|
||||
{workspaceEntries().map(e => (
|
||||
<button key={e.key} className="ws-quick-item" title={`${e.title}(${e.shortcut})`}
|
||||
onClick={() => store.openWorkspace(e.key)}>
|
||||
<span>{e.icon}</span><span className="name">{e.title}</span><span className="sc">{e.shortcut}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{creating && (
|
||||
<div style={{ padding: '0 10px 8px', display: 'flex', gap: 6 }}>
|
||||
<input className="search-input" value={newName} onChange={e => setNewName(e.target.value)}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// 右栏工作台:收件箱(D25 置首)+ Plan / Todos / Artifacts / Agents + 插件面板;tab 显隐排序见设置。
|
||||
import { store, useStore, isTerminal } from '../store'
|
||||
// 右栏终态(提案 T4/D25):纯收件箱 + 「当前任务」常驻卡(计划/待办并入;工件/代理/
|
||||
// 插件面板迁为工作区弹层,见 Workspaces.tsx)。
|
||||
import { store, useStore } from '../store'
|
||||
import type { Run } from '../contracts'
|
||||
import { UIRenderer, InboxList, pendingCount, type DocBridge, type ActivityCard } from '@ui'
|
||||
import { InboxList, pendingCount, type ActivityCard, type DocBridge } from '@ui'
|
||||
|
||||
// 工件解析/打开注入:渲染器纯展示,doc-ref 词汇经此桥接本仓状态(D29)。
|
||||
export const docBridge: DocBridge = {
|
||||
@@ -13,127 +14,79 @@ export const docBridge: DocBridge = {
|
||||
}
|
||||
|
||||
export function Workbench() {
|
||||
const ui = useStore(s => s.ui)
|
||||
const cards = useStore(s => s.cards)
|
||||
const plan = useStore(s => s.plan)
|
||||
const todos = useStore(s => s.todos)
|
||||
const artifacts = useStore(s => s.artifacts)
|
||||
const runs = useStore(s => s.runs)
|
||||
const pluginPanels = useStore(s => s.pluginPanels)
|
||||
const cards = useStore(s => s.cards)
|
||||
const pending = pendingCount(cards)
|
||||
|
||||
const tabs: { id: string; label: string }[] = [
|
||||
{ id: 'inbox', label: pending > 0 ? `收件箱 ${pending}` : '收件箱' },
|
||||
{ id: 'plan', label: '计划' },
|
||||
{ id: 'todos', label: '待办' },
|
||||
{ id: 'artifacts', label: `工件${artifacts.length ? ` ${artifacts.length}` : ''}` },
|
||||
{ id: 'agents', label: '代理' },
|
||||
...pluginPanels.map(p => ({ id: `plugin:${p.id}`, label: p.title })),
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="tabs">
|
||||
{tabs.map(t => (
|
||||
<button key={t.id} className={`tab ${ui.rightTab === t.id ? 'active' : ''}`}
|
||||
onClick={() => store.setUI({ rightTab: t.id })}>{t.label}</button>
|
||||
))}
|
||||
<button className="tab" title="收起工作台" onClick={store.toggleRight}>»</button>
|
||||
<span className="tab active">收件箱{pending > 0 ? ` ${pending}` : ''}</span>
|
||||
<button className="tab" title="收起收件箱" onClick={store.toggleRight}>»</button>
|
||||
</div>
|
||||
<div className="tab-body">
|
||||
{ui.rightTab === 'inbox' && <InboxTab cards={cards} />}
|
||||
{ui.rightTab === 'plan' && <PlanTab plan={plan} />}
|
||||
{ui.rightTab === 'todos' && <TodosTab todos={todos} />}
|
||||
{ui.rightTab === 'artifacts' && <ArtifactsTab />}
|
||||
{ui.rightTab === 'agents' && <AgentsTab runs={runs} />}
|
||||
{ui.rightTab.startsWith('plugin:') && (
|
||||
<UIRenderer nodes={pluginPanels.find(p => `plugin:${p.id}` === ui.rightTab)?.body ?? []} docBridge={docBridge} />
|
||||
)}
|
||||
<CurrentTaskCard plan={plan} todos={todos} />
|
||||
<InboxList cards={cards} callbacks={{
|
||||
onAction: (c: ActivityCard, id: string) => store.cardAction(c, id),
|
||||
onPin: (c: ActivityCard, p: boolean) => store.cardPin(c, p),
|
||||
onDismiss: (c: ActivityCard) => store.cardDismiss(c),
|
||||
onLocate: (c: ActivityCard) => store.openInbox(c.id),
|
||||
}} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function InboxTab({ cards }: { cards: ActivityCard[] }) {
|
||||
return <InboxList cards={cards} callbacks={{
|
||||
onAction: (c, id) => store.cardAction(c, id),
|
||||
onPin: (c, p) => store.cardPin(c, p),
|
||||
onDismiss: c => store.cardDismiss(c),
|
||||
}} />
|
||||
}
|
||||
|
||||
function StatusIcon({ status }: { status?: string }) {
|
||||
if (status === 'done' || status === 'completed') return <span style={{ color: 'var(--ok)' }}>●</span>
|
||||
if (status === 'in_progress' || status === 'running') return <span style={{ color: 'var(--accent)' }}>◐</span>
|
||||
if (status === 'skipped') return <span>◌</span>
|
||||
return <span style={{ color: 'var(--text-faint)' }}>○</span>
|
||||
}
|
||||
|
||||
function PlanTab({ plan }: { plan: { title: string; steps: { id: string; text: string; status: string; agentRun?: string }[] } | null }) {
|
||||
if (!plan?.steps?.length) return <Empty text="暂无计划——模型可通过 update_plan 工具创建(mock:#agents)" />
|
||||
/** 当前任务常驻卡(D25 R4):计划+待办合成一张卡,钉在收件箱顶部——右栏只回答"现在要我做什么"。 */
|
||||
export function CurrentTaskCard({ plan, todos }: {
|
||||
plan: { title: string; steps: { id: string; text: string; status: string; agentRun?: string }[] } | null
|
||||
todos: { items: { id: string; text: string; status: string }[] } | null
|
||||
}) {
|
||||
const steps = plan?.steps ?? []
|
||||
const items = todos?.items ?? []
|
||||
if (steps.length === 0 && items.length === 0) return null
|
||||
const done = [...steps, ...items].filter(x => x.status === 'done' || x.status === 'completed').length
|
||||
const total = steps.length + items.length
|
||||
return (
|
||||
<div>
|
||||
<h5 style={{ margin: '4px 0 10px' }}>{plan.title}</h5>
|
||||
{plan.steps.map(s => (
|
||||
<div className="current-task" data-testid="current-task">
|
||||
<div className="ct-head">
|
||||
<span className="lamp lamp-ok">●</span>
|
||||
<span className="ct-title">{plan?.title ?? '当前任务'}</span>
|
||||
<span className="ct-progress">{done}/{total}</span>
|
||||
</div>
|
||||
{steps.map(s => (
|
||||
<div key={s.id} className={`step ${s.status}`}>
|
||||
<span className="step-icon"><StatusIcon status={s.status} /></span>
|
||||
<StatusIcon status={s.status} />
|
||||
<span style={{ flex: 1 }}>{s.text}</span>
|
||||
{s.agentRun && <span className="status-dot status-completed" title="子代理执行" />}
|
||||
</div>
|
||||
))}
|
||||
{items.map(t => (
|
||||
<div key={t.id} className={`step ${t.status}`}>
|
||||
<StatusIcon status={t.status} />
|
||||
<span>{t.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TodosTab({ todos }: { todos: { items: { id: string; text: string; status: string }[] } | null }) {
|
||||
if (!todos?.items?.length) return <Empty text="暂无待办(mock:#agents)" />
|
||||
return <>{todos.items.map(t => (
|
||||
<div key={t.id} className={`step ${t.status}`}>
|
||||
<span className="step-icon"><StatusIcon status={t.status} /></span>
|
||||
<span>{t.text}</span>
|
||||
</div>
|
||||
))}</>
|
||||
export function StatusIcon({ status }: { status?: string }) {
|
||||
if (status === 'done' || status === 'completed') return <span style={{ color: 'var(--ok)' }}>●</span>
|
||||
if (status === 'in_progress' || status === 'running') return <span style={{ color: 'var(--accent)' }}>◐</span>
|
||||
if (status === 'skipped') return <span>◌</span>
|
||||
return <span style={{ color: 'var(--text-faint)' }}>○</span>
|
||||
}
|
||||
|
||||
function ArtifactsTab() {
|
||||
const artifacts = useStore(s => s.artifacts)
|
||||
if (artifacts.length === 0) return <Empty text="暂无工件——上传附件或让模型产出文档(mock:#doc)" />
|
||||
return <>{artifacts.map(a => (
|
||||
<div key={a.id} className="artifact-row" onClick={() => store.viewArtifact(a)}>
|
||||
<span>{a.kind === 'image' ? '🖼' : a.kind === 'document' ? '📄' : '🗂'}</span>
|
||||
<span className="name" title={a.name}>{a.name}</span>
|
||||
<span className="kind">{a.kind}</span>
|
||||
</div>
|
||||
))}</>
|
||||
}
|
||||
|
||||
function AgentsTab({ runs }: { runs: Run[] }) {
|
||||
const convRuns = runs.filter(r => r.kind !== 'tool')
|
||||
if (convRuns.length === 0) return <Empty text="暂无运行——多代理任务会在此形成任务树(mock:#agents)" />
|
||||
// 任务树:顶层 run + 其子代理。
|
||||
const roots = convRuns.filter(r => !r.parentRunId)
|
||||
return <div className="subagent-tree">
|
||||
{roots.map(r => (
|
||||
<div key={r.id}>
|
||||
<AgentRow run={r} depth={0} />
|
||||
{convRuns.filter(c => c.parentRunId === r.id).map(c => <AgentRow key={c.id} run={c} depth={1} />)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="agent-row" style={{ paddingLeft: 8 + depth * 18 }} onClick={() => store.openTracker(run.id)}>
|
||||
<span className={`status-dot status-${run.status}`} />
|
||||
<span style={{ flex: 1 }}>{label}</span>
|
||||
<span style={{ color: 'var(--text-faint)', fontSize: 11 }}>{terminal ? run.status : (run.phase || run.status)}</span>
|
||||
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>{label}</span>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-faint)' }}>{run.status}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Empty({ text }: { text: string }) {
|
||||
return <div style={{ color: 'var(--text-faint)', fontSize: 12.5, padding: '10px 4px', lineHeight: 1.8 }}>{text}</div>
|
||||
}
|
||||
|
||||
@@ -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 <OverlayHost title="工件" icon="📦" shortcut="⌘1" badge={artifacts.length || undefined} onClose={close}>
|
||||
<ArtifactsPanel />
|
||||
</OverlayHost>
|
||||
}
|
||||
if (active === 'agents') {
|
||||
const convRuns = runs.filter(r => r.kind !== 'tool')
|
||||
return <OverlayHost title="代理" icon="🤖" shortcut="⌘2" onClose={close}>
|
||||
<AgentsPanel runs={convRuns} />
|
||||
</OverlayHost>
|
||||
}
|
||||
if (active.startsWith('plugin:')) {
|
||||
const panel = pluginPanels.find(p => `plugin:${p.id}` === active)
|
||||
return <OverlayHost title={panel?.title ?? '插件面板'} icon="🧩" onClose={close}
|
||||
nodes={panel?.body ?? []} docBridge={docBridge as DocBridge} onRefresh={() => store.refreshPlugins()} />
|
||||
}
|
||||
if (active.startsWith('ws:')) {
|
||||
const ws = serverWorkspaces.find(w => `ws:${w.id}` === active)
|
||||
return <OverlayHost title={ws?.title ?? '工作区'} icon={ws?.icon || '🗂'} shortcut={entry?.shortcut} onClose={close}
|
||||
nodes={ws?.body ?? []} docBridge={docBridge as DocBridge} onRefresh={() => store.refreshPlugins()} />
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** 工件面板(自 Workbench tabs 迁入;点击进 DocViewer 深读)。 */
|
||||
export function ArtifactsPanel() {
|
||||
const artifacts = useStore(s => s.artifacts)
|
||||
if (artifacts.length === 0) return <div className="empty-note">暂无工件——上传附件或让模型产出文档(mock:#doc)</div>
|
||||
return <>{artifacts.map(a => (
|
||||
<div key={a.id} className="artifact-row" onClick={() => store.viewArtifact(a)}>
|
||||
<span>{a.kind === 'image' ? '🖼' : a.kind === 'document' ? '📄' : '🗂'}</span>
|
||||
<span className="name" title={a.name}>{a.name}</span>
|
||||
<span className="kind">{a.kind}</span>
|
||||
</div>
|
||||
))}</>
|
||||
}
|
||||
|
||||
/** 代理面板(任务树;点击进 AgentTracker 深读)。 */
|
||||
export function AgentsPanel({ runs }: { runs: Run[] }) {
|
||||
if (runs.length === 0) return <div className="empty-note">暂无运行——多代理任务会在此形成任务树(mock:#agents)</div>
|
||||
const roots = runs.filter(r => !r.parentRunId)
|
||||
return <div className="subagent-tree">
|
||||
{roots.map(r => (
|
||||
<div key={r.id}>
|
||||
<AgentRow run={r} depth={0} />
|
||||
{runs.filter(c => c.parentRunId === r.id).map(c => <AgentRow key={c.id} run={c} depth={1} />)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
||||
/** ⌘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)
|
||||
}
|
||||
@@ -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<string | null> {
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"}
|
||||
{"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"}
|
||||
Reference in New Issue
Block a user