@oh-my-pi/pi-tui 16.3.5 → 16.3.7
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 +23 -0
- package/dist/types/autocomplete.d.ts +2 -0
- package/dist/types/components/box.d.ts +1 -1
- package/dist/types/components/cancellable-loader.d.ts +1 -1
- package/dist/types/components/editor.d.ts +4 -4
- package/dist/types/components/image.d.ts +2 -2
- package/dist/types/components/input.d.ts +1 -1
- package/dist/types/components/loader.d.ts +2 -2
- package/dist/types/components/markdown.d.ts +12 -2
- package/dist/types/components/scroll-view.d.ts +2 -2
- package/dist/types/components/select-list.d.ts +3 -3
- package/dist/types/components/settings-list.d.ts +2 -2
- package/dist/types/components/spacer.d.ts +1 -1
- package/dist/types/components/tab-bar.d.ts +1 -1
- package/dist/types/components/text.d.ts +1 -1
- package/dist/types/components/truncated-text.d.ts +1 -1
- package/dist/types/desktop-notify.d.ts +1 -1
- package/dist/types/editor-component.d.ts +2 -2
- package/dist/types/index.d.ts +32 -32
- package/dist/types/keybindings.d.ts +1 -1
- package/dist/types/terminal-capabilities.d.ts +6 -0
- package/dist/types/terminal.d.ts +1 -1
- package/dist/types/tui.d.ts +37 -65
- package/package.json +3 -3
- package/src/autocomplete.ts +57 -45
- package/src/components/image.ts +18 -2
- package/src/components/markdown.ts +58 -5
- package/src/terminal-capabilities.ts +16 -0
- package/src/tui.ts +190 -318
package/src/components/image.ts
CHANGED
|
@@ -65,6 +65,7 @@ export class ImageBudget {
|
|
|
65
65
|
#requestRender: () => void;
|
|
66
66
|
#nextId = nextImageIdSeed();
|
|
67
67
|
#keyToId = new Map<string, number>();
|
|
68
|
+
#idToKey = new Map<number, string>();
|
|
68
69
|
/** Display-order image ids observed during the in-flight pass. */
|
|
69
70
|
#passIds: number[] = [];
|
|
70
71
|
/**
|
|
@@ -132,6 +133,7 @@ export class ImageBudget {
|
|
|
132
133
|
const id = this.#nextId;
|
|
133
134
|
this.#nextId = (this.#nextId + 1) & 0xffffff || 1;
|
|
134
135
|
this.#keyToId.set(key, id);
|
|
136
|
+
this.#idToKey.set(id, key);
|
|
135
137
|
return id;
|
|
136
138
|
}
|
|
137
139
|
const id = this.#nextId;
|
|
@@ -163,11 +165,15 @@ export class ImageBudget {
|
|
|
163
165
|
*/
|
|
164
166
|
observe(imageId: number): boolean {
|
|
165
167
|
if (this.#stablePass) {
|
|
166
|
-
|
|
168
|
+
const suppressed = this.#cap > 0 && this.#suppressedIds.has(imageId);
|
|
169
|
+
if (suppressed) this.#forgetKeyForId(imageId);
|
|
170
|
+
return suppressed;
|
|
167
171
|
}
|
|
168
172
|
const index = this.#passIds.length;
|
|
169
173
|
this.#passIds.push(imageId);
|
|
170
|
-
|
|
174
|
+
const suppressed = this.#cap > 0 && index < this.#planned;
|
|
175
|
+
if (suppressed) this.#forgetKeyForId(imageId);
|
|
176
|
+
return suppressed;
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
/**
|
|
@@ -185,6 +191,7 @@ export class ImageBudget {
|
|
|
185
191
|
this.#purgeIds.push(id);
|
|
186
192
|
// d=I frees the data too, so the image must re-transmit if it returns.
|
|
187
193
|
this.#transmitted.delete(id);
|
|
194
|
+
this.#forgetKeyForId(id);
|
|
188
195
|
}
|
|
189
196
|
this.#onTerminal = this.#planned;
|
|
190
197
|
this.#applyingReset = false;
|
|
@@ -214,6 +221,8 @@ export class ImageBudget {
|
|
|
214
221
|
this.#transmitted.clear();
|
|
215
222
|
this.#purgeIds = [];
|
|
216
223
|
this.#pendingTransmits = [];
|
|
224
|
+
this.#keyToId.clear();
|
|
225
|
+
this.#idToKey.clear();
|
|
217
226
|
return ids;
|
|
218
227
|
}
|
|
219
228
|
|
|
@@ -274,6 +283,13 @@ export class ImageBudget {
|
|
|
274
283
|
this.#pendingTransmits = [];
|
|
275
284
|
}
|
|
276
285
|
|
|
286
|
+
#forgetKeyForId(id: number): void {
|
|
287
|
+
const key = this.#idToKey.get(id);
|
|
288
|
+
if (key === undefined) return;
|
|
289
|
+
this.#idToKey.delete(id);
|
|
290
|
+
if (this.#keyToId.get(key) === id) this.#keyToId.delete(key);
|
|
291
|
+
}
|
|
292
|
+
|
|
277
293
|
#reconcile(total: number): void {
|
|
278
294
|
const desired = this.#cap > 0 ? Math.max(0, total - this.#cap) : 0;
|
|
279
295
|
if (desired === this.#planned) {
|
|
@@ -817,6 +817,21 @@ export class Markdown implements Component {
|
|
|
817
817
|
#streamPrefixText?: string;
|
|
818
818
|
#streamPrefixTokens?: Token[];
|
|
819
819
|
#streamPrefixLineCache?: StreamPrefixLineCache;
|
|
820
|
+
// Rows of the most recent render() that are settled — top padding plus the
|
|
821
|
+
// rendered frozen token prefix — exposed via getLastRenderSettledRows()
|
|
822
|
+
// for native-scrollback commit gating.
|
|
823
|
+
#lastRenderSettledRows = 0;
|
|
824
|
+
// Frozen-prefix text backing the last non-zero settled exposure. Settled
|
|
825
|
+
// rows are declared final downstream, so a render whose frozen text no
|
|
826
|
+
// longer extends this prefix (a rewind / wholesale rewrite) resets the
|
|
827
|
+
// exposure to 0 and re-earns it — the exposure is hard-monotone within a
|
|
828
|
+
// text lineage.
|
|
829
|
+
#settledExposedText?: string;
|
|
830
|
+
// True while #renderStreamingContentLines renders the frozen token range:
|
|
831
|
+
// frozen code blocks highlight even in transient mode so their bytes match
|
|
832
|
+
// the finalized render (they render once into the prefix line cache, so
|
|
833
|
+
// the FFI cost is amortized); the volatile tail stays unhighlighted.
|
|
834
|
+
#renderingFrozenPrefix = false;
|
|
820
835
|
|
|
821
836
|
#ignoreTight = false;
|
|
822
837
|
|
|
@@ -857,6 +872,7 @@ export class Markdown implements Component {
|
|
|
857
872
|
this.#streamPrefixText = undefined;
|
|
858
873
|
this.#streamPrefixTokens = undefined;
|
|
859
874
|
this.#streamPrefixLineCache = undefined;
|
|
875
|
+
this.#settledExposedText = undefined;
|
|
860
876
|
}
|
|
861
877
|
this.invalidate();
|
|
862
878
|
return true;
|
|
@@ -878,6 +894,19 @@ export class Markdown implements Component {
|
|
|
878
894
|
this.invalidate();
|
|
879
895
|
}
|
|
880
896
|
|
|
897
|
+
/**
|
|
898
|
+
* Rows at the top of the most recent render() (top padding + rendered
|
|
899
|
+
* frozen-token prefix) whose bytes are settled: byte-stable at this
|
|
900
|
+
* width/theme for as long as the text keeps growing append-only. Hosts
|
|
901
|
+
* feed this to transcript commit gating (see the coding agent's
|
|
902
|
+
* `FinalizableBlock.getTranscriptBlockSettledRows`). 0 outside streaming
|
|
903
|
+
* (`transientRenderCache`) mode, after a text rewind (re-earned on the new
|
|
904
|
+
* lineage), and on cache-served non-streaming renders.
|
|
905
|
+
*/
|
|
906
|
+
getLastRenderSettledRows(): number {
|
|
907
|
+
return this.#lastRenderSettledRows;
|
|
908
|
+
}
|
|
909
|
+
|
|
881
910
|
// Lex `text` into block tokens, reusing the frozen stable prefix when the text
|
|
882
911
|
// only grew (the streaming path). Falls back to a full lex whenever the prefix
|
|
883
912
|
// is no longer a prefix (non-append edit), the text carries reference-link
|
|
@@ -965,6 +994,10 @@ export class Markdown implements Component {
|
|
|
965
994
|
return this.#cachedLines;
|
|
966
995
|
}
|
|
967
996
|
|
|
997
|
+
// Recomputed below by the streaming path; every other path (cache-served,
|
|
998
|
+
// empty text, non-streaming full render) exposes no settled rows.
|
|
999
|
+
this.#lastRenderSettledRows = 0;
|
|
1000
|
+
|
|
968
1001
|
// Calculate available width for content (subtract horizontal padding)
|
|
969
1002
|
const paddingX = this.#ignoreTight ? this.#paddingX : getPaddingX(this.#paddingX);
|
|
970
1003
|
const contentWidth = Math.max(1, width - paddingX * 2);
|
|
@@ -1076,9 +1109,16 @@ export class Markdown implements Component {
|
|
|
1076
1109
|
}
|
|
1077
1110
|
|
|
1078
1111
|
if (renderedUntil < frozenTokenCount) {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1112
|
+
// Frozen tokens render with full fidelity (syntax highlighting on)
|
|
1113
|
+
// so these cached rows byte-match the finalized render.
|
|
1114
|
+
this.#renderingFrozenPrefix = true;
|
|
1115
|
+
try {
|
|
1116
|
+
contentLines.push(
|
|
1117
|
+
...this.#renderContentLines(tokens, renderedUntil, frozenTokenCount, contentWidth, signature),
|
|
1118
|
+
);
|
|
1119
|
+
} finally {
|
|
1120
|
+
this.#renderingFrozenPrefix = false;
|
|
1121
|
+
}
|
|
1082
1122
|
renderedUntil = frozenTokenCount;
|
|
1083
1123
|
}
|
|
1084
1124
|
|
|
@@ -1089,6 +1129,19 @@ export class Markdown implements Component {
|
|
|
1089
1129
|
lines: contentLines.slice(),
|
|
1090
1130
|
};
|
|
1091
1131
|
|
|
1132
|
+
// Settled exposure (hard-monotone): these rows are declared final to
|
|
1133
|
+
// the host, so expose them only while the frozen text still extends
|
|
1134
|
+
// the previously exposed prefix; a rewind resets to 0 and re-earns on
|
|
1135
|
+
// the rewritten lineage.
|
|
1136
|
+
if (contentLines.length > 0) {
|
|
1137
|
+
if (this.#settledExposedText === undefined || frozenText.startsWith(this.#settledExposedText)) {
|
|
1138
|
+
this.#settledExposedText = frozenText;
|
|
1139
|
+
this.#lastRenderSettledRows = signature.paddingY + contentLines.length;
|
|
1140
|
+
} else {
|
|
1141
|
+
this.#settledExposedText = undefined;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1092
1145
|
if (renderedUntil < tokens.length) {
|
|
1093
1146
|
contentLines.push(...this.#renderContentLines(tokens, renderedUntil, tokens.length, contentWidth, signature));
|
|
1094
1147
|
}
|
|
@@ -1361,7 +1414,7 @@ export class Markdown implements Component {
|
|
|
1361
1414
|
|
|
1362
1415
|
const codeIndent = padding(this.#codeBlockIndent);
|
|
1363
1416
|
lines.push(this.#theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
|
|
1364
|
-
if (this.#theme.highlightCode && !this.transientRenderCache) {
|
|
1417
|
+
if (this.#theme.highlightCode && (!this.transientRenderCache || this.#renderingFrozenPrefix)) {
|
|
1365
1418
|
const highlightedLines = this.#theme.highlightCode(token.text, token.lang);
|
|
1366
1419
|
for (const hlLine of highlightedLines) {
|
|
1367
1420
|
lines.push(`${codeIndent}${hlLine}`);
|
|
@@ -1751,7 +1804,7 @@ export class Markdown implements Component {
|
|
|
1751
1804
|
// Code block in list item
|
|
1752
1805
|
const codeIndent = padding(this.#codeBlockIndent);
|
|
1753
1806
|
lines.push({ text: this.#theme.codeBlockBorder(`\`\`\`${token.lang || ""}`), nested: false });
|
|
1754
|
-
if (this.#theme.highlightCode && !this.transientRenderCache) {
|
|
1807
|
+
if (this.#theme.highlightCode && (!this.transientRenderCache || this.#renderingFrozenPrefix)) {
|
|
1755
1808
|
const highlightedLines = this.#theme.highlightCode(token.text, token.lang);
|
|
1756
1809
|
for (const hlLine of highlightedLines) {
|
|
1757
1810
|
lines.push({ text: `${codeIndent}${hlLine}`, nested: false });
|
|
@@ -122,6 +122,13 @@ export class TerminalInfo {
|
|
|
122
122
|
process.stdout.write(`${wrapTmuxPassthrough(formatted)}\x07`);
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
|
+
// Zellij drops OSC 9/99 and has no DCS passthrough envelope, but raises its
|
|
126
|
+
// `[!]` bell flag on a bare BEL — the same backgrounded-pane signal tmux
|
|
127
|
+
// users get. So follow the (Zellij-swallowed) OSC with a plain BEL.
|
|
128
|
+
if (this.notifyProtocol !== NotifyProtocol.Bell && isInsideZellij()) {
|
|
129
|
+
process.stdout.write(`${formatted}\x07`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
125
132
|
process.stdout.write(formatted);
|
|
126
133
|
// VTE-family terminals (Ptyxis, GNOME Terminal, Tilix, …) plus Alacritty
|
|
127
134
|
// and bare xterm-on-Wayland have no in-band escape that surfaces an
|
|
@@ -156,6 +163,15 @@ export function isInsideTerminalMultiplexer(env: NodeJS.ProcessEnv = Bun.env): b
|
|
|
156
163
|
return term.startsWith("tmux") || term.startsWith("screen");
|
|
157
164
|
}
|
|
158
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Whether the agent process is running inside a Zellij session. Read fresh on
|
|
168
|
+
* each call (like {@link isInsideTmux}) so a session attached/detached mid-run
|
|
169
|
+
* is observed and tests can toggle `Bun.env.ZELLIJ` per case.
|
|
170
|
+
*/
|
|
171
|
+
export function isInsideZellij(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
172
|
+
return Boolean(env.ZELLIJ);
|
|
173
|
+
}
|
|
174
|
+
|
|
159
175
|
/**
|
|
160
176
|
* Wrap a control-sequence payload in tmux's DCS passthrough envelope. Each
|
|
161
177
|
* ESC byte inside `payload` is doubled per tmux's escape rules. tmux strips
|