@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
@@ -26,11 +26,15 @@ import type { EvalExecutionComponent } from "./components/eval-execution";
26
26
  import type { HookEditorComponent } from "./components/hook-editor";
27
27
  import type { HookInputComponent } from "./components/hook-input";
28
28
  import type { HookSelectorComponent } from "./components/hook-selector";
29
+ import type { PetMode } from "./components/sayknow-pet-widget";
29
30
  import type { StatusLineComponent } from "./components/status-line";
30
31
  import type { ToolExecutionHandle } from "./components/tool-execution";
32
+ import type { IrcObservationLedger } from "./irc-observation-ledger";
31
33
  import type { OAuthManualInputManager } from "./oauth-manual-input";
32
34
  import type { Theme } from "./theme/theme";
35
+ import type { ParsedIrcMessage } from "./utils/irc-message";
33
36
 
37
+ export type TranscriptRebuildPolicy = "replace-identity" | "reconcile-same-transcript";
34
38
  export type CompactionQueuedMessage = {
35
39
  text: string;
36
40
  mode: "steer" | "followUp";
@@ -60,6 +64,14 @@ export type TodoPhase = {
60
64
  tasks: TodoItem[];
61
65
  };
62
66
 
67
+ export type IrcArrivalSnapshot = Readonly<{
68
+ panelVisible: boolean;
69
+ /** User-requested open state; may be true while the panel yields at narrow widths. */
70
+ panelRequestedVisible: boolean;
71
+ sidebarAvailable: boolean;
72
+ resolvedToggleKey: string | null;
73
+ }>;
74
+
63
75
  export interface InteractiveModeContext {
64
76
  // UI access
65
77
  ui: TUI;
@@ -84,6 +96,7 @@ export interface InteractiveModeContext {
84
96
  mcpManager?: MCPManager;
85
97
  lspServers?: LspStartupServerInfo[];
86
98
 
99
+ readonly ircLedger: IrcObservationLedger;
87
100
  // State
88
101
  isInitialized: boolean;
89
102
  isBackgrounded: boolean;
@@ -163,6 +176,11 @@ export interface InteractiveModeContext {
163
176
  setWorkingMessage(message?: string): void;
164
177
  applyPendingWorkingMessage(): void;
165
178
  ensureLoadingAnimation(): void;
179
+ setPetMode(mode: PetMode): void;
180
+ /** Live-preview a pet skin during a selector without persisting. */
181
+ previewPetMode(mode: PetMode): void;
182
+ /** Re-mount the composer (pet-aware) after an overlay/selector closes. */
183
+ restoreComposer(): void;
166
184
  startPendingSubmission(input: {
167
185
  text: string;
168
186
  images?: ImageContent[];
@@ -187,21 +205,34 @@ export interface InteractiveModeContext {
187
205
  withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: { imageCount?: number }): Promise<T>;
188
206
  isKnownSlashCommand(text: string): boolean;
189
207
  addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): Component[];
208
+ addLiveIrcObservationToChat(message: ParsedIrcMessage, arrival: IrcArrivalSnapshot): Component[];
209
+ removeRenderedIrcInlineComponents(observationId: string): readonly Component[] | undefined;
210
+ resetRenderedIrcInlineComponents(): readonly (readonly Component[])[];
190
211
  renderSessionContext(
191
212
  sessionContext: SessionContext,
192
213
  options?: { updateFooter?: boolean; populateHistory?: boolean },
193
214
  ): void;
194
- renderInitialMessages(prebuiltContext?: SessionContext, options?: { preserveExistingChat?: boolean }): void;
215
+ rebuildInitialMessages(
216
+ policy: TranscriptRebuildPolicy,
217
+ prebuiltContext?: SessionContext,
218
+ options?: { preserveExistingChat?: boolean },
219
+ ): void;
195
220
  getUserMessageText(message: Message): string;
221
+ getAssistantViewportAnchorId?(message: AssistantMessage): string;
196
222
  findLastAssistantMessage(): AssistantMessage | undefined;
197
223
  extractAssistantText(message: AssistantMessage): string;
198
224
  updateEditorTopBorder(): void;
199
225
  updateEditorBorderColor(): void;
200
- rebuildChatFromMessages(): void;
226
+ rebuildChatFromMessages(policy: TranscriptRebuildPolicy): void;
201
227
  setTodos(todos: TodoItem[] | TodoPhase[]): void;
202
228
  reloadTodos(): Promise<void>;
203
229
  toggleTodoExpansion(): void;
204
230
 
231
+ // IRC sidebar
232
+ toggleIrcSidebar(): void;
233
+ captureIrcArrivalSnapshot(): IrcArrivalSnapshot;
234
+ applyIrcSidebarAvailability(enabled: boolean): void;
235
+ resetIrcSidebarSession(): void;
205
236
  // Command handling
206
237
  handleExportCommand(text: string): Promise<void>;
207
238
  handleShareCommand(): Promise<void>;
@@ -242,6 +273,7 @@ export interface InteractiveModeContext {
242
273
  // Selector handling
243
274
  showSettingsSelector(): void;
244
275
  showThemeSelector(): void;
276
+ showPetSelector(): void;
245
277
  showHistorySearch(): void;
246
278
  showExtensionsDashboard(): void;
247
279
  showAgentsDashboard(): void;
@@ -3,16 +3,20 @@ import type { CompactionSettings } from "@sayknow-cli/agent-core/compaction";
3
3
  import {
4
4
  effectiveReserveTokens,
5
5
  estimateMessageTokensHeuristic,
6
- estimateTextTokensHeuristic,
7
6
  resolveThresholdTokens,
8
7
  } from "@sayknow-cli/agent-core/compaction";
9
8
  import type { Model } from "@sayknow-cli/ai";
10
9
  import { formatNumber } from "@sayknow-cli/utils";
11
- import type { Skill } from "../../extensibility/skills";
12
10
  import type { AgentSession } from "../../session/agent-session";
13
- import type { Tool } from "../../tools";
11
+ import { computeNonMessageBreakdown } from "../../session/context-estimation";
14
12
  import type { theme as Theme } from "../theme/theme";
15
13
 
14
+ export {
15
+ computeNonMessageTokens,
16
+ estimateSkillsTokens,
17
+ estimateToolSchemaTokens,
18
+ } from "../../session/context-estimation";
19
+
16
20
  const GRID_COLS = 20;
17
21
  const GRID_ROWS = 10;
18
22
  const GRID_CELLS = GRID_COLS * GRID_ROWS;
@@ -38,90 +42,13 @@ export interface ContextBreakdown {
38
42
  contextWindow: number;
39
43
  categories: CategoryInfo[];
40
44
  lastUserTurnTokens: number;
41
- usedTokens: number;
45
+ estimatedCategoryTotal: number;
46
+ usedTokens: number | null;
47
+ source: "provider_anchor" | "heuristic" | "unknown";
42
48
  autoCompactBufferTokens: number;
43
49
  freeTokens: number;
44
50
  }
45
51
 
46
- export function estimateSkillsTokens(skills: readonly Skill[]): number {
47
- const fragments: string[] = [];
48
- for (const skill of skills) {
49
- // "- name: description\n" wire framing tokenizes ~identically to the
50
- // concatenated form, so encode each piece separately and sum.
51
- fragments.push(skill.name, skill.description);
52
- }
53
- return estimateTextTokensHeuristic(fragments);
54
- }
55
-
56
- export function estimateToolSchemaTokens(
57
- tools: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">>,
58
- ): number {
59
- const fragments: string[] = [];
60
- for (const tool of tools) {
61
- fragments.push(tool.name, tool.description);
62
- try {
63
- fragments.push(JSON.stringify(tool.parameters ?? {}));
64
- } catch {
65
- // Schema may contain functions or cycles; ignore.
66
- }
67
- }
68
- return estimateTextTokensHeuristic(fragments);
69
- }
70
-
71
- /**
72
- * Compute just the NON-MESSAGE token total: system prompt (with its skills
73
- * section subtracted, since skills are tokenized separately) + system context
74
- * (the rest of the system-prompt array) + tools + skills.
75
- *
76
- * Exposed so callers like `StatusLineComponent` can cache the non-message
77
- * total separately from the message total. Non-message inputs (skills,
78
- * tools, system prompt) change rarely; the message list grows on every
79
- * streaming turn. Splitting the two lets the caller refresh each on its own
80
- * cadence — non-message recomputed only when the inputs identity changes,
81
- * messages walked incrementally as new entries append.
82
- */
83
- export function computeNonMessageTokens(session: AgentSession): number {
84
- const parts = computeNonMessageBreakdown(session);
85
- return (
86
- parts.systemPromptTokens + parts.systemContextTokens + parts.rulesTokens + parts.toolsTokens + parts.skillsTokens
87
- );
88
- }
89
-
90
- /**
91
- * Shared helper for the four non-message token totals. Single source of truth
92
- * for both `computeNonMessageTokens` (status-line incremental cache) and
93
- * `computeContextBreakdown` (/context panel). The split avoids drift between
94
- * the two surfaces — they MUST report the same numbers.
95
- */
96
- function computeNonMessageBreakdown(session: AgentSession): {
97
- rulesTokens: number;
98
- skillsTokens: number;
99
- toolsTokens: number;
100
- systemContextTokens: number;
101
- systemPromptTokens: number;
102
- } {
103
- const skillsTokens = estimateSkillsTokens(session.skills ?? []);
104
- const toolsTokens = estimateToolSchemaTokens(session.agent?.state?.tools ?? []);
105
- const systemPromptParts = session.systemPrompt ?? [];
106
- const rulesTokens = estimateRulesTokens(systemPromptParts);
107
- const systemContextTokens = estimateTextTokensHeuristic(systemPromptParts.slice(1));
108
- const systemPromptTokens = Math.max(
109
- 0,
110
- estimateTextTokensHeuristic(systemPromptParts[0] ?? "") - skillsTokens - rulesTokens,
111
- );
112
- return { rulesTokens, skillsTokens, toolsTokens, systemContextTokens, systemPromptTokens };
113
- }
114
-
115
- function estimateRulesTokens(systemPromptParts: readonly string[]): number {
116
- const fragments: string[] = [];
117
- for (const part of systemPromptParts) {
118
- for (const match of part.matchAll(/<rules>[\s\S]*?<\/rules>/g)) {
119
- fragments.push(match[0]);
120
- }
121
- }
122
- return fragments.length === 0 ? 0 : estimateTextTokensHeuristic(fragments);
123
- }
124
-
125
52
  function splitLastUserTurn(messages: readonly AgentMessage[]): {
126
53
  regularMessagesTokens: number;
127
54
  lastUserTurnTokens: number;
@@ -198,7 +125,11 @@ export function computeContextBreakdown(
198
125
  },
199
126
  ];
200
127
 
201
- const usedTokens = categories.reduce((sum, c) => sum + c.tokens, 0);
128
+ const estimatedCategoryTotal = categories.reduce((sum, c) => sum + c.tokens, 0);
129
+ const contextUsage = session.getContextUsage?.();
130
+ const source = contextUsage?.source ?? "heuristic";
131
+ const usedTokens = source === "unknown" ? null : (contextUsage?.tokens ?? estimatedCategoryTotal);
132
+ const tokensForFreeSpace = usedTokens ?? estimatedCategoryTotal;
202
133
 
203
134
  let autoCompactBufferTokens = 0;
204
135
  if (contextWindow > 0) {
@@ -214,16 +145,18 @@ export function computeContextBreakdown(
214
145
  autoCompactBufferTokens = effectiveReserveTokens(contextWindow, compactionSettings);
215
146
  }
216
147
  }
217
- autoCompactBufferTokens = Math.min(autoCompactBufferTokens, Math.max(0, contextWindow - usedTokens));
148
+ autoCompactBufferTokens = Math.min(autoCompactBufferTokens, Math.max(0, contextWindow - tokensForFreeSpace));
218
149
 
219
- const freeTokens = Math.max(0, contextWindow - usedTokens - autoCompactBufferTokens);
150
+ const freeTokens = Math.max(0, contextWindow - tokensForFreeSpace - autoCompactBufferTokens);
220
151
 
221
152
  return {
222
153
  model,
223
154
  contextWindow,
224
155
  categories,
225
156
  lastUserTurnTokens,
157
+ estimatedCategoryTotal,
226
158
  usedTokens,
159
+ source,
227
160
  autoCompactBufferTokens,
228
161
  freeTokens,
229
162
  };
@@ -310,18 +243,48 @@ function percentString(part: number, whole: number, fractionDigits = 1): string
310
243
 
311
244
  function buildLegendLines(breakdown: ContextBreakdown, theme: typeof Theme): string[] {
312
245
  const lines: string[] = [];
313
- const { model, contextWindow, categories, usedTokens, autoCompactBufferTokens, freeTokens } = breakdown;
246
+ const {
247
+ model,
248
+ contextWindow,
249
+ categories,
250
+ estimatedCategoryTotal,
251
+ usedTokens,
252
+ source,
253
+ autoCompactBufferTokens,
254
+ freeTokens,
255
+ } = breakdown;
314
256
 
315
257
  const modelName = model?.name ?? model?.id ?? "no model";
316
258
  const modelId = model?.id ?? "unknown";
317
259
  const windowLabel = formatNumber(contextWindow).toLowerCase();
260
+ const totalSourceLabel =
261
+ source === "provider_anchor"
262
+ ? "provider-reported"
263
+ : source === "unknown"
264
+ ? "estimated; exact count unknown until next response"
265
+ : "estimated";
318
266
 
319
267
  lines.push(theme.bold(`${modelName}`) + theme.fg("dim", ` (${windowLabel} context)`));
320
268
  lines.push(theme.fg("muted", `${modelId}[${windowLabel}]`));
321
- lines.push(
322
- `${theme.bold(formatNumber(usedTokens))}${theme.fg("dim", `/${windowLabel} tokens`)}` +
323
- theme.fg("muted", ` (${percentString(usedTokens, contextWindow)})`),
324
- );
269
+ if (usedTokens === null) {
270
+ lines.push(
271
+ `${theme.bold("unknown")}${theme.fg("dim", `/${windowLabel} tokens`)}` +
272
+ theme.fg("muted", " (exact count unknown until next response)"),
273
+ );
274
+ } else {
275
+ lines.push(
276
+ `${theme.bold(formatNumber(usedTokens))}${theme.fg("dim", `/${windowLabel} tokens`)}` +
277
+ theme.fg("muted", ` (${percentString(usedTokens, contextWindow)}) (${totalSourceLabel})`),
278
+ );
279
+ }
280
+ if (source !== "unknown" && usedTokens !== null && estimatedCategoryTotal !== usedTokens) {
281
+ lines.push(
282
+ theme.fg(
283
+ "muted",
284
+ `Estimated category total: ${formatNumber(estimatedCategoryTotal)} tokens (composition below is estimated)`,
285
+ ),
286
+ );
287
+ }
325
288
  lines.push("");
326
289
  lines.push(theme.fg("muted", "Estimated usage by category"));
327
290
 
@@ -334,8 +297,9 @@ function buildLegendLines(breakdown: ContextBreakdown, theme: typeof Theme): str
334
297
  }
335
298
 
336
299
  const freeDot = theme.fg("dim", CELL_FREE);
300
+ const freeLabel = usedTokens === null ? "Free space (estimated)" : "Free space";
337
301
  lines.push(
338
- `${freeDot} Free space: ${theme.bold(formatNumber(freeTokens))} ${theme.fg("dim", `(${percentString(freeTokens, contextWindow)})`)}`,
302
+ `${freeDot} ${freeLabel}: ${theme.bold(formatNumber(freeTokens))} ${theme.fg("dim", `(${percentString(freeTokens, contextWindow)})`)}`,
339
303
  );
340
304
 
341
305
  if (autoCompactBufferTokens > 0) {
@@ -54,6 +54,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
54
54
  `| \`${appKey(bindings, "app.editor.external")}\` | Edit message in external editor |`,
55
55
  `| \`${appKey(bindings, "app.clipboard.pasteImage")}\` | Paste image from clipboard |`,
56
56
  `| \`${appKey(bindings, "app.stt.toggle")}\` | Toggle speech-to-text recording |`,
57
+ `| \`${appKey(bindings, "app.irc.sidebar.toggle")}\` | Toggle IRC sidebar |`,
57
58
  "| `?` | Show help / shortcuts |",
58
59
  "| `#` | Prompt actions (command-palette style actions) |",
59
60
  "| `/` | Slash commands (try `/help` or `/new`) |",
@@ -0,0 +1,138 @@
1
+ import * as crypto from "node:crypto";
2
+ import type { AgentMessage } from "@sayknow-cli/agent-core";
3
+ import { getSegmenter } from "@sayknow-cli/tui";
4
+ import { sanitizeText } from "@sayknow-cli/utils";
5
+ import { associateSessionMessageObservationId, getSessionMessageObservationId } from "../../session/session-manager";
6
+
7
+ const graphemeSegmenter = getSegmenter();
8
+ const IRC_IDENTITY_SOURCE_MAX_UTF8_BYTES = 4 * 1_024;
9
+ const IRC_IDENTITY_DISPLAY_MAX_UTF8_BYTES = 256;
10
+ export type IrcMessageKind = "incoming" | "autoreply" | "relay";
11
+
12
+ export type ParsedIrcMessage = {
13
+ observationId: string;
14
+ from: string;
15
+ to: string;
16
+ text: string;
17
+ timestamp: number;
18
+ kind: IrcMessageKind;
19
+ };
20
+
21
+ export interface IrcMessageBlock {
22
+ readonly sender: string;
23
+ readonly recipient: string;
24
+ readonly kind: "incoming" | "autoreply" | "outgoing";
25
+ readonly time: string;
26
+ readonly bodyLines: readonly string[];
27
+ }
28
+
29
+ export function projectIrcText(
30
+ text: string,
31
+ maxUtf8Bytes: number,
32
+ ): { text: string; truncated: boolean; utf8Bytes: number } {
33
+ let end = 0;
34
+ let utf8Bytes = 0;
35
+ for (const part of graphemeSegmenter.segment(text)) {
36
+ const segmentBytes = Buffer.byteLength(part.segment, "utf8");
37
+ if (utf8Bytes + segmentBytes > maxUtf8Bytes) break;
38
+ utf8Bytes += segmentBytes;
39
+ end = part.index + part.segment.length;
40
+ }
41
+ return { text: text.slice(0, end), truncated: end < text.length, utf8Bytes };
42
+ }
43
+
44
+ /** Normalizes and bounds untrusted IRC identity fields without splitting visible graphemes. */
45
+ export function normalizeIrcIdentity(identity: string): string {
46
+ const source = projectIrcText(identity, IRC_IDENTITY_SOURCE_MAX_UTF8_BYTES);
47
+ const sanitized = sanitizeText(source.text.replace(/[\r\n\t\u2028\u2029]+/g, " ")).replace(
48
+ /[\u061C\u200E-\u200F\u202A-\u202E\u2066-\u2069]/g,
49
+ "",
50
+ );
51
+ const display = projectIrcText(sanitized, IRC_IDENTITY_DISPLAY_MAX_UTF8_BYTES);
52
+ return source.truncated || display.truncated ? `${display.text}…` : display.text;
53
+ }
54
+
55
+ /** Formats IRC observations into display-neutral semantic blocks for both IRC surfaces. */
56
+ export function formatIrcMessageBlock(message: ParsedIrcMessage & { timestamp: number }): IrcMessageBlock {
57
+ const date = new Date(message.timestamp);
58
+ // IRC timestamps are human-facing, so use local time rather than UTC serialization.
59
+ const time = Number.isFinite(date.getTime())
60
+ ? `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`
61
+ : "--:--";
62
+ const bodyLines = message.text === "" ? [] : message.text.replaceAll("\t", " ").split("\n");
63
+ return {
64
+ sender: normalizeIrcIdentity(message.from),
65
+ recipient: normalizeIrcIdentity(message.to),
66
+ kind: message.kind === "relay" ? "outgoing" : message.kind,
67
+ time,
68
+ bodyLines,
69
+ };
70
+ }
71
+
72
+ type IrcCustomMessage = Extract<AgentMessage, { role: "custom" }>;
73
+
74
+ export function isIrcCustomType(customType: string | undefined): customType is `irc:${IrcMessageKind}` {
75
+ return customType === "irc:incoming" || customType === "irc:autoreply" || customType === "irc:relay";
76
+ }
77
+
78
+ function stringDetail(details: unknown, key: string): string {
79
+ if (!details || typeof details !== "object") return "";
80
+ const value = (details as Record<string, unknown>)[key];
81
+ return typeof value === "string" ? value : "";
82
+ }
83
+
84
+ function legacyObservationId(fields: readonly string[]): string {
85
+ const hash = crypto.createHash("sha256");
86
+ hash.update("skc:irc:legacy-observation:v1\0");
87
+ for (const field of fields) {
88
+ hash.update(String(Buffer.byteLength(field, "utf8")));
89
+ hash.update(":");
90
+ hash.update(field);
91
+ }
92
+ return `legacy:sha256:${hash.digest("hex")}`;
93
+ }
94
+
95
+ /**
96
+ * Normalizes every supported IRC custom message into its UI observation shape.
97
+ * Legacy records have no UUID, so their immutable payload is used as a stable key.
98
+ */
99
+ export function parseIrcMessage(message: IrcCustomMessage): ParsedIrcMessage | undefined {
100
+ if (!isIrcCustomType(message.customType)) return undefined;
101
+
102
+ const kind = message.customType.slice(4) as IrcMessageKind;
103
+ const sourceTimestamp =
104
+ typeof message.timestamp === "number" && Number.isFinite(message.timestamp) ? message.timestamp : undefined;
105
+ const timestamp = sourceTimestamp ?? Date.now();
106
+ const rawFrom = kind === "autoreply" ? "you" : stringDetail(message.details, "from") || "?";
107
+ const rawTo = kind === "incoming" ? "you" : stringDetail(message.details, "to") || "?";
108
+ const rawText =
109
+ kind === "incoming"
110
+ ? stringDetail(message.details, "message")
111
+ : kind === "autoreply"
112
+ ? stringDetail(message.details, "reply")
113
+ : stringDetail(message.details, "body");
114
+ const from = normalizeIrcIdentity(rawFrom);
115
+ const to = normalizeIrcIdentity(rawTo);
116
+ const text = sanitizeText(rawText);
117
+ const explicitObservationId = stringDetail(message.details, "observationId");
118
+ const observationId = associateSessionMessageObservationId(
119
+ message,
120
+ explicitObservationId ||
121
+ getSessionMessageObservationId(message) ||
122
+ legacyObservationId([
123
+ kind,
124
+ sourceTimestamp === undefined ? "null" : String(sourceTimestamp),
125
+ rawFrom,
126
+ rawTo,
127
+ rawText,
128
+ ]),
129
+ );
130
+ return {
131
+ observationId,
132
+ from,
133
+ to,
134
+ text,
135
+ timestamp,
136
+ kind,
137
+ };
138
+ }