@sayknow-cli/coding-agent 0.3.12 → 0.3.15

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 (248) hide show
  1. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  2. package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
  3. package/dist/types/cli/classify-install-target.d.ts +0 -10
  4. package/dist/types/cli/daemon-cli.d.ts +2 -0
  5. package/dist/types/cli/initial-message.d.ts +1 -1
  6. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  7. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  8. package/dist/types/cli/notify-cli.d.ts +3 -1
  9. package/dist/types/cli/session-picker.d.ts +4 -3
  10. package/dist/types/cli/update-cli.d.ts +22 -3
  11. package/dist/types/cli.d.ts +5 -0
  12. package/dist/types/commands/daemon.d.ts +6 -2
  13. package/dist/types/commands/launch.d.ts +1 -0
  14. package/dist/types/commands/notify.d.ts +6 -0
  15. package/dist/types/commands/web-search.d.ts +1 -1
  16. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
  17. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
  18. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
  19. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
  20. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  21. package/dist/types/commit/analysis/summary.d.ts +1 -1
  22. package/dist/types/commit/changelog/generate.d.ts +3 -3
  23. package/dist/types/commit/changelog/index.d.ts +2 -2
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  26. package/dist/types/commit/shared-llm.d.ts +9 -9
  27. package/dist/types/config/keybindings.d.ts +134 -129
  28. package/dist/types/config/model-profile-activation.d.ts +1 -0
  29. package/dist/types/config/model-registry.d.ts +35 -35
  30. package/dist/types/config/models-config-schema.d.ts +111 -111
  31. package/dist/types/config/settings-schema.d.ts +56 -16
  32. package/dist/types/config/settings.d.ts +2 -0
  33. package/dist/types/coordinator/contract.d.ts +1 -1
  34. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  35. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  36. package/dist/types/coordinator-mcp/server.d.ts +79 -6
  37. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  38. package/dist/types/daemon/control-types.d.ts +10 -0
  39. package/dist/types/daemon/operator-contract.d.ts +47 -0
  40. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  41. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
  42. package/dist/types/discovery/helpers.d.ts +1 -1
  43. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  44. package/dist/types/edit/modes/patch.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  46. package/dist/types/goals/tools/goal-tool.d.ts +6 -6
  47. package/dist/types/i18n/messages/en.d.ts +8 -8
  48. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  49. package/dist/types/lsp/index.d.ts +9 -9
  50. package/dist/types/lsp/types.d.ts +9 -9
  51. package/dist/types/main.d.ts +66 -2
  52. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  53. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  54. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  56. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  57. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  58. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  59. package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
  60. package/dist/types/modes/components/session-selector.d.ts +22 -9
  61. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  62. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  63. package/dist/types/modes/components/status-line.d.ts +0 -10
  64. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  65. package/dist/types/modes/components/user-message.d.ts +2 -1
  66. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  67. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  68. package/dist/types/modes/interactive-mode.d.ts +24 -2
  69. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  70. package/dist/types/modes/print-mode.d.ts +3 -4
  71. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  72. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  73. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  74. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  75. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  76. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  77. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  78. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  79. package/dist/types/modes/types.d.ts +28 -2
  80. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  81. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  82. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  83. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  84. package/dist/types/notifications/config.d.ts +34 -0
  85. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  86. package/dist/types/notifications/index.d.ts +27 -0
  87. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  88. package/dist/types/notifications/notification-service.d.ts +144 -0
  89. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  90. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  91. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  92. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  93. package/dist/types/notifications/topic-registry.d.ts +24 -4
  94. package/dist/types/registry/agent-registry.d.ts +2 -0
  95. package/dist/types/rlm/index.d.ts +1 -1
  96. package/dist/types/sdk.d.ts +2 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +40 -3
  99. package/dist/types/session/cache-economics.d.ts +79 -0
  100. package/dist/types/session/context-estimation.d.ts +31 -0
  101. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  102. package/dist/types/session/session-manager.d.ts +124 -4
  103. package/dist/types/session/session-storage.d.ts +155 -13
  104. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  105. package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
  106. package/dist/types/skc-runtime/state-schema.d.ts +19 -19
  107. package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
  108. package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  109. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
  110. package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
  111. package/dist/types/task/agents.d.ts +2 -2
  112. package/dist/types/task/executor.d.ts +3 -0
  113. package/dist/types/task/receipt.d.ts +3 -1
  114. package/dist/types/task/types.d.ts +35 -27
  115. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  116. package/dist/types/tools/bash.d.ts +1 -1
  117. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  118. package/dist/types/tools/browser.d.ts +28 -28
  119. package/dist/types/tools/computer.d.ts +20 -20
  120. package/dist/types/tools/cron.d.ts +2 -2
  121. package/dist/types/tools/debug.d.ts +40 -40
  122. package/dist/types/tools/gh.d.ts +10 -10
  123. package/dist/types/tools/image-gen.d.ts +2 -2
  124. package/dist/types/tools/index.d.ts +58 -6
  125. package/dist/types/tools/monitor.d.ts +6 -6
  126. package/dist/types/tools/skill-discovery.d.ts +4 -4
  127. package/dist/types/tools/subagent.d.ts +12 -12
  128. package/dist/types/tools/telegram-send.d.ts +7 -0
  129. package/dist/types/tools/todo-write.d.ts +8 -8
  130. package/dist/types/utils/external-editor.d.ts +2 -0
  131. package/dist/types/web/insane/bridge.d.ts +9 -9
  132. package/dist/types/web/search/index.d.ts +4 -4
  133. package/package.json +8 -8
  134. package/scripts/g004-tmux-smoke.ts +196 -36
  135. package/src/cli/daemon-cli.ts +19 -29
  136. package/src/cli/malloc-env-guard.test.ts +104 -0
  137. package/src/cli/malloc-env-guard.ts +149 -0
  138. package/src/cli/notify-cli.ts +78 -14
  139. package/src/cli/session-picker.ts +23 -40
  140. package/src/cli/update-cli.ts +56 -37
  141. package/src/cli.ts +51 -10
  142. package/src/commands/daemon.ts +19 -5
  143. package/src/commands/harness.ts +515 -47
  144. package/src/commands/launch.ts +11 -3
  145. package/src/commands/notify.ts +5 -1
  146. package/src/commands/session.ts +6 -1
  147. package/src/config/keybindings.ts +5 -0
  148. package/src/config/model-profile-activation.ts +1 -1
  149. package/src/config/model-profiles.ts +14 -14
  150. package/src/config/settings-schema.ts +41 -1
  151. package/src/config/settings.ts +96 -45
  152. package/src/coordinator/contract.ts +1 -0
  153. package/src/coordinator-mcp/model-preset.ts +100 -0
  154. package/src/coordinator-mcp/policy.ts +30 -0
  155. package/src/coordinator-mcp/server.ts +2958 -430
  156. package/src/coordinator-mcp/session-reaper.ts +129 -0
  157. package/src/daemon/control-types.ts +11 -0
  158. package/src/daemon/operator-contract.ts +121 -0
  159. package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  160. package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  161. package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  162. package/src/extensibility/extensions/runner.ts +1 -1
  163. package/src/extensibility/extensions/types.ts +3 -1
  164. package/src/internal-urls/docs-index.generated.ts +10 -9
  165. package/src/internal-urls/local-protocol.ts +29 -10
  166. package/src/main.ts +301 -80
  167. package/src/modes/acp/acp-agent.ts +730 -100
  168. package/src/modes/acp/acp-mode.ts +29 -2
  169. package/src/modes/components/assistant-message.ts +32 -3
  170. package/src/modes/components/bash-execution.ts +43 -13
  171. package/src/modes/components/custom-editor.ts +5 -0
  172. package/src/modes/components/footer.ts +3 -3
  173. package/src/modes/components/hook-selector.ts +12 -0
  174. package/src/modes/components/irc-sidebar.ts +206 -0
  175. package/src/modes/components/pet-selector.ts +55 -0
  176. package/src/modes/components/sayknow-pet-widget.ts +360 -0
  177. package/src/modes/components/session-selector.ts +222 -221
  178. package/src/modes/components/settings-selector.ts +10 -0
  179. package/src/modes/components/status-line/segments.ts +14 -7
  180. package/src/modes/components/status-line/types.ts +1 -1
  181. package/src/modes/components/status-line.ts +4 -171
  182. package/src/modes/components/tool-execution.ts +47 -6
  183. package/src/modes/components/user-message.ts +26 -9
  184. package/src/modes/controllers/command-controller.ts +28 -2
  185. package/src/modes/controllers/event-controller.ts +159 -36
  186. package/src/modes/controllers/extension-ui-controller.ts +37 -18
  187. package/src/modes/controllers/input-controller.ts +8 -2
  188. package/src/modes/controllers/selector-controller.ts +62 -15
  189. package/src/modes/interactive-mode.ts +199 -7
  190. package/src/modes/irc-observation-ledger.ts +127 -0
  191. package/src/modes/print-mode.ts +228 -92
  192. package/src/modes/rpc/rpc-client.ts +58 -1
  193. package/src/modes/rpc/rpc-mode.ts +286 -85
  194. package/src/modes/rpc/rpc-types.ts +15 -1
  195. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  196. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  197. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  198. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  199. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  200. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  201. package/src/modes/types.ts +34 -2
  202. package/src/modes/utils/context-usage.ts +56 -92
  203. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  204. package/src/modes/utils/irc-message.ts +138 -0
  205. package/src/modes/utils/ui-helpers.ts +216 -50
  206. package/src/notifications/config.ts +44 -0
  207. package/src/notifications/daemon-paths.ts +8 -0
  208. package/src/notifications/index.ts +535 -75
  209. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  210. package/src/notifications/notification-service.ts +710 -0
  211. package/src/notifications/rate-limit-pool.ts +46 -12
  212. package/src/notifications/telegram-daemon-control.ts +27 -2
  213. package/src/notifications/telegram-daemon.ts +635 -94
  214. package/src/notifications/telegram-reference.ts +73 -13
  215. package/src/notifications/topic-registry.ts +86 -9
  216. package/src/prompts/system/irc-peer-roster.md +1 -0
  217. package/src/prompts/system/subagent-system-prompt.md +1 -0
  218. package/src/prompts/system/system-prompt.md +1 -0
  219. package/src/prompts/tools/irc.md +1 -1
  220. package/src/registry/agent-registry.ts +3 -0
  221. package/src/sdk.ts +52 -1
  222. package/src/secrets/obfuscator.ts +4 -2
  223. package/src/session/agent-session.ts +670 -217
  224. package/src/session/cache-economics.ts +275 -0
  225. package/src/session/context-estimation.ts +91 -0
  226. package/src/session/provider-safety-stop.ts +9 -0
  227. package/src/session/session-dump-format.ts +6 -0
  228. package/src/session/session-manager.ts +809 -39
  229. package/src/session/session-storage.ts +582 -38
  230. package/src/skc-runtime/session-state-sidecar.ts +756 -135
  231. package/src/skc-runtime/tmux-common.ts +26 -7
  232. package/src/skc-runtime/tmux-gc.ts +52 -9
  233. package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
  234. package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
  235. package/src/skc-runtime/tmux-sessions.ts +871 -68
  236. package/src/slash-commands/builtin-registry.ts +153 -9
  237. package/src/slash-commands/helpers/context-report.ts +26 -4
  238. package/src/task/executor.ts +12 -3
  239. package/src/task/index.ts +8 -0
  240. package/src/task/receipt.ts +7 -2
  241. package/src/task/types.ts +26 -0
  242. package/src/tools/ask-answer-registry.ts +25 -0
  243. package/src/tools/browser/tab-supervisor.ts +63 -6
  244. package/src/tools/browser.ts +8 -3
  245. package/src/tools/index.ts +81 -6
  246. package/src/tools/sqlite-reader.ts +8 -4
  247. package/src/tui/output-block.ts +18 -6
  248. 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();
