Skip to content

feat(desktop): reflect Claude Code session title/color in tabs and sidebar - #6154

Closed
moshfeu wants to merge 4 commits into
ParadevAI:mainfrom
moshfeu:feat/claude-tab-title-color
Closed

moshfeu wants to merge 4 commits into
ParadevAI:mainfrom
moshfeu:feat/claude-tab-title-color

Conversation

@moshfeu

@moshfeu moshfeu commented Aug 4, 2026 •

Copy link
Copy Markdown

Summary

  • Reflect the active Claude Code session's title and color in desktop tabs and the sidebar
  • Tint tab backgrounds with the Claude session color, with an appearance toggle to disable it
  • Surface a clear message when Postgres/Neon is unreachable
  • Address code review findings from prior iterations

Summary by cubic

Reflects each Claude Code session’s live title and color across the desktop: single-terminal tabs use the session name and get a matching background tint (toggle in Settings). Also shows a clear “Database unavailable” message when Postgres/Neon can’t be reached.

  • New Features

    • Tabs: Single-terminal Terminal tabs use the session’s title and tint the entire tab with the session /color; the tab-bar icon matches the in-pane icon, and pane icons show a small color dot. Toggle in Settings → Appearance (“Claude Code session name & color”).
    • Sidebar: Agent hover rows show a color dot and use the session title when available.
    • Live updates: Host-service tails Claude’s transcript to read /color and /rename/auto-title, overlays live title/color on bindings, publishes agent:meta events, and the renderer updates via React Query selectors without unnecessary re-renders.
  • Bug Fixes

    • Database connectivity: Detects DB-down errors in auth and tRPC, sets a shared flag/message, and shows a throttled “Database unavailable” toast in the desktop app instead of a silent 500.
    • Security/robustness: Sanitizes sessionId when resolving transcript paths and forwards cwd from the agent hook. Clears static pane titles once a live session title is known. Adds tests for transcript parsing, store overlays, and DB error detection.

Written for commit eba7a15. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Claude Code terminal tabs can now display live session names and color accents.
    • Added an Appearance setting to enable or disable Claude tab decorations.
    • Sidebar agent entries now show session-specific labels and color indicators.
    • Terminal session metadata updates appear automatically without restarting the workspace.
  • Bug Fixes
    • Improved handling of database connectivity failures with clearer messages and temporary-unavailability notifications.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request adds centralized database connectivity error handling and live Claude session metadata. Host-service transcript watchers publish titles and colors to desktop clients, which render them in terminal tabs and sidebar agent rows through a persisted appearance setting.

Changes

Database connectivity error handling

