@tiens.nguyen/gu-cli 1.0.787 → 1.0.789
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/clear-logs.mjs +352 -0
- package/gonext_agent_chat.py +252 -14
- package/gu-repl.mjs +311 -32
- package/package.json +2 -1
- package/terminal-layout.mjs +171 -2
package/terminal-layout.mjs
CHANGED
|
@@ -2434,11 +2434,104 @@ export function firstToolArg(codePart) {
|
|
|
2434
2434
|
return { name: m[1], arg: String(value).trim() };
|
|
2435
2435
|
}
|
|
2436
2436
|
|
|
2437
|
+
/**
|
|
2438
|
+
* WHERE A THOUGHT LINE REALLY STARTS (task #333).
|
|
2439
|
+
*
|
|
2440
|
+
* The live line once read `one, call use_skill(name) FIRST, read what it returns, and follow it".
|
|
2441
|
+
* I'll…` for 83 seconds, and later `` `, `bootstrap-api-service.mjs` — add … `` — both the tail of
|
|
2442
|
+
* a sentence whose head had been cut away. The main cause is fixed where it happened (a heartbeat
|
|
2443
|
+
* splitting the thought, see the fence handling in gu-repl.mjs); this is the guard for every other
|
|
2444
|
+
* way a line can arrive headless (a poll boundary, a buffer cap): a line that opens with closing
|
|
2445
|
+
* punctuation, or with a lower-case word, is the middle of something. Start at the next sentence
|
|
2446
|
+
* instead — and when there is none, say nothing ("" keeps whatever the line showed before).
|
|
2447
|
+
*/
|
|
2448
|
+
const _SENTENCE_END = /[.!?]["'”’`)\]]*\s+(?=["'“‘`(\[]?[A-Z0-9])/g;
|
|
2449
|
+
|
|
2450
|
+
export function thoughtLineStart(line) {
|
|
2451
|
+
const s = String(line ?? "").trim();
|
|
2452
|
+
if (!s) return "";
|
|
2453
|
+
const headless =
|
|
2454
|
+
/^[`'"”’)\]}]*\s*[,.;:!?)\]}]/.test(s) || // "`, `bootstrap…", ", and then", ") so"
|
|
2455
|
+
/^[a-z]/.test(s); // "one, call use_skill…" — a sentence starts with a capital
|
|
2456
|
+
if (!headless) return s;
|
|
2457
|
+
_SENTENCE_END.lastIndex = 0;
|
|
2458
|
+
const m = _SENTENCE_END.exec(s);
|
|
2459
|
+
return m ? s.slice(m.index + m[0].length).trim() : "";
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
/**
|
|
2463
|
+
* gu's OWN INSTRUCTIONS, recited (task #333). A model weighing its prompt quotes it — "Rules say
|
|
2464
|
+
* call use_skill FIRST when the task matches" — and the status line then shows gu's scaffolding as
|
|
2465
|
+
* if it were news about the user's task. Matched by the fixed phrases of the prompt blocks the
|
|
2466
|
+
* worker writes (gu_skills.prompt_block, the workspace directives); a sentence carrying one is
|
|
2467
|
+
* not shown on the live line.
|
|
2468
|
+
*/
|
|
2469
|
+
const _PROMPT_ECHO = /use_skill\(name\)|call use_skill\b[^.]*\bFIRST\b|When the TASK matches|WORK LIKE A SENIOR DEVELOPER|packaged instructions for particular kinds of work/i;
|
|
2470
|
+
|
|
2471
|
+
/** Split prose into sentences, keeping each one's own punctuation. */
|
|
2472
|
+
function _sentences(text) {
|
|
2473
|
+
const s = String(text ?? "").trim();
|
|
2474
|
+
const out = [];
|
|
2475
|
+
let from = 0;
|
|
2476
|
+
_SENTENCE_END.lastIndex = 0;
|
|
2477
|
+
let m;
|
|
2478
|
+
while ((m = _SENTENCE_END.exec(s))) {
|
|
2479
|
+
out.push(s.slice(from, m.index + m[0].length).trim());
|
|
2480
|
+
from = m.index + m[0].length;
|
|
2481
|
+
}
|
|
2482
|
+
if (from < s.length) out.push(s.slice(from).trim());
|
|
2483
|
+
return out.filter(Boolean);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/** The line to show live: from its first real sentence, past any recital of gu's own prompt. */
|
|
2487
|
+
export function liveThoughtText(line) {
|
|
2488
|
+
const sentences = _sentences(thoughtLineStart(line));
|
|
2489
|
+
const at = sentences.findIndex((x) => !_PROMPT_ECHO.test(x));
|
|
2490
|
+
return at < 0 ? "" : sentences.slice(at).join(" ");
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
/**
|
|
2494
|
+
* THE QUESTION, SAID BACK (task #333). A thought that opens "The user wants me to use the .env
|
|
2495
|
+
* file and add a template file." sits directly under ">> currently we are not using the .env
|
|
2496
|
+
* file, please use that and also add a template file" — the same thing twice, and on the two
|
|
2497
|
+
* collapsed rows that are all most people read. Measured on this machine's stored turns: 169 of
|
|
2498
|
+
* 296 first thoughts (57%) open this way.
|
|
2499
|
+
*
|
|
2500
|
+
* Leading sentences that restate are dropped; what the model said NEXT — what it will look at,
|
|
2501
|
+
* what it found — becomes the block's first row. Two tests, either is enough: the sentence is
|
|
2502
|
+
* shaped like a restatement ("The user wants/says/asks…", "Let me understand the task"), or most
|
|
2503
|
+
* of its words are the question's own. Only from the START, and never the whole thought: when
|
|
2504
|
+
* every sentence restates, the last one is kept, so a thought is shortened, not lost.
|
|
2505
|
+
* Display only — what the model wrote, and what is sent back to it, are untouched.
|
|
2506
|
+
*/
|
|
2507
|
+
const _RESTATE_SHAPE = /^(?:(?:ok(?:ay)?|so|alright|right|now|first)[,.]?\s+)*(?:let me (?:first\s+)?(?:understand|re-?read|restate|parse|break down|think about)\b[^.]*\b(?:task|request|question|ask)|the user(?:'s (?:request|question|task|message))?\s+(?:wants?|says?|said|is asking|asks?|asked|would like|needs?|is|has asked|requests?)|the (?:task|request|question)(?:\s+is)?\s*[:,]|(?:the )?user (?:request|query|message)\s*[:,]|the (?:system prompt|instructions?|rules?|skill)\s+(?:says?|tells?|states?|wants?|requires?)|(?:rules?|instructions?) say\b)/i;
|
|
2508
|
+
|
|
2509
|
+
const _words = (t) => String(t ?? "").toLowerCase().match(/[a-z0-9_.-]{3,}/g) ?? [];
|
|
2510
|
+
|
|
2511
|
+
/** Is this ONE sentence the question (or gu's instructions) said back? */
|
|
2512
|
+
export function isRestatement(sentence, question = "") {
|
|
2513
|
+
if (_RESTATE_SHAPE.test(sentence)) return true;
|
|
2514
|
+
const asked = new Set(_words(question));
|
|
2515
|
+
if (asked.size < 4) return false;
|
|
2516
|
+
const w = _words(sentence);
|
|
2517
|
+
if (w.length < 4) return false;
|
|
2518
|
+
return w.filter((x) => asked.has(x)).length / w.length >= 0.7;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
export function dropRestatement(prose, question = "") {
|
|
2522
|
+
const sentences = _sentences(prose);
|
|
2523
|
+
if (sentences.length < 2) return String(prose ?? "").trim();
|
|
2524
|
+
const restates = (sentence) => isRestatement(sentence, question);
|
|
2525
|
+
let at = 0;
|
|
2526
|
+
while (at < sentences.length - 1 && restates(sentences[at])) at++;
|
|
2527
|
+
return sentences.slice(at).join(" ");
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2437
2530
|
/**
|
|
2438
2531
|
* Raw in-fence stream → a short, meaningful status: a real Thought clause if the model wrote
|
|
2439
2532
|
* one, else a label naming the tool it is calling, else "" (the caller shows "Thinking…").
|
|
2440
2533
|
*/
|
|
2441
|
-
export function normalizeThought(buf) {
|
|
2534
|
+
export function normalizeThought(buf, { question = null } = {}) {
|
|
2442
2535
|
const t = String(buf || "");
|
|
2443
2536
|
const ci = t.search(/<code[\s>]/i);
|
|
2444
2537
|
const head = ci >= 0 ? t.slice(0, ci) : t;
|
|
@@ -2455,7 +2548,24 @@ export function normalizeThought(buf) {
|
|
|
2455
2548
|
.map((x) => x.trim())
|
|
2456
2549
|
.find(Boolean);
|
|
2457
2550
|
const proseIsClause = !!prose && prose.length >= 6 && prose.trim().split(/\s+/).length >= 2;
|
|
2458
|
-
if (proseIsClause && !_thoughtLooksCodey(prose))
|
|
2551
|
+
if (proseIsClause && !_thoughtLooksCodey(prose)) {
|
|
2552
|
+
// …from where a sentence starts, and never gu's own prompt said back (#333). A line with
|
|
2553
|
+
// nothing left falls through to the tool label / "", like any other non-thought.
|
|
2554
|
+
//
|
|
2555
|
+
// WITH THE QUESTION KNOWN (the terminal passes it), the line is taken from the whole head
|
|
2556
|
+
// rather than its first line, minus the question said back: "The user wants me to read
|
|
2557
|
+
// package.json…" sat on the status row for the whole wait while the next sentence — what
|
|
2558
|
+
// the model was actually about to do — was already there. While all there is so far IS the
|
|
2559
|
+
// restatement, nothing is shown: the row keeps "Thinking" for the second that takes.
|
|
2560
|
+
let text = prose;
|
|
2561
|
+
if (question !== null) {
|
|
2562
|
+
const whole = head.replace(/^\s*Thought:\s*/i, "").replace(/\s+/g, " ").trim();
|
|
2563
|
+
text = dropRestatement(whole, question);
|
|
2564
|
+
if (isRestatement(text, question)) text = "";
|
|
2565
|
+
}
|
|
2566
|
+
const shown = liveThoughtText(text);
|
|
2567
|
+
if (shown.length >= 6 && shown.split(/\s+/).length >= 2) return shown.slice(0, 100);
|
|
2568
|
+
}
|
|
2459
2569
|
// 2) otherwise name the tool being called (from the code part).
|
|
2460
2570
|
const codePart = ci >= 0 ? t.slice(ci) : t;
|
|
2461
2571
|
const call = firstToolArg(codePart);
|
|
@@ -2586,6 +2696,65 @@ export function runningDetailRows(detail, { width = 80, indent = 5, maxRows = 2
|
|
|
2586
2696
|
];
|
|
2587
2697
|
}
|
|
2588
2698
|
|
|
2699
|
+
/**
|
|
2700
|
+
* THE COMMAND THAT IS RUNNING, on the live status row (task #331): "$ find . -type f … (17s · 5 lines)".
|
|
2701
|
+
*
|
|
2702
|
+
* It used to read "Running <first 70 characters>… (17s)" with every newline squashed to a space,
|
|
2703
|
+
* so a heredoc was one run-on line and a long pipeline stopped mid-flag. Now: the shell's "$", the
|
|
2704
|
+
* command's FIRST LINE (a heredoc's first line is what names it), and at the right what the user
|
|
2705
|
+
* is waiting to learn — how long it has run and how much it has printed.
|
|
2706
|
+
*
|
|
2707
|
+
* ONE ROW, ALWAYS — the status block is redrawn in place and a taller block moves the input line.
|
|
2708
|
+
* The rest of the command is a click (or Ctrl+T) away: see runningCommandRows. → { label, more }
|
|
2709
|
+
* `more` = there is more of the command than this row shows, i.e. the click has something to open.
|
|
2710
|
+
*/
|
|
2711
|
+
export function runningCommandLabel(command, { room = 66, secs = 0, lines = 0, expanded = false } = {}) {
|
|
2712
|
+
const all = String(command ?? "").replace(/\r/g, "").split("\n").map((l) => l.replace(/\t/g, " ").replace(/\s+$/, "")).filter((l) => l.trim());
|
|
2713
|
+
const n = Math.max(0, Number(lines) || 0);
|
|
2714
|
+
const suffix = `(${Math.max(0, secs | 0)}s${n ? ` · ${n} line${n === 1 ? "" : "s"}` : ""})`;
|
|
2715
|
+
const multi = all.length > 1;
|
|
2716
|
+
// opened: the rows underneath ARE the command, so the row above them just says what they are
|
|
2717
|
+
if (expanded) return { label: `Running command ${suffix}`, more: true };
|
|
2718
|
+
const head = `$ ${(all[0] ?? "").trim()}`;
|
|
2719
|
+
const space = Math.max(8, (room | 0) - suffix.length - 2);
|
|
2720
|
+
const cut = head.length + (multi ? 2 : 0) > space;
|
|
2721
|
+
const shown = cut ? head.slice(0, Math.max(1, space - 1)).replace(/\s+$/, "") + "…" : head + (multi ? " …" : "");
|
|
2722
|
+
return { label: `${shown} ${suffix}`, more: cut || multi };
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
/**
|
|
2726
|
+
* The whole command, opened under the status row: "└ $ …" with a hanging indent, its own line
|
|
2727
|
+
* breaks kept (a heredoc reads as the script it is), long lines wrapped at a space, and a cap —
|
|
2728
|
+
* past it the last row ends in "…". Rows NEVER exceed `width`: the block is redrawn by walking
|
|
2729
|
+
* physical rows, and a row that wraps on its own breaks every row under it (#109, #138).
|
|
2730
|
+
*/
|
|
2731
|
+
export function runningCommandRows(command, { width = 76, maxRows = 5 } = {}) {
|
|
2732
|
+
const lead = "└ $ ";
|
|
2733
|
+
const pad = " ".repeat(lead.length);
|
|
2734
|
+
const room = Math.max(12, (width | 0) - lead.length);
|
|
2735
|
+
const out = [];
|
|
2736
|
+
for (const raw of String(command ?? "").replace(/\r/g, "").split("\n")) {
|
|
2737
|
+
let rest = raw.replace(/\t/g, " ").replace(/\s+$/, "");
|
|
2738
|
+
if (!rest.trim()) continue;
|
|
2739
|
+
while (rest.length > room) {
|
|
2740
|
+
const sp = rest.lastIndexOf(" ", room);
|
|
2741
|
+
// …but never break inside the leading indentation, or a wrapped line loses its shape
|
|
2742
|
+
const cut = sp > room * 0.5 ? sp : room;
|
|
2743
|
+
out.push(rest.slice(0, cut));
|
|
2744
|
+
rest = rest.slice(cut).replace(/^ /, "");
|
|
2745
|
+
}
|
|
2746
|
+
out.push(rest);
|
|
2747
|
+
}
|
|
2748
|
+
if (!out.length) return [];
|
|
2749
|
+
const cap = Math.max(1, maxRows | 0);
|
|
2750
|
+
const rows = out.slice(0, cap);
|
|
2751
|
+
if (out.length > cap) {
|
|
2752
|
+
const last = rows[cap - 1];
|
|
2753
|
+
rows[cap - 1] = (last.length >= room ? last.slice(0, room - 1) : last) + "…";
|
|
2754
|
+
}
|
|
2755
|
+
return rows.map((r, i) => (i ? pad : lead) + r);
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2589
2758
|
/**
|
|
2590
2759
|
* How long the turn took, for the line above the answer (task #178).
|
|
2591
2760
|
*
|