@timurproko/a1 0.2.1-dev.604 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,31 +3,10 @@
3
3
  ## Install
4
4
 
5
5
  ```sh
6
- # release
7
- npx -y @timurproko/a1-install
8
-
9
- # develop
10
- npx -y @timurproko/a1-install --develop
11
-
12
- # preview number
13
- npx -y @timurproko/a1-install --develop 107
14
-
15
- # exact preview
16
- npx -y @timurproko/a1-install --develop 0.1.8-dev.107
17
- ```
18
-
19
- The installer keeps npm's dependency transcript out of the normal terminal output,
20
- shows one progress row, activates the installed release, and verifies the command
21
- before reporting success. The shorter `npx @timurproko/a1-install` form also works,
22
- but npm may ask for first-use confirmation; `-y` avoids that prompt.
23
-
24
- Direct npm installation remains available as a fallback or manual recovery path.
25
- npm's `latest` tag carries the release and `next` carries develop:
26
-
27
- ```sh
28
- npm install -g @timurproko/a1@latest # release
29
- npm install -g @timurproko/a1@next # develop
30
- npm install -g @timurproko/a1@0.1.8-dev.107 # exact preview
6
+ npx -y @timurproko/a1-install # release
7
+ npx -y @timurproko/a1-install --develop # develop
8
+ npx -y @timurproko/a1-install --develop 107 # preview number
9
+ npx -y @timurproko/a1-install --develop 0.1.8-dev.107 # exact preview
31
10
  ```
32
11
 
33
12
  ## Use
@@ -36,126 +15,59 @@ npm install -g @timurproko/a1@0.1.8-dev.107 # exact preview
36
15
  a1 # launch A1 (profile: ~/.a1/agent)
37
16
  a1 help # show all commands
38
17
  a1 version # show the version
18
+ a1 pi # vanilla Pi oracle: ~/.pi/agent
19
+ ```
20
+
21
+ Update:
22
+
23
+ ```sh
39
24
  a1 update # install the release
25
+ a1 update --models # refresh A1's model catalogs
40
26
  a1 update --develop # install develop
41
27
  a1 update --develop 107 # install preview 107
42
28
  a1 update --develop 0.1.8-dev.107 # install that exact preview
43
- a1 update --models # refresh A1's model catalogs
44
- a1 update --extensions # update every installed package
45
- a1 update npm:pi-mcp-adapter # update one package
46
29
  ```
47
30
 
48
- Release builds print only their installed version. Develop builds also show the
49
- current develop and release channel versions.
50
-
51
- Develop previews add the Pi comparison profile; release builds do not carry it.
31
+ ## Extensions
52
32
 
53
33
  ```sh
54
- a1 pi # vanilla Pi oracle: ~/.pi/agent
34
+ a1 install npm:pi-mcp-adapter # install a package
35
+ a1 remove npm:pi-mcp-adapter # remove it (alias: a1 uninstall)
36
+ a1 list # list installed packages
55
37
  ```
56
38
 
57
- Unsupported commands exit quietly without launching anything. The removed
58
- `update:<preview>` forms are not aliases; use `update --develop`.
59
-
60
- ## Extensions
61
-
62
- Pi-compatible extension packages install into A1's own profile (`~/.a1/agent`), so
63
- bare `a1` loads them and the `a1 pi` comparison profile does not. Sources use Pi's
64
- `npm:`, git, or local-path grammar.
39
+ Update:
65
40
 
66
41
  ```sh
67
- a1 install npm:pi-mcp-adapter # install a package
68
- a1 remove npm:pi-mcp-adapter # remove it (alias: a1 uninstall)
69
- a1 list # list installed packages
70
- a1 update --extensions # update every installed package
71
- a1 update npm:pi-mcp-adapter # update one
72
- a1 update --models # refresh A1's model catalogs
42
+ a1 update --extensions # update every installed package
43
+ a1 update npm:pi-mcp-adapter # update one
73
44
  ```
74
45
 
75
- A1 pins the Pi runtime carried by each release, so Pi self-update forms are refused.
76
- Update A1 itself with `a1 update` or `a1 update --develop`.
77
-
78
- A running session picks up a newly installed package after a restart.
79
- Configuration is isolated the same way: bare `a1` reads `~/.a1/agent` (or the
80
- project), never `~/.pi/agent`. Configure MCP with `/mcp setup` inside bare `a1`.
81
-
82
46
  ## Develop
83
47
 
84
- A source build needs Node.js `>=22.19.0 <25`, npm 11, git, and Rust/Cargo 1.85 or
85
- newer—the build compiles the `native/process-guardian` crate. Publishing a release
86
- or a pull request additionally needs an authenticated [GitHub CLI](https://cli.github.com).
87
- `npm run doctor` reports every prerequisite with the exact command that installs
88
- whatever is missing, and `npm run build` runs the same check first so a fresh clone
89
- fails by name instead of inside a resolver or Cargo.
90
-
91
48
  ```sh
