@gajae-code/coding-agent 0.9.6 → 0.10.0

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 (193) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/types/cli/classify-install-target.d.ts +0 -10
  3. package/dist/types/cli/daemon-cli.d.ts +2 -0
  4. package/dist/types/cli/initial-message.d.ts +1 -1
  5. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  6. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  7. package/dist/types/cli/notify-cli.d.ts +3 -1
  8. package/dist/types/cli/session-picker.d.ts +4 -3
  9. package/dist/types/cli/update-cli.d.ts +22 -3
  10. package/dist/types/cli.d.ts +4 -0
  11. package/dist/types/commands/daemon.d.ts +6 -2
  12. package/dist/types/commands/launch.d.ts +1 -0
  13. package/dist/types/commands/notify.d.ts +6 -0
  14. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  15. package/dist/types/commit/analysis/summary.d.ts +1 -1
  16. package/dist/types/commit/changelog/generate.d.ts +1 -1
  17. package/dist/types/commit/changelog/index.d.ts +2 -2
  18. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  19. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  20. package/dist/types/config/keybindings.d.ts +5 -0
  21. package/dist/types/config/model-profile-activation.d.ts +1 -0
  22. package/dist/types/config/settings-schema.d.ts +47 -7
  23. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  24. package/dist/types/coordinator-mcp/server.d.ts +67 -6
  25. package/dist/types/daemon/control-types.d.ts +10 -0
  26. package/dist/types/daemon/operator-contract.d.ts +47 -0
  27. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  28. package/dist/types/discovery/helpers.d.ts +1 -1
  29. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  30. package/dist/types/gjc-runtime/launch-tmux.d.ts +26 -7
  31. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +34 -3
  32. package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
  33. package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +309 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +49 -17
  36. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  37. package/dist/types/main.d.ts +66 -2
  38. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  39. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  40. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  42. package/dist/types/modes/components/gajae-pet-widget.d.ts +59 -0
  43. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  44. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  45. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  46. package/dist/types/modes/components/session-selector.d.ts +22 -9
  47. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  48. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  49. package/dist/types/modes/components/user-message.d.ts +2 -1
  50. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  51. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  52. package/dist/types/modes/interactive-mode.d.ts +24 -2
  53. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  54. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  55. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  56. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  57. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  58. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  59. package/dist/types/modes/types.d.ts +28 -2
  60. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  61. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  62. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  63. package/dist/types/notifications/config.d.ts +34 -0
  64. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  65. package/dist/types/notifications/index.d.ts +27 -0
  66. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  67. package/dist/types/notifications/notification-service.d.ts +144 -0
  68. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  69. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  70. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  71. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  72. package/dist/types/notifications/topic-registry.d.ts +24 -4
  73. package/dist/types/registry/agent-registry.d.ts +2 -0
  74. package/dist/types/rlm/index.d.ts +1 -1
  75. package/dist/types/sdk.d.ts +2 -0
  76. package/dist/types/secrets/obfuscator.d.ts +1 -1
  77. package/dist/types/session/agent-session.d.ts +26 -2
  78. package/dist/types/session/cache-economics.d.ts +79 -0
  79. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  80. package/dist/types/session/session-manager.d.ts +124 -4
  81. package/dist/types/session/session-storage.d.ts +155 -13
  82. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  83. package/dist/types/task/executor.d.ts +3 -0
  84. package/dist/types/task/receipt.d.ts +3 -1
  85. package/dist/types/task/types.d.ts +8 -0
  86. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  87. package/dist/types/tools/ask.d.ts +25 -1
  88. package/dist/types/tools/bash.d.ts +1 -1
  89. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  90. package/dist/types/tools/index.d.ts +58 -6
  91. package/dist/types/tools/telegram-send.d.ts +7 -0
  92. package/dist/types/utils/external-editor.d.ts +2 -0
  93. package/package.json +9 -9
  94. package/scripts/g004-tmux-smoke.ts +196 -36
  95. package/src/cli/daemon-cli.ts +19 -29
  96. package/src/cli/malloc-env-guard.test.ts +104 -0
  97. package/src/cli/malloc-env-guard.ts +149 -0
  98. package/src/cli/notify-cli.ts +78 -14
  99. package/src/cli/session-picker.ts +23 -40
  100. package/src/cli/update-cli.ts +56 -37
  101. package/src/cli.ts +50 -10
  102. package/src/commands/daemon.ts +19 -5
  103. package/src/commands/harness.ts +515 -47
  104. package/src/commands/launch.ts +11 -3
  105. package/src/commands/notify.ts +5 -1
  106. package/src/commands/session.ts +6 -1
  107. package/src/config/keybindings.ts +5 -0
  108. package/src/config/model-profile-activation.ts +1 -1
  109. package/src/config/model-profiles.ts +14 -14
  110. package/src/config/settings-schema.ts +41 -1
  111. package/src/coordinator-mcp/model-preset.ts +100 -0
  112. package/src/coordinator-mcp/server.ts +2653 -425
  113. package/src/daemon/control-types.ts +11 -0
  114. package/src/daemon/operator-contract.ts +121 -0
  115. package/src/deep-interview/render-middleware.ts +67 -2
  116. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  117. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  118. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  119. package/src/gjc-runtime/launch-tmux.ts +702 -135
  120. package/src/gjc-runtime/session-state-sidecar.ts +756 -135
  121. package/src/gjc-runtime/team-runtime.ts +6 -0
  122. package/src/gjc-runtime/tmux-common.ts +26 -7
  123. package/src/gjc-runtime/tmux-gc.ts +52 -9
  124. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +185 -0
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +2060 -0
  126. package/src/gjc-runtime/tmux-sessions.ts +871 -68
  127. package/src/internal-urls/docs-index.generated.ts +9 -8
  128. package/src/internal-urls/local-protocol.ts +29 -10
  129. package/src/main.ts +298 -75
  130. package/src/modes/acp/acp-agent.ts +728 -98
  131. package/src/modes/acp/acp-mode.ts +29 -2
  132. package/src/modes/components/assistant-message.ts +32 -3
  133. package/src/modes/components/bash-execution.ts +43 -13
  134. package/src/modes/components/custom-editor.ts +5 -0
  135. package/src/modes/components/gajae-pet-widget.ts +360 -0
  136. package/src/modes/components/hook-selector.ts +12 -0
  137. package/src/modes/components/irc-sidebar.ts +206 -0
  138. package/src/modes/components/pet-selector.ts +55 -0
  139. package/src/modes/components/session-selector.ts +222 -221
  140. package/src/modes/components/settings-selector.ts +10 -0
  141. package/src/modes/components/tool-execution.ts +47 -6
  142. package/src/modes/components/user-message.ts +26 -9
  143. package/src/modes/controllers/command-controller.ts +28 -2
  144. package/src/modes/controllers/event-controller.ts +159 -36
  145. package/src/modes/controllers/extension-ui-controller.ts +36 -17
  146. package/src/modes/controllers/input-controller.ts +8 -2
  147. package/src/modes/controllers/selector-controller.ts +62 -15
  148. package/src/modes/interactive-mode.ts +183 -7
  149. package/src/modes/irc-observation-ledger.ts +127 -0
  150. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  151. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  152. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  153. package/src/modes/types.ts +34 -2
  154. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  155. package/src/modes/utils/irc-message.ts +138 -0
  156. package/src/modes/utils/ui-helpers.ts +216 -50
  157. package/src/notifications/config.ts +44 -0
  158. package/src/notifications/daemon-paths.ts +8 -0
  159. package/src/notifications/index.ts +535 -75
  160. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  161. package/src/notifications/notification-service.ts +710 -0
  162. package/src/notifications/rate-limit-pool.ts +46 -12
  163. package/src/notifications/telegram-daemon-cli.ts +2 -0
  164. package/src/notifications/telegram-daemon-control.ts +27 -2
  165. package/src/notifications/telegram-daemon.ts +635 -94
  166. package/src/notifications/telegram-reference.ts +73 -13
  167. package/src/notifications/topic-registry.ts +86 -9
  168. package/src/prompts/system/irc-peer-roster.md +1 -0
  169. package/src/prompts/system/subagent-system-prompt.md +1 -0
  170. package/src/prompts/system/system-prompt.md +1 -0
  171. package/src/prompts/tools/irc.md +1 -1
  172. package/src/registry/agent-registry.ts +3 -0
  173. package/src/sdk.ts +52 -1
  174. package/src/secrets/obfuscator.ts +4 -2
  175. package/src/session/agent-session.ts +396 -145
  176. package/src/session/cache-economics.ts +275 -0
  177. package/src/session/provider-safety-stop.ts +9 -0
  178. package/src/session/session-dump-format.ts +6 -0
  179. package/src/session/session-manager.ts +809 -39
  180. package/src/session/session-storage.ts +582 -38
  181. package/src/slash-commands/builtin-registry.ts +143 -9
  182. package/src/task/executor.ts +12 -3
  183. package/src/task/index.ts +8 -0
  184. package/src/task/receipt.ts +7 -2
  185. package/src/task/types.ts +26 -0
  186. package/src/tools/ask-answer-registry.ts +25 -0
  187. package/src/tools/ask.ts +355 -33
  188. package/src/tools/browser/tab-supervisor.ts +63 -6
  189. package/src/tools/browser.ts +8 -3
  190. package/src/tools/index.ts +81 -6
  191. package/src/tools/sqlite-reader.ts +8 -4
  192. package/src/tui/output-block.ts +18 -6
  193. package/src/utils/external-editor.ts +6 -1
