The current CLI-based approach (claude -p "..." --model haiku) spawns a fresh Claude Code process for each title. This incurs a ~30-60s cold start (Node bootstrap, SDK init, auth handshake) and reliably times out. We need a fast approach that reuses the already-loaded SDK.
Use the already-imported @anthropic-ai/claude-agent-sdk directly via loadClaudeSDK() → sdk.query() to run a lightweight Haiku query. The SDK is already loaded in memory — no cold start, no CLI process spawn.
To avoid polluting real project directories with SDK session artifacts (.claude/ folders, JSONL transcripts), we run the query with cwd set to ~/.hive/titles/.
Replace the entire CLI-based implementation with an SDK-based one:
import { loadClaudeSDK } from './claude-sdk-loader'New function signature (drop claudeBinaryPath and executor params — no longer needed):
export async function generateSessionTitle(
message: string,
claudeBinaryPath?: string | null
): Promise<string | null>Implementation:
import { mkdirSync, existsSync } from 'node:fs'andimport { join } from 'node:path'andimport { homedir } from 'node:os'- Ensure
~/.hive/titles/exists:mkdirSync(titlesDir, { recursive: true }) - Load SDK:
const sdk = await loadClaudeSDK() - Build the title prompt (same
TITLE_PROMPT + truncatedMessageas today) - Call
sdk.query()with minimal options:const query = sdk.query({ prompt: fullPrompt, options: { cwd: titlesDir, model: 'haiku', maxTurns: 1, ...(claudeBinaryPath ? { pathToClaudeCodeExecutable: claudeBinaryPath } : {}) } })
- Iterate the async generator, collecting assistant text:
let resultText = '' for await (const msg of query) { if (msg.type === 'result') { resultText = (msg as any).result ?? '' break } }
- Trim, validate (non-empty, ≤50 chars), return title or
null - Wrap everything in try/catch — never throws
Key options:
cwd: ~/.hive/titles/— isolates SDK session artifacts from real projectsmodel: 'haiku'— fast and cheapmaxTurns: 1— one prompt, one response, donepathToClaudeCodeExecutable— still needed for ASAR compatibility, passed through fromClaudeCodeImplementer
No longer needed:
execFile/ExecFileExecutor/defaultExecFile— removed entirelyCLAUDECODEenv stripping — not relevant (SDK, not CLI subprocess)- 60s timeout — SDK query is fast (already connected)
Timeout handling:
- Use
AbortControllerwith a 15ssetTimeoutto abort the query if it takes too long - Clean up with
clearTimeouton success
Minimal change — update the call to match new signature:
// Before:
const title = await generateSessionTitle(this.claudeBinaryPath!, userMessage)
// After:
const title = await generateSessionTitle(userMessage, this.claudeBinaryPath)Minimal change — remove the this.claudeBinaryPath guard since the SDK approach doesn't require a binary path to function:
// Before:
if (wasPending && this.claudeBinaryPath) {
this.handleTitleGeneration(session, prompt).catch(() => {})
}
// After:
if (wasPending) {
this.handleTitleGeneration(session, prompt).catch(() => {})
}(The binary path is still passed as an optional hint for ASAR compat, but its absence no longer prevents title generation.)
Rewrite to mock loadClaudeSDK instead of execFile:
vi.mock('../src/main/services/claude-sdk-loader', () => ({
loadClaudeSDK: vi.fn()
}))Mock sdk.query() to return an async generator yielding { type: 'result', result: 'Fix auth refresh' }.
Test cases (keep the same coverage):
- Returns trimmed title on successful SDK query
- Returns
nullwhen SDK returns empty result - Returns
nullwhen title >50 chars - Returns
nullwhen SDK query throws - Truncates messages >2000 chars in the prompt
- Uses
model: 'haiku'in query options - Uses
~/.hive/titles/as cwd - Passes
maxTurns: 1 - Never throws — always returns string or
null - Passes
pathToClaudeCodeExecutablewhen provided - Omits
pathToClaudeCodeExecutablewhen not provided - Aborts query after timeout
Minimal change — update the mock for generateSessionTitle to match new signature (message first, binary path second). All 13 existing integration tests should continue to pass with only the mock update.
- Remove
ExecFileExecutortype export - Remove
defaultExecFilefunction - Remove
import { execFile } from 'node:child_process' - Remove the
env: { ...process.env, CLAUDECODE: undefined }logic
npx vitest run test/claude-session-title.test.tsnpx vitest run test/claude-code-title-integration.test.tsnpx vitest run test/phase-21/session-2/claude-code-implementer.test.ts
| File | Action | What Changes |
|---|---|---|
src/main/services/claude-session-title.ts |
REWRITE | Replace CLI spawn with sdk.query(), drop execFile deps |
src/main/services/claude-code-implementer.ts |
MODIFY | Update call signature (2 lines), remove binary path guard (1 line) |
test/claude-session-title.test.ts |
REWRITE | Mock loadClaudeSDK instead of execFile |
test/claude-code-title-integration.test.ts |
MODIFY | Update mock signature |
| CLI approach (old) | SDK approach (new) | |
|---|---|---|
| Cold start | ~30-60s (spawn Node, load SDK, auth) | ~0s (SDK already loaded in memory) |
| Process overhead | New OS process per title | In-process async call |
| Auth | Needs own auth handshake | Shares parent session's auth |
| Timeout risk | High (60s still marginal) | Low (~2-5s expected) |
| CLAUDECODE env hack | Required | Not needed |
| Binary path required | Yes (hard requirement) | No (optional ASAR hint) |