@timurproko/a1 0.1.8-dev.335 → 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 (98) hide show
  1. package/README.md +18 -0
  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/integrations/pi/components/owned-editor-ux.d.ts +9 -1
  31. package/dist/integrations/pi/components/owned-editor-ux.js +11 -8
  32. package/dist/integrations/pi/components/prompt-input-port.d.ts +18 -0
  33. package/dist/integrations/pi/components/prompt-input-port.js +1 -0
  34. package/dist/integrations/pi/components/shell-editor-autocomplete.js +66 -11
  35. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  36. package/dist/integrations/pi/components/shell-footer-status.js +9 -5
  37. package/dist/integrations/pi/components/shell-presenters-info.js +7 -2
  38. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +6 -2
  39. package/dist/integrations/pi/components/shell-presenters-transcript.js +81 -49
  40. package/dist/integrations/pi/components/shell-shared-facade.d.ts +17 -4
  41. package/dist/integrations/pi/components/shell-shared-facade.js +2 -1
  42. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +3 -1
  43. package/dist/integrations/pi/components/submitted-prompt-adapter.js +20 -4
  44. package/dist/integrations/pi/components/tool-result-adapter.d.ts +6 -0
  45. package/dist/integrations/pi/components/tool-result-adapter.js +27 -0
  46. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -0
  47. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +5 -0
  48. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -2
  49. package/dist/integrations/pi/components/upstream/components/owned-editor.js +41 -33
  50. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  51. package/dist/integrations/pi/components/upstream/components/session-footer.js +12 -7
  52. package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
  53. package/dist/integrations/pi/engine/adapter.d.ts +2 -1
  54. package/dist/integrations/pi/engine/adapter.js +181 -84
  55. package/dist/integrations/pi/engine/tool-rendering.d.ts +13 -0
  56. package/dist/integrations/pi/engine/tool-rendering.js +122 -0
  57. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +30 -0
  58. package/dist/integrations/pi/session-ui/prompt-chips.js +128 -1
  59. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +8 -0
  60. package/dist/integrations/pi/session-ui/prompt-history-controller.js +30 -2
  61. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +11 -3
  62. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +126 -51
  63. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +24 -2
  64. package/dist/integrations/pi/session-ui/session-shell-root.js +108 -38
  65. package/dist/integrations/pi/session-ui/session-shell.js +51 -31
  66. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -1
  67. package/dist/integrations/pi/session-ui/session-viewport-controller.js +106 -16
  68. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  69. package/dist/integrations/pi/tui-runtime/adapter.js +35 -11
  70. package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -1
  71. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -0
  72. package/dist/integrations/pi/tui-runtime/index.js +1 -0
  73. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +13 -0
  74. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +59 -0
  75. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +32 -0
  76. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +159 -0
  77. package/dist/native/darwin-arm64/manifest.json +1 -1
  78. package/dist/native/linux-x64/manifest.json +1 -1
  79. package/dist/native/win32-x64/manifest.json +2 -2
  80. package/dist/native/win32-x64/process-guardian.exe +0 -0
  81. package/dist/product-identity.d.ts +1 -1
  82. package/dist/product-identity.js +1 -1
  83. package/dist/product-identity.json +1 -0
  84. package/dist/ui/components/index.d.ts +1 -0
  85. package/dist/ui/components/index.js +1 -0
  86. package/dist/ui/components/line-input.d.ts +2 -3
  87. package/dist/ui/components/line-input.js +4 -20
  88. package/dist/ui/components/mouse.d.ts +1 -1
  89. package/dist/ui/components/mouse.js +4 -3
  90. package/dist/ui/components/prompt-input.d.ts +32 -0
  91. package/dist/ui/components/prompt-input.js +54 -0
  92. package/docs/architecture/boundaries.md +1 -0
  93. package/docs/architecture/history-editor-provenance.md +5 -1
  94. package/docs/architecture/project-structure.md +1 -0
  95. package/docs/architecture/prompt-suggestions.md +68 -0
  96. package/docs/architecture/ui-reference-provenance.md +23 -0
  97. package/docs/features/modal-content-interaction.md +37 -0
  98. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, statSync } from "
3
3
  import { dirname } from "node:path";
4
4
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
5
  import { assertPromptHistorySubmission, PROMPT_HISTORY_MAX_ENTRY_BYTES, PROMPT_HISTORY_MAX_TEXT_BYTES } from "../../contracts/owned-ui/index.js";
