@oh-my-pi/pi-tui 18.9.0 → 18.9.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/CHANGELOG.md +16 -0
- package/dist/types/chrome/transcript-container.d.ts +5 -0
- package/dist/types/components/image.d.ts +2 -44
- package/dist/types/terminal-capabilities.d.ts +3 -3
- package/dist/types/tmux.d.ts +10 -0
- package/package.json +9 -9
- package/src/chrome/transcript-container.ts +22 -2
- package/src/components/image.ts +13 -145
- package/src/render/composite.ts +2 -0
- package/src/terminal-capabilities.ts +10 -8
- package/src/terminal.ts +39 -0
- package/src/tmux.ts +43 -0
- package/src/tui.ts +66 -38
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.9.2] - 2026-10-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added native TSP rendering support for Tern terminal sessions running inside tmux, with automatic session detection and safe title-setting behavior.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Improved responsiveness when inserting assistant messages after tool calls in long transcripts.
|
|
14
|
+
|
|
15
|
+
## [18.9.1] - 2026-10-11
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed inline charts and images leaving overlapping copies over text when moved, resized, replaced, or hidden, while preserving images in scrollback.
|
|
20
|
+
|
|
5
21
|
## [18.9.0] - 2026-10-11
|
|
6
22
|
|
|
7
23
|
### Added
|
|
@@ -63,6 +63,11 @@ export interface TranscriptViewportSpan {
|
|
|
63
63
|
export declare class TranscriptContainer extends Container {
|
|
64
64
|
#private;
|
|
65
65
|
addChild(component: Component): void;
|
|
66
|
+
/**
|
|
67
|
+
* `#entries` mirrors `children`; move both together or the next `#syncEntries`
|
|
68
|
+
* pays a full O(n) rebuild.
|
|
69
|
+
*/
|
|
70
|
+
insertChildAt(component: Component, index: number): void;
|
|
66
71
|
/** Removes `component` if {@link canRemoveBlock} allows it; otherwise does nothing. */
|
|
67
72
|
removeChild(component: Component): void;
|
|
68
73
|
clear(): void;
|
|
@@ -149,50 +149,8 @@ export declare class ImageBudget {
|
|
|
149
149
|
* the image is purged from the terminal store.
|
|
150
150
|
*/
|
|
151
151
|
registerPlacementGeometry(imageId: number, widthPx: number, heightPx: number): void;
|
|
152
|
-
/**
|
|
153
|
-
|
|
154
|
-
* that is committed once the frame's writes land). Called once per rendered
|
|
155
|
-
* frame — including frames that emit no placements — so an epoch whose rows
|
|
156
|
-
* commit while its line is never rewritten is still flagged before the next
|
|
157
|
-
* re-emission.
|
|
158
|
-
*/
|
|
159
|
-
observeCommitWatermark(committedTo: number): void;
|
|
160
|
-
/**
|
|
161
|
-
* End the physical-row coordinate epoch after observing its final commit
|
|
162
|
-
* watermark. Placement ids and latched archive state survive, but attachment
|
|
163
|
-
* rows do not: the next placement emit records them in the new-width frame.
|
|
164
|
-
*/
|
|
165
|
-
beginPlacementCoordinateEpoch(): void;
|
|
166
|
-
/**
|
|
167
|
-
* Resolve the placement id and geometry for a direct-placement emit whose
|
|
168
|
-
* topmost attached cell sits at `attachTopFrameRow` — the first frame row
|
|
169
|
-
* the placement covers, i.e. the block's first *visible* row, not its
|
|
170
|
-
* origin (-1 when the writer has no frame-space position: alt-screen,
|
|
171
|
-
* resize, ConPTY-truncated replays). `committedTo` is this frame's commit
|
|
172
|
-
* target in the same frame-row space (-1 when unknown).
|
|
173
|
-
*
|
|
174
|
-
* Invariant: a placement id may be re-used (Kitty replace strips that id's
|
|
175
|
-
* cells everywhere, scrollback included) only while none of the cells it
|
|
176
|
-
* attached have entered native scrollback. The epoch — the `p=` id —
|
|
177
|
-
* advances exactly when the archived flag says otherwise; rewrites with no
|
|
178
|
-
* commit progression keep replacing the same id in place.
|
|
179
|
-
*/
|
|
180
|
-
resolvePlacementEmit(imageId: number, attachTopFrameRow: number, committedTo: number): {
|
|
181
|
-
placementId: number;
|
|
182
|
-
widthPx: number;
|
|
183
|
-
heightPx: number;
|
|
184
|
-
} | null;
|
|
185
|
-
/**
|
|
186
|
-
* Restart every placement epoch after a destructive history clear (`CSI 3 J`
|
|
187
|
-
* full paint). The clear destroys all placement cells — scrollback rows are
|
|
188
|
-
* gone and the replay rewrites the viewport — so no archive remains to
|
|
189
|
-
* protect. Reverting to epoch 1 lets the replay recreate every visible
|
|
190
|
-
* placement after the terminal-wide cleanup.
|
|
191
|
-
*/
|
|
192
|
-
resetPlacementEpochs(): ReadonlyArray<{
|
|
193
|
-
imageId: number;
|
|
194
|
-
lastEpoch: number;
|
|
195
|
-
}>;
|
|
152
|
+
/** Source dimensions used by the frame writer to clip a direct placement. */
|
|
153
|
+
getPlacementGeometry(imageId: number): ImageDimensions | undefined;
|
|
196
154
|
/**
|
|
197
155
|
* Queue a one-time transmit for `imageId` on the surface the in-flight pass
|
|
198
156
|
* paints. No-op if that surface already holds the data, so a repeated call
|
|
@@ -323,7 +323,7 @@ export interface ParsedKittyPlacementLine {
|
|
|
323
323
|
/**
|
|
324
324
|
* Parse a frame line that consists solely of a Kitty direct placement (the
|
|
325
325
|
* last line of an {@link Image} block). Returns null for anything else —
|
|
326
|
-
* placeholder grids,
|
|
326
|
+
* placeholder grids, sixel/iTerm2 payloads — so
|
|
327
327
|
* callers fall back to writing the line verbatim.
|
|
328
328
|
*/
|
|
329
329
|
export declare function parseKittyDirectPlacementLine(line: string): ParsedKittyPlacementLine | null;
|
|
@@ -363,8 +363,8 @@ export declare function encodeKittyDeleteAllImages(): string;
|
|
|
363
363
|
/**
|
|
364
364
|
* Delete a single placement of an image (`d=i`, lowercase): removes its cells
|
|
365
365
|
* and registry entry but keeps the transmitted data, so a later `a=p` under a
|
|
366
|
-
* fresh placement id needs no retransmit.
|
|
367
|
-
*
|
|
366
|
+
* fresh placement id needs no retransmit. The frame writer uses this before
|
|
367
|
+
* replacing a live image without deleting its archived copies.
|
|
368
368
|
*/
|
|
369
369
|
export declare function encodeKittyDeletePlacement(imageId: number, placementId: number): string;
|
|
370
370
|
export declare function encodeITerm2(base64Data: string, options?: {
|
package/dist/types/tmux.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export { wrapTmuxPassthrough } from "./multiplexers/tmux.js";
|
|
2
2
|
/** Pass a control sequence through tmux, leaving direct-terminal output unchanged. */
|
|
3
3
|
export declare function wrapTmuxPassthroughIfNeeded(payload: string, env?: NodeJS.ProcessEnv): string;
|
|
4
|
+
/**
|
|
5
|
+
* Whether the terminal hosting this tmux session draws TSP programs natively:
|
|
6
|
+
* it sets the session user option `@tern-tsp` while it watches the session.
|
|
7
|
+
*/
|
|
8
|
+
export declare function tmuxSessionSpeaksTsp(env?: NodeJS.ProcessEnv): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Stop (`on` false) or restore (`on` true) tmux turning this pane's APC
|
|
11
|
+
* strings into its title, which would otherwise swallow every TSP message.
|
|
12
|
+
*/
|
|
13
|
+
export declare function setTmuxPaneApcTitles(on: boolean, env?: NodeJS.ProcessEnv): void;
|
|
4
14
|
/**
|
|
5
15
|
* Resolve the terminal emulator name recorded for this tmux client.
|
|
6
16
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.9.
|
|
4
|
+
"version": "18.9.2",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"gen:glyphs": "bun scripts/gen-glyphs.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@oh-my-pi/omptype": "18.9.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.9.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.9.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.9.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.9.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.9.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.9.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.9.
|
|
44
|
+
"@oh-my-pi/omptype": "18.9.2",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.9.2",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.9.2",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.9.2",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.9.2",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.9.2",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.9.2",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.9.2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -227,11 +227,31 @@ export class TranscriptContainer extends Container {
|
|
|
227
227
|
emitted: 0,
|
|
228
228
|
stableFrozen: false,
|
|
229
229
|
});
|
|
230
|
-
//
|
|
231
|
-
//
|
|
230
|
+
// `children` is public and may be edited externally; re-check the whole
|
|
231
|
+
// list once instead of trusting positions. Insert-after-anchor callers
|
|
232
|
+
// use insertChildAt, which keeps both arrays in lockstep.
|
|
232
233
|
this.#entriesUnverified = true;
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
/**
|
|
237
|
+
* `#entries` mirrors `children`; move both together or the next `#syncEntries`
|
|
238
|
+
* pays a full O(n) rebuild.
|
|
239
|
+
*/
|
|
240
|
+
insertChildAt(component: Component, index: number): void {
|
|
241
|
+
this.addChild(component);
|
|
242
|
+
const from = this.children.length - 1;
|
|
243
|
+
if (index >= from) {
|
|
244
|
+
this.#entriesUnverified = false;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const [moved] = this.children.splice(from, 1);
|
|
248
|
+
if (moved) this.children.splice(index, 0, moved);
|
|
249
|
+
const [entry] = this.#entries.splice(this.#entries.length - 1, 1);
|
|
250
|
+
if (entry) this.#entries.splice(index, 0, entry);
|
|
251
|
+
// Both arrays moved in lockstep, so the live-tail check applies again.
|
|
252
|
+
this.#entriesUnverified = false;
|
|
253
|
+
}
|
|
254
|
+
|
|
235
255
|
/** Removes `component` if {@link canRemoveBlock} allows it; otherwise does nothing. */
|
|
236
256
|
override removeChild(component: Component): void {
|
|
237
257
|
const index = this.#removableIndex(component);
|
package/src/components/image.ts
CHANGED
|
@@ -57,7 +57,6 @@ export function renderForScrollback<T>(render: () => T): T {
|
|
|
57
57
|
|
|
58
58
|
const EMPTY_IDS: readonly number[] = [];
|
|
59
59
|
const EMPTY_TRANSMITS: readonly string[] = [];
|
|
60
|
-
const EMPTY_STALE_EPOCHS: ReadonlyArray<{ imageId: number; lastEpoch: number }> = [];
|
|
61
60
|
const SAVE_CURSOR = "\x1b7";
|
|
62
61
|
const RESTORE_CURSOR = "\x1b8";
|
|
63
62
|
// Direct placements reserve height with leading zero-width rows. Keep them
|
|
@@ -72,25 +71,6 @@ export function imageMaxColumns(width: number): number {
|
|
|
72
71
|
/** Default count of inline images kept as live graphics before older ones fall back to text. */
|
|
73
72
|
export const DEFAULT_MAX_INLINE_IMAGES = 8;
|
|
74
73
|
|
|
75
|
-
/** Per-image direct-placement emit state tracked by {@link ImageBudget}. */
|
|
76
|
-
interface PlacementEmitState {
|
|
77
|
-
widthPx: number;
|
|
78
|
-
heightPx: number;
|
|
79
|
-
/** Current placement-id (`p=`) generation. */
|
|
80
|
-
epoch: number;
|
|
81
|
-
/** First frame row the current epoch's last emit attached cells to. */
|
|
82
|
-
lastAttachTopFrameRow: number | undefined;
|
|
83
|
-
/**
|
|
84
|
-
* Whether any cell attached by the current epoch's last emit has entered
|
|
85
|
-
* native scrollback. Set by {@link ImageBudget.observeCommitWatermark}
|
|
86
|
-
* comparing each frame's raw commit target against the attach top —
|
|
87
|
-
* era-local comparisons, so a divergence recommit that rewinds and
|
|
88
|
-
* re-advances the ledger is detected the moment it re-crosses the attach
|
|
89
|
-
* top, and a stale pre-rewind peak can never re-trigger.
|
|
90
|
-
*/
|
|
91
|
-
cellsArchived: boolean;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
74
|
/** A surface the renderer paints frames on. */
|
|
95
75
|
type Surface = "screen" | "alt";
|
|
96
76
|
const SURFACES: readonly Surface[] = ["screen", "alt"];
|
|
@@ -222,22 +202,8 @@ export class ImageBudget {
|
|
|
222
202
|
* fullscreen overlay keeps its placements and is restored from cache on exit.
|
|
223
203
|
*/
|
|
224
204
|
#liveIds: Record<Surface, Set<number>> = { screen: new Set(), alt: new Set() };
|
|
225
|
-
/**
|
|
226
|
-
|
|
227
|
-
* renderer's clipped source rectangle, plus the placement-id epoch (see
|
|
228
|
-
* {@link resolvePlacementEmit}). Entries deliberately live as long as the
|
|
229
|
-
* terminal's own placement registry for the image — they are the ledger the
|
|
230
|
-
* destructive-clear sweep uses to delete every registry entry an image ever
|
|
231
|
-
* placed — and die with it on demotion purge (`d=I`) or full cleanup.
|
|
232
|
-
*/
|
|
233
|
-
#placementState = new Map<number, PlacementEmitState>();
|
|
234
|
-
/**
|
|
235
|
-
* States with an un-archived live attach top — the only ones a frame's
|
|
236
|
-
* commit watermark can affect. {@link observeCommitWatermark} runs every
|
|
237
|
-
* rendered frame, so it scans this set (bounded by concurrently live
|
|
238
|
-
* placements) instead of every image ever registered.
|
|
239
|
-
*/
|
|
240
|
-
#watchedPlacements = new Set<PlacementEmitState>();
|
|
205
|
+
/** Source pixel dimensions for clipping direct placements at the viewport edge. */
|
|
206
|
+
#placementGeometry = new Map<number, ImageDimensions>();
|
|
241
207
|
/** Ids whose owner replaced them for good ({@link release}); retired once no frame shows them. */
|
|
242
208
|
#released = new Set<number>();
|
|
243
209
|
|
|
@@ -480,7 +446,7 @@ export class ImageBudget {
|
|
|
480
446
|
// terminal, so cancel it instead of transmitting then purging.
|
|
481
447
|
if (!this.#pendingTransmits[this.#surface].delete(imageId)) this.#purgeIds.push(imageId);
|
|
482
448
|
this.#transmitted[this.#surface].delete(imageId);
|
|
483
|
-
if (!this.#isTransmitted(imageId)) this.#
|
|
449
|
+
if (!this.#isTransmitted(imageId)) this.#placementGeometry.delete(imageId);
|
|
484
450
|
this.#forgetKeyForId(imageId);
|
|
485
451
|
return true;
|
|
486
452
|
}
|
|
@@ -523,8 +489,7 @@ export class ImageBudget {
|
|
|
523
489
|
this.#resetPurgeIds = [];
|
|
524
490
|
this.#keyToId.clear();
|
|
525
491
|
this.#idToKey.clear();
|
|
526
|
-
this.#
|
|
527
|
-
this.#watchedPlacements.clear();
|
|
492
|
+
this.#placementGeometry.clear();
|
|
528
493
|
this.#released.clear();
|
|
529
494
|
for (const surface of SURFACES) this.#liveIds[surface].clear();
|
|
530
495
|
return [...ids];
|
|
@@ -541,115 +506,18 @@ export class ImageBudget {
|
|
|
541
506
|
* the image is purged from the terminal store.
|
|
542
507
|
*/
|
|
543
508
|
registerPlacementGeometry(imageId: number, widthPx: number, heightPx: number): void {
|
|
544
|
-
const
|
|
545
|
-
if (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
this.#placementState.set(imageId, {
|
|
551
|
-
widthPx,
|
|
552
|
-
heightPx,
|
|
553
|
-
epoch: 1,
|
|
554
|
-
lastAttachTopFrameRow: undefined,
|
|
555
|
-
cellsArchived: false,
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Record this frame's native-scrollback commit target (the frame-row count
|
|
561
|
-
* that is committed once the frame's writes land). Called once per rendered
|
|
562
|
-
* frame — including frames that emit no placements — so an epoch whose rows
|
|
563
|
-
* commit while its line is never rewritten is still flagged before the next
|
|
564
|
-
* re-emission.
|
|
565
|
-
*/
|
|
566
|
-
observeCommitWatermark(committedTo: number): void {
|
|
567
|
-
if (committedTo < 0 || this.#watchedPlacements.size === 0) return;
|
|
568
|
-
for (const state of this.#watchedPlacements) {
|
|
569
|
-
if (state.lastAttachTopFrameRow !== undefined && committedTo > state.lastAttachTopFrameRow) {
|
|
570
|
-
// Latched: the flag only clears when the next emit consumes it,
|
|
571
|
-
// so the state needs no further per-frame scans until then.
|
|
572
|
-
state.cellsArchived = true;
|
|
573
|
-
this.#watchedPlacements.delete(state);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* End the physical-row coordinate epoch after observing its final commit
|
|
580
|
-
* watermark. Placement ids and latched archive state survive, but attachment
|
|
581
|
-
* rows do not: the next placement emit records them in the new-width frame.
|
|
582
|
-
*/
|
|
583
|
-
beginPlacementCoordinateEpoch(): void {
|
|
584
|
-
for (const state of this.#placementState.values()) state.lastAttachTopFrameRow = undefined;
|
|
585
|
-
this.#watchedPlacements.clear();
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Resolve the placement id and geometry for a direct-placement emit whose
|
|
590
|
-
* topmost attached cell sits at `attachTopFrameRow` — the first frame row
|
|
591
|
-
* the placement covers, i.e. the block's first *visible* row, not its
|
|
592
|
-
* origin (-1 when the writer has no frame-space position: alt-screen,
|
|
593
|
-
* resize, ConPTY-truncated replays). `committedTo` is this frame's commit
|
|
594
|
-
* target in the same frame-row space (-1 when unknown).
|
|
595
|
-
*
|
|
596
|
-
* Invariant: a placement id may be re-used (Kitty replace strips that id's
|
|
597
|
-
* cells everywhere, scrollback included) only while none of the cells it
|
|
598
|
-
* attached have entered native scrollback. The epoch — the `p=` id —
|
|
599
|
-
* advances exactly when the archived flag says otherwise; rewrites with no
|
|
600
|
-
* commit progression keep replacing the same id in place.
|
|
601
|
-
*/
|
|
602
|
-
resolvePlacementEmit(
|
|
603
|
-
imageId: number,
|
|
604
|
-
attachTopFrameRow: number,
|
|
605
|
-
committedTo: number,
|
|
606
|
-
): { placementId: number; widthPx: number; heightPx: number } | null {
|
|
607
|
-
const state = this.#placementState.get(imageId);
|
|
608
|
-
if (!state) return null;
|
|
609
|
-
// Frames that commit as they write (seam/full-paint chunk passes) pass
|
|
610
|
-
// their own commit target; fold it in before deciding, so a commit that
|
|
611
|
-
// lands in the same frame as the re-emission still advances the epoch.
|
|
612
|
-
if (committedTo >= 0 && state.lastAttachTopFrameRow !== undefined && committedTo > state.lastAttachTopFrameRow) {
|
|
613
|
-
state.cellsArchived = true;
|
|
614
|
-
this.#watchedPlacements.delete(state);
|
|
615
|
-
}
|
|
616
|
-
if (state.cellsArchived) {
|
|
617
|
-
state.epoch += 1;
|
|
618
|
-
state.cellsArchived = false;
|
|
619
|
-
state.lastAttachTopFrameRow = undefined;
|
|
620
|
-
}
|
|
621
|
-
if (attachTopFrameRow >= 0) {
|
|
622
|
-
state.lastAttachTopFrameRow = attachTopFrameRow;
|
|
623
|
-
this.#watchedPlacements.add(state);
|
|
624
|
-
}
|
|
625
|
-
return { placementId: state.epoch, widthPx: state.widthPx, heightPx: state.heightPx };
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* Restart every placement epoch after a destructive history clear (`CSI 3 J`
|
|
630
|
-
* full paint). The clear destroys all placement cells — scrollback rows are
|
|
631
|
-
* gone and the replay rewrites the viewport — so no archive remains to
|
|
632
|
-
* protect. Reverting to epoch 1 lets the replay recreate every visible
|
|
633
|
-
* placement after the terminal-wide cleanup.
|
|
634
|
-
*/
|
|
635
|
-
resetPlacementEpochs(): ReadonlyArray<{ imageId: number; lastEpoch: number }> {
|
|
636
|
-
let stale: Array<{ imageId: number; lastEpoch: number }> | undefined;
|
|
637
|
-
for (const [imageId, state] of this.#placementState) {
|
|
638
|
-
stale ??= [];
|
|
639
|
-
stale.push({ imageId, lastEpoch: state.epoch });
|
|
640
|
-
state.epoch = 1;
|
|
641
|
-
state.lastAttachTopFrameRow = undefined;
|
|
642
|
-
state.cellsArchived = false;
|
|
509
|
+
const geometry = this.#placementGeometry.get(imageId);
|
|
510
|
+
if (geometry) {
|
|
511
|
+
geometry.widthPx = widthPx;
|
|
512
|
+
geometry.heightPx = heightPx;
|
|
513
|
+
} else {
|
|
514
|
+
this.#placementGeometry.set(imageId, { widthPx, heightPx });
|
|
643
515
|
}
|
|
644
|
-
this.#watchedPlacements.clear();
|
|
645
|
-
return stale ?? EMPTY_STALE_EPOCHS;
|
|
646
516
|
}
|
|
647
517
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
this.#watchedPlacements.delete(state);
|
|
652
|
-
this.#placementState.delete(imageId);
|
|
518
|
+
/** Source dimensions used by the frame writer to clip a direct placement. */
|
|
519
|
+
getPlacementGeometry(imageId: number): ImageDimensions | undefined {
|
|
520
|
+
return this.#placementGeometry.get(imageId);
|
|
653
521
|
}
|
|
654
522
|
|
|
655
523
|
/**
|
package/src/render/composite.ts
CHANGED
|
@@ -11,6 +11,8 @@ export function compositeLineAt(
|
|
|
11
11
|
overlayWidth: number,
|
|
12
12
|
totalWidth: number,
|
|
13
13
|
): string {
|
|
14
|
+
// A full-width image row owns its terminal row; the writer must see its exact placement shape.
|
|
15
|
+
if (startCol === 0 && overlayWidth >= totalWidth && TERMINAL.isImageLine(overlayLine)) return overlayLine;
|
|
14
16
|
if (TERMINAL.isImageLine(baseLine)) {
|
|
15
17
|
// Full-width overlays such as /switch are opaque: replace the
|
|
16
18
|
// Unicode placeholder cells so the image cannot cover the modal.
|
|
@@ -917,8 +917,7 @@ export function encodeKittyPlacement(options: {
|
|
|
917
917
|
/**
|
|
918
918
|
* Exact shape of the direct-placement line {@link Image} emits as its block's
|
|
919
919
|
* last row: optional `ESC 7` + `CUU(rows-1)` prefix, the {@link encodeKittyPlacement}
|
|
920
|
-
* APC, optional `ESC 8` suffix. tmux
|
|
921
|
-
* not match (passthrough placements stay untouched).
|
|
920
|
+
* APC, optional `ESC 8` suffix. The parser unwraps tmux passthrough first.
|
|
922
921
|
*/
|
|
923
922
|
const KITTY_DIRECT_PLACEMENT_LINE =
|
|
924
923
|
/^(?:\x1b7(?:\x1b\[(\d+)A)?)?\x1b_Ga=p,q=2,C=1,i=(\d+)(?:,p=(\d+))?(?:,c=(\d+))?(?:,r=(\d+))?\x1b\\(?:\x1b8)?$/;
|
|
@@ -933,10 +932,15 @@ export interface ParsedKittyPlacementLine {
|
|
|
933
932
|
/**
|
|
934
933
|
* Parse a frame line that consists solely of a Kitty direct placement (the
|
|
935
934
|
* last line of an {@link Image} block). Returns null for anything else —
|
|
936
|
-
* placeholder grids,
|
|
935
|
+
* placeholder grids, sixel/iTerm2 payloads — so
|
|
937
936
|
* callers fall back to writing the line verbatim.
|
|
938
937
|
*/
|
|
939
938
|
export function parseKittyDirectPlacementLine(line: string): ParsedKittyPlacementLine | null {
|
|
939
|
+
if (line.includes("\x1bPtmux;")) {
|
|
940
|
+
line = line.replace(/\x1bPtmux;((?:[^\x1b]|\x1b\x1b)*)\x1b\\/g, (_match, payload: string) =>
|
|
941
|
+
payload.replaceAll("\x1b\x1b", "\x1b"),
|
|
942
|
+
);
|
|
943
|
+
}
|
|
940
944
|
const m = KITTY_DIRECT_PLACEMENT_LINE.exec(line);
|
|
941
945
|
if (!m) return null;
|
|
942
946
|
const columns = m[4] !== undefined ? Number(m[4]) : 0;
|
|
@@ -981,9 +985,7 @@ export function encodeKittyPlacementLine(options: {
|
|
|
981
985
|
const srcY = Math.floor((options.imageHeightPx * hiddenRows) / options.rows);
|
|
982
986
|
params.push(`y=${srcY}`, `h=${Math.max(1, options.imageHeightPx - srcY)}`);
|
|
983
987
|
}
|
|
984
|
-
|
|
985
|
-
// wrapped, never parses, and never reaches this rewrite.
|
|
986
|
-
const apc = `\x1b_G${params.join(",")}\x1b\\`;
|
|
988
|
+
const apc = wrapTmuxPassthroughIfNeeded(`\x1b_G${params.join(",")}\x1b\\`);
|
|
987
989
|
const cuu = visibleRows - 1;
|
|
988
990
|
return cuu > 0 ? `\x1b7\x1b[${cuu}A${apc}\x1b8` : apc;
|
|
989
991
|
}
|
|
@@ -1010,8 +1012,8 @@ export function encodeKittyDeleteAllImages(): string {
|
|
|
1010
1012
|
/**
|
|
1011
1013
|
* Delete a single placement of an image (`d=i`, lowercase): removes its cells
|
|
1012
1014
|
* and registry entry but keeps the transmitted data, so a later `a=p` under a
|
|
1013
|
-
* fresh placement id needs no retransmit.
|
|
1014
|
-
*
|
|
1015
|
+
* fresh placement id needs no retransmit. The frame writer uses this before
|
|
1016
|
+
* replacing a live image without deleting its archived copies.
|
|
1015
1017
|
*/
|
|
1016
1018
|
export function encodeKittyDeletePlacement(imageId: number, placementId: number): string {
|
|
1017
1019
|
return wrapTmuxPassthroughIfNeeded(`\x1b_Ga=d,d=i,i=${imageId},p=${placementId},q=2\x1b\\`);
|
package/src/terminal.ts
CHANGED
|
@@ -28,8 +28,10 @@ import {
|
|
|
28
28
|
setTerminalGlyphProtocol,
|
|
29
29
|
TERMINAL,
|
|
30
30
|
} from "./terminal-capabilities";
|
|
31
|
+
import { tmuxMultiplexer } from "./multiplexers/tmux";
|
|
31
32
|
import type { TerminalMultiplexerBackgroundColorCache } from "./multiplexers/types";
|
|
32
33
|
import { terminalMultiplexerSessions } from "./terminal-multiplexer";
|
|
34
|
+
import { setTmuxPaneApcTitles, tmuxSessionSpeaksTsp } from "./tmux";
|
|
33
35
|
import { setHangulCompatibilityJamoWidth } from "./utils";
|
|
34
36
|
import { translateWindowsAltGrSequence } from "./windows-altgr";
|
|
35
37
|
import { Win32InputModeDecoder, Win32PasteMarkerNormalizer } from "./windows-input-mode";
|
|
@@ -50,6 +52,9 @@ const WIN32_RELAYED_ESCAPE_TIMEOUT_MS = 75;
|
|
|
50
52
|
*/
|
|
51
53
|
const TSP_OSC_PREFIX = `\x1b]877;${TSP_PREFIX.slice(2)}`;
|
|
52
54
|
|
|
55
|
+
/** How long a TSP hello waits for its reply inside tmux, which answers DA1 itself. */
|
|
56
|
+
const TSP_TMUX_HELLO_TIMEOUT_MS = 1000;
|
|
57
|
+
|
|
53
58
|
/**
|
|
54
59
|
* The TSP message a reassembled input string holds, in APC form, or
|
|
55
60
|
* `undefined` while its terminator has not arrived. An OSC 877 message may end
|
|
@@ -874,6 +879,10 @@ export class ProcessTerminal implements Terminal {
|
|
|
874
879
|
#tspPending = false;
|
|
875
880
|
#tspResult: TspHello | null | undefined;
|
|
876
881
|
#tspCallbacks: TspHelloHandler[] = [];
|
|
882
|
+
/** Hello deadline inside tmux, where no DA1 sentinel can end the probe. */
|
|
883
|
+
#tspTimeout: NodeJS.Timeout | undefined;
|
|
884
|
+
/** This tmux pane's APC titles are off for TSP and must be restored. */
|
|
885
|
+
#tspTmuxPaneGated = false;
|
|
877
886
|
#tspReplyBuffer = "";
|
|
878
887
|
#privateCsiResponseBuffer = "";
|
|
879
888
|
#da1SentinelOwners: Da1SentinelOwner[] = [];
|
|
@@ -1831,6 +1840,9 @@ export class ProcessTerminal implements Terminal {
|
|
|
1831
1840
|
#shouldQueryTspSupport(): boolean {
|
|
1832
1841
|
const override = $env.PI_TUI_NATIVE;
|
|
1833
1842
|
if (override === "0") return false;
|
|
1843
|
+
// tmux makes APC bodies the pane title unless the hosting terminal asked
|
|
1844
|
+
// for TSP through `@tern-tsp`: no override may force the probe there.
|
|
1845
|
+
if (tmuxMultiplexer.isInside($env)) return tmuxSessionSpeaksTsp($env);
|
|
1834
1846
|
if (override === "1") return true;
|
|
1835
1847
|
// Multiplexers swallow APC, so the reply could never arrive.
|
|
1836
1848
|
if (isInsideTerminalMultiplexer($env)) return false;
|
|
@@ -1848,10 +1860,30 @@ export class ProcessTerminal implements Terminal {
|
|
|
1848
1860
|
return;
|
|
1849
1861
|
}
|
|
1850
1862
|
this.#tspPending = true;
|
|
1863
|
+
if (tmuxMultiplexer.isInside($env)) {
|
|
1864
|
+
// tmux answers DA1 itself, ahead of any reply: wait for the reply instead.
|
|
1865
|
+
this.#tspTmuxPaneGated = true;
|
|
1866
|
+
setTmuxPaneApcTitles(false, $env);
|
|
1867
|
+
this.#tspTimeout = setTimeout(() => {
|
|
1868
|
+
this.#tspTimeout = undefined;
|
|
1869
|
+
this.#resolveTspSupport(null);
|
|
1870
|
+
}, TSP_TMUX_HELLO_TIMEOUT_MS);
|
|
1871
|
+
this.#safeWrite(encodeTspHelloQuery());
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1851
1874
|
this.#da1SentinelOwners.push({ kind: "tsp" });
|
|
1852
1875
|
this.#safeWrite(`${encodeTspHelloQuery()}\x1b[c`);
|
|
1853
1876
|
}
|
|
1854
1877
|
|
|
1878
|
+
/** Hand APC titles back to tmux once this pane stops speaking TSP. */
|
|
1879
|
+
#endTspTmuxPane(): void {
|
|
1880
|
+
clearTimeout(this.#tspTimeout);
|
|
1881
|
+
this.#tspTimeout = undefined;
|
|
1882
|
+
if (!this.#tspTmuxPaneGated) return;
|
|
1883
|
+
this.#tspTmuxPaneGated = false;
|
|
1884
|
+
setTmuxPaneApcTitles(true, $env);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1855
1887
|
#handleTspMessage(sequence: string): void {
|
|
1856
1888
|
const message = parseTspMessage(sequence);
|
|
1857
1889
|
if (message?.verb === "r" && message.reply.r === "hello") {
|
|
@@ -1867,6 +1899,12 @@ export class ProcessTerminal implements Terminal {
|
|
|
1867
1899
|
this.#tspPending = false;
|
|
1868
1900
|
const result = hello !== null && hello.v === TSP_VERSION ? hello : null;
|
|
1869
1901
|
if (hello !== null && result === null) logger.warn("TSP: unsupported protocol version", { v: hello.v });
|
|
1902
|
+
if (result === null) {
|
|
1903
|
+
this.#endTspTmuxPane();
|
|
1904
|
+
} else {
|
|
1905
|
+
clearTimeout(this.#tspTimeout);
|
|
1906
|
+
this.#tspTimeout = undefined;
|
|
1907
|
+
}
|
|
1870
1908
|
this.#tspResult = result;
|
|
1871
1909
|
for (const cb of this.#tspCallbacks) {
|
|
1872
1910
|
try {
|
|
@@ -2255,6 +2293,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
2255
2293
|
this.#sixelSupportCallbacks = [];
|
|
2256
2294
|
setTerminalGlyphProtocol(false);
|
|
2257
2295
|
this.#tspPending = false;
|
|
2296
|
+
this.#endTspTmuxPane();
|
|
2258
2297
|
this.#tspResult = undefined;
|
|
2259
2298
|
this.#tspReplyBuffer = "";
|
|
2260
2299
|
this.#tspCallbacks = [];
|
package/src/tmux.ts
CHANGED
|
@@ -31,6 +31,49 @@ function queryTmuxClientTerminalName(env: NodeJS.ProcessEnv): string | null {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
const TMUX_COMMAND_TIMEOUT_MS = 500;
|
|
35
|
+
|
|
36
|
+
/** Run a tmux command against the current server; its stdout, or null on failure. */
|
|
37
|
+
function runTmux(args: string[], env: NodeJS.ProcessEnv): string | null {
|
|
38
|
+
const tmux = $which("tmux", { PATH: env.PATH });
|
|
39
|
+
if (!tmux) return null;
|
|
40
|
+
try {
|
|
41
|
+
const result = Bun.spawnSync([tmux, ...args], {
|
|
42
|
+
env,
|
|
43
|
+
stdout: "pipe",
|
|
44
|
+
stderr: "ignore",
|
|
45
|
+
timeout: TMUX_COMMAND_TIMEOUT_MS,
|
|
46
|
+
killSignal: "SIGKILL",
|
|
47
|
+
});
|
|
48
|
+
return result.exitCode === 0 ? result.stdout.toString() : null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether the terminal hosting this tmux session draws TSP programs natively:
|
|
56
|
+
* it sets the session user option `@tern-tsp` while it watches the session.
|
|
57
|
+
*/
|
|
58
|
+
export function tmuxSessionSpeaksTsp(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
59
|
+
return (runTmux(["show-options", "-qv", "@tern-tsp"], env)?.trim() ?? "") !== "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Stop (`on` false) or restore (`on` true) tmux turning this pane's APC
|
|
64
|
+
* strings into its title, which would otherwise swallow every TSP message.
|
|
65
|
+
*/
|
|
66
|
+
export function setTmuxPaneApcTitles(on: boolean, env: NodeJS.ProcessEnv = Bun.env): void {
|
|
67
|
+
const pane = env.TMUX_PANE;
|
|
68
|
+
if (!pane) return;
|
|
69
|
+
runTmux(
|
|
70
|
+
on
|
|
71
|
+
? ["set-option", "-pu", "-t", pane, "allow-set-title"]
|
|
72
|
+
: ["set-option", "-p", "-t", pane, "allow-set-title", "off"],
|
|
73
|
+
env,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
34
77
|
/**
|
|
35
78
|
* Resolve the terminal emulator name recorded for this tmux client.
|
|
36
79
|
*
|
package/src/tui.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { classifyTerminalMultiplexerModule, terminalMultiplexerSessions } from "
|
|
|
33
33
|
import {
|
|
34
34
|
encodeKittyDeleteAllImages,
|
|
35
35
|
encodeKittyDeleteImage,
|
|
36
|
+
encodeKittyDeletePlacement,
|
|
36
37
|
encodeKittyPlacementLine,
|
|
37
38
|
ImageProtocol,
|
|
38
39
|
isImageProtocolForced,
|
|
@@ -642,6 +643,13 @@ interface HardwareCursorState {
|
|
|
642
643
|
visible: boolean;
|
|
643
644
|
}
|
|
644
645
|
|
|
646
|
+
/** A cursor-positioned image owned by a mutable viewport, never by history. */
|
|
647
|
+
interface LiveImagePlacement {
|
|
648
|
+
imageId: number;
|
|
649
|
+
placementId: number;
|
|
650
|
+
line: string;
|
|
651
|
+
}
|
|
652
|
+
|
|
645
653
|
interface PreparedLine {
|
|
646
654
|
raw: string;
|
|
647
655
|
width: number;
|
|
@@ -981,6 +989,9 @@ export class TUI extends Container {
|
|
|
981
989
|
// Caps how many inline images render as live graphics; older ones fall back
|
|
982
990
|
// to text via a purge + full redraw. Cap is configured by the host app.
|
|
983
991
|
#imageBudget = new ImageBudget(DEFAULT_MAX_INLINE_IMAGES, () => this.requestRender());
|
|
992
|
+
#screenImagePlacements = new Map<number, LiveImagePlacement>();
|
|
993
|
+
#altImagePlacements = new Map<number, LiveImagePlacement>();
|
|
994
|
+
#nextImagePlacementId = 1;
|
|
984
995
|
#ghosttyInitialImageDelayDone = false;
|
|
985
996
|
#ghosttyInitialImageDelayTimer: RenderTimer | undefined;
|
|
986
997
|
#ghosttyImageReadyAtMs = 0;
|
|
@@ -1162,6 +1173,8 @@ export class TUI extends Container {
|
|
|
1162
1173
|
|
|
1163
1174
|
#purgeInlineImages(): void {
|
|
1164
1175
|
const transmittedIds = this.#imageBudget.takeAllTransmittedIds();
|
|
1176
|
+
this.#screenImagePlacements.clear();
|
|
1177
|
+
this.#altImagePlacements.clear();
|
|
1165
1178
|
if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
|
|
1166
1179
|
for (const id of transmittedIds) {
|
|
1167
1180
|
this.terminal.write(encodeKittyDeleteImage(id));
|
|
@@ -3148,34 +3161,49 @@ export class TUI extends Container {
|
|
|
3148
3161
|
}
|
|
3149
3162
|
|
|
3150
3163
|
/**
|
|
3151
|
-
*
|
|
3152
|
-
*
|
|
3153
|
-
* under the placement id resolved by the budget's epoch tracking (see
|
|
3154
|
-
* {@link ImageBudget.resolvePlacementEmit}). `screenRow` -1 (write position
|
|
3155
|
-
* unknown) and non-placement image lines (placeholder grids, sixel, iTerm2,
|
|
3156
|
-
* tmux-wrapped) pass through verbatim.
|
|
3164
|
+
* Remove placements before their viewport cells are replaced. History uses
|
|
3165
|
+
* separate placement ids, so deleting a live copy cannot erase scrollback.
|
|
3157
3166
|
*/
|
|
3158
|
-
#
|
|
3167
|
+
#retireImagePlacements(
|
|
3168
|
+
placements: Map<number, LiveImagePlacement>,
|
|
3169
|
+
lines: readonly string[],
|
|
3170
|
+
top: number,
|
|
3171
|
+
repaint: boolean,
|
|
3172
|
+
): string {
|
|
3173
|
+
let output = "";
|
|
3174
|
+
for (const [row, placement] of placements) {
|
|
3175
|
+
if (!repaint && lines[row - top] === placement.line) continue;
|
|
3176
|
+
output += encodeKittyDeletePlacement(placement.imageId, placement.placementId);
|
|
3177
|
+
placements.delete(row);
|
|
3178
|
+
}
|
|
3179
|
+
return output;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
/** Clip a direct placement and record its live owner; history placements have no mutable owner. */
|
|
3183
|
+
#imageLineSequence(
|
|
3184
|
+
line: string,
|
|
3185
|
+
screenRow: number,
|
|
3186
|
+
placements: Map<number, LiveImagePlacement> | undefined,
|
|
3187
|
+
placementRow: number,
|
|
3188
|
+
): string {
|
|
3159
3189
|
if (screenRow < 0) return line;
|
|
3160
3190
|
const parsed = parseKittyDirectPlacementLine(line);
|
|
3161
3191
|
if (!parsed) return line;
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3192
|
+
const previous = placements?.get(placementRow);
|
|
3193
|
+
const placementId = previous?.placementId ?? this.#nextImagePlacementId++;
|
|
3194
|
+
const erase = previous ? encodeKittyDeletePlacement(previous.imageId, previous.placementId) : "";
|
|
3195
|
+
placements?.set(placementRow, { imageId: parsed.imageId, placementId, line });
|
|
3196
|
+
return (
|
|
3197
|
+
erase +
|
|
3198
|
+
encodeKittyPlacementLine({
|
|
3199
|
+
imageId: parsed.imageId,
|
|
3200
|
+
placementId,
|
|
3201
|
+
columns: parsed.columns,
|
|
3202
|
+
rows: parsed.rows,
|
|
3203
|
+
screenRow,
|
|
3204
|
+
imageHeightPx: this.#imageBudget.getPlacementGeometry(parsed.imageId)?.heightPx ?? 0,
|
|
3205
|
+
})
|
|
3169
3206
|
);
|
|
3170
|
-
if (!placement) return line;
|
|
3171
|
-
return encodeKittyPlacementLine({
|
|
3172
|
-
imageId: parsed.imageId,
|
|
3173
|
-
placementId: placement.placementId,
|
|
3174
|
-
columns: parsed.columns,
|
|
3175
|
-
rows: parsed.rows,
|
|
3176
|
-
screenRow,
|
|
3177
|
-
imageHeightPx: placement.heightPx,
|
|
3178
|
-
});
|
|
3179
3207
|
}
|
|
3180
3208
|
|
|
3181
3209
|
#terminalLine(line: PreparedLine): string {
|
|
@@ -3449,7 +3477,7 @@ export class TUI extends Container {
|
|
|
3449
3477
|
// reset forgot are named explicitly here — their tracking is gone, so
|
|
3450
3478
|
// nothing downstream could find them again.
|
|
3451
3479
|
for (const id of this.#imageBudget.takeResetPurgeIds()) buffer += encodeKittyDeleteImage(id);
|
|
3452
|
-
this.#
|
|
3480
|
+
this.#screenImagePlacements.clear();
|
|
3453
3481
|
}
|
|
3454
3482
|
if (TERMINAL.imageProtocol === ImageProtocol.Kitty) {
|
|
3455
3483
|
for (const id of this.#imageBudget.takePurgeIds()) buffer += encodeKittyDeleteImage(id);
|
|
@@ -3472,7 +3500,6 @@ export class TUI extends Container {
|
|
|
3472
3500
|
// image the terminal no longer had and every inline image vanished
|
|
3473
3501
|
// after a settled width resize.
|
|
3474
3502
|
if (destructiveReset) buffer += `${LINE_TERMINATOR}\x1b[H\x1b[2J\x1b[3J`;
|
|
3475
|
-
for (const sequence of this.#imageBudget.takeTransmits()) append(sequence);
|
|
3476
3503
|
const diffable =
|
|
3477
3504
|
geometryStable &&
|
|
3478
3505
|
historyRows.length === 0 &&
|
|
@@ -3480,6 +3507,8 @@ export class TUI extends Container {
|
|
|
3480
3507
|
!this.#forceViewportRepaintOnNextRender &&
|
|
3481
3508
|
!destructiveReset &&
|
|
3482
3509
|
this.#providerWindow.length > 0;
|
|
3510
|
+
buffer += this.#retireImagePlacements(this.#screenImagePlacements, prepared.lines, newTop, !diffable);
|
|
3511
|
+
for (const sequence of this.#imageBudget.takeTransmits()) append(sequence);
|
|
3483
3512
|
if (diffable) {
|
|
3484
3513
|
for (let index = 0; index < rows; index++) {
|
|
3485
3514
|
if (
|
|
@@ -3498,9 +3527,8 @@ export class TUI extends Container {
|
|
|
3498
3527
|
current,
|
|
3499
3528
|
width,
|
|
3500
3529
|
newTop + index,
|
|
3501
|
-
-1,
|
|
3502
|
-
-1,
|
|
3503
3530
|
this.#osc66SpacerGlyphWidth(prepared.lines, index),
|
|
3531
|
+
{ imagePlacements: this.#screenImagePlacements },
|
|
3504
3532
|
)}`;
|
|
3505
3533
|
}
|
|
3506
3534
|
if (this.#providerWindow.length > rows && newTop + rows < height) {
|
|
@@ -3525,8 +3553,6 @@ export class TUI extends Container {
|
|
|
3525
3553
|
preparedHistory.rows[index]!,
|
|
3526
3554
|
width,
|
|
3527
3555
|
Math.min(screenRow, height - 1),
|
|
3528
|
-
-1,
|
|
3529
|
-
-1,
|
|
3530
3556
|
this.#osc66SpacerGlyphWidth(preparedHistory.lines, index),
|
|
3531
3557
|
{ blankRow: compactReplay },
|
|
3532
3558
|
),
|
|
@@ -3540,10 +3566,12 @@ export class TUI extends Container {
|
|
|
3540
3566
|
prepared.rows[index]!,
|
|
3541
3567
|
width,
|
|
3542
3568
|
Math.min(screenRow, height - 1),
|
|
3543
|
-
-1,
|
|
3544
|
-
-1,
|
|
3545
3569
|
this.#osc66SpacerGlyphWidth(prepared.lines, index),
|
|
3546
|
-
{
|
|
3570
|
+
{
|
|
3571
|
+
blankRow: compactReplay,
|
|
3572
|
+
imagePlacements: index < replayViewportRows ? undefined : this.#screenImagePlacements,
|
|
3573
|
+
imageRow: newTop + index,
|
|
3574
|
+
},
|
|
3547
3575
|
),
|
|
3548
3576
|
);
|
|
3549
3577
|
screenRow++;
|
|
@@ -4164,10 +4192,8 @@ export class TUI extends Container {
|
|
|
4164
4192
|
line: PreparedLine,
|
|
4165
4193
|
width: number,
|
|
4166
4194
|
screenRow = -1,
|
|
4167
|
-
frameRow = -1,
|
|
4168
|
-
committedTo = -1,
|
|
4169
4195
|
spacerGlyphWidth = -1,
|
|
4170
|
-
options?: { blankRow?: boolean },
|
|
4196
|
+
options?: { blankRow?: boolean; imagePlacements?: Map<number, LiveImagePlacement>; imageRow?: number },
|
|
4171
4197
|
): string {
|
|
4172
4198
|
// End every rewrite at column zero. ConPTY can materialize a pending
|
|
4173
4199
|
// wrap before a following cursor-addressing sequence even while DECAWM is
|
|
@@ -4184,7 +4210,9 @@ export class TUI extends Container {
|
|
|
4184
4210
|
if (spacerGlyphWidth >= 0) {
|
|
4185
4211
|
rewrite = spacerGlyphWidth >= width ? "" : `${SEGMENT_RESET}\x1b[${spacerGlyphWidth}C${ERASE_TO_END_OF_LINE}`;
|
|
4186
4212
|
} else if (line.isImage) {
|
|
4187
|
-
rewrite =
|
|
4213
|
+
rewrite =
|
|
4214
|
+
ERASE_LINE +
|
|
4215
|
+
this.#imageLineSequence(line.line, screenRow, options?.imagePlacements, options?.imageRow ?? screenRow);
|
|
4188
4216
|
} else {
|
|
4189
4217
|
const terminalLine = this.#terminalLine(line);
|
|
4190
4218
|
if (options?.blankRow) {
|
|
@@ -4331,6 +4359,7 @@ export class TUI extends Container {
|
|
|
4331
4359
|
prepared.rows.some((_row, r) =>
|
|
4332
4360
|
this.#rowNeedsRewrite(this.#altPreviousLines, this.#altPreparedRows, prepared.lines, prepared.rows, r),
|
|
4333
4361
|
));
|
|
4362
|
+
prelude += this.#retireImagePlacements(this.#altImagePlacements, prepared.lines, 0, full);
|
|
4334
4363
|
let rowsBuffer = "";
|
|
4335
4364
|
for (let r = 0; r < height; r++) {
|
|
4336
4365
|
if (full) {
|
|
@@ -4346,9 +4375,8 @@ export class TUI extends Container {
|
|
|
4346
4375
|
prepared.rows[r]!,
|
|
4347
4376
|
width,
|
|
4348
4377
|
r,
|
|
4349
|
-
-1,
|
|
4350
|
-
-1,
|
|
4351
4378
|
this.#osc66SpacerGlyphWidth(prepared.lines, r),
|
|
4379
|
+
{ imagePlacements: this.#altImagePlacements },
|
|
4352
4380
|
);
|
|
4353
4381
|
}
|
|
4354
4382
|
this.#altPreviousLines = prepared.lines;
|