@@ -26,6 +26,7 @@ import type { InteractiveModeContext } from "../../modes/types";
26
26
  import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
27
27
  import { applyInjectedUserSubmission } from "../utils/injected-user-submission";
28
28
  import { classifyHookSelectorBellEvent, ringTerminalBell } from "../utils/terminal-bell";
29
+ import { prepareTranscriptRebuild } from "../utils/ui-helpers";
29
30
 
30
31
  const MAX_WIDGET_LINES = 10;
31
32
  const HOOK_SELECTOR_CHROME_ROWS = 7;
@@ -151,9 +152,11 @@ export class ExtensionUiController {
151
152
  getContextUsage: () => this.ctx.session.getContextUsage(),
152
153
  waitForIdle: () => this.ctx.session.agent.waitForIdle(),
153
154
  reload: async () => {
155
+ const previousSessionId = this.ctx.sessionManager.getSessionId();
154
156
  await this.ctx.session.reload();
155
- this.ctx.chatContainer.clear();
156
- this.ctx.renderInitialMessages();
157
+ const sessionIdentityChanged = previousSessionId !== this.ctx.sessionManager.getSessionId();
158
+ if (sessionIdentityChanged) this.ctx.resetIrcSidebarSession();
159
+ this.ctx.rebuildInitialMessages(sessionIdentityChanged ? "replace-identity" : "reconcile-same-transcript");
157
160
  await this.ctx.reloadTodos();
158
161
  this.ctx.showStatus("Reloaded session");
159
162
  },
@@ -172,6 +175,8 @@ export class ExtensionUiController {
172
175
  if (!success) {
173
176
  return { cancelled: true };
174
177
  }
178
+ this.ctx.resetIrcSidebarSession();
179
+
175
180
  setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
176
181
 
177
182
  // Call setup callback if provided
@@ -186,6 +191,7 @@ export class ExtensionUiController {
186
191
  this.ctx.ui.requestRender();
187
192
 
188
193
  // Clear UI state
194
+ prepareTranscriptRebuild(this.ctx.ui, "replace-identity");
189
195
  this.ctx.chatContainer.clear();
190
196
  this.ctx.pendingMessagesContainer.clear();
191
197
  this.ctx.compactionQueuedMessages = [];
@@ -206,10 +212,10 @@ export class ExtensionUiController {
206
212
  if (result.cancelled) {
207
213
  return { cancelled: true };
208
214
  }
215
+ this.ctx.resetIrcSidebarSession();
209
216
 
210
217
  // Update UI
211
- this.ctx.chatContainer.clear();
212
- this.ctx.renderInitialMessages();
218
+ this.ctx.rebuildInitialMessages("replace-identity");
213
219
  await this.ctx.reloadTodos();
214
220
  this.ctx.editor.setText(result.selectedText);
215
221
  this.ctx.showStatus("Branched to new session");
@@ -223,8 +229,7 @@ export class ExtensionUiController {
223
229
  }
224
230
 
225
231
  // Update UI
226
- this.ctx.chatContainer.clear();
227
- this.ctx.renderInitialMessages();
232
+ this.ctx.rebuildInitialMessages("reconcile-same-transcript");
228
233
  await this.ctx.reloadTodos();
229
234
  if (result.editorText && !this.ctx.editor.getText().trim()) {
230
235
  this.ctx.editor.setText(result.editorText);
@@ -235,14 +240,20 @@ export class ExtensionUiController {
235
240
  },
236
241
  compact: async instructionsOrOptions => this.#handleInteractiveCompact(instructionsOrOptions),
237
242
  switchSession: async sessionPath => {
243
+ const previousSessionId = this.ctx.sessionManager.getSessionId();
244
+
238
245
  this.clearHookWidgets();
239
246
  const result = await this.ctx.session.switchSession(sessionPath);
240
247
  if (!result) {
241
248
  return { cancelled: true };
242
249
  }
250
+ const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
251
+ if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
252
+
243
253
  setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
244
- this.ctx.chatContainer.clear();
245
- this.ctx.renderInitialMessages();
254
+ this.ctx.rebuildInitialMessages(
255
+ switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript",
256
+ );
246
257
  await this.ctx.reloadTodos();
247
258
  return { cancelled: false };
248
259
  },
@@ -393,9 +404,11 @@ export class ExtensionUiController {
393
404
  if (this.ctx.isBackgrounded) {
394
405
  return;
395
406
  }
407
+ const previousSessionId = this.ctx.sessionManager.getSessionId();
396
408
  await this.ctx.session.reload();
397
- this.ctx.chatContainer.clear();
398
- this.ctx.renderInitialMessages();
409
+ const sessionIdentityChanged = previousSessionId !== this.ctx.sessionManager.getSessionId();
410
+ if (sessionIdentityChanged) this.ctx.resetIrcSidebarSession();
411
+ this.ctx.rebuildInitialMessages(sessionIdentityChanged ? "replace-identity" : "reconcile-same-transcript");
399
412
  await this.ctx.reloadTodos();
400
413
  this.ctx.showStatus("Reloaded session");
401
414
  },
@@ -417,6 +430,7 @@ export class ExtensionUiController {
417
430
  if (!success) {
418
431
  return { cancelled: true };
419
432
  }
433
+ this.ctx.resetIrcSidebarSession();
420
434
 
421
435
  // Call setup callback if provided
422
436
  if (options?.setup) {
@@ -424,6 +438,7 @@ export class ExtensionUiController {
424
438
  }
425
439
 
426
440
  // Clear UI state
441
+ prepareTranscriptRebuild(this.ctx.ui, "replace-identity");
427
442
  this.ctx.chatContainer.clear();
428
443
  this.ctx.pendingMessagesContainer.clear();
429
444
  this.ctx.compactionQueuedMessages = [];
@@ -447,10 +462,10 @@ export class ExtensionUiController {
447
462
  if (result.cancelled) {
448
463
  return { cancelled: true };
449
464
  }
465
+ this.ctx.resetIrcSidebarSession();
450
466
 
451
467
  // Update UI
452
- this.ctx.chatContainer.clear();
453
- this.ctx.renderInitialMessages();
468
+ this.ctx.rebuildInitialMessages("replace-identity");
454
469
  await this.ctx.reloadTodos();
455
470
  this.ctx.editor.setText(result.selectedText);
456
471
  this.ctx.showStatus("Branched to new session");
@@ -467,8 +482,7 @@ export class ExtensionUiController {
467
482
  }
468
483
 
469
484
  // Update UI
470
- this.ctx.chatContainer.clear();
471
- this.ctx.renderInitialMessages();
485
+ this.ctx.rebuildInitialMessages("reconcile-same-transcript");
472
486
  await this.ctx.reloadTodos();
473
487
  if (result.editorText && !this.ctx.editor.getText().trim()) {
474
488
  this.ctx.editor.setText(result.editorText);
@@ -482,13 +496,18 @@ export class ExtensionUiController {
482
496
  if (this.ctx.isBackgrounded) {
483
497
  return { cancelled: true };
484
498
  }
499
+ const previousSessionId = this.ctx.sessionManager.getSessionId();
500
+
485
501
  this.clearHookWidgets();
486
502
  const result = await this.ctx.session.switchSession(sessionPath);
487
503
  if (!result) {
488
504
  return { cancelled: true };
489
505
  }
490
- this.ctx.chatContainer.clear();
491
- this.ctx.renderInitialMessages();
506
+ const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
507
+ if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
508
+ this.ctx.rebuildInitialMessages(
509
+ switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript",
510
+ );
492
511
  await this.ctx.reloadTodos();
493
512
  return { cancelled: false };
494
513
  },
@@ -560,7 +579,7 @@ export class ExtensionUiController {
560
579
  shutdown: () => {
561
580
  // Signal shutdown request
562
581
  },
563
- getSystemPrompt: () => this.ctx.session.systemPrompt,
582
+ getSystemPrompt: () => [...this.ctx.session.systemPrompt],
564
583
  });
565
584
  } catch (err) {
566
585
  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 "@sayknow-cli/agent-core";
2
2
  import { getOAuthProviders } from "@sayknow-cli/ai/utils/oauth";
3
3
  import type { OAuthProvider } from "@sayknow-cli/ai/utils/oauth/types";
4
4
  import type { Component, OverlayHandle } from "@sayknow-cli/tui";
5
- import { Input, Loader, Spacer, Text } from "@sayknow-cli/tui";
5
+ import { Input, isPetMode, Loader, Spacer, Text } from "@sayknow-cli/tui";
6
6
  import { getAgentDbPath, getProjectDir } from "@sayknow-cli/utils";
7
7
  import {
8
8
  activateModelProfile,
@@ -71,11 +71,13 @@ import { HistorySearchComponent } from "../components/history-search";
71
71
  import { JobsOverlayComponent } from "../components/jobs-overlay";
72
72
  import { ModelSelectorComponent } from "../components/model-selector";
73
73
  import { OAuthSelectorComponent } from "../components/oauth-selector";
74
+ import { PetSelectorComponent } from "../components/pet-selector";
74
75
  import { PluginSelectorComponent } from "../components/plugin-selector";
75
76
  import {
76
77
  type ProviderOnboardingAction,
77
78
  ProviderOnboardingSelectorComponent,
78
79
  } from "../components/provider-onboarding-selector";
80
+ import type { PetMode } from "../components/sayknow-pet-widget";
79
81
  import { SessionObserverOverlayComponent } from "../components/session-observer-overlay";
80
82
  import { SessionSelectorComponent } from "../components/session-selector";
81
83
  import { SettingsSelectorComponent } from "../components/settings-selector";
@@ -155,9 +157,15 @@ export class SelectorController {
155
157
  */
156
158
  showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {
157
159
  const done = () => {
158
- this.ctx.editorContainer.clear();
159
- this.ctx.editorContainer.addChild(this.ctx.editor);
160
- this.ctx.ui.setFocus(this.ctx.editor);
160
+ // Prefer the pet-aware composer restore (InteractiveMode.restoreComposer); fall back
161
+ // to a plain editor swap for contexts that predate it (e.g. lightweight test doubles).
162
+ if (typeof this.ctx.restoreComposer === "function") {
163
+ this.ctx.restoreComposer();
164
+ } else {
165
+ this.ctx.editorContainer.clear();
166
+ this.ctx.editorContainer.addChild(this.ctx.editor);
167
+ this.ctx.ui.setFocus(this.ctx.editor);
168
+ }
161
169
  };
162
170
  const { component, focus } = create(done);
163
171
  this.ctx.editorContainer.clear();
@@ -400,6 +408,9 @@ export class SelectorController {
400
408
  this.#refreshThemeUi();
401
409
  });
402
410
  },
411
+ onPetPreview: mode => {
412
+ this.ctx.previewPetMode(mode as PetMode);
413
+ },
403
414
  onStatusLinePreview: previewSettings => {
404
415
  // Update status line with preview settings
405
416
  this.ctx.statusLine.updateSettings({
@@ -474,6 +485,29 @@ export class SelectorController {
474
485
  });
475
486
  }
476
487
 
488
+ showPetSelector(): void {
489
+ const stored = settings.get("pet.mode");
490
+ const initial: PetMode = isPetMode(stored) ? stored : "off";
491
+ this.showSelector(done => {
492
+ // Live-preview via previewMode (no editor re-mount, so the overlay stays);
493
+ // Enter commits + persists, Esc restores the initial skin.
494
+ const selector = new PetSelectorComponent(
495
+ initial,
496
+ mode => {
497
+ this.ctx.setPetMode(mode);
498
+ done();
499
+ },
500
+ () => {
501
+ this.ctx.previewPetMode(initial);
502
+ done();
503
+ },
504
+ mode => {
505
+ this.ctx.previewPetMode(mode);
506
+ },
507
+ );
508
+ return { component: selector, focus: selector.getSelectList() };
509
+ });
510
+ }
477
511
  showHistorySearch(): void {
478
512
  const historyStorage = this.ctx.historyStorage;
479
513
  if (!historyStorage) return;
@@ -612,8 +646,7 @@ export class SelectorController {
612
646
  child.setHideThinkingBlock(value as boolean);
613
647
  }
614
648
  }
615
- this.ctx.chatContainer.clear();
616
- this.ctx.rebuildChatFromMessages();
649
+ this.ctx.rebuildChatFromMessages("reconcile-same-transcript");
617
650
  break;
618
651
  case "theme": {
619
652
  setTheme(value as string, true).then(result => {
@@ -626,6 +659,12 @@ export class SelectorController {
626
659
  });
627
660
  break;
628
661
  }
662
+ case "pet.mode":
663
+ // The settings submenu already persisted the value; apply it to the live
664
+ // widget via previewMode (the settings overlay is still open, so a full
665
+ // re-mount would tear it down — restoreComposer re-mounts on close).
666
+ this.ctx.previewPetMode(value as PetMode);
667
+ break;
629
668
  case "symbolPreset": {
630
669
  setSymbolPreset(value as "unicode" | "nerd" | "ascii").then(() => {
631
670
  this.ctx.statusLine.invalidate();
@@ -697,6 +736,12 @@ export class SelectorController {
697
736
  this.ctx.ui.requestRender();
698
737
  break;
699
738
  }
739
+ case "irc.enabled":
740
+ case "irc.sidebar.enabled":
741
+ this.ctx.applyIrcSidebarAvailability(
742
+ this.ctx.settings.get("irc.enabled") === true && this.ctx.settings.get("irc.sidebar.enabled") === true,
743
+ );
744
+ break;
700
745
 
701
746
  // Provider settings - update runtime preferences
702
747
  case "providers.webSearch":
@@ -1075,9 +1120,9 @@ export class SelectorController {
1075
1120
  this.ctx.ui.requestRender();
1076
1121
  return;
1077
1122
  }
1123
+ this.ctx.resetIrcSidebarSession();
1078
1124
 
1079
- this.ctx.chatContainer.clear();
1080
- this.ctx.renderInitialMessages();
1125
+ this.ctx.rebuildInitialMessages("replace-identity");
1081
1126
  this.ctx.editor.setText(result.selectedText);
1082
1127
  done();
1083
1128
  this.ctx.showStatus("Branched to new session");
@@ -1187,8 +1232,7 @@ export class SelectorController {
1187
1232
  }
1188
1233
 
1189
1234
  // Update UI — pass the context built by navigateTree to skip a second O(N) walk.
1190
- this.ctx.chatContainer.clear();
1191
- this.ctx.renderInitialMessages(result.sessionContext);
1235
+ this.ctx.rebuildInitialMessages("reconcile-same-transcript", result.sessionContext);
1192
1236
  await this.ctx.reloadTodos();
1193
1237
  if (result.editorText && !this.ctx.editor.getText().trim()) {
1194
1238
  this.ctx.editor.setText(result.editorText);
@@ -1289,6 +1333,8 @@ export class SelectorController {
1289
1333
  if (!detached) {
1290
1334
  return false;
1291
1335
  }
1336
+ this.ctx.resetIrcSidebarSession();
1337
+
1292
1338
  this.#refreshSessionTerminalTitle();
1293
1339
 
1294
1340
  this.#clearTransientSessionUi();
@@ -1296,23 +1342,24 @@ export class SelectorController {
1296
1342
  this.ctx.statusLine.setSessionStartTime(Date.now());
1297
1343
  this.ctx.updateEditorTopBorder();
1298
1344
  this.ctx.updateEditorBorderColor();
1299
- this.ctx.renderInitialMessages();
1345
+ this.ctx.rebuildInitialMessages("replace-identity");
1300
1346
  await this.ctx.reloadTodos();
1301
1347
  this.ctx.ui.requestRender();
1302
1348
  return true;
1303
1349
  }
1304
1350
 
1305
1351
  async handleResumeSession(sessionPath: string): Promise<void> {
1352
+ const previousSessionId = this.ctx.sessionManager.getSessionId();
1306
1353
  this.#clearTransientSessionUi();
1307
1354
 
1308
1355
  // Switch session via AgentSession (emits hook and tool session events)
1309
- await this.ctx.session.switchSession(sessionPath);
1356
+ if (!(await this.ctx.session.switchSession(sessionPath))) return;
1357
+ const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
1358
+ if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
1310
1359
  this.#refreshSessionTerminalTitle();
1311
1360
  this.ctx.updateEditorBorderColor();
1312
1361
 
1313
- // Clear and re-render the chat
1314
- this.ctx.chatContainer.clear();
1315
- this.ctx.renderInitialMessages();
1362
+ this.ctx.rebuildInitialMessages(switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript");
1316
1363
  await this.ctx.reloadTodos();
1317
1364
  this.ctx.showStatus("Resumed session");
1318
1365
  }