@gajae-code/coding-agent 0.9.5 → 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 (194) hide show
  1. package/CHANGELOG.md +54 -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/compile-args.ts +8 -2
  95. package/scripts/g004-tmux-smoke.ts +196 -36
  96. package/src/cli/daemon-cli.ts +19 -29
  97. package/src/cli/malloc-env-guard.test.ts +104 -0
  98. package/src/cli/malloc-env-guard.ts +149 -0
  99. package/src/cli/notify-cli.ts +78 -14
  100. package/src/cli/session-picker.ts +23 -40
  101. package/src/cli/update-cli.ts +56 -37
  102. package/src/cli.ts +50 -10
  103. package/src/commands/daemon.ts +19 -5
  104. package/src/commands/harness.ts +515 -47
  105. package/src/commands/launch.ts +11 -3
  106. package/src/commands/notify.ts +5 -1
  107. package/src/commands/session.ts +6 -1
  108. package/src/config/keybindings.ts +5 -0
  109. package/src/config/model-profile-activation.ts +1 -1
  110. package/src/config/model-profiles.ts +23 -23
  111. package/src/config/settings-schema.ts +41 -1
  112. package/src/coordinator-mcp/model-preset.ts +100 -0
  113. package/src/coordinator-mcp/server.ts +2653 -425
  114. package/src/daemon/control-types.ts +11 -0
  115. package/src/daemon/operator-contract.ts +121 -0
  116. package/src/deep-interview/render-middleware.ts +67 -2
  117. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  118. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  119. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  120. package/src/gjc-runtime/launch-tmux.ts +702 -135
  121. package/src/gjc-runtime/session-state-sidecar.ts +756 -135
  122. package/src/gjc-runtime/team-runtime.ts +6 -0
  123. package/src/gjc-runtime/tmux-common.ts +26 -7
  124. package/src/gjc-runtime/tmux-gc.ts +52 -9
  125. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +185 -0
  126. package/src/gjc-runtime/tmux-owner-isolation.ts +2060 -0
  127. package/src/gjc-runtime/tmux-sessions.ts +871 -68
  128. package/src/internal-urls/docs-index.generated.ts +9 -8
  129. package/src/internal-urls/local-protocol.ts +29 -10
  130. package/src/main.ts +298 -75
  131. package/src/modes/acp/acp-agent.ts +728 -98
  132. package/src/modes/acp/acp-mode.ts +29 -2
  133. package/src/modes/components/assistant-message.ts +32 -3
  134. package/src/modes/components/bash-execution.ts +43 -13
  135. package/src/modes/components/custom-editor.ts +5 -0
  136. package/src/modes/components/gajae-pet-widget.ts +360 -0
  137. package/src/modes/components/hook-selector.ts +12 -0
  138. package/src/modes/components/irc-sidebar.ts +206 -0
  139. package/src/modes/components/pet-selector.ts +55 -0
  140. package/src/modes/components/session-selector.ts +222 -221
  141. package/src/modes/components/settings-selector.ts +10 -0
  142. package/src/modes/components/tool-execution.ts +47 -6
  143. package/src/modes/components/user-message.ts +26 -9
  144. package/src/modes/controllers/command-controller.ts +28 -2
  145. package/src/modes/controllers/event-controller.ts +159 -36
  146. package/src/modes/controllers/extension-ui-controller.ts +36 -17
  147. package/src/modes/controllers/input-controller.ts +8 -2
  148. package/src/modes/controllers/selector-controller.ts +62 -15
  149. package/src/modes/interactive-mode.ts +183 -7
  150. package/src/modes/irc-observation-ledger.ts +127 -0
  151. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  152. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  153. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  154. package/src/modes/types.ts +34 -2
  155. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  156. package/src/modes/utils/irc-message.ts +138 -0
  157. package/src/modes/utils/ui-helpers.ts +216 -50
  158. package/src/notifications/config.ts +44 -0
  159. package/src/notifications/daemon-paths.ts +8 -0
  160. package/src/notifications/index.ts +535 -75
  161. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  162. package/src/notifications/notification-service.ts +710 -0
  163. package/src/notifications/rate-limit-pool.ts +46 -12
  164. package/src/notifications/telegram-daemon-cli.ts +2 -0
  165. package/src/notifications/telegram-daemon-control.ts +27 -2
  166. package/src/notifications/telegram-daemon.ts +635 -94
  167. package/src/notifications/telegram-reference.ts +73 -13
  168. package/src/notifications/topic-registry.ts +86 -9
  169. package/src/prompts/system/irc-peer-roster.md +1 -0
  170. package/src/prompts/system/subagent-system-prompt.md +1 -0
  171. package/src/prompts/system/system-prompt.md +1 -0
  172. package/src/prompts/tools/irc.md +1 -1
  173. package/src/registry/agent-registry.ts +3 -0
  174. package/src/sdk.ts +52 -1
  175. package/src/secrets/obfuscator.ts +4 -2
  176. package/src/session/agent-session.ts +396 -145
  177. package/src/session/cache-economics.ts +275 -0
  178. package/src/session/provider-safety-stop.ts +9 -0
  179. package/src/session/session-dump-format.ts +6 -0
  180. package/src/session/session-manager.ts +809 -39
  181. package/src/session/session-storage.ts +582 -38
  182. package/src/slash-commands/builtin-registry.ts +143 -9
  183. package/src/task/executor.ts +12 -3
  184. package/src/task/index.ts +8 -0
  185. package/src/task/receipt.ts +7 -2
  186. package/src/task/types.ts +26 -0
  187. package/src/tools/ask-answer-registry.ts +25 -0
  188. package/src/tools/ask.ts +355 -33
  189. package/src/tools/browser/tab-supervisor.ts +63 -6
  190. package/src/tools/browser.ts +8 -3
  191. package/src/tools/index.ts +81 -6
  192. package/src/tools/sqlite-reader.ts +8 -4
  193. package/src/tui/output-block.ts +18 -6
  194. package/src/utils/external-editor.ts +6 -1
