@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
@@ -5,19 +5,46 @@ import { AcpAgent } from "./acp-agent";
5
5
 
6
6
  export type AcpSessionFactory = (cwd: string) => Promise<AgentSession>;
7
7
 
8
+ export interface AcpConnectionHandle {
9
+ connection: AgentSideConnection;
10
+ agent: AcpAgent;
11
+ }
12
+
8
13
  export function createAcpConnection(
9
14
  transport: Stream,
10
15
  createSession: AcpSessionFactory,
11
16
  initialSession?: AgentSession,
12
17
  ): AgentSideConnection {
13
- return new AgentSideConnection(conn => new AcpAgent(conn, createSession, initialSession), transport);
18
+ return createAcpConnectionWithAgent(transport, createSession, initialSession).connection;
19
+ }
20
+
21
+ /**
22
+ * Create an ACP connection and return both the {@link AgentSideConnection} and the
23
+ * underlying {@link AcpAgent}. Callers that need to await agent shutdown (terminal
24
+ * delete/dispose work) after transport closure should use this instead of
25
+ * {@link createAcpConnection}.
26
+ */
27
+ export function createAcpConnectionWithAgent(
28
+ transport: Stream,
29
+ createSession: AcpSessionFactory,
30
+ initialSession?: AgentSession,
31
+ ): AcpConnectionHandle {
32
+ let agent: AcpAgent | undefined;
33
+ const connection = new AgentSideConnection(conn => {
34
+ agent = new AcpAgent(conn, createSession, initialSession);
35
+ return agent;
36
+ }, transport);
37
+ return { connection, agent: agent! };
14
38
  }
15
39
 
16
40
  export async function runAcpMode(createSession: AcpSessionFactory, initialSession?: AgentSession): Promise<never> {
17
41
  const input = stream.Writable.toWeb(process.stdout);
18
42
  const output = stream.Readable.toWeb(process.stdin);
19
43
  const transport = ndJsonStream(input, output);
20
- const connection = createAcpConnection(transport, createSession, initialSession);
44
+ const { connection, agent } = createAcpConnectionWithAgent(transport, createSession, initialSession);
21
45
  await connection.closed;
46
+ // Await connection-local terminal work (active deletes, disposal) so
47
+ // `process.exit` never kills in-flight deletion/cleanup.
48
+ await agent.shutdownPromise;
22
49
  process.exit(0);
23
50
  }
@@ -1,5 +1,16 @@
1
1
  import type { AssistantMessage, ImageContent, Usage } from "@sayknow-cli/ai";
