@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/README.md
CHANGED
|
@@ -57,6 +57,12 @@ Launch A1 (profile: `~/.a1/agent`):
|
|
|
57
57
|
a1
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
Launch A1 in single agent mode:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
a1 solo
|
|
64
|
+
```
|
|
65
|
+
|
|
60
66
|
Show all commands:
|
|
61
67
|
|
|
62
68
|
```text
|
|
@@ -77,6 +83,30 @@ a1 pi
|
|
|
77
83
|
|
|
78
84
|
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/assets/readme/separator-dark.svg"><img src="docs/assets/readme/separator.svg" alt="" width="280"></picture></p>
|
|
79
85
|
|
|
86
|
+
## Tabs
|
|
87
|
+
|
|
88
|
+
Tabs keep running after their terminal closes. List them, or stop one; its session stays resumable:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
a1 tabs
|
|
92
|
+
a1 tabs stop <id>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Stop every tab and the resident host, for example before going back to single agent mode with `a1 solo`:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
a1 tabs host stop
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Show host health, or write a redacted diagnostic bundle:
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
a1 tabs host status
|
|
105
|
+
a1 tabs doctor
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/assets/readme/separator-dark.svg"><img src="docs/assets/readme/separator.svg" alt="" width="280"></picture></p>
|
|
109
|
+
|
|
80
110
|
## Update
|
|
81
111
|
|
|
82
112
|
Install the release:
|
package/bin/cli.js
CHANGED
|
@@ -69,6 +69,10 @@ process.exitCode = await dispatchCli(process.argv.slice(2), {
|
|
|
69
69
|
},
|
|
70
70
|
});
|
|
71
71
|
},
|
|
72
|
+
tabs: async request => {
|
|
73
|
+
const { runTabsCommand } = await import("../dist/cli/tabs.js");
|
|
74
|
+
return await runTabsCommand(request, { packageRoot: fileURLToPath(packageRoot) });
|
|
75
|
+
},
|
|
72
76
|
}, {
|
|
73
77
|
stdout: message => process.stdout.write(message),
|
|
74
78
|
stderr: message => process.stderr.write(message),
|
|
@@ -12,6 +12,8 @@ export function assignFreshSessionRuntimeId(
|
|
|
12
12
|
createId: () => string,
|
|
13
13
|
): string;
|
|
14
14
|
|
|
15
|
+
export function waitForStandbyPromotion(input: NodeJS.ReadStream, promotionFile?: string): Promise<void>;
|
|
16
|
+
|
|
15
17
|
export function consumeTabLaunchContext(
|
|
16
18
|
arguments_: readonly string[],
|
|
17
19
|
environment: Record<string, string | undefined>,
|
|
@@ -20,4 +22,5 @@ export function consumeTabLaunchContext(
|
|
|
20
22
|
readonly tabCredentials: TabLaunchCredentials | undefined;
|
|
21
23
|
readonly tabDataDir?: string;
|
|
22
24
|
readonly tabIncarnation?: number;
|
|
25
|
+
readonly tabStandby: boolean;
|
|
23
26
|
};
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
1
3
|
const TAB_ENVIRONMENT_NAMES = ["A1_TAB_ID", "A1_TAB_TOKEN", "A1_TAB_ENDPOINT"];
|
|
2
4
|
|
|
3
5
|
/** @param {string} reason */
|
|
4
6
|
export function formatResidentFallbackNotice(reason) {
|
|
5
|
-
|
|
7
|
+
// Protocol: a host of another generation is never replaced automatically; only the user stops it.
|
|
8
|
+
if (reason === "incompatible generation") {
|
|
9
|
+
return "A1 normal mode unavailable (running tabs use another version); running in single agent mode. Run `a1 tabs host stop` to restart them on this version.";
|
|
10
|
+
}
|
|
11
|
+
return `A1 normal mode unavailable (${reason}); running in single agent mode.`;
|
|
6
12
|
}
|
|
7
13
|
|
|
8
14
|
/**
|
|
@@ -17,6 +23,46 @@ export function assignFreshSessionRuntimeId(environment, name, createId) {
|
|
|
17
23
|
return id;
|
|
18
24
|
}
|
|
19
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Keep the standby below the session/runtime boundary until its authenticated holder promotes it.
|
|
28
|
+
* Platform: Windows ConPTY may drop the in-band marker, so the holder also creates the promotion
|
|
29
|
+
* file inside the tab's private data directory; whichever arrives first promotes the standby.
|
|
30
|
+
* @param {NodeJS.ReadStream} input
|
|
31
|
+
* @param {string} [promotionFile]
|
|
32
|
+
*/
|
|
33
|
+
export function waitForStandbyPromotion(input, promotionFile) {
|
|
34
|
+
return /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
|
|
35
|
+
const marker = Buffer.from("\x1b]a1;standby-promote\x07");
|
|
36
|
+
let buffered = Buffer.alloc(0);
|
|
37
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
38
|
+
let poll;
|
|
39
|
+
const cleanup = () => {
|
|
40
|
+
if (poll !== undefined) clearInterval(poll);
|
|
41
|
+
input.off("data", onData);
|
|
42
|
+
input.off("error", onError);
|
|
43
|
+
input.pause();
|
|
44
|
+
if (input.isTTY) input.setRawMode(false);
|
|
45
|
+
};
|
|
46
|
+
/** @param {Error} error */
|
|
47
|
+
const onError = error => { cleanup(); reject(error); };
|
|
48
|
+
/** @param {Buffer | string} chunk */
|
|
49
|
+
const onData = chunk => {
|
|
50
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
51
|
+
buffered = Buffer.concat([buffered, bytes]).subarray(-marker.length);
|
|
52
|
+
if (buffered.equals(marker)) { cleanup(); resolve(undefined); }
|
|
53
|
+
};
|
|
54
|
+
if (input.isTTY) input.setRawMode(true);
|
|
55
|
+
input.on("data", onData);
|
|
56
|
+
input.on("error", onError);
|
|
57
|
+
input.resume();
|
|
58
|
+
if (promotionFile !== undefined) {
|
|
59
|
+
poll = setInterval(() => {
|
|
60
|
+
if (existsSync(promotionFile)) { cleanup(); resolve(undefined); }
|
|
61
|
+
}, 10);
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
20
66
|
/**
|
|
21
67
|
* Security: consume bridge credentials into caller-private memory and erase their inheritable
|
|
22
68
|
* environment entries before any extension-capable module loads.
|
|
@@ -28,6 +74,9 @@ export function consumeTabLaunchContext(arguments_, environment) {
|
|
|
28
74
|
const tabArgument = uiArguments.indexOf("--tab");
|
|
29
75
|
const tabMode = tabArgument !== -1;
|
|
30
76
|
if (tabMode) uiArguments.splice(tabArgument, 1);
|
|
77
|
+
const standbyArgument = uiArguments.indexOf("--tab-standby");
|
|
78
|
+
const tabStandby = standbyArgument !== -1;
|
|
79
|
+
if (tabStandby) uiArguments.splice(standbyArgument, 1);
|
|
31
80
|
const dataArgument = uiArguments.indexOf("--tab-data-dir");
|
|
32
81
|
const tabDataDir = dataArgument === -1 ? undefined : uiArguments[dataArgument + 1];
|
|
33
82
|
if (dataArgument !== -1) uiArguments.splice(dataArgument, 2);
|
|
@@ -36,10 +85,10 @@ export function consumeTabLaunchContext(arguments_, environment) {
|
|
|
36
85
|
if (incarnationArgument !== -1) uiArguments.splice(incarnationArgument, 2);
|
|
37
86
|
const [tabId, token, endpoint] = TAB_ENVIRONMENT_NAMES.map(name => environment[name]);
|
|
38
87
|
for (const name of TAB_ENVIRONMENT_NAMES) delete environment[name];
|
|
39
|
-
if (!tabMode) return { uiArguments, tabCredentials: undefined };
|
|
88
|
+
if (!tabMode) return { uiArguments, tabCredentials: undefined, tabStandby: false };
|
|
40
89
|
if (!tabId || !token || !endpoint || !tabDataDir || tabDataDir.includes("\0")
|
|
41
90
|
|| typeof tabIncarnation !== "number" || !Number.isSafeInteger(tabIncarnation) || tabIncarnation < 1) {
|
|
42
91
|
throw new Error("A1 tab mode requires bridge credentials, recovery directory, and incarnation");
|
|
43
92
|
}
|
|
44
|
-
return { uiArguments, tabCredentials: { tabId, token, endpoint }, tabDataDir, tabIncarnation };
|
|
93
|
+
return { uiArguments, tabCredentials: { tabId, token, endpoint }, tabDataDir, tabIncarnation, tabStandby };
|
|
45
94
|
}
|
package/bin/ui.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// Security: this helper has no A1, Pi, tool, or extension dependency and erases credentials before those modules load.
|
|
4
|
-
const { assignFreshSessionRuntimeId, consumeTabLaunchContext, formatResidentFallbackNotice } = await import("./tab-launch-context.js");
|
|
5
|
-
const { uiArguments, tabCredentials, tabDataDir, tabIncarnation } = consumeTabLaunchContext(process.argv.slice(2), process.env);
|
|
4
|
+
const { assignFreshSessionRuntimeId, consumeTabLaunchContext, formatResidentFallbackNotice, waitForStandbyPromotion } = await import("./tab-launch-context.js");
|
|
5
|
+
const { uiArguments, tabCredentials, tabDataDir, tabIncarnation, tabStandby } = 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");
|
|
9
9
|
installPinnedPiTuiResolver(new URL("..", import.meta.url).pathname.replace(/^\/(.:)/, "$1").replace(/\/$/, ""));
|
|
10
10
|
const startup = await import("../dist/foundation/startup/startup-runtime.js");
|
|
11
11
|
startup.enableEnvironmentCompileCache(process.env);
|
|
12
|
-
const { readLaunchContext } = await import("../dist/foundation/launch-context/index.js");
|
|
12
|
+
const { hidesTabSettings, readLaunchContext } = await import("../dist/foundation/launch-context/index.js");
|
|
13
13
|
const launchContext = readLaunchContext(process.env, "profile");
|
|
14
14
|
const profile = launchContext.launchProfile;
|
|
15
15
|
if (profile === undefined) throw new Error("A1 launch profile is missing");
|
|
@@ -52,6 +52,8 @@ const [
|
|
|
52
52
|
{ composeOwnedUi },
|
|
53
53
|
] = await modules;
|
|
54
54
|
|
|
55
|
+
if (tabStandby) await waitForStandbyPromotion(process.stdin, tabDataDir === undefined ? undefined : join(tabDataDir, "standby-promote"));
|
|
56
|
+
|
|
55
57
|
// Compatibility: before the composition uses pinned Pi's terminal stack: confirm A1 and Pi
|
|
56
58
|
// resolve it to the same copy, so extensions and the owned UI share one module identity.
|
|
57
59
|
identity.assertSinglePiTuiModuleAtLaunch(packageRootPath, (/** @type {string} */ message) => process.stderr.write(message));
|
|
@@ -76,6 +78,7 @@ Promise.resolve().then(async () => {
|
|
|
76
78
|
cwd: process.cwd(),
|
|
77
79
|
profileId,
|
|
78
80
|
ownedSurfaces,
|
|
81
|
+
...(hidesTabSettings(launchContext) ? { hideTabSettings: true } : {}),
|
|
79
82
|
...(processInspector === null ? {} : { inspectProcess: pid => processInspector.observe(pid) }),
|
|
80
83
|
projectTrustPrompt: createConsoleProjectTrustPrompt({ presentation: profile === "a1" ? "bare" : "comparison" }),
|
|
81
84
|
sessionForkPrompt: createConsoleSessionForkPrompt(),
|
|
@@ -46,7 +46,6 @@ 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
49
|
reportStatus?(status: "working" | "idle" | "stalled"): void;
|
|
51
50
|
readonly stallNoticeSeconds?: number;
|
|
52
51
|
admit?(input: {
|
|
@@ -218,6 +217,8 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
|
|
|
218
217
|
readonly keybindings?: PiKeybindingsHost;
|
|
219
218
|
readonly promptImageLimit?: () => number;
|
|
220
219
|
readonly skillsPresentation?: () => "collapse" | "expand";
|
|
220
|
+
/** A resident tab chip already shows the session name, so its footer omits the duplicate. */
|
|
221
|
+
readonly footerSessionName?: boolean;
|
|
221
222
|
}, startup?: PiShellHeaderOptions, agentDir?: string, extensionRenderers?: OwnedUiTranscriptRendererPort, sessionLayout?: "pinned" | "custom-viewport", imageAssets?: PiShellImageAssetResolver);
|
|
222
223
|
setEditorPaddingX(padding: number): void;
|
|
223
224
|
setAutocompleteMaxVisible(maxVisible: number): void;
|
|
@@ -119,7 +119,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
119
119
|
frame: progressStatusFrame,
|
|
120
120
|
}, handlers);
|
|
121
121
|
this.#status.setProgressPresentation(this.#customViewport ? "custom-viewport" : "pinned");
|
|
122
|
-
this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd, this.#customViewport ? "a1" : "pi", () => this.#footerLevel);
|
|
122
|
+
this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd, this.#customViewport ? "a1" : "pi", () => this.#footerLevel, handlers.footerSessionName ?? true);
|
|
123
123
|
this.editor = createPiShellEditor({
|
|
124
124
|
...handlers,
|
|
125
125
|
keybindingProfile: this.#customViewport ? "a1" : "pi",
|
|
@@ -916,7 +916,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
916
916
|
if (result.command === "hotkeys" && result.outcome === "completed") {
|
|
917
917
|
this.#lastWorkflowStatusId = undefined;
|
|
918
918
|
const presentation = this.hotkeysPresentation();
|
|
919
|
-
const hotkeys = createPiShellHotkeys(presentation.bindings, presentation.getShortcuts, presentation.profile);
|
|
919
|
+
const hotkeys = createPiShellHotkeys(presentation.bindings, presentation.getShortcuts, presentation.profile, presentation.residentTabs);
|
|
920
920
|
this.#appendAnchoredWorkflowComponent(width => hotkeys.render(width), () => hotkeys.dispose?.());
|
|
921
921
|
return;
|
|
922
922
|
}
|
|
@@ -188,6 +188,7 @@ export class OwnedUiSessionPresenter {
|
|
|
188
188
|
replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
|
|
189
189
|
}),
|
|
190
190
|
enableDockInputReuse: inputPresentation?.viewportReuse !== false,
|
|
191
|
+
footerSessionName: this.#residentTabOptions === undefined,
|
|
191
192
|
persistentHistory: this.#customViewport && promptHistory !== undefined,
|
|
192
193
|
...(promptHistory === undefined ? {} : { historyEditor: promptHistory.editor }),
|
|
193
194
|
onSubmit: text => { void this.submit(text).catch(() => this.#reportSubmissionError()); },
|
|
@@ -1276,7 +1277,7 @@ export class OwnedUiSessionPresenter {
|
|
|
1276
1277
|
this.#residentStalled = true;
|
|
1277
1278
|
resident.reportStatus?.("stalled");
|
|
1278
1279
|
const minutes = Math.max(1, Math.round(seconds / 60));
|
|
1279
|
-
this.root.appendWorkflowStatus(`No progress for ${minutes} min — Esc interrupts ·
|
|
1280
|
+
this.root.appendWorkflowStatus(`No progress for ${minutes} min — Esc interrupts · use the tab restart action to restart`);
|
|
1280
1281
|
this.#terminal.requestRender();
|
|
1281
1282
|
}, seconds * 1_000);
|
|
1282
1283
|
this.#residentProgressTimer.unref?.();
|
|
@@ -1759,7 +1760,8 @@ export class OwnedUiSessionPresenter {
|
|
|
1759
1760
|
this.#dialogId = dialog.id;
|
|
1760
1761
|
}
|
|
1761
1762
|
hotkeysPresentation() {
|
|
1762
|
-
|
|
1763
|
+
const presentation = this.root.hotkeysPresentation();
|
|
1764
|
+
return this.#residentTabOptions === undefined ? presentation : { ...presentation, residentTabs: true };
|
|
1763
1765
|
}
|
|
1764
1766
|
async #slashCommand(text) {
|
|
1765
1767
|
const body = text.slice(1).trim();
|
|
@@ -1768,12 +1770,6 @@ export class OwnedUiSessionPresenter {
|
|
|
1768
1770
|
const argument = separator < 0 ? "" : body.slice(separator + 1).trimStart();
|
|
1769
1771
|
if (this.#routeHost?.claims(name))
|
|
1770
1772
|
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
|
-
}
|
|
1777
1773
|
if (this.#isWorkflowRoute(name))
|
|
1778
1774
|
return this.runWorkflow({ command: name, argument });
|
|
1779
1775
|
if (this.#skillsCollapsed()) {
|
|
@@ -98,6 +98,8 @@ export declare class OwnedUiTerminalHost implements OwnedUiTerminalHostContract<
|
|
|
98
98
|
active(): OwnedUiHostedPresenter | null;
|
|
99
99
|
requestRender(from: OwnedUiHostedPresenter, force?: boolean): void;
|
|
100
100
|
start(): void;
|
|
101
|
+
/** Hidden resident tabs retain engine/status work but stop producing terminal animation frames. */
|
|
102
|
+
setPresentationVisible(visible: boolean): void;
|
|
101
103
|
/** Closes one presenter. Closing the last one ends the terminal. */
|
|
102
104
|
close(presenter: OwnedUiHostedPresenter): Promise<void>;
|
|
103
105
|
dispose(): Promise<void>;
|
|
@@ -42,6 +42,8 @@ export class OwnedUiTerminalHost {
|
|
|
42
42
|
#focused = false;
|
|
43
43
|
#pointerReporting = false;
|
|
44
44
|
#started = false;
|
|
45
|
+
#presentationVisible = true;
|
|
46
|
+
#releaseVisibilityHold;
|
|
45
47
|
#ending;
|
|
46
48
|
constructor(options) {
|
|
47
49
|
this.#customViewport = options.layout === "custom-viewport";
|
|
@@ -147,7 +149,7 @@ export class OwnedUiTerminalHost {
|
|
|
147
149
|
return this.#active;
|
|
148
150
|
}
|
|
149
151
|
requestRender(from, force = false) {
|
|
150
|
-
if (from === this.#active)
|
|
152
|
+
if (this.#presentationVisible && from === this.#active)
|
|
151
153
|
this.runtime.requestRender(force);
|
|
152
154
|
}
|
|
153
155
|
start() {
|
|
@@ -155,9 +157,26 @@ export class OwnedUiTerminalHost {
|
|
|
155
157
|
return;
|
|
156
158
|
this.#started = true;
|
|
157
159
|
this.runtime.start();
|
|
160
|
+
if (!this.#presentationVisible)
|
|
161
|
+
this.#releaseVisibilityHold = this.runtime.beginPresentationHold();
|
|
158
162
|
if (this.#customViewport)
|
|
159
163
|
this.#setPointerReporting(true);
|
|
160
164
|
}
|
|
165
|
+
/** Hidden resident tabs retain engine/status work but stop producing terminal animation frames. */
|
|
166
|
+
setPresentationVisible(visible) {
|
|
167
|
+
if (this.#presentationVisible === visible)
|
|
168
|
+
return;
|
|
169
|
+
this.#presentationVisible = visible;
|
|
170
|
+
if (!this.#started || this.#ending !== undefined)
|
|
171
|
+
return;
|
|
172
|
+
if (visible) {
|
|
173
|
+
this.#releaseVisibilityHold?.();
|
|
174
|
+
this.#releaseVisibilityHold = undefined;
|
|
175
|
+
}
|
|
176
|
+
else if (this.#releaseVisibilityHold === undefined) {
|
|
177
|
+
this.#releaseVisibilityHold = this.runtime.beginPresentationHold();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
161
180
|
/** Closes one presenter. Closing the last one ends the terminal. */
|
|
162
181
|
close(presenter) {
|
|
163
182
|
if (this.#ending !== undefined || !this.#presenters.has(presenter))
|
package/dist/cli/dispatch.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type InteractiveLaunchIntent, type LaunchProfileId, type SessionSelection } from "../features/launch/index.js";
|
|
1
|
+
import { type InteractiveLaunchIntent, type InteractiveLaunchMode, type LaunchProfileId, type SessionSelection } from "../features/launch/index.js";
|
|
2
2
|
import type { CliCapabilities } from "./capabilities.js";
|
|
3
3
|
import type { PackageCommandRequest } from "./packages.js";
|
|
4
4
|
import type { SessionContextRequest } from "./session-context.js";
|
|
5
|
+
import type { TabsRequest } from "./tabs-request.js";
|
|
5
6
|
import { type PackageCliVerb, type PackageCommandNamespace, type PackageSyntaxDiagnostic } from "./package-messages.js";
|
|
6
7
|
export type UpdateChannel = "stable" | "next";
|
|
7
8
|
export interface CliHandlers {
|
|
@@ -10,6 +11,7 @@ export interface CliHandlers {
|
|
|
10
11
|
readonly update: (channel: UpdateChannel, target?: string) => Promise<number>;
|
|
11
12
|
readonly packages: (request: PackageCommandRequest) => Promise<number>;
|
|
12
13
|
readonly sessionContext: (request: SessionContextRequest) => Promise<number>;
|
|
14
|
+
readonly tabs: (request: TabsRequest) => Promise<number>;
|
|
13
15
|
}
|
|
14
16
|
export interface CliOutput {
|
|
15
17
|
readonly stdout: (message: string) => void;
|
|
@@ -17,6 +19,8 @@ export interface CliOutput {
|
|
|
17
19
|
}
|
|
18
20
|
export declare function cliUsage(capabilities: CliCapabilities): string;
|
|
19
21
|
export declare function cliHelp(capabilities: CliCapabilities): string;
|
|
22
|
+
/** Focused help for the resident-tab maintenance forms; printing it contacts no host. */
|
|
23
|
+
export declare function tabsHelp(): string;
|
|
20
24
|
export declare function dispatchCli(arguments_: readonly string[], handlers: CliHandlers, output: CliOutput, capabilities: CliCapabilities): Promise<number>;
|
|
21
25
|
export type CliCommand = {
|
|
22
26
|
readonly kind: "noop";
|
|
@@ -33,6 +37,7 @@ export type CliCommand = {
|
|
|
33
37
|
} | {
|
|
34
38
|
readonly kind: "launch";
|
|
35
39
|
readonly profileId: LaunchProfileId;
|
|
40
|
+
readonly launchMode: InteractiveLaunchMode;
|
|
36
41
|
readonly sessionSelection?: SessionSelection;
|
|
37
42
|
} | {
|
|
38
43
|
readonly kind: "version";
|
|
@@ -46,6 +51,11 @@ export type CliCommand = {
|
|
|
46
51
|
} | {
|
|
47
52
|
readonly kind: "session-context";
|
|
48
53
|
readonly request: SessionContextRequest;
|
|
54
|
+
} | {
|
|
55
|
+
readonly kind: "tabs";
|
|
56
|
+
readonly request: TabsRequest;
|
|
57
|
+
} | {
|
|
58
|
+
readonly kind: "tabs-help";
|
|
49
59
|
} | {
|
|
50
60
|
readonly kind: "error";
|
|
51
61
|
readonly message: string;
|
package/dist/cli/dispatch.js
CHANGED
|
@@ -6,6 +6,7 @@ export function cliUsage(capabilities) {
|
|
|
6
6
|
"",
|
|
7
7
|
"--session <path|id>",
|
|
8
8
|
"--session-dir <dir> --session <path|id>",
|
|
9
|
+
"solo",
|
|
9
10
|
...(capabilities.developmentComparison ? ["pi"] : []),
|
|
10
11
|
"help",
|
|
11
12
|
"version",
|
|
@@ -21,6 +22,10 @@ export function cliUsage(capabilities) {
|
|
|
21
22
|
"session worktrees",
|
|
22
23
|
"session link-worktree <path>",
|
|
23
24
|
"session unlink-worktree",
|
|
25
|
+
"tabs",
|
|
26
|
+
"tabs stop <id>|--all",
|
|
27
|
+
"tabs host status|stop",
|
|
28
|
+
"tabs doctor",
|
|
24
29
|
]);
|
|
25
30
|
}
|
|
26
31
|
export function cliHelp(capabilities) {
|
|
@@ -30,6 +35,7 @@ export function cliHelp(capabilities) {
|
|
|
30
35
|
` ${command}`,
|
|
31
36
|
` ${command} --session <path|id>`,
|
|
32
37
|
` ${command} --session-dir <dir> --session <path|id>`,
|
|
38
|
+
` ${command} solo`,
|
|
33
39
|
...(capabilities.developmentComparison ? [` ${command} pi`] : []),
|
|
34
40
|
` ${command} help`,
|
|
35
41
|
` ${command} version`,
|
|
@@ -52,9 +58,32 @@ export function cliHelp(capabilities) {
|
|
|
52
58
|
` ${command} session link-worktree <path>`,
|
|
53
59
|
` ${command} session unlink-worktree`,
|
|
54
60
|
"",
|
|
61
|
+
"Tabs:",
|
|
62
|
+
` ${command} tabs`,
|
|
63
|
+
` ${command} tabs stop <id>|--all`,
|
|
64
|
+
` ${command} tabs host status|stop`,
|
|
65
|
+
` ${command} tabs doctor`,
|
|
66
|
+
"",
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
|
69
|
+
/** Focused help for the resident-tab maintenance forms; printing it contacts no host. */
|
|
70
|
+
export function tabsHelp() {
|
|
71
|
+
const command = PRODUCT_TEXT.commandName;
|
|
72
|
+
return [
|
|
73
|
+
"Resident tabs:",
|
|
74
|
+
` ${command} tabs List running tabs`,
|
|
75
|
+
` ${command} tabs stop <id> Stop one tab; its session stays resumable`,
|
|
76
|
+
` ${command} tabs stop --all Stop every tab and keep the host`,
|
|
77
|
+
` ${command} tabs host status Show host identity, limits, and health counters`,
|
|
78
|
+
` ${command} tabs host stop Stop every tab and the host`,
|
|
79
|
+
` ${command} tabs doctor Write a redacted diagnostic bundle`,
|
|
80
|
+
"",
|
|
81
|
+
"Options:",
|
|
82
|
+
" --json Print the versioned machine-readable report",
|
|
83
|
+
"",
|
|
55
84
|
].join("\n");
|
|
56
85
|
}
|
|
57
|
-
const PROFILE_WORDS = new Set(["pi"]);
|
|
86
|
+
const PROFILE_WORDS = new Set(["pi", "solo"]);
|
|
58
87
|
export async function dispatchCli(arguments_, handlers, output, capabilities) {
|
|
59
88
|
const command = parseCliCommand(arguments_, capabilities);
|
|
60
89
|
if (command.kind === "noop")
|
|
@@ -76,23 +105,29 @@ export async function dispatchCli(arguments_, handlers, output, capabilities) {
|
|
|
76
105
|
return 2;
|
|
77
106
|
}
|
|
78
107
|
if (command.kind === "launch")
|
|
79
|
-
return await handlers.launch(interactiveLaunchIntent(command.profileId, command.sessionSelection));
|
|
108
|
+
return await handlers.launch(interactiveLaunchIntent(command.profileId, command.sessionSelection, command.launchMode));
|
|
80
109
|
if (command.kind === "version")
|
|
81
110
|
return await handlers.version();
|
|
82
111
|
if (command.kind === "packages")
|
|
83
112
|
return await handlers.packages(command.request);
|
|
84
113
|
if (command.kind === "session-context")
|
|
85
114
|
return await handlers.sessionContext(command.request);
|
|
115
|
+
if (command.kind === "tabs-help") {
|
|
116
|
+
output.stdout(tabsHelp());
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
if (command.kind === "tabs")
|
|
120
|
+
return await handlers.tabs(command.request);
|
|
86
121
|
return await handlers.update(command.channel, command.target);
|
|
87
122
|
}
|
|
88
123
|
export function parseCliCommand(arguments_, capabilities) {
|
|
89
124
|
if (arguments_.length === 0)
|
|
90
|
-
return { kind: "launch", profileId: "a1" };
|
|
125
|
+
return { kind: "launch", profileId: "a1", launchMode: "tabs" };
|
|
91
126
|
const [command, ...rest] = arguments_;
|
|
92
127
|
if (command === "--session" || command === "--session-dir") {
|
|
93
128
|
try {
|
|
94
129
|
const sessionSelection = parseSessionSelection(arguments_);
|
|
95
|
-
return { kind: "launch", profileId: "a1", ...(sessionSelection === undefined ? {} : { sessionSelection }) };
|
|
130
|
+
return { kind: "launch", profileId: "a1", launchMode: "tabs", ...(sessionSelection === undefined ? {} : { sessionSelection }) };
|
|
96
131
|
}
|
|
97
132
|
catch (error) {
|
|
98
133
|
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`could not parse session launch: ${error instanceof Error ? error.message : String(error)}`) };
|
|
@@ -108,18 +143,57 @@ export function parseCliCommand(arguments_, capabilities) {
|
|
|
108
143
|
if (command === "pi") {
|
|
109
144
|
if (rest.length > 0)
|
|
110
145
|
return parsePackageCommand(rest, "pi");
|
|
111
|
-
return capabilities.developmentComparison ? { kind: "launch", profileId: "pi" } : { kind: "noop" };
|
|
146
|
+
return capabilities.developmentComparison ? { kind: "launch", profileId: "pi", launchMode: "solo" } : { kind: "noop" };
|
|
112
147
|
}
|
|
148
|
+
if (command === "solo")
|
|
149
|
+
return withoutArguments(rest, { kind: "launch", profileId: "a1", launchMode: "solo" });
|
|
113
150
|
if (command === "update")
|
|
114
151
|
return parseUpdate(rest);
|
|
115
152
|
if (command === "session")
|
|
116
153
|
return parseSessionContext(rest);
|
|
154
|
+
if (command === "tabs")
|
|
155
|
+
return parseTabs(rest);
|
|
117
156
|
if (command?.startsWith("update:"))
|
|
118
157
|
return { kind: "noop" };
|
|
119
158
|
// Rationale: unsupported and reserved command spaces are deliberately quiet. Help is
|
|
120
159
|
// explicit, and a typo must never start an interactive or maintenance path.
|
|
121
160
|
return { kind: "noop" };
|
|
122
161
|
}
|
|
162
|
+
/** Parses `a1 tabs` before any I/O; a malformed form fails with one line and contacts no host. */
|
|
163
|
+
function parseTabs(rest) {
|
|
164
|
+
if (rest.some(value => value === "--help" || value === "-h"))
|
|
165
|
+
return { kind: "tabs-help" };
|
|
166
|
+
const json = rest.includes("--json");
|
|
167
|
+
const words = rest.filter(value => value !== "--json");
|
|
168
|
+
const fail = (detail) => ({ kind: "error", message: PRODUCT_TEXT.diagnostic(`could not parse tabs: ${detail}`) });
|
|
169
|
+
const [action, value, extra] = words;
|
|
170
|
+
if (action === undefined)
|
|
171
|
+
return { kind: "tabs", request: { action: "list", json } };
|
|
172
|
+
if (action === "stop") {
|
|
173
|
+
if (value === undefined)
|
|
174
|
+
return fail("stop requires a tab id or --all.");
|
|
175
|
+
if (extra !== undefined)
|
|
176
|
+
return fail("stop accepts one tab id or --all.");
|
|
177
|
+
if (value === "--all")
|
|
178
|
+
return { kind: "tabs", request: { action: "stop-all", json } };
|
|
179
|
+
if (value.startsWith("-"))
|
|
180
|
+
return fail(`unknown option ${value}.`);
|
|
181
|
+
return { kind: "tabs", request: { action: "stop", tabId: value, json } };
|
|
182
|
+
}
|
|
183
|
+
if (action === "host") {
|
|
184
|
+
if (extra !== undefined || (value !== "status" && value !== "stop"))
|
|
185
|
+
return fail("host accepts status or stop.");
|
|
186
|
+
return { kind: "tabs", request: { action: value === "status" ? "host-status" : "host-stop", json } };
|
|
187
|
+
}
|
|
188
|
+
if (action === "doctor") {
|
|
189
|
+
if (value !== undefined)
|
|
190
|
+
return fail("doctor does not accept arguments.");
|
|
191
|
+
return { kind: "tabs", request: { action: "doctor", json } };
|
|
192
|
+
}
|
|
193
|
+
if (action.startsWith("-"))
|
|
194
|
+
return fail(`unknown option ${action}.`);
|
|
195
|
+
return fail(`unknown subcommand ${action}; use list, stop, host, or doctor.`);
|
|
196
|
+
}
|
|
123
197
|
function parseSessionContext(rest) {
|
|
124
198
|
const [action, value, extra] = rest;
|
|
125
199
|
if (action === "link-worktree") {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type TabsAction = "list" | "stop" | "stop-all" | "host-status" | "host-stop" | "doctor";
|
|
2
|
+
/** One parsed `a1 tabs` form. Parsing finishes before any host is contacted. */
|
|
3
|
+
export interface TabsRequest {
|
|
4
|
+
readonly action: TabsAction;
|
|
5
|
+
readonly tabId?: string;
|
|
6
|
+
readonly json: boolean;
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { runTerminalHostMaintenance, writeDoctorBundle } from "../foundation/resident-tabs/index.js";
|
|
2
|
+
import type { TabsRequest } from "./tabs-request.js";
|
|
3
|
+
export type { TabsRequest } from "./tabs-request.js";
|
|
4
|
+
export interface TabsCommandOptions {
|
|
5
|
+
/** The running package root; its terminal host is a candidate after the active release. */
|
|
6
|
+
readonly packageRoot?: string;
|
|
7
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
8
|
+
readonly stdout?: (message: string) => void;
|
|
9
|
+
readonly stderr?: (message: string) => void;
|
|
10
|
+
readonly maintenance?: typeof runTerminalHostMaintenance;
|
|
11
|
+
readonly doctor?: typeof writeDoctorBundle;
|
|
12
|
+
}
|
|
13
|
+
/** `a1 tabs`: inspect or stop resident tabs without attaching, and never start a host to report state. */
|
|
14
|
+
export declare function runTabsCommand(request: TabsRequest, options?: TabsCommandOptions): Promise<number>;
|