@timurproko/a1 0.2.7-dev.810 → 0.2.7-dev.831

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 (124) hide show
  1. package/README.md +7 -1
  2. package/bin/automatic-update.js +7 -0
  3. package/bin/ui.js +1 -0
  4. package/dist/app/session-shell/resident-tab-coordinator.d.ts +6 -9
  5. package/dist/app/session-shell/resident-tab-coordinator.js +9 -52
  6. package/dist/app/session-shell/session-shell-root.d.ts +9 -3
  7. package/dist/app/session-shell/session-shell.js +70 -20
  8. package/dist/app/session-shell/session-viewport-controller.js +6 -4
  9. package/dist/app/session-shell/transcript-document.js +1 -1
  10. package/dist/composition/owned-ui.d.ts +3 -11
  11. package/dist/composition/owned-ui.js +93 -43
  12. package/dist/composition/resident-tab-bridge.d.ts +10 -1
  13. package/dist/composition/resident-tab-bridge.js +21 -0
  14. package/dist/composition/settings-route-host.d.ts +12 -2
  15. package/dist/composition/settings-route-host.js +27 -9
  16. package/dist/composition/settings-search-input.d.ts +9 -0
  17. package/dist/composition/settings-search-input.js +58 -0
  18. package/dist/contracts/owned-ui/session-backend.d.ts +2 -0
  19. package/dist/contracts/owned-ui/session-workflows.d.ts +7 -3
  20. package/dist/contracts/presentation/index.d.ts +11 -0
  21. package/dist/contracts/presentation/index.js +12 -0
  22. package/dist/features/launch/development-launch.js +6 -0
  23. package/dist/features/launch/profile-paths.js +4 -0
  24. package/dist/features/owned-ui/plus-menu-dialog.d.ts +22 -0
  25. package/dist/features/owned-ui/plus-menu-dialog.js +76 -0
  26. package/dist/features/owned-ui/reference-screen-app.d.ts +2 -0
  27. package/dist/features/owned-ui/reference-screen-app.js +12 -2
  28. package/dist/features/owned-ui/settings-app.d.ts +10 -3
  29. package/dist/features/owned-ui/settings-app.js +98 -45
  30. package/dist/features/owned-ui/settings-search-input.d.ts +29 -0
  31. package/dist/features/owned-ui/settings-search-input.js +1 -0
  32. package/dist/features/prompt-history/index.d.ts +1 -1
  33. package/dist/features/prompt-history/paths.d.ts +2 -1
  34. package/dist/features/prompt-history/paths.js +10 -2
  35. package/dist/features/prompt-history/service.d.ts +5 -0
  36. package/dist/features/prompt-history/service.js +1 -1
  37. package/dist/features/resident-tab/shortcut-declarations.d.ts +1 -1
  38. package/dist/features/resident-tab/shortcut-declarations.js +2 -0
  39. package/dist/foundation/launch-context/index.d.ts +2 -0
  40. package/dist/foundation/launch-context/index.js +1 -0
  41. package/dist/foundation/release/automatic-update.d.ts +37 -0
  42. package/dist/foundation/release/automatic-update.js +265 -0
  43. package/dist/foundation/release/bootstrap.d.ts +1 -9
  44. package/dist/foundation/release/bootstrap.js +7 -3
  45. package/dist/foundation/release/index.d.ts +1 -1
  46. package/dist/foundation/release/index.js +1 -1
  47. package/dist/foundation/release/latest-release.d.ts +3 -2
  48. package/dist/foundation/release/latest-release.js +1 -1
  49. package/dist/foundation/release/update-execution-lease.d.ts +36 -0
  50. package/dist/foundation/release/update-execution-lease.js +145 -0
  51. package/dist/foundation/release/update.d.ts +8 -6
  52. package/dist/foundation/release/update.js +232 -167
  53. package/dist/foundation/resident-tabs/bridge-client.d.ts +9 -3
  54. package/dist/foundation/resident-tabs/bridge-client.js +14 -7
  55. package/dist/foundation/resident-tabs/routes.d.ts +5 -0
  56. package/dist/foundation/resident-tabs/routes.js +5 -0
  57. package/dist/foundation/resident-tabs/settings.d.ts +0 -1
  58. package/dist/foundation/resident-tabs/settings.js +0 -2
  59. package/dist/integrations/pi/components/editor-interaction.d.ts +2 -0
  60. package/dist/integrations/pi/components/modal-frame.d.ts +17 -1
  61. package/dist/integrations/pi/components/modal-frame.js +48 -9
  62. package/dist/integrations/pi/components/models-dialog.js +4 -52
  63. package/dist/integrations/pi/components/owned-editor-ux.d.ts +3 -0
  64. package/dist/integrations/pi/components/owned-editor-ux.js +75 -4
  65. package/dist/integrations/pi/components/shell-editor-autocomplete.js +57 -37
  66. package/dist/integrations/pi/components/shell-footer-status.js +19 -3
  67. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -1
  68. package/dist/integrations/pi/components/shell-selectors-dialogs.js +4 -1
  69. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  70. package/dist/integrations/pi/components/skills-dialog.js +2 -2
  71. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -5
  72. package/dist/integrations/pi/components/upstream/components/owned-editor.js +15 -8
  73. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -3
  74. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +2 -2
  75. package/dist/integrations/pi/components/upstream/components/session-footer.js +26 -23
  76. package/dist/integrations/pi/components/upstream/components/session-selector.js +7 -6
  77. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
  78. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +9 -8
  79. package/dist/integrations/pi/components/upstream/components/tool-execution.js +6 -4
  80. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +8 -8
  81. package/dist/integrations/pi/components/upstream/components/tree-selector.js +10 -10
  82. package/dist/integrations/pi/components/upstream/history/editor-core.js +2 -0
  83. package/dist/integrations/pi/engine/adapter.js +7 -2
  84. package/dist/integrations/pi/engine/ports.d.ts +1 -1
  85. package/dist/integrations/pi/engine/ports.js +4 -0
  86. package/dist/integrations/pi/engine/workflow-runner.js +9 -2
  87. package/dist/native/darwin-arm64/manifest.json +1 -1
  88. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  89. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  90. package/dist/native/darwin-x64/manifest.json +1 -1
  91. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  92. package/dist/native/darwin-x64/terminal-host.node +0 -0
  93. package/dist/native/linux-x64/manifest.json +1 -1
  94. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  95. package/dist/native/linux-x64/terminal-host.node +0 -0
  96. package/dist/native/win32-x64/manifest.json +2 -2
  97. package/dist/native/win32-x64/process-guardian.node +0 -0
  98. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  99. package/dist/native/win32-x64/terminal-host.node +0 -0
  100. package/dist/product-identity.d.ts +1 -1
  101. package/dist/product-identity.js +1 -1
  102. package/dist/product-identity.json +1 -0
  103. package/dist/ui/apps/host.d.ts +2 -0
  104. package/dist/ui/apps/host.js +7 -1
  105. package/dist/ui/components/dialog-panel.js +35 -30
  106. package/dist/ui/components/line-input.d.ts +3 -0
  107. package/dist/ui/components/line-input.js +3 -1
  108. package/dist/ui/components/prompt-input.d.ts +5 -4
  109. package/dist/ui/components/prompt-input.js +8 -6
  110. package/dist/ui/components/submitted-prompt.d.ts +2 -2
  111. package/dist/ui/components/submitted-prompt.js +3 -3
  112. package/dist/ui/components/surface.js +4 -2
  113. package/dist/ui/components/transcript-viewport.d.ts +1 -1
  114. package/dist/ui/components/transcript-viewport.js +141 -24
  115. package/dist/ui/settings/declarations.d.ts +65 -15
  116. package/dist/ui/settings/declarations.js +45 -13
  117. package/dist/ui/settings/manager.js +7 -3
  118. package/dist/ui/settings/migrations.js +22 -0
  119. package/dist/ui/settings/sections.d.ts +4 -0
  120. package/dist/ui/settings/sections.js +9 -3
  121. package/docs/architecture/resident-tabs.md +39 -11
  122. package/docs/architecture/tool-image-presentation.md +2 -1
  123. package/docs/features/prompt-history.md +41 -20
  124. package/package.json +1 -1
@@ -0,0 +1,76 @@
1
+ import { LineInput, displayWidth, handleLineInputKey, renderInputRow, truncateToWidth, } from "../../ui/components/index.js";
2
+ /** The A1 setting holding the tab kinds the strip's `+` offers. */
3
+ export const PLUS_MENU_SETTING_ID = "tabBarPlusMenu";
4
+ const TITLE = "Plus Menu";
5
+ const DESCRIPTION = "Choose what the + button in the tab bar can add.";
6
+ const HINT = "Type search · ↑↓ navigate · Space toggle · Esc close";
7
+ /**
8
+ * The plus-menu dialog: every tab kind with `●` when `+` offers it and `○` when not, filtered by typed
9
+ * text. It owns only its filter and the row in hand; the enabled record and its saving stay with the
10
+ * Settings screen that opened it.
11
+ */
12
+ export class PlusMenuDialog {
13
+ #kinds;
14
+ #filter = new LineInput("");
15
+ #index = 0;
16
+ constructor(kinds) {
17
+ this.#kinds = kinds;
18
+ }
19
+ /** The kinds the filter keeps, in kind order. */
20
+ shown() {
21
+ const needle = this.#filter.value.trim().toLowerCase();
22
+ return this.#kinds.filter(kind => needle.length === 0 || kind.label.toLowerCase().includes(needle));
23
+ }
24
+ /** Handles one key by its resolved name; `toggle` flips a kind. Returns whether the dialog closed. */
25
+ handle(name, data, toggle) {
26
+ const shown = this.shown();
27
+ if (name === "escape")
28
+ return "close";
29
+ if (name === "up" || name === "down") {
30
+ this.#index = Math.min(Math.max(0, shown.length - 1), Math.max(0, this.#index + (name === "down" ? 1 : -1)));
31
+ return "handled";
32
+ }
33
+ if (name === "space") {
34
+ const kind = shown[this.#index];
35
+ if (kind !== undefined)
36
+ toggle(kind.key);
37
+ return "handled";
38
+ }
39
+ handleLineInputKey(this.#filter, data);
40
+ this.#index = 0;
41
+ return "handled";
42
+ }
43
+ lines(enabled, width, theme) {
44
+ const rule = theme.fg("border", "─".repeat(Math.max(0, width)));
45
+ const inset = width > 0 ? " " : "";
46
+ const contentWidth = Math.max(0, width - inset.length);
47
+ const row = (painted, raw) => {
48
+ const clipped = truncateToWidth(painted, contentWidth);
49
+ return `${inset}${clipped}${" ".repeat(Math.max(0, contentWidth - displayWidth(raw)))}`;
50
+ };
51
+ const shown = this.shown();
52
+ const index = Math.min(this.#index, Math.max(0, shown.length - 1));
53
+ const rows = shown.length === 0
54
+ ? [row(theme.fg("dim", "No tabs match"), "No tabs match")]
55
+ : shown.map((kind, position) => {
56
+ const selected = position === index;
57
+ const cursor = selected ? "→ " : " ";
58
+ const marker = enabled[kind.key] === true ? "●" : "○";
59
+ const raw = `${cursor}${marker} ${kind.label}`;
60
+ const content = `${selected ? theme.fg("accent", cursor) : cursor}${theme.fg("text", marker)} ${theme.fg("text", kind.label)}`;
61
+ return row(selected ? theme.highlight(content) : content, raw);
62
+ });
63
+ return [
64
+ rule,
65
+ row(theme.bold(theme.fg("accent", TITLE)), TITLE),
66
+ row(theme.fg("dim", DESCRIPTION), DESCRIPTION),
67
+ "",
68
+ ...renderInputRow(this.#filter, width, { ruled: false, promptGlyph: "> ", theme }).lines,
69
+ "",
70
+ ...rows,
71
+ "",
72
+ row(theme.fg("dim", HINT), HINT),
73
+ rule,
74
+ ];
75
+ }
76
+ }
@@ -24,6 +24,8 @@ export interface ReferenceScreenOptions {
24
24
  readonly document: ReferenceDocumentProvider;
25
25
  /** The reader's scroll settings, read on every frame so a live change is honored. */
26
26
  readonly scrollSettings: () => OwnedUiViewportSettings;
27
+ /** The screen is a resident tab, such as the Changelog tab: it offers no screen-level close. */
28
+ readonly pinned?: boolean;
27
29
  }
28
30
  /**
29
31
  * Presents one read-only document full screen between two accent rules: the bold
@@ -48,6 +48,7 @@ export class ReferenceScreenApp {
48
48
  #title;
49
49
  #document;
50
50
  #scrollSettings;
51
+ #pinned;
51
52
  #scroll = 0;
52
53
  #interruptArmed = false;
53
54
  #failure = null;
@@ -67,6 +68,7 @@ export class ReferenceScreenApp {
67
68
  this.#title = options.title;
68
69
  this.#document = options.document;
69
70
  this.#scrollSettings = options.scrollSettings;
71
+ this.#pinned = options.pinned === true;
70
72
  }
71
73
  onActivate(host) {
72
74
  const ready = this.#document.ready;
@@ -139,7 +141,7 @@ export class ReferenceScreenApp {
139
141
  const withRail = withScrollbarRail(body.slice(0, bodyHeight), geometry, contentWidth, theme, { presentation });
140
142
  const hint = this.#interruptArmed
141
143
  ? theme.fg("dim", " press ctrl+c again to exit a1")
142
- : renderShortcutHintsWithClose(REFERENCE_SCREEN_SHORTCUTS.hintEntries(SCOPE), theme, rect.width, 1);
144
+ : renderShortcutHintsWithClose(this.#hintEntries(), theme, rect.width, 1);
143
145
  // Compatibility: the v2 reference screen frames its document between two border-coloured rules.
144
146
  const rule = theme.fg("border", "─".repeat(rect.width));
145
147
  // Invariant: dialog chrome shares the title's one-cell inset; document content keeps its own layout.
@@ -153,7 +155,8 @@ export class ReferenceScreenApp {
153
155
  return { consumed: false };
154
156
  switch (action) {
155
157
  case "close":
156
- host.close();
158
+ if (!this.#pinned)
159
+ host.close();
157
160
  return { consumed: true };
158
161
  case "up":
159
162
  this.#scrollBy(-1);
@@ -216,6 +219,13 @@ export class ReferenceScreenApp {
216
219
  }
217
220
  return { owned: true, changed: true };
218
221
  }
222
+ // Invariant: a pinned screen is its tab and `Esc` does not close it, so it shows no close hint.
223
+ #hintEntries() {
224
+ const entries = REFERENCE_SCREEN_SHORTCUTS.hintEntries(SCOPE);
225
+ if (!this.#pinned)
226
+ return entries;
227
+ return entries.filter(entry => entry.key !== DIALOG_CLOSE_SHORTCUT_HINT.key || entry.action !== DIALOG_CLOSE_SHORTCUT_HINT.action);
228
+ }
219
229
  // Invariant: the title is the first document row, as in v2, so it scrolls with the document.
220
230
  #content(width, theme) {
221
231
  // Compatibility: one leading space, so the title lines up with the padded Markdown rows as in v2.
@@ -1,8 +1,9 @@
1
1
  import type { AppHostServices, UiApp } from "../../ui/apps/index.js";
2
2
  import { ShortcutRegistry, type PaneInputResult, type PaneMouseEvent, type PaneRect } from "../../ui/components/index.js";
3
3
  import type { OwnedSettingsManager } from "../../ui/settings/index.js";
4
+ import type { SettingsSearchInputFactory } from "./settings-search-input.js";
4
5
  export { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
5
- type Action = "move-up" | "move-down" | "block-up" | "block-down" | "first" | "last" | "page-up" | "page-down" | "previous-value" | "next-value" | "activate" | "open-filter" | "close" | "undo" | "part-previous" | "part-next" | "part-change";
6
+ type Action = "move-up" | "move-down" | "block-up" | "block-down" | "first" | "last" | "page-up" | "page-down" | "previous-value" | "next-value" | "activate" | "edit-filter" | "close" | "undo" | "part-previous" | "part-next" | "part-change";
6
7
  export declare const SETTINGS_SHORTCUTS: ShortcutRegistry<Action>;
7
8
  /**
8
9
  * The settings screen. Rows, sticky headers, scrolling, and the keymap come from
@@ -12,9 +13,15 @@ export declare const SETTINGS_SHORTCUTS: ShortcutRegistry<Action>;
12
13
  export declare class SettingsApp implements UiApp {
13
14
  #private;
14
15
  readonly id = "settings";
15
- /** `pinned` presents the screen as a resident Settings tab, which offers no screen-level close. */
16
- constructor(session: OwnedSettingsManager, options?: {
16
+ /**
17
+ * `pinned` presents the screen as a resident Settings tab, which offers no screen-level close;
18
+ * `configurePlusMenu` opens the tab bar's Plus Menu dialog in place of this screen's own.
19
+ */
20
+ constructor(session: OwnedSettingsManager, options: {
17
21
  readonly pinned?: boolean;
22
+ readonly searchInputFactory: SettingsSearchInputFactory;
23
+ readonly copyText?: (text: string) => void;
24
+ readonly configurePlusMenu?: (() => boolean) | null;
18
25
  });
19
26
  onActivate(host: AppHostServices): void;
20
27
  onClose(_host: AppHostServices): void;
@@ -1,5 +1,6 @@
1
1
  import { DIALOG_BACK_SHORTCUT_HINT, DIALOG_CLOSE_SHORTCUT_HINT } from "../../contracts/presentation/index.js";
2
- import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, numericValues, RAIL_COLUMNS, renderDialogPanel, renderSteppedDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderListRow, renderNote, renderShortcutHintsWithClose, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, formatLocalTime, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
2
+ import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, numericValues, RAIL_COLUMNS, renderDialogPanel, renderSteppedDialogPanel, renderEmptyState, renderGroupHeader, renderListRow, renderNote, renderShortcutHintsWithClose, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, formatLocalTime, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
3
+ import { PLUS_MENU_SETTING_ID, PlusMenuDialog } from "./plus-menu-dialog.js";
3
4
  import { SETTINGS_APP_ID } from "./settings-route.js";
4
5
  export { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
5
6
  const SCOPE = SETTINGS_APP_ID;
@@ -19,11 +20,10 @@ const RAIL_KEY = "settings";
19
20
  // Compatibility: the transcript rail stays lit this long after a scroll, and repaints just after.
20
21
  const SCROLL_LINGER_MS = 900;
21
22
  const SCROLL_LINGER_REPAINT_MS = 925;
22
- const SEARCH_PLACEHOLDER = "search settings";
23
23
  /** What a structured value offers instead of printing itself. */
24
24
  const CONFIGURE = "configure";
25
25
  export const SETTINGS_SHORTCUTS = new ShortcutRegistry();
26
- SETTINGS_SHORTCUTS.declare({ key: "/", scope: SCOPE, description: "Search settings", section: "Change", hint: { keys: "/", does: "search" } }, "open-filter");
26
+ SETTINGS_SHORTCUTS.declare({ key: "edit", scope: SCOPE, description: "Search settings", section: "Change", hint: { keys: "Type", does: "search" } }, "edit-filter");
27
27
  SETTINGS_SHORTCUTS.declare({ key: "up", scope: SCOPE, description: "Previous setting", section: "Navigate", hint: { keys: "↑↓", does: "navigate" } }, "move-up");
28
28
  SETTINGS_SHORTCUTS.declare({ key: "down", scope: SCOPE, description: "Next setting", section: "Navigate", hint: { keys: "↑↓", does: "navigate" } }, "move-down");
29
29
  SETTINGS_SHORTCUTS.declare({ key: "shift+up", scope: SCOPE, description: "Previous section", section: "Navigate", hint: { keys: "Shift+↑↓", does: "jump" } }, "block-up");
@@ -78,7 +78,6 @@ const KEYS = {
78
78
  "\r": "enter",
79
79
  "\n": "enter",
80
80
  " ": "space",
81
- "/": "/",
82
81
  "\u001a": "ctrl+z",
83
82
  };
84
83
  /**
@@ -94,8 +93,7 @@ export class SettingsApp {
94
93
  // Invariant: keyboard navigation requests visibility once; pointer scrolling then stays free.
95
94
  #selectionNeedsReveal = true;
96
95
  #reveal;
97
- #filter = null;
98
- #scrollBeforeFilter = null;
96
+ #search;
99
97
  #menu = null;
100
98
  #structured = null;
101
99
  // Invariant: pending values remain visible until the source reflects them.
@@ -120,11 +118,20 @@ export class SettingsApp {
120
118
  #activeUntil = 0;
121
119
  #activityTimer;
122
120
  #renderedScroll;
123
- #pinned;
124
- /** `pinned` presents the screen as a resident Settings tab, which offers no screen-level close. */
125
- constructor(session, options = {}) {
121
+ #options;
122
+ /**
123
+ * `pinned` presents the screen as a resident Settings tab, which offers no screen-level close;
124
+ * `configurePlusMenu` opens the tab bar's Plus Menu dialog in place of this screen's own.
125
+ */
126
+ constructor(session, options) {
126
127
  this.#session = session;
127
- this.#pinned = options.pinned === true;
128
+ this.#options = { pinned: options.pinned === true, configurePlusMenu: options.configurePlusMenu ?? null };
129
+ this.#search = {
130
+ input: null,
131
+ scrollBefore: null,
132
+ factory: options.searchInputFactory,
133
+ copyText: options.copyText ?? (() => { }),
134
+ };
128
135
  }
129
136
  onActivate(host) {
130
137
  this.#closed = false;
@@ -134,6 +141,8 @@ export class SettingsApp {
134
141
  this.#closed = true;
135
142
  this.#undoHistory.length = 0;
136
143
  this.#undoRequests = 0;
144
+ this.#search.input?.setFocused(false);
145
+ this.#search.input = null;
137
146
  this.#clearActivityTimer();
138
147
  this.#rails.clear();
139
148
  }
@@ -144,7 +153,7 @@ export class SettingsApp {
144
153
  const footer = this.#footerLines(rect.width, theme);
145
154
  // Invariant: the shared search input and structured panel each begin with their own standard rule.
146
155
  // That rule replaces the ordinary content/footer divider instead of stacking beneath it.
147
- const dividerRows = this.#filter === null && this.#structured === null ? SETTINGS_FOOTER_DIVIDER_ROWS : 0;
156
+ const dividerRows = this.#search.input === null && this.#structured === null ? SETTINGS_FOOTER_DIVIDER_ROWS : 0;
148
157
  const topRows = Math.min(SETTINGS_TOP_RULE_ROWS, rect.height);
149
158
  const contentHeight = Math.max(0, rect.height - topRows - dividerRows - footer.length);
150
159
  let titleRows = this.#scroll <= 0 ? Math.min(SETTINGS_TITLE_ROWS, contentHeight) : 0;
@@ -205,7 +214,7 @@ export class SettingsApp {
205
214
  const body = [];
206
215
  this.#frameRows = [];
207
216
  if (rows.length === 0) {
208
- body.push(...renderEmptyState("No settings found.", "👀", bodyHeight, contentWidth, theme));
217
+ body.push(...renderEmptyState("No settings found", "👀", bodyHeight, contentWidth, theme));
209
218
  }
210
219
  else {
211
220
  if (layout.topPadding > 0)
@@ -240,27 +249,32 @@ export class SettingsApp {
240
249
  return frame.slice(0, rect.height).concat(Array(Math.max(0, rect.height - frame.length)).fill(""));
241
250
  }
242
251
  onInput(data, host) {
252
+ if (data === "\u0003" && this.#search.input === null)
253
+ return { consumed: false };
243
254
  if (this.#structured !== null)
244
255
  return this.#structuredKey(data);
245
256
  if (this.#menu !== null)
246
257
  return this.#menuKey(data);
247
- if (this.#filter !== null)
258
+ if (this.#search.input !== null)
248
259
  return this.#filterKey(data);
249
- const key = KEYS[data] ?? data;
250
- const action = SETTINGS_SHORTCUTS.resolve(key, SCOPE);
251
- if (action === null)
252
- return { consumed: false };
260
+ const key = KEYS[data];
261
+ const action = key === undefined ? null : SETTINGS_SHORTCUTS.resolve(key, SCOPE);
262
+ if (action === null) {
263
+ if (!this.#search.factory.isTextInput(data))
264
+ return { consumed: false };
265
+ openSettingsSearch(this.#search, host, data, this.#scroll);
266
+ this.#scroll = 0;
267
+ return { consumed: true };
268
+ }
253
269
  const rows = this.#rows();
254
270
  const selected = indexOfKey(rows, this.#selectedKey);
255
271
  switch (action) {
256
272
  case "close":
257
- if (!this.#pinned)
273
+ if (!this.#options.pinned)
258
274
  host.close();
259
275
  return { consumed: true };
260
- case "open-filter":
261
- this.#scrollBeforeFilter = this.#scroll;
262
- this.#filter = new LineInput("");
263
- return { consumed: true };
276
+ case "edit-filter":
277
+ return { consumed: false };
264
278
  case "activate": {
265
279
  const row = rows[selected];
266
280
  if (row?.kind === "element" && row.value.structured)
@@ -310,6 +324,9 @@ export class SettingsApp {
310
324
  }
311
325
  }
312
326
  onMouse(event, _host) {
327
+ const searchPointer = handleSettingsSearchPointer(this.#search, event, this.#panelTopForFrame);
328
+ if (searchPointer !== null)
329
+ return searchPointer;
313
330
  const menu = this.#menu;
314
331
  const frame = this.#menuFrame;
315
332
  if (menu !== null && frame !== null) {
@@ -349,13 +366,13 @@ export class SettingsApp {
349
366
  // Invariant: the whole list pane owns wheel scrolling, including blank space beside
350
367
  // short labels. A fixed structured dialog keeps that content interaction available.
351
368
  const screenRow = event.row - 1;
352
- const wheelBottom = this.#filter === null
369
+ const wheelBottom = this.#search.input === null
353
370
  ? this.#bodyTopForFrame + this.#bodyHeightForFrame
354
371
  : this.#panelTopForFrame + SETTINGS_SEARCH_INPUT_ROWS + SETTINGS_STATUS_ROWS;
355
372
  if (screenRow >= 0 && screenRow < wheelBottom) {
356
373
  const distance = scrollbarWheelRows(this.#scrollbarSpeed());
357
- if (this.#filter !== null)
358
- this.#scrollBeforeFilter = null;
374
+ if (this.#search.input !== null)
375
+ this.#search.scrollBefore = null;
359
376
  this.#scroll = Math.max(0, this.#scroll + (event.kind === "wheel-down" ? distance : -distance));
360
377
  return { consumed: true };
361
378
  }
@@ -491,6 +508,9 @@ export class SettingsApp {
491
508
  // flag still has a row, showing the default the source would apply.
492
509
  if (entry.flags.length === 0)
493
510
  return;
511
+ // Rationale: beside a tab bar, its own Plus Menu dialog configures `+`, so both ways look the same.
512
+ if (entry.backend === "a1" && entry.id === PLUS_MENU_SETTING_ID && this.#options.configurePlusMenu?.() === true)
513
+ return;
494
514
  const stored = typeof entry.rawValue === "object" && entry.rawValue !== null && !Array.isArray(entry.rawValue)
495
515
  ? entry.rawValue
496
516
  : {};
@@ -511,6 +531,7 @@ export class SettingsApp {
511
531
  entry,
512
532
  flags: entry.flags.map(flag => flag.key),
513
533
  modelDialog,
534
+ plusMenu: entry.backend === "a1" && entry.id === PLUS_MENU_SETTING_ID ? new PlusMenuDialog(entry.flags) : null,
514
535
  filter: modelDialog ? new LineInput("") : null,
515
536
  step: "model",
516
537
  modelKey: null,
@@ -525,6 +546,12 @@ export class SettingsApp {
525
546
  return { consumed: false };
526
547
  if (open.modelDialog)
527
548
  return this.#modelDialogKey(open, data);
549
+ if (open.plusMenu !== null) {
550
+ const outcome = open.plusMenu.handle(KEYS[data] ?? data, data, key => this.#toggleFlag(open.flags.indexOf(key)));
551
+ if (outcome === "close")
552
+ this.#structured = null;
553
+ return { consumed: true };
554
+ }
528
555
  switch (SETTINGS_SHORTCUTS.resolve(KEYS[data] ?? data, DIALOG_SCOPE)) {
529
556
  case "close":
530
557
  this.#structured = null;
@@ -679,18 +706,18 @@ export class SettingsApp {
679
706
  return { consumed: true };
680
707
  }
681
708
  #filterKey(data) {
682
- const input = this.#filter;
709
+ const input = this.#search.input;
683
710
  if (input === null)
684
711
  return { consumed: false };
685
- const key = KEYS[data];
686
- if (SETTINGS_SHORTCUTS.resolve(key ?? data, SCOPE) === "undo") {
687
- this.#undo();
712
+ if (data === "\u001b" || (data === "\u0003" && !input.hasSelection())) {
713
+ this.#scroll = closeSettingsSearch(this.#search) ?? this.#scroll;
688
714
  return { consumed: true };
689
715
  }
716
+ const key = KEYS[data];
690
717
  // Rationale: the boundary chords jump through the results; plain Home and End stay with
691
- // the search cursor, which the shared line input moves below.
718
+ // the search cursor, which the shared interactive input moves below.
692
719
  if (key === "ctrl+home" || key === "ctrl+end") {
693
- this.#scrollBeforeFilter = null;
720
+ this.#search.scrollBefore = null;
694
721
  const rows = this.#rows();
695
722
  const selectable = selectableIndexes(rows);
696
723
  const target = key === "ctrl+end" ? selectable.at(-1) : selectable[0];
@@ -702,7 +729,7 @@ export class SettingsApp {
702
729
  return { consumed: true };
703
730
  }
704
731
  if (key === "up" || key === "down" || key === "shift+up" || key === "shift+down") {
705
- this.#scrollBeforeFilter = null;
732
+ this.#search.scrollBefore = null;
706
733
  const rows = this.#rows();
707
734
  // Rationale: nothing found means nothing to move through; the key is still swallowed
708
735
  // rather than typed into the search.
@@ -719,15 +746,10 @@ export class SettingsApp {
719
746
  this.#select(rows, moveSelection(rows, selected, forward ? 1 : -1));
720
747
  return { consumed: true };
721
748
  }
722
- const outcome = handleLineInputKey(input, data);
723
- if (outcome.kind === "cancelled") {
724
- this.#filter = null;
725
- if (this.#scrollBeforeFilter !== null)
726
- this.#scroll = this.#scrollBeforeFilter;
727
- this.#scrollBeforeFilter = null;
728
- }
729
- else {
730
- this.#scrollBeforeFilter = null;
749
+ const before = input.getText();
750
+ input.handleInput(data);
751
+ if (input.getText() !== before) {
752
+ this.#search.scrollBefore = null;
731
753
  this.#scroll = 0;
732
754
  }
733
755
  return { consumed: true };
@@ -862,7 +884,7 @@ export class SettingsApp {
862
884
  this.#selectionNeedsReveal = true;
863
885
  }
864
886
  #rows() {
865
- const needle = this.#filter?.value.trim().toLowerCase() ?? "";
887
+ const needle = this.#search.input?.getText().trim().toLowerCase() ?? "";
866
888
  const matches = (entry) => needle.length === 0
867
889
  || entry.id.toLowerCase().includes(needle)
868
890
  || labelOf(entry).toLowerCase().includes(needle);
@@ -964,6 +986,8 @@ export class SettingsApp {
964
986
  #dialogLines(open, width, theme) {
965
987
  if (open.modelDialog)
966
988
  return this.#modelDialogLines(open, width, theme);
989
+ if (open.plusMenu !== null)
990
+ return open.plusMenu.lines(open.record, width, theme);
967
991
  const rows = open.flags.map(key => {
968
992
  const declared = open.entry.flags.find(flag => flag.key === key);
969
993
  return {
@@ -1020,15 +1044,44 @@ export class SettingsApp {
1020
1044
  return this.#dialogLines(open, width, theme);
1021
1045
  // Rationale: standing guidance is more useful than transient feedback; failures still
1022
1046
  // roll back without displacing the Settings interaction contract.
1023
- const entries = SETTINGS_SHORTCUTS.hintEntries(SCOPE).filter(entry => !this.#pinned
1047
+ const entries = SETTINGS_SHORTCUTS.hintEntries(SCOPE).filter(entry => !this.#options.pinned
1024
1048
  || entry.key !== DIALOG_CLOSE_SHORTCUT_HINT.key || entry.action !== DIALOG_CLOSE_SHORTCUT_HINT.action);
1025
1049
  const status = renderShortcutHintsWithClose(entries, theme, width, width > 0 ? 1 : 0);
1026
- const input = this.#filter;
1050
+ const input = this.#search.input;
1027
1051
  if (input === null)
1028
1052
  return [status];
1029
- return [...renderInputRow(input, width, { placeholder: SEARCH_PLACEHOLDER, theme }).lines, status];
1053
+ return [...input.render(width), status];
1030
1054
  }
1031
1055
  }
1056
+ function openSettingsSearch(state, host, initial, scroll) {
1057
+ state.scrollBefore = scroll;
1058
+ state.input = state.factory.create({
1059
+ theme: host.theme ?? PLAIN_THEME,
1060
+ getRows: () => host.getSize().height,
1061
+ requestRender: () => host.requestRender(),
1062
+ copyText: state.copyText,
1063
+ });
1064
+ state.input.setFocused(true);
1065
+ state.input.handleInput(initial);
1066
+ }
1067
+ function closeSettingsSearch(state) {
1068
+ state.input?.setFocused(false);
1069
+ state.input = null;
1070
+ const scroll = state.scrollBefore;
1071
+ state.scrollBefore = null;
1072
+ return scroll;
1073
+ }
1074
+ function handleSettingsSearchPointer(state, event, panelTop) {
1075
+ const input = state.input;
1076
+ if (input === null || event.kind === "wheel-up" || event.kind === "wheel-down")
1077
+ return null;
1078
+ const row = event.row - panelTop;
1079
+ const overInput = row >= 1 && row <= SETTINGS_SEARCH_INPUT_ROWS;
1080
+ if (!overInput && !input.ownsPointer())
1081
+ return null;
1082
+ const handled = input.handlePointer({ kind: event.kind, button: event.button, column: event.column, row });
1083
+ return { consumed: handled || overInput, render: handled };
1084
+ }
1032
1085
  function modelLabelParts(label) {
1033
1086
  const match = /^(.*?)(\s+\[[^\]]+\])$/u.exec(label);
1034
1087
  return match === null ? { label } : { label: match[1] ?? label, labelSuffix: match[2] ?? "" };
@@ -0,0 +1,29 @@
1
+ import type { UiTheme } from "../../ui/components/index.js";
2
+ export interface SettingsSearchPointerEvent {
3
+ readonly kind: "press" | "motion" | "release";
4
+ readonly button: number;
5
+ readonly column: number;
6
+ /** One-based row relative to the search input's top rule. */
7
+ readonly row: number;
8
+ }
9
+ /** The shared interactive input as consumed by the Settings surface. */
10
+ export interface SettingsSearchInput {
11
+ render(width: number): readonly string[];
12
+ handleInput(data: string): void;
13
+ handlePointer(event: SettingsSearchPointerEvent): boolean;
14
+ getText(): string;
15
+ hasSelection(): boolean;
16
+ ownsPointer(): boolean;
17
+ setFocused(focused: boolean): void;
18
+ }
19
+ export interface SettingsSearchInputOptions {
20
+ readonly theme: UiTheme;
21
+ readonly getRows: () => number;
22
+ readonly requestRender: () => void;
23
+ readonly copyText: (text: string) => void;
24
+ }
25
+ /** Composition supplies the same configurable editor component used by the agent prompt. */
26
+ export interface SettingsSearchInputFactory {
27
+ create(options: SettingsSearchInputOptions): SettingsSearchInput;
28
+ isTextInput(data: string): boolean;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
1
  export { PromptHistoryService } from "./service.js";
2
- export { resolvePromptHistoryPath } from "./paths.js";
2
+ export { resolvePromptHistoryPath, type PromptHistoryScope } from "./paths.js";
3
3
  export { PromptImageSidecar, collectImageChipIdentifiers } from "./image-sidecar.js";
4
4
  export type { PromptImageSidecarRecord, PromptImageSidecarAttachment } from "./image-sidecar.js";
@@ -1,4 +1,5 @@
1
- export declare function resolvePromptHistoryPath(dataDir: string, profileRoot: string, platform?: NodeJS.Platform): {
1
+ export type PromptHistoryScope = "unique" | "shared";
2
+ export declare function resolvePromptHistoryPath(dataDir: string, profileRoot: string, platform?: NodeJS.Platform, scope?: PromptHistoryScope, sessionId?: string): {
2
3
  path: string;
3
4
  profileId: string;
4
5
  imagesDir: string;
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { posix, win32 } from "node:path";
3
3
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
4
- export function resolvePromptHistoryPath(dataDir, profileRoot, platform = process.platform) {
4
+ export function resolvePromptHistoryPath(dataDir, profileRoot, platform = process.platform, scope = "shared", sessionId) {
5
5
  const paths = platform === "win32" ? win32 : posix;
6
6
  if (!paths.isAbsolute(dataDir) || !paths.isAbsolute(profileRoot) || dataDir.includes("\0") || profileRoot.includes("\0"))
7
7
  throw new TypeError("History requires absolute roots");
@@ -11,7 +11,15 @@ export function resolvePromptHistoryPath(dataDir, profileRoot, platform = proces
11
11
  if (platform === "win32")
12
12
  normalized = normalized.replaceAll("\\", "/").toLowerCase();
13
13
  const digest = createHash("sha256").update(JSON.stringify([1, PRODUCT_IDENTITY.commandName, normalized])).digest("hex");
14
- const profileId = `${PRODUCT_IDENTITY.filesystem.temporaryPrefix}${digest}`;
14
+ if (scope !== "unique" && scope !== "shared")
15
+ throw new TypeError("Invalid history scope");
16
+ if (scope === "unique" && (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > 256 || sessionId.includes("\0"))) {
17
+ throw new TypeError("Unique history requires a valid session ID");
18
+ }
19
+ // Compatibility: preserve the former Shared identity; a separate domain prevents
20
+ // collisions with per-session databases and image sidecars.
21
+ const profileId = scope === "shared" ? `${PRODUCT_IDENTITY.filesystem.temporaryPrefix}${digest}`
22
+ : `${PRODUCT_IDENTITY.filesystem.temporaryPrefix}${createHash("sha256").update(JSON.stringify([2, PRODUCT_IDENTITY.commandName, normalized, sessionId])).digest("hex")}`;
15
23
  const historyDir = paths.join(dataDir, "history");
16
24
  return { path: paths.join(historyDir, `${profileId}.sqlite3`), profileId, imagesDir: paths.join(historyDir, `${profileId}-images`) };
17
25
  }
@@ -1,5 +1,6 @@
1
1
  import type { Worker } from "node:worker_threads";
2
2
  import { type PromptHistoryFailure, type PromptHistoryPort, type PromptHistoryResult, type PromptHistorySnapshot, type PromptHistorySubmission } from "../../contracts/owned-ui/index.js";
3
+ import { type PromptHistoryScope } from "./paths.js";
3
4
  export type HistoryRecoveryState = "starting" | "ready" | "recovering" | "blocked-storage" | "closing" | "closed";
4
5
  export interface HistoryClock {
5
6
  now(): number;
@@ -13,6 +14,8 @@ export declare class PromptHistoryService implements PromptHistoryPort {
13
14
  dataDir: string;
14
15
  profileRoot: string;
15
16
  limit: number;
17
+ scope?: PromptHistoryScope;
18
+ sessionId?: string;
16
19
  createWorker?: () => Worker;
17
20
  clock?: HistoryClock;
18
21
  random?: () => number;
@@ -21,6 +24,8 @@ export declare class PromptHistoryService implements PromptHistoryPort {
21
24
  dataDir: string;
22
25
  profileRoot: string;
23
26
  limit: number;
27
+ scope?: PromptHistoryScope;
28
+ sessionId?: string;
24
29
  createWorker?: () => Worker;
25
30
  clock?: HistoryClock;
26
31
  random?: () => number;
@@ -129,7 +129,7 @@ export class PromptHistoryService {
129
129
  const { Worker } = await import("node:worker_threads");
130
130
  if (this.#isClosed() || generation !== this.#generation)
131
131
  return;
132
- const location = resolvePromptHistoryPath(this.options.dataDir, this.options.profileRoot);
132
+ const location = resolvePromptHistoryPath(this.options.dataDir, this.options.profileRoot, process.platform, this.options.scope, this.options.sessionId);
133
133
  const source = import.meta.url.endsWith(".ts");
134
134
  const entry = new URL(source ? "./worker.ts" : "./worker.js", import.meta.url);
135
135
  const options = { workerData: { ...location, limit: this.options.limit }, stdout: true, stderr: true };
@@ -1,4 +1,4 @@
1
- export type ResidentTabAction = "tabs.new" | "tabs.close" | "tabs.rename" | "tabs.settings" | "tabs.retry" | "tabs.fresh" | "tabs.next" | "tabs.previous" | "tabs.moveRight" | "tabs.moveLeft" | `tabs.jump${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10}`;
1
+ export type ResidentTabAction = "tabs.new" | "tabs.close" | "tabs.rename" | "tabs.settings" | "tabs.changelog" | "tabs.commands" | "tabs.retry" | "tabs.fresh" | "tabs.next" | "tabs.previous" | "tabs.moveRight" | "tabs.moveLeft" | `tabs.jump${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10}`;
2
2
  export interface ResidentTabShortcut {
3
3
  readonly id: ResidentTabAction;
4
4
  readonly defaultKey: string | null;
@@ -14,6 +14,8 @@ export const RESIDENT_TAB_SHORTCUTS = Object.freeze([
14
14
  { id: "tabs.close", defaultKey: "alt+w", scope: "global", description: "Close A1 tab" },
15
15
  { id: "tabs.rename", defaultKey: "f2", scope: "global", description: "Rename A1 tab" },
16
16
  { id: "tabs.settings", defaultKey: "alt+,", scope: "global", description: "Open Settings tab" },
17
+ { id: "tabs.changelog", defaultKey: null, scope: "global", description: "Open Changelog tab" },
18
+ { id: "tabs.commands", defaultKey: "alt+/", scope: "global", description: "Open command palette" },
17
19
  ...jumps,
18
20
  { id: "tabs.next", defaultKey: null, scope: "global", description: "View next A1 tab" },
19
21
  { id: "tabs.previous", defaultKey: null, scope: "global", description: "View previous A1 tab" },
@@ -7,6 +7,7 @@ export declare const PRIVATE_ENVIRONMENT: Readonly<{
7
7
  releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS";
8
8
  launchProfile: "LAUNCH_CONTEXT_PROFILE";
9
9
  launchMode: "LAUNCH_CONTEXT_MODE";
10
+ updateVersion: "LAUNCH_CONTEXT_UPDATE_VERSION";
10
11
  immutableWarmup: "LAUNCH_CONTEXT_WARMUP";
11
12
  residentRootExecutable: "LAUNCH_CONTEXT_RESIDENT_ROOT_EXECUTABLE";
12
13
  residentRootArguments: "LAUNCH_CONTEXT_RESIDENT_ROOT_ARGUMENTS";
@@ -35,6 +36,7 @@ export interface LaunchContext {
35
36
  readonly releaseLayers?: string;
36
37
  readonly launchProfile?: "a1" | "pi";
37
38
  readonly launchMode?: "tabs" | "solo";
39
+ readonly updateVersion?: string;
38
40
  readonly immutableWarmup?: "1";
39
41
  readonly residentRootExecutable?: string;
40
42
  readonly residentRootArguments?: string;
@@ -7,6 +7,7 @@ export const PRIVATE_ENVIRONMENT = Object.freeze({
7
7
  releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS",
8
8
  launchProfile: "LAUNCH_CONTEXT_PROFILE",
9
9
  launchMode: "LAUNCH_CONTEXT_MODE",
10
+ updateVersion: "LAUNCH_CONTEXT_UPDATE_VERSION",
10
11
  immutableWarmup: "LAUNCH_CONTEXT_WARMUP",
11
12
  residentRootExecutable: "LAUNCH_CONTEXT_RESIDENT_ROOT_EXECUTABLE",
12
13
  residentRootArguments: "LAUNCH_CONTEXT_RESIDENT_ROOT_ARGUMENTS",