@timurproko/a1 0.2.7-dev.746 → 0.2.7-dev.767
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/bin/tab-launch-context.d.ts +6 -1
- package/bin/tab-launch-context.js +11 -2
- package/bin/ui.js +7 -3
- package/dist/app/session-shell/session-shell-root.d.ts +23 -0
- package/dist/app/session-shell/session-shell.js +112 -6
- package/dist/composition/owned-ui.d.ts +4 -1
- package/dist/composition/owned-ui.js +54 -8
- package/dist/contracts/owned-ui/session-backend.d.ts +1 -0
- package/dist/features/owned-ui/settings-app.js +8 -35
- package/dist/foundation/release/bootstrap.d.ts +1 -1
- package/dist/foundation/release/bootstrap.js +15 -4
- package/dist/foundation/release/dependency-certification.js +54 -19
- package/dist/foundation/release/progress-palette.generated.d.ts +2 -2
- package/dist/foundation/release/progress-palette.generated.js +2 -2
- package/dist/foundation/resident-tabs/bridge-client.d.ts +7 -2
- package/dist/foundation/resident-tabs/bridge-client.js +58 -16
- package/dist/foundation/resident-tabs/draft-checkpoint.d.ts +27 -0
- package/dist/foundation/resident-tabs/draft-checkpoint.js +145 -0
- package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +2 -0
- package/dist/foundation/resident-tabs/ensure-terminal-host.js +7 -1
- package/dist/foundation/resident-tabs/fault-injection.d.ts +8 -0
- package/dist/foundation/resident-tabs/fault-injection.js +26 -0
- package/dist/foundation/resident-tabs/index.d.ts +2 -1
- package/dist/foundation/resident-tabs/index.js +1 -1
- package/dist/foundation/resident-tabs/prompt-journal.d.ts +65 -0
- package/dist/foundation/resident-tabs/prompt-journal.js +214 -0
- package/dist/foundation/resident-tabs/recovery.d.ts +26 -0
- package/dist/foundation/resident-tabs/recovery.js +205 -0
- package/dist/foundation/resident-tabs/session-sync.d.ts +14 -0
- package/dist/foundation/resident-tabs/session-sync.js +37 -0
- package/dist/foundation/resident-tabs/settings.d.ts +9 -2
- package/dist/foundation/resident-tabs/settings.js +55 -7
- package/dist/foundation/session-writer-lock/index.d.ts +27 -0
- package/dist/foundation/session-writer-lock/index.js +197 -0
- package/dist/foundation/startup/startup-descriptor.js +2 -2
- package/dist/integrations/pi/components/resources/builtin-themes.d.ts +4 -4
- package/dist/integrations/pi/components/resources/builtin-themes.js +4 -4
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +17 -4
- package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
- package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/session-footer.js +1 -1
- package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
- package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
- package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +2 -2
- package/dist/integrations/pi/components/upstream/components/status-indicator.js +3 -3
- package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +0 -2
- package/dist/integrations/pi/components/upstream/components/tool-execution.js +41 -20
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +2 -2
- package/dist/integrations/pi/components/upstream/history/editor-core.js +5 -5
- package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
- package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
- package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
- package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
- package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
- package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/theme/theme.d.ts +3 -0
- package/dist/integrations/pi/components/upstream/theme/theme.js +24 -1
- package/dist/integrations/pi/engine/adapter.js +1 -0
- package/dist/integrations/pi/engine/pi-settings-metadata.json +2 -2
- package/dist/integrations/pi/engine/resources/changelog.json +4 -4
- package/dist/integrations/pi/engine/runtime-integration.js +102 -29
- package/dist/integrations/pi/engine/session-runtime.d.ts +0 -6
- package/dist/integrations/pi/engine/session-runtime.js +8 -3
- package/dist/integrations/pi/engine/session-selection.d.ts +3 -0
- package/dist/integrations/pi/engine/session-selection.js +45 -9
- package/dist/integrations/pi/engine/workflow-contexts.js +10 -1
- package/dist/integrations/pi/startup-public.js +152 -222
- package/dist/integrations/pi/startup-public.manifest.json +266 -177
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
- 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/runtime-payload-inventory.json +5349 -6035
- package/dist/ui/components/line-input.d.ts +1 -1
- package/dist/ui/components/prompt-input.d.ts +7 -5
- package/dist/ui/components/prompt-input.js +7 -7
- package/dist/ui/settings/declarations.d.ts +38 -2
- package/dist/ui/settings/declarations.js +29 -2
- package/dist/ui/settings/index.d.ts +4 -2
- package/dist/ui/settings/index.js +2 -1
- package/dist/ui/settings/manager.d.ts +9 -3
- package/dist/ui/settings/manager.js +81 -29
- package/dist/ui/settings/migrations.d.ts +6 -0
- package/dist/ui/settings/migrations.js +25 -0
- package/dist/ui/settings/profile-document.d.ts +26 -0
- package/dist/ui/settings/profile-document.js +164 -0
- package/dist/ui/settings/sections.js +4 -0
- package/docs/architecture/resident-tabs-requirements.md +1 -234
- package/docs/architecture/resident-tabs.md +30 -10
- package/docs/openspec-archive-automation.md +7 -7
- package/package.json +6 -4
|
@@ -15,4 +15,9 @@ export function assignFreshSessionRuntimeId(
|
|
|
15
15
|
export function consumeTabLaunchContext(
|
|
16
16
|
arguments_: readonly string[],
|
|
17
17
|
environment: Record<string, string | undefined>,
|
|
18
|
-
): {
|
|
18
|
+
): {
|
|
19
|
+
readonly uiArguments: string[];
|
|
20
|
+
readonly tabCredentials: TabLaunchCredentials | undefined;
|
|
21
|
+
readonly tabDataDir?: string;
|
|
22
|
+
readonly tabIncarnation?: number;
|
|
23
|
+
};
|
|
@@ -28,9 +28,18 @@ export function consumeTabLaunchContext(arguments_, environment) {
|
|
|
28
28
|
const tabArgument = uiArguments.indexOf("--tab");
|
|
29
29
|
const tabMode = tabArgument !== -1;
|
|
30
30
|
if (tabMode) uiArguments.splice(tabArgument, 1);
|
|
31
|
+
const dataArgument = uiArguments.indexOf("--tab-data-dir");
|
|
32
|
+
const tabDataDir = dataArgument === -1 ? undefined : uiArguments[dataArgument + 1];
|
|
33
|
+
if (dataArgument !== -1) uiArguments.splice(dataArgument, 2);
|
|
34
|
+
const incarnationArgument = uiArguments.indexOf("--tab-incarnation");
|
|
35
|
+
const tabIncarnation = incarnationArgument === -1 ? undefined : Number(uiArguments[incarnationArgument + 1]);
|
|
36
|
+
if (incarnationArgument !== -1) uiArguments.splice(incarnationArgument, 2);
|
|
31
37
|
const [tabId, token, endpoint] = TAB_ENVIRONMENT_NAMES.map(name => environment[name]);
|
|
32
38
|
for (const name of TAB_ENVIRONMENT_NAMES) delete environment[name];
|
|
33
39
|
if (!tabMode) return { uiArguments, tabCredentials: undefined };
|
|
34
|
-
if (!tabId || !token || !endpoint
|
|
35
|
-
|
|
40
|
+
if (!tabId || !token || !endpoint || !tabDataDir || tabDataDir.includes("\0")
|
|
41
|
+
|| typeof tabIncarnation !== "number" || !Number.isSafeInteger(tabIncarnation) || tabIncarnation < 1) {
|
|
42
|
+
throw new Error("A1 tab mode requires bridge credentials, recovery directory, and incarnation");
|
|
43
|
+
}
|
|
44
|
+
return { uiArguments, tabCredentials: { tabId, token, endpoint }, tabDataDir, tabIncarnation };
|
|
36
45
|
}
|
package/bin/ui.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// Security: this helper has no A1, Pi, tool, or extension dependency and erases credentials before those modules load.
|
|
4
4
|
const { assignFreshSessionRuntimeId, consumeTabLaunchContext, formatResidentFallbackNotice } = await import("./tab-launch-context.js");
|
|
5
|
-
const { uiArguments, tabCredentials } = consumeTabLaunchContext(process.argv.slice(2), process.env);
|
|
5
|
+
const { uiArguments, tabCredentials, tabDataDir, tabIncarnation } = consumeTabLaunchContext(process.argv.slice(2), process.env);
|
|
6
6
|
|
|
7
7
|
// Invariant: one terminal module identity per process, decided before any A1 or Pi module loads.
|
|
8
8
|
const { installPinnedPiTuiResolver } = await import("./module-resolver.js");
|
|
@@ -22,11 +22,13 @@ if (profile === "a1") {
|
|
|
22
22
|
}
|
|
23
23
|
const { installFatalExit } = await import("../dist/foundation/terminal-cleanup/fatal-exit.js");
|
|
24
24
|
const { resolveProductPaths } = await import("../dist/foundation/lifecycle/paths.js");
|
|
25
|
-
const { join } = await import("node:path");
|
|
25
|
+
const { dirname, join } = await import("node:path");
|
|
26
26
|
/** @type {{ dispose(): void | Promise<unknown> } | undefined} */
|
|
27
27
|
let runningApplication;
|
|
28
28
|
const fatal = profile === "a1" ? installFatalExit({
|
|
29
|
-
directory:
|
|
29
|
+
directory: tabDataDir === undefined
|
|
30
|
+
? join(resolveProductPaths().runtimeDir, "crashes")
|
|
31
|
+
: join(dirname(tabDataDir), "diagnostics"),
|
|
30
32
|
...(launchContext.releaseId === undefined ? {} : { releaseId: launchContext.releaseId }),
|
|
31
33
|
dispose: () => runningApplication?.dispose(),
|
|
32
34
|
}) : undefined;
|
|
@@ -59,6 +61,8 @@ const sessionSelection = parseSessionSelection(uiArguments);
|
|
|
59
61
|
Promise.resolve().then(async () => {
|
|
60
62
|
const tab = tabCredentials === undefined ? undefined : {
|
|
61
63
|
bridge: new (await import("../dist/foundation/resident-tabs/bridge-client.js")).TabBridgeClient({ credentials: tabCredentials }),
|
|
64
|
+
dataDir: /** @type {string} */ (tabDataDir),
|
|
65
|
+
incarnation: /** @type {number} */ (tabIncarnation),
|
|
62
66
|
};
|
|
63
67
|
if (sessionSelection && profile !== "a1") throw new Error("session selection requires the normal A1 profile");
|
|
64
68
|
return runSelectedInteractiveRuntime(profile, {
|
|
@@ -46,6 +46,29 @@ export interface OwnedUiShellEngineOptions {
|
|
|
46
46
|
/** Resident tab quit routes detach the outer client without stopping this engine session. */
|
|
47
47
|
readonly residentTab?: {
|
|
48
48
|
requestDetach(): Promise<boolean>;
|
|
49
|
+
requestRestart?(): Promise<boolean>;
|
|
50
|
+
reportStatus?(status: "working" | "idle" | "stalled"): void;
|
|
51
|
+
readonly stallNoticeSeconds?: number;
|
|
52
|
+
admit?(input: {
|
|
53
|
+
readonly sessionId: string;
|
|
54
|
+
readonly sessionFile: string | null;
|
|
55
|
+
readonly kind: "prompt" | "steer" | "follow-up";
|
|
56
|
+
readonly text: string;
|
|
57
|
+
readonly images?: readonly unknown[];
|
|
58
|
+
}): Promise<{
|
|
59
|
+
readonly id: string;
|
|
60
|
+
}>;
|
|
61
|
+
dispatched?(): void;
|
|
62
|
+
correlated?(submissionId: string, entryId: string): Promise<void>;
|
|
63
|
+
checkpoint?(text: string): void;
|
|
64
|
+
gracefulStop?(sessionFile: string | null): Promise<void>;
|
|
65
|
+
readonly recovered?: {
|
|
66
|
+
readonly prompts: readonly {
|
|
67
|
+
readonly id: string;
|
|
68
|
+
readonly text: string;
|
|
69
|
+
}[];
|
|
70
|
+
readonly draft: string | null;
|
|
71
|
+
};
|
|
49
72
|
readonly unavailableNotice?: string;
|
|
50
73
|
};
|
|
51
74
|
}
|
|
@@ -54,6 +54,9 @@ export class OwnedUiSessionPresenter {
|
|
|
54
54
|
#routeHost;
|
|
55
55
|
#startupRoute;
|
|
56
56
|
#startupRouteTimer;
|
|
57
|
+
#residentProgressTimer;
|
|
58
|
+
#residentWorking = false;
|
|
59
|
+
#residentStalled = false;
|
|
57
60
|
#dialogHandle;
|
|
58
61
|
#sequence = 0;
|
|
59
62
|
#editorRevision = 0;
|
|
@@ -204,6 +207,7 @@ export class OwnedUiSessionPresenter {
|
|
|
204
207
|
onDequeue: () => this.restoreQueuedInput(),
|
|
205
208
|
onEditorChange: text => {
|
|
206
209
|
this.#editorRevision++;
|
|
210
|
+
this.#residentTabOptions?.checkpoint?.(text);
|
|
207
211
|
promptSuggestionController?.abortPending();
|
|
208
212
|
if (text.length === 0) {
|
|
209
213
|
// Concurrency: the editor synchronizes deletion-owned autocomplete after onChange.
|
|
@@ -265,6 +269,7 @@ export class OwnedUiSessionPresenter {
|
|
|
265
269
|
// Invariant: the presenter owns its root (theme subscription and helper clients) and disposes it once the
|
|
266
270
|
// terminal no longer renders it.
|
|
267
271
|
constructed.push(() => this.root.dispose());
|
|
272
|
+
this.#restoreResidentInput();
|
|
268
273
|
const initialPiSettings = this.backend.settings.snapshot();
|
|
269
274
|
this.#programStatus = new ProgramStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
|
|
270
275
|
this.#reloadPresentation = reloadPresentation;
|
|
@@ -476,6 +481,7 @@ export class OwnedUiSessionPresenter {
|
|
|
476
481
|
this.#settleStoppedLifecycle();
|
|
477
482
|
return;
|
|
478
483
|
}
|
|
484
|
+
this.#observeResidentProgress(event);
|
|
479
485
|
// Performance: a streamed chunk names one block, and touching only that block is what keeps the
|
|
480
486
|
// cost of a chunk the same in a long session as in a new one. Everything else
|
|
481
487
|
// resynchronizes the view, which is cheap next to re-reading the transcript.
|
|
@@ -781,15 +787,38 @@ export class OwnedUiSessionPresenter {
|
|
|
781
787
|
const type = this.view().lifecycle === "busy" ? "steer" : "prompt";
|
|
782
788
|
this.#rememberInput(this.root.omitUnsentPromptImages(displayInput).trim(), type);
|
|
783
789
|
this.root.resumeViewportFollowing();
|
|
784
|
-
const
|
|
790
|
+
const command = {
|
|
791
|
+
type,
|
|
792
|
+
correlationId: this.#correlation(type),
|
|
793
|
+
sessionId: this.backend.identity.sessionId,
|
|
794
|
+
text: input,
|
|
795
|
+
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
796
|
+
};
|
|
797
|
+
let submission;
|
|
785
798
|
try {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
799
|
+
submission = await this.#residentTabOptions?.admit?.({
|
|
800
|
+
sessionId: command.sessionId,
|
|
801
|
+
sessionFile: this.#currentSessionFile(),
|
|
802
|
+
kind: type,
|
|
790
803
|
text: input,
|
|
791
804
|
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
792
|
-
}
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
if (this.root.editor.getText().length === 0)
|
|
809
|
+
this.root.editor.setText(displayInput);
|
|
810
|
+
const message = "Prompt was not sent because its recovery journal could not be synchronized.";
|
|
811
|
+
this.#reportSubmissionError(undefined, message);
|
|
812
|
+
return rejected(message);
|
|
813
|
+
}
|
|
814
|
+
const finishSending = prepared.images.length === 0 ? () => { } : this.root.beginImageSubmissionStatus();
|
|
815
|
+
try {
|
|
816
|
+
const result = await this.#execute(command, displayInput);
|
|
817
|
+
if (submission !== undefined && result.outcome !== "rejected") {
|
|
818
|
+
this.#residentTabOptions?.dispatched?.();
|
|
819
|
+
await this.#residentTabOptions?.correlated?.(submission.id, command.correlationId).catch(() => undefined);
|
|
820
|
+
}
|
|
821
|
+
return result;
|
|
793
822
|
}
|
|
794
823
|
finally {
|
|
795
824
|
finishSending();
|
|
@@ -1183,9 +1212,75 @@ export class OwnedUiSessionPresenter {
|
|
|
1183
1212
|
this.#resolveStoppedLifecycle();
|
|
1184
1213
|
}
|
|
1185
1214
|
}
|
|
1215
|
+
#restoreResidentInput() {
|
|
1216
|
+
const recovered = this.#residentTabOptions?.recovered;
|
|
1217
|
+
if (recovered === undefined)
|
|
1218
|
+
return;
|
|
1219
|
+
const prompts = recovered.prompts.map(prompt => prompt.text).filter(Boolean);
|
|
1220
|
+
if (prompts.length > 0) {
|
|
1221
|
+
this.root.editor.setText(prompts[0]);
|
|
1222
|
+
if (recovered.draft)
|
|
1223
|
+
this.root.editor.addToHistory(recovered.draft);
|
|
1224
|
+
for (const additional of prompts.slice(1))
|
|
1225
|
+
this.root.editor.addToHistory(additional);
|
|
1226
|
+
this.root.appendWorkflowStatus(recovered.draft
|
|
1227
|
+
? `Recovered an unfinished prompt; the prior draft and ${Math.max(0, prompts.length - 1)} additional prompt${prompts.length === 2 ? "" : "s"} are in editor history.`
|
|
1228
|
+
: `Recovered an unfinished prompt${prompts.length > 1 ? `; ${prompts.length - 1} additional prompts are in editor history` : ""}.`);
|
|
1229
|
+
}
|
|
1230
|
+
else if (recovered.draft) {
|
|
1231
|
+
this.root.editor.setText(recovered.draft);
|
|
1232
|
+
this.root.appendWorkflowStatus("Recovered the editor draft from the previous tab process.");
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
#currentSessionFile() {
|
|
1236
|
+
const direct = this.backend.identity.currentSessionFile?.();
|
|
1237
|
+
if (direct !== undefined && direct !== null)
|
|
1238
|
+
return direct;
|
|
1239
|
+
const resume = this.backend.identity.currentSessionResumeMetadata();
|
|
1240
|
+
return resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`;
|
|
1241
|
+
}
|
|
1186
1242
|
#residentTab() {
|
|
1187
1243
|
return this.#residentTabOptions;
|
|
1188
1244
|
}
|
|
1245
|
+
#observeResidentProgress(event) {
|
|
1246
|
+
const resident = this.#residentTabOptions;
|
|
1247
|
+
const seconds = resident?.stallNoticeSeconds;
|
|
1248
|
+
if (resident === undefined || seconds === undefined)
|
|
1249
|
+
return;
|
|
1250
|
+
if (event.type === "session-lifecycle") {
|
|
1251
|
+
this.#residentWorking = event.lifecycle === "busy";
|
|
1252
|
+
if (!this.#residentWorking) {
|
|
1253
|
+
if (this.#residentProgressTimer !== undefined)
|
|
1254
|
+
clearTimeout(this.#residentProgressTimer);
|
|
1255
|
+
this.#residentProgressTimer = undefined;
|
|
1256
|
+
this.#residentStalled = false;
|
|
1257
|
+
resident.reportStatus?.("idle");
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
else if (!this.#residentWorking || ![
|
|
1262
|
+
"transcript-block", "session-view", "assistant-message-completed", "agent-run-started",
|
|
1263
|
+
].includes(event.type)) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (this.#residentStalled) {
|
|
1267
|
+
this.#residentStalled = false;
|
|
1268
|
+
resident.reportStatus?.("working");
|
|
1269
|
+
}
|
|
1270
|
+
if (this.#residentProgressTimer !== undefined)
|
|
1271
|
+
clearTimeout(this.#residentProgressTimer);
|
|
1272
|
+
this.#residentProgressTimer = setTimeout(() => {
|
|
1273
|
+
this.#residentProgressTimer = undefined;
|
|
1274
|
+
if (this.#disposed || !this.#residentWorking)
|
|
1275
|
+
return;
|
|
1276
|
+
this.#residentStalled = true;
|
|
1277
|
+
resident.reportStatus?.("stalled");
|
|
1278
|
+
const minutes = Math.max(1, Math.round(seconds / 60));
|
|
1279
|
+
this.root.appendWorkflowStatus(`No progress for ${minutes} min — Esc interrupts · /restart-tab restarts`);
|
|
1280
|
+
this.#terminal.requestRender();
|
|
1281
|
+
}, seconds * 1_000);
|
|
1282
|
+
this.#residentProgressTimer.unref?.();
|
|
1283
|
+
}
|
|
1189
1284
|
async runWorkflow(request) {
|
|
1190
1285
|
if (request.command === "quit")
|
|
1191
1286
|
return this.shutdown();
|
|
@@ -1443,6 +1538,9 @@ export class OwnedUiSessionPresenter {
|
|
|
1443
1538
|
if (this.#startupRouteTimer !== undefined)
|
|
1444
1539
|
clearTimeout(this.#startupRouteTimer);
|
|
1445
1540
|
this.#startupRouteTimer = undefined;
|
|
1541
|
+
if (this.#residentProgressTimer !== undefined)
|
|
1542
|
+
clearTimeout(this.#residentProgressTimer);
|
|
1543
|
+
this.#residentProgressTimer = undefined;
|
|
1446
1544
|
});
|
|
1447
1545
|
attempt(() => this.#promptSuggestions?.dispose());
|
|
1448
1546
|
attempt(() => this.#unsubscribePromptSuggestions());
|
|
@@ -1479,6 +1577,8 @@ export class OwnedUiSessionPresenter {
|
|
|
1479
1577
|
}
|
|
1480
1578
|
await historyCleanup.catch(() => false); // Security: background durability outcomes never enter terminal output.
|
|
1481
1579
|
await boundedCleanup(() => pasteCleanup).catch(error => settled.push(error));
|
|
1580
|
+
await boundedCleanup(() => this.#residentTabOptions?.gracefulStop?.(this.#currentSessionFile()) ?? Promise.resolve())
|
|
1581
|
+
.catch(error => settled.push(error));
|
|
1482
1582
|
await boundedCleanup(() => this.backend.extensions.unbindExtensionUi()).catch(error => settled.push(error));
|
|
1483
1583
|
return settled;
|
|
1484
1584
|
};
|
|
@@ -1668,6 +1768,12 @@ export class OwnedUiSessionPresenter {
|
|
|
1668
1768
|
const argument = separator < 0 ? "" : body.slice(separator + 1).trimStart();
|
|
1669
1769
|
if (this.#routeHost?.claims(name))
|
|
1670
1770
|
return this.#openOwnedRoute(name);
|
|
1771
|
+
if (name === "restart-tab" && this.#residentTabOptions?.requestRestart !== undefined) {
|
|
1772
|
+
const accepted = await this.#residentTabOptions.requestRestart().catch(() => false);
|
|
1773
|
+
this.root.appendWorkflowStatus(accepted ? "Restarting resident tab…" : "Resident tab restart is unavailable.");
|
|
1774
|
+
this.#terminal.requestRender();
|
|
1775
|
+
return accepted ? { outcome: "completed", diagnostic: null } : rejected("resident tab restart is unavailable");
|
|
1776
|
+
}
|
|
1671
1777
|
if (this.#isWorkflowRoute(name))
|
|
1672
1778
|
return this.runWorkflow({ command: name, argument });
|
|
1673
1779
|
if (this.#skillsCollapsed()) {
|
|
@@ -11,6 +11,7 @@ import type { AgentSettingsPort } from "../contracts/agent-engine/index.js";
|
|
|
11
11
|
import type { OwnedUiPromptSuggestionGeneratorPort, UiAccentColor } from "../contracts/owned-ui/index.js";
|
|
12
12
|
import type { ReleaseNoteCatalog } from "../features/owned-ui/release-notes.js";
|
|
13
13
|
import type { TabBridgeClient } from "../foundation/resident-tabs/bridge-client.js";
|
|
14
|
+
import { ResidentTabRecovery } from "../foundation/resident-tabs/recovery.js";
|
|
14
15
|
/**
|
|
15
16
|
* The session backend composition wires: the owned-UI port, the prompt-suggestion generator, and
|
|
16
17
|
* the agent settings port. `settingsPort` stays outside the owned-UI contract because its type
|
|
@@ -59,6 +60,8 @@ export interface OwnedUiCompositionOptions {
|
|
|
59
60
|
/** Private credentials were consumed by the entry before composition or extensions loaded. */
|
|
60
61
|
readonly tab?: {
|
|
61
62
|
readonly bridge: TabBridgeClient;
|
|
63
|
+
readonly dataDir: string;
|
|
64
|
+
readonly incarnation: number;
|
|
62
65
|
};
|
|
63
66
|
/** One prompt-adjacent notice explaining why an opted-in resident launch fell back. */
|
|
64
67
|
readonly residentFallbackNotice?: string;
|
|
@@ -79,4 +82,4 @@ export declare function residentStartupPresentation(resident: boolean, fallbackN
|
|
|
79
82
|
};
|
|
80
83
|
export declare function composeOwnedUiApplication(options?: OwnedUiCompositionOptions): Promise<OwnedUiApplicationPort>;
|
|
81
84
|
export declare function composeOwnedUi(options?: OwnedUiCompositionOptions): Promise<OwnedUiComposition>;
|
|
82
|
-
export declare function bindResidentTabBridge(tabBridge: TabBridgeClient, backend: Pick<ComposedSessionBackend, "identity" | "session">): Promise<() => void>;
|
|
85
|
+
export declare function bindResidentTabBridge(tabBridge: TabBridgeClient, backend: Pick<ComposedSessionBackend, "identity" | "session">, recovery?: Pick<ResidentTabRecovery, "settled"> | null): Promise<() => void>;
|
|
@@ -4,7 +4,7 @@ import { PRODUCT_IDENTITY } from "../product-identity.js";
|
|
|
4
4
|
import { PromptHistoryService } from "../features/prompt-history/service.js";
|
|
5
5
|
import { PromptImageSidecar } from "../features/prompt-history/image-sidecar.js";
|
|
6
6
|
import { resolvePromptHistoryPath } from "../features/prompt-history/paths.js";
|
|
7
|
-
import { resolvePromptHistoryDataDir } from "../features/launch/profile-paths.js";
|
|
7
|
+
import { resolveLaunchProfilePaths, resolvePromptHistoryDataDir } from "../features/launch/profile-paths.js";
|
|
8
8
|
import { resolveProductPaths } from "../foundation/lifecycle/paths.js";
|
|
9
9
|
import { applyConfiguredPiTheme, getAvailablePiThemes, setPiAccentColor, setPiPackageBorderProjectionEnabled } from "../integrations/pi/components/upstream/theme/theme.js";
|
|
10
10
|
import { createPiEngineHost } from "../integrations/pi/engine/host.js";
|
|
@@ -18,6 +18,7 @@ import { createPiTerminalBridge } from "../integrations/pi/tui-runtime/presentat
|
|
|
18
18
|
import { createOwnedRouteHost } from "./settings-route-host.js";
|
|
19
19
|
import { renderPiShellChangelogLines } from "../integrations/pi/components/shell-presenters-info.js";
|
|
20
20
|
import { nativeHyperlinkStyle } from "../ui/components/spans.js";
|
|
21
|
+
import { ResidentTabRecovery } from "../foundation/resident-tabs/recovery.js";
|
|
21
22
|
const SESSION_RUNTIME_REFRESH_MS = 30_000;
|
|
22
23
|
export function residentStartupPresentation(resident, fallbackNotice) {
|
|
23
24
|
if (resident)
|
|
@@ -87,13 +88,16 @@ export async function composeOwnedUi(options = {}) {
|
|
|
87
88
|
sessionRuntimeRefreshTimer.unref();
|
|
88
89
|
};
|
|
89
90
|
const productPaths = resolveProductPaths();
|
|
91
|
+
const profilePaths = resolveLaunchProfilePaths();
|
|
90
92
|
// Rationale: the manager reads the engine's settings port lazily, after the session exists.
|
|
91
93
|
let backend;
|
|
92
94
|
const settings = options.profileId === undefined
|
|
93
95
|
? null
|
|
94
96
|
: new OwnedSettingsManager({
|
|
95
|
-
|
|
97
|
+
profileDir: profilePaths.agentProfile,
|
|
96
98
|
profileId: options.profileId,
|
|
99
|
+
namespace: PRODUCT_IDENTITY.commandName,
|
|
100
|
+
legacyConfigDir: productPaths.configDir,
|
|
97
101
|
agentProvider: () => backend.settingsPort(),
|
|
98
102
|
hiddenAgentSettingIds: ["fullscreenWheelScrollLines"],
|
|
99
103
|
agentSettingLabelOverrides: { fullscreenCopyOnSelect: "Copy on select" },
|
|
@@ -126,7 +130,17 @@ export async function composeOwnedUi(options = {}) {
|
|
|
126
130
|
throw error;
|
|
127
131
|
}
|
|
128
132
|
const tabBridge = options.tab?.bridge ?? null;
|
|
129
|
-
|
|
133
|
+
let recoveryDegraded = false;
|
|
134
|
+
let presentRecoveryDegraded = (_degraded) => { };
|
|
135
|
+
const tabRecovery = options.tab === undefined ? null : new ResidentTabRecovery(options.tab.dataDir, {
|
|
136
|
+
onDegraded: degraded => {
|
|
137
|
+
recoveryDegraded = degraded;
|
|
138
|
+
options.tab.bridge.degraded(degraded);
|
|
139
|
+
presentRecoveryDegraded(degraded);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const recoveredTabInput = await tabRecovery?.initialize(currentBackendSessionFile(backend), backend.identity.sessionId);
|
|
143
|
+
const unsubscribeTabBridge = tabBridge === null ? () => { } : await bindResidentTabBridge(tabBridge, backend, tabRecovery);
|
|
130
144
|
let releaseNotes = null;
|
|
131
145
|
let releaseNotesFailure;
|
|
132
146
|
if (ownedSurfaces) {
|
|
@@ -235,7 +249,18 @@ export async function composeOwnedUi(options = {}) {
|
|
|
235
249
|
},
|
|
236
250
|
}),
|
|
237
251
|
...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
|
|
238
|
-
...(tabBridge === null ? {} : { residentTab: {
|
|
252
|
+
...(tabBridge === null || tabRecovery === null ? {} : { residentTab: {
|
|
253
|
+
requestDetach: () => tabBridge.requestDetach(),
|
|
254
|
+
requestRestart: () => tabBridge.requestRestart(),
|
|
255
|
+
reportStatus: status => tabBridge.status(status),
|
|
256
|
+
stallNoticeSeconds: settings?.value("tabsStallNoticeSeconds") ?? 300,
|
|
257
|
+
admit: input => tabRecovery.admit({ ...input, incarnation: options.tab.incarnation }),
|
|
258
|
+
dispatched: () => tabRecovery.dispatched(),
|
|
259
|
+
correlated: (submissionId, entryId) => tabRecovery.correlated(submissionId, entryId),
|
|
260
|
+
checkpoint: text => tabRecovery.checkpoint(text),
|
|
261
|
+
gracefulStop: sessionFile => tabRecovery.gracefulStop(sessionFile),
|
|
262
|
+
...(recoveredTabInput === undefined ? {} : { recovered: recoveredTabInput }),
|
|
263
|
+
} }),
|
|
239
264
|
},
|
|
240
265
|
presentation: {
|
|
241
266
|
...(options.terminal === undefined ? {} : { terminal: createPiTerminalBridge(options.terminal) }),
|
|
@@ -264,6 +289,14 @@ export async function composeOwnedUi(options = {}) {
|
|
|
264
289
|
// the host and paints once attached.
|
|
265
290
|
terminalHost = createdTerminalHost = new OwnedUiTerminalHost(sessionTerminalHostOptions(presenterOptions));
|
|
266
291
|
presenter = new OwnedUiSessionPresenter(terminalHost, presenterOptions);
|
|
292
|
+
presentRecoveryDegraded = degraded => {
|
|
293
|
+
if (!degraded)
|
|
294
|
+
return;
|
|
295
|
+
presenter.root.appendWorkflowStatus("draft recovery degraded");
|
|
296
|
+
terminalHost.runtime.requestRender();
|
|
297
|
+
};
|
|
298
|
+
if (recoveryDegraded)
|
|
299
|
+
presentRecoveryDegraded(true);
|
|
267
300
|
terminalHost.attach(presenter);
|
|
268
301
|
}
|
|
269
302
|
catch (error) {
|
|
@@ -341,22 +374,35 @@ export async function composeOwnedUi(options = {}) {
|
|
|
341
374
|
};
|
|
342
375
|
return { application, settings };
|
|
343
376
|
}
|
|
344
|
-
export async function bindResidentTabBridge(tabBridge, backend) {
|
|
377
|
+
export async function bindResidentTabBridge(tabBridge, backend, recovery = null) {
|
|
345
378
|
await tabBridge.start().catch(() => false);
|
|
346
379
|
tabBridge.status("starting");
|
|
380
|
+
const currentSessionFile = () => currentBackendSessionFile(backend);
|
|
347
381
|
const announceSession = () => {
|
|
348
|
-
|
|
349
|
-
tabBridge.session(resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`, backend.session.view().status.footer?.sessionName ?? null);
|
|
382
|
+
tabBridge.session(currentSessionFile(), backend.session.view().status.footer?.sessionName ?? null);
|
|
350
383
|
};
|
|
384
|
+
let working = false;
|
|
351
385
|
announceSession();
|
|
352
386
|
return backend.session.onEvent(event => {
|
|
353
387
|
if (event.type === "session-lifecycle") {
|
|
354
|
-
|
|
388
|
+
const wasWorking = working;
|
|
389
|
+
working = event.lifecycle === "busy";
|
|
390
|
+
tabBridge.status(working ? "working" : event.lifecycle === "starting" ? "starting" : "idle");
|
|
391
|
+
if (wasWorking && !working && recovery !== null) {
|
|
392
|
+
void recovery.settled(currentSessionFile()).catch(() => tabBridge.degraded(true));
|
|
393
|
+
}
|
|
355
394
|
}
|
|
356
395
|
if (event.type === "session-view")
|
|
357
396
|
announceSession();
|
|
358
397
|
});
|
|
359
398
|
}
|
|
399
|
+
function currentBackendSessionFile(backend) {
|
|
400
|
+
const direct = backend.identity.currentSessionFile?.();
|
|
401
|
+
if (direct !== undefined && direct !== null)
|
|
402
|
+
return direct;
|
|
403
|
+
const resume = backend.identity.currentSessionResumeMetadata();
|
|
404
|
+
return resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`;
|
|
405
|
+
}
|
|
360
406
|
async function readPackageVersion() {
|
|
361
407
|
const manifest = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
|
|
362
408
|
const version = manifest?.version;
|
|
@@ -36,6 +36,7 @@ export interface OwnedUiSessionIdentityPort {
|
|
|
36
36
|
/** Actual session replacements, excluding delivery-only invalidation of callbacks. */
|
|
37
37
|
readonly sessionBindingGeneration: number;
|
|
38
38
|
readonly disposed: boolean;
|
|
39
|
+
currentSessionFile(): string | null;
|
|
39
40
|
currentSessionResumeMetadata(): OwnedUiSessionResumeMetadata | null;
|
|
40
41
|
}
|
|
41
42
|
/** The session lifecycle, its view, its event stream, and command admission. */
|
|
@@ -94,12 +94,10 @@ export class SettingsApp {
|
|
|
94
94
|
// Invariant: keyboard navigation requests visibility once; pointer scrolling then stays free.
|
|
95
95
|
#selectionNeedsReveal = true;
|
|
96
96
|
#reveal;
|
|
97
|
-
#notice = null;
|
|
98
97
|
#filter = null;
|
|
99
98
|
#scrollBeforeFilter = null;
|
|
100
99
|
#menu = null;
|
|
101
100
|
#structured = null;
|
|
102
|
-
#interruptArmed = false;
|
|
103
101
|
// Invariant: pending values remain visible until the source reflects them.
|
|
104
102
|
#pending = new Map();
|
|
105
103
|
#undoHistory = [];
|
|
@@ -138,7 +136,6 @@ export class SettingsApp {
|
|
|
138
136
|
}
|
|
139
137
|
render(rect, host) {
|
|
140
138
|
const theme = host.theme ?? PLAIN_THEME;
|
|
141
|
-
this.#interruptArmed = host.interruptArmed;
|
|
142
139
|
const rows = this.#rows();
|
|
143
140
|
const selected = indexOfKey(rows, this.#selectedKey);
|
|
144
141
|
const footer = this.#footerLines(rect.width, theme);
|
|
@@ -259,7 +256,6 @@ export class SettingsApp {
|
|
|
259
256
|
case "open-filter":
|
|
260
257
|
this.#scrollBeforeFilter = this.#scroll;
|
|
261
258
|
this.#filter = new LineInput("");
|
|
262
|
-
this.#notice = null;
|
|
263
259
|
return { consumed: true };
|
|
264
260
|
case "activate": {
|
|
265
261
|
const row = rows[selected];
|
|
@@ -385,7 +381,6 @@ export class SettingsApp {
|
|
|
385
381
|
const index = rows.findIndex(candidate => rowKey(candidate) === row.key);
|
|
386
382
|
if (index >= 0) {
|
|
387
383
|
// Rationale: the pointer acts where it points; the arrow belongs to the keyboard.
|
|
388
|
-
this.#notice = null;
|
|
389
384
|
if (this.#hoverRegion === "minus")
|
|
390
385
|
this.#cycle(rows, index, -1);
|
|
391
386
|
else if (this.#hoverRegion === "plus")
|
|
@@ -458,10 +453,8 @@ export class SettingsApp {
|
|
|
458
453
|
// and pointing at it is not a request for anything else.
|
|
459
454
|
if (typeof shown === "number")
|
|
460
455
|
return;
|
|
461
|
-
if (!entry.editable || entry.choices === null || entry.choices.length === 0)
|
|
462
|
-
this.#notice = `${labelOf(entry)} cannot be changed here`;
|
|
456
|
+
if (!entry.editable || entry.choices === null || entry.choices.length === 0)
|
|
463
457
|
return;
|
|
464
|
-
}
|
|
465
458
|
const current = shown === null ? 0 : Math.max(0, entry.choices.indexOf(shown));
|
|
466
459
|
const anchorKey = `${entry.backend}:${entry.id}`;
|
|
467
460
|
// Compatibility: pointer opening already brightens the source value. Enter borrows that
|
|
@@ -492,10 +485,8 @@ export class SettingsApp {
|
|
|
492
485
|
#openStructured(entry) {
|
|
493
486
|
// Invariant: the flags come from the declaration, not from the stored value: an unset
|
|
494
487
|
// flag still has a row, showing the default the source would apply.
|
|
495
|
-
if (entry.flags.length === 0)
|
|
496
|
-
this.#notice = `${labelOf(entry)} has nothing to configure`;
|
|
488
|
+
if (entry.flags.length === 0)
|
|
497
489
|
return;
|
|
498
|
-
}
|
|
499
490
|
const stored = typeof entry.rawValue === "object" && entry.rawValue !== null && !Array.isArray(entry.rawValue)
|
|
500
491
|
? entry.rawValue
|
|
501
492
|
: {};
|
|
@@ -648,13 +639,11 @@ export class SettingsApp {
|
|
|
648
639
|
const failure = changeFailure(outcome);
|
|
649
640
|
if (failure === null) {
|
|
650
641
|
this.#recordUndo({ kind: "structured", sequence, entry: open.entry, previous, forward });
|
|
651
|
-
this.#notice = null;
|
|
652
642
|
return;
|
|
653
643
|
}
|
|
654
644
|
// Concurrency: only roll back the dialog if no later press has moved the record on.
|
|
655
645
|
if (this.#structured === open && sameRecord(open.record, forward))
|
|
656
646
|
replaceRecord(open.record, previous);
|
|
657
|
-
this.#notice = `Could not save ${labelOf(open.entry)}: ${failure}`;
|
|
658
647
|
});
|
|
659
648
|
}
|
|
660
649
|
#menuKey(data) {
|
|
@@ -744,10 +733,8 @@ export class SettingsApp {
|
|
|
744
733
|
if (row === undefined || row.kind !== "element")
|
|
745
734
|
return;
|
|
746
735
|
const entry = row.value;
|
|
747
|
-
if (!entry.editable)
|
|
748
|
-
this.#notice = `${labelOf(entry)} cannot be changed here`;
|
|
736
|
+
if (!entry.editable)
|
|
749
737
|
return;
|
|
750
|
-
}
|
|
751
738
|
const shown = this.#shownValue(entry);
|
|
752
739
|
if (typeof shown === "number") {
|
|
753
740
|
// Rationale: at the end of the range there is nothing to say: the arrow already reads
|
|
@@ -758,10 +745,8 @@ export class SettingsApp {
|
|
|
758
745
|
return;
|
|
759
746
|
}
|
|
760
747
|
const choices = entry.choices;
|
|
761
|
-
if (choices === null || choices.length === 0)
|
|
762
|
-
this.#notice = `${labelOf(entry)} cannot be changed here`;
|
|
748
|
+
if (choices === null || choices.length === 0)
|
|
763
749
|
return;
|
|
764
|
-
}
|
|
765
750
|
const current = shown === null ? -1 : choices.indexOf(shown);
|
|
766
751
|
const next = choices[current < 0
|
|
767
752
|
? (delta > 0 ? 0 : choices.length - 1)
|
|
@@ -781,7 +766,6 @@ export class SettingsApp {
|
|
|
781
766
|
if (failure !== null) {
|
|
782
767
|
if (this.#pending.get(key) === value)
|
|
783
768
|
this.#pending.delete(key);
|
|
784
|
-
this.#notice = `Could not save ${labelOf(entry)}: ${failure}`;
|
|
785
769
|
return;
|
|
786
770
|
}
|
|
787
771
|
if (previous !== null && previous !== value)
|
|
@@ -789,7 +773,6 @@ export class SettingsApp {
|
|
|
789
773
|
// Concurrency: a later press may have moved on; only the last request clears itself.
|
|
790
774
|
if (this.#pending.get(key) === value)
|
|
791
775
|
this.#pending.delete(key);
|
|
792
|
-
this.#notice = null;
|
|
793
776
|
});
|
|
794
777
|
}
|
|
795
778
|
#shownValue(entry) {
|
|
@@ -841,13 +824,8 @@ export class SettingsApp {
|
|
|
841
824
|
}
|
|
842
825
|
#finishUndo(record, outcome) {
|
|
843
826
|
const failure = changeFailure(outcome);
|
|
844
|
-
if (failure !== null)
|
|
827
|
+
if (failure !== null)
|
|
845
828
|
this.#recordUndo(record);
|
|
846
|
-
this.#notice = `Could not restore ${labelOf(record.entry)}: ${failure}`;
|
|
847
|
-
}
|
|
848
|
-
else {
|
|
849
|
-
this.#notice = null;
|
|
850
|
-
}
|
|
851
829
|
this.#undoActive = false;
|
|
852
830
|
this.#drainUndo();
|
|
853
831
|
}
|
|
@@ -878,7 +856,6 @@ export class SettingsApp {
|
|
|
878
856
|
return;
|
|
879
857
|
this.#selectedKey = rowKey(rows[index]);
|
|
880
858
|
this.#selectionNeedsReveal = true;
|
|
881
|
-
this.#notice = null;
|
|
882
859
|
}
|
|
883
860
|
#rows() {
|
|
884
861
|
const needle = this.#filter?.value.trim().toLowerCase() ?? "";
|
|
@@ -1037,13 +1014,9 @@ export class SettingsApp {
|
|
|
1037
1014
|
const open = this.#structured;
|
|
1038
1015
|
if (open !== null)
|
|
1039
1016
|
return this.#dialogLines(open, width, theme);
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
: this.#interruptArmed ? "press ctrl+c again to exit a1" : null;
|
|
1044
|
-
const status = plainStatus === null
|
|
1045
|
-
? renderShortcutHintsWithClose(SETTINGS_SHORTCUTS.hintEntries(SCOPE), theme, width, width > 0 ? 1 : 0)
|
|
1046
|
-
: truncateToWidth(`${width > 0 ? " " : ""}${theme.fg("dim", plainStatus)}`, width);
|
|
1017
|
+
// Rationale: standing guidance is more useful than transient feedback; failures still
|
|
1018
|
+
// roll back without displacing the Settings interaction contract.
|
|
1019
|
+
const status = renderShortcutHintsWithClose(SETTINGS_SHORTCUTS.hintEntries(SCOPE), theme, width, width > 0 ? 1 : 0);
|
|
1047
1020
|
const input = this.#filter;
|
|
1048
1021
|
if (input === null)
|
|
1049
1022
|
return [status];
|
|
@@ -25,7 +25,7 @@ export interface BootstrapOptions {
|
|
|
25
25
|
/** Test seam for bounded maintenance scheduled after stale endpoint sweeping. */
|
|
26
26
|
readonly scheduleMaintenance?: (dataDir: string, paths: ReturnType<typeof resolveProductPaths>) => Promise<void>;
|
|
27
27
|
/** Test seams for resident launch routing before the guardian starts. */
|
|
28
|
-
readonly readResidentTabs?: (
|
|
28
|
+
readonly readResidentTabs?: (profileDir: string, namespace: string, legacyConfigDir: string, profileId: string) => Promise<boolean>;
|
|
29
29
|
readonly ensureTerminalHost?: (options: EnsureTerminalHostOptions) => Promise<EnsureTerminalHostResult>;
|
|
30
30
|
}
|
|
31
31
|
export declare function runBootstrap(options: BootstrapOptions): Promise<number>;
|
|
@@ -18,7 +18,7 @@ import { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDoc
|
|
|
18
18
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
19
19
|
import { markStartupPhase } from "../startup/index.js";
|
|
20
20
|
import { isCurrentLaunchContract, launchContractTarget, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
|
|
21
|
-
import { ensureTerminalHost,
|
|
21
|
+
import { ensureTerminalHost, readResidentTabLaunchSettings, selectResidentHost } from "../resident-tabs/index.js";
|
|
22
22
|
const RELEASE_RESELECTION_MESSAGE = "a1-release-reselection";
|
|
23
23
|
export async function runBootstrap(options) {
|
|
24
24
|
return await runBootstrapAttempt(options, 1);
|
|
@@ -36,17 +36,27 @@ async function runBootstrapAttempt(options, retriesRemaining) {
|
|
|
36
36
|
await mkdir(paths.runtimeDir, { recursive: true, mode: 0o700 });
|
|
37
37
|
const launchSelected = async (release) => {
|
|
38
38
|
let residentContext = {};
|
|
39
|
+
// Compatibility: public launch preparation supplies PI_CODING_AGENT_DIR; the fallback keeps
|
|
40
|
+
// direct SDK/bootstrap callers hermetic while retaining their former config-root read.
|
|
41
|
+
const agentProfileDir = environment.PI_CODING_AGENT_DIR ?? paths.configDir;
|
|
42
|
+
const residentLaunchSettings = launchProfileId === "a1"
|
|
43
|
+
? readResidentTabLaunchSettings(agentProfileDir, PRODUCT_IDENTITY.commandName, paths.configDir, launchProfileId)
|
|
44
|
+
: null;
|
|
39
45
|
const resident = await selectResidentHost({
|
|
40
46
|
profileId: launchProfileId,
|
|
41
47
|
hasSessionSelection: sessionArgs.length > 0,
|
|
42
48
|
currentLaunchContract: isCurrentLaunchContract(release),
|
|
43
|
-
readSetting:
|
|
44
|
-
|
|
49
|
+
readSetting: options.readResidentTabs === undefined
|
|
50
|
+
? async () => (await residentLaunchSettings).residentTabs
|
|
51
|
+
: () => options.readResidentTabs(agentProfileDir, PRODUCT_IDENTITY.commandName, paths.configDir, launchProfileId),
|
|
52
|
+
ensureHost: async () => (options.ensureTerminalHost ?? ensureTerminalHost)({
|
|
45
53
|
releaseRoot: release.releaseRoot,
|
|
46
54
|
profileRoot: environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir,
|
|
47
55
|
dataDir: paths.dataDir,
|
|
48
56
|
runtimeDir: paths.runtimeDir,
|
|
49
57
|
environment,
|
|
58
|
+
maxConcurrentStarts: (await residentLaunchSettings).maxConcurrentStarts,
|
|
59
|
+
unresponsiveRestartSeconds: (await residentLaunchSettings).unresponsiveRestartSeconds,
|
|
50
60
|
}),
|
|
51
61
|
});
|
|
52
62
|
if (resident !== null) {
|
|
@@ -55,7 +65,8 @@ async function runBootstrapAttempt(options, retriesRemaining) {
|
|
|
55
65
|
residentRootArguments: JSON.stringify([
|
|
56
66
|
"attach", "--endpoint", resident.endpoint, "--cwd", process.cwd(), "--clear-exit-ms", String(CLEAR_EXIT_INTERVAL_MS),
|
|
57
67
|
"--profile", environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir,
|
|
58
|
-
"--data-dir", paths.dataDir, "--
|
|
68
|
+
"--data-dir", paths.dataDir, "--runtime-dir", paths.runtimeDir,
|
|
69
|
+
"--release-root", release.releaseRoot, "--node-executable", process.execPath,
|
|
59
70
|
]),
|
|
60
71
|
} : { residentFallback: resident.reason };
|
|
61
72
|
}
|