@@ -8,6 +8,7 @@ import {
8
8
  Image,
9
9
  ImageProtocol,
10
10
  imageFallback,
11
+ isTerminalGraphicsFallbackActive,
11
12
  registerAnimationCallback,
12
13
  Spacer,
13
14
  TERMINAL,
@@ -33,7 +34,7 @@ import {
33
34
  import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
34
35
  import { toolRenderers } from "../../tools/renderers";
35
36
  import { renderStatusLine } from "../../tui";
36
- import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
37
+ import { containsSixelSequence, getSixelLineMask, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
37
38
  import { renderDiff } from "./diff";
38
39
 
39
40
  function ensureInvalidate(component: unknown): Component {
@@ -44,6 +45,23 @@ function ensureInvalidate(component: unknown): Component {
44
45
  return c as Component;
45
46
  }
46
47
 
48
+ const SIXEL_FALLBACK_PLACEHOLDER = "[SIXEL image hidden while IRC sidebar is visible]";
49
+
50
+ function replaceSixelOutputForGraphicsFallback(text: string): string {
51
+ const lines = text.split("\n");
52
+ const sixelLineMask = getSixelLineMask(lines);
53
+ if (!sixelLineMask.some(Boolean)) return text;
54
+
55
+ return lines
56
+ .flatMap((line, index) => {
57
+ if (!sixelLineMask[index]) return [line];
58
+ return index === 0 || !sixelLineMask[index - 1] || containsSixelSequence(line)
59
+ ? [SIXEL_FALLBACK_PLACEHOLDER]
60
+ : [];
61
+ })
62
+ .join("\n");
63
+ }
64
+
47
65
  function cloneToolArgs<T>(args: T): T {
48
66
  if (args === null || args === undefined) return args;
49
67
  try {
@@ -169,7 +187,14 @@ export class ToolExecutionComponent extends Container {
169
187
  isError?: boolean;
170
188
  details?: any;
171
189
  };
172
- #textOutputCache?: { content: unknown; showImages: boolean; terminalImageProtocol: unknown; output: string };
190
+ #textOutputCache?: {
191
+ content: unknown;
192
+ showImages: boolean;
193
+ terminalImageProtocol: unknown;
194
+ graphicsFallbackActive: boolean;
195
+ output: string;
196
+ };
197
+ #displayBuiltWithGraphicsFallback: boolean | undefined;
173
198
  // Edit preview state
174
199
  #editMode?: EditMode;
175
200
  #editDiffPreview?: PerFileDiffPreview[];
@@ -441,6 +466,13 @@ export class ToolExecutionComponent extends Container {
441
466
  this.#updateDisplay();
442
467
  }
443
468
 
469
+ override render(width: number): string[] {
470
+ if (this.#displayBuiltWithGraphicsFallback !== isTerminalGraphicsFallbackActive()) {
471
+ this.#updateDisplay();
472
+ }
473
+ return super.render(width);
474
+ }
475
+
444
476
  #updateDisplay(): void {
445
477
  // Set background based on state
446
478
  const bgFn = this.#isPartial
@@ -685,6 +717,7 @@ export class ToolExecutionComponent extends Container {
685
717
  }
686
718
  }
687
719
  }
720
+ this.#displayBuiltWithGraphicsFallback = isTerminalGraphicsFallbackActive();
688
721
  }
689
722
 
690
723
  #getCallArgsForRender(): any {
@@ -761,11 +794,13 @@ export class ToolExecutionComponent extends Container {
761
794
 
762
795
  const content = this.#result.content;
763
796
  const terminalImageProtocol = TERMINAL.imageProtocol;
797
+ const graphicsFallbackActive = isTerminalGraphicsFallbackActive();
764
798
  const cached = this.#textOutputCache;
765
799
  if (
766
800
  cached?.content === content &&
767
801
  cached.showImages === this.#showImages &&
768
- cached.terminalImageProtocol === terminalImageProtocol
802
+ cached.terminalImageProtocol === terminalImageProtocol &&
803
+ cached.graphicsFallbackActive === graphicsFallbackActive
769
804
  ) {
770
805
  return cached.output;
771
806
  }
@@ -773,8 +808,8 @@ export class ToolExecutionComponent extends Container {
773
808
  const textParts: string[] = [];
774
809
  for (const block of content ?? []) {
775
810
  if (block.type !== "text") continue;
776
- const text = block.text || "";
777
- textParts.push(sanitizeWithOptionalSixelPassthrough(text, sanitizeText));
811
+ const text = sanitizeWithOptionalSixelPassthrough(block.text || "", sanitizeText);
812
+ textParts.push(graphicsFallbackActive ? replaceSixelOutputForGraphicsFallback(text) : text);
778
813
  }
779
814
  let output = textParts.join("\n");
780
815
 
@@ -789,7 +824,13 @@ export class ToolExecutionComponent extends Container {
789
824
  output = output ? `${output}\n${imageIndicators}` : imageIndicators;
790
825
  }
791
826
 
792
- this.#textOutputCache = { content, showImages: this.#showImages, terminalImageProtocol, output };
827
+ this.#textOutputCache = {
828
+ content,
829
+ showImages: this.#showImages,
830
+ terminalImageProtocol,
831
+ graphicsFallbackActive,
832
+ output,
833
+ };
793
834
  return output;
794
835
  }
795
836
 
@@ -1,4 +1,12 @@
1
- import { type Component, Container, Markdown, Spacer, Text } from "@gajae-code/tui";
1
+ import {
2
+ type Component,
3
+ Container,
4
+ Markdown,
5
+ Spacer,
6
+ Text,
7
+ type ViewportAnchorRender,
8
+ type ViewportAnchorSource,
9
+ } from "@gajae-code/tui";
2
10
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
3
11
 
4
12
  // OSC 133 shell integration: marks prompt zones for terminal multiplexers
@@ -10,8 +18,11 @@ const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
10
18
  * Component that renders a user message
11
19
  */
12
20
  export class UserMessageComponent extends Container {
13
- constructor(text: string, synthetic = false) {
21
+ #viewportAnchorSource?: ViewportAnchorSource;
22
+
23
+ constructor(text: string, synthetic = false, viewportAnchorId?: string) {
14
24
  super();
25
+ if (viewportAnchorId) this.#viewportAnchorSource = { id: viewportAnchorId };
15
26
  const bgColor = (value: string) => theme.bg("userMessageBg", value);
16
27
  const color = synthetic
17
28
  ? (value: string) => theme.fg("dim", value)
@@ -19,7 +30,9 @@ export class UserMessageComponent extends Container {
19
30
  this.addChild(new Spacer(1));
20
31
  const label = synthetic ? "replay" : "user";
21
32
  this.addChild(new Text(theme.bold(theme.fg("accent", label)), 1, 0));
22
- this.addChild(new PromptZoneMarkdown(text, bgColor, color));
33
+ const prompt = new PromptZoneMarkdown(text, bgColor, color);
34
+ this.addChild(prompt);
35
+ if (this.#viewportAnchorSource) this.setViewportAnchorSource(prompt, this.#viewportAnchorSource);
23
36
  }
24
37
  }
25
38
 
@@ -37,15 +50,19 @@ class PromptZoneMarkdown implements Component {
37
50
  this.#markdown.invalidate();
38
51
  }
39
52
 
40
- render(width: number): string[] {
41
- const lines = this.#markdown.render(width);
42
- if (lines.length === 0) {
43
- return lines;
44
- }
45
-
53
+ #withPromptZone(lines: string[]): string[] {
54
+ if (lines.length === 0) return lines;
46
55
  const zoned = [...lines];
47
56
  zoned[0] = OSC133_ZONE_START + zoned[0];
48
57
  zoned[zoned.length - 1] = `${zoned[zoned.length - 1]}${OSC133_ZONE_END}${OSC133_ZONE_FINAL}`;
49
58
  return zoned;
50
59
  }
60
+
61
+ renderWithViewportAnchorSource(width: number, source: ViewportAnchorSource): ViewportAnchorRender {
62
+ const rendered = this.#markdown.renderWithViewportAnchorSource(width, source);
63
+ return { lines: this.#withPromptZone(rendered.lines), anchors: rendered.anchors };
64
+ }
65
+ render(width: number): string[] {
66
+ return this.#withPromptZone(this.#markdown.render(width));
67
+ }
51
68
  }
@@ -40,6 +40,7 @@ import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
40
40
  import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
41
41
  import type { AsyncJobSnapshotItem } from "../../session/agent-session";
42
42
  import type { AuthStorage } from "../../session/auth-storage";
43
+ import { computeCacheMissCostSummary, formatCacheMissSummaryLines } from "../../session/cache-economics";
43
44
  import type { NewSessionOptions } from "../../session/session-manager";
44
45
  import { outputMeta } from "../../tools/output-meta";
45
46
  import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
@@ -48,6 +49,7 @@ import { getDisplayChangelogEntries } from "../../utils/changelog";
48
49
  import { copyToClipboard } from "../../utils/clipboard";
49
50
  import { openPath } from "../../utils/open";
50
51
  import { setSessionTerminalTitle } from "../../utils/title-generator";
52
+ import { prepareTranscriptRebuild } from "../utils/ui-helpers";
51
53
 
52
54
  function showMarkdownPanel(ctx: InteractiveModeContext, title: string, markdown: string): void {
53
55
  ctx.chatContainer.addChild(new Spacer(1));
@@ -426,6 +428,24 @@ export class CommandController {
426
428
  info += `${theme.fg("dim", "Premium Requests:")} ${normalizedPremiumRequests.toLocaleString()}\n`;
427
429
  }
428
430
  }
431
+ const cacheMissSummary = stats.costBreakdown
432
+ ? computeCacheMissCostSummary(stats.tokens, {
433
+ kind: "persisted-aggregate",
434
+ costBreakdown: stats.costBreakdown,
435
+ })
436
+ : undefined;
437
+ if (cacheMissSummary) {
438
+ info += `\n${theme.bold("Cache Miss Cost")}`;
439
+ for (const line of formatCacheMissSummaryLines(cacheMissSummary)) {
440
+ const separator = line.indexOf(":");
441
+ if (separator === -1) {
442
+ info += `\n${line}`;
443
+ } else {
444
+ info += `\n${theme.fg("dim", `${line.slice(0, separator)}:`)}${line.slice(separator + 1)}`;
445
+ }
446
+ }
447
+ info += `\n`;
448
+ }
429
449
 
430
450
  if (this.ctx.lspServers && this.ctx.lspServers.length > 0) {
431
451
  info += `\n${theme.bold("LSP Servers")}\n`;
@@ -911,6 +931,8 @@ export class CommandController {
911
931
  }
912
932
  }
913
933
  if (!(await this.ctx.session.newSession(options))) return;
934
+ this.ctx.resetIrcSidebarSession();
935
+
914
936
  this.ctx.resetObserverRegistry();
915
937
  setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
916
938
 
@@ -920,6 +942,7 @@ export class CommandController {
920
942
  this.ctx.updateEditorBorderColor();
921
943
  this.ctx.ui.requestRender();
922
944
 
945
+ prepareTranscriptRebuild(this.ctx.ui, "replace-identity");
923
946
  this.ctx.chatContainer.clear();
924
947
  this.ctx.pendingMessagesContainer.clear();
925
948
  this.ctx.compactionQueuedMessages = [];
@@ -958,6 +981,7 @@ export class CommandController {
958
981
  this.ctx.updateEditorBorderColor();
959
982
  this.ctx.ui.requestRender();
960
983
 
984
+ prepareTranscriptRebuild(this.ctx.ui, "replace-identity");
961
985
  this.ctx.chatContainer.clear();
962
986
  this.ctx.pendingMessagesContainer.clear();
963
987
  this.ctx.compactionQueuedMessages = [];
@@ -996,6 +1020,7 @@ export class CommandController {
996
1020
  this.ctx.showError("Fork failed (session not persisted or cancelled)");
997
1021
  return;
998
1022
  }
1023
+ this.ctx.resetIrcSidebarSession();
999
1024
 
1000
1025
  this.ctx.statusLine.invalidate();
1001
1026
  this.ctx.updateEditorTopBorder();
@@ -1219,7 +1244,7 @@ export class CommandController {
1219
1244
  : undefined;
1220
1245
  await this.ctx.session.compact(instructions, options);
1221
1246
 
1222
- this.ctx.rebuildChatFromMessages();
1247
+ this.ctx.rebuildChatFromMessages("reconcile-same-transcript");
1223
1248
 
1224
1249
  this.ctx.statusLine.invalidate();
1225
1250
  this.ctx.updateEditorTopBorder();
@@ -1279,9 +1304,10 @@ export class CommandController {
1279
1304
  this.ctx.showError("Handoff cancelled");
1280
1305
  return;
1281
1306
  }
1307
+ this.ctx.resetIrcSidebarSession();
1282
1308
 
1283
1309
  // Rebuild chat from the new session (which now contains the handoff document)
1284
- this.ctx.rebuildChatFromMessages();
1310
+ this.ctx.rebuildChatFromMessages("replace-identity");
1285
1311
 
1286
1312
  this.ctx.statusLine.invalidate();
1287
1313
  this.ctx.updateEditorTopBorder();
@@ -20,18 +20,20 @@ import { completionNotifyDisabledByEnv } from "../../notifications/config";
20
20
  import { summaryFromMessage } from "../../notifications/helpers";
21
21
  import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
22
22
  import type { AgentSessionEvent } from "../../session/agent-session";
23
- import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
23
+ import { type CustomMessage, isSilentAbort, readPendingDisplayTag } from "../../session/messages";
24
+ import { transferSessionMessageIdentity } from "../../session/session-manager";
24
25
  import type { ResolveToolDetails } from "../../tools/resolve";
26
+ import type { IrcObservationRecord } from "../irc-observation-ledger";
25
27
  import { interruptHint } from "../shared";
26
28
  import { buildAbortDisplayMessage } from "../utils/abort-message";
27
29
  import { consumeInjectedOptimisticSignature } from "../utils/injected-user-submission";
30
+ import { parseIrcMessage } from "../utils/irc-message";
28
31
  import { ringTerminalBell } from "../utils/terminal-bell";
32
+
29
33
  import { addChatChild, argsWithPartialJson } from "../utils/ui-helpers";
30
34
 
31
35
  type AgentSessionEventKind = AgentSessionEvent["type"];
32
36
 
33
- const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
34
-
35
37
  /** Test-only performance counters for advisory baseline tests. */
36
38
  export const __eventControllerPerfCounters = {
37
39
  messageUpdateContentVisits: 0,
@@ -112,6 +114,7 @@ export class EventController {
112
114
  #lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
113
115
  #idleCompactionTimer?: NodeJS.Timeout;
114
116
  #ircExpiryTimers = new Map<string, NodeJS.Timeout>();
117
+ #renderedIrcComponents = new Map<string, readonly Component[]>();
115
118
  #toolIntentCache = new Map<string, { args: unknown; intent: string | undefined }>();
116
119
  #thinkingContentIndices = new Set<number>();
117
120
  #handlers: AgentSessionEventHandlers;
@@ -157,10 +160,7 @@ export class EventController {
157
160
  this.ctx.retryLoader.stop();
158
161
  this.ctx.retryLoader = undefined;
159
162
  }
160
- for (const timer of this.#ircExpiryTimers.values()) {
161
- clearTimeout(timer);
162
- }
163
- this.#ircExpiryTimers.clear();
163
+ this.clearIrcExpiryTimers();
164
164
  }
165
165
 
166
166
  #resetReadGroup(): void {
@@ -263,8 +263,61 @@ export class EventController {
263
263
  this.ctx.ui.requestRender();
264
264
  }
265
265
 
266
+ #observeIrcMessage(message: CustomMessage): void {
267
+ const parsed = parseIrcMessage(message);
268
+ if (!parsed) return;
269
+ const arrival = this.ctx.captureIrcArrivalSnapshot();
270
+ const record = this.ctx.ircLedger.observe(parsed, arrival.panelVisible);
271
+ this.#cleanupEvictedIrcObservations();
272
+ if (!record) return;
273
+ const signature = `irc:${record.observationId}`;
274
+ if (this.#renderedCustomMessages.has(signature)) return;
275
+ this.#renderedCustomMessages.add(signature);
276
+ this.#resetReadGroup();
277
+ const components = this.ctx.addLiveIrcObservationToChat(record, arrival);
278
+ this.#renderedIrcComponents.set(record.observationId, components);
279
+ this.#scheduleIrcExpiry(record, components);
280
+ this.ctx.ui.requestRender();
281
+ }
282
+
283
+ #cleanupIrcObservationIds(observationIds: readonly string[]): void {
284
+ const removedComponents = new Set<Component>();
285
+ for (const observationId of observationIds) {
286
+ const timer = this.#ircExpiryTimers.get(observationId);
287
+ if (timer) clearTimeout(timer);
288
+ this.#ircExpiryTimers.delete(observationId);
289
+ for (const component of this.#renderedIrcComponents.get(observationId) ?? []) {
290
+ removedComponents.add(component);
291
+ }
292
+ this.#renderedIrcComponents.delete(observationId);
293
+ for (const component of this.ctx.removeRenderedIrcInlineComponents(observationId) ?? []) {
294
+ removedComponents.add(component);
295
+ }
296
+
297
+ this.#renderedCustomMessages.delete(`irc:${observationId}`);
298
+ }
299
+ for (const component of removedComponents) this.ctx.chatContainer.removeChild(component);
300
+ }
301
+
302
+ #cleanupEvictedIrcObservations(): void {
303
+ this.#cleanupIrcObservationIds(this.ctx.ircLedger.drainEvictedObservationIds());
304
+ }
305
+
306
+ #cleanupExpiredIrcInlineComponents(observationId: string): void {
307
+ const removedComponents = new Set<Component>(this.#renderedIrcComponents.get(observationId));
308
+ this.#renderedIrcComponents.delete(observationId);
309
+ for (const component of this.ctx.removeRenderedIrcInlineComponents(observationId) ?? []) {
310
+ removedComponents.add(component);
311
+ }
312
+ for (const component of removedComponents) this.ctx.chatContainer.removeChild(component);
313
+ }
314
+
266
315
  async #handleMessageStart(event: Extract<AgentSessionEvent, { type: "message_start" }>): Promise<void> {
267
316
  if (event.message.role === "hookMessage" || event.message.role === "custom") {
317
+ if (event.message.role === "custom" && parseIrcMessage(event.message)) {
318
+ this.#observeIrcMessage(event.message);
319
+ return;
320
+ }
268
321
  const signature = `${event.message.role}:${event.message.customType}:${event.message.timestamp}`;
269
322
  if (this.#renderedCustomMessages.has(signature)) {
270
323
  return;
@@ -328,8 +381,11 @@ export class EventController {
328
381
  this.#resetReadGroup();
329
382
  this.#toolIntentCache.clear();
330
383
  this.#thinkingContentIndices.clear();
331
- this.ctx.streamingComponent = new AssistantMessageComponent(undefined, this.ctx.hideThinkingBlock, () =>
332
- this.ctx.ui.requestRender(),
384
+ this.ctx.streamingComponent = new AssistantMessageComponent(
385
+ undefined,
386
+ this.ctx.hideThinkingBlock,
387
+ () => this.ctx.ui.requestRender(),
388
+ this.ctx.getAssistantViewportAnchorId?.(event.message),
333
389
  );
334
390
  this.ctx.streamingMessage = event.message;
335
391
  addChatChild(this.ctx, this.ctx.streamingComponent);
@@ -339,15 +395,89 @@ export class EventController {
339
395
  }
340
396
 
341
397
  async #handleIrcMessage(event: Extract<AgentSessionEvent, { type: "irc_message" }>): Promise<void> {
342
- const signature = `${event.message.role}:${event.message.customType}:${event.message.timestamp}`;
343
- if (this.#renderedCustomMessages.has(signature)) {
344
- return;
398
+ this.#observeIrcMessage(event.message);
399
+ }
400
+
401
+ clearIrcExpiryTimers(): void {
402
+ for (const timer of this.#ircExpiryTimers.values()) {
403
+ clearTimeout(timer);
404
+ }
405
+ this.#ircExpiryTimers.clear();
406
+ }
407
+
408
+ resetIrcObservations(): void {
409
+ this.clearIrcExpiryTimers();
410
+ const observationIds = new Set<string>(this.#renderedIrcComponents.keys());
411
+ for (const observationId of this.ctx.ircLedger.drainEvictedObservationIds()) observationIds.add(observationId);
412
+ this.#cleanupIrcObservationIds([...observationIds]);
413
+ for (const components of this.ctx.resetRenderedIrcInlineComponents()) {
414
+ for (const component of components) this.ctx.chatContainer.removeChild(component);
415
+ }
416
+ this.#renderedIrcComponents.clear();
417
+ for (const signature of this.#renderedCustomMessages) {
418
+ if (signature.startsWith("irc:")) this.#renderedCustomMessages.delete(signature);
419
+ }
420
+ }
421
+
422
+ reconcileIrcExpiryTimers(componentsByObservationId: Map<string, readonly Component[]>): void {
423
+ this.clearIrcExpiryTimers();
424
+ this.#renderedIrcComponents.clear();
425
+ const now = Date.now();
426
+ const inlineProjection = this.ctx.ircLedger.getInlineProjection(now);
427
+
428
+ this.#cleanupEvictedIrcObservations();
429
+ const projectedObservationIds = new Set(inlineProjection.map(record => record.observationId));
430
+ for (const [observationId, components] of componentsByObservationId) {
431
+ const record = this.ctx.ircLedger.getRecord(observationId);
432
+ if (
433
+ !projectedObservationIds.has(observationId) ||
434
+ (record?.mode === "ephemeral" && record.expiresAt! - now <= 0)
435
+ ) {
436
+ for (const component of components) {
437
+ this.ctx.chatContainer.removeChild(component);
438
+ }
439
+ this.#renderedIrcComponents.delete(observationId);
440
+ componentsByObservationId.delete(observationId);
441
+ }
442
+ }
443
+ for (const record of inlineProjection) {
444
+ const components = componentsByObservationId.get(record.observationId);
445
+ if (!components) continue;
446
+ this.#renderedIrcComponents.set(record.observationId, components);
447
+ // #scheduleIrcExpiry owns the single expiry clock read: a record that
448
+ // crossed its deadline while projection/reconciliation ran above is
449
+ // cleaned up there (old timers were already cleared, so silently
450
+ // skipping the timer would leave the row inline indefinitely).
451
+ if (!this.#scheduleIrcExpiry(record, components)) {
452
+ componentsByObservationId.delete(record.observationId);
453
+ }
345
454
  }
346
- this.#renderedCustomMessages.add(signature);
347
- this.#resetReadGroup();
348
- const components = this.ctx.addMessageToChat(event.message);
349
- this.#scheduleIrcExpiry(signature, components);
350
- this.ctx.ui.requestRender();
455
+ }
456
+
457
+ /**
458
+ * Arms the removal timer for an ephemeral record using a single clock read.
459
+ * Returns false when the record's deadline has already elapsed — in that
460
+ * case the rendered components are removed from the chat container and
461
+ * {@link #renderedIrcComponents} here, and the caller must drop its own
462
+ * bookkeeping entry. Persistent records always return true (no timer).
463
+ */
464
+ #scheduleIrcExpiry(record: IrcObservationRecord, components: readonly Component[]): boolean {
465
+ if (record.mode !== "ephemeral" || components.length === 0 || this.#ircExpiryTimers.has(record.observationId)) {
466
+ return true;
467
+ }
468
+ const remainingMs = record.expiresAt! - Date.now();
469
+ if (remainingMs <= 0) {
470
+ this.#cleanupExpiredIrcInlineComponents(record.observationId);
471
+ return false;
472
+ }
473
+ const timer = setTimeout(() => {
474
+ this.#ircExpiryTimers.delete(record.observationId);
475
+ this.#cleanupExpiredIrcInlineComponents(record.observationId);
476
+ this.ctx.ui.requestRender();
477
+ }, remainingMs);
478
+ timer.unref?.();
479
+ this.#ircExpiryTimers.set(record.observationId, timer);
480
+ return true;
351
481
  }
352
482
 
353
483
  async #handleSubagentSteerMessage(
@@ -360,28 +490,13 @@ export class EventController {
360
490
  const signature = obsId
361
491
  ? `steer:${obsId}`
362
492
  : `${event.message.role}:${event.message.customType}:${event.message.timestamp}:${details?.from}:${details?.to}:${details?.state}:${details?.body}`;
363
- if (this.#renderedCustomMessages.has(signature)) {
364
- return;
365
- }
493
+ if (this.#renderedCustomMessages.has(signature)) return;
366
494
  this.#renderedCustomMessages.add(signature);
367
495
  this.#resetReadGroup();
368
496
  this.ctx.addMessageToChat(event.message);
369
497
  this.ctx.ui.requestRender();
370
498
  }
371
499
 
372
- #scheduleIrcExpiry(signature: string, components: Component[]): void {
373
- if (components.length === 0 || this.#ircExpiryTimers.has(signature)) return;
374
- const timer = setTimeout(() => {
375
- this.#ircExpiryTimers.delete(signature);
376
- for (const component of components) {
377
- this.ctx.chatContainer.removeChild(component);
378
- }
379
- this.ctx.ui.requestRender();
380
- }, IRC_MESSAGE_VISIBLE_TTL_MS);
381
- timer.unref?.();
382
- this.#ircExpiryTimers.set(signature, timer);
383
- }
384
-
385
500
  async #handleNotice(event: Extract<AgentSessionEvent, { type: "notice" }>): Promise<void> {
386
501
  const message = event.source ? `${event.source}: ${event.message}` : event.message;
387
502
  if (event.level === "error") {
@@ -395,6 +510,9 @@ export class EventController {
395
510
 
396
511
  async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
397
512
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
513
+ if (this.ctx.streamingMessage?.role === "assistant") {
514
+ transferSessionMessageIdentity([this.ctx.streamingMessage], [event.message]);
515
+ }
398
516
  this.ctx.streamingMessage = event.message;
399
517
  this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
400
518
  const contentIndex = event.assistantMessageEvent?.contentIndex;
@@ -498,6 +616,9 @@ export class EventController {
498
616
  async #handleMessageEnd(event: Extract<AgentSessionEvent, { type: "message_end" }>): Promise<void> {
499
617
  if (event.message.role === "user") return;
500
618
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
619
+ if (this.ctx.streamingMessage?.role === "assistant") {
620
+ transferSessionMessageIdentity([this.ctx.streamingMessage], [event.message]);
621
+ }
501
622
  this.ctx.streamingMessage = event.message;
502
623
  let errorMessage: string | undefined;
503
624
  const aborted = this.ctx.streamingMessage.stopReason === "aborted";
@@ -750,7 +871,7 @@ export class EventController {
750
871
  if (event.aborted) {
751
872
  this.ctx.showStatus(isHandoffAction ? "Auto-handoff cancelled" : "Auto context-full maintenance cancelled");
752
873
  } else if (event.result) {
753
- this.ctx.rebuildChatFromMessages();
874
+ this.ctx.rebuildChatFromMessages("reconcile-same-transcript");
754
875
  this.ctx.statusLine.invalidate();
755
876
  this.ctx.updateEditorTopBorder();
756
877
  if (continuationDisabled && !isHandoffAction) {
@@ -761,8 +882,10 @@ export class EventController {
761
882
  } else if (event.errorMessage) {
762
883
  this.ctx.showWarning(event.errorMessage);
763
884
  } else if (isHandoffAction) {
764
- this.ctx.chatContainer.clear();
765
- this.ctx.rebuildChatFromMessages();
885
+ // Reset BEFORE rebuild so the new session's transcript is not replayed
886
+ // from the old ledger and then cleared out from under its timers.
887
+ this.ctx.resetIrcSidebarSession();
888
+ this.ctx.rebuildChatFromMessages("replace-identity");
766
889
  this.ctx.statusLine.invalidate();
767
890
  this.ctx.updateEditorTopBorder();
768
891
  await this.ctx.reloadTodos();