@oh-my-pi/pi-tui 18.4.11 → 18.5.0
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 +37 -0
- package/dist/types/components/editor.d.ts +1 -1
- package/dist/types/components/input.d.ts +1 -1
- package/dist/types/native/encode.d.ts +2 -1
- package/dist/types/native/node.d.ts +22 -4
- package/dist/types/overlays/jobs-panel.d.ts +1 -7
- package/dist/types/overlays/report-panel.d.ts +68 -0
- package/dist/types/overlays/thinking-selector.d.ts +2 -2
- package/dist/types/prompt/composer-cache.d.ts +0 -16
- package/dist/types/prompt/composer.d.ts +17 -7
- package/dist/types/prompt/welcome.d.ts +17 -42
- package/dist/types/status-line/context-usage.d.ts +7 -5
- package/dist/types/thinking.d.ts +1 -1
- package/dist/types/tools/task.d.ts +5 -1
- package/package.json +9 -9
- package/src/apps/git/sidebar.ts +2 -2
- package/src/components/editor.ts +2 -1
- package/src/components/input.ts +2 -1
- package/src/desktop-notify.ts +2 -1
- package/src/native/backend.ts +5 -1
- package/src/native/encode.ts +4 -2
- package/src/native/node.ts +21 -4
- package/src/native/reconcile.ts +12 -4
- package/src/overlays/jobs-panel.ts +6 -53
- package/src/overlays/plan-review-overlay.ts +15 -1
- package/src/overlays/report-panel.ts +191 -0
- package/src/overlays/thinking-selector.ts +11 -11
- package/src/prompt/composer-cache.ts +11 -92
- package/src/prompt/composer.ts +29 -27
- package/src/prompt/custom-editor.ts +8 -2
- package/src/prompt/tips.txt +4 -0
- package/src/prompt/welcome.ts +101 -372
- package/src/render/render-utils.ts +5 -1
- package/src/status-line/context-usage.ts +21 -48
- package/src/thinking.ts +1 -1
- package/src/tools/streaming-output.ts +5 -0
- package/src/tools/task.ts +6 -2
package/src/native/node.ts
CHANGED
|
@@ -29,10 +29,12 @@ export type NativeNode = {
|
|
|
29
29
|
*/
|
|
30
30
|
readonly key?: string;
|
|
31
31
|
/**
|
|
32
|
-
* Scroll the node into view, placed like the `reveal` op
|
|
33
|
-
* added
|
|
32
|
+
* Scroll the node into view, placed like the `reveal` op. A placement
|
|
33
|
+
* reveals it when it is added: key a node by what it points at to
|
|
34
|
+
* reveal it again on a move. A {@link NativeReveal} reveals it whenever
|
|
35
|
+
* its `n` differs from the previous description of the same node.
|
|
34
36
|
*/
|
|
35
|
-
readonly reveal?:
|
|
37
|
+
readonly reveal?: NativeRevealAt | NativeReveal;
|
|
36
38
|
/**
|
|
37
39
|
* Keyboard scrolling forwarded to the terminal (PgUp/PgDn/End reach the
|
|
38
40
|
* program): moves the scroller at or above the node by `by` whenever
|
|
@@ -44,6 +46,19 @@ export type NativeNode = {
|
|
|
44
46
|
};
|
|
45
47
|
}[TspKind];
|
|
46
48
|
|
|
49
|
+
/** Where a revealed node lands in its scroller. */
|
|
50
|
+
export type NativeRevealAt = "start" | "end" | "nearest";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A repeatable {@link NativeNode.reveal}: bump `n` to bring an existing node
|
|
54
|
+
* into view again (a Contents entry jumping to its section). A freshly added
|
|
55
|
+
* node is not revealed.
|
|
56
|
+
*/
|
|
57
|
+
export interface NativeReveal {
|
|
58
|
+
readonly at: NativeRevealAt;
|
|
59
|
+
readonly n: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
/** A {@link NativeNode.scroll} request. */
|
|
48
63
|
export interface NativeScroll {
|
|
49
64
|
readonly by: TspScrollBy;
|
|
@@ -113,7 +128,9 @@ export type NativeUiEvent =
|
|
|
113
128
|
readonly value: boolean | number | string | readonly string[] | null;
|
|
114
129
|
}
|
|
115
130
|
/** An edit over the terminal's own selection in an `editor`/`input` node (see {@link NativeTextEdit}). */
|
|
116
|
-
| ({ readonly type: "edit"; readonly key: string } & NativeTextEdit)
|
|
131
|
+
| ({ readonly type: "edit"; readonly key: string } & NativeTextEdit)
|
|
132
|
+
/** Undo the last change to an `editor`/`input` node's text (the terminal's ⌃Z); a no-op with no history. */
|
|
133
|
+
| { readonly type: "undo"; readonly key: string };
|
|
117
134
|
|
|
118
135
|
/**
|
|
119
136
|
* A primitive edit the terminal made over its own text selection (cut,
|
package/src/native/reconcile.ts
CHANGED
|
@@ -31,7 +31,7 @@ import * as logger from "@oh-my-pi/pi-utils/logger";
|
|
|
31
31
|
import { TSP_TEXT_KINDS, type TspKind, type TspNode, type TspOp, type TspScrollBy } from "@oh-my-pi/pi-wire";
|
|
32
32
|
import { type Component, Container, CURSOR_MARKER } from "../tui";
|
|
33
33
|
import { normalizeIconProps } from "./icons";
|
|
34
|
-
import type { DescribeContext, NativeChild, NativeNode } from "./node";
|
|
34
|
+
import type { DescribeContext, NativeChild, NativeNode, NativeRevealAt } from "./node";
|
|
35
35
|
import { isNativeSettled } from "./settle";
|
|
36
36
|
|
|
37
37
|
/** The regions a frame fills. */
|
|
@@ -213,8 +213,11 @@ export class Reconciler {
|
|
|
213
213
|
#ops: TspOp[] = [];
|
|
214
214
|
#dels: Entry[] = [];
|
|
215
215
|
#settles: string[] = [];
|
|
216
|
-
/**
|
|
217
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Selected list items, added `reveal` nodes and nodes whose reveal `n` moved this frame,
|
|
218
|
+
* revealed once the frame's adds have landed.
|
|
219
|
+
*/
|
|
220
|
+
#reveals: [id: string, at: NativeRevealAt][] = [];
|
|
218
221
|
/** `scroll` requests whose `n` moved this frame, sent after the reveals. */
|
|
219
222
|
#scrolls: [id: string, by: TspScrollBy][] = [];
|
|
220
223
|
/** Wire ids `list.selected` keys resolved to (an unresolved key names no node to reveal). */
|
|
@@ -595,6 +598,11 @@ export class Reconciler {
|
|
|
595
598
|
this.#revisitNode(next, inner);
|
|
596
599
|
return;
|
|
597
600
|
}
|
|
601
|
+
const reveal = next.node.reveal;
|
|
602
|
+
if (typeof reveal === "object") {
|
|
603
|
+
const was = old.node.reveal;
|
|
604
|
+
if (reveal.n !== (typeof was === "object" ? was.n : undefined)) this.#reveals.push([next.id, reveal.at]);
|
|
605
|
+
}
|
|
598
606
|
if (old.node.k !== next.node.k) {
|
|
599
607
|
this.#dropSubtree(old);
|
|
600
608
|
this.#ops.push(["del", next.id]);
|
|
@@ -743,7 +751,7 @@ export class Reconciler {
|
|
|
743
751
|
|
|
744
752
|
/** A full wire subtree for an entry; nested components get states (or pending moves when mounted elsewhere). */
|
|
745
753
|
#materialize(entry: NodeEntry, walk: Walk): TspNode {
|
|
746
|
-
if (entry.node.reveal) this.#reveals.push([entry.id, entry.node.reveal]);
|
|
754
|
+
if (typeof entry.node.reveal === "string") this.#reveals.push([entry.id, entry.node.reveal]);
|
|
747
755
|
const entries = this.#entries(entry.node.c, entry.keypath, entry.owner, entry.hoist, walk);
|
|
748
756
|
const children: TspNode[] = [];
|
|
749
757
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The async background jobs of a session
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* inspects the selected one (command, cwd, live pids, exit code, output tail).
|
|
2
|
+
* The async background jobs of a session as {@link JobsSheet}: the dismissable
|
|
3
|
+
* sheet the native jobs pill and `/jobs` open. It draws task jobs as `agent`
|
|
4
|
+
* nodes and the others as a status dot, the one-line label and a live
|
|
5
|
+
* `elapsed`, lists every job and inspects the selected one (command, cwd, live
|
|
6
|
+
* pids, exit code, output tail).
|
|
8
7
|
*/
|
|
9
8
|
import type { TspAgentProps, TspSpan } from "@oh-my-pi/pi-wire";
|
|
10
9
|
import { formatDuration } from "@oh-my-pi/pi-utils";
|
|
11
|
-
import {
|
|
10
|
+
import type { Component } from "../tui";
|
|
12
11
|
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
|
|
13
12
|
import { matchesKey } from "../keys";
|
|
14
13
|
import { truncateToWidth } from "../utils";
|
|
@@ -92,31 +91,6 @@ const DOT: Record<JobsPanelJob["status"], TspSpan> = {
|
|
|
92
91
|
/** Output tail rows the terminal fallback shows under the selected job. */
|
|
93
92
|
const FALLBACK_TAIL_LINES = 8;
|
|
94
93
|
|
|
95
|
-
/** The `/jobs` block: `ansi` renders in the terminal, the snapshot describes the native frame. */
|
|
96
|
-
export class JobsPanel extends Container {
|
|
97
|
-
readonly #snapshot: JobsPanelSnapshot;
|
|
98
|
-
readonly #nowMs: number;
|
|
99
|
-
#native: { agent: boolean; node: NativeNode } | undefined;
|
|
100
|
-
|
|
101
|
-
constructor(snapshot: JobsPanelSnapshot, nowMs: number, ansi: readonly Component[]) {
|
|
102
|
-
super();
|
|
103
|
-
this.#snapshot = snapshot;
|
|
104
|
-
this.#nowMs = nowMs;
|
|
105
|
-
for (const child of ansi) this.addChild(child);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
override describe(cx: DescribeContext): NativeNode {
|
|
109
|
-
const agent = cx.supports("agent");
|
|
110
|
-
if (this.#native?.agent === agent) return this.#native.node;
|
|
111
|
-
const head: TspSpan[] = [span("Background jobs", "strong")];
|
|
112
|
-
const running = this.#snapshot.running.length;
|
|
113
|
-
if (running > 0) head.push(span(` · ${running} running`, "muted"));
|
|
114
|
-
const described = card({ role: "omp.jobs", head }, describeJobs(this.#snapshot, this.#nowMs, agent));
|
|
115
|
-
this.#native = { agent, node: described };
|
|
116
|
-
return described;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
94
|
/**
|
|
121
95
|
* The jobs pill's sheet: a centred `lg` glass sheet titled Background jobs.
|
|
122
96
|
* A selectable list of every job (running first) sits over the selected
|
|
@@ -254,27 +228,6 @@ function jobAge(job: JobsPanelJob, nowMs: number): number {
|
|
|
254
228
|
return Math.max(0, (job.endTime ?? nowMs) - job.startTime);
|
|
255
229
|
}
|
|
256
230
|
|
|
257
|
-
/** The body the `/jobs` panel draws: running rows, then a Recent section; an empty note when there are none. */
|
|
258
|
-
function describeJobs(snapshot: JobsPanelSnapshot, nowMs: number, agent: boolean): NativeChild[] {
|
|
259
|
-
const { running, recent } = snapshot;
|
|
260
|
-
const children: NativeChild[] = [];
|
|
261
|
-
if (running.length === 0 && recent.length === 0) {
|
|
262
|
-
children.push(node("text", { text: "No background jobs", role: "omp.jobs.empty" }, undefined, "empty"));
|
|
263
|
-
}
|
|
264
|
-
for (const job of running) children.push(describeJob(job, nowMs, agent));
|
|
265
|
-
if (recent.length > 0) {
|
|
266
|
-
children.push(
|
|
267
|
-
node(
|
|
268
|
-
"section",
|
|
269
|
-
{ head: [span("Recent", "muted")] },
|
|
270
|
-
recent.map(job => describeJob(job, nowMs, agent)),
|
|
271
|
-
"recent",
|
|
272
|
-
),
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
return children;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
231
|
/** A task job as an `agent` node when the terminal draws them, else a one-line dot row. */
|
|
279
232
|
function describeJob(job: JobsPanelJob, nowMs: number, agent: boolean): NativeNode {
|
|
280
233
|
const settled = job.status !== "running";
|
|
@@ -249,6 +249,11 @@ export class PlanReviewOverlay implements Component {
|
|
|
249
249
|
#nativeRoot: NativeNode | undefined;
|
|
250
250
|
/** Content the memoized root was built from. */
|
|
251
251
|
#nativeRootContent: NativePlanContent | undefined;
|
|
252
|
+
/**
|
|
253
|
+
* The section the last Contents jump targets under a native surface, which
|
|
254
|
+
* scrolls the body itself: `n` counts the jumps, so each one reveals it again.
|
|
255
|
+
*/
|
|
256
|
+
#jump: { section: number; n: number } | undefined;
|
|
252
257
|
|
|
253
258
|
constructor(
|
|
254
259
|
planContent: string,
|
|
@@ -798,6 +803,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
798
803
|
#scrubBodyToToc(): void {
|
|
799
804
|
const sectionIndex = this.#toc[this.#tocCursor];
|
|
800
805
|
if (sectionIndex === undefined) return;
|
|
806
|
+
this.#jump = { section: sectionIndex, n: (this.#jump?.n ?? 0) + 1 };
|
|
801
807
|
const offset = this.#sectionOffsets[sectionIndex];
|
|
802
808
|
if (offset !== undefined) {
|
|
803
809
|
this.#scrollView.setScrollOffset(offset);
|
|
@@ -1404,6 +1410,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
1404
1410
|
const sig = [
|
|
1405
1411
|
this.#focus,
|
|
1406
1412
|
this.#tocCursor,
|
|
1413
|
+
this.#jump?.n ?? 0,
|
|
1407
1414
|
this.#selectedIndex,
|
|
1408
1415
|
this.#sliderIndex,
|
|
1409
1416
|
this.#committed ? `committed:${this.#committedLabel ?? ""}` : "",
|
|
@@ -1419,10 +1426,17 @@ export class PlanReviewOverlay implements Component {
|
|
|
1419
1426
|
const children: NativeChild[] = [];
|
|
1420
1427
|
const tools = this.#describeTools();
|
|
1421
1428
|
if (tools) children.push(tools);
|
|
1429
|
+
// The section a Contents jump targets scrolls to the body's top.
|
|
1430
|
+
const jump = this.#jump;
|
|
1431
|
+
const body = jump
|
|
1432
|
+
? content.body.map((section): NativeNode =>
|
|
1433
|
+
section.key === `s${jump.section}` ? { ...section, reveal: { at: "start", n: jump.n } } : section,
|
|
1434
|
+
)
|
|
1435
|
+
: content.body;
|
|
1422
1436
|
const bodyCol = node(
|
|
1423
1437
|
"col",
|
|
1424
1438
|
{ role: "omp.plan.body", grow: 1, gap: "md", tone: this.#focus === "body" ? "accent" : undefined },
|
|
1425
|
-
|
|
1439
|
+
body,
|
|
1426
1440
|
"body",
|
|
1427
1441
|
);
|
|
1428
1442
|
if (this.#sidebarShown) {
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { TspScrollBy, TspText } from "@oh-my-pi/pi-wire";
|
|
2
|
+
import { OverlayPanel } from "../chrome/overlay-box";
|
|
3
|
+
import { editorKey } from "../chrome/keybinding-hints";
|
|
4
|
+
import { Ellipsis } from "../index";
|
|
5
|
+
import { ScrollView } from "../components/scroll-view";
|
|
6
|
+
import { Spacer } from "../components/spacer";
|
|
7
|
+
import { Text } from "../components/text";
|
|
8
|
+
import {
|
|
9
|
+
matchesSelectCancel,
|
|
10
|
+
matchesSelectDown,
|
|
11
|
+
matchesSelectPageDown,
|
|
12
|
+
matchesSelectPageUp,
|
|
13
|
+
matchesSelectUp,
|
|
14
|
+
} from "../keybinding-matchers";
|
|
15
|
+
import { matchesKey } from "../keys";
|
|
16
|
+
import { routeSgrMouseInput } from "../mouse";
|
|
17
|
+
import { actionBar, actionButton } from "../native/overlay";
|
|
18
|
+
import { col } from "../native/describe";
|
|
19
|
+
import type { NativeNode, NativeScroll, NativeUiEvent } from "../native/node";
|
|
20
|
+
import { theme } from "../theme/theme";
|
|
21
|
+
import type { Component } from "../tui";
|
|
22
|
+
|
|
23
|
+
/** Options of a {@link ReportPanel}. */
|
|
24
|
+
export interface ReportPanelOptions {
|
|
25
|
+
/** Plain title: the text-mode border title, and the native sheet head unless {@link head} is given. */
|
|
26
|
+
readonly title: string;
|
|
27
|
+
/** Styled native sheet head. */
|
|
28
|
+
readonly head?: TspText;
|
|
29
|
+
/** The read-only report; it draws no frame or title of its own. */
|
|
30
|
+
readonly body: Component;
|
|
31
|
+
/**
|
|
32
|
+
* Display form of the key that dismisses the report while it sits above the
|
|
33
|
+
* editor (the editor's interrupt key, which routes to it). Once the panel
|
|
34
|
+
* holds focus ({@link ReportPanel.holdFocus}) the hint shows the key its own
|
|
35
|
+
* input closes on instead.
|
|
36
|
+
*/
|
|
37
|
+
readonly closeKey: string;
|
|
38
|
+
/** The focused panel (native sheet, text-mode full-screen page) closes on Esc / its Close button: this runs. */
|
|
39
|
+
readonly onClose: () => void;
|
|
40
|
+
/**
|
|
41
|
+
* Most rows the text-mode panel may take, borders included (the screen rows
|
|
42
|
+
* left above the editor); a taller report scrolls inside it. `undefined`
|
|
43
|
+
* leaves it uncapped.
|
|
44
|
+
*/
|
|
45
|
+
readonly maxRows?: () => number | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Text-mode rows around the scrolled body: the borders, two spacers and the footer. */
|
|
49
|
+
const TEXT_CHROME_ROWS = 5;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A read-only command report (`/changelog`, `/context`, `/tools`, …).
|
|
53
|
+
*
|
|
54
|
+
* Text mode draws a titled box with the body and an Esc hint: above the editor
|
|
55
|
+
* like the `/btw` panel while it fits there (the editor keeps focus and its
|
|
56
|
+
* Esc takes it away), or as a full-screen page on the alternate screen when it
|
|
57
|
+
* is taller, whose body scrolls on the arrow/page/Home/End keys and the wheel.
|
|
58
|
+
* Natively it is a sheet like `/usage`: a centred `lg` glass sheet titled by
|
|
59
|
+
* the report whose body the terminal scrolls (wheel, and the same keys
|
|
60
|
+
* forwarded as scroll requests), then a Close button; Esc closes it.
|
|
61
|
+
*/
|
|
62
|
+
export class ReportPanel extends OverlayPanel {
|
|
63
|
+
readonly nativeOverlay: NonNullable<Component["nativeOverlay"]>;
|
|
64
|
+
readonly #body: Component;
|
|
65
|
+
readonly #onClose: () => void;
|
|
66
|
+
readonly #maxRows: (() => number | undefined) | undefined;
|
|
67
|
+
readonly #view: ScrollView;
|
|
68
|
+
readonly #footer: Text;
|
|
69
|
+
#closeHint: string;
|
|
70
|
+
#bodyRows = 0;
|
|
71
|
+
#scroll: NativeScroll | undefined;
|
|
72
|
+
#native: { scroll: NativeScroll | undefined; node: NativeNode } | undefined;
|
|
73
|
+
|
|
74
|
+
constructor(options: ReportPanelOptions) {
|
|
75
|
+
super(options.title, "omp.overlay.report");
|
|
76
|
+
this.nativeOverlay = {
|
|
77
|
+
role: "omp.overlay.report",
|
|
78
|
+
size: "lg",
|
|
79
|
+
anchor: "center",
|
|
80
|
+
head: options.head ?? options.title,
|
|
81
|
+
};
|
|
82
|
+
this.#body = options.body;
|
|
83
|
+
this.#onClose = options.onClose;
|
|
84
|
+
this.#maxRows = options.maxRows;
|
|
85
|
+
this.#closeHint = `${options.closeKey} to close`;
|
|
86
|
+
this.#view = new ScrollView(options.body, {
|
|
87
|
+
height: 0,
|
|
88
|
+
scrollbar: "auto",
|
|
89
|
+
ellipsis: Ellipsis.Omit,
|
|
90
|
+
theme: { track: text => theme.fg("dim", text), thumb: text => theme.fg("accent", text) },
|
|
91
|
+
});
|
|
92
|
+
this.#footer = new Text(theme.fg("muted", this.#closeHint), 0, 0);
|
|
93
|
+
this.addChild(new Spacer(1));
|
|
94
|
+
this.addChild(this.#view);
|
|
95
|
+
this.addChild(new Spacer(1));
|
|
96
|
+
this.addChild(this.#footer);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The panel is shown with focus (native sheet, text-mode full-screen page):
|
|
101
|
+
* its own {@link handleInput} closes it, so the hint names that key.
|
|
102
|
+
*/
|
|
103
|
+
holdFocus(): void {
|
|
104
|
+
this.#closeHint = `${editorKey("tui.select.cancel")} to close`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Text mode: cap the box to {@link ReportPanelOptions.maxRows}, scrolling a
|
|
109
|
+
* taller body. The cap is a hard limit: when a resize leaves fewer rows than
|
|
110
|
+
* the box's chrome needs, its bottom (the hint and closing border) is cut
|
|
111
|
+
* rather than spilling over the editor.
|
|
112
|
+
*/
|
|
113
|
+
override render(width: number): readonly string[] {
|
|
114
|
+
const innerWidth = Math.max(1, width - 4);
|
|
115
|
+
const rows = this.#body.render(innerWidth).length;
|
|
116
|
+
const cap = this.#maxRows?.();
|
|
117
|
+
const bodyMax = cap === undefined ? rows : Math.max(1, cap - TEXT_CHROME_ROWS);
|
|
118
|
+
const overflows = rows > bodyMax;
|
|
119
|
+
this.#bodyRows = Math.min(rows, bodyMax);
|
|
120
|
+
this.#view.setHeight(this.#bodyRows);
|
|
121
|
+
const scrollHint = `${editorKey("tui.select.pageUp")}/${editorKey("tui.select.pageDown")} scroll · `;
|
|
122
|
+
this.#footer.setText(theme.fg("muted", overflows ? scrollHint + this.#closeHint : this.#closeHint));
|
|
123
|
+
const lines = super.render(width);
|
|
124
|
+
return cap !== undefined && lines.length > cap ? lines.slice(0, Math.max(0, cap)) : lines;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Text mode: rows the whole box takes at `width` when nothing caps it. */
|
|
128
|
+
heightAt(width: number): number {
|
|
129
|
+
return this.#body.render(Math.max(1, width - 4)).length + TEXT_CHROME_ROWS;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The sheet body (the overlay is the frame, and its body the scroller): the report, then Close (Esc). */
|
|
133
|
+
override describe(): NativeNode {
|
|
134
|
+
const scroll = this.#scroll;
|
|
135
|
+
if (this.#native && this.#native.scroll === scroll) return this.#native.node;
|
|
136
|
+
const body: NativeNode = { ...col([this.#body]), key: "body", scroll };
|
|
137
|
+
const node = col([body, actionBar([null, actionButton("Close", "close", { keys: "escape" })])], { gap: "md" });
|
|
138
|
+
this.#native = { scroll, node };
|
|
139
|
+
return node;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
override invalidate(): void {
|
|
143
|
+
this.#native = undefined;
|
|
144
|
+
super.invalidate();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Focused (the native sheet, or the text-mode full-screen page): Esc closes;
|
|
149
|
+
* the arrow, page, Home and End keys and the wheel scroll the body.
|
|
150
|
+
*/
|
|
151
|
+
handleInput(data: string): void {
|
|
152
|
+
if (
|
|
153
|
+
routeSgrMouseInput(data, event => {
|
|
154
|
+
if (event.wheel === null) return false;
|
|
155
|
+
this.#view.scroll(event.wheel * 3);
|
|
156
|
+
return true;
|
|
157
|
+
})
|
|
158
|
+
) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (matchesSelectCancel(data)) {
|
|
162
|
+
this.#onClose();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const by: TspScrollBy | undefined = matchesSelectUp(data)
|
|
166
|
+
? "line-up"
|
|
167
|
+
: matchesSelectDown(data)
|
|
168
|
+
? "line-down"
|
|
169
|
+
: matchesSelectPageUp(data)
|
|
170
|
+
? "page-up"
|
|
171
|
+
: matchesSelectPageDown(data)
|
|
172
|
+
? "page-down"
|
|
173
|
+
: matchesKey(data, "home")
|
|
174
|
+
? "start"
|
|
175
|
+
: matchesKey(data, "end")
|
|
176
|
+
? "end"
|
|
177
|
+
: undefined;
|
|
178
|
+
if (by === undefined) return;
|
|
179
|
+
// Natively the terminal scrolls the sheet body; text mode scrolls the box.
|
|
180
|
+
this.#scroll = { by, n: (this.#scroll?.n ?? 0) + 1 };
|
|
181
|
+
const page = Math.max(1, this.#bodyRows - 1);
|
|
182
|
+
if (by === "start") this.#view.scrollToTop();
|
|
183
|
+
else if (by === "end") this.#view.scrollToBottom();
|
|
184
|
+
else this.#view.scroll(by === "line-up" ? -1 : by === "line-down" ? 1 : by === "page-up" ? -page : page);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Close runs what Esc runs. */
|
|
188
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
189
|
+
if (event.type === "action" && event.act === "close") this.#onClose();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { AUTO_THINKING, type ConfiguredThinkingLevel, getConfiguredThinkingLevelMetadata } from "../thinking";
|
|
2
2
|
import { type SelectItem, SelectList, type SgrMouseEvent } from "../index";
|
|
3
3
|
import { getSelectListTheme } from "../theme/theme";
|
|
4
|
-
import { getThinkingLevelMetadata } from "../thinking";
|
|
5
4
|
import { OverlayPanel } from "../chrome/overlay-box";
|
|
6
5
|
import { routeSelectListMouseWithTopBorder } from "../chrome/select-list-mouse-routing";
|
|
7
6
|
import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node";
|
|
@@ -15,14 +14,14 @@ export class ThinkingSelectorComponent extends OverlayPanel {
|
|
|
15
14
|
#sheet: SelectListSheet;
|
|
16
15
|
|
|
17
16
|
constructor(
|
|
18
|
-
currentLevel:
|
|
19
|
-
availableLevels:
|
|
20
|
-
onSelect: (level:
|
|
17
|
+
currentLevel: ConfiguredThinkingLevel | undefined,
|
|
18
|
+
availableLevels: ConfiguredThinkingLevel[],
|
|
19
|
+
onSelect: (level: ConfiguredThinkingLevel) => void,
|
|
21
20
|
onCancel: () => void,
|
|
22
21
|
) {
|
|
23
22
|
super("Thinking Level", "omp.overlay.thinking");
|
|
24
23
|
|
|
25
|
-
const thinkingLevels: SelectItem[] = availableLevels.map(
|
|
24
|
+
const thinkingLevels: SelectItem[] = availableLevels.map(getConfiguredThinkingLevelMetadata);
|
|
26
25
|
|
|
27
26
|
// Create selector
|
|
28
27
|
this.#selectList = new SelectList(thinkingLevels, thinkingLevels.length, getSelectListTheme());
|
|
@@ -34,7 +33,7 @@ export class ThinkingSelectorComponent extends OverlayPanel {
|
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
this.#selectList.onSelect = item => {
|
|
37
|
-
onSelect(item.value as
|
|
36
|
+
onSelect(item.value as ConfiguredThinkingLevel);
|
|
38
37
|
};
|
|
39
38
|
|
|
40
39
|
this.#selectList.onCancel = () => {
|
|
@@ -46,10 +45,11 @@ export class ThinkingSelectorComponent extends OverlayPanel {
|
|
|
46
45
|
title: "Thinking level",
|
|
47
46
|
icon: "brain",
|
|
48
47
|
noun: "levels",
|
|
49
|
-
current: [currentLevel],
|
|
50
|
-
decorate: item =>
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
current: currentLevel ? [currentLevel] : [],
|
|
49
|
+
decorate: item =>
|
|
50
|
+
item.value === AUTO_THINKING
|
|
51
|
+
? {}
|
|
52
|
+
: { chips: [{ text: "", dot: `thinking${item.value.charAt(0).toUpperCase()}${item.value.slice(1)}` }] },
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
* (`~/.omp/agent/cache/composer.db`).
|
|
4
4
|
*
|
|
5
5
|
* Each row is one JSON payload keyed by project (the resolved cwd) and kind.
|
|
6
|
-
* Settings-derived kinds (theme/composer preferences,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* rows stay per project.
|
|
6
|
+
* Settings-derived kinds (theme/composer preferences, status-bar inputs) are
|
|
7
|
+
* also written under the empty project, so a folder that never ran omp still
|
|
8
|
+
* paints with the user's theme and status bar: those are rarely
|
|
9
|
+
* project-specific, and path/branch render live.
|
|
11
10
|
*
|
|
12
11
|
* ```text
|
|
13
12
|
* entries (project TEXT, kind TEXT, value TEXT JSON, PRIMARY KEY (project, kind))
|
|
@@ -25,7 +24,6 @@ import * as logger from "@oh-my-pi/pi-utils/logger";
|
|
|
25
24
|
import * as postmortem from "@oh-my-pi/pi-utils/postmortem";
|
|
26
25
|
import { openSqliteDatabaseSync } from "@oh-my-pi/pi-utils/sqlite";
|
|
27
26
|
import { isRecord } from "@oh-my-pi/pi-utils/type-guards";
|
|
28
|
-
import type { LspServerInfo, RecentSession } from "./welcome";
|
|
29
27
|
import type { ComposerPreferences, ComposerStatusCache } from "./composer";
|
|
30
28
|
import { readStatusLineStartupData } from "../status-line/startup";
|
|
31
29
|
import type { SymbolPreset } from "../theme/theme";
|
|
@@ -35,7 +33,6 @@ import { isWordCompletionMethod } from "./word-completion";
|
|
|
35
33
|
const FORMAT_VERSION = 1;
|
|
36
34
|
/** Project key of rows that serve every project lacking its own. */
|
|
37
35
|
const ANY_PROJECT = "";
|
|
38
|
-
const RECENT_SESSION_LIMIT = 4;
|
|
39
36
|
|
|
40
37
|
const SCHEMA = `
|
|
41
38
|
PRAGMA journal_mode=WAL;
|
|
@@ -48,9 +45,8 @@ CREATE TABLE IF NOT EXISTS entries (
|
|
|
48
45
|
) WITHOUT ROWID;
|
|
49
46
|
`;
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
type SharedEntryKind = "ui" | "welcome" | "status";
|
|
48
|
+
/** Every kind is mirrored under {@link ANY_PROJECT} as the fallback for projects without their own row. */
|
|
49
|
+
type EntryKind = "ui" | "status";
|
|
54
50
|
|
|
55
51
|
/** Theme inputs cached from the last resolved settings load for stable prepaint colors. */
|
|
56
52
|
export interface ComposerThemePreferences {
|
|
@@ -60,20 +56,10 @@ export interface ComposerThemePreferences {
|
|
|
60
56
|
readonly lightTheme?: string;
|
|
61
57
|
}
|
|
62
58
|
|
|
63
|
-
/** Last authoritative model labels shown in the welcome component. */
|
|
64
|
-
export interface ComposerWelcomeCache {
|
|
65
|
-
readonly modelName: string;
|
|
66
|
-
readonly providerName: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
59
|
/** Speculative composer state read before the settings/session graph is available. */
|
|
70
60
|
export interface ComposerStartupCache {
|
|
71
61
|
readonly preferences?: ComposerPreferences;
|
|
72
62
|
readonly theme?: ComposerThemePreferences;
|
|
73
|
-
readonly welcome?: ComposerWelcomeCache;
|
|
74
|
-
readonly recentSessions: RecentSession[];
|
|
75
|
-
/** `null` when the last run had LSP disabled. */
|
|
76
|
-
readonly lspServers: LspServerInfo[] | null;
|
|
77
63
|
readonly status?: ComposerStatusCache;
|
|
78
64
|
}
|
|
79
65
|
|
|
@@ -86,44 +72,6 @@ function parseJson(value: string | undefined): unknown {
|
|
|
86
72
|
}
|
|
87
73
|
}
|
|
88
74
|
|
|
89
|
-
function parseRecentSessions(value: unknown): RecentSession[] {
|
|
90
|
-
if (!Array.isArray(value)) return [];
|
|
91
|
-
const sessions: RecentSession[] = [];
|
|
92
|
-
for (const item of value) {
|
|
93
|
-
if (!isRecord(item)) continue;
|
|
94
|
-
const { name, timeAgo } = item;
|
|
95
|
-
if (typeof name === "string" && typeof timeAgo === "string") sessions.push({ name, timeAgo });
|
|
96
|
-
if (sessions.length === RECENT_SESSION_LIMIT) break;
|
|
97
|
-
}
|
|
98
|
-
return sessions;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function parseLspServers(value: unknown): LspServerInfo[] | null {
|
|
102
|
-
if (value === null) return null;
|
|
103
|
-
if (!Array.isArray(value)) return [];
|
|
104
|
-
const servers: LspServerInfo[] = [];
|
|
105
|
-
for (const item of value) {
|
|
106
|
-
if (!isRecord(item)) continue;
|
|
107
|
-
const { name, status, fileTypes } = item;
|
|
108
|
-
if (
|
|
109
|
-
typeof name !== "string" ||
|
|
110
|
-
(status !== "ready" && status !== "error" && status !== "connecting" && status !== "available") ||
|
|
111
|
-
!Array.isArray(fileTypes) ||
|
|
112
|
-
!fileTypes.every(fileType => typeof fileType === "string")
|
|
113
|
-
) {
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
servers.push({ name, status, fileTypes });
|
|
117
|
-
}
|
|
118
|
-
return servers;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function parseWelcome(value: unknown): ComposerWelcomeCache | undefined {
|
|
122
|
-
if (!isRecord(value)) return undefined;
|
|
123
|
-
const { modelName, providerName } = value;
|
|
124
|
-
return typeof modelName === "string" && typeof providerName === "string" ? { modelName, providerName } : undefined;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
75
|
function parseStatus(value: unknown): ComposerStatusCache | undefined {
|
|
128
76
|
if (!isRecord(value)) return undefined;
|
|
129
77
|
const statusLine = readStatusLineStartupData(value.statusLine);
|
|
@@ -275,9 +223,6 @@ export class ComposerCache {
|
|
|
275
223
|
return {
|
|
276
224
|
preferences: ui?.preferences,
|
|
277
225
|
theme: ui?.theme,
|
|
278
|
-
welcome: parseWelcome(parseJson(own.welcome)) ?? parseWelcome(parseJson(anyProject.welcome)),
|
|
279
|
-
recentSessions: parseRecentSessions(parseJson(own["recent-sessions"])),
|
|
280
|
-
lspServers: own["lsp-servers"] === undefined ? [] : parseLspServers(parseJson(own["lsp-servers"])),
|
|
281
226
|
status: parseStatus(parseJson(own.status)) ?? parseStatus(parseJson(anyProject.status)),
|
|
282
227
|
};
|
|
283
228
|
}
|
|
@@ -287,21 +232,6 @@ export class ComposerCache {
|
|
|
287
232
|
this.#putShared(cwd, "ui", { preferences, theme });
|
|
288
233
|
}
|
|
289
234
|
|
|
290
|
-
/** Authoritative model/provider labels for the next welcome prepaint. */
|
|
291
|
-
writeWelcome(cwd: string, welcome: ComposerWelcomeCache): void {
|
|
292
|
-
this.#putShared(cwd, "welcome", welcome);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** The latest recent-session rows (first four). */
|
|
296
|
-
writeRecentSessions(cwd: string, sessions: readonly RecentSession[]): void {
|
|
297
|
-
this.#put(cwd, "recent-sessions", sessions.slice(0, RECENT_SESSION_LIMIT));
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** The latest detected project LSP rows; `null` records that LSP is disabled. */
|
|
301
|
-
writeLspServers(cwd: string, servers: readonly LspServerInfo[] | null): void {
|
|
302
|
-
this.#put(cwd, "lsp-servers", servers);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
235
|
/** Status-bar inputs for the next prepaint's startup status line. */
|
|
306
236
|
writeStatus(cwd: string, status: ComposerStatusCache): void {
|
|
307
237
|
this.#putShared(cwd, "status", status);
|
|
@@ -314,22 +244,11 @@ export class ComposerCache {
|
|
|
314
244
|
this.#db.close();
|
|
315
245
|
}
|
|
316
246
|
|
|
317
|
-
/**
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (this.#known.get(key) === json) return;
|
|
323
|
-
try {
|
|
324
|
-
this.#upsert.run(project, kind, json);
|
|
325
|
-
this.#known.set(key, json);
|
|
326
|
-
} catch (error) {
|
|
327
|
-
logger.debug("composer cache write failed", { kind, error: String(error) });
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** {@link #put} for this project plus the any-project fallback row, atomically. */
|
|
332
|
-
#putShared(cwd: string, kind: SharedEntryKind, value: unknown): void {
|
|
247
|
+
/**
|
|
248
|
+
* Best-effort upsert of this project's row plus the any-project fallback row,
|
|
249
|
+
* atomically: a failed write only costs the next launch its speculation.
|
|
250
|
+
*/
|
|
251
|
+
#putShared(cwd: string, kind: EntryKind, value: unknown): void {
|
|
333
252
|
const project = path.resolve(cwd);
|
|
334
253
|
const json = JSON.stringify(value);
|
|
335
254
|
const ownKey = `${project}\0${kind}`;
|