✨ feat(vscode): 提交代码按钮实时显示进度并预检 git 身份

- spawnClaude 新增 onProgress 回调,给了回调就改走 stream-json 逐行输出,按行解析出工具调用 / 回复文本
- 新增 claudeProgress 把每行 NDJSON 压成可读进度短语
- 提交前先读 git user.name / user.email,缺失直接提示,不再让 claude 白跑
- 运行期间用带转圈的 toast 滚动显示进度,完成后带上 claude 的结果摘要

Claude-Session: https://claude.ai/code/session_01HTMgwaE3woqM5yvsHiuSAa
This commit is contained in:
cruldra committed 2026-08-31 22:11:09 +08:00
1 parent 443a8e472a
commit 73a0c46f9f
6 files changed
+322 -25

No files matched your search

+45 -10
View File
@@ -1,16 +1,18 @@
/**
* Typed wrapper around the `claude` CLI in `-p` (one-shot) mode with
* `--output-format json`. We rely on `claude` being available on PATH;
* callers should surface the install-hint error message to the user when
* ENOENT is raised.
* Typed wrapper around the `claude` CLI in `-p` (one-shot) mode. We rely on
* `claude` being available on PATH; callers should surface the install-hint
* error message to the user when ENOENT is raised.
*
* Two execution paths share the same output-parsing logic:
*
* - **Text-only** (no images): plain `claude -p "<prompt>" --output-format json`
* via `spawn` with stdin ignored so the CLI does not wait for piped input.
* When the caller passes `onProgress`, this path switches to
* `--output-format stream-json --verbose` so tool calls can be reported
* live instead of only when the process exits.
*
* - **With images**: switch to `claude -p --input-format stream-json
* --output-format json` via `spawn`, then feed a single NDJSON line on
* --output-format stream-json` via `spawn`, then feed a single NDJSON line on
* stdin containing the user message with text + image content blocks
* (Anthropic Messages API shape). The official docs state single-message
* input does NOT support image attachments — stream-json is the only
@@ -21,6 +23,7 @@
import { spawn } from 'node:child_process'
import * as path from 'node:path'
import { resolveClaudeBin } from './claudeBin'
import { summarizeClaudeStreamLine } from './claudeProgress'
const DEFAULT_TIMEOUT_MS = 300_000
const MAX_BUFFER_BYTES = 10 * 1024 * 1024
@@ -177,6 +180,27 @@ function buildClaudeExitDiagnostic(stderr: string, stdout: string): string {
return extractStdoutDiagnostic(stdout) ?? '(无 stderr/stdout)'
}
export type ClaudeProgressListener = (label: string) => void
/**
* 按行切 stdout:stream-json 每行一个对象,但 data 事件的 chunk 边界和行边界
* 无关,一行常被拆进两个 chunk,所以末尾那段不完整的残行要留到下次拼上。
*/
function createProgressFeeder(onProgress?: ClaudeProgressListener): (chunk: string) => void {
if (!onProgress)
return () => {}
let pending = ''
return (chunk) => {
pending += chunk
const lines = pending.split('\n')
pending = lines.pop() ?? ''
for (const line of lines) {
for (const label of summarizeClaudeStreamLine(line))
onProgress(label)
}
}
}
function buildStreamJsonLine(prompt: string, images: ClaudeImage[]): string {
const content: Array<Record<string, unknown>> = [
{ type: 'text', text: prompt },
@@ -246,14 +270,16 @@ export async function spawnClaude(opts: {
bare?: boolean
/** 续会话:传入则 `--resume <id>`,让本次 `-p` 接在该会话之后(复用上下文,省 token)。 */
resumeSessionId?: string
/** 实时进度:每识别出一个工具调用 / 一句回复就回调一次可读标签。 */
onProgress?: ClaudeProgressListener
}): Promise<ClaudeResult> {
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
const hasImages = !!opts.images && opts.images.length > 0
if (!hasImages) {
return spawnClaudeText(opts.prompt, opts.cwd, timeoutMs, opts.profilePath, opts.bare, opts.resumeSessionId)
return spawnClaudeText(opts.prompt, opts.cwd, timeoutMs, opts.profilePath, opts.bare, opts.resumeSessionId, opts.onProgress)
}
return spawnClaudeStreamed(opts.prompt, opts.cwd, timeoutMs, opts.images!, opts.profilePath, opts.bare, opts.resumeSessionId)
return spawnClaudeStreamed(opts.prompt, opts.cwd, timeoutMs, opts.images!, opts.profilePath, opts.bare, opts.resumeSessionId, opts.onProgress)
}
function spawnClaudeText(
@@ -263,7 +289,9 @@ function spawnClaudeText(
profilePath?: string,
bare?: boolean,
resumeSessionId?: string,
onProgress?: ClaudeProgressListener,
): Promise<ClaudeResult> {
// `--output-format json` 要到进程结束才有输出;要报进度就得换成逐行吐的 stream-json
const args = [
...(bare ? ['--bare'] : []),
'--dangerously-skip-permissions',
@@ -272,7 +300,7 @@ function spawnClaudeText(
'-p',
prompt,
'--output-format',
'json',
...(onProgress ? ['stream-json', '--verbose'] : ['json']),
]
const bin = resolveClaudeBin()
@@ -284,6 +312,7 @@ function spawnClaudeText(
let stderr = ''
let stdoutBytes = 0
let settled = false
const feedProgress = createProgressFeeder(onProgress)
const child = spawn(bin, args, {
cwd,
@@ -332,7 +361,9 @@ function spawnClaudeText(
))
return
}
stdout += chunk.toString('utf8')
const text = chunk.toString('utf8')
stdout += text
feedProgress(text)
})
child.stderr.on('data', (chunk: Buffer) => {
@@ -377,6 +408,7 @@ function spawnClaudeStreamed(
profilePath?: string,
bare?: boolean,
resumeSessionId?: string,
onProgress?: ClaudeProgressListener,
): Promise<ClaudeResult> {
// Local Claude Code v2.1.143 enforces that --input-format=stream-json must
// be paired with --output-format=stream-json (the friendlier `--output-format
@@ -405,6 +437,7 @@ function spawnClaudeStreamed(
let stderr = ''
let stdoutBytes = 0
let settled = false
const feedProgress = createProgressFeeder(onProgress)
const child = spawn(bin, args, {
cwd,
@@ -453,7 +486,9 @@ function spawnClaudeStreamed(
))
return
}
stdout += chunk.toString('utf8')
const text = chunk.toString('utf8')
stdout += text
feedProgress(text)
})
child.stderr.on('data', (chunk: Buffer) => {