✨ 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

+39 -15
View File
@@ -8,9 +8,11 @@ import { resolveProfilePath } from '../../cc/profiles'
import { spawnClaude } from '../../cc/spawnClaude'
import { findEnvFiles, lockEnvFiles, unlockEnvFiles } from '../../files/envLock'
import { checkBranchSync, runBranchSync } from '../../git/branchSync'
import { missingGitIdentityKeys, readGitIdentity } from '../../git/identity'
import { logger } from '../../logging/logger'
import { getEffectiveCommitProfilePath, getSettings } from '../../settings/store'
import { makeNonce } from '../KanbanPanel'
import { dismiss, toast } from './handoffFlow'
/**
* Minimal structural typing for the bits of the built-in `vscode.git`
@@ -86,43 +88,65 @@ export async function handleCommitRun(panel: KanbanWebviewPanel): Promise<void>
return
}
// 身份缺失时 git commit 必然失败,提前拦下,别让 claude 白跑十几分钟
const missingIdentity = missingGitIdentityKeys(await readGitIdentity(workspaceRoot))
if (missingIdentity.length > 0) {
toast(
panel,
'error',
`git 未配置 ${missingIdentity.join(' 和 ')},无法提交。请先执行:git config --global user.name "你的名字" && git config --global user.email "你的邮箱"`,
{ dismissOnTimer: 15000 },
)
return
}
panel.commitRunning = true
panel.postMessage({ type: 'commit/state', running: true })
// 同一个 id 反复发 toast 会原地替换,正好用来滚动显示实时进度
const progressToastId = toast(panel, 'info', '提交中:正在启动 claude…', {
spinner: true,
dismissOnTimer: 31 * 60 * 1000,
})
try {
// claude -p 提交流程可能跑得比较久(要 git add / 编 commit message / git
// commit),所以给一个比较宽松的超时(30 min)。
await spawnClaude({
const { resultText } = await spawnClaude({
prompt: '提交下代码',
cwd: workspaceRoot,
profilePath: resolution.profilePath,
timeoutMs: 30 * 60 * 1000,
bare: true,
onProgress: (label) => {
toast(panel, 'info', `提交中:${label}`, {
id: progressToastId,
spinner: true,
dismissOnTimer: 31 * 60 * 1000,
})
},
})
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'success',
message: '提交完成',
dismissOnTimer: 5000,
})
toast(panel, 'success', formatCommitDone(resultText), { dismissOnTimer: 5000 })
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err)
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: `提交失败: ${msg}`,
dismissOnTimer: 8000,
})
toast(panel, 'error', `提交失败: ${msg}`, { dismissOnTimer: 8000 })
}
finally {
dismiss(panel, progressToastId)
panel.commitRunning = false
panel.postMessage({ type: 'commit/state', running: false })
}
}
function formatCommitDone(resultText: string): string {
const firstLine = resultText.split('\n').map(l => l.trim()).find(l => l.length > 0) ?? ''
if (!firstLine)
return '提交完成'
const summary = firstLine.length > 100 ? `${firstLine.slice(0, 100)}…` : firstLine
return `提交完成:${summary}`
}
export async function handleBranchSyncCheck(panel: KanbanWebviewPanel): Promise<void> {
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
const s = getSettings(panel.context)