@bolloon/bolloon-agent 0.6.4 → 0.6.6

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.
Files changed (173) hide show
  1. package/dist/agents/chain/chain-settlement.js +0 -1
  2. package/dist/agents/chat-segmenter.js +4 -16
  3. package/dist/agents/contacts/human-directory.js +155 -0
  4. package/dist/agents/contacts/tools.js +79 -0
  5. package/dist/agents/experience-review.js +0 -3
  6. package/dist/agents/goal-flywheel/goal-change.js +3 -4
  7. package/dist/agents/goal-resume.js +0 -4
  8. package/dist/agents/intent-classifier.js +1 -1
  9. package/dist/agents/iteration-budget.js +12 -0
  10. package/dist/agents/knowledge-organizer.js +0 -2
  11. package/dist/agents/lesson-to-skill.js +0 -1
  12. package/dist/agents/loop-review.js +2 -2
  13. package/dist/agents/memory-store.js +359 -0
  14. package/dist/agents/opportunity-inbox.js +147 -0
  15. package/dist/agents/opportunity-match.js +48 -7
  16. package/dist/agents/opportunity-web-search.js +222 -0
  17. package/dist/agents/p2p-chat-tools.js +1 -2
  18. package/dist/agents/parse-tool-call.js +9 -22
  19. package/dist/agents/pi-harness.js +1 -2
  20. package/dist/agents/pi-sdk-tools.js +180 -5
  21. package/dist/agents/pi-sdk.js +143 -9
  22. package/dist/agents/process-runner.js +28 -9
  23. package/dist/agents/react-loop.js +9 -17
  24. package/dist/agents/runner-resolver.js +4 -3
  25. package/dist/agents/skill-loader.js +22 -15
  26. package/dist/agents/skills-manager.js +1 -25
  27. package/dist/agents/workflow-pivot-loop.js +152 -61
  28. package/dist/agents/world-watcher.js +30 -1
  29. package/dist/bootstrap/context-collector.js +2 -4
  30. package/dist/bootstrap/context-hierarchy.js +13 -1
  31. package/dist/cli/ink-app.js +44 -17
  32. package/dist/cli/intent-command.js +22 -12
  33. package/dist/cli/loading-tui.js +8 -2
  34. package/dist/cli/log-gate.js +40 -8
  35. package/dist/cli/mention-data.js +1 -0
  36. package/dist/cli/reply-hygiene.js +22 -0
  37. package/dist/cli/trace-line.js +61 -2
  38. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  39. package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -1
  40. package/dist/electron-build/agents/chain/chain-settlement.js +0 -1
  41. package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -1
  42. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  43. package/dist/electron-build/agents/contacts/human-directory.js +204 -0
  44. package/dist/electron-build/agents/contacts/human-directory.js.map +1 -0
  45. package/dist/electron-build/agents/contacts/policy.js.map +1 -1
  46. package/dist/electron-build/agents/contacts/tools.js +112 -0
  47. package/dist/electron-build/agents/contacts/tools.js.map +1 -1
  48. package/dist/electron-build/agents/experience-review.js +0 -3
  49. package/dist/electron-build/agents/experience-review.js.map +1 -1
  50. package/dist/electron-build/agents/external-events.js.map +1 -1
  51. package/dist/electron-build/agents/goal-flywheel/goal-change.js +3 -4
  52. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -1
  53. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -1
  54. package/dist/electron-build/agents/goal-resume.js +0 -4
  55. package/dist/electron-build/agents/goal-resume.js.map +1 -1
  56. package/dist/electron-build/agents/intent-classifier.js +1 -1
  57. package/dist/electron-build/agents/lesson-to-skill.js +0 -1
  58. package/dist/electron-build/agents/lesson-to-skill.js.map +1 -1
  59. package/dist/electron-build/agents/loop-review.js +2 -2
  60. package/dist/electron-build/agents/loop-review.js.map +1 -1
  61. package/dist/electron-build/agents/memory-recall.js.map +1 -1
  62. package/dist/electron-build/agents/memory-store.js +404 -0
  63. package/dist/electron-build/agents/memory-store.js.map +1 -0
  64. package/dist/electron-build/agents/parse-tool-call.js +9 -22
  65. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  66. package/dist/electron-build/agents/pi-harness.js +1 -2
  67. package/dist/electron-build/agents/pi-harness.js.map +1 -1
  68. package/dist/electron-build/agents/pi-sdk-tools.js +180 -5
  69. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  70. package/dist/electron-build/agents/pi-sdk.js +143 -9
  71. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  72. package/dist/electron-build/agents/pre-tool-validator.js.map +1 -1
  73. package/dist/electron-build/agents/process-runner.js +28 -9
  74. package/dist/electron-build/agents/process-runner.js.map +1 -1
  75. package/dist/electron-build/agents/runner-resolver.js +4 -3
  76. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  77. package/dist/electron-build/agents/skill-loader.js +22 -16
  78. package/dist/electron-build/agents/skill-loader.js.map +1 -1
  79. package/dist/electron-build/agents/skill-supervisor-link.js.map +1 -1
  80. package/dist/electron-build/agents/skills-manager.js +0 -24
  81. package/dist/electron-build/agents/skills-manager.js.map +1 -1
  82. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  83. package/dist/electron-build/agents/workflow-engine.js.map +1 -1
  84. package/dist/electron-build/agents/workflow-pivot-loop.js +155 -62
  85. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  86. package/dist/electron-build/agents/x402/goal-run-bridge.js.map +1 -1
  87. package/dist/electron-build/bollharness-integration/gate-state-machine.js.map +1 -1
  88. package/dist/electron-build/bootstrap/context-collector.js +2 -4
  89. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  90. package/dist/electron-build/bootstrap/context-hierarchy.js +13 -1
  91. package/dist/electron-build/bootstrap/context-hierarchy.js.map +1 -1
  92. package/dist/electron-build/electron-preload.js.map +1 -1
  93. package/dist/electron-build/external-engines/delegate.js.map +1 -1
  94. package/dist/electron-build/kernel/channel-actor.js +31 -2
  95. package/dist/electron-build/kernel/channel-actor.js.map +1 -1
  96. package/dist/electron-build/kernel/model-runtime.js +8 -1
  97. package/dist/electron-build/kernel/model-runtime.js.map +1 -1
  98. package/dist/electron-build/llm/config-store.js +20 -0
  99. package/dist/electron-build/llm/config-store.js.map +1 -1
  100. package/dist/electron-build/llm/embedding-adapter.js +75 -0
  101. package/dist/electron-build/llm/embedding-adapter.js.map +1 -0
  102. package/dist/electron-build/llm/pi-ai.js +96 -35
  103. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  104. package/dist/electron-build/network/agent-network.js.map +1 -1
  105. package/dist/electron-build/orbitdb/memory-sync.js +76 -0
  106. package/dist/electron-build/orbitdb/memory-sync.js.map +1 -0
  107. package/dist/electron-build/pi-ecosystem-judgment/adaptive-scan.js.map +1 -1
  108. package/dist/electron-build/security/tool-gate.js +2 -0
  109. package/dist/electron-build/security/tool-gate.js.map +1 -1
  110. package/dist/electron-build/setup/setup-store.js +34 -6
  111. package/dist/electron-build/setup/setup-store.js.map +1 -1
  112. package/dist/electron-build/social/heartbeat.js +18 -5
  113. package/dist/electron-build/social/heartbeat.js.map +1 -1
  114. package/dist/electron-build/utils/update-manager.js.map +1 -1
  115. package/dist/index.js +250 -12
  116. package/dist/ios/bolloon-web.json +3 -3
  117. package/dist/ios/client.js +150 -113
  118. package/dist/ios/icons/icon-1400x1400.png +0 -0
  119. package/dist/ios/icons/icon-1400x1400.webp +0 -0
  120. package/dist/ios/index.html +106 -107
  121. package/dist/ios/mobile-agent.js +346 -45
  122. package/dist/ios/mobile-core.js +7303 -4590
  123. package/dist/ios/mobile-harness.js +1004 -0
  124. package/dist/ios/mobile-orbitdb.js +194 -0
  125. package/dist/ios/mobile-persona.js +80 -0
  126. package/dist/ios/mobile-privacy.js +2 -3
  127. package/dist/ios/mobile.css +286 -21
  128. package/dist/ios/mobile.html +106 -107
  129. package/dist/ios/mobile.js +300 -132
  130. package/dist/ios/routes-world.js +58 -1
  131. package/dist/ios/server.js +3 -1
  132. package/dist/ios/style.css +49 -2
  133. package/dist/ios/sw.js +1 -1
  134. package/dist/ios/ui/message-renderer.js +4 -18
  135. package/dist/ios/ui/step-timeline.js +14 -1
  136. package/dist/kernel/channel-actor.js +30 -2
  137. package/dist/kernel/gate-scan.js +1 -1
  138. package/dist/kernel/index.js +33 -0
  139. package/dist/kernel/model-runtime.js +8 -1
  140. package/dist/kernel/plan.js +3 -3
  141. package/dist/kernel/roster.js +3 -3
  142. package/dist/llm/config-store.js +20 -0
  143. package/dist/llm/embedding-adapter.js +69 -0
  144. package/dist/llm/pi-ai.js +94 -34
  145. package/dist/orbitdb/memory-sync.js +70 -0
  146. package/dist/pi-ecosystem-judgment/adaptive-scan.js +1 -1
  147. package/dist/security/tool-gate.js +2 -0
  148. package/dist/setup/setup-store.js +34 -6
  149. package/dist/social/heartbeat.js +18 -5
  150. package/dist/web/bolloon-web.json +3 -3
  151. package/dist/web/client.js +150 -113
  152. package/dist/web/icons/icon-1400x1400.png +0 -0
  153. package/dist/web/icons/icon-1400x1400.webp +0 -0
  154. package/dist/web/index.html +52 -59
  155. package/dist/web/mobile-agent.js +351 -45
  156. package/dist/web/mobile-contacts.js +0 -4
  157. package/dist/web/mobile-core.js +7430 -4590
  158. package/dist/web/mobile-harness.js +1004 -0
  159. package/dist/web/mobile-orbitdb.js +194 -0
  160. package/dist/web/mobile-persona.js +80 -0
  161. package/dist/web/mobile-privacy.js +2 -3
  162. package/dist/web/mobile.css +286 -21
  163. package/dist/web/mobile.html +121 -107
  164. package/dist/web/mobile.js +409 -136
  165. package/dist/web/routes-contacts.js +94 -1
  166. package/dist/web/routes-world.js +58 -1
  167. package/dist/web/server.js +4 -2
  168. package/dist/web/style.css +49 -2
  169. package/dist/web/sw.js +1 -1
  170. package/dist/web/ui/message-renderer.js +4 -18
  171. package/dist/web/ui/step-timeline.js +14 -1
  172. package/package.json +3 -2
  173. package/scripts/build-web.ts +16 -1
