@timurproko/a1 0.2.7-dev.1221 → 0.2.7-dev.1225
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/README.md +8 -6
- package/bin/ui.js +1 -0
- package/dist/app/session-shell/dock-chrome.js +1 -1
- package/dist/app/session-shell/exit-notice.d.ts +2 -2
- package/dist/app/session-shell/exit-notice.js +4 -4
- package/dist/app/session-shell/session-lifecycle.d.ts +2 -1
- package/dist/app/session-shell/session-lifecycle.js +4 -2
- package/dist/app/session-shell/session-shell-root.d.ts +2 -0
- package/dist/app/session-shell/session-shell-root.js +3 -2
- package/dist/app/session-shell/session-shell.js +9 -1
- package/dist/app/session-shell/session-viewport-controller.d.ts +2 -1
- package/dist/app/session-shell/session-viewport-controller.js +26 -37
- package/dist/app/session-shell/shell-editor-factory.js +4 -1
- package/dist/cli/dispatch.js +15 -2
- package/dist/composition/owned-ui.d.ts +2 -0
- package/dist/composition/owned-ui.js +43 -11
- package/dist/composition/reference-selection-options.d.ts +3 -0
- package/dist/composition/reference-selection-options.js +16 -0
- package/dist/composition/settings-route-host.d.ts +1 -2
- package/dist/composition/settings-route-host.js +19 -13
- package/dist/composition/settings-search-input.js +4 -1
- package/dist/composition/utility-tab.js +4 -2
- package/dist/features/launch/intent.js +2 -2
- package/dist/features/owned-ui/plus-menu-dialog.js +28 -20
- package/dist/features/owned-ui/reference-document-selection.d.ts +30 -0
- package/dist/features/owned-ui/reference-document-selection.js +179 -0
- package/dist/features/owned-ui/reference-screen-app.d.ts +4 -1
- package/dist/features/owned-ui/reference-screen-app.js +39 -8
- package/dist/features/owned-ui/settings-app.d.ts +1 -0
- package/dist/features/owned-ui/settings-app.js +23 -19
- package/dist/foundation/release/automatic-update.js +90 -22
- package/dist/foundation/release/bootstrap.js +2 -2
- package/dist/foundation/release/latest-release.d.ts +5 -1
- package/dist/foundation/release/latest-release.js +74 -31
- package/dist/integrations/pi/components/owned-editor-ux.js +48 -5
- package/dist/integrations/pi/engine/workflow-runner.js +1 -1
- 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 +17 -8
- package/dist/ui/components/dialog-panel.js +97 -26
- package/dist/ui/components/index.d.ts +3 -3
- package/dist/ui/components/index.js +3 -3
- package/dist/ui/components/scrollbar.d.ts +1 -1
- package/dist/ui/components/scrollbar.js +1 -1
- package/dist/ui/components/selection-copy.d.ts +2 -4
- package/dist/ui/components/selection-copy.js +4 -2
- package/dist/ui/components/text-selection.d.ts +1 -2
- package/dist/ui/components/text-selection.js +5 -2
- package/dist/ui/components/transcript-viewport.js +3 -3
- package/dist/ui/settings/declarations.d.ts +1 -1
- package/dist/ui/settings/declarations.js +1 -1
- package/docs/architecture/resident-tabs.md +5 -4
- package/docs/features/launch-profiles.md +7 -4
- package/package.json +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
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, 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";
|
|
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
3
|
import { 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";
|
|
@@ -125,7 +125,11 @@ export class SettingsApp {
|
|
|
125
125
|
*/
|
|
126
126
|
constructor(session, options) {
|
|
127
127
|
this.#session = session;
|
|
128
|
-
this.#options = {
|
|
128
|
+
this.#options = {
|
|
129
|
+
pinned: options.pinned === true,
|
|
130
|
+
configurePlusMenu: options.configurePlusMenu ?? null,
|
|
131
|
+
dialogPanel: options.dialogPanel ?? null,
|
|
132
|
+
};
|
|
129
133
|
this.#search = {
|
|
130
134
|
input: null,
|
|
131
135
|
scrollBefore: null,
|
|
@@ -148,12 +152,11 @@ export class SettingsApp {
|
|
|
148
152
|
}
|
|
149
153
|
render(rect, host) {
|
|
150
154
|
const theme = host.theme ?? PLAIN_THEME;
|
|
151
|
-
const rows = this.#rows();
|
|
152
|
-
const selected = indexOfKey(rows, this.#selectedKey);
|
|
155
|
+
const rows = this.#rows(), selected = indexOfKey(rows, this.#selectedKey);
|
|
153
156
|
const footer = this.#footerLines(rect.width, theme);
|
|
154
|
-
// Invariant:
|
|
155
|
-
//
|
|
156
|
-
const dividerRows = this.#search.input === null
|
|
157
|
+
// Invariant: search owns its ruled footer. Configure dialogs float over the complete ordinary
|
|
158
|
+
// Settings frame, so the standing divider and guidance remain visible in the dimmed backdrop.
|
|
159
|
+
const dividerRows = this.#search.input === null ? SETTINGS_FOOTER_DIVIDER_ROWS : 0;
|
|
157
160
|
const topRows = Math.min(SETTINGS_TOP_RULE_ROWS, rect.height);
|
|
158
161
|
const contentHeight = Math.max(0, rect.height - topRows - dividerRows - footer.length);
|
|
159
162
|
let titleRows = this.#scroll <= 0 ? Math.min(SETTINGS_TITLE_ROWS, contentHeight) : 0;
|
|
@@ -246,7 +249,8 @@ export class SettingsApp {
|
|
|
246
249
|
});
|
|
247
250
|
const rule = theme.fg("border", "─".repeat(Math.max(0, rect.width)));
|
|
248
251
|
const frame = this.#withMenu([rule, ...withRail, ...(dividerRows === 0 ? [] : [rule]), ...footer], selected, layout, valueColumn, theme, rect, reservesRail ? RAIL_COLUMNS : 0);
|
|
249
|
-
|
|
252
|
+
const complete = frame.slice(0, rect.height).concat(Array(Math.max(0, rect.height - frame.length)).fill("")), open = this.#structured, dialogTheme = this.#options.dialogPanel === null ? theme : { ...theme, panel: this.#options.dialogPanel };
|
|
253
|
+
return open === null ? complete : renderCenteredDialogOverlay(complete, this.#dialogLines(open, configureDialogWidth(rect.width), rect.height, dialogTheme), rect, dialogTheme);
|
|
250
254
|
}
|
|
251
255
|
onInput(data, host) {
|
|
252
256
|
if (data === "\u0003" && this.#search.input === null)
|
|
@@ -983,9 +987,9 @@ export class SettingsApp {
|
|
|
983
987
|
this.#menuFrame = frame;
|
|
984
988
|
return renderValueMenu(lines, state, frame, theme);
|
|
985
989
|
}
|
|
986
|
-
#dialogLines(open, width, theme) {
|
|
990
|
+
#dialogLines(open, width, height, theme) {
|
|
987
991
|
if (open.modelDialog)
|
|
988
|
-
return this.#modelDialogLines(open, width, theme);
|
|
992
|
+
return this.#modelDialogLines(open, width, height, theme);
|
|
989
993
|
if (open.plusMenu !== null)
|
|
990
994
|
return open.plusMenu.lines(open.record, width, theme);
|
|
991
995
|
const rows = open.flags.map(key => {
|
|
@@ -997,9 +1001,10 @@ export class SettingsApp {
|
|
|
997
1001
|
...(declared?.description === undefined ? {} : { description: declared.description }),
|
|
998
1002
|
};
|
|
999
1003
|
});
|
|
1000
|
-
|
|
1004
|
+
const visible = dialogWindow(rows, open.index, configureDialogChoiceCapacity(height, false, configureDialogTextRows(rows[open.index]?.description ?? "", width)));
|
|
1005
|
+
return renderDialogPanel({ title: labelOf(open.entry), rows: visible.rows, index: visible.index, hint: SETTINGS_SHORTCUTS.hintEntries(DIALOG_SCOPE) }, width, theme);
|
|
1001
1006
|
}
|
|
1002
|
-
#modelDialogLines(open, width, theme) {
|
|
1007
|
+
#modelDialogLines(open, width, height, theme) {
|
|
1003
1008
|
if (open.step === "model") {
|
|
1004
1009
|
const flags = this.#filteredModelFlags(open);
|
|
1005
1010
|
const stored = structuredValue(open.entry, open.record);
|
|
@@ -1009,12 +1014,13 @@ export class SettingsApp {
|
|
|
1009
1014
|
...modelLabelParts(flag.label ?? humanizeLabel(flag.key)),
|
|
1010
1015
|
value: stored[flag.key] === undefined ? "" : String(stored[flag.key]),
|
|
1011
1016
|
}));
|
|
1012
|
-
const
|
|
1017
|
+
const description = "Select a model to configure";
|
|
1018
|
+
const visible = dialogWindow(allRows, open.index, configureDialogChoiceCapacity(height, true, configureDialogTextRows(description, width)));
|
|
1013
1019
|
return renderSteppedDialogPanel({
|
|
1014
1020
|
title: "Thinking Level",
|
|
1015
1021
|
step: 1,
|
|
1016
1022
|
steps: 2,
|
|
1017
|
-
description
|
|
1023
|
+
description,
|
|
1018
1024
|
...(open.filter === null ? {} : { input: open.filter }),
|
|
1019
1025
|
rows: visible.rows,
|
|
1020
1026
|
index: visible.index,
|
|
@@ -1027,21 +1033,19 @@ export class SettingsApp {
|
|
|
1027
1033
|
label: choice.value === current ? `✓ ${choice.label}` : choice.label,
|
|
1028
1034
|
value: choice.description,
|
|
1029
1035
|
}));
|
|
1030
|
-
const
|
|
1036
|
+
const description = `Select default thinking level for ${selected?.label ?? open.modelKey ?? "model"}`;
|
|
1037
|
+
const visible = dialogWindow(allRows, open.index, configureDialogChoiceCapacity(height, false, configureDialogTextRows(description, width)));
|
|
1031
1038
|
return renderSteppedDialogPanel({
|
|
1032
1039
|
title: "Thinking Level",
|
|
1033
1040
|
step: 2,
|
|
1034
1041
|
steps: 2,
|
|
1035
|
-
description
|
|
1042
|
+
description,
|
|
1036
1043
|
rows: visible.rows,
|
|
1037
1044
|
index: visible.index,
|
|
1038
1045
|
hint: MODEL_SHORTCUTS.hintEntries(MODEL_LEVEL_SCOPE),
|
|
1039
1046
|
}, width, theme);
|
|
1040
1047
|
}
|
|
1041
1048
|
#footerLines(width, theme) {
|
|
1042
|
-
const open = this.#structured;
|
|
1043
|
-
if (open !== null)
|
|
1044
|
-
return this.#dialogLines(open, width, theme);
|
|
1045
1049
|
// Rationale: standing guidance is more useful than transient feedback; failures still
|
|
1046
1050
|
// roll back without displacing the Settings interaction contract.
|
|
1047
1051
|
const entries = SETTINGS_SHORTCUTS.hintEntries(SCOPE).filter(entry => !this.#options.pinned
|
|
@@ -9,7 +9,8 @@ import { CohortStateStore } from "./cohort-state.js";
|
|
|
9
9
|
import { processIsAlive } from "./process-cleanup.js";
|
|
10
10
|
import { isNewerRelease, releaseChannelOf } from "./latest-release.js";
|
|
11
11
|
import { readCertifiedReleaseManifest, resolveReleaseEntryPoint } from "./release-store.js";
|
|
12
|
-
import { readUpdateExecutionOwner } from "./update-execution-lease.js";
|
|
12
|
+
import { acquireUpdateExecutionLease, readUpdateExecutionOwner } from "./update-execution-lease.js";
|
|
13
|
+
import { UpdateTransactionStore } from "./update-transaction.js";
|
|
13
14
|
import { runSelfUpdate, UPDATE_ALREADY_RUNNING_EXIT_CODE } from "./update.js";
|
|
14
15
|
export const AUTOMATIC_UPDATE_STATE_SCHEMA = 1;
|
|
15
16
|
export const AUTOMATIC_UPDATE_RETRY_MS = 24 * 60 * 60_000;
|
|
@@ -72,10 +73,37 @@ export async function requestAutomaticUpdate(options) {
|
|
|
72
73
|
catch {
|
|
73
74
|
return null;
|
|
74
75
|
}
|
|
75
|
-
const readActiveVersion = options.activeVersion ?? (async () => await activeNewerVersion(paths.dataDir, options.runningVersion));
|
|
76
|
+
const readActiveVersion = options.activeVersion ?? (async () => await activeNewerVersion(paths.dataDir, options.runningVersion, options.release));
|
|
76
77
|
const active = await readActiveVersion();
|
|
77
78
|
if (active !== null)
|
|
78
79
|
return active;
|
|
80
|
+
let owner;
|
|
81
|
+
try {
|
|
82
|
+
owner = await readUpdateExecutionOwner(paths.dataDir);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
let transaction;
|
|
88
|
+
try {
|
|
89
|
+
transaction = await new UpdateTransactionStore(paths.dataDir).read();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
// Invariant: recovery of a foreign in-progress transaction belongs to its own owner.
|
|
95
|
+
if (transaction?.status === "active" && (transaction.targetVersion !== options.release.version
|
|
96
|
+
|| transaction.channel !== (options.release.channel === "stable" ? "stable" : "next")))
|
|
97
|
+
return null;
|
|
98
|
+
if (existing?.status === "running" && now() - existing.updatedAt >= WORKER_START_GRACE_MS
|
|
99
|
+
&& (owner === null || !processIsAlive(owner.pid))) {
|
|
100
|
+
// Invariant: a coordinator record is not ownership. Settle only when no transaction
|
|
101
|
+
// remains to recover; never overwrite or roll back a foreign in-progress target.
|
|
102
|
+
if (transaction === null) {
|
|
103
|
+
existing = { ...existing, status: "failed", updatedAt: now(), failure: "worker-exited" };
|
|
104
|
+
await store.write(existing).catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
79
107
|
if (existing?.status === "failed" && existing.channel === options.release.channel && now() - existing.attemptedAt >= 0
|
|
80
108
|
&& now() - existing.attemptedAt < AUTOMATIC_UPDATE_RETRY_MS
|
|
81
109
|
&& !isNewerRelease(options.release.version, existing.targetVersion))
|
|
@@ -100,15 +128,16 @@ export async function requestAutomaticUpdate(options) {
|
|
|
100
128
|
"--target-version", options.release.version,
|
|
101
129
|
"--data-dir", paths.dataDir,
|
|
102
130
|
];
|
|
103
|
-
let owner;
|
|
104
|
-
try {
|
|
105
|
-
owner = await readUpdateExecutionOwner(paths.dataDir);
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
131
|
if (owner === null || !processIsAlive(owner.pid)) {
|
|
111
|
-
|
|
132
|
+
try {
|
|
133
|
+
await (options.spawnWorker ?? spawnAutomaticUpdateWorker)(worker.entry, arguments_, environment);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Invariant: no worker began this attempt. Do not invent a successful install or an owner.
|
|
137
|
+
await store.write(state({ packageRoot: worker.packageRoot, channel: options.release.channel,
|
|
138
|
+
targetVersion: options.release.version, dataDir: paths.dataDir }, "failed", now(), null, "worker-start-failed")).catch(() => undefined);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
112
141
|
}
|
|
113
142
|
const startedAt = now();
|
|
114
143
|
const sleep = options.sleep ?? (async (durationMs) => await new Promise(resolvePromise => {
|
|
@@ -134,15 +163,22 @@ export async function runAutomaticUpdateWorker(argv, environment = process.env)
|
|
|
134
163
|
const paths = resolveProductPaths(environment);
|
|
135
164
|
if (!samePath(parsed.dataDir, paths.dataDir))
|
|
136
165
|
return 1;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
let verified = false;
|
|
167
|
+
try {
|
|
168
|
+
const launch = readLaunchContext(environment, "release");
|
|
169
|
+
const cohort = await new CohortStateStore(paths.dataDir).read();
|
|
170
|
+
const record = launch.releaseId === undefined ? undefined : cohort.releases[launch.releaseId];
|
|
171
|
+
if (record?.approval === "approved" && samePath(record.releaseRoot, launch.releaseRoot ?? "")
|
|
172
|
+
&& record.contentDigest === launch.releaseDigest) {
|
|
173
|
+
const selected = await readCertifiedReleaseManifest(record, resolve(paths.dataDir, "releases"));
|
|
174
|
+
verified = samePath(selected.packageRoot, parsed.packageRoot);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch { /* Security: invalid provenance fails closed without running the updater. */ }
|
|
178
|
+
if (!verified) {
|
|
179
|
+
await recordAutomaticPreflightFailure(parsed, paths.dataDir);
|
|
145
180
|
return 1;
|
|
181
|
+
}
|
|
146
182
|
const store = new AutomaticUpdateStateStore(paths.dataDir);
|
|
147
183
|
const attemptedAt = Date.now();
|
|
148
184
|
const output = { stdout() { }, stderr() { } };
|
|
@@ -166,7 +202,10 @@ export async function runAutomaticUpdateWorker(argv, environment = process.env)
|
|
|
166
202
|
return 0;
|
|
167
203
|
if (code !== 0) {
|
|
168
204
|
await runningWrite;
|
|
169
|
-
|
|
205
|
+
if (acquired)
|
|
206
|
+
await store.write(state(parsed, "failed", attemptedAt, null, `exit-${code}`));
|
|
207
|
+
else
|
|
208
|
+
await recordAutomaticPreflightFailure(parsed, paths.dataDir);
|
|
170
209
|
return code;
|
|
171
210
|
}
|
|
172
211
|
const active = await activeRelease(paths.dataDir);
|
|
@@ -176,6 +215,8 @@ export async function runAutomaticUpdateWorker(argv, environment = process.env)
|
|
|
176
215
|
await runningWrite;
|
|
177
216
|
await store.write(state(parsed, "failed", attemptedAt, null, "target-not-active"));
|
|
178
217
|
}
|
|
218
|
+
else
|
|
219
|
+
await recordAutomaticPreflightFailure(parsed, paths.dataDir);
|
|
179
220
|
return 1;
|
|
180
221
|
}
|
|
181
222
|
await runningWrite;
|
|
@@ -184,10 +225,34 @@ export async function runAutomaticUpdateWorker(argv, environment = process.env)
|
|
|
184
225
|
}
|
|
185
226
|
catch (error) {
|
|
186
227
|
await runningWrite.catch(() => undefined);
|
|
187
|
-
|
|
228
|
+
if (acquired)
|
|
229
|
+
await store.write(state(parsed, "failed", attemptedAt, null, errorMessage(error).slice(0, 512))).catch(() => undefined);
|
|
230
|
+
else
|
|
231
|
+
await recordAutomaticPreflightFailure(parsed, paths.dataDir);
|
|
188
232
|
return 1;
|
|
189
233
|
}
|
|
190
234
|
}
|
|
235
|
+
async function recordAutomaticPreflightFailure(parsed, dataDir) {
|
|
236
|
+
// Security: even a rejected worker must claim the ordinary update lease before recording
|
|
237
|
+
// failure, so it cannot overwrite another owner's result or an unfinished transaction.
|
|
238
|
+
const lease = await acquireUpdateExecutionLease({ dataDir, packageRoot: parsed.packageRoot,
|
|
239
|
+
channel: parsed.channel === "stable" ? "stable" : "next", targetVersion: parsed.targetVersion, mode: "automatic" }).catch(() => null);
|
|
240
|
+
if (lease?.status !== "acquired")
|
|
241
|
+
return;
|
|
242
|
+
try {
|
|
243
|
+
if (await new UpdateTransactionStore(dataDir).read() !== null)
|
|
244
|
+
return;
|
|
245
|
+
const store = new AutomaticUpdateStateStore(dataDir);
|
|
246
|
+
const existing = await store.read();
|
|
247
|
+
if (existing !== null && (existing.targetVersion !== parsed.targetVersion || existing.status === "installed"))
|
|
248
|
+
return;
|
|
249
|
+
await store.write(state(parsed, "failed", Date.now(), null, "worker-preflight-failed"));
|
|
250
|
+
}
|
|
251
|
+
catch { /* Rationale: preflight failure is best effort; never mask the rejected worker. */ }
|
|
252
|
+
finally {
|
|
253
|
+
await lease.release().catch(() => undefined);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
191
256
|
async function spawnAutomaticUpdateWorker(entry, arguments_, environment) {
|
|
192
257
|
const child = spawn(process.execPath, [entry, ...arguments_], {
|
|
193
258
|
detached: true,
|
|
@@ -201,9 +266,12 @@ async function spawnAutomaticUpdateWorker(entry, arguments_, environment) {
|
|
|
201
266
|
});
|
|
202
267
|
child.unref();
|
|
203
268
|
}
|
|
204
|
-
async function activeNewerVersion(dataDir, runningVersion) {
|
|
269
|
+
async function activeNewerVersion(dataDir, runningVersion, target) {
|
|
205
270
|
const active = await activeRelease(dataDir);
|
|
206
|
-
return active !== null &&
|
|
271
|
+
return active !== null && releaseChannelOf(active.packageVersion) === target.channel
|
|
272
|
+
&& isNewerRelease(active.packageVersion, runningVersion)
|
|
273
|
+
&& (active.packageVersion === target.version || isNewerRelease(active.packageVersion, target.version))
|
|
274
|
+
? active.packageVersion : null;
|
|
207
275
|
}
|
|
208
276
|
async function activeRelease(dataDir) {
|
|
209
277
|
const cohort = await new CohortStateStore(dataDir).read();
|
|
@@ -31,8 +31,8 @@ async function runBootstrapAttempt(options, retriesRemaining) {
|
|
|
31
31
|
await markStartupPhase(environment, "bootstrap-start");
|
|
32
32
|
assertLaunchProfileId(launchProfileId);
|
|
33
33
|
const sessionArgs = sessionSelectionArguments(options.launchIntent?.sessionSelection);
|
|
34
|
-
if (sessionArgs.length > 0 &&
|
|
35
|
-
throw new Error("session selection requires the
|
|
34
|
+
if (sessionArgs.length > 0 && launchProfileId !== "a1") {
|
|
35
|
+
throw new Error("session selection requires the A1 profile");
|
|
36
36
|
}
|
|
37
37
|
const output = options.output ?? process.stderr;
|
|
38
38
|
const paths = resolveProductPaths(environment);
|
|
@@ -21,6 +21,7 @@ export interface AvailableRelease {
|
|
|
21
21
|
}
|
|
22
22
|
export declare const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
23
23
|
export declare const UPDATE_CHECK_INTERVAL_MS: number;
|
|
24
|
+
export declare const NOTIFY_CHECK_INTERVAL_MS: number;
|
|
24
25
|
/** One published version with the pull request its manifest records, when it records one. */
|
|
25
26
|
export interface PublishedVersion {
|
|
26
27
|
readonly version: string;
|
|
@@ -54,7 +55,7 @@ export declare function parsePublishedVersions(document: unknown): PublishedVers
|
|
|
54
55
|
/** Forget the startup release check so the next interactive launch asks the registry again. */
|
|
55
56
|
export declare function invalidateReleaseCheckCache(configDir: string): Promise<void>;
|
|
56
57
|
export declare function unavailableDistTags(error: string): DistTagVersions;
|
|
57
|
-
export declare function createRegistryFetcher(timeoutMs: number): RegistryFetcher;
|
|
58
|
+
export declare function createRegistryFetcher(timeoutMs: number, signal?: AbortSignal): RegistryFetcher;
|
|
58
59
|
export interface StartupReleaseCheckOptions {
|
|
59
60
|
readonly runningVersion: string;
|
|
60
61
|
readonly configDir: string;
|
|
@@ -64,6 +65,9 @@ export interface StartupReleaseCheckOptions {
|
|
|
64
65
|
/** The bare-A1 `updateCheck` setting; comparison profiles omit it and use auto. */
|
|
65
66
|
readonly settingMode?: StartupUpdateMode;
|
|
66
67
|
readonly fetcher?: RegistryFetcher;
|
|
68
|
+
readonly signal?: AbortSignal;
|
|
69
|
+
/** Remaining cache lifetime for a live Auto observer, when discovery succeeds. */
|
|
70
|
+
readonly onNextCheckDelay?: (milliseconds: number) => void;
|
|
67
71
|
readonly now?: () => number;
|
|
68
72
|
}
|
|
69
73
|
/** Why the startup check is skipped before any cache read or network access, or null when it may run. */
|
|
@@ -2,12 +2,14 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { gt, prerelease, valid as validSemver } from "semver";
|
|
5
|
+
import lockfile from "proper-lockfile";
|
|
5
6
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
6
7
|
export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
7
|
-
export const UPDATE_CHECK_INTERVAL_MS =
|
|
8
|
+
export const UPDATE_CHECK_INTERVAL_MS = 15 * 60_000;
|
|
9
|
+
export const NOTIFY_CHECK_INTERVAL_MS = 24 * 60 * 60_000;
|
|
8
10
|
const STARTUP_FETCH_TIMEOUT_MS = 3_000;
|
|
9
11
|
const CACHE_FILENAME = "update-check.json";
|
|
10
|
-
const CACHE_VERSION =
|
|
12
|
+
const CACHE_VERSION = 2;
|
|
11
13
|
const MAX_CACHE_BYTES = 4096;
|
|
12
14
|
const MAX_PACKAGE_DOCUMENT_BYTES = 32 * 1024 * 1024;
|
|
13
15
|
const DEVELOPMENT_BUILD = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-dev\.(0|[1-9]\d*)$/u;
|
|
@@ -105,13 +107,14 @@ export function parsePublishedVersions(document) {
|
|
|
105
107
|
}
|
|
106
108
|
/** Forget the startup release check so the next interactive launch asks the registry again. */
|
|
107
109
|
export async function invalidateReleaseCheckCache(configDir) {
|
|
108
|
-
await rm(join(configDir, CACHE_FILENAME), { force: true });
|
|
110
|
+
await withCacheLock(configDir, async () => { await rm(join(configDir, CACHE_FILENAME), { force: true }); });
|
|
109
111
|
}
|
|
110
112
|
export function unavailableDistTags(error) {
|
|
111
113
|
return { release: null, develop: null, error };
|
|
112
114
|
}
|
|
113
|
-
export function createRegistryFetcher(timeoutMs) {
|
|
114
|
-
return async (url) => await fetch(url, { headers: { accept: "application/json" },
|
|
115
|
+
export function createRegistryFetcher(timeoutMs, signal) {
|
|
116
|
+
return async (url) => await fetch(url, { headers: { accept: "application/json" },
|
|
117
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs) });
|
|
115
118
|
}
|
|
116
119
|
/** Why the startup check is skipped before any cache read or network access, or null when it may run. */
|
|
117
120
|
export function startupReleaseCheckSkipReason(options) {
|
|
@@ -136,22 +139,33 @@ export function startupReleaseCheckSkipReason(options) {
|
|
|
136
139
|
* answer replaces the cache. Every failure resolves to null: the notice is best-effort.
|
|
137
140
|
*/
|
|
138
141
|
export async function checkForNewerRelease(options) {
|
|
139
|
-
if (startupReleaseCheckSkipReason(options) !== null)
|
|
142
|
+
if (options.signal?.aborted || startupReleaseCheckSkipReason(options) !== null)
|
|
140
143
|
return null;
|
|
141
144
|
const channel = releaseChannelOf(options.runningVersion);
|
|
142
145
|
const environment = options.environment ?? process.env;
|
|
143
146
|
const now = options.now ?? Date.now;
|
|
144
147
|
const cachePath = join(options.configDir, CACHE_FILENAME);
|
|
145
148
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
latest = channel === "stable" ? tags.release : tags.develop;
|
|
150
|
-
if (tags.error !== null || latest === null)
|
|
149
|
+
const interval = options.settingMode === "notify" ? NOTIFY_CHECK_INTERVAL_MS : UPDATE_CHECK_INTERVAL_MS;
|
|
150
|
+
const latest = await withCacheLock(options.configDir, async () => {
|
|
151
|
+
if (options.signal?.aborted)
|
|
151
152
|
return null;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
const document = await readCache(cachePath);
|
|
154
|
+
const cached = freshVersion(document[channel], now(), interval);
|
|
155
|
+
if (cached !== null)
|
|
156
|
+
return cached;
|
|
157
|
+
const tags = await queryRegistryDistTags(options.fetcher ?? createRegistryFetcher(STARTUP_FETCH_TIMEOUT_MS, options.signal), environment.npm_config_registry?.trim() || undefined);
|
|
158
|
+
const discovered = channel === "stable" ? tags.release : tags.develop;
|
|
159
|
+
if (tags.error !== null || discovered === null || options.signal?.aborted)
|
|
160
|
+
return null;
|
|
161
|
+
const entry = { latest: discovered, checkedAt: now() };
|
|
162
|
+
await writeCache(options.configDir, cachePath, { ...document, [channel]: entry }).catch(() => undefined);
|
|
163
|
+
return entry;
|
|
164
|
+
});
|
|
165
|
+
if (options.signal?.aborted || latest === null)
|
|
166
|
+
return null;
|
|
167
|
+
options.onNextCheckDelay?.(Math.min(interval, Math.max(1, interval - (now() - latest.checkedAt))));
|
|
168
|
+
return isNewerRelease(latest.latest, options.runningVersion) ? availableRelease(channel, latest.latest) : null;
|
|
155
169
|
}
|
|
156
170
|
catch {
|
|
157
171
|
return null;
|
|
@@ -165,29 +179,58 @@ export function availableRelease(channel, version) {
|
|
|
165
179
|
changelogUrl: channel === "stable" ? `${RELEASE_PAGE}${version}` : null,
|
|
166
180
|
};
|
|
167
181
|
}
|
|
168
|
-
|
|
169
|
-
|
|
182
|
+
function validEntry(value, channel) {
|
|
183
|
+
if (typeof value !== "object" || value === null)
|
|
184
|
+
return undefined;
|
|
185
|
+
const entry = value;
|
|
186
|
+
return typeof entry.latest === "string" && releaseChannelOf(entry.latest) === channel
|
|
187
|
+
&& typeof entry.checkedAt === "number" && Number.isFinite(entry.checkedAt) ? { latest: entry.latest, checkedAt: entry.checkedAt } : undefined;
|
|
188
|
+
}
|
|
189
|
+
async function readCache(path) {
|
|
170
190
|
try {
|
|
171
191
|
const source = await readFile(path, "utf8");
|
|
172
|
-
if (source
|
|
173
|
-
return
|
|
174
|
-
|
|
192
|
+
if (Buffer.byteLength(source, "utf8") > MAX_CACHE_BYTES)
|
|
193
|
+
return { version: CACHE_VERSION };
|
|
194
|
+
const value = JSON.parse(source);
|
|
195
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
196
|
+
return { version: CACHE_VERSION };
|
|
197
|
+
const cache = value;
|
|
198
|
+
if (cache.version === 1) {
|
|
199
|
+
// Compatibility: an old cache has exactly one channel; do not infer a value for the other.
|
|
200
|
+
if (cache.channel !== "stable" && cache.channel !== "development")
|
|
201
|
+
return { version: CACHE_VERSION };
|
|
202
|
+
const entry = validEntry(cache, cache.channel);
|
|
203
|
+
return entry ? { version: CACHE_VERSION, [cache.channel]: entry } : { version: CACHE_VERSION };
|
|
204
|
+
}
|
|
205
|
+
if (cache.version !== CACHE_VERSION)
|
|
206
|
+
return { version: CACHE_VERSION };
|
|
207
|
+
const stable = validEntry(cache.stable, "stable");
|
|
208
|
+
const development = validEntry(cache.development, "development");
|
|
209
|
+
return { version: CACHE_VERSION, ...(stable ? { stable } : {}), ...(development ? { development } : {}) };
|
|
175
210
|
}
|
|
176
211
|
catch {
|
|
177
|
-
return
|
|
212
|
+
return { version: CACHE_VERSION };
|
|
178
213
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (cache.version !== CACHE_VERSION || cache.channel !== channel)
|
|
183
|
-
return null;
|
|
184
|
-
if (typeof cache.latest !== "string" || validSemver(cache.latest) === null)
|
|
185
|
-
return null;
|
|
186
|
-
if (typeof cache.checkedAt !== "number" || !Number.isFinite(cache.checkedAt))
|
|
214
|
+
}
|
|
215
|
+
function freshVersion(entry, now, interval) {
|
|
216
|
+
if (!entry)
|
|
187
217
|
return null;
|
|
188
|
-
// Invariant: a timestamp from the future
|
|
189
|
-
const age = now -
|
|
190
|
-
return age >= 0 && age <
|
|
218
|
+
// Invariant: a timestamp from the future is stale rather than fresh forever.
|
|
219
|
+
const age = now - entry.checkedAt;
|
|
220
|
+
return age >= 0 && age < interval ? entry : null;
|
|
221
|
+
}
|
|
222
|
+
async function withCacheLock(directory, operation) {
|
|
223
|
+
await mkdir(directory, { recursive: true });
|
|
224
|
+
const release = await lockfile.lock(join(directory, CACHE_FILENAME), {
|
|
225
|
+
realpath: false, stale: 30_000, update: 10_000,
|
|
226
|
+
retries: { retries: 20, factor: 1.2, minTimeout: 20, maxTimeout: 100, randomize: true },
|
|
227
|
+
});
|
|
228
|
+
try {
|
|
229
|
+
return await operation();
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
await release();
|
|
233
|
+
}
|
|
191
234
|
}
|
|
192
235
|
async function writeCache(directory, path, document) {
|
|
193
236
|
await mkdir(directory, { recursive: true });
|
|
@@ -67,7 +67,7 @@ class PromptSelectionInterceptor {
|
|
|
67
67
|
#lastClick;
|
|
68
68
|
#redoStack = [];
|
|
69
69
|
#selectionRevision = 0;
|
|
70
|
-
#
|
|
70
|
+
#pasteState = { generation: 0, preview: undefined };
|
|
71
71
|
#terminalPaste;
|
|
72
72
|
#wordDirection;
|
|
73
73
|
#geometry;
|
|
@@ -81,6 +81,7 @@ class PromptSelectionInterceptor {
|
|
|
81
81
|
installAtomicSegmentation(editor, options.atomicRanges, () => this.#wordDirection);
|
|
82
82
|
}
|
|
83
83
|
handleInput(data, next) {
|
|
84
|
+
this.#pasteState.preview = undefined;
|
|
84
85
|
if (this.options.pasteEnabled === false ? discardTerminalPaste(this, data, value => this.handleInput(value, next)) : this.#handleTerminalPaste(data, next))
|
|
85
86
|
return;
|
|
86
87
|
if (this.keybindings.matches(data, "owned.editor.selectAll")) {
|
|
@@ -195,7 +196,26 @@ class PromptSelectionInterceptor {
|
|
|
195
196
|
this.#redoStack = [];
|
|
196
197
|
}
|
|
197
198
|
render(width, next) {
|
|
199
|
+
const preview = this.#pasteState.preview;
|
|
198
200
|
const state = editorState(this.editor);
|
|
201
|
+
if (preview !== undefined && this.editor.getText() === preview.reservedText
|
|
202
|
+
&& this.#selectionRevision === preview.revision
|
|
203
|
+
&& samePosition(this.#cursor(), preview.reservedCursor)
|
|
204
|
+
&& this.#selection === undefined) {
|
|
205
|
+
// Invariant: painting does not commit the reserved edit.
|
|
206
|
+
const original = { lines: [...state.lines], cursor: this.#cursor(), selection: this.#selection };
|
|
207
|
+
this.#pasteState.preview = undefined;
|
|
208
|
+
try {
|
|
209
|
+
this.#setRenderState(preview.lines, preview.cursor);
|
|
210
|
+
this.#selection = preview.selection;
|
|
211
|
+
return this.render(width, next);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
this.#setRenderState(original.lines, original.cursor);
|
|
215
|
+
this.#selection = original.selection;
|
|
216
|
+
this.#pasteState.preview = preview;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
199
219
|
const hidden = state.lines.map(line => this.options.hiddenRanges?.(line) ?? []);
|
|
200
220
|
if (!hidden.some(ranges => ranges.length > 0))
|
|
201
221
|
return this.#renderVisible(width, next);
|
|
@@ -277,6 +297,7 @@ class PromptSelectionInterceptor {
|
|
|
277
297
|
return rows.map((row, index) => this.options.decorateRow(row, width, index));
|
|
278
298
|
}
|
|
279
299
|
reset() {
|
|
300
|
+
this.#pasteState.preview = undefined;
|
|
280
301
|
this.#selection = undefined;
|
|
281
302
|
this.#pointerSelecting = false;
|
|
282
303
|
this.#lastClick = undefined;
|
|
@@ -285,7 +306,8 @@ class PromptSelectionInterceptor {
|
|
|
285
306
|
this.#selectionRevision += 1;
|
|
286
307
|
}
|
|
287
308
|
cancelPendingPastes() {
|
|
288
|
-
this.#
|
|
309
|
+
this.#pasteState.generation += 1;
|
|
310
|
+
this.#pasteState.preview = undefined;
|
|
289
311
|
this.#terminalPaste = undefined;
|
|
290
312
|
DISABLED_TERMINAL_PASTES.delete(this);
|
|
291
313
|
}
|
|
@@ -309,6 +331,14 @@ class PromptSelectionInterceptor {
|
|
|
309
331
|
handlePointer(event) {
|
|
310
332
|
if (event.button !== 0)
|
|
311
333
|
return false;
|
|
334
|
+
if (this.#pasteState.preview !== undefined) {
|
|
335
|
+
this.#pasteState.preview = undefined;
|
|
336
|
+
// Invariant: preview coordinates cannot address a hidden marker.
|
|
337
|
+
this.#geometry = undefined;
|
|
338
|
+
this.#pointerSelecting = event.kind === "press";
|
|
339
|
+
this.#requestRender();
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
312
342
|
if (event.kind === "press")
|
|
313
343
|
return this.#pointerPress(event.column, event.row);
|
|
314
344
|
if (event.kind === "motion") {
|
|
@@ -539,17 +569,30 @@ class PromptSelectionInterceptor {
|
|
|
539
569
|
const begin = source === undefined ? this.options.beginClipboardPaste
|
|
540
570
|
: this.options.beginTextPaste === undefined ? undefined : () => this.options.beginTextPaste(source);
|
|
541
571
|
if (begin !== undefined) {
|
|
542
|
-
const generation = this.#
|
|
572
|
+
const generation = this.#pasteState.generation;
|
|
543
573
|
const restore = this.#orderedSelection() === undefined ? "" : this.#selectedText();
|
|
544
574
|
const paste = begin();
|
|
545
575
|
this.#redoStack = [];
|
|
546
|
-
|
|
576
|
+
const selected = this.#orderedSelection();
|
|
577
|
+
if (selected !== undefined) {
|
|
578
|
+
const lines = [...editorState(this.editor).lines];
|
|
579
|
+
const cursor = this.#cursor();
|
|
580
|
+
const selection = this.#selection;
|
|
581
|
+
const current = this.editor.getText();
|
|
582
|
+
const from = positionOffset(lines, selected.start);
|
|
583
|
+
const to = positionOffset(lines, selected.end);
|
|
584
|
+
const reservedText = current.slice(0, from) + normalizeInsertedText(paste.marker) + current.slice(to);
|
|
547
585
|
this.#replaceSelection(paste.marker);
|
|
586
|
+
this.#pasteState.preview = { marker: paste.marker, reservedText, lines, cursor, selection,
|
|
587
|
+
reservedCursor: this.#cursor(), revision: this.#selectionRevision };
|
|
588
|
+
}
|
|
548
589
|
else
|
|
549
590
|
this.editor.insertTextAtCursor(paste.marker);
|
|
550
591
|
this.#requestRender();
|
|
551
592
|
const replace = (text) => {
|
|
552
|
-
if (
|
|
593
|
+
if (this.#pasteState.preview?.marker === paste.marker)
|
|
594
|
+
this.#pasteState.preview = undefined;
|
|
595
|
+
if (generation === this.#pasteState.generation)
|
|
553
596
|
this.#replacePasteMarker(paste.marker, paste.isCurrent?.() === false ? restore : text || restore);
|
|
554
597
|
paste.complete?.();
|
|
555
598
|
};
|
|
@@ -443,7 +443,7 @@ export class PiWorkflowRunner {
|
|
|
443
443
|
case "copy": {
|
|
444
444
|
const text = requireCapability(session.getLastAssistantText, "getLastAssistantText").call(session);
|
|
445
445
|
if (typeof text !== "string" || text.length === 0)
|
|
446
|
-
return workflowResult(request.command, "
|
|
446
|
+
return workflowResult(request.command, "completed", "", undefined, "silent");
|
|
447
447
|
const acknowledged = await this.copyWorkflowText(text);
|
|
448
448
|
return workflowResult(request.command, "completed", acknowledged ? "Copied last agent message to clipboard" : "Submitted last agent message to clipboard");
|
|
449
449
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"architecture": "arm64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-10-
|
|
10
|
+
"builtAt": "2026-10-29T14:00:11.225Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
13
|
"sha256": "a87eaaffcd986b624c8b044b756446cc1bdaf4334a1775b97820542fe34f9fb7",
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"architecture": "arm64",
|
|
10
10
|
"target": "aarch64-apple-darwin",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-10-
|
|
12
|
+
"builtAt": "2026-10-29T14:03:13.305Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "5f1a93ca9a050c2769a4e322f4d765b00f2ccc9451bcde7ac978b573ee544bef",
|
|
16
|
+
"size": 6019328
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-10-
|
|
10
|
+
"builtAt": "2026-10-29T14:00:18.680Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
13
|
"sha256": "783a65f0de287a1c02cd5429dbe97bc8633f4cc8a7b92e3e961180b9bdcff814",
|