@gajae-code/coding-agent 0.11.3 → 0.11.5

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 (213) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/cli/worktree-cli.d.ts +8 -25
  4. package/dist/types/commands/gc.d.ts +4 -0
  5. package/dist/types/commands/worktree.d.ts +7 -32
  6. package/dist/types/config/settings-schema.d.ts +20 -0
  7. package/dist/types/export/html/template.generated.d.ts +1 -1
  8. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  9. package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
  10. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  11. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  12. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  13. package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
  14. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  15. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  16. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  17. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  18. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  19. package/dist/types/main.d.ts +2 -2
  20. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  21. package/dist/types/modes/components/custom-editor.d.ts +8 -3
  22. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  23. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
  26. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  27. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  28. package/dist/types/modes/controllers/input-controller.d.ts +8 -2
  29. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  30. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  31. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  32. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  33. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  34. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  35. package/dist/types/sdk/broker/broker.d.ts +1 -0
  36. package/dist/types/sdk/broker/discovery.d.ts +10 -0
  37. package/dist/types/sdk/broker/ensure.d.ts +24 -1
  38. package/dist/types/sdk/broker/internal.d.ts +3 -0
  39. package/dist/types/sdk/broker/session-index.d.ts +12 -0
  40. package/dist/types/sdk/broker/state-version.d.ts +2 -1
  41. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
  43. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  44. package/dist/types/sdk/bus/config.d.ts +22 -0
  45. package/dist/types/sdk/bus/index.d.ts +162 -2
  46. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  47. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  48. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  52. package/dist/types/sdk/session.d.ts +16 -1
  53. package/dist/types/secrets/index.d.ts +4 -2
  54. package/dist/types/secrets/obfuscator.d.ts +1 -1
  55. package/dist/types/session/agent-session.d.ts +31 -2
  56. package/dist/types/session/artifacts.d.ts +30 -8
  57. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  58. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  59. package/dist/types/session/session-manager.d.ts +88 -18
  60. package/dist/types/session/session-storage.d.ts +27 -0
  61. package/dist/types/session/streaming-output.d.ts +4 -0
  62. package/dist/types/task/executor.d.ts +9 -0
  63. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  64. package/dist/types/tools/ask.d.ts +87 -12
  65. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  66. package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
  67. package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
  68. package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
  69. package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
  70. package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/path-utils.d.ts +9 -14
  73. package/dist/types/tools/render-utils.d.ts +1 -8
  74. package/dist/types/tools/resource-gc.d.ts +14 -0
  75. package/dist/types/utils/image-loading.d.ts +23 -0
  76. package/dist/types/utils/image-resize.d.ts +2 -0
  77. package/dist/types/utils/pasted-image-loading.d.ts +39 -0
  78. package/dist/types/utils/pasted-image-path.d.ts +21 -19
  79. package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
  80. package/dist/types/web/scrapers/types.d.ts +1 -5
  81. package/package.json +25 -9
  82. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  83. package/scripts/run-threat-model-check.ts +60 -0
  84. package/src/cli/notify-cli.ts +40 -18
  85. package/src/cli/worktree-cli.ts +9 -287
  86. package/src/cli.ts +10 -9
  87. package/src/commands/gc.ts +11 -1
  88. package/src/commands/sdk.ts +10 -4
  89. package/src/commands/worktree.ts +10 -53
  90. package/src/config/model-profiles.ts +15 -15
  91. package/src/config/settings-schema.ts +17 -0
  92. package/src/daemon/runtime.ts +2 -2
  93. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  94. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  95. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  96. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  97. package/src/eval/js/shared/runtime.ts +1 -1
  98. package/src/eval/py/index.ts +2 -2
  99. package/src/export/html/template.generated.ts +1 -1
  100. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  101. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  102. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  103. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  104. package/src/gjc-runtime/gc-render.ts +19 -1
  105. package/src/gjc-runtime/gc-runtime.ts +68 -3
  106. package/src/gjc-runtime/launch-tmux.ts +61 -6
  107. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  108. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  109. package/src/gjc-runtime/state-argv.ts +292 -0
  110. package/src/gjc-runtime/state-runtime.ts +80 -222
  111. package/src/gjc-runtime/team-runtime.ts +1 -1
  112. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  113. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  114. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  115. package/src/harness-control-plane/owner.ts +59 -3
  116. package/src/hindsight/client.ts +65 -2
  117. package/src/internal-urls/agent-protocol.ts +76 -5
  118. package/src/internal-urls/docs-index.generated.ts +7 -6
  119. package/src/internal-urls/local-protocol.ts +416 -18
  120. package/src/lsp/index.ts +4 -88
  121. package/src/main.ts +37 -13
  122. package/src/modes/acp/acp-agent.ts +8 -0
  123. package/src/modes/components/ansi-display-validator.ts +54 -0
  124. package/src/modes/components/custom-editor.ts +103 -32
  125. package/src/modes/components/custom-provider-wizard.ts +26 -1
  126. package/src/modes/components/notifications-settings-editor.ts +26 -2
  127. package/src/modes/components/session-observer-overlay.ts +40 -37
  128. package/src/modes/components/settings-selector.ts +3 -1
  129. package/src/modes/components/tool-transcript-format.ts +393 -0
  130. package/src/modes/components/transcript-viewer-overlay.ts +63 -1
  131. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  132. package/src/modes/controllers/input-controller.ts +98 -37
  133. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  134. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  135. package/src/modes/controllers/selector-controller.ts +71 -9
  136. package/src/modes/interactive-mode.ts +14 -2
  137. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  138. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  139. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  140. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  141. package/src/plan-mode/approved-plan.ts +3 -1
  142. package/src/prompts/agents/architect.md +1 -0
  143. package/src/prompts/agents/critic.md +1 -0
  144. package/src/prompts/system/custom-system-prompt.md +1 -1
  145. package/src/prompts/system/system-prompt.md +2 -1
  146. package/src/prompts/tools/lsp.md +3 -3
  147. package/src/runtime-mcp/manager.ts +16 -1
  148. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  149. package/src/sdk/broker/broker.ts +139 -32
  150. package/src/sdk/broker/discovery.ts +125 -0
  151. package/src/sdk/broker/ensure.ts +103 -19
  152. package/src/sdk/broker/internal.ts +10 -0
  153. package/src/sdk/broker/session-index.ts +309 -38
  154. package/src/sdk/broker/state-version.ts +5 -2
  155. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  156. package/src/sdk/bus/chat-daemon-control.ts +526 -130
  157. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  158. package/src/sdk/bus/config-commands.ts +22 -0
  159. package/src/sdk/bus/config.ts +37 -0
  160. package/src/sdk/bus/index.ts +384 -92
  161. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  162. package/src/sdk/bus/notification-service.ts +284 -62
  163. package/src/sdk/bus/recent-activity.ts +14 -5
  164. package/src/sdk/bus/session-control.ts +150 -44
  165. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  166. package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
  167. package/src/sdk/bus/telegram-daemon-control.ts +294 -76
  168. package/src/sdk/bus/telegram-daemon.ts +1842 -281
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/session.ts +155 -27
  171. package/src/secrets/index.ts +53 -10
  172. package/src/secrets/obfuscator.ts +21 -18
  173. package/src/secrets/regex.ts +2 -0
  174. package/src/session/agent-session.ts +495 -253
  175. package/src/session/artifacts.ts +188 -23
  176. package/src/session/context-estimation.ts +7 -2
  177. package/src/session/internal/managed-session-scope.ts +215 -39
  178. package/src/session/internal/managed-session-storage.ts +1152 -46
  179. package/src/session/session-manager.ts +1055 -148
  180. package/src/session/session-storage.ts +109 -11
  181. package/src/session/streaming-output.ts +6 -0
  182. package/src/task/executor.ts +71 -26
  183. package/src/task/index.ts +63 -20
  184. package/src/tool-discovery/tool-index.ts +18 -0
  185. package/src/tools/ask.ts +329 -57
  186. package/src/tools/bash-allowed-prefixes.ts +79 -29
  187. package/src/tools/bash-skill-urls.ts +14 -3
  188. package/src/tools/bash.ts +2 -2
  189. package/src/tools/browser/launch.ts +3 -1
  190. package/src/tools/browser/readable.ts +1 -1
  191. package/src/tools/browser/screenshot-format.ts +34 -0
  192. package/src/tools/browser/tab-protocol.ts +3 -0
  193. package/src/tools/browser/tab-supervisor.ts +125 -122
  194. package/src/tools/browser/tab-worker-entry.ts +33 -14
  195. package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
  196. package/src/tools/browser/tab-worker-smoke.ts +87 -0
  197. package/src/tools/browser/tab-worker.ts +7 -5
  198. package/src/tools/index.ts +4 -0
  199. package/src/tools/path-utils.ts +42 -35
  200. package/src/tools/plan-mode-guard.ts +1 -0
  201. package/src/tools/read.ts +7 -1
  202. package/src/tools/render-utils.ts +1 -27
  203. package/src/tools/resource-gc.ts +123 -34
  204. package/src/tools/sqlite-reader.ts +3 -2
  205. package/src/tools/subagent.ts +2 -0
  206. package/src/utils/image-loading.ts +85 -34
  207. package/src/utils/image-resize.ts +16 -9
  208. package/src/utils/pasted-image-loading.ts +514 -0
  209. package/src/utils/pasted-image-path.ts +223 -98
  210. package/src/web/scrapers/html-to-markdown.ts +68 -0
  211. package/src/web/scrapers/readthedocs.ts +1 -1
  212. package/src/web/scrapers/types.ts +1 -68
  213. package/src/web/search/providers/openai-compatible.ts +76 -23
