@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
@@ -21,6 +21,7 @@ import {
21
21
  getRenderCacheRetainedBytes,
22
22
  Loader,
23
23
  Markdown,
24
+ onImageProtocolChanged,
24
25
  ProcessTerminal,
25
26
  Spacer,
26
27
  Text,
@@ -85,6 +86,8 @@ import type { EvalExecutionComponent } from "./components/eval-execution";
85
86
  import type { HookEditorComponent } from "./components/hook-editor";
86
87
  import type { HookInputComponent } from "./components/hook-input";
87
88
  import type { HookSelectorComponent } from "./components/hook-selector";
89
+ import { IrcSplitViewComponent } from "./components/irc-sidebar";
90
+ import { type PetMode, SayknowPetWidget } from "./components/sayknow-pet-widget";
88
91
  import { StatusLineComponent } from "./components/status-line";
89
92
  import type { ToolExecutionHandle } from "./components/tool-execution";
90
93
  import {
@@ -100,6 +103,7 @@ import { InputController } from "./controllers/input-controller";
100
103
  import { SelectorController } from "./controllers/selector-controller";
101
104
  import { SSHCommandController } from "./controllers/ssh-command-controller";
102
105
  import { TodoCommandController } from "./controllers/todo-command-controller";
106
+ import { IrcObservationLedger } from "./irc-observation-ledger";
103
107
  import { JobsObserver } from "./jobs-observer";
104
108
  import { OAuthManualInputManager } from "./oauth-manual-input";
105
109
  import { SessionObserverRegistry } from "./session-observer-registry";
@@ -115,13 +119,42 @@ import {
115
119
  onThemeChange,
116
120
  theme,
117
121
  } from "./theme/theme";
118
- import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInput, TodoItem, TodoPhase } from "./types";
119
- import { addChatChild, UiHelpers } from "./utils/ui-helpers";
122
+ import type {
123
+ CompactionQueuedMessage,
124
+ InteractiveModeContext,
125
+ IrcArrivalSnapshot,
126
+ SubmittedUserInput,
127
+ TodoItem,
128
+ TodoPhase,
129
+ TranscriptRebuildPolicy,
130
+ } from "./types";
131
+ import type { ParsedIrcMessage } from "./utils/irc-message";
132
+ import { addChatChild, prepareTranscriptRebuild, UiHelpers } from "./utils/ui-helpers";
120
133
 
121
134
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
122
135
  const COMPOSER_NEWLINE_HINT = process.platform === "win32" ? "Alt+Enter/Ctrl+J" : "Shift+Enter/Ctrl+J";
123
136
  export const DEFAULT_COMPOSER_PLACEHOLDER = `Type your message... ${COMPOSER_NEWLINE_HINT}: New line · Ctrl+C: Clear · Ctrl+R: Search history · Shift+Tab: Reasoning`;
124
137
  const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
138
+
139
+ const IRC_SIDEBAR_TOGGLE_SHADOWING_ACTIONS: readonly AppKeybinding[] = [
140
+ "app.plan.toggle",
141
+ "app.session.new",
142
+ "app.session.tree",
143
+ "app.session.fork",
144
+ "app.session.resume",
145
+ "app.message.followUp",
146
+ "app.stt.toggle",
147
+ "app.clipboard.copyLine",
148
+ "app.session.observe",
149
+ "app.jobs.open",
150
+ "app.tool.backgroundFold",
151
+ ];
152
+
153
+ export function getWelcomeTranscriptReservedRows(chatContainer: Container, width: number): number {
154
+ return chatContainer.children.length === 0 || chatContainer.children.length > WELCOME_RESERVED_CONTAINER_CHILD_LIMIT
155
+ ? 0
156
+ : chatContainer.render(width).length;
157
+ }
125
158
  const FRIENDLY_KEY_PARTS: Record<string, string> = {
126
159
  alt: "Alt",
127
160
  cmd: "Cmd",
@@ -281,6 +314,7 @@ export class InteractiveMode implements InteractiveModeContext {
281
314
  keybindings: KeybindingsManager;
282
315
  agent: Agent;
283
316
  historyStorage?: HistoryStorage;
317
+ readonly ircLedger = new IrcObservationLedger();
284
318
 
285
319
  ui: TUI;
286
320
  chatContainer: Container;
@@ -292,6 +326,8 @@ export class InteractiveMode implements InteractiveModeContext {
292
326
  editorContainer: Container;
293
327
  hookWidgetContainerAbove: Container;
294
328
  hookWidgetContainerBelow: Container;
329
+ petFloorContainer: Container = new Container();
330
+ petWidget: SayknowPetWidget | undefined;
295
331
  statusLine: StatusLineComponent;
296
332
 
297
333
  isInitialized = false;
@@ -353,6 +389,7 @@ export class InteractiveMode implements InteractiveModeContext {
353
389
  #baseReservedSlashCommandNames: Set<string> = new Set();
354
390
  #cleanupUnsubscribe?: () => void;
355
391
  #subprocessTeardownUnsubscribe?: () => void;
392
+ #petProtocolUnsubscribe?: () => void;
356
393
  readonly #version: string;
357
394
  readonly #changelogMarkdown: string | undefined;
358
395
  #planModePreviousTools: string[] | undefined;
@@ -388,6 +425,9 @@ export class InteractiveMode implements InteractiveModeContext {
388
425
  #eventBus?: EventBus;
389
426
  #eventBusUnsubscribers: Array<() => void> = [];
390
427
  #welcomeComponent?: WelcomeComponent;
428
+ #ircSplitView: IrcSplitViewComponent;
429
+ #ircSidebarAvailable = false;
430
+ #ircSidebarRequestedVisible = false;
391
431
 
392
432
  constructor(
393
433
  session: AgentSession,
@@ -424,6 +464,7 @@ export class InteractiveMode implements InteractiveModeContext {
424
464
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
425
465
  this.ui.setClearOnShrink(settings.get("clearOnShrink"));
426
466
  this.chatContainer = new Container();
467
+ this.#ircSplitView = new IrcSplitViewComponent(this.chatContainer, this.ircLedger, () => theme);
427
468
  this.pendingMessagesContainer = new Container();
428
469
  this.statusContainer = new Container();
429
470
  this.todoContainer = new Container();
@@ -443,6 +484,7 @@ export class InteractiveMode implements InteractiveModeContext {
443
484
  this.#syncEditorMaxHeight();
444
485
  this.updateEditorChrome();
445
486
  this.editor.invalidate();
487
+ this.#invalidateIrcSidebarRender();
446
488
  this.ui.requestResizeRender();
447
489
  };
448
490
  process.stdout.on("resize", this.#resizeHandler);
@@ -562,7 +604,9 @@ export class InteractiveMode implements InteractiveModeContext {
562
604
  this.#welcomeComponent.playIntro(() => this.ui.requestRender());
563
605
  }
564
606
 
565
- this.ui.addChild(this.chatContainer);
607
+ this.ui.addChild(this.#ircSplitView);
608
+ this.ui.setViewportAnchorComponent(this.#ircSplitView);
609
+
566
610
  this.ui.addChild(this.pendingMessagesContainer);
567
611
  this.ui.addChild(this.statusContainer);
568
612
  this.ui.addChild(this.todoContainer);
@@ -570,9 +614,24 @@ export class InteractiveMode implements InteractiveModeContext {
570
614
  this.ui.addChild(this.statusLine); // Main status rail + hook statuses; composer chrome is rendered by the editor.
571
615
  this.ui.addChild(this.hookWidgetContainerAbove);
572
616
  this.ui.addChild(this.editorContainer);
617
+ this.ui.addChild(this.petFloorContainer);
573
618
  this.ui.addChild(this.hookWidgetContainerBelow);
574
619
  this.ui.setBottomPinnedComponent(this.statusLine);
575
620
  this.ui.setFocus(this.editor);
621
+ this.petWidget = this.#createPetWidget(this.editor);
622
+ this.petWidget.setMode(settings.get("pet.mode"));
623
+ // The async sixel capability probe can enable graphics after the saved
624
+ // pet mode was applied and dropped (no protocol yet at startup).
625
+ // Re-apply the configured mode when capability arrives so the pet
626
+ // appears without the user re-running /pet.
627
+ this.#petProtocolUnsubscribe?.();
628
+ this.#petProtocolUnsubscribe = onImageProtocolChanged(protocol => {
629
+ if (!protocol) return;
630
+ const saved = settings.get("pet.mode");
631
+ if (saved !== "off" && this.petWidget && this.petWidget.mode === "off") {
632
+ this.petWidget.setMode(saved);
633
+ }
634
+ });
576
635
 
577
636
  this.#inputController.setupKeyHandlers();
578
637
  this.#inputController.setupEditorSubmitHandler();
@@ -610,6 +669,7 @@ export class InteractiveMode implements InteractiveModeContext {
610
669
  this.updateEditorChrome();
611
670
  this.#syncEditorMaxHeight();
612
671
  this.isInitialized = true;
672
+ this.#syncIrcSidebarAvailabilityFromSettings();
613
673
  this.ui.requestRender(true);
614
674
 
615
675
  // GitHub star reminder (interactive-only). Register the decline-driven
@@ -875,7 +935,7 @@ export class InteractiveMode implements InteractiveModeContext {
875
935
  }
876
936
  if (!submission.customType) {
877
937
  this.pendingImages = submission.images ? [...submission.images] : [];
878
- this.rebuildChatFromMessages();
938
+ this.rebuildChatFromMessages("reconcile-same-transcript");
879
939
  this.editor.setText(submission.text);
880
940
  }
881
941
  this.updateEditorChrome();
@@ -951,8 +1011,9 @@ export class InteractiveMode implements InteractiveModeContext {
951
1011
  }
952
1012
 
953
1013
  #getWelcomeReservedRows(width: number): number {
1014
+ const transcriptRows = getWelcomeTranscriptReservedRows(this.chatContainer, width);
1015
+
954
1016
  const transientRows = [
955
- this.chatContainer,
956
1017
  this.pendingMessagesContainer,
957
1018
  this.statusContainer,
958
1019
  this.todoContainer,
@@ -966,7 +1027,7 @@ export class InteractiveMode implements InteractiveModeContext {
966
1027
  this.hookWidgetContainerBelow,
967
1028
  ].reduce((rows, component) => rows + component.render(width).length, 0);
968
1029
 
969
- return transientRows + pinnedRows;
1030
+ return transcriptRows + transientRows + pinnedRows;
970
1031
  }
971
1032
 
972
1033
  #renderShortContainerRowsForWelcomeReservation(width: number, container: Container): number {
@@ -1018,7 +1079,42 @@ export class InteractiveMode implements InteractiveModeContext {
1018
1079
  this.editor.setTopBorder(undefined);
1019
1080
  }
1020
1081
 
1021
- rebuildChatFromMessages(): void {
1082
+ setPetMode(mode: PetMode): void {
1083
+ this.petWidget?.setMode(mode);
1084
+ settings.set("pet.mode", mode);
1085
+ this.ui.requestRender();
1086
+ }
1087
+
1088
+ previewPetMode(mode: PetMode): void {
1089
+ this.petWidget?.previewMode(mode);
1090
+ this.ui.requestRender();
1091
+ }
1092
+
1093
+ restoreComposer(): void {
1094
+ if (this.petWidget) {
1095
+ this.petWidget.remountComposer();
1096
+ } else {
1097
+ this.editorContainer.clear();
1098
+ this.editorContainer.addChild(this.editor);
1099
+ }
1100
+ this.ui.setFocus(this.editor);
1101
+ }
1102
+
1103
+ #createPetWidget(editor: CustomEditor): SayknowPetWidget {
1104
+ return new SayknowPetWidget({
1105
+ ui: this.ui,
1106
+ editor,
1107
+ editorContainer: this.editorContainer,
1108
+ floorContainer: this.petFloorContainer,
1109
+ isWorking: () => this.loadingAnimation !== undefined,
1110
+ getComposerBottomOffset: () =>
1111
+ this.petFloorContainer.render(this.ui.terminal.columns).length +
1112
+ this.hookWidgetContainerBelow.render(this.ui.terminal.columns).length,
1113
+ });
1114
+ }
1115
+
1116
+ rebuildChatFromMessages(policy: TranscriptRebuildPolicy): void {
1117
+ prepareTranscriptRebuild(this.ui, policy);
1022
1118
  this.chatContainer.clear();
1023
1119
  const context = this.session.buildDisplaySessionContext();
1024
1120
  this.renderSessionContext(context);
@@ -1993,6 +2089,10 @@ export class InteractiveMode implements InteractiveModeContext {
1993
2089
  }
1994
2090
 
1995
2091
  stop(): void {
2092
+ this.#petProtocolUnsubscribe?.();
2093
+ this.#petProtocolUnsubscribe = undefined;
2094
+ this.petWidget?.dispose();
2095
+ this.petWidget = undefined;
1996
2096
  if (this.loadingAnimation) {
1997
2097
  this.loadingAnimation.stop();
1998
2098
  this.loadingAnimation = undefined;
@@ -2124,11 +2224,17 @@ export class InteractiveMode implements InteractiveModeContext {
2124
2224
  nextEditor.setText(previousText);
2125
2225
  previousEditor.dispose();
2126
2226
 
2227
+ const petMode = this.petWidget?.mode ?? settings.get("pet.mode");
2228
+ this.petWidget?.dispose();
2229
+
2127
2230
  this.editorContainer.clear();
2128
2231
  this.editor = nextEditor;
2129
2232
  this.editorContainer.addChild(nextEditor);
2130
2233
  this.ui.setFocus(nextEditor);
2131
2234
 
2235
+ this.petWidget = this.#createPetWidget(nextEditor);
2236
+ this.petWidget.setMode(petMode);
2237
+
2132
2238
  this.#inputController.setupKeyHandlers();
2133
2239
  this.#inputController.setupEditorSubmitHandler();
2134
2240
 
@@ -2285,13 +2391,32 @@ export class InteractiveMode implements InteractiveModeContext {
2285
2391
  return this.#uiHelpers.addMessageToChat(message, options);
2286
2392
  }
2287
2393
 
2394
+ addLiveIrcObservationToChat(message: ParsedIrcMessage, arrival: IrcArrivalSnapshot): Component[] {
2395
+ return this.#uiHelpers.addLiveIrcObservationToChat(message, arrival);
2396
+ }
2397
+ removeRenderedIrcInlineComponents(observationId: string): readonly Component[] | undefined {
2398
+ return this.#uiHelpers.removeRenderedIrcInlineComponents(observationId);
2399
+ }
2400
+ resetRenderedIrcInlineComponents(): readonly (readonly Component[])[] {
2401
+ return this.#uiHelpers.resetRenderedIrcInlineComponents();
2402
+ }
2403
+
2288
2404
  renderSessionContext(
2289
2405
  sessionContext: SessionContext,
2290
2406
  options?: { updateFooter?: boolean; populateHistory?: boolean },
2291
2407
  ): void {
2292
2408
  this.#uiHelpers.renderSessionContext(sessionContext, options);
2409
+ this.#eventController.reconcileIrcExpiryTimers(this.#uiHelpers.getRenderedIrcInlineComponents());
2293
2410
  }
2294
2411
 
2412
+ rebuildInitialMessages(
2413
+ policy: TranscriptRebuildPolicy,
2414
+ prebuiltContext?: SessionContext,
2415
+ options?: { preserveExistingChat?: boolean },
2416
+ ): void {
2417
+ prepareTranscriptRebuild(this.ui, policy);
2418
+ this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
2419
+ }
2295
2420
  renderInitialMessages(prebuiltContext?: SessionContext, options?: { preserveExistingChat?: boolean }): void {
2296
2421
  this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
2297
2422
  }
@@ -2304,6 +2429,10 @@ export class InteractiveMode implements InteractiveModeContext {
2304
2429
  return this.#uiHelpers.findLastAssistantMessage();
2305
2430
  }
2306
2431
 
2432
+ getAssistantViewportAnchorId(message: AssistantMessage): string {
2433
+ return this.#uiHelpers.assistantViewportAnchorId(message);
2434
+ }
2435
+
2307
2436
  extractAssistantText(message: AssistantMessage): string {
2308
2437
  return this.#uiHelpers.extractAssistantText(message);
2309
2438
  }
@@ -2554,6 +2683,10 @@ export class InteractiveMode implements InteractiveModeContext {
2554
2683
  this.#selectorController.showThemeSelector();
2555
2684
  }
2556
2685
 
2686
+ showPetSelector(): void {
2687
+ this.#selectorController.showPetSelector();
2688
+ }
2689
+
2557
2690
  showHistorySearch(): void {
2558
2691
  this.#selectorController.showHistorySearch();
2559
2692
  }
@@ -2669,6 +2802,65 @@ export class InteractiveMode implements InteractiveModeContext {
2669
2802
  this.#inputController.setToolsExpanded(expanded);
2670
2803
  }
2671
2804
 
2805
+ #resolveEffectiveIrcSidebarToggleKey(): string | null {
2806
+ for (const key of this.keybindings.getKeys("app.irc.sidebar.toggle")) {
2807
+ if (this.editor.hasActionKey(key)) continue;
2808
+ const shadowed = IRC_SIDEBAR_TOGGLE_SHADOWING_ACTIONS.some(action =>
2809
+ this.keybindings.getKeys(action).includes(key),
2810
+ );
2811
+ if (!shadowed) return key;
2812
+ }
2813
+ return null;
2814
+ }
2815
+
2816
+ captureIrcArrivalSnapshot(): IrcArrivalSnapshot {
2817
+ return {
2818
+ panelVisible: this.#ircSplitView.effectiveSidebarVisible(this.ui.terminal.columns),
2819
+ panelRequestedVisible: this.#ircSidebarRequestedVisible,
2820
+ sidebarAvailable: this.#ircSidebarAvailable,
2821
+ resolvedToggleKey: this.#resolveEffectiveIrcSidebarToggleKey(),
2822
+ };
2823
+ }
2824
+ toggleIrcSidebar(): void {
2825
+ if (
2826
+ !this.#ircSidebarAvailable ||
2827
+ this.settings.get("irc.enabled") !== true ||
2828
+ this.settings.get("irc.sidebar.enabled") !== true
2829
+ )
2830
+ return;
2831
+ this.#ircSidebarRequestedVisible = !this.#ircSidebarRequestedVisible;
2832
+ this.#ircSplitView.setVisible(this.#ircSidebarRequestedVisible);
2833
+ this.#invalidateIrcSidebarRender();
2834
+ this.ui.requestRender();
2835
+ }
2836
+
2837
+ applyIrcSidebarAvailability(enabled: boolean): void {
2838
+ this.#ircSidebarAvailable = enabled;
2839
+ this.#ircSplitView.setVisible(enabled && this.#ircSidebarRequestedVisible);
2840
+ this.#invalidateIrcSidebarRender();
2841
+ this.ui.requestRender();
2842
+ }
2843
+
2844
+ #syncIrcSidebarAvailabilityFromSettings(): void {
2845
+ this.applyIrcSidebarAvailability(
2846
+ this.settings.get("irc.enabled") === true && this.settings.get("irc.sidebar.enabled") === true,
2847
+ );
2848
+ }
2849
+
2850
+ resetIrcSidebarSession(): void {
2851
+ this.ircLedger.reset();
2852
+ this.#eventController.resetIrcObservations();
2853
+ this.#ircSidebarRequestedVisible = false;
2854
+ this.#ircSplitView.setVisible(false);
2855
+ this.#uiHelpers.resetIrcSidebarHint();
2856
+ this.#syncIrcSidebarAvailabilityFromSettings();
2857
+ }
2858
+
2859
+ #invalidateIrcSidebarRender(): void {
2860
+ clearRenderCache();
2861
+ this.#ircSplitView.invalidate();
2862
+ }
2863
+
2672
2864
  toggleThinkingBlockVisibility(): void {
2673
2865
  this.#inputController.toggleThinkingBlockVisibility();
2674
2866
  }
@@ -0,0 +1,127 @@
1
+ import * as crypto from "node:crypto";
2
+
3
+ import type { ParsedIrcMessage } from "./utils/irc-message";
4
+
5
+ type InlineMode = "persistent" | "ephemeral";
6
+
7
+ const MAX_RECORDS = 10_000;
8
+ const MAX_RETAINED_UTF8_BYTES = 16 * 1024 * 1024;
9
+ const MAX_SEEN_IDENTITIES = 100_000;
10
+
11
+ export type IrcObservationRecord = Readonly<
12
+ ParsedIrcMessage & {
13
+ mode: InlineMode;
14
+ observedAt: number;
15
+ sequence: number;
16
+ expiresAt?: number;
17
+ retainedUtf8Bytes: number;
18
+ }
19
+ >;
20
+
21
+ function measureRetainedUtf8Bytes(message: ParsedIrcMessage): number {
22
+ let bytes = 4;
23
+ for (const value of [message.observationId, message.from, message.to, message.text, message.kind]) {
24
+ bytes += Buffer.byteLength(value, "utf8");
25
+ if (bytes > MAX_RETAINED_UTF8_BYTES) return bytes;
26
+ }
27
+ return bytes;
28
+ }
29
+
30
+ function tombstoneIdentity(observationId: string): string {
31
+ const hash = crypto.createHash("sha256");
32
+ hash.update("skc:irc:observation-id:utf16le:v1\0");
33
+ for (let offset = 0; offset < observationId.length; offset += 4_096) {
34
+ hash.update(Buffer.from(observationId.slice(offset, offset + 4_096), "utf16le"));
35
+ }
36
+ return hash.digest("hex");
37
+ }
38
+
39
+ /** Runtime-only IRC observations. This intentionally has no persistence layer. */
40
+ export class IrcObservationLedger {
41
+ #records = new Map<string, IrcObservationRecord>();
42
+ #retainedUtf8Bytes = 0;
43
+ #nextSequence = 0;
44
+ #seenObservationIdentities = new Set<string>();
45
+ #identityCapacityExhausted = false;
46
+ #evictedObservationIds = new Set<string>();
47
+
48
+ #rememberObservationIdentity(observationId: string): boolean {
49
+ const identity = tombstoneIdentity(observationId);
50
+ if (this.#seenObservationIdentities.has(identity) || this.#identityCapacityExhausted) return false;
51
+ if (this.#seenObservationIdentities.size >= MAX_SEEN_IDENTITIES) {
52
+ this.#identityCapacityExhausted = true;
53
+ return false;
54
+ }
55
+ this.#seenObservationIdentities.add(identity);
56
+ return true;
57
+ }
58
+
59
+ #evict(observationId: string): void {
60
+ const record = this.#records.get(observationId);
61
+ if (!record) return;
62
+ this.#records.delete(observationId);
63
+ this.#retainedUtf8Bytes -= record.retainedUtf8Bytes;
64
+ this.#evictedObservationIds.add(observationId);
65
+ }
66
+
67
+ #enforceBounds(): void {
68
+ while (this.#records.size > MAX_RECORDS || this.#retainedUtf8Bytes > MAX_RETAINED_UTF8_BYTES) {
69
+ const oldestObservationId = this.#records.keys().next().value;
70
+ if (oldestObservationId === undefined) return;
71
+ this.#evict(oldestObservationId);
72
+ }
73
+ }
74
+
75
+ /** Inline observations expire after 10 seconds; closed-panel observations persist inline. */
76
+ observe(message: ParsedIrcMessage, panelVisibleAtObservation: boolean): IrcObservationRecord | undefined {
77
+ const existing = this.#records.get(message.observationId);
78
+ if (existing) return existing;
79
+ if (!this.#rememberObservationIdentity(message.observationId)) return undefined;
80
+
81
+ const estimatedBytes = measureRetainedUtf8Bytes(message);
82
+ if (estimatedBytes > MAX_RETAINED_UTF8_BYTES) return undefined;
83
+
84
+ const observedAt = Date.now();
85
+ const mode: InlineMode = panelVisibleAtObservation ? "ephemeral" : "persistent";
86
+ const record: IrcObservationRecord = Object.freeze({
87
+ ...message,
88
+ mode,
89
+ observedAt,
90
+ sequence: this.#nextSequence++,
91
+ ...(mode === "ephemeral" ? { expiresAt: observedAt + 10_000 } : {}),
92
+ retainedUtf8Bytes: estimatedBytes,
93
+ });
94
+ this.#records.set(record.observationId, record);
95
+ this.#retainedUtf8Bytes += estimatedBytes;
96
+ this.#enforceBounds();
97
+ return this.#records.get(record.observationId);
98
+ }
99
+
100
+ getRecord(observationId: string): IrcObservationRecord | undefined {
101
+ return this.#records.get(observationId);
102
+ }
103
+
104
+ getSidebarRecords(): readonly IrcObservationRecord[] {
105
+ return [...this.#records.values()];
106
+ }
107
+
108
+ getInlineProjection(now: number): readonly IrcObservationRecord[] {
109
+ return [...this.#records.values()].filter(record => record.mode === "persistent" || now < record.expiresAt!);
110
+ }
111
+
112
+ /** Returns and clears IDs whose retained payload was released. */
113
+ drainEvictedObservationIds(): readonly string[] {
114
+ const observationIds = [...this.#evictedObservationIds];
115
+ this.#evictedObservationIds.clear();
116
+ return observationIds;
117
+ }
118
+
119
+ reset(): void {
120
+ for (const observationId of this.#records.keys()) {
121
+ this.#evictedObservationIds.add(observationId);
122
+ }
123
+ this.#records.clear();
124
+ this.#retainedUtf8Bytes = 0;
125
+ this.#nextSequence = 0;
126
+ }
127
+ }