92
- npm run doctor # report Node, npm, git, Rust, and dependency readiness
93
- npm ci # install exact locked dependencies
94
- npm run build # compile TypeScript and the process guardian into dist
95
- npm start # build and launch a source `a1`
96
- npm run start:pi # build and launch a source `a1 pi`
97
- npm run test:fast # typecheck + fast suite (alias: npm test)
98
- npm run test:full # complete non-physical suite
49
+ npm run doctor # report Node, npm, git, Rust, and dependency readiness
50
+ npm ci # install exact locked dependencies
51
+ npm run build # compile TypeScript and the process guardian into dist
52
+ npm start # build and launch a source `a1`
53
+ npm run start:pi # build and launch a source `a1 pi`
54
+ npm run test:fast # typecheck + fast suite (alias: npm test)
55
+ npm run test:full # complete non-physical suite
99
56
  ```
100
57
 
101
- Create every task worktree at `{working-dir}/.worktrees/<task-id>`, where
102
- `{working-dir}` is the session's initial working directory. For working directory
103
- `D:/Git/a1`, use `D:/Git/a1/.worktrees/<task-id>`—never a sibling such as
104
- `D:/Git/a1-<task-id>`. The primary worktree remains on `develop` for integration only.
105
-
106
- ## Release
107
-
108
- Two channels, both published by CI from the exact bytes it validated — never from
109
- a workstation.
58
+ ## Publish
110
59
 
111
- ### Develop previews
112
-
113
- A preview is `<major.minor.patch>-dev.<pull-request number>`, e.g. `0.1.8-dev.107`.
114
- The nightly run (`03:17 UTC`) verifies current `origin/develop` and publishes only
115
- when that source's preview is absent. `npm run develop` requests the same GitHub
116
- Actions run and waits for it. The published version list is authoritative: an
117
- unpublished number is refused.
118
-
119
- Publish:
120
-
121
- ```sh
122
- npm run develop # request the preview publish run and wait for it
123
- ```
124
-
125
- Install:
60
+ ### Develop
126
61
 
127
62
  ```sh
128
- a1 update --develop 107 # install preview 107
129
- a1 update --develop 0.1.8-dev.107 # install that exact full preview version
63
+ npm run develop # request the preview publish
130
64
  ```
131
65
 
132
66
  ### Release
133
67
 
134
68
  ```sh
135
- npm run release -- patch # 0.1.8-dev -> 0.1.8
136
- npm run release -- minor # 0.1.8-dev -> 0.2.0
137
- npm run release -- major # 0.1.8-dev -> 1.0.0
138
- npm run release -- 0.4.0 # an exact stable version
69
+ npm run release -- patch # 0.1.8-dev -> 0.1.8
70
+ npm run release -- minor # 0.1.8-dev -> 0.2.0
71
+ npm run release -- major # 0.1.8-dev -> 1.0.0
72
+ npm run release -- 0.4.0 # an exact stable version
139
73
  ```
140
-
141
- Run from the repository root on a clean `develop` matching `origin/develop` that
142
- declares the open `-dev` version. A target is required; bare `npm run release`
143
- displays usage and releases nothing. `patch` promotes the current prerelease rather
144
- than skipping its stable version.
145
-
146
- The stable version is never committed. The command dispatches publication for the
147
- exact authoritative `develop` commit with the stable version named in the request,
148
- and waits for success. CI stamps that version on the checked-out source before
149
- packing, validates the packed release on Windows, Linux, and macOS, publishes to
150
- npm `latest` with provenance, then writes the `v<version>` tag on that same
151
- `develop` commit, records the GitHub Release, and fast-forwards `master`.
152
-
153
- Only after confirmed publication of `0.1.8` does the command prepare the one
154
- version-only PR, `0.1.9-dev`, in an isolated detached worktree, print its URL, and
155
- wait for you to **merge it manually**. It is not auto-merged, and green CI alone does
156
- not advance the release. Work added to your checkout during the wait is preserved,
157
- not reset. If publication fails or is uncertain, no reopening PR is prepared. If
158
- publication succeeded but reopening failed, merge or repair the reported PR by
159
- hand; do not republish the immutable stable version.
160
-
161
- `docs/ci-release-runbook.md` has the full picture.
package/bin/ui.js CHANGED
@@ -67,9 +67,12 @@ Promise.resolve().then(() => {
67
67
  return terminateOwnedUiProcess(code);
68
68
  },
69
69
  error => {
70
- if (fatal && error?.name !== "PiSessionSelectionError") { fatal.fail(error); return; }
70
+ const sessionSelectionError = error?.name === "PiSessionSelectionError";
71
+ const trustExit = error?.name === "ProjectTrustPromptExitError";
72
+ if (fatal && !sessionSelectionError && !trustExit) { fatal.fail(error); return; }
71
73
  fatal?.remove();
74
+ if (trustExit) return terminateOwnedUiProcess(error.exitCode);
72
75
  console.error(error instanceof Error ? error.message : String(error));
73
- process.exitCode = error?.name === "PiSessionSelectionError" ? error.exitCode : 1;
76
+ process.exitCode = sessionSelectionError ? error.exitCode : 1;
74
77
  },
75
78
  );
@@ -61,15 +61,9 @@ export declare class PromptChipStore {
61
61
  readonly image: PromptImageAttachment;
62
62
  }[];
63
63
  /**
64
- * Rehydrate a durable recall value into an editor-ready draft: image chip tags are looked up
65
- * against a caller-supplied sidecar resolver, and detectable URL / file / folder / large text
66
- * substrings are re-registered as atomic chips through the same identity rules the paste path
67
- * uses. Image chips whose sidecar returns null are silently stripped without a placeholder.
68
- *
69
- * Every chip returned is registered in the session `#chips` map so atomic-range,
70
- * hyperlink-range, and submission-expansion paths behave identically to a freshly typed draft.
71
- * Callers should treat the returned string as programmatic editor text, not another draft to
72
- * scan.
64
+ * Builds editor-ready recall. Images use sidecars, URL/path text is re-registered,
65
+ * multiline text stays expanded, and missing image payloads silently remove their tags.
66
+ * Returned chips remain live for atomic editing, links, and submission expansion.
73
67
  */
74
68
  rehydrateHistoryText(text: string, resolveImage: (id: string) => PromptImageAttachment | null): string;
