@sayknow-cli/coding-agent 0.3.7 → 0.3.8

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/append-only-mode.d.ts +31 -0
  2. package/dist/types/capability/rule.d.ts +8 -0
  3. package/dist/types/cli/completion-cli.d.ts +41 -0
  4. package/dist/types/cli/config-cli.d.ts +1 -0
  5. package/dist/types/cli/fixture-report.d.ts +20 -0
  6. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  7. package/dist/types/cli/nofile-limit.d.ts +11 -0
  8. package/dist/types/cli/notify-cli.d.ts +7 -0
  9. package/dist/types/cli/update-cli.d.ts +22 -0
  10. package/dist/types/cli.d.ts +124 -1
  11. package/dist/types/commands/completion.d.ts +29 -0
  12. package/dist/types/commands/config.d.ts +3 -0
  13. package/dist/types/commands/deep-interview.d.ts +3 -0
  14. package/dist/types/config/file-lock.d.ts +7 -6
  15. package/dist/types/config/keybindings.d.ts +2 -2
  16. package/dist/types/config/model-profile-activation.d.ts +6 -0
  17. package/dist/types/config/model-profiles.d.ts +1 -0
  18. package/dist/types/config/settings-schema.d.ts +105 -1
  19. package/dist/types/config/settings.d.ts +8 -0
  20. package/dist/types/discovery/index.d.ts +1 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  24. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  25. package/dist/types/goals/runtime.d.ts +1 -0
  26. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  27. package/dist/types/main.d.ts +4 -1
  28. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  29. package/dist/types/modes/components/model-selector.d.ts +4 -0
  30. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  31. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  32. package/dist/types/modes/components/status-line.d.ts +9 -0
  33. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  34. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  35. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  36. package/dist/types/modes/interactive-mode.d.ts +2 -0
  37. package/dist/types/modes/print-mode.d.ts +13 -1
  38. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  39. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  40. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  41. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  42. package/dist/types/modes/types.d.ts +1 -0
  43. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  44. package/dist/types/notifications/config.d.ts +14 -1
  45. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  46. package/dist/types/notifications/index.d.ts +6 -3
  47. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  48. package/dist/types/notifications/telegram-daemon.d.ts +23 -3
  49. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  50. package/dist/types/notifications/threaded-render.d.ts +6 -0
  51. package/dist/types/runtime-api-key.d.ts +7 -0
  52. package/dist/types/sdk.d.ts +3 -0
  53. package/dist/types/session/agent-session.d.ts +61 -6
  54. package/dist/types/session/agent-storage.d.ts +1 -0
  55. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  56. package/dist/types/session/messages.d.ts +4 -0
  57. package/dist/types/session/session-manager.d.ts +17 -1
  58. package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
  59. package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
  60. package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
  61. package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
  62. package/dist/types/skc-runtime/state-writer.d.ts +3 -0
  63. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  64. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
  65. package/dist/types/skill-state/active-state.d.ts +18 -1
  66. package/dist/types/system-prompt.d.ts +18 -2
  67. package/dist/types/task/executor.d.ts +3 -0
  68. package/dist/types/task/fork-context-budget.d.ts +7 -0
  69. package/dist/types/task/token-log.d.ts +30 -0
  70. package/dist/types/task/token-log.test.d.ts +1 -0
  71. package/dist/types/tools/bisect.d.ts +81 -0
  72. package/dist/types/tools/computer.d.ts +2 -2
  73. package/dist/types/tools/index.d.ts +1 -0
  74. package/dist/types/tools/search.d.ts +2 -2
  75. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  76. package/dist/types/utils/file-mentions.d.ts +14 -0
  77. package/dist/types/utils/git.d.ts +32 -0
  78. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  79. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  80. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  81. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  82. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  83. package/package.json +8 -8
  84. package/src/append-only-mode.ts +58 -0
  85. package/src/autoresearch/prompt.md +1 -1
  86. package/src/autoresearch/tools/log-experiment.ts +1 -1
  87. package/src/capability/rule.ts +9 -0
  88. package/src/cli/completion-cli.ts +234 -0
  89. package/src/cli/config-cli.ts +61 -9
  90. package/src/cli/fast-help.ts +1 -0
  91. package/src/cli/fixture-report.test.ts +230 -0
  92. package/src/cli/fixture-report.ts +171 -0
  93. package/src/cli/nofile-limit.ts +48 -0
  94. package/src/cli/notify-cli.ts +92 -8
  95. package/src/cli/telegram-cli.ts +1 -1
  96. package/src/cli/update-cli.ts +85 -9
  97. package/src/cli.ts +29 -1
  98. package/src/commands/completion.ts +84 -0
  99. package/src/commands/config.ts +2 -0
  100. package/src/commands/contribution-prep.ts +1 -1
  101. package/src/commands/deep-interview.ts +2 -1
  102. package/src/config/file-lock.ts +73 -34
  103. package/src/config/keybindings.ts +2 -2
  104. package/src/config/model-profile-activation.ts +65 -0
  105. package/src/config/model-profiles.ts +31 -2
  106. package/src/config/settings-schema.ts +68 -1
  107. package/src/config/settings.ts +22 -0
  108. package/src/defaults/skc/skills/team/SKILL.md +17 -14
  109. package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
  110. package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  111. package/src/defaults/skc-defaults.ts +9 -0
  112. package/src/discovery/helpers.ts +10 -0
  113. package/src/discovery/index.ts +1 -0
  114. package/src/export/html/template.generated.ts +1 -1
  115. package/src/export/html/template.js +60 -2
  116. package/src/export/ttsr.ts +96 -18
  117. package/src/extensibility/extensions/runner.ts +183 -219
  118. package/src/extensibility/extensions/types.ts +9 -0
  119. package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
  120. package/src/goals/runtime.ts +10 -2
  121. package/src/hindsight/backend.ts +1 -3
  122. package/src/hooks/native-skill-hook.ts +124 -4
  123. package/src/internal-urls/docs-index.generated.ts +14 -5
  124. package/src/main.ts +95 -43
  125. package/src/memories/index.ts +12 -1
  126. package/src/modes/components/assistant-message.ts +241 -82
  127. package/src/modes/components/custom-editor.ts +1 -0
  128. package/src/modes/components/model-selector.ts +54 -11
  129. package/src/modes/components/queued-message-selector.ts +92 -0
  130. package/src/modes/components/settings-selector.ts +4 -0
  131. package/src/modes/components/status-line.ts +167 -54
  132. package/src/modes/components/tool-execution.ts +44 -19
  133. package/src/modes/components/welcome.ts +181 -44
  134. package/src/modes/controllers/command-controller.ts +40 -4
  135. package/src/modes/controllers/event-controller.ts +66 -47
  136. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  137. package/src/modes/controllers/input-controller.ts +180 -18
  138. package/src/modes/controllers/selector-controller.ts +127 -24
  139. package/src/modes/interactive-mode.ts +51 -16
  140. package/src/modes/print-mode.ts +43 -4
  141. package/src/modes/prompt-action-autocomplete.ts +21 -20
  142. package/src/modes/rpc/rpc-mode.ts +29 -3
  143. package/src/modes/rpc/rpc-types.ts +9 -0
  144. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  145. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  146. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  147. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  148. package/src/modes/types.ts +1 -0
  149. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  150. package/src/modes/utils/ui-helpers.ts +20 -7
  151. package/src/notifications/config.ts +25 -5
  152. package/src/notifications/daemon-paths.ts +2 -0
  153. package/src/notifications/index.ts +117 -14
  154. package/src/notifications/lifecycle-commands.ts +37 -7
  155. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  156. package/src/notifications/telegram-daemon-control.ts +5 -3
  157. package/src/notifications/telegram-daemon.ts +302 -51
  158. package/src/notifications/telegram-reference.ts +0 -16
  159. package/src/notifications/threaded-render.ts +17 -1
  160. package/src/prompts/agents/architect.md +19 -10
  161. package/src/prompts/agents/critic.md +16 -18
  162. package/src/prompts/agents/executor.md +14 -11
  163. package/src/prompts/agents/planner.md +18 -13
  164. package/src/prompts/ci-green-request.md +3 -8
  165. package/src/prompts/goals/goal-continuation.md +0 -5
  166. package/src/prompts/goals/goal-mode-active.md +0 -5
  167. package/src/prompts/system/agent-creation-architect.md +2 -2
  168. package/src/prompts/system/plan-mode-active.md +1 -1
  169. package/src/prompts/system/plan-mode-subagent.md +3 -3
  170. package/src/prompts/system/project-prompt.md +0 -13
  171. package/src/prompts/system/subagent-system-prompt.md +3 -2
  172. package/src/prompts/system/system-prompt.md +29 -17
  173. package/src/prompts/system/volatile-project-context.md +10 -0
  174. package/src/prompts/system/web-search.md +4 -7
  175. package/src/prompts/tools/apply-patch.md +6 -0
  176. package/src/prompts/tools/ast-edit.md +1 -0
  177. package/src/prompts/tools/bash.md +1 -1
  178. package/src/prompts/tools/bisect.md +22 -0
  179. package/src/prompts/tools/browser.md +1 -1
  180. package/src/prompts/tools/computer.md +2 -2
  181. package/src/prompts/tools/cron.md +0 -2
  182. package/src/prompts/tools/eval.md +2 -2
  183. package/src/prompts/tools/find.md +1 -1
  184. package/src/prompts/tools/github.md +6 -6
  185. package/src/prompts/tools/goal.md +1 -3
  186. package/src/prompts/tools/image-gen.md +12 -5
  187. package/src/prompts/tools/irc.md +5 -4
  188. package/src/prompts/tools/job.md +2 -2
  189. package/src/prompts/tools/lsp.md +1 -1
  190. package/src/prompts/tools/monitor.md +2 -2
  191. package/src/prompts/tools/patch.md +2 -2
  192. package/src/prompts/tools/replace.md +2 -21
  193. package/src/prompts/tools/rewind.md +1 -1
  194. package/src/prompts/tools/search-tool-bm25.md +1 -11
  195. package/src/prompts/tools/search.md +1 -1
  196. package/src/prompts/tools/skill.md +3 -2
  197. package/src/prompts/tools/ssh.md +2 -1
  198. package/src/prompts/tools/subagent.md +2 -2
  199. package/src/prompts/tools/task.md +4 -5
  200. package/src/prompts/tools/todo-write.md +1 -1
  201. package/src/prompts/tools/vim.md +1 -0
  202. package/src/prompts/tools/web-search.md +2 -0
  203. package/src/prompts/tools/write.md +8 -2
  204. package/src/runtime-api-key.ts +16 -0
  205. package/src/sdk.ts +52 -28
  206. package/src/session/agent-session.ts +1011 -141
  207. package/src/session/agent-storage.ts +4 -0
  208. package/src/session/file-mention-pruning.ts +64 -0
  209. package/src/session/messages.ts +4 -0
  210. package/src/session/session-manager.ts +103 -4
  211. package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
  212. package/src/skc-runtime/deep-interview-recorder.ts +62 -8
  213. package/src/skc-runtime/deep-interview-state.ts +44 -2
  214. package/src/skc-runtime/session-state-sidecar.ts +263 -20
  215. package/src/skc-runtime/state-runtime.ts +12 -2
  216. package/src/skc-runtime/state-writer.ts +15 -0
  217. package/src/skc-runtime/ultragoal-guard.ts +55 -66
  218. package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
  219. package/src/skc-runtime/ultragoal-runtime.ts +1663 -159
  220. package/src/skc-runtime/workflow-manifest.generated.json +113 -2
  221. package/src/skc-runtime/workflow-manifest.ts +74 -1
  222. package/src/skill-state/active-state.ts +141 -5
  223. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  224. package/src/slash-commands/builtin-registry.ts +218 -71
  225. package/src/system-prompt.ts +57 -16
  226. package/src/task/executor.ts +132 -24
  227. package/src/task/fork-context-advisory.ts +2 -7
  228. package/src/task/fork-context-budget.ts +9 -0
  229. package/src/task/index.ts +13 -6
  230. package/src/task/output-manager.ts +20 -8
  231. package/src/task/receipt.ts +26 -7
  232. package/src/task/token-log.test.ts +188 -0
  233. package/src/task/token-log.ts +179 -0
  234. package/src/tools/bisect.ts +367 -0
  235. package/src/tools/computer.ts +246 -43
  236. package/src/tools/index.ts +3 -0
  237. package/src/tools/output-meta.ts +116 -4
  238. package/src/tools/search.ts +8 -2
  239. package/src/tools/skill.ts +15 -0
  240. package/src/tools/telegram-send.ts +6 -0
  241. package/src/tools/ultragoal-ask-guard.ts +39 -5
  242. package/src/utils/file-mentions.ts +39 -7
  243. package/src/utils/git.ts +73 -0
  244. package/src/web/scrapers/youtube.ts +24 -12
  245. package/src/workflow/workflow-intent-diff.ts +29 -6
  246. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  247. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  248. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -669,6 +669,8 @@ export interface SettingsRuntimeContext {
669
669
  thinkingLevel: ThinkingLevel | undefined;
670
670
  /** Available themes */
671
671
  availableThemes: string[];
672
+ /** Available model profile names (from the model registry) */
673
+ availableModelProfiles: string[];
672
674
  /** Working directory for plugins tab */
673
675
  cwd: string;
674
676
  }
