@oh-my-pi/pi-tui 18.4.4 → 18.4.6
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 +46 -0
- package/dist/types/apps/git/sidebar.d.ts +2 -2
- package/dist/types/chat/recap-notice.d.ts +13 -0
- package/dist/types/components/editor.d.ts +15 -3
- package/dist/types/components/input.d.ts +11 -1
- package/dist/types/native/blobs.d.ts +1 -1
- package/dist/types/native/node.d.ts +63 -1
- package/dist/types/overlays/agent-hub-projection.d.ts +9 -0
- package/dist/types/overlays/ask-dialog.d.ts +9 -5
- package/dist/types/overlays/copy-selector.d.ts +0 -23
- package/dist/types/overlays/error-banner.d.ts +5 -2
- package/dist/types/overlays/hook-editor.d.ts +12 -0
- package/dist/types/overlays/jobs-panel.d.ts +25 -1
- package/dist/types/overlays/model-hub.d.ts +2 -0
- package/dist/types/overlays/rewind-selector.d.ts +17 -4
- package/dist/types/prompt/custom-editor.d.ts +16 -8
- package/dist/types/prompt/welcome.d.ts +3 -3
- package/dist/types/render/render-utils.d.ts +5 -0
- package/dist/types/status-line/component.d.ts +2 -0
- package/dist/types/status-line/host.d.ts +2 -0
- package/dist/types/status-line/metrics.d.ts +4 -0
- package/dist/types/status-line/types.d.ts +13 -2
- package/dist/types/tools/edit.d.ts +2 -0
- package/dist/types/tools/task.d.ts +7 -0
- package/dist/types/tui.d.ts +8 -1
- package/package.json +9 -9
- package/src/apps/git/sidebar.ts +15 -2
- package/src/chat/assistant-message.ts +2 -2
- package/src/chat/recap-notice.ts +30 -0
- package/src/chat/tool-execution.ts +19 -3
- package/src/chat/user-message.ts +2 -1
- package/src/components/editor.ts +115 -16
- package/src/components/input.ts +57 -1
- package/src/native/backend.ts +28 -10
- package/src/native/blobs.ts +1 -1
- package/src/native/encode.ts +1 -0
- package/src/native/node.ts +108 -1
- package/src/native/reconcile.ts +5 -4
- package/src/overlays/agent-hub-projection.ts +25 -0
- package/src/overlays/agent-hub.ts +4 -15
- package/src/overlays/ask-dialog.ts +20 -32
- package/src/overlays/copy-selector.ts +6 -6
- package/src/overlays/error-banner.ts +14 -3
- package/src/overlays/hook-editor.ts +59 -6
- package/src/overlays/jobs-panel.ts +132 -49
- package/src/overlays/model-browser.ts +56 -12
- package/src/overlays/model-hub.ts +82 -29
- package/src/overlays/rewind-selector.ts +235 -332
- package/src/overlays/session-selector.ts +19 -20
- package/src/prompt/custom-editor.ts +119 -30
- package/src/prompt/welcome.ts +4 -33
- package/src/render/render-utils.ts +51 -3
- package/src/status-line/component.ts +61 -30
- package/src/status-line/host.ts +2 -0
- package/src/status-line/metrics.ts +36 -5
- package/src/status-line/segments.ts +9 -1
- package/src/status-line/types.ts +13 -2
- package/src/terminal.ts +27 -6
- package/src/tools/edit.ts +23 -0
- package/src/tools/eval.ts +43 -11
- package/src/tools/grouped-file-output.ts +6 -0
- package/src/tools/task.ts +29 -4
- package/src/tui.ts +9 -1
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
* filter with the selection kept), Enter rewinds to the outlined item, A loads
|
|
25
25
|
* earlier turns without changing selection (stepping above the oldest replayed
|
|
26
26
|
* turn loads them too), Esc cancels.
|
|
27
|
+
*
|
|
28
|
+
* In a TSP terminal (Tern) the same replay is a page, not a sheet: a screen
|
|
29
|
+
* surface of the transcript's own blocks with `pick`/`drop` marks in place of
|
|
30
|
+
* the dotted outline (see `describeScreen`).
|
|
27
31
|
*/
|
|
28
32
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
29
33
|
import {
|
|
@@ -57,23 +61,12 @@ import {
|
|
|
57
61
|
positionRail,
|
|
58
62
|
userTurnLabel,
|
|
59
63
|
} from "../chat/transcript-outline";
|
|
60
|
-
import type {
|
|
61
|
-
import {
|
|
62
|
-
import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
63
|
-
import { actionHint, hintsRow
|
|
64
|
-
import { CLOSE_ACTION, type PickerEvent, picker, pickerAction, pickerEvent, pickerQuery } from "../native/picker";
|
|
64
|
+
import type { TspMark } from "@oh-my-pi/pi-wire";
|
|
65
|
+
import { kbd, node, span, text } from "../native/describe";
|
|
66
|
+
import type { DescribeContext, NativeChild, NativeNode, NativeScreen, NativeUiEvent } from "../native/node";
|
|
67
|
+
import { actionBar, actionButton, actionHint, hintsRow } from "../native/overlay";
|
|
65
68
|
import { isNativeRendering } from "../native/state";
|
|
66
|
-
import {
|
|
67
|
-
collectBlocks,
|
|
68
|
-
EARLIER_TURNS_KEY,
|
|
69
|
-
earlierTurnsItem,
|
|
70
|
-
TIMELINE_COLUMNS,
|
|
71
|
-
TimelineItems,
|
|
72
|
-
targetCopy,
|
|
73
|
-
timelineItem,
|
|
74
|
-
turnItem,
|
|
75
|
-
turnPreview,
|
|
76
|
-
} from "./copy-selector";
|
|
69
|
+
import { collectBlocks, targetCopy } from "./copy-selector";
|
|
77
70
|
|
|
78
71
|
/** One alternate branch at a divergence: its root and message path root → most-recent leaf. */
|
|
79
72
|
export interface BranchVariantPath {
|
|
@@ -106,12 +99,58 @@ interface SiblingColumn {
|
|
|
106
99
|
targets: OutlineTarget[];
|
|
107
100
|
/** Short label for the column header: the branch's first user prompt. */
|
|
108
101
|
label: string;
|
|
109
|
-
/** Native list items for this column, built on first describe. */
|
|
110
|
-
nativeItems?: NativeNode[];
|
|
111
|
-
/** Picker catalogue while this column's tab is open: the main items, then this branch's. */
|
|
112
|
-
pickerItems?: { main: readonly TspPickerItem[]; items: TspPickerItem[] };
|
|
113
102
|
}
|
|
114
103
|
|
|
104
|
+
/**
|
|
105
|
+
* A replayed block on the rewind page, carrying a transient `mark`: it
|
|
106
|
+
* describes as the block with the mark merged into its root props, and hands
|
|
107
|
+
* events and rendering to the block.
|
|
108
|
+
*/
|
|
109
|
+
class MarkedBlock implements Component {
|
|
110
|
+
mark: TspMark | undefined;
|
|
111
|
+
#memo: { node: NativeNode; mark: TspMark; out: NativeNode } | undefined;
|
|
112
|
+
|
|
113
|
+
constructor(readonly block: Component) {}
|
|
114
|
+
|
|
115
|
+
render(width: number): readonly string[] {
|
|
116
|
+
return this.block.render(width);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
invalidate(): void {
|
|
120
|
+
this.block.invalidate?.();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe(cx: DescribeContext): NativeNode | null {
|
|
124
|
+
const described = this.block.describe?.(cx) ?? null;
|
|
125
|
+
const mark = this.mark;
|
|
126
|
+
if (!described || !mark) return described;
|
|
127
|
+
const memo = this.#memo;
|
|
128
|
+
if (memo?.node === described && memo.mark === mark) return memo.out;
|
|
129
|
+
const out = node(described.k, { ...described.p, mark }, described.c, described.key);
|
|
130
|
+
this.#memo = { node: described, mark, out };
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
135
|
+
this.block.handleNativeEvent?.(event);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const kMarked = Symbol("rewind.marked");
|
|
140
|
+
|
|
141
|
+
/** A replayed block tagged with its page wrapper, so stepping reuses one wrapper per block. */
|
|
142
|
+
interface MarkTagged {
|
|
143
|
+
[kMarked]?: MarkedBlock;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The page's first row while older history is unreplayed: `a` loads it. */
|
|
147
|
+
const EARLIER_TURNS = node(
|
|
148
|
+
"row",
|
|
149
|
+
{ role: "omp.rewind.earlier", gap: "xs", align: "center" },
|
|
150
|
+
[kbd("a"), text([span("load earlier turns", "muted")])],
|
|
151
|
+
"earlier",
|
|
152
|
+
);
|
|
153
|
+
|
|
115
154
|
/** Blank columns between branch-strip columns. */
|
|
116
155
|
const STRIP_GAP = 2;
|
|
117
156
|
/** Duration of the branch-swap camera slide. */
|
|
@@ -155,14 +194,8 @@ export class RewindSelectorComponent implements Component {
|
|
|
155
194
|
#rowText = new WeakMap<readonly string[], string>();
|
|
156
195
|
/** Native filter haystack: lowercased turn text per main target (no rendered rows under TSP). */
|
|
157
196
|
#nativeTexts: { targets: OutlineTarget[]; texts: string[] } | undefined;
|
|
158
|
-
/** Last described
|
|
159
|
-
#
|
|
160
|
-
/** Main-path list items, rebuilt only when the replayed targets change. */
|
|
161
|
-
#nativeItems: { targets: OutlineTarget[]; truncated: boolean; items: NativeNode[] } | undefined;
|
|
162
|
-
/** Timeline picker items of the replayed main path. */
|
|
163
|
-
#pickerItems = new TimelineItems();
|
|
164
|
-
/** Last described picker and the state it was built from. */
|
|
165
|
-
#picker: { memo: string; targets: OutlineTarget[]; node: NativeNode } | undefined;
|
|
197
|
+
/** Last described bar and the state it was built from. */
|
|
198
|
+
#bar: { memo: string; targets: OutlineTarget[]; node: NativeNode } | undefined;
|
|
166
199
|
|
|
167
200
|
constructor(
|
|
168
201
|
entries: TranscriptEntry[],
|
|
@@ -579,338 +612,208 @@ export class RewindSelectorComponent implements Component {
|
|
|
579
612
|
}
|
|
580
613
|
|
|
581
614
|
// ========================================================================
|
|
582
|
-
// Native
|
|
615
|
+
// Native: the transcript as a page
|
|
583
616
|
// ========================================================================
|
|
584
617
|
|
|
585
|
-
/** A `picker` is its own sheet: the backend mounts it in `layer`, over the transcript. */
|
|
586
|
-
nativeSheet(cx: DescribeContext): boolean {
|
|
587
|
-
return cx.supports("picker");
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/** A branch tab click moves between the current path and its alternates, as Left/Right do. */
|
|
591
|
-
#showVariant(value: string | undefined): void {
|
|
592
|
-
const variant = Number(value);
|
|
593
|
-
if (!Number.isInteger(variant) || variant === this.#activeVariant) return;
|
|
594
|
-
if (variant < 0 || variant > this.#stripColumns().length) return;
|
|
595
|
-
this.#siblingSelected = 0;
|
|
596
|
-
if (variant === 0) {
|
|
597
|
-
this.#activeVariant = 0;
|
|
598
|
-
this.#stopSlide();
|
|
599
|
-
this.deps.requestRender();
|
|
600
|
-
} else {
|
|
601
|
-
this.#slideTo(variant);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
618
|
/**
|
|
606
|
-
*
|
|
607
|
-
*
|
|
619
|
+
* The rewind page: the replayed transcript fills a screen surface block by
|
|
620
|
+
* block, as the live one reads, the outlined turn marked `pick` under a
|
|
621
|
+
* "Continue from here" caption (revealed as the outline moves) and what the
|
|
622
|
+
* rewind drops marked `drop`. At a fork the region below it becomes a strip
|
|
623
|
+
* of branch columns, the current path first. The bar is this component's
|
|
624
|
+
* own node, docked under the page, so the filter field keeps the caret.
|
|
608
625
|
*/
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
switch (event.act) {
|
|
612
|
-
case "tab":
|
|
613
|
-
this.#showVariant(event.value);
|
|
614
|
-
return;
|
|
615
|
-
case "rewind":
|
|
616
|
-
if (this.#filter === undefined) this.#selectOutlined();
|
|
617
|
-
else this.#selectFiltered();
|
|
618
|
-
return;
|
|
619
|
-
case "lateral":
|
|
620
|
-
// A click steps back a user turn, or on to the next branch (wrapping to the current path).
|
|
621
|
-
if (this.#filter !== undefined || this.#stripColumns().length === 0) this.#left();
|
|
622
|
-
else if (this.#activeVariant < this.#stripColumns().length) this.#right();
|
|
623
|
-
else this.#showVariant("0");
|
|
624
|
-
return;
|
|
625
|
-
case "filter":
|
|
626
|
-
this.#openFilter();
|
|
627
|
-
return;
|
|
628
|
-
case "earlier":
|
|
629
|
-
this.#loadFullHistory();
|
|
630
|
-
return;
|
|
631
|
-
case "close":
|
|
632
|
-
if (this.#filter === undefined) this.deps.onCancel();
|
|
633
|
-
else this.#closeFilter();
|
|
634
|
-
return;
|
|
635
|
-
case "clear":
|
|
636
|
-
this.#closeFilter();
|
|
637
|
-
return;
|
|
638
|
-
default:
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
if (!this.#outline(event.item)) return;
|
|
643
|
-
this.deps.requestRender();
|
|
644
|
-
if (event.kind === "activate") {
|
|
645
|
-
if (this.#filter === undefined) this.#selectOutlined();
|
|
646
|
-
else this.#selectFiltered();
|
|
647
|
-
}
|
|
626
|
+
describeScreen(_cx: DescribeContext): NativeScreen {
|
|
627
|
+
return { role: "omp.rewind", main: this.#page(), dock: [this] };
|
|
648
628
|
}
|
|
649
629
|
|
|
650
|
-
/**
|
|
651
|
-
#
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
630
|
+
/** The page's blocks, marked for the current outline. */
|
|
631
|
+
#page(): NativeChild[] {
|
|
632
|
+
const blocks = this.#builder.container.children;
|
|
633
|
+
const page: NativeChild[] = [];
|
|
634
|
+
const filter = this.#filter;
|
|
635
|
+
if (filter !== undefined) {
|
|
636
|
+
// Only the matching turns of the current path, like the filtered frame.
|
|
637
|
+
const matches = this.#filterMatches();
|
|
638
|
+
for (const index of matches) {
|
|
639
|
+
const target = this.#targets[index]!;
|
|
640
|
+
const picked = index === this.#selected;
|
|
641
|
+
if (picked) page.push(this.#caption(this.#targets, index, "main"));
|
|
642
|
+
for (let i = target.start; i < target.end; i++)
|
|
643
|
+
page.push(this.#marked(blocks[i]!, picked ? "pick" : undefined));
|
|
658
644
|
}
|
|
645
|
+
if (matches.length === 0) {
|
|
646
|
+
page.push(text([span(`No turns match "${filter}"`, "muted")], { role: "omp.rewind.empty" }));
|
|
647
|
+
}
|
|
648
|
+
return page;
|
|
659
649
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
this.#stopSlide();
|
|
667
|
-
return true;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
handleNativeEvent(event: NativeUiEvent): void {
|
|
671
|
-
const picked = pickerEvent(event);
|
|
672
|
-
if (picked) {
|
|
673
|
-
this.#handlePickerEvent(picked);
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
if (event.type !== "select" && event.type !== "activate") return;
|
|
677
|
-
if (event.key === "branches") {
|
|
678
|
-
this.#showVariant(event.item);
|
|
679
|
-
return;
|
|
650
|
+
if (this.#truncated) page.push(EARLIER_TURNS);
|
|
651
|
+
const columns = this.#stripColumns();
|
|
652
|
+
const anchor = this.#targets[this.#selected];
|
|
653
|
+
if (columns.length === 0 || !anchor) {
|
|
654
|
+
this.#markRun(page, blocks, 0, this.#targets, this.#selected, "main");
|
|
655
|
+
return page;
|
|
680
656
|
}
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
this.#
|
|
690
|
-
this.#activeVariant
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
this.#
|
|
698
|
-
|
|
699
|
-
|
|
657
|
+
// Shared history above the fork at full width, then the branches side by side.
|
|
658
|
+
for (let i = 0; i < anchor.start; i++) page.push(this.#marked(blocks[i]!, undefined));
|
|
659
|
+
const count = columns.length + 1;
|
|
660
|
+
const current: NativeChild[] = [this.#columnHead(0, count, "current")];
|
|
661
|
+
this.#markRun(
|
|
662
|
+
current,
|
|
663
|
+
blocks,
|
|
664
|
+
anchor.start,
|
|
665
|
+
this.#targets,
|
|
666
|
+
this.#activeVariant === 0 ? this.#selected : -1,
|
|
667
|
+
"main",
|
|
668
|
+
);
|
|
669
|
+
const strip = [this.#column(0, current, "current")];
|
|
670
|
+
for (let index = 0; index < columns.length; index++) {
|
|
671
|
+
const column = columns[index]!;
|
|
672
|
+
const active = this.#activeVariant === index + 1;
|
|
673
|
+
const children: NativeChild[] = [this.#columnHead(index + 1, count, column.label)];
|
|
674
|
+
const picked = active ? this.#siblingSelected : -1;
|
|
675
|
+
this.#markRun(children, column.builder.container.children, 0, column.targets, picked, column.rootId);
|
|
676
|
+
strip.push(this.#column(index + 1, children, column.rootId));
|
|
700
677
|
}
|
|
701
|
-
|
|
702
|
-
|
|
678
|
+
page.push(node("row", { role: "omp.rewind.strip", gap: "lg", align: "start" }, strip, "strip"));
|
|
679
|
+
return page;
|
|
703
680
|
}
|
|
704
681
|
|
|
705
|
-
|
|
706
|
-
|
|
682
|
+
/**
|
|
683
|
+
* Push `blocks[from..]` marked for `targets[picked]` (-1: none): unmarked
|
|
684
|
+
* above it, then the caption and `pick` on its blocks, `drop` below.
|
|
685
|
+
*/
|
|
686
|
+
#markRun(
|
|
687
|
+
out: NativeChild[],
|
|
688
|
+
blocks: readonly Component[],
|
|
689
|
+
from: number,
|
|
690
|
+
targets: readonly OutlineTarget[],
|
|
691
|
+
picked: number,
|
|
692
|
+
column: string,
|
|
693
|
+
): void {
|
|
694
|
+
const target = targets[picked];
|
|
695
|
+
for (let i = from; i < blocks.length; i++) {
|
|
696
|
+
if (target && i === target.start) out.push(this.#caption(targets, picked, column));
|
|
697
|
+
const mark = !target || i < target.start ? undefined : i < target.end ? "pick" : "drop";
|
|
698
|
+
out.push(this.#marked(blocks[i]!, mark));
|
|
699
|
+
}
|
|
707
700
|
}
|
|
708
701
|
|
|
709
702
|
/**
|
|
710
|
-
* The
|
|
711
|
-
*
|
|
712
|
-
* as the preview under the drop warning.
|
|
703
|
+
* The line over the outlined turn: what Enter does and what it drops.
|
|
704
|
+
* Keyed by the turn, so every step adds a fresh one that scrolls into view.
|
|
713
705
|
*/
|
|
714
|
-
#
|
|
715
|
-
const
|
|
716
|
-
const
|
|
717
|
-
const
|
|
718
|
-
|
|
706
|
+
#caption(targets: readonly OutlineTarget[], picked: number, column: string): NativeNode {
|
|
707
|
+
const target = targets[picked]!;
|
|
708
|
+
const below = targets.length - picked - 1;
|
|
709
|
+
const spans = [
|
|
710
|
+
span(`${theme.icon.rewind} `, "accent"),
|
|
711
|
+
span(target.isUserTurn ? "Rewind to here" : "Continue from here", "accent strong"),
|
|
712
|
+
];
|
|
713
|
+
if (target.isUserTurn) spans.push(span(`${theme.sep.dot}the prompt returns to the editor`, "dim"));
|
|
714
|
+
spans.push(
|
|
715
|
+
span(
|
|
716
|
+
below > 0
|
|
717
|
+
? `${theme.sep.dot}${below} turn${below === 1 ? "" : "s"} below dropped`
|
|
718
|
+
: `${theme.sep.dot}nothing below to drop`,
|
|
719
|
+
"dim",
|
|
720
|
+
),
|
|
721
|
+
);
|
|
722
|
+
const caption = text(spans, { role: "omp.rewind.here", wrap: "none" });
|
|
723
|
+
return { ...caption, key: `here:${column}:${target.turnId}`, reveal: "start" };
|
|
724
|
+
}
|
|
719
725
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
if (!matches.includes(this.#selected)) props.selected = null;
|
|
743
|
-
}
|
|
744
|
-
if (columns.length > 0) {
|
|
745
|
-
props.tabs = [
|
|
746
|
-
{ id: "0", label: "Current" },
|
|
747
|
-
...columns.map((strip, index) => ({
|
|
748
|
-
id: String(index + 1),
|
|
749
|
-
label: strip.label.length > 32 ? `${strip.label.slice(0, 31)}…` : strip.label,
|
|
750
|
-
})),
|
|
751
|
-
];
|
|
752
|
-
props.tab = String(this.#activeVariant);
|
|
753
|
-
}
|
|
754
|
-
if (column) {
|
|
755
|
-
// The shared history above the fork, then the alternate branch.
|
|
756
|
-
if (column.pickerItems?.main !== main) {
|
|
757
|
-
column.pickerItems = { main, items: [...main, ...column.targets.map(timelineItem)] };
|
|
758
|
-
}
|
|
759
|
-
props.items = column.pickerItems.items;
|
|
760
|
-
props.order = [
|
|
761
|
-
...this.#targets.slice(0, this.#selected).map(target => target.turnId),
|
|
762
|
-
...column.targets.map(target => target.turnId),
|
|
763
|
-
];
|
|
764
|
-
props.selected = column.targets[this.#siblingSelected]?.turnId ?? null;
|
|
765
|
-
}
|
|
766
|
-
props.actions = compact([
|
|
767
|
-
pickerAction("rewind", "Rewind here", "enter", { primary: true }),
|
|
768
|
-
pickerAction("lateral", columns.length > 0 ? "Branches" : "User turns", ["left", "right"]),
|
|
769
|
-
filter === undefined ? pickerAction("filter", "Filter", "f") : undefined,
|
|
770
|
-
filter === undefined && this.#truncated ? pickerAction("earlier", "Earlier turns", "a") : undefined,
|
|
771
|
-
filter === undefined ? CLOSE_ACTION : { ...CLOSE_ACTION, label: "Show all" },
|
|
772
|
-
]);
|
|
773
|
-
|
|
774
|
-
const preview: NativeChild[] = [];
|
|
775
|
-
const outlined = column ? column.targets[this.#siblingSelected] : this.#targets[this.#selected];
|
|
776
|
-
if (outlined && props.selected !== null) {
|
|
777
|
-
// A user turn rewinds past itself (its text returns to the editor); anything else keeps itself.
|
|
778
|
-
const kept = column || this.#targets[this.#selected]!.isUserTurn ? this.#selected : this.#selected + 1;
|
|
779
|
-
const dropped = this.#targets.length - kept;
|
|
780
|
-
preview.push(
|
|
781
|
-
text(
|
|
782
|
-
dropped > 0
|
|
783
|
-
? [
|
|
784
|
-
span("Continue from here: everything below is dropped", "warning"),
|
|
785
|
-
span(`${theme.sep.dot}${dropped} turn${dropped === 1 ? "" : "s"}`, "dim"),
|
|
786
|
-
]
|
|
787
|
-
: [span("Continue from here: nothing below to drop", "warning")],
|
|
788
|
-
{ role: "omp.rewind.drop" },
|
|
789
|
-
),
|
|
790
|
-
...(column?.builder ?? this.#builder).container.children.slice(outlined.start, outlined.end),
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
const root = picker(props, preview);
|
|
794
|
-
this.#picker = { memo, targets: this.#targets, node: root };
|
|
795
|
-
return root;
|
|
726
|
+
/** One branch column of the strip; the active one carries the accent tone. */
|
|
727
|
+
#column(index: number, children: readonly NativeChild[], key: string): NativeNode {
|
|
728
|
+
const active = index === this.#activeVariant;
|
|
729
|
+
return node(
|
|
730
|
+
"col",
|
|
731
|
+
{ role: "omp.rewind.branch", gap: "lg", ...(active ? { tone: "accent" } : {}) },
|
|
732
|
+
children,
|
|
733
|
+
key,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** A strip column's caption: `⎇ i/n · label`. */
|
|
738
|
+
#columnHead(index: number, count: number, label: string): NativeNode {
|
|
739
|
+
const active = index === this.#activeVariant;
|
|
740
|
+
return text(
|
|
741
|
+
[
|
|
742
|
+
span(`${theme.icon.branch} `, active ? "accent" : "dim"),
|
|
743
|
+
span(`${index + 1}/${count}`, active ? "accent" : "dim"),
|
|
744
|
+
span(`${theme.sep.dot}${label}`, active ? "strong" : "dim"),
|
|
745
|
+
],
|
|
746
|
+
{ role: "omp.rewind.branch.head", wrap: "none" },
|
|
747
|
+
);
|
|
796
748
|
}
|
|
797
749
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const
|
|
801
|
-
|
|
750
|
+
/** `block` wrapped to carry `mark`; one wrapper per block, so ids and view state persist while stepping. */
|
|
751
|
+
#marked(block: Component & MarkTagged, mark: TspMark | undefined): MarkedBlock {
|
|
752
|
+
const marked = (block[kMarked] ??= new MarkedBlock(block));
|
|
753
|
+
marked.mark = mark;
|
|
754
|
+
return marked;
|
|
755
|
+
}
|
|
802
756
|
|
|
757
|
+
/** The bar docked under the page: position, key hints, Cancel and Rewind; the filter field while filtering. */
|
|
758
|
+
describe(_cx: DescribeContext): NativeNode {
|
|
803
759
|
const filter = this.#filter;
|
|
804
|
-
const children: NativeChild[] = [];
|
|
805
|
-
const allItems = this.#mainItems();
|
|
806
760
|
const columns = filter === undefined ? this.#stripColumns() : [];
|
|
807
|
-
const
|
|
808
|
-
if (
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
761
|
+
const memo = `${this.#selected}|${this.#activeVariant}|${this.#siblingSelected}|${this.#truncated}|${columns.length}|${filter ?? "\0"}`;
|
|
762
|
+
if (this.#bar?.memo === memo && this.#bar.targets === this.#targets) return this.#bar.node;
|
|
763
|
+
|
|
764
|
+
const title = text([span(`${theme.icon.rewind} `, "accent"), span("Rewind", "strong")], { wrap: "none" });
|
|
765
|
+
const upDown = actionHint(["tui.select.up", "tui.select.down"], "step");
|
|
766
|
+
const rewind = actionButton("Rewind here", "rewind", { keys: "enter", tone: "accent" });
|
|
767
|
+
let children: NativeChild[];
|
|
768
|
+
if (filter === undefined) {
|
|
769
|
+
const column = columns[this.#activeVariant - 1];
|
|
770
|
+
const at = column ? this.#siblingSelected : this.#selected;
|
|
771
|
+
const of = column ? column.targets.length : this.#targets.length;
|
|
772
|
+
children = [
|
|
773
|
+
title,
|
|
774
|
+
text([span(`${at + 1}/${of}`, "dim")], { wrap: "none" }),
|
|
775
|
+
hintsRow([
|
|
776
|
+
upDown,
|
|
777
|
+
{ keys: ["left", "right"], label: columns.length > 0 ? "branches" : "user turns" },
|
|
778
|
+
{ keys: ["f"], label: "filter" },
|
|
779
|
+
this.#truncated ? { keys: ["a"], label: "earlier turns" } : undefined,
|
|
780
|
+
]),
|
|
781
|
+
actionBar([null, actionButton("Cancel", "cancel", { keys: "escape" }), rewind]),
|
|
782
|
+
];
|
|
824
783
|
} else {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
);
|
|
838
|
-
}
|
|
839
|
-
if (columns.length > 0) {
|
|
840
|
-
children.push(
|
|
841
|
-
node(
|
|
842
|
-
"tabs",
|
|
843
|
-
{
|
|
844
|
-
items: [
|
|
845
|
-
{ id: "0", label: "current" },
|
|
846
|
-
...columns.map((column, index) => ({ id: String(index + 1), label: column.label })),
|
|
847
|
-
],
|
|
848
|
-
active: String(this.#activeVariant),
|
|
849
|
-
},
|
|
850
|
-
undefined,
|
|
851
|
-
"branches",
|
|
784
|
+
const matches = this.#filterMatches();
|
|
785
|
+
const at = matches.indexOf(this.#selected);
|
|
786
|
+
children = [
|
|
787
|
+
title,
|
|
788
|
+
this.#filterInput!,
|
|
789
|
+
text(
|
|
790
|
+
[
|
|
791
|
+
matches.length === 0
|
|
792
|
+
? span("no matches", "error")
|
|
793
|
+
: span(`${at >= 0 ? at + 1 : "-"}/${matches.length}`, "dim"),
|
|
794
|
+
],
|
|
795
|
+
{ wrap: "none" },
|
|
852
796
|
),
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
column.nativeItems ??= column.targets.map(target => turnItem(target));
|
|
857
|
-
children.push(
|
|
858
|
-
node(
|
|
859
|
-
"list",
|
|
860
|
-
{ selected: column.targets[this.#siblingSelected]?.turnId ?? null, virtual: true, max: 0.4 },
|
|
861
|
-
column.nativeItems,
|
|
862
|
-
"branch",
|
|
863
|
-
),
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
const outlined = filter === undefined ? this.#outlinedTarget() : this.#targets[this.#selected];
|
|
868
|
-
if (outlined && (!matches || matches.includes(this.#selected))) {
|
|
869
|
-
const item = targetCopy(outlined, collectBlocks(outlined.entries));
|
|
870
|
-
children.push(
|
|
871
|
-
node("section", { head: [span(item.label, "strong")] }, [turnPreview(outlined, item.content)], "preview"),
|
|
872
|
-
);
|
|
797
|
+
hintsRow([upDown, { keys: ["left", "right"], label: "user turns" }]),
|
|
798
|
+
actionBar([null, actionButton("Show all", "cancel", { keys: "escape" }), rewind]),
|
|
799
|
+
];
|
|
873
800
|
}
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
hintsRow(
|
|
877
|
-
filter === undefined
|
|
878
|
-
? [
|
|
879
|
-
upDown,
|
|
880
|
-
{ keys: ["left", "right"], label: columns.length > 0 ? "branches" : "user turns" },
|
|
881
|
-
{ keys: ["f"], label: "filter" },
|
|
882
|
-
{ keys: ["enter"], label: "rewind" },
|
|
883
|
-
this.#truncated ? { keys: ["a"], label: "earlier turns" } : undefined,
|
|
884
|
-
actionHint("tui.select.cancel", "cancel"),
|
|
885
|
-
]
|
|
886
|
-
: [
|
|
887
|
-
upDown,
|
|
888
|
-
{ keys: ["left", "right"], label: "user turns" },
|
|
889
|
-
{ keys: ["enter"], label: "rewind" },
|
|
890
|
-
actionHint("tui.select.cancel", "show all"),
|
|
891
|
-
],
|
|
892
|
-
),
|
|
893
|
-
);
|
|
894
|
-
const root = overlayCard(
|
|
895
|
-
"omp.overlay.rewind",
|
|
896
|
-
[
|
|
897
|
-
span(`${theme.icon.rewind} `),
|
|
898
|
-
span("Rewind", "strong"),
|
|
899
|
-
span(`${theme.sep.dot}pick the point to continue from`, "dim"),
|
|
900
|
-
],
|
|
901
|
-
children,
|
|
902
|
-
);
|
|
903
|
-
this.#native = { memo, targets: this.#targets, node: root };
|
|
801
|
+
const root = node("row", { role: "omp.rewind.bar", gap: "md", align: "center", wrap: true }, children);
|
|
802
|
+
this.#bar = { memo, targets: this.#targets, node: root };
|
|
904
803
|
return root;
|
|
905
804
|
}
|
|
906
805
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (
|
|
910
|
-
const
|
|
911
|
-
if (
|
|
912
|
-
|
|
913
|
-
|
|
806
|
+
/** The bar's buttons run their keys' paths; everything else belongs to the blocks. */
|
|
807
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
808
|
+
if (event.type !== "action") return;
|
|
809
|
+
const filtering = this.#filter !== undefined;
|
|
810
|
+
if (event.act === "rewind") {
|
|
811
|
+
if (filtering) this.#selectFiltered();
|
|
812
|
+
else this.#selectOutlined();
|
|
813
|
+
} else if (event.act === "cancel") {
|
|
814
|
+
if (filtering) this.#closeFilter();
|
|
815
|
+
else this.deps.onCancel();
|
|
816
|
+
}
|
|
914
817
|
}
|
|
915
818
|
|
|
916
819
|
// ========================================================================
|