@oh-my-pi/pi-tui 18.14.1 → 18.14.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 +24 -0
- package/dist/types/charts/table-data.d.ts +4 -2
- package/dist/types/chat/transcript-render-helpers.d.ts +3 -3
- package/dist/types/native/describe.d.ts +4 -2
- package/dist/types/native/overlay.d.ts +42 -2
- package/dist/types/native/state.d.ts +4 -0
- package/dist/types/prompt/composer.d.ts +13 -5
- package/dist/types/render/render-utils.d.ts +2 -0
- package/dist/types/theme/theme.d.ts +1 -1
- package/dist/types/theme/tui-adapters.d.ts +2 -0
- package/dist/types/thinking.d.ts +8 -6
- package/dist/types/tools/ast-grep.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +2 -0
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/grouped-file-output.d.ts +24 -11
- package/dist/types/tools/read.d.ts +4 -4
- package/dist/types/tools/task.d.ts +2 -2
- package/package.json +9 -9
- package/src/apps/git/git-tui.ts +10 -17
- package/src/apps/git/sidebar.ts +21 -35
- package/src/apps/ps-top.ts +4 -12
- package/src/charts/chart-plan.ts +0 -29
- package/src/charts/table-data.ts +55 -6
- package/src/chat/transcript-render-helpers.ts +18 -16
- package/src/native/backend.ts +7 -3
- package/src/native/describe.ts +9 -4
- package/src/native/overlay.ts +124 -16
- package/src/native/state.ts +25 -5
- package/src/overlays/extensions/extension-dashboard.ts +7 -8
- package/src/overlays/login-dialog.ts +8 -26
- package/src/overlays/tree-selector.ts +64 -18
- package/src/overlays/usage-dashboard.ts +2 -4
- package/src/prompt/composer.ts +17 -5
- package/src/render/render-utils.ts +2 -0
- package/src/theme/theme-class.ts +12 -3
- package/src/theme/theme.ts +1 -0
- package/src/theme/tui-adapters.ts +8 -3
- package/src/thinking.ts +17 -11
- package/src/tools/ast-grep.ts +10 -2
- package/src/tools/eval.ts +2 -0
- package/src/tools/find.ts +7 -2
- package/src/tools/grep.ts +8 -2
- package/src/tools/grouped-file-output.ts +172 -47
- package/src/tools/lsp.ts +158 -8
- package/src/tools/read.ts +6 -5
- package/src/tools/task.ts +2 -2
package/src/apps/ps-top.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { type Component, TUI } from "../tui";
|
|
|
17
17
|
import type { TspSpan, TspText, TspTone } from "@oh-my-pi/pi-wire";
|
|
18
18
|
import { col, compact, elapsed, keyed, node, row, span, stableKey, text } from "../native/describe";
|
|
19
19
|
import type { NativeNode, NativeUiEvent } from "../native/node";
|
|
20
|
-
import { actionBar, actionButton } from "../native/overlay";
|
|
20
|
+
import { actionBar, actionButton, iconButton, segmentedTabs } from "../native/overlay";
|
|
21
21
|
import { Memo } from "../native/memo";
|
|
22
22
|
import { truncateToWidth } from "../utils";
|
|
23
23
|
import { formatDuration } from "@oh-my-pi/pi-utils";
|
|
@@ -410,13 +410,7 @@ export class PsTopComponent implements Component {
|
|
|
410
410
|
);
|
|
411
411
|
} else {
|
|
412
412
|
left.push(
|
|
413
|
-
|
|
414
|
-
name: "back",
|
|
415
|
-
role: "omp.app.ibtn",
|
|
416
|
-
title: "Back esc",
|
|
417
|
-
aria: "Back",
|
|
418
|
-
actions: { click: "back" },
|
|
419
|
-
}),
|
|
413
|
+
iconButton("back", "back", "Back esc", { aria: "Back" }),
|
|
420
414
|
text(
|
|
421
415
|
this.#view === "logs" ? `Logs · ${entry?.row.snapshot.name ?? "?"}` : (entry?.row.snapshot.name ?? "?"),
|
|
422
416
|
{
|
|
@@ -432,8 +426,7 @@ export class PsTopComponent implements Component {
|
|
|
432
426
|
const right: NativeNode[] = [];
|
|
433
427
|
if (this.#view === "table") {
|
|
434
428
|
right.push(
|
|
435
|
-
|
|
436
|
-
"tabs",
|
|
429
|
+
segmentedTabs(
|
|
437
430
|
{
|
|
438
431
|
items: [
|
|
439
432
|
{
|
|
@@ -448,7 +441,6 @@ export class PsTopComponent implements Component {
|
|
|
448
441
|
active: this.#all ? "all" : "current",
|
|
449
442
|
role: "omp.app.seg",
|
|
450
443
|
},
|
|
451
|
-
undefined,
|
|
452
444
|
"scope",
|
|
453
445
|
),
|
|
454
446
|
);
|
|
@@ -468,7 +460,7 @@ export class PsTopComponent implements Component {
|
|
|
468
460
|
align: "center",
|
|
469
461
|
role: "omp.app.fresh",
|
|
470
462
|
}),
|
|
471
|
-
|
|
463
|
+
iconButton("x", "quit", "Quit q", { aria: "Quit" }),
|
|
472
464
|
);
|
|
473
465
|
return keyed(
|
|
474
466
|
row(
|
package/src/charts/chart-plan.ts
CHANGED
|
@@ -1523,10 +1523,6 @@ export function buildChart(table: TableAnalysis, plan: ChartPlan): ChartSpec | u
|
|
|
1523
1523
|
// its rows are in different units.
|
|
1524
1524
|
if (arrows.length === 1 && plan.kind !== "paired" && plan.kind !== "range" && !plan.transpose)
|
|
1525
1525
|
return buildChart(table, { ...plan, kind: arrows[0]!.mixed ? "change" : "paired", series: [arrows[0]!.index] });
|
|
1526
|
-
// A dumbbell of two measures beside their ratio (`pi-walker | Go | Speedup`): the ratio restates the gap the
|
|
1527
|
-
// dumbbell draws (on a log axis its length), so the pair draws alone.
|
|
1528
|
-
const pair = plan.kind === "paired" && !plan.transpose ? ratioPair(table, columns) : undefined;
|
|
1529
|
-
if (pair) return buildChart(table, { ...plan, series: pair.map(column => column.index) });
|
|
1530
1526
|
// Unitless before/after values spanning decades are rows of different quantities, and rows of very different
|
|
1531
1527
|
// sizes beside a baseline column share no readable axis; their factors do (`3.95M → 190k` is 21× less).
|
|
1532
1528
|
// Intervals keep their values: a range chart prints the factor between a pair's midpoints beside both.
|
|
@@ -2810,31 +2806,6 @@ function againstBaseline(
|
|
|
2810
2806
|
return [baseline, ...columns.filter(column => column !== baseline)];
|
|
2811
2807
|
}
|
|
2812
2808
|
|
|
2813
|
-
/** Relative slack within which a ratio column restates the ratio of two others (`4.1×` for 75.3 / 18.2). */
|
|
2814
|
-
const RATIO_SLACK = 0.05;
|
|
2815
|
-
|
|
2816
|
-
/**
|
|
2817
|
-
* Of three plotted columns, the two same-unit measures when the third is their ratio (`Speedup`
|
|
2818
|
-
* `4.1×`, a `ratio` column) matching `a ÷ b` or `b ÷ a` within {@link RATIO_SLACK}
|
|
2819
|
-
* in every row holding all three, two rows at least; else `undefined`.
|
|
2820
|
-
*/
|
|
2821
|
-
function ratioPair(table: TableAnalysis, columns: readonly TableColumn[]): [TableColumn, TableColumn] | undefined {
|
|
2822
|
-
if (columns.length !== 3) return undefined;
|
|
2823
|
-
const ratio = columns.find(column => column.dim === "ratio");
|
|
2824
|
-
const rest = columns.filter(column => column !== ratio);
|
|
2825
|
-
const [a, b] = rest;
|
|
2826
|
-
if (!ratio || !a || !b || a.dim !== b.dim || a.unit !== b.unit || a.dim === "ratio") return undefined;
|
|
2827
|
-
const near = (value: number, target: number) => Math.abs(value - target) <= RATIO_SLACK * target;
|
|
2828
|
-
let matched = 0;
|
|
2829
|
-
for (const row of table.rows) {
|
|
2830
|
-
const [x, y, r] = [a.cells[row], b.cells[row], ratio.cells[row]];
|
|
2831
|
-
if (!isNumber(x) || !isNumber(y) || !isNumber(r) || x.value <= 0 || y.value <= 0) continue;
|
|
2832
|
-
if (!near(r.value, x.value / y.value) && !near(r.value, y.value / x.value)) return undefined;
|
|
2833
|
-
matched++;
|
|
2834
|
-
}
|
|
2835
|
-
return matched >= 2 ? [a, b] : undefined;
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
2809
|
/**
|
|
2839
2810
|
* Measure columns pairing a before condition with another over the same
|
|
2840
2811
|
* metric (`Baseline Turns` → `Iter1 Turns`, `Baseline Tokens In` → `Iter1
|
package/src/charts/table-data.ts
CHANGED
|
@@ -108,8 +108,10 @@ export interface TableColumn {
|
|
|
108
108
|
/**
|
|
109
109
|
* Index of the measure column this one restates, read from the values: a
|
|
110
110
|
* proportional copy (`pass` 33/38 beside `pass%` 87%, `Samples` beside `% of
|
|
111
|
-
* Total`, `Leverage vs $200` beside the cost)
|
|
112
|
-
* (`Offset` summing `Verts`)
|
|
111
|
+
* Total`, `Leverage vs $200` beside the cost), a running total of it
|
|
112
|
+
* (`Offset` summing `Verts`), or its ratio to another column of its unit
|
|
113
|
+
* (`ratio` 0.997 for `Python (ms)` ÷ `C++ (ms)`, `Speedup` 4.1×: the
|
|
114
|
+
* numerator's index). Such a column is never a measure of its own.
|
|
113
115
|
*/
|
|
114
116
|
readonly restates?: number;
|
|
115
117
|
}
|
|
@@ -906,10 +908,11 @@ function plainValue(column: TableColumn, row: number): NumberCell | undefined {
|
|
|
906
908
|
|
|
907
909
|
/**
|
|
908
910
|
* Columns restating another column's values: `y = k·x` within the written
|
|
909
|
-
* precision over three or more rows where `x` varies,
|
|
910
|
-
* (`Offset` stepping by `Verts`)
|
|
911
|
-
* stays, unless the second is a share
|
|
912
|
-
* table bolded more of its cells
|
|
911
|
+
* precision over three or more rows where `x` varies, a running total
|
|
912
|
+
* (`Offset` stepping by `Verts`), or the ratio of two others ({@link quotient}).
|
|
913
|
+
* Of a proportional pair the first column stays, unless the second is a share
|
|
914
|
+
* summing to 100% beside counts or the table bolded more of its cells
|
|
915
|
+
* (`**87%**` beside `33/38`).
|
|
913
916
|
*/
|
|
914
917
|
function restatedColumns(measures: readonly TableColumn[], rows: readonly number[]): Map<number, number> {
|
|
915
918
|
const restated = new Map<number, number>();
|
|
@@ -931,9 +934,55 @@ function restatedColumns(measures: readonly TableColumn[], rows: readonly number
|
|
|
931
934
|
const source = measures.find(other => other !== column && runningTotal(column, other, rows));
|
|
932
935
|
if (source) restated.set(column.index, source.index);
|
|
933
936
|
}
|
|
937
|
+
const kept = measures.filter(column => !restated.has(column.index));
|
|
938
|
+
for (const ratio of kept) {
|
|
939
|
+
if (restated.has(ratio.index)) continue;
|
|
940
|
+
const terms = kept.filter(column => column !== ratio && !restated.has(column.index));
|
|
941
|
+
const over = terms
|
|
942
|
+
.flatMap(a => terms.map(b => [a, b] as const))
|
|
943
|
+
.find(([a, b]) => a !== b && quotient(ratio, a, b, rows));
|
|
944
|
+
if (over) restated.set(ratio.index, over[0].index);
|
|
945
|
+
}
|
|
934
946
|
return restated;
|
|
935
947
|
}
|
|
936
948
|
|
|
949
|
+
/** Most relative rounding a row's quotient may carry to count toward proving it (`2×` beside its terms proves little). */
|
|
950
|
+
const LOOSE_QUOTIENT = 0.05;
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* `ratio = a ÷ b` in every row holding all three, `a` and `b` in one unit
|
|
954
|
+
* (`Python (ms)` ÷ `old C++ (ms)` = `0.997`; `Go` ÷ `pi-walker` = `4.1×`),
|
|
955
|
+
* within the precision each figure is written to, over three or more rows
|
|
956
|
+
* pinned to {@link LOOSE_QUOTIENT}. Percent columns are left alone: a part's
|
|
957
|
+
* share of a whole (`Share` = `Tokens` ÷ `Total`) is a measure the table states.
|
|
958
|
+
*
|
|
959
|
+
* Bare numbers all read as counts, and of three columns related so each is
|
|
960
|
+
* the others' quotient or product (`C++ = Python ÷ ratio` holds too), the
|
|
961
|
+
* ratio is the one that varies less from row to row than both its terms:
|
|
962
|
+
* workloads of different sizes move both timings, not their ratio. Where none
|
|
963
|
+
* does, none is taken for the ratio.
|
|
964
|
+
*/
|
|
965
|
+
function quotient(ratio: TableColumn, a: TableColumn, b: TableColumn, rows: readonly number[]): boolean {
|
|
966
|
+
if ((ratio.dim !== "count" && ratio.dim !== "ratio") || a.dim !== b.dim || a.unit !== b.unit) return false;
|
|
967
|
+
let tight = 0;
|
|
968
|
+
const matched: (readonly [number, number, number])[] = [];
|
|
969
|
+
for (const row of rows) {
|
|
970
|
+
const [r, x, y] = [plainValue(ratio, row), plainValue(a, row), plainValue(b, row)];
|
|
971
|
+
if (!r || !x || !y) continue;
|
|
972
|
+
if (x.value <= 0 || y.value <= 0 || r.value <= 0) return false;
|
|
973
|
+
const exact = x.value / y.value;
|
|
974
|
+
const slack = writtenSlack(r) + exact * (writtenSlack(x) / x.value + writtenSlack(y) / y.value) + 1e-9 * exact;
|
|
975
|
+
if (Math.abs(r.value - exact) > slack) return false;
|
|
976
|
+
if (slack <= LOOSE_QUOTIENT * exact) tight++;
|
|
977
|
+
matched.push([r.value, x.value, y.value]);
|
|
978
|
+
}
|
|
979
|
+
if (tight < 3) return false;
|
|
980
|
+
if (ratio.dim !== a.dim) return true;
|
|
981
|
+
const spread = (at: 0 | 1 | 2) =>
|
|
982
|
+
Math.max(...matched.map(values => values[at])) / Math.min(...matched.map(values => values[at]));
|
|
983
|
+
return spread(0) < spread(1) && spread(0) < spread(2);
|
|
984
|
+
}
|
|
985
|
+
|
|
937
986
|
/** How many of `rows` the table bolded in `column`. */
|
|
938
987
|
function emphasized(column: TableColumn, rows: readonly number[]): number {
|
|
939
988
|
return rows.filter(row => column.cells[row]?.emphasis).length;
|
|
@@ -137,9 +137,9 @@ export function buildLaunchCompletionBlock(message: CustomOrHookMessage): ToolAc
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
|
-
* Render
|
|
141
|
-
* `irc:relay`) as a
|
|
142
|
-
* expanded-state getter for the cached card.
|
|
140
|
+
* Render an IRC coordination custom message (`irc:incoming` / `irc:autoreply` /
|
|
141
|
+
* `irc:relay` / `irc:workpool`) as a tool-activity card. `getExpanded` supplies
|
|
142
|
+
* the live expanded-state getter for the cached card.
|
|
143
143
|
*/
|
|
144
144
|
export function buildIrcMessageCard(message: CustomOrHookMessage, getExpanded: () => boolean): Component {
|
|
145
145
|
const details = (
|
|
@@ -161,19 +161,21 @@ export function buildIrcMessageCard(message: CustomOrHookMessage, getExpanded: (
|
|
|
161
161
|
: message.customType === "irc:workpool"
|
|
162
162
|
? ("workpool" as const)
|
|
163
163
|
: ("relay" as const);
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
164
|
+
return new ToolActivityContainer(
|
|
165
|
+
createIrcMessageCard(
|
|
166
|
+
{
|
|
167
|
+
kind,
|
|
168
|
+
from: details?.from,
|
|
169
|
+
to: details?.to,
|
|
170
|
+
body: kind === "incoming" ? details?.message : details?.body,
|
|
171
|
+
replyTo: details?.replyTo,
|
|
172
|
+
timestamp: message.timestamp,
|
|
173
|
+
pool: details?.pool,
|
|
174
|
+
mode: details?.mode,
|
|
175
|
+
},
|
|
176
|
+
getExpanded,
|
|
177
|
+
theme,
|
|
178
|
+
),
|
|
177
179
|
);
|
|
178
180
|
}
|
|
179
181
|
|
package/src/native/backend.ts
CHANGED
|
@@ -48,7 +48,7 @@ import { encodeTspJson, encodeTspMessage, type TspHello, TspReader, splitTspMess
|
|
|
48
48
|
import type { DescribeContext, NativeChild, NativeNode, NativeSurface, NativeUiEvent } from "./node";
|
|
49
49
|
import { NARROW_SHEET_COLS } from "./overlay";
|
|
50
50
|
import { nativeComponentId, Reconciler } from "./reconcile";
|
|
51
|
-
import { setNativeRendering } from "./state";
|
|
51
|
+
import { setNativeButtons, setNativeRendering } from "./state";
|
|
52
52
|
|
|
53
53
|
/** A visible TUI overlay, bottom to top. */
|
|
54
54
|
export interface NativeOverlay {
|
|
@@ -199,9 +199,10 @@ function sameSet(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
|
|
|
199
199
|
* Kinds newer than some Tern builds that answer `TERM_PROGRAM=tern`: used only
|
|
200
200
|
* once the real `hello` lists them, so the optimistic start never emits a node
|
|
201
201
|
* an older terminal cannot draw (`media`: files Tern loads by path; `json`:
|
|
202
|
-
* raw JSON values Tern draws as its own tree
|
|
202
|
+
* raw JSON values Tern draws as its own tree; `button`: omp's buttons, which
|
|
203
|
+
* older builds get as role-styled rows).
|
|
203
204
|
*/
|
|
204
|
-
const UNASSUMED_KINDS: Partial<Record<TspKind, true>> = { media: true, json: true };
|
|
205
|
+
const UNASSUMED_KINDS: Partial<Record<TspKind, true>> = { media: true, json: true, button: true };
|
|
205
206
|
|
|
206
207
|
/**
|
|
207
208
|
* The reply a v1 terminal is assumed to give before its real `hello` arrives
|
|
@@ -380,6 +381,7 @@ export class NativeBackend {
|
|
|
380
381
|
this.#close(this.#inline, keep);
|
|
381
382
|
this.#inline.doc?.close();
|
|
382
383
|
setNativeRendering(false);
|
|
384
|
+
setNativeButtons(false);
|
|
383
385
|
this.#useNerdSymbols(false);
|
|
384
386
|
this.#clearStallTimer();
|
|
385
387
|
// Replies no longer reach the backend: forget the queries outright.
|
|
@@ -539,6 +541,8 @@ export class NativeBackend {
|
|
|
539
541
|
this.#limit = hello.apc !== undefined && hello.apc > 0 ? hello.apc : TSP_DEFAULT_APC_LIMIT;
|
|
540
542
|
this.#credits = hello.credits !== undefined && hello.credits > 0 ? hello.credits : TSP_DEFAULT_CREDITS;
|
|
541
543
|
this.#cx = new NativeContext(hello, hello.cols ?? this.#cx.cols);
|
|
544
|
+
// A changed vocabulary re-describes every component (`confirm`).
|
|
545
|
+
setNativeButtons(this.#cx.supports("button"));
|
|
542
546
|
this.#sawResize = false;
|
|
543
547
|
}
|
|
544
548
|
|
package/src/native/describe.ts
CHANGED
|
@@ -132,10 +132,15 @@ export function elapsed(age: number, stopped?: boolean): NativeNode {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
135
|
+
* The key names of one key id (`"ctrl+o"` → `["ctrl", "o"]`), as `kbd` and
|
|
136
|
+
* `button` `keys` take them. A literal `+` key survives (`"+"`, `"ctrl++"`).
|
|
137
137
|
*/
|
|
138
|
-
export function
|
|
138
|
+
export function keyNames(key: string): string[] {
|
|
139
139
|
const keys = key === "+" ? ["+"] : key.endsWith("++") ? [...key.slice(0, -2).split("+"), "+"] : key.split("+");
|
|
140
|
-
return
|
|
140
|
+
return keys.filter(part => part.length > 0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Keycap for one key id ({@link keyNames}); the terminal draws platform glyphs. */
|
|
144
|
+
export function kbd(key: string, nodeKey?: string): NativeNode {
|
|
145
|
+
return node("kbd", { keys: keyNames(key) }, undefined, nodeKey);
|
|
141
146
|
}
|
package/src/native/overlay.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Overlay chrome for `describe()`: root cards, key-hint footers,
|
|
3
|
-
* lists and annotation callouts.
|
|
2
|
+
* Overlay chrome for `describe()`: root cards, key-hint footers, buttons and
|
|
3
|
+
* segmented controls, selectable lists and annotation callouts.
|
|
4
4
|
*
|
|
5
5
|
* The overlay frame itself (glass, ring, placement) is the terminal's: the
|
|
6
6
|
* engine wraps every overlay-stack component in an `overlay` node. An overlay
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
import type { TspProps, TspSpan, TspText } from "@oh-my-pi/pi-wire";
|
|
10
10
|
import { formatTooltipKey, type KeyName } from "../key-hint-format";
|
|
11
11
|
import { getKeybindings, type Keybinding } from "../keybindings";
|
|
12
|
-
import { card, kbd, list, node, row, span, text } from "./describe";
|
|
12
|
+
import { card, kbd, keyNames, list, node, row, span, text } from "./describe";
|
|
13
13
|
import type { NativeChild, NativeNode } from "./node";
|
|
14
|
+
import { nativeButtons } from "./state";
|
|
14
15
|
|
|
15
16
|
/** Below this many columns a centered sheet fills the pane instead of floating with margins. */
|
|
16
17
|
export const NARROW_SHEET_COLS = 100;
|
|
@@ -96,29 +97,68 @@ export interface ActionButtonOptions {
|
|
|
96
97
|
readonly title?: string;
|
|
97
98
|
/** Target of an `open` action (URL or `file://` path). */
|
|
98
99
|
readonly href?: string;
|
|
100
|
+
/** Why the action can't run now: the button dims and a click sends nothing. */
|
|
101
|
+
readonly disabled?: string;
|
|
102
|
+
/** The button's work is running: a spinner leads the label. */
|
|
103
|
+
readonly busy?: boolean;
|
|
104
|
+
/** Icon beside the label (before it on a `button` node, after it on a row). */
|
|
105
|
+
readonly icon?: string;
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
/**
|
|
102
109
|
* A native button (role `omp.btn`): a label plus an optional keycap. A click
|
|
103
110
|
* sends `act` back as an `action` event (or runs a terminal-local `open`/`copy`),
|
|
104
111
|
* which the component routes to the same code path as the key.
|
|
112
|
+
*
|
|
113
|
+
* A terminal that draws the `button` kind ({@link nativeButtons}) gets one
|
|
114
|
+
* (tone `accent` → variant `primary`, `error` → `danger`, `disabled` with its
|
|
115
|
+
* reason as the tooltip); older ones get the row their stylesheets draw by
|
|
116
|
+
* role. Both keep the role and tone the omp sheets' context rules key on; a
|
|
117
|
+
* disabled button's tone is `muted`.
|
|
105
118
|
*/
|
|
106
119
|
export function actionButton(label: string, act: string, options: ActionButtonOptions = {}): NativeNode {
|
|
107
|
-
const
|
|
108
|
-
|
|
120
|
+
const { keys, href, disabled, busy, icon } = options;
|
|
121
|
+
const title = options.title ?? (keys ? `${label} ${formatTooltipKey(keys)}` : label);
|
|
122
|
+
const tone = disabled === undefined ? options.tone : "muted";
|
|
123
|
+
const key = options.key ?? act;
|
|
124
|
+
if (nativeButtons()) {
|
|
125
|
+
return node(
|
|
126
|
+
"button",
|
|
127
|
+
{
|
|
128
|
+
role: "omp.btn",
|
|
129
|
+
text: label,
|
|
130
|
+
actions: { click: act },
|
|
131
|
+
title,
|
|
132
|
+
...(keys ? { keys: keyNames(keys) } : {}),
|
|
133
|
+
...(icon ? { icon } : {}),
|
|
134
|
+
...(tone === "accent" ? { variant: "primary" } : tone === "error" ? { variant: "danger" } : {}),
|
|
135
|
+
...(tone ? { tone } : {}),
|
|
136
|
+
...(disabled !== undefined ? { disabled: disabled || true } : {}),
|
|
137
|
+
...(busy ? { busy } : {}),
|
|
138
|
+
...(href ? { href } : {}),
|
|
139
|
+
},
|
|
140
|
+
undefined,
|
|
141
|
+
key,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const children: NativeChild[] = [];
|
|
145
|
+
if (busy) children.push(node("spinner", { style: "dots" }));
|
|
146
|
+
children.push(text(label));
|
|
147
|
+
if (icon) children.push(node("icon", { name: icon }, undefined, "icon"));
|
|
148
|
+
if (keys) children.push(kbd(keys));
|
|
109
149
|
return node(
|
|
110
150
|
"row",
|
|
111
151
|
{
|
|
112
152
|
role: "omp.btn",
|
|
113
153
|
gap: "xs",
|
|
114
154
|
align: "center",
|
|
115
|
-
actions: { click: act },
|
|
116
|
-
title
|
|
117
|
-
...(
|
|
118
|
-
...(
|
|
155
|
+
...(disabled === undefined ? { actions: { click: act } } : {}),
|
|
156
|
+
title,
|
|
157
|
+
...(tone ? { tone } : {}),
|
|
158
|
+
...(href ? { href } : {}),
|
|
119
159
|
},
|
|
120
160
|
children,
|
|
121
|
-
|
|
161
|
+
key,
|
|
122
162
|
);
|
|
123
163
|
}
|
|
124
164
|
|
|
@@ -127,12 +167,80 @@ export function actionButton(label: string, act: string, options: ActionButtonOp
|
|
|
127
167
|
* `act` (default `close`), which the component routes to its Esc path.
|
|
128
168
|
*/
|
|
129
169
|
export function escCloseButton(act = "close", key = "esc-close"): NativeNode {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
170
|
+
const title = `Close ${formatTooltipKey("escape")}`;
|
|
171
|
+
if (nativeButtons()) {
|
|
172
|
+
return node(
|
|
173
|
+
"button",
|
|
174
|
+
{ role: "omp.btn", keys: ["escape"], actions: { click: act }, title, aria: "Close" },
|
|
175
|
+
undefined,
|
|
176
|
+
key,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return node("row", { role: "omp.btn", align: "center", actions: { click: act }, title }, [kbd("escape")], key);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Options of an {@link iconButton}. */
|
|
183
|
+
export interface IconButtonOptions {
|
|
184
|
+
/** Sibling identity key; unkeyed buttons take their position. */
|
|
185
|
+
readonly key?: string;
|
|
186
|
+
/** Accessible name; defaults to the title. */
|
|
187
|
+
readonly aria?: string;
|
|
188
|
+
/** A toggle that is on (role `omp.app.ibtn.set`). */
|
|
189
|
+
readonly on?: boolean;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A quiet icon-only button (role `omp.app.ibtn`, the kit's icon button) for an
|
|
194
|
+
* app head or toolbar: a `button` node where the terminal draws one
|
|
195
|
+
* ({@link nativeButtons}), else an `icon` with a click action.
|
|
196
|
+
*/
|
|
197
|
+
export function iconButton(name: string, act: string, title: string, options: IconButtonOptions = {}): NativeNode {
|
|
198
|
+
const role = options.on ? "omp.app.ibtn.set" : "omp.app.ibtn";
|
|
199
|
+
const aria = options.aria ?? title;
|
|
200
|
+
if (nativeButtons()) {
|
|
201
|
+
return node(
|
|
202
|
+
"button",
|
|
203
|
+
{
|
|
204
|
+
role,
|
|
205
|
+
variant: "quiet",
|
|
206
|
+
icon: name,
|
|
207
|
+
title,
|
|
208
|
+
aria,
|
|
209
|
+
actions: { click: act },
|
|
210
|
+
...(options.on ? { on: true } : {}),
|
|
211
|
+
},
|
|
212
|
+
undefined,
|
|
213
|
+
options.key,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return node("icon", { name, role, title, aria, actions: { click: act } }, undefined, options.key);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A quiet text button (role `omp.app.qbtn`, the kit's quiet button) for a
|
|
221
|
+
* section head or toolbar, keyed by its action: a small quiet `button` where
|
|
222
|
+
* the terminal draws one ({@link nativeButtons}), else a row.
|
|
223
|
+
*/
|
|
224
|
+
export function quietButton(label: string, act: string, title: string): NativeNode {
|
|
225
|
+
if (nativeButtons()) {
|
|
226
|
+
return node(
|
|
227
|
+
"button",
|
|
228
|
+
{ role: "omp.app.qbtn", variant: "quiet", size: "sm", text: label, actions: { click: act }, title },
|
|
229
|
+
undefined,
|
|
230
|
+
act,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return node("row", { role: "omp.app.qbtn", actions: { click: act }, title }, [text(label)], act);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A segmented control: `tabs` drawn as a well with a raised thumb on the
|
|
238
|
+
* active tab. A terminal that draws the `button` kind ({@link nativeButtons})
|
|
239
|
+
* draws `variant: "segmented"` too; older ones restyle the tabs by role
|
|
240
|
+
* (`omp.app.seg`) or parent.
|
|
241
|
+
*/
|
|
242
|
+
export function segmentedTabs(p: TspProps<"tabs">, key: string): NativeNode {
|
|
243
|
+
return node("tabs", nativeButtons() ? { ...p, variant: "segmented" } : p, undefined, key);
|
|
136
244
|
}
|
|
137
245
|
|
|
138
246
|
/** A row of {@link actionButton}s (role `omp.actions`); `null` entries become the spacer that end-aligns what follows. */
|
package/src/native/state.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Process-wide
|
|
2
|
+
* Process-wide native rendering state, set only by the native backend.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Whether a Tern Surface Protocol backend is rendering: components that
|
|
5
|
+
* animate by re-rendering on a timer (spinners, shimmer, thinking frames,
|
|
6
|
+
* countdowns) check it and skip scheduling, because the terminal clocks
|
|
7
|
+
* motion from the described nodes. Set when a surface opens and cleared when
|
|
8
|
+
* it closes.
|
|
8
9
|
*/
|
|
9
10
|
let active = false;
|
|
10
11
|
const listeners = new Set<(on: boolean) => void>();
|
|
@@ -21,6 +22,25 @@ export function setNativeRendering(on: boolean): void {
|
|
|
21
22
|
for (const listener of listeners) listener(on);
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Whether the live terminal draws the `button` kind (its `hello` lists it),
|
|
27
|
+
* and with it `tabs` `variant: "segmented"`. The button builders
|
|
28
|
+
* (`native/overlay.ts`) read it; older Terns get the `row` buttons they style
|
|
29
|
+
* by role. Set only by the native backend from the `hello` it adopts; false
|
|
30
|
+
* while no surface is live.
|
|
31
|
+
*/
|
|
32
|
+
let buttons = false;
|
|
33
|
+
|
|
34
|
+
/** Whether omp's buttons go out as `button` nodes (see {@link setNativeButtons}). */
|
|
35
|
+
export function nativeButtons(): boolean {
|
|
36
|
+
return buttons;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Called by the native backend when it adopts a `hello` reply and when it stops. */
|
|
40
|
+
export function setNativeButtons(on: boolean): void {
|
|
41
|
+
buttons = on;
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
/** Call `listener` whenever native rendering starts or stops; returns the unsubscribe. */
|
|
25
45
|
export function onNativeRenderingChange(listener: (on: boolean) => void): () => void {
|
|
26
46
|
listeners.add(listener);
|
|
@@ -23,7 +23,7 @@ import type { Component } from "../../tui";
|
|
|
23
23
|
import { col, keyed, node, span, text } from "../../native/describe";
|
|
24
24
|
import { Memo } from "../../native/memo";
|
|
25
25
|
import type { DescribeContext, NativeNode, NativeScroll, NativeUiEvent } from "../../native/node";
|
|
26
|
-
import { actionBar, actionButton } from "../../native/overlay";
|
|
26
|
+
import { actionBar, actionButton, iconButton } from "../../native/overlay";
|
|
27
27
|
import { CLOSE_ACTION, type PickerEvent, picker, pickerAction, pickerEvent } from "../../native/picker";
|
|
28
28
|
import { type KeyId, matchesKey } from "../../keys";
|
|
29
29
|
import { parseSgrMouse } from "../../mouse";
|
|
@@ -711,13 +711,12 @@ export class ExtensionDashboard implements Component {
|
|
|
711
711
|
text(DASHBOARD_TITLE, { role: "omp.app.title" }),
|
|
712
712
|
text([span(query ? `${shown} of ${total}` : `${total} extensions`, "muted")], { truncate: "end" }),
|
|
713
713
|
]),
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
aria: "Close",
|
|
719
|
-
|
|
720
|
-
}),
|
|
714
|
+
iconButton(
|
|
715
|
+
"x",
|
|
716
|
+
"close",
|
|
717
|
+
`Close ${formatTooltipKey(boundKeys("app.interrupt", ["escape"])[0] ?? "escape")}`,
|
|
718
|
+
{ aria: "Close" },
|
|
719
|
+
),
|
|
721
720
|
],
|
|
722
721
|
"head",
|
|
723
722
|
);
|
|
@@ -468,18 +468,7 @@ export class LoginDialogComponent extends OverlayPanel {
|
|
|
468
468
|
[
|
|
469
469
|
node("icon", { name: "link" }, undefined, "icon"),
|
|
470
470
|
keyed(shown, "url"),
|
|
471
|
-
|
|
472
|
-
"row",
|
|
473
|
-
{
|
|
474
|
-
role: "omp.btn",
|
|
475
|
-
align: "center",
|
|
476
|
-
actions: { click: "copy" },
|
|
477
|
-
href: url,
|
|
478
|
-
title: "Copy the sign-in link",
|
|
479
|
-
},
|
|
480
|
-
[text("Copy")],
|
|
481
|
-
"copyLink",
|
|
482
|
-
),
|
|
471
|
+
actionButton("Copy", "copy", { key: "copyLink", href: url, title: "Copy the sign-in link" }),
|
|
483
472
|
],
|
|
484
473
|
"link",
|
|
485
474
|
);
|
|
@@ -487,20 +476,13 @@ export class LoginDialogComponent extends OverlayPanel {
|
|
|
487
476
|
|
|
488
477
|
/** Open sign-in page (Tern opens the link itself): the primary action while nothing waits for an answer. */
|
|
489
478
|
#describeOpen(url: string, primary: boolean): NativeNode {
|
|
490
|
-
return
|
|
491
|
-
"
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
href: url,
|
|
498
|
-
title: `Open ${urlPlace(url)} in your browser`,
|
|
499
|
-
...(primary ? { tone: "accent" as const } : {}),
|
|
500
|
-
},
|
|
501
|
-
[text("Open sign-in page"), node("icon", { name: "open" }, undefined, "icon")],
|
|
502
|
-
"openLink",
|
|
503
|
-
);
|
|
479
|
+
return actionButton("Open sign-in page", "open", {
|
|
480
|
+
key: "openLink",
|
|
481
|
+
href: url,
|
|
482
|
+
title: `Open ${urlPlace(url)} in your browser`,
|
|
483
|
+
icon: "open",
|
|
484
|
+
tone: primary ? "accent" : undefined,
|
|
485
|
+
});
|
|
504
486
|
}
|
|
505
487
|
|
|
506
488
|
/** The stage running now: a spinner and what it waits for. */
|