@ashx-j/lunr 0.2.12 → 0.2.14

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 (225) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +6 -3
  3. package/dist/builtin-extensions/ashxj-tui.d.ts +16 -1
  4. package/dist/builtin-extensions/ashxj-tui.d.ts.map +1 -1
  5. package/dist/builtin-extensions/ashxj-tui.js +44 -20
  6. package/dist/builtin-extensions/ashxj-tui.js.map +1 -1
  7. package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts +2 -0
  8. package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts.map +1 -1
  9. package/dist/builtin-extensions/pi-subagents/src/extension/index.js +27 -22
  10. package/dist/builtin-extensions/pi-subagents/src/extension/index.js.map +1 -1
  11. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.d.ts.map +1 -1
  12. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.js +5 -2
  13. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.js.map +1 -1
  14. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-resume.d.ts.map +1 -1
  15. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-resume.js +12 -1
  16. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-resume.js.map +1 -1
  17. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-status.d.ts +1 -0
  18. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-status.d.ts.map +1 -1
  19. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-status.js +3 -2
  20. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-status.js.map +1 -1
  21. package/dist/builtin-extensions/pi-subagents/src/runs/background/chain-append.d.ts.map +1 -1
  22. package/dist/builtin-extensions/pi-subagents/src/runs/background/chain-append.js +1 -0
  23. package/dist/builtin-extensions/pi-subagents/src/runs/background/chain-append.js.map +1 -1
  24. package/dist/builtin-extensions/pi-subagents/src/runs/background/chain-root-attachment.d.ts.map +1 -1
  25. package/dist/builtin-extensions/pi-subagents/src/runs/background/chain-root-attachment.js +3 -0
  26. package/dist/builtin-extensions/pi-subagents/src/runs/background/chain-root-attachment.js.map +1 -1
  27. package/dist/builtin-extensions/pi-subagents/src/runs/background/fleet-view.d.ts.map +1 -1
  28. package/dist/builtin-extensions/pi-subagents/src/runs/background/fleet-view.js +3 -3
  29. package/dist/builtin-extensions/pi-subagents/src/runs/background/fleet-view.js.map +1 -1
  30. package/dist/builtin-extensions/pi-subagents/src/runs/background/run-status.d.ts.map +1 -1
  31. package/dist/builtin-extensions/pi-subagents/src/runs/background/run-status.js +2 -2
  32. package/dist/builtin-extensions/pi-subagents/src/runs/background/run-status.js.map +1 -1
  33. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.d.ts.map +1 -1
  34. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.js +3 -0
  35. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.js.map +1 -1
  36. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.d.ts.map +1 -1
  37. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.js +7 -1
  38. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.js.map +1 -1
  39. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.d.ts.map +1 -1
  40. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js +2 -0
  41. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js.map +1 -1
  42. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.d.ts +1 -0
  43. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.d.ts.map +1 -1
  44. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js +19 -3
  45. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js.map +1 -1
  46. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.d.ts +10 -1
  47. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.d.ts.map +1 -1
  48. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.js +19 -0
  49. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.js.map +1 -1
  50. package/dist/builtin-extensions/pi-subagents/src/runs/shared/parallel-utils.d.ts +1 -0
  51. package/dist/builtin-extensions/pi-subagents/src/runs/shared/parallel-utils.d.ts.map +1 -1
  52. package/dist/builtin-extensions/pi-subagents/src/runs/shared/parallel-utils.js.map +1 -1
  53. package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.d.ts +2 -1
  54. package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.d.ts.map +1 -1
  55. package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.js +17 -0
  56. package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.js.map +1 -1
  57. package/dist/builtin-extensions/pi-subagents/src/shared/formatters.d.ts +3 -1
  58. package/dist/builtin-extensions/pi-subagents/src/shared/formatters.d.ts.map +1 -1
  59. package/dist/builtin-extensions/pi-subagents/src/shared/formatters.js +14 -3
  60. package/dist/builtin-extensions/pi-subagents/src/shared/formatters.js.map +1 -1
  61. package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts +14 -0
  62. package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts.map +1 -1
  63. package/dist/builtin-extensions/pi-subagents/src/shared/types.js.map +1 -1
  64. package/dist/builtin-extensions/pi-subagents/src/tui/fleet.d.ts.map +1 -1
  65. package/dist/builtin-extensions/pi-subagents/src/tui/fleet.js +2 -2
  66. package/dist/builtin-extensions/pi-subagents/src/tui/fleet.js.map +1 -1
  67. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
  68. package/dist/builtin-extensions/pi-subagents/src/tui/render.js +12 -10
  69. package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
  70. package/dist/catalog/models.json +1 -1
  71. package/dist/catalog/providers/amazon-bedrock.json +1 -1
  72. package/dist/catalog/providers/anthropic.json +1 -1
  73. package/dist/catalog/providers/azure-openai-responses.json +1 -1
  74. package/dist/catalog/providers/cloudflare-ai-gateway.json +1 -1
  75. package/dist/catalog/providers/cloudflare-workers-ai.json +1 -1
  76. package/dist/catalog/providers/fireworks.json +1 -1
  77. package/dist/catalog/providers/github-copilot.json +1 -1
  78. package/dist/catalog/providers/google-vertex.json +1 -1
  79. package/dist/catalog/providers/google.json +1 -1
  80. package/dist/catalog/providers/groq.json +1 -1
  81. package/dist/catalog/providers/huggingface.json +1 -1
  82. package/dist/catalog/providers/nvidia.json +1 -1
  83. package/dist/catalog/providers/openai-codex.json +1 -1
  84. package/dist/catalog/providers/openai.json +1 -1
  85. package/dist/catalog/providers/opencode-go.json +1 -1
  86. package/dist/catalog/providers/opencode.json +1 -1
  87. package/dist/catalog/providers/openrouter.json +1 -1
  88. package/dist/catalog/providers/together.json +1 -1
  89. package/dist/catalog/providers/vercel-ai-gateway.json +1 -1
  90. package/dist/catalog/publication.json +131 -3
  91. package/dist/cli/list-models.d.ts.map +1 -1
  92. package/dist/cli/list-models.js +1 -0
  93. package/dist/cli/list-models.js.map +1 -1
  94. package/dist/cli.d.ts.map +1 -1
  95. package/dist/cli.js +52 -27
  96. package/dist/cli.js.map +1 -1
  97. package/dist/core/catalog-cache-scope.d.ts +3 -0
  98. package/dist/core/catalog-cache-scope.d.ts.map +1 -0
  99. package/dist/core/catalog-cache-scope.js +12 -0
  100. package/dist/core/catalog-cache-scope.js.map +1 -0
  101. package/dist/core/catalog-merge.d.ts +2 -0
  102. package/dist/core/catalog-merge.d.ts.map +1 -1
  103. package/dist/core/catalog-merge.js +29 -2
  104. package/dist/core/catalog-merge.js.map +1 -1
  105. package/dist/core/catalog-refresh-polling.d.ts +8 -0
  106. package/dist/core/catalog-refresh-polling.d.ts.map +1 -0
  107. package/dist/core/catalog-refresh-polling.js +30 -0
  108. package/dist/core/catalog-refresh-polling.js.map +1 -0
  109. package/dist/core/codex-catalog.d.ts +3 -0
  110. package/dist/core/codex-catalog.d.ts.map +1 -0
  111. package/dist/core/codex-catalog.js +15 -0
  112. package/dist/core/codex-catalog.js.map +1 -0
  113. package/dist/core/context-breakdown.d.ts +3 -1
  114. package/dist/core/context-breakdown.d.ts.map +1 -1
  115. package/dist/core/context-breakdown.js +5 -2
  116. package/dist/core/context-breakdown.js.map +1 -1
  117. package/dist/core/customize.d.ts +2 -4
  118. package/dist/core/customize.d.ts.map +1 -1
  119. package/dist/core/customize.js +2 -8
  120. package/dist/core/customize.js.map +1 -1
  121. package/dist/core/footer-data-provider.d.ts +3 -4
  122. package/dist/core/footer-data-provider.d.ts.map +1 -1
  123. package/dist/core/footer-data-provider.js +54 -82
  124. package/dist/core/footer-data-provider.js.map +1 -1
  125. package/dist/core/live-catalog.d.ts +5 -2
  126. package/dist/core/live-catalog.d.ts.map +1 -1
  127. package/dist/core/live-catalog.js +108 -27
  128. package/dist/core/live-catalog.js.map +1 -1
  129. package/dist/core/model-runtime.d.ts +8 -0
  130. package/dist/core/model-runtime.d.ts.map +1 -1
  131. package/dist/core/model-runtime.js +64 -12
  132. package/dist/core/model-runtime.js.map +1 -1
  133. package/dist/core/official-catalog.d.ts +4 -0
  134. package/dist/core/official-catalog.d.ts.map +1 -1
  135. package/dist/core/official-catalog.js +86 -21
  136. package/dist/core/official-catalog.js.map +1 -1
  137. package/dist/core/settings-manager.d.ts +7 -6
  138. package/dist/core/settings-manager.d.ts.map +1 -1
  139. package/dist/core/settings-manager.js +13 -17
  140. package/dist/core/settings-manager.js.map +1 -1
  141. package/dist/core/slash-commands.d.ts.map +1 -1
  142. package/dist/core/slash-commands.js +1 -0
  143. package/dist/core/slash-commands.js.map +1 -1
  144. package/dist/index.d.ts +1 -1
  145. package/dist/index.d.ts.map +1 -1
  146. package/dist/index.js.map +1 -1
  147. package/dist/main.d.ts +2 -0
  148. package/dist/main.d.ts.map +1 -1
  149. package/dist/main.js +106 -50
  150. package/dist/main.js.map +1 -1
  151. package/dist/migrations.d.ts +2 -0
  152. package/dist/migrations.d.ts.map +1 -1
  153. package/dist/migrations.js +14 -4
  154. package/dist/migrations.js.map +1 -1
  155. package/dist/modes/interactive/components/assistant-message.d.ts +1 -3
  156. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  157. package/dist/modes/interactive/components/assistant-message.js +4 -32
  158. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  159. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  160. package/dist/modes/interactive/components/custom-editor.js +2 -1
  161. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  162. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  163. package/dist/modes/interactive/components/model-selector.js +4 -1
  164. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  165. package/dist/modes/interactive/components/plan-message.d.ts +1 -3
  166. package/dist/modes/interactive/components/plan-message.d.ts.map +1 -1
  167. package/dist/modes/interactive/components/plan-message.js +1 -16
  168. package/dist/modes/interactive/components/plan-message.js.map +1 -1
  169. package/dist/modes/interactive/components/settings-selector.d.ts +3 -5
  170. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  171. package/dist/modes/interactive/components/settings-selector.js +113 -117
  172. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  173. package/dist/modes/interactive/components/user-message.d.ts +1 -3
  174. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  175. package/dist/modes/interactive/components/user-message.js +2 -19
  176. package/dist/modes/interactive/components/user-message.js.map +1 -1
  177. package/dist/modes/interactive/interactive-mode.d.ts +12 -1
  178. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  179. package/dist/modes/interactive/interactive-mode.js +153 -130
  180. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  181. package/dist/modes/interactive/skill-tag-autocomplete.d.ts +13 -0
  182. package/dist/modes/interactive/skill-tag-autocomplete.d.ts.map +1 -0
  183. package/dist/modes/interactive/skill-tag-autocomplete.js +72 -0
  184. package/dist/modes/interactive/skill-tag-autocomplete.js.map +1 -0
  185. package/dist/modes/interactive/theme/moon.json +0 -1
  186. package/dist/modes/interactive/theme/theme-schema.json +3 -3
  187. package/dist/modes/interactive/theme/theme.d.ts +1 -1
  188. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  189. package/dist/modes/interactive/theme/theme.js +95 -90
  190. package/dist/modes/interactive/theme/theme.js.map +1 -1
  191. package/dist/startup/interactive-view.d.ts +55 -0
  192. package/dist/startup/interactive-view.d.ts.map +1 -0
  193. package/dist/startup/interactive-view.js +332 -0
  194. package/dist/startup/interactive-view.js.map +1 -0
  195. package/dist/startup/launch-routing.d.ts +8 -0
  196. package/dist/startup/launch-routing.d.ts.map +1 -0
  197. package/dist/startup/launch-routing.js +29 -0
  198. package/dist/startup/launch-routing.js.map +1 -0
  199. package/dist/startup/startup-milestones.d.ts +4 -0
  200. package/dist/startup/startup-milestones.d.ts.map +1 -0
  201. package/dist/startup/startup-milestones.js +14 -0
  202. package/dist/startup/startup-milestones.js.map +1 -0
  203. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  204. package/dist/utils/syntax-highlight.js +10 -4
  205. package/dist/utils/syntax-highlight.js.map +1 -1
  206. package/docs/features.md +2 -2
  207. package/docs/interactive-startup.md +58 -0
  208. package/docs/keybindings.md +1 -1
  209. package/docs/model-catalog.md +29 -0
  210. package/docs/quickstart.md +1 -1
  211. package/docs/settings.md +2 -3
  212. package/docs/skills.md +10 -0
  213. package/docs/terminal-setup.md +2 -2
  214. package/docs/usage.md +2 -1
  215. package/docs/windows.md +1 -1
  216. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  217. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  218. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  219. package/examples/extensions/gondolin/package-lock.json +2 -2
  220. package/examples/extensions/gondolin/package.json +1 -1
  221. package/examples/extensions/sandbox/package-lock.json +2 -2
  222. package/examples/extensions/sandbox/package.json +1 -1
  223. package/examples/extensions/with-deps/package-lock.json +2 -2
  224. package/examples/extensions/with-deps/package.json +1 -1
  225. package/package.json +16 -16
package/CHANGELOG.md CHANGED
@@ -1,18 +1,33 @@
1
1
  # Changelog
2
2
 
3
- lunR is derived from pi. The published npm package is **`@ashx-j/lunr@0.2.12`**. Versioned sections below (`0.80.x` and earlier) are **upstream pi history** and do not describe lunR releases.
3
+ lunR is derived from pi. The published npm package is **`@ashx-j/lunr@0.2.14`**. Versioned sections below (`0.80.x` and earlier) are **upstream pi history** and do not describe lunR releases.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.14] - 2026-09-05
8
+
7
9
  ### Documentation
