march-cli 0.1.28 → 0.1.30

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.
@@ -4,12 +4,9 @@ import { renderMarkdown, renderStreamingMarkdown } from "./markdown-renderer.mjs
4
4
  import { renderEditDiffBlock } from "./tui-diff-rendering.mjs";
5
5
  import { OutputScrollState } from "./output/scroll-state.mjs";
6
6
  import { appendTextLines, wrapLine } from "./output/text-line-renderer.mjs";
7
- import { sliceLinesWithTail } from "./output/visible-lines.mjs";
7
+ import { appendSelectableEntries, copySourceTextForRange, sliceEntriesWithTail } from "./output/selectable-copy.mjs";
8
8
 
9
9
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
10
-
11
-
12
-
13
10
  function currentTextToBlocks(textLines, sealed, cache = null) {
14
11
  const blocks = [];
15
12
  for (let i = 0; i < textLines.length;) {
@@ -72,13 +69,11 @@ export class OutputBuffer {
72
69
  this._activeThinking = null;
73
70
  this.overlayStatus = null;
74
71
  this.scrollState = new OutputScrollState();
75
- this._segmentLinesCache = new Map();
76
72
  this._baseLinesCache = new Map();
73
+ this._baseEntriesCache = new Map();
77
74
  }
78
75
 
79
- get scrollOffset() {
80
- return this.scrollState.offset;
81
- }
76
+ get scrollOffset() { return this.scrollState.offset; }
82
77
 
83
78
  clear() {
84
79
  this.segments = [];
@@ -90,27 +85,19 @@ export class OutputBuffer {
90
85
  this._activeThinking = null;
91
86
  this.overlayStatus = null;
92
87
  this.scrollState.clear();
93
- this._segmentLinesCache = new Map();
94
88
  this._baseLinesCache = new Map();
89
+ this._baseEntriesCache = new Map();
95
90
  }
96
91
 
97
- write(text) {
98
- this._writeText(text, false);
99
- }
100
-
101
- writeMarkdown(text) {
102
- this._writeText(text, true);
103
- }
92
+ write(text) { this._writeText(text, false); }
93
+ writeMarkdown(text) { this._writeText(text, true); }
104
94
 
105
95
  _writeText(text, markdown) {
106
96
  this.overlayStatus = null;
107
97
  this._invalidateBaseLines();
108
98
  const current = this.currentText.at(-1);
109
- if (current.markdown !== markdown && current.text !== "") {
110
- this.currentText.push({ text: "", markdown });
111
- } else {
112
- current.markdown = markdown;
113
- }
99
+ if (current.markdown !== markdown && current.text !== "") this.currentText.push({ text: "", markdown });
100
+ else current.markdown = markdown;
114
101
  const parts = text.split("\n");
115
102
  this.currentText[this.currentText.length - 1].text += parts[0];
116
103
  for (let i = 1; i < parts.length; i++) this.currentText.push({ text: parts[i], markdown });
@@ -134,10 +121,9 @@ export class OutputBuffer {
134
121
  startThinking() {
135
122
  this.overlayStatus = null;
136
123
  this._flushText();
137
- const seg = { type: "thinking", tokens: 0, content: [] };
138
- this.segments.push(seg);
139
- this._invalidateSegmentLines();
140
- this._activeThinking = seg;
124
+ this._activeThinking = { type: "thinking", tokens: 0, content: [] };
125
+ this.segments.push(this._activeThinking);
126
+ this._invalidateBaseLines();
141
127
  }
142
128
 
143
129
  appendThinking(text) {
@@ -151,25 +137,24 @@ export class OutputBuffer {
151
137
  }
152
138
 
153
139
  endThinking(tokens) {
154
- if (this._activeThinking) {
155
- this._activeThinking.tokens = tokens;
156
- this._activeThinking = null;
157
- this._invalidateSegmentLines();
158
- }
140
+ if (!this._activeThinking) return;
141
+ this._activeThinking.tokens = tokens;
142
+ this._activeThinking = null;
143
+ this._invalidateBaseLines();
159
144
  }
160
145
 
161
146
  addThinkingBlock(tokens, content) {
162
147
  this.overlayStatus = null;
163
148
  this._flushText();
164
149
  this.segments.push({ type: "thinking", tokens, content: content.split("\n") });
165
- this._invalidateSegmentLines();
150
+ this._invalidateBaseLines();
166
151
  }
167
152
 
168
153
  addBlock(block) {
169
154
  this.overlayStatus = null;
170
155
  this._flushText();
171
156
  this.segments.push(block);
172
- this._invalidateSegmentLines();
157
+ this._invalidateBaseLines();
173
158
  }
174
159
 
175
160
  setOverlayStatus(lines) {
@@ -182,14 +167,12 @@ export class OutputBuffer {
182
167
  this._invalidateBaseLines();
183
168
  }
184
169
 
185
- sealCurrentText() {
186
- return this._flushText();
187
- }
170
+ sealCurrentText() { return this._flushText(); }
188
171
 
189
172
  _flushText() {
190
173
  if (this.currentText.length <= 1 && this.currentText[0].text === "") return false;
191
174
  this.segments.push(...currentTextToBlocks(this.currentText, true));
192
- this._invalidateSegmentLines();
175
+ this._invalidateBaseLines();
193
176
  this.currentText = [{ text: "", markdown: false }];
194
177
  this.currentTextCache = new Map();
195
178
  return true;
@@ -200,60 +183,44 @@ export class OutputBuffer {
200
183
  if (text !== undefined) this.spinnerText = text;
201
184
  }
202
185
 
203
- tick() {
204
- this.spinnerIdx = (this.spinnerIdx + 1) % SPINNER_FRAMES.length;
205
- }
206
-
207
- scroll(delta, options) {
208
- return this.scrollState.scroll(delta, options);
209
- }
210
-
211
- getScrollStep() {
212
- return this.scrollState.getStep();
213
- }
214
-
215
- getMaxScrollOffset() {
216
- return this.scrollState.getMaxOffset();
217
- }
218
-
219
- setViewportHeight(height) {
220
- this.scrollState.setViewportHeight(height);
221
- }
222
-
223
- resetScroll() {
224
- this.scrollState.reset();
225
- }
186
+ tick() { this.spinnerIdx = (this.spinnerIdx + 1) % SPINNER_FRAMES.length; }
187
+ scroll(delta, options) { return this.scrollState.scroll(delta, options); }
188
+ getScrollStep() { return this.scrollState.getStep(); }
189
+ getMaxScrollOffset() { return this.scrollState.getMaxOffset(); }
190
+ setViewportHeight(height) { this.scrollState.setViewportHeight(height); }
191
+ resetScroll() { this.scrollState.reset(); }
226
192
 
227
193
  setToolCardsExpanded(expanded) {
228
194
  let changed = false;
229
195
  for (const seg of this.segments) {
230
- if (seg.type !== "tool-card") continue;
231
- if (seg.expanded === expanded) continue;
196
+ if (seg.type !== "tool-card" || seg.expanded === expanded) continue;
232
197
  seg.expanded = expanded;
233
198
  changed = true;
234
199
  }
235
- if (changed) this._invalidateSegmentLines();
200
+ if (changed) this._invalidateBaseLines();
236
201
  return changed;
237
202
  }
238
203
 
239
- invalidate() {
240
- this._invalidateSegmentLines();
241
- }
242
-
243
- _invalidateSegmentLines() {
244
- this._segmentLinesCache.clear();
245
- this._invalidateBaseLines();
246
- }
204
+ invalidate() { this._invalidateBaseLines(); }
247
205
 
248
206
  _invalidateBaseLines() {
249
207
  this._baseLinesCache.clear();
208
+ this._baseEntriesCache.clear();
250
209
  }
251
210
 
252
211
  render(width) {
253
- const baseLines = this._renderBaseLines(width);
212
+ return this.renderSelectable(width).lines;
213
+ }
214
+
215
+ renderSelectable(width) {
216
+ const baseEntries = this._renderBaseEntries(width);
254
217
  const tailLine = this.spinning ? this._spinnerLine() : null;
255
- this.scrollState.setTotalLines(baseLines.length + (tailLine == null ? 0 : 1));
256
- return sliceLinesWithTail(baseLines, tailLine, this.scrollState.sliceRange());
218
+ this.scrollState.setTotalLines(baseEntries.length + (tailLine == null ? 0 : 1));
219
+ const entries = sliceEntriesWithTail(baseEntries, tailLine, this.scrollState.sliceRange());
220
+ return {
221
+ lines: entries.map((entry) => entry.line),
222
+ copyText: (range) => copySourceTextForRange(entries, range),
223
+ };
257
224
  }
258
225
 
259
226
  _spinnerLine() {
@@ -263,31 +230,28 @@ export class OutputBuffer {
263
230
  _renderBaseLines(width) {
264
231
  const cached = this._baseLinesCache.get(width);
265
232
  if (cached) return cached;
266
- const lines = [...this._renderCachedSegmentLines(width)];
267
- const dynamicStart = this._cachedSegmentPrefixCount();
268
- for (const seg of this.segments.slice(dynamicStart)) for (const line of renderBlock(seg, width)) lines.push(line);
269
- for (const block of currentTextToBlocks(this.currentText, false, this.currentTextCache)) for (const line of renderBlock(block, width)) lines.push(line);
270
- if (this.overlayStatus) for (const line of renderBlock(this.overlayStatus, width)) lines.push(line);
233
+ const lines = this._renderBaseEntries(width).map((entry) => entry.line);
271
234
  this._baseLinesCache.set(width, lines);
272
235
  return lines;
273
236
  }
274
237
 
275
- _renderCachedSegmentLines(width) {
276
- const prefixCount = this._cachedSegmentPrefixCount();
277
- const cached = this._segmentLinesCache.get(width);
278
- if (cached?.prefixCount === prefixCount) return cached.lines;
279
-
280
- const lines = [];
281
- for (let i = 0; i < prefixCount; i += 1) {
282
- for (const line of renderBlock(this.segments[i], width)) lines.push(line);
283
- }
284
- this._segmentLinesCache.set(width, { prefixCount, lines });
285
- return lines;
238
+ _renderBaseEntries(width) {
239
+ const cached = this._baseEntriesCache.get(width);
240
+ if (cached) return cached;
241
+ const entries = [];
242
+ for (const block of this._blocksForRender()) appendBlockEntries(entries, block, width);
243
+ this._baseEntriesCache.set(width, entries);
244
+ return entries;
286
245
  }
287
246
 
288
- _cachedSegmentPrefixCount() {
289
- if (!this._activeThinking) return this.segments.length;
290
- const index = this.segments.indexOf(this._activeThinking);
291
- return index < 0 ? this.segments.length : index;
247
+ _blocksForRender() {
248
+ const blocks = [...this.segments];
249
+ blocks.push(...currentTextToBlocks(this.currentText, false, this.currentTextCache));
250
+ if (this.overlayStatus) blocks.push(this.overlayStatus);
251
+ return blocks;
292
252
  }
293
253
  }
254
+
255
+ function appendBlockEntries(entries, block, width) {
256
+ appendSelectableEntries(entries, block, renderBlock(block, width), width);
257
+ }
@@ -73,6 +73,19 @@ export class ScreenSelection {
73
73
  return hadSelection;
74
74
  }
75
75
 
76
+ copyText() {
77
+ const sourceText = this.sourceText();
78
+ return sourceText || this.text();
79
+ }
80
+
81
+ sourceText() {
82
+ const range = this.range();
83
+ if (!range) return "";
84
+ const region = this._singleRegionForRange(range);
85
+ if (!region?.copyText) return "";
86
+ return region.copyText(localRange(range, region)) || "";
87
+ }
88
+
76
89
  text() {
77
90
  const range = this.range();
78
91
  if (!range) return "";
@@ -110,6 +123,15 @@ export class ScreenSelection {
110
123
  return this._plainLines.get(row);
111
124
  }
112
125
 
126
+ _singleRegionForRange(range) {
127
+ const matches = this.regions.filter((region) => {
128
+ const start = region.docStart;
129
+ const end = region.docStart + region.lines.length - 1;
130
+ return range.start.row >= start && range.end.row <= end;
131
+ });
132
+ return matches.length === 1 ? matches[0] : null;
133
+ }
134
+
113
135
  range() {
114
136
  if (!this.anchor || !this.focus) return null;
115
137
  const [start, end] = comparePoints(this.anchor, this.focus) <= 0
@@ -133,6 +155,14 @@ function normalizeRegion(region, index) {
133
155
  topRow: Math.max(0, Math.trunc(region.topRow ?? 0)),
134
156
  leftCol: Math.max(0, Math.trunc(region.leftCol ?? 0)),
135
157
  width,
158
+ copyText: typeof region.copyText === "function" ? region.copyText : null,
159
+ };
160
+ }
161
+
162
+ function localRange(range, region) {
163
+ return {
164
+ start: { row: range.start.row - region.docStart, col: range.start.col },
165
+ end: { row: range.end.row - region.docStart, col: range.end.col },
136
166
  };
137
167
  }
138
168
 
package/src/cli/ui.mjs CHANGED
@@ -58,7 +58,6 @@ export function createTuiUI({
58
58
  tui.setFocus(editor);
59
59
 
60
60
  let started = false;
61
- let mouseOn = true;
62
61
  let toolsExpanded = false;
63
62
  const activeToolBlocks = [];
64
63
  const renderScheduler = createRenderScheduler({ requestRender: () => tui.requestRender() });
@@ -98,7 +97,7 @@ export function createTuiUI({
98
97
  function ensureStarted() {
99
98
  if (!started) {
100
99
  tui.addInputListener((data) => {
101
- const mouseResult = mouseSelectionController.handleMouseInput(data, mouseOn);
100
+ const mouseResult = mouseSelectionController.handleMouseInput(data);
102
101
  if (mouseResult) return mouseResult;
103
102
  const copyKeyResult = mouseSelectionController.handleCopyKey(data);
104
103
  if (copyKeyResult) return copyKeyResult;
@@ -118,7 +117,7 @@ export function createTuiUI({
118
117
  }
119
118
 
120
119
  function openExternalEditor() {
121
- runTuiExternalEditor({ terminal, tui, editor, output, requestRender, mouseOn: () => mouseOn });
120
+ runTuiExternalEditor({ terminal, tui, editor, output, requestRender, mouseOn: () => true });
122
121
  }
123
122
 
124
123
  function toggleToolOutput() {
@@ -240,18 +239,6 @@ export function createTuiUI({
240
239
  requestRender();
241
240
  },
242
241
 
243
- toggleMouse: () => {
244
- if (mouseOn) {
245
- terminal.write("\x1b[?1002l\x1b[?1006l");
246
- mouseOn = false;
247
- return false;
248
- } else {
249
- terminal.write("\x1b[?1002h\x1b[?1006h");
250
- mouseOn = true;
251
- return true;
252
- }
253
- },
254
-
255
242
  requestPermission: async ({ toolName, params, category }) => {
256
243
  ensureStarted();
257
244
  spinnerStatus.stop();
@@ -281,7 +268,7 @@ export function createTuiUI({
281
268
  retryStatus.stop();
282
269
  if (started) {
283
270
  await terminal.drainInput?.();
284
- if (mouseOn) terminal.write("\x1b[?1002l\x1b[?1006l");
271
+ terminal.write("\x1b[?1002l\x1b[?1006l");
285
272
  tui.stop();
286
273
  terminal.write("\x1b[?1049l");
287
274
  }
package/src/main.mjs CHANGED
@@ -37,6 +37,9 @@ import { registerSuperGrokOAuthProvider } from "./supergrok/oauth-provider.mjs";
37
37
  import { installNetworkEnvironment } from "./network/environment.mjs";
38
38
  import { runMemoryCommand } from "./memory/command.mjs";
39
39
  import { normalizeRemoteMemorySources } from "./memory/remote/config.mjs";
40
+ import { resolveMemoryRoot } from "./memory/root.mjs";
41
+ import { runBrowserCommand } from "./browser/cli/command.mjs";
42
+ import { ensureBrowserDaemon } from "./browser/client/lifecycle.mjs";
40
43
  export async function run(argv) {
41
44
  const cwd = process.cwd();
42
45
  loadDotEnv(cwd);
@@ -49,6 +52,7 @@ export async function run(argv) {
49
52
  }
50
53
 
51
54
  const config = loadConfig(cwd);
55
+ const stateRoot = join(homedir(), ".march");
52
56
  const useRuntimeProcess = process.env.MARCH_RUNTIME_PROCESS !== "0";
53
57
  installNetworkEnvironment(config.network);
54
58
  if (args.command?.name === "login") {
@@ -70,11 +74,19 @@ export async function run(argv) {
70
74
  return 1;
71
75
  }
72
76
  if (args.command?.name === "memory") {
73
- args.memoryRoot = resolveMemoryRoot(config.memoryRoot, join(homedir(), ".march"));
77
+ args.memoryRoot = resolveMemoryRoot(config.memoryRoot, stateRoot);
74
78
  return await runMemoryCommand(args, { homeDir: homedir() });
75
79
  }
76
- const stateRoot = join(homedir(), ".march");
80
+ if (args.command?.name === "browser") {
81
+ try {
82
+ return await runBrowserCommand(args, { stateRoot });
83
+ } catch (err) {
84
+ process.stderr.write(`Error: ${err.message}\n`);
85
+ return 1;
86
+ }
87
+ }
77
88
  if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
89
+ await ensureBrowserDaemon({ stateRoot }).catch(() => {});
78
90
  const logger = createLogger({ logDir: join(stateRoot, "logs") });
79
91
  installProcessLogHandlers(logger);
80
92
  logger.event("process.start", {
@@ -223,7 +235,6 @@ export async function run(argv) {
223
235
  modeState,
224
236
  });
225
237
 
226
-
227
238
  const startupResume = await resumeStartupSession({
228
239
  resumeId: args.resume,
229
240
  runner,
@@ -233,7 +244,6 @@ export async function run(argv) {
233
244
  });
234
245
  refreshStatusBar();
235
246
 
236
-
237
247
  if (args.prompt) {
238
248
  turnRunning = true;
239
249
  try {
@@ -285,10 +295,4 @@ export async function run(argv) {
285
295
  return 0;
286
296
  }
287
297
 
288
- function resolveMemoryRoot(configured, stateRoot) {
289
- if (configured) return resolve(String(configured));
290
- if (process.env.MARCH_MEMORY_ROOT) return resolve(process.env.MARCH_MEMORY_ROOT);
291
- return resolve(stateRoot, "March Memories");
292
- }
293
-
294
298
  if (process.argv[1] === fileURLToPath(import.meta.url)) process.exitCode = await run(process.argv.slice(2));
@@ -0,0 +1,7 @@
1
+ import { resolve } from "node:path";
2
+
3
+ export function resolveMemoryRoot(configured, stateRoot) {
4
+ if (configured) return resolve(String(configured));
5
+ if (process.env.MARCH_MEMORY_ROOT) return resolve(process.env.MARCH_MEMORY_ROOT);
6
+ return resolve(stateRoot, "March Memories");
7
+ }