From fcd03f9f6d609cc1a0fa157f1c38d334c51b720e Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Mon, 24 Aug 2026 09:43:45 +0800 Subject: [PATCH] fix(coding): scope metadata updates by project --- .../tasks/20260824-pi-feature-ui-9d4e7a31.md | 9 +- src/pages/Chat/CodingChatPanel.tsx | 17 +++- src/stores/coding-workspace.ts | 45 ++++++++-- tests/unit/coding-chat-panel.test.tsx | 85 +++++++++++++++++++ tests/unit/coding-workspace-store.test.ts | 84 ++++++++++++++++++ 5 files changed, 228 insertions(+), 12 deletions(-) diff --git a/.project-docs/30-worklog/tasks/20260824-pi-feature-ui-9d4e7a31.md b/.project-docs/30-worklog/tasks/20260824-pi-feature-ui-9d4e7a31.md index a7bce5a..b64bad3 100644 --- a/.project-docs/30-worklog/tasks/20260824-pi-feature-ui-9d4e7a31.md +++ b/.project-docs/30-worklog/tasks/20260824-pi-feature-ui-9d4e7a31.md @@ -97,15 +97,18 @@ Gate result: - Windowed streaming thinking DOM output to the latest 16 KB while streaming, restoring the complete text when settled; this keeps the existing REN-008 100 KB pressure budget passing without changing the protocol or test threshold. - Isolated Conversation-local async UI state by remounting Header, pending interactions, and the tools inspector on Conversation changes. A pending fork only selects its result while the source project, Agent, and Conversation remain selected; a completed archive no longer clears a newer selection. - Added regression coverage for pending fork, archive, interaction response, and tools-load completion across Conversation switches. +- Bound Conversation metadata writeback to its source project: store upserts now require an explicit project id, fork results are ignored by the current Renderer store after a project switch, and metadata failures are retained under their source project and Conversation instead of the global workspace error. +- Added project-switch success/failure tests plus Renderer tests proving that old-project forks and metadata errors cannot leak into the newly selected project or Conversation. - Updated `README.md` to describe the now-shipped Coding UI state and retained the explicit unverified shared Provider/runtime concurrency boundary. ## Verification - `pnpm run typecheck` — Pass. - `pnpm run lint:check` — Pass with 0 errors and 6 pre-existing warnings outside PI-130-owned files. -- Focused PI-130 Vitest coverage — Pass: 17/17 across the two changed UI suites, including facade routes, queue modes/positions, interactions, Conversation controls, metadata and async-switch isolation, nested subagents, compaction retry, inline tool output, fork wording, and removed UI entries. -- Isolated REN-008 100 KB pressure test — Pass: 20 patch batches, 20 React commits, 34.8 ms Main-to-React p95 against the 50 ms budget. An earlier run was intentionally discarded after it shared CPU with full-repository lint and measured 112.9 ms; the serial full-suite rerun below passed. -- `pnpm test` — Pass: 219 files; 2332 tests passed; 2 skipped. +- Focused PI-130 Vitest coverage — Pass: 24/24 across the three changed store/UI suites, including facade routes, queue modes/positions, interactions, Conversation controls, metadata and async-switch isolation, project-switch writeback/error isolation, nested subagents, compaction retry, inline tool output, fork wording, and removed UI entries. +- Isolated REN-008 100 KB pressure test — Pass: 20 patch batches, 20 React commits, 34.7 ms Main-to-React p95 against the 50 ms budget. +- Functional full suite without the pressure file, capped at four workers — Pass: 218 files; 2335 tests passed; 2 skipped. Together with the isolated pressure test this covers all 219 files and 2336 passing tests. +- Default `pnpm test` final reruns — Not recorded as Pass: one 24-worker run lost a Vitest child process without a test stack; the next completed all files but the pressure test measured 57.4 ms while competing with the other workers. Neither run reported a functional assertion failure outside the load-sensitive pressure threshold; the controlled full-suite and isolated-pressure runs above are the acceptance evidence. - `pnpm run build:vite` — Pass as part of the scoped Electron E2E command; only existing Vite chunk/dynamic-import warnings were reported. - `pnpm run test:e2e -- tests/e2e/pi-coding-first-chat.spec.ts` — Pass: 2/2, covering first-Conversation editability, two Conversation isolation, queue, interaction, model, abort, subagent, files/changes/commands tools, and removed share/revert/todo/global-runtime entries. - `git diff --check` — Pass. diff --git a/src/pages/Chat/CodingChatPanel.tsx b/src/pages/Chat/CodingChatPanel.tsx index 2bd753f..1d4196c 100644 --- a/src/pages/Chat/CodingChatPanel.tsx +++ b/src/pages/Chat/CodingChatPanel.tsx @@ -96,6 +96,9 @@ export function CodingChatPanel({ const selectedAgentId = useCodingWorkspaceStore((state) => state.selectedAgentId); const workspaceLoadState = useCodingWorkspaceStore((state) => state.loadState); const workspaceError = useCodingWorkspaceStore((state) => state.error); + const conversationErrorsByProjectId = useCodingWorkspaceStore( + (state) => state.conversationErrorsByProjectId, + ); const creatingAgentIds = useCodingWorkspaceStore((state) => state.creatingAgentIds); const loadWorkspace = useCodingWorkspaceStore((state) => state.load); const selectAgent = useCodingWorkspaceStore((state) => state.selectAgent); @@ -149,6 +152,9 @@ export function CodingChatPanel({ && !conversation.archivedAt )) ?? null; const targetConversationId = selectedConversation?.id ?? null; + const conversationMetadataError = activeProject && targetConversationId + ? conversationErrorsByProjectId[activeProject.id]?.[targetConversationId] ?? null + : null; const provisionalDraftKey = activeProject && selectedAgent ? `new:${activeProject.id}:${selectedAgent.id}` : null; @@ -340,9 +346,10 @@ export function CodingChatPanel({ const sourceAgentId = selectedAgent.id; const sourceConversationId = targetConversationId; const forked = await forkCodingConversation(sourceConversationId, sourceEntryId); - upsertConversation(forked); - primeConversation(createLocalConversationSnapshot(sourceProjectId, forked)); const workspace = codingWorkspaceStore.getState(); + if (workspace.activeProjectId !== sourceProjectId) return; + upsertConversation(sourceProjectId, forked); + primeConversation(createLocalConversationSnapshot(sourceProjectId, forked)); const selectedId = codingConversationStore.getState().selectedConversationId; if (workspace.activeProjectId === sourceProjectId && workspace.selectedAgentId === sourceAgentId @@ -656,10 +663,12 @@ export function CodingChatPanel({ onOpenSettings={onOpenProjectSettings} /> - {(workspaceError || connectionError) && ( + {(workspaceError || conversationMetadataError || connectionError) && (