@timurproko/a1 0.2.7-dev.767 → 0.2.7-dev.779
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 +30 -0
- package/bin/cli.js +4 -0
- package/bin/tab-launch-context.d.ts +3 -0
- package/bin/tab-launch-context.js +52 -3
- package/bin/ui.js +6 -3
- package/dist/app/session-shell/session-shell-root.d.ts +2 -1
- package/dist/app/session-shell/session-shell-root.js +2 -2
- package/dist/app/session-shell/session-shell.js +4 -8
- package/dist/app/session-shell/shell-host.d.ts +2 -0
- package/dist/app/session-shell/shell-host.js +20 -1
- package/dist/cli/dispatch.d.ts +11 -1
- package/dist/cli/dispatch.js +79 -5
- package/dist/cli/tabs-request.d.ts +7 -0
- package/dist/cli/tabs-request.js +1 -0
- package/dist/cli/tabs.d.ts +14 -0
- package/dist/cli/tabs.js +140 -0
- package/dist/composition/owned-ui.d.ts +8 -2
- package/dist/composition/owned-ui.js +109 -12
- package/dist/features/launch/index.d.ts +1 -1
- package/dist/features/launch/intent.d.ts +3 -1
- package/dist/features/launch/intent.js +7 -4
- package/dist/features/resident-tab/auto-name.d.ts +11 -0
- package/dist/features/resident-tab/auto-name.js +42 -0
- package/dist/features/resident-tab/index.d.ts +3 -0
- package/dist/features/resident-tab/index.js +3 -0
- package/dist/features/resident-tab/shortcuts.d.ts +10 -0
- package/dist/features/resident-tab/shortcuts.js +35 -0
- package/dist/features/resident-tab/status-deriver.d.ts +9 -0
- package/dist/features/resident-tab/status-deriver.js +15 -0
- package/dist/foundation/launch-context/index.d.ts +4 -0
- package/dist/foundation/launch-context/index.js +10 -0
- package/dist/foundation/release/bootstrap.d.ts +4 -2
- package/dist/foundation/release/bootstrap.js +11 -8
- package/dist/foundation/release/cohort-state.d.ts +1 -1
- package/dist/foundation/release/cohort-state.js +1 -1
- package/dist/foundation/release/release-gc.d.ts +3 -0
- package/dist/foundation/release/release-gc.js +44 -8
- package/dist/foundation/resident-tabs/bounded-host-command.d.ts +16 -0
- package/dist/foundation/resident-tabs/bounded-host-command.js +35 -0
- package/dist/foundation/resident-tabs/bridge-client.d.ts +18 -3
- package/dist/foundation/resident-tabs/bridge-client.js +87 -7
- package/dist/foundation/resident-tabs/doctor.d.ts +29 -0
- package/dist/foundation/resident-tabs/doctor.js +166 -0
- package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +2 -1
- package/dist/foundation/resident-tabs/ensure-terminal-host.js +33 -7
- package/dist/foundation/resident-tabs/index.d.ts +7 -1
- package/dist/foundation/resident-tabs/index.js +4 -1
- package/dist/foundation/resident-tabs/maintenance.d.ts +42 -0
- package/dist/foundation/resident-tabs/maintenance.js +78 -0
- package/dist/foundation/resident-tabs/release-holds.d.ts +27 -0
- package/dist/foundation/resident-tabs/release-holds.js +77 -0
- package/dist/foundation/resident-tabs/routing.d.ts +4 -3
- package/dist/foundation/resident-tabs/routing.js +6 -4
- package/dist/foundation/resident-tabs/settings.d.ts +7 -2
- package/dist/foundation/resident-tabs/settings.js +17 -16
- package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +18 -0
- package/dist/foundation/resident-tabs/terminal-host-artifact.js +53 -0
- package/dist/foundation/resident-tabs/zip-archive.d.ts +9 -0
- package/dist/foundation/resident-tabs/zip-archive.js +56 -0
- package/dist/foundation/session-writer-lock/index.d.ts +5 -0
- package/dist/foundation/session-writer-lock/index.js +13 -9
- package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
- package/dist/integrations/pi/components/shell-footer-status.js +2 -2
- package/dist/integrations/pi/components/shell-hotkey-sections.js +1 -1
- package/dist/integrations/pi/components/shell-presenters-info.d.ts +4 -2
- package/dist/integrations/pi/components/shell-presenters-info.js +38 -4
- package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
- package/dist/integrations/pi/components/upstream/components/session-footer.js +7 -4
- package/dist/integrations/pi/engine/adapter.d.ts +1 -0
- package/dist/integrations/pi/engine/adapter.js +3 -0
- package/dist/integrations/pi/engine/prompt-suggestions.d.ts +1 -0
- package/dist/integrations/pi/engine/prompt-suggestions.js +40 -0
- package/dist/native/darwin-arm64/a1_session_lock.node +0 -0
- package/dist/native/darwin-arm64/manifest.json +2 -2
- package/dist/native/darwin-arm64/process-guardian +0 -0
- package/dist/native/darwin-arm64/terminal-host +0 -0
- package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +10500 -0
- package/dist/native/darwin-arm64/terminal-host.manifest.json +44 -0
- package/dist/native/darwin-x64/a1_session_lock.node +0 -0
- package/dist/native/darwin-x64/manifest.json +20 -0
- package/dist/native/darwin-x64/process-guardian +0 -0
- package/dist/native/darwin-x64/terminal-host +0 -0
- package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10737 -0
- package/dist/native/darwin-x64/terminal-host.manifest.json +44 -0
- package/dist/native/linux-x64/a1_session_lock.node +0 -0
- package/dist/native/linux-x64/manifest.json +2 -2
- package/dist/native/linux-x64/process-guardian +0 -0
- package/dist/native/linux-x64/terminal-host +0 -0
- package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +11226 -0
- package/dist/native/linux-x64/terminal-host.manifest.json +44 -0
- package/dist/native/win32-x64/a1_session_lock.node +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10757 -0
- package/dist/native/win32-x64/terminal-host.exe +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +44 -0
- package/dist/ui/settings/declarations.d.ts +19 -43
- package/dist/ui/settings/declarations.js +19 -35
- package/dist/ui/settings/index.d.ts +1 -1
- package/dist/ui/settings/index.js +1 -1
- package/dist/ui/settings/manager.d.ts +1 -0
- package/dist/ui/settings/manager.js +9 -1
- package/dist/ui/settings/migrations.js +12 -0
- package/dist/ui/settings/resolution.d.ts +0 -2
- package/dist/ui/settings/resolution.js +1 -13
- package/dist/ui/settings/sections.js +0 -4
- package/docs/architecture/evidence/resident-tabs/linux-x64.md +37 -0
- package/docs/architecture/evidence/resident-tabs/macos-arm64.md +36 -0
- package/docs/architecture/evidence/resident-tabs/macos-x64.md +35 -0
- package/docs/architecture/evidence/resident-tabs/windows-x64.md +36 -0
- package/docs/architecture/internal-naming.md +2 -0
- package/docs/architecture/process-guardian-provenance.md +2 -0
- package/docs/architecture/resident-tabs-requirements.md +1 -563
- package/docs/architecture/resident-tabs.md +166 -94
- package/docs/architecture/terminal-host-provenance.md +13 -3
- package/docs/architecture/toolchain.md +1 -1
- package/docs/ci-release-runbook.md +2 -2
- package/docs/features/launch-profiles.md +11 -8
- package/docs/manual-resident-tabs-acceptance.md +51 -0
- package/docs/openspec-archive-automation.md +15 -3
- package/docs/validation.md +9 -1
- package/package.json +5 -1
package/dist/cli/tabs.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { resolveProductPaths } from "../foundation/lifecycle/index.js";
|
|
2
|
+
import { CohortStateStore } from "../foundation/release/index.js";
|
|
3
|
+
import { readResidentTabLaunchSettings, runTerminalHostMaintenance, writeDoctorBundle, } from "../foundation/resident-tabs/index.js";
|
|
4
|
+
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../product-identity.js";
|
|
5
|
+
/** `a1 tabs`: inspect or stop resident tabs without attaching, and never start a host to report state. */
|
|
6
|
+
export async function runTabsCommand(request, options = {}) {
|
|
7
|
+
const environment = options.environment ?? process.env;
|
|
8
|
+
const stdout = options.stdout ?? (message => process.stdout.write(message));
|
|
9
|
+
const stderr = options.stderr ?? (message => process.stderr.write(message));
|
|
10
|
+
const paths = resolveProductPaths(environment);
|
|
11
|
+
const profileRoot = environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir;
|
|
12
|
+
const releaseRoots = await installedReleaseRoots(paths.dataDir, options.packageRoot);
|
|
13
|
+
const maintenance = options.maintenance ?? runTerminalHostMaintenance;
|
|
14
|
+
const report = await maintenance({
|
|
15
|
+
action: request.action === "doctor" ? "host-status" : request.action,
|
|
16
|
+
...(request.tabId === undefined ? {} : { tabId: request.tabId }),
|
|
17
|
+
profileRoot,
|
|
18
|
+
dataDir: paths.dataDir,
|
|
19
|
+
runtimeDir: paths.runtimeDir,
|
|
20
|
+
releaseRoots,
|
|
21
|
+
environment,
|
|
22
|
+
});
|
|
23
|
+
if (request.action === "doctor") {
|
|
24
|
+
if (!report.stateDir) {
|
|
25
|
+
stderr(`${PRODUCT_TEXT.diagnostic(`tabs doctor could not locate resident state: ${report.message ?? report.host}`)}\n`);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const settings = await readResidentTabLaunchSettings(environment.PI_CODING_AGENT_DIR ?? paths.configDir, PRODUCT_IDENTITY.commandName, paths.configDir, "a1");
|
|
29
|
+
const bundle = await (options.doctor ?? writeDoctorBundle)({
|
|
30
|
+
stateDir: report.stateDir,
|
|
31
|
+
hostStatus: report,
|
|
32
|
+
releaseRoots,
|
|
33
|
+
settings: { tabsMax: settings.tabsMax, tabsAutoName: settings.tabsAutoName },
|
|
34
|
+
});
|
|
35
|
+
if (request.json)
|
|
36
|
+
stdout(`${JSON.stringify({ schema: "a1-tabs-doctor-v1", path: bundle.path, bytes: bundle.bytes, dropped: bundle.dropped })}\n`);
|
|
37
|
+
else
|
|
38
|
+
stdout(`Wrote ${bundle.path}\n`);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
if (request.json) {
|
|
42
|
+
const { exitCode: _exitCode, ...printed } = report;
|
|
43
|
+
stdout(`${JSON.stringify(printed)}\n`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const text = describe(request, report);
|
|
47
|
+
if (report.exitCode === 0)
|
|
48
|
+
stdout(text);
|
|
49
|
+
else
|
|
50
|
+
stderr(text);
|
|
51
|
+
}
|
|
52
|
+
return report.exitCode === 0 ? 0 : 1;
|
|
53
|
+
}
|
|
54
|
+
async function installedReleaseRoots(dataDir, packageRoot) {
|
|
55
|
+
const roots = [];
|
|
56
|
+
try {
|
|
57
|
+
const state = await new CohortStateStore(dataDir).read();
|
|
58
|
+
for (const id of [state.references.active, state.references.rollback]) {
|
|
59
|
+
if (id && state.releases[id])
|
|
60
|
+
roots.push(state.releases[id].releaseRoot);
|
|
61
|
+
}
|
|
62
|
+
roots.push(...Object.values(state.releases).map(record => record.releaseRoot));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Rationale: unreadable release state still leaves the running package's own host.
|
|
66
|
+
}
|
|
67
|
+
if (packageRoot)
|
|
68
|
+
roots.push(packageRoot);
|
|
69
|
+
return [...new Set(roots)];
|
|
70
|
+
}
|
|
71
|
+
const NO_TABS = "No resident tabs are running.\n";
|
|
72
|
+
function describe(request, report) {
|
|
73
|
+
const command = PRODUCT_TEXT.commandName;
|
|
74
|
+
if (report.host === "none")
|
|
75
|
+
return NO_TABS;
|
|
76
|
+
if (report.host === "unavailable")
|
|
77
|
+
return `${PRODUCT_TEXT.diagnostic(`tabs could not run maintenance: ${report.message ?? "terminal host unavailable"}`)}\n`;
|
|
78
|
+
if (report.host === "unresponsive") {
|
|
79
|
+
return `The resident host (pid ${report.pid ?? "unknown"}) is not responding. Run \`${command} tabs host stop\` to stop it.\n`;
|
|
80
|
+
}
|
|
81
|
+
if (report.host === "incompatible") {
|
|
82
|
+
return `${PRODUCT_TEXT.diagnostic(report.message ?? "running tabs use another version")} Run \`${command} tabs host stop\` from that version.\n`;
|
|
83
|
+
}
|
|
84
|
+
if (report.host === "unverifiable" || report.host === "refused") {
|
|
85
|
+
return `${PRODUCT_TEXT.diagnostic(report.message ?? "the resident host could not be verified; nothing was changed")}\n`;
|
|
86
|
+
}
|
|
87
|
+
const result = report.result ?? {};
|
|
88
|
+
if (request.action === "list")
|
|
89
|
+
return listing(result);
|
|
90
|
+
if (request.action === "host-status")
|
|
91
|
+
return status(result);
|
|
92
|
+
if (request.action === "host-stop") {
|
|
93
|
+
if (report.host === "stopped")
|
|
94
|
+
return "Stopped every tab and the resident host.\n";
|
|
95
|
+
return `Could not stop every tab: ${remaining(result)}. The host is still running.\n`;
|
|
96
|
+
}
|
|
97
|
+
if (result.accepted === false && typeof result.message === "string") {
|
|
98
|
+
return `${PRODUCT_TEXT.diagnostic(`tabs stop: ${result.message === "unknown tab" ? `no tab ${request.tabId ?? ""}` : result.message}`)}\n`;
|
|
99
|
+
}
|
|
100
|
+
const stopped = Array.isArray(result.stopped) ? result.stopped.length : 0;
|
|
101
|
+
if (result.accepted === false)
|
|
102
|
+
return `Could not stop: ${remaining(result)}.\n`;
|
|
103
|
+
if (request.action === "stop")
|
|
104
|
+
return `Stopped tab ${request.tabId ?? ""}; its session can be resumed.\n`;
|
|
105
|
+
return stopped === 0 ? NO_TABS : `Stopped ${stopped} tab${stopped === 1 ? "" : "s"}; their sessions can be resumed.\n`;
|
|
106
|
+
}
|
|
107
|
+
function remaining(result) {
|
|
108
|
+
return Array.isArray(result.remaining) && result.remaining.length > 0 ? result.remaining.join(", ") : "unknown tabs";
|
|
109
|
+
}
|
|
110
|
+
function listing(result) {
|
|
111
|
+
const tabs = Array.isArray(result.tabs) ? result.tabs : [];
|
|
112
|
+
if (tabs.length === 0)
|
|
113
|
+
return NO_TABS;
|
|
114
|
+
const rows = [
|
|
115
|
+
["ID", "NAME", "STATUS", "CWD", "RELEASE", "HOLDER"],
|
|
116
|
+
...tabs.map(tab => [tab.tabId, tab.name, tab.status, tab.cwd, tab.release, tab.holderPid ?? "-"].map(value => String(value ?? "-"))),
|
|
117
|
+
];
|
|
118
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map(row => row[column].length)));
|
|
119
|
+
return `${rows.map(row => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ")).join("\n")}\n`;
|
|
120
|
+
}
|
|
121
|
+
function status(result) {
|
|
122
|
+
const limits = (result.limits ?? {});
|
|
123
|
+
const counters = (result.counters ?? {});
|
|
124
|
+
const byStatus = (result.tabsByStatus ?? {});
|
|
125
|
+
const lines = [
|
|
126
|
+
`pid: ${String(result.pid ?? "-")}`,
|
|
127
|
+
`build: ${String(result.build ?? "-")}`,
|
|
128
|
+
`release: ${String(result.release ?? "-")}`,
|
|
129
|
+
`generation: ${String(result.generation ?? "-")}`,
|
|
130
|
+
`epoch: ${String(result.epoch ?? "-")}`,
|
|
131
|
+
`uptime: ${String(result.uptimeSeconds ?? "-")}s`,
|
|
132
|
+
`detachment: ${String(result.detachmentMode ?? "unknown")}${result.detachmentVerified === false ? " (not verified)" : ""}`,
|
|
133
|
+
`tabs: ${String(result.tabs ?? 0)}${Object.keys(byStatus).length > 0 ? ` (${Object.entries(byStatus).map(([key, value]) => `${key} ${String(value)}`).join(", ")})` : ""}`,
|
|
134
|
+
`clients: ${String(result.clients ?? 0)}`,
|
|
135
|
+
`limits: ${Object.entries(limits).map(([key, value]) => `${key} ${String(value)}`).join(", ")}`,
|
|
136
|
+
`counters: ${Object.entries(counters).map(([key, value]) => `${key} ${String(value)}`).join(", ")}`,
|
|
137
|
+
...(Array.isArray(result.logs) ? result.logs.map(path => `log: ${String(path)}`) : []),
|
|
138
|
+
];
|
|
139
|
+
return `${lines.join("\n")}\n`;
|
|
140
|
+
}
|
|
@@ -19,6 +19,7 @@ import { ResidentTabRecovery } from "../foundation/resident-tabs/recovery.js";
|
|
|
19
19
|
*/
|
|
20
20
|
export interface ComposedSessionBackend extends PiSessionPresenterBackend, OwnedUiPromptSuggestionGeneratorPort {
|
|
21
21
|
settingsPort(): AgentSettingsPort | null;
|
|
22
|
+
generateResidentTabName(instruction: string, maxTokens: number, signal: AbortSignal): Promise<string | null>;
|
|
22
23
|
}
|
|
23
24
|
/** The process-level engine host as composition drives it: sessions, the live accent, and disposal. */
|
|
24
25
|
export interface ComposedEngineHost {
|
|
@@ -40,6 +41,8 @@ export interface OwnedUiCompositionOptions {
|
|
|
40
41
|
* session settings-free, which is what the pinned comparison paths use.
|
|
41
42
|
*/
|
|
42
43
|
readonly profileId?: string;
|
|
44
|
+
/** Hide resident-only tab controls for the explicit direct single-agent launch. */
|
|
45
|
+
readonly hideTabSettings?: boolean;
|
|
43
46
|
/**
|
|
44
47
|
* Whether A1's product-specific surfaces are reachable. Comparison profiles
|
|
45
48
|
* use the same composition with those surfaces withheld.
|
|
@@ -63,7 +66,7 @@ export interface OwnedUiCompositionOptions {
|
|
|
63
66
|
readonly dataDir: string;
|
|
64
67
|
readonly incarnation: number;
|
|
65
68
|
};
|
|
66
|
-
/** One prompt-adjacent notice explaining why
|
|
69
|
+
/** One prompt-adjacent notice explaining why a resident launch fell back. */
|
|
67
70
|
readonly residentFallbackNotice?: string;
|
|
68
71
|
}
|
|
69
72
|
export interface OwnedUiComposition {
|
|
@@ -82,4 +85,7 @@ export declare function residentStartupPresentation(resident: boolean, fallbackN
|
|
|
82
85
|
};
|
|
83
86
|
export declare function composeOwnedUiApplication(options?: OwnedUiCompositionOptions): Promise<OwnedUiApplicationPort>;
|
|
84
87
|
export declare function composeOwnedUi(options?: OwnedUiCompositionOptions): Promise<OwnedUiComposition>;
|
|
85
|
-
export declare function bindResidentTabBridge(tabBridge: TabBridgeClient, backend: Pick<ComposedSessionBackend, "identity" | "session">, recovery?: Pick<ResidentTabRecovery, "settled"> | null
|
|
88
|
+
export declare function bindResidentTabBridge(tabBridge: TabBridgeClient, backend: Pick<ComposedSessionBackend, "identity" | "session" | "workflows" | "generateResidentTabName">, recovery?: Pick<ResidentTabRecovery, "settled"> | null, options?: {
|
|
89
|
+
readonly autoName?: boolean;
|
|
90
|
+
readonly onVisibility?: (visible: boolean) => void;
|
|
91
|
+
}): Promise<() => void>;
|
|
@@ -19,6 +19,7 @@ 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
21
|
import { ResidentTabRecovery } from "../foundation/resident-tabs/recovery.js";
|
|
22
|
+
import { deriveResidentTabStatus } from "../features/resident-tab/status-deriver.js";
|
|
22
23
|
const SESSION_RUNTIME_REFRESH_MS = 30_000;
|
|
23
24
|
export function residentStartupPresentation(resident, fallbackNotice) {
|
|
24
25
|
if (resident)
|
|
@@ -99,9 +100,22 @@ export async function composeOwnedUi(options = {}) {
|
|
|
99
100
|
namespace: PRODUCT_IDENTITY.commandName,
|
|
100
101
|
legacyConfigDir: productPaths.configDir,
|
|
101
102
|
agentProvider: () => backend.settingsPort(),
|
|
103
|
+
...(options.hideTabSettings ? { hiddenOwnedSettingIds: ["tabsMax", "tabsAutoName"] } : {}),
|
|
102
104
|
hiddenAgentSettingIds: ["fullscreenWheelScrollLines"],
|
|
103
105
|
agentSettingLabelOverrides: { fullscreenCopyOnSelect: "Copy on select" },
|
|
104
106
|
});
|
|
107
|
+
const tabBridge = options.tab?.bridge ?? null;
|
|
108
|
+
if (tabBridge !== null)
|
|
109
|
+
await tabBridge.start().catch(() => false);
|
|
110
|
+
const projectTrustPrompt = options.projectTrustPrompt === undefined ? undefined : async (request) => {
|
|
111
|
+
tabBridge?.status("needs-input", false, 1, 0);
|
|
112
|
+
try {
|
|
113
|
+
return await options.projectTrustPrompt(request);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
tabBridge?.status("starting");
|
|
117
|
+
}
|
|
118
|
+
};
|
|
105
119
|
// Invariant: process-wide engine state has one owner. The host installs the HTTP dispatcher, applies
|
|
106
120
|
// the theme singletons, and announces startup notices once; every session is created through it.
|
|
107
121
|
const host = await (options.createEngineHost ?? (async (hostOptions) => createPiEngineHost(hostOptions)))({
|
|
@@ -114,22 +128,30 @@ export async function composeOwnedUi(options = {}) {
|
|
|
114
128
|
packageBorderProjection: ownedSurfaces,
|
|
115
129
|
apply: { base: applyConfiguredPiTheme, accentColor: setPiAccentColor, packageBorderProjection: setPiPackageBorderProjectionEnabled },
|
|
116
130
|
},
|
|
117
|
-
...(
|
|
131
|
+
...(projectTrustPrompt === undefined ? {} : { projectTrustPrompt }),
|
|
118
132
|
});
|
|
133
|
+
const sessionForkPrompt = options.sessionForkPrompt === undefined ? undefined : async (request) => {
|
|
134
|
+
tabBridge?.status("needs-input", false, 1, 0);
|
|
135
|
+
try {
|
|
136
|
+
return await options.sessionForkPrompt(request);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
tabBridge?.status("starting");
|
|
140
|
+
}
|
|
141
|
+
};
|
|
119
142
|
try {
|
|
120
143
|
backend = await host.create({
|
|
121
144
|
cwd,
|
|
122
145
|
...(ownedSurfaces ? { repositoryContextReader: repositoryContextReaderFor(undefined) } : {}),
|
|
123
146
|
...(options.sessionPath === undefined ? {} : { sessionPath: options.sessionPath }),
|
|
124
147
|
...(options.sessionSelection === undefined ? {} : { sessionSelection: options.sessionSelection }),
|
|
125
|
-
...(
|
|
148
|
+
...(sessionForkPrompt === undefined ? {} : { sessionForkPrompt }),
|
|
126
149
|
});
|
|
127
150
|
}
|
|
128
151
|
catch (error) {
|
|
129
152
|
await host.dispose();
|
|
130
153
|
throw error;
|
|
131
154
|
}
|
|
132
|
-
const tabBridge = options.tab?.bridge ?? null;
|
|
133
155
|
let recoveryDegraded = false;
|
|
134
156
|
let presentRecoveryDegraded = (_degraded) => { };
|
|
135
157
|
const tabRecovery = options.tab === undefined ? null : new ResidentTabRecovery(options.tab.dataDir, {
|
|
@@ -140,7 +162,15 @@ export async function composeOwnedUi(options = {}) {
|
|
|
140
162
|
},
|
|
141
163
|
});
|
|
142
164
|
const recoveredTabInput = await tabRecovery?.initialize(currentBackendSessionFile(backend), backend.identity.sessionId);
|
|
143
|
-
|
|
165
|
+
if (tabBridge !== null && settings !== null && ownedSurfaces)
|
|
166
|
+
tabBridge.accent(settings.value("accentColor"));
|
|
167
|
+
let residentTabVisible = true;
|
|
168
|
+
let applyResidentTabVisibility = () => { };
|
|
169
|
+
let removeControllerBoundaryInput = () => { };
|
|
170
|
+
const unsubscribeTabBridge = tabBridge === null ? () => { } : await bindResidentTabBridge(tabBridge, backend, tabRecovery, {
|
|
171
|
+
autoName: settings?.value("tabsAutoName") ?? true,
|
|
172
|
+
onVisibility: visible => { residentTabVisible = visible; applyResidentTabVisibility(); },
|
|
173
|
+
});
|
|
144
174
|
let releaseNotes = null;
|
|
145
175
|
let releaseNotesFailure;
|
|
146
176
|
if (ownedSurfaces) {
|
|
@@ -163,7 +193,11 @@ export async function composeOwnedUi(options = {}) {
|
|
|
163
193
|
}));
|
|
164
194
|
const unsubscribeAccent = settings === null || !ownedSurfaces
|
|
165
195
|
? () => { }
|
|
166
|
-
: settings.onChange(current =>
|
|
196
|
+
: settings.onChange(current => {
|
|
197
|
+
const accent = current.value("accentColor");
|
|
198
|
+
host.setAccentColor(accent);
|
|
199
|
+
tabBridge?.accent(accent);
|
|
200
|
+
});
|
|
167
201
|
// Rationale: Routes open only after shell construction.
|
|
168
202
|
const references = {
|
|
169
203
|
changelog: async (input) => {
|
|
@@ -251,9 +285,8 @@ export async function composeOwnedUi(options = {}) {
|
|
|
251
285
|
...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
|
|
252
286
|
...(tabBridge === null || tabRecovery === null ? {} : { residentTab: {
|
|
253
287
|
requestDetach: () => tabBridge.requestDetach(),
|
|
254
|
-
requestRestart: () => tabBridge.requestRestart(),
|
|
255
288
|
reportStatus: status => tabBridge.status(status),
|
|
256
|
-
stallNoticeSeconds:
|
|
289
|
+
stallNoticeSeconds: 300,
|
|
257
290
|
admit: input => tabRecovery.admit({ ...input, incarnation: options.tab.incarnation }),
|
|
258
291
|
dispatched: () => tabRecovery.dispatched(),
|
|
259
292
|
correlated: (submissionId, entryId) => tabRecovery.correlated(submissionId, entryId),
|
|
@@ -288,6 +321,12 @@ export async function composeOwnedUi(options = {}) {
|
|
|
288
321
|
// Invariant: one terminal host owns the process terminal; the session presenter reaches it only through
|
|
289
322
|
// the host and paints once attached.
|
|
290
323
|
terminalHost = createdTerminalHost = new OwnedUiTerminalHost(sessionTerminalHostOptions(presenterOptions));
|
|
324
|
+
if (tabBridge !== null) {
|
|
325
|
+
applyResidentTabVisibility = () => terminalHost.setPresentationVisible(residentTabVisible);
|
|
326
|
+
removeControllerBoundaryInput = terminalHost.runtime.addPreInputListener(data => ({
|
|
327
|
+
data: tabBridge.consumeControllerBoundaryInput(data),
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
291
330
|
presenter = new OwnedUiSessionPresenter(terminalHost, presenterOptions);
|
|
292
331
|
presentRecoveryDegraded = degraded => {
|
|
293
332
|
if (!degraded)
|
|
@@ -336,6 +375,7 @@ export async function composeOwnedUi(options = {}) {
|
|
|
336
375
|
exitNotice = import("../app/session-shell/exit-notice.js")
|
|
337
376
|
.then(module => module.armExitNotice(backend, exitNoticePath), () => null);
|
|
338
377
|
presenter.start();
|
|
378
|
+
applyResidentTabVisibility();
|
|
339
379
|
void announceNewerRelease().catch(() => undefined);
|
|
340
380
|
},
|
|
341
381
|
flush: () => backend.session.flushEvents(),
|
|
@@ -350,6 +390,7 @@ export async function composeOwnedUi(options = {}) {
|
|
|
350
390
|
finally {
|
|
351
391
|
// Invariant: the shared spares outlive every shell and stop only after the last one is disposed.
|
|
352
392
|
clipboardServices?.dispose();
|
|
393
|
+
removeControllerBoundaryInput();
|
|
353
394
|
unsubscribeAccent();
|
|
354
395
|
sessionRuntimeDisposed = true;
|
|
355
396
|
if (sessionRuntimeRefreshTimer !== null)
|
|
@@ -374,27 +415,83 @@ export async function composeOwnedUi(options = {}) {
|
|
|
374
415
|
};
|
|
375
416
|
return { application, settings };
|
|
376
417
|
}
|
|
377
|
-
export async function bindResidentTabBridge(tabBridge, backend, recovery = null) {
|
|
418
|
+
export async function bindResidentTabBridge(tabBridge, backend, recovery = null, options = {}) {
|
|
378
419
|
await tabBridge.start().catch(() => false);
|
|
379
420
|
tabBridge.status("starting");
|
|
380
421
|
const currentSessionFile = () => currentBackendSessionFile(backend);
|
|
422
|
+
let pendingAutoName = null;
|
|
381
423
|
const announceSession = () => {
|
|
382
|
-
|
|
424
|
+
const name = backend.session.view().status.footer?.sessionName ?? null;
|
|
425
|
+
const nameSource = name === pendingAutoName ? "auto" : name === null || /^agent(?: \d+)?$/i.test(name) ? "default" : "user";
|
|
426
|
+
tabBridge.session(currentSessionFile(), name, nameSource);
|
|
383
427
|
};
|
|
384
428
|
let working = false;
|
|
429
|
+
let lastTurnFailed = false;
|
|
430
|
+
let autoNameAttempted = false;
|
|
431
|
+
const reportStatus = (lifecycle) => {
|
|
432
|
+
const view = backend.session.view();
|
|
433
|
+
const pendingRequests = view.dialog == null ? 0 : 1;
|
|
434
|
+
const queuedInput = view.editor?.queuedSubmissions.length ?? 0;
|
|
435
|
+
tabBridge.status(deriveResidentTabStatus({
|
|
436
|
+
lifecycle: lifecycle ?? view.lifecycle,
|
|
437
|
+
pendingRequests,
|
|
438
|
+
queuedInput,
|
|
439
|
+
...(lastTurnFailed ? { stopReason: "error" } : {}),
|
|
440
|
+
}), false, pendingRequests, queuedInput);
|
|
441
|
+
};
|
|
385
442
|
announceSession();
|
|
386
|
-
|
|
443
|
+
const unsubscribeCommands = typeof tabBridge.onCommand === "function" ? tabBridge.onCommand(command => {
|
|
444
|
+
if (command.type === "visibility") {
|
|
445
|
+
options.onVisibility?.(command.visible);
|
|
446
|
+
}
|
|
447
|
+
else if (command.type === "rename") {
|
|
448
|
+
void backend.workflows.executeWorkflow({ command: "name", argument: command.name }).catch(() => { });
|
|
449
|
+
}
|
|
450
|
+
else if (command.type === "stop") {
|
|
451
|
+
void backend.session.dispose().catch(() => { });
|
|
452
|
+
}
|
|
453
|
+
}) : () => { };
|
|
454
|
+
const unsubscribeEvents = backend.session.onEvent(event => {
|
|
387
455
|
if (event.type === "session-lifecycle") {
|
|
388
456
|
const wasWorking = working;
|
|
389
457
|
working = event.lifecycle === "busy";
|
|
390
|
-
|
|
458
|
+
if (working)
|
|
459
|
+
lastTurnFailed = false;
|
|
460
|
+
reportStatus(event.lifecycle);
|
|
391
461
|
if (wasWorking && !working && recovery !== null) {
|
|
392
462
|
void recovery.settled(currentSessionFile()).catch(() => tabBridge.degraded(true));
|
|
393
463
|
}
|
|
464
|
+
if (wasWorking && !working && options.autoName === true && !autoNameAttempted) {
|
|
465
|
+
autoNameAttempted = true;
|
|
466
|
+
const view = backend.session.view();
|
|
467
|
+
const currentName = view.status.footer?.sessionName;
|
|
468
|
+
const prompt = view.transcript.find(block => block.kind === "user")?.text.trim();
|
|
469
|
+
if (prompt && (currentName == null || /^agent(?: \d+)?$/i.test(currentName))) {
|
|
470
|
+
void import("../features/resident-tab/index.js").then(({ generateAutoName }) => generateAutoName(prompt, request => backend.generateResidentTabName(request.prompt, request.maxTokens, request.signal))).then(async (name) => {
|
|
471
|
+
pendingAutoName = name;
|
|
472
|
+
const result = await backend.workflows.executeWorkflow({ command: "name", argument: name });
|
|
473
|
+
if (result.outcome === "completed")
|
|
474
|
+
tabBridge.session(currentSessionFile(), name, "auto");
|
|
475
|
+
else if (pendingAutoName === name)
|
|
476
|
+
pendingAutoName = null;
|
|
477
|
+
}).catch(() => { pendingAutoName = null; });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (event.type === "agent-run-settled") {
|
|
482
|
+
lastTurnFailed = !event.successful && !event.aborted;
|
|
483
|
+
tabBridge.prompt(event.aborted);
|
|
484
|
+
reportStatus();
|
|
394
485
|
}
|
|
395
|
-
if (event.type === "session-view")
|
|
486
|
+
if (event.type === "session-view") {
|
|
396
487
|
announceSession();
|
|
488
|
+
reportStatus();
|
|
489
|
+
}
|
|
397
490
|
});
|
|
491
|
+
return () => {
|
|
492
|
+
unsubscribeCommands();
|
|
493
|
+
unsubscribeEvents();
|
|
494
|
+
};
|
|
398
495
|
}
|
|
399
496
|
function currentBackendSessionFile(backend) {
|
|
400
497
|
const direct = backend.identity.currentSessionFile?.();
|
|
@@ -3,7 +3,7 @@ export type { DevelopmentLaunchEnvironment } from "./development-launch.js";
|
|
|
3
3
|
export { PI_PROFILE_RESOURCE_DIRECTORIES, initializeProductProfile } from "./initialize-profile.js";
|
|
4
4
|
export type { InitializedProfile } from "./initialize-profile.js";
|
|
5
5
|
export { interactiveLaunchIntent, parseSessionSelection } from "./intent.js";
|
|
6
|
-
export type { InteractiveLaunchIntent, LaunchIntent, LaunchProfileId, SessionSelection } from "./intent.js";
|
|
6
|
+
export type { InteractiveLaunchIntent, InteractiveLaunchMode, LaunchIntent, LaunchProfileId, SessionSelection } from "./intent.js";
|
|
7
7
|
export { configurationRootForProfile, resolveLaunchProfilePaths, resolvePromptHistoryDataDir } from "./profile-paths.js";
|
|
8
8
|
export type { LaunchProfilePathOptions, LaunchProfilePaths } from "./profile-paths.js";
|
|
9
9
|
export { prepareInteractiveLaunch } from "./prepare-launch.js";
|
|
@@ -3,10 +3,12 @@ import type { LaunchProfileId } from "../../foundation/lifecycle/index.js";
|
|
|
3
3
|
export { parseSessionSelection } from "../../foundation/lifecycle/index.js";
|
|
4
4
|
export type { SessionSelection } from "../../foundation/lifecycle/index.js";
|
|
5
5
|
export type { LaunchProfileId } from "../../foundation/lifecycle/index.js";
|
|
6
|
+
export type InteractiveLaunchMode = "tabs" | "solo";
|
|
6
7
|
export interface InteractiveLaunchIntent {
|
|
7
8
|
readonly kind: "interactive";
|
|
8
9
|
readonly profileId: LaunchProfileId;
|
|
10
|
+
readonly launchMode: InteractiveLaunchMode;
|
|
9
11
|
readonly sessionSelection?: SessionSelection;
|
|
10
12
|
}
|
|
11
13
|
export type LaunchIntent = InteractiveLaunchIntent;
|
|
12
|
-
export declare function interactiveLaunchIntent(profileId: LaunchProfileId, sessionSelection?: SessionSelection): InteractiveLaunchIntent;
|
|
14
|
+
export declare function interactiveLaunchIntent(profileId: LaunchProfileId, sessionSelection?: SessionSelection, launchMode?: InteractiveLaunchMode): InteractiveLaunchIntent;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { sessionSelectionArguments } from "../../foundation/lifecycle/index.js";
|
|
2
2
|
export { parseSessionSelection } from "../../foundation/lifecycle/index.js";
|
|
3
|
-
export function interactiveLaunchIntent(profileId, sessionSelection) {
|
|
4
|
-
if (sessionSelection !== undefined && profileId !== "a1")
|
|
5
|
-
throw new Error("session selection requires the normal A1
|
|
3
|
+
export function interactiveLaunchIntent(profileId, sessionSelection, launchMode = profileId === "a1" ? "tabs" : "solo") {
|
|
4
|
+
if (sessionSelection !== undefined && (profileId !== "a1" || launchMode === "solo")) {
|
|
5
|
+
throw new Error("session selection requires the normal tabbed A1 launch");
|
|
6
|
+
}
|
|
7
|
+
if (profileId === "pi" && launchMode !== "solo")
|
|
8
|
+
throw new Error("the Pi comparison launch is always solo");
|
|
6
9
|
sessionSelectionArguments(sessionSelection);
|
|
7
|
-
return Object.freeze({ kind: "interactive", profileId, ...(sessionSelection === undefined ? {} : { sessionSelection }) });
|
|
10
|
+
return Object.freeze({ kind: "interactive", profileId, launchMode, ...(sessionSelection === undefined ? {} : { sessionSelection }) });
|
|
8
11
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const AUTO_NAME_MAX_LENGTH = 16;
|
|
2
|
+
export declare const AUTO_NAME_TIMEOUT_MS = 10000;
|
|
3
|
+
export declare const AUTO_NAME_MAX_TOKENS = 64;
|
|
4
|
+
export declare function normalizeAutoName(value: string): string | null;
|
|
5
|
+
export declare function fallbackAutoName(prompt: string): string;
|
|
6
|
+
export interface AutoNameCompletion {
|
|
7
|
+
readonly prompt: string;
|
|
8
|
+
readonly maxTokens: number;
|
|
9
|
+
readonly signal: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
export declare function generateAutoName(firstPrompt: string, complete: (request: AutoNameCompletion) => Promise<string | null>, timeoutMs?: number): Promise<string>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const AUTO_NAME_MAX_LENGTH = 16;
|
|
2
|
+
export const AUTO_NAME_TIMEOUT_MS = 10_000;
|
|
3
|
+
export const AUTO_NAME_MAX_TOKENS = 64;
|
|
4
|
+
export function normalizeAutoName(value) {
|
|
5
|
+
const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
6
|
+
if (!normalized)
|
|
7
|
+
return null;
|
|
8
|
+
if (normalized.length <= AUTO_NAME_MAX_LENGTH)
|
|
9
|
+
return normalized;
|
|
10
|
+
const clipped = normalized.slice(0, AUTO_NAME_MAX_LENGTH + 1);
|
|
11
|
+
const boundary = clipped.lastIndexOf("-");
|
|
12
|
+
const result = (boundary >= 4 ? clipped.slice(0, boundary) : normalized.slice(0, AUTO_NAME_MAX_LENGTH)).replace(/-+$/g, "");
|
|
13
|
+
return result || null;
|
|
14
|
+
}
|
|
15
|
+
export function fallbackAutoName(prompt) {
|
|
16
|
+
const words = prompt.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
17
|
+
return normalizeAutoName(words.slice(0, 3).join("-")) ?? "new-task";
|
|
18
|
+
}
|
|
19
|
+
export async function generateAutoName(firstPrompt, complete, timeoutMs = AUTO_NAME_TIMEOUT_MS) {
|
|
20
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
23
|
+
timer.unref?.();
|
|
24
|
+
try {
|
|
25
|
+
const response = await complete({
|
|
26
|
+
prompt: `Name this coding task with a lowercase hyphenated title of at most ${AUTO_NAME_MAX_LENGTH} characters. Reply only with the title.\n\n${firstPrompt.slice(0, 500)}`,
|
|
27
|
+
maxTokens: AUTO_NAME_MAX_TOKENS,
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
});
|
|
30
|
+
const name = response === null ? null : normalizeAutoName(response);
|
|
31
|
+
if (name !== null)
|
|
32
|
+
return name;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Rationale: naming is best effort; one retry precedes the deterministic fallback.
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return fallbackAutoName(firstPrompt);
|
|
42
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { AUTO_NAME_MAX_LENGTH, AUTO_NAME_MAX_TOKENS, AUTO_NAME_TIMEOUT_MS, fallbackAutoName, generateAutoName, normalizeAutoName, type AutoNameCompletion, } from "./auto-name.js";
|
|
2
|
+
export { RESIDENT_TAB_SHORTCUTS, residentTabShortcutRegistry, type ResidentTabAction, type ResidentTabShortcut, } from "./shortcuts.js";
|
|
3
|
+
export { deriveResidentTabStatus, type ResidentTabDerivedStatus, type ResidentTabFacts, } from "./status-deriver.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { AUTO_NAME_MAX_LENGTH, AUTO_NAME_MAX_TOKENS, AUTO_NAME_TIMEOUT_MS, fallbackAutoName, generateAutoName, normalizeAutoName, } from "./auto-name.js";
|
|
2
|
+
export { RESIDENT_TAB_SHORTCUTS, residentTabShortcutRegistry, } from "./shortcuts.js";
|
|
3
|
+
export { deriveResidentTabStatus, } from "./status-deriver.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ShortcutRegistry } from "../../ui/components/index.js";
|
|
2
|
+
export type ResidentTabAction = "tabs.new" | "tabs.close" | "tabs.rename" | "tabs.retry" | "tabs.fresh" | "tabs.next" | "tabs.previous" | "tabs.moveRight" | "tabs.moveLeft" | `tabs.jump${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10}`;
|
|
3
|
+
export interface ResidentTabShortcut {
|
|
4
|
+
readonly id: ResidentTabAction;
|
|
5
|
+
readonly defaultKey: string | null;
|
|
6
|
+
readonly scope: "global" | "failed-banner";
|
|
7
|
+
readonly description: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const RESIDENT_TAB_SHORTCUTS: readonly ResidentTabShortcut[];
|
|
10
|
+
export declare function residentTabShortcutRegistry(assignments?: Readonly<Partial<Record<ResidentTabAction, string | null>>>): ShortcutRegistry<ResidentTabAction>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { GLOBAL_SCOPE, ShortcutRegistry } from "../../ui/components/index.js";
|
|
2
|
+
const jumps = Array.from({ length: 10 }, (_, index) => ({
|
|
3
|
+
id: `tabs.jump${index + 1}`,
|
|
4
|
+
defaultKey: `alt+${(index + 1) % 10}`,
|
|
5
|
+
scope: "global",
|
|
6
|
+
description: `View A1 tab ${index + 1}`,
|
|
7
|
+
}));
|
|
8
|
+
export const RESIDENT_TAB_SHORTCUTS = Object.freeze([
|
|
9
|
+
{ id: "tabs.new", defaultKey: "alt+a", scope: "global", description: "New A1 tab" },
|
|
10
|
+
{ id: "tabs.close", defaultKey: "alt+w", scope: "global", description: "Close A1 tab" },
|
|
11
|
+
{ id: "tabs.rename", defaultKey: "f2", scope: "global", description: "Rename A1 tab" },
|
|
12
|
+
...jumps,
|
|
13
|
+
{ id: "tabs.next", defaultKey: null, scope: "global", description: "View next A1 tab" },
|
|
14
|
+
{ id: "tabs.previous", defaultKey: null, scope: "global", description: "View previous A1 tab" },
|
|
15
|
+
{ id: "tabs.moveRight", defaultKey: null, scope: "global", description: "Move A1 tab right" },
|
|
16
|
+
{ id: "tabs.moveLeft", defaultKey: null, scope: "global", description: "Move A1 tab left" },
|
|
17
|
+
{ id: "tabs.retry", defaultKey: "r", scope: "failed-banner", description: "Retry failed A1 tab" },
|
|
18
|
+
{ id: "tabs.fresh", defaultKey: "f", scope: "failed-banner", description: "Start failed A1 tab fresh" },
|
|
19
|
+
]);
|
|
20
|
+
export function residentTabShortcutRegistry(assignments = {}) {
|
|
21
|
+
const registry = new ShortcutRegistry();
|
|
22
|
+
for (const shortcut of RESIDENT_TAB_SHORTCUTS) {
|
|
23
|
+
const key = assignments[shortcut.id] === undefined ? shortcut.defaultKey : assignments[shortcut.id];
|
|
24
|
+
if (!key)
|
|
25
|
+
continue;
|
|
26
|
+
const declaration = {
|
|
27
|
+
key,
|
|
28
|
+
scope: shortcut.scope === "global" ? GLOBAL_SCOPE : shortcut.scope,
|
|
29
|
+
section: "Tabs",
|
|
30
|
+
description: shortcut.description,
|
|
31
|
+
};
|
|
32
|
+
registry.declare(declaration, shortcut.id);
|
|
33
|
+
}
|
|
34
|
+
return registry;
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ResidentTabDerivedStatus = "starting" | "working" | "needs-input" | "done-unseen" | "idle" | "failed";
|
|
2
|
+
export interface ResidentTabFacts {
|
|
3
|
+
readonly lifecycle: string;
|
|
4
|
+
readonly pendingRequests: number;
|
|
5
|
+
readonly queuedInput: number;
|
|
6
|
+
readonly stopReason?: string;
|
|
7
|
+
readonly settledUnseen?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function deriveResidentTabStatus(facts: ResidentTabFacts): ResidentTabDerivedStatus;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function deriveResidentTabStatus(facts) {
|
|
2
|
+
if (facts.lifecycle === "failed")
|
|
3
|
+
return "failed";
|
|
4
|
+
if (facts.pendingRequests > 0)
|
|
5
|
+
return "needs-input";
|
|
6
|
+
if (facts.lifecycle === "busy" || facts.queuedInput > 0)
|
|
7
|
+
return "working";
|
|
8
|
+
if (facts.lifecycle === "starting")
|
|
9
|
+
return "starting";
|
|
10
|
+
if (facts.stopReason === "error")
|
|
11
|
+
return "failed";
|
|
12
|
+
if (facts.settledUnseen === true)
|
|
13
|
+
return "done-unseen";
|
|
14
|
+
return "idle";
|
|
15
|
+
}
|
|
@@ -6,6 +6,7 @@ export declare const PRIVATE_ENVIRONMENT: Readonly<{
|
|
|
6
6
|
releaseDigest: "LAUNCH_CONTEXT_RELEASE_DIGEST";
|
|
7
7
|
releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS";
|
|
8
8
|
launchProfile: "LAUNCH_CONTEXT_PROFILE";
|
|
9
|
+
launchMode: "LAUNCH_CONTEXT_MODE";
|
|
9
10
|
immutableWarmup: "LAUNCH_CONTEXT_WARMUP";
|
|
10
11
|
residentRootExecutable: "LAUNCH_CONTEXT_RESIDENT_ROOT_EXECUTABLE";
|
|
11
12
|
residentRootArguments: "LAUNCH_CONTEXT_RESIDENT_ROOT_ARGUMENTS";
|
|
@@ -33,11 +34,14 @@ export interface LaunchContext {
|
|
|
33
34
|
readonly releaseDigest?: string;
|
|
34
35
|
readonly releaseLayers?: string;
|
|
35
36
|
readonly launchProfile?: "a1" | "pi";
|
|
37
|
+
readonly launchMode?: "tabs" | "solo";
|
|
36
38
|
readonly immutableWarmup?: "1";
|
|
37
39
|
readonly residentRootExecutable?: string;
|
|
38
40
|
readonly residentRootArguments?: string;
|
|
39
41
|
readonly residentFallback?: string;
|
|
40
42
|
}
|
|
43
|
+
/** The Tabs settings configure resident tabs; a direct-route launch, by `a1 solo` or by a fallback, hides them. */
|
|
44
|
+
export declare function hidesTabSettings(context: Pick<LaunchContext, "launchMode" | "residentFallback">): boolean;
|
|
41
45
|
export type LaunchContextRequirement = "optional" | "profile" | "release" | "warmup";
|
|
42
46
|
/** Read current keys, fall back to superseded keys per field, with bounded diagnostics. */
|
|
43
47
|
export declare function readLaunchContext(environment: NodeJS.ProcessEnv, requirement?: LaunchContextRequirement, platform?: NodeJS.Platform): LaunchContext;
|
|
@@ -6,6 +6,7 @@ export const PRIVATE_ENVIRONMENT = Object.freeze({
|
|
|
6
6
|
releaseDigest: "LAUNCH_CONTEXT_RELEASE_DIGEST",
|
|
7
7
|
releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS",
|
|
8
8
|
launchProfile: "LAUNCH_CONTEXT_PROFILE",
|
|
9
|
+
launchMode: "LAUNCH_CONTEXT_MODE",
|
|
9
10
|
immutableWarmup: "LAUNCH_CONTEXT_WARMUP",
|
|
10
11
|
residentRootExecutable: "LAUNCH_CONTEXT_RESIDENT_ROOT_EXECUTABLE",
|
|
11
12
|
residentRootArguments: "LAUNCH_CONTEXT_RESIDENT_ROOT_ARGUMENTS",
|
|
@@ -30,6 +31,10 @@ const SUPERSEDED_ENTRIES = Object.entries(SUPERSEDED_ENVIRONMENT);
|
|
|
30
31
|
const PRIVATE_KEYS = new Set([...Object.values(PRIVATE_ENVIRONMENT), ...Object.values(SUPERSEDED_ENVIRONMENT)]);
|
|
31
32
|
const LOGICAL_KEYS = new Map(PRIVATE_ENTRIES.map(([logical, key]) => [key, logical]));
|
|
32
33
|
const SUPERSEDED_LOGICAL_KEYS = new Map(SUPERSEDED_ENTRIES.map(([logical, key]) => [key, logical]));
|
|
34
|
+
/** The Tabs settings configure resident tabs; a direct-route launch, by `a1 solo` or by a fallback, hides them. */
|
|
35
|
+
export function hidesTabSettings(context) {
|
|
36
|
+
return context.launchMode === "solo" || context.residentFallback !== undefined;
|
|
37
|
+
}
|
|
33
38
|
/** Read current keys, fall back to superseded keys per field, with bounded diagnostics. */
|
|
34
39
|
export function readLaunchContext(environment, requirement = "optional", platform = process.platform) {
|
|
35
40
|
const values = {};
|
|
@@ -64,6 +69,11 @@ export function readLaunchContext(environment, requirement = "optional", platfor
|
|
|
64
69
|
if (values.launchProfile !== undefined && values.launchProfile !== "a1" && values.launchProfile !== "pi") {
|
|
65
70
|
throw invalidContext("launchProfile", "unsupported profile");
|
|
66
71
|
}
|
|
72
|
+
if (values.launchMode !== undefined && values.launchMode !== "tabs" && values.launchMode !== "solo") {
|
|
73
|
+
throw invalidContext("launchMode", "unsupported launch mode");
|
|
74
|
+
}
|
|
75
|
+
if (values.launchProfile === "pi" && values.launchMode === "tabs")
|
|
76
|
+
throw invalidContext("launchMode", "Pi comparison cannot use tabs");
|
|
67
77
|
if (values.immutableWarmup !== undefined && values.immutableWarmup !== "1")
|
|
68
78
|
throw invalidContext("immutableWarmup", "invalid permission flag");
|
|
69
79
|
if ((values.residentRootExecutable === undefined) !== (values.residentRootArguments === undefined))
|