agent-from-zero 0.28.0 → 0.29.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 +10 -3
- package/dist/drop.js +104 -0
- package/dist/editor.js +12 -0
- package/dist/loop.js +38 -17
- package/dist/tui.js +27 -0
- package/package.json +2 -2
package/dist/agent.js
CHANGED
|
@@ -20,9 +20,10 @@ import { renderDiff } from "./diff.js"; // show what /undo put back / what /diff
|
|
|
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
22
|
import { banner, framedPrompt, formatModelChoices, statusLine, inputRule } from "./ui.js"; // welcome box, prompt, model labels, status line
|
|
23
|
-
import { gitBranch, toggleLastCollapsed, cleanup as tuiCleanup } from "./tui.js"; // git branch for the status line + collapsible output
|
|
23
|
+
import { gitBranch, toggleLastCollapsed, revealReasoning, clearReasoning, cleanup as tuiCleanup } from "./tui.js"; // git branch for the status line + collapsible output + collapsed reasoning (Ctrl+R)
|
|
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
|
+
import { normalizeDroppedPaths } from "./drop.js"; // drag-and-drop: a dropped file's path → a clean absolute path in the input
|
|
26
27
|
import { rememberTool, readMemory, readMemoryTyped, extractMemories, MEMORY_PATH } from "./memory.js"; // long-term project memory + auto-extract
|
|
27
28
|
import { loadSkills, buildSkillTool, findSkill, skillInstructions } from "./skills.js"; // Markdown-as-plugin skills
|
|
28
29
|
import { initCostMeter, DEFAULT_PRICING } from "./cost.js"; // token & cost accounting for /cost
|
|
@@ -78,7 +79,11 @@ const SESSION_HELP = `commands:
|
|
|
78
79
|
/resume list recent sessions in this project and continue one of them
|
|
79
80
|
/skills list the reusable skills available in this project
|
|
80
81
|
/skill <name> run a skill yourself (works even for user-only skills)
|
|
81
|
-
exit leave (Ctrl+C at the prompt does the same)
|
|
82
|
+
exit leave (Ctrl+C at the prompt does the same)
|
|
83
|
+
|
|
84
|
+
keys (at the prompt):
|
|
85
|
+
Ctrl+R reveal the model's thinking for the last answer (collapsed behind a spinner by default)
|
|
86
|
+
Tab expand/collapse the most recent tool output`;
|
|
82
87
|
// Injected when the user turns plan mode on, so the model knows the rules of the
|
|
83
88
|
// mode it now lives in (the permission gate enforces them regardless).
|
|
84
89
|
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.`;
|
|
@@ -253,7 +258,7 @@ async function main() {
|
|
|
253
258
|
// so the status bar stays pinned below however the line wraps.
|
|
254
259
|
editing = true;
|
|
255
260
|
try {
|
|
256
|
-
const res = await editLine(rl, { prompt, footer, history, onTab: () => toggleLastCollapsed() });
|
|
261
|
+
const res = await editLine(rl, { prompt, footer, history, onTab: () => toggleLastCollapsed(), onReveal: () => revealReasoning(), transformPaste: normalizeDroppedPaths });
|
|
257
262
|
if (res.type === "line") {
|
|
258
263
|
if (res.value.trim())
|
|
259
264
|
history.push(res.value); // remember non-empty entries for ↑/↓
|
|
@@ -437,6 +442,7 @@ async function main() {
|
|
|
437
442
|
clearTodos(); // the plan belonged to the old task — drop it
|
|
438
443
|
resetTeam(); // the team belonged to the old task — forget it (mailboxes re-wipe on next use)
|
|
439
444
|
resetBoard(); // and the shared task board (Day 40)
|
|
445
|
+
clearReasoning(); // drop the collapsed thinking too
|
|
440
446
|
sessionId = newSessionId(); // a fresh conversation is a fresh session file
|
|
441
447
|
console.log(chalk.dim("(history cleared)")); // confirm the reset
|
|
442
448
|
return true;
|
|
@@ -670,6 +676,7 @@ async function main() {
|
|
|
670
676
|
if (refused.length)
|
|
671
677
|
console.log(chalk.yellow(`(refused secret file${refused.length === 1 ? "" : "s"}: ${refused.join(", ")})`));
|
|
672
678
|
messages.push({ role: "user", content: augmented + injected }); // the new turn (with any attached files + hook context) joins the shared history
|
|
679
|
+
clearReasoning(); // Ctrl+R should reveal THIS turn's thinking, not the previous answer's
|
|
673
680
|
running = true; // Ctrl+C now means "interrupt the task"
|
|
674
681
|
interrupted = false; // fresh interrupt state for this task
|
|
675
682
|
controller = new AbortController(); // fresh abort signal for this task
|
package/dist/drop.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Drag-and-drop a file onto the terminal and it pastes the file's path into the
|
|
2
|
+
// input — but as a SHELL token: backslash-escaped spaces (/Users/me/my\ file.txt)
|
|
3
|
+
// or wrapped in quotes, and usually with a trailing space. That's noisy to read
|
|
4
|
+
// and clumsy to hand to the agent. This turns a dropped path (or several) into
|
|
5
|
+
// clean ABSOLUTE paths, so the user never types a long path by hand.
|
|
6
|
+
//
|
|
7
|
+
// It is deliberately conservative: it only rewrites text that is, after
|
|
8
|
+
// unescaping, one or more EXISTING ABSOLUTE paths (that's exactly what a terminal
|
|
9
|
+
// drag produces). Anything else — ordinary pasted prose, a relative snippet — is
|
|
10
|
+
// returned untouched, so this can run on every paste without surprising anyone.
|
|
11
|
+
import fs from "node:fs"; // confirm the tokens are real paths (the drag signal)
|
|
12
|
+
import os from "node:os"; // expand a leading ~
|
|
13
|
+
import path from "node:path"; // resolve + normalize to absolute
|
|
14
|
+
// Terminals with "bracketed paste" wrap the text in these markers; strip them so
|
|
15
|
+
// a drop still parses if the mode happens to be on.
|
|
16
|
+
const BRACKET_PASTE = /\x1b\[20[01]~/g;
|
|
17
|
+
// Split a dropped string into path tokens, honoring the escaping a terminal adds:
|
|
18
|
+
// backslash escapes (\<space>, \( …) and single/double quotes. Whitespace OUTSIDE
|
|
19
|
+
// a quote/escape separates tokens (so several dropped files become several paths).
|
|
20
|
+
function splitTokens(raw) {
|
|
21
|
+
const s = raw.replace(BRACKET_PASTE, "").trim();
|
|
22
|
+
const tokens = [];
|
|
23
|
+
let cur = "";
|
|
24
|
+
let started = false; // did the current token get any character? (distinguishes "" from absent)
|
|
25
|
+
let i = 0;
|
|
26
|
+
while (i < s.length) {
|
|
27
|
+
const c = s[i];
|
|
28
|
+
if (c === "\\" && i + 1 < s.length) {
|
|
29
|
+
cur += s[i + 1];
|
|
30
|
+
i += 2;
|
|
31
|
+
started = true;
|
|
32
|
+
continue;
|
|
33
|
+
} // \x → literal x
|
|
34
|
+
if (c === "'") { // single quotes: everything literal until the next '
|
|
35
|
+
i++;
|
|
36
|
+
while (i < s.length && s[i] !== "'")
|
|
37
|
+
cur += s[i++];
|
|
38
|
+
i++;
|
|
39
|
+
started = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (c === '"') { // double quotes: backslash still escapes inside
|
|
43
|
+
i++;
|
|
44
|
+
while (i < s.length && s[i] !== '"') {
|
|
45
|
+
if (s[i] === "\\" && i + 1 < s.length) {
|
|
46
|
+
cur += s[i + 1];
|
|
47
|
+
i += 2;
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
cur += s[i++];
|
|
51
|
+
}
|
|
52
|
+
i++;
|
|
53
|
+
started = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (c === " " || c === "\t" || c === "\n" || c === "\r") { // a separator between tokens
|
|
57
|
+
if (started) {
|
|
58
|
+
tokens.push(cur);
|
|
59
|
+
cur = "";
|
|
60
|
+
started = false;
|
|
61
|
+
}
|
|
62
|
+
i++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
cur += c;
|
|
66
|
+
i++;
|
|
67
|
+
started = true;
|
|
68
|
+
}
|
|
69
|
+
if (started)
|
|
70
|
+
tokens.push(cur);
|
|
71
|
+
return tokens;
|
|
72
|
+
}
|
|
73
|
+
// A drop produces ABSOLUTE paths (macOS/Linux terminals). Treat a leading ~ as
|
|
74
|
+
// home. Anything not absolute-ish is not a drop — we leave it alone.
|
|
75
|
+
const looksAbsolute = (t) => t.startsWith("/") || t === "~" || t.startsWith("~/");
|
|
76
|
+
const expandTilde = (t) => (t === "~" || t.startsWith("~/") ? os.homedir() + t.slice(1) : t);
|
|
77
|
+
// Quote a path that contains whitespace so several dropped paths stay separable
|
|
78
|
+
// (and the whole thing survives being read as shell-ish tokens later). Prefer
|
|
79
|
+
// single quotes; fall back to double if the path itself contains a single quote.
|
|
80
|
+
function quoteIfNeeded(p) {
|
|
81
|
+
if (!/\s/.test(p))
|
|
82
|
+
return p;
|
|
83
|
+
return p.includes("'") ? `"${p}"` : `'${p}'`;
|
|
84
|
+
}
|
|
85
|
+
// The entry point: given a pasted string, return clean absolute path(s) if it is
|
|
86
|
+
// a drop, otherwise the original text unchanged. A successful normalization ends
|
|
87
|
+
// with a single trailing space, so the cursor lands ready to type the question.
|
|
88
|
+
export function normalizeDroppedPaths(raw) {
|
|
89
|
+
if (!raw)
|
|
90
|
+
return raw;
|
|
91
|
+
const tokens = splitTokens(raw);
|
|
92
|
+
if (!tokens.length)
|
|
93
|
+
return raw;
|
|
94
|
+
const abs = [];
|
|
95
|
+
for (const t of tokens) {
|
|
96
|
+
if (!looksAbsolute(t))
|
|
97
|
+
return raw; // not a dragged absolute path → leave the paste as-is
|
|
98
|
+
const resolved = path.resolve(expandTilde(t)); // normalize (//, .., trailing /) → canonical absolute
|
|
99
|
+
if (!fs.existsSync(resolved))
|
|
100
|
+
return raw; // a path that doesn't exist isn't a real drop
|
|
101
|
+
abs.push(resolved);
|
|
102
|
+
}
|
|
103
|
+
return abs.map(quoteIfNeeded).join(" ") + " ";
|
|
104
|
+
}
|
package/dist/editor.js
CHANGED
|
@@ -26,6 +26,8 @@ export function reduceEditor(s, str, key) {
|
|
|
26
26
|
return { state: { buffer, cursor: Math.max(0, cursor - 1) }, action: "edit" }; // back one char
|
|
27
27
|
case "f":
|
|
28
28
|
return { state: { buffer, cursor: Math.min(buffer.length, cursor + 1) }, action: "edit" }; // forward one char
|
|
29
|
+
case "r":
|
|
30
|
+
return { state: s, action: "reveal" }; // Ctrl+R → reveal the model's collapsed thinking
|
|
29
31
|
default:
|
|
30
32
|
return { state: s, action: "none" };
|
|
31
33
|
}
|
|
@@ -254,6 +256,12 @@ export function editLine(rl, opts) {
|
|
|
254
256
|
const onKey = (str, key) => {
|
|
255
257
|
if (done)
|
|
256
258
|
return;
|
|
259
|
+
// A drag-and-drop (or any paste) arrives as one multi-char string. Give the
|
|
260
|
+
// caller a chance to rewrite it — e.g. turn dropped file paths into clean
|
|
261
|
+
// absolute paths — before it lands in the buffer. No-op for normal text.
|
|
262
|
+
if (str && str.length > 1 && !key?.ctrl && !key?.meta && opts.transformPaste) {
|
|
263
|
+
str = opts.transformPaste(str);
|
|
264
|
+
}
|
|
257
265
|
const next = reduceEditor(state, str, key || {});
|
|
258
266
|
state = next.state;
|
|
259
267
|
switch (next.action) {
|
|
@@ -282,6 +290,10 @@ export function editLine(rl, opts) {
|
|
|
282
290
|
opts.onTab?.(); // may print above us (e.g. expand a collapsed block) and scroll
|
|
283
291
|
first = true; // so the next paint lands fresh at the cursor, not a stale climb-up
|
|
284
292
|
return draw();
|
|
293
|
+
case "reveal":
|
|
294
|
+
opts.onReveal?.(); // print the model's collapsed thinking above us, then repaint
|
|
295
|
+
first = true; // the next paint lands fresh, not a stale climb-up
|
|
296
|
+
return draw();
|
|
285
297
|
case "edit":
|
|
286
298
|
return draw();
|
|
287
299
|
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 { printToolSummary } from "./tui.js"; // collapsible tool output
|
|
15
|
+
import { printToolSummary, recordReasoning } from "./tui.js"; // collapsible tool output + 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
|
|
@@ -328,6 +328,11 @@ opts) {
|
|
|
328
328
|
const word = thinkingWord(modelCallSeq++); // a rotating "thinking" word for this call
|
|
329
329
|
const startedAt = Date.now(); // for the spinner's live elapsed counter
|
|
330
330
|
let streamedChars = 0; // bytes of content+reasoning streamed this call → a live token estimate
|
|
331
|
+
// Reasoning is COLLAPSED by default: while the model thinks, the spinner keeps
|
|
332
|
+
// spinning (with a 💭 hint) instead of dumping the trace; we stash it for Ctrl+R.
|
|
333
|
+
let reasoningBuf = ""; // the accumulated thinking, hidden behind the spinner
|
|
334
|
+
let reasoningActive = false; // true while reasoning tokens are arriving → spinner shows 💭
|
|
335
|
+
let reasoningShown = false; // have we printed the "thought for Ns" indicator yet?
|
|
331
336
|
const spinner = opts.quiet
|
|
332
337
|
? null // the eval harness wants silence
|
|
333
338
|
: ora({ text: spinnerText(word, 0, !!opts.subAgent, opts.model), discardStdin: false }).start(); // discardStdin:false — ora would otherwise eat Ctrl+C
|
|
@@ -338,8 +343,10 @@ opts) {
|
|
|
338
343
|
// - stall (slow but alive for 30s) → log it and keep waiting
|
|
339
344
|
// It also ticks the spinner: elapsed seconds + a live token estimate (~chars/4).
|
|
340
345
|
const watchdog = setInterval(() => {
|
|
346
|
+
// While the model is reasoning, prefix the spinner with 💭 so "it's thinking"
|
|
347
|
+
// reads differently from "it's waiting on the first token".
|
|
341
348
|
if (spinner?.isSpinning)
|
|
342
|
-
spinner.text = spinnerText(word, Math.floor((Date.now() - startedAt) / 1000), !!opts.subAgent, opts.model, Math.round(streamedChars / 4));
|
|
349
|
+
spinner.text = (reasoningActive ? chalk.dim("💭 ") : "") + spinnerText(word, Math.floor((Date.now() - startedAt) / 1000), !!opts.subAgent, opts.model, Math.round(streamedChars / 4));
|
|
343
350
|
const quietMs = Date.now() - lastEvent; // ms since the last event
|
|
344
351
|
if (quietMs > IDLE_TIMEOUT_MS) {
|
|
345
352
|
emit("agent_watchdog_idle"); // record the cut — these should be rare
|
|
@@ -364,9 +371,27 @@ opts) {
|
|
|
364
371
|
{ model: opts.model, messages, tools: toolsFor(opts), stream: true, stream_options: { include_usage: true } }, { signal });
|
|
365
372
|
let content = ""; // accumulated answer text
|
|
366
373
|
let printedPrefix = false; // have we printed the answer prefix yet?
|
|
367
|
-
let printedThinking = false; // have we started printing the reasoning trace yet?
|
|
368
374
|
let md = null; // renders the streamed answer as markdown, block by block
|
|
369
375
|
const calls = []; // tool calls under assembly, indexed by delta.index
|
|
376
|
+
// The model has finished thinking and is about to say/do something. Close out
|
|
377
|
+
// the spinner, stash the trace, and print a single one-line indicator with the
|
|
378
|
+
// shortcut to reveal it. Idempotent + collapses to nothing for sub-agents and
|
|
379
|
+
// the eval harness (they never show thinking).
|
|
380
|
+
const flushReasoning = () => {
|
|
381
|
+
if (!reasoningBuf || reasoningShown)
|
|
382
|
+
return;
|
|
383
|
+
reasoningShown = true;
|
|
384
|
+
reasoningActive = false;
|
|
385
|
+
if (spinner?.isSpinning)
|
|
386
|
+
spinner.stop();
|
|
387
|
+
if (opts.quiet || opts.subAgent)
|
|
388
|
+
return; // no UI for silent / nested runs
|
|
389
|
+
recordReasoning(reasoningBuf); // keep it for Ctrl+R (never goes into history)
|
|
390
|
+
const secs = Math.round((Date.now() - startedAt) / 1000);
|
|
391
|
+
const toks = Math.round(reasoningBuf.length / 4); // same ~chars/4 estimate as the spinner
|
|
392
|
+
const tokStr = toks >= 1000 ? `${(toks / 1000).toFixed(1)}k` : `${toks}`;
|
|
393
|
+
process.stdout.write(chalk.dim(`💭 thought for ${secs}s (~${tokStr} tokens) · Ctrl+R to view`) + "\n");
|
|
394
|
+
};
|
|
370
395
|
for await (const chunk of stream) {
|
|
371
396
|
lastEvent = Date.now(); // feed the watchdog
|
|
372
397
|
stallWarned = false; // the stream spoke — reset the stall warning
|
|
@@ -376,31 +401,24 @@ opts) {
|
|
|
376
401
|
if (!delta)
|
|
377
402
|
continue; // keep-alive or usage chunk — nothing to do
|
|
378
403
|
// Reasoning models (e.g. deepseek-reasoner / R1) stream their thinking in a
|
|
379
|
-
// separate `reasoning_content` field BEFORE the answer.
|
|
380
|
-
//
|
|
381
|
-
//
|
|
404
|
+
// separate `reasoning_content` field BEFORE the answer. We do NOT print it
|
|
405
|
+
// (it buries the actual reply) and do NOT keep it (it must not be replayed
|
|
406
|
+
// in later turns): we let the spinner keep spinning, stash the trace, and
|
|
407
|
+
// surface it only behind Ctrl+R.
|
|
382
408
|
const reasoning = delta.reasoning_content;
|
|
383
409
|
if (reasoning) {
|
|
384
410
|
streamedChars += reasoning.length; // count reasoning toward the live token estimate
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (!opts.quiet && !opts.subAgent) {
|
|
388
|
-
if (!printedThinking) {
|
|
389
|
-
process.stdout.write("\n" + chalk.dim("💭 thinking: ")); // label the trace once
|
|
390
|
-
printedThinking = true;
|
|
391
|
-
}
|
|
392
|
-
process.stdout.write(chalk.dim(reasoning)); // stream the thinking, dimmed
|
|
393
|
-
}
|
|
411
|
+
reasoningBuf += reasoning; // stash it for Ctrl+R instead of streaming it
|
|
412
|
+
reasoningActive = true; // the spinner now shows 💭 (set by the watchdog)
|
|
394
413
|
}
|
|
395
414
|
if (delta.content) {
|
|
415
|
+
flushReasoning(); // thinking is over — show the one-line indicator first
|
|
396
416
|
streamedChars += delta.content.length; // count the answer toward the live token estimate
|
|
397
417
|
if (spinner?.isSpinning)
|
|
398
418
|
spinner.stop(); // first token: replace the spinner with real output
|
|
399
419
|
if (!opts.quiet && !opts.subAgent) {
|
|
400
420
|
// Only the top-level agent streams to the screen — a sub-agent's
|
|
401
421
|
// inner monologue would be mistaken for the answer.
|
|
402
|
-
if (printedThinking && !printedPrefix)
|
|
403
|
-
process.stdout.write("\n"); // end the thinking block before the answer
|
|
404
422
|
if (!printedPrefix) {
|
|
405
423
|
// Stream the answer through the markdown renderer: it buffers a block
|
|
406
424
|
// (paragraph / heading / list / table) and prints it formatted the
|
|
@@ -413,6 +431,8 @@ opts) {
|
|
|
413
431
|
}
|
|
414
432
|
content += delta.content; // always keep it for the history (the answer only, never the reasoning)
|
|
415
433
|
}
|
|
434
|
+
if (delta.tool_calls?.length)
|
|
435
|
+
flushReasoning(); // thinking is over — indicator before the tool logs (which print after we return)
|
|
416
436
|
for (const tc of delta.tool_calls ?? []) {
|
|
417
437
|
if (spinner?.isSpinning)
|
|
418
438
|
spinner.stop(); // tool call starting — spinner served its purpose
|
|
@@ -425,6 +445,7 @@ opts) {
|
|
|
425
445
|
slot.args += tc.function.arguments; // arguments stream in fragments — concatenate
|
|
426
446
|
}
|
|
427
447
|
}
|
|
448
|
+
flushReasoning(); // a reasoning-only turn (no content, no tools) still records + indicates
|
|
428
449
|
if (md)
|
|
429
450
|
md.end(); // flush the final (un-terminated) block through the renderer
|
|
430
451
|
if (printedPrefix)
|
package/dist/tui.js
CHANGED
|
@@ -58,6 +58,33 @@ export function toggleLastCollapsed() {
|
|
|
58
58
|
}
|
|
59
59
|
return true;
|
|
60
60
|
}
|
|
61
|
+
// ---- collapsed reasoning (the model's thinking) ------------------------------
|
|
62
|
+
// Reasoning models (deepseek-reasoner / R1) stream a long thinking trace before
|
|
63
|
+
// the answer. Dumping it all to the screen buries the actual reply, so we hide
|
|
64
|
+
// it behind a spinner and a one-line indicator, keeping the trace here for the
|
|
65
|
+
// user to reveal with Ctrl+R (handled by the line editor, like Tab for tools).
|
|
66
|
+
// Stored as one entry per model round; cleared when a new user turn begins, so
|
|
67
|
+
// Ctrl+R always shows the thinking behind the LAST answer.
|
|
68
|
+
const reasoningTrace = [];
|
|
69
|
+
export function recordReasoning(text) {
|
|
70
|
+
const t = text.trim();
|
|
71
|
+
if (t)
|
|
72
|
+
reasoningTrace.push(t);
|
|
73
|
+
}
|
|
74
|
+
// Drop the current turn's thinking. Called when a new prompt is submitted (the
|
|
75
|
+
// old thinking belonged to the old answer) and on /clear.
|
|
76
|
+
export function clearReasoning() {
|
|
77
|
+
reasoningTrace.length = 0;
|
|
78
|
+
}
|
|
79
|
+
// Ctrl+R handler: print the stored thinking for the last answer, dimmed and
|
|
80
|
+
// nested. Returns false (nothing to show) so the editor can ignore the key.
|
|
81
|
+
export function revealReasoning() {
|
|
82
|
+
if (!reasoningTrace.length)
|
|
83
|
+
return false;
|
|
84
|
+
process.stdout.write(chalk.dim("💭 model's thinking:\n"));
|
|
85
|
+
process.stdout.write(chalk.dim(reasoningTrace.join("\n\n— — —\n\n").replace(/^/gm, " ")) + "\n");
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
61
88
|
// ---- cleanup -----------------------------------------------------------------
|
|
62
89
|
// Called on exit. Nothing fancy to restore now (we no longer hijack the bottom
|
|
63
90
|
// of the screen), but keep the hook so callers don't have to change if that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-from-zero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"eval": "tsx --env-file=.env eval/run.ts",
|
|
17
17
|
"build": "tsc -p tsconfig.build.json",
|
|
18
18
|
"prepare": "npm run build",
|
|
19
|
-
"test": "tsx tests/tools.test.ts && tsx tests/permissions.test.ts && tsx tests/context.test.ts && tsx tests/hooks.test.ts && tsx tests/mcp.test.ts && tsx tests/judge.test.ts && tsx tests/memory.test.ts && tsx tests/cost.test.ts && tsx tests/validate.test.ts && tsx tests/plan.test.ts && tsx tests/diff.test.ts && tsx tests/undo.test.ts && tsx tests/tiering.test.ts && tsx tests/mcp-http.test.ts && tsx tests/changes.test.ts && tsx tests/mentions.test.ts && tsx tests/session.test.ts && tsx tests/ui.test.ts && tsx tests/approval.test.ts && tsx tests/form.test.ts && tsx tests/model.test.ts && tsx tests/reasoning.test.ts && tsx tests/skills.test.ts && tsx tests/markdown.test.ts && tsx tests/editor.test.ts && tsx tests/todos.test.ts && tsx tests/background.test.ts && tsx tests/team.test.ts && tsx tests/board.test.ts"
|
|
19
|
+
"test": "tsx tests/tools.test.ts && tsx tests/permissions.test.ts && tsx tests/context.test.ts && tsx tests/hooks.test.ts && tsx tests/mcp.test.ts && tsx tests/judge.test.ts && tsx tests/memory.test.ts && tsx tests/cost.test.ts && tsx tests/validate.test.ts && tsx tests/plan.test.ts && tsx tests/diff.test.ts && tsx tests/undo.test.ts && tsx tests/tiering.test.ts && tsx tests/mcp-http.test.ts && tsx tests/changes.test.ts && tsx tests/mentions.test.ts && tsx tests/session.test.ts && tsx tests/ui.test.ts && tsx tests/approval.test.ts && tsx tests/form.test.ts && tsx tests/model.test.ts && tsx tests/reasoning.test.ts && tsx tests/skills.test.ts && tsx tests/markdown.test.ts && tsx tests/editor.test.ts && tsx tests/todos.test.ts && tsx tests/background.test.ts && tsx tests/team.test.ts && tsx tests/board.test.ts && tsx tests/drop.test.ts"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20.6"
|