@gajae-code/coding-agent 0.18.11 → 0.18.12

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 (270) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/cli/accounts-cli.d.ts +2 -2
  7. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  8. package/dist/types/cli/install-activation.d.ts +10 -4
  9. package/dist/types/cli/public-command-errors.d.ts +1 -0
  10. package/dist/types/cli/update-cli.d.ts +14 -1
  11. package/dist/types/cli-ordinary.d.ts +2 -2
  12. package/dist/types/commands/accounts.d.ts +2 -1
  13. package/dist/types/commands/migrate.d.ts +1 -1
  14. package/dist/types/commands/sdk.d.ts +5 -0
  15. package/dist/types/commit/changelog/index.d.ts +1 -0
  16. package/dist/types/config/autorouting-tier-map.d.ts +30 -0
  17. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  18. package/dist/types/config/config-hot-reload.d.ts +39 -0
  19. package/dist/types/config/file-lock.d.ts +8 -1
  20. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  21. package/dist/types/config/model-profile-activation.d.ts +62 -6
  22. package/dist/types/config/model-registry.d.ts +56 -5
  23. package/dist/types/config/model-resolver.d.ts +2 -0
  24. package/dist/types/config/models-config-schema.d.ts +10 -0
  25. package/dist/types/config/settings-schema.d.ts +46 -3
  26. package/dist/types/config/settings.d.ts +33 -1
  27. package/dist/types/dap/config.d.ts +8 -4
  28. package/dist/types/debug/system-info.d.ts +6 -1
  29. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  30. package/dist/types/edit/modes/patch.d.ts +5 -0
  31. package/dist/types/edit/modes/replace.d.ts +3 -0
  32. package/dist/types/exec/bash-executor.d.ts +12 -0
  33. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  34. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  35. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  36. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  37. package/dist/types/migrate/adapters/index.d.ts +10 -1
  38. package/dist/types/migrate/executor.d.ts +3 -1
  39. package/dist/types/migrate/types.d.ts +15 -3
  40. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  42. package/dist/types/modes/theme/theme.d.ts +1 -0
  43. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  44. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  45. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  46. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  47. package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
  48. package/dist/types/sdk/broker/broker.d.ts +7 -1
  49. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  50. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  51. package/dist/types/sdk/broker/ensure.d.ts +136 -7
  52. package/dist/types/sdk/broker/hop.d.ts +36 -0
  53. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  54. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +4 -1
  56. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  57. package/dist/types/sdk/bus/index.d.ts +23 -0
  58. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  59. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  60. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  61. package/dist/types/sdk/host/session-runtime.d.ts +32 -0
  62. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  63. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  64. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  65. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  66. package/dist/types/sdk/session-list.d.ts +1 -1
  67. package/dist/types/session/agent-session.d.ts +58 -2
  68. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  69. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  70. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  71. package/dist/types/session/messages.d.ts +5 -1
  72. package/dist/types/session/session-manager.d.ts +13 -3
  73. package/dist/types/session/startup-auth-config.d.ts +24 -2
  74. package/dist/types/session/streaming-output.d.ts +11 -0
  75. package/dist/types/session/terminal-abort.d.ts +3 -1
  76. package/dist/types/session-import/service.d.ts +1 -1
  77. package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
  78. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  79. package/dist/types/task/executor.d.ts +19 -2
  80. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  81. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  82. package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
  83. package/dist/types/tools/descriptors.d.ts +2 -0
  84. package/dist/types/tools/fetch.d.ts +1 -1
  85. package/dist/types/tools/index.d.ts +2 -0
  86. package/dist/types/tools/python.d.ts +1 -1
  87. package/dist/types/tools/report-issue.d.ts +41 -0
  88. package/dist/types/tools/subagent.d.ts +23 -0
  89. package/dist/types/web/scrapers/types.d.ts +8 -1
  90. package/dist/types/web/search/providers/insane.d.ts +7 -1
  91. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  92. package/package.json +8 -8
  93. package/scripts/build-binary.ts +2 -1
  94. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  95. package/scripts/generate-docs-index.ts +8 -1
  96. package/scripts/generate-sdk-operation-inventory.ts +22 -4
  97. package/src/async/job-manager.ts +356 -56
  98. package/src/async/result-formatting.ts +40 -0
  99. package/src/autoresearch/git.ts +9 -2
  100. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  101. package/src/capability/fs.ts +27 -19
  102. package/src/capability/index.ts +2 -2
  103. package/src/cli/accounts-cli.ts +176 -8
  104. package/src/cli/auth-broker-cli.ts +21 -11
  105. package/src/cli/doctor/install-repairs.ts +35 -5
  106. package/src/cli/fast-help.ts +1 -1
  107. package/src/cli/install-activation.ts +47 -4
  108. package/src/cli/migrate-cli.ts +7 -5
  109. package/src/cli/public-command-errors.ts +4 -0
  110. package/src/cli/root-flags.ts +2 -2
  111. package/src/cli/stats-cli.ts +9 -5
  112. package/src/cli/update-cli.ts +224 -7
  113. package/src/cli-ordinary.ts +7 -2
  114. package/src/commands/accounts.ts +10 -5
  115. package/src/commands/migrate.ts +3 -3
  116. package/src/commands/sdk.ts +199 -42
  117. package/src/commands/ultragoal.ts +16 -8
  118. package/src/commit/changelog/index.ts +39 -1
  119. package/src/commit/message.ts +10 -3
  120. package/src/config/autorouting-tier-map.ts +30 -0
  121. package/src/config/config-hot-reload-trust.ts +148 -0
  122. package/src/config/config-hot-reload.ts +734 -0
  123. package/src/config/file-lock.ts +216 -56
  124. package/src/config/model-bindings-applier.ts +85 -47
  125. package/src/config/model-discovery-manager.ts +10 -0
  126. package/src/config/model-profile-activation.ts +716 -113
  127. package/src/config/model-registry.ts +1334 -158
  128. package/src/config/model-resolver.ts +3 -1
  129. package/src/config/models-config-schema.ts +6 -0
  130. package/src/config/settings-schema.ts +60 -3
  131. package/src/config/settings.ts +458 -24
  132. package/src/coordinator-mcp/policy.ts +26 -1
  133. package/src/crash/sanitize.ts +4 -0
  134. package/src/customization/import.ts +3 -1
  135. package/src/dap/config.ts +31 -11
  136. package/src/dap/defaults.json +1 -1
  137. package/src/debug/crash-diagnostics.ts +2 -1
  138. package/src/debug/report-bundle.ts +19 -0
  139. package/src/debug/system-info.ts +8 -3
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  141. package/src/discovery/agents-md.ts +1 -1
  142. package/src/discovery/helpers.ts +1 -2
  143. package/src/discovery/mcp-compat.ts +120 -17
  144. package/src/edit/apply-patch/index.ts +12 -1
  145. package/src/edit/index.ts +13 -17
  146. package/src/edit/modes/patch.ts +187 -8
  147. package/src/edit/modes/replace.ts +136 -0
  148. package/src/eval/js/context-manager.ts +29 -3
  149. package/src/eval/js/worker-entry.ts +51 -18
  150. package/src/eval/py/executor.ts +14 -4
  151. package/src/eval/py/runner.py +144 -5
  152. package/src/exec/bash-executor.ts +81 -4
  153. package/src/exec/bash-shell-guardian.ts +4 -1
  154. package/src/exec/bash-shell-supervisor.ts +8 -4
  155. package/src/exec/isolated-shell.ts +5 -1
  156. package/src/export/html/index.ts +46 -6
  157. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  158. package/src/extensibility/extensions/loader.ts +8 -1
  159. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  160. package/src/gjc-runtime/gc-runtime.ts +33 -6
  161. package/src/gjc-runtime/launch-tmux.ts +7 -2
  162. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  163. package/src/gjc-runtime/launch-worktree.ts +101 -14
  164. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  165. package/src/gjc-runtime/session-resolution.ts +38 -6
  166. package/src/gjc-runtime/session-state-lock.ts +59 -14
  167. package/src/gjc-runtime/state-runtime.ts +23 -19
  168. package/src/hooks/native-skill-hook.ts +14 -11
  169. package/src/internal-urls/docs-index.generated.ts +14 -13
  170. package/src/internal-urls/local-protocol.ts +142 -8
  171. package/src/lsp/index.ts +29 -0
  172. package/src/lsp/workspace-path.ts +23 -0
  173. package/src/migrate/action-planner.ts +57 -0
  174. package/src/migrate/adapters/claude-code.ts +23 -4
  175. package/src/migrate/adapters/codex.ts +22 -4
  176. package/src/migrate/adapters/index.ts +22 -4
  177. package/src/migrate/adapters/opencode.ts +6 -1
  178. package/src/migrate/executor.ts +84 -7
  179. package/src/migrate/report.ts +19 -2
  180. package/src/migrate/types.ts +14 -4
  181. package/src/modes/DESIGN.md +51 -0
  182. package/src/modes/acp/permission-mode.ts +18 -1
  183. package/src/modes/components/assistant-message.ts +55 -1
  184. package/src/modes/components/settings-selector.ts +103 -37
  185. package/src/modes/controllers/command-controller.ts +38 -2
  186. package/src/modes/controllers/plan-mode-controller.ts +17 -7
  187. package/src/modes/controllers/selector-controller.ts +62 -6
  188. package/src/modes/interactive-mode.ts +68 -0
  189. package/src/modes/theme/theme.ts +15 -0
  190. package/src/prompts/tools/debug.md +1 -1
  191. package/src/prompts/tools/read.md +1 -1
  192. package/src/prompts/tools/report-issue.md +13 -0
  193. package/src/prompts/tools/task.md +1 -1
  194. package/src/runtime-credential-selector.ts +2 -2
  195. package/src/runtime-mcp/content-limits.ts +7 -0
  196. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  197. package/src/runtime-mcp/manager.ts +32 -0
  198. package/src/runtime-mcp/oauth-flow.ts +32 -6
  199. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  200. package/src/runtime-mcp/render.ts +16 -1
  201. package/src/runtime-mcp/tool-bridge.ts +126 -17
  202. package/src/sdk/broker/broker-exit.ts +8 -5
  203. package/src/sdk/broker/broker.ts +342 -82
  204. package/src/sdk/broker/daemon-entry.ts +338 -116
  205. package/src/sdk/broker/doctor-restart.ts +2 -1
  206. package/src/sdk/broker/ensure.ts +1143 -265
  207. package/src/sdk/broker/hop.ts +274 -0
  208. package/src/sdk/broker/lifecycle-ledger.ts +35 -11
  209. package/src/sdk/broker/lock-host-identity.ts +29 -0
  210. package/src/sdk/broker/session-index.ts +62 -12
  211. package/src/sdk/bus/chat-command-policy.ts +16 -3
  212. package/src/sdk/bus/index.ts +52 -21
  213. package/src/sdk/bus/reconciliation-store.ts +201 -8
  214. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  215. package/src/sdk/cli/session-cli.ts +21 -6
  216. package/src/sdk/host/session-runtime.test.ts +118 -23
  217. package/src/sdk/host/session-runtime.ts +352 -47
  218. package/src/sdk/lifecycle/service.ts +1 -0
  219. package/src/sdk/prompt-deadline-flush.ts +78 -5
  220. package/src/sdk/prompt-deadline-manager.ts +68 -14
  221. package/src/sdk/protocol/adapter-validation.ts +53 -11
  222. package/src/sdk/protocol/operation-inventory.generated.json +101 -2
  223. package/src/sdk/reconciliation-extensions.ts +1 -0
  224. package/src/sdk/session-list.ts +1 -0
  225. package/src/sdk/session.ts +114 -61
  226. package/src/session/agent-session.ts +1043 -81
  227. package/src/session/internal/managed-session-scope.ts +126 -39
  228. package/src/session/internal/managed-session-storage.ts +14 -11
  229. package/src/session/internal/native-publish-outcome.ts +5 -1
  230. package/src/session/messages.ts +47 -6
  231. package/src/session/session-manager.ts +236 -14
  232. package/src/session/startup-auth-config.ts +116 -8
  233. package/src/session/streaming-output.ts +262 -4
  234. package/src/session/terminal-abort.ts +14 -2
  235. package/src/session-import/service.ts +1 -0
  236. package/src/skill-state/workflow-mutation-guard.ts +491 -32
  237. package/src/slash-commands/builtin-registry.ts +15 -3
  238. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  239. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  240. package/src/task/executor.ts +103 -12
  241. package/src/task/index.ts +389 -175
  242. package/src/tools/ast-edit-write-path.ts +64 -0
  243. package/src/tools/ast-edit.ts +18 -1
  244. package/src/tools/atomic-file-write.ts +46 -15
  245. package/src/tools/auto-generated-guard.ts +3 -0
  246. package/src/tools/bash-allowed-prefixes.ts +153 -5
  247. package/src/tools/bash-interactive.ts +14 -2
  248. package/src/tools/bash.ts +14 -0
  249. package/src/tools/debug.ts +7 -6
  250. package/src/tools/descriptors.test.ts +44 -0
  251. package/src/tools/descriptors.ts +34 -2
  252. package/src/tools/fetch.ts +3 -2
  253. package/src/tools/index.ts +2 -0
  254. package/src/tools/output-meta.ts +23 -2
  255. package/src/tools/python.ts +1 -1
  256. package/src/tools/read.ts +17 -35
  257. package/src/tools/report-issue.ts +184 -0
  258. package/src/tools/subagent-render.ts +114 -12
  259. package/src/tools/subagent.ts +108 -1
  260. package/src/tools/todo-write.ts +41 -3
  261. package/src/tools/tool-catalog.generated.ts +31 -3
  262. package/src/tools/write.ts +35 -3
  263. package/src/utils/commit-message-generator.ts +9 -2
  264. package/src/utils/file-mentions.ts +29 -1
  265. package/src/utils/title-generator.ts +7 -2
  266. package/src/web/scrapers/index.ts +3 -1
  267. package/src/web/scrapers/types.ts +8 -0
  268. package/src/web/scrapers/youtube.ts +3 -2
  269. package/src/web/search/providers/insane.ts +91 -28
  270. package/src/web/search/providers/searxng.ts +62 -10
