@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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, resolve } from "node:path";
|
|
4
|
+
export const TERMINAL_HOST_MANIFEST_FILENAME = "terminal-host.manifest.json";
|
|
5
|
+
export const TERMINAL_HOST_GENERATION = 1;
|
|
6
|
+
export class TerminalHostArtifactError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(message, code) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "TerminalHostArtifactError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Verifies a release's terminal host and its session-writer lock against their manifest before
|
|
16
|
+
* anything executes or loads them: schema, generation, platform, architecture, file names, sizes,
|
|
17
|
+
* and SHA-256 digests.
|
|
18
|
+
*/
|
|
19
|
+
export async function verifyTerminalHostArtifact(executable, platform = process.platform, architecture = process.arch, generation = TERMINAL_HOST_GENERATION) {
|
|
20
|
+
const directory = dirname(executable);
|
|
21
|
+
let manifest;
|
|
22
|
+
try {
|
|
23
|
+
manifest = JSON.parse(await readFile(resolve(directory, TERMINAL_HOST_MANIFEST_FILENAME), "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw new TerminalHostArtifactError(`terminal host manifest is missing or malformed: ${errorMessage(error)}`, "TERMINAL_HOST_ARTIFACT_MISSING");
|
|
27
|
+
}
|
|
28
|
+
if (manifest.schema !== "a1-terminal-host-artifact-v1" || manifest.protocolVersion !== 1 || manifest.generation !== generation
|
|
29
|
+
|| manifest.platform !== platform || manifest.architecture !== architecture
|
|
30
|
+
|| manifest.artifact?.filename !== basename(executable) || typeof manifest.sessionLock?.filename !== "string"
|
|
31
|
+
|| basename(manifest.sessionLock.filename) !== manifest.sessionLock.filename) {
|
|
32
|
+
throw new TerminalHostArtifactError("terminal host manifest is incompatible with this platform or generation", "TERMINAL_HOST_ARTIFACT_INCOMPATIBLE");
|
|
33
|
+
}
|
|
34
|
+
if (manifest.capability !== "supported") {
|
|
35
|
+
throw new TerminalHostArtifactError(`resident tabs are not supported on ${platform}-${architecture}`, "TERMINAL_HOST_ARTIFACT_UNSUPPORTED");
|
|
36
|
+
}
|
|
37
|
+
const sessionLock = resolve(directory, manifest.sessionLock.filename);
|
|
38
|
+
await verifyFile(executable, manifest.artifact, "terminal host");
|
|
39
|
+
await verifyFile(sessionLock, manifest.sessionLock, "session-writer lock");
|
|
40
|
+
return { executable, sessionLock, generation: manifest.generation };
|
|
41
|
+
}
|
|
42
|
+
async function verifyFile(path, expected, label) {
|
|
43
|
+
const [bytes, metadata] = await Promise.all([readFile(path), stat(path)]).catch((error) => {
|
|
44
|
+
throw new TerminalHostArtifactError(`${label} artifact is unavailable: ${errorMessage(error)}`, "TERMINAL_HOST_ARTIFACT_MISSING");
|
|
45
|
+
});
|
|
46
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
47
|
+
if (metadata.size !== expected.size || digest !== expected.sha256) {
|
|
48
|
+
throw new TerminalHostArtifactError(`${label} artifact integrity verification failed`, "TERMINAL_HOST_ARTIFACT_TAMPERED");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function errorMessage(error) {
|
|
52
|
+
return error instanceof Error ? error.message : String(error);
|
|
53
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ZipEntry {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly data: Buffer;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Encodes a small deflated zip archive. Names are forward-slash relative paths; there are no
|
|
7
|
+
* directories, extra fields, or encryption, so any standard unzip tool reads it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function encodeZip(entries: readonly ZipEntry[], modified?: Date): Buffer;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { crc32, deflateRawSync } from "node:zlib";
|
|
2
|
+
/**
|
|
3
|
+
* Encodes a small deflated zip archive. Names are forward-slash relative paths; there are no
|
|
4
|
+
* directories, extra fields, or encryption, so any standard unzip tool reads it.
|
|
5
|
+
*/
|
|
6
|
+
export function encodeZip(entries, modified = new Date()) {
|
|
7
|
+
const time = ((modified.getHours() << 11) | (modified.getMinutes() << 5) | Math.floor(modified.getSeconds() / 2)) & 0xffff;
|
|
8
|
+
const date = (((modified.getFullYear() - 1980) << 9) | ((modified.getMonth() + 1) << 5) | modified.getDate()) & 0xffff;
|
|
9
|
+
const locals = [];
|
|
10
|
+
const centrals = [];
|
|
11
|
+
let offset = 0;
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const segments = entry.name.split("/");
|
|
14
|
+
if (segments.some(segment => !/^[A-Za-z0-9._-]+$/u.test(segment) || segment === "." || segment === "..")) {
|
|
15
|
+
throw new Error(`zip entry name is unsafe: ${entry.name}`);
|
|
16
|
+
}
|
|
17
|
+
const name = Buffer.from(entry.name, "utf8");
|
|
18
|
+
const compressed = deflateRawSync(entry.data);
|
|
19
|
+
const checksum = crc32(entry.data);
|
|
20
|
+
const local = Buffer.alloc(30);
|
|
21
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
22
|
+
local.writeUInt16LE(20, 4);
|
|
23
|
+
local.writeUInt16LE(0x0800, 6);
|
|
24
|
+
local.writeUInt16LE(8, 8);
|
|
25
|
+
local.writeUInt16LE(time, 10);
|
|
26
|
+
local.writeUInt16LE(date, 12);
|
|
27
|
+
local.writeUInt32LE(checksum, 14);
|
|
28
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
29
|
+
local.writeUInt32LE(entry.data.length, 22);
|
|
30
|
+
local.writeUInt16LE(name.length, 26);
|
|
31
|
+
locals.push(local, name, compressed);
|
|
32
|
+
const central = Buffer.alloc(46);
|
|
33
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
34
|
+
central.writeUInt16LE(20, 4);
|
|
35
|
+
central.writeUInt16LE(20, 6);
|
|
36
|
+
central.writeUInt16LE(0x0800, 8);
|
|
37
|
+
central.writeUInt16LE(8, 10);
|
|
38
|
+
central.writeUInt16LE(time, 12);
|
|
39
|
+
central.writeUInt16LE(date, 14);
|
|
40
|
+
central.writeUInt32LE(checksum, 16);
|
|
41
|
+
central.writeUInt32LE(compressed.length, 20);
|
|
42
|
+
central.writeUInt32LE(entry.data.length, 24);
|
|
43
|
+
central.writeUInt16LE(name.length, 28);
|
|
44
|
+
central.writeUInt32LE(offset, 42);
|
|
45
|
+
centrals.push(central, name);
|
|
46
|
+
offset += local.length + name.length + compressed.length;
|
|
47
|
+
}
|
|
48
|
+
const directory = Buffer.concat(centrals);
|
|
49
|
+
const end = Buffer.alloc(22);
|
|
50
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
51
|
+
end.writeUInt16LE(entries.length, 8);
|
|
52
|
+
end.writeUInt16LE(entries.length, 10);
|
|
53
|
+
end.writeUInt32LE(directory.length, 12);
|
|
54
|
+
end.writeUInt32LE(offset, 16);
|
|
55
|
+
return Buffer.concat([...locals, directory, end]);
|
|
56
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
export interface NativeLock {
|
|
2
3
|
readonly bound: boolean;
|
|
3
4
|
bindCreated(path: string): void;
|
|
@@ -18,6 +19,10 @@ export interface SessionWriterHandle {
|
|
|
18
19
|
bind(createdPath?: string): void;
|
|
19
20
|
release(): void;
|
|
20
21
|
}
|
|
22
|
+
/** Development-only override naming a native lock addon to load first. */
|
|
23
|
+
export declare const SESSION_LOCK_OVERRIDE_ENVIRONMENT = "A1_SESSION_LOCK_PATH";
|
|
24
|
+
/** Security: a release that carries its own lock never loads an addon chosen by the environment or cwd. */
|
|
25
|
+
export declare function nativeCandidates(moduleUrl?: string, env?: NodeJS.ProcessEnv, cwd?: string, exists?: typeof existsSync): string[];
|
|
21
26
|
export declare function acquireExisting(path: string): SessionWriterHandle;
|
|
22
27
|
export declare function reserveNew(parent: string, name: string): SessionWriterHandle;
|
|
23
28
|
export declare function acquireSessionWriter(path: string): SessionWriterHandle;
|
|
@@ -2,7 +2,9 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync,
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
7
|
+
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
6
8
|
/** Reports that a canonical session identity already has a live writer. */
|
|
7
9
|
export class SessionWriterConflict extends Error {
|
|
8
10
|
path;
|
|
@@ -16,14 +18,16 @@ export class SessionWriterConflict extends Error {
|
|
|
16
18
|
}
|
|
17
19
|
let backendOverride;
|
|
18
20
|
let cachedBackend;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
/** Development-only override naming a native lock addon to load first. */
|
|
22
|
+
export const SESSION_LOCK_OVERRIDE_ENVIRONMENT = "A1_SESSION_LOCK_PATH";
|
|
23
|
+
/** Security: a release that carries its own lock never loads an addon chosen by the environment or cwd. */
|
|
24
|
+
export function nativeCandidates(moduleUrl = import.meta.url, env = process.env, cwd = process.cwd(), exists = existsSync) {
|
|
25
|
+
const addon = "a1_session_lock.node";
|
|
26
|
+
// Invariant: this module sits at dist/foundation/session-writer-lock/ inside the release root.
|
|
27
|
+
const own = resolve(fileURLToPath(new URL("../../../", moduleUrl)), "dist", "native", `${process.platform}-${process.arch}`, addon);
|
|
28
|
+
const override = env[PRODUCT_IDENTITY.environment.developmentInstanceId] ? env[SESSION_LOCK_OVERRIDE_ENVIRONMENT]?.trim() || undefined : undefined;
|
|
29
|
+
const local = exists(own) ? [own] : [resolve(cwd, "dist", "native", `${process.platform}-${process.arch}`, addon), resolve(cwd, ".artifacts", "terminal-host", addon)];
|
|
30
|
+
return override ? [override, ...local] : local;
|
|
27
31
|
}
|
|
28
32
|
function loadNativeBackend() {
|
|
29
33
|
const require = createRequire(import.meta.url);
|
|
@@ -50,7 +54,7 @@ function processIsLive(pid) {
|
|
|
50
54
|
return false;
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
|
-
/** Development fallback.
|
|
57
|
+
/** Development fallback. Installed releases carry the verified Node-API lock resolved above. */
|
|
54
58
|
function portableBackend() {
|
|
55
59
|
const root = join(tmpdir(), "a1-session-writer-locks");
|
|
56
60
|
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
@@ -4,5 +4,5 @@ import { type PiShellViewComponentPort, type PiShellStatusPort, type PiShellProg
|
|
|
4
4
|
export declare function createPiShellHeader(options?: PiShellHeaderOptions): PiShellHeaderPort;
|
|
5
5
|
export declare function createPiShellLoadedResources(resources: readonly PiShellResourceEntry[], initialExpanded?: boolean): PiShellLoadedResourcesPort;
|
|
6
6
|
export declare function createPiShellStatus(view: OwnedUiSessionViewModel, progressStatus: PiShellProgressStatusPresentation, runtime?: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">): PiShellStatusPort;
|
|
7
|
-
export declare function createPiShellFooter(view: OwnedUiSessionViewModel, cwd: string, profile?: "pi" | "a1", showLevel?: () => boolean): PiShellViewComponentPort;
|
|
7
|
+
export declare function createPiShellFooter(view: OwnedUiSessionViewModel, cwd: string, profile?: "pi" | "a1", showLevel?: () => boolean, showSessionName?: boolean): PiShellViewComponentPort;
|
|
8
8
|
export declare function createPiQueuedInputStatus(submissions: readonly string[], presentation?: "pinned" | "custom-viewport", getKeybindings?: () => KeybindingsConfig): PiShellQueuedInputPort;
|
|
@@ -136,9 +136,9 @@ export function createPiShellStatus(view, progressStatus, runtime) {
|
|
|
136
136
|
},
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
|
-
export function createPiShellFooter(view, cwd, profile = "pi", showLevel = () => true) {
|
|
139
|
+
export function createPiShellFooter(view, cwd, profile = "pi", showLevel = () => true, showSessionName = true) {
|
|
140
140
|
ensureTheme();
|
|
141
|
-
const footer = new SessionFooter(() => view, cwd, profile, showLevel);
|
|
141
|
+
const footer = new SessionFooter(() => view, cwd, profile, showLevel, showSessionName);
|
|
142
142
|
return {
|
|
143
143
|
render: width => footer.render(width),
|
|
144
144
|
invalidate: () => footer.invalidate(),
|
|
@@ -6,7 +6,7 @@ import { hotkeysMarkdown } from "./shell-presenters-info.js";
|
|
|
6
6
|
export function renderPiShellHotkeySections(presentation, width) {
|
|
7
7
|
ensureTheme();
|
|
8
8
|
const shortcuts = presentation.getShortcuts ?? (() => []);
|
|
9
|
-
const markdown = hotkeysMarkdown(presentation.bindings, shortcuts, presentation.profile ?? "pi");
|
|
9
|
+
const markdown = hotkeysMarkdown(presentation.bindings, shortcuts, presentation.profile ?? "pi", presentation.residentTabs);
|
|
10
10
|
const sections = [];
|
|
11
11
|
for (const line of markdown.split("\n")) {
|
|
12
12
|
const heading = /^\*\*(.+)\*\*$/.exec(line)?.[1];
|
|
@@ -68,11 +68,13 @@ export interface PiShellHotkeysPresentation {
|
|
|
68
68
|
readonly bindings?: KeybindingsConfig;
|
|
69
69
|
readonly getShortcuts?: NonNullable<PiShellExtensionRendererResolver["getShortcuts"]>;
|
|
70
70
|
readonly profile?: "pi" | "a1";
|
|
71
|
+
readonly residentTabs?: boolean;
|
|
71
72
|
}
|
|
72
|
-
export declare function createPiShellHotkeys(bindings?: KeybindingsConfig, getShortcuts?: NonNullable<PiShellExtensionRendererResolver["getShortcuts"]>, profile?: "pi" | "a1"): PiShellComponentPort;
|
|
73
|
+
export declare function createPiShellHotkeys(bindings?: KeybindingsConfig, getShortcuts?: NonNullable<PiShellExtensionRendererResolver["getShortcuts"]>, profile?: "pi" | "a1", residentTabs?: boolean): PiShellComponentPort;
|
|
73
74
|
/** The keyboard-shortcut document rows the feed presenter shows, without its spacer, borders, and heading. */
|
|
74
75
|
export declare function renderPiShellHotkeysLines(presentation: PiShellHotkeysPresentation, width: number): readonly string[];
|
|
75
|
-
export declare
|
|
76
|
+
export declare const TAB_HOTKEYS: readonly (readonly [string, string | null, string])[];
|
|
77
|
+
export declare function hotkeysMarkdown(bindings: KeybindingsConfig | undefined, getShortcuts: NonNullable<PiShellExtensionRendererResolver["getShortcuts"]>, profile: "pi" | "a1", residentTabs?: boolean): string;
|
|
76
78
|
/**
|
|
77
79
|
* Pinned Pi's `showPackageUpdateNotification` banner: warning-coloured dynamic
|
|
78
80
|
* borders around a bold warning title, the muted update instruction with the
|
|
@@ -147,9 +147,9 @@ function shortcutDisplay(key) {
|
|
|
147
147
|
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
148
148
|
}).join("+")).join("/");
|
|
149
149
|
}
|
|
150
|
-
export function createPiShellHotkeys(bindings, getShortcuts = () => [], profile = "pi") {
|
|
150
|
+
export function createPiShellHotkeys(bindings, getShortcuts = () => [], profile = "pi", residentTabs = false) {
|
|
151
151
|
ensureTheme();
|
|
152
|
-
const markdown = hotkeysMarkdown(bindings, getShortcuts, profile);
|
|
152
|
+
const markdown = hotkeysMarkdown(bindings, getShortcuts, profile, residentTabs);
|
|
153
153
|
const container = new Container();
|
|
154
154
|
container.addChild(new Spacer(1));
|
|
155
155
|
container.addChild(new DynamicBorder(paintPiBorder));
|
|
@@ -162,12 +162,20 @@ export function createPiShellHotkeys(bindings, getShortcuts = () => [], profile
|
|
|
162
162
|
/** The keyboard-shortcut document rows the feed presenter shows, without its spacer, borders, and heading. */
|
|
163
163
|
export function renderPiShellHotkeysLines(presentation, width) {
|
|
164
164
|
ensureTheme();
|
|
165
|
-
return hotkeysMarkdownComponent(hotkeysMarkdown(presentation.bindings, presentation.getShortcuts ?? (() => []), presentation.profile ?? "pi")).render(width);
|
|
165
|
+
return hotkeysMarkdownComponent(hotkeysMarkdown(presentation.bindings, presentation.getShortcuts ?? (() => []), presentation.profile ?? "pi", presentation.residentTabs)).render(width);
|
|
166
166
|
}
|
|
167
167
|
function hotkeysMarkdownComponent(markdown) {
|
|
168
168
|
return new Markdown(markdown, 1, 1, getPiHotkeysMarkdownTheme());
|
|
169
169
|
}
|
|
170
|
-
|
|
170
|
+
// Performance: keep startup presentation independent from the registry; parity is contract-tested.
|
|
171
|
+
export const TAB_HOTKEYS = [
|
|
172
|
+
["tabs.new", "alt+a", "New A1 tab"], ["tabs.close", "alt+w", "Close A1 tab"],
|
|
173
|
+
["tabs.rename", "f2", "Rename A1 tab"],
|
|
174
|
+
...Array.from({ length: 10 }, (_, index) => [`tabs.jump${index + 1}`, `alt+${(index + 1) % 10}`, `View A1 tab ${index + 1}`]),
|
|
175
|
+
["tabs.next", null, "View next A1 tab"], ["tabs.previous", null, "View previous A1 tab"],
|
|
176
|
+
["tabs.moveRight", null, "Move A1 tab right"], ["tabs.moveLeft", null, "Move A1 tab left"],
|
|
177
|
+
];
|
|
178
|
+
export function hotkeysMarkdown(bindings, getShortcuts, profile, residentTabs = false) {
|
|
171
179
|
const keys = profile === "a1" ? KeybindingsManager.fromOwnedBindings(bindings) : new KeybindingsManager(bindings);
|
|
172
180
|
// Compatibility: the owned viewport consumes these physical chords before editor actions.
|
|
173
181
|
const display = (action) => keys.getKeys(action)
|
|
@@ -180,6 +188,32 @@ export function hotkeysMarkdown(bindings, getShortcuts, profile) {
|
|
|
180
188
|
: `\`${label}\``;
|
|
181
189
|
}).join(" / ")} | ${description} |`;
|
|
182
190
|
let markdown = ["**Navigation**", "| Key | Action |", "|-----|--------|", row(["tui.editor.cursorUp", "tui.editor.cursorDown", "tui.editor.cursorLeft", "tui.editor.cursorRight"], "Move cursor / browse history"), row(["tui.editor.cursorWordLeft", "tui.editor.cursorWordRight"], "Move by word"), row(["tui.editor.cursorLineStart"], profile === "a1" ? "Start of prompt line" : "Start of line"), row(["tui.editor.cursorLineEnd"], profile === "a1" ? "End of prompt line" : "End of line"), ...(profile === "a1" ? ["| `Ctrl+Home` | Start of content |", "| `Ctrl+End` | End of content / follow output |"] : []), row(["tui.editor.jumpForward"], "Jump forward to character"), row(["tui.editor.jumpBackward"], "Jump backward to character"), row(["tui.editor.pageUp", "tui.editor.pageDown"], "Scroll by page"), "", "**Editing**", "| Key | Action |", "|-----|--------|", row(["tui.input.submit"], "Send message"), row(["tui.input.newLine"], `New line${process.platform === "win32" ? " (Ctrl+Enter on Windows Terminal)" : ""}`), row(["tui.editor.deleteWordBackward"], "Delete word backwards"), row(["tui.editor.deleteWordForward"], "Delete word forwards"), row(["tui.editor.deleteToLineStart"], "Delete to start of line"), row(["tui.editor.deleteToLineEnd"], "Delete to end of line"), row(["tui.editor.yank"], "Paste the most-recently-deleted text"), row(["tui.editor.yankPop"], "Cycle through the deleted text after pasting"), row(["tui.editor.undo"], "Undo"), "", "**Other**", "| Key | Action |", "|-----|--------|", row(["tui.input.tab"], "Path completion / accept autocomplete"), row(["app.interrupt"], "Cancel autocomplete / abort streaming"), row(["app.clear"], "Clear editor (first) / exit (second)"), row(["app.exit"], "Exit (when editor is empty)"), row(["app.suspend"], "Suspend to background"), row(["app.thinking.cycle"], "Cycle thinking level"), row(["app.model.cycleForward", "app.model.cycleBackward"], "Cycle models"), row(["app.model.select"], profile === "a1" ? "Open the Models dialog" : "Open model selector"), row(["app.tools.expand"], "Toggle tool output expansion"), row(["app.thinking.toggle"], "Toggle thinking block visibility"), row(["app.editor.external"], "Edit message in external editor"), row(["app.message.copy"], "Copy selection or last assistant message"), row(["app.message.followUp"], "Queue follow-up message"), row(["app.message.dequeue"], "Restore queued messages"), row(["app.clipboard.pasteImage"], profile === "a1" ? "Paste image or text from clipboard" : "Paste files on macOS, images, or text from clipboard"), "| `/` | Slash commands |", "| `!` | Run bash command |", "| `!!` | Run bash command (excluded from context) |", ...(profile === "a1" ? ["", "**Models dialog**", "| Key | Action |", "|-----|--------|", "| `Space` | Toggle the selected model in the cycling scope |", "| `Tab` | Switch the all/scoped filter |", row(["app.models.save"], "Save the scope to settings"), row(["app.models.enableAll"], "Scope every listed model"), row(["app.models.clearAll"], "Clear the listed models from the scope"), row(["app.models.toggleProvider"], "Toggle the selected model's provider"), row(["app.models.reorderUp", "app.models.reorderDown"], "Reorder the cycling scope")] : [])].join("\n");
|
|
191
|
+
if (profile === "a1" && residentTabs) {
|
|
192
|
+
const configured = (bindings ?? {});
|
|
193
|
+
markdown += "\n\n**Tabs**\n| Key | Action |\n|-----|--------|\n";
|
|
194
|
+
const claimed = new Set();
|
|
195
|
+
for (const [action, value] of Object.entries(configured)) {
|
|
196
|
+
if (action.startsWith("tabs."))
|
|
197
|
+
continue;
|
|
198
|
+
const keys = typeof value === "string" ? [value] : Array.isArray(value) ? value : [];
|
|
199
|
+
for (const key of keys)
|
|
200
|
+
if (typeof key === "string")
|
|
201
|
+
claimed.add(key.toLowerCase());
|
|
202
|
+
}
|
|
203
|
+
markdown += TAB_HOTKEYS.map(([id, defaultKey, description]) => {
|
|
204
|
+
const value = configured[id];
|
|
205
|
+
const assigned = (typeof value === "string" ? [value] : Array.isArray(value) ? value.filter((key) => typeof key === "string") : defaultKey === null ? [] : [defaultKey])
|
|
206
|
+
.filter(key => {
|
|
207
|
+
const normalized = key.toLowerCase();
|
|
208
|
+
if (claimed.has(normalized))
|
|
209
|
+
return false;
|
|
210
|
+
claimed.add(normalized);
|
|
211
|
+
return true;
|
|
212
|
+
});
|
|
213
|
+
const label = assigned.length === 0 ? "Unbound" : `\`${assigned.map(shortcutDisplay).join("/")}\``;
|
|
214
|
+
return `| ${label} | ${description} |`;
|
|
215
|
+
}).join("\n");
|
|
216
|
+
}
|
|
183
217
|
const shortcuts = getShortcuts(bindings ?? keys.getEffectiveConfig());
|
|
184
218
|
if (shortcuts.length > 0) {
|
|
185
219
|
markdown += "\n\n**Extensions**\n| Key | Action |\n|-----|--------|\n";
|
|
@@ -5,7 +5,8 @@ export declare class SessionFooter implements Component {
|
|
|
5
5
|
private readonly cwd;
|
|
6
6
|
private readonly profile;
|
|
7
7
|
private readonly show;
|
|
8
|
-
|
|
8
|
+
private readonly showSessionName;
|
|
9
|
+
constructor(getView: () => OwnedUiSessionViewModel, cwd: string, profile?: "pi" | "a1", show?: () => boolean, showSessionName?: boolean);
|
|
9
10
|
invalidate(): void;
|
|
10
11
|
dispose(): void;
|
|
11
12
|
render(width: number): string[];
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* preserves the latest physical route for virtual models; an explicit bare-A1 profile colors the
|
|
6
6
|
* thinking-level name, renders its repository-context path, and reserves its linked pull-request badge
|
|
7
7
|
* before path truncation while preserving the remaining footer layout and formatting; replacement
|
|
8
|
-
* surfaces may suppress a thinking level they already present
|
|
9
|
-
*
|
|
8
|
+
* surfaces may suppress a thinking level they already present; a resident tab whose chip already shows
|
|
9
|
+
* the session name may suppress its path-row suffix.
|
|
10
|
+
* Deviations: owned-status-level-color, owned-pull-request-badge, owned-resident-session-name.
|
|
10
11
|
*/
|
|
11
12
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
12
13
|
import { hyperlink, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
@@ -16,11 +17,13 @@ export class SessionFooter {
|
|
|
16
17
|
cwd;
|
|
17
18
|
profile;
|
|
18
19
|
show;
|
|
19
|
-
|
|
20
|
+
showSessionName;
|
|
21
|
+
constructor(getView, cwd, profile = "pi", show = () => true, showSessionName = true) {
|
|
20
22
|
this.getView = getView;
|
|
21
23
|
this.cwd = cwd;
|
|
22
24
|
this.profile = profile;
|
|
23
25
|
this.show = show;
|
|
26
|
+
this.showSessionName = showSessionName;
|
|
24
27
|
}
|
|
25
28
|
invalidate() { }
|
|
26
29
|
dispose() { }
|
|
@@ -42,7 +45,7 @@ export class SessionFooter {
|
|
|
42
45
|
const branch = view.status.footer?.branch;
|
|
43
46
|
if (branch)
|
|
44
47
|
pwd = `${pwd} (${branch})`;
|
|
45
|
-
const sessionName = view.status.footer?.sessionName;
|
|
48
|
+
const sessionName = this.showSessionName ? view.status.footer?.sessionName : null;
|
|
46
49
|
const pullRequest = this.profile === "a1" ? view.status.footer?.pullRequest : null;
|
|
47
50
|
let pathRow;
|
|
48
51
|
if (pullRequest === undefined || pullRequest === null) {
|
|
@@ -98,6 +98,7 @@ export declare class PiEngineAdapter implements OwnedUiSessionBackend, OwnedUiPr
|
|
|
98
98
|
announceReleaseUpdate(release: OwnedUiReleaseUpdate): void;
|
|
99
99
|
suggestionReasoningPolicy(): OwnedUiPromptSuggestionReasoning;
|
|
100
100
|
generate(request: OwnedUiPromptSuggestionRequest): Promise<OwnedUiPromptSuggestionResult>;
|
|
101
|
+
generateResidentTabName(instruction: string, maxTokens: number, signal: AbortSignal): Promise<string | null>;
|
|
101
102
|
start(): Promise<OwnedUiSessionViewModel>;
|
|
102
103
|
/** Announce pinned Pi's changelog since the profile's last seen version; the host calls this once per process. */
|
|
103
104
|
announceStartupChangelog(): Promise<void>;
|
|
@@ -280,6 +280,9 @@ export class PiEngineAdapter {
|
|
|
280
280
|
generate(request) {
|
|
281
281
|
return this.#suggestions.generate(request);
|
|
282
282
|
}
|
|
283
|
+
generateResidentTabName(instruction, maxTokens, signal) {
|
|
284
|
+
return this.#suggestions.completeInstruction(instruction, maxTokens, signal);
|
|
285
|
+
}
|
|
283
286
|
async start() {
|
|
284
287
|
if (this.#engine.started)
|
|
285
288
|
return this.view();
|
|
@@ -26,5 +26,6 @@ export declare class PiPromptSuggestions implements OwnedUiPromptSuggestionGener
|
|
|
26
26
|
#private;
|
|
27
27
|
constructor(ports: PiPromptSuggestionPorts);
|
|
28
28
|
reasoningPolicy(): OwnedUiPromptSuggestionReasoning;
|
|
29
|
+
completeInstruction(instruction: string, maxTokens: number, signal: AbortSignal): Promise<string | null>;
|
|
29
30
|
generate(request: OwnedUiPromptSuggestionRequest): Promise<OwnedUiPromptSuggestionResult>;
|
|
30
31
|
}
|
|
@@ -20,6 +20,46 @@ export class PiPromptSuggestions {
|
|
|
20
20
|
// Performance: the run's own level keeps the provider's thinking parameters, and the cached prefix, identical.
|
|
21
21
|
return readSuggestionReasoning(session.thinkingLevel);
|
|
22
22
|
}
|
|
23
|
+
async completeInstruction(instruction, maxTokens, signal) {
|
|
24
|
+
const session = this.#ports.session();
|
|
25
|
+
const runtime = this.#ports.runtime();
|
|
26
|
+
if (this.#ports.unavailable() || session === undefined || runtime === undefined || signal.aborted
|
|
27
|
+
|| session.model === undefined || typeof runtime.services.modelRuntime.completeSimple !== "function")
|
|
28
|
+
return null;
|
|
29
|
+
const agent = session.agent;
|
|
30
|
+
try {
|
|
31
|
+
const transformed = typeof agent.transformContext === "function"
|
|
32
|
+
? await agent.transformContext(agent.state.messages, signal)
|
|
33
|
+
: agent.state.messages;
|
|
34
|
+
const messages = typeof agent.convertToLlm === "function"
|
|
35
|
+
? await agent.convertToLlm(transformed)
|
|
36
|
+
: transformed.filter(message => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
|
|
37
|
+
const response = await runtime.services.modelRuntime.completeSimple(session.model, {
|
|
38
|
+
systemPrompt: agent.state.systemPrompt,
|
|
39
|
+
messages: [...messages, { role: "user", content: instruction, timestamp: Date.now() }],
|
|
40
|
+
tools: [],
|
|
41
|
+
}, {
|
|
42
|
+
signal,
|
|
43
|
+
maxTokens,
|
|
44
|
+
...(agent.sessionId === undefined ? {} : { sessionId: agent.sessionId }),
|
|
45
|
+
...(agent.transport === undefined ? {} : { transport: agent.transport }),
|
|
46
|
+
...(agent.onPayload === undefined ? {} : { onPayload: agent.onPayload }),
|
|
47
|
+
});
|
|
48
|
+
if (signal.aborted || !isRecord(response) || response.stopReason === "aborted"
|
|
49
|
+
|| response.stopReason === "error" || response.stopReason === "length"
|
|
50
|
+
|| stringValue(response.errorMessage) !== undefined)
|
|
51
|
+
return null;
|
|
52
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
53
|
+
if (content.some(block => isRecord(block) && block.type === "toolCall"))
|
|
54
|
+
return null;
|
|
55
|
+
const text = content.filter(block => isRecord(block) && block.type === "text")
|
|
56
|
+
.map(block => stringValue(block.text) ?? "").join("\n").trim();
|
|
57
|
+
return text || null;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
23
63
|
async generate(request) {
|
|
24
64
|
assertOwnedUiPromptSuggestionRequest(request);
|
|
25
65
|
const identity = request.identity;
|
|
Binary file
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-10-
|
|
8
|
+
"builtAt": "2026-10-16T04:40:04.241Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "47e9052599983a6bb0078ce6203ba18d71bec0b7b94e6a49019d11d46c3fbeca",
|
|
12
12
|
"size": 410368
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
Binary file
|