6
+ import { collectImageChipIdentifiers, PromptImageSidecar } from "./image-sidecar.js";
6
7
  export class HistoryStorageError extends Error {
7
8
  code;
8
9
  certainty;
@@ -17,11 +18,13 @@ export class PromptHistoryStore {
17
18
  path;
18
19
  profileId;
19
20
  #database;
20
- constructor(path, profileId, limit) {
21
+ #sidecar;
22
+ constructor(path, profileId, limit, imagesDir) {
21
23
  this.path = path;
22
24
  this.profileId = profileId;
23
25
  if (!Number.isInteger(limit) || limit < 10 || limit > 100 || limit % 10 !== 0)
24
26
  throw new HistoryStorageError("schema");
27
+ this.#sidecar = imagesDir === undefined ? undefined : new PromptImageSidecar(imagesDir);
25
28
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
26
29
  if (lstatSync(dirname(path)).isSymbolicLink())
27
30
  throw new HistoryStorageError("unavailable");
@@ -67,6 +70,10 @@ export class PromptHistoryStore {
67
70
  this.#database.close();
68
71
  throw error;
69
72
  }
73
+ // Rationale: opportunistically reclaim any sidecar whose row was pruned in a prior process
74
+ // crash or by another concurrent opener. Bounded by the number of files on disk minus the
75
+ // surviving reference set; failures are swallowed to preserve honest history behavior.
76
+ this.#sweepOrphanedSidecars();
70
77
  }
71
78
  record(input) {
72
79
  assertPromptHistorySubmission(input);
@@ -133,11 +140,48 @@ export class PromptHistoryStore {
133
140
  #prune(limit) {
134
141
  const rows = this.#database.prepare("SELECT sequence,bytes FROM prompts ORDER BY sequence DESC").all();
135
142
  let bytes = 0;
143
+ const dropped = [];
136
144
  for (const [index, row] of rows.entries()) {
137
145
  bytes += Number(row.bytes);
138
146
  if (index >= limit || bytes > PROMPT_HISTORY_MAX_TEXT_BYTES)
139
- this.#database.prepare("DELETE FROM prompts WHERE sequence=?").run(row.sequence);
147
+ dropped.push(row.sequence);
140
148
  }
149
+ if (dropped.length === 0)
150
+ return;
151
+ // Invariant: sidecar reap uses text still referenced by SURVIVING rows so a row still
152
+ // referencing an id shared with a pruned row keeps the file. Collect surviving ids before
153
+ // deleting so a concurrent snapshot cannot see a live row whose sidecar is already gone.
154
+ const droppedTexts = dropped.map(sequence => this.#database.prepare("SELECT text FROM prompts WHERE sequence=?").get(sequence)?.text).filter((value) => typeof value === "string");
155
+ for (const sequence of dropped)
156
+ this.#database.prepare("DELETE FROM prompts WHERE sequence=?").run(sequence);
157
+ if (this.#sidecar === undefined)
158
+ return;
159
+ const survivingIds = this.#collectReferencedIdentifiers();
160
+ for (const text of droppedTexts) {
161
+ for (const id of collectImageChipIdentifiers(text)) {
162
+ if (!survivingIds.has(id))
163
+ this.#sidecar.unlink(id);
164
+ }
165
+ }
166
+ }
167
+ #collectReferencedIdentifiers() {
168
+ const identifiers = new Set();
169
+ const rows = this.#database.prepare("SELECT text FROM prompts").all();
170
+ for (const row of rows) {
171
+ if (typeof row.text !== "string")
172
+ continue;
173
+ for (const id of collectImageChipIdentifiers(row.text))
174
+ identifiers.add(id);
175
+ }
176
+ return identifiers;
177
+ }
178
+ #sweepOrphanedSidecars() {
179
+ if (this.#sidecar === undefined)
180
+ return;
181
+ try {
182
+ this.#sidecar.sweep(this.#collectReferencedIdentifiers());
183
+ }
184
+ catch { /* Rationale: sidecar sweep never blocks history readiness. */ }
141
185
  }
142
186
  #checkSize(maintain = false) {
143
187
  let size = 0;
@@ -6,7 +6,7 @@ if (port !== null) {
6
6
  let store;
7
7
  let chain = Promise.resolve();
8
8
  const options = workerData;
9
- const open = retry(() => { store = new PromptHistoryStore(options.path, options.profileId, options.limit); });
9
+ const open = retry(() => { store = new PromptHistoryStore(options.path, options.profileId, options.limit, options.imagesDir); });
10
10
  void open.then(() => port.postMessage({ id: 0, ok: true }), error => port.postMessage({ id: 0, ok: false, code: classifyHistoryError(error) }));
11
11
  port.on("message", (request) => {
12
12
  chain = chain.then(async () => {
@@ -0,0 +1,18 @@
1
+ import { type SuggestionDiagnosticObserver, type SuggestionDiagnosticRecord } from "../../contracts/owned-ui/index.js";
2
+ export interface SuggestionDiagnosticCaptureOptions {
3
+ readonly enabled?: boolean;
4
+ /** Explicit local destination, or an injected sink for deterministic I/O tests. */
5
+ readonly destination?: string;
6
+ readonly writeSnapshot?: (snapshot: string) => Promise<void>;
7
+ }
8
+ /** Bounded metadata snapshots; one in-flight write and one replaceable latest snapshot. */
9
+ export declare class SuggestionDiagnosticCapture implements SuggestionDiagnosticObserver {
10
+ #private;
11
+ readonly options: SuggestionDiagnosticCaptureOptions;
12
+ constructor(options?: SuggestionDiagnosticCaptureOptions);
13
+ record(record: SuggestionDiagnosticRecord): void;
14
+ snapshot(): readonly SuggestionDiagnosticRecord[];
15
+ /** Explicit evidence boundary, never awaited by input or rendering. */
16
+ flush(): Promise<void>;
17
+ dispose(): void;
18
+ }
@@ -0,0 +1,82 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { SUGGESTION_DECISION_REASONS, SUGGESTION_DIAGNOSTIC_EVENTS, } from "../../contracts/owned-ui/index.js";
3
+ /** Bounded metadata snapshots; one in-flight write and one replaceable latest snapshot. */
4
+ export class SuggestionDiagnosticCapture {
5
+ options;
6
+ #records = [];
7
+ #pending;
8
+ #writing;
9
+ #disposed = false;
10
+ #write;
11
+ constructor(options = {}) {
12
+ this.options = options;
13
+ this.#write = options.writeSnapshot ?? (options.destination === undefined ? undefined
14
+ : snapshot => writeFile(options.destination, snapshot, { encoding: "utf8", mode: 0o600 }));
15
+ }
16
+ record(record) {
17
+ if (!this.options.enabled || this.#disposed)
18
+ return;
19
+ const safe = sanitize(record);
20
+ if (safe === null)
21
+ return;
22
+ this.#records.push(safe);
23
+ if (this.#records.length > 128)
24
+ this.#records.shift();
25
+ let snapshot = this.#serialize();
26
+ while (Buffer.byteLength(snapshot, "utf8") > 65_536) {
27
+ this.#records.shift();
28
+ snapshot = this.#serialize();
29
+ }
30
+ if (this.#write) {
31
+ this.#pending = snapshot;
32
+ this.#pump();
33
+ }
34
+ }
35
+ snapshot() { return this.#records.map(record => ({ ...record })); }
36
+ /** Explicit evidence boundary, never awaited by input or rendering. */
37
+ async flush() {
38
+ while (this.#writing)
39
+ await this.#writing;
40
+ }
41
+ dispose() {
42
+ this.#disposed = true;
43
+ this.#records = [];
44
+ this.#pending = undefined;
45
+ }
46
+ #serialize() { return JSON.stringify({ schema: "prompt-suggestion-diagnostics-v1", records: this.#records }); }
47
+ #pump() {
48
+ if (this.#writing || this.#pending === undefined || !this.#write || this.#disposed)
49
+ return;
50
+ const snapshot = this.#pending;
51
+ this.#pending = undefined;
52
+ this.#writing = Promise.resolve().then(() => this.#write(snapshot)).catch(() => {
53
+ // Security: do not retain or display raw filesystem/provider errors.
54
+ }).finally(() => {
55
+ this.#writing = undefined;
56
+ this.#pump();
57
+ });
58
+ }
59
+ }
60
+ function sanitize(record) {
61
+ if (!SUGGESTION_DIAGNOSTIC_EVENTS.includes(record.event))
62
+ return null;
63
+ const reasoning = ["ordinary", "off", "minimal", "low", "medium", "high", "xhigh", "max", "unavailable"].includes(record.reasoning)
64
+ ? record.reasoning : "unavailable";
65
+ const reason = record.reason !== undefined && SUGGESTION_DECISION_REASONS.includes(record.reason) ? record.reason : undefined;
66
+ // Security: explicit projection prevents arbitrary extra fields, including raw error payloads, from escaping.
67
+ return {
68
+ event: record.event, ...(reason === undefined ? {} : { reason }),
69
+ session: boundedNumber(record.session), run: boundedNumber(record.run), response: boundedNumber(record.response),
70
+ request: boundedNumber(record.request), provider: identifier(record.provider), model: identifier(record.model),
71
+ reasoning, elapsedMs: boundedNumber(record.elapsedMs),
72
+ };
73
+ }
74
+ function boundedNumber(value) {
75
+ return Number.isFinite(value) ? Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.floor(value))) : 0;
76
+ }
77
+ function identifier(value) {
78
+ // Security: identifiers are metadata, not display names or arbitrary JSON. Reject controls/path-shaped values.
79
+ if (typeof value !== "string" || /[\p{C}\\\s]/u.test(value) || value.includes(":") || value.startsWith("/"))
80
+ return "redacted";
81
+ return value.slice(0, 64);
82
+ }
@@ -0,0 +1 @@
1
+ export * from "./diagnostics.js";
@@ -0,0 +1 @@
1
+ export * from "./diagnostics.js";
@@ -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