apex-code 0.3.0 → 0.5.0

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 (213) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/client/transcript.d.ts.map +1 -1
  3. package/dist/client/transcript.js +1 -1
  4. package/dist/client/transcript.js.map +1 -1
  5. package/dist/core/agent-session-services.d.ts.map +1 -1
  6. package/dist/core/agent-session-services.js +2 -1
  7. package/dist/core/agent-session-services.js.map +1 -1
  8. package/dist/core/delegation/runtime.d.ts.map +1 -1
  9. package/dist/core/delegation/runtime.js +2 -2
  10. package/dist/core/delegation/runtime.js.map +1 -1
  11. package/dist/core/durable-state/daemon.d.ts.map +1 -1
  12. package/dist/core/durable-state/daemon.js +1 -1
  13. package/dist/core/durable-state/daemon.js.map +1 -1
  14. package/dist/core/extensions/types.d.ts +1 -1
  15. package/dist/core/extensions/types.d.ts.map +1 -1
  16. package/dist/core/extensions/types.js.map +1 -1
  17. package/dist/core/keybindings.d.ts +1 -1
  18. package/dist/core/keybindings.d.ts.map +1 -1
  19. package/dist/core/keybindings.js.map +1 -1
  20. package/dist/core/mcp/mcp-tool.d.ts.map +1 -1
  21. package/dist/core/mcp/mcp-tool.js +2 -4
  22. package/dist/core/mcp/mcp-tool.js.map +1 -1
  23. package/dist/core/mcp/oauth/authorize.d.ts.map +1 -1
  24. package/dist/core/mcp/oauth/authorize.js +7 -2
  25. package/dist/core/mcp/oauth/authorize.js.map +1 -1
  26. package/dist/core/mcp/runtime.d.ts +2 -2
  27. package/dist/core/mcp/runtime.d.ts.map +1 -1
  28. package/dist/core/mcp/runtime.js +8 -3
  29. package/dist/core/mcp/runtime.js.map +1 -1
  30. package/dist/core/permissions/store.d.ts +7 -2
  31. package/dist/core/permissions/store.d.ts.map +1 -1
  32. package/dist/core/permissions/store.js +6 -1
  33. package/dist/core/permissions/store.js.map +1 -1
  34. package/dist/core/project-resources.d.ts +12 -0
  35. package/dist/core/project-resources.d.ts.map +1 -1
  36. package/dist/core/project-resources.js +19 -0
  37. package/dist/core/project-resources.js.map +1 -1
  38. package/dist/core/provider-composer.d.ts.map +1 -1
  39. package/dist/core/provider-composer.js +10 -1
  40. package/dist/core/provider-composer.js.map +1 -1
  41. package/dist/core/resource-loader.d.ts +6 -0
  42. package/dist/core/resource-loader.d.ts.map +1 -1
  43. package/dist/core/resource-loader.js +9 -3
  44. package/dist/core/resource-loader.js.map +1 -1
  45. package/dist/core/sdk.d.ts +1 -1
  46. package/dist/core/sdk.d.ts.map +1 -1
  47. package/dist/core/sdk.js +3 -3
  48. package/dist/core/sdk.js.map +1 -1
  49. package/dist/core/settings-manager.d.ts +16 -3
  50. package/dist/core/settings-manager.d.ts.map +1 -1
  51. package/dist/core/settings-manager.js +39 -16
  52. package/dist/core/settings-manager.js.map +1 -1
  53. package/dist/core/tools/bash.d.ts +11 -7
  54. package/dist/core/tools/bash.d.ts.map +1 -1
  55. package/dist/core/tools/bash.js +37 -46
  56. package/dist/core/tools/bash.js.map +1 -1
  57. package/dist/core/tools/delegate.d.ts.map +1 -1
  58. package/dist/core/tools/delegate.js +2 -2
  59. package/dist/core/tools/delegate.js.map +1 -1
  60. package/dist/core/tools/edit.d.ts.map +1 -1
  61. package/dist/core/tools/edit.js +3 -3
  62. package/dist/core/tools/edit.js.map +1 -1
  63. package/dist/core/tools/find.d.ts.map +1 -1
  64. package/dist/core/tools/find.js +4 -4
  65. package/dist/core/tools/find.js.map +1 -1
  66. package/dist/core/tools/grep.d.ts.map +1 -1
  67. package/dist/core/tools/grep.js +4 -4
  68. package/dist/core/tools/grep.js.map +1 -1
  69. package/dist/core/tools/ls.d.ts.map +1 -1
  70. package/dist/core/tools/ls.js +4 -4
  71. package/dist/core/tools/ls.js.map +1 -1
  72. package/dist/core/tools/path-utils.d.ts +9 -0
  73. package/dist/core/tools/path-utils.d.ts.map +1 -1
  74. package/dist/core/tools/path-utils.js +235 -10
  75. package/dist/core/tools/path-utils.js.map +1 -1
  76. package/dist/core/tools/read.d.ts.map +1 -1
  77. package/dist/core/tools/read.js +11 -8
  78. package/dist/core/tools/read.js.map +1 -1
  79. package/dist/core/tools/write.d.ts.map +1 -1
  80. package/dist/core/tools/write.js +7 -8
  81. package/dist/core/tools/write.js.map +1 -1
  82. package/dist/core/workspace/state.d.ts.map +1 -1
  83. package/dist/core/workspace/state.js +1 -1
  84. package/dist/core/workspace/state.js.map +1 -1
  85. package/dist/extensions/llama/ui.d.ts.map +1 -1
  86. package/dist/extensions/llama/ui.js +3 -2
  87. package/dist/extensions/llama/ui.js.map +1 -1
  88. package/dist/main.d.ts.map +1 -1
  89. package/dist/main.js +3 -1
  90. package/dist/main.js.map +1 -1
  91. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  92. package/dist/modes/interactive/components/assistant-message.js +0 -10
  93. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  94. package/dist/modes/interactive/components/autocomplete-source.d.ts +8 -0
  95. package/dist/modes/interactive/components/autocomplete-source.d.ts.map +1 -0
  96. package/dist/modes/interactive/components/autocomplete-source.js +23 -0
  97. package/dist/modes/interactive/components/autocomplete-source.js.map +1 -0
  98. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  99. package/dist/modes/interactive/components/bash-execution.js +6 -4
  100. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  101. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  102. package/dist/modes/interactive/components/bordered-loader.js +1 -1
  103. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  104. package/dist/modes/interactive/components/composer-dock.d.ts +12 -0
  105. package/dist/modes/interactive/components/composer-dock.d.ts.map +1 -0
  106. package/dist/modes/interactive/components/composer-dock.js +24 -0
  107. package/dist/modes/interactive/components/composer-dock.js.map +1 -0
  108. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  109. package/dist/modes/interactive/components/config-selector.js +11 -9
  110. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  111. package/dist/modes/interactive/components/diff.d.ts.map +1 -1
  112. package/dist/modes/interactive/components/diff.js +2 -2
  113. package/dist/modes/interactive/components/diff.js.map +1 -1
  114. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  115. package/dist/modes/interactive/components/extension-editor.js +9 -9
  116. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  117. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  118. package/dist/modes/interactive/components/extension-input.js +9 -5
  119. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  120. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  121. package/dist/modes/interactive/components/extension-selector.js +11 -10
  122. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  123. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  124. package/dist/modes/interactive/components/first-time-setup.js +6 -6
  125. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  126. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  127. package/dist/modes/interactive/components/footer.js +8 -3
  128. package/dist/modes/interactive/components/footer.js.map +1 -1
  129. package/dist/modes/interactive/components/keybinding-hints.d.ts +21 -0
  130. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  131. package/dist/modes/interactive/components/keybinding-hints.js +34 -0
  132. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  133. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  134. package/dist/modes/interactive/components/login-dialog.js +27 -23
  135. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  136. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  137. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  138. package/dist/modes/interactive/components/model-selector.js +14 -18
  139. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  140. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  141. package/dist/modes/interactive/components/oauth-selector.js +7 -6
  142. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  143. package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -1
  144. package/dist/modes/interactive/components/permission-preview.js +3 -3
  145. package/dist/modes/interactive/components/permission-preview.js.map +1 -1
  146. package/dist/modes/interactive/components/prompt-input.d.ts +12 -0
  147. package/dist/modes/interactive/components/prompt-input.d.ts.map +1 -0
  148. package/dist/modes/interactive/components/prompt-input.js +18 -0
  149. package/dist/modes/interactive/components/prompt-input.js.map +1 -0
  150. package/dist/modes/interactive/components/scoped-models-selector.d.ts +1 -1
  151. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  152. package/dist/modes/interactive/components/scoped-models-selector.js +4 -3
  153. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  154. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  155. package/dist/modes/interactive/components/session-selector.js +26 -19
  156. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  157. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  158. package/dist/modes/interactive/components/settings-selector.js +39 -6
  159. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  160. package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -1
  161. package/dist/modes/interactive/components/settings-submenu.js +8 -6
  162. package/dist/modes/interactive/components/settings-submenu.js.map +1 -1
  163. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  164. package/dist/modes/interactive/components/thinking-selector.js +10 -5
  165. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  166. package/dist/modes/interactive/components/tool-execution.d.ts +1 -11
  167. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  168. package/dist/modes/interactive/components/tool-execution.js +9 -38
  169. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  170. package/dist/modes/interactive/components/tool-panel.d.ts.map +1 -1
  171. package/dist/modes/interactive/components/tool-panel.js +7 -2
  172. package/dist/modes/interactive/components/tool-panel.js.map +1 -1
  173. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/tree-selector.js +9 -5
  175. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  176. package/dist/modes/interactive/components/trust-selector.d.ts.map +1 -1
  177. package/dist/modes/interactive/components/trust-selector.js +11 -11
  178. package/dist/modes/interactive/components/trust-selector.js.map +1 -1
  179. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  180. package/dist/modes/interactive/components/user-message-selector.js +2 -2
  181. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  182. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  183. package/dist/modes/interactive/components/user-message.js +6 -3
  184. package/dist/modes/interactive/components/user-message.js.map +1 -1
  185. package/dist/modes/interactive/interactive-mode.d.ts +7 -2
  186. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  187. package/dist/modes/interactive/interactive-mode.js +65 -65
  188. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  189. package/dist/modes/print-mode.d.ts.map +1 -1
  190. package/dist/modes/print-mode.js +49 -23
  191. package/dist/modes/print-mode.js.map +1 -1
  192. package/dist/tsconfig.json +3 -0
  193. package/dist/utils/clipboard-image.d.ts.map +1 -1
  194. package/dist/utils/clipboard-image.js +1 -1
  195. package/dist/utils/clipboard-image.js.map +1 -1
  196. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  197. package/dist/utils/syntax-highlight.js +22 -22
  198. package/dist/utils/syntax-highlight.js.map +1 -1
  199. package/docs/json.md +36 -0
  200. package/docs/models.md +1 -1
  201. package/docs/settings.md +1 -1
  202. package/examples/extensions/gondolin/package-lock.json +3 -3
  203. package/examples/extensions/minimal-mode.ts +6 -6
  204. package/examples/extensions/sandbox/package-lock.json +4 -4
  205. package/examples/extensions/sandbox/package.json +1 -1
  206. package/examples/sdk/10-settings.ts +2 -2
  207. package/examples/sdk/README.md +1 -1
  208. package/npm-shrinkwrap.json +19 -34
  209. package/package.json +6 -6
  210. package/dist/modes/interactive/components/message-spine.d.ts +0 -14
  211. package/dist/modes/interactive/components/message-spine.d.ts.map +0 -1
  212. package/dist/modes/interactive/components/message-spine.js +0 -19
  213. package/dist/modes/interactive/components/message-spine.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,wBAAwB,CAAC;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAgBzE;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAKD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,YAAY,EAAE,SAAS,GAAG,OAAO,GAC/B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAWnC;AAED,mFAAmF;AACnF,MAAM,WAAW,2BAA2B;IAC3C,eAAe,IAAI,SAAS,GAAG,OAAO,CAAC;IACvC,iBAAiB,IAAI,OAAO,CAAC;IAC7B,oBAAoB,IAAI,KAAK,GAAG,SAAS,GAAG,MAAM,CAAC;CACnD;AAQD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYhF;AAED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,qBAAqB,CAA8B;IAC3D,OAAO,CAAC,QAAQ,CAAwB;IAExC,YACC,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EACtC,qBAAqB,GAAE,2BAA4D,EAKnF;IAED,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAKtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAE5C;IAED;;;;;;;;OAQG;IACH,WAAW,CAAC,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CAEjD;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED;;;;;OAKG;IACH,OAAO,CAAC,UAAU,CAAC,CAIjB;IAEF,OAAO,CAAC,YAAY;IA+BpB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAgU9B;CACD","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport { areExperimentalFeaturesEnabled } from \"../../../core/experimental.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { PermissionMode } from \"../../../core/permissions/store.ts\";\nimport { addUsageToTotals, createUsageTotals } from \"../../../core/usage-totals.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts for compact footer display.\n */\nexport function formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/** Cells in the context gauge. Eight reads as a bar without crowding the tray. */\nconst CONTEXT_GAUGE_CELLS = 8;\n\n/**\n * A filled bar for the context window, split into its filled and empty runs so\n * the caller can colour them separately.\n *\n * This is a redundant channel and never the only one: the percentage beside it\n * states the same value as text (WCAG 1.4.1). An unknown percentage renders an\n * empty track rather than guessing a position.\n */\nexport function renderContextGauge(\n\tpercent: number | undefined,\n\tsymbolPreset: \"unicode\" | \"ascii\",\n): { filled: string; empty: string } {\n\tconst filledGlyph = symbolPreset === \"ascii\" ? \"#\" : \"█\";\n\tconst emptyGlyph = symbolPreset === \"ascii\" ? \"-\" : \"░\";\n\tif (percent === undefined) {\n\t\treturn { filled: \"\", empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS) };\n\t}\n\tconst clamped = Math.min(100, Math.max(0, percent));\n\t// Any non-zero usage lights at least one cell, so the bar never reads empty\n\t// while the window is genuinely filling.\n\tconst cells = clamped === 0 ? 0 : Math.max(1, Math.round((clamped / 100) * CONTEXT_GAUGE_CELLS));\n\treturn { filled: filledGlyph.repeat(cells), empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS - cells) };\n}\n\n/** Accessibility/display settings the footer reads (roadmap Phase 8, task 8.6). */\nexport interface FooterAccessibilitySettings {\n\tgetSymbolPreset(): \"unicode\" | \"ascii\";\n\tgetColorBlindMode(): boolean;\n\tgetTokenUsageDisplay(): \"off\" | \"compact\" | \"full\";\n}\n\nconst DEFAULT_ACCESSIBILITY_SETTINGS: FooterAccessibilitySettings = {\n\tgetSymbolPreset: () => \"unicode\",\n\tgetColorBlindMode: () => false,\n\tgetTokenUsageDisplay: () => \"compact\",\n};\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~/${relativeToHome.replaceAll(\"\\\\\", \"/\")}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate permissionMode: PermissionMode = \"default\";\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate accessibilitySettings: FooterAccessibilitySettings;\n\tprivate activity: Component | undefined;\n\n\tconstructor(\n\t\tsession: AgentSession,\n\t\tfooterData: ReadonlyFooterDataProvider,\n\t\taccessibilitySettings: FooterAccessibilitySettings = DEFAULT_ACCESSIBILITY_SETTINGS,\n\t) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t\tthis.accessibilitySettings = accessibilitySettings;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t\t// The usage totals are cached against the previous session's entry\n\t\t// version; a session switch must not reuse them.\n\t\tthis.usageCache = undefined;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\tsetPermissionMode(mode: PermissionMode): void {\n\t\tthis.permissionMode = mode;\n\t}\n\n\t/**\n\t * The working indicator, rendered inline in the tray instead of in its own\n\t * rows above the editor.\n\t *\n\t * Taking the component rather than a string keeps the spinner animation, the\n\t * extension-supplied `WorkingIndicatorOptions`, and disposal where they already\n\t * live. This setter only moves where the line is drawn, so it cannot disturb\n\t * the usage cache.\n\t */\n\tsetActivity(activity: Component | undefined): void {\n\t\tthis.activity = activity;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\t/**\n\t * Usage totals walked from all session entries. getEntries() copies the\n\t * whole entry list, so this is cached against the session manager's\n\t * entries version: without the cache the footer re-walked the entire\n\t * transcript on every rendered frame.\n\t */\n\tprivate usageCache?: {\n\t\tversion: number;\n\t\ttotals: ReturnType<typeof createUsageTotals>;\n\t\tlatestCacheHitRate?: number;\n\t};\n\n\tprivate computeUsage(): {\n\t\ttotals: ReturnType<typeof createUsageTotals>;\n\t\tlatestCacheHitRate?: number;\n\t} {\n\t\tconst version = this.session.sessionManager.getEntriesVersion();\n\t\tif (this.usageCache && this.usageCache.version === version) {\n\t\t\treturn { totals: this.usageCache.totals, latestCacheHitRate: this.usageCache.latestCacheHitRate };\n\t\t}\n\n\t\tconst usageTotals = createUsageTotals();\n\t\tlet latestCacheHitRate: number | undefined;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\n\t\t\t\tconst latestPromptTokens =\n\t\t\t\t\tentry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;\n\t\t\t\tlatestCacheHitRate =\n\t\t\t\t\tlatestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;\n\t\t\t} else if (entry.type === \"message\" && entry.message.role === \"toolResult\" && entry.message.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\t\t\t} else if ((entry.type === \"branch_summary\" || entry.type === \"compaction\") && entry.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.usage);\n\t\t\t}\n\t\t}\n\n\t\tthis.usageCache = { version, totals: usageTotals, latestCacheHitRate };\n\t\treturn { totals: usageTotals, latestCacheHitRate };\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\t\tconst symbolPreset = this.accessibilitySettings.getSymbolPreset();\n\t\tconst colorBlindMode = this.accessibilitySettings.getColorBlindMode();\n\t\tconst tokenUsageDisplay = this.accessibilitySettings.getTokenUsageDisplay();\n\t\tconst upSymbol = symbolPreset === \"ascii\" ? \"^\" : \"↑\";\n\t\tconst downSymbol = symbolPreset === \"ascii\" ? \"v\" : \"↓\";\n\t\tconst bulletSymbol = symbolPreset === \"ascii\" ? \"-\" : \"•\";\n\t\t// One line, unpadded, so the fit ladder can measure it as a segment. Loader\n\t\t// prepends a blank row to stand off from the transcript, which the tray does\n\t\t// not need, so this takes the first row that actually carries the message.\n\t\tconst activityLine = this.activity\n\t\t\t?.render(width)\n\t\t\t.map((line) => line.replace(/ +$/, \"\"))\n\t\t\t.find((line) => line.length > 0);\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages),\n\t\t// cached against the session's entry version (see computeUsage).\n\t\tconst { totals: usageTotals, latestCacheHitRate } = this.computeUsage();\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} ${bulletSymbol} ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line. tokenUsageDisplay: \"off\" hides this whole section\n\t\t// (context% and model name are separate and always shown); \"full\" shows\n\t\t// exact counts instead of formatTokens' abbreviation.\n\t\tconst formatTokenCount = tokenUsageDisplay === \"full\" ? (n: number) => n.toLocaleString() : formatTokens;\n\t\tconst statsParts = [];\n\t\tif (tokenUsageDisplay !== \"off\") {\n\t\t\tif (usageTotals.input) statsParts.push(`${upSymbol}${formatTokenCount(usageTotals.input)}`);\n\t\t\tif (usageTotals.output) statsParts.push(`${downSymbol}${formatTokenCount(usageTotals.output)}`);\n\t\t\tif (usageTotals.cacheRead) statsParts.push(`R${formatTokenCount(usageTotals.cacheRead)}`);\n\t\t\tif (usageTotals.cacheWrite) statsParts.push(`W${formatTokenCount(usageTotals.cacheWrite)}`);\n\t\t\tif ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {\n\t\t\t\tstatsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);\n\t\t\t}\n\t\t}\n\n\t\t// Kimi Coding is subscription-backed despite using API-key authentication.\n\t\tconst usingSubscription = state.model\n\t\t\t? state.model.provider === \"kimi-coding\" || this.session.modelRuntime.isUsingSubscription(state.model.provider)\n\t\t\t: false;\n\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\tconst costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Context pressure is signalled through text at every setting -- never\n\t\t// color alone (WCAG 1.4.1) -- with color as an additional channel. The\n\t\t// default error/warning pair (red/amber) is already distinguishable for\n\t\t// most red-green colorblindness; colorBlindMode's palette adjustment\n\t\t// moves the critical (>90%) threshold to \"accent\" -- a hue outside that\n\t\t// red/amber family entirely -- rather than relying on a style attribute\n\t\t// like bold/underline, which several terminals (and this theme's own\n\t\t// chalk-backed style methods, suppressed outside a real TTY) silently\n\t\t// drop, making it an unreliable second channel.\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst pressureMarker = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%${pressureMarker}/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(colorBlindMode ? \"accent\" : \"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\t// Only worth saying when pressure is real and nothing will act on its own.\n\t\t// With auto-compaction on, the \"(auto)\" marker above already answers it.\n\t\tconst compactPrompt =\n\t\t\tcontextPercentValue > 70 && !this.autoCompactEnabled ? theme.fg(\"warning\", \"/compact soon\") : undefined;\n\t\tstatsParts.push(contextPercentStr);\n\t\tif (compactPrompt) statsParts.push(compactPrompt);\n\t\tif (activityLine) statsParts.unshift(activityLine);\n\t\tif (areExperimentalFeaturesEnabled()) {\n\t\t\tstatsParts.push(`${theme.fg(\"dim\", bulletSymbol)} ${theme.bold(theme.fg(\"warning\", \"xp\"))}`);\n\t\t}\n\n\t\t// Permission posture is always named, including `default`. It is operational\n\t\t// state rather than routine telemetry: hiding the safe default would make the\n\t\t// tray change meaning by omission, while hiding bypass would conceal risk.\n\t\t// Text carries the signal and color remains a second channel (WCAG 1.4.1).\n\t\tconst modeColor =\n\t\t\tthis.permissionMode === \"bypassPermissions\" ? \"error\" : this.permissionMode === \"default\" ? \"dim\" : \"warning\";\n\t\tstatsParts.unshift(theme.fg(modeColor, this.permissionMode));\n\n\t\tif (tokenUsageDisplay !== \"full\") {\n\t\t\tconst separator = theme.fg(\"dim\", symbolPreset === \"ascii\" ? \" - \" : \" · \");\n\t\t\tconst compactPermissionNames: Record<PermissionMode, string> = {\n\t\t\t\tdefault: \"default\",\n\t\t\t\tplan: \"plan\",\n\t\t\t\tacceptEdits: \"accept\",\n\t\t\t\tbypassPermissions: \"bypass\",\n\t\t\t\tdontAsk: \"dontAsk\",\n\t\t\t};\n\t\t\tconst permissionColor =\n\t\t\t\tthis.permissionMode === \"bypassPermissions\"\n\t\t\t\t\t? \"error\"\n\t\t\t\t\t: this.permissionMode === \"default\"\n\t\t\t\t\t\t? \"dim\"\n\t\t\t\t\t\t: \"warning\";\n\t\t\tconst permissionFull = theme.fg(permissionColor, this.permissionMode);\n\t\t\tconst permissionCompact = theme.fg(permissionColor, compactPermissionNames[this.permissionMode]);\n\t\t\tconst compactPercent = contextPercent === \"?\" ? \"?\" : contextPercentValue.toFixed(0);\n\t\t\tconst compactPressure = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\t\tconst contextColor =\n\t\t\t\tcontextPercentValue > 90\n\t\t\t\t\t? colorBlindMode\n\t\t\t\t\t\t? \"accent\"\n\t\t\t\t\t\t: \"error\"\n\t\t\t\t\t: contextPercentValue > 70\n\t\t\t\t\t\t? \"warning\"\n\t\t\t\t\t\t: \"dim\";\n\t\t\t// The gauge rides along only in the roomy form. The compact form drops\n\t\t\t// it, which is what lets it keep winning the fit ladder below.\n\t\t\tconst gauge = renderContextGauge(contextPercent === \"?\" ? undefined : contextPercentValue, symbolPreset);\n\t\t\t// Below the warning threshold the text is dim, which would make the bar\n\t\t\t// invisible; the fill takes the accent there and tracks the text colour\n\t\t\t// at every level that actually signals pressure.\n\t\t\tconst gaugeColor = contextColor === \"dim\" ? \"accent\" : contextColor;\n\t\t\t// An empty run would still emit an open/close SGR pair, so skip it.\n\t\t\tconst paintRun = (color: Parameters<typeof theme.fg>[0], text: string) => (text ? theme.fg(color, text) : \"\");\n\t\t\tconst contextGauged =\n\t\t\t\ttheme.fg(contextColor, \"context \") +\n\t\t\t\tpaintRun(gaugeColor, gauge.filled) +\n\t\t\t\tpaintRun(\"borderMuted\", gauge.empty) +\n\t\t\t\ttheme.fg(contextColor, ` ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextFull = theme.fg(contextColor, `context ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextCompact = theme.fg(contextColor, `ctx ${compactPercent}%${compactPressure}`);\n\n\t\t\tconst joinSegments = (segments: string[]): string => segments.join(separator);\n\t\t\tconst fits = (segments: string[]): boolean => visibleWidth(joinSegments(segments)) <= width;\n\t\t\tconst safetyFirst = (...rest: Array<string | undefined>): string[] => [\n\t\t\t\tpermissionFull,\n\t\t\t\t...rest.filter((segment): segment is string => segment !== undefined),\n\t\t\t];\n\t\t\tlet left: string[] = safetyFirst(compactPrompt, activityLine);\n\t\t\tlet right = contextGauged;\n\n\t\t\t// The gauge is decoration over a value the text already states, so it\n\t\t\t// is the first thing to go. Dropping it must not cost the tray its\n\t\t\t// spelled-out permission mode, which is why this is its own rung.\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tright = contextFull;\n\t\t\t}\n\n\t\t\t// Activity is operational, so it outranks every optional segment below.\n\t\t\t// It still yields to the safety posture and to the pressure reading,\n\t\t\t// which are the two things the tray must never lose.\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = safetyFirst(compactPrompt);\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = safetyFirst();\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = [permissionCompact];\n\t\t\t\tright = contextCompact;\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tconst safetyOnly = left[0];\n\t\t\t\tconst line =\n\t\t\t\t\tvisibleWidth(safetyOnly) <= width ? safetyOnly : truncateToWidth(safetyOnly, Math.max(0, width), \"\");\n\t\t\t\treturn width > 0 ? [line] : [];\n\t\t\t}\n\n\t\t\tconst optional: string[] = [];\n\t\t\toptional.push(theme.fg(\"dim\", state.model?.id || \"no-model\"));\n\t\t\tif (state.model?.reasoning && state.thinkingLevel && state.thinkingLevel !== \"off\") {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.thinkingLevel));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.input || usageTotals.output)) {\n\t\t\t\toptional.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`${upSymbol}${formatTokens(usageTotals.input)} ${downSymbol}${formatTokens(usageTotals.output)}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (\n\t\t\t\ttokenUsageDisplay !== \"off\" &&\n\t\t\t\t(usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) &&\n\t\t\t\tlatestCacheHitRate !== undefined\n\t\t\t) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `CH${latestCacheHitRate.toFixed(1)}%`));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`));\n\t\t\t}\n\t\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.model.provider));\n\t\t\t}\n\t\t\tif (areExperimentalFeaturesEnabled()) optional.push(theme.fg(\"warning\", \"xp\"));\n\t\t\toptional.push(theme.fg(\"dim\", pwd));\n\n\t\t\tfor (const segment of optional) {\n\t\t\t\tif (fits([...left, segment, right])) left.push(segment);\n\t\t\t}\n\n\t\t\tconst leftText = joinSegments(left);\n\t\t\tconst padding = \" \".repeat(Math.max(1, width - visibleWidth(leftText) - visibleWidth(right)));\n\t\t\tconst tray = leftText ? `${leftText}${padding}${right}` : right;\n\t\t\tconst lines = [tray];\n\t\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\t\tif (extensionStatuses.size > 0) {\n\t\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\t\tlines.push(truncateToWidth(sortedStatuses.join(\" \"), width, theme.fg(\"dim\", \"...\")));\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\t// Permission mode is the first segment and must remain textual at every\n\t\t\t// positive width. An ellipsis suffix can consume the entire narrow line.\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\"\n\t\t\t\t\t? `${modelName} ${bulletSymbol} thinking off`\n\t\t\t\t\t: `${modelName} ${bulletSymbol} ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,wBAAwB,CAAC;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAgBzE;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAKD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,YAAY,EAAE,SAAS,GAAG,OAAO,GAC/B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAWnC;AAED,mFAAmF;AACnF,MAAM,WAAW,2BAA2B;IAC3C,eAAe,IAAI,SAAS,GAAG,OAAO,CAAC;IACvC,iBAAiB,IAAI,OAAO,CAAC;IAC7B,oBAAoB,IAAI,KAAK,GAAG,SAAS,GAAG,MAAM,CAAC;CACnD;AAQD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYhF;AAED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,qBAAqB,CAA8B;IAC3D,OAAO,CAAC,QAAQ,CAAwB;IAExC,YACC,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EACtC,qBAAqB,GAAE,2BAA4D,EAKnF;IAED,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAKtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAE5C;IAED;;;;;;;;OAQG;IACH,WAAW,CAAC,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CAEjD;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED;;;;;OAKG;IACH,OAAO,CAAC,UAAU,CAAC,CAIjB;IAEF,OAAO,CAAC,YAAY;IA+BpB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAsU9B;CACD","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport { areExperimentalFeaturesEnabled } from \"../../../core/experimental.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { PermissionMode } from \"../../../core/permissions/store.ts\";\nimport { addUsageToTotals, createUsageTotals } from \"../../../core/usage-totals.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts for compact footer display.\n */\nexport function formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/** Cells in the context gauge. Eight reads as a bar without crowding the tray. */\nconst CONTEXT_GAUGE_CELLS = 8;\n\n/**\n * A filled bar for the context window, split into its filled and empty runs so\n * the caller can colour them separately.\n *\n * This is a redundant channel and never the only one: the percentage beside it\n * states the same value as text (WCAG 1.4.1). An unknown percentage renders an\n * empty track rather than guessing a position.\n */\nexport function renderContextGauge(\n\tpercent: number | undefined,\n\tsymbolPreset: \"unicode\" | \"ascii\",\n): { filled: string; empty: string } {\n\tconst filledGlyph = symbolPreset === \"ascii\" ? \"#\" : \"█\";\n\tconst emptyGlyph = symbolPreset === \"ascii\" ? \"-\" : \"░\";\n\tif (percent === undefined) {\n\t\treturn { filled: \"\", empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS) };\n\t}\n\tconst clamped = Math.min(100, Math.max(0, percent));\n\t// Any non-zero usage lights at least one cell, so the bar never reads empty\n\t// while the window is genuinely filling.\n\tconst cells = clamped === 0 ? 0 : Math.max(1, Math.round((clamped / 100) * CONTEXT_GAUGE_CELLS));\n\treturn { filled: filledGlyph.repeat(cells), empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS - cells) };\n}\n\n/** Accessibility/display settings the footer reads (roadmap Phase 8, task 8.6). */\nexport interface FooterAccessibilitySettings {\n\tgetSymbolPreset(): \"unicode\" | \"ascii\";\n\tgetColorBlindMode(): boolean;\n\tgetTokenUsageDisplay(): \"off\" | \"compact\" | \"full\";\n}\n\nconst DEFAULT_ACCESSIBILITY_SETTINGS: FooterAccessibilitySettings = {\n\tgetSymbolPreset: () => \"unicode\",\n\tgetColorBlindMode: () => false,\n\tgetTokenUsageDisplay: () => \"compact\",\n};\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~/${relativeToHome.replaceAll(\"\\\\\", \"/\")}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate permissionMode: PermissionMode = \"default\";\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate accessibilitySettings: FooterAccessibilitySettings;\n\tprivate activity: Component | undefined;\n\n\tconstructor(\n\t\tsession: AgentSession,\n\t\tfooterData: ReadonlyFooterDataProvider,\n\t\taccessibilitySettings: FooterAccessibilitySettings = DEFAULT_ACCESSIBILITY_SETTINGS,\n\t) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t\tthis.accessibilitySettings = accessibilitySettings;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t\t// The usage totals are cached against the previous session's entry\n\t\t// version; a session switch must not reuse them.\n\t\tthis.usageCache = undefined;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\tsetPermissionMode(mode: PermissionMode): void {\n\t\tthis.permissionMode = mode;\n\t}\n\n\t/**\n\t * The working indicator, rendered inline in the tray instead of in its own\n\t * rows above the editor.\n\t *\n\t * Taking the component rather than a string keeps the spinner animation, the\n\t * extension-supplied `WorkingIndicatorOptions`, and disposal where they already\n\t * live. This setter only moves where the line is drawn, so it cannot disturb\n\t * the usage cache.\n\t */\n\tsetActivity(activity: Component | undefined): void {\n\t\tthis.activity = activity;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\t/**\n\t * Usage totals walked from all session entries. getEntries() copies the\n\t * whole entry list, so this is cached against the session manager's\n\t * entries version: without the cache the footer re-walked the entire\n\t * transcript on every rendered frame.\n\t */\n\tprivate usageCache?: {\n\t\tversion: number;\n\t\ttotals: ReturnType<typeof createUsageTotals>;\n\t\tlatestCacheHitRate?: number;\n\t};\n\n\tprivate computeUsage(): {\n\t\ttotals: ReturnType<typeof createUsageTotals>;\n\t\tlatestCacheHitRate?: number;\n\t} {\n\t\tconst version = this.session.sessionManager.getEntriesVersion();\n\t\tif (this.usageCache && this.usageCache.version === version) {\n\t\t\treturn { totals: this.usageCache.totals, latestCacheHitRate: this.usageCache.latestCacheHitRate };\n\t\t}\n\n\t\tconst usageTotals = createUsageTotals();\n\t\tlet latestCacheHitRate: number | undefined;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\n\t\t\t\tconst latestPromptTokens =\n\t\t\t\t\tentry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;\n\t\t\t\tlatestCacheHitRate =\n\t\t\t\t\tlatestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;\n\t\t\t} else if (entry.type === \"message\" && entry.message.role === \"toolResult\" && entry.message.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\t\t\t} else if ((entry.type === \"branch_summary\" || entry.type === \"compaction\") && entry.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.usage);\n\t\t\t}\n\t\t}\n\n\t\tthis.usageCache = { version, totals: usageTotals, latestCacheHitRate };\n\t\treturn { totals: usageTotals, latestCacheHitRate };\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\t\tconst symbolPreset = this.accessibilitySettings.getSymbolPreset();\n\t\tconst colorBlindMode = this.accessibilitySettings.getColorBlindMode();\n\t\tconst tokenUsageDisplay = this.accessibilitySettings.getTokenUsageDisplay();\n\t\tconst upSymbol = symbolPreset === \"ascii\" ? \"^\" : \"↑\";\n\t\tconst downSymbol = symbolPreset === \"ascii\" ? \"v\" : \"↓\";\n\t\tconst bulletSymbol = symbolPreset === \"ascii\" ? \"-\" : \"•\";\n\t\t// One line, unpadded, so the fit ladder can measure it as a segment. Loader\n\t\t// prepends a blank row to stand off from the transcript, which the tray does\n\t\t// not need, so this takes the first row that actually carries the message.\n\t\tconst activityLine = this.activity\n\t\t\t?.render(width)\n\t\t\t.map((line) => line.replace(/ +$/, \"\"))\n\t\t\t.find((line) => line.length > 0);\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages),\n\t\t// cached against the session's entry version (see computeUsage).\n\t\tconst { totals: usageTotals, latestCacheHitRate } = this.computeUsage();\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\t// The gauge lights a cell for any usage at all, so a figure that rounds to zero says so.\n\t\tconst contextPercent =\n\t\t\tcontextUsage?.percent === null\n\t\t\t\t? \"?\"\n\t\t\t\t: contextPercentValue > 0 && contextPercentValue < 0.05\n\t\t\t\t\t? \"<0.1\"\n\t\t\t\t\t: contextPercentValue.toFixed(1);\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} ${bulletSymbol} ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line. tokenUsageDisplay: \"off\" hides this whole section\n\t\t// (context% and model name are separate and always shown); \"full\" shows\n\t\t// exact counts instead of formatTokens' abbreviation.\n\t\tconst formatTokenCount = tokenUsageDisplay === \"full\" ? (n: number) => n.toLocaleString() : formatTokens;\n\t\tconst statsParts = [];\n\t\tif (tokenUsageDisplay !== \"off\") {\n\t\t\tif (usageTotals.input) statsParts.push(`${upSymbol}${formatTokenCount(usageTotals.input)}`);\n\t\t\tif (usageTotals.output) statsParts.push(`${downSymbol}${formatTokenCount(usageTotals.output)}`);\n\t\t\tif (usageTotals.cacheRead) statsParts.push(`R${formatTokenCount(usageTotals.cacheRead)}`);\n\t\t\tif (usageTotals.cacheWrite) statsParts.push(`W${formatTokenCount(usageTotals.cacheWrite)}`);\n\t\t\tif ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {\n\t\t\t\tstatsParts.push(`cache ${Math.round(latestCacheHitRate)}%`);\n\t\t\t}\n\t\t}\n\n\t\t// Kimi Coding is subscription-backed despite using API-key authentication.\n\t\tconst usingSubscription = state.model\n\t\t\t? state.model.provider === \"kimi-coding\" || this.session.modelRuntime.isUsingSubscription(state.model.provider)\n\t\t\t: false;\n\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\tconst costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Context pressure is signalled through text at every setting -- never\n\t\t// color alone (WCAG 1.4.1) -- with color as an additional channel. The\n\t\t// default error/warning pair (red/amber) is already distinguishable for\n\t\t// most red-green colorblindness; colorBlindMode's palette adjustment\n\t\t// moves the critical (>90%) threshold to \"accent\" -- a hue outside that\n\t\t// red/amber family entirely -- rather than relying on a style attribute\n\t\t// like bold/underline, which several terminals (and this theme's own\n\t\t// chalk-backed style methods, suppressed outside a real TTY) silently\n\t\t// drop, making it an unreliable second channel.\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst pressureMarker = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%${pressureMarker}/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(colorBlindMode ? \"accent\" : \"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\t// Only worth saying when pressure is real and nothing will act on its own.\n\t\t// With auto-compaction on, the \"(auto)\" marker above already answers it.\n\t\tconst compactPrompt =\n\t\t\tcontextPercentValue > 70 && !this.autoCompactEnabled ? theme.fg(\"warning\", \"/compact soon\") : undefined;\n\t\tstatsParts.push(contextPercentStr);\n\t\tif (compactPrompt) statsParts.push(compactPrompt);\n\t\tif (activityLine) statsParts.unshift(activityLine);\n\t\tif (areExperimentalFeaturesEnabled()) {\n\t\t\tstatsParts.push(`${theme.fg(\"dim\", bulletSymbol)} ${theme.bold(theme.fg(\"warning\", \"xp\"))}`);\n\t\t}\n\n\t\t// Permission posture is always named, including `default`. It is operational\n\t\t// state rather than routine telemetry: hiding the safe default would make the\n\t\t// tray change meaning by omission, while hiding bypass would conceal risk.\n\t\t// Text carries the signal and color remains a second channel (WCAG 1.4.1).\n\t\tconst modeColor =\n\t\t\tthis.permissionMode === \"bypassPermissions\" ? \"error\" : this.permissionMode === \"default\" ? \"dim\" : \"warning\";\n\t\tstatsParts.unshift(theme.fg(modeColor, this.permissionMode));\n\n\t\tif (tokenUsageDisplay !== \"full\") {\n\t\t\tconst separator = theme.fg(\"dim\", symbolPreset === \"ascii\" ? \" - \" : \" · \");\n\t\t\tconst compactPermissionNames: Record<PermissionMode, string> = {\n\t\t\t\tdefault: \"default\",\n\t\t\t\tplan: \"plan\",\n\t\t\t\tacceptEdits: \"accept\",\n\t\t\t\tbypassPermissions: \"bypass\",\n\t\t\t\tdontAsk: \"dontAsk\",\n\t\t\t};\n\t\t\tconst permissionColor =\n\t\t\t\tthis.permissionMode === \"bypassPermissions\"\n\t\t\t\t\t? \"error\"\n\t\t\t\t\t: this.permissionMode === \"default\"\n\t\t\t\t\t\t? \"dim\"\n\t\t\t\t\t\t: \"warning\";\n\t\t\tconst permissionFull = theme.fg(permissionColor, this.permissionMode);\n\t\t\tconst permissionCompact = theme.fg(permissionColor, compactPermissionNames[this.permissionMode]);\n\t\t\tconst compactPercent = contextPercent === \"?\" ? \"?\" : contextPercentValue.toFixed(0);\n\t\t\tconst compactPressure = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\t\tconst contextColor =\n\t\t\t\tcontextPercentValue > 90\n\t\t\t\t\t? colorBlindMode\n\t\t\t\t\t\t? \"accent\"\n\t\t\t\t\t\t: \"error\"\n\t\t\t\t\t: contextPercentValue > 70\n\t\t\t\t\t\t? \"warning\"\n\t\t\t\t\t\t: \"dim\";\n\t\t\t// The gauge rides along only in the roomy form. The compact form drops\n\t\t\t// it, which is what lets it keep winning the fit ladder below.\n\t\t\tconst gauge = renderContextGauge(contextPercent === \"?\" ? undefined : contextPercentValue, symbolPreset);\n\t\t\t// Below the warning threshold the text is dim, which would make the bar\n\t\t\t// invisible; the fill takes the accent there and tracks the text colour\n\t\t\t// at every level that actually signals pressure.\n\t\t\tconst gaugeColor = contextColor === \"dim\" ? \"accent\" : contextColor;\n\t\t\t// An empty run would still emit an open/close SGR pair, so skip it.\n\t\t\tconst paintRun = (color: Parameters<typeof theme.fg>[0], text: string) => (text ? theme.fg(color, text) : \"\");\n\t\t\tconst contextGauged =\n\t\t\t\ttheme.fg(contextColor, \"context \") +\n\t\t\t\tpaintRun(gaugeColor, gauge.filled) +\n\t\t\t\tpaintRun(\"borderMuted\", gauge.empty) +\n\t\t\t\ttheme.fg(contextColor, ` ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextFull = theme.fg(contextColor, `context ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextCompact = theme.fg(contextColor, `ctx ${compactPercent}%${compactPressure}`);\n\n\t\t\tconst joinSegments = (segments: string[]): string => segments.join(separator);\n\t\t\tconst fits = (segments: string[]): boolean => visibleWidth(joinSegments(segments)) <= width;\n\t\t\tconst safetyFirst = (...rest: Array<string | undefined>): string[] => [\n\t\t\t\tpermissionFull,\n\t\t\t\t...rest.filter((segment): segment is string => segment !== undefined),\n\t\t\t];\n\t\t\tlet left: string[] = safetyFirst(compactPrompt, activityLine);\n\t\t\tlet right = contextGauged;\n\n\t\t\t// The gauge is decoration over a value the text already states, so it\n\t\t\t// is the first thing to go. Dropping it must not cost the tray its\n\t\t\t// spelled-out permission mode, which is why this is its own rung.\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tright = contextFull;\n\t\t\t}\n\n\t\t\t// Activity is operational, so it outranks every optional segment below.\n\t\t\t// It still yields to the safety posture and to the pressure reading,\n\t\t\t// which are the two things the tray must never lose.\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = safetyFirst(compactPrompt);\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = safetyFirst();\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = [permissionCompact];\n\t\t\t\tright = contextCompact;\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tconst safetyOnly = left[0];\n\t\t\t\tconst line =\n\t\t\t\t\tvisibleWidth(safetyOnly) <= width ? safetyOnly : truncateToWidth(safetyOnly, Math.max(0, width), \"\");\n\t\t\t\treturn width > 0 ? [line] : [];\n\t\t\t}\n\n\t\t\tconst optional: string[] = [];\n\t\t\toptional.push(theme.fg(\"dim\", state.model?.id || \"no-model\"));\n\t\t\tif (state.model?.reasoning && state.thinkingLevel && state.thinkingLevel !== \"off\") {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.thinkingLevel));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.input || usageTotals.output)) {\n\t\t\t\toptional.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`${upSymbol}${formatTokens(usageTotals.input)} ${downSymbol}${formatTokens(usageTotals.output)}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (\n\t\t\t\ttokenUsageDisplay !== \"off\" &&\n\t\t\t\t(usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) &&\n\t\t\t\tlatestCacheHitRate !== undefined\n\t\t\t) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `cache ${Math.round(latestCacheHitRate)}%`));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`));\n\t\t\t}\n\t\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.model.provider));\n\t\t\t}\n\t\t\tif (areExperimentalFeaturesEnabled()) optional.push(theme.fg(\"warning\", \"xp\"));\n\t\t\toptional.push(theme.fg(\"dim\", pwd));\n\n\t\t\tfor (const segment of optional) {\n\t\t\t\tif (fits([...left, segment, right])) left.push(segment);\n\t\t\t}\n\n\t\t\tconst leftText = joinSegments(left);\n\t\t\tconst padding = \" \".repeat(Math.max(1, width - visibleWidth(leftText) - visibleWidth(right)));\n\t\t\tconst tray = leftText ? `${leftText}${padding}${right}` : right;\n\t\t\tconst lines = [tray];\n\t\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\t\tif (extensionStatuses.size > 0) {\n\t\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\t\tlines.push(truncateToWidth(sortedStatuses.join(\" \"), width, theme.fg(\"dim\", \"...\")));\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\t// Permission mode is the first segment and must remain textual at every\n\t\t\t// positive width. An ellipsis suffix can consume the entire narrow line.\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\"\n\t\t\t\t\t? `${modelName} ${bulletSymbol} thinking off`\n\t\t\t\t\t: `${modelName} ${bulletSymbol} ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
