Merge branch 'codex/investigate-desktop-first-token-latency'

This commit is contained in:
程序员阿江(Relakkes) 2026-04-21 00:41:06 +08:00
commit c69c07fcfe
5 changed files with 66 additions and 21 deletions

View File

@ -39,7 +39,7 @@ type SettingsStore = {
export const useSettingsStore = create<SettingsStore>((set, get) => ({ export const useSettingsStore = create<SettingsStore>((set, get) => ({
permissionMode: 'default', permissionMode: 'default',
currentModel: null, currentModel: null,
effortLevel: 'high', effortLevel: 'medium',
availableModels: [], availableModels: [],
activeProviderName: null, activeProviderName: null,
locale: getStoredLocale(), locale: getStoredLocale(),

View File

@ -2723,16 +2723,30 @@ async function run(): Promise<CommanderCommand> {
})); }));
}, configs).catch(err => logForDebugging(`[MCP] ${label} connect error: ${err}`)); }, configs).catch(err => logForDebugging(`[MCP] ${label} connect error: ${err}`));
}; };
// Await all MCP configs — print mode is often single-turn, so // Await all MCP configs for regular print-mode sessions — they're
// "late-connecting servers visible next turn" doesn't help. SDK init // often single-turn, so "late-connecting servers visible next turn"
// message and turn-1 tool list both need configured MCP tools present. // doesn't help. SDK init message and turn-1 tool list should include
// configured MCP tools when running plain `claude -p`.
//
// Desktop/bridge sessions (`--sdk-url`) are different: the user is
// waiting on the first visible assistant token, not a fully populated
// MCP inventory. Blocking startup on slow or failing MCP servers (for
// example a local `chatlog` HTTP endpoint timing out) makes the whole
// chat feel frozen before the first word appears. In that mode we still
// kick off the connections immediately, but let them settle in the
// background so headlessStore picks them up after the session starts.
//
// Zero-server case is free via the early return in connectMcpBatch. // Zero-server case is free via the early return in connectMcpBatch.
// Connectors parallelize inside getMcpToolsCommandsAndResources // Connectors parallelize inside getMcpToolsCommandsAndResources
// (processBatched with Promise.all). claude.ai is awaited too — its // (processBatched with Promise.all). claude.ai is awaited too — its
// fetch was kicked off early (line ~2558) so only residual time blocks // fetch was kicked off early (line ~2558) so only residual time blocks
// here. --bare skips claude.ai entirely for perf-sensitive scripts. // here. --bare skips claude.ai entirely for perf-sensitive scripts.
profileCheckpoint('before_connectMcp'); profileCheckpoint('before_connectMcp');
await connectMcpBatch(regularMcpConfigs, 'regular'); if (sdkUrl) {
void connectMcpBatch(regularMcpConfigs, 'regular');
} else {
await connectMcpBatch(regularMcpConfigs, 'regular');
}
profileCheckpoint('after_connectMcp'); profileCheckpoint('after_connectMcp');
// Dedup: suppress plugin MCP servers that duplicate a claude.ai // Dedup: suppress plugin MCP servers that duplicate a claude.ai
// connector (connector wins), then connect claude.ai servers. // connector (connector wins), then connect claude.ai servers.

View File

@ -170,4 +170,18 @@ describe('ConversationService', () => {
expect(args[0]).toContain(path.join('bin', 'claude-haha')) expect(args[0]).toContain(path.join('bin', 'claude-haha'))
} }
}) })
test('buildSessionCliArgs enables partial assistant messages for desktop streaming', () => {
const service = new ConversationService() as any
const args = service.buildSessionCliArgs(
'123e4567-e89b-12d3-a456-426614174000',
'ws://127.0.0.1:3456/sdk/test-session?token=test-token',
false,
{ permissionMode: 'bypassPermissions' },
) as string[]
expect(args).toContain('--include-partial-messages')
expect(args).toContain('--sdk-url')
expect(args).toContain('--replay-user-messages')
})
}) })

View File

@ -38,7 +38,7 @@ const DEFAULT_MODELS = [
const EFFORT_LEVELS = ['low', 'medium', 'high', 'max'] as const const EFFORT_LEVELS = ['low', 'medium', 'high', 'max'] as const
const DEFAULT_MODEL = 'claude-opus-4-7' const DEFAULT_MODEL = 'claude-opus-4-7'
const DEFAULT_EFFORT = 'max' const DEFAULT_EFFORT = 'medium'
const settingsService = new SettingsService() const settingsService = new SettingsService()
const providerService = new ProviderService() const providerService = new ProviderService()

View File

@ -64,6 +64,33 @@ export class ConversationStartupError extends Error {
export class ConversationService { export class ConversationService {
private sessions = new Map<string, SessionProcess>() private sessions = new Map<string, SessionProcess>()
private buildSessionCliArgs(
sessionId: string,
sdkUrl: string,
shouldResume: boolean,
options?: SessionStartOptions,
): string[] {
const dangerousMode = process.env.CLAUDE_DANGEROUS_MODE === '1'
return this.resolveCliArgs([
'--print',
'--verbose',
'--sdk-url',
sdkUrl,
'--enable-auth-status',
'--input-format',
'stream-json',
'--output-format',
'stream-json',
// Desktop chat depends on partial assistant deltas; without this the
// server only sees the completed assistant message at turn end.
'--include-partial-messages',
...(shouldResume ? ['--resume', sessionId] : ['--session-id', sessionId]),
'--replay-user-messages',
...this.getRuntimeArgs(options),
...this.getPermissionArgs(options?.permissionMode, dangerousMode),
])
}
async startSession( async startSession(
sessionId: string, sessionId: string,
workDir: string, workDir: string,
@ -88,22 +115,12 @@ export class ConversationService {
) )
} }
const dangerousMode = process.env.CLAUDE_DANGEROUS_MODE === '1' const args = this.buildSessionCliArgs(
const args = this.resolveCliArgs([ sessionId,
'--print',
'--verbose',
'--sdk-url',
sdkUrl, sdkUrl,
'--enable-auth-status', shouldResume,
'--input-format', options,
'stream-json', )
'--output-format',
'stream-json',
...(shouldResume ? ['--resume', sessionId] : ['--session-id', sessionId]),
'--replay-user-messages',
...this.getRuntimeArgs(options),
...this.getPermissionArgs(options?.permissionMode, dangerousMode),
])
console.log( console.log(
`[ConversationService] Starting CLI for ${sessionId}, cwd: ${workDir} (process.cwd()=${process.cwd()}, CALLER_DIR will be pinned to workDir)`, `[ConversationService] Starting CLI for ${sessionId}, cwd: ${workDir} (process.cwd()=${process.cwd()}, CALLER_DIR will be pinned to workDir)`,