@timurproko/a1 0.1.8-dev.536 → 0.1.8-dev.546

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 (82) hide show
  1. package/bin/cli.js +4 -0
  2. package/bin/update-recovery.js +25 -3
  3. package/dist/app/session-shell/session-shell-root.js +15 -5
  4. package/dist/cli/dispatch.d.ts +5 -0
  5. package/dist/cli/dispatch.js +26 -0
  6. package/dist/cli/session-context.d.ts +9 -0
  7. package/dist/cli/session-context.js +29 -0
  8. package/dist/composition/owned-ui.js +6 -2
  9. package/dist/contracts/owned-ui/model.d.ts +5 -0
  10. package/dist/contracts/owned-ui/validation.js +6 -0
  11. package/dist/contracts/presentation/index.d.ts +13 -0
  12. package/dist/contracts/presentation/index.js +30 -0
  13. package/dist/features/launch/index.d.ts +2 -0
  14. package/dist/features/launch/index.js +1 -0
  15. package/dist/features/launch/session-context.d.ts +8 -0
  16. package/dist/features/launch/session-context.js +8 -0
  17. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  18. package/dist/features/owned-ui/reference-screen-app.js +7 -8
  19. package/dist/features/owned-ui/settings-app.js +9 -5
  20. package/dist/foundation/lifecycle/index.d.ts +2 -0
  21. package/dist/foundation/lifecycle/index.js +1 -0
  22. package/dist/foundation/lifecycle/session-repository-context.d.ts +18 -0
  23. package/dist/foundation/lifecycle/session-repository-context.js +173 -0
  24. package/dist/foundation/release/index.d.ts +2 -2
  25. package/dist/foundation/release/index.js +1 -1
  26. package/dist/foundation/release/update-recovery.d.ts +7 -1
  27. package/dist/foundation/release/update-recovery.js +39 -9
  28. package/dist/foundation/release/update.d.ts +7 -0
  29. package/dist/foundation/release/update.js +115 -24
  30. package/dist/integrations/pi/components/models-dialog.js +9 -9
  31. package/dist/integrations/pi/components/shell-editor-autocomplete.js +27 -3
  32. package/dist/integrations/pi/components/shell-extension-ui.js +4 -2
  33. package/dist/integrations/pi/components/shell-footer-status.js +1 -1
  34. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -2
  35. package/dist/integrations/pi/components/skills-command.d.ts +2 -0
  36. package/dist/integrations/pi/components/skills-command.js +11 -10
  37. package/dist/integrations/pi/components/skills-dialog.js +8 -3
  38. package/dist/integrations/pi/components/theme.d.ts +4 -0
  39. package/dist/integrations/pi/components/theme.js +10 -0
  40. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +4 -4
  41. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -2
  42. package/dist/integrations/pi/components/upstream/components/extension-editor.js +9 -10
  43. package/dist/integrations/pi/components/upstream/components/extension-input.d.ts +17 -0
  44. package/dist/integrations/pi/components/upstream/components/extension-input.js +72 -0
  45. package/dist/integrations/pi/components/upstream/components/extension-selector.d.ts +15 -0
  46. package/dist/integrations/pi/components/upstream/components/extension-selector.js +92 -0
  47. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -2
  48. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +16 -15
  49. package/dist/integrations/pi/components/upstream/components/session-footer.js +25 -9
  50. package/dist/integrations/pi/components/upstream/components/session-selector.js +25 -16
  51. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +6 -4
  52. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +22 -12
  53. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +3 -2
  54. package/dist/integrations/pi/components/upstream/components/tree-selector.js +14 -12
  55. package/dist/integrations/pi/components/upstream/components/trust-selector.js +11 -5
  56. package/dist/integrations/pi/engine/adapter.d.ts +2 -1
  57. package/dist/integrations/pi/engine/adapter.js +2 -0
  58. package/dist/integrations/pi/engine/message-values.js +1 -0
  59. package/dist/integrations/pi/engine/session-runtime.d.ts +11 -2
  60. package/dist/integrations/pi/engine/session-runtime.js +136 -33
  61. package/dist/integrations/pi/engine/transcript-projection.js +35 -0
  62. package/dist/integrations/pi/startup-public.manifest.json +2 -0
  63. package/dist/native/darwin-arm64/manifest.json +1 -1
  64. package/dist/native/linux-x64/manifest.json +1 -1
  65. package/dist/native/win32-x64/manifest.json +2 -2
  66. package/dist/native/win32-x64/process-guardian.exe +0 -0
  67. package/dist/product-identity.d.ts +1 -1
  68. package/dist/product-identity.js +1 -1
  69. package/dist/product-identity.json +2 -1
  70. package/dist/ui/components/dialog-panel.d.ts +2 -1
  71. package/dist/ui/components/dialog-panel.js +6 -1
  72. package/dist/ui/components/index.d.ts +2 -0
  73. package/dist/ui/components/index.js +1 -0
  74. package/dist/ui/components/shortcut-hints.d.ts +7 -0
  75. package/dist/ui/components/shortcut-hints.js +12 -0
  76. package/dist/ui/components/shortcuts.d.ts +2 -0
  77. package/dist/ui/components/shortcuts.js +5 -2
  78. package/dist/ui/components/transcript-viewport.d.ts +1 -1
  79. package/docs/ci-release-runbook.md +9 -7
  80. package/docs/openspec-archive-automation.md +6 -0
  81. package/docs/validation.md +5 -2
  82. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -54,6 +54,10 @@ process.exitCode = await dispatchCli(process.argv.slice(2), {
54
54
  const { createPiPackagesPort } = await import("../dist/integrations/pi/engine/package-integration.js");
55
55
  return await runPackageCommand(request, { createPort: createPiPackagesPort });
56
56
  },
57
+ sessionContext: async request => {
58
+ const { runSessionContextCommand } = await import("../dist/cli/session-context.js");
59
+ return await runSessionContextCommand(request);
60
+ },
57
61
  }, {
58
62
  stdout: message => process.stdout.write(message),
59
63
  stderr: message => process.stderr.write(message),
@@ -5,7 +5,7 @@
5
5
  import { spawn } from "node:child_process";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { chmod, lstat, mkdir, open, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
8
- import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
8
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
 
11
11
  const SCHEMA = "a1-update-recovery-v1";
@@ -200,9 +200,15 @@ async function readCapsule(path) {
200
200
  const packageRoot = await realpath(value.packageRoot).catch(() => resolve(value.packageRoot));
201
201
  const expectedPackage = resolve(globalRoot, ...value.packageName.split("/"));
202
202
  if (!samePath(packageRoot, expectedPackage) || !containedBy(globalRoot, packageRoot)) throw new Error("A1 update recovery package root escapes npm global root");
203
- const expectedNpmArguments = ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${value.packageName}@${value.targetVersion}`];
204
- if (JSON.stringify(value.npmArguments) !== JSON.stringify(expectedNpmArguments)) throw new Error("A1 update recovery npm arguments are invalid");
203
+ const prefix = npmPrefixForGlobalRoot(globalRoot);
204
+ const expectedNpmArguments = ["install", "--global", "--prefix", prefix, "--loglevel=error", "--no-fund", "--no-audit", `${value.packageName}@${value.targetVersion}`];
205
+ const legacyNpmArguments = ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${value.packageName}@${value.targetVersion}`];
206
+ if (![expectedNpmArguments, legacyNpmArguments].some(arguments_ => sameArguments(value.npmArguments, arguments_))) {
207
+ throw new Error("A1 update recovery npm arguments are invalid");
208
+ }
205
209
  const launcherRoot = resolve(value.launcherRoot);
210
+ const expectedLauncherRoot = process.platform === "win32" ? prefix : resolve(prefix, "bin");
211
+ if (!samePath(launcherRoot, expectedLauncherRoot)) throw new Error("A1 update recovery launcher root is invalid");
206
212
  const expectedLaunchers = process.platform === "win32"
207
213
  ? [resolve(launcherRoot, "a1"), resolve(launcherRoot, "a1.cmd"), resolve(launcherRoot, "a1.ps1")]
208
214
  : [resolve(launcherRoot, "a1")];
@@ -306,6 +312,22 @@ async function writeJson(path, value) {
306
312
  await rename(temporary, path);
307
313
  }
308
314
 
315
+ /** @param {string} globalRoot */
316
+ function npmPrefixForGlobalRoot(globalRoot) {
317
+ const root = resolve(globalRoot);
318
+ const nodeModules = process.platform === "win32" ? basename(root).toLowerCase() === "node_modules" : basename(root) === "node_modules";
319
+ if (!nodeModules) throw new Error("A1 update recovery npm global root layout is invalid");
320
+ if (process.platform === "win32") return dirname(root);
321
+ const libraryRoot = dirname(root);
322
+ if (basename(libraryRoot) !== "lib") throw new Error("A1 update recovery npm global root layout is invalid");
323
+ return dirname(libraryRoot);
324
+ }
325
+
326
+ /** @param {string[]} left @param {string[]} right */
327
+ function sameArguments(left, right) {
328
+ return left.length === right.length && left.every((value, index) => value === right[index]);
329
+ }
330
+
309
331
  /**
310
332
  * @param {string} left
311
333
  * @param {string} right
@@ -430,7 +430,7 @@ export class OwnedUiSessionShellRoot {
430
430
  // fitting alignment, and live Working remain transient presentation chrome.
431
431
  selectableDocumentRowCount,
432
432
  ...(document.liveTailStartRow === undefined ? {} : { liveTailStartRow: document.liveTailStartRow }),
433
- bottomAlignedTailRowCount: statusRows.length,
433
+ bottomAlignedTailRowCount: steeringRows.length + statusRows.length,
434
434
  dockRows,
435
435
  promptAnchors: document.promptAnchors,
436
436
  width,
@@ -547,10 +547,14 @@ export class OwnedUiSessionShellRoot {
547
547
  };
548
548
  }
549
549
  #renderDockNotice(width) {
550
- if (this.#dockNotice === undefined)
550
+ const notice = this.#dockNotice;
551
+ if (notice === undefined)
551
552
  return [];
552
- // Compatibility: Pi pads its status text by one cell regardless of the output pad setting.
553
- return ["", ...renderPiShellStatusText(this.#dockNotice, width, PINNED_PI_LAYOUT.outputPad)];
553
+ if (notice.kind === "status") {
554
+ // Compatibility: Pi pads its status text by one cell regardless of the output pad setting.
555
+ return ["", ...renderPiShellStatusText(notice.message, width, PINNED_PI_LAYOUT.outputPad)];
556
+ }
557
+ return renderPiShellCommandMessage({ kind: notice.kind, message: notice.message }, width, this.#outputPad);
554
558
  }
555
559
  #dismissDockNotice() {
556
560
  if (this.#dockNotice === undefined)
@@ -763,7 +767,7 @@ export class OwnedUiSessionShellRoot {
763
767
  // reader's eye already is, instead of a transcript row that opens an empty session at the
764
768
  // top-left or sinks into a long feed. The pinned route keeps Pi's chat placement.
765
769
  if (this.#customViewport) {
766
- this.#dockNotice = message;
770
+ this.#dockNotice = { kind: "status", message };
767
771
  this.#invalidateChrome();
768
772
  return;
769
773
  }
@@ -788,6 +792,11 @@ export class OwnedUiSessionShellRoot {
788
792
  this.appendWorkflowStatus(message.message);
789
793
  return;
790
794
  }
795
+ if (this.#customViewport && (message.kind === "warning" || message.kind === "error")) {
796
+ this.#dockNotice = { kind: message.kind, message: message.message };
797
+ this.#invalidateChrome();
798
+ return;
799
+ }
791
800
  this.#lastWorkflowStatusId = undefined;
792
801
  const presentation = { kind: message.kind, message: message.message };
793
802
  this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
@@ -1148,6 +1157,7 @@ export class OwnedUiSessionShellRoot {
1148
1157
  status: {
1149
1158
  ...view.status,
1150
1159
  footer: {
1160
+ ...footer,
1151
1161
  branch: footer?.branch ?? null,
1152
1162
  sessionName: footer?.sessionName ?? null,
1153
1163
  availableProviderCount: footer?.availableProviderCount ?? 1,
@@ -1,6 +1,7 @@
1
1
  import { type InteractiveLaunchIntent, 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
+ import type { SessionContextRequest } from "./session-context.js";
4
5
  import { type PackageCliVerb, type PackageSyntaxDiagnostic } from "./package-messages.js";
5
6
  export type UpdateChannel = "stable" | "next";
6
7
  export interface CliHandlers {
@@ -8,6 +9,7 @@ export interface CliHandlers {
8
9
  readonly version: () => Promise<number>;
9
10
  readonly update: (channel: UpdateChannel, target?: string) => Promise<number>;
10
11
  readonly packages: (request: PackageCommandRequest) => Promise<number>;
12
+ readonly sessionContext: (request: SessionContextRequest) => Promise<number>;
11
13
  }
12
14
  export interface CliOutput {
13
15
  readonly stdout: (message: string) => void;
@@ -39,6 +41,9 @@ export type CliCommand = {
39
41
  } | {
40
42
  readonly kind: "packages";
41
43
  readonly request: PackageCommandRequest;
44
+ } | {
45
+ readonly kind: "session-context";
46
+ readonly request: SessionContextRequest;
42
47
  } | {
43
48
  readonly kind: "error";
44
49
  readonly message: string;
@@ -14,6 +14,8 @@ export function cliUsage(capabilities) {
14
14
  "update",
15
15
  "update --develop [preview-or-version]",
16
16
  "update --models",
17
+ "session link-worktree <path>",
18
+ "session unlink-worktree",
17
19
  "pi install <source>",
18
20
  "pi remove <source>",
19
21
  "pi uninstall <source>",
@@ -41,6 +43,10 @@ export function cliHelp(capabilities) {
41
43
  ` ${command} update --develop [preview-or-version]`,
42
44
  ` ${command} update --models`,
43
45
  "",
46
+ "Session context:",
47
+ ` ${command} session link-worktree <path>`,
48
+ ` ${command} session unlink-worktree`,
49
+ "",
44
50
  "Pi-compatible packages:",
45
51
  ` ${command} pi install <source>`,
46
52
  ` ${command} pi remove <source>`,
@@ -79,6 +85,8 @@ export async function dispatchCli(arguments_, handlers, output, capabilities) {
79
85
  return await handlers.version();
80
86
  if (command.kind === "packages")
81
87
  return await handlers.packages(command.request);
88
+ if (command.kind === "session-context")
89
+ return await handlers.sessionContext(command.request);
82
90
  return await handlers.update(command.channel, command.target);
83
91
  }
84
92
  export function parseCliCommand(arguments_, capabilities) {
@@ -105,12 +113,30 @@ export function parseCliCommand(arguments_, capabilities) {
105
113
  }
106
114
  if (command === "update")
107
115
  return parseUpdate(rest);
116
+ if (command === "session")
117
+ return parseSessionContext(rest);
108
118
  if (command?.startsWith("update:"))
109
119
  return { kind: "noop" };
110
120
  // Rationale: unsupported and reserved command spaces are deliberately quiet. Help is
111
121
  // explicit, and a typo must never start an interactive or maintenance path.
112
122
  return { kind: "noop" };
113
123
  }
124
+ function parseSessionContext(rest) {
125
+ const [action, value, extra] = rest;
126
+ if (action === "link-worktree") {
127
+ if (value === undefined)
128
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic("session link-worktree requires one path.") };
129
+ if (extra !== undefined)
130
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic("session link-worktree accepts one path.") };
131
+ return { kind: "session-context", request: { action, path: value } };
132
+ }
133
+ if (action === "unlink-worktree") {
134
+ if (value !== undefined)
135
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic("session unlink-worktree does not accept arguments.") };
136
+ return { kind: "session-context", request: { action } };
137
+ }
138
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic("session accepts link-worktree <path> or unlink-worktree.") };
139
+ }
114
140
  /**
115
141
  * Stable update is the empty form. `--develop` selects the moving development
116
142
  * head or one immutable numbered preview, while model refresh remains a separate
@@ -0,0 +1,9 @@
1
+ import { type SessionContextRequest } from "../features/launch/index.js";
2
+ export type { SessionContextRequest } from "../features/launch/index.js";
3
+ export interface SessionContextCommandOptions {
4
+ readonly environment?: NodeJS.ProcessEnv;
5
+ readonly stdout?: (message: string) => void;
6
+ readonly stderr?: (message: string) => void;
7
+ }
8
+ /** Apply repository presentation context only when invoked by an active Pi session shell tool. */
9
+ export declare function runSessionContextCommand(request: SessionContextRequest, options?: SessionContextCommandOptions): Promise<number>;
@@ -0,0 +1,29 @@
1
+ import { updateSessionRepositoryContext, } from "../features/launch/index.js";
2
+ import { PRODUCT_TEXT } from "../product-identity.js";
3
+ /** Apply repository presentation context only when invoked by an active Pi session shell tool. */
4
+ export async function runSessionContextCommand(request, options = {}) {
5
+ const environment = options.environment ?? process.env;
6
+ const stdout = options.stdout ?? (message => process.stdout.write(message));
7
+ const stderr = options.stderr ?? (message => process.stderr.write(message));
8
+ const identity = readIdentity(environment);
9
+ if (identity === null) {
10
+ stderr(`${PRODUCT_TEXT.diagnostic("session context requires an active A1 shell-tool session.")}\n`);
11
+ return 2;
12
+ }
13
+ try {
14
+ const cwd = await updateSessionRepositoryContext(identity, request);
15
+ stdout(cwd === null ? "Session worktree context cleared.\n" : `Session worktree context linked: ${cwd}\n`);
16
+ return 0;
17
+ }
18
+ catch (error) {
19
+ stderr(`${PRODUCT_TEXT.diagnostic(`could not update session context: ${error instanceof Error ? error.message : String(error)}`)}\n`);
20
+ return 1;
21
+ }
22
+ }
23
+ function readIdentity(environment) {
24
+ const sessionId = environment.PI_SESSION_ID;
25
+ const sessionFile = environment.PI_SESSION_FILE;
26
+ if (!sessionId || !sessionFile)
27
+ return null;
28
+ return { sessionId, sessionFile };
29
+ }
@@ -5,6 +5,7 @@ import { PromptImageSidecar } from "../features/prompt-history/image-sidecar.js"
5
5
  import { resolvePromptHistoryPath } from "../features/prompt-history/paths.js";
6
6
  import { resolvePromptHistoryDataDir } from "../features/launch/profile-paths.js";
7
7
  import { resolveProductPaths } from "../foundation/lifecycle/paths.js";
8
+ import { readSessionRepositoryContext } from "../foundation/lifecycle/session-repository-context.js";
8
9
  import { applyConfiguredPiTheme, getAvailablePiThemes } from "../integrations/pi/components/upstream/theme/theme.js";
9
10
  import { createPiEngineAdapter } from "../integrations/pi/engine/adapter.js";
10
11
  import { ClipboardDiagnosticCapture } from "../app/session-shell/clipboard-diagnostics.js";
@@ -19,18 +20,21 @@ export async function composeOwnedUiApplication(options = {}) {
19
20
  }
20
21
  export async function composeOwnedUi(options = {}) {
21
22
  const cwd = options.cwd ?? process.cwd();
23
+ const ownedSurfaces = options.ownedSurfaces !== "off";
22
24
  const adapter = options.createPiAdapter
23
25
  ? await options.createPiAdapter()
24
26
  : await createPiEngineAdapter({
25
27
  cwd,
26
28
  availableThemes: () => getAvailablePiThemes().map(theme => theme.name),
27
- settingsProductMode: options.ownedSurfaces === "off" ? "comparison" : "bare",
29
+ settingsProductMode: ownedSurfaces ? "bare" : "comparison",
30
+ ...(ownedSurfaces ? {
31
+ repositoryContextReader: async (sessionId, sessionFile, signal) => await readSessionRepositoryContext({ sessionId, sessionFile }, { signal }),
32
+ } : {}),
28
33
  ...(options.sessionPath === undefined ? {} : { sessionPath: options.sessionPath }),
29
34
  ...(options.sessionSelection === undefined ? {} : { sessionSelection: options.sessionSelection }),
30
35
  ...(options.sessionForkPrompt === undefined ? {} : { sessionForkPrompt: options.sessionForkPrompt }),
31
36
  ...(options.projectTrustPrompt === undefined ? {} : { projectTrustPrompt: options.projectTrustPrompt }),
32
37
  });
33
- const ownedSurfaces = options.ownedSurfaces !== "off";
34
38
  const settings = options.profileId === undefined
35
39
  ? null
36
40
  : new OwnedSettingsManager({
@@ -115,6 +115,7 @@ export interface OwnedUiUsageView {
115
115
  }
116
116
  export interface OwnedUiFooterView {
117
117
  readonly branch: string | null;
118
+ readonly repositoryPath?: string | null;
118
119
  readonly pullRequest?: {
119
120
  readonly number: number;
120
121
  readonly url: string;
@@ -183,6 +184,10 @@ export interface OwnedUiTranscriptBlock {
183
184
  readonly title: string | null;
184
185
  readonly text: string;
185
186
  readonly payload: unknown;
187
+ /** Bare-A1-only user presentation derived without mutating persisted/model message text. */
188
+ readonly userPresentation?: {
189
+ readonly visibleText: string;
190
+ };
186
191
  /** Tool-call message completion alone must not settle execution. */
187
192
  readonly toolState?: OwnedUiToolState;
188
193
  /** Authoritative renderer inputs, separate from diagnostic payloads. */
@@ -278,6 +278,11 @@ export function assertOwnedUiTranscriptBlock(block) {
278
278
  assertNonNegativeInteger(block.revision, "owned-UI transcript block revision");
279
279
  assertOptionalText(block.title, "owned-UI transcript block title", MAX_LABEL_LENGTH);
280
280
  assertPossiblyEmptyText(block.text, "owned-UI transcript block text", MAX_TEXT_BYTES);
281
+ if (block.userPresentation !== undefined) {
282
+ if (block.kind !== "user")
283
+ throw new TypeError("owned-UI user presentation requires a user block");
284
+ assertPossiblyEmptyText(block.userPresentation.visibleText, "owned-UI visible user text", MAX_TEXT_BYTES);
285
+ }
281
286
  assertJsonValue(block.toolRendering === undefined ? block.payload : {
282
287
  payload: block.payload, toolRendering: block.toolRendering,
283
288
  }, "owned-UI transcript block payload", MAX_PAYLOAD_BYTES);
@@ -396,6 +401,7 @@ export function assertOwnedUiStatusView(status) {
396
401
  }
397
402
  if (status.footer !== undefined) {
398
403
  assertOptionalText(status.footer.branch, "owned-UI footer branch", MAX_LABEL_LENGTH);
404
+ assertOptionalText(status.footer.repositoryPath ?? null, "owned-UI footer repository path", MAX_MESSAGE_LENGTH);
399
405
  if (status.footer.pullRequest !== undefined && status.footer.pullRequest !== null) {
400
406
  const pullRequest = status.footer.pullRequest;
401
407
  assertIntegerInRange(pullRequest.number, 1, Number.MAX_SAFE_INTEGER, "owned-UI footer pull request number");
@@ -1,4 +1,17 @@
1
1
  export type PresentationRuntimeState = "idle" | "running" | "stopping" | "stopped" | "failed";
2
+ export interface SemanticShortcutHint {
3
+ readonly key?: string;
4
+ readonly action: string;
5
+ readonly actionFirst?: boolean;
6
+ }
7
+ export interface ShortcutHintRoles {
8
+ key(label: string): string;
9
+ action(name: string): string;
10
+ }
11
+ /** Applies the common display casing to each named key and letter in a shortcut label. */
12
+ export declare function displayShortcutKeyLabel(label: string): string;
13
+ /** Renders the canonical semantic shortcut row independently of either presentation framework. */
14
+ export declare function renderSemanticShortcutHints(entries: readonly SemanticShortcutHint[], roles: ShortcutHintRoles, indent?: number): string;
2
15
  export interface PresentationComponentPort {
3
16
  render(width: number): readonly string[];
4
17
  invalidate(): void;
@@ -1,3 +1,33 @@
1
+ const SHORTCUT_KEY_NAMES = Object.freeze({
2
+ alt: "Alt", backspace: "Backspace", cmd: "Cmd", ctrl: "Ctrl", delete: "Delete", down: "Down", end: "End",
3
+ enter: "Enter", esc: "Esc", escape: "Escape", home: "Home", insert: "Insert", left: "Left", meta: "Meta",
4
+ option: "Option", pagedown: "PageDown", pageup: "PageUp", pgdn: "PgDn", pgup: "PgUp", return: "Return",
5
+ right: "Right", shift: "Shift", space: "Space", tab: "Tab", up: "Up",
6
+ });
7
+ /** Applies the common display casing to each named key and letter in a shortcut label. */
8
+ export function displayShortcutKeyLabel(label) {
9
+ if (label === "/")
10
+ return label;
11
+ return label.split("/").map(chord => chord.split("+").map(part => {
12
+ const named = SHORTCUT_KEY_NAMES[part.toLowerCase()];
13
+ if (named !== undefined)
14
+ return named;
15
+ return /^\p{L}$/u.test(part) ? part.toUpperCase() : part;
16
+ }).join("+")).join("/");
17
+ }
18
+ /** Renders the canonical semantic shortcut row independently of either presentation framework. */
19
+ export function renderSemanticShortcutHints(entries, roles, indent = 0) {
20
+ const rendered = entries.flatMap(entry => {
21
+ if (entry.key === "" || (entry.key !== undefined && entry.key.trim().length === 0))
22
+ return [];
23
+ if (entry.key === undefined)
24
+ return entry.action.length === 0 ? [] : [roles.action(entry.action)];
25
+ const key = roles.key(displayShortcutKeyLabel(entry.key));
26
+ const action = roles.action(entry.action);
27
+ return [entry.actionFirst ? `${action} ${key}` : `${key} ${action}`];
28
+ });
29
+ return `${" ".repeat(Math.max(0, indent))}${rendered.join(" ")}`;
30
+ }
1
31
  export function assertPresentationComponent(component) {
2
32
  requiredFunctions(component, ["render", "invalidate"], "presentation component");
3
33
  const lines = component.render(80);
@@ -8,5 +8,7 @@ export { configurationRootForProfile, resolveLaunchProfilePaths, resolvePromptHi
8
8
  export type { LaunchProfilePathOptions, LaunchProfilePaths } from "./profile-paths.js";
9
9
  export { prepareInteractiveLaunch } from "./prepare-launch.js";
10
10
  export type { PrepareInteractiveLaunchOptions, PreparedInteractiveLaunch } from "./prepare-launch.js";
11
+ export { updateSessionRepositoryContext } from "./session-context.js";
12
+ export type { SessionContextRequest } from "./session-context.js";
11
13
  export { runSelectedInteractiveRuntime, selectInteractiveRuntime } from "./runtime-selection.js";
12
14
  export type { InteractiveRuntimeRunners, InteractiveRuntimeSelection, OwnedUiProfileId } from "./runtime-selection.js";
@@ -3,4 +3,5 @@ export { PI_PROFILE_RESOURCE_DIRECTORIES, initializeProductProfile } from "./ini
3
3
  export { interactiveLaunchIntent, parseSessionSelection } from "./intent.js";
4
4
  export { configurationRootForProfile, resolveLaunchProfilePaths, resolvePromptHistoryDataDir } from "./profile-paths.js";
5
5
  export { prepareInteractiveLaunch } from "./prepare-launch.js";
6
+ export { updateSessionRepositoryContext } from "./session-context.js";
6
7
  export { runSelectedInteractiveRuntime, selectInteractiveRuntime } from "./runtime-selection.js";
@@ -0,0 +1,8 @@
1
+ import { type SessionRepositoryIdentity } from "../../foundation/lifecycle/index.js";
2
+ export type SessionContextRequest = {
3
+ readonly action: "link-worktree";
4
+ readonly path: string;
5
+ } | {
6
+ readonly action: "unlink-worktree";
7
+ };
8
+ export declare function updateSessionRepositoryContext(identity: SessionRepositoryIdentity, request: SessionContextRequest): Promise<string | null>;
@@ -0,0 +1,8 @@
1
+ import { clearSessionRepositoryContext, setSessionRepositoryContext, } from "../../foundation/lifecycle/index.js";
2
+ export async function updateSessionRepositoryContext(identity, request) {
3
+ if (request.action === "unlink-worktree") {
4
+ await clearSessionRepositoryContext(identity);
5
+ return null;
6
+ }
7
+ return (await setSessionRepositoryContext(identity, request.path)).cwd;
8
+ }
@@ -42,7 +42,7 @@ export function createConsoleProjectTrustPrompt(options = {}) {
42
42
  optionRow("Trust", selected === 0),
43
43
  optionRow("Do not trust", selected === 1),
44
44
  "",
45
- `${DIM} ↑/↓ to navigate · Enter to select · Esc to cancel${RESET_FG}`,
45
+ `${DIM} ↑/↓${MUTED} to navigate ${DIM}Enter${MUTED} to select ${DIM}Esc${MUTED} to cancel${RESET_FG}`,
46
46
  ];
47
47
  output.write(`${CLEAR_HOME}${lines.map(line => clipAnsiSafe(line, width)).join("\n")}`);
48
48
  };
@@ -1,4 +1,4 @@
1
- import { GLOBAL_SCOPE, PLAIN_THEME, RAIL_COLUMNS, ShortcutRegistry, ScrollbarRails, assertNoShortcutConflicts, displayWidth, layoutList, padToWidth, renderGroupHeader, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, scrollbarWheelRows, truncateToWidth, withScrollbarRail, } from "../../ui/components/index.js";
1
+ import { GLOBAL_SCOPE, PLAIN_THEME, RAIL_COLUMNS, ShortcutRegistry, ScrollbarRails, assertNoShortcutConflicts, displayWidth, layoutList, padToWidth, renderGroupHeader, renderShortcutHints, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, scrollbarWheelRows, truncateToWidth, withScrollbarRail, } from "../../ui/components/index.js";
2
2
  const SCOPE = "reference-screen";
3
3
  /** Rows above the document: the top rule. */
4
4
  const HEADER_ROWS = 1;
@@ -9,8 +9,7 @@ const SCROLL_LINGER_MS = 900;
9
9
  const SCROLL_LINGER_REPAINT_MS = 925;
10
10
  const LOADING_NOTICE = "Loading…";
11
11
  export const REFERENCE_SCREEN_SHORTCUTS = new ShortcutRegistry();
12
- // Compatibility: the footer reads as v2's `esc/ctrl+c close • ↑↓ scroll`, without ctrl+c, which is
13
- // the A1 interrupt chord here rather than a close key.
12
+ // Compatibility: Ctrl+C remains the A1 interrupt chord here rather than a close key.
14
13
  REFERENCE_SCREEN_SHORTCUTS.declare({ key: "escape", scope: GLOBAL_SCOPE, description: "Close", section: "Screen", hint: { keys: "esc", does: "close" } }, "close");
15
14
  REFERENCE_SCREEN_SHORTCUTS.declare({ key: "up", scope: SCOPE, description: "Scroll up", section: "Navigate", hint: { keys: "↑↓", does: "scroll" } }, "up");
16
15
  REFERENCE_SCREEN_SHORTCUTS.declare({ key: "down", scope: SCOPE, description: "Scroll down", section: "Navigate", hint: { keys: "↑↓", does: "scroll" } }, "down");
@@ -18,8 +17,6 @@ REFERENCE_SCREEN_SHORTCUTS.declare({ key: "pageUp", scope: SCOPE, description: "
18
17
  REFERENCE_SCREEN_SHORTCUTS.declare({ key: "pageDown", scope: SCOPE, description: "Down a page", section: "Navigate" }, "page-down");
19
18
  REFERENCE_SCREEN_SHORTCUTS.declare({ key: "home", scope: SCOPE, description: "First row", section: "Navigate" }, "first");
20
19
  REFERENCE_SCREEN_SHORTCUTS.declare({ key: "end", scope: SCOPE, description: "Last row", section: "Navigate" }, "last");
21
- /** The separator v2's footer puts between its hints. */
22
- const HINT_SEPARATOR = " • ";
23
20
  assertNoShortcutConflicts(REFERENCE_SCREEN_SHORTCUTS.assemble());
24
21
  const KEYS = {
25
22
  "\u001b[A": "up",
@@ -136,11 +133,13 @@ export class ReferenceScreenApp {
136
133
  ? { rail: { key: this.id, column: rect.width, rowStart: HEADER_ROWS, trackHeight: geometry.trackHeight }, geometry, page: viewportLength }
137
134
  : null;
138
135
  const withRail = withScrollbarRail(body.slice(0, bodyHeight), geometry, contentWidth, theme, { presentation });
139
- const hint = this.#interruptArmed ? "press ctrl+c again to exit a1" : REFERENCE_SCREEN_SHORTCUTS.hint(SCOPE, HINT_SEPARATOR);
136
+ const hint = this.#interruptArmed
137
+ ? theme.fg("dim", "press ctrl+c again to exit a1")
138
+ : renderShortcutHints(REFERENCE_SCREEN_SHORTCUTS.hintEntries(SCOPE), theme);
140
139
  // Compatibility: the v2 reference screen frames its document between two border-coloured rules.
141
140
  const rule = theme.fg("border", "─".repeat(rect.width));
142
- // Compatibility: v2 sets its footer against the left edge, unlike the settings status line.
143
- const frame = [rule, ...withRail, rule, padToWidth(theme.fg("dim", hint), rect.width)];
141
+ // Compatibility: the footer remains against the left edge, unlike the settings status line.
142
+ const frame = [rule, ...withRail, rule, padToWidth(hint, rect.width)];
144
143
  // Invariant: a rectangle too small for the chrome still gets exactly its rows, top first.
145
144
  return frame.slice(0, rect.height).concat(Array(Math.max(0, rect.height - frame.length)).fill(""));
146
145
  }
@@ -1,4 +1,4 @@
1
- import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, dialogValueColumn, numericValues, RAIL_COLUMNS, renderDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderListRow, renderNote, statusText, dialogRowAt, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, isThumbRow, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
1
+ import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, dialogValueColumn, numericValues, RAIL_COLUMNS, renderDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderListRow, renderNote, renderShortcutHints, dialogRowAt, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, isThumbRow, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
2
2
  import { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
3
3
  export { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
4
4
  const SCOPE = SETTINGS_APP_ID;
@@ -786,15 +786,19 @@ export class SettingsApp {
786
786
  });
787
787
  this.#dialogValueColumn = dialogValueColumn(rows);
788
788
  this.#panelTop = this.#panelTopForFrame;
789
- return renderDialogPanel({ rows, index: open.index, hint: SETTINGS_SHORTCUTS.hint(DIALOG_SCOPE) }, width, theme);
789
+ return renderDialogPanel({ rows, index: open.index, hint: SETTINGS_SHORTCUTS.hintEntries(DIALOG_SCOPE) }, width, theme);
790
790
  }
791
791
  #footerLines(width, theme) {
792
792
  const open = this.#structured;
793
793
  if (open !== null)
794
794
  return this.#dialogLines(open, width, theme);
795
- const hint = this.#interruptArmed ? "press ctrl+c again to exit a1" : SETTINGS_SHORTCUTS.hint(SCOPE);
796
- const report = statusText({ hint, report: this.#notice });
797
- const status = truncateToWidth(`${width > 0 ? " " : ""}${theme.fg("dim", report)}`, width);
795
+ const notice = this.#notice;
796
+ const plainStatus = notice !== null && notice.length > 0
797
+ ? notice
798
+ : this.#interruptArmed ? "press ctrl+c again to exit a1" : null;
799
+ const status = plainStatus === null
800
+ ? truncateToWidth(renderShortcutHints(SETTINGS_SHORTCUTS.hintEntries(SCOPE), theme, width > 0 ? 1 : 0), width)
801
+ : truncateToWidth(`${width > 0 ? " " : ""}${theme.fg("dim", plainStatus)}`, width);
798
802
  const input = this.#filter;
799
803
  if (input === null)
800
804
  return [status];
@@ -7,5 +7,7 @@ export { resolveCohortEndpoint, resolveProductPaths } from "./paths.js";
7
7
  export type { CohortEndpointPaths, ProductPaths } from "./paths.js";
8
8
  export { parseSessionSelection, sessionSelectionArguments } from "./session-selection.js";
9
9
  export type { SessionSelection } from "./session-selection.js";
10
+ export { clearSessionRepositoryContext, readSessionRepositoryContext, setSessionRepositoryContext } from "./session-repository-context.js";
11
+ export type { SessionRepositoryContext, SessionRepositoryContextOptions, SessionRepositoryIdentity } from "./session-repository-context.js";
10
12
  export { createSupervisorStartupAttempt, publishSupervisorStartupResult, readSupervisorStartupResult, supervisorStartupFailure, supervisorStartupReady, supervisorStartupResultPath, } from "./supervisor-startup.js";
11
13
  export type { SupervisorStartupAttemptIdentity, SupervisorStartupResult } from "./supervisor-startup.js";
@@ -2,4 +2,5 @@ export { assertLaunchInstance, assertLaunchInstanceOutcome, assertLaunchInstance
2
2
  export { assertDimensions, assertLaunchProfileId, assertNativeProcessIdentity, assertTransparentTerminalLaunchProfile } from "./model.js";
3
3
  export { resolveCohortEndpoint, resolveProductPaths } from "./paths.js";
4
4
  export { parseSessionSelection, sessionSelectionArguments } from "./session-selection.js";
5
+ export { clearSessionRepositoryContext, readSessionRepositoryContext, setSessionRepositoryContext } from "./session-repository-context.js";
5
6
  export { createSupervisorStartupAttempt, publishSupervisorStartupResult, readSupervisorStartupResult, supervisorStartupFailure, supervisorStartupReady, supervisorStartupResultPath, } from "./supervisor-startup.js";
@@ -0,0 +1,18 @@
1
+ export interface SessionRepositoryIdentity {
2
+ readonly sessionId: string;
3
+ readonly sessionFile: string;
4
+ }
5
+ export interface SessionRepositoryContext {
6
+ readonly cwd: string;
7
+ readonly branch: string;
8
+ }
9
+ export interface SessionRepositoryContextOptions {
10
+ readonly dataDir?: string;
11
+ readonly signal?: AbortSignal;
12
+ }
13
+ /** Persist one explicit same-repository worktree association for a stable Pi session. */
14
+ export declare function setSessionRepositoryContext(identity: SessionRepositoryIdentity, worktreePath: string, options?: SessionRepositoryContextOptions): Promise<SessionRepositoryContext>;
15
+ /** Remove only the association belonging to the supplied stable session. */
16
+ export declare function clearSessionRepositoryContext(identity: SessionRepositoryIdentity, options?: SessionRepositoryContextOptions): Promise<void>;
17
+ /** Read and revalidate a persisted association; malformed or stale state fails closed to absence. */
18
+ export declare function readSessionRepositoryContext(identity: SessionRepositoryIdentity, options?: SessionRepositoryContextOptions): Promise<SessionRepositoryContext | null>;