agent-from-zero 0.35.1 → 0.35.3
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 +141 -0
- package/dist/agent.js +43 -26
- package/dist/completion.js +93 -3
- package/dist/config.js +38 -1
- package/dist/context.js +46 -6
- package/dist/ink/app.js +158 -30
- package/dist/ink/chat.js +3 -1
- package/dist/ink/commands.js +6 -3
- package/dist/ink/completion.js +13 -5
- package/dist/ink/setup.js +16 -15
- package/dist/ink/skills-panel.js +86 -0
- package/dist/loop.js +34 -8
- package/dist/mcp.js +332 -16
- package/dist/skills.js +282 -41
- package/dist/ui.js +8 -1
- package/package.json +2 -1
package/dist/ink/app.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { stripVTControlCharacters } from "node:util"; // plain text of a styled status line, to find the verb
|
|
2
3
|
import { statusCommand } from "../status.js";
|
|
3
4
|
import React, { useState, useEffect, useRef } from "react";
|
|
4
5
|
import { Box, Text, Static, useInput, useApp } from "ink";
|
|
@@ -9,8 +10,9 @@ import { initFormState, reduceForm, renderForm, collectAnswers } from "../form.j
|
|
|
9
10
|
import { CONFIG, saveGlobalSetting } from "../config.js"; // session allowlist + /model save
|
|
10
11
|
import { effortMenu, setEffort } from "../effort.js";
|
|
11
12
|
import { useSlashCompletion } from "./completion.js";
|
|
13
|
+
import { commandNames, commandTokenRanges, inlineCommandGhost } from "../completion.js"; // which /command tokens to paint + the mid-text ghost suggestion
|
|
12
14
|
import { TerminateReason, listAgentViews } from "../loop.js"; // how a turn can end
|
|
13
|
-
import { compactHistory,
|
|
15
|
+
import { compactHistory, compactThreshold, contextWindowFor } from "../context.js"; // /compact + /model info
|
|
14
16
|
import { forgetFilesExcept } from "../tools.js"; // /clear resets the file read-state
|
|
15
17
|
import { clearUndo } from "../undo.js";
|
|
16
18
|
import { clearTodos } from "../todos.js";
|
|
@@ -23,7 +25,8 @@ import { cronItemsPending, consumeCronQueue, cronTriggerContent } from "../cron.
|
|
|
23
25
|
import { newSessionId, saveSession, listSessions, loadSession, setSessionTitle } from "../session.js";
|
|
24
26
|
import { generateSessionTitle, setTerminalTitle } from "../title.js"; // concise session name, generated after the first message + the terminal tab that shows it
|
|
25
27
|
import { isPlanMode, setPlanMode } from "../permissions.js";
|
|
26
|
-
import { findSkill,
|
|
28
|
+
import { findSkill, recordSkillUsage, skillSource, skillUsageScores, userSkillMessages } from "../skills.js";
|
|
29
|
+
import { SkillsPanel } from "./skills-panel.js"; // /skills: Claude Code's on / user-only / off manager
|
|
27
30
|
import { extractMemories } from "../memory.js";
|
|
28
31
|
import { displayWidth } from "../editor.js"; // display-width measurement (CJK-aware)
|
|
29
32
|
import { runHooks } from "../hooks.js";
|
|
@@ -37,10 +40,17 @@ import { detailPage, summarizeActivity } from "./activity.js";
|
|
|
37
40
|
import { useTerminalSize } from "./viewport.js";
|
|
38
41
|
import { makeInkSink } from "./sink.js"; // turns the loop's output into React state
|
|
39
42
|
import { runInfoCommand, mcpStatusText } from "./commands.js"; // the non-interactive slash commands + /mcp status
|
|
40
|
-
import { listMcpServers, mcpActionsFor, runMcpAction } from "../mcp.js"; // /mcp: list + per-server actions
|
|
43
|
+
import { listMcpServers, MCP_SUBCOMMANDS, mcpActionsFor, mcpProblems, mcpServerDetails, reloadMcpServers, runMcpAction } from "../mcp.js"; // /mcp: list + per-server actions
|
|
41
44
|
// Injected when plan mode turns on, so the model knows the rules it now lives in.
|
|
42
45
|
const PLAN_MODE_NOTICE = `[plan mode ON] Investigate this request using only read-only tools — read_file, search, and safe read-only shell (ls, cat, git status). Do NOT write files, edit, or run mutating commands; the permission gate will block them. When you have a concrete, ordered plan, call the exit_plan_mode tool with that plan. The user reviews and approves it before you make any change.`;
|
|
43
|
-
|
|
46
|
+
// No ✳ (U+2733): it has an emoji presentation, and terminals that honour it draw
|
|
47
|
+
// it two cells wide — the text after the glyph then jumps on every other frame.
|
|
48
|
+
const SPINNER_FRAMES = ["·", "✢", "✶", "✻", "✽", "✻", "✶", "✢"];
|
|
49
|
+
const SPINNER_COLOR = "#D77757"; // Claude Code's orange
|
|
50
|
+
const SKILL_COLOR = "#B1B9F9"; // Claude Code's lavender for a recognised /skill in the input
|
|
51
|
+
const SHIMMER_PEAK = "#FFD2BD"; // the highlight that sweeps across the verb
|
|
52
|
+
const SHIMMER_TICK_MS = 100; // one animation clock for glyph + shimmer → one repaint per tick
|
|
53
|
+
const SHIMMER_RADIUS = 3; // how many cells the highlight fades over on each side
|
|
44
54
|
// Cap the live streaming preview to a terminal-aware tail. THIS IS LOAD-BEARING:
|
|
45
55
|
// Ink redraws the whole dynamic region (everything below <Static>) on every
|
|
46
56
|
// token, and when that region grows TALLER than the terminal, Ink can no longer
|
|
@@ -56,13 +66,49 @@ function liveTail(s, rows, columns, queued) {
|
|
|
56
66
|
const page = detailPage(s, 0, maxLines, Math.max(1, columns - 2));
|
|
57
67
|
return (page.pages > 1 ? "… " : "") + page.text;
|
|
58
68
|
}
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
// A shared animation clock. One interval drives both the glyph and the shimmer,
|
|
70
|
+
// so the live region repaints once per tick instead of once per effect.
|
|
71
|
+
function useTick() {
|
|
72
|
+
const [tick, setTick] = useState(0);
|
|
61
73
|
useEffect(() => {
|
|
62
|
-
const id = setInterval(() =>
|
|
74
|
+
const id = setInterval(() => setTick((t) => t + 1), SHIMMER_TICK_MS);
|
|
63
75
|
return () => clearInterval(id);
|
|
64
76
|
}, []);
|
|
65
|
-
return
|
|
77
|
+
return tick;
|
|
78
|
+
}
|
|
79
|
+
// The glyph lives in a fixed two-cell box: whatever the frame, the text after it
|
|
80
|
+
// starts in the same column.
|
|
81
|
+
function Glyph({ tick }) {
|
|
82
|
+
return (_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: SPINNER_COLOR, children: SPINNER_FRAMES[Math.floor(tick / 2) % SPINNER_FRAMES.length] }) }));
|
|
83
|
+
}
|
|
84
|
+
function Spinner() {
|
|
85
|
+
return _jsx(Glyph, { tick: useTick() });
|
|
86
|
+
}
|
|
87
|
+
// Blend two #rrggbb colours; t = 0 → a, 1 → b.
|
|
88
|
+
function mix(a, b, t) {
|
|
89
|
+
const ch = (hex, i) => parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16);
|
|
90
|
+
return "#" + [0, 1, 2].map((i) => Math.round(ch(a, i) + (ch(b, i) - ch(a, i)) * t).toString(16).padStart(2, "0")).join("");
|
|
91
|
+
}
|
|
92
|
+
// The light sweep: each character is coloured by its distance from a moving
|
|
93
|
+
// highlight. Only COLOUR changes between frames — never a character, never a
|
|
94
|
+
// width — so the line shimmers without a single cell moving.
|
|
95
|
+
export function shimmer(text, pos) {
|
|
96
|
+
return [...text].map((c, i) => {
|
|
97
|
+
const glow = Math.max(0, 1 - Math.abs(i - pos) / SHIMMER_RADIUS);
|
|
98
|
+
return chalk.hex(mix(SPINNER_COLOR, SHIMMER_PEAK, glow))(c);
|
|
99
|
+
}).join("");
|
|
100
|
+
}
|
|
101
|
+
// "✶ Thinking… (3s · ↓ 281 tokens · thought for 1s)": the verb (everything up to
|
|
102
|
+
// and including the first "…") shimmers; the stats after it stay put. A pause
|
|
103
|
+
// between sweeps (the highlight travels past the end) keeps it calm.
|
|
104
|
+
function SpinnerLine({ text }) {
|
|
105
|
+
const tick = useTick();
|
|
106
|
+
const plain = stripVTControlCharacters(text);
|
|
107
|
+
const cut = plain.indexOf("…");
|
|
108
|
+
const verb = cut >= 0 && text.startsWith(plain.slice(0, cut + 1)) ? plain.slice(0, cut + 1) : ""; // shimmer only an unstyled verb
|
|
109
|
+
const period = verb.length + SHIMMER_RADIUS * 2 + 8;
|
|
110
|
+
const pos = (tick % period) - SHIMMER_RADIUS;
|
|
111
|
+
return (_jsxs(Box, { children: [_jsx(Glyph, { tick: tick }), _jsxs(Text, { color: SPINNER_COLOR, wrap: "truncate-end", children: [verb ? shimmer(verb, pos) : "", text.slice(verb.length)] })] }));
|
|
66
112
|
}
|
|
67
113
|
// One committed line of conversation. `user` is a highlight bar; `answer` is a
|
|
68
114
|
// finished reply rendered as markdown; `note` is already-formatted narration.
|
|
@@ -88,6 +134,24 @@ function StatusBar({ model, dir, branch, status }) {
|
|
|
88
134
|
];
|
|
89
135
|
return (_jsx(Text, { wrap: "truncate-end", children: parts.map((p, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx(Text, { dimColor: true, children: " · " }), p] }, i))) }));
|
|
90
136
|
}
|
|
137
|
+
// Bottom-right MCP health, like Claude Code's footer notice: red when a
|
|
138
|
+
// server failed, yellow when one needs sign-in, dim while servers are still
|
|
139
|
+
// connecting in the background; nothing at all when every server is fine.
|
|
140
|
+
// Re-read on every render (the app ticks once a second), so it follows
|
|
141
|
+
// background connects, /mcp actions and settings reloads with no wiring.
|
|
142
|
+
function McpBadge() {
|
|
143
|
+
const { failed, needsAuth, pending } = mcpProblems();
|
|
144
|
+
const parts = [];
|
|
145
|
+
if (failed.length)
|
|
146
|
+
parts.push(_jsxs(Text, { color: "red", children: ["\u2717 MCP failed: ", failed.join(", ")] }, "f"));
|
|
147
|
+
if (needsAuth.length)
|
|
148
|
+
parts.push(_jsxs(Text, { color: "yellow", children: ["\u25B3 ", needsAuth.join(", "), " needs auth"] }, "a"));
|
|
149
|
+
if (pending)
|
|
150
|
+
parts.push(_jsxs(Text, { dimColor: true, children: ["\u25CC connecting ", pending, " MCP server", pending === 1 ? "" : "s", "\u2026"] }, "p"));
|
|
151
|
+
if (!parts.length)
|
|
152
|
+
return null;
|
|
153
|
+
return (_jsx(Box, { flexShrink: 1, marginLeft: 2, children: _jsxs(Text, { wrap: "truncate-end", children: [parts.map((p, i) => _jsxs(React.Fragment, { children: [i > 0 && _jsx(Text, { dimColor: true, children: " · " }), p] }, i)), failed.length || needsAuth.length ? _jsx(Text, { dimColor: true, children: " · /mcp" }) : null] }) }));
|
|
154
|
+
}
|
|
91
155
|
// One human note per non-Done ending, so the user always learns why a turn stopped.
|
|
92
156
|
const EXIT_NOTES = {
|
|
93
157
|
[TerminateReason.ReviewLimit]: "Repeated automatic denials — stopped. Tell the agent how to proceed; prohibited actions were not executed.",
|
|
@@ -102,7 +166,7 @@ const EXIT_NOTES = {
|
|
|
102
166
|
};
|
|
103
167
|
export function App({ session, runTurn, clipboard }) {
|
|
104
168
|
const { rows, columns } = useTerminalSize();
|
|
105
|
-
const { client, messages, systemMessage, costMeter, skills, judge, autoMode, model, dir, branch, getStatus } = session;
|
|
169
|
+
const { client, messages, systemMessage, costMeter, skills: liveSkills, allSkills: everySkill, judge, autoMode, model, dir, branch, getStatus } = session;
|
|
106
170
|
// Seed the scrollback with the welcome banner + the dim startup notices.
|
|
107
171
|
const [items, setItems] = useState(() => [{ kind: "note", text: session.bannerText }, ...session.notices.map((n) => ({ kind: "note", text: chalk.dim(n) }))]);
|
|
108
172
|
const [status, setStatus] = useState(null); // the live spinner line
|
|
@@ -149,9 +213,12 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
149
213
|
modelListEndpoint.current = CONFIG.baseURL;
|
|
150
214
|
void client.models.list().then((page) => setCompletionModels(page.data.map((m) => m.id))).catch(() => { });
|
|
151
215
|
}, [wantsModels, client]);
|
|
216
|
+
const typeaheadSkills = liveSkills().map((s) => ({ name: s.name, description: s.description, whenToUse: s.whenToUse, argNames: s.argNames, argumentHint: s.argumentHint, source: skillSource(s) }));
|
|
152
217
|
const completion = useSlashCompletion(input, completing, {
|
|
153
|
-
model: CONFIG.model, skills, models: completionModels, servers: listMcpServers().map((s) => s.name),
|
|
154
|
-
}, rows - 10);
|
|
218
|
+
model: CONFIG.model, skills: typeaheadSkills, models: completionModels, servers: listMcpServers().map((s) => s.name), usage: completing && input.startsWith("/") ? skillUsageScores() : undefined,
|
|
219
|
+
}, rows - 10, columns);
|
|
220
|
+
// A "/" later in the text: no list, just the rest of the best match as dim ghost text (Tab accepts).
|
|
221
|
+
const ghost = completing && cursor === input.length ? inlineCommandGhost(input, { skills: typeaheadSkills }) : null;
|
|
155
222
|
useEffect(() => {
|
|
156
223
|
const id = setInterval(() => setTick((t) => t + 1), 1000);
|
|
157
224
|
return () => clearInterval(id);
|
|
@@ -162,10 +229,10 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
162
229
|
setTerminalTitle(session.initialTitle);
|
|
163
230
|
}, [session.initialTitle]);
|
|
164
231
|
// Open a one-of-N menu and run `onChoose` with the picked index (-1 = cancel).
|
|
165
|
-
const openSelect = (header, options, onChoose) => {
|
|
232
|
+
const openSelect = (header, options, onChoose, body) => {
|
|
166
233
|
setDetails(null);
|
|
167
234
|
setMenuSel(0);
|
|
168
|
-
setPending({ kind: "select", header, options, onChoose });
|
|
235
|
+
setPending({ kind: "select", header, body, options, onChoose });
|
|
169
236
|
};
|
|
170
237
|
// Run one conversation turn through the real loop. `display` is what scrolls up
|
|
171
238
|
// as the prompt (a user highlight bar, or a note for /skill); the loop appends
|
|
@@ -314,7 +381,7 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
314
381
|
setFormState(null);
|
|
315
382
|
if (pending.kind === "form")
|
|
316
383
|
pending.resolve(null);
|
|
317
|
-
else
|
|
384
|
+
else if (pending.kind === "select")
|
|
318
385
|
pending.onChoose(-1);
|
|
319
386
|
}
|
|
320
387
|
if (immediate)
|
|
@@ -336,12 +403,24 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
336
403
|
// /mcp — list configured servers + status, then pick one to manage it.
|
|
337
404
|
const handleMcpCommand = async (arg) => {
|
|
338
405
|
const parts = arg.trim().split(/\s+/).filter(Boolean);
|
|
406
|
+
// /mcp reload — re-read settings.json now (the file watcher normally does this on save).
|
|
407
|
+
if (parts[0] === "reload") {
|
|
408
|
+
setBusy(true);
|
|
409
|
+
try {
|
|
410
|
+
const changes = await reloadMcpServers();
|
|
411
|
+
note(chalk.dim(changes.length ? `(mcp: ${changes.join(", ")})` : "(mcp: no changes in settings.json)"));
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
setBusy(false);
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
339
418
|
// Direct subcommands (scriptable, matching Claude Code): /mcp reconnect|auth|enable|disable <name>
|
|
340
|
-
if (parts.length >= 2 && [
|
|
419
|
+
if (parts.length >= 2 && MCP_SUBCOMMANDS[parts[0]]) {
|
|
341
420
|
const name = parts.slice(1).join(" ");
|
|
342
421
|
setBusy(true);
|
|
343
422
|
try {
|
|
344
|
-
const action =
|
|
423
|
+
const action = MCP_SUBCOMMANDS[parts[0]];
|
|
345
424
|
const msg = await runMcpAction(name, action, (url) => note(chalk.dim(`authenticate in your browser: ${url}`)));
|
|
346
425
|
note(chalk.dim(msg));
|
|
347
426
|
}
|
|
@@ -364,7 +443,7 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
364
443
|
return note(chalk.dim("(cancelled)"));
|
|
365
444
|
const server = servers[i];
|
|
366
445
|
const actions = mcpActionsFor(server);
|
|
367
|
-
openSelect(`${server.name}
|
|
446
|
+
openSelect(`${server.name} MCP Server`, actions.map((a) => a.label), (j) => {
|
|
368
447
|
if (j < 0)
|
|
369
448
|
return note(chalk.dim("(cancelled)"));
|
|
370
449
|
setBusy(true);
|
|
@@ -372,7 +451,7 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
372
451
|
.then((msg) => note(chalk.dim(msg)))
|
|
373
452
|
.catch((err) => note(chalk.yellow(err.message)))
|
|
374
453
|
.finally(() => setBusy(false));
|
|
375
|
-
});
|
|
454
|
+
}, mcpServerDetails(server)); // Claude Code's server screen: status, auth, protocol, URL, config, capabilities, tools
|
|
376
455
|
});
|
|
377
456
|
};
|
|
378
457
|
const handleCommand = async (line) => {
|
|
@@ -393,6 +472,11 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
393
472
|
}
|
|
394
473
|
if (line.startsWith("/skills "))
|
|
395
474
|
line = `/skill ${line.slice(8).trim()}`;
|
|
475
|
+
if (line === "/skills") {
|
|
476
|
+
setDetails(null);
|
|
477
|
+
setPending({ kind: "skills" });
|
|
478
|
+
return true;
|
|
479
|
+
} // Claude Code's manager: on / user-only / off per skill
|
|
396
480
|
if (line === "/effort" || line.startsWith("/effort ")) {
|
|
397
481
|
const target = CONFIG.model;
|
|
398
482
|
const value = line.slice("/effort".length).trim();
|
|
@@ -418,12 +502,12 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
418
502
|
setAutoEnabled(autoMode.enabled);
|
|
419
503
|
return true;
|
|
420
504
|
}
|
|
421
|
-
const info = runInfoCommand(line, { skills, costMeter }); // /help /cost /memory /stats /todos /bg /team /tasks /skills /undo /diff
|
|
505
|
+
const info = runInfoCommand(line, { skills: liveSkills(), costMeter }); // /help /cost /memory /stats /todos /bg /team /tasks /skills /undo /diff
|
|
422
506
|
if (info !== null) {
|
|
423
507
|
note(info);
|
|
424
508
|
return true;
|
|
425
509
|
}
|
|
426
|
-
// /mcp [reconnect|auth|enable|disable <name>]
|
|
510
|
+
// /mcp [reload | reconnect|auth|enable|disable <name>]
|
|
427
511
|
if (line === "/mcp" || line.startsWith("/mcp ")) {
|
|
428
512
|
await handleMcpCommand(line.slice("/mcp".length).trim());
|
|
429
513
|
return true;
|
|
@@ -448,7 +532,7 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
448
532
|
apply(name);
|
|
449
533
|
return true;
|
|
450
534
|
}
|
|
451
|
-
note(chalk.dim(`model: ${CONFIG.model}\nendpoint: ${CONFIG.baseURL}\ncontext window: ${
|
|
535
|
+
note(chalk.dim(`model: ${CONFIG.model}\nendpoint: ${CONFIG.baseURL}\ncontext window: ${contextWindowFor()} tokens (auto-compact at ~${compactThreshold()})`));
|
|
452
536
|
let models = [];
|
|
453
537
|
try {
|
|
454
538
|
const page = await client.models.list();
|
|
@@ -468,9 +552,12 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
468
552
|
});
|
|
469
553
|
return true;
|
|
470
554
|
}
|
|
471
|
-
// /skill <name> — run a skill on the user's behalf (works for user-only
|
|
555
|
+
// /skill <name> [args] — run a skill on the user's behalf (works for user-only
|
|
556
|
+
// skills). Claude Code's pair: a <command-name> marker, then the body.
|
|
472
557
|
if (line === "/skill" || line.startsWith("/skill ")) {
|
|
473
|
-
const
|
|
558
|
+
const skills = liveSkills(); // live — a SKILL.md added mid-session is found
|
|
559
|
+
const [name = "", ...rest] = line.slice("/skill".length).trim().split(/\s+/);
|
|
560
|
+
const args = rest.join(" ");
|
|
474
561
|
if (!name) {
|
|
475
562
|
note(chalk.dim(skills.length ? `usage: /skill <name> — available: ${skills.map((s) => s.name).join(", ")}` : "(no skills in this project — add one at .mini-agent/skills/<name>/SKILL.md)"));
|
|
476
563
|
return true;
|
|
@@ -480,8 +567,9 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
480
567
|
note(chalk.yellow(`(no skill named "${name}")`));
|
|
481
568
|
return true;
|
|
482
569
|
}
|
|
483
|
-
autoMode.recordRequest(
|
|
484
|
-
|
|
570
|
+
autoMode.recordRequest(`/${s.name}${args ? ` ${args}` : ""}`);
|
|
571
|
+
recordSkillUsage(s.name); // feeds the "/" typeahead's most-used ranking
|
|
572
|
+
runConversationTurn(userSkillMessages(s, args), { kind: "note", text: chalk.dim(`(running skill: ${s.name})`) });
|
|
485
573
|
return true;
|
|
486
574
|
}
|
|
487
575
|
switch (line) {
|
|
@@ -562,6 +650,8 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
562
650
|
return true;
|
|
563
651
|
}
|
|
564
652
|
default:
|
|
653
|
+
if (line.startsWith("/") && findSkill(liveSkills(), line.slice(1).split(/\s+/)[0]))
|
|
654
|
+
return handleCommand(`/skill ${line.slice(1)}`); // /<skill-name> [args], like Claude Code
|
|
565
655
|
if (line.startsWith("/")) {
|
|
566
656
|
note(chalk.dim(`unknown command: ${line} — try /help`));
|
|
567
657
|
return true; // consumed — don't send typos to the model
|
|
@@ -586,6 +676,8 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
586
676
|
tuiCleanup();
|
|
587
677
|
return exit(); // Ctrl+C quits immediately (process.on exit cleans up MCP/background)
|
|
588
678
|
}
|
|
679
|
+
if (pending?.kind === "skills")
|
|
680
|
+
return; // the /skills panel owns the keyboard while it is open
|
|
589
681
|
const modifiedReturn = char === "[13;5u" || char === "[27;5;13~";
|
|
590
682
|
// A submitted follow-up may still be passing hooks. A second Enter must
|
|
591
683
|
// not approve the old action while that asynchronous preparation runs.
|
|
@@ -714,6 +806,13 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
714
806
|
return;
|
|
715
807
|
}
|
|
716
808
|
}
|
|
809
|
+
if (key.tab && ghost) { // accept the inline suggestion as "/name "
|
|
810
|
+
const next = input.slice(0, ghost.start) + `/${ghost.full} `;
|
|
811
|
+
setInput(next);
|
|
812
|
+
setCursor(next.length);
|
|
813
|
+
setHistIdx(null);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
717
816
|
const completionAction = completion.handleKey(key, (value) => { setInput(value); setCursor(value.length); setHistIdx(null); });
|
|
718
817
|
if (completionAction.handled && !completionAction.submit)
|
|
719
818
|
return;
|
|
@@ -854,7 +953,7 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
854
953
|
const agentDetailView = detailPage(selectedAgent?.transcript || "(Waiting for activity…)", agentPage, Math.max(1, Math.min(10, rows - 10)), Math.max(1, columns - 4));
|
|
855
954
|
const detailView = detailPage(details === "tools" ? getToolCalls() ?? "No tool calls this turn." : details === "reasoning" ? getReasoning() ?? "No thinking recorded this turn." : "", detailOffset, Math.max(1, Math.min(12, rows - 9)), Math.max(1, columns - 4));
|
|
856
955
|
const activity = rows >= 20 && busy && !details && !pending && !subAgentDetail && !followUps.size ? summarizeActivity(getToolActivity(), Date.now(), rows >= 30 && !agents.length ? 2 : 1) : [];
|
|
857
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: items, children: (item, i) => _jsx(ItemView, { item: item }, i) }), _jsxs(Box, { flexDirection: "column", height: !pending && (details || selectedAgent) ? Math.max(1, rows - 1) : undefined, overflow: "hidden", children: [live !== null && !details && !pending && !selectedAgent && (_jsxs(Box, { marginTop: rows >= 20 ? 1 : 0, flexDirection: "row", children: [_jsx(Text, { color: "green", children: "\u23FA " }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { children: live === "" ? chalk.dim("…") : liveTail(live, rows, columns, followUps.size > 0) }) })] })), activity.map((group, i) => (_jsxs(Box, { flexDirection: "column", marginTop: i === 0 ? 1 : 0, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: group.failed ? "yellow" : group.running ? "cyan" : "green", children: "\u25CF " }), _jsx(Text, { bold: true, children: group.title })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" \u23BF ", group.detail] })] }, i))), !details && !pending && !selectedAgent && (status !== null || busy) && (
|
|
956
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: items, children: (item, i) => _jsx(ItemView, { item: item }, i) }), _jsxs(Box, { flexDirection: "column", height: !pending && (details || selectedAgent) ? Math.max(1, rows - 1) : undefined, overflow: "hidden", children: [live !== null && !details && !pending && !selectedAgent && (_jsxs(Box, { marginTop: rows >= 20 ? 1 : 0, flexDirection: "row", children: [_jsx(Text, { color: "green", children: "\u23FA " }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { children: live === "" ? chalk.dim("…") : liveTail(live, rows, columns, followUps.size > 0) }) })] })), activity.map((group, i) => (_jsxs(Box, { flexDirection: "column", marginTop: i === 0 ? 1 : 0, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: group.failed ? "yellow" : group.running ? "cyan" : "green", children: "\u25CF " }), _jsx(Text, { bold: true, children: group.title })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" \u23BF ", group.detail] })] }, i))), !details && !pending && !selectedAgent && (status !== null || busy) && (_jsx(Box, { marginTop: rows >= 20 ? 1 : 0, children: _jsx(SpinnerLine, { text: status?.replace("Ctrl+C to interrupt", "Esc to interrupt") ?? (live !== null ? "Writing…" : "Working…") }) })), rows >= 20 && !details && !selectedAgent && !pending && (busy || getToolCallCount() > 0) && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", getToolCallCount(), " tool calls \u00B7 Ctrl+T details \u00B7 Ctrl+R thinking", busy ? " · Esc interrupt" : ""] })), details && !pending && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [details === "tools" ? "Tool details" : "Thinking", " \u00B7 ", detailView.pages - detailView.page, "/", detailView.pages, " \u00B7 \u2191\u2193 page \u00B7 Esc close", followUps.size ? ` · ${followUps.size} queued` : ""] }), _jsx(Text, { children: detailView.text })] })), selectedAgent && !pending && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "blue", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "cyan", wrap: "truncate-end", children: [selectedAgent.name, " \u00B7 ", selectedAgent.model, " \u00B7 ", selectedAgent.status, " \u00B7 Esc main"] }), _jsxs(Box, { children: [selectedAgent.status === "running" && _jsx(Spinner, {}), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", selectedAgent.activity, " \u00B7 ", selectedAgent.toolCalls, " tools \u00B7 PgUp/PgDn details"] })] }), _jsx(Text, { children: agentDetailView.text })] })), pending?.kind === "select" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: pending.header }), pending.body && _jsx(Box, { marginY: 1, children: _jsx(Text, { children: pending.body }) }), _jsx(Text, { children: renderMenu(pending.options, menuSel) }), _jsx(Text, { children: MENU_HINT })] })), pending?.kind === "skills" && (_jsx(SkillsPanel, { skills: everySkill(), maxRows: Math.max(3, rows - 14), onClose: (summary) => { setPending(null); note(chalk.dim(summary)); } })), pending?.kind === "form" && formState && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: renderForm(pending.questions, formState) }) })), pending && pending.kind !== "skills" && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Type a follow-up \u00B7 Enter sends text; empty input selects \u00B7 Esc cancels" }), !details && !selectedAgent && !pending && followUps.size > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: rows >= 20 ? 1 : 0, children: [_jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Messages queued for the next tool boundary \u00B7 Esc / Ctrl+Enter interrupt and send" }), followUps.pending.slice(rows >= 20 ? -2 : -1).map((message, i) => _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" \u21B3 ", message.displayText ?? message.text] }, i)), followUps.size > 2 && _jsxs(Text, { dimColor: true, children: [" \u2026 ", followUps.size - 2, " earlier messages queued"] })] })), pending?.kind !== "skills" && (_jsx(Box, { borderStyle: "round", borderColor: planMode ? "magenta" : CONFIG.bypassPermissions ? "red" : autoEnabled ? "yellow" : "cyan", paddingX: 1, marginTop: rows >= 20 ? 1 : 0, flexDirection: "column", flexShrink: 0, children: (() => {
|
|
858
957
|
const prompt = "❯ ";
|
|
859
958
|
const promptColor = planMode ? "magenta" : CONFIG.bypassPermissions ? "red" : autoEnabled ? "yellow" : "cyan";
|
|
860
959
|
const cols = columns;
|
|
@@ -863,6 +962,7 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
863
962
|
const firstW = Math.max(1, innerWidth - promptW);
|
|
864
963
|
// Build visual lines display-width-aware (CJK, emoji = 2 cols).
|
|
865
964
|
const lines = [""];
|
|
965
|
+
const lineStarts = [0]; // code-unit offset where each visual line begins (for skill highlighting)
|
|
866
966
|
let row = 0;
|
|
867
967
|
let col = 0;
|
|
868
968
|
// Cursor tracking: map code-unit offset → (line, display-column).
|
|
@@ -882,6 +982,7 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
882
982
|
row++;
|
|
883
983
|
col = 0;
|
|
884
984
|
lines[row] = "";
|
|
985
|
+
lineStarts[row] = cu;
|
|
885
986
|
}
|
|
886
987
|
lines[row] += ch;
|
|
887
988
|
col += cw;
|
|
@@ -896,9 +997,35 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
896
997
|
if (cursorCol >= edge) {
|
|
897
998
|
cursorLine++;
|
|
898
999
|
cursorCol = 0;
|
|
899
|
-
if (lines.length <= cursorLine)
|
|
1000
|
+
if (lines.length <= cursorLine) {
|
|
900
1001
|
lines.push("");
|
|
1002
|
+
lineStarts.push(cu);
|
|
1003
|
+
}
|
|
901
1004
|
}
|
|
1005
|
+
// "/<skill>" tokens anywhere in the draft are painted — the typeahead
|
|
1006
|
+
// only opens at the start, but a real skill name lights up wherever
|
|
1007
|
+
// it is. paint() splits a slice of a line into plain and lit runs.
|
|
1008
|
+
const lit = commandTokenRanges(input, commandNames({ skills: liveSkills() }));
|
|
1009
|
+
const isLit = (at) => lit.some(([a, b]) => at >= a && at < b);
|
|
1010
|
+
const paint = (text, from, keyBase) => {
|
|
1011
|
+
const runs = [];
|
|
1012
|
+
let run = "";
|
|
1013
|
+
let runLit = false;
|
|
1014
|
+
let at = from;
|
|
1015
|
+
const flush = () => { if (run)
|
|
1016
|
+
runs.push(_jsx(Text, { color: runLit ? SKILL_COLOR : undefined, children: run }, `${keyBase}${runs.length}`)); run = ""; };
|
|
1017
|
+
for (const ch of text) {
|
|
1018
|
+
const l = isLit(at);
|
|
1019
|
+
if (l !== runLit) {
|
|
1020
|
+
flush();
|
|
1021
|
+
runLit = l;
|
|
1022
|
+
}
|
|
1023
|
+
run += ch;
|
|
1024
|
+
at += ch.length;
|
|
1025
|
+
}
|
|
1026
|
+
flush();
|
|
1027
|
+
return runs;
|
|
1028
|
+
};
|
|
902
1029
|
// Map display-column back to code-unit offset within the target line.
|
|
903
1030
|
const cursorLineText = lines[cursorLine] ?? "";
|
|
904
1031
|
let cuOff = 0; // code-unit offset into cursorLineText that matches cursorCol
|
|
@@ -919,9 +1046,10 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
919
1046
|
const before = line.slice(0, cuOff);
|
|
920
1047
|
const at = line.slice(cuOff, cuOff + 1) || " ";
|
|
921
1048
|
const after = line.slice(cuOff + 1);
|
|
922
|
-
|
|
1049
|
+
const base = lineStarts[i] ?? 0;
|
|
1050
|
+
return (_jsxs(Box, { children: [i === 0 && _jsx(Text, { color: promptColor, children: prompt }), _jsx(Text, { children: paint(before, base, "b") }), _jsx(Text, { inverse: true, color: isLit(base + cuOff) ? SKILL_COLOR : undefined, children: ghost ? ghost.suffix[0] : at }), _jsx(Text, { children: paint(after, base + cuOff + at.length, "a") }), ghost && _jsx(Text, { dimColor: true, children: ghost.suffix.slice(1) })] }, i));
|
|
923
1051
|
}
|
|
924
|
-
return (_jsxs(Box, { children: [i === 0 && _jsx(Text, { color: promptColor, children: prompt }), _jsx(Text, { children: line })] }, i));
|
|
1052
|
+
return (_jsxs(Box, { children: [i === 0 && _jsx(Text, { color: promptColor, children: prompt }), _jsx(Text, { children: paint(line, lineStarts[i] ?? 0, "l") })] }, i));
|
|
925
1053
|
});
|
|
926
|
-
})() }), completion.view, rows >= 20 && busy && !details && !selectedAgent && !pending && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Enter queue follow-up \u00B7 Ctrl+Enter interrupt and send now" }), rows >= 20 && !details && !selectedAgent && !pending && !completion.visible && _jsx(AgentList, { agents: agents, focus: subAgentFocus, viewing: subAgentDetail, mainBusy: busy, maxRows: Math.max(2, Math.min(3, rows - 24)) }), _jsx(StatusBar, { model: CONFIG.model, dir: dir, branch: branch, status: getStatus() }), (planMode || autoEnabled || CONFIG.bypassPermissions) && (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: planMode ? "magenta" : CONFIG.bypassPermissions ? "red" : "yellow", children: planMode ? "⏸ plan mode on" : CONFIG.bypassPermissions ? "▶▶ bypass permissions on" : "▶▶ auto mode on" }), _jsx(Text, { dimColor: true, children: planMode ? " (/plan to toggle)" : CONFIG.bypassPermissions ? " (this run)" : " (/auto to toggle)" })] }))] })] }));
|
|
1054
|
+
})() })), completion.view, rows >= 20 && busy && !details && !selectedAgent && !pending && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Enter queue follow-up \u00B7 Ctrl+Enter interrupt and send now" }), rows >= 20 && !details && !selectedAgent && !pending && !completion.visible && _jsx(AgentList, { agents: agents, focus: subAgentFocus, viewing: subAgentDetail, mainBusy: busy, maxRows: Math.max(2, Math.min(3, rows - 24)) }), _jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 1, children: _jsx(StatusBar, { model: CONFIG.model, dir: dir, branch: branch, status: getStatus() }) }), _jsx(McpBadge, {})] }), (planMode || autoEnabled || CONFIG.bypassPermissions) && (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: planMode ? "magenta" : CONFIG.bypassPermissions ? "red" : "yellow", children: planMode ? "⏸ plan mode on" : CONFIG.bypassPermissions ? "▶▶ bypass permissions on" : "▶▶ auto mode on" }), _jsx(Text, { dimColor: true, children: planMode ? " (/plan to toggle)" : CONFIG.bypassPermissions ? " (this run)" : " (/auto to toggle)" })] }))] })] }));
|
|
927
1055
|
}
|
package/dist/ink/chat.js
CHANGED
|
@@ -8,11 +8,13 @@ import { userContent } from "../images.js";
|
|
|
8
8
|
// rest. All screen output flows through hooks.output (the Ink sink).
|
|
9
9
|
export function makeRunTurn(client, messages) {
|
|
10
10
|
let reviewBudget = new AutoReviewBudget();
|
|
11
|
+
// input: one user message, or several in order (a /skill run sends the
|
|
12
|
+
// <command-name> marker, then the skill body); images ride on the first.
|
|
11
13
|
return async (input, hooks) => {
|
|
12
14
|
if (input !== null)
|
|
13
15
|
reviewBudget = new AutoReviewBudget();
|
|
14
16
|
if (input !== null)
|
|
15
|
-
messages.push({ role: "user", content: userContent(
|
|
17
|
+
[input].flat().forEach((text, i) => messages.push({ role: "user", content: i === 0 ? userContent(text, hooks.images ?? []) : text }));
|
|
16
18
|
return runLoop(messages, {
|
|
17
19
|
client,
|
|
18
20
|
model: CONFIG.model,
|
package/dist/ink/commands.js
CHANGED
|
@@ -25,7 +25,8 @@ export const SESSION_HELP = `commands:
|
|
|
25
25
|
/memory show the durable facts the agent remembers about this project
|
|
26
26
|
/status session usage and vendor account balance/cost
|
|
27
27
|
/cost tokens, cache hit rate and estimated spend this session (local)
|
|
28
|
-
/mcp list configured MCP servers + status; select one to authenticate / reconnect / disable
|
|
28
|
+
/mcp list configured MCP servers + status; select one to view tools / authenticate / clear auth / reconnect / disable
|
|
29
|
+
/mcp reload re-read mcpServers from settings.json (saves are also picked up automatically)
|
|
29
30
|
/plan toggle plan mode — research-only; the agent presents a plan you approve before any change
|
|
30
31
|
/effort [level] list/select the current model’s supported reasoning effort
|
|
31
32
|
/auto toggle auto mode — risky or uncertain actions still require approval
|
|
@@ -38,8 +39,8 @@ export const SESSION_HELP = `commands:
|
|
|
38
39
|
/undo revert the most recent file write (write_file / edit_file) this session
|
|
39
40
|
/diff show every file changed this session, as a diff from where it started
|
|
40
41
|
/resume list recent sessions in this project and continue one of them
|
|
41
|
-
/skills
|
|
42
|
-
/skill <name> run a skill yourself (works even for user-only skills)
|
|
42
|
+
/skills manage skills: turn each on, off, or user-only (search with /, sort with t)
|
|
43
|
+
/skill <name> [args] run a skill yourself (works even for user-only skills); /<name> [args] works too
|
|
43
44
|
exit leave (Ctrl+C does the same)
|
|
44
45
|
|
|
45
46
|
keys (during or after a turn):
|
|
@@ -57,6 +58,8 @@ keys (during or after a turn):
|
|
|
57
58
|
// Colored status word for an MCP server (shared by the Ink + readline /mcp UIs).
|
|
58
59
|
export function mcpStatusText(info) {
|
|
59
60
|
switch (info.status) {
|
|
61
|
+
case "pending":
|
|
62
|
+
return chalk.dim("connecting…");
|
|
60
63
|
case "connected":
|
|
61
64
|
return chalk.green("connected");
|
|
62
65
|
case "needs-auth":
|
package/dist/ink/completion.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { slashCompletions } from "../completion.js";
|
|
5
5
|
// Keep the full list in state, but render only a window around the selection.
|
|
6
6
|
// Long lists must never grow Ink's redraw region beyond the terminal height.
|
|
7
|
-
export function useSlashCompletion(input, enabled, context, maxRows) {
|
|
7
|
+
export function useSlashCompletion(input, enabled, context, maxRows, columns = 80) {
|
|
8
8
|
const [selection, setSelection] = useState({ input: "", index: 0 });
|
|
9
9
|
const [dismissed, setDismissed] = useState(null);
|
|
10
10
|
useEffect(() => { setDismissed(null); }, [input]);
|
|
11
11
|
const menu = enabled && dismissed !== input ? slashCompletions(input, context) : null;
|
|
12
12
|
const items = menu?.items ?? [];
|
|
13
13
|
const selected = Math.min(selection.input === input ? selection.index : 0, Math.max(0, items.length - 1));
|
|
14
|
-
const count = Math.max(1, Math.min(
|
|
14
|
+
const count = Math.max(1, Math.min(6, maxRows)); // Claude Code shows at most 6, centred on the selection
|
|
15
15
|
const start = Math.max(0, Math.min(selected - Math.floor(count / 2), items.length - count));
|
|
16
|
+
const nameWidth = Math.min(Math.max(0, ...items.map((item) => item.value.length)) + 2, Math.floor(columns * 0.4));
|
|
16
17
|
return {
|
|
17
18
|
visible: menu !== null,
|
|
18
19
|
handleKey: (key, replace) => {
|
|
@@ -30,14 +31,21 @@ export function useSlashCompletion(input, enabled, context, maxRows) {
|
|
|
30
31
|
}
|
|
31
32
|
if (key.tab || (key.return && !key.ctrl && !key.meta)) {
|
|
32
33
|
const item = items[selected];
|
|
34
|
+
// Tab always just fills it in. Enter runs it — unless it takes
|
|
35
|
+
// arguments (a skill with `arguments`, or a command with sub-choices),
|
|
36
|
+
// then it fills "/name " so you can type them. (Claude Code's rule.)
|
|
33
37
|
if (key.tab || item.children)
|
|
34
|
-
replace(item.value +
|
|
38
|
+
replace(item.value + " ");
|
|
35
39
|
else
|
|
36
40
|
return { handled: true, submit: item.value };
|
|
37
41
|
return { handled: true };
|
|
38
42
|
}
|
|
39
43
|
return { handled: false };
|
|
40
44
|
},
|
|
41
|
-
view: menu && (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [items.slice(start, start + count).map((item, i) =>
|
|
45
|
+
view: menu && (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [items.slice(start, start + count).map((item, i) => {
|
|
46
|
+
const on = start + i === selected;
|
|
47
|
+
// Names in one column (at most 40% of the width); the description is one line, cut to fit.
|
|
48
|
+
return (_jsxs(Text, { color: on ? "#B1B9F9" : undefined, dimColor: !on, wrap: "truncate-end", children: [on ? "› " : " ", item.value.padEnd(nameWidth), item.description.replace(/\s+/g, " ")] }, item.value));
|
|
49
|
+
}), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: items.length ? `↑↓ choose · Tab complete · Enter select · Esc close · ${selected + 1}/${items.length}` : menu.hint || "No matching commands" })] })),
|
|
42
50
|
};
|
|
43
51
|
}
|
package/dist/ink/setup.js
CHANGED
|
@@ -3,18 +3,18 @@ import path from "node:path";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { CONFIG, requireApiKey } from "../config.js";
|
|
5
5
|
import { buildSystemMessage, readProjectInstructions } from "../prompt.js";
|
|
6
|
-
import {
|
|
6
|
+
import { contextPercent } from "../context.js";
|
|
7
7
|
import { initCostMeter, DEFAULT_PRICING } from "../cost.js";
|
|
8
8
|
import { gitBranch } from "../tui.js";
|
|
9
9
|
import { newSessionId, latestSession } from "../session.js";
|
|
10
10
|
import { initTelemetry, emit } from "../telemetry.js";
|
|
11
11
|
import { runHooks } from "../hooks.js";
|
|
12
|
-
import { connectMcpServers } from "../mcp.js";
|
|
12
|
+
import { connectMcpServers, watchMcpConfig } from "../mcp.js";
|
|
13
13
|
import { Judge } from "../judge.js";
|
|
14
14
|
import { AutoMode } from "../auto.js";
|
|
15
15
|
import { registerExternalTool } from "../tools.js";
|
|
16
16
|
import { rememberTool, readMemory } from "../memory.js";
|
|
17
|
-
import {
|
|
17
|
+
import { allSkills, currentSkills } from "../skills.js";
|
|
18
18
|
import { killAllBackground } from "../background.js";
|
|
19
19
|
import { killAllSubAgents } from "../loop.js";
|
|
20
20
|
import { loadDurableJobs, startCronScheduler, stopCronScheduler, listJobs } from "../cron.js"; // cron scheduler (Day s14)
|
|
@@ -52,10 +52,12 @@ export async function buildInkSession(opts = {}) {
|
|
|
52
52
|
}
|
|
53
53
|
initTelemetry(sessionId);
|
|
54
54
|
emit("agent_session_start", { mode: "repl" });
|
|
55
|
-
// MCP servers
|
|
56
|
-
//
|
|
57
|
-
|
|
55
|
+
// MCP servers connect in the BACKGROUND, like Claude Code: the prompt is
|
|
56
|
+
// ready at once, each server's tools join the next model call the moment it
|
|
57
|
+
// connects, and a failure is flagged in the status bar (red, bottom right).
|
|
58
|
+
const disconnectMcp = await connectMcpServers({ background: true });
|
|
58
59
|
process.on("exit", disconnectMcp);
|
|
60
|
+
process.on("exit", watchMcpConfig()); // hot reload: a settings.json save adds/removes/reconnects servers, no restart
|
|
59
61
|
process.on("exit", killAllBackground); // SIGKILL any background job so it never outlives the agent
|
|
60
62
|
process.on("exit", killAllSubAgents); // mark any still-running sub-agents as killed
|
|
61
63
|
process.on("exit", stopCronScheduler); // Day s14: stop the setInterval on exit
|
|
@@ -75,13 +77,11 @@ export async function buildInkSession(opts = {}) {
|
|
|
75
77
|
const memCount = readMemory().length;
|
|
76
78
|
if (memCount)
|
|
77
79
|
notices.push(`(long-term memory: ${memCount} facts loaded)`);
|
|
78
|
-
// Skills
|
|
79
|
-
//
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
if (skills.length)
|
|
84
|
-
notices.push(`(skills: ${skills.length} loaded — ${skills.map((s) => s.name).join(", ")})`);
|
|
80
|
+
// Skills are re-read before every model call (no restart needed); the loop
|
|
81
|
+
// registers the `skill` tool and sends the listing. Just announce them here.
|
|
82
|
+
const startupSkills = currentSkills();
|
|
83
|
+
if (startupSkills.length)
|
|
84
|
+
notices.push(`(skills: ${startupSkills.length} loaded — ${startupSkills.map((s) => s.name).join(", ")})`);
|
|
85
85
|
const costMeter = initCostMeter({
|
|
86
86
|
inputPerM: CONFIG.pricing.inputPerM ?? DEFAULT_PRICING.inputPerM,
|
|
87
87
|
cachedInputPerM: CONFIG.pricing.cachedInputPerM ?? DEFAULT_PRICING.cachedInputPerM,
|
|
@@ -94,7 +94,7 @@ export async function buildInkSession(opts = {}) {
|
|
|
94
94
|
const dir = path.basename(process.cwd());
|
|
95
95
|
const branch = gitBranch();
|
|
96
96
|
const getStatus = () => ({
|
|
97
|
-
ctxPct:
|
|
97
|
+
ctxPct: contextPercent(messages), // % of the way to auto-compaction
|
|
98
98
|
cost: costMeter.cost(),
|
|
99
99
|
elapsedMs: Date.now() - startedAt,
|
|
100
100
|
});
|
|
@@ -106,7 +106,8 @@ export async function buildInkSession(opts = {}) {
|
|
|
106
106
|
initialTitle,
|
|
107
107
|
startedAt,
|
|
108
108
|
costMeter,
|
|
109
|
-
skills,
|
|
109
|
+
skills: () => currentSkills(),
|
|
110
|
+
allSkills: () => allSkills(),
|
|
110
111
|
judge,
|
|
111
112
|
autoMode,
|
|
112
113
|
model: CONFIG.model,
|