@@ -84,6 +84,9 @@ export class LazyAgentTool implements AgentTool<any, any, any> {
84
84
  #loader?: () => ToolDescriptorLoadResult;
85
85
  #session?: ToolSession;
86
86
  #loadPromise?: Promise<AgentTool<any, any, any>>;
87
+ #loadGeneration = 0;
88
+ #loadToken?: object;
89
+ #expectedCwd?: string;
87
90
 
88
91
  constructor(
89
92
  descriptor: ToolDescriptor,
@@ -97,9 +100,31 @@ export class LazyAgentTool implements AgentTool<any, any, any> {
97
100
  this.#session = session;
98
101
  }
99
102
 
103
+ /** Clear a session-bound materialization after its owning session moves. */
104
+ invalidateForSessionMove(expectedCwd?: string): void {
105
+ this.#loadGeneration++;
106
+ this.#tool = undefined;
107
+ this.#loadPromise = undefined;
108
+ this.#loadToken = undefined;
109
+ this.#expectedCwd = expectedCwd;
110
+ if (!this.#loader && this.#session) {
111
+ const session = this.#session;
112
+ this.#loader = () => this.descriptor.load(session);
113
+ }
114
+ }
115
+
100
116
  async #get(): Promise<AgentTool<any, any, any>> {
117
+ const sessionCwd = this.#session?.cwd;
118
+ if (this.#expectedCwd !== undefined && sessionCwd !== this.#expectedCwd) {
119
+ throw new ToolError(
120
+ `Tool "${this.descriptor.metadata.name}" session cwd changed outside a committed move (expected ${this.#expectedCwd}, got ${sessionCwd ?? "unknown"})`,
121
+ );
122
+ }
101
123
  if (this.#tool) return this.#tool;
102
124
  if (!this.#loadPromise) {
125
+ const generation = this.#loadGeneration;
126
+ const token = {};
127
+ this.#loadToken = token;
103
128
  const load = this.#loader;
104
129
  this.#loadPromise = Promise.resolve()
105
130
  .then(() => {
@@ -108,11 +133,14 @@ export class LazyAgentTool implements AgentTool<any, any, any> {
108
133
  })
109
134
  .then(tool => {
110
135
  if (!tool) throw new ToolError(`Tool "${this.descriptor.metadata.name}" failed to load`);
111
- this.#tool = tool;
136
+ if (generation === this.#loadGeneration && this.#loadToken === token) this.#tool = tool;
112
137
  return tool;
113
138
  })
114
139
  .catch(error => {
115
- this.#loadPromise = undefined;
140
+ if (generation === this.#loadGeneration && this.#loadToken === token) {
141
+ this.#loadPromise = undefined;
142
+ this.#loadToken = undefined;
143
+ }
116
144
  if (error instanceof ToolError) throw error;
117
145
  throw new ToolError(
118
146
  `Tool "${this.descriptor.metadata.name}" failed to load implementation: ${error instanceof Error ? error.message : String(error)}`,
@@ -270,6 +298,7 @@ function availableFor(name: string, session: ToolSession, context = defaultAvail
270
298
  if (name === "ast_grep") return Boolean(session.settings.get("astGrep.enabled"));
271
299
  if (name === "ast_edit") return Boolean(session.settings.get("astEdit.enabled"));
272
300
  if (name === "render_mermaid") return Boolean(session.settings.get("renderMermaid.enabled"));
301
+ if (name === "report_issue") return Boolean(session.settings.get("reportIssue.enabled"));
273
302
  if (name === "web_search") return Boolean(session.settings.get("web_search.enabled"));
274
303
  if (name === "search_tool_bm25") return context.discoveryActive ?? resolveDiscoveryActive(session);
275
304
  if (name === "calc") return Boolean(session.settings.get("calc.enabled"));
@@ -389,6 +418,8 @@ const loaders: Record<string, Loader> = {
389
418
  cached("goal", () => import("../goals/tools/goal-tool")).then(module => new module.GoalTool(session)),
390
419
  move_session: session =>
391
420
  cached("move_session", () => import("./move-session")).then(module => new module.MoveSessionTool(session)),
421
+ report_issue: session =>
422
+ cached("report_issue", () => import("./report-issue")).then(module => new module.ReportIssueTool(session)),
392
423
  yield: session => cached("yield", () => import("./yield")).then(module => new module.YieldTool(session)),
393
424
  report_finding: _session => cached("review", () => import("./review")).then(module => module.reportFindingTool),
394
425
  resolve: session => cached("resolve", () => import("./resolve")).then(module => new module.ResolveTool(session)),
@@ -481,6 +512,7 @@ const names: Array<[name: string, label: string, summary: string | undefined, lo
481
512
  ["skill", "Skill", "Chain into another available skill", "essential"],
482
513
  ["goal", "Goal", undefined, "essential"],
483
514
  ["move_session", "Move Session", undefined, "essential"],
515
+ ["report_issue", "Report Issue", "Record a tool that behaved inconsistently with its description", "essential"],
484
516
  ];
485
517
 
486
518
  const sessionDescriptions: Readonly<Record<string, ToolDescriptorMetadata["descriptionForSession"]>> = {
@@ -677,12 +677,13 @@ async function handleSpecialUrls(
677
677
  timeout: number,
678
678
  signal: AbortSignal | undefined,
679
679
  storage: AgentStorage | null,
680
+ settings: Settings,
680
681
  ): Promise<FetchRenderResult | null> {
681
682
  for (const handler of getSpecialHandlers()) {
682
683
  if (signal?.aborted) {
683
684
  throw new ToolAbortError();
684
685
  }
685
- const result = await handler(url, timeout, signal, storage);
686
+ const result = await handler(url, timeout, signal, storage, settings);
686
687
  if (result) return result;
687
688
  }
688
689
  return null;
@@ -766,7 +767,7 @@ async function renderUrl(
766
767
 
767
768
  // Step 1: Try special handlers for known sites (unless raw mode)
768
769
  if (!raw) {
769
- const specialResult = await handleSpecialUrls(url, timeout, signal, storage);
770
+ const specialResult = await handleSpecialUrls(url, timeout, signal, storage, settings);
770
771
  if (specialResult) return specialResult;
771
772
  }
772
773
 
@@ -364,6 +364,8 @@ export interface ToolSession {
364
364
  * directory, or escapes the current scope.
365
365
  */
366
366
  rescopeSessionCwd?: (path: string) => Promise<{ from: string; to: string }>;
367
+ /** Live ACP/SDK approval mode. Task children copy this at spawn; absent means `allow`. */
368
+ getSdkPermissionMode?: () => "prompt" | "allow" | "deny";
367
369
  /** Bridge to the connected client (e.g. ACP editor host). Tools should route fs/terminal/permission requests through this when available. */
368
370
  getClientBridge?: () => ClientBridge | undefined;
369
371
  /** Get compact conversation context for subagents (excludes tool results, system prompts) */
@@ -20,13 +20,14 @@ import {
20
20
  formatMiddleElisionMarker,
21
21
  type OutputSummary,
22
22
  type ReadWindow,
23
+ runWithOutputRedaction,
23
24
  type TruncationResult,
24
25
  truncateMiddle,
25
26
  truncateTail,
26
27
  } from "../session/streaming-output";
27
28
 
28
29
  import { formatBytes, wrapBrackets } from "./render-utils";
29
- import { renderError, ToolError } from "./tool-errors";
30
+ import { renderError, ToolError, throwIfAborted } from "./tool-errors";
30
31
 
31
32
  /**
32
33
  * Truncation metadata for the output notice.
@@ -1155,6 +1156,19 @@ async function enforceInlineResultBackstop(
1155
1156
  // Tool wrapper
1156
1157
  // =============================================================================
1157
1158
 
1159
+ async function outputRedactionFor(settings: Settings): Promise<readonly string[] | undefined> {
1160
+ const { collectEnvSecrets, loadSecrets } = await import("../secrets");
1161
+ const entries = [...(await loadSecrets(settings.getCwd(), settings.getAgentDir())), ...collectEnvSecrets()];
1162
+ const plain: string[] = [];
1163
+ const seen = new Set<string>();
1164
+ for (const entry of entries) {
1165
+ if (entry.type !== "plain" || entry.content.length === 0 || seen.has(entry.content)) continue;
1166
+ seen.add(entry.content);
1167
+ plain.push(entry.content);
1168
+ }
1169
+ return plain.length > 0 ? plain : undefined;
1170
+ }
1171
+
1158
1172
  async function wrappedExecute(
1159
1173
  this: AgentTool & { [kUnwrappedExecute]: AgentToolExecFn },
1160
1174
  toolCallId: string,
@@ -1166,7 +1180,14 @@ async function wrappedExecute(
1166
1180
  const originalExecute = this[kUnwrappedExecute];
1167
1181
 
1168
1182
  try {
1169
- let result = await originalExecute.call(this, toolCallId, params, signal, onUpdate, context);
1183
+ const settings = context?.settings;
1184
+ let plainSecrets: readonly string[] | undefined;
1185
+ if (settings?.get("secrets.enabled") === true) {
1186
+ plainSecrets = await outputRedactionFor(settings);
1187
+ throwIfAborted(signal);
1188
+ }
1189
+ const run = () => originalExecute.call(this, toolCallId, params, signal, onUpdate, context);
1190
+ let result = plainSecrets ? await runWithOutputRedaction(plainSecrets, run) : await run();
1170
1191
 
1171
1192
  // Spill large results to artifact, truncate to tail
1172
1193
  result = await spillLargeResultToArtifact(result, this.name, context);
@@ -31,7 +31,7 @@ export interface SessionPythonToolInput {
31
31
  /** Resolve the GJC session id used for the kernel owner and transcript paths. */
32
32
  getSessionId: () => string | null;
33
33
  /** Register cleanup with the current logical session lifecycle. */
34
- registerSessionCleanup: (cleanup: () => Promise<void> | void) => (() => void) | void;
34
+ registerSessionCleanup: (cleanup: () => Promise<void> | void) => (() => void) | undefined;
35
35
  /** Reject execution after the owning session has begun disposal. */
36
36
  assertEvalExecutionAllowed?: () => void;
37
37
  /** Track this whole invocation through its transcript append. */
package/src/tools/read.ts CHANGED
@@ -26,9 +26,9 @@ import {
26
26
  formatMiddleElisionMarker,
27
27
  truncateHead,
28
28
  truncateHeadBytes,
29
- truncateLine,
30
29
  truncateMiddleWindows,
31
30
  truncateTailBytes,
31
+ truncateTextWithSecretAwareness,
32
32
  } from "../session/streaming-output";
33
33
  import { fileHyperlink, renderCodeCell, renderMarkdownCell, renderStatusLine, tryResolveInternalUrlSync } from "../tui";
34
34
  import { CachedOutputBlock } from "../tui/output-block";
@@ -253,12 +253,7 @@ function renderReadWindow(
253
253
  const hash = shouldAddHashLines && segment.kind === "lines";
254
254
  const numbers = !hash && shouldAddLineNumbers;
255
255
  const renderContent =
256
- !hash && maxColumns > 0
257
- ? segment.content
258
- .split("\n")
259
- .map(line => truncateLine(line, maxColumns).text)
260
- .join("\n")
261
- : segment.content;
256
+ !hash && maxColumns > 0 ? truncateTextWithSecretAwareness(segment.content, maxColumns).text : segment.content;
262
257
  return formatTextWithMode(renderContent, segment.origin.startLine, hash, numbers);
263
258
  };
264
259
  const marker = formatMiddleElisionMarker(window.elidedLines, window.elidedBytes);
@@ -1906,17 +1901,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1906
1901
  // A partial line is a byte suffix, not a complete source line. Never
1907
1902
  // give it an anchor that could make the preview look fully readable.
1908
1903
  const numbers = !hash && shouldAddLineNumbers && segment.kind === "lines";
1909
- const renderContent =
1910
- !options.raw && !hash && maxColumns > 0
1911
- ? segment.content
1912
- .split("\n")
1913
- .map(line => {
1914
- const result = truncateLine(line, maxColumns);
1915
- if (result.wasTruncated) columnTruncated = maxColumns;
1916
- return result.text;
1917
- })
1918
- .join("\n")
1919
- : segment.content;
1904
+ let renderContent = segment.content;
1905
+ if (!options.raw && !hash && maxColumns > 0) {
1906
+ const result = truncateTextWithSecretAwareness(segment.content, maxColumns);
1907
+ if (result.wasTruncated) columnTruncated = maxColumns;
1908
+ renderContent = result.text;
1909
+ }
1920
1910
  return formatTextWithMode(renderContent, segment.origin.startLine, hash, numbers);
1921
1911
  };
1922
1912
  let text = "";
@@ -2097,12 +2087,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2097
2087
  const collectedLines = streamResult.lines;
2098
2088
  const cacheLines = collectedLines.slice();
2099
2089
  if (!rawSelector && !shouldAddHashLines && maxColumns > 0) {
2100
- for (let i = 0; i < collectedLines.length; i++) {
2101
- const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
2102
- if (wasTruncated) {
2103
- collectedLines[i] = text;
2104
- columnTruncated = maxColumns;
2105
- }
2090
+ const clipped = truncateTextWithSecretAwareness(collectedLines.join("\n"), maxColumns);
2091
+ if (clipped.wasTruncated) {
2092
+ collectedLines.splice(0, collectedLines.length, ...clipped.text.split("\n"));
2093
+ columnTruncated = maxColumns;
2106
2094
  }
2107
2095
  }
2108
2096
 
@@ -3136,11 +3124,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
3136
3124
  const directionalMaxColumns = rawSelector ? 0 : resolveOutputMaxColumns(this.session.settings);
3137
3125
  if (!rawSelector && !shouldAddDirectionalHashLines && directionalMaxColumns > 0) {
3138
3126
  for (const segment of windowSegments(window)) {
3139
- if (
3140
- segment.content
3141
- .split("\n")
3142
- .some(line => truncateLine(line, directionalMaxColumns).wasTruncated)
3143
- ) {
3127
+ if (truncateTextWithSecretAwareness(segment.content, directionalMaxColumns).wasTruncated) {
3144
3128
  columnTruncated = directionalMaxColumns;
3145
3129
  break;
3146
3130
  }
@@ -3268,12 +3252,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
3268
3252
  const shouldAddHashLines = !rawSelector && displayMode.hashLines;
3269
3253
  const maxColumns = resolveOutputMaxColumns(this.session.settings);
3270
3254
  if (!rawSelector && !shouldAddHashLines && maxColumns > 0) {
3271
- for (let i = 0; i < collectedLines.length; i++) {
3272
- const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
3273
- if (wasTruncated) {
3274
- collectedLines[i] = text;
3275
- columnTruncated = maxColumns;
3276
- }
3255
+ const clipped = truncateTextWithSecretAwareness(collectedLines.join("\n"), maxColumns);
3256
+ if (clipped.wasTruncated) {
3257
+ collectedLines.splice(0, collectedLines.length, ...clipped.text.split("\n"));
3258
+ columnTruncated = maxColumns;
3277
3259
  }
3278
3260
  }
3279
3261
 
@@ -0,0 +1,184 @@
1
+ /**
2
+ * `report_issue` — local auto-QA for tool behavior.
3
+ *
4
+ * The model records a tool whose output contradicts that tool's own
5
+ * description. Reports are appended to a local SQLite database
6
+ * (`~/.gjc/tool-issues.db`); nothing leaves the machine. Inspect them with the
7
+ * read tool's SQLite support, e.g.
8
+ * `gjc read '~/.gjc/tool-issues.db?q=SELECT tool, count(*) FROM tool_issues GROUP BY tool'`.
9
+ *
10
+ * Inspired by oh-my-pi's `xd://report_issue` auto-QA device; collector push
11
+ * and consent prompts are intentionally not part of this local-only port.
12
+ */
13
+ import { Database } from "bun:sqlite";
14
+ import * as fs from "node:fs/promises";
15
+ import * as path from "node:path";
16
+ import type { AgentTool, AgentToolResult } from "@gajae-code/agent-core";
17
+ import { getToolIssuesDbPath, logger, prompt, VERSION } from "@gajae-code/utils";
18
+ import * as z from "zod/v4";
19
+ import reportIssueDescription from "../prompts/tools/report-issue.md" with { type: "text" };
20
+ import type { ToolSession } from "./index";
21
+ import { ToolError } from "./tool-errors";
22
+
23
+ /** Upper bound on the stored tool name, in UTF-8 bytes. */
24
+ const MAX_TOOL_BYTES = 128;
25
+ /** Upper bound on the stored report body, in characters. */
26
+ const MAX_REPORT_CHARS = 4_000;
27
+
28
+ const reportIssueSchema = z.object({
29
+ tool: z.string().describe("name of the tool that misbehaved"),
30
+ report: z.string().describe("what was called, what was expected, what happened"),
31
+ });
32
+
33
+ export type ReportIssueToolInput = z.infer<typeof reportIssueSchema>;
34
+
35
+ export interface ReportIssueToolDetails {
36
+ recorded: boolean;
37
+ tool: string;
38
+ }
39
+
40
+ export interface ToolIssueEntry {
41
+ tool: string;
42
+ report: string;
43
+ model: string | null;
44
+ sessionId: string | null;
45
+ }
46
+
47
+ /**
48
+ * Open the database for a single report. Reports are rare, so no connection is cached: a file the
49
+ * user deleted is recreated on the next report, and no handle keeps the file locked between reports.
50
+ */
51
+ async function openToolIssueDb(dbPath: string): Promise<Database> {
52
+ // Only a directory created here gets 0700; an existing shared config/data root keeps its mode.
53
+ await fs.mkdir(path.dirname(dbPath), { recursive: true, mode: 0o700 });
54
+ // Create (or tighten) the database file before SQLite opens it. SQLite's unix VFS creates the
55
+ // WAL/SHM sidecars with the main file's permissions, so they are private from their first byte.
56
+ const handle = await fs.open(dbPath, "a", 0o600);
57
+ try {
58
+ if (process.platform !== "win32") await handle.chmod(0o600);
59
+ } finally {
60
+ await handle.close();
61
+ }
62
+ const db = new Database(dbPath);
63
+ try {
64
+ db.run(`
65
+ PRAGMA busy_timeout=5000;
66
+ PRAGMA journal_mode=WAL;
67
+ CREATE TABLE IF NOT EXISTS tool_issues (
68
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
69
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
70
+ tool TEXT NOT NULL,
71
+ report TEXT NOT NULL,
72
+ model TEXT,
73
+ gjc_version TEXT NOT NULL,
74
+ session_id TEXT
75
+ );
76
+ CREATE INDEX IF NOT EXISTS idx_tool_issues_tool_created ON tool_issues(tool, created_at);
77
+ `);
78
+ } catch (error) {
79
+ db.close();
80
+ throw error;
81
+ }
82
+ return db;
83
+ }
84
+
85
+ function truncateUtf8(text: string, maxBytes: number): string {
86
+ const bytes = Buffer.from(text, "utf8");
87
+ if (bytes.length <= maxBytes) return text;
88
+ // Decoding a cut multi-byte sequence yields U+FFFD; drop it.
89
+ return bytes
90
+ .subarray(0, maxBytes)
91
+ .toString("utf8")
92
+ .replace(/\uFFFD+$/u, "");
93
+ }
94
+
95
+ /**
96
+ * Truncate text by complete Unicode code points, avoiding incomplete surrogate pairs.
97
+ * JavaScript strings use UTF-16 encoding, where astral characters use two code units
98
+ * (high surrogate + low surrogate). We must count actual code points, not UTF-16 code units.
99
+ */
100
+ function truncateByCodePoint(text: string, maxChars: number): string {
101
+ let codePointCount = 0;
102
+ let result = "";
103
+ // Iterate through actual code points using for-of, which handles surrogate pairs correctly
104
+ for (const codePoint of text) {
105
+ if (codePointCount >= maxChars) break;
106
+ result += codePoint;
107
+ codePointCount++;
108
+ }
109
+ return result;
110
+ }
111
+
112
+ function codePointLength(text: string): number {
113
+ let count = 0;
114
+ for (const _ of text) count++;
115
+ return count;
116
+ }
117
+
118
+ /** Normalize and clamp a report to the stored field limits. */
119
+ export function normalizeToolIssue(tool: string, report: string): { tool: string; report: string } {
120
+ const normalizedTool = truncateUtf8(tool.trim(), MAX_TOOL_BYTES);
121
+ const trimmedReport = report.trim();
122
+ const normalizedReport =
123
+ codePointLength(trimmedReport) > MAX_REPORT_CHARS
124
+ ? `${truncateByCodePoint(trimmedReport, MAX_REPORT_CHARS - 1)}…`
125
+ : trimmedReport;
126
+ return { tool: normalizedTool, report: normalizedReport };
127
+ }
128
+
129
+ /** Append one report to the tool-issue database at `dbPath`. */
130
+ export async function recordToolIssue(dbPath: string, entry: ToolIssueEntry): Promise<void> {
131
+ const db = await openToolIssueDb(dbPath);
132
+ try {
133
+ db.run("INSERT INTO tool_issues (tool, report, model, gjc_version, session_id) VALUES (?, ?, ?, ?, ?)", [
134
+ entry.tool,
135
+ entry.report,
136
+ entry.model,
137
+ VERSION,
138
+ entry.sessionId,
139
+ ]);
140
+ } finally {
141
+ db.close();
142
+ }
143
+ }
144
+
145
+ export class ReportIssueTool implements AgentTool<typeof reportIssueSchema, ReportIssueToolDetails> {
146
+ readonly name = "report_issue";
147
+ readonly label = "Report Issue";
148
+ readonly loadMode = "essential" as const;
149
+ readonly description = prompt.render(reportIssueDescription);
150
+ readonly parameters = reportIssueSchema;
151
+ readonly strict = true;
152
+ readonly intent = "omit" as const;
153
+ readonly #session: ToolSession;
154
+ readonly #dbPath: string;
155
+
156
+ constructor(session: ToolSession, dbPath: string = getToolIssuesDbPath()) {
157
+ this.#session = session;
158
+ this.#dbPath = dbPath;
159
+ }
160
+
161
+ async execute(_toolCallId: string, params: ReportIssueToolInput): Promise<AgentToolResult<ReportIssueToolDetails>> {
162
+ const { tool, report } = normalizeToolIssue(params.tool, params.report);
163
+ if (!tool) throw new ToolError("tool must name the tool that misbehaved.");
164
+ if (!report) throw new ToolError("report must describe the unexpected behavior.");
165
+ try {
166
+ await recordToolIssue(this.#dbPath, {
167
+ tool,
168
+ report,
169
+ model: this.#session.getActiveModelString?.() ?? null,
170
+ sessionId: this.#session.getSessionId?.() ?? null,
171
+ });
172
+ } catch (error) {
173
+ logger.warn("report_issue: failed to record tool issue", { error: String(error), dbPath: this.#dbPath });
174
+ return {
175
+ content: [{ type: "text", text: "Report could not be stored locally; continue the task." }],
176
+ details: { recorded: false, tool },
177
+ };
178
+ }
179
+ return {
180
+ content: [{ type: "text", text: "Recorded. Continue the task." }],
181
+ details: { recorded: true, tool },
182
+ };
183
+ }
184
+ }
@@ -8,8 +8,9 @@
8
8
  * producer is available (resumed-from-disk or backward-compat records).
9
9
  */
10
10
  import type { Component } from "@gajae-code/tui";
11
- import { Text } from "@gajae-code/tui";
11
+ import { getKeybindings, Text } from "@gajae-code/tui";
12
12
  import { formatNumber } from "@gajae-code/utils";
13
+ import { isSettingsInitialized, settings } from "../config/settings";
13
14
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
14
15
  import type { Theme } from "../modes/theme/theme";
15
16
  import { providerRetryPhaseLabel } from "../task/provider-retry-status";
@@ -18,21 +19,26 @@ import {
18
19
  formatDuration,
19
20
  formatStatusIcon,
20
21
  getPreviewLines,
22
+ pluralize,
21
23
  replaceTabs,
22
24
  type ToolUIStatus,
23
25
  truncateToWidth,
26
+ wrapTextWithAnsi,
24
27
  } from "./render-utils";
25
28
  import {
26
29
  SUBAGENT_ACTIVITY_BUCKET_MS,
30
+ SUBAGENT_COMPACT_IDLE_MS,
27
31
  type SubagentLiveProgress,
28
32
  type SubagentSnapshot,
29
33
  type SubagentToolDetails,
30
34
  subagentAwaitRenderedStateSignature,
35
+ summarizeSubagentGroup,
31
36
  } from "./subagent";
32
37
 
33
38
  export { subagentAwaitRenderedStateSignature } from "./subagent";
34
39
 
35
40
  const PREVIEW_LINES_COLLAPSED = 1;
41
+ const GUIDANCE_MAX_LINES = 12;
36
42
  const PREVIEW_LINES_EXPANDED = 4;
37
43
  const PREVIEW_LINE_WIDTH = 80;
38
44
 
@@ -114,10 +120,90 @@ function collectProviderDegradationGroups(
114
120
  .sort((a, b) => b.count - a.count || a.provider.localeCompare(b.provider));
115
121
  }
116
122
 
123
+ function compactSubagentPanelEnabled(): boolean {
124
+ return isSettingsInitialized() && settings.get("task.compactSubagentPanel") === true;
125
+ }
126
+
127
+ // One summary line for the whole group. Counts only, so the line stays O(1) in the
128
+ // number of subagents; each subagent is still one expand toggle away.
129
+ function renderCompactSubagentSummary(subagents: readonly SubagentSnapshot[], theme: Theme): string {
130
+ const summary = summarizeSubagentGroup(subagents, Date.now());
131
+ const parts: string[] = [];
132
+ if (summary.running > 0) parts.push(`${summary.running} ${pluralize("subagent", summary.running)} running`);
133
+ // Attention-worthy counts come first so a narrow terminal truncates the rest.
134
+ if (summary.failed > 0) parts.push(`${summary.failed} failed`);
135
+ if (summary.retrying > 0) parts.push(`${summary.retrying} retrying`);
136
+ if (summary.idle > 0) parts.push(`${summary.idle} idle >${SUBAGENT_COMPACT_IDLE_MS / 1000}s`);
137
+ if (summary.queued > 0) parts.push(`${summary.queued} queued`);
138
+ if (summary.paused > 0) parts.push(`${summary.paused} paused`);
139
+ if (summary.completed > 0) parts.push(`${summary.completed} done`);
140
+ if (summary.cancelled > 0) parts.push(`${summary.cancelled} cancelled`);
141
+ return ` ${theme.fg("dim", parts.join(" · "))}`;
142
+ }
143
+
144
+ // At most one warning row in compact mode, however many providers degrade, so the
145
+ // collapsed height stays independent of the subagent count. The expanded view keeps
146
+ // one row per provider.
147
+ function renderCompactDegradationLine(
148
+ groups: ReadonlyArray<{ provider: string; count: number }>,
149
+ theme: Theme,
150
+ ): string | undefined {
151
+ const [worst] = groups;
152
+ if (!worst) return undefined;
153
+ const more = groups.length > 1 ? ` (+${groups.length - 1} more ${pluralize("provider", groups.length - 1)})` : "";
154
+ return ` ${theme.fg("warning", `provider degraded: ${worst.count} subagents retrying on ${worst.provider}${more}`)}`;
155
+ }
156
+
157
+ // The expand hint gets its own line so a long summary never truncates it away.
158
+ function renderCompactSubagentHint(running: boolean, theme: Theme): string {
159
+ const expandKey = getKeybindings().getKeys("app.tools.expand")[0];
160
+ const hints = [expandKey ? `${expandKey} to list each` : "expand tool output to list each"];
161
+ if (running) hints.push("ctrl+s to observe sessions");
162
+ return ` ${theme.fg("dim", `(${hints.join(" · ")})`)}`;
163
+ }
164
+
117
165
  function boundSubagentBodyLines(lines: string[], width: number): string[] {
118
166
  return lines.map(line => (line.length > 0 ? truncateToWidth(replaceTabs(line), width, Ellipsis.Omit) : ""));
119
167
  }
120
168
 
169
+ // Guidance is an instruction the caller must act on (e.g. "inspect before
170
+ // cancelling" after an await timeout), so it wraps to the terminal width instead
171
+ // of being cut to one preview line.
172
+ function renderSubagentGuidanceLines(snapshot: SubagentSnapshot, width: number, theme: Theme): string[] {
173
+ const guidance = snapshot.guidance?.trim();
174
+ if (!guidance) return [];
175
+ const indent = " ";
176
+ const wrapWidth = Math.max(1, width - indent.length);
177
+ const wrapped = guidance.split("\n").flatMap(segment => wrapTextWithAnsi(replaceTabs(segment), wrapWidth));
178
+ // Still bounded: producer guidance is a short fixed sentence, so the cap only
179
+ // bites on pathologically narrow terminals.
180
+ if (wrapped.length > GUIDANCE_MAX_LINES) {
181
+ wrapped.length = GUIDANCE_MAX_LINES;
182
+ wrapped[GUIDANCE_MAX_LINES - 1] = truncateToWidth(
183
+ `${wrapped[GUIDANCE_MAX_LINES - 1]!}…`,
184
+ wrapWidth,
185
+ Ellipsis.Unicode,
186
+ );
187
+ }
188
+ return wrapped.map(line => `${indent}${theme.fg("dim", line)}`);
189
+ }
190
+
191
+ function renderBoundSubagentBody(
192
+ snapshot: SubagentSnapshot,
193
+ expanded: boolean,
194
+ width: number,
195
+ theme: Theme,
196
+ staticTime: boolean,
197
+ ): string[] {
198
+ return boundSubagentBodyLines(
199
+ [
200
+ ...renderSubagentSnapshotBody(snapshot, expanded, theme, staticTime),
201
+ ...renderSubagentGuidanceLines(snapshot, width, theme),
202
+ ],
203
+ width,
204
+ );
205
+ }
206
+
121
207
  function renderCachedSubagentBody(
122
208
  snapshot: SubagentSnapshot,
123
209
  signature: string,
@@ -133,7 +219,7 @@ function renderCachedSubagentBody(
133
219
  subagentBodyCache.set(key, hit);
134
220
  return hit;
135
221
  }
136
- const lines = boundSubagentBodyLines(renderSubagentSnapshotBody(snapshot, expanded, theme), width);
222
+ const lines = renderBoundSubagentBody(snapshot, expanded, width, theme, true);
137
223
  subagentBodyRenderCount += 1;
138
224
  subagentBodyCache.set(key, lines);
139
225
  if (subagentBodyCache.size > SUBAGENT_BODY_CACHE_MAX) {
@@ -150,7 +236,7 @@ function renderDynamicSubagentBody(
150
236
  theme: Theme,
151
237
  ): string[] {
152
238
  subagentBodyRenderCount += 1;
153
- return boundSubagentBodyLines(renderSubagentSnapshotBody(snapshot, expanded, theme, false), width);
239
+ return renderBoundSubagentBody(snapshot, expanded, width, theme, false);
154
240
  }
155
241
 
156
242
  function statusIconKind(status: SubagentSnapshot["status"]): ToolUIStatus {
@@ -363,11 +449,7 @@ function renderSubagentSnapshotBody(
363
449
  }
364
450
  }
365
451
 
366
- if (snapshot.guidance) {
367
- for (const line of getPreviewLines(snapshot.guidance, 1, PREVIEW_LINE_WIDTH, Ellipsis.Unicode)) {
368
- lines.push(` ${theme.fg("dim", replaceTabs(line))}`);
369
- }
370
- }
452
+ // `snapshot.guidance` is appended by renderBoundSubagentBody, wrapped to width.
371
453
 
372
454
  return lines;
373
455
  }
@@ -421,6 +503,29 @@ export const subagentToolRenderer = {
421
503
  theme,
422
504
  );
423
505
  const out: string[] = [truncateToWidth(replaceTabs(header), width, Ellipsis.Omit)];
506
+ const liveProgress = subagents.flatMap(snapshot =>
507
+ snapshot.progress && snapshot.liveProgressAvailable !== false ? [snapshot.progress] : [],
508
+ );
509
+ const degradationGroups = collectProviderDegradationGroups(liveProgress);
510
+ // Compact applies only to live await progress. Final results (timeout
511
+ // guidance, failure and completion receipts, status/steer replies) keep
512
+ // the full per-subagent rendering.
513
+ if (!expanded && options.isPartial && compactSubagentPanelEnabled()) {
514
+ out.push(
515
+ truncateToWidth(replaceTabs(renderCompactSubagentSummary(subagents, theme)), width, Ellipsis.Unicode),
516
+ );
517
+ const compactDegradation = renderCompactDegradationLine(degradationGroups, theme);
518
+ if (compactDegradation)
519
+ out.push(truncateToWidth(replaceTabs(compactDegradation), width, Ellipsis.Unicode));
520
+ out.push(
521
+ truncateToWidth(
522
+ replaceTabs(renderCompactSubagentHint(runningCount > 0, theme)),
523
+ width,
524
+ Ellipsis.Unicode,
525
+ ),
526
+ );
527
+ return out;
528
+ }
424
529
  // Discoverability: the inline panel is a bounded preview; the session
425
530
  // observer (ctrl+s) streams the full per-subagent message history.
426
531
  if (runningCount > 0) {
@@ -432,10 +537,7 @@ export const subagentToolRenderer = {
432
537
  ),
433
538
  );
434
539
  }
435
- const liveProgress = subagents.flatMap(snapshot =>
436
- snapshot.progress && snapshot.liveProgressAvailable !== false ? [snapshot.progress] : [],
437
- );
438
- for (const group of collectProviderDegradationGroups(liveProgress)) {
540
+ for (const group of degradationGroups) {
439
541
  out.push(
440
542
  truncateToWidth(
441
543
  replaceTabs(