Skip to content

docs(chat): v2 chat architecture proposals + reference research#3716

Merged
AviPeltz merged 13 commits intomainfrom
v2-chat-architecture-rearchitect-with-trpc-and-web
Apr 27, 2026
Merged

docs(chat): v2 chat architecture proposals + reference research#3716
AviPeltz merged 13 commits intomainfrom
v2-chat-architecture-rearchitect-with-trpc-and-web

Conversation

@AviPeltz
Copy link
Copy Markdown
Collaborator

@AviPeltz AviPeltz commented Apr 25, 2026

Summary

Plans-only PR. No code changes. Adds the chat-architecture research and proposals for the v2 chat rearchitect, plus a pragmatic v1→host-service migration plan that unblocks the greenfield work.

Greenfield architecture

  • plans/v2-chat-greenfield-architecture.md — proposed transport + state architecture. tRPC subscriptions over WS, EventLog interface with LocalEventLog (SQLite) for P0–P4 and two cloud paths (Postgres / Durable Objects) for P5. P6 covers device handoff (turn-boundary, git-as-courier). P7 sketches mid-turn handoff but explicitly defers it.

Fast migration

  • plans/v1-to-v2-fast-migration.md — ship-this-month plan. Move ChatRuntimeService into host-service, collapse getDisplayState + listMessages into one atomic getSnapshot query (kills the dual-poll race), gate behind a per-workspace flag, canary, GA, delete legacy paths. ~2–3 weeks for one engineer.

Reference research

Architectural notes from three external chat systems studied while designing the above:

  • plans/t3code-chat-architecture-reference.md — Effect-RPC + per-session SQLite event log + dual stream split.
  • plans/opencode-electron-chat-architecture-reference.md — in-process Electron sidecar + REST writes / SSE reads + append-style delta events.
  • plans/background-agents-chat-architecture-reference.md — Cloudflare Durable Objects per session, multiplayer, parallel sub-tasks, Modal sandboxes.

Each reference doc has both a Mermaid and a plain-text architecture diagram, plus a "Signal for our rearchitecture" section.

Test plan

  • n/a — docs only, no code changes.
  • Mermaid diagrams render in GitHub PR view.

Summary by cubic

Adds v2 chat architecture proposals and reference research, and ships the v2 chat pane with snapshot-based state, real model‑provider auth, reliable runtime cleanup, pre‑session slash commands, and a fix for first‑message flicker. Also reduces idle rerenders by removing a timestamp from snapshots and memoizing the slash‑command mapping.

  • New Features

    • Enable the v2 ChatPane with session wiring, unread badges, and optimistic delete.
    • Decouple TiptapPromptEditor via prop injection for file search and slash previews: v1 uses @superset/chat/client, v2 uses @superset/workspace-client; the chip preview stays off in v2.
    • Add host‑service auth router and a long‑lived ChatService (@superset/chat/server/desktop); wire the v2 ModelPicker to workspaceTrpc.auth.* for real Anthropic/OpenAI status with refetch on open.
    • Wire slash commands to the host‑service; make them workspace‑scoped and usable before a session; resolve custom commands via workspaceTrpc.chat.resolveSlashCommand.
  • Bug Fixes

    • Replace dual polling with chat.getSnapshot; render the first user message using local optimistic state (not the cache) so it appears immediately without flicker.
    • Unify display shaping via buildDisplayState, restore the answeredQuestionIds filter, keep a consistent sandbox shape, and pass fallback observer/reflector models based on the authenticated provider to avoid post‑turn reflection failures without Google API keys.
    • Dispose runtimes on session delete via chat.endSession/disposeRuntime, now validated by workspaceId, awaiting in‑flight creations, and disconnecting the MCP manager; router passes workspaceId.
    • Guard in‑flight runtime creation by workspaceId, update cloud lastActiveAt on send, drop the fps: 60 override to cut RPC load, and reduce idle rerenders by removing observedAt from getSnapshot responses and memoizing the slashCommands select mapper.

Written for commit 6a8c264. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Editor: integrated file search and slash-command preview capabilities.
    • Chat runtime: snapshot-based retrieval, session disposal endpoint, and improved send-message flow.
    • Auth: provider management endpoints and model selector refresh before showing options.
    • Exposed slash-command APIs for resolution.
  • Bug Fixes

    • Prevented cross-workspace session races and improved runtime creation/disposal reliability.
  • Documentation

    • Added multiple chat architecture reference guides.

Plans-only PR. Adds the chat-architecture research and proposals for
the v2 chat rearchitect, plus a pragmatic v1->host-service migration
plan that unblocks the greenfield work.

- v2-chat-greenfield-architecture.md: tRPC subscriptions over WS,
  EventLog interface, P0-P4 race-fix + LocalEventLog (SQLite),
  P5a (Postgres) / P5b (Durable Objects), P6 (handoff via git
  courier), P7 (mid-turn handoff, deferred).

- v1-to-v2-fast-migration.md: ship-this-month plan. Move
  ChatRuntimeService into host-service, collapse two polling
  queries into one atomic getSnapshot, gate behind per-workspace
  flag, canary, GA, delete legacy paths.

- t3code/opencode/background-agents reference docs: research
  notes informing the design.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Injects async workspace ops into editor/footer (file search, slash-preview), consolidates chat retrieval via a new chat.getSnapshot TRPC endpoint, adds runtime lifecycle APIs (disposeRuntime, getSnapshot, endSession), mounts an auth TRPC surface backed by ChatService, and adds multiple architecture reference documents.

Changes

