Every tool window keeps what the member typed, not just one

Fixkamo-internal
Shipped
September 6, 2026 at 12:02 AM UTC
Author
Kamo
Commit
62ed74f

Follow-up to c45b3857, which fixed the bug/improvement window and left four others named in a "known gaps" list. That list was the wrong answer: a gap recorded is still a gap, and a member losing a half-written text message does not care which window it was. First, the nav options are innocent, and this is measured rather than argued. A dev server running the real provider tree, driven by headless Chrome through router.push('/hr') → '/analytics' → back, logs one ToolDock mount, one SoftphoneProvider mount, zero unmounts and one document — and a system-bug window populated beforehand still holds its text at the end. Every route also answers an RSC navigation with text/x-component in production, so there is no MPA fallback either. What destroys a tool window is a tree REBUILD: an ancestor swapping element type, or a real document load — the session store's navigate('/logout') on a no/invalid session (including its five-minute background tick), apiClient's 401 handler, SessionManager's expiry and idle paths, the allowlisted /validate and OAuth jumps, a chunk that fails to fetch, bfcache eviction. None of those announce themselves, and none can be prevented by routing better. So close it at the window instead, for all eleven: - draftSeed(windowId, key, initial) + useDraftPersist(windowId, key, value) is the per-cell form, for the nine windows that keep their state in the leaf that draws it rather than in one object at the root. A seed function plus a write hook, deliberately NOT a custom hook returning its own setter: that shape stops looking like useState to the React Compiler and cost thirty-five new lint errors across these files before it was reshaped. - The softphone carries its half-dialled number, the note being taken beside a live call, and its tab. The number is not re-prefilled from the number the window was OPENED with, which would otherwise land on top of it. - SMS carries the message being written, the staged search and the CRM notes; the new-conversation pad carries the number. - The calculator carries all nine modes: the running calculation, the memory and angle mode, the programmer's buffer and base, the graphed functions and viewport, both converters' units and amounts, the emoji and character searches, and the text converter's input. - The support window carries the ticket being written, the rating comment, and the convert dialog — its open state as well as its text, because restoring a title into a dialog that closed restores nothing. - Send To AI carries the instruction, title and restatement an operator spends minutes on, and the report window remembers WHICH report it was handing over so the dialog comes back at all. Re-seeding stands down over a restored draft; a fresh opening still re-seeds, which is the rule that was already tested. - Chat, social and support composer drafts move out of useDraftParking's ref and into **************** — module scope plus per-tab sessionStorage, keyed by conversation because that is what a half-written reply belongs to. They were parked across a conversation switch and lost with the tree, which is the one case that actually happens. The guard gets teeth to match, and the gap list is gone: - It now asks per FILE, not per tool: a file with a field a member types into must be a file that persists it. Per tool would have passed a calculator where eight of nine modes still lost everything. - Two escapes, both written AT the control so they cannot drift from it: `tool-window-draft: transient` for something that is not content (a filter, a click-to-edit title committed on blur, an <input> the model wrote into its own markdown) and `tool-window-draft: lifted` for a control whose parent owns and persists it. `<TextField select>` is a dropdown and no longer counts. - No third category. "Known gap" existed for exactly as long as it took to close the four it named. 454 test files pass; the guard rejects both a new tool window with no persistence and an existing one that loses it; eslint reports one problem fewer than the untouched tree over the same files.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing