@sayknow-cli/coding-agent 0.3.5 → 0.3.7

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 (238) hide show
  1. package/README.md +76 -0
  2. package/dist/types/capability/mcp.d.ts +2 -0
  3. package/dist/types/cli/local-provider-smoke.d.ts +59 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -1
  5. package/dist/types/cli/telegram-cli.d.ts +13 -0
  6. package/dist/types/cli/update-cli.d.ts +24 -1
  7. package/dist/types/commands/local-provider.d.ts +34 -0
  8. package/dist/types/commands/telegram.d.ts +22 -0
  9. package/dist/types/config/keybindings.d.ts +12 -6
  10. package/dist/types/config/model-registry.d.ts +5 -0
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +284 -4
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/config/telegram-autostart.d.ts +14 -0
  15. package/dist/types/config/telegram-env-bridge.d.ts +18 -0
  16. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  17. package/dist/types/defaults/skc-defaults.d.ts +7 -0
  18. package/dist/types/discovery/cursor.d.ts +4 -1
  19. package/dist/types/discovery/index.d.ts +0 -1
  20. package/dist/types/discovery/windsurf.d.ts +3 -1
  21. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  22. package/dist/types/i18n/messages/en.d.ts +1 -0
  23. package/dist/types/modes/components/custom-editor.d.ts +6 -1
  24. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  25. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  26. package/dist/types/modes/components/model-selector.d.ts +2 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +5 -1
  28. package/dist/types/modes/components/skill-message.d.ts +1 -0
  29. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  30. package/dist/types/modes/components/status-line.d.ts +15 -3
  31. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  32. package/dist/types/modes/components/welcome.d.ts +9 -1
  33. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  34. package/dist/types/modes/controllers/input-controller.d.ts +6 -1
  35. package/dist/types/modes/controllers/selector-controller.d.ts +10 -1
  36. package/dist/types/modes/interactive-mode.d.ts +2 -0
  37. package/dist/types/modes/prompt-action-autocomplete.d.ts +3 -0
  38. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  39. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
  40. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  41. package/dist/types/modes/theme/theme.d.ts +1 -1
  42. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  43. package/dist/types/modes/types.d.ts +5 -1
  44. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  45. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  46. package/dist/types/notifications/html-format.d.ts +2 -0
  47. package/dist/types/notifications/recent-activity.d.ts +5 -1
  48. package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
  49. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  50. package/dist/types/notifications/telegram-daemon.d.ts +4 -10
  51. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  52. package/dist/types/notifications/threaded-render.d.ts +1 -0
  53. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  54. package/dist/types/runtime-mcp/config.d.ts +2 -0
  55. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  56. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  57. package/dist/types/runtime-mcp/types.d.ts +6 -0
  58. package/dist/types/sdk.d.ts +1 -1
  59. package/dist/types/skc-runtime/launch-tmux.d.ts +2 -2
  60. package/dist/types/skc-runtime/ralplan-runtime.d.ts +3 -2
  61. package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +1 -0
  62. package/dist/types/skc-runtime/session-state-sidecar.d.ts +5 -0
  63. package/dist/types/skc-runtime/state-writer.d.ts +1 -0
  64. package/dist/types/skc-runtime/team-runtime.d.ts +1 -1
  65. package/dist/types/skc-runtime/tmux-common.d.ts +12 -0
  66. package/dist/types/skc-runtime/ultragoal-guard.d.ts +7 -1
  67. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -4
  68. package/dist/types/slash-commands/types.d.ts +2 -0
  69. package/dist/types/system-prompt.d.ts +7 -0
  70. package/dist/types/tools/skill.d.ts +1 -1
  71. package/dist/types/utils/cmux-workspace.d.ts +52 -0
  72. package/dist/types/web/search/index.d.ts +4 -2
  73. package/dist/types/web/search/provider.d.ts +14 -0
  74. package/dist/types/web/search/providers/utils.d.ts +42 -13
  75. package/dist/types/web/search/render.d.ts +1 -0
  76. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  77. package/package.json +8 -8
  78. package/scripts/build-binary.ts +3 -0
  79. package/scripts/verify-insane-vendor.ts +0 -5
  80. package/src/capability/mcp.ts +2 -0
  81. package/src/cli/local-provider-smoke.ts +632 -0
  82. package/src/cli/notify-cli.ts +4 -2
  83. package/src/cli/telegram-cli.ts +156 -0
  84. package/src/cli/update-cli.ts +179 -13
  85. package/src/cli/web-search-cli.ts +5 -7
  86. package/src/cli.ts +20 -1
  87. package/src/commands/launch.ts +47 -2
  88. package/src/commands/local-provider.ts +62 -0
  89. package/src/commands/notify.ts +1 -1
  90. package/src/commands/ralplan.ts +1 -0
  91. package/src/commands/telegram.ts +47 -0
  92. package/src/config/keybindings.ts +15 -6
  93. package/src/config/mcp-schema.json +4 -0
  94. package/src/config/model-registry.ts +55 -1
  95. package/src/config/models-config-schema.ts +9 -0
  96. package/src/config/settings-schema.ts +256 -4
  97. package/src/config/settings.ts +11 -0
  98. package/src/config/telegram-autostart.ts +107 -0
  99. package/src/config/telegram-env-bridge.ts +92 -0
  100. package/src/coordinator-mcp/server.ts +276 -34
  101. package/src/defaults/skc/skills/ralplan/SKILL.md +29 -20
  102. package/src/defaults/skc/skills/ultragoal/SKILL.md +37 -112
  103. package/src/defaults/skc-defaults.ts +16 -0
  104. package/src/discovery/builtin.ts +12 -0
  105. package/src/discovery/cursor.ts +5 -84
  106. package/src/discovery/gemini.ts +3 -89
  107. package/src/discovery/index.ts +4 -1
  108. package/src/discovery/mcp-json.ts +11 -0
  109. package/src/discovery/opencode.ts +5 -122
  110. package/src/discovery/windsurf.ts +4 -82
  111. package/src/edit/read-file.ts +9 -1
  112. package/src/extensibility/extensions/runner.ts +2 -1
  113. package/src/extensibility/extensions/types.ts +11 -0
  114. package/src/extensibility/skills.ts +30 -2
  115. package/src/hooks/skill-state.ts +33 -73
  116. package/src/i18n/messages/de.ts +1 -0
  117. package/src/i18n/messages/en.ts +1 -0
  118. package/src/i18n/messages/es.ts +1 -0
  119. package/src/i18n/messages/fr.ts +1 -0
  120. package/src/i18n/messages/ja.settings.ts +28 -0
  121. package/src/i18n/messages/ja.ts +1 -0
  122. package/src/i18n/messages/ko.settings.ts +28 -0
  123. package/src/i18n/messages/ko.ts +1 -0
  124. package/src/i18n/messages/zh.settings.ts +28 -0
  125. package/src/i18n/messages/zh.ts +1 -0
  126. package/src/internal-urls/docs-index.generated.ts +12 -7
  127. package/src/main.ts +5 -2
  128. package/src/migrate/executor.ts +23 -6
  129. package/src/modes/acp/acp-agent.ts +7 -6
  130. package/src/modes/components/agent-dashboard.ts +3 -35
  131. package/src/modes/components/custom-editor.ts +26 -0
  132. package/src/modes/components/custom-model-preset-wizard.ts +30 -189
  133. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  134. package/src/modes/components/hook-selector.ts +21 -3
  135. package/src/modes/components/model-selector.ts +246 -11
  136. package/src/modes/components/session-observer-overlay.ts +3 -3
  137. package/src/modes/components/session-selector.ts +19 -9
  138. package/src/modes/components/settings-selector.ts +551 -11
  139. package/src/modes/components/skill-message.ts +18 -10
  140. package/src/modes/components/status-line/presets.ts +11 -0
  141. package/src/modes/components/status-line/segments.ts +16 -19
  142. package/src/modes/components/status-line/types.ts +6 -2
  143. package/src/modes/components/status-line.ts +281 -117
  144. package/src/modes/components/tree-selector.ts +28 -8
  145. package/src/modes/components/two-column-body.ts +36 -0
  146. package/src/modes/components/welcome.ts +165 -30
  147. package/src/modes/controllers/command-controller.ts +26 -2
  148. package/src/modes/controllers/event-controller.ts +102 -4
  149. package/src/modes/controllers/extension-ui-controller.ts +21 -2
  150. package/src/modes/controllers/input-controller.ts +214 -47
  151. package/src/modes/controllers/selector-controller.ts +35 -33
  152. package/src/modes/interactive-mode.ts +36 -30
  153. package/src/modes/prompt-action-autocomplete.ts +48 -8
  154. package/src/modes/rpc/rpc-types.ts +1 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +72 -15
  156. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  157. package/src/modes/slash-command-visibility.ts +9 -0
  158. package/src/modes/theme/theme.ts +5 -1
  159. package/src/modes/tmux-scroll.ts +92 -0
  160. package/src/modes/types.ts +8 -1
  161. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  162. package/src/modes/utils/terminal-bell.ts +48 -0
  163. package/src/modes/utils/ui-helpers.ts +76 -3
  164. package/src/notifications/daemon-paths.ts +22 -0
  165. package/src/notifications/html-format.ts +75 -1
  166. package/src/notifications/index.ts +32 -2
  167. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  168. package/src/notifications/recent-activity.ts +32 -8
  169. package/src/notifications/telegram-daemon-cli.ts +125 -13
  170. package/src/notifications/telegram-daemon-control.ts +9 -0
  171. package/src/notifications/telegram-daemon.ts +88 -46
  172. package/src/notifications/telegram-reference.ts +36 -14
  173. package/src/notifications/threaded-render.ts +14 -6
  174. package/src/prompts/agents/architect.md +7 -4
  175. package/src/prompts/agents/critic.md +15 -6
  176. package/src/prompts/agents/planner.md +8 -3
  177. package/src/prompts/system/system-prompt.md +20 -2
  178. package/src/prompts/tools/bash.md +1 -1
  179. package/src/runtime-mcp/config-writer.ts +28 -0
  180. package/src/runtime-mcp/config.ts +7 -0
  181. package/src/runtime-mcp/loader.ts +3 -0
  182. package/src/runtime-mcp/manager.ts +3 -0
  183. package/src/runtime-mcp/types.ts +6 -0
  184. package/src/sdk.ts +10 -11
  185. package/src/session/agent-session.ts +103 -13
  186. package/src/session/session-manager.ts +58 -18
  187. package/src/skc-runtime/launch-tmux.ts +34 -8
  188. package/src/skc-runtime/launch-worktree.ts +16 -2
  189. package/src/skc-runtime/ralplan-runtime.ts +22 -6
  190. package/src/skc-runtime/restricted-role-agent-bash.ts +1 -0
  191. package/src/skc-runtime/session-state-sidecar.ts +167 -24
  192. package/src/skc-runtime/state-runtime.ts +8 -5
  193. package/src/skc-runtime/state-writer.ts +5 -5
  194. package/src/skc-runtime/team-runtime.ts +21 -6
  195. package/src/skc-runtime/tmux-common.ts +16 -0
  196. package/src/skc-runtime/tmux-sessions.ts +13 -9
  197. package/src/skc-runtime/ultragoal-guard.ts +150 -24
  198. package/src/skc-runtime/ultragoal-runtime.ts +16 -124
  199. package/src/skc-runtime/workflow-manifest.generated.json +0 -8
  200. package/src/skc-runtime/workflow-manifest.ts +0 -1
  201. package/src/slash-commands/builtin-registry.ts +136 -23
  202. package/src/slash-commands/types.ts +2 -0
  203. package/src/system-prompt.ts +8 -0
  204. package/src/task/executor.ts +5 -0
  205. package/src/tools/ast-grep.ts +10 -7
  206. package/src/tools/bash.ts +39 -14
  207. package/src/tools/index.ts +8 -1
  208. package/src/tools/skill.ts +1 -1
  209. package/src/utils/cmux-workspace.ts +233 -0
  210. package/src/utils/title-generator.ts +2 -0
  211. package/src/web/kagi.ts +1 -1
  212. package/src/web/parallel.ts +2 -2
  213. package/src/web/search/index.ts +137 -52
  214. package/src/web/search/provider.ts +80 -0
  215. package/src/web/search/providers/anthropic.ts +1 -1
  216. package/src/web/search/providers/brave.ts +1 -1
  217. package/src/web/search/providers/codex.ts +1 -1
  218. package/src/web/search/providers/duckduckgo.ts +1 -1
  219. package/src/web/search/providers/exa.ts +1 -1
  220. package/src/web/search/providers/gemini.ts +7 -3
  221. package/src/web/search/providers/jina.ts +1 -1
  222. package/src/web/search/providers/kimi.ts +6 -1
  223. package/src/web/search/providers/openai-compatible.ts +1 -1
  224. package/src/web/search/providers/parallel.ts +1 -1
  225. package/src/web/search/providers/perplexity.ts +2 -2
  226. package/src/web/search/providers/searxng.ts +1 -1
  227. package/src/web/search/providers/synthetic.ts +1 -1
  228. package/src/web/search/providers/tavily.ts +1 -1
  229. package/src/web/search/providers/utils.ts +67 -17
  230. package/src/web/search/providers/xai.ts +1 -1
  231. package/src/web/search/providers/zai.ts +1 -1
  232. package/src/web/search/render.ts +3 -0
  233. package/src/workflow/workflow-intent-diff.ts +199 -0
  234. package/vendor/insane-search/MANIFEST.json +1 -3
  235. package/vendor/insane-search/engine/tests/test_u1.py +0 -28
  236. package/vendor/insane-search/engine/transport.py +2 -3
  237. package/vendor/insane-search/engine/validators.py +0 -14
  238. package/src/defaults/skc/rules/ponytail.md +0 -68