@@ -81,9 +81,11 @@ import type { BashExecutionComponent } from "./components/bash-execution";
81
81
  import { CustomEditor } from "./components/custom-editor";
82
82
  import { DynamicBorder } from "./components/dynamic-border";
83
83
  import type { EvalExecutionComponent } from "./components/eval-execution";
84
+ import { GajaePetWidget, type PetMode } from "./components/gajae-pet-widget";
84
85
  import type { HookEditorComponent } from "./components/hook-editor";
85
86
  import type { HookInputComponent } from "./components/hook-input";
86
87
  import type { HookSelectorComponent } from "./components/hook-selector";
88
+ import { IrcSplitViewComponent } from "./components/irc-sidebar";
87
89
  import { StatusLineComponent } from "./components/status-line";
88
90
  import type { ToolExecutionHandle } from "./components/tool-execution";
89
91
  import {
@@ -99,6 +101,7 @@ import { InputController } from "./controllers/input-controller";
99
101
  import { SelectorController } from "./controllers/selector-controller";
100
102
  import { SSHCommandController } from "./controllers/ssh-command-controller";
101
103
  import { TodoCommandController } from "./controllers/todo-command-controller";
104
+ import { IrcObservationLedger } from "./irc-observation-ledger";
102
105
  import { JobsObserver } from "./jobs-observer";
103
106
  import { OAuthManualInputManager } from "./oauth-manual-input";
104
107
  import { SessionObserverRegistry } from "./session-observer-registry";
@@ -114,13 +117,42 @@ import {
114
117
  onThemeChange,
115
118
  theme,
116
119
  } from "./theme/theme";
117
- import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInput, TodoItem, TodoPhase } from "./types";
118
- import { addChatChild, UiHelpers } from "./utils/ui-helpers";
120
+ import type {
121
+ CompactionQueuedMessage,
122
+ InteractiveModeContext,
123
+ IrcArrivalSnapshot,
124
+ SubmittedUserInput,
125
+ TodoItem,
126
+ TodoPhase,
127
+ TranscriptRebuildPolicy,
128
+ } from "./types";
129
+ import type { ParsedIrcMessage } from "./utils/irc-message";
130
+ import { addChatChild, prepareTranscriptRebuild, UiHelpers } from "./utils/ui-helpers";
119
131
 
