@gajae-code/coding-agent 0.9.6 → 0.10.1

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 (226) hide show
  1. package/CHANGELOG.md +58 -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 +5 -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/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator/contract.d.ts +1 -1
  25. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  26. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  27. package/dist/types/coordinator-mcp/server.d.ts +79 -6
  28. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  29. package/dist/types/daemon/control-types.d.ts +10 -0
  30. package/dist/types/daemon/operator-contract.d.ts +47 -0
  31. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  32. package/dist/types/discovery/helpers.d.ts +1 -1
  33. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  34. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  35. package/dist/types/gjc-runtime/launch-tmux.d.ts +26 -7
  36. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +34 -3
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
  38. package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  39. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +309 -0
  40. package/dist/types/gjc-runtime/tmux-sessions.d.ts +49 -17
  41. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  42. package/dist/types/main.d.ts +66 -2
  43. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  44. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  45. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  46. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  47. package/dist/types/modes/components/gajae-pet-widget.d.ts +59 -0
  48. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  49. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  50. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  51. package/dist/types/modes/components/session-selector.d.ts +22 -9
  52. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  54. package/dist/types/modes/components/status-line.d.ts +0 -10
  55. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  56. package/dist/types/modes/components/user-message.d.ts +2 -1
  57. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  59. package/dist/types/modes/interactive-mode.d.ts +24 -2
  60. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  61. package/dist/types/modes/print-mode.d.ts +3 -4
  62. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  63. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  64. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  65. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  66. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  67. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  68. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  69. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  70. package/dist/types/modes/types.d.ts +28 -2
  71. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  72. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  73. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  74. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  75. package/dist/types/notifications/config.d.ts +34 -0
  76. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  77. package/dist/types/notifications/index.d.ts +27 -0
  78. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  79. package/dist/types/notifications/notification-service.d.ts +144 -0
  80. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  81. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  82. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  83. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  84. package/dist/types/notifications/topic-registry.d.ts +24 -4
  85. package/dist/types/registry/agent-registry.d.ts +2 -0
  86. package/dist/types/rlm/index.d.ts +1 -1
  87. package/dist/types/sdk.d.ts +2 -0
  88. package/dist/types/secrets/obfuscator.d.ts +1 -1
  89. package/dist/types/session/agent-session.d.ts +40 -3
  90. package/dist/types/session/cache-economics.d.ts +79 -0
  91. package/dist/types/session/context-estimation.d.ts +31 -0
  92. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  93. package/dist/types/session/session-manager.d.ts +124 -4
  94. package/dist/types/session/session-storage.d.ts +155 -13
  95. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  96. package/dist/types/task/executor.d.ts +3 -0
  97. package/dist/types/task/receipt.d.ts +3 -1
  98. package/dist/types/task/types.d.ts +8 -0
  99. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  100. package/dist/types/tools/ask.d.ts +25 -1
  101. package/dist/types/tools/bash.d.ts +1 -1
  102. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  103. package/dist/types/tools/index.d.ts +58 -6
  104. package/dist/types/tools/telegram-send.d.ts +7 -0
  105. package/dist/types/utils/external-editor.d.ts +2 -0
  106. package/package.json +9 -9
  107. package/scripts/g004-tmux-smoke.ts +196 -36
  108. package/src/cli/daemon-cli.ts +19 -29
  109. package/src/cli/malloc-env-guard.test.ts +104 -0
  110. package/src/cli/malloc-env-guard.ts +149 -0
  111. package/src/cli/notify-cli.ts +78 -14
  112. package/src/cli/session-picker.ts +23 -40
  113. package/src/cli/update-cli.ts +56 -37
  114. package/src/cli.ts +51 -10
  115. package/src/commands/daemon.ts +19 -5
  116. package/src/commands/harness.ts +515 -47
  117. package/src/commands/launch.ts +11 -3
  118. package/src/commands/notify.ts +5 -1
  119. package/src/commands/session.ts +6 -1
  120. package/src/config/keybindings.ts +5 -0
  121. package/src/config/model-profile-activation.ts +1 -1
  122. package/src/config/model-profiles.ts +14 -14
  123. package/src/config/settings-schema.ts +41 -1
  124. package/src/config/settings.ts +96 -45
  125. package/src/coordinator/contract.ts +1 -0
  126. package/src/coordinator-mcp/model-preset.ts +100 -0
  127. package/src/coordinator-mcp/policy.ts +30 -0
  128. package/src/coordinator-mcp/server.ts +2881 -425
  129. package/src/coordinator-mcp/session-reaper.ts +129 -0
  130. package/src/daemon/control-types.ts +11 -0
  131. package/src/daemon/operator-contract.ts +121 -0
  132. package/src/deep-interview/render-middleware.ts +67 -2
  133. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  134. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  135. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  136. package/src/extensibility/extensions/runner.ts +1 -1
  137. package/src/extensibility/extensions/types.ts +3 -1
  138. package/src/gjc-runtime/launch-tmux.ts +702 -135
  139. package/src/gjc-runtime/session-state-sidecar.ts +756 -135
  140. package/src/gjc-runtime/team-runtime.ts +6 -0
  141. package/src/gjc-runtime/tmux-common.ts +26 -7
  142. package/src/gjc-runtime/tmux-gc.ts +52 -9
  143. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +185 -0
  144. package/src/gjc-runtime/tmux-owner-isolation.ts +2060 -0
  145. package/src/gjc-runtime/tmux-sessions.ts +871 -68
  146. package/src/internal-urls/docs-index.generated.ts +12 -11
  147. package/src/internal-urls/local-protocol.ts +29 -10
  148. package/src/main.ts +301 -80
  149. package/src/modes/acp/acp-agent.ts +730 -100
  150. package/src/modes/acp/acp-mode.ts +29 -2
  151. package/src/modes/components/assistant-message.ts +32 -3
  152. package/src/modes/components/bash-execution.ts +43 -13
  153. package/src/modes/components/custom-editor.ts +5 -0
  154. package/src/modes/components/footer.ts +3 -3
  155. package/src/modes/components/gajae-pet-widget.ts +360 -0
  156. package/src/modes/components/hook-selector.ts +12 -0
  157. package/src/modes/components/irc-sidebar.ts +206 -0
  158. package/src/modes/components/pet-selector.ts +55 -0
  159. package/src/modes/components/session-selector.ts +222 -221
  160. package/src/modes/components/settings-selector.ts +10 -0
  161. package/src/modes/components/status-line/segments.ts +13 -6
  162. package/src/modes/components/status-line/types.ts +1 -1
  163. package/src/modes/components/status-line.ts +4 -171
  164. package/src/modes/components/tool-execution.ts +47 -6
  165. package/src/modes/components/user-message.ts +26 -9
  166. package/src/modes/controllers/command-controller.ts +28 -2
  167. package/src/modes/controllers/event-controller.ts +159 -36
  168. package/src/modes/controllers/extension-ui-controller.ts +37 -18
  169. package/src/modes/controllers/input-controller.ts +8 -2
  170. package/src/modes/controllers/selector-controller.ts +62 -15
  171. package/src/modes/interactive-mode.ts +199 -7
  172. package/src/modes/irc-observation-ledger.ts +127 -0
  173. package/src/modes/print-mode.ts +228 -92
  174. package/src/modes/rpc/rpc-client.ts +58 -1
  175. package/src/modes/rpc/rpc-mode.ts +286 -85
  176. package/src/modes/rpc/rpc-types.ts +15 -1
  177. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  178. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  179. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  180. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  181. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  182. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  183. package/src/modes/types.ts +34 -2
  184. package/src/modes/utils/context-usage.ts +56 -92
  185. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  186. package/src/modes/utils/irc-message.ts +138 -0
  187. package/src/modes/utils/ui-helpers.ts +216 -50
  188. package/src/notifications/config.ts +44 -0
  189. package/src/notifications/daemon-paths.ts +8 -0
  190. package/src/notifications/index.ts +535 -75
  191. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  192. package/src/notifications/notification-service.ts +710 -0
  193. package/src/notifications/rate-limit-pool.ts +46 -12
  194. package/src/notifications/telegram-daemon-cli.ts +2 -0
  195. package/src/notifications/telegram-daemon-control.ts +27 -2
  196. package/src/notifications/telegram-daemon.ts +635 -94
  197. package/src/notifications/telegram-reference.ts +73 -13
  198. package/src/notifications/topic-registry.ts +86 -9
  199. package/src/prompts/system/irc-peer-roster.md +1 -0
  200. package/src/prompts/system/subagent-system-prompt.md +1 -0
  201. package/src/prompts/system/system-prompt.md +1 -0
  202. package/src/prompts/tools/irc.md +1 -1
  203. package/src/registry/agent-registry.ts +3 -0
  204. package/src/sdk.ts +52 -1
  205. package/src/secrets/obfuscator.ts +4 -2
  206. package/src/session/agent-session.ts +670 -217
  207. package/src/session/cache-economics.ts +275 -0
  208. package/src/session/context-estimation.ts +91 -0
  209. package/src/session/provider-safety-stop.ts +9 -0
  210. package/src/session/session-dump-format.ts +6 -0
  211. package/src/session/session-manager.ts +809 -39
  212. package/src/session/session-storage.ts +582 -38
  213. package/src/slash-commands/builtin-registry.ts +153 -9
  214. package/src/slash-commands/helpers/context-report.ts +26 -4
  215. package/src/task/executor.ts +12 -3
  216. package/src/task/index.ts +8 -0
  217. package/src/task/receipt.ts +7 -2
  218. package/src/task/types.ts +26 -0
  219. package/src/tools/ask-answer-registry.ts +25 -0
  220. package/src/tools/ask.ts +355 -33
  221. package/src/tools/browser/tab-supervisor.ts +63 -6
  222. package/src/tools/browser.ts +8 -3
  223. package/src/tools/index.ts +81 -6
  224. package/src/tools/sqlite-reader.ts +8 -4
  225. package/src/tui/output-block.ts +18 -6
  226. package/src/utils/external-editor.ts +6 -1