Cohort / File(s) Summary
Editor & Input Footer
apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/ChatInputFooter.tsx, apps/desktop/src/renderer/routes/.../ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/ChatInputFooter.tsx, apps/desktop/src/renderer/components/Chat/ChatInterface/components/TiptapPromptEditor/TiptapPromptEditor.tsx, apps/desktop/src/renderer/components/Chat/ChatInterface/components/TiptapPromptEditor/SlashCommandPreviewPopover.tsx
Replaces in-component TRPC queries with injected async callbacks: searchFiles and optional previewSlashCommand. Adds local state/effects (debounce, cancellation), new exported types (PreviewSlashCommandFn, SlashPreviewResult), and wires callbacks into TiptapPromptEditor.
Host-service Runtime & TRPC Chat Router
packages/host-service/src/runtime/chat/chat.ts, packages/host-service/src/trpc/router/chat/chat.ts, packages/trpc/src/router/chat/chat.ts
Adds workspace-aware runtime lifecycle: enforces workspace consistency for in-flight creation, introduces disposeRuntime and getSnapshot, consolidates display/messages into snapshots. Exposes chat.getSnapshot and chat.endSession TRPC procedures; sendMessage now awaits runtime send and fire-and-forget updates updateSession (optional lastActiveAt).
Client Hooks & Controllers
apps/desktop/src/renderer/routes/.../hooks/usePaneRegistry/usePaneRegistry.tsx, .../useWorkspaceChatDisplay/useWorkspaceChatDisplay.ts, .../useWorkspaceChatController/useWorkspaceChatController.ts, .../ChatPane/components/WorkspaceChatInterface/ChatPaneInterface.tsx
Switches display hook to chat.getSnapshot (combines displayState + messages), relaxes currentMessage typing, removes explicit fps arg, and invokes new endSession mutation non-blocking on session delete. Pane registry now renders active ChatPane and forwards pane state/callbacks.
Auth Router & Host-service Runtime Shape
packages/host-service/src/trpc/router/auth/auth.ts, packages/host-service/src/trpc/router/auth/index.ts, packages/host-service/src/trpc/router/router.ts, packages/host-service/src/app.ts, packages/host-service/src/types.ts
Adds new authRouter with OAuth/key endpoints delegating to ctx.runtime.auth (ChatService), re-exports and mounts auth on appRouter, injects auth into HostServiceRuntime, and composes ChatService into the runtime.
Model Picker UI
apps/desktop/src/renderer/routes/.../ModelPicker/ModelPicker.tsx
Adds TRPC checks for Anthropic/OpenAI auth status and refetches statuses when the model selector opens.
Slash Command Execution
apps/desktop/src/renderer/routes/.../useSlashCommandExecutor/useSlashCommandExecutor.ts
Unrecognized slash commands now call workspaceTrpc.chat.resolveSlashCommand, normalize results, surface errors as toast, and track slash-command usage when handled.
Runtime/session lifecycle (public API)
packages/host-service/src/runtime/chat/chat.ts
Enforces workspaceId consistency for concurrent runtime creation, tracks in-flight creations, and exposes disposeRuntime and getSnapshot returning normalized { displayState, messages, observedAt }. Updates slash-command resolution to delegate to workspace CWD and allow "custom" kind.
Public exports for slash-commands
packages/chat/src/server/desktop/index.ts
Re-exports SlashCommand, getSlashCommands, and resolveSlashCommand to the package public surface.
Architecture Documentation
plans/background-agents-chat-architecture-reference.md, plans/opencode-electron-chat-architecture-reference.md, plans/t3code-chat-architecture-reference.md, plans/v1-to-v2-fast-migration.md, plans/v2-chat-greenfield-architecture.md
Adds five architecture reference documents covering background agents, Electron transport, T3 Code event-log ideas, v1→v2 migration plan, and v2 event-log greenfield proposal.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant TRPC as TRPC API
  participant Runtime as HostService Runtime
  participant DB as Persistence

  Client->>TRPC: query chat.getSnapshot({ sessionId, workspaceId })
  TRPC->>Runtime: getSnapshot(sessionId, workspaceId)
  Runtime->>DB: fetch messages & display state
  DB-->>Runtime: messages + persisted state
  Runtime-->>TRPC: { displayState, messages, observedAt }
  TRPC-->>Client: snapshot response

  Client->>TRPC: mutation chat.endSession({ sessionId, workspaceId })
  TRPC->>Runtime: disposeRuntime(sessionId, workspaceId)
  Runtime->>Runtime: abort harness & cleanup
  Runtime-->>TRPC: { ok: true }
  TRPC-->>Client: success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I hopped through code with nimble paws,

I wired search and slash-preview laws,
Snapshots rounded, runtimes told to sleep,
Auth doors opened, docs piled deep,
A carrot nudge — now ship the leap! 🍪🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'docs(chat): v2 chat architecture proposals + reference research' clearly and specifically describes the main change: addition of documentation files proposing and researching v2 chat architecture.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering greenfield architecture, fast migration plan, and reference research with clear context for each addition.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v2-chat-architecture-rearchitect-with-trpc-and-web

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 25, 2026

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Vercel API (Vercel) Open Preview
Vercel Web (Vercel) Open Preview
Vercel Marketing (Vercel) Open Preview
Vercel Admin (Vercel) Open Preview
Vercel Docs (Vercel) Open Preview

Preview updates automatically with new commits

Adds an Implementation Audit section (file inventory, what's solid,
stubs, gaps, verified bugs), a Fixes-at-a-glance priority table,
and restructures Phased Migration so P0 captures the two HIGH
defects concretely:

- Runtime leak on session delete (no disposeRuntime / endSession)
- Cross-workspace sessionId race in runtimeCreations map

P1 folds in the dual-poll race fix via a single getSnapshot query,
the fps:60 -> default 4fps polling cadence drop, and the missing
cloud lastActiveAt update on host send (selector ordering).

P4 enumerates parity gaps with v1 file references; Stop and
Notification hook events explicitly deferred per scope decision.
Lands the P0 + P1 fixes from plans/v1-to-v2-fast-migration.md.

Host-service runtime (packages/host-service/src/runtime/chat/chat.ts):
- runtimeCreations map now stores { workspaceId, promise } and
  validates the workspace at the in-flight path. Concurrent requests
  with the same sessionId across different workspaceIds no longer
  silently share the wrong-workspace runtime.
- Adds disposeRuntime(sessionId) - aborts harness, drops from map,
  idempotent. Closes the runtime leak on session delete.
- Adds getSnapshot(input) returning { displayState, messages,
  observedAt } from a single observation. Reuses the existing
  display-state shaping logic.

Host-service router (packages/host-service/src/trpc/router/chat/chat.ts):
- New chat.getSnapshot query.
- New chat.endSession mutation.
- chat.sendMessage fires-and-forgets ctx.api.chat.updateSession with
  lastActiveAt: new Date() after success so the v2 session selector
  keeps reordering after activity.

Cloud router (packages/trpc/src/router/chat/chat.ts):
- updateSession now accepts an optional lastActiveAt: z.date().

V2-workspace ChatPane:
- useWorkspaceChatDisplay: replaces dual getDisplayState +
  listMessages queries with a single getSnapshot query. Derives
  state from snapshot.displayState / snapshot.messages.
- ChatPaneInterface: drops fps: 60 override (~120 RPCs/sec/pane)
  in favor of the hook's default fps: 4 (matches v1).
- useWorkspaceChatController: handleDeleteSession now also calls
  workspaceTrpc.chat.endSession after the cloud delete to tear
  down the host-service runtime.

P4 parity gaps (slash commands, file search, hooks, title
generation, MCP, model auth state) are deferred per scope.
Drops the last v1-IPC coupling from TiptapPromptEditor by switching the
file-search and slash-command-preview calls to prop injection. v1's
ChatInputFooter wires them to chatServiceTrpc (legacy IPC, unchanged);
v2's wires searchFiles to workspaceTrpc.filesystem.searchFiles so the
@-mention popup works for both local and remote host-service workspaces.

The chip-anchored SlashCommandPreviewPopover becomes opt-in via prop and
stays off in v2, which already mounts its own SlashCommandPreview using
workspaceTrpc.chat.previewSlashCommand.

usePaneRegistry now mounts the real <ChatPane> instead of the
"temporarily disabled" placeholder, threading sessionId / launchConfig
through ctx.actions.updateData.
…-web

Conflicts were compose-don't-pick:

- useWorkspaceChatController.ts: main switched the cloud delete to an
  optimistic transaction via useOptimisticCollectionActions. Kept that,
  then still fire workspaceTrpc.chat.endSession after to tear down the
  host-service runtime.

- usePaneRegistry.tsx: main added a renderTitle with
  V2NotificationStatusIndicator, shrunk the chat icon to size-3.5, and
  dropped browserRuntimeRegistry from the imports (browser destruction
  is now handled by useGlobalBrowserLifecycle). Kept all of that and
  replaced the placeholder renderPane with the real <ChatPane>.

Net effect: v2 chat is enabled, sessions delete optimistically, the
host-service runtime is disposed on delete, and chat tabs show unread
notification badges.
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 27, 2026

Greptile Summary

This PR ships P0/P1 bug fixes alongside architecture docs: it replaces the dual getDisplayState+listMessages poll with a single getSnapshot RPC (eliminating the client-side race), adds disposeRuntime/endSession to stop runtime leaks on session delete, guards inflight runtime creation by workspaceId, fires a lastActiveAt update on send, and removes the fps: 60 override to cut RPC load.

  • P1 — runtime leak in disposeRuntime: if endSession is called while the runtime is still being created (in runtimeCreations but not yet in runtimes), disposeRuntime returns early and the creation completes normally, inserting a leaked runtime into runtimes with no future cleanup. Needs to also cancel/discard the inflight entry.

