@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
package/src/components/image.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { encodeSixelAsync } from "@oh-my-pi/pi-natives";
|
|
1
2
|
import { getKittyGraphics } from "../kitty-graphics";
|
|
2
3
|
import {
|
|
4
|
+
encodeSixelNow,
|
|
3
5
|
getCellDimensions,
|
|
4
6
|
getImageDimensions,
|
|
5
7
|
type ImageDimensions,
|
|
@@ -29,6 +31,29 @@ export interface ImageOptions {
|
|
|
29
31
|
* repaint replaces the placement instead of stacking a duplicate.
|
|
30
32
|
*/
|
|
31
33
|
imageKey?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Schedules a repaint once this image's off-thread SIXEL encode lands.
|
|
36
|
+
* Defaults to the budget's repaint; a host without a budget must pass one,
|
|
37
|
+
* or the image's reserved rows stay blank until an unrelated repaint.
|
|
38
|
+
*/
|
|
39
|
+
requestRender?: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Renders in progress whose rows are bound for native scrollback. */
|
|
43
|
+
let scrollbackRenderDepth = 0;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run `render` for rows bound for native scrollback. Those rows are never
|
|
47
|
+
* repainted, so an image whose off-thread SIXEL encode has not landed encodes
|
|
48
|
+
* synchronously instead of committing its reserved blank rows.
|
|
49
|
+
*/
|
|
50
|
+
export function renderForScrollback<T>(render: () => T): T {
|
|
51
|
+
scrollbackRenderDepth++;
|
|
52
|
+
try {
|
|
53
|
+
return render();
|
|
54
|
+
} finally {
|
|
55
|
+
scrollbackRenderDepth--;
|
|
56
|
+
}
|
|
32
57
|
}
|
|
33
58
|
|
|
34
59
|
const EMPTY_IDS: readonly number[] = [];
|
|
@@ -227,6 +252,11 @@ export class ImageBudget {
|
|
|
227
252
|
this.#requestRender = requestRender;
|
|
228
253
|
}
|
|
229
254
|
|
|
255
|
+
/** Ask for a repaint, e.g. once an image's off-thread encode settles. */
|
|
256
|
+
requestRender(): void {
|
|
257
|
+
this.#requestRender();
|
|
258
|
+
}
|
|
259
|
+
|
|
230
260
|
setCap(cap: number): void {
|
|
231
261
|
const next = normalizeCap(cap);
|
|
232
262
|
if (next === this.#cap) return;
|
|
@@ -735,6 +765,8 @@ export class Image implements Component {
|
|
|
735
765
|
// (its rows may already be committed to native scrollback).
|
|
736
766
|
#renderedGraphicRows = 0;
|
|
737
767
|
#native?: NativeNode;
|
|
768
|
+
/** Newest SIXEL encode: its target size and, once settled, the sequence (`null` on failure). */
|
|
769
|
+
#sixel?: { widthPx: number; heightPx: number; sequence?: string | null };
|
|
738
770
|
|
|
739
771
|
constructor(
|
|
740
772
|
base64Data: string,
|
|
@@ -768,6 +800,38 @@ export class Image implements Component {
|
|
|
768
800
|
this.#cachedWidth = undefined;
|
|
769
801
|
}
|
|
770
802
|
|
|
803
|
+
/**
|
|
804
|
+
* SIXEL sequence for a target size. A new size starts the encode off the JS
|
|
805
|
+
* thread and answers `undefined` until it settles; the settled encode
|
|
806
|
+
* invalidates the cached lines and requests a repaint. A render bound for
|
|
807
|
+
* scrollback cannot wait, so it encodes synchronously.
|
|
808
|
+
*/
|
|
809
|
+
#sixelSequence(widthPx: number, heightPx: number): string | null | undefined {
|
|
810
|
+
const current = this.#sixel;
|
|
811
|
+
const sameSize = current !== undefined && current.widthPx === widthPx && current.heightPx === heightPx;
|
|
812
|
+
if (sameSize && current.sequence !== undefined) return current.sequence;
|
|
813
|
+
if (scrollbackRenderDepth > 0) {
|
|
814
|
+
// Replacing the entry makes a still-pending async encode settle as stale.
|
|
815
|
+
const sequence = encodeSixelNow(this.#base64Data, widthPx, heightPx);
|
|
816
|
+
this.#sixel = { widthPx, heightPx, sequence };
|
|
817
|
+
return sequence;
|
|
818
|
+
}
|
|
819
|
+
if (sameSize) return undefined;
|
|
820
|
+
const request: { widthPx: number; heightPx: number; sequence?: string | null } = { widthPx, heightPx };
|
|
821
|
+
this.#sixel = request;
|
|
822
|
+
const settle = (sequence: string | null): void => {
|
|
823
|
+
request.sequence = sequence;
|
|
824
|
+
if (this.#sixel !== request) return;
|
|
825
|
+
this.invalidate();
|
|
826
|
+
if (this.#options.requestRender) this.#options.requestRender();
|
|
827
|
+
else this.#budget?.requestRender();
|
|
828
|
+
};
|
|
829
|
+
encodeSixelAsync(new Uint8Array(Buffer.from(this.#base64Data, "base64")), widthPx, heightPx).then(settle, () =>
|
|
830
|
+
settle(null),
|
|
831
|
+
);
|
|
832
|
+
return undefined;
|
|
833
|
+
}
|
|
834
|
+
|
|
771
835
|
/**
|
|
772
836
|
* A native `image` backed by a content-addressed blob; the terminal fits
|
|
773
837
|
* it. Cell caps become `ch`/`lines` bounds. The inline-image budget and
|
|
@@ -820,7 +884,9 @@ export class Image implements Component {
|
|
|
820
884
|
this.#cachedCellWidthPx === cellDimensions.widthPx &&
|
|
821
885
|
this.#cachedCellHeightPx === cellDimensions.heightPx &&
|
|
822
886
|
this.#cachedKittyUnicodePlaceholders === kittyUnicodePlaceholders &&
|
|
823
|
-
(!transmitsSeparately || this.#budget?.shouldTransmit(imageId) !== true)
|
|
887
|
+
(!transmitsSeparately || this.#budget?.shouldTransmit(imageId) !== true) &&
|
|
888
|
+
// Cached rows reserved for a pending SIXEL encode must not reach scrollback.
|
|
889
|
+
!(scrollbackRenderDepth > 0 && this.#sixel !== undefined && this.#sixel.sequence === undefined)
|
|
824
890
|
) {
|
|
825
891
|
return this.#cachedLines;
|
|
826
892
|
}
|
|
@@ -839,6 +905,7 @@ export class Image implements Component {
|
|
|
839
905
|
maxHeightCells: this.#options.maxHeightCells,
|
|
840
906
|
imageId: this.#imageId,
|
|
841
907
|
includeTransmit: needsTransmit,
|
|
908
|
+
sixel: (widthPx, heightPx) => this.#sixelSequence(widthPx, heightPx),
|
|
842
909
|
});
|
|
843
910
|
|
|
844
911
|
if (result?.transmit && this.#imageId != null && this.#budget !== undefined) {
|
package/src/components/input.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BracketedPasteHandler, decodeReencodedPasteControls } from "../bracketed-paste";
|
|
2
|
-
import { getKeybindings } from "../keybindings";
|
|
3
|
-
import { extractPrintableText,
|
|
2
|
+
import { canonicalKeyId, getKeybindings } from "../keybindings";
|
|
3
|
+
import { extractPrintableText, parseKey } from "../keys";
|
|
4
4
|
import { KillRing } from "../kill-ring";
|
|
5
5
|
import type { TspInputProps } from "@oh-my-pi/pi-wire";
|
|
6
6
|
import { node } from "../native/describe";
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type NativeUiEvent,
|
|
15
15
|
resolveTextEdit,
|
|
16
16
|
} from "../native/node";
|
|
17
|
-
import { SpaceHoldGesture } from "../space-hold";
|
|
17
|
+
import { getSpaceHoldText, SpaceHoldGesture } from "../space-hold";
|
|
18
18
|
import { type Component, CURSOR_MARKER, type Focusable } from "../tui";
|
|
19
19
|
import { cursorColumnWindow } from "./scroll-viewport";
|
|
20
20
|
import {
|
|
@@ -134,6 +134,10 @@ export class Input implements Component, Focusable {
|
|
|
134
134
|
return this.#useTerminalCursor;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
capturesInput(data: string): boolean {
|
|
138
|
+
return this.spaceHold.shouldRoute(data);
|
|
139
|
+
}
|
|
140
|
+
|
|
137
141
|
/**
|
|
138
142
|
* Apply one key: the editor's text bindings (motion, deletion, kill ring,
|
|
139
143
|
* undo), pastes and printable text. Returns whether the key was the
|
|
@@ -154,10 +158,14 @@ export class Input implements Component, Focusable {
|
|
|
154
158
|
return true;
|
|
155
159
|
}
|
|
156
160
|
|
|
157
|
-
//
|
|
158
|
-
|
|
161
|
+
// Reserve configured keys before text bindings or submit/delete handlers can consume them.
|
|
162
|
+
const parsedKey = parseKey(data);
|
|
163
|
+
const canonical = parsedKey !== undefined ? canonicalKeyId(parsedKey) : undefined;
|
|
164
|
+
const spaceHoldText = getSpaceHoldText(data, canonical);
|
|
165
|
+
switch (this.spaceHold.process(canonical, spaceHoldText?.length ?? 0)) {
|
|
159
166
|
case "type":
|
|
160
|
-
this.#insertCharacter(
|
|
167
|
+
this.#insertCharacter(spaceHoldText!);
|
|
168
|
+
this.spaceHold.recordTyped(spaceHoldText!.length);
|
|
161
169
|
return true;
|
|
162
170
|
case "swallow":
|
|
163
171
|
return true;
|
package/src/components/loader.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { TspProps } from "@oh-my-pi/pi-wire";
|
|
2
|
+
import { formatTooltipKey } from "../key-hint-format";
|
|
3
|
+
import type { KeyId } from "../keys";
|
|
2
4
|
import { elapsed, kbd, keyed, node, row, span, text } from "../native/describe";
|
|
3
5
|
import { plainText } from "../native/spans";
|
|
4
6
|
import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
@@ -38,22 +40,15 @@ export interface WorkingRowSpec {
|
|
|
38
40
|
readonly variant?: WorkingRowVariant;
|
|
39
41
|
/** Shimmer palette of the label (a session-accented intent). */
|
|
40
42
|
readonly palette?: ShimmerPalette;
|
|
41
|
-
/** Live tok/s, docked before the stop control. */
|
|
42
|
-
readonly rate?: number;
|
|
43
43
|
/** Key id that interrupts (`escape`); undefined hides the stop control (Esc would not cancel). */
|
|
44
|
-
readonly interruptKey?:
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** A key id as tooltip keys: `escape` reads `esc`, the rest as bound. */
|
|
48
|
-
function titleKey(key: string): string {
|
|
49
|
-
return key === "escape" ? "esc" : key;
|
|
44
|
+
readonly interruptKey?: KeyId;
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
/**
|
|
53
48
|
* The dock's working row: `starburst` spinner (or a retry countdown ring),
|
|
54
|
-
* the
|
|
55
|
-
* control whose click sends `interrupt`. Compaction adds an
|
|
56
|
-
* `progress` before the stop control; a retry says "Cancel".
|
|
49
|
+
* the elapsed time, the `·` divider, the intent shimmering, a grow spacer,
|
|
50
|
+
* then the stop control whose click sends `interrupt`. Compaction adds an
|
|
51
|
+
* indeterminate `progress` before the stop control; a retry says "Cancel".
|
|
57
52
|
*/
|
|
58
53
|
export function describeWorkingRow(spec: WorkingRowSpec, cx: DescribeContext, now = Date.now()): NativeNode {
|
|
59
54
|
const variant = spec.variant;
|
|
@@ -67,13 +62,12 @@ export function describeWorkingRow(spec: WorkingRowSpec, cx: DescribeContext, no
|
|
|
67
62
|
}
|
|
68
63
|
const label = describeShimmer([{ text: spec.label, palette: spec.palette }], "label");
|
|
69
64
|
children.push(
|
|
70
|
-
node(label.k, { ...label.p, role: "omp.working.label" } as TspProps, label.c, label.key),
|
|
71
|
-
node("text", { text: "·", role: "omp.working.sep" }, undefined, "sep"),
|
|
72
65
|
keyed(elapsed(now - spec.startedAt), "elapsed"),
|
|
66
|
+
node("text", { text: "·", role: "omp.working.sep" }, undefined, "sep"),
|
|
67
|
+
node(label.k, { ...label.p, role: "omp.working.label" } as TspProps, label.c, label.key),
|
|
73
68
|
node("row", { grow: 1 }, undefined, "fill"),
|
|
74
69
|
);
|
|
75
70
|
if (variant?.kind === "compaction") children.push(node("progress", { value: null }, undefined, "progress"));
|
|
76
|
-
if (spec.rate !== undefined) children.push(node("rate", { value: spec.rate, unit: "tok/s" }, undefined, "rate"));
|
|
77
71
|
if (spec.interruptKey !== undefined) {
|
|
78
72
|
const verb = variant?.kind === "retry" ? "Cancel" : "Stop";
|
|
79
73
|
children.push(
|
|
@@ -83,10 +77,10 @@ export function describeWorkingRow(spec: WorkingRowSpec, cx: DescribeContext, no
|
|
|
83
77
|
role: "omp.working.stop",
|
|
84
78
|
gap: "xs",
|
|
85
79
|
align: "center",
|
|
86
|
-
title: `${verb} ${
|
|
80
|
+
title: `${verb} ${formatTooltipKey(spec.interruptKey)}`,
|
|
87
81
|
actions: { click: "interrupt" },
|
|
88
82
|
},
|
|
89
|
-
[kbd(
|
|
83
|
+
[kbd(spec.interruptKey), text(verb)],
|
|
90
84
|
"stop",
|
|
91
85
|
),
|
|
92
86
|
);
|
|
@@ -285,7 +279,6 @@ export class Loader extends Text {
|
|
|
285
279
|
variant?.kind,
|
|
286
280
|
variant?.kind === "retry" ? `${variant.attempt}/${variant.max}` : "",
|
|
287
281
|
countdown,
|
|
288
|
-
spec.rate,
|
|
289
282
|
spec.interruptKey,
|
|
290
283
|
spec.palette?.mid,
|
|
291
284
|
shimmerEnabled(),
|
package/src/hotkeys-markdown.ts
CHANGED
|
@@ -62,6 +62,7 @@ function hotkeyGroups(bindings: HotkeysMarkdownBindings): HotkeyGroup[] {
|
|
|
62
62
|
if (keys.length > 0) return { keys: keys.map(formatKeyHint), ids: keys, action };
|
|
63
63
|
return { keys: [bindings.keybindings.getDisplayString(action) || "Disabled"], ids: [], action };
|
|
64
64
|
};
|
|
65
|
+
const pushToTalkKeys = bindings.keybindings.getKeys("app.stt.pushToTalk");
|
|
65
66
|
// CustomEditor tests the chord that was actually pressed, so exit keys split by role: a key
|
|
66
67
|
// that also carries tui.editor.deleteCharForward (the readline `^D` overlap) forward-deletes
|
|
67
68
|
// while the prompt holds a draft, any other exit key quits immediately. Mixed bindings such as
|
|
@@ -139,8 +140,14 @@ function hotkeyGroups(bindings: HotkeysMarkdownBindings): HotkeyGroup[] {
|
|
|
139
140
|
{ keys: [act("app.retry")], action: "Retry last failed assistant turn" },
|
|
140
141
|
{ keys: [act("app.clipboard.pasteImage")], action: "Paste image or text from clipboard" },
|
|
141
142
|
{
|
|
142
|
-
keys:
|
|
143
|
-
|
|
143
|
+
keys:
|
|
144
|
+
pushToTalkKeys.length > 0
|
|
145
|
+
? [{ text: "Hold " }, act("app.stt.pushToTalk")]
|
|
146
|
+
: [act("app.stt.pushToTalk")],
|
|
147
|
+
action:
|
|
148
|
+
pushToTalkKeys.length > 0
|
|
149
|
+
? "Speech-to-text (push-to-talk): hold to record, release to transcribe"
|
|
150
|
+
: "Speech-to-text (push-to-talk) (disabled)",
|
|
144
151
|
},
|
|
145
152
|
{ keys: [act("app.live.toggle")], action: "Start/stop live voice mode (/live)" },
|
|
146
153
|
{
|
package/src/key-hint-format.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { KeyId } from "./keybindings";
|
|
8
8
|
import type { ModifierName } from "./keys";
|
|
9
9
|
import { activeThemeSymbol } from "./theme/active-symbols";
|
|
10
|
-
import type
|
|
10
|
+
import { SYMBOL_PRESETS, type SymbolKey } from "./theme/symbols";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Key hint formatting: every key shown to the user renders through
|
|
@@ -69,9 +69,8 @@ const KEY_WORDS: Record<string, string | undefined> = {
|
|
|
69
69
|
insert: "Ins",
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
72
|
+
/** Where a key's glyphs come from: the active theme, or a fixed preset. */
|
|
73
|
+
type KeySymbols = (key: SymbolKey) => string;
|
|
75
74
|
|
|
76
75
|
const HAS_LETTER = /[A-Za-z0-9]/;
|
|
77
76
|
|
|
@@ -89,6 +88,22 @@ export type KeyName = KeyId | ModifierName;
|
|
|
89
88
|
* is capitalized. Other keys are capitalized (`f5` → `F5`).
|
|
90
89
|
*/
|
|
91
90
|
export function formatKeyHint(key: KeyName): string {
|
|
91
|
+
return formatKey(key, activeThemeSymbol);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Format one key for a native tooltip (a TSP `title`): unicode keycap glyphs
|
|
96
|
+
* whatever the theme's symbol preset (`shift+tab` → `⇧⇥`), which Tern's
|
|
97
|
+
* tooltip splits into keycaps, and `esc` as Tern's own keycaps read. The nerd
|
|
98
|
+
* preset's icons are missing from Tern's UI font, and its spaced chords would
|
|
99
|
+
* not parse as keys.
|
|
100
|
+
*/
|
|
101
|
+
export function formatTooltipKey(key: KeyName): string {
|
|
102
|
+
return key === "escape" ? "esc" : formatKey(key, symbol => SYMBOL_PRESETS.unicode[symbol]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** {@link formatKeyHint} with glyphs from `symbol`. */
|
|
106
|
+
function formatKey(key: KeyName, symbol: KeySymbols): string {
|
|
92
107
|
const mac = keyHintPlatform() === "darwin";
|
|
93
108
|
// A trailing `+` is the plus key itself (`+`, `ctrl++`), not a separator.
|
|
94
109
|
const parts = key.endsWith("+") ? [...key.slice(0, -1).split("+").slice(0, -1), "+"] : key.split("+");
|
|
@@ -96,14 +111,14 @@ export function formatKeyHint(key: KeyName): string {
|
|
|
96
111
|
let out = "";
|
|
97
112
|
for (const modifier in MODIFIER_SYMBOLS) {
|
|
98
113
|
if (!parts.includes(modifier)) continue;
|
|
99
|
-
const label =
|
|
100
|
-
out += HAS_LETTER.test(label) ? `${label}+` : label +
|
|
114
|
+
const label = symbol(MODIFIER_SYMBOLS[modifier]![mac ? 1 : 0]);
|
|
115
|
+
out += HAS_LETTER.test(label) ? `${label}+` : label + symbol("key.joiner");
|
|
101
116
|
}
|
|
102
117
|
const lower = base.toLowerCase();
|
|
103
118
|
const modifier = MODIFIER_SYMBOLS[lower];
|
|
104
|
-
const
|
|
105
|
-
if (modifier) out +=
|
|
106
|
-
else if (
|
|
119
|
+
const glyph = KEY_SYMBOLS[lower];
|
|
120
|
+
if (modifier) out += symbol(modifier[mac ? 1 : 0]);
|
|
121
|
+
else if (glyph !== undefined) out += symbol(glyph);
|
|
107
122
|
else if (KEY_WORDS[lower] !== undefined) out += KEY_WORDS[lower];
|
|
108
123
|
else if (base.length === 1) out += parts.length === 0 ? base : base.toUpperCase();
|
|
109
124
|
else out += base[0]!.toUpperCase() + base.slice(1);
|
|
@@ -118,5 +133,5 @@ export function formatKeyHints(keys: KeyName | readonly KeyName[]): string {
|
|
|
118
133
|
/** Format a double-tap gesture: `←←` with glyphs, `Left Left` with words. */
|
|
119
134
|
export function formatDoubleTap(key: KeyName): string {
|
|
120
135
|
const label = formatKeyHint(key);
|
|
121
|
-
return `${label}${HAS_LETTER.test(label) ? " " :
|
|
136
|
+
return `${label}${HAS_LETTER.test(label) ? " " : activeThemeSymbol("key.joiner")}${label}`;
|
|
122
137
|
}
|
package/src/native/apply.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reference document applier: applies TSP frames to a node tree the way the
|
|
3
|
-
* spec says the terminal does (
|
|
4
|
-
* one; a rejected op is reported and
|
|
5
|
-
* the frame. `settle` is recorded as a
|
|
3
|
+
* spec says the terminal does (Tern SDK, `protocol/documents.md`, frame ops).
|
|
4
|
+
* Ops are validated and applied one by one; a rejected op is reported and
|
|
5
|
+
* skipped without affecting the rest of the frame. `settle` is recorded as a
|
|
6
|
+
* hint and never restricts later ops.
|
|
6
7
|
*
|
|
7
8
|
* Used by tests, the debug server's `doc` op, and the TSP test harness.
|
|
8
9
|
*/
|
package/src/native/backend.ts
CHANGED
|
@@ -120,6 +120,7 @@ class NativeContext implements DescribeContext {
|
|
|
120
120
|
cols: number;
|
|
121
121
|
reduceMotion: boolean;
|
|
122
122
|
dark: boolean;
|
|
123
|
+
hour12: boolean | undefined;
|
|
123
124
|
#kinds: ReadonlySet<string>;
|
|
124
125
|
#features: ReadonlySet<string>;
|
|
125
126
|
|
|
@@ -127,6 +128,7 @@ class NativeContext implements DescribeContext {
|
|
|
127
128
|
this.cols = cols;
|
|
128
129
|
this.reduceMotion = hello.reduceMotion === true;
|
|
129
130
|
this.dark = hello.dark !== false;
|
|
131
|
+
this.hour12 = hello.hour12;
|
|
130
132
|
this.#kinds = new Set(hello.kinds);
|
|
131
133
|
this.#features = new Set(hello.features);
|
|
132
134
|
}
|
|
@@ -398,8 +400,8 @@ export class NativeBackend {
|
|
|
398
400
|
* The terminal's real `hello` reply after an optimistic start: adopt its
|
|
399
401
|
* APC limit, credits, cell size, kinds, appearance and motion preference.
|
|
400
402
|
* A width the terminal already reported in a `resize` event wins over the
|
|
401
|
-
* reply's. A different vocabulary
|
|
402
|
-
* component, so kinds the terminal lacks fall back.
|
|
403
|
+
* reply's. A different vocabulary, motion preference or clock re-describes
|
|
404
|
+
* every component, so kinds the terminal lacks fall back.
|
|
403
405
|
*/
|
|
404
406
|
confirm(hello: TspHello): void {
|
|
405
407
|
const before = this.#cx;
|
|
@@ -411,7 +413,12 @@ export class NativeBackend {
|
|
|
411
413
|
this.#sawResize = true;
|
|
412
414
|
}
|
|
413
415
|
if (this.#cx.dark !== before.dark) this.#host.appearanceChanged(this.#cx.dark);
|
|
414
|
-
if (
|
|
416
|
+
if (
|
|
417
|
+
this.#cx.reduceMotion !== before.reduceMotion ||
|
|
418
|
+
this.#cx.hour12 !== before.hour12 ||
|
|
419
|
+
!this.#cx.sameVocabulary(before)
|
|
420
|
+
)
|
|
421
|
+
this.#host.invalidate();
|
|
415
422
|
this.#host.requestRender();
|
|
416
423
|
}
|
|
417
424
|
|
package/src/native/encode.ts
CHANGED
|
@@ -108,9 +108,10 @@ export function encodeTspJson(verb: TspVerb, value: unknown, params?: TspParams,
|
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* The `hello` query; callers follow it with a DA1 sentinel. `features: ["edit"]`
|
|
111
|
-
* tells the terminal that omp applies its `edit` events (
|
|
112
|
-
*
|
|
113
|
-
* says omp applies `undo` events,
|
|
111
|
+
* tells the terminal that omp applies its `edit` events (Tern SDK,
|
|
112
|
+
* `protocol/input.md`), so it may keep a native selection in omp's editors;
|
|
113
|
+
* without it, every key stays omp's. `"undo"` says omp applies `undo` events,
|
|
114
|
+
* so the terminal may turn ⌃Z in a field into one.
|
|
114
115
|
* `"send"` accepts an explicit prompt for a live composer without simulating keys.
|
|
115
116
|
*/
|
|
116
117
|
export function encodeTspHelloQuery(version?: string): string {
|
package/src/native/node.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* `rows` fallback node rendered through `render(cx.cols)`.
|
|
12
12
|
*
|
|
13
13
|
* See `packages/wire/src/tsp.ts` for the wire vocabulary and
|
|
14
|
-
* `
|
|
14
|
+
* the Tern SDK's Surface Protocol reference (`docs/sdk/src/protocol` in the
|
|
15
|
+
* Stencil repository, https://docs.stencil.so/tern/protocol/) for the spec.
|
|
15
16
|
*/
|
|
16
17
|
import type { TspEvent, TspKind, TspProps, TspScrollBy, TspSpan } from "@oh-my-pi/pi-wire";
|
|
17
18
|
import type { Component } from "../tui";
|
|
@@ -76,6 +77,12 @@ export interface DescribeContext {
|
|
|
76
77
|
readonly reduceMotion: boolean;
|
|
77
78
|
/** The terminal's appearance is dark. */
|
|
78
79
|
readonly dark: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* The user's system reads a 12-hour clock (`false`: 24-hour); `undefined`
|
|
82
|
+
* when the terminal doesn't say. Pass it as `hour12` when formatting times:
|
|
83
|
+
* the process's own default locale may not know.
|
|
84
|
+
*/
|
|
85
|
+
readonly hour12?: boolean;
|
|
79
86
|
/** Whether the terminal renders `kind` natively (else describe something simpler or let it fall back). */
|
|
80
87
|
supports(kind: TspKind): boolean;
|
|
81
88
|
/**
|
package/src/native/overlay.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* component's root is a `card` whose `head` carries the title.
|
|
8
8
|
*/
|
|
9
9
|
import type { TspProps, TspSpan, TspText } from "@oh-my-pi/pi-wire";
|
|
10
|
-
import type
|
|
10
|
+
import { formatTooltipKey, type KeyName } from "../key-hint-format";
|
|
11
11
|
import { getKeybindings, type Keybinding } from "../keybindings";
|
|
12
12
|
import { card, kbd, list, node, row, span, text } from "./describe";
|
|
13
13
|
import type { NativeChild, NativeNode } from "./node";
|
|
@@ -86,10 +86,10 @@ export interface ActionButtonOptions {
|
|
|
86
86
|
/** Sibling identity key (defaults to the action id); `leafKey(event.key)` reports it back. */
|
|
87
87
|
readonly key?: string;
|
|
88
88
|
/** Key id of the keyboard path the button mirrors, drawn as a keycap inside it (`"r"`, `"ctrl+g"`). */
|
|
89
|
-
readonly keys?:
|
|
89
|
+
readonly keys?: KeyName;
|
|
90
90
|
/** `accent` = the view's hero action, `error` = destructive. */
|
|
91
91
|
readonly tone?: "accent" | "error";
|
|
92
|
-
/** Tooltip; defaults to the label plus the keycap (`"Refresh r"
|
|
92
|
+
/** Tooltip; defaults to the label plus the keycap (`"Refresh r"`, see {@link formatTooltipKey}). */
|
|
93
93
|
readonly title?: string;
|
|
94
94
|
/** Target of an `open` action (URL or `file://` path). */
|
|
95
95
|
readonly href?: string;
|
|
@@ -110,7 +110,7 @@ export function actionButton(label: string, act: string, options: ActionButtonOp
|
|
|
110
110
|
gap: "xs",
|
|
111
111
|
align: "center",
|
|
112
112
|
actions: { click: act },
|
|
113
|
-
title: options.title ?? (options.keys ? `${label} ${options.keys}` : label),
|
|
113
|
+
title: options.title ?? (options.keys ? `${label} ${formatTooltipKey(options.keys)}` : label),
|
|
114
114
|
...(options.tone ? { tone: options.tone } : {}),
|
|
115
115
|
...(options.href ? { href: options.href } : {}),
|
|
116
116
|
},
|
package/src/native/settle.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Settling: the frame provider marks finalized transcript blocks so the
|
|
3
|
-
* native reconciler stops keeping their description (
|
|
4
|
-
*
|
|
5
|
-
* targeted ops by id, followed by a fresh `settle`
|
|
3
|
+
* native reconciler stops keeping their description (Tern SDK,
|
|
4
|
+
* `protocol/operations.md`, "Settling"). A settled block stays editable: a
|
|
5
|
+
* later change is sent as targeted ops by id, followed by a fresh `settle`
|
|
6
|
+
* hint.
|
|
6
7
|
*/
|
|
7
8
|
import type { Component } from "../tui";
|
|
8
9
|
|
|
@@ -300,6 +300,7 @@ export interface AgentHubDeps<TRecord extends AgentRecordLike = AgentRecordLike>
|
|
|
300
300
|
/** Mirrors the main transcript's thinking-block visibility. */
|
|
301
301
|
hideThinkingBlock?: () => boolean;
|
|
302
302
|
proseOnlyThinking?: () => boolean;
|
|
303
|
+
expandThinkingBlocks?: () => boolean;
|
|
303
304
|
/** Keys toggling tool output expansion (app.tools.expand). */
|
|
304
305
|
expandKeys?: KeyId[];
|
|
305
306
|
/** Focus the main view on this agent's live session (ctx.focusAgentSession). When absent (collab guest, tests), Enter opens the in-hub chat view instead. */
|
|
@@ -445,6 +446,7 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
|
|
|
445
446
|
#cwd: string;
|
|
446
447
|
#hideThinkingBlock: (() => boolean) | undefined;
|
|
447
448
|
#proseOnlyThinking: (() => boolean) | undefined;
|
|
449
|
+
#expandThinkingBlocks: (() => boolean) | undefined;
|
|
448
450
|
#expandKeys: KeyId[];
|
|
449
451
|
#focusAgent: ((id: string) => Promise<void>) | undefined;
|
|
450
452
|
|
|
@@ -490,6 +492,7 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
|
|
|
490
492
|
this.#cwd = deps.cwd ?? getProjectDir();
|
|
491
493
|
this.#hideThinkingBlock = deps.hideThinkingBlock;
|
|
492
494
|
this.#proseOnlyThinking = deps.proseOnlyThinking;
|
|
495
|
+
this.#expandThinkingBlocks = deps.expandThinkingBlocks;
|
|
493
496
|
this.#expandKeys = deps.expandKeys ?? ["ctrl+o"];
|
|
494
497
|
this.#focusAgent = deps.focusAgent;
|
|
495
498
|
|
|
@@ -639,6 +642,7 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
|
|
|
639
642
|
cwd: this.#cwd,
|
|
640
643
|
hideThinkingBlock: this.#hideThinkingBlock,
|
|
641
644
|
proseOnlyThinking: this.#proseOnlyThinking,
|
|
645
|
+
expandThinkingBlocks: this.#expandThinkingBlocks,
|
|
642
646
|
expandKeys: this.#expandKeys,
|
|
643
647
|
hubKeys: this.#hubKeys,
|
|
644
648
|
requestRender: this.#requestRender,
|
|
@@ -73,6 +73,7 @@ export interface AgentTranscriptViewerDeps {
|
|
|
73
73
|
cwd: string;
|
|
74
74
|
hideThinkingBlock?: () => boolean;
|
|
75
75
|
proseOnlyThinking?: () => boolean;
|
|
76
|
+
expandThinkingBlocks?: () => boolean;
|
|
76
77
|
expandKeys: KeyId[];
|
|
77
78
|
/** Keys that toggle the whole hub closed (app.agents.hub + app.session.observe). */
|
|
78
79
|
hubKeys: KeyId[];
|
|
@@ -199,6 +200,7 @@ export class AgentTranscriptViewer implements Component {
|
|
|
199
200
|
cwd: deps.cwd,
|
|
200
201
|
hideThinkingBlock: deps.hideThinkingBlock,
|
|
201
202
|
proseOnlyThinking: deps.proseOnlyThinking,
|
|
203
|
+
expandThinkingBlocks: deps.expandThinkingBlocks,
|
|
202
204
|
requestRender: deps.requestRender,
|
|
203
205
|
});
|
|
204
206
|
this.#browser = new TranscriptBrowser({
|