fix(tab-manager): stabilize AI chat — reactivity fixes, prompt hardening, and dead code removal#1527
Merged
fix(tab-manager): stabilize AI chat — reactivity fixes, prompt hardening, and dead code removal#1527
Conversation
Replace fixed h-[400px] with clamp(300px, 50vh, 600px) so the chat panel scales with viewport—comfortable on laptops and desktops alike.
Delete tab-actions.ts and move Chrome API execution logic directly into the workspace .withActions() mutation handlers. The file was a single-consumer indirection layer — every execute* function was only called from one corresponding handler in workspace.ts. Adds nativeTabId helper alongside the existing composite ID parsers and imports generateId for the save handler.
Shallow-clone messages and parts in onMessagesChange to break reference identity—TanStack AI's StreamProcessor mutates tool-call parts in place (output, state, approval) but SvelteMap doesn't deep-proxy, so Svelte 5's fine-grained reactivity missed in-place mutations on tool-call badges, approval UI, and tool results. Inject current device ID into the system prompt so the LLM knows which tabs are local vs read-only from other synced devices. Previously the AI would attempt to close tabs from other devices, resulting in closedCount: 0. Update system-prompt wording to clarify that destructive actions have their own approval UI (don't double-confirm in prose) and replace 'mutation' terminology with 'action'. Add visible logging (console.log for status/loading transitions, onError callback) to diagnose hung continuation requests where Stream 3 never resolves.
Replace 'mutation' with 'action' in tool-bridge JSDoc to match the terminology used throughout the codebase. Mark two completed success criteria in the progressive tool trust spec.
Update all TanStack AI packages from 0.5.x to 0.6.x: - @tanstack/ai: ^0.5.1 → ^0.6.3 - @tanstack/ai-anthropic: ^0.5.0 → ^0.6.0 - @tanstack/ai-openai: ^0.5.0 → ^0.6.0 - @tanstack/ai-client: ^0.4.5 → ^0.6.0 - @tanstack/ai-gemini: ^0.5.0 → ^0.8.0 - @tanstack/ai-grok: ^0.5.0 → ^0.6.0 - @tanstack/ai-svelte: ^0.5.4 → ^0.6.4 The new version tightens the tools parameter in chat() from object[] to Tool[]. Destructure tools separately and assert the proper type in ai-chat.ts to satisfy the stricter signature.
OPENAI_CHAT_MODELS[0] is now the default model for new conversations, picking up the latest model from the updated provider package.
Three prompt architecture improvements per expert review:
1. Split system prompt into separate messages — device constraints
in their own system message, base/custom prompt in a second.
2. Rewrite device block as hard constraints using imperative language
('Never call a mutating tool for any tab ID that does not start
with…') instead of softer advisory phrasing.
3. Immutable safety block — device constraints always first, even
when a conversation overrides the base prompt.
Add a 60-second timeout for the 'submitted' status. After a tool
executes and the ChatClient fires a continuation request, the server
sometimes never starts streaming (LLM API timeout, rate limiting,
network issue). Without a timeout the loading dots persist forever.
Now auto-stops and surfaces an error after 60 seconds.
# Conflicts: # apps/tab-manager/src/lib/state/chat-state.svelte.ts # apps/tab-manager/src/lib/tab-actions.ts # apps/tab-manager/src/lib/workspace.ts
…allSettled, and batch operations
…ient.extensions.sync.reconnect() All consumers now call through the workspace client directly, removing a layer of indirection and keeping everything derived from the single workspaceClient instance.
… toNativeIds reconnectSync() has no remaining callers after the previous commit. Move toNativeIds helper to bottom of file near the action handlers that use it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The tab-manager's AI chat feature was built on early-alpha TanStack AI. It worked, but accumulated a layer of workarounds and indirection that made it fragile. This branch is a focused stabilization pass: fix the bugs we could reproduce, harden the prompt architecture so the LLM stops acting on tabs it can't control, and remove dead code that was just getting in the way.
The reactivity bug
TanStack AI's
StreamProcessormutates tool-call parts in place — it writesoutput,state, andapprovaldirectly onto existing objects. Svelte 5's fine-grained reactivity tracks object identity, andSvelteMapdoesn't deep-proxy its values. So when TanStack mutated a tool-call part, Svelte never noticed. Tool-call badges, approval UI, and results just… didn't update.The fix is a shallow clone on every
onMessagesChangecallback:This is the correct approach for raw
ChatClient. A future migration to@tanstack/ai-svelte'suseChat(which owns the$stateinternally) would eliminate the need for manual cloning entirely — that's tracked in a separate spec.The cross-device problem
The AI was happily trying to close tabs from other synced devices — and succeeding at calling the Chrome API with those IDs, which returned
closedCount: 0with no error. The LLM had no way to know which tabs were local.The fix was two-layered: inject the current device ID into a separate, immutable system message so the LLM knows the rules before it acts, and write those rules as hard constraints rather than suggestions:
The prompt architecture now sends two system messages — device constraints first (always), then the base/custom prompt second. Even if a conversation overrides its system prompt, the device constraints can't be bypassed:
Removing the indirection layer
tab-actions.ts(345 lines) was a single-consumer indirection layer. Everyexecute*function was called from exactly one.withActions()handler inworkspace.ts. The file existed because the actions were defined before the workspace had a.withActions()API — once that API landed, the indirection was just noise.Deleting it and inlining the Chrome API calls directly into the mutation handlers made the code easier to follow and revealed opportunities for
tryAsync,Promise.allSettled, and batch operations that weren't obvious when the logic was split across two files:Similarly,
reconnectSync()was a one-liner wrapper aroundworkspaceClient.extensions.sync.reconnect()— five callsites were using the wrapper instead of the method directly. Removed the wrapper, updated the callsites.Everything else
TanStack AI 0.5.x → 0.6.x — all seven
@tanstack/ai-*packages bumped. The new version tightens thetoolsparameter fromobject[]toTool[], requiring a type assertion inai-chat.ts.Default provider → OpenAI —
OPENAI_CHAT_MODELS[0]is now the default model for new conversations.Continuation timeout — after a tool executes, the
ChatClientfires a continuation request for the LLM to respond to the tool result. Sometimes the server never starts streaming (API timeout, rate limiting, network). Without a timeout, the loading dots persist forever. Now auto-stops after 60 seconds and surfaces an error.Responsive chat drawer — replaced fixed
h-[400px]withclamp(300px, 50vh, 600px)so the panel scales with viewport.Terminology —
mutation→actionacross tool-bridge JSDoc and the progressive tool trust spec. Matches the naming convention used everywhere else.Changelog