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.
@@ -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-nkG0O5n8.js"></script>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentquad",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "AgentQuad — local four-quadrant AI task scheduler with embedded Claude Code / Codex terminals",
5
5
  "license": "MIT",
6
6
  "author": "LIUZHENHUA521",
@@ -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 }
@@ -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
- const extractSource = promptText || session.recentOutput || ''
203
- // 主源 recentOutput 是 4KB 滑窗,TUI redraw 抖动会冲掉真实 prompt 文本;
204
- // 兜底用 outputHistory(最大 5MB)的尾部 ~64KB,让 extractor 能找到锚点。
205
- let historicalRaw = null
206
- if (!promptText && Array.isArray(session.outputHistory) && session.outputHistory.length > 0) {
207
- const joined = session.outputHistory.join('')
208
- historicalRaw = joined.length > 65536 ? joined.slice(-65536) : joined
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 viewer 进入时:清掉历史 scrollback 并跳过 replay。
868
- // scrollback 多半是窄 cols(如默认 80、Dock 卡片宽度)状态下 PTY 写下的硬换行,
869
- // replay 到全屏视图就是"行间短文字 + 右侧大片空白"的乱码观感。
870
- // 真正的对话历史在 Conversation tab 由 jsonl 还原,不依赖这里的 PTY scrollback。
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 }))