@@ -26,12 +26,14 @@ import { getPreset } from "./status-line/presets";
26
26
  import { renderSegment, type SegmentContext } from "./status-line/segments";
27
27
  import { getSeparator } from "./status-line/separators";
28
28
  import { calculateTokensPerSecond } from "./status-line/token-rate";
29
+ import type { SeparatorDef } from "./status-line/types";
29
30
 
30
31
  export interface StatusLineSegmentOptions {
31
32
  model?: { showThinkingLevel?: boolean };
32
33
  path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
33
34
  git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
34
35
  time?: { format?: "12h" | "24h"; showSeconds?: boolean };
36
+ usage?: { mode?: "used" | "remaining" };
35
37
  }
36
38
 
37
39
  export interface StatusLineSettings {
@@ -40,15 +42,34 @@ export interface StatusLineSettings {
40
42
  rightSegments?: StatusLineSegmentId[];
41
43
  separator?: StatusLineSeparatorStyle;
42
44
  segmentOptions?: StatusLineSegmentOptions;
45
+ previewHighlightSegment?: StatusLineSegmentId;
43
46
  showHookStatus?: boolean;
44
47
  showSkillHud?: boolean;
45
48
  sessionAccent?: boolean;
49
+ maxRows?: number;
46
50
  }
47
51
 
48
52
  export interface StatusLineComponentOptions {
49
53
  version?: string;
50
54
  }
51
55
 
56
+ interface CollectedStatusSegments {
57
+ ctx: SegmentContext;
58
+ separatorDef: SeparatorDef;
59
+ bgAnsi: string;
60
+ fgAnsi: string;
61
+ sepAnsi: string;
62
+ left: string[];
63
+ leftSegIds: StatusLineSegmentId[];
64
+ right: string[];
65
+ previewHighlightSegment: StatusLineSegmentId | undefined;
66
+ sessionAccent: boolean | undefined;
67
+ leftSepWidth: number;
68
+ rightSepWidth: number;
69
+ leftCapWidth: number;
70
+ rightCapWidth: number;
71
+ }
72
+
52
73
  // ═══════════════════════════════════════════════════════════════════════════
53
74
  // Per-message token cache
54
75
  // ═══════════════════════════════════════════════════════════════════════════
@@ -181,11 +202,8 @@ export class StatusLineComponent implements Component {
181
202
  #lastTokensPerSecond: number | null = null;
182
203
  #lastTokensPerSecondTimestamp: number | null = null;
183
204
 
184
- // Anthropic usage caching (5-min TTL, OAuth/sub only)
185
- #cachedUsage: {
186
- fiveHour?: { percent: number; resetMinutes?: number };
187
- sevenDay?: { percent: number; resetHours?: number };
188
- } | null = null;
205
+ // Provider usage caching (5-min TTL, OAuth/sub only)
206
+ #cachedUsage: SegmentContext["usage"] = null;
189
207
  #usageFetchedAt = 0;
190
208
  #usageInFlight = false;
191
209
  // Context breakdown — incremental cache. Replaces the previous 2-second
@@ -216,12 +234,13 @@ export class StatusLineComponent implements Component {
216
234
  showSkillHud: settings.get("statusLine.showSkillHud"),
217
235
  segmentOptions: settings.getGroup("statusLine").segmentOptions,
218
236
  sessionAccent: settings.get("statusLine.sessionAccent"),
237
+ maxRows: settings.get("statusLine.maxRows"),
219
238
  };
220
239
  this.#version = options.version?.trim() || undefined;
221
240
  }