@@ -8,10 +8,13 @@ import type { SessionMessageEntry } from "../../session/session-manager";
8
8
  import { parseSessionEntries } from "../../session/session-manager";
9
9
  import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
10
10
  import { theme } from "../theme/theme";
11
+ import {
12
+ buildToolTranscriptEntry,
13
+ composeToolText,
14
+ createToolTranscriptRenderDescriptor,
15
+ } from "./tool-transcript-format";
11
16
  import { type TranscriptViewerEntry, TranscriptViewerOverlay } from "./transcript-viewer-overlay";
12
17
 
13
- const MAX_TOOL_ARGS_CHARS = 500;
14
-
15
18
  /** Session-observer adapter. The shared viewer owns navigation and fold state. */
16
19
  export class SessionObserverOverlayComponent extends TranscriptViewerOverlay {
17
20
  #registry: SessionObserverRegistry;
@@ -149,7 +152,7 @@ export class SessionObserverOverlayComponent extends TranscriptViewerOverlay {
149
152
  }
150
153
  }
151
154
 
152
- function entriesFromMessages(entries: readonly SessionMessageEntry[]): TranscriptViewerEntry[] {
155
+ export function entriesFromMessages(entries: readonly SessionMessageEntry[]): TranscriptViewerEntry[] {
153
156
  const results = new Map<string, ToolResultMessage>();
154
157
  for (const entry of entries)
155
158
  if (entry.message.role === "toolResult") results.set(entry.message.toolCallId, entry.message);
@@ -191,25 +194,42 @@ function entriesFromMessages(entries: readonly SessionMessageEntry[]): Transcrip
191
194
  .map(part => part.text)
192
195
  .join("\n")
193
196
  .trim() ?? "";
194
- const summary = formatToolArgs(content.name, content.arguments);
195
- const callText = [summary, content.intent].filter(Boolean).join("\n");
196
- const resultDisplay = result?.isError ? `✗ ${resultText || "Error"}` : resultText || "✓ done";
197
- output.push({
198
- id: `tool:${content.id}`,
199
- kind: "tool",
200
- label: content.name,
201
- payload: {
202
- text: [callText, resultDisplay].filter(Boolean).join("\n"),
203
- metadata: {
197
+ const canonicalPayload = {
198
+ text: composeToolText({
199
+ name: content.name,
200
+ args: content.arguments,
201
+ intent: content.intent,
202
+ resultText,
203
+ isError: result?.isError ?? false,
204
+ hasResult: results.has(content.id),
205
+ }),
206
+ metadata: {
207
+ name: content.name,
208
+ arguments: content.arguments,
209
+ intent: content.intent,
210
+ resultText,
211
+ isError: result?.isError ?? false,
212
+ hasResult: results.has(content.id),
213
+ detailsData: result?.details,
214
+ },
215
+ source: { call: content, result },
216
+ };
217
+ output.push(
218
+ buildToolTranscriptEntry({
219
+ canonicalPayload,
220
+ renderDescriptor: createToolTranscriptRenderDescriptor({
204
221
  name: content.name,
205
- arguments: content.arguments,
222
+ args: content.arguments,
206
223
  intent: content.intent,
207
- isError: result?.isError ?? false,
208
- },
209
- source: { call: content, result },
210
- },
211
- foldable: true,
212
- });
224
+ resultContent: resultText,
225
+ isError: result?.isError,
226
+ hasResult: results.has(content.id),
227
+ detailsData: result?.details,
228
+ }),
229
+ capabilities: { copyable: true, foldable: true, rawViewable: true },
230
+ identity: { id: `tool:${content.id}`, label: content.name, display: "full" },
231
+ }),
232
+ );
213
233
  }
214
234
  });
