@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.
- package/dist/app/session-shell/dock-chrome.js +1 -1
- package/dist/app/session-shell/session-shell-root.js +3 -2
- package/dist/app/session-shell/session-shell.js +4 -1
- package/dist/app/session-shell/session-viewport-controller.d.ts +2 -1
- package/dist/app/session-shell/session-viewport-controller.js +26 -37
- package/dist/app/session-shell/shell-editor-factory.js +4 -1
- package/dist/composition/owned-ui.js +4 -2
- package/dist/composition/reference-selection-options.d.ts +3 -0
- package/dist/composition/reference-selection-options.js +16 -0
- package/dist/composition/settings-route-host.d.ts +1 -2
- package/dist/composition/settings-route-host.js +19 -13
- package/dist/composition/settings-search-input.js +4 -1
- package/dist/composition/utility-tab.js +4 -2
- package/dist/features/owned-ui/plus-menu-dialog.js +28 -20
- package/dist/features/owned-ui/reference-document-selection.d.ts +30 -0
- package/dist/features/owned-ui/reference-document-selection.js +179 -0
- package/dist/features/owned-ui/reference-screen-app.d.ts +4 -1
- package/dist/features/owned-ui/reference-screen-app.js +39 -8
- package/dist/features/owned-ui/settings-app.d.ts +1 -0
- package/dist/features/owned-ui/settings-app.js +23 -19
- package/dist/integrations/pi/components/owned-editor-ux.js +48 -5
- package/dist/integrations/pi/engine/workflow-runner.js +1 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
- package/dist/native/linux-x64/terminal-host.node +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
- package/dist/native/win32-x64/terminal-host.node +0 -0
- package/dist/ui/components/dialog-panel.d.ts +17 -8
- package/dist/ui/components/dialog-panel.js +97 -26
- package/dist/ui/components/index.d.ts +3 -3
- package/dist/ui/components/index.js +3 -3
- package/dist/ui/components/scrollbar.d.ts +1 -1
- package/dist/ui/components/scrollbar.js +1 -1
- package/dist/ui/components/selection-copy.d.ts +2 -4
- package/dist/ui/components/selection-copy.js +4 -2
- package/dist/ui/components/text-selection.d.ts +1 -2
- package/dist/ui/components/text-selection.js +5 -2
- package/dist/ui/components/transcript-viewport.js +3 -3
- package/docs/architecture/resident-tabs.md +5 -4
- 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,
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 = {
|
|
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:
|
|
155
|
-
//
|
|
156
|
-
const dividerRows = this.#search.input === null
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
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 (
|
|
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, "
|
|
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-
|
|
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-
|
|
12
|
+
"builtAt": "2026-10-28T17:59:01.060Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "5f1a93ca9a050c2769a4e322f4d765b00f2ccc9451bcde7ac978b573ee544bef",
|
|
16
|
+
"size": 6019328
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-10-
|
|
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-
|
|
12
|
+
"builtAt": "2026-10-28T18:01:29.016Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "5b2b2de17a50a371271c32546cc3c89bc6b16a96c39a534174fe5bb4ca1d60d6",
|
|
16
|
+
"size": 6562120
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "linux",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-10-
|
|
10
|
+
"builtAt": "2026-10-28T17:55:36.629Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
13
|
"sha256": "118e5954e3864672bd8482ab7edf419cb27dcdf2533720316979b7f5cc5a53a5",
|