@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
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
4
4
  import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@gajae-code/tui";
5
- import { $env, sanitizeText } from "@gajae-code/utils";
5
+ import { $env, logger, sanitizeText } from "@gajae-code/utils";
6
6
  import { type AppKeybinding, KEYBINDINGS } from "../../config/keybindings";
7
7
  import { isSettingsInitialized, settings } from "../../config/settings";
8
8
  import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
@@ -22,12 +22,14 @@ import { getUserMessageViewportAnchorIds } from "../../session/session-manager";
22
22
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
23
23
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
24
24
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
25
- import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
25
+ import { ensureSupportedImageInput, ImageInputTooLargeError } from "../../utils/image-loading";
26
26
  import { resizeImage } from "../../utils/image-resize";
27
- import { formatPastedImageReference, resolvePastedImagePath } from "../../utils/pasted-image-path";
27
+ import { loadPastedImageBatch, PastedImageBatchError } from "../../utils/pasted-image-loading";
28
+ import { formatPastedImageReference, parsePastedImagePaths } from "../../utils/pasted-image-path";
28
29
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
29
30
  import { ActionRegistry, APP_ACTION_METADATA } from "../action-registry";
30
31
  import { CommandPalette, type CommandPaletteAction, type CommandPaletteEntry } from "../components/command-palette";
32
+ import type { PasteTextContext } from "../components/custom-editor";
31
33
  import { QueuePaneComponent } from "../components/queue-pane";
32
34
  import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
33
35
 
@@ -44,14 +46,23 @@ const EMPTY_EDITOR_DOUBLE_ESCAPE_WINDOW_MS = 500;
44
46
  const IMAGE_PLACEHOLDER_PATTERN = /\[image ([1-9]\d*)\]/g;
45
47
  const IMAGE_PLACEHOLDER_PRESENT_PATTERN = /\[image [1-9]\d*\]/;
46
48
 
49
+ interface InputControllerDependencies {
50
+ loadPastedImageBatch?: typeof loadPastedImageBatch;
51
+ }
52
+
47
53
  function isExpandable(obj: unknown): obj is Expandable {
48
54
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
49
55
  }
50
56
 
