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.
- package/package.json +1 -1
- package/src/agent/runner.mjs +1 -1
- package/src/agent/runtime/runner-runtime-host.mjs +1 -0
- package/src/agent/session/session-options.mjs +2 -1
- package/src/agent/tools.mjs +3 -1
- package/src/browser/cli/command.mjs +61 -0
- package/src/browser/cli/open-url.mjs +21 -0
- package/src/browser/client/http.mjs +18 -0
- package/src/browser/client/lifecycle.mjs +57 -0
- package/src/browser/client/rpc.mjs +8 -0
- package/src/browser/client/state.mjs +35 -0
- package/src/browser/daemon/constants.mjs +3 -0
- package/src/browser/daemon/entry.mjs +28 -0
- package/src/browser/daemon/server.mjs +146 -0
- package/src/browser/extension/background.js +225 -0
- package/src/browser/extension/errors.js +19 -0
- package/src/browser/extension/execute-code.js +53 -0
- package/src/browser/extension/manifest.json +15 -0
- package/src/browser/extension-install.mjs +21 -0
- package/src/browser/tools/index.mjs +89 -0
- package/src/cli/args.mjs +4 -1
- package/src/cli/commands/help-command.mjs +1 -1
- package/src/cli/fallback-ui.mjs +0 -2
- package/src/cli/input/autocomplete.mjs +0 -1
- package/src/cli/slash-commands.mjs +5 -6
- package/src/cli/tui/input/mouse-selection-controller.mjs +2 -3
- package/src/cli/tui/layout/main-pane-layout.mjs +3 -2
- package/src/cli/tui/output/selectable-copy.mjs +107 -0
- package/src/cli/tui/output-buffer.mjs +57 -93
- package/src/cli/tui/selection-screen.mjs +30 -0
- package/src/cli/ui.mjs +3 -16
- package/src/main.mjs +14 -10
- package/src/memory/root.mjs +7 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function serializeError(err) {
|
|
2
|
+
if (!err) return { message: "Unknown error" };
|
|
3
|
+
if (typeof err === "string") return { message: err };
|
|
4
|
+
const name = typeof err.name === "string" && err.name ? err.name : "Error";
|
|
5
|
+
const stack = typeof err.stack === "string" ? err.stack : "";
|
|
6
|
+
const message = readableErrorMessage(err);
|
|
7
|
+
return { name, message, stack };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readableErrorMessage(err) {
|
|
11
|
+
if (typeof err?.message === "string" && err.message && err.message !== "[object Object]") return err.message;
|
|
12
|
+
if (typeof err?.message === "object") return safeStringify(err.message);
|
|
13
|
+
const json = safeStringify(err);
|
|
14
|
+
return json === "{}" ? String(err) : json;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function safeStringify(value) {
|
|
18
|
+
try { return JSON.stringify(value); } catch { return String(value); }
|
|
19
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function buildExecCode(code) {
|
|
2
|
+
return `(async () => {
|
|
3
|
+
function smartResult(value) {
|
|
4
|
+
if (value == null || typeof value !== "object") return value;
|
|
5
|
+
try { if (value.window === value && value.document) return "[Window: " + (value.location?.href || "about:blank") + "]"; } catch {}
|
|
6
|
+
if (typeof Node !== "undefined" && value.nodeType === Node.ELEMENT_NODE) return value.outerHTML;
|
|
7
|
+
if (typeof NodeList !== "undefined" && (value instanceof NodeList || value instanceof HTMLCollection)) {
|
|
8
|
+
return Array.from(value).slice(0, 300).map((item) => item?.nodeType === Node.ELEMENT_NODE ? item.outerHTML : String(item));
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(JSON.stringify(value, (_key, item) => {
|
|
12
|
+
if (item && typeof item === "object") {
|
|
13
|
+
if (typeof Node !== "undefined" && item.nodeType === Node.ELEMENT_NODE) return item.outerHTML;
|
|
14
|
+
try { if (item.window === item && item.document) return "[Window]"; } catch {}
|
|
15
|
+
}
|
|
16
|
+
return item;
|
|
17
|
+
}));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return "[Unserializable: " + (err?.message || String(err)) + "]";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function executable(source) {
|
|
23
|
+
const lines = source.split(/\\r?\\n/);
|
|
24
|
+
let index = lines.length - 1;
|
|
25
|
+
while (index >= 0 && !lines[index].trim()) index--;
|
|
26
|
+
if (index < 0) return source;
|
|
27
|
+
const last = lines[index].trim();
|
|
28
|
+
if (/^(return\\b|let\\b|const\\b|var\\b|if\\b|for\\b|while\\b|switch\\b|try\\b|throw\\b|class\\b|function\\b|async\\b|import\\b|export\\b|\\/\\/|})/.test(last)) return source;
|
|
29
|
+
lines[index] = lines[index].match(/^(\\s*)/)[1] + "return " + last;
|
|
30
|
+
return lines.join("\\n");
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const source = ${JSON.stringify(code)}.trim();
|
|
34
|
+
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
35
|
+
let value;
|
|
36
|
+
if (/^return\\b/.test(source) || /\\n\\s*return\\b/.test(source)) {
|
|
37
|
+
value = await (new AsyncFunction(source))();
|
|
38
|
+
} else {
|
|
39
|
+
try {
|
|
40
|
+
value = eval(source);
|
|
41
|
+
if (value && typeof value.then === "function") value = await value;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err instanceof SyntaxError) value = await (new AsyncFunction(executable(source)))();
|
|
44
|
+
else throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, data: smartResult(value) };
|
|
48
|
+
} catch (err) {
|
|
49
|
+
const message = err?.message || String(err);
|
|
50
|
+
return { ok: false, csp: /unsafe-eval|Content Security Policy|Refused to evaluate/i.test(message), error: { name: err?.name || "Error", message, stack: err?.stack || "" } };
|
|
51
|
+
}
|
|
52
|
+
})()`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "March Browser Bridge",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Connects the user's browser tabs to March CLI.",
|
|
6
|
+
"permissions": ["tabs", "scripting", "webNavigation", "storage", "alarms", "debugger"],
|
|
7
|
+
"host_permissions": ["<all_urls>"],
|
|
8
|
+
"background": {
|
|
9
|
+
"service_worker": "background.js",
|
|
10
|
+
"type": "module"
|
|
11
|
+
},
|
|
12
|
+
"action": {
|
|
13
|
+
"default_title": "March Browser Bridge"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cpSync, existsSync, rmSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export function sourceBrowserExtensionPath() {
|
|
6
|
+
const path = resolve(dirname(fileURLToPath(import.meta.url)), "extension");
|
|
7
|
+
if (!existsSync(path)) throw new Error(`Browser extension not found: ${path}`);
|
|
8
|
+
return path;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function installedBrowserExtensionPath(stateRoot) {
|
|
12
|
+
return resolve(stateRoot, "browser-extension");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function syncBrowserExtensionInstall(stateRoot) {
|
|
16
|
+
const source = sourceBrowserExtensionPath();
|
|
17
|
+
const target = installedBrowserExtensionPath(stateRoot);
|
|
18
|
+
rmSync(target, { recursive: true, force: true });
|
|
19
|
+
cpSync(source, target, { recursive: true });
|
|
20
|
+
return target;
|
|
21
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { toolText } from "../../agent/tool-result.mjs";
|
|
6
|
+
import { callBrowserDaemon } from "../client/rpc.mjs";
|
|
7
|
+
|
|
8
|
+
export function createBrowserTools({ stateRoot = join(homedir(), ".march") } = {}) {
|
|
9
|
+
return [
|
|
10
|
+
browserTabsTool(stateRoot),
|
|
11
|
+
browserOpenTool(stateRoot),
|
|
12
|
+
browserReadTool(stateRoot),
|
|
13
|
+
browserScriptTool(stateRoot),
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function browserTabsTool(stateRoot) {
|
|
18
|
+
return defineTool({
|
|
19
|
+
name: "browser_tabs",
|
|
20
|
+
label: "Browser Tabs",
|
|
21
|
+
description: "List all tabs visible to the March browser extension. Use this first to choose a tabId.",
|
|
22
|
+
parameters: Type.Object({}),
|
|
23
|
+
execute: async () => safeToolJson(() => callBrowserDaemon({ stateRoot, method: "tabs" })),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function browserOpenTool(stateRoot) {
|
|
28
|
+
return defineTool({
|
|
29
|
+
name: "browser_open",
|
|
30
|
+
label: "Browser Open",
|
|
31
|
+
description: "Create, navigate, focus, close, reload, back, or forward browser tabs. Returns the affected tab when available.",
|
|
32
|
+
parameters: Type.Object({
|
|
33
|
+
action: Type.String({ enum: ["new", "navigate", "focus", "close", "reload", "back", "forward"] }),
|
|
34
|
+
url: Type.Optional(Type.String({ description: "URL for new or navigate actions." })),
|
|
35
|
+
tabId: Type.Optional(Type.String({ description: "Target tab id for existing-tab actions." })),
|
|
36
|
+
active: Type.Optional(Type.Boolean({ description: "Whether a new tab should become active. Default true." })),
|
|
37
|
+
}),
|
|
38
|
+
execute: async (_id, params) => safeToolJson(() => callBrowserDaemon({ stateRoot, method: "open", params })),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function browserReadTool(stateRoot) {
|
|
43
|
+
return defineTool({
|
|
44
|
+
name: "browser_read",
|
|
45
|
+
label: "Browser Read",
|
|
46
|
+
description: "Read content from a specific browser tab. Defaults to visible text and interactive elements; can include HTML.",
|
|
47
|
+
parameters: Type.Object({
|
|
48
|
+
tabId: Type.String({ description: "Target tab id from browser_tabs or browser_open." }),
|
|
49
|
+
include: Type.Optional(Type.Object({
|
|
50
|
+
text: Type.Optional(Type.Boolean()),
|
|
51
|
+
html: Type.Optional(Type.Boolean()),
|
|
52
|
+
elements: Type.Optional(Type.Boolean()),
|
|
53
|
+
})),
|
|
54
|
+
}),
|
|
55
|
+
execute: async (_id, params) => safeToolJson(() => callBrowserDaemon({ stateRoot, method: "read", params })),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function browserScriptTool(stateRoot) {
|
|
60
|
+
return defineTool({
|
|
61
|
+
name: "browser_script",
|
|
62
|
+
label: "Browser Script",
|
|
63
|
+
description: "Execute arbitrary JavaScript in a specific browser tab. The code may return a JSON-serializable value or a Promise.",
|
|
64
|
+
parameters: Type.Object({
|
|
65
|
+
tabId: Type.String({ description: "Target tab id from browser_tabs or browser_open." }),
|
|
66
|
+
code: Type.String({ description: "JavaScript function body. Use return to send a result back." }),
|
|
67
|
+
awaitPromise: Type.Optional(Type.Boolean({ description: "Await a returned Promise. Default true." })),
|
|
68
|
+
timeoutMs: Type.Optional(Type.Number({ description: "Request timeout in milliseconds. Default 30000." })),
|
|
69
|
+
}),
|
|
70
|
+
execute: async (_id, params) => safeToolJson(() => callBrowserDaemon({
|
|
71
|
+
stateRoot,
|
|
72
|
+
method: "script",
|
|
73
|
+
params,
|
|
74
|
+
timeoutMs: params.timeoutMs ?? 30000,
|
|
75
|
+
})),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function safeToolJson(run) {
|
|
80
|
+
try {
|
|
81
|
+
return toolJson(await run());
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return toolJson({ ok: false, error: err.message }, { error: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function toolJson(payload, details = {}) {
|
|
88
|
+
return toolText(JSON.stringify(payload, null, 2), details);
|
|
89
|
+
}
|
package/src/cli/args.mjs
CHANGED
|
@@ -28,7 +28,7 @@ export function parseCliArgs(argv) {
|
|
|
28
28
|
allowPositionals: true,
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
const commandName = ["login", "provider", "websearch", "memory"].includes(positionals[0]) ? positionals[0] : null;
|
|
31
|
+
const commandName = ["login", "provider", "websearch", "memory", "browser"].includes(positionals[0]) ? positionals[0] : null;
|
|
32
32
|
|
|
33
33
|
return {
|
|
34
34
|
command: commandName ? { name: commandName, args: positionals.slice(1) } : null,
|
|
@@ -70,6 +70,9 @@ Usage:
|
|
|
70
70
|
march memory add <url>
|
|
71
71
|
march memory list
|
|
72
72
|
march memory remove <name>
|
|
73
|
+
march browser install Install the developer browser extension
|
|
74
|
+
march browser status Show browser daemon/extension status
|
|
75
|
+
march browser restart Restart the browser daemon
|
|
73
76
|
|
|
74
77
|
Options:
|
|
75
78
|
-m, --model <id> Initial model ID override
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function formatHelpLines() {
|
|
2
2
|
return [
|
|
3
|
-
"Commands: /new, /exit, /help, /hotkeys, /templates, /export jsonl, /export html, /export gist <jsonl|html>, /settings, /extensions, /providers, /providers <name>, /model, /models, /session, /status, /shell, /shell spawn [name], /save, /name, /copy
|
|
3
|
+
"Commands: /new, /exit, /help, /hotkeys, /templates, /export jsonl, /export html, /export gist <jsonl|html>, /settings, /extensions, /providers, /providers <name>, /model, /models, /session, /status, /shell, /shell spawn [name], /save, /name, /copy",
|
|
4
4
|
"Sessions: /session opens previous sessions and restores the selected one.",
|
|
5
5
|
"Shortcuts: Tab = toggle Do/Discuss, Esc = abort turn, Ctrl+C = abort turn / press twice to exit when idle, Ctrl+O = toggle tool output, Alt+S = shell pane, Alt+N = next shell, Alt+K/J = shell scroll, PageUp/PageDown = output scroll, Ctrl+G = external editor, Shift+Tab = thinking selector, Ctrl+T = thinking selector, Ctrl+L = model selector",
|
|
6
6
|
];
|
package/src/cli/fallback-ui.mjs
CHANGED
|
@@ -58,7 +58,6 @@ export function createJsonUI() {
|
|
|
58
58
|
getInputText: () => "",
|
|
59
59
|
insertTextAtCursor: () => {},
|
|
60
60
|
openExternalEditor: () => {},
|
|
61
|
-
toggleMouse: () => false,
|
|
62
61
|
toggleToolOutput: () => false,
|
|
63
62
|
requestExit: () => {},
|
|
64
63
|
close: () => {},
|
|
@@ -148,7 +147,6 @@ export function createPlainUI() {
|
|
|
148
147
|
getInputText: () => "",
|
|
149
148
|
insertTextAtCursor: () => {},
|
|
150
149
|
openExternalEditor: () => {},
|
|
151
|
-
toggleMouse: () => false,
|
|
152
150
|
toggleToolOutput: () => false,
|
|
153
151
|
requestExit: () => {},
|
|
154
152
|
close: () => {},
|
|
@@ -14,7 +14,6 @@ const MARCH_COMMANDS = [
|
|
|
14
14
|
{ name: "copy", description: "Copy last assistant response to clipboard" },
|
|
15
15
|
{ name: "thinking", description: "Open thinking selector" },
|
|
16
16
|
{ name: "thinking list", description: "List available thinking levels" },
|
|
17
|
-
{ name: "mouse", description: "Toggle mouse wheel and TUI selection copy" },
|
|
18
17
|
{ name: "hotkeys", description: "Show keyboard shortcuts and input prefixes" },
|
|
19
18
|
{ name: "templates", description: "List project prompt templates" },
|
|
20
19
|
{ name: "export jsonl", description: "Export current session turns as JSONL" },
|
|
@@ -105,12 +105,6 @@ export async function handleSlashCommand(trimmed, {
|
|
|
105
105
|
return { handled: true };
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
if (trimmed === "/mouse") {
|
|
109
|
-
const on = ui.toggleMouse();
|
|
110
|
-
ui.writeln(on ? "Mouse tracking: ON (wheel scroll and TUI selection copy enabled)" : "Mouse tracking: OFF (native terminal selection enabled)");
|
|
111
|
-
return { handled: true };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
108
|
if (trimmed === "/status") {
|
|
115
109
|
for (const line of statusCommand({
|
|
116
110
|
runner,
|
|
@@ -145,6 +139,11 @@ export async function handleSlashCommand(trimmed, {
|
|
|
145
139
|
return { handled: true };
|
|
146
140
|
}
|
|
147
141
|
|
|
142
|
+
if (trimmed === "/mouse") {
|
|
143
|
+
ui.writeln("Mouse selection is always enabled.");
|
|
144
|
+
return { handled: true };
|
|
145
|
+
}
|
|
146
|
+
|
|
148
147
|
const sessionSourceCommand = await handleSessionSourceCommand(trimmed, {
|
|
149
148
|
ui,
|
|
150
149
|
runner,
|
|
@@ -44,8 +44,7 @@ export function createMouseSelectionController({
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
return {
|
|
47
|
-
handleMouseInput(data
|
|
48
|
-
if (!mouseOn) return undefined;
|
|
47
|
+
handleMouseInput(data) {
|
|
49
48
|
const mouse = parseMouseEvent(data);
|
|
50
49
|
if (mouse?.type === "scroll") {
|
|
51
50
|
if (shellDrawer.isVisible?.() && mouse.col > Math.floor((terminal.columns || 80) * 0.64)) {
|
|
@@ -76,7 +75,7 @@ export function createMouseSelectionController({
|
|
|
76
75
|
|
|
77
76
|
handleCopyKey(data) {
|
|
78
77
|
if (data !== "\x03") return undefined;
|
|
79
|
-
const text = selection.text();
|
|
78
|
+
const text = selection.copyText?.() ?? selection.text();
|
|
80
79
|
if (!text) return undefined;
|
|
81
80
|
selection.clear();
|
|
82
81
|
copySelectionText(text);
|
|
@@ -18,11 +18,12 @@ export class MainPaneLayout {
|
|
|
18
18
|
const fixedHeight = statusTopLines.length + editorLines.length + statusBottomLines.length;
|
|
19
19
|
const viewportHeight = Math.max(1, (this.terminal?.rows || 30) - fixedHeight);
|
|
20
20
|
this.output.setViewportHeight(viewportHeight);
|
|
21
|
-
const
|
|
21
|
+
const outputView = this.output.renderSelectable?.(safeWidth) ?? { lines: this.output.render(safeWidth), copyText: null };
|
|
22
|
+
const outputLines = outputView.lines;
|
|
22
23
|
const outputTop = Math.max(0, viewportHeight - outputLines.length);
|
|
23
24
|
const editorTop = viewportHeight + statusTopLines.length;
|
|
24
25
|
this.selection?.setRegions?.([
|
|
25
|
-
{ id: "output", topRow: outputTop, leftCol: 0, width: safeWidth, lines: outputLines },
|
|
26
|
+
{ id: "output", topRow: outputTop, leftCol: 0, width: safeWidth, lines: outputLines, copyText: outputView.copyText },
|
|
26
27
|
{ id: "editor", topRow: editorTop, leftCol: 0, width: safeWidth, lines: editorLines },
|
|
27
28
|
]);
|
|
28
29
|
const selectedOutputLines = this.selection?.applyRegion?.("output", outputLines) ?? outputLines;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
import { renderMarkdown } from "../markdown-renderer.mjs";
|
|
4
|
+
|
|
5
|
+
export function appendSelectableEntries(entries, block, lines, width) {
|
|
6
|
+
if (block.type !== "markdown") {
|
|
7
|
+
for (const line of lines) entries.push({ line, source: null, codeSource: null, baseRow: entries.length });
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const source = { kind: "markdown", text: block.text, startRow: entries.length, endRow: entries.length + lines.length - 1 };
|
|
11
|
+
const fragmentRanges = renderedFragmentRanges(block.text, width, entries.length);
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
const baseRow = entries.length;
|
|
14
|
+
const fragmentSource = fragmentRanges.find((range) => baseRow >= range.startRow && baseRow <= range.endRow) ?? null;
|
|
15
|
+
const codeSource = fragmentSource?.kind === "code" ? fragmentSource : null;
|
|
16
|
+
entries.push({ line, source, codeSource, fragmentSource, baseRow });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sliceEntriesWithTail(baseEntries, tailLine, range) {
|
|
21
|
+
if (!range) return tailLine == null ? baseEntries : [...baseEntries, { line: tailLine, source: null, codeSource: null, baseRow: baseEntries.length }];
|
|
22
|
+
const { start, end } = range;
|
|
23
|
+
const visible = baseEntries.slice(start, Math.min(end, baseEntries.length));
|
|
24
|
+
if (tailLine != null && end > baseEntries.length) visible.push({ line: tailLine, source: null, codeSource: null, baseRow: baseEntries.length });
|
|
25
|
+
return visible;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function copySourceTextForRange(entries, range) {
|
|
29
|
+
if (!range) return "";
|
|
30
|
+
const selected = trimEmptyBoundaryEntries(entries.slice(range.start.row, range.end.row + 1));
|
|
31
|
+
const codeText = copyCompleteCodeSource(selected, entries, range);
|
|
32
|
+
if (codeText) return codeText;
|
|
33
|
+
const fragmentText = copyCompleteFragmentSource(selected, entries, range);
|
|
34
|
+
if (fragmentText) return fragmentText;
|
|
35
|
+
if (!selected.length || selected.some((entry) => !entry.source)) return "";
|
|
36
|
+
const sources = uniqueSources(selected, "source");
|
|
37
|
+
if (!sources.length || !sources.every((source) => sourceIsFullySelected(source, entries, range, "source"))) return "";
|
|
38
|
+
return sources.map((source) => source.text).join("\n\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function trimEmptyBoundaryEntries(entries) {
|
|
42
|
+
let start = 0;
|
|
43
|
+
let end = entries.length;
|
|
44
|
+
while (start < end && !entries[start].source && stripAnsi(entries[start].line).trim() === "") start += 1;
|
|
45
|
+
while (end > start && !entries[end - 1].source && stripAnsi(entries[end - 1].line).trim() === "") end -= 1;
|
|
46
|
+
return entries.slice(start, end);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function uniqueSources(entries, key) {
|
|
50
|
+
const result = [];
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const source = entry[key];
|
|
53
|
+
if (!source || result.includes(source)) continue;
|
|
54
|
+
result.push(source);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function copyCompleteCodeSource(selected, entries, range) {
|
|
60
|
+
if (!selected.length || selected.some((entry) => !entry.codeSource)) return "";
|
|
61
|
+
const sources = uniqueSources(selected, "codeSource");
|
|
62
|
+
if (sources.length !== 1 || !sourceIsFullySelected(sources[0], entries, range, "codeSource")) return "";
|
|
63
|
+
return sources[0].text;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function copyCompleteFragmentSource(selected, entries, range) {
|
|
67
|
+
if (!selected.length || selected.some((entry) => !entry.fragmentSource)) return "";
|
|
68
|
+
const sources = uniqueSources(selected, "fragmentSource");
|
|
69
|
+
if (sources.length !== 1 || !sourceIsFullySelected(sources[0], entries, range, "fragmentSource")) return "";
|
|
70
|
+
return sources[0].text;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sourceIsFullySelected(source, entries, range, key) {
|
|
74
|
+
const startIndex = entries.findIndex((entry) => entry[key] === source && entry.baseRow === source.startRow);
|
|
75
|
+
const endIndex = entries.findLastIndex((entry) => entry[key] === source && entry.baseRow === source.endRow);
|
|
76
|
+
if (startIndex < 0 || endIndex < 0) return false;
|
|
77
|
+
if (range.start.row > startIndex || range.end.row < endIndex) return false;
|
|
78
|
+
const lastLine = stripAnsi(entries[endIndex]?.line ?? "");
|
|
79
|
+
const coversStart = range.start.row < startIndex || range.start.col <= 0;
|
|
80
|
+
const coversEnd = range.end.row > endIndex || range.end.col >= visibleWidth(lastLine);
|
|
81
|
+
return coversStart && coversEnd;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderedFragmentRanges(markdown, width, baseRow) {
|
|
85
|
+
let tokens = [];
|
|
86
|
+
try { tokens = marked.lexer(String(markdown ?? "")); } catch { return []; }
|
|
87
|
+
let row = baseRow;
|
|
88
|
+
const ranges = [];
|
|
89
|
+
for (const token of tokens) {
|
|
90
|
+
const raw = token.raw ?? token.text ?? "";
|
|
91
|
+
const lineCount = renderMarkdown(raw, width).length;
|
|
92
|
+
const range = sourceRangeForToken(token, raw, row, lineCount);
|
|
93
|
+
if (range) ranges.push(range);
|
|
94
|
+
row += lineCount;
|
|
95
|
+
}
|
|
96
|
+
return ranges;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sourceRangeForToken(token, raw, row, lineCount) {
|
|
100
|
+
if (token.type === "code") return { kind: "code", text: String(token.text ?? ""), startRow: row, endRow: row + lineCount - 1 };
|
|
101
|
+
if (token.type === "table") return { kind: "table", text: String(raw).trimEnd(), startRow: row, endRow: row + lineCount - 1 };
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stripAnsi(text) {
|
|
106
|
+
return String(text ?? "").replace(/\x1b(?:\][^\x07]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/g, "");
|
|
107
|
+
}
|