@timurproko/a1 0.2.7-dev.1221 → 0.2.7-dev.1222

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 (48) hide show
  1. package/dist/app/session-shell/dock-chrome.js +1 -1
  2. package/dist/app/session-shell/session-shell-root.js +3 -2
  3. package/dist/app/session-shell/session-shell.js +4 -1
  4. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -1
  5. package/dist/app/session-shell/session-viewport-controller.js +26 -37
  6. package/dist/app/session-shell/shell-editor-factory.js +4 -1
  7. package/dist/composition/owned-ui.js +4 -2
  8. package/dist/composition/reference-selection-options.d.ts +3 -0
  9. package/dist/composition/reference-selection-options.js +16 -0
  10. package/dist/composition/settings-route-host.d.ts +1 -2
  11. package/dist/composition/settings-route-host.js +19 -13
  12. package/dist/composition/settings-search-input.js +4 -1
  13. package/dist/composition/utility-tab.js +4 -2
  14. package/dist/features/owned-ui/plus-menu-dialog.js +28 -20
  15. package/dist/features/owned-ui/reference-document-selection.d.ts +30 -0
  16. package/dist/features/owned-ui/reference-document-selection.js +179 -0
  17. package/dist/features/owned-ui/reference-screen-app.d.ts +4 -1
  18. package/dist/features/owned-ui/reference-screen-app.js +39 -8
  19. package/dist/features/owned-ui/settings-app.d.ts +1 -0
  20. package/dist/features/owned-ui/settings-app.js +23 -19
  21. package/dist/integrations/pi/components/owned-editor-ux.js +48 -5
  22. package/dist/integrations/pi/engine/workflow-runner.js +1 -1
  23. package/dist/native/darwin-arm64/manifest.json +1 -1
  24. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  25. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  26. package/dist/native/darwin-x64/manifest.json +1 -1
  27. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  28. package/dist/native/darwin-x64/terminal-host.node +0 -0
  29. package/dist/native/linux-x64/manifest.json +1 -1
  30. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  31. package/dist/native/linux-x64/terminal-host.node +0 -0
  32. package/dist/native/win32-x64/manifest.json +2 -2
  33. package/dist/native/win32-x64/process-guardian.node +0 -0
  34. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  35. package/dist/native/win32-x64/terminal-host.node +0 -0
  36. package/dist/ui/components/dialog-panel.d.ts +17 -8
  37. package/dist/ui/components/dialog-panel.js +97 -26
  38. package/dist/ui/components/index.d.ts +3 -3
  39. package/dist/ui/components/index.js +3 -3
  40. package/dist/ui/components/scrollbar.d.ts +1 -1
  41. package/dist/ui/components/scrollbar.js +1 -1
  42. package/dist/ui/components/selection-copy.d.ts +2 -4
  43. package/dist/ui/components/selection-copy.js +4 -2
  44. package/dist/ui/components/text-selection.d.ts +1 -2
  45. package/dist/ui/components/text-selection.js +5 -2
  46. package/dist/ui/components/transcript-viewport.js +3 -3
  47. package/docs/architecture/resident-tabs.md +5 -4
  48. package/package.json +3 -3