8
10
 
9
11
  - User-facing shipped docs now describe lunR: `@ashx-j/lunr`, binary `lunr`, config `~/.lunr/agent` and project `.lunr/`, `/refresh` for catalogs, and baked-in features (MCP, subagents, permissions, plan, todos, cron, gateway).
10
12
 
11
13
  ### New Features
12
14
 
15
+ - **Autonomous model catalog** — Codex uses public metadata plus authenticated, version-gated discovery, with idle refresh and checksummed hourly snapshots that do not require an agent turn.
16
+ - **Mid-message skill tags** — `/settings` Skill tag (`+`, `~`, or `$`) lists loaded skills after a space so you can insert `{char}{name}` without expanding SKILL.md. `/skill:name` remains the force-load path.
13
17
  - **Agent-managed factual memory** — Added an Agent memory setting beside the character cap. Turning it off removes memory injection and tools without deleting stored facts. Global behavior now comes only from a user-created `~/.lunr/agent/AGENTS.md`; behavior presets and `behavior.md` injection were removed.
14
18
  - **Qwen Token Plan** — Added built-in providers for QwenCloud's Credits-based Token Plan (`qwen-token-plan` international and `qwen-token-plan-cn` China), using the DashScope OpenAI-compatible endpoint with `DASHSCOPE_TOKEN_PLAN_API_KEY` / `DASHSCOPE_TOKEN_PLAN_CN_API_KEY`. Includes 15 text models and defaults to the token-plan-only `qwen3.8-max-preview`.
15
19
 
20
+ ### Changed
21
+
22
+ - **Real TUI first paint** — The normal moon chatbox, header, and stats render before runtime hydration while retaining the same terminal, editor, draft, cursor, and image chips through feature attachment.
23
+ - **Offline release validation** — Routine builds no longer regenerate the live model catalog; published dependencies and standalone installer locks preserve exact reviewed resolutions.
24
+ - **Settings menu copy** — `/settings` rows use short feature summaries instead of implementation inventories; choice-specific details stay beside the relevant option.
25
+ - **Global AGENTS.md label** — `/context` and `/usage` label `~/.lunr/agent/AGENTS.md` as `Global AGENTS.md`; project instruction files stay `AGENTS.md`.
26
+
27
+ ### Fixed
28
+
29
+ - **VS Code image paste** — Image paste flows through the normal app action, including custom editors, and `/paste-image` works without a terminal shortcut when VS Code owns Alt+V.
30
+
16
31
  ## [0.80.10] - 2026-07-16
17
32
 
18
33
  ### New Features
