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

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 (65) hide show
  1. package/README.md +8 -6
  2. package/bin/ui.js +1 -0
  3. package/dist/app/session-shell/dock-chrome.js +1 -1
  4. package/dist/app/session-shell/exit-notice.d.ts +2 -2
  5. package/dist/app/session-shell/exit-notice.js +4 -4
  6. package/dist/app/session-shell/session-lifecycle.d.ts +2 -1
  7. package/dist/app/session-shell/session-lifecycle.js +4 -2
  8. package/dist/app/session-shell/session-shell-root.d.ts +2 -0
  9. package/dist/app/session-shell/session-shell-root.js +3 -2
  10. package/dist/app/session-shell/session-shell.js +9 -1
  11. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -1
  12. package/dist/app/session-shell/session-viewport-controller.js +26 -37
  13. package/dist/app/session-shell/shell-editor-factory.js +4 -1
  14. package/dist/cli/dispatch.js +15 -2
  15. package/dist/composition/owned-ui.d.ts +2 -0
  16. package/dist/composition/owned-ui.js +43 -11
  17. package/dist/composition/reference-selection-options.d.ts +3 -0
  18. package/dist/composition/reference-selection-options.js +16 -0
  19. package/dist/composition/settings-route-host.d.ts +1 -2
  20. package/dist/composition/settings-route-host.js +19 -13
  21. package/dist/composition/settings-search-input.js +4 -1
  22. package/dist/composition/utility-tab.js +4 -2
  23. package/dist/features/launch/intent.js +2 -2
  24. package/dist/features/owned-ui/plus-menu-dialog.js +28 -20
  25. package/dist/features/owned-ui/reference-document-selection.d.ts +30 -0
  26. package/dist/features/owned-ui/reference-document-selection.js +179 -0
  27. package/dist/features/owned-ui/reference-screen-app.d.ts +4 -1
  28. package/dist/features/owned-ui/reference-screen-app.js +39 -8
  29. package/dist/features/owned-ui/settings-app.d.ts +1 -0
  30. package/dist/features/owned-ui/settings-app.js +23 -19
  31. package/dist/foundation/release/automatic-update.js +90 -22
  32. package/dist/foundation/release/bootstrap.js +2 -2
  33. package/dist/foundation/release/latest-release.d.ts +5 -1
  34. package/dist/foundation/release/latest-release.js +74 -31
  35. package/dist/integrations/pi/components/owned-editor-ux.js +48 -5
  36. package/dist/integrations/pi/engine/workflow-runner.js +1 -1
  37. package/dist/native/darwin-arm64/manifest.json +1 -1
  38. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  39. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  40. package/dist/native/darwin-x64/manifest.json +1 -1
  41. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  42. package/dist/native/darwin-x64/terminal-host.node +0 -0
  43. package/dist/native/linux-x64/manifest.json +1 -1
  44. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  45. package/dist/native/linux-x64/terminal-host.node +0 -0
  46. package/dist/native/win32-x64/manifest.json +2 -2
  47. package/dist/native/win32-x64/process-guardian.node +0 -0
  48. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  49. package/dist/native/win32-x64/terminal-host.node +0 -0
  50. package/dist/ui/components/dialog-panel.d.ts +17 -8
  51. package/dist/ui/components/dialog-panel.js +97 -26
  52. package/dist/ui/components/index.d.ts +3 -3
  53. package/dist/ui/components/index.js +3 -3
  54. package/dist/ui/components/scrollbar.d.ts +1 -1
  55. package/dist/ui/components/scrollbar.js +1 -1
  56. package/dist/ui/components/selection-copy.d.ts +2 -4
  57. package/dist/ui/components/selection-copy.js +4 -2
  58. package/dist/ui/components/text-selection.d.ts +1 -2
  59. package/dist/ui/components/text-selection.js +5 -2
  60. package/dist/ui/components/transcript-viewport.js +3 -3
  61. package/dist/ui/settings/declarations.d.ts +1 -1
  62. package/dist/ui/settings/declarations.js +1 -1
  63. package/docs/architecture/resident-tabs.md +5 -4
  64. package/docs/features/launch-profiles.md +7 -4
  65. package/package.json +3 -3
@@ -18,7 +18,7 @@ export function createOwnedRouteHost(settings, references, options = {}) {
18
18
  const title = session ? "Session Info" : changelog ? input ? "What's New" : "Changelog" : HOTKEYS_TITLE;
19
19
  const document = session ? references.session() : changelog ? references.changelog(input) : references.hotkeys();
20
20
  const pinned = options.tabRoute === route;
21
- return deferredSurface(route, title, () => loadReferenceSurface(settings, route, title, document, pinned));
21
+ return deferredSurface(route, title, () => loadReferenceSurface(settings, route, title, document, pinned, options));
22
22
  },
