Parent: #1583
Context
The session entity rail's /amicode/problem resource (packages/app-bundle/overlay/packages/ui/src/amicode/entity-rail.tsx:135) is keyed only on the completed-amicode-part counter. When a fetch fails during a connection blip (amicodeGet has no timeout and rejects on any socket error / non-2xx — utils/amicode-fetch.ts:8), railState() (problem.ts:400-404) flips the rail to status unavailable (entity-rail.tsx:221-243) or a stale view. It then never refetches when the stream reconnects — only a new completed part, the Retry button, or a window reload recovers it. Slice 1 makes the stream itself reconnect; this slice makes the rail heal itself once it does.
Change
Refetch /amicode/problem on the stream's disconnect→connect edge (a genuine reconnect), driven by the app's existing serverSDK.event.status() signal — not on the first connect and not while steadily connected.
Acceptance criteria (TDD, red→green)
- Pure decision fn, unit-tested. Add
shouldRefetchOnReconnect(previous: boolean | undefined, next: boolean): boolean to packages/app-bundle/overlay/packages/ui/src/amicode/problem.ts, returning true iff previous === false && next === true (a rising edge that follows a real disconnect; undefined previous — the initial connect — returns false). Add tests to problem.test.ts (bun:test) covering: undefined → true ⇒ false (first connect, no refetch); false → true ⇒ true (reconnect); true → true ⇒ false; true → false ⇒ false. Runs in-place: cd packages/app-bundle/overlay/packages/ui && bun test --conditions=solid ./src/amicode/problem.test.ts.
- Rail wired.
AmicodeEntityRail accepts an optional streamConnected?: () => boolean prop. A createEffect tracks the previous value and, on a shouldRefetchOnReconnect-true transition, calls the resource's refetch(). No refetch fires on the initial connect or while steadily connected. Omitting the prop leaves behavior unchanged (host that can't supply status).
- App supplies status. The rail mount in
packages/app-bundle/overlay/packages/app/src/pages/session/timeline/message-timeline.tsx passes streamConnected={() => serverSDK.event.status() === "connected"} (serverSDK is already in scope in that component).
- Compiles.
pnpm --filter amicode run build:binary succeeds.
Files
packages/app-bundle/overlay/packages/ui/src/amicode/problem.ts (+ problem.test.ts)
packages/app-bundle/overlay/packages/ui/src/amicode/entity-rail.tsx (prop + effect)
packages/app-bundle/overlay/packages/app/src/pages/session/timeline/message-timeline.tsx (pass the prop)
Notes for the implementer
- The decision fn is pure and lives in the UI package's
problem.ts, which already runs its tests in-place (no solid-js/workspace imports needed for the test) — keep it dependency-free.
- The edge effect must not create a feedback loop: track
previous in a plain closure let, don't read the resource's own state inside the effect.
bun is at /Users/jj/.bun/bin/bun. Independent of Slice 1 (disjoint files) — grabbable in parallel.
Parent: #1583
Context
The session entity rail's
/amicode/problemresource (packages/app-bundle/overlay/packages/ui/src/amicode/entity-rail.tsx:135) is keyed only on the completed-amicode-part counter. When a fetch fails during a connection blip (amicodeGethas no timeout and rejects on any socket error / non-2xx —utils/amicode-fetch.ts:8),railState()(problem.ts:400-404) flips the rail tostatus unavailable(entity-rail.tsx:221-243) or a stale view. It then never refetches when the stream reconnects — only a new completed part, the Retry button, or a window reload recovers it. Slice 1 makes the stream itself reconnect; this slice makes the rail heal itself once it does.Change
Refetch
/amicode/problemon the stream's disconnect→connect edge (a genuine reconnect), driven by the app's existingserverSDK.event.status()signal — not on the first connect and not while steadily connected.Acceptance criteria (TDD, red→green)
shouldRefetchOnReconnect(previous: boolean | undefined, next: boolean): booleantopackages/app-bundle/overlay/packages/ui/src/amicode/problem.ts, returningtrueiffprevious === false && next === true(a rising edge that follows a real disconnect;undefinedprevious — the initial connect — returnsfalse). Add tests toproblem.test.ts(bun:test) covering:undefined → true ⇒ false(first connect, no refetch);false → true ⇒ true(reconnect);true → true ⇒ false;true → false ⇒ false. Runs in-place:cd packages/app-bundle/overlay/packages/ui && bun test --conditions=solid ./src/amicode/problem.test.ts.AmicodeEntityRailaccepts an optionalstreamConnected?: () => booleanprop. AcreateEffecttracks the previous value and, on ashouldRefetchOnReconnect-true transition, calls the resource'srefetch(). No refetch fires on the initial connect or while steadily connected. Omitting the prop leaves behavior unchanged (host that can't supply status).packages/app-bundle/overlay/packages/app/src/pages/session/timeline/message-timeline.tsxpassesstreamConnected={() => serverSDK.event.status() === "connected"}(serverSDKis already in scope in that component).pnpm --filter amicode run build:binarysucceeds.Files
packages/app-bundle/overlay/packages/ui/src/amicode/problem.ts(+problem.test.ts)packages/app-bundle/overlay/packages/ui/src/amicode/entity-rail.tsx(prop + effect)packages/app-bundle/overlay/packages/app/src/pages/session/timeline/message-timeline.tsx(pass the prop)Notes for the implementer
problem.ts, which already runs its tests in-place (nosolid-js/workspace imports needed for the test) — keep it dependency-free.previousin a plain closurelet, don't read the resource's own state inside the effect.bunis at/Users/jj/.bun/bin/bun. Independent of Slice 1 (disjoint files) — grabbable in parallel.