package/README.md CHANGED
@@ -99,7 +99,9 @@ lunR ships a built-in catalog. First paint is **cache-only** (no GitHub or live
99
99
 
100
100
  `/model` lists **stored-credential** providers only (`~/.lunr/agent/auth.json`, subscriptions, or `--api-key`). An ambient `OPENROUTER_API_KEY` does not make OpenRouter models appear in `/model`.
101
101
 
102
- Refresh catalogs with **`/refresh`**, not `lunr update`. `/refresh` downloads `providers.json` and then shards for providers that already have stored credentials. `lunr update` only reinstalls the global CLI.
102
+ Model catalogs refresh automatically after startup and periodically while lunR is idle. Startup itself uses cached or bundled data. **`/refresh`** forces an immediate check for providers with stored credentials; `lunr update` only reinstalls the global CLI.
103
+
104
+ For ChatGPT/Codex, lunR discovers the models available to your account directly, including new model IDs. Provider-supplied context and reasoning capabilities take priority over older published metadata; explicit `models.json` overrides remain yours. Failed requests keep the last successful catalog for the same account. Models with missing pricing are marked as such in the model picker; new IDs do not trigger metadata questions.
103
105
 
104
106
  **Subscriptions:**
105
107
  - Anthropic Claude Pro/Max
@@ -129,7 +131,7 @@ The interface from top to bottom:
129
131
  - **Startup header** - Shows shortcuts (`/hotkeys` for all), loaded AGENTS.md files, prompt templates, skills, and extensions
130
132
  - **Messages** - Your messages, assistant responses, tool calls and results, notifications, errors, and extension UI
131
133
  - **Editor** - Where you type; border color indicates thinking level
132
- - **Footer** - Working directory, git branch, session name, token/cache usage, cost, context usage, plan bar, current model. Customize under `/settings` → Customize → Footer.
134
+ - **Footer** - Working directory, git branch, session name, token/cache usage, cost, context usage, plan bar, current model. Customize its segments under `/settings` → Customize.
133
135
 
134
136
  The editor can be temporarily replaced by other UI, like built-in `/settings` or custom UI from extensions. [Extensions](#extensions) can also replace the editor, add widgets above/below it, a status line, custom footer, or overlays.
135
137
 
@@ -141,7 +143,7 @@ The editor can be temporarily replaced by other UI, like built-in `/settings` or
141
143
  | Path completion | Tab to complete paths |
142
144
  | Multi-line | Shift+Enter (or Ctrl+Enter on Windows Terminal) |
143
145
  | External editor | Ctrl+G opens `externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere |
144
- | Clipboard | Ctrl+V to paste text or an image (Alt+V on Windows). Images insert `[image_n]` chips, not a temp path. Drag images onto the terminal also works. |
146
+ | Clipboard | Ctrl+V normally, Alt+V on Windows. VS Code must forward Alt+V, or use `/paste-image` without a shortcut. Images insert `[image_n]` chips, not a temp path. |
145
147
  | Bash commands | `!command` runs and sends output to LLM, `!!command` runs without sending |
146
148
 
147
149
  Standard editing keybindings for delete word, undo, etc. See [docs/keybindings.md](docs/keybindings.md).
@@ -181,6 +183,7 @@ Type `/` in the editor to trigger commands. [Extensions](#extensions) can regist
181
183
  | `/clone` | Duplicate the current active branch |
182
184
  | `/compact [prompt]` | Manually compact context |
183
185
  | `/copy` | Copy last assistant message |
186
+ | `/paste-image` | Paste an image from the clipboard |
184
187
  | `/export [file]` | Export session to HTML or JSONL |
185
188
  | `/import <file>` | Import and resume a JSONL session |
186
189
  | `/share` | Upload as a private GitHub gist (HTML viewer still defaults to https://pi.dev/session/ — leftover, not lunR-hosted) |
@@ -30,6 +30,8 @@
30
30
  * re-exports main.ts which imports this file — a circular import that crashes
31
31
  * under vite-node (`Class extends value undefined`).
32
32
  */
33
+ import { CustomEditor } from "../modes/interactive/components/custom-editor.js";
34
+ export declare function lunrPromptGlyph(theme: Theme | undefined): string;
33
35
  type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
34
36
  /** Theme token for the chatbox thinking-level border / effort chip. */
35
37
  export declare function thinkingThemeToken(level: ThinkingLevel): string;
@@ -39,7 +41,7 @@ export declare function formatChipEffort(level: ThinkingLevel): string;
39
41
  interface Theme {
40
42
  fg(token: string, text: string): string;
41
43
  /** lunr: prompt glyph tokens (optional for forward/backward compat). */
42
- glyph?(name: "promptMoon" | "promptArrow"): string;
44
+ glyph?(name: "promptArrow"): string;
43
45
  }
44
46
  /** Trimmed view of `pi-tui`'s `TUI` — only the method we use. */
45
47
  interface TUIHandle {
@@ -142,7 +144,20 @@ interface FooterComponentLike {
142
144
  dispose?(): void;
143
145
  invalidate?(): void;
144
146
  }
147
+ export declare class ChatboxEditor extends CustomEditor {
148
+ private ctx;
149
+ private pi;
150
+ bind(ctx: ExtensionContextLike, pi: ExtensionAPI): void;
151
+ /** Keep the displayed values while /new or /resume replaces the old runtime. */
152
+ unbind(): void;
153
+ constructor(tui: TUIHandle, theme: EditorThemeLike, keybindings: KeybindingsLike, ctx: ExtensionContextLike, pi: ExtensionAPI);
154
+ private color;
155
+ render(width: number): string[];
156
+ private renderBottomBorder;
157
+ }
145
158
  export declare function renderStatsLine(width: number, ctx: ExtensionContextLike, theme: Theme, footerData: ReadonlyFooterDataProvider): string[];
159
+ /** The startup view and extension bind the same editor, preserving its complete draft state. */
160
+ export declare function getChatboxEditor(tui: TUIHandle, theme: EditorThemeLike, keybindings: KeybindingsLike, ctx: ExtensionContextLike, pi: ExtensionAPI): ChatboxEditor;
146
161
  export default function (pi: ExtensionAPI): void;
147
162
  export {};
148
163
  //# sourceMappingURL=ashxj-tui.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ashxj-tui.d.ts","sourceRoot":"","sources":["../../src/builtin-extensions/ashxj-tui.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ashxj-tui — a custom slim chatbox for pi.\n *\n * Replaces pi-zentui's prompt box + statusline with:\n * - A rounded prompt box that auto-expands as the typed message wraps to\n * multiple rows (the sides extend; height grows with content).\n * - A right-aligned chip on the box's BOTTOM border:\n * model · provider · effort (e.g. `glm-5.2 · Ollama Cloud · xhigh`)\n * - A slim stats line (extension statuses · context% · ↑↓; each segment\n * toggle-gated via the lunr customize bridge) BELOW the box, not wrapped\n * around the input.\n * - No session-mode indicator anywhere (pi has no such concept).\n *\n * Loaded by pi via jiti — no build step, plain TypeScript.\n *\n * NOTE on type imports (see `ashxj-thinking`/`simple-memory` for the convention):\n * This file declares structural types inline rather than importing them from\n * `@ashx-j/lunr` / `@ashx-j/lunr-tui`.\n * pi-coding-agent's `index.d.ts` re-exports from internal `.d.ts` files that\n * contain subpath imports like `@ashx-j/lunr-ai/compat`, which do not\n * resolve under `tsc` with a file argument. We keep the typecheck\n * self-contained with inline types and verify via the included `tsconfig.json`\n * (`moduleResolution: \"Bundler\"` + `skipLibCheck: true`).\n *\n * The ONE exception is `CustomEditor`, imported as a RUNTIME VALUE directly\n * from its defining module (`modes/interactive/components/custom-editor.ts`).\n * We must `extends` it to inherit pi's app keybindings (escape, ctrl+d, model\n * switching, extension shortcuts) — wrapping would lose those. It is imported\n * from the concrete module, NOT the package barrel, because the barrel\n * re-exports main.ts which imports this file — a circular import that crashes\n * under vite-node (`Class extends value undefined`).\n */\n\nimport { CustomEditor } from \"../modes/interactive/components/custom-editor.js\";\n\n// lunr: customize bridge — read the prompt-symbol toggle (bridgeless → no symbol).\nconst PROMPT_SYMBOL_BRIDGE = Symbol.for(\"@lunr/customize\");\nconst PERMISSION_MODE_BRIDGE = Symbol.for(\"@lunr/permission-mode\");\ninterface CustomizeBridgeForPrompt {\n\tgetPromptSymbol(): boolean;\n\tgetOpenAIFastMode?(): boolean;\n\t// lunr: footer element toggles (added to the same bridge).\n\tgetFooterMcp?(): boolean;\n\tgetFooterLsp?(): boolean;\n\tgetFooterContext?(): boolean;\n\tgetFooterTokens?(): boolean;\n\tgetFooterCacheHitRate?(): boolean;\n\tgetFooterTps?(): boolean;\n\tgetFooterStatuses?(): boolean;\n\tgetFooterGit?(): boolean;\n\tgetFooterPlan?(): boolean;\n\tgetFooterPlanBar?(): boolean;\n}\ninterface PermissionModeBridgeForFooter {\n\tgetMode(): string | undefined;\n}\nfunction lunrCustomizeBridge(): CustomizeBridgeForPrompt | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[PROMPT_SYMBOL_BRIDGE] as CustomizeBridgeForPrompt | undefined;\n}\nfunction lunrPermissionModeBridge(): PermissionModeBridgeForFooter | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[PERMISSION_MODE_BRIDGE] as PermissionModeBridgeForFooter | undefined;\n}\nfunction lunrPromptSymbolEnabled(): boolean {\n\treturn lunrCustomizeBridge()?.getPromptSymbol() ?? false;\n}\n// lunr: footer toggles, read at render time. Bridgeless / missing-getter fallback\n// matches the settings-manager defaults: MCP on, LSP off, everything else on.\nfunction lunrFooterToggles(): {\n\tmcp: boolean;\n\tlsp: boolean;\n\tcontext: boolean;\n\ttokens: boolean;\n\tcacheHitRate: boolean;\n\ttps: boolean;\n\tstatuses: boolean;\n\tgit: boolean;\n\tplan: boolean;\n\tplanBar: boolean;\n} {\n\tconst bridge = lunrCustomizeBridge();\n\treturn {\n\t\tmcp: bridge?.getFooterMcp?.() ?? true,\n\t\tlsp: bridge?.getFooterLsp?.() ?? false,\n\t\tcontext: bridge?.getFooterContext?.() ?? true,\n\t\ttokens: bridge?.getFooterTokens?.() ?? true,\n\t\tcacheHitRate: bridge?.getFooterCacheHitRate?.() ?? true,\n\t\ttps: bridge?.getFooterTps?.() ?? true,\n\t\tstatuses: bridge?.getFooterStatuses?.() ?? true,\n\t\tgit: bridge?.getFooterGit?.() ?? true,\n\t\tplan: bridge?.getFooterPlan?.() ?? true,\n\t\tplanBar: bridge?.getFooterPlanBar?.() ?? true,\n\t};\n}\n\nconst USAGE_SERVICE_BRIDGE = Symbol.for(\"@lunr/usage-service\");\ninterface UsageServiceBridgeForFooter {\n\tpickForFooter?(providerId: string): { label: string; usedPercent: number } | undefined;\n\tprefetch?(providerId: string): void;\n}\nfunction lunrUsageBridge(): UsageServiceBridgeForFooter | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[USAGE_SERVICE_BRIDGE] as UsageServiceBridgeForFooter | undefined;\n}\n\nfunction compactPlanBar(percent: number, theme: Theme): string {\n\tconst cells = 8;\n\tconst clamped = Math.max(0, Math.min(100, percent));\n\tconst filled = Math.round((clamped / 100) * cells);\n\tconst token = clamped > 90 ? \"error\" : clamped > 70 ? \"warning\" : \"success\";\n\tconst fill = filled > 0 ? color(theme, token, \"█\".repeat(filled)) : \"\";\n\tconst rest = filled < cells ? color(theme, \"dim\", \"░\".repeat(cells - filled)) : \"\";\n\treturn fill + rest;\n}\n// lunr: permission mode for the footer safety indicator (always shown).\nfunction lunrPermissionMode(): string | undefined {\n\treturn lunrPermissionModeBridge()?.getMode();\n}\n// lunr: prompt glyph comes from the theme tokens `glyphs.promptMoon` /\n// `glyphs.promptArrow` (defaults below match moon.json). An empty token hides\n// that part; both empty = no glyph at all.\nfunction lunrPromptGlyph(theme: Theme | undefined): string {\n\tconst moon = theme?.glyph?.(\"promptMoon\") ?? \"☾\";\n\tconst arrow = theme?.glyph?.(\"promptArrow\") ?? \">\";\n\tconst parts = [moon, arrow].filter((s) => s !== \"\");\n\treturn parts.length > 0 ? `${parts.join(\" \")} ` : \"\";\n}\n\n// ---------------------------------------------------------------------------\n// Minimal structural types (inline — see file header)\n// ---------------------------------------------------------------------------\n\ntype ThinkingLevel = \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n\n/** Theme token for the chatbox thinking-level border / effort chip. */\nexport function thinkingThemeToken(level: ThinkingLevel): string {\n\tswitch (level) {\n\t\tcase \"off\":\n\t\t\treturn \"thinkingOff\";\n\t\tcase \"minimal\":\n\t\t\treturn \"thinkingMinimal\";\n\t\tcase \"low\":\n\t\t\treturn \"thinkingLow\";\n\t\tcase \"medium\":\n\t\t\treturn \"thinkingMedium\";\n\t\tcase \"high\":\n\t\t\treturn \"thinkingHigh\";\n\t\tcase \"xhigh\":\n\t\t\treturn \"thinkingXhigh\";\n\t\tcase \"max\":\n\t\t\treturn \"thinkingMax\";\n\t\tdefault:\n\t\t\treturn \"thinkingOff\";\n\t}\n}\n\n/** Chip effort label — identity, locked so xhigh/max are never mapped down to high. */\nexport function formatChipEffort(level: ThinkingLevel): string {\n\treturn level;\n}\n\n/** Trimmed view of pi's `Theme` — only the methods we use. */\ninterface Theme {\n\tfg(token: string, text: string): string;\n\t/** lunr: prompt glyph tokens (optional for forward/backward compat). */\n\tglyph?(name: \"promptMoon\" | \"promptArrow\"): string;\n}\n\n/** Trimmed view of `pi-tui`'s `TUI` — only the method we use. */\ninterface TUIHandle {\n\trequestRender(force?: boolean): void;\n}\n\n/** Trimmed view of `pi-tui`'s `EditorTheme`. */\ninterface EditorThemeLike {\n\tborderColor: (str: string) => string;\n\tselectList: unknown;\n}\n\n/** Opaque keybindings manager. */\ninterface KeybindingsLike {\n\treadonly _?: unknown;\n}\n\n/** Trimmed view of an autocomplete list (the editor's private `autocompleteList`). */\ninterface AutocompleteListLike {\n\trender(width: number): string[];\n}\n\n/** Trimmed view of pi's `ReadonlyFooterDataProvider`. */\ninterface ReadonlyFooterDataProvider {\n\tgetGitBranch(): string | null;\n\tgetGitDiffstat?(): { added: number; removed: number } | null;\n\tgetExtensionStatuses(): ReadonlyMap<string, string>;\n\tgetAvailableProviderCount(): number;\n\tonBranchChange(callback: () => void): () => void;\n}\n\n/** Trimmed view of the `getContextUsage` return value. */\ninterface ContextUsage {\n\ttokens: number | null;\n\tcontextWindow: number;\n\tpercent: number | null;\n}\n\n/** Trimmed view of an assistant message's usage — the bits we render. */\ninterface AssistantUsage {\n\tinput: number;\n\toutput: number;\n\tcacheRead?: number;\n\tcacheWrite?: number;\n\tcost: { total: number };\n}\n\n/** Trimmed view of an assistant message. */\ninterface AssistantMessageLike {\n\trole: \"assistant\";\n\tusage: AssistantUsage;\n}\n\n/** Trimmed view of a session entry — only what we iterate over. */\ntype SessionEntry =\n\t| { type: \"message\"; message: AssistantMessageLike }\n\t| { type: string; message?: unknown };\n\n/** Trimmed view of a `ReadonlySessionManager`. */\ninterface ReadonlySessionManagerLike {\n\tgetEntries(): readonly SessionEntry[];\n\tgetBranch?(): readonly SessionEntry[];\n}\n\n/** Trimmed view of a `Model`. */\ninterface ModelLike {\n\tid?: string;\n\tprovider?: string;\n\tcontextWindow?: number;\n\treasoning?: boolean;\n\tthinkingLevelMap?: Record<string, string | null>;\n}\n\ninterface ExtensionUIContextLike {\n\tsetEditorComponent(factory: EditorFactoryLike | undefined): void;\n\tsetFooter(factory: FooterFactoryLike | undefined): void;\n\treadonly theme?: Theme;\n}\n\n/** Trimmed view of `ExtensionContext`. */\ninterface ExtensionContextLike {\n\tmode: \"tui\" | \"rpc\" | \"json\" | \"print\";\n\thasUI: boolean;\n\tmodel: ModelLike | undefined;\n\tsessionManager: ReadonlySessionManagerLike;\n\tgetContextUsage(): ContextUsage | undefined;\n\tui: ExtensionUIContextLike;\n}\n\n/** Trimmed view of `ExtensionAPI`. */\ninterface ExtensionAPI {\n\tgetThinkingLevel(): ThinkingLevel;\n\ton(event: string, handler: (event: unknown, ctx: ExtensionContextLike) => void): void;\n}\n\n/** `setEditorComponent` factory shape. */\ntype EditorFactoryLike = (\n\ttui: TUIHandle,\n\ttheme: EditorThemeLike,\n\tkeybindings: KeybindingsLike,\n) => EditorComponentLike;\n\ninterface EditorComponentLike {\n\trender(width: number): string[];\n\tgetText(): string;\n\tsetText(text: string): void;\n\tinvalidate(): void;\n\thandleInput?(data: string): void;\n}\n\n/** `setFooter` factory shape. */\ntype FooterFactoryLike = (\n\ttui: TUIHandle,\n\ttheme: Theme,\n\tfooterData: ReadonlyFooterDataProvider,\n) => FooterComponentLike;\n\ninterface FooterComponentLike {\n\trender(width: number): string[];\n\tdispose?(): void;\n\tinvalidate?(): void;\n}\n\n// ---------------------------------------------------------------------------\n// Display-width helpers (ANSI-aware; CJK-aware)\n//\n// The editor (`Editor.render`) word-wraps to the given width using grapheme\n// width, so each returned line is ≤ that many visual columns. To right-pad a\n// line for our box we need a width model that matches closely enough: skip ALL\n// escape sequences (SGR color, OSC/DCS/APC/PM/SOS string sequences such as\n// pi-tui's `CURSOR_MARKER` `ESC _pi:c BEL`, and two-char escapes), count common\n// East-Asian wide ranges as 2, combining marks and variation selectors as 0.\n// Handling the string sequences as 0-width is what keeps the right `│` rail\n// aligned on the focused (cursor) line — the base editor embeds the marker and\n// the TUI strips it at flush time, so we must not count it here.\n// ---------------------------------------------------------------------------\n\nfunction skipAnsi(str: string, pos: number): number {\n\tif (str.charCodeAt(pos) !== 0x1b) return 0;\n\tconst next = str.charCodeAt(pos + 1);\n\t// CSI: ESC [ ... <final 0x40-0x7e>\n\tif (next === 0x5b /* [ */) {\n\t\tlet j = pos + 2;\n\t\twhile (j < str.length) {\n\t\t\tconst c = str.charCodeAt(j);\n\t\t\tif (c >= 0x40 && c <= 0x7e) return j + 1 - pos;\n\t\t\tj++;\n\t\t}\n\t\treturn str.length - pos; // unterminated; consume the rest\n\t}\n\t// String sequences — OSC (]), DCS (P), APC (_), PM (^), SOS (X) — terminated\n\t// by BEL (0x07) or ST (ESC \\). pi-tui's CURSOR_MARKER `ESC _pi:c BEL` is APC;\n\t// OSC color queries are OSC. All are zero visual width.\n\tif (\n\t\tnext === 0x5d /* ] */ ||\n\t\tnext === 0x50 /* P */ ||\n\t\tnext === 0x5f /* _ */ ||\n\t\tnext === 0x5e /* ^ */ ||\n\t\tnext === 0x58 /* X */\n\t) {\n\t\tlet j = pos + 2;\n\t\twhile (j < str.length) {\n\t\t\tconst c = str.charCodeAt(j);\n\t\t\tif (c === 0x07 /* BEL */) return j + 1 - pos;\n\t\t\tif (c === 0x1b && str.charCodeAt(j + 1) === 0x5c /* \\ */) return j + 2 - pos;\n\t\t\tj++;\n\t\t}\n\t\treturn str.length - pos; // unterminated; consume the rest\n\t}\n\t// Any other ESC sequence: consume ESC + one final byte (e.g. ESC c, ESC \\,\n\t// ESC ( B). A lone trailing ESC with no following byte is 0 width too.\n\treturn Number.isNaN(next) ? 1 : 2;\n}\n\nfunction isWide(cp: number): boolean {\n\treturn (\n\t\t(cp >= 0x1100 && cp <= 0x115f) ||\n\t\t(cp >= 0x2e80 && cp <= 0x303e) ||\n\t\t(cp >= 0x3041 && cp <= 0x33ff) ||\n\t\t(cp >= 0x3400 && cp <= 0x4dbf) ||\n\t\t(cp >= 0x4e00 && cp <= 0x9fff) ||\n\t\t(cp >= 0xa000 && cp <= 0xa4cf) ||\n\t\t(cp >= 0xac00 && cp <= 0xd7a3) ||\n\t\t(cp >= 0xf900 && cp <= 0xfaff) ||\n\t\t(cp >= 0xfe30 && cp <= 0xfe4f) ||\n\t\t(cp >= 0xff00 && cp <= 0xff60) ||\n\t\t(cp >= 0xffe0 && cp <= 0xffe6) ||\n\t\t(cp >= 0x1f300 && cp <= 0x1faff) ||\n\t\t(cp >= 0x20000 && cp <= 0x3fffd)\n\t);\n}\n\nfunction charWidth(cp: number): number {\n\tif (cp >= 0x20 && cp < 0x7f) return 1;\n\tif ((cp >= 0x0300 && cp <= 0x036f) || (cp >= 0xfe00 && cp <= 0xfe0f) || cp === 0x200d) return 0;\n\treturn isWide(cp) ? 2 : 1;\n}\n\n/** Visible width of a string, ignoring SGR color codes. */\nfunction displayWidth(str: string): number {\n\tlet w = 0;\n\tlet i = 0;\n\twhile (i < str.length) {\n\t\tconst skip = skipAnsi(str, i);\n\t\tif (skip > 0) {\n\t\t\ti += skip;\n\t\t\tcontinue;\n\t\t}\n\t\tconst cp = str.codePointAt(i) ?? 0;\n\t\tw += charWidth(cp);\n\t\ti += cp > 0xffff ? 2 : 1;\n\t}\n\treturn w;\n}\n\n/** Right-pad a (possibly colored) line to `width` visible columns. */\nfunction padRight(line: string, width: number): string {\n\tconst w = displayWidth(line);\n\tif (w >= width) return line;\n\treturn line + \" \".repeat(width - w);\n}\n\n/** Truncate a plain (non-ANSI) string to `maxWidth` visible columns. */\nfunction truncatePlain(text: string, maxWidth: number): string {\n\tif (maxWidth <= 0) return \"\";\n\tif (displayWidth(text) <= maxWidth) return text;\n\tlet out = \"\";\n\tlet w = 0;\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst code = text.codePointAt(i) ?? 0;\n\t\tconst cw = charWidth(code);\n\t\tif (w + cw > maxWidth) break;\n\t\tout += String.fromCodePoint(code);\n\t\tw += cw;\n\t\ti += code > 0xffff ? 2 : 1;\n\t}\n\treturn out;\n}\n\n/** Truncate a (possibly ANSI-colored) string to `maxWidth` visible columns,\n * re-emitting an SGR reset at the cut so styling doesn't bleed. */\nfunction truncateToWidth(text: string, maxWidth: number, ellipsis = \"\"): string {\n\tif (maxWidth <= 0) return \"\";\n\tif (displayWidth(text) <= maxWidth) return text;\n\tconst target = Math.max(0, maxWidth - displayWidth(ellipsis));\n\tlet out = \"\";\n\tlet w = 0;\n\tlet i = 0;\n\twhile (i < text.length && w < target) {\n\t\tconst skip = skipAnsi(text, i);\n\t\tif (skip > 0) {\n\t\t\tout += text.slice(i, i + skip);\n\t\t\ti += skip;\n\t\t\tcontinue;\n\t\t}\n\t\tconst code = text.codePointAt(i) ?? 0;\n\t\tconst cw = charWidth(code);\n\t\tif (w + cw > target) break;\n\t\tout += String.fromCodePoint(code);\n\t\tw += cw;\n\t\ti += code > 0xffff ? 2 : 1;\n\t}\n\tout += \"\\x1b[0m\";\n\treturn out + ellipsis;\n}\n\n/** Clamp each rendered line to `width` visible columns (fallback path). */\nfunction clampLines(lines: string[], width: number): string[] {\n\treturn lines.map((l) => (displayWidth(l) > width ? truncateToWidth(l, width, \"\") : l));\n}\n\n/** Apply a theme `fg` token safely; falls back to plain text. */\nfunction color(theme: Theme | undefined, token: string, text: string): string {\n\ttry {\n\t\treturn theme?.fg?.(token, text) ?? text;\n\t} catch {\n\t\treturn text;\n\t}\n}\n\n// lunr: strip ANSI escape sequences so footer statuses can be re-colored uniformly.\nfunction stripAnsi(s: string): string {\n\treturn s.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n}\n\n// ---------------------------------------------------------------------------\n// Stats formatting\n// ---------------------------------------------------------------------------\n\n/** Compact token-count formatter: `999`, `1.2k`, `15k`, `1.2M`, `15M`.\n * Matches pi's FooterComponent / the captured zentui examples\n * (`↑9.0M ↓112k`, `20%/1.0M`). */\nfunction formatCount(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/** Sum session token totals and derive the latest request's cache-hit rate. */\nfunction getUsageTotals(ctx: ExtensionContextLike): {\n\tinput: number;\n\toutput: number;\n\tcacheActivity: boolean;\n\tlatestCacheHitRate: number | undefined;\n} {\n\tlet input = 0;\n\tlet output = 0;\n\tlet cacheRead = 0;\n\tlet cacheWrite = 0;\n\tlet latestCacheHitRate: number | undefined;\n\tconst entries = ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch?.() ?? [];\n\tfor (const entry of entries as readonly SessionEntry[]) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst m = entry.message as AssistantMessageLike | undefined;\n\t\tif (!m || m.role !== \"assistant\") continue;\n\t\tconst u = m.usage;\n\t\tif (!u) continue;\n\t\tconst usageInput = u.input ?? 0;\n\t\tconst usageCacheRead = u.cacheRead ?? 0;\n\t\tconst usageCacheWrite = u.cacheWrite ?? 0;\n\t\tinput += usageInput;\n\t\toutput += u.output ?? 0;\n\t\tcacheRead += usageCacheRead;\n\t\tcacheWrite += usageCacheWrite;\n\t\tconst promptTokens = usageInput + usageCacheRead + usageCacheWrite;\n\t\tlatestCacheHitRate = promptTokens > 0 ? (usageCacheRead / promptTokens) * 100 : undefined;\n\t}\n\treturn { input, output, cacheActivity: cacheRead > 0 || cacheWrite > 0, latestCacheHitRate };\n}\n\n// ---------------------------------------------------------------------------\n// Provider display label\n//\n// `ctx.model.provider` is a ProviderId string (e.g. `\"ollama-cloud\"`), NOT the\n// human label. `BUILT_IN_PROVIDER_DISPLAY_NAMES` exists in pi core but is not\n// re-exported to extensions, so we derive the label (mirrors zentui's\n// `formatProviderLabel`, with an explicit `ollama-cloud` entry).\n// ---------------------------------------------------------------------------\n\nfunction formatProviderLabel(provider: string | undefined): string {\n\tif (!provider) return \"Unknown\";\n\tconst known: Record<string, string> = {\n\t\tanthropic: \"Anthropic\",\n\t\tgemini: \"Google\",\n\t\tgoogle: \"Google\",\n\t\tollama: \"Ollama\",\n\t\t\"ollama-cloud\": \"Ollama Cloud\",\n\t\topenai: \"OpenAI\",\n\t\t\"openai-codex\": \"OpenAI\",\n\t\tmistral: \"Mistral\",\n\t};\n\treturn (\n\t\tknown[provider] ??\n\t\tprovider.replace(/[-_]/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase())\n\t);\n}\n\n// ---------------------------------------------------------------------------\n// The chip: `model · provider · effort`\n// ---------------------------------------------------------------------------\n\nfunction colorEffort(theme: Theme | undefined, level: ThinkingLevel, text: string): string {\n\tconst token = thinkingThemeToken(level);\n\tconst painted = color(theme, token, text);\n\tif (token === \"thinkingMax\" && painted === text) {\n\t\treturn color(theme, \"thinkingXhigh\", text);\n\t}\n\treturn painted;\n}\n\nfunction buildChip(ctx: ExtensionContextLike, pi: ExtensionAPI): { left: string; effort: string; level: ThinkingLevel } {\n\tconst modelId = ctx.model?.id ?? \"no-model\";\n\tconst provider = ctx.model?.provider;\n\tconst providerLabel = formatProviderLabel(provider);\n\tconst fast = provider === \"openai-codex\" && lunrCustomizeBridge()?.getOpenAIFastMode?.() ? \" · fast\" : \"\";\n\tconst level = pi.getThinkingLevel();\n\tconst effort = formatChipEffort(level);\n\treturn { left: `${modelId}${fast} · ${providerLabel} · `, effort, level };\n}\n\n// ---------------------------------------------------------------------------\n// The prompt box — `ChatboxEditor extends CustomEditor`\n// ---------------------------------------------------------------------------\n\nclass ChatboxEditor extends CustomEditor {\n\tprivate readonly ctx: ExtensionContextLike;\n\tprivate readonly pi: ExtensionAPI;\n\n\tconstructor(\n\t\ttui: TUIHandle,\n\t\ttheme: EditorThemeLike,\n\t\tkeybindings: KeybindingsLike,\n\t\tctx: ExtensionContextLike,\n\t\tpi: ExtensionAPI,\n\t) {\n\t\t// CustomEditor ctor: (tui, theme, keybindings, options?: EditorOptions).\n\t\t// Casts keep the inline types from leaking into the real base signature.\n\t\tsuper(tui as unknown as never, theme as unknown as never, keybindings as unknown as never, {\n\t\t\tpaddingX: 0,\n\t\t});\n\t\tthis.ctx = ctx;\n\t\tthis.pi = pi;\n\t\t// lunr: do not clobber borderColor — InteractiveMode.updateEditorBorderColor()\n\t\t// owns it (thinkingOff…thinkingMax, or bashMode).\n\t}\n\n\tprivate color(token: string, text: string): string {\n\t\treturn color(this.ctx.ui?.theme, token, text);\n\t}\n\n\toverride render(width: number): string[] {\n\t\t// Too narrow to draw a box — defer to the base editor, clamped.\n\t\tif (width <= 4) {\n\t\t\treturn clampLines(super.render(width), width);\n\t\t}\n\n\t\t// Rails: `│ ` (left) + ` │` (right) => 4 columns of chrome.\n\t\tconst promptSymbol = lunrPromptSymbolEnabled();\n\t\tconst glyph = lunrPromptGlyph(this.ctx.ui?.theme);\n\t\tconst glyphW = promptSymbol ? displayWidth(glyph) : 0;\n\t\tconst innerWidth = Math.max(1, width - 4 - glyphW);\n\t\tconst base = super.render(innerWidth);\n\n\t\t// The base editor appends the autocomplete menu lines (if any) to the END\n\t\t// of its render output. Split them off so the body sits inside the box and\n\t\t// the autocomplete renders BELOW the bottom border (preserves slash\n\t\t// commands). Mirrors zentui's `renderPolishedFrame`.\n\t\tconst showing = this.isShowingAutocomplete();\n\t\tconst acList = (this as unknown as { autocompleteList?: AutocompleteListLike }).autocompleteList;\n\t\tlet acCount = 0;\n\t\tif (showing && acList && typeof acList.render === \"function\") {\n\t\t\ttry {\n\t\t\t\tacCount = acList.render(innerWidth).length;\n\t\t\t} catch {\n\t\t\t\tacCount = 0;\n\t\t\t}\n\t\t}\n\n\t\t// `super.render` returns [base top ─ border, ...wrapped text lines, base\n\t\t// bottom ─ border, ...autocomplete lines] — the base editor draws its OWN\n\t\t// straight borders and (when active) appends the autocomplete menu. Split the\n\t\t// autocomplete off the end, then STRIP the base's first/last lines (its own\n\t\t// borders) so we don't render them a second time inside our rounded frame.\n\t\t// Mirrors zentui's `editorFrame.slice(1, -1)` (renderPolishedFrame).\n\t\tlet frame = base;\n\t\tlet acLines: string[] = [];\n\t\tif (acCount > 0 && acCount < frame.length) {\n\t\t\tacLines = frame.slice(frame.length - acCount);\n\t\t\tframe = frame.slice(0, frame.length - acCount);\n\t\t}\n\t\tconst inner = frame.length >= 2 ? frame.slice(1, frame.length - 1) : frame;\n\t\tconst body = inner.length > 0 ? inner : [\"\"];\n\n\t\tconst border = (s: string): string => this.borderColor(s);\n\n\t\t// Top border: ╭─…─╮\n\t\tconst top = border(\"\\u256d\" + \"\\u2500\".repeat(width - 2) + \"\\u256e\");\n\n\t\t// Body: │ <padded line> │ (auto-grows with the number of wrapped lines)\n\t\t// lunr: prefix the first body line with the dim theme prompt glyph (☾ > by\n\t\t// default); a same-width blank gutter keeps subsequent lines aligned.\n\t\tconst gutter = glyphW > 0 ? \" \".repeat(glyphW) : \"\";\n\t\tconst bodyLines = body.map((ln: string, i: number) => {\n\t\t\tconst prefix = i === 0 && glyphW > 0 ? this.color(\"dim\", glyph) : gutter;\n\t\t\treturn border(\"\\u2502 \") + prefix + padRight(ln, innerWidth) + border(\" \\u2502\");\n\t\t});\n\n\t\t// Bottom border with the right-aligned chip: ╰─…─ <chip> ─╯\n\t\tconst bottom = this.renderBottomBorder(width);\n\n\t\treturn [top, ...bodyLines, bottom, ...acLines];\n\t}\n\n\tprivate renderBottomBorder(width: number): string {\n\t\tconst border = (s: string): string => this.borderColor(s);\n\t\tconst parts = buildChip(this.ctx, this.pi);\n\t\tconst plain = parts.left + parts.effort;\n\t\tconst overhead = 4;\n\t\tconst maxChip = Math.max(0, width - overhead);\n\t\tconst truncated = displayWidth(plain) > maxChip;\n\t\tconst chipPlain = truncated ? truncatePlain(plain, maxChip) : plain;\n\t\tconst chipW = displayWidth(chipPlain);\n\t\tconst dashTotal = Math.max(0, width - overhead - chipW);\n\t\t// Right-align the chip: a single trailing dash before the corner, the\n\t\t// rest lead (right-aligns the chip near the corner).\n\t\tconst rightDashes = Math.min(dashTotal, 1);\n\t\tconst leftDashes = dashTotal - rightDashes;\n\n\t\tconst left = \"\\u2570\" + \"\\u2500\".repeat(leftDashes) + \" \";\n\t\tconst right = \" \" + \"\\u2500\".repeat(rightDashes) + \"\\u256f\";\n\t\tconst theme = this.ctx.ui?.theme;\n\t\t// Truncated chips may have lost the effort token — paint all white.\n\t\t// Otherwise model/provider stay white and the effort uses the thinking token.\n\t\tconst chip = truncated\n\t\t\t? this.color(\"white\", chipPlain)\n\t\t\t: this.color(\"white\", parts.left) + colorEffort(theme, parts.level, parts.effort);\n\t\treturn border(left) + chip + border(right);\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// The stats line (footer) — slim, single line, BELOW the box\n// ---------------------------------------------------------------------------\n\nexport function renderStatsLine(\n\twidth: number,\n\tctx: ExtensionContextLike,\n\ttheme: Theme,\n\tfooterData: ReadonlyFooterDataProvider,\n): string[] {\n\tconst sep = color(theme, \"accent2\", \" | \"); // lunr: theme-polish — separator uses subtle accent2 blue (was bright-black plain fallback)\n\tconst parts: string[] = [];\n\n\t// lunr: mode zone — permission mode + agent-mode statuses (plan/goal/swarm)\n\t// render as ONE segment (dim middot-separated), not scattered across piped sections.\n\tconst modeZone: string[] = [];\n\n\t// lunr: permission mode safety indicator — always shown (not toggle-gated).\n\tconst mode = lunrPermissionMode();\n\tif (mode === \"yolo\" || mode === \"auto\") modeZone.push(color(theme, \"warning\", mode));\n\telse if (mode === \"manual\" || mode === \"plan\") modeZone.push(color(theme, \"white\", mode)); // lunr: theme-polish — manual/plan read white (was dim)\n\n\t// lunr: footer element toggles from the customize bridge (read at render time).\n\tconst footerToggles = lunrFooterToggles();\n\n\t// 1) Extension statuses published by other extensions (rendered as-is,\n\t// already styled): LSP (`pi-lsp-extension`), MCP (`pi-mcp-adapter`),\n\t// throughput/tokens (`pi-tps-was-taken`). These are NOT on `ctx` directly.\n\tconst statuses = footerData.getExtensionStatuses?.();\n\tif (statuses && statuses.size > 0) {\n\t\t// lunr: status segments are toggle-gated via the customize bridge:\n\t\t// footerStatuses gates plan/goal/swarm, footerTps gates tps,\n\t\t// footerMcp gates mcp/mcp-auth, footerLsp gates lsp. Publishers keep\n\t\t// calling ctx.ui.setStatus harmlessly when their segment is hidden.\n\t\t// lunr: plan/goal/swarm join the mode zone; tps/mcp/lsp stay piped.\n\t\tconst modeKeys: string[] = footerToggles.statuses ? [\"plan\", \"goal\", \"swarm\"] : [];\n\t\tfor (const key of modeKeys) {\n\t\t\tconst v = statuses.get(key);\n\t\t\tif (v) modeZone.push(color(theme, \"white\", stripAnsi(v)));\n\t\t}\n\t\tconst pipedKeys: string[] = [];\n\t\tif (footerToggles.tps) pipedKeys.push(\"tps\");\n\t\tif (footerToggles.mcp) pipedKeys.push(\"mcp\", \"mcp-auth\");\n\t\tif (footerToggles.lsp) pipedKeys.push(\"lsp\");\n\t\tfor (const key of pipedKeys) {\n\t\t\tconst v = statuses.get(key);\n\t\t\t// lunr: unify footer status colors (white, was dim) so the whole stats line reads as one tone.\n\t\t\tif (v) parts.push(color(theme, \"white\", stripAnsi(v))); // lunr: theme-polish — status segments white (was dim)\n\t\t}\n\t}\n\tif (modeZone.length > 0) {\n\t\tparts.unshift(modeZone.join(color(theme, \"dim\", \" · \")));\n\t}\n\n\t// lunr: git branch + working-tree +/- vs HEAD (gated on footerGit).\n\tif (footerToggles.git) {\n\t\tconst branch = footerData.getGitBranch?.();\n\t\tif (branch) {\n\t\t\tconst diff = footerData.getGitDiffstat?.();\n\t\t\tlet gitSeg = color(theme, \"white\", branch);\n\t\t\tif (diff && (diff.added !== 0 || diff.removed !== 0)) {\n\t\t\t\tgitSeg +=\n\t\t\t\t\t` ${color(theme, \"success\", `+${diff.added}`)} ${color(theme, \"error\", `-${diff.removed}`)}`;\n\t\t\t}\n\t\t\tparts.push(gitSeg);\n\t\t}\n\t}\n\n\t// lunr: live subscription window bar (gated on footerPlan). Hidden with no plan.\n\tif (footerToggles.plan) {\n\t\tconst provider = ctx.model?.provider;\n\t\tif (provider) {\n\t\t\tconst usage = lunrUsageBridge();\n\t\t\tusage?.prefetch?.(provider);\n\t\t\tconst seg = usage?.pickForFooter?.(provider);\n\t\t\tif (seg) {\n\t\t\t\tconst pct = `${Math.round(seg.usedPercent)}%`;\n\t\t\t\tconst barColor = seg.usedPercent > 90 ? \"error\" : seg.usedPercent > 70 ? \"warning\" : \"success\";\n\t\t\t\tconst bar = footerToggles.planBar ? `${compactPlanBar(seg.usedPercent, theme)} ` : \"\";\n\t\t\t\tparts.push(color(theme, \"white\", seg.label) + \" \" + bar + color(theme, barColor, pct));\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2) Context usage: `pct/window` (lunr: gated on footerContext).\n\tif (footerToggles.context) {\n\t\tconst usage = ctx.getContextUsage();\n\t\tconst win = ctx.model?.contextWindow ?? usage?.contextWindow ?? 0;\n\t\tconst pct = usage?.percent == null ? \"?\" : `${Math.round(usage.percent)}%`;\n\t\tconst ctxSeg = `${pct}/${formatCount(win)}`;\n\t\tconst pv = usage?.percent ?? 0;\n\t\tif (pv > 90) parts.push(color(theme, \"error\", ctxSeg));\n\t\telse if (pv > 70) parts.push(color(theme, \"warning\", ctxSeg));\n\t\telse parts.push(color(theme, \"white\", ctxSeg)); // lunr: theme-polish — normal context % white (was dim)\n\t}\n\n\tconst totals = getUsageTotals(ctx);\n\n\t// 3) Token totals: ↑in ↓out (lunr: gated on footerTokens).\n\tif (footerToggles.tokens) {\n\t\tparts.push(color(theme, \"white\", `\\u2191${formatCount(totals.input)} \\u2193${formatCount(totals.output)}`)); // lunr: theme-polish — token totals white (was dim)\n\t}\n\n\t// 4) Latest prompt cache hit rate (gated on footerCacheHitRate).\n\tif (footerToggles.cacheHitRate && totals.cacheActivity && totals.latestCacheHitRate !== undefined) {\n\t\tparts.push(color(theme, \"white\", `CH${totals.latestCacheHitRate.toFixed(1)}%`));\n\t}\n\n\t// lunr: cost/usage counter segment removed entirely (both the $x.xxx dollar\n\t// counter for API-key providers and the plan-usage limit bar for OAuth/subscription\n\t// providers). Segments 1-3 (statuses, context, tokens) remain.\n\n\tlet line = parts.join(sep);\n\tif (displayWidth(line) > width) {\n\t\tline = truncateToWidth(line, width, \"\");\n\t}\n\treturn [line];\n}\n\n// ---------------------------------------------------------------------------\n// Install / teardown\n// ---------------------------------------------------------------------------\n\nfunction installEditor(\n\tctx: ExtensionContextLike,\n\tpi: ExtensionAPI,\n\tsetRequestor: (r: () => void) => void,\n): void {\n\tif (typeof ctx.ui.setEditorComponent !== \"function\") return;\n\tconst factory: EditorFactoryLike = (tui, _theme, _keybindings) => {\n\t\tsetRequestor(() => {\n\t\t\ttry {\n\t\t\t\ttui.requestRender();\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t});\n\t\treturn new ChatboxEditor(tui, _theme, _keybindings, ctx, pi);\n\t};\n\tctx.ui.setEditorComponent(factory);\n}\n\nfunction installFooter(\n\tctx: ExtensionContextLike,\n\tsetRequestor: (r: () => void) => void,\n): void {\n\tif (typeof ctx.ui.setFooter !== \"function\") return;\n\tlet disposed = false;\n\tconst factory: FooterFactoryLike = (tui, _theme, _footerData) => {\n\t\tsetRequestor(() => {\n\t\t\ttry {\n\t\t\t\ttui.requestRender();\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t});\n\t\treturn {\n\t\t\tinvalidate(): void {\n\t\t\t\t// render is pure\n\t\t\t},\n\t\t\tdispose(): void {\n\t\t\t\tdisposed = true;\n\t\t\t},\n\t\t\trender(width: number): string[] {\n\t\t\t\tif (disposed) return [];\n\t\t\t\treturn renderStatsLine(width, ctx, _theme, _footerData);\n\t\t\t},\n\t\t};\n\t};\n\tctx.ui.setFooter(factory);\n}\n\n// ---------------------------------------------------------------------------\n// Extension entry point\n// ---------------------------------------------------------------------------\n\nexport default function (pi: ExtensionAPI): void {\n\tlet renderRequestor: (() => void) | undefined = undefined;\n\tconst requestRender = (): void => {\n\t\ttry {\n\t\t\trenderRequestor?.();\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t};\n\n\tpi.on(\"session_start\", (_event, ctx) => {\n\t\tif (!ctx.hasUI || ctx.mode !== \"tui\") return;\n\t\tinstallEditor(ctx, pi, (r) => {\n\t\t\trenderRequestor = r;\n\t\t});\n\t\tinstallFooter(ctx, (r) => {\n\t\t\tif (!renderRequestor) renderRequestor = r;\n\t\t});\n\t});\n\n\tpi.on(\"session_shutdown\", (_event, ctx) => {\n\t\ttry {\n\t\t\tif (typeof ctx.ui?.setEditorComponent === \"function\") ctx.ui.setEditorComponent(undefined);\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t\ttry {\n\t\t\tif (typeof ctx.ui?.setFooter === \"function\") ctx.ui.setFooter(undefined);\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t\trenderRequestor = undefined;\n\t});\n\n\t// Re-render triggers: model/provider + effort (chip), and token/cost/context\n\t// (stats line). Event names match those zentui wires.\n\tconst reRenderEvents = [\n\t\t\"model_select\",\n\t\t\"thinking_level_select\",\n\t\t\"agent_end\",\n\t\t\"message_end\",\n\t\t\"tool_execution_end\",\n\t\t\"session_compact\",\n\t];\n\tfor (const ev of reRenderEvents) {\n\t\tpi.on(ev, () => requestRender());\n\t}\n}"]}
