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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/bin/cli.js +4 -0
  2. package/dist/app/session-shell/session-shell-root.js +15 -5
  3. package/dist/cli/dispatch.d.ts +5 -0
  4. package/dist/cli/dispatch.js +26 -0
  5. package/dist/cli/session-context.d.ts +9 -0
  6. package/dist/cli/session-context.js +29 -0
  7. package/dist/composition/owned-ui.js +6 -2
  8. package/dist/contracts/owned-ui/model.d.ts +5 -0
  9. package/dist/contracts/owned-ui/validation.js +6 -0
  10. package/dist/contracts/presentation/index.d.ts +13 -0
  11. package/dist/contracts/presentation/index.js +30 -0
  12. package/dist/features/launch/index.d.ts +2 -0
  13. package/dist/features/launch/index.js +1 -0
  14. package/dist/features/launch/session-context.d.ts +8 -0
  15. package/dist/features/launch/session-context.js +8 -0
  16. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  17. package/dist/features/owned-ui/reference-screen-app.js +7 -8
  18. package/dist/features/owned-ui/settings-app.js +9 -5
  19. package/dist/foundation/lifecycle/index.d.ts +2 -0
  20. package/dist/foundation/lifecycle/index.js +1 -0
  21. package/dist/foundation/lifecycle/session-repository-context.d.ts +18 -0
  22. package/dist/foundation/lifecycle/session-repository-context.js +173 -0
  23. package/dist/integrations/pi/components/models-dialog.js +9 -9
  24. package/dist/integrations/pi/components/shell-editor-autocomplete.js +27 -3
  25. package/dist/integrations/pi/components/shell-extension-ui.js +4 -2
  26. package/dist/integrations/pi/components/shell-footer-status.js +1 -1
  27. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -2
  28. package/dist/integrations/pi/components/skills-command.d.ts +2 -0
  29. package/dist/integrations/pi/components/skills-command.js +11 -10
  30. package/dist/integrations/pi/components/skills-dialog.js +8 -3
  31. package/dist/integrations/pi/components/theme.d.ts +4 -0
  32. package/dist/integrations/pi/components/theme.js +10 -0
  33. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +4 -4
  34. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -2
  35. package/dist/integrations/pi/components/upstream/components/extension-editor.js +9 -10
  36. package/dist/integrations/pi/components/upstream/components/extension-input.d.ts +17 -0
  37. package/dist/integrations/pi/components/upstream/components/extension-input.js +72 -0
  38. package/dist/integrations/pi/components/upstream/components/extension-selector.d.ts +15 -0
  39. package/dist/integrations/pi/components/upstream/components/extension-selector.js +92 -0
  40. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -2
  41. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +16 -15
  42. package/dist/integrations/pi/components/upstream/components/session-footer.js +25 -9
  43. package/dist/integrations/pi/components/upstream/components/session-selector.js +25 -16
  44. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +6 -4
  45. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +22 -12
  46. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +3 -2
  47. package/dist/integrations/pi/components/upstream/components/tree-selector.js +14 -12
  48. package/dist/integrations/pi/components/upstream/components/trust-selector.js +11 -5
  49. package/dist/integrations/pi/engine/adapter.d.ts +2 -1
  50. package/dist/integrations/pi/engine/adapter.js +2 -0
  51. package/dist/integrations/pi/engine/message-values.js +1 -0
  52. package/dist/integrations/pi/engine/session-runtime.d.ts +11 -2
  53. package/dist/integrations/pi/engine/session-runtime.js +136 -33
  54. package/dist/integrations/pi/engine/transcript-projection.js +35 -0
  55. package/dist/integrations/pi/startup-public.manifest.json +2 -0
  56. package/dist/native/darwin-arm64/manifest.json +1 -1
  57. package/dist/native/linux-x64/manifest.json +1 -1
  58. package/dist/native/win32-x64/manifest.json +2 -2
  59. package/dist/native/win32-x64/process-guardian.exe +0 -0
  60. package/dist/product-identity.d.ts +1 -1
  61. package/dist/product-identity.js +1 -1
  62. package/dist/product-identity.json +2 -1
  63. package/dist/ui/components/dialog-panel.d.ts +2 -1
  64. package/dist/ui/components/dialog-panel.js +6 -1
  65. package/dist/ui/components/index.d.ts +2 -0
  66. package/dist/ui/components/index.js +1 -0
  67. package/dist/ui/components/shortcut-hints.d.ts +7 -0
  68. package/dist/ui/components/shortcut-hints.js +12 -0
  69. package/dist/ui/components/shortcuts.d.ts +2 -0
  70. package/dist/ui/components/shortcuts.js +5 -2
  71. package/dist/ui/components/transcript-viewport.d.ts +1 -1
  72. package/docs/ci-release-runbook.md +9 -7
  73. package/docs/openspec-archive-automation.md +6 -0
  74. package/docs/validation.md +5 -2
  75. package/package.json +1 -1