@@ -175,7 +175,12 @@ export class FooterComponent {
175
175
  const contextUsage = this.session.getContextUsage();
176
176
  const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
177
177
  const contextPercentValue = contextUsage?.percent ?? 0;
178
- const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
178
+ // The gauge lights a cell for any usage at all, so a figure that rounds to zero says so.
179
+ const contextPercent = contextUsage?.percent === null
180
+ ? "?"
181
+ : contextPercentValue > 0 && contextPercentValue < 0.05
182
+ ? "<0.1"
183
+ : contextPercentValue.toFixed(1);
179
184
  // Replace home directory with ~
180
185
  let pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);
181
186
  // Add git branch if available
@@ -203,7 +208,7 @@ export class FooterComponent {
203
208
  if (usageTotals.cacheWrite)
204
209
  statsParts.push(`W${formatTokenCount(usageTotals.cacheWrite)}`);
205
210
  if ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {
206
- statsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);
211
+ statsParts.push(`cache ${Math.round(latestCacheHitRate)}%`);
207
212
  }
208
213
  }
209
214
  // Kimi Coding is subscription-backed despite using API-key authentication.
@@ -338,7 +343,7 @@ export class FooterComponent {
338
343
  if (tokenUsageDisplay !== "off" &&
339
344
  (usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) &&
340
345
  latestCacheHitRate !== undefined) {
341
- optional.push(theme.fg("dim", `CH${latestCacheHitRate.toFixed(1)}%`));
346
+ optional.push(theme.fg("dim", `cache ${Math.round(latestCacheHitRate)}%`));
342
347
  }
343
348
  if (tokenUsageDisplay !== "off" && (usageTotals.cost || usingSubscription)) {
344
349
  optional.push(theme.fg("dim", `$${usageTotals.cost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`));
@@ -1 +1 @@
1
- {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport { areExperimentalFeaturesEnabled } from \"../../../core/experimental.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { PermissionMode } from \"../../../core/permissions/store.ts\";\nimport { addUsageToTotals, createUsageTotals } from \"../../../core/usage-totals.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts for compact footer display.\n */\nexport function formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/** Cells in the context gauge. Eight reads as a bar without crowding the tray. */\nconst CONTEXT_GAUGE_CELLS = 8;\n\n/**\n * A filled bar for the context window, split into its filled and empty runs so\n * the caller can colour them separately.\n *\n * This is a redundant channel and never the only one: the percentage beside it\n * states the same value as text (WCAG 1.4.1). An unknown percentage renders an\n * empty track rather than guessing a position.\n */\nexport function renderContextGauge(\n\tpercent: number | undefined,\n\tsymbolPreset: \"unicode\" | \"ascii\",\n): { filled: string; empty: string } {\n\tconst filledGlyph = symbolPreset === \"ascii\" ? \"#\" : \"█\";\n\tconst emptyGlyph = symbolPreset === \"ascii\" ? \"-\" : \"░\";\n\tif (percent === undefined) {\n\t\treturn { filled: \"\", empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS) };\n\t}\n\tconst clamped = Math.min(100, Math.max(0, percent));\n\t// Any non-zero usage lights at least one cell, so the bar never reads empty\n\t// while the window is genuinely filling.\n\tconst cells = clamped === 0 ? 0 : Math.max(1, Math.round((clamped / 100) * CONTEXT_GAUGE_CELLS));\n\treturn { filled: filledGlyph.repeat(cells), empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS - cells) };\n}\n\n/** Accessibility/display settings the footer reads (roadmap Phase 8, task 8.6). */\nexport interface FooterAccessibilitySettings {\n\tgetSymbolPreset(): \"unicode\" | \"ascii\";\n\tgetColorBlindMode(): boolean;\n\tgetTokenUsageDisplay(): \"off\" | \"compact\" | \"full\";\n}\n\nconst DEFAULT_ACCESSIBILITY_SETTINGS: FooterAccessibilitySettings = {\n\tgetSymbolPreset: () => \"unicode\",\n\tgetColorBlindMode: () => false,\n\tgetTokenUsageDisplay: () => \"compact\",\n};\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~/${relativeToHome.replaceAll(\"\\\\\", \"/\")}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate permissionMode: PermissionMode = \"default\";\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate accessibilitySettings: FooterAccessibilitySettings;\n\tprivate activity: Component | undefined;\n\n\tconstructor(\n\t\tsession: AgentSession,\n\t\tfooterData: ReadonlyFooterDataProvider,\n\t\taccessibilitySettings: FooterAccessibilitySettings = DEFAULT_ACCESSIBILITY_SETTINGS,\n\t) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t\tthis.accessibilitySettings = accessibilitySettings;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t\t// The usage totals are cached against the previous session's entry\n\t\t// version; a session switch must not reuse them.\n\t\tthis.usageCache = undefined;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\tsetPermissionMode(mode: PermissionMode): void {\n\t\tthis.permissionMode = mode;\n\t}\n\n\t/**\n\t * The working indicator, rendered inline in the tray instead of in its own\n\t * rows above the editor.\n\t *\n\t * Taking the component rather than a string keeps the spinner animation, the\n\t * extension-supplied `WorkingIndicatorOptions`, and disposal where they already\n\t * live. This setter only moves where the line is drawn, so it cannot disturb\n\t * the usage cache.\n\t */\n\tsetActivity(activity: Component | undefined): void {\n\t\tthis.activity = activity;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\t/**\n\t * Usage totals walked from all session entries. getEntries() copies the\n\t * whole entry list, so this is cached against the session manager's\n\t * entries version: without the cache the footer re-walked the entire\n\t * transcript on every rendered frame.\n\t */\n\tprivate usageCache?: {\n\t\tversion: number;\n\t\ttotals: ReturnType<typeof createUsageTotals>;\n\t\tlatestCacheHitRate?: number;\n\t};\n\n\tprivate computeUsage(): {\n\t\ttotals: ReturnType<typeof createUsageTotals>;\n\t\tlatestCacheHitRate?: number;\n\t} {\n\t\tconst version = this.session.sessionManager.getEntriesVersion();\n\t\tif (this.usageCache && this.usageCache.version === version) {\n\t\t\treturn { totals: this.usageCache.totals, latestCacheHitRate: this.usageCache.latestCacheHitRate };\n\t\t}\n\n\t\tconst usageTotals = createUsageTotals();\n\t\tlet latestCacheHitRate: number | undefined;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\n\t\t\t\tconst latestPromptTokens =\n\t\t\t\t\tentry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;\n\t\t\t\tlatestCacheHitRate =\n\t\t\t\t\tlatestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;\n\t\t\t} else if (entry.type === \"message\" && entry.message.role === \"toolResult\" && entry.message.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\t\t\t} else if ((entry.type === \"branch_summary\" || entry.type === \"compaction\") && entry.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.usage);\n\t\t\t}\n\t\t}\n\n\t\tthis.usageCache = { version, totals: usageTotals, latestCacheHitRate };\n\t\treturn { totals: usageTotals, latestCacheHitRate };\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\t\tconst symbolPreset = this.accessibilitySettings.getSymbolPreset();\n\t\tconst colorBlindMode = this.accessibilitySettings.getColorBlindMode();\n\t\tconst tokenUsageDisplay = this.accessibilitySettings.getTokenUsageDisplay();\n\t\tconst upSymbol = symbolPreset === \"ascii\" ? \"^\" : \"↑\";\n\t\tconst downSymbol = symbolPreset === \"ascii\" ? \"v\" : \"↓\";\n\t\tconst bulletSymbol = symbolPreset === \"ascii\" ? \"-\" : \"•\";\n\t\t// One line, unpadded, so the fit ladder can measure it as a segment. Loader\n\t\t// prepends a blank row to stand off from the transcript, which the tray does\n\t\t// not need, so this takes the first row that actually carries the message.\n\t\tconst activityLine = this.activity\n\t\t\t?.render(width)\n\t\t\t.map((line) => line.replace(/ +$/, \"\"))\n\t\t\t.find((line) => line.length > 0);\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages),\n\t\t// cached against the session's entry version (see computeUsage).\n\t\tconst { totals: usageTotals, latestCacheHitRate } = this.computeUsage();\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} ${bulletSymbol} ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line. tokenUsageDisplay: \"off\" hides this whole section\n\t\t// (context% and model name are separate and always shown); \"full\" shows\n\t\t// exact counts instead of formatTokens' abbreviation.\n\t\tconst formatTokenCount = tokenUsageDisplay === \"full\" ? (n: number) => n.toLocaleString() : formatTokens;\n\t\tconst statsParts = [];\n\t\tif (tokenUsageDisplay !== \"off\") {\n\t\t\tif (usageTotals.input) statsParts.push(`${upSymbol}${formatTokenCount(usageTotals.input)}`);\n\t\t\tif (usageTotals.output) statsParts.push(`${downSymbol}${formatTokenCount(usageTotals.output)}`);\n\t\t\tif (usageTotals.cacheRead) statsParts.push(`R${formatTokenCount(usageTotals.cacheRead)}`);\n\t\t\tif (usageTotals.cacheWrite) statsParts.push(`W${formatTokenCount(usageTotals.cacheWrite)}`);\n\t\t\tif ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {\n\t\t\t\tstatsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);\n\t\t\t}\n\t\t}\n\n\t\t// Kimi Coding is subscription-backed despite using API-key authentication.\n\t\tconst usingSubscription = state.model\n\t\t\t? state.model.provider === \"kimi-coding\" || this.session.modelRuntime.isUsingSubscription(state.model.provider)\n\t\t\t: false;\n\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\tconst costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Context pressure is signalled through text at every setting -- never\n\t\t// color alone (WCAG 1.4.1) -- with color as an additional channel. The\n\t\t// default error/warning pair (red/amber) is already distinguishable for\n\t\t// most red-green colorblindness; colorBlindMode's palette adjustment\n\t\t// moves the critical (>90%) threshold to \"accent\" -- a hue outside that\n\t\t// red/amber family entirely -- rather than relying on a style attribute\n\t\t// like bold/underline, which several terminals (and this theme's own\n\t\t// chalk-backed style methods, suppressed outside a real TTY) silently\n\t\t// drop, making it an unreliable second channel.\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst pressureMarker = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%${pressureMarker}/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(colorBlindMode ? \"accent\" : \"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\t// Only worth saying when pressure is real and nothing will act on its own.\n\t\t// With auto-compaction on, the \"(auto)\" marker above already answers it.\n\t\tconst compactPrompt =\n\t\t\tcontextPercentValue > 70 && !this.autoCompactEnabled ? theme.fg(\"warning\", \"/compact soon\") : undefined;\n\t\tstatsParts.push(contextPercentStr);\n\t\tif (compactPrompt) statsParts.push(compactPrompt);\n\t\tif (activityLine) statsParts.unshift(activityLine);\n\t\tif (areExperimentalFeaturesEnabled()) {\n\t\t\tstatsParts.push(`${theme.fg(\"dim\", bulletSymbol)} ${theme.bold(theme.fg(\"warning\", \"xp\"))}`);\n\t\t}\n\n\t\t// Permission posture is always named, including `default`. It is operational\n\t\t// state rather than routine telemetry: hiding the safe default would make the\n\t\t// tray change meaning by omission, while hiding bypass would conceal risk.\n\t\t// Text carries the signal and color remains a second channel (WCAG 1.4.1).\n\t\tconst modeColor =\n\t\t\tthis.permissionMode === \"bypassPermissions\" ? \"error\" : this.permissionMode === \"default\" ? \"dim\" : \"warning\";\n\t\tstatsParts.unshift(theme.fg(modeColor, this.permissionMode));\n\n\t\tif (tokenUsageDisplay !== \"full\") {\n\t\t\tconst separator = theme.fg(\"dim\", symbolPreset === \"ascii\" ? \" - \" : \" · \");\n\t\t\tconst compactPermissionNames: Record<PermissionMode, string> = {\n\t\t\t\tdefault: \"default\",\n\t\t\t\tplan: \"plan\",\n\t\t\t\tacceptEdits: \"accept\",\n\t\t\t\tbypassPermissions: \"bypass\",\n\t\t\t\tdontAsk: \"dontAsk\",\n\t\t\t};\n\t\t\tconst permissionColor =\n\t\t\t\tthis.permissionMode === \"bypassPermissions\"\n\t\t\t\t\t? \"error\"\n\t\t\t\t\t: this.permissionMode === \"default\"\n\t\t\t\t\t\t? \"dim\"\n\t\t\t\t\t\t: \"warning\";\n\t\t\tconst permissionFull = theme.fg(permissionColor, this.permissionMode);\n\t\t\tconst permissionCompact = theme.fg(permissionColor, compactPermissionNames[this.permissionMode]);\n\t\t\tconst compactPercent = contextPercent === \"?\" ? \"?\" : contextPercentValue.toFixed(0);\n\t\t\tconst compactPressure = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\t\tconst contextColor =\n\t\t\t\tcontextPercentValue > 90\n\t\t\t\t\t? colorBlindMode\n\t\t\t\t\t\t? \"accent\"\n\t\t\t\t\t\t: \"error\"\n\t\t\t\t\t: contextPercentValue > 70\n\t\t\t\t\t\t? \"warning\"\n\t\t\t\t\t\t: \"dim\";\n\t\t\t// The gauge rides along only in the roomy form. The compact form drops\n\t\t\t// it, which is what lets it keep winning the fit ladder below.\n\t\t\tconst gauge = renderContextGauge(contextPercent === \"?\" ? undefined : contextPercentValue, symbolPreset);\n\t\t\t// Below the warning threshold the text is dim, which would make the bar\n\t\t\t// invisible; the fill takes the accent there and tracks the text colour\n\t\t\t// at every level that actually signals pressure.\n\t\t\tconst gaugeColor = contextColor === \"dim\" ? \"accent\" : contextColor;\n\t\t\t// An empty run would still emit an open/close SGR pair, so skip it.\n\t\t\tconst paintRun = (color: Parameters<typeof theme.fg>[0], text: string) => (text ? theme.fg(color, text) : \"\");\n\t\t\tconst contextGauged =\n\t\t\t\ttheme.fg(contextColor, \"context \") +\n\t\t\t\tpaintRun(gaugeColor, gauge.filled) +\n\t\t\t\tpaintRun(\"borderMuted\", gauge.empty) +\n\t\t\t\ttheme.fg(contextColor, ` ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextFull = theme.fg(contextColor, `context ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextCompact = theme.fg(contextColor, `ctx ${compactPercent}%${compactPressure}`);\n\n\t\t\tconst joinSegments = (segments: string[]): string => segments.join(separator);\n\t\t\tconst fits = (segments: string[]): boolean => visibleWidth(joinSegments(segments)) <= width;\n\t\t\tconst safetyFirst = (...rest: Array<string | undefined>): string[] => [\n\t\t\t\tpermissionFull,\n\t\t\t\t...rest.filter((segment): segment is string => segment !== undefined),\n\t\t\t];\n\t\t\tlet left: string[] = safetyFirst(compactPrompt, activityLine);\n\t\t\tlet right = contextGauged;\n\n\t\t\t// The gauge is decoration over a value the text already states, so it\n\t\t\t// is the first thing to go. Dropping it must not cost the tray its\n\t\t\t// spelled-out permission mode, which is why this is its own rung.\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tright = contextFull;\n\t\t\t}\n\n\t\t\t// Activity is operational, so it outranks every optional segment below.\n\t\t\t// It still yields to the safety posture and to the pressure reading,\n\t\t\t// which are the two things the tray must never lose.\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = safetyFirst(compactPrompt);\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = safetyFirst();\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = [permissionCompact];\n\t\t\t\tright = contextCompact;\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tconst safetyOnly = left[0];\n\t\t\t\tconst line =\n\t\t\t\t\tvisibleWidth(safetyOnly) <= width ? safetyOnly : truncateToWidth(safetyOnly, Math.max(0, width), \"\");\n\t\t\t\treturn width > 0 ? [line] : [];\n\t\t\t}\n\n\t\t\tconst optional: string[] = [];\n\t\t\toptional.push(theme.fg(\"dim\", state.model?.id || \"no-model\"));\n\t\t\tif (state.model?.reasoning && state.thinkingLevel && state.thinkingLevel !== \"off\") {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.thinkingLevel));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.input || usageTotals.output)) {\n\t\t\t\toptional.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`${upSymbol}${formatTokens(usageTotals.input)} ${downSymbol}${formatTokens(usageTotals.output)}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (\n\t\t\t\ttokenUsageDisplay !== \"off\" &&\n\t\t\t\t(usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) &&\n\t\t\t\tlatestCacheHitRate !== undefined\n\t\t\t) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `CH${latestCacheHitRate.toFixed(1)}%`));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`));\n\t\t\t}\n\t\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.model.provider));\n\t\t\t}\n\t\t\tif (areExperimentalFeaturesEnabled()) optional.push(theme.fg(\"warning\", \"xp\"));\n\t\t\toptional.push(theme.fg(\"dim\", pwd));\n\n\t\t\tfor (const segment of optional) {\n\t\t\t\tif (fits([...left, segment, right])) left.push(segment);\n\t\t\t}\n\n\t\t\tconst leftText = joinSegments(left);\n\t\t\tconst padding = \" \".repeat(Math.max(1, width - visibleWidth(leftText) - visibleWidth(right)));\n\t\t\tconst tray = leftText ? `${leftText}${padding}${right}` : right;\n\t\t\tconst lines = [tray];\n\t\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\t\tif (extensionStatuses.size > 0) {\n\t\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\t\tlines.push(truncateToWidth(sortedStatuses.join(\" \"), width, theme.fg(\"dim\", \"...\")));\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\t// Permission mode is the first segment and must remain textual at every\n\t\t\t// positive width. An ellipsis suffix can consume the entire narrow line.\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\"\n\t\t\t\t\t? `${modelName} ${bulletSymbol} thinking off`\n\t\t\t\t\t: `${modelName} ${bulletSymbol} ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport { areExperimentalFeaturesEnabled } from \"../../../core/experimental.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { PermissionMode } from \"../../../core/permissions/store.ts\";\nimport { addUsageToTotals, createUsageTotals } from \"../../../core/usage-totals.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts for compact footer display.\n */\nexport function formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/** Cells in the context gauge. Eight reads as a bar without crowding the tray. */\nconst CONTEXT_GAUGE_CELLS = 8;\n\n/**\n * A filled bar for the context window, split into its filled and empty runs so\n * the caller can colour them separately.\n *\n * This is a redundant channel and never the only one: the percentage beside it\n * states the same value as text (WCAG 1.4.1). An unknown percentage renders an\n * empty track rather than guessing a position.\n */\nexport function renderContextGauge(\n\tpercent: number | undefined,\n\tsymbolPreset: \"unicode\" | \"ascii\",\n): { filled: string; empty: string } {\n\tconst filledGlyph = symbolPreset === \"ascii\" ? \"#\" : \"█\";\n\tconst emptyGlyph = symbolPreset === \"ascii\" ? \"-\" : \"░\";\n\tif (percent === undefined) {\n\t\treturn { filled: \"\", empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS) };\n\t}\n\tconst clamped = Math.min(100, Math.max(0, percent));\n\t// Any non-zero usage lights at least one cell, so the bar never reads empty\n\t// while the window is genuinely filling.\n\tconst cells = clamped === 0 ? 0 : Math.max(1, Math.round((clamped / 100) * CONTEXT_GAUGE_CELLS));\n\treturn { filled: filledGlyph.repeat(cells), empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS - cells) };\n}\n\n/** Accessibility/display settings the footer reads (roadmap Phase 8, task 8.6). */\nexport interface FooterAccessibilitySettings {\n\tgetSymbolPreset(): \"unicode\" | \"ascii\";\n\tgetColorBlindMode(): boolean;\n\tgetTokenUsageDisplay(): \"off\" | \"compact\" | \"full\";\n}\n\nconst DEFAULT_ACCESSIBILITY_SETTINGS: FooterAccessibilitySettings = {\n\tgetSymbolPreset: () => \"unicode\",\n\tgetColorBlindMode: () => false,\n\tgetTokenUsageDisplay: () => \"compact\",\n};\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~/${relativeToHome.replaceAll(\"\\\\\", \"/\")}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate permissionMode: PermissionMode = \"default\";\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate accessibilitySettings: FooterAccessibilitySettings;\n\tprivate activity: Component | undefined;\n\n\tconstructor(\n\t\tsession: AgentSession,\n\t\tfooterData: ReadonlyFooterDataProvider,\n\t\taccessibilitySettings: FooterAccessibilitySettings = DEFAULT_ACCESSIBILITY_SETTINGS,\n\t) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t\tthis.accessibilitySettings = accessibilitySettings;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t\t// The usage totals are cached against the previous session's entry\n\t\t// version; a session switch must not reuse them.\n\t\tthis.usageCache = undefined;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\tsetPermissionMode(mode: PermissionMode): void {\n\t\tthis.permissionMode = mode;\n\t}\n\n\t/**\n\t * The working indicator, rendered inline in the tray instead of in its own\n\t * rows above the editor.\n\t *\n\t * Taking the component rather than a string keeps the spinner animation, the\n\t * extension-supplied `WorkingIndicatorOptions`, and disposal where they already\n\t * live. This setter only moves where the line is drawn, so it cannot disturb\n\t * the usage cache.\n\t */\n\tsetActivity(activity: Component | undefined): void {\n\t\tthis.activity = activity;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\t/**\n\t * Usage totals walked from all session entries. getEntries() copies the\n\t * whole entry list, so this is cached against the session manager's\n\t * entries version: without the cache the footer re-walked the entire\n\t * transcript on every rendered frame.\n\t */\n\tprivate usageCache?: {\n\t\tversion: number;\n\t\ttotals: ReturnType<typeof createUsageTotals>;\n\t\tlatestCacheHitRate?: number;\n\t};\n\n\tprivate computeUsage(): {\n\t\ttotals: ReturnType<typeof createUsageTotals>;\n\t\tlatestCacheHitRate?: number;\n\t} {\n\t\tconst version = this.session.sessionManager.getEntriesVersion();\n\t\tif (this.usageCache && this.usageCache.version === version) {\n\t\t\treturn { totals: this.usageCache.totals, latestCacheHitRate: this.usageCache.latestCacheHitRate };\n\t\t}\n\n\t\tconst usageTotals = createUsageTotals();\n\t\tlet latestCacheHitRate: number | undefined;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\n\t\t\t\tconst latestPromptTokens =\n\t\t\t\t\tentry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;\n\t\t\t\tlatestCacheHitRate =\n\t\t\t\t\tlatestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;\n\t\t\t} else if (entry.type === \"message\" && entry.message.role === \"toolResult\" && entry.message.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\t\t\t} else if ((entry.type === \"branch_summary\" || entry.type === \"compaction\") && entry.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.usage);\n\t\t\t}\n\t\t}\n\n\t\tthis.usageCache = { version, totals: usageTotals, latestCacheHitRate };\n\t\treturn { totals: usageTotals, latestCacheHitRate };\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\t\tconst symbolPreset = this.accessibilitySettings.getSymbolPreset();\n\t\tconst colorBlindMode = this.accessibilitySettings.getColorBlindMode();\n\t\tconst tokenUsageDisplay = this.accessibilitySettings.getTokenUsageDisplay();\n\t\tconst upSymbol = symbolPreset === \"ascii\" ? \"^\" : \"↑\";\n\t\tconst downSymbol = symbolPreset === \"ascii\" ? \"v\" : \"↓\";\n\t\tconst bulletSymbol = symbolPreset === \"ascii\" ? \"-\" : \"•\";\n\t\t// One line, unpadded, so the fit ladder can measure it as a segment. Loader\n\t\t// prepends a blank row to stand off from the transcript, which the tray does\n\t\t// not need, so this takes the first row that actually carries the message.\n\t\tconst activityLine = this.activity\n\t\t\t?.render(width)\n\t\t\t.map((line) => line.replace(/ +$/, \"\"))\n\t\t\t.find((line) => line.length > 0);\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages),\n\t\t// cached against the session's entry version (see computeUsage).\n\t\tconst { totals: usageTotals, latestCacheHitRate } = this.computeUsage();\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\t// The gauge lights a cell for any usage at all, so a figure that rounds to zero says so.\n\t\tconst contextPercent =\n\t\t\tcontextUsage?.percent === null\n\t\t\t\t? \"?\"\n\t\t\t\t: contextPercentValue > 0 && contextPercentValue < 0.05\n\t\t\t\t\t? \"<0.1\"\n\t\t\t\t\t: contextPercentValue.toFixed(1);\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} ${bulletSymbol} ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line. tokenUsageDisplay: \"off\" hides this whole section\n\t\t// (context% and model name are separate and always shown); \"full\" shows\n\t\t// exact counts instead of formatTokens' abbreviation.\n\t\tconst formatTokenCount = tokenUsageDisplay === \"full\" ? (n: number) => n.toLocaleString() : formatTokens;\n\t\tconst statsParts = [];\n\t\tif (tokenUsageDisplay !== \"off\") {\n\t\t\tif (usageTotals.input) statsParts.push(`${upSymbol}${formatTokenCount(usageTotals.input)}`);\n\t\t\tif (usageTotals.output) statsParts.push(`${downSymbol}${formatTokenCount(usageTotals.output)}`);\n\t\t\tif (usageTotals.cacheRead) statsParts.push(`R${formatTokenCount(usageTotals.cacheRead)}`);\n\t\t\tif (usageTotals.cacheWrite) statsParts.push(`W${formatTokenCount(usageTotals.cacheWrite)}`);\n\t\t\tif ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {\n\t\t\t\tstatsParts.push(`cache ${Math.round(latestCacheHitRate)}%`);\n\t\t\t}\n\t\t}\n\n\t\t// Kimi Coding is subscription-backed despite using API-key authentication.\n\t\tconst usingSubscription = state.model\n\t\t\t? state.model.provider === \"kimi-coding\" || this.session.modelRuntime.isUsingSubscription(state.model.provider)\n\t\t\t: false;\n\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\tconst costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Context pressure is signalled through text at every setting -- never\n\t\t// color alone (WCAG 1.4.1) -- with color as an additional channel. The\n\t\t// default error/warning pair (red/amber) is already distinguishable for\n\t\t// most red-green colorblindness; colorBlindMode's palette adjustment\n\t\t// moves the critical (>90%) threshold to \"accent\" -- a hue outside that\n\t\t// red/amber family entirely -- rather than relying on a style attribute\n\t\t// like bold/underline, which several terminals (and this theme's own\n\t\t// chalk-backed style methods, suppressed outside a real TTY) silently\n\t\t// drop, making it an unreliable second channel.\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst pressureMarker = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%${pressureMarker}/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(colorBlindMode ? \"accent\" : \"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\t// Only worth saying when pressure is real and nothing will act on its own.\n\t\t// With auto-compaction on, the \"(auto)\" marker above already answers it.\n\t\tconst compactPrompt =\n\t\t\tcontextPercentValue > 70 && !this.autoCompactEnabled ? theme.fg(\"warning\", \"/compact soon\") : undefined;\n\t\tstatsParts.push(contextPercentStr);\n\t\tif (compactPrompt) statsParts.push(compactPrompt);\n\t\tif (activityLine) statsParts.unshift(activityLine);\n\t\tif (areExperimentalFeaturesEnabled()) {\n\t\t\tstatsParts.push(`${theme.fg(\"dim\", bulletSymbol)} ${theme.bold(theme.fg(\"warning\", \"xp\"))}`);\n\t\t}\n\n\t\t// Permission posture is always named, including `default`. It is operational\n\t\t// state rather than routine telemetry: hiding the safe default would make the\n\t\t// tray change meaning by omission, while hiding bypass would conceal risk.\n\t\t// Text carries the signal and color remains a second channel (WCAG 1.4.1).\n\t\tconst modeColor =\n\t\t\tthis.permissionMode === \"bypassPermissions\" ? \"error\" : this.permissionMode === \"default\" ? \"dim\" : \"warning\";\n\t\tstatsParts.unshift(theme.fg(modeColor, this.permissionMode));\n\n\t\tif (tokenUsageDisplay !== \"full\") {\n\t\t\tconst separator = theme.fg(\"dim\", symbolPreset === \"ascii\" ? \" - \" : \" · \");\n\t\t\tconst compactPermissionNames: Record<PermissionMode, string> = {\n\t\t\t\tdefault: \"default\",\n\t\t\t\tplan: \"plan\",\n\t\t\t\tacceptEdits: \"accept\",\n\t\t\t\tbypassPermissions: \"bypass\",\n\t\t\t\tdontAsk: \"dontAsk\",\n\t\t\t};\n\t\t\tconst permissionColor =\n\t\t\t\tthis.permissionMode === \"bypassPermissions\"\n\t\t\t\t\t? \"error\"\n\t\t\t\t\t: this.permissionMode === \"default\"\n\t\t\t\t\t\t? \"dim\"\n\t\t\t\t\t\t: \"warning\";\n\t\t\tconst permissionFull = theme.fg(permissionColor, this.permissionMode);\n\t\t\tconst permissionCompact = theme.fg(permissionColor, compactPermissionNames[this.permissionMode]);\n\t\t\tconst compactPercent = contextPercent === \"?\" ? \"?\" : contextPercentValue.toFixed(0);\n\t\t\tconst compactPressure = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\t\tconst contextColor =\n\t\t\t\tcontextPercentValue > 90\n\t\t\t\t\t? colorBlindMode\n\t\t\t\t\t\t? \"accent\"\n\t\t\t\t\t\t: \"error\"\n\t\t\t\t\t: contextPercentValue > 70\n\t\t\t\t\t\t? \"warning\"\n\t\t\t\t\t\t: \"dim\";\n\t\t\t// The gauge rides along only in the roomy form. The compact form drops\n\t\t\t// it, which is what lets it keep winning the fit ladder below.\n\t\t\tconst gauge = renderContextGauge(contextPercent === \"?\" ? undefined : contextPercentValue, symbolPreset);\n\t\t\t// Below the warning threshold the text is dim, which would make the bar\n\t\t\t// invisible; the fill takes the accent there and tracks the text colour\n\t\t\t// at every level that actually signals pressure.\n\t\t\tconst gaugeColor = contextColor === \"dim\" ? \"accent\" : contextColor;\n\t\t\t// An empty run would still emit an open/close SGR pair, so skip it.\n\t\t\tconst paintRun = (color: Parameters<typeof theme.fg>[0], text: string) => (text ? theme.fg(color, text) : \"\");\n\t\t\tconst contextGauged =\n\t\t\t\ttheme.fg(contextColor, \"context \") +\n\t\t\t\tpaintRun(gaugeColor, gauge.filled) +\n\t\t\t\tpaintRun(\"borderMuted\", gauge.empty) +\n\t\t\t\ttheme.fg(contextColor, ` ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextFull = theme.fg(contextColor, `context ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextCompact = theme.fg(contextColor, `ctx ${compactPercent}%${compactPressure}`);\n\n\t\t\tconst joinSegments = (segments: string[]): string => segments.join(separator);\n\t\t\tconst fits = (segments: string[]): boolean => visibleWidth(joinSegments(segments)) <= width;\n\t\t\tconst safetyFirst = (...rest: Array<string | undefined>): string[] => [\n\t\t\t\tpermissionFull,\n\t\t\t\t...rest.filter((segment): segment is string => segment !== undefined),\n\t\t\t];\n\t\t\tlet left: string[] = safetyFirst(compactPrompt, activityLine);\n\t\t\tlet right = contextGauged;\n\n\t\t\t// The gauge is decoration over a value the text already states, so it\n\t\t\t// is the first thing to go. Dropping it must not cost the tray its\n\t\t\t// spelled-out permission mode, which is why this is its own rung.\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tright = contextFull;\n\t\t\t}\n\n\t\t\t// Activity is operational, so it outranks every optional segment below.\n\t\t\t// It still yields to the safety posture and to the pressure reading,\n\t\t\t// which are the two things the tray must never lose.\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = safetyFirst(compactPrompt);\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = safetyFirst();\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = [permissionCompact];\n\t\t\t\tright = contextCompact;\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tconst safetyOnly = left[0];\n\t\t\t\tconst line =\n\t\t\t\t\tvisibleWidth(safetyOnly) <= width ? safetyOnly : truncateToWidth(safetyOnly, Math.max(0, width), \"\");\n\t\t\t\treturn width > 0 ? [line] : [];\n\t\t\t}\n\n\t\t\tconst optional: string[] = [];\n\t\t\toptional.push(theme.fg(\"dim\", state.model?.id || \"no-model\"));\n\t\t\tif (state.model?.reasoning && state.thinkingLevel && state.thinkingLevel !== \"off\") {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.thinkingLevel));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.input || usageTotals.output)) {\n\t\t\t\toptional.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`${upSymbol}${formatTokens(usageTotals.input)} ${downSymbol}${formatTokens(usageTotals.output)}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (\n\t\t\t\ttokenUsageDisplay !== \"off\" &&\n\t\t\t\t(usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) &&\n\t\t\t\tlatestCacheHitRate !== undefined\n\t\t\t) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `cache ${Math.round(latestCacheHitRate)}%`));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`));\n\t\t\t}\n\t\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.model.provider));\n\t\t\t}\n\t\t\tif (areExperimentalFeaturesEnabled()) optional.push(theme.fg(\"warning\", \"xp\"));\n\t\t\toptional.push(theme.fg(\"dim\", pwd));\n\n\t\t\tfor (const segment of optional) {\n\t\t\t\tif (fits([...left, segment, right])) left.push(segment);\n\t\t\t}\n\n\t\t\tconst leftText = joinSegments(left);\n\t\t\tconst padding = \" \".repeat(Math.max(1, width - visibleWidth(leftText) - visibleWidth(right)));\n\t\t\tconst tray = leftText ? `${leftText}${padding}${right}` : right;\n\t\t\tconst lines = [tray];\n\t\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\t\tif (extensionStatuses.size > 0) {\n\t\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\t\tlines.push(truncateToWidth(sortedStatuses.join(\" \"), width, theme.fg(\"dim\", \"...\")));\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\t// Permission mode is the first segment and must remain textual at every\n\t\t\t// positive width. An ellipsis suffix can consume the entire narrow line.\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\"\n\t\t\t\t\t? `${modelName} ${bulletSymbol} thinking off`\n\t\t\t\t\t: `${modelName} ${bulletSymbol} ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
@@ -16,4 +16,25 @@ export declare function keyText(keybinding: Keybinding, options?: KeyTextFormatO
16
16
  export declare function keyDisplayText(keybinding: Keybinding): string;
17
17
  export declare function keyHint(keybinding: Keybinding, description: string): string;
18
18
  export declare function rawKeyHint(key: string, description: string): string;
19
+ /**
20
+ * The one shape for "output is hidden here": `... (12 more lines, ctrl+o to expand)`.
21
+ * `all` is a body collapsed to nothing. Empty when nothing is hidden, so a caller can
22
+ * append it unconditionally.
23
+ */
24
+ export declare function formatHiddenLines(count: number, position: "more" | "earlier" | "all"): string;
25
+ /** How many lines a collapsed preview shows. Hiding one line would spend its row on the hint. */
26
+ export declare function previewLineCount(total: number, limit: number): number;
27
+ /** A binding, or a key that is not one. */
28
+ export type HintKeyPart = Keybinding | {
29
+ literal: string;
30
+ };
31
+ /** One key, or several shown as `a/b` by their primary binding. */
32
+ export type HintKey = HintKeyPart | readonly HintKeyPart[];
33
+ /**
34
+ * The one grammar for a row of key hints: `up/down move · enter select · escape close`.
35
+ * Keys come from the live bindings, so a rebound key is reported as the user bound it.
36
+ */
37
+ export declare function hintRow(hints: ReadonlyArray<readonly [key: HintKey, action: string]>, options?: {
38
+ ascii?: boolean;
39
+ }): string;
19
40
  //# sourceMappingURL=keybinding-hints.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"keybinding-hints.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/keybinding-hints.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAkB,KAAK,UAAU,EAAc,MAAM,wBAAwB,CAAC;AAGrF,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAOD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAUrF;AAOD,wBAAgB,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAG1F;AAED,wBAAgB,cAAc,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CAE7D;AAED,wBAAgB,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEnE","sourcesContent":["/**\n * Utilities for formatting keybinding hints in the UI.\n */\n\nimport { getKeybindings, type Keybinding, type KeyId } from \"@earendil-works/pi-tui\";\nimport { theme } from \"../theme/theme.ts\";\n\nexport interface KeyTextFormatOptions {\n\tcapitalize?: boolean;\n\t/**\n\t * Name only the first binding. A hint sitting alongside several others has\n\t * no room to list every alias, and \"left/ctrl+b/right/ctrl+f effort\" is\n\t * longer than the thing it explains.\n\t */\n\tprimaryOnly?: boolean;\n}\n\nfunction formatKeyPart(part: string, options: KeyTextFormatOptions): string {\n\tconst displayPart = process.platform === \"darwin\" && part.toLowerCase() === \"alt\" ? \"option\" : part;\n\treturn options.capitalize ? displayPart.charAt(0).toUpperCase() + displayPart.slice(1) : displayPart;\n}\n\nexport function formatKeyText(key: string, options: KeyTextFormatOptions = {}): string {\n\treturn key\n\t\t.split(\"/\")\n\t\t.map((k) =>\n\t\t\tk\n\t\t\t\t.split(\"+\")\n\t\t\t\t.map((part) => formatKeyPart(part, options))\n\t\t\t\t.join(\"+\"),\n\t\t)\n\t\t.join(\"/\");\n}\n\nfunction formatKeys(keys: KeyId[], options: KeyTextFormatOptions = {}): string {\n\tif (keys.length === 0) return \"\";\n\treturn formatKeyText(keys.join(\"/\"), options);\n}\n\nexport function keyText(keybinding: Keybinding, options: KeyTextFormatOptions = {}): string {\n\tconst keys = getKeybindings().getKeys(keybinding);\n\treturn formatKeys(options.primaryOnly ? keys.slice(0, 1) : keys, options);\n}\n\nexport function keyDisplayText(keybinding: Keybinding): string {\n\treturn formatKeys(getKeybindings().getKeys(keybinding), { capitalize: true });\n}\n\nexport function keyHint(keybinding: Keybinding, description: string): string {\n\treturn theme.fg(\"dim\", keyText(keybinding)) + theme.fg(\"muted\", ` ${description}`);\n}\n\nexport function rawKeyHint(key: string, description: string): string {\n\treturn theme.fg(\"dim\", formatKeyText(key)) + theme.fg(\"muted\", ` ${description}`);\n}\n"]}
1
+ {"version":3,"file":"keybinding-hints.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/keybinding-hints.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAkB,KAAK,UAAU,EAAc,MAAM,wBAAwB,CAAC;AAGrF,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAOD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAUrF;AAOD,wBAAgB,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAG1F;AAED,wBAAgB,cAAc,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CAE7D;AAED,wBAAgB,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,MAAM,CAK7F;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,2CAA2C;AAC3C,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D,mEAAmE;AACnE,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,SAAS,WAAW,EAAE,CAAC;AAM3D;;;GAGG;AACH,wBAAgB,OAAO,CACtB,KAAK,EAAE,aAAa,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAC7D,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,MAAM,CAUR","sourcesContent":["/**\n * Utilities for formatting keybinding hints in the UI.\n */\n\nimport { getKeybindings, type Keybinding, type KeyId } from \"@earendil-works/pi-tui\";\nimport { theme } from \"../theme/theme.ts\";\n\nexport interface KeyTextFormatOptions {\n\tcapitalize?: boolean;\n\t/**\n\t * Name only the first binding. A hint sitting alongside several others has\n\t * no room to list every alias, and \"left/ctrl+b/right/ctrl+f effort\" is\n\t * longer than the thing it explains.\n\t */\n\tprimaryOnly?: boolean;\n}\n\nfunction formatKeyPart(part: string, options: KeyTextFormatOptions): string {\n\tconst displayPart = process.platform === \"darwin\" && part.toLowerCase() === \"alt\" ? \"option\" : part;\n\treturn options.capitalize ? displayPart.charAt(0).toUpperCase() + displayPart.slice(1) : displayPart;\n}\n\nexport function formatKeyText(key: string, options: KeyTextFormatOptions = {}): string {\n\treturn key\n\t\t.split(\"/\")\n\t\t.map((k) =>\n\t\t\tk\n\t\t\t\t.split(\"+\")\n\t\t\t\t.map((part) => formatKeyPart(part, options))\n\t\t\t\t.join(\"+\"),\n\t\t)\n\t\t.join(\"/\");\n}\n\nfunction formatKeys(keys: KeyId[], options: KeyTextFormatOptions = {}): string {\n\tif (keys.length === 0) return \"\";\n\treturn formatKeyText(keys.join(\"/\"), options);\n}\n\nexport function keyText(keybinding: Keybinding, options: KeyTextFormatOptions = {}): string {\n\tconst keys = getKeybindings().getKeys(keybinding);\n\treturn formatKeys(options.primaryOnly ? keys.slice(0, 1) : keys, options);\n}\n\nexport function keyDisplayText(keybinding: Keybinding): string {\n\treturn formatKeys(getKeybindings().getKeys(keybinding), { capitalize: true });\n}\n\nexport function keyHint(keybinding: Keybinding, description: string): string {\n\treturn theme.fg(\"dim\", keyText(keybinding)) + theme.fg(\"muted\", ` ${description}`);\n}\n\nexport function rawKeyHint(key: string, description: string): string {\n\treturn theme.fg(\"dim\", formatKeyText(key)) + theme.fg(\"muted\", ` ${description}`);\n}\n\n/**\n * The one shape for \"output is hidden here\": `... (12 more lines, ctrl+o to expand)`.\n * `all` is a body collapsed to nothing. Empty when nothing is hidden, so a caller can\n * append it unconditionally.\n */\nexport function formatHiddenLines(count: number, position: \"more\" | \"earlier\" | \"all\"): string {\n\tif (count <= 0) return \"\";\n\tconst noun = count === 1 ? \"line\" : \"lines\";\n\tconst counted = position === \"all\" ? `${count} ${noun}` : `${count} ${position} ${noun}`;\n\treturn `${theme.fg(\"muted\", `... (${counted},`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n}\n\n/** How many lines a collapsed preview shows. Hiding one line would spend its row on the hint. */\nexport function previewLineCount(total: number, limit: number): number {\n\treturn total <= limit + 1 ? total : limit;\n}\n\n/** A binding, or a key that is not one. */\nexport type HintKeyPart = Keybinding | { literal: string };\n\n/** One key, or several shown as `a/b` by their primary binding. */\nexport type HintKey = HintKeyPart | readonly HintKeyPart[];\n\nfunction hintKeyText(part: HintKeyPart, options: KeyTextFormatOptions): string {\n\treturn typeof part === \"string\" ? keyText(part, options) : formatKeyText(part.literal);\n}\n\n/**\n * The one grammar for a row of key hints: `up/down move · enter select · escape close`.\n * Keys come from the live bindings, so a rebound key is reported as the user bound it.\n */\nexport function hintRow(\n\thints: ReadonlyArray<readonly [key: HintKey, action: string]>,\n\toptions: { ascii?: boolean } = {},\n): string {\n\tconst separator = theme.fg(\"borderMuted\", options.ascii ? \" - \" : \" · \");\n\treturn hints\n\t\t.map(([key, action]) => {\n\t\t\tconst text = Array.isArray(key)\n\t\t\t\t? key.map((part: HintKeyPart) => hintKeyText(part, { primaryOnly: true })).join(\"/\")\n\t\t\t\t: hintKeyText(key as HintKeyPart, {});\n\t\t\treturn `${theme.fg(\"dim\", text)}${theme.fg(\"muted\", ` ${action}`)}`;\n\t\t})\n\t\t.join(separator);\n}\n"]}
@@ -34,4 +34,38 @@ export function keyHint(keybinding, description) {
34
34
  export function rawKeyHint(key, description) {
35
35
  return theme.fg("dim", formatKeyText(key)) + theme.fg("muted", ` ${description}`);
36
36
  }
37
+ /**
38
+ * The one shape for "output is hidden here": `... (12 more lines, ctrl+o to expand)`.
39
+ * `all` is a body collapsed to nothing. Empty when nothing is hidden, so a caller can
40
+ * append it unconditionally.
41
+ */
42
+ export function formatHiddenLines(count, position) {
43
+ if (count <= 0)
44
+ return "";
45
+ const noun = count === 1 ? "line" : "lines";
46
+ const counted = position === "all" ? `${count} ${noun}` : `${count} ${position} ${noun}`;
47
+ return `${theme.fg("muted", `... (${counted},`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
48
+ }
49
+ /** How many lines a collapsed preview shows. Hiding one line would spend its row on the hint. */
50
+ export function previewLineCount(total, limit) {
51
+ return total <= limit + 1 ? total : limit;
52
+ }
53
+ function hintKeyText(part, options) {
54
+ return typeof part === "string" ? keyText(part, options) : formatKeyText(part.literal);
55
+ }
56
+ /**
57
+ * The one grammar for a row of key hints: `up/down move · enter select · escape close`.
58
+ * Keys come from the live bindings, so a rebound key is reported as the user bound it.
59
+ */
60
+ export function hintRow(hints, options = {}) {
61
+ const separator = theme.fg("borderMuted", options.ascii ? " - " : " · ");
62
+ return hints
63
+ .map(([key, action]) => {
64
+ const text = Array.isArray(key)
65
+ ? key.map((part) => hintKeyText(part, { primaryOnly: true })).join("/")
66
+ : hintKeyText(key, {});
67
+ return `${theme.fg("dim", text)}${theme.fg("muted", ` ${action}`)}`;
68
+ })
69
+ .join(separator);
70
+ }
37
71
  //# sourceMappingURL=keybinding-hints.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"keybinding-hints.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/keybinding-hints.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,cAAc,EAA+B,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAY1C,SAAS,aAAa,CAAC,IAAY,EAAE,OAA6B,EAAU;IAC3E,MAAM,WAAW,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IACpG,OAAO,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;AAAA,CACrG;AAED,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,OAAO,GAAyB,EAAE,EAAU;IACtF,OAAO,GAAG;SACR,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACV,CAAC;SACC,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;SAC3C,IAAI,CAAC,GAAG,CAAC,CACX;SACA,IAAI,CAAC,GAAG,CAAC,CAAC;AAAA,CACZ;AAED,SAAS,UAAU,CAAC,IAAa,EAAE,OAAO,GAAyB,EAAE,EAAU;IAC9E,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;AAAA,CAC9C;AAED,MAAM,UAAU,OAAO,CAAC,UAAsB,EAAE,OAAO,GAAyB,EAAE,EAAU;IAC3F,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAClD,OAAO,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAAA,CAC1E;AAED,MAAM,UAAU,cAAc,CAAC,UAAsB,EAAU;IAC9D,OAAO,UAAU,CAAC,cAAc,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;AAAA,CAC9E;AAED,MAAM,UAAU,OAAO,CAAC,UAAsB,EAAE,WAAmB,EAAU;IAC5E,OAAO,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,WAAW,EAAE,CAAC,CAAC;AAAA,CACnF;AAED,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,WAAmB,EAAU;IACpE,OAAO,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,WAAW,EAAE,CAAC,CAAC;AAAA,CAClF","sourcesContent":["/**\n * Utilities for formatting keybinding hints in the UI.\n */\n\nimport { getKeybindings, type Keybinding, type KeyId } from \"@earendil-works/pi-tui\";\nimport { theme } from \"../theme/theme.ts\";\n\nexport interface KeyTextFormatOptions {\n\tcapitalize?: boolean;\n\t/**\n\t * Name only the first binding. A hint sitting alongside several others has\n\t * no room to list every alias, and \"left/ctrl+b/right/ctrl+f effort\" is\n\t * longer than the thing it explains.\n\t */\n\tprimaryOnly?: boolean;\n}\n\nfunction formatKeyPart(part: string, options: KeyTextFormatOptions): string {\n\tconst displayPart = process.platform === \"darwin\" && part.toLowerCase() === \"alt\" ? \"option\" : part;\n\treturn options.capitalize ? displayPart.charAt(0).toUpperCase() + displayPart.slice(1) : displayPart;\n}\n\nexport function formatKeyText(key: string, options: KeyTextFormatOptions = {}): string {\n\treturn key\n\t\t.split(\"/\")\n\t\t.map((k) =>\n\t\t\tk\n\t\t\t\t.split(\"+\")\n\t\t\t\t.map((part) => formatKeyPart(part, options))\n\t\t\t\t.join(\"+\"),\n\t\t)\n\t\t.join(\"/\");\n}\n\nfunction formatKeys(keys: KeyId[], options: KeyTextFormatOptions = {}): string {\n\tif (keys.length === 0) return \"\";\n\treturn formatKeyText(keys.join(\"/\"), options);\n}\n\nexport function keyText(keybinding: Keybinding, options: KeyTextFormatOptions = {}): string {\n\tconst keys = getKeybindings().getKeys(keybinding);\n\treturn formatKeys(options.primaryOnly ? keys.slice(0, 1) : keys, options);\n}\n\nexport function keyDisplayText(keybinding: Keybinding): string {\n\treturn formatKeys(getKeybindings().getKeys(keybinding), { capitalize: true });\n}\n\nexport function keyHint(keybinding: Keybinding, description: string): string {\n\treturn theme.fg(\"dim\", keyText(keybinding)) + theme.fg(\"muted\", ` ${description}`);\n}\n\nexport function rawKeyHint(key: string, description: string): string {\n\treturn theme.fg(\"dim\", formatKeyText(key)) + theme.fg(\"muted\", ` ${description}`);\n}\n"]}
1
+ {"version":3,"file":"keybinding-hints.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/keybinding-hints.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Utilities for formatting keybinding hints in the UI.\n */\n\nimport { getKeybindings, type Keybinding, type KeyId } from \"@earendil-works/pi-tui\";\nimport { theme } from \"../theme/theme.ts\";\n\nexport interface KeyTextFormatOptions {\n\tcapitalize?: boolean;\n\t/**\n\t * Name only the first binding. A hint sitting alongside several others has\n\t * no room to list every alias, and \"left/ctrl+b/right/ctrl+f effort\" is\n\t * longer than the thing it explains.\n\t */\n\tprimaryOnly?: boolean;\n}\n\nfunction formatKeyPart(part: string, options: KeyTextFormatOptions): string {\n\tconst displayPart = process.platform === \"darwin\" && part.toLowerCase() === \"alt\" ? \"option\" : part;\n\treturn options.capitalize ? displayPart.charAt(0).toUpperCase() + displayPart.slice(1) : displayPart;\n}\n\nexport function formatKeyText(key: string, options: KeyTextFormatOptions = {}): string {\n\treturn key\n\t\t.split(\"/\")\n\t\t.map((k) =>\n\t\t\tk\n\t\t\t\t.split(\"+\")\n\t\t\t\t.map((part) => formatKeyPart(part, options))\n\t\t\t\t.join(\"+\"),\n\t\t)\n\t\t.join(\"/\");\n}\n\nfunction formatKeys(keys: KeyId[], options: KeyTextFormatOptions = {}): string {\n\tif (keys.length === 0) return \"\";\n\treturn formatKeyText(keys.join(\"/\"), options);\n}\n\nexport function keyText(keybinding: Keybinding, options: KeyTextFormatOptions = {}): string {\n\tconst keys = getKeybindings().getKeys(keybinding);\n\treturn formatKeys(options.primaryOnly ? keys.slice(0, 1) : keys, options);\n}\n\nexport function keyDisplayText(keybinding: Keybinding): string {\n\treturn formatKeys(getKeybindings().getKeys(keybinding), { capitalize: true });\n}\n\nexport function keyHint(keybinding: Keybinding, description: string): string {\n\treturn theme.fg(\"dim\", keyText(keybinding)) + theme.fg(\"muted\", ` ${description}`);\n}\n\nexport function rawKeyHint(key: string, description: string): string {\n\treturn theme.fg(\"dim\", formatKeyText(key)) + theme.fg(\"muted\", ` ${description}`);\n}\n\n/**\n * The one shape for \"output is hidden here\": `... (12 more lines, ctrl+o to expand)`.\n * `all` is a body collapsed to nothing. Empty when nothing is hidden, so a caller can\n * append it unconditionally.\n */\nexport function formatHiddenLines(count: number, position: \"more\" | \"earlier\" | \"all\"): string {\n\tif (count <= 0) return \"\";\n\tconst noun = count === 1 ? \"line\" : \"lines\";\n\tconst counted = position === \"all\" ? `${count} ${noun}` : `${count} ${position} ${noun}`;\n\treturn `${theme.fg(\"muted\", `... (${counted},`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n}\n\n/** How many lines a collapsed preview shows. Hiding one line would spend its row on the hint. */\nexport function previewLineCount(total: number, limit: number): number {\n\treturn total <= limit + 1 ? total : limit;\n}\n\n/** A binding, or a key that is not one. */\nexport type HintKeyPart = Keybinding | { literal: string };\n\n/** One key, or several shown as `a/b` by their primary binding. */\nexport type HintKey = HintKeyPart | readonly HintKeyPart[];\n\nfunction hintKeyText(part: HintKeyPart, options: KeyTextFormatOptions): string {\n\treturn typeof part === \"string\" ? keyText(part, options) : formatKeyText(part.literal);\n}\n\n/**\n * The one grammar for a row of key hints: `up/down move · enter select · escape close`.\n * Keys come from the live bindings, so a rebound key is reported as the user bound it.\n */\nexport function hintRow(\n\thints: ReadonlyArray<readonly [key: HintKey, action: string]>,\n\toptions: { ascii?: boolean } = {},\n): string {\n\tconst separator = theme.fg(\"borderMuted\", options.ascii ? \" - \" : \" · \");\n\treturn hints\n\t\t.map(([key, action]) => {\n\t\t\tconst text = Array.isArray(key)\n\t\t\t\t? key.map((part: HintKeyPart) => hintKeyText(part, { primaryOnly: true })).join(\"/\")\n\t\t\t\t: hintKeyText(key as HintKeyPart, {});\n\t\t\treturn `${theme.fg(\"dim\", text)}${theme.fg(\"muted\", ` ${action}`)}`;\n\t\t})\n\t\t.join(separator);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"login-dialog.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/login-dialog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAuC,KAAK,GAAG,EAAE,MAAM,wBAAwB,CAAC;AAMlH;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,SAAU,YAAW,SAAS;IACvE,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,aAAa,CAAC,CAA0B;IAChD,OAAO,CAAC,aAAa,CAAC,CAAyB;IAC/C,OAAO,CAAC,UAAU,CAA+C;IAGjE,OAAO,CAAC,QAAQ,CAAS;IACzB,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAGzB;IAED,YACC,GAAG,EAAE,GAAG,EACR,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,EACxD,oBAAoB,CAAC,EAAE,MAAM,EAC7B,aAAa,CAAC,EAAE,MAAM,EAoCtB;IAED,IAAI,MAAM,IAAI,WAAW,CAExB;IAED,OAAO,CAAC,6BAA6B;IAMrC,OAAO,CAAC,MAAM;IAUd;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAiBjD;IAED;;OAEG;IACH,cAAc,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAa9C;IAED;;OAEG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY/C;IAED;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsBjE;IAED,yDAAyD;IACzD,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAOjC;IAED,wFAAwF;IACxF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,SAAS,YAAY,EAAO,EAAE,aAAa,UAAQ,GAAG,IAAI,CAa1F;IAED;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKjC;IAED;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGlC;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAU9B;CACD","sourcesContent":["import type { AuthInfoLink, OAuthDeviceCodeInfo } from \"@earendil-works/pi-ai\";\nimport { Container, type Focusable, getKeybindings, Input, Spacer, Text, type TUI } from \"@earendil-works/pi-tui\";\nimport { openBrowser } from \"../../../utils/open-browser.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint } from \"./keybinding-hints.ts\";\n\n/**\n * Login dialog component - replaces editor during OAuth login flow\n */\nexport class LoginDialogComponent extends Container implements Focusable {\n\tprivate contentContainer: Container;\n\tprivate input: Input;\n\tprivate tui: TUI;\n\tprivate abortController = new AbortController();\n\tprivate inputResolver?: (value: string) => void;\n\tprivate inputRejecter?: (error: Error) => void;\n\tprivate onComplete: (success: boolean, message?: string) => void;\n\n\t// Focusable implementation - propagate to input for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.input.focused = value;\n\t}\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tproviderId: string,\n\t\tonComplete: (success: boolean, message?: string) => void,\n\t\tproviderNameOverride?: string,\n\t\ttitleOverride?: string,\n\t) {\n\t\tsuper();\n\t\tthis.tui = tui;\n\t\tthis.onComplete = onComplete;\n\n\t\tconst providerName = providerNameOverride || providerId;\n\t\tconst title = titleOverride ?? `Login to ${providerName}`;\n\n\t\t// Top border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Title\n\t\tthis.addChild(new Text(theme.fg(\"accent\", theme.bold(title)), 1, 0));\n\n\t\t// Dynamic content area\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\t// Input (always present, used when needed)\n\t\tthis.input = new Input();\n\t\tthis.input.onSubmit = () => {\n\t\t\tif (this.inputResolver) {\n\t\t\t\tconst value = this.input.getValue();\n\t\t\t\tthis.replaceInputWithSubmittedText(value);\n\t\t\t\tthis.inputResolver(value);\n\t\t\t\tthis.inputResolver = undefined;\n\t\t\t\tthis.inputRejecter = undefined;\n\t\t\t}\n\t\t};\n\t\tthis.input.onEscape = () => {\n\t\t\tthis.cancel();\n\t\t};\n\n\t\t// Bottom border\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\tget signal(): AbortSignal {\n\t\treturn this.abortController.signal;\n\t}\n\n\tprivate replaceInputWithSubmittedText(value: string): void {\n\t\tthis.contentContainer.children = this.contentContainer.children.map((child) =>\n\t\t\tchild === this.input ? new Text(`> ${value}`, 0, 0) : child,\n\t\t);\n\t}\n\n\tprivate cancel(): void {\n\t\tthis.abortController.abort();\n\t\tif (this.inputRejecter) {\n\t\t\tthis.inputRejecter(new Error(\"Login cancelled\"));\n\t\t\tthis.inputResolver = undefined;\n\t\t\tthis.inputRejecter = undefined;\n\t\t}\n\t\tthis.onComplete(false, \"Login cancelled\");\n\t}\n\n\t/**\n\t * Called by onAuth callback - show URL and optional instructions\n\t */\n\tshowAuth(url: string, instructions?: string): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tconst linkedUrl = `\\x1b]8;;${url}\\x07${url}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", linkedUrl), 1, 0));\n\n\t\tconst clickHint = process.platform === \"darwin\" ? \"Cmd+click to open\" : \"Ctrl+click to open\";\n\t\tconst hyperlink = `\\x1b]8;;${url}\\x07${clickHint}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", hyperlink), 1, 0));\n\n\t\tif (instructions) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"warning\", instructions), 1, 0));\n\t\t}\n\n\t\topenBrowser(url);\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Called by onDeviceCode callback - show URL and user code.\n\t */\n\tshowDeviceCode(info: OAuthDeviceCodeInfo): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tconst linkedUrl = `\\x1b]8;;${info.verificationUri}\\x07${info.verificationUri}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", linkedUrl), 1, 0));\n\n\t\tconst clickHint = process.platform === \"darwin\" ? \"Cmd+click to open\" : \"Ctrl+click to open\";\n\t\tconst hyperlink = `\\x1b]8;;${info.verificationUri}\\x07${clickHint}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", hyperlink), 1, 0));\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"warning\", `Enter code: ${info.userCode}`), 1, 0));\n\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Show input for manual code/URL entry (for callback server providers)\n\t */\n\tshowManualInput(prompt: string): Promise<string> {\n\t\tthis.input.setValue(\"\");\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", prompt), 1, 0));\n\t\tthis.contentContainer.addChild(this.input);\n\t\tthis.contentContainer.addChild(new Text(`(${keyHint(\"tui.select.cancel\", \"to cancel\")})`, 1, 0));\n\t\tthis.tui.requestRender();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.inputResolver = resolve;\n\t\t\tthis.inputRejecter = reject;\n\t\t});\n\t}\n\n\t/**\n\t * Called by onPrompt callback - show prompt and wait for input\n\t * Note: Does NOT clear content, appends to existing (preserves URL from showAuth)\n\t */\n\tshowPrompt(message: string, placeholder?: string): Promise<string> {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"text\", message), 1, 0));\n\t\tif (placeholder) {\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", `e.g., ${placeholder}`), 1, 0));\n\t\t}\n\t\tthis.contentContainer.addChild(this.input);\n\t\tthis.contentContainer.addChild(\n\t\t\tnew Text(\n\t\t\t\t`(${keyHint(\"tui.select.cancel\", \"to cancel,\")} ${keyHint(\"tui.select.confirm\", \"to submit\")})`,\n\t\t\t\t1,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\n\t\tthis.input.setValue(\"\");\n\t\tthis.tui.requestRender();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.inputResolver = resolve;\n\t\t\tthis.inputRejecter = reject;\n\t\t});\n\t}\n\n\t/** Show informational text before another login step. */\n\tshowDetails(lines: string[]): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tfor (const line of lines) {\n\t\t\tthis.contentContainer.addChild(new Text(line, 1, 0));\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\t/** Show provider-owned information and links without starting an auth callback flow. */\n\tshowInfo(message: string, links: readonly AuthInfoLink[] = [], showCloseHint = false): void {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"text\", message), 1, 0));\n\t\tfor (const link of links) {\n\t\t\tconst text = link.label ? `${link.label}: ${link.url}` : link.url;\n\t\t\tconst hyperlink = `\\x1b]8;;${link.url}\\x07${text}\\x1b]8;;\\x07`;\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", hyperlink), 1, 0));\n\t\t}\n\t\tif (showCloseHint) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(new Text(`(${keyHint(\"tui.select.cancel\", \"to close\")})`, 1, 0));\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Show waiting message (for polling flows like GitHub Copilot)\n\t */\n\tshowWaiting(message: string): void {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", message), 1, 0));\n\t\tthis.contentContainer.addChild(new Text(`(${keyHint(\"tui.select.cancel\", \"to cancel\")})`, 1, 0));\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Called by onProgress callback\n\t */\n\tshowProgress(message: string): void {\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", message), 1, 0));\n\t\tthis.tui.requestRender();\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getKeybindings();\n\n\t\tif (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.cancel();\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass to input\n\t\tthis.input.handleInput(data);\n\t}\n}\n"]}
1
+ {"version":3,"file":"login-dialog.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/login-dialog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAA4C,KAAK,GAAG,EAAE,MAAM,wBAAwB,CAAC;AAOvH;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,SAAU,YAAW,SAAS;IACvE,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,aAAa,CAAC,CAA0B;IAChD,OAAO,CAAC,aAAa,CAAC,CAAyB;IAC/C,OAAO,CAAC,UAAU,CAA+C;IAGjE,OAAO,CAAC,QAAQ,CAAS;IACzB,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAGzB;IAED,YACC,GAAG,EAAE,GAAG,EACR,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,EACxD,oBAAoB,CAAC,EAAE,MAAM,EAC7B,aAAa,CAAC,EAAE,MAAM,EAoCtB;IAED,IAAI,MAAM,IAAI,WAAW,CAExB;IAED,OAAO,CAAC,6BAA6B;IAMrC,OAAO,CAAC,MAAM;IAUd;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAiBjD;IAED;;OAEG;IACH,cAAc,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAa9C;IAED;;OAEG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY/C;IAED;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBjE;IAED,yDAAyD;IACzD,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAOjC;IAED,wFAAwF;IACxF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,SAAS,YAAY,EAAO,EAAE,aAAa,UAAQ,GAAG,IAAI,CAa1F;IAED;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKjC;IAED;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGlC;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAU9B;CACD","sourcesContent":["import type { AuthInfoLink, OAuthDeviceCodeInfo } from \"@earendil-works/pi-ai\";\nimport { Container, type Focusable, getKeybindings, type Input, Spacer, Text, type TUI } from \"@earendil-works/pi-tui\";\nimport { openBrowser } from \"../../../utils/open-browser.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { hintRow } from \"./keybinding-hints.ts\";\nimport { PromptInput, withComposerPrompt } from \"./prompt-input.ts\";\n\n/**\n * Login dialog component - replaces editor during OAuth login flow\n */\nexport class LoginDialogComponent extends Container implements Focusable {\n\tprivate contentContainer: Container;\n\tprivate input: Input;\n\tprivate tui: TUI;\n\tprivate abortController = new AbortController();\n\tprivate inputResolver?: (value: string) => void;\n\tprivate inputRejecter?: (error: Error) => void;\n\tprivate onComplete: (success: boolean, message?: string) => void;\n\n\t// Focusable implementation - propagate to input for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.input.focused = value;\n\t}\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tproviderId: string,\n\t\tonComplete: (success: boolean, message?: string) => void,\n\t\tproviderNameOverride?: string,\n\t\ttitleOverride?: string,\n\t) {\n\t\tsuper();\n\t\tthis.tui = tui;\n\t\tthis.onComplete = onComplete;\n\n\t\tconst providerName = providerNameOverride || providerId;\n\t\tconst title = titleOverride ?? `Login to ${providerName}`;\n\n\t\t// Top border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Title\n\t\tthis.addChild(new Text(theme.fg(\"accent\", theme.bold(title)), 0, 0));\n\n\t\t// Dynamic content area\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\t// Input (always present, used when needed)\n\t\tthis.input = new PromptInput();\n\t\tthis.input.onSubmit = () => {\n\t\t\tif (this.inputResolver) {\n\t\t\t\tconst value = this.input.getValue();\n\t\t\t\tthis.replaceInputWithSubmittedText(value);\n\t\t\t\tthis.inputResolver(value);\n\t\t\t\tthis.inputResolver = undefined;\n\t\t\t\tthis.inputRejecter = undefined;\n\t\t\t}\n\t\t};\n\t\tthis.input.onEscape = () => {\n\t\t\tthis.cancel();\n\t\t};\n\n\t\t// Bottom border\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\tget signal(): AbortSignal {\n\t\treturn this.abortController.signal;\n\t}\n\n\tprivate replaceInputWithSubmittedText(value: string): void {\n\t\tthis.contentContainer.children = this.contentContainer.children.map((child) =>\n\t\t\tchild === this.input ? new Text(withComposerPrompt(`> ${value}`), 0, 0) : child,\n\t\t);\n\t}\n\n\tprivate cancel(): void {\n\t\tthis.abortController.abort();\n\t\tif (this.inputRejecter) {\n\t\t\tthis.inputRejecter(new Error(\"Login cancelled\"));\n\t\t\tthis.inputResolver = undefined;\n\t\t\tthis.inputRejecter = undefined;\n\t\t}\n\t\tthis.onComplete(false, \"Login cancelled\");\n\t}\n\n\t/**\n\t * Called by onAuth callback - show URL and optional instructions\n\t */\n\tshowAuth(url: string, instructions?: string): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tconst linkedUrl = `\\x1b]8;;${url}\\x07${url}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", linkedUrl), 0, 0));\n\n\t\tconst clickHint = process.platform === \"darwin\" ? \"Cmd+click to open\" : \"Ctrl+click to open\";\n\t\tconst hyperlink = `\\x1b]8;;${url}\\x07${clickHint}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", hyperlink), 0, 0));\n\n\t\tif (instructions) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"warning\", instructions), 0, 0));\n\t\t}\n\n\t\topenBrowser(url);\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Called by onDeviceCode callback - show URL and user code.\n\t */\n\tshowDeviceCode(info: OAuthDeviceCodeInfo): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tconst linkedUrl = `\\x1b]8;;${info.verificationUri}\\x07${info.verificationUri}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", linkedUrl), 0, 0));\n\n\t\tconst clickHint = process.platform === \"darwin\" ? \"Cmd+click to open\" : \"Ctrl+click to open\";\n\t\tconst hyperlink = `\\x1b]8;;${info.verificationUri}\\x07${clickHint}\\x1b]8;;\\x07`;\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", hyperlink), 0, 0));\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"warning\", `Enter code: ${info.userCode}`), 0, 0));\n\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Show input for manual code/URL entry (for callback server providers)\n\t */\n\tshowManualInput(prompt: string): Promise<string> {\n\t\tthis.input.setValue(\"\");\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", prompt), 0, 0));\n\t\tthis.contentContainer.addChild(this.input);\n\t\tthis.contentContainer.addChild(new Text(hintRow([[\"tui.select.cancel\", \"cancel\"]]), 0, 0));\n\t\tthis.tui.requestRender();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.inputResolver = resolve;\n\t\t\tthis.inputRejecter = reject;\n\t\t});\n\t}\n\n\t/**\n\t * Called by onPrompt callback - show prompt and wait for input\n\t * Note: Does NOT clear content, appends to existing (preserves URL from showAuth)\n\t */\n\tshowPrompt(message: string, placeholder?: string): Promise<string> {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"text\", message), 0, 0));\n\t\tif (placeholder) {\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", `e.g., ${placeholder}`), 0, 0));\n\t\t}\n\t\tthis.contentContainer.addChild(this.input);\n\t\tthis.contentContainer.addChild(\n\t\t\tnew Text(\n\t\t\t\thintRow([\n\t\t\t\t\t[\"tui.select.confirm\", \"submit\"],\n\t\t\t\t\t[\"tui.select.cancel\", \"cancel\"],\n\t\t\t\t]),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\n\t\tthis.input.setValue(\"\");\n\t\tthis.tui.requestRender();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.inputResolver = resolve;\n\t\t\tthis.inputRejecter = reject;\n\t\t});\n\t}\n\n\t/** Show informational text before another login step. */\n\tshowDetails(lines: string[]): void {\n\t\tthis.contentContainer.clear();\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tfor (const line of lines) {\n\t\t\tthis.contentContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\t/** Show provider-owned information and links without starting an auth callback flow. */\n\tshowInfo(message: string, links: readonly AuthInfoLink[] = [], showCloseHint = false): void {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"text\", message), 0, 0));\n\t\tfor (const link of links) {\n\t\t\tconst text = link.label ? `${link.label}: ${link.url}` : link.url;\n\t\t\tconst hyperlink = `\\x1b]8;;${link.url}\\x07${text}\\x1b]8;;\\x07`;\n\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"accent\", hyperlink), 0, 0));\n\t\t}\n\t\tif (showCloseHint) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\tthis.contentContainer.addChild(new Text(hintRow([[\"tui.select.cancel\", \"close\"]]), 0, 0));\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Show waiting message (for polling flows like GitHub Copilot)\n\t */\n\tshowWaiting(message: string): void {\n\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", message), 0, 0));\n\t\tthis.contentContainer.addChild(new Text(hintRow([[\"tui.select.cancel\", \"cancel\"]]), 0, 0));\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Called by onProgress callback\n\t */\n\tshowProgress(message: string): void {\n\t\tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", message), 0, 0));\n\t\tthis.tui.requestRender();\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getKeybindings();\n\n\t\tif (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.cancel();\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass to input\n\t\tthis.input.handleInput(data);\n\t}\n}\n"]}