51
57
  export class InputController {
52
58
  readonly actionRegistry: ActionRegistry<void>;
59
+ readonly #loadPastedImageBatch: typeof loadPastedImageBatch;
53
60
 
54
- constructor(private ctx: InteractiveModeContext) {
61
+ constructor(
62
+ private ctx: InteractiveModeContext,
63
+ dependencies: InputControllerDependencies = {},
64
+ ) {
65
+ this.#loadPastedImageBatch = dependencies.loadPastedImageBatch ?? loadPastedImageBatch;
55
66
  this.actionRegistry = new ActionRegistry({
56
67
  context: undefined,
57
68
  showError: actionId => this.ctx.showError(actionId),
@@ -585,11 +596,11 @@ export class InputController {
585
596
  );
586
597
  this.ctx.editor.onCopyPrompt = () => this.#executeAction("app.clipboard.copyPrompt");
587
598
  this.#registerCommandPaletteAction("app.clipboard.copyPrompt", copyPrompt);
588
- this.ctx.editor.onPasteText = text => this.handleTextPaste(text);
589
- this.ctx.editor.onPastePendingInputCleared = (reason, droppedInputCount) => {
599
+ this.ctx.editor.onPasteText = (text, context) => this.handleTextPaste(text, context);
600
+ this.ctx.editor.onPastePendingInputCleared = (reason, restoredInputCount) => {
590
601
  const reasonText = reason === "timeout" ? "timed out" : "exceeded the input queue limit";
591
602
  this.ctx.showWarning(
592
- `Paste handling ${reasonText}; discarded ${droppedInputCount} buffered input event${droppedInputCount === 1 ? "" : "s"}.`,
603
+ `Paste handling ${reasonText}; restored the original paste and ${restoredInputCount} buffered input event${restoredInputCount === 1 ? "" : "s"}.`,
593
604
  );
594
605
  };
595
606
  const expandTools = () => this.toggleToolOutputExpansion();
@@ -1522,46 +1533,96 @@ export class InputController {
1522
1533
  return true;
1523
1534
  }
1524
1535
 
1525
- handleTextPaste(text: string): boolean | Promise<boolean> {
1526
- const imagePath = resolvePastedImagePath(text, { cwd: this.ctx.sessionManager.getCwd() });
1527
- return imagePath ? this.#attachPastedImagePath(imagePath) : false;
1536
+ handleTextPaste(text: string, context: PasteTextContext): boolean | Promise<boolean> {
1537
+ if (this.ctx.isBashMode || this.ctx.isPythonMode) return false;
1538
+ const parsed = parsePastedImagePaths(text, { cwd: this.ctx.sessionManager.getCwd() });
1539
+ if (!parsed) return false;
1540
+ if (parsed.kind === "too-many") {
1541
+ this.ctx.showStatus(`Cannot attach more than ${parsed.maxCandidates} pasted images.`);
1542
+ return false;
1543
+ }
1544
+ return this.#attachPastedImagePaths(parsed.paths, parsed.requiresConfirmation, context);
1528
1545
  }
1529
1546
 
1530
- /**
1531
- * Returns `false` on every failure path so the editor replays the original
1532
- * bracketed paste — the raw path text must never be lost when attachment
1533
- * is impossible (unsupported content, oversized image, load error).
1534
- */
1535
- async #attachPastedImagePath(imagePath: string): Promise<boolean> {
1547
+ /** Return false on every failure so CustomEditor restores the exact paste. */
1548
+ async #attachPastedImagePaths(
1549
+ imagePaths: readonly string[],
1550
+ requiresConfirmation: boolean,
1551
+ context: PasteTextContext,
1552
+ ): Promise<boolean> {
1553
+ const signal = context.signal;
1554
+ const editor = this.ctx.editor;
1555
+ const editorText = editor.getText();
1556
+ const pendingImages = this.ctx.pendingImages;
1557
+ const pendingImageCount = pendingImages.length;
1536
1558
  try {
1537
- const image = await loadImageInput({
1538
- path: imagePath,
1539
- cwd: this.ctx.sessionManager.getCwd(),
1559
+ if (requiresConfirmation) {
1560
+ const selection = await this.ctx.showHookSelector(
1561
+ `Attach ${imagePaths.length} pasted images?\nOnly attach files you intend to send to the model.`,
1562
+ ["Attach images", "Paste paths literally"],
1563
+ { signal, helpText: "Enter: choose · Esc: paste paths literally" },
1564
+ );
1565
+ signal.throwIfAborted();
1566
+ if (selection !== "Attach images") return false;
1567
+ }
1568
+
1569
+ const loaded = await this.#loadPastedImageBatch({
1570
+ paths: imagePaths,
1540
1571
  autoResize: this.ctx.settings.get("images.autoResize"),
1572
+ sourcePolicy: requiresConfirmation ? "confirmed" : "automatic-temp",
1573
+ signal,
1541
1574
  });
1542
- if (!image) {
1543
- this.ctx.showStatus("Unsupported pasted image file");
1544
- return false;
1545
- }
1575
+ signal.throwIfAborted();
1576
+ return context.commit(() => {
1577
+ if (
1578
+ this.ctx.editor !== editor ||
1579
+ editor.getText() !== editorText ||
1580
+ this.ctx.pendingImages !== pendingImages ||
1581
+ pendingImages.length !== pendingImageCount
1582
+ ) {
1583
+ return false;
1584
+ }
1546
1585
 
1547
- this.ctx.pendingImages = [
1548
- ...this.ctx.pendingImages,
1549
- {
1550
- type: "image",
1551
- data: image.data,
1552
- mimeType: image.mimeType,
1553
- },
1554
- ];
1555
- this.ctx.editor.insertText(`${formatPastedImageReference(this.#nextImagePlaceholder(), image.resolvedPath)} `);
1556
- this.ctx.showStatus(`Attached image: ${path.basename(image.resolvedPath)}`, { dim: true });
1557
- this.ctx.ui.requestRender();
1558
- return true;
1586
+ const firstImageNumber = pendingImages.length + 1;
1587
+ const references = loaded.sourcePaths.map((sourcePath, index) =>
1588
+ formatPastedImageReference(`[image ${firstImageNumber + index}]`, sourcePath),
1589
+ );
1590
+ try {
1591
+ editor.insertText(`${references.join(" ")} `);
1592
+ this.ctx.pendingImages = [...pendingImages, ...loaded.images];
1593
+ } catch (error) {
1594
+ try {
1595
+ editor.setText(editorText);
1596
+ } catch {
1597
+ // Preserve the original mutation error when rollback itself fails.
1598
+ } finally {
1599
+ this.ctx.pendingImages = pendingImages;
1600
+ }
1601
+ throw error;
1602
+ }
1603
+
1604
+ try {
1605
+ this.ctx.showStatus(
1606
+ loaded.images.length === 1
1607
+ ? `Attached image: ${path.basename(loaded.sourcePaths[0] ?? "")}`
1608
+ : `Attached ${loaded.images.length} images`,
1609
+ { dim: true },
1610
+ );
1611
+ this.ctx.ui.requestRender();
1612
+ } catch (error) {
1613
+ logger.warn("Pasted images attached but status rendering failed", { error: String(error) });
1614
+ }
1615
+ return true;
1616
+ });
1559
1617
  } catch (error) {
1560
- if (error instanceof ImageInputTooLargeError) {
1618
+ if (signal.aborted) return false;
1619
+ if (error instanceof ImageInputTooLargeError || error instanceof PastedImageBatchError) {
1561
1620
  this.ctx.showStatus(error.message);
1562
1621
  return false;
1563
1622
  }
1564
- this.ctx.showStatus("Failed to attach pasted image");
1623
+ this.ctx.showStatus(
1624
+ imagePaths.length === 1 ? "Failed to attach pasted image" : "Failed to attach pasted images",
1625
+ );
1565
1626
  return false;
1566
1627
  }
1567
1628
  }
@@ -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) {
@@ -1060,7 +1122,7 @@ export class SelectorController {
1060
1122
  };
1061
1123
  wizard = new CustomProviderWizardComponent(
1062
1124
  input => {
1063
- void submit(input);
1125
+ return submit(input);
1064
1126
  },
1065
1127
  () => {
1066
1128
  done();
@@ -62,6 +62,7 @@ import {
62
62
  } from "./components/pet-capability";
63
63
  import type { ToolExecutionHandle } from "./components/tool-execution";
64
64
  import { StatusLineComponent } from "./components/tool-status-header";
65
+ import { composeToolText } from "./components/tool-transcript-format";
65
66
  import {
66
67
  WelcomeComponent,
67
68
  type WelcomeLogoMode,
@@ -1817,17 +1818,28 @@ export class InteractiveMode implements InteractiveModeContext {
1817
1818
  result?.content
1818
1819
  .filter(part => part.type === "text")
1819
1820
  .map(part => part.text)
1820
- .join("\n") ?? "";
1821
+ .join("\n")
1822
+ .trim() ?? "";
1821
1823
  items.push({
1822
1824
  kind: "tool",
1823
1825
  source: { toolCallId: content.id, content, result },
1824
1826
  getPayload: () => ({
1825
- text: resultText || JSON.stringify(content.arguments, null, 2),
1827
+ text: composeToolText({
1828
+ name: content.name,
1829
+ args: content.arguments,
1830
+ intent: content.intent,
1831
+ resultText,
1832
+ isError: result?.isError ?? false,
1833
+ hasResult: toolResults.has(content.id),
1834
+ }),
1826
1835
  metadata: {
1827
1836
  name: content.name,
1828
1837
  arguments: content.arguments,
1829
1838
  intent: content.intent,
1830
1839
  isError: result?.isError ?? false,
1840
+ resultText,
1841
+ hasResult: toolResults.has(content.id),
1842
+ detailsData: result?.details,
1831
1843
  },
1832
1844
  source: { content, result },
1833
1845
  }),
@@ -10,6 +10,11 @@
10
10
  * This is the pure mapping primitive used by SDK-native workflow gate emitters.
11
11
  */
12
12
 
13
+ import { isDeepInterviewAskQuestion } from "../../../deep-interview/render-middleware";
14
+ import {
15
+ assertDeepInterviewInputWithinLimit,
16
+ MAX_USER_RESPONSE_LENGTH,
17
+ } from "../../../gjc-runtime/deep-interview-state";
13
18
  import type { OpenGateInput } from "./workflow-gate-broker";
14
19
  import {
15
20
  type AskGateDeepInterviewState,
@@ -134,7 +139,17 @@ export function buildAskGateStageState(question: AskGateQuestion, labels: string
134
139
  /** Build the `workflow_gate` open-input for one ask question. */
135
140
  export function questionToGate(question: AskGateQuestion): OpenGateInput {
136
141
  const labels = question.options.map(o => o.label);
137
- const schema = buildAskGateAnswerSchema(question, labels);
142
+ const schema = buildAskGateAnswerSchema(
143
+ {
144
+ multi: question.multi,
145
+ allowEmpty: question.allowEmpty,
146
+ customMaxLength:
147
+ question.deepInterview || isDeepInterviewAskQuestion(question.question)
148
+ ? MAX_USER_RESPONSE_LENGTH
149
+ : undefined,
150
+ },
151
+ labels,
152
+ );
138
153
  return {
139
154
  stage: question.workflowGate?.stage ?? "deep-interview",
140
155
  kind: question.workflowGate?.kind ?? "question",
@@ -229,6 +244,12 @@ export function gateAnswerToResult(question: AskGateQuestion, answer: unknown):
229
244
  if (other && (answer.custom === undefined || answer.custom.trim() === "")) {
230
245
  throw new DeepInterviewGateError("missing_custom", "custom text is required when `other` is true");
231
246
  }
247
+ if (
248
+ other &&
249
+ (question.deepInterview || isDeepInterviewAskQuestion(question.question)) &&
250
+ answer.custom !== undefined
251
+ )
252
+ assertDeepInterviewInputWithinLimit(answer.custom, MAX_USER_RESPONSE_LENGTH, "user_response");
232
253
  return {
233
254
  id: question.id,
234
255
  question: question.question,
@@ -261,6 +282,15 @@ export function classifyAskGateDisposition(
261
282
  multi: (state as Record<string, unknown>).multi === true,
262
283
  allowEmpty: (state as Record<string, unknown>).allow_empty === true,
263
284
  };
285
+ if (
286
+ (state as Record<string, unknown>).deep_interview_metadata === true &&
287
+ typeof (answer as Record<string, unknown>)?.custom === "string"
288
+ )
289
+ assertDeepInterviewInputWithinLimit(
290
+ (answer as Record<string, unknown>).custom as string,
291
+ MAX_USER_RESPONSE_LENGTH,
292
+ "user_response",
293
+ );
264
294
  if (typeof answer === "object" && answer !== null && (answer as Record<string, unknown>).action === "clarify") {
265
295
  gateAnswerToResult(question, answer);
266
296
  return "resolve_without_commit";
@@ -76,6 +76,13 @@ export interface NotificationGateResolutionOptions {
76
76
  closeClaimInvalid(reason: string): void;
77
77
  }
78
78
 
79
+ export class NotificationGatePolicyChangedError extends Error {
80
+ constructor() {
81
+ super("Notification policy changed while resolving the workflow gate");
82
+ this.name = "NotificationGatePolicyChangedError";
83
+ }
84
+ }
85
+
79
86
  export interface AskSelectedAckRecoveryParticipant {
80
87
  requestRecoveredAskSelectedAck(input: {
81
88
  sessionId: string;
@@ -361,7 +368,8 @@ export class BrokerWorkflowGateEmitter implements WorkflowGateEmitter {
361
368
  daemonDeadlineAt: Date.now() + 8_000,
362
369
  hostTimeoutMs: 10_000,
363
370
  });
364
- } catch {
371
+ } catch (error) {
372
+ if (error instanceof NotificationGatePolicyChangedError) throw error;
365
373
  outcome = { status: "unknown", reason: "host_timeout" };
366
374
  }
367
375
  this.#broker.updateAckPolicy(response.gate_id, {
@@ -378,6 +386,14 @@ export class BrokerWorkflowGateEmitter implements WorkflowGateEmitter {
378
386
  else closeClaimInvalid(resolution.error?.code ?? "invalid_answer");
379
387
  return resolution;
380
388
  } catch (error) {
389
+ if (error instanceof NotificationGatePolicyChangedError) {
390
+ try {
391
+ closeClaimInvalid(error.message);
392
+ } finally {
393
+ this.quarantineGate(response.gate_id);
394
+ }
395
+ throw error;
396
+ }
381
397
  closeClaimInvalid(error instanceof Error ? error.message : "invalid_answer");
382
398
  this.#scheduleRecovery();
383
399
  throw error;
@@ -12,6 +12,7 @@
12
12
  * swapped in later without changing callers.
13
13
  */
14
14
  import { createHash } from "node:crypto";
15
+ import { deepInterviewCharacterCount } from "../../../gjc-runtime/deep-interview-state";
15
16
  import type { JsonSchema, WorkflowGateValidationError } from "./workflow-gate-types";
16
17
 
17
18
  /** Keywords this wrapper understands. Any other keyword is rejected. */
@@ -238,7 +239,8 @@ function validateValue(schema: JsonSchema, value: unknown, path: string, errors:
238
239
  if (!ok) errors.push({ path, keyword: "enum", message: "value not in enum", expected: schema.enum });
239
240
  }
240
241
  if (typeof value === "string") {
241
- if (schema.minLength !== undefined && value.length < schema.minLength) {
242
+ const characterCount = deepInterviewCharacterCount(value);
243
+ if (schema.minLength !== undefined && characterCount < schema.minLength) {
242
244
  errors.push({
243
245
  path,
244
246
  keyword: "minLength",
@@ -246,7 +248,7 @@ function validateValue(schema: JsonSchema, value: unknown, path: string, errors:
246
248
  expected: schema.minLength,
247
249
  });
248
250
  }
249
- if (schema.maxLength !== undefined && value.length > schema.maxLength) {
251
+ if (schema.maxLength !== undefined && characterCount > schema.maxLength) {
250
252
  errors.push({
251
253
  path,
252
254
  keyword: "maxLength",