march-cli 0.1.2 → 0.1.4
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 +3 -1
- package/src/agent/runner.mjs +41 -10
- package/src/agent/turn/turn-runner.mjs +191 -18
- package/src/assets/march-icon.png +0 -0
- package/src/cli/repl-loop.mjs +27 -3
- package/src/cli/slash-commands.mjs +17 -1
- package/src/cli/tui/input/mouse-selection-controller.mjs +3 -1
- package/src/cli/tui/markdown-renderer.mjs +6 -0
- package/src/cli/tui/output/scroll-state.mjs +2 -2
- package/src/cli/tui/output/text-line-renderer.mjs +50 -0
- package/src/cli/tui/output-buffer.mjs +40 -53
- package/src/cli/tui/render/render-scheduler.mjs +26 -0
- package/src/cli/tui/tool-rendering.mjs +3 -0
- package/src/cli/ui.mjs +6 -7
- package/src/config/loader.mjs +1 -1
- package/src/context/center-memory.mjs +14 -0
- package/src/context/engine.mjs +49 -1
- package/src/context/system-core/base.md +5 -1
- package/src/main.mjs +4 -0
- package/src/notification/desktop-notifier.mjs +197 -20
- package/src/session/sidecar.mjs +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "march-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "March CLI — terminal-native coding agent with context reconstruction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/main.mjs",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"test:shell-tui-real": "node test/shell-tui-real.acceptance.mjs",
|
|
20
20
|
"test:tui-key-real": "node test/tui-key-real.acceptance.mjs",
|
|
21
21
|
"context": "cd .. && node march-cli/bin/march.mjs --dump-context",
|
|
22
|
+
"notify:experiment": "node scripts/notify-experiment.mjs",
|
|
22
23
|
"publish:env": "node scripts/npm-publish-from-env.mjs"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
30
|
"@xterm/headless": "^5.5.0",
|
|
30
31
|
"marked": "^18.0.3",
|
|
32
|
+
"node-notifier": "^10.0.1",
|
|
31
33
|
"node-pty": "^1.1.0",
|
|
32
34
|
"typebox": "^1.0.58",
|
|
33
35
|
"web-tree-sitter": "^0.26.8"
|
package/src/agent/runner.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { createMarchLifecycleAdapter } from "../extensions/lifecycle-adapter.mjs
|
|
|
9
9
|
import { syncPiSessionSidecar } from "../session/sidecar-sync.mjs";
|
|
10
10
|
import { LspService } from "../lsp/service.mjs";
|
|
11
11
|
import { formatRecallHints } from "../memory/markdown-store.mjs";
|
|
12
|
-
import { appendProviderUserMessage, installModelPayloadDumper, replaceProviderContextMessages } from "./model-payload-dumper.mjs";
|
|
12
|
+
import { appendProviderUserMessage, estimateProviderPayloadTokens, installModelPayloadDumper, replaceProviderContextMessages } from "./model-payload-dumper.mjs";
|
|
13
13
|
import { resolveInitialModel, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
|
|
14
14
|
import { runRunnerCleanup } from "./runner/runner-cleanup.mjs";
|
|
15
15
|
import { createRunnerRuntimeHost } from "./runtime/runner-runtime-host.mjs";
|
|
@@ -18,7 +18,7 @@ import { resolveRunnerSessionOptions } from "./session/session-options.mjs";
|
|
|
18
18
|
import { createSessionBinding } from "./session/session-binding.mjs";
|
|
19
19
|
import { maybeAutoNameSession } from "./session/session-auto-name.mjs";
|
|
20
20
|
import { MARCH_BASE_TOOL_NAMES } from "./tool-names.mjs";
|
|
21
|
-
import { runRunnerTurn } from "./turn/turn-runner.mjs";
|
|
21
|
+
import { isModelStreamIdleTimeoutError, runRunnerTurn } from "./turn/turn-runner.mjs";
|
|
22
22
|
import { appendFastVariants, createFastModelEntry, fromFastEntryModel, isFastProvider } from "./runner/fast-model.mjs";
|
|
23
23
|
import { registerSuperGrokProvider } from "../supergrok/provider.mjs";
|
|
24
24
|
|
|
@@ -27,7 +27,7 @@ export { installModelPayloadDumper } from "./model-payload-dumper.mjs";
|
|
|
27
27
|
export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
|
|
28
28
|
export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
29
29
|
|
|
30
|
-
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, memoryStore = null, memoryTools = [], 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, onModelPayload = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null }) {
|
|
30
|
+
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, centerMemoryPath = null, memoryStore = null, memoryTools = [], 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, onModelPayload = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null, modelStreamIdleTimeoutMs = 7000, modelStreamIdleMaxRetries = 5 }) {
|
|
31
31
|
if (!useRuntimeHost && extensionPaths.length > 0) {
|
|
32
32
|
throw new Error("--extension requires the default pi runtime host path");
|
|
33
33
|
}
|
|
@@ -47,7 +47,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
47
47
|
retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
|
|
48
48
|
});
|
|
49
49
|
const lspService = new LspService({ cwd });
|
|
50
|
-
const engine = new ContextEngine({ cwd, modelId, provider, namespace, memoryRoot, shellRuntime, lspService, injections: mcpInjections, maxTurns, trimBatch });
|
|
50
|
+
const engine = new ContextEngine({ cwd, modelId, provider, namespace, memoryRoot, centerMemoryPath, shellRuntime, lspService, injections: mcpInjections, maxTurns, trimBatch });
|
|
51
51
|
const resolvedSessionManager = resolveRunnerSessionManager(cwd, sessionManager);
|
|
52
52
|
const sessionBinding = createSessionBinding(null);
|
|
53
53
|
let currentModelCallKind = "model";
|
|
@@ -55,6 +55,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
55
55
|
let currentTurnContextMode = "rebuild";
|
|
56
56
|
let nextTurnContextMode = "rebuild";
|
|
57
57
|
let pendingMidTurnRecallHints = [];
|
|
58
|
+
let lastNotificationResult = null;
|
|
58
59
|
let runtimeHost = null;
|
|
59
60
|
let lifecycleAdapter = null;
|
|
60
61
|
let _currentFastEntry = null;
|
|
@@ -109,9 +110,10 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
109
110
|
currentTurnContextMode = contextMode;
|
|
110
111
|
nextTurnContextMode = "rebuild";
|
|
111
112
|
pendingMidTurnRecallHints = [];
|
|
113
|
+
const turnStartedAt = Date.now();
|
|
112
114
|
try {
|
|
113
115
|
const result = await runRunnerTurn({
|
|
114
|
-
prompt, userMessage, options: { userRecallHints, currentProject },
|
|
116
|
+
prompt, userMessage, options: { userRecallHints, currentProject, modelStreamIdleTimeoutMs, modelStreamIdleMaxRetries },
|
|
115
117
|
sessionBinding, engine, ui, projectMarchDir, memoryStore,
|
|
116
118
|
setModelCallKind: (kind) => { currentModelCallKind = kind; },
|
|
117
119
|
onMidTurnRecallHints: (hints) => { pendingMidTurnRecallHints.push(...hints); },
|
|
@@ -119,17 +121,22 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
119
121
|
autoNameSession,
|
|
120
122
|
contextMode,
|
|
121
123
|
});
|
|
122
|
-
await notifyTurnEndBestEffort(turnNotifier, {
|
|
124
|
+
lastNotificationResult = await notifyTurnEndBestEffort(turnNotifier, {
|
|
123
125
|
status: "success",
|
|
124
126
|
sessionName: engine.sessionName,
|
|
125
127
|
draft: result?.draft ?? "",
|
|
128
|
+
durationMs: Date.now() - turnStartedAt,
|
|
126
129
|
});
|
|
127
130
|
return result;
|
|
128
131
|
} catch (err) {
|
|
129
|
-
|
|
132
|
+
if (isModelStreamIdleTimeoutError(err)) {
|
|
133
|
+
nextTurnContextMode = "continueExistingPiTranscript";
|
|
134
|
+
}
|
|
135
|
+
lastNotificationResult = await notifyTurnEndBestEffort(turnNotifier, {
|
|
130
136
|
status: "error",
|
|
131
137
|
sessionName: engine.sessionName,
|
|
132
138
|
errorMessage: err?.message ?? String(err),
|
|
139
|
+
durationMs: Date.now() - turnStartedAt,
|
|
133
140
|
});
|
|
134
141
|
throw err;
|
|
135
142
|
} finally {
|
|
@@ -176,6 +183,20 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
176
183
|
return [...new Set([...configured, ...available])];
|
|
177
184
|
},
|
|
178
185
|
getSessionStats() { return getRunnerSessionStats(sessionBinding.get(), runtimeHost); },
|
|
186
|
+
getLastNotificationResult() { return lastNotificationResult; },
|
|
187
|
+
async notifyTest({ title = "March", message = "If you see this, March runtime notifications work." } = {}) {
|
|
188
|
+
lastNotificationResult = await notifyTurnEndBestEffort(turnNotifier, {
|
|
189
|
+
status: "success",
|
|
190
|
+
sessionName: engine.sessionName,
|
|
191
|
+
title,
|
|
192
|
+
message,
|
|
193
|
+
durationMs: 0,
|
|
194
|
+
});
|
|
195
|
+
return lastNotificationResult;
|
|
196
|
+
},
|
|
197
|
+
estimateContextTokens(userMessage = "") {
|
|
198
|
+
return estimateProviderPayloadTokens(providerContextToPayload(engine.buildProviderContext(userMessage)));
|
|
199
|
+
},
|
|
179
200
|
setSessionName(name) {
|
|
180
201
|
const activeSession = sessionBinding.get();
|
|
181
202
|
activeSession.setSessionName?.(name);
|
|
@@ -256,11 +277,21 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
256
277
|
}
|
|
257
278
|
}
|
|
258
279
|
|
|
280
|
+
function providerContextToPayload(providerContext) {
|
|
281
|
+
return {
|
|
282
|
+
messages: [
|
|
283
|
+
{ role: "system", content: providerContext.system },
|
|
284
|
+
...(providerContext.userMessages ?? []).map((message) => ({ role: "user", content: message.content })),
|
|
285
|
+
],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
259
289
|
async function notifyTurnEndBestEffort(turnNotifier, event) {
|
|
260
|
-
if (!turnNotifier?.notifyTurnEnd) return;
|
|
290
|
+
if (!turnNotifier?.notifyTurnEnd) return { ok: false, reason: "not-configured", results: [] };
|
|
261
291
|
try {
|
|
262
|
-
await turnNotifier.notifyTurnEnd(event);
|
|
263
|
-
} catch {
|
|
292
|
+
return await turnNotifier.notifyTurnEnd(event);
|
|
293
|
+
} catch (err) {
|
|
264
294
|
// Notification must never change turn behavior.
|
|
295
|
+
return { ok: false, reason: err?.message ?? String(err), results: [] };
|
|
265
296
|
}
|
|
266
297
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { resolveImageAttachmentReferences } from "../../session/attachment-references.mjs";
|
|
2
2
|
import { closeAssistantReply, createTurnEventState, handleRunnerSessionEvent } from "./turn-events.mjs";
|
|
3
3
|
|
|
4
|
+
export function isModelStreamIdleTimeoutError(err) {
|
|
5
|
+
return err?.code === "MODEL_STREAM_IDLE_TIMEOUT";
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
export async function runRunnerTurn({
|
|
5
9
|
prompt,
|
|
6
10
|
userMessage,
|
|
@@ -16,19 +20,31 @@ export async function runRunnerTurn({
|
|
|
16
20
|
autoNameSession,
|
|
17
21
|
contextMode = "rebuild",
|
|
18
22
|
}) {
|
|
19
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
userRecallHints = [],
|
|
25
|
+
currentProject = "",
|
|
26
|
+
modelStreamIdleTimeoutMs = 7000,
|
|
27
|
+
modelStreamIdleMaxRetries = 5,
|
|
28
|
+
} = options;
|
|
20
29
|
const activeSession = sessionBinding.get();
|
|
21
30
|
const turnState = createTurnEventState();
|
|
22
31
|
const midTurnRecallHints = [];
|
|
32
|
+
const modelIdleWatchdog = createModelStreamIdleWatchdog({
|
|
33
|
+
session: activeSession,
|
|
34
|
+
ui,
|
|
35
|
+
timeoutMs: modelStreamIdleTimeoutMs,
|
|
36
|
+
});
|
|
23
37
|
ui.turnStart();
|
|
24
38
|
|
|
25
39
|
const unsubscribe = activeSession.subscribe((event) => {
|
|
40
|
+
modelIdleWatchdog.handleEvent(event);
|
|
26
41
|
handleRunnerSessionEvent(event, { ui, engine, state: turnState });
|
|
27
42
|
if (event.type === "tool_execution_start") {
|
|
28
43
|
const hints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
|
|
29
44
|
if (hints.length > 0) {
|
|
30
45
|
midTurnRecallHints.push(...hints);
|
|
31
46
|
onMidTurnRecallHints?.(hints);
|
|
47
|
+
ui.memoryHint?.({ source: "assistant", hints });
|
|
32
48
|
}
|
|
33
49
|
}
|
|
34
50
|
});
|
|
@@ -40,29 +56,48 @@ export async function runRunnerTurn({
|
|
|
40
56
|
});
|
|
41
57
|
setModelCallKind("user");
|
|
42
58
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
contextMode === "continueExistingPiTranscript" ? (userMessage ?? prompt) : prompt,
|
|
46
|
-
attachmentReferences.images.length > 0 ? { images: attachmentReferences.images } : undefined,
|
|
47
|
-
|
|
59
|
+
await promptWithModelIdleRetry({
|
|
60
|
+
session: activeSession,
|
|
61
|
+
prompt: contextMode === "continueExistingPiTranscript" ? (userMessage ?? prompt) : prompt,
|
|
62
|
+
promptOptions: attachmentReferences.images.length > 0 ? { images: attachmentReferences.images } : undefined,
|
|
63
|
+
resetBeforeAttempt: contextMode === "rebuild",
|
|
64
|
+
maxRetries: modelStreamIdleMaxRetries,
|
|
65
|
+
watchdog: modelIdleWatchdog,
|
|
66
|
+
});
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (!isModelStreamIdleTimeoutError(err)) throw err;
|
|
69
|
+
finalizeTurn({
|
|
70
|
+
prompt,
|
|
71
|
+
userMessage,
|
|
72
|
+
userRecallHints,
|
|
73
|
+
currentProject,
|
|
74
|
+
memoryStore,
|
|
75
|
+
engine,
|
|
76
|
+
ui,
|
|
77
|
+
turnState,
|
|
78
|
+
midTurnRecallHints,
|
|
79
|
+
syncCurrentPiSidecar,
|
|
80
|
+
autoNameSession,
|
|
81
|
+
});
|
|
82
|
+
throw err;
|
|
48
83
|
} finally {
|
|
84
|
+
modelIdleWatchdog.stop();
|
|
49
85
|
setModelCallKind("model");
|
|
50
86
|
}
|
|
51
87
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
ui.memoryHint?.({ source: "assistant", hints: recordedAssistantRecallHints });
|
|
56
|
-
|
|
57
|
-
engine.recordTurn({
|
|
58
|
-
userMessage: userMessage ?? prompt.slice(0, 300),
|
|
59
|
-
assistantMessage: turnState.draft,
|
|
88
|
+
finalizeTurn({
|
|
89
|
+
prompt,
|
|
90
|
+
userMessage,
|
|
60
91
|
userRecallHints,
|
|
61
|
-
|
|
92
|
+
currentProject,
|
|
93
|
+
memoryStore,
|
|
94
|
+
engine,
|
|
95
|
+
ui,
|
|
96
|
+
turnState,
|
|
97
|
+
midTurnRecallHints,
|
|
98
|
+
syncCurrentPiSidecar,
|
|
99
|
+
autoNameSession,
|
|
62
100
|
});
|
|
63
|
-
|
|
64
|
-
autoNameSession?.();
|
|
65
|
-
syncCurrentPiSidecar();
|
|
66
101
|
return { draft: turnState.draft };
|
|
67
102
|
} finally {
|
|
68
103
|
ui.turnEnd();
|
|
@@ -70,6 +105,144 @@ export async function runRunnerTurn({
|
|
|
70
105
|
}
|
|
71
106
|
}
|
|
72
107
|
|
|
108
|
+
async function promptWithModelIdleRetry({ session, prompt, promptOptions, resetBeforeAttempt, maxRetries, watchdog }) {
|
|
109
|
+
let attempt = 0;
|
|
110
|
+
while (true) {
|
|
111
|
+
attempt += 1;
|
|
112
|
+
if (resetBeforeAttempt) resetPiMessageHistory(session);
|
|
113
|
+
const messageHistorySnapshot = clonePiMessageHistory(session);
|
|
114
|
+
watchdog.startAttempt({ attempt, maxAttempts: maxRetries + 1 });
|
|
115
|
+
let idleTimedOut = false;
|
|
116
|
+
try {
|
|
117
|
+
await session.prompt(prompt, promptOptions);
|
|
118
|
+
idleTimedOut = watchdog.consumeTimedOut();
|
|
119
|
+
if (!idleTimedOut) {
|
|
120
|
+
if (attempt > 1) watchdog.reportRecovered({ attempt: attempt - 1 });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
idleTimedOut = watchdog.consumeTimedOut();
|
|
125
|
+
if (!idleTimedOut) throw err;
|
|
126
|
+
} finally {
|
|
127
|
+
watchdog.stop();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (attempt > maxRetries) {
|
|
131
|
+
const err = createModelStreamIdleTimeoutError(watchdog.timeoutMs);
|
|
132
|
+
watchdog.reportExhausted({ attempt, error: err });
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
restorePiMessageHistory(session, messageHistorySnapshot);
|
|
136
|
+
watchdog.reportRetry({ attempt, maxAttempts: maxRetries + 1 });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentPiSidecar, autoNameSession }) {
|
|
141
|
+
closeAssistantReply({ ui, state: turnState });
|
|
142
|
+
const assistantRecallHints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
|
|
143
|
+
engine.setPendingAssistantRecallHints?.(assistantRecallHints);
|
|
144
|
+
const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...assistantRecallHints]);
|
|
145
|
+
|
|
146
|
+
engine.recordTurn({
|
|
147
|
+
userMessage: userMessage ?? prompt.slice(0, 300),
|
|
148
|
+
assistantMessage: turnState.draft,
|
|
149
|
+
userRecallHints,
|
|
150
|
+
assistantRecallHints: recordedAssistantRecallHints,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
autoNameSession?.();
|
|
154
|
+
syncCurrentPiSidecar();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createModelStreamIdleTimeoutError(timeoutMs) {
|
|
158
|
+
const err = new Error(`Model stream idle for ${timeoutMs}ms`);
|
|
159
|
+
err.code = "MODEL_STREAM_IDLE_TIMEOUT";
|
|
160
|
+
return err;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createModelStreamIdleWatchdog({ session, ui, timeoutMs }) {
|
|
164
|
+
let timer = null;
|
|
165
|
+
let active = false;
|
|
166
|
+
let inTool = false;
|
|
167
|
+
let timedOut = false;
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
timeoutMs,
|
|
171
|
+
startAttempt() {
|
|
172
|
+
timedOut = false;
|
|
173
|
+
inTool = false;
|
|
174
|
+
active = true;
|
|
175
|
+
arm();
|
|
176
|
+
},
|
|
177
|
+
handleEvent(event) {
|
|
178
|
+
if (!active || timedOut) return;
|
|
179
|
+
if (event.type === "tool_execution_start") {
|
|
180
|
+
inTool = true;
|
|
181
|
+
clear();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (event.type === "tool_execution_end") {
|
|
185
|
+
inTool = false;
|
|
186
|
+
arm();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (event.type === "message_update") arm();
|
|
190
|
+
},
|
|
191
|
+
stop() {
|
|
192
|
+
active = false;
|
|
193
|
+
clear();
|
|
194
|
+
},
|
|
195
|
+
consumeTimedOut() {
|
|
196
|
+
const value = timedOut;
|
|
197
|
+
timedOut = false;
|
|
198
|
+
return value;
|
|
199
|
+
},
|
|
200
|
+
reportRetry({ attempt, maxAttempts }) {
|
|
201
|
+
ui.retryStart?.({
|
|
202
|
+
attempt,
|
|
203
|
+
maxAttempts,
|
|
204
|
+
delayMs: 0,
|
|
205
|
+
errorMessage: `Model stream idle for ${timeoutMs}ms; retrying request`,
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
reportRecovered({ attempt }) {
|
|
209
|
+
ui.retryEnd?.({ success: true, attempt });
|
|
210
|
+
},
|
|
211
|
+
reportExhausted({ attempt, error }) {
|
|
212
|
+
ui.retryEnd?.({ success: false, attempt, finalError: error.message });
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
function arm() {
|
|
217
|
+
clear();
|
|
218
|
+
if (!active || inTool || timeoutMs <= 0) return;
|
|
219
|
+
timer = setTimeout(() => {
|
|
220
|
+
timedOut = true;
|
|
221
|
+
active = false;
|
|
222
|
+
clear();
|
|
223
|
+
session.abortRetry?.();
|
|
224
|
+
session.abort();
|
|
225
|
+
}, timeoutMs);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function clear() {
|
|
229
|
+
if (!timer) return;
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
timer = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function clonePiMessageHistory(session) {
|
|
236
|
+
const messages = session?.agent?.state?.messages;
|
|
237
|
+
if (!Array.isArray(messages)) return null;
|
|
238
|
+
return JSON.parse(JSON.stringify(messages));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function restorePiMessageHistory(session, messages) {
|
|
242
|
+
if (!Array.isArray(session?.agent?.state?.messages) || !Array.isArray(messages)) return;
|
|
243
|
+
session.agent.state.messages = JSON.parse(JSON.stringify(messages));
|
|
244
|
+
}
|
|
245
|
+
|
|
73
246
|
function flushAssistantRecall({ memoryStore, engine, turnState, currentProject }) {
|
|
74
247
|
if (!memoryStore) return [];
|
|
75
248
|
const text = assistantRecallDeltaText(turnState);
|
|
Binary file
|
package/src/cli/repl-loop.mjs
CHANGED
|
@@ -18,16 +18,21 @@ export async function runSingleShotPrompt({
|
|
|
18
18
|
modeState = null,
|
|
19
19
|
}) {
|
|
20
20
|
memoryStore.beginTurn();
|
|
21
|
+
const carryoverAlreadyRendered = runner.engine.hasRenderedPendingAssistantRecallHints?.() ?? false;
|
|
22
|
+
const carryoverRecallHints = runner.engine.takePendingAssistantRecallHints?.() ?? [];
|
|
21
23
|
const userRecallHints = memoryStore.recallForUser(prompt, { currentProject, excludedIds: runner.engine.getRecentRecallMemoryIds?.() ?? [] });
|
|
22
24
|
const recallBlock = formatRecallHints("user", userRecallHints);
|
|
25
|
+
const carryoverRecallBlock = formatRecallHints("assistant", carryoverRecallHints);
|
|
23
26
|
const shellHints = formatShellHints(runner.shellRuntime);
|
|
24
27
|
const modePrompt = appendModeReminder(prompt, modeState?.get?.());
|
|
25
|
-
const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, shellHints);
|
|
28
|
+
const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, carryoverRecallBlock, shellHints);
|
|
26
29
|
ui.writeln(formatUserDisplayMessage(prompt));
|
|
27
30
|
ui.memoryHint?.({ source: "user", hints: userRecallHints });
|
|
31
|
+
if (carryoverRecallHints.length > 0 && !carryoverAlreadyRendered) ui.memoryHint?.({ source: "assistant", hints: carryoverRecallHints });
|
|
28
32
|
refreshStatusBar.startWorking?.();
|
|
29
33
|
try {
|
|
30
34
|
await runner.runTurn(fullPrompt, prompt, { userRecallHints, currentProject });
|
|
35
|
+
renderPendingAssistantRecallPreview({ runner, ui });
|
|
31
36
|
} finally {
|
|
32
37
|
refreshStatusBar.stopWorking?.();
|
|
33
38
|
memoryStore.endTurn();
|
|
@@ -87,7 +92,7 @@ export async function runInteractiveRepl({
|
|
|
87
92
|
});
|
|
88
93
|
if (slashResult.exit) break;
|
|
89
94
|
if (slashResult.handled) {
|
|
90
|
-
refreshStatusBar();
|
|
95
|
+
refreshStatusBar(contextTokenRefreshOptions(slashResult, runner));
|
|
91
96
|
continue;
|
|
92
97
|
}
|
|
93
98
|
|
|
@@ -111,6 +116,12 @@ export async function runInteractiveRepl({
|
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
export function contextTokenRefreshOptions(slashResult, runner) {
|
|
120
|
+
if (!slashResult?.refreshContextTokens) return undefined;
|
|
121
|
+
if (typeof runner.estimateContextTokens !== "function") return undefined;
|
|
122
|
+
return { contextTokens: runner.estimateContextTokens("") };
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
function handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand }) {
|
|
115
126
|
const inlineShell = parseInlineShellInput(trimmed, lastInlineShellCommand);
|
|
116
127
|
if (inlineShell.type === "error") {
|
|
@@ -126,17 +137,22 @@ function handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand }) {
|
|
|
126
137
|
|
|
127
138
|
async function runReplTurn({ prompt, args, runner, memoryStore, currentProject, ui, refreshStatusBar, setTurnRunning, modeState = null }) {
|
|
128
139
|
memoryStore.beginTurn();
|
|
140
|
+
const carryoverAlreadyRendered = runner.engine.hasRenderedPendingAssistantRecallHints?.() ?? false;
|
|
141
|
+
const carryoverRecallHints = runner.engine.takePendingAssistantRecallHints?.() ?? [];
|
|
129
142
|
const userRecallHints = memoryStore.recallForUser(prompt, { currentProject, excludedIds: runner.engine.getRecentRecallMemoryIds?.() ?? [] });
|
|
130
143
|
const recallBlock = formatRecallHints("user", userRecallHints);
|
|
144
|
+
const carryoverRecallBlock = formatRecallHints("assistant", carryoverRecallHints);
|
|
131
145
|
const shellHints = formatShellHints(runner.shellRuntime);
|
|
132
146
|
const modePrompt = appendModeReminder(prompt, modeState?.get?.());
|
|
133
|
-
const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, shellHints);
|
|
147
|
+
const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, carryoverRecallBlock, shellHints);
|
|
134
148
|
try {
|
|
135
149
|
ui.writeln(formatUserDisplayMessage(prompt));
|
|
136
150
|
ui.memoryHint?.({ source: "user", hints: userRecallHints });
|
|
151
|
+
if (carryoverRecallHints.length > 0 && !carryoverAlreadyRendered) ui.memoryHint?.({ source: "assistant", hints: carryoverRecallHints });
|
|
137
152
|
setTurnRunning(true);
|
|
138
153
|
refreshStatusBar.startWorking?.();
|
|
139
154
|
await runner.runTurn(fullPrompt, prompt, { userRecallHints, currentProject });
|
|
155
|
+
renderPendingAssistantRecallPreview({ runner, ui });
|
|
140
156
|
ui.writeln("");
|
|
141
157
|
} catch (err) {
|
|
142
158
|
ui.writeln(`Error: ${err.message}`);
|
|
@@ -155,3 +171,11 @@ export function formatUserDisplayMessage(prompt) {
|
|
|
155
171
|
function appendPromptBlocks(prompt, ...blocks) {
|
|
156
172
|
return [prompt, ...blocks.filter(Boolean)].join("\n\n");
|
|
157
173
|
}
|
|
174
|
+
|
|
175
|
+
function renderPendingAssistantRecallPreview({ runner, ui }) {
|
|
176
|
+
if (runner.engine.hasRenderedPendingAssistantRecallHints?.()) return;
|
|
177
|
+
const hints = runner.engine.peekPendingAssistantRecallHints?.() ?? [];
|
|
178
|
+
if (hints.length === 0) return;
|
|
179
|
+
ui.memoryHint?.({ source: "assistant", hints });
|
|
180
|
+
runner.engine.markPendingAssistantRecallHintsRendered?.();
|
|
181
|
+
}
|
|
@@ -40,11 +40,13 @@ export async function handleSlashCommand(trimmed, {
|
|
|
40
40
|
ui.writeln("Error: pi runtime host is not enabled");
|
|
41
41
|
return { handled: true };
|
|
42
42
|
}
|
|
43
|
+
let refreshContextTokens = false;
|
|
43
44
|
try {
|
|
44
45
|
const result = await runner.startNewSession();
|
|
45
46
|
if (result?.cancelled) {
|
|
46
47
|
ui.writeln("New session cancelled");
|
|
47
48
|
} else {
|
|
49
|
+
refreshContextTokens = true;
|
|
48
50
|
ui.clearOutput?.();
|
|
49
51
|
const bannerLines = typeof renderStartupBanner === "function" ? renderStartupBanner() : [];
|
|
50
52
|
if (bannerLines.length > 0) {
|
|
@@ -56,7 +58,7 @@ export async function handleSlashCommand(trimmed, {
|
|
|
56
58
|
} catch (err) {
|
|
57
59
|
ui.writeln(`Error: ${err.message}`);
|
|
58
60
|
}
|
|
59
|
-
return { handled: true };
|
|
61
|
+
return { handled: true, refreshContextTokens };
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
if (trimmed === "/help") {
|
|
@@ -120,6 +122,12 @@ export async function handleSlashCommand(trimmed, {
|
|
|
120
122
|
return { handled: true };
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
if (trimmed === "/notify") {
|
|
126
|
+
const result = await runner.notifyTest?.();
|
|
127
|
+
ui.writeln(formatNotificationResult(result));
|
|
128
|
+
return { handled: true };
|
|
129
|
+
}
|
|
130
|
+
|
|
123
131
|
const shellCommand = parseShellCommand(trimmed);
|
|
124
132
|
if (shellCommand.type !== "none") {
|
|
125
133
|
for (const line of handleShellCommand(shellCommand, { shellRuntime: runner.shellRuntime })) ui.writeln(line);
|
|
@@ -174,3 +182,11 @@ export async function handleSlashCommand(trimmed, {
|
|
|
174
182
|
|
|
175
183
|
return { handled: false };
|
|
176
184
|
}
|
|
185
|
+
|
|
186
|
+
function formatNotificationResult(result) {
|
|
187
|
+
if (!result) return "notification: unavailable";
|
|
188
|
+
const channels = (result.results ?? [])
|
|
189
|
+
.map((entry) => `${entry.channel}:${entry.ok ? "ok" : entry.reason ?? "failed"}`)
|
|
190
|
+
.join(", ");
|
|
191
|
+
return `notification: ${result.ok ? "ok" : result.reason ?? "failed"}${channels ? ` (${channels})` : ""}`;
|
|
192
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { parseMouseEvent } from "./mouse-tracking.mjs";
|
|
2
2
|
import { brightBlack } from "../ui-theme.mjs";
|
|
3
3
|
|
|
4
|
+
const OUTPUT_WHEEL_SCROLL_LINES = 3;
|
|
5
|
+
|
|
4
6
|
export function createMouseSelectionController({
|
|
5
7
|
terminal,
|
|
6
8
|
output,
|
|
@@ -49,7 +51,7 @@ export function createMouseSelectionController({
|
|
|
49
51
|
if (shellDrawer.isVisible?.() && mouse.col > Math.floor((terminal.columns || 80) * 0.64)) {
|
|
50
52
|
shellDrawerControls.scroll(mouse.delta);
|
|
51
53
|
} else {
|
|
52
|
-
output.scroll(mouse.delta);
|
|
54
|
+
output.scroll(mouse.delta, { step: OUTPUT_WHEEL_SCROLL_LINES });
|
|
53
55
|
}
|
|
54
56
|
requestRender();
|
|
55
57
|
return { consume: true };
|
|
@@ -167,6 +167,12 @@ function appendWrappedRuns(lines, runs, width, indent = 0, firstPrefix = null) {
|
|
|
167
167
|
const maxWidth = Math.max(1, width);
|
|
168
168
|
for (const run of runs) {
|
|
169
169
|
for (const ch of run.text) {
|
|
170
|
+
if (ch === "\n") {
|
|
171
|
+
lines.push(current);
|
|
172
|
+
current = restPrefix;
|
|
173
|
+
currentWidth = visibleWidth(restPrefix);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
170
176
|
const charWidth = visibleWidth(ch);
|
|
171
177
|
if (currentWidth + charWidth > maxWidth && currentWidth > visibleWidth(stripAnsi(prefix))) {
|
|
172
178
|
lines.push(current);
|
|
@@ -17,11 +17,11 @@ export class OutputScrollState {
|
|
|
17
17
|
this.totalLines = Math.max(0, Math.trunc(total));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
scroll(delta) {
|
|
20
|
+
scroll(delta, { step = this.getStep() } = {}) {
|
|
21
21
|
const total = this.totalLines;
|
|
22
22
|
const win = this._windowHeight();
|
|
23
23
|
const maxOffset = this.getMaxOffset();
|
|
24
|
-
|
|
24
|
+
step = Math.max(1, Math.trunc(step));
|
|
25
25
|
const currentStart = this.anchorStart ?? Math.max(0, total - this.offset - win);
|
|
26
26
|
const maxStart = Math.max(0, total - win);
|
|
27
27
|
const nextStart = clamp(currentStart + (delta < 0 ? -step : step), 0, maxStart);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { R } from "../ui-theme.mjs";
|
|
3
|
+
|
|
4
|
+
export function appendTextLines(lines, textLines, width) {
|
|
5
|
+
for (const line of textLines) {
|
|
6
|
+
for (const part of String(line ?? "").split(/\r?\n/)) {
|
|
7
|
+
for (const wrapped of wrapLine(part, width)) lines.push(wrapped);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function wrapLine(text, maxWidth) {
|
|
13
|
+
if (maxWidth <= 0) return [""];
|
|
14
|
+
const result = [];
|
|
15
|
+
let cur = "";
|
|
16
|
+
let curW = 0;
|
|
17
|
+
let activeSgr = "";
|
|
18
|
+
for (let i = 0; i < text.length;) {
|
|
19
|
+
if (text[i] === "\x1b") {
|
|
20
|
+
const match = text.slice(i).match(/^\x1b\[[0-?]*[ -/]*[@-~]/);
|
|
21
|
+
if (match) {
|
|
22
|
+
const seq = match[0];
|
|
23
|
+
cur += seq;
|
|
24
|
+
activeSgr = updateActiveSgr(activeSgr, seq);
|
|
25
|
+
i += seq.length;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const ch = text[i];
|
|
30
|
+
const w = visibleWidth(ch);
|
|
31
|
+
if (curW + w > maxWidth) {
|
|
32
|
+
result.push(activeSgr ? `${cur}${R}` : cur);
|
|
33
|
+
cur = activeSgr + ch;
|
|
34
|
+
curW = w;
|
|
35
|
+
} else {
|
|
36
|
+
cur += ch;
|
|
37
|
+
curW += w;
|
|
38
|
+
}
|
|
39
|
+
i += 1;
|
|
40
|
+
}
|
|
41
|
+
if (cur) result.push(cur);
|
|
42
|
+
return result.length > 0 ? result : [""];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function updateActiveSgr(activeSgr, seq) {
|
|
46
|
+
if (!seq.endsWith("m")) return activeSgr;
|
|
47
|
+
const body = seq.slice(2, -1);
|
|
48
|
+
if (body === "" || body.split(";").includes("0")) return "";
|
|
49
|
+
return seq;
|
|
50
|
+
}
|