@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
package/src/main.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  VERSION,
21
21
  } from "@gajae-code/utils";
22
22
  import chalk from "chalk";
23
+ import { runCli } from "./cli";
23
24
  import type { Args } from "./cli/args";
24
25
  import { processFileArguments } from "./cli/file-processor";
25
26
  import { buildInitialMessage } from "./cli/initial-message";
@@ -36,6 +37,8 @@ import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory }
36
37
  import { initializeWithSettings } from "./discovery";
37
38
  import { exportFromFile } from "./export/html";
38
39
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
40
+ import { admitManagedOwnerBeforeCli, completeManagedOwnerRecovery } from "./gjc-runtime/managed-owner-admission";
41
+ import { isManagedOwnerSupervisorArgv, runManagedOwnerSupervisor } from "./gjc-runtime/managed-owner-supervisor";
39
42
  import { persistCoordinatorRuntimeInputReady } from "./gjc-runtime/session-state-sidecar";
40
43
  import { isTmuxOwnerIsolationCliArgv, runTmuxOwnerIsolationCliFromStdin } from "./gjc-runtime/tmux-owner-isolation-cli";
41
44
  import type { AcpStartupOptions } from "./modes/acp/startup-options";
@@ -58,6 +61,7 @@ import type { AgentSession } from "./session/agent-session";
58
61
  import {
59
62
  type ResumeSessionIdentity,
60
63
  resolveResumableSession,
64
+ type SessionDestination,
61
65
  type SessionDirectoryMigrationPolicy,
62
66
  type SessionInfo,
63
67
  SessionManager,
@@ -535,7 +539,8 @@ type ListForResumePickerReadOnly = (cwd: string, sessionDir?: string) => Promise
535
539
  type SelectResumeSession = (sessions: SessionInfo[]) => Promise<SessionSelectionResult>;
536
540
  type OpenExistingSessionStrict = (
537
541
  identity: ResumeSessionIdentity,
538
- sessionDir?: string,
542
+ destination: SessionDestination,
543
+ storage?: undefined,
539
544
  migrationPolicy?: SessionDirectoryMigrationPolicy,
540
545
  ) => Promise<StrictSessionOpenResult>;
541
546
 
@@ -722,6 +727,10 @@ export async function createSessionManager(
722
727
  activeSettings: Settings = settings,
723
728
  ): Promise<SessionManager | undefined> {
724
729
  const migrationPolicy = activeSettings.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
730
+ const sessionDestination = () =>
731
+ parsed.sessionDir
732
+ ? SessionManager.explicitDestination(parsed.sessionDir)
733
+ : SessionManager.managedDestination(cwd, activeSettings.getAgentDir());
725
734
  if (parsed.resume === true) {
726
735
  return undefined;
727
736
  }
@@ -731,13 +740,13 @@ export async function createSessionManager(
731
740
  }
732
741
  const forkSource = parsed.fork;
733
742
  if (forkSource.includes("/") || forkSource.includes("\\") || forkSource.endsWith(".jsonl")) {
734
- return await SessionManager.forkFrom(forkSource, cwd, parsed.sessionDir, undefined, migrationPolicy);
743
+ return await SessionManager.forkFrom(forkSource, cwd, sessionDestination(), undefined, migrationPolicy);
735
744
  }
736
745
  const match = await resolveResumableSession(forkSource, cwd, parsed.sessionDir);
737
746
  if (!match) {
738
747
  throw new Error(`Session "${forkSource}" not found.`);
739
748
  }
740
- return await SessionManager.forkFrom(match.session.path, cwd, parsed.sessionDir, undefined, migrationPolicy);
749
+ return await SessionManager.forkFrom(match.session.path, cwd, sessionDestination(), undefined, migrationPolicy);
741
750
  }
742
751
 
743
752
  if (parsed.noSession) {
@@ -746,7 +755,12 @@ export async function createSessionManager(
746
755
  if (typeof parsed.resume === "string") {
747
756
  const sessionArg = parsed.resume;
748
757
  if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
749
- return await SessionManager.open(sessionArg, parsed.sessionDir, undefined, migrationPolicy);
758
+ return await SessionManager.open(
759
+ sessionArg,
760
+ SessionManager.explicitDestination(path.dirname(sessionArg)),
761
+ undefined,
762
+ migrationPolicy,
763
+ );
750
764
  }
751
765
  const match = await resolveResumableSession(sessionArg, cwd, parsed.sessionDir);
752
766
  if (!match) {
@@ -763,21 +777,21 @@ export async function createSessionManager(
763
777
  return await SessionManager.forkFrom(
764
778
  match.session.path,
765
779
  cwd,
766
- parsed.sessionDir,
780
+ sessionDestination(),
767
781
  undefined,
768
782
  migrationPolicy,
769
783
  );
770
784
  }
771
785
  }
772
- return await SessionManager.open(match.session.path, parsed.sessionDir, undefined, migrationPolicy);
786
+ return await SessionManager.open(match.session.path, sessionDestination(), undefined, migrationPolicy);
773
787
  }
774
788
  if (parsed.continue) {
775
- return await SessionManager.continueRecent(cwd, parsed.sessionDir, undefined, migrationPolicy);
789
+ return await SessionManager.continueRecent(cwd, sessionDestination(), undefined, migrationPolicy);
776
790
  }
777
791
  // --resume without value is handled separately (needs picker UI)
778
792
  // If --session-dir provided without --continue/--resume, create new session there
779
793
  if (parsed.sessionDir) {
780
- return SessionManager.create(cwd, parsed.sessionDir);
794
+ return SessionManager.create(cwd, SessionManager.explicitDestination(parsed.sessionDir));
781
795
  }
782
796
  // A lifecycle `/session_create` child must start a FRESH session that adopts
783
797
  // the pre-allocated id (GJC_SESSION_ID), never auto-resume existing history in
@@ -795,14 +809,13 @@ export async function createSessionManager(
795
809
  // buildSessionOptions restores the session's model/thinking instead of
796
810
  // overriding them with CLI defaults.
797
811
  if (activeSettings.get("autoResume")) {
798
- const manager = await SessionManager.continueRecent(cwd, parsed.sessionDir, undefined, migrationPolicy);
812
+ const manager = await SessionManager.continueRecent(cwd, sessionDestination(), undefined, migrationPolicy);
799
813
  if (manager.getEntries().length > 0) {
800
814
  parsed.continue = true;
801
815
  }
802
816
  return manager;
803
817
  }
804
- const sessionDir = parsed.sessionDir ?? SessionManager.getDefaultSessionDir(cwd, activeSettings.getAgentDir());
805
- return SessionManager.create(cwd, sessionDir);
818
+ return SessionManager.create(cwd, SessionManager.managedDestination(cwd, activeSettings.getAgentDir()));
806
819
  }
807
820
 
808
821
  async function maybeAutoChdir(parsed: Args): Promise<void> {
@@ -1106,7 +1119,9 @@ export async function runRootCommand(
1106
1119
  try {
1107
1120
  opened = await (deps.openExistingSessionStrict ?? SessionManager.openExistingStrict)(
1108
1121
  selection.identity,
1109
- parsedArgs.sessionDir,
1122
+ parsedArgs.sessionDir
1123
+ ? SessionManager.explicitDestination(parsedArgs.sessionDir)
1124
+ : SessionManager.managedDestination(resumeCwd, settings.getAgentDir()),
1110
1125
  undefined,
1111
1126
  resumeMigrationPolicy,
1112
1127
  );
@@ -1604,6 +1619,15 @@ export async function main(args: string[]): Promise<void> {
1604
1619
  await runTmuxOwnerIsolationCliFromStdin();
1605
1620
  return;
1606
1621
  }
1607
- const { runCli } = await import("./cli");
1622
+ if (isManagedOwnerSupervisorArgv(args)) {
1623
+ await runManagedOwnerSupervisor();
1624
+ return;
1625
+ }
1626
+ const admission = await admitManagedOwnerBeforeCli();
1627
+ if (admission.kind === "blocked") return;
1628
+ if (admission.kind === "recovery") {
1629
+ await completeManagedOwnerRecovery(admission.context);
1630
+ return;
1631
+ }
1608
1632
  await runCli(args.length === 0 ? ["launch"] : args);
1609
1633
  }
@@ -74,6 +74,7 @@ interface PromptWaiter {
74
74
  /** Highest inbound frame sequence already observed when the prompt was acknowledged. */
75
75
  boundary: number;
76
76
  correlation: PromptCorrelation;
77
+ messageProgress?: { textEmitted: boolean; thoughtEmitted: boolean };
77
78
  pendingTerminal?: PromptCorrelation;
78
79
  resolve: (response: PromptResponse) => void;
79
80
  reject: (error: Error) => void;
@@ -1298,9 +1299,16 @@ export class AcpAgent implements Agent {
1298
1299
  if (wirePayload) {
1299
1300
  for (const notification of mapAgentWireEventPayloadToAcpSessionUpdates(wirePayload as never, id, {
1300
1301
  cwd: record.cwd,
1302
+ getMessageProgress: message => {
1303
+ if (!activePrompt || !object(message)) return undefined;
1304
+ activePrompt.messageProgress ??= { textEmitted: false, thoughtEmitted: false };
1305
+ return activePrompt.messageProgress;
1306
+ },
1301
1307
  }))
1302
1308
  await this.#publishSessionUpdate(id, notification, adapter);
1303
1309
  }
1310
+ if (event.type === "message_end" && object(event.message)?.role === "assistant" && activePrompt)
1311
+ activePrompt.messageProgress = undefined;
1304
1312
  if (event.type === "agent_end") await this.#emitEndOfTurnUpdates(id, adapter);
1305
1313
  if (activePrompt) this.#settlePrompt(record, activePrompt);
1306
1314
  }
@@ -0,0 +1,54 @@
1
+ /** Removes every terminal control sequence except bounded SGR parameters (0-255). */
2
+ export function validateDisplayLine(line: string): string {
3
+ let result = "";
4
+ for (let index = 0; index < line.length; index++) {
5
+ const char = line[index]!;
6
+ const code = char.charCodeAt(0);
7
+ if (char === "\t") {
8
+ result += " ";
9
+ continue;
10
+ }
11
+ if (char === "\x1b") {
12
+ const next = line[index + 1];
13
+ if (next === "[") {
14
+ const match = /^\x1b\[([0-9]{1,3}(?:;[0-9]{1,3})*)m/.exec(line.slice(index));
15
+ if (match && match[0].length <= 64 && match[1]!.split(";").every(parameter => Number(parameter) <= 255)) {
16
+ result += match[0];
17
+ index += match[0].length - 1;
18
+ continue;
19
+ }
20
+ index = skipEscapeSequence(line, index + 2, /[@-~]/);
21
+ continue;
22
+ }
23
+ if (next === "]" || next === "P" || next === "_" || next === "^" || next === "X") {
24
+ index = skipStringControl(line, index + 2);
25
+ continue;
26
+ }
27
+ if (next === "_") {
28
+ index = skipStringControl(line, index + 2);
29
+ continue;
30
+ }
31
+ index += next ? 1 : 0;
32
+ continue;
33
+ }
34
+ if (code >= 0x80 && code <= 0x9f) continue;
35
+ if (code < 0x20 || code === 0x7f) continue;
36
+ result += char;
37
+ }
38
+ return result;
39
+ }
40
+
41
+ function skipEscapeSequence(text: string, start: number, final: RegExp): number {
42
+ for (let index = start; index < text.length; index++) {
43
+ if (final.test(text[index]!)) return index;
44
+ }
45
+ return text.length - 1;
46
+ }
47
+
48
+ function skipStringControl(text: string, start: number): number {
49
+ for (let index = start; index < text.length; index++) {
50
+ if (text[index] === "\x07") return index;
51
+ if (text[index] === "\x1b" && text[index + 1] === "\\") return index + 1;
52
+ }
53
+ return text.length - 1;
54
+ }
@@ -59,9 +59,22 @@ const DEFAULT_ACTION_KEYS = Object.fromEntries(
59
59
 
60
60
  const PASTE_DECISION_TIMEOUT_MS = 5_000;
61
61
  const PENDING_PASTE_INPUT_MAX = 64;
62
+ const PENDING_PASTE_INPUT_MAX_BYTES = 256 * 1024;
62
63
 
63
64
  type PastePendingClearReason = "timeout" | "queue-limit";
64
65
 
66
+ export interface PasteTextContext {
67
+ readonly signal: AbortSignal;
68
+ /** Run one synchronous composer mutation only while this paste transaction is current. */
69
+ commit(commit: () => boolean): boolean;
70
+ }
71
+
72
+ interface PendingPasteDecision {
73
+ readonly controller: AbortController;
74
+ readonly pasteContent: string;
75
+ readonly remaining: string;
76
+ }
77
+
65
78
  /**
66
79
  * Custom editor that handles configurable app-level shortcuts for coding-agent.
67
80
  */
@@ -94,9 +107,9 @@ export class CustomEditor extends Editor {
94
107
  /** Called when the configured image-paste shortcut is pressed. */
95
108
  onPasteImage?: () => Promise<boolean>;
96
109
  /** Called before bracketed paste content is inserted. Return true to consume it. */
97
- onPasteText?: (text: string) => boolean | Promise<boolean>;
98
- /** Called when async paste handling drops queued input instead of replaying it. */
99
- onPastePendingInputCleared?: (reason: PastePendingClearReason, droppedInputCount: number) => void;
110
+ onPasteText?: (text: string, context: PasteTextContext) => boolean | Promise<boolean>;
111
+ /** Called when pending paste handling is cancelled after restoring queued input. */
112
+ onPastePendingInputCleared?: (reason: PastePendingClearReason, restoredInputCount: number) => void;
100
113
  /** Called when the configured dequeue shortcut is pressed. */
101
114
  onDequeue?: () => void;
102
115
  /** Called when the configured queue shortcut is pressed. */
@@ -117,7 +130,9 @@ export class CustomEditor extends Editor {
117
130
  #pasteDecisionPending = false;
118
131
  #pasteDecisionToken = 0;
119
132
  #pasteDecisionTimeout: NodeJS.Timeout | undefined;
133
+ #pendingPasteDecision: PendingPasteDecision | undefined;
120
134
  #pendingPasteInput: string[] = [];
135
+ #pendingPasteInputBytes = 0;
121
136
 
122
137
  setActionKeys(action: ConfigurableEditorAction, keys: KeyId[]): void {
123
138
  this.#actionKeys.set(action, [...keys]);
@@ -165,75 +180,127 @@ export class CustomEditor extends Editor {
165
180
  }
166
181
  }
167
182
 
168
- #clearPendingPasteState(): number {
183
+ #resetPendingPasteState(abortReason?: Error): string[] {
169
184
  this.#clearPasteDecisionTimeout();
185
+ if (abortReason) this.#pendingPasteDecision?.controller.abort(abortReason);
170
186
  this.#pasteDecisionPending = false;
171
187
  this.#pasteDecisionToken += 1;
172
- const droppedInputCount = this.#pendingPasteInput.length;
188
+ this.#pendingPasteDecision = undefined;
189
+ const queuedInput = this.#pendingPasteInput;
173
190
  this.#pendingPasteInput = [];
174
- return droppedInputCount;
191
+ this.#pendingPasteInputBytes = 0;
192
+ return queuedInput;
193
+ }
194
+
195
+ #pendingPasteEventCount(): number {
196
+ return this.#pendingPasteInput.length + (this.#pendingPasteDecision?.remaining ? 1 : 0);
197
+ }
198
+
199
+ #restoreCancelledPaste(reason: PastePendingClearReason, overflowInput?: string): void {
200
+ const decision = this.#pendingPasteDecision;
201
+ const queuedInput = this.#resetPendingPasteState(new Error(`Paste handling cancelled: ${reason}`));
202
+ if (decision) {
203
+ super.handleInput(`\x1b[200~${decision.pasteContent}\x1b[201~`);
204
+ this.#drainPendingPasteInput(decision.remaining, queuedInput);
205
+ if (overflowInput !== undefined) this.handleInput(overflowInput);
206
+ }
207
+ this.onPastePendingInputCleared?.(
208
+ reason,
209
+ queuedInput.length + (decision?.remaining ? 1 : 0) + (overflowInput === undefined ? 0 : 1),
210
+ );
175
211
  }
176
212
 
177
213
  #startPasteDecisionTimeout(token: number): void {
178
214
  this.#clearPasteDecisionTimeout();
179
215
  this.#pasteDecisionTimeout = setTimeout(() => {
180
216
  if (token !== this.#pasteDecisionToken) return;
181
- const droppedInputCount = this.#clearPendingPasteState();
182
- this.onPastePendingInputCleared?.("timeout", droppedInputCount);
217
+ this.#restoreCancelledPaste("timeout");
183
218
  }, PASTE_DECISION_TIMEOUT_MS);
184
219
  this.#pasteDecisionTimeout.unref?.();
185
220
  }
186
221
 
187
222
  dispose(): void {
188
- this.#clearPendingPasteState();
223
+ this.#resetPendingPasteState(new Error("Editor disposed during paste handling"));
189
224
  this.#pasteHandler = new BracketedPasteHandler();
190
225
  super.dispose();
191
226
  }
192
227
 
193
- #drainPendingPasteInput(initialInput?: string): void {
228
+ #drainPendingPasteInput(initialInput: string | undefined, queuedInput: readonly string[]): void {
194
229
  if (initialInput && initialInput.length > 0) {
195
230
  this.handleInput(initialInput);
196
231
  }
197
- while (!this.#pasteDecisionPending) {
198
- const nextInput = this.#pendingPasteInput.shift();
199
- if (nextInput === undefined) break;
200
- this.handleInput(nextInput);
232
+ for (const input of queuedInput) {
233
+ this.handleInput(input);
201
234
  }
202
235
  }
203
236
 
204
237
  #handleBracketedPaste(pasteContent: string, remaining: string): void {
205
- const applyPasteResult = (token: number, handled: boolean | undefined) => {
238
+ const remainingBytes = Buffer.byteLength(remaining);
239
+ if (remainingBytes > PENDING_PASTE_INPUT_MAX_BYTES) {
240
+ super.handleInput(`\x1b[200~${pasteContent}\x1b[201~`);
241
+ if (remaining.length > 0) this.handleInput(remaining);
242
+ this.onPastePendingInputCleared?.("queue-limit", 1);
243
+ return;
244
+ }
245
+ const token = this.#pasteDecisionToken + 1;
246
+ const controller = new AbortController();
247
+ this.#pasteDecisionToken = token;
248
+ this.#pasteDecisionPending = true;
249
+ this.#pendingPasteDecision = { controller, pasteContent, remaining };
250
+ this.#pendingPasteInputBytes = remainingBytes;
251
+ let commitUsed = false;
252
+
253
+ const applyPasteResult = (handled: boolean | undefined) => {
206
254
  if (token !== this.#pasteDecisionToken) return;
207
- this.#clearPasteDecisionTimeout();
255
+ const queuedInput = this.#resetPendingPasteState();
208
256
  if (!handled) {
209
257
  super.handleInput(`\x1b[200~${pasteContent}\x1b[201~`);
210
258
  }
211
- this.#pasteDecisionPending = false;
212
- this.#drainPendingPasteInput(remaining);
259
+ this.#drainPendingPasteInput(remaining, queuedInput);
213
260
  };
214
- const pasteResult = this.onPasteText?.(pasteContent);
261
+
262
+ let pasteResult: boolean | Promise<boolean> | undefined;
263
+ try {
264
+ pasteResult = this.onPasteText?.(pasteContent, {
265
+ signal: controller.signal,
266
+ commit: commit => {
267
+ if (
268
+ commitUsed ||
269
+ token !== this.#pasteDecisionToken ||
270
+ !this.#pasteDecisionPending ||
271
+ controller.signal.aborted
272
+ ) {
273
+ return false;
274
+ }
275
+ commitUsed = true;
276
+ return commit();
277
+ },
278
+ });
279
+ } catch {
280
+ applyPasteResult(false);
281
+ return;
282
+ }
215
283
 
216
284
  if (pasteResult instanceof Promise) {
217
- const token = this.#pasteDecisionToken + 1;
218
- this.#pasteDecisionToken = token;
219
- this.#pasteDecisionPending = true;
220
285
  this.#startPasteDecisionTimeout(token);
221
- void pasteResult.then(
222
- handled => applyPasteResult(token, handled),
223
- () => applyPasteResult(token, false),
224
- );
286
+ void pasteResult.then(applyPasteResult, () => applyPasteResult(false));
225
287
  } else {
226
- applyPasteResult(this.#pasteDecisionToken, pasteResult);
288
+ applyPasteResult(pasteResult);
227
289
  }
228
290
  }
229
291
 
230
292
  handleInput(data: string): void {
231
293
  if (this.#pasteDecisionPending) {
232
- this.#pendingPasteInput.push(data);
233
- if (this.#pendingPasteInput.length > PENDING_PASTE_INPUT_MAX) {
234
- const droppedInputCount = this.#clearPendingPasteState();
235
- this.onPastePendingInputCleared?.("queue-limit", droppedInputCount);
294
+ const inputBytes = Buffer.byteLength(data);
295
+ if (
296
+ this.#pendingPasteEventCount() >= PENDING_PASTE_INPUT_MAX ||
297
+ this.#pendingPasteInputBytes + inputBytes > PENDING_PASTE_INPUT_MAX_BYTES
298
+ ) {
299
+ this.#restoreCancelledPaste("queue-limit", data);
300
+ return;
236
301
  }
302
+ this.#pendingPasteInput.push(data);
303
+ this.#pendingPasteInputBytes += inputBytes;
237
304
  return;
238
305
  }
239
306
 
@@ -245,8 +312,12 @@ export class CustomEditor extends Editor {
245
312
  }
246
313
 
247
314
  if (this.onPasteText) {
248
- const paste = this.#pasteHandler.process(data);
315
+ const paste =
316
+ data === "\x1b" && !this.#pasteHandler.hasPendingFrame
317
+ ? ({ handled: false } as const)
318
+ : this.#pasteHandler.process(data);
249
319
  if (paste.handled) {
320
+ if (paste.leading.length > 0) this.handleInput(paste.leading);
250
321
  if (paste.pasteContent !== undefined) {
251
322
  this.onViewportFollowLive?.();
252
323
  this.#handleBracketedPaste(paste.pasteContent, paste.remaining);
@@ -41,6 +41,7 @@ export class CustomProviderWizardComponent extends Container {
41
41
  credential: "",
42
42
  models: "",
43
43
  };
44
+ #submitInFlight = false;
44
45
  #onSubmit: (input: CustomProviderWizardSubmit) => void;
45
46
  #onCancel: () => void;
46
47
  #onRender: () => void;
@@ -294,7 +295,7 @@ export class CustomProviderWizardComponent extends Container {
294
295
  this.#step = "credential";
295
296
  } else if (this.#step === "confirm" || this.#step === "force-confirm") {
296
297
  if (this.#selectedIndex === 0) {
297
- this.#onSubmit(this.#buildInput(this.#step === "force-confirm"));
298
+ this.#submit();
298
299
  return;
299
300
  }
300
301
  this.#step = "models";
@@ -303,6 +304,30 @@ export class CustomProviderWizardComponent extends Container {
303
304
  this.#onRender();
304
305
  }
305
306
 
307
+ #submit(): void {
308
+ if (this.#submitInFlight) return;
309
+ this.#submitInFlight = true;
310
+ let submission: unknown;
311
+ try {
312
+ submission = this.#onSubmit(this.#buildInput(this.#step === "force-confirm"));
313
+ } catch (error) {
314
+ this.#submitInFlight = false;
315
+ throw error;
316
+ }
317
+ if (!(submission instanceof Promise)) {
318
+ this.#submitInFlight = false;
319
+ return;
320
+ }
321
+ void submission.then(
322
+ () => {
323
+ this.#submitInFlight = false;
324
+ },
325
+ () => {
326
+ this.#submitInFlight = false;
327
+ },
328
+ );
329
+ }
330
+
306
331
  #buildInput(force: boolean): CustomProviderWizardSubmit {
307
332
  const input = {
308
333
  compatibility: this.#state.compatibility,
@@ -30,6 +30,8 @@ export interface NotificationsEditorPreferences {
30
30
  sessionScope: "all" | "primary";
31
31
  richEnabled: boolean;
32
32
  richDraftEnabled: boolean;
33
+ toolActivityEnabled: boolean;
34
+ streamingEnabled: boolean;
33
35
  }
34
36
 
35
37
  /** Secret-safe snapshot used to render the Notifications tab. */
@@ -50,6 +52,7 @@ export interface NotificationsEditorSetupInput {
50
52
  chatId?: string;
51
53
  richEnabled: boolean;
52
54
  richDraftEnabled: boolean;
55
+ streamingEnabled: boolean;
53
56
  }
54
57
 
55
58
  /**
@@ -62,6 +65,7 @@ export interface PreparedTelegramConfiguration {
62
65
  tokenFingerprint?: string;
63
66
  richEnabled: boolean;
64
67
  richDraftEnabled: boolean;
68
+ streamingEnabled: boolean;
65
69
  }
66
70
 
67
71
  export interface NotificationsPreflightResult {
@@ -155,7 +159,8 @@ type HomeActionId =
155
159
  | "recover"
156
160
  | "reconnect"
157
161
  | "remove"
158
- | "preferences";
162
+ | "preferences"
163
+ | "tool-activity";
159
164
 
160
165
  interface Action {
161
166
  id: HomeActionId | "telegram" | "external" | "save" | "save-inactive" | "cancel" | "confirm";
@@ -190,6 +195,8 @@ function emptyState(): NotificationsEditorState {
190
195
  sessionScope: "all",
191
196
  richEnabled: true,
192
197
  richDraftEnabled: false,
198
+ toolActivityEnabled: true,
199
+ streamingEnabled: true,
193
200
  },
194
201
  };
195
202
  }
@@ -549,6 +556,7 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
549
556
  chatId,
550
557
  richEnabled: preferences.richEnabled,
551
558
  richDraftEnabled: preferences.richDraftEnabled,
559
+ streamingEnabled: preferences.streamingEnabled,
552
560
  },
553
561
  signal,
554
562
  ),
@@ -928,6 +936,12 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
928
936
  case "refresh":
929
937
  draft.richDraftEnabled = !draft.richDraftEnabled;
930
938
  return;
939
+ case "tool-activity":
940
+ draft.toolActivityEnabled = !draft.toolActivityEnabled;
941
+ return;
942
+ case "test":
943
+ draft.streamingEnabled = !draft.streamingEnabled;
944
+ return;
931
945
  default:
932
946
  return;
933
947
  }
@@ -1068,6 +1082,16 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
1068
1082
  label: `Telegram rich drafts: ${draft.richDraftEnabled ? "on" : "off"}`,
1069
1083
  description: "Toggle the unsaved rich-draft preference.",
1070
1084
  },
1085
+ {
1086
+ id: "tool-activity",
1087
+ label: `Telegram tool activity: ${draft.toolActivityEnabled ? "on" : "off"}`,
1088
+ description: "Toggle tool start and completion notifications.",
1089
+ },
1090
+ {
1091
+ id: "test",
1092
+ label: `Telegram streaming: ${draft.streamingEnabled ? "on" : "off"}`,
1093
+ description: "Toggle the unsaved Telegram streaming preference.",
1094
+ },
1071
1095
  { id: "save", label: "Save preferences", description: "Atomically persist this preference draft." },
1072
1096
  { id: "cancel", label: "Cancel and discard draft", description: "Leave saved preferences unchanged." },
1073
1097
  ];
@@ -1162,7 +1186,7 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
1162
1186
  );
1163
1187
  lines.push(
1164
1188
  this.#truncate(
1165
- ` Private chat: ${draft?.chatId ?? "(expired)"} rich: ${draft?.richEnabled ? "on" : "off"} drafts: ${draft?.richDraftEnabled ? "on" : "off"}`,
1189
+ ` Private chat: ${draft?.chatId ?? "(expired)"} rich: ${draft?.richEnabled ? "on" : "off"} drafts: ${draft?.richDraftEnabled ? "on" : "off"} streaming: ${draft?.streamingEnabled ? "on" : "off"}`,
1166
1190
  width,
1167
1191
  ),
1168
1192
  );