@timurproko/a1 0.1.8-dev.563 → 0.1.8-dev.573

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 (71) hide show
  1. package/README.md +13 -11
  2. package/dist/app/session-shell/prompt-chips.js +20 -28
  3. package/dist/app/session-shell/session-shell-root.d.ts +2 -0
  4. package/dist/app/session-shell/session-shell-root.js +60 -17
  5. package/dist/app/session-shell/session-shell.js +35 -1
  6. package/dist/app/session-shell/session-viewport-controller.d.ts +1 -0
  7. package/dist/app/session-shell/session-viewport-controller.js +52 -30
  8. package/dist/cli/dispatch.d.ts +3 -1
  9. package/dist/cli/dispatch.js +56 -56
  10. package/dist/cli/package-messages.d.ts +4 -3
  11. package/dist/cli/package-messages.js +23 -11
  12. package/dist/contracts/owned-ui/index.d.ts +2 -0
  13. package/dist/contracts/owned-ui/index.js +1 -0
  14. package/dist/contracts/owned-ui/prompt-chips.d.ts +15 -0
  15. package/dist/contracts/owned-ui/prompt-chips.js +59 -0
  16. package/dist/contracts/owned-ui/validation.js +1 -1
  17. package/dist/integrations/pi/components/index.d.ts +1 -0
  18. package/dist/integrations/pi/components/index.js +1 -0
  19. package/dist/integrations/pi/components/modal-frame.d.ts +41 -0
  20. package/dist/integrations/pi/components/modal-frame.js +110 -0
  21. package/dist/integrations/pi/components/models-dialog.js +19 -5
  22. package/dist/integrations/pi/components/shell-footer-status.d.ts +4 -3
  23. package/dist/integrations/pi/components/shell-footer-status.js +53 -15
  24. package/dist/integrations/pi/components/shell-selectors-dialogs.js +38 -2
  25. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -1
  26. package/dist/integrations/pi/components/skills-dialog.js +3 -3
  27. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
  28. package/dist/integrations/pi/components/submitted-prompt-adapter.js +5 -2
  29. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +2 -2
  30. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +3 -2
  31. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -1
  32. package/dist/integrations/pi/components/upstream/components/extension-editor.js +7 -6
  33. package/dist/integrations/pi/components/upstream/components/extension-input.js +9 -8
  34. package/dist/integrations/pi/components/upstream/components/extension-selector.js +10 -9
  35. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  36. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -3
  37. package/dist/integrations/pi/components/upstream/components/session-selector.js +13 -6
  38. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
  39. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +15 -12
  40. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  41. package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -6
  42. package/dist/integrations/pi/components/upstream/components/trust-selector.js +9 -9
  43. package/dist/integrations/pi/engine/compaction-progress.d.ts +3 -7
  44. package/dist/integrations/pi/engine/compaction-progress.js +11 -10
  45. package/dist/integrations/pi/engine/repository-pr.d.ts +3 -3
  46. package/dist/integrations/pi/engine/repository-pr.js +6 -6
  47. package/dist/integrations/pi/engine/session-runtime.js +5 -4
  48. package/dist/integrations/pi/engine/settings-bridge.js +1 -1
  49. package/dist/integrations/pi/engine/settings-port.js +1 -1
  50. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  51. package/dist/integrations/pi/tui-runtime/adapter.js +11 -2
  52. package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -0
  53. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +6 -0
  54. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +34 -0
  55. package/dist/native/darwin-arm64/manifest.json +1 -1
  56. package/dist/native/linux-x64/manifest.json +1 -1
  57. package/dist/native/win32-x64/manifest.json +3 -3
  58. package/dist/native/win32-x64/process-guardian.exe +0 -0
  59. package/dist/ui/components/dialog-panel.js +10 -6
  60. package/dist/ui/components/index.d.ts +2 -1
  61. package/dist/ui/components/index.js +1 -1
  62. package/dist/ui/components/mouse.d.ts +2 -2
  63. package/dist/ui/components/mouse.js +2 -2
  64. package/dist/ui/components/progress-status.d.ts +9 -2
  65. package/dist/ui/components/progress-status.js +24 -3
  66. package/dist/ui/components/transcript-viewport.d.ts +9 -3
  67. package/dist/ui/components/transcript-viewport.js +116 -136
  68. package/docs/architecture/ui-reference-provenance.md +8 -5
  69. package/docs/openspec-archive-automation.md +2 -2
  70. package/docs/validation.md +1 -1
  71. package/package.json +1 -1
@@ -1,7 +1,15 @@
1
1
  import { Input, Key, getKeybindings, matchesKey, truncateToWidth, } from "@earendil-works/pi-tui";
2
+ import { DynamicBorder } from "../startup-public.js";
3
+ import { PiModalFrame } from "./modal-frame.js";
2
4
  import { piTheme, renderPiModalShortcutHints } from "./theme.js";
3
5
  const MAX_VISIBLE_ROWS = 10;
4
6
  const MODELS_TITLE = "Models";
