@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/types/components/editor.d.ts +1 -1
  3. package/dist/types/components/input.d.ts +1 -1
  4. package/dist/types/native/encode.d.ts +2 -1
  5. package/dist/types/native/node.d.ts +22 -4
  6. package/dist/types/overlays/jobs-panel.d.ts +1 -7
  7. package/dist/types/overlays/report-panel.d.ts +68 -0
  8. package/dist/types/overlays/thinking-selector.d.ts +2 -2
  9. package/dist/types/prompt/composer-cache.d.ts +0 -16
  10. package/dist/types/prompt/composer.d.ts +17 -7
  11. package/dist/types/prompt/welcome.d.ts +17 -42
  12. package/dist/types/status-line/context-usage.d.ts +7 -5
  13. package/dist/types/thinking.d.ts +1 -1
  14. package/dist/types/tools/task.d.ts +5 -1
  15. package/package.json +9 -9
  16. package/src/apps/git/sidebar.ts +2 -2
  17. package/src/components/editor.ts +2 -1
  18. package/src/components/input.ts +2 -1
  19. package/src/desktop-notify.ts +2 -1
  20. package/src/native/backend.ts +5 -1
  21. package/src/native/encode.ts +4 -2
  22. package/src/native/node.ts +21 -4
  23. package/src/native/reconcile.ts +12 -4
  24. package/src/overlays/jobs-panel.ts +6 -53
  25. package/src/overlays/plan-review-overlay.ts +15 -1
  26. package/src/overlays/report-panel.ts +191 -0
  27. package/src/overlays/thinking-selector.ts +11 -11
  28. package/src/prompt/composer-cache.ts +11 -92
  29. package/src/prompt/composer.ts +29 -27
  30. package/src/prompt/custom-editor.ts +8 -2
  31. package/src/prompt/tips.txt +4 -0
  32. package/src/prompt/welcome.ts +101 -372
  33. package/src/render/render-utils.ts +5 -1
  34. package/src/status-line/context-usage.ts +21 -48
  35. package/src/thinking.ts +1 -1
  36. package/src/tools/streaming-output.ts +5 -0
  37. package/src/tools/task.ts +6 -2
@@ -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, when it is
33
- * added. Key a node by what it points at to reveal it again on a move.
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?: "start" | "end" | "nearest";
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,
@@ -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
- /** Selected list items and added `reveal` nodes sent this frame, revealed once the frame's adds have landed. */
217
- #reveals: [id: string, at: "start" | "end" | "nearest"][] = [];
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, twice: {@link JobsPanel} is the
3
- * `/jobs` transcript block (ANSI keeps the caller's text report; natively one
4
- * frame), {@link JobsSheet} the dismissable sheet the native jobs pill opens.
5
- * The panel draws task jobs as `agent` nodes and the others as a status dot,
6
- * the one-line label and a live `elapsed`; the sheet lists every job and
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 { type Component, Container } from "../tui";
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
- content.body,
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 { Effort } from "@oh-my-pi/pi-ai";
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: Effort,
19
- availableLevels: Effort[],
20
- onSelect: (level: Effort) => void,
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(getThinkingLevelMetadata);
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 Effort);
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
- chips: [{ text: "", dot: `thinking${item.value.charAt(0).toUpperCase()}${item.value.slice(1)}` }],
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, welcome model labels,
7
- * status-bar inputs) are also written under the empty project, so a folder that
8
- * never ran omp still paints with the user's theme and status bar: those are
9
- * rarely project-specific, and path/branch render live. Recent sessions and LSP
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
- type EntryKind = "ui" | "welcome" | "recent-sessions" | "lsp-servers" | "status";
52
- /** Kinds mirrored under {@link ANY_PROJECT} as the fallback for projects without their own row. */
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
- /** Best-effort upsert: a failed write only costs the next launch its speculation. */
318
- #put(cwd: string, kind: EntryKind, value: unknown): void {
319
- const project = path.resolve(cwd);
320
- const json = JSON.stringify(value);
321
- const key = `${project}\0${kind}`;
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}`;