Confidence Score: 4/5

Safe to merge after addressing the disposeRuntime inflight-creation leak.

One P1 defect: disposeRuntime silently no-ops when the runtime is still being created, causing a guaranteed leak for sessions deleted immediately after first open. All other changes are clean.

packages/host-service/src/runtime/chat/chat.ts — disposeRuntime method

Important Files Changed

Filename Overview
packages/host-service/src/runtime/chat/chat.ts Adds getSnapshot (combined display-state + messages) and disposeRuntime. The dispose function has a P1 leak: it skips cleanup when the runtime is still in-flight in runtimeCreations.
packages/host-service/src/trpc/router/chat/chat.ts Exposes getSnapshot query and endSession mutation; adds fire-and-forget lastActiveAt update on sendMessage. Logic is straightforward and correct.
packages/trpc/src/router/chat/chat.ts Adds optional lastActiveAt field to updateSession schema and applies it to the DB update. Clean, no issues.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/hooks/useWorkspaceChatDisplay/useWorkspaceChatDisplay.ts Replaces dual getDisplayState + listMessages queries with a single getSnapshot query, eliminating the client-side dual-poll race. Migration is clean.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/hooks/useWorkspaceChatController/useWorkspaceChatController.ts Adds endSession mutation call on session delete to tear down host-service runtime; correctly fire-and-forgets so UI delete is never blocked.

Sequence Diagram

sequenceDiagram
    participant UI as ChatPane (React)
    participant TRPC as host-service tRPC
    participant MGR as ChatRuntimeManager
    participant H as RuntimeHarness

    Note over UI,H: Before – dual poll (race condition)
    UI->>TRPC: getDisplayState(sessionId)
    UI->>TRPC: listMessages(sessionId)
    TRPC-->>UI: displayState (t₁)
    TRPC-->>UI: messages (t₂ ≠ t₁ → mismatch)

    Note over UI,H: After – single snapshot
    UI->>TRPC: getSnapshot(sessionId)
    TRPC->>MGR: getSnapshot(sessionId, workspaceId)
    MGR->>H: getDisplayState()
    H-->>MGR: displayStateRaw
    MGR->>H: listMessages()
    H-->>MGR: messages
    MGR-->>TRPC: {displayState, messages, observedAt}
    TRPC-->>UI: snapshot

    Note over UI,H: Session delete flow
    UI->>TRPC: deleteSessionRecord(sessionId)
    TRPC-->>UI: ok
    UI-->>TRPC: endSession(sessionId) [fire-and-forget]
    TRPC->>MGR: disposeRuntime(sessionId)
    MGR->>H: abort()
    MGR-->>TRPC: ok
Loading

Comments Outside Diff (2)

  1. packages/host-service/src/runtime/chat/chat.ts, line 470-480 (link)

    P1 disposeRuntime misses inflight creations, causing a runtime leak

    If disposeRuntime is called while the runtime for sessionId is still being created (i.e. it exists in runtimeCreations but not yet in runtimes), the early-return on line 472 causes the function to silently no-op. The in-flight createRuntime promise then completes, calls runtimeCreations.delete in its .finally(), and inserts itself into runtimes — permanently leaking the runtime with no future cleanup path.

    This will reliably trigger when a user deletes a session immediately after opening it for the first time (before the first getSnapshot poll resolves).

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/host-service/src/runtime/chat/chat.ts
    Line: 470-480
    
    Comment:
    **`disposeRuntime` misses inflight creations, causing a runtime leak**
    
    If `disposeRuntime` is called while the runtime for `sessionId` is still being created (i.e. it exists in `runtimeCreations` but not yet in `runtimes`), the early-return on line 472 causes the function to silently no-op. The in-flight `createRuntime` promise then completes, calls `runtimeCreations.delete` in its `.finally()`, and inserts itself into `runtimes` — permanently leaking the runtime with no future cleanup path.
    
    This will reliably trigger when a user deletes a session immediately after opening it for the first time (before the first `getSnapshot` poll resolves).
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. packages/host-service/src/runtime/chat/chat.ts, line 216-218 (link)

    P2 observedAt timestamp is recorded after data collection, not before

    observedAt = Date.now() is set after listMessages() resolves rather than before the first read. The timestamp therefore reflects the end of the snapshot window, not its start. For a value named observedAt (implying "the state as of this moment"), it would be more accurate to record the timestamp before reading displayStateRaw.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/host-service/src/runtime/chat/chat.ts
    Line: 216-218
    
    Comment:
    **`observedAt` timestamp is recorded after data collection, not before**
    
    `observedAt = Date.now()` is set after `listMessages()` resolves rather than before the first read. The timestamp therefore reflects the end of the snapshot window, not its start. For a value named `observedAt` (implying "the state as of this moment"), it would be more accurate to record the timestamp before reading `displayStateRaw`.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/host-service/src/runtime/chat/chat.ts
Line: 470-480

Comment:
**`disposeRuntime` misses inflight creations, causing a runtime leak**

If `disposeRuntime` is called while the runtime for `sessionId` is still being created (i.e. it exists in `runtimeCreations` but not yet in `runtimes`), the early-return on line 472 causes the function to silently no-op. The in-flight `createRuntime` promise then completes, calls `runtimeCreations.delete` in its `.finally()`, and inserts itself into `runtimes` — permanently leaking the runtime with no future cleanup path.

This will reliably trigger when a user deletes a session immediately after opening it for the first time (before the first `getSnapshot` poll resolves).

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/host-service/src/runtime/chat/chat.ts
Line: 216-218

Comment:
**`observedAt` timestamp is recorded after data collection, not before**

`observedAt = Date.now()` is set after `listMessages()` resolves rather than before the first read. The timestamp therefore reflects the end of the snapshot window, not its start. For a value named `observedAt` (implying "the state as of this moment"), it would be more accurate to record the timestamp before reading `displayStateRaw`.

