march-cli 0.1.11 → 0.1.13
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/command-exec-tool.mjs +42 -8
- package/src/agent/runner/runner-utils.mjs +6 -0
- package/src/agent/runner.mjs +16 -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/ui-event-bridge.mjs +85 -0
- package/src/agent/tool-names.mjs +1 -1
- package/src/agent/tool-summary.mjs +112 -0
- package/src/agent/tools.mjs +0 -3
- package/src/agent/turn/turn-events.mjs +46 -0
- package/src/agent/turn/turn-runner.mjs +2 -1
- 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 +154 -18
- package/src/cli/tui/syntax/highlighting.mjs +7 -24
- package/src/cli/tui/syntax/languages.mjs +1 -1
- 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/context/engine.mjs +10 -9
- package/src/context/profiles.mjs +39 -0
- package/src/main.mjs +35 -29
- package/src/agent/find-tool.mjs +0 -112
- package/src/context/center-memory.mjs +0 -14
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { createRunner } from "../runner.mjs";
|
|
3
|
+
import { createProcessRuntimeIpcPeer } from "./ipc/process-ipc-transport.mjs";
|
|
4
|
+
import { createRemoteRuntimeUiClient } from "./remote-ui-client.mjs";
|
|
5
|
+
import { createRunnerIpcTarget } from "./runner-ipc-target.mjs";
|
|
6
|
+
import { createMarchAuthStorage } from "../../auth/storage.mjs";
|
|
7
|
+
import { createCliShellRuntime } from "../../shell/cli-runtime.mjs";
|
|
8
|
+
import { MarkdownMemoryStore } from "../../memory/markdown-store.mjs";
|
|
9
|
+
import { createMarkdownMemoryTools } from "../../memory/markdown-tools.mjs";
|
|
10
|
+
import { initializeMcp } from "../../mcp/index.mjs";
|
|
11
|
+
import { createWebToolsFromConfig } from "../../web/tools.mjs";
|
|
12
|
+
import { createPermissionController } from "../../cli/permissions.mjs";
|
|
13
|
+
import { resolvePiSessionManager } from "../../session/pi-manager.mjs";
|
|
14
|
+
import { createModelContextDumper } from "../../debug/model-context-dumper.mjs";
|
|
15
|
+
import { createLogger, installProcessLogHandlers } from "../../debug/logger.mjs";
|
|
16
|
+
import { createDesktopTurnNotifier } from "../../notification/desktop-notifier.mjs";
|
|
17
|
+
|
|
18
|
+
const peer = createProcessRuntimeIpcPeer({
|
|
19
|
+
target: createRunnerIpcTarget({ createRunnerImpl: createIsolatedRunner }),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
process.once("disconnect", () => peer.dispose());
|
|
23
|
+
|
|
24
|
+
async function createIsolatedRunner(options = {}) {
|
|
25
|
+
const ui = createRemoteRuntimeUiClient(peer);
|
|
26
|
+
const memoryStore = new MarkdownMemoryStore({ root: options.memoryRoot });
|
|
27
|
+
const memoryTools = createMarkdownMemoryTools(memoryStore);
|
|
28
|
+
const shellRuntime = options.shellRuntime ? createCliShellRuntime({ cwd: options.cwd }) : null;
|
|
29
|
+
const mcpInit = await initializeMcp({ projectDir: options.cwd });
|
|
30
|
+
const logger = createLogger({ logDir: options.logDir ?? (options.stateRoot ? join(options.stateRoot, "logs") : undefined) });
|
|
31
|
+
installProcessLogHandlers(logger);
|
|
32
|
+
|
|
33
|
+
const runner = await createRunner({
|
|
34
|
+
cwd: options.cwd,
|
|
35
|
+
modelId: options.modelId,
|
|
36
|
+
provider: options.provider,
|
|
37
|
+
serviceTier: options.serviceTier,
|
|
38
|
+
providers: options.providers,
|
|
39
|
+
stateRoot: options.stateRoot,
|
|
40
|
+
ui,
|
|
41
|
+
memoryRoot: options.memoryRoot,
|
|
42
|
+
profilePaths: options.profilePaths,
|
|
43
|
+
memoryStore,
|
|
44
|
+
memoryTools,
|
|
45
|
+
shellRuntime,
|
|
46
|
+
mcpTools: mcpInit.mcpTools,
|
|
47
|
+
mcpInjections: mcpInit.mcpInjections,
|
|
48
|
+
mcpClientManager: mcpInit.clientManager,
|
|
49
|
+
webTools: createWebToolsFromConfig(options.config ?? {}),
|
|
50
|
+
namespace: options.namespace,
|
|
51
|
+
projectMarchDir: options.projectMarchDir,
|
|
52
|
+
extensionPaths: options.extensionPaths ?? [],
|
|
53
|
+
sessionManager: resolvePiSessionManager({
|
|
54
|
+
cwd: options.cwd,
|
|
55
|
+
projectMarchDir: options.projectMarchDir,
|
|
56
|
+
enabled: true,
|
|
57
|
+
}),
|
|
58
|
+
useRuntimeHost: true,
|
|
59
|
+
syncPiSidecar: true,
|
|
60
|
+
lifecycleHooks: options.lifecycleHooks ?? [],
|
|
61
|
+
lifecycleDiagnostics: options.lifecycleDiagnostics ?? [],
|
|
62
|
+
authStorage: createMarchAuthStorage({
|
|
63
|
+
provider: options.provider ?? "deepseek",
|
|
64
|
+
providers: options.providers,
|
|
65
|
+
cwd: options.cwd,
|
|
66
|
+
}).authStorage,
|
|
67
|
+
maxTurns: options.config?.maxTurns ?? undefined,
|
|
68
|
+
trimBatch: options.config?.trimBatch ?? undefined,
|
|
69
|
+
hostedTools: options.config?.hostedTools,
|
|
70
|
+
permissionController: createPermissionController({ mode: options.permissionMode }),
|
|
71
|
+
modelContextDumper: createModelContextDumper(options.modelContextDumper ?? { enabled: false }),
|
|
72
|
+
turnNotifier: createDesktopTurnNotifier({
|
|
73
|
+
enabled: Boolean(options.config?.notifications?.turnEnd),
|
|
74
|
+
config: options.config?.notifications,
|
|
75
|
+
}),
|
|
76
|
+
logger,
|
|
77
|
+
onModelPayload: (event) => peer.notify("modelPayload", pickModelPayloadEvent(event)),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const originalDispose = runner.dispose;
|
|
81
|
+
runner.dispose = async () => {
|
|
82
|
+
try {
|
|
83
|
+
await originalDispose.call(runner);
|
|
84
|
+
} finally {
|
|
85
|
+
memoryStore.close?.();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return runner;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function pickModelPayloadEvent({ estimatedTokens, provider, model, kind, turnId } = {}) {
|
|
92
|
+
return { estimatedTokens, provider, model, kind, turnId };
|
|
93
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export function createRuntimeUiEventTarget(ui) {
|
|
2
|
+
return {
|
|
3
|
+
uiEvent: (event) => dispatchRuntimeUiEvent(ui, event),
|
|
4
|
+
uiRequest: (event) => dispatchRuntimeUiEvent(ui, event),
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createRuntimeUiEventBus() {
|
|
9
|
+
const listeners = new Set();
|
|
10
|
+
return {
|
|
11
|
+
on(listener) {
|
|
12
|
+
listeners.add(listener);
|
|
13
|
+
return () => listeners.delete(listener);
|
|
14
|
+
},
|
|
15
|
+
emit(event) {
|
|
16
|
+
for (const listener of [...listeners]) listener(event);
|
|
17
|
+
},
|
|
18
|
+
async request(event) {
|
|
19
|
+
let response;
|
|
20
|
+
for (const listener of [...listeners]) {
|
|
21
|
+
const result = await listener(event);
|
|
22
|
+
if (response === undefined && result !== undefined) response = result;
|
|
23
|
+
}
|
|
24
|
+
return response;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createRuntimeUiBridge(ui, { eventBus = createRuntimeUiEventBus() } = {}) {
|
|
30
|
+
const detach = eventBus.on((event) => dispatchRuntimeUiEvent(ui, event));
|
|
31
|
+
return {
|
|
32
|
+
ui: createRuntimeUiClient(eventBus),
|
|
33
|
+
eventBus,
|
|
34
|
+
detach,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createRuntimeUiClient(eventBus) {
|
|
39
|
+
return {
|
|
40
|
+
turnStart: () => eventBus.emit({ type: "turn_start" }),
|
|
41
|
+
turnEnd: () => eventBus.emit({ type: "turn_end" }),
|
|
42
|
+
assistantReplyEnd: () => eventBus.emit({ type: "assistant_reply_end" }),
|
|
43
|
+
textDelta: (delta) => eventBus.emit({ type: "text_delta", delta }),
|
|
44
|
+
thinkingStart: () => eventBus.emit({ type: "thinking_start" }),
|
|
45
|
+
thinkingDelta: (delta) => eventBus.emit({ type: "thinking_delta", delta }),
|
|
46
|
+
thinkingEnd: (tokens) => eventBus.emit({ type: "thinking_end", tokens }),
|
|
47
|
+
toolStart: (name, args) => eventBus.emit({ type: "tool_start", name, args }),
|
|
48
|
+
toolEnd: (name, isError, result) => eventBus.emit({ type: "tool_end", name, isError, result }),
|
|
49
|
+
retryStart: (event) => eventBus.emit({ type: "retry_start", ...event }),
|
|
50
|
+
retryEnd: (event) => eventBus.emit({ type: "retry_end", ...event }),
|
|
51
|
+
status: (text) => eventBus.emit({ type: "status", text }),
|
|
52
|
+
memoryHint: ({ source, hints }) => eventBus.emit({ type: "memory_hint", source, hints }),
|
|
53
|
+
editDiff: (path, diffLines) => eventBus.emit({ type: "edit_diff", path, diffLines }),
|
|
54
|
+
requestPermission: (request) => eventBus.request({ type: "permission_request", ...request }),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function dispatchRuntimeUiEvent(ui, event) {
|
|
59
|
+
switch (event.type) {
|
|
60
|
+
case "turn_start": return ui.turnStart?.();
|
|
61
|
+
case "turn_end": return ui.turnEnd?.();
|
|
62
|
+
case "assistant_reply_end": return ui.assistantReplyEnd?.();
|
|
63
|
+
case "text_delta": return ui.textDelta?.(event.delta);
|
|
64
|
+
case "thinking_start": return ui.thinkingStart?.();
|
|
65
|
+
case "thinking_delta": return ui.thinkingDelta?.(event.delta);
|
|
66
|
+
case "thinking_end": return ui.thinkingEnd?.(event.tokens);
|
|
67
|
+
case "tool_start": return ui.toolStart?.(event.name, event.args);
|
|
68
|
+
case "tool_end": return ui.toolEnd?.(event.name, event.isError, event.result);
|
|
69
|
+
case "retry_start": return ui.retryStart?.(pickRetryStart(event));
|
|
70
|
+
case "retry_end": return ui.retryEnd?.(pickRetryEnd(event));
|
|
71
|
+
case "status": return ui.status?.(event.text);
|
|
72
|
+
case "memory_hint": return ui.memoryHint?.({ source: event.source, hints: event.hints });
|
|
73
|
+
case "edit_diff": return ui.editDiff?.(event.path, event.diffLines);
|
|
74
|
+
case "permission_request": return ui.requestPermission?.({ toolName: event.toolName, params: event.params, category: event.category });
|
|
75
|
+
default: return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function pickRetryStart({ attempt, maxAttempts, delayMs, errorMessage }) {
|
|
80
|
+
return { attempt, maxAttempts, delayMs, errorMessage };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function pickRetryEnd({ success, attempt, finalError }) {
|
|
84
|
+
return { success, attempt, finalError };
|
|
85
|
+
}
|
package/src/agent/tool-names.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MARCH_BASE_TOOL_NAMES = ["grep", "ls"];
|
|
1
|
+
export const MARCH_BASE_TOOL_NAMES = ["grep", "find", "ls"];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export function formatToolStartLine(name, args = {}) {
|
|
2
|
+
if (name === "edit_file") {
|
|
3
|
+
const path = compactPath(args?.path ?? "");
|
|
4
|
+
const editCount = Array.isArray(args?.edits) ? args.edits.length : 0;
|
|
5
|
+
const mode = args?.mode ?? "patch";
|
|
6
|
+
const summary = mode === "patch" ? `${editCount} edit${editCount === 1 ? "" : "s"}` : mode;
|
|
7
|
+
return joinToolParts("◆", name, [path, summary]);
|
|
8
|
+
}
|
|
9
|
+
if (name === "command_exec") return joinToolParts("◆", name, [compactText(args?.command ?? "")]);
|
|
10
|
+
if (name === "terminal_send") return joinToolParts("◆", name, [args?.shell_id, formatTerminalSendAction(args)]);
|
|
11
|
+
if (name?.startsWith?.("terminal_")) return joinToolParts("◆", name, [args?.shell_id, formatTerminalDetails(args)]);
|
|
12
|
+
if (name === "external_web_search") return joinToolParts("◆", name, [quoteCompact(args?.query ?? "")]);
|
|
13
|
+
if (name === "web_fetch") return joinToolParts("◆", name, [compactText(args?.url ?? "")]);
|
|
14
|
+
if (name === "context_stats") return joinToolParts("◆", name, []);
|
|
15
|
+
if (name === "read") {
|
|
16
|
+
const path = compactPath(args?.path ?? args?.filePath ?? "");
|
|
17
|
+
return joinToolParts("→", name, [path, formatReadRange(args)]);
|
|
18
|
+
}
|
|
19
|
+
if (name === "grep") {
|
|
20
|
+
const path = compactPath(args?.path ?? "");
|
|
21
|
+
return joinToolParts("✱", name, [quoteCompact(args?.pattern ?? ""), path]);
|
|
22
|
+
}
|
|
23
|
+
if (name === "glob") {
|
|
24
|
+
const path = compactPath(args?.path ?? "");
|
|
25
|
+
return joinToolParts("✱", name, [quoteCompact(args?.pattern ?? ""), path]);
|
|
26
|
+
}
|
|
27
|
+
if (name === "find") {
|
|
28
|
+
const path = compactPath(args?.path ?? "");
|
|
29
|
+
return joinToolParts("✱", name, [quoteCompact(args?.pattern ?? ""), path]);
|
|
30
|
+
}
|
|
31
|
+
return joinToolParts("◆", name, [formatSmallOptions(args)]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatToolSuccessSummary(name, result, out = "") {
|
|
35
|
+
if (name === "grep") {
|
|
36
|
+
const matches = result?.details?.results?.length ?? countMatchLines(out);
|
|
37
|
+
return `${matches} match${matches === 1 ? "" : "es"}`;
|
|
38
|
+
}
|
|
39
|
+
if (name === "glob") {
|
|
40
|
+
const matches = Array.isArray(result?.details?.matches) ? result.details.matches.length : countNonEmptyLines(out);
|
|
41
|
+
return `${matches} file${matches === 1 ? "" : "s"}`;
|
|
42
|
+
}
|
|
43
|
+
if (name === "find") {
|
|
44
|
+
const matches = result?.details?.count ?? countNonEmptyLines(out);
|
|
45
|
+
return `${matches} file${matches === 1 ? "" : "s"}`;
|
|
46
|
+
}
|
|
47
|
+
if (name === "memory_open") {
|
|
48
|
+
return compactText(result?.details?.entry?.name ?? compactPath(result?.details?.path ?? ""));
|
|
49
|
+
}
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function joinToolParts(icon, name, parts) {
|
|
54
|
+
const clean = parts.map((part) => String(part ?? "").trim()).filter(Boolean);
|
|
55
|
+
return `${icon} ${name}${clean.length ? ` · ${clean.join(" · ")}` : ""}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatReadRange(args = {}) {
|
|
59
|
+
if (args.offset == null && args.limit == null) return "";
|
|
60
|
+
if (args.offset != null && args.limit != null) return `lines ${args.offset}-${Number(args.offset) + Number(args.limit) - 1}`;
|
|
61
|
+
if (args.offset != null) return `from line ${args.offset}`;
|
|
62
|
+
return `limit ${args.limit}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatTerminalSendAction(args = {}) {
|
|
66
|
+
const hasText = typeof args.text === "string" && args.text.length > 0;
|
|
67
|
+
const key = args.key ? String(args.key) : "";
|
|
68
|
+
if (hasText && key) return `text+${key}`;
|
|
69
|
+
if (hasText) return args.text.includes("\n") || args.text.includes("\r") ? "text+enter" : "text";
|
|
70
|
+
return key || "send";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatTerminalDetails(args = {}) {
|
|
74
|
+
const details = [];
|
|
75
|
+
if (args.pattern) details.push(quoteCompact(args.pattern));
|
|
76
|
+
if (args.cols && args.rows) details.push(`${args.cols}x${args.rows}`);
|
|
77
|
+
if (args.command) details.push(compactText(args.command));
|
|
78
|
+
return details.join(" · ");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatSmallOptions(args = {}) {
|
|
82
|
+
const parts = [];
|
|
83
|
+
for (const [key, value] of Object.entries(args ?? {})) {
|
|
84
|
+
if (value == null || typeof value === "object") continue;
|
|
85
|
+
parts.push(`${key}=${compactText(value)}`);
|
|
86
|
+
if (parts.length >= 2) break;
|
|
87
|
+
}
|
|
88
|
+
return parts.join(", ");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function compactPath(path) {
|
|
92
|
+
return String(path ?? "").split(/[/\\]/).filter(Boolean).slice(-4).join("\\");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function quoteCompact(value) {
|
|
96
|
+
return JSON.stringify(compactText(value));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function compactText(value, limit = 80) {
|
|
100
|
+
const text = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
101
|
+
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function countMatchLines(text) {
|
|
105
|
+
const match = String(text ?? "").match(/(\d+)\s+matches?\b/i);
|
|
106
|
+
if (match) return Number(match[1]);
|
|
107
|
+
return countNonEmptyLines(text);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function countNonEmptyLines(text) {
|
|
111
|
+
return String(text ?? "").split("\n").filter(Boolean).length;
|
|
112
|
+
}
|
package/src/agent/tools.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import { Type } from "typebox";
|
|
|
3
3
|
import { createCommandExecTool } from "./command-exec-tool.mjs";
|
|
4
4
|
import { createContextStatsTool } from "./context-stats-tool.mjs";
|
|
5
5
|
import { createEditFileTool } from "./file-edit-tool.mjs";
|
|
6
|
-
import { createFindTool } from "./find-tool.mjs";
|
|
7
6
|
import { createReadFileTool } from "./read-file-tool.mjs";
|
|
8
7
|
import { toolText } from "./tool-result.mjs";
|
|
9
8
|
import { createShellTools } from "../shell/tools.mjs";
|
|
@@ -14,7 +13,6 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
|
|
|
14
13
|
const commandExecTool = createCommandExecTool({ cwd });
|
|
15
14
|
const contextStatsTool = createContextStatsTool({ engine });
|
|
16
15
|
const editFileTool = createEditFileTool({ engine, ui, lspService });
|
|
17
|
-
const findTool = createFindTool({ cwd });
|
|
18
16
|
const readFileTool = createReadFileTool({ engine });
|
|
19
17
|
|
|
20
18
|
const tools = [
|
|
@@ -22,7 +20,6 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
|
|
|
22
20
|
contextStatsTool,
|
|
23
21
|
commandExecTool,
|
|
24
22
|
editFileTool,
|
|
25
|
-
findTool,
|
|
26
23
|
...createShellTools(shellRuntime),
|
|
27
24
|
...memoryTools,
|
|
28
25
|
...mcpTools,
|
|
@@ -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
|
});
|
|
@@ -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
|
+
}
|