agentquad 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist-web/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>AgentQuad</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DuZ_lMdf.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-CEiuiF0m.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
package/src/claude-transcript.js
CHANGED
|
@@ -200,4 +200,54 @@ export function buildFullTranscript(jsonlPath, opts = {}) {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* 找最近一个"还没拿到 tool_result 的 tool_use"。
|
|
205
|
+
*
|
|
206
|
+
* 用途:Claude Code Notification fire 时,jsonl 末尾通常已经写了 assistant 的
|
|
207
|
+
* `tool_use` 块(比如 Bash 命令),但还没拿到 `tool_result`(用户没批准之前
|
|
208
|
+
* Claude Code 不会 invoke 工具)。把这一块直接读出来,比从 PTY redraw 噪声
|
|
209
|
+
* 里抽要干净一万倍——前端 PermissionCard 直接拿来展示要授权的是哪个命令。
|
|
210
|
+
*
|
|
211
|
+
* 返回 { id, name, input, timestamp } 或 null。
|
|
212
|
+
* 算法:从 jsonl 末尾向前扫,先收集所有已经回收的 tool_use_id;遇到第一个
|
|
213
|
+
* "id 不在已回收集合里"的 tool_use 即返回。
|
|
214
|
+
*/
|
|
215
|
+
export function findLatestPendingToolUse(jsonlPath) {
|
|
216
|
+
const lines = readJsonlLines(jsonlPath)
|
|
217
|
+
if (lines.length === 0) return null
|
|
218
|
+
const resolvedIds = new Set()
|
|
219
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
220
|
+
const obj = parseJsonlLine(lines[i])
|
|
221
|
+
if (!obj) continue
|
|
222
|
+
if (obj.isMeta || obj.isSidechain) continue
|
|
223
|
+
const content = normalizeContent(obj.message?.content)
|
|
224
|
+
if (content.length === 0) continue
|
|
225
|
+
|
|
226
|
+
if (obj.type === 'user') {
|
|
227
|
+
for (const block of content) {
|
|
228
|
+
if (block?.type === 'tool_result' && block.tool_use_id) {
|
|
229
|
+
resolvedIds.add(block.tool_use_id)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
if (obj.type !== 'assistant') continue
|
|
235
|
+
|
|
236
|
+
// assistant 块里可能多个 tool_use(并发工具)—— 取末尾(最新)那一个是
|
|
237
|
+
// pending 的就返回。
|
|
238
|
+
for (let j = content.length - 1; j >= 0; j--) {
|
|
239
|
+
const block = content[j]
|
|
240
|
+
if (block?.type !== 'tool_use' || !block.id) continue
|
|
241
|
+
if (resolvedIds.has(block.id)) continue
|
|
242
|
+
return {
|
|
243
|
+
id: block.id,
|
|
244
|
+
name: block.name || 'tool',
|
|
245
|
+
input: block.input || {},
|
|
246
|
+
timestamp: obj.timestamp || null,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return null
|
|
251
|
+
}
|
|
252
|
+
|
|
203
253
|
export const __test__ = { normalizeContent, blockToText, parseJsonlLine }
|
package/src/permission-prompt.js
CHANGED
|
@@ -160,6 +160,50 @@ function takeWindow(lines, maxLines) {
|
|
|
160
160
|
*
|
|
161
161
|
* 返回 { text, options };text 不超过 maxChars,options 默认 maxLines=30。
|
|
162
162
|
*/
|
|
163
|
+
/**
|
|
164
|
+
* 把 jsonl 里的 pending tool_use 块渲染成 PermissionCard 要显示的 prompt 文本。
|
|
165
|
+
*
|
|
166
|
+
* Claude Code 的工具有十几种,这里只把"用户最关心的字段"挑出来:
|
|
167
|
+
* Bash → input.command(完整命令,最多 1200 字)
|
|
168
|
+
* Edit/Write → input.file_path
|
|
169
|
+
* Read → input.file_path
|
|
170
|
+
* Glob/Grep → input.pattern / input.glob_pattern
|
|
171
|
+
* WebFetch → input.url
|
|
172
|
+
* 其它 → JSON.stringify(input)
|
|
173
|
+
* + 如果 input.description 存在,补一行说明。
|
|
174
|
+
*/
|
|
175
|
+
export function formatToolUseAsPrompt(toolUse, { maxChars = 1200 } = {}) {
|
|
176
|
+
if (!toolUse || typeof toolUse !== 'object') return ''
|
|
177
|
+
const name = String(toolUse.name || 'tool')
|
|
178
|
+
const input = toolUse.input || {}
|
|
179
|
+
let body = ''
|
|
180
|
+
if (typeof input.command === 'string') body = input.command
|
|
181
|
+
else if (typeof input.cmd === 'string') body = input.cmd
|
|
182
|
+
else if (typeof input.file_path === 'string') body = input.file_path
|
|
183
|
+
else if (typeof input.path === 'string') body = input.path
|
|
184
|
+
else if (typeof input.url === 'string') body = input.url
|
|
185
|
+
else if (typeof input.pattern === 'string') body = input.pattern
|
|
186
|
+
else if (typeof input.glob_pattern === 'string') body = input.glob_pattern
|
|
187
|
+
else if (typeof input.query === 'string') body = input.query
|
|
188
|
+
else {
|
|
189
|
+
try { body = JSON.stringify(input, null, 2) } catch { body = String(input) }
|
|
190
|
+
}
|
|
191
|
+
if (body.length > maxChars) body = body.slice(0, maxChars) + ' …(truncated)'
|
|
192
|
+
const desc = typeof input.description === 'string' && input.description.trim()
|
|
193
|
+
? `\n\n${input.description.trim()}`
|
|
194
|
+
: ''
|
|
195
|
+
return `${name}:\n${body}${desc}`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Claude Code 标准 3 选项授权弹窗。当我们从 jsonl 拿到 pending tool_use 时,
|
|
199
|
+
// 选项是固定的——不必再去 PTY 里猜。前端按这三项渲染。
|
|
200
|
+
// 文案保持英文原样,与 TUI 一致,方便用户对照终端确认。
|
|
201
|
+
export const CLAUDE_DEFAULT_PERMISSION_OPTIONS = [
|
|
202
|
+
{ index: 1, label: 'Yes' },
|
|
203
|
+
{ index: 2, label: "Yes, and don't ask again this session" },
|
|
204
|
+
{ index: 3, label: 'No, and tell Claude what to do differently' },
|
|
205
|
+
]
|
|
206
|
+
|
|
163
207
|
export function extractPermissionPrompt(
|
|
164
208
|
raw,
|
|
165
209
|
{ historicalRaw = null, maxLines = 30, maxChars = 1200 } = {},
|
|
@@ -7,7 +7,8 @@ import { homedir } from 'node:os'
|
|
|
7
7
|
import pidusage from 'pidusage'
|
|
8
8
|
import { loadConfig, resolveToolsConfig, SUPPORTED_TOOLS, DEFAULT_ROOT_DIR } from '../config.js'
|
|
9
9
|
import { writeRuntimeMcpConfig } from '../agent-installer-shared.js'
|
|
10
|
-
import { extractPermissionPrompt } from '../permission-prompt.js'
|
|
10
|
+
import { CLAUDE_DEFAULT_PERMISSION_OPTIONS, extractPermissionPrompt, formatToolUseAsPrompt } from '../permission-prompt.js'
|
|
11
|
+
import { findLatestPendingToolUse } from '../claude-transcript.js'
|
|
11
12
|
|
|
12
13
|
const MAX_OUTPUT_BUFFER = 5 * 1024 * 1024
|
|
13
14
|
const CLEANUP_MS = 30 * 60_000
|
|
@@ -199,15 +200,38 @@ export function createAiTerminal({ db, pty, logDir, defaultCwd, getDefaultCwd, o
|
|
|
199
200
|
const wasPending = session.status === 'pending_confirm'
|
|
200
201
|
if (!wasPending && session.status !== 'running') return false
|
|
201
202
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
let text = ''
|
|
204
|
+
let options = []
|
|
205
|
+
|
|
206
|
+
// Claude 优先走 jsonl 路径:Notification fire 时 jsonl 末尾通常已经写好了
|
|
207
|
+
// pending 的 tool_use 块(Bash 命令、Edit 文件 path 等),结构化、无 ANSI 噪声。
|
|
208
|
+
if (!promptText && session.tool === 'claude' && session.nativeSessionId && pty?.findClaudeSession) {
|
|
209
|
+
try {
|
|
210
|
+
const loc = pty.findClaudeSession(session.nativeSessionId)
|
|
211
|
+
if (loc?.filePath) {
|
|
212
|
+
const toolUse = findLatestPendingToolUse(loc.filePath)
|
|
213
|
+
if (toolUse) {
|
|
214
|
+
text = formatToolUseAsPrompt(toolUse)
|
|
215
|
+
options = CLAUDE_DEFAULT_PERMISSION_OPTIONS
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch { /* ignore — 走 PTY 兜底 */ }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 兜底:从 PTY 提取(Codex 主路径 / Claude jsonl 拿不到时的 backup)。
|
|
222
|
+
// recentOutput 是 4KB 滑窗,TUI redraw 抖动会冲掉真实 prompt 文本;
|
|
223
|
+
// 再兜底用 outputHistory(最大 5MB)的尾部 ~64KB,让 extractor 能找到锚点。
|
|
224
|
+
if (!text) {
|
|
225
|
+
const extractSource = promptText || session.recentOutput || ''
|
|
226
|
+
let historicalRaw = null
|
|
227
|
+
if (!promptText && Array.isArray(session.outputHistory) && session.outputHistory.length > 0) {
|
|
228
|
+
const joined = session.outputHistory.join('')
|
|
229
|
+
historicalRaw = joined.length > 65536 ? joined.slice(-65536) : joined
|
|
230
|
+
}
|
|
231
|
+
const r = extractPermissionPrompt(extractSource, { historicalRaw })
|
|
232
|
+
text = r.text
|
|
233
|
+
options = r.options
|
|
209
234
|
}
|
|
210
|
-
const { text, options } = extractPermissionPrompt(extractSource, { historicalRaw })
|
|
211
235
|
const hasContent = !!(text || options.length)
|
|
212
236
|
const prevPrompt = session.permissionPrompt || null
|
|
213
237
|
|
|
@@ -864,14 +888,10 @@ export function createAiTerminal({ db, pty, logDir, defaultCwd, getDefaultCwd, o
|
|
|
864
888
|
const effectiveRole = role === 'primary' ? 'primary' : 'secondary'
|
|
865
889
|
if (ws) ws.__quadtodoRole = effectiveRole
|
|
866
890
|
session.browsers.add(ws)
|
|
867
|
-
// primary
|
|
868
|
-
//
|
|
869
|
-
// replay
|
|
870
|
-
|
|
871
|
-
if (effectiveRole === 'primary') {
|
|
872
|
-
session.outputHistory = []
|
|
873
|
-
session.outputSize = 0
|
|
874
|
-
} else if (session.outputHistory.length > 0) {
|
|
891
|
+
// 不分 primary / secondary,都回放——否则 reopen SessionFocus 会看到一片空白。
|
|
892
|
+
// 旧顾虑是"窄 cols 时代 scrollback 在宽 viewer 里重排乱码";现在交给 init/resize 后
|
|
893
|
+
// TUI 的 SIGWINCH 自重绘兜底,replay 内容沉到 scrollback 上面、用户可滚回去看。
|
|
894
|
+
if (session.outputHistory.length > 0) {
|
|
875
895
|
ws.send(JSON.stringify({ type: 'replay', chunks: session.outputHistory }))
|
|
876
896
|
}
|
|
877
897
|
ws.send(JSON.stringify({ type: 'auto_mode', autoMode: session.autoMode || null }))
|