120
132
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
121
133
  const COMPOSER_NEWLINE_HINT = process.platform === "win32" ? "Alt+Enter/Ctrl+J" : "Shift+Enter/Ctrl+J";
122
134
  export const DEFAULT_COMPOSER_PLACEHOLDER = `Type your message... ${COMPOSER_NEWLINE_HINT}: New line · Ctrl+C: Clear · Ctrl+R: Search history · Shift+Tab: Reasoning`;
123
135
  const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
136
+
137
+ const IRC_SIDEBAR_TOGGLE_SHADOWING_ACTIONS: readonly AppKeybinding[] = [
138
+ "app.plan.toggle",
139
+ "app.session.new",
140
+ "app.session.tree",
141
+ "app.session.fork",
142
+ "app.session.resume",
143
+ "app.message.followUp",
144
+ "app.stt.toggle",
145
+ "app.clipboard.copyLine",
146
+ "app.session.observe",
147
+ "app.jobs.open",
148
+ "app.tool.backgroundFold",
149
+ ];
150
+
151
+ export function getWelcomeTranscriptReservedRows(chatContainer: Container, width: number): number {
152
+ return chatContainer.children.length === 0 || chatContainer.children.length > WELCOME_RESERVED_CONTAINER_CHILD_LIMIT
153
+ ? 0
154
+ : chatContainer.render(width).length;
155
+ }
124
156
  const FRIENDLY_KEY_PARTS: Record<string, string> = {
125
157
  alt: "Alt",
126
158
  cmd: "Cmd",
@@ -280,6 +312,7 @@ export class InteractiveMode implements InteractiveModeContext {
280
312
  keybindings: KeybindingsManager;
281
313
  agent: Agent;
282
314
  historyStorage?: HistoryStorage;
315
+ readonly ircLedger = new IrcObservationLedger();
283
316
 
284
317
  ui: TUI;
285
318
  chatContainer: Container;
@@ -291,6 +324,8 @@ export class InteractiveMode implements InteractiveModeContext {
291
324
  editorContainer: Container;
292
325
  hookWidgetContainerAbove: Container;
293
326
  hookWidgetContainerBelow: Container;
327
+ petFloorContainer: Container = new Container();
328
+ petWidget: GajaePetWidget | undefined;
294
329
  statusLine: StatusLineComponent;
295
330
 
296
331
  isInitialized = false;
@@ -387,6 +422,9 @@ export class InteractiveMode implements InteractiveModeContext {
387
422
  #eventBus?: EventBus;
388
423
  #eventBusUnsubscribers: Array<() => void> = [];
389
424
  #welcomeComponent?: WelcomeComponent;
425
+ #ircSplitView: IrcSplitViewComponent;
426
+ #ircSidebarAvailable = false;
427
+ #ircSidebarRequestedVisible = false;
390
428
 
391
429
  constructor(
392
430
  session: AgentSession,
@@ -423,6 +461,7 @@ export class InteractiveMode implements InteractiveModeContext {
423
461
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
424
462
  this.ui.setClearOnShrink(settings.get("clearOnShrink"));
425
463
  this.chatContainer = new Container();
464
+ this.#ircSplitView = new IrcSplitViewComponent(this.chatContainer, this.ircLedger, () => theme);
426
465
  this.pendingMessagesContainer = new Container();
427
466
  this.statusContainer = new Container();
428
467
  this.todoContainer = new Container();
@@ -442,6 +481,7 @@ export class InteractiveMode implements InteractiveModeContext {
442
481
  this.#syncEditorMaxHeight();
443
482
  this.updateEditorChrome();
444
483
  this.editor.invalidate();
484
+ this.#invalidateIrcSidebarRender();
445
485
  this.ui.requestResizeRender();
446
486
  };
447
487
  process.stdout.on("resize", this.#resizeHandler);
@@ -561,7 +601,9 @@ export class InteractiveMode implements InteractiveModeContext {
561
601
  this.#welcomeComponent.playIntro(() => this.ui.requestRender());
562
602
  }
563
603
 
564
- this.ui.addChild(this.chatContainer);
604
+ this.ui.addChild(this.#ircSplitView);
605
+ this.ui.setViewportAnchorComponent(this.#ircSplitView);
606
+
565
607
  this.ui.addChild(this.pendingMessagesContainer);
566
608
  this.ui.addChild(this.statusContainer);
567
609
  this.ui.addChild(this.todoContainer);
@@ -569,9 +611,12 @@ export class InteractiveMode implements InteractiveModeContext {
569
611
  this.ui.addChild(this.statusLine); // Main status rail + hook statuses; composer chrome is rendered by the editor.
570
612
  this.ui.addChild(this.hookWidgetContainerAbove);
571
613
  this.ui.addChild(this.editorContainer);
614
+ this.ui.addChild(this.petFloorContainer);
572
615
  this.ui.addChild(this.hookWidgetContainerBelow);
573
616
  this.ui.setBottomPinnedComponent(this.statusLine);
574
617
  this.ui.setFocus(this.editor);
618
+ this.petWidget = this.#createPetWidget(this.editor);
619
+ this.petWidget.setMode(settings.get("pet.mode"));
575
620
 
576
621
  this.#inputController.setupKeyHandlers();
577
622
  this.#inputController.setupEditorSubmitHandler();
@@ -609,6 +654,7 @@ export class InteractiveMode implements InteractiveModeContext {
609
654
  this.updateEditorChrome();
610
655
  this.#syncEditorMaxHeight();
611
656
  this.isInitialized = true;
657
+ this.#syncIrcSidebarAvailabilityFromSettings();
612
658
  this.ui.requestRender(true);
613
659
 
614
660
  // GitHub star reminder (interactive-only). Register the decline-driven
@@ -874,7 +920,7 @@ export class InteractiveMode implements InteractiveModeContext {
874
920
  }
875
921
  if (!submission.customType) {
876
922
  this.pendingImages = submission.images ? [...submission.images] : [];
877
- this.rebuildChatFromMessages();
923
+ this.rebuildChatFromMessages("reconcile-same-transcript");
878
924
  this.editor.setText(submission.text);
879
925
  }
880
926
  this.updateEditorChrome();
@@ -950,8 +996,9 @@ export class InteractiveMode implements InteractiveModeContext {
950
996
  }
951
997
 
952
998
  #getWelcomeReservedRows(width: number): number {
999
+ const transcriptRows = getWelcomeTranscriptReservedRows(this.chatContainer, width);
1000
+
953
1001
  const transientRows = [
954
- this.chatContainer,
955
1002
  this.pendingMessagesContainer,
956
1003
  this.statusContainer,
957
1004
  this.todoContainer,
@@ -965,7 +1012,7 @@ export class InteractiveMode implements InteractiveModeContext {
965
1012
  this.hookWidgetContainerBelow,
966
1013
  ].reduce((rows, component) => rows + component.render(width).length, 0);
967
1014
 
968
- return transientRows + pinnedRows;
1015
+ return transcriptRows + transientRows + pinnedRows;
969
1016
  }
970
1017
 
971
1018
  #renderShortContainerRowsForWelcomeReservation(width: number, container: Container): number {
@@ -1017,7 +1064,42 @@ export class InteractiveMode implements InteractiveModeContext {
1017
1064
  this.editor.setTopBorder(undefined);
1018
1065
  }
1019
1066
 
1020
- rebuildChatFromMessages(): void {
1067
+ setPetMode(mode: PetMode): void {
1068
+ this.petWidget?.setMode(mode);
1069
+ settings.set("pet.mode", mode);
1070
+ this.ui.requestRender();
1071
+ }
1072
+
1073
+ previewPetMode(mode: PetMode): void {
1074
+ this.petWidget?.previewMode(mode);
1075
+ this.ui.requestRender();
1076
+ }
1077
+
1078
+ restoreComposer(): void {
1079
+ if (this.petWidget) {
1080
+ this.petWidget.remountComposer();
1081
+ } else {
1082
+ this.editorContainer.clear();
1083
+ this.editorContainer.addChild(this.editor);
1084
+ }
1085
+ this.ui.setFocus(this.editor);
1086
+ }
1087
+
1088
+ #createPetWidget(editor: CustomEditor): GajaePetWidget {
1089
+ return new GajaePetWidget({
1090
+ ui: this.ui,
1091
+ editor,
1092
+ editorContainer: this.editorContainer,
1093
+ floorContainer: this.petFloorContainer,
1094
+ isWorking: () => this.loadingAnimation !== undefined,
1095
+ getComposerBottomOffset: () =>
1096
+ this.petFloorContainer.render(this.ui.terminal.columns).length +
1097
+ this.hookWidgetContainerBelow.render(this.ui.terminal.columns).length,
1098
+ });
1099
+ }
1100
+
1101
+ rebuildChatFromMessages(policy: TranscriptRebuildPolicy): void {
1102
+ prepareTranscriptRebuild(this.ui, policy);
1021
1103
  this.chatContainer.clear();
1022
1104
  const context = this.session.buildDisplaySessionContext();
1023
1105
  this.renderSessionContext(context);
@@ -1992,6 +2074,8 @@ export class InteractiveMode implements InteractiveModeContext {
1992
2074
  }
1993
2075
 
1994
2076
  stop(): void {
2077
+ this.petWidget?.dispose();
2078
+ this.petWidget = undefined;
1995
2079
  if (this.loadingAnimation) {
1996
2080
  this.loadingAnimation.stop();
1997
2081
  this.loadingAnimation = undefined;
@@ -2123,11 +2207,17 @@ export class InteractiveMode implements InteractiveModeContext {
2123
2207
  nextEditor.setText(previousText);
2124
2208
  previousEditor.dispose();
2125
2209
 
2210
+ const petMode = this.petWidget?.mode ?? settings.get("pet.mode");
2211
+ this.petWidget?.dispose();
2212
+
2126
2213
  this.editorContainer.clear();
2127
2214
  this.editor = nextEditor;
2128
2215
  this.editorContainer.addChild(nextEditor);
2129
2216
  this.ui.setFocus(nextEditor);
2130
2217
 
2218
+ this.petWidget = this.#createPetWidget(nextEditor);
2219
+ this.petWidget.setMode(petMode);
2220
+
2131
2221
  this.#inputController.setupKeyHandlers();
2132
2222
  this.#inputController.setupEditorSubmitHandler();
2133
2223
 
@@ -2284,13 +2374,32 @@ export class InteractiveMode implements InteractiveModeContext {
2284
2374
  return this.#uiHelpers.addMessageToChat(message, options);
2285
2375
  }
2286
2376
 
2377
+ addLiveIrcObservationToChat(message: ParsedIrcMessage, arrival: IrcArrivalSnapshot): Component[] {
2378
+ return this.#uiHelpers.addLiveIrcObservationToChat(message, arrival);
2379
+ }
2380
+ removeRenderedIrcInlineComponents(observationId: string): readonly Component[] | undefined {
2381
+ return this.#uiHelpers.removeRenderedIrcInlineComponents(observationId);
2382
+ }
2383
+ resetRenderedIrcInlineComponents(): readonly (readonly Component[])[] {
2384
+ return this.#uiHelpers.resetRenderedIrcInlineComponents();
2385
+ }
2386
+
2287
2387
  renderSessionContext(
2288
2388
  sessionContext: SessionContext,
2289
2389
  options?: { updateFooter?: boolean; populateHistory?: boolean },
2290
2390
  ): void {
2291
2391
  this.#uiHelpers.renderSessionContext(sessionContext, options);
2392
+ this.#eventController.reconcileIrcExpiryTimers(this.#uiHelpers.getRenderedIrcInlineComponents());
2292
2393
  }
2293
2394
 
2395
+ rebuildInitialMessages(
2396
+ policy: TranscriptRebuildPolicy,
2397
+ prebuiltContext?: SessionContext,
2398
+ options?: { preserveExistingChat?: boolean },
2399
+ ): void {
2400
+ prepareTranscriptRebuild(this.ui, policy);
2401
+ this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
2402
+ }
2294
2403
  renderInitialMessages(prebuiltContext?: SessionContext, options?: { preserveExistingChat?: boolean }): void {
2295
2404
  this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
2296
2405
  }
@@ -2303,6 +2412,10 @@ export class InteractiveMode implements InteractiveModeContext {
2303
2412
  return this.#uiHelpers.findLastAssistantMessage();
2304
2413
  }
2305
2414
 
2415
+ getAssistantViewportAnchorId(message: AssistantMessage): string {
2416
+ return this.#uiHelpers.assistantViewportAnchorId(message);
2417
+ }
2418
+
2306
2419
  extractAssistantText(message: AssistantMessage): string {
2307
2420
  return this.#uiHelpers.extractAssistantText(message);
2308
2421
  }
@@ -2553,6 +2666,10 @@ export class InteractiveMode implements InteractiveModeContext {
2553
2666
  this.#selectorController.showThemeSelector();
2554
2667
  }
2555
2668
 
2669
+ showPetSelector(): void {
2670
+ this.#selectorController.showPetSelector();
2671
+ }
2672
+
2556
2673
  showHistorySearch(): void {
2557
2674
  this.#selectorController.showHistorySearch();
2558
2675
  }
@@ -2668,6 +2785,65 @@ export class InteractiveMode implements InteractiveModeContext {
2668
2785
  this.#inputController.setToolsExpanded(expanded);
2669
2786
  }
2670
2787
 
2788
+ #resolveEffectiveIrcSidebarToggleKey(): string | null {
2789
+ for (const key of this.keybindings.getKeys("app.irc.sidebar.toggle")) {
2790
+ if (this.editor.hasActionKey(key)) continue;
2791
+ const shadowed = IRC_SIDEBAR_TOGGLE_SHADOWING_ACTIONS.some(action =>
2792
+ this.keybindings.getKeys(action).includes(key),
2793
+ );
2794
+ if (!shadowed) return key;
2795
+ }
2796
+ return null;
2797
+ }
2798
+
2799
+ captureIrcArrivalSnapshot(): IrcArrivalSnapshot {
2800
+ return {
2801
+ panelVisible: this.#ircSplitView.effectiveSidebarVisible(this.ui.terminal.columns),
2802
+ panelRequestedVisible: this.#ircSidebarRequestedVisible,
2803
+ sidebarAvailable: this.#ircSidebarAvailable,
2804
+ resolvedToggleKey: this.#resolveEffectiveIrcSidebarToggleKey(),
2805
+ };
2806
+ }
2807
+ toggleIrcSidebar(): void {
2808
+ if (
2809
+ !this.#ircSidebarAvailable ||
2810
+ this.settings.get("irc.enabled") !== true ||
2811
+ this.settings.get("irc.sidebar.enabled") !== true
2812
+ )
2813
+ return;
2814
+ this.#ircSidebarRequestedVisible = !this.#ircSidebarRequestedVisible;
2815
+ this.#ircSplitView.setVisible(this.#ircSidebarRequestedVisible);
2816
+ this.#invalidateIrcSidebarRender();
2817
+ this.ui.requestRender();
2818
+ }
2819
+
2820
+ applyIrcSidebarAvailability(enabled: boolean): void {
2821
+ this.#ircSidebarAvailable = enabled;
2822
+ this.#ircSplitView.setVisible(enabled && this.#ircSidebarRequestedVisible);
2823
+ this.#invalidateIrcSidebarRender();
2824
+ this.ui.requestRender();
2825
+ }
2826
+
2827
+ #syncIrcSidebarAvailabilityFromSettings(): void {
2828
+ this.applyIrcSidebarAvailability(
2829
+ this.settings.get("irc.enabled") === true && this.settings.get("irc.sidebar.enabled") === true,
2830
+ );
2831
+ }
2832
+
2833
+ resetIrcSidebarSession(): void {
2834
+ this.ircLedger.reset();
2835
+ this.#eventController.resetIrcObservations();
2836
+ this.#ircSidebarRequestedVisible = false;
2837
+ this.#ircSplitView.setVisible(false);
2838
+ this.#uiHelpers.resetIrcSidebarHint();
2839
+ this.#syncIrcSidebarAvailabilityFromSettings();
2840
+ }
2841
+
2842
+ #invalidateIrcSidebarRender(): void {
2843
+ clearRenderCache();
2844
+ this.#ircSplitView.invalidate();
2845
+ }
2846
+
2671
2847
  toggleThinkingBlockVisibility(): void {
2672
2848
  this.#inputController.toggleThinkingBlockVisibility();
2673
2849
  }
@@ -0,0 +1,127 @@
1
+ import * as crypto from "node:crypto";
2
+
3
+ import type { ParsedIrcMessage } from "./utils/irc-message";
4
+
5
+ type InlineMode = "persistent" | "ephemeral";
6
+
7
+ const MAX_RECORDS = 10_000;
8
+ const MAX_RETAINED_UTF8_BYTES = 16 * 1024 * 1024;
9
+ const MAX_SEEN_IDENTITIES = 100_000;
10
+
11
+ export type IrcObservationRecord = Readonly<
12
+ ParsedIrcMessage & {
13
+ mode: InlineMode;
14
+ observedAt: number;
15
+ sequence: number;
16
+ expiresAt?: number;
17
+ retainedUtf8Bytes: number;
18
+ }
19
+ >;
20
+
21
+ function measureRetainedUtf8Bytes(message: ParsedIrcMessage): number {
22
+ let bytes = 4;
23
+ for (const value of [message.observationId, message.from, message.to, message.text, message.kind]) {
24
+ bytes += Buffer.byteLength(value, "utf8");
25
+ if (bytes > MAX_RETAINED_UTF8_BYTES) return bytes;
26
+ }
27
+ return bytes;
28
+ }
29
+
30
+ function tombstoneIdentity(observationId: string): string {
31
+ const hash = crypto.createHash("sha256");
32
+ hash.update("gjc:irc:observation-id:utf16le:v1\0");
33
+ for (let offset = 0; offset < observationId.length; offset += 4_096) {
34
+ hash.update(Buffer.from(observationId.slice(offset, offset + 4_096), "utf16le"));
35
+ }
36
+ return hash.digest("hex");
37
+ }
38
+
39
+ /** Runtime-only IRC observations. This intentionally has no persistence layer. */
40
+ export class IrcObservationLedger {
41
+ #records = new Map<string, IrcObservationRecord>();
42
+ #retainedUtf8Bytes = 0;
43
+ #nextSequence = 0;
44
+ #seenObservationIdentities = new Set<string>();
45
+ #identityCapacityExhausted = false;
46
+ #evictedObservationIds = new Set<string>();
47
+
48
+ #rememberObservationIdentity(observationId: string): boolean {
49
+ const identity = tombstoneIdentity(observationId);
50
+ if (this.#seenObservationIdentities.has(identity) || this.#identityCapacityExhausted) return false;
51
+ if (this.#seenObservationIdentities.size >= MAX_SEEN_IDENTITIES) {
52
+ this.#identityCapacityExhausted = true;
53
+ return false;
54
+ }
55
+ this.#seenObservationIdentities.add(identity);
56
+ return true;
57
+ }
58
+
59
+ #evict(observationId: string): void {
60
+ const record = this.#records.get(observationId);
61
+ if (!record) return;
62
+ this.#records.delete(observationId);
63
+ this.#retainedUtf8Bytes -= record.retainedUtf8Bytes;
64
+ this.#evictedObservationIds.add(observationId);
65
+ }
66
+
67
+ #enforceBounds(): void {
68
+ while (this.#records.size > MAX_RECORDS || this.#retainedUtf8Bytes > MAX_RETAINED_UTF8_BYTES) {
69
+ const oldestObservationId = this.#records.keys().next().value;
70
+ if (oldestObservationId === undefined) return;
71
+ this.#evict(oldestObservationId);
72
+ }
73
+ }
74
+
75
+ /** Inline observations expire after 10 seconds; closed-panel observations persist inline. */
76
+ observe(message: ParsedIrcMessage, panelVisibleAtObservation: boolean): IrcObservationRecord | undefined {
77
+ const existing = this.#records.get(message.observationId);
78
+ if (existing) return existing;
79
+ if (!this.#rememberObservationIdentity(message.observationId)) return undefined;
80
+
81
+ const estimatedBytes = measureRetainedUtf8Bytes(message);
82
+ if (estimatedBytes > MAX_RETAINED_UTF8_BYTES) return undefined;
83
+
84
+ const observedAt = Date.now();
85
+ const mode: InlineMode = panelVisibleAtObservation ? "ephemeral" : "persistent";
86
+ const record: IrcObservationRecord = Object.freeze({
87
+ ...message,
88
+ mode,
89
+ observedAt,
90
+ sequence: this.#nextSequence++,
91
+ ...(mode === "ephemeral" ? { expiresAt: observedAt + 10_000 } : {}),
92
+ retainedUtf8Bytes: estimatedBytes,
93
+ });
94
+ this.#records.set(record.observationId, record);
95
+ this.#retainedUtf8Bytes += estimatedBytes;
96
+ this.#enforceBounds();
97
+ return this.#records.get(record.observationId);
98
+ }
99
+
100
+ getRecord(observationId: string): IrcObservationRecord | undefined {
101
+ return this.#records.get(observationId);
102
+ }
103
+
104
+ getSidebarRecords(): readonly IrcObservationRecord[] {
105
+ return [...this.#records.values()];
106
+ }
107
+
108
+ getInlineProjection(now: number): readonly IrcObservationRecord[] {
109
+ return [...this.#records.values()].filter(record => record.mode === "persistent" || now < record.expiresAt!);
110
+ }
111
+
112
+ /** Returns and clears IDs whose retained payload was released. */
113
+ drainEvictedObservationIds(): readonly string[] {
114
+ const observationIds = [...this.#evictedObservationIds];
115
+ this.#evictedObservationIds.clear();
116
+ return observationIds;
117
+ }
118
+
119
+ reset(): void {
120
+ for (const observationId of this.#records.keys()) {
121
+ this.#evictedObservationIds.add(observationId);
122
+ }
123
+ this.#records.clear();
124
+ this.#retainedUtf8Bytes = 0;
125
+ this.#nextSequence = 0;
126
+ }
127
+ }
@@ -44,6 +44,8 @@ export interface AskGateQuestion {
44
44
  deepInterview?: AskGateDeepInterviewState;
45
45
  /** Override the emitted workflow gate address for non-deep-interview ask prompts. */
46
46
  workflowGate?: AskGateWorkflowGateMeta;
47
+ allowEmpty?: boolean;
48
+ navigationLabel?: "Next" | "Done";
47
49
  }
48
50
 
49
51
  export interface AskGateResult {
@@ -119,7 +121,7 @@ function questionAnswerSchema(question: AskGateQuestion, labels: string[]): RpcJ
119
121
  const selectedBase: RpcJsonSchema = { type: "array", items: selectedItems, uniqueItems: true };
120
122
  const selectedOnly: RpcJsonSchema = {
121
123
  ...selectedBase,
122
- minItems: 1,
124
+ minItems: question.allowEmpty ? 0 : 1,
123
125
  ...(multi ? {} : { maxItems: 1 }),
124
126
  };
125
127
  const selectedWithOther: RpcJsonSchema = {
@@ -216,6 +218,8 @@ export function questionToGate(question: AskGateQuestion): OpenGateInput {
216
218
  options: labels,
217
219
  other_option: GATE_OTHER_OPTION,
218
220
  clarification_action: "clarify",
221
+ allow_empty: question.allowEmpty === true,
222
+ navigation_label: question.navigationLabel,
219
223
  ...(question.deepInterview
220
224
  ? structuredDeepInterviewState(question.deepInterview)
221
225
  : deepInterviewQuestionState(question.question)),
@@ -280,12 +284,21 @@ export function gateAnswerToResult(question: AskGateQuestion, answer: unknown):
280
284
  }
281
285
  const other = answer.other === true;
282
286
  const totalPicks = deduped.length + (other ? 1 : 0);
283
- if (totalPicks === 0) {
287
+ if (totalPicks === 0 && !question.allowEmpty) {
284
288
  throw new DeepInterviewGateError(
285
289
  "empty_selection",
286
290
  "at least one option (or the free-text other) must be selected",
287
291
  );
288
292
  }
293
+ if (totalPicks === 0) {
294
+ return {
295
+ id: question.id,
296
+ question: question.question,
297
+ options: labels,
298
+ multi,
299
+ selectedOptions: [],
300
+ };
301
+ }
289
302
  if (!multi && totalPicks > 1) {
290
303
  throw new DeepInterviewGateError("multi_not_allowed", "this question accepts a single selection");
291
304
  }
@@ -302,6 +315,36 @@ export function gateAnswerToResult(question: AskGateQuestion, answer: unknown):
302
315
  };
303
316
  }
304
317
 
318
+ /**
319
+ * Classify an accepted ask-domain gate answer independently of its transport.
320
+ * Schema-invalid and semantically invalid answers throw, so callers leave the
321
+ * durable gate pending rather than treating them as a non-committing answer.
322
+ */
323
+ export function classifyAskGateDisposition(
324
+ gate: Pick<OpenGateInput, "context" | "options">,
325
+ answer: unknown,
326
+ ): "commit" | "resolve_without_commit" {
327
+ const state = gate.context?.stage_state;
328
+ if (typeof state !== "object" || state === null) return "commit";
329
+ const questionId = (state as Record<string, unknown>).question_id;
330
+ if (typeof questionId !== "string") return "commit";
331
+ const question: AskGateQuestion = {
332
+ id: questionId,
333
+ question: typeof gate.context?.prompt === "string" ? gate.context.prompt : questionId,
334
+ options: (gate.options ?? []).map(option => ({
335
+ label: typeof option.value === "string" ? option.value : option.label,
336
+ })),
337
+ multi: (state as Record<string, unknown>).multi === true,
338
+ allowEmpty: (state as Record<string, unknown>).allow_empty === true,
339
+ };
340
+ if (typeof answer === "object" && answer !== null && (answer as Record<string, unknown>).action === "clarify") {
341
+ gateAnswerToResult(question, answer);
342
+ return "resolve_without_commit";
343
+ }
344
+ const result = gateAnswerToResult(question, answer);
345
+ return result.selectedOptions.length === 0 && result.customInput === undefined ? "resolve_without_commit" : "commit";
346
+ }
347
+
305
348
  /** Convenience: map a batch of ask questions to gate open-inputs. */
306
349
  export function questionsToGates(questions: AskGateQuestion[]): OpenGateInput[] {
307
350
  return questions.map(questionToGate);