```suggestion
const displayStateRaw = runtime.harness.getDisplayState();
		const observedAt = Date.now();
		const messages = await runtime.harness.listMessages();
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Merge origin/main into v2-chat-architect..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/hooks/useWorkspaceChatController/useWorkspaceChatController.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/hooks/useWorkspaceChatController/useWorkspaceChatController.ts:113">
P2: Do not silently swallow `endSession` failures; add at least warning-level logging so runtime teardown issues are observable.

(Based on your team's feedback about handling async failures explicitly and avoiding empty catch blocks.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/host-service/src/trpc/router/chat/chat.ts">

<violation number="1" location="packages/host-service/src/trpc/router/chat/chat.ts:64">
P2: Avoid silently swallowing `updateSession` failures; log or surface the error context while keeping the call non-blocking.

(Based on your team's feedback about handling async errors explicitly and avoiding silent catch blocks.) [FEEDBACK_USED]</violation>
</file>

<file name="plans/background-agents-chat-architecture-reference.md">

<violation number="1" location="plans/background-agents-chat-architecture-reference.md:3">
P3: This markdown link points to a non-existent repository path, creating a broken docs reference.</violation>
</file>

<file name="plans/t3code-chat-architecture-reference.md">

<violation number="1" location="plans/t3code-chat-architecture-reference.md:3">
P3: The new Markdown link points to a non-existent path, so readers get a broken link in this reference doc.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

// Failures here must not block the user-visible delete.
void endSessionMutation
.mutateAsync({ sessionId: sessionIdToDelete, workspaceId })
.catch(() => {});
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Do not silently swallow endSession failures; add at least warning-level logging so runtime teardown issues are observable.

(Based on your team's feedback about handling async failures explicitly and avoiding empty catch blocks.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/hooks/useWorkspaceChatController/useWorkspaceChatController.ts, line 113:

<comment>Do not silently swallow `endSession` failures; add at least warning-level logging so runtime teardown issues are observable.

(Based on your team's feedback about handling async failures explicitly and avoiding empty catch blocks.) </comment>

<file context>
@@ -105,6 +106,11 @@ export function useWorkspaceChatController({
+			// Failures here must not block the user-visible delete.
+			void endSessionMutation
+				.mutateAsync({ sessionId: sessionIdToDelete, workspaceId })
+				.catch(() => {});
 			posthog.capture("chat_session_deleted", {
 				workspace_id: workspaceId,
</file context>
Suggested change
.catch(() => {});
.catch((error) => {
console.warn("Failed to end chat session runtime", {
sessionId: sessionIdToDelete,
workspaceId,
error,
});
});
Fix with Cubic

// turn — the user already sees their message land via the snapshot.
void ctx.api.chat.updateSession
.mutate({ sessionId: input.sessionId, lastActiveAt: new Date() })
.catch(() => {});
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Avoid silently swallowing updateSession failures; log or surface the error context while keeping the call non-blocking.

(Based on your team's feedback about handling async errors explicitly and avoiding silent catch blocks.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/chat/chat.ts, line 64:

<comment>Avoid silently swallowing `updateSession` failures; log or surface the error context while keeping the call non-blocking.

(Based on your team's feedback about handling async errors explicitly and avoiding silent catch blocks.) </comment>

<file context>
@@ -41,15 +41,35 @@ export const chatRouter = router({
+			// turn — the user already sees their message land via the snapshot.
+			void ctx.api.chat.updateSession
+				.mutate({ sessionId: input.sessionId, lastActiveAt: new Date() })
+				.catch(() => {});
+			return result;
+		}),
</file context>
Suggested change
.catch(() => {});
.catch((error) => {
console.warn("Failed to update chat session lastActiveAt", error);
});
Fix with Cubic

Comment thread plans/background-agents-chat-architecture-reference.md
Comment thread plans/t3code-chat-architecture-reference.md
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/trpc/src/router/chat/chat.ts (1)

86-105: ⚠️ Potential issue | 🟡 Minor

Consider deriving lastActiveAt server-side instead of accepting it from the client.

Letting any authenticated caller pick lastActiveAt means a misbehaving (or compromised) client can backdate or future-date sessions, distorting orderBy(lastActiveAt, "desc") in the sidebar (e.g. useWorkspaceChatController's live query). Since the only documented caller is host-service's fire-and-forget update right after sendMessage, the simpler/safer shape is a boolean flag (e.g. touch: true) and updates.lastActiveAt = new Date() here. If you want to keep the date on the wire for flexibility, consider validating it (<= now, not too far in the past) before persisting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trpc/src/router/chat/chat.ts` around lines 86 - 105, The mutation
currently accepts input.lastActiveAt from the client which allows clients to
set/backdate session times; change the input shape and update logic so the
server derives lastActiveAt instead: update the zod input schema to
replace/augment lastActiveAt with a boolean flag (e.g. touch:
z.boolean().optional()) and in the mutation handler (where you build updates:
Partial<typeof chatSessions.$inferInsert>) stop trusting input.lastActiveAt and
instead, when input.touch is true, set updates.lastActiveAt = new Date();
alternatively, if you must accept a date keep it and validate it server-side
(ensure <= now and not excessively old) before assigning to updates.lastActiveAt
to avoid tampering that would affect ordering in useWorkspaceChatController live
queries.
🧹 Nitpick comments (6)
apps/desktop/src/renderer/components/Chat/ChatInterface/components/TiptapPromptEditor/SlashCommandPreviewPopover.tsx (1)

72-86: Minor: stale slashPreview lingers when input becomes too short or cwd clears.

The early-return at line 74 skips both the call and any state reset, so a previously fetched preview stays in slashPreview until the next non-trivial input resolves. Today this is masked by previewMatchesInputCommand (canonical command name check) and the showPopover guard, so it's not user-visible — but it's fragile if those checks are ever loosened. Resetting to null on the guard branch would make the state strictly reflect the latest input.

