diff --git a/src/api.ts b/src/api.ts
index bfe31e6..2f0cef4 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -4,6 +4,7 @@ import { auth } from './auth'
import type {
Project, Conversation, Message, Run, Artifact, HITLCard, HITLResponse,
AgentProfile, SettingDesc, StreamEvent, PlanSnapshot, TodoList, UploadInfo,
+ ActivityCard,
} from './contracts'
const BASE = ''
@@ -43,6 +44,16 @@ export const api = {
auth.fetch(`${BASE}/api/hitl/${cardId}/resolve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(resp) })
.then(r => json<{ resolved: boolean; pivoted: boolean; pivotText: string }>(r)),
+ listCards: (conversationId: string) =>
+ auth.fetch(`${BASE}/api/conversations/${conversationId}/cards`).then(r => json<{ cards: ActivityCard[] }>(r)).then(d => d.cards ?? []),
+ cardAction: (cardId: string, actionId: string, freeText?: string) =>
+ auth.fetch(`${BASE}/api/cards/${cardId}/actions/${encodeURIComponent(actionId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ freeText }) })
+ .then(r => json<{ card: ActivityCard; receipt?: Message }>(r)),
+ cardPin: (cardId: string, pinned: boolean) =>
+ auth.fetch(`${BASE}/api/cards/${cardId}/pin`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pinned }) }).then(r => json<{ card: ActivityCard }>(r)),
+ cardDismiss: (cardId: string) =>
+ auth.fetch(`${BASE}/api/cards/${cardId}/dismiss`, { method: 'POST' }).then(r => json<{ card: ActivityCard }>(r)),
+
upload: (projectId: string, file: File) => {
const fd = new FormData()
fd.append('file', file)
diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx
index 46bc84e..4facd2e 100644
--- a/src/components/Workbench.tsx
+++ b/src/components/Workbench.tsx
@@ -1,7 +1,7 @@
-// 右栏工作台:Plan / Todos / Artifacts / Agents + 插件面板(4.4);tab 显隐排序见设置。
+// 右栏工作台:收件箱(D25 置首)+ Plan / Todos / Artifacts / Agents + 插件面板;tab 显隐排序见设置。
import { store, useStore, isTerminal } from '../store'
import type { Run } from '../contracts'
-import { UIRenderer, type DocBridge } from '@ui'
+import { UIRenderer, InboxList, pendingCount, type DocBridge, type ActivityCard } from '@ui'
// 工件解析/打开注入:渲染器纯展示,doc-ref 词汇经此桥接本仓状态(D29)。
export const docBridge: DocBridge = {
@@ -19,8 +19,11 @@ export function Workbench() {
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}` : ''}` },
@@ -38,6 +41,7 @@ export function Workbench() {
+ {ui.rightTab === 'inbox' &&
}
{ui.rightTab === 'plan' &&
}
{ui.rightTab === 'todos' &&
}
{ui.rightTab === 'artifacts' &&
}
@@ -50,6 +54,14 @@ export function Workbench() {
)
}
+function InboxTab({ cards }: { cards: ActivityCard[] }) {
+ return
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 ●
if (status === 'in_progress' || status === 'running') return ◐
diff --git a/src/store.ts b/src/store.ts
index edb078b..3612a49 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -4,7 +4,7 @@ import { useSyncExternalStore } from 'react'
import { api, connectEvents } from './api'
import type {
Project, Conversation, Message, Run, Artifact, HITLCard, PlanSnapshot, TodoList,
- AgentProfile, StreamEvent, PluginPanel,
+ AgentProfile, StreamEvent, PluginPanel, ActivityCard,
} from './contracts'
export interface ContextObs {
@@ -50,6 +50,7 @@ interface AppState {
plan: PlanSnapshot | null
todos: TodoList | null
artifacts: Artifact[]
+ cards: ActivityCard[]
profiles: AgentProfile[]
pluginPanels: PluginPanel[]
ui: UIState
@@ -63,7 +64,7 @@ const DEFAULT_UI: UIState = {
leftCollapsed: false,
rightWidth: 340,
rightCollapsed: false,
- rightTab: 'plan',
+ rightTab: 'inbox',
theme: 'dark',
autoCollapseProcess: true,
settingsOpen: false,
@@ -83,7 +84,7 @@ let state: AppState = {
contextObs: {},
projects: [], currentProjectId: null, conversations: [], currentConvId: null,
messages: [], runs: [], activeHitl: [], lastSeq: 0, live: null,
- plan: null, todos: null, artifacts: [], profiles: [], pluginPanels: [],
+ plan: null, todos: null, artifacts: [], cards: [], profiles: [], pluginPanels: [],
ui: loadUI(), connected: false, error: null, toast: null,
}
@@ -151,7 +152,7 @@ export const store = {
async openConversation(convId: string) {
if (sse) { sse.close(); sse = null }
- set({ currentConvId: convId, messages: [], runs: [], activeHitl: [], lastSeq: 0, live: null, plan: null, todos: null })
+ set({ currentConvId: convId, messages: [], runs: [], activeHitl: [], lastSeq: 0, live: null, plan: null, todos: null, cards: [] })
await store.reconcile()
await store.connectLive()
},
@@ -172,6 +173,7 @@ export const store = {
})
const [plan, todos] = await Promise.all([api.getPlan(d.conversation.id), api.getTodos(d.conversation.id)])
set({ plan, todos })
+ store.refreshCards(d.conversation.id)
} catch (e) {
set({ error: String(e) })
}
@@ -252,6 +254,24 @@ export const store = {
set({ todos: p.list })
break
}
+ case 'card.created':
+ case 'card.updated': {
+ const p = e.payload as { card: ActivityCard }
+ if (p?.card) store.upsertCard(p.card)
+ break
+ }
+ case 'card.status': {
+ const p = e.payload as { cardId: string; status: ActivityCard['status']; tone?: string }
+ const cards = state.cards.map(c => c.id === p.cardId ? { ...c, status: p.status, tone: p.tone ?? c.tone } : c)
+ set({ cards })
+ break
+ }
+ case 'card.resolved': {
+ const p = e.payload as { cardId: string }
+ const cards = state.cards.map(c => c.id === p.cardId ? { ...c, status: 'resolved' as const } : c)
+ set({ cards })
+ break
+ }
case 'artifact.created': {
if (state.currentProjectId) store.refreshArtifacts(state.currentProjectId)
break
@@ -319,6 +339,41 @@ export const store = {
}
},
+ upsertCard(card: ActivityCard) {
+ const i = state.cards.findIndex(c => c.id === card.id)
+ const cards = i >= 0 ? state.cards.map(c => c.id === card.id ? card : c) : [card, ...state.cards]
+ set({ cards })
+ },
+
+ async refreshCards(convId: string) {
+ try { set({ cards: await api.listCards(convId) }) } catch { /* 会话竞态,静默 */ }
+ },
+
+ // 卡片动作:确认投影卡(来源 hitl:)路由回 HITL 解析(恢复挂起 run,服务端同步销项);
+ // 其余卡走卡片动作 API,并把服务端持久化的回执消息追加进本地时间线(id 一致,对账不重复)。
+ async cardAction(card: ActivityCard, actionId: string, freeText?: string) {
+ const hitlRef = card.body?.find(n => n.type === 'kv')?.kv?.find(([k]) => k === '来源')?.[1]
+ if (hitlRef?.startsWith('hitl:')) {
+ await store.resolveHITL(hitlRef.slice(5), actionId ? { choiceId: actionId } : { freeText })
+ return
+ }
+ try {
+ const out = await api.cardAction(card.id, actionId, freeText)
+ store.upsertCard(out.card)
+ if (out.receipt) set({ messages: [...state.messages, out.receipt as Message] })
+ } catch (e) {
+ store.toast(`操作失败:${e}`)
+ }
+ },
+
+ async cardPin(card: ActivityCard, pinned: boolean) {
+ try { store.upsertCard((await api.cardPin(card.id, pinned)).card) } catch (e) { store.toast(`置顶失败:${e}`) }
+ },
+
+ async cardDismiss(card: ActivityCard) {
+ try { store.upsertCard((await api.cardDismiss(card.id)).card) } catch (e) { store.toast(`关闭失败:${e}`) }
+ },
+
async refreshArtifacts(projectId: string) {
try {
const artifacts = await api.listArtifacts(projectId)