@tiens.nguyen/gu-cli 1.0.812 → 1.0.813
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/gonext_agent_chat.py +42 -1
- package/gu-repl.mjs +33 -5
- package/package.json +2 -1
- package/terminal-layout.mjs +46 -3
package/gonext_agent_chat.py
CHANGED
|
@@ -4769,6 +4769,42 @@ def _collapse_inner_openers(text):
|
|
|
4769
4769
|
return text[:body_from] + cleaned + "\n</code>"
|
|
4770
4770
|
|
|
4771
4771
|
|
|
4772
|
+
# DeepSeek DSML tool-call markup (issue #119). Some DeepSeek-family models (seen via
|
|
4773
|
+
# token-plan.maas.qwencloudapi.com) answer with their NATIVE tool-call text instead of our code
|
|
4774
|
+
# block: prose, then a DSML "calls" tag, a DSML invoke tag with name="code", then plain Python,
|
|
4775
|
+
# then closers that are often wrong (a DSML "parameter" close) or missing. The bars in the tags
|
|
4776
|
+
# are FULLWIDTH, so the patterns tolerate any short run of non-bracket characters around DSML.
|
|
4777
|
+
_DSML_INVOKE_CODE = re.compile(
|
|
4778
|
+
r"<[^<>\n]{0,8}DSML[^<>\n]{0,8}invoke\s+name\s*=\s*[\x22\x27]code[\x22\x27]\s*>[ \t]*\r?\n?"
|
|
4779
|
+
)
|
|
4780
|
+
_DSML_ANY = re.compile(r"<[^<>\n]{0,8}DSML")
|
|
4781
|
+
_DSML_CLOSER = re.compile(r"\n?[ \t]*</[^<>\n]{0,8}DSML")
|
|
4782
|
+
|
|
4783
|
+
|
|
4784
|
+
def _dsml_to_code_block(text):
|
|
4785
|
+
"""Rewrite a DSML invoke name="code" body into a normal code block, or return the text
|
|
4786
|
+
unchanged. Fires only when there is NO code tag already and the body COMPILES (the same
|
|
4787
|
+
conservative contract as the other readings)."""
|
|
4788
|
+
if not text or "DSML" not in text or re.search(r"<\s*/?\s*code\b", text, re.I):
|
|
4789
|
+
return text
|
|
4790
|
+
m = _DSML_INVOKE_CODE.search(text)
|
|
4791
|
+
if not m:
|
|
4792
|
+
return text
|
|
4793
|
+
rest = text[m.end():]
|
|
4794
|
+
c = _DSML_CLOSER.search(rest)
|
|
4795
|
+
body = (rest[:c.start()] if c else rest).strip("\n")
|
|
4796
|
+
if not body.strip():
|
|
4797
|
+
return text
|
|
4798
|
+
try:
|
|
4799
|
+
compile(body, "<gu-code>", "exec")
|
|
4800
|
+
except Exception: # noqa: BLE001
|
|
4801
|
+
return text
|
|
4802
|
+
head = text[:m.start()]
|
|
4803
|
+
first = _DSML_ANY.search(head)
|
|
4804
|
+
prose = (head[:first.start()] if first else head).rstrip()
|
|
4805
|
+
return (prose + "\n" if prose else "") + "<code>\n" + body + "\n</code>"
|
|
4806
|
+
|
|
4807
|
+
|
|
4772
4808
|
def _code_block_readings(text):
|
|
4773
4809
|
"""Every plausible reading of where the code block is, CHEAPEST FIRST.
|
|
4774
4810
|
|
|
@@ -4829,6 +4865,10 @@ def _canonicalise_code_reply(text):
|
|
|
4829
4865
|
# reproduced, inside the fix, the very shape of the bug being fixed: a strict cheap check
|
|
4830
4866
|
# guarding a tolerant repair. If a pre-check cannot be as permissive as what it guards, it
|
|
4831
4867
|
# does not belong in front of it.
|
|
4868
|
+
_dsml = _dsml_to_code_block(text)
|
|
4869
|
+
if _dsml != text:
|
|
4870
|
+
_log("DSML tool-call markup converted to a code block (#119)")
|
|
4871
|
+
return _dsml
|
|
4832
4872
|
if not text or not _CODE_ANY_TAGGISH.search(text):
|
|
4833
4873
|
return text
|
|
4834
4874
|
fallback = None
|
|
@@ -11825,7 +11865,8 @@ def run_agent_chat(cfg):
|
|
|
11825
11865
|
if _still_bad:
|
|
11826
11866
|
self._parse_fail_streak += 1
|
|
11827
11867
|
_log("tool-call code still unparseable after repair "
|
|
11828
|
-
f"(streak {self._parse_fail_streak}/3)"
|
|
11868
|
+
f"(streak {self._parse_fail_streak}/3) model={getattr(self, 'model_id', '?')} "
|
|
11869
|
+
f"reply[:300]={(msg.content or '')[:300]!r}")
|
|
11829
11870
|
if self._parse_fail_streak >= 3:
|
|
11830
11871
|
_log("parse-fail streak hit limit → ending the turn honestly "
|
|
11831
11872
|
"instead of looping on an unparseable tool call")
|
package/gu-repl.mjs
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
thoughtFull,
|
|
62
62
|
thoughtBlockRows,
|
|
63
63
|
ragSegmentLabel,
|
|
64
|
+
ragBarNote,
|
|
64
65
|
searchSegmentLabel,
|
|
65
66
|
runningDetailRows,
|
|
66
67
|
formatThinkTime,
|
|
@@ -2276,9 +2277,13 @@ const ragSegmentText = () => {
|
|
|
2276
2277
|
// RAG FAILED MID-TURN and searches were answered by the grep fallback: a WARNING (yellow),
|
|
2277
2278
|
// always visible — the turn still got results, but not semantic ones.
|
|
2278
2279
|
const liveFallback = b.ragState === "fallback" && !b.stale;
|
|
2279
|
-
|
|
2280
|
+
// Task #34: a LIVE grep fallback (embedder down, or RAG failed mid-turn) is no longer on this
|
|
2281
|
+
// row. In red with a "✗" it read as a serious fault while search kept working. It is now a soft
|
|
2282
|
+
// yellow note on the bottom bar's mode row (ragBarNote → footerRows' modeNote).
|
|
2283
|
+
if (liveDown || liveFallback) return lexSeg;
|
|
2284
|
+
if (!REPL_VERBOSE) return lexSeg;
|
|
2280
2285
|
if (b.stale) return dim(" · ") + dim(`${label}·`) + lexSeg;
|
|
2281
|
-
return dim(" · ") +
|
|
2286
|
+
return dim(" · ") + dim(label) + lexSeg;
|
|
2282
2287
|
};
|
|
2283
2288
|
/**
|
|
2284
2289
|
* IS ANYTHING NEWER? (task #181) — cached, never probed from a repaint.
|
|
@@ -2729,6 +2734,10 @@ const footerRows = () =>
|
|
|
2729
2734
|
identity: identityLineText(),
|
|
2730
2735
|
token: tokenLineText(),
|
|
2731
2736
|
mode: modeLineText(),
|
|
2737
|
+
// Task #34: "rag: embedder down → grep fallback" as a soft yellow note beside the mode, not an
|
|
2738
|
+
// alarming red segment on the token row. yellow goes through paint(), so NO_COLOR turns it off.
|
|
2739
|
+
modeNote: ragBarNote(lastPromptBreakdown),
|
|
2740
|
+
notePaint: yellow,
|
|
2732
2741
|
// How to paste here (issue #67): the platform's text key, plus gu's image key when the coder
|
|
2733
2742
|
// can take an image.
|
|
2734
2743
|
modeHint: pasteHint({ images: coderTakesImages() }),
|
|
@@ -3540,6 +3549,7 @@ function printThoughtBlock(raw, before, { gap = false } = {}) {
|
|
|
3540
3549
|
collapsed: true,
|
|
3541
3550
|
lines: THOUGHT_COLLAPSED_LINES,
|
|
3542
3551
|
});
|
|
3552
|
+
const rowsBeforeMerge = screenRows().length;
|
|
3543
3553
|
const newEnd = transcript.replaceRange(prev.from, prev.to, grown.map((r) => GUTTER + dim(r)));
|
|
3544
3554
|
if (newEnd !== null) {
|
|
3545
3555
|
prev.prose = merged;
|
|
@@ -3548,7 +3558,7 @@ function printThoughtBlock(raw, before, { gap = false } = {}) {
|
|
|
3548
3558
|
// move under the pointer, so the cached hover row must be dropped — same reason
|
|
3549
3559
|
// toggleThoughtBlock does it after a resize of the block.
|
|
3550
3560
|
hoverCtl.reset();
|
|
3551
|
-
|
|
3561
|
+
repaintTailOrHoldView(rowsBeforeMerge); // #113/#35: a finished thought never pulls a reader down
|
|
3552
3562
|
return true;
|
|
3553
3563
|
}
|
|
3554
3564
|
// Scrolled out of the buffer — fall through and print a new block rather than lose it.
|
|
@@ -3855,6 +3865,21 @@ function toggleNewestThought() {
|
|
|
3855
3865
|
return toggleThoughtBlock(block);
|
|
3856
3866
|
}
|
|
3857
3867
|
|
|
3868
|
+
/**
|
|
3869
|
+
* #113 / #35 — A BLOCK AT THE TAIL CHANGED SIZE (a thought merged, a step group grew). At the
|
|
3870
|
+
* live tail that is a paintLiveTail. Scrolled back, it is NOT: paintLiveTail sets scrollOffset
|
|
3871
|
+
* to 0, which is exactly the "it finished thinking and threw me back to the bottom" report. The
|
|
3872
|
+
* change happened below (or at the edge of) what the reader is looking at, so keep the same
|
|
3873
|
+
* lines on screen — the offset moves by the rows added — and let the jump notice count up.
|
|
3874
|
+
*/
|
|
3875
|
+
function repaintTailOrHoldView(rowsBefore) {
|
|
3876
|
+
if (barPinned() && scrolledBack()) {
|
|
3877
|
+
scrollOffset = Math.max(1, scrollOffset + (screenRows().length - rowsBefore));
|
|
3878
|
+
renderScrollback(); // same slice as before; ends in paintBar (the "N lines below" count)
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
paintLiveTail();
|
|
3882
|
+
}
|
|
3858
3883
|
/** Back to the live tail: repaint it from the transcript and hand the screen back. */
|
|
3859
3884
|
function jumpToBottom() {
|
|
3860
3885
|
if (!barPinned() || !scrollOffset) return; // already live — nothing to come back from
|
|
@@ -11876,13 +11901,16 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11876
11901
|
if (stepGroupStillOpen()) {
|
|
11877
11902
|
stepGroup.items.push(text);
|
|
11878
11903
|
const painted = paintThoughtRows(stepGroup, hoverCtl.hovered === stepGroup);
|
|
11904
|
+
const rowsBeforeStep = screenRows().length;
|
|
11879
11905
|
const newEnd = transcript.replaceRange(stepGroup.from, stepGroup.to, painted);
|
|
11880
11906
|
if (newEnd !== null) {
|
|
11881
11907
|
const delta = newEnd - stepGroup.to;
|
|
11882
11908
|
stepGroup.to = newEnd;
|
|
11883
11909
|
if (delta === 0) {
|
|
11884
11910
|
// Collapsed: one row before, one row after — rewrite that row and nothing else.
|
|
11885
|
-
|
|
11911
|
+
// Scrolled back (#113/#35): those rows are live-tail coordinates, NOT the history the
|
|
11912
|
+
// reader is looking at — painting them would draw over it. The transcript has it.
|
|
11913
|
+
if (!scrolledBack()) ui(thoughtBlockRepaintSeq(stepGroup));
|
|
11886
11914
|
paintBar();
|
|
11887
11915
|
} else {
|
|
11888
11916
|
// Open (the user clicked it mid-turn): it grew, and what is under it moved down.
|
|
@@ -11893,7 +11921,7 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11893
11921
|
}
|
|
11894
11922
|
}
|
|
11895
11923
|
hoverCtl.reset();
|
|
11896
|
-
|
|
11924
|
+
repaintTailOrHoldView(rowsBeforeStep); // #113/#35: never pull a reader down
|
|
11897
11925
|
}
|
|
11898
11926
|
return;
|
|
11899
11927
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.813",
|
|
4
4
|
"description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"test:cache-e2e": "GONEXT_OLLAMA1_LIVE=1 node --test --test-timeout=1800000 tests/prefix-cache-e2e.test.mjs",
|
|
41
41
|
"test:tokens": "python3 -m unittest discover tests -p \"test_cached_tokens.py\" -v && node --test tests/prefix-cache.test.mjs tests/agent-turn-shape.test.mjs",
|
|
42
42
|
"test:selection": "node --test tests/selection-scrollback.test.mjs tests/selection-while-thinking.test.mjs tests/terminal-selection.test.mjs tests/terminal-copy.test.mjs tests/terminal-viewport.test.mjs tests/hover-repaint.test.mjs",
|
|
43
|
+
"test:autoscroll": "node --test tests/no-autoscroll-hide-total.test.mjs tests/thought-spacing.test.mjs tests/terminal-viewport.test.mjs tests/selection-scrollback.test.mjs",
|
|
43
44
|
"test:steps": "python3 scripts/py-tap.py tests/test_read_caps.py tests/test_trim_payback.py tests/test_explore.py tests/test_and_chains.py tests/test_edit_file_primary.py tests/test_bare_code_rescue.py tests/test_batching_rule.py tests/test_no_pdf_routing.py",
|
|
44
45
|
"check:image": "node scripts/image-paste-check.mjs",
|
|
45
46
|
"check:image-clipboard": "node scripts/image-paste-check.mjs --clipboard",
|
package/terminal-layout.mjs
CHANGED
|
@@ -146,8 +146,9 @@ export function ragSegmentLabel(b) {
|
|
|
146
146
|
const rag = Number.isFinite(b.rag) ? b.rag : 0;
|
|
147
147
|
if (rag > 0) return null; // …the caller renders the number + percent instead
|
|
148
148
|
switch (b.ragState) {
|
|
149
|
+
// Task #34: no "✗" — search still works through grep, so this is a notice, not an error.
|
|
149
150
|
case "unreachable":
|
|
150
|
-
return "rag
|
|
151
|
+
return "rag: embedder down → grep fallback";
|
|
151
152
|
case "no-index":
|
|
152
153
|
return "rag — not indexed";
|
|
153
154
|
case "unavailable":
|
|
@@ -167,7 +168,7 @@ export function ragSegmentLabel(b) {
|
|
|
167
168
|
// RAG FAILED MID-TURN (indexing or query embedding) and the search was answered by the
|
|
168
169
|
// keyword fallback instead. A warning, not an error: the turn still got results.
|
|
169
170
|
case "fallback":
|
|
170
|
-
return "rag
|
|
171
|
+
return "rag: unavailable → grep fallback";
|
|
171
172
|
// Nothing has been measured yet (the state before any turn has run). "0" is a claim we
|
|
172
173
|
// cannot support; a dash says "no figure", which is the truth.
|
|
173
174
|
case "unknown":
|
|
@@ -182,6 +183,34 @@ export function ragSegmentLabel(b) {
|
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
185
|
|
|
186
|
+
/**
|
|
187
|
+
* The RAG notice for the BOTTOM BAR's mode row (task #34), or "" when there is none.
|
|
188
|
+
*
|
|
189
|
+
* Only THIS session's live verdict that search fell back to grep: "unreachable" (the embedder is
|
|
190
|
+
* down) or "fallback" (RAG failed mid-turn). It used to sit in red with a "✗" in the middle of the
|
|
191
|
+
* token row and read as a serious fault, while every search was still answered by grep. It is
|
|
192
|
+
* now a soft note beside "accept edits on". A stale verdict reloaded from a saved session is
|
|
193
|
+
* not a live fault, so it gets no notice.
|
|
194
|
+
*/
|
|
195
|
+
export function ragBarNote(b) {
|
|
196
|
+
if (!b || typeof b !== "object" || b.stale) return "";
|
|
197
|
+
if (b.ragState !== "unreachable" && b.ragState !== "fallback") return "";
|
|
198
|
+
return ragSegmentLabel(b) || "";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Cut plain text to `room` visible columns, ending with "…" when it was cut. */
|
|
202
|
+
function fitPlain(s, room) {
|
|
203
|
+
s = String(s ?? "");
|
|
204
|
+
if (visibleLen(s) <= room) return s;
|
|
205
|
+
if (room <= 1) return room === 1 ? "…" : "";
|
|
206
|
+
let out = "";
|
|
207
|
+
for (const ch of s) {
|
|
208
|
+
if (visibleLen(out + ch) > room - 1) break;
|
|
209
|
+
out += ch;
|
|
210
|
+
}
|
|
211
|
+
return out + "…";
|
|
212
|
+
}
|
|
213
|
+
|
|
185
214
|
// The six origin buckets. They are scaled to sum to the server's prompt_tokens for ONE
|
|
186
215
|
// request, so their sum IS the size of the newest context — the number to compare against
|
|
187
216
|
// the model's window. Deliberately excludes ragFresh/ragCarried (shares OF rag, not extra)
|
|
@@ -544,11 +573,25 @@ export function buildFooterRows({
|
|
|
544
573
|
// Right-aligned on the mode row (issue #67), dim. DROPPED WHOLE when it does not fit: every
|
|
545
574
|
// footer row must stay one physical row (#109/#132/#138), and half a key name is worse than none.
|
|
546
575
|
modeHint = "",
|
|
576
|
+
// Task #34: a soft notice (PLAIN text) after the mode text, painted with notePaint. It takes
|
|
577
|
+
// priority over the paste hint and is cut with "…" on a narrow window, never wrapped.
|
|
578
|
+
modeNote = "",
|
|
579
|
+
notePaint = dim,
|
|
547
580
|
notice = "",
|
|
548
581
|
missing = "",
|
|
549
582
|
}) {
|
|
550
583
|
const w = Math.max(20, width || 80);
|
|
551
|
-
|
|
584
|
+
let modeLeft = gutter + mode;
|
|
585
|
+
if (modeNote) {
|
|
586
|
+
const NOTE_GAP = " ";
|
|
587
|
+
const hintRoom = modeHint ? HINT_MIN_GAP + visibleLen(modeHint) : 0;
|
|
588
|
+
let room = w - 1 - visibleLen(modeLeft) - NOTE_GAP.length;
|
|
589
|
+
// Keep the paste hint only when the whole notice still fits beside it.
|
|
590
|
+
if (modeHint && visibleLen(modeNote) <= room - hintRoom) room -= hintRoom;
|
|
591
|
+
else modeHint = "";
|
|
592
|
+
// Too little room to say anything readable: leave the notice out.
|
|
593
|
+
if (room >= 8) modeLeft += NOTE_GAP + notePaint(fitPlain(modeNote, room));
|
|
594
|
+
}
|
|
552
595
|
const hintGap = modeHint ? w - 1 - visibleLen(modeLeft) - visibleLen(modeHint) : -1;
|
|
553
596
|
const modeRow =
|
|
554
597
|
hintGap >= HINT_MIN_GAP
|