7
+ class ModalTitleLine {
8
+ #text = "";
9
+ setText(text) { this.#text = text; }
10
+ invalidate() { }
11
+ render(width) { return [truncateToWidth(this.#text, width)]; }
12
+ }
5
13
  function fullModelId(model) {
6
14
  return `${model.provider}/${model.id}`;
7
15
  }
@@ -27,6 +35,12 @@ function keyLabel(action) {
27
35
  */
28
36
  export class ModelsDialogComponent {
29
37
  #input = new Input();
38
+ #title = new ModalTitleLine();
39
+ #body = {
40
+ invalidate: () => this.#input.invalidate(),
41
+ render: width => this.#renderBody(width),
42
+ };
43
+ #frame = new PiModalFrame(new DynamicBorder(), [this.#title, this.#body], new DynamicBorder());
30
44
  #callbacks;
31
45
  #models = [];
32
46
  #activeModelId;
@@ -182,15 +196,16 @@ export class ModelsDialogComponent {
182
196
  this.#selectedIndex = 0;
183
197
  }
184
198
  render(width) {
199
+ const theme = piTheme();
200
+ this.#title.setText(theme.fg("accent", theme.bold(MODELS_TITLE)) + (this.dirty ? theme.fg("warning", " (unsaved)") : ""));
201
+ return this.#frame.render(width);
202
+ }
203
+ #renderBody(width) {
185
204
  const theme = piTheme();
186
205
  const lines = [];
187
206
  const push = (line = "") => { lines.push(truncateToWidth(line, width)); };
188
- const border = theme.fg("border", "─".repeat(Math.max(1, width)));
189
207
  const rows = this.#rows();
190
208
  this.#clampSelection(rows);
191
- push(border);
192
- push();
193
- push(theme.fg("accent", theme.bold(MODELS_TITLE)) + (this.dirty ? theme.fg("warning", " (unsaved)") : ""));
194
209
  push(theme.fg("dim", "Filter: ")
195
210
  + theme.fg(this.#filter === "all" ? "accent" : "dim", "all")
196
211
  + theme.fg("dim", " | ")
@@ -229,7 +244,6 @@ export class ModelsDialogComponent {
229
244
  if (this.#refreshStatus !== undefined)
230
245
  push(theme.fg(this.#refreshStatus.kind, ` ${this.#refreshStatus.message}`));
231
246
  push(renderPiModalShortcutHints(this.#hints()));
232
- push(border);
233
247
  return lines;
234
248
  }
235
249
  #hints() {
@@ -1,7 +1,8 @@
1
1
  import type { OwnedUiSessionViewModel } from "../../../contracts/owned-ui/index.js";
2
- import { type PiShellViewComponentPort, type PiShellStatusPort, type PiShellQueuedInputPort, type PiShellHeaderPort, type PiShellResourceEntry, type PiShellLoadedResourcesPort, type PiShellHeaderOptions, type PiShellEditorOptions } from "./shell-shared-facade.js";
2
+ import { type KeybindingsConfig } from "./upstream/adjacent/core/keybindings.js";
3
+ import { type PiShellViewComponentPort, type PiShellStatusPort, type PiShellProgressStatusPresentation, type PiShellQueuedInputPort, type PiShellHeaderPort, type PiShellResourceEntry, type PiShellLoadedResourcesPort, type PiShellHeaderOptions, type PiShellEditorOptions } from "./shell-shared-facade.js";
3
4
  export declare function createPiShellHeader(options?: PiShellHeaderOptions): PiShellHeaderPort;
4
5
  export declare function createPiShellLoadedResources(resources: readonly PiShellResourceEntry[], initialExpanded?: boolean): PiShellLoadedResourcesPort;
5
- export declare function createPiShellStatus(view: OwnedUiSessionViewModel, formatProgressStatus: (message: string) => string, runtime?: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">): PiShellStatusPort;
6
+ export declare function createPiShellStatus(view: OwnedUiSessionViewModel, progressStatus: PiShellProgressStatusPresentation, runtime?: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">): PiShellStatusPort;
6
7
  export declare function createPiShellFooter(view: OwnedUiSessionViewModel, cwd: string, profile?: "pi" | "a1", showLevel?: () => boolean): PiShellViewComponentPort;
7
- export declare function createPiQueuedInputStatus(submissions: readonly string[], presentation?: "pinned" | "custom-viewport"): PiShellQueuedInputPort;
8
+ export declare function createPiQueuedInputStatus(submissions: readonly string[], presentation?: "pinned" | "custom-viewport", getKeybindings?: () => KeybindingsConfig): PiShellQueuedInputPort;
@@ -2,7 +2,7 @@ import { rawKeyHint, VERSION } from "../startup-public.js";
2
2
  import { Spacer, Text, } from "@earendil-works/pi-tui";
3
3
  import { SessionFooter } from "./upstream/components/session-footer.js";
4
4
  import { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
5
- import { WorkingStatusIndicator } from "./upstream/components/status-indicator.js";
5
+ import { StatusIndicator, WorkingStatusIndicator } from "./upstream/components/status-indicator.js";
6
6
  import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
7
7
  import { createTuiFacade, ensureTheme, } from "./shell-shared-facade.js";
8
8
  export function createPiShellHeader(options = {}) {
@@ -70,15 +70,15 @@ export function createPiShellLoadedResources(resources, initialExpanded = false)
70
70
  invalidate() { },
71
71
  };
72
72
  }
73
- export function createPiShellStatus(view, formatProgressStatus, runtime) {
73
+ export function createPiShellStatus(view, progressStatus, runtime) {
74
74
  ensureTheme();
75
75
  const statusUi = createTuiFacade(runtime ?? { getColumns: () => 80, getRows: () => 24, requestRender() { } });
76
76
  let workingOverride;
77
77
  let outputPad = PINNED_PI_LAYOUT.outputPad;
78
78
  let progressPresentation = "pinned";
79
79
  let placement = statusPlacement(view, workingOverride);
80
- const liveStatusText = () => formatProgressStatus(liveWorkingText(view, workingOverride, progressPresentation));
81
- let component = statusComponent(view, statusUi, outputPad, liveStatusText, placement);
80
+ const liveStatusText = () => progressStatus.text(liveWorkingText(view, workingOverride, progressPresentation), progressPresentation);
81
+ let component = statusComponent(view, statusUi, outputPad, liveStatusText, placement, progressPresentation, progressStatus);
82
82
  let signature = statusSignature(view, workingOverride, outputPad, placement, progressPresentation);
83
83
  const rebuild = () => {
84
84
  const nextPlacement = statusPlacement(view, workingOverride);
@@ -86,7 +86,10 @@ export function createPiShellStatus(view, formatProgressStatus, runtime) {
86
86
  if (nextSignature === signature)
87
87
  return;
88
88
  // Performance: progress ticks change only the live message; the spinner keeps its frame and timer.
89
- if (placement === "live" && nextPlacement === "live" && component instanceof WorkingStatusIndicator
89
+ const componentMatchesPresentation = progressPresentation === "custom-viewport"
90
+ ? component instanceof OwnedWorkingStatusIndicator
91
+ : component instanceof WorkingStatusIndicator;
92
+ if (placement === "live" && nextPlacement === "live" && component instanceof StatusIndicator && componentMatchesPresentation
90
93
  && statusSignature(view, workingOverride, outputPad, nextPlacement, progressPresentation, false)
91
94
  === statusSignature(view, workingOverride, outputPad, placement, progressPresentation, false)) {
92
95
  component.setMessage(liveStatusText());
@@ -97,7 +100,7 @@ export function createPiShellStatus(view, formatProgressStatus, runtime) {
97
100
  component.dispose();
98
101
  placement = nextPlacement;
99
102
  signature = nextSignature;
100
- component = statusComponent(view, statusUi, outputPad, liveStatusText, placement);
103
+ component = statusComponent(view, statusUi, outputPad, liveStatusText, placement, progressPresentation, progressStatus);
101
104
  };
102
105
  return {
103
106
  render: width => component?.render(width) ?? [],
@@ -138,14 +141,27 @@ export function createPiShellFooter(view, cwd, profile = "pi", showLevel = () =>
138
141
  dispose: () => footer.dispose(),
139
142
  };
140
143
  }
141
- export function createPiQueuedInputStatus(submissions, presentation = "pinned") {
142
- const text = new Text(queuedInputText(submissions, presentation), 1, 0);
144
+ export function createPiQueuedInputStatus(submissions, presentation = "pinned", getKeybindings) {
145
+ let renderedText = queuedInputText(submissions, presentation, getKeybindings?.());
146
+ const text = new Text(renderedText, 1, 0);
147
+ const refresh = () => {
148
+ const next = queuedInputText(submissions, presentation, getKeybindings?.());
149
+ if (next === renderedText)
150
+ return;
151
+ renderedText = next;
152
+ text.setText(next);
153
+ };
143
154
  return {
144
- render: width => submissions.length === 0 ? [] : text.render(width),
155
+ render(width) {
156
+ if (submissions.length === 0)
157
+ return [];
158
+ refresh();
159
+ return text.render(width);
160
+ },
145
161
  invalidate: () => text.invalidate(),
146
162
  update(next) {
147
163
  submissions = next;
148
- text.setText(queuedInputText(next, presentation));
164
+ refresh();
149
165
  },
150
166
  };
151
167
  }
@@ -168,9 +184,23 @@ function liveWorkingText(view, workingOverride, progressPresentation) {
168
184
  ? `${message}(${progress}%)`
169
185
  : message;
170
186
  }
171
- function statusComponent(view, ui, outputPad, liveStatusText, placement) {
187
+ class OwnedWorkingStatusIndicator extends StatusIndicator {
188
+ constructor(ui, message, frame) {
189
+ let phase = -1;
190
+ super("working", ui, spinner => {
191
+ phase += 1;
192
+ return piTheme().fg("accent", spinner);
193
+ }, text => frame(text, phase, {
194
+ muted: value => piTheme().fg("muted", value),
195
+ accent: value => piTheme().fg("accent", value),
196
+ }), message);
197
+ }
198
+ }
199
+ function statusComponent(view, ui, outputPad, liveStatusText, placement, progressPresentation, progressStatus) {
172
200
  if (placement === "live") {
173
- return new WorkingStatusIndicator(ui, liveStatusText());
201
+ return progressPresentation === "custom-viewport"
202
+ ? new OwnedWorkingStatusIndicator(ui, liveStatusText(), progressStatus.frame)
203
+ : new WorkingStatusIndicator(ui, liveStatusText());
174
204
  }
175
205
  if (placement === "dock") {
176
206
  if (view.lifecycle === "failed") {
@@ -184,7 +214,7 @@ function statusSignature(view, workingOverride, outputPad, placement, progressPr
184
214
  const message = withMessage ? `${workingOverride ?? ""}\u0000${view.status.workingMessage ?? ""}\u0000${view.status.workingProgress ?? ""}` : "";
185
215
  return `${placement}\u0000${outputPad}\u0000${view.lifecycle}\u0000${progressPresentation}\u0000${message}\u0000${view.status.diagnostics.at(-1) ?? ""}`;
186
216
  }
187
- function queuedInputText(submissions, presentation) {
217
+ function queuedInputText(submissions, presentation, bindings) {
188
218
  if (submissions.length === 0)
189
219
  return "";
190
220
  const theme = piTheme();
@@ -192,11 +222,19 @@ function queuedInputText(submissions, presentation) {
192
222
  return submissions.map(submission => theme.fg("muted", `Steering: ${submission.replaceAll("\n", " ⏎ ")}`)).join("\n");
193
223
  }
194
224
  const messages = submissions.map(submission => theme.fg("dim", `Steering: ${submission.replaceAll("\n", " ⏎ ")}`));
195
- const dequeueHint = theme.fg("dim", "↳ Alt+Up to edit all queued messages");
225
+ const keys = KeybindingsManager.fromOwnedBindings(bindings).getKeys("app.message.dequeue");
226
+ const label = keys.length === 0 ? "Unbound" : keys.map(displayKeybinding).join("/");
227
+ const dequeueHint = theme.fg("dim", `↳ ${label} to edit all queued messages`);
196
228
  // Compatibility: the custom viewport matches Pi's interactive queue presentation while the
197
229
  // comparison shell remains byte-for-byte compatible with its pinned fixture.
198
230
  return ["", ...messages, dequeueHint].join("\n");
199
231
  }
232
+ function displayKeybinding(key) {
233
+ return key.split("+").map(part => {
234
+ const display = process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part;
235
+ return display.charAt(0).toUpperCase() + display.slice(1);
236
+ }).join("+");
237
+ }
200
238
  function compactHeaderText() {
201
239
  const theme = piTheme();
202
240
  const instructions = [
@@ -231,7 +269,7 @@ function expandedHeaderText(bindings) {
231
269
  rawKeyHint("!", "to run bash"),
232
270
  rawKeyHint("!!", "to run bash (no context)"),
233
271
  rawKeyHint("alt+enter", "to queue follow-up"),
234
- rawKeyHint("alt+up", "to edit all queued messages"),
272
+ rawKeyHint(keys === undefined ? "alt+up" : keys.getKeys("app.message.dequeue").join("/") || "unbound", "to edit all queued messages"),
235
273
  rawKeyHint(process.platform === "win32" ? "alt+v" : "ctrl+v", "to paste image (with text fallback)"),
236
274
  rawKeyHint("drop files", "to attach"),
237
275
  ].join("\n");
@@ -3,6 +3,7 @@ import { DaxnutsComponent, } from "./upstream/components/daxnuts.js";
3
3
  import { EarendilAnnouncementComponent, } from "./upstream/components/earendil-announcement.js";
4
4
  import { Box, Container, SelectList, Spacer, Text, } from "@earendil-works/pi-tui";
5
5
  import { ScopedModelsSelectorComponent, } from "./upstream/components/scoped-models-selector.js";
6
+ import { adoptPiModalFrame, adoptPiModalHeader } from "./modal-frame.js";
6
7
  import { ModelsDialogComponent, } from "./models-dialog.js";
7
8
  import { TrustSelectorComponent, } from "./upstream/components/trust-selector.js";
8
9
  import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
@@ -72,6 +73,7 @@ export function createPiShellSettingsSelector(options) {
72
73
  export function createPiShellModelSelector(options) {
73
74
  ensureTheme();
74
75
  const selector = new ModelSelectorComponent(createTuiFacade(options.runtime), options.currentModel, options.modelRuntime, options.scopedModels, options.onSelect, options.onCancel, options.initialSearchInput, options.onSelectAsDefault, options.defaultModel);
76
+ adoptPiModalFrame(selector, { topIndex: 0, bottomIndex: selector.children.length - 1 });
75
77
  return componentPort(selector);
76
78
  }
77
79
  function toPiScopedModel(model) {
@@ -137,11 +139,20 @@ export function createPiShellUserMessageSelector(messages, onSelect, onCancel, i
137
139
  ensureTheme();
138
140
  const selector = new UserMessageSelectorComponent(messages.map(message => ({ id: message.id, text: message.label })), onSelect, onCancel, initialSelectedId);
139
141
  const list = selector.getMessageList();
142
+ adoptPiModalFrame(selector, { topIndex: 4, bottomIndex: selector.children.length - 1 });
140
143
  return componentPort(selector, data => list.handleInput(data));
141
144
  }
142
145
  export function createPiShellLoginDialog(runtime, providerId, onComplete, providerName, title) {
143
146
  ensureTheme();
144
147
  const dialog = new LoginDialogComponent(createTuiFacade(runtime), providerId, onComplete, providerName, title);
148
+ const preInsetTitle = dialog.children[1];
149
+ const header = adoptPiModalHeader(dialog, 0, 1);
150
+ adoptPiModalFrame(dialog, {
151
+ topIndex: 0,
152
+ bottomIndex: dialog.children.length - 1,
153
+ header,
154
+ preInsetContent: [preInsetTitle],
155
+ });
145
156
  return {
146
157
  ...componentPort(dialog),
147
158
  showAuth: (url, instructions) => dialog.showAuth(url, instructions),
@@ -169,6 +180,11 @@ export function createPiShellEarendilAnnouncement() {
169
180
  export function createPiShellOperationLoader(runtime, message) {
170
181
  ensureTheme();
171
182
  const loader = new BorderedLoader(createTuiFacade(runtime), piTheme(), message, { cancellable: true });
183
+ adoptPiModalFrame(loader, {
184
+ topIndex: 0,
185
+ bottomIndex: loader.children.length - 1,
186
+ preInsetContent: loader.children[3] === undefined ? [] : [loader.children[3]],
187
+ });
172
188
  return { ...componentPort(loader), signal: loader.signal };
173
189
  }
174
190
  export function createPiShellReloadBox() {
@@ -177,9 +193,10 @@ export function createPiShellReloadBox() {
177
193
  const borderColor = (text) => piTheme().fg("border", text);
178
194
  container.addChild(new DynamicBorder(borderColor));
179
195
  container.addChild(new Spacer(1));
180
- container.addChild(new Text(piTheme().fg("muted", "Reloading keybindings, extensions, skills, prompts, themes, and context files..."), 1, 0));
196
+ container.addChild(new Text(piTheme().fg("muted", "Reloading keybindings, extensions, skills, prompts, themes, and context files..."), 0, 0));
181
197
  container.addChild(new Spacer(1));
182
198
  container.addChild(new DynamicBorder(borderColor));
199
+ adoptPiModalFrame(container, { topIndex: 0, bottomIndex: container.children.length - 1 });
183
200
  return componentPort(container);
184
201
  }
185
202
  export function createPiShellAuthProviderSelector(mode, providers, onSelect, onCancel, initialSearchInput) {
@@ -194,22 +211,41 @@ export function createPiShellAuthProviderSelector(mode, providers, onSelect, onC
194
211
  if (selected)
195
212
  onSelect(selected.id);
196
213
  }, onCancel, initialSearchInput);
214
+ const preInsetContent = [selector.children[2], selector.children[6]];
215
+ const header = adoptPiModalHeader(selector, 0, 2);
216
+ adoptPiModalFrame(selector, {
217
+ topIndex: 0,
218
+ bottomIndex: selector.children.length - 1,
219
+ header,
220
+ preInsetContent,
221
+ });
197
222
  return componentPort(selector);
198
223
  }
199
224
  export function createPiShellExtensionSelector(title, options, onSelect, onCancel) {
200
225
  ensureTheme();
201
- return componentPort(new ExtensionSelectorComponent(title, [...options], onSelect, onCancel));
226
+ const selector = new ExtensionSelectorComponent(title, [...options], onSelect, onCancel);
227
+ const preInsetContent = [selector.children[2], selector.children[4], selector.children[6]];
228
+ const header = adoptPiModalHeader(selector, 0, 2);
229
+ adoptPiModalFrame(selector, {
230
+ topIndex: 0,
231
+ bottomIndex: selector.children.length - 1,
232
+ header,
233
+ preInsetContent,
234
+ });
235
+ return componentPort(selector);
202
236
  }
203
237
  export function createPiShellThemeSelector(currentTheme, onSelect, onCancel, onPreview) {
204
238
  ensureTheme();
205
239
  const selector = new ThemeSelectorComponent(currentTheme, onSelect, onCancel, onPreview);
206
240
  const list = selector.getSelectList();
241
+ adoptPiModalFrame(selector, { topIndex: 0, bottomIndex: selector.children.length - 1 });
207
242
  return componentPort(selector, data => list.handleInput(data));
208
243
  }
209
244
  export function createPiShellShowImagesSelector(currentValue, onSelect, onCancel) {
210
245
  ensureTheme();
211
246
  const selector = new ShowImagesSelectorComponent(currentValue, onSelect, onCancel);
212
247
  const list = selector.getSelectList();
248
+ adoptPiModalFrame(selector, { topIndex: 0, bottomIndex: selector.children.length - 1 });
213
249
  return componentPort(selector, data => list.handleInput(data));
214
250
  }
215
251
  export function createPiShellDialog(dialog, handlers = {}) {
@@ -80,11 +80,19 @@ export interface PiShellViewComponentPort extends PiShellComponentPort {
80
80
  update(view: OwnedUiSessionViewModel): void;
81
81
  }
82
82
  export type PiShellStatusPlacement = "live" | "dock" | "hidden";
83
+ export type PiShellProgressPresentationMode = "pinned" | "custom-viewport";
84
+ export interface PiShellProgressStatusPresentation {
85
+ readonly text: (message: string, mode: PiShellProgressPresentationMode) => string;
86
+ readonly frame: (message: string, phase: number, styles: {
87
+ readonly muted: (text: string) => string;
88
+ readonly accent: (text: string) => string;
89
+ }) => string;
90
+ }
83
91
  export interface PiShellStatusPort extends PiShellViewComponentPort {
84
92
  setWorkingOverride(message: string | undefined): void;
85
93
  setOutputPad(padding: 0 | 1): void;
86
94
  /** Bare A1 shows engine-measured progress beside the working word; the pinned route keeps the bare word. */
87
- setProgressPresentation(presentation: "pinned" | "custom-viewport"): void;
95
+ setProgressPresentation(presentation: PiShellProgressPresentationMode): void;
88
96
  /** Semantic row placement; callers must not inspect rendered text. */
89
97
  placement(): PiShellStatusPlacement;
90
98
  renderDock(width: number): readonly string[];
@@ -1,5 +1,6 @@
1
1
  import { DynamicBorder } from "../startup-public.js";
2
2
  import { Container, getKeybindings, Input, Spacer, Text } from "@earendil-works/pi-tui";
3
+ import { addPiModalHeader, adoptPiModalFrame } from "./modal-frame.js";
3
4
  import { PINNED_PI_LAYOUT, piTheme, renderPiModalShortcutHints } from "./theme.js";
4
5
  import { componentPort, ensureTheme, piShellTruncateToWidth, piShellVisibleWidth } from "./shell-shared-facade.js";
5
6
  import { SKILL_COMMAND_PREFIX, skillMatchesQuery } from "./skills-command.js";
@@ -36,9 +37,7 @@ class SkillsSelectorComponent extends Container {
36
37
  this.#onSelect = options.onSelect;
37
38
  this.#onCancel = options.onCancel;
38
39
  this.#searchInput.onSubmit = () => this.#selectCurrent();
39
- this.addChild(new DynamicBorder());
40
- this.addChild(new Spacer(1));
41
- this.addChild(new Text(piTheme().fg("accent", piTheme().bold("Skills")), 0, 0));
40
+ const header = addPiModalHeader(this, new DynamicBorder(), new Text(piTheme().fg("accent", piTheme().bold("Skills")), 0, 0));
42
41
  this.addChild(new Spacer(1));
43
42
  this.addChild(this.#searchInput);
44
43
  this.addChild(new Spacer(1));
@@ -51,6 +50,7 @@ class SkillsSelectorComponent extends Container {
51
50
  { key: bindings.getKeys("tui.select.cancel").join("/"), action: "cancel" },
52
51
  ]), 0, 0));
53
52
  this.addChild(new DynamicBorder());
53
+ adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
54
54
  this.#updateList();
55
55
  }
56
56
  // Protocol: focus propagates to the search input so the terminal cursor follows the query.
@@ -1,5 +1,5 @@
1
1
  import { type Component } from "@earendil-works/pi-tui";
2
- import type { OwnedUiTranscriptBlock } from "../../../contracts/owned-ui/index.js";
2
+ import { type OwnedUiTranscriptBlock } from "../../../contracts/owned-ui/index.js";
3
3
  export interface PiShellSubmittedPromptComposer {
4
4
  readonly layout: (width: number, source?: Date | number | null) => {
5
5
  readonly contentWidth: number;
@@ -1,5 +1,6 @@
1
1
  import { getMarkdownTheme } from "../startup-public.js";
2
2
  import { Box, Markdown, Text } from "@earendil-works/pi-tui";
3
+ import { protectPromptChipWrapping } from "../../../contracts/owned-ui/index.js";
3
4
  import { piTheme } from "./theme.js";
4
5
  /** Branch summaries share the block kind but do not participate in prompt navigation. */
5
6
  export function isPiPromptStyleCompaction(block) {
@@ -18,11 +19,13 @@ export function createPiSubmittedPromptComponent(block, composer, heading) {
18
19
  const timestamp = numericTimestamp(block);
19
20
  // Invariant: presentation-only chrome stays outside the summary's Markdown parser and stored text.
20
21
  const header = heading === undefined ? undefined : new Text(heading, 0, 0);
21
- const markdown = new Markdown(block.text, 0, 0, getMarkdownTheme(), { color: content => piTheme().fg("userMessageText", content) }, { preserveOrderedListMarkers: true, preserveBackslashEscapes: true });
22
+ const chipWrapping = block.kind === "user" ? protectPromptChipWrapping(block.text) : undefined;
23
+ const markdown = new Markdown(chipWrapping?.text ?? block.text, 0, 0, getMarkdownTheme(), { color: content => piTheme().fg("userMessageText", content) }, { preserveOrderedListMarkers: true, preserveBackslashEscapes: true });
22
24
  const content = {
23
25
  render(width) {
24
26
  const layout = composer.layout(width, timestamp);
25
- const bodyRows = markdown.render(layout.contentWidth);
27
+ const renderedBodyRows = markdown.render(layout.contentWidth);
28
+ const bodyRows = chipWrapping === undefined ? renderedBodyRows : renderedBodyRows.map(row => chipWrapping.restore(row));
26
29
  const rows = header === undefined ? bodyRows : [
27
30
  ...header.render(layout.contentWidth).map(row => piTheme().fg("userMessageText", row)),
28
31
  ...(bodyRows.length === 0 ? [] : ["", ...bodyRows]),
@@ -2,8 +2,8 @@
2
2
  * Provenance: @earendil-works/pi-coding-agent 0.87.1 (MIT), commit f07218c4d4bbc12bef056a7058c3dd49dfe41abe,
3
3
  * packages/coding-agent/src/core/keybindings.ts.
4
4
  * Modifications: Mechanical source port with Node import prefixes, public package-root agent-directory
5
- * resolution, and an opt-in bare-A1 input profile including Ctrl+L level cycling and unbound model
6
- * selection.
5
+ * resolution, and an opt-in bare-A1 input profile including Ctrl+L level cycling, unbound model
6
+ * selection, and cross-platform Alt+Up queued-message restoration.
7
7
  * Deviations: keybindings-public-config-boundary, owned-level-cycle-shortcut,
8
8
  * owned-input-keybinding-aliases.
9
9
  */
@@ -2,8 +2,8 @@
2
2
  * Provenance: @earendil-works/pi-coding-agent 0.87.1 (MIT), commit f07218c4d4bbc12bef056a7058c3dd49dfe41abe,
3
3
  * packages/coding-agent/src/core/keybindings.ts.
4
4
  * Modifications: Mechanical source port with Node import prefixes, public package-root agent-directory
5
- * resolution, and an opt-in bare-A1 input profile including Ctrl+L level cycling and unbound model
6
- * selection.
5
+ * resolution, and an opt-in bare-A1 input profile including Ctrl+L level cycling, unbound model
6
+ * selection, and cross-platform Alt+Up queued-message restoration.
7
7
  * Deviations: keybindings-public-config-boundary, owned-level-cycle-shortcut,
8
8
  * owned-input-keybinding-aliases.
9
9
  */
@@ -184,6 +184,7 @@ const OWNED_INPUT_KEYBINDINGS = {
184
184
  ...KEYBINDINGS,
185
185
  "app.thinking.cycle": { ...KEYBINDINGS["app.thinking.cycle"], defaultKeys: "ctrl+l" },
186
186
  "app.model.select": { ...KEYBINDINGS["app.model.select"], defaultKeys: [] },
187
+ "app.message.dequeue": { ...KEYBINDINGS["app.message.dequeue"], defaultKeys: "alt+up" },
187
188
  "tui.editor.deleteWordBackward": {
188
189
  ...KEYBINDINGS["tui.editor.deleteWordBackward"],
189
190
  defaultKeys: [...KEYBINDINGS["tui.editor.deleteWordBackward"].defaultKeys, "ctrl+backspace"],
@@ -3,7 +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; bare A1 uses the shared semantic modal shortcut row.
6
+ * cancellation, and external-editor lifecycle; bare A1 uses the shared semantic modal shortcut row and
7
+ * compact padded modal frame.
7
8
  * Deviations: owned-modal-shortcut-hints.
8
9
  */
9
10
  import { Container, type TUI } from "@earendil-works/pi-tui";
@@ -3,12 +3,14 @@
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; bare A1 uses the shared semantic modal shortcut row.
6
+ * cancellation, and external-editor lifecycle; bare A1 uses the shared semantic modal shortcut row and
7
+ * compact padded modal frame.
7
8
  * Deviations: owned-modal-shortcut-hints.
8
9
  */
9
10
  import { Container, Editor, Spacer, Text, } from "@earendil-works/pi-tui";
10
11
  import { DynamicBorder, getSelectListTheme } from "../../../startup-public.js";
11
12
  import { editInExternalEditor } from "../external-editor.js";
13
+ import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
12
14
  import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
13
15
  export class ExtensionEditorComponent extends Container {
14
16
  #editor;
@@ -34,12 +36,10 @@ export class ExtensionEditorComponent extends Container {
34
36
  || process.env.EDITOR
35
37
  || (process.platform === "win32" ? "notepad" : "nano");
36
38
  const { description, ...editorOptions } = options ?? {};
37
- this.addChild(new DynamicBorder());
38
- this.addChild(new Spacer(1));
39
- this.addChild(new Text(piTheme().fg("accent", title), 1, 0));
39
+ const header = addPiModalHeader(this, new DynamicBorder(), new Text(piTheme().fg("accent", title), 0, 0));
40
40
  if (description) {
41
41
  this.addChild(new Spacer(1));
42
- this.addChild(new Text(piTheme().fg("text", description), 1, 0));
42
+ this.addChild(new Text(piTheme().fg("text", description), 0, 0));
43
43
  }
44
44
  this.addChild(new Spacer(1));
45
45
  this.#editor = new Editor(tui, {
@@ -57,9 +57,10 @@ export class ExtensionEditorComponent extends Container {
57
57
  { key: this.#keybindings.getKeys("tui.select.cancel").join("/"), action: "cancel" },
58
58
  { key: this.#keybindings.getKeys("app.editor.external").join("/"), action: "external editor" },
59
59
  ]);
60
- this.addChild(new Text(hint, 1, 0));
60
+ this.addChild(new Text(hint, 0, 0));
61
61
  this.addChild(new Spacer(1));
62
62
  this.addChild(new DynamicBorder());
63
+ adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
63
64
  }
64
65
  handleInput(data) {
65
66
  if (this.#keybindings.matches(data, "tui.select.cancel")) {
@@ -2,12 +2,14 @@
2
2
  * Provenance: @earendil-works/pi-coding-agent 0.87.1 (MIT), commit f07218c4d4bbc12bef056a7058c3dd49dfe41abe,
3
3
  * packages/coding-agent/src/modes/interactive/components/extension-input.ts.
4
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.
5
+ * bare-A1 instruction row and chrome through the shared semantic shortcut and compact padded
6
+ * modal-frame components while preserving input, timeout, focus, submission, cancellation, and
7
+ * disposal behavior.
7
8
  * Deviations: owned-modal-shortcut-hints.
8
9
  */
9
10
  import { DynamicBorder } from "../../../startup-public.js";
10
11
  import { Container, getKeybindings, Input, Spacer, Text } from "@earendil-works/pi-tui";
12
+ import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
11
13
  import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
12
14
  import { CountdownTimer } from "./countdown-timer.js";
13
15
  /** Bare-A1 extension text input using the common modal shortcut row. */
@@ -30,13 +32,11 @@ export class ExtensionInputComponent extends Container {
30
32
  this.#onCancel = onCancel;
31
33
  this.#baseTitle = title;
32
34
  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);
35
+ this.#titleText = new Text(theme.fg("accent", title), 0, 0);
36
+ const header = addPiModalHeader(this, new DynamicBorder(), this.#titleText);
37
37
  if (opts?.description) {
38
38
  this.addChild(new Spacer(1));
39
- this.addChild(new Text(theme.fg("text", opts.description), 1, 0));
39
+ this.addChild(new Text(theme.fg("text", opts.description), 0, 0));
40
40
  }
41
41
  this.addChild(new Spacer(1));
42
42
  this.#countdown = opts?.timeout !== undefined && opts.timeout > 0 && opts.tui !== undefined
@@ -53,9 +53,10 @@ export class ExtensionInputComponent extends Container {
53
53
  this.addChild(new Text(renderPiModalShortcutHints([
54
54
  { key: keys.getKeys("tui.select.confirm").join("/"), action: "submit" },
55
55
  { key: keys.getKeys("tui.select.cancel").join("/"), action: "cancel" },
56
- ]), 1, 0));
56
+ ]), 0, 0));
57
57
  this.addChild(new Spacer(1));
58
58
  this.addChild(new DynamicBorder());
59
+ adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
59
60
  }
60
61
  handleInput(keyData) {
61
62
  const keys = getKeybindings();
@@ -2,12 +2,14 @@
2
2
  * Provenance: @earendil-works/pi-coding-agent 0.87.1 (MIT), commit f07218c4d4bbc12bef056a7058c3dd49dfe41abe,
3
3
  * packages/coding-agent/src/modes/interactive/components/extension-selector.ts.
4
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 options,
6
- * timeout, navigation, selection, cancellation, and disposal behavior.
5
+ * bare-A1 instruction row and chrome through the shared semantic shortcut and compact padded
6
+ * modal-frame components while preserving options, timeout, navigation, selection, cancellation, and
7
+ * disposal behavior.
7
8
  * Deviations: owned-modal-shortcut-hints.
8
9
  */
9
10
  import { DynamicBorder } from "../../../startup-public.js";
10
11
  import { Container, getKeybindings, Spacer, Text } from "@earendil-works/pi-tui";
12
+ import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
11
13
  import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
12
14
  import { CountdownTimer } from "./countdown-timer.js";
13
15
  /** Bare-A1 extension selector using the common modal shortcut row. */
@@ -29,13 +31,11 @@ export class ExtensionSelectorComponent extends Container {
29
31
  this.#baseTitle = title;
30
32
  this.#onToggleToolsExpanded = opts?.onToggleToolsExpanded;
31
33
  const theme = piTheme();
32
- this.addChild(new DynamicBorder());
33
- this.addChild(new Spacer(1));
34
- this.#titleText = new Text(theme.fg("accent", theme.bold(title)), 1, 0);
35
- this.addChild(this.#titleText);
34
+ this.#titleText = new Text(theme.fg("accent", theme.bold(title)), 0, 0);
35
+ const header = addPiModalHeader(this, new DynamicBorder(), this.#titleText);
36
36
  if (opts?.description) {
37
37
  this.addChild(new Spacer(1));
38
- this.addChild(new Text(theme.fg("text", opts.description), 1, 0));
38
+ this.addChild(new Text(theme.fg("text", opts.description), 0, 0));
39
39
  }
40
40
  this.addChild(new Spacer(1));
41
41
  this.#countdown = opts?.timeout !== undefined && opts.timeout > 0 && opts.tui !== undefined
@@ -51,9 +51,10 @@ export class ExtensionSelectorComponent extends Container {
51
51
  { key: "↑↓", action: "navigate" },
52
52
  { key: keys.getKeys("tui.select.confirm").join("/"), action: "select" },
53
53
  { key: keys.getKeys("tui.select.cancel").join("/"), action: "cancel" },
54
- ]), 1, 0));
54
+ ]), 0, 0));
55
55
  this.addChild(new Spacer(1));
56
56
  this.addChild(new DynamicBorder());
57
+ adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
57
58
  this.#updateList();
58
59
  }
59
60
  #updateList() {
@@ -63,7 +64,7 @@ export class ExtensionSelectorComponent extends Container {
63
64
  const option = this.#options[index];
64
65
  this.#listContainer.addChild(new Text(index === this.#selectedIndex
65
66
  ? theme.fg("accent", "→ ") + theme.fg("accent", option)
66
- : ` ${theme.fg("text", option)}`, 1, 0));
67
+ : ` ${theme.fg("text", option)}`, 0, 0));
67
68
  }
68
69
  }
69
70
  handleInput(keyData) {
@@ -5,7 +5,7 @@
5
5
  * search, bulk/provider/reorder actions, dirty state, Ctrl+S persistence, refresh status,
6
6
  * cancellation, and focus while remapping theme and public helper imports; local key labels preserve
7
7
  * pinned platform formatting before layout without changing binding identities, and bare A1 uses the
8
- * shared semantic modal shortcut row.
8
+ * shared semantic modal shortcut row and compact padded modal frame.
9
9
  * Deviations: owned-modal-shortcut-hints.
10
10
  */
11
11
  interface ScopedModel {
@@ -1,6 +1,7 @@
1
1
  import { Container, fuzzyFilter, getKeybindings, Input, Key, matchesKey, Spacer, Text, } from "@earendil-works/pi-tui";
2
2
  import { DynamicBorder } from "../../../startup-public.js";
3
3
  import { getModelSearchText } from "../model-search.js";
4
+ import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
4
5
  import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
5
6
  // Rationale: upstream's capitalized hint formatter is private to the package; the same mapping lives here.
6
7
  function keyDisplayText(keybinding) {
@@ -110,9 +111,7 @@ export class ScopedModelsSelectorComponent extends Container {
110
111
  this.enabledIds = config.enabledModelIds === null ? null : [...config.enabledModelIds];
111
112
  this.filteredItems = this.buildItems();
112
113
  // Header
113
- this.addChild(new DynamicBorder());
114
- this.addChild(new Spacer(1));
115
- this.addChild(new Text(theme.fg("accent", theme.bold("Model Configuration")), 0, 0));
114
+ const header = addPiModalHeader(this, new DynamicBorder(), new Text(theme.fg("accent", theme.bold("Model Configuration")), 0, 0));
116
115
  const saveKey = keyDisplayText("app.models.save");
117
116
  this.addChild(new Text(theme.fg("muted", "Session-only.")
118
117
  + (saveKey ? ` ${renderPiModalShortcutHints([{ key: saveKey, action: "to save to settings." }])}` : ""), 0, 0));
@@ -133,6 +132,7 @@ export class ScopedModelsSelectorComponent extends Container {
133
132
  this.footerText = new Text(this.getFooterText(), 0, 0);
134
133
  this.addChild(this.footerText);
135
134
  this.addChild(new DynamicBorder());
135
+ adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
136
136
  this.updateList();
137
137
  }
138
138
  updateModels(models, enabledModelIds) {