agent-from-zero 0.32.2 → 0.32.4
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/ink/app.js +33 -7
- package/dist/ink/sink.js +14 -1
- package/package.json +1 -1
package/dist/ink/app.js
CHANGED
|
@@ -36,7 +36,10 @@ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧",
|
|
|
36
36
|
// <Static> (formatted as markdown) the moment it finishes.
|
|
37
37
|
function liveTail(s) {
|
|
38
38
|
const rows = process.stdout.rows || 24;
|
|
39
|
-
|
|
39
|
+
// Keep the preview SMALL (≤10 lines) even on a tall terminal: Ink repaints this
|
|
40
|
+
// whole region on every (throttled) update, and a smaller block repaints with
|
|
41
|
+
// far less flicker. The full reply still lands in <Static> on commit.
|
|
42
|
+
const maxLines = Math.max(3, Math.min(10, rows - 14)); // reserve rows for the input box + status bar + agent bar + margins
|
|
40
43
|
const cols = process.stdout.columns || 80;
|
|
41
44
|
const maxChars = maxLines * cols; // rough bound so one very long line can't blow past either
|
|
42
45
|
let t = s.length > maxChars ? s.slice(-maxChars) : s;
|
|
@@ -95,6 +98,7 @@ export function App({ session, runTurn }) {
|
|
|
95
98
|
const [status, setStatus] = useState(null); // the live spinner line
|
|
96
99
|
const [live, setLive] = useState(null); // the streaming answer
|
|
97
100
|
const [input, setInput] = useState(""); // the current input buffer
|
|
101
|
+
const [cursor, setCursor] = useState(0); // caret position WITHIN `input` (0..input.length), for ←/→ editing
|
|
98
102
|
const [busy, setBusy] = useState(false); // a turn is in flight
|
|
99
103
|
const [pending, setPending] = useState(null); // a prompt/menu blocking input
|
|
100
104
|
const [menuSel, setMenuSel] = useState(0); // the select menu's cursor
|
|
@@ -462,6 +466,7 @@ export function App({ session, runTurn }) {
|
|
|
462
466
|
setSubAgentFocus(null);
|
|
463
467
|
const text = input.trim();
|
|
464
468
|
setInput("");
|
|
469
|
+
setCursor(0);
|
|
465
470
|
setHistIdx(null);
|
|
466
471
|
if (!text)
|
|
467
472
|
return;
|
|
@@ -469,13 +474,26 @@ export function App({ session, runTurn }) {
|
|
|
469
474
|
history.push(text); // remember non-empty entries for ↑/↓
|
|
470
475
|
void submitLine(text);
|
|
471
476
|
}
|
|
477
|
+
else if (key.leftArrow) {
|
|
478
|
+
setCursor((c) => Math.max(0, c - 1)); // move the caret left within the line
|
|
479
|
+
}
|
|
480
|
+
else if (key.rightArrow) {
|
|
481
|
+
setCursor((c) => Math.min(input.length, c + 1)); // ...and right
|
|
482
|
+
}
|
|
483
|
+
else if (key.ctrl && char === "a") {
|
|
484
|
+
setCursor(0); // Home — jump to the start of the line
|
|
485
|
+
}
|
|
486
|
+
else if (key.ctrl && char === "e") {
|
|
487
|
+
setCursor(input.length); // End — jump to the end of the line
|
|
488
|
+
}
|
|
472
489
|
else if (key.upArrow) {
|
|
473
|
-
// ↑ recall an earlier prompt (newest-first), like a shell.
|
|
490
|
+
// ↑ recall an earlier prompt (newest-first), like a shell; caret to its end.
|
|
474
491
|
if (!history.length)
|
|
475
492
|
return;
|
|
476
493
|
const idx = histIdx === null ? history.length - 1 : Math.max(0, histIdx - 1);
|
|
477
494
|
setHistIdx(idx);
|
|
478
495
|
setInput(history[idx]);
|
|
496
|
+
setCursor(history[idx].length);
|
|
479
497
|
}
|
|
480
498
|
else if (key.downArrow) {
|
|
481
499
|
// ↓ walk back toward the fresh line.
|
|
@@ -485,24 +503,32 @@ export function App({ session, runTurn }) {
|
|
|
485
503
|
if (idx >= history.length) {
|
|
486
504
|
setHistIdx(null);
|
|
487
505
|
setInput("");
|
|
506
|
+
setCursor(0);
|
|
488
507
|
}
|
|
489
508
|
else {
|
|
490
509
|
setHistIdx(idx);
|
|
491
510
|
setInput(history[idx]);
|
|
511
|
+
setCursor(history[idx].length);
|
|
492
512
|
}
|
|
493
513
|
}
|
|
494
514
|
else if (key.backspace || key.delete) {
|
|
495
|
-
|
|
515
|
+
// Delete the char BEFORE the caret. Both Backspace and macOS DEL land here,
|
|
516
|
+
// so this is always a backward delete (forward-delete is rare; we skip it).
|
|
517
|
+
if (cursor > 0) {
|
|
518
|
+
setInput(input.slice(0, cursor - 1) + input.slice(cursor));
|
|
519
|
+
setCursor(cursor - 1);
|
|
520
|
+
}
|
|
496
521
|
}
|
|
497
522
|
else if (char && !key.ctrl && !key.meta) {
|
|
498
|
-
//
|
|
499
|
-
// file becomes a clean absolute path;
|
|
523
|
+
// Insert at the caret (a bulk chunk — paste / dropped file path — is
|
|
524
|
+
// normalized: a dropped file becomes a clean absolute path; prose unchanged).
|
|
500
525
|
const add = char.length > 1 ? normalizeDroppedPaths(char) : char;
|
|
501
526
|
setHistIdx(null);
|
|
502
|
-
setInput((
|
|
527
|
+
setInput(input.slice(0, cursor) + add + input.slice(cursor));
|
|
528
|
+
setCursor(cursor + add.length);
|
|
503
529
|
}
|
|
504
530
|
});
|
|
505
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: items, children: (item, i) => _jsx(ItemView, { item: item }, i) }), live !== null && (_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "green", children: "\u23FA " }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { children: live === "" ? chalk.dim("…") : liveTail(live) }) })] })), (status !== null || (busy && live === null)) && (_jsxs(Box, { marginTop: live === null ? 1 : 0, children: [_jsx(Spinner, {}), _jsxs(Text, { children: [" ", status ?? chalk.dim("working…")] })] })), pending?.kind === "select" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: pending.header }), _jsx(Text, { children: renderMenu(pending.options, menuSel) }), _jsx(Text, { children: MENU_HINT })] })), pending?.kind === "form" && formState && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: renderForm(pending.questions, formState) }) })), _jsxs(Box, { borderStyle: "round", borderColor: planMode ? "magenta" : "cyan", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: planMode ? "magenta" : "cyan", children: planMode ? "plan ❯ " : "❯ " }), _jsx(Text, { children: input }), _jsx(Text, { inverse: true, children: " " })] }), (() => {
|
|
531
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: items, children: (item, i) => _jsx(ItemView, { item: item }, i) }), live !== null && (_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "green", children: "\u23FA " }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { children: live === "" ? chalk.dim("…") : liveTail(live) }) })] })), (status !== null || (busy && live === null)) && (_jsxs(Box, { marginTop: live === null ? 1 : 0, children: [_jsx(Spinner, {}), _jsxs(Text, { children: [" ", status ?? chalk.dim("working…")] })] })), pending?.kind === "select" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: pending.header }), _jsx(Text, { children: renderMenu(pending.options, menuSel) }), _jsx(Text, { children: MENU_HINT })] })), pending?.kind === "form" && formState && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: renderForm(pending.questions, formState) }) })), _jsxs(Box, { borderStyle: "round", borderColor: planMode ? "magenta" : "cyan", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: planMode ? "magenta" : "cyan", children: planMode ? "plan ❯ " : "❯ " }), _jsx(Text, { children: input.slice(0, cursor) }), _jsx(Text, { inverse: true, children: input.slice(cursor, cursor + 1) || " " }), _jsx(Text, { children: input.slice(cursor + 1) })] }), (() => {
|
|
506
532
|
const agents = listSubAgents();
|
|
507
533
|
if (!agents.length)
|
|
508
534
|
return null;
|
package/dist/ink/sink.js
CHANGED
|
@@ -29,15 +29,28 @@ export function makeInkSink(api) {
|
|
|
29
29
|
// The streamed answer accumulates in the dynamic region as raw text (live,
|
|
30
30
|
// "it's typing"), then commits as ONE markdown-rendered item when it ends —
|
|
31
31
|
// partial markdown mid-stream renders badly, so we format only once it's whole.
|
|
32
|
+
//
|
|
33
|
+
// THROTTLED on purpose: a fast stream delivers tokens dozens of times a
|
|
34
|
+
// second, and Ink erases+redraws the whole live region on every state change
|
|
35
|
+
// — repainting a multi-line block that often is what makes a long answer
|
|
36
|
+
// "shake" violently. So we coalesce a burst of tokens into ONE setLive every
|
|
37
|
+
// ~90ms (≈11 fps, smooth but cheap). The final, complete answer still commits
|
|
38
|
+
// to <Static> on end(), so nothing is lost or under-rendered.
|
|
32
39
|
answer() {
|
|
33
40
|
let buf = "";
|
|
41
|
+
let timer = null;
|
|
34
42
|
api.setLive("");
|
|
35
43
|
return {
|
|
36
44
|
push: (tok) => {
|
|
37
45
|
buf += tok;
|
|
38
|
-
|
|
46
|
+
if (!timer)
|
|
47
|
+
timer = setTimeout(() => { timer = null; api.setLive(buf); }, 90); // leading-edge throttle: schedule one repaint, ignore the burst until it fires
|
|
39
48
|
},
|
|
40
49
|
end: () => {
|
|
50
|
+
if (timer) {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
timer = null;
|
|
53
|
+
} // drop the pending repaint — we're committing now
|
|
41
54
|
api.setLive(null); // clear the live preview first, then commit (no flicker/duplicate)
|
|
42
55
|
if (buf.trim())
|
|
43
56
|
api.pushItem({ kind: "answer", text: buf });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-from-zero",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.4",
|
|
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": {
|