2
- import { type Component, Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@sayknow-cli/tui";
2
+ import {
3
+ type Component,
4
+ Container,
5
+ Image,
6
+ ImageProtocol,
7
+ isViewportAnchorSourceRenderer,
8
+ Markdown,
9
+ Spacer,
10
+ TERMINAL,
11
+ Text,
12
+ type ViewportAnchorSource,
13
+ } from "@sayknow-cli/tui";
3
14
  import { formatNumber } from "@sayknow-cli/utils";
4
15
  import { settings } from "../../config/settings";
5
16
  import { renderDeepInterviewAssistantText } from "../../deep-interview/render-middleware";
@@ -62,6 +73,7 @@ interface AssistantMessageUpdateOptions {
62
73
  type AssistantChildDescriptor = {
63
74
  key: string;
64
75
  component: Component;
76
+ anchorSource?: ViewportAnchorSource;
65
77
  };
66
78
 
67
79
  /**
@@ -81,15 +93,16 @@ export class AssistantMessageComponent extends Container {
81
93
  #contentBlockKeys = new WeakMap<object, string>();
82
94
  #nextContentBlockKey = 0;
83
95
  #reusableChildren = new WeakSet<Component>();
84
-
96
+ #viewportAnchorId?: string;
85
97
  constructor(
86
98
  message?: AssistantMessage,
87
99
  private hideThinkingBlock = false,
88
100
  private readonly onImageUpdate?: () => void,
101
+ viewportAnchorId?: string,
89
102
  ) {
90
103
  super();
104
+ this.#viewportAnchorId = viewportAnchorId;
91
105
 
92
- // Container for text/thinking content
93
106
  this.#contentContainer = new Container();
94
107
  this.addChild(this.#contentContainer);
95
108
 
@@ -98,6 +111,13 @@ export class AssistantMessageComponent extends Container {
98
111
  }
99
112
  }
100
113
 
114
+ #contentAnchorSource(
115
+ index: number,
116
+ kind: "text" | "thinking" | "thinking-hidden",
117
+ ): ViewportAnchorSource | undefined {
118
+ return this.#viewportAnchorId ? { id: `${this.#viewportAnchorId}:content:${index}:${kind}` } : undefined;
119
+ }
120
+
101
121
  override invalidate(): void {
102
122
  super.invalidate();
103
123
  if (this.#lastMessage) {
@@ -288,6 +308,12 @@ export class AssistantMessageComponent extends Container {
288
308
  }
289
309
 
290
310
  #reconcileChildren(descriptors: AssistantChildDescriptor[]): void {
311
+ for (const child of this.#contentContainer.children) this.#contentContainer.setViewportAnchorSource(child, null);
312
+ for (const descriptor of descriptors) {
313
+ if (descriptor.anchorSource && isViewportAnchorSourceRenderer(descriptor.component)) {
314
+ this.#contentContainer.setViewportAnchorSource(descriptor.component, descriptor.anchorSource);
315
+ }
316
+ }
291
317
  const nextChildren = descriptors.map(descriptor => descriptor.component);
292
318
  if (
293
319
  nextChildren.length === this.#contentContainer.children.length &&
@@ -346,6 +372,7 @@ export class AssistantMessageComponent extends Container {
346
372
  descriptors.push({
347
373
  key: `${blockKey}:text`,
348
374
  component: this.#renderTextBlock(content, streaming && i === activeContentIndex),
375
+ anchorSource: this.#contentAnchorSource(i, "text"),
349
376
  });
350
377
  } else if (content.type === "thinking" && content.thinking.trim()) {
351
378
  // Add spacing only when another visible assistant content block follows.
@@ -358,11 +385,13 @@ export class AssistantMessageComponent extends Container {
358
385
  `${blockKey}:thinking-hidden`,
359
386
  () => new Text(theme.italic(theme.fg("thinkingText", "Thinking...")), 1, 0),
360
387
  ),
388
+ anchorSource: this.#contentAnchorSource(i, "thinking-hidden"),
361
389
  });
362
390
  } else {
363
391
  descriptors.push({
364
392
  key: `${blockKey}:thinking`,
365
393
  component: this.#renderThinkingBlock(content, streaming && i === activeContentIndex),
394
+ anchorSource: this.#contentAnchorSource(i, "thinking"),
366
395
  });
367
396
  }
368
397
  if (visibleContentAfter[i]) {
@@ -6,6 +6,7 @@ import {
6
6
  Container,
7
7
  Ellipsis,
8
8
  ImageProtocol,
9
+ isTerminalGraphicsFallbackActive,
9
10
  type Loader,
10
11
  TERMINAL,
11
12
  Text,
@@ -16,7 +17,12 @@ import {
16
17
  import { sanitizeText } from "@sayknow-cli/utils";
17
18
  import { theme } from "../../modes/theme/theme";
18
19
  import type { TruncationMeta } from "../../tools/output-meta";
19
- import { getSixelLineMask, isSixelPassthroughEnabled, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
20
+ import {
21
+ containsSixelSequence,
22
+ getSixelLineMask,
23
+ isSixelPassthroughEnabled,
24
+ sanitizeWithOptionalSixelPassthrough,
25
+ } from "../../utils/sixel";
20
26
  import {
21
27
  buildExecutionFrame,
22
28
  buildStatusFooter,
@@ -41,6 +47,7 @@ export class BashExecutionComponent extends Container {
41
47
  #truncation?: TruncationMeta;
42
48
  #expanded = false;
43
49
  #displayDirty = false;
50
+ #displayBuiltWithGraphicsFallback: boolean | undefined;
44
51
  #chunkGate = false;
45
52
  #contentContainer: Container;
46
53
  #headerText: Text;
@@ -135,7 +142,8 @@ export class BashExecutionComponent extends Container {
135
142
  }
136
143
 
137
144
  override render(width: number): string[] {
138
- if (this.#displayDirty) {
145
+ const fallbackActive = isTerminalGraphicsFallbackActive();
146
+ if (this.#displayDirty || this.#displayBuiltWithGraphicsFallback !== fallbackActive) {
139
147
  this.#displayDirty = false;
140
148
  this.#updateDisplay();
141
149
  }
@@ -143,16 +151,16 @@ export class BashExecutionComponent extends Container {
143
151
  }
144
152
 
145
153
  #updateDisplay(): void {
154
+ const fallbackActive = isTerminalGraphicsFallbackActive();
146
155
  const availableLines = this.#outputLines;
147
-
148
- // Apply preview truncation based on expanded state
149
- const previewLogicalLines = availableLines.slice(-PREVIEW_LINES);
150
- const hiddenLineCount = availableLines.length - previewLogicalLines.length;
151
156
  const sixelLineMask =
152
- TERMINAL.imageProtocol === ImageProtocol.Sixel && isSixelPassthroughEnabled()
157
+ fallbackActive || (TERMINAL.imageProtocol === ImageProtocol.Sixel && isSixelPassthroughEnabled())
153
158
  ? getSixelLineMask(availableLines)
154
159
  : undefined;
155
160
  const hasSixelOutput = sixelLineMask?.some(Boolean) ?? false;
161
+ const selectedLines =
162
+ this.#expanded || (hasSixelOutput && !fallbackActive) ? availableLines : availableLines.slice(-PREVIEW_LINES);
163
+ const hiddenLineCount = availableLines.length - selectedLines.length;
156
164
 
157
165
  // Rebuild content container
158
166
  // Detach (not dispose): #headerText and the running #loader are persistent,
@@ -165,15 +173,36 @@ export class BashExecutionComponent extends Container {
165
173
  this.#contentContainer.addChild(this.#headerText);
166
174
 
167
175
  // Output
168
- if (availableLines.length > 0) {
169
- if (this.#expanded || hasSixelOutput) {
170
- const displayText = availableLines
171
- .map((line, index) => (sixelLineMask?.[index] ? line : theme.fg("muted", line)))
176
+ if (selectedLines.length > 0) {
177
+ if (fallbackActive && hasSixelOutput) {
178
+ const displayLines: string[] = [];
179
+ const selectedStart = availableLines.length - selectedLines.length;
180
+ for (const [selectedIndex, line] of selectedLines.entries()) {
181
+ const index = selectedStart + selectedIndex;
182
+ if (sixelLineMask?.[index]) {
183
+ // Emit one placeholder per visible SIXEL block: at a true sequence
184
+ // start, at the top of a collapsed slice that begins mid-sequence,
185
+ // or when the line itself opens a new sequence.
186
+ const isBlockStart = selectedIndex === 0 || !sixelLineMask[index - 1] || containsSixelSequence(line);
187
+ if (isBlockStart) {
188
+ displayLines.push(theme.fg("muted", "[SIXEL image hidden while IRC sidebar is visible]"));
189
+ }
190
+ } else {
191
+ displayLines.push(theme.fg("muted", line));
192
+ }
193
+ }
194
+ this.#contentContainer.addChild(new Text(`\n${displayLines.join("\n")}`, 1, 0));
195
+ } else if (this.#expanded || hasSixelOutput) {
196
+ const displayText = selectedLines
197
+ .map((line, selectedIndex) => {
198
+ const index = availableLines.length - selectedLines.length + selectedIndex;
199
+ return sixelLineMask?.[index] ? line : theme.fg("muted", line);
200
+ })
172
201
  .join("\n");
173
202
  this.#contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
174
203
  } else {
175
204
  // Use shared visual truncation utility, recomputed per render width
176
- const styledOutput = previewLogicalLines.map(line => theme.fg("muted", line)).join("\n");
205
+ const styledOutput = selectedLines.map(line => theme.fg("muted", line)).join("\n");
177
206
  this.#contentContainer.addChild(createCollapsedPreview(`\n${styledOutput}`, PREVIEW_LINES));
178
207
  }
179
208
  }
@@ -187,10 +216,11 @@ export class BashExecutionComponent extends Container {
187
216
  exitCode: this.#exitCode,
188
217
  truncation: this.#truncation,
189
218
  hiddenLineCount,
190
- suppressHiddenCount: hasSixelOutput,
219
+ suppressHiddenCount: hasSixelOutput && !fallbackActive,
191
220
  });
192
221
  if (footer) this.#contentContainer.addChild(footer);
193
222
  }
223
+ this.#displayBuiltWithGraphicsFallback = fallbackActive;
194
224
  }
195
225
 
196
226
  #clampDisplayLine(line: string): string {
@@ -123,6 +123,11 @@ export class CustomEditor extends Editor {
123
123
  this.#actionKeys.set(action, [...keys]);
124
124
  }
125
125
 
126
+ /** Returns whether a built-in editor action consumes this configured key before custom handlers. */
127
+ hasActionKey(key: KeyId): boolean {
128
+ return [...this.#actionKeys.values()].some(keys => keys.includes(key));
129
+ }
130
+
126
131
  #matchesAction(data: string, action: ConfigurableEditorAction): boolean {
127
132
  const keys = this.#actionKeys.get(action);
128
133
  if (!keys) return false;
@@ -139,8 +139,8 @@ export class FooterComponent implements Component {
139
139
  // After compaction, tokens are unknown until the next LLM response.
140
140
  const contextUsage = this.session.getContextUsage();
141
141
  const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
142
- const contextPercentValue = contextUsage?.percent ?? 0;
143
- const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
142
+ const contextPercentValue = contextUsage?.percent;
143
+ const contextPercent = typeof contextPercentValue === "number" ? contextPercentValue.toFixed(1) : "?";
144
144
 
145
145
  // Replace home directory with ~
146
146
  let pwd = shortenPath(getProjectDir());
@@ -188,7 +188,7 @@ export class FooterComponent implements Component {
188
188
  contextPercent === "?"
189
189
  ? `?/${formatNumber(contextWindow)}${autoIndicator}`
190
190
  : `${contextPercent}%/${formatNumber(contextWindow)}${autoIndicator}`;
191
- if (contextUsage?.percent !== null && contextUsage?.percent !== undefined) {
191
+ if (typeof contextPercentValue === "number") {
192
192
  const color = getContextUsageThemeColor(getContextUsageLevel(contextPercentValue, contextWindow));
193
193
  contextPercentStr =
194
194
  color === "statusLineContext" ? contextPercentDisplay : theme.fg(color, contextPercentDisplay);
@@ -70,6 +70,11 @@ export interface HookSelectorOptions {
70
70
  * completion behavior as the main prompt editor.
71
71
  */
72
72
  autocompleteProvider?: AutocompleteProvider;
73
+ /**
74
+ * Maps raw single-key accelerators to option labels. Unset maps preserve
75
+ * the legacy selector key handling exactly.
76
+ */
77
+ acceleratorMap?: Readonly<Record<string, string>>;
73
78
  }
74
79
 
75
80
  class OutlinedList extends Container {
@@ -352,6 +357,7 @@ export class HookSelectorComponent extends Container {
352
357
  #baseHelpText: string;
353
358
  #tui: TUI | undefined;
354
359
  #autocompleteProvider: AutocompleteProvider | undefined;
360
+ #acceleratorMap: Readonly<Record<string, string>> | undefined;
355
361
  constructor(
356
362
  title: string,
357
363
  options: string[],
@@ -376,6 +382,7 @@ export class HookSelectorComponent extends Container {
376
382
  this.#clarificationInput = opts?.clarificationInput;
377
383
  this.#tui = opts?.tui;
378
384
  this.#autocompleteProvider = opts?.autocompleteProvider;
385
+ this.#acceleratorMap = opts?.acceleratorMap;
379
386
 
380
387
  this.addChild(new DynamicBorder());
381
388
  this.addChild(new Spacer(1));
@@ -503,6 +510,11 @@ export class HookSelectorComponent extends Container {
503
510
  this.#handleInputModeKey(keyData, this.#inlineEditor);
504
511
  return;
505
512
  }
513
+ const acceleratedOption = this.#acceleratorMap?.[keyData.toLowerCase()];
514
+ if (acceleratedOption && this.#options.includes(acceleratedOption)) {
515
+ this.#onSelectCallback(acceleratedOption);
516
+ return;
517
+ }
506
518
  if (matchesKey(keyData, "up") || keyData === "k") {
507
519
  this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
508
520
  this.#updateList();
@@ -0,0 +1,206 @@
1
+ import {
2
+ type Component,
3
+ padding,
4
+ renderComponentWithViewportAnchors,
5
+ TERMINAL,
6
+ truncateToWidth,
7
+ type ViewportAnchorProvider,
8
+ type ViewportAnchorRender,
9
+ visibleWidth,
10
+ withTerminalGraphicsFallback,
11
+ wrapTextWithAnsi,
12
+ } from "@sayknow-cli/tui";
13
+ import type { IrcObservationLedger, IrcObservationRecord } from "../irc-observation-ledger";
14
+ import { formatIrcMessageBlock, projectIrcText } from "../utils/irc-message";
15
+
16
+ export const IRC_SIDEBAR_WIDTH_RATIO = 0.3;
17
+ const IRC_SIDEBAR_MIN_WIDTH = 30;
18
+ const IRC_SEPARATOR_WIDTH = 3;
19
+ export const IRC_SIDEBAR_MAX_RENDER_ROWS = 2_048;
20
+ const IRC_SIDEBAR_MAX_RENDER_SOURCE_UTF8_BYTES = 64 * 1_024;
21
+ const IRC_SIDEBAR_OLDER_MESSAGES_ELISION = "… older IRC messages elided …";
22
+ const IRC_SIDEBAR_MESSAGE_ELISION = " … message elided …";
23
+
24
+ /** Computes transcript/sidebar widths while preserving at least half the terminal for the transcript. */
25
+ export function computeIrcSplitWidths(width: number): {
26
+ leftWidth: number;
27
+ separatorWidth: number;
28
+ rightWidth: number;
29
+ } {
30
+ const normalizedWidth = Math.max(0, Math.floor(width));
31
+ const transcriptFloor = Math.floor(normalizedWidth * 0.5);
32
+ const preferredSidebar = Math.max(IRC_SIDEBAR_MIN_WIDTH, Math.floor(normalizedWidth * IRC_SIDEBAR_WIDTH_RATIO));
33
+ const sidebarWidth = Math.max(
34
+ 0,
35
+ Math.min(preferredSidebar, normalizedWidth - IRC_SEPARATOR_WIDTH - transcriptFloor),
36
+ );
37
+
38
+ // The sidebar yields entirely rather than rendering below its readable minimum.
39
+ if (sidebarWidth < IRC_SIDEBAR_MIN_WIDTH) {
40
+ return { leftWidth: normalizedWidth, separatorWidth: 0, rightWidth: 0 };
41
+ }
42
+
43
+ return {
44
+ leftWidth: normalizedWidth - IRC_SEPARATOR_WIDTH - sidebarWidth,
45
+ separatorWidth: IRC_SEPARATOR_WIDTH,
46
+ rightWidth: sidebarWidth,
47
+ };
48
+ }
49
+
50
+ function styleSender(componentTheme: IrcSidebarTheme, sender: string): string {
51
+ return componentTheme.fg("accent", componentTheme.bold(sender));
52
+ }
53
+
54
+ function renderSidebarRecord(
55
+ record: IrcObservationRecord,
56
+ width: number,
57
+ componentTheme: IrcSidebarTheme,
58
+ maxRows: number,
59
+ maxSourceUtf8Bytes: number,
60
+ ): { lines: string[]; sourceUtf8Bytes: number; truncated: boolean } {
61
+ const bodyWidth = Math.max(1, width - 2);
62
+ const sourceLimit = Math.min(maxSourceUtf8Bytes, Math.max(0, maxRows - 1) * bodyWidth);
63
+ const clipped = projectIrcText(record.text, sourceLimit);
64
+ const block = formatIrcMessageBlock({ ...record, text: clipped.text });
65
+ const sender = styleSender(componentTheme, block.sender);
66
+ const time = componentTheme.fg("dim", block.time);
67
+ const lines = [truncateToWidth(`${sender} → ${block.recipient} · ${time}`, width)];
68
+ let truncated = clipped.truncated;
69
+
70
+ body: for (const bodyLine of block.bodyLines) {
71
+ for (const wrappedLine of wrapTextWithAnsi(bodyLine, bodyWidth)) {
72
+ if (lines.length >= maxRows) {
73
+ truncated = true;
74
+ break body;
75
+ }
76
+ lines.push(truncateToWidth(` ${wrappedLine}`, width));
77
+ }
78
+ }
79
+ if (truncated && maxRows > 1) {
80
+ const marker = componentTheme.fg("dim", IRC_SIDEBAR_MESSAGE_ELISION);
81
+ if (lines.length >= maxRows) lines[maxRows - 1] = truncateToWidth(marker, width);
82
+ else lines.push(truncateToWidth(marker, width));
83
+ }
84
+ return { lines, sourceUtf8Bytes: clipped.utf8Bytes, truncated };
85
+ }
86
+
87
+ function renderSidebarRecords(ledger: IrcObservationLedger, width: number, componentTheme: IrcSidebarTheme): string[] {
88
+ if (width <= 0) return [];
89
+
90
+ const records = ledger.getSidebarRecords();
91
+ const newestFirstBlocks: string[][] = [];
92
+ let renderedRows = 0;
93
+ let renderedSourceUtf8Bytes = 0;
94
+ let omittedOlderRecords = false;
95
+ for (let index = records.length - 1; index >= 0; index--) {
96
+ const separatorRows = newestFirstBlocks.length > 0 ? 1 : 0;
97
+ const availableRows = IRC_SIDEBAR_MAX_RENDER_ROWS - renderedRows - separatorRows - 2;
98
+ const availableSourceUtf8Bytes = IRC_SIDEBAR_MAX_RENDER_SOURCE_UTF8_BYTES - renderedSourceUtf8Bytes;
99
+ const minimumRecordRows = records[index].text.length > 0 ? 2 : 1;
100
+ if (availableRows < minimumRecordRows || (availableSourceUtf8Bytes <= 0 && records[index].text.length > 0)) {
101
+ omittedOlderRecords = true;
102
+ break;
103
+ }
104
+
105
+ const rendered = renderSidebarRecord(
106
+ records[index],
107
+ width,
108
+ componentTheme,
109
+ availableRows,
110
+ availableSourceUtf8Bytes,
111
+ );
112
+ newestFirstBlocks.push(rendered.lines);
113
+ renderedRows += separatorRows + rendered.lines.length;
114
+ renderedSourceUtf8Bytes += rendered.sourceUtf8Bytes;
115
+ if (rendered.truncated) {
116
+ omittedOlderRecords = index > 0;
117
+ break;
118
+ }
119
+ }
120
+
121
+ const lines: string[] = [];
122
+ if (omittedOlderRecords) {
123
+ lines.push(truncateToWidth(componentTheme.fg("dim", IRC_SIDEBAR_OLDER_MESSAGES_ELISION), width));
124
+ }
125
+ for (const block of newestFirstBlocks.reverse()) {
126
+ if (lines.length > 0) lines.push("");
127
+ lines.push(...block);
128
+ }
129
+ return lines.slice(0, IRC_SIDEBAR_MAX_RENDER_ROWS);
130
+ }
131
+
132
+ export interface IrcSidebarTheme {
133
+ fg(color: "dim" | "accent", text: string): string;
134
+ bold(text: string): string;
135
+ readonly boxSharp: { readonly vertical: string };
136
+ }
137
+
138
+ export type IrcSidebarThemeSource = IrcSidebarTheme | (() => IrcSidebarTheme);
139
+
140
+ /** Read-only IRC history alongside the active transcript. */
141
+ export class IrcSplitViewComponent implements ViewportAnchorProvider {
142
+ #visible = false;
143
+
144
+ constructor(
145
+ private readonly leftPane: Component,
146
+ private readonly ledger: IrcObservationLedger,
147
+ private readonly componentTheme: IrcSidebarThemeSource,
148
+ ) {}
149
+
150
+ get visible(): boolean {
151
+ return this.#visible;
152
+ }
153
+
154
+ effectiveSidebarVisible(width = process.stdout.columns ?? 0): boolean {
155
+ return this.#visible && computeIrcSplitWidths(width).rightWidth > 0;
156
+ }
157
+
158
+ setVisible(visible: boolean): void {
159
+ if (this.#visible === visible) return;
160
+ this.#visible = visible;
161
+ this.invalidate();
162
+ }
163
+
164
+ render(width: number): string[] {
165
+ return this.renderWithViewportAnchors(width).lines;
166
+ }
167
+
168
+ renderWithViewportAnchors(width: number): ViewportAnchorRender {
169
+ if (!this.#visible) return renderComponentWithViewportAnchors(this.leftPane, width);
170
+
171
+ const componentTheme = typeof this.componentTheme === "function" ? this.componentTheme() : this.componentTheme;
172
+ const { leftWidth, separatorWidth, rightWidth } = computeIrcSplitWidths(width);
173
+ if (rightWidth === 0) return renderComponentWithViewportAnchors(this.leftPane, width);
174
+
175
+ const separatorText = componentTheme.fg("dim", ` ${componentTheme.boxSharp.vertical} `);
176
+ const separator = separatorWidth > 0 ? separatorText : "";
177
+ const leftRender = withTerminalGraphicsFallback(
178
+ () => renderComponentWithViewportAnchors(this.leftPane, leftWidth),
179
+ { allowCursorNeutralImages: true },
180
+ );
181
+ const rightLines = renderSidebarRecords(this.ledger, rightWidth, componentTheme);
182
+ const lineCount = Math.max(leftRender.lines.length, rightLines.length);
183
+ const lines: string[] = [];
184
+ const anchors: ViewportAnchorRender["anchors"] = [];
185
+
186
+ const leftOffset = lineCount - leftRender.lines.length;
187
+ const rightOffset = lineCount - rightLines.length;
188
+ for (let index = 0; index < lineCount; index++) {
189
+ const leftIndex = index - leftOffset;
190
+ const leftRaw = leftRender.lines[leftIndex] ?? "";
191
+ const right = truncateToWidth(rightLines[index - rightOffset] ?? "", rightWidth);
192
+ if (TERMINAL.isImageLine(leftRaw)) {
193
+ lines.push(leftRaw + padding(leftWidth) + separator + right);
194
+ } else {
195
+ const left = truncateToWidth(leftRaw, leftWidth);
196
+ lines.push(left + padding(Math.max(0, leftWidth - visibleWidth(left))) + separator + right);
197
+ }
198
+ anchors.push(leftRender.anchors[leftIndex] ?? null);
199
+ }
200
+ return { lines, anchors };
201
+ }
202
+
203
+ invalidate(): void {
204
+ this.leftPane.invalidate?.();
205
+ }
206
+ }
@@ -0,0 +1,55 @@
1
+ import { Container, PET_SKIN_IDS, PET_SKINS, type SelectItem, SelectList } from "@sayknow-cli/tui";
2
+ import { getSelectListTheme } from "../theme/theme";
3
+ import { DynamicBorder } from "./dynamic-border";
4
+ import type { PetMode } from "./sayknow-pet-widget";
5
+
6
+ const PET_OPTIONS: { value: PetMode; label: string; description: string }[] = [
7
+ { value: "off", label: "Off", description: "No pet" },
8
+ ...PET_SKIN_IDS.map(id => ({
9
+ value: id,
10
+ label: PET_SKINS[id].label,
11
+ description: PET_SKINS[id].description,
12
+ })),
13
+ ];
14
+
15
+ /**
16
+ * Theme-style picker for the sayknow pet skin (Off / RedSayknow / BlueSayknow). Preview
17
+ * fires as the selection moves; select commits, cancel restores.
18
+ */
19
+ export class PetSelectorComponent extends Container {
20
+ #selectList: SelectList;
21
+
22
+ constructor(
23
+ current: PetMode,
24
+ onSelect: (mode: PetMode) => void,
25
+ onCancel: () => void,
26
+ onPreview: (mode: PetMode) => void,
27
+ ) {
28
+ super();
29
+
30
+ const items: SelectItem[] = PET_OPTIONS.map(option => ({
31
+ value: option.value,
32
+ label: option.label,
33
+ description: option.value === current ? `${option.description} (current)` : option.description,
34
+ }));
35
+
36
+ this.addChild(new DynamicBorder());
37
+
38
+ this.#selectList = new SelectList(items, 10, getSelectListTheme());
39
+ const currentIndex = PET_OPTIONS.findIndex(option => option.value === current);
40
+ if (currentIndex !== -1) {
41
+ this.#selectList.setSelectedIndex(currentIndex);
42
+ }
43
+
44
+ this.#selectList.onSelect = item => onSelect(item.value as PetMode);
45
+ this.#selectList.onCancel = () => onCancel();
46
+ this.#selectList.onSelectionChange = item => onPreview(item.value as PetMode);
47
+
48
+ this.addChild(this.#selectList);
49
+ this.addChild(new DynamicBorder());
50
+ }
51
+
52
+ getSelectList(): SelectList {
53
+ return this.#selectList;
54
+ }
55
+ }