@timurproko/a1 0.1.8-dev.572 → 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 (49) hide show
  1. package/dist/app/session-shell/prompt-chips.js +20 -28
  2. package/dist/app/session-shell/session-shell-root.d.ts +2 -0
  3. package/dist/app/session-shell/session-shell-root.js +54 -14
  4. package/dist/app/session-shell/session-shell.js +30 -2
  5. package/dist/app/session-shell/session-viewport-controller.d.ts +1 -0
  6. package/dist/app/session-shell/session-viewport-controller.js +5 -1
  7. package/dist/contracts/owned-ui/index.d.ts +2 -0
  8. package/dist/contracts/owned-ui/index.js +1 -0
  9. package/dist/contracts/owned-ui/prompt-chips.d.ts +15 -0
  10. package/dist/contracts/owned-ui/prompt-chips.js +59 -0
  11. package/dist/contracts/owned-ui/validation.js +1 -1
  12. package/dist/integrations/pi/components/index.d.ts +1 -0
  13. package/dist/integrations/pi/components/index.js +1 -0
  14. package/dist/integrations/pi/components/modal-frame.d.ts +41 -0
  15. package/dist/integrations/pi/components/modal-frame.js +110 -0
  16. package/dist/integrations/pi/components/models-dialog.js +19 -5
  17. package/dist/integrations/pi/components/shell-selectors-dialogs.js +38 -2
  18. package/dist/integrations/pi/components/skills-dialog.js +3 -3
  19. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
  20. package/dist/integrations/pi/components/submitted-prompt-adapter.js +5 -2
  21. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -1
  22. package/dist/integrations/pi/components/upstream/components/extension-editor.js +7 -6
  23. package/dist/integrations/pi/components/upstream/components/extension-input.js +9 -8
  24. package/dist/integrations/pi/components/upstream/components/extension-selector.js +10 -9
  25. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  26. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -3
  27. package/dist/integrations/pi/components/upstream/components/session-selector.js +13 -6
  28. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
  29. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +15 -12
  30. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  31. package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -6
  32. package/dist/integrations/pi/components/upstream/components/trust-selector.js +9 -9
  33. package/dist/integrations/pi/engine/compaction-progress.d.ts +2 -6
  34. package/dist/integrations/pi/engine/compaction-progress.js +10 -10
  35. package/dist/integrations/pi/engine/settings-bridge.js +1 -1
  36. package/dist/integrations/pi/engine/settings-port.js +1 -1
  37. package/dist/integrations/pi/tui-runtime/adapter.js +6 -2
  38. package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -0
  39. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +6 -0
  40. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +34 -0
  41. package/dist/native/darwin-arm64/manifest.json +1 -1
  42. package/dist/native/linux-x64/manifest.json +1 -1
  43. package/dist/native/win32-x64/manifest.json +2 -2
  44. package/dist/native/win32-x64/process-guardian.exe +0 -0
  45. package/dist/ui/components/dialog-panel.js +10 -6
  46. package/dist/ui/components/mouse.d.ts +2 -2
  47. package/dist/ui/components/mouse.js +2 -2
  48. package/dist/ui/components/transcript-viewport.js +35 -13
  49. package/package.json +1 -1
