@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
|
@@ -29,13 +29,13 @@ import type { SpaceHoldHandler } from "../space-hold";
|
|
|
29
29
|
import { sanitizeErrorLine } from "../chrome/error-block";
|
|
30
30
|
import { sanitizeDisplayLine, sanitizeDisplayText } from "./extensions/display-text";
|
|
31
31
|
import { editorKey, editorKeys, keyHint, rawKeyHint } from "../chrome/keybinding-hints";
|
|
32
|
-
import { formatKeyHint } from "../app-keybindings";
|
|
32
|
+
import { formatKeyHint, type KeyId } from "../app-keybindings";
|
|
33
33
|
import { bottomBorder, row, topBorder } from "../chrome/overlay-box";
|
|
34
34
|
import { padToWidth } from "../render/utils";
|
|
35
35
|
import { SplitPane } from "../components/layout/split-pane";
|
|
36
36
|
import { clampSelection, contentRowWidth, padLinesToHeight, renderScrollableList } from "../chrome/selector-helpers";
|
|
37
|
-
import type { TspSpan } from "@oh-my-pi/pi-wire";
|
|
38
|
-
import type { NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
37
|
+
import type { TspScrollBy, TspSpan } from "@oh-my-pi/pi-wire";
|
|
38
|
+
import type { NativeChild, NativeNode, NativeScroll, NativeUiEvent } from "../native/node";
|
|
39
39
|
import { col, md, node, span, text } from "../native/describe";
|
|
40
40
|
import { actionHint, hintsRow, type NativeHint, statusHintsRow } from "../native/overlay";
|
|
41
41
|
|
|
@@ -46,10 +46,17 @@ interface BtwHistoryPanelOptions {
|
|
|
46
46
|
onCancel: (record: BtwHistoryRecord) => void;
|
|
47
47
|
canFollowUp?: (record: BtwHistoryRecord) => boolean;
|
|
48
48
|
onFollowUp?: (record: BtwHistoryRecord, question: string, signal: AbortSignal) => Promise<boolean>;
|
|
49
|
-
/**
|
|
49
|
+
/** Configured push-to-talk keys for a follow-up composer. */
|
|
50
|
+
spaceHoldKeys?: readonly KeyId[];
|
|
50
51
|
spaceHold?: (input: Input) => SpaceHoldHandler;
|
|
51
52
|
requestRender: () => void;
|
|
52
53
|
getHeight: () => number;
|
|
54
|
+
/**
|
|
55
|
+
* Esc closes the panel even while the selected answer runs (it keeps
|
|
56
|
+
* streaming in the background) and `x` cancels it, instead of Esc cancelling
|
|
57
|
+
* first. For a host with no inline panel to fall back to (Tern).
|
|
58
|
+
*/
|
|
59
|
+
escapeHides?: boolean;
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
interface FollowUpComposer {
|
|
@@ -78,6 +85,7 @@ interface BtwHistoryNativeMemo {
|
|
|
78
85
|
followUpPending: boolean;
|
|
79
86
|
canFollowUp: boolean;
|
|
80
87
|
copied: boolean;
|
|
88
|
+
scroll: NativeScroll | undefined;
|
|
81
89
|
node: NativeNode;
|
|
82
90
|
}
|
|
83
91
|
|
|
@@ -108,6 +116,8 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
108
116
|
#lastCopiedId: string | undefined;
|
|
109
117
|
#lastCopiedText: string | undefined;
|
|
110
118
|
#native: BtwHistoryNativeMemo | undefined;
|
|
119
|
+
/** The last answer-pane scroll key, forwarded natively (see {@link NativeNode.scroll}). */
|
|
120
|
+
#scroll: NativeScroll | undefined;
|
|
111
121
|
readonly #detail = new ScrollView([], {
|
|
112
122
|
height: 1,
|
|
113
123
|
scrollbar: "auto",
|
|
@@ -159,6 +169,7 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
159
169
|
this.#options = options;
|
|
160
170
|
this.#records = options.records;
|
|
161
171
|
this.#selectedId = options.records[0]?.id;
|
|
172
|
+
if (options.records.length === 1) this.#focus = "answer";
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
get focused(): boolean {
|
|
@@ -187,6 +198,8 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
187
198
|
this.#followLatest = false;
|
|
188
199
|
this.#detail.scrollToTop();
|
|
189
200
|
}
|
|
201
|
+
// One side question has nothing to pick: its answer keeps the arrow keys.
|
|
202
|
+
if (records.length === 1) this.#focus = "answer";
|
|
190
203
|
this.#options.requestRender();
|
|
191
204
|
}
|
|
192
205
|
|
|
@@ -247,9 +260,30 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
247
260
|
return true;
|
|
248
261
|
}
|
|
249
262
|
|
|
263
|
+
/** Select a record with its answer pane focused, read from the top (a just-asked /btw). */
|
|
264
|
+
showRecord(recordId: string): void {
|
|
265
|
+
const index = this.#records.findIndex(record => record.id === recordId);
|
|
266
|
+
if (index === -1) return;
|
|
267
|
+
this.#select(index);
|
|
268
|
+
this.#focus = "answer";
|
|
269
|
+
this.#options.requestRender();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Jump the answer pane to its bottom once (opening or sending a follow-up):
|
|
274
|
+
* text mode keeps following the newest row at render until a scroll key;
|
|
275
|
+
* natively the sheet body scrolls to its end. Tern reports no wheel scroll,
|
|
276
|
+
* so streamed updates never re-pin it there and a reader is never yanked.
|
|
277
|
+
*/
|
|
278
|
+
#jumpToEnd(): void {
|
|
279
|
+
this.#followLatest = true;
|
|
280
|
+
this.#scroll = { by: "end", n: (this.#scroll?.n ?? 0) + 1 };
|
|
281
|
+
}
|
|
282
|
+
|
|
250
283
|
#openComposer(record: BtwHistoryRecord): void {
|
|
251
284
|
const input = new Input();
|
|
252
285
|
input.prompt = theme.fg("accent", "Follow up: ");
|
|
286
|
+
if (this.#options.spaceHoldKeys !== undefined) input.spaceHold.keys = this.#options.spaceHoldKeys;
|
|
253
287
|
input.spaceHold.handler = this.#options.spaceHold?.(input);
|
|
254
288
|
const composer: FollowUpComposer = { recordId: record.id, input, abortController: new AbortController() };
|
|
255
289
|
input.onEscape = () => {
|
|
@@ -261,6 +295,8 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
261
295
|
};
|
|
262
296
|
this.#composer = composer;
|
|
263
297
|
this.#focus = "answer";
|
|
298
|
+
// Following up reads from the newest turn, right above the composer.
|
|
299
|
+
this.#jumpToEnd();
|
|
264
300
|
this.#options.requestRender();
|
|
265
301
|
}
|
|
266
302
|
|
|
@@ -286,7 +322,7 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
286
322
|
this.#composer = undefined;
|
|
287
323
|
this.#selectedId = composer.recordId;
|
|
288
324
|
this.#focus = "answer";
|
|
289
|
-
this.#
|
|
325
|
+
this.#jumpToEnd();
|
|
290
326
|
} else {
|
|
291
327
|
composer.notice = `Follow-up was not started. Your draft is kept; ${editorKey("tui.input.submit")} to retry.`;
|
|
292
328
|
}
|
|
@@ -300,6 +336,11 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
300
336
|
}
|
|
301
337
|
}
|
|
302
338
|
|
|
339
|
+
/** Only an active follow-up composer can claim its configured push-to-talk keys. */
|
|
340
|
+
capturesInput(data: string): boolean {
|
|
341
|
+
return this.#composer?.input.capturesInput(data) ?? false;
|
|
342
|
+
}
|
|
343
|
+
|
|
303
344
|
/** Enhanced clipboard pastes belong only to the active composer. */
|
|
304
345
|
pasteText(text: string): void {
|
|
305
346
|
if (!this.#composer) return;
|
|
@@ -314,11 +355,17 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
314
355
|
return;
|
|
315
356
|
}
|
|
316
357
|
const record = this.#selected();
|
|
358
|
+
const running = record !== undefined && getBtwLatestTurn(record).status === "running" ? record : undefined;
|
|
359
|
+
const hides = this.#options.escapeHides === true;
|
|
317
360
|
if (matchesSelectCancel(data) || matchesKey(data, "escape")) {
|
|
318
|
-
if (
|
|
361
|
+
if (running && !hides) this.#options.onCancel(running);
|
|
319
362
|
else this.#options.onClose();
|
|
320
363
|
return;
|
|
321
364
|
}
|
|
365
|
+
if (hides && matchesKey(data, "x")) {
|
|
366
|
+
if (running) this.#options.onCancel(running);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
322
369
|
if (matchesKey(data, "f") || matchesKey(data, "enter")) {
|
|
323
370
|
if (record) this.openFollowUp(record.id);
|
|
324
371
|
return;
|
|
@@ -335,11 +382,12 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
335
382
|
matchesKey(data, "ctrl+_") ||
|
|
336
383
|
data === String.fromCharCode(31)
|
|
337
384
|
) {
|
|
338
|
-
|
|
385
|
+
// One side question has nothing to pick: its answer keeps the arrow keys.
|
|
386
|
+
if (this.#records.length > 1) this.#focus = this.#focus === "list" ? "answer" : "list";
|
|
339
387
|
} else if (matchesKey(data, "right")) {
|
|
340
388
|
this.#focus = "answer";
|
|
341
389
|
} else if (matchesKey(data, "left")) {
|
|
342
|
-
this.#focus = "list";
|
|
390
|
+
if (this.#records.length > 1) this.#focus = "list";
|
|
343
391
|
} else if (this.#focus === "list") {
|
|
344
392
|
const index = this.#selectedIndex();
|
|
345
393
|
if (matchesSelectUp(data)) this.#select(index - 1);
|
|
@@ -350,11 +398,25 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
350
398
|
else if (matchesKey(data, "end")) this.#select(this.#records.length - 1);
|
|
351
399
|
else return;
|
|
352
400
|
} else {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
401
|
+
const by: TspScrollBy | undefined = matchesSelectUp(data)
|
|
402
|
+
? "line-up"
|
|
403
|
+
: matchesSelectDown(data)
|
|
404
|
+
? "line-down"
|
|
405
|
+
: matchesSelectPageUp(data)
|
|
406
|
+
? "page-up"
|
|
407
|
+
: matchesSelectPageDown(data)
|
|
408
|
+
? "page-down"
|
|
409
|
+
: matchesKey(data, "home")
|
|
410
|
+
? "start"
|
|
411
|
+
: matchesKey(data, "end")
|
|
412
|
+
? "end"
|
|
413
|
+
: undefined;
|
|
414
|
+
if (by === "line-up") this.#detail.scroll(-1);
|
|
415
|
+
else if (by === "line-down") this.#detail.scroll(1);
|
|
416
|
+
else if (by === "page-up") this.#detail.page(-1);
|
|
417
|
+
else if (by === "page-down") this.#detail.page(1);
|
|
357
418
|
else if (!this.#detail.handleScrollKey(data)) return;
|
|
419
|
+
if (by) this.#scroll = { by, n: (this.#scroll?.n ?? 0) + 1 };
|
|
358
420
|
this.#followLatest = matchesKey(data, "end");
|
|
359
421
|
}
|
|
360
422
|
this.#options.requestRender();
|
|
@@ -378,31 +440,39 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
378
440
|
memo.notice === composer?.notice &&
|
|
379
441
|
memo.followUpPending === this.#followUpPending &&
|
|
380
442
|
memo.canFollowUp === canFollowUp &&
|
|
381
|
-
memo.copied === copied
|
|
443
|
+
memo.copied === copied &&
|
|
444
|
+
memo.scroll === this.#scroll
|
|
382
445
|
) {
|
|
383
446
|
return memo.node;
|
|
384
447
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
448
|
+
// Plain columns, not `section`s: Tern draws a section as a foldable `##` heading,
|
|
449
|
+
// and a flex item's min-content width (a long code line in the answer) would
|
|
450
|
+
// crush the list pane. The answer pane may shrink to nothing; the list may not.
|
|
451
|
+
// With one side question the list and the pane labels are noise: the answer alone.
|
|
452
|
+
const showList = this.#records.length > 1;
|
|
453
|
+
const detail = this.#describeDetail(record, copied);
|
|
454
|
+
const answerPane: NativeNode = {
|
|
455
|
+
...node(
|
|
456
|
+
"col",
|
|
457
|
+
{ gap: "sm", grow: 1, basis: 0, min: { w: 0 } },
|
|
458
|
+
showList ? [text(this.#describeFocusLabel("answer"), { truncate: "end", lines: 1 }), detail] : [detail],
|
|
459
|
+
"details",
|
|
460
|
+
),
|
|
461
|
+
// Natively the sheet body is the scroller; arrow/page keys on the answer pane move it.
|
|
462
|
+
scroll: this.#scroll,
|
|
463
|
+
};
|
|
464
|
+
const panes: NativeChild[] = showList
|
|
465
|
+
? [
|
|
390
466
|
node(
|
|
391
|
-
"
|
|
392
|
-
{
|
|
393
|
-
[this.#describeList()],
|
|
467
|
+
"col",
|
|
468
|
+
{ gap: "sm", shrink: 0, basis: 0.3, max: { w: "40ch" } },
|
|
469
|
+
[text(this.#describeFocusLabel("list"), { truncate: "end", lines: 1 }), this.#describeList()],
|
|
394
470
|
"history",
|
|
395
471
|
),
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
"details",
|
|
401
|
-
),
|
|
402
|
-
],
|
|
403
|
-
"body",
|
|
404
|
-
),
|
|
405
|
-
];
|
|
472
|
+
answerPane,
|
|
473
|
+
]
|
|
474
|
+
: [answerPane];
|
|
475
|
+
const children: NativeChild[] = [node("row", { gap: "lg", align: "start" }, panes, "body")];
|
|
406
476
|
if (composer) {
|
|
407
477
|
composer.input.focused = this.#focused;
|
|
408
478
|
const lines: NativeChild[] = [
|
|
@@ -427,6 +497,7 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
427
497
|
followUpPending: this.#followUpPending,
|
|
428
498
|
canFollowUp,
|
|
429
499
|
copied,
|
|
500
|
+
scroll: this.#scroll,
|
|
430
501
|
node: described,
|
|
431
502
|
};
|
|
432
503
|
return described;
|
|
@@ -492,10 +563,21 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
492
563
|
const answer = turn.answer.trim()
|
|
493
564
|
? md(sanitizeDisplayText(turn.answer), { stream: turn.status === "running" })
|
|
494
565
|
: text([span(turn.status === "running" ? "Waiting for response…" : "No answer text.", "dim")]);
|
|
566
|
+
// Plain labels, not `section`s (Tern draws those as foldable `##` headings);
|
|
567
|
+
// the question stays markdown so its inline code and links render.
|
|
495
568
|
const children: NativeChild[] = [
|
|
496
|
-
text(
|
|
497
|
-
|
|
498
|
-
|
|
569
|
+
text(
|
|
570
|
+
[
|
|
571
|
+
span("Question", "accent strong"),
|
|
572
|
+
span(" · ", "dim"),
|
|
573
|
+
span(status.label, status.color),
|
|
574
|
+
span(` · ${this.#dateFormat.format(turn.createdAt)}`, "dim"),
|
|
575
|
+
],
|
|
576
|
+
{ wrap: "word" },
|
|
577
|
+
),
|
|
578
|
+
md(sanitizeDisplayText(turn.question)),
|
|
579
|
+
text([span("Answer", "accent strong")]),
|
|
580
|
+
answer,
|
|
499
581
|
];
|
|
500
582
|
if (turn.error) children.push(text([span(sanitizeErrorLine(turn.error), "error")], { wrap: "word" }));
|
|
501
583
|
if (turn.status === "interrupted") children.push(text([span("Not resumed in this view.", "muted")]));
|
|
@@ -510,9 +592,11 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
510
592
|
]);
|
|
511
593
|
}
|
|
512
594
|
const latest = record ? getBtwLatestTurn(record) : undefined;
|
|
595
|
+
const running = latest?.status === "running";
|
|
513
596
|
const hints: (NativeHint | undefined)[] = [
|
|
514
|
-
actionHint("tui.select.cancel",
|
|
515
|
-
{ keys: ["
|
|
597
|
+
actionHint("tui.select.cancel", running && !this.#options.escapeHides ? "cancel" : "close"),
|
|
598
|
+
running && this.#options.escapeHides ? { keys: ["x"], label: "cancel" } : undefined,
|
|
599
|
+
this.#records.length > 1 ? { keys: ["tab", "ctrl+/"], label: "switch pane" } : undefined,
|
|
516
600
|
actionHint(["tui.select.up", "tui.select.down"], this.#focus === "list" ? "select" : "scroll"),
|
|
517
601
|
];
|
|
518
602
|
if (canFollowUp) hints.push({ keys: ["f", "enter"], label: "follow up" });
|
|
@@ -689,14 +773,16 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
689
773
|
const record = this.#selected();
|
|
690
774
|
const composer = this.#composer;
|
|
691
775
|
const latest = record ? getBtwLatestTurn(record) : undefined;
|
|
776
|
+
const running = latest?.status === "running";
|
|
692
777
|
const actions = composer
|
|
693
778
|
? [
|
|
694
779
|
keyHint("tui.input.submit", this.#followUpPending ? "starting…" : "send"),
|
|
695
780
|
keyHint("tui.select.cancel", "cancel"),
|
|
696
781
|
]
|
|
697
782
|
: [
|
|
698
|
-
keyHint("tui.select.cancel",
|
|
699
|
-
rawKeyHint(
|
|
783
|
+
keyHint("tui.select.cancel", running && !this.#options.escapeHides ? "cancel" : "close"),
|
|
784
|
+
...(running && this.#options.escapeHides ? [rawKeyHint("x", "cancel")] : []),
|
|
785
|
+
...(this.#records.length > 1 ? [rawKeyHint(["tab", "ctrl+/"], "switch pane")] : []),
|
|
700
786
|
];
|
|
701
787
|
if (!composer) {
|
|
702
788
|
if (record && this.#canFollowUp(record)) actions.push(rawKeyHint(["f", "enter"], "follow up"));
|
|
@@ -274,6 +274,7 @@ export interface CopySelectorDeps {
|
|
|
274
274
|
cwd: string;
|
|
275
275
|
hideThinkingBlock?: () => boolean;
|
|
276
276
|
proseOnlyThinking?: () => boolean;
|
|
277
|
+
expandThinkingBlocks?: () => boolean;
|
|
277
278
|
linkTargets?: ReadonlyMap<string, string>;
|
|
278
279
|
requestRender: () => void;
|
|
279
280
|
/** Replaces the "Copy" header when the picker is reused for another purpose. */
|
|
@@ -379,6 +380,7 @@ export class CopySelectorComponent implements Component {
|
|
|
379
380
|
cwd: this.deps.cwd,
|
|
380
381
|
hideThinkingBlock: this.deps.hideThinkingBlock,
|
|
381
382
|
proseOnlyThinking: this.deps.proseOnlyThinking,
|
|
383
|
+
expandThinkingBlocks: this.deps.expandThinkingBlocks,
|
|
382
384
|
linkTargets: this.deps.linkTargets,
|
|
383
385
|
requestRender: this.deps.requestRender,
|
|
384
386
|
});
|
|
@@ -25,7 +25,7 @@ import { Memo } from "../../native/memo";
|
|
|
25
25
|
import type { DescribeContext, NativeNode, NativeScroll, NativeUiEvent } from "../../native/node";
|
|
26
26
|
import { actionBar, actionButton } from "../../native/overlay";
|
|
27
27
|
import { CLOSE_ACTION, type PickerEvent, picker, pickerAction, pickerEvent } from "../../native/picker";
|
|
28
|
-
import { matchesKey } from "../../keys";
|
|
28
|
+
import { type KeyId, matchesKey } from "../../keys";
|
|
29
29
|
import { parseSgrMouse } from "../../mouse";
|
|
30
30
|
import { SplitPane, type SplitPaneHit } from "../../components/layout/split-pane";
|
|
31
31
|
import { Stack } from "../../components/layout/stack";
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
matchesSelectPageUp,
|
|
42
42
|
} from "../../keybinding-matchers";
|
|
43
43
|
import { expandKeyHint } from "../../render/render-utils";
|
|
44
|
-
import { formatKeyHint, formatKeyHints } from "../../app-keybindings";
|
|
44
|
+
import { formatKeyHint, formatKeyHints, formatTooltipKey } from "../../app-keybindings";
|
|
45
45
|
import { boundKeys, editorKeys, interruptKey } from "../../chrome/keybinding-hints";
|
|
46
46
|
import { bottomBorder, divider, PanelRows, row, topBorder } from "../../chrome/overlay-box";
|
|
47
47
|
import { ExtensionList, type ExtensionListSwitch } from "./extension-list";
|
|
@@ -694,7 +694,7 @@ export class ExtensionDashboard implements Component {
|
|
|
694
694
|
];
|
|
695
695
|
}
|
|
696
696
|
|
|
697
|
-
#expandKey():
|
|
697
|
+
#expandKey(): KeyId {
|
|
698
698
|
return boundKeys("app.tools.expand", ["ctrl+o"])[0] ?? "ctrl+o";
|
|
699
699
|
}
|
|
700
700
|
|
|
@@ -714,7 +714,7 @@ export class ExtensionDashboard implements Component {
|
|
|
714
714
|
node("icon", {
|
|
715
715
|
name: "x",
|
|
716
716
|
role: "omp.app.ibtn",
|
|
717
|
-
title: `Close ${
|
|
717
|
+
title: `Close ${formatTooltipKey(boundKeys("app.interrupt", ["escape"])[0] ?? "escape")}`,
|
|
718
718
|
aria: "Close",
|
|
719
719
|
actions: { click: "close" },
|
|
720
720
|
}),
|
|
@@ -17,7 +17,7 @@ import { formatCoarseDuration } from "../chrome/format";
|
|
|
17
17
|
import { centerLine } from "../utils";
|
|
18
18
|
import { theme } from "../theme/theme";
|
|
19
19
|
import { matchesAppInterrupt } from "../keybinding-matchers";
|
|
20
|
-
import { formatKeyHint } from "../app-keybindings";
|
|
20
|
+
import { formatKeyHint, formatTooltipKey } from "../app-keybindings";
|
|
21
21
|
import { boundKeys, interruptKey } from "../chrome/keybinding-hints";
|
|
22
22
|
import type { NativeNode, NativeUiEvent } from "../native/node";
|
|
23
23
|
import { col, node, row, span, text } from "../native/describe";
|
|
@@ -198,7 +198,7 @@ export class PauseScreenComponent implements Component, OverlayFocusOwner {
|
|
|
198
198
|
actionButton("Resume", "resume", {
|
|
199
199
|
keys: resumeKey,
|
|
200
200
|
tone: "accent",
|
|
201
|
-
title: `Resume ${resumeKey} · enter · space`,
|
|
201
|
+
title: `Resume ${formatTooltipKey(resumeKey)} · enter · space`,
|
|
202
202
|
}),
|
|
203
203
|
]),
|
|
204
204
|
);
|
|
@@ -83,6 +83,7 @@ export interface RewindSelectorDeps {
|
|
|
83
83
|
cwd: string;
|
|
84
84
|
hideThinkingBlock?: () => boolean;
|
|
85
85
|
proseOnlyThinking?: () => boolean;
|
|
86
|
+
expandThinkingBlocks?: () => boolean;
|
|
86
87
|
linkTargets?: ReadonlyMap<string, string>;
|
|
87
88
|
requestRender: () => void;
|
|
88
89
|
/** Sibling branch paths of `entryId`'s turn (excluding the turn itself). */
|
|
@@ -226,6 +227,7 @@ export class RewindSelectorComponent implements Component {
|
|
|
226
227
|
cwd: this.deps.cwd,
|
|
227
228
|
hideThinkingBlock: this.deps.hideThinkingBlock,
|
|
228
229
|
proseOnlyThinking: this.deps.proseOnlyThinking,
|
|
230
|
+
expandThinkingBlocks: this.deps.expandThinkingBlocks,
|
|
229
231
|
linkTargets: this.deps.linkTargets,
|
|
230
232
|
requestRender: this.deps.requestRender,
|
|
231
233
|
});
|
|
@@ -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 { actionButton } from "../native/overlay";
|
|
46
|
+
import { actionBar, actionButton } from "../native/overlay";
|
|
47
47
|
|
|
48
48
|
/** Local calendar-day activity consumed by the usage heatmap. */
|
|
49
49
|
export interface DailyActivityPoint {
|
|
@@ -935,9 +935,9 @@ export class UsageDashboardComponent implements Component {
|
|
|
935
935
|
* The sheet body (the terminal's `lg` overlay is the frame): a head row
|
|
936
936
|
* (checked ago, Overview/Details tabs, Refresh), then either the provider
|
|
937
937
|
* grid of frames with window meters and the activity heatmap, or the
|
|
938
|
-
* per-provider detail tables
|
|
939
|
-
* `progress`/`table` on terminals without
|
|
940
|
-
* inputs change.
|
|
938
|
+
* per-provider detail tables, then Close like the other report sheets.
|
|
939
|
+
* `meter`/`chart` fall back to `progress`/`table` on terminals without
|
|
940
|
+
* them. Rebuilt only when its inputs change.
|
|
941
941
|
*/
|
|
942
942
|
describe(cx: DescribeContext): NativeNode {
|
|
943
943
|
const meter = cx.supports("meter");
|
|
@@ -947,15 +947,20 @@ export class UsageDashboardComponent implements Component {
|
|
|
947
947
|
return cache.node;
|
|
948
948
|
}
|
|
949
949
|
const body = this.#view === "detail" ? this.#describeDetail() : this.#describeOverview(meter, chart);
|
|
950
|
-
|
|
950
|
+
// Esc leaves Details for Overview first, so only Overview's Close wears its keycap.
|
|
951
|
+
const close = actionButton("Close", "close", this.#view === "overview" ? { keys: "escape" } : {});
|
|
952
|
+
const root = col([this.#describeHead(), node("col", { gap: "lg" }, body, this.#view), actionBar([null, close])], {
|
|
953
|
+
gap: "lg",
|
|
954
|
+
});
|
|
951
955
|
this.#nativeCache = { revision: this.#revision, meter, chart, node: root };
|
|
952
956
|
return root;
|
|
953
957
|
}
|
|
954
958
|
|
|
955
|
-
/** Tab clicks switch views like Enter/Esc; the Refresh button runs `r
|
|
959
|
+
/** Tab clicks switch views like Enter/Esc; the Refresh button runs `r`; Close closes from either view. */
|
|
956
960
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
957
961
|
if (event.type === "action") {
|
|
958
962
|
if (event.act === "refresh") void this.#refresh();
|
|
963
|
+
else if (event.act === "close") this.#close();
|
|
959
964
|
return;
|
|
960
965
|
}
|
|
961
966
|
if (event.type !== "select" && event.type !== "activate") return;
|
|
@@ -1379,8 +1384,7 @@ export class UsageDashboardComponent implements Component {
|
|
|
1379
1384
|
this.#setView("overview");
|
|
1380
1385
|
return;
|
|
1381
1386
|
}
|
|
1382
|
-
this
|
|
1383
|
-
this.#options.onClose();
|
|
1387
|
+
this.#close();
|
|
1384
1388
|
return;
|
|
1385
1389
|
}
|
|
1386
1390
|
if (matchesKey(data, "r")) {
|
|
@@ -1403,4 +1407,9 @@ export class UsageDashboardComponent implements Component {
|
|
|
1403
1407
|
this.#options.requestRender();
|
|
1404
1408
|
} else if (matchesKey(data, "end")) this.#scrollBy(Number.MAX_SAFE_INTEGER);
|
|
1405
1409
|
}
|
|
1410
|
+
|
|
1411
|
+
#close(): void {
|
|
1412
|
+
this.dispose();
|
|
1413
|
+
this.#options.onClose();
|
|
1414
|
+
}
|
|
1406
1415
|
}
|
|
@@ -5,17 +5,19 @@ import { formatDuration, formatNumber } from "@oh-my-pi/pi-utils";
|
|
|
5
5
|
import { theme } from "../theme/theme";
|
|
6
6
|
import { formatMetricRow, MetricRow, type MetricSpec } from "../components/metric";
|
|
7
7
|
import { node, row, span, text } from "../native/describe";
|
|
8
|
-
import type { NativeChild, NativeNode } from "../native/node";
|
|
8
|
+
import type { DescribeContext, NativeChild, NativeNode } from "../native/node";
|
|
9
9
|
|
|
10
10
|
/** Below this the rate is nonsense (cached/instant responses yield absurd tok/s). */
|
|
11
11
|
const MIN_DURATION_MS = 100;
|
|
12
12
|
|
|
13
|
-
/** Local
|
|
14
|
-
function formatUsageTimestamp(ms: number): string {
|
|
13
|
+
/** Local ISO date with a wall-clock time; only the hour cycle follows the terminal. */
|
|
14
|
+
function formatUsageTimestamp(ms: number, hour12 = false): string {
|
|
15
15
|
const d = new Date(ms);
|
|
16
16
|
const pad = (n: number): string => String(n).padStart(2, "0");
|
|
17
17
|
const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
18
|
-
const time =
|
|
18
|
+
const time = hour12
|
|
19
|
+
? d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12 })
|
|
20
|
+
: `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
19
21
|
return `${date} ${time}`;
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -161,6 +163,8 @@ class UsageRowBlock extends Container {
|
|
|
161
163
|
readonly #timestamp: number | undefined;
|
|
162
164
|
readonly #turnElapsedMs: number | undefined;
|
|
163
165
|
#nativeNode: NativeNode | undefined;
|
|
166
|
+
/** The terminal's clock {@link #nativeNode} was described with. */
|
|
167
|
+
#nativeHour12: boolean | undefined;
|
|
164
168
|
|
|
165
169
|
constructor(usage: Usage, durationMs?: number, ttftMs?: number, timestamp?: number, turnElapsedMs?: number) {
|
|
166
170
|
super();
|
|
@@ -186,13 +190,23 @@ class UsageRowBlock extends Container {
|
|
|
186
190
|
* ttft 0.9s · 96 tok/s`): worded metrics instead of the ANSI icons, the
|
|
187
191
|
* full timestamp in the tooltip, and throughput as a `rate`.
|
|
188
192
|
*/
|
|
189
|
-
override describe(): NativeNode {
|
|
190
|
-
|
|
193
|
+
override describe(cx?: DescribeContext): NativeNode {
|
|
194
|
+
// The terminal's clock: the process locale doesn't reliably carry the user's 12/24-hour choice.
|
|
195
|
+
const hour12 = cx?.hour12;
|
|
196
|
+
if (this.#nativeNode && this.#nativeHour12 === hour12) return this.#nativeNode;
|
|
197
|
+
this.#nativeHour12 = hour12;
|
|
191
198
|
const usage = this.#usage;
|
|
192
199
|
const parts: string[] = [];
|
|
193
200
|
const timestamp = this.#timestamp;
|
|
194
201
|
const stamped = timestamp !== undefined && Number.isFinite(timestamp) && timestamp > 0;
|
|
195
|
-
|
|
202
|
+
// Only a 12-hour clock goes through the locale; 24-hour keeps the tooltip's own `HH:mm`.
|
|
203
|
+
if (stamped) {
|
|
204
|
+
parts.push(
|
|
205
|
+
hour12
|
|
206
|
+
? new Date(timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12 })
|
|
207
|
+
: formatUsageTimestamp(timestamp).slice(11, 16),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
196
210
|
if (this.#turnElapsedMs !== undefined && this.#turnElapsedMs > 0) {
|
|
197
211
|
parts.push(`Δ ${formatDuration(Math.round(this.#turnElapsedMs))}`);
|
|
198
212
|
}
|
|
@@ -211,7 +225,7 @@ class UsageRowBlock extends Container {
|
|
|
211
225
|
role: "omp.usage.turn",
|
|
212
226
|
gap: "none",
|
|
213
227
|
align: "baseline",
|
|
214
|
-
...(stamped ? { title: formatUsageTimestamp(timestamp) } : {}),
|
|
228
|
+
...(stamped ? { title: formatUsageTimestamp(timestamp, hour12) } : {}),
|
|
215
229
|
});
|
|
216
230
|
return this.#nativeNode;
|
|
217
231
|
}
|