march-cli 0.1.36 → 0.1.37
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/code-search/tool.mjs +1 -1
- package/src/agent/runner.mjs +9 -10
- package/src/agent/runtime/runner-process-client.mjs +5 -0
- package/src/agent/runtime/runner-process-factory.mjs +5 -0
- package/src/agent/runtime/runner-runtime-host.mjs +2 -0
- package/src/agent/session/session-options.mjs +2 -1
- package/src/agent/tools.mjs +4 -1
- package/src/agent/turn/turn-events.mjs +41 -0
- package/src/agent/turn/turn-runner.mjs +5 -2
- package/src/cli/input/history-store.mjs +65 -3
- package/src/cli/repl-loop.mjs +8 -6
- package/src/cli/startup/app-runtime.mjs +5 -29
- package/src/cli/startup/create-runtime-runner.mjs +4 -46
- package/src/cli/tui/input/history-navigation-controller.mjs +56 -0
- package/src/cli/turn/turn-input-preparer.mjs +0 -1
- package/src/cli/ui.mjs +9 -0
- package/src/context/engine.mjs +4 -2
- package/src/context/system-core/base.md +4 -1
- package/src/history/runner.mjs +11 -0
- package/src/history/store.mjs +129 -0
- package/src/history/tool.mjs +39 -0
- package/src/lsp/client.mjs +12 -5
- package/src/lsp/service.mjs +15 -3
- package/src/main.mjs +1 -2
- package/src/web-ui/command.mjs +2 -2
- package/src/web-ui/runtime-host.mjs +4 -23
- package/src/web-ui/session-manager.mjs +2 -2
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ export function createCodeSearchTool({ engine, stateRoot = null }) {
|
|
|
13
13
|
return defineTool({
|
|
14
14
|
name: "code_search",
|
|
15
15
|
label: "Code Search",
|
|
16
|
-
description: "Native code
|
|
16
|
+
description: "Native semantic/symbol code search over the workspace. Use it first for unknown entry points, cross-module flows, responsibility boundaries, and related implementations. Use grep/read afterward for exact confirmation before editing or making claims.",
|
|
17
17
|
parameters: Type.Object({
|
|
18
18
|
query: Type.Optional(Type.String({ description: "Natural-language or symbol query" })),
|
|
19
19
|
path: Type.Optional(Type.String({ description: "Relative or absolute workspace path to search; default current workspace" })),
|
package/src/agent/runner.mjs
CHANGED
|
@@ -28,19 +28,16 @@ import { registerCustomProviders } from "../provider/custom-provider.mjs";
|
|
|
28
28
|
import { injectHostedTools } from "../provider/hosted-tools.mjs";
|
|
29
29
|
import { createRunnerLifecycle } from "./lifecycle/runner-lifecycle.mjs";
|
|
30
30
|
import { createRunnerProviderQuotaRuntime } from "./runner/provider-quota-runtime.mjs";
|
|
31
|
+
import { appendRunnerTurnHistory, createRunnerHistoryStore } from "../history/runner.mjs";
|
|
31
32
|
export { MARCH_BASE_TOOL_NAMES, installModelPayloadDumper };
|
|
32
33
|
export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
|
|
33
34
|
export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
34
|
-
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, profilePaths = null, memoryStore = null, memoryTools = [], remoteMemorySources = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, permissionController = null, modelContextDumper = null, turnNotifier = null, logger = null, onModelPayload = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null, hostedTools = {} }) {
|
|
35
|
+
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, profilePaths = null, memoryStore = null, memoryTools = [], remoteMemorySources = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, permissionController = null, modelContextDumper = null, turnNotifier = null, logger = null, onModelPayload = null, onLspStatusChange = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null, hostedTools = {} }) {
|
|
35
36
|
installCodexLargeContextGuard();
|
|
36
37
|
installCodexTransportCompression();
|
|
37
38
|
installCodexWebSocketEventDebug();
|
|
38
|
-
if (!useRuntimeHost && extensionPaths.length > 0)
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
const authConfig = authStorage
|
|
42
|
-
? { authStorage, hasAuth: true }
|
|
43
|
-
: createMarchAuthStorage({ provider: provider ?? "deepseek", providers, cwd });
|
|
39
|
+
if (!useRuntimeHost && extensionPaths.length > 0) throw new Error("--extension requires the default pi runtime host path");
|
|
40
|
+
const authConfig = authStorage ? { authStorage, hasAuth: true } : createMarchAuthStorage({ provider: provider ?? "deepseek", providers, cwd });
|
|
44
41
|
if (!authConfig.hasAuth) throw new Error("No providers configured. Run: march provider --config");
|
|
45
42
|
const resolvedAuth = authConfig.authStorage;
|
|
46
43
|
const modelRegistry = ModelRegistry.create(resolvedAuth);
|
|
@@ -55,8 +52,9 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
55
52
|
retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
|
|
56
53
|
});
|
|
57
54
|
const { ui: runtimeUi, eventBus: runtimeUiEvents, detach: detachRuntimeUi } = createRuntimeUiBridge(ui);
|
|
58
|
-
const lspService = new LspService({ cwd, onEvent: (event) => runtimeUi.status?.(formatLspServiceEvent(event)) });
|
|
55
|
+
const lspService = new LspService({ cwd, onEvent: (event) => runtimeUi.status?.(formatLspServiceEvent(event)), onStatusChange: (event) => onLspStatusChange?.(event) });
|
|
59
56
|
const engine = new ContextEngine({ cwd, modelId, provider, namespace, memoryRoot, profilePaths, remoteMemorySources, shellRuntime, lspService, injections: mcpInjections, maxTurns, trimBatch });
|
|
57
|
+
const historyStore = createRunnerHistoryStore({ stateRoot, cwd });
|
|
60
58
|
const resolvedSessionManager = resolveRunnerSessionManager(cwd, sessionManager);
|
|
61
59
|
const sessionBinding = createSessionBinding(null);
|
|
62
60
|
let currentModelCallKind = "model", currentTurnId = null, currentPromptForContext = "";
|
|
@@ -72,7 +70,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
72
70
|
providers,
|
|
73
71
|
sessionManager: resolvedSessionManager, sessionBinding, engine, ui: runtimeUi,
|
|
74
72
|
projectMarchDir,
|
|
75
|
-
memoryTools, memoryStore, shellRuntime, lspService, mcpTools, webTools,
|
|
73
|
+
memoryTools, memoryStore, historyStore, shellRuntime, lspService, mcpTools, webTools,
|
|
76
74
|
lifecycle, permissionController, extensionPaths, hostedTools,
|
|
77
75
|
onRebind: (session) => {
|
|
78
76
|
installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, injectMarchSystemContext);
|
|
@@ -85,7 +83,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
85
83
|
} else {
|
|
86
84
|
const sessionOptions = resolveRunnerSessionOptions({
|
|
87
85
|
cwd, stateRoot, provider, modelId, modelRegistry, engine, ui: runtimeUi,
|
|
88
|
-
memoryTools, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController,
|
|
86
|
+
memoryTools, historyStore, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController,
|
|
89
87
|
authStorage: resolvedAuth, projectMarchDir,
|
|
90
88
|
getCurrentModel: () => sessionBinding.get()?.model ?? selectedModel,
|
|
91
89
|
});
|
|
@@ -134,6 +132,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
134
132
|
syncCurrentPiSidecar,
|
|
135
133
|
autoNameSession,
|
|
136
134
|
contextMode,
|
|
135
|
+
recordHistory: (turn) => appendRunnerTurnHistory({ store: historyStore, turn, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost), modelId: engine.modelId, provider: engine.provider }),
|
|
137
136
|
});
|
|
138
137
|
notifyTurnEndDetached(turnNotifier, {
|
|
139
138
|
status: "success",
|
|
@@ -10,6 +10,7 @@ export async function createRunnerProcessClient({
|
|
|
10
10
|
runnerOptions,
|
|
11
11
|
ui,
|
|
12
12
|
onModelPayload = null,
|
|
13
|
+
onLspStatusChange = null,
|
|
13
14
|
entry = fileURLToPath(DEFAULT_ENTRY),
|
|
14
15
|
forkImpl = fork,
|
|
15
16
|
timeoutMs = 0,
|
|
@@ -46,6 +47,10 @@ export async function createRunnerProcessClient({
|
|
|
46
47
|
target: {
|
|
47
48
|
...createRuntimeUiEventTarget(ui),
|
|
48
49
|
modelPayload: (event) => onModelPayload?.(event),
|
|
50
|
+
lspStatusChange: async (event) => {
|
|
51
|
+
await active?.runner?.refreshState?.();
|
|
52
|
+
onLspStatusChange?.(event);
|
|
53
|
+
},
|
|
49
54
|
},
|
|
50
55
|
timeoutMs,
|
|
51
56
|
});
|
|
@@ -93,6 +93,7 @@ export async function createIsolatedRunner(options = {}, deps = {}) {
|
|
|
93
93
|
}),
|
|
94
94
|
logger,
|
|
95
95
|
onModelPayload: (event) => d.peer.notify("modelPayload", pickModelPayloadEvent(event)),
|
|
96
|
+
onLspStatusChange: (event) => d.peer.notify("lspStatusChange", pickLspStatusEvent(event)),
|
|
96
97
|
});
|
|
97
98
|
|
|
98
99
|
const originalDispose = runner.dispose;
|
|
@@ -109,3 +110,7 @@ export async function createIsolatedRunner(options = {}, deps = {}) {
|
|
|
109
110
|
function pickModelPayloadEvent({ estimatedTokens, provider, model, kind, turnId } = {}) {
|
|
110
111
|
return { estimatedTokens, provider, model, kind, turnId };
|
|
111
112
|
}
|
|
113
|
+
|
|
114
|
+
function pickLspStatusEvent({ id, root, status, reason, managed } = {}) {
|
|
115
|
+
return { id, root, status, reason, managed };
|
|
116
|
+
}
|
|
@@ -20,6 +20,7 @@ export async function createRunnerRuntimeHost({
|
|
|
20
20
|
ui,
|
|
21
21
|
projectMarchDir = null,
|
|
22
22
|
memoryTools = [],
|
|
23
|
+
historyStore = null,
|
|
23
24
|
shellRuntime = null,
|
|
24
25
|
lspService = null,
|
|
25
26
|
mcpTools = [],
|
|
@@ -56,6 +57,7 @@ export async function createRunnerRuntimeHost({
|
|
|
56
57
|
engine,
|
|
57
58
|
ui,
|
|
58
59
|
memoryTools,
|
|
60
|
+
historyStore,
|
|
59
61
|
shellRuntime,
|
|
60
62
|
lspService,
|
|
61
63
|
mcpTools,
|
|
@@ -10,6 +10,7 @@ export function resolveRunnerSessionOptions({
|
|
|
10
10
|
engine,
|
|
11
11
|
ui,
|
|
12
12
|
memoryTools = [],
|
|
13
|
+
historyStore = null,
|
|
13
14
|
shellRuntime = null,
|
|
14
15
|
lspService = null,
|
|
15
16
|
mcpTools = [],
|
|
@@ -31,7 +32,7 @@ export function resolveRunnerSessionOptions({
|
|
|
31
32
|
?? (provider && modelId ? getModel(provider, modelId) : null);
|
|
32
33
|
if (!model) throw new Error(`Model not found: ${provider}/${modelId}`);
|
|
33
34
|
|
|
34
|
-
const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController, authStorage, projectMarchDir, stateRoot, getCurrentModel: () => getCurrentModel?.() ?? model });
|
|
35
|
+
const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, historyStore, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController, authStorage, projectMarchDir, stateRoot, getCurrentModel: () => getCurrentModel?.() ?? model });
|
|
35
36
|
const customToolNames = customTools.map((tool) => tool.name);
|
|
36
37
|
const tools = [
|
|
37
38
|
...customToolNames.filter((name) => name === "read"),
|
package/src/agent/tools.mjs
CHANGED
|
@@ -13,11 +13,13 @@ import { initImageGen } from "../image-gen/index.mjs";
|
|
|
13
13
|
import { createSuperGrokTool } from "../supergrok/tool.mjs";
|
|
14
14
|
import { createBrowserTools } from "../browser/tools/index.mjs";
|
|
15
15
|
import { createRuntimeRestartTool } from "./lifecycle/runtime-restart-tool.mjs";
|
|
16
|
+
import { createHistorySearchTool } from "../history/tool.mjs";
|
|
16
17
|
|
|
17
|
-
export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shellRuntime = null, lspService = null, mcpTools = [], webTools = [], lifecycle = null, permissionController = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
|
|
18
|
+
export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], historyStore = null, shellRuntime = null, lspService = null, mcpTools = [], webTools = [], lifecycle = null, permissionController = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
|
|
18
19
|
const commandExecTool = createCommandExecTool({ cwd });
|
|
19
20
|
const codeSearchTool = createCodeSearchTool({ engine, stateRoot });
|
|
20
21
|
const contextStatsTool = createContextStatsTool({ engine });
|
|
22
|
+
const historySearchTool = createHistorySearchTool({ store: historyStore });
|
|
21
23
|
const editFileTool = createEditFileTool({ engine, ui, lspService });
|
|
22
24
|
const readFileTool = createReadFileTool({ engine });
|
|
23
25
|
const readImageTool = createReadImageTool({ engine, getCurrentModel });
|
|
@@ -36,6 +38,7 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
|
|
|
36
38
|
commandExecTool,
|
|
37
39
|
editFileTool,
|
|
38
40
|
...createShellTools(shellRuntime),
|
|
41
|
+
...(historySearchTool ? [historySearchTool] : []),
|
|
39
42
|
...memoryTools,
|
|
40
43
|
...mcpTools,
|
|
41
44
|
...webTools,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { formatToolStartLine, formatToolSuccessSummary } from "../tool-summary.mjs";
|
|
2
2
|
|
|
3
|
+
const TOOL_ERROR_EXCERPT_LIMIT = 4000;
|
|
4
|
+
|
|
3
5
|
export function createTurnEventState() {
|
|
4
6
|
return {
|
|
5
7
|
draft: "",
|
|
@@ -9,6 +11,7 @@ export function createTurnEventState() {
|
|
|
9
11
|
assistantReplyOpen: false,
|
|
10
12
|
assistantContextParts: [],
|
|
11
13
|
activeToolContextPart: null,
|
|
14
|
+
toolCalls: [],
|
|
12
15
|
};
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -19,10 +22,12 @@ export function handleRunnerSessionEvent(event, { ui, engine, state }) {
|
|
|
19
22
|
if (event.type === "tool_execution_start") {
|
|
20
23
|
closeAssistantReply({ ui, state });
|
|
21
24
|
appendToolStartContext(state, event.toolName, event.args);
|
|
25
|
+
recordToolStart(state, event.toolName, event.args);
|
|
22
26
|
ui.toolStart(event.toolName, event.args);
|
|
23
27
|
}
|
|
24
28
|
if (event.type === "tool_execution_end") {
|
|
25
29
|
updateToolEndContext(state, event.toolName, event.isError, event.result);
|
|
30
|
+
recordToolEnd(state, event.toolName, event.isError, event.result);
|
|
26
31
|
ui.toolEnd(event.toolName, event.isError, event.result);
|
|
27
32
|
}
|
|
28
33
|
if (event.type === "auto_retry_start") {
|
|
@@ -109,3 +114,39 @@ function updateToolEndContext(state, name, isError, result) {
|
|
|
109
114
|
if (summary && summary !== "done") part.text = `${part.text.trimEnd()} (${summary})\n`;
|
|
110
115
|
state.activeToolContextPart = null;
|
|
111
116
|
}
|
|
117
|
+
|
|
118
|
+
function recordToolStart(state, name, args) {
|
|
119
|
+
state.toolCalls.push({ name, args: cloneJson(args), status: "running" });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function recordToolEnd(state, name, isError, result) {
|
|
123
|
+
const call = [...state.toolCalls].reverse().find((item) => item.name === name && item.status === "running");
|
|
124
|
+
if (!call) return;
|
|
125
|
+
call.status = isError ? "failed" : "success";
|
|
126
|
+
if (!isError) return;
|
|
127
|
+
const output = extractToolOutput(result);
|
|
128
|
+
call.error = {
|
|
129
|
+
message: output.split(/\r?\n/).find(Boolean) ?? "Tool call failed",
|
|
130
|
+
details: cloneJson(result?.details ?? null),
|
|
131
|
+
excerpt: truncate(output, TOOL_ERROR_EXCERPT_LIMIT),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function extractToolOutput(result) {
|
|
136
|
+
const content = result?.content;
|
|
137
|
+
if (!Array.isArray(content)) return typeof result === "string" ? result : "";
|
|
138
|
+
return content.filter((item) => item?.type === "text").map((item) => item.text ?? "").join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function cloneJson(value) {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(JSON.stringify(value ?? null));
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function truncate(text, limit) {
|
|
150
|
+
const value = String(text ?? "");
|
|
151
|
+
return value.length > limit ? `${value.slice(0, limit)}\n[truncated]` : value;
|
|
152
|
+
}
|
|
@@ -17,6 +17,7 @@ export async function runRunnerTurn({
|
|
|
17
17
|
syncCurrentPiSidecar,
|
|
18
18
|
autoNameSession,
|
|
19
19
|
contextMode = "rebuild",
|
|
20
|
+
recordHistory = null,
|
|
20
21
|
}) {
|
|
21
22
|
const {
|
|
22
23
|
userRecallHints = [],
|
|
@@ -80,6 +81,7 @@ export async function runRunnerTurn({
|
|
|
80
81
|
midTurnRecallHints,
|
|
81
82
|
syncCurrentPiSidecar,
|
|
82
83
|
autoNameSession,
|
|
84
|
+
recordHistory,
|
|
83
85
|
});
|
|
84
86
|
return { draft: turnState.draft };
|
|
85
87
|
} finally {
|
|
@@ -127,19 +129,20 @@ function logSessionEvent(logger, event) {
|
|
|
127
129
|
});
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentPiSidecar, autoNameSession }) {
|
|
132
|
+
function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentPiSidecar, autoNameSession, recordHistory }) {
|
|
131
133
|
closeAssistantReply({ ui, state: turnState });
|
|
132
134
|
const assistantRecallHints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
|
|
133
135
|
engine.setPendingAssistantRecallHints?.(assistantRecallHints);
|
|
134
136
|
const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...assistantRecallHints]);
|
|
135
137
|
|
|
136
|
-
engine.recordTurn({
|
|
138
|
+
const turn = engine.recordTurn({
|
|
137
139
|
userMessage: userMessage ?? prompt.slice(0, 300),
|
|
138
140
|
assistantMessage: turnState.draft,
|
|
139
141
|
assistantContext: compactAssistantContext(turnState),
|
|
140
142
|
userRecallHints,
|
|
141
143
|
assistantRecallHints: recordedAssistantRecallHints,
|
|
142
144
|
});
|
|
145
|
+
recordHistory?.({ ...turn, thinking: assistantThinkingText(turnState), toolCalls: turnState.toolCalls });
|
|
143
146
|
|
|
144
147
|
autoNameSession?.();
|
|
145
148
|
syncCurrentPiSidecar();
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
|
|
4
4
|
const HISTORY_VERSION = 1;
|
|
5
5
|
const MAX_HISTORY_ITEMS = 100;
|
|
6
|
+
const LOCK_WAIT_MS = 2000;
|
|
7
|
+
const LOCK_STALE_MS = 5000;
|
|
6
8
|
|
|
7
9
|
export function createInputHistoryStore({ path, maxItems = MAX_HISTORY_ITEMS } = {}) {
|
|
8
10
|
return {
|
|
@@ -20,8 +22,10 @@ export function createInputHistoryStore({ path, maxItems = MAX_HISTORY_ITEMS } =
|
|
|
20
22
|
save(items) {
|
|
21
23
|
if (!path) return;
|
|
22
24
|
const normalized = normalizeItems(items, maxItems);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
withHistoryLock(path, () => {
|
|
26
|
+
const merged = mergeItems(normalized, this.load(), maxItems);
|
|
27
|
+
writeHistoryFile(path, merged);
|
|
28
|
+
});
|
|
25
29
|
},
|
|
26
30
|
};
|
|
27
31
|
}
|
|
@@ -33,3 +37,61 @@ function normalizeItems(items, maxItems) {
|
|
|
33
37
|
.filter(Boolean)
|
|
34
38
|
.slice(0, maxItems);
|
|
35
39
|
}
|
|
40
|
+
|
|
41
|
+
function mergeItems(primaryItems, existingItems, maxItems) {
|
|
42
|
+
const merged = [];
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
for (const item of [...primaryItems, ...existingItems]) {
|
|
45
|
+
if (seen.has(item)) continue;
|
|
46
|
+
seen.add(item);
|
|
47
|
+
merged.push(item);
|
|
48
|
+
if (merged.length >= maxItems) break;
|
|
49
|
+
}
|
|
50
|
+
return merged;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeHistoryFile(path, items) {
|
|
54
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
55
|
+
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
56
|
+
const payload = `${JSON.stringify({ version: HISTORY_VERSION, items }, null, 2)}\n`;
|
|
57
|
+
writeFileSync(tmpPath, payload, "utf8");
|
|
58
|
+
renameSync(tmpPath, path);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function withHistoryLock(path, fn) {
|
|
62
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
63
|
+
const lockPath = `${path}.lock`;
|
|
64
|
+
const fd = acquireLock(lockPath);
|
|
65
|
+
try {
|
|
66
|
+
return fn();
|
|
67
|
+
} finally {
|
|
68
|
+
closeSync(fd);
|
|
69
|
+
try { unlinkSync(lockPath); } catch {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function acquireLock(lockPath) {
|
|
74
|
+
const start = Date.now();
|
|
75
|
+
while (true) {
|
|
76
|
+
try {
|
|
77
|
+
const fd = openSync(lockPath, "wx");
|
|
78
|
+
writeFileSync(fd, `${process.pid}\n`, "utf8");
|
|
79
|
+
return fd;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err?.code !== "EEXIST") throw err;
|
|
82
|
+
removeStaleLock(lockPath);
|
|
83
|
+
if (Date.now() - start >= LOCK_WAIT_MS) throw err;
|
|
84
|
+
sleepSync(25);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function removeStaleLock(lockPath) {
|
|
90
|
+
try {
|
|
91
|
+
if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) unlinkSync(lockPath);
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sleepSync(ms) {
|
|
96
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
97
|
+
}
|
package/src/cli/repl-loop.mjs
CHANGED
|
@@ -14,12 +14,13 @@ export async function runSingleShotPrompt({
|
|
|
14
14
|
refreshStatusBar,
|
|
15
15
|
modeState = null,
|
|
16
16
|
}) {
|
|
17
|
-
|
|
18
|
-
ui.writeln(turnInput.displayMessage);
|
|
19
|
-
ui.recall?.({ source: "user", hints: turnInput.userRecallHints });
|
|
20
|
-
if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ source: "assistant", hints: turnInput.carryoverRecallHints });
|
|
21
|
-
refreshStatusBar.startWorking?.();
|
|
17
|
+
memoryStore.beginTurn();
|
|
22
18
|
try {
|
|
19
|
+
const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
|
|
20
|
+
ui.writeln(turnInput.displayMessage);
|
|
21
|
+
ui.recall?.({ source: "user", hints: turnInput.userRecallHints });
|
|
22
|
+
if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ source: "assistant", hints: turnInput.carryoverRecallHints });
|
|
23
|
+
refreshStatusBar.startWorking?.();
|
|
23
24
|
const result = await runner.runTurn(turnInput.fullPrompt, turnInput.userMessage, turnInput.runOptions);
|
|
24
25
|
renderPendingAssistantRecallPreview({ runner, ui });
|
|
25
26
|
await handleTurnLifecycleAction(result?.lifecycleAction, { runner, ui });
|
|
@@ -127,8 +128,9 @@ function handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand }) {
|
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
async function runReplTurn({ prompt, runner, memoryStore, currentProject, ui, refreshStatusBar, setTurnRunning, modeState = null }) {
|
|
130
|
-
|
|
131
|
+
memoryStore.beginTurn();
|
|
131
132
|
try {
|
|
133
|
+
const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
|
|
132
134
|
ui.writeln(turnInput.displayMessage);
|
|
133
135
|
ui.recall?.({ source: "user", hints: turnInput.userRecallHints });
|
|
134
136
|
if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ source: "assistant", hints: turnInput.carryoverRecallHints });
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { basename, join, resolve } from "node:path";
|
|
2
2
|
import { existsSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { createUI } from "../ui.mjs";
|
|
4
|
-
import { createPermissionController, MODE } from "../permissions.mjs";
|
|
5
4
|
import { loadKeybindings } from "../input/keybindings.mjs";
|
|
6
5
|
import { createInputHistoryStore } from "../input/history-store.mjs";
|
|
7
6
|
import { createModeState } from "../input/mode-state.mjs";
|
|
@@ -12,21 +11,16 @@ import { createMarchAuthStorage } from "../../auth/storage.mjs";
|
|
|
12
11
|
import { createRuntimeRunner } from "./create-runtime-runner.mjs";
|
|
13
12
|
import { createCliShellRuntime } from "../../shell/cli-runtime.mjs";
|
|
14
13
|
import { MarkdownMemoryStore } from "../../memory/markdown-store.mjs";
|
|
15
|
-
import { createMarkdownMemoryTools } from "../../memory/markdown-tools.mjs";
|
|
16
14
|
import { discoverProjectExtensionPaths } from "../../extensions/discovery.mjs";
|
|
17
15
|
import { loadProjectLifecycleHookManifests } from "../../extensions/lifecycle-manifest.mjs";
|
|
18
16
|
import { loadOrCreateProjectId, resumeStartupSession } from "./startup-session.mjs";
|
|
19
|
-
import { initializeMcp } from "../../mcp/index.mjs";
|
|
20
|
-
import { createWebToolsFromConfig } from "../../web/tools.mjs";
|
|
21
|
-
import { createModelContextDumper } from "../../debug/model-context-dumper.mjs";
|
|
22
17
|
import { createLogger, installProcessLogHandlers } from "../../debug/logger.mjs";
|
|
23
18
|
import { defaultProfilePaths, ensureProfileFiles } from "../../context/profiles.mjs";
|
|
24
|
-
import { createDesktopTurnNotifier } from "../../notification/desktop-notifier.mjs";
|
|
25
19
|
import { normalizeRemoteMemorySources } from "../../memory/remote/config.mjs";
|
|
26
20
|
import { resolveMemoryRoot } from "../../memory/root.mjs";
|
|
27
21
|
import { ensureBrowserDaemon } from "../../browser/client/lifecycle.mjs";
|
|
28
22
|
|
|
29
|
-
export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot
|
|
23
|
+
export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot } = {}) {
|
|
30
24
|
if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
|
|
31
25
|
await ensureBrowserDaemon({ stateRoot }).catch(() => {});
|
|
32
26
|
|
|
@@ -67,23 +61,10 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
|
|
|
67
61
|
ensureProfileFiles(profilePaths);
|
|
68
62
|
const memoryStore = new MarkdownMemoryStore({ root: memoryRoot });
|
|
69
63
|
const remoteMemorySources = normalizeRemoteMemorySources(config);
|
|
70
|
-
const memoryTools = createMarkdownMemoryTools(memoryStore, { remoteSources: remoteMemorySources });
|
|
71
64
|
const currentProject = basename(cwd);
|
|
72
65
|
const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
|
|
73
66
|
|
|
74
|
-
const
|
|
75
|
-
? { clientManager: null, mcpTools: [], mcpInjections: [], errors: [] }
|
|
76
|
-
: await initializeMcp({ projectDir: cwd });
|
|
77
|
-
for (const { server, error } of mcpInit.errors) {
|
|
78
|
-
if (!args.json) process.stderr.write(`[mcp] ${server}: ${error}\n`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const webTools = createWebToolsFromConfig(config);
|
|
82
|
-
const turnNotifier = createDesktopTurnNotifier({ enabled: Boolean(config.notifications?.turnEnd), config: config.notifications });
|
|
83
|
-
const permissionMode = args.permissionMode ?? MODE.BYPASS;
|
|
84
|
-
const permissionController = createPermissionController({ mode: permissionMode });
|
|
85
|
-
const usePiSessions = true;
|
|
86
|
-
const usePiRuntimeHost = true;
|
|
67
|
+
const permissionMode = args.permissionMode;
|
|
87
68
|
const sessionSource = "pi";
|
|
88
69
|
const sessionsRoot = join(projectMarchDir, "sessions");
|
|
89
70
|
const sessionState = {
|
|
@@ -92,10 +73,6 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
|
|
|
92
73
|
};
|
|
93
74
|
sessionState.sessionDir = join(sessionsRoot, sessionState.sessionId);
|
|
94
75
|
const contextDumpRoot = resolve(projectMarchDir, "context-dumps", sessionState.sessionId);
|
|
95
|
-
const modelContextDumper = createModelContextDumper({
|
|
96
|
-
enabled: args.dumpContext,
|
|
97
|
-
rootDir: contextDumpRoot,
|
|
98
|
-
});
|
|
99
76
|
|
|
100
77
|
const ui = createUI({
|
|
101
78
|
json: args.json,
|
|
@@ -132,10 +109,9 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
|
|
|
132
109
|
let runner;
|
|
133
110
|
try {
|
|
134
111
|
runner = await createRuntimeRunner({
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
permissionController, modelContextDumper, turnNotifier, logger,
|
|
112
|
+
runnerOptions,
|
|
113
|
+
ui,
|
|
114
|
+
shellRuntime,
|
|
139
115
|
refreshStatusBar: (...args) => refreshStatusBar?.(...args),
|
|
140
116
|
});
|
|
141
117
|
} catch (err) {
|
|
@@ -1,61 +1,19 @@
|
|
|
1
|
-
import { createRunner } from "../../agent/runner.mjs";
|
|
2
1
|
import { createRunnerProcessClient } from "../../agent/runtime/runner-process-client.mjs";
|
|
3
|
-
import { resolvePiSessionManager } from "../../session/pi-manager.mjs";
|
|
4
2
|
|
|
5
3
|
export async function createRuntimeRunner({
|
|
6
|
-
useRuntimeProcess = false,
|
|
7
4
|
runnerOptions,
|
|
8
5
|
ui,
|
|
9
|
-
memoryStore,
|
|
10
|
-
memoryTools,
|
|
11
6
|
shellRuntime,
|
|
12
|
-
mcpTools,
|
|
13
|
-
mcpInjections,
|
|
14
|
-
mcpClientManager,
|
|
15
|
-
webTools,
|
|
16
|
-
usePiSessions,
|
|
17
|
-
usePiRuntimeHost,
|
|
18
|
-
authStorage,
|
|
19
|
-
permissionController,
|
|
20
|
-
modelContextDumper,
|
|
21
|
-
turnNotifier,
|
|
22
|
-
logger,
|
|
23
7
|
refreshStatusBar,
|
|
24
8
|
} = {}) {
|
|
25
9
|
const onModelPayload = ({ estimatedTokens }) => {
|
|
26
10
|
refreshStatusBar?.({ contextTokens: estimatedTokens });
|
|
27
11
|
};
|
|
12
|
+
const onLspStatusChange = () => {
|
|
13
|
+
refreshStatusBar?.();
|
|
14
|
+
};
|
|
28
15
|
|
|
29
|
-
const runner =
|
|
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
|
-
|
|
16
|
+
const { runner } = await createRunnerProcessClient({ runnerOptions, ui, onModelPayload, onLspStatusChange });
|
|
59
17
|
runner.shellRuntime ??= shellRuntime;
|
|
60
18
|
return runner;
|
|
61
19
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export function createHistoryNavigationController({ editor, requestRender, isAutocompleteOpen = () => false, hasOverlay = () => false } = {}) {
|
|
4
|
+
let draftText = null;
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
handleInput(data) {
|
|
8
|
+
if (isAutocompleteOpen() || hasOverlay()) return undefined;
|
|
9
|
+
|
|
10
|
+
if (matchesKey(data, "alt+up")) return moveWithinInput(-1);
|
|
11
|
+
if (matchesKey(data, "alt+down")) return moveWithinInput(1);
|
|
12
|
+
if (matchesKey(data, "up")) return navigateHistory(-1);
|
|
13
|
+
if (matchesKey(data, "down")) return navigateHistory(1);
|
|
14
|
+
|
|
15
|
+
return undefined;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function navigateHistory(direction) {
|
|
20
|
+
const history = Array.isArray(editor?.history) ? editor.history : [];
|
|
21
|
+
const currentIndex = Number.isInteger(editor?.historyIndex) ? editor.historyIndex : -1;
|
|
22
|
+
const nextIndex = currentIndex - direction;
|
|
23
|
+
if (nextIndex < -1 || nextIndex >= history.length) return undefined;
|
|
24
|
+
|
|
25
|
+
if (currentIndex === -1) draftText = editor.getText?.() ?? "";
|
|
26
|
+
editor.lastAction = null;
|
|
27
|
+
editor.historyIndex = nextIndex;
|
|
28
|
+
setEditorTextPreservingHistory(nextIndex === -1 ? draftText ?? "" : history[nextIndex] ?? "");
|
|
29
|
+
if (nextIndex === -1) draftText = null;
|
|
30
|
+
requestRender?.();
|
|
31
|
+
return { consume: true };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function moveWithinInput(direction) {
|
|
35
|
+
editor.lastAction = null;
|
|
36
|
+
if (direction < 0) {
|
|
37
|
+
if (editor.isOnFirstVisualLine?.()) editor.moveToLineStart?.();
|
|
38
|
+
else editor.moveCursor?.(-1, 0);
|
|
39
|
+
} else {
|
|
40
|
+
if (editor.isOnLastVisualLine?.()) editor.moveToLineEnd?.();
|
|
41
|
+
else editor.moveCursor?.(1, 0);
|
|
42
|
+
}
|
|
43
|
+
requestRender?.();
|
|
44
|
+
return { consume: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setEditorTextPreservingHistory(text) {
|
|
48
|
+
if (typeof editor.setTextInternal === "function") {
|
|
49
|
+
editor.setTextInternal(text);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const historyIndex = editor.historyIndex;
|
|
53
|
+
editor.setText?.(text);
|
|
54
|
+
editor.historyIndex = historyIndex;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -5,7 +5,6 @@ import { formatMessageAttachmentsForDisplay } from "../../session/attachment-dis
|
|
|
5
5
|
import { formatShellHints } from "../../shell/hints.mjs";
|
|
6
6
|
|
|
7
7
|
export function prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState = null }) {
|
|
8
|
-
memoryStore.beginTurn();
|
|
9
8
|
const engine = runner.engine ?? {};
|
|
10
9
|
const carryoverAlreadyRendered = engine.hasRenderedPendingAssistantRecallHints?.() ?? false;
|
|
11
10
|
const carryoverRecallHints = engine.takePendingAssistantRecallHints?.() ?? [];
|
package/src/cli/ui.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { showEditorSelectList } from "./tui/select/editor-select-list.mjs";
|
|
|
16
16
|
import { StatusBar } from "./tui/status/status-bar.mjs";
|
|
17
17
|
import { MainPaneLayout } from "./tui/layout/main-pane-layout.mjs";
|
|
18
18
|
import { SafeRenderBoundary } from "./tui/layout/safe-render-boundary.mjs";
|
|
19
|
+
import { createHistoryNavigationController } from "./tui/input/history-navigation-controller.mjs";
|
|
19
20
|
import { createMouseSelectionController } from "./tui/input/mouse-selection-controller.mjs";
|
|
20
21
|
import { ScreenSelection } from "./tui/selection-screen.mjs";
|
|
21
22
|
import { writeEditDiff } from "./tui/tui-diff-rendering.mjs";
|
|
@@ -68,6 +69,12 @@ export function createTuiUI({
|
|
|
68
69
|
const retryStatus = createRetryStatusController({ output, requestRender, stopSpinner: spinnerStatus.stop });
|
|
69
70
|
const shellDrawerControls = createShellDrawerControls({ shellDrawer, output, requestRender });
|
|
70
71
|
const mouseSelectionController = createMouseSelectionController({ terminal, output, shellDrawer, shellDrawerControls, selection, writeClipboard, requestRender });
|
|
72
|
+
const historyNavigationController = createHistoryNavigationController({
|
|
73
|
+
editor,
|
|
74
|
+
requestRender,
|
|
75
|
+
isAutocompleteOpen: () => editor.isShowingAutocomplete(),
|
|
76
|
+
hasOverlay: () => tui.hasOverlay(),
|
|
77
|
+
});
|
|
71
78
|
|
|
72
79
|
let onEscapeHandler = null, onCtrlCHandler = null, onShiftTabHandler = null;
|
|
73
80
|
let onCtrlTHandler = null, onCtrlLHandler = null, onPasteImageHandler = null, onToggleModeHandler = null;
|
|
@@ -108,6 +115,8 @@ export function createTuiUI({
|
|
|
108
115
|
requestRender();
|
|
109
116
|
return { consume: true };
|
|
110
117
|
}
|
|
118
|
+
const historyNavigationResult = historyNavigationController.handleInput(data);
|
|
119
|
+
if (historyNavigationResult) return historyNavigationResult;
|
|
111
120
|
});
|
|
112
121
|
terminal.write("\x1b[?1049h");
|
|
113
122
|
terminal.write("\x1b[?1002h\x1b[?1006h");
|
package/src/context/engine.mjs
CHANGED
|
@@ -71,18 +71,20 @@ export class ContextEngine {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
recordTurn({ userMessage, assistantMessage, assistantContext = "", userRecallHints = [], assistantRecallHints = [] }) {
|
|
74
|
-
|
|
74
|
+
const turn = {
|
|
75
75
|
index: this.turns.length + 1,
|
|
76
76
|
userMessage,
|
|
77
77
|
assistantMessage: assistantMessage ?? "",
|
|
78
78
|
assistantContext: assistantContext ?? "",
|
|
79
79
|
userRecallHints,
|
|
80
80
|
assistantRecallHints,
|
|
81
|
-
}
|
|
81
|
+
};
|
|
82
|
+
this.turns.push(turn);
|
|
82
83
|
if (this.turns.length > this.maxTurns) {
|
|
83
84
|
const keep = Math.max(1, this.maxTurns - this.trimBatch);
|
|
84
85
|
this.turns = this.turns.slice(-keep);
|
|
85
86
|
}
|
|
87
|
+
return turn;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
getRecentRecallMemoryIds() {
|
|
@@ -69,7 +69,10 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
|
|
|
69
69
|
|
|
70
70
|
<editing_contract>
|
|
71
71
|
- Use read(path) for file inspection with 1-based line numbers.
|
|
72
|
-
- Use
|
|
72
|
+
- Use code_search first when locating unknown implementations, responsibility boundaries, cross-module flows, or concept-level behavior.
|
|
73
|
+
- Use grep(pattern) and find(pattern) for exact symbol, string, filename, or call-site confirmation.
|
|
74
|
+
- Use ls(path) to inspect directory shape when structure matters.
|
|
75
|
+
- Treat code_search as a semantic map, not proof; verify important results with grep/read before editing or concluding.
|
|
73
76
|
- Prefer dedicated read/search/edit tools over shell commands for file inspection and modification.
|
|
74
77
|
- Use command_exec for one-shot commands. Use terminal_* only for interactive programs, long-running processes, or when preserving terminal state matters.
|
|
75
78
|
- Keep the working directory stable; use paths instead of cd unless the user asks otherwise.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { HistoryStore } from "./store.mjs";
|
|
3
|
+
|
|
4
|
+
export function createRunnerHistoryStore({ stateRoot, cwd } = {}) {
|
|
5
|
+
if (!stateRoot) return null;
|
|
6
|
+
return new HistoryStore({ root: join(stateRoot, "history"), cwd });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function appendRunnerTurnHistory({ store, turn, sessionStats, modelId, provider }) {
|
|
10
|
+
return store?.appendTurn({ turn, sessionStats, runtime: { modelId, provider } });
|
|
11
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import { searchMarkdownRoot } from "../memory/search.mjs";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_HISTORY_LIMIT = 20;
|
|
7
|
+
const MAX_HISTORY_LIMIT = 50;
|
|
8
|
+
|
|
9
|
+
export class HistoryStore {
|
|
10
|
+
constructor({ root, cwd, now = () => new Date() } = {}) {
|
|
11
|
+
if (!root) throw new Error("history root is required");
|
|
12
|
+
this.root = root;
|
|
13
|
+
this.cwd = cwd;
|
|
14
|
+
this.now = now;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
appendTurn({ turn, sessionStats = {}, runtime = {} } = {}) {
|
|
18
|
+
if (!turn) return null;
|
|
19
|
+
const sessionId = sanitizeId(sessionStats.sessionId ?? sessionStats.sessionFile ?? "session");
|
|
20
|
+
const filePath = join(this.#projectDir(), `${sessionId}.md`);
|
|
21
|
+
mkdirSync(this.#projectDir(), { recursive: true });
|
|
22
|
+
if (!existsSync(filePath)) writeFileSync(filePath, this.#fileHeader({ sessionId, sessionStats }), "utf8");
|
|
23
|
+
appendFileSync(filePath, this.#formatTurn({ turn, sessionId, sessionStats, runtime }), "utf8");
|
|
24
|
+
return filePath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
searchRipgrep(query, { allProjects = false, sessionId = null, limit = DEFAULT_HISTORY_LIMIT, context = 2, syntax = "regex", case: caseMode = "smart" } = {}) {
|
|
28
|
+
const root = allProjects ? this.root : this.#projectDir();
|
|
29
|
+
const glob = sessionId ? [`**/${sanitizeId(sessionId)}.md`] : [];
|
|
30
|
+
return searchMarkdownRoot({
|
|
31
|
+
root,
|
|
32
|
+
query,
|
|
33
|
+
limit: clampInt(limit, 1, MAX_HISTORY_LIMIT, DEFAULT_HISTORY_LIMIT),
|
|
34
|
+
context,
|
|
35
|
+
syntax,
|
|
36
|
+
caseMode,
|
|
37
|
+
glob,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#projectDir() {
|
|
42
|
+
return join(this.root, "projects", cwdHash(this.cwd));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#fileHeader({ sessionId, sessionStats }) {
|
|
46
|
+
return [
|
|
47
|
+
`# March History · ${sessionId}`,
|
|
48
|
+
"",
|
|
49
|
+
`- cwd: ${this.cwd}`,
|
|
50
|
+
`- project: ${basename(this.cwd) || this.cwd}`,
|
|
51
|
+
sessionStats.sessionFile ? `- session_file: ${sessionStats.sessionFile}` : null,
|
|
52
|
+
"",
|
|
53
|
+
].filter(Boolean).join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#formatTurn({ turn, sessionId, sessionStats, runtime }) {
|
|
57
|
+
const now = this.now().toISOString();
|
|
58
|
+
return [
|
|
59
|
+
`\n## Turn ${turn.index ?? "?"} · ${now}`,
|
|
60
|
+
"",
|
|
61
|
+
"metadata:",
|
|
62
|
+
`- session: ${sessionId}`,
|
|
63
|
+
`- cwd: ${this.cwd}`,
|
|
64
|
+
runtime.provider ? `- provider: ${runtime.provider}` : null,
|
|
65
|
+
runtime.modelId ? `- model: ${runtime.modelId}` : null,
|
|
66
|
+
sessionStats.sessionName ? `- session_name: ${sessionStats.sessionName}` : null,
|
|
67
|
+
"",
|
|
68
|
+
"### User",
|
|
69
|
+
safeBlock(turn.userMessage),
|
|
70
|
+
formatRecallSection("User memory recall", turn.userRecallHints),
|
|
71
|
+
"### Assistant",
|
|
72
|
+
safeBlock(turn.assistantMessage),
|
|
73
|
+
turn.thinking ? ["### Thinking", safeBlock(turn.thinking)].join("\n") : null,
|
|
74
|
+
"### Tool calls",
|
|
75
|
+
formatToolCalls(turn.toolCalls),
|
|
76
|
+
formatRecallSection("Assistant memory recall", turn.assistantRecallHints),
|
|
77
|
+
"",
|
|
78
|
+
].filter(Boolean).join("\n");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatToolCalls(calls = []) {
|
|
83
|
+
if (!Array.isArray(calls) || calls.length === 0) return "(none)";
|
|
84
|
+
return calls.map((call) => {
|
|
85
|
+
const lines = [`- ${call.name ?? "unknown"} status=${call.status ?? "unknown"}`];
|
|
86
|
+
const args = JSON.stringify(call.args ?? null);
|
|
87
|
+
if (args && args !== "null") lines.push(` args: ${args}`);
|
|
88
|
+
if (call.status === "failed") {
|
|
89
|
+
lines.push(" error:");
|
|
90
|
+
if (call.error?.message) lines.push(` message: ${escapeSingleLine(call.error.message)}`);
|
|
91
|
+
if (call.error?.details) lines.push(` details: ${JSON.stringify(call.error.details)}`);
|
|
92
|
+
if (call.error?.excerpt) lines.push(" excerpt:", fence(call.error.excerpt));
|
|
93
|
+
}
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}).join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatRecallSection(title, hints = []) {
|
|
99
|
+
if (!Array.isArray(hints) || hints.length === 0) return null;
|
|
100
|
+
return [`### ${title}`, ...hints.map((hint) => `- ${hint.id ?? "unknown"} | ${hint.name ?? hint.title ?? ""} | ${hint.description ?? ""}`)].join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function safeBlock(value) {
|
|
104
|
+
const text = String(value ?? "").trim();
|
|
105
|
+
return text || "(empty)";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function fence(value) {
|
|
109
|
+
return " ```text\n" + String(value ?? "").trimEnd() + "\n ```";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sanitizeId(value) {
|
|
113
|
+
const raw = String(value ?? "session").trim() || "session";
|
|
114
|
+
return raw.replace(/[^a-zA-Z0-9_.-]+/g, "_").slice(0, 120) || "session";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function cwdHash(cwd) {
|
|
118
|
+
return createHash("sha1").update(String(cwd ?? "")).digest("hex").slice(0, 16);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function escapeSingleLine(value) {
|
|
122
|
+
return JSON.stringify(String(value ?? "").replace(/\s+/g, " "));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function clampInt(value, min, max, fallback) {
|
|
126
|
+
const number = Number(value);
|
|
127
|
+
if (!Number.isFinite(number)) return fallback;
|
|
128
|
+
return Math.min(max, Math.max(min, Math.floor(number)));
|
|
129
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { toolText } from "../agent/tool-result.mjs";
|
|
4
|
+
|
|
5
|
+
export function createHistorySearchTool({ store } = {}) {
|
|
6
|
+
if (!store) return null;
|
|
7
|
+
return defineTool({
|
|
8
|
+
name: "history_search",
|
|
9
|
+
label: "History Search",
|
|
10
|
+
description: "Search archived March turn history with ripgrep. Use it when you need details from previous sessions or earlier turns. History stores user/assistant text, visible thinking, tool call metadata, memory recall hints, and failed tool error excerpts; successful tool results are not stored.",
|
|
11
|
+
parameters: Type.Object({
|
|
12
|
+
query: Type.String({ description: "Ripgrep query/pattern to search in archived turn history" }),
|
|
13
|
+
allProjects: Type.Optional(Type.Boolean({ description: "Search all project histories instead of the current cwd history. Default false." })),
|
|
14
|
+
sessionId: Type.Optional(Type.String({ description: "Limit search to a specific session id when known" })),
|
|
15
|
+
syntax: Type.Optional(Type.Union([Type.Literal("regex"), Type.Literal("literal")], { description: "Pattern syntax. Default: regex" })),
|
|
16
|
+
case: Type.Optional(Type.Union([Type.Literal("smart"), Type.Literal("sensitive"), Type.Literal("insensitive")], { description: "Case matching mode. Default: smart" })),
|
|
17
|
+
context: Type.Optional(Type.Number({ description: "Context lines around each match. Default: 2" })),
|
|
18
|
+
limit: Type.Optional(Type.Number({ description: "Maximum matches to return. Default: 20, max: 50" })),
|
|
19
|
+
}),
|
|
20
|
+
execute: async (_toolCallId, params) => {
|
|
21
|
+
try {
|
|
22
|
+
const results = store.searchRipgrep(params.query, params);
|
|
23
|
+
if (results.length === 0) return toolText(`history_search found no matches for ${JSON.stringify(params.query)}.`);
|
|
24
|
+
return toolText(formatHistorySearchResults(results), { results });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return toolText(`Error: ${err.message}`, { error: true });
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatHistorySearchResults(results) {
|
|
33
|
+
const lines = [`history_search found ${results.length} match${results.length === 1 ? "" : "es"}.`];
|
|
34
|
+
results.forEach((result, index) => {
|
|
35
|
+
lines.push("", `[${index + 1}] ${result.path}:${result.line}`);
|
|
36
|
+
if (result.excerpt?.text) lines.push(result.excerpt.text);
|
|
37
|
+
});
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
package/src/lsp/client.mjs
CHANGED
|
@@ -73,13 +73,14 @@ export function languageIdForPath(path) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export class LspClient {
|
|
76
|
-
constructor({ serverId, command, args = [], cwd, initialization = {}, store }) {
|
|
76
|
+
constructor({ serverId, command, args = [], cwd, initialization = {}, store, onStatusChange = null }) {
|
|
77
77
|
this.serverId = serverId;
|
|
78
78
|
this.command = command;
|
|
79
79
|
this.args = args;
|
|
80
80
|
this.cwd = cwd;
|
|
81
81
|
this.initialization = initialization;
|
|
82
82
|
this.store = store;
|
|
83
|
+
this.onStatusChange = onStatusChange;
|
|
83
84
|
this.status = "starting";
|
|
84
85
|
this.process = null;
|
|
85
86
|
this.buffer = Buffer.alloc(0);
|
|
@@ -98,7 +99,7 @@ export class LspClient {
|
|
|
98
99
|
});
|
|
99
100
|
this.process.stdout.on("data", (chunk) => this.#onData(chunk));
|
|
100
101
|
this.process.on("exit", () => {
|
|
101
|
-
this
|
|
102
|
+
this.#setStatus("failed");
|
|
102
103
|
for (const pending of this.pending.values()) pending.reject(new Error("LSP exited"));
|
|
103
104
|
this.pending.clear();
|
|
104
105
|
});
|
|
@@ -124,7 +125,7 @@ export class LspClient {
|
|
|
124
125
|
}), INITIALIZE_TIMEOUT_MS);
|
|
125
126
|
this.syncKind = getSyncKind(initialized?.capabilities);
|
|
126
127
|
this.#notify("initialized", {});
|
|
127
|
-
this
|
|
128
|
+
this.#setStatus("ready");
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
touchFile(path) {
|
|
@@ -132,7 +133,7 @@ export class LspClient {
|
|
|
132
133
|
const text = readFileSync(path, "utf8");
|
|
133
134
|
const uri = pathToFileURL(path).href;
|
|
134
135
|
const existing = this.documents.get(path);
|
|
135
|
-
this
|
|
136
|
+
this.#setStatus("busy");
|
|
136
137
|
if (existing) {
|
|
137
138
|
const version = existing.version + 1;
|
|
138
139
|
this.documents.set(path, { version, text });
|
|
@@ -199,7 +200,7 @@ export class LspClient {
|
|
|
199
200
|
uri: message.params?.uri,
|
|
200
201
|
diagnostics: message.params?.diagnostics ?? [],
|
|
201
202
|
});
|
|
202
|
-
this
|
|
203
|
+
this.#setStatus("idle");
|
|
203
204
|
return;
|
|
204
205
|
}
|
|
205
206
|
|
|
@@ -208,6 +209,12 @@ export class LspClient {
|
|
|
208
209
|
}
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
#setStatus(status) {
|
|
213
|
+
if (this.status === status) return;
|
|
214
|
+
this.status = status;
|
|
215
|
+
this.onStatusChange?.({ id: this.serverId, root: this.cwd, status });
|
|
216
|
+
}
|
|
217
|
+
|
|
211
218
|
#requestResult(method) {
|
|
212
219
|
if (method === "workspace/configuration") return [];
|
|
213
220
|
if (method === "workspace/workspaceFolders") return [{ name: "workspace", uri: pathToFileURL(this.cwd).href }];
|
package/src/lsp/service.mjs
CHANGED
|
@@ -3,9 +3,10 @@ import { LspDiagnosticStore } from "./diagnostic-store.mjs";
|
|
|
3
3
|
import { resolveLspServerStatus } from "./servers.mjs";
|
|
4
4
|
|
|
5
5
|
export class LspService {
|
|
6
|
-
constructor({ cwd, onEvent = null }) {
|
|
6
|
+
constructor({ cwd, onEvent = null, onStatusChange = null }) {
|
|
7
7
|
this.cwd = cwd;
|
|
8
8
|
this.onEvent = onEvent;
|
|
9
|
+
this.onStatusChange = onStatusChange;
|
|
9
10
|
this.store = new LspDiagnosticStore();
|
|
10
11
|
this.clients = new Map();
|
|
11
12
|
this.spawning = new Map();
|
|
@@ -19,6 +20,7 @@ export class LspService {
|
|
|
19
20
|
if (result.status === "unavailable") {
|
|
20
21
|
this.unavailable.set(result.id, result);
|
|
21
22
|
this.#emitOnce(`unavailable:${result.id}:${result.reason}`, result);
|
|
23
|
+
this.#emitStatusChange(result);
|
|
22
24
|
return result;
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -34,7 +36,9 @@ export class LspService {
|
|
|
34
36
|
return { status: "starting", id: server.id, root: server.root };
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
const startingEvent = { status: "starting", id: server.id, root: server.root, managed: server.managed };
|
|
40
|
+
this.#emitOnce(`starting:${key}`, startingEvent);
|
|
41
|
+
this.#emitStatusChange(startingEvent);
|
|
38
42
|
const task = this.#startClient(server, key).then((client) => {
|
|
39
43
|
client?.touchFile(path);
|
|
40
44
|
return client;
|
|
@@ -76,18 +80,22 @@ export class LspService {
|
|
|
76
80
|
cwd: server.root,
|
|
77
81
|
initialization: server.initialization,
|
|
78
82
|
store: this.store,
|
|
83
|
+
onStatusChange: (event) => this.#emitStatusChange(event),
|
|
79
84
|
});
|
|
80
85
|
try {
|
|
81
86
|
await client.start();
|
|
82
87
|
this.clients.set(key, client);
|
|
83
88
|
this.unavailable.delete(server.id);
|
|
84
|
-
|
|
89
|
+
const attachedEvent = { status: "attached", id: server.id, root: server.root, managed: server.managed };
|
|
90
|
+
this.#emitOnce(`attached:${key}`, attachedEvent);
|
|
91
|
+
this.#emitStatusChange(attachedEvent);
|
|
85
92
|
return client;
|
|
86
93
|
} catch (err) {
|
|
87
94
|
client.status = "failed";
|
|
88
95
|
const event = { status: "failed", id: server.id, root: server.root, reason: err.message };
|
|
89
96
|
this.unavailable.set(server.id, event);
|
|
90
97
|
this.#emitOnce(`failed:${key}:${err.message}`, event);
|
|
98
|
+
this.#emitStatusChange(event);
|
|
91
99
|
return null;
|
|
92
100
|
}
|
|
93
101
|
}
|
|
@@ -97,6 +105,10 @@ export class LspService {
|
|
|
97
105
|
this.announced.add(key);
|
|
98
106
|
this.onEvent?.(event);
|
|
99
107
|
}
|
|
108
|
+
|
|
109
|
+
#emitStatusChange(event) {
|
|
110
|
+
this.onStatusChange?.(event);
|
|
111
|
+
}
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
function summarizeStatus(servers) {
|
package/src/main.mjs
CHANGED
|
@@ -26,13 +26,12 @@ export async function run(argv) {
|
|
|
26
26
|
|
|
27
27
|
const config = loadConfig(cwd);
|
|
28
28
|
const stateRoot = join(homedir(), ".march");
|
|
29
|
-
const useRuntimeProcess = process.env.MARCH_RUNTIME_PROCESS !== "0";
|
|
30
29
|
installNetworkEnvironment(config.network);
|
|
31
30
|
|
|
32
31
|
const earlyCommand = await runEarlyCliCommand(args, { config, cwd, stateRoot });
|
|
33
32
|
if (earlyCommand.handled) return earlyCommand.code;
|
|
34
33
|
|
|
35
|
-
const app = await createCliAppRuntime({ args, config, cwd, argv, stateRoot
|
|
34
|
+
const app = await createCliAppRuntime({ args, config, cwd, argv, stateRoot });
|
|
36
35
|
if (!app.ok) return app.code;
|
|
37
36
|
|
|
38
37
|
const gatewayDaemonCommand = await maybeRunGatewayDaemonCommand(args, {
|
package/src/web-ui/command.mjs
CHANGED
|
@@ -6,11 +6,11 @@ import { createWebSessionManager, resolveWorkspace } from "./session-manager.mjs
|
|
|
6
6
|
const DEFAULT_HOST = "127.0.0.1";
|
|
7
7
|
const DEFAULT_PORT = 4174;
|
|
8
8
|
|
|
9
|
-
export async function runWebUiCommand(args, { config, cwd, stateRoot
|
|
9
|
+
export async function runWebUiCommand(args, { config, cwd, stateRoot } = {}) {
|
|
10
10
|
const host = args.host ?? DEFAULT_HOST;
|
|
11
11
|
assertLoopbackHost(host);
|
|
12
12
|
const port = Number.parseInt(args.port ?? "", 10) || DEFAULT_PORT;
|
|
13
|
-
const runtime = createWebSessionManager({ args, config, launchCwd: cwd, stateRoot
|
|
13
|
+
const runtime = createWebSessionManager({ args, config, launchCwd: cwd, stateRoot });
|
|
14
14
|
const initialWorkspace = resolveInitialWorkspace(args, cwd);
|
|
15
15
|
if (initialWorkspace) await runtime.createSession(initialWorkspace);
|
|
16
16
|
|
|
@@ -3,17 +3,12 @@ import { basename, join, resolve } from "node:path";
|
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
4
4
|
import { createMarchAuthStorage } from "../auth/storage.mjs";
|
|
5
5
|
import { createRuntimeRunner } from "../cli/startup/create-runtime-runner.mjs";
|
|
6
|
-
import { createPermissionController, MODE } from "../cli/permissions.mjs";
|
|
7
6
|
import { createCliShellRuntime } from "../shell/cli-runtime.mjs";
|
|
8
7
|
import { MarkdownMemoryStore } from "../memory/markdown-store.mjs";
|
|
9
|
-
import { createMarkdownMemoryTools } from "../memory/markdown-tools.mjs";
|
|
10
8
|
import { resolveMemoryRoot } from "../memory/root.mjs";
|
|
11
9
|
import { defaultProfilePaths, ensureProfileFiles } from "../context/profiles.mjs";
|
|
12
10
|
import { loadOrCreateProjectId } from "../cli/startup/startup-session.mjs";
|
|
13
|
-
import { createWebToolsFromConfig } from "../web/tools.mjs";
|
|
14
11
|
import { createLogger, installProcessLogHandlers } from "../debug/logger.mjs";
|
|
15
|
-
import { createModelContextDumper } from "../debug/model-context-dumper.mjs";
|
|
16
|
-
import { createDesktopTurnNotifier } from "../notification/desktop-notifier.mjs";
|
|
17
12
|
import { discoverProjectExtensionPaths } from "../extensions/discovery.mjs";
|
|
18
13
|
import { loadProjectLifecycleHookManifests } from "../extensions/lifecycle-manifest.mjs";
|
|
19
14
|
import { normalizeRemoteMemorySources } from "../memory/remote/config.mjs";
|
|
@@ -22,7 +17,7 @@ import { prepareTurnInput } from "../cli/turn/turn-input-preparer.mjs";
|
|
|
22
17
|
const MAX_WORKSPACE_DEPTH = 3;
|
|
23
18
|
const MAX_WORKSPACE_ENTRIES = 200;
|
|
24
19
|
|
|
25
|
-
export async function createWebRuntimeHost({ args, config, cwd, stateRoot
|
|
20
|
+
export async function createWebRuntimeHost({ args, config, cwd, stateRoot } = {}) {
|
|
26
21
|
stateRoot ??= join(homedir(), ".march");
|
|
27
22
|
if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
|
|
28
23
|
const logger = createLogger({ logDir: join(stateRoot, "logs") });
|
|
@@ -40,14 +35,10 @@ export async function createWebRuntimeHost({ args, config, cwd, stateRoot, useRu
|
|
|
40
35
|
|
|
41
36
|
const memoryStore = new MarkdownMemoryStore({ root: memoryRoot });
|
|
42
37
|
const remoteMemorySources = normalizeRemoteMemorySources(config);
|
|
43
|
-
const memoryTools = createMarkdownMemoryTools(memoryStore, { remoteSources: remoteMemorySources });
|
|
44
38
|
const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
|
|
45
39
|
const extensionPaths = discoverProjectExtensionPaths(cwd);
|
|
46
40
|
const lifecycleManifests = loadProjectLifecycleHookManifests(cwd);
|
|
47
41
|
const contextDumpRoot = resolve(projectMarchDir, "context-dumps", Date.now().toString(36));
|
|
48
|
-
const modelContextDumper = createModelContextDumper({ enabled: args.dumpContext, rootDir: contextDumpRoot });
|
|
49
|
-
const permissionController = createPermissionController({ mode: args.permissionMode ?? MODE.BYPASS });
|
|
50
|
-
const turnNotifier = createDesktopTurnNotifier({ enabled: Boolean(config.notifications?.turnEnd), config: config.notifications });
|
|
51
42
|
const ui = createHeadlessWebUi();
|
|
52
43
|
const currentProject = basename(cwd);
|
|
53
44
|
const namespace = loadOrCreateProjectId(projectMarchDir);
|
|
@@ -72,20 +63,9 @@ export async function createWebRuntimeHost({ args, config, cwd, stateRoot, useRu
|
|
|
72
63
|
remoteMemorySources,
|
|
73
64
|
};
|
|
74
65
|
const runner = await createRuntimeRunner({
|
|
75
|
-
useRuntimeProcess,
|
|
76
66
|
runnerOptions,
|
|
77
67
|
ui,
|
|
78
|
-
memoryStore,
|
|
79
|
-
memoryTools,
|
|
80
68
|
shellRuntime,
|
|
81
|
-
webTools: createWebToolsFromConfig(config),
|
|
82
|
-
usePiSessions: true,
|
|
83
|
-
usePiRuntimeHost: true,
|
|
84
|
-
authStorage: authConfig.authStorage,
|
|
85
|
-
permissionController,
|
|
86
|
-
modelContextDumper,
|
|
87
|
-
turnNotifier,
|
|
88
|
-
logger,
|
|
89
69
|
});
|
|
90
70
|
let turnRunning = false;
|
|
91
71
|
|
|
@@ -100,9 +80,10 @@ export async function createWebRuntimeHost({ args, config, cwd, stateRoot, useRu
|
|
|
100
80
|
async runTurn(prompt) {
|
|
101
81
|
if (turnRunning) throw new Error("A turn is already running");
|
|
102
82
|
turnRunning = true;
|
|
103
|
-
|
|
104
|
-
runner.runtimeUiEvents.emit({ type: "web_user_message", text: input.userMessage });
|
|
83
|
+
memoryStore.beginTurn();
|
|
105
84
|
try {
|
|
85
|
+
const input = prepareTurnInput({ prompt, runner, memoryStore, currentProject });
|
|
86
|
+
runner.runtimeUiEvents.emit({ type: "web_user_message", text: input.userMessage });
|
|
106
87
|
return await runner.runTurn(input.fullPrompt, input.userMessage, input.runOptions);
|
|
107
88
|
} finally {
|
|
108
89
|
turnRunning = false;
|
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { basename, resolve } from "node:path";
|
|
4
4
|
import { createWebRuntimeHost } from "./runtime-host.mjs";
|
|
5
5
|
|
|
6
|
-
export function createWebSessionManager({ args, config, launchCwd, stateRoot
|
|
6
|
+
export function createWebSessionManager({ args, config, launchCwd, stateRoot } = {}) {
|
|
7
7
|
const sessions = new Map();
|
|
8
8
|
const activities = [];
|
|
9
9
|
let activeSessionId = null;
|
|
@@ -12,7 +12,7 @@ export function createWebSessionManager({ args, config, launchCwd, stateRoot, us
|
|
|
12
12
|
async function createSession(workspacePath) {
|
|
13
13
|
const workspace = resolveWorkspace(workspacePath, launchCwd);
|
|
14
14
|
const id = `session-${Date.now().toString(36)}-${nextSessionNumber++}`;
|
|
15
|
-
const runtime = await createWebRuntimeHost({ args, config, cwd: workspace, stateRoot
|
|
15
|
+
const runtime = await createWebRuntimeHost({ args, config, cwd: workspace, stateRoot });
|
|
16
16
|
const session = { id, workspace, title: basename(workspace) || workspace, runtime, createdAt: Date.now() };
|
|
17
17
|
sessions.set(id, session);
|
|
18
18
|
activeSessionId = id;
|