@timurproko/a1 0.2.7-dev.767 → 0.2.7-dev.772

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.
Files changed (115) hide show
  1. package/README.md +30 -0
  2. package/bin/cli.js +4 -0
  3. package/bin/tab-launch-context.d.ts +3 -0
  4. package/bin/tab-launch-context.js +52 -3
  5. package/bin/ui.js +6 -3
  6. package/dist/app/session-shell/session-shell-root.d.ts +0 -1
  7. package/dist/app/session-shell/session-shell-root.js +1 -1
  8. package/dist/app/session-shell/session-shell.js +3 -8
  9. package/dist/app/session-shell/shell-host.d.ts +2 -0
  10. package/dist/app/session-shell/shell-host.js +20 -1
  11. package/dist/cli/dispatch.d.ts +11 -1
  12. package/dist/cli/dispatch.js +79 -5
  13. package/dist/cli/tabs-request.d.ts +7 -0
  14. package/dist/cli/tabs-request.js +1 -0
  15. package/dist/cli/tabs.d.ts +14 -0
  16. package/dist/cli/tabs.js +140 -0
  17. package/dist/composition/owned-ui.d.ts +8 -2
  18. package/dist/composition/owned-ui.js +109 -12
  19. package/dist/features/launch/index.d.ts +1 -1
  20. package/dist/features/launch/intent.d.ts +3 -1
  21. package/dist/features/launch/intent.js +7 -4
  22. package/dist/features/resident-tab/auto-name.d.ts +11 -0
  23. package/dist/features/resident-tab/auto-name.js +42 -0
  24. package/dist/features/resident-tab/index.d.ts +3 -0
  25. package/dist/features/resident-tab/index.js +3 -0
  26. package/dist/features/resident-tab/shortcuts.d.ts +10 -0
  27. package/dist/features/resident-tab/shortcuts.js +35 -0
  28. package/dist/features/resident-tab/status-deriver.d.ts +9 -0
  29. package/dist/features/resident-tab/status-deriver.js +15 -0
  30. package/dist/foundation/launch-context/index.d.ts +4 -0
  31. package/dist/foundation/launch-context/index.js +10 -0
  32. package/dist/foundation/release/bootstrap.d.ts +4 -2
  33. package/dist/foundation/release/bootstrap.js +11 -8
  34. package/dist/foundation/release/cohort-state.d.ts +1 -1
  35. package/dist/foundation/release/cohort-state.js +1 -1
  36. package/dist/foundation/release/release-gc.d.ts +3 -0
  37. package/dist/foundation/release/release-gc.js +44 -8
  38. package/dist/foundation/resident-tabs/bounded-host-command.d.ts +16 -0
  39. package/dist/foundation/resident-tabs/bounded-host-command.js +35 -0
  40. package/dist/foundation/resident-tabs/bridge-client.d.ts +18 -3
  41. package/dist/foundation/resident-tabs/bridge-client.js +87 -7
  42. package/dist/foundation/resident-tabs/doctor.d.ts +29 -0
  43. package/dist/foundation/resident-tabs/doctor.js +166 -0
  44. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +2 -1
  45. package/dist/foundation/resident-tabs/ensure-terminal-host.js +33 -7
  46. package/dist/foundation/resident-tabs/index.d.ts +7 -1
  47. package/dist/foundation/resident-tabs/index.js +4 -1
  48. package/dist/foundation/resident-tabs/maintenance.d.ts +42 -0
  49. package/dist/foundation/resident-tabs/maintenance.js +78 -0
  50. package/dist/foundation/resident-tabs/release-holds.d.ts +27 -0
  51. package/dist/foundation/resident-tabs/release-holds.js +77 -0
  52. package/dist/foundation/resident-tabs/routing.d.ts +4 -3
  53. package/dist/foundation/resident-tabs/routing.js +6 -4
  54. package/dist/foundation/resident-tabs/settings.d.ts +7 -2
  55. package/dist/foundation/resident-tabs/settings.js +17 -16
  56. package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +18 -0
  57. package/dist/foundation/resident-tabs/terminal-host-artifact.js +53 -0
  58. package/dist/foundation/resident-tabs/zip-archive.d.ts +9 -0
  59. package/dist/foundation/resident-tabs/zip-archive.js +56 -0
  60. package/dist/foundation/session-writer-lock/index.d.ts +5 -0
  61. package/dist/foundation/session-writer-lock/index.js +13 -9
  62. package/dist/integrations/pi/components/shell-hotkey-sections.js +1 -1
  63. package/dist/integrations/pi/components/shell-presenters-info.d.ts +4 -2
  64. package/dist/integrations/pi/components/shell-presenters-info.js +38 -4
  65. package/dist/integrations/pi/engine/adapter.d.ts +1 -0
  66. package/dist/integrations/pi/engine/adapter.js +3 -0
  67. package/dist/integrations/pi/engine/prompt-suggestions.d.ts +1 -0
  68. package/dist/integrations/pi/engine/prompt-suggestions.js +40 -0
  69. package/dist/native/darwin-arm64/a1_session_lock.node +0 -0
  70. package/dist/native/darwin-arm64/manifest.json +1 -1
  71. package/dist/native/darwin-arm64/terminal-host +0 -0
  72. package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +10500 -0
  73. package/dist/native/darwin-arm64/terminal-host.manifest.json +44 -0
  74. package/dist/native/darwin-x64/a1_session_lock.node +0 -0
  75. package/dist/native/darwin-x64/manifest.json +20 -0
  76. package/dist/native/darwin-x64/process-guardian +0 -0
  77. package/dist/native/darwin-x64/terminal-host +0 -0
  78. package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10737 -0
  79. package/dist/native/darwin-x64/terminal-host.manifest.json +44 -0
  80. package/dist/native/linux-x64/a1_session_lock.node +0 -0
  81. package/dist/native/linux-x64/manifest.json +1 -1
  82. package/dist/native/linux-x64/terminal-host +0 -0
  83. package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +11226 -0
  84. package/dist/native/linux-x64/terminal-host.manifest.json +44 -0
  85. package/dist/native/win32-x64/a1_session_lock.node +0 -0
  86. package/dist/native/win32-x64/manifest.json +2 -2
  87. package/dist/native/win32-x64/process-guardian.exe +0 -0
  88. package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10757 -0
  89. package/dist/native/win32-x64/terminal-host.exe +0 -0
  90. package/dist/native/win32-x64/terminal-host.manifest.json +44 -0
  91. package/dist/ui/settings/declarations.d.ts +19 -43
  92. package/dist/ui/settings/declarations.js +19 -35
  93. package/dist/ui/settings/index.d.ts +1 -1
  94. package/dist/ui/settings/index.js +1 -1
  95. package/dist/ui/settings/manager.d.ts +1 -0
  96. package/dist/ui/settings/manager.js +9 -1
  97. package/dist/ui/settings/migrations.js +12 -0
  98. package/dist/ui/settings/resolution.d.ts +0 -2
  99. package/dist/ui/settings/resolution.js +1 -13
  100. package/dist/ui/settings/sections.js +0 -4
  101. package/docs/architecture/evidence/resident-tabs/linux-x64.md +37 -0
  102. package/docs/architecture/evidence/resident-tabs/macos-arm64.md +36 -0
  103. package/docs/architecture/evidence/resident-tabs/macos-x64.md +35 -0
  104. package/docs/architecture/evidence/resident-tabs/windows-x64.md +36 -0
  105. package/docs/architecture/internal-naming.md +2 -0
  106. package/docs/architecture/process-guardian-provenance.md +2 -0
  107. package/docs/architecture/resident-tabs-requirements.md +1 -563
  108. package/docs/architecture/resident-tabs.md +166 -94
  109. package/docs/architecture/terminal-host-provenance.md +13 -3
  110. package/docs/architecture/toolchain.md +1 -1
  111. package/docs/ci-release-runbook.md +2 -2
  112. package/docs/features/launch-profiles.md +11 -8
  113. package/docs/manual-resident-tabs-acceptance.md +51 -0
  114. package/docs/validation.md +9 -1
  115. 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
- function nativeCandidates() {
20
- const suffix = process.platform === "win32" ? "win32" : process.platform;
21
- const target = `${suffix}-${process.arch}`;
22
- return [
23
- process.env.A1_SESSION_LOCK_PATH,
24
- resolve(process.cwd(), "dist", "native", target, "a1_session_lock.node"),
25
- resolve(process.cwd(), ".artifacts", "terminal-host", "a1_session_lock.node"),
26
- ].filter((value) => typeof value === "string" && value.length > 0);
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. Packaged builds use the Node-API lock above. */
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 });
@@ -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 function hotkeysMarkdown(bindings: KeybindingsConfig | undefined, getShortcuts: NonNullable<PiShellExtensionRendererResolver["getShortcuts"]>, profile: "pi" | "a1"): string;
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
- export function hotkeysMarkdown(bindings, getShortcuts, profile) {
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";
@@ -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;
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-10-14T05:01:03.252Z",
8
+ "builtAt": "2026-10-15T16:36:26.826Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "51b0b2dc1c9ae577f2586c4217066929278726c2c15308d9144eb6f538e118ff",