@timurproko/a1 0.1.8-dev.299 → 0.1.8-dev.322

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 (73) hide show
  1. package/README.md +16 -4
  2. package/bin/guardian.js +4 -3
  3. package/bin/ui.js +4 -2
  4. package/bin/update-recovery.js +3 -1
  5. package/dist/composition/owned-ui.js +2 -1
  6. package/dist/features/launch/profile-paths.d.ts +2 -0
  7. package/dist/features/launch/profile-paths.js +11 -0
  8. package/dist/foundation/launch-context/index.d.ts +29 -0
  9. package/dist/foundation/launch-context/index.js +83 -0
  10. package/dist/foundation/launch-guardian/main.js +4 -3
  11. package/dist/foundation/release/bootstrap.d.ts +1 -1
  12. package/dist/foundation/release/bootstrap.js +18 -12
  13. package/dist/foundation/release/cohort-state.d.ts +1 -0
  14. package/dist/foundation/release/cohort-state.js +4 -0
  15. package/dist/foundation/release/release-gc.js +2 -2
  16. package/dist/foundation/release/release-store.js +10 -3
  17. package/dist/foundation/release/release.d.ts +3 -1
  18. package/dist/foundation/release/release.js +7 -3
  19. package/dist/foundation/release/restart-certification.d.ts +1 -0
  20. package/dist/foundation/release/restart-certification.js +5 -0
  21. package/dist/foundation/release/update-recovery.d.ts +2 -0
  22. package/dist/foundation/release/update-recovery.js +4 -0
  23. package/dist/foundation/release/update.js +5 -0
  24. package/dist/foundation/release/warmup.d.ts +1 -1
  25. package/dist/foundation/release/warmup.js +5 -4
  26. package/dist/foundation/startup/startup-runtime.js +13 -11
  27. package/dist/foundation/supervision/main.js +8 -7
  28. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -0
  29. package/dist/integrations/pi/components/owned-editor-ux.js +54 -3
  30. package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -0
  31. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  32. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  33. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  34. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  35. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  36. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  37. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  38. package/dist/integrations/pi/engine/adapter.d.ts +13 -1
  39. package/dist/integrations/pi/engine/adapter.js +86 -25
  40. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  41. package/dist/integrations/pi/engine/changelog.js +63 -0
  42. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  43. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  44. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  45. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +3 -1
  46. package/dist/integrations/pi/session-ui/prompt-chips.js +37 -13
  47. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +25 -0
  48. package/dist/integrations/pi/session-ui/session-shell-root.js +65 -74
  49. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  50. package/dist/integrations/pi/session-ui/session-shell.js +58 -7
  51. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
  52. package/dist/integrations/pi/session-ui/session-viewport-controller.js +21 -15
  53. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
  54. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +7 -6
  55. package/dist/native/darwin-arm64/manifest.json +1 -1
  56. package/dist/native/linux-x64/manifest.json +1 -1
  57. package/dist/native/win32-x64/manifest.json +2 -2
  58. package/dist/native/win32-x64/process-guardian.exe +0 -0
  59. package/dist/product-identity.d.ts +1 -1
  60. package/dist/product-identity.js +2 -35
  61. package/dist/product-identity.json +0 -22
  62. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  63. package/dist/ui/components/transcript-viewport.js +23 -2
  64. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  65. package/dist/ui/components/visible-hyperlinks.js +1 -0
  66. package/dist/ui/settings/declarations.js +1 -0
  67. package/dist/ui/settings/sections.js +11 -1
  68. package/docs/architecture/boundaries.md +2 -1
  69. package/docs/architecture/internal-naming.md +89 -0
  70. package/docs/architecture/resource-and-data-policy.md +9 -4
  71. package/docs/architecture/toolchain.md +15 -1
  72. package/docs/features/prompt-history.md +39 -11
  73. package/package.json +8 -3
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
7
7
  import { processIsAlive } from "./process-cleanup.js";
8
+ import { PRIVATE_LAUNCH_CONTRACT, assertCurrentLaunchContract } from "../launch-context/index.js";
8
9
  export const UPDATE_RECOVERY_SCHEMA = "a1-update-recovery-v1";
9
10
  const WORKER_TIMEOUT_MS = 15 * 60 * 1_000;
10
11
  const POLL_MS = 50;
