diff --git a/src/components/Conversation.tsx b/src/components/Conversation.tsx index 19b129a..263aca7 100644 --- a/src/components/Conversation.tsx +++ b/src/components/Conversation.tsx @@ -5,8 +5,8 @@ import { marked } from 'marked' import { api } from '../api' import { store, useStore, activeRun, type LiveState, type ContextObs } from '../store' import { liveToMessage, showProcessInline } from '../live' -import type { Message, Part, HITLCard } from '../contracts' -import { UIRenderer, InlineConfirmCard, hasOpenConfirm } from '@ui' +import type { Message, Part, HITLCard, ActivityCard } from '../contracts' +import { UIRenderer, InlineConfirmCard, hasOpenConfirm, CardAnchor } from '@ui' import { docBridge } from './Workbench' import { groupParts, artifactIcon } from '../messageparts' @@ -55,8 +55,16 @@ function PartView({ part, live }: { part: Part; live?: boolean | null }) { case 'plan': return null // 计划在右栏 Plan 标签呈现 case 'card': { - // cardlink 互定位(D32):点击 ↗ 定位收件箱对应卡(从 data 中解析 card:)。 - const m = /card:([A-Za-z0-9_]+)/.exec(JSON.stringify(part.card?.data ?? '')) + // 卡流锚点(D39):cardId 非空 = submit_card 引用部件——中栏自足(可展开全文)+ 跳收件箱。 + if (part.card?.cardId) { + const ref = part.card.cardId + const card = findCard(ref) + const bodyChars = card?.body?.reduce((n, node) => n + (node.markdown?.length ?? 0), 0) + return store.openInbox(ref)} docBridge={docBridge} /> + } + // 插件内嵌卡片部件(cardlink 互定位,D32):点击 ↗ 定位收件箱对应卡(从 data 解析 card:)。 + const m = JSON.stringify(part.card?.data ?? '').match(/card:([A-Za-z0-9_]+)/) return (
m && store.openInbox(m[1])}> @@ -77,6 +85,12 @@ function findArtifact(id: string) { return artifactCache[id] } +// 收件箱卡片查询缓存(store.cards 的派生索引;卡流锚点取卡数据用)。 +let cardCache: Record = {} +function findCard(id: string) { + return cardCache[id] +} + // 过程折叠:run 终态后把连续 process 部件折叠为摘要行(可展开回看)。 function ProcessCollapse({ parts }: { parts: Part[] }) { const toolCount = parts.filter(p => p.type === 'tool_call').length @@ -131,7 +145,7 @@ function AssistantMessage({ msg, live }: { msg: Message; live?: LiveState | null return -1 })() return ( -
+
助手 @@ -261,6 +275,7 @@ export function Conversation() { const bodyRef = useRef(null) artifactCache = Object.fromEntries(artifacts.map(a => [a.id, a])) + cardCache = Object.fromEntries(cards.map(c => [c.id, c])) const active = activeRun(runs) @@ -326,7 +341,7 @@ export function Conversation() { // 内联确认卡(T3):锚定本消息的危险命令确认,双现于对话流与收件箱。 const anchored = cards.filter(c => c.anchorMessageId === m.id && c.kind === 'confirm') return ( -
+
你
diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx index 477f637..bf3ab1d 100644 --- a/src/components/Workbench.tsx +++ b/src/components/Workbench.tsx @@ -33,7 +33,7 @@ export function Workbench() { 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), + onLocate: (c: ActivityCard) => store.locateMessage(c.anchorMessageId), }} />
diff --git a/src/contracts.ts b/src/contracts.ts index 2cdbb20..294e977 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -33,7 +33,7 @@ export interface Part { artifact?: { artifactId: ID; name: string; kind?: string }; hitl?: { cardId: ID; question: string; resolved: boolean; answer?: string }; plan?: PlanSnapshot; - card?: { kind: string; data: UINode[] }; + card?: { kind: string; data: UINode[]; cardId?: ID }; // cardId 非空 = 收件箱卡片引用部件(submit_card) error?: { code: string; message: string }; process?: boolean; // run 终态后折叠为摘要行 } diff --git a/src/store.ts b/src/store.ts index 522118f..885fb39 100644 --- a/src/store.ts +++ b/src/store.ts @@ -503,6 +503,13 @@ export const store = { document.querySelector(`[data-card-id="${cardId}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' }) }, 60) }, + // 卡→消息互定位另一半(D32):从收件箱卡滚回产生它的对话消息。 + locateMessage(messageId?: string) { + if (!messageId) return + setTimeout(() => { + document.querySelector(`[data-msg-id="${messageId}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' }) + }, 60) + }, openTracker(runId: string) { setUI({ trackerRunId: runId }) }, viewArtifact(a: Artifact | null) { setUI({ viewingArtifact: a }) }, openSettings(open: boolean) { setUI({ settingsOpen: open }) },