23
23
  };
24
24
  }
@@ -63,9 +63,10 @@ function deferredSurface(id, label, load) {
63
63
  };
64
64
  }
65
65
  async function loadSettingsSurface(settings, options) {
66
- const [{ SettingsApp }, { createSettingsSearchInputFactory }] = await Promise.all([
66
+ const [{ SettingsApp }, { createSettingsSearchInputFactory }, { renderFloatingDialogSurface, renderFloatingMenuSurface }] = await Promise.all([
67
67
  import("../features/owned-ui/settings-app.js"),
68
68
  import("./settings-search-input.js"),
69
+ import("../ui/components/dialog-panel.js"),
69
70
  ]);
70
71
  const pinned = options.tabRoute === SETTINGS_ROUTE;
71
72
  const searchInputFactory = createSettingsSearchInputFactory({
@@ -78,14 +79,19 @@ async function loadSettingsSurface(settings, options) {
78
79
  pinned,
79
80
  searchInputFactory,
80
81
  configurePlusMenu: options.configurePlusMenu ?? null,
82
+ dialogPanel: text => renderFloatingDialogSurface(text, piTheme().getColorMode()),
81
83
  ...(options.copyText === undefined ? {} : { copyText: options.copyText }),
82
- }), !pinned, true);
84
+ }), !pinned, true, text => renderFloatingMenuSurface(text, piTheme().getColorMode()));
83
85
  }
84
- async function loadReferenceSurface(settings, route, title, document, pinned) {
86
+ async function loadReferenceSurface(settings, route, title, document, pinned, options) {
85
87
  const id = route;
86
- // Rationale: the app module and the document load together, so a document that fails
87
- // to render is reported by the placeholder rather than by a screen that never fills.
88
- const [{ ReferenceScreenApp }, content] = await Promise.all([import("../features/owned-ui/reference-screen-app.js"), document]);
88
+ const [{ ReferenceScreenApp }, content, selection] = await Promise.all([
89
+ import("../features/owned-ui/reference-screen-app.js"),
90
+ document,
91
+ route === CHANGELOG_ROUTE
92
+ ? import("./reference-selection-options.js").then(module => module.createReferenceSelectionOptions(settings.file, options.copyText))
93
+ : undefined,
94
+ ]);
89
95
  return hostApp(id, route, () => new ReferenceScreenApp({
90
96
  id,
91
97
  title,
@@ -96,9 +102,10 @@ async function loadReferenceSurface(settings, route, title, document, pinned) {
96
102
  scrollbarSpeed: settings.value("scrollbarSpeed"),
97
103
  }),
98
104
  pinned,
99
- }), !pinned);
105
+ ...(selection === undefined ? {} : { selection }),
106
+ }), !pinned, selection !== undefined);
100
107
  }
