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) => ({
permissionMode: 'default',
currentModel: null,
effortLevel: 'high',
effortLevel: 'medium',
availableModels: [],
activeProviderName: null,
locale: getStoredLocale(),

View File

@ -2723,16 +2723,30 @@ async function run(): Promise<CommanderCommand> {
}));
}, configs).catch(err => logForDebugging(`[MCP] ${label} connect error: ${err}`));
};
// Await all MCP configs — print mode is often single-turn, so
// "late-connecting servers visible next turn" doesn't help. SDK init
// message and turn-1 tool list both need configured MCP tools present.
// Await all MCP configs for regular print-mode sessions — they're
// often single-turn, so "late-connecting servers visible next turn"
// 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.
// Connectors parallelize inside getMcpToolsCommandsAndResources
// (processBatched with Promise.all). claude.ai is awaited too — its
// fetch was kicked off early (line ~2558) so only residual time blocks
// here. --bare skips claude.ai entirely for perf-sensitive scripts.
profileCheckpoint('before_connectMcp');
await connectMcpBatch(regularMcpConfigs, 'regular');
if (sdkUrl) {
void connectMcpBatch(regularMcpConfigs, 'regular');
} else {
await connectMcpBatch(regularMcpConfigs, 'regular');
}
profileCheckpoint('after_connectMcp');
// Dedup: suppress plugin MCP servers that duplicate a claude.ai
// 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'))
}
})
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 DEFAULT_MODEL = 'claude-opus-4-7'
const DEFAULT_EFFORT = 'max'
const DEFAULT_EFFORT = 'medium'
const settingsService = new SettingsService()
const providerService = new ProviderService()

View File

@ -64,6 +64,33 @@ export class ConversationStartupError extends Error {
export class ConversationService {
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(
sessionId: string,
workDir: string,
@ -88,22 +115,12 @@ export class ConversationService {
)
}
const dangerousMode = process.env.CLAUDE_DANGEROUS_MODE === '1'
const args = this.resolveCliArgs([
'--print',
'--verbose',
'--sdk-url',
const args = this.buildSessionCliArgs(
sessionId,
sdkUrl,
'--enable-auth-status',
'--input-format',
'stream-json',
'--output-format',
'stream-json',
...(shouldResume ? ['--resume', sessionId] : ['--session-id', sessionId]),
'--replay-user-messages',
...this.getRuntimeArgs(options),
...this.getPermissionArgs(options?.permissionMode, dangerousMode),
])
shouldResume,
options,
)
console.log(
`[ConversationService] Starting CLI for ${sessionId}, cwd: ${workDir} (process.cwd()=${process.cwd()}, CALLER_DIR will be pinned to workDir)`,