@timurproko/a1 0.1.8-dev.290 → 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 (112) hide show
  1. package/README.md +21 -0
  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 +13 -1
  6. package/dist/contracts/owned-ui/index.d.ts +1 -0
  7. package/dist/contracts/owned-ui/index.js +1 -0
  8. package/dist/contracts/owned-ui/prompt-history.d.ts +37 -0
  9. package/dist/contracts/owned-ui/prompt-history.js +23 -0
  10. package/dist/features/launch/profile-paths.d.ts +2 -0
  11. package/dist/features/launch/profile-paths.js +11 -0
  12. package/dist/features/prompt-history/index.d.ts +2 -0
  13. package/dist/features/prompt-history/index.js +2 -0
  14. package/dist/features/prompt-history/paths.d.ts +4 -0
  15. package/dist/features/prompt-history/paths.js +16 -0
  16. package/dist/features/prompt-history/service.d.ts +24 -0
  17. package/dist/features/prompt-history/service.js +260 -0
  18. package/dist/features/prompt-history/store.d.ts +15 -0
  19. package/dist/features/prompt-history/store.js +166 -0
  20. package/dist/features/prompt-history/worker.d.ts +9 -0
  21. package/dist/features/prompt-history/worker.js +42 -0
  22. package/dist/foundation/launch-context/index.d.ts +29 -0
  23. package/dist/foundation/launch-context/index.js +83 -0
  24. package/dist/foundation/launch-guardian/main.js +4 -3
  25. package/dist/foundation/release/bootstrap.d.ts +1 -1
  26. package/dist/foundation/release/bootstrap.js +18 -12
  27. package/dist/foundation/release/cohort-state.d.ts +1 -0
  28. package/dist/foundation/release/cohort-state.js +4 -0
  29. package/dist/foundation/release/release-gc.js +2 -2
  30. package/dist/foundation/release/release-store.js +10 -3
  31. package/dist/foundation/release/release.d.ts +3 -1
  32. package/dist/foundation/release/release.js +7 -3
  33. package/dist/foundation/release/restart-certification.d.ts +1 -0
  34. package/dist/foundation/release/restart-certification.js +5 -0
  35. package/dist/foundation/release/update-recovery.d.ts +2 -0
  36. package/dist/foundation/release/update-recovery.js +4 -0
  37. package/dist/foundation/release/update.js +5 -0
  38. package/dist/foundation/release/warmup.d.ts +1 -1
  39. package/dist/foundation/release/warmup.js +5 -4
  40. package/dist/foundation/startup/startup-runtime.js +13 -11
  41. package/dist/foundation/supervision/main.js +8 -7
  42. package/dist/integrations/pi/components/editor-interaction.d.ts +38 -0
  43. package/dist/integrations/pi/components/editor-interaction.js +1 -0
  44. package/dist/integrations/pi/components/history-editor-loader.d.ts +5 -0
  45. package/dist/integrations/pi/components/history-editor-loader.js +5 -0
  46. package/dist/integrations/pi/components/index.d.ts +1 -0
  47. package/dist/integrations/pi/components/index.js +1 -0
  48. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -1
  49. package/dist/integrations/pi/components/owned-editor-ux.js +95 -22
  50. package/dist/integrations/pi/components/shell-editor-autocomplete.js +12 -1
  51. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  52. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  53. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  54. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  55. package/dist/integrations/pi/components/shell-shared-facade.d.ts +15 -2
  56. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  57. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  58. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +13 -9
  59. package/dist/integrations/pi/components/upstream/components/owned-editor.js +137 -133
  60. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +280 -0
  61. package/dist/integrations/pi/components/upstream/history/editor-core.js +2062 -0
  62. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +32 -0
  63. package/dist/integrations/pi/components/upstream/history/kill-ring.js +48 -0
  64. package/dist/integrations/pi/components/upstream/history/printable-key.d.ts +1 -0
  65. package/dist/integrations/pi/components/upstream/history/printable-key.js +40 -0
  66. package/dist/integrations/pi/components/upstream/history/text-helpers.d.ts +14 -0
  67. package/dist/integrations/pi/components/upstream/history/text-helpers.js +28 -0
  68. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +22 -0
  69. package/dist/integrations/pi/components/upstream/history/undo-stack.js +36 -0
  70. package/dist/integrations/pi/components/upstream/history/word-navigation.d.ts +24 -0
  71. package/dist/integrations/pi/components/upstream/history/word-navigation.js +100 -0
  72. package/dist/integrations/pi/engine/adapter.d.ts +13 -1
  73. package/dist/integrations/pi/engine/adapter.js +86 -25
  74. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  75. package/dist/integrations/pi/engine/changelog.js +63 -0
  76. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  77. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  78. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  79. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +4 -1
  80. package/dist/integrations/pi/session-ui/prompt-chips.js +40 -13
  81. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +30 -0
  82. package/dist/integrations/pi/session-ui/prompt-history-controller.js +147 -0
  83. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +33 -0
  84. package/dist/integrations/pi/session-ui/session-shell-root.js +68 -74
  85. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  86. package/dist/integrations/pi/session-ui/session-shell.js +98 -12
  87. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
  88. package/dist/integrations/pi/session-ui/session-viewport-controller.js +21 -15
  89. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
  90. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +7 -6
  91. package/dist/native/darwin-arm64/manifest.json +1 -1
  92. package/dist/native/linux-x64/manifest.json +1 -1
  93. package/dist/native/win32-x64/manifest.json +2 -2
  94. package/dist/native/win32-x64/process-guardian.exe +0 -0
  95. package/dist/product-identity.d.ts +2 -2
  96. package/dist/product-identity.js +3 -36
  97. package/dist/product-identity.json +1 -22
  98. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  99. package/dist/ui/components/transcript-viewport.js +23 -2
  100. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  101. package/dist/ui/components/visible-hyperlinks.js +1 -0
  102. package/dist/ui/settings/declarations.js +19 -0
  103. package/dist/ui/settings/sections.js +11 -1
  104. package/dist/ui/settings/session.js +13 -4
  105. package/docs/architecture/boundaries.md +2 -0
  106. package/docs/architecture/history-editor-provenance.md +96 -0
  107. package/docs/architecture/internal-naming.md +89 -0
  108. package/docs/architecture/project-structure.md +1 -0
  109. package/docs/architecture/resource-and-data-policy.md +27 -1
  110. package/docs/architecture/toolchain.md +15 -1
  111. package/docs/features/prompt-history.md +149 -0
  112. package/package.json +8 -3
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
3
3
  import { basename, isAbsolute, relative, resolve, sep } from "node:path";
