march-cli 0.1.12 → 0.1.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.
- package/package.json +2 -1
- package/src/agent/command-exec-tool.mjs +42 -8
- package/src/agent/{read-file-tool.mjs → file-tools/read-file-tool.mjs} +2 -2
- package/src/agent/file-tools/read-image-tool.mjs +76 -0
- package/src/agent/runner/runner-utils.mjs +6 -0
- package/src/agent/runner.mjs +17 -16
- package/src/agent/runtime/ipc/ipc-peer.mjs +99 -0
- package/src/agent/runtime/ipc/process-ipc-transport.mjs +16 -0
- package/src/agent/runtime/remote-runner-client.mjs +73 -0
- package/src/agent/runtime/remote-ui-client.mjs +19 -0
- package/src/agent/runtime/runner-ipc-target.mjs +126 -0
- package/src/agent/runtime/runner-process-client.mjs +47 -0
- package/src/agent/runtime/runner-process-entry.mjs +93 -0
- package/src/agent/runtime/runner-runtime-host.mjs +1 -0
- package/src/agent/runtime/ui-event-bridge.mjs +85 -0
- package/src/agent/screen-tools/list-windows-tool.mjs +39 -0
- package/src/agent/screen-tools/screen-tool.mjs +49 -0
- package/src/agent/screen-tools/windows-screen.mjs +133 -0
- package/src/agent/session/session-options.mjs +2 -1
- package/src/agent/tool-summary.mjs +112 -0
- package/src/agent/tools.mjs +12 -5
- package/src/agent/turn/turn-events.mjs +46 -0
- package/src/agent/turn/turn-runner.mjs +2 -1
- package/src/agent/vision-capability.mjs +14 -0
- package/src/cli/args.mjs +8 -0
- package/src/cli/commands/copy-command.mjs +16 -2
- package/src/cli/commands/status-command.mjs +7 -4
- package/src/cli/commands/thinking-command.mjs +10 -3
- package/src/cli/repl-loop.mjs +3 -1
- package/src/cli/startup/create-runtime-runner.mjs +61 -0
- package/src/cli/startup/startup-banner.mjs +64 -10
- package/src/cli/tui/layout/main-pane-layout.mjs +16 -7
- package/src/cli/tui/selection-screen.mjs +83 -34
- package/src/cli/tui/status/status-bar.mjs +155 -18
- package/src/cli/tui/tool-rendering.mjs +3 -113
- package/src/cli/tui/tui-handlers.mjs +1 -1
- package/src/cli/tui/ui-theme.mjs +14 -5
- package/src/cli/ui.mjs +1 -1
- package/src/config/config-json.mjs +11 -0
- package/src/context/engine.mjs +10 -9
- package/src/context/profiles.mjs +39 -0
- package/src/context/system-core/base.md +2 -1
- package/src/main.mjs +42 -35
- package/src/provider/accept-command.mjs +89 -0
- package/src/provider/command.mjs +21 -0
- package/src/provider/custom-provider.mjs +5 -4
- package/src/provider/share-command.mjs +79 -0
- package/src/provider/share-payload.mjs +52 -0
- package/src/supergrok/tool.mjs +6 -6
- package/src/context/center-memory.mjs +0 -14
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { formatToolStartLine, formatToolSuccessSummary } from "../tool-summary.mjs";
|
|
2
|
+
|
|
1
3
|
export function createTurnEventState() {
|
|
2
4
|
return {
|
|
3
5
|
draft: "",
|
|
@@ -5,6 +7,8 @@ export function createTurnEventState() {
|
|
|
5
7
|
thinkingAccumulator: "",
|
|
6
8
|
recallCursor: { draftLength: 0, thinkingLength: 0 },
|
|
7
9
|
assistantReplyOpen: false,
|
|
10
|
+
assistantContextParts: [],
|
|
11
|
+
activeToolContextPart: null,
|
|
8
12
|
};
|
|
9
13
|
}
|
|
10
14
|
|
|
@@ -14,9 +18,11 @@ export function handleRunnerSessionEvent(event, { ui, engine, state }) {
|
|
|
14
18
|
}
|
|
15
19
|
if (event.type === "tool_execution_start") {
|
|
16
20
|
closeAssistantReply({ ui, state });
|
|
21
|
+
appendToolStartContext(state, event.toolName, event.args);
|
|
17
22
|
ui.toolStart(event.toolName, event.args);
|
|
18
23
|
}
|
|
19
24
|
if (event.type === "tool_execution_end") {
|
|
25
|
+
updateToolEndContext(state, event.toolName, event.isError, event.result);
|
|
20
26
|
ui.toolEnd(event.toolName, event.isError, event.result);
|
|
21
27
|
}
|
|
22
28
|
if (event.type === "auto_retry_start") {
|
|
@@ -45,6 +51,7 @@ export function closeAssistantReply({ ui, state }) {
|
|
|
45
51
|
function handleAssistantMessageEvent(event, { ui, state }) {
|
|
46
52
|
if (event.type === "text_delta") {
|
|
47
53
|
state.draft += event.delta;
|
|
54
|
+
appendAssistantContextText(state, event.delta, "output");
|
|
48
55
|
state.assistantReplyOpen = true;
|
|
49
56
|
ui.textDelta(event.delta);
|
|
50
57
|
}
|
|
@@ -54,6 +61,7 @@ function handleAssistantMessageEvent(event, { ui, state }) {
|
|
|
54
61
|
}
|
|
55
62
|
if (event.type === "thinking_delta") {
|
|
56
63
|
state.thinkingText += event.delta;
|
|
64
|
+
appendAssistantContextText(state, event.delta, "thinking");
|
|
57
65
|
ui.thinkingDelta(event.delta);
|
|
58
66
|
}
|
|
59
67
|
if (event.type === "thinking_end" && state.thinkingText) {
|
|
@@ -63,3 +71,41 @@ function handleAssistantMessageEvent(event, { ui, state }) {
|
|
|
63
71
|
state.thinkingText = "";
|
|
64
72
|
}
|
|
65
73
|
}
|
|
74
|
+
|
|
75
|
+
export function compactAssistantContext(state) {
|
|
76
|
+
return (state?.assistantContextParts ?? [])
|
|
77
|
+
.map((part) => part?.text ?? "")
|
|
78
|
+
.join("")
|
|
79
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
80
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
81
|
+
.trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function appendAssistantContextText(state, text, type) {
|
|
85
|
+
if (!text) return;
|
|
86
|
+
const parts = state.assistantContextParts;
|
|
87
|
+
const last = parts.at(-1);
|
|
88
|
+
if (last?.type === type) {
|
|
89
|
+
last.text += text;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (last && !last.text.endsWith("\n")) last.text += "\n";
|
|
93
|
+
parts.push({ type, text });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function appendToolStartContext(state, name, args) {
|
|
97
|
+
const parts = state.assistantContextParts;
|
|
98
|
+
const last = parts.at(-1);
|
|
99
|
+
if (last && !last.text.endsWith("\n")) last.text += "\n";
|
|
100
|
+
const part = { type: "tool", name, text: `${formatToolStartLine(name, args)}\n` };
|
|
101
|
+
parts.push(part);
|
|
102
|
+
state.activeToolContextPart = part;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function updateToolEndContext(state, name, isError, result) {
|
|
106
|
+
const part = state.activeToolContextPart;
|
|
107
|
+
if (!part || part.name !== name) return;
|
|
108
|
+
const summary = isError ? "failed" : formatToolSuccessSummary(name, result, "");
|
|
109
|
+
if (summary && summary !== "done") part.text = `${part.text.trimEnd()} (${summary})\n`;
|
|
110
|
+
state.activeToolContextPart = null;
|
|
111
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveImageAttachmentReferences } from "../../session/attachment-references.mjs";
|
|
2
|
-
import { closeAssistantReply, createTurnEventState, handleRunnerSessionEvent } from "./turn-events.mjs";
|
|
2
|
+
import { closeAssistantReply, compactAssistantContext, createTurnEventState, handleRunnerSessionEvent } from "./turn-events.mjs";
|
|
3
3
|
|
|
4
4
|
export async function runRunnerTurn({
|
|
5
5
|
prompt,
|
|
@@ -122,6 +122,7 @@ function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, me
|
|
|
122
122
|
engine.recordTurn({
|
|
123
123
|
userMessage: userMessage ?? prompt.slice(0, 300),
|
|
124
124
|
assistantMessage: turnState.draft,
|
|
125
|
+
assistantContext: compactAssistantContext(turnState),
|
|
125
126
|
userRecallHints,
|
|
126
127
|
assistantRecallHints: recordedAssistantRecallHints,
|
|
127
128
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function modelSupportsImageInput(model) {
|
|
2
|
+
if (!model || typeof model !== "object") return false;
|
|
3
|
+
if (Array.isArray(model.input) && model.input.includes("image")) return true;
|
|
4
|
+
if (model.capabilities?.images === true || model.capabilities?.vision === true) return true;
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function currentModelImageInputError(getCurrentModel) {
|
|
9
|
+
if (typeof getCurrentModel !== "function") return null;
|
|
10
|
+
const model = getCurrentModel();
|
|
11
|
+
if (modelSupportsImageInput(model)) return null;
|
|
12
|
+
const label = model ? `${model.name || model.id || "unknown"} (${model.provider || "unknown provider"})` : "unknown";
|
|
13
|
+
return `Current model does not support image input: ${label}. Switch to a vision-capable model before using read_image or screen.`;
|
|
14
|
+
}
|
package/src/cli/args.mjs
CHANGED
|
@@ -11,6 +11,8 @@ export function parseCliArgs(argv) {
|
|
|
11
11
|
extension: { type: "string", short: "e", multiple: true },
|
|
12
12
|
extension: { type: "string", short: "e", multiple: true },
|
|
13
13
|
config: { type: "boolean" },
|
|
14
|
+
"include-key": { type: "boolean" },
|
|
15
|
+
"profile-only": { type: "boolean" },
|
|
14
16
|
"dump-context": { type: "boolean" },
|
|
15
17
|
"pi-sessions": { type: "boolean" },
|
|
16
18
|
"pi-runtime-host": { type: "boolean" },
|
|
@@ -33,6 +35,8 @@ export function parseCliArgs(argv) {
|
|
|
33
35
|
extensions: values.extension ?? [],
|
|
34
36
|
dumpContext: values["dump-context"] ?? false,
|
|
35
37
|
providerConfig: values.config ?? false,
|
|
38
|
+
includeKey: values["include-key"] ?? false,
|
|
39
|
+
profileOnly: values["profile-only"] ?? false,
|
|
36
40
|
piSessions: values["pi-sessions"] ?? false,
|
|
37
41
|
piRuntimeHost: values["pi-runtime-host"] ?? false,
|
|
38
42
|
shellRuntime: values["no-shell-runtime"] ? false : true,
|
|
@@ -50,6 +54,8 @@ Usage:
|
|
|
50
54
|
march [options] (starts REPL)
|
|
51
55
|
march login [provider] Login to an OAuth provider
|
|
52
56
|
march provider --config Configure provider credentials
|
|
57
|
+
march provider share [id] Share a provider profile
|
|
58
|
+
march provider accept <token>
|
|
53
59
|
march websearch --config Configure web search credentials
|
|
54
60
|
|
|
55
61
|
Options:
|
|
@@ -58,6 +64,8 @@ Options:
|
|
|
58
64
|
--resume <id> Resume a pi session by default
|
|
59
65
|
--json JSON output mode (no TUI)
|
|
60
66
|
--config With provider/websearch command, open configuration
|
|
67
|
+
--include-key With provider share, include API key
|
|
68
|
+
--profile-only With provider share, omit API key
|
|
61
69
|
--dump-context Write every prompt sent to the model under .march/context-dumps/
|
|
62
70
|
--pi-sessions Force pi JSONL SessionManager persistence
|
|
63
71
|
--pi-runtime-host Force pi AgentSessionRuntime host path
|
|
@@ -61,13 +61,27 @@ export function writeSystemClipboardAsync(text, { platform = process.platform }
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function clipboardCommand(platform) {
|
|
64
|
+
export function clipboardCommand(platform) {
|
|
65
65
|
if (platform === "win32") {
|
|
66
66
|
return {
|
|
67
67
|
bin: "powershell.exe",
|
|
68
|
-
args: ["-NoProfile", "-
|
|
68
|
+
args: ["-NoProfile", "-EncodedCommand", encodePowerShellCommand(readUtf8StdinToClipboardScript())],
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
if (platform === "darwin") return { bin: "pbcopy", args: [] };
|
|
72
72
|
return { bin: "sh", args: ["-lc", "command -v wl-copy >/dev/null && wl-copy || xclip -selection clipboard || xsel --clipboard --input"] };
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
function readUtf8StdinToClipboardScript() {
|
|
76
|
+
return [
|
|
77
|
+
"$stdin = [Console]::OpenStandardInput()",
|
|
78
|
+
"$memory = New-Object System.IO.MemoryStream",
|
|
79
|
+
"$stdin.CopyTo($memory)",
|
|
80
|
+
"$text = [System.Text.Encoding]::UTF8.GetString($memory.ToArray())",
|
|
81
|
+
"Set-Clipboard -Value $text",
|
|
82
|
+
].join("; ");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function encodePowerShellCommand(script) {
|
|
86
|
+
return Buffer.from(script, "utf16le").toString("base64");
|
|
87
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { MODES, formatModeLabel } from "../input/mode-state.mjs";
|
|
3
|
-
import { PREFIX, R } from "../tui/ui-theme.mjs";
|
|
3
|
+
import { modeLabel, PREFIX, R } from "../tui/ui-theme.mjs";
|
|
4
4
|
|
|
5
5
|
export function statusCommand({
|
|
6
6
|
runner,
|
|
@@ -78,9 +78,7 @@ export function formatStatusBarLine({
|
|
|
78
78
|
|
|
79
79
|
const C = PREFIX; // foreground-only color prefixes (no reset)
|
|
80
80
|
const DIM = C.brightBlack;
|
|
81
|
-
const
|
|
82
|
-
const WARN = "\x1b[33m"; // yellow, no reset
|
|
83
|
-
const modeSegment = `${mode === MODES.DISCUSS ? WARN : OK}${formatModeLabel(mode)}`;
|
|
81
|
+
const modeSegment = formatModeSegment(mode);
|
|
84
82
|
const runtime = `${C.cyan}${model}${DIM}·${thinking}`;
|
|
85
83
|
const segments = [modeSegment, runtime];
|
|
86
84
|
const lspText = formatLspSegment(lspStatus);
|
|
@@ -94,6 +92,11 @@ export function formatStatusBarLine({
|
|
|
94
92
|
return `${inner}${R}`;
|
|
95
93
|
}
|
|
96
94
|
|
|
95
|
+
function formatModeSegment(mode) {
|
|
96
|
+
const label = formatModeLabel(mode);
|
|
97
|
+
return (modeLabel[mode] ?? modeLabel.fallback)(label);
|
|
98
|
+
}
|
|
99
|
+
|
|
97
100
|
export function formatLspSegment(lspStatus) {
|
|
98
101
|
if (!lspStatus) return "";
|
|
99
102
|
const servers = lspStatus.servers ?? [];
|
|
@@ -55,9 +55,9 @@ export async function handleThinkingCommand(parsed, { runner, ui = null } = {})
|
|
|
55
55
|
runner.getThinkingLevel?.(),
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
|
-
if (parsed.type === "select") return [
|
|
58
|
+
if (parsed.type === "select") return [await selectThinkingByIndexAsync(parsed.index, { runner })];
|
|
59
59
|
if (parsed.type === "set") {
|
|
60
|
-
const level = runner.setThinkingLevel(parsed.level);
|
|
60
|
+
const level = await runner.setThinkingLevel(parsed.level);
|
|
61
61
|
return [`thinking: ${level}`];
|
|
62
62
|
}
|
|
63
63
|
if (parsed.type === "error") return [`Error: ${parsed.message}`];
|
|
@@ -76,5 +76,12 @@ async function selectThinkingInteractively({ runner, ui }) {
|
|
|
76
76
|
suppressInitialConfirm: true,
|
|
77
77
|
});
|
|
78
78
|
if (!item) return "thinking: unchanged";
|
|
79
|
-
return `thinking: ${runner.setThinkingLevel(item.level)}`;
|
|
79
|
+
return `thinking: ${await runner.setThinkingLevel(item.level)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function selectThinkingByIndexAsync(index, { runner }) {
|
|
83
|
+
const levels = runner.getAvailableThinkingLevels?.() || THINKING_LEVELS;
|
|
84
|
+
const level = levels[index - 1];
|
|
85
|
+
if (!level) return `Error: thinking index out of range: ${index}`;
|
|
86
|
+
return `thinking: ${await runner.setThinkingLevel(level)}`;
|
|
80
87
|
}
|
package/src/cli/repl-loop.mjs
CHANGED
|
@@ -119,7 +119,9 @@ export async function runInteractiveRepl({
|
|
|
119
119
|
export function contextTokenRefreshOptions(slashResult, runner) {
|
|
120
120
|
if (!slashResult?.refreshContextTokens) return undefined;
|
|
121
121
|
if (typeof runner.estimateContextTokens !== "function") return undefined;
|
|
122
|
-
|
|
122
|
+
const contextTokens = runner.estimateContextTokens("");
|
|
123
|
+
if (contextTokens && typeof contextTokens.then === "function") return undefined;
|
|
124
|
+
return { contextTokens };
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
function handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand }) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createRunner } from "../../agent/runner.mjs";
|
|
2
|
+
import { createRunnerProcessClient } from "../../agent/runtime/runner-process-client.mjs";
|
|
3
|
+
import { resolvePiSessionManager } from "../../session/pi-manager.mjs";
|
|
4
|
+
|
|
5
|
+
export async function createRuntimeRunner({
|
|
6
|
+
useRuntimeProcess = false,
|
|
7
|
+
runnerOptions,
|
|
8
|
+
ui,
|
|
9
|
+
memoryStore,
|
|
10
|
+
memoryTools,
|
|
11
|
+
shellRuntime,
|
|
12
|
+
mcpTools,
|
|
13
|
+
mcpInjections,
|
|
14
|
+
mcpClientManager,
|
|
15
|
+
webTools,
|
|
16
|
+
usePiSessions,
|
|
17
|
+
usePiRuntimeHost,
|
|
18
|
+
authStorage,
|
|
19
|
+
permissionController,
|
|
20
|
+
modelContextDumper,
|
|
21
|
+
turnNotifier,
|
|
22
|
+
logger,
|
|
23
|
+
refreshStatusBar,
|
|
24
|
+
} = {}) {
|
|
25
|
+
const onModelPayload = ({ estimatedTokens }) => {
|
|
26
|
+
refreshStatusBar?.({ contextTokens: estimatedTokens });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const runner = useRuntimeProcess
|
|
30
|
+
? (await createRunnerProcessClient({ runnerOptions, ui, onModelPayload })).runner
|
|
31
|
+
: await createRunner({
|
|
32
|
+
...runnerOptions,
|
|
33
|
+
ui,
|
|
34
|
+
memoryStore,
|
|
35
|
+
memoryTools,
|
|
36
|
+
shellRuntime,
|
|
37
|
+
mcpTools,
|
|
38
|
+
mcpInjections,
|
|
39
|
+
mcpClientManager,
|
|
40
|
+
webTools,
|
|
41
|
+
sessionManager: resolvePiSessionManager({
|
|
42
|
+
cwd: runnerOptions.cwd,
|
|
43
|
+
projectMarchDir: runnerOptions.projectMarchDir,
|
|
44
|
+
enabled: usePiSessions,
|
|
45
|
+
}),
|
|
46
|
+
useRuntimeHost: usePiRuntimeHost,
|
|
47
|
+
syncPiSidecar: usePiSessions || usePiRuntimeHost,
|
|
48
|
+
authStorage,
|
|
49
|
+
maxTurns: runnerOptions.config?.maxTurns ?? undefined,
|
|
50
|
+
trimBatch: runnerOptions.config?.trimBatch ?? undefined,
|
|
51
|
+
hostedTools: runnerOptions.config?.hostedTools,
|
|
52
|
+
permissionController,
|
|
53
|
+
modelContextDumper,
|
|
54
|
+
turnNotifier,
|
|
55
|
+
logger,
|
|
56
|
+
onModelPayload,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
runner.shellRuntime ??= shellRuntime;
|
|
60
|
+
return runner;
|
|
61
|
+
}
|
|
@@ -1,17 +1,71 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { MODES, formatModeLabel } from "../input/mode-state.mjs";
|
|
4
|
+
import { brightBlack, cyan, modeLabel, white } from "../tui/ui-theme.mjs";
|
|
5
|
+
|
|
6
|
+
const CARD_WIDTH = 76;
|
|
7
|
+
const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
8
|
+
const { version: packageVersion = "0.1" } = createRequire(import.meta.url)("../../../package.json");
|
|
3
9
|
|
|
4
10
|
export function formatStartupBanner({ cwd, modelId = "model?", thinkingLevel = "thinking?", mode = MODES.DO, dumpContextPath = null } = {}) {
|
|
5
|
-
const nextMode = mode === MODES.DISCUSS ?
|
|
6
|
-
const
|
|
7
|
-
? brightBlack(
|
|
8
|
-
: brightBlack(
|
|
11
|
+
const nextMode = mode === MODES.DISCUSS ? MODES.DO : MODES.DISCUSS;
|
|
12
|
+
const tip = dumpContextPath
|
|
13
|
+
? `${brightBlack("Tip:")} ${cyan("dumps:")} ${brightBlack(dumpContextPath)}`
|
|
14
|
+
: `${brightBlack("Tip:")} ${brightBlack("Tab to")} ${formatModeTip(nextMode)} ${brightBlack("·")} ${cyan("/help")} ${brightBlack("for commands")}`;
|
|
9
15
|
return [
|
|
10
|
-
`${cyan(" █▙ ▟█")} ${bold("March")}`,
|
|
11
|
-
`${cyan(" █▜▙▟▛█")} ${brightBlack(`${modelId} · ${thinkingLevel}`)}`,
|
|
12
|
-
`${cyan(" ▀ ▀")} ${brightBlack(cwd ?? "")}`,
|
|
13
16
|
"",
|
|
14
|
-
|
|
17
|
+
...renderStartupCard([
|
|
18
|
+
`${cyan(" █▙ ▟█")} ${white("March")} ${brightBlack(`v${packageVersion}`)}`,
|
|
19
|
+
`${cyan(" █▜▙▟▛█")} ${brightBlack("Describe a task to get started.")}`,
|
|
20
|
+
`${cyan(" ▀ ▀")}`,
|
|
21
|
+
"",
|
|
22
|
+
tip,
|
|
23
|
+
brightBlack("March uses AI. Check for mistakes."),
|
|
24
|
+
]),
|
|
15
25
|
"",
|
|
16
26
|
];
|
|
17
27
|
}
|
|
28
|
+
|
|
29
|
+
function formatModeTip(mode) {
|
|
30
|
+
const label = formatModeLabel(mode);
|
|
31
|
+
return (modeLabel[mode] ?? modeLabel.fallback)(label);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderStartupCard(contentLines, width = CARD_WIDTH) {
|
|
35
|
+
const safeWidth = Math.max(24, Math.trunc(width));
|
|
36
|
+
const innerWidth = safeWidth - 4;
|
|
37
|
+
return [
|
|
38
|
+
white(`╭${"─".repeat(safeWidth - 2)}╮`),
|
|
39
|
+
...contentLines.map((line) => white("│ ") + padAnsi(line, innerWidth) + white(" │")),
|
|
40
|
+
white(`╰${"─".repeat(safeWidth - 2)}╯`),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function padAnsi(text, width) {
|
|
45
|
+
const clipped = clipAnsi(text, width);
|
|
46
|
+
const padding = Math.max(0, width - visibleWidth(stripAnsi(clipped)));
|
|
47
|
+
return `${clipped}${" ".repeat(padding)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function clipAnsi(text, width) {
|
|
51
|
+
let output = "";
|
|
52
|
+
let plainWidth = 0;
|
|
53
|
+
let inAnsi = false;
|
|
54
|
+
for (const ch of Array.from(String(text ?? ""))) {
|
|
55
|
+
if (ch === "\x1b") inAnsi = true;
|
|
56
|
+
if (inAnsi) {
|
|
57
|
+
output += ch;
|
|
58
|
+
if (/[@-~]/.test(ch)) inAnsi = false;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const charWidth = visibleWidth(ch);
|
|
62
|
+
if (plainWidth + charWidth > width) break;
|
|
63
|
+
output += ch;
|
|
64
|
+
plainWidth += charWidth;
|
|
65
|
+
}
|
|
66
|
+
return output;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stripAnsi(text) {
|
|
70
|
+
return String(text ?? "").replace(ANSI_RE, "");
|
|
71
|
+
}
|
|
@@ -9,19 +9,28 @@ export class MainPaneLayout {
|
|
|
9
9
|
|
|
10
10
|
render(width) {
|
|
11
11
|
const safeWidth = Math.max(1, Math.trunc(width));
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
12
|
+
const statusTopLines = this.statusBar.renderTop?.(safeWidth) ?? this.statusBar.render(safeWidth);
|
|
13
|
+
const rawEditorLines = this.editor.render(safeWidth);
|
|
14
|
+
const editorLines = this.statusBar.renderInputLines?.(rawEditorLines, safeWidth)
|
|
15
|
+
?? rawEditorLines.map((line) => this.statusBar.renderInputLine?.(line, safeWidth) ?? line);
|
|
16
|
+
const statusBottomLines = this.statusBar.renderBottom?.(safeWidth) ?? [];
|
|
17
|
+
const fixedHeight = statusTopLines.length + editorLines.length + statusBottomLines.length;
|
|
15
18
|
const viewportHeight = Math.max(1, (this.terminal?.rows || 30) - fixedHeight);
|
|
16
19
|
this.output.setViewportHeight(viewportHeight);
|
|
17
20
|
const outputLines = this.output.render(safeWidth);
|
|
18
21
|
const outputTop = Math.max(0, viewportHeight - outputLines.length);
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
const editorTop = viewportHeight + statusTopLines.length;
|
|
23
|
+
this.selection?.setRegions?.([
|
|
24
|
+
{ id: "output", topRow: outputTop, leftCol: 0, width: safeWidth, lines: outputLines },
|
|
25
|
+
{ id: "editor", topRow: editorTop, leftCol: 0, width: safeWidth, lines: editorLines },
|
|
26
|
+
]);
|
|
27
|
+
const selectedOutputLines = this.selection?.applyRegion?.("output", outputLines) ?? outputLines;
|
|
28
|
+
const selectedEditorLines = this.selection?.applyRegion?.("editor", editorLines) ?? editorLines;
|
|
21
29
|
return [
|
|
22
30
|
...padToHeight(selectedOutputLines, viewportHeight),
|
|
23
|
-
...
|
|
24
|
-
...
|
|
31
|
+
...statusTopLines,
|
|
32
|
+
...selectedEditorLines,
|
|
33
|
+
...statusBottomLines,
|
|
25
34
|
];
|
|
26
35
|
}
|
|
27
36
|
|
|
@@ -9,30 +9,37 @@ export class ScreenSelection {
|
|
|
9
9
|
this.active = false;
|
|
10
10
|
this.anchor = null;
|
|
11
11
|
this.focus = null;
|
|
12
|
-
this.
|
|
13
|
-
this._plainLines =
|
|
12
|
+
this.regions = [];
|
|
13
|
+
this._plainLines = new Map();
|
|
14
14
|
this.viewport = { topRow: 0, leftCol: 0, width: Infinity, height: 0 };
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
setLines(lines) {
|
|
18
|
-
this.
|
|
19
|
-
this._plainLines = [];
|
|
20
|
-
this.viewport = { topRow: 0, leftCol: 0, width: Infinity, height: this.lines.length };
|
|
18
|
+
this.setViewport({ topRow: 0, leftCol: 0, width: Infinity, lines });
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
setViewport({ topRow = 0, leftCol = 0, width = Infinity, lines = [] } = {}) {
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
this.setRegions([{ id: "default", topRow, leftCol, width, lines }]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setRegions(regions = []) {
|
|
26
|
+
let docRow = 0;
|
|
27
|
+
this.regions = regions
|
|
28
|
+
.map((region, index) => normalizeRegion(region, index))
|
|
29
|
+
.filter((region) => region.lines.length > 0)
|
|
30
|
+
.sort((a, b) => a.topRow - b.topRow || a.leftCol - b.leftCol)
|
|
31
|
+
.map((region) => {
|
|
32
|
+
const normalized = { ...region, docStart: docRow };
|
|
33
|
+
docRow += region.lines.length;
|
|
34
|
+
return normalized;
|
|
35
|
+
});
|
|
36
|
+
this.lines = this.regions.flatMap((region) => region.lines);
|
|
37
|
+
this._plainLines = new Map();
|
|
38
|
+
this.viewport = { topRow: 0, leftCol: 0, width: Infinity, height: this.lines.length };
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
start(point) {
|
|
35
|
-
const normalized = normalizePoint(point, this.
|
|
42
|
+
const normalized = normalizePoint(point, this.regions, true);
|
|
36
43
|
if (!normalized) {
|
|
37
44
|
this.clear();
|
|
38
45
|
return false;
|
|
@@ -45,13 +52,13 @@ export class ScreenSelection {
|
|
|
45
52
|
|
|
46
53
|
update(point) {
|
|
47
54
|
if (!this.active || !this.anchor) return false;
|
|
48
|
-
this.focus = normalizePoint(point, this.
|
|
55
|
+
this.focus = normalizePoint(point, this.regions, true) ?? this.focus;
|
|
49
56
|
return true;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
finish(point, { clear = true } = {}) {
|
|
53
60
|
if (!this.active || !this.anchor) return "";
|
|
54
|
-
this.focus = normalizePoint(point, this.
|
|
61
|
+
this.focus = normalizePoint(point, this.regions, true) ?? this.focus;
|
|
55
62
|
const text = this.text();
|
|
56
63
|
if (clear) this.clear();
|
|
57
64
|
else this.active = false;
|
|
@@ -80,21 +87,27 @@ export class ScreenSelection {
|
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
apply(lines) {
|
|
90
|
+
return this.applyRegion("default", lines);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
applyRegion(id, lines) {
|
|
83
94
|
const range = this.range();
|
|
84
|
-
|
|
95
|
+
const region = this.regions.find((candidate) => candidate.id === id);
|
|
96
|
+
if (!range || !region) return lines;
|
|
85
97
|
return lines.map((line, row) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
98
|
+
const docRow = region.docStart + row;
|
|
99
|
+
if (docRow < range.start.row || docRow > range.end.row) return line;
|
|
100
|
+
const plain = this._plainLine(docRow);
|
|
101
|
+
const startCol = docRow === range.start.row ? range.start.col : 0;
|
|
102
|
+
const endCol = docRow === range.end.row ? range.end.col : visibleWidth(plain);
|
|
90
103
|
if (endCol <= startCol) return line;
|
|
91
104
|
return highlightAnsiLine(line, startCol, endCol);
|
|
92
105
|
});
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
_plainLine(row) {
|
|
96
|
-
if (this._plainLines
|
|
97
|
-
return this._plainLines
|
|
109
|
+
if (!this._plainLines.has(row)) this._plainLines.set(row, stripAnsi(this.lines[row] ?? ""));
|
|
110
|
+
return this._plainLines.get(row);
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
range() {
|
|
@@ -111,19 +124,55 @@ export function stripAnsi(text) {
|
|
|
111
124
|
return String(text ?? "").replace(CONTROL_RE, "");
|
|
112
125
|
}
|
|
113
126
|
|
|
114
|
-
function
|
|
127
|
+
function normalizeRegion(region, index) {
|
|
128
|
+
const lines = [...(region.lines ?? [])];
|
|
129
|
+
const width = Number.isFinite(region.width) ? Math.max(1, Math.trunc(region.width)) : Infinity;
|
|
130
|
+
return {
|
|
131
|
+
id: region.id ?? `region-${index}`,
|
|
132
|
+
lines,
|
|
133
|
+
topRow: Math.max(0, Math.trunc(region.topRow ?? 0)),
|
|
134
|
+
leftCol: Math.max(0, Math.trunc(region.leftCol ?? 0)),
|
|
135
|
+
width,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizePoint({ row, col }, regions, clamp) {
|
|
115
140
|
const screenRow = Math.trunc(row) - 1;
|
|
116
141
|
const screenCol = Math.trunc(col) - 1;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
142
|
+
if (regions.length === 0) return null;
|
|
143
|
+
|
|
144
|
+
for (const region of regions) {
|
|
145
|
+
const localRow = screenRow - region.topRow;
|
|
146
|
+
const localCol = screenCol - region.leftCol;
|
|
147
|
+
const maxCol = Number.isFinite(region.width) ? region.width : Infinity;
|
|
148
|
+
if (localRow >= 0 && localRow < region.lines.length) {
|
|
149
|
+
if (!clamp && (localCol < 0 || localCol > maxCol)) return null;
|
|
150
|
+
return {
|
|
151
|
+
row: region.docStart + localRow,
|
|
152
|
+
col: clampNumber(localCol, 0, maxCol),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!clamp) return null;
|
|
158
|
+
const first = regions[0];
|
|
159
|
+
const last = regions.at(-1);
|
|
160
|
+
if (screenRow < first.topRow) return { row: first.docStart, col: 0 };
|
|
161
|
+
if (screenRow > last.topRow + last.lines.length - 1) {
|
|
162
|
+
return { row: last.docStart + last.lines.length - 1, col: last.width };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let nearest = null;
|
|
166
|
+
for (const region of regions) {
|
|
167
|
+
const beforeDistance = Math.abs(screenRow - region.topRow);
|
|
168
|
+
const afterDistance = Math.abs(screenRow - (region.topRow + region.lines.length - 1));
|
|
169
|
+
const before = { row: region.docStart, col: 0, distance: beforeDistance };
|
|
170
|
+
const after = { row: region.docStart + region.lines.length - 1, col: region.width, distance: afterDistance };
|
|
171
|
+
for (const candidate of [before, after]) {
|
|
172
|
+
if (!nearest || candidate.distance < nearest.distance) nearest = candidate;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return nearest ? { row: nearest.row, col: nearest.col } : null;
|
|
127
176
|
}
|
|
128
177
|
|
|
129
178
|
function comparePoints(a, b) {
|