@gajae-code/coding-agent 0.11.4 → 0.11.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 (154) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/config/settings-schema.d.ts +31 -1
  4. package/dist/types/export/html/template.generated.d.ts +1 -1
  5. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  6. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  7. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  8. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  9. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  10. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  12. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  13. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  14. package/dist/types/main.d.ts +2 -2
  15. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  16. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  17. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/modes/components/tool-transcript-format.d.ts +58 -0
  20. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  21. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  22. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  23. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  24. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  25. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  26. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  27. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  28. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  29. package/dist/types/sdk/bus/chat-daemon-control.d.ts +42 -4
  30. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  31. package/dist/types/sdk/bus/config.d.ts +22 -0
  32. package/dist/types/sdk/bus/index.d.ts +162 -2
  33. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  34. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  35. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  36. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -3
  37. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  38. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  39. package/dist/types/sdk/session.d.ts +16 -1
  40. package/dist/types/secrets/index.d.ts +4 -2
  41. package/dist/types/secrets/obfuscator.d.ts +1 -1
  42. package/dist/types/session/agent-session.d.ts +31 -2
  43. package/dist/types/session/artifacts.d.ts +30 -8
  44. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  45. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  46. package/dist/types/session/session-manager.d.ts +98 -18
  47. package/dist/types/session/session-storage.d.ts +27 -0
  48. package/dist/types/session/streaming-output.d.ts +4 -0
  49. package/dist/types/session/yield-queue.d.ts +8 -0
  50. package/dist/types/task/executor.d.ts +9 -0
  51. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  52. package/dist/types/tools/ask.d.ts +48 -0
  53. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  54. package/dist/types/tools/index.d.ts +4 -0
  55. package/package.json +9 -9
  56. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  57. package/src/cli/notify-cli.ts +40 -18
  58. package/src/commands/sdk.ts +7 -2
  59. package/src/config/settings-schema.ts +31 -1
  60. package/src/daemon/runtime.ts +2 -2
  61. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  62. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  63. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  64. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  65. package/src/eval/py/index.ts +2 -2
  66. package/src/export/html/template.generated.ts +1 -1
  67. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  68. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  69. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  70. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  71. package/src/gjc-runtime/launch-tmux.ts +61 -6
  72. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  73. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  74. package/src/gjc-runtime/state-runtime.ts +38 -12
  75. package/src/gjc-runtime/team-runtime.ts +1 -1
  76. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  77. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  78. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  79. package/src/harness-control-plane/owner.ts +59 -3
  80. package/src/hindsight/client.ts +65 -2
  81. package/src/internal-urls/agent-protocol.ts +76 -5
  82. package/src/internal-urls/docs-index.generated.ts +8 -7
  83. package/src/internal-urls/local-protocol.ts +416 -18
  84. package/src/lsp/index.ts +4 -88
  85. package/src/main.ts +36 -12
  86. package/src/modes/acp/acp-agent.ts +8 -0
  87. package/src/modes/components/ansi-display-validator.ts +54 -0
  88. package/src/modes/components/notifications-settings-editor.ts +26 -2
  89. package/src/modes/components/session-observer-overlay.ts +40 -24
  90. package/src/modes/components/settings-selector.ts +3 -1
  91. package/src/modes/components/tool-transcript-format.ts +321 -0
  92. package/src/modes/components/transcript-viewer-overlay.ts +39 -17
  93. package/src/modes/controllers/command-controller.ts +15 -0
  94. package/src/modes/controllers/extension-ui-controller.ts +14 -2
  95. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  96. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  97. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  98. package/src/modes/controllers/selector-controller.ts +70 -8
  99. package/src/modes/interactive-mode.ts +1 -0
  100. package/src/modes/runtime-init.ts +16 -2
  101. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  102. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  103. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  104. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  105. package/src/plan-mode/approved-plan.ts +3 -1
  106. package/src/prompts/agents/architect.md +1 -0
  107. package/src/prompts/agents/critic.md +1 -0
  108. package/src/prompts/system/custom-system-prompt.md +1 -1
  109. package/src/prompts/system/system-prompt.md +2 -1
  110. package/src/prompts/tools/lsp.md +3 -3
  111. package/src/runtime-mcp/manager.ts +16 -1
  112. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  113. package/src/sdk/broker/session-index.ts +122 -64
  114. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  115. package/src/sdk/bus/chat-daemon-control.ts +538 -130
  116. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  117. package/src/sdk/bus/config-commands.ts +22 -0
  118. package/src/sdk/bus/config.ts +37 -0
  119. package/src/sdk/bus/index.ts +388 -94
  120. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  121. package/src/sdk/bus/notification-service.ts +284 -62
  122. package/src/sdk/bus/recent-activity.ts +14 -5
  123. package/src/sdk/bus/session-control.ts +150 -44
  124. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  125. package/src/sdk/bus/telegram-daemon-contract.ts +11 -3
  126. package/src/sdk/bus/telegram-daemon-control.ts +312 -76
  127. package/src/sdk/bus/telegram-daemon.ts +1916 -285
  128. package/src/sdk/host/control/dispatch.ts +14 -5
  129. package/src/sdk/protocol/operation-inventory.generated.json +29 -0
  130. package/src/sdk/session.ts +155 -27
  131. package/src/secrets/index.ts +53 -10
  132. package/src/secrets/obfuscator.ts +21 -18
  133. package/src/secrets/regex.ts +2 -0
  134. package/src/session/agent-session.ts +1447 -852
  135. package/src/session/artifacts.ts +188 -23
  136. package/src/session/context-estimation.ts +7 -2
  137. package/src/session/internal/managed-session-scope.ts +200 -38
  138. package/src/session/internal/managed-session-storage.ts +1152 -46
  139. package/src/session/session-manager.ts +1121 -148
  140. package/src/session/session-storage.ts +109 -11
  141. package/src/session/streaming-output.ts +6 -0
  142. package/src/session/yield-queue.ts +20 -0
  143. package/src/slash-commands/builtin-registry.ts +36 -0
  144. package/src/task/executor.ts +71 -26
  145. package/src/task/index.ts +63 -20
  146. package/src/tool-discovery/tool-index.ts +18 -0
  147. package/src/tools/ask-answer-registry.ts +15 -1
  148. package/src/tools/ask.ts +129 -32
  149. package/src/tools/bash-skill-urls.ts +14 -3
  150. package/src/tools/bash.ts +2 -2
  151. package/src/tools/index.ts +4 -0
  152. package/src/tools/plan-mode-guard.ts +1 -0
  153. package/src/tools/sqlite-reader.ts +3 -2
  154. package/src/tools/subagent.ts +2 -0