@@ -18,6 +19,7 @@ export function updateLauncherPaths(globalRoot, platform = process.platform) {
18
19
  /** Validate transaction-scoped recovery authority without trusting npm temporary names. */
19
20
  export async function readUpdateRecoveryCapsule(manifestPath, platform = process.platform) {
20
21
  const capsule = JSON.parse(await readFile(manifestPath, "utf8"));
22
+ assertCurrentLaunchContract(capsule);
21
23
  if (capsule.schema !== UPDATE_RECOVERY_SCHEMA || !/^[0-9a-f-]{36}$/i.test(capsule.transactionId) || typeof capsule.targetVersion !== "string"
22
24
  || capsule.packageName !== PRODUCT_TEXT.packageName || typeof capsule.packageRoot !== "string" || typeof capsule.globalRoot !== "string"
23
25
  || typeof capsule.launcherRoot !== "string" || !Array.isArray(capsule.launchers) || typeof capsule.priorReleaseId !== "string"
@@ -59,6 +61,7 @@ export async function readUpdateRecoveryCapsule(manifestPath, platform = process
59
61
  if (priorReleaseRoot.split(sep).at(-1) !== capsule.priorReleaseId)
60
62
  throw new Error("recovery prior release identity is invalid");
61
63
  const priorManifest = JSON.parse(await readFile(resolve(priorReleaseRoot, capsule.releaseManifestName), "utf8"));
64
+ assertCurrentLaunchContract(priorManifest);
62
65
  if (priorManifest.releaseId !== capsule.priorReleaseId || priorManifest.contentDigest !== capsule.priorContentDigest) {
63
66
  throw new Error("recovery prior release manifest differs from the capsule");
64
67
  }
@@ -95,6 +98,7 @@ export async function prepareUpdateRecoveryCapsule(options) {
95
98
  const npmCli = await resolveNpmCli(canonicalGlobal, options.environment ?? process.env);
96
99
  const capsule = {
97
100
  schema: UPDATE_RECOVERY_SCHEMA,
101
+ launchContract: PRIVATE_LAUNCH_CONTRACT,
98
102
  transactionId: options.transaction.transactionId,
99
103
  packageName: PRODUCT_TEXT.packageName,
100
104
  targetVersion: options.transaction.targetVersion,
@@ -13,6 +13,7 @@ import { cleanupVerifiedOwner, processIsAlive } from "./process-cleanup.js";
13
13
  import { materializeRelease, readMaterializedRelease } from "./release-store.js";
14
14
  import { scheduleReleaseCleanup } from "./release-gc.js";
15
15
  import { warmMaterializedRelease } from "./warmup.js";
16
+ import { assertCurrentLaunchContract } from "../launch-context/index.js";
16
17
  import { UpdateTransactionStore } from "./update-transaction.js";
17
18
  import { removeUpdateRecoveryCapsule, runProtectedPackageReplacement } from "./update-recovery.js";
18
19
  export const PRODUCT_PACKAGE = PRODUCT_TEXT.packageName;
@@ -77,6 +78,8 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
77
78
  async targetIsActive(targetVersion) {
78
79
  const state = await stateStore.read();
79
80
  const activeId = state.references.active;
81
+ if (activeId)
82
+ assertCurrentLaunchContract(state.releases[activeId] ?? {});
80
83
  if (!activeId || state.releases[activeId]?.packageVersion !== targetVersion)
81
84
  return false;
82
85
  const endpoint = await readActiveEndpoint(paths, activeId, environment);
@@ -85,6 +88,8 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
85
88
  async shutdownVerifiedOwners(targetVersion) {
86
89
  const state = await stateStore.read();
87
90
  const activeId = state.references.active;
91
+ if (activeId)
92
+ assertCurrentLaunchContract(state.releases[activeId] ?? {});
88
93
  const priorActiveVersion = activeId ? state.releases[activeId]?.packageVersion ?? null : null;
89
94
  const owner = await readActiveEndpoint(paths, activeId, environment);
90
95
  if (!owner)
@@ -1,3 +1,3 @@
1
- import type { MaterializedRelease } from "./release-store.js";
1
+ import { type MaterializedRelease } from "./release-store.js";
2
2
  /** Import the exact immutable startup graph in a terminal-free bounded child process. */
3
3
  export declare function warmMaterializedRelease(release: MaterializedRelease, environment: NodeJS.ProcessEnv, timeoutMs?: number): Promise<void>;
@@ -1,13 +1,14 @@
1
1
  import { spawn } from "node:child_process";
2
- import { resolve } from "node:path";
2
+ import { resolveReleaseEntryPoint } from "./release-store.js";
3
3
  import { releaseEnvironment } from "./bootstrap.js";
4
- import { PRODUCT_IDENTITY } from "../../product-identity.js";
4
+ import { PRIVATE_ENVIRONMENT, assertCurrentLaunchContract } from "../launch-context/index.js";
5
5
  /** Import the exact immutable startup graph in a terminal-free bounded child process. */
6
6
  export async function warmMaterializedRelease(release, environment, timeoutMs = 30_000) {
7
- const entry = resolve(release.releaseRoot, "bin", "warmup.js");
7
+ assertCurrentLaunchContract(release);
8
+ const entry = await resolveReleaseEntryPoint(release, "bin/warmup.js");
8
9
  await new Promise((resolvePromise, rejectPromise) => {
9
10
  const child = spawn(process.execPath, [entry], {
10
- env: { ...releaseEnvironment(environment, release), [PRODUCT_IDENTITY.environment.immutableWarmup]: "1" },
11
+ env: { ...releaseEnvironment(environment, release), [PRIVATE_ENVIRONMENT.immutableWarmup]: "1" },
11
12
  stdio: "ignore",
12
13
  windowsHide: true,
13
14
  });
@@ -5,18 +5,20 @@ import { dirname, resolve } from "node:path";
5
5
  import { homedir, platform } from "node:os";
6
6
  import { performance } from "node:perf_hooks";
7
7
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
8
+ import { readLaunchContext } from "../launch-context/index.js";
8
9
  /** Initialize opt-in startup tracing from a caller-provided evidence path. */
9
10
  export function initializeStartupTrace(environment, profileId, startedAtMs = performance.timeOrigin + performance.now()) {
10
11
  const configured = environment[PRODUCT_IDENTITY.environment.startupTrace];
11
12
  if (!configured || parseContext(configured) !== null)
12
13
  return;
14
+ const launch = readLaunchContext(environment);
13
15
  const context = {
14
16
  path: resolve(configured),
15
17
  traceId: randomUUID(),
16
18
  startedAtMs,
17
19
  profileId,
18
- releaseId: environment[PRODUCT_IDENTITY.environment.releaseId] ?? null,
19
- dependencyLayerIds: parseLayerIds(environment[PRODUCT_IDENTITY.environment.releaseLayers]),
20
+ releaseId: launch.releaseId ?? null,
21
+ dependencyLayerIds: parseLayerIds(launch.releaseLayers),
20
22
  };
21
23
  environment[PRODUCT_IDENTITY.environment.startupTrace] = JSON.stringify(context);
22
24
  }
@@ -25,17 +27,18 @@ export async function markStartupPhase(environment, phase) {
25
27
  const context = parseContext(environment[PRODUCT_IDENTITY.environment.startupTrace]);
26
28
  if (context === null)
27
29
  return;
30
+ const elapsedMs = Math.max(0, performance.timeOrigin + performance.now() - context.startedAtMs);
31
+ const launch = readLaunchContext(environment);
32
+ const dependencyLayerIds = parseLayerIds(launch.releaseLayers);
28
33
  const event = {
29
34
  schema: PRODUCT_IDENTITY.evidence.startupTraceSchema,
30
35
  traceId: context.traceId,
31
36
  phase,
32
- elapsedMs: Math.max(0, performance.timeOrigin + performance.now() - context.startedAtMs),
37
+ elapsedMs,
33
38
  processId: process.pid,
34
39
  profileId: context.profileId,
35
- releaseId: environment[PRODUCT_IDENTITY.environment.releaseId] ?? context.releaseId,
36
- dependencyLayerIds: parseLayerIds(environment[PRODUCT_IDENTITY.environment.releaseLayers]).length > 0
37
- ? parseLayerIds(environment[PRODUCT_IDENTITY.environment.releaseLayers])
38
- : context.dependencyLayerIds,
40
+ releaseId: launch.releaseId ?? context.releaseId,
41
+ dependencyLayerIds: dependencyLayerIds.length > 0 ? dependencyLayerIds : context.dependencyLayerIds,
39
42
  nodeVersion: process.version,
40
43
  fileReadOperations: process.resourceUsage().fsRead,
41
44
  };
@@ -43,9 +46,7 @@ export async function markStartupPhase(environment, phase) {
43
46
  await appendFile(context.path, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600 });
44
47
  }
45
48
  export function assertImmutableWarmupEnvironment(environment) {
46
- if (environment[PRODUCT_IDENTITY.environment.immutableWarmup] !== "1") {
47
- throw new Error("warmup entry is private to verified update activation");
48
- }
49
+ readLaunchContext(environment, "warmup");
49
50
  }
50
51
  /** Enable the compile cache directly from launch environment without loading profile services. */
51
52
  export function enableEnvironmentCompileCache(environment) {
@@ -54,7 +55,8 @@ export function enableEnvironmentCompileCache(environment) {
54
55
  ?? (platform() === "win32"
55
56
  ? resolve(environment.LOCALAPPDATA ?? home, PRODUCT_IDENTITY.state.windowsControlDirectory)
56
57
  : resolve(environment.XDG_DATA_HOME ?? resolve(home, ".local", "share"), PRODUCT_IDENTITY.state.unixControlDirectory)));
57
- return enableStartupCompileCache(dataDir, environment[PRODUCT_IDENTITY.environment.releaseId] ?? null, parseLayerIds(environment[PRODUCT_IDENTITY.environment.releaseLayers]));
58
+ const launch = readLaunchContext(environment);
59
+ return enableStartupCompileCache(dataDir, launch.releaseId ?? null, parseLayerIds(launch.releaseLayers));
58
60
  }
59
61
  /** Enable Node's supported persistent compile cache in an immutable-identity namespace. */
60
62
  export function enableStartupCompileCache(dataDir, releaseId, dependencyLayerIds) {
@@ -6,16 +6,18 @@ import { publishSupervisorStartupResult, supervisorStartupFailure, supervisorSta
6
6
  import { ControlStore } from "../storage/index.js";
7
7
  import { resolveCohortEndpoint, resolveProductPaths } from "./paths.js";
8
8
  import { SupervisorServer } from "./server.js";
9
- import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
9
+ import { PRODUCT_TEXT } from "../../product-identity.js";
10
+ import { readLaunchContext } from "../launch-context/index.js";
10
11
  export async function runSupervisor(arguments_ = []) {
11
12
  const productPaths = resolveProductPaths();
12
13
  mkdirSync(productPaths.runtimeDir, { recursive: true, mode: 0o700 });
13
14
  mkdirSync(productPaths.endpointsDir, { recursive: true, mode: 0o700 });
14
15
  let paths = productPaths;
15
16
  const log = (message) => appendFileSync(paths.supervisorLogPath, `${new Date().toISOString()} ${message}\n`);
16
- const releaseRoot = process.env[PRODUCT_IDENTITY.environment.releaseRoot];
17
- const releaseId = process.env[PRODUCT_IDENTITY.environment.releaseId];
18
- const contentDigest = process.env[PRODUCT_IDENTITY.environment.releaseDigest];
17
+ const context = readLaunchContext(process.env, "release");
18
+ const releaseRoot = context.releaseRoot;
19
+ const releaseId = context.releaseId;
20
+ const contentDigest = context.releaseDigest;
19
21
  const attemptId = startupAttempt(arguments_);
20
22
  const resultPath = attemptId ? supervisorStartupResultPath(productPaths.runtimeDir, attemptId) : null;
21
23
  let stage = "release-environment";
@@ -24,9 +26,8 @@ export async function runSupervisor(arguments_ = []) {
24
26
  if (!releaseRoot || !releaseId || !contentDigest)
25
27
  throw new Error(PRODUCT_TEXT.diagnostic("supervisor must be launched from a verified immutable release"));
26
28
  stage = "release-certification";
27
- const release = await readCertifiedReleaseManifest({ releaseRoot, releaseId, contentDigest }, resolve(paths.dataDir, "releases"), { allowLegacyParentCertification: true });
28
- // Compatibility: an older updater may have written release evidence before restart seals
29
- // existed, even when its layer certification has already been upgraded by a retry.
29
+ const release = await readCertifiedReleaseManifest({ releaseRoot, releaseId, contentDigest }, resolve(paths.dataDir, "releases"));
30
+ // Invariant: readiness follows current-contract certification, never an old-updater upgrade path.
30
31
  await recordParentCertifiedRelease(release, paths.dataDir);
31
32
  stage = "immutable-root";
32
33
  await assertImmutableExecutionRoot(release, paths.dataDir);
@@ -52,6 +52,7 @@ export interface PromptSelectionUxOptions {
52
52
  };
53
53
  readonly transformPastedContent: (content: PiShellClipboardContent) => string;
54
54
  readonly atomicRanges: (line: string) => readonly PiShellEditorTextRange[];
55
+ readonly hiddenRanges?: (line: string) => readonly PiShellEditorTextRange[];
55
56
  readonly expandCopiedText: (text: string) => string;
56
57
  readonly paintSelection: (line: string, from: number, to: number, atomic: boolean) => string;
57
58
  readonly decorateRow: (row: string, width: number) => string;
@@ -202,6 +202,47 @@ class PromptSelectionInterceptor {
202
202
  this.#redoStack = [];
203
203
  }
204
204
  render(width, next) {
205
+ const state = editorState(this.editor);
206
+ const hidden = state.lines.map(line => this.options.hiddenRanges?.(line) ?? []);
207
+ if (!hidden.some(ranges => ranges.length > 0))
208
+ return this.#renderVisible(width, next);
209
+ const original = { ...state, lines: [...state.lines] };
210
+ const selection = this.#selection;
211
+ const project = (position) => ({
212
+ line: position.line,
213
+ col: hideColumn(position.col, hidden[position.line] ?? []),
214
+ });
215
+ const lines = state.lines.map((line, index) => {
216
+ for (const range of [...(hidden[index] ?? [])].reverse())
217
+ line = line.slice(0, range.start) + line.slice(range.end);
218
+ return line;
219
+ });
220
+ // Rationale: project pending clipboard identities out before layout, not after painting.
221
+ // Keep the semantic draft/undo/submission markers intact and restore them even if rendering throws.
222
+ try {
223
+ this.#setRenderState(lines, project({ line: original.cursorLine, col: original.cursorCol }));
224
+ if (selection !== undefined)
225
+ this.#selection = { anchor: project(selection.anchor), head: project(selection.head) };
226
+ const rows = this.#renderVisible(width, next);
227
+ if (this.#geometry !== undefined) {
228
+ this.#geometry.hiddenRanges = hidden;
229
+ this.#geometry.lines = lines;
230
+ }
231
+ return rows;
232
+ }
233
+ finally {
234
+ this.#setRenderState(original.lines, { line: original.cursorLine, col: original.cursorCol });
235
+ this.#selection = selection;
236
+ }
237
+ }
238
+ #setRenderState(lines, cursor) {
239
+ if (this.editor.interaction !== undefined)
240
+ this.editor.interaction.replaceLines(lines);
241
+ else
242
+ editorState(this.editor).lines = lines;
243
+ this.#setCursor(cursor);
244
+ }
245
+ #renderVisible(width, next) {
205
246
  const rows = next().map(row => row.replaceAll(ATOMIC_SPACE_SENTINEL, " "));
206
247
  const maxPadding = Math.max(0, Math.floor((width - 1) / 2));
207
248
  const padding = Math.min(this.editor.getPaddingX(), maxPadding);
@@ -214,7 +255,7 @@ class PromptSelectionInterceptor {
214
255
  const scrollOffset = this.editor.interaction?.scrollOffset() ?? numericProperty(this.editor, "scrollOffset");
215
256
  const maxVisibleLines = Math.max(5, Math.floor(this.options.getRows() * 0.3));
216
257
  const textRows = Math.max(0, Math.min(visualLines.length - scrollOffset, maxVisibleLines, rows.length - 2));
217
- this.#geometry = { width, padding, layoutWidth, visualLines, scrollOffset, textRows };
258
+ this.#geometry = { width, padding, layoutWidth, visualLines, scrollOffset, textRows, hiddenRanges: [] };
218
259
  if (this.#atomicFocus() !== undefined) {
219
260
  for (let row = 0; row < rows.length; row += 1) {
220
261
  rows[row] = (rows[row] ?? "").replaceAll(CURSOR_MARKER, "");
@@ -344,12 +385,12 @@ class PromptSelectionInterceptor {
344
385
  const visual = geometry.visualLines[geometry.scrollOffset + textRow];
345
386
  if (visual === undefined)
346
387
  return undefined;
347
- const line = editorState(this.editor).lines[visual.logicalLine] ?? "";
388
+ const line = (geometry.lines ?? editorState(this.editor).lines)[visual.logicalLine] ?? "";
348
389
  const segment = line.slice(visual.startCol, visual.startCol + visual.length);
349
390
  const displayColumn = Math.max(0, column - 1 - geometry.padding - (this.options.promptPrefixWidth ?? 0));
350
391
  return {
351
392
  line: visual.logicalLine,
352
- col: visual.startCol + indexAtDisplayWidth(segment, displayColumn),
393
+ col: restoreColumn(visual.startCol + indexAtDisplayWidth(segment, displayColumn), geometry.hiddenRanges[visual.logicalLine] ?? []),
353
394
  };
354
395
  }
355
396
  #selectWord(position) {
@@ -804,6 +845,16 @@ function samePosition(left, right) {
804
845
  function sameSnapshot(left, right) {
805
846
  return left.text === right.text && samePosition(left.cursor, right.cursor);
806
847
  }
848
+ function hideColumn(column, ranges) {
849
+ return column - ranges.reduce((removed, range) => removed + Math.max(0, Math.min(column, range.end) - range.start), 0);
850
+ }
851
+ function restoreColumn(column, ranges) {
852
+ let restored = column;
853
+ for (const range of ranges)
854
+ if (restored >= range.start)
855
+ restored += range.end - range.start;
856
+ return restored;
857
+ }
807
858
  function editorState(editor) {
808
859
  if (editor.interaction !== undefined)
809
860
  return editor.interaction.snapshot();
@@ -64,6 +64,7 @@ export function createPiShellEditor(options) {
64
64
  ...(options.beginClipboardPaste === undefined ? {} : { beginClipboardPaste: options.beginClipboardPaste }),
65
65
  transformPastedContent: options.transformPastedContent ?? (content => content.kind === "text" ? content.text : ""),
66
66
  atomicRanges: options.editorAtomicRanges ?? (() => []),
67
+ hiddenRanges: options.editorHiddenRanges ?? (() => []),
67
68
  expandCopiedText: options.expandCopiedEditorText ?? (text => text),
68
69
  paintSelection: options.paintEditorSelection ?? (line => line),
69
70
  decorateRow: options.decorateEditorRow ?? (row => row),
@@ -149,6 +150,8 @@ export function createPiShellEditor(options) {
149
150
  return {
150
151
  render: width => editorUx?.render(width) ?? editor.render(width),
151
152
  activateKeybindings: () => setKeybindings(keybindings),
153
+ keybindingConfig: () => keybindings.getEffectiveConfig(),
154
+ reloadKeybindings: () => { keybindings.reload(); setKeybindings(keybindings); },
152
155
  matchesTerminalKey: (data, key) => matchesKey(data, key),
153
156
  handleInput: data => {
154
157
  if (editorUx === undefined)
@@ -1,4 +1,5 @@
1
- import { type PiShellComponentPort } from "./shell-shared-facade.js";
1
+ import { type KeybindingsConfig } from "#pi-tui";
2
+ import { type PiShellComponentPort, type PiShellExtensionRendererResolver } from "./shell-shared-facade.js";
2
3
  export interface PiShellSessionInfoPresentation {
3
4
  readonly sessionName?: string;
4
5
  readonly stats: {
@@ -30,7 +31,13 @@ export interface PiShellSessionInfoPresentation {
30
31
  }[];
31
32
  }
32
33
  export declare function renderPiShellStatusText(message: string, width: number, outputPad?: 0 | 1): readonly string[];
34
+ export interface PiShellCommandMessagePresentation {
35
+ readonly kind: "error" | "warning" | "accent" | "new" | "name" | "debug";
36
+ readonly message: string;
37
+ readonly detail?: string;
38
+ }
39
+ export declare function renderPiShellCommandMessage(presentation: PiShellCommandMessagePresentation, width: number, outputPad?: 0 | 1): readonly string[];
33
40
  export declare function createPiShellSessionInfo(presentation: PiShellSessionInfoPresentation): PiShellComponentPort;
34
41
  export declare function createPiShellCollapsedChangelog(): PiShellComponentPort;
35
42
  export declare function createPiShellChangelog(markdown: string): PiShellComponentPort;
36
- export declare function createPiShellHotkeys(): PiShellComponentPort;
43
+ export declare function createPiShellHotkeys(bindings?: KeybindingsConfig, getShortcuts?: NonNullable<PiShellExtensionRendererResolver["getShortcuts"]>, profile?: "pi" | "a1"): PiShellComponentPort;
@@ -7,6 +7,22 @@ export function renderPiShellStatusText(message, width, outputPad = PINNED_PI_LA
7
7
  ensureTheme();
8
8
  return new Text(piTheme().fg("dim", message), outputPad, 0).render(width);
9
9
  }
10
+ export function renderPiShellCommandMessage(presentation, width, outputPad = PINNED_PI_LAYOUT.outputPad) {
11
+ ensureTheme();
12
+ const { kind, message, detail } = presentation;
13
+ const prefix = kind === "error" ? "Error: " : kind === "warning" ? "Warning: " : "";
14
+ const color = kind === "name" ? "dim" : kind === "error" || kind === "warning" ? kind : "accent";
15
+ const content = piTheme().fg(color, `${prefix}${message}`)
16
+ + (kind === "debug" && detail ? `\n${piTheme().fg("muted", detail)}` : "");
17
+ // Compatibility: pinned errors honor outputPad; warnings and command notices retain
18
+ // one-cell padding. New-session/debug notices also own one vertical padding row.
19
+ const text = new Text(content, kind === "error" ? outputPad : 1, kind === "new" || kind === "debug" ? 1 : 0);
20
+ return [
21
+ ...(kind === "name" && detail ? renderPiShellCommandMessage({ kind: "warning", message: detail }, width) : []),
22
+ ...new Spacer(1).render(width),
23
+ ...text.render(width),
24
+ ];
25
+ }
10
26
  export function createPiShellSessionInfo(presentation) {
11
27
  ensureTheme();
12
28
  const { stats, sessionName, cacheWaste, usageBreakdown } = presentation;
@@ -61,12 +77,27 @@ export function createPiShellChangelog(markdown) {
61
77
  container.addChild(new DynamicBorder());
62
78
  return componentPort(container);
63
79
  }
64
- export function createPiShellHotkeys() {
80
+ function shortcutDisplay(key) {
81
+ // Platform: pinned Pi labels Alt as Option on macOS, including extension shortcuts.
82
+ return key.split("/").map(binding => binding.split("+").map(part => {
83
+ const label = process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part;
84
+ return label.charAt(0).toUpperCase() + label.slice(1);
85
+ }).join("+")).join("/");
86
+ }
87
+ export function createPiShellHotkeys(bindings, getShortcuts = () => [], profile = "pi") {
65
88
  ensureTheme();
66
- const keys = new KeybindingsManager();
67
- const display = (action) => keys.getKeys(action).map(key => key.split("+").map(part => part.charAt(0).toUpperCase() + part.slice(1)).join("+")).join("/");
89
+ const keys = new KeybindingsManager(bindings);
90
+ // Compatibility: the owned viewport consumes these physical chords before editor actions.
91
+ const display = (action) => keys.getKeys(action)
92
+ .filter(key => profile !== "a1" || (key !== "ctrl+home" && key !== "ctrl+end"))
93
+ .map(shortcutDisplay).join("/");
68
94
  const row = (actions, description) => `| ${actions.map(action => `\`${display(action)}\``).join(" / ")} | ${description} |`;
69
- const 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"], "Start of line"), row(["tui.editor.cursorLineEnd"], "End of line"), 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"], "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 last assistant message"), row(["app.message.followUp"], "Queue follow-up message"), row(["app.message.dequeue"], "Restore queued messages"), row(["app.clipboard.pasteImage"], "Paste image or text from clipboard"), "| `/` | Slash commands |", "| `!` | Run bash command |", "| `!!` | Run bash command (excluded from context) |"].join("\n");
95
+ 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"], "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 last assistant message"), row(["app.message.followUp"], "Queue follow-up message"), row(["app.message.dequeue"], "Restore queued messages"), row(["app.clipboard.pasteImage"], "Paste image or text from clipboard"), "| `/` | Slash commands |", "| `!` | Run bash command |", "| `!!` | Run bash command (excluded from context) |"].join("\n");
96
+ const shortcuts = getShortcuts(bindings ?? keys.getEffectiveConfig());
97
+ if (shortcuts.length > 0) {
98
+ markdown += "\n\n**Extensions**\n| Key | Action |\n|-----|--------|\n";
99
+ markdown += shortcuts.map(shortcut => `| \`${shortcutDisplay(shortcut.key)}\` | ${shortcut.description} |`).join("\n");
100
+ }
70
101
  const container = new Container();
71
102
  container.addChild(new Spacer(1));
72
103
  container.addChild(new DynamicBorder());
@@ -67,6 +67,7 @@ export declare function createPiShellTreeSelector(options: {
67
67
  readonly onSelect: (id: string) => void;
68
68
  readonly onCancel: () => void;
69
69
  readonly onLabelChange: (entryId: string, label: string | undefined) => void;
70
+ readonly onCopy?: (text: string | undefined) => void;
70
71
  readonly initialSelectedId?: string;
71
72
  readonly initialFilterMode?: "default" | "no-tools" | "user-only" | "labeled-only" | "all";
72
73
  }): PiShellComponentPort;
@@ -87,7 +88,7 @@ export interface PiShellLoginDialogPort extends PiShellComponentPort {
87
88
  showWaiting(message: string): void;
88
89
  showProgress(message: string): void;
89
90
  }
90
- export declare function createPiShellLoginDialog(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">, providerId: string, onComplete: (success: boolean, message?: string) => void, providerName?: string): PiShellLoginDialogPort;
91
+ export declare function createPiShellLoginDialog(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">, providerId: string, onComplete: (success: boolean, message?: string) => void, providerName?: string, title?: string): PiShellLoginDialogPort;
91
92
  export declare function createPiShellArmin(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">): PiShellComponentPort;
92
93
  export declare function createPiShellDaxnuts(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">): PiShellComponentPort;
93
94
  export declare function createPiShellEarendilAnnouncement(): PiShellComponentPort;
@@ -104,7 +105,7 @@ export interface PiShellAuthProviderOption extends PiShellSelectorOption {
104
105
  readonly source?: string;
105
106
  };
106
107
  }
107
- export declare function createPiShellAuthProviderSelector(mode: "login" | "logout", providers: readonly PiShellAuthProviderOption[], onSelect: (id: string) => void, onCancel: () => void): PiShellComponentPort;
108
+ export declare function createPiShellAuthProviderSelector(mode: "login" | "logout", providers: readonly PiShellAuthProviderOption[], onSelect: (id: string) => void, onCancel: () => void, initialSearchInput?: string): PiShellComponentPort;
108
109
  export declare function createPiShellExtensionSelector(title: string, options: readonly string[], onSelect: (value: string) => void, onCancel: () => void): PiShellComponentPort;
109
110
  export declare function createPiShellThinkingSelector(currentLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", availableLevels: readonly ("off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max")[], onSelect: (level: string) => void, onCancel: () => void): PiShellComponentPort;
110
111
  export declare function createPiShellThemeSelector(currentTheme: string, onSelect: (theme: string) => void, onCancel: () => void, onPreview: (theme: string) => void): PiShellComponentPort;
@@ -111,7 +111,10 @@ export function createPiShellSessionSelector(options) {
111
111
  }
112
112
  export function createPiShellTreeSelector(options) {
113
113
  ensureTheme();
114
- return componentPort(new TreeSelectorComponent([...options.tree], options.currentLeafId, options.terminalHeight, options.onSelect, options.onCancel, options.onLabelChange, options.initialSelectedId, options.initialFilterMode));
114
+ const selector = new TreeSelectorComponent([...options.tree], options.currentLeafId, options.terminalHeight, options.onSelect, options.onCancel, options.onLabelChange, options.initialSelectedId, options.initialFilterMode);
115
+ if (options.onCopy)
116
+ selector.onCopy = options.onCopy;
117
+ return componentPort(selector);
115
118
  }
116
119
  export function createPiShellUserMessageSelector(messages, onSelect, onCancel, initialSelectedId) {
117
120
  ensureTheme();
@@ -119,9 +122,9 @@ export function createPiShellUserMessageSelector(messages, onSelect, onCancel, i
119
122
  const list = selector.getMessageList();
120
123
  return componentPort(selector, data => list.handleInput(data));
121
124
  }
122
- export function createPiShellLoginDialog(runtime, providerId, onComplete, providerName) {
125
+ export function createPiShellLoginDialog(runtime, providerId, onComplete, providerName, title) {
123
126
  ensureTheme();
124
- const dialog = new LoginDialogComponent(createTuiFacade(runtime), providerId, onComplete, providerName);
127
+ const dialog = new LoginDialogComponent(createTuiFacade(runtime), providerId, onComplete, providerName, title);
125
128
  return {
126
129
  ...componentPort(dialog),
127
130
  showAuth: (url, instructions) => dialog.showAuth(url, instructions),
@@ -162,7 +165,7 @@ export function createPiShellReloadBox() {
162
165
  container.addChild(new DynamicBorder(borderColor));
163
166
  return componentPort(container);
164
167
  }
165
- export function createPiShellAuthProviderSelector(mode, providers, onSelect, onCancel) {
168
+ export function createPiShellAuthProviderSelector(mode, providers, onSelect, onCancel, initialSearchInput) {
166
169
  ensureTheme();
167
170
  const selector = new OAuthSelectorComponent(mode, providers.map(provider => ({
168
171
  id: provider.providerId,
@@ -173,7 +176,7 @@ export function createPiShellAuthProviderSelector(mode, providers, onSelect, onC
173
176
  const selected = providers.find(provider => provider.providerId === providerId && provider.authType === authType);
174
177
  if (selected)
175
178
  onSelect(selected.id);
176
- }, onCancel);
179
+ }, onCancel, initialSearchInput);
177
180
  return componentPort(selector);
178
181
  }
179
182
  export function createPiShellExtensionSelector(title, options, onSelect, onCancel) {
@@ -1,4 +1,4 @@
1
- import { visibleWidth, type AutocompleteProvider, type Component, type TUI } from "#pi-tui";
1
+ import { visibleWidth, type AutocompleteProvider, type Component, type KeybindingsConfig, type TUI } from "#pi-tui";
2
2
  import type { OwnedUiImageAttachment, OwnedUiSessionViewModel, OwnedUiThinkingLevel, OwnedUiTranscriptBlock } from "../../../contracts/owned-ui/index.js";
3
3
  import type { HistoryEditorConstructor } from "./history-editor-loader.js";
4
4
  import type { EditorRecallPort } from "./editor-interaction.js";
@@ -31,8 +31,10 @@ export interface PiShellEditorPort extends PiShellComponentPort {
31
31
  readonly historyReplacement?: typeof import("../../../contracts/owned-ui/index.js").PROMPT_HISTORY_EDITOR_REPLACEMENT;
32
32
  /** Restores this editor's profile after another Pi component changed the global manager. */
33
33
  activateKeybindings(): void;
34
+ keybindingConfig(): KeybindingsConfig;
35
+ reloadKeybindings(): void;
34
36
  /** Uses Pi's terminal decoder rather than assuming one terminal escape spelling. */
35
- matchesTerminalKey(data: string, key: "home" | "end" | "ctrl+v"): boolean;
37
+ matchesTerminalKey(data: string, key: "home" | "end" | "ctrl+home" | "ctrl+end" | "ctrl+v"): boolean;
36
38
  getText(): string;
37
39
  setText(text: string): void;
38
40
  insertText(text: string): void;
@@ -94,6 +96,10 @@ export interface PiShellImageAssetResolver {
94
96
  export interface PiShellExtensionRendererResolver {
95
97
  getMessageRenderer(customType: string): unknown;
96
98
  getToolDefinition(toolName: string): unknown;
99
+ getShortcuts?(bindings: KeybindingsConfig): readonly {
100
+ readonly key: string;
101
+ readonly description: string;
102
+ }[];
97
103
  }
98
104
  export interface PiShellTranscriptComponentPort extends PiShellComponentPort {
99
105
  readonly id: string;
@@ -147,6 +153,7 @@ export interface PiShellEditorOptions {
147
153
  readonly marker: string;
148
154
  readonly result: Promise<string>;
149
155
  };
156
+ readonly editorHiddenRanges?: (line: string) => readonly PiShellEditorTextRange[];
150
157
  readonly transformPastedContent?: (content: PiShellClipboardContent) => string;
151
158
  readonly editorAtomicRanges?: (line: string) => readonly PiShellEditorTextRange[];
152
159
  readonly expandCopiedEditorText?: (text: string) => string;