@@ -0,0 +1,173 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { open, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
6
+ import { resolveProductPaths } from "./paths.js";
7
+ const SCHEMA = "a1-session-repository-context-v1";
8
+ const MAX_RECORD_BYTES = 16 * 1024;
9
+ const MAX_HEADER_BYTES = 64 * 1024;
10
+ const GIT_TIMEOUT_MS = 5_000;
11
+ /** Persist one explicit same-repository worktree association for a stable Pi session. */
12
+ export async function setSessionRepositoryContext(identity, worktreePath, options = {}) {
13
+ const header = await readSessionHeader(identity);
14
+ const startup = await readGitWorktreeIdentity(header.cwd, false, options.signal);
15
+ const selected = await readGitWorktreeIdentity(worktreePath, true, options.signal);
16
+ if (!samePath(startup.commonDir, selected.commonDir)) {
17
+ throw new Error("associated worktree belongs to a different Git repository");
18
+ }
19
+ const record = {
20
+ schema: SCHEMA,
21
+ sessionId: header.id,
22
+ sessionFile: header.file,
23
+ startupRoot: startup.root,
24
+ startupCommonDir: startup.commonDir,
25
+ worktreeRoot: selected.root,
26
+ worktreeCommonDir: selected.commonDir,
27
+ worktreeGitDir: selected.gitDir,
28
+ updatedAt: new Date().toISOString(),
29
+ };
30
+ const destination = recordPath(header, options);
31
+ const directory = dirname(destination);
32
+ await mkdir(directory, { recursive: true, mode: 0o700 });
33
+ const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
34
+ try {
35
+ await writeFile(temporary, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
36
+ await rename(temporary, destination);
37
+ }
38
+ finally {
39
+ await rm(temporary, { force: true }).catch(() => undefined);
40
+ }
41
+ return { cwd: selected.root, branch: requireBranch(selected) };
42
+ }
43
+ /** Remove only the association belonging to the supplied stable session. */
44
+ export async function clearSessionRepositoryContext(identity, options = {}) {
45
+ const header = await readSessionHeader(identity);
46
+ await rm(recordPath(header, options), { force: true });
47
+ }
48
+ /** Read and revalidate a persisted association; malformed or stale state fails closed to absence. */
49
+ export async function readSessionRepositoryContext(identity, options = {}) {
50
+ let header;
51
+ try {
52
+ header = await readSessionHeader(identity);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ try {
58
+ const path = recordPath(header, options);
59
+ const metadata = await lstat(path);
60
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size === 0 || metadata.size > MAX_RECORD_BYTES)
61
+ return null;
62
+ const bytes = await readFile(path, { signal: options.signal });
63
+ const value = JSON.parse(bytes.toString("utf8"));
64
+ if (!isRecord(value)
65
+ || value.schema !== SCHEMA
66
+ || value.sessionId !== header.id
67
+ || value.sessionFile !== header.file
68
+ || !validPath(value.startupRoot)
69
+ || !validPath(value.startupCommonDir)
70
+ || !validPath(value.worktreeRoot)
71
+ || !validPath(value.worktreeCommonDir)
72
+ || !validPath(value.worktreeGitDir)
73
+ || typeof value.updatedAt !== "string")
74
+ return null;
75
+ const startup = await readGitWorktreeIdentity(header.cwd, false, options.signal);
76
+ const selected = await readGitWorktreeIdentity(value.worktreeRoot, true, options.signal);
77
+ if (!samePath(startup.root, value.startupRoot)
78
+ || !samePath(startup.commonDir, value.startupCommonDir)
79
+ || !samePath(selected.root, value.worktreeRoot)
80
+ || !samePath(selected.commonDir, value.worktreeCommonDir)
81
+ || !samePath(selected.gitDir, value.worktreeGitDir)
82
+ || !samePath(startup.commonDir, selected.commonDir))
83
+ return null;
84
+ return { cwd: selected.root, branch: requireBranch(selected) };
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ async function readSessionHeader(identity) {
91
+ if (!validSessionId(identity.sessionId) || !validPath(identity.sessionFile))
92
+ throw new Error("active session identity is unavailable");
93
+ const file = await realpath(resolve(identity.sessionFile));
94
+ const handle = await open(file, "r");
95
+ try {
96
+ const buffer = Buffer.alloc(MAX_HEADER_BYTES);
97
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, 0);
98
+ const newline = buffer.subarray(0, bytesRead).indexOf(0x0a);
99
+ if (newline < 0)
100
+ throw new Error("session header is unavailable");
101
+ const value = JSON.parse(buffer.subarray(0, newline).toString("utf8"));
102
+ if (!isRecord(value) || value.type !== "session" || value.id !== identity.sessionId || !validPath(value.cwd)) {
103
+ throw new Error("active session identity does not match its session file");
104
+ }
105
+ return { id: identity.sessionId, cwd: await realpath(resolve(value.cwd)), file };
106
+ }
107
+ finally {
108
+ await handle.close();
109
+ }
110
+ }
111
+ async function readGitWorktreeIdentity(path, requireWorktreeRoot, signal) {
112
+ if (!validPath(path))
113
+ throw new Error("associated worktree path is invalid");
114
+ const cwd = await realpath(resolve(path));
115
+ const { stdout } = await executeGit(["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-common-dir", "--git-dir"], cwd, MAX_RECORD_BYTES, signal);
116
+ const lines = stdout.split(/\r?\n/u).filter(Boolean);
117
+ if (lines.length !== 3)
118
+ throw new Error("associated worktree identity is invalid");
119
+ const [rootValue, commonValue, gitValue] = lines;
120
+ if (!rootValue || !commonValue || !gitValue)
121
+ throw new Error("associated worktree identity is incomplete");
122
+ const root = await realpath(rootValue);
123
+ if (requireWorktreeRoot && !samePath(root, cwd))
124
+ throw new Error("associated path must be a Git worktree root");
125
+ const commonDir = await realpath(commonValue);
126
+ const gitDir = await realpath(gitValue);
127
+ let branch = null;
128
+ if (requireWorktreeRoot) {
129
+ const branchResult = await executeGit(["symbolic-ref", "--quiet", "--short", "HEAD"], cwd, 4_096, signal);
130
+ branch = branchResult.stdout.trim();
131
+ if (branch.length === 0 || branch.length > 256)
132
+ throw new Error("associated worktree has a detached or invalid branch");
133
+ }
134
+ return { root, commonDir, gitDir, branch };
135
+ }
136
+ async function executeGit(arguments_, cwd, maxBuffer, signal) {
137
+ const { execFile } = await import("node:child_process");
138
+ const execute = promisify(execFile);
139
+ return await execute("git", [...arguments_], {
140
+ cwd,
141
+ windowsHide: true,
142
+ timeout: GIT_TIMEOUT_MS,
143
+ maxBuffer,
144
+ encoding: "utf8",
145
+ signal,
146
+ });
147
+ }
148
+ function requireBranch(identity) {
149
+ if (identity.branch === null)
150
+ throw new Error("associated worktree branch is unavailable");
151
+ return identity.branch;
152
+ }
153
+ function recordPath(header, options) {
154
+ const dataDir = options.dataDir ?? resolveProductPaths().dataDir;
155
+ const digest = createHash("sha256").update(header.id).update("\0").update(pathKey(header.file)).digest("hex");
156
+ return join(dataDir, PRODUCT_IDENTITY.state.sessionRepositoryDirectory, `${digest}.json`);
157
+ }
158
+ function samePath(left, right) {
159
+ return pathKey(left) === pathKey(right);
160
+ }
161
+ function pathKey(value) {
162
+ const normalized = resolve(value);
163
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
164
+ }
165
+ function validSessionId(value) {
166
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u.test(value);
167
+ }
168
+ function validPath(value) {
169
+ return typeof value === "string" && value.length > 0 && value.length <= 32_768 && !value.includes("\0") && isAbsolute(value);
170
+ }
171
+ function isRecord(value) {
172
+ return typeof value === "object" && value !== null && !Array.isArray(value);
173
+ }
@@ -1,5 +1,5 @@
1
1
  import { Input, Key, getKeybindings, matchesKey, truncateToWidth, } from "@earendil-works/pi-tui";
2
- import { piTheme } from "./theme.js";
2
+ import { piTheme, renderPiModalShortcutHints } from "./theme.js";
3
3
  const MAX_VISIBLE_ROWS = 10;
4
4
  const MODELS_TITLE = "Models";
5
5
  function fullModelId(model) {
@@ -228,7 +228,7 @@ export class ModelsDialogComponent {
228
228
  push();
229
229
  if (this.#refreshStatus !== undefined)
230
230
  push(theme.fg(this.#refreshStatus.kind, ` ${this.#refreshStatus.message}`));
231
- push(theme.fg("dim", ` ${this.#hints().join(" · ")}`));
231
+ push(renderPiModalShortcutHints(this.#hints(), 2));
232
232
  push(border);
233
233
  return lines;
234
234
  }
@@ -237,13 +237,13 @@ export class ModelsDialogComponent {
237
237
  const confirm = keyLabel("tui.select.confirm");
238
238
  const save = keyLabel("app.models.save");
239
239
  return [
240
- "type to search",
241
- "↑↓ navigate",
242
- ...(tab.length === 0 ? [] : [`${tab} filter`]),
243
- ...(confirm.length === 0 ? [] : [`${confirm} switch`]),
244
- "space scope",
245
- ...(save.length === 0 ? [] : [`${save} save`]),
246
- "esc close",
240
+ { action: "type to search" },
241
+ { key: "↑↓", action: "navigate" },
242
+ ...(tab.length === 0 ? [] : [{ key: tab, action: "filter" }]),
243
+ ...(confirm.length === 0 ? [] : [{ key: confirm, action: "switch" }]),
244
+ { key: "space", action: "scope" },
245
+ ...(save.length === 0 ? [] : [{ key: save, action: "save" }]),
246
+ { key: "esc", action: "close" },
247
247
  ];
248
248
  }
249
249
  #emptyText() {
@@ -132,9 +132,17 @@ export function createPiShellEditor(options) {
132
132
  const commands = collapsed.commands;
133
133
  const additions = new Map(commands.map(command => [command.name, command]));
134
134
  const builtInNames = new Set(builtInCatalog.map(command => command.name));
135
- const builtIns = builtInCatalog.map(command => autocompleteCommand(command, additions.get(command.name)));
136
- // Invariant: the engine's built-in additions only decorate this profile's catalog; they never surface as resources.
137
- const resources = commands.filter(command => command.source !== "builtin" && !builtInNames.has(command.name)).map(command => autocompleteCommand(command));
135
+ const builtIns = builtInCatalog.flatMap(command => [
136
+ autocompleteCommand(command, additions.get(command.name)),
137
+ // Rationale: the collapsed browser is a primary bare-A1 command, not a resource after quit.
138
+ ...(command.name === "settings" && collapsed.collapsedCommand !== undefined
139
+ ? [autocompleteCommand(collapsed.collapsedCommand)]
140
+ : []),
141
+ ]);
142
+ // Invariant: built-in additions and the promoted collapsed browser never reappear as resources.
143
+ const resources = commands
144
+ .filter(command => command.name !== collapsed.collapsedCommand?.name && command.source !== "builtin" && !builtInNames.has(command.name))
145
+ .map(command => autocompleteCommand(command));
138
146
  const combined = new CombinedAutocompleteProvider([...builtIns, ...resources], options.cwd ?? process.cwd());
139
147
  autocompleteProvider = tunnelSkills.length === 0 ? combined : createSkillsTunnelProvider(combined, tunnelSkills);
140
148
  editor.setAutocompleteProvider(autocompleteProvider);
@@ -229,10 +237,26 @@ export function createPiShellEditor(options) {
229
237
  if (editorUx !== undefined && matchesKey(data, "shift+tab")
230
238
  && !Object.values(keybindings.getEffectiveConfig()).some(keys => (Array.isArray(keys) ? keys : [keys]).some(key => key !== undefined && matchesKey(data, key))))
231
239
  return;
240
+ const hadDraft = editor.getText().length > 0;
232
241
  if (editorUx === undefined)
233
242
  editor.handleInput(data);
234
243
  else
235
244
  editorUx.handleInput(data);
245
+ if (editorUx === undefined || !hadDraft || editor.getText().length > 0)
246
+ return;
247
+ // Invariant: Pi's deletion paths can leave a visible or debounced autocomplete request owned by
248
+ // the removed draft. Reapplying empty text cancels both forms without a duplicate change notice.
249
+ const onChange = editor.onChange;
250
+ delete editor.onChange;
251
+ try {
252
+ editor.setText("");
253
+ }
254
+ finally {
255
+ if (onChange === undefined)
256
+ delete editor.onChange;
257
+ else
258
+ editor.onChange = onChange;
259
+ }
236
260
  },
237
261
  invalidate: () => editor.invalidate(),
238
262
  setFocused: focused => {
@@ -1,5 +1,7 @@
1
- import { ExtensionInputComponent, ExtensionSelectorComponent, getSelectListTheme, } from "../startup-public.js";
2
- import { ExtensionEditorComponent, } from "./upstream/components/extension-editor.js";
1
+ import { getSelectListTheme, } from "../startup-public.js";
2
+ import { ExtensionEditorComponent } from "./upstream/components/extension-editor.js";
3
+ import { ExtensionInputComponent } from "./upstream/components/extension-input.js";
4
+ import { ExtensionSelectorComponent } from "./upstream/components/extension-selector.js";
3
5
  import { setKeybindings, Text, } from "@earendil-works/pi-tui";
4
6
  import { KeybindingsManager, } from "./upstream/adjacent/core/keybindings.js";
5
7
  import { PINNED_PI_LAYOUT, applyPiTheme, applyPiThemeInstance, getAvailablePiThemes, loadPiTheme, piTheme, } from "./theme.js";
@@ -165,7 +165,7 @@ function liveWorkingText(view, workingOverride, progressPresentation) {
165
165
  const message = workingOverride ?? view.status.workingMessage ?? "Working";
166
166
  const progress = view.status.workingProgress;
167
167
  return workingOverride === undefined && progressPresentation === "custom-viewport" && typeof progress === "number"
168
- ? `${message} (${progress}%)`
168
+ ? `${message}(${progress}%)`
169
169
  : message;
170
170
  }
171
171
  function statusComponent(view, ui, outputPad, liveStatusText, placement) {
@@ -191,9 +191,11 @@ export function renderPiShellPackageUpdateNotice(packages, width) {
191
191
  function transcriptComponent(block, cwd, expanded, extensions, submittedPrompt, outputPad, hideThinkingBlock, mermaidRenderingMode, showImages, imageWidthCells, imageAssets, tui) {
192
192
  switch (block.kind) {
193
193
  case "user": {
194
- const skill = parseSkillBlock(block.text);
194
+ const visibleBlock = submittedPrompt === undefined || block.userPresentation === undefined ? block
195
+ : { ...block, text: block.userPresentation.visibleText };
196
+ const skill = parseSkillBlock(visibleBlock.text);
195
197
  if (!skill)
196
- return submittedPrompt ? createPiSubmittedPromptComponent(block, submittedPrompt) : new UserMessageComponent(block.text);
198
+ return submittedPrompt ? createPiSubmittedPromptComponent(visibleBlock, submittedPrompt) : new UserMessageComponent(visibleBlock.text);
197
199
  const invocation = new SkillInvocationMessageComponent(skill, getMarkdownTheme());
198
200
  invocation.setExpanded(expanded);
199
201
  if (!skill.userMessage)
@@ -11,6 +11,8 @@ export interface PiShellSkillSummary {
11
11
  }
12
12
  export interface PiShellCollapsedSkillCommands {
13
13
  readonly commands: readonly PiShellAutocompleteCommand[];
14
+ /** The synthetic command replacing the withheld entries, when skill commands were present. */
15
+ readonly collapsedCommand?: PiShellAutocompleteCommand;
14
16
  /** The skills withheld from the top-level menu, in the engine's discovery order. */
15
17
  readonly skills: readonly PiShellSkillSummary[];
16
18
  }
@@ -27,6 +27,15 @@ export function collapseSkillCommands(commands) {
27
27
  if (skills.length === 0)
28
28
  return { commands, skills };
29
29
  const collapsed = [];
30
+ const collapsedCommand = {
31
+ name: SKILLS_COMMAND_NAME,
32
+ description: SKILLS_COMMAND_DESCRIPTION,
33
+ argumentOptions: skills.map(skill => ({
34
+ id: skill.name,
35
+ label: skill.name,
36
+ ...(skill.description.length === 0 ? {} : { description: skill.description }),
37
+ })),
38
+ };
30
39
  let inserted = false;
31
40
  for (const command of commands) {
32
41
  if (!command.name.startsWith(SKILL_COMMAND_PREFIX)) {
@@ -36,17 +45,9 @@ export function collapseSkillCommands(commands) {
36
45
  if (inserted)
37
46
  continue;
38
47
  inserted = true;
39
- collapsed.push({
40
- name: SKILLS_COMMAND_NAME,
41
- description: SKILLS_COMMAND_DESCRIPTION,
42
- argumentOptions: skills.map(skill => ({
43
- id: skill.name,
44
- label: skill.name,
45
- ...(skill.description.length === 0 ? {} : { description: skill.description }),
46
- })),
47
- });
48
+ collapsed.push(collapsedCommand);
48
49
  }
49
- return { commands: collapsed, skills };
50
+ return { commands: collapsed, collapsedCommand, skills };
50
51
  }
51
52
  /** Case-insensitive substring match on the name or description; a leading `skill:`/`skills:` in the query is ignored for the name. */
52
53
  export function skillMatchesQuery(skill, query) {
@@ -1,6 +1,6 @@
1
- import { DynamicBorder, keyHint, rawKeyHint } from "../startup-public.js";
1
+ import { DynamicBorder } from "../startup-public.js";
2
2
  import { Container, getKeybindings, Input, Spacer, Text } from "@earendil-works/pi-tui";
3
- import { PINNED_PI_LAYOUT, piTheme } from "./theme.js";
3
+ import { PINNED_PI_LAYOUT, piTheme, renderPiModalShortcutHints } from "./theme.js";
4
4
  import { componentPort, ensureTheme, piShellTruncateToWidth, piShellVisibleWidth } from "./shell-shared-facade.js";
5
5
  import { SKILL_COMMAND_PREFIX, skillMatchesQuery } from "./skills-command.js";
6
6
  /** A single line clipped at the viewport width: no wrap, no ellipsis, padded to the full width. */
@@ -44,7 +44,12 @@ class SkillsSelectorComponent extends Container {
44
44
  this.addChild(new Spacer(1));
45
45
  this.addChild(this.#listContainer);
46
46
  this.addChild(new Spacer(1));
47
- this.addChild(new Text(` ${rawKeyHint("↑↓", "navigate")} ${keyHint("tui.select.confirm", "select")} ${keyHint("tui.select.cancel", "cancel")}`, 0, 0));
47
+ const bindings = getKeybindings();
48
+ this.addChild(new Text(renderPiModalShortcutHints([
49
+ { key: "↑↓", action: "navigate" },
50
+ { key: bindings.getKeys("tui.select.confirm").join("/"), action: "select" },
51
+ { key: bindings.getKeys("tui.select.cancel").join("/"), action: "cancel" },
52
+ ], 2), 0, 0));
48
53
  this.addChild(new DynamicBorder());
49
54
  this.#updateList();
50
55
  }
@@ -1 +1,5 @@
1
+ import { type SemanticShortcutHint } from "../../../contracts/presentation/index.js";
1
2
  export * from "./upstream/theme/theme.js";
3
+ export type PiModalShortcutHint = SemanticShortcutHint;
4
+ /** The common bare-A1 modal footer: quiet display-cased keys, muted names, and whitespace-only gaps. */
5
+ export declare function renderPiModalShortcutHints(entries: readonly PiModalShortcutHint[], indent?: number): string;
@@ -1 +1,11 @@
1
+ import { renderSemanticShortcutHints, } from "../../../contracts/presentation/index.js";
2
+ import { piTheme } from "./upstream/theme/theme.js";
1
3
  export * from "./upstream/theme/theme.js";
4
+ /** The common bare-A1 modal footer: quiet display-cased keys, muted names, and whitespace-only gaps. */
5
+ export function renderPiModalShortcutHints(entries, indent = 0) {
6
+ const theme = piTheme();
7
+ return renderSemanticShortcutHints(entries, {
8
+ key: text => theme.fg("dim", text),
9
+ action: text => theme.fg("muted", text),
10
+ }, indent);
11
+ }
@@ -139,7 +139,7 @@ export declare const KEYBINDINGS: {
139
139
  readonly description: "Copy selection or last assistant message";
140
140
  };
141
141
  readonly "app.message.followUp": {
142
- readonly defaultKeys: "ctrl+q" | "alt+enter";
142
+ readonly defaultKeys: "alt+enter" | "ctrl+q";
143
143
  readonly description: "Queue follow-up message";
144
144
  };
145
145
  readonly "app.message.dequeue": {
@@ -147,7 +147,7 @@ export declare const KEYBINDINGS: {
147
147
  readonly description: "Restore queued messages";
148
148
  };
149
149
  readonly "app.clipboard.pasteImage": {
150
- readonly defaultKeys: "ctrl+v" | "alt+v";
150
+ readonly defaultKeys: "alt+v" | "ctrl+v";
151
151
  readonly description: "Paste image from clipboard (text fallback)";
152
152
  };
153
153
  readonly "app.session.new": {
@@ -167,11 +167,11 @@ export declare const KEYBINDINGS: {
167
167
  readonly description: "Resume a session";
168
168
  };
169
169
  readonly "app.tree.foldOrUp": {
170
- readonly defaultKeys: ("ctrl+left" | "alt+left")[];
170
+ readonly defaultKeys: ("alt+left" | "ctrl+left")[];
171
171
  readonly description: "Fold tree branch or move up";
172
172
  };
173
173
  readonly "app.tree.unfoldOrDown": {
174
- readonly defaultKeys: ("ctrl+right" | "alt+right")[];
174
+ readonly defaultKeys: ("alt+right" | "ctrl+right")[];
175
175
  readonly description: "Unfold tree branch or move down";
176
176
  };
177
177
  readonly "app.tree.editLabel": {
@@ -3,8 +3,8 @@
3
3
  * packages/coding-agent/src/modes/interactive/components/extension-editor.ts.
4
4
  * Modifications: Mechanical port: remap pi-tui to the root public singleton, use owned
5
5
  * keybindings/theme and external-editor seams, preserve editor layout, hints, focus, submission,
6
- * cancellation, and external-editor lifecycle.
7
- * Deviations: none.
6
+ * cancellation, and external-editor lifecycle; bare A1 uses the shared semantic modal shortcut row.
7
+ * Deviations: owned-modal-shortcut-hints.
8
8
  */
9
9
  import { Container, type TUI } from "@earendil-works/pi-tui";
10
10
  import type { KeybindingsManager } from "../adjacent/core/keybindings.js";
@@ -3,16 +3,13 @@
3
3
  * packages/coding-agent/src/modes/interactive/components/extension-editor.ts.
4
4
  * Modifications: Mechanical port: remap pi-tui to the root public singleton, use owned
5
5
  * keybindings/theme and external-editor seams, preserve editor layout, hints, focus, submission,
6
- * cancellation, and external-editor lifecycle.
7
- * Deviations: none.
6
+ * cancellation, and external-editor lifecycle; bare A1 uses the shared semantic modal shortcut row.
7
+ * Deviations: owned-modal-shortcut-hints.
8
8
  */
9
9
  import { Container, Editor, Spacer, Text, } from "@earendil-works/pi-tui";
10
10
  import { DynamicBorder, getSelectListTheme } from "../../../startup-public.js";
11
11
  import { editInExternalEditor } from "../external-editor.js";
12
- import { piTheme } from "../../theme.js";
13
- function keyHint(keybindings, keybinding, description) {
14
- return piTheme().fg("dim", keybindings.getKeys(keybinding).join("/")) + piTheme().fg("muted", ` ${description}`);
15
- }
12
+ import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
16
13
  export class ExtensionEditorComponent extends Container {
17
14
  #editor;
18
15
  #tui;
@@ -54,10 +51,12 @@ export class ExtensionEditorComponent extends Container {
54
51
  this.#editor.onSubmit = onSubmit;
55
52
  this.addChild(this.#editor);
56
53
  this.addChild(new Spacer(1));
57
- const hint = keyHint(this.#keybindings, "tui.select.confirm", "submit")
58
- + " " + keyHint(this.#keybindings, "tui.input.newLine", "newline")
59
- + " " + keyHint(this.#keybindings, "tui.select.cancel", "cancel")
60
- + " " + keyHint(this.#keybindings, "app.editor.external", "external editor");
54
+ const hint = renderPiModalShortcutHints([
55
+ { key: this.#keybindings.getKeys("tui.select.confirm").join("/"), action: "submit" },
56
+ { key: this.#keybindings.getKeys("tui.input.newLine").join("/"), action: "newline" },
57
+ { key: this.#keybindings.getKeys("tui.select.cancel").join("/"), action: "cancel" },
58
+ { key: this.#keybindings.getKeys("app.editor.external").join("/"), action: "external editor" },
59
+ ]);
61
60
  this.addChild(new Text(hint, 1, 0));
62
61
  this.addChild(new Spacer(1));
63
62
  this.addChild(new DynamicBorder());
@@ -0,0 +1,17 @@
1
+ import { Container, type TUI } from "@earendil-works/pi-tui";
2
+ interface ExtensionInputOptions {
3
+ readonly tui?: TUI;
4
+ readonly timeout?: number;
5
+ readonly description?: string;
6
+ readonly initialValue?: string;
7
+ }
8
+ /** Bare-A1 extension text input using the common modal shortcut row. */
9
+ export declare class ExtensionInputComponent extends Container {
10
+ #private;
11
+ get focused(): boolean;
12
+ set focused(value: boolean);
13
+ constructor(title: string, _placeholder: string | undefined, onSubmit: (value: string) => void, onCancel: () => void, opts?: ExtensionInputOptions);
14
+ handleInput(keyData: string): void;
15
+ dispose(): void;
16
+ }
17
+ export {};
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Provenance: @earendil-works/pi-coding-agent 0.87.0 (MIT), commit 16787ad5b2dc748047f314ca1bfe7708f30f54f3,
3
+ * packages/coding-agent/src/modes/interactive/components/extension-input.ts.
4
+ * Modifications: Mechanical port: remap public imports, use ECMAScript private fields, and route the
5
+ * bare-A1 instruction row through the shared semantic shortcut renderer while preserving input,
6
+ * timeout, focus, submission, cancellation, and disposal behavior.
7
+ * Deviations: owned-modal-shortcut-hints.
8
+ */
9
+ import { DynamicBorder } from "../../../startup-public.js";
10
+ import { Container, getKeybindings, Input, Spacer, Text } from "@earendil-works/pi-tui";
11
+ import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
12
+ import { CountdownTimer } from "./countdown-timer.js";
13
+ /** Bare-A1 extension text input using the common modal shortcut row. */
14
+ export class ExtensionInputComponent extends Container {
15
+ #input = new Input();
16
+ #onSubmit;
17
+ #onCancel;
18
+ #titleText;
19
+ #baseTitle;
20
+ #countdown;
21
+ #focused = false;
22
+ get focused() { return this.#focused; }
23
+ set focused(value) {
24
+ this.#focused = value;
25
+ this.#input.focused = value;
26
+ }
27
+ constructor(title, _placeholder, onSubmit, onCancel, opts) {
28
+ super();
29
+ this.#onSubmit = onSubmit;
30
+ this.#onCancel = onCancel;
31
+ this.#baseTitle = title;
32
+ const theme = piTheme();
33
+ this.addChild(new DynamicBorder());
34
+ this.addChild(new Spacer(1));
35
+ this.#titleText = new Text(theme.fg("accent", title), 1, 0);
36
+ this.addChild(this.#titleText);
37
+ if (opts?.description) {
38
+ this.addChild(new Spacer(1));
39
+ this.addChild(new Text(theme.fg("text", opts.description), 1, 0));
40
+ }
41
+ this.addChild(new Spacer(1));
42
+ this.#countdown = opts?.timeout !== undefined && opts.timeout > 0 && opts.tui !== undefined
43
+ ? new CountdownTimer(opts.timeout, opts.tui, seconds => {
44
+ const current = piTheme();
45
+ this.#titleText.setText(current.fg("accent", `${this.#baseTitle} (${seconds}s)`));
46
+ }, () => this.#onCancel())
47
+ : undefined;
48
+ if (opts?.initialValue !== undefined)
49
+ this.#input.setValue(opts.initialValue);
50
+ this.addChild(this.#input);
51
+ this.addChild(new Spacer(1));
52
+ const keys = getKeybindings();
53
+ this.addChild(new Text(renderPiModalShortcutHints([
54
+ { key: keys.getKeys("tui.select.confirm").join("/"), action: "submit" },
55
+ { key: keys.getKeys("tui.select.cancel").join("/"), action: "cancel" },
56
+ ]), 1, 0));
57
+ this.addChild(new Spacer(1));
58
+ this.addChild(new DynamicBorder());
59
+ }
60
+ handleInput(keyData) {
61
+ const keys = getKeybindings();
62
+ if (keys.matches(keyData, "tui.select.confirm") || keyData === "\n")
63
+ this.#onSubmit(this.#input.getValue());
64
+ else if (keys.matches(keyData, "tui.select.cancel"))
65
+ this.#onCancel();
66
+ else
67
+ this.#input.handleInput(keyData);
68
+ }
69
+ dispose() {
70
+ this.#countdown?.dispose();
71
+ }
72
+ }
@@ -0,0 +1,15 @@
1
+ import { Container, type TUI } from "@earendil-works/pi-tui";
2
+ interface ExtensionSelectorOptions {
3
+ readonly tui?: TUI;
4
+ readonly timeout?: number;
5
+ readonly description?: string;
6
+ readonly onToggleToolsExpanded?: () => void;
7
+ }
8
+ /** Bare-A1 extension selector using the common modal shortcut row. */
9
+ export declare class ExtensionSelectorComponent extends Container {
10
+ #private;
11
+ constructor(title: string, options: readonly string[], onSelect: (value: string) => void, onCancel: () => void, opts?: ExtensionSelectorOptions);
12
+ handleInput(keyData: string): void;
13
+ dispose(): void;
14
+ }
15
+ export {};