@@ -1,5 +1,326 @@
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
+
1
9
  export const MAX_TOOL_ARGS_CHARS = 500;
2
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
+ }
3
324
 
4
325
  type ToolCallFields = {
5
326
  name: string;
@@ -11,7 +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 { toolDisplayText } from "./tool-transcript-format";
14
+ import {
15
+ buildToolTranscriptEntry,
16
+ createToolTranscriptRenderDescriptor,
17
+ renderToolDisplayLines,
18
+ type ToolTranscriptRenderDescriptor,
19
+ } from "./tool-transcript-format";
15
20
 
16
21
  const INDENT = " ";
17
22
  const PAGE_SIZE = 15;
@@ -26,6 +31,8 @@ export type TranscriptViewerEntry = {
26
31
  foldable?: boolean;
27
32
  rawViewable?: boolean;
28
33
  getDisplayText?: (expanded: boolean) => string;
34
+ renderDescriptor?: ToolTranscriptRenderDescriptor;
35
+ richRenderEligible?: boolean;
29
36
  };
30
37
 
31
38
  export type TranscriptViewerOverlayOptions = {
@@ -333,6 +340,19 @@ export class TranscriptViewerOverlay extends Container {
333
340
  lines.push(
334
341
  `${selected ? theme.fg("accent", "▶") : " "} ${theme.fg("muted", `[${sanitizeText(entry.label ?? entry.kind)}]`)}`,
335
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
+ }
336
356
  const text = sanitizeText(
337
357
  (raw
338
358
  ? entry.payload.text
@@ -404,21 +424,23 @@ export function transcriptViewerEntries(registry: TranscriptItemRegistry): Trans
404
424
  ? "User"
405
425
  : item.kind;
406
426
  const toolMetadata = hasToolTranscriptMetadata(payload.metadata) ? payload.metadata : undefined;
407
- const getDisplayText =
408
- item.kind === "tool" && toolMetadata
409
- ? (expanded: boolean) =>
410
- toolDisplayText(
411
- {
412
- name: toolMetadata.name,
413
- args: toolMetadata.arguments,
414
- intent: typeof toolMetadata.intent === "string" ? toolMetadata.intent : undefined,
415
- resultText: toolMetadata.resultText,
416
- isError: toolMetadata.isError,
417
- hasResult: toolMetadata.hasResult,
418
- },
419
- expanded,
420
- )
421
- : undefined;
422
- return [{ id: item.id, kind: item.kind, label, payload, ...capabilities, getDisplayText }];
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
+ ];
444
+ return [{ id: item.id, kind: item.kind, label, payload, ...capabilities }];
423
445
  });
424
446
  }
@@ -1260,6 +1260,10 @@ export class CommandController {
1260
1260
  }
1261
1261
 
1262
1262
  async handleHandoffCommand(customInstructions?: string): Promise<void> {
1263
+ if (this.ctx.session.isStreaming) {
1264
+ this.ctx.showWarning("Wait for the current response to finish or abort it before handing off.");
1265
+ return;
1266
+ }
1263
1267
  const entries = this.ctx.sessionManager.getEntries();
1264
1268
  const messageCount = entries.filter(e => e.type === "message").length;
1265
1269
 
@@ -1321,6 +1325,17 @@ export class CommandController {
1321
1325
  } else {
1322
1326
  this.ctx.showError(`Handoff failed: ${message}`);
1323
1327
  }
1328
+ // The failure is non-destructive: the current session is unchanged. If the
1329
+ // document was already generated, preserve it on the clipboard for retry.
1330
+ const retainedDocument =
1331
+ error &&
1332
+ typeof error === "object" &&
1333
+ typeof (error as { handoffDocument?: unknown }).handoffDocument === "string"
1334
+ ? (error as { handoffDocument: string }).handoffDocument
1335
+ : undefined;
1336
+ if (retainedDocument) {
1337
+ this.#doCopy(retainedDocument, "Handoff document preserved on clipboard; current session is unchanged.");
1338
+ }
1324
1339
  } finally {
1325
1340
  handoffLoader.stop();
1326
1341
  this.ctx.statusContainer.clear();
@@ -214,13 +214,25 @@ export class ExtensionUiController {
214
214
  try {
215
215
  return {
216
216
  handoff: await session.handoff(
217
- typeof input.instructions === "string" ? input.instructions : undefined,
217
+ typeof input.target === "string"
218
+ ? input.target
219
+ : typeof input.instructions === "string"
220
+ ? input.instructions
221
+ : undefined,
218
222
  ),
219
223
  };
220
224
  } catch (error) {
225
+ const typed = error as { code?: unknown; handoffDocument?: unknown };
226
+ const handoffDocument =
227
+ typeof typed?.handoffDocument === "string" ? { handoffDocument: typed.handoffDocument } : undefined;
228
+ // Preserve a safe typed code (e.g. transient `busy`) so clients keep
229
+ // correct retry/backoff semantics; only synthesize invalid_request for
230
+ // otherwise-untyped failures.
231
+ const code = typed?.code === "busy" ? "busy" : "invalid_request";
221
232
  throw Object.assign(
222
233
  new Error(error instanceof Error ? error.message : "Handoff is unavailable for the current state."),
223
- { code: "invalid_request" },
234
+ { code },
235
+ handoffDocument,
224
236
  );
225
237
  }
226
238
  case "session.export_html":
@@ -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;
@@ -334,6 +334,7 @@ export class PlanModeController {
334
334
  return planFilePath.startsWith("local:")
335
335
  ? resolveLocalUrlToPath(normalizeLocalScheme(planFilePath), {
336
336
  getArtifactsDir: () => this.ctx.sessionManager.getArtifactsDir(),
337
+ isManagedDestination: () => this.ctx.sessionManager.isManagedDestination(),
337
338
  getSessionId: () => this.ctx.sessionManager.getSessionId(),
338
339
  })
339
340
  : path.resolve(this.ctx.sessionManager.getCwd(), planFilePath);
@@ -448,6 +449,7 @@ export class PlanModeController {
448
449
  await Bun.write(
449
450
  resolveLocalUrlToPath(options.finalPlanFilePath, {
450
451
  getArtifactsDir: () => this.ctx.sessionManager.getArtifactsDir(),
452
+ isManagedDestination: () => this.ctx.sessionManager.isManagedDestination(),
451
453
  getSessionId: () => this.ctx.sessionManager.getSessionId(),
452
454
  }),
453
455
  planContent,
@@ -1450,6 +1450,9 @@ export class MCPCommandController {
1450
1450
  if (!this.ctx.mcpManager) {
1451
1451
  return;
1452
1452
  }
1453
+ if (this.ctx.mcpManager.isConnectionSetSealed()) {
1454
+ throw new Error("This session's plugin-bundle MCP connections are fixed for its lifetime.");
1455
+ }
1453
1456
 
1454
1457
  // Disconnect all existing servers
1455
1458
  await this.ctx.mcpManager.disconnectAll();
@@ -43,7 +43,7 @@ import {
43
43
  theme,
44
44
  } from "../../modes/theme/theme";
45
45
  import type { InteractiveModeContext, OAuthSelectorOptions } from "../../modes/types";
46
- import { getNotificationConfig, maskToken } from "../../sdk/bus/config";
46
+ import { getNotificationConfig, isTelegramConfigured, maskToken } from "../../sdk/bus/config";
47
47
  import {
48
48
  clearTelegramActivationMarker,
49
49
  createTelegramActivationMarker,
@@ -228,6 +228,13 @@ export interface NotificationsEditorOperationDependencies {
228
228
  saveTelegramInactive: typeof saveTelegramInactive;
229
229
  removeTelegramConfiguration: typeof removeTelegramConfiguration;
230
230
  unregisterNotificationRoot: typeof unregisterNotificationRoot;
231
+ reloadTelegramDaemon(settings: Settings): Promise<{ ok: boolean; message: string }>;
232
+ restartTelegramDaemon(settings: Settings): Promise<{ ok: boolean; message: string }>;
233
+ stopTelegramDaemon(settings: Settings): Promise<{
234
+ ok: boolean;
235
+ message: string;
236
+ before?: { health?: string };
237
+ }>;
231
238
  }
232
239
 
233
240
  const notificationEditorOperationDependencies: NotificationsEditorOperationDependencies = {
@@ -245,6 +252,11 @@ const notificationEditorOperationDependencies: NotificationsEditorOperationDepen
245
252
  saveTelegramInactive,
246
253
  removeTelegramConfiguration,
247
254
  unregisterNotificationRoot,
255
+ reloadTelegramDaemon: async settings =>
256
+ await new TelegramDaemonController(settings).reload({ spawnIfStopped: false }),
257
+ restartTelegramDaemon: async settings =>
258
+ await new TelegramDaemonController(settings).reload({ spawnIfStopped: true }),
259
+ stopTelegramDaemon: async settings => await new TelegramDaemonController(settings).stop(),
248
260
  };
249
261
 
250
262
  function unavailableNotificationSessionStatus(): NotificationSessionStatus {
@@ -333,6 +345,8 @@ export function createNotificationsEditorOperations(
333
345
  sessionScope: config.sessionScope,
334
346
  richEnabled: config.rich.enabled,
335
347
  richDraftEnabled: config.richDraft.enabled,
348
+ toolActivityEnabled: config.toolActivity.enabled,
349
+ streamingEnabled: config.streaming.enabled,
336
350
  },
337
351
  };
338
352
  },
@@ -470,6 +484,7 @@ export function createNotificationsEditorOperations(
470
484
  tokenFingerprint: setup.tokenFingerprint,
471
485
  richEnabled: input.richEnabled,
472
486
  richDraftEnabled: input.richDraftEnabled,
487
+ streamingEnabled: input.streamingEnabled,
473
488
  };
474
489
  drafts.set(draft, token);
475
490
  const pairingMessage =
@@ -513,6 +528,7 @@ export function createNotificationsEditorOperations(
513
528
  { path: "notifications.telegram.chatId", op: "set", value: draft.chatId },
514
529
  { path: "notifications.telegram.rich.enabled", op: "set", value: draft.richEnabled },
515
530
  { path: "notifications.telegram.richDraft.enabled", op: "set", value: draft.richDraftEnabled },
531
+ { path: "notifications.telegram.streaming.enabled", op: "set", value: draft.streamingEnabled },
516
532
  ]);
517
533
  drafts.delete(draft);
518
534
  const activationMarker = createTelegramActivationMarker({
@@ -676,14 +692,60 @@ export function createNotificationsEditorOperations(
676
692
  },
677
693
 
678
694
  commitPreferences: async preferences => {
695
+ let daemonWasRunningForDisable = false;
679
696
  try {
680
- const receipt = await ctx.settings.commitAtomicBatch([
681
- { path: "notifications.redact", op: "set", value: preferences.redact },
682
- { path: "notifications.verbosity", op: "set", value: preferences.verbosity },
683
- { path: "notifications.sessionScope", op: "set", value: preferences.sessionScope },
684
- { path: "notifications.telegram.rich.enabled", op: "set", value: preferences.richEnabled },
685
- { path: "notifications.telegram.richDraft.enabled", op: "set", value: preferences.richDraftEnabled },
686
- ]);
697
+ const before = services.getNotificationConfig(ctx.settings);
698
+ const disablingToolActivity =
699
+ isTelegramConfigured(before) && before.toolActivity.enabled && !preferences.toolActivityEnabled;
700
+ if (disablingToolActivity) {
701
+ const stopped = await services.stopTelegramDaemon(ctx.settings);
702
+ if (!stopped.ok)
703
+ throw new Error(
704
+ `Notification preferences were not saved because daemon stop failed: ${stopped.message}`,
705
+ );
706
+ daemonWasRunningForDisable = stopped.before?.health === "running";
707
+ }
708
+
709
+ let receipt: Awaited<ReturnType<typeof ctx.settings.commitAtomicBatch>>;
710
+ try {
711
+ receipt = await ctx.settings.commitAtomicBatch([
712
+ { path: "notifications.redact", op: "set", value: preferences.redact },
713
+ { path: "notifications.verbosity", op: "set", value: preferences.verbosity },
714
+ { path: "notifications.sessionScope", op: "set", value: preferences.sessionScope },
715
+ { path: "notifications.telegram.rich.enabled", op: "set", value: preferences.richEnabled },
716
+ { path: "notifications.telegram.richDraft.enabled", op: "set", value: preferences.richDraftEnabled },
717
+ { path: "notifications.telegram.streaming.enabled", op: "set", value: preferences.streamingEnabled },
718
+ {
719
+ path: "notifications.telegram.toolActivity.enabled",
720
+ op: "set",
721
+ value: preferences.toolActivityEnabled,
722
+ },
723
+ ]);
724
+ } catch (error) {
725
+ if (daemonWasRunningForDisable) {
726
+ try {
727
+ const restarted = await services.restartTelegramDaemon(ctx.settings);
728
+ if (!restarted.ok) throw new Error(restarted.message);
729
+ } catch (restartError) {
730
+ const commitMessage = error instanceof Error ? error.message : String(error);
731
+ const restartMessage = restartError instanceof Error ? restartError.message : String(restartError);
732
+ throw new Error(
733
+ `Notification preference commit failed (${commitMessage}) and daemon restart failed (${restartMessage}).`,
734
+ { cause: new AggregateError([error, restartError]) },
735
+ );
736
+ }
737
+ }
738
+ throw error;
739
+ }
740
+
741
+ const config = services.getNotificationConfig(ctx.settings);
742
+ if (isTelegramConfigured(config)) {
743
+ const reload = daemonWasRunningForDisable
744
+ ? await services.restartTelegramDaemon(ctx.settings)
745
+ : await services.reloadTelegramDaemon(ctx.settings);
746
+ if (!reload.ok)
747
+ throw new Error(`Notification preferences were saved, but daemon reload failed: ${reload.message}`);
748
+ }
687
749
  await notifyAfterDurableCommit();
688
750
  return { receipt, message: "Notification preferences saved atomically." };
689
751
  } catch (error) {
@@ -1839,6 +1839,7 @@ export class InteractiveMode implements InteractiveModeContext {
1839
1839
  isError: result?.isError ?? false,
1840
1840
  resultText,
1841
1841
  hasResult: toolResults.has(content.id),
1842
+ detailsData: result?.details,
1842
1843
  },
1843
1844
  source: { content, result },
1844
1845
  }),