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,12 +1,17 @@
|
|
|
1
1
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import { statusBar, R } from "../ui-theme.mjs";
|
|
2
|
+
import { modeLabel, statusBar, R } from "../ui-theme.mjs";
|
|
3
3
|
|
|
4
4
|
const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
5
5
|
const DEFAULT_STATUS_TEXT = "March";
|
|
6
|
+
const DEFAULT_HELP_TEXT = "/ commands · ? help";
|
|
7
|
+
const INPUT_BG = "\x1b[48;2;32;34;38m";
|
|
8
|
+
const INPUT_PROMPT = "▌";
|
|
6
9
|
|
|
7
10
|
export class StatusBar {
|
|
8
|
-
constructor(text = DEFAULT_STATUS_TEXT) {
|
|
11
|
+
constructor(text = DEFAULT_STATUS_TEXT, { cwd = process.cwd(), helpText = DEFAULT_HELP_TEXT } = {}) {
|
|
9
12
|
this.text = normalizeStatusText(text);
|
|
13
|
+
this.cwd = normalizeStatusText(cwd);
|
|
14
|
+
this.helpText = normalizeStatusText(helpText);
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
setText(text) {
|
|
@@ -16,22 +21,136 @@ export class StatusBar {
|
|
|
16
21
|
return true;
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
setCwd(cwd) {
|
|
25
|
+
const next = normalizeStatusText(cwd);
|
|
26
|
+
if (next === this.cwd) return false;
|
|
27
|
+
this.cwd = next;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
invalidate() {}
|
|
20
32
|
|
|
21
33
|
render(width) {
|
|
34
|
+
return this.renderTop(width);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
renderTop(width) {
|
|
22
38
|
if (width <= 0) return [""];
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
return [
|
|
39
|
+
const { left, innerWidth, right } = insetForWidth(width);
|
|
40
|
+
const parts = statusParts(this.text);
|
|
41
|
+
const cwdName = currentDirectoryName(this.cwd);
|
|
42
|
+
const lsp = formatLspStatus(parts.lsp);
|
|
43
|
+
const leftText = [cwdName, lsp, parts.context].filter(Boolean).join(" • ");
|
|
44
|
+
const line = composeMetaLine({ left: leftText, right: "", width: innerWidth });
|
|
45
|
+
return [`${left}${line}${right}`, ""];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
renderInputLines(lines, width) {
|
|
49
|
+
if (width <= 0) return [""];
|
|
50
|
+
const { left, innerWidth, right } = insetForWidth(width);
|
|
51
|
+
const contentLines = lines.filter((line) => !isEditorChromeLine(line));
|
|
52
|
+
const visibleLines = contentLines.length > 0 ? contentLines : [""];
|
|
53
|
+
const paintWidth = inputPaintWidth(innerWidth);
|
|
54
|
+
const inputPadding = `${left}${renderInputPaddingLine(paintWidth)}${right}`;
|
|
55
|
+
const inputContent = visibleLines.map((line, index) =>
|
|
56
|
+
`${left}${this.renderInputLine(line, innerWidth, { isFirst: index === 0 })}${right}`,
|
|
57
|
+
);
|
|
58
|
+
return [inputPadding, ...inputContent, inputPadding];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
renderInputLine(line, width, { isFirst = true } = {}) {
|
|
62
|
+
if (width <= 0) return "";
|
|
63
|
+
const paintWidth = inputPaintWidth(width);
|
|
64
|
+
const prompt = isFirst ? statusBar.prompt(INPUT_PROMPT) : " ";
|
|
65
|
+
const promptWidth = visibleWidth(stripAnsi(INPUT_PROMPT));
|
|
66
|
+
const maxContentWidth = Math.max(0, paintWidth - promptWidth - 2);
|
|
67
|
+
const content = clipToWidth(line, maxContentWidth);
|
|
68
|
+
return applyInputBackground(padToWidth(`${prompt}${content}`, paintWidth));
|
|
30
69
|
}
|
|
70
|
+
|
|
71
|
+
renderBottom(width) {
|
|
72
|
+
if (width <= 0) return [""];
|
|
73
|
+
const { left: insetLeft, innerWidth, right: insetRight } = insetForWidth(width);
|
|
74
|
+
const parts = statusParts(this.text);
|
|
75
|
+
const mode = formatModeLabel(parts.mode || DEFAULT_STATUS_TEXT);
|
|
76
|
+
const activity = parts.activity ? statusBar.muted(`${parts.activity} · `) : "";
|
|
77
|
+
const right = [parts.model, parts.thinking].filter(Boolean).join(" • ");
|
|
78
|
+
const paintWidth = inputPaintWidth(innerWidth);
|
|
79
|
+
const line = composeMetaLine({ left: `${activity}${mode}`, right, width: paintWidth, muteLeft: false });
|
|
80
|
+
return ["", `${insetLeft}${line}${insetRight}`];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function insetForWidth(width) {
|
|
85
|
+
const safeWidth = Math.max(1, Math.trunc(width));
|
|
86
|
+
return { left: "", innerWidth: safeWidth, right: "" };
|
|
31
87
|
}
|
|
32
88
|
|
|
33
|
-
function
|
|
34
|
-
|
|
89
|
+
function composeMetaLine({ left, right, width, muteLeft = true }) {
|
|
90
|
+
const safeWidth = Math.max(1, Math.trunc(width));
|
|
91
|
+
const rightWidth = visibleWidth(stripAnsi(right));
|
|
92
|
+
const colorLeft = (text) => (muteLeft ? statusBar.muted(text) : text);
|
|
93
|
+
if (!right) return colorLeft(padToWidth(clipToWidth(left, safeWidth), safeWidth));
|
|
94
|
+
if (rightWidth >= safeWidth) return statusBar.muted(padToWidth(clipToWidth(right, safeWidth), safeWidth));
|
|
95
|
+
|
|
96
|
+
const maxLeftWidth = Math.max(0, safeWidth - rightWidth - 1);
|
|
97
|
+
const fittedLeft = maxLeftWidth > 0 ? clipToWidth(left, maxLeftWidth) : "";
|
|
98
|
+
const gap = Math.max(1, safeWidth - visibleWidth(stripAnsi(fittedLeft)) - rightWidth);
|
|
99
|
+
return `${colorLeft(fittedLeft)}${" ".repeat(gap)}${statusBar.muted(right)}${R}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderInputPaddingLine(width) {
|
|
103
|
+
return applyInputBackground(" ".repeat(Math.max(1, Math.trunc(width))));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function inputPaintWidth(width) {
|
|
107
|
+
const safeWidth = Math.max(1, Math.trunc(width));
|
|
108
|
+
return safeWidth > 1 ? safeWidth - 1 : safeWidth;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function applyInputBackground(line) {
|
|
112
|
+
return `${INPUT_BG}${String(line).replaceAll(R, `${R}${INPUT_BG}`)}${R}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isEditorChromeLine(line) {
|
|
116
|
+
const plain = stripAnsi(line).trim();
|
|
117
|
+
return plain.length > 0 && (/^─+$/.test(plain) || /^─+\s[↑↓].*more\s─*$/.test(plain));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function currentDirectoryName(path) {
|
|
121
|
+
const normalized = normalizeStatusText(path);
|
|
122
|
+
const parts = normalized.split(/[\\/]+/).filter(Boolean);
|
|
123
|
+
return parts.at(-1) || normalized || DEFAULT_STATUS_TEXT;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatLspStatus(lsp) {
|
|
127
|
+
if (!lsp) return "LSP off";
|
|
128
|
+
const server = lsp.replace(/^lsp:/, "").replace(/[✓✗]$/u, "").trim();
|
|
129
|
+
return server ? `LSP [${server}]` : "LSP off";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatModeLabel(mode) {
|
|
133
|
+
const label = normalizeStatusText(mode);
|
|
134
|
+
const color = modeLabel[label.toLowerCase()] || modeLabel.fallback;
|
|
135
|
+
return color(label);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function statusParts(text) {
|
|
139
|
+
const segments = plainSegments(text);
|
|
140
|
+
const runtime = segments.find((segment) => segment.includes("·")) || "";
|
|
141
|
+
const [model = "", thinking = ""] = runtime.split("·").map((part) => part.trim());
|
|
142
|
+
const lsp = segments.find((segment) => segment.startsWith("lsp:")) || "";
|
|
143
|
+
const context = [...segments].reverse().find((segment) => /^\d+(?:\.\d+)?[KM]?$/.test(segment)) || "";
|
|
144
|
+
const activity = segments.find((segment) => /(?:Working|Aborted)$/.test(segment)) || "";
|
|
145
|
+
const mode = segments.find((segment) => segment && segment !== runtime && segment !== lsp && segment !== activity && segment !== context) || segments[0] || "";
|
|
146
|
+
return { mode, model, thinking, lsp, activity, context };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function plainSegments(text) {
|
|
150
|
+
return stripAnsi(normalizeStatusText(text))
|
|
151
|
+
.split(" | ")
|
|
152
|
+
.map((segment) => segment.trim())
|
|
153
|
+
.filter(Boolean);
|
|
35
154
|
}
|
|
36
155
|
|
|
37
156
|
export function normalizeStatusText(text) {
|
|
@@ -64,15 +183,15 @@ export function fitStatusText(text, width) {
|
|
|
64
183
|
}
|
|
65
184
|
|
|
66
185
|
export function clipToWidth(text, width) {
|
|
67
|
-
// For ANSI-containing text, build output character by character and measure plain width
|
|
68
186
|
let output = "";
|
|
69
187
|
let plainWidth = 0;
|
|
70
|
-
|
|
71
|
-
for (
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
188
|
+
const chars = Array.from(String(text || ""));
|
|
189
|
+
for (let index = 0; index < chars.length; index += 1) {
|
|
190
|
+
const ch = chars[index];
|
|
191
|
+
if (ch === "\x1b") {
|
|
192
|
+
const { sequence, nextIndex } = readAnsiSequence(chars, index);
|
|
193
|
+
output += sequence;
|
|
194
|
+
index = nextIndex;
|
|
76
195
|
continue;
|
|
77
196
|
}
|
|
78
197
|
const charWidth = visibleWidth(ch);
|
|
@@ -83,6 +202,24 @@ export function clipToWidth(text, width) {
|
|
|
83
202
|
return output;
|
|
84
203
|
}
|
|
85
204
|
|
|
205
|
+
function readAnsiSequence(chars, startIndex) {
|
|
206
|
+
let sequence = chars[startIndex];
|
|
207
|
+
let index = startIndex;
|
|
208
|
+
const intro = chars[startIndex + 1];
|
|
209
|
+
if (!intro) return { sequence, nextIndex: index };
|
|
210
|
+
sequence += intro;
|
|
211
|
+
index += 1;
|
|
212
|
+
if (intro !== "[") return { sequence, nextIndex: index };
|
|
213
|
+
|
|
214
|
+
while (index + 1 < chars.length) {
|
|
215
|
+
index += 1;
|
|
216
|
+
const ch = chars[index];
|
|
217
|
+
sequence += ch;
|
|
218
|
+
if (/[\x40-\x7e]/.test(ch)) break;
|
|
219
|
+
}
|
|
220
|
+
return { sequence, nextIndex: index };
|
|
221
|
+
}
|
|
222
|
+
|
|
86
223
|
function stripAnsi(text) {
|
|
87
224
|
return String(text ?? "").replace(ANSI_RE, "");
|
|
88
225
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { extractToolOutput } from "../tool-output.mjs";
|
|
2
|
+
import { formatToolStartLine, formatToolSuccessSummary } from "../../agent/tool-summary.mjs";
|
|
2
3
|
import { dim, red } from "./ui-theme.mjs";
|
|
3
4
|
|
|
5
|
+
export { formatToolStartLine, formatToolSuccessSummary } from "../../agent/tool-summary.mjs";
|
|
6
|
+
|
|
4
7
|
const TOOL_BODY_LIMIT = 40;
|
|
5
8
|
const TOOL_ERROR_LIMIT = 6;
|
|
6
9
|
|
|
@@ -44,58 +47,6 @@ export function writeToolEnd({
|
|
|
44
47
|
return true;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
export function formatToolStartLine(name, args = {}) {
|
|
48
|
-
if (name === "edit_file") {
|
|
49
|
-
const path = compactPath(args?.path ?? "");
|
|
50
|
-
const editCount = Array.isArray(args?.edits) ? args.edits.length : 0;
|
|
51
|
-
const mode = args?.mode ?? "patch";
|
|
52
|
-
const summary = mode === "patch" ? `${editCount} edit${editCount === 1 ? "" : "s"}` : mode;
|
|
53
|
-
return joinToolParts("◆", name, [path, summary]);
|
|
54
|
-
}
|
|
55
|
-
if (name === "command_exec") return joinToolParts("◆", name, [compactText(args?.command ?? "")]);
|
|
56
|
-
if (name === "terminal_send") return joinToolParts("◆", name, [args?.shell_id, formatTerminalSendAction(args)]);
|
|
57
|
-
if (name?.startsWith?.("terminal_")) return joinToolParts("◆", name, [args?.shell_id, formatTerminalDetails(args)]);
|
|
58
|
-
if (name === "external_web_search") return joinToolParts("◆", name, [quoteCompact(args?.query ?? "")]);
|
|
59
|
-
if (name === "web_fetch") return joinToolParts("◆", name, [compactText(args?.url ?? "")]);
|
|
60
|
-
if (name === "context_stats") return joinToolParts("◆", name, []);
|
|
61
|
-
if (name === "read") {
|
|
62
|
-
const path = compactPath(args?.path ?? args?.filePath ?? "");
|
|
63
|
-
return joinToolParts("→", name, [path, formatReadRange(args)]);
|
|
64
|
-
}
|
|
65
|
-
if (name === "grep") {
|
|
66
|
-
const path = compactPath(args?.path ?? "");
|
|
67
|
-
return joinToolParts("✱", name, [quoteCompact(args?.pattern ?? ""), path]);
|
|
68
|
-
}
|
|
69
|
-
if (name === "glob") {
|
|
70
|
-
const path = compactPath(args?.path ?? "");
|
|
71
|
-
return joinToolParts("✱", name, [quoteCompact(args?.pattern ?? ""), path]);
|
|
72
|
-
}
|
|
73
|
-
if (name === "find") {
|
|
74
|
-
const path = compactPath(args?.path ?? "");
|
|
75
|
-
return joinToolParts("✱", name, [quoteCompact(args?.pattern ?? ""), path]);
|
|
76
|
-
}
|
|
77
|
-
return joinToolParts("◆", name, [formatSmallOptions(args)]);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function formatToolSuccessSummary(name, result, out = "") {
|
|
81
|
-
if (name === "grep") {
|
|
82
|
-
const matches = result?.details?.results?.length ?? countMatchLines(out);
|
|
83
|
-
return `${matches} match${matches === 1 ? "" : "es"}`;
|
|
84
|
-
}
|
|
85
|
-
if (name === "glob") {
|
|
86
|
-
const matches = Array.isArray(result?.details?.matches) ? result.details.matches.length : countNonEmptyLines(out);
|
|
87
|
-
return `${matches} file${matches === 1 ? "" : "s"}`;
|
|
88
|
-
}
|
|
89
|
-
if (name === "find") {
|
|
90
|
-
const matches = result?.details?.count ?? countNonEmptyLines(out);
|
|
91
|
-
return `${matches} file${matches === 1 ? "" : "s"}`;
|
|
92
|
-
}
|
|
93
|
-
if (name === "memory_open") {
|
|
94
|
-
return compactText(result?.details?.entry?.name ?? compactPath(result?.details?.path ?? ""));
|
|
95
|
-
}
|
|
96
|
-
return "";
|
|
97
|
-
}
|
|
98
|
-
|
|
99
50
|
function formatToolEndCard({ name, isError, result, extractToolOutputImpl }) {
|
|
100
51
|
const out = extractToolOutputImpl(result);
|
|
101
52
|
if (isError) {
|
|
@@ -134,64 +85,3 @@ function writeStructuredLines(output, block) {
|
|
|
134
85
|
for (const line of lines) output.writeln(line);
|
|
135
86
|
}
|
|
136
87
|
}
|
|
137
|
-
|
|
138
|
-
function joinToolParts(icon, name, parts) {
|
|
139
|
-
const clean = parts.map((part) => String(part ?? "").trim()).filter(Boolean);
|
|
140
|
-
return `${icon} ${name}${clean.length ? ` · ${clean.join(" · ")}` : ""}`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function formatReadRange(args = {}) {
|
|
144
|
-
if (args.offset == null && args.limit == null) return "";
|
|
145
|
-
if (args.offset != null && args.limit != null) return `lines ${args.offset}-${Number(args.offset) + Number(args.limit) - 1}`;
|
|
146
|
-
if (args.offset != null) return `from line ${args.offset}`;
|
|
147
|
-
return `limit ${args.limit}`;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function formatTerminalSendAction(args = {}) {
|
|
151
|
-
const hasText = typeof args.text === "string" && args.text.length > 0;
|
|
152
|
-
const key = args.key ? String(args.key) : "";
|
|
153
|
-
if (hasText && key) return `text+${key}`;
|
|
154
|
-
if (hasText) return args.text.includes("\n") || args.text.includes("\r") ? "text+enter" : "text";
|
|
155
|
-
return key || "send";
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function formatTerminalDetails(args = {}) {
|
|
159
|
-
const details = [];
|
|
160
|
-
if (args.pattern) details.push(quoteCompact(args.pattern));
|
|
161
|
-
if (args.cols && args.rows) details.push(`${args.cols}x${args.rows}`);
|
|
162
|
-
if (args.command) details.push(compactText(args.command));
|
|
163
|
-
return details.join(" · ");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function formatSmallOptions(args = {}) {
|
|
167
|
-
const parts = [];
|
|
168
|
-
for (const [key, value] of Object.entries(args ?? {})) {
|
|
169
|
-
if (value == null || typeof value === "object") continue;
|
|
170
|
-
parts.push(`${key}=${compactText(value)}`);
|
|
171
|
-
if (parts.length >= 2) break;
|
|
172
|
-
}
|
|
173
|
-
return parts.join(", ");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function compactPath(path) {
|
|
177
|
-
return String(path ?? "").split(/[/\\]/).filter(Boolean).slice(-4).join("\\");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function quoteCompact(value) {
|
|
181
|
-
return JSON.stringify(compactText(value));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function compactText(value, limit = 80) {
|
|
185
|
-
const text = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
186
|
-
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function countMatchLines(text) {
|
|
190
|
-
const match = String(text ?? "").match(/(\d+)\s+matches?\b/i);
|
|
191
|
-
if (match) return Number(match[1]);
|
|
192
|
-
return countNonEmptyLines(text);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function countNonEmptyLines(text) {
|
|
196
|
-
return String(text ?? "").split("\n").filter(Boolean).length;
|
|
197
|
-
}
|
|
@@ -56,7 +56,7 @@ export function wireTuiHandlers({
|
|
|
56
56
|
ui.writeln(brightBlack(`● thinking: unchanged`));
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
|
-
ui.writeln(brightBlack(`● thinking: ${runner.setThinkingLevel(item.level)}`));
|
|
59
|
+
ui.writeln(brightBlack(`● thinking: ${await runner.setThinkingLevel(item.level)}`));
|
|
60
60
|
refreshStatusBar();
|
|
61
61
|
} catch (err) {
|
|
62
62
|
ui.writeln(`Error: ${err.message}`);
|
package/src/cli/tui/ui-theme.mjs
CHANGED
|
@@ -17,6 +17,7 @@ const brightRed = (s) => `\x1b[91m${s}${R}`;
|
|
|
17
17
|
const brightGreen = (s) => `\x1b[92m${s}${R}`;
|
|
18
18
|
const orange = (s) => `\x1b[38;2;245;167;66m${s}${R}`;
|
|
19
19
|
const softGreen = (s) => `\x1b[38;2;127;216;143m${s}${R}`;
|
|
20
|
+
const violet = (s) => `\x1b[38;2;232;91;226m${s}${R}`;
|
|
20
21
|
|
|
21
22
|
// ── Formatters ───────────────────────────────────────────────────────
|
|
22
23
|
const bold = (s) => `${B}${s}${R}`;
|
|
@@ -79,9 +80,16 @@ const message = {
|
|
|
79
80
|
};
|
|
80
81
|
|
|
81
82
|
const statusBar = {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
muted: brightBlack,
|
|
84
|
+
cwd: (s) => `${D}\x1b[38;5;244m${s}${R}`,
|
|
85
|
+
prompt: fg256(250),
|
|
86
|
+
accent: violet,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const modeLabel = {
|
|
90
|
+
do: orange,
|
|
91
|
+
discuss: green,
|
|
92
|
+
fallback: orange,
|
|
85
93
|
};
|
|
86
94
|
|
|
87
95
|
const shell = {
|
|
@@ -105,7 +113,7 @@ const selectList = {
|
|
|
105
113
|
|
|
106
114
|
// ── Editor theme (consumed by pi-tui Editor component) ──────────────
|
|
107
115
|
const EDITOR_THEME = {
|
|
108
|
-
borderColor:
|
|
116
|
+
borderColor: fg256(238),
|
|
109
117
|
selectList,
|
|
110
118
|
};
|
|
111
119
|
|
|
@@ -127,7 +135,7 @@ export {
|
|
|
127
135
|
R, B, D,
|
|
128
136
|
black, red, green, yellow, blue, magenta, cyan, white,
|
|
129
137
|
brightBlack, brightRed, brightGreen,
|
|
130
|
-
orange, softGreen,
|
|
138
|
+
orange, softGreen, violet,
|
|
131
139
|
bold, dim, inverse,
|
|
132
140
|
fg256, bg256,
|
|
133
141
|
// Semantic
|
|
@@ -140,6 +148,7 @@ export {
|
|
|
140
148
|
tool,
|
|
141
149
|
message,
|
|
142
150
|
statusBar,
|
|
151
|
+
modeLabel,
|
|
143
152
|
shell,
|
|
144
153
|
spinner,
|
|
145
154
|
selectList,
|
package/src/cli/ui.mjs
CHANGED
|
@@ -41,7 +41,7 @@ export function createTuiUI({
|
|
|
41
41
|
const tui = new TUI(terminal);
|
|
42
42
|
const output = new OutputBuffer();
|
|
43
43
|
const shellDrawer = new ShellDrawer({ shellRuntime });
|
|
44
|
-
const statusBar = new StatusBar();
|
|
44
|
+
const statusBar = new StatusBar(undefined, { cwd });
|
|
45
45
|
const editor = new Editor(tui, EDITOR_THEME, { paddingX: 1 });
|
|
46
46
|
const selection = new ScreenSelection();
|
|
47
47
|
const mainPane = new MainPaneLayout({ output, statusBar, editor, terminal, selection });
|
|
@@ -38,6 +38,17 @@ export function upsertProviderProfile({ path = globalConfigJsonPath(), id, type,
|
|
|
38
38
|
return config;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function upsertSharedProviderProfile({ path = globalConfigJsonPath(), id, provider }) {
|
|
42
|
+
const config = readConfigJson(path);
|
|
43
|
+
const providers = config.providers && typeof config.providers === "object" && !Array.isArray(config.providers)
|
|
44
|
+
? config.providers
|
|
45
|
+
: {};
|
|
46
|
+
providers[id] = provider;
|
|
47
|
+
config.providers = providers;
|
|
48
|
+
writeConfigJson(path, config);
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
export function upsertModelSelection({ path = globalConfigJsonPath(), provider, model, serviceTier }) {
|
|
42
53
|
const config = readConfigJson(path);
|
|
43
54
|
config.provider = provider;
|
package/src/context/engine.mjs
CHANGED
|
@@ -3,14 +3,14 @@ import { buildSessionIdentity } from "./session-status.mjs";
|
|
|
3
3
|
import { buildSystemCore, resolveSystemCorePromptKey } from "./system-core.mjs";
|
|
4
4
|
import { buildInjectionsLayer } from "./injections.mjs";
|
|
5
5
|
import { buildProjectContext } from "./project-context.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { buildProfileLayers } from "./profiles.mjs";
|
|
7
7
|
import { formatRecallHints } from "../memory/markdown-store.mjs";
|
|
8
8
|
|
|
9
9
|
export class ContextEngine {
|
|
10
|
-
constructor({ cwd, modelId, provider = "deepseek", thinkingLevel = "medium", namespace = "", memoryRoot = null,
|
|
10
|
+
constructor({ cwd, modelId, provider = "deepseek", thinkingLevel = "medium", namespace = "", memoryRoot = null, profilePaths = null, shellRuntime = null, lspService = null, injections = [], maxTurns, trimBatch }) {
|
|
11
11
|
this.cwd = cwd;
|
|
12
12
|
this.memoryRoot = memoryRoot;
|
|
13
|
-
this.
|
|
13
|
+
this.profilePaths = profilePaths;
|
|
14
14
|
this.modelId = modelId;
|
|
15
15
|
this.provider = provider;
|
|
16
16
|
this.thinkingLevel = thinkingLevel;
|
|
@@ -62,19 +62,19 @@ export class ContextEngine {
|
|
|
62
62
|
const projectCtx = buildProjectContext(this.cwd);
|
|
63
63
|
if (projectCtx) layers.push({ name: "project_context", text: projectCtx });
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
if (centerMemory) layers.push({ name: "center_memory", text: centerMemory });
|
|
65
|
+
layers.push(...buildProfileLayers(this.profilePaths));
|
|
67
66
|
|
|
68
67
|
layers.push({ name: "recent_chat", text: this.#buildRecentChat() });
|
|
69
68
|
|
|
70
69
|
return layers;
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
recordTurn({ userMessage, assistantMessage, userRecallHints = [], assistantRecallHints = [] }) {
|
|
72
|
+
recordTurn({ userMessage, assistantMessage, assistantContext = "", userRecallHints = [], assistantRecallHints = [] }) {
|
|
74
73
|
this.turns.push({
|
|
75
74
|
index: this.turns.length + 1,
|
|
76
75
|
userMessage,
|
|
77
76
|
assistantMessage: assistantMessage ?? "",
|
|
77
|
+
assistantContext: assistantContext ?? "",
|
|
78
78
|
userRecallHints,
|
|
79
79
|
assistantRecallHints,
|
|
80
80
|
});
|
|
@@ -167,9 +167,10 @@ export class ContextEngine {
|
|
|
167
167
|
`[user]\n${String(turn.userMessage ?? "")}\n`;
|
|
168
168
|
const userRecall = formatRecallHints("user", turn.userRecallHints ?? []);
|
|
169
169
|
if (userRecall) block += `\n${userRecall}\n`;
|
|
170
|
-
block += `\n[
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
block += `\n[assistant]\n`;
|
|
171
|
+
const assistantText = turn.assistantContext || turn.assistantMessage;
|
|
172
|
+
if (assistantText) {
|
|
173
|
+
block += `\n${String(assistantText ?? "")}\n`;
|
|
173
174
|
}
|
|
174
175
|
const assistantRecall = formatRecallHints("assistant", turn.assistantRecallHints ?? []);
|
|
175
176
|
if (assistantRecall) block += `\n${assistantRecall}\n`;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export function defaultProfilePaths() {
|
|
6
|
+
const root = join(homedir(), ".march", "memory", "profiles");
|
|
7
|
+
return {
|
|
8
|
+
agent: join(root, "agent.md"),
|
|
9
|
+
user: join(root, "user.md"),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ensureProfileFiles(paths = defaultProfilePaths()) {
|
|
14
|
+
for (const [kind, path] of Object.entries(paths)) {
|
|
15
|
+
if (!path || existsSync(path)) continue;
|
|
16
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
17
|
+
writeFileSync(path, defaultProfileContent(kind), "utf8");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildProfileLayers(paths) {
|
|
22
|
+
if (!paths) return [];
|
|
23
|
+
return [
|
|
24
|
+
buildProfileLayer("agent_profile", paths.agent),
|
|
25
|
+
buildProfileLayer("user_profile", paths.user),
|
|
26
|
+
].filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildProfileLayer(name, path) {
|
|
30
|
+
if (!path || !existsSync(path)) return null;
|
|
31
|
+
const content = readFileSync(path, "utf8").trimEnd();
|
|
32
|
+
if (!content.trim()) return null;
|
|
33
|
+
return { name, text: `[${name}]\n--- ${path} ---\n${content}` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function defaultProfileContent(kind) {
|
|
37
|
+
const title = kind === "agent" ? "Agent Profile" : "User Profile";
|
|
38
|
+
return `# ${title}\n\n`;
|
|
39
|
+
}
|
|
@@ -53,7 +53,8 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
|
|
|
53
53
|
</git_contract>
|
|
54
54
|
|
|
55
55
|
<memory_system>
|
|
56
|
-
- [memory_hint source="..."] blocks in recent_chat
|
|
56
|
+
- [memory_hint source="..."] blocks in recent_chat are lightweight recall hints matched from prior thinking output. Treat them as possibly relevant pointers, not as complete facts.
|
|
57
|
+
- If a memory hint may help the current task, use memory_open(id) to read the full memory before relying on it. Ignore hints that are clearly unrelated or too low-value for the task.
|
|
57
58
|
- Use memory_search(query) for full-text search across all memories.
|
|
58
59
|
- To edit an existing memory, use memory_open(id) to get its path, then edit_file with mode="patch" for targeted edits.
|
|
59
60
|
- Use memory_save() to create memories or update whole fields. Before creating a new memory, first search/open related memories and merge updates into an existing memory when they share the same topic, project, or decision thread; prefer modifying the existing memory file over creating a scattered new one. Tags are the primary retrieval key for future recall. Prefer lowercase kebab-case tags like 'march-cli', 'tooling', 'permissions'.
|