1
+ {"version":3,"file":"ashxj-tui.d.ts","sourceRoot":"","sources":["../../src/builtin-extensions/ashxj-tui.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ashxj-tui — a custom slim chatbox for pi.\n *\n * Replaces pi-zentui's prompt box + statusline with:\n * - A rounded prompt box that auto-expands as the typed message wraps to\n * multiple rows (the sides extend; height grows with content).\n * - A right-aligned chip on the box's BOTTOM border:\n * model · provider · effort (e.g. `glm-5.2 · Ollama Cloud · xhigh`)\n * - A slim stats line (extension statuses · context% · ↑↓; each segment\n * toggle-gated via the lunr customize bridge) BELOW the box, not wrapped\n * around the input.\n * - No session-mode indicator anywhere (pi has no such concept).\n *\n * Loaded by pi via jiti — no build step, plain TypeScript.\n *\n * NOTE on type imports (see `ashxj-thinking`/`simple-memory` for the convention):\n * This file declares structural types inline rather than importing them from\n * `@ashx-j/lunr` / `@ashx-j/lunr-tui`.\n * pi-coding-agent's `index.d.ts` re-exports from internal `.d.ts` files that\n * contain subpath imports like `@ashx-j/lunr-ai/compat`, which do not\n * resolve under `tsc` with a file argument. We keep the typecheck\n * self-contained with inline types and verify via the included `tsconfig.json`\n * (`moduleResolution: \"Bundler\"` + `skipLibCheck: true`).\n *\n * The ONE exception is `CustomEditor`, imported as a RUNTIME VALUE directly\n * from its defining module (`modes/interactive/components/custom-editor.ts`).\n * We must `extends` it to inherit pi's app keybindings (escape, ctrl+d, model\n * switching, extension shortcuts) — wrapping would lose those. It is imported\n * from the concrete module, NOT the package barrel, because the barrel\n * re-exports main.ts which imports this file — a circular import that crashes\n * under vite-node (`Class extends value undefined`).\n */\n\nimport { CustomEditor } from \"../modes/interactive/components/custom-editor.js\";\n\nconst CUSTOMIZE_BRIDGE = Symbol.for(\"@lunr/customize\");\nconst PERMISSION_MODE_BRIDGE = Symbol.for(\"@lunr/permission-mode\");\ninterface CustomizeBridgeForPrompt {\n\tgetOpenAIFastMode?(): boolean;\n\t// lunr: footer element toggles (added to the same bridge).\n\tgetFooterMcp?(): boolean;\n\tgetFooterLsp?(): boolean;\n\tgetFooterContext?(): boolean;\n\tgetFooterTokens?(): boolean;\n\tgetFooterCacheHitRate?(): boolean;\n\tgetFooterTps?(): boolean;\n\tgetFooterStatuses?(): boolean;\n\tgetFooterGit?(): boolean;\n\tgetFooterPlan?(): boolean;\n\tgetFooterPlanBar?(): boolean;\n}\ninterface PermissionModeBridgeForFooter {\n\tgetMode(): string | undefined;\n}\nfunction lunrCustomizeBridge(): CustomizeBridgeForPrompt | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[CUSTOMIZE_BRIDGE] as CustomizeBridgeForPrompt | undefined;\n}\nfunction lunrPermissionModeBridge(): PermissionModeBridgeForFooter | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[PERMISSION_MODE_BRIDGE] as PermissionModeBridgeForFooter | undefined;\n}\n// lunr: footer toggles, read at render time. Bridgeless / missing-getter fallback\n// matches the settings-manager defaults: MCP on, LSP off, everything else on.\nfunction lunrFooterToggles(): {\n\tmcp: boolean;\n\tlsp: boolean;\n\tcontext: boolean;\n\ttokens: boolean;\n\tcacheHitRate: boolean;\n\ttps: boolean;\n\tstatuses: boolean;\n\tgit: boolean;\n\tplan: boolean;\n\tplanBar: boolean;\n} {\n\tconst bridge = lunrCustomizeBridge();\n\treturn {\n\t\tmcp: bridge?.getFooterMcp?.() ?? true,\n\t\tlsp: bridge?.getFooterLsp?.() ?? false,\n\t\tcontext: bridge?.getFooterContext?.() ?? true,\n\t\ttokens: bridge?.getFooterTokens?.() ?? true,\n\t\tcacheHitRate: bridge?.getFooterCacheHitRate?.() ?? true,\n\t\ttps: bridge?.getFooterTps?.() ?? true,\n\t\tstatuses: bridge?.getFooterStatuses?.() ?? true,\n\t\tgit: bridge?.getFooterGit?.() ?? true,\n\t\tplan: bridge?.getFooterPlan?.() ?? true,\n\t\tplanBar: bridge?.getFooterPlanBar?.() ?? true,\n\t};\n}\n\nconst USAGE_SERVICE_BRIDGE = Symbol.for(\"@lunr/usage-service\");\ninterface UsageServiceBridgeForFooter {\n\tpickForFooter?(providerId: string): { label: string; usedPercent: number } | undefined;\n\tprefetch?(providerId: string): void;\n}\nfunction lunrUsageBridge(): UsageServiceBridgeForFooter | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[USAGE_SERVICE_BRIDGE] as UsageServiceBridgeForFooter | undefined;\n}\n\nfunction compactPlanBar(percent: number, theme: Theme): string {\n\tconst cells = 8;\n\tconst clamped = Math.max(0, Math.min(100, percent));\n\tconst filled = Math.round((clamped / 100) * cells);\n\tconst token = clamped > 90 ? \"error\" : clamped > 70 ? \"warning\" : \"success\";\n\tconst fill = filled > 0 ? color(theme, token, \"█\".repeat(filled)) : \"\";\n\tconst rest = filled < cells ? color(theme, \"dim\", \"░\".repeat(cells - filled)) : \"\";\n\treturn fill + rest;\n}\n// lunr: permission mode for the footer safety indicator (always shown).\nfunction lunrPermissionMode(): string | undefined {\n\treturn lunrPermissionModeBridge()?.getMode();\n}\nexport function lunrPromptGlyph(theme: Theme | undefined): string {\n\tconst arrow = theme?.glyph?.(\"promptArrow\") ?? \">\";\n\treturn arrow ? `${arrow} ` : \"\";\n}\n\n// ---------------------------------------------------------------------------\n// Minimal structural types (inline — see file header)\n// ---------------------------------------------------------------------------\n\ntype ThinkingLevel = \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n\n/** Theme token for the chatbox thinking-level border / effort chip. */\nexport function thinkingThemeToken(level: ThinkingLevel): string {\n\tswitch (level) {\n\t\tcase \"off\":\n\t\t\treturn \"thinkingOff\";\n\t\tcase \"minimal\":\n\t\t\treturn \"thinkingMinimal\";\n\t\tcase \"low\":\n\t\t\treturn \"thinkingLow\";\n\t\tcase \"medium\":\n\t\t\treturn \"thinkingMedium\";\n\t\tcase \"high\":\n\t\t\treturn \"thinkingHigh\";\n\t\tcase \"xhigh\":\n\t\t\treturn \"thinkingXhigh\";\n\t\tcase \"max\":\n\t\t\treturn \"thinkingMax\";\n\t\tdefault:\n\t\t\treturn \"thinkingOff\";\n\t}\n}\n\n/** Chip effort label — identity, locked so xhigh/max are never mapped down to high. */\nexport function formatChipEffort(level: ThinkingLevel): string {\n\treturn level;\n}\n\n/** Trimmed view of pi's `Theme` — only the methods we use. */\ninterface Theme {\n\tfg(token: string, text: string): string;\n\t/** lunr: prompt glyph tokens (optional for forward/backward compat). */\n\tglyph?(name: \"promptArrow\"): string;\n}\n\n/** Trimmed view of `pi-tui`'s `TUI` — only the method we use. */\ninterface TUIHandle {\n\trequestRender(force?: boolean): void;\n}\n\n/** Trimmed view of `pi-tui`'s `EditorTheme`. */\ninterface EditorThemeLike {\n\tborderColor: (str: string) => string;\n\tselectList: unknown;\n}\n\n/** Opaque keybindings manager. */\ninterface KeybindingsLike {\n\treadonly _?: unknown;\n}\n\n/** Trimmed view of an autocomplete list (the editor's private `autocompleteList`). */\ninterface AutocompleteListLike {\n\trender(width: number): string[];\n}\n\n/** Trimmed view of pi's `ReadonlyFooterDataProvider`. */\ninterface ReadonlyFooterDataProvider {\n\tgetGitBranch(): string | null;\n\tgetGitDiffstat?(): { added: number; removed: number } | null;\n\tgetExtensionStatuses(): ReadonlyMap<string, string>;\n\tgetAvailableProviderCount(): number;\n\tonBranchChange(callback: () => void): () => void;\n}\n\n/** Trimmed view of the `getContextUsage` return value. */\ninterface ContextUsage {\n\ttokens: number | null;\n\tcontextWindow: number;\n\tpercent: number | null;\n}\n\n/** Trimmed view of an assistant message's usage — the bits we render. */\ninterface AssistantUsage {\n\tinput: number;\n\toutput: number;\n\tcacheRead?: number;\n\tcacheWrite?: number;\n\tcost: { total: number };\n}\n\n/** Trimmed view of an assistant message. */\ninterface AssistantMessageLike {\n\trole: \"assistant\";\n\tusage: AssistantUsage;\n}\n\n/** Trimmed view of a session entry — only what we iterate over. */\ntype SessionEntry =\n\t| { type: \"message\"; message: AssistantMessageLike }\n\t| { type: string; message?: unknown };\n\n/** Trimmed view of a `ReadonlySessionManager`. */\ninterface ReadonlySessionManagerLike {\n\tgetEntries(): readonly SessionEntry[];\n\tgetBranch?(): readonly SessionEntry[];\n}\n\n/** Trimmed view of a `Model`. */\ninterface ModelLike {\n\tid?: string;\n\tprovider?: string;\n\tcontextWindow?: number;\n\treasoning?: boolean;\n\tthinkingLevelMap?: Record<string, string | null>;\n}\n\ninterface ExtensionUIContextLike {\n\tsetEditorComponent(factory: EditorFactoryLike | undefined): void;\n\tsetFooter(factory: FooterFactoryLike | undefined): void;\n\treadonly theme?: Theme;\n}\n\n/** Trimmed view of `ExtensionContext`. */\ninterface ExtensionContextLike {\n\tmode: \"tui\" | \"rpc\" | \"json\" | \"print\";\n\thasUI: boolean;\n\tmodel: ModelLike | undefined;\n\tsessionManager: ReadonlySessionManagerLike;\n\tgetContextUsage(): ContextUsage | undefined;\n\tui: ExtensionUIContextLike;\n}\n\n/** Trimmed view of `ExtensionAPI`. */\ninterface ExtensionAPI {\n\tgetThinkingLevel(): ThinkingLevel;\n\ton(event: string, handler: (event: unknown, ctx: ExtensionContextLike) => void): void;\n}\n\n/** `setEditorComponent` factory shape. */\ntype EditorFactoryLike = (\n\ttui: TUIHandle,\n\ttheme: EditorThemeLike,\n\tkeybindings: KeybindingsLike,\n) => EditorComponentLike;\n\ninterface EditorComponentLike {\n\trender(width: number): string[];\n\tgetText(): string;\n\tsetText(text: string): void;\n\tinvalidate(): void;\n\thandleInput?(data: string): void;\n}\n\n/** `setFooter` factory shape. */\ntype FooterFactoryLike = (\n\ttui: TUIHandle,\n\ttheme: Theme,\n\tfooterData: ReadonlyFooterDataProvider,\n) => FooterComponentLike;\n\ninterface FooterComponentLike {\n\trender(width: number): string[];\n\tdispose?(): void;\n\tinvalidate?(): void;\n}\n\n// ---------------------------------------------------------------------------\n// Display-width helpers (ANSI-aware; CJK-aware)\n//\n// The editor (`Editor.render`) word-wraps to the given width using grapheme\n// width, so each returned line is ≤ that many visual columns. To right-pad a\n// line for our box we need a width model that matches closely enough: skip ALL\n// escape sequences (SGR color, OSC/DCS/APC/PM/SOS string sequences such as\n// pi-tui's `CURSOR_MARKER` `ESC _pi:c BEL`, and two-char escapes), count common\n// East-Asian wide ranges as 2, combining marks and variation selectors as 0.\n// Handling the string sequences as 0-width is what keeps the right `│` rail\n// aligned on the focused (cursor) line — the base editor embeds the marker and\n// the TUI strips it at flush time, so we must not count it here.\n// ---------------------------------------------------------------------------\n\nfunction skipAnsi(str: string, pos: number): number {\n\tif (str.charCodeAt(pos) !== 0x1b) return 0;\n\tconst next = str.charCodeAt(pos + 1);\n\t// CSI: ESC [ ... <final 0x40-0x7e>\n\tif (next === 0x5b /* [ */) {\n\t\tlet j = pos + 2;\n\t\twhile (j < str.length) {\n\t\t\tconst c = str.charCodeAt(j);\n\t\t\tif (c >= 0x40 && c <= 0x7e) return j + 1 - pos;\n\t\t\tj++;\n\t\t}\n\t\treturn str.length - pos; // unterminated; consume the rest\n\t}\n\t// String sequences — OSC (]), DCS (P), APC (_), PM (^), SOS (X) — terminated\n\t// by BEL (0x07) or ST (ESC \\). pi-tui's CURSOR_MARKER `ESC _pi:c BEL` is APC;\n\t// OSC color queries are OSC. All are zero visual width.\n\tif (\n\t\tnext === 0x5d /* ] */ ||\n\t\tnext === 0x50 /* P */ ||\n\t\tnext === 0x5f /* _ */ ||\n\t\tnext === 0x5e /* ^ */ ||\n\t\tnext === 0x58 /* X */\n\t) {\n\t\tlet j = pos + 2;\n\t\twhile (j < str.length) {\n\t\t\tconst c = str.charCodeAt(j);\n\t\t\tif (c === 0x07 /* BEL */) return j + 1 - pos;\n\t\t\tif (c === 0x1b && str.charCodeAt(j + 1) === 0x5c /* \\ */) return j + 2 - pos;\n\t\t\tj++;\n\t\t}\n\t\treturn str.length - pos; // unterminated; consume the rest\n\t}\n\t// Any other ESC sequence: consume ESC + one final byte (e.g. ESC c, ESC \\,\n\t// ESC ( B). A lone trailing ESC with no following byte is 0 width too.\n\treturn Number.isNaN(next) ? 1 : 2;\n}\n\nfunction isWide(cp: number): boolean {\n\treturn (\n\t\t(cp >= 0x1100 && cp <= 0x115f) ||\n\t\t(cp >= 0x2e80 && cp <= 0x303e) ||\n\t\t(cp >= 0x3041 && cp <= 0x33ff) ||\n\t\t(cp >= 0x3400 && cp <= 0x4dbf) ||\n\t\t(cp >= 0x4e00 && cp <= 0x9fff) ||\n\t\t(cp >= 0xa000 && cp <= 0xa4cf) ||\n\t\t(cp >= 0xac00 && cp <= 0xd7a3) ||\n\t\t(cp >= 0xf900 && cp <= 0xfaff) ||\n\t\t(cp >= 0xfe30 && cp <= 0xfe4f) ||\n\t\t(cp >= 0xff00 && cp <= 0xff60) ||\n\t\t(cp >= 0xffe0 && cp <= 0xffe6) ||\n\t\t(cp >= 0x1f300 && cp <= 0x1faff) ||\n\t\t(cp >= 0x20000 && cp <= 0x3fffd)\n\t);\n}\n\nfunction charWidth(cp: number): number {\n\tif (cp >= 0x20 && cp < 0x7f) return 1;\n\tif ((cp >= 0x0300 && cp <= 0x036f) || (cp >= 0xfe00 && cp <= 0xfe0f) || cp === 0x200d) return 0;\n\treturn isWide(cp) ? 2 : 1;\n}\n\n/** Visible width of a string, ignoring SGR color codes. */\nfunction displayWidth(str: string): number {\n\tlet w = 0;\n\tlet i = 0;\n\twhile (i < str.length) {\n\t\tconst skip = skipAnsi(str, i);\n\t\tif (skip > 0) {\n\t\t\ti += skip;\n\t\t\tcontinue;\n\t\t}\n\t\tconst cp = str.codePointAt(i) ?? 0;\n\t\tw += charWidth(cp);\n\t\ti += cp > 0xffff ? 2 : 1;\n\t}\n\treturn w;\n}\n\n/** Right-pad a (possibly colored) line to `width` visible columns. */\nfunction padRight(line: string, width: number): string {\n\tconst w = displayWidth(line);\n\tif (w >= width) return line;\n\treturn line + \" \".repeat(width - w);\n}\n\n/** Truncate a plain (non-ANSI) string to `maxWidth` visible columns. */\nfunction truncatePlain(text: string, maxWidth: number): string {\n\tif (maxWidth <= 0) return \"\";\n\tif (displayWidth(text) <= maxWidth) return text;\n\tlet out = \"\";\n\tlet w = 0;\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst code = text.codePointAt(i) ?? 0;\n\t\tconst cw = charWidth(code);\n\t\tif (w + cw > maxWidth) break;\n\t\tout += String.fromCodePoint(code);\n\t\tw += cw;\n\t\ti += code > 0xffff ? 2 : 1;\n\t}\n\treturn out;\n}\n\n/** Truncate a (possibly ANSI-colored) string to `maxWidth` visible columns,\n * re-emitting an SGR reset at the cut so styling doesn't bleed. */\nfunction truncateToWidth(text: string, maxWidth: number, ellipsis = \"\"): string {\n\tif (maxWidth <= 0) return \"\";\n\tif (displayWidth(text) <= maxWidth) return text;\n\tconst target = Math.max(0, maxWidth - displayWidth(ellipsis));\n\tlet out = \"\";\n\tlet w = 0;\n\tlet i = 0;\n\twhile (i < text.length && w < target) {\n\t\tconst skip = skipAnsi(text, i);\n\t\tif (skip > 0) {\n\t\t\tout += text.slice(i, i + skip);\n\t\t\ti += skip;\n\t\t\tcontinue;\n\t\t}\n\t\tconst code = text.codePointAt(i) ?? 0;\n\t\tconst cw = charWidth(code);\n\t\tif (w + cw > target) break;\n\t\tout += String.fromCodePoint(code);\n\t\tw += cw;\n\t\ti += code > 0xffff ? 2 : 1;\n\t}\n\tout += \"\\x1b[0m\";\n\treturn out + ellipsis;\n}\n\n/** Clamp each rendered line to `width` visible columns (fallback path). */\nfunction clampLines(lines: string[], width: number): string[] {\n\treturn lines.map((l) => (displayWidth(l) > width ? truncateToWidth(l, width, \"\") : l));\n}\n\n/** Apply a theme `fg` token safely; falls back to plain text. */\nfunction color(theme: Theme | undefined, token: string, text: string): string {\n\ttry {\n\t\treturn theme?.fg?.(token, text) ?? text;\n\t} catch {\n\t\treturn text;\n\t}\n}\n\n// lunr: strip ANSI escape sequences so footer statuses can be re-colored uniformly.\nfunction stripAnsi(s: string): string {\n\treturn s.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n}\n\n// ---------------------------------------------------------------------------\n// Stats formatting\n// ---------------------------------------------------------------------------\n\n/** Compact token-count formatter: `999`, `1.2k`, `15k`, `1.2M`, `15M`.\n * Matches pi's FooterComponent / the captured zentui examples\n * (`↑9.0M ↓112k`, `20%/1.0M`). */\nfunction formatCount(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/** Sum session token totals and derive the latest request's cache-hit rate. */\nfunction getUsageTotals(ctx: ExtensionContextLike): {\n\tinput: number;\n\toutput: number;\n\tcacheActivity: boolean;\n\tlatestCacheHitRate: number | undefined;\n} {\n\tlet input = 0;\n\tlet output = 0;\n\tlet cacheRead = 0;\n\tlet cacheWrite = 0;\n\tlet latestCacheHitRate: number | undefined;\n\tconst entries = ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch?.() ?? [];\n\tfor (const entry of entries as readonly SessionEntry[]) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst m = entry.message as AssistantMessageLike | undefined;\n\t\tif (!m || m.role !== \"assistant\") continue;\n\t\tconst u = m.usage;\n\t\tif (!u) continue;\n\t\tconst usageInput = u.input ?? 0;\n\t\tconst usageCacheRead = u.cacheRead ?? 0;\n\t\tconst usageCacheWrite = u.cacheWrite ?? 0;\n\t\tinput += usageInput;\n\t\toutput += u.output ?? 0;\n\t\tcacheRead += usageCacheRead;\n\t\tcacheWrite += usageCacheWrite;\n\t\tconst promptTokens = usageInput + usageCacheRead + usageCacheWrite;\n\t\tlatestCacheHitRate = promptTokens > 0 ? (usageCacheRead / promptTokens) * 100 : undefined;\n\t}\n\treturn { input, output, cacheActivity: cacheRead > 0 || cacheWrite > 0, latestCacheHitRate };\n}\n\n// ---------------------------------------------------------------------------\n// Provider display label\n//\n// `ctx.model.provider` is a ProviderId string (e.g. `\"ollama-cloud\"`), NOT the\n// human label. `BUILT_IN_PROVIDER_DISPLAY_NAMES` exists in pi core but is not\n// re-exported to extensions, so we derive the label (mirrors zentui's\n// `formatProviderLabel`, with an explicit `ollama-cloud` entry).\n// ---------------------------------------------------------------------------\n\nfunction formatProviderLabel(provider: string | undefined): string {\n\tif (!provider) return \"Unknown\";\n\tconst known: Record<string, string> = {\n\t\tanthropic: \"Anthropic\",\n\t\tgemini: \"Google\",\n\t\tgoogle: \"Google\",\n\t\tollama: \"Ollama\",\n\t\t\"ollama-cloud\": \"Ollama Cloud\",\n\t\topenai: \"OpenAI\",\n\t\t\"openai-codex\": \"OpenAI\",\n\t\tmistral: \"Mistral\",\n\t};\n\treturn (\n\t\tknown[provider] ??\n\t\tprovider.replace(/[-_]/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase())\n\t);\n}\n\n// ---------------------------------------------------------------------------\n// The chip: `model · provider · effort`\n// ---------------------------------------------------------------------------\n\nfunction colorEffort(theme: Theme | undefined, level: ThinkingLevel, text: string): string {\n\tconst token = thinkingThemeToken(level);\n\tconst painted = color(theme, token, text);\n\tif (token === \"thinkingMax\" && painted === text) {\n\t\treturn color(theme, \"thinkingXhigh\", text);\n\t}\n\treturn painted;\n}\n\nfunction buildChip(ctx: ExtensionContextLike, pi: ExtensionAPI): { left: string; effort: string; level: ThinkingLevel } {\n\tconst modelId = ctx.model?.id ?? \"no-model\";\n\tconst provider = ctx.model?.provider;\n\tconst providerLabel = formatProviderLabel(provider);\n\tconst fast = provider === \"openai-codex\" && lunrCustomizeBridge()?.getOpenAIFastMode?.() ? \" · fast\" : \"\";\n\tconst level = pi.getThinkingLevel();\n\tconst effort = formatChipEffort(level);\n\treturn { left: `${modelId}${fast} · ${providerLabel} · `, effort, level };\n}\n\n// ---------------------------------------------------------------------------\n// The prompt box — `ChatboxEditor extends CustomEditor`\n// ---------------------------------------------------------------------------\n\nexport class ChatboxEditor extends CustomEditor {\n\tprivate ctx: ExtensionContextLike;\n\tprivate pi: ExtensionAPI;\n\n\tbind(ctx: ExtensionContextLike, pi: ExtensionAPI): void {\n\t\tthis.ctx = ctx;\n\t\tthis.pi = pi;\n\t}\n\n\t/** Keep the displayed values while /new or /resume replaces the old runtime. */\n\tunbind(): void {\n\t\tconst level = this.pi.getThinkingLevel();\n\t\tthis.bind(\n\t\t\t{\n\t\t\t\tmode: \"tui\",\n\t\t\t\thasUI: true,\n\t\t\t\tmodel: this.ctx.model,\n\t\t\t\tsessionManager: { getEntries: () => [] },\n\t\t\t\tgetContextUsage: () => undefined,\n\t\t\t\tui: { theme: this.ctx.ui?.theme, setEditorComponent() {}, setFooter() {} },\n\t\t\t},\n\t\t\t{ getThinkingLevel: () => level, on() {} },\n\t\t);\n\t}\n\n\tconstructor(\n\t\ttui: TUIHandle,\n\t\ttheme: EditorThemeLike,\n\t\tkeybindings: KeybindingsLike,\n\t\tctx: ExtensionContextLike,\n\t\tpi: ExtensionAPI,\n\t) {\n\t\t// CustomEditor ctor: (tui, theme, keybindings, options?: EditorOptions).\n\t\t// Casts keep the inline types from leaking into the real base signature.\n\t\tsuper(tui as unknown as never, theme as unknown as never, keybindings as unknown as never, {\n\t\t\tpaddingX: 0,\n\t\t});\n\t\tthis.ctx = ctx;\n\t\tthis.pi = pi;\n\t\t// lunr: do not clobber borderColor — InteractiveMode.updateEditorBorderColor()\n\t\t// owns it (thinkingOff…thinkingMax, or bashMode).\n\t}\n\n\tprivate color(token: string, text: string): string {\n\t\treturn color(this.ctx.ui?.theme, token, text);\n\t}\n\n\toverride render(width: number): string[] {\n\t\t// Too narrow to draw a box — defer to the base editor, clamped.\n\t\tif (width <= 4) {\n\t\t\treturn clampLines(super.render(width), width);\n\t\t}\n\n\t\t// Rails: `│ ` (left) + ` │` (right) => 4 columns of chrome.\n\t\tconst glyph = lunrPromptGlyph(this.ctx.ui?.theme);\n\t\tconst glyphW = displayWidth(glyph);\n\t\tconst innerWidth = Math.max(1, width - 4 - glyphW);\n\t\tconst base = super.render(innerWidth);\n\n\t\t// The base editor appends the autocomplete menu lines (if any) to the END\n\t\t// of its render output. Split them off so the body sits inside the box and\n\t\t// the autocomplete renders BELOW the bottom border (preserves slash\n\t\t// commands). Mirrors zentui's `renderPolishedFrame`.\n\t\tconst showing = this.isShowingAutocomplete();\n\t\tconst acList = (this as unknown as { autocompleteList?: AutocompleteListLike }).autocompleteList;\n\t\tlet acCount = 0;\n\t\tif (showing && acList && typeof acList.render === \"function\") {\n\t\t\ttry {\n\t\t\t\tacCount = acList.render(innerWidth).length;\n\t\t\t} catch {\n\t\t\t\tacCount = 0;\n\t\t\t}\n\t\t}\n\n\t\t// `super.render` returns [base top ─ border, ...wrapped text lines, base\n\t\t// bottom ─ border, ...autocomplete lines] — the base editor draws its OWN\n\t\t// straight borders and (when active) appends the autocomplete menu. Split the\n\t\t// autocomplete off the end, then STRIP the base's first/last lines (its own\n\t\t// borders) so we don't render them a second time inside our rounded frame.\n\t\t// Mirrors zentui's `editorFrame.slice(1, -1)` (renderPolishedFrame).\n\t\tlet frame = base;\n\t\tlet acLines: string[] = [];\n\t\tif (acCount > 0 && acCount < frame.length) {\n\t\t\tacLines = frame.slice(frame.length - acCount);\n\t\t\tframe = frame.slice(0, frame.length - acCount);\n\t\t}\n\t\tconst inner = frame.length >= 2 ? frame.slice(1, frame.length - 1) : frame;\n\t\tconst body = inner.length > 0 ? inner : [\"\"];\n\n\t\tconst border = (s: string): string => this.borderColor(s);\n\n\t\t// Top border: ╭─…─╮\n\t\tconst top = border(\"\\u256d\" + \"\\u2500\".repeat(width - 2) + \"\\u256e\");\n\n\t\t// Body: │ <padded line> │ (auto-grows with the number of wrapped lines)\n\t\t// lunr: prefix the first body line with the theme-controlled prompt arrow.\n\t\t// A same-width blank gutter keeps subsequent lines aligned.\n\t\tconst gutter = glyphW > 0 ? \" \".repeat(glyphW) : \"\";\n\t\tconst bodyLines = body.map((ln: string, i: number) => {\n\t\t\tconst prefix = i === 0 && glyphW > 0 ? this.color(\"dim\", glyph) : gutter;\n\t\t\treturn border(\"\\u2502 \") + prefix + padRight(ln, innerWidth) + border(\" \\u2502\");\n\t\t});\n\n\t\t// Bottom border with the right-aligned chip: ╰─…─ <chip> ─╯\n\t\tconst bottom = this.renderBottomBorder(width);\n\n\t\treturn [top, ...bodyLines, bottom, ...acLines];\n\t}\n\n\tprivate renderBottomBorder(width: number): string {\n\t\tconst border = (s: string): string => this.borderColor(s);\n\t\tconst parts = buildChip(this.ctx, this.pi);\n\t\tconst plain = parts.left + parts.effort;\n\t\tconst overhead = 4;\n\t\tconst maxChip = Math.max(0, width - overhead);\n\t\tconst truncated = displayWidth(plain) > maxChip;\n\t\tconst chipPlain = truncated ? truncatePlain(plain, maxChip) : plain;\n\t\tconst chipW = displayWidth(chipPlain);\n\t\tconst dashTotal = Math.max(0, width - overhead - chipW);\n\t\t// Right-align the chip: a single trailing dash before the corner, the\n\t\t// rest lead (right-aligns the chip near the corner).\n\t\tconst rightDashes = Math.min(dashTotal, 1);\n\t\tconst leftDashes = dashTotal - rightDashes;\n\n\t\tconst left = \"\\u2570\" + \"\\u2500\".repeat(leftDashes) + \" \";\n\t\tconst right = \" \" + \"\\u2500\".repeat(rightDashes) + \"\\u256f\";\n\t\tconst theme = this.ctx.ui?.theme;\n\t\t// Truncated chips may have lost the effort token — paint all white.\n\t\t// Otherwise model/provider stay white and the effort uses the thinking token.\n\t\tconst chip = truncated\n\t\t\t? this.color(\"white\", chipPlain)\n\t\t\t: this.color(\"white\", parts.left) + colorEffort(theme, parts.level, parts.effort);\n\t\treturn border(left) + chip + border(right);\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// The stats line (footer) — slim, single line, BELOW the box\n// ---------------------------------------------------------------------------\n\nexport function renderStatsLine(\n\twidth: number,\n\tctx: ExtensionContextLike,\n\ttheme: Theme,\n\tfooterData: ReadonlyFooterDataProvider,\n): string[] {\n\tconst sep = color(theme, \"accent2\", \" | \"); // lunr: theme-polish — separator uses subtle accent2 blue (was bright-black plain fallback)\n\tconst parts: string[] = [];\n\n\t// lunr: mode zone — permission mode + agent-mode statuses (plan/goal/swarm)\n\t// render as ONE segment (dim middot-separated), not scattered across piped sections.\n\tconst modeZone: string[] = [];\n\n\t// lunr: permission mode safety indicator — always shown (not toggle-gated).\n\tconst mode = lunrPermissionMode();\n\tif (mode === \"yolo\" || mode === \"auto\") modeZone.push(color(theme, \"warning\", mode));\n\telse if (mode === \"manual\" || mode === \"plan\") modeZone.push(color(theme, \"white\", mode)); // lunr: theme-polish — manual/plan read white (was dim)\n\n\t// lunr: footer element toggles from the customize bridge (read at render time).\n\tconst footerToggles = lunrFooterToggles();\n\n\t// 1) Extension statuses published by other extensions (rendered as-is,\n\t// already styled): LSP (`pi-lsp-extension`), MCP (`pi-mcp-adapter`),\n\t// throughput/tokens (`pi-tps-was-taken`). These are NOT on `ctx` directly.\n\tconst statuses = footerData.getExtensionStatuses?.();\n\tif (statuses && statuses.size > 0) {\n\t\t// lunr: status segments are toggle-gated via the customize bridge:\n\t\t// footerStatuses gates plan/goal/swarm, footerTps gates tps,\n\t\t// footerMcp gates mcp/mcp-auth, footerLsp gates lsp. Publishers keep\n\t\t// calling ctx.ui.setStatus harmlessly when their segment is hidden.\n\t\t// lunr: plan/goal/swarm join the mode zone; tps/mcp/lsp stay piped.\n\t\tconst modeKeys: string[] = footerToggles.statuses ? [\"plan\", \"goal\", \"swarm\"] : [];\n\t\tfor (const key of modeKeys) {\n\t\t\tconst v = statuses.get(key);\n\t\t\tif (v) modeZone.push(color(theme, \"white\", stripAnsi(v)));\n\t\t}\n\t\tconst pipedKeys: string[] = [];\n\t\tif (footerToggles.tps) pipedKeys.push(\"tps\");\n\t\tif (footerToggles.mcp) pipedKeys.push(\"mcp\", \"mcp-auth\");\n\t\tif (footerToggles.lsp) pipedKeys.push(\"lsp\");\n\t\tfor (const key of pipedKeys) {\n\t\t\tconst v = statuses.get(key);\n\t\t\t// lunr: unify footer status colors (white, was dim) so the whole stats line reads as one tone.\n\t\t\tif (v) parts.push(color(theme, \"white\", stripAnsi(v))); // lunr: theme-polish — status segments white (was dim)\n\t\t}\n\t}\n\tif (modeZone.length > 0) {\n\t\tparts.unshift(modeZone.join(color(theme, \"dim\", \" · \")));\n\t}\n\n\t// lunr: git branch + working-tree +/- vs HEAD (gated on footerGit).\n\tif (footerToggles.git) {\n\t\tconst branch = footerData.getGitBranch?.();\n\t\tif (branch) {\n\t\t\tconst diff = footerData.getGitDiffstat?.();\n\t\t\tlet gitSeg = color(theme, \"white\", branch);\n\t\t\tif (diff && (diff.added !== 0 || diff.removed !== 0)) {\n\t\t\t\tgitSeg +=\n\t\t\t\t\t` ${color(theme, \"success\", `+${diff.added}`)} ${color(theme, \"error\", `-${diff.removed}`)}`;\n\t\t\t}\n\t\t\tparts.push(gitSeg);\n\t\t}\n\t}\n\n\t// lunr: live subscription window bar (gated on footerPlan). Hidden with no plan.\n\tif (footerToggles.plan) {\n\t\tconst provider = ctx.model?.provider;\n\t\tif (provider) {\n\t\t\tconst usage = lunrUsageBridge();\n\t\t\tusage?.prefetch?.(provider);\n\t\t\tconst seg = usage?.pickForFooter?.(provider);\n\t\t\tif (seg) {\n\t\t\t\tconst pct = `${Math.round(seg.usedPercent)}%`;\n\t\t\t\tconst barColor = seg.usedPercent > 90 ? \"error\" : seg.usedPercent > 70 ? \"warning\" : \"success\";\n\t\t\t\tconst bar = footerToggles.planBar ? `${compactPlanBar(seg.usedPercent, theme)} ` : \"\";\n\t\t\t\tparts.push(color(theme, \"white\", seg.label) + \" \" + bar + color(theme, barColor, pct));\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2) Context usage: `pct/window` (lunr: gated on footerContext).\n\tif (footerToggles.context) {\n\t\tconst usage = ctx.getContextUsage();\n\t\tconst win = ctx.model?.contextWindow ?? usage?.contextWindow ?? 0;\n\t\tconst pct = usage?.percent == null ? \"?\" : `${Math.round(usage.percent)}%`;\n\t\tconst ctxSeg = `${pct}/${formatCount(win)}`;\n\t\tconst pv = usage?.percent ?? 0;\n\t\tif (pv > 90) parts.push(color(theme, \"error\", ctxSeg));\n\t\telse if (pv > 70) parts.push(color(theme, \"warning\", ctxSeg));\n\t\telse parts.push(color(theme, \"white\", ctxSeg)); // lunr: theme-polish — normal context % white (was dim)\n\t}\n\n\tconst totals = getUsageTotals(ctx);\n\n\t// 3) Token totals: ↑in ↓out (lunr: gated on footerTokens).\n\tif (footerToggles.tokens) {\n\t\tparts.push(color(theme, \"white\", `\\u2191${formatCount(totals.input)} \\u2193${formatCount(totals.output)}`)); // lunr: theme-polish — token totals white (was dim)\n\t}\n\n\t// 4) Latest prompt cache hit rate (gated on footerCacheHitRate).\n\tif (footerToggles.cacheHitRate && totals.cacheActivity && totals.latestCacheHitRate !== undefined) {\n\t\tparts.push(color(theme, \"white\", `CH${totals.latestCacheHitRate.toFixed(1)}%`));\n\t}\n\n\t// lunr: cost/usage counter segment removed entirely (both the $x.xxx dollar\n\t// counter for API-key providers and the plan-usage limit bar for OAuth/subscription\n\t// providers). Segments 1-3 (statuses, context, tokens) remain.\n\n\tlet line = parts.join(sep);\n\tif (displayWidth(line) > width) {\n\t\tline = truncateToWidth(line, width, \"\");\n\t}\n\treturn [line];\n}\n\n// ---------------------------------------------------------------------------\n// Install / teardown\n// ---------------------------------------------------------------------------\n\nconst chatboxEditors = new WeakMap<TUIHandle, ChatboxEditor>();\n\n/** The startup view and extension bind the same editor, preserving its complete draft state. */\nexport function getChatboxEditor(\n\ttui: TUIHandle,\n\ttheme: EditorThemeLike,\n\tkeybindings: KeybindingsLike,\n\tctx: ExtensionContextLike,\n\tpi: ExtensionAPI,\n): ChatboxEditor {\n\tlet editor = chatboxEditors.get(tui);\n\tif (editor) editor.bind(ctx, pi);\n\telse {\n\t\teditor = new ChatboxEditor(tui, theme, keybindings, ctx, pi);\n\t\tchatboxEditors.set(tui, editor);\n\t}\n\treturn editor;\n}\n\nfunction installEditor(\n\tctx: ExtensionContextLike,\n\tpi: ExtensionAPI,\n\tsetRequestor: (r: () => void) => void,\n): (() => void) | undefined {\n\tif (typeof ctx.ui.setEditorComponent !== \"function\") return;\n\tlet editor: ChatboxEditor | undefined;\n\tconst factory: EditorFactoryLike = (tui, _theme, _keybindings) => {\n\t\tsetRequestor(() => {\n\t\t\ttry {\n\t\t\t\ttui.requestRender();\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t});\n\t\teditor = getChatboxEditor(tui, _theme, _keybindings, ctx, pi);\n\t\treturn editor;\n\t};\n\tctx.ui.setEditorComponent(factory);\n\treturn () => editor?.unbind();\n}\n\nfunction installFooter(\n\tctx: ExtensionContextLike,\n\tsetRequestor: (r: () => void) => void,\n): void {\n\tif (typeof ctx.ui.setFooter !== \"function\") return;\n\tlet disposed = false;\n\tconst factory: FooterFactoryLike = (tui, _theme, _footerData) => {\n\t\tsetRequestor(() => {\n\t\t\ttry {\n\t\t\t\ttui.requestRender();\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t});\n\t\treturn {\n\t\t\tinvalidate(): void {\n\t\t\t\t// render is pure\n\t\t\t},\n\t\t\tdispose(): void {\n\t\t\t\tdisposed = true;\n\t\t\t},\n\t\t\trender(width: number): string[] {\n\t\t\t\tif (disposed) return [];\n\t\t\t\treturn renderStatsLine(width, ctx, _theme, _footerData);\n\t\t\t},\n\t\t};\n\t};\n\tctx.ui.setFooter(factory);\n}\n\n// ---------------------------------------------------------------------------\n// Extension entry point\n// ---------------------------------------------------------------------------\n\nexport default function (pi: ExtensionAPI): void {\n\tlet unbindEditor: (() => void) | undefined;\n\tlet renderRequestor: (() => void) | undefined = undefined;\n\tconst requestRender = (): void => {\n\t\ttry {\n\t\t\trenderRequestor?.();\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t};\n\n\tpi.on(\"session_start\", (_event, ctx) => {\n\t\tif (!ctx.hasUI || ctx.mode !== \"tui\") return;\n\t\tunbindEditor = installEditor(ctx, pi, (r) => {\n\t\t\trenderRequestor = r;\n\t\t});\n\t\tinstallFooter(ctx, (r) => {\n\t\t\tif (!renderRequestor) renderRequestor = r;\n\t\t});\n\t});\n\n\tpi.on(\"session_shutdown\", (_event, ctx) => {\n\t\tunbindEditor?.();\n\t\tunbindEditor = undefined;\n\t\ttry {\n\t\t\tif (typeof ctx.ui?.setEditorComponent === \"function\") ctx.ui.setEditorComponent(undefined);\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t\ttry {\n\t\t\tif (typeof ctx.ui?.setFooter === \"function\") ctx.ui.setFooter(undefined);\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t\trenderRequestor = undefined;\n\t});\n\n\t// Re-render triggers: model/provider + effort (chip), and token/cost/context\n\t// (stats line). Event names match those zentui wires.\n\tconst reRenderEvents = [\n\t\t\"model_select\",\n\t\t\"thinking_level_select\",\n\t\t\"agent_end\",\n\t\t\"message_end\",\n\t\t\"tool_execution_end\",\n\t\t\"session_compact\",\n\t];\n\tfor (const ev of reRenderEvents) {\n\t\tpi.on(ev, () => requestRender());\n\t}\n}\n"]}
@@ -31,18 +31,14 @@
31
31
  * under vite-node (`Class extends value undefined`).
