march-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/bin/march.mjs +13 -0
  2. package/package.json +36 -0
  3. package/src/agent/command-exec-tool.mjs +91 -0
  4. package/src/agent/context-stats-tool.mjs +57 -0
  5. package/src/agent/editing/diff-apply.mjs +28 -0
  6. package/src/agent/editing/diff-format.mjs +57 -0
  7. package/src/agent/file-edit-tool.mjs +276 -0
  8. package/src/agent/find-tool.mjs +112 -0
  9. package/src/agent/model-payload-dumper.mjs +201 -0
  10. package/src/agent/pi-session/pi-session-sidecar-failure.mjs +10 -0
  11. package/src/agent/provider/payload-messages.mjs +138 -0
  12. package/src/agent/read-file-tool.mjs +112 -0
  13. package/src/agent/runner/fast-model.mjs +36 -0
  14. package/src/agent/runner/runner-cleanup.mjs +12 -0
  15. package/src/agent/runner/runner-init.mjs +15 -0
  16. package/src/agent/runner/runner-session-state.mjs +40 -0
  17. package/src/agent/runner.mjs +266 -0
  18. package/src/agent/runtime/runner-runtime-host.mjs +73 -0
  19. package/src/agent/runtime/runtime-factory.mjs +42 -0
  20. package/src/agent/runtime/runtime-host.mjs +34 -0
  21. package/src/agent/session/session-auto-name.mjs +41 -0
  22. package/src/agent/session/session-binding.mjs +12 -0
  23. package/src/agent/session/session-options.mjs +46 -0
  24. package/src/agent/tool-names.mjs +1 -0
  25. package/src/agent/tool-result.mjs +3 -0
  26. package/src/agent/tools.mjs +54 -0
  27. package/src/agent/turn/turn-events.mjs +64 -0
  28. package/src/agent/turn/turn-runner.mjs +103 -0
  29. package/src/auth/login-command.mjs +90 -0
  30. package/src/auth/storage.mjs +33 -0
  31. package/src/cli/args.mjs +71 -0
  32. package/src/cli/commands/copy-command.mjs +73 -0
  33. package/src/cli/commands/export-command.mjs +206 -0
  34. package/src/cli/commands/extensions-command.mjs +53 -0
  35. package/src/cli/commands/help-command.mjs +7 -0
  36. package/src/cli/commands/model-command.mjs +110 -0
  37. package/src/cli/commands/paste-image-command.mjs +43 -0
  38. package/src/cli/commands/provider-command.mjs +55 -0
  39. package/src/cli/commands/status-command.mjs +157 -0
  40. package/src/cli/commands/thinking-command.mjs +80 -0
  41. package/src/cli/fallback-ui.mjs +156 -0
  42. package/src/cli/input/attachment-tokens.mjs +20 -0
  43. package/src/cli/input/autocomplete.mjs +106 -0
  44. package/src/cli/input/external-editor.mjs +39 -0
  45. package/src/cli/input/history-store.mjs +35 -0
  46. package/src/cli/input/image-clipboard.mjs +55 -0
  47. package/src/cli/input/keybinding-dispatch.mjs +76 -0
  48. package/src/cli/input/keybindings.mjs +96 -0
  49. package/src/cli/input/mode-state.mjs +43 -0
  50. package/src/cli/input/prompt-templates.mjs +84 -0
  51. package/src/cli/input/select-with-keyboard.mjs +67 -0
  52. package/src/cli/permissions.mjs +103 -0
  53. package/src/cli/repl-commands.mjs +86 -0
  54. package/src/cli/repl-loop.mjs +157 -0
  55. package/src/cli/selector-list.mjs +21 -0
  56. package/src/cli/session/pi-session-switch-command.mjs +41 -0
  57. package/src/cli/session/session-command.mjs +23 -0
  58. package/src/cli/session/session-list-command.mjs +68 -0
  59. package/src/cli/session/session-name-command.mjs +26 -0
  60. package/src/cli/session/session-source-command.mjs +89 -0
  61. package/src/cli/session/session-switch-command.mjs +1 -0
  62. package/src/cli/shell/shell-command.mjs +55 -0
  63. package/src/cli/shell/shell-drawer-controls.mjs +33 -0
  64. package/src/cli/shell/shell-drawer.mjs +192 -0
  65. package/src/cli/shell/shell-split-layout.mjs +70 -0
  66. package/src/cli/slash-commands.mjs +176 -0
  67. package/src/cli/startup/startup-banner.mjs +17 -0
  68. package/src/cli/startup/startup-session.mjs +51 -0
  69. package/src/cli/status-line-updater.mjs +74 -0
  70. package/src/cli/tool-output.mjs +9 -0
  71. package/src/cli/tui/editor/external-editor-runner.mjs +24 -0
  72. package/src/cli/tui/input/mouse-selection-controller.mjs +89 -0
  73. package/src/cli/tui/input/mouse-tracking.mjs +20 -0
  74. package/src/cli/tui/layout/main-pane-layout.mjs +38 -0
  75. package/src/cli/tui/layout/safe-render-boundary.mjs +46 -0
  76. package/src/cli/tui/markdown-renderer.mjs +279 -0
  77. package/src/cli/tui/output/scroll-state.mjs +79 -0
  78. package/src/cli/tui/output/tool-card-renderer.mjs +59 -0
  79. package/src/cli/tui/output-buffer.mjs +297 -0
  80. package/src/cli/tui/permission-request-ui.mjs +18 -0
  81. package/src/cli/tui/recall-rendering.mjs +25 -0
  82. package/src/cli/tui/select/editor-select-list.mjs +111 -0
  83. package/src/cli/tui/selection-screen.mjs +212 -0
  84. package/src/cli/tui/status/retry-status.mjs +72 -0
  85. package/src/cli/tui/status/spinner-status.mjs +42 -0
  86. package/src/cli/tui/status/status-bar.mjs +88 -0
  87. package/src/cli/tui/syntax/highlighting.mjs +277 -0
  88. package/src/cli/tui/syntax/languages.mjs +91 -0
  89. package/src/cli/tui/syntax/tree-sitter/bash.highlights.scm +261 -0
  90. package/src/cli/tui/syntax/tree-sitter/c.highlights.scm +341 -0
  91. package/src/cli/tui/syntax/tree-sitter/cpp.highlights.scm +268 -0
  92. package/src/cli/tui/syntax/tree-sitter/csharp.highlights.scm +577 -0
  93. package/src/cli/tui/syntax/tree-sitter/css.highlights.scm +109 -0
  94. package/src/cli/tui/syntax/tree-sitter/diff.highlights.scm +49 -0
  95. package/src/cli/tui/syntax/tree-sitter/go.highlights.scm +254 -0
  96. package/src/cli/tui/syntax/tree-sitter/html.highlights.scm +13 -0
  97. package/src/cli/tui/syntax/tree-sitter/java.highlights.scm +330 -0
  98. package/src/cli/tui/syntax/tree-sitter/json.highlights.scm +38 -0
  99. package/src/cli/tui/syntax/tree-sitter/php.highlights.scm +203 -0
  100. package/src/cli/tui/syntax/tree-sitter/python.highlights.scm +137 -0
  101. package/src/cli/tui/syntax/tree-sitter/ruby.highlights.scm +309 -0
  102. package/src/cli/tui/syntax/tree-sitter/rust.highlights.scm +531 -0
  103. package/src/cli/tui/syntax/tree-sitter/toml.highlights.scm +39 -0
  104. package/src/cli/tui/syntax/tree-sitter/tree-sitter-bash.wasm +0 -0
  105. package/src/cli/tui/syntax/tree-sitter/tree-sitter-c-sharp.wasm +0 -0
  106. package/src/cli/tui/syntax/tree-sitter/tree-sitter-c.wasm +0 -0
  107. package/src/cli/tui/syntax/tree-sitter/tree-sitter-cpp.wasm +0 -0
  108. package/src/cli/tui/syntax/tree-sitter/tree-sitter-css.wasm +0 -0
  109. package/src/cli/tui/syntax/tree-sitter/tree-sitter-diff.wasm +0 -0
  110. package/src/cli/tui/syntax/tree-sitter/tree-sitter-go.wasm +0 -0
  111. package/src/cli/tui/syntax/tree-sitter/tree-sitter-html.wasm +0 -0
  112. package/src/cli/tui/syntax/tree-sitter/tree-sitter-java.wasm +0 -0
  113. package/src/cli/tui/syntax/tree-sitter/tree-sitter-json.wasm +0 -0
  114. package/src/cli/tui/syntax/tree-sitter/tree-sitter-php.wasm +0 -0
  115. package/src/cli/tui/syntax/tree-sitter/tree-sitter-python.wasm +0 -0
  116. package/src/cli/tui/syntax/tree-sitter/tree-sitter-ruby.wasm +0 -0
  117. package/src/cli/tui/syntax/tree-sitter/tree-sitter-rust.wasm +0 -0
  118. package/src/cli/tui/syntax/tree-sitter/tree-sitter-toml.wasm +0 -0
  119. package/src/cli/tui/syntax/tree-sitter/tree-sitter-tsx.wasm +0 -0
  120. package/src/cli/tui/syntax/tree-sitter/tree-sitter-typescript.wasm +0 -0
  121. package/src/cli/tui/syntax/tree-sitter/tree-sitter-yaml.wasm +0 -0
  122. package/src/cli/tui/syntax/tree-sitter/tsx.highlights.scm +35 -0
  123. package/src/cli/tui/syntax/tree-sitter/typescript.highlights.scm +35 -0
  124. package/src/cli/tui/syntax/tree-sitter/yaml.highlights.scm +99 -0
  125. package/src/cli/tui/tool-rendering.mjs +194 -0
  126. package/src/cli/tui/tui-diff-rendering.mjs +157 -0
  127. package/src/cli/tui/tui-handlers.mjs +110 -0
  128. package/src/cli/tui/tui-input-controller.mjs +61 -0
  129. package/src/cli/tui/ui-theme.mjs +148 -0
  130. package/src/cli/ui.mjs +299 -0
  131. package/src/config/config-json.mjs +73 -0
  132. package/src/config/dotenv.mjs +20 -0
  133. package/src/config/features.mjs +75 -0
  134. package/src/config/loader.mjs +109 -0
  135. package/src/config/settings-command.mjs +97 -0
  136. package/src/context/diagnostics.mjs +70 -0
  137. package/src/context/engine.mjs +148 -0
  138. package/src/context/injections.mjs +26 -0
  139. package/src/context/project-context.mjs +20 -0
  140. package/src/context/session-status.mjs +15 -0
  141. package/src/context/shell-layers.mjs +23 -0
  142. package/src/context/system-core/base.md +60 -0
  143. package/src/context/system-core/prompts/deepseek-v4-pro.md +3 -0
  144. package/src/context/system-core/prompts/default.md +3 -0
  145. package/src/context/system-core.mjs +35 -0
  146. package/src/debug/model-context-dumper.mjs +52 -0
  147. package/src/extensions/discovery.mjs +40 -0
  148. package/src/extensions/lifecycle-adapter.mjs +210 -0
  149. package/src/extensions/lifecycle-manifest.mjs +69 -0
  150. package/src/image-gen/index.mjs +7 -0
  151. package/src/image-gen/provider.mjs +231 -0
  152. package/src/image-gen/tool.mjs +84 -0
  153. package/src/lsp/client.mjs +204 -0
  154. package/src/lsp/diagnostic-store.mjs +39 -0
  155. package/src/lsp/servers.mjs +212 -0
  156. package/src/lsp/service.mjs +65 -0
  157. package/src/main.mjs +294 -0
  158. package/src/mcp/client.mjs +195 -0
  159. package/src/mcp/config.mjs +130 -0
  160. package/src/mcp/index.mjs +48 -0
  161. package/src/mcp/tools.mjs +98 -0
  162. package/src/memory/database.mjs +219 -0
  163. package/src/memory/glossary.mjs +124 -0
  164. package/src/memory/graph/graph-cascades.mjs +109 -0
  165. package/src/memory/graph/graph-diagnostics.mjs +73 -0
  166. package/src/memory/graph/graph-path-removal.mjs +50 -0
  167. package/src/memory/graph/graph-path-utils.mjs +17 -0
  168. package/src/memory/graph/graph-primitives.mjs +103 -0
  169. package/src/memory/graph/graph-read.mjs +159 -0
  170. package/src/memory/graph.mjs +282 -0
  171. package/src/memory/markdown/markdown-delete.mjs +23 -0
  172. package/src/memory/markdown/markdown-format.mjs +128 -0
  173. package/src/memory/markdown/markdown-recall.mjs +28 -0
  174. package/src/memory/markdown/ripgrep.mjs +16 -0
  175. package/src/memory/markdown/sqlite-index.mjs +87 -0
  176. package/src/memory/markdown-store.mjs +286 -0
  177. package/src/memory/markdown-tools.mjs +103 -0
  178. package/src/memory/search.mjs +142 -0
  179. package/src/memory/snapshot.mjs +86 -0
  180. package/src/memory/system-views.mjs +120 -0
  181. package/src/memory/tools.mjs +282 -0
  182. package/src/notification/desktop-notifier.mjs +85 -0
  183. package/src/platform/open-file.mjs +28 -0
  184. package/src/provider/config-command.mjs +129 -0
  185. package/src/provider/presets.mjs +72 -0
  186. package/src/session/attachment-display.mjs +16 -0
  187. package/src/session/attachment-references.mjs +65 -0
  188. package/src/session/attachments.mjs +140 -0
  189. package/src/session/persist.mjs +1 -0
  190. package/src/session/pi-manager.mjs +34 -0
  191. package/src/session/session-utils.mjs +16 -0
  192. package/src/session/sidecar-sync.mjs +19 -0
  193. package/src/session/sidecar.mjs +68 -0
  194. package/src/session/transcript.mjs +83 -0
  195. package/src/session/tree.mjs +42 -0
  196. package/src/shell/cli-runtime.mjs +11 -0
  197. package/src/shell/hints.mjs +12 -0
  198. package/src/shell/node-pty-adapter.mjs +81 -0
  199. package/src/shell/runtime-state.mjs +126 -0
  200. package/src/shell/runtime.mjs +244 -0
  201. package/src/shell/screen-buffer.mjs +136 -0
  202. package/src/shell/tool-read.mjs +74 -0
  203. package/src/shell/tools.mjs +299 -0
  204. package/src/supergrok/actions/image-generate.mjs +60 -0
  205. package/src/supergrok/actions/search.mjs +78 -0
  206. package/src/supergrok/auth.mjs +36 -0
  207. package/src/supergrok/constants.mjs +18 -0
  208. package/src/supergrok/oauth-provider.mjs +278 -0
  209. package/src/supergrok/provider.mjs +36 -0
  210. package/src/supergrok/response.mjs +76 -0
  211. package/src/supergrok/tool.mjs +61 -0
  212. package/src/text/ansi.mjs +3 -0
  213. package/src/web/config-command.mjs +43 -0
  214. package/src/web/fetch.mjs +78 -0
  215. package/src/web/presets.mjs +16 -0
  216. package/src/web/search.mjs +83 -0
  217. package/src/web/tools.mjs +107 -0
