@timurproko/a1 0.1.8-dev.447 → 0.1.8-dev.459
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/README.md +9 -0
- package/dist/composition/owned-ui.js +12 -0
- package/dist/contracts/owned-ui/model.d.ts +12 -0
- package/dist/features/owned-ui/settings-app.d.ts +1 -0
- package/dist/features/owned-ui/settings-app.js +129 -17
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +1 -0
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +2 -0
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +17 -0
- package/dist/integrations/pi/engine/adapter.d.ts +2 -0
- package/dist/integrations/pi/engine/adapter.js +25 -6
- package/dist/integrations/pi/engine/settings-effects.d.ts +1 -1
- package/dist/integrations/pi/engine/settings-effects.js +1 -1
- package/dist/integrations/pi/session-ui/quit-outro-effects.d.ts +29 -0
- package/dist/integrations/pi/session-ui/quit-outro-effects.js +242 -0
- package/dist/integrations/pi/session-ui/quit-outro.d.ts +38 -0
- package/dist/integrations/pi/session-ui/quit-outro.js +98 -0
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +12 -1
- package/dist/integrations/pi/session-ui/session-shell.js +65 -5
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +10 -2
- package/dist/integrations/pi/tui-runtime/adapter.js +53 -4
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +12 -0
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/components/list-view.js +5 -6
- package/dist/ui/components/surface.d.ts +7 -2
- package/dist/ui/components/surface.js +13 -3
- package/dist/ui/settings/declarations.d.ts +3 -1
- package/dist/ui/settings/declarations.js +33 -1
- package/dist/ui/settings/migrations.js +20 -0
- package/docs/architecture/ui-reference-provenance.md +2 -2
- package/docs/ci-release-runbook.md +5 -5
- package/docs/local-worktree-cleanup.md +40 -9
- package/docs/manual-owned-ui-checkpoint.md +3 -2
- package/docs/openspec-archive-automation.md +22 -9
- package/docs/validation.md +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,6 +71,15 @@ preferred when consistent with your intent; genuinely unclear cases can remain
|
|
|
71
71
|
empty. [Private diagnostic capture](docs/architecture/prompt-suggestions.md)
|
|
72
72
|
explains how to distinguish skipped, empty, rejected, and timed-out suggestions.
|
|
73
73
|
|
|
74
|
+
## Quitting
|
|
75
|
+
|
|
76
|
+
`/quit`, `Ctrl+C` twice, and `Ctrl+D` on an empty prompt end a bare A1 session.
|
|
77
|
+
The last screen dissolves on the alternate screen, the terminal is restored once,
|
|
78
|
+
and only the dim `To resume this session:` hint is printed; the conversation is
|
|
79
|
+
not echoed into your scrollback. `/settings` → Quit chooses the effect (`fall`,
|
|
80
|
+
`dissolve`, `starburst`, `waves`, or `off`) and its duration (300–2000 ms). The
|
|
81
|
+
`a1 pi` comparison profile keeps Pi's `fullscreenExitOutput` behavior.
|
|
82
|
+
|
|
74
83
|
## Prompt history
|
|
75
84
|
|
|
76
85
|
Bare A1 recalls recent unique prompts across sessions with Up/Down. History is
|
|
@@ -84,6 +84,9 @@ export async function composeOwnedUi(options = {}) {
|
|
|
84
84
|
inputPresentation: { onEvent: event => clipboardDiagnostics.runtime(event) },
|
|
85
85
|
}),
|
|
86
86
|
...(viewportSettings === null ? {} : { viewportSettings }),
|
|
87
|
+
...(settings === null || !ownedSurfaces ? {} : {
|
|
88
|
+
quitOutro: { snapshot: () => quitOutroSettingsSnapshot(settings), interactive: process.stdout.isTTY === true },
|
|
89
|
+
}),
|
|
87
90
|
...(promptSuggestions === null ? {} : { promptSuggestions }),
|
|
88
91
|
...(promptHistory === null ? {} : { promptHistory: {
|
|
89
92
|
...promptHistory,
|
|
@@ -113,6 +116,15 @@ export async function composeOwnedUi(options = {}) {
|
|
|
113
116
|
};
|
|
114
117
|
return { application, settings };
|
|
115
118
|
}
|
|
119
|
+
function quitOutroSettingsSnapshot(settings) {
|
|
120
|
+
const effect = settings.value("quitEffect");
|
|
121
|
+
const durationMs = settings.value("quitEffectDurationMs");
|
|
122
|
+
return {
|
|
123
|
+
enabled: settings.value("quitAnimation") !== false,
|
|
124
|
+
effect: effect === "dissolve" || effect === "starburst" || effect === "waves" ? effect : "fall",
|
|
125
|
+
durationMs: typeof durationMs === "number" ? durationMs : 800,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
116
128
|
function viewportSettingsSnapshot(settings) {
|
|
117
129
|
const appearance = settings.value("scrollbarAppearance");
|
|
118
130
|
const style = settings.value("scrollbarStyle");
|
|
@@ -64,6 +64,18 @@ export interface OwnedUiViewportSettingsPort {
|
|
|
64
64
|
snapshot(): OwnedUiViewportSettings;
|
|
65
65
|
onChange(listener: (settings: OwnedUiViewportSettings) => void): () => void;
|
|
66
66
|
}
|
|
67
|
+
export type OwnedUiQuitEffect = "fall" | "dissolve" | "starburst" | "waves";
|
|
68
|
+
/** Profile-local quit outro choice, read at the moment bare A1 quits. */
|
|
69
|
+
export interface OwnedUiQuitOutroSettings {
|
|
70
|
+
/** False leaves the terminal immediately; the effect and duration are then ignored. */
|
|
71
|
+
readonly enabled: boolean;
|
|
72
|
+
readonly effect: OwnedUiQuitEffect;
|
|
73
|
+
/** Requested playback length; the player clamps it to its supported range. */
|
|
74
|
+
readonly durationMs: number;
|
|
75
|
+
}
|
|
76
|
+
export interface OwnedUiQuitOutroSettingsPort {
|
|
77
|
+
snapshot(): OwnedUiQuitOutroSettings;
|
|
78
|
+
}
|
|
67
79
|
export interface OwnedUiTerminalSurface {
|
|
68
80
|
readonly columns: number;
|
|
69
81
|
readonly rows: number;
|
|
@@ -14,6 +14,7 @@ export declare class SettingsApp implements UiApp {
|
|
|
14
14
|
readonly id = "settings";
|
|
15
15
|
constructor(session: OwnedUiSettingsSession);
|
|
16
16
|
onActivate(host: AppHostServices): void;
|
|
17
|
+
onClose(_host: AppHostServices): void;
|
|
17
18
|
render(rect: PaneRect, host: AppHostServices): readonly string[];
|
|
18
19
|
onInput(data: string, host: AppHostServices): PaneInputResult;
|
|
19
20
|
onMouse(event: PaneMouseEvent, _host: AppHostServices): PaneInputResult;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, dialogValueColumn, numericValues, RAIL_COLUMNS, renderDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderListRow, renderNote, renderStatusLine, dialogRowAt, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, isThumbRow, layoutList, moveSelection, rowKey, scrollForSelection, scrollbarGeometry, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
|
|
1
|
+
import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, dialogValueColumn, numericValues, RAIL_COLUMNS, renderDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderListRow, renderNote, renderStatusLine, dialogRowAt, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, isThumbRow, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
|
|
2
2
|
import { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
|
|
3
3
|
export { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
|
|
4
4
|
const SCOPE = SETTINGS_APP_ID;
|
|
5
5
|
/** The panel a setting with parts opens: its own keys, its own hint. */
|
|
6
6
|
const DIALOG_SCOPE = `${SETTINGS_APP_ID}-parts`;
|
|
7
7
|
const SCROLLBAR_TOP_INSET = 1;
|
|
8
|
+
/** Identity of the settings list rail in the shared rail state. */
|
|
9
|
+
const RAIL_KEY = "settings";
|
|
10
|
+
// Compatibility: the transcript rail stays lit this long after a scroll, and repaints just after.
|
|
11
|
+
const SCROLL_LINGER_MS = 900;
|
|
12
|
+
const SCROLL_LINGER_REPAINT_MS = 925;
|
|
8
13
|
const SEARCH_PLACEHOLDER = "search settings";
|
|
9
14
|
/** What a structured value offers instead of printing itself. */
|
|
10
15
|
const CONFIGURE = "configure";
|
|
@@ -16,8 +21,10 @@ SETTINGS_SHORTCUTS.declare({ key: "shift+up", scope: SCOPE, description: "Previo
|
|
|
16
21
|
SETTINGS_SHORTCUTS.declare({ key: "shift+down", scope: SCOPE, description: "Next section", section: "Navigate", hint: { keys: "Shift+↑↓", does: "to jump" } }, "block-down");
|
|
17
22
|
SETTINGS_SHORTCUTS.declare({ key: "pageUp", scope: SCOPE, description: "Up a page", section: "Navigate" }, "page-up");
|
|
18
23
|
SETTINGS_SHORTCUTS.declare({ key: "pageDown", scope: SCOPE, description: "Down a page", section: "Navigate" }, "page-down");
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
// Compatibility: the same chords the transcript uses for its content boundaries; plain
|
|
25
|
+
// Home and End stay with the search input's cursor.
|
|
26
|
+
SETTINGS_SHORTCUTS.declare({ key: "ctrl+home", scope: SCOPE, description: "First setting", section: "Navigate" }, "first");
|
|
27
|
+
SETTINGS_SHORTCUTS.declare({ key: "ctrl+end", scope: SCOPE, description: "Last setting", section: "Navigate" }, "last");
|
|
21
28
|
SETTINGS_SHORTCUTS.declare({ key: "enter", scope: SCOPE, description: "Change value", section: "Change", hint: { keys: "Enter/Space", does: "to change" } }, "activate");
|
|
22
29
|
SETTINGS_SHORTCUTS.declare({ key: "space", scope: SCOPE, description: "Change value", section: "Change", hint: { keys: "Enter/Space", does: "to change" } }, "activate");
|
|
23
30
|
SETTINGS_SHORTCUTS.declare({ key: "left", scope: SCOPE, description: "Previous value", section: "Change", hint: { keys: "←→", does: "to adjust" } }, "previous-value");
|
|
@@ -39,8 +46,11 @@ const KEYS = {
|
|
|
39
46
|
"\u001b[C": "right",
|
|
40
47
|
"\u001b[5~": "pageUp",
|
|
41
48
|
"\u001b[6~": "pageDown",
|
|
42
|
-
|
|
43
|
-
"\u001b[
|
|
49
|
+
// Protocol: xterm modifier form, then the rxvt Ctrl form of the same chords.
|
|
50
|
+
"\u001b[1;5H": "ctrl+home",
|
|
51
|
+
"\u001b[1;5F": "ctrl+end",
|
|
52
|
+
"\u001b[7^": "ctrl+home",
|
|
53
|
+
"\u001b[8^": "ctrl+end",
|
|
44
54
|
"\u001b": "escape",
|
|
45
55
|
"\r": "enter",
|
|
46
56
|
"\n": "enter",
|
|
@@ -76,6 +86,14 @@ export class SettingsApp {
|
|
|
76
86
|
#hoverRegion = "label";
|
|
77
87
|
#frameRows = [];
|
|
78
88
|
#menuFrame = null;
|
|
89
|
+
// Invariant: rail hover and drag live in the shared keyed state, as the transcript's do.
|
|
90
|
+
#rails = new ScrollbarRails();
|
|
91
|
+
// Rationale: the rail as drawn in the last frame, or null when nothing can be pointed at.
|
|
92
|
+
#railFrame = null;
|
|
93
|
+
// Invariant: a scroll lights the rail until this time; the timer repaints once it has passed.
|
|
94
|
+
#activeUntil = 0;
|
|
95
|
+
#activityTimer;
|
|
96
|
+
#renderedScroll;
|
|
79
97
|
constructor(session) {
|
|
80
98
|
this.#session = session;
|
|
81
99
|
}
|
|
@@ -85,6 +103,10 @@ export class SettingsApp {
|
|
|
85
103
|
host.requestRender();
|
|
86
104
|
});
|
|
87
105
|
}
|
|
106
|
+
onClose(_host) {
|
|
107
|
+
this.#clearActivityTimer();
|
|
108
|
+
this.#rails.clear();
|
|
109
|
+
}
|
|
88
110
|
render(rect, host) {
|
|
89
111
|
const theme = host.theme ?? PLAIN_THEME;
|
|
90
112
|
this.#interruptArmed = host.interruptArmed;
|
|
@@ -102,7 +124,17 @@ export class SettingsApp {
|
|
|
102
124
|
this.#reveal = undefined;
|
|
103
125
|
const layout = layoutList(rows, bodyHeight, this.#scroll);
|
|
104
126
|
this.#scroll = layout.scroll;
|
|
105
|
-
const
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
// Rationale: every way of scrolling ends in this frame, so a moved list is noticed here
|
|
129
|
+
// once rather than at each wheel, drag, key, and search branch.
|
|
130
|
+
if (this.#renderedScroll !== undefined && this.#renderedScroll !== layout.scroll)
|
|
131
|
+
this.#noteScrollActivity(host, now);
|
|
132
|
+
this.#renderedScroll = layout.scroll;
|
|
133
|
+
// Invariant: auto and always keep the rail columns while the list fits, so a revealed
|
|
134
|
+
// rail never reflows the rows; hidden gives the columns back to the rows.
|
|
135
|
+
const appearance = this.#scrollbarAppearance();
|
|
136
|
+
const reservesRail = appearance !== "hidden";
|
|
137
|
+
const contentWidth = reservesRail ? Math.max(0, rect.width - RAIL_COLUMNS) : rect.width;
|
|
106
138
|
const valueColumn = this.#valueColumn(rows);
|
|
107
139
|
const geometry = scrollbarGeometry({
|
|
108
140
|
contentLength: rows.length,
|
|
@@ -110,6 +142,22 @@ export class SettingsApp {
|
|
|
110
142
|
scroll: layout.scroll,
|
|
111
143
|
trackHeight: Math.max(0, bodyHeight - SCROLLBAR_TOP_INSET),
|
|
112
144
|
});
|
|
145
|
+
const presentation = scrollbarPresentation({
|
|
146
|
+
geometry,
|
|
147
|
+
appearance,
|
|
148
|
+
style: this.#scrollbarStyle(),
|
|
149
|
+
hovered: this.#rails.isHovered(RAIL_KEY),
|
|
150
|
+
dragging: this.#rails.isDragging(RAIL_KEY),
|
|
151
|
+
activeUntil: this.#activeUntil,
|
|
152
|
+
now,
|
|
153
|
+
});
|
|
154
|
+
this.#railFrame = reservesRail && geometry !== null
|
|
155
|
+
? {
|
|
156
|
+
rail: { key: RAIL_KEY, column: rect.width, rowStart: SCROLLBAR_TOP_INSET, trackHeight: geometry.trackHeight },
|
|
157
|
+
geometry,
|
|
158
|
+
page: layout.visible,
|
|
159
|
+
}
|
|
160
|
+
: null;
|
|
113
161
|
const body = [];
|
|
114
162
|
this.#frameRows = [];
|
|
115
163
|
if (rows.length === 0) {
|
|
@@ -140,8 +188,9 @@ export class SettingsApp {
|
|
|
140
188
|
}
|
|
141
189
|
const withRail = withScrollbarRail(body.slice(0, bodyHeight), geometry, contentWidth, theme, {
|
|
142
190
|
topInset: SCROLLBAR_TOP_INSET,
|
|
191
|
+
presentation,
|
|
143
192
|
});
|
|
144
|
-
return this.#withMenu([...withRail, ...footer], selected, layout, valueColumn, theme, rect);
|
|
193
|
+
return this.#withMenu([...withRail, ...footer], selected, layout, valueColumn, theme, rect, reservesRail ? RAIL_COLUMNS : 0);
|
|
145
194
|
}
|
|
146
195
|
onInput(data, host) {
|
|
147
196
|
if (this.#structured !== null)
|
|
@@ -278,13 +327,16 @@ export class SettingsApp {
|
|
|
278
327
|
this.#scroll = Math.max(0, this.#scroll + (event.kind === "wheel-down" ? distance : -distance));
|
|
279
328
|
return { consumed: true };
|
|
280
329
|
}
|
|
330
|
+
const rail = this.#railPointer(event);
|
|
331
|
+
if (rail.owned)
|
|
332
|
+
return { consumed: true };
|
|
281
333
|
const row = this.#frameRows.find(candidate => candidate.screenRow === event.row - 1);
|
|
282
334
|
const previousKey = this.#hoverKey;
|
|
283
335
|
const previousRegion = this.#hoverRegion;
|
|
284
336
|
if (row === undefined) {
|
|
285
337
|
this.#hoverKey = null;
|
|
286
338
|
this.#hoverRegion = "label";
|
|
287
|
-
return { consumed: event.kind !== "motion", render: previousKey !== null };
|
|
339
|
+
return { consumed: event.kind !== "motion", render: previousKey !== null || rail.changed };
|
|
288
340
|
}
|
|
289
341
|
this.#hoverKey = row.key;
|
|
290
342
|
this.#hoverRegion = regionAt(row, event.column);
|
|
@@ -303,9 +355,55 @@ export class SettingsApp {
|
|
|
303
355
|
}
|
|
304
356
|
return { consumed: true };
|
|
305
357
|
}
|
|
306
|
-
const changed = previousKey !== this.#hoverKey || previousRegion !== this.#hoverRegion;
|
|
358
|
+
const changed = previousKey !== this.#hoverKey || previousRegion !== this.#hoverRegion || rail.changed;
|
|
307
359
|
return { consumed: event.kind !== "motion", render: changed };
|
|
308
360
|
}
|
|
361
|
+
// Rationale: the rail takes its share of a pointer report first: hover, a thumb drag, or a
|
|
362
|
+
// track page. Owned means the list must not see the report; changed means the rail looks different.
|
|
363
|
+
#railPointer(event) {
|
|
364
|
+
const frame = this.#railFrame;
|
|
365
|
+
const wasHovered = this.#rails.isHovered(RAIL_KEY);
|
|
366
|
+
if (frame === null) {
|
|
367
|
+
this.#rails.clear();
|
|
368
|
+
return { owned: false, changed: wasHovered };
|
|
369
|
+
}
|
|
370
|
+
const pointer = { column: event.column, row: event.row - 1 };
|
|
371
|
+
if (this.#rails.isDragging(RAIL_KEY)) {
|
|
372
|
+
// Invariant: a drag keeps the pointer wherever it goes until the button comes up.
|
|
373
|
+
if (event.kind === "release")
|
|
374
|
+
this.#rails.endDrag();
|
|
375
|
+
else {
|
|
376
|
+
const target = this.#rails.dragTo(frame.rail, frame.geometry, pointer);
|
|
377
|
+
if (target !== null)
|
|
378
|
+
this.#scroll = target;
|
|
379
|
+
}
|
|
380
|
+
return { owned: true, changed: true };
|
|
381
|
+
}
|
|
382
|
+
const over = this.#rails.notePointer([frame.rail], pointer) !== null;
|
|
383
|
+
if (!over)
|
|
384
|
+
return { owned: false, changed: wasHovered };
|
|
385
|
+
// Invariant: the rail is not a row: pointing at it lights nothing in the list.
|
|
386
|
+
this.#hoverKey = null;
|
|
387
|
+
this.#hoverRegion = "label";
|
|
388
|
+
if (event.kind === "press" && !this.#rails.beginDrag(frame.rail, frame.geometry, pointer)) {
|
|
389
|
+
this.#scroll = scrollForTrackPage(frame.geometry, pointer.row - frame.rail.rowStart, this.#scroll, frame.page);
|
|
390
|
+
}
|
|
391
|
+
return { owned: true, changed: true };
|
|
392
|
+
}
|
|
393
|
+
#noteScrollActivity(host, now) {
|
|
394
|
+
this.#activeUntil = Math.max(this.#activeUntil, now + SCROLL_LINGER_MS);
|
|
395
|
+
this.#clearActivityTimer();
|
|
396
|
+
this.#activityTimer = setTimeout(() => {
|
|
397
|
+
this.#activityTimer = undefined;
|
|
398
|
+
host.requestRender();
|
|
399
|
+
}, SCROLL_LINGER_REPAINT_MS);
|
|
400
|
+
this.#activityTimer.unref?.();
|
|
401
|
+
}
|
|
402
|
+
#clearActivityTimer() {
|
|
403
|
+
if (this.#activityTimer !== undefined)
|
|
404
|
+
clearTimeout(this.#activityTimer);
|
|
405
|
+
this.#activityTimer = undefined;
|
|
406
|
+
}
|
|
309
407
|
#openMenu(rows, selected) {
|
|
310
408
|
const row = rows[selected];
|
|
311
409
|
if (row === undefined || row.kind !== "element")
|
|
@@ -348,6 +446,7 @@ export class SettingsApp {
|
|
|
348
446
|
// thing under the pointer, so it stops looking like it.
|
|
349
447
|
this.#hoverKey = null;
|
|
350
448
|
this.#hoverRegion = "label";
|
|
449
|
+
this.#rails.clear();
|
|
351
450
|
this.#structured = { entry, flags: entry.flags.map(flag => flag.key), index: 0, record };
|
|
352
451
|
}
|
|
353
452
|
#structuredKey(data) {
|
|
@@ -410,13 +509,15 @@ export class SettingsApp {
|
|
|
410
509
|
if (input === null)
|
|
411
510
|
return { consumed: false };
|
|
412
511
|
const key = KEYS[data];
|
|
413
|
-
|
|
512
|
+
// Rationale: the boundary chords jump through the results; plain Home and End stay with
|
|
513
|
+
// the search cursor, which the shared line input moves below.
|
|
514
|
+
if (key === "ctrl+home" || key === "ctrl+end") {
|
|
414
515
|
const rows = this.#rows();
|
|
415
516
|
const selectable = selectableIndexes(rows);
|
|
416
|
-
const target = key === "end" ? selectable.at(-1) : selectable[0];
|
|
517
|
+
const target = key === "ctrl+end" ? selectable.at(-1) : selectable[0];
|
|
417
518
|
if (target !== undefined) {
|
|
418
519
|
this.#select(rows, target);
|
|
419
|
-
if (key === "home")
|
|
520
|
+
if (key === "ctrl+home")
|
|
420
521
|
this.#scroll = 0;
|
|
421
522
|
}
|
|
422
523
|
return { consumed: true };
|
|
@@ -502,11 +603,22 @@ export class SettingsApp {
|
|
|
502
603
|
return this.#pending.get(`${entry.backend}:${entry.id}`) ?? entry.value;
|
|
503
604
|
}
|
|
504
605
|
#scrollbarSpeed() {
|
|
606
|
+
const value = this.#scrollSetting("scrollbarSpeed");
|
|
607
|
+
return isScrollbarSpeed(value) ? value : "normal";
|
|
608
|
+
}
|
|
609
|
+
#scrollbarAppearance() {
|
|
610
|
+
const value = this.#scrollSetting("scrollbarAppearance");
|
|
611
|
+
return value === "always" || value === "hidden" ? value : "auto";
|
|
612
|
+
}
|
|
613
|
+
#scrollbarStyle() {
|
|
614
|
+
return this.#scrollSetting("scrollbarStyle") === "thick" ? "thick" : "thin";
|
|
615
|
+
}
|
|
616
|
+
// Invariant: a Scroll setting reads as the screen shows it: an accepted value first, then the source.
|
|
617
|
+
#scrollSetting(id) {
|
|
505
618
|
const entry = this.#session.sections()
|
|
506
619
|
.flatMap(section => section.entries)
|
|
507
|
-
.find(candidate => candidate.backend === "a1" && candidate.id ===
|
|
508
|
-
|
|
509
|
-
return isScrollbarSpeed(value) ? value : "normal";
|
|
620
|
+
.find(candidate => candidate.backend === "a1" && candidate.id === id);
|
|
621
|
+
return entry === undefined ? this.#session.value(id) : this.#shownValue(entry);
|
|
510
622
|
}
|
|
511
623
|
#jump(rows, target) {
|
|
512
624
|
this.#select(rows, target);
|
|
@@ -590,7 +702,7 @@ export class SettingsApp {
|
|
|
590
702
|
...(typeof shown === "number" && entry.editable ? { stepper: stepperEnds(range, shown) } : {}),
|
|
591
703
|
};
|
|
592
704
|
}
|
|
593
|
-
#withMenu(lines, _selected, _layout, valueColumn, theme, rect) {
|
|
705
|
+
#withMenu(lines, _selected, _layout, valueColumn, theme, rect, reservedRight) {
|
|
594
706
|
const menu = this.#menu;
|
|
595
707
|
const anchor = menu === null ? undefined : this.#frameRows.find(candidate => candidate.key === menu.anchorKey);
|
|
596
708
|
if (menu === null || anchor === undefined) {
|
|
@@ -605,7 +717,7 @@ export class SettingsApp {
|
|
|
605
717
|
const frame = valueMenuFrame(state, { screenRow: anchor.screenRow, valueColumn }, {
|
|
606
718
|
bodyHeight: lines.length - this.#footerHeight,
|
|
607
719
|
surfaceWidth: rect.width,
|
|
608
|
-
reservedRight
|
|
720
|
+
reservedRight,
|
|
609
721
|
});
|
|
610
722
|
this.#menuFrame = frame;
|
|
611
723
|
return renderValueMenu(lines, state, frame, theme);
|
|
@@ -63,6 +63,7 @@ export function createPiShellEditor(options) {
|
|
|
63
63
|
...(options.keybindingProfile === "a1" ? {
|
|
64
64
|
terminalRows: options.getRows,
|
|
65
65
|
getVisualLineCount: (width) => editorVisualLineCount(editor, width),
|
|
66
|
+
clearCommandSearchOnEscape: true,
|
|
66
67
|
} : {}),
|
|
67
68
|
...(options.keybindingProfile === "a1" && options.promptPresentation !== undefined ? {
|
|
68
69
|
...(inputPresentation === undefined ? {} : { inputPresentation }),
|
|
@@ -17,6 +17,8 @@ export interface OwnedEditorOptions extends EditorOptions {
|
|
|
17
17
|
readonly terminalRows?: () => number;
|
|
18
18
|
/** Reuses the editor's established atomic-aware visual layout when available. */
|
|
19
19
|
readonly getVisualLineCount?: (width: number) => number | undefined;
|
|
20
|
+
/** Bare-A1 exception: Escape on a sole top-level slash-command search also clears the prompt. */
|
|
21
|
+
readonly clearCommandSearchOnEscape?: boolean;
|
|
20
22
|
}
|
|
21
23
|
export interface ShellEditorInstance extends EditorSurface {
|
|
22
24
|
readonly actionHandlers: Map<AppKeybinding, () => void>;
|
|
@@ -21,6 +21,7 @@ export function createOwnedEditorClass(Base) {
|
|
|
21
21
|
#styleSuggestionCaret;
|
|
22
22
|
#terminalRows;
|
|
23
23
|
#getVisualLineCount;
|
|
24
|
+
#clearCommandSearchOnEscape;
|
|
24
25
|
#renderedBodyRowCount = 0;
|
|
25
26
|
constructor(tui, theme, keybindings, options = {}) {
|
|
26
27
|
super(tui, theme, options);
|
|
@@ -30,6 +31,7 @@ export function createOwnedEditorClass(Base) {
|
|
|
30
31
|
this.#styleSuggestionCaret = options.styleSuggestionCaret ?? (text => `\u001b[7m${text}\u001b[27m`);
|
|
31
32
|
this.#terminalRows = options.terminalRows ?? (() => 24);
|
|
32
33
|
this.#getVisualLineCount = options.getVisualLineCount;
|
|
34
|
+
this.#clearCommandSearchOnEscape = options.clearCommandSearchOnEscape === true;
|
|
33
35
|
}
|
|
34
36
|
getRenderedBodyRowCount() { return this.#renderedBodyRowCount; }
|
|
35
37
|
setPromptSuggestion(text) {
|
|
@@ -42,6 +44,16 @@ export function createOwnedEditorClass(Base) {
|
|
|
42
44
|
&& this.getText().length === 0
|
|
43
45
|
&& !this.isShowingAutocomplete();
|
|
44
46
|
}
|
|
47
|
+
/** True when autocomplete is searching one space-free slash command that is the editor's only content. */
|
|
48
|
+
isTopLevelCommandSearch() {
|
|
49
|
+
if (!this.isShowingAutocomplete())
|
|
50
|
+
return false;
|
|
51
|
+
const text = this.getText();
|
|
52
|
+
if (!/^\/\S*$/.test(text))
|
|
53
|
+
return false;
|
|
54
|
+
const cursor = this.getCursor();
|
|
55
|
+
return cursor.line === 0 && cursor.col === text.length;
|
|
56
|
+
}
|
|
45
57
|
setText(text) {
|
|
46
58
|
if (text.length > 0)
|
|
47
59
|
this.#promptSuggestion = null;
|
|
@@ -92,6 +104,11 @@ export function createOwnedEditorClass(Base) {
|
|
|
92
104
|
return;
|
|
93
105
|
}
|
|
94
106
|
}
|
|
107
|
+
else if (this.#clearCommandSearchOnEscape && this.isTopLevelCommandSearch()) {
|
|
108
|
+
// Escape on a bare slash-command search restores the empty prompt instead of only closing the menu.
|
|
109
|
+
this.setText("");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
95
112
|
super.handleInput(data);
|
|
96
113
|
return;
|
|
97
114
|
}
|
|
@@ -178,6 +178,8 @@ export declare class PiEngineAdapter implements OwnedUiPromptSuggestionGenerator
|
|
|
178
178
|
configuredTheme(): string | undefined;
|
|
179
179
|
/** Settings port for the live runtime, or null before the runtime is available. */
|
|
180
180
|
settingsPort(): PiSettingsIntegration | null;
|
|
181
|
+
/** Which settings surface this adapter serves; hidden-in-bare effects are unbindable in bare mode. */
|
|
182
|
+
get settingsProductMode(): "bare" | "comparison";
|
|
181
183
|
bindSettingsOwner(owner: AgentSettingOwner, handlers: PiSettingOwnerHandlers): () => void;
|
|
182
184
|
pinnedModelSelectorContext(): {
|
|
183
185
|
readonly currentModel: unknown;
|
|
@@ -224,10 +224,8 @@ export class PiEngineAdapter {
|
|
|
224
224
|
return "unavailable";
|
|
225
225
|
if (!session.model.reasoning)
|
|
226
226
|
return "ordinary";
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return ["off", "minimal", "low", "medium", "high", "xhigh", "max"]
|
|
230
|
-
.find(level => levels.includes(level)) ?? "unavailable";
|
|
227
|
+
// Performance: the run's own level keeps the provider's thinking parameters, and the cached prefix, identical.
|
|
228
|
+
return readSuggestionReasoning(session.thinkingLevel);
|
|
231
229
|
}
|
|
232
230
|
async generate(request) {
|
|
233
231
|
assertOwnedUiPromptSuggestionRequest(request);
|
|
@@ -246,15 +244,23 @@ export class PiEngineAdapter {
|
|
|
246
244
|
return { identity, outcome: request.signal.aborted ? "cancelled" : "unavailable", text: null };
|
|
247
245
|
}
|
|
248
246
|
const model = session.model;
|
|
249
|
-
const
|
|
247
|
+
const agent = session.agent;
|
|
248
|
+
const agentState = agent.state;
|
|
250
249
|
const policy = this.suggestionReasoningPolicy();
|
|
251
250
|
if (model === undefined || policy === "unavailable" || typeof runtime.services.modelRuntime.completeSimple !== "function") {
|
|
252
251
|
return { identity, outcome: "unavailable", text: null };
|
|
253
252
|
}
|
|
254
|
-
|
|
253
|
+
// Performance: mirror the primary loop's request shape so the provider serves the conversation prefix
|
|
254
|
+
// from the run's prompt cache. `onResponse` stays out: extensions must not see a suggestion as a response.
|
|
255
255
|
const reasoning = policy === "ordinary" || policy === "off" ? undefined : policy;
|
|
256
256
|
let response;
|
|
257
257
|
try {
|
|
258
|
+
const transformed = typeof agent.transformContext === "function"
|
|
259
|
+
? await agent.transformContext(agentState.messages, request.signal)
|
|
260
|
+
: agentState.messages;
|
|
261
|
+
const messages = typeof agent.convertToLlm === "function"
|
|
262
|
+
? await agent.convertToLlm(transformed)
|
|
263
|
+
: transformed.filter(message => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
|
|
258
264
|
response = await runtime.services.modelRuntime.completeSimple(model, {
|
|
259
265
|
systemPrompt: agentState.systemPrompt,
|
|
260
266
|
messages: [
|
|
@@ -265,6 +271,10 @@ export class PiEngineAdapter {
|
|
|
265
271
|
}, {
|
|
266
272
|
signal: request.signal,
|
|
267
273
|
...(reasoning === undefined ? {} : { reasoning }),
|
|
274
|
+
...(agent.sessionId === undefined ? {} : { sessionId: agent.sessionId }),
|
|
275
|
+
...(agent.thinkingBudgets === undefined ? {} : { thinkingBudgets: agent.thinkingBudgets }),
|
|
276
|
+
...(agent.transport === undefined ? {} : { transport: agent.transport }),
|
|
277
|
+
...(agent.onPayload === undefined ? {} : { onPayload: agent.onPayload }),
|
|
268
278
|
});
|
|
269
279
|
}
|
|
270
280
|
catch {
|
|
@@ -748,6 +758,10 @@ export class PiEngineAdapter {
|
|
|
748
758
|
}
|
|
749
759
|
return this.#settingsIntegration;
|
|
750
760
|
}
|
|
761
|
+
/** Which settings surface this adapter serves; hidden-in-bare effects are unbindable in bare mode. */
|
|
762
|
+
get settingsProductMode() {
|
|
763
|
+
return this.#settingsProductMode;
|
|
764
|
+
}
|
|
751
765
|
bindSettingsOwner(owner, handlers) {
|
|
752
766
|
const settings = this.settingsPort();
|
|
753
767
|
if (settings === null)
|
|
@@ -3235,6 +3249,11 @@ function readModel(value) {
|
|
|
3235
3249
|
displayName: stringValue(value.name) ?? modelId,
|
|
3236
3250
|
};
|
|
3237
3251
|
}
|
|
3252
|
+
function readSuggestionReasoning(value) {
|
|
3253
|
+
return value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max"
|
|
3254
|
+
? value
|
|
3255
|
+
: "off";
|
|
3256
|
+
}
|
|
3238
3257
|
function readThinkingLevel(value) {
|
|
3239
3258
|
return value === "off" || value === "minimal" || value === "low" || value === "medium"
|
|
3240
3259
|
|| value === "high" || value === "xhigh"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentJsonValue, AgentSettingApplicationBoundary, AgentSettingChangeOutcome, AgentSettingOwner } from "../../../contracts/agent-engine/index.js";
|
|
2
2
|
export type PiSettingKey = "autoCompact" | "showImages" | "imageWidthCells" | "autoResizeImages" | "blockImages" | "enableSkillCommands" | "steeringMode" | "followUpMode" | "transport" | "httpIdleTimeoutMs" | "thinkingLevel" | "theme" | "hideThinkingBlock" | "mermaidRenderingMode" | "showCacheMissNotices" | "collapseChangelog" | "enableInstallTelemetry" | "quietStartup" | "defaultProjectTrust" | "doubleEscapeAction" | "treeFilterMode" | "showHardwareCursor" | "editorPaddingX" | "outputPad" | "autocompleteMaxVisible" | "clearOnShrink" | "showTerminalProgress" | "tuiMode" | "fullscreenExitOutput" | "fullscreenScrollbar" | "warnings";
|
|
3
|
-
export type PiSettingVisualClass = "none" | "transcript" | "transcript-geometry" | "editor-menu" | "queue-transcript" | "status-error" | "retry-error" | "footer-transcript" | "markdown" | "transcript-notice" | "startup-transcript" | "startup-selector" | "selector" | "terminal-cursor" | "editor-geometry" | "menu-geometry" | "terminal-frame" | "terminal-status" | "
|
|
3
|
+
export type PiSettingVisualClass = "none" | "transcript" | "transcript-geometry" | "editor-menu" | "queue-transcript" | "status-error" | "retry-error" | "footer-transcript" | "markdown" | "transcript-notice" | "startup-transcript" | "startup-selector" | "selector" | "terminal-cursor" | "editor-geometry" | "menu-geometry" | "terminal-frame" | "terminal-status" | "hidden";
|
|
4
4
|
export interface PiSettingVisualEvidence {
|
|
5
5
|
/** Reviewed visual family; `none` still names the behavior that can emit styled diagnostics. */
|
|
6
6
|
readonly class: PiSettingVisualClass;
|
|
@@ -31,7 +31,7 @@ export const PI_SETTING_EFFECTS = Object.freeze({
|
|
|
31
31
|
clearOnShrink: effect("live", "terminal", "terminal-frame", "pinned resize clearing and resulting terminal frame", "pi-terminal-operation-parity"),
|
|
32
32
|
showTerminalProgress: effect("live", "terminal", "terminal-status", "pinned OSC progress lifecycle", "pi-terminal-operation-parity"),
|
|
33
33
|
tuiMode: hiddenEffect("next-session", "shell", "pinned regular/fullscreen selector and terminal lifecycle", "pi-terminal-operation-parity"),
|
|
34
|
-
fullscreenExitOutput:
|
|
34
|
+
fullscreenExitOutput: hiddenEffect("current-exit", "shutdown", "pinned styled transcript and compact dim resume hint", "pinned-fullscreen-exit-parity"),
|
|
35
35
|
fullscreenScrollbar: hiddenEffect("live", "shell", "pinned fullscreen scrollbar reservation", "pi-terminal-operation-parity"),
|
|
36
36
|
warnings: effect("live", "agent", "transcript-notice", "pinned warning rows by warning part", "pinned-transcript-lifecycle-parity"),
|
|
37
37
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic quit-outro effect plans, ported from the v2 sketch.
|
|
3
|
+
*
|
|
4
|
+
* Each effect turns the captured frame's per-row visible widths and a seed into
|
|
5
|
+
* two sorted schedules over unit playback progress: sparkles paint a glyph at a
|
|
6
|
+
* cell from `start`, and clears blank a cell at `end`. The player consumes both
|
|
7
|
+
* in order, so a fixed seed yields a byte-stable animation.
|
|
8
|
+
*/
|
|
9
|
+
export interface QuitOutroCell {
|
|
10
|
+
readonly row: number;
|
|
11
|
+
readonly col: number;
|
|
12
|
+
/** Progress at which the sparkle glyph is painted. */
|
|
13
|
+
readonly start: number;
|
|
14
|
+
/** Progress at which the cell is cleared. */
|
|
15
|
+
readonly end: number;
|
|
16
|
+
readonly glyph: string;
|
|
17
|
+
readonly color: string;
|
|
18
|
+
}
|
|
19
|
+
export interface QuitOutroPlan {
|
|
20
|
+
/** Sorted by `start`. */
|
|
21
|
+
readonly sparkles: readonly QuitOutroCell[];
|
|
22
|
+
/** Sorted by `end`. */
|
|
23
|
+
readonly clears: readonly QuitOutroCell[];
|
|
24
|
+
}
|
|
25
|
+
export declare const QUIT_OUTRO_EFFECTS: readonly ["fall", "dissolve", "starburst", "waves"];
|
|
26
|
+
export type QuitOutroEffect = (typeof QUIT_OUTRO_EFFECTS)[number];
|
|
27
|
+
export declare function isQuitOutroEffect(value: unknown): value is QuitOutroEffect;
|
|
28
|
+
/** Builds the selected effect's deterministic plan for the captured row widths. */
|
|
29
|
+
export declare function createQuitOutroPlan(effect: QuitOutroEffect, rowWidths: readonly number[], seed: number): QuitOutroPlan;
|