@timurproko/a1 0.1.8-dev.332 → 0.1.8-dev.368

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 (110) hide show
  1. package/README.md +40 -11
  2. package/dist/composition/owned-ui.d.ts +2 -0
  3. package/dist/composition/owned-ui.js +20 -3
  4. package/dist/contracts/owned-ui/index.d.ts +2 -0
  5. package/dist/contracts/owned-ui/index.js +2 -0
  6. package/dist/contracts/owned-ui/model.d.ts +38 -3
  7. package/dist/contracts/owned-ui/prompt-history.d.ts +18 -0
  8. package/dist/contracts/owned-ui/prompt-suggestions.d.ts +1 -1
  9. package/dist/contracts/owned-ui/prompt-suggestions.js +4 -1
  10. package/dist/contracts/owned-ui/suggestion-diagnostics.d.ts +22 -0
  11. package/dist/contracts/owned-ui/suggestion-diagnostics.js +9 -0
  12. package/dist/contracts/owned-ui/transcript-lifecycle.d.ts +5 -0
  13. package/dist/contracts/owned-ui/transcript-lifecycle.js +33 -0
  14. package/dist/contracts/owned-ui/validation.js +45 -2
  15. package/dist/contracts/presentation/index.d.ts +8 -0
  16. package/dist/features/owned-ui/settings-app.js +1 -1
  17. package/dist/features/prompt-history/image-sidecar.d.ts +32 -0
  18. package/dist/features/prompt-history/image-sidecar.js +130 -0
  19. package/dist/features/prompt-history/index.d.ts +2 -0
  20. package/dist/features/prompt-history/index.js +1 -0
  21. package/dist/features/prompt-history/paths.d.ts +1 -0
  22. package/dist/features/prompt-history/paths.js +2 -1
  23. package/dist/features/prompt-history/store.d.ts +1 -1
  24. package/dist/features/prompt-history/store.js +46 -2
  25. package/dist/features/prompt-history/worker.js +1 -1
  26. package/dist/features/prompt-suggestions/diagnostics.d.ts +18 -0
  27. package/dist/features/prompt-suggestions/diagnostics.js +82 -0
  28. package/dist/features/prompt-suggestions/index.d.ts +1 -0
  29. package/dist/features/prompt-suggestions/index.js +1 -0
  30. package/dist/foundation/release/bootstrap.d.ts +2 -0
  31. package/dist/foundation/release/bootstrap.js +54 -18
  32. package/dist/foundation/release/index.d.ts +1 -0
  33. package/dist/foundation/release/index.js +1 -0
  34. package/dist/foundation/release/update-launch.d.ts +6 -0
  35. package/dist/foundation/release/update-launch.js +17 -0
  36. package/dist/foundation/release/update.d.ts +2 -0
  37. package/dist/foundation/release/update.js +20 -21
  38. package/dist/foundation/supervision/main.js +5 -2
  39. package/dist/foundation/supervision/server.js +40 -13
  40. package/dist/integrations/pi/components/owned-editor-ux.d.ts +9 -1
  41. package/dist/integrations/pi/components/owned-editor-ux.js +11 -8
  42. package/dist/integrations/pi/components/prompt-input-port.d.ts +18 -0
  43. package/dist/integrations/pi/components/prompt-input-port.js +1 -0
  44. package/dist/integrations/pi/components/shell-editor-autocomplete.js +66 -11
  45. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  46. package/dist/integrations/pi/components/shell-footer-status.js +9 -5
  47. package/dist/integrations/pi/components/shell-presenters-info.js +7 -2
  48. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +6 -2
  49. package/dist/integrations/pi/components/shell-presenters-transcript.js +81 -49
  50. package/dist/integrations/pi/components/shell-shared-facade.d.ts +17 -4
  51. package/dist/integrations/pi/components/shell-shared-facade.js +2 -1
  52. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +3 -1
  53. package/dist/integrations/pi/components/submitted-prompt-adapter.js +20 -4
  54. package/dist/integrations/pi/components/tool-result-adapter.d.ts +6 -0
  55. package/dist/integrations/pi/components/tool-result-adapter.js +27 -0
  56. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -0
  57. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +5 -0
  58. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -2
  59. package/dist/integrations/pi/components/upstream/components/owned-editor.js +41 -33
  60. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  61. package/dist/integrations/pi/components/upstream/components/session-footer.js +12 -7
  62. package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
  63. package/dist/integrations/pi/engine/adapter.d.ts +2 -1
  64. package/dist/integrations/pi/engine/adapter.js +181 -84
  65. package/dist/integrations/pi/engine/tool-rendering.d.ts +13 -0
  66. package/dist/integrations/pi/engine/tool-rendering.js +122 -0
  67. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +30 -0
  68. package/dist/integrations/pi/session-ui/prompt-chips.js +128 -1
  69. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +8 -0
  70. package/dist/integrations/pi/session-ui/prompt-history-controller.js +30 -2
  71. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +11 -3
  72. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +126 -51
  73. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +24 -2
  74. package/dist/integrations/pi/session-ui/session-shell-root.js +108 -38
  75. package/dist/integrations/pi/session-ui/session-shell.js +51 -31
  76. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -1
  77. package/dist/integrations/pi/session-ui/session-viewport-controller.js +106 -16
  78. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  79. package/dist/integrations/pi/tui-runtime/adapter.js +35 -11
  80. package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -1
  81. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -0
  82. package/dist/integrations/pi/tui-runtime/index.js +1 -0
  83. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +13 -0
  84. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +59 -0
  85. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +32 -0
  86. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +159 -0
  87. package/dist/native/darwin-arm64/manifest.json +1 -1
  88. package/dist/native/linux-x64/manifest.json +1 -1
  89. package/dist/native/win32-x64/manifest.json +2 -2
  90. package/dist/native/win32-x64/process-guardian.exe +0 -0
  91. package/dist/product-identity.d.ts +1 -1
  92. package/dist/product-identity.js +1 -1
  93. package/dist/product-identity.json +1 -0
  94. package/dist/ui/components/index.d.ts +1 -0
  95. package/dist/ui/components/index.js +1 -0
  96. package/dist/ui/components/line-input.d.ts +2 -3
  97. package/dist/ui/components/line-input.js +4 -20
  98. package/dist/ui/components/mouse.d.ts +1 -1
  99. package/dist/ui/components/mouse.js +4 -3
  100. package/dist/ui/components/prompt-input.d.ts +32 -0
  101. package/dist/ui/components/prompt-input.js +54 -0
  102. package/docs/architecture/boundaries.md +1 -0
  103. package/docs/architecture/history-editor-provenance.md +5 -1
  104. package/docs/architecture/project-structure.md +1 -0
  105. package/docs/architecture/prompt-suggestions.md +68 -0
  106. package/docs/architecture/toolchain.md +1 -1
  107. package/docs/architecture/ui-reference-provenance.md +23 -0
  108. package/docs/ci-release-runbook.md +45 -7
  109. package/docs/features/modal-content-interaction.md +37 -0
  110. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { chmod, lstat, mkdir, rename, rm, writeFile } from "node:fs/promises";
