@oh-my-pi/pi-coding-agent 15.10.9 → 15.10.10
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/CHANGELOG.md +26 -0
- package/dist/types/config/model-registry.d.ts +13 -0
- package/dist/types/config/settings-schema.d.ts +0 -9
- package/dist/types/debug/terminal-info.d.ts +0 -1
- package/dist/types/modes/components/transcript-container.d.ts +12 -26
- package/dist/types/task/discovery.d.ts +1 -2
- package/dist/types/tiny/title-client.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +2 -0
- package/package.json +9 -9
- package/src/cli/list-models.ts +5 -11
- package/src/config/model-registry.ts +91 -20
- package/src/config/settings-schema.ts +0 -10
- package/src/debug/terminal-info.ts +0 -3
- package/src/edit/diff.ts +48 -15
- package/src/eval/js/shared/rewrite-imports.ts +9 -1
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/modes/components/model-selector.ts +62 -52
- package/src/modes/components/transcript-container.ts +134 -107
- package/src/modes/controllers/event-controller.ts +0 -45
- package/src/modes/controllers/input-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +0 -4
- package/src/modes/interactive-mode.ts +2 -10
- package/src/prompts/system/system-prompt.md +3 -3
- package/src/prompts/tools/bash.md +3 -3
- package/src/prompts/tools/todo.md +5 -1
- package/src/task/discovery.ts +17 -24
- package/src/tiny/title-client.ts +6 -3
- package/src/tools/todo.ts +16 -7
- package/src/web/search/providers/anthropic.ts +8 -2
|
@@ -263,21 +263,26 @@ export class ModelSelectorComponent extends Container {
|
|
|
263
263
|
// Add bottom border
|
|
264
264
|
this.addChild(new DynamicBorder());
|
|
265
265
|
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
266
|
+
// Hydrate synchronously from the current registry snapshot so the first
|
|
267
|
+
// Enter after opening the selector acts on cached models instead of being
|
|
268
|
+
// dropped while the offline refresh promise is still pending. This stays
|
|
269
|
+
// on the open path, so it must remain cheap — heavy lifting lives in the
|
|
270
|
+
// registry's one-pass getCanonicalModelSelections.
|
|
271
|
+
this.#syncFromRegistryState();
|
|
272
|
+
|
|
273
|
+
// Reconcile with cached discovery state in the background. A --models
|
|
274
|
+
// scope is registry-independent, so the offline reload would only repeat
|
|
275
|
+
// the synchronous hydration above.
|
|
276
|
+
if (this.#scopedModels.length === 0) {
|
|
277
|
+
this.#modelRegistry
|
|
278
|
+
.refresh("offline")
|
|
279
|
+
.then(() => this.#syncFromRegistryState())
|
|
280
|
+
.catch(error => {
|
|
281
|
+
this.#errorMessage = error instanceof Error ? error.message : String(error);
|
|
282
|
+
this.#updateList();
|
|
283
|
+
})
|
|
284
|
+
.finally(() => this.#tui.requestRender());
|
|
285
|
+
}
|
|
281
286
|
}
|
|
282
287
|
|
|
283
288
|
#buildMenuRoleActions(): void {
|
|
@@ -477,37 +482,30 @@ export class ModelSelectorComponent extends Container {
|
|
|
477
482
|
|
|
478
483
|
const candidates = models.map(item => item.model);
|
|
479
484
|
this.#loadRoleModels(candidates);
|
|
480
|
-
const
|
|
485
|
+
const canonicalSelections = this.#modelRegistry.getCanonicalModelSelections({
|
|
481
486
|
availableOnly: this.#scopedModels.length === 0,
|
|
482
487
|
candidates,
|
|
483
488
|
});
|
|
484
|
-
const canonicalModels =
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
variantCount: record.variants.length,
|
|
505
|
-
searchText,
|
|
506
|
-
normalizedSearchText: normalizeSearchText(searchText),
|
|
507
|
-
compactSearchText: compactSearchText(searchText),
|
|
508
|
-
};
|
|
509
|
-
})
|
|
510
|
-
.filter((item): item is CanonicalModelItem => item !== undefined);
|
|
489
|
+
const canonicalModels = canonicalSelections.map(({ record, model: selectedModel }): CanonicalModelItem => {
|
|
490
|
+
const searchText = [
|
|
491
|
+
record.id,
|
|
492
|
+
record.name,
|
|
493
|
+
selectedModel.provider,
|
|
494
|
+
selectedModel.id,
|
|
495
|
+
selectedModel.name,
|
|
496
|
+
...record.variants.flatMap(variant => [variant.selector, variant.model.name]),
|
|
497
|
+
].join(" ");
|
|
498
|
+
return {
|
|
499
|
+
kind: "canonical",
|
|
500
|
+
id: record.id,
|
|
501
|
+
model: selectedModel,
|
|
502
|
+
selector: record.id,
|
|
503
|
+
variantCount: record.variants.length,
|
|
504
|
+
searchText,
|
|
505
|
+
normalizedSearchText: normalizeSearchText(searchText),
|
|
506
|
+
compactSearchText: compactSearchText(searchText),
|
|
507
|
+
};
|
|
508
|
+
});
|
|
511
509
|
|
|
512
510
|
this.#sortModels(models);
|
|
513
511
|
this.#sortCanonicalModels(canonicalModels);
|
|
@@ -523,12 +521,27 @@ export class ModelSelectorComponent extends Container {
|
|
|
523
521
|
);
|
|
524
522
|
}
|
|
525
523
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
524
|
+
/**
|
|
525
|
+
* Rebuild the visible model lists from the registry's in-memory state.
|
|
526
|
+
* Re-entrant: runs once synchronously at construction and again whenever a
|
|
527
|
+
* background refresh lands, so it re-applies the live search query and pins
|
|
528
|
+
* the highlighted item by selector — a refresh that reorders or inserts
|
|
529
|
+
* models must not yank the user's selection out from under a pending Enter.
|
|
530
|
+
*/
|
|
531
|
+
#syncFromRegistryState(): void {
|
|
532
|
+
const selectedKey = this.#getSelectedItem()?.selector;
|
|
531
533
|
this.#loadModelsFromCurrentRegistryState();
|
|
534
|
+
this.#buildProviderTabs();
|
|
535
|
+
this.#updateTabBar();
|
|
536
|
+
this.#applyTabFilter();
|
|
537
|
+
if (selectedKey) {
|
|
538
|
+
const visibleItems = this.#getVisibleItems();
|
|
539
|
+
const restoredIndex = visibleItems.findIndex(item => item.selector === selectedKey);
|
|
540
|
+
if (restoredIndex >= 0 && restoredIndex !== this.#selectedIndex) {
|
|
541
|
+
this.#selectedIndex = this.#coerceSelectedIndex(restoredIndex, visibleItems);
|
|
542
|
+
this.#updateList();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
532
545
|
}
|
|
533
546
|
|
|
534
547
|
#buildProviderTabs(): void {
|
|
@@ -631,10 +644,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
631
644
|
// here must stay purely in-memory — do not call modelRegistry.refresh()
|
|
632
645
|
// again or tab switches will pay an extra whole-registry reload after the
|
|
633
646
|
// network round-trip completes.
|
|
634
|
-
this.#
|
|
635
|
-
this.#buildProviderTabs();
|
|
636
|
-
this.#updateTabBar();
|
|
637
|
-
this.#applyTabFilter();
|
|
647
|
+
this.#syncFromRegistryState();
|
|
638
648
|
} catch (error) {
|
|
639
649
|
this.#errorMessage = error instanceof Error ? error.message : String(error);
|
|
640
650
|
this.#updateList();
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import { type Component, Container, type NativeScrollbackLiveRegion
|
|
1
|
+
import { type Component, Container, type NativeScrollbackLiveRegion } from "@oh-my-pi/pi-tui";
|
|
2
2
|
|
|
3
|
-
const kSnapshot = Symbol("transcript.
|
|
3
|
+
const kSnapshot = Symbol("transcript.liveDiffSnapshot");
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Per-block diff cache: the block's previous stripped contribution plus the
|
|
7
|
+
* derived append-only state. Purely an input to {@link deriveLiveCommitState}
|
|
8
|
+
* for still-live blocks — it is never replayed as render output. Every block
|
|
9
|
+
* renders its current content on every frame.
|
|
10
|
+
*/
|
|
11
|
+
interface LiveDiffSnapshot {
|
|
6
12
|
width: number;
|
|
7
13
|
lines: string[];
|
|
8
14
|
generation: number;
|
|
@@ -12,10 +18,19 @@ interface FrozenRender {
|
|
|
12
18
|
* append-only status. `0` means the block is not under rewrite suspicion.
|
|
13
19
|
*/
|
|
14
20
|
volatileCooldown: number;
|
|
21
|
+
/**
|
|
22
|
+
* Stable-prefix ratchet (see {@link deriveLiveCommitState}): leading rows
|
|
23
|
+
* promoted as commit-safe because they stayed visibly identical for
|
|
24
|
+
* {@link STABLE_PREFIX_COMMIT_FRAMES} consecutive frames, plus the in-flight
|
|
25
|
+
* candidate run and its age.
|
|
26
|
+
*/
|
|
27
|
+
stablePrefixLength: number;
|
|
28
|
+
candidatePrefixLength: number;
|
|
29
|
+
candidatePrefixAge: number;
|
|
15
30
|
}
|
|
16
31
|
|
|
17
32
|
interface SnapshotCarrier {
|
|
18
|
-
[kSnapshot]?:
|
|
33
|
+
[kSnapshot]?: LiveDiffSnapshot;
|
|
19
34
|
}
|
|
20
35
|
|
|
21
36
|
/**
|
|
@@ -56,6 +71,9 @@ function stripPlainBlankEdges(lines: string[]): string[] {
|
|
|
56
71
|
interface LiveCommitState {
|
|
57
72
|
appendOnly: boolean;
|
|
58
73
|
volatileCooldown: number;
|
|
74
|
+
stablePrefixLength: number;
|
|
75
|
+
candidatePrefixLength: number;
|
|
76
|
+
candidatePrefixAge: number;
|
|
59
77
|
safeLength: number;
|
|
60
78
|
}
|
|
61
79
|
|
|
@@ -72,6 +90,22 @@ interface LiveCommitState {
|
|
|
72
90
|
*/
|
|
73
91
|
const VOLATILE_REARM_FRAMES = 30;
|
|
74
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Consecutive frames a leading row run must stay visibly identical before it
|
|
95
|
+
* is promoted as commit-safe even though the block's tail keeps rewriting.
|
|
96
|
+
* Append-only detection alone is all-or-nothing per block: one perpetually
|
|
97
|
+
* ticking row (a task tool's progress tree, per-agent cost/tool counters, a
|
|
98
|
+
* log line spinner) suspends commits for the WHOLE block forever, so once the
|
|
99
|
+
* block outgrows the viewport its static head — e.g. a task's prompt/context
|
|
100
|
+
* markdown — is neither committed to native scrollback nor on screen: the
|
|
101
|
+
* transcript reads as cut off for the entire (possibly minutes-long) run.
|
|
102
|
+
* The ratchet commits the settled head while only the genuinely volatile tail
|
|
103
|
+
* stays deferred. If a promoted row is later rewritten (a collapsing
|
|
104
|
+
* preview), the engine's committed-prefix audit re-anchors and recommits —
|
|
105
|
+
* duplication, never loss — and the ratchet retreats to the divergence.
|
|
106
|
+
*/
|
|
107
|
+
const STABLE_PREFIX_COMMIT_FRAMES = 30;
|
|
108
|
+
|
|
75
109
|
/**
|
|
76
110
|
* Visible-content form of a row: SGR/OSC bytes and trailing pad spaces are
|
|
77
111
|
* write framing, not content. A styled line's closing escape moves when the
|
|
@@ -91,10 +125,10 @@ function rowsVisiblyEqual(prev: string, cur: string): boolean {
|
|
|
91
125
|
}
|
|
92
126
|
|
|
93
127
|
function hasValidSnapshot(
|
|
94
|
-
snapshot:
|
|
128
|
+
snapshot: LiveDiffSnapshot | undefined,
|
|
95
129
|
width: number,
|
|
96
130
|
generation: number,
|
|
97
|
-
): snapshot is
|
|
131
|
+
): snapshot is LiveDiffSnapshot {
|
|
98
132
|
return snapshot !== undefined && snapshot.generation === generation && snapshot.width === width;
|
|
99
133
|
}
|
|
100
134
|
|
|
@@ -113,16 +147,22 @@ function commonSuffixLength(prev: string[], cur: string[], prefixLength: number)
|
|
|
113
147
|
}
|
|
114
148
|
|
|
115
149
|
function deriveLiveCommitState(
|
|
116
|
-
previous:
|
|
150
|
+
previous: LiveDiffSnapshot | undefined,
|
|
117
151
|
current: string[],
|
|
118
152
|
width: number,
|
|
119
153
|
generation: number,
|
|
120
154
|
): LiveCommitState {
|
|
121
155
|
let appendOnly = false;
|
|
122
156
|
let volatileCooldown = 0;
|
|
157
|
+
let stablePrefixLength = 0;
|
|
158
|
+
let candidatePrefixLength = 0;
|
|
159
|
+
let candidatePrefixAge = 0;
|
|
123
160
|
if (hasValidSnapshot(previous, width, generation)) {
|
|
124
161
|
appendOnly = previous.appendOnly;
|
|
125
162
|
volatileCooldown = previous.volatileCooldown;
|
|
163
|
+
stablePrefixLength = previous.stablePrefixLength;
|
|
164
|
+
candidatePrefixLength = previous.candidatePrefixLength;
|
|
165
|
+
candidatePrefixAge = previous.candidatePrefixAge;
|
|
126
166
|
|
|
127
167
|
const prefixLength = commonPrefixLength(previous.lines, current);
|
|
128
168
|
const staticRender = prefixLength === previous.lines.length && prefixLength === current.length;
|
|
@@ -163,65 +203,79 @@ function deriveLiveCommitState(
|
|
|
163
203
|
}
|
|
164
204
|
}
|
|
165
205
|
if (cleanFrame && volatileCooldown > 0) volatileCooldown--;
|
|
206
|
+
|
|
207
|
+
// Stable-prefix ratchet, independent of append-only. `prefixLength` is
|
|
208
|
+
// this frame's visibly-unchanged leading run; the candidate accumulates
|
|
209
|
+
// the MINIMUM prefix across a STABLE_PREFIX_COMMIT_FRAMES window, so
|
|
210
|
+
// promotion means every promoted row stayed identical for the whole
|
|
211
|
+
// window (row r is inside frame i's common prefix iff r < p_i, so
|
|
212
|
+
// r < min(p) holds for every frame of the window). A row settling
|
|
213
|
+
// mid-window promotes at most two windows later. A change above the
|
|
214
|
+
// already-promoted run retreats it to the divergence — the engine
|
|
215
|
+
// audit owns any rows that already committed (recommit, never loss).
|
|
216
|
+
if (prefixLength < stablePrefixLength) {
|
|
217
|
+
stablePrefixLength = prefixLength;
|
|
218
|
+
candidatePrefixLength = prefixLength;
|
|
219
|
+
candidatePrefixAge = 0;
|
|
220
|
+
} else {
|
|
221
|
+
candidatePrefixLength =
|
|
222
|
+
candidatePrefixAge === 0 ? prefixLength : Math.min(candidatePrefixLength, prefixLength);
|
|
223
|
+
candidatePrefixAge++;
|
|
224
|
+
if (candidatePrefixAge >= STABLE_PREFIX_COMMIT_FRAMES) {
|
|
225
|
+
stablePrefixLength = candidatePrefixLength;
|
|
226
|
+
candidatePrefixLength = prefixLength;
|
|
227
|
+
candidatePrefixAge = 0;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
166
230
|
}
|
|
167
231
|
|
|
168
232
|
return {
|
|
169
233
|
appendOnly,
|
|
170
234
|
volatileCooldown,
|
|
171
|
-
|
|
235
|
+
stablePrefixLength,
|
|
236
|
+
candidatePrefixLength,
|
|
237
|
+
candidatePrefixAge,
|
|
238
|
+
// An append-only block's whole body is committable; otherwise the
|
|
239
|
+
// settled head still is — only the volatile tail stays deferred.
|
|
240
|
+
safeLength: appendOnly ? current.length : stablePrefixLength,
|
|
172
241
|
};
|
|
173
242
|
}
|
|
174
243
|
|
|
175
244
|
/**
|
|
176
|
-
* Transcript container that
|
|
177
|
-
* the
|
|
178
|
-
*
|
|
245
|
+
* Transcript container that always renders every block's current content and
|
|
246
|
+
* reports the live-region seam (`NativeScrollbackLiveRegion`) that gates the
|
|
247
|
+
* engine's append-only scrollback commits.
|
|
179
248
|
*
|
|
180
|
-
*
|
|
181
|
-
* the
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* This container provides that guarantee: a block's render is snapshotted while
|
|
190
|
-
* it is the live (bottom-most) block, and once a newer block is appended it
|
|
191
|
-
* replays the snapshot instead of recomputing. Mutations after a block leaves
|
|
192
|
-
* live are intentionally deferred until the next checkpoint {@link thaw} (prompt
|
|
193
|
-
* submit → native-scrollback rebuild), where the whole transcript is replayed
|
|
194
|
-
* and any drift reconciles safely. On terminals that can rebuild history this
|
|
195
|
-
* freezing is unnecessary, so it renders every block live for full fidelity.
|
|
249
|
+
* The engine never rewrites committed history: rows above the seam that have
|
|
250
|
+
* entered the tape keep whatever bytes they were committed with ("let the
|
|
251
|
+
* history be"), while the visible window always repaints from each block's
|
|
252
|
+
* latest render — a late tool result, a post-finalize error pin, or an expand
|
|
253
|
+
* toggle is always reflected on screen. Blocks that are still mutating (an
|
|
254
|
+
* unfinalized tool, a streaming assistant message) stay below the seam so
|
|
255
|
+
* their rows do not enter history while they can still change; a streaming
|
|
256
|
+
* block whose render grows append-only deepens the seam through its settled
|
|
257
|
+
* head so a long reply's scrolled-off rows still reach scrollback mid-stream.
|
|
196
258
|
*/
|
|
197
259
|
export class TranscriptContainer extends Container implements NativeScrollbackLiveRegion {
|
|
198
|
-
// Bumped to
|
|
199
|
-
// honored when its stored generation
|
|
260
|
+
// Bumped to retire every block's diff snapshot at once (theme change /
|
|
261
|
+
// clear); a snapshot is only honored when its stored generation matches.
|
|
200
262
|
#generation = 0;
|
|
201
|
-
// Line index where the live (repaintable) region began on the previous
|
|
202
|
-
// render — the start of the earliest still-mutating block, or the bottom
|
|
203
|
-
// block when everything is finalized. A block leaves the live region only
|
|
204
|
-
// once it has finalized AND a finalized block sits below it; the frame it
|
|
205
|
-
// crosses out is recomputed so it freezes at its true final content, not the
|
|
206
|
-
// mid-stream snapshot it last rendered while live (TUI render coalescing can
|
|
207
|
-
// advance a block's content in the very frame it stops being live).
|
|
208
|
-
#prevLiveStartIndex = 0;
|
|
209
263
|
// Local line index where the current live region begins in the most recent
|
|
210
|
-
// render. TUI
|
|
211
|
-
//
|
|
264
|
+
// render. TUI commits rows to native scrollback only above this seam (or
|
|
265
|
+
// the deeper commit-safe end below).
|
|
212
266
|
#nativeScrollbackLiveRegionStart: number | undefined;
|
|
213
267
|
// Local line index up to which the leading run of live blocks is safe to
|
|
214
|
-
// commit. Finalized blocks contribute their full
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
268
|
+
// commit. Finalized blocks contribute their full body; still-live blocks
|
|
269
|
+
// contribute only while their render has been observed growing without
|
|
270
|
+
// visibly rewriting a previously rendered interior row (escape placement
|
|
271
|
+
// and pad drift are ignored). A rewrite suspends the block's contribution
|
|
272
|
+
// until it re-earns append-only via VOLATILE_REARM_FRAMES clean frames;
|
|
273
|
+
// the engine then backfills the stalled gap.
|
|
220
274
|
#nativeScrollbackCommitSafeEnd: number | undefined;
|
|
221
275
|
|
|
222
276
|
override invalidate(): void {
|
|
223
|
-
//
|
|
224
|
-
//
|
|
277
|
+
// Theme/global invalidation: retire every diff snapshot so stale styling
|
|
278
|
+
// is not diffed against the recolored render.
|
|
225
279
|
this.#generation++;
|
|
226
280
|
super.invalidate();
|
|
227
281
|
}
|
|
@@ -239,32 +293,19 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
239
293
|
return this.#nativeScrollbackCommitSafeEnd;
|
|
240
294
|
}
|
|
241
295
|
|
|
242
|
-
/**
|
|
243
|
-
* Retire all frozen snapshots so the next render reflects each block's current
|
|
244
|
-
* state. Call at reconciliation checkpoints (prompt submit) where the whole
|
|
245
|
-
* transcript is replayed into native scrollback and any drift a frozen block
|
|
246
|
-
* accumulated is reconciled.
|
|
247
|
-
*/
|
|
248
|
-
thaw(): void {
|
|
249
|
-
this.#generation++;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
296
|
override render(width: number): string[] {
|
|
253
297
|
width = Math.max(1, width);
|
|
254
298
|
this.#nativeScrollbackLiveRegionStart = undefined;
|
|
255
299
|
this.#nativeScrollbackCommitSafeEnd = undefined;
|
|
256
300
|
|
|
257
|
-
// Freezing/snapshotting only applies on ED3-risk terminals; elsewhere every
|
|
258
|
-
// block renders live. Inter-block spacing applies on BOTH paths so the gap
|
|
259
|
-
// between blocks is identical regardless of terminal.
|
|
260
|
-
const risk = TERMINAL.eagerEraseScrollbackRisk;
|
|
261
301
|
const count = this.children.length;
|
|
262
302
|
|
|
263
303
|
// The live region spans from the earliest still-mutating block through the
|
|
264
|
-
// bottom. A block that has not finalized must stay
|
|
265
|
-
// inserts (TTSR/todo cards) can append a finalized block *below* a
|
|
266
|
-
// is still awaiting its result, and
|
|
267
|
-
//
|
|
304
|
+
// bottom. A block that has not finalized must stay below the seam: out-of-
|
|
305
|
+
// band inserts (TTSR/todo cards) can append a finalized block *below* a
|
|
306
|
+
// tool that is still awaiting its result, and committing the tool there
|
|
307
|
+
// would strand its history rows on the mid-stream preview the late result
|
|
308
|
+
// never reaches.
|
|
268
309
|
let liveStartIndex = count - 1;
|
|
269
310
|
for (let i = 0; i < count; i++) {
|
|
270
311
|
if (!isBlockFinalized(this.children[i]!)) {
|
|
@@ -272,62 +313,48 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
272
313
|
break;
|
|
273
314
|
}
|
|
274
315
|
}
|
|
275
|
-
// Blocks at [prevLiveStart, liveStart) just crossed out of the live region;
|
|
276
|
-
// recompute them so they freeze at their final content. Everything below
|
|
277
|
-
// the lower of the two cutoffs was already frozen last frame and replays.
|
|
278
|
-
const replayCutoff = Math.min(liveStartIndex, this.#prevLiveStartIndex);
|
|
279
|
-
if (risk) this.#prevLiveStartIndex = liveStartIndex;
|
|
280
316
|
|
|
281
317
|
const lines: string[] = [];
|
|
282
318
|
// Tracks whether we are still inside the leading run of commit-safe live
|
|
283
319
|
// blocks. The first still-live volatile block closes it, but rendering
|
|
284
320
|
// continues so lower blocks remain visible.
|
|
285
321
|
let commitSafeOpen = true;
|
|
286
|
-
// The live-region start is recorded at the first visible row at/after
|
|
287
|
-
//
|
|
322
|
+
// The live-region start is recorded at the first visible row at/after
|
|
323
|
+
// liveStartIndex; empty leading blocks (or a separator) must not claim it
|
|
324
|
+
// early.
|
|
288
325
|
let liveRecorded = false;
|
|
289
326
|
for (let i = 0; i < count; i++) {
|
|
290
327
|
const child = this.children[i]! as Component & SnapshotCarrier;
|
|
291
328
|
|
|
292
|
-
//
|
|
293
|
-
// top/bottom edges stripped (the container owns inter-block gaps).
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
if (risk && i < liveStartIndex && i < replayCutoff) {
|
|
300
|
-
if (hasValidSnapshot(previousSnapshot, width, this.#generation)) {
|
|
301
|
-
contribution = previousSnapshot.lines;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
329
|
+
// This child's contribution: its current render with plain-blank
|
|
330
|
+
// top/bottom edges stripped (the container owns inter-block gaps).
|
|
331
|
+
// Always the latest content — committed history keeps whatever bytes
|
|
332
|
+
// it was written with, but the window must reflect the present state
|
|
333
|
+
// (late tool results, post-finalize re-layouts, expand toggles).
|
|
334
|
+
const previousSnapshot = child[kSnapshot];
|
|
335
|
+
const contribution = stripPlainBlankEdges(child.render(width));
|
|
304
336
|
let liveCommitState: LiveCommitState | undefined;
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
contribution = stripPlainBlankEdges(rendered);
|
|
308
|
-
if (risk && i >= liveStartIndex && !isBlockFinalized(child)) {
|
|
309
|
-
liveCommitState = deriveLiveCommitState(previousSnapshot, contribution, width, this.#generation);
|
|
310
|
-
}
|
|
311
|
-
// Cache every block's latest contribution. While a block is in the
|
|
312
|
-
// live region this keeps its snapshot current; on the frame it crosses
|
|
313
|
-
// out, the recompute above refreshes it before it freezes.
|
|
314
|
-
if (risk) {
|
|
315
|
-
child[kSnapshot] = {
|
|
316
|
-
width,
|
|
317
|
-
lines: contribution,
|
|
318
|
-
generation: this.#generation,
|
|
319
|
-
appendOnly: liveCommitState?.appendOnly ?? false,
|
|
320
|
-
volatileCooldown: liveCommitState?.volatileCooldown ?? 0,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
337
|
+
if (i >= liveStartIndex && !isBlockFinalized(child)) {
|
|
338
|
+
liveCommitState = deriveLiveCommitState(previousSnapshot, contribution, width, this.#generation);
|
|
323
339
|
}
|
|
340
|
+
// Cache the latest contribution as the next frame's diff input.
|
|
341
|
+
child[kSnapshot] = {
|
|
342
|
+
width,
|
|
343
|
+
lines: contribution,
|
|
344
|
+
generation: this.#generation,
|
|
345
|
+
appendOnly: liveCommitState?.appendOnly ?? false,
|
|
346
|
+
volatileCooldown: liveCommitState?.volatileCooldown ?? 0,
|
|
347
|
+
stablePrefixLength: liveCommitState?.stablePrefixLength ?? 0,
|
|
348
|
+
candidatePrefixLength: liveCommitState?.candidatePrefixLength ?? 0,
|
|
349
|
+
candidatePrefixAge: liveCommitState?.candidatePrefixAge ?? 0,
|
|
350
|
+
};
|
|
324
351
|
|
|
325
352
|
// Empty (or stripped-to-nothing) children contribute nothing and never
|
|
326
353
|
// affect spacing or the live-region offsets. An empty still-live child
|
|
327
354
|
// still closes the commit-safe run: if it later gains rows, it pushes
|
|
328
355
|
// everything below it.
|
|
329
356
|
if (contribution.length === 0) {
|
|
330
|
-
if (
|
|
357
|
+
if (i >= liveStartIndex && commitSafeOpen && !isBlockFinalized(child)) commitSafeOpen = false;
|
|
331
358
|
continue;
|
|
332
359
|
}
|
|
333
360
|
|
|
@@ -336,10 +363,10 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
336
363
|
// already a plain blank (a fragment's own trailing pad), never doubling.
|
|
337
364
|
const sep = lines.length > 0 && !isPlainBlank(lines[lines.length - 1]!) ? 1 : 0;
|
|
338
365
|
|
|
339
|
-
// The separator before the first live block stays in the committed
|
|
340
|
-
// (it is deterministic
|
|
366
|
+
// The separator before the first live block stays in the committed
|
|
367
|
+
// prefix (it is deterministic once the prior block's body is settled),
|
|
341
368
|
// so the live region begins at the block's first content row.
|
|
342
|
-
if (
|
|
369
|
+
if (!liveRecorded && i >= liveStartIndex) {
|
|
343
370
|
this.#nativeScrollbackLiveRegionStart = lines.length + sep;
|
|
344
371
|
liveRecorded = true;
|
|
345
372
|
}
|
|
@@ -348,7 +375,7 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
348
375
|
const blockStart = lines.length;
|
|
349
376
|
for (let j = 0; j < contribution.length; j++) lines.push(contribution[j]!);
|
|
350
377
|
|
|
351
|
-
if (
|
|
378
|
+
if (i >= liveStartIndex && commitSafeOpen) {
|
|
352
379
|
const finalized = isBlockFinalized(child);
|
|
353
380
|
const safeLength = finalized ? contribution.length : (liveCommitState?.safeLength ?? 0);
|
|
354
381
|
if (safeLength > 0) {
|
|
@@ -36,19 +36,6 @@ const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
|
|
|
36
36
|
*/
|
|
37
37
|
export const INTERRUPTING_WORKING_MESSAGE = "Interrupting…";
|
|
38
38
|
|
|
39
|
-
// Events that change foreground streaming state, or that reset a turn. The TUI
|
|
40
|
-
// eager native-scrollback rebuild mode is recomputed only on these so unrelated
|
|
41
|
-
// IRC/notices/status refreshes do not toggle scrollback replay policy.
|
|
42
|
-
const STREAM_RENDER_MODE_EVENTS: Record<string, true> = {
|
|
43
|
-
agent_start: true,
|
|
44
|
-
agent_end: true,
|
|
45
|
-
message_start: true,
|
|
46
|
-
message_end: true,
|
|
47
|
-
tool_execution_start: true,
|
|
48
|
-
tool_execution_update: true,
|
|
49
|
-
tool_execution_end: true,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
39
|
type AgentSessionEventHandlers = {
|
|
53
40
|
[E in AgentSessionEventKind]: (event: Extract<AgentSessionEvent, { type: E }>) => Promise<void>;
|
|
54
41
|
};
|
|
@@ -65,7 +52,6 @@ export class EventController {
|
|
|
65
52
|
#renderedCustomMessages = new Set<string>();
|
|
66
53
|
#lastIntent: string | undefined = undefined;
|
|
67
54
|
#backgroundToolCallIds = new Set<string>();
|
|
68
|
-
#assistantMessageStreaming = false;
|
|
69
55
|
#agentTurnActive = false;
|
|
70
56
|
#interrupting = false;
|
|
71
57
|
#readToolCallArgs = new Map<string, Record<string, unknown>>();
|
|
@@ -217,30 +203,6 @@ export class EventController {
|
|
|
217
203
|
|
|
218
204
|
const run = this.#handlers[event.type] as (e: AgentSessionEvent) => Promise<void>;
|
|
219
205
|
await run(event);
|
|
220
|
-
// While an assistant turn is active, visible status chrome and foreground
|
|
221
|
-
// transcript blocks can re-render after rows have entered native scrollback
|
|
222
|
-
// (idle Working loader, Markdown fences, wrapping, tool previews). Let the
|
|
223
|
-
// TUI use its foreground live-region path instead of idle deferral, which
|
|
224
|
-
// can otherwise leave the loader/status frame frozen until the next input.
|
|
225
|
-
// Background-running tools after the turn ends are excluded so late async
|
|
226
|
-
// updates keep the no-yank deferral; agent_start/agent_end bracket the
|
|
227
|
-
// foreground turn.
|
|
228
|
-
if (STREAM_RENDER_MODE_EVENTS[event.type]) {
|
|
229
|
-
this.#refreshToolRenderMode();
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
#refreshToolRenderMode(): void {
|
|
234
|
-
let foregroundToolActive = this.#agentTurnActive || this.#assistantMessageStreaming;
|
|
235
|
-
if (!foregroundToolActive) {
|
|
236
|
-
for (const toolCallId of this.ctx.pendingTools.keys()) {
|
|
237
|
-
if (!this.#backgroundToolCallIds.has(toolCallId)) {
|
|
238
|
-
foregroundToolActive = true;
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
this.ctx.ui.setEagerNativeScrollbackRebuild(foregroundToolActive);
|
|
244
206
|
}
|
|
245
207
|
|
|
246
208
|
async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
|
|
@@ -250,7 +212,6 @@ export class EventController {
|
|
|
250
212
|
this.#readToolCallArgs.clear();
|
|
251
213
|
this.#readToolCallAssistantComponents.clear();
|
|
252
214
|
this.#resetReadGroup();
|
|
253
|
-
this.#assistantMessageStreaming = false;
|
|
254
215
|
this.#lastAssistantComponent = undefined;
|
|
255
216
|
// Restore the previous turn's inline error in the transcript before dropping
|
|
256
217
|
// the banner, so the error stays in history once the banner is gone.
|
|
@@ -267,7 +228,6 @@ export class EventController {
|
|
|
267
228
|
this.ctx.statusContainer.clear();
|
|
268
229
|
}
|
|
269
230
|
this.#cancelIdleCompaction();
|
|
270
|
-
this.#refreshToolRenderMode();
|
|
271
231
|
this.ctx.ensureLoadingAnimation();
|
|
272
232
|
this.ctx.ui.requestRender();
|
|
273
233
|
}
|
|
@@ -340,7 +300,6 @@ export class EventController {
|
|
|
340
300
|
this.ctx.addMessageToChat(event.message);
|
|
341
301
|
this.ctx.ui.requestRender();
|
|
342
302
|
} else if (event.message.role === "assistant") {
|
|
343
|
-
this.#assistantMessageStreaming = true;
|
|
344
303
|
this.#lastVisibleBlockCount = 0;
|
|
345
304
|
this.ctx.streamingComponent = new AssistantMessageComponent(
|
|
346
305
|
undefined,
|
|
@@ -491,9 +450,6 @@ export class EventController {
|
|
|
491
450
|
|
|
492
451
|
async #handleMessageEnd(event: Extract<AgentSessionEvent, { type: "message_end" }>): Promise<void> {
|
|
493
452
|
if (event.message.role === "user") return;
|
|
494
|
-
if (event.message.role === "assistant") {
|
|
495
|
-
this.#assistantMessageStreaming = false;
|
|
496
|
-
}
|
|
497
453
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
498
454
|
this.ctx.streamingMessage = event.message;
|
|
499
455
|
this.#streamingReveal.stop();
|
|
@@ -701,7 +657,6 @@ export class EventController {
|
|
|
701
657
|
}
|
|
702
658
|
async #handleAgentEnd(_event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
|
|
703
659
|
this.#agentTurnActive = false;
|
|
704
|
-
this.#assistantMessageStreaming = false;
|
|
705
660
|
this.#streamingReveal.stop();
|
|
706
661
|
if (this.ctx.loadingAnimation) {
|
|
707
662
|
this.ctx.loadingAnimation.stop();
|
|
@@ -267,7 +267,7 @@ export class InputController {
|
|
|
267
267
|
const focused = this.ctx.ui.getFocused();
|
|
268
268
|
const target = focused && focused !== this.ctx.editor && hasPasteText(focused) ? focused : this.ctx.editor;
|
|
269
269
|
target.pasteText(text);
|
|
270
|
-
this.ctx.ui.requestRender(
|
|
270
|
+
this.ctx.ui.requestRender();
|
|
271
271
|
},
|
|
272
272
|
pasteImage: async image => {
|
|
273
273
|
// Images can only land in the main editor — when a modal Input is
|
|
@@ -755,7 +755,7 @@ export class InputController {
|
|
|
755
755
|
const dims = await this.#imageDimensions(imageData);
|
|
756
756
|
const label = dims ? `[Image #${imageNum}, ${dims.width}x${dims.height}]` : `[Image #${imageNum}]`;
|
|
757
757
|
this.ctx.editor.insertText(`${label} `);
|
|
758
|
-
this.ctx.ui.requestRender(
|
|
758
|
+
this.ctx.ui.requestRender();
|
|
759
759
|
}
|
|
760
760
|
|
|
761
761
|
/** Probe pixel dimensions for the marker label (`[Image #N, WxH]`). Returns undefined when the
|
|
@@ -801,7 +801,7 @@ export class InputController {
|
|
|
801
801
|
});
|
|
802
802
|
if (!image) {
|
|
803
803
|
this.ctx.editor.pasteText(path);
|
|
804
|
-
this.ctx.ui.requestRender(
|
|
804
|
+
this.ctx.ui.requestRender();
|
|
805
805
|
this.ctx.showStatus("Pasted path is not a supported image");
|
|
806
806
|
return;
|
|
807
807
|
}
|
|
@@ -811,7 +811,7 @@ export class InputController {
|
|
|
811
811
|
);
|
|
812
812
|
} catch (error) {
|
|
813
813
|
this.ctx.editor.pasteText(path);
|
|
814
|
-
this.ctx.ui.requestRender(
|
|
814
|
+
this.ctx.ui.requestRender();
|
|
815
815
|
this.ctx.showStatus(
|
|
816
816
|
error instanceof ImageInputTooLargeError ? error.message : "Failed to read pasted image path",
|
|
817
817
|
);
|