@@ -0,0 +1,110 @@
1
+ import { Container, Spacer } from "@earendil-works/pi-tui";
2
+ export const PI_MODAL_CONTENT_PADDING_X = 1;
3
+ /** The semantic top rule/title pair used while a modal assembles its frame. */
4
+ export class PiModalHeader {
5
+ rule;
6
+ title;
7
+ constructor(rule, title) {
8
+ this.rule = rule;
9
+ this.title = title;
10
+ }
11
+ invalidate() {
12
+ this.rule.invalidate();
13
+ this.title.invalidate();
14
+ }
15
+ render(width) {
16
+ return [...this.rule.render(width), ...this.title.render(width)];
17
+ }
18
+ }
19
+ /**
20
+ * Complete framed-modal chrome. Rules remain frame-wide; every semantic content component renders
21
+ * in the same one-cell-smaller rectangle and receives the same left inset.
22
+ */
23
+ export class PiModalFrame {
24
+ #topRule;
25
+ #content;
26
+ #bottomRule;
27
+ #fullWidthContent;
28
+ #preInsetContent;
29
+ constructor(topRule, content, bottomRule, fullWidthContent = [], preInsetContent = []) {
30
+ this.#topRule = topRule;
31
+ this.#content = content;
32
+ this.#bottomRule = bottomRule;
33
+ this.#fullWidthContent = new Set(fullWidthContent);
34
+ this.#preInsetContent = new Set(preInsetContent);
35
+ }
36
+ invalidate() {
37
+ this.#topRule.invalidate();
38
+ for (const component of this.#content)
39
+ component.invalidate();
40
+ this.#bottomRule.invalidate();
41
+ }
42
+ render(width) {
43
+ const padding = Math.min(PI_MODAL_CONTENT_PADDING_X, Math.max(0, width));
44
+ const contentWidth = Math.max(0, width - padding);
45
+ const rows = [...this.#topRule.render(width)];
46
+ for (const component of this.#content) {
47
+ if (this.#fullWidthContent.has(component) || this.#preInsetContent.has(component)) {
48
+ rows.push(...component.render(width));
49
+ }
50
+ else if (component instanceof Spacer) {
51
+ rows.push(...component.render(contentWidth));
52
+ }
53
+ else {
54
+ rows.push(...component.render(contentWidth).map(row => `${" ".repeat(padding)}${row}`));
55
+ }
56
+ }
57
+ rows.push(...this.#bottomRule.render(width));
58
+ return rows;
59
+ }
60
+ }
61
+ /** Add compact titled-modal chrome while a component assembles its semantic body. */
62
+ export function addPiModalHeader(container, rule, title) {
63
+ const header = new PiModalHeader(rule, title);
64
+ container.addChild(header);
65
+ return header;
66
+ }
67
+ /**
68
+ * Replace explicitly identified structural modal children with the shared padded frame. Callers
69
+ * identify semantic rules/components; rendered text is never inspected or rewritten.
70
+ */
71
+ export function adoptPiModalFrame(container, adoption) {
72
+ const { topIndex, bottomIndex, header, fullWidthContent = [], preInsetContent = [] } = adoption;
73
+ if (topIndex < 0 || bottomIndex <= topIndex || bottomIndex >= container.children.length) {
74
+ throw new TypeError("invalid modal frame child positions");
75
+ }
76
+ const top = container.children[topIndex];
77
+ const bottom = container.children[bottomIndex];
78
+ const contentStart = topIndex + 1;
79
+ let topRule = top;
80
+ const content = [];
81
+ if (header !== undefined) {
82
+ if (top !== header)
83
+ throw new TypeError("modal header is not at the declared top position");
84
+ topRule = header.rule;
85
+ content.push(header.title);
86
+ }
87
+ content.push(...container.children.slice(contentStart, bottomIndex));
88
+ const frame = new PiModalFrame(topRule, content, bottom, fullWidthContent, preInsetContent);
89
+ container.children.splice(topIndex, bottomIndex - topIndex + 1, frame);
90
+ return frame;
91
+ }
92
+ /**
93
+ * Adopt a public component's semantic rule/title children at the bare-A1 boundary. Only an optional
94
+ * structural Spacer between those explicitly identified children is accepted; rendered text is
95
+ * never inspected or rewritten. The pinned comparison profile does not call this adapter.
96
+ */
97
+ export function adoptPiModalHeader(container, ruleIndex, titleIndex) {
98
+ if (ruleIndex < 0 || titleIndex <= ruleIndex || titleIndex >= container.children.length) {
99
+ throw new TypeError("invalid modal rule/title child positions");
100
+ }
101
+ const between = container.children.slice(ruleIndex + 1, titleIndex);
102
+ if (between.length > 1 || between.some(child => !(child instanceof Spacer))) {
103
+ throw new TypeError("modal rule and title are not separated by an optional single Spacer");
104
+ }
105
+ const rule = container.children[ruleIndex];
106
+ const title = container.children[titleIndex];
107
+ const header = new PiModalHeader(rule, title);
108
+ container.children.splice(ruleIndex, titleIndex - ruleIndex + 1, header);
109
+ return header;
110
+ }
@@ -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() {
@@ -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 = {}) {
@@ -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]),
@@ -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) {
@@ -4,7 +4,7 @@
4
4
  * Modifications: Source-synchronized session selector port: preserve threaded/current/all scope,
5
5
  * search, sort, named/path filters, rename, delete confirmation, active-session protection, loading
6
6
  * progress, cancellation, focus, and disposal while remapping public helpers, owned keybindings/theme,
7
- * canonical path handling, and the shared bare-A1 modal shortcut row.
7
+ * canonical path handling, and the shared bare-A1 modal shortcut row and compact padded modal frame.
8
8
  * Deviations: owned-modal-shortcut-hints.
9
9
  */
10
10
  import { spawnSync } from "node:child_process";
@@ -14,6 +14,7 @@ import * as os from "node:os";
14
14
  import { Container, getKeybindings, Input, Spacer, Text, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
15
15
  import { KeybindingsManager } from "../adjacent/core/keybindings.js";
16
16
  import { DynamicBorder } from "../../../startup-public.js";
17
+ import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
17
18
  import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
18
19
  const theme = new Proxy({}, {
19
20
  get(_target, property) {
@@ -665,15 +666,21 @@ export class SessionSelectorComponent extends Container {
665
666
  buildBaseLayout(content, options) {
666
667
  this.clear();
667
668
  this.addChild(new Spacer(1));
668
- this.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
669
+ const showHeader = options?.showHeader ?? true;
670
+ const modalHeader = showHeader
671
+ ? addPiModalHeader(this, new DynamicBorder((s) => theme.fg("accent", s)), this.header)
672
+ : undefined;
673
+ if (!showHeader)
674
+ this.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
669
675
  this.addChild(new Spacer(1));
670
- if (options?.showHeader ?? true) {
671
- this.addChild(this.header);
672
- this.addChild(new Spacer(1));
673
- }
674
676
  this.addChild(content);
675
677
  this.addChild(new Spacer(1));
676
678
  this.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
679
+ const frame = { topIndex: 1, bottomIndex: this.children.length - 1 };
680
+ if (modalHeader === undefined)
681
+ adoptPiModalFrame(this, frame);
682
+ else
683
+ adoptPiModalFrame(this, { ...frame, header: modalHeader });
677
684
  }
678
685
  constructor(currentSessionsLoader, allSessionsLoader, onSelect, onCancel, onExit, requestRender, options, currentSessionFilePath) {
679
686
  super();
@@ -3,11 +3,12 @@
3
3
  * packages/coding-agent/src/modes/interactive/components/thinking-selector.ts.
4
4
  * Modifications: Preserve the searchable thinking-level selector, current/default semantics,
5
5
  * selection, save, cancellation, and focus while accepting the active bare-A1 cycle-key label from the
6
- * shell, styling the title with the established bold semantic accent treatment, placing its muted hint
7
- * directly below it, deduplicating levels, and rendering aligned muted descriptions after adjacent
8
- * active and bracketed default markers. All list and border colors use the owned theme and its
9
- * explicit color mode, and the footer uses the shared bare-A1 modal shortcut row. The comparison
10
- * profile retains the public pinned component.
6
+ * shell, styling the title with the established bold semantic accent treatment, placing it directly
7
+ * below the top rule through the shared compact padded modal frame and its muted hint directly below
8
+ * it, deduplicating levels, and rendering aligned muted descriptions after adjacent active and
9
+ * bracketed default markers. All list and border colors use the owned theme and its explicit color
10
+ * mode, and the footer uses the shared bare-A1 modal shortcut row. The comparison profile retains the
11
+ * public pinned component.
11
12
  * Deviations: owned-modal-shortcut-hints, owned-level-cycle-shortcut, owned-thinking-selector-heading.
12
13
  */
13
14
  import { Container, type Focusable, SelectList } from "@earendil-works/pi-tui";
@@ -16,7 +17,7 @@ export type ThinkingSelectorLevel = "off" | "minimal" | "low" | "medium" | "high
16
17
  export declare class OwnedThinkingSelectorComponent extends Container implements Focusable {
17
18
  private searchInput;
18
19
  private selectList;
19
- private selectListChildIndex;
20
+ private selectListContainer;
20
21
  private allItems;
21
22
  private onSelect;
22
23
  private onCancel;