smolcoder 0.4.2 → 0.5.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/README.md +12 -1
- package/dist/agent.js +9 -0
- package/dist/config.js +72 -0
- package/dist/index.js +85 -396
- package/dist/session.js +480 -0
- package/dist/tools/tasks.js +15 -0
- package/dist/web/channel.js +222 -0
- package/dist/web/client.js +815 -0
- package/dist/web/hub.js +786 -0
- package/dist/web/page.js +73 -366
- package/dist/web/store.js +199 -0
- package/dist/web/styles.js +222 -0
- package/dist/web/terminal.js +190 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// Interactive: an opencode-style inline TUI. No upfront questions — the last
|
|
6
6
|
// (or first) detected model is picked automatically; switch with /models,
|
|
7
7
|
// cycle modes with shift+tab, set reasoning effort with /effort.
|
|
8
|
+
// Web: smol --web serves a browser UI with a workspace sidebar — many
|
|
9
|
+
// projects and sessions side by side, started from anywhere.
|
|
8
10
|
// Headless: smol -p "prompt" for people and automations.
|
|
9
11
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
12
|
if (k2 === undefined) k2 = k;
|
|
@@ -44,22 +46,20 @@ const fs = __importStar(require("fs"));
|
|
|
44
46
|
const os = __importStar(require("os"));
|
|
45
47
|
const path = __importStar(require("path"));
|
|
46
48
|
const agent_1 = require("./agent");
|
|
49
|
+
const config_1 = require("./config");
|
|
47
50
|
const context_1 = require("./context");
|
|
48
|
-
const detect_1 = require("./detect");
|
|
49
51
|
const events_1 = require("./events");
|
|
50
52
|
const plan_1 = require("./plan");
|
|
51
53
|
const prompt_1 = require("./prompt");
|
|
52
|
-
const
|
|
53
|
-
const ollama_1 = require("./providers/ollama");
|
|
54
|
-
const index_1 = require("./tools/index");
|
|
54
|
+
const session_1 = require("./session");
|
|
55
55
|
const shell_1 = require("./tools/shell");
|
|
56
56
|
const tasks_1 = require("./tools/tasks");
|
|
57
57
|
const tui_1 = require("./tui/tui");
|
|
58
58
|
const ui_1 = require("./ui");
|
|
59
|
-
const webui_1 = require("./web/webui");
|
|
60
59
|
const util_1 = require("./util");
|
|
60
|
+
const hub_1 = require("./web/hub");
|
|
61
61
|
const VERSION = require("../package.json").version;
|
|
62
|
-
const
|
|
62
|
+
const DEFAULT_WEB_PORT = 7433;
|
|
63
63
|
function parseArgs(argv) {
|
|
64
64
|
const args = { workspace: process.cwd() };
|
|
65
65
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -105,8 +105,10 @@ function parseArgs(argv) {
|
|
|
105
105
|
if (argv[i + 1] && /^\d+$/.test(argv[i + 1]))
|
|
106
106
|
args.webPort = Number(argv[++i]);
|
|
107
107
|
}
|
|
108
|
-
else if (!a.startsWith("-"))
|
|
108
|
+
else if (!a.startsWith("-")) {
|
|
109
109
|
args.workspace = path.resolve(a);
|
|
110
|
+
args.workspaceGiven = true;
|
|
111
|
+
}
|
|
110
112
|
else {
|
|
111
113
|
console.error(`Unknown option "${a}". Try smol --help.`);
|
|
112
114
|
process.exit(1);
|
|
@@ -129,7 +131,10 @@ ${util_1.c.bold("Options:")}
|
|
|
129
131
|
--model <name> pick a model by (partial) name
|
|
130
132
|
--ctx <tokens> force a context window (Ollama: sends num_ctx)
|
|
131
133
|
--effort <level> reasoning effort: off, low, medium, high, default
|
|
132
|
-
--web [port]
|
|
134
|
+
--web [port] browser UI (default port ${DEFAULT_WEB_PORT}): a sidebar of your
|
|
135
|
+
workspaces and sessions, an embedded browser and
|
|
136
|
+
terminal panel. Run it from anywhere; a second
|
|
137
|
+
smol --web adds its folder to the running UI.
|
|
133
138
|
-p, --print "<prompt>" headless: run a single prompt and exit
|
|
134
139
|
-h, --help this help
|
|
135
140
|
-v, --version version
|
|
@@ -147,69 +152,6 @@ ${util_1.c.bold("Slash commands:")}
|
|
|
147
152
|
/context context usage /clear reset conversation
|
|
148
153
|
/compact compact now /exit quit
|
|
149
154
|
`;
|
|
150
|
-
function loadConfig() {
|
|
151
|
-
try {
|
|
152
|
-
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
153
|
-
// Never let bypass be inherited implicitly from a past session — a single
|
|
154
|
-
// shift+tab into it would otherwise silently persist unattended, unchecked
|
|
155
|
-
// command execution into every later run, including headless -p in CI.
|
|
156
|
-
// Requires an explicit flag (-m bypass / --bypass) each time. Old configs
|
|
157
|
-
// saved "write"/"yolo" under the previous mode names.
|
|
158
|
-
if (cfg.lastMode === "write")
|
|
159
|
-
cfg.lastMode = "edit";
|
|
160
|
-
if (cfg.lastMode === "yolo" || cfg.lastMode === "bypass")
|
|
161
|
-
cfg.lastMode = "edit";
|
|
162
|
-
return cfg;
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
return {};
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
function saveConfig(cfg) {
|
|
169
|
-
try {
|
|
170
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
/* non-fatal */
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
/** Output budget scales with the window: big windows can afford whole-file
|
|
177
|
-
* writes (a single write_file's JSON must fit in the output), tiny windows
|
|
178
|
-
* must stay conservative. */
|
|
179
|
-
function outputBudget(window) {
|
|
180
|
-
return Math.max(1024, Math.min(16384, Math.floor(window / 4)));
|
|
181
|
-
}
|
|
182
|
-
function makeProvider(m) {
|
|
183
|
-
const maxOut = outputBudget(m.contextWindow);
|
|
184
|
-
return m.backend === "ollama"
|
|
185
|
-
? new ollama_1.OllamaProvider(m.baseUrl, m.id, m.contextWindow, m.numCtx, maxOut)
|
|
186
|
-
: new lmstudio_1.LmStudioProvider(m.baseUrl, m.id, m.contextWindow, maxOut, m.reasoning);
|
|
187
|
-
}
|
|
188
|
-
/** One-line advice when the effective reasoning setting will be slow: LM
|
|
189
|
-
* Studio applies the model's own default level when none is chosen, and for
|
|
190
|
-
* current qwen builds that default is the maximum. */
|
|
191
|
-
function effortAdvice(m, effort) {
|
|
192
|
-
if (m.backend !== "lmstudio" || !m.reasoning?.default)
|
|
193
|
-
return null;
|
|
194
|
-
const d = m.reasoning.default;
|
|
195
|
-
if (effort === null && /^(high|xhigh)$/.test(d)) {
|
|
196
|
-
return `this model thinks at "${d}" by default on LM Studio — expect long pauses before each tool call. /effort off (or --effort off) is many times faster; /effort low or medium keeps some reasoning.`;
|
|
197
|
-
}
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
/** Tell the user what context management just did (both UIs; headless logs
|
|
201
|
-
* it to stderr so a long run's log shows when and how hard compaction hit). */
|
|
202
|
-
function reportCompactions(bus, ui) {
|
|
203
|
-
bus.on("post_compact", (report) => {
|
|
204
|
-
const delta = `${report?.before} → ${report?.after} tokens est.`;
|
|
205
|
-
if (report?.action === "evicted")
|
|
206
|
-
ui.status(`· freed context by dropping old tool output (${delta})`);
|
|
207
|
-
else if (report?.action === "compacted")
|
|
208
|
-
ui.status(`· compacted the conversation into hand-over notes (${delta})`);
|
|
209
|
-
else if (report?.action === "floor")
|
|
210
|
-
ui.warn(`· context is at its floor: system prompt + tools + the working tail no longer fit comfortably (${delta}). Consider a bigger context window.`);
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
155
|
/** Node fires 'exit' on normal termination but NOT on a killing signal, so
|
|
214
156
|
* background tasks (dev servers) survive a closed terminal (SIGHUP) or `kill`
|
|
215
157
|
* (SIGTERM) unless we handle those explicitly. Runs synchronous cleanup then
|
|
@@ -232,36 +174,13 @@ function installSignalCleanup(cleanup) {
|
|
|
232
174
|
process.on("SIGHUP", () => run(129));
|
|
233
175
|
process.on("SIGINT", () => run(130));
|
|
234
176
|
}
|
|
235
|
-
function autoPickModel(models, wanted, remembered) {
|
|
236
|
-
if (wanted) {
|
|
237
|
-
const hit = models.find((m) => m.id === wanted) ??
|
|
238
|
-
models.find((m) => m.id.toLowerCase().includes(wanted.toLowerCase()));
|
|
239
|
-
if (hit)
|
|
240
|
-
return hit;
|
|
241
|
-
}
|
|
242
|
-
return (models.find((m) => m.id === remembered) ??
|
|
243
|
-
models.find((m) => m.backend === "ollama") ??
|
|
244
|
-
models.find((m) => m.loaded) ??
|
|
245
|
-
models[0]);
|
|
246
|
-
}
|
|
247
|
-
function noBackendsMessage() {
|
|
248
|
-
return (util_1.c.red("No local model backend found.") +
|
|
249
|
-
`\n\nsmolcoder looks for:\n` +
|
|
250
|
-
` · ${util_1.c.bold("Ollama")} at http://127.0.0.1:11434 ${util_1.c.dim("(or $OLLAMA_HOST)")} — install: https://ollama.com, then: ollama pull qwen3\n` +
|
|
251
|
-
` · ${util_1.c.bold("LM Studio")} at http://127.0.0.1:1234 — start its local server (Developer tab → Start Server)\n\n` +
|
|
252
|
-
`Start one of them and run smol again. No configuration needed.`);
|
|
253
|
-
}
|
|
254
|
-
function sessionLine(m, mode) {
|
|
255
|
-
return `${util_1.c.green("●")} ${m.backend} · ${util_1.c.bold(m.id)} · ctx ${m.contextWindow.toLocaleString()} · ${index_1.MODE_LABELS[mode]} mode`;
|
|
256
|
-
}
|
|
257
|
-
const MODE_ORDER = ["ro", "edit", "bypass"];
|
|
258
177
|
const LOGO_ROWS = [
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
" ██║
|
|
262
|
-
" ██║
|
|
263
|
-
"
|
|
264
|
-
"
|
|
178
|
+
"███████╗ ███╗ ███╗ ██████╗ ██╗ ",
|
|
179
|
+
"██╔════╝ ████╗ ████║ ██╔═══██╗ ██║ ",
|
|
180
|
+
"███████╗ ██╔████╔██║ ██║ ██║ ██║ ",
|
|
181
|
+
"╚════██║ ██║╚██╔╝██║ ██║ ██║ ██║ ",
|
|
182
|
+
"███████║ ██║ ╚═╝ ██║ ╚██████╔╝ ███████╗",
|
|
183
|
+
"╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝",
|
|
265
184
|
];
|
|
266
185
|
function printLogo() {
|
|
267
186
|
const cols = process.stdout.columns || 80;
|
|
@@ -277,16 +196,8 @@ function printLogo() {
|
|
|
277
196
|
console.log(`${util_1.c.bold("smol")}${util_1.c.dim(util_1.c.bold("coder"))} ${util_1.c.dim("v" + VERSION)}`);
|
|
278
197
|
}
|
|
279
198
|
}
|
|
280
|
-
function
|
|
281
|
-
return
|
|
282
|
-
}
|
|
283
|
-
function modeColored(mode) {
|
|
284
|
-
const label = index_1.MODE_LABELS[mode];
|
|
285
|
-
if (mode === "bypass")
|
|
286
|
-
return util_1.c.red(util_1.c.bold(label));
|
|
287
|
-
if (mode === "ro")
|
|
288
|
-
return util_1.c.magenta(util_1.c.bold(label));
|
|
289
|
-
return util_1.c.cyan(util_1.c.bold(label));
|
|
199
|
+
function prefsOf(args) {
|
|
200
|
+
return { mode: args.mode, model: args.model, ctx: args.ctx, effort: args.effort };
|
|
290
201
|
}
|
|
291
202
|
// ---------------------------------------------------------------------------
|
|
292
203
|
async function main() {
|
|
@@ -303,29 +214,27 @@ async function main() {
|
|
|
303
214
|
console.error(`Workspace folder does not exist: ${args.workspace}`);
|
|
304
215
|
process.exit(1);
|
|
305
216
|
}
|
|
306
|
-
if (args.print !== undefined)
|
|
217
|
+
if (args.print !== undefined)
|
|
307
218
|
await runHeadless(args);
|
|
308
|
-
|
|
309
|
-
|
|
219
|
+
else if (args.web)
|
|
220
|
+
await runWeb(args);
|
|
221
|
+
else
|
|
310
222
|
await runInteractive(args);
|
|
311
|
-
}
|
|
312
223
|
}
|
|
313
224
|
// ---- headless (-p) ---------------------------------------------------------
|
|
314
225
|
async function runHeadless(args) {
|
|
315
226
|
const ui = new ui_1.UI();
|
|
316
227
|
const bus = new events_1.EventBus();
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
228
|
+
const cfg = (0, config_1.loadConfig)();
|
|
229
|
+
const chosen = await (0, session_1.prepareModel)(prefsOf(args), cfg);
|
|
230
|
+
if (!chosen) {
|
|
231
|
+
ui.println((0, session_1.noBackendsMessage)());
|
|
320
232
|
ui.close();
|
|
321
233
|
process.exit(1);
|
|
322
234
|
}
|
|
323
|
-
const cfg = loadConfig();
|
|
324
|
-
let chosen = autoPickModel(models, args.model, cfg.lastModel);
|
|
325
|
-
chosen = await (0, detect_1.resolveContextWindow)(chosen, args.ctx);
|
|
326
235
|
const mode = args.mode ?? cfg.lastMode ?? "edit";
|
|
327
236
|
const shell = (0, shell_1.pickShell)();
|
|
328
|
-
const provider = makeProvider(chosen);
|
|
237
|
+
const provider = (0, session_1.makeProvider)(chosen);
|
|
329
238
|
provider.setEffort(args.effort !== undefined ? args.effort : (cfg.effort ?? null));
|
|
330
239
|
const taskManager = new tasks_1.TaskManager(args.workspace);
|
|
331
240
|
const toolCtx = {
|
|
@@ -341,15 +250,15 @@ async function runHeadless(args) {
|
|
|
341
250
|
ui.status(`· AGENTS.md loaded (${agentsMd.split("\n").length} lines)`);
|
|
342
251
|
const systemPrompt = (0, prompt_1.buildSystemPrompt)({ workspace: args.workspace, mode, shellLabel: shell.label, agentsMd });
|
|
343
252
|
const agent = new agent_1.Agent(provider, mode, systemPrompt, toolCtx, ctxMgr, bus, ui, false, 1000);
|
|
344
|
-
reportCompactions(bus, ui);
|
|
253
|
+
(0, session_1.reportCompactions)(bus, ui);
|
|
345
254
|
process.on("exit", () => taskManager.killAll());
|
|
346
255
|
installSignalCleanup(() => taskManager.killAll());
|
|
347
|
-
ui.println(sessionLine(chosen, mode));
|
|
256
|
+
ui.println((0, session_1.sessionLine)(chosen, mode));
|
|
348
257
|
if (chosen.note)
|
|
349
258
|
ui.warn(` ${chosen.note}`);
|
|
350
259
|
const effortSetting = args.effort !== undefined ? args.effort : (cfg.effort ?? null);
|
|
351
260
|
ui.status(` effort ${provider.effortLabel() ?? effortSetting ?? "default"}`);
|
|
352
|
-
const advice = effortAdvice(chosen, effortSetting);
|
|
261
|
+
const advice = (0, session_1.effortAdvice)(chosen, effortSetting);
|
|
353
262
|
if (advice)
|
|
354
263
|
ui.warn(` ${advice}`);
|
|
355
264
|
try {
|
|
@@ -380,96 +289,28 @@ async function runHeadless(args) {
|
|
|
380
289
|
ui.close();
|
|
381
290
|
}
|
|
382
291
|
// ---- interactive TUI -------------------------------------------------------
|
|
383
|
-
const SLASH_COMMANDS = [
|
|
384
|
-
{ name: "models", desc: "Switch model" },
|
|
385
|
-
{ name: "mode", desc: "Set mode (ro / edit / bypass)" },
|
|
386
|
-
{ name: "effort", desc: "Set reasoning effort" },
|
|
387
|
-
{ name: "plan", desc: "Show the agent's plan" },
|
|
388
|
-
{ name: "context", desc: "Show context usage" },
|
|
389
|
-
{ name: "compact", desc: "Compact the conversation now" },
|
|
390
|
-
{ name: "tasks", desc: "List background tasks" },
|
|
391
|
-
{ name: "logs", desc: "Show task output — /logs t1" },
|
|
392
|
-
{ name: "stop", desc: "Stop a background task — /stop t1" },
|
|
393
|
-
{ name: "clear", desc: "Reset the conversation" },
|
|
394
|
-
{ name: "help", desc: "Show help" },
|
|
395
|
-
{ name: "exit", desc: "Quit smolcoder" },
|
|
396
|
-
];
|
|
397
292
|
async function runInteractive(args) {
|
|
398
|
-
|
|
399
|
-
if (!isWeb && (!process.stdout.isTTY || !process.stdin.isTTY)) {
|
|
293
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
400
294
|
console.error('Interactive mode needs a terminal. For headless use, run: smol -p "your prompt" — or serve a browser UI with --web');
|
|
401
295
|
process.exit(1);
|
|
402
296
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
else
|
|
406
|
-
printLogo();
|
|
297
|
+
printLogo();
|
|
298
|
+
const cfg = (0, config_1.loadConfig)();
|
|
407
299
|
process.stdout.write(util_1.c.dim("· looking for Ollama and LM Studio…"));
|
|
408
|
-
const
|
|
300
|
+
const chosen = await (0, session_1.prepareModel)(prefsOf(args), cfg, (label) => {
|
|
301
|
+
process.stdout.write("\r\x1b[2K" + util_1.c.dim(`· ${label}…`));
|
|
302
|
+
});
|
|
409
303
|
process.stdout.write("\r\x1b[2K");
|
|
410
|
-
if (
|
|
411
|
-
console.log(noBackendsMessage());
|
|
304
|
+
if (!chosen) {
|
|
305
|
+
console.log((0, session_1.noBackendsMessage)());
|
|
412
306
|
process.exit(1);
|
|
413
307
|
}
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
process.stdout.write("\r\x1b[2K");
|
|
419
|
-
const mode0 = args.mode ?? cfg.lastMode ?? "edit";
|
|
420
|
-
let effort = args.effort !== undefined ? args.effort : (cfg.effort ?? null);
|
|
421
|
-
const shell = (0, shell_1.pickShell)();
|
|
422
|
-
const bus = new events_1.EventBus();
|
|
423
|
-
const provider = makeProvider(chosen);
|
|
424
|
-
provider.setEffort(effort);
|
|
425
|
-
const taskManager = new tasks_1.TaskManager(args.workspace);
|
|
426
|
-
const toolCtx = {
|
|
427
|
-
workspace: args.workspace,
|
|
428
|
-
taskManager,
|
|
429
|
-
plan: new plan_1.Plan(),
|
|
430
|
-
filesTouched: new Set(),
|
|
431
|
-
commandsRun: [],
|
|
432
|
-
};
|
|
433
|
-
const ctxMgr = new context_1.ContextManager(chosen.contextWindow, provider.maxOutputTokens);
|
|
434
|
-
const agentsMd = (0, prompt_1.loadAgentsMd)(args.workspace);
|
|
435
|
-
const sysPrompt = (m) => (0, prompt_1.buildSystemPrompt)({ workspace: args.workspace, mode: m, shellLabel: shell.label, agentsMd });
|
|
436
|
-
// The step cap is a runaway-loop backstop, not a work limit — esc/ctrl+c is
|
|
437
|
-
// the user's real kill switch, so set it far above any legitimate task.
|
|
438
|
-
const tui = isWeb ? new webui_1.WebUI(args.webPort ?? 7433) : new tui_1.Tui();
|
|
439
|
-
const agent = new agent_1.Agent(provider, mode0, sysPrompt(mode0), toolCtx, ctxMgr, bus, tui, true, 1000);
|
|
440
|
-
const persist = () => saveConfig({ lastModel: chosen.id, lastMode: agent.mode, effort });
|
|
441
|
-
tui.slashCommands = SLASH_COMMANDS;
|
|
442
|
-
tui.hintLeft = args.workspace.replace(os.homedir(), "~");
|
|
443
|
-
tui.getStatus = () => {
|
|
444
|
-
const tasks = taskManager.runningSummary().length;
|
|
445
|
-
return (`${modeColored(agent.mode)} ${util_1.c.dim("·")} ${chosen.id} ${util_1.c.dim(chosen.backend)}` +
|
|
446
|
-
(effort || agent.provider.effortLabel()
|
|
447
|
-
? ` ${util_1.c.dim("·")} ${util_1.c.yellow(agent.provider.effortLabel() ?? effort ?? "")}`
|
|
448
|
-
: "") +
|
|
449
|
-
` ${util_1.c.dim("·")} ${util_1.c.dim(`${fmtTokens(agent.contextTokens())} (${agent.contextPercent()}%)`)}` +
|
|
450
|
-
(toolCtx.plan.exists
|
|
451
|
-
? ` ${util_1.c.dim("·")} ${toolCtx.plan.currentIndex < 0
|
|
452
|
-
? util_1.c.green(`plan ${toolCtx.plan.doneCount}/${toolCtx.plan.steps.length}`)
|
|
453
|
-
: util_1.c.cyan(`plan ${toolCtx.plan.doneCount}/${toolCtx.plan.steps.length}`)}`
|
|
454
|
-
: "") +
|
|
455
|
-
(tasks ? ` ${util_1.c.dim("·")} ${util_1.c.green(`${tasks} task${tasks > 1 ? "s" : ""}`)}` : ""));
|
|
456
|
-
};
|
|
457
|
-
tui.onModeCycle = () => {
|
|
458
|
-
const next = MODE_ORDER[(MODE_ORDER.indexOf(agent.mode) + 1) % MODE_ORDER.length];
|
|
459
|
-
agent.setMode(next, sysPrompt(next));
|
|
460
|
-
persist();
|
|
461
|
-
};
|
|
462
|
-
tui.onCancel = () => agent.cancel();
|
|
463
|
-
const shutdown = async () => {
|
|
464
|
-
await bus.emit("session_end");
|
|
465
|
-
taskManager.killAll();
|
|
466
|
-
tui.close();
|
|
467
|
-
process.exit(0);
|
|
468
|
-
};
|
|
469
|
-
tui.onExit = () => void shutdown();
|
|
470
|
-
process.on("exit", () => taskManager.killAll());
|
|
308
|
+
const tui = new tui_1.Tui();
|
|
309
|
+
const session = new session_1.Session(tui, { workspace: args.workspace, chosen, prefs: prefsOf(args), cfg, help: HELP });
|
|
310
|
+
session.onExit = () => process.exit(0);
|
|
311
|
+
process.on("exit", () => session.taskManager.killAll());
|
|
471
312
|
installSignalCleanup(() => {
|
|
472
|
-
taskManager.killAll();
|
|
313
|
+
session.taskManager.killAll();
|
|
473
314
|
try {
|
|
474
315
|
tui.close(); // restore the raw-mode terminal on signal death
|
|
475
316
|
}
|
|
@@ -477,202 +318,50 @@ async function runInteractive(args) {
|
|
|
477
318
|
/* best effort */
|
|
478
319
|
}
|
|
479
320
|
});
|
|
480
|
-
reportCompactions(bus, tui);
|
|
481
|
-
if (tui instanceof webui_1.WebUI) {
|
|
482
|
-
tui.getState = () => ({
|
|
483
|
-
mode: agent.mode,
|
|
484
|
-
model: chosen.id,
|
|
485
|
-
backend: chosen.backend,
|
|
486
|
-
effort: agent.provider.effortLabel() ?? effort,
|
|
487
|
-
ctxTokens: agent.contextTokens(),
|
|
488
|
-
ctxPct: agent.contextPercent(),
|
|
489
|
-
plan: toolCtx.plan.exists
|
|
490
|
-
? { steps: toolCtx.plan.steps, current: toolCtx.plan.currentIndex }
|
|
491
|
-
: null,
|
|
492
|
-
tasks: taskManager.runningSummary().length,
|
|
493
|
-
workspace: args.workspace,
|
|
494
|
-
commands: SLASH_COMMANDS,
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
321
|
tui.start();
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
? "ollama"
|
|
523
|
-
: `lm studio${m.loaded ? ` · ctx ${m.contextWindow.toLocaleString()}` : " · not loaded"}`,
|
|
524
|
-
current: m.id === chosen.id && m.backend === chosen.backend,
|
|
525
|
-
}));
|
|
526
|
-
const idx = await tui.select("Select model", options);
|
|
527
|
-
if (idx === null)
|
|
322
|
+
session.announce();
|
|
323
|
+
tui.println("");
|
|
324
|
+
await session.run();
|
|
325
|
+
}
|
|
326
|
+
// ---- web hub (--web) -------------------------------------------------------
|
|
327
|
+
/** The home folder or a drive root is not a project: launching there opens
|
|
328
|
+
* the hub with the sidebar and lets the user pick a workspace. */
|
|
329
|
+
function isHomeOrRoot(p) {
|
|
330
|
+
const norm = (s) => (process.platform === "win32" ? s.toLowerCase() : s);
|
|
331
|
+
const r = path.resolve(p);
|
|
332
|
+
return norm(r) === norm(os.homedir()) || path.dirname(r) === r;
|
|
333
|
+
}
|
|
334
|
+
async function runWeb(args) {
|
|
335
|
+
console.log(`${util_1.c.bold("smol")}${util_1.c.dim(util_1.c.bold("coder"))} ${util_1.c.dim("v" + VERSION + " · web")}`);
|
|
336
|
+
const port = args.webPort ?? DEFAULT_WEB_PORT;
|
|
337
|
+
const workspace = args.workspace;
|
|
338
|
+
const autoStart = !!args.workspaceGiven || !isHomeOrRoot(workspace);
|
|
339
|
+
// A hub is already running: hand it this folder instead of starting another.
|
|
340
|
+
const rec = (0, hub_1.readHubRecord)();
|
|
341
|
+
if (rec && (args.webPort === undefined || rec.port === port) && (await (0, hub_1.pingHub)(rec))) {
|
|
342
|
+
const r = await (0, hub_1.askHubToOpen)(rec, workspace, autoStart);
|
|
343
|
+
if (r) {
|
|
344
|
+
const url = `http://127.0.0.1:${rec.port}/?k=${rec.token}${r.id ? "#" + r.id : ""}`;
|
|
345
|
+
console.log(`\n ${autoStart ? "started a session for" : "added"} ${workspace} in the running web UI:\n ${url}\n`);
|
|
528
346
|
return;
|
|
529
|
-
tui.startSpinner(`loading ${fresh[idx].id}`);
|
|
530
|
-
const next = await (0, detect_1.resolveContextWindow)(fresh[idx], args.ctx);
|
|
531
|
-
tui.stopSpinner();
|
|
532
|
-
chosen = next;
|
|
533
|
-
const p = makeProvider(next);
|
|
534
|
-
p.setEffort(effort);
|
|
535
|
-
agent.setProvider(p);
|
|
536
|
-
ctxMgr.setWindow(next.contextWindow, p.maxOutputTokens);
|
|
537
|
-
persist();
|
|
538
|
-
tui.println(sessionLine(next, agent.mode));
|
|
539
|
-
if (next.note)
|
|
540
|
-
tui.warn(` ${next.note}`);
|
|
541
|
-
{
|
|
542
|
-
const advice = effortAdvice(next, effort);
|
|
543
|
-
if (advice)
|
|
544
|
-
tui.warn(` ${advice}`);
|
|
545
|
-
}
|
|
546
|
-
void filter;
|
|
547
|
-
};
|
|
548
|
-
const setMode = async (arg) => {
|
|
549
|
-
let next = arg === "ro"
|
|
550
|
-
? "ro"
|
|
551
|
-
: arg === "edit" || arg === "write"
|
|
552
|
-
? "edit"
|
|
553
|
-
: arg === "bypass" || arg === "yolo"
|
|
554
|
-
? "bypass"
|
|
555
|
-
: undefined;
|
|
556
|
-
if (!next) {
|
|
557
|
-
const idx = await tui.select("Select mode", [
|
|
558
|
-
{ label: "read-only", hint: "read and search files only", current: agent.mode === "ro" },
|
|
559
|
-
{
|
|
560
|
-
label: "edit",
|
|
561
|
-
hint: "edit files; run commands inside the workspace, ask y/n for anything outside it",
|
|
562
|
-
current: agent.mode === "edit",
|
|
563
|
-
},
|
|
564
|
-
{
|
|
565
|
-
label: "bypass permissions",
|
|
566
|
-
hint: "full access, never asks for approval",
|
|
567
|
-
current: agent.mode === "bypass",
|
|
568
|
-
},
|
|
569
|
-
]);
|
|
570
|
-
if (idx === null)
|
|
571
|
-
return;
|
|
572
|
-
next = MODE_ORDER[idx];
|
|
573
|
-
}
|
|
574
|
-
agent.setMode(next, sysPrompt(next));
|
|
575
|
-
persist();
|
|
576
|
-
};
|
|
577
|
-
const setEffort = async (arg) => {
|
|
578
|
-
const levels = ["default", "off", "low", "medium", "high"];
|
|
579
|
-
let next;
|
|
580
|
-
if (arg && levels.includes(arg)) {
|
|
581
|
-
next = arg === "default" ? null : arg;
|
|
582
|
-
}
|
|
583
|
-
else {
|
|
584
|
-
const idx = await tui.select("Reasoning effort", [
|
|
585
|
-
{
|
|
586
|
-
label: "default",
|
|
587
|
-
hint: chosen.reasoning?.default ? `the model's own default (${chosen.reasoning.default})` : "leave it to the model",
|
|
588
|
-
current: effort === null,
|
|
589
|
-
},
|
|
590
|
-
{ label: "off", hint: "no thinking — fastest, best for long tool loops", current: effort === "off" },
|
|
591
|
-
{ label: "low", hint: "brief reasoning", current: effort === "low" },
|
|
592
|
-
{ label: "medium", hint: "", current: effort === "medium" },
|
|
593
|
-
{ label: "high", hint: "most thorough — slow on local models", current: effort === "high" },
|
|
594
|
-
]);
|
|
595
|
-
if (idx === null)
|
|
596
|
-
return;
|
|
597
|
-
next = idx === 0 ? null : levels[idx];
|
|
598
|
-
}
|
|
599
|
-
effort = next;
|
|
600
|
-
agent.provider.setEffort(effort);
|
|
601
|
-
persist();
|
|
602
|
-
const label = agent.provider.effortLabel();
|
|
603
|
-
tui.status(`· effort ${label ?? effort ?? "default"}`);
|
|
604
|
-
const advice = effortAdvice(chosen, effort);
|
|
605
|
-
if (advice)
|
|
606
|
-
tui.warn(` ${advice}`);
|
|
607
|
-
};
|
|
608
|
-
for (;;) {
|
|
609
|
-
const input = await tui.readInput();
|
|
610
|
-
if (input.startsWith("/")) {
|
|
611
|
-
const [cmd, ...rest] = input.slice(1).split(/\s+/);
|
|
612
|
-
const arg = rest[0];
|
|
613
|
-
switch (cmd) {
|
|
614
|
-
case "exit":
|
|
615
|
-
case "quit":
|
|
616
|
-
case "q":
|
|
617
|
-
await shutdown();
|
|
618
|
-
return;
|
|
619
|
-
case "help":
|
|
620
|
-
tui.println(HELP);
|
|
621
|
-
break;
|
|
622
|
-
case "models":
|
|
623
|
-
case "model":
|
|
624
|
-
await switchModel(arg);
|
|
625
|
-
break;
|
|
626
|
-
case "mode":
|
|
627
|
-
await setMode(arg);
|
|
628
|
-
break;
|
|
629
|
-
case "effort":
|
|
630
|
-
await setEffort(arg);
|
|
631
|
-
break;
|
|
632
|
-
case "plan":
|
|
633
|
-
if (toolCtx.plan.exists)
|
|
634
|
-
tui.planUpdated(toolCtx.plan);
|
|
635
|
-
else
|
|
636
|
-
tui.status("· no plan yet — the agent creates one when it starts a multi-step task");
|
|
637
|
-
break;
|
|
638
|
-
case "tasks":
|
|
639
|
-
tui.println(taskManager.list());
|
|
640
|
-
break;
|
|
641
|
-
case "logs":
|
|
642
|
-
tui.println(taskManager.logs(arg ?? "", Number(rest[1]) || 50));
|
|
643
|
-
break;
|
|
644
|
-
case "stop":
|
|
645
|
-
tui.println(taskManager.stop(arg ?? ""));
|
|
646
|
-
break;
|
|
647
|
-
case "compact":
|
|
648
|
-
tui.startSpinner("compacting");
|
|
649
|
-
await agent.compactNow();
|
|
650
|
-
tui.stopSpinner();
|
|
651
|
-
tui.status(`· compacted — ctx now ${agent.contextPercent()}%`);
|
|
652
|
-
break;
|
|
653
|
-
case "context":
|
|
654
|
-
tui.status(`· ctx ${agent.contextPercent()}% of ${chosen.contextWindow.toLocaleString()} tokens · ${agent.messages.length} messages`);
|
|
655
|
-
break;
|
|
656
|
-
case "clear":
|
|
657
|
-
agent.resetTranscript();
|
|
658
|
-
toolCtx.plan.reset();
|
|
659
|
-
tui.status("· conversation cleared");
|
|
660
|
-
break;
|
|
661
|
-
default:
|
|
662
|
-
tui.warn(`Unknown command /${cmd} — try /help`);
|
|
663
|
-
}
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
try {
|
|
667
|
-
await agent.runTurn(input);
|
|
668
347
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
348
|
+
}
|
|
349
|
+
const hub = new hub_1.WebHub({ port, prefs: prefsOf(args), help: HELP, version: VERSION });
|
|
350
|
+
try {
|
|
351
|
+
await hub.start();
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
if (err?.code === "EADDRINUSE") {
|
|
355
|
+
console.error(`\nPort ${port} is already in use. Pick another with: smol --web ${port + 1}`);
|
|
356
|
+
process.exit(1);
|
|
674
357
|
}
|
|
358
|
+
throw err;
|
|
675
359
|
}
|
|
360
|
+
process.on("exit", () => hub.shutdownSync());
|
|
361
|
+
installSignalCleanup(() => hub.shutdownSync());
|
|
362
|
+
if (autoStart)
|
|
363
|
+
hub.openSession(workspace);
|
|
364
|
+
console.log(`\n smolcoder web UI: ${hub.url()}\n ${autoStart ? `workspace ${workspace}` : "pick a workspace in the sidebar"} · ctrl+c stops the server\n`);
|
|
676
365
|
}
|
|
677
366
|
main().catch((err) => {
|
|
678
367
|
console.error(err);
|