222
241
 
223
242
  updateSettings(settings: StatusLineSettings): void {
224
- this.#settings = { ...this.#settings, ...settings };
243
+ this.#settings = { ...this.#settings, previewHighlightSegment: undefined, ...settings };
225
244
  }
226
245
 
227
246
  setAutoCompactEnabled(enabled: boolean): void {
@@ -462,9 +481,9 @@ export class StatusLineComponent implements Component {
462
481
  }
463
482
 
464
483
  /**
465
- * Background-refresh the Anthropic OAuth quota report. Guarded by a 5-min
466
- * TTL on both success (cache lifetime) and error (backoff). Exposed
467
- * (non-private) so unit tests can verify the backoff invariant.
484
+ * Background-refresh the OAuth quota report. Guarded by a 5-min TTL on both
485
+ * success (cache lifetime) and error (backoff). Exposed (non-private) so
486
+ * unit tests can verify the backoff invariant.
468
487
  */
469
488
  refreshUsageInBackground(): void {
470
489
  const now = Date.now();
@@ -478,6 +497,9 @@ export class StatusLineComponent implements Component {
478
497
  .then(reports => {
479
498
  this.#cachedUsage = this.#normalizeUsageReports(reports);
480
499
  this.#usageFetchedAt = Date.now();
500
+ if (this.#onBranchChange) {
501
+ this.#onBranchChange();
502
+ }
481
503
  })
482
504
  .catch(() => {
483
505
  // Backoff on error: stamp the fetch time so the 5-min TTL guard
@@ -491,47 +513,77 @@ export class StatusLineComponent implements Component {
491
513
  });
492
514
  }
493
515
 
494
- #normalizeUsageReports(reports: unknown): {
495
- fiveHour?: { percent: number; resetMinutes?: number };
496
- sevenDay?: { percent: number; resetHours?: number };
497
- } | null {
516
+ #normalizeUsageReports(reports: unknown): SegmentContext["usage"] {
498
517
  if (!Array.isArray(reports)) return null;
499
- let fiveHour: { percent: number; resetMinutes?: number } | undefined;
500
- let sevenDay: { percent: number; resetHours?: number } | undefined;
518
+ const windows: NonNullable<SegmentContext["usage"]>["windows"] = [];
519
+ const seen = new Set<string>();
501
520
  const now = Date.now();
521
+
522
+ const codexWindowLabel = (windowId: string | undefined, fallback: string): string => {
523
+ if (windowId && /^\d+[hd]$/.test(windowId)) return windowId;
524
+ return fallback;
525
+ };
526
+ const codexResetUnit = (label: string): "m" | "h" => (label.endsWith("d") ? "h" : "m");
527
+
528
+ const pushWindow = (
529
+ key: string,
530
+ label: string,
531
+ fraction: number,
532
+ resetsAt: number | undefined,
533
+ resetUnit: "m" | "h",
534
+ ) => {
535
+ if (seen.has(key)) return;
536
+ seen.add(key);
537
+ windows.push({
538
+ label,
539
+ percent: fraction * 100,
540
+ resetValue:
541
+ typeof resetsAt === "number"
542
+ ? Math.max(0, Math.round((resetsAt - now) / (resetUnit === "m" ? 60_000 : 3_600_000)))
543
+ : undefined,
544
+ resetUnit,
545
+ });
546
+ };
547
+
502
548
  for (const report of reports) {
503
549
  if (!report || typeof report !== "object") continue;
550
+ const provider = (report as { provider?: unknown }).provider;
551
+ const providerId = typeof provider === "string" ? provider : undefined;
504
552
  const limits = (report as { limits?: unknown }).limits;
505
553
  if (!Array.isArray(limits)) continue;
506
554
  for (const limit of limits) {
507
555
  if (!limit || typeof limit !== "object") continue;
508
556
  const l = limit as {
509
- scope?: { windowId?: string; tier?: string };
510
- window?: { resetsAt?: number };
557
+ id?: unknown;
558
+ scope?: { windowId?: string; tier?: string; modelId?: string };
559
+ window?: { id?: string; resetsAt?: number };
511
560
  amount?: { usedFraction?: number };
512
561
  };
513
562
  const fraction = l.amount?.usedFraction;
514
563
  if (typeof fraction !== "number") continue;
515
- const windowId = l.scope?.windowId;
564
+ const id = typeof l.id === "string" ? l.id : "";
565
+ const windowId = l.scope?.windowId ?? l.window?.id;
516
566
  const tier = l.scope?.tier;
567
+ const modelId = l.scope?.modelId;
517
568
  const resetsAt = l.window?.resetsAt;
518
- if (windowId === "5h" && !tier && !fiveHour) {
519
- fiveHour = {
520
- percent: fraction * 100,
521
- resetMinutes:
522
- typeof resetsAt === "number" ? Math.max(0, Math.round((resetsAt - now) / 60_000)) : undefined,
523
- };
524
- } else if (windowId === "7d" && !tier && !sevenDay) {
525
- sevenDay = {
526
- percent: fraction * 100,
527
- resetHours:
528
- typeof resetsAt === "number" ? Math.max(0, Math.round((resetsAt - now) / 3_600_000)) : undefined,
529
- };
569
+
570
+ if (providerId === "openai-codex") {
571
+ if (id === "openai-codex:primary" || (!id && !!windowId && windowId !== "7d" && !modelId)) {
572
+ const label = codexWindowLabel(windowId, "primary");
573
+ pushWindow("codex:primary", label, fraction, resetsAt, codexResetUnit(label));
574
+ } else if (id === "openai-codex:secondary" || (!id && windowId === "7d" && !modelId)) {
575
+ const label = codexWindowLabel(windowId, "secondary");
576
+ pushWindow("codex:secondary", label, fraction, resetsAt, codexResetUnit(label));
577
+ }
578
+ } else if (windowId === "5h" && !tier) {
579
+ pushWindow(`${providerId ?? "provider"}:5h`, "5h", fraction, resetsAt, "m");
580
+ } else if (windowId === "7d" && !tier) {
581
+ pushWindow(`${providerId ?? "provider"}:7d`, "7d", fraction, resetsAt, "h");
530
582
  }
531
583
  }
532
584
  }
533
- if (!fiveHour && !sevenDay) return null;
534
- return { fiveHour, sevenDay };
585
+
586
+ return windows.length > 0 ? { windows } : null;
535
587
  }
536
588
 
537
589
  /**
@@ -670,7 +722,70 @@ export class StatusLineComponent implements Component {
670
722
  };
671
723
  }
672
724
 
673
- #buildStatusLine(width: number): string {
725
+ #groupWidth(parts: string[], capWidth: number, sepWidth: number): number {
726
+ if (parts.length === 0) return 0;
727
+ const partsWidth = parts.reduce((sum, part) => sum + visibleWidth(part), 0);
728
+ const sepTotal = Math.max(0, parts.length - 1) * (sepWidth + 2);
729
+ return partsWidth + sepTotal + 2 + capWidth;
730
+ }
731
+
732
+ #renderStatusGroup(
733
+ parts: string[],
734
+ direction: "left" | "right",
735
+ separatorDef: SeparatorDef,
736
+ bgAnsi: string,
737
+ fgAnsi: string,
738
+ sepAnsi: string,
739
+ ): string {
740
+ if (parts.length === 0) return "";
741
+ const sep = direction === "left" ? separatorDef.left : separatorDef.right;
742
+ const cap = separatorDef.endCaps
743
+ ? direction === "left"
744
+ ? separatorDef.endCaps.right
745
+ : separatorDef.endCaps.left
746
+ : "";
747
+ const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : bgAnsi + sepAnsi;
748
+ const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
749
+
750
+ let content = bgAnsi + fgAnsi;
751
+ content += ` ${parts.join(` ${sepAnsi}${sep}${fgAnsi} `)} `;
752
+ content += "\x1b[0m";
753
+
754
+ if (capText) {
755
+ return direction === "right" ? capText + content : content + capText;
756
+ }
757
+ return content;
758
+ }
759
+
760
+ #shrinkPathToWidth(content: string, ctx: SegmentContext, shrinkBy: number): string | null {
761
+ const currentPathVW = visibleWidth(content);
762
+ const minPathVW = 8; // icon + ellipsis + a few chars
763
+ const shrinkable = currentPathVW - minPathVW;
764
+ if (shrinkable <= 0 || shrinkBy <= 0) return null;
765
+ const targetShrink = Math.min(shrinkable, shrinkBy);
766
+ const currentMaxLen = ctx.options.path?.maxLength ?? 40;
767
+ let newMaxLen = Math.max(4, Math.min(currentMaxLen, currentPathVW) - targetShrink);
768
+ const pathCtx = (maxLen: number): SegmentContext => ({
769
+ ...ctx,
770
+ options: { ...ctx.options, path: { ...ctx.options.path, maxLength: maxLen } },
771
+ });
772
+ let reRendered = renderSegment("path", pathCtx(newMaxLen));
773
+ if (!reRendered.visible || !reRendered.content) return null;
774
+ // maxLength governs path text, not icon prefix; iterate to compensate.
775
+ for (let i = 0; i < 8; i++) {
776
+ const saved = currentPathVW - visibleWidth(reRendered.content);
777
+ if (saved >= targetShrink) break;
778
+ const nextMaxLen = Math.max(4, newMaxLen - (targetShrink - saved));
779
+ if (nextMaxLen >= newMaxLen) break; // no progress or hit floor
780
+ newMaxLen = nextMaxLen;
781
+ const adjusted = renderSegment("path", pathCtx(newMaxLen));
782
+ if (!adjusted.visible || !adjusted.content) break;
783
+ reRendered = adjusted;
784
+ }
785
+ return reRendered.content;
786
+ }
787
+
788
+ #collectStatusSegments(width: number): CollectedStatusSegments {
674
789
  const ctx = this.#buildSegmentContext(width);
675
790
  const effectiveSettings = this.#resolveSettings();
676
791
  const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
@@ -683,22 +798,25 @@ export class StatusLineComponent implements Component {
683
798
  const fgAnsi = theme.getFgAnsi("text");
684
799
  const sepAnsi = theme.getFgAnsi("statusLineSep");
685
800
 
686
- // Collect visible segment contents
687
- const leftParts: string[] = [];
801
+ const previewHighlightSegment = effectiveSettings.previewHighlightSegment;
802
+ const highlightSegment = (segId: StatusLineSegmentId, content: string): string =>
803
+ previewHighlightSegment === segId ? `\x1b[7m${content}\x1b[27m` : content;
804
+
805
+ const left: string[] = [];
688
806
  const leftSegIds: StatusLineSegmentId[] = [];
689
807
  for (const segId of effectiveSettings.leftSegments) {
690
808
  const rendered = renderSegment(segId, ctx);
691
809
  if (rendered.visible && rendered.content) {
692
- leftParts.push(rendered.content);
810
+ left.push(highlightSegment(segId, rendered.content));
693
811
  leftSegIds.push(segId);
694
812
  }
695
813
  }
696
814
 
697
- const rightParts: string[] = [];
815
+ const right: string[] = [];
698
816
  for (const segId of effectiveSettings.rightSegments) {
699
817
  const rendered = renderSegment(segId, ctx);
700
818
  if (rendered.visible && rendered.content) {
701
- rightParts.push(rendered.content);
819
+ right.push(highlightSegment(segId, rendered.content));
702
820
  }
703
821
  }
704
822
 
@@ -707,116 +825,152 @@ export class StatusLineComponent implements Component {
707
825
  if (runningBackgroundJobs > 0) {
708
826
  const icon = theme.icon.agents ? `${theme.icon.agents} ` : "";
709
827
  const label = `${formatCount("job", runningBackgroundJobs)} running`;
710
- rightParts.push(theme.fg("statusLineSubagents", `${icon}${label}`));
828
+ right.push(theme.fg("statusLineSubagents", `${icon}${label}`));
711
829
  }
712
830
  if (this.#version) {
713
- rightParts.push(theme.fg("dim", `v${this.#version}`));
831
+ right.push(theme.fg("dim", `v${this.#version}`));
714
832
  }
715
- const topFillWidth = Math.max(0, width);
716
- const left = [...leftParts];
717
- const right = [...rightParts];
718
-
719
- const leftSepWidth = visibleWidth(separatorDef.left);
720
- const rightSepWidth = visibleWidth(separatorDef.right);
721
- const leftCapWidth = separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.right) : 0;
722
- const rightCapWidth = separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.left) : 0;
723
-
724
- const groupWidth = (parts: string[], capWidth: number, sepWidth: number): number => {
725
- if (parts.length === 0) return 0;
726
- const partsWidth = parts.reduce((sum, part) => sum + visibleWidth(part), 0);
727
- const sepTotal = Math.max(0, parts.length - 1) * (sepWidth + 2);
728
- return partsWidth + sepTotal + 2 + capWidth;
833
+
834
+ return {
835
+ ctx,
836
+ separatorDef,
837
+ bgAnsi,
838
+ fgAnsi,
839
+ sepAnsi,
840
+ left,
841
+ leftSegIds,
842
+ right,
843
+ previewHighlightSegment,
844
+ sessionAccent: effectiveSettings.sessionAccent,
845
+ leftSepWidth: visibleWidth(separatorDef.left),
846
+ rightSepWidth: visibleWidth(separatorDef.right),
847
+ leftCapWidth: separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.right) : 0,
848
+ rightCapWidth: separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.left) : 0,
729
849
  };
850
+ }
851
+
852
+ #resolveMaxRows(): number {
853
+ const raw = this.#settings.maxRows ?? 1;
854
+ if (!Number.isFinite(raw)) return 1;
855
+ return Math.max(1, Math.min(3, Math.trunc(raw)));
856
+ }
857
+
858
+ #buildStatusLine(width: number, precollected?: CollectedStatusSegments): string {
859
+ const seg = precollected ?? this.#collectStatusSegments(width);
860
+ const { ctx, separatorDef, bgAnsi, fgAnsi, sepAnsi, previewHighlightSegment } = seg;
861
+ const { leftSepWidth, rightSepWidth, leftCapWidth, rightCapWidth } = seg;
862
+ const left = [...seg.left];
863
+ const leftIds = [...seg.leftSegIds];
864
+ const right = [...seg.right];
865
+ const topFillWidth = Math.max(0, width);
730
866
 
731
- let leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
732
- let rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
867
+ let leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
868
+ let rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
733
869
  const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
734
870
 
735
871
  if (topFillWidth > 0) {
736
872
  while (totalWidth() > topFillWidth && right.length > 0) {
737
873
  right.pop();
738
- rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
874
+ rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
739
875
  }
740
876
  // Shrink path before dropping left segments — path is the only elastic segment
741
- const pathIdx = leftSegIds.indexOf("path");
877
+ const pathIdx = leftIds.indexOf("path");
742
878
  if (pathIdx >= 0 && totalWidth() > topFillWidth) {
743
879
  const overflow = totalWidth() - topFillWidth;
744
- const currentPathVW = visibleWidth(left[pathIdx]);
745
- const minPathVW = 8; // icon + ellipsis + a few chars
746
- const shrinkable = currentPathVW - minPathVW;
747
- if (shrinkable > 0) {
748
- const shrinkBy = Math.min(shrinkable, overflow);
749
- const currentMaxLen = ctx.options.path?.maxLength ?? 40;
750
- let newMaxLen = Math.max(4, Math.min(currentMaxLen, currentPathVW) - shrinkBy);
751
- const pathCtx = (maxLen: number): SegmentContext => ({
752
- ...ctx,
753
- options: { ...ctx.options, path: { ...ctx.options.path, maxLength: maxLen } },
754
- });
755
- let reRendered = renderSegment("path", pathCtx(newMaxLen));
756
- if (reRendered.visible && reRendered.content) {
757
- // maxLength governs path text, not icon prefix; iterate to compensate
758
- for (let i = 0; i < 8; i++) {
759
- const saved = currentPathVW - visibleWidth(reRendered.content);
760
- if (saved >= shrinkBy) break;
761
- const nextMaxLen = Math.max(4, newMaxLen - (shrinkBy - saved));
762
- if (nextMaxLen >= newMaxLen) break; // no progress or hit floor
763
- newMaxLen = nextMaxLen;
764
- const adjusted = renderSegment("path", pathCtx(newMaxLen));
765
- if (!adjusted.visible || !adjusted.content) break;
766
- reRendered = adjusted;
767
- }
768
- left[pathIdx] = reRendered.content;
769
- leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
770
- }
880
+ const shrunk = this.#shrinkPathToWidth(left[pathIdx], ctx, overflow);
881
+ if (shrunk !== null) {
882
+ left[pathIdx] = previewHighlightSegment === "path" ? `\x1b[7m${shrunk}\x1b[27m` : shrunk;
883
+ leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
771
884
  }
772
885
  }
773
886
  while (totalWidth() > topFillWidth && left.length > 0) {
774
887
  left.pop();
775
- leftSegIds.pop();
776
- leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
888
+ leftIds.pop();
889
+ leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
777
890
  }
778
891
  }
779
892
 
780
- const renderGroup = (parts: string[], direction: "left" | "right"): string => {
781
- if (parts.length === 0) return "";
782
- const sep = direction === "left" ? separatorDef.left : separatorDef.right;
783
- const cap = separatorDef.endCaps
784
- ? direction === "left"
785
- ? separatorDef.endCaps.right
786
- : separatorDef.endCaps.left
787
- : "";
788
- const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : bgAnsi + sepAnsi;
789
- const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
790
-
791
- let content = bgAnsi + fgAnsi;
792
- content += ` ${parts.join(` ${sepAnsi}${sep}${fgAnsi} `)} `;
793
- content += "\x1b[0m";
794
-
795
- if (capText) {
796
- return direction === "right" ? capText + content : content + capText;
797
- }
798
- return content;
799
- };
800
-
801
- const leftGroup = renderGroup(left, "left");
802
- const rightGroup = renderGroup(right, "right");
893
+ const leftGroup = this.#renderStatusGroup(left, "left", separatorDef, bgAnsi, fgAnsi, sepAnsi);
894
+ const rightGroup = this.#renderStatusGroup(right, "right", separatorDef, bgAnsi, fgAnsi, sepAnsi);
803
895
  if (!leftGroup && !rightGroup) return "";
804
896
 
805
897
  if (topFillWidth === 0 || left.length === 0 || right.length === 0) {
806
898
  return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup;
807
899
  }
808
900
 
809
- leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
810
- rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
901
+ leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
902
+ rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
811
903
  const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
812
- const sessionName =
813
- effectiveSettings.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
904
+ const sessionName = seg.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
814
905
  const accentHex = sessionName ? getSessionAccentHex(sessionName) : undefined;
815
906
  const gapColor = getSessionAccentAnsi(accentHex) ?? theme.getFgAnsi("border");
816
907
  const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[39m`;
817
908
  return leftGroup + gapFill + rightGroup;
818
909
  }
819
910
 
911
+ /**
912
+ * Multi-row status line. When `maxRows > 1` and the single-line layout would
913
+ * overflow, segments wrap onto additional left-justified rows instead of
914
+ * being dropped. Falls back to the polished justified single row whenever
915
+ * everything fits on one line.
916
+ */
917
+ #buildStatusRows(width: number, maxRows: number): string[] {
918
+ const seg = this.#collectStatusSegments(width);
919
+ if (seg.left.length === 0 && seg.right.length === 0) return [];
920
+
921
+ const topFillWidth = Math.max(1, width);
922
+ const leftWidth = this.#groupWidth(seg.left, seg.leftCapWidth, seg.leftSepWidth);
923
+ const rightWidth = this.#groupWidth(seg.right, seg.rightCapWidth, seg.rightSepWidth);
924
+ const gap = seg.left.length > 0 && seg.right.length > 0 ? 1 : 0;
925
+ const fitsSingleRow = leftWidth + rightWidth + gap <= topFillWidth;
926
+
927
+ if (maxRows <= 1 || fitsSingleRow) {
928
+ 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;
945
+ }
946
+ }
947
+
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;
955
+ }
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;
963
+ }
964
+ }
965
+ if (rows.length < maxRows && current.length > 0) {
966
+ rows.push(current);
967
+ }
968
+
969
+ return rows.map(row =>
970
+ this.#renderStatusGroup(row, "left", seg.separatorDef, seg.bgAnsi, seg.fgAnsi, seg.sepAnsi),
971
+ );
972
+ }
973
+
820
974
  getTopBorder(width: number): { content: string; width: number } {
821
975
  const content = this.#buildStatusLine(width);
822
976
  return {
@@ -825,6 +979,16 @@ export class StatusLineComponent implements Component {
825
979
  };
826
980
  }
827
981
 
982
+ /**
983
+ * Multi-row-aware content for the settings preview: the wrapped rows joined
984
+ * with newlines so a single `Text` renders them stacked. Honors the current
985
+ * `maxRows`; identical to the single status line when `maxRows` is 1 or when
986
+ * everything fits on one row.
987
+ */
988
+ getPreviewContent(width: number): string {
989
+ return this.#buildStatusRows(width, this.#resolveMaxRows()).join("\n");
990
+ }
991
+
828
992
  render(width: number): string[] {
829
993
  const lines: string[] = [];
830
994
  this.#refreshSkillHudInBackground();
@@ -833,9 +997,9 @@ export class StatusLineComponent implements Component {
833
997
  lines.push(skillHud);
834
998
  }
835
999
 
836
- const statusLine = this.#buildStatusLine(width);
837
- if (statusLine) {
838
- lines.push(truncateToWidth(statusLine, width));
1000
+ const statusRows = this.#buildStatusRows(width, this.#resolveMaxRows());
1001
+ for (const statusRow of statusRows) {
1002
+ if (statusRow) lines.push(truncateToWidth(statusRow, width));
839
1003
  }
840
1004
 
841
1005
  const showHooks = this.#settings.showHookStatus ?? true;
@@ -135,6 +135,14 @@ class TreeList implements Component {
135
135
  return this.#filteredNodes.length - 1;
136
136
  }
137
137
 
138
+ #clampSelectedIndex(): void {
139
+ if (this.#filteredNodes.length === 0) {
140
+ this.#selectedIndex = 0;
141
+ return;
142
+ }
143
+ this.#selectedIndex = Math.max(0, Math.min(this.#selectedIndex, this.#filteredNodes.length - 1));
144
+ }
145
+
138
146
  #flattenTree(roots: SessionTreeNode[]): FlatNode[] {
139
147
  const result: FlatNode[] = [];
140
148
  this.#toolCallMap.clear();
@@ -333,12 +341,11 @@ class TreeList implements Component {
333
341
  return true;
334
342
  });
335
343
 
336
- // Try to preserve cursor on the same node, or find nearest visible ancestor
344
+ // Try to preserve cursor on the same node, or keep the index in range.
337
345
  if (this.#lastSelectedId) {
338
346
  this.#selectedIndex = this.#findNearestVisibleIndex(this.#lastSelectedId);
339
- } else if (this.#selectedIndex >= this.#filteredNodes.length) {
340
- // Clamp index if out of bounds
341
- this.#selectedIndex = Math.max(0, this.#filteredNodes.length - 1);
347
+ } else {
348
+ this.#clampSelectedIndex();
342
349
  }
343
350
 
344
351
  // Update lastSelectedId to the actual selection (may have changed due to parent walk)
@@ -710,16 +717,29 @@ class TreeList implements Component {
710
717
  }
711
718
 
712
719
  handleInput(keyData: string): void {
720
+ const moveSelection = (delta: number, wrap: boolean): void => {
721
+ if (this.#filteredNodes.length === 0) return;
722
+ if (wrap) {
723
+ const next = this.#selectedIndex + delta;
724
+ this.#selectedIndex =
725
+ next < 0 ? this.#filteredNodes.length - 1 : next >= this.#filteredNodes.length ? 0 : next;
726
+ } else {
727
+ this.#selectedIndex += delta;
728
+ this.#clampSelectedIndex();
729
+ }
730
+ this.#lastSelectedId = this.#filteredNodes[this.#selectedIndex]?.node.entry.id ?? this.#lastSelectedId;
731
+ };
732
+
713
733
  if (matchesKey(keyData, "up")) {
714
- this.#selectedIndex = this.#selectedIndex === 0 ? this.#filteredNodes.length - 1 : this.#selectedIndex - 1;
734
+ moveSelection(-1, true);
715
735
  } else if (matchesKey(keyData, "down")) {
716
- this.#selectedIndex = this.#selectedIndex === this.#filteredNodes.length - 1 ? 0 : this.#selectedIndex + 1;
736
+ moveSelection(1, true);
717
737
  } else if (matchesKey(keyData, "left")) {
718
738
  // Page up
719
- this.#selectedIndex = Math.max(0, this.#selectedIndex - this.maxVisibleLines);
739
+ moveSelection(-this.maxVisibleLines, false);
720
740
  } else if (matchesKey(keyData, "right")) {
721
741
  // Page down
722
- this.#selectedIndex = Math.min(this.#filteredNodes.length - 1, this.#selectedIndex + this.maxVisibleLines);
742
+ moveSelection(this.maxVisibleLines, false);
723
743
  } else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
724
744
  const selected = this.#filteredNodes[this.#selectedIndex];
725
745
  if (selected && this.onSelect) {
@@ -0,0 +1,36 @@
1
+ import { type Component, padding, truncateToWidth, visibleWidth } from "@sayknow-cli/tui";
2
+ import { theme } from "../theme/theme";
3
+
4
+ export class TwoColumnBody implements Component {
5
+ constructor(
6
+ private readonly leftPane: Component,
7
+ private readonly rightPane: Component,
8
+ private readonly maxHeight: number,
9
+ ) {}
10
+
11
+ render(width: number): string[] {
12
+ const leftWidth = Math.floor(width * 0.5);
13
+ const separatorText = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
14
+ const separatorWidth = width - leftWidth > 3 ? visibleWidth(separatorText) : 0;
15
+ const separator = separatorWidth > 0 ? separatorText : "";
16
+ const rightWidth = Math.max(0, width - leftWidth - separatorWidth);
17
+ const leftLines = this.leftPane.render(leftWidth);
18
+ const rightLines = this.rightPane.render(rightWidth);
19
+ const lineCount = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
20
+ const out: string[] = [];
21
+
22
+ for (let i = 0; i < lineCount; i++) {
23
+ const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
24
+ const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
25
+ const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
26
+ out.push(leftPadded + separator + right);
27
+ }
28
+
29
+ return out;
30
+ }
31
+
32
+ invalidate(): void {
33
+ this.leftPane.invalidate?.();
34
+ this.rightPane.invalidate?.();
35
+ }
36
+ }