4
4
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
+ import { assertCurrentLaunchContract } from "../launch-context/index.js";
5
6
  import { DEPENDENCY_LAYER_MANIFEST, dependencyLayerCertificationPath } from "./dependency-layer.js";
6
7
  import { immutablePlatformPolicy } from "./immutable-platform.js";
7
8
  import { PRODUCT_PACKAGE_NAME } from "./release.js";
@@ -11,6 +12,7 @@ const RESTART_SEAL_VERSION = 1;
11
12
  const REQUIRED_LAUNCH_ENTRIES = ["bin/supervisor.js", "bin/guardian.js"];
12
13
  /** Build compact restart authority after complete content verification established read-only payload files. */
13
14
  export async function createRestartSeal(release, dataDir) {
15
+ assertCurrentLaunchContract(release);
14
16
  const policy = immutablePlatformPolicy();
15
17
  if (policy === null)
16
18
  return null;
@@ -45,6 +47,7 @@ export async function createRestartSeal(release, dataDir) {
45
47
  }));
46
48
  const core = {
47
49
  version: RESTART_SEAL_VERSION,
50
+ launchContract: release.launchContract,
48
51
  platform: process.platform,
49
52
  platformPolicy: policy,
50
53
  releaseId: release.releaseId,
@@ -81,6 +84,7 @@ export async function readRestartCertifiedRelease(record, dataDir, onEvent) {
81
84
  throw new Error("restart certification differs from the approved release record");
82
85
  }
83
86
  const seal = normalizeRestartSeal(certification.restartSeal);
87
+ assertCurrentLaunchContract(seal);
84
88
  if (seal.platform !== process.platform || seal.platformPolicy !== immutablePlatformPolicy()) {
85
89
  throw new Error("restart certification platform immutability evidence is unsupported");
86
90
  }
@@ -129,6 +133,7 @@ export async function readRestartCertifiedRelease(record, dataDir, onEvent) {
129
133
  }
130
134
  return {
131
135
  packageName: PRODUCT_PACKAGE_NAME,
136
+ launchContract: seal.launchContract,
132
137
  packageVersion: seal.packageVersion,
133
138
  contentDigest: seal.contentDigest,
134
139
  releaseId: seal.releaseId,
@@ -1,7 +1,9 @@
1
+ import { PRIVATE_LAUNCH_CONTRACT } from "../launch-context/index.js";
1
2
  import type { UpdateTransaction, UpdateRecoveryState } from "./update-transaction.js";
2
3
  export declare const UPDATE_RECOVERY_SCHEMA: "a1-update-recovery-v1";
3
4
  export interface UpdateRecoveryCapsule {
4
5
  readonly schema: typeof UPDATE_RECOVERY_SCHEMA;
6
+ readonly launchContract: typeof PRIVATE_LAUNCH_CONTRACT;
5
7
  readonly transactionId: string;
6
8
  readonly packageName: string;
7
9
  readonly targetVersion: string;
@@ -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);
@@ -0,0 +1,38 @@
1
+ import type { Editor } from "#pi-tui";
2
+ export interface EditorInteractionState {
3
+ lines: string[];
4
+ cursorLine: number;
5
+ cursorCol: number;
6
+ }
7
+ export interface EditorInteractionPort {
8
+ snapshot(): EditorInteractionState;
9
+ setCursor(line: number, col: number): void;
10
+ replaceLines(lines: readonly string[]): void;
11
+ updateUndoStates(update: (state: EditorInteractionState) => void): void;
12
+ scrollOffset(): number;
13
+ visualLines(width: number): Array<{
14
+ logicalLine: number;
15
+ startCol: number;
16
+ length: number;
17
+ }>;
18
+ setSegmentTransform(transform: (text: string, mode: "word" | "grapheme", segments: Iterable<Intl.SegmentData>) => Iterable<Intl.SegmentData>): void;
19
+ }
20
+ export interface EditorRecallPort {
21
+ replace(entries: readonly string[]): void;
22
+ position(): {
23
+ readonly index: number;
24
+ readonly total: number;
25
+ };
26
+ observe(listener: () => void): () => void;
27
+ reset(): void;
28
+ }
29
+ export interface EditorSurface extends Pick<Editor, Exclude<keyof Editor, "setText" | "render" | "handleInput">> {
30
+ setText(text: string): void;
31
+ render(width: number): string[];
32
+ handleInput(data: string): void;
33
+ readonly interaction?: EditorInteractionPort;
34
+ readonly recall?: EditorRecallPort;
35
+ }
36
+ export type SelectionEditor = Pick<Editor, "getText" | "getCursor" | "setText" | "insertTextAtCursor" | "invalidate" | "onChange" | "getPaddingX" | "isShowingAutocomplete"> & {
37
+ readonly interaction?: EditorInteractionPort;
38
+ };
@@ -0,0 +1,5 @@
1
+ import { type ShellEditorInstance, type OwnedEditorOptions } from "./upstream/components/owned-editor.js";
2
+ import type { EditorTheme, TUI } from "#pi-tui";
3
+ import type { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
4
+ export type HistoryEditorConstructor = new (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: OwnedEditorOptions) => ShellEditorInstance;
5
+ export declare function loadHistoryEditor(): Promise<HistoryEditorConstructor>;
@@ -0,0 +1,5 @@
1
+ import { createOwnedEditorClass } from "./upstream/components/owned-editor.js";
2
+ export async function loadHistoryEditor() {
3
+ const { HistoryEditorCore } = await import("./upstream/history/editor-core.js");
4
+ return createOwnedEditorClass(HistoryEditorCore);
5
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./components.js";
2
+ export { loadHistoryEditor, type HistoryEditorConstructor } from "./history-editor-loader.js";
2
3
  export * from "./conformance.js";
3
4
  export * from "./shell-components.js";
4
5
  export * from "./theme.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./components.js";
2
+ export { loadHistoryEditor } from "./history-editor-loader.js";
2
3
  export * from "./conformance.js";
3
4
  export * from "./shell-components.js";
4
5
  export * from "./theme.js";
@@ -1,4 +1,4 @@
1
- import { type Editor } from "#pi-tui";
1
+ import type { SelectionEditor as Editor } from "./editor-interaction.js";
2
2
  import type { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
3
3
  import type { PiShellClipboardContent, PiShellEditorTextRange } from "./shell-shared-facade.js";
4
4
  /**
@@ -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);
@@ -211,10 +252,10 @@ class PromptSelectionInterceptor {
211
252
  const layoutWidth = Math.max(1, contentWidth - (padding ? 0 : 1));
212
253
  const visualLines = editorVisualLineMap(this.editor, layoutWidth)
213
254
  ?? buildVisualLineMap(editorState(this.editor).lines, layoutWidth);
214
- const scrollOffset = numericProperty(this.editor, "scrollOffset");
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) {
@@ -464,15 +505,21 @@ class PromptSelectionInterceptor {
464
505
  const selection = this.#selection === undefined ? undefined : { anchor: remap(this.#selection.anchor), head: remap(this.#selection.head) };
465
506
  const next = current.slice(0, from) + text + current.slice(to);
466
507
  // Compatibility: completing a paste updates its provisional undo snapshots, not the user's undo history.
467
- editorState(this.editor).lines = next.split("\n");
468
- const undo = Reflect.get(this.editor, "undoStack");
469
- const snapshots = typeof undo === "object" && undo !== null ? Reflect.get(undo, "stack") : undefined;
470
- if (Array.isArray(snapshots))
471
- for (const snapshot of snapshots) {
472
- const state = typeof snapshot === "object" && snapshot !== null ? Reflect.get(snapshot, "state") : undefined;
473
- if (isEditorState(state))
474
- replaceSnapshotMarker(state, marker, text);
475
- }
508
+ if (this.editor.interaction !== undefined) {
509
+ this.editor.interaction.replaceLines(next.split("\n"));
510
+ this.editor.interaction.updateUndoStates(state => replaceSnapshotMarker(state, marker, text));
511
+ }
512
+ else {
513
+ editorState(this.editor).lines = next.split("\n");
514
+ const undo = Reflect.get(this.editor, "undoStack");
515
+ const snapshots = typeof undo === "object" && undo !== null ? Reflect.get(undo, "stack") : undefined;
516
+ if (Array.isArray(snapshots))
517
+ for (const snapshot of snapshots) {
518
+ const state = typeof snapshot === "object" && snapshot !== null ? Reflect.get(snapshot, "state") : undefined;
519
+ if (isEditorState(state))
520
+ replaceSnapshotMarker(state, marker, text);
521
+ }
522
+ }
476
523
  for (const snapshot of this.#redoStack) {
477
524
  const state = { lines: snapshot.text.split("\n"), cursorLine: snapshot.cursor.line, cursorCol: snapshot.cursor.col };
478
525
  replaceSnapshotMarker(state, marker, text);
@@ -602,6 +649,10 @@ class PromptSelectionInterceptor {
602
649
  return this.editor.getCursor();
603
650
  }
604
651
  #setCursor(position) {
652
+ if (this.editor.interaction !== undefined) {
653
+ this.editor.interaction.setCursor(position.line, position.col);
654
+ return;
655
+ }
605
656
  const state = editorState(this.editor);
606
657
  const line = clamp(position.line, 0, Math.max(0, state.lines.length - 1));
607
658
  state.cursorLine = line;
@@ -615,6 +666,7 @@ class PromptSelectionInterceptor {
615
666
  : state.cursorCol >= range.start && state.cursorCol < range.end);
616
667
  if (atomic !== undefined) {
617
668
  state.cursorCol = delta < 0 ? atomic.start : atomic.end;
669
+ this.editor.interaction?.setCursor(state.cursorLine, state.cursorCol);
618
670
  return;
619
671
  }
620
672
  if (delta < 0) {
@@ -626,6 +678,7 @@ class PromptSelectionInterceptor {
626
678
  state.cursorLine -= 1;
627
679
  state.cursorCol = (state.lines[state.cursorLine] ?? "").length;
628
680
  }
681
+ this.editor.interaction?.setCursor(state.cursorLine, state.cursorCol);
629
682
  return;
630
683
  }
631
684
  if (state.cursorCol < line.length) {
@@ -635,6 +688,7 @@ class PromptSelectionInterceptor {
635
688
  state.cursorLine += 1;
636
689
  state.cursorCol = 0;
637
690
  }
691
+ this.editor.interaction?.setCursor(state.cursorLine, state.cursorCol);
638
692
  }
639
693
  #atomicRangeAt(position) {
640
694
  const line = editorState(this.editor).lines[position.line] ?? "";
@@ -674,14 +728,8 @@ class PromptSelectionInterceptor {
674
728
  }
675
729
  }
676
730
  function installAtomicSegmentation(editor, rangesForText, wordDirection) {
677
- if (Reflect.get(editor, ATOMIC_SEGMENTATION) === true)
678
- return;
679
- const originalValue = Reflect.get(editor, "segment");
680
- if (typeof originalValue !== "function")
681
- return;
682
- const original = originalValue.bind(editor);
683
- Reflect.set(editor, "segment", (text, mode) => {
684
- const segments = [...original(text, mode)].filter(isEditorSegment);
731
+ const transform = (text, mode, values) => {
732
+ const segments = [...values].filter(isEditorSegment);
685
733
  const ranges = rangesForText(text).map(range => ({ ...range, wordLike: false }));
686
734
  if (mode === "word") {
687
735
  for (const range of contextualPathRanges(editor, text, wordDirection())) {
@@ -713,7 +761,18 @@ function installAtomicSegmentation(editor, rangesForText, wordDirection) {
713
761
  merged.push(segment);
714
762
  }
715
763
  return merged;
716
- });
764
+ };
765
+ if (editor.interaction !== undefined) {
766
+ editor.interaction.setSegmentTransform(transform);
767
+ return;
768
+ }
769
+ if (Reflect.get(editor, ATOMIC_SEGMENTATION) === true)
770
+ return;
771
+ const originalValue = Reflect.get(editor, "segment");
772
+ if (typeof originalValue !== "function")
773
+ return;
774
+ const original = originalValue.bind(editor);
775
+ Reflect.set(editor, "segment", (text, mode) => transform(text, mode, original(text, mode)));
717
776
  Reflect.set(editor, ATOMIC_SEGMENTATION, true);
718
777
  }
719
778
  function contextualPathRanges(editor, text, direction) {
@@ -786,7 +845,19 @@ function samePosition(left, right) {
786
845
  function sameSnapshot(left, right) {
787
846
  return left.text === right.text && samePosition(left.cursor, right.cursor);
788
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
+ }
789
858
  function editorState(editor) {
859
+ if (editor.interaction !== undefined)
860
+ return editor.interaction.snapshot();
790
861
  const value = Object.getOwnPropertyDescriptor(editor, "state")?.value;
791
862
  if (!isEditorState(value))
792
863
  throw new Error("Pi editor state is unavailable");
@@ -806,6 +877,8 @@ function numericProperty(target, key) {
806
877
  return typeof value === "number" ? value : 0;
807
878
  }
808
879
  function editorVisualLineMap(editor, width) {
880
+ if (editor.interaction !== undefined)
881
+ return editor.interaction.visualLines(width);
809
882
  const builder = Reflect.get(editor, "buildVisualLineMap");
810
883
  if (typeof builder !== "function")
811
884
  return undefined;
@@ -1,5 +1,6 @@
1
1
  import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
2
2
  import { CombinedAutocompleteProvider, matchesKey, setKeybindings, } from "#pi-tui";
3
+ import { PROMPT_HISTORY_EDITOR_REPLACEMENT } from "../../../contracts/owned-ui/index.js";
3
4
  import { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
4
5
  import { OwnedEditor } from "./upstream/components/owned-editor.js";
5
6
  import { OwnedEditorUxInterception, createPromptSelectionInterceptor, } from "./owned-editor-ux.js";
@@ -36,12 +37,18 @@ export function createPiShellEditor(options) {
36
37
  ? KeybindingsManager.createForOwnedInput(options.agentDir)
37
38
  : KeybindingsManager.create(options.agentDir);
38
39
  setKeybindings(keybindings);
39
- const editor = new OwnedEditor(tui, {
40
+ if (options.persistentHistory === true && options.keybindingProfile === "a1" && options.historyEditor === undefined) {
41
+ throw new Error("Persistent history requires the loaded owned editor");
42
+ }
43
+ const EditorClass = options.keybindingProfile === "a1" && options.persistentHistory === true ? options.historyEditor : OwnedEditor;
44
+ const editor = new EditorClass(tui, {
40
45
  borderColor: (value) => piTheme().fg("borderMuted", value),
41
46
  selectList: getSelectListTheme(),
42
47
  }, keybindings, {
43
48
  paddingX: PINNED_PI_LAYOUT.editorPaddingX,
44
49
  autocompleteMaxVisible: PINNED_PI_LAYOUT.autocompleteMaxVisible,
50
+ persistentHistory: options.persistentHistory === true,
51
+ styleHistoryLabel: text => piTheme().fg("dim", text),
45
52
  ...(options.keybindingProfile === "a1" && options.promptPresentation !== undefined ? {
46
53
  promptPrefix: options.promptPresentation.prefix,
47
54
  styleSuggestion: options.promptPresentation.styleSuggestion,
@@ -57,6 +64,7 @@ export function createPiShellEditor(options) {
57
64
  ...(options.beginClipboardPaste === undefined ? {} : { beginClipboardPaste: options.beginClipboardPaste }),
58
65
  transformPastedContent: options.transformPastedContent ?? (content => content.kind === "text" ? content.text : ""),
59
66
  atomicRanges: options.editorAtomicRanges ?? (() => []),
67
+ hiddenRanges: options.editorHiddenRanges ?? (() => []),
60
68
  expandCopiedText: options.expandCopiedEditorText ?? (text => text),
61
69
  paintSelection: options.paintEditorSelection ?? (line => line),
62
70
  decorateRow: options.decorateEditorRow ?? (row => row),
@@ -142,6 +150,8 @@ export function createPiShellEditor(options) {
142
150
  return {
143
151
  render: width => editorUx?.render(width) ?? editor.render(width),
144
152
  activateKeybindings: () => setKeybindings(keybindings),
153
+ keybindingConfig: () => keybindings.getEffectiveConfig(),
154
+ reloadKeybindings: () => { keybindings.reload(); setKeybindings(keybindings); },
145
155
  matchesTerminalKey: (data, key) => matchesKey(data, key),
146
156
  handleInput: data => {
147
157
  if (editorUx === undefined)
@@ -163,6 +173,7 @@ export function createPiShellEditor(options) {
163
173
  editor.insertTextAtCursor(text);
164
174
  },
165
175
  addToHistory: text => editor.addToHistory(text),
176
+ ...(editor.recall === undefined ? {} : { recall: editor.recall, historyReplacement: PROMPT_HISTORY_EDITOR_REPLACEMENT }),
166
177
  setSubmitEnabled: enabled => {
167
178
  editor.disableSubmit = !enabled;
168
179
  },
@@ -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;