@oh-my-pi/pi-tui 18.6.0 → 18.6.3
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 +32 -0
- package/README.md +6 -2
- package/dist/types/app-keybindings.d.ts +6 -1
- package/dist/types/apps/git/git-tui.d.ts +3 -2
- package/dist/types/chat/assistant-message.d.ts +6 -1
- package/dist/types/chat/chat-transcript-builder.d.ts +1 -0
- package/dist/types/chat/user-message.d.ts +2 -2
- package/dist/types/chrome/status-notice.d.ts +15 -3
- package/dist/types/components/editor.d.ts +4 -0
- package/dist/types/components/image.d.ts +14 -0
- package/dist/types/components/input.d.ts +1 -0
- package/dist/types/components/loader.d.ts +5 -6
- package/dist/types/key-hint-format.d.ts +8 -0
- package/dist/types/native/apply.d.ts +4 -3
- package/dist/types/native/backend.d.ts +2 -2
- package/dist/types/native/encode.d.ts +4 -3
- package/dist/types/native/node.d.ts +8 -1
- package/dist/types/native/overlay.d.ts +3 -3
- package/dist/types/native/settle.d.ts +4 -3
- package/dist/types/overlays/agent-hub.d.ts +1 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +1 -0
- package/dist/types/overlays/btw-history-panel.d.ts +13 -1
- package/dist/types/overlays/copy-selector.d.ts +1 -0
- package/dist/types/overlays/rewind-selector.d.ts +1 -0
- package/dist/types/overlays/usage-dashboard.d.ts +4 -4
- package/dist/types/prompt/custom-editor.d.ts +10 -4
- package/dist/types/prompt/interactive-context-helpers.d.ts +1 -0
- package/dist/types/space-hold.d.ts +40 -25
- package/dist/types/terminal-capabilities.d.ts +13 -0
- package/dist/types/tui.d.ts +5 -0
- package/package.json +9 -9
- package/src/app-keybindings.ts +6 -1
- package/src/apps/git/diff-pane.ts +70 -18
- package/src/apps/git/git-tui.ts +21 -14
- package/src/apps/git/sidebar.ts +6 -1
- package/src/chat/assistant-message.ts +26 -4
- package/src/chat/chat-transcript-builder.ts +4 -0
- package/src/chat/tool-execution.ts +6 -1
- package/src/chat/user-message.ts +16 -5
- package/src/chrome/status-notice.ts +25 -7
- package/src/chrome/transcript-container.ts +6 -3
- package/src/components/editor.ts +14 -0
- package/src/components/image.ts +68 -1
- package/src/components/input.ts +14 -6
- package/src/components/loader.ts +10 -17
- package/src/hotkeys-markdown.ts +9 -2
- package/src/key-hint-format.ts +25 -10
- package/src/native/apply.ts +4 -3
- package/src/native/backend.ts +10 -3
- package/src/native/encode.ts +4 -3
- package/src/native/node.ts +8 -1
- package/src/native/overlay.ts +4 -4
- package/src/native/settle.ts +4 -3
- package/src/overlays/agent-hub.ts +4 -0
- package/src/overlays/agent-transcript-viewer.ts +2 -0
- package/src/overlays/btw-history-panel.ts +124 -38
- package/src/overlays/copy-selector.ts +2 -0
- package/src/overlays/extensions/extension-dashboard.ts +4 -4
- package/src/overlays/pause-screen.ts +2 -2
- package/src/overlays/rewind-selector.ts +2 -0
- package/src/overlays/usage-dashboard.ts +17 -8
- package/src/overlays/usage-row.ts +22 -8
- package/src/prompt/custom-editor.ts +188 -67
- package/src/prompt/interactive-context-helpers.ts +2 -0
- package/src/prompt/queued-messages.ts +6 -4
- package/src/space-hold.ts +156 -52
- package/src/terminal-capabilities.ts +42 -23
- package/src/tui.ts +36 -14
|
@@ -284,6 +284,13 @@ export interface ImageRenderOptions {
|
|
|
284
284
|
placementId?: number;
|
|
285
285
|
/** When true (Kitty + {@link imageId}), also return the one-time transmit sequence. */
|
|
286
286
|
includeTransmit?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* SIXEL sequence for the target pixel size renderImage computes, so the
|
|
289
|
+
* caller can encode off the JS thread: answer `undefined` while the encode
|
|
290
|
+
* is pending (the image's rows stay reserved) and `null` once it failed.
|
|
291
|
+
* Without a provider, renderImage encodes synchronously.
|
|
292
|
+
*/
|
|
293
|
+
sixel?: (widthPx: number, heightPx: number) => string | null | undefined;
|
|
287
294
|
}
|
|
288
295
|
export declare function getCellDimensions(): CellDimensions;
|
|
289
296
|
export declare function setCellDimensions(dims: CellDimensions): void;
|
|
@@ -381,6 +388,12 @@ export declare function getJpegDimensions(base64Data: string): ImageDimensions |
|
|
|
381
388
|
export declare function getGifDimensions(base64Data: string): ImageDimensions | null;
|
|
382
389
|
export declare function getWebpDimensions(base64Data: string): ImageDimensions | null;
|
|
383
390
|
export declare function getImageDimensions(base64Data: string, mimeType: string): ImageDimensions | null;
|
|
391
|
+
/**
|
|
392
|
+
* SIXEL sequence for `base64Data` at the given pixel size, encoded on the JS
|
|
393
|
+
* thread; `null` when the encode fails. For output that cannot wait for an
|
|
394
|
+
* off-thread encode.
|
|
395
|
+
*/
|
|
396
|
+
export declare function encodeSixelNow(base64Data: string, widthPx: number, heightPx: number): string | null;
|
|
384
397
|
export declare function renderImage(base64Data: string, imageDimensions: ImageDimensions, options?: ImageRenderOptions): {
|
|
385
398
|
sequence?: string;
|
|
386
399
|
lines?: string[];
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -150,6 +150,11 @@ export interface Component {
|
|
|
150
150
|
describeScreen?(cx: DescribeContext): NativeScreen;
|
|
151
151
|
/** User actions on nodes this component described (toggle, select, activate, custom). */
|
|
152
152
|
handleNativeEvent?(event: NativeUiEvent): void;
|
|
153
|
+
/**
|
|
154
|
+
* Pure preflight for focused input that must precede TUI-wide input listeners
|
|
155
|
+
* and debug shortcuts. A true result routes the event to this component first.
|
|
156
|
+
*/
|
|
157
|
+
capturesInput?(data: string): boolean;
|
|
153
158
|
/**
|
|
154
159
|
* Optional handler for keyboard input when component has focus
|
|
155
160
|
*/
|
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.6.
|
|
4
|
+
"version": "18.6.3",
|
|
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.6.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.6.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.6.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.6.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.6.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.6.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.6.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.6.
|
|
44
|
+
"@oh-my-pi/omptype": "18.6.3",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.6.3",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.6.3",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.6.3",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.6.3",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.6.3",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.6.3",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.6.3"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/app-keybindings.ts
CHANGED
|
@@ -60,6 +60,7 @@ interface AppKeybindings {
|
|
|
60
60
|
"app.tree.unfoldOrDown": true;
|
|
61
61
|
"app.plan.toggle": true;
|
|
62
62
|
"app.history.search": true;
|
|
63
|
+
"app.stt.pushToTalk": true;
|
|
63
64
|
"app.stt.toggle": true;
|
|
64
65
|
"app.live.toggle": true;
|
|
65
66
|
}
|
|
@@ -237,9 +238,13 @@ export const KEYBINDINGS = {
|
|
|
237
238
|
defaultKeys: "ctrl+r",
|
|
238
239
|
description: "Search history",
|
|
239
240
|
},
|
|
241
|
+
"app.stt.pushToTalk": {
|
|
242
|
+
defaultKeys: "space",
|
|
243
|
+
description: "Hold to record speech-to-text",
|
|
244
|
+
},
|
|
240
245
|
"app.stt.toggle": {
|
|
241
246
|
defaultKeys: [],
|
|
242
|
-
description: "Toggle speech-to-text
|
|
247
|
+
description: "Toggle speech-to-text recording",
|
|
243
248
|
},
|
|
244
249
|
"app.live.toggle": {
|
|
245
250
|
defaultKeys: "ctrl+l",
|
|
@@ -184,6 +184,10 @@ export interface DiffBuildOptions {
|
|
|
184
184
|
streamResult?: DiffStreamResult;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
function filePatchHeader(filePath: string): string {
|
|
188
|
+
return `diff --git a/${filePath} b/${filePath}\n--- a/${filePath}\n+++ b/${filePath}\n`;
|
|
189
|
+
}
|
|
190
|
+
|
|
187
191
|
/** Build the aligned document for one file from its raw old/new texts. */
|
|
188
192
|
export function buildDiffDocument(
|
|
189
193
|
oldRaw: string,
|
|
@@ -376,10 +380,11 @@ export function buildDiffDocument(
|
|
|
376
380
|
// cannot use the raw streamed result because its equality basis differs.
|
|
377
381
|
const canPatch = !ignoreWs;
|
|
378
382
|
const tightHunks = streamed?.hunks ?? structuredPatchHunks(oldBasis, newBasis, DIFF_CONTEXT_LINES);
|
|
383
|
+
const patchHeader = canPatch && tightHunks.length > 0 ? filePatchHeader(filePath) : "";
|
|
379
384
|
const allHunks: HunkBlock[] = tightHunks.map(hunk => ({
|
|
380
385
|
header: `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`,
|
|
381
386
|
patch: canPatch
|
|
382
|
-
?
|
|
387
|
+
? `${patchHeader}@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@\n${hunk.lines.join("\n")}\n`
|
|
383
388
|
: "",
|
|
384
389
|
rows: walkHunk(hunk, false),
|
|
385
390
|
}));
|
|
@@ -476,7 +481,7 @@ export function buildLineSelectionPatch(
|
|
|
476
481
|
`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@\n${hunk.lines.join("\n")}`,
|
|
477
482
|
)
|
|
478
483
|
.join("\n");
|
|
479
|
-
return
|
|
484
|
+
return `${filePatchHeader(doc.filePath)}${body}\n`;
|
|
480
485
|
}
|
|
481
486
|
|
|
482
487
|
// ── palette ──────────────────────────────────────────────────────────────────
|
|
@@ -596,6 +601,12 @@ export class DiffPane {
|
|
|
596
601
|
#layoutCache: { key: string; visuals: Visual[] } | undefined;
|
|
597
602
|
/** Per visible row: clickable hunk-button ranges recorded during render. */
|
|
598
603
|
#hits: ({ hunk: number; primary?: [number, number]; discard?: [number, number] } | undefined)[] = [];
|
|
604
|
+
/**
|
|
605
|
+
* Asset images from the last render, by placement and size. Reusing them
|
|
606
|
+
* lets a SIXEL image show the sequence its off-thread encode delivers on a
|
|
607
|
+
* later render; a fresh `Image` per render would restart the encode forever.
|
|
608
|
+
*/
|
|
609
|
+
#assetImages = new Map<string, Image>();
|
|
599
610
|
constructor(imageBudget?: ImageBudget) {
|
|
600
611
|
this.#imageBudget = imageBudget;
|
|
601
612
|
}
|
|
@@ -721,6 +732,7 @@ export class DiffPane {
|
|
|
721
732
|
this.scrollLeft = 0;
|
|
722
733
|
this.cursor = 0;
|
|
723
734
|
this.anchor = null;
|
|
735
|
+
if (mode === "hunk") this.selectedHunk = 0;
|
|
724
736
|
}
|
|
725
737
|
|
|
726
738
|
cycleMode(): void {
|
|
@@ -771,6 +783,7 @@ export class DiffPane {
|
|
|
771
783
|
this.anchor = null;
|
|
772
784
|
}
|
|
773
785
|
this.cursor = Math.max(0, Math.min(total - 1, this.cursor + delta));
|
|
786
|
+
this.#syncHunkToCursor();
|
|
774
787
|
this.scrollTop = scrollOffsetForRow(this.scrollTop, this.cursor, total, this.#lastHeight);
|
|
775
788
|
this.#clampScroll();
|
|
776
789
|
}
|
|
@@ -854,6 +867,7 @@ export class DiffPane {
|
|
|
854
867
|
if (total === 0) return;
|
|
855
868
|
this.anchor = null;
|
|
856
869
|
this.cursor = edge === "start" ? 0 : total - 1;
|
|
870
|
+
this.#syncHunkToCursor();
|
|
857
871
|
this.scrollTop = scrollOffsetForRow(this.scrollTop, this.cursor, total, this.#lastHeight);
|
|
858
872
|
this.#clampScroll();
|
|
859
873
|
}
|
|
@@ -870,6 +884,19 @@ export class DiffPane {
|
|
|
870
884
|
}
|
|
871
885
|
}
|
|
872
886
|
|
|
887
|
+
/** Keep the hunk action and highlighted header on the cursor's hunk, including blank separators. */
|
|
888
|
+
#syncHunkToCursor(): void {
|
|
889
|
+
if (this.mode !== "hunk") return;
|
|
890
|
+
const visuals = this.#layout(this.#lastWidth || 80);
|
|
891
|
+
for (let row = this.cursor; row >= 0; row--) {
|
|
892
|
+
const visual = visuals[row];
|
|
893
|
+
if (visual?.t === "header") {
|
|
894
|
+
this.selectedHunk = visual.hunk;
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
873
900
|
/** First visual row of every change block, in document order. */
|
|
874
901
|
#changeStarts(visuals: Visual[]): number[] {
|
|
875
902
|
const starts: number[] = [];
|
|
@@ -917,6 +944,10 @@ export class DiffPane {
|
|
|
917
944
|
const hit = this.#hits[row];
|
|
918
945
|
if (hit && this.#doc) {
|
|
919
946
|
this.selectedHunk = hit.hunk;
|
|
947
|
+
if (this.mode === "hunk") {
|
|
948
|
+
this.cursor = this.scrollTop + row;
|
|
949
|
+
this.anchor = null;
|
|
950
|
+
}
|
|
920
951
|
if (hit.primary && col >= hit.primary[0] && col < hit.primary[1] && this.patchTarget) {
|
|
921
952
|
return { type: "hunk-action", hunk: this.#doc.hunks[hit.hunk], action: this.patchTarget };
|
|
922
953
|
}
|
|
@@ -933,6 +964,7 @@ export class DiffPane {
|
|
|
933
964
|
this.anchor = null;
|
|
934
965
|
}
|
|
935
966
|
this.cursor = visual;
|
|
967
|
+
this.#syncHunkToCursor();
|
|
936
968
|
return { type: "handled" };
|
|
937
969
|
}
|
|
938
970
|
return null;
|
|
@@ -1077,8 +1109,16 @@ export class DiffPane {
|
|
|
1077
1109
|
const leftWidth = Math.max(1, Math.floor((width - 1) / 2));
|
|
1078
1110
|
const rightWidth = Math.max(1, width - leftWidth - 1);
|
|
1079
1111
|
const bodyHeight = Math.max(0, height - 1);
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1112
|
+
const previousImages = this.#assetImages;
|
|
1113
|
+
this.#assetImages = new Map();
|
|
1114
|
+
const oldLines = this.#renderAssetSide(asset.old, leftWidth, bodyHeight, `old:${asset.filePath}`, previousImages);
|
|
1115
|
+
const newLines = this.#renderAssetSide(
|
|
1116
|
+
asset.new,
|
|
1117
|
+
rightWidth,
|
|
1118
|
+
bodyHeight,
|
|
1119
|
+
`new:${asset.filePath}`,
|
|
1120
|
+
previousImages,
|
|
1121
|
+
);
|
|
1082
1122
|
const border = theme.fg("borderMuted", "│");
|
|
1083
1123
|
const lines: string[] = [];
|
|
1084
1124
|
for (let index = 0; index < height; index++) {
|
|
@@ -1099,24 +1139,36 @@ export class DiffPane {
|
|
|
1099
1139
|
return lines;
|
|
1100
1140
|
}
|
|
1101
1141
|
|
|
1102
|
-
#renderAssetSide(
|
|
1142
|
+
#renderAssetSide(
|
|
1143
|
+
side: FileAssetSide,
|
|
1144
|
+
width: number,
|
|
1145
|
+
height: number,
|
|
1146
|
+
placementKey: string,
|
|
1147
|
+
previousImages: ReadonlyMap<string, Image>,
|
|
1148
|
+
): readonly string[] {
|
|
1103
1149
|
if (height <= 0) return [];
|
|
1104
1150
|
let content: readonly string[];
|
|
1105
1151
|
if (side.kind === "image") {
|
|
1106
1152
|
const image = side.image;
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1153
|
+
const imageKey = `git-review:${placementKey}:${image.key}`;
|
|
1154
|
+
const cacheKey = `${imageKey}:${width}x${height}`;
|
|
1155
|
+
const component =
|
|
1156
|
+
previousImages.get(cacheKey) ??
|
|
1157
|
+
new Image(
|
|
1158
|
+
image.data,
|
|
1159
|
+
image.mimeType,
|
|
1160
|
+
{ fallbackColor: text => theme.fg("dim", text) },
|
|
1161
|
+
{
|
|
1162
|
+
maxWidthCells: Math.max(1, width - 2),
|
|
1163
|
+
maxHeightCells: height,
|
|
1164
|
+
filename: this.#asset?.filePath,
|
|
1165
|
+
budget: this.#imageBudget,
|
|
1166
|
+
imageKey,
|
|
1167
|
+
},
|
|
1168
|
+
{ widthPx: image.widthPx, heightPx: image.heightPx },
|
|
1169
|
+
);
|
|
1170
|
+
this.#assetImages.set(cacheKey, component);
|
|
1171
|
+
content = component.render(width);
|
|
1120
1172
|
} else {
|
|
1121
1173
|
let details: string[];
|
|
1122
1174
|
switch (side.kind) {
|
package/src/apps/git/git-tui.ts
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* arrows/PgUp/PgDn, vim motions (`j`/`k`/`h`/`l`/`g`/`G`), and mouse
|
|
14
14
|
* clicks/wheel. All toolbar buttons are clickable and mirrored by keys:
|
|
15
15
|
* `v` cycles the view (`1`–`4` pick one), `alt+↓`/`alt+↑` jump hunks and roll
|
|
16
|
-
* into the adjacent file at the edges, `
|
|
17
|
-
*
|
|
16
|
+
* into the adjacent file at the edges, `space` stages/unstages the focused
|
|
17
|
+
* diff row (the focused hunk in hunk view), `s`/`u` stage/unstage
|
|
18
|
+
* (hunk-aware), `x` discards a hunk, `delete` discards the
|
|
18
19
|
* whole file (press twice to confirm), `w` wraps, `b` cycles
|
|
19
20
|
* whitespace handling (exact → ignore whitespace → ignore
|
|
20
21
|
* formatting/import-only changes), `c` jumps to the commit form, `r`
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
import type { TspSpan } from "@oh-my-pi/pi-wire";
|
|
28
|
-
import { formatKeyHint, formatKeyHints } from "../../app-keybindings";
|
|
29
|
+
import { formatKeyHint, formatKeyHints, formatTooltipKey } from "../../app-keybindings";
|
|
29
30
|
import { SplitPane } from "../../components/layout/split-pane";
|
|
30
31
|
import { Stack } from "../../components/layout/stack";
|
|
31
32
|
import { matchesKey } from "../../keys";
|
|
@@ -669,14 +670,20 @@ class GitTuiComponent implements Component {
|
|
|
669
670
|
else if (matchesKey(data, "up") || data === "k") this.#pane.moveCursor(-1, false);
|
|
670
671
|
else if (matchesKey(data, "down") || data === "j") this.#pane.moveCursor(1, false);
|
|
671
672
|
else if (matchesKey(data, "pageUp")) this.#pane.moveCursor(-Math.max(1, this.#contentHeight - 2), false);
|
|
672
|
-
else if (matchesKey(data, "pageDown")
|
|
673
|
-
this.#pane.moveCursor(Math.max(1, this.#contentHeight - 2), false);
|
|
673
|
+
else if (matchesKey(data, "pageDown")) this.#pane.moveCursor(Math.max(1, this.#contentHeight - 2), false);
|
|
674
674
|
else if (matchesKey(data, "left") || data === "h") this.#pane.scrollLeftBy(-8);
|
|
675
675
|
else if (matchesKey(data, "right") || data === "l") this.#pane.scrollLeftBy(8);
|
|
676
676
|
else if (matchesKey(data, "home") || data === "g") this.#pane.cursorToEdge("start");
|
|
677
677
|
else if (matchesKey(data, "end") || data === "G") this.#pane.cursorToEdge("end");
|
|
678
678
|
else if (matchesKey(data, "enter")) return this.#jumpHunkOrFile(1);
|
|
679
|
-
else if (data
|
|
679
|
+
else if (matchesKey(data, "space")) {
|
|
680
|
+
// Hunk-view rows do not map back to document rows, so Space acts on the focused hunk there.
|
|
681
|
+
const target = this.#pane.patchTarget;
|
|
682
|
+
const hunk = this.#pane.mode === "hunk" ? this.#pane.currentHunk : null;
|
|
683
|
+
if (target && hunk) void this.#hunkAction(hunk, target);
|
|
684
|
+
else if (target && this.#pane.selection) void this.#lineAction(target);
|
|
685
|
+
return;
|
|
686
|
+
} else if (data === "s" || data === "u") {
|
|
680
687
|
if (this.#pane.selection?.explicit && this.#pane.patchTarget) {
|
|
681
688
|
void this.#lineAction(this.#pane.patchTarget);
|
|
682
689
|
return;
|
|
@@ -1023,8 +1030,8 @@ class GitTuiComponent implements Component {
|
|
|
1023
1030
|
const right: NativeNode[] = [
|
|
1024
1031
|
row(
|
|
1025
1032
|
[
|
|
1026
|
-
iconButton("chev-up", "prev-hunk", `Previous change (${
|
|
1027
|
-
iconButton("chev", "next-hunk", `Next change (${
|
|
1033
|
+
iconButton("chev-up", "prev-hunk", `Previous change (${formatTooltipKey("alt+up")})`),
|
|
1034
|
+
iconButton("chev", "next-hunk", `Next change (${formatTooltipKey("alt+down")})`),
|
|
1028
1035
|
],
|
|
1029
1036
|
{ gap: "none", align: "center" },
|
|
1030
1037
|
),
|
|
@@ -1045,13 +1052,13 @@ class GitTuiComponent implements Component {
|
|
|
1045
1052
|
"type",
|
|
1046
1053
|
"whitespace",
|
|
1047
1054
|
this.#whitespace === "off"
|
|
1048
|
-
? `Ignore whitespace (${
|
|
1055
|
+
? `Ignore whitespace (${formatTooltipKey("b")})`
|
|
1049
1056
|
: this.#whitespace === "whitespace"
|
|
1050
|
-
? `Ignoring whitespace; also ignore formatting (${
|
|
1051
|
-
: `Ignoring formatting and imports; show all (${
|
|
1057
|
+
? `Ignoring whitespace; also ignore formatting (${formatTooltipKey("b")})`
|
|
1058
|
+
: `Ignoring formatting and imports; show all (${formatTooltipKey("b")})`,
|
|
1052
1059
|
this.#whitespace !== "off",
|
|
1053
1060
|
),
|
|
1054
|
-
iconButton("corner-down-right", "wrap", `Wrap lines (${
|
|
1061
|
+
iconButton("corner-down-right", "wrap", `Wrap lines (${formatTooltipKey("w")})`, this.#pane.wrap),
|
|
1055
1062
|
);
|
|
1056
1063
|
}
|
|
1057
1064
|
if (file?.area === "unstaged" || file?.area === "staged") {
|
|
@@ -1063,8 +1070,8 @@ class GitTuiComponent implements Component {
|
|
|
1063
1070
|
);
|
|
1064
1071
|
}
|
|
1065
1072
|
right.push(
|
|
1066
|
-
iconButton("keyboard", "help", `Keyboard shortcuts (${
|
|
1067
|
-
iconButton("x", "close", `Close (${
|
|
1073
|
+
iconButton("keyboard", "help", `Keyboard shortcuts (${formatTooltipKey("?")})`),
|
|
1074
|
+
iconButton("x", "close", `Close (${formatTooltipKey("q")})`),
|
|
1068
1075
|
);
|
|
1069
1076
|
return keyed(
|
|
1070
1077
|
row(
|
package/src/apps/git/sidebar.ts
CHANGED
|
@@ -1699,7 +1699,12 @@ export class Sidebar {
|
|
|
1699
1699
|
png,
|
|
1700
1700
|
"image/png",
|
|
1701
1701
|
{ fallbackColor: text => theme.fg("dim", text) },
|
|
1702
|
-
{
|
|
1702
|
+
{
|
|
1703
|
+
maxHeightCells: 3,
|
|
1704
|
+
budget: this.#imageBudget,
|
|
1705
|
+
imageKey: `git-avatar:${email}`,
|
|
1706
|
+
requestRender: this.#requestRender,
|
|
1707
|
+
},
|
|
1703
1708
|
),
|
|
1704
1709
|
};
|
|
1705
1710
|
}
|
|
@@ -456,6 +456,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
456
456
|
readonly #thinkingRenderers: readonly AssistantThinkingRenderer[];
|
|
457
457
|
readonly #imageBudget?: ImageBudget;
|
|
458
458
|
#proseOnlyThinking: boolean;
|
|
459
|
+
#expandThinkingBlocks: boolean;
|
|
459
460
|
|
|
460
461
|
constructor(
|
|
461
462
|
message?: AssistantMessage,
|
|
@@ -465,6 +466,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
465
466
|
imageBudget?: ImageBudget,
|
|
466
467
|
proseOnlyThinking = true,
|
|
467
468
|
linkTargets?: ReadonlyMap<string, string>,
|
|
469
|
+
expandThinkingBlocks = false,
|
|
468
470
|
) {
|
|
469
471
|
super();
|
|
470
472
|
this.#hideThinkingBlock = hideThinkingBlock;
|
|
@@ -472,6 +474,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
472
474
|
this.#thinkingRenderers = thinkingRenderers;
|
|
473
475
|
this.#imageBudget = imageBudget;
|
|
474
476
|
this.#proseOnlyThinking = proseOnlyThinking;
|
|
477
|
+
this.#expandThinkingBlocks = expandThinkingBlocks;
|
|
475
478
|
|
|
476
479
|
ensureThemeSync();
|
|
477
480
|
this.#transcriptBlockFinalized = message !== undefined;
|
|
@@ -560,6 +563,16 @@ export class AssistantMessageComponent extends Container {
|
|
|
560
563
|
this.#proseOnlyThinking = proseOnly;
|
|
561
564
|
}
|
|
562
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Keep finished thinking sections expanded instead of folding them to "Thought for 12s".
|
|
568
|
+
* Sections the user folded or unfolded by hand keep that choice.
|
|
569
|
+
*/
|
|
570
|
+
setExpandThinkingBlocks(expand: boolean): void {
|
|
571
|
+
if (this.#expandThinkingBlocks === expand) return;
|
|
572
|
+
this.#expandThinkingBlocks = expand;
|
|
573
|
+
this.#nativeViewVersion++;
|
|
574
|
+
}
|
|
575
|
+
|
|
563
576
|
override dispose(): void {
|
|
564
577
|
this.#stopThinkingAnimation();
|
|
565
578
|
super.dispose();
|
|
@@ -831,8 +844,10 @@ export class AssistantMessageComponent extends Container {
|
|
|
831
844
|
// `.live` while streaming: the body clamps to its tail under a fade.
|
|
832
845
|
role: thinkingLive ? "omp.thinking.live" : "omp.thinking",
|
|
833
846
|
collapsible: true,
|
|
834
|
-
// Open while it streams; a finished thought folds to its "Thought for 12s" line.
|
|
835
|
-
collapsed:
|
|
847
|
+
// Open while it streams; a finished thought folds to its "Thought for 12s" line unless the user keeps thinking expanded.
|
|
848
|
+
collapsed:
|
|
849
|
+
this.#thinkingCollapsed.get(index) ??
|
|
850
|
+
(this.#hideThinkingBlock || (!thinkingLive && !this.#expandThinkingBlocks)),
|
|
836
851
|
// Tern's fold head sums it into "Worked for 12s".
|
|
837
852
|
took: clock?.end === undefined ? undefined : Math.max(0, Math.round(clock.end - clock.start)),
|
|
838
853
|
},
|
|
@@ -1373,7 +1388,12 @@ export class AssistantMessageComponent extends Container {
|
|
|
1373
1388
|
displayImage.data,
|
|
1374
1389
|
displayImage.mimeType,
|
|
1375
1390
|
{ fallbackColor: (text: string) => theme.fg("toolOutput", text) },
|
|
1376
|
-
{
|
|
1391
|
+
{
|
|
1392
|
+
...resolveImageOptions(),
|
|
1393
|
+
budget: this.#imageBudget,
|
|
1394
|
+
imageKey: key,
|
|
1395
|
+
requestRender: this.#onImageUpdate,
|
|
1396
|
+
},
|
|
1377
1397
|
),
|
|
1378
1398
|
);
|
|
1379
1399
|
continue;
|
|
@@ -1415,7 +1435,9 @@ export class AssistantMessageComponent extends Container {
|
|
|
1415
1435
|
}
|
|
1416
1436
|
|
|
1417
1437
|
#computeShapeKey(message: AssistantMessage): string {
|
|
1418
|
-
const parts: string[] = [
|
|
1438
|
+
const parts: string[] = [
|
|
1439
|
+
`htb:${this.#hideThinkingBlock ? 1 : 0}|pot:${this.#proseOnlyThinking ? 1 : 0}|etb:${this.#expandThinkingBlocks ? 1 : 0}`,
|
|
1440
|
+
];
|
|
1419
1441
|
for (const content of message.content) {
|
|
1420
1442
|
if (content.type === "text") {
|
|
1421
1443
|
parts.push(canonicalizeMessage(content.text) ? "T1" : "T0");
|
|
@@ -80,6 +80,7 @@ export interface ChatTranscriptBuilderDeps {
|
|
|
80
80
|
cwd: string;
|
|
81
81
|
hideThinkingBlock?: () => boolean;
|
|
82
82
|
proseOnlyThinking?: () => boolean;
|
|
83
|
+
expandThinkingBlocks?: () => boolean;
|
|
83
84
|
/** Session-scoped resolved destinations for model-authored Markdown links. */
|
|
84
85
|
linkTargets?: ReadonlyMap<string, string>;
|
|
85
86
|
requestRender: () => void;
|
|
@@ -404,6 +405,7 @@ export class ChatTranscriptBuilder {
|
|
|
404
405
|
#appendAssistantMessage(message: Extract<AgentMessage, { role: "assistant" }>): void {
|
|
405
406
|
const hideThinkingBlock = this.#deps.hideThinkingBlock?.() ?? false;
|
|
406
407
|
const proseOnlyThinking = this.#deps.proseOnlyThinking ? this.#deps.proseOnlyThinking() : true;
|
|
408
|
+
const expandThinkingBlocks = this.#deps.expandThinkingBlocks?.() ?? false;
|
|
407
409
|
const timeline = splitAssistantMessageToolTimeline(message);
|
|
408
410
|
const assistantComponent = new AssistantMessageComponent(
|
|
409
411
|
timeline.beforeTools,
|
|
@@ -413,6 +415,7 @@ export class ChatTranscriptBuilder {
|
|
|
413
415
|
this.#deps.ui.imageBudget,
|
|
414
416
|
proseOnlyThinking,
|
|
415
417
|
this.#deps.linkTargets,
|
|
418
|
+
expandThinkingBlocks,
|
|
416
419
|
);
|
|
417
420
|
assistantComponent.setImagesVisible(displayPreferences.showImages);
|
|
418
421
|
assistantComponent.setToolResultImagesVisible(!displayPreferences.hideToolActivity);
|
|
@@ -450,6 +453,7 @@ export class ChatTranscriptBuilder {
|
|
|
450
453
|
undefined,
|
|
451
454
|
proseOnlyThinking,
|
|
452
455
|
this.#deps.linkTargets,
|
|
456
|
+
expandThinkingBlocks,
|
|
453
457
|
);
|
|
454
458
|
component.setImagesVisible(displayPreferences.showImages);
|
|
455
459
|
component.setToolResultImagesVisible(!displayPreferences.hideToolActivity);
|
|
@@ -1639,7 +1639,12 @@ export class ToolExecutionComponent extends Container {
|
|
|
1639
1639
|
imageData,
|
|
1640
1640
|
imageMimeType,
|
|
1641
1641
|
{ fallbackColor: (s: string) => theme.fg("toolOutput", s) },
|
|
1642
|
-
{
|
|
1642
|
+
{
|
|
1643
|
+
...resolveImageOptions(),
|
|
1644
|
+
budget: this.#ui.imageBudget,
|
|
1645
|
+
imageKey: `te${this.#instanceId}:${i}`,
|
|
1646
|
+
requestRender: () => this.#ui.requestRender(),
|
|
1647
|
+
},
|
|
1643
1648
|
);
|
|
1644
1649
|
this.#imageComponents.push(imageComponent);
|
|
1645
1650
|
this.addChild(imageComponent);
|
package/src/chat/user-message.ts
CHANGED
|
@@ -24,7 +24,13 @@ import type { ReactionTarget } from "./reaction";
|
|
|
24
24
|
import { card, md, node, row, span, text } from "../native/describe";
|
|
25
25
|
import { base64ImageNode } from "../native/blobs";
|
|
26
26
|
import { hasTranscriptActions, runTranscriptAction } from "./transcript-actions";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
type DescribeContext,
|
|
29
|
+
type NativeChild,
|
|
30
|
+
type NativeNode,
|
|
31
|
+
type NativeUiEvent,
|
|
32
|
+
rootToggleExpanded,
|
|
33
|
+
} from "../native/node";
|
|
28
34
|
import { Memo } from "../native/memo";
|
|
29
35
|
|
|
30
36
|
// OSC 133 shell integration: marks prompt zones for terminal multiplexers.
|
|
@@ -137,6 +143,8 @@ export class UserMessageComponent extends Container implements ReactionTarget {
|
|
|
137
143
|
readonly #tokens: RegExp;
|
|
138
144
|
#reaction: string | undefined;
|
|
139
145
|
#native: NativeNode | undefined;
|
|
146
|
+
/** The terminal's clock {@link #native} was described with. */
|
|
147
|
+
#nativeHour12: boolean | undefined;
|
|
140
148
|
|
|
141
149
|
constructor(text: string, options: UserBubbleOptions = {}) {
|
|
142
150
|
super();
|
|
@@ -184,17 +192,20 @@ export class UserMessageComponent extends Container implements ReactionTarget {
|
|
|
184
192
|
* agent's reaction are chips at the bottom-right, so a reaction landing
|
|
185
193
|
* later updates the frame in place, even deep in scrollback.
|
|
186
194
|
*/
|
|
187
|
-
override describe(): NativeNode {
|
|
188
|
-
|
|
195
|
+
override describe(cx?: DescribeContext): NativeNode {
|
|
196
|
+
// The terminal's clock: Bun's own default locale ignores the user's.
|
|
197
|
+
const hour12 = cx?.hour12;
|
|
198
|
+
if (this.#native && this.#nativeHour12 === hour12) return this.#native;
|
|
199
|
+
this.#nativeHour12 = hour12;
|
|
189
200
|
const children: NativeChild[] = [];
|
|
190
201
|
if (!this.#synthetic && hasTranscriptActions()) {
|
|
191
202
|
const tools: NativeChild[] = [];
|
|
192
203
|
if (this.#timestamp !== undefined) {
|
|
193
204
|
const at = new Date(this.#timestamp);
|
|
194
205
|
tools.push(
|
|
195
|
-
text([span(at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), "dim mono")], {
|
|
206
|
+
text([span(at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12 }), "dim mono")], {
|
|
196
207
|
role: "omp.user.time",
|
|
197
|
-
title: at.toLocaleString(),
|
|
208
|
+
title: at.toLocaleString([], { hour12 }),
|
|
198
209
|
}),
|
|
199
210
|
);
|
|
200
211
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Spacer } from "../components/spacer";
|
|
2
2
|
import { Text } from "../components/text";
|
|
3
|
-
import { node } from "../native/describe";
|
|
3
|
+
import { EMPTY_NODE, node } from "../native/describe";
|
|
4
4
|
import type { NativeNode } from "../native/node";
|
|
5
5
|
import { styledSpans } from "../native/spans";
|
|
6
6
|
import { Container } from "../tui";
|
|
@@ -8,33 +8,51 @@ import { Container } from "../tui";
|
|
|
8
8
|
/** How long a native status toast stays up (ms). */
|
|
9
9
|
const TOAST_TTL = 2400;
|
|
10
10
|
|
|
11
|
+
/** How a {@link StatusNotice} shows. */
|
|
12
|
+
export interface StatusNoticeOptions {
|
|
13
|
+
/** Styles the ANSI line; resolved at render so a theme change re-shapes it. */
|
|
14
|
+
styleFn?: (text: string) => string;
|
|
15
|
+
/**
|
|
16
|
+
* Whether a native terminal toasts it (default). False keeps the notice to
|
|
17
|
+
* the ANSI transcript: background chatter such as MCP connection progress,
|
|
18
|
+
* which would otherwise re-toast on every server that connects or fails.
|
|
19
|
+
*/
|
|
20
|
+
toast?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
/**
|
|
12
24
|
* A transient status notice (`showStatus`: "Thinking blocks: hidden", "Copied
|
|
13
25
|
* to clipboard"). The ANSI render appends a blank row and the dim line to the
|
|
14
26
|
* transcript; a native terminal shows it as a toast instead, so notices never
|
|
15
|
-
* pile up in the history. Updating the text re-shows the toast
|
|
27
|
+
* pile up in the history. Updating the text re-shows the toast; a notice
|
|
28
|
+
* with `toast: false` describes nothing natively.
|
|
16
29
|
*/
|
|
17
30
|
export class StatusNotice extends Container {
|
|
18
31
|
readonly #text: Text;
|
|
19
32
|
#message: string;
|
|
33
|
+
#toast: boolean;
|
|
20
34
|
#native: NativeNode | undefined;
|
|
21
35
|
|
|
22
|
-
constructor(message: string,
|
|
36
|
+
constructor(message: string, options: StatusNoticeOptions = {}) {
|
|
23
37
|
super();
|
|
24
38
|
this.#message = message;
|
|
25
|
-
this.#
|
|
39
|
+
this.#toast = options.toast ?? true;
|
|
40
|
+
this.#text = new Text(message, 1, 0).setStyleFn(options.styleFn);
|
|
26
41
|
this.addChild(new Spacer(1));
|
|
27
42
|
this.addChild(this.#text);
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
setMessage(message: string,
|
|
31
|
-
|
|
45
|
+
setMessage(message: string, options: StatusNoticeOptions = {}): void {
|
|
46
|
+
const toast = options.toast ?? true;
|
|
47
|
+
this.#text.setStyleFn(options.styleFn);
|
|
32
48
|
this.#text.setText(message);
|
|
33
|
-
if (message !== this.#message) this.#native = undefined;
|
|
49
|
+
if (message !== this.#message || toast !== this.#toast) this.#native = undefined;
|
|
34
50
|
this.#message = message;
|
|
51
|
+
this.#toast = toast;
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
override describe(): NativeNode {
|
|
55
|
+
if (!this.#toast) return EMPTY_NODE;
|
|
38
56
|
if (this.#native) return this.#native;
|
|
39
57
|
// Callers may pass pre-styled text; a toast takes plain text.
|
|
40
58
|
const plain = styledSpans(this.#message)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Component, Container, type HistoryBatch } from "../tui";
|
|
2
2
|
import * as logger from "@oh-my-pi/pi-utils/logger";
|
|
3
3
|
import { popLoopPhase, pushLoopPhase } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { renderForScrollback } from "../components/image";
|
|
4
5
|
import { col } from "../native/describe";
|
|
5
6
|
import type { NativeNode } from "../native/node";
|
|
6
7
|
import { isNativeSettled, settleNative } from "../native/settle";
|
|
@@ -1000,9 +1001,11 @@ export class TranscriptContainer extends Container {
|
|
|
1000
1001
|
// Only the range head is sliced by its emitted stable prefix; every other
|
|
1001
1002
|
// entry renders whole, so the append-only verification pass (a second
|
|
1002
1003
|
// full render of the block's stable prefix) is skipped for them. This
|
|
1003
|
-
// keeps a complete-ledger replay at one render per block.
|
|
1004
|
-
|
|
1005
|
-
|
|
1004
|
+
// keeps a complete-ledger replay at one render per block. These rows go
|
|
1005
|
+
// to native scrollback, where nothing can repaint them.
|
|
1006
|
+
const rendered = renderForScrollback(() =>
|
|
1007
|
+
index === start ? this.#renderEntry(entry, width) : trimBlankEdges(entry.component.render(width)),
|
|
1008
|
+
);
|
|
1006
1009
|
const emittedRows = index === start ? this.#renderStablePrefix(entry, entry.emitted, width).length : 0;
|
|
1007
1010
|
const block = rendered.slice(emittedRows);
|
|
1008
1011
|
reached = index + 1;
|
package/src/components/editor.ts
CHANGED
|
@@ -3042,6 +3042,20 @@ export class Editor implements Component, Focusable {
|
|
|
3042
3042
|
return true;
|
|
3043
3043
|
}
|
|
3044
3044
|
|
|
3045
|
+
/** Whether a character-jump hotkey is waiting for its target character. */
|
|
3046
|
+
get isJumpPending(): boolean {
|
|
3047
|
+
return this.#jumpMode !== null;
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
/** Type literal text through the character-editing pipeline without keybinding dispatch. */
|
|
3051
|
+
typeCharacter(text: string): void {
|
|
3052
|
+
// Same as #handleInputChunk: typed input wins over a stale in-flight provider lookup.
|
|
3053
|
+
if (this.#autocompleteRequestRunning && this.#autocompleteState === null) {
|
|
3054
|
+
this.#invalidateAutocompleteRequests();
|
|
3055
|
+
}
|
|
3056
|
+
this.#insertCharacter(text);
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3045
3059
|
#insertCharacter(char: string): void {
|
|
3046
3060
|
this.#exitHistoryForEditing();
|
|
3047
3061
|
// Undo coalescing: consecutive word typing collapses into one undo unit
|