2
+ import { chmod, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { createConnection, createServer } from "node:net";
4
4
  import { platform } from "node:os";
5
5
  import { dirname } from "node:path";
@@ -54,6 +54,7 @@ export class SupervisorServer {
54
54
  startedAt = new Date().toISOString();
55
55
  paths;
56
56
  #server = null;
57
+ #ownsEndpoint = false;
57
58
  #instances = new Map();
58
59
  #instanceOwners = new Map();
59
60
  #clientIds = new Map();
@@ -104,15 +105,24 @@ export class SupervisorServer {
104
105
  if (platform() !== "win32") {
105
106
  if (this.paths.endpointDirectory)
106
107
  await ensureManagedEndpointDirectory(dirname(this.paths.endpoint));
107
- if (await endpointIsLive(this.paths.endpoint))
108
- throw new Error(PRODUCT_TEXT.diagnostic(`supervisor already owns ${this.paths.endpoint}`));
109
- await rm(this.paths.endpoint, { force: true });
108
+ const existing = await lstat(this.paths.endpoint).catch(error => {
109
+ if (error.code === "ENOENT")
110
+ return null;
111
+ throw error;
112
+ });
113
+ if (existing) {
114
+ if (await endpointIsLive(this.paths.endpoint)) {
115
+ throw Object.assign(new Error(PRODUCT_TEXT.diagnostic(`supervisor already owns ${this.paths.endpoint}`)), { code: "EADDRINUSE" });
116
+ }
117
+ await rm(this.paths.endpoint, { force: true });
118
+ }
110
119
  }
111
120
  const server = createServer(socket => this.#attach(socket));
112
121
  this.#server = server;
113
122
  await new Promise((resolve, reject) => {
114
123
  server.once("error", reject);
115
124
  server.listen(this.paths.endpoint, () => {
125
+ this.#ownsEndpoint = true;
116
126
  server.off("error", reject);
117
127
  resolve();
118
128
  });
@@ -146,7 +156,9 @@ export class SupervisorServer {
146
156
  await this.close(false);
147
157
  }
148
158
  async close(stopAgents = false) {
149
- if (stopAgents && !await this.#drainInstances("update", this.shutdownDeadlineMs)) {
159
+ if (this.#closing)
160
+ return;
161
+ if (stopAgents && this.#ownsEndpoint && !await this.#drainInstances("update", this.shutdownDeadlineMs)) {
150
162
  throw new Error("active launch instances did not release ownership within the shutdown deadline");
151
163
  }
152
164
  this.#closing = true;
@@ -155,14 +167,27 @@ export class SupervisorServer {
155
167
  this.#supersededPoll = null;
156
168
  for (const client of this.#clients)
157
169
  client.destroy();
158
- if (this.#server)
159
- await new Promise(resolve => this.#server?.close(() => resolve()));
160
- this.#server = null;
161
- this.store.close();
162
- await this.#metadataWrites;
163
- await rm(this.paths.endpointMetadataPath, { force: true });
164
- if (platform() !== "win32")
165
- await rm(this.paths.endpoint, { force: true });
170
+ try {
171
+ await this.#metadataWrites.catch(() => undefined);
172
+ if (this.#ownsEndpoint) {
173
+ // Concurrency: remove only our publication, before releasing the listening endpoint.
174
+ // A contender that failed to bind must never erase the successful owner's discovery file.
175
+ const metadata = await readFile(this.paths.endpointMetadataPath, "utf8").then(source => JSON.parse(source)).catch(() => null);
176
+ if (metadata?.supervisorId === this.id && metadata.bootNonce === this.bootNonce) {
177
+ await rm(this.paths.endpointMetadataPath, { force: true });
178
+ }
179
+ // Platform: Node unlinks its Unix socket when the server closes; unlinking it early would
180
+ // let a replacement bind before the old server finishes releasing that path.
181
+ }
182
+ }
183
+ finally {
184
+ // Platform: a metadata sharing error must not leave a bound but undiscoverable listener.
185
+ if (this.#server)
186
+ await new Promise(resolve => this.#server?.close(() => resolve()));
187
+ this.#server = null;
188
+ this.#ownsEndpoint = false;
189
+ this.store.close();
190
+ }
166
191
  }
167
192
  async closeForReleaseReplacement(stopAgents) {
168
193
  await this.close(stopAgents);
@@ -473,6 +498,8 @@ export class SupervisorServer {
473
498
  return [...this.#instances.keys()];
474
499
  }
475
500
  #writeEndpointMetadata() {
501
+ if (this.#closing)
502
+ return Promise.resolve();
476
503
  const liveInstanceIds = this.#liveInstanceIds();
477
504
  const metadata = {
478
505
  schema: PRODUCT_IDENTITY.protocol.supervisorSchema,
@@ -59,6 +59,14 @@ export interface PromptSelectionUxOptions {
59
59
  readonly requestRender: () => void;
60
60
  readonly getRows: () => number;
61
61
  /** Presentation-only columns reserved before semantic editor text. */
62
- readonly promptPrefixWidth?: number;
62
+ readonly promptGeometry?: (width: number, padding: number) => {
63
+ readonly prefixWidth: number;
64
+ readonly innerWidth: number;
65
+ readonly paddingX: number;
66
+ readonly contentWidth: number;
67
+ readonly layoutWidth: number;
68
+ };
63
69
  }
64
70
  export declare function createPromptSelectionInterceptor(editor: Editor, keybindings: KeybindingsManager, options: PromptSelectionUxOptions): OwnedEditorUxInterceptor;
71
+ /** Shares the existing atomic-aware body layout with prompt/menu composition. */
72
+ export declare function editorVisualLineCount(editor: Editor, width: number): number | undefined;
@@ -230,18 +230,17 @@ class PromptSelectionInterceptor {
230
230
  }
231
231
  #renderVisible(width, next) {
232
232
  const rows = next().map(row => row.replaceAll(ATOMIC_SPACE_SENTINEL, " "));
233
- const maxPadding = Math.max(0, Math.floor((width - 1) / 2));
234
- const padding = Math.min(this.editor.getPaddingX(), maxPadding);
235
- const prefixWidth = this.options.promptPrefixWidth ?? 0;
236
- const innerWidth = Math.max(1, width - prefixWidth);
237
- const contentWidth = Math.max(1, innerWidth - padding * 2);
238
- const layoutWidth = Math.max(1, contentWidth - (padding ? 0 : 1));
233
+ const shared = this.options.promptGeometry?.(width, this.editor.getPaddingX());
234
+ const padding = shared?.paddingX ?? Math.min(this.editor.getPaddingX(), Math.max(0, Math.floor((width - 1) / 2)));
235
+ const prefixWidth = shared?.prefixWidth ?? 0;
236
+ const contentWidth = shared?.contentWidth ?? Math.max(1, width - padding * 2);
237
+ const layoutWidth = shared?.layoutWidth ?? Math.max(1, contentWidth - (padding ? 0 : 1));
239
238
  const visualLines = editorVisualLineMap(this.editor, layoutWidth)
240
239
  ?? buildVisualLineMap(editorState(this.editor).lines, layoutWidth);
241
240
  const scrollOffset = this.editor.interaction?.scrollOffset() ?? numericProperty(this.editor, "scrollOffset");
242
241
  const maxVisibleLines = Math.max(5, Math.floor(this.options.getRows() * 0.3));
243
242
  const textRows = Math.max(0, Math.min(visualLines.length - scrollOffset, maxVisibleLines, rows.length - 2));
244
- this.#geometry = { width, padding, layoutWidth, visualLines, scrollOffset, textRows, hiddenRanges: [] };
243
+ this.#geometry = { width, padding, prefixWidth, layoutWidth, visualLines, scrollOffset, textRows, hiddenRanges: [] };
245
244
  if (this.#atomicFocus() !== undefined) {
246
245
  for (let row = 0; row < rows.length; row += 1) {
247
246
  rows[row] = (rows[row] ?? "").replaceAll(CURSOR_MARKER, "");
@@ -376,7 +375,7 @@ class PromptSelectionInterceptor {
376
375
  return undefined;
377
376
  const line = (geometry.lines ?? editorState(this.editor).lines)[visual.logicalLine] ?? "";
378
377
  const segment = line.slice(visual.startCol, visual.startCol + visual.length);
379
- const displayColumn = Math.max(0, column - 1 - geometry.padding - (this.options.promptPrefixWidth ?? 0));
378
+ const displayColumn = Math.max(0, column - 1 - geometry.padding - geometry.prefixWidth);
380
379
  return {
381
380
  line: visual.logicalLine,
382
381
  col: restoreColumn(visual.startCol + indexAtDisplayWidth(segment, displayColumn), geometry.hiddenRanges[visual.logicalLine] ?? []),
@@ -900,6 +899,10 @@ function numericProperty(target, key) {
900
899
  const value = Object.getOwnPropertyDescriptor(target, key)?.value;
901
900
  return typeof value === "number" ? value : 0;
902
901
  }
902
+ /** Shares the existing atomic-aware body layout with prompt/menu composition. */
903
+ export function editorVisualLineCount(editor, width) {
904
+ return editorVisualLineMap(editor, width)?.length;
905
+ }
903
906
  function editorVisualLineMap(editor, width) {
904
907
  if (editor.interaction !== undefined)
905
908
  return editor.interaction.visualLines(width);
@@ -0,0 +1,18 @@
1
+ export interface PiShellPromptInputGeometry {
2
+ readonly prefixWidth: number;
3
+ readonly innerWidth: number;
4
+ readonly paddingX: number;
5
+ readonly contentWidth: number;
6
+ readonly layoutWidth: number;
7
+ }
8
+ export interface PiShellPromptInputBody {
9
+ readonly rows: readonly string[];
10
+ readonly topRule?: string | undefined;
11
+ readonly bottomRule?: string | undefined;
12
+ readonly after?: readonly string[];
13
+ }
14
+ export interface PiShellPromptInputPresentation {
15
+ geometry(width: number, padding?: number): PiShellPromptInputGeometry;
16
+ styleRule(text: string): string;
17
+ render(width: number, body: (innerWidth: number) => PiShellPromptInputBody, ruled?: boolean, padding?: number): string[];
18
+ }
@@ -1,9 +1,9 @@
1
1
  import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
2
- import { CombinedAutocompleteProvider, matchesKey, setKeybindings, } from "#pi-tui";
2
+ import { CombinedAutocompleteProvider, matchesKey, setKeybindings, visibleWidth, } from "#pi-tui";
3
3
  import { PROMPT_HISTORY_EDITOR_REPLACEMENT } from "../../../contracts/owned-ui/index.js";
4
4
  import { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
5
5
  import { OwnedEditor } from "./upstream/components/owned-editor.js";
6
- import { OwnedEditorUxInterception, createPromptSelectionInterceptor, } from "./owned-editor-ux.js";
6
+ import { OwnedEditorUxInterception, createPromptSelectionInterceptor, editorVisualLineCount, } from "./owned-editor-ux.js";
7
7
  import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
8
8
  import { createTuiFacade, ensureTheme, isAutocompleteProvider, } from "./shell-shared-facade.js";
9
9
  export const PINNED_PI_BUILTIN_SLASH_COMMANDS = [
@@ -33,6 +33,7 @@ export const PINNED_PI_BUILTIN_SLASH_COMMANDS = [
33
33
  export function createPiShellEditor(options) {
34
34
  ensureTheme();
35
35
  const tui = createTuiFacade(options);
36
+ const inputPresentation = options.keybindingProfile === "a1" ? options.promptPresentation?.input : undefined;
36
37
  const keybindings = options.keybindingProfile === "a1"
37
38
  ? KeybindingsManager.createForOwnedInput(options.agentDir)
38
39
  : KeybindingsManager.create(options.agentDir);
@@ -40,20 +41,33 @@ export function createPiShellEditor(options) {
40
41
  if (options.persistentHistory === true && options.keybindingProfile === "a1" && options.historyEditor === undefined) {
41
42
  throw new Error("Persistent history requires the loaded owned editor");
42
43
  }
44
+ const scrollInfo = { emitted: false, counter: undefined };
45
+ const selectListTheme = getSelectListTheme();
43
46
  const EditorClass = options.keybindingProfile === "a1" && options.persistentHistory === true ? options.historyEditor : OwnedEditor;
44
47
  const editor = new EditorClass(tui, {
45
- borderColor: (value) => piTheme().fg("borderMuted", value),
46
- selectList: getSelectListTheme(),
48
+ borderColor: (value) => inputPresentation === undefined ? piTheme().fg("borderMuted", value) : inputPresentation.styleRule(value),
49
+ selectList: options.keybindingProfile !== "a1" ? selectListTheme : {
50
+ ...selectListTheme,
51
+ scrollInfo: text => {
52
+ // Protocol: the pinned list emits its final counter row through this callback.
53
+ scrollInfo.emitted = true;
54
+ scrollInfo.counter = /^ \((\d+\/\d+)\)$/u.exec(text)?.[1];
55
+ return selectListTheme.scrollInfo(text);
56
+ },
57
+ },
47
58
  }, keybindings, {
48
59
  paddingX: PINNED_PI_LAYOUT.editorPaddingX,
49
60
  autocompleteMaxVisible: PINNED_PI_LAYOUT.autocompleteMaxVisible,
50
61
  persistentHistory: options.persistentHistory === true,
51
62
  styleHistoryLabel: text => piTheme().fg("dim", text),
63
+ ...(options.keybindingProfile === "a1" ? {
64
+ terminalRows: options.getRows,
65
+ getVisualLineCount: (width) => editorVisualLineCount(editor, width),
66
+ } : {}),
52
67
  ...(options.keybindingProfile === "a1" && options.promptPresentation !== undefined ? {
53
- promptPrefix: options.promptPresentation.prefix,
68
+ ...(inputPresentation === undefined ? {} : { inputPresentation }),
54
69
  styleSuggestion: options.promptPresentation.styleSuggestion,
55
70
  styleSuggestionCaret: options.promptPresentation.styleSuggestionCaret,
56
- terminalRows: options.getRows,
57
71
  } : {}),
58
72
  });
59
73
  const editorUx = options.keybindingProfile === "a1"
@@ -70,7 +84,7 @@ export function createPiShellEditor(options) {
70
84
  decorateRow: options.decorateEditorRow ?? (row => row),
71
85
  requestRender: options.requestRender,
72
86
  getRows: options.getRows,
73
- ...(options.promptPresentation === undefined ? {} : { promptPrefixWidth: 2 }),
87
+ ...(inputPresentation === undefined ? {} : { promptGeometry: (width, padding) => inputPresentation.geometry(width, padding) }),
74
88
  }),
75
89
  ], {
76
90
  render: width => editor.render(width),
@@ -80,9 +94,9 @@ export function createPiShellEditor(options) {
80
94
  let thinkingLevel = "off";
81
95
  let isBashMode = false;
82
96
  const updateBorderColor = () => {
83
- editor.borderColor = isBashMode
84
- ? piTheme().getBashModeBorderColor()
85
- : piTheme().getThinkingBorderColor(thinkingLevel);
97
+ editor.borderColor = inputPresentation !== undefined
98
+ ? text => inputPresentation.styleRule(text)
99
+ : isBashMode ? piTheme().getBashModeBorderColor() : piTheme().getThinkingBorderColor(thinkingLevel);
86
100
  tui.requestRender();
87
101
  };
88
102
  let autocompleteProvider;
@@ -147,13 +161,43 @@ export function createPiShellEditor(options) {
147
161
  if (options.onPromptSuggestionAccepted !== undefined) {
148
162
  editor.onPromptSuggestionAccepted = options.onPromptSuggestionAccepted;
149
163
  }
164
+ let bodyGeometry = { rowOffset: 0, rowCount: 0 };
150
165
  return {
151
- render: width => editorUx?.render(width) ?? editor.render(width),
166
+ ...(editorUx === undefined ? {} : { bodyGeometry: () => bodyGeometry }),
167
+ render: width => {
168
+ scrollInfo.emitted = false;
169
+ scrollInfo.counter = undefined;
170
+ const rows = editorUx?.render(width) ?? editor.render(width);
171
+ if (editorUx === undefined)
172
+ return rows;
173
+ const rowCount = editor.getRenderedBodyRowCount();
174
+ const menu = rows.slice(rowCount, scrollInfo.emitted ? -1 : undefined);
175
+ const label = scrollInfo.counter === undefined ? "" : `${scrollInfo.counter} `;
176
+ // Compatibility: match the history border's four-cell inset and dim label style.
177
+ const counterBorder = label.length > 0 && 4 + visibleWidth(label) <= width
178
+ ? editor.borderColor("─── ") + piTheme().fg("dim", label)
179
+ + editor.borderColor("─".repeat(width - 4 - visibleWidth(label)))
180
+ : editor.borderColor("─".repeat(width));
181
+ // Rationale: when the menu is open, drop the plain top border above the
182
+ // menu and instead show the counter on the border directly above the input
183
+ // prompt (the body's own top border row), mirroring the history header.
184
+ const bodyRows = menu.length === 0
185
+ ? rows.slice(0, rowCount)
186
+ : [counterBorder, ...rows.slice(1, rowCount)];
187
+ bodyGeometry = { rowOffset: menu.length, rowCount };
188
+ // Invariant: decorate/select in body coordinates first, then move the whole menu.
189
+ // Completion state, sizing, styles, and pagination still belong to the editor.
190
+ return [...menu, ...bodyRows];
191
+ },
152
192
  activateKeybindings: () => setKeybindings(keybindings),
153
193
  keybindingConfig: () => keybindings.getEffectiveConfig(),
154
194
  reloadKeybindings: () => { keybindings.reload(); setKeybindings(keybindings); },
155
195
  matchesTerminalKey: (data, key) => matchesKey(data, key),
156
196
  handleInput: data => {
197
+ // Compatibility: an unassigned reverse Tab must not clear selection or reach autocomplete fallback.
198
+ if (editorUx !== undefined && matchesKey(data, "shift+tab")
199
+ && !Object.values(keybindings.getEffectiveConfig()).some(keys => (Array.isArray(keys) ? keys : [keys]).some(key => key !== undefined && matchesKey(data, key))))
200
+ return;
157
201
  if (editorUx === undefined)
158
202
  editor.handleInput(data);
159
203
  else
@@ -210,6 +254,17 @@ export function createPiShellEditor(options) {
210
254
  editor.invalidate();
211
255
  },
212
256
  canPresentPromptSuggestion: () => editor.canPresentPromptSuggestion(),
257
+ promptSuggestionBlockReason: () => {
258
+ if (!editor.focused)
259
+ return "not-focused";
260
+ if (editor.disableSubmit)
261
+ return "not-ready";
262
+ if (editor.getText().length > 0)
263
+ return "draft";
264
+ if (editor.isShowingAutocomplete())
265
+ return "autocomplete";
266
+ return editor.canPresentPromptSuggestion() ? null : "prompt-mode";
267
+ },
213
268
  hasSelection: () => editorUx?.hasSelection() ?? false,
214
269
  ownsPointer: () => editorUx?.ownsPointer() ?? false,
215
270
  handlePointer: event => editorUx?.handlePointer(event) ?? false,
@@ -3,5 +3,5 @@ import { type PiShellViewComponentPort, type PiShellStatusPort, type PiShellQueu
3
3
  export declare function createPiShellHeader(options?: PiShellHeaderOptions): PiShellHeaderPort;
4
4
  export declare function createPiShellLoadedResources(resources: readonly PiShellResourceEntry[], initialExpanded?: boolean): PiShellLoadedResourcesPort;
5
5
  export declare function createPiShellStatus(view: OwnedUiSessionViewModel, formatProgressStatus: (message: string) => string, runtime?: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">): PiShellStatusPort;
6
- export declare function createPiShellFooter(view: OwnedUiSessionViewModel, cwd: string): PiShellViewComponentPort;
6
+ export declare function createPiShellFooter(view: OwnedUiSessionViewModel, cwd: string, profile?: "pi" | "a1"): PiShellViewComponentPort;
7
7
  export declare function createPiQueuedInputStatus(submissions: readonly string[], presentation?: "pinned" | "custom-viewport"): PiShellQueuedInputPort;
@@ -1,6 +1,7 @@
1
1
  import { rawKeyHint, VERSION } from "@earendil-works/pi-coding-agent";
2
2
  import { Spacer, Text, } from "#pi-tui";
3
3
  import { SessionFooter } from "./upstream/components/session-footer.js";
4
+ import { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
4
5
  import { WorkingStatusIndicator } from "./upstream/components/status-indicator.js";
5
6
  import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
6
7
  import { createTuiFacade, ensureTheme, } from "./shell-shared-facade.js";
@@ -16,6 +17,8 @@ export function createPiShellHeader(options = {}) {
16
17
  render(width) {
17
18
  if (options.quiet)
18
19
  return [];
20
+ if (expanded && options.getKeybindings !== undefined)
21
+ full.setText(expandedHeaderText(options.getKeybindings()));
19
22
  return [
20
23
  ...new Spacer(1).render(width),
21
24
  ...(expanded ? full : compact).render(width),
@@ -111,9 +114,9 @@ export function createPiShellStatus(view, formatProgressStatus, runtime) {
111
114
  },
112
115
  };
113
116
  }
114
- export function createPiShellFooter(view, cwd) {
117
+ export function createPiShellFooter(view, cwd, profile = "pi") {
115
118
  ensureTheme();
116
- const footer = new SessionFooter(() => view, cwd);
119
+ const footer = new SessionFooter(() => view, cwd, profile);
117
120
  return {
118
121
  render: width => footer.render(width),
119
122
  invalidate: () => footer.invalidate(),
@@ -184,7 +187,8 @@ function compactHeaderText() {
184
187
  const onboarding = theme.fg("dim", "Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.");
185
188
  return `${logo}\n${instructions}\n${compactOnboarding}\n\n${onboarding}`;
186
189
  }
187
- function expandedHeaderText() {
190
+ function expandedHeaderText(bindings) {
191
+ const keys = bindings === undefined ? undefined : KeybindingsManager.fromOwnedBindings(bindings);
188
192
  const instructions = [
189
193
  rawKeyHint("escape", "to interrupt"),
190
194
  rawKeyHint("ctrl+c", "to clear"),
@@ -192,9 +196,9 @@ function expandedHeaderText() {
192
196
  rawKeyHint("ctrl+d", "to exit (empty)"),
193
197
  rawKeyHint(process.platform === "win32" ? "" : "ctrl+z", "to suspend"),
194
198
  rawKeyHint("ctrl+k", "to delete to end"),
195
- rawKeyHint("shift+tab", "to cycle thinking level"),
199
+ rawKeyHint(keys?.getKeys("app.thinking.cycle").join("/") ?? "shift+tab", "to cycle thinking level"),
196
200
  rawKeyHint("ctrl+p/shift+ctrl+p", "to cycle models"),
197
- rawKeyHint("ctrl+l", "to select model"),
201
+ rawKeyHint(keys === undefined ? "ctrl+l" : keys.getKeys("app.model.select").join("/") || "/model", "to select model"),
198
202
  rawKeyHint("ctrl+o", "to expand tools"),
199
203
  rawKeyHint("ctrl+t", "to expand thinking"),
200
204
  rawKeyHint("ctrl+g", "for external editor"),
@@ -86,12 +86,17 @@ function shortcutDisplay(key) {
86
86
  }
87
87
  export function createPiShellHotkeys(bindings, getShortcuts = () => [], profile = "pi") {
88
88
  ensureTheme();
89
- const keys = new KeybindingsManager(bindings);
89
+ const keys = profile === "a1" ? KeybindingsManager.fromOwnedBindings(bindings) : new KeybindingsManager(bindings);
90
90
  // Compatibility: the owned viewport consumes these physical chords before editor actions.
91
91
  const display = (action) => keys.getKeys(action)
92
92
  .filter(key => profile !== "a1" || (key !== "ctrl+home" && key !== "ctrl+end"))
93
93
  .map(shortcutDisplay).join("/");
94
- const row = (actions, description) => `| ${actions.map(action => `\`${display(action)}\``).join(" / ")} | ${description} |`;
94
+ const row = (actions, description) => `| ${actions.map(action => {
95
+ const label = display(action);
96
+ return profile === "a1" && label.length === 0
97
+ ? action === "app.model.select" ? "Unbound (`/model`)" : "Unbound"
98
+ : `\`${label}\``;
99
+ }).join(" / ")} | ${description} |`;
95
100
  let markdown = ["**Navigation**", "| Key | Action |", "|-----|--------|", row(["tui.editor.cursorUp", "tui.editor.cursorDown", "tui.editor.cursorLeft", "tui.editor.cursorRight"], "Move cursor / browse history"), row(["tui.editor.cursorWordLeft", "tui.editor.cursorWordRight"], "Move by word"), row(["tui.editor.cursorLineStart"], profile === "a1" ? "Start of prompt line" : "Start of line"), row(["tui.editor.cursorLineEnd"], profile === "a1" ? "End of prompt line" : "End of line"), ...(profile === "a1" ? ["| `Ctrl+Home` | Start of content |", "| `Ctrl+End` | End of content / follow output |"] : []), row(["tui.editor.jumpForward"], "Jump forward to character"), row(["tui.editor.jumpBackward"], "Jump backward to character"), row(["tui.editor.pageUp", "tui.editor.pageDown"], "Scroll by page"), "", "**Editing**", "| Key | Action |", "|-----|--------|", row(["tui.input.submit"], "Send message"), row(["tui.input.newLine"], `New line${process.platform === "win32" ? " (Ctrl+Enter on Windows Terminal)" : ""}`), row(["tui.editor.deleteWordBackward"], "Delete word backwards"), row(["tui.editor.deleteWordForward"], "Delete word forwards"), row(["tui.editor.deleteToLineStart"], "Delete to start of line"), row(["tui.editor.deleteToLineEnd"], "Delete to end of line"), row(["tui.editor.yank"], "Paste the most-recently-deleted text"), row(["tui.editor.yankPop"], "Cycle through the deleted text after pasting"), row(["tui.editor.undo"], "Undo"), "", "**Other**", "| Key | Action |", "|-----|--------|", row(["tui.input.tab"], "Path completion / accept autocomplete"), row(["app.interrupt"], "Cancel autocomplete / abort streaming"), row(["app.clear"], "Clear editor (first) / exit (second)"), row(["app.exit"], "Exit (when editor is empty)"), row(["app.suspend"], "Suspend to background"), row(["app.thinking.cycle"], "Cycle thinking level"), row(["app.model.cycleForward", "app.model.cycleBackward"], "Cycle models"), row(["app.model.select"], "Open model selector"), row(["app.tools.expand"], "Toggle tool output expansion"), row(["app.thinking.toggle"], "Toggle thinking block visibility"), row(["app.editor.external"], "Edit message in external editor"), row(["app.message.copy"], "Copy last assistant message"), row(["app.message.followUp"], "Queue follow-up message"), row(["app.message.dequeue"], "Restore queued messages"), row(["app.clipboard.pasteImage"], "Paste image or text from clipboard"), "| `/` | Slash commands |", "| `!` | Run bash command |", "| `!!` | Run bash command (excluded from context) |"].join("\n");
96
101
  const shortcuts = getShortcuts(bindings ?? keys.getEffectiveConfig());
97
102
  if (shortcuts.length > 0) {
@@ -2,9 +2,13 @@ import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
2
2
  import { type MermaidRenderingMode } from "./upstream/components/mermaid.js";
3
3
  import type { OwnedUiTranscriptBlock } from "../../../contracts/owned-ui/index.js";
4
4
  import { type PiShellSubmittedPromptComposer } from "./submitted-prompt-adapter.js";
5
- export type { PiShellSubmittedPromptComposer } from "./submitted-prompt-adapter.js";
5
+ export { isPiPromptStyleCompaction, type PiShellSubmittedPromptComposer } from "./submitted-prompt-adapter.js";
6
6
  import { type PiShellExtensionRendererResolver, type PiShellImageAssetResolver, type PiShellTranscriptComponentPort } from "./shell-shared-facade.js";
7
- export declare function createPiShellTranscriptComponent(initial: OwnedUiTranscriptBlock, cwd: string, extensions?: PiShellExtensionRendererResolver, submittedPrompt?: PiShellSubmittedPromptComposer, initialOutputPad?: 0 | 1, initialHideThinkingBlock?: boolean, initialMermaidRenderingMode?: MermaidRenderingMode, initialShowImages?: boolean, initialImageWidthCells?: number, imageAssets?: PiShellImageAssetResolver): PiShellTranscriptComponentPort;
7
+ export declare function createPiShellTranscriptComponent(initial: OwnedUiTranscriptBlock, cwd: string, extensions?: PiShellExtensionRendererResolver, submittedPrompt?: PiShellSubmittedPromptComposer, initialOutputPad?: 0 | 1, initialHideThinkingBlock?: boolean, initialMermaidRenderingMode?: MermaidRenderingMode, initialShowImages?: boolean, initialImageWidthCells?: number, imageAssets?: PiShellImageAssetResolver, presentation?: {
8
+ readonly getColumns: () => number;
9
+ readonly getRows: () => number;
10
+ readonly changed: () => void;
11
+ }): PiShellTranscriptComponentPort;
8
12
  export declare function renderPiShellTranscriptBlock(block: OwnedUiTranscriptBlock, width: number, cwd: string): readonly string[];
9
13
  /**
10
14
  * Pinned Pi's CLI prints startup diagnostics with `reportDiagnostics` before