diff --git a/.project-docs/30-worklog/tasks/20260901-conversation-abort-stall-6f4c2a91.md b/.project-docs/30-worklog/tasks/20260901-conversation-abort-stall-6f4c2a91.md new file mode 100644 index 0000000..af1b238 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-conversation-abort-stall-6f4c2a91.md @@ -0,0 +1,54 @@ +# Task: Fix running conversation abort stall + +## Identity + +- Task ID: 20260901-conversation-abort-stall-6f4c2a91 +- Mode: Feature +- Branch: codex/20260901-conversation-abort-stall-6f4c2a91-conversation-abort-stall +- Worktree: D:\Datas\OthersProjects\makelore-conversation-abort-stall-6f4c2a91 +- Base commit: 143aaec3d6770dd263bf1e418e89bf722989caef +- Owner: codex-root +- Status: Completed + +## Scope + +- Diagnose the installed 1.2.1 state where a Conversation continued to render as running and neither visible abort entry point unlocked the UI. +- Correlate the screenshot with privacy-safe Main lifecycle and Pi Session metadata, then reproduce the stale-Renderer state at the public Chat seam. +- Make both the Header and Composer abort actions reconcile the authoritative Conversation Snapshot after the abort request completes. +- Add focused Renderer regression coverage and extend the existing Electron E2E fixture for a missed terminal SSE patch. + +## Intent And Constraints + +- Preserve ADR-006: Pi `0.84.2` remains the sole production runtime and Electron Main remains the only Pi/Host API authority. +- Do not replay accepted or uncertain work, change the abort RPC/Host API contract, add polling, or introduce a fallback/compatibility path. +- Treat the visible running state as evidence to investigate, not proof that the bash subprocess or Pi turn is still active. +- Keep the change at the existing Renderer Host API boundary and use the existing target-only Snapshot recovery semantics. +- Work only in the isolated feature worktree; do not modify the occupied `main` worktree or the completed 1.2.1 packaging task. + +## Outcome + +- Confirmed the screenshot was from installed Makelore 1.2.1. The affected Pi JSONL Session recorded three bash tool results and a final assistant `stop` by 14:55:35, while the 14:59 screenshot still rendered the earlier bash call as executing. Main later stopped the already-idle logical thread through background sleep, so the supported incident was stale Renderer state rather than an indefinitely running curl process. +- Reproduced the defect with a focused Chat test: the UI held a running Snapshot, the abort request succeeded against an already-terminal authority, but `getCodingConversationSnapshot` remained at one call and the UI stayed on `中止生成`. +- Added one shared `abortConversation` path in `CodingChatPanel`: after the existing POST abort completes, it silently reloads that Conversation's authoritative Snapshot. Both the Header `中止` button and Composer `中止生成` button now use this path. +- Kept error ownership in the existing controls: Header action failures remain local to Header, while Composer failures remain scoped to the originating draft/Conversation. +- Updated the Electron E2E host fixture so a deliberately missed terminal SSE patch becomes an authoritative aborted Snapshot only after the user clicks abort; the test now proves both abort controls disappear and runtime settings unlock after reconciliation. + +## Verification + +- Red regression before the fix: `tests/unit/coding-chat-panel.test.tsx` failed because the Snapshot API was called once instead of twice after abort. +- `pnpm exec vitest run tests/unit/coding-chat-panel.test.tsx tests/unit/coding-feature-ui.test.tsx tests/unit/coding-conversations-facade.test.ts tests/unit/pi-conversation-runtime.test.ts tests/unit/pi-worker-pool-process-integration.test.ts` passed: 5 files, 35 tests. +- `pnpm run typecheck` passed. +- Scoped ESLint over the two product files and three changed test files passed with no findings. +- `pnpm run build:vite` passed for Renderer, Electron Main, Preload, and release utility bundles; only existing bundle-size/dynamic-import warnings were reported. +- `pnpm exec playwright test tests/e2e/pi-coding-first-chat.spec.ts --grep "PI feature UI"` passed: 1/1 Electron E2E. +- `pnpm test` passed: 215 files / 1780 tests, 2 skipped, plus the isolated pressure test 1/1. +- `pnpm run lint:check` completed with 0 errors and 5 existing warnings in `src/pages/Home/index.tsx` and `src/pages/Makelore/index.tsx`; no warning is in a changed file. +- `git diff --check` passed. + +## Follow-ups + +- A new Windows installer must be built and installed before claiming the user's installed app contains this fix. Do not overwrite or relabel the already-built 1.2.1 artifact with changed source under the same version. + +## Promotion Candidates + +- None. This fix enforces the existing abort/terminal convergence and Snapshot recovery contracts without changing architecture or product direction. diff --git a/src/pages/Chat/CodingChatPanel.tsx b/src/pages/Chat/CodingChatPanel.tsx index 0e46119..1770f85 100644 --- a/src/pages/Chat/CodingChatPanel.tsx +++ b/src/pages/Chat/CodingChatPanel.tsx @@ -170,6 +170,10 @@ export function CodingChatPanel({ ? `new:${activeProject.id}:${selectedAgent.id}` : null; const draftKey = targetConversationId ?? provisionalDraftKey; + const abortConversation = useCallback(async (conversationId: string) => { + await abortCodingConversation(conversationId); + await loadConversationSnapshot(conversationId, 'silent'); + }, [loadConversationSnapshot]); const promptMode = draftKey ? modesByDraftKey[draftKey] ?? 'prompt' : 'prompt'; const provisionalDraft = provisionalDraftKey ? provisionalDrafts[provisionalDraftKey] ?? '' : ''; const submissionError = draftKey ? submissionErrors[draftKey] ?? null : null; @@ -598,6 +602,9 @@ export function CodingChatPanel({ onRename={async (title) => { if (targetConversationId) await patchConversation(targetConversationId, { title }); }} + onAbort={async () => { + if (targetConversationId) await abortConversation(targetConversationId); + }} onRecover={async () => { if (targetConversationId) await recoverConversation(targetConversationId); }} @@ -698,13 +705,15 @@ export function CodingChatPanel({ onSubmit={handleSubmit} onAbort={() => { if (!targetConversationId) return; - void abortCodingConversation(targetConversationId).catch((error) => { - if (!draftKey) return; - setSubmissionErrors((current) => ({ - ...current, - [draftKey]: localSubmissionError(error), - })); - }); + const conversationId = targetConversationId; + void abortConversation(conversationId) + .catch((error) => { + if (!draftKey) return; + setSubmissionErrors((current) => ({ + ...current, + [draftKey]: localSubmissionError(error), + })); + }); }} onRecover={() => { if (targetConversationId) { diff --git a/src/pages/Chat/CodingConversationHeader.tsx b/src/pages/Chat/CodingConversationHeader.tsx index 77f5336..522d399 100644 --- a/src/pages/Chat/CodingConversationHeader.tsx +++ b/src/pages/Chat/CodingConversationHeader.tsx @@ -16,7 +16,6 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; -import { abortCodingConversation } from '@/lib/coding-conversations'; import { cn } from '@/lib/utils'; import { useSettingsStore } from '@/stores/settings'; import type { ConversationSnapshot } from '@/types/coding-conversation'; @@ -54,11 +53,13 @@ export function CodingConversationHeader({ conversation, snapshot, onRename, + onAbort, onRecover, }: { conversation: CodingConversationMetadata | null; snapshot: ConversationSnapshot | null; onRename(title: string): Promise; + onAbort(): Promise; onRecover(): Promise; }) { const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed); @@ -128,7 +129,7 @@ export function CodingConversationHeader({ disabled={Boolean(busyAction) || !conversation} aria-label="中止" title="中止" - onClick={() => perform('abort', async () => abortCodingConversation(conversation!.id))} + onClick={() => perform('abort', onAbort)} > {busyAction === 'abort' ?