@oh-my-pi/pi-tui 18.9.2 → 18.10.1
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 +53 -105
- package/dist/types/app-keybindings.d.ts +5 -0
- package/dist/types/chat/assistant-message.d.ts +1 -1
- package/dist/types/chat/user-message.d.ts +3 -2
- package/dist/types/chrome/countdown-timer.d.ts +3 -2
- package/dist/types/debug-server.d.ts +2 -0
- package/dist/types/overlays/annotation-overlay.d.ts +19 -0
- package/dist/types/overlays/ask-dialog.d.ts +11 -6
- package/dist/types/overlays/btw-history-panel.d.ts +2 -6
- package/dist/types/overlays/hook-editor.d.ts +7 -2
- package/dist/types/overlays/hook-input.d.ts +12 -4
- package/dist/types/overlays/jobs-panel.d.ts +8 -7
- package/dist/types/overlays/login-dialog.d.ts +8 -3
- package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
- package/dist/types/overlays/pause-screen.d.ts +5 -0
- package/dist/types/overlays/report-panel.d.ts +5 -1
- package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
- package/dist/types/overlays/session-account-selector.d.ts +4 -0
- package/dist/types/overlays/session-info-overlay.d.ts +7 -3
- package/dist/types/overlays/usage-dashboard.d.ts +6 -6
- package/dist/types/prompt/composer.d.ts +1 -20
- package/dist/types/status-line/context-usage.d.ts +1 -1
- package/dist/types/tools/grouped-file-output.d.ts +4 -1
- package/dist/types/tui.d.ts +10 -3
- package/package.json +9 -9
- package/src/app-keybindings.ts +5 -0
- package/src/apps/autoresearch-dashboard.ts +440 -27
- package/src/chat/assistant-message.ts +41 -13
- package/src/chat/user-message.ts +32 -19
- package/src/chrome/countdown-timer.ts +3 -3
- package/src/chrome/transcript-container.ts +202 -28
- package/src/debug-server.ts +51 -69
- package/src/hotkeys-markdown.ts +1 -0
- package/src/native/reconcile.ts +24 -6
- package/src/overlays/advisor-config.ts +259 -8
- package/src/overlays/annotation-overlay.ts +420 -168
- package/src/overlays/ask-dialog.ts +249 -105
- package/src/overlays/btw-history-panel.ts +178 -124
- package/src/overlays/hook-editor.ts +90 -32
- package/src/overlays/hook-input.ts +105 -19
- package/src/overlays/jobs-panel.ts +154 -132
- package/src/overlays/login-dialog.ts +201 -91
- package/src/overlays/mcp-add-wizard.ts +559 -279
- package/src/overlays/pause-screen.ts +81 -23
- package/src/overlays/plugin-selector.ts +17 -1
- package/src/overlays/queue-mode-selector.ts +10 -0
- package/src/overlays/report-panel.ts +6 -2
- package/src/overlays/reset-usage-selector.ts +130 -21
- package/src/overlays/session-account-selector.ts +36 -0
- package/src/overlays/session-info-overlay.ts +197 -76
- package/src/overlays/show-images-selector.ts +1 -0
- package/src/overlays/theme-selector.ts +6 -1
- package/src/overlays/thinking-selector.ts +8 -4
- package/src/overlays/usage-dashboard.ts +495 -143
- package/src/prompt/composer.ts +35 -72
- package/src/status-line/context-usage.ts +154 -57
- package/src/tools/grep.ts +1 -0
- package/src/tools/grouped-file-output.ts +11 -2
- package/src/tui.ts +27 -4
package/src/debug-server.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createServer, type Server, type Socket } from "node:net";
|
|
1
|
+
import { type IpcConnection, IpcServer } from "@oh-my-pi/pi-natives";
|
|
3
2
|
import { type Component, isFocusable, type OverlayOptions, type TUI } from "./tui";
|
|
4
3
|
import { plainText } from "./native/spans";
|
|
5
4
|
|
|
@@ -209,72 +208,60 @@ function mouseSequence(x: number, y: number, action: string): string {
|
|
|
209
208
|
export class TuiDebugServer {
|
|
210
209
|
readonly #tui: TUI;
|
|
211
210
|
readonly #path: string;
|
|
212
|
-
#server:
|
|
213
|
-
#
|
|
211
|
+
#server: IpcServer | undefined;
|
|
212
|
+
#listening: Promise<boolean> | undefined;
|
|
213
|
+
#stopped = false;
|
|
214
|
+
#connections = new Set<IpcConnection>();
|
|
214
215
|
|
|
215
216
|
constructor(tui: TUI, path: string) {
|
|
216
217
|
this.#tui = tui;
|
|
217
218
|
this.#path = path;
|
|
218
219
|
}
|
|
219
220
|
|
|
221
|
+
/** Resolves true once the socket accepts connections, false if listen failed or the server stopped first. */
|
|
222
|
+
get ready(): Promise<boolean> {
|
|
223
|
+
return this.#listening ?? Promise.resolve(false);
|
|
224
|
+
}
|
|
225
|
+
|
|
220
226
|
start(): void {
|
|
221
|
-
if (this.#
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
} catch {
|
|
235
|
-
this.#server = undefined;
|
|
236
|
-
server.close();
|
|
237
|
-
}
|
|
227
|
+
if (this.#listening !== undefined) return;
|
|
228
|
+
this.#stopped = false;
|
|
229
|
+
this.#listening = IpcServer.listen(this.#path, { framing: "lines" }, connection => this.#accept(connection)).then(
|
|
230
|
+
server => {
|
|
231
|
+
if (this.#stopped) {
|
|
232
|
+
void server.close().catch(() => {});
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
this.#server = server;
|
|
236
|
+
return true;
|
|
237
|
+
},
|
|
238
|
+
() => false, // A live peer or unusable path disables the debug socket; the host application is unaffected.
|
|
239
|
+
);
|
|
238
240
|
}
|
|
239
241
|
|
|
240
242
|
stop(): void {
|
|
243
|
+
this.#stopped = true;
|
|
241
244
|
const server = this.#server;
|
|
242
245
|
this.#server = undefined;
|
|
243
|
-
|
|
244
|
-
this.#
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
} catch {
|
|
248
|
-
// A failed or already-closed listener still needs its stale path removed.
|
|
249
|
-
}
|
|
250
|
-
try {
|
|
251
|
-
if (existsSync(this.#path)) unlinkSync(this.#path);
|
|
252
|
-
} catch {
|
|
253
|
-
// Teardown must not affect the host application.
|
|
254
|
-
}
|
|
246
|
+
this.#listening = undefined;
|
|
247
|
+
for (const connection of this.#connections) connection.destroy();
|
|
248
|
+
this.#connections.clear();
|
|
249
|
+
void server?.close().catch(() => {});
|
|
255
250
|
}
|
|
256
251
|
|
|
257
|
-
#accept(
|
|
258
|
-
this.#
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (line.length > 0) this.#handleLine(socket, line);
|
|
268
|
-
newline = buffer.indexOf("\n");
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
socket.on("error", error => {
|
|
272
|
-
void error;
|
|
273
|
-
});
|
|
274
|
-
socket.on("close", () => this.#sockets.delete(socket));
|
|
252
|
+
#accept(connection: IpcConnection): void {
|
|
253
|
+
if (this.#stopped) {
|
|
254
|
+
connection.destroy();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
this.#connections.add(connection);
|
|
258
|
+
connection.start(
|
|
259
|
+
line => void this.#handleLine(connection, line),
|
|
260
|
+
() => this.#connections.delete(connection),
|
|
261
|
+
);
|
|
275
262
|
}
|
|
276
263
|
|
|
277
|
-
#handleLine(
|
|
264
|
+
async #handleLine(connection: IpcConnection, line: string): Promise<void> {
|
|
278
265
|
let request: DebugRequest;
|
|
279
266
|
try {
|
|
280
267
|
const parsed: unknown = JSON.parse(line);
|
|
@@ -282,36 +269,31 @@ export class TuiDebugServer {
|
|
|
282
269
|
throw new Error("request must be an object");
|
|
283
270
|
request = parsed as DebugRequest;
|
|
284
271
|
} catch (error) {
|
|
285
|
-
this.#write(
|
|
272
|
+
this.#write(connection, { ok: false, error: `malformed JSON: ${errorMessage(error)}` });
|
|
286
273
|
return;
|
|
287
274
|
}
|
|
275
|
+
let response: DebugResponse;
|
|
288
276
|
try {
|
|
289
|
-
|
|
290
|
-
if (request.op === "quit" && response.ok) {
|
|
291
|
-
this.#write(socket, response, () => {
|
|
292
|
-
this.#tui.stop();
|
|
293
|
-
process.exit(0);
|
|
294
|
-
});
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
this.#write(socket, response);
|
|
277
|
+
response = this.#dispatch(request);
|
|
298
278
|
} catch (error) {
|
|
299
|
-
|
|
279
|
+
response = { ok: false, error: errorMessage(error) };
|
|
280
|
+
}
|
|
281
|
+
this.#write(connection, response);
|
|
282
|
+
if (request.op === "quit" && response.ok) {
|
|
283
|
+
await connection.drained();
|
|
284
|
+
this.#tui.stop();
|
|
285
|
+
process.exit(0);
|
|
300
286
|
}
|
|
301
287
|
}
|
|
302
288
|
|
|
303
|
-
#write(
|
|
289
|
+
#write(connection: IpcConnection, response: DebugResponse): void {
|
|
304
290
|
let line: string;
|
|
305
291
|
try {
|
|
306
292
|
line = JSON.stringify(response);
|
|
307
293
|
} catch (error) {
|
|
308
294
|
line = JSON.stringify({ ok: false, error: errorMessage(error) });
|
|
309
295
|
}
|
|
310
|
-
|
|
311
|
-
socket.write(`${line}\n`, flushed);
|
|
312
|
-
} catch {
|
|
313
|
-
// Broken clients cannot affect the TUI.
|
|
314
|
-
}
|
|
296
|
+
connection.send(line);
|
|
315
297
|
}
|
|
316
298
|
|
|
317
299
|
#dispatch(request: DebugRequest): DebugResponse {
|
package/src/hotkeys-markdown.ts
CHANGED
|
@@ -135,6 +135,7 @@ function hotkeyGroups(bindings: HotkeysMarkdownBindings): HotkeyGroup[] {
|
|
|
135
135
|
{ keys: [act("app.history.search")], action: "Search prompt history" },
|
|
136
136
|
{ keys: [act("app.tools.expand")], action: "Toggle tool output expansion" },
|
|
137
137
|
{ keys: [act("app.tools.toggleVisibility")], action: "Toggle tool activity visibility" },
|
|
138
|
+
{ keys: [act("app.ask.hide")], action: "Hide/show a pending ask question" },
|
|
138
139
|
{ keys: [act("app.thinking.toggle")], action: "Toggle thinking block visibility" },
|
|
139
140
|
{ keys: [act("app.editor.external")], action: "Edit message in external editor" },
|
|
140
141
|
{ keys: [act("app.retry")], action: "Retry last failed assistant turn" },
|
package/src/native/reconcile.ts
CHANGED
|
@@ -405,13 +405,31 @@ export class Reconciler {
|
|
|
405
405
|
return this.#findEditorIn(node, state.id, "", state.owner, depth);
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
|
|
408
|
+
/**
|
|
409
|
+
* The first editor under `node`. An `overlay`/`picker` child outside a hoisted
|
|
410
|
+
* sheet is hoisted into `layer` under its `^` keypath (see {@link #entries}), so
|
|
411
|
+
* a field in a sheet a dock component describes is found under that id.
|
|
412
|
+
*/
|
|
413
|
+
#findEditorIn(
|
|
414
|
+
node: NativeNode,
|
|
415
|
+
id: string,
|
|
416
|
+
keypath: string,
|
|
417
|
+
owner: Owner,
|
|
418
|
+
depth: number,
|
|
419
|
+
hoist = true,
|
|
420
|
+
): string | null {
|
|
409
421
|
if (node.k === "editor" || node.k === "input") return id;
|
|
410
|
-
for (const entry of this.#entries(node.c, keypath, owner,
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
422
|
+
for (const entry of this.#entries(node.c, keypath, owner, false, null)) {
|
|
423
|
+
let found: string | null;
|
|
424
|
+
if (entry.comp !== undefined) {
|
|
425
|
+
found = this.#findEditorOf(entry.comp, depth + 1);
|
|
426
|
+
} else if (hoist && (entry.node.k === "overlay" || entry.node.k === "picker")) {
|
|
427
|
+
const path = entry.keypath;
|
|
428
|
+
const portalPath = keypath === "" ? `^${path}` : `${keypath}/^${path.slice(keypath.length + 1)}`;
|
|
429
|
+
found = this.#findEditorIn(entry.node, nodeId(owner.base, portalPath), portalPath, owner, depth, false);
|
|
430
|
+
} else {
|
|
431
|
+
found = this.#findEditorIn(entry.node, entry.id, entry.keypath, owner, depth, hoist);
|
|
432
|
+
}
|
|
415
433
|
if (found !== null) return found;
|
|
416
434
|
}
|
|
417
435
|
return null;
|
|
@@ -34,7 +34,16 @@ import { sanitizeDisplayWarnings } from "../render/render-utils";
|
|
|
34
34
|
import { formatKeyHint } from "../app-keybindings";
|
|
35
35
|
import { editorKey, editorKeys } from "../chrome/keybinding-hints";
|
|
36
36
|
import { HookEditorComponent } from "./hook-editor";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
buildBrowserItems,
|
|
39
|
+
MODEL_PICKER_COLUMNS,
|
|
40
|
+
ModelBrowser,
|
|
41
|
+
type ModelBrowserItem,
|
|
42
|
+
type ModelBrowserSource,
|
|
43
|
+
type ModelPickerCatalogue,
|
|
44
|
+
type ModelPickerOrder,
|
|
45
|
+
sortModelItems,
|
|
46
|
+
} from "./model-browser";
|
|
38
47
|
import { bottomBorder, divider, dividerSplit, PanelRows, row, topBorder, topBorderSplit } from "../chrome/overlay-box";
|
|
39
48
|
import { isLayoutMouseRoutable } from "../components/layout/geometry";
|
|
40
49
|
import { SplitPane } from "../components/layout/split-pane";
|
|
@@ -42,7 +51,16 @@ import { Stack } from "../components/layout/stack";
|
|
|
42
51
|
import type { TspPrefsProps, TspPrefsRow, TspSpan } from "@oh-my-pi/pi-wire";
|
|
43
52
|
import { col, node, span, text } from "../native/describe";
|
|
44
53
|
import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
45
|
-
import { actionHint, hintsRow, type NativeHint, overlayCard } from "../native/overlay";
|
|
54
|
+
import { actionBar, actionButton, actionHint, hintsRow, type NativeHint, overlayCard } from "../native/overlay";
|
|
55
|
+
import {
|
|
56
|
+
CLOSE_ACTION,
|
|
57
|
+
pickerAction,
|
|
58
|
+
pickerEvent,
|
|
59
|
+
routeSelectListPicker,
|
|
60
|
+
type SelectPickerOptions,
|
|
61
|
+
selectListPicker,
|
|
62
|
+
} from "../native/picker";
|
|
63
|
+
import { getThinkingLevelMetadata, parseThinkingLevel } from "../thinking";
|
|
46
64
|
|
|
47
65
|
export const ADVISOR_REVIEW_MODES = ["turn", "agent-end"] as const;
|
|
48
66
|
export const ADVISOR_SYNC_BACKLOG_MODES = ["off", "1", "3", "5", "strict"] as const;
|
|
@@ -205,6 +223,40 @@ function formatAdvisorTools(tools: readonly string[] | undefined, emptyLabel: st
|
|
|
205
223
|
/** Choice value for "no per-advisor override"; matches no {@link ADVISOR_SYNC_BACKLOG_MODES} entry. */
|
|
206
224
|
const SYNC_BACKLOG_INHERIT = "inherit";
|
|
207
225
|
|
|
226
|
+
/** The tools list's terminal-only "Done" row; the picker applies with its own action instead. */
|
|
227
|
+
const TOOLS_DONE = "__done";
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A picker editor as a component of its own: a component's root picker stays
|
|
231
|
+
* where it is described, the prefs page's editor slot, where a picker
|
|
232
|
+
* described inside the page would be hoisted into `layer` under the sheet.
|
|
233
|
+
*/
|
|
234
|
+
class EditorSheet implements Component {
|
|
235
|
+
node: NativeNode | undefined;
|
|
236
|
+
readonly #route: (event: NativeUiEvent) => void;
|
|
237
|
+
|
|
238
|
+
constructor(route: (event: NativeUiEvent) => void) {
|
|
239
|
+
this.#route = route;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
render(): readonly string[] {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
describe(): NativeNode | null {
|
|
247
|
+
return this.node ?? null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
251
|
+
this.#route(event);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Whether two memo dependency lists hold the same values by identity. */
|
|
256
|
+
function sameDeps(a: readonly unknown[], b: readonly unknown[]): boolean {
|
|
257
|
+
return a.length === b.length && a.every((value, i) => value === b[i]);
|
|
258
|
+
}
|
|
259
|
+
|
|
208
260
|
/** One-line cadence label: mode plus interval. */
|
|
209
261
|
function formatReviewCadence(advisor: AdvisorConfig): string {
|
|
210
262
|
const reviewInterval = advisor.reviewInterval ?? 1;
|
|
@@ -297,7 +349,15 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
297
349
|
|
|
298
350
|
/** The advisor the detail screen (and its editors) edits; -1 on the roster or the shared instructions. */
|
|
299
351
|
#detailIndex = -1;
|
|
300
|
-
#nativePrefs: { signature: string; editor:
|
|
352
|
+
#nativePrefs: { signature: string; editor: NativeNode | undefined; node: NativeNode } | undefined;
|
|
353
|
+
/** The editor over the prefs page, rebuilt only when one of `deps` changes identity. */
|
|
354
|
+
#nativeEditor: { deps: readonly unknown[]; node: NativeNode } | undefined;
|
|
355
|
+
/** The model screen's catalogue and the advisor's current model among it (picker `current`). */
|
|
356
|
+
#modelItems: ModelBrowserItem[] = [];
|
|
357
|
+
#modelCurrent: string | undefined;
|
|
358
|
+
/** Picker sheet options of the current list screen (thinking, review mode, sync backlog, tools). */
|
|
359
|
+
#sheetOptions: SelectPickerOptions | undefined;
|
|
360
|
+
readonly #editorSheet = new EditorSheet(event => this.#routeEditorPicker(event));
|
|
301
361
|
#screen: Screen = "list";
|
|
302
362
|
/** The interactive element for the current screen. */
|
|
303
363
|
#active: Component = new SelectList([], 1, getSelectListTheme());
|
|
@@ -587,7 +647,7 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
587
647
|
this.#screen === "sync-backlog" ||
|
|
588
648
|
this.#screen === "tools" ||
|
|
589
649
|
this.#screen === "instructions";
|
|
590
|
-
const editor = overlay ? this.#
|
|
650
|
+
const editor = overlay ? this.#prefsEditor() : undefined;
|
|
591
651
|
const focus =
|
|
592
652
|
this.#screen === "detail" && this.#active instanceof SelectList
|
|
593
653
|
? (this.#active.getSelectedItem()?.value ?? null)
|
|
@@ -606,12 +666,137 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
606
666
|
const signature = JSON.stringify(props);
|
|
607
667
|
const prev = this.#nativePrefs;
|
|
608
668
|
if (prev && prev.signature === signature && prev.editor === editor) return prev.node;
|
|
609
|
-
|
|
669
|
+
let children: NativeChild[] = [];
|
|
670
|
+
if (editor?.k === "picker") {
|
|
671
|
+
this.#editorSheet.node = editor;
|
|
672
|
+
children = [this.#editorSheet];
|
|
673
|
+
} else if (editor) {
|
|
674
|
+
children = [editor];
|
|
675
|
+
}
|
|
610
676
|
const root = node("prefs", props, children);
|
|
611
677
|
this.#nativePrefs = { signature, editor, node: root };
|
|
612
678
|
return root;
|
|
613
679
|
}
|
|
614
680
|
|
|
681
|
+
/**
|
|
682
|
+
* The editor over the prefs page. Choices are picker sheets on `/model`'s
|
|
683
|
+
* anatomy (search, rows, the highlighted model's facts, one primary
|
|
684
|
+
* action); the instructions and review-interval editors sit in the page's
|
|
685
|
+
* editor card.
|
|
686
|
+
*/
|
|
687
|
+
#prefsEditor(): NativeNode {
|
|
688
|
+
const active = this.#active;
|
|
689
|
+
const memo = this.#nativeEditor;
|
|
690
|
+
if (active instanceof ModelBrowser) {
|
|
691
|
+
const order = active.pickerOrder({ providers: false, rest: active.query.trim() ? "Matches" : "All" });
|
|
692
|
+
const catalogue = active.pickerItems(this.#modelItems);
|
|
693
|
+
const preview = active.pickerPreview("compact", this.#modelCurrent);
|
|
694
|
+
const deps = [active, order, catalogue, preview, active.query, active.cursor, active.pickerSelected];
|
|
695
|
+
if (memo && sameDeps(memo.deps, deps)) return memo.node;
|
|
696
|
+
const sheet = this.#modelSheet(active, order, catalogue, preview);
|
|
697
|
+
this.#nativeEditor = { deps, node: sheet };
|
|
698
|
+
return sheet;
|
|
699
|
+
}
|
|
700
|
+
const options = this.#sheetOptions;
|
|
701
|
+
if (active instanceof SelectList && options) {
|
|
702
|
+
const view = active.pickerView();
|
|
703
|
+
const deps = [active, view.items, view.selected, view.pending, view.query, view.cursor];
|
|
704
|
+
if (memo && sameDeps(memo.deps, deps)) return memo.node;
|
|
705
|
+
const props = selectListPicker(active, options);
|
|
706
|
+
const sheet = node(
|
|
707
|
+
"picker",
|
|
708
|
+
this.#screen === "tools"
|
|
709
|
+
? {
|
|
710
|
+
...props,
|
|
711
|
+
items: props.items?.filter(item => item.id !== TOOLS_DONE),
|
|
712
|
+
actions: [
|
|
713
|
+
pickerAction("confirm", "Toggle", "enter"),
|
|
714
|
+
pickerAction("apply", "Apply", "escape", { primary: true, end: true }),
|
|
715
|
+
],
|
|
716
|
+
}
|
|
717
|
+
: props,
|
|
718
|
+
);
|
|
719
|
+
this.#nativeEditor = { deps, node: sheet };
|
|
720
|
+
return sheet;
|
|
721
|
+
}
|
|
722
|
+
if (memo && sameDeps(memo.deps, [active])) return memo.node;
|
|
723
|
+
const content = this.#screen === "review-interval" ? this.#intervalCard(active) : active;
|
|
724
|
+
const card = col([content], { role: "omp.prefs.editor" });
|
|
725
|
+
this.#nativeEditor = { deps: [active], node: card };
|
|
726
|
+
return card;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
#modelSheet(
|
|
730
|
+
browser: ModelBrowser,
|
|
731
|
+
order: ModelPickerOrder,
|
|
732
|
+
catalogue: ModelPickerCatalogue,
|
|
733
|
+
preview: readonly NativeChild[],
|
|
734
|
+
): NativeNode {
|
|
735
|
+
const name = this.#doc.advisors[this.#detailIndex]?.name || "This advisor";
|
|
736
|
+
const fallback = this.#defaultModelLabel ?? "the advisor role default";
|
|
737
|
+
return node(
|
|
738
|
+
"picker",
|
|
739
|
+
{
|
|
740
|
+
title: "Advisor model",
|
|
741
|
+
subtitle: `${name} reviews with this model. Unset, it uses ${fallback}.`,
|
|
742
|
+
icon: "model",
|
|
743
|
+
noun: "models",
|
|
744
|
+
size: "md",
|
|
745
|
+
layout: "rows",
|
|
746
|
+
preview: "below",
|
|
747
|
+
query: browser.query,
|
|
748
|
+
cursor: browser.cursor,
|
|
749
|
+
placeholder: "Search models…",
|
|
750
|
+
columns: MODEL_PICKER_COLUMNS,
|
|
751
|
+
...catalogue,
|
|
752
|
+
order: order.order,
|
|
753
|
+
...(order.hits ? { hits: order.hits } : {}),
|
|
754
|
+
selected: browser.pickerSelected,
|
|
755
|
+
...(this.#modelCurrent ? { current: [this.#modelCurrent] } : {}),
|
|
756
|
+
total: browser.baseCount,
|
|
757
|
+
empty: "No models available",
|
|
758
|
+
actions: [
|
|
759
|
+
pickerAction("use", "Use for advisor", "enter", { primary: true }),
|
|
760
|
+
browser.query.length > 0 ? { ...CLOSE_ACTION, label: "Clear search" } : CLOSE_ACTION,
|
|
761
|
+
],
|
|
762
|
+
},
|
|
763
|
+
preview,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** The review-interval field as a titled card: what the number means, the field, Cancel and Save. */
|
|
768
|
+
#intervalCard(input: Component): NativeNode {
|
|
769
|
+
const name = this.#doc.advisors[this.#detailIndex]?.name || "this advisor";
|
|
770
|
+
return overlayCard("omp.overlay.advisor.interval", `Review interval · ${name}`, [
|
|
771
|
+
text([span("Review every Nth eligible update; skipped updates join the next review.", "muted")], {
|
|
772
|
+
wrap: "word",
|
|
773
|
+
}),
|
|
774
|
+
input,
|
|
775
|
+
actionBar([
|
|
776
|
+
null,
|
|
777
|
+
actionButton("Cancel", "field-cancel", { keys: "escape" }),
|
|
778
|
+
actionButton("Save", "field-save", { keys: "enter", tone: "accent" }),
|
|
779
|
+
]),
|
|
780
|
+
]);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** A pointer event on the editor's picker sheet, on the path of the key it stands for. */
|
|
784
|
+
#routeEditorPicker(event: NativeUiEvent): void {
|
|
785
|
+
const picked = pickerEvent(event);
|
|
786
|
+
if (!picked) return;
|
|
787
|
+
const active = this.#active;
|
|
788
|
+
if (active instanceof ModelBrowser) {
|
|
789
|
+
if (picked.kind !== "action") active.routePickerItem(picked.item, picked.kind === "activate");
|
|
790
|
+
else if (picked.act === "close") active.handleCancel();
|
|
791
|
+
else if (picked.act === "use" && active.pickerSelected) active.routePickerItem(active.pickerSelected, true);
|
|
792
|
+
} else if (active instanceof SelectList) {
|
|
793
|
+
// The tools list applies on cancel, like its Esc.
|
|
794
|
+
if (picked.kind === "action" && picked.act === "apply") active.onCancel?.();
|
|
795
|
+
else routeSelectListPicker(active, event);
|
|
796
|
+
}
|
|
797
|
+
this.#cb.requestRender();
|
|
798
|
+
}
|
|
799
|
+
|
|
615
800
|
/**
|
|
616
801
|
* Native page events, each through the roster/detail selections the keys
|
|
617
802
|
* make: a page opens that advisor (or the shared instructions), a field
|
|
@@ -619,6 +804,14 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
619
804
|
* or the page cancels like Esc.
|
|
620
805
|
*/
|
|
621
806
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
807
|
+
// The review-interval card's buttons, on its Enter and Esc paths.
|
|
808
|
+
if (event.type === "action" && (event.act === "field-save" || event.act === "field-cancel")) {
|
|
809
|
+
const input = this.#active;
|
|
810
|
+
if (!(input instanceof Input)) return;
|
|
811
|
+
if (event.act === "field-save") input.onSubmit?.(input.getValue());
|
|
812
|
+
else input.onEscape?.();
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
622
815
|
if (event.key !== "") return;
|
|
623
816
|
const index = this.#pageIndex();
|
|
624
817
|
switch (event.type) {
|
|
@@ -979,9 +1172,11 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
979
1172
|
|
|
980
1173
|
// ───────────────────────────── screens ───────────────────────────
|
|
981
1174
|
|
|
982
|
-
|
|
1175
|
+
/** `sheet`: how a list screen describes as a picker over the prefs page. */
|
|
1176
|
+
#setScreen(screen: Screen, active: Component, footerHint: string, sheet?: SelectPickerOptions): void {
|
|
983
1177
|
this.#screen = screen;
|
|
984
1178
|
this.#active = active;
|
|
1179
|
+
this.#sheetOptions = sheet;
|
|
985
1180
|
this.#footerHint = footerHint;
|
|
986
1181
|
this.#previewScroll = 0;
|
|
987
1182
|
this.#cb.requestRender();
|
|
@@ -1155,6 +1350,8 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1155
1350
|
}
|
|
1156
1351
|
|
|
1157
1352
|
#onDetailSelect(index: number, field: string): void {
|
|
1353
|
+
// Editors opened straight from the prefs page edit this advisor too.
|
|
1354
|
+
this.#detailIndex = index;
|
|
1158
1355
|
switch (field) {
|
|
1159
1356
|
case "toggleEnabled": {
|
|
1160
1357
|
const a = this.#doc.advisors[index];
|
|
@@ -1232,11 +1429,19 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1232
1429
|
}
|
|
1233
1430
|
const items = buildBrowserItems(models);
|
|
1234
1431
|
sortModelItems(items, { mruOrder });
|
|
1432
|
+
// The configured model, whatever thinking suffix it carries, is the picker's current row.
|
|
1433
|
+
const model = this.#doc.advisors[index]?.model?.trim();
|
|
1434
|
+
const current = model
|
|
1435
|
+
? items.find(item => model === item.selector || model.startsWith(`${item.selector}:`))?.selector
|
|
1436
|
+
: undefined;
|
|
1437
|
+
this.#modelItems = items;
|
|
1438
|
+
this.#modelCurrent = current;
|
|
1235
1439
|
|
|
1236
1440
|
const picker = new ModelBrowser(this.#deps.browserSource, {});
|
|
1237
1441
|
picker.setMruOrder(mruOrder);
|
|
1238
1442
|
picker.setPerfStats(this.#deps.browserSource.modelPerf);
|
|
1239
1443
|
picker.setItems(items);
|
|
1444
|
+
if (current) picker.selectSelector(current);
|
|
1240
1445
|
picker.onActivate = item => {
|
|
1241
1446
|
const efforts = getSupportedEfforts(item.model);
|
|
1242
1447
|
if (efforts.length === 0) {
|
|
@@ -1259,6 +1464,11 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1259
1464
|
const items: SelectItem[] = [{ value: "", label: "(model default thinking)" }];
|
|
1260
1465
|
for (const effort of efforts) items.push({ value: effort, label: effort });
|
|
1261
1466
|
const list = new SelectList(items, Math.max(1, items.length), getSelectListTheme());
|
|
1467
|
+
// The level the advisor already runs this model at, when it is this model.
|
|
1468
|
+
const model = this.#doc.advisors[index]?.model?.trim();
|
|
1469
|
+
const current =
|
|
1470
|
+
model === selector ? "" : model?.startsWith(`${selector}:`) ? model.slice(selector.length + 1) : undefined;
|
|
1471
|
+
if (current !== undefined) list.setSelectedValue(current);
|
|
1262
1472
|
list.onSelect = item => {
|
|
1263
1473
|
// Values are supported efforts or the empty model-default choice.
|
|
1264
1474
|
this.#doc.advisors[index].model = item.value ? `${selector}:${item.value}` : selector;
|
|
@@ -1270,6 +1480,23 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1270
1480
|
"thinking",
|
|
1271
1481
|
list,
|
|
1272
1482
|
`Thinking effort for ${selector} · ${editorKey("tui.select.confirm")} / click pick · ${editorKey("tui.select.cancel")} back`,
|
|
1483
|
+
{
|
|
1484
|
+
title: "Thinking level",
|
|
1485
|
+
subtitle: `How hard ${this.#doc.advisors[index]?.name || "the advisor"} reasons on ${selector}.`,
|
|
1486
|
+
icon: "thinking",
|
|
1487
|
+
noun: "levels",
|
|
1488
|
+
current: current === undefined ? [] : [current],
|
|
1489
|
+
confirm: "Use level",
|
|
1490
|
+
decorate: item => {
|
|
1491
|
+
if (!item.value) return { label: "Model default", detail: "No level; the provider decides." };
|
|
1492
|
+
const level = parseThinkingLevel(item.value);
|
|
1493
|
+
const dot = `thinking${item.value.charAt(0).toUpperCase()}${item.value.slice(1)}`;
|
|
1494
|
+
return {
|
|
1495
|
+
detail: level ? getThinkingLevelMetadata(level).description : undefined,
|
|
1496
|
+
chips: [{ text: "", dot }],
|
|
1497
|
+
};
|
|
1498
|
+
},
|
|
1499
|
+
},
|
|
1273
1500
|
);
|
|
1274
1501
|
}
|
|
1275
1502
|
#showReviewModePicker(index: number): void {
|
|
@@ -1299,6 +1526,14 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1299
1526
|
"review-mode",
|
|
1300
1527
|
list,
|
|
1301
1528
|
`${editorKey("tui.select.confirm")} / click choose review mode · ${editorKey("tui.select.cancel")} back`,
|
|
1529
|
+
{
|
|
1530
|
+
title: "Review mode",
|
|
1531
|
+
subtitle: `When ${advisor.name || "the advisor"} reviews the session.`,
|
|
1532
|
+
icon: "review",
|
|
1533
|
+
noun: "modes",
|
|
1534
|
+
current: [current],
|
|
1535
|
+
decorate: item => ({ label: item.value }),
|
|
1536
|
+
},
|
|
1302
1537
|
);
|
|
1303
1538
|
}
|
|
1304
1539
|
|
|
@@ -1361,6 +1596,14 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1361
1596
|
"sync-backlog",
|
|
1362
1597
|
list,
|
|
1363
1598
|
`${editorKey("tui.select.confirm")} / click choose catch-up policy · ${editorKey("tui.select.cancel")} back`,
|
|
1599
|
+
{
|
|
1600
|
+
title: "Sync backlog",
|
|
1601
|
+
subtitle: `How long the session waits for ${advisor.name || "the advisor"}'s pending reviews.`,
|
|
1602
|
+
icon: "time",
|
|
1603
|
+
noun: "policies",
|
|
1604
|
+
current: [current ?? SYNC_BACKLOG_INHERIT],
|
|
1605
|
+
decorate: item => ({ label: item.value }),
|
|
1606
|
+
},
|
|
1364
1607
|
);
|
|
1365
1608
|
}
|
|
1366
1609
|
|
|
@@ -1370,7 +1613,7 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1370
1613
|
value: name,
|
|
1371
1614
|
label: `${selected.has(name) ? "[x]" : "[ ]"} ${name}`,
|
|
1372
1615
|
}));
|
|
1373
|
-
items.push({ value:
|
|
1616
|
+
items.push({ value: TOOLS_DONE, label: "Done" });
|
|
1374
1617
|
const list = new SelectList(items, Math.max(1, items.length), getSelectListTheme());
|
|
1375
1618
|
list.setSelectedIndex(cursor);
|
|
1376
1619
|
let cursorIndex = cursor;
|
|
@@ -1378,7 +1621,7 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1378
1621
|
cursorIndex = items.findIndex(i => i.value === item.value);
|
|
1379
1622
|
};
|
|
1380
1623
|
list.onSelect = item => {
|
|
1381
|
-
if (item.value ===
|
|
1624
|
+
if (item.value === TOOLS_DONE) {
|
|
1382
1625
|
this.#doc.advisors[index].tools = commitTools(selected, all, this.#deps.defaultToolNames);
|
|
1383
1626
|
this.#dirty = true;
|
|
1384
1627
|
this.#showDetail(index);
|
|
@@ -1397,6 +1640,14 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1397
1640
|
"tools",
|
|
1398
1641
|
list,
|
|
1399
1642
|
`${editorKey("tui.select.confirm")} / click toggle · select Done or ${editorKey("tui.select.cancel")} to apply (empty = no tools; read/grep/glob = default)`,
|
|
1643
|
+
{
|
|
1644
|
+
title: "Tools",
|
|
1645
|
+
subtitle: `What ${this.#doc.advisors[index]?.name || "the advisor"} may call. None grants no tools; read, grep and glob are the default.`,
|
|
1646
|
+
icon: "tools",
|
|
1647
|
+
noun: "tools",
|
|
1648
|
+
current: [...selected],
|
|
1649
|
+
decorate: item => ({ label: item.value }),
|
|
1650
|
+
},
|
|
1400
1651
|
);
|
|
1401
1652
|
}
|
|
1402
1653
|
|