215
235
  }
@@ -234,23 +254,6 @@ function entriesFromMessages(entries: readonly SessionMessageEntry[]): Transcrip
234
254
  return output;
235
255
  }
236
256
 
237
- function formatToolArgs(name: string, args: Record<string, unknown>): string {
238
- if (name === "read" || name === "write" || name === "edit") return args.path ? `path: ${args.path}` : "";
239
- if (name === "bash") return typeof args.command === "string" ? args.command.replaceAll("\t", " ") : "";
240
- if (name === "search")
241
- return [
242
- args.pattern ? `pattern: ${args.pattern}` : "",
243
- Array.isArray(args.paths) ? `paths: ${args.paths.join(", ")}` : "",
244
- ]
245
- .filter(Boolean)
246
- .join(", ");
247
- return Object.entries(args)
248
- .filter(([key]) => !key.startsWith("_"))
249
- .map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`)
250
- .join(", ")
251
- .slice(0, MAX_TOOL_ARGS_CHARS);
252
- }
253
-
254
257
  function truncateThinking(text: string, expanded: boolean): string {
255
258
  const limit = expanded ? 4_000 : 200;
256
259
  return text.length > limit ? `${text.slice(0, limit)}...` : text;
@@ -669,6 +669,8 @@ export interface SettingsRuntimeContext {
669
669
  cwd: string;
670
670
  /** Whether this terminal can render the pet overlay. */
671
671
  petAvailable?: boolean;
672
+ /** Terminal environment used to select unavailable-pet guidance. Omitted in production to use Bun.env. */
673
+ terminalEnv?: NodeJS.ProcessEnv;
672
674
  }
673
675
 
674
676
  /** Status line settings subset for preview */
@@ -896,7 +898,7 @@ export class SettingsSelectorComponent extends Container {
896
898
  options = createPetSelectItems(options, currentValue, petAvailable);
897
899
  // Unsupported terminals must see the same actionable guidance the
898
900
  // startup notice and /pet show, not only dimmed option descriptions.
899
- if (!petAvailable) description = getPetUnavailableWarning();
901
+ if (!petAvailable) description = getPetUnavailableWarning(this.context.terminalEnv);
900
902
  }
901
903
  // Preview handlers
902
904
  let onPreview: ((value: string) => void | Promise<void>) | undefined;
@@ -0,0 +1,393 @@
1
+ import { wrapTextWithAnsi } from "@gajae-code/tui";
2
+ import { sanitizeText } from "@gajae-code/utils";
3
+ import { renderJsonTreeLines } from "../../tools/json-tree";
4
+ import { renderStatusLine } from "../../tui";
5
+ import type { Theme } from "../theme/theme";
6
+ import { validateDisplayLine } from "./ansi-display-validator";
7
+ import { renderDiff } from "./diff";
8
+
9
+ export const MAX_TOOL_ARGS_CHARS = 500;
10
+ export const TOOL_RESULT_MAX_EXPANDED_LINES = 100;
11
+ export const INPUT_MAX_SOURCE_BYTES = 1_048_576;
12
+ export const INPUT_MAX_SOURCE_LINES = 50_000;
13
+ export const INPUT_MAX_SCALAR_LEN = 8_192;
14
+ export const INPUT_MAX_JSON_DEPTH = 32;
15
+ export const INPUT_MAX_JSON_NODES = 20_000;
16
+
17
+ export type ToolDisplaySections = {
18
+ callLines: string[];
19
+ statusLines: string[];
20
+ resultText: string;
21
+ };
22
+
23
+ /**
24
+ * Produces final, terminal-safe rich display lines for an expanded tool entry.
25
+ * It owns validation, width-aware wrapping, and the result-only visible cap.
26
+ */
27
+ export function renderToolDisplayLines(
28
+ descriptor: ToolTranscriptRenderDescriptor,
29
+ contentWidth: number,
30
+ theme: Theme,
31
+ ): string[] {
32
+ const bounded =
33
+ descriptor.inputTruncated || exceedsInputBudget(descriptor) ? truncatedDescriptor(descriptor) : descriptor;
34
+ const sections = bounded.inputTruncated ? truncatedPlainSections(bounded) : richSections(bounded, theme);
35
+ const wrap = (lines: string[]) =>
36
+ balanceWrappedSgr(lines.flatMap(line => wrapTextWithAnsi(validateDisplayLine(line), Math.max(1, contentWidth))));
37
+ const result = cappedResultLines(sections.resultText);
38
+ const resultLines = wrap(result.lines);
39
+ const shown = resultLines.slice(0, TOOL_RESULT_MAX_EXPANDED_LINES);
40
+ if (result.omittedLineCount > 0) shown.push(`... ${result.omittedLineCount} more lines`);
41
+ else if (resultLines.length > TOOL_RESULT_MAX_EXPANDED_LINES)
42
+ shown.push(`... ${resultLines.length - TOOL_RESULT_MAX_EXPANDED_LINES} more lines`);
43
+ return [...wrap(sections.callLines), ...wrap(sections.statusLines), ...shown];
44
+ }
45
+
46
+ function truncatedPlainSections(descriptor: ToolTranscriptRenderDescriptor): ToolDisplaySections {
47
+ return {
48
+ callLines: [descriptor.name, ...composeToolCall(descriptor).split("\n").filter(Boolean)],
49
+ statusLines: [!descriptor.hasResult ? "⏳ pending" : descriptor.isError ? "✗ Error" : "✓ done"],
50
+ resultText: `... input truncated for rendering (press r for raw)\n${boundedPlainResult(descriptor.resultContent)}`,
51
+ };
52
+ }
53
+
54
+ function truncatedDescriptor(descriptor: ToolTranscriptRenderDescriptor): ToolTranscriptRenderDescriptor {
55
+ return {
56
+ ...descriptor,
57
+ name: descriptor.name.slice(0, 256),
58
+ args: exceedsStructuredInputBudget(descriptor.args) ? {} : descriptor.args,
59
+ intent: exceedsScalarInputBudget(descriptor.intent) ? undefined : descriptor.intent,
60
+ resultContent: boundedPlainResult(descriptor.resultContent),
61
+ details: undefined,
62
+ detailsData: undefined,
63
+ inputTruncated: true,
64
+ };
65
+ }
66
+
67
+ function balanceWrappedSgr(lines: string[]): string[] {
68
+ let active: string[] = [];
69
+ return lines.map(line => {
70
+ const prefix = active.join("");
71
+ const matches = [...line.matchAll(/\x1b\[([0-9;]*)m/g)];
72
+ for (const match of matches) {
73
+ if (
74
+ match[1]!
75
+ .split(";")
76
+ .some(parameter => ["0", "22", "23", "24", "25", "27", "28", "29", "39", "49"].includes(parameter))
77
+ )
78
+ active = [];
79
+ else active = [match[0]];
80
+ }
81
+ const last = matches.at(-1);
82
+ return `${prefix}${line}${prefix || (last && !last[1]!.split(";").includes("0")) ? "\x1b[0m" : ""}`;
83
+ });
84
+ }
85
+
86
+ function plainSections(descriptor: ToolTranscriptRenderDescriptor, theme: Theme): ToolDisplaySections {
87
+ return {
88
+ callLines: [theme.fg("accent", descriptor.name), ...composeToolCall(descriptor).split("\n").filter(Boolean)],
89
+ statusLines: [statusLine(descriptor, theme)],
90
+ resultText: boundedPlainResult(descriptor.resultContent),
91
+ };
92
+ }
93
+
94
+ function richSections(descriptor: ToolTranscriptRenderDescriptor, theme: Theme): ToolDisplaySections {
95
+ const sections = plainSections(descriptor, theme);
96
+ const diff = extractDiff(descriptor.detailsData) ?? descriptor.details;
97
+ if ((descriptor.name === "edit" || descriptor.name === "write" || descriptor.name === "apply_patch") && diff) {
98
+ sections.resultText = renderDiff(diff, { filePath: extractPath(descriptor.detailsData) });
99
+ } else if (descriptor.detailsData && typeof descriptor.detailsData === "object") {
100
+ sections.resultText = renderJsonTreeLines(
101
+ descriptor.detailsData,
102
+ theme,
103
+ INPUT_MAX_JSON_DEPTH,
104
+ TOOL_RESULT_MAX_EXPANDED_LINES * 2,
105
+ INPUT_MAX_SCALAR_LEN,
106
+ ).lines.join("\n");
107
+ }
108
+ return sections;
109
+ }
110
+
111
+ function statusLine(descriptor: ToolTranscriptRenderDescriptor, theme: Theme): string {
112
+ const status = !descriptor.hasResult
113
+ ? "⏳ pending"
114
+ : descriptor.isError
115
+ ? `✗ ${descriptor.resultContent.split("\n", 1)[0]?.trim() || "Error"}`
116
+ : "✓ done";
117
+ return renderStatusLine(
118
+ { title: status, titleColor: descriptor.isError ? "error" : descriptor.hasResult ? "success" : "warning" },
119
+ theme,
120
+ );
121
+ }
122
+
123
+ function boundedPlainResult(text: string): string {
124
+ const boundedBytes =
125
+ Buffer.byteLength(text) > INPUT_MAX_SOURCE_BYTES
126
+ ? Buffer.from(text).subarray(0, INPUT_MAX_SOURCE_BYTES).toString()
127
+ : text;
128
+ let end = boundedBytes.length;
129
+ let lineCount = 1;
130
+ for (let index = 0; index < boundedBytes.length; index++) {
131
+ if (boundedBytes[index] !== "\n") continue;
132
+ if (lineCount === INPUT_MAX_SOURCE_LINES) {
133
+ end = index;
134
+ break;
135
+ }
136
+ lineCount += 1;
137
+ }
138
+ return boundedBytes.slice(0, end);
139
+ }
140
+
141
+ function cappedResultLines(result: string): { lines: string[]; omittedLineCount: number } {
142
+ let lineCount = 1;
143
+ let prefixEnd = result.length;
144
+ for (let index = 0; index < result.length; index++) {
145
+ if (result[index] !== "\n") continue;
146
+ if (lineCount === TOOL_RESULT_MAX_EXPANDED_LINES) prefixEnd = index;
147
+ lineCount += 1;
148
+ }
149
+ return {
150
+ lines: result.slice(0, prefixEnd).split("\n"),
151
+ omittedLineCount: Math.max(0, lineCount - TOOL_RESULT_MAX_EXPANDED_LINES),
152
+ };
153
+ }
154
+
155
+ function extractDiff(value: unknown): string | undefined {
156
+ if (!value || typeof value !== "object") return undefined;
157
+ const record = value as Record<string, unknown>;
158
+ if (typeof record.diff === "string") return record.diff;
159
+ if (Array.isArray(record.perFileResults))
160
+ return record.perFileResults
161
+ .map(item =>
162
+ item && typeof item === "object" && typeof (item as Record<string, unknown>).diff === "string"
163
+ ? (item as Record<string, unknown>).diff
164
+ : "",
165
+ )
166
+ .filter(Boolean)
167
+ .join("\n");
168
+ return undefined;
169
+ }
170
+
171
+ function extractPath(value: unknown): string | undefined {
172
+ if (!value || typeof value !== "object") return undefined;
173
+ const record = value as Record<string, unknown>;
174
+ return typeof record.path === "string" ? record.path : undefined;
175
+ }
176
+
177
+ /**
178
+ * Field-specific input budgets:
179
+ * - resultContent is bounded by total bytes and lines.
180
+ * - args and detailsData are bounded by cumulative bytes, per-scalar length, depth, and nodes.
181
+ * - intent and details are bounded by per-scalar length.
182
+ */
183
+ function exceedsInputBudget(descriptor: ToolTranscriptRenderDescriptor): boolean {
184
+ return (
185
+ exceedsResultContentBudget(descriptor.resultContent) ||
186
+ exceedsStructuredInputBudget(descriptor.args) ||
187
+ exceedsScalarInputBudget(descriptor.intent) ||
188
+ exceedsScalarInputBudget(descriptor.details) ||
189
+ exceedsStructuredInputBudget(descriptor.detailsData)
190
+ );
191
+ }
192
+
193
+ function exceedsResultContentBudget(value: string): boolean {
194
+ return Buffer.byteLength(value) > INPUT_MAX_SOURCE_BYTES || value.split("\n").length - 1 > INPUT_MAX_SOURCE_LINES;
195
+ }
196
+
197
+ function exceedsScalarInputBudget(value: unknown): boolean {
198
+ return typeof value === "string" && value.length > INPUT_MAX_SCALAR_LEN;
199
+ }
200
+
201
+ function exceedsStructuredInputBudget(value: unknown): boolean {
202
+ let nodes = 0;
203
+ let bytes = 0;
204
+ let exceeded = false;
205
+ const visit = (item: unknown, depth: number): void => {
206
+ if (exceeded) return;
207
+ nodes++;
208
+ if (nodes > INPUT_MAX_JSON_NODES || depth > INPUT_MAX_JSON_DEPTH) {
209
+ exceeded = true;
210
+ return;
211
+ }
212
+ if (typeof item === "string") {
213
+ bytes += Buffer.byteLength(item);
214
+ if (item.length > INPUT_MAX_SCALAR_LEN || bytes > INPUT_MAX_SOURCE_BYTES) exceeded = true;
215
+ return;
216
+ }
217
+ if (Array.isArray(item)) for (const child of item) visit(child, depth + 1);
218
+ else if (item && typeof item === "object") for (const child of Object.values(item)) visit(child, depth + 1);
219
+ };
220
+ visit(value, 0);
221
+ return exceeded;
222
+ }
223
+
224
+ export type ToolTranscriptRenderDescriptor = Readonly<{
225
+ name: string;
226
+ args: Record<string, unknown>;
227
+ intent?: string;
228
+ resultContent: string;
229
+ details?: string;
230
+ detailsData?: unknown;
231
+ isError: boolean;
232
+ isPartial: boolean;
233
+ hasResult: boolean;
234
+ inputTruncated: boolean;
235
+ }>;
236
+
237
+ export type BuildToolTranscriptEntryInput = {
238
+ canonicalPayload: import("../transcript-item-registry").TranscriptSourcePayload;
239
+ renderDescriptor: ToolTranscriptRenderDescriptor;
240
+ capabilities: import("../transcript-item-registry").TranscriptItemCapabilities;
241
+ identity: { id: string; label: string; display?: "full" };
242
+ };
243
+
244
+ /**
245
+ * Shared tool projection for the main transcript and session observer.
246
+ * `payload` is canonical/copyable source; display is derived only from the
247
+ * sanitized, frozen descriptor. The observer's whole-entry 100-line cap and
248
+ * the main view's tool-only result cap intentionally remain surface-specific.
249
+ */
250
+ export function buildToolTranscriptEntry(
251
+ input: BuildToolTranscriptEntryInput,
252
+ ): import("./transcript-viewer-overlay").TranscriptViewerEntry {
253
+ const { canonicalPayload: payload, renderDescriptor, capabilities, identity } = input;
254
+ const fields: ToolTranscriptFields = {
255
+ name: renderDescriptor.name,
256
+ args: renderDescriptor.args,
257
+ intent: renderDescriptor.intent,
258
+ resultText: renderDescriptor.resultContent,
259
+ isError: renderDescriptor.isError,
260
+ hasResult: renderDescriptor.hasResult,
261
+ };
262
+ return {
263
+ id: identity.id,
264
+ kind: "tool",
265
+ label: identity.label,
266
+ payload,
267
+ renderDescriptor,
268
+ ...capabilities,
269
+ richRenderEligible: identity.display !== "full",
270
+ getDisplayText: expanded =>
271
+ identity.display === "full" ? composeToolText(fields) : toolDisplayText(fields, expanded),
272
+ };
273
+ }
274
+
275
+ /** Sanitizes and freezes all display-only values before tool formatting. */
276
+ export function createToolTranscriptRenderDescriptor(input: {
277
+ name: unknown;
278
+ args: unknown;
279
+ intent?: unknown;
280
+ resultContent: unknown;
281
+ details?: unknown;
282
+ detailsData?: unknown;
283
+ isError?: unknown;
284
+ isPartial?: unknown;
285
+ hasResult?: unknown;
286
+ inputTruncated?: unknown;
287
+ }): ToolTranscriptRenderDescriptor {
288
+ const argsTruncated = exceedsStructuredInputBudget(input.args);
289
+ const detailsDataTruncated = exceedsStructuredInputBudget(input.detailsData);
290
+ return freezeDisplayValue({
291
+ name: sanitizeString(input.name),
292
+ args: argsTruncated ? {} : ((sanitizeDisplayValue(input.args) ?? {}) as Record<string, unknown>),
293
+ intent: typeof input.intent === "string" ? sanitizeString(input.intent) : undefined,
294
+ resultContent: sanitizeString(input.resultContent),
295
+ details: typeof input.details === "string" ? sanitizeString(input.details) : undefined,
296
+ detailsData: detailsDataTruncated ? undefined : sanitizeDisplayValue(input.detailsData),
297
+ isError: Boolean(input.isError),
298
+ isPartial: Boolean(input.isPartial),
299
+ hasResult: Boolean(input.hasResult),
300
+ inputTruncated: Boolean(input.inputTruncated) || argsTruncated || detailsDataTruncated,
301
+ });
302
+ }
303
+
304
+ function sanitizeString(value: unknown): string {
305
+ return typeof value === "string" ? sanitizeText(value) : "";
306
+ }
307
+
308
+ function sanitizeDisplayValue(value: unknown): unknown {
309
+ if (typeof value === "string") return sanitizeText(value);
310
+ if (Array.isArray(value)) return value.map(sanitizeDisplayValue);
311
+ if (!value || typeof value !== "object") return value;
312
+ return Object.fromEntries(
313
+ Object.entries(value).map(([key, item]) => [sanitizeText(key), sanitizeDisplayValue(item)]),
314
+ );
315
+ }
316
+
317
+ function freezeDisplayValue<T>(value: T): T {
318
+ if (value && typeof value === "object") {
319
+ for (const item of Object.values(value)) freezeDisplayValue(item);
320
+ Object.freeze(value);
321
+ }
322
+ return value;
323
+ }
324
+
325
+ type ToolCallFields = {
326
+ name: string;
327
+ args: Record<string, unknown>;
328
+ intent?: string;
329
+ };
330
+
331
+ type ToolResultFields = {
332
+ resultText: string;
333
+ isError: boolean;
334
+ hasResult: boolean;
335
+ };
336
+
337
+ export type ToolTranscriptFields = ToolCallFields & ToolResultFields;
338
+
339
+ export function formatToolArgs(name: string, args: Record<string, unknown>): string {
340
+ if (name === "read" || name === "write" || name === "edit") return args.path ? `path: ${args.path}` : "";
341
+ if (name === "bash") return typeof args.command === "string" ? args.command.replaceAll("\t", " ") : "";
342
+ if (name === "search")
343
+ return [
344
+ args.pattern ? `pattern: ${args.pattern}` : "",
345
+ Array.isArray(args.paths) ? `paths: ${args.paths.join(", ")}` : "",
346
+ ]
347
+ .filter(Boolean)
348
+ .join(", ");
349
+ return Object.entries(args)
350
+ .filter(([key]) => !key.startsWith("_"))
351
+ .map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`)
352
+ .join(", ")
353
+ .slice(0, MAX_TOOL_ARGS_CHARS);
354
+ }
355
+
356
+ export function composeToolCall({ name, args, intent }: ToolCallFields): string {
357
+ return [formatToolArgs(name, args), intent].filter(Boolean).join("\n");
358
+ }
359
+
360
+ export function composeToolResult({ resultText, isError, hasResult }: ToolResultFields): string {
361
+ const text = resultText.trim();
362
+ if (!hasResult) return "⏳ pending";
363
+ if (isError) return `✗ ${text || "Error"}`;
364
+ return text || "✓ done";
365
+ }
366
+
367
+ export function composeToolText(fields: ToolTranscriptFields): string {
368
+ return [composeToolCall(fields), composeToolResult(fields)].filter(Boolean).join("\n");
369
+ }
370
+
371
+ /**
372
+ * Caps result output without allocating an array for every source line. The final
373
+ * line count still requires scanning the result so the omitted-line count is exact.
374
+ */
375
+ function expandedToolResult(result: string): string {
376
+ let lineCount = 1;
377
+ let prefixEnd = result.length;
378
+ for (let index = 0; index < result.length; index++) {
379
+ if (result[index] !== "\n") continue;
380
+ if (lineCount === TOOL_RESULT_MAX_EXPANDED_LINES) prefixEnd = index;
381
+ lineCount += 1;
382
+ }
383
+ const prefix = result.slice(0, prefixEnd);
384
+ return lineCount > TOOL_RESULT_MAX_EXPANDED_LINES
385
+ ? `${prefix}\n... ${lineCount - TOOL_RESULT_MAX_EXPANDED_LINES} more lines`
386
+ : prefix;
387
+ }
388
+
389
+ export function toolDisplayText(fields: ToolTranscriptFields, expanded: boolean): string {
390
+ const call = composeToolCall(fields);
391
+ if (!expanded) return call;
392
+ return [call, expandedToolResult(composeToolResult(fields))].filter(Boolean).join("\n");
393
+ }
@@ -11,6 +11,12 @@ import { sanitizeText } from "@gajae-code/utils";
11
11
  import { getMarkdownTheme, theme } from "../theme/theme";