75
69
  prepareSubmission(text: string): PreparedPrompt;
@@ -320,15 +320,9 @@ export class PromptChipStore {
320
320
  return results;
321
321
  }
322
322
  /**
323
- * Rehydrate a durable recall value into an editor-ready draft: image chip tags are looked up
324
- * against a caller-supplied sidecar resolver, and detectable URL / file / folder / large text
325
- * substrings are re-registered as atomic chips through the same identity rules the paste path
326
- * uses. Image chips whose sidecar returns null are silently stripped without a placeholder.
327
- *
328
- * Every chip returned is registered in the session `#chips` map so atomic-range,
329
- * hyperlink-range, and submission-expansion paths behave identically to a freshly typed draft.
330
- * Callers should treat the returned string as programmatic editor text, not another draft to
331
- * scan.
323
+ * Builds editor-ready recall. Images use sidecars, URL/path text is re-registered,
324
+ * multiline text stays expanded, and missing image payloads silently remove their tags.
325
+ * Returned chips remain live for atomic editing, links, and submission expansion.
332
326
  */
333
327
  rehydrateHistoryText(text, resolveImage) {
334
328
  if (!text)
@@ -346,6 +340,7 @@ export class PromptChipStore {
346
340
  this.#chips.set(match.text, { kind: "image", tag: match.text, image: attachment });
347
341
  return match.text;
348
342
  });
343
+ const preserveMultiline = resolvedImages.includes("\n");
349
344
  // Rationale: split around any surviving image tags so paste-time classification runs on the
350
345
  // intervening prose exactly as it would on a fresh clipboard payload, plus a substring URL
351
346
  // scan so embedded links become atomic chips even in the middle of surrounding text.