@@ -861,6 +863,8 @@ export class SettingsSelectorComponent extends Container {
861
863
  });
862
864
  } else if (def.path === "theme.dark" || def.path === "theme.light") {
863
865
  options = this.context.availableThemes.map(t => ({ value: t, label: t }));
866
+ } else if (def.path === "modelProfile.default") {
867
+ options = this.context.availableModelProfiles.map(p => ({ value: p, label: p }));
864
868
  }
865
869
  if (def.path === "statusLine.preset") {
866
870
  options = options.filter(option => option.value !== "custom");
@@ -175,6 +175,9 @@ export class StatusLineComponent implements Component {
175
175
  #settings: StatusLineSettings = {};
176
176
  #cachedBranch: string | null | undefined = undefined;
177
177
  #cachedBranchRepoId: string | null | undefined = undefined;
178
+ #branchProjectDir: string | undefined;
179
+ #branchLastFetch = 0;
180
+ #branchInFlight = false;
178
181
  #gitWatcher: fs.FSWatcher | null = null;
179
182
  #onBranchChange: (() => void) | null = null;
180
183
  #autoCompactEnabled: boolean = true;
@@ -188,6 +191,14 @@ export class StatusLineComponent implements Component {
188
191
  #skillHudLastFetch = 0;
189
192
  #skillHudInFlight = false;
190
193
  #version: string | undefined;
194
+ #resolvedSettingsCache:
195
+ | (Required<Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">> &
196
+ StatusLineSettings)
197
+ | undefined;
198
+ #resolvedSettingsFingerprint: string | undefined;
199
+ #renderedRowsCache: { key: string; rows: string[] } | undefined;
200
+ #renderedRowsCacheHits = 0;
201
+ #renderedRowsCacheMisses = 0;
191
202
 
192
203
  // Git status caching (1s TTL)
193
204
  #cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
@@ -320,18 +331,39 @@ export class StatusLineComponent implements Component {
320
331
  #invalidateGitCaches(): void {
321
332
  this.#cachedBranch = undefined;
322
333
  this.#cachedBranchRepoId = undefined;
334
+ this.#branchProjectDir = undefined;
323
335
  this.#cachedPrContext = undefined;
336
+ this.#branchLastFetch = 0;
337
+ this.#branchInFlight = false;
338
+ this.#renderedRowsCache = undefined;
324
339
  }
325
340
  #getCurrentBranch(): string | null {
326
- const current = resolveCurrentBranch(getProjectDir());
327
- if (this.#cachedBranch !== undefined && this.#cachedBranchRepoId === current.repoId) {
328
- return this.#cachedBranch;
341
+ const now = Date.now();
342
+ const projectDir = getProjectDir();
343
+ const withinTtl =
344
+ this.#cachedBranch !== undefined &&
345
+ this.#branchProjectDir === projectDir &&
346
+ Date.now() - this.#branchLastFetch < 1000;
347
+ if (withinTtl || this.#branchInFlight) {
348
+ return this.#cachedBranch ?? null;
329
349
  }
330
350
 
331
- this.#cachedBranchRepoId = current.repoId;
332
- this.#cachedBranch = current.branch;
333
-
334
- return this.#cachedBranch ?? null;
351
+ this.#branchInFlight = true;
352
+ try {
353
+ const current = resolveCurrentBranch(projectDir);
354
+ this.#cachedBranchRepoId = current.repoId;
355
+ this.#cachedBranch = current.branch;
356
+ this.#branchProjectDir = projectDir;
357
+ return this.#cachedBranch ?? null;
358
+ } catch {
359
+ this.#cachedBranchRepoId = null;
360
+ this.#cachedBranch = null;
361
+ this.#branchProjectDir = projectDir;
362
+ return null;
363
+ } finally {
364
+ this.#branchLastFetch = now;
365
+ this.#branchInFlight = false;
366
+ }
335
367
  }
336
368
 
337
369
  #isDefaultBranch(branch: string): boolean {
@@ -467,7 +499,7 @@ export class StatusLineComponent implements Component {
467
499
  const cwd = typeof getCwd === "function" ? getCwd.call(this.session.sessionManager) : getProjectDir();
468
500
  const sessionId = typeof getSessionId === "function" ? getSessionId.call(this.session.sessionManager) : undefined;
469
501
  this.#skillHudInFlight = true;
470
- void readVisibleSkillActiveState(cwd, sessionId)
502
+ void readVisibleSkillActiveState(cwd, sessionId, { tier: "hud" })
471
503
  .then(state => {
472
504
  this.#skillHudEntries = state?.active_skills ?? [];
473
505
  })
@@ -637,7 +669,13 @@ export class StatusLineComponent implements Component {
637
669
  return `${modelId}|${sp.length}:${sp[0]?.length ?? 0}|${tools.length}|${skills.length}`;
638
670
  }
639
671
 
640
- #buildSegmentContext(width: number): SegmentContext {
672
+ #buildSegmentContext(
673
+ width: number,
674
+ effectiveSettings: Required<
675
+ Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
676
+ > &
677
+ StatusLineSettings,
678
+ ): SegmentContext {
641
679
  const state = this.session.state;
642
680
 
643
681
  // Trigger background fetch (5-min TTL); render uses cached value
@@ -666,7 +704,7 @@ export class StatusLineComponent implements Component {
666
704
  return {
667
705
  session: this.session,
668
706
  width,
669
- options: this.#resolveSettings().segmentOptions ?? {},
707
+ options: effectiveSettings.segmentOptions ?? {},
670
708
  planMode: this.#planModeStatus,
671
709
  goalMode: this.#goalModeStatus,
672
710
  usageStats,
@@ -685,10 +723,19 @@ export class StatusLineComponent implements Component {
685
723
  };
686
724
  }
687
725
 
726
+ #settingsFingerprint(): string {
727
+ return JSON.stringify(this.#settings);
728
+ }
729
+
688
730
  #resolveSettings(): Required<
689
731
  Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
690
732
  > &
691
733
  StatusLineSettings {
734
+ const fingerprint = this.#settingsFingerprint();
735
+ if (this.#resolvedSettingsCache && this.#resolvedSettingsFingerprint === fingerprint) {
736
+ return this.#resolvedSettingsCache;
737
+ }
738
+
692
739
  const preset = this.#settings.preset ?? "default";
693
740
  const presetDef = getPreset(preset);
694
741
  const useCustomSegments = preset === "custom";
@@ -713,13 +760,15 @@ export class StatusLineComponent implements Component {
713
760
  ? (this.#settings.rightSegments ?? presetDef.rightSegments)
714
761
  : presetDef.rightSegments;
715
762
 
716
- return {
763
+ this.#resolvedSettingsFingerprint = fingerprint;
764
+ this.#resolvedSettingsCache = {
717
765
  ...this.#settings,
718
766
  leftSegments,
719
767
  rightSegments,
720
768
  separator: this.#settings.separator ?? presetDef.separator,
721
769
  segmentOptions: mergedSegmentOptions,
722
770
  };
771
+ return this.#resolvedSettingsCache;
723
772
  }
724
773
 
725
774
  #groupWidth(parts: string[], capWidth: number, sepWidth: number): number {
@@ -785,9 +834,14 @@ export class StatusLineComponent implements Component {
785
834
  return reRendered.content;
786
835
  }
787
836
 
788
- #collectStatusSegments(width: number): CollectedStatusSegments {
789
- const ctx = this.#buildSegmentContext(width);
790
- const effectiveSettings = this.#resolveSettings();
837
+ #collectStatusSegments(
838
+ width: number,
839
+ effectiveSettings: Required<
840
+ Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
841
+ > &
842
+ StatusLineSettings,
843
+ ): CollectedStatusSegments {
844
+ const ctx = this.#buildSegmentContext(width, effectiveSettings);
791
845
  const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
792
846
 
793
847
  // Use the subtle surface tone (the same elevated background as user-message
@@ -856,7 +910,7 @@ export class StatusLineComponent implements Component {
856
910
  }
857
911
 
858
912
  #buildStatusLine(width: number, precollected?: CollectedStatusSegments): string {
859
- const seg = precollected ?? this.#collectStatusSegments(width);
913
+ const seg = precollected ?? this.#collectStatusSegments(width, this.#resolveSettings());
860
914
  const { ctx, separatorDef, bgAnsi, fgAnsi, sepAnsi, previewHighlightSegment } = seg;
861
915
  const { leftSepWidth, rightSepWidth, leftCapWidth, rightCapWidth } = seg;
862
916
  const left = [...seg.left];
@@ -915,8 +969,52 @@ export class StatusLineComponent implements Component {
915
969
  * everything fits on one line.
916
970
  */
917
971
  #buildStatusRows(width: number, maxRows: number): string[] {
918
- const seg = this.#collectStatusSegments(width);
919
- if (seg.left.length === 0 && seg.right.length === 0) return [];
972
+ const effectiveSettings = this.#resolveSettings();
973
+ const seg = this.#collectStatusSegments(width, effectiveSettings);
974
+ const cacheKey = JSON.stringify({
975
+ width,
976
+ maxRows,
977
+ settings: this.#resolvedSettingsFingerprint,
978
+ left: seg.left,
979
+ leftSegIds: seg.leftSegIds,
980
+ right: seg.right,
981
+ separator: effectiveSettings.separator,
982
+ previewHighlightSegment: seg.previewHighlightSegment,
983
+ sessionAccent: seg.sessionAccent,
984
+ theme: [seg.bgAnsi, seg.fgAnsi, seg.sepAnsi],
985
+ rowLayout: {
986
+ separatorLeft: seg.separatorDef.left,
987
+ separatorRight: seg.separatorDef.right,
988
+ separatorEndCapLeft: seg.separatorDef.endCaps?.left,
989
+ separatorEndCapRight: seg.separatorDef.endCaps?.right,
990
+ separatorEndCapUseBgAsFg: seg.separatorDef.endCaps?.useBgAsFg,
991
+ borderFgAnsi: theme.getFgAnsi("border"),
992
+ boxRoundHorizontal: theme.boxRound.horizontal,
993
+ },
994
+ context: [seg.ctx.contextPercent, seg.ctx.contextWindow],
995
+ usageStats: seg.ctx.usageStats,
996
+ usage: seg.ctx.usage,
997
+ git: seg.ctx.git,
998
+ modes: [seg.ctx.planMode, seg.ctx.goalMode, seg.ctx.autoCompactEnabled],
999
+ runtime: [seg.ctx.subagentCount, seg.ctx.jobs, seg.ctx.sessionStartTime],
1000
+ sessionName: seg.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined,
1001
+ asyncJobs: this.session
1002
+ .getAsyncJobSnapshot()
1003
+ ?.running.filter(job => job.metadata?.monitor !== true)
1004
+ .map(job => job.id ?? job.metadata ?? job)
1005
+ .join(","),
1006
+ version: this.#version,
1007
+ });
1008
+ if (this.#renderedRowsCache?.key === cacheKey) {
1009
+ this.#renderedRowsCacheHits++;
1010
+ return [...this.#renderedRowsCache.rows];
1011
+ }
1012
+ this.#renderedRowsCacheMisses++;
1013
+
1014
+ if (seg.left.length === 0 && seg.right.length === 0) {
1015
+ this.#renderedRowsCache = { key: cacheKey, rows: [] };
1016
+ return [];
1017
+ }
920
1018
 
921
1019
  const topFillWidth = Math.max(1, width);
922
1020
  const leftWidth = this.#groupWidth(seg.left, seg.leftCapWidth, seg.leftSepWidth);
@@ -924,51 +1022,52 @@ export class StatusLineComponent implements Component {
924
1022
  const gap = seg.left.length > 0 && seg.right.length > 0 ? 1 : 0;
925
1023
  const fitsSingleRow = leftWidth + rightWidth + gap <= topFillWidth;
926
1024
 
1025
+ let rows: string[];
927
1026
  if (maxRows <= 1 || fitsSingleRow) {
928
1027
  const single = this.#buildStatusLine(width, seg);
929
- return single ? [single] : [];
930
- }
931
-
932
- // Ordered sequence: left segments first (in order), then right segments.
933
- const items: { content: string; isPath: boolean }[] = [
934
- ...seg.left.map((content, i) => ({ content, isPath: seg.leftSegIds[i] === "path" })),
935
- ...seg.right.map(content => ({ content, isPath: false })),
936
- ];
937
-
938
- // Pre-shrink an over-wide path so it never blows out a row on its own.
939
- for (const item of items) {
940
- if (!item.isPath) continue;
941
- const alone = this.#groupWidth([item.content], seg.leftCapWidth, seg.leftSepWidth);
942
- if (alone > topFillWidth) {
943
- const shrunk = this.#shrinkPathToWidth(item.content, seg.ctx, alone - topFillWidth);
944
- if (shrunk !== null) item.content = shrunk;
1028
+ rows = single ? [single] : [];
1029
+ } else {
1030
+ const items: { content: string; isPath: boolean }[] = [
1031
+ ...seg.left.map((content, i) => ({ content, isPath: seg.leftSegIds[i] === "path" })),
1032
+ ...seg.right.map(content => ({ content, isPath: false })),
1033
+ ];
1034
+
1035
+ for (const item of items) {
1036
+ if (!item.isPath) continue;
1037
+ const alone = this.#groupWidth([item.content], seg.leftCapWidth, seg.leftSepWidth);
1038
+ if (alone > topFillWidth) {
1039
+ const shrunk = this.#shrinkPathToWidth(item.content, seg.ctx, alone - topFillWidth);
1040
+ if (shrunk !== null) item.content = shrunk;
1041
+ }
945
1042
  }
946
- }
947
1043
 
948
- // Greedy pack into up to `maxRows` left-justified rows.
949
- const rows: string[][] = [];
950
- let current: string[] = [];
951
- for (const item of items) {
952
- if (current.length === 0) {
953
- current.push(item.content);
954
- continue;
1044
+ const packedRows: string[][] = [];
1045
+ let current: string[] = [];
1046
+ for (const item of items) {
1047
+ if (current.length === 0) {
1048
+ current.push(item.content);
1049
+ continue;
1050
+ }
1051
+ const tentative = [...current, item.content];
1052
+ if (this.#groupWidth(tentative, seg.leftCapWidth, seg.leftSepWidth) <= topFillWidth) {
1053
+ current = tentative;
1054
+ } else {
1055
+ packedRows.push(current);
1056
+ current = [item.content];
1057
+ if (packedRows.length >= maxRows) break;
1058
+ }
955
1059
  }
956
- const tentative = [...current, item.content];
957
- if (this.#groupWidth(tentative, seg.leftCapWidth, seg.leftSepWidth) <= topFillWidth) {
958
- current = tentative;
959
- } else {
960
- rows.push(current);
961
- current = [item.content];
962
- if (rows.length >= maxRows) break;
1060
+ if (packedRows.length < maxRows && current.length > 0) {
1061
+ packedRows.push(current);
963
1062
  }
964
- }
965
- if (rows.length < maxRows && current.length > 0) {
966
- rows.push(current);
1063
+
1064
+ rows = packedRows.map(row =>
1065
+ this.#renderStatusGroup(row, "left", seg.separatorDef, seg.bgAnsi, seg.fgAnsi, seg.sepAnsi),
1066
+ );
967
1067
  }
968
1068
 
969
- return rows.map(row =>
970
- this.#renderStatusGroup(row, "left", seg.separatorDef, seg.bgAnsi, seg.fgAnsi, seg.sepAnsi),
971
- );
1069
+ this.#renderedRowsCache = { key: cacheKey, rows };
1070
+ return [...rows];
972
1071
  }
973
1072
 
974
1073
  getTopBorder(width: number): { content: string; width: number } {
@@ -989,6 +1088,20 @@ export class StatusLineComponent implements Component {
989
1088
  return this.#buildStatusRows(width, this.#resolveMaxRows()).join("\n");
990
1089
  }
991
1090
 
1091
+ getCacheStatsForTest(): { rowHits: number; rowMisses: number } {
1092
+ return { rowHits: this.#renderedRowsCacheHits, rowMisses: this.#renderedRowsCacheMisses };
1093
+ }
1094
+
1095
+ invalidateBranchForTest(): void {
1096
+ this.#invalidateGitCaches();
1097
+ }
1098
+
1099
+ setCachedPrForTest(pr: { number: number; url: string } | null): void {
1100
+ const branch = this.#getCurrentBranch();
1101
+ this.#cachedPr = pr;
1102
+ this.#cachedPrContext = branch ? createPrCacheContext(branch, this.#cachedBranchRepoId ?? null) : undefined;
1103
+ }
1104
+
992
1105
  render(width: number): string[] {
993
1106
  const lines: string[] = [];
994
1107
  this.#refreshSkillHudInBackground();
@@ -1,5 +1,6 @@
1
1
  import type { AgentTool } from "@sayknow-cli/agent-core";
2
2
  import {
3
+ type AnimationRegistration,
3
4
  Box,
4
5
  type Component,
5
6
  Container,
@@ -7,6 +8,7 @@ import {
7
8
  Image,
8
9
  ImageProtocol,
9
10
  imageFallback,
11
+ registerAnimationCallback,
10
12
  Spacer,
11
13
  TERMINAL,
12
14
  Text,
@@ -51,6 +53,21 @@ function cloneToolArgs<T>(args: T): T {
51
53
  }
52
54
  }
53
55
 
56
+ // Built-in tool renderers that treat call args as read-only, so streaming UI can
57
+ // avoid per-delta defensive clone churn. Kept here (not in the tool renderer
58
+ // registry) because it is a rendering-perf capability of these renderers, not a
59
+ // tool-registration concern.
60
+ const READONLY_ARG_RENDERER_TOOLS = new Set(["bash", "recipe", "eval", "edit", "apply_patch"]);
61
+
62
+ function argsCanBeSharedWithRenderer(toolName: string, tool: AgentTool | undefined): boolean {
63
+ return !tool?.renderCall && !tool?.renderResult && READONLY_ARG_RENDERER_TOOLS.has(toolName);
64
+ }
65
+
66
+ function partialJsonLength(args: unknown): number {
67
+ const value = args && typeof args === "object" ? (args as { __partialJson?: unknown }).__partialJson : undefined;
68
+ return typeof value === "string" ? value.length : -1;
69
+ }
70
+
54
71
  /**
55
72
  * Drop trailing removal/hunk-header lines that appear in a streaming diff
56
73
  * before the matching `+added` lines have arrived. Without this, a partial
@@ -158,11 +175,14 @@ export class ToolExecutionComponent extends Container {
158
175
  #editDiffPreview?: PerFileDiffPreview[];
159
176
  #editDiffAbort?: AbortController;
160
177
  #editDiffLastArgsKey?: string;
178
+ #argsIdentityVersion = 0;
179
+ #lastArgsReference: any;
180
+ #shareArgsWithRenderer = false;
161
181
  // Cached converted images for Kitty protocol (which requires PNG), keyed by index
162
182
  #convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
163
183
  // Spinner animation for partial task results
164
184
  #spinnerFrame?: number;
165
- #spinnerInterval?: NodeJS.Timeout;
185
+ #spinnerAnimation?: AnimationRegistration;
166
186
  // Track if args are still being streamed (for edit/write spinner)
167
187
  #argsComplete = false;
168
188
  #renderState: {
@@ -194,7 +214,9 @@ export class ToolExecutionComponent extends Container {
194
214
  this.#tool = tool;
195
215
  this.#ui = ui;
196
216
  this.#cwd = cwd;
197
- this.#args = cloneToolArgs(args);
217
+ this.#shareArgsWithRenderer = argsCanBeSharedWithRenderer(toolName, tool);
218
+ this.#lastArgsReference = args;
219
+ this.#args = this.#shareArgsWithRenderer ? args : cloneToolArgs(args);
198
220
 
199
221
  this.addChild(new Spacer(1));
200
222
 
@@ -220,7 +242,11 @@ export class ToolExecutionComponent extends Container {
220
242
  }
221
243
 
222
244
  updateArgs(args: any, _toolCallId?: string): void {
223
- this.#args = cloneToolArgs(args);
245
+ if (args !== this.#lastArgsReference) {
246
+ this.#lastArgsReference = args;
247
+ this.#argsIdentityVersion += 1;
248
+ }
249
+ this.#args = this.#shareArgsWithRenderer ? args : cloneToolArgs(args);
224
250
  this.#updateSpinnerAnimation();
225
251
  void this.#runPreviewDiff();
226
252
  this.#updateDisplay();
@@ -253,13 +279,13 @@ export class ToolExecutionComponent extends Container {
253
279
  effectiveArgs = args;
254
280
  }
255
281
 
256
- // Coalesce duplicate computes for identical args.
257
- let argsKey: string;
258
- try {
259
- argsKey = JSON.stringify(effectiveArgs);
260
- } catch {
261
- argsKey = String(Date.now());
262
- }
282
+ // Coalesce duplicate computes without serializing multi-KB streamed args every delta.
283
+ const argsKey = [
284
+ this.#toolName,
285
+ this.#argsIdentityVersion,
286
+ partialJsonLength(args),
287
+ this.#argsComplete ? 1 : 0,
288
+ ].join(":");
263
289
  if (argsKey === this.#editDiffLastArgsKey) return;
264
290
  this.#editDiffLastArgsKey = argsKey;
265
291
 
@@ -367,18 +393,17 @@ export class ToolExecutionComponent extends Container {
367
393
  (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
368
394
  const isPartialTask = this.#isPartial && this.#toolName === "task" && !isBackgroundAsyncTask;
369
395
  const needsSpinner = isStreamingArgs || isPartialTask;
370
- if (needsSpinner && !this.#spinnerInterval) {
371
- this.#spinnerInterval = setInterval(() => {
396
+ if (needsSpinner && !this.#spinnerAnimation) {
397
+ this.#spinnerAnimation = registerAnimationCallback(() => {
372
398
  const frameCount = theme.spinnerFrames.length;
373
399
  if (frameCount === 0) return;
374
400
  this.#spinnerFrame = ((this.#spinnerFrame ?? -1) + 1) % frameCount;
375
401
  this.#renderState.spinnerFrame = this.#spinnerFrame;
376
402
  this.#ui.requestRender();
377
403
  }, 80);
378
- this.#spinnerInterval?.unref?.();
379
- } else if (!needsSpinner && this.#spinnerInterval) {
380
- clearInterval(this.#spinnerInterval);
381
- this.#spinnerInterval = undefined;
404
+ } else if (!needsSpinner && this.#spinnerAnimation) {
405
+ this.#spinnerAnimation.unregister();
406
+ this.#spinnerAnimation = undefined;
382
407
  }
383
408
  }
384
409
 
@@ -386,9 +411,9 @@ export class ToolExecutionComponent extends Container {
386
411
  * Stop spinner animation and cleanup resources.
387
412
  */
388
413
  stopAnimation(): void {
389
- if (this.#spinnerInterval) {
390
- clearInterval(this.#spinnerInterval);
391
- this.#spinnerInterval = undefined;
414
+ if (this.#spinnerAnimation) {
415
+ this.#spinnerAnimation.unregister();
416
+ this.#spinnerAnimation = undefined;
392
417
  this.#spinnerFrame = undefined;
393
418
  }
394
419
  this.#editDiffAbort?.abort();