@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
@@ -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();
@@ -25,6 +25,7 @@ import type { InteractiveModeContext } from "../../modes/types";
25
25
  import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
26
26
  import { applyInjectedUserSubmission } from "../utils/injected-user-submission";
27
27
  import { classifyHookSelectorBellEvent, ringTerminalBell } from "../utils/terminal-bell";
28
+ import { prepareTranscriptRebuild } from "../utils/ui-helpers";
28
29
 
29
30
  const MAX_WIDGET_LINES = 10;
30
31
  const HOOK_SELECTOR_CHROME_ROWS = 7;
@@ -150,9 +151,11 @@ export class ExtensionUiController {
150
151
  getContextUsage: () => this.ctx.session.getContextUsage(),
151
152
  waitForIdle: () => this.ctx.session.agent.waitForIdle(),
152
153
  reload: async () => {
154
+ const previousSessionId = this.ctx.sessionManager.getSessionId();
153
155
  await this.ctx.session.reload();
154
- this.ctx.chatContainer.clear();
155
- this.ctx.renderInitialMessages();
156
+ const sessionIdentityChanged = previousSessionId !== this.ctx.sessionManager.getSessionId();
157
+ if (sessionIdentityChanged) this.ctx.resetIrcSidebarSession();
158
+ this.ctx.rebuildInitialMessages(sessionIdentityChanged ? "replace-identity" : "reconcile-same-transcript");
156
159
  await this.ctx.reloadTodos();
157
160
  this.ctx.showStatus("Reloaded session");
158
161
  },
@@ -171,6 +174,8 @@ export class ExtensionUiController {
171
174
  if (!success) {
172
175
  return { cancelled: true };
173
176
  }
177
+ this.ctx.resetIrcSidebarSession();
178
+
174
179
  setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
175
180
 
176
181
  // Call setup callback if provided
@@ -185,6 +190,7 @@ export class ExtensionUiController {
185
190
  this.ctx.ui.requestRender();
186
191
 
187
192
  // Clear UI state
193
+ prepareTranscriptRebuild(this.ctx.ui, "replace-identity");
188
194
  this.ctx.chatContainer.clear();
189
195
  this.ctx.pendingMessagesContainer.clear();
190
196
  this.ctx.compactionQueuedMessages = [];
@@ -205,10 +211,10 @@ export class ExtensionUiController {
205
211
  if (result.cancelled) {
206
212
  return { cancelled: true };
207
213
  }
214
+ this.ctx.resetIrcSidebarSession();
208
215
 
209
216
  // Update UI
210
- this.ctx.chatContainer.clear();
211
- this.ctx.renderInitialMessages();
217
+ this.ctx.rebuildInitialMessages("replace-identity");
212
218
  await this.ctx.reloadTodos();
213
219
  this.ctx.editor.setText(result.selectedText);
214
220
  this.ctx.showStatus("Branched to new session");
@@ -222,8 +228,7 @@ export class ExtensionUiController {
222
228
  }
223
229
 
224
230
  // Update UI
225
- this.ctx.chatContainer.clear();
226
- this.ctx.renderInitialMessages();
231
+ this.ctx.rebuildInitialMessages("reconcile-same-transcript");
227
232
  await this.ctx.reloadTodos();
228
233
  if (result.editorText && !this.ctx.editor.getText().trim()) {
229
234
  this.ctx.editor.setText(result.editorText);
@@ -234,14 +239,20 @@ export class ExtensionUiController {
234
239
  },
235
240
  compact: async instructionsOrOptions => this.#handleInteractiveCompact(instructionsOrOptions),
236
241
  switchSession: async sessionPath => {
242
+ const previousSessionId = this.ctx.sessionManager.getSessionId();
243
+
237
244
  this.clearHookWidgets();
238
245
  const result = await this.ctx.session.switchSession(sessionPath);
239
246
  if (!result) {
240
247
  return { cancelled: true };
241
248
  }
249
+ const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
250
+ if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
251
+
242
252
  setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
243
- this.ctx.chatContainer.clear();
244
- this.ctx.renderInitialMessages();
253
+ this.ctx.rebuildInitialMessages(
254
+ switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript",
255
+ );
245
256
  await this.ctx.reloadTodos();
246
257
  return { cancelled: false };
247
258
  },
@@ -392,9 +403,11 @@ export class ExtensionUiController {
392
403
  if (this.ctx.isBackgrounded) {
393
404
  return;
394
405
  }
406
+ const previousSessionId = this.ctx.sessionManager.getSessionId();
395
407
  await this.ctx.session.reload();
396
- this.ctx.chatContainer.clear();
397
- this.ctx.renderInitialMessages();
408
+ const sessionIdentityChanged = previousSessionId !== this.ctx.sessionManager.getSessionId();
409
+ if (sessionIdentityChanged) this.ctx.resetIrcSidebarSession();
410
+ this.ctx.rebuildInitialMessages(sessionIdentityChanged ? "replace-identity" : "reconcile-same-transcript");
398
411
  await this.ctx.reloadTodos();
399
412
  this.ctx.showStatus("Reloaded session");
400
413
  },
@@ -416,6 +429,7 @@ export class ExtensionUiController {
416
429
  if (!success) {
417
430
  return { cancelled: true };
418
431
  }
432
+ this.ctx.resetIrcSidebarSession();
419
433
 
420
434
  // Call setup callback if provided
421
435
  if (options?.setup) {
@@ -423,6 +437,7 @@ export class ExtensionUiController {
423
437
  }
424
438
 
425
439
  // Clear UI state
440
+ prepareTranscriptRebuild(this.ctx.ui, "replace-identity");
426
441
  this.ctx.chatContainer.clear();
427
442
  this.ctx.pendingMessagesContainer.clear();
428
443
  this.ctx.compactionQueuedMessages = [];
@@ -446,10 +461,10 @@ export class ExtensionUiController {
446
461
  if (result.cancelled) {
447
462
  return { cancelled: true };
448
463
  }
464
+ this.ctx.resetIrcSidebarSession();
449
465
 
450
466
  // Update UI
451
- this.ctx.chatContainer.clear();
452
- this.ctx.renderInitialMessages();
467
+ this.ctx.rebuildInitialMessages("replace-identity");
453
468
  await this.ctx.reloadTodos();
454
469
  this.ctx.editor.setText(result.selectedText);
455
470
  this.ctx.showStatus("Branched to new session");
@@ -466,8 +481,7 @@ export class ExtensionUiController {
466
481
  }
467
482
 
468
483
  // Update UI
469
- this.ctx.chatContainer.clear();
470
- this.ctx.renderInitialMessages();
484
+ this.ctx.rebuildInitialMessages("reconcile-same-transcript");
471
485
  await this.ctx.reloadTodos();
472
486
  if (result.editorText && !this.ctx.editor.getText().trim()) {
473
487
  this.ctx.editor.setText(result.editorText);
@@ -481,13 +495,18 @@ export class ExtensionUiController {
481
495
  if (this.ctx.isBackgrounded) {
482
496
  return { cancelled: true };
483
497
  }
498
+ const previousSessionId = this.ctx.sessionManager.getSessionId();
499
+
484
500
  this.clearHookWidgets();
485
501
  const result = await this.ctx.session.switchSession(sessionPath);
486
502
  if (!result) {
487
503
  return { cancelled: true };
488
504
  }
489
- this.ctx.chatContainer.clear();
490
- this.ctx.renderInitialMessages();
505
+ const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
506
+ if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
507
+ this.ctx.rebuildInitialMessages(
508
+ switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript",
509
+ );
491
510
  await this.ctx.reloadTodos();
492
511
  return { cancelled: false };
493
512
  },
@@ -559,7 +578,7 @@ export class ExtensionUiController {
559
578
  shutdown: () => {
560
579
  // Signal shutdown request
561
580
  },
562
- getSystemPrompt: () => this.ctx.session.systemPrompt,
581
+ getSystemPrompt: () => [...this.ctx.session.systemPrompt],
563
582
  });
564
583
  } catch (err) {
565
584
  this.showToolError(registeredTool.definition.name, err instanceof Error ? err.message : String(err));
@@ -970,7 +989,7 @@ export class ExtensionUiController {
970
989
  // For non-streaming cases with display=true, update UI
971
990
  // (streaming cases update via message_end event)
972
991
  if (!this.ctx.isBackgrounded && !wasStreaming && shouldDisplay) {
973
- this.ctx.rebuildChatFromMessages();
992
+ this.ctx.rebuildChatFromMessages("reconcile-same-transcript");
974
993
  }
975
994
  }
976
995
 
@@ -331,6 +331,13 @@ export class InputController {
331
331
  // Wire up extension shortcuts
332
332
  this.registerExtensionShortcuts();
333
333
 
334
+ for (const key of this.ctx.keybindings.getKeys("app.irc.sidebar.toggle")) {
335
+ this.ctx.editor.setCustomKeyHandler(key, () => {
336
+ this.ctx.toggleIrcSidebar();
337
+ return true;
338
+ });
339
+ }
340
+
334
341
  const planModeKeys = this.ctx.keybindings.getKeys("app.plan.toggle");
335
342
  for (const key of planModeKeys) {
336
343
  this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handlePlanModeCommand());
@@ -1344,8 +1351,7 @@ export class InputController {
1344
1351
  if (this.ctx.streamingComponent) {
1345
1352
  this.ctx.chatContainer.detachChild(this.ctx.streamingComponent);
1346
1353
  }
1347
- this.ctx.chatContainer.clear();
1348
- this.ctx.rebuildChatFromMessages();
1354
+ this.ctx.rebuildChatFromMessages("reconcile-same-transcript");
1349
1355
 
1350
1356
  // If streaming, re-add the streaming component with updated visibility and re-render
1351
1357
  if (this.ctx.streamingComponent && this.ctx.streamingMessage) {
@@ -2,7 +2,7 @@ import { ThinkingLevel } from "@gajae-code/agent-core";
2
2
  import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
3
3
  import type { OAuthProvider } from "@gajae-code/ai/utils/oauth/types";
4
4
  import type { Component, OverlayHandle } from "@gajae-code/tui";
5
- import { Input, Loader, Spacer, Text } from "@gajae-code/tui";
5
+ import { Input, isPetMode, Loader, Spacer, Text } from "@gajae-code/tui";
6
6
  import { getAgentDbPath, getProjectDir } from "@gajae-code/utils";
7
7
  import {
8
8
  activateModelProfile,
@@ -74,10 +74,12 @@ import {
74
74
  } from "../components/custom-model-preset-wizard";
75
75
  import { CustomProviderWizardComponent, type CustomProviderWizardSubmit } from "../components/custom-provider-wizard";
76
76
  import { ExtensionDashboard } from "../components/extensions";
77
+ import type { PetMode } from "../components/gajae-pet-widget";
77
78
  import { HistorySearchComponent } from "../components/history-search";
78
79
  import { JobsOverlayComponent } from "../components/jobs-overlay";
79
80
  import { ModelSelectorComponent } from "../components/model-selector";
80
81
  import { OAuthSelectorComponent } from "../components/oauth-selector";
82
+ import { PetSelectorComponent } from "../components/pet-selector";
81
83
  import { PluginSelectorComponent } from "../components/plugin-selector";
82
84
  import {
83
85
  type ProviderOnboardingAction,
@@ -158,9 +160,15 @@ export class SelectorController {
158
160
  */
159
161
  showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {
160
162
  const done = () => {
161
- this.ctx.editorContainer.clear();
162
- this.ctx.editorContainer.addChild(this.ctx.editor);
163
- this.ctx.ui.setFocus(this.ctx.editor);
163
+ // Prefer the pet-aware composer restore (InteractiveMode.restoreComposer); fall back
164
+ // to a plain editor swap for contexts that predate it (e.g. lightweight test doubles).
165
+ if (typeof this.ctx.restoreComposer === "function") {
166
+ this.ctx.restoreComposer();
167
+ } else {
168
+ this.ctx.editorContainer.clear();
169
+ this.ctx.editorContainer.addChild(this.ctx.editor);
170
+ this.ctx.ui.setFocus(this.ctx.editor);
171
+ }
164
172
  };
165
173
  const { component, focus } = create(done);
166
174
  this.ctx.editorContainer.clear();
@@ -503,6 +511,9 @@ export class SelectorController {
503
511
  this.#refreshThemeUi();
504
512
  });
505
513
  },
514
+ onPetPreview: mode => {
515
+ this.ctx.previewPetMode(mode as PetMode);
516
+ },
506
517
  onStatusLinePreview: previewSettings => {
507
518
  // Update status line with preview settings
508
519
  this.ctx.statusLine.updateSettings({
@@ -577,6 +588,29 @@ export class SelectorController {
577
588
  });
578
589
  }
579
590
 
591
+ showPetSelector(): void {
592
+ const stored = settings.get("pet.mode");
593
+ const initial: PetMode = isPetMode(stored) ? stored : "off";
594
+ this.showSelector(done => {
595
+ // Live-preview via previewMode (no editor re-mount, so the overlay stays);
596
+ // Enter commits + persists, Esc restores the initial skin.
597
+ const selector = new PetSelectorComponent(
598
+ initial,
599
+ mode => {
600
+ this.ctx.setPetMode(mode);
601
+ done();
602
+ },
603
+ () => {
604
+ this.ctx.previewPetMode(initial);
605
+ done();
606
+ },
607
+ mode => {
608
+ this.ctx.previewPetMode(mode);
609
+ },
610
+ );
611
+ return { component: selector, focus: selector.getSelectList() };
612
+ });
613
+ }
580
614
  showHistorySearch(): void {
581
615
  const historyStorage = this.ctx.historyStorage;
582
616
  if (!historyStorage) return;
@@ -715,8 +749,7 @@ export class SelectorController {
715
749
  child.setHideThinkingBlock(value as boolean);
716
750
  }
717
751
  }
718
- this.ctx.chatContainer.clear();
719
- this.ctx.rebuildChatFromMessages();
752
+ this.ctx.rebuildChatFromMessages("reconcile-same-transcript");
720
753
  break;
721
754
  case "theme": {
722
755
  setTheme(value as string, true).then(result => {
@@ -729,6 +762,12 @@ export class SelectorController {
729
762
  });
730
763
  break;
731
764
  }
765
+ case "pet.mode":
766
+ // The settings submenu already persisted the value; apply it to the live
767
+ // widget via previewMode (the settings overlay is still open, so a full
768
+ // re-mount would tear it down — restoreComposer re-mounts on close).
769
+ this.ctx.previewPetMode(value as PetMode);
770
+ break;
732
771
  case "symbolPreset": {
733
772
  setSymbolPreset(value as "unicode" | "nerd" | "ascii").then(() => {
734
773
  this.ctx.statusLine.invalidate();
@@ -800,6 +839,12 @@ export class SelectorController {
800
839
  this.ctx.ui.requestRender();
801
840
  break;
802
841
  }
842
+ case "irc.enabled":
843
+ case "irc.sidebar.enabled":
844
+ this.ctx.applyIrcSidebarAvailability(
845
+ this.ctx.settings.get("irc.enabled") === true && this.ctx.settings.get("irc.sidebar.enabled") === true,
846
+ );
847
+ break;
803
848
 
804
849
  // Provider settings - update runtime preferences
805
850
  case "providers.webSearch":
@@ -1173,9 +1218,9 @@ export class SelectorController {
1173
1218
  this.ctx.ui.requestRender();
1174
1219
  return;
1175
1220
  }
1221
+ this.ctx.resetIrcSidebarSession();
1176
1222
 
1177
- this.ctx.chatContainer.clear();
1178
- this.ctx.renderInitialMessages();
1223
+ this.ctx.rebuildInitialMessages("replace-identity");
1179
1224
  this.ctx.editor.setText(result.selectedText);
1180
1225
  done();
1181
1226
  this.ctx.showStatus("Branched to new session");
@@ -1285,8 +1330,7 @@ export class SelectorController {
1285
1330
  }
1286
1331
 
1287
1332
  // Update UI — pass the context built by navigateTree to skip a second O(N) walk.
1288
- this.ctx.chatContainer.clear();
1289
- this.ctx.renderInitialMessages(result.sessionContext);
1333
+ this.ctx.rebuildInitialMessages("reconcile-same-transcript", result.sessionContext);
1290
1334
  await this.ctx.reloadTodos();
1291
1335
  if (result.editorText && !this.ctx.editor.getText().trim()) {
1292
1336
  this.ctx.editor.setText(result.editorText);
@@ -1387,6 +1431,8 @@ export class SelectorController {
1387
1431
  if (!detached) {
1388
1432
  return false;
1389
1433
  }
1434
+ this.ctx.resetIrcSidebarSession();
1435
+
1390
1436
  this.#refreshSessionTerminalTitle();
1391
1437
 
1392
1438
  this.#clearTransientSessionUi();
@@ -1394,23 +1440,24 @@ export class SelectorController {
1394
1440
  this.ctx.statusLine.setSessionStartTime(Date.now());
1395
1441
  this.ctx.updateEditorTopBorder();
1396
1442
  this.ctx.updateEditorBorderColor();
1397
- this.ctx.renderInitialMessages();
1443
+ this.ctx.rebuildInitialMessages("replace-identity");
1398
1444
  await this.ctx.reloadTodos();
1399
1445
  this.ctx.ui.requestRender();
1400
1446
  return true;
1401
1447
  }
1402
1448
 
1403
1449
  async handleResumeSession(sessionPath: string): Promise<void> {
1450
+ const previousSessionId = this.ctx.sessionManager.getSessionId();
1404
1451
  this.#clearTransientSessionUi();
1405
1452
 
1406
1453
  // Switch session via AgentSession (emits hook and tool session events)
1407
- await this.ctx.session.switchSession(sessionPath);
1454
+ if (!(await this.ctx.session.switchSession(sessionPath))) return;
1455
+ const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
1456
+ if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
1408
1457
  this.#refreshSessionTerminalTitle();
1409
1458
  this.ctx.updateEditorBorderColor();
1410
1459
 
1411
- // Clear and re-render the chat
1412
- this.ctx.chatContainer.clear();
1413
- this.ctx.renderInitialMessages();
1460
+ this.ctx.rebuildInitialMessages(switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript");
1414
1461
  await this.ctx.reloadTodos();
1415
1462
  this.ctx.showStatus("Resumed session");
1416
1463
  }