@@ -369,12 +364,9 @@ export class PromptChipStore {
369
364
  const body = segment.slice(leading.length, segment.length - trailing.length);
370
365
  if (body.length === 0)
371
366
  return segment;
372
- // Rationale: substring URL rehydration reuses paste-time URL classification and identity
373
- // rules, producing a URL chip whenever a bare http(s) URL appears in the prose. Whole-text
374
- // classification runs afterwards to catch path / large-paste candidates that consume the
375
- // entire remaining body.
367
+ // Rationale: keep multiline history readable; otherwise reuse paste-time URL/path classification.
376
368
  const withUrlChips = this.#rehydrateUrlSubstrings(body);
377
- if (withUrlChips !== body)
369
+ if (withUrlChips !== body || preserveMultiline)
378
370
  return `${leading}${withUrlChips}${trailing}`;
379
371
  const classified = this.transformPastedContent({ kind: "text", text: body });
380
372
  return `${leading}${classified}${trailing}`;
@@ -678,7 +678,10 @@ export class OwnedUiSessionShellRoot {
678
678
  return cached;
679
679
  const diagnostics = this.#view.diagnostics;
680
680
  const startupRows = diagnostics
681
- .filter(diagnostic => diagnostic.code === "engine-startup")
681
+ // Compatibility: the pinned route keeps trust failures with Pi-style startup diagnostics;
682
+ // bare A1 presents them in its prompt-adjacent notice dock instead.
683
+ .filter(diagnostic => diagnostic.code === "engine-startup"
684
+ || !this.#customViewport && diagnostic.code === "project-trust")
682
685
  .flatMap(diagnostic => renderPiShellStartupDiagnostic(diagnostic, width));
683
686
  const resourceRows = this.#customViewport ? [] : [...this.resources.render(width)];
684
687
  if (resourceRows.at(-1) === "")
@@ -744,8 +747,9 @@ export class OwnedUiSessionShellRoot {
744
747
  ? createPiShellCollapsedChangelog().render(width)
745
748
  : createPiShellChangelog(diagnostic.message).render(width)));
746
749
  rows.push(...diagnostics
747
- .filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "package-updates"
748
- && diagnostic.code !== "changelog-collapsed" && diagnostic.code !== "changelog-expanded")
750
+ .filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "project-trust"
751
+ && diagnostic.code !== "package-updates" && diagnostic.code !== "changelog-collapsed"
752
+ && diagnostic.code !== "changelog-expanded")
749
753
  .slice(-3)
750
754
  .flatMap(diagnostic => renderPiShellTranscriptBlock({
751
755
  id: `diagnostic-${diagnostic.sequence}`,
@@ -55,8 +55,9 @@ export class OwnedUiSessionShell {
55
55
  #shutdownPromise;
56
56
  #disposePromise;
57
57
  #pointerReporting = false;
58
- // Invariant: the startup release-note diagnostic creates at most one dock notice per shell.
58
+ // Invariant: translated startup diagnostics create at most one dock notice per kind per shell.
59
59
  #startupChangelogHandled = false;
60
+ #startupTrustHandled = false;
60
61
  #customViewport;
61
62
  #responseCopy;
62
63
  // Rationale: only an executor this shell created owns a spare copy helper worth warming and disposing.
@@ -1706,6 +1707,7 @@ export class OwnedUiSessionShell {
1706
1707
  this.root.update(view);
1707
1708
  this.#syncDialog(view.dialog);
1708
1709
  this.#presentStartupChangelog(view);
1710
+ this.#presentStartupTrustWarning(view);
1709
1711
  this.runtime.requestRender();
1710
1712
  for (const listener of this.#listeners)
1711
1713
  listener(view);
@@ -1723,6 +1725,17 @@ export class OwnedUiSessionShell {
1723
1725
  this.#startupChangelogHandled = true;
1724
1726
  this.root.appendWorkflowStatus(STARTUP_CHANGELOG_NOTICE);
1725
1727
  }
1728
+ // Rationale: a cancelled or failed trust choice explains the restricted session where the
1729
+ // user will next type, rather than leaving a lone warning at the top of an empty viewport.
1730
+ #presentStartupTrustWarning(view) {
1731
+ if (this.#startupTrustHandled || !this.#customViewport)
1732
+ return;
1733
+ const diagnostic = view.diagnostics.find(candidate => candidate.code === "project-trust");
1734
+ if (diagnostic === undefined || !this.runtime.active)
1735
+ return;
1736
+ this.#startupTrustHandled = true;
1737
+ this.root.appendWorkflowMessage({ kind: "warning", message: diagnostic.message });
1738
+ }
1726
1739
  #openOwnedRoute(route, input) {
1727
1740
  const surface = this.#routeHost?.open(route, input) ?? null;
1728
1741
  if (surface === null)
@@ -1,2 +1,2 @@
1
1
  /** Fixed startup-safe rendering only; project-derived presentation is forbidden before trust resolves. */
2
- export declare function renderProjectTrustDialog(cwd: string, selected: number, terminalWidth: number, terminalRows: number): readonly string[];
2
+ export declare function renderProjectTrustDialog(cwd: string, options: readonly string[], selected: number, terminalWidth: number, terminalRows: number): readonly string[];
@@ -7,13 +7,13 @@ const BOLD = "\u001b[1m";
7
7
  const RESET_BOLD = "\u001b[22m";
8
8
  const EXPLANATION = "This allows to load project settings and resources, install missing project packages, and execute project extensions.";
9
9
  /** Fixed startup-safe rendering only; project-derived presentation is forbidden before trust resolves. */
10
- export function renderProjectTrustDialog(cwd, selected, terminalWidth, terminalRows) {
10
+ export function renderProjectTrustDialog(cwd, options, selected, terminalWidth, terminalRows) {
11
11
  const width = Math.max(1, terminalWidth);
12
12
  const rule = `${BORDER}${"─".repeat(width)}${RESET_FG}`;
13
13
  const title = ` ${BOLD}${ACCENT}Trust project folder?${RESET_FG}${RESET_BOLD}`;
14
14
  const path = ` ${MUTED}${sanitize(cwd)}${RESET_FG}`;
15
- const choices = [choice("Trust", selected === 0), choice("Do not trust", selected === 1)];
16
- const hint = ` ${DIM}↑/↓${MUTED} to navigate ${DIM}Enter${MUTED} to select ${DIM}Esc${MUTED} to cancel${RESET_FG}`;
15
+ const choices = options.map((label, index) => choice(label, selected === index));
16
+ const hint = ` ${DIM}↑/↓${MUTED} to navigate ${DIM}Enter${MUTED} to select ${DIM}Esc${MUTED} to exit${RESET_FG}`;
17
17
  const explanation = wrap(EXPLANATION, Math.max(1, width - 2)).map(line => ` ${line}`);
18
18
  const preferred = [rule, title, path, "", ...explanation, "", ...choices, "", hint, rule];
19
19
  if (preferred.length <= terminalRows)
@@ -28,14 +28,18 @@ export function renderProjectTrustDialog(cwd, selected, terminalWidth, terminalR
28
28
  if (essential.length <= terminalRows)
29
29
  return essential;
30
30
  if (terminalRows >= 3)
31
- return [...choices, hint];
31
+ return [title, ...choiceWindow(choices, selected, terminalRows - 2), hint];
32
32
  if (terminalRows === 2)
33
- return choices;
33
+ return choiceWindow(choices, selected, 2);
34
34
  return [choices[selected] ?? choices[0] ?? ""];
35
35
  }
36
36
  function choice(label, selected) {
37
37
  return selected ? ` ${ACCENT}→ ${label}${RESET_FG}` : ` ${label}`;
38
38
  }
39
+ function choiceWindow(choices, selected, capacity) {
40
+ const start = Math.min(Math.max(0, selected - Math.floor(capacity / 2)), Math.max(0, choices.length - capacity));
41
+ return choices.slice(start, start + capacity);
42
+ }
39
43
  function wrap(text, width) {
40
44
  const lines = [];
41
45
  let line = "";
@@ -1,9 +1,14 @@
1
1
  import type { Readable, Writable } from "node:stream";
2
+ export type OwnedProjectTrustChoiceId = "trust" | "trust-parent" | "trust-session" | "deny" | "deny-session";
2
3
  export interface OwnedProjectTrustPromptRequest {
3
4
  readonly cwd: string;
4
5
  readonly defaultDecision: "ask" | "always" | "never";
6
+ readonly choices: readonly {
7
+ readonly id: OwnedProjectTrustChoiceId;
8
+ readonly label: string;
9
+ }[];
5
10
  }
6
- export type OwnedProjectTrustPrompt = (request: OwnedProjectTrustPromptRequest) => Promise<boolean | null>;
11
+ export type OwnedProjectTrustPrompt = (request: OwnedProjectTrustPromptRequest) => Promise<OwnedProjectTrustChoiceId | null>;
7
12
  interface RawTtyInput extends Readable {
8
13
  readonly isTTY?: boolean;
9
14
  readonly isRaw?: boolean;
@@ -1,7 +1,6 @@
1
+ import { EMERGENCY_TERMINAL_RESET } from "../../foundation/terminal-cleanup/terminal-reset.js";
1
2
  const ENTER_ALTERNATE_SCREEN = "\u001b[?1049h";
2
- const LEAVE_ALTERNATE_SCREEN = "\u001b[?1049l";
3
3
  const HIDE_CURSOR = "\u001b[?25l";
4
- const SHOW_CURSOR = "\u001b[?25h";
5
4
  const CLEAR_HOME = "\u001b[2J\u001b[H";
6
5
  const ACCENT = "\u001b[38;2;138;190;183m";
7
6
  const MUTED = "\u001b[38;2;128;128;128m";
@@ -9,15 +8,26 @@ const DIM = "\u001b[38;2;102;102;102m";
9
8
  const RESET_FG = "\u001b[39m";
10
9
  const BOLD = "\u001b[1m";
11
10
  const RESET_BOLD = "\u001b[22m";
11
+ const PROJECT_TRUST_EXIT = "ProjectTrustPromptExitError";
12
+ class ProjectTrustPromptExitError extends Error {
13
+ exitCode = 0;
14
+ constructor() {
15
+ super("Project trust prompt exited");
16
+ this.name = PROJECT_TRUST_EXIT;
17
+ }
18
+ }
12
19
  /** Fixed pre-resource selector; no project resource is available. */
13
20
  export function createConsoleProjectTrustPrompt(options = {}) {
14
21
  const input = options.input ?? process.stdin;
15
22
  const output = options.output ?? process.stdout;
16
- return async ({ cwd }) => {
23
+ return async ({ cwd, choices }) => {
17
24
  if (input.isTTY !== true || output.isTTY !== true) {
18
25
  throw new Error("an interactive terminal is unavailable");
19
26
  }
20
- const renderBareDialog = options.presentation === "comparison"
27
+ if (choices.length === 0)
28
+ throw new Error("project trust choices are unavailable");
29
+ const comparison = options.presentation === "comparison";
30
+ const renderBareDialog = comparison
21
31
  ? undefined : (await import("./project-trust-dialog.js")).renderProjectTrustDialog;
22
32
  const wasRaw = input.isRaw === true;
23
33
  let selected = 0;
@@ -27,19 +37,18 @@ export function createConsoleProjectTrustPrompt(options = {}) {
27
37
  return;
28
38
  restored = true;
29
39
  input.setRawMode?.(wasRaw);
30
- output.write(`${CLEAR_HOME}${SHOW_CURSOR}${LEAVE_ALTERNATE_SCREEN}`);
40
+ output.write(`${CLEAR_HOME}${EMERGENCY_TERMINAL_RESET}`);
31
41
  };
32
42
  const render = () => {
33
43
  const width = Math.max(renderBareDialog ? 1 : 20, output.columns ?? 80);
34
44
  const rows = Math.max(1, output.rows ?? 24);
35
- const lines = (renderBareDialog?.(cwd, selected, width, rows) ?? [
45
+ const lines = (renderBareDialog?.(cwd, choices.map(choice => choice.label), selected, width, rows) ?? [
36
46
  `${BOLD}${ACCENT}Trust project folder?${RESET_FG}${RESET_BOLD}`,
37
47
  cwd,
38
48
  "",
39
49
  "This allows a1 to load project settings and resources, install missing project packages, and execute project extensions.",
40
50
  "",
41
- optionRow("Trust", selected === 0),
42
- optionRow("Do not trust", selected === 1),
51
+ ...choices.map((choice, index) => optionRow(choice.label, selected === index)),
43
52
  "",
44
53
  `${DIM}↑/↓${MUTED} to navigate ${DIM}Enter${MUTED} to select ${DIM}Esc${MUTED} to cancel${RESET_FG}`,
45
54
  ]).map(line => clipAnsiSafe(line, width));
@@ -66,19 +75,20 @@ export function createConsoleProjectTrustPrompt(options = {}) {
66
75
  settled = true;
67
76
  cleanup();
68
77
  restore();
78
+ // Protocol: restoration preserves the parent cursor; only the shell paints there.
69
79
  reject(error);
70
80
  };
71
81
  const onData = (chunk) => {
72
82
  const data = chunk.toString();
73
83
  for (let index = 0; index < data.length;) {
74
84
  if (data.startsWith("\u001b[A", index)) {
75
- selected = 0;
85
+ selected = (selected - 1 + choices.length) % choices.length;
76
86
  index += 3;
77
87
  render();
78
88
  continue;
79
89
  }
80
90
  if (data.startsWith("\u001b[B", index) || data[index] === "\t") {
81
- selected = selected === 0 ? 1 : 0;
91
+ selected = (selected + 1) % choices.length;
82
92
  index += data[index] === "\t" ? 1 : 3;
83
93
  render();
84
94
  continue;
@@ -86,21 +96,31 @@ export function createConsoleProjectTrustPrompt(options = {}) {
86
96
  const key = data[index] ?? "";
87
97
  index += 1;
88
98
  if (key === "\r" || key === "\n") {
89
- finish(selected === 0);
99
+ finish(choices[selected]?.id ?? null);
90
100
  return;
91
101
  }
92
- if (key === "\u001b" || key === "\u0003") {
93
- finish(null);
102
+ if (key === "\u001b") {
103
+ if (comparison)
104
+ finish(null);
105
+ else
106
+ fail(new ProjectTrustPromptExitError());
94
107
  return;
95
108
  }
109
+ if (key === "\u0003") {
110
+ if (comparison) {
111
+ finish(null);
112
+ return;
113
+ }
114
+ continue;
115
+ }
96
116
  // Compatibility: retain y/n aliases for terminals or automation that cannot send
97
117
  // navigation keys; the visible interaction remains selector-first.
98
118
  if (key === "y" || key === "Y") {
99
- finish(true);
119
+ finish(choices.find(choice => choice.id === "trust")?.id ?? null);
100
120
  return;
101
121
  }
102
122
  if (key === "n" || key === "N") {
103
- finish(false);
123
+ finish(choices.find(choice => choice.id === "deny")?.id ?? null);
104
124
  return;
105
125
  }
106
126
  }
@@ -1,9 +1,10 @@
1
1
  import { writeSync } from "node:fs";
2
2
  // Protocol: reset modes on both screens; setting keyboard flags to zero is idempotent,
3
3
  // unlike repeatedly popping a keyboard-protocol stack owned by a parent application.
4
+ // Resetting parent-screen margins homes the cursor, so preserve its restored position.
4
5
  export const EMERGENCY_TERMINAL_RESET = "\x1b[?2026l\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1004l\x1b[?1006l"
5
6
  + "\x1b[?2004l\x1b[=0u\x1b[>4;0m\x1b]8;;\x1b\\\x1b[0m\x1b[r\x1b[?7h\x1b[?25h"
6
- + "\x1b[?1049l\x1b[r\x1b[?7h\x1b[?25h\x1b]9;4;0\x07";
7
+ + "\x1b[?1049l\x1b7\x1b[r\x1b8\x1b[?7h\x1b[?25h\x1b]9;4;0\x07";
7
8
  export function restoreProcessTerminal() {
8
9
  try {
9
10
  if (process.stdout.isTTY)
@@ -2,10 +2,12 @@
2
2
  * Provenance: @earendil-works/pi-tui 0.87.1 (MIT), commit f07218c4d4bbc12bef056a7058c3dd49dfe41abe,
3
3
  * packages/tui/src/components/editor.ts.
4
4
  * Modifications: Owned editor core or minimal editor-local helper subset; public imports, strict
5
- * types, typed persistent-history hooks, and semantic border state with the user-approved numeric-only
6
- * history label. Public terminal runtime/exports remain shared and unchanged. See
7
- * docs/architecture/history-editor-provenance.md.
8
- * Deviations: compact-history-counter-label, persistent-history-owned-editor-boundary.
5
+ * types, typed persistent-history hooks, semantic border state with the user-approved numeric-only
6
+ * history label and separate centered history-overflow cue, and history-count retention during cursor
7
+ * placement within recalled multiline text. Public terminal runtime/exports remain shared and
8
+ * unchanged. See docs/architecture/history-editor-provenance.md.
9
+ * Deviations: compact-history-counter-label, history-overflow-cue-separation,
10
+ * history-recall-cursor-retention, persistent-history-owned-editor-boundary.
9
11
  */
10
12
  import { type AutocompleteProvider, type Component, type Focusable, type TUI, type TuiMouseEvent, type TuiMouseEventResult, type SelectListTheme } from "@earendil-works/pi-tui";
11
13
  import type { EditorInteractionPort, EditorRecallPort } from "../../editor-interaction.js";
@@ -2,10 +2,12 @@
2
2
  * Provenance: @earendil-works/pi-tui 0.87.1 (MIT), commit f07218c4d4bbc12bef056a7058c3dd49dfe41abe,
3
3
  * packages/tui/src/components/editor.ts.
4
4
  * Modifications: Owned editor core or minimal editor-local helper subset; public imports, strict
5
- * types, typed persistent-history hooks, and semantic border state with the user-approved numeric-only
6
- * history label. Public terminal runtime/exports remain shared and unchanged. See
7
- * docs/architecture/history-editor-provenance.md.
8
- * Deviations: compact-history-counter-label, persistent-history-owned-editor-boundary.
5
+ * types, typed persistent-history hooks, semantic border state with the user-approved numeric-only
6
+ * history label and separate centered history-overflow cue, and history-count retention during cursor
7
+ * placement within recalled multiline text. Public terminal runtime/exports remain shared and
8
+ * unchanged. See docs/architecture/history-editor-provenance.md.
9
+ * Deviations: compact-history-counter-label, history-overflow-cue-separation,
10
+ * history-recall-cursor-retention, persistent-history-owned-editor-boundary.
9
11
  */
10
12
  import { getKeybindings, matchesKey, CURSOR_MARKER, sliceByColumn, truncateToWidth, visibleWidth, SelectList } from "@earendil-works/pi-tui";
11
13
  import { decodePrintableKey } from "./printable-key.js";
@@ -187,21 +189,27 @@ function buildDebouncePattern(triggerCharacters) {
187
189
  const escapedWithoutAt = triggerCharacters.filter((character) => character !== "@").map(escapeCharacterClass);
188
190
  return new RegExp(`${autocompleteBoundaryRegex.source}(?:@(?:"[^"]*|${unquotedAutocompleteSuffixRegex.source})|[${escapedWithoutAt.join("")}]${unquotedAutocompleteSuffixRegex.source})$`, "u");
189
191
  }
190
- function createScrollBorder(direction, hiddenLineCount, width) {
192
+ function createScrollBorder(direction, hiddenLineCount, width, minimumCueStart = 0) {
191
193
  const availableWidth = Math.max(0, width);
192
194
  const label = ` ${direction} ${hiddenLineCount} more `;
193
195
  const labelWidth = visibleWidth(label);
194
196
  if (labelWidth + 2 <= availableWidth) {
195
- const leftWidth = Math.floor((availableWidth - labelWidth) / 2);
196
- return "─".repeat(leftWidth) + label + "─".repeat(availableWidth - leftWidth - labelWidth);
197
+ const centeredStart = Math.floor((availableWidth - labelWidth) / 2);
198
+ const cueStart = Math.max(centeredStart, minimumCueStart);
199
+ if (cueStart + labelWidth <= availableWidth) {
200
+ return "─".repeat(cueStart) + label + "─".repeat(availableWidth - cueStart - labelWidth);
201
+ }
197
202
  }
203
+ if (minimumCueStart > 0)
204
+ return "─".repeat(availableWidth);
198
205
  const indicator = `─── ${direction} ${hiddenLineCount} more `;
199
- const remaining = availableWidth - visibleWidth(indicator);
206
+ const indicatorWidth = visibleWidth(indicator);
207
+ const remaining = availableWidth - indicatorWidth;
200
208
  if (remaining >= 0)
201
209
  return indicator + "─".repeat(remaining);
202
210
  const ellipsis = "...".slice(0, availableWidth);
203
- const indicatorWidth = availableWidth - visibleWidth(ellipsis);
204
- return sliceByColumn(indicator, 0, indicatorWidth, true) + ellipsis;
211
+ const visibleIndicatorWidth = availableWidth - visibleWidth(ellipsis);
212
+ return sliceByColumn(indicator, 0, visibleIndicatorWidth, true) + ellipsis;
205
213
  }
206
214
  export class HistoryEditorCore {
207
215
  segmentTransform;
@@ -479,8 +487,19 @@ export class HistoryEditorCore {
479
487
  }
480
488
  renderTopBorder(width, hiddenLineCount) {
481
489
  if (this.persistentHistory && this.historyIndex >= 0) {
482
- const overflow = hiddenLineCount > 0 ? ` · ↑ ${hiddenLineCount} more` : "";
483
- const label = `─── ${this.history.length - this.historyIndex}/${this.history.length}${overflow} `;
490
+ const history = `${this.history.length - this.historyIndex}/${this.history.length} `;
491
+ if (hiddenLineCount > 0) {
492
+ const historyStart = 4;
493
+ const historyEnd = historyStart + visibleWidth(history);
494
+ const historyAnchorStart = historyStart - 1;
495
+ if (historyEnd <= width) {
496
+ const border = createScrollBorder("↑", hiddenLineCount, width, historyEnd);
497
+ return this.borderColor(border.slice(0, historyAnchorStart) + " ")
498
+ + this.styleHistoryLabel(history)
499
+ + this.borderColor(border.slice(historyEnd));
500
+ }
501
+ }
502
+ const label = `─── ${history}`;
484
503
  const shown = truncateToWidth(label, width);
485
504
  const remaining = Math.max(0, width - visibleWidth(shown));
486
505
  return this.borderColor(shown.slice(0, 4)) + this.styleHistoryLabel(shown.slice(4)) + this.borderColor("─".repeat(remaining));
@@ -642,7 +661,8 @@ export class HistoryEditorCore {
642
661
  this.state.cursorLine = visualLine.logicalLine;
643
662
  this.setCursorCol(visualLine.startCol + targetIndex);
644
663
  this.lastAction = null;
645
- this.exitHistoryBrowsing();
664
+ // Cursor placement within recalled multiline text does not leave browsing:
665
+ // keep its position/total border visible until content changes or recall exits.
646
666
  if (this.autocompleteState)
647
667
  this.updateAutocomplete();
648
668
  return { handled: true, focus: true };
@@ -1,8 +1,13 @@
1
+ export type PiProjectTrustChoiceId = "trust" | "trust-parent" | "trust-session" | "deny" | "deny-session";
1
2
  export interface PiProjectTrustPreflightRequest {
2
3
  readonly cwd: string;
3
4
  readonly defaultDecision: "ask" | "always" | "never";
5
+ readonly choices: readonly {
6
+ readonly id: PiProjectTrustChoiceId;
7
+ readonly label: string;
8
+ }[];
4
9
  }
5
- export type PiProjectTrustPreflightPrompt = (request: PiProjectTrustPreflightRequest) => Promise<boolean | null>;
10
+ export type PiProjectTrustPreflightPrompt = (request: PiProjectTrustPreflightRequest) => Promise<PiProjectTrustChoiceId | null>;
6
11
  export interface PiProjectTrustPreflightResult {
7
12
  readonly trusted: boolean;
8
13
  readonly source: "saved" | "default" | "interactive" | "fail-closed";
@@ -1,3 +1,5 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
1
3
  import { ProjectTrustStore, SettingsManager, } from "../startup-public.js";
2
4
  /**
3
5
  * Resolves trust from global state only. It never constructs a project-trusted
@@ -25,7 +27,12 @@ export async function resolvePiProjectTrustPreflight(options) {
25
27
  };
26
28
  }
27
29
  try {
28
- const decision = await options.prompt({ cwd: options.cwd, defaultDecision: fallback });
30
+ const choices = projectTrustChoices(options.cwd);
31
+ const decision = await options.prompt({
32
+ cwd: options.cwd,
33
+ defaultDecision: fallback,
34
+ choices: choices.map(({ id, label }) => ({ id, label })),
35
+ });
29
36
  if (decision === null) {
30
37
  return {
31
38
  trusted: false,
@@ -33,10 +40,18 @@ export async function resolvePiProjectTrustPreflight(options) {
33
40
  diagnostic: `Project resources in ${options.cwd} were withheld because trust selection was cancelled`,
34
41
  };
35
42
  }
36
- trustStore.set(options.cwd, decision);
37
- return { trusted: decision, source: "interactive", diagnostic: null };
43
+ const selected = choices.find(choice => choice.id === decision);
44
+ if (selected === undefined)
45
+ throw new Error(`unknown trust selection: ${decision}`);
46
+ if (selected.updates.length > 0)
47
+ trustStore.setMany([...selected.updates]);
48
+ return { trusted: selected.trusted, source: "interactive", diagnostic: null };
38
49
  }
39
50
  catch (error) {
51
+ // Protocol: an explicit bare-A1 exit propagates after the prompt restores the terminal;
52
+ // ordinary prompt failures still continue through the fail-closed restricted shell.
53
+ if (error instanceof Error && error.name === "ProjectTrustPromptExitError")
54
+ throw error;
40
55
  return {
41
56
  trusted: false,
42
57
  source: "fail-closed",
@@ -44,3 +59,26 @@ export async function resolvePiProjectTrustPreflight(options) {
44
59
  };
45
60
  }
46
61
  }
62
+ /** Pinned Pi's five startup outcomes, with canonical parent scope resolved before project resources load. */
63
+ function projectTrustChoices(cwd) {
64
+ let trustPath = resolve(cwd);
65
+ try {
66
+ trustPath = realpathSync(trustPath);
67
+ }
68
+ catch {
69
+ // Compatibility: pinned Pi keeps the resolved path when real-path lookup fails.
70
+ }
71
+ const parent = dirname(trustPath);
72
+ return [
73
+ { id: "trust", label: "Trust", trusted: true, updates: [{ path: trustPath, decision: true }] },
74
+ ...(parent === trustPath ? [] : [{
75
+ id: "trust-parent",
76
+ label: `Trust parent folder (${parent})`,
77
+ trusted: true,
78
+ updates: [{ path: parent, decision: true }, { path: trustPath, decision: null }],
79
+ }]),
80
+ { id: "trust-session", label: "Trust (this session only)", trusted: true, updates: [] },
81
+ { id: "deny", label: "Do not trust", trusted: false, updates: [{ path: trustPath, decision: false }] },
82
+ { id: "deny-session", label: "Do not trust (this session only)", trusted: false, updates: [] },
83
+ ];
84
+ }
@@ -91,7 +91,7 @@ export async function createPiRuntimeIntegration(options) {
91
91
  ...created,
92
92
  services,
93
93
  diagnostics: [
94
- ...(trust.diagnostic === null ? [] : [{ type: "warning", message: trust.diagnostic }]),
94
+ ...(trust.diagnostic === null ? [] : [{ type: "warning", code: "project-trust", message: trust.diagnostic }]),
95
95
  ...modelScope.diagnostics,
96
96
  ],
97
97
  };
@@ -116,7 +116,10 @@ export class PiEngineRuntime {
116
116
  this.#ports.rebound();
117
117
  });
118
118
  for (const diagnostic of [...runtime.diagnostics, ...runtime.services.diagnostics]) {
119
- this.#ports.diagnostic(diagnostic.type === "error" ? "error" : diagnostic.type === "warning" ? "warning" : "info", "engine-startup", diagnostic.message, diagnostic.type !== "error");
119
+ const code = "code" in diagnostic && diagnostic.code === "project-trust"
120
+ ? "project-trust"
121
+ : "engine-startup";
122
+ this.#ports.diagnostic(diagnostic.type === "error" ? "error" : diagnostic.type === "warning" ? "warning" : "info", code, diagnostic.message, diagnostic.type !== "error");
120
123
  }
121
124
  this.bindSession(runtime.session);
122
125
  await this.#announceChangelog(runtime.services.settingsManager);
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-27T18:55:25.844Z",
8
+ "builtAt": "2026-09-28T08:09:05.323Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-27T18:55:10.602Z",
8
+ "builtAt": "2026-09-28T08:08:49.459Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-27T18:56:04.854Z",
8
+ "builtAt": "2026-09-28T08:09:42.804Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "b888d78656b1112789dbb9b3d990082c8bdecc23256518478a1381c283874092",
11
+ "sha256": "9f36428f29224cc424dd262aa0495a075e915c9ecce6bdfcc45bd972270afdde",
12
12
  "size": 178176
13
13
  },
14
14
  "provenance": {
@@ -41,11 +41,19 @@ The editor renders position/overflow in its existing border. The position label
41
41
  uses an injected neutral status-text style (`dim`), while the surrounding rules
42
42
  retain the active input-border color, as clarified during manual review. The approved
43
43
  `show-autocomplete-above-prompt` review refinement omits the literal `History` title:
44
- `─── 1/100 ─…` keeps the same four-cell inset, count calculation, dim color, optional
45
- scroll-overflow suffix, and clipping. Core and shell regressions verify numbering,
46
- 100-entry recall, draft restoration, and the absence of the title. Recall temporarily
47
- separates the draft's live paste backing from recalled literal text, restoring it
48
- on return; ordinary pinned mode remains source-equivalent.
44
+ `─── 1/100 ─…` keeps the same four-cell inset, count calculation, dim color, and
45
+ clipping. When recalled input has hidden lines above, the approved
46
+ `separate-history-scroll-label` refinement keeps that count left-aligned without a
47
+ separator and independently centers `↑ N more` in the active border color, matching
48
+ the lower cue. At collision widths the history count remains visible while a complete
49
+ cue shifts right only as needed, or is omitted when both labels cannot fit. Cursor
50
+ movement and pointer placement within recalled multiline text retain the active browse
51
+ position and its count; content mutation or leaving recall still ends that presentation.
52
+ Core and shell regressions verify numbering, 100-entry recall, separated styling and
53
+ placement, multiline cursor-placement continuity, narrow-width history continuity and
54
+ recovery, draft restoration, and the absence of the title. Recall temporarily separates the draft's
55
+ live paste backing from recalled literal text, restoring it on return; ordinary
56
+ pinned mode remains source-equivalent.
49
57
 
50
58
  The integration inventory is:
51
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.2.1-dev.604",
3
+ "version": "0.2.1",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "privateLaunchContract": "neutral-launch-v1",