32
32
  */
33
33
  import { CustomEditor } from "../modes/interactive/components/custom-editor.js";
34
- // lunr: customize bridge — read the prompt-symbol toggle (bridgeless → no symbol).
35
- const PROMPT_SYMBOL_BRIDGE = Symbol.for("@lunr/customize");
34
+ const CUSTOMIZE_BRIDGE = Symbol.for("@lunr/customize");
36
35
  const PERMISSION_MODE_BRIDGE = Symbol.for("@lunr/permission-mode");
37
36
  function lunrCustomizeBridge() {
38
- return globalThis[PROMPT_SYMBOL_BRIDGE];
37
+ return globalThis[CUSTOMIZE_BRIDGE];
39
38
  }
40
39
  function lunrPermissionModeBridge() {
41
40
  return globalThis[PERMISSION_MODE_BRIDGE];
42
41
  }
43
- function lunrPromptSymbolEnabled() {
44
- return lunrCustomizeBridge()?.getPromptSymbol() ?? false;
45
- }
46
42
  // lunr: footer toggles, read at render time. Bridgeless / missing-getter fallback
47
43
  // matches the settings-manager defaults: MCP on, LSP off, everything else on.
48
44
  function lunrFooterToggles() {
@@ -77,14 +73,9 @@ function compactPlanBar(percent, theme) {
77
73
  function lunrPermissionMode() {
78
74
  return lunrPermissionModeBridge()?.getMode();
79
75
  }
80
- // lunr: prompt glyph comes from the theme tokens `glyphs.promptMoon` /
81
- // `glyphs.promptArrow` (defaults below match moon.json). An empty token hides
82
- // that part; both empty = no glyph at all.
83
- function lunrPromptGlyph(theme) {
84
- const moon = theme?.glyph?.("promptMoon") ?? "☾";
76
+ export function lunrPromptGlyph(theme) {
85
77
  const arrow = theme?.glyph?.("promptArrow") ?? ">";
86
- const parts = [moon, arrow].filter((s) => s !== "");
87
- return parts.length > 0 ? `${parts.join(" ")} ` : "";
78
+ return arrow ? `${arrow} ` : "";
88
79
  }
89
80
  /** Theme token for the chatbox thinking-level border / effort chip. */
90
81
  export function thinkingThemeToken(level) {
@@ -366,9 +357,25 @@ function buildChip(ctx, pi) {
366
357
  // ---------------------------------------------------------------------------
367
358
  // The prompt box — `ChatboxEditor extends CustomEditor`
368
359
  // ---------------------------------------------------------------------------
369
- class ChatboxEditor extends CustomEditor {
360
+ export class ChatboxEditor extends CustomEditor {
370
361
  ctx;
371
362
  pi;
363
+ bind(ctx, pi) {
364
+ this.ctx = ctx;
365
+ this.pi = pi;
366
+ }
367
+ /** Keep the displayed values while /new or /resume replaces the old runtime. */
368
+ unbind() {
369
+ const level = this.pi.getThinkingLevel();
370
+ this.bind({
371
+ mode: "tui",
372
+ hasUI: true,
373
+ model: this.ctx.model,
374
+ sessionManager: { getEntries: () => [] },
375
+ getContextUsage: () => undefined,
376
+ ui: { theme: this.ctx.ui?.theme, setEditorComponent() { }, setFooter() { } },
377
+ }, { getThinkingLevel: () => level, on() { } });
378
+ }
372
379
  constructor(tui, theme, keybindings, ctx, pi) {
373
380
  // CustomEditor ctor: (tui, theme, keybindings, options?: EditorOptions).
374
381
  // Casts keep the inline types from leaking into the real base signature.
@@ -389,9 +396,8 @@ class ChatboxEditor extends CustomEditor {
389
396
  return clampLines(super.render(width), width);
390
397
  }
391
398
  // Rails: `│ ` (left) + ` │` (right) => 4 columns of chrome.
392
- const promptSymbol = lunrPromptSymbolEnabled();
393
399
  const glyph = lunrPromptGlyph(this.ctx.ui?.theme);
394
- const glyphW = promptSymbol ? displayWidth(glyph) : 0;
400
+ const glyphW = displayWidth(glyph);
395
401
  const innerWidth = Math.max(1, width - 4 - glyphW);
396
402
  const base = super.render(innerWidth);
397
403
  // The base editor appends the autocomplete menu lines (if any) to the END
@@ -427,8 +433,8 @@ class ChatboxEditor extends CustomEditor {
427
433
  // Top border: ╭─…─╮
428
434
  const top = border("\u256d" + "\u2500".repeat(width - 2) + "\u256e");
429
435
  // Body: │ <padded line> │ (auto-grows with the number of wrapped lines)
430
- // lunr: prefix the first body line with the dim theme prompt glyph (☾ > by
431
- // default); a same-width blank gutter keeps subsequent lines aligned.
436
+ // lunr: prefix the first body line with the theme-controlled prompt arrow.
437
+ // A same-width blank gutter keeps subsequent lines aligned.
432
438
  const gutter = glyphW > 0 ? " ".repeat(glyphW) : "";
433
439
  const bodyLines = body.map((ln, i) => {
434
440
  const prefix = i === 0 && glyphW > 0 ? this.color("dim", glyph) : gutter;
@@ -576,9 +582,22 @@ export function renderStatsLine(width, ctx, theme, footerData) {
576
582
  // ---------------------------------------------------------------------------
577
583
  // Install / teardown
578
584
  // ---------------------------------------------------------------------------
585
+ const chatboxEditors = new WeakMap();
586
+ /** The startup view and extension bind the same editor, preserving its complete draft state. */
587
+ export function getChatboxEditor(tui, theme, keybindings, ctx, pi) {
588
+ let editor = chatboxEditors.get(tui);
589
+ if (editor)
590
+ editor.bind(ctx, pi);
591
+ else {
592
+ editor = new ChatboxEditor(tui, theme, keybindings, ctx, pi);
593
+ chatboxEditors.set(tui, editor);
594
+ }
595
+ return editor;
596
+ }
579
597
  function installEditor(ctx, pi, setRequestor) {
580
598
  if (typeof ctx.ui.setEditorComponent !== "function")
581
599
  return;
600
+ let editor;
582
601
  const factory = (tui, _theme, _keybindings) => {
583
602
  setRequestor(() => {
584
603
  try {
@@ -588,9 +607,11 @@ function installEditor(ctx, pi, setRequestor) {
588
607
  /* ignore */
589
608
  }
590
609
  });
591
- return new ChatboxEditor(tui, _theme, _keybindings, ctx, pi);
610
+ editor = getChatboxEditor(tui, _theme, _keybindings, ctx, pi);
611
+ return editor;
592
612
  };
593
613
  ctx.ui.setEditorComponent(factory);
614
+ return () => editor?.unbind();
594
615
  }
595
616
  function installFooter(ctx, setRequestor) {
596
617
  if (typeof ctx.ui.setFooter !== "function")
@@ -625,6 +646,7 @@ function installFooter(ctx, setRequestor) {
625
646
  // Extension entry point
626
647
  // ---------------------------------------------------------------------------
627
648
  export default function (pi) {
649
+ let unbindEditor;
628
650
  let renderRequestor = undefined;
629
651
  const requestRender = () => {
630
652
  try {
@@ -637,7 +659,7 @@ export default function (pi) {
637
659
  pi.on("session_start", (_event, ctx) => {
638
660
  if (!ctx.hasUI || ctx.mode !== "tui")
639
661
  return;
640
- installEditor(ctx, pi, (r) => {
662
+ unbindEditor = installEditor(ctx, pi, (r) => {
641
663
  renderRequestor = r;
642
664
  });
643
665
  installFooter(ctx, (r) => {
@@ -646,6 +668,8 @@ export default function (pi) {
646
668
  });
647
669
  });
648
670
  pi.on("session_shutdown", (_event, ctx) => {
671
+ unbindEditor?.();
672
+ unbindEditor = undefined;
649
673
  try {
650
674
  if (typeof ctx.ui?.setEditorComponent === "function")
651
675
  ctx.ui.setEditorComponent(undefined);