101
- async function hostApp(id, route, create, closeOnInterrupt = true, appInputBeforeInterrupt = false) {
108
+ async function hostApp(id, route, create, closeOnInterrupt = true, appInputBeforeInterrupt = false, panel = text => text) {
102
109
  const [{ UiAppHost }, { UiAppRegistry }] = await Promise.all([
103
110
  import("../ui/apps/host.js"),
104
111
  import("../ui/apps/registry.js"),
@@ -114,7 +121,7 @@ async function hostApp(id, route, create, closeOnInterrupt = true, appInputBefor
114
121
  registry,
115
122
  closeOnInterrupt,
116
123
  appInputBeforeInterrupt,
117
- theme: pinnedTheme(),
124
+ theme: pinnedTheme(panel),
118
125
  surface: {
119
126
  size: () => size,
120
127
  requestRender: () => onRender(),
@@ -143,8 +150,7 @@ async function hostApp(id, route, create, closeOnInterrupt = true, appInputBefor
143
150
  onExitRequested: listener => { onExit = listener; },
144
151
  };
145
152
  }
146
- /** Maps A1 UI tokens onto the pinned Pi theme so owned screens match the shell. */
147
- function pinnedTheme() {
153
+ function pinnedTheme(panel) {
148
154
  return {
149
155
  fg: (token, text) => piTheme().fg(token, text),
150
156
  bold: (text) => piTheme().bold(text),
@@ -152,6 +158,6 @@ function pinnedTheme() {
152
158
  accentPreview: (color, text) => renderPiAccentPreview(color, text),
153
159
  disabled: (text) => faint(piTheme().fg("dim", text)),
154
160
  highlight: (text) => piTheme().bg("selectedBg", text),
155
- panel: (text) => `\u001b[48;2;55;55;55m${text}\u001b[49m`,
161
+ panel,
156
162
  };
157
163
  }
@@ -3,6 +3,7 @@ import { createPiShellEditor } from "../integrations/pi/components/shell-editor-
3
3
  import { piShellTruncateToWidth, piShellVisibleWidth, } from "../integrations/pi/components/shell-shared-facade.js";
4
4
  import { backgroundSgrSpan } from "../ui/components/spans.js";
5
5
  import { caretCell } from "../ui/components/line-input.js";
6
+ import { paintTextSelection } from "../ui/components/text-selection.js";
6
7
  import { PromptInput } from "../ui/components/prompt-input.js";
7
8
  /** Builds Settings search from the same configurable owned editor used by the agent prompt. */
8
9
  export function createSettingsSearchInputFactory(options = {}) {
@@ -32,7 +33,9 @@ function createSettingsSearchInput(surface, options) {
32
33
  requestRender: surface.requestRender,
33
34
  onSubmit: () => { },
34
35
  onCopyText: text => surface.copyText(text),
35
- paintEditorSelection: (line, from, to, atomic) => backgroundSgrSpan(line, from, to, atomic ? "\u001b[7m" : "\u001b[27m\u001b[48;2;38;79;120m", atomic ? "\u001b[27m" : "\u001b[49m", piShellVisibleWidth),
36
+ paintEditorSelection: (line, from, to, atomic) => atomic
37
+ ? backgroundSgrSpan(line, from, to, "\u001b[7m", "\u001b[27m", piShellVisibleWidth)
38
+ : paintTextSelection(line, from, to, piShellVisibleWidth, true),
36
39
  promptPresentation: {
37
40
  input: new PromptInput({ fg: (token, text) => surface.theme.fg(token, text) }, {
38
41
  measure: piShellVisibleWidth,
@@ -73,8 +73,10 @@ function createUtilityRouteHost(options, settings, keybindings, cwd, agentDir) {
73
73
  agentDir,
74
74
  cwd,
75
75
  copyText: text => {
76
- if (Buffer.byteLength(text) <= MAX_CLIPBOARD_TEXT_BYTES)
77
- void writeSystemClipboardText(text).catch(() => { });
76
+ if (Buffer.byteLength(text) > MAX_CLIPBOARD_TEXT_BYTES)
77
+ return false;
78
+ void writeSystemClipboardText(text).catch(() => { });
79
+ return true;
78
80
  },
79
81
  }));
80
82
  }
@@ -1,8 +1,8 @@
1
1
  import { sessionSelectionArguments } from "../../foundation/lifecycle/index.js";
2
2
  export { parseSessionSelection } from "../../foundation/lifecycle/index.js";
3
3
  export function interactiveLaunchIntent(profileId, sessionSelection, launchMode = profileId === "a1" ? "tabs" : "solo") {
4
- if (sessionSelection !== undefined && (profileId !== "a1" || launchMode === "solo")) {
5
- throw new Error("session selection requires the normal tabbed A1 launch");
4
+ if (sessionSelection !== undefined && profileId !== "a1") {
5
+ throw new Error("session selection requires the A1 profile");
6
6
  }
7
7
  if (profileId === "pi" && launchMode !== "solo")
8
8
  throw new Error("the Pi comparison launch is always solo");
@@ -1,9 +1,15 @@
1
- import { LineInput, displayWidth, handleLineInputKey, renderInputRow, truncateToWidth, } from "../../ui/components/index.js";
1
+ import { LineInput, handleLineInputKey, padToWidth, renderInputRow, renderShortcutHintsWithClose, truncateToWidth, } from "../../ui/components/index.js";
2
2
  /** The A1 setting holding the tab kinds the strip's `+` offers. */
3
3
  export const PLUS_MENU_SETTING_ID = "tabBarPlusMenu";
4
4
  const TITLE = "Plus Menu";
5
5
  const DESCRIPTION = "Choose what the + button in the tab bar can add.";
6
- const HINT = "Type search · ↑↓ navigate · Space toggle · Esc close";
6
+ const HINTS = [
7
+ { key: "Type", action: "search" },
8
+ { key: "↑↓", action: "navigate" },
9
+ { key: "Space", action: "toggle" },
10
+ { key: "esc", action: "close" },
11
+ ];
12
+ const PADDING_X = 2;
7
13
  /**
8
14
  * The plus-menu dialog: every tab kind with `●` when `+` offers it and `○` when not, filtered by typed
9
15
  * text. It owns only its filter and the row in hand; the enabled record and its saving stay with the
@@ -41,36 +47,38 @@ export class PlusMenuDialog {
41
47
  return "handled";
42
48
  }
43
49
  lines(enabled, width, theme) {
44
- const rule = theme.fg("border", "─".repeat(Math.max(0, width)));
45
- const inset = width > 0 ? " " : "";
46
- const contentWidth = Math.max(0, width - inset.length);
47
- const row = (painted, raw) => {
48
- const clipped = truncateToWidth(painted, contentWidth);
49
- return `${inset}${clipped}${" ".repeat(Math.max(0, contentWidth - displayWidth(raw)))}`;
50
+ const frameWidth = Math.max(0, width);
51
+ const left = Math.min(PADDING_X, frameWidth);
52
+ const right = Math.min(PADDING_X, frameWidth - left);
53
+ const contentWidth = Math.max(0, frameWidth - left - right);
54
+ const blank = " ".repeat(frameWidth);
55
+ const row = (painted, selected = false) => {
56
+ const content = padToWidth(truncateToWidth(painted, contentWidth), contentWidth);
57
+ const rightPadding = " ".repeat(right);
58
+ return `${" ".repeat(left)}${selected ? theme.highlight(content) : content}${selected ? theme.panel(rightPadding) : rightPadding}`;
50
59
  };
51
60
  const shown = this.shown();
52
61
  const index = Math.min(this.#index, Math.max(0, shown.length - 1));
53
62
  const rows = shown.length === 0
54
- ? [row(theme.fg("dim", "No tabs match"), "No tabs match")]
63
+ ? [row(theme.fg("dim", "No tabs match"))]
55
64
  : shown.map((kind, position) => {
56
65
  const selected = position === index;
57
66
  const cursor = selected ? "→ " : " ";
58
67
  const marker = enabled[kind.key] === true ? "●" : "○";
59
- const raw = `${cursor}${marker} ${kind.label}`;
60
68
  const content = `${selected ? theme.fg("accent", cursor) : cursor}${theme.fg("text", marker)} ${theme.fg("text", kind.label)}`;
61
- return row(selected ? theme.highlight(content) : content, raw);
69
+ return row(content, selected);
62
70
  });
63
71
  return [
64
- rule,
65
- row(theme.bold(theme.fg("accent", TITLE)), TITLE),
66
- row(theme.fg("dim", DESCRIPTION), DESCRIPTION),
67
- "",
68
- ...renderInputRow(this.#filter, width, { ruled: false, promptGlyph: "> ", theme }).lines,
69
- "",
72
+ blank,
73
+ row(theme.bold(theme.fg("accent", TITLE))),
74
+ row(theme.fg("dim", DESCRIPTION)),
75
+ blank,
76
+ ...renderInputRow(this.#filter, contentWidth, { ruled: false, promptGlyph: "> ", theme }).lines.map(line => row(line)),
77
+ blank,
70
78
  ...rows,
71
- "",
72
- row(theme.fg("dim", HINT), HINT),
73
- rule,
79
+ blank,
80
+ row(renderShortcutHintsWithClose(HINTS, theme, contentWidth)),
81
+ blank,
74
82
  ];
75
83
  }
76
84
  }
@@ -0,0 +1,30 @@
1
+ import { type PaneMouseEvent, type UiTheme } from "../../ui/components/index.js";
2
+ export interface ReferenceDocumentSelectionOptions {
3
+ readonly copyOnSelect: () => boolean;
4
+ readonly copyText: (text: string) => boolean | void;
5
+ }
6
+ interface ReferenceSelectionFrame {
7
+ readonly bodyRowStart: number;
8
+ readonly contentWidth: number;
9
+ readonly documentRows: readonly string[];
10
+ readonly visibleDocumentRows: readonly (number | undefined)[];
11
+ }
12
+ /** Selection state for an opted-in read-only reference document. */
13
+ export declare class ReferenceDocumentSelection {
14
+ #private;
15
+ constructor(options: ReferenceDocumentSelectionOptions);
16
+ get selecting(): boolean;
17
+ get hasSelection(): boolean;
18
+ updateFrame(frame: ReferenceSelectionFrame): void;
19
+ paintVisibleRow(visibleRow: number, line: string, theme: UiTheme): string;
20
+ handleInput(data: string): {
21
+ readonly consumed: boolean;
22
+ readonly cleared: boolean;
23
+ };
24
+ handleMouse(event: PaneMouseEvent, requestRender?: () => void): {
25
+ readonly consumed: boolean;
26
+ readonly changed: boolean;
27
+ };
28
+ clear(): void;
29
+ }
30
+ export {};
@@ -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;