Layer / File(s) Summary
Shared error contracts and API handling
packages/shared/*, packages/trpc/src/trpc.ts, apps/api/src/app/api/auth/[...all]/route.ts
Database connectivity failures receive stable response metadata and messages. Auth routes return logged JSON 503 responses for matching failures.
Desktop query and mutation notifications
apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx
React Query cache callbacks display a throttled database-unavailable toast for matching tRPC errors.

Live Claude agent metadata

Layer / File(s) Summary
Metadata input and storage
apps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.sh, packages/host-service/src/trpc/router/notifications/notifications.ts, packages/host-service/src/terminal-agents/{types,store}.*
Notifications capture cwd. The terminal-agent store tracks ephemeral cwd, title, and color metadata across binding read paths.
Transcript parsing and watching
packages/host-service/src/terminal-agents/claude-transcript*, packages/host-service/src/app.ts
Transcript utilities parse Claude title and color values. ClaudeTranscriptWatcher monitors eligible transcripts with rescans and debounced updates.
Metadata event propagation
packages/host-service/src/events/*, packages/workspace-client/src/*, apps/desktop/src/renderer/hooks/host-service/*
The new agent:meta event travels from host service to workspace listeners and invalidates selected terminal-agent queries.

Claude tab decoration

Layer / File(s) Summary
Decoration setting and color resolution
apps/desktop/src/renderer/stores/claude-tab-decoration.ts, apps/desktop/src/shared/constants/claude-agent-colors.ts, apps/desktop/src/renderer/routes/_authenticated/settings/appearance/...
A persisted setting controls Claude tab decoration. Appearance settings expose the toggle, and named colors resolve to CSS values and accent backgrounds.
Tab rendering extension
packages/panes/src/react/components/Workspace/...
Workspace tab components accept an optional wrapper for complete tab content, including icons, titles, and accessory controls.
Terminal workspace decoration
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/...
Terminal titles prefer live Claude titles. Terminal tabs render workspace-aware icons, color dots, and optional accent backgrounds.
Sidebar agent metadata
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/...
Running-agent data includes live titles and colors. Sidebar rows display the title fallback and a resolved color indicator.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ClaudeCode
  participant ClaudeTranscriptWatcher
  participant TerminalAgentStore
  participant EventBus
  participant DesktopBindings
  participant WorkspaceTabs
  ClaudeCode->>ClaudeTranscriptWatcher: Write title and color to transcript
  ClaudeTranscriptWatcher->>TerminalAgentStore: Update live metadata
  ClaudeTranscriptWatcher->>EventBus: Broadcast agent:meta
  EventBus->>DesktopBindings: Invalidate selected binding query
  DesktopBindings->>WorkspaceTabs: Provide title and color
  WorkspaceTabs->>WorkspaceTabs: Render icon dot and accent background
Loading

Possibly related PRs

Suggested reviewers: kitenite

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the changes in detail but omits the required testing section and checklist. Add the commands or steps used for testing, include screenshots for the user-visible changes, and complete the checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary desktop change: reflecting Claude Code session titles and colors in tabs and the sidebar.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR derives Claude Code session titles and colors from transcript files, propagates metadata through host-service events, decorates desktop tabs/sidebar entries, and adds clearer database-connectivity errors.

  • Adds transcript parsing, watching, binding metadata, and workspace event invalidation.
  • Adds configurable tab title/color decoration and sidebar metadata display.
  • Adds structured database-unavailable handling for auth and tRPC clients.

Confidence Score: 4/5

This PR should not merge until pane launch labels are preserved and superseded transcript watchers are prevented from overwriting current-session metadata.

The title-decoration path irreversibly deletes fallback state, and the watcher replacement path permits delayed callbacks from an old Claude session to mutate metadata for a newly bound session.

Files Needing Attention: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx; packages/host-service/src/terminal-agents/claude-transcript-watcher.ts

Important Files Changed

Filename Overview
packages/host-service/src/terminal-agents/claude-transcript-watcher.ts Adds live transcript watching, but superseded watchers can still publish stale metadata through retained debounce callbacks.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx Integrates live Claude titles into pane titles, but destructively clears the only stored launch-label fallback.
packages/host-service/src/terminal-agents/claude-transcript.ts Adds sanitized transcript-path derivation and latest title/color parsing with custom titles taking precedence.
packages/host-service/src/trpc/router/notifications/notifications.ts Extends lifecycle hooks with cwd metadata while retaining terminal existence and workspace resolution.
apps/api/src/app/api/auth/[...all]/route.ts Converts recognized database-connectivity failures into structured 503 responses while rethrowing unrelated errors.
packages/trpc/src/trpc.ts Adds structured database-unavailable metadata to tRPC errors for client-wide presentation.

Sequence Diagram

sequenceDiagram
  participant Claude as Claude Code
  participant Hook as Notify hook
  participant Store as Terminal-agent store
  participant Watcher as Transcript watcher
  participant Bus as Workspace event bus
  participant UI as Desktop renderer
  Claude->>Hook: lifecycle event with cwd/session ID
  Hook->>Store: record binding metadata
  Watcher->>Watcher: resolve and watch transcript
  Watcher->>Store: update title/color
  Watcher->>Bus: broadcast agent:meta
  Bus-->>UI: invalidate workspace bindings
  UI->>Store: list bindings
  Store-->>UI: title/color metadata
  UI->>UI: render tab and sidebar decoration
Loading
Prompt To Fix All With AI
### Issue 1
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx:180-192
**Launch-label fallback is deleted**

When a Claude transcript title arrives, this effect permanently clears the pane's launch-time `titleOverride`. Disabling decoration or losing live metadata then falls through to the PTY title or generic “Terminal” title instead of restoring the original label such as “Claude”.

### Issue 2
packages/host-service/src/terminal-agents/claude-transcript-watcher.ts:99-102
**Superseded watcher retains callbacks**

When a terminal changes Claude sessions while its previous watcher has a pending debounce callback, this branch closes the watcher without cancelling that callback or tying it to the current session. The delayed callback can consequently read the old transcript and overwrite the new session's tab and sidebar title or color until another current-session update occurs.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(host-service,desktop,trpc): address ..." | Re-trigger Greptile

Comment on lines +180 to +192
useEffect(() => {
if (!claudeTabDecorationEnabled) return;
const state = store.getState();
for (const tab of state.tabs) {
for (const pane of Object.values(tab.panes)) {
if (pane.kind !== "terminal" || !pane.titleOverride) continue;
const { terminalId } = pane.data as TerminalPaneData;
if (!terminalAgentBindings.get(terminalId)?.title) continue;
state.setPaneTitleOverride({
tabId: tab.id,
paneId: pane.id,
titleOverride: undefined,
});

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.

P1 Launch-label fallback is deleted

When a Claude transcript title arrives, this effect permanently clears the pane's launch-time titleOverride. Disabling decoration or losing live metadata then falls through to the PTY title or generic “Terminal” title instead of restoring the original label such as “Claude”.

Knowledge Base Used: Desktop App (Electron main + renderer)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
Line: 180-192

Comment:
**Launch-label fallback is deleted**

When a Claude transcript title arrives, this effect permanently clears the pane's launch-time `titleOverride`. Disabling decoration or losing live metadata then falls through to the PTY title or generic “Terminal” title instead of restoring the original label such as “Claude”.

**Knowledge Base Used:** [Desktop App (Electron main + renderer)](https://app.greptile.com/personal-org-6810/-/custom-context/knowledge-base/superset-sh/superset/-/docs/desktop-app.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +99 to +102
if (!binding || expectedPath !== entry.path) {
entry.watcher.close();
this.watched.delete(terminalId);
}

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.

P1 Superseded watcher retains callbacks

When a terminal changes Claude sessions while its previous watcher has a pending debounce callback, this branch closes the watcher without cancelling that callback or tying it to the current session. The delayed callback can consequently read the old transcript and overwrite the new session's tab and sidebar title or color until another current-session update occurs.

Knowledge Base Used: Desktop App (Electron main + renderer)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/terminal-agents/claude-transcript-watcher.ts
Line: 99-102

Comment:
**Superseded watcher retains callbacks**

When a terminal changes Claude sessions while its previous watcher has a pending debounce callback, this branch closes the watcher without cancelling that callback or tying it to the current session. The delayed callback can consequently read the old transcript and overwrite the new session's tab and sidebar title or color until another current-session update occurs.

**Knowledge Base Used:** [Desktop App (Electron main + renderer)](https://app.greptile.com/personal-org-6810/-/custom-context/knowledge-base/superset-sh/superset/-/docs/desktop-app.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx (1)

190-202: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Apply the content wrapper during inline rename.

When isEditing is true, this branch renders the input directly and skips renderContentWrapper. A Claude tab loses its configured accent background while its rename input is open. Build the editing and normal variants into one content value, then pass that value through the wrapper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx`
around lines 190 - 202, Update the content rendering in TabItem so both the
inline rename view and normal tab content are first selected into a single
content value, then pass that value through renderContentWrapper. Preserve the
existing TabRenameInput behavior and fallback to tabContent when no wrapper is
provided, ensuring configured wrappers remain applied while isEditing is true.
🧹 Nitpick comments (2)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalTabContentWrapper.tsx (1)

15-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move TerminalTabContentWrapper into its own component folder.

TerminalTabContentWrapper is a component. Place it in TerminalTabContentWrapper/TerminalTabContentWrapper.tsx and add an index.ts export.

As per coding guidelines, “Use one folder per component with ComponentName/ComponentName.tsx and index.ts; keep one component per file.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalTabContentWrapper.tsx
around lines 15 - 59, Move the TerminalTabContentWrapper component and its
related createRenderTerminalTabContentWrapper function into a dedicated
TerminalTabContentWrapper/TerminalTabContentWrapper.tsx file, and add an
index.ts that exports the component module. Update imports to use the new folder
export while preserving the existing behavior.

Source: Coding guidelines

packages/host-service/src/terminal-agents/claude-transcript.test.ts (1)

92-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test custom-title precedence after an automatic title.

The fixture has no ai-title after custom-title. It passes if the parser returns the last title record. Add a later automatic title and keep the "my session" expectation to protect /rename precedence.

Proposed test update
 				JSON.stringify({ type: "agent-color", agentColor: "red" }),
 				JSON.stringify({ type: "custom-title", customTitle: "my session" }),
+				JSON.stringify({ type: "ai-title", aiTitle: "generated later" }),
 				JSON.stringify({ type: "agent-name", agentName: "my session" }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/terminal-agents/claude-transcript.test.ts` around
lines 92 - 107, The test for readLatestColorAndTitle should verify custom-title
precedence over a later automatic title. Update the fixture in the “reads a
user-set title from a custom-title line” test to add an ai-title record after
the custom-title entry, while retaining the expected title “my session”.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/renderer/stores/claude-tab-decoration.ts`:
- Around line 14-25: Register the "claude-tab-decoration" persistence key in
persisted-key-registry.test-data.ts, including an explicit size bound, the
responsible deletion owner/path, and a documented removal plan. Keep the
existing key name in use by useClaudeTabDecorationStore unchanged and follow the
registry’s established entry format.

In `@packages/host-service/src/terminal-agents/claude-transcript-watcher.ts`:
- Around line 91-102: Update the watch-removal branch in the watched-entry
reconciliation loop to clear the terminal’s pending debounce timer before
closing and deleting the watch. Reuse the existing timer-management symbol and
apply the same cancellation in the corresponding removal path around the
additionally referenced block, ensuring callbacks for stale transcript paths
cannot update metadata or broadcast events.
- Around line 125-144: Update the watcher setup in the start() flow to create
and register the FSWatcher in this.watched before calling checkAndEmit().
Preserve the existing retry behavior when watch(path, ...) fails, and ensure the
registered watcher retains the correct terminalId, workspaceId, path, callback,
and error cleanup handling.

In `@packages/host-service/src/terminal-agents/claude-transcript.ts`:
- Around line 71-108: Update the transcript reader used by
ClaudeTranscriptWatcher.checkAndEmit to maintain per-path file offsets and
latest metadata, reading and parsing only newly appended complete lines instead
of rescanning the entire file. Detect truncation or replacement, reset that
path’s cached offset and metadata, and preserve the existing metadata extraction
behavior for appended records.

In `@packages/host-service/src/terminal-agents/store.ts`:
- Around line 95-100: Update the terminal metadata handling around the liveMeta
updates to detect changes to the terminal’s agentId or agentSessionId, clear the
existing overlay metadata before applying the new binding, and then store the
new cwd without retaining the previous title or color. Add coverage for creating
a new session on an existing terminal.
- Around line 171-182: Update packages/host-service/src/terminal-agents/store.ts
lines 171-182 in updateAgentMeta() to compare only fields supplied in meta and
return whether the overlay actually changed. Update
packages/host-service/src/terminal-agents/claude-transcript-watcher.ts lines
163-185 so checkAndEmit() calls broadcastAgentMeta() only when updateAgentMeta()
reports a change.

In `@packages/host-service/src/trpc/router/notifications/notifications.ts`:
- Line 77: Update the cwd handling around encodeProjectDirName to preserve every
character of a nonempty input, including trailing whitespace; replace
trimOrUndefined with logic that treats only absent or empty values as missing
while leaving valid filesystem paths unchanged.

---

Outside diff comments:
In
`@packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx`:
- Around line 190-202: Update the content rendering in TabItem so both the
inline rename view and normal tab content are first selected into a single
content value, then pass that value through renderContentWrapper. Preserve the
existing TabRenameInput behavior and fallback to tabContent when no wrapper is
provided, ensuring configured wrappers remain applied while isEditing is true.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalTabContentWrapper.tsx:
- Around line 15-59: Move the TerminalTabContentWrapper component and its
related createRenderTerminalTabContentWrapper function into a dedicated
TerminalTabContentWrapper/TerminalTabContentWrapper.tsx file, and add an
index.ts that exports the component module. Update imports to use the new folder
export while preserving the existing behavior.

In `@packages/host-service/src/terminal-agents/claude-transcript.test.ts`:
- Around line 92-107: The test for readLatestColorAndTitle should verify
custom-title precedence over a later automatic title. Update the fixture in the
“reads a user-set title from a custom-title line” test to add an ai-title record
after the custom-title entry, while retaining the expected title “my session”.
🪄 Autofix

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 Plus

Run ID: 263b7223-9f43-4b38-9752-1070bba9a06f

📥 Commits

Reviewing files that changed from the base of the PR and between d530b98 and 8297c88.

📒 Files selected for processing (40)
  • apps/api/src/app/api/auth/[...all]/route.ts
  • apps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.sh
  • apps/desktop/src/renderer/hooks/host-service/useTerminalAgentBindings/useTerminalAgentBindings.ts
  • apps/desktop/src/renderer/hooks/host-service/useWorkspaceEvent/useWorkspaceEvent.ts
  • apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarAgentsChip/components/DashboardSidebarAgentHoverRow/DashboardSidebarAgentHoverRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/hooks/useDashboardSidebarWorkspaceRunningAgents/useDashboardSidebarWorkspaceRunningAgents.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalPaneIcon.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalTabContentWrapper.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/renderTerminalTabIcon.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/AppearanceSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/ClaudeTabDecorationSection/ClaudeTabDecorationSection.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/ClaudeTabDecorationSection/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
  • apps/desktop/src/renderer/stores/claude-tab-decoration.ts
  • apps/desktop/src/shared/constants/claude-agent-colors.ts
  • packages/host-service/src/app.ts
  • packages/host-service/src/events/event-bus.ts
  • packages/host-service/src/events/types.ts
  • packages/host-service/src/terminal-agents/claude-transcript-watcher.ts
  • packages/host-service/src/terminal-agents/claude-transcript.test.ts
  • packages/host-service/src/terminal-agents/claude-transcript.ts
  • packages/host-service/src/terminal-agents/index.ts
  • packages/host-service/src/terminal-agents/store.test.ts
  • packages/host-service/src/terminal-agents/store.ts
  • packages/host-service/src/terminal-agents/types.ts
  • packages/host-service/src/trpc/router/notifications/notifications.ts
  • packages/panes/src/react/components/Workspace/Workspace.tsx
  • packages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsx
  • packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx
  • packages/panes/src/react/types.ts
  • packages/shared/package.json
  • packages/shared/src/db-connectivity-error.test.ts
  • packages/shared/src/db-connectivity-error.ts
  • packages/trpc/src/trpc.ts
  • packages/workspace-client/src/index.ts
  • packages/workspace-client/src/lib/eventBus.ts

Comment on lines +14 to +25
export const useClaudeTabDecorationStore = create<ClaudeTabDecorationState>()(
devtools(
persist(
(set) => ({
enabled: true,
setEnabled: (enabled) => set({ enabled }),
}),
{ name: "claude-tab-decoration" },
),
{ name: "ClaudeTabDecorationStore" },
),
);

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

registry_files="$(fd -a 'persisted-key-registry.test-data.ts' apps/desktop/src/renderer)"
rg -n -F 'claude-tab-decoration' \
  apps/desktop/src/renderer/stores/claude-tab-decoration.ts \
  $registry_files

Repository: superset-sh/superset

Length of output: 199


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "Locate registry file(s):"
fd -a 'persisted-key-registry.test-data.ts' apps/desktop/src/renderer || true

registry_files="$(fd -a 'persisted-key-registry.test-data.ts' apps/desktop/src/renderer | tr '\n' ' ')"
if [ -n "$registry_files" ]; then
  echo
  echo "Registry file outline/usages:"
  wc -l $registry_files
  rg -n -C 4 'localStorage|registry|writers|deletion|owner|path|bound|bounded|claude-tab-decoration|PersistedKey|PersistedKeyValue|persist' $registry_files || true
fi

echo
echo "Target store:"
cat -n apps/desktop/src/renderer/stores/claude-tab-decoration.ts

echo
echo "Search store imports/usages:"
rg -n "useClaudeTabDecorationStore|claude-tab-decoration" apps/desktop/src/renderer || true

Repository: superset-sh/superset

Length of output: 3666


Register and document this renderer localStorage key.

claude-tab-decoration is persisted to renderer localStorage but is not registered in a persisted-key registry. Add it to persisted-key-registry.test-data.ts with an explicit bound, deletion owner/path, and removal plan.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/stores/claude-tab-decoration.ts` around lines 14 -
25, Register the "claude-tab-decoration" persistence key in
persisted-key-registry.test-data.ts, including an explicit size bound, the
responsible deletion owner/path, and a documented removal plan. Keep the
existing key name in use by useClaudeTabDecorationStore unchanged and follow the
registry’s established entry format.

Source: Coding guidelines

Comment on lines +91 to +102
for (const [terminalId, entry] of this.watched) {
const binding = eligible.get(terminalId);
const expectedPath = binding
? resolveTranscriptPath(
binding.cwd as string,
binding.agentSessionId as string,
)
: null;
if (!binding || expectedPath !== entry.path) {
entry.watcher.close();
this.watched.delete(terminalId);
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cancel pending debounce work when a transcript watch is removed.

A scheduled callback retains the previous transcript path. If a terminal exits, a session path changes, or FSWatcher fails during the debounce window, the callback still updates metadata and broadcasts an event for the old binding. Clear the terminal timer whenever this code removes a watch.

Also applies to: 139-142

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/terminal-agents/claude-transcript-watcher.ts`
around lines 91 - 102, Update the watch-removal branch in the watched-entry
reconciliation loop to clear the terminal’s pending debounce timer before
closing and deleting the watch. Reuse the existing timer-management symbol and
apply the same cancellation in the corresponding removal path around the
additionally referenced block, ensuring callbacks for stale transcript paths
cannot update metadata or broadcast events.

Comment on lines +125 to +144
// Check immediately in case the transcript already has lines from
// before this watcher started (e.g. host-service restart mid-session).
this.checkAndEmit(terminalId, workspaceId, path);

let watcher: FSWatcher;
try {
watcher = watch(path, () => {
this.scheduleCheck(terminalId, workspaceId, path);
});
} catch {
// Transcript doesn't exist yet — the next rescan retries.
return;
}

watcher.on("error", () => {
watcher.close();
this.watched.delete(terminalId);
});

this.watched.set(terminalId, { workspaceId, path, watcher });

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Register the watcher before the initial metadata read.

checkAndEmit() can synchronously emit the store change event. The listener installed in start() then calls refresh() while watched has no entry for this terminal. The nested refresh creates a second FSWatcher, and the outer call overwrites the map entry. One watcher remains untracked and can emit duplicate updates.

Create and store the watcher before calling checkAndEmit().

Proposed fix
-		this.checkAndEmit(terminalId, workspaceId, path);
-
 		let watcher: FSWatcher;
 		try {
 			watcher = watch(path, () => {
 				this.scheduleCheck(terminalId, workspaceId, path);
 			});
@@
 
 		this.watched.set(terminalId, { workspaceId, path, watcher });
+		this.checkAndEmit(terminalId, workspaceId, path);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/terminal-agents/claude-transcript-watcher.ts`
around lines 125 - 144, Update the watcher setup in the start() flow to create
and register the FSWatcher in this.watched before calling checkAndEmit().
Preserve the existing retry behavior when watch(path, ...) fails, and ensure the
registered watcher retains the correct terminalId, workspaceId, path, callback,
and error cleanup handling.

Comment on lines +71 to +108
let contents: string;
try {
contents = readFileSync(transcriptPath, "utf8");
} catch {
return {};
}

let color: string | undefined;
let aiTitle: string | undefined;
let customTitle: string | undefined;

for (const line of contents.split("\n")) {
if (!line) continue;
let parsed: unknown;
try {
parsed = JSON.parse(line);
} catch {
continue;
}
if (typeof parsed !== "object" || parsed === null) continue;
const record = parsed as Record<string, unknown>;
if (
record.type === "agent-color" &&
typeof record.agentColor === "string"
) {
color = record.agentColor;
} else if (
record.type === "ai-title" &&
typeof record.aiTitle === "string"
) {
aiTitle = record.aiTitle;
} else if (
record.type === "custom-title" &&
typeof record.customTitle === "string"
) {
customTitle = record.customTitle;
}
}

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.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Avoid a complete synchronous scan for each transcript update.

ClaudeTranscriptWatcher.checkAndEmit calls this reader for watched transcript changes. This function reads, splits, and parses the complete file on every call. The work and event-loop blocking time grow with transcript size, even when one line was appended. Cache a file offset and the latest metadata per path. Parse only appended complete lines, and reset the cache after truncation or replacement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/terminal-agents/claude-transcript.ts` around lines
71 - 108, Update the transcript reader used by
ClaudeTranscriptWatcher.checkAndEmit to maintain per-path file offsets and
latest metadata, reading and parsing only newly appended complete lines instead
of rescanning the entire file. Detect truncation or replacement, reset that
path’s cached offset and metadata, and preserve the existing metadata extraction
behavior for appended records.

Comment on lines +95 to +100
if (cwd) {
this.liveMeta.set(terminalId, {
...this.liveMeta.get(terminalId),
cwd,
});
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear live metadata when the binding identity changes.

liveMeta is keyed only by terminalId. If agentId or agentSessionId changes on the same terminal, the old title and color remain in the overlay and are returned with the new binding. Clear the old overlay on an identity transition before storing the new cwd.

Add coverage for a new session on an existing terminal.

Also applies to: 121-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/terminal-agents/store.ts` around lines 95 - 100,
Update the terminal metadata handling around the liveMeta updates to detect
changes to the terminal’s agentId or agentSessionId, clear the existing overlay
metadata before applying the new binding, and then store the new cwd without
retaining the previous title or color. Add coverage for creating a new session
on an existing terminal.

Comment on lines +171 to +182
updateAgentMeta(
terminalId: string,
workspaceId: string,
meta: { title?: string; color?: string },
): void {
const existing = this.liveMeta.get(terminalId);
if (existing?.title === meta.title && existing?.color === meta.color) {
return;
}
this.liveMeta.set(terminalId, { ...existing, ...meta });
this.emit("change", workspaceId);
}

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.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Publish agent:meta only when metadata changes.

updateAgentMeta() treats an omitted partial field as a change when that field already exists. checkAndEmit() also broadcasts agent:meta even when updateAgentMeta() performs no update. The periodic rescan therefore invalidates renderer binding queries for unchanged sessions.

  • packages/host-service/src/terminal-agents/store.ts#L171-L182: compare only metadata fields present in meta, then return whether the overlay changed.
  • packages/host-service/src/terminal-agents/claude-transcript-watcher.ts#L163-L185: call broadcastAgentMeta() only when updateAgentMeta() reports a change.

Based on review stack context: renderer subscriptions invalidate binding queries for agent:meta events.

📍 Affects 2 files
  • packages/host-service/src/terminal-agents/store.ts#L171-L182 (this comment)
  • packages/host-service/src/terminal-agents/claude-transcript-watcher.ts#L163-L185
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/terminal-agents/store.ts` around lines 171 - 182,
Update packages/host-service/src/terminal-agents/store.ts lines 171-182 in
updateAgentMeta() to compare only fields supplied in meta and return whether the
overlay actually changed. Update
packages/host-service/src/terminal-agents/claude-transcript-watcher.ts lines
163-185 so checkAndEmit() calls broadcastAgentMeta() only when updateAgentMeta()
reports a change.


const agent = normalizeAgentIdentity(input.agent);
const occurredAt = Date.now();
const cwd = trimOrUndefined(input.cwd);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the exact cwd value.

cwd is a filesystem identity. trimOrUndefined changes a valid path whose final directory name ends with whitespace. encodeProjectDirName uses every character to derive the Claude transcript directory, so the watcher reads a different path and cannot find the session metadata. Treat only an absent or empty value as missing. Do not trim a nonempty cwd.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/trpc/router/notifications/notifications.ts` at line
77, Update the cwd handling around encodeProjectDirName to preserve every
character of a nonempty input, including trailing whitespace; replace
trimOrUndefined with logic that treats only absent or empty values as missing
while leaving valid filesystem paths unchanged.

moshfeu added 4 commits August 4, 2026 15:37
…n tabs and sidebar

Watch each Claude terminal-agent's transcript file for /rename and
/color changes and surface them as the tab bar title/icon and the
sidebar's agent hover-list, instead of the generic "Claude" label.
… unreachable

Better Auth and tRPC only catch their own typed errors — a raw driver
failure (e.g. Docker/Postgres down locally) rethrows as an opaque,
bodiless 500 instead. Detect this specific case in the auth route and
the shared tRPC errorFormatter, and surface it as a toast in the
desktop app instead of failing silently or showing a generic error.
…dd appearance toggle

Wraps the tab's full content (icon, title, close button) in a color-tinted
background derived from the Claude Code session's /color, replacing the
earlier corner-dot approach. Adds a Settings > Appearance toggle to opt out.
Sidebar hover-row decoration is deferred to a separate PR.
- Sanitize sessionId before it reaches the transcript file path (was only
  cwd-sanitized), closing a path-traversal opening in the unauthenticated
  hook endpoint
- Drop leftover debug console.log calls from claude-transcript-watcher
- Fix stale doc comments left over from the renderTabLabelWrapper rename
- Share the databaseUnavailable error-data key/type between packages/trpc
  and the desktop provider instead of re-typing it by hand on each side
- Add test coverage for isDatabaseConnectivityError
- Scope useTerminalAgentBinding to a single terminal via a query selector
  so one tab's decoration wrapper no longer re-renders on every other
  terminal's binding change
@moshfeu
moshfeu force-pushed the feat/claude-tab-title-color branch from 8297c88 to eba7a15 Compare August 4, 2026 12:42

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/hooks/host-service/useTerminalAgentBindings/useTerminalAgentBindings.ts (1)

16-22: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Trigger a binding refresh when hostUrl changes.

The workspace query key does not invalidate when useWorkspaceHostUrl changes state to the same workspace on a different host URL. With staleTime: 30_000, fresh cached bindings can stay visible from the previous host until another lifecycle/meta event arrives or the cache becomes stale. Keep the workspace-only key and invalidate/refetch the active query when hostUrl changes within staleTime, including a regression test for that path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/hooks/host-service/useTerminalAgentBindings/useTerminalAgentBindings.ts`
around lines 16 - 22, Keep getTerminalAgentBindingsQueryKey keyed only by
workspaceId, but update the hook’s hostUrl-change handling to invalidate or
refetch the active terminal-agent-bindings query when the URL changes for the
same workspace, even while cached data is fresh. Reuse the existing query client
and host URL state flow, and add a regression test covering a hostUrl change
within the 30-second staleTime.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@apps/desktop/src/renderer/hooks/host-service/useTerminalAgentBindings/useTerminalAgentBindings.ts`:
- Around line 16-22: Keep getTerminalAgentBindingsQueryKey keyed only by
workspaceId, but update the hook’s hostUrl-change handling to invalidate or
refetch the active terminal-agent-bindings query when the URL changes for the
same workspace, even while cached data is fresh. Reuse the existing query client
and host URL state flow, and add a regression test covering a hostUrl change
within the 30-second staleTime.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5bdfde69-9f73-4ea7-9db8-860c08fed81f

📥 Commits

Reviewing files that changed from the base of the PR and between 8297c88 and eba7a15.

📒 Files selected for processing (13)
  • apps/api/src/app/api/auth/[...all]/route.ts
  • apps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.sh
  • apps/desktop/src/renderer/hooks/host-service/useTerminalAgentBindings/useTerminalAgentBindings.ts
  • apps/desktop/src/renderer/hooks/host-service/useWorkspaceEvent/useWorkspaceEvent.ts
  • apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarAgentsChip/components/DashboardSidebarAgentHoverRow/DashboardSidebarAgentHoverRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/hooks/useDashboardSidebarWorkspaceRunningAgents/useDashboardSidebarWorkspaceRunningAgents.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalPaneIcon.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalTabContentWrapper.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/renderTerminalTabIcon.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
🚧 Files skipped from review as they are similar to previous changes (12)
  • apps/desktop/src/renderer/hooks/host-service/useWorkspaceEvent/useWorkspaceEvent.ts
  • apps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.sh
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/hooks/useDashboardSidebarWorkspaceRunningAgents/useDashboardSidebarWorkspaceRunningAgents.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/index.ts
  • apps/api/src/app/api/auth/[...all]/route.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/renderTerminalTabIcon.tsx
  • apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalTabContentWrapper.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarAgentsChip/components/DashboardSidebarAgentHoverRow/DashboardSidebarAgentHoverRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalPaneIcon.tsx

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

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.

12 issues found and verified against the latest diff

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="packages/shared/src/db-connectivity-error.ts">

<violation number="1" location="packages/shared/src/db-connectivity-error.ts:12">
P2: Ordinary Neon query errors will be reported to clients as database connectivity failures because `NeonDbError` is the driver’s general error class, not a connectivity-specific class. Restrict this check to the driver’s connection message/codes (while retaining the existing `ECONNREFUSED` and `Unable to connect` cases).</violation>

<violation number="2" location="packages/shared/src/db-connectivity-error.ts:16">
P2: Production users will receive a local-development Docker diagnosis when the deployed API’s database is unavailable. Use environment-neutral wording, optionally adding the Docker check only as a local-development hint.</violation>
</file>

<file name="packages/host-service/src/terminal-agents/claude-transcript.test.ts">

<violation number="1" location="packages/host-service/src/terminal-agents/claude-transcript.test.ts:92">
P3: The custom-title test doesn't actually cover the priority behavior it's meant to guard. It appends an agent-name line carrying the same 'my session' value as custom-title, so the assertion passes even if the implementation read agent-name (which the source explicitly says to ignore) instead of custom-title. Separately, the most important branch — custom-title winning over a later ai-title line, which the implementation comments call out as the reason for `customTitle ?? aiTitle` — has no test. Consider adding a case with an ai-title line after the custom-title to lock in that behavior.</violation>
</file>

<file name="packages/host-service/src/events/types.ts">

<violation number="1" location="packages/host-service/src/events/types.ts:43">
P2: Consumers of `@superset/host-service/events` cannot import the new `AgentMetaMessage` type by name, unlike every other server-event message. Re-export it from `packages/host-service/src/events/index.ts` so the public event API stays complete and consistent.</violation>
</file>

<file name="packages/host-service/src/terminal-agents/store.ts">

<violation number="1" location="packages/host-service/src/terminal-agents/store.ts:96">
P1: Agent/session swaps retain the previous terminal's Claude title/color (and possibly cwd), so a new Codex binding or Claude session can be returned with stale session metadata; clear the live-meta entry whenever agent identity or session changes before applying the new metadata.</violation>

<violation number="2" location="packages/host-service/src/terminal-agents/store.ts:177">
P2: A partial transcript refresh is treated as changed whenever the omitted title or color was previously populated, causing needless cache invalidations and, during a session transition, recursive watcher refreshes; compare only metadata keys present in `meta`.</violation>

<violation number="3" location="packages/host-service/src/terminal-agents/store.ts:256">
P2: Metadata recorded before the first agent binding is never cleaned up on terminal exit because `deleteTerminal` returns before deleting `liveMeta`, allowing stale title/color/cwd on a reused terminal ID and leaking one map entry per such terminal; delete the overlay independently of whether a binding exists.</violation>
</file>

<file name="packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx">

<violation number="1" location="packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx:36">
P3: When a tab enters rename/edit mode (double-click or context menu), the `TabRenameInput` is rendered inside a plain `<div>` that does **not** go through `renderContentWrapper`. If the wrapper applies visual styling such as a background tint from the Claude session color (as described in the PR), the background will disappear during the entire rename operation and reappear only after saving or canceling.

This is a minor visual discontinuity since editing is transient, but it's worth considering whether the wrapper should also wrap the editing state for consistency — or whether the caller should be aware of this exclusion if they depend on persistent background styling.</violation>
</file>

<file name="packages/host-service/src/terminal-agents/claude-transcript-watcher.ts">

<violation number="1" location="packages/host-service/src/terminal-agents/claude-transcript-watcher.ts:127">
P1: Bindings discovered after startup can leak an FSWatcher when their transcript already has a title or color. The initial metadata check emits a synchronous store change before the watcher is registered, causing a re-entrant `watchBinding`; the second watcher overwrites the first in `watched`, so the orphaned watcher is never closed. Registering the watcher/map entry before the initial check, or guarding refresh against re-entrancy, would avoid the per-binding resource leak.</violation>

<violation number="2" location="packages/host-service/src/terminal-agents/claude-transcript-watcher.ts:157">
P2: A pending debounced transcript check can resurrect metadata after its terminal has exited. The timer callback runs even after `refresh()` removes the terminal's watcher, and `checkAndEmit` updates the store without checking that the path is still active. Canceling the timer when removing a watcher, or validating the current watched entry before checking, would prevent stale titles/colors from being reintroduced.</violation>

<violation number="3" location="packages/host-service/src/terminal-agents/claude-transcript-watcher.ts:181">
P2: The renderer's binding query is invalidated every 10 seconds for every watched Claude session even when its title/color did not change. `updateAgentMeta` suppresses its own change event for identical metadata, but `broadcastAgentMeta` is unconditional; real changes can also be broadcast twice because the store listener re-enters `refresh`. Broadcasting only when the store actually changes would keep rescans from causing repeated query work.</violation>
</file>

<file name="apps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.sh">

<violation number="1" location="apps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.sh:25">
P2: Claude session metadata is not discovered for cwd values containing JSON escapes, which commonly includes Windows paths. `CWD` is extracted from raw JSON with grep and then escaped again when constructing the payload, so the host resolves a different transcript directory. Parsing the hook input with a real JSON parser before building the payload would preserve the actual cwd.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}

if (cwd) {
this.liveMeta.set(terminalId, {

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.

P1: Agent/session swaps retain the previous terminal's Claude title/color (and possibly cwd), so a new Codex binding or Claude session can be returned with stale session metadata; clear the live-meta entry whenever agent identity or session changes before applying the new metadata.

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

<comment>Agent/session swaps retain the previous terminal's Claude title/color (and possibly cwd), so a new Codex binding or Claude session can be returned with stale session metadata; clear the live-meta entry whenever agent identity or session changes before applying the new metadata.</comment>

<file context>
@@ -70,13 +84,21 @@ export class TerminalAgentStore extends EventEmitter {
 		}
 
+		if (cwd) {
+			this.liveMeta.set(terminalId, {
+				...this.liveMeta.get(terminalId),
+				cwd,
</file context>


// Check immediately in case the transcript already has lines from
// before this watcher started (e.g. host-service restart mid-session).
this.checkAndEmit(terminalId, workspaceId, path);

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.

P1: Bindings discovered after startup can leak an FSWatcher when their transcript already has a title or color. The initial metadata check emits a synchronous store change before the watcher is registered, causing a re-entrant watchBinding; the second watcher overwrites the first in watched, so the orphaned watcher is never closed. Registering the watcher/map entry before the initial check, or guarding refresh against re-entrancy, would avoid the per-binding resource leak.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/terminal-agents/claude-transcript-watcher.ts, line 127:

<comment>Bindings discovered after startup can leak an FSWatcher when their transcript already has a title or color. The initial metadata check emits a synchronous store change before the watcher is registered, causing a re-entrant `watchBinding`; the second watcher overwrites the first in `watched`, so the orphaned watcher is never closed. Registering the watcher/map entry before the initial check, or guarding refresh against re-entrancy, would avoid the per-binding resource leak.</comment>

<file context>
@@ -0,0 +1,187 @@
+
+		// Check immediately in case the transcript already has lines from
+		// before this watcher started (e.g. host-service restart mid-session).
+		this.checkAndEmit(terminalId, workspaceId, path);
+
+		let watcher: FSWatcher;
</file context>

}

export const DATABASE_UNAVAILABLE_MESSAGE =
"Could not reach the database. Is your local dev DB stack (Docker) running?";

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.

P2: Production users will receive a local-development Docker diagnosis when the deployed API’s database is unavailable. Use environment-neutral wording, optionally adding the Docker check only as a local-development hint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/shared/src/db-connectivity-error.ts, line 16:

<comment>Production users will receive a local-development Docker diagnosis when the deployed API’s database is unavailable. Use environment-neutral wording, optionally adding the Docker check only as a local-development hint.</comment>

<file context>
@@ -0,0 +1,24 @@
+}
+
+export const DATABASE_UNAVAILABLE_MESSAGE =
+	"Could not reach the database. Is your local dev DB stack (Docker) running?";
+
+/** Key set on tRPC's `errorFormatter` `data` field — shared so the client
</file context>

error instanceof Error
? `${error.name} ${error.message} ${String(error.cause ?? "")}`
: String(error);
return /ECONNREFUSED|NeonDbError|Unable to connect/i.test(text);

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.

P2: Ordinary Neon query errors will be reported to clients as database connectivity failures because NeonDbError is the driver’s general error class, not a connectivity-specific class. Restrict this check to the driver’s connection message/codes (while retaining the existing ECONNREFUSED and Unable to connect cases).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/shared/src/db-connectivity-error.ts, line 12:

<comment>Ordinary Neon query errors will be reported to clients as database connectivity failures because `NeonDbError` is the driver’s general error class, not a connectivity-specific class. Restrict this check to the driver’s connection message/codes (while retaining the existing `ECONNREFUSED` and `Unable to connect` cases).</comment>

<file context>
@@ -0,0 +1,24 @@
+		error instanceof Error
+			? `${error.name} ${error.message} ${String(error.cause ?? "")}`
+			: String(error);
+	return /ECONNREFUSED|NeonDbError|Unable to connect/i.test(text);
+}
+
</file context>

* `agent:lifecycle` — that type's `eventType` drives the working
* indicator/completion chime, and a meta-only change must not trigger those.
*/
export interface AgentMetaMessage {

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.

P2: Consumers of @superset/host-service/events cannot import the new AgentMetaMessage type by name, unlike every other server-event message. Re-export it from packages/host-service/src/events/index.ts so the public event API stays complete and consistent.

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

<comment>Consumers of `@superset/host-service/events` cannot import the new `AgentMetaMessage` type by name, unlike every other server-event message. Re-export it from `packages/host-service/src/events/index.ts` so the public event API stays complete and consistent.</comment>

<file context>
@@ -34,6 +34,19 @@ export interface AgentLifecycleMessage {
+ * `agent:lifecycle` — that type's `eventType` drives the working
+ * indicator/completion chime, and a meta-only change must not trigger those.
+ */
+export interface AgentMetaMessage {
+	type: "agent:meta";
+	workspaceId: string;
</file context>

// The renderer's terminal-agent-bindings query only invalidates on
// `agent:lifecycle`/`terminal:lifecycle`/`agent:meta` broadcasts — the
// store's own "change" EventEmitter never reaches the renderer.
this.eventBus.broadcastAgentMeta({

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.

P2: The renderer's binding query is invalidated every 10 seconds for every watched Claude session even when its title/color did not change. updateAgentMeta suppresses its own change event for identical metadata, but broadcastAgentMeta is unconditional; real changes can also be broadcast twice because the store listener re-enters refresh. Broadcasting only when the store actually changes would keep rescans from causing repeated query work.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/terminal-agents/claude-transcript-watcher.ts, line 181:

<comment>The renderer's binding query is invalidated every 10 seconds for every watched Claude session even when its title/color did not change. `updateAgentMeta` suppresses its own change event for identical metadata, but `broadcastAgentMeta` is unconditional; real changes can also be broadcast twice because the store listener re-enters `refresh`. Broadcasting only when the store actually changes would keep rescans from causing repeated query work.</comment>

<file context>
@@ -0,0 +1,187 @@
+		// The renderer's terminal-agent-bindings query only invalidates on
+		// `agent:lifecycle`/`terminal:lifecycle`/`agent:meta` broadcasts — the
+		// store's own "change" EventEmitter never reaches the renderer.
+		this.eventBus.broadcastAgentMeta({
+			workspaceId,
+			terminalId,
</file context>

this.debounceTimers.set(
terminalId,
setTimeout(() => {
this.debounceTimers.delete(terminalId);

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.

P2: A pending debounced transcript check can resurrect metadata after its terminal has exited. The timer callback runs even after refresh() removes the terminal's watcher, and checkAndEmit updates the store without checking that the path is still active. Canceling the timer when removing a watcher, or validating the current watched entry before checking, would prevent stale titles/colors from being reintroduced.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/terminal-agents/claude-transcript-watcher.ts, line 157:

<comment>A pending debounced transcript check can resurrect metadata after its terminal has exited. The timer callback runs even after `refresh()` removes the terminal's watcher, and `checkAndEmit` updates the store without checking that the path is still active. Canceling the timer when removing a watcher, or validating the current watched entry before checking, would prevent stale titles/colors from being reintroduced.</comment>

<file context>
@@ -0,0 +1,187 @@
+		this.debounceTimers.set(
+			terminalId,
+			setTimeout(() => {
+				this.debounceTimers.delete(terminalId);
+				this.checkAndEmit(terminalId, workspaceId, path);
+			}, DEBOUNCE_MS),
</file context>

fi
SESSION_ID=${RESOURCE_ID:-$HOOK_SESSION_ID}

CWD=$(echo "$INPUT" | grep -oE '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -oE '"[^"]*"$' | tr -d '"')

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.

P2: Claude session metadata is not discovered for cwd values containing JSON escapes, which commonly includes Windows paths. CWD is extracted from raw JSON with grep and then escaped again when constructing the payload, so the host resolves a different transcript directory. Parsing the hook input with a real JSON parser before building the payload would preserve the actual cwd.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.sh, line 25:

<comment>Claude session metadata is not discovered for cwd values containing JSON escapes, which commonly includes Windows paths. `CWD` is extracted from raw JSON with grep and then escaped again when constructing the payload, so the host resolves a different transcript directory. Parsing the hook input with a real JSON parser before building the payload would preserve the actual cwd.</comment>

<file context>
@@ -22,6 +22,8 @@ if [ -z "$RESOURCE_ID" ]; then
 fi
 SESSION_ID=${RESOURCE_ID:-$HOOK_SESSION_ID}
 
+CWD=$(echo "$INPUT" | grep -oE '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -oE '"[^"]*"$' | tr -d '"')
+
 # Claude/Mastra/Droid/Kimi use "hook_event_name"; Grok uses camelCase
</file context>

expect(readLatestColorAndTitle(path)).toEqual({ color: "blue" });
});

it("reads a user-set title from a custom-title line", () => {

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.

P3: The custom-title test doesn't actually cover the priority behavior it's meant to guard. It appends an agent-name line carrying the same 'my session' value as custom-title, so the assertion passes even if the implementation read agent-name (which the source explicitly says to ignore) instead of custom-title. Separately, the most important branch — custom-title winning over a later ai-title line, which the implementation comments call out as the reason for customTitle ?? aiTitle — has no test. Consider adding a case with an ai-title line after the custom-title to lock in that behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/terminal-agents/claude-transcript.test.ts, line 92:

<comment>The custom-title test doesn't actually cover the priority behavior it's meant to guard. It appends an agent-name line carrying the same 'my session' value as custom-title, so the assertion passes even if the implementation read agent-name (which the source explicitly says to ignore) instead of custom-title. Separately, the most important branch — custom-title winning over a later ai-title line, which the implementation comments call out as the reason for `customTitle ?? aiTitle` — has no test. Consider adding a case with an ai-title line after the custom-title to lock in that behavior.</comment>

<file context>
@@ -0,0 +1,108 @@
+		expect(readLatestColorAndTitle(path)).toEqual({ color: "blue" });
+	});
+
+	it("reads a user-set title from a custom-title line", () => {
+		const path = join(dir, "custom-title.jsonl");
+		writeFileSync(
</file context>

onRename: (title: string | undefined) => void;
icon?: ReactNode;
/** Wraps the tab's full content (icon, title, AND the accessory/close-button area). */
renderContentWrapper?: (tab: Tab<TData>, children: ReactNode) => ReactNode;

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.

P3: When a tab enters rename/edit mode (double-click or context menu), the TabRenameInput is rendered inside a plain <div> that does not go through renderContentWrapper. If the wrapper applies visual styling such as a background tint from the Claude session color (as described in the PR), the background will disappear during the entire rename operation and reappear only after saving or canceling.

This is a minor visual discontinuity since editing is transient, but it's worth considering whether the wrapper should also wrap the editing state for consistency — or whether the caller should be aware of this exclusion if they depend on persistent background styling.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx, line 36:

<comment>When a tab enters rename/edit mode (double-click or context menu), the `TabRenameInput` is rendered inside a plain `<div>` that does **not** go through `renderContentWrapper`. If the wrapper applies visual styling such as a background tint from the Claude session color (as described in the PR), the background will disappear during the entire rename operation and reappear only after saving or canceling.

This is a minor visual discontinuity since editing is transient, but it's worth considering whether the wrapper should also wrap the editing state for consistency — or whether the caller should be aware of this exclusion if they depend on persistent background styling.</comment>

<file context>
@@ -32,6 +32,8 @@ interface TabItemProps<TData> {
 	onRename: (title: string | undefined) => void;
 	icon?: ReactNode;
+	/** Wraps the tab's full content (icon, title, AND the accessory/close-button area). */
+	renderContentWrapper?: (tab: Tab<TData>, children: ReactNode) => ReactNode;
 	accessory?: ReactNode;
 }
</file context>

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