apex-code 0.0.1-alpha.7 → 0.0.1-alpha.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 (256) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +5 -0
  3. package/dist/cli/args.d.ts +3 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +15 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/concurrent-session.d.ts +3 -0
  8. package/dist/cli/concurrent-session.d.ts.map +1 -0
  9. package/dist/cli/concurrent-session.js +13 -0
  10. package/dist/cli/concurrent-session.js.map +1 -0
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +1 -2
  13. package/dist/cli.js.map +1 -1
  14. package/dist/core/agent-session-services.d.ts.map +1 -1
  15. package/dist/core/agent-session-services.js +20 -0
  16. package/dist/core/agent-session-services.js.map +1 -1
  17. package/dist/core/agent-session.d.ts +15 -2
  18. package/dist/core/agent-session.d.ts.map +1 -1
  19. package/dist/core/agent-session.js +9 -6
  20. package/dist/core/agent-session.js.map +1 -1
  21. package/dist/core/auth-storage.d.ts +4 -1
  22. package/dist/core/auth-storage.d.ts.map +1 -1
  23. package/dist/core/auth-storage.js +6 -2
  24. package/dist/core/auth-storage.js.map +1 -1
  25. package/dist/core/bash-executor.d.ts.map +1 -1
  26. package/dist/core/bash-executor.js +10 -9
  27. package/dist/core/bash-executor.js.map +1 -1
  28. package/dist/core/experimental.d.ts +4 -0
  29. package/dist/core/experimental.d.ts.map +1 -1
  30. package/dist/core/experimental.js +4 -0
  31. package/dist/core/experimental.js.map +1 -1
  32. package/dist/core/extensions/types.d.ts +4 -0
  33. package/dist/core/extensions/types.d.ts.map +1 -1
  34. package/dist/core/extensions/types.js.map +1 -1
  35. package/dist/core/http-dispatcher.d.ts +1 -2
  36. package/dist/core/http-dispatcher.d.ts.map +1 -1
  37. package/dist/core/http-dispatcher.js +2 -17
  38. package/dist/core/http-dispatcher.js.map +1 -1
  39. package/dist/core/http-idle-timeout.d.ts +12 -0
  40. package/dist/core/http-idle-timeout.d.ts.map +1 -0
  41. package/dist/core/http-idle-timeout.js +27 -0
  42. package/dist/core/http-idle-timeout.js.map +1 -0
  43. package/dist/core/keybindings.d.ts +24 -0
  44. package/dist/core/keybindings.d.ts.map +1 -1
  45. package/dist/core/lsp/client.d.ts +2 -0
  46. package/dist/core/lsp/client.d.ts.map +1 -1
  47. package/dist/core/lsp/client.js +27 -2
  48. package/dist/core/lsp/client.js.map +1 -1
  49. package/dist/core/model-config.d.ts +5 -0
  50. package/dist/core/model-config.d.ts.map +1 -1
  51. package/dist/core/model-config.js +1 -0
  52. package/dist/core/model-config.js.map +1 -1
  53. package/dist/core/sandbox/child-entry.d.ts.map +1 -1
  54. package/dist/core/sandbox/child-entry.js +8 -1
  55. package/dist/core/sandbox/child-entry.js.map +1 -1
  56. package/dist/core/sandbox/cli-launch.d.ts +3 -0
  57. package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
  58. package/dist/core/sandbox/cli-launch.js +25 -4
  59. package/dist/core/sandbox/cli-launch.js.map +1 -1
  60. package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
  61. package/dist/core/sandbox/cli-supervisor.js +53 -13
  62. package/dist/core/sandbox/cli-supervisor.js.map +1 -1
  63. package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
  64. package/dist/core/sandbox/linux-backend.js +15 -0
  65. package/dist/core/sandbox/linux-backend.js.map +1 -1
  66. package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
  67. package/dist/core/sandbox/macos-backend.js +24 -1
  68. package/dist/core/sandbox/macos-backend.js.map +1 -1
  69. package/dist/core/sandbox/rpc/credential-client.d.ts +20 -0
  70. package/dist/core/sandbox/rpc/credential-client.d.ts.map +1 -0
  71. package/dist/core/sandbox/rpc/credential-client.js +170 -0
  72. package/dist/core/sandbox/rpc/credential-client.js.map +1 -0
  73. package/dist/core/sandbox/rpc/credential-proxy.d.ts +44 -0
  74. package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -0
  75. package/dist/core/sandbox/rpc/credential-proxy.js +381 -0
  76. package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -0
  77. package/dist/core/sandbox/supervisor.d.ts +11 -0
  78. package/dist/core/sandbox/supervisor.d.ts.map +1 -1
  79. package/dist/core/sandbox/supervisor.js.map +1 -1
  80. package/dist/core/sandbox/terminal-size.d.ts +40 -0
  81. package/dist/core/sandbox/terminal-size.d.ts.map +1 -0
  82. package/dist/core/sandbox/terminal-size.js +105 -0
  83. package/dist/core/sandbox/terminal-size.js.map +1 -0
  84. package/dist/core/sdk.d.ts +6 -3
  85. package/dist/core/sdk.d.ts.map +1 -1
  86. package/dist/core/sdk.js +16 -4
  87. package/dist/core/sdk.js.map +1 -1
  88. package/dist/core/session-lease.d.ts +31 -0
  89. package/dist/core/session-lease.d.ts.map +1 -0
  90. package/dist/core/session-lease.js +138 -0
  91. package/dist/core/session-lease.js.map +1 -0
  92. package/dist/core/session-manager.d.ts +5 -0
  93. package/dist/core/session-manager.d.ts.map +1 -1
  94. package/dist/core/session-manager.js +3 -3
  95. package/dist/core/session-manager.js.map +1 -1
  96. package/dist/core/settings-manager.d.ts +26 -0
  97. package/dist/core/settings-manager.d.ts.map +1 -1
  98. package/dist/core/settings-manager.js +42 -3
  99. package/dist/core/settings-manager.js.map +1 -1
  100. package/dist/core/slash-commands.d.ts.map +1 -1
  101. package/dist/core/slash-commands.js +2 -0
  102. package/dist/core/slash-commands.js.map +1 -1
  103. package/dist/core/system-prompt.d.ts +2 -2
  104. package/dist/core/system-prompt.d.ts.map +1 -1
  105. package/dist/core/system-prompt.js +47 -62
  106. package/dist/core/system-prompt.js.map +1 -1
  107. package/dist/core/tools/bash.d.ts.map +1 -1
  108. package/dist/core/tools/bash.js +2 -0
  109. package/dist/core/tools/bash.js.map +1 -1
  110. package/dist/core/tools/delegate.d.ts.map +1 -1
  111. package/dist/core/tools/delegate.js +24 -0
  112. package/dist/core/tools/delegate.js.map +1 -1
  113. package/dist/core/tools/edit.d.ts.map +1 -1
  114. package/dist/core/tools/edit.js +2 -0
  115. package/dist/core/tools/edit.js.map +1 -1
  116. package/dist/core/tools/find.d.ts.map +1 -1
  117. package/dist/core/tools/find.js +1 -1
  118. package/dist/core/tools/find.js.map +1 -1
  119. package/dist/core/tools/grep.d.ts +2 -0
  120. package/dist/core/tools/grep.d.ts.map +1 -1
  121. package/dist/core/tools/grep.js +35 -10
  122. package/dist/core/tools/grep.js.map +1 -1
  123. package/dist/core/tools/ls.d.ts +2 -0
  124. package/dist/core/tools/ls.d.ts.map +1 -1
  125. package/dist/core/tools/ls.js +5 -1
  126. package/dist/core/tools/ls.js.map +1 -1
  127. package/dist/core/tools/read.d.ts.map +1 -1
  128. package/dist/core/tools/read.js +2 -0
  129. package/dist/core/tools/read.js.map +1 -1
  130. package/dist/core/tools/web-search-exa.d.ts +33 -0
  131. package/dist/core/tools/web-search-exa.d.ts.map +1 -0
  132. package/dist/core/tools/web-search-exa.js +128 -0
  133. package/dist/core/tools/web-search-exa.js.map +1 -0
  134. package/dist/core/tools/write.d.ts.map +1 -1
  135. package/dist/core/tools/write.js +2 -0
  136. package/dist/core/tools/write.js.map +1 -1
  137. package/dist/core/web-search-provider.d.ts +64 -0
  138. package/dist/core/web-search-provider.d.ts.map +1 -0
  139. package/dist/core/web-search-provider.js +149 -0
  140. package/dist/core/web-search-provider.js.map +1 -0
  141. package/dist/index.d.ts +1 -1
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js.map +1 -1
  144. package/dist/main.d.ts +1 -0
  145. package/dist/main.d.ts.map +1 -1
  146. package/dist/main.js +20 -1
  147. package/dist/main.js.map +1 -1
  148. package/dist/modes/interactive/components/custom-editor.d.ts +23 -0
  149. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  150. package/dist/modes/interactive/components/custom-editor.js +105 -2
  151. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  152. package/dist/modes/interactive/components/dynamic-border.d.ts +7 -1
  153. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  154. package/dist/modes/interactive/components/dynamic-border.js +11 -2
  155. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  156. package/dist/modes/interactive/components/extension-selector.d.ts +4 -0
  157. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  158. package/dist/modes/interactive/components/extension-selector.js +24 -6
  159. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  160. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  161. package/dist/modes/interactive/components/first-time-setup.js +6 -3
  162. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  163. package/dist/modes/interactive/components/first-use-hints.d.ts +8 -0
  164. package/dist/modes/interactive/components/first-use-hints.d.ts.map +1 -0
  165. package/dist/modes/interactive/components/first-use-hints.js +22 -0
  166. package/dist/modes/interactive/components/first-use-hints.js.map +1 -0
  167. package/dist/modes/interactive/components/footer.d.ts +12 -0
  168. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  169. package/dist/modes/interactive/components/footer.js +128 -9
  170. package/dist/modes/interactive/components/footer.js.map +1 -1
  171. package/dist/modes/interactive/components/index.d.ts +1 -0
  172. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  173. package/dist/modes/interactive/components/index.js +1 -0
  174. package/dist/modes/interactive/components/index.js.map +1 -1
  175. package/dist/modes/interactive/components/model-selector.d.ts +2 -0
  176. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  177. package/dist/modes/interactive/components/model-selector.js +20 -5
  178. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  179. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  180. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  181. package/dist/modes/interactive/components/settings-selector.js +10 -0
  182. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  183. package/dist/modes/interactive/components/splash-header.d.ts +29 -4
  184. package/dist/modes/interactive/components/splash-header.d.ts.map +1 -1
  185. package/dist/modes/interactive/components/splash-header.js +58 -23
  186. package/dist/modes/interactive/components/splash-header.js.map +1 -1
  187. package/dist/modes/interactive/components/tool-execution.d.ts +14 -0
  188. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  189. package/dist/modes/interactive/components/tool-execution.js +113 -35
  190. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  191. package/dist/modes/interactive/components/tool-panel.d.ts +30 -0
  192. package/dist/modes/interactive/components/tool-panel.d.ts.map +1 -0
  193. package/dist/modes/interactive/components/tool-panel.js +130 -0
  194. package/dist/modes/interactive/components/tool-panel.js.map +1 -0
  195. package/dist/modes/interactive/interactive-mode.d.ts +44 -9
  196. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  197. package/dist/modes/interactive/interactive-mode.js +282 -70
  198. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  199. package/dist/modes/interactive/model-catalog-refresh.d.ts +7 -0
  200. package/dist/modes/interactive/model-catalog-refresh.d.ts.map +1 -0
  201. package/dist/modes/interactive/model-catalog-refresh.js +34 -0
  202. package/dist/modes/interactive/model-catalog-refresh.js.map +1 -0
  203. package/dist/modes/interactive/theme/apex.json +62 -63
  204. package/dist/modes/interactive/theme/dark.json +2 -0
  205. package/dist/modes/interactive/theme/light.json +2 -0
  206. package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
  207. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  208. package/dist/modes/interactive/theme/theme-controller.js +25 -11
  209. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  210. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  211. package/dist/modes/interactive/theme/theme.d.ts +14 -3
  212. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  213. package/dist/modes/interactive/theme/theme.js +55 -6
  214. package/dist/modes/interactive/theme/theme.js.map +1 -1
  215. package/dist/modes/json-event.d.ts +4 -1
  216. package/dist/modes/json-event.d.ts.map +1 -1
  217. package/dist/modes/json-event.js +5 -2
  218. package/dist/modes/json-event.js.map +1 -1
  219. package/dist/server/create-harness.d.ts.map +1 -1
  220. package/dist/server/create-harness.js +2 -0
  221. package/dist/server/create-harness.js.map +1 -1
  222. package/dist/themes/apex-logo.d.ts +36 -16
  223. package/dist/themes/apex-logo.d.ts.map +1 -1
  224. package/dist/themes/apex-logo.js +32 -28
  225. package/dist/themes/apex-logo.js.map +1 -1
  226. package/dist/utils/tools-manager.d.ts +10 -1
  227. package/dist/utils/tools-manager.d.ts.map +1 -1
  228. package/dist/utils/tools-manager.js +15 -20
  229. package/dist/utils/tools-manager.js.map +1 -1
  230. package/docs/compaction.md +3 -3
  231. package/docs/custom-provider.md +1 -1
  232. package/docs/environment-variables.md +1 -0
  233. package/docs/extensions.md +4 -0
  234. package/docs/json.md +6 -4
  235. package/docs/keybindings.md +8 -2
  236. package/docs/rpc.md +15 -4
  237. package/docs/sdk.md +9 -0
  238. package/docs/settings.md +63 -0
  239. package/docs/terminal-setup.md +23 -2
  240. package/docs/themes.md +24 -3
  241. package/docs/tui.md +2 -2
  242. package/docs/usage.md +3 -2
  243. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  244. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  245. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  246. package/examples/extensions/gondolin/package-lock.json +2 -2
  247. package/examples/extensions/gondolin/package.json +1 -1
  248. package/examples/extensions/sandbox/package-lock.json +2 -2
  249. package/examples/extensions/sandbox/package.json +1 -1
  250. package/examples/extensions/subagent/README.md +2 -0
  251. package/examples/extensions/subagent/agents.ts +40 -9
  252. package/examples/extensions/subagent/index.ts +21 -3
  253. package/examples/extensions/with-deps/package-lock.json +2 -2
  254. package/examples/extensions/with-deps/package.json +1 -1
  255. package/npm-shrinkwrap.json +27 -78
  256. package/package.json +6 -6