@@ -0,0 +1,297 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+ import { R, brightBlack, dim } from "./ui-theme.mjs";
3
+ import { renderToolCardBlock } from "./output/tool-card-renderer.mjs";
4
+ import { renderMarkdown, renderStreamingMarkdown } from "./markdown-renderer.mjs";
5
+ import { renderEditDiffBlock } from "./tui-diff-rendering.mjs";
6
+ import { OutputScrollState } from "./output/scroll-state.mjs";
7
+
8
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
9
+
10
+ function wrapLine(text, maxWidth) {
11
+ if (maxWidth <= 0) return [""];
12
+ const result = [];
13
+ let cur = "";
14
+ let curW = 0;
15
+ let activeSgr = "";
16
+ for (let i = 0; i < text.length;) {
17
+ if (text[i] === "\x1b") {
18
+ const match = text.slice(i).match(/^\x1b\[[0-?]*[ -/]*[@-~]/);
19
+ if (match) {
20
+ const seq = match[0];
21
+ cur += seq;
22
+ activeSgr = updateActiveSgr(activeSgr, seq);
23
+ i += seq.length;
24
+ continue;
25
+ }
26
+ }
27
+ const ch = text[i];
28
+ const w = visibleWidth(ch);
29
+ if (curW + w > maxWidth) {
30
+ result.push(activeSgr ? `${cur}${R}` : cur);
31
+ cur = activeSgr + ch;
32
+ curW = w;
33
+ } else {
34
+ cur += ch;
35
+ curW += w;
36
+ }
37
+ i += 1;
38
+ }
39
+ if (cur) result.push(cur);
40
+ return result.length > 0 ? result : [""];
41
+ }
42
+
43
+ function updateActiveSgr(activeSgr, seq) {
44
+ if (!seq.endsWith("m")) return activeSgr;
45
+ const body = seq.slice(2, -1);
46
+ if (body === "" || body.split(";").includes("0")) return "";
47
+ return seq;
48
+ }
49
+
50
+ function appendTextLines(lines, textLines, width) {
51
+ for (const line of textLines) {
52
+ for (const part of String(line ?? "").split(/\r?\n/)) {
53
+ for (const wrapped of wrapLine(part, width)) lines.push(wrapped);
54
+ }
55
+ }
56
+ }
57
+
58
+ function currentTextToBlocks(textLines, sealed, cache = null) {
59
+ const blocks = [];
60
+ for (let i = 0; i < textLines.length;) {
61
+ const markdown = textLines[i].markdown;
62
+ const batch = [];
63
+ while (i < textLines.length && textLines[i].markdown === markdown) {
64
+ batch.push(textLines[i].text);
65
+ i += 1;
66
+ }
67
+ blocks.push(markdown
68
+ ? { type: "markdown", text: batch.join("\n"), sealed, cache: sealed ? new Map() : (cache ?? new Map()) }
69
+ : { type: "plain", lines: batch });
70
+ }
71
+ return blocks;
72
+ }
73
+
74
+ function renderBlock(block, width) {
75
+ if (block.type === "diff") return renderEditDiffBlock(block, width);
76
+ if (block.type === "tool-card") return renderToolCardBlock(block, width);
77
+ if (block.type === "plain" || block.type === "tool" || block.type === "status") return renderPlainBlock(block, width);
78
+ if (block.type === "markdown") return renderMarkdownBlock(block, width);
79
+ if (block.type === "thinking") return renderThinkingBlock(block, width);
80
+ return [];
81
+ }
82
+
83
+ function renderPlainBlock(block, width) {
84
+ const lines = [];
85
+ appendTextLines(lines, block.lines, width);
86
+ return lines;
87
+ }
88
+
89
+ function renderMarkdownBlock(block, width) {
90
+ if (!block.sealed) return renderStreamingMarkdown(block.text, width, block.cache);
91
+ const cached = block.cache.get(width);
92
+ if (cached) return cached;
93
+ const rendered = renderMarkdown(block.text, width);
94
+ block.cache.set(width, rendered);
95
+ return rendered;
96
+ }
97
+
98
+ function renderThinkingBlock(block, width) {
99
+ const lines = [dim(`· thinking (${block.tokens} tokens)`)];
100
+ const indent = width > 40 ? width - 40 : width - 2;
101
+ const maxContentWidth = Math.max(20, indent);
102
+ for (const line of block.content) {
103
+ for (const w of wrapLine(line, maxContentWidth)) lines.push(dim(` ${w}`));
104
+ }
105
+ return lines;
106
+ }
107
+
108
+
109
+ export class OutputBuffer {
110
+ constructor() {
111
+ this.segments = [];
112
+ this.currentText = [{ text: "", markdown: false }];
113
+ this.currentTextCache = new Map();
114
+ this.spinning = false;
115
+ this.spinnerText = "";
116
+ this.spinnerIdx = 0;
117
+ this._activeThinking = null;
118
+ this.overlayStatus = null;
119
+ this.scrollState = new OutputScrollState();
120
+ }
121
+
122
+ get scrollOffset() {
123
+ return this.scrollState.offset;
124
+ }
125
+
126
+ clear() {
127
+ this.segments = [];
128
+ this.currentText = [{ text: "", markdown: false }];
129
+ this.currentTextCache = new Map();
130
+ this.spinning = false;
131
+ this.spinnerText = "";
132
+ this.spinnerIdx = 0;
133
+ this._activeThinking = null;
134
+ this.overlayStatus = null;
135
+ this.scrollState.clear();
136
+ }
137
+
138
+ write(text) {
139
+ this._writeText(text, false);
140
+ }
141
+
142
+ writeMarkdown(text) {
143
+ this._writeText(text, true);
144
+ }
145
+
146
+ _writeText(text, markdown) {
147
+ this.overlayStatus = null;
148
+ const current = this.currentText.at(-1);
149
+ if (current.markdown !== markdown && current.text !== "") {
150
+ this.currentText.push({ text: "", markdown });
151
+ } else {
152
+ current.markdown = markdown;
153
+ }
154
+ const parts = text.split("\n");
155
+ this.currentText[this.currentText.length - 1].text += parts[0];
156
+ for (let i = 1; i < parts.length; i++) this.currentText.push({ text: parts[i], markdown });
157
+ }
158
+
159
+ writeln(text) {
160
+ this.overlayStatus = null;
161
+ this.currentText[this.currentText.length - 1].text += text;
162
+ this.currentText.push({ text: "", markdown: false });
163
+ }
164
+
165
+ ensureNewline() {
166
+ const current = this.currentText.at(-1);
167
+ if (!current || current.text === "") return false;
168
+ this.currentText.push({ text: "", markdown: false });
169
+ return true;
170
+ }
171
+
172
+ startThinking() {
173
+ this.overlayStatus = null;
174
+ this._flushText();
175
+ const seg = { type: "thinking", tokens: 0, content: [] };
176
+ this.segments.push(seg);
177
+ this._activeThinking = seg;
178
+ }
179
+
180
+ appendThinking(text) {
181
+ if (!this._activeThinking) this.startThinking();
182
+ const parts = text.split("\n");
183
+ const lastIdx = this._activeThinking.content.length - 1;
184
+ if (lastIdx >= 0) this._activeThinking.content[lastIdx] += parts[0];
185
+ else this._activeThinking.content.push(parts[0]);
186
+ for (let i = 1; i < parts.length; i++) this._activeThinking.content.push(parts[i]);
187
+ }
188
+
189
+ endThinking(tokens) {
190
+ if (this._activeThinking) {
191
+ this._activeThinking.tokens = tokens;
192
+ this._activeThinking = null;
193
+ }
194
+ }
195
+
196
+ addThinkingBlock(tokens, content) {
197
+ this.overlayStatus = null;
198
+ this._flushText();
199
+ this.segments.push({ type: "thinking", tokens, content: content.split("\n") });
200
+ }
201
+
202
+ addBlock(block) {
203
+ this.overlayStatus = null;
204
+ this._flushText();
205
+ this.segments.push(block);
206
+ }
207
+
208
+ setOverlayStatus(lines) {
209
+ this.overlayStatus = Array.isArray(lines) ? { type: "status", lines } : null;
210
+ }
211
+
212
+ clearOverlayStatus() {
213
+ this.overlayStatus = null;
214
+ }
215
+
216
+ sealCurrentText() {
217
+ return this._flushText();
218
+ }
219
+
220
+ _flushText() {
221
+ if (this.currentText.length <= 1 && this.currentText[0].text === "") return false;
222
+ this.segments.push(...currentTextToBlocks(this.currentText, true));
223
+ this.currentText = [{ text: "", markdown: false }];
224
+ this.currentTextCache = new Map();
225
+ return true;
226
+ }
227
+
228
+ setSpinner(on, text) {
229
+ this.spinning = on;
230
+ if (text !== undefined) this.spinnerText = text;
231
+ }
232
+
233
+ tick() {
234
+ this.spinnerIdx = (this.spinnerIdx + 1) % SPINNER_FRAMES.length;
235
+ }
236
+
237
+ scroll(delta) {
238
+ return this.scrollState.scroll(delta);
239
+ }
240
+
241
+ getScrollStep() {
242
+ return this.scrollState.getStep();
243
+ }
244
+
245
+ getMaxScrollOffset() {
246
+ return this.scrollState.getMaxOffset();
247
+ }
248
+
249
+ setViewportHeight(height) {
250
+ this.scrollState.setViewportHeight(height);
251
+ }
252
+
253
+ resetScroll() {
254
+ this.scrollState.reset();
255
+ }
256
+
257
+ setToolCardsExpanded(expanded) {
258
+ let changed = false;
259
+ for (const seg of this.segments) {
260
+ if (seg.type !== "tool-card") continue;
261
+ if (seg.expanded === expanded) continue;
262
+ seg.expanded = expanded;
263
+ changed = true;
264
+ }
265
+ return changed;
266
+ }
267
+
268
+ invalidate() {}
269
+
270
+ render(width) {
271
+ const allLines = this._computeLines(width);
272
+ this._cachedTotalLines = allLines.length;
273
+ this.scrollState.setTotalLines(allLines.length);
274
+ const range = this.scrollState.sliceRange();
275
+ if (!range) return allLines;
276
+ const { start, end } = range;
277
+ return allLines.slice(start, end);
278
+ }
279
+
280
+ _computeLines(width) {
281
+ const lines = [];
282
+ for (const seg of this.segments) {
283
+ for (const line of renderBlock(seg, width)) lines.push(line);
284
+ }
285
+ for (const block of currentTextToBlocks(this.currentText, false, this.currentTextCache)) {
286
+ for (const line of renderBlock(block, width)) lines.push(line);
287
+ }
288
+ if (this.overlayStatus) {
289
+ for (const line of renderBlock(this.overlayStatus, width)) lines.push(line);
290
+ }
291
+ if (this.spinning) {
292
+ const frame = SPINNER_FRAMES[this.spinnerIdx];
293
+ lines.push(brightBlack(`${frame} ${this.spinnerText}`));
294
+ }
295
+ return lines;
296
+ }
297
+ }
@@ -0,0 +1,18 @@
1
+ import { permissionLabel } from "../permissions.mjs";
2
+ import { formatToolStartLine } from "./tool-rendering.mjs";
3
+ import { brightBlack, yellow } from "./ui-theme.mjs";
4
+
5
+ export async function requestToolPermission({ toolName, params, category, output, selectList, requestRender }) {
6
+ const label = permissionLabel(category);
7
+ output.writeln(yellow(`● ${toolName} needs ${label} permission`));
8
+ output.writeln(brightBlack(` ${formatToolStartLine(toolName, params)}`));
9
+ requestRender();
10
+ const choice = await selectList({
11
+ items: [
12
+ { label: "Approve once", description: `Allow ${toolName} this time (${label})` },
13
+ { label: "Deny", description: "Block this tool call" },
14
+ ],
15
+ width: 58,
16
+ });
17
+ return choice?.label === "Approve once";
18
+ }
@@ -0,0 +1,25 @@
1
+ import { brightBlack } from "./ui-theme.mjs";
2
+
3
+ const RECALL_ICON = "◈";
4
+
5
+ export function formatMemoryHintLines(hints = []) {
6
+ if (!hints.length) return [];
7
+ if (hints.length === 1) {
8
+ const hint = hints[0];
9
+ return [`${RECALL_ICON} memory hint: ${formatHint(hint)}`];
10
+ }
11
+ return [
12
+ `${RECALL_ICON} memory hint: ${hints.length} memories`,
13
+ ...hints.map((hint) => ` ${formatHint(hint)}`),
14
+ ];
15
+ }
16
+
17
+ export function writeMemoryHint({ output, hints = [] }) {
18
+ for (const line of formatMemoryHintLines(hints)) {
19
+ output.writeln(brightBlack(line));
20
+ }
21
+ }
22
+
23
+ function formatHint(hint) {
24
+ return [hint.id, hint.name].filter(Boolean).join(" ");
25
+ }
@@ -0,0 +1,111 @@
1
+ import { SelectList, fuzzyFilter, matchesKey } from "@earendil-works/pi-tui";
2
+ import { EDITOR_THEME } from "../ui-theme.mjs";
3
+
4
+ export function showEditorSelectList({
5
+ tui,
6
+ editor,
7
+ items,
8
+ selectedIndex = 0,
9
+ maxVisible = 8,
10
+ requestRender,
11
+ suppressInitialLineFeed = false,
12
+ suppressInitialConfirm = false,
13
+ searchable = false,
14
+ getSearchText = defaultSearchText,
15
+ }) {
16
+ if (!Array.isArray(items) || items.length === 0) return Promise.resolve(null);
17
+ return new Promise((resolve) => {
18
+ editor.cancelAutocomplete?.();
19
+ const list = new SelectList(items, maxVisible, EDITOR_THEME.selectList, {
20
+ minPrimaryColumnWidth: 18,
21
+ maxPrimaryColumnWidth: 32,
22
+ });
23
+ let settled = false;
24
+ let removeInputListener = null;
25
+ let query = "";
26
+ const finish = (item) => {
27
+ if (settled) return;
28
+ settled = true;
29
+ removeInputListener?.();
30
+ if (searchable) setEditorText(editor, "");
31
+ editor.autocompleteState = null;
32
+ editor.autocompleteList = undefined;
33
+ requestRender();
34
+ resolve(item);
35
+ };
36
+ list.setSelectedIndex(selectedIndex);
37
+ list.onSelect = (item) => finish(item);
38
+ list.onCancel = () => finish(null);
39
+ editor.autocompleteState = "force";
40
+ editor.autocompleteList = list;
41
+ let isFirstInput = true;
42
+ removeInputListener = tui.addInputListener((data) => {
43
+ if (isFirstInput && (suppressInitialConfirm || suppressInitialLineFeed) && isConfirmInput(data)) {
44
+ isFirstInput = false;
45
+ requestRender();
46
+ return { consume: true };
47
+ }
48
+ isFirstInput = false;
49
+ if (searchable && handleSearchInput(data)) {
50
+ requestRender();
51
+ return { consume: true };
52
+ }
53
+ list.handleInput(data);
54
+ requestRender();
55
+ return { consume: true };
56
+ });
57
+ requestRender();
58
+
59
+ function handleSearchInput(data) {
60
+ if (isBackspace(data)) {
61
+ if (query.length === 0) return true;
62
+ query = query.slice(0, -1);
63
+ applySearch();
64
+ return true;
65
+ }
66
+ const printable = decodeSinglePrintable(data);
67
+ if (printable === undefined) return false;
68
+ query += printable;
69
+ applySearch();
70
+ return true;
71
+ }
72
+
73
+ function applySearch() {
74
+ setEditorText(editor, query);
75
+ list.filteredItems = fuzzyFilter(items, query, getSearchText);
76
+ list.setSelectedIndex(query ? 0 : selectedIndex);
77
+ }
78
+ });
79
+ }
80
+
81
+ function defaultSearchText(item) {
82
+ return `${item?.label ?? ""} ${item?.description ?? ""} ${item?.value ?? ""}`;
83
+ }
84
+
85
+ function isBackspace(data) {
86
+ return data === "\x7f" || data === "\b" || matchesKey(data, "backspace");
87
+ }
88
+
89
+ function isConfirmInput(data) {
90
+ return data === "\r" || data === "\n" || matchesKey(data, "enter") || matchesKey(data, "return");
91
+ }
92
+
93
+ function decodeSinglePrintable(data) {
94
+ if (typeof data !== "string" || data.length !== 1) return undefined;
95
+ const code = data.charCodeAt(0);
96
+ if (code < 32 || code === 127) return undefined;
97
+ return data;
98
+ }
99
+
100
+ function setEditorText(editor, text) {
101
+ if (typeof editor.setTextInternal === "function") {
102
+ editor.setTextInternal(text);
103
+ return;
104
+ }
105
+ if (editor.state) {
106
+ editor.state.lines = [text];
107
+ editor.state.cursorLine = 0;
108
+ editor.state.cursorCol = text.length;
109
+ }
110
+ editor.onChange?.(text);
111
+ }
@@ -0,0 +1,212 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+
3
+ const CONTROL_RE = /\x1b(?:\][^\x07]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/g;
4
+ const INVERSE = "\x1b[7m";
5
+ const RESET = "\x1b[0m";
6
+
7
+ export class ScreenSelection {
8
+ constructor() {
9
+ this.active = false;
10
+ this.anchor = null;
11
+ this.focus = null;
12
+ this.lines = [];
13
+ this.viewport = { topRow: 0, leftCol: 0, width: Infinity, height: 0 };
14
+ }
15
+
16
+ setLines(lines) {
17
+ this.lines = lines.map((line) => stripAnsi(line));
18
+ this.viewport = { topRow: 0, leftCol: 0, width: Infinity, height: this.lines.length };
19
+ }
20
+
21
+ setViewport({ topRow = 0, leftCol = 0, width = Infinity, lines = [] } = {}) {
22
+ this.lines = lines.map((line) => stripAnsi(line));
23
+ this.viewport = {
24
+ topRow: Math.max(0, Math.trunc(topRow)),
25
+ leftCol: Math.max(0, Math.trunc(leftCol)),
26
+ width: Number.isFinite(width) ? Math.max(1, Math.trunc(width)) : Infinity,
27
+ height: this.lines.length,
28
+ };
29
+ }
30
+
31
+ start(point) {
32
+ const normalized = normalizePoint(point, this.viewport, true);
33
+ if (!normalized) {
34
+ this.clear();
35
+ return false;
36
+ }
37
+ this.active = true;
38
+ this.anchor = normalized;
39
+ this.focus = this.anchor;
40
+ return true;
41
+ }
42
+
43
+ update(point) {
44
+ if (!this.active || !this.anchor) return false;
45
+ this.focus = normalizePoint(point, this.viewport, true) ?? this.focus;
46
+ return true;
47
+ }
48
+
49
+ finish(point, { clear = true } = {}) {
50
+ if (!this.active || !this.anchor) return "";
51
+ this.focus = normalizePoint(point, this.viewport, true) ?? this.focus;
52
+ const text = this.text();
53
+ if (clear) this.clear();
54
+ else this.active = false;
55
+ return text;
56
+ }
57
+
58
+ clear() {
59
+ const hadSelection = this.active || Boolean(this.anchor || this.focus);
60
+ this.active = false;
61
+ this.anchor = null;
62
+ this.focus = null;
63
+ return hadSelection;
64
+ }
65
+
66
+ text() {
67
+ const range = this.range();
68
+ if (!range) return "";
69
+ const selected = [];
70
+ for (let row = range.start.row; row <= range.end.row; row += 1) {
71
+ const line = this.lines[row] ?? "";
72
+ const startCol = row === range.start.row ? range.start.col : 0;
73
+ const endCol = row === range.end.row ? range.end.col : visibleWidth(line);
74
+ selected.push(sliceColumns(line, startCol, endCol));
75
+ }
76
+ return selected.join("\n").replace(/[ \t]+$/gm, "").trimEnd();
77
+ }
78
+
79
+ apply(lines) {
80
+ const range = this.range();
81
+ if (!range) return lines;
82
+ return lines.map((line, row) => {
83
+ if (row < range.start.row || row > range.end.row) return line;
84
+ const plain = stripAnsi(line);
85
+ const startCol = row === range.start.row ? range.start.col : 0;
86
+ const endCol = row === range.end.row ? range.end.col : visibleWidth(plain);
87
+ if (endCol <= startCol) return line;
88
+ return highlightAnsiLine(line, startCol, endCol);
89
+ });
90
+ }
91
+
92
+ range() {
93
+ if (!this.anchor || !this.focus) return null;
94
+ const [start, end] = comparePoints(this.anchor, this.focus) <= 0
95
+ ? [this.anchor, this.focus]
96
+ : [this.focus, this.anchor];
97
+ if (start.row === end.row && start.col === end.col) return null;
98
+ return { start, end };
99
+ }
100
+ }
101
+
102
+ export function stripAnsi(text) {
103
+ return String(text ?? "").replace(CONTROL_RE, "");
104
+ }
105
+
106
+ function normalizePoint({ row, col }, viewport, clamp) {
107
+ const screenRow = Math.trunc(row) - 1;
108
+ const screenCol = Math.trunc(col) - 1;
109
+ const height = viewport?.height ?? 0;
110
+ if (height <= 0) return null;
111
+
112
+ let localRow = screenRow - (viewport?.topRow ?? 0);
113
+ let localCol = screenCol - (viewport?.leftCol ?? 0);
114
+ const maxCol = Number.isFinite(viewport?.width) ? viewport.width : Infinity;
115
+ if (!clamp && (localRow < 0 || localRow >= height || localCol < 0 || localCol > maxCol)) return null;
116
+ localRow = clampNumber(localRow, 0, height - 1);
117
+ localCol = clampNumber(localCol, 0, maxCol);
118
+ return { row: localRow, col: localCol };
119
+ }
120
+
121
+ function comparePoints(a, b) {
122
+ if (a.row !== b.row) return a.row - b.row;
123
+ return a.col - b.col;
124
+ }
125
+
126
+ function highlightAnsiLine(line, startCol, endCol) {
127
+ const { before, selected, after, activeAtStart, activeAtEnd } = splitAnsiColumns(line, startCol, endCol);
128
+ return `${before}${INVERSE}${activeAtStart}${keepInverseAfterReset(selected)}${RESET}${activeAtEnd}${after}`;
129
+ }
130
+
131
+ function keepInverseAfterReset(text) {
132
+ return String(text ?? "").replace(/\x1b\[([0-9;]*)m/g, (seq, body) => {
133
+ const params = body === "" ? ["0"] : body.split(";");
134
+ return params.includes("0") ? `${seq}${INVERSE}` : seq;
135
+ });
136
+ }
137
+
138
+ function sliceColumns(text, startCol, endCol) {
139
+ let col = 0;
140
+ let result = "";
141
+ for (const ch of String(text ?? "")) {
142
+ const next = col + visibleWidth(ch);
143
+ if (next > startCol && col < endCol) result += ch;
144
+ col = next;
145
+ if (col >= endCol) break;
146
+ }
147
+ return result;
148
+ }
149
+
150
+ function splitAnsiColumns(text, startCol, endCol) {
151
+ let col = 0;
152
+ let i = 0;
153
+ let before = "";
154
+ let selected = "";
155
+ let after = "";
156
+ let active = "";
157
+ let activeAtStart = "";
158
+ let activeAtEnd = "";
159
+ let capturedStart = false;
160
+ let capturedEnd = false;
161
+ const source = String(text ?? "");
162
+
163
+ while (i < source.length) {
164
+ const ansi = readAnsi(source, i);
165
+ if (ansi) {
166
+ active = updateActiveSgr(active, ansi);
167
+ if (col < startCol) before += ansi;
168
+ else if (col < endCol) selected += ansi;
169
+ else after += ansi;
170
+ i += ansi.length;
171
+ continue;
172
+ }
173
+
174
+ const ch = source[i];
175
+ if (!capturedStart && col >= startCol) {
176
+ activeAtStart = active;
177
+ capturedStart = true;
178
+ }
179
+ if (!capturedEnd && col >= endCol) {
180
+ activeAtEnd = active;
181
+ capturedEnd = true;
182
+ }
183
+
184
+ const next = col + visibleWidth(ch);
185
+ if (next <= startCol) before += ch;
186
+ else if (col >= endCol) after += ch;
187
+ else selected += ch;
188
+ col = next;
189
+ i += 1;
190
+ }
191
+
192
+ if (!capturedStart) activeAtStart = active;
193
+ if (!capturedEnd) activeAtEnd = active;
194
+ return { before, selected, after, activeAtStart, activeAtEnd };
195
+ }
196
+
197
+ function readAnsi(text, offset) {
198
+ if (text[offset] !== "\x1b") return null;
199
+ const match = text.slice(offset).match(/^\x1b(?:\][^\x07]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/);
200
+ return match?.[0] ?? null;
201
+ }
202
+
203
+ function updateActiveSgr(active, seq) {
204
+ if (!seq.startsWith("\x1b[") || !seq.endsWith("m")) return active;
205
+ const body = seq.slice(2, -1);
206
+ if (body === "" || body.split(";").includes("0")) return "";
207
+ return seq;
208
+ }
209
+
210
+ function clampNumber(value, min, max) {
211
+ return Math.min(max, Math.max(min, value));
212
+ }