@oh-my-pi/pi-coding-agent 15.10.8 → 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 +41 -1
- 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/extensibility/custom-tools/loader.d.ts +22 -3
- package/dist/types/extensibility/extensions/index.d.ts +1 -1
- package/dist/types/extensibility/extensions/loader.d.ts +17 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
- package/dist/types/mcp/transports/stdio.d.ts +12 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -2
- package/dist/types/modes/components/transcript-container.d.ts +12 -26
- package/dist/types/sdk.d.ts +42 -2
- package/dist/types/task/discovery.d.ts +1 -2
- package/dist/types/task/executor.d.ts +16 -0
- package/dist/types/tiny/title-client.d.ts +1 -1
- package/dist/types/tools/index.d.ts +17 -0
- package/dist/types/tools/todo.d.ts +2 -0
- package/dist/types/tui/hyperlink.d.ts +8 -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/extensibility/custom-tools/loader.ts +43 -19
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/loader.ts +29 -6
- package/src/extensibility/plugins/legacy-pi-compat.ts +30 -6
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/mcp/transports/stdio.ts +139 -3
- package/src/modes/components/custom-editor.ts +69 -9
- package/src/modes/components/model-selector.ts +62 -52
- package/src/modes/components/transcript-container.ts +204 -125
- package/src/modes/controllers/event-controller.ts +0 -45
- package/src/modes/controllers/input-controller.ts +5 -5
- package/src/modes/controllers/mcp-command-controller.ts +2 -2
- 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/sdk.ts +138 -56
- package/src/ssh/ssh-executor.ts +60 -4
- package/src/task/discovery.ts +17 -24
- package/src/task/executor.ts +19 -0
- package/src/task/index.ts +4 -0
- package/src/tiny/title-client.ts +6 -3
- package/src/tools/index.ts +17 -0
- package/src/tools/todo.ts +16 -7
- package/src/tui/hyperlink.ts +27 -3
- package/src/web/search/providers/anthropic.ts +8 -2
|
@@ -1,17 +1,36 @@
|
|
|
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;
|
|
9
15
|
appendOnly: boolean;
|
|
10
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Frames remaining until a block that rewrote an interior row may re-earn
|
|
18
|
+
* append-only status. `0` means the block is not under rewrite suspicion.
|
|
19
|
+
*/
|
|
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;
|
|
11
30
|
}
|
|
12
31
|
|
|
13
32
|
interface SnapshotCarrier {
|
|
14
|
-
[kSnapshot]?:
|
|
33
|
+
[kSnapshot]?: LiveDiffSnapshot;
|
|
15
34
|
}
|
|
16
35
|
|
|
17
36
|
/**
|
|
@@ -51,125 +70,212 @@ function stripPlainBlankEdges(lines: string[]): string[] {
|
|
|
51
70
|
|
|
52
71
|
interface LiveCommitState {
|
|
53
72
|
appendOnly: boolean;
|
|
54
|
-
|
|
73
|
+
volatileCooldown: number;
|
|
74
|
+
stablePrefixLength: number;
|
|
75
|
+
candidatePrefixLength: number;
|
|
76
|
+
candidatePrefixAge: number;
|
|
55
77
|
safeLength: number;
|
|
56
78
|
}
|
|
57
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Render frames a block must stay clean (static or append-shaped) after an
|
|
82
|
+
* interior rewrite before its rows become committable again. A one-off
|
|
83
|
+
* re-layout (a codespan finalizing across a wrap boundary, a paragraph
|
|
84
|
+
* re-parsed as a heading) only suspends commits briefly — the pinned emitter
|
|
85
|
+
* appends from the stalled high-water mark, so the gap backfills contiguously
|
|
86
|
+
* once the block re-earns append-only. Periodic animations (a spinner rewrites
|
|
87
|
+
* its row every few frames) keep resetting the countdown and never re-earn it,
|
|
88
|
+
* so genuinely volatile blocks stay deferred. Frames arrive at most at the
|
|
89
|
+
* TUI's 30 Hz render cadence, so 30 frames ≈ 1s of clean streaming.
|
|
90
|
+
*/
|
|
91
|
+
const VOLATILE_REARM_FRAMES = 30;
|
|
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
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Visible-content form of a row: SGR/OSC bytes and trailing pad spaces are
|
|
111
|
+
* write framing, not content. A styled line's closing escape moves when the
|
|
112
|
+
* line stops being the last of its span (a wrapped thinking paragraph growing
|
|
113
|
+
* by one row), and width-padded rows shift their trailing spaces as text
|
|
114
|
+
* grows; both leave the on-screen cells identical and must not count as a
|
|
115
|
+
* rewrite of a committed-candidate row. Committed scrollback rows are written
|
|
116
|
+
* with a full SGR/OSC reset terminator, so escape-placement drift between
|
|
117
|
+
* visually identical renders cannot bleed styles across rows.
|
|
118
|
+
*/
|
|
119
|
+
function normalizeRow(line: string): string {
|
|
120
|
+
return Bun.stripANSI(line).trimEnd();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function rowsVisiblyEqual(prev: string, cur: string): boolean {
|
|
124
|
+
return prev === cur || normalizeRow(prev) === normalizeRow(cur);
|
|
125
|
+
}
|
|
126
|
+
|
|
58
127
|
function hasValidSnapshot(
|
|
59
|
-
snapshot:
|
|
128
|
+
snapshot: LiveDiffSnapshot | undefined,
|
|
60
129
|
width: number,
|
|
61
130
|
generation: number,
|
|
62
|
-
): snapshot is
|
|
131
|
+
): snapshot is LiveDiffSnapshot {
|
|
63
132
|
return snapshot !== undefined && snapshot.generation === generation && snapshot.width === width;
|
|
64
133
|
}
|
|
65
134
|
|
|
66
135
|
function commonPrefixLength(prev: string[], cur: string[]): number {
|
|
67
136
|
const limit = Math.min(prev.length, cur.length);
|
|
68
137
|
let i = 0;
|
|
69
|
-
while (i < limit && prev[i]
|
|
138
|
+
while (i < limit && rowsVisiblyEqual(prev[i]!, cur[i]!)) i++;
|
|
70
139
|
return i;
|
|
71
140
|
}
|
|
72
141
|
|
|
73
142
|
function commonSuffixLength(prev: string[], cur: string[], prefixLength: number): number {
|
|
74
143
|
const limit = Math.min(prev.length - prefixLength, cur.length - prefixLength);
|
|
75
144
|
let i = 0;
|
|
76
|
-
while (i < limit && prev[prev.length - 1 - i]
|
|
145
|
+
while (i < limit && rowsVisiblyEqual(prev[prev.length - 1 - i]!, cur[cur.length - 1 - i]!)) i++;
|
|
77
146
|
return i;
|
|
78
147
|
}
|
|
79
148
|
|
|
80
149
|
function deriveLiveCommitState(
|
|
81
|
-
previous:
|
|
150
|
+
previous: LiveDiffSnapshot | undefined,
|
|
82
151
|
current: string[],
|
|
83
152
|
width: number,
|
|
84
153
|
generation: number,
|
|
85
154
|
): LiveCommitState {
|
|
86
155
|
let appendOnly = false;
|
|
87
|
-
let
|
|
156
|
+
let volatileCooldown = 0;
|
|
157
|
+
let stablePrefixLength = 0;
|
|
158
|
+
let candidatePrefixLength = 0;
|
|
159
|
+
let candidatePrefixAge = 0;
|
|
88
160
|
if (hasValidSnapshot(previous, width, generation)) {
|
|
89
161
|
appendOnly = previous.appendOnly;
|
|
90
|
-
|
|
162
|
+
volatileCooldown = previous.volatileCooldown;
|
|
163
|
+
stablePrefixLength = previous.stablePrefixLength;
|
|
164
|
+
candidatePrefixLength = previous.candidatePrefixLength;
|
|
165
|
+
candidatePrefixAge = previous.candidatePrefixAge;
|
|
91
166
|
|
|
92
167
|
const prefixLength = commonPrefixLength(previous.lines, current);
|
|
93
168
|
const staticRender = prefixLength === previous.lines.length && prefixLength === current.length;
|
|
169
|
+
let cleanFrame = true;
|
|
94
170
|
if (!staticRender) {
|
|
95
171
|
const suffixLength = commonSuffixLength(previous.lines, current, prefixLength);
|
|
96
172
|
// Append-only growth never rewrites a row that may already have scrolled
|
|
97
|
-
// into native scrollback; it only grows the block at/near its tail.
|
|
173
|
+
// into native scrollback; it only grows the block at/near its tail. Four
|
|
98
174
|
// shapes qualify: a pure bottom append, an insertion above stable trailing
|
|
99
|
-
// chrome (a streaming tool's footer/border),
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
175
|
+
// chrome (a streaming tool's footer/border), an in-place extension of the
|
|
176
|
+
// current line by one streamed token (line count unchanged), and a
|
|
177
|
+
// wrap-shrink of the current line where its last word grew past the wrap
|
|
178
|
+
// column and moved down onto an appended row. The first two preserve every
|
|
179
|
+
// previous row across a matching prefix + suffix; the last two leave a
|
|
180
|
+
// single divergent previous row — the block's in-flight bottom line, which
|
|
181
|
+
// cannot have been committed (commits stop at the viewport top and the
|
|
182
|
+
// bottom line is by definition on screen). Any other divergent interior
|
|
183
|
+
// row means the block re-laid-out committed-candidate content — a rewrite,
|
|
184
|
+
// which suspends commits until the block re-earns append-only.
|
|
105
185
|
const preservedEveryRow = prefixLength + suffixLength >= previous.lines.length;
|
|
106
|
-
|
|
186
|
+
let tailExtendedInPlace = false;
|
|
187
|
+
if (
|
|
188
|
+
!preservedEveryRow &&
|
|
107
189
|
prefixLength + suffixLength === previous.lines.length - 1 &&
|
|
108
|
-
prefixLength < current.length
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
190
|
+
prefixLength < current.length
|
|
191
|
+
) {
|
|
192
|
+
const prevTail = normalizeRow(previous.lines[prefixLength]!);
|
|
193
|
+
const curTail = normalizeRow(current[prefixLength]!);
|
|
194
|
+
tailExtendedInPlace =
|
|
195
|
+
curTail.startsWith(prevTail) || (current.length > previous.lines.length && prevTail.startsWith(curTail));
|
|
196
|
+
}
|
|
197
|
+
if ((preservedEveryRow || tailExtendedInPlace) && current.length >= previous.lines.length) {
|
|
198
|
+
if (volatileCooldown === 0) appendOnly = true;
|
|
199
|
+
} else {
|
|
200
|
+
cleanFrame = false;
|
|
114
201
|
appendOnly = false;
|
|
202
|
+
volatileCooldown = VOLATILE_REARM_FRAMES;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
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;
|
|
115
228
|
}
|
|
116
229
|
}
|
|
117
230
|
}
|
|
118
231
|
|
|
119
232
|
return {
|
|
120
233
|
appendOnly,
|
|
121
|
-
|
|
122
|
-
|
|
234
|
+
volatileCooldown,
|
|
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,
|
|
123
241
|
};
|
|
124
242
|
}
|
|
125
243
|
|
|
126
244
|
/**
|
|
127
|
-
* Transcript container that
|
|
128
|
-
* the
|
|
129
|
-
*
|
|
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.
|
|
130
248
|
*
|
|
131
|
-
*
|
|
132
|
-
* the
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* This container provides that guarantee: a block's render is snapshotted while
|
|
141
|
-
* it is the live (bottom-most) block, and once a newer block is appended it
|
|
142
|
-
* replays the snapshot instead of recomputing. Mutations after a block leaves
|
|
143
|
-
* live are intentionally deferred until the next checkpoint {@link thaw} (prompt
|
|
144
|
-
* submit → native-scrollback rebuild), where the whole transcript is replayed
|
|
145
|
-
* and any drift reconciles safely. On terminals that can rebuild history this
|
|
146
|
-
* 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.
|
|
147
258
|
*/
|
|
148
259
|
export class TranscriptContainer extends Container implements NativeScrollbackLiveRegion {
|
|
149
|
-
// Bumped to
|
|
150
|
-
// 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.
|
|
151
262
|
#generation = 0;
|
|
152
|
-
// Line index where the live (repaintable) region began on the previous
|
|
153
|
-
// render — the start of the earliest still-mutating block, or the bottom
|
|
154
|
-
// block when everything is finalized. A block leaves the live region only
|
|
155
|
-
// once it has finalized AND a finalized block sits below it; the frame it
|
|
156
|
-
// crosses out is recomputed so it freezes at its true final content, not the
|
|
157
|
-
// mid-stream snapshot it last rendered while live (TUI render coalescing can
|
|
158
|
-
// advance a block's content in the very frame it stops being live).
|
|
159
|
-
#prevLiveStartIndex = 0;
|
|
160
263
|
// Local line index where the current live region begins in the most recent
|
|
161
|
-
// render. TUI
|
|
162
|
-
//
|
|
264
|
+
// render. TUI commits rows to native scrollback only above this seam (or
|
|
265
|
+
// the deeper commit-safe end below).
|
|
163
266
|
#nativeScrollbackLiveRegionStart: number | undefined;
|
|
164
267
|
// Local line index up to which the leading run of live blocks is safe to
|
|
165
|
-
// commit. Finalized blocks contribute their full
|
|
166
|
-
//
|
|
167
|
-
//
|
|
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.
|
|
168
274
|
#nativeScrollbackCommitSafeEnd: number | undefined;
|
|
169
275
|
|
|
170
276
|
override invalidate(): void {
|
|
171
|
-
//
|
|
172
|
-
//
|
|
277
|
+
// Theme/global invalidation: retire every diff snapshot so stale styling
|
|
278
|
+
// is not diffed against the recolored render.
|
|
173
279
|
this.#generation++;
|
|
174
280
|
super.invalidate();
|
|
175
281
|
}
|
|
@@ -187,32 +293,19 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
187
293
|
return this.#nativeScrollbackCommitSafeEnd;
|
|
188
294
|
}
|
|
189
295
|
|
|
190
|
-
/**
|
|
191
|
-
* Retire all frozen snapshots so the next render reflects each block's current
|
|
192
|
-
* state. Call at reconciliation checkpoints (prompt submit) where the whole
|
|
193
|
-
* transcript is replayed into native scrollback and any drift a frozen block
|
|
194
|
-
* accumulated is reconciled.
|
|
195
|
-
*/
|
|
196
|
-
thaw(): void {
|
|
197
|
-
this.#generation++;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
296
|
override render(width: number): string[] {
|
|
201
297
|
width = Math.max(1, width);
|
|
202
298
|
this.#nativeScrollbackLiveRegionStart = undefined;
|
|
203
299
|
this.#nativeScrollbackCommitSafeEnd = undefined;
|
|
204
300
|
|
|
205
|
-
// Freezing/snapshotting only applies on ED3-risk terminals; elsewhere every
|
|
206
|
-
// block renders live. Inter-block spacing applies on BOTH paths so the gap
|
|
207
|
-
// between blocks is identical regardless of terminal.
|
|
208
|
-
const risk = TERMINAL.eagerEraseScrollbackRisk;
|
|
209
301
|
const count = this.children.length;
|
|
210
302
|
|
|
211
303
|
// The live region spans from the earliest still-mutating block through the
|
|
212
|
-
// bottom. A block that has not finalized must stay
|
|
213
|
-
// inserts (TTSR/todo cards) can append a finalized block *below* a
|
|
214
|
-
// is still awaiting its result, and
|
|
215
|
-
//
|
|
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.
|
|
216
309
|
let liveStartIndex = count - 1;
|
|
217
310
|
for (let i = 0; i < count; i++) {
|
|
218
311
|
if (!isBlockFinalized(this.children[i]!)) {
|
|
@@ -220,62 +313,48 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
220
313
|
break;
|
|
221
314
|
}
|
|
222
315
|
}
|
|
223
|
-
// Blocks at [prevLiveStart, liveStart) just crossed out of the live region;
|
|
224
|
-
// recompute them so they freeze at their final content. Everything below
|
|
225
|
-
// the lower of the two cutoffs was already frozen last frame and replays.
|
|
226
|
-
const replayCutoff = Math.min(liveStartIndex, this.#prevLiveStartIndex);
|
|
227
|
-
if (risk) this.#prevLiveStartIndex = liveStartIndex;
|
|
228
316
|
|
|
229
317
|
const lines: string[] = [];
|
|
230
318
|
// Tracks whether we are still inside the leading run of commit-safe live
|
|
231
319
|
// blocks. The first still-live volatile block closes it, but rendering
|
|
232
320
|
// continues so lower blocks remain visible.
|
|
233
321
|
let commitSafeOpen = true;
|
|
234
|
-
// The live-region start is recorded at the first visible row at/after
|
|
235
|
-
//
|
|
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.
|
|
236
325
|
let liveRecorded = false;
|
|
237
326
|
for (let i = 0; i < count; i++) {
|
|
238
327
|
const child = this.children[i]! as Component & SnapshotCarrier;
|
|
239
328
|
|
|
240
|
-
//
|
|
241
|
-
// top/bottom edges stripped (the container owns inter-block gaps).
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
if (risk && i < liveStartIndex && i < replayCutoff) {
|
|
248
|
-
if (hasValidSnapshot(previousSnapshot, width, this.#generation)) {
|
|
249
|
-
contribution = previousSnapshot.lines;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
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));
|
|
252
336
|
let liveCommitState: LiveCommitState | undefined;
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
contribution = stripPlainBlankEdges(rendered);
|
|
256
|
-
if (risk && i >= liveStartIndex && !isBlockFinalized(child)) {
|
|
257
|
-
liveCommitState = deriveLiveCommitState(previousSnapshot, contribution, width, this.#generation);
|
|
258
|
-
}
|
|
259
|
-
// Cache every block's latest contribution. While a block is in the
|
|
260
|
-
// live region this keeps its snapshot current; on the frame it crosses
|
|
261
|
-
// out, the recompute above refreshes it before it freezes.
|
|
262
|
-
if (risk) {
|
|
263
|
-
child[kSnapshot] = {
|
|
264
|
-
width,
|
|
265
|
-
lines: contribution,
|
|
266
|
-
generation: this.#generation,
|
|
267
|
-
appendOnly: liveCommitState?.appendOnly ?? false,
|
|
268
|
-
volatile: liveCommitState?.volatile ?? false,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
337
|
+
if (i >= liveStartIndex && !isBlockFinalized(child)) {
|
|
338
|
+
liveCommitState = deriveLiveCommitState(previousSnapshot, contribution, width, this.#generation);
|
|
271
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
|
+
};
|
|
272
351
|
|
|
273
352
|
// Empty (or stripped-to-nothing) children contribute nothing and never
|
|
274
353
|
// affect spacing or the live-region offsets. An empty still-live child
|
|
275
354
|
// still closes the commit-safe run: if it later gains rows, it pushes
|
|
276
355
|
// everything below it.
|
|
277
356
|
if (contribution.length === 0) {
|
|
278
|
-
if (
|
|
357
|
+
if (i >= liveStartIndex && commitSafeOpen && !isBlockFinalized(child)) commitSafeOpen = false;
|
|
279
358
|
continue;
|
|
280
359
|
}
|
|
281
360
|
|
|
@@ -284,10 +363,10 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
284
363
|
// already a plain blank (a fragment's own trailing pad), never doubling.
|
|
285
364
|
const sep = lines.length > 0 && !isPlainBlank(lines[lines.length - 1]!) ? 1 : 0;
|
|
286
365
|
|
|
287
|
-
// The separator before the first live block stays in the committed
|
|
288
|
-
// (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),
|
|
289
368
|
// so the live region begins at the block's first content row.
|
|
290
|
-
if (
|
|
369
|
+
if (!liveRecorded && i >= liveStartIndex) {
|
|
291
370
|
this.#nativeScrollbackLiveRegionStart = lines.length + sep;
|
|
292
371
|
liveRecorded = true;
|
|
293
372
|
}
|
|
@@ -296,7 +375,7 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
296
375
|
const blockStart = lines.length;
|
|
297
376
|
for (let j = 0; j < contribution.length; j++) lines.push(contribution[j]!);
|
|
298
377
|
|
|
299
|
-
if (
|
|
378
|
+
if (i >= liveStartIndex && commitSafeOpen) {
|
|
300
379
|
const finalized = isBlockFinalized(child);
|
|
301
380
|
const safeLength = finalized ? contribution.length : (liveCommitState?.safeLength ?? 0);
|
|
302
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();
|
|
@@ -191,7 +191,7 @@ export class InputController {
|
|
|
191
191
|
this.ctx.keybindings.getKeys("app.clipboard.pasteImage"),
|
|
192
192
|
);
|
|
193
193
|
this.ctx.editor.onPasteImage = () => this.handleImagePaste();
|
|
194
|
-
this.ctx.editor.onPasteImagePath = path =>
|
|
194
|
+
this.ctx.editor.onPasteImagePath = path => this.handleImagePathPaste(path);
|
|
195
195
|
this.ctx.editor.setActionKeys(
|
|
196
196
|
"app.clipboard.pasteTextRaw",
|
|
197
197
|
this.ctx.keybindings.getKeys("app.clipboard.pasteTextRaw"),
|
|
@@ -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
|
);
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
import type { MCPAuthConfig, MCPServerConfig, MCPServerConnection } from "../../mcp/types";
|
|
37
37
|
import type { OAuthCredential } from "../../session/auth-storage";
|
|
38
38
|
import { shortenPath } from "../../tools/render-utils";
|
|
39
|
-
import {
|
|
39
|
+
import { urlHyperlinkAlways } from "../../tui";
|
|
40
40
|
import { openPath } from "../../utils/open";
|
|
41
41
|
import { ChatBlock } from "../components/chat-block";
|
|
42
42
|
import { MCPAddWizard } from "../components/mcp-add-wizard";
|
|
@@ -63,7 +63,7 @@ export class MCPAuthorizationLinkPrompt implements Component {
|
|
|
63
63
|
invalidate(): void {}
|
|
64
64
|
|
|
65
65
|
render(_width: number): string[] {
|
|
66
|
-
const link =
|
|
66
|
+
const link = urlHyperlinkAlways(this.#url, "Click here to authorize");
|
|
67
67
|
return [
|
|
68
68
|
` ${theme.fg("success", "Open authorization URL:")}`,
|
|
69
69
|
` ${theme.fg("accent", link)}`,
|
|
@@ -266,10 +266,6 @@ export class SelectorController {
|
|
|
266
266
|
this.ctx.updateEditorBorderColor();
|
|
267
267
|
break;
|
|
268
268
|
|
|
269
|
-
case "clearOnShrink":
|
|
270
|
-
this.ctx.ui.setClearOnShrink(value as boolean);
|
|
271
|
-
break;
|
|
272
|
-
|
|
273
269
|
case "autocompleteMaxVisible":
|
|
274
270
|
this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value));
|
|
275
271
|
break;
|