@@ -1,4 +1,5 @@
1
1
  import { type Component } from "@earendil-works/pi-tui";
2
+ import { type BrandMark, type MarkRow, type MarkSymbolPreset } from "../../../themes/apex-logo.ts";
2
3
  /** One `label value` row in the metadata column. */
3
4
  export interface SplashHeaderMetadataLine {
4
5
  label: string;
@@ -7,23 +8,48 @@ export interface SplashHeaderMetadataLine {
7
8
  export interface ApexSplashHeaderOptions {
8
9
  /** Rows appended after `cwd`, for extensions that want to contribute context. */
9
10
  getExtraMetadata?: () => readonly SplashHeaderMetadataLine[];
11
+ /**
12
+ * One line of counted totals, rendered between the rules under the mark.
13
+ * The header does not know how to count anything; the caller supplies the
14
+ * finished line so the resource loader stays on its own side of the seam.
15
+ */
16
+ getInventory?: () => string | undefined;
17
+ /**
18
+ * Affordance appended to the inventory line, e.g. the command that shows the
19
+ * detail. Dropped whole rather than clipped, so it never renders as a stub.
20
+ */
21
+ inventoryHint?: string;
10
22
  /** Hint rendered under the metadata block. Return undefined for none. */
11
23
  getHint?: () => string | undefined;
24
+ /**
25
+ * One row of entry-point sigils under the hint, already coloured by the
26
+ * caller. The header does not know the keybindings, so it does not build it.
27
+ */
28
+ getShortcuts?: () => string | undefined;
12
29
  /** Resolves `terminal.symbolPreset`. Defaults to unicode. */
13
- getSymbolPreset?: () => "unicode" | "ascii";
30
+ getSymbolPreset?: () => MarkSymbolPreset;
14
31
  /** Emit a leading blank line above the mark. */
15
32
  topPadding?: boolean;
16
33
  /** Override the mark. Used by tests and by extensions that rebrand. */
17
34
  logo?: string;
18
35
  }
19
36
  /**
20
- * Startup header: brand mark on the left, runtime metadata on the right.
37
+ * Paint one mark row: accent run then text run.
38
+ *
39
+ * Empty runs are skipped so no row carries a bare, unclosed SGR pair.
40
+ */
41
+ export declare function paintMarkRow(row: MarkRow): string;
42
+ /** The whole mark as painted lines, for surfaces that just want to stamp it. */
43
+ export declare function paintBrandMark(brandMark: BrandMark): string[];
44
+ /**
45
+ * Startup header: brand mark on the left, runtime metadata on the right, then a
46
+ * ruled band carrying the counted inventory.
21
47
  *
22
48
  * Values are read through getters rather than captured at construction, so the
23
49
  * model fills in once the session binds without an explicit refresh.
24
50
  *
25
51
  * The metadata column is dropped when the terminal cannot hold it, and the mark
26
- * falls back to a compact variant rather than being truncated mid-glyph.
52
+ * falls back to a type lockup rather than being truncated mid-glyph.
27
53
  */
28
54
  export declare class ApexSplashHeader implements Component {
29
55
  private readonly version;
@@ -34,7 +60,6 @@ export declare class ApexSplashHeader implements Component {
34
60
  constructor(version: string, getModelId: () => string | undefined, getCwd: () => string, verboseInstructions?: string, options?: ApexSplashHeaderOptions);
35
61
  invalidate(): void;
36
62
  render(width: number): string[];
37
- private selectLogo;
38
63
  private buildMetadata;
39
64
  private padLine;
40
65
  }
@@ -1 +1 @@
1
- {"version":3,"file":"splash-header.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/splash-header.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,wBAAwB,CAAC;AAKvF,qDAAqD;AACrD,MAAM,WAAW,wBAAwB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACvC,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,MAAM,SAAS,wBAAwB,EAAE,CAAC;IAC7D,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACnC,6DAA6D;IAC7D,eAAe,CAAC,EAAE,MAAM,SAAS,GAAG,OAAO,CAAC;IAC5C,gDAAgD;IAChD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AA8DD;;;;;;;;GAQG;AACH,qBAAa,gBAAiB,YAAW,SAAS;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD,YACC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,MAAM,GAAG,SAAS,EACpC,MAAM,EAAE,MAAM,MAAM,EACpB,mBAAmB,CAAC,EAAE,MAAM,EAC5B,OAAO,GAAE,uBAA4B,EAOrC;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAwC9B;IAED,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,OAAO;CAIf","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport { APEX_PEAK_LOGO, APEX_PEAK_LOGO_ASCII, APEX_PEAK_LOGO_COMPACT } from \"../../../themes/apex-logo.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { formatCwdForFooter } from \"./footer.ts\";\n\n/** One `label value` row in the metadata column. */\nexport interface SplashHeaderMetadataLine {\n\tlabel: string;\n\tvalue: string;\n}\n\nexport interface ApexSplashHeaderOptions {\n\t/** Rows appended after `cwd`, for extensions that want to contribute context. */\n\tgetExtraMetadata?: () => readonly SplashHeaderMetadataLine[];\n\t/** Hint rendered under the metadata block. Return undefined for none. */\n\tgetHint?: () => string | undefined;\n\t/** Resolves `terminal.symbolPreset`. Defaults to unicode. */\n\tgetSymbolPreset?: () => \"unicode\" | \"ascii\";\n\t/** Emit a leading blank line above the mark. */\n\ttopPadding?: boolean;\n\t/** Override the mark. Used by tests and by extensions that rebrand. */\n\tlogo?: string;\n}\n\nconst GUTTER = 4;\nconst LABEL_WIDTH = 9;\n/** Smallest value column worth rendering; below this the metadata column is dropped. */\nconst MIN_VALUE_WIDTH = 8;\n\n/**\n * Keep the rightmost path segments that fit, so the leaf directory stays visible.\n *\n * `truncateToWidth` clips the tail, which for a path hides the part that\n * identifies the session. This clips the head instead.\n */\nfunction truncatePathTail(path: string, maxWidth: number): string {\n\tif (maxWidth <= 0) {\n\t\treturn \"\";\n\t}\n\tif (visibleWidth(path) <= maxWidth) {\n\t\treturn path;\n\t}\n\tconst ellipsis = \"…/\";\n\tconst budget = Math.max(0, maxWidth - visibleWidth(ellipsis));\n\tconst segments = path.split(\"/\");\n\tlet tail = \"\";\n\tfor (let index = segments.length - 1; index >= 0; index--) {\n\t\tconst candidate = tail ? `${segments[index]}/${tail}` : segments[index];\n\t\tif (visibleWidth(candidate) > budget) {\n\t\t\tbreak;\n\t\t}\n\t\ttail = candidate;\n\t}\n\t// A single segment longer than the whole budget: clip it from the left.\n\treturn tail ? ellipsis + tail : ellipsis + trailingByWidth(path, budget);\n}\n\n/**\n * The widest suffix of `text` that fits `maxWidth` terminal columns.\n *\n * Walks grapheme clusters from the right rather than using `String.slice`, whose\n * UTF-16 offsets would cut an astral character in half — emitting a lone\n * surrogate — and would miscount every double-width character on the way.\n */\nfunction trailingByWidth(text: string, maxWidth: number): string {\n\tif (maxWidth <= 0) {\n\t\treturn \"\";\n\t}\n\tconst graphemes = [...new Intl.Segmenter(undefined, { granularity: \"grapheme\" }).segment(text)].map(\n\t\t(entry) => entry.segment,\n\t);\n\tlet width = 0;\n\tlet index = graphemes.length;\n\twhile (index > 0) {\n\t\tconst next = width + visibleWidth(graphemes[index - 1]);\n\t\tif (next > maxWidth) {\n\t\t\tbreak;\n\t\t}\n\t\twidth = next;\n\t\tindex -= 1;\n\t}\n\treturn graphemes.slice(index).join(\"\");\n}\n\n/**\n * Startup header: brand mark on the left, runtime metadata on the right.\n *\n * Values are read through getters rather than captured at construction, so the\n * model fills in once the session binds without an explicit refresh.\n *\n * The metadata column is dropped when the terminal cannot hold it, and the mark\n * falls back to a compact variant rather than being truncated mid-glyph.\n */\nexport class ApexSplashHeader implements Component {\n\tprivate readonly version: string;\n\tprivate readonly getModelId: () => string | undefined;\n\tprivate readonly getCwd: () => string;\n\tprivate readonly verboseInstructions: string | undefined;\n\tprivate readonly options: ApexSplashHeaderOptions;\n\n\tconstructor(\n\t\tversion: string,\n\t\tgetModelId: () => string | undefined,\n\t\tgetCwd: () => string,\n\t\tverboseInstructions?: string,\n\t\toptions: ApexSplashHeaderOptions = {},\n\t) {\n\t\tthis.version = version;\n\t\tthis.getModelId = getModelId;\n\t\tthis.getCwd = getCwd;\n\t\tthis.verboseInstructions = verboseInstructions;\n\t\tthis.options = options;\n\t}\n\n\tinvalidate(): void {\n\t\t// Render output is derived from current theme and session state.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst safeWidth = Math.max(1, width);\n\t\tconst paddingX = safeWidth > 1 ? 1 : 0;\n\t\tconst contentWidth = Math.max(1, safeWidth - paddingX * 2);\n\n\t\tconst logoLines = this.selectLogo(contentWidth);\n\t\tconst logoWidth = logoLines.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);\n\n\t\tconst metaWidth = contentWidth - logoWidth - GUTTER;\n\t\tconst showMeta = metaWidth >= LABEL_WIDTH + MIN_VALUE_WIDTH;\n\t\tconst valueWidth = Math.max(1, metaWidth - LABEL_WIDTH);\n\t\tconst metaLines = showMeta ? this.buildMetadata(valueWidth) : [];\n\t\tconst metaStart = Math.max(0, Math.floor((logoLines.length - metaLines.length) / 2));\n\n\t\tconst lines = this.options.topPadding ? [this.padLine(\"\", safeWidth, paddingX)] : [];\n\n\t\tlogoLines.forEach((line, index) => {\n\t\t\tconst meta = index >= metaStart && index < metaStart + metaLines.length ? metaLines[index - metaStart] : \"\";\n\t\t\tconst gap = showMeta ? \" \".repeat(Math.max(0, logoWidth - visibleWidth(line) + GUTTER)) : \"\";\n\t\t\tconst content = truncateToWidth(theme.fg(\"text\", line) + gap + meta, contentWidth, \"\");\n\t\t\tlines.push(this.padLine(content, safeWidth, paddingX));\n\t\t});\n\n\t\t// The hint is product copy rather than a runtime fact, so it sits under the\n\t\t// whole block at full width instead of inside the narrow metadata column,\n\t\t// where it would truncate even on a wide terminal.\n\t\tconst hint = this.options.getHint?.();\n\t\tif (hint) {\n\t\t\tlines.push(this.padLine(\"\", safeWidth, paddingX));\n\t\t\tlines.push(this.padLine(theme.fg(\"dim\", truncateToWidth(hint, contentWidth)), safeWidth, paddingX));\n\t\t}\n\n\t\tif (this.verboseInstructions) {\n\t\t\tlines.push(this.padLine(\"\", safeWidth, paddingX));\n\t\t\tfor (const instruction of this.verboseInstructions.split(\"\\n\")) {\n\t\t\t\tlines.push(this.padLine(truncateToWidth(instruction, contentWidth), safeWidth, paddingX));\n\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\tprivate selectLogo(contentWidth: number): string[] {\n\t\tif (this.options.logo) {\n\t\t\treturn this.options.logo.split(\"\\n\");\n\t\t}\n\t\tif (this.options.getSymbolPreset?.() === \"ascii\") {\n\t\t\treturn APEX_PEAK_LOGO_ASCII.split(\"\\n\");\n\t\t}\n\t\tconst full = APEX_PEAK_LOGO.split(\"\\n\");\n\t\tconst fullWidth = full.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);\n\t\treturn contentWidth >= fullWidth ? full : APEX_PEAK_LOGO_COMPACT.split(\"\\n\");\n\t}\n\n\tprivate buildMetadata(valueWidth: number): string[] {\n\t\tconst labelled = (label: string, value: string) =>\n\t\t\ttheme.fg(\"dim\", label.padEnd(LABEL_WIDTH)) + theme.fg(\"muted\", truncateToWidth(value, valueWidth));\n\n\t\tconst cwd = formatCwdForFooter(this.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\t\treturn [\n\t\t\tlabelled(\"version\", `v${this.version}`),\n\t\t\tlabelled(\"model\", this.getModelId() ?? \"—\"),\n\t\t\ttheme.fg(\"dim\", \"cwd\".padEnd(LABEL_WIDTH)) + theme.fg(\"muted\", truncatePathTail(cwd, valueWidth)),\n\t\t\t...(this.options.getExtraMetadata?.() ?? []).map((line) => labelled(line.label, line.value)),\n\t\t];\n\t}\n\n\tprivate padLine(content: string, safeWidth: number, paddingX: number): string {\n\t\tconst trailing = Math.max(0, safeWidth - paddingX - visibleWidth(content));\n\t\treturn \" \".repeat(paddingX) + content + \" \".repeat(trailing);\n\t}\n}\n"]}
1
+ {"version":3,"file":"splash-header.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/splash-header.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,wBAAwB,CAAC;AACvF,OAAO,EACN,KAAK,SAAS,EAEd,KAAK,OAAO,EACZ,KAAK,gBAAgB,EAErB,MAAM,8BAA8B,CAAC;AAItC,qDAAqD;AACrD,MAAM,WAAW,wBAAwB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACvC,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,MAAM,SAAS,wBAAwB,EAAE,CAAC;IAC7D;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACxC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACnC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACxC,6DAA6D;IAC7D,eAAe,CAAC,EAAE,MAAM,gBAAgB,CAAC;IACzC,gDAAgD;IAChD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAIjD;AAED,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,EAAE,CAE7D;AAgED;;;;;;;;;GASG;AACH,qBAAa,gBAAiB,YAAW,SAAS;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD,YACC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,MAAM,GAAG,SAAS,EACpC,MAAM,EAAE,MAAM,MAAM,EACpB,mBAAmB,CAAC,EAAE,MAAM,EAC5B,OAAO,GAAE,uBAA4B,EAOrC;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAwE9B;IAED,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,OAAO;CAIf","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport {\n\ttype BrandMark,\n\tcustomBrandMark,\n\ttype MarkRow,\n\ttype MarkSymbolPreset,\n\tselectBrandMark,\n} from \"../../../themes/apex-logo.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { formatCwdForFooter } from \"./footer.ts\";\n\n/** One `label value` row in the metadata column. */\nexport interface SplashHeaderMetadataLine {\n\tlabel: string;\n\tvalue: string;\n}\n\nexport interface ApexSplashHeaderOptions {\n\t/** Rows appended after `cwd`, for extensions that want to contribute context. */\n\tgetExtraMetadata?: () => readonly SplashHeaderMetadataLine[];\n\t/**\n\t * One line of counted totals, rendered between the rules under the mark.\n\t * The header does not know how to count anything; the caller supplies the\n\t * finished line so the resource loader stays on its own side of the seam.\n\t */\n\tgetInventory?: () => string | undefined;\n\t/**\n\t * Affordance appended to the inventory line, e.g. the command that shows the\n\t * detail. Dropped whole rather than clipped, so it never renders as a stub.\n\t */\n\tinventoryHint?: string;\n\t/** Hint rendered under the metadata block. Return undefined for none. */\n\tgetHint?: () => string | undefined;\n\t/**\n\t * One row of entry-point sigils under the hint, already coloured by the\n\t * caller. The header does not know the keybindings, so it does not build it.\n\t */\n\tgetShortcuts?: () => string | undefined;\n\t/** Resolves `terminal.symbolPreset`. Defaults to unicode. */\n\tgetSymbolPreset?: () => MarkSymbolPreset;\n\t/** Emit a leading blank line above the mark. */\n\ttopPadding?: boolean;\n\t/** Override the mark. Used by tests and by extensions that rebrand. */\n\tlogo?: string;\n}\n\n/**\n * Paint one mark row: accent run then text run.\n *\n * Empty runs are skipped so no row carries a bare, unclosed SGR pair.\n */\nexport function paintMarkRow(row: MarkRow): string {\n\tconst accent = row.accent ? theme.fg(\"accent\", row.accent) : \"\";\n\tconst text = row.text ? theme.fg(\"text\", row.text) : \"\";\n\treturn accent + text;\n}\n\n/** The whole mark as painted lines, for surfaces that just want to stamp it. */\nexport function paintBrandMark(brandMark: BrandMark): string[] {\n\treturn brandMark.rows.map(paintMarkRow);\n}\n\nconst GUTTER = 4;\nconst LABEL_WIDTH = 9;\n/** Smallest value column worth rendering; below this the metadata column is dropped. */\nconst MIN_VALUE_WIDTH = 8;\n/** Below this the rules and the inventory line are dropped rather than clipped. */\nconst MIN_RULE_WIDTH = 12;\n\n/**\n * Keep the rightmost path segments that fit, so the leaf directory stays visible.\n *\n * `truncateToWidth` clips the tail, which for a path hides the part that\n * identifies the session. This clips the head instead.\n */\nfunction truncatePathTail(path: string, maxWidth: number): string {\n\tif (maxWidth <= 0) {\n\t\treturn \"\";\n\t}\n\tif (visibleWidth(path) <= maxWidth) {\n\t\treturn path;\n\t}\n\tconst ellipsis = \"…/\";\n\tconst budget = Math.max(0, maxWidth - visibleWidth(ellipsis));\n\tconst segments = path.split(\"/\");\n\tlet tail = \"\";\n\tfor (let index = segments.length - 1; index >= 0; index--) {\n\t\tconst candidate = tail ? `${segments[index]}/${tail}` : segments[index];\n\t\tif (visibleWidth(candidate) > budget) {\n\t\t\tbreak;\n\t\t}\n\t\ttail = candidate;\n\t}\n\t// A single segment longer than the whole budget: clip it from the left.\n\treturn tail ? ellipsis + tail : ellipsis + trailingByWidth(path, budget);\n}\n\n/**\n * The widest suffix of `text` that fits `maxWidth` terminal columns.\n *\n * Walks grapheme clusters from the right rather than using `String.slice`, whose\n * UTF-16 offsets would cut an astral character in half — emitting a lone\n * surrogate — and would miscount every double-width character on the way.\n */\nfunction trailingByWidth(text: string, maxWidth: number): string {\n\tif (maxWidth <= 0) {\n\t\treturn \"\";\n\t}\n\tconst graphemes = [...new Intl.Segmenter(undefined, { granularity: \"grapheme\" }).segment(text)].map(\n\t\t(entry) => entry.segment,\n\t);\n\tlet width = 0;\n\tlet index = graphemes.length;\n\twhile (index > 0) {\n\t\tconst next = width + visibleWidth(graphemes[index - 1]);\n\t\tif (next > maxWidth) {\n\t\t\tbreak;\n\t\t}\n\t\twidth = next;\n\t\tindex -= 1;\n\t}\n\treturn graphemes.slice(index).join(\"\");\n}\n\n/**\n * Startup header: brand mark on the left, runtime metadata on the right, then a\n * ruled band carrying the counted inventory.\n *\n * Values are read through getters rather than captured at construction, so the\n * model fills in once the session binds without an explicit refresh.\n *\n * The metadata column is dropped when the terminal cannot hold it, and the mark\n * falls back to a type lockup rather than being truncated mid-glyph.\n */\nexport class ApexSplashHeader implements Component {\n\tprivate readonly version: string;\n\tprivate readonly getModelId: () => string | undefined;\n\tprivate readonly getCwd: () => string;\n\tprivate readonly verboseInstructions: string | undefined;\n\tprivate readonly options: ApexSplashHeaderOptions;\n\n\tconstructor(\n\t\tversion: string,\n\t\tgetModelId: () => string | undefined,\n\t\tgetCwd: () => string,\n\t\tverboseInstructions?: string,\n\t\toptions: ApexSplashHeaderOptions = {},\n\t) {\n\t\tthis.version = version;\n\t\tthis.getModelId = getModelId;\n\t\tthis.getCwd = getCwd;\n\t\tthis.verboseInstructions = verboseInstructions;\n\t\tthis.options = options;\n\t}\n\n\tinvalidate(): void {\n\t\t// Render output is derived from current theme and session state.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst safeWidth = Math.max(1, width);\n\t\tconst paddingX = safeWidth > 1 ? 1 : 0;\n\t\tconst contentWidth = Math.max(1, safeWidth - paddingX * 2);\n\t\tconst symbolPreset = this.options.getSymbolPreset?.() ?? \"unicode\";\n\n\t\tconst brandMark = this.options.logo\n\t\t\t? customBrandMark(this.options.logo)\n\t\t\t: selectBrandMark(contentWidth, symbolPreset);\n\n\t\tconst metaWidth = contentWidth - brandMark.width - GUTTER;\n\t\tconst showMeta = metaWidth >= LABEL_WIDTH + MIN_VALUE_WIDTH;\n\t\tconst valueWidth = Math.max(1, metaWidth - LABEL_WIDTH);\n\t\tconst metaLines = showMeta ? this.buildMetadata(valueWidth) : [];\n\t\t// Bottom-aligned, so the last metadata row sits on the mark's ember\n\t\t// baseline and the two read as one block rather than two stacks.\n\t\tconst metaStart = Math.max(0, brandMark.rows.length - metaLines.length);\n\n\t\tconst lines = this.options.topPadding ? [this.padLine(\"\", safeWidth, paddingX)] : [];\n\n\t\t// The mark and the metadata column are two stacks of different heights\n\t\t// sharing one block, so the block is as tall as the taller of them. A\n\t\t// one-row mark beside three metadata rows still shows all three.\n\t\tconst blockRows = Math.max(brandMark.rows.length, metaStart + metaLines.length);\n\t\tfor (let index = 0; index < blockRows; index++) {\n\t\t\tconst row = brandMark.rows[index];\n\t\t\tconst painted = row ? paintMarkRow(row) : \"\";\n\t\t\tconst rowWidth = row ? visibleWidth(row.accent + row.text) : 0;\n\t\t\tconst meta = index >= metaStart ? (metaLines[index - metaStart] ?? \"\") : \"\";\n\t\t\tconst gap = showMeta ? \" \".repeat(Math.max(0, brandMark.width - rowWidth + GUTTER)) : \"\";\n\t\t\tconst content = truncateToWidth(painted + (meta ? gap + meta : \"\"), contentWidth, \"\");\n\t\t\tlines.push(this.padLine(content, safeWidth, paddingX));\n\t\t}\n\n\t\t// Counted totals sit in a ruled band of their own. The rules are what\n\t\t// carry the structure on this screen, so nothing here is boxed.\n\t\tconst inventory = this.options.getInventory?.();\n\t\tif (inventory && contentWidth >= MIN_RULE_WIDTH) {\n\t\t\tconst rule = theme.fg(\"borderMuted\", (symbolPreset === \"ascii\" ? \"-\" : \"─\").repeat(contentWidth));\n\t\t\tlines.push(this.padLine(\"\", safeWidth, paddingX));\n\t\t\tlines.push(this.padLine(rule, safeWidth, paddingX));\n\t\t\tconst hint = this.options.inventoryHint;\n\t\t\tconst withHint = hint ? `${inventory} ${theme.fg(\"dim\", hint)}` : inventory;\n\t\t\tconst fitted =\n\t\t\t\tvisibleWidth(withHint) <= contentWidth ? withHint : truncateToWidth(inventory, contentWidth, \"\");\n\t\t\tlines.push(this.padLine(fitted, safeWidth, paddingX));\n\t\t\tlines.push(this.padLine(rule, safeWidth, paddingX));\n\t\t}\n\n\t\t// The hint is product copy rather than a runtime fact, so it sits under the\n\t\t// whole block at full width instead of inside the narrow metadata column,\n\t\t// where it would truncate even on a wide terminal.\n\t\tconst hint = this.options.getHint?.();\n\t\tif (hint) {\n\t\t\tlines.push(this.padLine(\"\", safeWidth, paddingX));\n\t\t\tlines.push(this.padLine(theme.fg(\"dim\", truncateToWidth(hint, contentWidth)), safeWidth, paddingX));\n\t\t}\n\n\t\tconst shortcuts = this.options.getShortcuts?.();\n\t\tif (shortcuts) {\n\t\t\tlines.push(this.padLine(\"\", safeWidth, paddingX));\n\t\t\tlines.push(this.padLine(truncateToWidth(shortcuts, contentWidth, \"\"), safeWidth, paddingX));\n\t\t}\n\n\t\tif (this.verboseInstructions) {\n\t\t\tlines.push(this.padLine(\"\", safeWidth, paddingX));\n\t\t\tfor (const instruction of this.verboseInstructions.split(\"\\n\")) {\n\t\t\t\tlines.push(this.padLine(truncateToWidth(instruction, contentWidth), safeWidth, paddingX));\n\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\tprivate buildMetadata(valueWidth: number): string[] {\n\t\tconst labelled = (label: string, value: string) =>\n\t\t\ttheme.fg(\"dim\", label.padEnd(LABEL_WIDTH)) + theme.fg(\"muted\", truncateToWidth(value, valueWidth));\n\n\t\tconst cwd = formatCwdForFooter(this.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\t\treturn [\n\t\t\tlabelled(\"version\", `v${this.version}`),\n\t\t\tlabelled(\"model\", this.getModelId() ?? \"—\"),\n\t\t\ttheme.fg(\"dim\", \"cwd\".padEnd(LABEL_WIDTH)) + theme.fg(\"muted\", truncatePathTail(cwd, valueWidth)),\n\t\t\t...(this.options.getExtraMetadata?.() ?? []).map((line) => labelled(line.label, line.value)),\n\t\t];\n\t}\n\n\tprivate padLine(content: string, safeWidth: number, paddingX: number): string {\n\t\tconst trailing = Math.max(0, safeWidth - paddingX - visibleWidth(content));\n\t\treturn \" \".repeat(paddingX) + content + \" \".repeat(trailing);\n\t}\n}\n"]}
@@ -1,11 +1,27 @@
1
1
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
- import { APEX_PEAK_LOGO, APEX_PEAK_LOGO_ASCII, APEX_PEAK_LOGO_COMPACT } from "../../../themes/apex-logo.js";
2
+ import { customBrandMark, selectBrandMark, } from "../../../themes/apex-logo.js";
3
3
  import { theme } from "../theme/theme.js";
4
4
  import { formatCwdForFooter } from "./footer.js";
5
+ /**
6
+ * Paint one mark row: accent run then text run.
7
+ *
8
+ * Empty runs are skipped so no row carries a bare, unclosed SGR pair.
9
+ */
10
+ export function paintMarkRow(row) {
11
+ const accent = row.accent ? theme.fg("accent", row.accent) : "";
12
+ const text = row.text ? theme.fg("text", row.text) : "";
13
+ return accent + text;
14
+ }
15
+ /** The whole mark as painted lines, for surfaces that just want to stamp it. */
16
+ export function paintBrandMark(brandMark) {
17
+ return brandMark.rows.map(paintMarkRow);
18
+ }
5
19
  const GUTTER = 4;
6
20
  const LABEL_WIDTH = 9;
7
21
  /** Smallest value column worth rendering; below this the metadata column is dropped. */
8
22
  const MIN_VALUE_WIDTH = 8;
23
+ /** Below this the rules and the inventory line are dropped rather than clipped. */
24
+ const MIN_RULE_WIDTH = 12;
9
25
  /**
10
26
  * Keep the rightmost path segments that fit, so the leaf directory stays visible.
11
27
  *
@@ -58,13 +74,14 @@ function trailingByWidth(text, maxWidth) {
58
74
  return graphemes.slice(index).join("");
59
75
  }
60
76
  /**
61
- * Startup header: brand mark on the left, runtime metadata on the right.
77
+ * Startup header: brand mark on the left, runtime metadata on the right, then a
78
+ * ruled band carrying the counted inventory.
62
79
  *
63
80
  * Values are read through getters rather than captured at construction, so the
64
81
  * model fills in once the session binds without an explicit refresh.
65
82
  *
66
83
  * The metadata column is dropped when the terminal cannot hold it, and the mark
67
- * falls back to a compact variant rather than being truncated mid-glyph.
84
+ * falls back to a type lockup rather than being truncated mid-glyph.
68
85
  */
69
86
  export class ApexSplashHeader {
70
87
  version;
@@ -86,20 +103,44 @@ export class ApexSplashHeader {
86
103
  const safeWidth = Math.max(1, width);
87
104
  const paddingX = safeWidth > 1 ? 1 : 0;
88
105
  const contentWidth = Math.max(1, safeWidth - paddingX * 2);
89
- const logoLines = this.selectLogo(contentWidth);
90
- const logoWidth = logoLines.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);
91
- const metaWidth = contentWidth - logoWidth - GUTTER;
106
+ const symbolPreset = this.options.getSymbolPreset?.() ?? "unicode";
107
+ const brandMark = this.options.logo
108
+ ? customBrandMark(this.options.logo)
109
+ : selectBrandMark(contentWidth, symbolPreset);
110
+ const metaWidth = contentWidth - brandMark.width - GUTTER;
92
111
  const showMeta = metaWidth >= LABEL_WIDTH + MIN_VALUE_WIDTH;
93
112
  const valueWidth = Math.max(1, metaWidth - LABEL_WIDTH);
94
113
  const metaLines = showMeta ? this.buildMetadata(valueWidth) : [];
95
- const metaStart = Math.max(0, Math.floor((logoLines.length - metaLines.length) / 2));
114
+ // Bottom-aligned, so the last metadata row sits on the mark's ember
115
+ // baseline and the two read as one block rather than two stacks.
116
+ const metaStart = Math.max(0, brandMark.rows.length - metaLines.length);
96
117
  const lines = this.options.topPadding ? [this.padLine("", safeWidth, paddingX)] : [];
97
- logoLines.forEach((line, index) => {
98
- const meta = index >= metaStart && index < metaStart + metaLines.length ? metaLines[index - metaStart] : "";
99
- const gap = showMeta ? " ".repeat(Math.max(0, logoWidth - visibleWidth(line) + GUTTER)) : "";
100
- const content = truncateToWidth(theme.fg("text", line) + gap + meta, contentWidth, "");
118
+ // The mark and the metadata column are two stacks of different heights
119
+ // sharing one block, so the block is as tall as the taller of them. A
120
+ // one-row mark beside three metadata rows still shows all three.
121
+ const blockRows = Math.max(brandMark.rows.length, metaStart + metaLines.length);
122
+ for (let index = 0; index < blockRows; index++) {
123
+ const row = brandMark.rows[index];
124
+ const painted = row ? paintMarkRow(row) : "";
125
+ const rowWidth = row ? visibleWidth(row.accent + row.text) : 0;
126
+ const meta = index >= metaStart ? (metaLines[index - metaStart] ?? "") : "";
127
+ const gap = showMeta ? " ".repeat(Math.max(0, brandMark.width - rowWidth + GUTTER)) : "";
128
+ const content = truncateToWidth(painted + (meta ? gap + meta : ""), contentWidth, "");
101
129
  lines.push(this.padLine(content, safeWidth, paddingX));
102
- });
130
+ }
131
+ // Counted totals sit in a ruled band of their own. The rules are what
132
+ // carry the structure on this screen, so nothing here is boxed.
133
+ const inventory = this.options.getInventory?.();
134
+ if (inventory && contentWidth >= MIN_RULE_WIDTH) {
135
+ const rule = theme.fg("borderMuted", (symbolPreset === "ascii" ? "-" : "─").repeat(contentWidth));
136
+ lines.push(this.padLine("", safeWidth, paddingX));
137
+ lines.push(this.padLine(rule, safeWidth, paddingX));
138
+ const hint = this.options.inventoryHint;
139
+ const withHint = hint ? `${inventory} ${theme.fg("dim", hint)}` : inventory;
140
+ const fitted = visibleWidth(withHint) <= contentWidth ? withHint : truncateToWidth(inventory, contentWidth, "");
141
+ lines.push(this.padLine(fitted, safeWidth, paddingX));
142
+ lines.push(this.padLine(rule, safeWidth, paddingX));
143
+ }
103
144
  // The hint is product copy rather than a runtime fact, so it sits under the
104
145
  // whole block at full width instead of inside the narrow metadata column,
105
146
  // where it would truncate even on a wide terminal.
@@ -108,6 +149,11 @@ export class ApexSplashHeader {
108
149
  lines.push(this.padLine("", safeWidth, paddingX));
109
150
  lines.push(this.padLine(theme.fg("dim", truncateToWidth(hint, contentWidth)), safeWidth, paddingX));
110
151
  }
152
+ const shortcuts = this.options.getShortcuts?.();
153
+ if (shortcuts) {
154
+ lines.push(this.padLine("", safeWidth, paddingX));
155
+ lines.push(this.padLine(truncateToWidth(shortcuts, contentWidth, ""), safeWidth, paddingX));
156
+ }
111
157
  if (this.verboseInstructions) {
112
158
  lines.push(this.padLine("", safeWidth, paddingX));
113
159
  for (const instruction of this.verboseInstructions.split("\n")) {
@@ -116,17 +162,6 @@ export class ApexSplashHeader {
116
162
  }
117
163
  return lines;
118
164
  }
119
- selectLogo(contentWidth) {
120
- if (this.options.logo) {
121
- return this.options.logo.split("\n");
122
- }
123
- if (this.options.getSymbolPreset?.() === "ascii") {
124
- return APEX_PEAK_LOGO_ASCII.split("\n");
125
- }
126
- const full = APEX_PEAK_LOGO.split("\n");
127
- const fullWidth = full.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);
128
- return contentWidth >= fullWidth ? full : APEX_PEAK_LOGO_COMPACT.split("\n");
129
- }
130
165
  buildMetadata(valueWidth) {
131
166
  const labelled = (label, value) => theme.fg("dim", label.padEnd(LABEL_WIDTH)) + theme.fg("muted", truncateToWidth(value, valueWidth));
132
167
  const cwd = formatCwdForFooter(this.getCwd(), process.env.HOME || process.env.USERPROFILE);
@@ -1 +1 @@
1
- {"version":3,"file":"splash-header.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/splash-header.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport { APEX_PEAK_LOGO, APEX_PEAK_LOGO_ASCII, APEX_PEAK_LOGO_COMPACT } from \"../../../themes/apex-logo.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { formatCwdForFooter } from \"./footer.ts\";\n\n/** One `label value` row in the metadata column. */\nexport interface SplashHeaderMetadataLine {\n\tlabel: string;\n\tvalue: string;\n}\n\nexport interface ApexSplashHeaderOptions {\n\t/** Rows appended after `cwd`, for extensions that want to contribute context. */\n\tgetExtraMetadata?: () => readonly SplashHeaderMetadataLine[];\n\t/** Hint rendered under the metadata block. Return undefined for none. */\n\tgetHint?: () => string | undefined;\n\t/** Resolves `terminal.symbolPreset`. Defaults to unicode. */\n\tgetSymbolPreset?: () => \"unicode\" | \"ascii\";\n\t/** Emit a leading blank line above the mark. */\n\ttopPadding?: boolean;\n\t/** Override the mark. Used by tests and by extensions that rebrand. */\n\tlogo?: string;\n}\n\nconst GUTTER = 4;\nconst LABEL_WIDTH = 9;\n/** Smallest value column worth rendering; below this the metadata column is dropped. */\nconst MIN_VALUE_WIDTH = 8;\n\n/**\n * Keep the rightmost path segments that fit, so the leaf directory stays visible.\n *\n * `truncateToWidth` clips the tail, which for a path hides the part that\n * identifies the session. This clips the head instead.\n */\nfunction truncatePathTail(path: string, maxWidth: number): string {\n\tif (maxWidth <= 0) {\n\t\treturn \"\";\n\t}\n\tif (visibleWidth(path) <= maxWidth) {\n\t\treturn path;\n\t}\n\tconst ellipsis = \"…/\";\n\tconst budget = Math.max(0, maxWidth - visibleWidth(ellipsis));\n\tconst segments = path.split(\"/\");\n\tlet tail = \"\";\n\tfor (let index = segments.length - 1; index >= 0; index--) {\n\t\tconst candidate = tail ? `${segments[index]}/${tail}` : segments[index];\n\t\tif (visibleWidth(candidate) > budget) {\n\t\t\tbreak;\n\t\t}\n\t\ttail = candidate;\n\t}\n\t// A single segment longer than the whole budget: clip it from the left.\n\treturn tail ? ellipsis + tail : ellipsis + trailingByWidth(path, budget);\n}\n\n/**\n * The widest suffix of `text` that fits `maxWidth` terminal columns.\n *\n * Walks grapheme clusters from the right rather than using `String.slice`, whose\n * UTF-16 offsets would cut an astral character in half — emitting a lone\n * surrogate — and would miscount every double-width character on the way.\n */\nfunction trailingByWidth(text: string, maxWidth: number): string {\n\tif (maxWidth <= 0) {\n\t\treturn \"\";\n\t}\n\tconst graphemes = [...new Intl.Segmenter(undefined, { granularity: \"grapheme\" }).segment(text)].map(\n\t\t(entry) => entry.segment,\n\t);\n\tlet width = 0;\n\tlet index = graphemes.length;\n\twhile (index > 0) {\n\t\tconst next = width + visibleWidth(graphemes[index - 1]);\n\t\tif (next > maxWidth) {\n\t\t\tbreak;\n\t\t}\n\t\twidth = next;\n\t\tindex -= 1;\n\t}\n\treturn graphemes.slice(index).join(\"\");\n}\n\n/**\n * Startup header: brand mark on the left, runtime metadata on the right.\n *\n * Values are read through getters rather than captured at construction, so the\n * model fills in once the session binds without an explicit refresh.\n *\n * The metadata column is dropped when the terminal cannot hold it, and the mark\n * falls back to a compact variant rather than being truncated mid-glyph.\n */\nexport class ApexSplashHeader implements Component {\n\tprivate readonly version: string;\n\tprivate readonly getModelId: () => string | undefined;\n\tprivate readonly getCwd: () => string;\n\tprivate readonly verboseInstructions: string | undefined;\n\tprivate readonly options: ApexSplashHeaderOptions;\n\n\tconstructor(\n\t\tversion: string,\n\t\tgetModelId: () => string | undefined,\n\t\tgetCwd: () => string,\n\t\tverboseInstructions?: string,\n\t\toptions: ApexSplashHeaderOptions = {},\n\t) {\n\t\tthis.version = version;\n\t\tthis.getModelId = getModelId;\n\t\tthis.getCwd = getCwd;\n\t\tthis.verboseInstructions = verboseInstructions;\n\t\tthis.options = options;\n\t}\n\n\tinvalidate(): void {\n\t\t// Render output is derived from current theme and session state.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst safeWidth = Math.max(1, width);\n\t\tconst paddingX = safeWidth > 1 ? 1 : 0;\n\t\tconst contentWidth = Math.max(1, safeWidth - paddingX * 2);\n\n\t\tconst logoLines = this.selectLogo(contentWidth);\n\t\tconst logoWidth = logoLines.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);\n\n\t\tconst metaWidth = contentWidth - logoWidth - GUTTER;\n\t\tconst showMeta = metaWidth >= LABEL_WIDTH + MIN_VALUE_WIDTH;\n\t\tconst valueWidth = Math.max(1, metaWidth - LABEL_WIDTH);\n\t\tconst metaLines = showMeta ? this.buildMetadata(valueWidth) : [];\n\t\tconst metaStart = Math.max(0, Math.floor((logoLines.length - metaLines.length) / 2));\n\n\t\tconst lines = this.options.topPadding ? [this.padLine(\"\", safeWidth, paddingX)] : [];\n\n\t\tlogoLines.forEach((line, index) => {\n\t\t\tconst meta = index >= metaStart && index < metaStart + metaLines.length ? metaLines[index - metaStart] : \"\";\n\t\t\tconst gap = showMeta ? \" \".repeat(Math.max(0, logoWidth - visibleWidth(line) + GUTTER)) : \"\";\n\t\t\tconst content = truncateToWidth(theme.fg(\"text\", line) + gap + meta, contentWidth, \"\");\n\t\t\tlines.push(this.padLine(content, safeWidth, paddingX));\n\t\t});\n\n\t\t// The hint is product copy rather than a runtime fact, so it sits under the\n\t\t// whole block at full width instead of inside the narrow metadata column,\n\t\t// where it would truncate even on a wide terminal.\n\t\tconst hint = this.options.getHint?.();\n\t\tif (hint) {\n\t\t\tlines.push(this.padLine(\"\", safeWidth, paddingX));\n\t\t\tlines.push(this.padLine(theme.fg(\"dim\", truncateToWidth(hint, contentWidth)), safeWidth, paddingX));\n\t\t}\n\n\t\tif (this.verboseInstructions) {\n\t\t\tlines.push(this.padLine(\"\", safeWidth, paddingX));\n\t\t\tfor (const instruction of this.verboseInstructions.split(\"\\n\")) {\n\t\t\t\tlines.push(this.padLine(truncateToWidth(instruction, contentWidth), safeWidth, paddingX));\n\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\tprivate selectLogo(contentWidth: number): string[] {\n\t\tif (this.options.logo) {\n\t\t\treturn this.options.logo.split(\"\\n\");\n\t\t}\n\t\tif (this.options.getSymbolPreset?.() === \"ascii\") {\n\t\t\treturn APEX_PEAK_LOGO_ASCII.split(\"\\n\");\n\t\t}\n\t\tconst full = APEX_PEAK_LOGO.split(\"\\n\");\n\t\tconst fullWidth = full.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);\n\t\treturn contentWidth >= fullWidth ? full : APEX_PEAK_LOGO_COMPACT.split(\"\\n\");\n\t}\n\n\tprivate buildMetadata(valueWidth: number): string[] {\n\t\tconst labelled = (label: string, value: string) =>\n\t\t\ttheme.fg(\"dim\", label.padEnd(LABEL_WIDTH)) + theme.fg(\"muted\", truncateToWidth(value, valueWidth));\n\n\t\tconst cwd = formatCwdForFooter(this.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\t\treturn [\n\t\t\tlabelled(\"version\", `v${this.version}`),\n\t\t\tlabelled(\"model\", this.getModelId() ?? \"—\"),\n\t\t\ttheme.fg(\"dim\", \"cwd\".padEnd(LABEL_WIDTH)) + theme.fg(\"muted\", truncatePathTail(cwd, valueWidth)),\n\t\t\t...(this.options.getExtraMetadata?.() ?? []).map((line) => labelled(line.label, line.value)),\n\t\t];\n\t}\n\n\tprivate padLine(content: string, safeWidth: number, paddingX: number): string {\n\t\tconst trailing = Math.max(0, safeWidth - paddingX - visibleWidth(content));\n\t\treturn \" \".repeat(paddingX) + content + \" \".repeat(trailing);\n\t}\n}\n"]}
1
+ {"version":3,"file":"splash-header.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/splash-header.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport {\n\ttype BrandMark,\n\tcustomBrandMark,\n\ttype MarkRow,\n\ttype MarkSymbolPreset,\n\tselectBrandMark,\n} from \"../../../themes/apex-logo.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { formatCwdForFooter } from \"./footer.ts\";\n\n/** One `label value` row in the metadata column. */\nexport interface SplashHeaderMetadataLine {\n\tlabel: string;\n\tvalue: string;\n}\n\nexport interface ApexSplashHeaderOptions {\n\t/** Rows appended after `cwd`, for extensions that want to contribute context. */\n\tgetExtraMetadata?: () => readonly SplashHeaderMetadataLine[];\n\t/**\n\t * One line of counted totals, rendered between the rules under the mark.\n\t * The header does not know how to count anything; the caller supplies the\n\t * finished line so the resource loader stays on its own side of the seam.\n\t */\n\tgetInventory?: () => string | undefined;\n\t/**\n\t * Affordance appended to the inventory line, e.g. the command that shows the\n\t * detail. Dropped whole rather than clipped, so it never renders as a stub.\n\t */\n\tinventoryHint?: string;\n\t/** Hint rendered under the metadata block. Return undefined for none. */\n\tgetHint?: () => string | undefined;\n\t/**\n\t * One row of entry-point sigils under the hint, already coloured by the\n\t * caller. The header does not know the keybindings, so it does not build it.\n\t */\n\tgetShortcuts?: () => string | undefined;\n\t/** Resolves `terminal.symbolPreset`. Defaults to unicode. */\n\tgetSymbolPreset?: () => MarkSymbolPreset;\n\t/** Emit a leading blank line above the mark. */\n\ttopPadding?: boolean;\n\t/** Override the mark. Used by tests and by extensions that rebrand. */\n\tlogo?: string;\n}\n\n/**\n * Paint one mark row: accent run then text run.\n *\n * Empty runs are skipped so no row carries a bare, unclosed SGR pair.\n */\nexport function paintMarkRow(row: MarkRow): string {\n\tconst accent = row.accent ? theme.fg(\"accent\", row.accent) : \"\";\n\tconst text = row.text ? theme.fg(\"text\", row.text) : \"\";\n\treturn accent + text;\n}\n\n/** The whole mark as painted lines, for surfaces that just want to stamp it. */\nexport function paintBrandMark(brandMark: BrandMark): string[] {\n\treturn brandMark.rows.map(paintMarkRow);\n}\n\nconst GUTTER = 4;\nconst LABEL_WIDTH = 9;\n/** Smallest value column worth rendering; below this the metadata column is dropped. */\nconst MIN_VALUE_WIDTH = 8;\n/** Below this the rules and the inventory line are dropped rather than clipped. */\nconst MIN_RULE_WIDTH = 12;\n\n/**\n * Keep the rightmost path segments that fit, so the leaf directory stays visible.\n *\n * `truncateToWidth` clips the tail, which for a path hides the part that\n * identifies the session. This clips the head instead.\n */\nfunction truncatePathTail(path: string, maxWidth: number): string {\n\tif (maxWidth <= 0) {\n\t\treturn \"\";\n\t}\n\tif (visibleWidth(path) <= maxWidth) {\n\t\treturn path;\n\t}\n\tconst ellipsis = \"…/\";\n\tconst budget = Math.max(0, maxWidth - visibleWidth(ellipsis));\n\tconst segments = path.split(\"/\");\n\tlet tail = \"\";\n\tfor (let index = segments.length - 1; index >= 0; index--) {\n\t\tconst candidate = tail ? `${segments[index]}/${tail}` : segments[index];\n\t\tif (visibleWidth(candidate) > budget) {\n\t\t\tbreak;\n\t\t}\n\t\ttail = candidate;\n\t}\n\t// A single segment longer than the whole budget: clip it from the left.\n\treturn tail ? ellipsis + tail : ellipsis + trailingByWidth(path, budget);\n}\n\n/**\n * The widest suffix of `text` that fits `maxWidth` terminal columns.\n *\n * Walks grapheme clusters from the right rather than using `String.slice`, whose\n * UTF-16 offsets would cut an astral character in half — emitting a lone\n * surrogate — and would miscount every double-width character on the way.\n */\nfunction trailingByWidth(text: string, maxWidth: number): string {\n\tif (maxWidth <= 0) {\n\t\treturn \"\";\n\t}\n\tconst graphemes = [...new Intl.Segmenter(undefined, { granularity: \"grapheme\" }).segment(text)].map(\n\t\t(entry) => entry.segment,\n\t);\n\tlet width = 0;\n\tlet index = graphemes.length;\n\twhile (index > 0) {\n\t\tconst next = width + visibleWidth(graphemes[index - 1]);\n\t\tif (next > maxWidth) {\n\t\t\tbreak;\n\t\t}\n\t\twidth = next;\n\t\tindex -= 1;\n\t}\n\treturn graphemes.slice(index).join(\"\");\n}\n\n/**\n * Startup header: brand mark on the left, runtime metadata on the right, then a\n * ruled band carrying the counted inventory.\n *\n * Values are read through getters rather than captured at construction, so the\n * model fills in once the session binds without an explicit refresh.\n *\n * The metadata column is dropped when the terminal cannot hold it, and the mark\n * falls back to a type lockup rather than being truncated mid-glyph.\n */\nexport class ApexSplashHeader implements Component {\n\tprivate readonly version: string;\n\tprivate readonly getModelId: () => string | undefined;\n\tprivate readonly getCwd: () => string;\n\tprivate readonly verboseInstructions: string | undefined;\n\tprivate readonly options: ApexSplashHeaderOptions;\n\n\tconstructor(\n\t\tversion: string,\n\t\tgetModelId: () => string | undefined,\n\t\tgetCwd: () => string,\n\t\tverboseInstructions?: string,\n\t\toptions: ApexSplashHeaderOptions = {},\n\t) {\n\t\tthis.version = version;\n\t\tthis.getModelId = getModelId;\n\t\tthis.getCwd = getCwd;\n\t\tthis.verboseInstructions = verboseInstructions;\n\t\tthis.options = options;\n\t}\n\n\tinvalidate(): void {\n\t\t// Render output is derived from current theme and session state.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst safeWidth = Math.max(1, width);\n\t\tconst paddingX = safeWidth > 1 ? 1 : 0;\n\t\tconst contentWidth = Math.max(1, safeWidth - paddingX * 2);\n\t\tconst symbolPreset = this.options.getSymbolPreset?.() ?? \"unicode\";\n\n\t\tconst brandMark = this.options.logo\n\t\t\t? customBrandMark(this.options.logo)\n\t\t\t: selectBrandMark(contentWidth, symbolPreset);\n\n\t\tconst metaWidth = contentWidth - brandMark.width - GUTTER;\n\t\tconst showMeta = metaWidth >= LABEL_WIDTH + MIN_VALUE_WIDTH;\n\t\tconst valueWidth = Math.max(1, metaWidth - LABEL_WIDTH);\n\t\tconst metaLines = showMeta ? this.buildMetadata(valueWidth) : [];\n\t\t// Bottom-aligned, so the last metadata row sits on the mark's ember\n\t\t// baseline and the two read as one block rather than two stacks.\n\t\tconst metaStart = Math.max(0, brandMark.rows.length - metaLines.length);\n\n\t\tconst lines = this.options.topPadding ? [this.padLine(\"\", safeWidth, paddingX)] : [];\n\n\t\t// The mark and the metadata column are two stacks of different heights\n\t\t// sharing one block, so the block is as tall as the taller of them. A\n\t\t// one-row mark beside three metadata rows still shows all three.\n\t\tconst blockRows = Math.max(brandMark.rows.length, metaStart + metaLines.length);\n\t\tfor (let index = 0; index < blockRows; index++) {\n\t\t\tconst row = brandMark.rows[index];\n\t\t\tconst painted = row ? paintMarkRow(row) : \"\";\n\t\t\tconst rowWidth = row ? visibleWidth(row.accent + row.text) : 0;\n\t\t\tconst meta = index >= metaStart ? (metaLines[index - metaStart] ?? \"\") : \"\";\n\t\t\tconst gap = showMeta ? \" \".repeat(Math.max(0, brandMark.width - rowWidth + GUTTER)) : \"\";\n\t\t\tconst content = truncateToWidth(painted + (meta ? gap + meta : \"\"), contentWidth, \"\");\n\t\t\tlines.push(this.padLine(content, safeWidth, paddingX));\n\t\t}\n\n\t\t// Counted totals sit in a ruled band of their own. The rules are what\n\t\t// carry the structure on this screen, so nothing here is boxed.\n\t\tconst inventory = this.options.getInventory?.();\n\t\tif (inventory && contentWidth >= MIN_RULE_WIDTH) {\n\t\t\tconst rule = theme.fg(\"borderMuted\", (symbolPreset === \"ascii\" ? \"-\" : \"─\").repeat(contentWidth));\n\t\t\tlines.push(this.padLine(\"\", safeWidth, paddingX));\n\t\t\tlines.push(this.padLine(rule, safeWidth, paddingX));\n\t\t\tconst hint = this.options.inventoryHint;\n\t\t\tconst withHint = hint ? `${inventory} ${theme.fg(\"dim\", hint)}` : inventory;\n\t\t\tconst fitted =\n\t\t\t\tvisibleWidth(withHint) <= contentWidth ? withHint : truncateToWidth(inventory, contentWidth, \"\");\n\t\t\tlines.push(this.padLine(fitted, safeWidth, paddingX));\n\t\t\tlines.push(this.padLine(rule, safeWidth, paddingX));\n\t\t}\n\n\t\t// The hint is product copy rather than a runtime fact, so it sits under the\n\t\t// whole block at full width instead of inside the narrow metadata column,\n\t\t// where it would truncate even on a wide terminal.\n\t\tconst hint = this.options.getHint?.();\n\t\tif (hint) {\n\t\t\tlines.push(this.padLine(\"\", safeWidth, paddingX));\n\t\t\tlines.push(this.padLine(theme.fg(\"dim\", truncateToWidth(hint, contentWidth)), safeWidth, paddingX));\n\t\t}\n\n\t\tconst shortcuts = this.options.getShortcuts?.();\n\t\tif (shortcuts) {\n\t\t\tlines.push(this.padLine(\"\", safeWidth, paddingX));\n\t\t\tlines.push(this.padLine(truncateToWidth(shortcuts, contentWidth, \"\"), safeWidth, paddingX));\n\t\t}\n\n\t\tif (this.verboseInstructions) {\n\t\t\tlines.push(this.padLine(\"\", safeWidth, paddingX));\n\t\t\tfor (const instruction of this.verboseInstructions.split(\"\\n\")) {\n\t\t\t\tlines.push(this.padLine(truncateToWidth(instruction, contentWidth), safeWidth, paddingX));\n\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\tprivate buildMetadata(valueWidth: number): string[] {\n\t\tconst labelled = (label: string, value: string) =>\n\t\t\ttheme.fg(\"dim\", label.padEnd(LABEL_WIDTH)) + theme.fg(\"muted\", truncateToWidth(value, valueWidth));\n\n\t\tconst cwd = formatCwdForFooter(this.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\t\treturn [\n\t\t\tlabelled(\"version\", `v${this.version}`),\n\t\t\tlabelled(\"model\", this.getModelId() ?? \"—\"),\n\t\t\ttheme.fg(\"dim\", \"cwd\".padEnd(LABEL_WIDTH)) + theme.fg(\"muted\", truncatePathTail(cwd, valueWidth)),\n\t\t\t...(this.options.getExtraMetadata?.() ?? []).map((line) => labelled(line.label, line.value)),\n\t\t];\n\t}\n\n\tprivate padLine(content: string, safeWidth: number, paddingX: number): string {\n\t\tconst trailing = Math.max(0, safeWidth - paddingX - visibleWidth(content));\n\t\treturn \" \".repeat(paddingX) + content + \" \".repeat(trailing);\n\t}\n}\n"]}
@@ -1,9 +1,11 @@
1
1
  import { Container, type TUI } from "@earendil-works/pi-tui";
2
2
  import type { ToolDefinition } from "../../../core/extensions/types.ts";
3
3
  import type { ApexToolDefinition } from "../../../core/tools/contract.ts";
4
+ import { type ToolSymbolPreset } from "./tool-panel.ts";
4
5
  export interface ToolExecutionOptions {
5
6
  showImages?: boolean;
6
7
  imageWidthCells?: number;
8
+ symbolPreset?: ToolSymbolPreset;
7
9
  }
8
10
  export declare class ToolExecutionComponent extends Container {
9
11
  private contentBox;
@@ -26,7 +28,11 @@ export declare class ToolExecutionComponent extends Container {
26
28
  private ui;
27
29
  private cwd;
28
30
  private executionStarted;
31
+ private executionStartedAt?;
32
+ private executionEndedAt?;
33
+ private elapsedTimer?;
29
34
  private argsComplete;
35
+ private symbolPreset;
30
36
  private result?;
31
37
  private convertedImages;
32
38
  private hideComponent;
@@ -51,11 +57,19 @@ export declare class ToolExecutionComponent extends Container {
51
57
  details?: any;
52
58
  isError: boolean;
53
59
  }, isPartial?: boolean): void;
60
+ dispose(): void;
61
+ private startElapsedTimer;
62
+ private stopElapsedTimer;
54
63
  private maybeConvertImagesForKitty;
55
64
  setExpanded(expanded: boolean): void;
56
65
  setShowImages(show: boolean): void;
57
66
  setImageWidthCells(width: number): void;
58
67
  invalidate(): void;
68
+ private getLifecycle;
69
+ private getDurationMs;
70
+ private renderWithStatus;
71
+ private renderDisclosureHint;
72
+ private renderCollapsedError;
59
73
  render(width: number): string[];
60
74
  private updateDisplay;
61
75
  private getTextOutput;
@@ -1 +1 @@
1
- {"version":3,"file":"tool-execution.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-execution.ts"],"names":[],"mappings":"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","sourcesContent":["import { Box, type Component, Container, getCapabilities, Image, Spacer, Text, type TUI } from \"@earendil-works/pi-tui\";\nimport type { ToolDefinition, ToolRenderContext } from \"../../../core/extensions/types.ts\";\nimport type { ApexToolDefinition } from \"../../../core/tools/contract.ts\";\nimport { createAllToolDefinitions, type ToolName } from \"../../../core/tools/index.ts\";\nimport { getTextOutput as getRenderedTextOutput } from \"../../../core/tools/render-utils.ts\";\nimport { convertToPng } from \"../../../utils/image-convert.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\nexport interface ToolExecutionOptions {\n\tshowImages?: boolean;\n\timageWidthCells?: number;\n}\n\nexport class ToolExecutionComponent extends Container {\n\tprivate contentBox: Box;\n\tprivate contentText: Text;\n\tprivate selfRenderContainer: Container;\n\tprivate callRendererComponent?: Component;\n\tprivate resultRendererComponent?: Component;\n\tprivate rendererState: any = {};\n\tprivate imageComponents: Image[] = [];\n\tprivate imageSpacers: Spacer[] = [];\n\tprivate toolName: string;\n\tprivate toolCallId: string;\n\tprivate args: any;\n\tprivate expanded = false;\n\tprivate showImages: boolean;\n\tprivate imageWidthCells: number;\n\tprivate isPartial = true;\n\tprivate toolDefinition?: ToolDefinition<any, any> | ApexToolDefinition<any, any>;\n\tprivate builtInToolDefinition?: ToolDefinition<any, any> | ApexToolDefinition<any, any>;\n\tprivate ui: TUI;\n\tprivate cwd: string;\n\tprivate executionStarted = false;\n\tprivate argsComplete = false;\n\tprivate result?: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tisError: boolean;\n\t\tdetails?: any;\n\t};\n\tprivate convertedImages: Map<number, { data: string; mimeType: string }> = new Map();\n\tprivate hideComponent = false;\n\n\tconstructor(\n\t\ttoolName: string,\n\t\ttoolCallId: string,\n\t\targs: any,\n\t\toptions: ToolExecutionOptions = {},\n\t\ttoolDefinition: ToolDefinition<any, any> | ApexToolDefinition<any, any> | undefined,\n\t\tui: TUI,\n\t\tcwd: string,\n\t) {\n\t\tsuper();\n\t\tthis.toolName = toolName;\n\t\tthis.toolCallId = toolCallId;\n\t\tthis.args = args;\n\t\tthis.toolDefinition = toolDefinition;\n\t\tthis.builtInToolDefinition = createAllToolDefinitions(cwd)[toolName as ToolName];\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.imageWidthCells = options.imageWidthCells ?? 60;\n\t\tthis.ui = ui;\n\t\tthis.cwd = cwd;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Always create all shell variants. contentBox is used for default renderer-based composition.\n\t\t// selfRenderContainer is used when the tool renders its own framing.\n\t\t// contentText is reserved for generic fallback rendering when no tool definition exists.\n\t\tthis.contentBox = new Box(1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\t\tthis.contentText = new Text(\"\", 1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\t\tthis.selfRenderContainer = new Container();\n\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tthis.addChild(this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox);\n\t\t} else {\n\t\t\tthis.addChild(this.contentText);\n\t\t}\n\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate getCallRenderer(): ToolDefinition<any, any>[\"renderCall\"] | undefined {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderCall;\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderCall;\n\t\t}\n\t\treturn this.toolDefinition.renderCall ?? this.builtInToolDefinition.renderCall;\n\t}\n\n\tprivate getResultRenderer(): ToolDefinition<any, any>[\"renderResult\"] | undefined {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderResult;\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderResult;\n\t\t}\n\t\treturn this.toolDefinition.renderResult ?? this.builtInToolDefinition.renderResult;\n\t}\n\n\tprivate hasRendererDefinition(): boolean {\n\t\treturn this.builtInToolDefinition !== undefined || this.toolDefinition !== undefined;\n\t}\n\n\tprivate getRenderShell(): \"default\" | \"self\" {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderShell ?? \"default\";\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderShell ?? \"default\";\n\t\t}\n\t\treturn this.toolDefinition.renderShell ?? this.builtInToolDefinition.renderShell ?? \"default\";\n\t}\n\n\tprivate getRenderContext(lastComponent: Component | undefined): ToolRenderContext {\n\t\treturn {\n\t\t\targs: this.args,\n\t\t\ttoolCallId: this.toolCallId,\n\t\t\tinvalidate: () => {\n\t\t\t\tthis.invalidate();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\tlastComponent,\n\t\t\tstate: this.rendererState,\n\t\t\tcwd: this.cwd,\n\t\t\texecutionStarted: this.executionStarted,\n\t\t\targsComplete: this.argsComplete,\n\t\t\tisPartial: this.isPartial,\n\t\t\texpanded: this.expanded,\n\t\t\tshowImages: this.showImages,\n\t\t\tisError: this.result?.isError ?? false,\n\t\t};\n\t}\n\n\tprivate createCallFallback(): Component {\n\t\treturn new Text(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 0, 0);\n\t}\n\n\tprivate createResultFallback(): Component | undefined {\n\t\tconst output = this.getTextOutput();\n\t\tif (!output) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn new Text(theme.fg(\"toolOutput\", output), 0, 0);\n\t}\n\n\tupdateArgs(args: any): void {\n\t\tthis.args = args;\n\t\tthis.updateDisplay();\n\t}\n\n\tmarkExecutionStarted(): void {\n\t\tthis.executionStarted = true;\n\t\tthis.updateDisplay();\n\t\tthis.ui.requestRender();\n\t}\n\n\tsetArgsComplete(): void {\n\t\tthis.argsComplete = true;\n\t\tthis.updateDisplay();\n\t\tthis.ui.requestRender();\n\t}\n\n\tupdateResult(\n\t\tresult: {\n\t\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\t\tdetails?: any;\n\t\t\tisError: boolean;\n\t\t},\n\t\tisPartial = false,\n\t): void {\n\t\tthis.result = result;\n\t\tthis.isPartial = isPartial;\n\t\tthis.updateDisplay();\n\t\tthis.maybeConvertImagesForKitty();\n\t}\n\n\tprivate maybeConvertImagesForKitty(): void {\n\t\tconst caps = getCapabilities();\n\t\tif (caps.images !== \"kitty\") return;\n\t\tif (!this.result) return;\n\n\t\tconst imageBlocks = this.result.content.filter((c) => c.type === \"image\");\n\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\tconst img = imageBlocks[i];\n\t\t\tif (!img.data || !img.mimeType) continue;\n\t\t\tif (img.mimeType === \"image/png\") continue;\n\t\t\tif (this.convertedImages.has(i)) continue;\n\n\t\t\tconst index = i;\n\t\t\tconvertToPng(img.data, img.mimeType).then((converted) => {\n\t\t\t\tif (converted) {\n\t\t\t\t\tthis.convertedImages.set(index, converted);\n\t\t\t\t\tthis.updateDisplay();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tthis.showImages = show;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetImageWidthCells(width: number): void {\n\t\tthis.imageWidthCells = Math.max(1, Math.floor(width));\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.hideComponent) {\n\t\t\treturn [];\n\t\t}\n\n\t\tif (this.hasRendererDefinition() && this.getRenderShell() === \"self\") {\n\t\t\tconst contentLines = this.selfRenderContainer.render(width);\n\t\t\tif (contentLines.length === 0 && this.imageComponents.length === 0) {\n\t\t\t\treturn [];\n\t\t\t}\n\n\t\t\tconst lines: string[] = [];\n\t\t\tif (contentLines.length > 0) {\n\t\t\t\tlines.push(\"\");\n\t\t\t\tlines.push(...contentLines);\n\t\t\t}\n\t\t\tfor (let i = 0; i < this.imageComponents.length; i++) {\n\t\t\t\tconst spacer = this.imageSpacers[i];\n\t\t\t\tif (spacer) {\n\t\t\t\t\tlines.push(...spacer.render(width));\n\t\t\t\t}\n\t\t\t\tconst imageComponent = this.imageComponents[i];\n\t\t\t\tif (imageComponent) {\n\t\t\t\t\tlines.push(...imageComponent.render(width));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\treturn super.render(width);\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tconst bgFn = this.isPartial\n\t\t\t? (text: string) => theme.bg(\"toolPendingBg\", text)\n\t\t\t: this.result?.isError\n\t\t\t\t? (text: string) => theme.bg(\"toolErrorBg\", text)\n\t\t\t\t: (text: string) => theme.bg(\"toolSuccessBg\", text);\n\n\t\tlet hasContent = false;\n\t\tthis.hideComponent = false;\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tconst renderContainer = this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox;\n\t\t\tif (renderContainer instanceof Box) {\n\t\t\t\trenderContainer.setBgFn(bgFn);\n\t\t\t}\n\t\t\trenderContainer.clear();\n\n\t\t\tconst callRenderer = this.getCallRenderer();\n\t\t\tif (!callRenderer) {\n\t\t\t\trenderContainer.addChild(this.createCallFallback());\n\t\t\t\thasContent = true;\n\t\t\t} else {\n\t\t\t\ttry {\n\t\t\t\t\tconst component = callRenderer(this.args, theme, this.getRenderContext(this.callRendererComponent));\n\t\t\t\t\tthis.callRendererComponent = component;\n\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\thasContent = true;\n\t\t\t\t} catch {\n\t\t\t\t\tthis.callRendererComponent = undefined;\n\t\t\t\t\trenderContainer.addChild(this.createCallFallback());\n\t\t\t\t\thasContent = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst resultRenderer = this.getResultRenderer();\n\t\t\t\tif (!resultRenderer) {\n\t\t\t\t\tconst component = this.createResultFallback();\n\t\t\t\t\tif (component) {\n\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst component = resultRenderer(\n\t\t\t\t\t\t\t{ content: this.result.content as any, details: this.result.details },\n\t\t\t\t\t\t\t{ expanded: this.expanded, isPartial: this.isPartial },\n\t\t\t\t\t\t\ttheme,\n\t\t\t\t\t\t\tthis.getRenderContext(this.resultRendererComponent),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.resultRendererComponent = component;\n\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tthis.resultRendererComponent = undefined;\n\t\t\t\t\t\tconst component = this.createResultFallback();\n\t\t\t\t\t\tif (component) {\n\t\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tthis.contentText.setCustomBgFn(bgFn);\n\t\t\tthis.contentText.setText(this.formatToolExecution());\n\t\t\thasContent = true;\n\t\t}\n\n\t\tfor (const img of this.imageComponents) {\n\t\t\tthis.removeChild(img);\n\t\t}\n\t\tthis.imageComponents = [];\n\t\tfor (const spacer of this.imageSpacers) {\n\t\t\tthis.removeChild(spacer);\n\t\t}\n\t\tthis.imageSpacers = [];\n\n\t\tif (this.result) {\n\t\t\tconst imageBlocks = this.result.content.filter((c) => c.type === \"image\");\n\t\t\tconst caps = getCapabilities();\n\t\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\t\tconst img = imageBlocks[i];\n\t\t\t\tif (caps.images && this.showImages && img.data && img.mimeType) {\n\t\t\t\t\tconst converted = this.convertedImages.get(i);\n\t\t\t\t\tconst imageData = converted?.data ?? img.data;\n\t\t\t\t\tconst imageMimeType = converted?.mimeType ?? img.mimeType;\n\t\t\t\t\tif (caps.images === \"kitty\" && imageMimeType !== \"image/png\") continue;\n\n\t\t\t\t\tconst spacer = new Spacer(1);\n\t\t\t\t\tthis.addChild(spacer);\n\t\t\t\t\tthis.imageSpacers.push(spacer);\n\t\t\t\t\tconst imageComponent = new Image(\n\t\t\t\t\t\timageData,\n\t\t\t\t\t\timageMimeType,\n\t\t\t\t\t\t{ fallbackColor: (s: string) => theme.fg(\"toolOutput\", s) },\n\t\t\t\t\t\t{ maxWidthCells: this.imageWidthCells },\n\t\t\t\t\t);\n\t\t\t\t\tthis.imageComponents.push(imageComponent);\n\t\t\t\t\tthis.addChild(imageComponent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (this.hasRendererDefinition() && !hasContent && this.imageComponents.length === 0) {\n\t\t\tthis.hideComponent = true;\n\t\t}\n\t}\n\n\tprivate getTextOutput(): string {\n\t\treturn getRenderedTextOutput(this.result, this.showImages);\n\t}\n\n\tprivate formatToolExecution(): string {\n\t\tlet text = theme.fg(\"toolTitle\", theme.bold(this.toolName));\n\t\tconst content = JSON.stringify(this.args, null, 2);\n\t\tif (content) {\n\t\t\ttext += `\\n\\n${content}`;\n\t\t}\n\t\tconst output = this.getTextOutput();\n\t\tif (output) {\n\t\t\ttext += `\\n${output}`;\n\t\t}\n\t\treturn text;\n\t}\n}\n"]}
1
+ {"version":3,"file":"tool-execution.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-execution.ts"],"names":[],"mappings":"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","sourcesContent":["import { Box, type Component, Container, getCapabilities, Image, Spacer, Text, type TUI } from \"@earendil-works/pi-tui\";\nimport type { ToolDefinition, ToolRenderContext } from \"../../../core/extensions/types.ts\";\nimport type { ApexToolDefinition } from \"../../../core/tools/contract.ts\";\nimport { createAllToolDefinitions, type ToolName } from \"../../../core/tools/index.ts\";\nimport { getTextOutput as getRenderedTextOutput } from \"../../../core/tools/render-utils.ts\";\nimport { convertToPng } from \"../../../utils/image-convert.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { keyHint } from \"./keybinding-hints.ts\";\nimport {\n\ttype ToolLifecycle,\n\tToolPanelComponent,\n\tToolStatusLineComponent,\n\ttype ToolSymbolPreset,\n} from \"./tool-panel.ts\";\n\nconst COLLAPSED_ERROR_VISUAL_LINE_LIMIT = 3;\n\nconst FALLBACK_PREVIEW_LINES = 10;\n\nexport interface ToolExecutionOptions {\n\tshowImages?: boolean;\n\timageWidthCells?: number;\n\tsymbolPreset?: ToolSymbolPreset;\n}\n\nexport class ToolExecutionComponent extends Container {\n\tprivate contentBox: Box;\n\tprivate contentText: Text;\n\tprivate selfRenderContainer: Container;\n\tprivate callRendererComponent?: Component;\n\tprivate resultRendererComponent?: Component;\n\tprivate rendererState: any = {};\n\tprivate imageComponents: Image[] = [];\n\tprivate imageSpacers: Spacer[] = [];\n\tprivate toolName: string;\n\tprivate toolCallId: string;\n\tprivate args: any;\n\tprivate expanded = false;\n\tprivate showImages: boolean;\n\tprivate imageWidthCells: number;\n\tprivate isPartial = true;\n\tprivate toolDefinition?: ToolDefinition<any, any> | ApexToolDefinition<any, any>;\n\tprivate builtInToolDefinition?: ToolDefinition<any, any> | ApexToolDefinition<any, any>;\n\tprivate ui: TUI;\n\tprivate cwd: string;\n\tprivate executionStarted = false;\n\tprivate executionStartedAt?: number;\n\tprivate executionEndedAt?: number;\n\tprivate elapsedTimer?: ReturnType<typeof setInterval>;\n\tprivate argsComplete = false;\n\tprivate symbolPreset: ToolSymbolPreset;\n\tprivate result?: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tisError: boolean;\n\t\tdetails?: any;\n\t};\n\tprivate convertedImages: Map<number, { data: string; mimeType: string }> = new Map();\n\tprivate hideComponent = false;\n\n\tconstructor(\n\t\ttoolName: string,\n\t\ttoolCallId: string,\n\t\targs: any,\n\t\toptions: ToolExecutionOptions = {},\n\t\ttoolDefinition: ToolDefinition<any, any> | ApexToolDefinition<any, any> | undefined,\n\t\tui: TUI,\n\t\tcwd: string,\n\t) {\n\t\tsuper();\n\t\tthis.toolName = toolName;\n\t\tthis.toolCallId = toolCallId;\n\t\tthis.args = args;\n\t\tthis.toolDefinition = toolDefinition;\n\t\tthis.builtInToolDefinition = createAllToolDefinitions(cwd)[toolName as ToolName];\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.imageWidthCells = options.imageWidthCells ?? 60;\n\t\tthis.symbolPreset = options.symbolPreset ?? \"unicode\";\n\t\tthis.ui = ui;\n\t\tthis.cwd = cwd;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Always create all shell variants. contentBox is used for default renderer-based composition.\n\t\t// selfRenderContainer is used when the tool renders its own framing.\n\t\t// contentText is reserved for generic fallback rendering when no tool definition exists.\n\t\tthis.contentBox = new Box(0, 0);\n\t\tthis.contentText = new Text(\"\", 0, 0);\n\t\tthis.selfRenderContainer = new Container();\n\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tthis.addChild(this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox);\n\t\t} else {\n\t\t\tthis.addChild(this.contentText);\n\t\t}\n\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate getCallRenderer(): ToolDefinition<any, any>[\"renderCall\"] | undefined {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderCall;\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderCall;\n\t\t}\n\t\treturn this.toolDefinition.renderCall ?? this.builtInToolDefinition.renderCall;\n\t}\n\n\tprivate getResultRenderer(): ToolDefinition<any, any>[\"renderResult\"] | undefined {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderResult;\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderResult;\n\t\t}\n\t\treturn this.toolDefinition.renderResult ?? this.builtInToolDefinition.renderResult;\n\t}\n\n\tprivate hasRendererDefinition(): boolean {\n\t\treturn this.builtInToolDefinition !== undefined || this.toolDefinition !== undefined;\n\t}\n\n\tprivate getRenderShell(): \"default\" | \"self\" {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderShell ?? \"default\";\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderShell ?? \"default\";\n\t\t}\n\t\treturn this.toolDefinition.renderShell ?? this.builtInToolDefinition.renderShell ?? \"default\";\n\t}\n\n\tprivate getRenderContext(lastComponent: Component | undefined): ToolRenderContext {\n\t\treturn {\n\t\t\targs: this.args,\n\t\t\ttoolCallId: this.toolCallId,\n\t\t\tinvalidate: () => {\n\t\t\t\tthis.invalidate();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\tlastComponent,\n\t\t\tstate: this.rendererState,\n\t\t\tcwd: this.cwd,\n\t\t\texecutionStarted: this.executionStarted,\n\t\t\targsComplete: this.argsComplete,\n\t\t\tisPartial: this.isPartial,\n\t\t\texpanded: this.expanded,\n\t\t\tshowImages: this.showImages,\n\t\t\tisError: this.result?.isError ?? false,\n\t\t};\n\t}\n\n\tprivate createCallFallback(): Component {\n\t\treturn new Text(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 0, 0);\n\t}\n\n\tprivate createResultFallback(): Component | undefined {\n\t\tconst output = this.getTextOutput();\n\t\tif (!output) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst displayLines = this.expanded ? lines : lines.slice(0, FALLBACK_PREVIEW_LINES);\n\t\tconst remaining = lines.length - displayLines.length;\n\t\tlet text = displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\");\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n\t\t}\n\t\treturn new Text(text, 0, 0);\n\t}\n\n\tupdateArgs(args: any): void {\n\t\tthis.args = args;\n\t\tthis.updateDisplay();\n\t}\n\n\tmarkExecutionStarted(): void {\n\t\tthis.executionStarted = true;\n\t\tthis.executionStartedAt ??= Date.now();\n\t\tthis.startElapsedTimer();\n\t\tthis.updateDisplay();\n\t\tthis.ui.requestRender();\n\t}\n\n\tsetArgsComplete(): void {\n\t\tthis.argsComplete = true;\n\t\tthis.updateDisplay();\n\t\tthis.ui.requestRender();\n\t}\n\n\tupdateResult(\n\t\tresult: {\n\t\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\t\tdetails?: any;\n\t\t\tisError: boolean;\n\t\t},\n\t\tisPartial = false,\n\t): void {\n\t\tthis.result = result;\n\t\tthis.isPartial = isPartial;\n\t\tif (!isPartial) {\n\t\t\tthis.executionEndedAt ??= Date.now();\n\t\t\tthis.stopElapsedTimer();\n\t\t}\n\t\tthis.updateDisplay();\n\t\tthis.maybeConvertImagesForKitty();\n\t}\n\n\tdispose(): void {\n\t\tthis.stopElapsedTimer();\n\t}\n\n\tprivate startElapsedTimer(): void {\n\t\tif (this.elapsedTimer) return;\n\t\tthis.elapsedTimer = setInterval(() => this.ui.requestRender(), 1000);\n\t}\n\n\tprivate stopElapsedTimer(): void {\n\t\tif (!this.elapsedTimer) return;\n\t\tclearInterval(this.elapsedTimer);\n\t\tthis.elapsedTimer = undefined;\n\t}\n\n\tprivate maybeConvertImagesForKitty(): void {\n\t\tconst caps = getCapabilities();\n\t\tif (caps.images !== \"kitty\") return;\n\t\tif (!this.result) return;\n\n\t\tconst imageBlocks = this.result.content.filter((c) => c.type === \"image\");\n\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\tconst img = imageBlocks[i];\n\t\t\tif (!img.data || !img.mimeType) continue;\n\t\t\tif (img.mimeType === \"image/png\") continue;\n\t\t\tif (this.convertedImages.has(i)) continue;\n\n\t\t\tconst index = i;\n\t\t\tconvertToPng(img.data, img.mimeType).then((converted) => {\n\t\t\t\tif (converted) {\n\t\t\t\t\tthis.convertedImages.set(index, converted);\n\t\t\t\t\tthis.updateDisplay();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tthis.showImages = show;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetImageWidthCells(width: number): void {\n\t\tthis.imageWidthCells = Math.max(1, Math.floor(width));\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate getLifecycle(): ToolLifecycle {\n\t\tif (this.result && !this.isPartial) return this.result.isError ? \"error\" : \"done\";\n\t\treturn this.executionStarted ? \"running\" : \"queued\";\n\t}\n\n\tprivate getDurationMs(): number | undefined {\n\t\tif (this.executionStartedAt === undefined) return undefined;\n\t\treturn (this.executionEndedAt ?? Date.now()) - this.executionStartedAt;\n\t}\n\n\tprivate renderWithStatus(component: Component, width: number): string[] {\n\t\tconst options = {\n\t\t\tlifecycle: this.getLifecycle(),\n\t\t\tsymbolPreset: this.symbolPreset,\n\t\t\tdurationMs: this.getDurationMs(),\n\t\t};\n\t\tif (this.hasRendererDefinition() && this.getRenderShell() === \"self\") {\n\t\t\treturn new ToolStatusLineComponent(component, options).render(width);\n\t\t}\n\t\treturn new ToolPanelComponent(component, options).render(width);\n\t}\n\n\tprivate renderDisclosureHint(width: number): string[] {\n\t\tif (this.expanded || !this.result || this.isPartial || width <= 0) return [];\n\t\tconst hint = keyHint(\"app.tools.expand\", \"to expand\");\n\t\treturn new Text(hint, 0, 0).render(width);\n\t}\n\n\tprivate renderCollapsedError(contentLines: string[], width: number): string[] {\n\t\tconst errorText = this.getTextOutput();\n\t\tif (!errorText || contentLines.length === 0) return contentLines;\n\n\t\tconst errorLines = new Text(theme.fg(\"toolOutput\", errorText), 0, 0).render(width);\n\t\tconst previewLines = errorLines.slice(0, COLLAPSED_ERROR_VISUAL_LINE_LIMIT);\n\t\tconst omittedLines = errorLines.length - previewLines.length;\n\t\tif (omittedLines === 0) return [...contentLines.slice(0, 1), ...previewLines];\n\n\t\treturn [...contentLines.slice(0, 1), ...previewLines, theme.fg(\"muted\", `${omittedLines} more lines omitted`)];\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.hideComponent || width <= 0) return [];\n\n\t\tconst shell = this.hasRendererDefinition()\n\t\t\t? this.getRenderShell() === \"self\"\n\t\t\t\t? this.selfRenderContainer\n\t\t\t\t: this.contentBox\n\t\t\t: this.contentText;\n\t\tconst selfRendered = this.hasRendererDefinition() && this.getRenderShell() === \"self\";\n\t\tconst panelContent: Component = selfRendered\n\t\t\t? shell\n\t\t\t: {\n\t\t\t\t\trender: (innerWidth: number) => {\n\t\t\t\t\t\tconst contentLines = shell.render(innerWidth);\n\t\t\t\t\t\tconst visibleLines =\n\t\t\t\t\t\t\tthis.result?.isError && !this.isPartial && !this.expanded\n\t\t\t\t\t\t\t\t? this.renderCollapsedError(contentLines, innerWidth)\n\t\t\t\t\t\t\t\t: contentLines;\n\t\t\t\t\t\treturn [...visibleLines, ...this.renderDisclosureHint(innerWidth)];\n\t\t\t\t\t},\n\t\t\t\t\tinvalidate: () => shell.invalidate?.(),\n\t\t\t\t};\n\t\tconst visibleContentLines = this.renderWithStatus(panelContent, width);\n\t\tif (visibleContentLines.length === 0 && this.imageComponents.length === 0) return [];\n\n\t\tconst lines: string[] = [];\n\t\tif (visibleContentLines.length > 0) {\n\t\t\tlines.push(\"\");\n\t\t\tlines.push(...visibleContentLines);\n\t\t\tif (selfRendered) lines.push(...this.renderDisclosureHint(width));\n\t\t}\n\t\tfor (let i = 0; i < this.imageComponents.length; i++) {\n\t\t\tconst spacer = this.imageSpacers[i];\n\t\t\tif (spacer) lines.push(...spacer.render(width));\n\t\t\tconst imageComponent = this.imageComponents[i];\n\t\t\tif (imageComponent) lines.push(...imageComponent.render(width));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tlet hasContent = false;\n\t\tthis.hideComponent = false;\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tconst renderContainer = this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox;\n\t\t\trenderContainer.clear();\n\n\t\t\tconst callRenderer = this.getCallRenderer();\n\t\t\tif (!callRenderer) {\n\t\t\t\trenderContainer.addChild(this.createCallFallback());\n\t\t\t\thasContent = true;\n\t\t\t} else {\n\t\t\t\ttry {\n\t\t\t\t\tconst component = callRenderer(this.args, theme, this.getRenderContext(this.callRendererComponent));\n\t\t\t\t\tthis.callRendererComponent = component;\n\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\thasContent = true;\n\t\t\t\t} catch {\n\t\t\t\t\tthis.callRendererComponent = undefined;\n\t\t\t\t\trenderContainer.addChild(this.createCallFallback());\n\t\t\t\t\thasContent = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst resultRenderer = this.getResultRenderer();\n\t\t\t\tif (!resultRenderer) {\n\t\t\t\t\tconst component = this.createResultFallback();\n\t\t\t\t\tif (component) {\n\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst component = resultRenderer(\n\t\t\t\t\t\t\t{ content: this.result.content as any, details: this.result.details },\n\t\t\t\t\t\t\t{ expanded: this.expanded, isPartial: this.isPartial },\n\t\t\t\t\t\t\ttheme,\n\t\t\t\t\t\t\tthis.getRenderContext(this.resultRendererComponent),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.resultRendererComponent = component;\n\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tthis.resultRendererComponent = undefined;\n\t\t\t\t\t\tconst component = this.createResultFallback();\n\t\t\t\t\t\tif (component) {\n\t\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tthis.contentText.setText(this.formatToolExecution());\n\t\t\thasContent = true;\n\t\t}\n\n\t\tfor (const img of this.imageComponents) {\n\t\t\tthis.removeChild(img);\n\t\t}\n\t\tthis.imageComponents = [];\n\t\tfor (const spacer of this.imageSpacers) {\n\t\t\tthis.removeChild(spacer);\n\t\t}\n\t\tthis.imageSpacers = [];\n\n\t\tif (this.result) {\n\t\t\tconst imageBlocks = this.result.content.filter((c) => c.type === \"image\");\n\t\t\tconst caps = getCapabilities();\n\t\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\t\tconst img = imageBlocks[i];\n\t\t\t\tif (caps.images && this.showImages && img.data && img.mimeType) {\n\t\t\t\t\tconst converted = this.convertedImages.get(i);\n\t\t\t\t\tconst imageData = converted?.data ?? img.data;\n\t\t\t\t\tconst imageMimeType = converted?.mimeType ?? img.mimeType;\n\t\t\t\t\tif (caps.images === \"kitty\" && imageMimeType !== \"image/png\") continue;\n\n\t\t\t\t\tconst spacer = new Spacer(1);\n\t\t\t\t\tthis.addChild(spacer);\n\t\t\t\t\tthis.imageSpacers.push(spacer);\n\t\t\t\t\tconst imageComponent = new Image(\n\t\t\t\t\t\timageData,\n\t\t\t\t\t\timageMimeType,\n\t\t\t\t\t\t{ fallbackColor: (s: string) => theme.fg(\"toolOutput\", s) },\n\t\t\t\t\t\t{ maxWidthCells: this.imageWidthCells },\n\t\t\t\t\t);\n\t\t\t\t\tthis.imageComponents.push(imageComponent);\n\t\t\t\t\tthis.addChild(imageComponent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (this.hasRendererDefinition() && !hasContent && this.imageComponents.length === 0) {\n\t\t\tthis.hideComponent = true;\n\t\t}\n\t}\n\n\tprivate getTextOutput(): string {\n\t\treturn getRenderedTextOutput(this.result, this.showImages);\n\t}\n\n\tprivate formatToolExecution(): string {\n\t\tlet text = theme.fg(\"toolTitle\", theme.bold(this.toolName));\n\t\tconst content = JSON.stringify(this.args, null, 2);\n\t\tif (content) {\n\t\t\ttext += `\\n\\n${content}`;\n\t\t}\n\t\tconst output = this.getTextOutput();\n\t\tif (output) {\n\t\t\ttext += `\\n${output}`;\n\t\t}\n\t\treturn text;\n\t}\n}\n"]}