coach: reopen an interrupted conversation with a visible retry

An aborted stream now leaves a trace on both ends. The unmount cleanup
records the interrupted conversation (sessionStorage, one-shot) before
the abort fires, and invalidates the cached thread + rail so the return
visit refetches the advanced server state. The /coach page consumes the
record on its next mount: a known id reopens that thread, a first-turn
abort resolves via most-recent; every explicit entry intent (?c=, ?doc=,
?workout=, ?scope=, ?ask=) wins over the resume. The persisted cancelled
marker renders as a muted interrupted note with an Ask-again action that
resubmits the question through the normal send path and its gates.
Localised in all six languages. Refs #781
This commit is contained in:
Marc-André Bombeck committed 2026-08-13 06:16:45 +02:00
1 parent 9a631c8f8b
commit 13b7d0379b
13 files changed
+713 -230

No files matched your search

@@ -77,6 +77,35 @@ describe("Coach streaming lifecycle", () => {
).toBe(true);
});
it("the unmount cleanup records the interrupted conversation before the abort (#781)", () => {
const source = read("components/insights/coach-panel/use-coach.ts");
// A cleanup that writes the resume value under the storage key. Without
// it the unmount abort is silent again: the user returns to a blank new
// chat with no route back to the interrupted thread.
const recordsInterruption =
/return\s*\(\)\s*=>\s*\{[\s\S]*?interruptedResumeValue\(\s*active\s*\)[\s\S]*?sessionStorage\.setItem\(\s*COACH_INTERRUPTED_STORAGE_KEY/.test(
source,
);
expect(
recordsInterruption,
"use-coach.ts must record the interrupted conversation " +
"(COACH_INTERRUPTED_STORAGE_KEY) in an unmount cleanup",
).toBe(true);
// And the recording effect must be defined BEFORE the abort effect so
// the flag is written while the send is still marked in flight.
const recordIndex = source.indexOf("COACH_INTERRUPTED_STORAGE_KEY,\n");
const abortIndex = source.indexOf("abortRef.current?.abort();\n };");
expect(recordIndex).toBeGreaterThan(-1);
expect(abortIndex).toBeGreaterThan(-1);
expect(
recordIndex < abortIndex,
"the interruption-recording cleanup must be defined before the " +
"mount-scoped abort cleanup",
).toBe(true);
});
it("handleNewChat still resets too — the two paths must not diverge again", () => {
const source = read(
"components/insights/coach-panel/coach-conversation.tsx",