agent-from-zero 0.29.0 → 0.30.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/dist/agent.js +13 -10
- package/dist/editor.js +6 -0
- package/dist/loop.js +19 -6
- package/dist/markdown.js +37 -2
- package/dist/tui.js +29 -7
- package/dist/ui.js +8 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -19,8 +19,8 @@ import { clearTodos, getTodos, renderTodos } from "./todos.js"; // the agent's p
|
|
|
19
19
|
import { renderDiff } from "./diff.js"; // show what /undo put back / what /diff changed, reusing the Day 21 diff renderer
|
|
20
20
|
import path from "node:path"; // shorten paths for the /diff summary
|
|
21
21
|
import { expandMentions } from "./mentions.js"; // @file mentions: pull referenced files into context (secret files refused)
|
|
22
|
-
import { banner, framedPrompt, formatModelChoices, statusLine
|
|
23
|
-
import { gitBranch, toggleLastCollapsed, revealReasoning, clearReasoning, cleanup as tuiCleanup } from "./tui.js"; // git branch
|
|
22
|
+
import { banner, framedPrompt, formatModelChoices, statusLine } from "./ui.js"; // welcome box, prompt, model labels, status line
|
|
23
|
+
import { gitBranch, toggleLastCollapsed, revealReasoning, clearReasoning, revealToolCalls, clearToolCalls, cleanup as tuiCleanup } from "./tui.js"; // git branch + collapsible output + collapsed reasoning (Ctrl+R) + folded tool-call trace (Ctrl+T)
|
|
24
24
|
import { promptSelect, promptForm } from "./menu.js"; // arrow-key approval menu + multi-question form
|
|
25
25
|
import { editLine } from "./editor.js"; // our own line editor (keeps the status footer pinned even when input wraps)
|
|
26
26
|
import { normalizeDroppedPaths } from "./drop.js"; // drag-and-drop: a dropped file's path → a clean absolute path in the input
|
|
@@ -83,6 +83,7 @@ const SESSION_HELP = `commands:
|
|
|
83
83
|
|
|
84
84
|
keys (at the prompt):
|
|
85
85
|
Ctrl+R reveal the model's thinking for the last answer (collapsed behind a spinner by default)
|
|
86
|
+
Ctrl+T reveal the folded tool-call trace for the last answer (read_file/search/… shown as a tally)
|
|
86
87
|
Tab expand/collapse the most recent tool output`;
|
|
87
88
|
// Injected when the user turns plan mode on, so the model knows the rules of the
|
|
88
89
|
// mode it now lives in (the permission gate enforces them regardless).
|
|
@@ -258,7 +259,7 @@ async function main() {
|
|
|
258
259
|
// so the status bar stays pinned below however the line wraps.
|
|
259
260
|
editing = true;
|
|
260
261
|
try {
|
|
261
|
-
const res = await editLine(rl, { prompt, footer, history, onTab: () => toggleLastCollapsed(), onReveal: () => revealReasoning(), transformPaste: normalizeDroppedPaths });
|
|
262
|
+
const res = await editLine(rl, { prompt, footer, history, onTab: () => toggleLastCollapsed(), onReveal: () => revealReasoning(), onTools: () => revealToolCalls(), transformPaste: normalizeDroppedPaths });
|
|
262
263
|
if (res.type === "line") {
|
|
263
264
|
if (res.value.trim())
|
|
264
265
|
history.push(res.value); // remember non-empty entries for ↑/↓
|
|
@@ -443,6 +444,7 @@ async function main() {
|
|
|
443
444
|
resetTeam(); // the team belonged to the old task — forget it (mailboxes re-wipe on next use)
|
|
444
445
|
resetBoard(); // and the shared task board (Day 40)
|
|
445
446
|
clearReasoning(); // drop the collapsed thinking too
|
|
447
|
+
clearToolCalls(); // ...and the folded tool-call trace
|
|
446
448
|
sessionId = newSessionId(); // a fresh conversation is a fresh session file
|
|
447
449
|
console.log(chalk.dim("(history cleared)")); // confirm the reset
|
|
448
450
|
return true;
|
|
@@ -635,16 +637,16 @@ async function main() {
|
|
|
635
637
|
});
|
|
636
638
|
}
|
|
637
639
|
while (true) {
|
|
638
|
-
// The input area
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
640
|
+
// The input area: a blank line for breathing room, the "❯" you type on, then
|
|
641
|
+
// a single status line (model · 📁 dir · 🌿 branch · ctx% · $ · time) pinned
|
|
642
|
+
// BELOW the input. We dropped the old top/bottom horizontal rules — two bare
|
|
643
|
+
// lines around an empty prompt just looked like noise. Only in a TTY; piped
|
|
644
|
+
// input stays clean.
|
|
643
645
|
let footer;
|
|
644
646
|
if (process.stdin.isTTY) {
|
|
645
647
|
const ctxPct = Math.min(100, Math.round((estimateHistoryTokens(messages) / CONFIG.contextWindow) * 100));
|
|
646
|
-
console.log("
|
|
647
|
-
footer =
|
|
648
|
+
console.log(""); // one blank line separates the previous answer from the prompt
|
|
649
|
+
footer = statusLine(CONFIG.model, path.basename(process.cwd()), gitBranch(), ctxPct, costMeter.cost(), Date.now() - sessionStartedAt); // status pinned below the prompt
|
|
648
650
|
}
|
|
649
651
|
const line = (await readUserLine(framedPrompt(isPlanMode()), footer)).trim(); // next input, or "exit" on EOF
|
|
650
652
|
if (!line)
|
|
@@ -677,6 +679,7 @@ async function main() {
|
|
|
677
679
|
console.log(chalk.yellow(`(refused secret file${refused.length === 1 ? "" : "s"}: ${refused.join(", ")})`));
|
|
678
680
|
messages.push({ role: "user", content: augmented + injected }); // the new turn (with any attached files + hook context) joins the shared history
|
|
679
681
|
clearReasoning(); // Ctrl+R should reveal THIS turn's thinking, not the previous answer's
|
|
682
|
+
clearToolCalls(); // ...and Ctrl+T should reveal THIS turn's tool calls
|
|
680
683
|
running = true; // Ctrl+C now means "interrupt the task"
|
|
681
684
|
interrupted = false; // fresh interrupt state for this task
|
|
682
685
|
controller = new AbortController(); // fresh abort signal for this task
|
package/dist/editor.js
CHANGED
|
@@ -28,6 +28,8 @@ export function reduceEditor(s, str, key) {
|
|
|
28
28
|
return { state: { buffer, cursor: Math.min(buffer.length, cursor + 1) }, action: "edit" }; // forward one char
|
|
29
29
|
case "r":
|
|
30
30
|
return { state: s, action: "reveal" }; // Ctrl+R → reveal the model's collapsed thinking
|
|
31
|
+
case "t":
|
|
32
|
+
return { state: s, action: "tools" }; // Ctrl+T → reveal the folded tool-call trace
|
|
31
33
|
default:
|
|
32
34
|
return { state: s, action: "none" };
|
|
33
35
|
}
|
|
@@ -294,6 +296,10 @@ export function editLine(rl, opts) {
|
|
|
294
296
|
opts.onReveal?.(); // print the model's collapsed thinking above us, then repaint
|
|
295
297
|
first = true; // the next paint lands fresh, not a stale climb-up
|
|
296
298
|
return draw();
|
|
299
|
+
case "tools":
|
|
300
|
+
opts.onTools?.(); // print the folded tool-call trace above us, then repaint
|
|
301
|
+
first = true;
|
|
302
|
+
return draw();
|
|
297
303
|
case "edit":
|
|
298
304
|
return draw();
|
|
299
305
|
case "none":
|
package/dist/loop.js
CHANGED
|
@@ -12,7 +12,7 @@ import { emit } from "./telemetry.js"; // local-only event log (no-op unless the
|
|
|
12
12
|
import { runHooks } from "./hooks.js"; // user lifecycle hooks (PreToolUse / PostToolUse / Stop)
|
|
13
13
|
import { recordUsage } from "./cost.js"; // meter token usage from the stream
|
|
14
14
|
import { mark, thinkingWord, spinnerText } from "./ui.js"; // centralized terminal styling (markers, spinner)
|
|
15
|
-
import {
|
|
15
|
+
import { recordToolCall, recordReasoning } from "./tui.js"; // folded tool-call trace (Ctrl+T) + the model's hidden thinking (Ctrl+R)
|
|
16
16
|
import { MarkdownStream } from "./markdown.js"; // render the streamed answer as terminal markdown
|
|
17
17
|
import { todoNag, getTodos, renderTodos } from "./todos.js"; // the agent's plan: show it on screen + nag when it goes stale
|
|
18
18
|
import { pendingNotifications } from "./background.js"; // background tasks (Day 37): surface finished jobs as a turn
|
|
@@ -458,17 +458,25 @@ opts) {
|
|
|
458
458
|
spinner.stop(); // and never leave a zombie spinner
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
|
+
// How this agent's activity is prefixed on screen: teammate tagged by name,
|
|
462
|
+
// plain sub-agent nested in blue, top-level flush-left. Shared by the per-call
|
|
463
|
+
// recording and the per-round tally so they line up.
|
|
464
|
+
function agentIndent(opts) {
|
|
465
|
+
return opts.teammate ? chalk.magenta(` ⎿ [${opts.teammate.name}] `) : opts.subAgent ? chalk.blue(" ⎿ ") : "";
|
|
466
|
+
}
|
|
461
467
|
// Run one tool call end to end: log it, run it through the permission gate,
|
|
462
468
|
// ask the human if needed, execute, and return the paired result. Read-only
|
|
463
469
|
// calls (allow/deny only, never "ask") are safe to run via this from inside a
|
|
464
470
|
// Promise.all batch; calls that can prompt must be awaited one at a time.
|
|
465
471
|
async function runOneCall(call, opts) {
|
|
466
|
-
const indent =
|
|
472
|
+
const indent = agentIndent(opts);
|
|
467
473
|
if (!opts.quiet) {
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
474
|
+
// The announcement is FOLDED (Day-after-40 UI fix): instead of printing a
|
|
475
|
+
// line per call (which floods the screen on a big exploration), we record it
|
|
476
|
+
// for the per-round tally + Ctrl+T. Diffs / prompts / results below still
|
|
477
|
+
// print live. A generous slice keeps Ctrl+T readable without dumping megabytes.
|
|
478
|
+
const line = indent + mark.tool(call.name, call.args.length > 300 ? call.args.slice(0, 297) + "..." : call.args);
|
|
479
|
+
recordToolCall(line, `tool: ${call.name}\nargs: ${call.args}`);
|
|
472
480
|
}
|
|
473
481
|
// The permission gate sits between the model's intent and execution.
|
|
474
482
|
const v = checkPermission(call.name, call.args);
|
|
@@ -1224,6 +1232,11 @@ opts) {
|
|
|
1224
1232
|
const r = await runOneCall(out.toolCalls[i++], opts);
|
|
1225
1233
|
messages.push({ role: "tool", tool_call_id: r.id, content: r.content });
|
|
1226
1234
|
}
|
|
1235
|
+
// Fold this round's tool calls into ONE tally line instead of one line per
|
|
1236
|
+
// call (the per-call announcements were recorded for Ctrl+T in runOneCall).
|
|
1237
|
+
if (!opts.quiet && out.toolCalls.length) {
|
|
1238
|
+
console.log(agentIndent(opts) + mark.toolTally(out.toolCalls.map((c) => c.name)) + chalk.dim(" · Ctrl+T"));
|
|
1239
|
+
}
|
|
1227
1240
|
// Background tasks (Day 37): if a job the model started has finished, slip
|
|
1228
1241
|
// its <task_notification> in here, right after this round's tool results —
|
|
1229
1242
|
// the model sees the outcome on the next round without ever having blocked
|
package/dist/markdown.js
CHANGED
|
@@ -75,8 +75,12 @@ export function renderMarkdown(md, width = termWidth()) {
|
|
|
75
75
|
const w = Math.max(20, width);
|
|
76
76
|
const m = new Marked();
|
|
77
77
|
m.use(renderer(w));
|
|
78
|
-
// Override the table
|
|
79
|
-
//
|
|
78
|
+
// Override the table AND list renderers AFTER marked-terminal so ours win.
|
|
79
|
+
// marked-terminal v7 predates marked v15's token-object renderer API, so its
|
|
80
|
+
// built-in table/list renderers receive tokens they don't understand and spit
|
|
81
|
+
// out the RAW markdown — literal `**bold**`, `` `code` ``, and `*` bullets
|
|
82
|
+
// inside list items (and unaligned tables). We render both ourselves, calling
|
|
83
|
+
// parseInline so the inline markup inside cells / list items actually renders.
|
|
80
84
|
m.use({
|
|
81
85
|
renderer: {
|
|
82
86
|
table(token) {
|
|
@@ -84,6 +88,37 @@ export function renderMarkdown(md, width = termWidth()) {
|
|
|
84
88
|
const rows = token.rows.map((r) => r.map((c) => this.parser.parseInline(c.tokens)));
|
|
85
89
|
return renderTable(header, rows, w) + "\n";
|
|
86
90
|
},
|
|
91
|
+
list(token) {
|
|
92
|
+
// Render items at a given nesting depth: a bullet (• / N.) + the item's
|
|
93
|
+
// inline content (bold/code/links parsed), wrapped to width with a hanging
|
|
94
|
+
// indent so continuation lines sit under the text. Nested lists recurse.
|
|
95
|
+
const parser = this.parser;
|
|
96
|
+
const render = (list, depth) => {
|
|
97
|
+
const pad = " ".repeat(depth);
|
|
98
|
+
const lines = [];
|
|
99
|
+
list.items.forEach((item, idx) => {
|
|
100
|
+
const bullet = list.ordered ? `${(typeof list.start === "number" ? list.start : 1) + idx}.` : "•";
|
|
101
|
+
const box = item.task ? (item.checked ? "[x] " : "[ ] ") : "";
|
|
102
|
+
let inline = "";
|
|
103
|
+
const nested = [];
|
|
104
|
+
for (const t of item.tokens) {
|
|
105
|
+
if (t.type === "list")
|
|
106
|
+
nested.push(...render(t, depth + 1));
|
|
107
|
+
else if (t.tokens)
|
|
108
|
+
inline += parser.parseInline(t.tokens);
|
|
109
|
+
else
|
|
110
|
+
inline += t.text ?? "";
|
|
111
|
+
}
|
|
112
|
+
const head = `${pad}${bullet} ${box}`;
|
|
113
|
+
const wrapped = wrapAnsi(head + inline.trim(), Math.max(10, w), { hard: true, trim: false }).split("\n");
|
|
114
|
+
const hang = " ".repeat(displayWidth(head)); // align wrapped lines under the text
|
|
115
|
+
lines.push(...wrapped.map((ln, i) => (i === 0 ? ln : hang + ln.replace(/^\s+/, ""))));
|
|
116
|
+
lines.push(...nested);
|
|
117
|
+
});
|
|
118
|
+
return lines;
|
|
119
|
+
};
|
|
120
|
+
return render(token, 0).join("\n") + "\n\n";
|
|
121
|
+
},
|
|
87
122
|
},
|
|
88
123
|
});
|
|
89
124
|
const out = m.parse(md, { async: false });
|
package/dist/tui.js
CHANGED
|
@@ -35,13 +35,6 @@ export function gitBranch() {
|
|
|
35
35
|
}
|
|
36
36
|
const collapsed = [];
|
|
37
37
|
const MAX_COLLAPSED = 50; // bound the memory — old blocks fall off
|
|
38
|
-
// Print the one-line summary now; stash the full text for Tab to reveal later.
|
|
39
|
-
export function printToolSummary(summary, full) {
|
|
40
|
-
process.stdout.write(summary + "\n");
|
|
41
|
-
collapsed.push({ full, expanded: false });
|
|
42
|
-
if (collapsed.length > MAX_COLLAPSED)
|
|
43
|
-
collapsed.shift();
|
|
44
|
-
}
|
|
45
38
|
// Tab handler: reveal (or re-hide) the full text of the most recent tool block.
|
|
46
39
|
// Returns false if there's nothing to toggle.
|
|
47
40
|
export function toggleLastCollapsed() {
|
|
@@ -85,6 +78,35 @@ export function revealReasoning() {
|
|
|
85
78
|
process.stdout.write(chalk.dim(reasoningTrace.join("\n\n— — —\n\n").replace(/^/gm, " ")) + "\n");
|
|
86
79
|
return true;
|
|
87
80
|
}
|
|
81
|
+
// ---- collapsed tool calls (the activity trace) -------------------------------
|
|
82
|
+
// A long task fires dozens of read_file/search calls; printing one line each
|
|
83
|
+
// floods the screen. Instead the loop prints a compact per-round tally and we
|
|
84
|
+
// keep the full call lines here for Ctrl+T to reveal. Cleared per turn, so
|
|
85
|
+
// Ctrl+T shows the calls behind the LAST answer. (Diffs, prompts, and results
|
|
86
|
+
// still print live — only the one-line announcements are folded.)
|
|
87
|
+
const toolCallLog = [];
|
|
88
|
+
const MAX_TOOL_LOG = 500; // bound the memory
|
|
89
|
+
// Record a tool call's full line for Ctrl+T, and stash its args for Tab.
|
|
90
|
+
export function recordToolCall(line, full) {
|
|
91
|
+
toolCallLog.push(line);
|
|
92
|
+
if (toolCallLog.length > MAX_TOOL_LOG)
|
|
93
|
+
toolCallLog.shift();
|
|
94
|
+
collapsed.push({ full, expanded: false }); // Tab still reveals the most recent call's full args
|
|
95
|
+
if (collapsed.length > MAX_COLLAPSED)
|
|
96
|
+
collapsed.shift();
|
|
97
|
+
}
|
|
98
|
+
export function clearToolCalls() {
|
|
99
|
+
toolCallLog.length = 0;
|
|
100
|
+
}
|
|
101
|
+
// Ctrl+T handler: print every tool call behind the last answer. Returns false
|
|
102
|
+
// (nothing to show) so the editor can ignore the key.
|
|
103
|
+
export function revealToolCalls() {
|
|
104
|
+
if (!toolCallLog.length)
|
|
105
|
+
return false;
|
|
106
|
+
process.stdout.write(chalk.dim(`tool calls this turn (${toolCallLog.length}):\n`));
|
|
107
|
+
process.stdout.write(toolCallLog.join("\n") + "\n");
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
88
110
|
// ---- cleanup -----------------------------------------------------------------
|
|
89
111
|
// Called on exit. Nothing fancy to restore now (we no longer hijack the bottom
|
|
90
112
|
// of the screen), but keep the hook so callers don't have to change if that
|
package/dist/ui.js
CHANGED
|
@@ -52,6 +52,14 @@ export const mark = {
|
|
|
52
52
|
teammateStart: (name, role) => chalk.magenta(" ⎿ teammate ") + chalk.bold(name) + chalk.magenta(` spawned`) + chalk.dim(` — ${role}`), // Day 38
|
|
53
53
|
teammateDone: (name, ok) => (ok ? chalk.magenta(" ⎿ teammate ") + chalk.bold(name) + chalk.magenta(" finished") : chalk.yellow(" ⎿ teammate ") + chalk.bold(name) + chalk.yellow(" ended (not done)")),
|
|
54
54
|
inbox: (n) => chalk.magenta(" ⎿ ") + chalk.magenta(`${n} team message${n === 1 ? "" : "s"} received`),
|
|
55
|
+
// A round's tool calls folded into one line: "⏺ 6 tools · read_file ×4 · search ×2" (Ctrl+T expands them).
|
|
56
|
+
toolTally: (names) => {
|
|
57
|
+
const counts = new Map();
|
|
58
|
+
for (const n of names)
|
|
59
|
+
counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
60
|
+
const parts = [...counts.entries()].map(([n, c]) => `${n} ×${c}`).join(" · ");
|
|
61
|
+
return chalk.cyan("⏺ ") + chalk.dim(`${names.length} tool${names.length === 1 ? "" : "s"} · ${parts}`);
|
|
62
|
+
},
|
|
55
63
|
note: (s) => chalk.dim(s), // dim asides (resumed, attached, cleared…)
|
|
56
64
|
warn: (s) => chalk.yellow(s),
|
|
57
65
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-from-zero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Build a Claude Code-style CLI agent from scratch, with AI as your pair programmer — companion code for a 10-day tutorial (DeepSeek by default, any OpenAI-compatible model)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|