♻️ Diff
 	useEffect(() => {
-		if (debouncedSlashPreviewInput.length <= 1 || !cwd) return;
+		if (debouncedSlashPreviewInput.length <= 1 || !cwd) {
+			setSlashPreview(null);
+			return;
+		}
 		let cancelled = false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/components/Chat/ChatInterface/components/TiptapPromptEditor/SlashCommandPreviewPopover.tsx`
around lines 72 - 86, The stale preview issue: when debouncedSlashPreviewInput
is too short or cwd is falsy the effect returns early and never clears previous
state; update the useEffect to explicitly reset slashPreview by calling
setSlashPreview(null) before returning in the guard branch (the branch that
checks debouncedSlashPreviewInput.length <= 1 || !cwd), keeping the existing
cancelled flag and previewSlashCommand promise flow intact so setSlashPreview is
still used only when appropriate (refer to slashPreview, setSlashPreview,
debouncedSlashPreviewInput, cwd, previewSlashCommand, and related
previewMatchesInputCommand/showPopover checks).
plans/v2-chat-greenfield-architecture.md (1)

391-421: Add a language identifier to the fenced code block (markdownlint MD040).

The ASCII topology diagram block opens with bare ``` , which trips MD040 fenced-code-language. Use text (or similar) to satisfy lint and keep rendering identical.

📝 Diff
-```
+```text
 Clients (phone / web / desktop renderer)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plans/v2-chat-greenfield-architecture.md` around lines 391 - 421, The fenced
ASCII topology diagram block opening with ``` (the topology diagram showing
Clients → Cloudflare → SessionDO/WorkspaceDO/D1) lacks a language identifier and
triggers markdownlint MD040; fix it by adding a neutral language tag such as
text (i.e., change the opening fence from ``` to ```text) so the renderer is
unchanged and the lint rule is satisfied—update the fenced code block around the
ASCII diagram in plans/v2-chat-greenfield-architecture.md (the block that starts
with the Clients (phone / web / desktop renderer) line).
apps/desktop/src/renderer/components/Chat/ChatInterface/components/TiptapPromptEditor/TiptapPromptEditor.tsx (1)

149-167: Minor: stale fileResults may briefly leak across mention sessions.

When the mention popup closes (e.g., Escape), fileResults retains the previous query's matches. Reopening the popup with a different query can flash those stale results until the new debounced fetch resolves. The isMentionVisible gate hides them while the popup is closed but doesn't reset the cache.

♻️ Optional: clear results when popup is hidden
 	useEffect(() => {
-		if (!isMentionVisible || !cwd || debouncedMentionQuery.length === 0) return;
+		if (!isMentionVisible || !cwd || debouncedMentionQuery.length === 0) {
+			setFileResults([]);
+			return;
+		}
 		let cancelled = false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/components/Chat/ChatInterface/components/TiptapPromptEditor/TiptapPromptEditor.tsx`
around lines 149 - 167, When the mention popup closes stale fileResults can
flash on reopen; update the component to clear the cached results whenever the
mention UI is hidden. Concretely, add a small effect or extend the existing
useEffect that watches isMentionVisible (and/or mentionState) and calls
setFileResults([]) when isMentionVisible becomes false (or when mentionState is
null), while keeping the existing cancellation logic in the searchFiles promise
chain; reference fileResults, setFileResults, isMentionVisible,
debouncedMentionQuery, mentionState, searchFiles and the surrounding useEffect
to locate where to add the reset.
plans/opencode-electron-chat-architecture-reference.md (1)

11-58: Optional: tag the ASCII diagram fence.

markdownlint flags MD040 because the fence has no language. Use text (or plaintext) to silence the warning and keep GitHub's syntax highlighter from guessing.

-```
+```text
 ┌────────────────────────── Electron (one Node / V8 process) ──────────────────────────┐
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plans/opencode-electron-chat-architecture-reference.md` around lines 11 - 58,
The code block containing the ASCII architecture diagram is missing a language
tag (triggers markdownlint MD040); update the opening fence (the triple
backticks immediately before the line that starts with
"┌────────────────────────── Electron (one Node / V8 process)
──────────────────────────┐") to include a language like text or plaintext
(e.g., ```text) so the diagram block is tagged and the linter warning is
suppressed.
plans/v1-to-v2-fast-migration.md (1)

200-210: Plan/code drift: implemented disposeRuntime is narrower than this checklist.

The actual disposeRuntime in packages/host-service/src/runtime/chat/chat.ts only calls harness.abort() and removes from the map. The checklist items "run any session-end hook (placeholder ok if hook wiring lands later)" and the workspace-deletion fan-out ("dispose all runtimes for sessions bound to it") are not yet wired. Either tighten the doc to reflect what landed in this PR (and move the missing items to P4) or land follow-up commits before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plans/v1-to-v2-fast-migration.md` around lines 200 - 210, The checklist in
plans/v1-to-v2-fast-migration.md is out of sync with the implemented
disposeRuntime: the current ChatRuntimeManager.disposeRuntime only calls
harness.abort() and removes the entry from the runtimes map and does not run
session-end hooks or fan out disposals on workspace deletion; update the doc to
reflect the implemented behavior (explicitly state that
disposeRuntime(sessionId) currently does: look up RuntimeSession, call
harness.abort(), remove from runtimes, idempotent) and move the missing items
("run session-end hook", "dispose all runtimes for sessions bound to a
workspace", plus adding chat.endSession mutation and wiring
useWorkspaceChatController.ts and workspace deletion flow) into a follow-up
TODO/raft with owner and priority, or alternatively implement the missing wiring
by adding the session-end hook invocation in disposeRuntime, adding
chat.endSession mutation, and wiring client call sites
(useWorkspaceChatController and workspace deletion) as separate commits —
reference disposeRuntime, ChatRuntimeManager, runtimes map, harness.abort(),
chat.endSession, and useWorkspaceChatController when making the doc change or
implementation.
packages/host-service/src/trpc/router/chat/chat.ts (1)

57-66: Consider observability on the silent fire-and-forget.

.catch(() => {}) swallows any cloud updateSession failure, which is appropriate (must not block the turn) but means a persistently broken cloud → host connection silently breaks session-selector ordering with no signal. A .catch((err) => logger.debug(...)) (or console.warn) would let this surface in diagnostics without changing behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/host-service/src/trpc/router/chat/chat.ts` around lines 57 - 66, The
fire-and-forget updateSession call currently swallows errors via .catch(() =>
{}) — change the catch to log the error (without rethrowing) so failures are
observable; update the .catch on ctx.api.chat.updateSession.mutate (inside the
mutation that calls ctx.runtime.chat.sendMessage) to call your existing logger
(e.g., processLogger.debug or console.warn) and include context
(sessionId/input) and the error details while preserving the non-blocking
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/ChatPane/hooks/useWorkspaceChatController/useWorkspaceChatController.ts:
- Around line 104-108: The silent .catch on the fire-and-forget call to
endSessionMutation.mutateAsync({ sessionId: sessionIdToDelete, workspaceId })
should log and report failures instead of swallowing them; change the catch to
e.g. .catch(err => { console.error("endSessionMutation failed for session",
sessionIdToDelete, "workspace", workspaceId, err);
posthog?.capture?.("chat.endSession_failure", { sessionId: sessionIdToDelete,
workspaceId, errorMessage: String(err) }); }) so runtime-disposal errors are
visible in console and PostHog while keeping the call fire-and-forget.

In `@packages/host-service/src/runtime/chat/chat.ts`:
- Around line 634-655: getSnapshot currently builds displayState from
displayStateRaw directly, which reintroduces the answered-question bug and
produces a different sandbox-question shape than getDisplayState; to fix, factor
the pendingQuestion construction into a single helper (e.g.,
buildDisplayStatePendingQuestion or buildDisplayState) and have both
getDisplayState and getSnapshot call it: the helper should accept
displayStateRaw and runtime, return a ChatDisplayState that applies the same
filter against runtime.answeredQuestionIds, and produce the same sandbox
question shape (top-level description from runtime.pendingSandboxQuestion.reason
and options with Yes/No descriptions), and preserve errorMessage resolution
(currentMessageError ?? runtime.lastErrorMessage) so both functions remain
consistent.
- Around line 524-534: disposeRuntime currently only aborts runtime.harness and
deletes from this.runtimes, causing race leaks, missing manager teardowns, and
no workspace validation; update disposeRuntime to accept workspaceId, validate
that the found runtime's workspaceId matches (like getOrCreateRuntime), and
refuse to dispose if it differs (matching endSession behavior); ensure you
handle in-flight creations by checking and cancelling/awaiting any matching
entry in this.runtimeCreations (or rechecking this.runtimes after a microtask
boundary) so a concurrent createRuntime cannot later insert a leaked runtime;
finally, invoke proper teardown on runtime.mcpManager and runtime.hookManager
(call their .dispose()/.close()/.shutdown() if available) before deleting the
entry and still wrap harness.abort in a try/catch to remain best-effort.

In `@packages/host-service/src/trpc/router/chat/chat.ts`:
- Around line 68-73: endSession currently calls ctx.runtime.chat.disposeRuntime
with only input.sessionId, which allows cross-workspace disposal; modify
endSession to pass both input.sessionId and input.workspaceId (or otherwise call
a runtime API that accepts workspaceId) and ensure the runtime manager verifies
the runtime's bound workspaceId before disposing, returning an error if they
mismatch; update the disposeRuntime signature (or add a new
disposeRuntimeWithWorkspace) and adjust call sites to enforce this check so only
the workspace that owns the session can end it.

In `@plans/v1-to-v2-fast-migration.md`:
- Line 18: The scope estimate is inconsistent between the PR description
("estimated 2–3 weeks for one engineer") and the "Scope:" line in this document
("roughly 1-2 weeks of implementation plus bake time"); update the "Scope:"
sentence in plans/v1-to-v2-fast-migration.md (the line starting with "Scope:")
so it matches the PR description's estimate (or vice versa) and use a single
reconciled wording (e.g., "estimated 2–3 weeks for one engineer, plus bake
time") to ensure consistency for reviewers and stakeholders.

---

Outside diff comments:
In `@packages/trpc/src/router/chat/chat.ts`:
- Around line 86-105: The mutation currently accepts input.lastActiveAt from the
client which allows clients to set/backdate session times; change the input
shape and update logic so the server derives lastActiveAt instead: update the
zod input schema to replace/augment lastActiveAt with a boolean flag (e.g.
touch: z.boolean().optional()) and in the mutation handler (where you build
updates: Partial<typeof chatSessions.$inferInsert>) stop trusting
input.lastActiveAt and instead, when input.touch is true, set
updates.lastActiveAt = new Date(); alternatively, if you must accept a date keep
it and validate it server-side (ensure <= now and not excessively old) before
assigning to updates.lastActiveAt to avoid tampering that would affect ordering
in useWorkspaceChatController live queries.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/components/Chat/ChatInterface/components/TiptapPromptEditor/SlashCommandPreviewPopover.tsx`:
- Around line 72-86: The stale preview issue: when debouncedSlashPreviewInput is
too short or cwd is falsy the effect returns early and never clears previous
state; update the useEffect to explicitly reset slashPreview by calling
setSlashPreview(null) before returning in the guard branch (the branch that
checks debouncedSlashPreviewInput.length <= 1 || !cwd), keeping the existing
cancelled flag and previewSlashCommand promise flow intact so setSlashPreview is
still used only when appropriate (refer to slashPreview, setSlashPreview,
debouncedSlashPreviewInput, cwd, previewSlashCommand, and related
previewMatchesInputCommand/showPopover checks).

In
`@apps/desktop/src/renderer/components/Chat/ChatInterface/components/TiptapPromptEditor/TiptapPromptEditor.tsx`:
- Around line 149-167: When the mention popup closes stale fileResults can flash
on reopen; update the component to clear the cached results whenever the mention
UI is hidden. Concretely, add a small effect or extend the existing useEffect
that watches isMentionVisible (and/or mentionState) and calls setFileResults([])
when isMentionVisible becomes false (or when mentionState is null), while
keeping the existing cancellation logic in the searchFiles promise chain;
reference fileResults, setFileResults, isMentionVisible, debouncedMentionQuery,
mentionState, searchFiles and the surrounding useEffect to locate where to add
the reset.

In `@packages/host-service/src/trpc/router/chat/chat.ts`:
- Around line 57-66: The fire-and-forget updateSession call currently swallows
errors via .catch(() => {}) — change the catch to log the error (without
rethrowing) so failures are observable; update the .catch on
ctx.api.chat.updateSession.mutate (inside the mutation that calls
ctx.runtime.chat.sendMessage) to call your existing logger (e.g.,
processLogger.debug or console.warn) and include context (sessionId/input) and
the error details while preserving the non-blocking behavior.

In `@plans/opencode-electron-chat-architecture-reference.md`:
- Around line 11-58: The code block containing the ASCII architecture diagram is
missing a language tag (triggers markdownlint MD040); update the opening fence
(the triple backticks immediately before the line that starts with
"┌────────────────────────── Electron (one Node / V8 process)
──────────────────────────┐") to include a language like text or plaintext
(e.g., ```text) so the diagram block is tagged and the linter warning is
suppressed.

In `@plans/v1-to-v2-fast-migration.md`:
- Around line 200-210: The checklist in plans/v1-to-v2-fast-migration.md is out
of sync with the implemented disposeRuntime: the current
ChatRuntimeManager.disposeRuntime only calls harness.abort() and removes the
entry from the runtimes map and does not run session-end hooks or fan out
disposals on workspace deletion; update the doc to reflect the implemented
behavior (explicitly state that disposeRuntime(sessionId) currently does: look
up RuntimeSession, call harness.abort(), remove from runtimes, idempotent) and
move the missing items ("run session-end hook", "dispose all runtimes for
sessions bound to a workspace", plus adding chat.endSession mutation and wiring
useWorkspaceChatController.ts and workspace deletion flow) into a follow-up
TODO/raft with owner and priority, or alternatively implement the missing wiring
by adding the session-end hook invocation in disposeRuntime, adding
chat.endSession mutation, and wiring client call sites
(useWorkspaceChatController and workspace deletion) as separate commits —
reference disposeRuntime, ChatRuntimeManager, runtimes map, harness.abort(),
chat.endSession, and useWorkspaceChatController when making the doc change or
implementation.

In `@plans/v2-chat-greenfield-architecture.md`:
- Around line 391-421: The fenced ASCII topology diagram block opening with ```
(the topology diagram showing Clients → Cloudflare → SessionDO/WorkspaceDO/D1)
lacks a language identifier and triggers markdownlint MD040; fix it by adding a
neutral language tag such as text (i.e., change the opening fence from ``` to
```text) so the renderer is unchanged and the lint rule is satisfied—update the
fenced code block around the ASCII diagram in
plans/v2-chat-greenfield-architecture.md (the block that starts with the Clients
(phone / web / desktop renderer) line).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f3f9f23f-bc0e-407a-96d0-e1684e8c0068

📥 Commits

Reviewing files that changed from the base of the PR and between 9f76264 and 059d55c.

📒 Files selected for processing (16)
  • apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/ChatInputFooter.tsx
  • apps/desktop/src/renderer/components/Chat/ChatInterface/components/TiptapPromptEditor/SlashCommandPreviewPopover.tsx
  • apps/desktop/src/renderer/components/Chat/ChatInterface/components/TiptapPromptEditor/TiptapPromptEditor.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/ChatPaneInterface.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/ChatInputFooter.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/hooks/useWorkspaceChatController/useWorkspaceChatController.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/hooks/useWorkspaceChatDisplay/useWorkspaceChatDisplay.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
  • packages/host-service/src/runtime/chat/chat.ts
  • packages/host-service/src/trpc/router/chat/chat.ts
  • packages/trpc/src/router/chat/chat.ts
  • plans/background-agents-chat-architecture-reference.md
  • plans/opencode-electron-chat-architecture-reference.md
  • plans/t3code-chat-architecture-reference.md
  • plans/v1-to-v2-fast-migration.md
  • plans/v2-chat-greenfield-architecture.md
💤 Files with no reviewable changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/ChatPaneInterface.tsx

Comment on lines +104 to +108
// Tear down the host-service in-memory runtime so it doesn't leak.
// Failures here must not block the user-visible delete.
void endSessionMutation
.mutateAsync({ sessionId: sessionIdToDelete, workspaceId })
.catch(() => {});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent .catch(() => {}) loses observability on runtime-disposal failures.

If chat.endSession fails (network, host-service down, race with workspace teardown), the host-service runtime can leak — exactly the case this mutation is meant to prevent — and we'll have nothing in logs/PostHog to diagnose it. Keep the fire-and-forget shape, but at minimum log to console (and ideally a posthog event) so the failure mode is visible.

♻️ Suggested change
-			void endSessionMutation
-				.mutateAsync({ sessionId: sessionIdToDelete, workspaceId })
-				.catch(() => {});
+			void endSessionMutation
+				.mutateAsync({ sessionId: sessionIdToDelete, workspaceId })
+				.catch((err) => {
+					console.error("Failed to end chat session runtime", err);
+				});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Tear down the host-service in-memory runtime so it doesn't leak.
// Failures here must not block the user-visible delete.
void endSessionMutation
.mutateAsync({ sessionId: sessionIdToDelete, workspaceId })
.catch(() => {});
// Tear down the host-service in-memory runtime so it doesn't leak.
// Failures here must not block the user-visible delete.
void endSessionMutation
.mutateAsync({ sessionId: sessionIdToDelete, workspaceId })
.catch((err) => {
console.error("Failed to end chat session runtime", err);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/ChatPane/hooks/useWorkspaceChatController/useWorkspaceChatController.ts
around lines 104 - 108, The silent .catch on the fire-and-forget call to
endSessionMutation.mutateAsync({ sessionId: sessionIdToDelete, workspaceId })
should log and report failures instead of swallowing them; change the catch to
e.g. .catch(err => { console.error("endSessionMutation failed for session",
sessionIdToDelete, "workspace", workspaceId, err);
posthog?.capture?.("chat.endSession_failure", { sessionId: sessionIdToDelete,
workspaceId, errorMessage: String(err) }); }) so runtime-disposal errors are
visible in console and PostHog while keeping the call fire-and-forget.

Comment thread packages/host-service/src/runtime/chat/chat.ts Outdated
Comment thread packages/host-service/src/runtime/chat/chat.ts Outdated
Comment thread packages/host-service/src/trpc/router/chat/chat.ts
Comment thread plans/v1-to-v2-fast-migration.md
Adds host-service auth surface and connects the v2 ModelPicker to it,
so the picker can show real Anthropic/OpenAI authentication state
instead of hardcoded `true`.

Host-service:
- types.ts: HostServiceRuntime gains an `auth: ChatService` field.
- app.ts: instantiate a long-lived ChatService singleton (auth is
  per-machine, not per-workspace) and expose it via runtime.auth.
- New trpc/router/auth/auth.ts router with 17 procedures mirroring
  v1's chatServiceTrpc.auth.* surface (getAnthropicStatus,
  startAnthropicOAuth, completeAnthropicOAuth, etc.).
- Wired into appRouter as `auth`.

ChatService delegates to mastra's createAuthStorage() under the hood,
so this is a thin proxy — no auth logic is duplicated. Mastra's storage
files are the same paths host-service already reads from when resolving
credentials at runtime, so v1 settings/models writes and v2 reads stay
in sync via disk.

Renderer:
- v2 ModelPicker now queries workspaceTrpc.auth.getAnthropicStatus and
  getOpenAIStatus; previously hardcoded both to authenticated. Refetches
  on selector open. The "open settings" action keeps navigating to
  /settings/models which is the existing v1 page (still works because
  it writes via Electron-main IPC to the same on-disk storage).

Remote-host auth UI is a follow-on; for remote workspaces the status
correctly reflects the remote machine's credentials, but the
/settings/models flow only authenticates the local machine.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/host-service/src/trpc/router/auth/auth.ts (1)

16-18: Consider tightening envText validation.

envText: z.string() permits empty strings, which then flow through setAnthropicEnvConfig. Since there's already a dedicated clearAnthropicEnvConfig mutation, accepting "" here makes the API ambiguous — callers can clear via either path with potentially different semantics inside ChatService. Either explicitly disallow empty (.min(1)) or document that empty == clear.

Also applies to: 49-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/host-service/src/trpc/router/auth/auth.ts` around lines 16 - 18,
Tighten validation for the anthropicEnvConfigInput's envText so empty strings
are rejected to avoid ambiguity with clearAnthropicEnvConfig: update the zod
schema for anthropicEnvConfigInput (envText) to require at least one character
(e.g., .min(1)) and adjust any related validation logic used by
setAnthropicEnvConfig; ensure callers know that clearing must use
clearAnthropicEnvConfig and that ChatService logic expects non-empty envText in
setAnthropicEnvConfig.
packages/host-service/src/types.ts (1)

2-2: LGTM — runtime now exposes provider auth surface.

Typing auth as ChatService is acceptable here given the comment in app.ts explaining the field is intentionally a narrow alias for the auth subset of ChatService. If ChatService grows non-auth methods later, consider tightening this field to a dedicated interface (e.g. ProviderAuthService) so the runtime type doesn't leak unrelated capabilities into all consumers.

Also applies to: 15-15

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/host-service/src/types.ts` at line 2, The current typing of the auth
field as ChatService is acceptable, but to avoid leaking non-auth methods create
a narrow interface (e.g. ProviderAuthService) that contains only the auth
methods used and replace the ChatService usage: define ProviderAuthService with
the required auth method signatures, export it, and change the auth field type
in types.ts (and the alias in app.ts) from ChatService to ProviderAuthService so
consumers only receive the auth surface.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ModelPicker/ModelPicker.tsx:
- Around line 45-53: The current useEffect triggers redundant refetches
(refetchAnthropicStatus/refetchOpenAIStatus) when the picker mounts and also
causes an "unauthenticated flash" because anthropicStatus/openAIStatus are
undefined while initial queries load; remove the Promise.all refetch effect and
instead rely on the original useQuery behavior or, if you need refetch-on-open,
only call refetch when the query is not already fetching (use the
isLoading/isFetching/isFetched flags from
workspaceTrpc.auth.getAnthropicStatus.useQuery and getOpenAIStatus.useQuery) and
change UI checks from treating undefined as false to treating undefined as
loading (e.g., use anthropicStatus === true for authenticated or guard with
isLoading/isFetched) so the providers aren’t shown as unauthenticated during the
initial load.

---

Nitpick comments:
In `@packages/host-service/src/trpc/router/auth/auth.ts`:
- Around line 16-18: Tighten validation for the anthropicEnvConfigInput's
envText so empty strings are rejected to avoid ambiguity with
clearAnthropicEnvConfig: update the zod schema for anthropicEnvConfigInput
(envText) to require at least one character (e.g., .min(1)) and adjust any
related validation logic used by setAnthropicEnvConfig; ensure callers know that
clearing must use clearAnthropicEnvConfig and that ChatService logic expects
non-empty envText in setAnthropicEnvConfig.

In `@packages/host-service/src/types.ts`:
- Line 2: The current typing of the auth field as ChatService is acceptable, but
to avoid leaking non-auth methods create a narrow interface (e.g.
ProviderAuthService) that contains only the auth methods used and replace the
ChatService usage: define ProviderAuthService with the required auth method
signatures, export it, and change the auth field type in types.ts (and the alias
in app.ts) from ChatService to ProviderAuthService so consumers only receive the
auth surface.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd8c9ced-ce2b-4719-9872-34df368e25d9

📥 Commits

Reviewing files that changed from the base of the PR and between 059d55c and 73ec868.

📒 Files selected for processing (6)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ModelPicker/ModelPicker.tsx
  • packages/host-service/src/app.ts
  • packages/host-service/src/trpc/router/auth/auth.ts
  • packages/host-service/src/trpc/router/auth/index.ts
  • packages/host-service/src/trpc/router/router.ts
  • packages/host-service/src/types.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/host-service/src/trpc/router/auth/index.ts

Comment on lines +45 to +53
const { data: anthropicStatus, refetch: refetchAnthropicStatus } =
workspaceTrpc.auth.getAnthropicStatus.useQuery();
const { data: openAIStatus, refetch: refetchOpenAIStatus } =
workspaceTrpc.auth.getOpenAIStatus.useQuery();

useEffect(() => {
if (!open) return;
void Promise.all([refetchAnthropicStatus(), refetchOpenAIStatus()]);
}, [open, refetchAnthropicStatus, refetchOpenAIStatus]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: duplicate initial fetch and "unauthenticated flash" risk.

useQuery already auto-fetches on mount, so when the picker mounts with open === true the effect at lines 50–53 will issue a redundant refetch() for both providers in addition to the in-flight initial query.

Separately, while the queries are loading, anthropicStatus/openAIStatus are undefined, so ?? false will render both providers as unauthenticated for the first frames after mount — previously these were hardcoded true, so users with valid OAuth will momentarily see disabled/auth-prompt UI on first open. Consider gating until data is loaded, or treating undefined as "loading" rather than "unauthenticated":

♻️ Suggested handling
-	const { data: anthropicStatus, refetch: refetchAnthropicStatus } =
-		workspaceTrpc.auth.getAnthropicStatus.useQuery();
-	const { data: openAIStatus, refetch: refetchOpenAIStatus } =
-		workspaceTrpc.auth.getOpenAIStatus.useQuery();
-
-	useEffect(() => {
-		if (!open) return;
-		void Promise.all([refetchAnthropicStatus(), refetchOpenAIStatus()]);
-	}, [open, refetchAnthropicStatus, refetchOpenAIStatus]);
+	const {
+		data: anthropicStatus,
+		isFetched: anthropicFetched,
+		refetch: refetchAnthropicStatus,
+	} = workspaceTrpc.auth.getAnthropicStatus.useQuery();
+	const {
+		data: openAIStatus,
+		isFetched: openAIFetched,
+		refetch: refetchOpenAIStatus,
+	} = workspaceTrpc.auth.getOpenAIStatus.useQuery();
+
+	// Refetch when picker re-opens, but skip the initial open since useQuery
+	// already fetched on mount.
+	useEffect(() => {
+		if (!open || !anthropicFetched || !openAIFetched) return;
+		void Promise.all([refetchAnthropicStatus(), refetchOpenAIStatus()]);
+	}, [open, anthropicFetched, openAIFetched, refetchAnthropicStatus, refetchOpenAIStatus]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ModelPicker/ModelPicker.tsx
around lines 45 - 53, The current useEffect triggers redundant refetches
(refetchAnthropicStatus/refetchOpenAIStatus) when the picker mounts and also
causes an "unauthenticated flash" because anthropicStatus/openAIStatus are
undefined while initial queries load; remove the Promise.all refetch effect and
instead rely on the original useQuery behavior or, if you need refetch-on-open,
only call refetch when the query is not already fetching (use the
isLoading/isFetching/isFetched flags from
workspaceTrpc.auth.getAnthropicStatus.useQuery and getOpenAIStatus.useQuery) and
change UI checks from treating undefined as false to treating undefined as
loading (e.g., use anthropicStatus === true for authenticated or guard with
isLoading/isFetched) so the providers aren’t shown as unauthenticated during the
initial load.

Mastra defaults observational-memory observer + reflector models to
google/gemini-2.5-flash. Without GOOGLE_GENERATIVE_AI_API_KEY in the
host-service process env, the post-turn reflection step fails with
"Could not find API key process.env.GOOGLE_GENERATIVE_AI_API_KEY".

v1's ChatRuntimeService already handles this in
packages/chat/src/server/trpc/service.ts:88 — derive the OM model
from whichever provider the user is authenticated with (Anthropic ->
Haiku 4.5, OpenAI -> GPT-4.1 nano, Google env -> Gemini). Mirror that
helper in host-service and pass the result as initialState
{observerModelId, reflectorModelId} to createMastraCode.
When v2 chat migrated from chat.listMessages to chat.getSnapshot in
c183954, sendMessageToSession kept writing optimistic user messages
to the listMessages cache via setData. The display now reads from
snapshot.messages, so the optimistic message landed in a dead cache
and the first user message disappeared until the next snapshot poll
fetched it back from the harness (~250ms+ at 4fps polling).

Most visible on the first message of a fresh chat (page is empty
before it), but every send had the gap.

Update sendMessageToSession to write to chat.getSnapshot.setData. Two
cases:

- Existing snapshot in cache: spread, append optimistic message.
- Fresh session, no snapshot yet: seed with a minimal displayState
  (the next poll fills it in) and an array containing just the
  optimistic message so it renders immediately.

Rollback path mirrors: filter the optimistic id out of snapshot.messages
on send failure.
Two small changes that eliminate forced rerenders when nothing has
actually changed in the snapshot.

1. Drop observedAt from chat.getSnapshot

The server was stamping every snapshot response with observedAt:
Date.now(). Nothing on the client read it. But because the timestamp
differs every poll, React Query's structuralSharing couldn't preserve
the response object's identity — every 250ms (4fps polling) produced
a new reference, forcing every component reading `snapshot` to rerender
even when displayState and messages were byte-identical.

Removing observedAt lets structural sharing kick in: idle polls
now produce identical objects and downstream consumers don't rerender.

Also drops the field from the optimistic-seed object in
sendMessageToSession so it stays in sync with the new shape.

2. Memoize the slashCommands select mapper

ChatPaneInterface.tsx had an inline `select: (commands) => commands.map(...)`
in the getSlashCommands.useQuery options. The arrow function was
recreated every render → mapper re-ran → new array reference → every
consumer of `slashCommands` (TiptapPromptEditor, executor, etc.)
rerendered even on identical input.

useCallback the mapper and pass the stable reference instead.

Together these two cuts close the cascade: poll resolves with
structurally-equal data → React Query preserves identity → consumers
see same reference → no rerender. Subsequent message-arrival polls
still rerender (because data actually changed) — that's correct.
…t RQ cache

User saw the message flicker in/out on the first send: thinking, then
the message, then nothing, then thinking again, then the full result.
Cause was a stale poll clobbering the optimistic update.

The existing-session send path already does this correctly — useChatDisplay
holds an `optimisticUserMessage` in component state and merges it into
the messages output, outside React Query's cache. Stale polls can't
overwrite local component state, so the message stays visible until
the harness's response includes it.

The new-session path (sendMessageToSession, only fired on first send)
took a different route: write the optimistic message into the
chat.getSnapshot React Query cache. With 4fps polling running
continuously, a poll often resolved during the mutation with the
harness's pre-message state and clobbered the optimistic write —
message vanished, then reappeared on the next poll once the harness
had processed it.

Switch the new-session path to the same pattern: set pendingUserTurn
local state in handleSend before calling sendMessageToSession.
getVisibleMessagesWithPendingUserTurn already merges that into the
displayed list and clears it once the server's messages array
contains a matching entry. sendMessageToSession reduces to a thin
mutation wrapper — no cache writes, no rollback.

The restart flow already used pendingUserTurn for the same reason; this
just brings the new-session path into parity.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/ChatPaneInterface.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/ChatPaneInterface.tsx:351">
P2: `sendMessageToSession` now assumes callers manage optimistic user-turn state, but other callers (like auto-launch) still invoke it without setting `pendingUserTurn`, causing inconsistent message visibility behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

// and clobber the optimistic write, making the user message vanish
// briefly. The pendingUserTurn local state is merged in via
// getVisibleMessagesWithPendingUserTurn so it survives stale polls.
await sendMessageMutation.mutateAsync({
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: sendMessageToSession now assumes callers manage optimistic user-turn state, but other callers (like auto-launch) still invoke it without setting pendingUserTurn, causing inconsistent message visibility behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/ChatPaneInterface.tsx, line 351:

<comment>`sendMessageToSession` now assumes callers manage optimistic user-turn state, but other callers (like auto-launch) still invoke it without setting `pendingUserTurn`, causing inconsistent message visibility behavior.</comment>

<file context>
@@ -343,63 +341,20 @@ export function ChatPaneInterface({
+			// and clobber the optimistic write, making the user message vanish
+			// briefly. The pendingUserTurn local state is merged in via
+			// getVisibleMessagesWithPendingUserTurn so it survives stale polls.
+			await sendMessageMutation.mutateAsync({
 				sessionId: targetSessionId,
 				workspaceId,
</file context>
Fix with Cubic

@AviPeltz AviPeltz merged commit 6c2a771 into main Apr 27, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant