@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.
Files changed (39) hide show
  1. package/README.md +9 -0
  2. package/dist/composition/owned-ui.js +12 -0
  3. package/dist/contracts/owned-ui/model.d.ts +12 -0
  4. package/dist/features/owned-ui/settings-app.d.ts +1 -0
  5. package/dist/features/owned-ui/settings-app.js +129 -17
  6. package/dist/integrations/pi/components/shell-editor-autocomplete.js +1 -0
  7. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +2 -0
  8. package/dist/integrations/pi/components/upstream/components/owned-editor.js +17 -0
  9. package/dist/integrations/pi/engine/adapter.d.ts +2 -0
  10. package/dist/integrations/pi/engine/adapter.js +25 -6
  11. package/dist/integrations/pi/engine/settings-effects.d.ts +1 -1
  12. package/dist/integrations/pi/engine/settings-effects.js +1 -1
  13. package/dist/integrations/pi/session-ui/quit-outro-effects.d.ts +29 -0
  14. package/dist/integrations/pi/session-ui/quit-outro-effects.js +242 -0
  15. package/dist/integrations/pi/session-ui/quit-outro.d.ts +38 -0
  16. package/dist/integrations/pi/session-ui/quit-outro.js +98 -0
  17. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +12 -1
  18. package/dist/integrations/pi/session-ui/session-shell.js +65 -5
  19. package/dist/integrations/pi/tui-runtime/adapter.d.ts +10 -2
  20. package/dist/integrations/pi/tui-runtime/adapter.js +53 -4
  21. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
  22. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +12 -0
  23. package/dist/native/darwin-arm64/manifest.json +1 -1
  24. package/dist/native/linux-x64/manifest.json +1 -1
  25. package/dist/native/win32-x64/manifest.json +2 -2
  26. package/dist/native/win32-x64/process-guardian.exe +0 -0
  27. package/dist/ui/components/list-view.js +5 -6
  28. package/dist/ui/components/surface.d.ts +7 -2
  29. package/dist/ui/components/surface.js +13 -3
  30. package/dist/ui/settings/declarations.d.ts +3 -1
  31. package/dist/ui/settings/declarations.js +33 -1
  32. package/dist/ui/settings/migrations.js +20 -0
  33. package/docs/architecture/ui-reference-provenance.md +2 -2
  34. package/docs/ci-release-runbook.md +5 -5
  35. package/docs/local-worktree-cleanup.md +40 -9
  36. package/docs/manual-owned-ui-checkpoint.md +3 -2
  37. package/docs/openspec-archive-automation.md +22 -9
  38. package/docs/validation.md +4 -2
  39. 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
- SETTINGS_SHORTCUTS.declare({ key: "home", scope: SCOPE, description: "First setting", section: "Navigate" }, "first");
20
- SETTINGS_SHORTCUTS.declare({ key: "end", scope: SCOPE, description: "Last setting", section: "Navigate" }, "last");
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
- "\u001b[H": "home",
43
- "\u001b[F": "end",
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 contentWidth = Math.max(0, rect.width - RAIL_COLUMNS);
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
- if (key === "home" || key === "end") {
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 === "scrollbarSpeed");
508
- const value = entry === undefined ? this.#session.value("scrollbarSpeed") : this.#shownValue(entry);
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: RAIL_COLUMNS,
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
- const levels = session.getAvailableThinkingLevels?.() ?? [];
228
- // Compatibility: capability order, not the user's current setting or a provider-name heuristic.
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 agentState = session.agent.state;
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
- const messages = agentState.messages.filter(message => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
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" | "restored-parent-output" | "hidden";
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: effect("current-exit", "shutdown", "restored-parent-output", "pinned styled transcript and compact dim resume hint", "pinned-fullscreen-exit-parity"),
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;