@timurproko/a1 0.2.5-dev.698 → 0.2.5-dev.704
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/dist/app/session-shell/session-shell.js +9 -8
- package/dist/composition/owned-ui.js +2 -0
- package/dist/contracts/presentation/index.d.ts +6 -1
- package/dist/contracts/presentation/index.js +4 -2
- package/dist/features/owned-ui/settings-app.d.ts +1 -1
- package/dist/features/owned-ui/settings-app.js +334 -63
- package/dist/integrations/pi/components/models-dialog.js +2 -2
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +40 -2
- package/dist/integrations/pi/components/skills-dialog.js +2 -2
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +4 -2
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +48 -12
- package/dist/integrations/pi/components/upstream/components/session-selector.js +14 -14
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +5 -4
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +6 -4
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +7 -6
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +24 -18
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/components/dialog-panel.d.ts +15 -0
- package/dist/ui/components/dialog-panel.js +50 -3
- package/dist/ui/components/index.d.ts +3 -3
- package/dist/ui/components/index.js +2 -2
- package/dist/ui/components/line-input.d.ts +2 -0
- package/dist/ui/components/line-input.js +2 -2
- package/dist/ui/components/prompt-input.d.ts +2 -2
- package/dist/ui/components/prompt-input.js +5 -5
- package/dist/ui/components/shortcut-hints.d.ts +8 -3
- package/dist/ui/components/shortcut-hints.js +25 -5
- package/dist/ui/components/shortcuts.d.ts +1 -1
- package/dist/ui/components/shortcuts.js +4 -0
- package/dist/ui/components/value-menu.js +3 -1
- package/dist/ui/settings/manager.d.ts +1 -0
- package/dist/ui/settings/manager.js +9 -3
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DIALOG_CLOSE_SHORTCUT_HINT } from "../../contracts/presentation/index.js";
|
|
2
|
-
import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget,
|
|
1
|
+
import { DIALOG_BACK_SHORTCUT_HINT, DIALOG_CLOSE_SHORTCUT_HINT } from "../../contracts/presentation/index.js";
|
|
2
|
+
import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, numericValues, RAIL_COLUMNS, renderDialogPanel, renderSteppedDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderListRow, renderNote, renderShortcutHintsWithClose, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, isThumbRow, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
|
|
3
3
|
import { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
|
|
4
4
|
export { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
|
|
5
5
|
const SCOPE = SETTINGS_APP_ID;
|
|
@@ -11,6 +11,8 @@ const SETTINGS_STATUS_ROWS = 1;
|
|
|
11
11
|
const SETTINGS_CONTENT_INSET = 1;
|
|
12
12
|
/** The panel a setting with parts opens: its own keys, its own hint. */
|
|
13
13
|
const DIALOG_SCOPE = `${SETTINGS_APP_ID}-parts`;
|
|
14
|
+
const MODEL_SCOPE = `${SETTINGS_APP_ID}-models`;
|
|
15
|
+
const MODEL_LEVEL_SCOPE = `${SETTINGS_APP_ID}-model-levels`;
|
|
14
16
|
const SCROLLBAR_TOP_INSET = 0;
|
|
15
17
|
/** Identity of the settings list rail in the shared rail state. */
|
|
16
18
|
const RAIL_KEY = "settings";
|
|
@@ -21,32 +23,43 @@ const SEARCH_PLACEHOLDER = "search settings";
|
|
|
21
23
|
/** What a structured value offers instead of printing itself. */
|
|
22
24
|
const CONFIGURE = "configure";
|
|
23
25
|
export const SETTINGS_SHORTCUTS = new ShortcutRegistry();
|
|
24
|
-
SETTINGS_SHORTCUTS.declare({ key: "/", scope: SCOPE, description: "Search settings", section: "Change", hint: { keys: "/", does: "
|
|
25
|
-
SETTINGS_SHORTCUTS.declare({ key: "up", scope: SCOPE, description: "Previous setting", section: "Navigate", hint: { keys: "↑↓", does: "
|
|
26
|
-
SETTINGS_SHORTCUTS.declare({ key: "down", scope: SCOPE, description: "Next setting", section: "Navigate", hint: { keys: "↑↓", does: "
|
|
27
|
-
SETTINGS_SHORTCUTS.declare({ key: "shift+up", scope: SCOPE, description: "Previous section", section: "Navigate", hint: { keys: "Shift+↑↓", does: "
|
|
28
|
-
SETTINGS_SHORTCUTS.declare({ key: "shift+down", scope: SCOPE, description: "Next section", section: "Navigate", hint: { keys: "Shift+↑↓", does: "
|
|
26
|
+
SETTINGS_SHORTCUTS.declare({ key: "/", scope: SCOPE, description: "Search settings", section: "Change", hint: { keys: "/", does: "search" } }, "open-filter");
|
|
27
|
+
SETTINGS_SHORTCUTS.declare({ key: "up", scope: SCOPE, description: "Previous setting", section: "Navigate", hint: { keys: "↑↓", does: "navigate" } }, "move-up");
|
|
28
|
+
SETTINGS_SHORTCUTS.declare({ key: "down", scope: SCOPE, description: "Next setting", section: "Navigate", hint: { keys: "↑↓", does: "navigate" } }, "move-down");
|
|
29
|
+
SETTINGS_SHORTCUTS.declare({ key: "shift+up", scope: SCOPE, description: "Previous section", section: "Navigate", hint: { keys: "Shift+↑↓", does: "jump" } }, "block-up");
|
|
30
|
+
SETTINGS_SHORTCUTS.declare({ key: "shift+down", scope: SCOPE, description: "Next section", section: "Navigate", hint: { keys: "Shift+↑↓", does: "jump" } }, "block-down");
|
|
29
31
|
SETTINGS_SHORTCUTS.declare({ key: "pageUp", scope: SCOPE, description: "Up a page", section: "Navigate" }, "page-up");
|
|
30
32
|
SETTINGS_SHORTCUTS.declare({ key: "pageDown", scope: SCOPE, description: "Down a page", section: "Navigate" }, "page-down");
|
|
31
33
|
// Compatibility: the same chords the transcript uses for its content boundaries; plain
|
|
32
34
|
// Home and End stay with the search input's cursor.
|
|
33
35
|
SETTINGS_SHORTCUTS.declare({ key: "ctrl+home", scope: SCOPE, description: "First setting", section: "Navigate" }, "first");
|
|
34
36
|
SETTINGS_SHORTCUTS.declare({ key: "ctrl+end", scope: SCOPE, description: "Last setting", section: "Navigate" }, "last");
|
|
35
|
-
SETTINGS_SHORTCUTS.declare({ key: "enter", scope: SCOPE, description: "Change value", section: "Change", hint: { keys: "Enter
|
|
36
|
-
SETTINGS_SHORTCUTS.declare({ key: "
|
|
37
|
-
SETTINGS_SHORTCUTS.declare({ key: "
|
|
38
|
-
SETTINGS_SHORTCUTS.declare({ key: "
|
|
39
|
-
SETTINGS_SHORTCUTS.declare({ key: "enter", scope: DIALOG_SCOPE, description: "Change this part", section: "Parts", hint: { keys: "Enter
|
|
40
|
-
SETTINGS_SHORTCUTS.declare({ key: "space", scope: DIALOG_SCOPE, description: "Change this part", section: "Parts", hint: { keys: "Enter/Space", does: "to change" } }, "part-change");
|
|
37
|
+
SETTINGS_SHORTCUTS.declare({ key: "enter", scope: SCOPE, description: "Change value", section: "Change", hint: { keys: "Enter", does: "change" } }, "activate");
|
|
38
|
+
SETTINGS_SHORTCUTS.declare({ key: "left", scope: SCOPE, description: "Previous value", section: "Change", hint: { keys: "←→", does: "adjust" } }, "previous-value");
|
|
39
|
+
SETTINGS_SHORTCUTS.declare({ key: "right", scope: SCOPE, description: "Next value", section: "Change", hint: { keys: "←→", does: "adjust" } }, "next-value");
|
|
40
|
+
SETTINGS_SHORTCUTS.declare({ key: "ctrl+z", scope: SCOPE, description: "Undo setting change", section: "Change", hint: { keys: "Ctrl+Z", does: "undo" } }, "undo");
|
|
41
|
+
SETTINGS_SHORTCUTS.declare({ key: "enter", scope: DIALOG_SCOPE, description: "Change this part", section: "Parts", hint: { keys: "Enter", does: "change" } }, "part-change");
|
|
41
42
|
SETTINGS_SHORTCUTS.declare({ key: "left", scope: DIALOG_SCOPE, description: "Change this part", section: "Parts" }, "part-change");
|
|
42
43
|
SETTINGS_SHORTCUTS.declare({ key: "right", scope: DIALOG_SCOPE, description: "Change this part", section: "Parts" }, "part-change");
|
|
43
44
|
SETTINGS_SHORTCUTS.declare({ key: "up", scope: DIALOG_SCOPE, description: "Previous part", section: "Parts" }, "part-previous");
|
|
44
45
|
SETTINGS_SHORTCUTS.declare({ key: "down", scope: DIALOG_SCOPE, description: "Next part", section: "Parts" }, "part-next");
|
|
46
|
+
SETTINGS_SHORTCUTS.declare({ key: "ctrl+z", scope: DIALOG_SCOPE, description: "Undo setting change", section: "Parts", hint: { keys: "Ctrl+Z", does: "undo" } }, "undo");
|
|
45
47
|
SETTINGS_SHORTCUTS.declare({
|
|
46
48
|
key: "escape", scope: GLOBAL_SCOPE, description: "Close", section: "Screen",
|
|
47
49
|
hint: { keys: DIALOG_CLOSE_SHORTCUT_HINT.key, does: DIALOG_CLOSE_SHORTCUT_HINT.action },
|
|
48
50
|
}, "close");
|
|
49
51
|
assertNoShortcutConflicts(SETTINGS_SHORTCUTS.assemble());
|
|
52
|
+
const MODEL_SHORTCUTS = new ShortcutRegistry();
|
|
53
|
+
MODEL_SHORTCUTS.declare({ key: "edit", scope: MODEL_SCOPE, description: "Search models", section: "Choose", hint: { keys: "Type", does: "search" } }, "edit-filter");
|
|
54
|
+
MODEL_SHORTCUTS.declare({ key: "up", scope: MODEL_SCOPE, description: "Previous model", section: "Choose" }, "previous");
|
|
55
|
+
MODEL_SHORTCUTS.declare({ key: "down", scope: MODEL_SCOPE, description: "Next model", section: "Choose" }, "next");
|
|
56
|
+
MODEL_SHORTCUTS.declare({ key: "enter", scope: MODEL_SCOPE, description: "Select model", section: "Choose", hint: { keys: "Enter", does: "select" } }, "select");
|
|
57
|
+
MODEL_SHORTCUTS.declare({ key: "escape", scope: MODEL_SCOPE, description: "Close selector", section: "Choose", hint: { keys: DIALOG_CLOSE_SHORTCUT_HINT.key, does: DIALOG_CLOSE_SHORTCUT_HINT.action } }, "close");
|
|
58
|
+
MODEL_SHORTCUTS.declare({ key: "up", scope: MODEL_LEVEL_SCOPE, description: "Previous level", section: "Choose", hint: { keys: "↑↓", does: "navigate" } }, "previous");
|
|
59
|
+
MODEL_SHORTCUTS.declare({ key: "down", scope: MODEL_LEVEL_SCOPE, description: "Next level", section: "Choose", hint: { keys: "↑↓", does: "navigate" } }, "next");
|
|
60
|
+
MODEL_SHORTCUTS.declare({ key: "enter", scope: MODEL_LEVEL_SCOPE, description: "Select level", section: "Choose", hint: { keys: "Enter", does: "select" } }, "select");
|
|
61
|
+
MODEL_SHORTCUTS.declare({ key: "escape", scope: MODEL_LEVEL_SCOPE, description: "Return to models", section: "Choose", hint: { keys: DIALOG_BACK_SHORTCUT_HINT.key, does: DIALOG_BACK_SHORTCUT_HINT.action } }, "back");
|
|
62
|
+
assertNoShortcutConflicts(MODEL_SHORTCUTS.assemble());
|
|
50
63
|
const KEYS = {
|
|
51
64
|
"\u001b[A": "up",
|
|
52
65
|
"\u001b[B": "down",
|
|
@@ -66,6 +79,7 @@ const KEYS = {
|
|
|
66
79
|
"\n": "enter",
|
|
67
80
|
" ": "space",
|
|
68
81
|
"/": "/",
|
|
82
|
+
"\u001a": "ctrl+z",
|
|
69
83
|
};
|
|
70
84
|
/**
|
|
71
85
|
* The settings screen. Rows, sticky headers, scrolling, and the keymap come from
|
|
@@ -88,10 +102,13 @@ export class SettingsApp {
|
|
|
88
102
|
#interruptArmed = false;
|
|
89
103
|
// Invariant: pending values remain visible until the source reflects them.
|
|
90
104
|
#pending = new Map();
|
|
91
|
-
#
|
|
105
|
+
#undoHistory = [];
|
|
106
|
+
#editSequence = 0;
|
|
107
|
+
#undoRequests = 0;
|
|
108
|
+
#undoActive = false;
|
|
109
|
+
#closed = false;
|
|
92
110
|
#bodyTopForFrame = SETTINGS_TOP_RULE_ROWS + SETTINGS_TITLE_ROWS;
|
|
93
111
|
#bodyHeightForFrame = 0;
|
|
94
|
-
#panelTop = 0;
|
|
95
112
|
#panelTopForFrame = 0;
|
|
96
113
|
#hoverKey = null;
|
|
97
114
|
#hoverRegion = "label";
|
|
@@ -109,9 +126,13 @@ export class SettingsApp {
|
|
|
109
126
|
this.#session = session;
|
|
110
127
|
}
|
|
111
128
|
onActivate(host) {
|
|
129
|
+
this.#closed = false;
|
|
112
130
|
void this.#session.load().then(() => host.requestRender());
|
|
113
131
|
}
|
|
114
132
|
onClose(_host) {
|
|
133
|
+
this.#closed = true;
|
|
134
|
+
this.#undoHistory.length = 0;
|
|
135
|
+
this.#undoRequests = 0;
|
|
115
136
|
this.#clearActivityTimer();
|
|
116
137
|
this.#rails.clear();
|
|
117
138
|
}
|
|
@@ -121,7 +142,9 @@ export class SettingsApp {
|
|
|
121
142
|
const rows = this.#rows();
|
|
122
143
|
const selected = indexOfKey(rows, this.#selectedKey);
|
|
123
144
|
const footer = this.#footerLines(rect.width, theme);
|
|
124
|
-
|
|
145
|
+
// Invariant: the shared search input and structured panel each begin with their own standard rule.
|
|
146
|
+
// That rule replaces the ordinary content/footer divider instead of stacking beneath it.
|
|
147
|
+
const dividerRows = this.#filter === null && this.#structured === null ? SETTINGS_FOOTER_DIVIDER_ROWS : 0;
|
|
125
148
|
const topRows = Math.min(SETTINGS_TOP_RULE_ROWS, rect.height);
|
|
126
149
|
const contentHeight = Math.max(0, rect.height - topRows - dividerRows - footer.length);
|
|
127
150
|
let titleRows = this.#scroll <= 0 ? Math.min(SETTINGS_TITLE_ROWS, contentHeight) : 0;
|
|
@@ -144,7 +167,6 @@ export class SettingsApp {
|
|
|
144
167
|
this.#bodyTopForFrame = topRows + titleRows;
|
|
145
168
|
this.#bodyHeightForFrame = bodyHeight;
|
|
146
169
|
this.#panelTopForFrame = topRows + contentHeight + dividerRows;
|
|
147
|
-
this.#panelTop = this.#panelTopForFrame;
|
|
148
170
|
this.#scroll = layout.scroll;
|
|
149
171
|
const now = Date.now();
|
|
150
172
|
// Rationale: every way of scrolling ends in this frame, so a moved list is noticed here
|
|
@@ -278,6 +300,9 @@ export class SettingsApp {
|
|
|
278
300
|
case "next-value":
|
|
279
301
|
this.#cycle(rows, selected, action === "next-value" ? 1 : -1);
|
|
280
302
|
return { consumed: true };
|
|
303
|
+
case "undo":
|
|
304
|
+
this.#undo();
|
|
305
|
+
return { consumed: true };
|
|
281
306
|
default:
|
|
282
307
|
return { consumed: false };
|
|
283
308
|
}
|
|
@@ -318,30 +343,9 @@ export class SettingsApp {
|
|
|
318
343
|
this.#apply(menu.entry, value);
|
|
319
344
|
return { consumed: true };
|
|
320
345
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
// The panel's rows begin one line below its rule.
|
|
325
|
-
const panel = { firstRow: this.#panelTop + 1, rows: open.flags.length, valueColumn: this.#dialogValueColumn };
|
|
326
|
-
const row = event.row - 1 - panel.firstRow;
|
|
327
|
-
if (row < 0 || row >= panel.rows)
|
|
328
|
-
return { consumed: true, render: false };
|
|
329
|
-
if (event.kind === "motion") {
|
|
330
|
-
if (open.index === row)
|
|
331
|
-
return { consumed: true, render: false };
|
|
332
|
-
open.index = row;
|
|
333
|
-
return { consumed: true, render: true };
|
|
334
|
-
}
|
|
335
|
-
if (event.kind === "press") {
|
|
336
|
-
open.index = row;
|
|
337
|
-
// Rationale: pointing at the label picks the row; the value is what changes it,
|
|
338
|
-
// exactly as in the list behind the dialog.
|
|
339
|
-
const key = open.flags[row] ?? "";
|
|
340
|
-
const width = displayWidth((open.record[key] ?? false) ? "true" : "false");
|
|
341
|
-
if (dialogRowAt(panel, event.row - 1, event.column, width) !== null)
|
|
342
|
-
this.#toggleFlag(row);
|
|
343
|
-
return { consumed: true };
|
|
344
|
-
}
|
|
346
|
+
if (this.#structured !== null) {
|
|
347
|
+
// Invariant: the structured surface is keyboard-only. It owns every pointer report so
|
|
348
|
+
// neither its selection nor the Settings list behind it can react.
|
|
345
349
|
return { consumed: true, render: false };
|
|
346
350
|
}
|
|
347
351
|
if (event.kind === "wheel-up" || event.kind === "wheel-down") {
|
|
@@ -482,12 +486,25 @@ export class SettingsApp {
|
|
|
482
486
|
this.#hoverKey = null;
|
|
483
487
|
this.#hoverRegion = "label";
|
|
484
488
|
this.#rails.clear();
|
|
485
|
-
|
|
489
|
+
const modelDialog = entry.backend === "agent" && entry.id === "modelThinkingLevels";
|
|
490
|
+
this.#structured = {
|
|
491
|
+
entry,
|
|
492
|
+
flags: entry.flags.map(flag => flag.key),
|
|
493
|
+
modelDialog,
|
|
494
|
+
filter: modelDialog ? new LineInput("") : null,
|
|
495
|
+
step: "model",
|
|
496
|
+
modelKey: null,
|
|
497
|
+
modelIndex: 0,
|
|
498
|
+
index: 0,
|
|
499
|
+
record,
|
|
500
|
+
};
|
|
486
501
|
}
|
|
487
502
|
#structuredKey(data) {
|
|
488
503
|
const open = this.#structured;
|
|
489
504
|
if (open === null)
|
|
490
505
|
return { consumed: false };
|
|
506
|
+
if (open.modelDialog)
|
|
507
|
+
return this.#modelDialogKey(open, data);
|
|
491
508
|
switch (SETTINGS_SHORTCUTS.resolve(KEYS[data] ?? data, DIALOG_SCOPE)) {
|
|
492
509
|
case "close":
|
|
493
510
|
this.#structured = null;
|
|
@@ -501,15 +518,94 @@ export class SettingsApp {
|
|
|
501
518
|
case "part-change":
|
|
502
519
|
this.#toggleFlag(open.index);
|
|
503
520
|
return { consumed: true };
|
|
521
|
+
case "undo":
|
|
522
|
+
this.#undo();
|
|
523
|
+
return { consumed: true };
|
|
504
524
|
default:
|
|
505
525
|
return { consumed: true, render: false };
|
|
506
526
|
}
|
|
507
527
|
}
|
|
528
|
+
#modelDialogKey(open, data) {
|
|
529
|
+
const key = KEYS[data] ?? data;
|
|
530
|
+
if (open.step === "model") {
|
|
531
|
+
const flags = this.#filteredModelFlags(open);
|
|
532
|
+
const action = MODEL_SHORTCUTS.resolve(key, MODEL_SCOPE) ?? "edit-filter";
|
|
533
|
+
if (action === "close") {
|
|
534
|
+
this.#structured = null;
|
|
535
|
+
return { consumed: true };
|
|
536
|
+
}
|
|
537
|
+
if (action === "previous" || action === "next") {
|
|
538
|
+
open.index = Math.min(Math.max(0, flags.length - 1), Math.max(0, open.index + (action === "next" ? 1 : -1)));
|
|
539
|
+
return { consumed: true };
|
|
540
|
+
}
|
|
541
|
+
if (action === "select") {
|
|
542
|
+
const selected = flags[open.index];
|
|
543
|
+
if (selected === undefined)
|
|
544
|
+
return { consumed: true, render: false };
|
|
545
|
+
open.modelIndex = open.index;
|
|
546
|
+
open.modelKey = selected.key;
|
|
547
|
+
open.step = "level";
|
|
548
|
+
const options = this.#modelLevelOptions(open, selected);
|
|
549
|
+
const current = structuredValue(open.entry, open.record)[selected.key];
|
|
550
|
+
open.index = Math.max(0, options.findIndex(option => option.value === current));
|
|
551
|
+
return { consumed: true };
|
|
552
|
+
}
|
|
553
|
+
const input = open.filter;
|
|
554
|
+
if (input === null)
|
|
555
|
+
return { consumed: true, render: false };
|
|
556
|
+
handleLineInputKey(input, data);
|
|
557
|
+
open.index = 0;
|
|
558
|
+
return { consumed: true };
|
|
559
|
+
}
|
|
560
|
+
const selected = open.entry.flags.find(flag => flag.key === open.modelKey);
|
|
561
|
+
const choices = selected === undefined ? [] : this.#modelLevelOptions(open, selected);
|
|
562
|
+
const action = MODEL_SHORTCUTS.resolve(key, MODEL_LEVEL_SCOPE);
|
|
563
|
+
if (action === "back") {
|
|
564
|
+
open.step = "model";
|
|
565
|
+
open.modelKey = null;
|
|
566
|
+
open.index = open.modelIndex;
|
|
567
|
+
return { consumed: true };
|
|
568
|
+
}
|
|
569
|
+
if (action === "previous" || action === "next") {
|
|
570
|
+
open.index = Math.min(Math.max(0, choices.length - 1), Math.max(0, open.index + (action === "next" ? 1 : -1)));
|
|
571
|
+
return { consumed: true };
|
|
572
|
+
}
|
|
573
|
+
if (action === "select") {
|
|
574
|
+
const choice = choices[open.index];
|
|
575
|
+
if (selected === undefined || choice === undefined)
|
|
576
|
+
return { consumed: true, render: false };
|
|
577
|
+
const previous = { ...open.record };
|
|
578
|
+
open.record[selected.key] = choice.value;
|
|
579
|
+
const forward = { ...open.record };
|
|
580
|
+
this.#saveStructured(open, previous, forward);
|
|
581
|
+
open.step = "model";
|
|
582
|
+
open.modelKey = null;
|
|
583
|
+
open.index = open.modelIndex;
|
|
584
|
+
return { consumed: true };
|
|
585
|
+
}
|
|
586
|
+
return { consumed: true, render: false };
|
|
587
|
+
}
|
|
588
|
+
#filteredModelFlags(open) {
|
|
589
|
+
const needle = open.filter?.value.trim().toLowerCase() ?? "";
|
|
590
|
+
return open.entry.flags.filter(flag => needle.length === 0
|
|
591
|
+
|| (flag.label ?? humanizeLabel(flag.key)).toLowerCase().includes(needle)
|
|
592
|
+
|| flag.key.toLowerCase().includes(needle));
|
|
593
|
+
}
|
|
594
|
+
#modelLevelOptions(open, selected) {
|
|
595
|
+
const fallback = String(selected.fallback);
|
|
596
|
+
const supported = (selected.choices ?? [])
|
|
597
|
+
.filter(choice => choice !== fallback)
|
|
598
|
+
.map(choice => ({ value: choice, label: choice, description: thinkingLevelDescription(choice) }));
|
|
599
|
+
return structuredValue(open.entry, open.record)[selected.key] === undefined
|
|
600
|
+
? supported
|
|
601
|
+
: [...supported, { value: fallback, label: "(clear override)", description: "Revert to global default" }];
|
|
602
|
+
}
|
|
508
603
|
#toggleFlag(index) {
|
|
509
604
|
const open = this.#structured;
|
|
510
605
|
const flag = open?.flags[index];
|
|
511
606
|
if (open === null || open === undefined || flag === undefined)
|
|
512
607
|
return;
|
|
608
|
+
const previous = { ...open.record };
|
|
513
609
|
const declared = open.entry.flags.find(candidate => candidate.key === flag);
|
|
514
610
|
if (declared?.choices === undefined)
|
|
515
611
|
open.record[flag] = !(open.record[flag] ?? false);
|
|
@@ -518,13 +614,24 @@ export class SettingsApp {
|
|
|
518
614
|
const at = declared.choices.indexOf(String(open.record[flag] ?? declared.fallback));
|
|
519
615
|
open.record[flag] = declared.choices[(at + 1) % declared.choices.length] ?? declared.fallback;
|
|
520
616
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
void this.#session.changeStructured(open.entry.backend, open.entry.id,
|
|
527
|
-
|
|
617
|
+
const forward = { ...open.record };
|
|
618
|
+
this.#saveStructured(open, previous, forward);
|
|
619
|
+
}
|
|
620
|
+
#saveStructured(open, previous, forward) {
|
|
621
|
+
const sequence = ++this.#editSequence;
|
|
622
|
+
void this.#session.changeStructured(open.entry.backend, open.entry.id, structuredValue(open.entry, forward)).then(outcome => {
|
|
623
|
+
const failure = changeFailure(outcome);
|
|
624
|
+
if (failure === null) {
|
|
625
|
+
this.#recordUndo({ kind: "structured", sequence, entry: open.entry, previous, forward });
|
|
626
|
+
this.#notice = outcome.status === "deferred" && outcome.application !== null
|
|
627
|
+
? `${labelOf(open.entry)} is stored and applies ${applicationLabel(outcome.application)}`
|
|
628
|
+
: null;
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
// Concurrency: only roll back the dialog if no later press has moved the record on.
|
|
632
|
+
if (this.#structured === open && sameRecord(open.record, forward))
|
|
633
|
+
replaceRecord(open.record, previous);
|
|
634
|
+
this.#notice = `Could not save ${labelOf(open.entry)}: ${failure}`;
|
|
528
635
|
});
|
|
529
636
|
}
|
|
530
637
|
#menuKey(data) {
|
|
@@ -532,6 +639,11 @@ export class SettingsApp {
|
|
|
532
639
|
if (menu === null)
|
|
533
640
|
return { consumed: false };
|
|
534
641
|
const key = KEYS[data] ?? data;
|
|
642
|
+
if (SETTINGS_SHORTCUTS.resolve(key, SCOPE) === "undo") {
|
|
643
|
+
this.#menu = null;
|
|
644
|
+
this.#undo();
|
|
645
|
+
return { consumed: true };
|
|
646
|
+
}
|
|
535
647
|
if (key === "escape") {
|
|
536
648
|
this.#menu = null;
|
|
537
649
|
return { consumed: true };
|
|
@@ -555,6 +667,10 @@ export class SettingsApp {
|
|
|
555
667
|
if (input === null)
|
|
556
668
|
return { consumed: false };
|
|
557
669
|
const key = KEYS[data];
|
|
670
|
+
if (SETTINGS_SHORTCUTS.resolve(key ?? data, SCOPE) === "undo") {
|
|
671
|
+
this.#undo();
|
|
672
|
+
return { consumed: true };
|
|
673
|
+
}
|
|
558
674
|
// Rationale: the boundary chords jump through the results; plain Home and End stay with
|
|
559
675
|
// the search cursor, which the shared line input moves below.
|
|
560
676
|
if (key === "ctrl+home" || key === "ctrl+end") {
|
|
@@ -632,20 +748,21 @@ export class SettingsApp {
|
|
|
632
748
|
}
|
|
633
749
|
#apply(entry, value) {
|
|
634
750
|
const key = `${entry.backend}:${entry.id}`;
|
|
751
|
+
const previous = this.#shownValue(entry);
|
|
752
|
+
const sequence = ++this.#editSequence;
|
|
635
753
|
// Invariant: shown immediately so the row never lags a keypress, and so the next press
|
|
636
754
|
// steps from here rather than from a value the source has not caught up to.
|
|
637
755
|
this.#pending.set(key, value);
|
|
638
756
|
void this.#session.change(entry.backend, entry.id, value).then(outcome => {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
this.#
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (outcome.status === "unavailable" || outcome.limitationReason !== null) {
|
|
645
|
-
this.#pending.delete(key);
|
|
646
|
-
this.#notice = outcome.limitationReason ?? `${labelOf(entry)} is unavailable`;
|
|
757
|
+
const failure = changeFailure(outcome);
|
|
758
|
+
if (failure !== null) {
|
|
759
|
+
if (this.#pending.get(key) === value)
|
|
760
|
+
this.#pending.delete(key);
|
|
761
|
+
this.#notice = `Could not save ${labelOf(entry)}: ${failure}`;
|
|
647
762
|
return;
|
|
648
763
|
}
|
|
764
|
+
if (previous !== null && previous !== value)
|
|
765
|
+
this.#recordUndo({ kind: "scalar", sequence, entry, previous });
|
|
649
766
|
// Concurrency: a later press may have moved on; only the last request clears itself.
|
|
650
767
|
if (this.#pending.get(key) === value)
|
|
651
768
|
this.#pending.delete(key);
|
|
@@ -657,6 +774,64 @@ export class SettingsApp {
|
|
|
657
774
|
#shownValue(entry) {
|
|
658
775
|
return this.#pending.get(`${entry.backend}:${entry.id}`) ?? entry.value;
|
|
659
776
|
}
|
|
777
|
+
#recordUndo(record) {
|
|
778
|
+
if (this.#closed)
|
|
779
|
+
return;
|
|
780
|
+
const after = this.#undoHistory.findIndex(candidate => candidate.sequence > record.sequence);
|
|
781
|
+
if (after < 0)
|
|
782
|
+
this.#undoHistory.push(record);
|
|
783
|
+
else
|
|
784
|
+
this.#undoHistory.splice(after, 0, record);
|
|
785
|
+
}
|
|
786
|
+
#undo() {
|
|
787
|
+
this.#undoRequests++;
|
|
788
|
+
this.#drainUndo();
|
|
789
|
+
}
|
|
790
|
+
#drainUndo() {
|
|
791
|
+
if (this.#undoActive || this.#undoRequests === 0)
|
|
792
|
+
return;
|
|
793
|
+
const record = this.#undoHistory.pop();
|
|
794
|
+
if (record === undefined) {
|
|
795
|
+
this.#undoRequests = 0;
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
this.#undoRequests--;
|
|
799
|
+
this.#undoActive = true;
|
|
800
|
+
if (record.kind === "scalar") {
|
|
801
|
+
const key = `${record.entry.backend}:${record.entry.id}`;
|
|
802
|
+
this.#pending.set(key, record.previous);
|
|
803
|
+
void this.#session.change(record.entry.backend, record.entry.id, record.previous).then(outcome => {
|
|
804
|
+
if (this.#pending.get(key) === record.previous)
|
|
805
|
+
this.#pending.delete(key);
|
|
806
|
+
this.#finishUndo(record, outcome);
|
|
807
|
+
});
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const open = this.#structured;
|
|
811
|
+
if (open !== null && sameEntry(open.entry, record.entry))
|
|
812
|
+
replaceRecord(open.record, record.previous);
|
|
813
|
+
void this.#session.changeStructured(record.entry.backend, record.entry.id, structuredValue(record.entry, record.previous)).then(outcome => {
|
|
814
|
+
const failure = changeFailure(outcome);
|
|
815
|
+
if (failure !== null && this.#structured === open && open !== null && sameRecord(open.record, record.previous)) {
|
|
816
|
+
replaceRecord(open.record, record.forward);
|
|
817
|
+
}
|
|
818
|
+
this.#finishUndo(record, outcome);
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
#finishUndo(record, outcome) {
|
|
822
|
+
const failure = changeFailure(outcome);
|
|
823
|
+
if (failure !== null) {
|
|
824
|
+
this.#recordUndo(record);
|
|
825
|
+
this.#notice = `Could not restore ${labelOf(record.entry)}: ${failure}`;
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
this.#notice = outcome.status === "deferred" && outcome.application !== null
|
|
829
|
+
? `${labelOf(record.entry)} is restored and applies ${applicationLabel(outcome.application)}`
|
|
830
|
+
: null;
|
|
831
|
+
}
|
|
832
|
+
this.#undoActive = false;
|
|
833
|
+
this.#drainUndo();
|
|
834
|
+
}
|
|
660
835
|
#scrollbarSpeed() {
|
|
661
836
|
const value = this.#scrollSetting("scrollbarSpeed");
|
|
662
837
|
return isScrollbarSpeed(value) ? value : "normal";
|
|
@@ -744,15 +919,16 @@ export class SettingsApp {
|
|
|
744
919
|
return ` ${renderListRow(this.#viewRow(entry), { selected, hovered, region: this.#hoverRegion }, valueColumn, innerWidth, theme)}`;
|
|
745
920
|
}
|
|
746
921
|
#viewRow(entry) {
|
|
922
|
+
const key = `${entry.backend}:${entry.id}`;
|
|
747
923
|
const shown = this.#shownValue(entry);
|
|
748
924
|
const value = entry.structured
|
|
749
925
|
? CONFIGURE
|
|
750
926
|
: shown === null
|
|
751
927
|
? describeRaw(entry.rawValue)
|
|
752
|
-
: effectiveDisplay(entry, shown);
|
|
928
|
+
: this.#pending.has(key) ? displayValue(shown) : effectiveDisplay(entry, shown);
|
|
753
929
|
const range = rangeOf(entry);
|
|
754
930
|
return {
|
|
755
|
-
key
|
|
931
|
+
key,
|
|
756
932
|
label: labelOf(entry),
|
|
757
933
|
value,
|
|
758
934
|
...(typeof shown === "number" && entry.editable ? { stepper: stepperEnds(range, shown) } : {}),
|
|
@@ -770,7 +946,10 @@ export class SettingsApp {
|
|
|
770
946
|
current: this.#shownValue(menu.entry) === null ? null : displayValue(this.#shownValue(menu.entry)),
|
|
771
947
|
index: menu.index,
|
|
772
948
|
};
|
|
773
|
-
const frame = valueMenuFrame(state, {
|
|
949
|
+
const frame = valueMenuFrame(state, {
|
|
950
|
+
screenRow: anchor.screenRow,
|
|
951
|
+
valueColumn: valueColumn + SETTINGS_CONTENT_INSET,
|
|
952
|
+
}, {
|
|
774
953
|
bodyTop: this.#bodyTopForFrame,
|
|
775
954
|
bodyHeight: this.#bodyHeightForFrame,
|
|
776
955
|
surfaceWidth: rect.width,
|
|
@@ -780,6 +959,8 @@ export class SettingsApp {
|
|
|
780
959
|
return renderValueMenu(lines, state, frame, theme);
|
|
781
960
|
}
|
|
782
961
|
#dialogLines(open, width, theme) {
|
|
962
|
+
if (open.modelDialog)
|
|
963
|
+
return this.#modelDialogLines(open, width, theme);
|
|
783
964
|
const rows = open.flags.map(key => {
|
|
784
965
|
const declared = open.entry.flags.find(flag => flag.key === key);
|
|
785
966
|
return {
|
|
@@ -789,9 +970,46 @@ export class SettingsApp {
|
|
|
789
970
|
...(declared?.description === undefined ? {} : { description: declared.description }),
|
|
790
971
|
};
|
|
791
972
|
});
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
973
|
+
return renderDialogPanel({ title: labelOf(open.entry), rows, index: open.index, hint: SETTINGS_SHORTCUTS.hintEntries(DIALOG_SCOPE) }, width, theme);
|
|
974
|
+
}
|
|
975
|
+
#modelDialogLines(open, width, theme) {
|
|
976
|
+
if (open.step === "model") {
|
|
977
|
+
const flags = this.#filteredModelFlags(open);
|
|
978
|
+
const stored = structuredValue(open.entry, open.record);
|
|
979
|
+
const allRows = flags.length === 0
|
|
980
|
+
? [{ label: "No models found", value: "" }]
|
|
981
|
+
: flags.map(flag => ({
|
|
982
|
+
...modelLabelParts(flag.label ?? humanizeLabel(flag.key)),
|
|
983
|
+
value: stored[flag.key] === undefined ? "" : String(stored[flag.key]),
|
|
984
|
+
}));
|
|
985
|
+
const visible = dialogWindow(allRows, open.index, 10);
|
|
986
|
+
return renderSteppedDialogPanel({
|
|
987
|
+
title: "Thinking Level",
|
|
988
|
+
step: 1,
|
|
989
|
+
steps: 2,
|
|
990
|
+
description: "Select a model to configure",
|
|
991
|
+
...(open.filter === null ? {} : { input: open.filter }),
|
|
992
|
+
rows: visible.rows,
|
|
993
|
+
index: visible.index,
|
|
994
|
+
hint: MODEL_SHORTCUTS.hintEntries(MODEL_SCOPE),
|
|
995
|
+
}, width, theme);
|
|
996
|
+
}
|
|
997
|
+
const selected = open.entry.flags.find(flag => flag.key === open.modelKey);
|
|
998
|
+
const current = selected === undefined ? undefined : structuredValue(open.entry, open.record)[selected.key];
|
|
999
|
+
const allRows = (selected === undefined ? [] : this.#modelLevelOptions(open, selected)).map(choice => ({
|
|
1000
|
+
label: choice.value === current ? `✓ ${choice.label}` : choice.label,
|
|
1001
|
+
value: choice.description,
|
|
1002
|
+
}));
|
|
1003
|
+
const visible = dialogWindow(allRows, open.index, 10);
|
|
1004
|
+
return renderSteppedDialogPanel({
|
|
1005
|
+
title: "Thinking Level",
|
|
1006
|
+
step: 2,
|
|
1007
|
+
steps: 2,
|
|
1008
|
+
description: `Select default thinking level for ${selected?.label ?? open.modelKey ?? "model"}`,
|
|
1009
|
+
rows: visible.rows,
|
|
1010
|
+
index: visible.index,
|
|
1011
|
+
hint: MODEL_SHORTCUTS.hintEntries(MODEL_LEVEL_SCOPE),
|
|
1012
|
+
}, width, theme);
|
|
795
1013
|
}
|
|
796
1014
|
#footerLines(width, theme) {
|
|
797
1015
|
const open = this.#structured;
|
|
@@ -810,6 +1028,59 @@ export class SettingsApp {
|
|
|
810
1028
|
return [...renderInputRow(input, width, { placeholder: SEARCH_PLACEHOLDER, theme }).lines, status];
|
|
811
1029
|
}
|
|
812
1030
|
}
|
|
1031
|
+
function modelLabelParts(label) {
|
|
1032
|
+
const match = /^(.*?)(\s+\[[^\]]+\])$/u.exec(label);
|
|
1033
|
+
return match === null ? { label } : { label: match[1] ?? label, labelSuffix: match[2] ?? "" };
|
|
1034
|
+
}
|
|
1035
|
+
function dialogWindow(rows, index, maximum) {
|
|
1036
|
+
if (rows.length <= maximum)
|
|
1037
|
+
return { rows, index: Math.min(Math.max(0, rows.length - 1), Math.max(0, index)) };
|
|
1038
|
+
const start = Math.min(rows.length - maximum, Math.max(0, index - Math.floor(maximum / 2)));
|
|
1039
|
+
return { rows: rows.slice(start, start + maximum), index: index - start };
|
|
1040
|
+
}
|
|
1041
|
+
function thinkingLevelDescription(level) {
|
|
1042
|
+
switch (level) {
|
|
1043
|
+
case "default": return "global default";
|
|
1044
|
+
case "off": return "No reasoning";
|
|
1045
|
+
case "minimal": return "Very brief reasoning (~1k tokens)";
|
|
1046
|
+
case "low": return "Light reasoning (~2k tokens)";
|
|
1047
|
+
case "medium": return "Moderate reasoning (~8k tokens)";
|
|
1048
|
+
case "high": return "Deep reasoning (~16k tokens)";
|
|
1049
|
+
case "xhigh": return "Extra-high reasoning (~32k tokens)";
|
|
1050
|
+
case "max": return "Maximum reasoning";
|
|
1051
|
+
default: return "";
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function structuredValue(entry, record) {
|
|
1055
|
+
// Invariant: a choice part at its fallback is unset, so the engine's default applies.
|
|
1056
|
+
return Object.fromEntries(Object.entries(record).filter(([key, value]) => {
|
|
1057
|
+
const part = entry.flags.find(candidate => candidate.key === key);
|
|
1058
|
+
return part?.choices === undefined || value !== part.fallback;
|
|
1059
|
+
}));
|
|
1060
|
+
}
|
|
1061
|
+
function sameEntry(left, right) {
|
|
1062
|
+
return left.backend === right.backend && left.id === right.id;
|
|
1063
|
+
}
|
|
1064
|
+
function sameRecord(left, right) {
|
|
1065
|
+
const leftEntries = Object.entries(left);
|
|
1066
|
+
const rightEntries = Object.entries(right);
|
|
1067
|
+
return leftEntries.length === rightEntries.length
|
|
1068
|
+
&& leftEntries.every(([key, value]) => right[key] === value);
|
|
1069
|
+
}
|
|
1070
|
+
function replaceRecord(target, source) {
|
|
1071
|
+
for (const key of Object.keys(target))
|
|
1072
|
+
delete target[key];
|
|
1073
|
+
Object.assign(target, source);
|
|
1074
|
+
}
|
|
1075
|
+
function changeFailure(outcome) {
|
|
1076
|
+
if (outcome.failure !== null)
|
|
1077
|
+
return outcome.failure;
|
|
1078
|
+
if (outcome.status === "failed")
|
|
1079
|
+
return "the effect failed";
|
|
1080
|
+
if (outcome.status === "unavailable")
|
|
1081
|
+
return outcome.limitationReason ?? "the setting is unavailable";
|
|
1082
|
+
return outcome.limitationReason;
|
|
1083
|
+
}
|
|
813
1084
|
/** The source's own wording when it has one, otherwise the id made readable. */
|
|
814
1085
|
function labelOf(entry) {
|
|
815
1086
|
return entry.label ?? humanizeLabel(entry.id);
|
|
@@ -303,7 +303,7 @@ export class ModelsDialogComponent {
|
|
|
303
303
|
const row = rows[index];
|
|
304
304
|
const selected = index === this.#selectedIndex;
|
|
305
305
|
const scoped = this.#scopeIds.includes(row.fullId);
|
|
306
|
-
// Invariant: arrow, scope marker, model id, [provider], then the active checkmark
|
|
306
|
+
// Invariant: arrow, scope marker, model id, [provider], then the active checkmark.
|
|
307
307
|
const prefix = selected ? theme.fg("accent", "→ ") : " ";
|
|
308
308
|
const marker = scoped ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
309
309
|
const label = selected ? theme.fg("text", row.model.id) : row.model.id;
|
|
@@ -330,7 +330,7 @@ export class ModelsDialogComponent {
|
|
|
330
330
|
const confirm = keyLabel("tui.select.confirm");
|
|
331
331
|
const save = keyLabel("app.models.save");
|
|
332
332
|
return [
|
|
333
|
-
{
|
|
333
|
+
{ key: "Type", action: "search" },
|
|
334
334
|
{ key: "↑↓", action: "navigate" },
|
|
335
335
|
...(tab.length === 0 ? [] : [{ key: tab, action: "filter" }]),
|
|
336
336
|
...(confirm.length === 0 ? [] : [{ key: confirm, action: "switch" }]),
|