@@ -56,6 +56,8 @@ export function canonicalizeToolCallArguments(argStr) {
56
56
  }
57
57
  /** 2026-08-11 (Hermes build_worker_context 全限幅): 工具输出进 LLM 上下文的上限 */
58
58
  export const TOOL_RESULT_MAX_CHARS = 12_000;
59
+ /** 2026-10-12: 单工具执行超时 (慢工具不再拖死整轮; 超时 → 失败结果 + 续跑提示给 LLM 下轮决策) */
60
+ export const TOOL_EXEC_TIMEOUT_MS = (typeof process !== 'undefined' && Number(process.env.BOLLOON_TOOL_TIMEOUT_MS)) || 120_000;
59
61
  /** 截断工具输出 (纯函数, 可单测): 超限 → 保留前 max + 可见省略标记 */
60
62
  export function truncateToolOutput(output, max = TOOL_RESULT_MAX_CHARS) {
61
63
  const s = output ?? '';
@@ -63,6 +65,34 @@ export function truncateToolOutput(output, max = TOOL_RESULT_MAX_CHARS) {
63
65
  return s;
64
66
  return s.slice(0, max) + `\n… [truncated, ${s.length - max} chars omitted]`;
65
67
  }
68
+ /** 2026-10-12 (leo: 「每个命令后面放一个命令执行时间的读秒」): 毫秒 → 人读耗时串 (纯函数, 可单测) */
69
+ export function formatDuration(ms) {
70
+ if (!Number.isFinite(ms) || ms < 0)
71
+ return '(耗时未知)';
72
+ if (ms < 1000)
73
+ return `${ms}ms`;
74
+ const s = ms / 1000;
75
+ if (s < 60)
76
+ return `${s.toFixed(1)}s`;
77
+ const m = Math.floor(s / 60);
78
+ const rem = Math.round(s % 60);
79
+ return `${m}m${rem}s`;
80
+ }
81
+ /**
82
+ * 2026-10-12 (leo: 「对于 terminal 执行的命令, 你可以提供一个压缩的摘要」):
83
+ * 工具输出进 UI 前的摘要 (纯函数, 可单测) —— 只压 `terminal`/`shell` 类长输出:
84
+ * 折叠空白、截断到 ~300 字符、保留头部 (命令结果的开头最有信息量)。
85
+ * 其它工具原样返回 (read_file 等短输出没必要压)。
86
+ */
87
+ export function summarizeToolOutput(toolName, output) {
88
+ const s = output ?? '';
89
+ if (!/terminal|shell|bash|exec/i.test(toolName))
90
+ return s;
91
+ const collapsed = s.replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
92
+ if (collapsed.length <= 300)
93
+ return collapsed;
94
+ return collapsed.slice(0, 300) + `\n… [摘要, 原输出 ${s.length} 字符]`;
95
+ }
66
96
  /**
67
97
  * Analyze input to determine task complexity
68
98
  */
@@ -386,24 +416,14 @@ export class WorkflowPivotLoop {
386
416
  const nativeTools = this.nativeToolCallsToDefinitions(llmResponse.toolCalls || []);
387
417
  const pendingTools = nativeTools.length > 0 ? nativeTools : this.extractPendingToolUses(reply);
388
418
  if (pendingTools.length === 0) {
389
- // 2026-07-06: LLM 显式 <final gen> 标记 — pivot 立即退出, 不再走 quality/iter 流程
390
- // 这个 marker 之前和 思考/ 同义被忽略, 害得 "你好" 类问题跑 11 iter 还不见停
391
- // 加上之后 iter=1 收到 <final gen> 就 accept
392
- if (/<final\s+gen\s*\/?>|<\/final\s+gen>/i.test(reply)) {
393
- const cleaned = reply.replace(/<final\s+gen\s*\/?>|<\/final\s+gen>/gi, '').trim();
394
- response = cleaned || reply;
395
- this.emit({
396
- type: 'status', internal: true, // K4-B: 循环遥测 = 内部运行日志 (口径同老 loop)
397
- content: `✅ 检测到 <final gen> 结束标记, 立即退出 (iter=${this.state.iteration})`,
398
- tool: 'system',
399
- });
400
- return this.createResult(true, response, 'final_gen_marker');
401
- }
419
+ // 2026-10-12 (leo 拍板: 全项目去掉 <final gen> 截止符):
420
+ // 旧实现这里检测 `<final gen>` 标记 → 立即 break 退出 —— 模型在中间轮次
421
+ // echo 出 "> <final gen>" 类文本 (思考引用/复述契约) 就被正则命中, 循环提前截止,
422
+ // 正是「中间生成了内容, 没办法衔接上就停止了」的根因之一。
423
+ // 现在循环停止只剩: 质量达标 / 连续无进展 / 用户中止 (leo 确认的三条路)。
402
424
  // Check if the reply contains tool call intent but couldn't be parsed
403
- // 2026-07-06: 排除合法的 思考/<final gen> 这些 tag (它们是 sentinel 不是 tool call)
404
425
  const cleanedForIntentCheck = reply
405
- .replace(/<final\s+gen\s*\/?>|<\/final\s+gen>/gi, '')
406
- .replace(/<\/?think(?:ing)?>/gi, ''); // 移除
426
+ .replace(/<\/?think(?:ing)?>/gi, ''); // 移除 think 块
407
427
  const containsToolCallIntent = cleanedForIntentCheck.includes('调用工具') || cleanedForIntentCheck.includes('tool(') ||
408
428
  cleanedForIntentCheck.includes('使用工具') || cleanedForIntentCheck.includes('需要获取') || cleanedForIntentCheck.includes('需要查看') ||
409
429
  cleanedForIntentCheck.includes('tool =>') || cleanedForIntentCheck.includes('[TOOL_CALL]') ||
@@ -421,6 +441,12 @@ export class WorkflowPivotLoop {
421
441
  }
422
442
  // No pending tool uses - this is a normal completion
423
443
  this.state.pendingToolUses = [];
444
+ // 2026-10-12 (leo: 「任务进行到一半就卡住了」): **0 工具轮也要把回复写进历史**。
445
+ // 旧代码只在工具执行分支 push assistant —— 纯回复轮 (模型给最终回答) 不进 history,
446
+ // 于是: ① 质量不达标被打回重跑时, 模型下一轮**看不到自己上轮说过什么**;
447
+ // ② 循环因无进展退出时, 兜底取 `lastAssistant?.content` 是**最后一次工具轮**的
448
+ // 空 content ⇒ 屏上 `│ │` 空框。现在: 每轮回复都进历史, 兜底能拿到真正的最终答复。
449
+ this.messageHistory.push({ role: 'assistant', content: reply });
424
450
  // Evaluate quality before accepting
425
451
  const quality = this.evaluateQuality(reply);
426
452
  this.state.qualityScores.push(quality);
@@ -429,32 +455,38 @@ export class WorkflowPivotLoop {
429
455
  content: `✅ 检测到最终回复 (质量: ${(quality * 10).toFixed(1)}/10)`,
430
456
  tool: 'system'
431
457
  });
432
- // Check if quality threshold met
433
- if (quality >= effectiveConfig.qualityThreshold) {
434
- response = reply;
435
- return this.createResult(true, reply, 'quality_threshold_met');
436
- }
437
- // Quality not met but no more tools to call
438
- // Accept response if we've done minimum iterations
439
- if (this.state.iteration >= effectiveConfig.minIterations) {
440
- response = reply;
441
- return this.createResult(true, reply, 'no_pending_tools');
458
+ // 2026-10-12 (leo: 「循环跑 1 次就截止, 要能多轮」): 质量未达标时旧代码在
459
+ // iter >= minIterations (默认 2) 就**直接收下** (no_pending_tools) —— 长任务
460
+ // 经常第 2 轮就给出不完整回答被收了, 正是「中间生成内容但衔接不上就停止」。
461
+ // 现在: 质量未达标一律**注入续跑提示 + 继续循环**, 由 maxConsecutiveNoProgress 兜底
462
+ // (真有进展的会持续往下, 空转的到阈值才停) —— 该停才停, 不是早停。
463
+ if (quality < effectiveConfig.qualityThreshold) {
464
+ this.pushContinuationHint(`上一轮回答质量评测 ${(quality * 10).toFixed(1)}/${(effectiveConfig.qualityThreshold * 10).toFixed(1)}, 未达完成标准 — 请对照用户需求检查是否还有未完成的子目标, 需要工具就继续调用, 全部完成再给出完整最终回答.`);
465
+ this.state.consecutiveNoProgress++;
466
+ this.emit({
467
+ type: 'status', internal: true, // K4-B: 循环遥测 = 内部运行日志 (口径同老 loop)
468
+ content: `📊 质量未达标 (${(quality * 10).toFixed(1)}/${(effectiveConfig.qualityThreshold * 10).toFixed(1)}),注入续跑提示,继续循环`,
469
+ tool: 'system'
470
+ });
471
+ continue;
442
472
  }
443
- // Too early, continue to see if we can improve
444
- this.state.consecutiveNoProgress++;
445
- this.emit({
446
- type: 'status', internal: true, // K4-B: 循环遥测 = 内部运行日志 (口径同老 loop)
447
- content: `📊 质量未达标 (${(quality * 10).toFixed(1)}/${(effectiveConfig.qualityThreshold * 10).toFixed(1)}),继续循环`,
448
- tool: 'system'
449
- });
450
- continue;
473
+ // Quality met — accept
474
+ response = reply;
475
+ return this.createResult(true, reply, 'quality_threshold_met');
451
476
  }
452
477
  // We have pending tool uses - execute them
453
478
  this.state.pendingToolUses = pendingTools;
454
479
  this.state.lastMeaningfulWork = this.state.iteration;
455
480
  this.state.consecutiveNoProgress = 0;
456
- for (const toolCall of pendingTools) {
481
+ // 2026-10-12 (leo: 「运行工具的时候没有在后台运行, 花了太久, 把时间给 pass 了」):
482
+ // 旧实现 for...of 串行 await + **无超时** —— 一个慢工具 (网络/重 IO) 拖住整轮,
483
+ // 后面的工具全被阻塞, 轮次预算被耗在等待上。
484
+ // 现在: ① 同一轮的多个工具**并行**执行 (老 loop 早已 Promise.all, pivot 是行为退化);
485
+ // ② 每个工具加超时包装 (默认 120s, BOLLOON_TOOL_TIMEOUT_MS 可调) —— 超时返回
486
+ // 失败结果 + 续跑提示, 让 LLM 下轮决定换法/重试, 而不是无限挂起。
487
+ const execTool = async (toolCall) => {
457
488
  this.state.toolCallsCount++;
489
+ const toolT0 = Date.now(); // 2026-10-12: 单工具耗时 (step_done 读秒用)
458
490
  const tool = this.tools.get(toolCall.name);
459
491
  if (!tool) {
460
492
  this.emit({
@@ -478,7 +510,7 @@ export class WorkflowPivotLoop {
478
510
  role: 'tool',
479
511
  content: JSON.stringify({ success: false, error: `Unknown tool: ${toolCall.name}` })
480
512
  });
481
- continue;
513
+ return;
482
514
  }
483
515
  this.emit({
484
516
  type: 'tool',
@@ -515,18 +547,38 @@ export class WorkflowPivotLoop {
515
547
  // **fail-closed**: 端口抛错 ⇒ 记拒执行文案且**绝不回落** `tool.execute` ——
516
548
  // 门坏掉若静默变成"没门"(旧实现那两条 fail-open 路径), 比没有门更危险。
517
549
  let result;
518
- if (this.config.guardedExecute) {
519
- try {
520
- result = await this.config.guardedExecute(tool, toolCall.args ?? {});
550
+ const executeFn = this.config.guardedExecute
551
+ ? () => this.config.guardedExecute(tool, toolCall.args ?? {})
552
+ : () => tool.execute(toolCall.args ?? {});
553
+ // 2026-10-12: 超时包装 — 慢工具不再拖死整轮 (失败结果给 LLM 下轮决策)
554
+ const toolTimeoutMs = TOOL_EXEC_TIMEOUT_MS;
555
+ // 2026-10-13 (leo: 「发送消息过程中的打断和衔接整个流程」): 工具执行阶段也要响应 abort —
556
+ // 之前 Promise.race 只有 executeFn + 超时两路, Ctrl+C 打断要等工具跑完/超时 (最长 120s) 才生效。
557
+ // 现在加第三路 abort: signal 触发立即返回"已中断"结果 → while 顶部 signal.aborted 检查 break。
558
+ const abortWait = new Promise((resolve) => {
559
+ const onAbort = () => resolve({ success: false, error: `已中断: ${toolCall.name} 被打断 (Ctrl+C)` });
560
+ if (signal?.aborted) {
561
+ onAbort();
562
+ return;
521
563
  }
522
- catch (guardErr) {
523
- const msg = String(guardErr?.message ?? guardErr);
524
- result = { success: false, error: `拒绝: [harness-error] ${msg}` };
525
- }
526
- }
527
- else {
528
- result = await tool.execute(toolCall.args ?? {});
529
- }
564
+ signal?.addEventListener?.('abort', onAbort, { once: true });
565
+ });
566
+ result = await Promise.race([
567
+ (async () => {
568
+ try {
569
+ return await executeFn();
570
+ }
571
+ catch (guardErr) {
572
+ const msg = String(guardErr?.message ?? guardErr);
573
+ return { success: false, error: `拒绝: [harness-error] ${msg}` };
574
+ }
575
+ })(),
576
+ new Promise((resolve) => setTimeout(() => {
577
+ resolve({ success: false, error: `工具超时 (${toolTimeoutMs}ms): ${toolCall.name} 未在时限内返回` });
578
+ }, toolTimeoutMs).unref?.()),
579
+ abortWait,
580
+ ]);
581
+ const toolCostMs = Date.now() - toolT0; // 2026-10-12: 实际耗时 (含等待)
530
582
  // 2026-08-11: 输出过大 → 续跑提示 (Hermes dropped-tools continuation 模式),
531
583
  // 上下文后续可能被压缩, 提醒 LLM 需要精确数据时缩小范围重查
532
584
  const outLen = (result.output || '').length;
@@ -537,19 +589,25 @@ export class WorkflowPivotLoop {
537
589
  this.emit({
538
590
  type: result.success ? 'step_done' : 'step_error',
539
591
  content: result.success
540
- ? `${toolCall.name} 成功`
541
- : `${toolCall.name} 失败: ${result.error}`,
592
+ ? `${toolCall.name} 成功 · ${formatDuration(toolCostMs)}`
593
+ : `${toolCall.name} 失败: ${result.error}`, // 失败文案带原因即可, 耗时由 UI 从 durationMs 读
542
594
  tool: toolCall.name,
543
595
  success: result.success,
544
- output: result.output,
596
+ output: summarizeToolOutput(toolCall.name, result.output),
545
597
  error: result.error,
598
+ durationMs: toolCostMs,
546
599
  });
547
- this.emit({
548
- type: result.success ? 'status' : 'error',
549
- content: result.success
550
- ? `✅ ${toolCall.name} 成功`
551
- : `❌ ${toolCall.name} 失败: ${result.error}`
552
- });
600
+ // 2026-10-12 (leo: 「✅ read_file 成功 不用发出来」):
601
+ // 成功不再发 `✅ X 成功` 状态行 —— step_done 已带 工具名+耗时+摘要, 重复的
602
+ // 成功汇报纯属噪音 (用户原话: 「这种执行工具成功的汇报不用发出来这个 log,
603
+ // 只需要把工具执行的记录写清楚就行了」)。**失败保留** (`❌` error 事件,
604
+ // 需人介入的信号不许吞)。
605
+ if (!result.success) {
606
+ this.emit({
607
+ type: 'error',
608
+ content: `❌ ${toolCall.name} 失败: ${result.error}`
609
+ });
610
+ }
553
611
  this.messageHistory.push({
554
612
  role: 'assistant',
555
613
  content: reply,
@@ -574,9 +632,29 @@ export class WorkflowPivotLoop {
574
632
  content: JSON.stringify({ success: false, error: String(execError) })
575
633
  });
576
634
  }
577
- }
635
+ };
636
+ await Promise.all(pendingTools.map(execTool));
578
637
  }
579
638
  catch (error) {
639
+ // 2026-10-13 (leo: 「多步执行的任务会返回空, 直接回复的不会」): 根因是**模型调用超时杀死循环**。
640
+ // 多步任务工具多轮后上下文膨胀 (40KB+), kernel 模型调用 30s 硬超时抛 ModelTimeoutError,
641
+ // 旧代码一律当「循环崩溃」处理 (return createResult(false, response, 'error')) ⇒
642
+ // 工具全干完、response 还是空 → 屏上 `│ │` 空框。
643
+ // 修复: 超时 = 供应商慢, 不是循环崩。emit 状态 + continue 重试下一轮;
644
+ // 真卡死由 maxConsecutiveNoProgress 兜底 (每条路都有终点, 唯独不该有「白干+空框」)。
645
+ const isTimeout = error instanceof Error && (error.name === 'ModelTimeoutError' || /模型调用超时/.test(error.message));
646
+ if (isTimeout) {
647
+ this.state.consecutiveNoProgress++;
648
+ this.emit({
649
+ type: 'status', internal: true, // K4-B: 循环遥测 = 内部运行日志 (口径同老 loop)
650
+ content: `⏳ 模型调用超时 (${String(error.message).replace(/^模型调用超时\s*\(/, '').replace(/\)$/, '')}), 重试下一轮 (连续 ${this.state.consecutiveNoProgress}/${effectiveConfig.maxConsecutiveNoProgress} 次则停)`,
651
+ tool: 'system',
652
+ });
653
+ // 2026-10-13 (leo: 「可以不用超时截止来确定时间周期, 有时候任务需要时间会比较久」):
654
+ // 超时只是这一轮没来得及, 任务该做多久做多久 —— **不**注入「快点给个回答」的收敛提示
655
+ // (那会让模型在任务没做完时草草收尾)。继续推进, 由质量门/无进展兜底决定什么时候算完。
656
+ continue;
657
+ }
580
658
  this.emit({
581
659
  type: 'error',
582
660
  content: `❌ 循环异常: ${error}`
@@ -618,12 +696,18 @@ export class WorkflowPivotLoop {
618
696
  }
619
697
  /**
620
698
  * Adapt configuration based on task profile
699
+ *
700
+ * 2026-10-12 (leo: 「工具轮次到了 10 次就截止了」): 旧实现用
701
+ * `Math.min(config.maxIterations, profile.suggestedMaxIterations)` 压迭代上限、
702
+ * `Math.min(config.maxTokenBudget, profile.tokenBudget)` 压 token 预算 ——
703
+ * simple 任务被压到 **10 iter + 800~2400 token**, 而 systemPrompt 本身就 40KB+,
704
+ * 第一次迭代 totalTokens 就超支 → 循环立刻 break, 长任务根本跑不完。
705
+ * 上限保留 createDefaultPivotConfig 的 200/1000/2000 (本来就注释「跑到 N 次也不报超时」),
706
+ * 由质量门 / maxConsecutiveNoProgress / user abort 兜底 —— 循环持久, 该停才停。
621
707
  */
622
708
  adaptConfigForTask(profile) {
623
709
  return {
624
710
  ...this.config,
625
- maxIterations: Math.min(this.config.maxIterations, profile.suggestedMaxIterations),
626
- maxTokenBudget: Math.min(this.config.maxTokenBudget, profile.tokenBudget),
627
711
  complexity: profile.complexity
628
712
  };
629
713
  }
@@ -847,6 +931,13 @@ export class WorkflowPivotLoop {
847
931
  * Evaluate response quality
848
932
  */
849
933
  evaluateQuality(response) {
934
+ // 2026-10-13 (leo: 「我发了一个命令, 返回空」): **空回复必须判不合格** —— 旧实现起点 0.7,
935
+ // 空串 '' 也有 0.7 分, 恰好等于默认阈值 ⇒ 空 reply 直接 quality_threshold_met 收尾 ⇒ CLI 空框
936
+ // (「你好」正常因为模型给文本; 「能看到我的仓库吗?」模型某轮返回空 content/tool-only 回复 ⇒ 空串过关)。
937
+ // 现在: 空/纯空白一律 0.3 (低于任何阈值), 打回让模型重给; 非空保持旧评分。
938
+ const t = String(response || '').trim();
939
+ if (!t)
940
+ return 0.3;
850
941
  let score = 0.7; // 2026-07-06: 起点 0.7 — 短回复不应被当成低质量打回去重跑
851
942
  // Length bonus
852
943
  if (response.length > 100)
@@ -908,7 +999,7 @@ export class WorkflowPivotLoop {
908
999
  const identityLine = lines.find((l) => l.includes('你是') && !l.includes('(续)')) ?? '';
909
1000
  return `[continuation] 你的 persona + tools + judgments 已在首轮 messageHistory 中.
910
1001
  ${identityLine ? identityLine + '\n' : ''}当前 step 别再读 persona 全文, 继续推进任务即可.
911
- > <final gen> 只在真完成时输出.`;
1002
+ > 任务完成前持续调用工具推进; 全部完成后给出完整最终回答.`;
912
1003
  }
913
1004
  /**
914
1005
  * Create result object
@@ -972,7 +1063,7 @@ ${identityLine ? identityLine + '\n' : ''}当前 step 别再读 persona 全文,
972
1063
  */
973
1064
  export function createDefaultPivotConfig(complexity) {
974
1065
  // 2026-07-06: maxIterations 默认大幅上调 — LLM 自己约束结束时机, 持久循环.
975
- // 真实停止由 LLM emit <final gen> / 质量达标 / 连续无进展 / user abort 触发.
1066
+ // 2026-10-12: 真实停止由 质量达标 / 连续无进展 / user abort 触发 (leo 拍板去掉 <final gen>).
976
1067
  // 旧 30/60 iter 在长任务 (eg. 大文档改写) 太短, 现在跑到 N 次也不报超时.
977
1068
  const profiles = {
978
1069
  simple: {
@@ -15,7 +15,8 @@ export { readProfile, setProfile };
15
15
  let watcherTimer = null;
16
16
  let watching = false; // 防重叠: 上一轮没跑完就跳过这一轮
17
17
  let watcherCount = 0;
18
- /** 单轮观察: 扫描世界 → 观察日志/校准环自动积累 (分发主体本地版) */
18
+ /** 单轮观察: 扫描世界 → 观察日志/校准环自动积累 (分发主体本地版)。
19
+ * 2026-10-05 (leo): 有 active intent / 画像时, 自动上网搜机会 (AI 主动搜索, 非被动响应)。 */
19
20
  export async function tickWorldWatcher() {
20
21
  if (watching)
21
22
  return { ok: true, observed: 0, count: watcherCount, error: '上一轮观察未完成, 跳过本轮 (防重叠)' };
@@ -23,12 +24,40 @@ export async function tickWorldWatcher() {
23
24
  try {
24
25
  const r = await scanOpportunities({ limit: 50 });
25
26
  watcherCount += 1;
27
+ // 2026-10-05: 主动搜索 — 有意图/画像才搜 (无 topic 不烧网络); 失败不影响扫描
28
+ await autoSearchForIntents();
26
29
  return { ok: r.ok, observed: r.results.length, count: watcherCount, error: r.error };
27
30
  }
28
31
  finally {
29
32
  watching = false;
30
33
  }
31
34
  }
35
+ /** 按 active intent + 画像文本自动搜索机会 (AI 主动上网找, 结果进 world/search/) */
36
+ async function autoSearchForIntents() {
37
+ try {
38
+ const { listIntents } = await import('./intent-store.js');
39
+ const intents = await listIntents();
40
+ const active = intents.ok ? intents.intents.filter((i) => i.status === 'active') : [];
41
+ const topics = active.map((i) => i.text);
42
+ if (!topics.length) {
43
+ const profile = await readProfile();
44
+ if (profile && (profile.about || profile.tags.length)) {
45
+ topics.push(profile.about || profile.tags.join(' '));
46
+ }
47
+ }
48
+ if (!topics.length)
49
+ return; // 无意图无画像 → 不主动搜
50
+ // 每轮至多搜 2 个主题 (网络调用有成本; 已有结果幂等不重复)
51
+ for (const t of topics.slice(0, 2)) {
52
+ const { searchOpportunitiesForTopic } = await import('./opportunity-web-search.js');
53
+ const r = await searchOpportunitiesForTopic(t, { limit: 4 });
54
+ if (r.added > 0) {
55
+ console.info(`[world-watcher] 主动搜索「${t.slice(0, 30)}」新增 ${r.added} 条机会`);
56
+ }
57
+ }
58
+ }
59
+ catch { /* 主动搜索失败静默, 下一轮再试 */ }
60
+ }
32
61
  /**
33
62
  * 启动世界自动观察器 (服务端启动时调用一次即可; 幂等, 重复调用不叠定时器)。
34
63
  * 每 intervalMs (默认 10 分钟) 自动扫描一次 —— 机会进入 AI 视野是主动行为, 不是人触发。
@@ -214,10 +214,8 @@ function collectEnv() {
214
214
  let llmProvider = 'unknown';
215
215
  try {
216
216
  const home = process.env.HOME || os.homedir() || '/tmp';
217
- // 2026-08-07: bolloon-config.json 优先, 旧 llm-config.json 兜底
218
- let cfgPath = path.join(home, '.bolloon', 'bolloon-config.json');
219
- if (!require('fs').existsSync(cfgPath))
220
- cfgPath = path.join(home, '.bolloon', 'llm-config.json');
217
+ // 2026-10-06 技术债清理: 只读 canonical bolloon-config.json (旧 llm-config.json 已废弃)
218
+ const cfgPath = path.join(home, '.bolloon', 'bolloon-config.json');
221
219
  const cfg = require(cfgPath);
222
220
  if (cfg && typeof cfg === 'object' && 'provider' in cfg) {
223
221
  llmProvider = String(cfg.provider);
@@ -71,7 +71,9 @@ async function readRuleFile(filePath) {
71
71
  }
72
72
  }
73
73
  /**
74
- * 读 project 层: 优先 Bolloon.md, fallback Bolloon.md, 最后尝试 .claude/rules/*.md
74
+ * 读 project 层: 优先 Bolloon.md, fallback Bolloon.md,
75
+ * 然后 AGENTS.md / CLAUDE.md (2026-10-12: Hermes/Claude Code 仓库约定文件, leo 要求对照 Hermes 主动加载),
76
+ * 最后尝试 .claude/rules/*.md
75
77
  */
76
78
  async function readProjectLayer(projectPath, projectRulesDir, maxChars) {
77
79
  // 1. Bolloon.md
@@ -83,6 +85,16 @@ async function readProjectLayer(projectPath, projectRulesDir, maxChars) {
83
85
  content = await readRuleFile(bolloonMdPath);
84
86
  if (content)
85
87
  return truncate(content, maxChars);
88
+ // 2b. 2026-10-12 (leo: 「开始的时候没有主动扫描加载这个仓库里面的技能, 看一下 hermes 怎么做的」):
89
+ // Hermes 启动会把当前仓库的 AGENTS.md / CLAUDE.md 自动装进 project context (仓库约定即技能/规则)。
90
+ // Bolloon 只认 Bolloon.md + .claude/rules, 漏了这两个最通用的约定文件 — 补上。
91
+ for (const f of ['AGENTS.md', 'CLAUDE.md']) {
92
+ const p = path.join(path.dirname(projectPath), f);
93
+ content = await readRuleFile(p);
94
+ if (content) {
95
+ return truncate(`### ${f}\n${content}`, maxChars);
96
+ }
97
+ }
86
98
  // 3. .claude/rules/*.md (合并所有)
87
99
  try {
88
100
  const entries = await fs.readdir(projectRulesDir, { withFileTypes: true });
@@ -15,7 +15,6 @@ import { Static, render, Box, Text, useInput, useApp, useStdout } from 'ink';
15
15
  import { stripUiNoise, collapsePaste, stripBracketedPaste, looksLikePasteChunk, logPasteChunk, shouldSuppressMention, singleLine, logPopupEvent, PASTE_MENTION_SHIELD_MS } from './input-paste.js';
16
16
  import TextInput from 'ink-text-input';
17
17
  import { dispWidth, LOADING_FRAMES as KAOMOJI } from './loading-tui.js';
18
- import { reasoningMode } from './reasoning-view.js';
19
18
  import { THEME, fg } from './theme.js';
20
19
  import { COMPOSER_PLACEHOLDER, POPUP_TITLE_TAB, POPUP_TITLE_AGENT, POPUP_TITLE_FILE, POPUP_TITLE_COMMAND } from './content.js';
21
20
  import { DOUBLE_ESC_MS, STATUS_TICK_MS, THINK_FRAME_MS } from './timing.js';
@@ -389,30 +388,48 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
389
388
  // 2026-09-08 (Hermes TUI 学习落地): 实时终端尺寸 — 原来 terminalW/H 在 mount 时冻结,
390
389
  // 终端 resize 后分隔线/logo 宽度全错位 (Hermes 用 resizeCoalescer + 实时 layout)。
391
390
  // 订阅 stdout resize, 每次渲染用最新列数。
391
+ // 2026-10-15 (leo 拖动侧边栏): **变窄**时终端把旧帧满宽行自动折行,
392
+ // Ink 的 eraseLines(lastOutputHeight) 按**未折行**行数擦 ⇒ 擦不干净 ⇒ 旧 footer/分隔线/
393
+ // 思考行残影被推进滚动区; 且 `<Static>` 历史区写进 scrollback 后不会重绘, 残影永远留在那里.
394
+ // 修法 (只对**变窄**): resize 停止 150ms 后整屏重建 —
395
+ // ① 发 `ESC[2J ESC[3J ESC[H` (清可视区 + 清 scrollback + 光标归位) 把残影彻底清掉;
396
+ // ② suspendInk + resumeInk 重挂全新 InkApp: `<Static>` 首次挂载对 store 全量重放
397
+ // (历史一行不少, 按新宽度重新 wrap), footer 按新宽度重画。
398
+ // 变宽不会折行 (旧行宽 ≤ 旧终端宽 < 新终端宽), Ink 自己重画即可, 不重建。
392
399
  const { stdout } = useStdout();
393
400
  const [termSize, setTermSize] = useState({ w: terminalW, h: terminalH });
394
401
  useEffect(() => {
395
- // resizeCoalescer (#9): 拖拽终端时会连发 resize — 聚合到 80ms 空闲后一次性应用, 避免中间态跳帧
396
- let raf = null;
397
- let pending = null;
398
- const apply = () => {
399
- if (pending) {
400
- const p = pending;
401
- pending = null;
402
- setTermSize(p);
402
+ let lastWidth = stdout?.columns || terminalW;
403
+ let rebuildTimer = null;
404
+ const rebuild = () => {
405
+ rebuildTimer = null;
406
+ try {
407
+ // 残影在 scrollback 里 —— eraseLines 够不到, 必须整屏清掉再让 Ink 全量重放
408
+ process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
409
+ suspendInk();
410
+ // 让 React 先完成 unmount 清理, 再重挂 (避免新旧实例抢 stdout)
411
+ setTimeout(() => { try {
412
+ resumeInk();
413
+ }
414
+ catch { /* 重建失败不致命, 下次 resize 再试 */ } }, 0);
403
415
  }
404
- raf = null;
416
+ catch { /* 清屏/重建失败不阻塞 resize 本身 */ }
405
417
  };
406
418
  const update = () => {
407
- pending = { w: stdout?.columns || terminalW, h: stdout?.rows || terminalH };
408
- if (raf)
409
- clearTimeout(raf);
410
- raf = setTimeout(apply, 80);
419
+ const w = stdout?.columns || terminalW;
420
+ setTermSize({ w, h: stdout?.rows || terminalH });
421
+ if (w < lastWidth) {
422
+ // 变窄: 折行残影无法增量修复, 停止拖动后整屏重建
423
+ if (rebuildTimer)
424
+ clearTimeout(rebuildTimer);
425
+ rebuildTimer = setTimeout(rebuild, 150);
426
+ }
427
+ lastWidth = w;
411
428
  };
412
429
  update();
413
430
  stdout?.on?.('resize', update);
414
- return () => { stdout?.removeListener?.('resize', update); if (raf)
415
- clearTimeout(raf); };
431
+ return () => { if (rebuildTimer)
432
+ clearTimeout(rebuildTimer); stdout?.removeListener?.('resize', update); };
416
433
  // eslint-disable-next-line react-hooks/exhaustive-deps
417
434
  }, [stdout]);
418
435
  const W = termSize.w;
@@ -509,6 +526,16 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
509
526
  setTimeout(() => process.exit(0), 2000);
510
527
  };
511
528
  if (key.ctrl && _input === 'c') {
529
+ // 2026-10-13 (leo: 「按一次 Ctrl+C 是中断当前对话; 按两次才是完整退出; 空闲时按一次就退出」):
530
+ // 走 index.ts 注册的两级语义处理器 —— 有回合在跑且未 armed → 中断回合 (abort kernel actor);
531
+ // 第二次 / 空闲 → 完整退出。Ink raw mode 下 Ctrl+C 被 useInput 拦截, 不会产生 SIGINT,
532
+ // 所以**这里**才是 Ctrl+C 的主入口 (进程级 SIGINT 只是启动早期兜底)。
533
+ const sigintHandler = globalThis.__bolloonCliSigint;
534
+ if (typeof sigintHandler === 'function') {
535
+ sigintHandler();
536
+ return;
537
+ }
538
+ // 兜底: 处理器没注册 (极早期) → 直接退出, 不吞 Ctrl+C
512
539
  requestExit();
513
540
  return;
514
541
  }
@@ -839,7 +866,7 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
839
866
  }
840
867
  catch { /* 探针写不进去绝不影响界面 */ }
841
868
  });
842
- return (_jsxs(_Fragment, { children: [_jsx(Static, { items: msgs, children: (m, i) => _jsx(Text, { children: m }, `m${i}`) }), _jsxs(Box, { flexDirection: "column", width: budget.cols, children: [hasRails && (_jsx(Box, { flexDirection: "column", children: railNames.map((n) => _jsx(Text, { color: THEME.muted, children: `${n}: ${String(widgets[n] ?? '').replace(/\n/g, ' ')}` }, n)) })), popupOpenNow && tabState && (_jsx(MentionPopup, { title: POPUP_TITLE_TAB, items: filtered, sel: safeSel, width: W, maxRows: budget.popupRows })), popupOpenNow && !tabState && mention && (_jsx(MentionPopup, { title: popupTitle, items: filtered, sel: safeSel, width: W, loading: loadingFiles, maxRows: budget.popupRows })), popupOpenNow && !tabState && !mention && picker && (_jsx(MentionPopup, { title: picker.title, items: picker.items, sel: Math.min(picker.sel, picker.items.length - 1), width: W, maxRows: budget.popupRows })), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: transient ? (_jsx(Text, { children: transient })) : thinking ? (_jsx(Text, { color: "yellow", children: reasoningMode() === 'trace' ? '思考中...' : `${KAOMOJI[thinkingIdx.current]} 思考中...` })) : (_jsx(Text, { color: THEME.muted, children: '· 回车发送 · ↑↓ 历史 · PgUp 回看 · Esc 双击退出' })) }), _jsx(Box, { width: budget.cols, height: 1, overflow: "hidden", children: _jsx(Text, { bold: true, color: THEME.accent, children: ruleFor(Math.max(10, budget.cols - 1)) }) }), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: _jsx(Text, { children: status }) }), _jsx(Box, { width: budget.cols, height: 1, overflow: "hidden", children: _jsx(Text, { bold: true, color: THEME.accent, children: ruleFor(Math.max(10, budget.cols - 1)) }) }), _jsxs(Box, { width: budget.cols, height: 1, overflow: "hidden", children: [_jsx(Text, { bold: true, color: THEME.accent, children: "\u276F " }), _jsx(Box, { width: Math.max(10, budget.cols - 2), height: 1, overflow: "hidden", flexShrink: 0, children: _jsx(TextInput, { value: singleLine(input), onChange: setInput, onSubmit: onSubmit, focus: !popupOpenNow, placeholder: COMPOSER_PLACEHOLDER }, tiKey) })] }), _jsx(Box, { width: budget.cols, height: 1, overflow: "hidden", children: _jsx(Text, { bold: true, color: THEME.accent, children: ruleFor(Math.max(10, budget.cols - 1)) }) })] })] }));
869
+ return (_jsxs(_Fragment, { children: [_jsx(Static, { items: msgs, children: (m, i) => _jsx(Text, { children: m }, `m${i}`) }), _jsxs(Box, { flexDirection: "column", width: budget.cols, children: [hasRails && (_jsx(Box, { flexDirection: "column", children: railNames.map((n) => _jsx(Text, { color: THEME.muted, children: `${n}: ${String(widgets[n] ?? '').replace(/\n/g, ' ')}` }, n)) })), popupOpenNow && tabState && (_jsx(MentionPopup, { title: POPUP_TITLE_TAB, items: filtered, sel: safeSel, width: W, maxRows: budget.popupRows })), popupOpenNow && !tabState && mention && (_jsx(MentionPopup, { title: popupTitle, items: filtered, sel: safeSel, width: W, loading: loadingFiles, maxRows: budget.popupRows })), popupOpenNow && !tabState && !mention && picker && (_jsx(MentionPopup, { title: picker.title, items: picker.items, sel: Math.min(picker.sel, picker.items.length - 1), width: W, maxRows: budget.popupRows })), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: transient ? (_jsx(Text, { children: transient })) : thinking ? (_jsxs(Text, { color: "yellow", children: [KAOMOJI[thinkingIdx.current], " \u601D\u8003\u4E2D..."] })) : (_jsx(Text, { color: THEME.muted, children: '· 回车发送 · ↑↓ 历史 · PgUp 回看 · Esc 双击退出' })) }), _jsx(Box, { width: budget.cols, height: 1, overflow: "hidden", children: _jsx(Text, { bold: true, color: THEME.accent, children: ruleFor(Math.max(10, budget.cols - 1)) }) }), _jsx(Box, { height: 1, width: budget.cols, overflow: "hidden", children: _jsx(Text, { children: status }) }), _jsx(Box, { width: budget.cols, height: 1, overflow: "hidden", children: _jsx(Text, { bold: true, color: THEME.accent, children: ruleFor(Math.max(10, budget.cols - 1)) }) }), _jsxs(Box, { width: budget.cols, height: 1, overflow: "hidden", children: [_jsx(Text, { bold: true, color: THEME.accent, children: "\u276F " }), _jsx(Box, { width: Math.max(10, budget.cols - 2), height: 1, overflow: "hidden", flexShrink: 0, children: _jsx(TextInput, { value: singleLine(input), onChange: setInput, onSubmit: onSubmit, focus: !popupOpenNow, placeholder: COMPOSER_PLACEHOLDER }, tiKey) })] }), _jsx(Box, { width: budget.cols, height: 1, overflow: "hidden", children: _jsx(Text, { bold: true, color: THEME.accent, children: ruleFor(Math.max(10, budget.cols - 1)) }) })] })] }));
843
870
  };
844
871
  export { InkApp };
845
872
  // ─── 启动 ────────────────────────────────────────────────────────────────────
@@ -172,14 +172,26 @@ export async function opportunityCommand(flags) {
172
172
  human: INTENT_USAGE,
173
173
  };
174
174
  }
175
- function cardRows(opts) {
176
- const pct = Math.round(opts.score * 100);
177
- const tag = opts.reason === 'match' ? '● 与你匹配' : '○ 世界变化';
178
- return [
179
- ` ${pct}% ${tag} ${opts.title}`,
180
- ` ${opts.summary}`,
181
- ` 预算: ${opts.budget ?? '未知'} · id=${opts.id}`,
182
- ];
175
+ /** 2026-10-06 (用户: 世界模式用表格呈现): ASCII 表格机会列表 — 列 = 分数/来源/标题/预算/id */
176
+ export function opportunityTable(results) {
177
+ if (!results.length)
178
+ return [' (没有机会)'];
179
+ const reasonShort = (r) => (r === 'match' ? '匹配' : r === 'inbox' ? '投递' : r === 'search' ? '搜索' : '世界');
180
+ const rows = results.map((o) => {
181
+ const pct = Math.round(o.score * 100);
182
+ const title = o.title.length > 32 ? o.title.slice(0, 31) + '…' : o.title;
183
+ return [String(pct) + '%', reasonShort(o.reason), title, (o.budget ?? '无').slice(0, 14), o.id];
184
+ });
185
+ const w = (idx, min) => Math.max(min, ...rows.map((r) => r[idx].length));
186
+ const wS = w(0, 5), wR = w(1, 4), wT = w(2, 32), wB = w(3, 6), wId = w(4, 8);
187
+ const sep = ` +${'-'.repeat(wS)}+${'-'.repeat(wR)}+${'-'.repeat(wT)}+${'-'.repeat(wB)}+${'-'.repeat(wId)}+`;
188
+ const head = ` | ${'分数'.padStart(wS - 2)} | ${'来源'.padEnd(wR)} | ${'标题'.padEnd(wT)} | ${'预算'.padEnd(wB)} | ${'ID'.padEnd(wId)} |`;
189
+ const line = (c) => ` | ${c[0].padStart(wS)} | ${c[1].padEnd(wR)} | ${c[2].padEnd(wT)} | ${c[3].padEnd(wB)} | ${c[4].padEnd(wId)} |`;
190
+ const out = [sep, head, sep];
191
+ for (const r of rows)
192
+ out.push(line(r));
193
+ out.push(sep);
194
+ return out;
183
195
  }
184
196
  async function opportunityScan(flags) {
185
197
  const head = 'bolloon opportunity scan';
@@ -201,8 +213,7 @@ async function opportunityScan(flags) {
201
213
  }
202
214
  const data = { count: r.results.length, minScore, opportunities: r.results };
203
215
  const human = [title(head), ` 找到 ${r.results.length} 个机会 (score ≥ ${minScore}):`, ''];
204
- for (const o of r.results)
205
- human.push(...cardRows(o));
216
+ human.push(...opportunityTable(r.results));
206
217
  human.push('', hint('用 bolloon opportunity accept <id> 确认, ignore <id> 忽略'));
207
218
  return { envelope: okEnvelope('OK', `${r.results.length} 个机会`, data, r.results.map((x) => x.id), null), human: human.join('\n') };
208
219
  }
@@ -219,8 +230,7 @@ async function opportunityList(flags) {
219
230
  if (!hits.length)
220
231
  return { envelope: okEnvelope('OK', `意图「${i.text}」暂无匹配`, { count: 0, opportunities: [] }, [], null), human: `${title(head)}\n 「${i.text}」暂无匹配机会` };
221
232
  const human = [title(head), ` 「${i.text}」的 ${hits.length} 个机会:`, ''];
222
- for (const o of hits)
223
- human.push(...cardRows(o));
233
+ human.push(...opportunityTable(hits));
224
234
  return { envelope: okEnvelope('OK', `${hits.length} 个机会`, { count: hits.length, opportunities: hits }, hits.map((x) => x.id), null), human: human.join('\n') };
225
235
  }
226
236
  return opportunityScan(flags);
@@ -12,6 +12,7 @@
12
12
  * LoadingTUI 原地刷新的启动仪表盘类
13
13
  */
14
14
  import { truncateSafe } from './status-segments.js';
15
+ import { toolArgsSummary } from './trace-line.js';
15
16
  import * as fs from 'fs';
16
17
  import * as path from 'path';
17
18
  import { fileURLToPath } from 'url';
@@ -433,12 +434,17 @@ function colorizeDiff(text) {
433
434
  });
434
435
  return out.join('\n');
435
436
  }
436
- /** 渲染单条工具调用为紧凑列表项 (增量显示, 一行, 无 args) */
437
+ /** 渲染单条工具调用为紧凑列表项 (增量显示, 一行, 参数摘要) */
437
438
  export function renderToolCallListItem(item, index, total) {
438
439
  const dur = item.durationMs != null ? ` ${C_DIM}${item.durationMs}ms${RESET}` : '';
439
440
  const label = item.status === 'error' ? `${C_ERROR}${item.tool}${RESET}` : `${C_ACCENT}${item.tool}${RESET}`;
441
+ // 2026-10-14 (leo: 「调用工具的时候, 把执行的命令缩写也附带上 —— 现在看不到过程」):
442
+ // 工具行附上参数摘要 (terminal→命令 / read_file→路径 / grep→pattern·dir), 灰色、截断;
443
+ // 看不清过程的问题 = 之前只有工具名, 不知道它具体跑了什么。
444
+ const sum = toolArgsSummary(item.tool, item.args);
445
+ const sumPart = sum ? ` ${C_DIM}${sum}${RESET}` : '';
440
446
  // 2026-10-01: 工具行也按**保守宽度**截断 —— 超宽会让终端折行、打乱 Ink 光标 (底栏那块就是这么坏的)
441
- return truncateSafe(` 🔧 ${label}${dur}`, Math.max(20, (process.stdout.columns || 100) - 1));
447
+ return truncateSafe(` 🔧 ${label}${sumPart}${dur}`, Math.max(20, (process.stdout.columns || 100) - 1));
442
448
  }
443
449
  /** 紧凑渲染工具输出 (检测到 diff 则着色, 否则普通截断) — 当前不显示 body */
444
450
  export function renderToolCallBody(item, width = 72) {