@@ -21,6 +21,7 @@ import {
21
21
  getRenderCacheRetainedBytes,
22
22
  Loader,
23
23
  Markdown,
24
+ onImageProtocolChanged,
24
25
  ProcessTerminal,
25
26
  Spacer,
26
27
  Text,
@@ -81,9 +82,11 @@ import type { BashExecutionComponent } from "./components/bash-execution";
81
82
  import { CustomEditor } from "./components/custom-editor";
82
83
  import { DynamicBorder } from "./components/dynamic-border";
83
84
  import type { EvalExecutionComponent } from "./components/eval-execution";
85
+ import { GajaePetWidget, type PetMode } from "./components/gajae-pet-widget";
84
86
  import type { HookEditorComponent } from "./components/hook-editor";
85
87
  import type { HookInputComponent } from "./components/hook-input";
86
88
  import type { HookSelectorComponent } from "./components/hook-selector";
89
+ import { IrcSplitViewComponent } from "./components/irc-sidebar";
87
90
  import { StatusLineComponent } from "./components/status-line";
88
91
  import type { ToolExecutionHandle } from "./components/tool-execution";
89
92
  import {
@@ -99,6 +102,7 @@ import { InputController } from "./controllers/input-controller";
99
102
  import { SelectorController } from "./controllers/selector-controller";
100
103
  import { SSHCommandController } from "./controllers/ssh-command-controller";
101
104
  import { TodoCommandController } from "./controllers/todo-command-controller";
105
+ import { IrcObservationLedger } from "./irc-observation-ledger";
102
106
  import { JobsObserver } from "./jobs-observer";
103
107
  import { OAuthManualInputManager } from "./oauth-manual-input";
104
108
  import { SessionObserverRegistry } from "./session-observer-registry";
@@ -114,13 +118,42 @@ import {
114
118
  onThemeChange,
115
119
  theme,
116
120
  } from "./theme/theme";
117
- import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInput, TodoItem, TodoPhase } from "./types";
118
- import { addChatChild, UiHelpers } from "./utils/ui-helpers";
121
+ import type {
122
+ CompactionQueuedMessage,
123
+ InteractiveModeContext,
124
+ IrcArrivalSnapshot,
125
+ SubmittedUserInput,
126
+ TodoItem,
127
+ TodoPhase,
128
+ TranscriptRebuildPolicy,
129
+ } from "./types";
130
+ import type { ParsedIrcMessage } from "./utils/irc-message";
131
+ import { addChatChild, prepareTranscriptRebuild, UiHelpers } from "./utils/ui-helpers";
119
132
 
120
133
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
121
134
  const COMPOSER_NEWLINE_HINT = process.platform === "win32" ? "Alt+Enter/Ctrl+J" : "Shift+Enter/Ctrl+J";
122
135
  export const DEFAULT_COMPOSER_PLACEHOLDER = `Type your message... ${COMPOSER_NEWLINE_HINT}: New line · Ctrl+C: Clear · Ctrl+R: Search history · Shift+Tab: Reasoning`;
123
136
  const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
137
+
138
+ const IRC_SIDEBAR_TOGGLE_SHADOWING_ACTIONS: readonly AppKeybinding[] = [
139
+ "app.plan.toggle",
140
+ "app.session.new",
141
+ "app.session.tree",
142
+ "app.session.fork",
143
+ "app.session.resume",
144
+ "app.message.followUp",
145
+ "app.stt.toggle",
146
+ "app.clipboard.copyLine",
147
+ "app.session.observe",
148
+ "app.jobs.open",
149
+ "app.tool.backgroundFold",
150
+ ];
151
+
152
+ export function getWelcomeTranscriptReservedRows(chatContainer: Container, width: number): number {
153
+ return chatContainer.children.length === 0 || chatContainer.children.length > WELCOME_RESERVED_CONTAINER_CHILD_LIMIT
154
+ ? 0
155
+ : chatContainer.render(width).length;
156
+ }
124
157
  const FRIENDLY_KEY_PARTS: Record<string, string> = {
125
158
  alt: "Alt",
126
159
  cmd: "Cmd",
@@ -280,6 +313,7 @@ export class InteractiveMode implements InteractiveModeContext {
280
313
  keybindings: KeybindingsManager;
281
314
  agent: Agent;
282
315
  historyStorage?: HistoryStorage;
316
+ readonly ircLedger = new IrcObservationLedger();
283
317
 
284
318
  ui: TUI;
285
319
  chatContainer: Container;
@@ -291,6 +325,8 @@ export class InteractiveMode implements InteractiveModeContext {
291
325
  editorContainer: Container;
292
326
  hookWidgetContainerAbove: Container;
293
327
  hookWidgetContainerBelow: Container;
328
+ petFloorContainer: Container = new Container();
329
+ petWidget: GajaePetWidget | undefined;
294
330
  statusLine: StatusLineComponent;
295
331
 
296
332
  isInitialized = false;
@@ -352,6 +388,7 @@ export class InteractiveMode implements InteractiveModeContext {
352
388
  #baseReservedSlashCommandNames: Set<string> = new Set();
353
389
  #cleanupUnsubscribe?: () => void;
354
390
  #subprocessTeardownUnsubscribe?: () => void;
391
+ #petProtocolUnsubscribe?: () => void;
355
392
  readonly #version: string;
356
393
  readonly #changelogMarkdown: string | undefined;
357
394
  #planModePreviousTools: string[] | undefined;
@@ -387,6 +424,9 @@ export class InteractiveMode implements InteractiveModeContext {
387
424
  #eventBus?: EventBus;
388
425
  #eventBusUnsubscribers: Array<() => void> = [];
389
426
  #welcomeComponent?: WelcomeComponent;
427
+ #ircSplitView: IrcSplitViewComponent;
428
+ #ircSidebarAvailable = false;
429
+ #ircSidebarRequestedVisible = false;
390
430
 
391
431
  constructor(
392
432
  session: AgentSession,
@@ -423,6 +463,7 @@ export class InteractiveMode implements InteractiveModeContext {
423
463
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
424
464
  this.ui.setClearOnShrink(settings.get("clearOnShrink"));
425
465
  this.chatContainer = new Container();
466
+ this.#ircSplitView = new IrcSplitViewComponent(this.chatContainer, this.ircLedger, () => theme);
426
467
  this.pendingMessagesContainer = new Container();
427
468
  this.statusContainer = new Container();
428
469
  this.todoContainer = new Container();
@@ -442,6 +483,7 @@ export class InteractiveMode implements InteractiveModeContext {
442
483
  this.#syncEditorMaxHeight();
443
484
  this.updateEditorChrome();
444
485
  this.editor.invalidate();
486
+ this.#invalidateIrcSidebarRender();
445
487
  this.ui.requestResizeRender();
446
488
  };
447
489
  process.stdout.on("resize", this.#resizeHandler);
@@ -561,7 +603,9 @@ export class InteractiveMode implements InteractiveModeContext {
561
603
  this.#welcomeComponent.playIntro(() => this.ui.requestRender());
562
604
  }
563
605
 
564
- this.ui.addChild(this.chatContainer);
606
+ this.ui.addChild(this.#ircSplitView);
607
+ this.ui.setViewportAnchorComponent(this.#ircSplitView);
608
+
565
609
  this.ui.addChild(this.pendingMessagesContainer);
566
610
  this.ui.addChild(this.statusContainer);
567
611
  this.ui.addChild(this.todoContainer);
@@ -569,9 +613,24 @@ export class InteractiveMode implements InteractiveModeContext {
569
613
  this.ui.addChild(this.statusLine); // Main status rail + hook statuses; composer chrome is rendered by the editor.
570
614
  this.ui.addChild(this.hookWidgetContainerAbove);
571
615
  this.ui.addChild(this.editorContainer);
616
+ this.ui.addChild(this.petFloorContainer);
572
617
  this.ui.addChild(this.hookWidgetContainerBelow);
573
618
  this.ui.setBottomPinnedComponent(this.statusLine);
574
619
  this.ui.setFocus(this.editor);
620
+ this.petWidget = this.#createPetWidget(this.editor);
621
+ this.petWidget.setMode(settings.get("pet.mode"));
622
+ // The async sixel capability probe can enable graphics after the saved
623
+ // pet mode was applied and dropped (no protocol yet at startup).
624
+ // Re-apply the configured mode when capability arrives so the pet
625
+ // appears without the user re-running /pet.
626
+ this.#petProtocolUnsubscribe?.();
627
+ this.#petProtocolUnsubscribe = onImageProtocolChanged(protocol => {
628
+ if (!protocol) return;
629
+ const saved = settings.get("pet.mode");
630
+ if (saved !== "off" && this.petWidget && this.petWidget.mode === "off") {
631
+ this.petWidget.setMode(saved);
632
+ }
633
+ });
575
634
 
576
635
  this.#inputController.setupKeyHandlers();
577
636
  this.#inputController.setupEditorSubmitHandler();
@@ -609,6 +668,7 @@ export class InteractiveMode implements InteractiveModeContext {
609
668
  this.updateEditorChrome();
610
669
  this.#syncEditorMaxHeight();
611
670
  this.isInitialized = true;
671
+ this.#syncIrcSidebarAvailabilityFromSettings();
612
672
  this.ui.requestRender(true);
613
673
 
614
674
  // GitHub star reminder (interactive-only). Register the decline-driven
@@ -874,7 +934,7 @@ export class InteractiveMode implements InteractiveModeContext {
874
934
  }
875
935
  if (!submission.customType) {
876
936
  this.pendingImages = submission.images ? [...submission.images] : [];
877
- this.rebuildChatFromMessages();
937
+ this.rebuildChatFromMessages("reconcile-same-transcript");
878
938
  this.editor.setText(submission.text);
879
939
  }
880
940
  this.updateEditorChrome();
@@ -950,8 +1010,9 @@ export class InteractiveMode implements InteractiveModeContext {
950
1010
  }
951
1011
 
952
1012
  #getWelcomeReservedRows(width: number): number {
1013
+ const transcriptRows = getWelcomeTranscriptReservedRows(this.chatContainer, width);
1014
+
953
1015
  const transientRows = [
954
- this.chatContainer,
955
1016
  this.pendingMessagesContainer,
956
1017
  this.statusContainer,
957
1018
  this.todoContainer,
@@ -965,7 +1026,7 @@ export class InteractiveMode implements InteractiveModeContext {
965
1026
  this.hookWidgetContainerBelow,
966
1027
  ].reduce((rows, component) => rows + component.render(width).length, 0);
967
1028
 
968
- return transientRows + pinnedRows;
1029
+ return transcriptRows + transientRows + pinnedRows;
969
1030
  }
970
1031
 
971
1032
  #renderShortContainerRowsForWelcomeReservation(width: number, container: Container): number {
@@ -1017,7 +1078,42 @@ export class InteractiveMode implements InteractiveModeContext {
1017
1078
  this.editor.setTopBorder(undefined);
1018
1079
  }
1019
1080
 
1020
- rebuildChatFromMessages(): void {
1081
+ setPetMode(mode: PetMode): void {
1082
+ this.petWidget?.setMode(mode);
1083
+ settings.set("pet.mode", mode);
1084
+ this.ui.requestRender();
1085
+ }
1086
+
1087
+ previewPetMode(mode: PetMode): void {
1088
+ this.petWidget?.previewMode(mode);
1089
+ this.ui.requestRender();
1090
+ }
1091
+
1092
+ restoreComposer(): void {
1093
+ if (this.petWidget) {
1094
+ this.petWidget.remountComposer();
1095
+ } else {
1096
+ this.editorContainer.clear();
1097
+ this.editorContainer.addChild(this.editor);
1098
+ }
1099
+ this.ui.setFocus(this.editor);
1100
+ }
1101
+
1102
+ #createPetWidget(editor: CustomEditor): GajaePetWidget {
1103
+ return new GajaePetWidget({
1104
+ ui: this.ui,
1105
+ editor,
1106
+ editorContainer: this.editorContainer,
1107
+ floorContainer: this.petFloorContainer,
1108
+ isWorking: () => this.loadingAnimation !== undefined,
1109
+ getComposerBottomOffset: () =>
1110
+ this.petFloorContainer.render(this.ui.terminal.columns).length +
1111
+ this.hookWidgetContainerBelow.render(this.ui.terminal.columns).length,
1112
+ });
1113
+ }
1114
+
1115
+ rebuildChatFromMessages(policy: TranscriptRebuildPolicy): void {
1116
+ prepareTranscriptRebuild(this.ui, policy);
1021
1117
  this.chatContainer.clear();
1022
1118
  const context = this.session.buildDisplaySessionContext();
1023
1119
  this.renderSessionContext(context);
@@ -1992,6 +2088,10 @@ export class InteractiveMode implements InteractiveModeContext {
1992
2088
  }
1993
2089
 
1994
2090
  stop(): void {
2091
+ this.#petProtocolUnsubscribe?.();
2092
+ this.#petProtocolUnsubscribe = undefined;
2093
+ this.petWidget?.dispose();
2094
+ this.petWidget = undefined;
1995
2095
  if (this.loadingAnimation) {
1996
2096
  this.loadingAnimation.stop();
1997
2097
  this.loadingAnimation = undefined;
@@ -2123,11 +2223,17 @@ export class InteractiveMode implements InteractiveModeContext {
2123
2223
  nextEditor.setText(previousText);
2124
2224
  previousEditor.dispose();
2125
2225
 
2226
+ const petMode = this.petWidget?.mode ?? settings.get("pet.mode");
2227
+ this.petWidget?.dispose();
2228
+
2126
2229
  this.editorContainer.clear();
2127
2230
  this.editor = nextEditor;
2128
2231
  this.editorContainer.addChild(nextEditor);
2129
2232
  this.ui.setFocus(nextEditor);
2130
2233
 
2234
+ this.petWidget = this.#createPetWidget(nextEditor);
2235
+ this.petWidget.setMode(petMode);
2236
+
2131
2237
  this.#inputController.setupKeyHandlers();
2132
2238
  this.#inputController.setupEditorSubmitHandler();
2133
2239
 
@@ -2284,13 +2390,32 @@ export class InteractiveMode implements InteractiveModeContext {
2284
2390
  return this.#uiHelpers.addMessageToChat(message, options);
2285
2391
  }
2286
2392
 
2393
+ addLiveIrcObservationToChat(message: ParsedIrcMessage, arrival: IrcArrivalSnapshot): Component[] {
2394
+ return this.#uiHelpers.addLiveIrcObservationToChat(message, arrival);
2395
+ }
2396
+ removeRenderedIrcInlineComponents(observationId: string): readonly Component[] | undefined {
2397
+ return this.#uiHelpers.removeRenderedIrcInlineComponents(observationId);
2398
+ }
2399
+ resetRenderedIrcInlineComponents(): readonly (readonly Component[])[] {
2400
+ return this.#uiHelpers.resetRenderedIrcInlineComponents();
2401
+ }
2402
+
2287
2403
  renderSessionContext(
2288
2404
  sessionContext: SessionContext,
2289
2405
  options?: { updateFooter?: boolean; populateHistory?: boolean },
2290
2406
  ): void {
2291
2407
  this.#uiHelpers.renderSessionContext(sessionContext, options);
2408
+ this.#eventController.reconcileIrcExpiryTimers(this.#uiHelpers.getRenderedIrcInlineComponents());
2292
2409
  }
2293
2410
 
2411
+ rebuildInitialMessages(
2412
+ policy: TranscriptRebuildPolicy,
2413
+ prebuiltContext?: SessionContext,
2414
+ options?: { preserveExistingChat?: boolean },
2415
+ ): void {
2416
+ prepareTranscriptRebuild(this.ui, policy);
2417
+ this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
2418
+ }
2294
2419
  renderInitialMessages(prebuiltContext?: SessionContext, options?: { preserveExistingChat?: boolean }): void {
2295
2420
  this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
2296
2421
  }
@@ -2303,6 +2428,10 @@ export class InteractiveMode implements InteractiveModeContext {
2303
2428
  return this.#uiHelpers.findLastAssistantMessage();
2304
2429
  }
2305
2430
 
2431
+ getAssistantViewportAnchorId(message: AssistantMessage): string {
2432
+ return this.#uiHelpers.assistantViewportAnchorId(message);
2433
+ }
2434
+
2306
2435
  extractAssistantText(message: AssistantMessage): string {
2307
2436
  return this.#uiHelpers.extractAssistantText(message);
2308
2437
  }
@@ -2553,6 +2682,10 @@ export class InteractiveMode implements InteractiveModeContext {
2553
2682
  this.#selectorController.showThemeSelector();
2554
2683
  }
2555
2684
 
2685
+ showPetSelector(): void {
2686
+ this.#selectorController.showPetSelector();
2687
+ }
2688
+
2556
2689
  showHistorySearch(): void {
2557
2690
  this.#selectorController.showHistorySearch();
2558
2691
  }
@@ -2668,6 +2801,65 @@ export class InteractiveMode implements InteractiveModeContext {
2668
2801
  this.#inputController.setToolsExpanded(expanded);
2669
2802
  }
2670
2803
 
2804
+ #resolveEffectiveIrcSidebarToggleKey(): string | null {
2805
+ for (const key of this.keybindings.getKeys("app.irc.sidebar.toggle")) {
2806
+ if (this.editor.hasActionKey(key)) continue;
2807
+ const shadowed = IRC_SIDEBAR_TOGGLE_SHADOWING_ACTIONS.some(action =>
2808
+ this.keybindings.getKeys(action).includes(key),
2809
+ );
2810
+ if (!shadowed) return key;
2811
+ }
2812
+ return null;
2813
+ }
2814
+
2815
+ captureIrcArrivalSnapshot(): IrcArrivalSnapshot {
2816
+ return {
2817
+ panelVisible: this.#ircSplitView.effectiveSidebarVisible(this.ui.terminal.columns),
2818
+ panelRequestedVisible: this.#ircSidebarRequestedVisible,
2819
+ sidebarAvailable: this.#ircSidebarAvailable,
2820
+ resolvedToggleKey: this.#resolveEffectiveIrcSidebarToggleKey(),
2821
+ };
2822
+ }
2823
+ toggleIrcSidebar(): void {
2824
+ if (
2825
+ !this.#ircSidebarAvailable ||
2826
+ this.settings.get("irc.enabled") !== true ||
2827
+ this.settings.get("irc.sidebar.enabled") !== true
2828
+ )
2829
+ return;
2830
+ this.#ircSidebarRequestedVisible = !this.#ircSidebarRequestedVisible;
2831
+ this.#ircSplitView.setVisible(this.#ircSidebarRequestedVisible);
2832
+ this.#invalidateIrcSidebarRender();
2833
+ this.ui.requestRender();
2834
+ }
2835
+
2836
+ applyIrcSidebarAvailability(enabled: boolean): void {
2837
+ this.#ircSidebarAvailable = enabled;
2838
+ this.#ircSplitView.setVisible(enabled && this.#ircSidebarRequestedVisible);
2839
+ this.#invalidateIrcSidebarRender();
2840
+ this.ui.requestRender();
2841
+ }
2842
+
2843
+ #syncIrcSidebarAvailabilityFromSettings(): void {
2844
+ this.applyIrcSidebarAvailability(
2845
+ this.settings.get("irc.enabled") === true && this.settings.get("irc.sidebar.enabled") === true,
2846
+ );
2847
+ }
2848
+
2849
+ resetIrcSidebarSession(): void {
2850
+ this.ircLedger.reset();
2851
+ this.#eventController.resetIrcObservations();
2852
+ this.#ircSidebarRequestedVisible = false;
2853
+ this.#ircSplitView.setVisible(false);
2854
+ this.#uiHelpers.resetIrcSidebarHint();
2855
+ this.#syncIrcSidebarAvailabilityFromSettings();
2856
+ }
2857
+
2858
+ #invalidateIrcSidebarRender(): void {
2859
+ clearRenderCache();
2860
+ this.#ircSplitView.invalidate();
2861
+ }
2862
+
2671
2863
  toggleThinkingBlockVisibility(): void {
2672
2864
  this.#inputController.toggleThinkingBlockVisibility();
2673
2865
  }
@@ -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
+ }