@@ -0,0 +1,179 @@
1
+ import { COPY_ACKNOWLEDGEMENT_DURATION_MS, copyAcknowledgementText, displayWidth, extendTextSelection, orderedTextSelection, overlaySpan, paintTextSelection, padToWidth, pressTextSelection, releaseTextSelection, stripAnsi, textSelectionLineExtendColumn, textSelectionPointAt, textSelectionText, truncateToWidth, usefulTextLineContent, } from "../../ui/components/index.js";
2
+ const INTERRUPT = "\u0003";
3
+ class CopyAcknowledgement {
4
+ #message;
5
+ #timer;
6
+ paint(line, finalRow, width, theme) {
7
+ if (!finalRow || this.#message === undefined || width === 0)
8
+ return line;
9
+ const message = truncateToWidth(theme.fg("accent", this.#message), width);
10
+ return overlaySpan(line, width - displayWidth(message), width, message, { inheritStartStyle: true });
11
+ }
12
+ show(characters, requestRender) {
13
+ this.#message = copyAcknowledgementText(characters);
14
+ this.#clearTimer();
15
+ this.#timer = setTimeout(() => {
16
+ this.#timer = undefined;
17
+ this.#message = undefined;
18
+ requestRender();
19
+ }, COPY_ACKNOWLEDGEMENT_DURATION_MS);
20
+ this.#timer.unref?.();
21
+ requestRender();
22
+ }
23
+ clear() {
24
+ this.#clearTimer();
25
+ this.#message = undefined;
26
+ }
27
+ #clearTimer() {
28
+ if (this.#timer !== undefined)
29
+ clearTimeout(this.#timer);
30
+ this.#timer = undefined;
31
+ }
32
+ }
33
+ /** Selection state for an opted-in read-only reference document. */
34
+ export class ReferenceDocumentSelection {
35
+ #copyOnSelect;
36
+ #copyText;
37
+ #selection;
38
+ #click;
39
+ #frame;
40
+ #acknowledgement = new CopyAcknowledgement();
41
+ constructor(options) {
42
+ this.#copyOnSelect = options.copyOnSelect;
43
+ this.#copyText = options.copyText;
44
+ }
45
+ get selecting() {
46
+ return this.#selection?.selecting === true;
47
+ }
48
+ get hasSelection() {
49
+ return orderedTextSelection(this.#selection) !== undefined;
50
+ }
51
+ updateFrame(frame) {
52
+ const previous = this.#frame;
53
+ if (this.#selection !== undefined && previous !== undefined && !selectionStillMaps(this.#selection, previous.documentRows, frame.documentRows)) {
54
+ this.#selection = undefined;
55
+ this.#click = undefined;
56
+ }
57
+ this.#frame = frame;
58
+ }
59
+ paintVisibleRow(visibleRow, line, theme) {
60
+ const frame = this.#frame;
61
+ const documentRow = frame?.visibleDocumentRows[visibleRow];
62
+ const ordered = orderedTextSelection(this.#selection);
63
+ const width = frame?.contentWidth ?? 0;
64
+ let painted = padToWidth(line, width);
65
+ if (documentRow !== undefined && ordered !== undefined
66
+ && documentRow >= ordered.start.line && documentRow <= ordered.end.line) {
67
+ const from = documentRow === ordered.start.line ? ordered.start.column : 0;
68
+ const to = documentRow === ordered.end.line ? ordered.end.column : width;
69
+ painted = paintTextSelection(painted, Math.max(0, from), Math.min(width, to));
70
+ }
71
+ return this.#acknowledgement.paint(painted, visibleRow === (frame?.visibleDocumentRows.length ?? 0) - 1, width, theme);
72
+ }
73
+ handleInput(data) {
74
+ if (data === INTERRUPT && this.hasSelection) {
75
+ this.#submitSelection();
76
+ this.#selection = undefined;
77
+ this.#click = undefined;
78
+ return { consumed: true, cleared: true };
79
+ }
80
+ if (this.#selection === undefined)
81
+ return { consumed: false, cleared: false };
82
+ this.#selection = undefined;
83
+ this.#click = undefined;
84
+ return { consumed: false, cleared: true };
85
+ }
86
+ handleMouse(event, requestRender) {
87
+ const frame = this.#frame;
88
+ if (frame === undefined)
89
+ return { consumed: false, changed: false };
90
+ if (event.kind === "motion" && this.selecting) {
91
+ const row = documentRowAt(frame, event.row, true);
92
+ if (row === undefined)
93
+ return { consumed: true, changed: false };
94
+ this.#extend(row, event.column);
95
+ return { consumed: true, changed: true };
96
+ }
97
+ if (event.kind === "release" && this.selecting) {
98
+ this.#selection = releaseTextSelection(this.#selection);
99
+ const copiedCharacters = this.hasSelection && this.#copyOnSelect() ? this.#submitSelection() : undefined;
100
+ if (copiedCharacters !== undefined && requestRender !== undefined) {
101
+ this.#acknowledgement.show(copiedCharacters, requestRender);
102
+ }
103
+ return { consumed: true, changed: true };
104
+ }
105
+ if (event.kind !== "press" || event.button !== 0)
106
+ return { consumed: false, changed: false };
107
+ const row = documentRowAt(frame, event.row, false);
108
+ if (row === undefined || event.column < 1 || event.column > frame.contentWidth) {
109
+ const changed = this.#selection !== undefined;
110
+ this.#selection = undefined;
111
+ this.#click = undefined;
112
+ return { consumed: false, changed };
113
+ }
114
+ const source = frame.documentRows[row] ?? "";
115
+ const pressed = pressTextSelection({
116
+ line: row,
117
+ column: event.column,
118
+ contentWidth: frame.contentWidth,
119
+ lineText: source,
120
+ lineContent: usefulTextLineContent(source),
121
+ ...(this.#click === undefined ? {} : { previousClick: this.#click }),
122
+ });
123
+ this.#selection = pressed.selection;
124
+ this.#click = pressed.click;
125
+ return { consumed: true, changed: true };
126
+ }
127
+ clear() {
128
+ this.#selection = undefined;
129
+ this.#click = undefined;
130
+ this.#frame = undefined;
131
+ this.#acknowledgement.clear();
132
+ }
133
+ #extend(row, column) {
134
+ const selection = this.#selection;
135
+ const frame = this.#frame;
136
+ if (selection?.selecting !== true || frame === undefined)
137
+ return;
138
+ const targetColumn = selection.fullRow
139
+ ? textSelectionLineExtendColumn(selection, row, frame.contentWidth)
140
+ : Math.max(1, Math.min(column, frame.contentWidth));
141
+ this.#selection = extendTextSelection(selection, selection.fullRow
142
+ ? { line: row, column: targetColumn }
143
+ : textSelectionPointAt(row, targetColumn, frame.documentRows[row] ?? ""));
144
+ }
145
+ #submitSelection() {
146
+ const frame = this.#frame;
147
+ const ordered = orderedTextSelection(this.#selection);
148
+ if (frame === undefined || ordered === undefined)
149
+ return undefined;
150
+ const text = textSelectionText(ordered, frame.documentRows, line => usefulTextLineContent(frame.documentRows[line] ?? "")).trim();
151
+ if (text.length === 0 || this.#copyText(text) === false)
152
+ return undefined;
153
+ return text.length;
154
+ }
155
+ }
156
+ function documentRowAt(frame, screenRow, clampToDocument) {
157
+ const visible = frame.visibleDocumentRows;
158
+ let index = screenRow - frame.bodyRowStart;
159
+ if (!clampToDocument)
160
+ return visible[index];
161
+ index = Math.max(0, Math.min(index, visible.length - 1));
162
+ if (visible[index] !== undefined)
163
+ return visible[index];
164
+ const defined = visible.flatMap(row => row === undefined ? [] : [row]);
165
+ if (defined.length === 0)
166
+ return undefined;
167
+ return screenRow < frame.bodyRowStart ? defined[0] : defined.at(-1);
168
+ }
169
+ function selectionStillMaps(selection, previous, next) {
170
+ const first = Math.min(selection.anchor.line, selection.head.line);
171
+ const last = Math.max(selection.anchor.line, selection.head.line);
172
+ if (last >= previous.length || last >= next.length)
173
+ return false;
174
+ for (let line = first; line <= last; line += 1) {
175
+ if (stripAnsi(previous[line] ?? "") !== stripAnsi(next[line] ?? ""))
176
+ return false;
177
+ }
178
+ return true;
179
+ }
@@ -1,4 +1,5 @@
1
1
  import type { OwnedUiViewportSettings } from "../../contracts/owned-ui/index.js";
2
+ import { type ReferenceDocumentSelectionOptions } from "./reference-document-selection.js";
2
3
  import type { AppHostServices, UiApp } from "../../ui/apps/index.js";
3
4
  import { ShortcutRegistry, type PaneInputResult, type PaneMouseEvent, type PaneRect } from "../../ui/components/index.js";
4
5
  type Action = "up" | "down" | "page-up" | "page-down" | "first" | "last" | "close";
@@ -26,6 +27,8 @@ export interface ReferenceScreenOptions {
26
27
  readonly scrollSettings: () => OwnedUiViewportSettings;
27
28
  /** The screen is a resident tab, such as the Changelog tab: it offers no screen-level close. */
28
29
  readonly pinned?: boolean;
30
+ /** Enables owned text selection for document routes that explicitly opt in. */
31
+ readonly selection?: ReferenceDocumentSelectionOptions;
29
32
  }
30
33
  /**
31
34
  * Presents one read-only document full screen between two accent rules: the bold
@@ -42,6 +45,6 @@ export declare class ReferenceScreenApp implements UiApp {
42
45
  onClose(_host: AppHostServices): void;
43
46
  render(rect: PaneRect, host: AppHostServices): readonly string[];
44
47
  onInput(data: string, host: AppHostServices): PaneInputResult;
45
- onMouse(event: PaneMouseEvent, _host: AppHostServices): PaneInputResult;
48
+ onMouse(event: PaneMouseEvent, host: AppHostServices): PaneInputResult;
46
49
  }
47
50
  export {};
@@ -1,3 +1,4 @@
1
+ import { ReferenceDocumentSelection } from "./reference-document-selection.js";
1
2
  import { DIALOG_CLOSE_SHORTCUT_HINT } from "../../contracts/presentation/index.js";
2
3
  import { GLOBAL_SCOPE, PLAIN_THEME, RAIL_COLUMNS, ShortcutRegistry, ScrollbarRails, assertNoShortcutConflicts, displayWidth, layoutList, padToWidth, renderGroupHeader, renderShortcutHintsWithClose, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, scrollbarWheelRows, truncateToWidth, withScrollbarRail, } from "../../ui/components/index.js";
3
4
  const SCOPE = "reference-screen";
@@ -49,6 +50,7 @@ export class ReferenceScreenApp {
49
50
  #document;
50
51
  #scrollSettings;
51
52
  #pinned;
53
+ #selection;
52
54
  #scroll = 0;
53
55
  #interruptArmed = false;
54
56
  #failure = null;
@@ -58,7 +60,6 @@ export class ReferenceScreenApp {
58
60
  #rails = new ScrollbarRails();
59
61
  // Rationale: the rail as drawn in the last frame, or null when nothing can be pointed at.
60
62
  #railFrame = null;
61
- #bodyHeight = 0;
62
63
  // Invariant: a scroll lights the rail until this time; the timer repaints once it has passed.
63
64
  #activeUntil = 0;
64
65
  #activityTimer;
@@ -69,6 +70,7 @@ export class ReferenceScreenApp {
69
70
  this.#document = options.document;
70
71
  this.#scrollSettings = options.scrollSettings;
71
72
  this.#pinned = options.pinned === true;
73
+ this.#selection = options.selection === undefined ? undefined : new ReferenceDocumentSelection(options.selection);
72
74
  }
73
75
  onActivate(host) {
74
76
  const ready = this.#document.ready;
@@ -85,6 +87,7 @@ export class ReferenceScreenApp {
85
87
  onClose(_host) {
86
88
  this.#clearActivityTimer();
87
89
  this.#rails.clear();
90
+ this.#selection?.clear();
88
91
  }
89
92
  render(rect, host) {
90
93
  const theme = host.theme ?? PLAIN_THEME;
@@ -93,9 +96,9 @@ export class ReferenceScreenApp {
93
96
  const reservesRail = settings.scrollbarAppearance !== "hidden";
94
97
  const contentWidth = reservesRail ? Math.max(0, rect.width - RAIL_COLUMNS) : rect.width;
95
98
  const bodyHeight = Math.max(0, rect.height - HEADER_ROWS - FOOTER_ROWS);
96
- this.#bodyHeight = bodyHeight;
97
99
  const content = this.#content(contentWidth, theme);
98
100
  const body = [];
101
+ const visibleDocumentRows = [];
99
102
  let contentLength;
100
103
  let viewportLength = bodyHeight;
101
104
  if (content.kind === "sections") {
@@ -115,10 +118,22 @@ export class ReferenceScreenApp {
115
118
  for (let index = this.#scroll; index < this.#scroll + bodyHeight; index++) {
116
119
  const row = content.rows[index] ?? "";
117
120
  body.push(displayWidth(row) > contentWidth ? truncateToWidth(row, contentWidth) : row);
121
+ visibleDocumentRows.push(index < content.rows.length ? index : undefined);
118
122
  }
119
123
  }
120
124
  while (body.length < bodyHeight)
121
125
  body.push("");
126
+ while (visibleDocumentRows.length < bodyHeight)
127
+ visibleDocumentRows.push(undefined);
128
+ this.#selection?.updateFrame({
129
+ bodyRowStart: HEADER_ROWS + 1,
130
+ contentWidth,
131
+ documentRows: content.kind === "flat" ? content.rows : [],
132
+ visibleDocumentRows,
133
+ });
134
+ const paintedBody = this.#selection === undefined
135
+ ? body
136
+ : body.map((row, index) => this.#selection.paintVisibleRow(index, row, theme));
122
137
  const now = Date.now();
123
138
  // Rationale: every way of scrolling ends in this frame, so a moved document is noticed here
124
139
  // once rather than at each wheel, drag, and key branch.
@@ -138,7 +153,7 @@ export class ReferenceScreenApp {
138
153
  this.#railFrame = reservesRail && geometry !== null
139
154
  ? { rail: { key: this.id, column: rect.width, rowStart: HEADER_ROWS, trackHeight: geometry.trackHeight }, geometry, page: viewportLength }
140
155
  : null;
141
- const withRail = withScrollbarRail(body.slice(0, bodyHeight), geometry, contentWidth, theme, { presentation });
156
+ const withRail = withScrollbarRail(paintedBody.slice(0, bodyHeight), geometry, contentWidth, theme, { presentation });
142
157
  const hint = this.#interruptArmed
143
158
  ? theme.fg("dim", " press ctrl+c again to exit a1")
144
159
  : renderShortcutHintsWithClose(this.#hintEntries(), theme, rect.width, 1);
@@ -150,11 +165,19 @@ export class ReferenceScreenApp {
150
165
  return frame.slice(0, rect.height).concat(Array(Math.max(0, rect.height - frame.length)).fill(""));
151
166
  }
152
167
  onInput(data, host) {
168
+ const explicitCopy = data === "\u0003" ? this.#selection?.handleInput(data) : undefined;
169
+ if (explicitCopy?.consumed === true)
170
+ return { consumed: true };
153
171
  const action = REFERENCE_SCREEN_SHORTCUTS.resolve(KEYS[data] ?? data, SCOPE);
154
- if (action === null)
172
+ if (action === null) {
173
+ const cleared = explicitCopy?.cleared ?? (this.#selection?.handleInput(data).cleared === true);
174
+ if (cleared)
175
+ host.requestRender();
155
176
  return { consumed: false };
177
+ }
156
178
  switch (action) {
157
179
  case "close":
180
+ this.#selection?.handleInput(data);
158
181
  if (!this.#pinned)
159
182
  host.close();
160
183
  return { consumed: true };
@@ -165,10 +188,10 @@ export class ReferenceScreenApp {
165
188
  this.#scrollBy(1);
166
189
  return { consumed: true };
167
190
  case "page-up":
168
- this.#scrollBy(-Math.max(1, this.#bodyHeight));
191
+ this.#scrollBy(-Math.max(1, host.getSize().height - HEADER_ROWS - FOOTER_ROWS));
169
192
  return { consumed: true };
170
193
  case "page-down":
171
- this.#scrollBy(Math.max(1, this.#bodyHeight));
194
+ this.#scrollBy(Math.max(1, host.getSize().height - HEADER_ROWS - FOOTER_ROWS));
172
195
  return { consumed: true };
173
196
  case "first":
174
197
  this.#scroll = 0;
@@ -178,17 +201,25 @@ export class ReferenceScreenApp {
178
201
  return { consumed: true };
179
202
  }
180
203
  }
181
- onMouse(event, _host) {
204
+ onMouse(event, host) {
182
205
  if (event.kind === "wheel-up" || event.kind === "wheel-down") {
183
206
  // Invariant: the whole screen owns wheel scrolling; it must not depend on the pointer's row.
184
207
  const distance = scrollbarWheelRows(this.#scrollSettings().scrollbarSpeed);
185
208
  this.#scrollBy(event.kind === "wheel-down" ? distance : -distance);
186
209
  return { consumed: true };
187
210
  }
211
+ if (this.#selection?.selecting === true) {
212
+ const selection = this.#selection.handleMouse(event, () => host.requestRender());
213
+ if (selection.consumed)
214
+ return { consumed: true, render: selection.changed };
215
+ }
188
216
  const rail = this.#railPointer(event);
189
217
  if (rail.owned)
190
218
  return { consumed: true };
191
- return { consumed: event.kind !== "motion", render: rail.changed };
219
+ const selection = this.#selection?.handleMouse(event, () => host.requestRender());
220
+ if (selection?.consumed === true)
221
+ return { consumed: true, render: selection.changed || rail.changed };
222
+ return { consumed: event.kind !== "motion", render: selection?.changed === true || rail.changed };
192
223
  }
193
224
  // Rationale: the rail takes its share of a pointer report first: hover, a thumb drag, or a
194
225
  // track page. Owned means the document must not see the report; changed means the rail looks different.
@@ -22,6 +22,7 @@ export declare class SettingsApp implements UiApp {
22
22
  readonly searchInputFactory: SettingsSearchInputFactory;
23
23
  readonly copyText?: (text: string) => void;
24
24
  readonly configurePlusMenu?: (() => boolean) | null;
25
+ readonly dialogPanel?: (text: string) => string;
25
26
  });
26
27
  onActivate(host: AppHostServices): void;
27
28
  onClose(_host: AppHostServices): void;
@@ -1,5 +1,5 @@
1
1
  import { DIALOG_BACK_SHORTCUT_HINT, DIALOG_CLOSE_SHORTCUT_HINT } from "../../contracts/presentation/index.js";
2
- import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, numericValues, RAIL_COLUMNS, renderDialogPanel, renderSteppedDialogPanel, renderEmptyState, renderGroupHeader, renderListRow, renderNote, renderShortcutHintsWithClose, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, formatLocalTime, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
2
+ import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, configureDialogChoiceCapacity, configureDialogTextRows, configureDialogWidth, numericValues, RAIL_COLUMNS, renderCenteredDialogOverlay, renderDialogPanel, renderSteppedDialogPanel, renderEmptyState, renderGroupHeader, renderListRow, renderNote, renderShortcutHintsWithClose, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, formatLocalTime, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
3
3
  import { PLUS_MENU_SETTING_ID, PlusMenuDialog } from "./plus-menu-dialog.js";
4
4
  import { SETTINGS_APP_ID } from "./settings-route.js";
5
5
  export { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
@@ -125,7 +125,11 @@ export class SettingsApp {
125
125
  */
126
126
  constructor(session, options) {
127
127
  this.#session = session;
128
- this.#options = { pinned: options.pinned === true, configurePlusMenu: options.configurePlusMenu ?? null };
128
+ this.#options = {
129
+ pinned: options.pinned === true,
130
+ configurePlusMenu: options.configurePlusMenu ?? null,
131
+ dialogPanel: options.dialogPanel ?? null,
132
+ };
129
133
  this.#search = {
130
134
  input: null,
131
135
  scrollBefore: null,
@@ -148,12 +152,11 @@ export class SettingsApp {
148
152
  }
149
153
  render(rect, host) {
150
154
  const theme = host.theme ?? PLAIN_THEME;
151
- const rows = this.#rows();
152
- const selected = indexOfKey(rows, this.#selectedKey);
155
+ const rows = this.#rows(), selected = indexOfKey(rows, this.#selectedKey);
153
156
  const footer = this.#footerLines(rect.width, theme);
154
- // Invariant: the shared search input and structured panel each begin with their own standard rule.
155
- // That rule replaces the ordinary content/footer divider instead of stacking beneath it.
156
- const dividerRows = this.#search.input === null && this.#structured === null ? SETTINGS_FOOTER_DIVIDER_ROWS : 0;
157
+ // Invariant: search owns its ruled footer. Configure dialogs float over the complete ordinary
158
+ // Settings frame, so the standing divider and guidance remain visible in the dimmed backdrop.
159
+ const dividerRows = this.#search.input === null ? SETTINGS_FOOTER_DIVIDER_ROWS : 0;
157
160
  const topRows = Math.min(SETTINGS_TOP_RULE_ROWS, rect.height);
158
161
  const contentHeight = Math.max(0, rect.height - topRows - dividerRows - footer.length);
159
162
  let titleRows = this.#scroll <= 0 ? Math.min(SETTINGS_TITLE_ROWS, contentHeight) : 0;
@@ -246,7 +249,8 @@ export class SettingsApp {
246
249
  });
247
250
  const rule = theme.fg("border", "─".repeat(Math.max(0, rect.width)));
248
251
  const frame = this.#withMenu([rule, ...withRail, ...(dividerRows === 0 ? [] : [rule]), ...footer], selected, layout, valueColumn, theme, rect, reservesRail ? RAIL_COLUMNS : 0);
249
- return frame.slice(0, rect.height).concat(Array(Math.max(0, rect.height - frame.length)).fill(""));
252
+ const complete = frame.slice(0, rect.height).concat(Array(Math.max(0, rect.height - frame.length)).fill("")), open = this.#structured, dialogTheme = this.#options.dialogPanel === null ? theme : { ...theme, panel: this.#options.dialogPanel };
253
+ return open === null ? complete : renderCenteredDialogOverlay(complete, this.#dialogLines(open, configureDialogWidth(rect.width), rect.height, dialogTheme), rect, dialogTheme);
250
254
  }
251
255
  onInput(data, host) {
252
256
  if (data === "\u0003" && this.#search.input === null)
@@ -983,9 +987,9 @@ export class SettingsApp {
983
987
  this.#menuFrame = frame;
984
988
  return renderValueMenu(lines, state, frame, theme);
985
989
  }
986
- #dialogLines(open, width, theme) {
990
+ #dialogLines(open, width, height, theme) {
987
991
  if (open.modelDialog)
988
- return this.#modelDialogLines(open, width, theme);
992
+ return this.#modelDialogLines(open, width, height, theme);
989
993
  if (open.plusMenu !== null)
990
994
  return open.plusMenu.lines(open.record, width, theme);
991
995
  const rows = open.flags.map(key => {
@@ -997,9 +1001,10 @@ export class SettingsApp {
997
1001
  ...(declared?.description === undefined ? {} : { description: declared.description }),
998
1002
  };
999
1003
  });
1000
- return renderDialogPanel({ title: labelOf(open.entry), rows, index: open.index, hint: SETTINGS_SHORTCUTS.hintEntries(DIALOG_SCOPE) }, width, theme);
1004
+ const visible = dialogWindow(rows, open.index, configureDialogChoiceCapacity(height, false, configureDialogTextRows(rows[open.index]?.description ?? "", width)));
1005
+ return renderDialogPanel({ title: labelOf(open.entry), rows: visible.rows, index: visible.index, hint: SETTINGS_SHORTCUTS.hintEntries(DIALOG_SCOPE) }, width, theme);
1001
1006
  }
1002
- #modelDialogLines(open, width, theme) {
1007
+ #modelDialogLines(open, width, height, theme) {
1003
1008
  if (open.step === "model") {
1004
1009
  const flags = this.#filteredModelFlags(open);
1005
1010
  const stored = structuredValue(open.entry, open.record);
@@ -1009,12 +1014,13 @@ export class SettingsApp {
1009
1014
  ...modelLabelParts(flag.label ?? humanizeLabel(flag.key)),
1010
1015
  value: stored[flag.key] === undefined ? "" : String(stored[flag.key]),
1011
1016
  }));
1012
- const visible = dialogWindow(allRows, open.index, 10);
1017
+ const description = "Select a model to configure";
1018
+ const visible = dialogWindow(allRows, open.index, configureDialogChoiceCapacity(height, true, configureDialogTextRows(description, width)));
1013
1019
  return renderSteppedDialogPanel({
1014
1020
  title: "Thinking Level",
1015
1021
  step: 1,
1016
1022
  steps: 2,
1017
- description: "Select a model to configure",
1023
+ description,
1018
1024
  ...(open.filter === null ? {} : { input: open.filter }),
1019
1025
  rows: visible.rows,
1020
1026
  index: visible.index,
@@ -1027,21 +1033,19 @@ export class SettingsApp {
1027
1033
  label: choice.value === current ? `✓ ${choice.label}` : choice.label,
1028
1034
  value: choice.description,
1029
1035
  }));
1030
- const visible = dialogWindow(allRows, open.index, 10);
1036
+ const description = `Select default thinking level for ${selected?.label ?? open.modelKey ?? "model"}`;
1037
+ const visible = dialogWindow(allRows, open.index, configureDialogChoiceCapacity(height, false, configureDialogTextRows(description, width)));
1031
1038
  return renderSteppedDialogPanel({
1032
1039
  title: "Thinking Level",
1033
1040
  step: 2,
1034
1041
  steps: 2,
1035
- description: `Select default thinking level for ${selected?.label ?? open.modelKey ?? "model"}`,
1042
+ description,
1036
1043
  rows: visible.rows,
1037
1044
  index: visible.index,
1038
1045
  hint: MODEL_SHORTCUTS.hintEntries(MODEL_LEVEL_SCOPE),
1039
1046
  }, width, theme);
1040
1047
  }
1041
1048
  #footerLines(width, theme) {
1042
- const open = this.#structured;
1043
- if (open !== null)
1044
- return this.#dialogLines(open, width, theme);
1045
1049
  // Rationale: standing guidance is more useful than transient feedback; failures still
1046
1050
  // roll back without displacing the Settings interaction contract.
1047
1051
  const entries = SETTINGS_SHORTCUTS.hintEntries(SCOPE).filter(entry => !this.#options.pinned
@@ -67,7 +67,7 @@ class PromptSelectionInterceptor {
67
67
  #lastClick;
68
68
  #redoStack = [];
69
69
  #selectionRevision = 0;
70
- #pasteGeneration = 0;
70
+ #pasteState = { generation: 0, preview: undefined };
71
71
  #terminalPaste;
72
72
  #wordDirection;
73
73
  #geometry;
@@ -81,6 +81,7 @@ class PromptSelectionInterceptor {
81
81
  installAtomicSegmentation(editor, options.atomicRanges, () => this.#wordDirection);
82
82
  }
83
83
  handleInput(data, next) {
84
+ this.#pasteState.preview = undefined;
84
85
  if (this.options.pasteEnabled === false ? discardTerminalPaste(this, data, value => this.handleInput(value, next)) : this.#handleTerminalPaste(data, next))
85
86
  return;
86
87
  if (this.keybindings.matches(data, "owned.editor.selectAll")) {
@@ -195,7 +196,26 @@ class PromptSelectionInterceptor {
195
196
  this.#redoStack = [];
196
197
  }
197
198
  render(width, next) {
199
+ const preview = this.#pasteState.preview;
198
200
  const state = editorState(this.editor);
201
+ if (preview !== undefined && this.editor.getText() === preview.reservedText
202
+ && this.#selectionRevision === preview.revision
203
+ && samePosition(this.#cursor(), preview.reservedCursor)
204
+ && this.#selection === undefined) {
205
+ // Invariant: painting does not commit the reserved edit.
206
+ const original = { lines: [...state.lines], cursor: this.#cursor(), selection: this.#selection };
207
+ this.#pasteState.preview = undefined;
208
+ try {
209
+ this.#setRenderState(preview.lines, preview.cursor);
210
+ this.#selection = preview.selection;
211
+ return this.render(width, next);
212
+ }
213
+ finally {
214
+ this.#setRenderState(original.lines, original.cursor);
215
+ this.#selection = original.selection;
216
+ this.#pasteState.preview = preview;
217
+ }
218
+ }
199
219
  const hidden = state.lines.map(line => this.options.hiddenRanges?.(line) ?? []);
200
220
  if (!hidden.some(ranges => ranges.length > 0))
201
221
  return this.#renderVisible(width, next);
@@ -277,6 +297,7 @@ class PromptSelectionInterceptor {
277
297
  return rows.map((row, index) => this.options.decorateRow(row, width, index));
278
298
  }
279
299
  reset() {
300
+ this.#pasteState.preview = undefined;
280
301
  this.#selection = undefined;
281
302
  this.#pointerSelecting = false;
282
303
  this.#lastClick = undefined;
@@ -285,7 +306,8 @@ class PromptSelectionInterceptor {
285
306
  this.#selectionRevision += 1;
286
307
  }
287
308
  cancelPendingPastes() {
288
- this.#pasteGeneration += 1;
309
+ this.#pasteState.generation += 1;
310
+ this.#pasteState.preview = undefined;
289
311
  this.#terminalPaste = undefined;
290
312
  DISABLED_TERMINAL_PASTES.delete(this);
291
313
  }
@@ -309,6 +331,14 @@ class PromptSelectionInterceptor {
309
331
  handlePointer(event) {
310
332
  if (event.button !== 0)
311
333
  return false;
334
+ if (this.#pasteState.preview !== undefined) {
335
+ this.#pasteState.preview = undefined;
336
+ // Invariant: preview coordinates cannot address a hidden marker.
337
+ this.#geometry = undefined;
338
+ this.#pointerSelecting = event.kind === "press";
339
+ this.#requestRender();
340
+ return true;
341
+ }
312
342
  if (event.kind === "press")
313
343
  return this.#pointerPress(event.column, event.row);
314
344
  if (event.kind === "motion") {
@@ -539,17 +569,30 @@ class PromptSelectionInterceptor {
539
569
  const begin = source === undefined ? this.options.beginClipboardPaste
540
570
  : this.options.beginTextPaste === undefined ? undefined : () => this.options.beginTextPaste(source);
541
571
  if (begin !== undefined) {
542
- const generation = this.#pasteGeneration;
572
+ const generation = this.#pasteState.generation;
543
573
  const restore = this.#orderedSelection() === undefined ? "" : this.#selectedText();
544
574
  const paste = begin();
545
575
  this.#redoStack = [];
546
- if (this.#orderedSelection() !== undefined)
576
+ const selected = this.#orderedSelection();
577
+ if (selected !== undefined) {
578
+ const lines = [...editorState(this.editor).lines];
579
+ const cursor = this.#cursor();
580
+ const selection = this.#selection;
581
+ const current = this.editor.getText();
582
+ const from = positionOffset(lines, selected.start);
583
+ const to = positionOffset(lines, selected.end);
584
+ const reservedText = current.slice(0, from) + normalizeInsertedText(paste.marker) + current.slice(to);
547
585
  this.#replaceSelection(paste.marker);
586
+ this.#pasteState.preview = { marker: paste.marker, reservedText, lines, cursor, selection,
587
+ reservedCursor: this.#cursor(), revision: this.#selectionRevision };
588
+ }
548
589
  else
549
590
  this.editor.insertTextAtCursor(paste.marker);
550
591
  this.#requestRender();
551
592
  const replace = (text) => {
552
- if (generation === this.#pasteGeneration)
593
+ if (this.#pasteState.preview?.marker === paste.marker)
594
+ this.#pasteState.preview = undefined;
595
+ if (generation === this.#pasteState.generation)
553
596
  this.#replacePasteMarker(paste.marker, paste.isCurrent?.() === false ? restore : text || restore);
554
597
  paste.complete?.();
555
598
  };
@@ -443,7 +443,7 @@ export class PiWorkflowRunner {
443
443
  case "copy": {
444
444
  const text = requireCapability(session.getLastAssistantText, "getLastAssistantText").call(session);
445
445
  if (typeof text !== "string" || text.length === 0)
446
- return workflowResult(request.command, "failed", "No agent messages to copy yet.");
446
+ return workflowResult(request.command, "completed", "", undefined, "silent");
447
447
  const acknowledged = await this.copyWorkflowText(text);
448
448
  return workflowResult(request.command, "completed", acknowledged ? "Copied last agent message to clipboard" : "Submitted last agent message to clipboard");
449
449
  }
@@ -7,7 +7,7 @@
7
7
  "platform": "darwin",
8
8
  "architecture": "arm64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-28T07:16:03.705Z",
10
+ "builtAt": "2026-10-28T17:55:49.435Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
13
  "sha256": "a87eaaffcd986b624c8b044b756446cc1bdaf4334a1775b97820542fe34f9fb7",
@@ -9,11 +9,11 @@
9
9
  "architecture": "arm64",
10
10
  "target": "aarch64-apple-darwin",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-28T07:18:47.307Z",
12
+ "builtAt": "2026-10-28T17:59:01.060Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "2c9107be459d3cce4f154eb34fd839b34d784a82b280d9f56896908d1e7cb1bf",
16
- "size": 6001744
15
+ "sha256": "5f1a93ca9a050c2769a4e322f4d765b00f2ccc9451bcde7ac978b573ee544bef",
16
+ "size": 6019328
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,7 +7,7 @@
7
7
  "platform": "darwin",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-28T07:16:10.391Z",
10
+ "builtAt": "2026-10-28T17:55:56.614Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
13
  "sha256": "783a65f0de287a1c02cd5429dbe97bc8633f4cc8a7b92e3e961180b9bdcff814",
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-apple-darwin",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-28T07:20:46.993Z",
12
+ "builtAt": "2026-10-28T18:01:29.016Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "b3dfb167c40179f65bb381bfd701f97bce42ee99bf935ae98c99e4a5bd4c7f01",
16
- "size": 6552896
15
+ "sha256": "5b2b2de17a50a371271c32546cc3c89bc6b16a96c39a534174fe5bb4ca1d60d6",
16
+ "size": 6562120
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,7 +7,7 @@
7
7
  "platform": "linux",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-28T07:15:51.475Z",
10
+ "builtAt": "2026-10-28T17:55:36.629Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
13
  "sha256": "118e5954e3864672bd8482ab7edf419cb27dcdf2533720316979b7f5cc5a53a5",