Compare commits

..
Author SHA1 Message Date
weichen 4f4711a22a Add the tidy action and collapsed-rail pending badge (T5)
Why: T5 notification strategy - the collapsed right rail must still surface the pending count (badge), and the inbox needs a one-key tidy (done cards dismissed; resolved verdicts keep their stamp).
Evidence: tsc clean; vitest 9/9; vite build ok; delegation-only logic.
Impact: App collapsed rail badge, store tidyCards (done->dismiss via existing cardDismiss), Workbench tidy button, styles.
Refs: docs/proposals/2026-09-30-ui-inbox-command-workspace.md T5; task card feature/polish-r1
2026-09-30 08:13:13 +00:00
weichen e5b65ca79a Merge pull request 'Make the right rail pure inbox and move deep views into overlay workspaces (T4)' (#7) from feature/workspaces-r1 into main 2026-09-30 08:08:25 +00:00
weichen 58f3599660 Sync main before merge (stale-candidate guard) 2026-09-30 08:08:01 +00:00
weichen 298c41ff67 Make the right rail pure inbox and move deep views into overlay workspaces (T4)
Why: D25 final state - the right rail answers only "what needs me now" (inbox + a pinned current-task card); artifacts/agents/plugin panels become overlay workspaces with Cmd shortcuts and a left-rail quick section; DocViewer/AgentTracker migrate onto the shared OverlayHost.
Evidence: tsc clean; vitest 9/9; vite build ok; delegation-only logic (overlay semantics covered by ui OverlayHost suite).
Impact: Workbench drops classic tabs and gains CurrentTaskCard (plan+todos merged); new Workspaces.tsx (workspaceEntries, Cmd1-9 handler, builtin/plugin/server workspace rendering); store gains activeWorkspace/serverWorkspaces + open/close; ProjectNav quick section; Conversation project badge (D26); DocViewer/AgentTracker render through OverlayHost.
Refs: docs/proposals/2026-09-30-ui-inbox-command-workspace.md T4; task card feature/workspaces-r1
2026-09-30 08:04:21 +00:00
weichen 6a2b21a28d Merge pull request 'Wire inline confirm, cardlink locate, drop zone, and symmetric command menu (T3)' (#6) from feature/command-bus-r1 into main 2026-09-30 07:55:00 +00:00
11 changed files with 212 additions and 101 deletions
+12 -1
View File
@@ -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,10 +41,17 @@ 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)
const viewingArtifact = useStore(s => s.ui.viewingArtifact)
const pendingBadge = useStore(s => s.cards.filter(c => c.status === 'action_required').length)
// authState:checking → ok(放行)| required(登录门)。
const [authState, setAuthState] = useState<'checking' | 'required' | 'ok'>('checking')
@@ -133,11 +141,14 @@ export function App() {
) : (
<div className="right-panel collapsed" style={{ width: 44 }}>
<div className="icon-only">
<button title="展开工作台" onClick={store.toggleRight}>◧</button>
<button title="展开收件箱" onClick={store.toggleRight}>◧
{pendingBadge > 0 && <span className="rail-badge" data-testid="rail-badge">{pendingBadge}</span>}
</button>
</div>
</div>
)}
<Workspaces />
{trackerRunId && <AgentTracker />}
{ui.settingsOpen && <SettingsModal />}
{viewingArtifact && <DocViewer artifact={viewingArtifact} />}
+2 -1
View File
@@ -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 游标续传;断线自动重连 ----
+3 -2
View File
@@ -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>
)
}
+2
View File
@@ -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 -2
View File
@@ -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>
)
}
+12
View File
@@ -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)}
+48 -94
View File
@@ -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,80 @@ 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 tab-action" title="收纳已完成卡(done→关闭;已确认卡留痕不动)" onClick={() => store.tidyCards()}>🧹 收纳</button>
<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>
}
+92
View File
@@ -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)
}
+18
View File
@@ -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,16 @@ export const store = {
setUI,
toggleLeft() { setUI({ leftCollapsed: !state.ui.leftCollapsed }) },
toggleRight() { setUI({ rightCollapsed: !state.ui.rightCollapsed }) },
openWorkspace(id: string) { setUI({ activeWorkspace: id }) },
/** 收纳策略(T5):一键收纳已完成卡(done→dismissed;resolved 盖章留痕不动)。 */
async tidyCards() {
const done = state.cards.filter(c => c.status === 'done')
for (const c of done) {
try { const out = await api.cardDismiss(c.id); store.upsertCard(out.card) } catch { /* 单卡失败不阻断 */ }
}
if (done.length > 0) store.toast(`已收纳 ${done.length} 张完成卡`)
},
closeWorkspace() { setUI({ activeWorkspace: null }) },
openInbox(cardId?: string) {
setUI({ rightCollapsed: false, rightTab: 'inbox' })
if (cardId) setTimeout(() => {
+19
View File
@@ -182,3 +182,22 @@ 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}
/* 收纳策略(T5):收起态角标 + 收纳按钮 */
.rail-badge{position:absolute;top:-4px;right:-6px;font-size:10px;background:var(--warn,#d29922);color:#fff;border-radius:8px;padding:0 5px;min-width:14px}
.right-panel.collapsed .icon-only button{position:relative}
.tab-action{font-size:11px;color:var(--text-faint)}
+1 -1
View File
@@ -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"}