✨ 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:
1 parent
443a8e472a
commit
73a0c46f9f
6 files changed
+322
-25
No files matched your search
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 把 `claude -p --output-format stream-json --verbose` 吐出的每行 NDJSON
|
||||
* 压缩成人能看懂的进度短语,供长任务(如提交代码)实时展示。
|
||||
*
|
||||
* 只关心 assistant 消息里的 `tool_use` 和 `text` 块,其它行(system / user /
|
||||
* result / thinking 块 / 坏 JSON)一律返回空数组,调用方无需再做判断。
|
||||
*/
|
||||
|
||||
import * as path from 'node:path'
|
||||
|
||||
const LABEL_MAX_CHARS = 60
|
||||
|
||||
interface ContentBlock {
|
||||
type?: unknown
|
||||
text?: unknown
|
||||
name?: unknown
|
||||
input?: unknown
|
||||
}
|
||||
|
||||
function truncate(text: string): string {
|
||||
if (text.length <= LABEL_MAX_CHARS)
|
||||
return text
|
||||
return `${text.slice(0, LABEL_MAX_CHARS)}…`
|
||||
}
|
||||
|
||||
function asString(value: unknown): string {
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
function describeToolUse(name: string, input: Record<string, unknown>): string {
|
||||
switch (name) {
|
||||
case 'Bash':
|
||||
return `执行命令:${truncate(asString(input.description) || asString(input.command))}`
|
||||
case 'Read':
|
||||
return `读取 ${path.basename(asString(input.file_path))}`
|
||||
case 'Edit':
|
||||
case 'Write':
|
||||
case 'MultiEdit':
|
||||
return `修改 ${path.basename(asString(input.file_path))}`
|
||||
case 'Grep':
|
||||
case 'Glob':
|
||||
return `搜索 ${truncate(asString(input.pattern))}`
|
||||
default:
|
||||
return `调用 ${name}`
|
||||
}
|
||||
}
|
||||
|
||||
function describeBlock(block: ContentBlock): string | null {
|
||||
if (block.type === 'tool_use' && typeof block.name === 'string') {
|
||||
const input = block.input && typeof block.input === 'object'
|
||||
? block.input as Record<string, unknown>
|
||||
: {}
|
||||
return describeToolUse(block.name, input)
|
||||
}
|
||||
if (block.type === 'text' && typeof block.text === 'string') {
|
||||
const firstLine = block.text.split('\n').map(l => l.trim()).find(l => l.length > 0)
|
||||
return firstLine ? truncate(firstLine) : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function summarizeClaudeStreamLine(line: string): string[] {
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(line)
|
||||
}
|
||||
catch {
|
||||
return []
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object')
|
||||
return []
|
||||
const obj = parsed as { type?: unknown, message?: { content?: unknown } }
|
||||
if (obj.type !== 'assistant' || !Array.isArray(obj.message?.content))
|
||||
return []
|
||||
|
||||
const labels: string[] = []
|
||||
for (const block of obj.message.content as ContentBlock[]) {
|
||||
if (!block || typeof block !== 'object')
|
||||
continue
|
||||
const label = describeBlock(block)
|
||||
if (label)
|
||||
labels.push(label)
|
||||
}
|
||||
return labels
|
||||
}
|
||||
Reference in new issue
Block a user