bermudis-pi-goodies 0.24.0 → 0.24.2
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/README.md +4 -3
- package/clean-tui.ts +128 -44
- package/package.json +1 -1
- package/side.ts +8 -11
- package/vision.ts +8 -2
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ extensions. One entry point, thirteen independent features.
|
|
|
26
26
|
After publishing the package to npm:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
pi install npm:bermudis-pi-goodies@0.24.
|
|
29
|
+
pi install npm:bermudis-pi-goodies@0.24.2
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Remove any old `bermudis-pi-goodies.ts` symlink before reloading Pi. Each
|
|
@@ -390,9 +390,10 @@ Details worth knowing:
|
|
|
390
390
|
- The footer badge (`side: kilo/glm-5.3`) tracks the active side model,
|
|
391
391
|
including manual ctrl+l switches; it is restored when pi reopens a session
|
|
392
392
|
that is already on a side limb. Handoffs speak for the model actually
|
|
393
|
-
active at exit (a mid-side
|
|
393
|
+
active at exit (a mid-side ctrl+l switch changes the summarizer
|
|
394
394
|
and the handoff labels), while the transcript itself keeps per-turn model
|
|
395
|
-
attribution.
|
|
395
|
+
attribution. `/side` inside a side session just points you at ctrl+l —
|
|
396
|
+
pi's model selector is the swap mechanism.
|
|
396
397
|
- Thinking levels round-trip: pi's model switch applies the per-model default
|
|
397
398
|
level, so the marker snapshots the session level at entry and `/side-exit`
|
|
398
399
|
restores it explicitly — a session parked at `:low` comes back at `:low`,
|
package/clean-tui.ts
CHANGED
|
@@ -143,7 +143,10 @@ type Entry = {
|
|
|
143
143
|
* groups. Calls with no boundary between them share a segment.
|
|
144
144
|
*/
|
|
145
145
|
seg: number;
|
|
146
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* Monotonic creation number (its position in `entries` is
|
|
148
|
+
* `index - entriesBase`; the base advances when history is pruned).
|
|
149
|
+
*/
|
|
147
150
|
index: number;
|
|
148
151
|
result?: {
|
|
149
152
|
content: Array<{ type: string; text?: string; data?: string }>;
|
|
@@ -186,9 +189,38 @@ let curAssistantMessage: any;
|
|
|
186
189
|
let replaying = true;
|
|
187
190
|
const replaySegByToolCallId = new Map<string, number>();
|
|
188
191
|
const entries: Entry[] = [];
|
|
192
|
+
// `entries[0]`'s position in the monotonic entry numbering. Pruning drops
|
|
193
|
+
// from the front (pruneHistoryIfNeeded) and advances this base, so an
|
|
194
|
+
// entry's `index` stays stable for its lifetime while its position in the
|
|
195
|
+
// array shifts. Callers that need an array position subtract the base.
|
|
196
|
+
let entriesBase = 0;
|
|
189
197
|
const entryById = new Map<string, Entry>();
|
|
190
198
|
const invalidateById = new Map<string, () => void>();
|
|
191
199
|
|
|
200
|
+
// History cap. `entries` is walked linearly by stampSummaryRequested on
|
|
201
|
+
// every bash renderCall and by invalidateRowsForCommand on every summary
|
|
202
|
+
// landing, so an unbounded history makes long sessions slower as they grow
|
|
203
|
+
// (thousands of finished rows re-scanned per render). Prune down to KEEP
|
|
204
|
+
// once MAX is exceeded. Pruned rows keep whatever is already painted on
|
|
205
|
+
// screen — pi components hold their own output — but a LATE re-render of a
|
|
206
|
+
// pruned row (expanding a burst far up the transcript) re-registers it as a
|
|
207
|
+
// solo row: burst context beyond the cap is gone. Cheap by design: entries
|
|
208
|
+
// are small metadata; the result payloads belong to pi's components.
|
|
209
|
+
const MAX_HISTORY_ENTRIES = 600;
|
|
210
|
+
const HISTORY_KEEP_ENTRIES = 400;
|
|
211
|
+
|
|
212
|
+
/** Drop the oldest history once the cap is exceeded (see MAX_HISTORY_ENTRIES). */
|
|
213
|
+
function pruneHistoryIfNeeded(): void {
|
|
214
|
+
if (entries.length <= MAX_HISTORY_ENTRIES) return;
|
|
215
|
+
const drop = entries.length - HISTORY_KEEP_ENTRIES;
|
|
216
|
+
for (let i = 0; i < drop; i++) {
|
|
217
|
+
entryById.delete(entries[i].toolCallId);
|
|
218
|
+
invalidateById.delete(entries[i].toolCallId);
|
|
219
|
+
}
|
|
220
|
+
entries.splice(0, drop);
|
|
221
|
+
entriesBase += drop;
|
|
222
|
+
}
|
|
223
|
+
|
|
192
224
|
function upsertEntry(
|
|
193
225
|
toolCallId: string,
|
|
194
226
|
toolName: string,
|
|
@@ -215,10 +247,11 @@ function upsertEntry(
|
|
|
215
247
|
toolName,
|
|
216
248
|
args,
|
|
217
249
|
seg,
|
|
218
|
-
index: entries.length,
|
|
250
|
+
index: entries.length + entriesBase,
|
|
219
251
|
};
|
|
220
252
|
entries.push(e);
|
|
221
253
|
entryById.set(toolCallId, e);
|
|
254
|
+
pruneHistoryIfNeeded();
|
|
222
255
|
} else {
|
|
223
256
|
e.args = args;
|
|
224
257
|
}
|
|
@@ -326,7 +359,7 @@ function getBurstForId(
|
|
|
326
359
|
): { entries: Entry[]; index: number } | null {
|
|
327
360
|
const entry = entryById.get(toolCallId);
|
|
328
361
|
if (!entry) return null;
|
|
329
|
-
const idx = entry.index;
|
|
362
|
+
const idx = entry.index - entriesBase;
|
|
330
363
|
let start = idx;
|
|
331
364
|
while (start > 0 && shouldGroup(entries[start - 1], entries[start])) start--;
|
|
332
365
|
let end = idx;
|
|
@@ -362,11 +395,21 @@ function revalidateBurstsAround(changedId: string) {
|
|
|
362
395
|
// burst; pending/error flags surface on the leader). Rerender those runs —
|
|
363
396
|
// bounded, unlike scanning the whole history per result.
|
|
364
397
|
//
|
|
365
|
-
// The changed row
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
398
|
+
// The changed row's OWN run is included deliberately. pi's updateDisplay
|
|
399
|
+
// runs renderCall BEFORE renderResult in the same pass, so the box painted
|
|
400
|
+
// when a result arrives still reflects pre-result state. Neighbors were
|
|
401
|
+
// already woken here, but a row with no groupable neighbor had no wake-up
|
|
402
|
+
// at all: a solo row stayed "running" after it finished, a solo failure
|
|
403
|
+
// never turned red, and an image result at the END of a burst never split
|
|
404
|
+
// off into its own row (a middle image only appeared because the next job
|
|
405
|
+
// woke it). The self-wake closes all three.
|
|
406
|
+
//
|
|
407
|
+
// The synchronous re-entry (invalidate -> updateDisplay -> renderResult
|
|
408
|
+
// -> recordResult) terminates immediately: the contentRef check above
|
|
409
|
+
// classifies the replayed wrapper as a plain re-render, not a new result,
|
|
410
|
+
// so no further invalidation fires.
|
|
411
|
+
const idx = changed.index - entriesBase;
|
|
412
|
+
const ranges: Array<[number, number]> = [runAround(idx)];
|
|
370
413
|
if (idx > 0) ranges.push(runAround(idx - 1));
|
|
371
414
|
if (idx + 1 < entries.length) ranges.push(runAround(idx + 1));
|
|
372
415
|
const seen = new Set<number>();
|
|
@@ -450,6 +493,12 @@ const THINKING_SUMMARY_PROMPT =
|
|
|
450
493
|
const SUMMARY_ERROR_SNIPPET_CHARS = 200;
|
|
451
494
|
|
|
452
495
|
const summaryCache = new Map<string, string>();
|
|
496
|
+
// Cache cap: one entry per distinct long command, and the key is the FULL
|
|
497
|
+
// command text (heredocs make fat keys), so a long session accumulates
|
|
498
|
+
// without bound. FIFO eviction is the right shape — recent commands are the
|
|
499
|
+
// ones whose rows still re-render; an evicted command just falls back to
|
|
500
|
+
// its raw text if it ever reappears.
|
|
501
|
+
const SUMMARY_CACHE_MAX = 200;
|
|
453
502
|
const pendingSummaries = new Set<string>();
|
|
454
503
|
// Commands whose requests were deferred by the inflight cap or a failure
|
|
455
504
|
// backoff. Drained whenever a slot frees (request settle) or a later
|
|
@@ -568,6 +617,24 @@ export function __clearSummaryCache(): void {
|
|
|
568
617
|
summaryFailureWaveObservedAt = 0;
|
|
569
618
|
}
|
|
570
619
|
|
|
620
|
+
/** History sizes for tests (entries cap + id maps must stay in lockstep). */
|
|
621
|
+
export function __historyStatsForTesting(): {
|
|
622
|
+
entries: number;
|
|
623
|
+
entryById: number;
|
|
624
|
+
invalidateById: number;
|
|
625
|
+
} {
|
|
626
|
+
return {
|
|
627
|
+
entries: entries.length,
|
|
628
|
+
entryById: entryById.size,
|
|
629
|
+
invalidateById: invalidateById.size,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Saved-summary count for tests (the cache is capped, not unbounded). */
|
|
634
|
+
export function __summaryCacheSizeForTesting(): number {
|
|
635
|
+
return summaryCache.size;
|
|
636
|
+
}
|
|
637
|
+
|
|
571
638
|
function isSummarizable(cmd: string): boolean {
|
|
572
639
|
return cmd.length > SUMMARY_THRESHOLD_CHARS;
|
|
573
640
|
}
|
|
@@ -1275,6 +1342,11 @@ function startSummaryRequest(cmd: string): void {
|
|
|
1275
1342
|
pendingSummaries.delete(cmd);
|
|
1276
1343
|
const recovered = noteSummarySuccess(requestStartedAt);
|
|
1277
1344
|
summaryCache.set(cmd, normalizeSummary(result.text));
|
|
1345
|
+
while (summaryCache.size > SUMMARY_CACHE_MAX) {
|
|
1346
|
+
const oldest = summaryCache.keys().next().value;
|
|
1347
|
+
if (oldest === undefined) break;
|
|
1348
|
+
summaryCache.delete(oldest);
|
|
1349
|
+
}
|
|
1278
1350
|
logGoodiesEvent({
|
|
1279
1351
|
type: "summary_request",
|
|
1280
1352
|
outcome: "ok",
|
|
@@ -1751,6 +1823,25 @@ function makeBox(
|
|
|
1751
1823
|
return box;
|
|
1752
1824
|
}
|
|
1753
1825
|
|
|
1826
|
+
/**
|
|
1827
|
+
* The shared expanded-detail rule (the read view's): preview the first `max`
|
|
1828
|
+
* lines, and whenever anything was cut, append a muted "... N more lines"
|
|
1829
|
+
* note — a cut with no note reads as the complete output. Every burst tool's
|
|
1830
|
+
* grouped details use this so the expanded view stays even across tools;
|
|
1831
|
+
* missing info is skipped or guarded by the callers, never interpolated.
|
|
1832
|
+
*/
|
|
1833
|
+
export function previewLines(txt: string, theme: any, max = 12): string {
|
|
1834
|
+
const lines = txt.split("\n");
|
|
1835
|
+
const shown = lines
|
|
1836
|
+
.slice(0, max)
|
|
1837
|
+
.map((l) => theme.fg("toolOutput", l))
|
|
1838
|
+
.join("\n");
|
|
1839
|
+
const remaining = lines.length - max;
|
|
1840
|
+
return remaining > 0
|
|
1841
|
+
? `${shown}\n${theme.fg("muted", `... ${remaining} more lines`)}`
|
|
1842
|
+
: shown;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1754
1845
|
/** While set, sibling extension tools in this package render in burst style
|
|
1755
1846
|
* (same contract @bermudi/pi-codex mirrors via Symbol.for). Set at load,
|
|
1756
1847
|
* cleared when the feature is disabled, so /reload converges. Consumers must
|
|
@@ -1835,6 +1926,9 @@ export function createBurstRenderer(spec: BurstToolSpec): {
|
|
|
1835
1926
|
|
|
1836
1927
|
// solo
|
|
1837
1928
|
let line = spec.soloHeader(args, theme, ctx);
|
|
1929
|
+
// Image parity with grouped bullets: a solo image read is visibly
|
|
1930
|
+
// marked too (the image itself is painted by pi's image layer).
|
|
1931
|
+
if (entry.hasImage) line += theme.fg("success", " [image]");
|
|
1838
1932
|
if (ctx.expanded) {
|
|
1839
1933
|
const extra = spec.soloExpanded(entry, args, theme);
|
|
1840
1934
|
if (extra) line += `\n${extra}`;
|
|
@@ -2067,6 +2161,7 @@ export default function cleanTui(pi: ExtensionAPI): void {
|
|
|
2067
2161
|
curAssistantMessage = undefined;
|
|
2068
2162
|
replaying = true;
|
|
2069
2163
|
entries.length = 0;
|
|
2164
|
+
entriesBase = 0;
|
|
2070
2165
|
entryById.clear();
|
|
2071
2166
|
invalidateById.clear();
|
|
2072
2167
|
pendingSummaries.clear();
|
|
@@ -2081,11 +2176,16 @@ export default function cleanTui(pi: ExtensionAPI): void {
|
|
|
2081
2176
|
// undefined and every TUI failure took the console.error branch, flashing
|
|
2082
2177
|
// raw stderr across the terminal; the widget never showed.
|
|
2083
2178
|
const ui = (ctx as { ui?: Partial<SummaryUi> } | undefined)?.ui;
|
|
2084
|
-
|
|
2085
|
-
|
|
2179
|
+
if (ui && typeof ui.setWidget === "function") {
|
|
2180
|
+
// Call through the ui object — never through a detached copy of the
|
|
2181
|
+
// method. The receiver IS the link back to pi: today ctx.ui.setWidget
|
|
2182
|
+
// arrives as a closure so detaching happens to work, but that is an
|
|
2183
|
+
// implementation detail; a prototype method would lose `this` and
|
|
2184
|
+
// silently break. Tests inject plain functions, so only calling
|
|
2185
|
+
// through the object keeps this honest.
|
|
2086
2186
|
summaryUi = {
|
|
2087
2187
|
hasUI: ctx.hasUI,
|
|
2088
|
-
setWidget: (key, content) => setWidget(key, content),
|
|
2188
|
+
setWidget: (key, content) => ui.setWidget!(key, content),
|
|
2089
2189
|
};
|
|
2090
2190
|
}
|
|
2091
2191
|
clearSummaryPauseWidget();
|
|
@@ -2146,7 +2246,13 @@ export default function cleanTui(pi: ExtensionAPI): void {
|
|
|
2146
2246
|
...createBurstRenderer(spec),
|
|
2147
2247
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
2148
2248
|
const tool = (getBuiltInTools(ctx.cwd) as any)[spec.name];
|
|
2149
|
-
|
|
2249
|
+
// Forward the ENTIRE pi call, ctx included. The built-ins mostly
|
|
2250
|
+
// ignore ctx today (they fall back to their construction cwd), but
|
|
2251
|
+
// not entirely: read's execute already reads ctx.model to append the
|
|
2252
|
+
// visionless-model image note, and any future field would be
|
|
2253
|
+
// silently dropped by a partial passthrough — the kind of quiet
|
|
2254
|
+
// breakage nothing tests because "it works today".
|
|
2255
|
+
return tool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
2150
2256
|
},
|
|
2151
2257
|
});
|
|
2152
2258
|
}
|
|
@@ -2171,16 +2277,9 @@ export default function cleanTui(pi: ExtensionAPI): void {
|
|
|
2171
2277
|
}
|
|
2172
2278
|
const txt = resultText(e.result as any);
|
|
2173
2279
|
if (!txt) continue;
|
|
2174
|
-
|
|
2175
|
-
.
|
|
2176
|
-
|
|
2177
|
-
.map((l) => theme.fg("toolOutput", l))
|
|
2178
|
-
.join("\n");
|
|
2179
|
-
const remaining = txt.split("\n").length - 12;
|
|
2180
|
-
let block = `\n${theme.fg("muted", `— ${shortenPath(e.args.path || "...")}`)}:\n${preview}`;
|
|
2181
|
-
if (remaining > 0)
|
|
2182
|
-
block += `\n${theme.fg("muted", `... ${remaining} more lines`)}`;
|
|
2183
|
-
details.push(block);
|
|
2280
|
+
details.push(
|
|
2281
|
+
`\n${theme.fg("muted", `— ${shortenPath(e.args.path || "...")}`)}:\n${previewLines(txt, theme)}`,
|
|
2282
|
+
);
|
|
2184
2283
|
}
|
|
2185
2284
|
return details.length ? `\n${details.join("\n")}` : "";
|
|
2186
2285
|
},
|
|
@@ -2232,12 +2331,9 @@ export default function cleanTui(pi: ExtensionAPI): void {
|
|
|
2232
2331
|
details.push(`\n${theme.fg("muted", `— $ ${cmd}`)}`);
|
|
2233
2332
|
continue;
|
|
2234
2333
|
}
|
|
2235
|
-
|
|
2236
|
-
.
|
|
2237
|
-
|
|
2238
|
-
.map((l) => theme.fg("toolOutput", l))
|
|
2239
|
-
.join("\n");
|
|
2240
|
-
details.push(`\n${theme.fg("muted", `— $ ${cmd}`)}:\n${preview}`);
|
|
2334
|
+
details.push(
|
|
2335
|
+
`\n${theme.fg("muted", `— $ ${cmd}`)}:\n${previewLines(txt, theme)}`,
|
|
2336
|
+
);
|
|
2241
2337
|
}
|
|
2242
2338
|
return details.length ? `\n${details.join("\n")}` : "";
|
|
2243
2339
|
},
|
|
@@ -2310,7 +2406,7 @@ export default function cleanTui(pi: ExtensionAPI): void {
|
|
|
2310
2406
|
.map((e) => {
|
|
2311
2407
|
const txt = e.result ? resultText(e.result as any) : undefined;
|
|
2312
2408
|
return txt
|
|
2313
|
-
? `\n${theme.fg("muted", `— ${shortenPath(e.args.path || "...")}`)}:\n${
|
|
2409
|
+
? `\n${theme.fg("muted", `— ${shortenPath(e.args.path || "...")}`)}:\n${previewLines(txt, theme)}`
|
|
2314
2410
|
: "";
|
|
2315
2411
|
})
|
|
2316
2412
|
.join("");
|
|
@@ -2338,11 +2434,7 @@ export default function cleanTui(pi: ExtensionAPI): void {
|
|
|
2338
2434
|
? resultText(e.result as any)?.trim()
|
|
2339
2435
|
: undefined;
|
|
2340
2436
|
return txt
|
|
2341
|
-
? `\n${theme.fg("muted", `— ${e.args.pattern}`)}:\n${txt
|
|
2342
|
-
.split("\n")
|
|
2343
|
-
.slice(0, 10)
|
|
2344
|
-
.map((l) => theme.fg("toolOutput", l))
|
|
2345
|
-
.join("\n")}`
|
|
2437
|
+
? `\n${theme.fg("muted", `— ${e.args.pattern ?? ""}`)}:\n${previewLines(txt, theme)}`
|
|
2346
2438
|
: "";
|
|
2347
2439
|
})
|
|
2348
2440
|
.join("");
|
|
@@ -2375,11 +2467,7 @@ export default function cleanTui(pi: ExtensionAPI): void {
|
|
|
2375
2467
|
? resultText(e.result as any)?.trim()
|
|
2376
2468
|
: undefined;
|
|
2377
2469
|
return txt
|
|
2378
|
-
? `\n${theme.fg("muted", `— /${e.args.pattern}/`)}:\n${txt
|
|
2379
|
-
.split("\n")
|
|
2380
|
-
.slice(0, 10)
|
|
2381
|
-
.map((l) => theme.fg("toolOutput", l))
|
|
2382
|
-
.join("\n")}`
|
|
2470
|
+
? `\n${theme.fg("muted", `— /${e.args.pattern ?? ""}/`)}:\n${previewLines(txt, theme)}`
|
|
2383
2471
|
: "";
|
|
2384
2472
|
})
|
|
2385
2473
|
.join("");
|
|
@@ -2414,11 +2502,7 @@ export default function cleanTui(pi: ExtensionAPI): void {
|
|
|
2414
2502
|
? resultText(e.result as any)?.trim()
|
|
2415
2503
|
: undefined;
|
|
2416
2504
|
return txt
|
|
2417
|
-
? `\n${theme.fg("muted", `— ${shortenPath(e.args.path || ".")}`)}:\n${txt
|
|
2418
|
-
.split("\n")
|
|
2419
|
-
.slice(0, 10)
|
|
2420
|
-
.map((l) => theme.fg("toolOutput", l))
|
|
2421
|
-
.join("\n")}`
|
|
2505
|
+
? `\n${theme.fg("muted", `— ${shortenPath(e.args.path || ".")}`)}:\n${previewLines(txt, theme)}`
|
|
2422
2506
|
: "";
|
|
2423
2507
|
})
|
|
2424
2508
|
.join("");
|
package/package.json
CHANGED
package/side.ts
CHANGED
|
@@ -508,7 +508,7 @@ function sideMarkerFromBranch(
|
|
|
508
508
|
|
|
509
509
|
/**
|
|
510
510
|
* The model actually serving this side limb: the branch's last model_change
|
|
511
|
-
* after the marker (a mid-side
|
|
511
|
+
* after the marker (a mid-side ctrl+l switch updates it), falling
|
|
512
512
|
* back to the model the marker recorded.
|
|
513
513
|
*/
|
|
514
514
|
export function activeSideModel(
|
|
@@ -605,7 +605,7 @@ export default function side(pi: ExtensionAPI): void {
|
|
|
605
605
|
|
|
606
606
|
// Restore the badge when a session resumes already on a side limb. The
|
|
607
607
|
// effective model is the branch's last model_change (a mid-side
|
|
608
|
-
//
|
|
608
|
+
// ctrl+l switch updates it; the marker keeps the original).
|
|
609
609
|
pi.on("session_start", (_event, ctx) => {
|
|
610
610
|
modelRegistryRef = ctx.modelRegistry; // for /side argument completions
|
|
611
611
|
const marker = sideMarkerFromBranch(ctx);
|
|
@@ -675,9 +675,12 @@ export default function side(pi: ExtensionAPI): void {
|
|
|
675
675
|
const markerIdx = findSideBoundary(branch);
|
|
676
676
|
const arg = args.trim();
|
|
677
677
|
|
|
678
|
-
|
|
678
|
+
// Model switching inside a side session is pi's own ctrl+l — the
|
|
679
|
+
// model_select handler keeps the badge honest and /side-exit reads the
|
|
680
|
+
// live model — so /side has no in-side behavior at all.
|
|
681
|
+
if (markerIdx !== -1) {
|
|
679
682
|
ctx.ui.notify(
|
|
680
|
-
"side: already in a side session —
|
|
683
|
+
"side: already in a side session — ctrl+l switches the side model, /side-exit to return",
|
|
681
684
|
"warning",
|
|
682
685
|
);
|
|
683
686
|
return;
|
|
@@ -736,12 +739,6 @@ export default function side(pi: ExtensionAPI): void {
|
|
|
736
739
|
}
|
|
737
740
|
updateBadge(ctx, model);
|
|
738
741
|
|
|
739
|
-
if (markerIdx !== -1) {
|
|
740
|
-
ctx.ui.notify(`side model swapped to ${modelRef(model)}`, "info");
|
|
741
|
-
logGoodiesEvent({ type: "side_model_swap", model: modelRef(model) });
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
742
|
// setModel appends a model_change entry; the marker branches from the
|
|
746
743
|
// new leaf so the parked tip includes it.
|
|
747
744
|
const mainTipId = ctx.sessionManager.getLeafId();
|
|
@@ -798,7 +795,7 @@ export default function side(pi: ExtensionAPI): void {
|
|
|
798
795
|
const branch = ctx.sessionManager.getBranch();
|
|
799
796
|
const markerIdx = findSideBoundary(branch);
|
|
800
797
|
// The handoff must speak for the model actually serving the side limb
|
|
801
|
-
// at exit — a mid-side
|
|
798
|
+
// at exit — a mid-side ctrl+l switch updates model_change
|
|
802
799
|
// entries, not the marker.
|
|
803
800
|
const effectiveSide = activeSideModel(branch, marker.data);
|
|
804
801
|
const sideEntries = branch.slice(markerIdx + 1);
|
package/vision.ts
CHANGED
|
@@ -45,6 +45,7 @@ import type {
|
|
|
45
45
|
import {
|
|
46
46
|
createBurstRenderer,
|
|
47
47
|
isCleanTuiActive,
|
|
48
|
+
previewLines,
|
|
48
49
|
shortenPath,
|
|
49
50
|
} from "./clean-tui.ts";
|
|
50
51
|
|
|
@@ -153,11 +154,16 @@ const visionBurstSpec = {
|
|
|
153
154
|
groupedDetails(entries: any[], theme: any) {
|
|
154
155
|
return entries
|
|
155
156
|
.map((e) => {
|
|
156
|
-
|
|
157
|
+
// Read-view rule: info that is missing is skipped, never rendered as
|
|
158
|
+
// junk (no `: ""` when a prompt is absent) — and long answers get the
|
|
159
|
+
// same 12-line preview + "... N more lines" note as every other tool.
|
|
160
|
+
const follow = e.args.followUp ? " (follow-up)" : "";
|
|
161
|
+
const q = questionPreview(e.args.prompt, 90);
|
|
162
|
+
const label = `— ${shortenPath(e.args.path || "...")}${follow}${q ? `: "${q}"` : ""}`;
|
|
157
163
|
if (!e.result) return `\n${theme.fg("warning", `${label} (pending)`)}`;
|
|
158
164
|
const txt = answerText(e.result);
|
|
159
165
|
if (!txt) return `\n${theme.fg("muted", label)}`;
|
|
160
|
-
return `\n${theme.fg("muted", label)}\n${e.isError ? theme.fg("error", txt) :
|
|
166
|
+
return `\n${theme.fg("muted", label)}\n${e.isError ? theme.fg("error", txt) : previewLines(txt, theme)}`;
|
|
161
167
|
})
|
|
162
168
|
.join("");
|
|
163
169
|
},
|