12
12
  import type { TranscriptItemRegistry, TranscriptSourcePayload } from "../transcript-item-registry";
13
13
  import { DynamicBorder } from "./dynamic-border";
14
+ import {
15
+ buildToolTranscriptEntry,
16
+ createToolTranscriptRenderDescriptor,
17
+ renderToolDisplayLines,
18
+ type ToolTranscriptRenderDescriptor,
19
+ } from "./tool-transcript-format";
14
20
 
15
21
  const INDENT = " ";
16
22
  const PAGE_SIZE = 15;
@@ -25,6 +31,8 @@ export type TranscriptViewerEntry = {
25
31
  foldable?: boolean;
26
32
  rawViewable?: boolean;
27
33
  getDisplayText?: (expanded: boolean) => string;
34
+ renderDescriptor?: ToolTranscriptRenderDescriptor;
35
+ richRenderEligible?: boolean;
28
36
  };
29
37
 
30
38
  export type TranscriptViewerOverlayOptions = {
@@ -332,6 +340,19 @@ export class TranscriptViewerOverlay extends Container {
332
340
  lines.push(
333
341
  `${selected ? theme.fg("accent", "▶") : " "} ${theme.fg("muted", `[${sanitizeText(entry.label ?? entry.kind)}]`)}`,
334
342
  );
343
+ if (
344
+ !raw &&
345
+ expanded &&
346
+ selected &&
347
+ entry.kind === "tool" &&
348
+ entry.richRenderEligible === true &&
349
+ entry.renderDescriptor
350
+ ) {
351
+ for (const line of renderToolDisplayLines(entry.renderDescriptor, contentWidth, theme))
352
+ lines.push(`${INDENT}${line}`);
353
+ this.#renderedEntries.push({ lineStart: start, lineCount: lines.length - start });
354
+ continue;
355
+ }
335
356
  const text = sanitizeText(
336
357
  (raw
337
358
  ? entry.payload.text
@@ -363,6 +384,29 @@ export class TranscriptViewerOverlay extends Container {
363
384
  }
364
385
  }
365
386
 
387
+ type ToolTranscriptMetadata = {
388
+ name: string;
389
+ arguments: Record<string, unknown>;
390
+ intent?: unknown;
391
+ resultText: string;
392
+ isError: boolean;
393
+ hasResult: boolean;
394
+ };
395
+
396
+ function hasToolTranscriptMetadata(
397
+ metadata: Readonly<Record<string, unknown>>,
398
+ ): metadata is Readonly<ToolTranscriptMetadata> {
399
+ return (
400
+ typeof metadata.name === "string" &&
401
+ metadata.arguments !== null &&
402
+ typeof metadata.arguments === "object" &&
403
+ !Array.isArray(metadata.arguments) &&
404
+ typeof metadata.resultText === "string" &&
405
+ typeof metadata.isError === "boolean" &&
406
+ typeof metadata.hasResult === "boolean"
407
+ );
408
+ }
409
+
366
410
  /** Main-session adapter over the registry's canonical payload resolver. */
367
411
  export function transcriptViewerEntries(registry: TranscriptItemRegistry): TranscriptViewerEntry[] {
368
412
  return registry.items().flatMap(item => {
@@ -375,10 +419,28 @@ export function transcriptViewerEntries(registry: TranscriptItemRegistry): Trans
375
419
  : item.kind === "assistant-thinking"
376
420
  ? "Thinking"
377
421
  : item.kind === "tool"
378
- ? "Tool"
422
+ ? String(payload.metadata.name ?? "Tool")
379
423
  : item.kind === "user"
380
424
  ? "User"
381
425
  : item.kind;
426
+ const toolMetadata = hasToolTranscriptMetadata(payload.metadata) ? payload.metadata : undefined;
427
+ if (item.kind === "tool" && toolMetadata)
428
+ return [
429
+ buildToolTranscriptEntry({
430
+ canonicalPayload: payload,
431
+ renderDescriptor: createToolTranscriptRenderDescriptor({
432
+ name: toolMetadata.name,
433
+ args: toolMetadata.arguments,
434
+ intent: toolMetadata.intent,
435
+ resultContent: toolMetadata.resultText,
436
+ detailsData: payload.metadata.detailsData,
437
+ isError: toolMetadata.isError,
438
+ hasResult: toolMetadata.hasResult,
439
+ }),
440
+ capabilities: capabilities ?? { copyable: true, foldable: true, rawViewable: true },
441
+ identity: { id: item.id, label },
442
+ }),
443
+ ];
382
444
  return [{ id: item.id, kind: item.kind, label, payload, ...capabilities }];
383
445
  });
384
446
  }
@@ -227,19 +227,37 @@ export class GoalModeController {
227
227
  this.scheduleContinuation();
228
228
  }
229
229
 
230
- async restoreFromSession(sessionContext: SessionContext): Promise<boolean> {
230
+ /**
231
+ * Restore durable goal state. Recovery hydration deliberately performs only
232
+ * in-memory restoration: it neither consumes a pending request nor repairs
233
+ * invalid persisted state by writing a fallback mode entry.
234
+ */
235
+ async restoreFromSession(
236
+ sessionContext: SessionContext,
237
+ options?: { recoveryHydration?: boolean },
238
+ ): Promise<boolean> {
239
+ const recoveryHydration = options?.recoveryHydration === true;
231
240
  const goalEnabled = this.ctx.session.settings.get("goal.enabled");
232
241
  if (!goalEnabled && (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused")) {
242
+ if (recoveryHydration) return false;
233
243
  this.ctx.sessionManager.appendModeChange("none");
234
244
  return true;
235
245
  }
236
246
  if (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused") {
237
247
  const goal = normalizeGoal(sessionContext.modeData?.goal) ?? undefined;
238
248
  if (!goal) {
249
+ if (recoveryHydration) throw new Error("Recovery hydration rejected an invalid persisted goal state.");
239
250
  this.ctx.sessionManager.appendModeChange("none");
240
251
  return true;
241
252
  }
242
253
  this.ctx.session.setGoalModeState({ enabled: sessionContext.mode === "goal", mode: "active", goal });
254
+ if (recoveryHydration) {
255
+ this.#enabled = sessionContext.mode === "goal";
256
+ this.#paused = sessionContext.mode === "goal_paused";
257
+ if (this.#enabled || this.#paused) this.ctx.modeGate.enter("goal");
258
+ this.#updateStatus();
259
+ return true;
260
+ }
243
261
  const restored = await this.ctx.session.goalRuntime.onThreadResumed();
244
262
  this.#enabled = restored?.enabled === true;
245
263
  this.#paused = restored?.enabled !== true && restored?.goal.status === "paused";
@@ -251,6 +269,7 @@ export class GoalModeController {
251
269
  this.#updateStatus();
252
270
  return true;
253
271
  }
272
+ if (recoveryHydration) return false;
254
273
  const pendingGoal = goalEnabled
255
274
  ? await consumePendingGoalModeRequest(this.ctx.sessionManager.getCwd(), this.ctx.sessionManager.getSessionId())
256
275
  : null;