Repository navigation
Conversation
📝 WalkthroughWalkthroughThe 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. ChangesDatabase connectivity error handling
Live Claude agent metadata
Claude tab decoration
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
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThe 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.
Confidence Score: 4/5This 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
|
| 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
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
| 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, | ||
| }); |
There was a problem hiding this 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)
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.| if (!binding || expectedPath !== entry.path) { | ||
| entry.watcher.close(); | ||
| this.watched.delete(terminalId); | ||
| } |
There was a problem hiding this 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)
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.There was a problem hiding this comment.
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 winApply the content wrapper during inline rename.
When
isEditingis true, this branch renders the input directly and skipsrenderContentWrapper. 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 valueMove
TerminalTabContentWrapperinto its own component folder.
TerminalTabContentWrapperis a component. Place it inTerminalTabContentWrapper/TerminalTabContentWrapper.tsxand add anindex.tsexport.As per coding guidelines, “Use one folder per component with
ComponentName/ComponentName.tsxandindex.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 winTest custom-title precedence after an automatic title.
The fixture has no
ai-titleaftercustom-title. It passes if the parser returns the last title record. Add a later automatic title and keep the"my session"expectation to protect/renameprecedence.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
📒 Files selected for processing (40)
apps/api/src/app/api/auth/[...all]/route.tsapps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.shapps/desktop/src/renderer/hooks/host-service/useTerminalAgentBindings/useTerminalAgentBindings.tsapps/desktop/src/renderer/hooks/host-service/useWorkspaceEvent/useWorkspaceEvent.tsapps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarAgentsChip/components/DashboardSidebarAgentHoverRow/DashboardSidebarAgentHoverRow.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/hooks/useDashboardSidebarWorkspaceRunningAgents/useDashboardSidebarWorkspaceRunningAgents.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalPaneIcon.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalTabContentWrapper.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/renderTerminalTabIcon.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsxapps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/AppearanceSettings.tsxapps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/ClaudeTabDecorationSection/ClaudeTabDecorationSection.tsxapps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/ClaudeTabDecorationSection/index.tsapps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.tsapps/desktop/src/renderer/stores/claude-tab-decoration.tsapps/desktop/src/shared/constants/claude-agent-colors.tspackages/host-service/src/app.tspackages/host-service/src/events/event-bus.tspackages/host-service/src/events/types.tspackages/host-service/src/terminal-agents/claude-transcript-watcher.tspackages/host-service/src/terminal-agents/claude-transcript.test.tspackages/host-service/src/terminal-agents/claude-transcript.tspackages/host-service/src/terminal-agents/index.tspackages/host-service/src/terminal-agents/store.test.tspackages/host-service/src/terminal-agents/store.tspackages/host-service/src/terminal-agents/types.tspackages/host-service/src/trpc/router/notifications/notifications.tspackages/panes/src/react/components/Workspace/Workspace.tsxpackages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsxpackages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsxpackages/panes/src/react/types.tspackages/shared/package.jsonpackages/shared/src/db-connectivity-error.test.tspackages/shared/src/db-connectivity-error.tspackages/trpc/src/trpc.tspackages/workspace-client/src/index.tspackages/workspace-client/src/lib/eventBus.ts
| export const useClaudeTabDecorationStore = create<ClaudeTabDecorationState>()( | ||
| devtools( | ||
| persist( | ||
| (set) => ({ | ||
| enabled: true, | ||
| setEnabled: (enabled) => set({ enabled }), | ||
| }), | ||
| { name: "claude-tab-decoration" }, | ||
| ), | ||
| { name: "ClaudeTabDecorationStore" }, | ||
| ), | ||
| ); |
There was a problem hiding this comment.
📐 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_filesRepository: 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 || trueRepository: 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
| 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); | ||
| } |
There was a problem hiding this comment.
🎯 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.
| // 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 }); |
There was a problem hiding this comment.
🩺 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.
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
🚀 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.
| if (cwd) { | ||
| this.liveMeta.set(terminalId, { | ||
| ...this.liveMeta.get(terminalId), | ||
| cwd, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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); | ||
| } |
There was a problem hiding this comment.
🚀 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 inmeta, then return whether the overlay changed.packages/host-service/src/terminal-agents/claude-transcript-watcher.ts#L163-L185: callbroadcastAgentMeta()only whenupdateAgentMeta()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); |
There was a problem hiding this comment.
🎯 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.
…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
8297c88 to
eba7a15
Compare
There was a problem hiding this comment.
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 winTrigger a binding refresh when
hostUrlchanges.The workspace query key does not invalidate when
useWorkspaceHostUrlchanges state to the same workspace on a different host URL. WithstaleTime: 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 whenhostUrlchanges withinstaleTime, 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
📒 Files selected for processing (13)
apps/api/src/app/api/auth/[...all]/route.tsapps/desktop/src/main/lib/agent-setup/templates/notify-hook.template.shapps/desktop/src/renderer/hooks/host-service/useTerminalAgentBindings/useTerminalAgentBindings.tsapps/desktop/src/renderer/hooks/host-service/useWorkspaceEvent/useWorkspaceEvent.tsapps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarAgentsChip/components/DashboardSidebarAgentHoverRow/DashboardSidebarAgentHoverRow.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/hooks/useDashboardSidebarWorkspaceRunningAgents/useDashboardSidebarWorkspaceRunningAgents.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalPaneIcon.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/TerminalTabContentWrapper.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneIcon/renderTerminalTabIcon.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsxapps/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
There was a problem hiding this comment.
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, { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?"; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 '"') |
There was a problem hiding this comment.
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", () => { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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>
Summary
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
/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”)./colorand/rename/auto-title, overlays livetitle/coloron bindings, publishesagent:metaevents, and the renderer updates via React Query selectors without unnecessary re-renders.Bug Fixes
sessionIdwhen resolving transcript paths and forwardscwdfrom 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.
Summary by CodeRabbit