@timurproko/a1 0.1.8-dev.457 → 0.1.8-dev.465
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/composition/owned-ui.js +2 -1
- package/dist/contracts/owned-ui/model.d.ts +5 -1
- package/dist/contracts/owned-ui/validation.js +3 -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-footer-status.js +32 -8
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +2 -0
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +2 -2
- package/dist/integrations/pi/engine/adapter.js +42 -4
- package/dist/integrations/pi/engine/compaction-progress.d.ts +28 -0
- package/dist/integrations/pi/engine/compaction-progress.js +96 -0
- package/dist/integrations/pi/engine/conformance.d.ts +1 -1
- package/dist/integrations/pi/engine/conformance.js +3 -3
- package/dist/integrations/pi/engine/session-integration.d.ts +25 -3
- package/dist/integrations/pi/engine/session-integration.js +93 -8
- package/dist/integrations/pi/session-ui/session-shell-root.js +1 -0
- package/dist/integrations/pi/session-ui/session-shell.js +14 -42
- 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 +1 -1
- package/dist/ui/settings/declarations.js +13 -2
- package/dist/ui/settings/migrations.js +13 -0
- package/docs/architecture/ui-reference-provenance.md +2 -2
- package/docs/local-worktree-cleanup.md +49 -6
- package/docs/openspec-archive-automation.md +1 -1
- package/package.json +1 -1
|
@@ -120,7 +120,8 @@ function quitOutroSettingsSnapshot(settings) {
|
|
|
120
120
|
const effect = settings.value("quitEffect");
|
|
121
121
|
const durationMs = settings.value("quitEffectDurationMs");
|
|
122
122
|
return {
|
|
123
|
-
|
|
123
|
+
enabled: settings.value("quitAnimation") !== false,
|
|
124
|
+
effect: effect === "dissolve" || effect === "starburst" || effect === "waves" ? effect : "fall",
|
|
124
125
|
durationMs: typeof durationMs === "number" ? durationMs : 800,
|
|
125
126
|
};
|
|
126
127
|
}
|
|
@@ -64,9 +64,11 @@ 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"
|
|
67
|
+
export type OwnedUiQuitEffect = "fall" | "dissolve" | "starburst" | "waves";
|
|
68
68
|
/** Profile-local quit outro choice, read at the moment bare A1 quits. */
|
|
69
69
|
export interface OwnedUiQuitOutroSettings {
|
|
70
|
+
/** False leaves the terminal immediately; the effect and duration are then ignored. */
|
|
71
|
+
readonly enabled: boolean;
|
|
70
72
|
readonly effect: OwnedUiQuitEffect;
|
|
71
73
|
/** Requested playback length; the player clamps it to its supported range. */
|
|
72
74
|
readonly durationMs: number;
|
|
@@ -124,6 +126,8 @@ export interface OwnedUiFooterView {
|
|
|
124
126
|
export interface OwnedUiStatusView {
|
|
125
127
|
readonly title: string;
|
|
126
128
|
readonly workingMessage: string | null;
|
|
129
|
+
/** Estimated progress of the shown work state as an integer percent below 100, when the engine can measure it. */
|
|
130
|
+
readonly workingProgress?: number | null;
|
|
127
131
|
readonly diagnostics: readonly string[];
|
|
128
132
|
readonly badges: readonly string[];
|
|
129
133
|
readonly usage?: OwnedUiUsageView;
|
|
@@ -347,6 +347,9 @@ export function assertOwnedUiEditorState(editor) {
|
|
|
347
347
|
export function assertOwnedUiStatusView(status) {
|
|
348
348
|
assertBoundedText(status.title, "owned-UI status title", MAX_LABEL_LENGTH);
|
|
349
349
|
assertOptionalText(status.workingMessage, "owned-UI working message", MAX_MESSAGE_LENGTH);
|
|
350
|
+
if (status.workingProgress !== undefined && status.workingProgress !== null) {
|
|
351
|
+
assertIntegerInRange(status.workingProgress, 0, 99, "owned-UI working progress");
|
|
352
|
+
}
|
|
350
353
|
assertCollection(status.diagnostics, "owned-UI status diagnostics", MAX_STATUS_DIAGNOSTICS);
|
|
351
354
|
for (const diagnostic of status.diagnostics) {
|
|
352
355
|
assertBoundedText(diagnostic, "owned-UI status diagnostic", MAX_MESSAGE_LENGTH);
|
|
@@ -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);
|
|
@@ -75,19 +75,29 @@ export function createPiShellStatus(view, formatProgressStatus, runtime) {
|
|
|
75
75
|
const statusUi = createTuiFacade(runtime ?? { getColumns: () => 80, getRows: () => 24, requestRender() { } });
|
|
76
76
|
let workingOverride;
|
|
77
77
|
let outputPad = PINNED_PI_LAYOUT.outputPad;
|
|
78
|
+
let progressPresentation = "pinned";
|
|
78
79
|
let placement = statusPlacement(view, workingOverride);
|
|
79
|
-
|
|
80
|
-
let
|
|
80
|
+
const liveStatusText = () => formatProgressStatus(liveWorkingText(view, workingOverride, progressPresentation));
|
|
81
|
+
let component = statusComponent(view, statusUi, outputPad, liveStatusText, placement);
|
|
82
|
+
let signature = statusSignature(view, workingOverride, outputPad, placement, progressPresentation);
|
|
81
83
|
const rebuild = () => {
|
|
82
84
|
const nextPlacement = statusPlacement(view, workingOverride);
|
|
83
|
-
const nextSignature = statusSignature(view, workingOverride, outputPad, nextPlacement);
|
|
85
|
+
const nextSignature = statusSignature(view, workingOverride, outputPad, nextPlacement, progressPresentation);
|
|
84
86
|
if (nextSignature === signature)
|
|
85
87
|
return;
|
|
88
|
+
// Performance: progress ticks change only the live message; the spinner keeps its frame and timer.
|
|
89
|
+
if (placement === "live" && nextPlacement === "live" && component instanceof WorkingStatusIndicator
|
|
90
|
+
&& statusSignature(view, workingOverride, outputPad, nextPlacement, progressPresentation, false)
|
|
91
|
+
=== statusSignature(view, workingOverride, outputPad, placement, progressPresentation, false)) {
|
|
92
|
+
component.setMessage(liveStatusText());
|
|
93
|
+
signature = nextSignature;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
86
96
|
if (component !== undefined && "dispose" in component && typeof component.dispose === "function")
|
|
87
97
|
component.dispose();
|
|
88
98
|
placement = nextPlacement;
|
|
89
99
|
signature = nextSignature;
|
|
90
|
-
component = statusComponent(view, statusUi,
|
|
100
|
+
component = statusComponent(view, statusUi, outputPad, liveStatusText, placement);
|
|
91
101
|
};
|
|
92
102
|
return {
|
|
93
103
|
render: width => component?.render(width) ?? [],
|
|
@@ -107,6 +117,10 @@ export function createPiShellStatus(view, formatProgressStatus, runtime) {
|
|
|
107
117
|
outputPad = padding;
|
|
108
118
|
rebuild();
|
|
109
119
|
},
|
|
120
|
+
setProgressPresentation(presentation) {
|
|
121
|
+
progressPresentation = presentation;
|
|
122
|
+
rebuild();
|
|
123
|
+
},
|
|
110
124
|
dispose() {
|
|
111
125
|
if (component !== undefined && "dispose" in component && typeof component.dispose === "function")
|
|
112
126
|
component.dispose();
|
|
@@ -145,9 +159,18 @@ function statusPlacement(view, workingOverride) {
|
|
|
145
159
|
return "dock";
|
|
146
160
|
return view.status.workingMessage === null ? "hidden" : "dock";
|
|
147
161
|
}
|
|
148
|
-
|
|
162
|
+
// Rationale: the engine publishes the semantic working word and a separate measured percent; bare A1
|
|
163
|
+
// composes them here, while the pinned route keeps the bare word for comparison with Pi.
|
|
164
|
+
function liveWorkingText(view, workingOverride, progressPresentation) {
|
|
165
|
+
const message = workingOverride ?? view.status.workingMessage ?? "Working";
|
|
166
|
+
const progress = view.status.workingProgress;
|
|
167
|
+
return workingOverride === undefined && progressPresentation === "custom-viewport" && typeof progress === "number"
|
|
168
|
+
? `${message} (${progress}%)`
|
|
169
|
+
: message;
|
|
170
|
+
}
|
|
171
|
+
function statusComponent(view, ui, outputPad, liveStatusText, placement) {
|
|
149
172
|
if (placement === "live") {
|
|
150
|
-
return new WorkingStatusIndicator(ui,
|
|
173
|
+
return new WorkingStatusIndicator(ui, liveStatusText());
|
|
151
174
|
}
|
|
152
175
|
if (placement === "dock") {
|
|
153
176
|
if (view.lifecycle === "failed") {
|
|
@@ -157,8 +180,9 @@ function statusComponent(view, ui, workingOverride, outputPad, formatProgressSta
|
|
|
157
180
|
}
|
|
158
181
|
return undefined;
|
|
159
182
|
}
|
|
160
|
-
function statusSignature(view, workingOverride, outputPad, placement) {
|
|
161
|
-
|
|
183
|
+
function statusSignature(view, workingOverride, outputPad, placement, progressPresentation, withMessage = true) {
|
|
184
|
+
const message = withMessage ? `${workingOverride ?? ""}\u0000${view.status.workingMessage ?? ""}\u0000${view.status.workingProgress ?? ""}` : "";
|
|
185
|
+
return `${placement}\u0000${outputPad}\u0000${view.lifecycle}\u0000${progressPresentation}\u0000${message}\u0000${view.status.diagnostics.at(-1) ?? ""}`;
|
|
162
186
|
}
|
|
163
187
|
function queuedInputText(submissions, presentation) {
|
|
164
188
|
if (submissions.length === 0)
|
|
@@ -79,6 +79,8 @@ export type PiShellStatusPlacement = "live" | "dock" | "hidden";
|
|
|
79
79
|
export interface PiShellStatusPort extends PiShellViewComponentPort {
|
|
80
80
|
setWorkingOverride(message: string | undefined): void;
|
|
81
81
|
setOutputPad(padding: 0 | 1): void;
|
|
82
|
+
/** Bare A1 shows engine-measured progress beside the working word; the pinned route keeps the bare word. */
|
|
83
|
+
setProgressPresentation(presentation: "pinned" | "custom-viewport"): void;
|
|
82
84
|
/** Semantic row placement; callers must not inspect rendered text. */
|
|
83
85
|
placement(): PiShellStatusPlacement;
|
|
84
86
|
renderDock(width: number): readonly string[];
|
|
@@ -44,12 +44,12 @@ export function createOwnedEditorClass(Base) {
|
|
|
44
44
|
&& this.getText().length === 0
|
|
45
45
|
&& !this.isShowingAutocomplete();
|
|
46
46
|
}
|
|
47
|
-
/** True when autocomplete is searching one
|
|
47
|
+
/** True when autocomplete is searching one space-free slash command that is the editor's only content. */
|
|
48
48
|
isTopLevelCommandSearch() {
|
|
49
49
|
if (!this.isShowingAutocomplete())
|
|
50
50
|
return false;
|
|
51
51
|
const text = this.getText();
|
|
52
|
-
if (
|
|
52
|
+
if (!/^\/\S*$/.test(text))
|
|
53
53
|
return false;
|
|
54
54
|
const cursor = this.getCursor();
|
|
55
55
|
return cursor.line === 0 && cursor.col === text.length;
|
|
@@ -14,6 +14,7 @@ import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACK
|
|
|
14
14
|
import { PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "./workflows.js";
|
|
15
15
|
import { createPiRuntimeIntegration } from "./runtime-integration.js";
|
|
16
16
|
import { PiSessionCommandIntegration } from "./session-integration.js";
|
|
17
|
+
import { observeCompactionProgress } from "./compaction-progress.js";
|
|
17
18
|
import { PiSettingsIntegration } from "./settings-integration.js";
|
|
18
19
|
import { PendingEngineDelivery } from "./pending-delivery.js";
|
|
19
20
|
/** Explicit flush failure when required delivery was interrupted rather than completed. */
|
|
@@ -147,6 +148,7 @@ export class PiEngineAdapter {
|
|
|
147
148
|
#assistantResponseSequence = 0;
|
|
148
149
|
#statusKind = null;
|
|
149
150
|
#sessionCommands;
|
|
151
|
+
#compactionProgress = null;
|
|
150
152
|
#gitBranch = null;
|
|
151
153
|
#extensionUi;
|
|
152
154
|
#extensionShutdown;
|
|
@@ -1059,6 +1061,7 @@ export class PiEngineAdapter {
|
|
|
1059
1061
|
clearQueuedWorkflows() {
|
|
1060
1062
|
const session = this.#requireWorkflowSession();
|
|
1061
1063
|
const result = session.clearQueue?.();
|
|
1064
|
+
this.#sessionCommands?.forgetQueuedImages();
|
|
1062
1065
|
if (!isRecord(result))
|
|
1063
1066
|
return [];
|
|
1064
1067
|
return [...readStringArray(result.steering), ...readStringArray(result.followUp)];
|
|
@@ -1257,6 +1260,8 @@ export class PiEngineAdapter {
|
|
|
1257
1260
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "stopping", reason: null });
|
|
1258
1261
|
this.#unsubscribe?.();
|
|
1259
1262
|
this.#unsubscribe = undefined;
|
|
1263
|
+
this.#compactionProgress?.dispose();
|
|
1264
|
+
this.#compactionProgress = null;
|
|
1260
1265
|
await this.#runtime?.dispose();
|
|
1261
1266
|
this.#lifecycle = "stopped";
|
|
1262
1267
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "stopped", reason: null });
|
|
@@ -2016,6 +2021,11 @@ export class PiEngineAdapter {
|
|
|
2016
2021
|
this.#activeCommandIds = [];
|
|
2017
2022
|
this.#completedCommands.clear();
|
|
2018
2023
|
this.#sessionCommands = new PiSessionCommandIntegration(session);
|
|
2024
|
+
this.#compactionProgress?.dispose();
|
|
2025
|
+
this.#compactionProgress = observeCompactionProgress(session, percent => {
|
|
2026
|
+
if (this.#session === session && !this.#disposed)
|
|
2027
|
+
this.#publishCompactionProgress(percent);
|
|
2028
|
+
});
|
|
2019
2029
|
this.#editor = {
|
|
2020
2030
|
text: "",
|
|
2021
2031
|
queuedSubmissions: [],
|
|
@@ -2024,7 +2034,7 @@ export class PiEngineAdapter {
|
|
|
2024
2034
|
historyRevision: this.#editor.historyRevision + 1,
|
|
2025
2035
|
submitEnabled: true,
|
|
2026
2036
|
};
|
|
2027
|
-
this.#status = { ...this.#status, workingMessage: null, badges: [] };
|
|
2037
|
+
this.#status = { ...this.#status, workingMessage: null, workingProgress: null, badges: [] };
|
|
2028
2038
|
this.#statusKind = null;
|
|
2029
2039
|
this.#agentRunActive = false;
|
|
2030
2040
|
this.#agentRunSequence = 0;
|
|
@@ -2115,7 +2125,7 @@ export class PiEngineAdapter {
|
|
|
2115
2125
|
const wasBusy = this.#lifecycle === "busy";
|
|
2116
2126
|
this.#statusKind = kind;
|
|
2117
2127
|
this.#lifecycle = "busy";
|
|
2118
|
-
this.#status = { ...this.#status, workingMessage: message };
|
|
2128
|
+
this.#status = { ...this.#status, workingMessage: message, workingProgress: null };
|
|
2119
2129
|
if (!wasBusy)
|
|
2120
2130
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
|
|
2121
2131
|
this.#emitEvent({ type: "status", status: this.#status });
|
|
@@ -2133,10 +2143,32 @@ export class PiEngineAdapter {
|
|
|
2133
2143
|
#leaveWorkStates() {
|
|
2134
2144
|
this.#statusKind = null;
|
|
2135
2145
|
this.#lifecycle = "ready";
|
|
2136
|
-
this.#status = { ...this.#status, workingMessage: null };
|
|
2146
|
+
this.#status = { ...this.#status, workingMessage: null, workingProgress: null };
|
|
2137
2147
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
|
|
2138
2148
|
this.#emitEvent({ type: "status", status: this.#status });
|
|
2139
2149
|
}
|
|
2150
|
+
// Invariant: progress belongs to the compaction state only and is published when the integer changes.
|
|
2151
|
+
#publishCompactionProgress(percent) {
|
|
2152
|
+
if (this.#statusKind !== "compaction" || this.#status.workingProgress === percent)
|
|
2153
|
+
return;
|
|
2154
|
+
this.#status = { ...this.#status, workingProgress: percent };
|
|
2155
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
2156
|
+
}
|
|
2157
|
+
// Rationale: a manual compaction ends with an idle session, so the engine would never consume
|
|
2158
|
+
// the messages queued during it; automatic compaction returns to a run or a pending prompt.
|
|
2159
|
+
#deliverQueuedAfterCompaction() {
|
|
2160
|
+
const commands = this.#sessionCommands;
|
|
2161
|
+
const generation = this.#sessionGeneration;
|
|
2162
|
+
if (commands === undefined)
|
|
2163
|
+
return;
|
|
2164
|
+
const report = (error) => {
|
|
2165
|
+
if (generation !== this.#sessionGeneration || this.#disposed)
|
|
2166
|
+
return;
|
|
2167
|
+
this.#addDiagnostic("error", "engine-command", `Queued input could not be sent after compaction: ${error instanceof Error ? error.message : String(error)}`, true);
|
|
2168
|
+
this.#emitView();
|
|
2169
|
+
};
|
|
2170
|
+
commands.deliverQueuedAfterCompaction(report).catch(report);
|
|
2171
|
+
}
|
|
2140
2172
|
#handlePiEvent(event) {
|
|
2141
2173
|
try {
|
|
2142
2174
|
this.#applyPiEvent(event);
|
|
@@ -2286,9 +2318,13 @@ export class PiEngineAdapter {
|
|
|
2286
2318
|
return;
|
|
2287
2319
|
case "compaction_start":
|
|
2288
2320
|
this.#enterWorkState("compaction", "Compacting");
|
|
2321
|
+
this.#compactionProgress?.begin();
|
|
2289
2322
|
return;
|
|
2290
2323
|
case "compaction_end":
|
|
2324
|
+
this.#compactionProgress?.end();
|
|
2291
2325
|
this.#endWorkState("compaction");
|
|
2326
|
+
if (event.reason === "manual")
|
|
2327
|
+
this.#deliverQueuedAfterCompaction();
|
|
2292
2328
|
return;
|
|
2293
2329
|
case "thinking_level_changed":
|
|
2294
2330
|
this.#thinkingLevel = readThinkingLevel(event.level);
|
|
@@ -2797,10 +2833,12 @@ export class PiEngineAdapter {
|
|
|
2797
2833
|
this.#admissionStopped = !canResume;
|
|
2798
2834
|
this.#unsubscribe?.();
|
|
2799
2835
|
this.#unsubscribe = undefined;
|
|
2836
|
+
this.#compactionProgress?.dispose();
|
|
2837
|
+
this.#compactionProgress = null;
|
|
2800
2838
|
++this.#sessionGeneration;
|
|
2801
2839
|
this.#agentRunActive = false;
|
|
2802
2840
|
this.#statusKind = null;
|
|
2803
|
-
this.#status = { ...this.#status, workingMessage: null };
|
|
2841
|
+
this.#status = { ...this.#status, workingMessage: null, workingProgress: null };
|
|
2804
2842
|
this.#lifecycle = this.#disposed ? "stopped" : canResume ? "ready" : "failed";
|
|
2805
2843
|
this.#editor = { ...this.#editor, submitEnabled: canResume && !this.#disposed };
|
|
2806
2844
|
this.#viewRevision += 1;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Expected summary size for a first compaction, when no previous summary on the branch can serve as the estimate. */
|
|
2
|
+
export declare const DEFAULT_EXPECTED_COMPACTION_SUMMARY_CHARS = 4000;
|
|
3
|
+
/** The public session surface compaction progress reads: the agent's stream function and the branch entries. */
|
|
4
|
+
export interface CompactionProgressSession {
|
|
5
|
+
readonly agent?: {
|
|
6
|
+
streamFunction?: unknown;
|
|
7
|
+
} | undefined;
|
|
8
|
+
readonly sessionManager?: {
|
|
9
|
+
getBranch?(): readonly unknown[];
|
|
10
|
+
} | undefined;
|
|
11
|
+
}
|
|
12
|
+
export interface CompactionProgressObserver {
|
|
13
|
+
/** Marks `compaction_start`: resets the streamed count and captures the expected summary size. */
|
|
14
|
+
begin(): void;
|
|
15
|
+
/** Marks `compaction_end`: later stream chunks are ignored. */
|
|
16
|
+
end(): void;
|
|
17
|
+
/** Restores the original stream function; the observer reports nothing afterwards. */
|
|
18
|
+
dispose(): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Observes the summarization stream of a compaction through the session agent's public stream
|
|
22
|
+
* function and reports an estimated integer percent. Pi never iterates that stream itself (it
|
|
23
|
+
* reads only its final result), so the observer iterates it in the background between
|
|
24
|
+
* `begin()` and `end()` and returns the same stream object to Pi. Outside that window the
|
|
25
|
+
* wrapper is a pass-through. Returns null when the agent exposes no callable stream function,
|
|
26
|
+
* in which case compaction proceeds without progress.
|
|
27
|
+
*/
|
|
28
|
+
export declare function observeCompactionProgress(session: CompactionProgressSession, onProgress: (percent: number) => void): CompactionProgressObserver | null;
|