@timurproko/a1 0.2.7-dev.1236 → 0.2.7-dev.1239
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/composition/owned-settings.js +1 -1
- package/dist/composition/release-update-observation.js +43 -14
- package/dist/contracts/owned-ui/session-workflows.d.ts +1 -1
- package/dist/features/owned-ui/plus-menu-dialog.d.ts +13 -7
- package/dist/features/owned-ui/plus-menu-dialog.js +17 -12
- package/dist/features/owned-ui/settings-app.js +94 -17
- package/dist/foundation/release/automatic-update.d.ts +12 -0
- package/dist/foundation/release/automatic-update.js +87 -5
- package/dist/integrations/pi/engine/default-model-setting.d.ts +12 -0
- package/dist/integrations/pi/engine/default-model-setting.js +70 -0
- package/dist/integrations/pi/engine/diagnostics-state.js +6 -5
- package/dist/integrations/pi/engine/index.d.ts +1 -1
- package/dist/integrations/pi/engine/index.js +1 -1
- package/dist/integrations/pi/engine/offline-settings-port.js +1 -0
- package/dist/integrations/pi/engine/runtime-integration.d.ts +1 -1
- package/dist/integrations/pi/engine/runtime-integration.js +12 -14
- package/dist/integrations/pi/engine/settings-bridge.d.ts +1 -2
- package/dist/integrations/pi/engine/settings-bridge.js +23 -29
- package/dist/integrations/pi/engine/settings-port.js +2 -1
- package/dist/integrations/pi/engine/workflow-runner.js +2 -2
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
- package/dist/native/linux-x64/terminal-host.node +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
- package/dist/native/win32-x64/terminal-host.node +0 -0
- package/dist/ui/components/dialog-panel.d.ts +6 -3
- package/dist/ui/components/dialog-panel.js +10 -7
- package/dist/ui/components/theme.d.ts +1 -1
- package/dist/ui/settings/declarations.d.ts +23 -2
- package/dist/ui/settings/declarations.js +18 -2
- package/dist/ui/settings/migrations.js +7 -0
- package/docs/architecture/ui-reference-provenance.md +3 -3
- package/package.json +3 -3
|
@@ -14,7 +14,7 @@ export function createOwnedSettingsManager(options) {
|
|
|
14
14
|
legacyConfigDir: productPaths.configDir,
|
|
15
15
|
...(options.agent === undefined ? {} : { agent: options.agent }),
|
|
16
16
|
...(options.agentProvider === undefined ? {} : { agentProvider: options.agentProvider }),
|
|
17
|
-
...(options.hideTabSettings ? { hiddenOwnedSettingIds: ["tabsAutoName", "tabsPosition", "tabBarPlusMenu"] } : {}),
|
|
17
|
+
...(options.hideTabSettings ? { hiddenOwnedSettingIds: ["tabsAutoName", "tabsPosition", "tabBarPlusMenu", "tabBarRightItems"] } : {}),
|
|
18
18
|
hiddenAgentSettingIds: ["fullscreenWheelScrollLines"],
|
|
19
19
|
agentSettingLabelOverrides: { fullscreenCopyOnSelect: "Copy on select" },
|
|
20
20
|
});
|
|
@@ -4,13 +4,41 @@ class ReleaseUpdateObservation {
|
|
|
4
4
|
#controller = new AbortController();
|
|
5
5
|
#timer;
|
|
6
6
|
#announced = null;
|
|
7
|
+
#installedAnnounced = false;
|
|
7
8
|
#eligible;
|
|
8
9
|
#cadence = null;
|
|
9
10
|
#nextDelay = null;
|
|
10
11
|
#started = false;
|
|
11
12
|
#running = false;
|
|
12
13
|
#refresh = false;
|
|
14
|
+
#stopActiveReleaseObservation = () => { };
|
|
13
15
|
constructor(input) { this.#input = input; this.#eligible = input.mode === "auto"; }
|
|
16
|
+
#announce(status, version) {
|
|
17
|
+
const key = `${status}:${version}`;
|
|
18
|
+
if (key === this.#announced || (status === "installed" && this.#installedAnnounced) || this.#controller.signal.aborted)
|
|
19
|
+
return;
|
|
20
|
+
this.#announced = key;
|
|
21
|
+
if (status === "installed") {
|
|
22
|
+
this.#installedAnnounced = true;
|
|
23
|
+
this.#eligible = false;
|
|
24
|
+
if (this.#timer !== undefined)
|
|
25
|
+
clearTimeout(this.#timer);
|
|
26
|
+
}
|
|
27
|
+
this.#input.backend.extensions.announceReleaseUpdate({ status, version });
|
|
28
|
+
}
|
|
29
|
+
async #startActiveReleaseObservation() {
|
|
30
|
+
const runningVersion = this.#input.packageVersion ?? await this.#input.readPackageVersion();
|
|
31
|
+
if (this.#controller.signal.aborted)
|
|
32
|
+
return;
|
|
33
|
+
const module = await import("../foundation/release/automatic-update.js");
|
|
34
|
+
if (this.#controller.signal.aborted)
|
|
35
|
+
return;
|
|
36
|
+
this.#stopActiveReleaseObservation = module.observeNewerActiveRelease({
|
|
37
|
+
runningVersion,
|
|
38
|
+
signal: this.#controller.signal,
|
|
39
|
+
onNewer: version => this.#announce("installed", version),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
14
42
|
async #observe() {
|
|
15
43
|
// Performance: registry modules and their locks stay outside the first-frame startup graph.
|
|
16
44
|
const releases = await import("../foundation/release/latest-release.js");
|
|
@@ -41,22 +69,18 @@ class ReleaseUpdateObservation {
|
|
|
41
69
|
}
|
|
42
70
|
if (input.mode !== "auto")
|
|
43
71
|
return;
|
|
44
|
-
const announce = (status, version) => {
|
|
45
|
-
const actual = input.clientVersion?.();
|
|
46
|
-
const key = `${status}:${version}`;
|
|
47
|
-
if (key === this.#announced || this.#controller.signal.aborted
|
|
48
|
-
|| status !== "blocked-cleared" && actual && !releases.isNewerRelease(version, actual))
|
|
49
|
-
return;
|
|
50
|
-
this.#announced = key;
|
|
51
|
-
input.backend.extensions.announceReleaseUpdate({ status, version });
|
|
52
|
-
};
|
|
53
72
|
const installed = await import("../foundation/release/automatic-update.js")
|
|
54
73
|
.then(module => module.requestAutomaticUpdate({
|
|
55
74
|
release, runningVersion, signal: this.#controller.signal,
|
|
56
|
-
onActivationBlocked: version =>
|
|
75
|
+
onActivationBlocked: version => {
|
|
76
|
+
const clientVersion = input.clientVersion?.();
|
|
77
|
+
if (version !== null && clientVersion && !releases.isNewerRelease(version, clientVersion))
|
|
78
|
+
return;
|
|
79
|
+
this.#announce(version === null ? "blocked-cleared" : "blocked", version ?? release.version);
|
|
80
|
+
},
|
|
57
81
|
}));
|
|
58
82
|
if (installed !== null)
|
|
59
|
-
announce("installed", installed);
|
|
83
|
+
this.#announce("installed", installed);
|
|
60
84
|
}
|
|
61
85
|
async #run() {
|
|
62
86
|
if (this.#running) {
|
|
@@ -83,13 +107,18 @@ class ReleaseUpdateObservation {
|
|
|
83
107
|
if (this.#started || this.#controller.signal.aborted)
|
|
84
108
|
return;
|
|
85
109
|
this.#started = true;
|
|
110
|
+
void this.#startActiveReleaseObservation().catch(() => undefined);
|
|
86
111
|
void this.#run();
|
|
87
112
|
}
|
|
88
|
-
stop() {
|
|
89
|
-
|
|
113
|
+
stop() {
|
|
114
|
+
this.#controller.abort();
|
|
115
|
+
this.#stopActiveReleaseObservation();
|
|
116
|
+
if (this.#timer !== undefined)
|
|
117
|
+
clearTimeout(this.#timer);
|
|
118
|
+
}
|
|
90
119
|
clientChanged() {
|
|
91
120
|
this.#announced = null;
|
|
92
|
-
if (!this.#started || this.#controller.signal.aborted || this.#input.mode === "no")
|
|
121
|
+
if (!this.#started || this.#controller.signal.aborted || this.#installedAnnounced || this.#input.mode === "no")
|
|
93
122
|
return;
|
|
94
123
|
if (this.#timer !== undefined)
|
|
95
124
|
clearTimeout(this.#timer);
|
|
@@ -292,7 +292,7 @@ export type OwnedUiJsonValue = null | boolean | number | string | readonly Owned
|
|
|
292
292
|
/** The layer that installs a setting's live effect. */
|
|
293
293
|
export type OwnedUiSettingOwner = "agent" | "shell" | "terminal" | "startup" | "shutdown" | "installation";
|
|
294
294
|
/** Engine settings whose live effect an owner may bind. */
|
|
295
|
-
export type OwnedUiSessionSettingKey = "autoCompact" | "showImages" | "imageWidthCells" | "autoResizeImages" | "blockImages" | "enableSkillCommands" | "steeringMode" | "followUpMode" | "transport" | "httpIdleTimeoutMs" | "cacheWarmingMode" | "modelThinkingLevels" | "theme" | "hideThinkingBlock" | "mermaidRenderingMode" | "showCacheMissNotices" | "collapseChangelog" | "enableInstallTelemetry" | "quietStartup" | "defaultProjectTrust" | "sessionShareDestination" | "doubleEscapeAction" | "treeFilterMode" | "showHardwareCursor" | "editorPaddingX" | "outputPad" | "autocompleteMaxVisible" | "clearOnShrink" | "showTerminalProgress" | "tuiMode" | "fullscreenExitOutput" | "fullscreenScrollbar" | "fullscreenCopyOnSelect" | "fullscreenWheelScrollLines" | "warnings";
|
|
295
|
+
export type OwnedUiSessionSettingKey = "autoCompact" | "showImages" | "imageWidthCells" | "autoResizeImages" | "blockImages" | "enableSkillCommands" | "steeringMode" | "followUpMode" | "transport" | "httpIdleTimeoutMs" | "cacheWarmingMode" | "defaultModel" | "modelThinkingLevels" | "theme" | "hideThinkingBlock" | "mermaidRenderingMode" | "showCacheMissNotices" | "collapseChangelog" | "enableInstallTelemetry" | "quietStartup" | "defaultProjectTrust" | "sessionShareDestination" | "doubleEscapeAction" | "treeFilterMode" | "showHardwareCursor" | "editorPaddingX" | "outputPad" | "autocompleteMaxVisible" | "clearOnShrink" | "showTerminalProgress" | "tuiMode" | "fullscreenExitOutput" | "fullscreenScrollbar" | "fullscreenCopyOnSelect" | "fullscreenWheelScrollLines" | "warnings";
|
|
296
296
|
export interface OwnedUiSettingEffectHandler {
|
|
297
297
|
/** Install one value in the active owner. Handlers must be idempotent and reversible. */
|
|
298
298
|
apply(value: OwnedUiJsonValue): void | Promise<void>;
|
|
@@ -6,17 +6,23 @@ export interface PlusMenuKind {
|
|
|
6
6
|
readonly key: string;
|
|
7
7
|
readonly label: string;
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
/** Presentation copy for a searchable ordered flag multi-select dialog. */
|
|
10
|
+
export interface FlagToggleDialogPresentation {
|
|
11
|
+
readonly title: string;
|
|
12
|
+
readonly description: string;
|
|
13
|
+
readonly empty: string;
|
|
14
|
+
}
|
|
15
|
+
/** Owns only filtering and selection; the Settings screen owns enabled state and persistence. */
|
|
16
|
+
export declare class FlagToggleDialog {
|
|
15
17
|
#private;
|
|
16
|
-
constructor(kinds: readonly PlusMenuKind[]);
|
|
18
|
+
constructor(kinds: readonly PlusMenuKind[], presentation: FlagToggleDialogPresentation);
|
|
17
19
|
/** The kinds the filter keeps, in kind order. */
|
|
18
20
|
shown(): readonly PlusMenuKind[];
|
|
19
21
|
/** Handles one key by its resolved name; `toggle` flips a kind. Returns whether the dialog closed. */
|
|
20
22
|
handle(name: string, data: string, toggle: (key: string) => void): "close" | "handled";
|
|
21
23
|
lines(enabled: Readonly<Record<string, boolean | string>>, width: number, theme: UiTheme): readonly string[];
|
|
22
24
|
}
|
|
25
|
+
/** The tab bar's specialized flag dialog, retained as the public Plus Menu surface. */
|
|
26
|
+
export declare class PlusMenuDialog extends FlagToggleDialog {
|
|
27
|
+
constructor(kinds: readonly PlusMenuKind[]);
|
|
28
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { LineInput, handleLineInputKey, padToWidth, renderInputRow, renderShortcutHintsWithClose, truncateToWidth, } from "../../ui/components/index.js";
|
|
2
2
|
/** The A1 setting holding the tab kinds the strip's `+` offers. */
|
|
3
3
|
export const PLUS_MENU_SETTING_ID = "tabBarPlusMenu";
|
|
4
|
-
const
|
|
5
|
-
|
|
4
|
+
const PLUS_MENU_PRESENTATION = Object.freeze({
|
|
5
|
+
title: "Plus Menu",
|
|
6
|
+
description: "Choose what the + button in the tab bar can add.",
|
|
7
|
+
empty: "No tabs match",
|
|
8
|
+
});
|
|
6
9
|
const HINTS = [
|
|
7
10
|
{ key: "Type", action: "search" },
|
|
8
11
|
{ key: "↑↓", action: "navigate" },
|
|
@@ -10,17 +13,15 @@ const HINTS = [
|
|
|
10
13
|
{ key: "esc", action: "close" },
|
|
11
14
|
];
|
|
12
15
|
const PADDING_X = 2;
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
* text. It owns only its filter and the row in hand; the enabled record and its saving stay with the
|
|
16
|
-
* Settings screen that opened it.
|
|
17
|
-
*/
|
|
18
|
-
export class PlusMenuDialog {
|
|
16
|
+
/** Owns only filtering and selection; the Settings screen owns enabled state and persistence. */
|
|
17
|
+
export class FlagToggleDialog {
|
|
19
18
|
#kinds;
|
|
19
|
+
#presentation;
|
|
20
20
|
#filter = new LineInput("");
|
|
21
21
|
#index = 0;
|
|
22
|
-
constructor(kinds) {
|
|
22
|
+
constructor(kinds, presentation) {
|
|
23
23
|
this.#kinds = kinds;
|
|
24
|
+
this.#presentation = presentation;
|
|
24
25
|
}
|
|
25
26
|
/** The kinds the filter keeps, in kind order. */
|
|
26
27
|
shown() {
|
|
@@ -60,7 +61,7 @@ export class PlusMenuDialog {
|
|
|
60
61
|
const shown = this.shown();
|
|
61
62
|
const index = Math.min(this.#index, Math.max(0, shown.length - 1));
|
|
62
63
|
const rows = shown.length === 0
|
|
63
|
-
? [row(theme.fg("dim",
|
|
64
|
+
? [row(theme.fg("dim", this.#presentation.empty))]
|
|
64
65
|
: shown.map((kind, position) => {
|
|
65
66
|
const selected = position === index;
|
|
66
67
|
const cursor = selected ? "→ " : " ";
|
|
@@ -70,8 +71,8 @@ export class PlusMenuDialog {
|
|
|
70
71
|
});
|
|
71
72
|
return [
|
|
72
73
|
blank,
|
|
73
|
-
row(theme.bold(theme.fg("accent",
|
|
74
|
-
row(theme.fg("dim",
|
|
74
|
+
row(theme.bold(theme.fg("accent", this.#presentation.title))),
|
|
75
|
+
row(theme.fg("dim", this.#presentation.description)),
|
|
75
76
|
blank,
|
|
76
77
|
...renderInputRow(this.#filter, contentWidth, { ruled: false, promptGlyph: "> ", theme }).lines.map(line => row(line)),
|
|
77
78
|
blank,
|
|
@@ -82,3 +83,7 @@ export class PlusMenuDialog {
|
|
|
82
83
|
];
|
|
83
84
|
}
|
|
84
85
|
}
|
|
86
|
+
/** The tab bar's specialized flag dialog, retained as the public Plus Menu surface. */
|
|
87
|
+
export class PlusMenuDialog extends FlagToggleDialog {
|
|
88
|
+
constructor(kinds) { super(kinds, PLUS_MENU_PRESENTATION); }
|
|
89
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DIALOG_BACK_SHORTCUT_HINT, DIALOG_CLOSE_SHORTCUT_HINT } from "../../contracts/presentation/index.js";
|
|
2
2
|
import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, configureDialogChoiceCapacity, configureDialogTextRows, configureDialogWidth, numericValues, RAIL_COLUMNS, renderCenteredDialogOverlay, renderDialogPanel, renderSteppedDialogPanel, renderEmptyState, renderGroupHeader, renderListRow, renderNote, renderShortcutHintsWithClose, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, formatLocalTime, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
|
|
3
|
-
import { PLUS_MENU_SETTING_ID, PlusMenuDialog } from "./plus-menu-dialog.js";
|
|
3
|
+
import { FlagToggleDialog, PLUS_MENU_SETTING_ID, PlusMenuDialog } from "./plus-menu-dialog.js";
|
|
4
4
|
import { SETTINGS_APP_ID } from "./settings-route.js";
|
|
5
5
|
export { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
|
|
6
6
|
const SCOPE = SETTINGS_APP_ID;
|
|
@@ -22,6 +22,7 @@ const SCROLL_LINGER_MS = 900;
|
|
|
22
22
|
const SCROLL_LINGER_REPAINT_MS = 925;
|
|
23
23
|
/** What a structured value offers instead of printing itself. */
|
|
24
24
|
const CONFIGURE = "configure";
|
|
25
|
+
const TAB_BAR_RIGHT_ITEMS_SETTING_ID = "tabBarRightItems";
|
|
25
26
|
export const SETTINGS_SHORTCUTS = new ShortcutRegistry();
|
|
26
27
|
SETTINGS_SHORTCUTS.declare({ key: "edit", scope: SCOPE, description: "Search settings", section: "Change", hint: { keys: "Type", does: "search" } }, "edit-filter");
|
|
27
28
|
SETTINGS_SHORTCUTS.declare({ key: "up", scope: SCOPE, description: "Previous setting", section: "Navigate", hint: { keys: "↑↓", does: "navigate" } }, "move-up");
|
|
@@ -281,7 +282,7 @@ export class SettingsApp {
|
|
|
281
282
|
return { consumed: false };
|
|
282
283
|
case "activate": {
|
|
283
284
|
const row = rows[selected];
|
|
284
|
-
if (row?.kind === "element" && row.value.structured)
|
|
285
|
+
if (row?.kind === "element" && (row.value.structured || isDefaultModelEntry(row.value)))
|
|
285
286
|
this.#openStructured(row.value);
|
|
286
287
|
else if (row?.kind === "element" && typeof this.#shownValue(row.value) === "number")
|
|
287
288
|
this.#cycle(rows, selected, 1);
|
|
@@ -469,7 +470,7 @@ export class SettingsApp {
|
|
|
469
470
|
if (row === undefined || row.kind !== "element")
|
|
470
471
|
return;
|
|
471
472
|
const entry = row.value;
|
|
472
|
-
if (entry.structured) {
|
|
473
|
+
if (entry.structured || isDefaultModelEntry(entry)) {
|
|
473
474
|
this.#openStructured(entry);
|
|
474
475
|
return;
|
|
475
476
|
}
|
|
@@ -508,9 +509,13 @@ export class SettingsApp {
|
|
|
508
509
|
}
|
|
509
510
|
// Rationale: a structured setting opens as its own flag list rather than a value menu.
|
|
510
511
|
#openStructured(entry) {
|
|
512
|
+
const modelDialog = entry.backend === "agent" && entry.id === "modelThinkingLevels"
|
|
513
|
+
? "thinking"
|
|
514
|
+
: isDefaultModelEntry(entry) ? "default" : null;
|
|
511
515
|
// Invariant: the flags come from the declaration, not from the stored value: an unset
|
|
512
|
-
// flag still has a row, showing the default the source would apply.
|
|
513
|
-
|
|
516
|
+
// flag still has a row, showing the default the source would apply. The default-model
|
|
517
|
+
// selector may open empty so it can truthfully report that no authenticated models exist.
|
|
518
|
+
if (entry.flags.length === 0 && modelDialog !== "default")
|
|
514
519
|
return;
|
|
515
520
|
// Rationale: beside a tab bar, its own Plus Menu dialog configures `+`, so both ways look the same.
|
|
516
521
|
if (entry.backend === "a1" && entry.id === PLUS_MENU_SETTING_ID && this.#options.configurePlusMenu?.() === true)
|
|
@@ -530,17 +535,24 @@ export class SettingsApp {
|
|
|
530
535
|
this.#hoverKey = null;
|
|
531
536
|
this.#hoverRegion = "label";
|
|
532
537
|
this.#rails.clear();
|
|
533
|
-
const
|
|
538
|
+
const currentDefault = typeof entry.rawValue === "string" ? entry.rawValue : null;
|
|
534
539
|
this.#structured = {
|
|
535
540
|
entry,
|
|
536
541
|
flags: entry.flags.map(flag => flag.key),
|
|
537
542
|
modelDialog,
|
|
538
|
-
|
|
539
|
-
|
|
543
|
+
flagDialog: entry.backend !== "a1" ? null
|
|
544
|
+
: entry.id === PLUS_MENU_SETTING_ID ? new PlusMenuDialog(entry.flags)
|
|
545
|
+
: entry.id === TAB_BAR_RIGHT_ITEMS_SETTING_ID ? new FlagToggleDialog(entry.flags, {
|
|
546
|
+
title: "Tab Bar Right Side",
|
|
547
|
+
description: "Choose what the right side of the tab bar shows.",
|
|
548
|
+
empty: "No items match",
|
|
549
|
+
}) : null,
|
|
550
|
+
filter: modelDialog === null ? null : new LineInput(""),
|
|
540
551
|
step: "model",
|
|
541
552
|
modelKey: null,
|
|
542
553
|
modelIndex: 0,
|
|
543
|
-
index: 0,
|
|
554
|
+
index: modelDialog === "default" ? Math.max(0, entry.flags.findIndex(flag => flag.key === currentDefault)) : 0,
|
|
555
|
+
error: null,
|
|
544
556
|
record,
|
|
545
557
|
};
|
|
546
558
|
}
|
|
@@ -548,10 +560,10 @@ export class SettingsApp {
|
|
|
548
560
|
const open = this.#structured;
|
|
549
561
|
if (open === null)
|
|
550
562
|
return { consumed: false };
|
|
551
|
-
if (open.modelDialog)
|
|
563
|
+
if (open.modelDialog !== null)
|
|
552
564
|
return this.#modelDialogKey(open, data);
|
|
553
|
-
if (open.
|
|
554
|
-
const outcome = open.
|
|
565
|
+
if (open.flagDialog !== null) {
|
|
566
|
+
const outcome = open.flagDialog.handle(KEYS[data] ?? data, data, key => this.#toggleFlag(open.flags.indexOf(key)));
|
|
555
567
|
if (outcome === "close")
|
|
556
568
|
this.#structured = null;
|
|
557
569
|
return { consumed: true };
|
|
@@ -578,6 +590,38 @@ export class SettingsApp {
|
|
|
578
590
|
}
|
|
579
591
|
#modelDialogKey(open, data) {
|
|
580
592
|
const key = KEYS[data] ?? data;
|
|
593
|
+
if (open.modelDialog === "default") {
|
|
594
|
+
const flags = this.#filteredModelFlags(open);
|
|
595
|
+
const action = MODEL_SHORTCUTS.resolve(key, MODEL_SCOPE) ?? "edit-filter";
|
|
596
|
+
if (action === "close") {
|
|
597
|
+
this.#structured = null;
|
|
598
|
+
return { consumed: true };
|
|
599
|
+
}
|
|
600
|
+
if (action === "previous" || action === "next") {
|
|
601
|
+
open.index = Math.min(Math.max(0, flags.length - 1), Math.max(0, open.index + (action === "next" ? 1 : -1)));
|
|
602
|
+
return { consumed: true };
|
|
603
|
+
}
|
|
604
|
+
if (action === "select") {
|
|
605
|
+
const selected = flags[open.index];
|
|
606
|
+
if (selected === undefined)
|
|
607
|
+
return { consumed: true, render: false };
|
|
608
|
+
open.error = null;
|
|
609
|
+
this.#structured = null;
|
|
610
|
+
void this.#session.change(open.entry.backend, open.entry.id, selected.key).then(outcome => {
|
|
611
|
+
const failure = changeFailure(outcome);
|
|
612
|
+
if (failure !== null) {
|
|
613
|
+
open.error = `Could not save: ${failure}`;
|
|
614
|
+
this.#structured = open;
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
return { consumed: true };
|
|
618
|
+
}
|
|
619
|
+
if (open.filter === null)
|
|
620
|
+
return { consumed: true, render: false };
|
|
621
|
+
handleLineInputKey(open.filter, data);
|
|
622
|
+
open.index = 0;
|
|
623
|
+
return { consumed: true };
|
|
624
|
+
}
|
|
581
625
|
if (open.step === "model") {
|
|
582
626
|
const flags = this.#filteredModelFlags(open);
|
|
583
627
|
const action = MODEL_SHORTCUTS.resolve(key, MODEL_SCOPE) ?? "edit-filter";
|
|
@@ -640,6 +684,7 @@ export class SettingsApp {
|
|
|
640
684
|
const needle = open.filter?.value.trim().toLowerCase() ?? "";
|
|
641
685
|
return open.entry.flags.filter(flag => needle.length === 0
|
|
642
686
|
|| (flag.label ?? humanizeLabel(flag.key)).toLowerCase().includes(needle)
|
|
687
|
+
|| flag.description.toLowerCase().includes(needle)
|
|
643
688
|
|| flag.key.toLowerCase().includes(needle));
|
|
644
689
|
}
|
|
645
690
|
#modelLevelOptions(open, selected) {
|
|
@@ -897,7 +942,8 @@ export class SettingsApp {
|
|
|
897
942
|
// Rationale: a section named by the search is what the reader asked for, so it arrives
|
|
898
943
|
// whole rather than narrowed to the entries that happen to repeat its name.
|
|
899
944
|
const named = needle.length > 0 && section.title.toLowerCase().includes(needle);
|
|
900
|
-
const
|
|
945
|
+
const matching = named ? section.entries : section.entries.filter(matches);
|
|
946
|
+
const entries = orderDefaultModelBeforeThinking(matching, section.id);
|
|
901
947
|
if (needle.length > 0 && entries.length === 0)
|
|
902
948
|
continue;
|
|
903
949
|
if (rows.length > 0)
|
|
@@ -947,7 +993,7 @@ export class SettingsApp {
|
|
|
947
993
|
#viewRow(entry) {
|
|
948
994
|
const key = `${entry.backend}:${entry.id}`;
|
|
949
995
|
const shown = this.#shownValue(entry);
|
|
950
|
-
const value = entry.structured
|
|
996
|
+
const value = entry.structured || isDefaultModelEntry(entry)
|
|
951
997
|
? CONFIGURE
|
|
952
998
|
: shown === null
|
|
953
999
|
? describeRaw(entry.rawValue)
|
|
@@ -988,10 +1034,10 @@ export class SettingsApp {
|
|
|
988
1034
|
return renderValueMenu(lines, state, frame, theme);
|
|
989
1035
|
}
|
|
990
1036
|
#dialogLines(open, width, height, theme) {
|
|
991
|
-
if (open.modelDialog)
|
|
1037
|
+
if (open.modelDialog !== null)
|
|
992
1038
|
return this.#modelDialogLines(open, width, height, theme);
|
|
993
|
-
if (open.
|
|
994
|
-
return open.
|
|
1039
|
+
if (open.flagDialog !== null)
|
|
1040
|
+
return open.flagDialog.lines(open.record, width, theme);
|
|
995
1041
|
const rows = open.flags.map(key => {
|
|
996
1042
|
const declared = open.entry.flags.find(flag => flag.key === key);
|
|
997
1043
|
return {
|
|
@@ -1005,6 +1051,23 @@ export class SettingsApp {
|
|
|
1005
1051
|
return renderDialogPanel({ title: labelOf(open.entry), rows: visible.rows, index: visible.index, hint: SETTINGS_SHORTCUTS.hintEntries(DIALOG_SCOPE) }, width, theme);
|
|
1006
1052
|
}
|
|
1007
1053
|
#modelDialogLines(open, width, height, theme) {
|
|
1054
|
+
if (open.modelDialog === "default") {
|
|
1055
|
+
const flags = this.#filteredModelFlags(open);
|
|
1056
|
+
const current = typeof open.entry.rawValue === "string" ? open.entry.rawValue : null;
|
|
1057
|
+
const rows = flags.length === 0
|
|
1058
|
+
? [{ label: "No models found", value: "" }]
|
|
1059
|
+
: flags.map(flag => ({
|
|
1060
|
+
...modelLabelParts(flag.label ?? humanizeLabel(flag.key)),
|
|
1061
|
+
...(flag.key === current ? { successSuffix: "✓" } : {}),
|
|
1062
|
+
value: "",
|
|
1063
|
+
}));
|
|
1064
|
+
const description = open.error ?? "Select the model used for new sessions";
|
|
1065
|
+
const visible = dialogWindow(rows, open.index, configureDialogChoiceCapacity(height, true, configureDialogTextRows(description, width)));
|
|
1066
|
+
return renderSteppedDialogPanel({
|
|
1067
|
+
title: "Default Model", description, input: open.filter ?? new LineInput(""),
|
|
1068
|
+
rows: visible.rows, index: visible.index, hint: MODEL_SHORTCUTS.hintEntries(MODEL_SCOPE),
|
|
1069
|
+
}, width, theme);
|
|
1070
|
+
}
|
|
1008
1071
|
if (open.step === "model") {
|
|
1009
1072
|
const flags = this.#filteredModelFlags(open);
|
|
1010
1073
|
const stored = structuredValue(open.entry, open.record);
|
|
@@ -1086,6 +1149,17 @@ function handleSettingsSearchPointer(state, event, panelTop) {
|
|
|
1086
1149
|
const handled = input.handlePointer({ kind: event.kind, button: event.button, column: event.column, row });
|
|
1087
1150
|
return { consumed: handled || overInput, render: handled };
|
|
1088
1151
|
}
|
|
1152
|
+
function orderDefaultModelBeforeThinking(entries, sectionId) {
|
|
1153
|
+
if (sectionId !== "agent")
|
|
1154
|
+
return entries;
|
|
1155
|
+
const defaultModel = entries.find(isDefaultModelEntry);
|
|
1156
|
+
if (defaultModel === undefined)
|
|
1157
|
+
return entries;
|
|
1158
|
+
const ordered = entries.filter(entry => entry !== defaultModel);
|
|
1159
|
+
const thinking = ordered.findIndex(entry => entry.backend === "agent" && entry.id === "modelThinkingLevels");
|
|
1160
|
+
ordered.splice(thinking < 0 ? ordered.length : thinking, 0, defaultModel);
|
|
1161
|
+
return ordered;
|
|
1162
|
+
}
|
|
1089
1163
|
function modelLabelParts(label) {
|
|
1090
1164
|
const match = /^(.*?)(\s+\[[^\]]+\])$/u.exec(label);
|
|
1091
1165
|
return match === null ? { label } : { label: match[1] ?? label, labelSuffix: match[2] ?? "" };
|
|
@@ -1139,6 +1213,9 @@ function changeFailure(outcome) {
|
|
|
1139
1213
|
return outcome.limitationReason ?? "the setting is unavailable";
|
|
1140
1214
|
return outcome.limitationReason;
|
|
1141
1215
|
}
|
|
1216
|
+
function isDefaultModelEntry(entry) {
|
|
1217
|
+
return entry.backend === "agent" && entry.id === "defaultModel";
|
|
1218
|
+
}
|
|
1142
1219
|
/** The source's own wording when it has one, otherwise the id made readable. */
|
|
1143
1220
|
function labelOf(entry) {
|
|
1144
1221
|
return entry.label ?? humanizeLabel(entry.id);
|
|
@@ -2,6 +2,7 @@ import { type AvailableRelease, type ReleaseChannel } from "./latest-release.js"
|
|
|
2
2
|
export declare const AUTOMATIC_UPDATE_STATE_SCHEMA = 1;
|
|
3
3
|
export declare const AUTOMATIC_UPDATE_RETRY_MS: number;
|
|
4
4
|
export declare const AUTOMATIC_UPDATE_ACTIVATION_RETRY_MS: number;
|
|
5
|
+
export declare const ACTIVE_RELEASE_FALLBACK_MS = 5000;
|
|
5
6
|
export interface AutomaticUpdateState {
|
|
6
7
|
readonly schema: typeof AUTOMATIC_UPDATE_STATE_SCHEMA;
|
|
7
8
|
readonly channel: ReleaseChannel;
|
|
@@ -18,6 +19,17 @@ export declare class AutomaticUpdateStateStore {
|
|
|
18
19
|
read(): Promise<AutomaticUpdateState | null>;
|
|
19
20
|
write(value: AutomaticUpdateState): Promise<void>;
|
|
20
21
|
}
|
|
22
|
+
export interface ActiveReleaseObservationOptions {
|
|
23
|
+
readonly runningVersion: string;
|
|
24
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
25
|
+
readonly signal?: AbortSignal;
|
|
26
|
+
readonly onNewer: (version: string) => void;
|
|
27
|
+
readonly fallbackMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/** Read the approved active release only when this process executes an older release on that channel. */
|
|
30
|
+
export declare function readNewerActiveRelease(runningVersion: string, environment?: NodeJS.ProcessEnv): Promise<string | null>;
|
|
31
|
+
/** Observe local release activation promptly, with a bounded fallback for missed filesystem events. */
|
|
32
|
+
export declare function observeNewerActiveRelease(options: ActiveReleaseObservationOptions): () => void;
|
|
21
33
|
export interface AutomaticUpdateRequestOptions {
|
|
22
34
|
readonly release: AvailableRelease;
|
|
23
35
|
readonly runningVersion: string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { watch } from "node:fs";
|
|
4
|
+
import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, resolve } from "node:path";
|
|
5
6
|
import { valid as validSemver } from "semver";
|
|
6
7
|
import { readLaunchContext } from "../launch-context/index.js";
|
|
7
8
|
import { resolveProductPaths } from "../lifecycle/index.js";
|
|
@@ -19,6 +20,7 @@ const AUTOMATIC_UPDATE_STATE_FILENAME = "automatic-update.json";
|
|
|
19
20
|
const MAX_STATE_BYTES = 16_384;
|
|
20
21
|
const OBSERVER_POLL_MS = 250;
|
|
21
22
|
const WORKER_START_GRACE_MS = 5_000;
|
|
23
|
+
export const ACTIVE_RELEASE_FALLBACK_MS = 5_000;
|
|
22
24
|
export class AutomaticUpdateStateStore {
|
|
23
25
|
path;
|
|
24
26
|
constructor(dataDir) { this.path = resolve(dataDir, AUTOMATIC_UPDATE_STATE_FILENAME); }
|
|
@@ -59,6 +61,82 @@ export class AutomaticUpdateStateStore {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
}
|
|
64
|
+
/** Read the approved active release only when this process executes an older release on that channel. */
|
|
65
|
+
export async function readNewerActiveRelease(runningVersion, environment = process.env) {
|
|
66
|
+
const channel = releaseChannelOf(runningVersion);
|
|
67
|
+
if (channel === null)
|
|
68
|
+
return null;
|
|
69
|
+
try {
|
|
70
|
+
const paths = resolveProductPaths(environment);
|
|
71
|
+
const active = await activeRelease(paths.dataDir);
|
|
72
|
+
return activeNewerPackageVersion(active, runningVersion, channel) ?? null;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Observe local release activation promptly, with a bounded fallback for missed filesystem events. */
|
|
79
|
+
export function observeNewerActiveRelease(options) {
|
|
80
|
+
if (releaseChannelOf(options.runningVersion) === null || options.signal?.aborted)
|
|
81
|
+
return () => { };
|
|
82
|
+
const paths = resolveProductPaths(options.environment ?? process.env);
|
|
83
|
+
const store = new CohortStateStore(paths.dataDir);
|
|
84
|
+
let closed = false;
|
|
85
|
+
let checking = false;
|
|
86
|
+
let requested = false;
|
|
87
|
+
let watcher = null;
|
|
88
|
+
let signature;
|
|
89
|
+
const stop = () => {
|
|
90
|
+
if (closed)
|
|
91
|
+
return;
|
|
92
|
+
closed = true;
|
|
93
|
+
clearInterval(fallback);
|
|
94
|
+
watcher?.close();
|
|
95
|
+
options.signal?.removeEventListener("abort", stop);
|
|
96
|
+
};
|
|
97
|
+
const check = async () => {
|
|
98
|
+
if (closed)
|
|
99
|
+
return;
|
|
100
|
+
if (checking) {
|
|
101
|
+
requested = true;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
checking = true;
|
|
105
|
+
try {
|
|
106
|
+
const version = await readNewerActiveRelease(options.runningVersion, options.environment);
|
|
107
|
+
if (version !== null && !closed) {
|
|
108
|
+
stop();
|
|
109
|
+
options.onNewer(version);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
checking = false;
|
|
114
|
+
if (requested && !closed) {
|
|
115
|
+
requested = false;
|
|
116
|
+
void check();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const checkChanged = async (force) => {
|
|
121
|
+
const next = await stat(store.path).then(value => `${value.dev}:${value.ino}:${value.ctimeMs}:${value.mtimeMs}:${value.size}`, () => null);
|
|
122
|
+
if (!force && next === signature)
|
|
123
|
+
return;
|
|
124
|
+
signature = next;
|
|
125
|
+
await check();
|
|
126
|
+
};
|
|
127
|
+
const fallback = setInterval(() => { void checkChanged(false); }, options.fallbackMs ?? ACTIVE_RELEASE_FALLBACK_MS);
|
|
128
|
+
fallback.unref?.();
|
|
129
|
+
try {
|
|
130
|
+
watcher = watch(dirname(store.path), { persistent: false }, (_event, filename) => {
|
|
131
|
+
if (filename === null || filename.toString() === basename(store.path))
|
|
132
|
+
void checkChanged(false);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch { /* Rationale: the bounded metadata fallback remains authoritative when watching is unavailable. */ }
|
|
136
|
+
options.signal?.addEventListener("abort", stop, { once: true });
|
|
137
|
+
void checkChanged(true);
|
|
138
|
+
return stop;
|
|
139
|
+
}
|
|
62
140
|
/** Start or observe one silent update and resolve with the active newer version once restart is useful. */
|
|
63
141
|
export async function requestAutomaticUpdate(options) {
|
|
64
142
|
if (options.signal?.aborted)
|
|
@@ -292,9 +370,13 @@ async function spawnAutomaticUpdateWorker(entry, arguments_, environment) {
|
|
|
292
370
|
}
|
|
293
371
|
async function activeNewerVersion(dataDir, runningVersion, target) {
|
|
294
372
|
const active = await activeRelease(dataDir);
|
|
295
|
-
return active
|
|
296
|
-
|
|
297
|
-
|
|
373
|
+
return activeNewerPackageVersion(active, runningVersion, target.channel, target.version);
|
|
374
|
+
}
|
|
375
|
+
function activeNewerPackageVersion(active, runningVersion, channel, minimum) {
|
|
376
|
+
if (active === null || releaseChannelOf(active.packageVersion) !== channel
|
|
377
|
+
|| !isNewerRelease(active.packageVersion, runningVersion))
|
|
378
|
+
return null;
|
|
379
|
+
return minimum === undefined || active.packageVersion === minimum || isNewerRelease(active.packageVersion, minimum)
|
|
298
380
|
? active.packageVersion : null;
|
|
299
381
|
}
|
|
300
382
|
async function activeRelease(dataDir) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SettingsManager } from "../startup-public.js";
|
|
2
|
+
import type { AgentSettingChangeOutcome, AgentSettingDescriptor } from "../../../contracts/agent-engine/index.js";
|
|
3
|
+
interface DefaultModelChoice {
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const DEFAULT_MODEL_SETTING_KEY = "defaultModel";
|
|
9
|
+
export declare const SUPPLEMENTAL_SETTING_KEYS: readonly ["defaultModel"];
|
|
10
|
+
export declare function defaultModelDescriptor(settings: SettingsManager, models: readonly DefaultModelChoice[]): AgentSettingDescriptor;
|
|
11
|
+
export declare function writeDefaultModel(settings: SettingsManager, models: readonly DefaultModelChoice[], value: unknown): Promise<AgentSettingChangeOutcome>;
|
|
12
|
+
export {};
|