micro-models-agent 0.63.0 → 1.1.0
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/CHANGELOG.md +174 -0
- package/dist/cli/cache-line.js +30 -0
- package/dist/cli/command-suggest.js +38 -0
- package/dist/cli/commands.js +285 -60
- package/dist/cli/completer.js +16 -16
- package/dist/cli/json-payload.js +32 -0
- package/dist/cli/main.js +165 -77
- package/dist/cli/plugin-commands.js +5 -4
- package/dist/cli/relaunch.js +37 -0
- package/dist/cli/repl-commands.js +441 -307
- package/dist/cli/repl.js +360 -83
- package/dist/cli/run-result.js +12 -6
- package/dist/cli/security-commands.js +64 -60
- package/dist/cli/setup-order.js +57 -0
- package/dist/cli/setup-prompt.js +49 -0
- package/dist/cli/setup.js +52 -48
- package/dist/config/budget.js +48 -0
- package/dist/config/config.js +132 -70
- package/dist/config/defaults.js +37 -11
- package/dist/config/domains.js +9 -50
- package/dist/config/utils.js +56 -0
- package/dist/core/agent/audit-gate.js +49 -0
- package/dist/core/agent/compaction.js +89 -0
- package/dist/core/agent/constants.js +61 -0
- package/dist/core/agent/context-renderer.js +40 -0
- package/dist/core/agent/hallucination-gate.js +87 -0
- package/dist/core/agent/loop-state.js +53 -0
- package/dist/core/agent/prefix-monitor.js +101 -0
- package/dist/core/agent/reasoning-resolver.js +56 -0
- package/dist/core/agent/token-tracker.js +96 -0
- package/dist/core/agent/tool-batch.js +237 -0
- package/dist/core/agent/tool-output.js +62 -0
- package/dist/core/agent-moe.js +214 -69
- package/dist/core/agent.js +506 -546
- package/dist/core/bootstrap.js +297 -98
- package/dist/core/crash-handler.js +2 -1
- package/dist/core/prompt-builder.js +3 -0
- package/dist/core/prompt-overflow.js +307 -0
- package/dist/core/session-logger.js +34 -2
- package/dist/i18n/en.json +8 -4
- package/dist/i18n/ru.json +8 -4
- package/dist/index.js +5 -1
- package/dist/llm/cache-usage.js +76 -0
- package/dist/llm/image-utils.js +20 -16
- package/dist/llm/llm-errors.js +41 -0
- package/dist/llm/model-loader.js +30 -0
- package/dist/llm/openai-compat.js +287 -101
- package/dist/llm/orchestrator.js +140 -68
- package/dist/llm/provider-budget.js +68 -0
- package/dist/llm/provider.js +0 -1
- package/dist/llm/stream-state.js +26 -0
- package/dist/llm/token-counter.js +28 -0
- package/dist/logger/app-logger.js +12 -15
- package/dist/main.js +1755 -841
- package/dist/migration/detect.js +3 -1
- package/dist/modules/browser/actions.js +0 -3
- package/dist/modules/browser/bridge-client.js +2 -0
- package/dist/modules/browser/bridge-server.mjs +37 -4
- package/dist/modules/browser/driver.js +46 -4
- package/dist/modules/certification/cli.js +85 -42
- package/dist/modules/certification/loader.js +15 -1
- package/dist/modules/certification/manifest.js +126 -15
- package/dist/modules/certification/runner.js +4 -26
- package/dist/modules/certification/scenarios.js +184 -5
- package/dist/modules/certification/syntax-scenarios.js +51 -0
- package/dist/modules/context/chunk-query.js +25 -5
- package/dist/modules/context/fact-extractor.js +6 -2
- package/dist/modules/context/manager.js +23 -7
- package/dist/modules/execution/audit-runners.js +7 -1
- package/dist/modules/execution/auditor.js +3 -3
- package/dist/modules/execution/execution-plugin.js +22 -15
- package/dist/modules/execution/input-from.js +46 -0
- package/dist/modules/execution/module.js +107 -18
- package/dist/modules/execution/moe-executor.js +166 -54
- package/dist/modules/execution/plan-actions.js +524 -0
- package/dist/modules/execution/plan-steps.js +23 -0
- package/dist/modules/execution/plan-store.js +15 -3
- package/dist/modules/execution/plan-tool.js +6 -488
- package/dist/modules/execution/plan-validator.js +24 -0
- package/dist/modules/execution/stuck-detector.js +3 -18
- package/dist/modules/execution/tracker.js +14 -5
- package/dist/modules/execution/transient-error.js +30 -0
- package/dist/modules/execution/verifier.js +94 -7
- package/dist/modules/execution/windows-commands.js +11 -0
- package/dist/modules/hallucination/confidence.js +36 -23
- package/dist/modules/hallucination/consistency.js +3 -0
- package/dist/modules/hallucination/detector.js +8 -3
- package/dist/modules/hallucination/factual.js +26 -7
- package/dist/modules/hallucination/llm-judge.js +12 -2
- package/dist/modules/indexer/map-command.js +35 -0
- package/dist/modules/indexer/map-select.js +87 -0
- package/dist/modules/indexer/module.js +34 -22
- package/dist/modules/indexer/symbols.js +189 -0
- package/dist/modules/indexer/walker.js +96 -42
- package/dist/modules/lsp/check-tool.js +2 -1
- package/dist/modules/lsp/client.js +49 -32
- package/dist/modules/lsp/config.js +55 -2
- package/dist/modules/lsp/module.js +38 -5
- package/dist/modules/lsp/probe.js +4 -3
- package/dist/modules/lsp/project-root.js +41 -1
- package/dist/modules/lsp/startup-check.js +12 -4
- package/dist/modules/mcp/client.js +153 -104
- package/dist/modules/mcp/module.js +165 -41
- package/dist/modules/memory/module.js +4 -3
- package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
- package/dist/modules/plugins/manager.js +47 -84
- package/dist/modules/pricing/index.js +17 -7
- package/dist/modules/pricing/prices.js +30 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +56 -0
- package/dist/modules/processes/registry.js +2 -54
- package/dist/modules/providers/cache.js +23 -0
- package/dist/modules/providers/factory.js +28 -0
- package/dist/modules/providers/fallback.js +7 -5
- package/dist/modules/providers/health.js +2 -1
- package/dist/modules/providers/index.js +1 -0
- package/dist/modules/providers/manager.js +17 -2
- package/dist/modules/providers/presets.js +79 -6
- package/dist/modules/reasoning/policy.js +40 -0
- package/dist/modules/reasoning/probe.js +111 -0
- package/dist/modules/security/audit-notifier.js +42 -27
- package/dist/modules/security/command-validator.js +25 -20
- package/dist/modules/security/encryption.js +6 -12
- package/dist/modules/security/network-validator.js +76 -5
- package/dist/modules/security/path-validator.js +77 -34
- package/dist/modules/security/rate-limiter.js +11 -0
- package/dist/modules/security/security-policies.js +1 -1
- package/dist/modules/security/session-encryption.js +13 -2
- package/dist/modules/security/session-isolation.js +2 -9
- package/dist/modules/session/manager.js +11 -0
- package/dist/modules/session/module.js +11 -3
- package/dist/modules/session/store.js +41 -5
- package/dist/modules/skills/loader.js +7 -1
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/changelog-reader.js +94 -0
- package/dist/modules/updater/dev-detect.js +17 -0
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +14 -3
- package/dist/output/bus.js +32 -0
- package/dist/output/channel.js +233 -0
- package/dist/output/format.js +14 -0
- package/dist/output/index.js +7 -0
- package/dist/output/json-sink.js +22 -0
- package/dist/output/machine.js +8 -0
- package/dist/output/session-sink.js +27 -0
- package/dist/output/types.js +1 -0
- package/dist/tools/approve.js +6 -2
- package/dist/tools/attach-image.js +11 -11
- package/dist/tools/auto-fixer.js +198 -0
- package/dist/tools/bash.js +142 -89
- package/dist/tools/chunk-query.js +10 -6
- package/dist/tools/download-file.js +1 -1
- package/dist/tools/edit-file.js +20 -2
- package/dist/tools/executor.js +54 -9
- package/dist/tools/glob-tool.js +7 -0
- package/dist/tools/grep-tool.js +15 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/list-dir.js +3 -1
- package/dist/tools/load-skill.js +2 -1
- package/dist/tools/mcp-call.js +1 -1
- package/dist/tools/move-file.js +5 -4
- package/dist/tools/path-utils.js +7 -0
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/prompt-io.js +28 -0
- package/dist/tools/question.js +12 -12
- package/dist/tools/scope-request.js +91 -0
- package/dist/tools/session-info.js +44 -0
- package/dist/tools/set-thinking.js +71 -0
- package/dist/tools/subagent.js +50 -9
- package/dist/tools/syntax-validator.js +177 -0
- package/dist/tools/user-input.js +16 -9
- package/dist/tools/write-file.js +17 -1
- package/dist/ui/diff.js +10 -0
- package/dist/ui/line-editor.js +179 -26
- package/dist/ui/line-math.js +20 -3
- package/dist/ui/md-formatter.js +100 -10
- package/dist/ui/output.js +5 -4
- package/dist/ui/plan-view.js +2 -7
- package/dist/ui/renderer.js +89 -85
- package/dist/ui/spinner.js +14 -4
- package/dist/utils/error.js +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/retry.js +17 -0
- package/dist/utils/sleep.js +23 -0
- package/dist/utils/truncate.js +9 -0
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as readline from "readline";
|
|
2
2
|
import { pc } from "../ui/colors";
|
|
3
3
|
import { LineEditor } from "../ui/line-editor";
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { Completer, SlashCommandProvider, SessionNameProvider, SubcommandProvider, SkillNameProvider, ProviderArgProvider, ModelArgProvider, } from "./completer";
|
|
8
8
|
import { Renderer } from "../ui/renderer";
|
|
@@ -12,11 +12,15 @@ import { registerAllCommands } from "./repl-commands";
|
|
|
12
12
|
import { probeLspServers } from "../modules/lsp/probe";
|
|
13
13
|
import { FileOnlyLogger } from "../modules/lsp/client";
|
|
14
14
|
import { DEFAULT_LSP_CONFIG } from "../modules/lsp/config";
|
|
15
|
+
import { errMsg } from "../utils";
|
|
15
16
|
import { SessionLogger } from "../core/session-logger";
|
|
16
17
|
import { formatPlanChecklist, stepContextForTool } from "../ui/plan-view";
|
|
17
18
|
import { formatCost } from "../modules/pricing/prices";
|
|
19
|
+
import { formatCacheLine } from "./cache-line";
|
|
18
20
|
import { probeProviders, targetsFromProviders } from "../modules/providers/health";
|
|
19
|
-
import {
|
|
21
|
+
import { formatWarning } from "../ui/md-formatter";
|
|
22
|
+
import { defaultOutputBus, getDefaultChannel, SessionOutputSink, writeNonTtyLine, } from "../output";
|
|
23
|
+
import { createPromptIO } from "../tools/prompt-io";
|
|
20
24
|
function formatContextBar(used, limit, compactions, quality) {
|
|
21
25
|
const pct = Math.min(100, Math.round((used / limit) * 100));
|
|
22
26
|
const barLen = 20;
|
|
@@ -53,6 +57,7 @@ export function createHostBridge(opts) {
|
|
|
53
57
|
if (opts.isBusy())
|
|
54
58
|
opts.interruptRun();
|
|
55
59
|
},
|
|
60
|
+
subscribeOutput: (fn) => defaultOutputBus.subscribe(fn),
|
|
56
61
|
};
|
|
57
62
|
}
|
|
58
63
|
export class Repl {
|
|
@@ -73,6 +78,17 @@ export class Repl {
|
|
|
73
78
|
exitOnClose = false;
|
|
74
79
|
/** Owner of plan state (bootstrap). Null in tests — plan UI degrades. */
|
|
75
80
|
execModule;
|
|
81
|
+
/**
|
|
82
|
+
* True only once the input prompt frame is actually on screen. The channel's
|
|
83
|
+
* prompt sink stays inactive until then, so the startup banner prints plainly
|
|
84
|
+
* instead of erasing/redrawing a frame that was never drawn.
|
|
85
|
+
*/
|
|
86
|
+
promptPresented = false;
|
|
87
|
+
/**
|
|
88
|
+
* Set by `runAgent` so the slash-command path knows the run already restored
|
|
89
|
+
* the prompt and must not present it a second time (e.g. `/run`).
|
|
90
|
+
*/
|
|
91
|
+
runHandledPrompt = false;
|
|
76
92
|
rl;
|
|
77
93
|
agent;
|
|
78
94
|
config;
|
|
@@ -81,8 +97,25 @@ export class Repl {
|
|
|
81
97
|
pluginManager;
|
|
82
98
|
logger;
|
|
83
99
|
slog;
|
|
100
|
+
/**
|
|
101
|
+
* Persists `data.persist === true` output events to the active session.
|
|
102
|
+
* Inert until a SessionManager exists; re-bound by `/reload`.
|
|
103
|
+
*/
|
|
104
|
+
sessionOutput;
|
|
84
105
|
envReport;
|
|
85
|
-
|
|
106
|
+
/** Background probe of the actually loaded model context (bootstrap). */
|
|
107
|
+
contextProbe;
|
|
108
|
+
/**
|
|
109
|
+
* Single terminal writer for this REPL. Public (not private) because
|
|
110
|
+
* `ReplContext` exposes it to slash commands as `ctx.output`; a private
|
|
111
|
+
* field cannot satisfy a public interface property.
|
|
112
|
+
*/
|
|
113
|
+
output;
|
|
114
|
+
/** Bridge that lets the channel draw above the live input frame. */
|
|
115
|
+
promptSink;
|
|
116
|
+
/** Terminal-aware PromptIO installed on the agent's tool executor. */
|
|
117
|
+
promptIO;
|
|
118
|
+
constructor(agent, config, sessionManager, skillsModule, pluginManager, configDir, baseDir, noAgentsMd, logger, exitOnClose, envReport, historyPath, execModule, contextProbe, output) {
|
|
86
119
|
this.agent = agent;
|
|
87
120
|
this.config = config;
|
|
88
121
|
this.exitOnClose = exitOnClose === true;
|
|
@@ -91,59 +124,137 @@ export class Repl {
|
|
|
91
124
|
this.pluginManager = pluginManager;
|
|
92
125
|
this.logger = logger;
|
|
93
126
|
this.envReport = envReport;
|
|
127
|
+
this.contextProbe = contextProbe;
|
|
94
128
|
this.execModule = execModule;
|
|
95
129
|
this.slog = new SessionLogger(sessionManager, logger);
|
|
130
|
+
this.bindSessionOutput();
|
|
96
131
|
this.configDir = configDir || join(homedir(), ".mma");
|
|
97
132
|
this.baseDir = baseDir || process.cwd();
|
|
98
133
|
this.noAgentsMd = noAgentsMd === true;
|
|
99
134
|
this.historyPath = historyPath ?? join(homedir(), ".mma", "repl-history");
|
|
100
135
|
this.loadHistory();
|
|
101
|
-
|
|
102
|
-
|
|
136
|
+
const editorCompleter = (line) => {
|
|
137
|
+
const [matches, partial] = this.completer.complete(line);
|
|
138
|
+
if (matches.length > 0)
|
|
139
|
+
return [matches, partial];
|
|
140
|
+
return [[], line];
|
|
141
|
+
};
|
|
142
|
+
if (process.stdin.isTTY) {
|
|
143
|
+
this.rl = new LineEditor({
|
|
103
144
|
input: process.stdin,
|
|
104
145
|
output: process.stdout,
|
|
105
|
-
prompt:
|
|
146
|
+
prompt: this.inputPrompt(),
|
|
106
147
|
history: this.history,
|
|
107
148
|
historySize: this.maxHistory,
|
|
108
|
-
completer:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
},
|
|
114
|
-
})
|
|
115
|
-
: readline.createInterface({
|
|
149
|
+
completer: editorCompleter,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
this.rl = readline.createInterface({
|
|
116
154
|
input: process.stdin,
|
|
117
155
|
output: process.stdout,
|
|
118
|
-
prompt:
|
|
156
|
+
prompt: this.inputPrompt(),
|
|
119
157
|
history: this.history,
|
|
120
158
|
historySize: this.maxHistory,
|
|
121
159
|
tabSize: 2,
|
|
122
|
-
completer:
|
|
123
|
-
const [matches, partial] = this.completer.complete(line);
|
|
124
|
-
if (matches.length > 0)
|
|
125
|
-
return [matches, partial];
|
|
126
|
-
return [[], line];
|
|
127
|
-
},
|
|
160
|
+
completer: editorCompleter,
|
|
128
161
|
});
|
|
162
|
+
}
|
|
163
|
+
this.output = output ?? getDefaultChannel();
|
|
164
|
+
this.promptSink = {
|
|
165
|
+
isActive: () => this.rl instanceof LineEditor &&
|
|
166
|
+
this.promptPresented &&
|
|
167
|
+
!this.agentRunning &&
|
|
168
|
+
!this.inputLocked,
|
|
169
|
+
printAbove: (text) => {
|
|
170
|
+
if (this.rl instanceof LineEditor) {
|
|
171
|
+
this.rl.printAbove(text);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Non-TTY fallback (readline / piped stdin): no input frame to redraw
|
|
175
|
+
// above, so write directly through the sanctioned machine-output helper
|
|
176
|
+
// — there is no frame for `printAbove` here.
|
|
177
|
+
writeNonTtyLine(text);
|
|
178
|
+
},
|
|
179
|
+
clearScreen: () => {
|
|
180
|
+
if (this.rl instanceof LineEditor)
|
|
181
|
+
this.rl.clearScreen();
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
this.output.attachPrompt(this.promptSink);
|
|
185
|
+
// Terminal-aware prompt surface for interactive tools (question/approve).
|
|
186
|
+
// The REPL owns stdin and is in `streaming` mode during a run, so tools
|
|
187
|
+
// must borrow the editor for a question instead of opening a competing
|
|
188
|
+
// readline that would fight the LineEditor's raw mode.
|
|
189
|
+
this.promptIO = createPromptIO({
|
|
190
|
+
ask: (prompt) => this.askViaEditor(prompt),
|
|
191
|
+
print: (line) => this.output.writeLine(line),
|
|
192
|
+
});
|
|
193
|
+
this.agent.setPromptIO?.(this.promptIO);
|
|
129
194
|
registerAllCommands(this);
|
|
130
195
|
this.setupCompleter();
|
|
131
196
|
this.setupListeners();
|
|
132
197
|
// Expose the agent to plugins (web UI etc.) through a narrow bridge.
|
|
133
198
|
if (pluginManager) {
|
|
134
|
-
|
|
135
|
-
isBusy: () => this.agentRunning,
|
|
136
|
-
addPendingImage: (url) => {
|
|
137
|
-
this.agent.contextManager?.addPendingImage?.({
|
|
138
|
-
type: "image_url",
|
|
139
|
-
image_url: { url },
|
|
140
|
-
});
|
|
141
|
-
},
|
|
142
|
-
runAgent: (text) => this.runAgent(text),
|
|
143
|
-
interruptRun: () => this.agent.shutdown(),
|
|
144
|
-
}));
|
|
199
|
+
this.attachHostBridge(pluginManager);
|
|
145
200
|
}
|
|
146
201
|
}
|
|
202
|
+
/** (Re)attach the plugin HostBridge. Called at construction and after
|
|
203
|
+
* /reload replaces the PluginManager — without this, a reloaded web-ui
|
|
204
|
+
* plugin silently loses the ability to submit messages. */
|
|
205
|
+
attachHostBridge(pm) {
|
|
206
|
+
const manager = pm ?? this.pluginManager;
|
|
207
|
+
if (!manager)
|
|
208
|
+
return;
|
|
209
|
+
manager.setHostBridge(createHostBridge({
|
|
210
|
+
isBusy: () => this.agentRunning,
|
|
211
|
+
addPendingImage: (url) => {
|
|
212
|
+
this.agent.contextManager?.addPendingImage?.({
|
|
213
|
+
type: "image_url",
|
|
214
|
+
image_url: { url },
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
runAgent: (text) => this.runAgent(text),
|
|
218
|
+
interruptRun: () => this.agent.shutdown(),
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
/** (Re)bind the session output sink after construction or `/reload`. */
|
|
222
|
+
bindSessionOutput() {
|
|
223
|
+
this.sessionOutput?.dispose();
|
|
224
|
+
this.sessionOutput = this.sessionManager
|
|
225
|
+
? new SessionOutputSink(defaultOutputBus, this.sessionManager)
|
|
226
|
+
: undefined;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Перезагрузка агента и модулей (команда /reload). Живёт здесь, а не в
|
|
230
|
+
* repl-commands, потому что меняет mutable-поля класса — команда через
|
|
231
|
+
* интерфейс только вызывает `ctx.reload()`, не ломая readonly-контракт.
|
|
232
|
+
*/
|
|
233
|
+
async reload() {
|
|
234
|
+
const { loadConfig } = await import("../config/config");
|
|
235
|
+
const { bootstrap } = await import("../core/bootstrap");
|
|
236
|
+
this.agent.shutdown();
|
|
237
|
+
const projectConfigPath = join(this.baseDir, ".mmrc");
|
|
238
|
+
const { config: freshConfig } = loadConfig({
|
|
239
|
+
configDir: this.configDir,
|
|
240
|
+
projectConfigPath,
|
|
241
|
+
});
|
|
242
|
+
Object.assign(this.config, freshConfig);
|
|
243
|
+
const result = await bootstrap(this.configDir, this.baseDir, false, false);
|
|
244
|
+
this.agent = result.agent;
|
|
245
|
+
// Re-install the prompt surface on the freshly bootstrapped agent — the
|
|
246
|
+
// REPL's `askViaEditor` closure still belongs to this instance.
|
|
247
|
+
this.agent.setPromptIO?.(this.promptIO);
|
|
248
|
+
this.sessionManager = result.sessionManager;
|
|
249
|
+
this.skillsModule = result.skillsModule;
|
|
250
|
+
this.pluginManager = result.pluginManager;
|
|
251
|
+
this.execModule = result.execModule;
|
|
252
|
+
this.bindSessionOutput();
|
|
253
|
+
// Fresh PluginManager has no bridge — re-register it, otherwise plugins
|
|
254
|
+
// like web-ui silently lose control of the agent.
|
|
255
|
+
this.attachHostBridge(result.pluginManager);
|
|
256
|
+
this.setupCompleter();
|
|
257
|
+
}
|
|
147
258
|
loadHistory() {
|
|
148
259
|
if (existsSync(this.historyPath)) {
|
|
149
260
|
try {
|
|
@@ -156,10 +267,21 @@ export class Repl {
|
|
|
156
267
|
}
|
|
157
268
|
}
|
|
158
269
|
saveHistory() {
|
|
159
|
-
|
|
160
|
-
|
|
270
|
+
try {
|
|
271
|
+
// The history dir may be gone (e.g. a temp dir removed during test or
|
|
272
|
+
// session teardown). Recreate it and never let a save throw.
|
|
273
|
+
mkdirSync(dirname(this.historyPath), { recursive: true });
|
|
274
|
+
const allHistory = this.history.slice(-this.maxHistory);
|
|
275
|
+
writeFileSync(this.historyPath, allHistory.join("\n"), "utf-8");
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
this.logger?.debug(`Failed to save REPL history: ${e.message}`);
|
|
279
|
+
}
|
|
161
280
|
}
|
|
162
281
|
setupCompleter() {
|
|
282
|
+
// Rebuild from scratch: /reload calls this again after bootstrap, and
|
|
283
|
+
// re-registering without a reset would duplicate every provider.
|
|
284
|
+
this.completer.reset();
|
|
163
285
|
const slashCommands = Array.from(this.commands.keys());
|
|
164
286
|
this.completer.registerProvider(new SlashCommandProvider(slashCommands));
|
|
165
287
|
if (this.sessionManager) {
|
|
@@ -170,7 +292,7 @@ export class Repl {
|
|
|
170
292
|
this.completer.registerProvider(new SkillNameProvider(this.skillsModule));
|
|
171
293
|
}
|
|
172
294
|
// Subcommand + argument completion for provider/model management.
|
|
173
|
-
this.completer.registerProvider(new SubcommandProvider("provider", ["list", "use"]));
|
|
295
|
+
this.completer.registerProvider(new SubcommandProvider("provider", ["list", "use", "add"]));
|
|
174
296
|
this.completer.registerProvider(new SubcommandProvider("model", ["list", "use"]));
|
|
175
297
|
const entryLabels = this.config.provider?.entries?.map((e) => e.label || e.type) ?? [];
|
|
176
298
|
if (entryLabels.length > 0) {
|
|
@@ -232,18 +354,15 @@ export class Repl {
|
|
|
232
354
|
if (fullInput) {
|
|
233
355
|
if (fullInput.startsWith("/")) {
|
|
234
356
|
await this.executeCommand(fullInput);
|
|
357
|
+
this.afterCommand();
|
|
235
358
|
}
|
|
236
359
|
else {
|
|
237
360
|
await this.runAgent(fullInput);
|
|
238
361
|
}
|
|
239
362
|
}
|
|
240
|
-
if (this.running) {
|
|
241
|
-
this.rl.setPrompt(pc.cyan(t("repl.you")));
|
|
242
|
-
this.rl.prompt();
|
|
243
|
-
}
|
|
244
363
|
}
|
|
245
364
|
else {
|
|
246
|
-
this.rl.setPrompt(
|
|
365
|
+
this.rl.setPrompt(this.inputPrompt(true));
|
|
247
366
|
this.rl.prompt();
|
|
248
367
|
}
|
|
249
368
|
return;
|
|
@@ -251,25 +370,22 @@ export class Repl {
|
|
|
251
370
|
if (this.isMultiLineInput(trimmed)) {
|
|
252
371
|
inMultiLine = true;
|
|
253
372
|
multiLineBuffer = trimmed;
|
|
254
|
-
this.rl.setPrompt(
|
|
373
|
+
this.rl.setPrompt(this.inputPrompt(true));
|
|
255
374
|
this.rl.prompt();
|
|
256
375
|
return;
|
|
257
376
|
}
|
|
258
377
|
if (!trimmed) {
|
|
259
|
-
this.rl.setPrompt(
|
|
378
|
+
this.rl.setPrompt(this.inputPrompt());
|
|
260
379
|
this.rl.prompt();
|
|
261
380
|
return;
|
|
262
381
|
}
|
|
263
382
|
if (trimmed.startsWith("/")) {
|
|
264
383
|
await this.executeCommand(trimmed);
|
|
384
|
+
this.afterCommand();
|
|
265
385
|
}
|
|
266
386
|
else {
|
|
267
387
|
await this.runAgent(trimmed);
|
|
268
388
|
}
|
|
269
|
-
if (this.running) {
|
|
270
|
-
this.rl.setPrompt(pc.cyan(t("repl.you")));
|
|
271
|
-
this.rl.prompt();
|
|
272
|
-
}
|
|
273
389
|
});
|
|
274
390
|
this.rl.on("close", () => {
|
|
275
391
|
this.running = false;
|
|
@@ -303,7 +419,7 @@ export class Repl {
|
|
|
303
419
|
}
|
|
304
420
|
Repl.sigintHandler = () => {
|
|
305
421
|
if (this.agentRunning) {
|
|
306
|
-
|
|
422
|
+
this.output.writeLine(pc.yellow(t("repl.ctrl_c_interrupt")));
|
|
307
423
|
this.agent.shutdown();
|
|
308
424
|
this.agentRunning = false;
|
|
309
425
|
if (forceExitTimer)
|
|
@@ -316,6 +432,56 @@ export class Repl {
|
|
|
316
432
|
};
|
|
317
433
|
process.on("SIGINT", Repl.sigintHandler);
|
|
318
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Re-present the input prompt after a slash command. Commands do not own a
|
|
437
|
+
* frame lifecycle, so they need this explicit redraw. The agent path is
|
|
438
|
+
* deliberately excluded: `runAgent`'s finally calls `LineEditor.resumeFrame()`
|
|
439
|
+
* as its single prompt presentation, so prompting here again would double-draw.
|
|
440
|
+
*/
|
|
441
|
+
presentPrompt() {
|
|
442
|
+
if (!this.running)
|
|
443
|
+
return;
|
|
444
|
+
this.rl.setPrompt(this.inputPrompt());
|
|
445
|
+
this.rl.prompt();
|
|
446
|
+
this.promptPresented = true;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Reasoning label shown in the input prompt. In `auto` mode the effective
|
|
450
|
+
* level is normally decided per iteration by the policy, so we show only the
|
|
451
|
+
* fact that reasoning is automatic. If the user set an explicit override
|
|
452
|
+
* (`/reasoning <level>`, persistent) or the agent set a still-active
|
|
453
|
+
* `set_thinking` override (transient), we show `auto→<level>`. In a fixed
|
|
454
|
+
* mode we show the effective level.
|
|
455
|
+
*/
|
|
456
|
+
reasoningLabel() {
|
|
457
|
+
const mode = this.config.reasoning?.mode ?? "default";
|
|
458
|
+
if (mode !== "auto")
|
|
459
|
+
return this.agent.reasoningLevel ?? mode;
|
|
460
|
+
const rs = this.agent.reasoningState;
|
|
461
|
+
const cooldown = this.config.reasoning?.overrideCooldown ?? 0;
|
|
462
|
+
const transientActive = rs.overrideIteration >= 0 && this.agent.currentIteration - rs.overrideIteration <= cooldown;
|
|
463
|
+
if (rs.manual || transientActive)
|
|
464
|
+
return `auto→${this.agent.reasoningLevel}`;
|
|
465
|
+
return "auto";
|
|
466
|
+
}
|
|
467
|
+
/** Localized input prompt embedding the active reasoning mode/level. */
|
|
468
|
+
inputPrompt(continuation = false) {
|
|
469
|
+
const key = continuation ? "repl.you_reasoning_cont" : "repl.you_reasoning";
|
|
470
|
+
return pc.cyan(t(key, { level: this.reasoningLabel() }));
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Prompt bookkeeping after a slash command. A command that drove a run
|
|
474
|
+
* (e.g. `/run`) already had its prompt restored by `runAgent`; presenting
|
|
475
|
+
* again would double-render, so skip it exactly once. All other commands
|
|
476
|
+
* present the prompt here.
|
|
477
|
+
*/
|
|
478
|
+
afterCommand() {
|
|
479
|
+
if (this.runHandledPrompt) {
|
|
480
|
+
this.runHandledPrompt = false;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
this.presentPrompt();
|
|
484
|
+
}
|
|
319
485
|
handleSpecialKey(str, key) {
|
|
320
486
|
if (key.name === "escape") {
|
|
321
487
|
// Bun's keypress decoder collapses a fast double-Esc into a single
|
|
@@ -329,7 +495,7 @@ export class Repl {
|
|
|
329
495
|
if (escBytes >= 2 || withinWindow) {
|
|
330
496
|
this.lastEscTime = 0;
|
|
331
497
|
if (this.agentRunning) {
|
|
332
|
-
|
|
498
|
+
this.output.writeLine(pc.yellow(`\n${t("repl.interrupt")}`));
|
|
333
499
|
this.agent.shutdown();
|
|
334
500
|
}
|
|
335
501
|
}
|
|
@@ -349,13 +515,13 @@ export class Repl {
|
|
|
349
515
|
const { dataUrl } = await bufferToDataUrl(clipBuf);
|
|
350
516
|
this.pendingClipboardImage = dataUrl;
|
|
351
517
|
const sizeKb = Math.round((dataUrl.length * 3) / 4 / 1024);
|
|
352
|
-
|
|
518
|
+
this.output.writeLine(pc.green(`\n${t("image.attached", { source: "clipboard", size: `${sizeKb} KB` })}`));
|
|
353
519
|
if (this.rl instanceof LineEditor)
|
|
354
520
|
this.rl.reset();
|
|
355
521
|
this.rl.prompt();
|
|
356
522
|
}
|
|
357
523
|
else {
|
|
358
|
-
|
|
524
|
+
this.output.writeLine(pc.yellow(`\n${t("image.clipboard_empty")}`));
|
|
359
525
|
if (this.rl instanceof LineEditor)
|
|
360
526
|
this.rl.reset();
|
|
361
527
|
this.rl.prompt();
|
|
@@ -390,23 +556,35 @@ export class Repl {
|
|
|
390
556
|
this.pendingClipboardImage = null;
|
|
391
557
|
}
|
|
392
558
|
this.logger?.logREPL("user", input);
|
|
393
|
-
|
|
559
|
+
// The prompt frame is about to be taken down; deactivate the sink before
|
|
560
|
+
// any streamed output so nothing tries to draw above it.
|
|
561
|
+
this.promptPresented = false;
|
|
562
|
+
// Hand the terminal to the channel for the duration of the run: the
|
|
563
|
+
// editor stops drawing its frame and ignores typed keys, so streamed
|
|
564
|
+
// output cannot corrupt the input line. suspendFrame() runs before
|
|
565
|
+
// reset(), which would otherwise draw a frame only to erase it.
|
|
566
|
+
if (this.rl instanceof LineEditor) {
|
|
567
|
+
this.rl.suspendFrame();
|
|
568
|
+
this.rl.setInputEnabled(false);
|
|
394
569
|
this.rl.reset();
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
//
|
|
398
|
-
|
|
570
|
+
}
|
|
571
|
+
this.output.beginStreaming();
|
|
572
|
+
// Divider + agent label, written raw (no trailing newline): the next
|
|
573
|
+
// Renderer.text() chunk adds its own leading "\n" ("- ..."), so a
|
|
574
|
+
// writeLine here would insert a blank line ("Agent:\n\n- ...").
|
|
575
|
+
this.output.writeRaw(`\r${divider()}\n${pc.green(t("repl.agent"))}`);
|
|
399
576
|
const renderer = new Renderer({
|
|
400
577
|
spinner: this.config.ui?.spinner ?? true,
|
|
401
|
-
toolStyle: this.config.ui?.toolStyle ?? "inline",
|
|
402
578
|
baseDir: this.baseDir,
|
|
579
|
+
channel: this.output,
|
|
403
580
|
});
|
|
581
|
+
renderer.showLoader();
|
|
404
582
|
const result = await this.agent.run(input, (c) => renderer.text(c), (m) => renderer.meta(m), (ev) => {
|
|
405
583
|
if (ev.type === "start") {
|
|
406
584
|
renderer.toolStart(ev.tool, ev.args, stepContextForTool(this.execModule?.getActivePlan() ?? null, ev.tool, ev.args), ev.icon);
|
|
407
585
|
}
|
|
408
586
|
else {
|
|
409
|
-
renderer.toolEnd(ev.tool, ev.duration ?? 0, ev.error, ev.ctxDelta, ev.costUsd
|
|
587
|
+
renderer.toolEnd(ev.tool, ev.duration ?? 0, ev.error, ev.ctxDelta, ev.costUsd);
|
|
410
588
|
if (ev.tool === "plan" || ev.tool === "todo") {
|
|
411
589
|
this.renderPlan(renderer);
|
|
412
590
|
}
|
|
@@ -420,18 +598,39 @@ export class Repl {
|
|
|
420
598
|
}
|
|
421
599
|
});
|
|
422
600
|
renderer.flush();
|
|
423
|
-
|
|
601
|
+
if (result.provider && result.model) {
|
|
602
|
+
renderer.footer(result.model, result.provider, result.llmDurationMs ?? result.durationMs ?? 0);
|
|
603
|
+
}
|
|
604
|
+
this.output.writeLine("");
|
|
424
605
|
this.logger?.logREPL(result.success ? "assistant" : "system", result.text?.slice(0, 400) || result.error || "");
|
|
425
606
|
if (!result.success) {
|
|
426
|
-
|
|
607
|
+
this.output.writeLine(pc.red(`${t("error.prefix")}${result.error}`), "stderr");
|
|
427
608
|
}
|
|
428
609
|
this.showContextBar(result);
|
|
429
610
|
// Close the agent's turn: a divider before the next prompt keeps
|
|
430
611
|
// consecutive user/agent messages visually separated.
|
|
431
|
-
|
|
612
|
+
this.output.writeLine(`\n${divider()}`);
|
|
432
613
|
}
|
|
433
614
|
finally {
|
|
434
615
|
this.agentRunning = false;
|
|
616
|
+
this.output.endStreaming();
|
|
617
|
+
if (this.rl instanceof LineEditor) {
|
|
618
|
+
this.rl.setInputEnabled(true);
|
|
619
|
+
// Single prompt presentation for the agent path: resumeFrame()
|
|
620
|
+
// redraws the frame, so the `line` listener must NOT prompt again
|
|
621
|
+
// after runAgent() (it still does after slash commands).
|
|
622
|
+
// Re-set the prompt first so a reasoning level changed by
|
|
623
|
+
// set_thinking during the run is reflected on the resumed frame.
|
|
624
|
+
this.rl.setPrompt(this.inputPrompt());
|
|
625
|
+
this.rl.resumeFrame();
|
|
626
|
+
this.promptPresented = true;
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
// Piped/non-TTY fallback has no frame to resume; present the readline
|
|
630
|
+
// prompt here to preserve the old post-run behavior.
|
|
631
|
+
this.presentPrompt();
|
|
632
|
+
}
|
|
633
|
+
this.runHandledPrompt = true;
|
|
435
634
|
}
|
|
436
635
|
}
|
|
437
636
|
/** Print the checklist of the plan currently tracked by ExecutionModule.
|
|
@@ -453,6 +652,11 @@ export class Repl {
|
|
|
453
652
|
}
|
|
454
653
|
}
|
|
455
654
|
async withExclusiveInput(fn) {
|
|
655
|
+
// The wizard owns the terminal through its own `rl.question()` calls. Between
|
|
656
|
+
// questions no frame is active, so its menus/scan results must be written
|
|
657
|
+
// directly — bracketing with beginModal/endModal would QUEUE the whole
|
|
658
|
+
// transcript and the user would only see bare `: ` prompts until completion.
|
|
659
|
+
// `inputLocked` makes `promptSink.isActive()` false, routing writes direct.
|
|
456
660
|
this.inputLocked = true;
|
|
457
661
|
try {
|
|
458
662
|
await fn();
|
|
@@ -461,22 +665,60 @@ export class Repl {
|
|
|
461
665
|
this.inputLocked = false;
|
|
462
666
|
}
|
|
463
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* Borrow the terminal for one interactive-tool question and hand it back.
|
|
670
|
+
* During a run the LineEditor is suspended with input disabled; enabling
|
|
671
|
+
* input + resuming the frame lets `question()` read keys while the agent
|
|
672
|
+
* blocks, and the `finally` restores exactly the streaming state so the
|
|
673
|
+
* agent's own renderer keeps working afterwards.
|
|
674
|
+
*
|
|
675
|
+
* Order matters: `beginModal()` first flushes any output still buffered by
|
|
676
|
+
* the streaming channel so it prints plainly before the frame returns;
|
|
677
|
+
* `endModal()` runs only after the frame is suspended again, so events
|
|
678
|
+
* queued during the question flush plainly instead of corrupting the frame.
|
|
679
|
+
*/
|
|
680
|
+
async askViaEditor(prompt) {
|
|
681
|
+
const editor = this.rl instanceof LineEditor ? this.rl : null;
|
|
682
|
+
const wasSuspended = editor ? editor.isFrameSuspended() : true;
|
|
683
|
+
const wasEnabled = editor ? editor.isInputEnabled() : false;
|
|
684
|
+
this.output.beginModal();
|
|
685
|
+
if (editor) {
|
|
686
|
+
editor.setInputEnabled(true);
|
|
687
|
+
editor.resumeFrame();
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
return await new Promise((resolve) => this.rl.question(prompt, resolve));
|
|
691
|
+
}
|
|
692
|
+
finally {
|
|
693
|
+
if (editor) {
|
|
694
|
+
editor.setInputEnabled(wasEnabled);
|
|
695
|
+
if (wasSuspended)
|
|
696
|
+
editor.suspendFrame();
|
|
697
|
+
else
|
|
698
|
+
editor.resumeFrame();
|
|
699
|
+
}
|
|
700
|
+
this.output.endModal();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
464
703
|
async executeCommand(input) {
|
|
465
704
|
const parts = input.split(/\s+/);
|
|
466
705
|
const name = parts[0].slice(1);
|
|
467
706
|
const args = parts.slice(1);
|
|
707
|
+
// Clear any stale run flag from a previous direct agent run so this
|
|
708
|
+
// command's own presentPrompt() decision is based on its own action.
|
|
709
|
+
this.runHandledPrompt = false;
|
|
468
710
|
const cmd = this.commands.get(name);
|
|
469
711
|
if (!cmd) {
|
|
470
|
-
|
|
712
|
+
this.output.writeLine(`${pc.red(t("cli.unknown_cmd", { name }))} ${t("cli.help_hint")}`);
|
|
471
713
|
return;
|
|
472
714
|
}
|
|
473
715
|
try {
|
|
474
716
|
await cmd.action(args);
|
|
475
717
|
}
|
|
476
718
|
catch (err) {
|
|
477
|
-
|
|
478
|
-
message:
|
|
479
|
-
})));
|
|
719
|
+
this.output.writeLine(pc.red(t("error.command_error", {
|
|
720
|
+
message: errMsg(err),
|
|
721
|
+
})), "stderr");
|
|
480
722
|
}
|
|
481
723
|
}
|
|
482
724
|
showHelp() {
|
|
@@ -498,15 +740,15 @@ export class Repl {
|
|
|
498
740
|
}
|
|
499
741
|
if (cmds.length === 0)
|
|
500
742
|
continue;
|
|
501
|
-
|
|
743
|
+
this.output.writeLine(pc.bold(t(`repl.group.${groupKey}`)));
|
|
502
744
|
for (const cmd of cmds) {
|
|
503
745
|
const aliases = cmd.aliases?.length ? ` (${pc.dim(cmd.aliases.join(", "))})` : "";
|
|
504
|
-
|
|
746
|
+
this.output.writeLine(` ${pc.cyan("/" + cmd.name)}${aliases} ${pc.dim(cmd.description)}`);
|
|
505
747
|
if (cmd.usage) {
|
|
506
|
-
|
|
748
|
+
this.output.writeLine(` ${pc.dim(cmd.usage)}`);
|
|
507
749
|
}
|
|
508
750
|
}
|
|
509
|
-
|
|
751
|
+
this.output.writeLine("");
|
|
510
752
|
}
|
|
511
753
|
}
|
|
512
754
|
lastCompactionShown = 0;
|
|
@@ -518,21 +760,25 @@ export class Repl {
|
|
|
518
760
|
}
|
|
519
761
|
const ui = this.config.ui;
|
|
520
762
|
if (ui?.showContextStats) {
|
|
521
|
-
|
|
763
|
+
this.output.writeLine("");
|
|
522
764
|
const ctxLine = formatContextBar(result.contextUsed, result.contextLimit, result.compactionCount, result.contextQuality);
|
|
523
|
-
|
|
765
|
+
this.output.writeLine(ctxLine);
|
|
524
766
|
if (result.totalTokens !== undefined && result.totalTokens > 0) {
|
|
525
767
|
const apiLine = pc.dim(` API: ${result.promptTokens} prompt + ${result.completionTokens} completion = ${result.totalTokens} total`);
|
|
526
|
-
|
|
768
|
+
this.output.writeLine(apiLine);
|
|
527
769
|
}
|
|
528
770
|
if (result.totalCost !== undefined && result.totalCost > 0) {
|
|
529
|
-
|
|
771
|
+
this.output.writeLine(pc.yellow(` ${t("repl.cost", { cost: formatCost(result.totalCost) })}`));
|
|
772
|
+
}
|
|
773
|
+
const cacheLine = formatCacheLine(result.cache);
|
|
774
|
+
if (cacheLine) {
|
|
775
|
+
this.output.writeLine(pc.dim(` ${cacheLine}`));
|
|
530
776
|
}
|
|
531
777
|
}
|
|
532
778
|
else if (ui?.showCompaction && result.compactionCount !== undefined) {
|
|
533
779
|
if (result.compactionCount > this.lastCompactionShown) {
|
|
534
780
|
this.lastCompactionShown = result.compactionCount;
|
|
535
|
-
|
|
781
|
+
this.output.writeLine(pc.dim(`\n ⟳ Context compacted (${result.compactionCount})`));
|
|
536
782
|
}
|
|
537
783
|
}
|
|
538
784
|
}
|
|
@@ -574,8 +820,9 @@ export class Repl {
|
|
|
574
820
|
const names = enabledServers.map(([name]) => name).join(", ");
|
|
575
821
|
row(t("repl.mcp_label"), `${pc.white(String(enabledServers.length))} ${pc.dim(`(${names})`)}`);
|
|
576
822
|
}
|
|
577
|
-
|
|
578
|
-
|
|
823
|
+
// Show the agent's workspace (the `-d` dir), not the process cwd: `bun run`
|
|
824
|
+
// sets cwd to the package root, which is NOT where the agent operates.
|
|
825
|
+
row(t("repl.work_dir"), pc.dim(this.baseDir));
|
|
579
826
|
if (this.noAgentsMd) {
|
|
580
827
|
row(t("repl.agents_label"), pc.red(t("repl.disabled")));
|
|
581
828
|
}
|
|
@@ -605,9 +852,9 @@ export class Repl {
|
|
|
605
852
|
const lspEnabled = isTty && (this.config.lsp ?? DEFAULT_LSP_CONFIG).enabled !== false;
|
|
606
853
|
// Plain banner without a surrounding box — line-based, wraps to width.
|
|
607
854
|
for (const line of info) {
|
|
608
|
-
|
|
855
|
+
this.output.writeLine(pc.dim(line).trimEnd());
|
|
609
856
|
}
|
|
610
|
-
|
|
857
|
+
this.output.writeLine("");
|
|
611
858
|
// Legacy config hint
|
|
612
859
|
try {
|
|
613
860
|
const { existsSync: exists } = await import("fs");
|
|
@@ -615,19 +862,22 @@ export class Repl {
|
|
|
615
862
|
const { hasDomainFiles } = await import("../config/domains");
|
|
616
863
|
const legacyPath = pathJoin(this.configDir, "config.json");
|
|
617
864
|
if (exists(legacyPath) && !hasDomainFiles(this.configDir)) {
|
|
618
|
-
|
|
619
|
-
|
|
865
|
+
this.output.writeLine(pc.yellow(` ${t("config.legacy_hint")}`));
|
|
866
|
+
this.output.writeLine("");
|
|
620
867
|
}
|
|
621
868
|
}
|
|
622
869
|
catch { }
|
|
870
|
+
// Single prompt presentation for startup: the banner above was written
|
|
871
|
+
// plainly (promptPresented was false), so no frame needs erasing here.
|
|
623
872
|
this.rl.prompt();
|
|
873
|
+
this.promptPresented = true;
|
|
624
874
|
if (lspEnabled) {
|
|
625
875
|
this.probeLspBanner()
|
|
626
876
|
.then((lspSummary) => {
|
|
627
877
|
if (lspSummary) {
|
|
628
878
|
const line = `${pc.green(t("repl.agent"))}${pc.yellow(t("repl.lsp_label"))} ${lspSummary}`;
|
|
629
|
-
|
|
630
|
-
this.
|
|
879
|
+
// Channel owns the prompt row now: no manual erase + prompt() hack.
|
|
880
|
+
this.output.writeLine(`${divider()}\n${line}`);
|
|
631
881
|
}
|
|
632
882
|
})
|
|
633
883
|
.catch(() => { });
|
|
@@ -645,12 +895,37 @@ export class Repl {
|
|
|
645
895
|
.then((results) => {
|
|
646
896
|
for (const r of results) {
|
|
647
897
|
if (!r.ok && r.name !== this.config.provider.active) {
|
|
648
|
-
|
|
898
|
+
this.output.writeLine(formatWarning(t("repl.provider_down", { name: r.name, error: r.error ?? "" })));
|
|
649
899
|
}
|
|
650
900
|
}
|
|
651
901
|
})
|
|
652
902
|
.catch(() => { });
|
|
653
903
|
}
|
|
904
|
+
// Context probe (background): compare the context length the model is
|
|
905
|
+
// actually loaded with against the configured window. Same non-blocking
|
|
906
|
+
// banner pattern as the LSP probe above.
|
|
907
|
+
if (this.contextProbe) {
|
|
908
|
+
void this.contextProbe
|
|
909
|
+
.then((probe) => {
|
|
910
|
+
if (!probe)
|
|
911
|
+
return;
|
|
912
|
+
this.sessionManager?.appendLog({
|
|
913
|
+
ts: new Date().toISOString(),
|
|
914
|
+
type: "context_probe",
|
|
915
|
+
content: `model=${probe.model} actual=${probe.actual} configured=${this.config.contextWindow}`,
|
|
916
|
+
});
|
|
917
|
+
const line = probe.actual < this.config.contextWindow
|
|
918
|
+
? `${pc.yellow("⚠")} ${t("repl.context_probe_small", { model: probe.model, actual: probe.actual, configured: this.config.contextWindow })}`
|
|
919
|
+
: probe.actual > this.config.contextWindow
|
|
920
|
+
? `${pc.dim(t("repl.context_probe_big", { model: probe.model, actual: probe.actual }))}`
|
|
921
|
+
: null;
|
|
922
|
+
if (line) {
|
|
923
|
+
// Channel owns the prompt row now: no manual erase + prompt() hack.
|
|
924
|
+
this.output.writeLine(`${divider()}\n${line}`);
|
|
925
|
+
}
|
|
926
|
+
})
|
|
927
|
+
.catch(() => { });
|
|
928
|
+
}
|
|
654
929
|
}
|
|
655
930
|
/**
|
|
656
931
|
* Probe the LSP servers applicable to the current project and format a
|
|
@@ -695,6 +970,8 @@ export class Repl {
|
|
|
695
970
|
this.running = false;
|
|
696
971
|
this.saveHistory();
|
|
697
972
|
this.agent.shutdown();
|
|
973
|
+
this.sessionOutput?.dispose();
|
|
974
|
+
this.sessionOutput = undefined;
|
|
698
975
|
// Close session log when REPL stops
|
|
699
976
|
this.logger?.closeSessionLog();
|
|
700
977
|
this.rl.close();
|