From 82c08bd9831cdbff47949c711321a8b9c0cfb714 Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 13 Oct 2026 01:09:31 +0000 Subject: [PATCH 01/16] Consume persistent work contracts Why: keep the workbench wire types aligned with Core. Evidence: TypeScript check passed on the synchronized contract. Impact: generated types only. Refs: U2/U3 Task1 --- src/contracts.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/contracts.ts b/src/contracts.ts index da69011..4f20236 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -150,6 +150,7 @@ export interface ActivityCard { id: ID; projectId: ID; conversationId?: ID; sourceRunId?: ID; anchorMessageId?: ID; kind: CardKind; title: string; status: CardStatus; tone?: string; + task?: TaskSnapshot; body?: UINode[]; actions?: { id: string; label: string; style?: 'primary' | 'default' | 'danger'; command?: string }[]; pinned?: boolean; createdAt: string; updatedAt: string; } @@ -335,7 +336,7 @@ export interface PluginPageDecl { export interface ParsedDocument { meta: { - filename: string; mime?: string; parser: string; confidence?: number; + filename: string; mime?: string; parser: string; confidence?: number; origin?: DocumentOrigin; crossCheck?: { with: string; diffBlocks: number; verdict: 'agree' | 'differ' }; parsedAt: string; }; @@ -398,3 +399,24 @@ export interface SurfaceMeta { conversationId?: ID; updatedAt: string; } + +// Persistent work projection; host-owned identities and revisions. +export interface DocumentOrigin { artifactId: ID; contentHash: string; parserRevision: string } +export interface TaskPhase { id: string; label: string; observedAt?: string } +export interface TaskSourceRef { id: string; artifactId: ID; contentHash: string; parserRevision: string; blockIndex: number; label: string; quote?: string; availability?: string } +export interface TaskResultRef { artifactId?: ID; surfaceId?: ID; version?: string; contentHash?: string; availability: string; name?: string } +export interface TaskFinding { id: string; text: string; impact?: string; kind: string; status: string; revision: number; sources: TaskSourceRef[]; assumptions?: string; corrects?: string } +export interface TaskSnapshot { + schemaVersion: number; taskId: ID; attemptId: ID; cardId: ID; projectId: ID; conversationId: ID; + sourceMessageId?: ID; parentRunId?: ID; runId?: ID; ownerInstanceId: string; executionEpoch?: number; + revision: number; sourceSeq: number; streamScope: string; inputRevision?: string; title: string; goal?: string; + executionState: 'queued'|'running'|'awaiting_user'|'completed'|'failed'|'interrupted'; outcomeCompleteness: 'none'|'partial'|'complete'; deliveryState: 'pending'|'available'|'reported'; phase: TaskPhase; + focus?: string; coverage?: string; remaining?: string; nextStep?: string; conclusion?: string; + findings?: TaskFinding[]; actions?: UIAction[]; resultRefs?: TaskResultRef[]; lastActivityAt?: string; lastMeaningfulUpdateAt?: string; + checkpointKeys?: Record; checkpointCount?: number; checkpointSeq?: number; +} +export interface TaskSourceQuote { sourceId: string; blockIndex: number; quote: string } +export interface WorkFindingCandidate { findingKey: string; text: string; impact?: string; kind: string; assumptions?: string; corrects?: string; sources: TaskSourceQuote[] } +export interface WorkCheckpointCandidate { checkpointId: string; phaseId: string; focus?: string; remaining?: string; nextStep?: string; findings: WorkFindingCandidate[] } + +export interface UIAction { id: string; label: string; style?: string; command?: string } From 54fe339e65f7850dcb4da458adf4186baaf9c979 Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 13 Oct 2026 01:59:40 +0000 Subject: [PATCH 02/16] Recover task projections without regressing work or reading state Why: Replayed events, snapshot races, and source navigation must preserve authoritative work and user focus. Evidence: SELF make evidence records 213 frontend tests, type checks, knip ratchet, and production build. Impact: Uses the existing store for task revisions and gap replay; opens sources in the existing preview with version context. Refs: fast-slow-u2-u3 Task 5; generated public contract types account for the measured knip baseline increase --- knip-baseline.json | 2 +- src/api.ts | 2 ++ src/components/ConversationWork.tsx | 2 +- src/components/PreviewPane.tsx | 2 ++ src/components/Workbench.tsx | 10 ++++++ src/contracts.ts | 1 + src/previewtabs.ts | 3 +- src/store.ts | 47 ++++++++++++++++++++++++----- test/work-state.test.ts | 26 +++++++++++++++- 9 files changed, 83 insertions(+), 12 deletions(-) diff --git a/knip-baseline.json b/knip-baseline.json index 317deae..bb3c8e8 100644 --- a/knip-baseline.json +++ b/knip-baseline.json @@ -1,3 +1,3 @@ { - "exports": 43 + "exports": 52 } diff --git a/src/api.ts b/src/api.ts index 5ee4594..54feb33 100644 --- a/src/api.ts +++ b/src/api.ts @@ -76,6 +76,8 @@ 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)), + taskSource: (cardId:string,sourceId:string) => auth.fetch(`${BASE}/api/cards/${encodeURIComponent(cardId)}/sources/${encodeURIComponent(sourceId)}`).then(r=>json<{source:import('./contracts').TaskSourceRef;versionState:string;artifact?:Artifact}>(r)), + listCardsSnapshot: (conversationId: string): Promise<{cards: ActivityCard[]; replaySeq?: number}> => auth.fetch(`${BASE}/api/conversations/${conversationId}/cards`).then(r => json<{cards: ActivityCard[]; replaySeq?: number}>(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) => diff --git a/src/components/ConversationWork.tsx b/src/components/ConversationWork.tsx index b547141..3ed0f79 100644 --- a/src/components/ConversationWork.tsx +++ b/src/components/ConversationWork.tsx @@ -24,7 +24,7 @@ export function ConversationWork() { if (card.kind === 'confirm' && card.anchorMessageId && messages.some(message => message.role === 'user' && message.id === card.anchorMessageId)) continue const summary = activitySummary(card) - if (card.status === 'action_required' || summary.failed || (card.status === 'done' && summary.remaining)) required.push(card) + if (card.task?.findings?.some(f=>f.status==='active'&&(f.kind==='needs_user'||f.corrects)) || card.status === 'action_required' || summary.failed || (card.status === 'done' && summary.remaining)) required.push(card) else remaining.push(card) } // Preserve the host's newest-first order for completed results and history. diff --git a/src/components/PreviewPane.tsx b/src/components/PreviewPane.tsx index da8e27f..c9676a1 100644 --- a/src/components/PreviewPane.tsx +++ b/src/components/PreviewPane.tsx @@ -96,6 +96,7 @@ export function PreviewPane() { await api.saveArtifactContent(art.id, content ?? '') setDirty(false) store.toast('已保存') + const conv=store.getState().currentConvId;if(conv)void store.refreshCards(conv) }}>保存} )} @@ -108,6 +109,7 @@ export function PreviewPane() { )} + {active.sourceContext&&

{active.sourceContext.notice}

{active.sourceContext.quote&&
{active.sourceContext.quote}
}
}
{editing && textKind ? (