Reference: PRD_TITLES.md
Goal: Replace the CLI-based (execFile) implementation with an in-process sdk.query() call.
- Rewrite
src/main/services/claude-session-title.ts:- Remove all
execFile/child_processimports and types (ExecFileExecutor,defaultExecFile) - Import
loadClaudeSDKfrom./claude-sdk-loader,mkdirSync/existsSyncfromnode:fs,joinfromnode:path,homedirfromnode:os - Compute
titlesDirasjoin(homedir(), '.hive', 'titles') - Change function signature to
generateSessionTitle(message: string, claudeBinaryPath?: string | null): Promise<string | null> - Ensure
~/.hive/titles/exists viamkdirSync(titlesDir, { recursive: true }) - Load SDK via
loadClaudeSDK() - Build prompt from
TITLE_PROMPT + truncatedMessage(keep existing prompt text andMAX_MESSAGE_LENGTHtruncation) - Call
sdk.query({ prompt, options: { cwd: titlesDir, model: 'haiku', maxTurns: 1, pathToClaudeCodeExecutable (if provided) } }) - Create an
AbortControllerwith a 15ssetTimeoutfor timeout protection, pass it in options, clean up on completion - Iterate async generator, extract text from the
resultmessage type (msg.type === 'result'→msg.result) - Trim result, validate (non-empty, ≤50 chars), return title or
null - Wrap everything in try/catch — never throws, log errors
- Remove all
claude-session-title.tshas zero references tochild_process,execFile,ExecFileExecutor,defaultExecFile,CLAUDECODE- Function uses
loadClaudeSDK()→sdk.query()withcwd: ~/.hive/titles/ - Function signature is
(message, claudeBinaryPath?) → Promise<string | null> - File compiles with no TypeScript errors:
npx tsc --noEmiton the file passes
- Defer to Session 2 (unit tests rewrite)
Goal: Replace all execFile-based mocks with loadClaudeSDK mocks.
- Rewrite
test/claude-session-title.test.ts:- Remove
ExecFileExecutorimport,mockExecutor,mockExecFileSuccess,mockExecFileErrorhelpers - Mock
../src/main/services/claude-sdk-loaderreturning a fakeloadClaudeSDKthat provides asdk.query()returning an async generator - Mock
node:fsformkdirSync/existsSync(to avoid creating real dirs in tests) - Create helper
mockQueryResult(text: string)that makessdk.query()return an async generator yielding{ type: 'result', result: text } - Create helper
mockQueryError(err: Error)that makessdk.query()throw - Rewrite all existing test cases to use new helpers:
- Returns trimmed title on successful SDK query
- Returns
nullon empty SDK result - Returns
nullon whitespace-only SDK result - Returns
nullwhen title >50 chars - Returns title when exactly 50 chars
- Returns
nullwhen SDK query throws - Truncates messages >2000 chars in the prompt passed to
sdk.query() - Does not truncate messages under 2000 chars
- Never throws — always returns string or
null
- Add new test cases for SDK-specific behavior:
- Uses
model: 'haiku'in query options - Sets
cwdto~/.hive/titles/path - Passes
maxTurns: 1in query options - Passes
pathToClaudeCodeExecutablewhenclaudeBinaryPathis provided - Omits
pathToClaudeCodeExecutablewhenclaudeBinaryPathisnull/undefined - Creates
~/.hive/titles/directory if it doesn't exist - Aborts query via AbortController after timeout
- Uses
- Remove
- Zero references to
execFile,ExecFileExecutor,mockExecutorin the test file - All tests mock
loadClaudeSDKandnode:fsinstead ofchild_process - All tests pass:
npx vitest run test/claude-session-title.test.ts
npx vitest run test/claude-session-title.test.ts— all tests pass- No skipped or pending tests
Goal: Wire up the new signature and remove the binary-path guard.
- In
src/main/services/claude-code-implementer.ts, updatehandleTitleGeneration:- Change call from
generateSessionTitle(this.claudeBinaryPath!, userMessage)togenerateSessionTitle(userMessage, this.claudeBinaryPath) - Update JSDoc comment to say "via Agent SDK" instead of "via Claude CLI"
- Change call from
- Update the call site in
prompt()(~line 498-504):- Change
if (wasPending && this.claudeBinaryPath)toif (wasPending) - (Binary path is now optional — SDK works without it)
- Change
- Remove the now-unused
import { execFile as nodeExecFile } from 'node:child_process'if it was only used by title generation (verify it's not used elsewhere in the file — it isn't,claude-code-implementer.tsnever imported it)
handleTitleGenerationcallsgenerateSessionTitle(userMessage, this.claudeBinaryPath)- Call site guard is
if (wasPending)without binary path check - File compiles with no TypeScript errors
npx vitest run test/claude-code-title-integration.test.ts— all 13 tests passnpx vitest run test/phase-21/session-2/claude-code-implementer.test.ts— all 18 tests passnpx vitest run test/phase-21/session-3/claude-lifecycle.test.ts— all 32 tests pass
Goal: Ensure integration tests match the new generateSessionTitle signature.
- In
test/claude-code-title-integration.test.ts:- Verify the mock for
generateSessionTitlestill works — it usesmockGenerateSessionTitlewhich is signature-agnostic (it captures all args via...args) - If any tests assert on the arguments passed to
generateSessionTitle, update the expected argument order from(binaryPath, message)to(message, binaryPath) - Run and confirm all 13 tests pass without changes (the mock is
(...args: any[]) => mockGenerateSessionTitle(...args)which is flexible)
- Verify the mock for
- All 13 integration tests pass with no modifications (or with minimal arg-order updates)
- No test references the old
(binaryPath, message)argument order
npx vitest run test/claude-code-title-integration.test.ts— 13/13 passnpx vitest run test/claude-session-title.test.ts test/claude-code-title-integration.test.ts test/phase-21/session-2/claude-code-implementer.test.ts— all pass together
Goal: Verify the full flow works, clean up dead code, confirm no regressions.
- Run the combined test suite:
npx vitest run test/claude-session-title.test.ts test/claude-code-title-integration.test.ts test/phase-21/session-2/claude-code-implementer.test.ts test/phase-21/session-3/claude-lifecycle.test.ts
- Verify
~/.hive/titles/directory behavior:- Confirm the directory gets created on first title generation
- Confirm SDK session artifacts land there, not in the project directory
- Clean up
PRD_TITLES.mdandIMPLEMENTATION_TITLE.md— remove or move to docs if desired - Manual smoke test in the app:
- Create a new Claude Code session
- Send a first message (e.g. "Fix the authentication bug in login.ts")
- Verify: session title updates in the sidebar within ~2-5s
- Verify: branch auto-renames from breed name to title-based name
- Send a second message — verify title does NOT regenerate
- Create a new session via undo+fork — verify title does NOT regenerate on the fork
- All automated tests pass
- Manual smoke test confirms title appears in sidebar and branch renames
- No references to the old CLI-based approach remain in source or test files
~/.hive/titles/directory is created and contains SDK artifacts
- All test files pass: unit (session-title), integration (title-integration), implementer, lifecycle
- Manual: title visible in sidebar within 5s of first message
- Manual: branch renamed from breed name to title-based name
- Manual: no title regeneration on second message or fork