@oh-my-pi/pi-tui 18.4.12 → 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 +31 -0
- package/dist/types/native/node.d.ts +16 -3
- 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/desktop-notify.ts +2 -1
- package/src/native/node.ts +18 -3
- 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
|
@@ -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}`;
|
package/src/prompt/composer.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { postmortem } from "@oh-my-pi/pi-utils";
|
|
|
22
22
|
import { CustomEditor } from "./custom-editor";
|
|
23
23
|
import type { WordCompletionMethod } from "./word-completion";
|
|
24
24
|
import { type AnimationFrame, TranscriptContainer } from "../chrome/transcript-container";
|
|
25
|
-
import {
|
|
25
|
+
import { WelcomeComponent } from "./welcome";
|
|
26
26
|
import { ensureThemeSync, getEditorTheme, theme } from "../theme/theme";
|
|
27
27
|
|
|
28
28
|
const DOUBLE_INTERRUPT_MS = 500;
|
|
@@ -58,11 +58,6 @@ export const COMPOSER_DEFAULTS: ComposerPreferences = {
|
|
|
58
58
|
/** Welcome data that can be supplied initially or patched as startup resolves it. */
|
|
59
59
|
export interface ComposerWelcomeUpdate {
|
|
60
60
|
readonly version?: string;
|
|
61
|
-
readonly modelName?: string;
|
|
62
|
-
readonly providerName?: string;
|
|
63
|
-
readonly recentSessions?: readonly RecentSession[];
|
|
64
|
-
/** Detected project servers; `null` means LSP is disabled and hides the welcome section. */
|
|
65
|
-
readonly lspServers?: readonly LspServerInfo[] | null;
|
|
66
61
|
}
|
|
67
62
|
|
|
68
63
|
/**
|
|
@@ -215,10 +210,6 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
215
210
|
#preferences: ComposerPreferences;
|
|
216
211
|
#welcome: WelcomeComponent | undefined;
|
|
217
212
|
#version = "";
|
|
218
|
-
#modelName = "";
|
|
219
|
-
#providerName = "";
|
|
220
|
-
#recentSessions: RecentSession[] = [];
|
|
221
|
-
#lspServers: LspServerInfo[] | null = [];
|
|
222
213
|
#headerBefore: readonly Component[] = [];
|
|
223
214
|
#headerAfter: readonly Component[] = [];
|
|
224
215
|
#runtimeChildren: readonly Component[] = [];
|
|
@@ -279,6 +270,8 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
279
270
|
#transientChrome: ReadonlySet<Component> = new Set();
|
|
280
271
|
#transientChromeFloor: number | undefined;
|
|
281
272
|
#anchorAfterInlineRetirement = false;
|
|
273
|
+
/** Rows the chrome below each below-transcript root took in the last frame (see {@link rowsBelow}). */
|
|
274
|
+
#rowsBelow = new Map<Component, number>();
|
|
282
275
|
#lastInterruptAt = 0;
|
|
283
276
|
/** Last described surface; its arrays are reused while their children are unchanged. */
|
|
284
277
|
#nativeSurface: NativeSurface = { main: [], dock: [] };
|
|
@@ -347,6 +340,27 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
347
340
|
this.ui.addChild(this.#statusHost);
|
|
348
341
|
this.ui.setFocus(this.editor);
|
|
349
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Rows the below-transcript chrome under `root` (editor, status line, …)
|
|
345
|
+
* took in the last frame, so a root that grows upward can cap itself to
|
|
346
|
+
* the screen rows left above them; `undefined` before `root` was laid out.
|
|
347
|
+
*/
|
|
348
|
+
rowsBelow(root: Component): number | undefined {
|
|
349
|
+
return this.#rowsBelow.get(root);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Keep the input on the bottom row while the live rows cannot fill the
|
|
354
|
+
* screen, as after an inline decision panel closes. A tall block that just
|
|
355
|
+
* left the chrome above the editor (a command report) may have scrolled
|
|
356
|
+
* rows into native history that cannot be pulled back; without the pin the
|
|
357
|
+
* editor would jump up to where the shorter frame now ends. The pin lifts
|
|
358
|
+
* once live rows fill the screen again.
|
|
359
|
+
*/
|
|
360
|
+
pinInputToBottom(): void {
|
|
361
|
+
this.#anchorAfterInlineRetirement = true;
|
|
362
|
+
}
|
|
363
|
+
|
|
350
364
|
/** Compose the bounded mutable viewport and the next ordered history append. */
|
|
351
365
|
renderFrame(viewport: ViewportSize): TerminalFramePlan {
|
|
352
366
|
if (!this.#started || this.#stopped) return { viewport: [] };
|
|
@@ -373,10 +387,12 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
373
387
|
let transientRows = 0;
|
|
374
388
|
let displacingRows = 0;
|
|
375
389
|
let decisionPanelOpen = false;
|
|
390
|
+
const ends: { root: Component; end: number }[] = [];
|
|
376
391
|
for (const root of afterRoots) {
|
|
377
392
|
const chrome: Component = root;
|
|
378
393
|
const start = after.length;
|
|
379
394
|
this.#renderBelowRoot(root, width, after, afterSpans);
|
|
395
|
+
ends.push({ root, end: after.length });
|
|
380
396
|
if (this.#transientChrome.has(root)) transientRows += after.length - start;
|
|
381
397
|
if (
|
|
382
398
|
chrome.retireDisplacedTranscript ||
|
|
@@ -386,6 +402,7 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
386
402
|
decisionPanelOpen = true;
|
|
387
403
|
}
|
|
388
404
|
}
|
|
405
|
+
this.#rowsBelow = new Map(ends.map(({ root, end }) => [root, after.length - end]));
|
|
389
406
|
// Offer history under capacity pressure only: blocks stay live (and keep
|
|
390
407
|
// reflowing to the current width) while the screen has room. A batch
|
|
391
408
|
// leaves the mutable viewport in the same frame it is appended, so its
|
|
@@ -871,7 +888,7 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
871
888
|
this.ui.requestRender();
|
|
872
889
|
}
|
|
873
890
|
|
|
874
|
-
/** Patch welcome data in place as
|
|
891
|
+
/** Patch welcome data in place as version, session, and project discovery complete. */
|
|
875
892
|
updateWelcome(update: ComposerWelcomeUpdate): void {
|
|
876
893
|
if (this.#stopped) return;
|
|
877
894
|
this.#applyWelcomeUpdate(update);
|
|
@@ -880,11 +897,6 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
880
897
|
const welcome = this.#welcome;
|
|
881
898
|
if (!welcome) return;
|
|
882
899
|
if (update.version !== undefined) welcome.setVersion(this.#version);
|
|
883
|
-
if (update.modelName !== undefined || update.providerName !== undefined) {
|
|
884
|
-
welcome.setModel(this.#modelName, this.#providerName);
|
|
885
|
-
}
|
|
886
|
-
if (update.recentSessions !== undefined) welcome.setRecentSessions(this.#recentSessions);
|
|
887
|
-
if (update.lspServers !== undefined) welcome.setLspServers(this.#lspServers);
|
|
888
900
|
this.ui.requestRender();
|
|
889
901
|
}
|
|
890
902
|
|
|
@@ -965,20 +977,10 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
965
977
|
|
|
966
978
|
#applyWelcomeUpdate(update: ComposerWelcomeUpdate): void {
|
|
967
979
|
if (update.version !== undefined) this.#version = update.version;
|
|
968
|
-
if (update.modelName !== undefined) this.#modelName = update.modelName;
|
|
969
|
-
if (update.providerName !== undefined) this.#providerName = update.providerName;
|
|
970
|
-
if (update.recentSessions !== undefined) this.#recentSessions = [...update.recentSessions];
|
|
971
|
-
if (update.lspServers !== undefined) this.#lspServers = update.lspServers && [...update.lspServers];
|
|
972
980
|
}
|
|
973
981
|
|
|
974
982
|
#ensureWelcome(): void {
|
|
975
|
-
this.#welcome ??= new WelcomeComponent(
|
|
976
|
-
this.#version,
|
|
977
|
-
this.#modelName,
|
|
978
|
-
this.#providerName,
|
|
979
|
-
this.#recentSessions,
|
|
980
|
-
this.#lspServers,
|
|
981
|
-
);
|
|
983
|
+
this.#welcome ??= new WelcomeComponent(this.#version);
|
|
982
984
|
}
|
|
983
985
|
|
|
984
986
|
#rebuildHeader(): void {
|
|
@@ -197,8 +197,14 @@ function normalizePastedPath(path: string): string {
|
|
|
197
197
|
try {
|
|
198
198
|
return url.fileURLToPath(unquoted);
|
|
199
199
|
} catch {
|
|
200
|
-
//
|
|
201
|
-
//
|
|
200
|
+
// Windows rejects drive-less URLs (`file:///Users/…`, forwarded from a
|
|
201
|
+
// macOS pasteboard or a remote session); decode them as POSIX paths.
|
|
202
|
+
try {
|
|
203
|
+
return url.fileURLToPath(unquoted, { windows: false });
|
|
204
|
+
} catch {
|
|
205
|
+
// Malformed file URL: drop through to the shell-unescape branch
|
|
206
|
+
// so the caller can still reject it as a non-explicit path.
|
|
207
|
+
}
|
|
202
208
|
}
|
|
203
209
|
}
|
|
204
210
|
return unquoted.replace(SHELL_ESCAPED_PATH_CHAR_REGEX, "$1");
|
package/src/prompt/tips.txt
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
Tired of typing "keep going"? Just send a '.'
|
|
2
|
+
Type `#` anywhere in your prompt for quick actions — copy the line or the whole prompt, undo, jump the cursor
|
|
3
|
+
Type `/` at the start of your prompt to browse every command
|
|
4
|
+
Start your prompt with `!` to run it as bash — `!!` runs it without adding the output to context
|
|
5
|
+
Start your prompt with `$ ` to run Python — `$$ ` runs it without adding the output to context
|
|
2
6
|
You can /btw to ask a side question
|
|
3
7
|
Use /tan to fork the current conversation into a background agent
|
|
4
8
|
With default keybindings, {key:ctrl+d} deletes the character under the cursor instead of quitting your draft — see /hotkeys for your exit key
|