@oh-my-pi/pi-tui 18.14.0 → 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 +36 -0
- package/README.md +6 -0
- package/dist/types/charts/table-data.d.ts +4 -2
- package/dist/types/chat/figure-markdown.d.ts +2 -0
- package/dist/types/chat/svg-figure.d.ts +5 -0
- package/dist/types/chat/table-chart.d.ts +2 -0
- package/dist/types/chat/transcript-render-helpers.d.ts +3 -3
- package/dist/types/components/image.d.ts +6 -0
- 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/overlays/btw-history-panel.d.ts +2 -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/figure-markdown.ts +6 -4
- package/src/chat/svg-figure.ts +55 -16
- package/src/chat/table-chart.ts +19 -2
- package/src/chat/transcript-render-helpers.ts +18 -16
- package/src/components/image.ts +13 -0
- 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/btw-history-panel.ts +8 -0
- package/src/overlays/extensions/extension-dashboard.ts +7 -8
- package/src/overlays/jobs-panel.ts +38 -4
- package/src/overlays/login-dialog.ts +8 -26
- package/src/overlays/tree-selector.ts +81 -27
- 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/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);
|
|
@@ -150,6 +150,7 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
150
150
|
#listScroll = 0;
|
|
151
151
|
#listHeight = 1;
|
|
152
152
|
#focused = false;
|
|
153
|
+
#useTerminalCursor = false;
|
|
153
154
|
#composer: FollowUpComposer | undefined;
|
|
154
155
|
readonly #pasteHandler = new BracketedPasteHandler();
|
|
155
156
|
#followUpPending = false;
|
|
@@ -225,6 +226,12 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
225
226
|
this.#focused = value;
|
|
226
227
|
}
|
|
227
228
|
|
|
229
|
+
/** Keep the follow-up input's cursor mode aligned so the hardware cursor (and IME preedit) lands on its caret. */
|
|
230
|
+
setUseTerminalCursor(useTerminalCursor: boolean): void {
|
|
231
|
+
this.#useTerminalCursor = useTerminalCursor;
|
|
232
|
+
this.#composer?.input.setUseTerminalCursor(useTerminalCursor);
|
|
233
|
+
}
|
|
234
|
+
|
|
228
235
|
update(records: readonly BtwHistoryRecord[]): void {
|
|
229
236
|
this.#records = records;
|
|
230
237
|
this.#detailDirty = true;
|
|
@@ -329,6 +336,7 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
329
336
|
const input = new Input();
|
|
330
337
|
input.prompt = theme.fg("accent", "Follow up: ");
|
|
331
338
|
input.placeholder = "Ask about this answer…";
|
|
339
|
+
input.setUseTerminalCursor(this.#useTerminalCursor);
|
|
332
340
|
if (this.#options.spaceHoldKeys !== undefined) input.spaceHold.keys = this.#options.spaceHoldKeys;
|
|
333
341
|
input.spaceHold.handler = this.#options.spaceHold?.(input);
|
|
334
342
|
const composer: FollowUpComposer = {
|
|
@@ -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
|
);
|
|
@@ -130,6 +130,12 @@ export class JobsSheet implements Component {
|
|
|
130
130
|
#native: { key: string; output: string | undefined; node: NativeNode } | undefined;
|
|
131
131
|
/** Fallback output tail, reused while the selected job's output is unchanged. */
|
|
132
132
|
#tail: { id: string; output: string; lines: readonly string[] } | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* The selected job as last seen: its row, detail and the subagents placing
|
|
135
|
+
* it. The session evicts settled jobs on its own clock; while the user keeps
|
|
136
|
+
* one selected it stays listed with its last output, until they select another.
|
|
137
|
+
*/
|
|
138
|
+
#held: { job: JobsPanelJob; detail: JobsPanelDetail | undefined; agents: readonly JobTreeAgent[] } | undefined;
|
|
133
139
|
|
|
134
140
|
constructor(source: JobsSheetSource) {
|
|
135
141
|
this.#source = source;
|
|
@@ -137,7 +143,7 @@ export class JobsSheet implements Component {
|
|
|
137
143
|
|
|
138
144
|
describe(_cx: DescribeContext): NativeNode {
|
|
139
145
|
const { rows, jobs, selected, cancellable } = this.#current();
|
|
140
|
-
const detail = selected && this.#
|
|
146
|
+
const detail = selected && this.#inspect(selected.id);
|
|
141
147
|
const now = Date.now();
|
|
142
148
|
// Elapsed nodes tick on their own; list values and the output pane redraw
|
|
143
149
|
// when a job starts, settles, leaves or moves in the tree, its process
|
|
@@ -201,7 +207,7 @@ export class JobsSheet implements Component {
|
|
|
201
207
|
),
|
|
202
208
|
);
|
|
203
209
|
}
|
|
204
|
-
const detail = selected && this.#
|
|
210
|
+
const detail = selected && this.#inspect(selected.id);
|
|
205
211
|
if (detail) {
|
|
206
212
|
// Rows show the cut label; the selected job's command wraps here,
|
|
207
213
|
// bounded so a long script never pushes the facts and output off the sheet.
|
|
@@ -262,15 +268,43 @@ export class JobsSheet implements Component {
|
|
|
262
268
|
selected: JobsPanelJob | undefined;
|
|
263
269
|
cancellable: boolean;
|
|
264
270
|
} {
|
|
265
|
-
const
|
|
266
|
-
const
|
|
271
|
+
const snapshot = this.#source.load();
|
|
272
|
+
const { running, rootId } = snapshot;
|
|
273
|
+
let { recent, agents = [] } = snapshot;
|
|
274
|
+
// A settled job the session evicted stays listed while it is selected.
|
|
275
|
+
const held = this.#held?.job.id === this.#selectedId ? this.#held : undefined;
|
|
276
|
+
const evicted =
|
|
277
|
+
held !== undefined &&
|
|
278
|
+
held.job.status !== "running" &&
|
|
279
|
+
!running.some(job => job.id === held.job.id) &&
|
|
280
|
+
!recent.some(job => job.id === held.job.id);
|
|
281
|
+
if (evicted) {
|
|
282
|
+
recent = [...recent, held.job];
|
|
283
|
+
const known = new Set(agents.map(agent => agent.id));
|
|
284
|
+
agents = [...agents, ...held.agents.filter(agent => !known.has(agent.id))];
|
|
285
|
+
}
|
|
286
|
+
const rows = buildJobTree([...running, ...recent], agents, rootId);
|
|
267
287
|
const jobs = rows.flatMap(entry => (entry.kind === "job" ? [entry.job] : []));
|
|
268
288
|
const selected = jobs.find(job => job.id === this.#selectedId) ?? jobs[0];
|
|
269
289
|
this.#selectedId = selected?.id;
|
|
290
|
+
if (!selected) this.#held = undefined;
|
|
291
|
+
else if (!evicted) {
|
|
292
|
+
const detail = held?.job.id === selected.id ? held.detail : undefined;
|
|
293
|
+
this.#held = { job: selected, detail, agents: snapshot.agents ?? [] };
|
|
294
|
+
}
|
|
270
295
|
const owned = selected?.ownerId === undefined || selected.ownerId === rootId;
|
|
271
296
|
return { rows, jobs, selected, cancellable: this.#source.cancel !== undefined && owned };
|
|
272
297
|
}
|
|
273
298
|
|
|
299
|
+
/** The job's detail from the source, else the last one seen while it was held. */
|
|
300
|
+
#inspect(id: string): JobsPanelDetail | undefined {
|
|
301
|
+
const detail = this.#source.inspect(id);
|
|
302
|
+
const held = this.#held?.job.id === id ? this.#held : undefined;
|
|
303
|
+
if (!held) return detail;
|
|
304
|
+
if (detail) held.detail = detail;
|
|
305
|
+
return held.detail;
|
|
306
|
+
}
|
|
307
|
+
|
|
274
308
|
#tailLines(id: string, output: string): readonly string[] {
|
|
275
309
|
const cached = this.#tail;
|
|
276
310
|
if (cached?.id === id && cached.output === output) return cached.lines;
|
|
@@ -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. */
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
TruncatedText,
|
|
11
11
|
truncateToWidth,
|
|
12
12
|
} from "../index";
|
|
13
|
-
import { isRecord, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
13
|
+
import { isRecord, oklabDistance, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
14
14
|
/** Available session-tree display filters. */
|
|
15
15
|
export const TREE_FILTER_MODES = ["default", "no-tools", "user-only", "labeled-only", "all"] as const;
|
|
16
16
|
/** Session-tree display filter. */
|
|
@@ -94,10 +94,15 @@ interface AdvisorTreeDisplay {
|
|
|
94
94
|
*/
|
|
95
95
|
function sanitizeTreeField(value: string): string {
|
|
96
96
|
return sanitizeText(value)
|
|
97
|
-
.replace(/[\n\t]/g, " ")
|
|
97
|
+
.replace(/[\r\n\t\u2028\u2029]/g, " ")
|
|
98
98
|
.trim();
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/** Strip ANSI/control characters and fold line breaks for single-line tree rows and headers. */
|
|
102
|
+
function treeRowText(value: string): string {
|
|
103
|
+
return plainText(value.replace(/[\r\n\u2028\u2029]/g, " "));
|
|
104
|
+
}
|
|
105
|
+
|
|
101
106
|
/**
|
|
102
107
|
* Extract display metadata from an advisor custom-message's `details.notes`,
|
|
103
108
|
* ignoring the model-facing `<advisory>` wrapper stored in `content`. Collects
|
|
@@ -165,10 +170,38 @@ function stripSystemWrapperTags(content: string): string {
|
|
|
165
170
|
/** Per-message cap on text folded into the tree search index. */
|
|
166
171
|
const SEARCH_TEXT_LIMIT = 200;
|
|
167
172
|
|
|
168
|
-
/** One theme-colored piece of an entry's display text. */
|
|
173
|
+
/** One theme-colored piece of an entry's display text; `strong` draws it bold. */
|
|
169
174
|
interface EntryPart {
|
|
170
175
|
t: string;
|
|
171
176
|
s?: ThemeColor;
|
|
177
|
+
strong?: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** An entry part as a native span. */
|
|
181
|
+
function partSpan(part: EntryPart): TspSpan {
|
|
182
|
+
const style = part.strong ? (part.s ? `${part.s} strong` : "strong") : part.s;
|
|
183
|
+
return span(treeRowText(part.t), style);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** OKLab distance under which two role colors read as the same color. */
|
|
187
|
+
const SAME_COLOR_DISTANCE = 0.05;
|
|
188
|
+
|
|
189
|
+
let assistantLabel: { theme: typeof theme; color: ThemeColor } | undefined;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The `assistant:` label color: `success`, unless it looks like the `user:` label's
|
|
193
|
+
* `accent` (`dark-terminal`, `*-poimandres`); then the path color, then `muted`.
|
|
194
|
+
*/
|
|
195
|
+
function assistantLabelColor(): ThemeColor {
|
|
196
|
+
if (assistantLabel?.theme !== theme) {
|
|
197
|
+
const accent = theme.getColorHex("accent");
|
|
198
|
+
const color =
|
|
199
|
+
(["success", "statusLinePath"] as const).find(
|
|
200
|
+
c => oklabDistance(theme.getColorHex(c), accent) >= SAME_COLOR_DISTANCE,
|
|
201
|
+
) ?? "muted";
|
|
202
|
+
assistantLabel = { theme, color };
|
|
203
|
+
}
|
|
204
|
+
return assistantLabel.color;
|
|
172
205
|
}
|
|
173
206
|
|
|
174
207
|
/** A cached native tree item and the inputs it was built from. */
|
|
@@ -234,6 +267,8 @@ class TreeList implements Component {
|
|
|
234
267
|
| undefined;
|
|
235
268
|
/** Bumped on every label edit: labels are item badges. */
|
|
236
269
|
#labelVersion = 0;
|
|
270
|
+
/** Theme the native item caches were built under: labels bake in the `assistant:` color. */
|
|
271
|
+
#nativeTheme = theme;
|
|
237
272
|
#pickerItems:
|
|
238
273
|
| { rows: readonly TreeRow<TreeSelectorNode, string>[]; labels: number; items: TspPickerItem[] }
|
|
239
274
|
| undefined;
|
|
@@ -703,6 +738,7 @@ class TreeList implements Component {
|
|
|
703
738
|
* only `order` (the visible entries) and the selection.
|
|
704
739
|
*/
|
|
705
740
|
pickerView(): TreePickerView {
|
|
741
|
+
this.#syncNativeTheme();
|
|
706
742
|
const allRows = this.#tree.allRows;
|
|
707
743
|
if (this.#pickerItems?.rows !== allRows || this.#pickerItems.labels !== this.#labelVersion) {
|
|
708
744
|
// Real branch points get a disclosure; linear chains stay flat at their head's depth.
|
|
@@ -734,9 +770,7 @@ class TreeList implements Component {
|
|
|
734
770
|
#pickerItem(row: TreeRow<TreeSelectorNode, string>, open: boolean): TspPickerItem {
|
|
735
771
|
const item = row.item;
|
|
736
772
|
const entry = item.entry;
|
|
737
|
-
const label = this.#entryParts(item).map(
|
|
738
|
-
span(part.t.includes("\x1b") ? plainText(part.t) : part.t, part.s),
|
|
739
|
-
);
|
|
773
|
+
const label = this.#entryParts(item).map(partSpan);
|
|
740
774
|
let kind: TspPickerItem["node"] = "marker";
|
|
741
775
|
let role: string | undefined;
|
|
742
776
|
if (isUserRequestEntry(entry)) {
|
|
@@ -755,7 +789,7 @@ class TreeList implements Component {
|
|
|
755
789
|
node: kind,
|
|
756
790
|
...(role ? { role } : {}),
|
|
757
791
|
...(open ? { open: true } : {}),
|
|
758
|
-
...(item.label ? { badges: [{ text:
|
|
792
|
+
...(item.label ? { badges: [{ text: treeRowText(item.label), tone: "warning" as const }] } : {}),
|
|
759
793
|
...(this.#activePathIds.has(row.key) ? { dot: "accent" as const } : {}),
|
|
760
794
|
};
|
|
761
795
|
}
|
|
@@ -772,7 +806,7 @@ class TreeList implements Component {
|
|
|
772
806
|
if (selected) {
|
|
773
807
|
const entry = selected.entry;
|
|
774
808
|
const parts = this.#entryParts(selected)
|
|
775
|
-
.map(part => (part.t
|
|
809
|
+
.map(part => treeRowText(part.t))
|
|
776
810
|
.join("");
|
|
777
811
|
if (entry.type === "message" || entry.type === "custom_message") {
|
|
778
812
|
const target: OutlineTarget = {
|
|
@@ -784,12 +818,14 @@ class TreeList implements Component {
|
|
|
784
818
|
entries: [entry],
|
|
785
819
|
};
|
|
786
820
|
const copy = targetCopy(target, collectBlocks(target.entries));
|
|
787
|
-
preview.push(
|
|
788
|
-
|
|
821
|
+
preview.push(
|
|
822
|
+
text(treeRowText(`${copy.label[0]!.toUpperCase()}${copy.label.slice(1)}`), { role: "omp.picker.title" }),
|
|
823
|
+
);
|
|
824
|
+
if (selected.label) preview.push(text([span(treeRowText(selected.label), "warning")]));
|
|
789
825
|
preview.push(turnPreview(target, copy.content || parts));
|
|
790
826
|
} else {
|
|
791
827
|
preview.push(text(parts, { role: "omp.picker.title" }));
|
|
792
|
-
if (selected.label) preview.push(text([span(
|
|
828
|
+
if (selected.label) preview.push(text([span(treeRowText(selected.label), "warning")]));
|
|
793
829
|
}
|
|
794
830
|
}
|
|
795
831
|
this.#previewMemo = { node: selected, labels: this.#labelVersion, preview };
|
|
@@ -803,6 +839,7 @@ class TreeList implements Component {
|
|
|
803
839
|
* instead of drawn tree connectors.
|
|
804
840
|
*/
|
|
805
841
|
describeList(): NativeNode {
|
|
842
|
+
this.#syncNativeTheme();
|
|
806
843
|
const rows = this.#tree.rows;
|
|
807
844
|
let items = this.#itemsNative;
|
|
808
845
|
if (items?.rows !== rows) {
|
|
@@ -852,6 +889,15 @@ class TreeList implements Component {
|
|
|
852
889
|
];
|
|
853
890
|
}
|
|
854
891
|
|
|
892
|
+
/** Drop native items built under another theme. */
|
|
893
|
+
#syncNativeTheme(): void {
|
|
894
|
+
if (this.#nativeTheme === theme) return;
|
|
895
|
+
this.#nativeTheme = theme;
|
|
896
|
+
this.#pickerItems = undefined;
|
|
897
|
+
this.#itemsNative = undefined;
|
|
898
|
+
this.#itemCache.clear();
|
|
899
|
+
}
|
|
900
|
+
|
|
855
901
|
#describeRow(row: TreeRow<TreeSelectorNode, string>): NativeNode {
|
|
856
902
|
const item = row.item;
|
|
857
903
|
const branch = row.parentKey !== undefined && row.siblingCount > 1;
|
|
@@ -859,10 +905,8 @@ class TreeList implements Component {
|
|
|
859
905
|
if (cached !== undefined && cached.label === item.label && cached.branch === branch) return cached.node;
|
|
860
906
|
const label: TspSpan[] = [];
|
|
861
907
|
if (this.#activePathIds.has(row.key)) label.push(span(`${theme.md.bullet} `, "accent"));
|
|
862
|
-
if (item.label) label.push(span(`[${
|
|
863
|
-
for (const part of this.#entryParts(item))
|
|
864
|
-
label.push(span(part.t.includes("\x1b") ? plainText(part.t) : part.t, part.s));
|
|
865
|
-
}
|
|
908
|
+
if (item.label) label.push(span(`[${treeRowText(item.label)}] `, "warning"));
|
|
909
|
+
for (const part of this.#entryParts(item)) label.push(partSpan(part));
|
|
866
910
|
const described = node("item", { label, icon: branch ? "branch" : undefined }, undefined, row.key);
|
|
867
911
|
this.#itemCache.set(row.key, { label: item.label, branch, node: described });
|
|
868
912
|
return described;
|
|
@@ -954,7 +998,7 @@ class TreeList implements Component {
|
|
|
954
998
|
// Active path marker - shown right before the entry text
|
|
955
999
|
const isOnActivePath = this.#activePathIds.has(node.entry.id);
|
|
956
1000
|
const pathMarker = isOnActivePath ? theme.fg("accent", `${theme.md.bullet} `) : "";
|
|
957
|
-
const label = node.label ? theme.fg("warning", `[${node.label}] `) : "";
|
|
1001
|
+
const label = node.label ? theme.fg("warning", `[${treeRowText(node.label)}] `) : "";
|
|
958
1002
|
return `${pathMarker}${label}${this.#getEntryDisplayText(node, selected)}`;
|
|
959
1003
|
}
|
|
960
1004
|
|
|
@@ -1050,7 +1094,13 @@ class TreeList implements Component {
|
|
|
1050
1094
|
|
|
1051
1095
|
#getEntryDisplayText(node: TreeSelectorNode, isSelected: boolean): string {
|
|
1052
1096
|
let result = "";
|
|
1053
|
-
for (const part of this.#entryParts(node))
|
|
1097
|
+
for (const part of this.#entryParts(node)) {
|
|
1098
|
+
let value = treeRowText(part.t);
|
|
1099
|
+
if (part.s) value = theme.fg(part.s, value);
|
|
1100
|
+
// A selected row is bolded whole; nested bold would close it early.
|
|
1101
|
+
if (part.strong && !isSelected) value = theme.bold(value);
|
|
1102
|
+
result += value;
|
|
1103
|
+
}
|
|
1054
1104
|
return isSelected ? theme.bold(result) : result;
|
|
1055
1105
|
}
|
|
1056
1106
|
|
|
@@ -1066,7 +1116,10 @@ class TreeList implements Component {
|
|
|
1066
1116
|
if (role === "user") {
|
|
1067
1117
|
const msgWithContent = msg as { content?: unknown };
|
|
1068
1118
|
const content = normalize(this.#extractContent(msgWithContent.content));
|
|
1069
|
-
return [
|
|
1119
|
+
return [
|
|
1120
|
+
{ t: "user: ", s: "accent", strong: true },
|
|
1121
|
+
{ t: content, s: "accent", strong: true },
|
|
1122
|
+
];
|
|
1070
1123
|
}
|
|
1071
1124
|
if (role === "developer") {
|
|
1072
1125
|
const msgWithContent = msg as { content?: unknown };
|
|
@@ -1078,15 +1131,10 @@ class TreeList implements Component {
|
|
|
1078
1131
|
}
|
|
1079
1132
|
if (role === "assistant") {
|
|
1080
1133
|
const presentation = resolveAssistantErrorPresentation(msg);
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
{ t: "assistant: ", s: "success" },
|
|
1084
|
-
{ t: presentation.text, s: "dim" },
|
|
1085
|
-
];
|
|
1086
|
-
}
|
|
1134
|
+
const head: EntryPart = { t: "assistant: ", s: assistantLabelColor() };
|
|
1135
|
+
if (presentation.kind === "compact-recovered") return [head, { t: presentation.text, s: "dim" }];
|
|
1087
1136
|
const msgWithContent = msg as { content?: unknown; stopReason?: string; errorMessage?: string };
|
|
1088
1137
|
const textContent = normalize(this.#extractContent(msgWithContent.content));
|
|
1089
|
-
const head: EntryPart = { t: "assistant: ", s: "success" };
|
|
1090
1138
|
if (textContent) return [head, { t: textContent }];
|
|
1091
1139
|
if (presentation.kind === "full") {
|
|
1092
1140
|
return [head, { t: normalize(presentation.text).slice(0, 80), s: "error" }];
|
|
@@ -1110,10 +1158,16 @@ class TreeList implements Component {
|
|
|
1110
1158
|
if (entry.customType === "advisor") {
|
|
1111
1159
|
const { qualifier, text } = advisorTreeDisplay(entry.details);
|
|
1112
1160
|
const label = qualifier ? `advisor (${qualifier}): ` : "advisor: ";
|
|
1113
|
-
return [
|
|
1161
|
+
return [
|
|
1162
|
+
{ t: label, s: "customMessageLabel" },
|
|
1163
|
+
{ t: normalize(text), s: "muted" },
|
|
1164
|
+
];
|
|
1114
1165
|
}
|
|
1115
1166
|
const content = stripSystemWrapperTags(this.#joinTextContent(entry.content));
|
|
1116
|
-
return [
|
|
1167
|
+
return [
|
|
1168
|
+
{ t: `[${entry.customType}]: `, s: "customMessageLabel" },
|
|
1169
|
+
{ t: normalize(content), s: "muted" },
|
|
1170
|
+
];
|
|
1117
1171
|
}
|
|
1118
1172
|
case "compaction": {
|
|
1119
1173
|
const tokens = Math.round(entry.tokensBefore / 1000);
|
|
@@ -43,7 +43,7 @@ import { editorKey, editorKeys } from "../chrome/keybinding-hints";
|
|
|
43
43
|
import type { TspSpan, TspTableColumn, TspText, TspTone } from "@oh-my-pi/pi-wire";
|
|
44
44
|
import { col, elapsed, node, span, text } from "../native/describe";
|
|
45
45
|
import { type DescribeContext, leafKey, type NativeChild, type NativeNode, type NativeUiEvent } from "../native/node";
|
|
46
|
-
import { actionBar, actionButton } from "../native/overlay";
|
|
46
|
+
import { actionBar, actionButton, segmentedTabs } from "../native/overlay";
|
|
47
47
|
|
|
48
48
|
/** Local calendar-day activity consumed by the usage heatmap. */
|
|
49
49
|
export interface DailyActivityPoint {
|
|
@@ -1135,8 +1135,7 @@ export class UsageDashboardComponent implements Component {
|
|
|
1135
1135
|
);
|
|
1136
1136
|
}
|
|
1137
1137
|
const children: NativeChild[] = [
|
|
1138
|
-
|
|
1139
|
-
"tabs",
|
|
1138
|
+
segmentedTabs(
|
|
1140
1139
|
{
|
|
1141
1140
|
items: [
|
|
1142
1141
|
{ id: "overview", label: "Overview" },
|
|
@@ -1144,7 +1143,6 @@ export class UsageDashboardComponent implements Component {
|
|
|
1144
1143
|
],
|
|
1145
1144
|
active: this.#view,
|
|
1146
1145
|
},
|
|
1147
|
-
undefined,
|
|
1148
1146
|
"tabs",
|
|
1149
1147
|
),
|
|
1150
1148
|
node("spacer", { grow: 1 }),
|