@timurproko/a1 0.1.1-dev.6 → 0.1.1-dev.7

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 (151) hide show
  1. package/README.md +32 -9
  2. package/bin/a1-guardian.js +22 -0
  3. package/bin/a1-ui.js +7 -4
  4. package/dist/native/darwin-arm64/a1-process-guardian +0 -0
  5. package/dist/native/darwin-arm64/manifest.json +20 -0
  6. package/dist/native/linux-x64/a1-process-guardian +0 -0
  7. package/dist/native/linux-x64/manifest.json +20 -0
  8. package/dist/native/win32-x64/a1-process-guardian.exe +0 -0
  9. package/dist/native/win32-x64/manifest.json +20 -0
  10. package/dist/src/composition/index.d.ts +26 -0
  11. package/dist/src/composition/index.js +88 -2
  12. package/dist/src/features/launch/runtime-selection.d.ts +2 -1
  13. package/dist/src/features/launch/runtime-selection.js +2 -2
  14. package/dist/src/features/owned-ui/index.d.ts +1 -0
  15. package/dist/src/features/owned-ui/index.js +1 -0
  16. package/dist/src/features/owned-ui/run.d.ts +6 -0
  17. package/dist/src/features/owned-ui/run.js +3 -1
  18. package/dist/src/features/owned-ui/settings-app.d.ts +22 -0
  19. package/dist/src/features/owned-ui/settings-app.js +631 -0
  20. package/dist/src/foundation/agent-engine-contracts/domain.d.ts +26 -0
  21. package/dist/src/foundation/launch-guardian/index.d.ts +1 -0
  22. package/dist/src/foundation/launch-guardian/index.js +1 -0
  23. package/dist/src/foundation/launch-guardian/main.d.ts +31 -0
  24. package/dist/src/foundation/launch-guardian/main.js +165 -0
  25. package/dist/src/foundation/lifecycle/commands.d.ts +40 -0
  26. package/dist/src/foundation/lifecycle/commands.js +1 -0
  27. package/dist/src/foundation/lifecycle/index.d.ts +2 -0
  28. package/dist/src/foundation/lifecycle/index.js +2 -0
  29. package/dist/src/foundation/lifecycle/launch-instance.d.ts +46 -0
  30. package/dist/src/foundation/lifecycle/launch-instance.js +134 -0
  31. package/dist/src/foundation/lifecycle/model.d.ts +6 -42
  32. package/dist/src/foundation/owned-ui-settings/declarations.d.ts +13 -0
  33. package/dist/src/foundation/owned-ui-settings/declarations.js +39 -0
  34. package/dist/src/foundation/owned-ui-settings/index.d.ts +6 -0
  35. package/dist/src/foundation/owned-ui-settings/index.js +6 -0
  36. package/dist/src/foundation/owned-ui-settings/migrations.d.ts +13 -0
  37. package/dist/src/foundation/owned-ui-settings/migrations.js +28 -0
  38. package/dist/src/foundation/owned-ui-settings/resolution.d.ts +45 -0
  39. package/dist/src/foundation/owned-ui-settings/resolution.js +147 -0
  40. package/dist/src/foundation/owned-ui-settings/sections.d.ts +70 -0
  41. package/dist/src/foundation/owned-ui-settings/sections.js +93 -0
  42. package/dist/src/foundation/owned-ui-settings/session.d.ts +44 -0
  43. package/dist/src/foundation/owned-ui-settings/session.js +145 -0
  44. package/dist/src/foundation/owned-ui-settings/store.d.ts +28 -0
  45. package/dist/src/foundation/owned-ui-settings/store.js +90 -0
  46. package/dist/src/foundation/pi-component-adapter/index.d.ts +1 -0
  47. package/dist/src/foundation/pi-component-adapter/index.js +1 -0
  48. package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.d.ts +9 -1
  49. package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.js +10 -8
  50. package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +12 -3
  51. package/dist/src/foundation/pi-engine-adapter/adapter.js +73 -7
  52. package/dist/src/foundation/pi-engine-adapter/pi-settings-metadata.json +374 -0
  53. package/dist/src/foundation/pi-engine-adapter/public-main-entry.js +10 -1
  54. package/dist/src/foundation/pi-engine-adapter/settings-integration.d.ts +42 -2
  55. package/dist/src/foundation/pi-engine-adapter/settings-integration.js +185 -24
  56. package/dist/src/foundation/pi-engine-adapter/workflows.d.ts +9 -0
  57. package/dist/src/foundation/pi-owned-ui-integration/index.d.ts +1 -0
  58. package/dist/src/foundation/pi-owned-ui-integration/index.js +1 -0
  59. package/dist/src/foundation/pi-owned-ui-integration/route-host.d.ts +29 -0
  60. package/dist/src/foundation/pi-owned-ui-integration/route-host.js +1 -0
  61. package/dist/src/foundation/pi-owned-ui-integration/session-shell.d.ts +6 -0
  62. package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +71 -1
  63. package/dist/src/foundation/pi-tui-runtime-adapter/adapter.d.ts +6 -0
  64. package/dist/src/foundation/pi-tui-runtime-adapter/adapter.js +9 -0
  65. package/dist/src/foundation/process-containment/artifact.d.ts +1 -0
  66. package/dist/src/foundation/process-containment/artifact.js +34 -0
  67. package/dist/src/foundation/process-containment/contracts.d.ts +31 -0
  68. package/dist/src/foundation/process-containment/contracts.js +35 -0
  69. package/dist/src/foundation/process-containment/index.d.ts +5 -0
  70. package/dist/src/foundation/process-containment/index.js +5 -0
  71. package/dist/src/foundation/process-containment/linux-process-inspector.d.ts +6 -0
  72. package/dist/src/foundation/process-containment/linux-process-inspector.js +26 -0
  73. package/dist/src/foundation/process-containment/native-guardian-containment.d.ts +16 -0
  74. package/dist/src/foundation/process-containment/native-guardian-containment.js +139 -0
  75. package/dist/src/foundation/process-containment/windows-process-inspector.d.ts +17 -0
  76. package/dist/src/foundation/process-containment/windows-process-inspector.js +53 -0
  77. package/dist/src/foundation/protocol/client.d.ts +2 -1
  78. package/dist/src/foundation/protocol/client.js +3 -0
  79. package/dist/src/foundation/protocol/messages.d.ts +5 -5
  80. package/dist/src/foundation/protocol/messages.js +1 -1
  81. package/dist/src/foundation/release/bootstrap.js +29 -10
  82. package/dist/src/foundation/release/cohort-selection.js +3 -3
  83. package/dist/src/foundation/release/cohort-state.d.ts +3 -2
  84. package/dist/src/foundation/release/process-cleanup.d.ts +1 -1
  85. package/dist/src/foundation/release/process-cleanup.js +2 -2
  86. package/dist/src/foundation/release/update.js +2 -2
  87. package/dist/src/foundation/storage/control-store.d.ts +8 -7
  88. package/dist/src/foundation/storage/control-store.js +105 -55
  89. package/dist/src/foundation/supervision/server.d.ts +4 -2
  90. package/dist/src/foundation/supervision/server.js +254 -87
  91. package/dist/src/foundation/transparent-terminal/command-resolution.js +6 -3
  92. package/dist/src/foundation/transparent-terminal/foreground-broker.d.ts +5 -13
  93. package/dist/src/foundation/transparent-terminal/foreground-broker.js +11 -68
  94. package/dist/src/foundation/transparent-terminal/main.js +1 -8
  95. package/dist/src/foundation/ui-apps/contracts.d.ts +42 -0
  96. package/dist/src/foundation/ui-apps/contracts.js +12 -0
  97. package/dist/src/foundation/ui-apps/host.d.ts +42 -0
  98. package/dist/src/foundation/ui-apps/host.js +152 -0
  99. package/dist/src/foundation/ui-apps/index.d.ts +3 -0
  100. package/dist/src/foundation/ui-apps/index.js +3 -0
  101. package/dist/src/foundation/ui-apps/registry.d.ts +11 -0
  102. package/dist/src/foundation/ui-apps/registry.js +32 -0
  103. package/dist/src/foundation/ui-components/dialog-panel.d.ts +36 -0
  104. package/dist/src/foundation/ui-components/dialog-panel.js +47 -0
  105. package/dist/src/foundation/ui-components/frame.d.ts +21 -0
  106. package/dist/src/foundation/ui-components/frame.js +54 -0
  107. package/dist/src/foundation/ui-components/index.d.ts +18 -0
  108. package/dist/src/foundation/ui-components/index.js +18 -0
  109. package/dist/src/foundation/ui-components/label.d.ts +8 -0
  110. package/dist/src/foundation/ui-components/label.js +36 -0
  111. package/dist/src/foundation/ui-components/line-input.d.ts +74 -0
  112. package/dist/src/foundation/ui-components/line-input.js +245 -0
  113. package/dist/src/foundation/ui-components/list-block.d.ts +66 -0
  114. package/dist/src/foundation/ui-components/list-block.js +176 -0
  115. package/dist/src/foundation/ui-components/list-view.d.ts +66 -0
  116. package/dist/src/foundation/ui-components/list-view.js +76 -0
  117. package/dist/src/foundation/ui-components/mouse.d.ts +15 -0
  118. package/dist/src/foundation/ui-components/mouse.js +46 -0
  119. package/dist/src/foundation/ui-components/pane.d.ts +33 -0
  120. package/dist/src/foundation/ui-components/pane.js +7 -0
  121. package/dist/src/foundation/ui-components/revision.d.ts +35 -0
  122. package/dist/src/foundation/ui-components/revision.js +79 -0
  123. package/dist/src/foundation/ui-components/scrollbar.d.ts +62 -0
  124. package/dist/src/foundation/ui-components/scrollbar.js +102 -0
  125. package/dist/src/foundation/ui-components/shortcuts.d.ts +57 -0
  126. package/dist/src/foundation/ui-components/shortcuts.js +81 -0
  127. package/dist/src/foundation/ui-components/spans.d.ts +7 -0
  128. package/dist/src/foundation/ui-components/spans.js +59 -0
  129. package/dist/src/foundation/ui-components/status-line.d.ts +16 -0
  130. package/dist/src/foundation/ui-components/status-line.js +14 -0
  131. package/dist/src/foundation/ui-components/stepper.d.ts +24 -0
  132. package/dist/src/foundation/ui-components/stepper.js +33 -0
  133. package/dist/src/foundation/ui-components/surface.d.ts +20 -0
  134. package/dist/src/foundation/ui-components/surface.js +38 -0
  135. package/dist/src/foundation/ui-components/text.d.ts +24 -0
  136. package/dist/src/foundation/ui-components/text.js +123 -0
  137. package/dist/src/foundation/ui-components/theme.d.ts +17 -0
  138. package/dist/src/foundation/ui-components/theme.js +12 -0
  139. package/dist/src/foundation/ui-components/value-menu.d.ts +41 -0
  140. package/dist/src/foundation/ui-components/value-menu.js +33 -0
  141. package/dist/src/product-identity.d.ts +2 -2
  142. package/dist/src/product-identity.js +2 -1
  143. package/dist/src/product-identity.json +3 -0
  144. package/docs/architecture/boundaries.md +31 -13
  145. package/docs/architecture/process-guardian-provenance.md +26 -0
  146. package/docs/architecture/toolchain.md +6 -0
  147. package/docs/architecture/ui-reference-provenance.md +50 -0
  148. package/docs/ci-release-runbook.md +38 -42
  149. package/docs/features/launch-profiles.md +7 -1
  150. package/docs/manual-launch-instance-acceptance.md +66 -0
  151. package/package.json +11 -12
@@ -0,0 +1,631 @@
1
+ import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, PROMPT_GLYPH, blockJumpTarget, dialogValueColumn, numericValues, RAIL_COLUMNS, renderDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderStatusLine, renderListRow, renderNote, dialogRowAt, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, valueColumnFor, valueMenuFrame, promptRule, caretCell, blockRowSpan, displayWidth, faint, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, isThumbRow, layoutList, moveSelection, rowKey, scrollForSelection, scrollbarGeometry, selectableIndexes, } from "../../foundation/ui-components/index.js";
2
+ export const SETTINGS_APP_ID = "settings";
3
+ export const SETTINGS_ROUTE = "settings";
4
+ const SCOPE = SETTINGS_APP_ID;
5
+ /** The panel a setting with parts opens: its own keys, its own hint. */
6
+ const DIALOG_SCOPE = `${SETTINGS_APP_ID}-parts`;
7
+ const SCROLLBAR_TOP_INSET = 1;
8
+ const SEARCH_PLACEHOLDER = "search settings";
9
+ /** What a structured value offers instead of printing itself. */
10
+ const CONFIGURE = "configure";
11
+ export const SETTINGS_SHORTCUTS = new ShortcutRegistry();
12
+ SETTINGS_SHORTCUTS.declare({ key: "/", scope: SCOPE, description: "Search settings", section: "Change", hint: { keys: "/", does: "to search" } }, "open-filter");
13
+ SETTINGS_SHORTCUTS.declare({ key: "up", scope: SCOPE, description: "Previous setting", section: "Navigate", hint: { keys: "↑↓", does: "to navigate" } }, "move-up");
14
+ SETTINGS_SHORTCUTS.declare({ key: "down", scope: SCOPE, description: "Next setting", section: "Navigate", hint: { keys: "↑↓", does: "to navigate" } }, "move-down");
15
+ SETTINGS_SHORTCUTS.declare({ key: "shift+up", scope: SCOPE, description: "Previous section", section: "Navigate", hint: { keys: "Shift+↑↓", does: "to jump" } }, "block-up");
16
+ SETTINGS_SHORTCUTS.declare({ key: "shift+down", scope: SCOPE, description: "Next section", section: "Navigate", hint: { keys: "Shift+↑↓", does: "to jump" } }, "block-down");
17
+ SETTINGS_SHORTCUTS.declare({ key: "pageUp", scope: SCOPE, description: "Up a page", section: "Navigate" }, "page-up");
18
+ 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");
21
+ SETTINGS_SHORTCUTS.declare({ key: "enter", scope: SCOPE, description: "Change value", section: "Change", hint: { keys: "Enter/Space", does: "to change" } }, "activate");
22
+ SETTINGS_SHORTCUTS.declare({ key: "space", scope: SCOPE, description: "Change value", section: "Change", hint: { keys: "Enter/Space", does: "to change" } }, "activate");
23
+ SETTINGS_SHORTCUTS.declare({ key: "left", scope: SCOPE, description: "Previous value", section: "Change", hint: { keys: "←→", does: "to adjust" } }, "previous-value");
24
+ SETTINGS_SHORTCUTS.declare({ key: "right", scope: SCOPE, description: "Next value", section: "Change", hint: { keys: "←→", does: "to adjust" } }, "next-value");
25
+ SETTINGS_SHORTCUTS.declare({ key: "escape", scope: GLOBAL_SCOPE, description: "Close", section: "Screen", hint: { keys: "Esc", does: "to cancel" } }, "close");
26
+ SETTINGS_SHORTCUTS.declare({ key: "enter", scope: DIALOG_SCOPE, description: "Change this part", section: "Parts", hint: { keys: "Enter/Space", does: "to change" } }, "part-change");
27
+ SETTINGS_SHORTCUTS.declare({ key: "space", scope: DIALOG_SCOPE, description: "Change this part", section: "Parts", hint: { keys: "Enter/Space", does: "to change" } }, "part-change");
28
+ SETTINGS_SHORTCUTS.declare({ key: "left", scope: DIALOG_SCOPE, description: "Change this part", section: "Parts" }, "part-change");
29
+ SETTINGS_SHORTCUTS.declare({ key: "right", scope: DIALOG_SCOPE, description: "Change this part", section: "Parts" }, "part-change");
30
+ SETTINGS_SHORTCUTS.declare({ key: "up", scope: DIALOG_SCOPE, description: "Previous part", section: "Parts" }, "part-previous");
31
+ SETTINGS_SHORTCUTS.declare({ key: "down", scope: DIALOG_SCOPE, description: "Next part", section: "Parts" }, "part-next");
32
+ const KEYS = {
33
+ "\u001b[A": "up",
34
+ "\u001b[B": "down",
35
+ "\u001b[1;2A": "shift+up",
36
+ "\u001b[1;2B": "shift+down",
37
+ "\u001b[D": "left",
38
+ "\u001b[C": "right",
39
+ "\u001b[5~": "pageUp",
40
+ "\u001b[6~": "pageDown",
41
+ "\u001b[H": "home",
42
+ "\u001b[F": "end",
43
+ "\u001b": "escape",
44
+ "\r": "enter",
45
+ "\n": "enter",
46
+ " ": "space",
47
+ "/": "/",
48
+ };
49
+ /**
50
+ * The settings screen. Rows, sticky headers, scrolling, and the keymap come from
51
+ * the shared component layer; what belongs to settings is which sections exist,
52
+ * how a value is shown, and where an accepted change is routed.
53
+ */
54
+ export class SettingsApp {
55
+ id = SETTINGS_APP_ID;
56
+ #session;
57
+ #selectedKey;
58
+ #scroll = 0;
59
+ #reveal;
60
+ #notice = null;
61
+ #filter = null;
62
+ #menu = null;
63
+ #structured = null;
64
+ #loading = true;
65
+ #interruptArmed = false;
66
+ /** Values requested but not yet reflected by the source, keyed by entry. */
67
+ #pending = new Map();
68
+ #footerHeight = 1;
69
+ /** Column the dialog's values start at, so a label stays a label. */
70
+ #dialogValueColumn = 0;
71
+ /** Screen row the dialog panel starts on, for pointer hits. */
72
+ #panelTop = 0;
73
+ #panelTopForFrame = 0;
74
+ /** Row key under the pointer, and where each row was drawn last frame. */
75
+ #hoverKey = null;
76
+ #hoverRegion = "label";
77
+ #frameRows = [];
78
+ /** Where the value menu was drawn last frame, for hit testing. */
79
+ #menuFrame = null;
80
+ constructor(session) {
81
+ this.#session = session;
82
+ }
83
+ onActivate(host) {
84
+ void this.#session.load().then(() => {
85
+ this.#loading = false;
86
+ host.requestRender();
87
+ });
88
+ }
89
+ render(rect, host) {
90
+ const theme = host.theme ?? PLAIN_THEME;
91
+ this.#interruptArmed = host.interruptArmed;
92
+ const rows = this.#rows();
93
+ const footer = this.#footerLines(rect.width, theme);
94
+ this.#footerHeight = footer.length;
95
+ this.#panelTopForFrame = Math.max(0, rect.height - footer.length);
96
+ this.#panelTop = this.#panelTopForFrame;
97
+ const bodyHeight = Math.max(0, rect.height - footer.length);
98
+ const selected = indexOfKey(rows, this.#selectedKey);
99
+ this.#scroll = scrollForSelection(rows, bodyHeight, this.#scroll, selected, this.#reveal);
100
+ this.#reveal = undefined;
101
+ const layout = layoutList(rows, bodyHeight, this.#scroll);
102
+ const contentWidth = Math.max(0, rect.width - RAIL_COLUMNS);
103
+ const valueColumn = this.#valueColumn(rows);
104
+ const geometry = scrollbarGeometry({
105
+ contentLength: rows.length,
106
+ viewportHeight: layout.visible,
107
+ scroll: layout.scroll,
108
+ trackHeight: Math.max(0, bodyHeight - SCROLLBAR_TOP_INSET),
109
+ });
110
+ const body = [];
111
+ this.#frameRows = [];
112
+ if (rows.length === 0) {
113
+ const message = this.#loading ? "Loading settings…" : "No settings found.";
114
+ body.push(...renderEmptyState(message, "👀", bodyHeight, contentWidth, theme));
115
+ }
116
+ else {
117
+ if (layout.topPadding > 0)
118
+ body.push("");
119
+ if (layout.stickyHeader !== undefined)
120
+ body.push(this.#header(layout.stickyHeader, theme, contentWidth));
121
+ for (const index of layout.rowIndexes) {
122
+ const row = rows[index];
123
+ if (row !== undefined && row.kind === "element") {
124
+ this.#frameRows.push({
125
+ key: `${row.value.backend}:${row.value.id}`,
126
+ screenRow: body.length,
127
+ valueColumn,
128
+ valueWidth: row.value.structured
129
+ ? CONFIGURE.length
130
+ : row.value.value === null ? 0 : displayWidth(displayValue(row.value.value)),
131
+ stepper: isStepper(row.value),
132
+ });
133
+ }
134
+ body.push(this.#renderRow(row, index === selected, contentWidth, valueColumn, theme));
135
+ }
136
+ while (body.length < bodyHeight)
137
+ body.push("");
138
+ }
139
+ const withRail = withScrollbarRail(body.slice(0, bodyHeight), geometry, contentWidth, theme, {
140
+ topInset: SCROLLBAR_TOP_INSET,
141
+ });
142
+ return this.#withMenu([...withRail, ...footer], selected, layout, valueColumn, theme, rect);
143
+ }
144
+ onInput(data, host) {
145
+ if (this.#structured !== null)
146
+ return this.#structuredKey(data);
147
+ if (this.#menu !== null)
148
+ return this.#menuKey(data);
149
+ if (this.#filter !== null)
150
+ return this.#filterKey(data);
151
+ const action = SETTINGS_SHORTCUTS.resolve(KEYS[data] ?? data, SCOPE);
152
+ if (action === null)
153
+ return { consumed: false };
154
+ const rows = this.#rows();
155
+ const selected = indexOfKey(rows, this.#selectedKey);
156
+ switch (action) {
157
+ case "close":
158
+ host.close();
159
+ return { consumed: true };
160
+ case "open-filter":
161
+ this.#filter = new LineInput("");
162
+ this.#notice = null;
163
+ return { consumed: true };
164
+ case "activate": {
165
+ const row = rows[selected];
166
+ if (row?.kind === "element" && row.value.structured)
167
+ this.#openStructured(row.value);
168
+ else
169
+ this.#cycle(rows, selected, 1);
170
+ return { consumed: true };
171
+ }
172
+ case "move-up":
173
+ case "move-down":
174
+ this.#select(rows, moveSelection(rows, selected, action === "move-down" ? 1 : -1));
175
+ return { consumed: true };
176
+ case "page-up":
177
+ case "page-down":
178
+ this.#select(rows, moveSelection(rows, selected, action === "page-down" ? 8 : -8));
179
+ return { consumed: true };
180
+ case "block-up":
181
+ case "block-down": {
182
+ const target = blockJumpTarget(rows, selected, action === "block-down" ? 1 : -1);
183
+ if (target !== undefined)
184
+ this.#jump(rows, target);
185
+ return { consumed: true };
186
+ }
187
+ case "first":
188
+ case "last": {
189
+ // End lands on the very last setting, not on the head of its section.
190
+ const selectable = selectableIndexes(rows);
191
+ const target = action === "last" ? selectable.at(-1) : selectable[0];
192
+ if (target !== undefined)
193
+ this.#jump(rows, target);
194
+ return { consumed: true };
195
+ }
196
+ case "previous-value":
197
+ case "next-value":
198
+ this.#cycle(rows, selected, action === "next-value" ? 1 : -1);
199
+ return { consumed: true };
200
+ default:
201
+ return { consumed: false };
202
+ }
203
+ }
204
+ onMouse(event, _host) {
205
+ const menu = this.#menu;
206
+ const frame = this.#menuFrame;
207
+ if (menu !== null && frame !== null) {
208
+ const overRow = menuRowAt(frame, event.row - 1, event.column);
209
+ const overMenu = overRow !== null;
210
+ if (event.kind === "motion") {
211
+ // While the menu is open it owns the pointer: the row under it lights up,
212
+ // and nothing behind it hovers.
213
+ const hadHover = this.#hoverKey !== null;
214
+ this.#hoverKey = null;
215
+ if (!overMenu) {
216
+ const cleared = menu.index !== -1;
217
+ menu.index = -1;
218
+ return { consumed: true, render: hadHover || cleared };
219
+ }
220
+ if (menu.index === overRow)
221
+ return { consumed: true, render: hadHover };
222
+ menu.index = overRow ?? -1;
223
+ return { consumed: true, render: true };
224
+ }
225
+ if (event.kind !== "press")
226
+ return { consumed: true, render: false };
227
+ if (overRow === null) {
228
+ // A press anywhere else dismisses the menu rather than acting through it.
229
+ this.#menu = null;
230
+ return { consumed: true };
231
+ }
232
+ const value = menu.choices[overRow];
233
+ this.#menu = null;
234
+ if (value !== undefined)
235
+ this.#apply(menu.entry, value);
236
+ return { consumed: true };
237
+ }
238
+ const open = this.#structured;
239
+ if (open !== null) {
240
+ // The panel owns the pointer while it is open; its flag rows are the targets.
241
+ // The panel's rows begin one line below its rule.
242
+ const panel = { firstRow: this.#panelTop + 1, rows: open.flags.length, valueColumn: this.#dialogValueColumn };
243
+ const row = event.row - 1 - panel.firstRow;
244
+ if (row < 0 || row >= panel.rows)
245
+ return { consumed: true, render: false };
246
+ if (event.kind === "motion") {
247
+ if (open.index === row)
248
+ return { consumed: true, render: false };
249
+ open.index = row;
250
+ return { consumed: true, render: true };
251
+ }
252
+ if (event.kind === "press") {
253
+ open.index = row;
254
+ // Pointing at the label picks the row; the value is what changes it,
255
+ // exactly as in the list behind the dialog.
256
+ const key = open.flags[row] ?? "";
257
+ const width = displayWidth((open.record[key] ?? false) ? "true" : "false");
258
+ if (dialogRowAt(panel, event.row - 1, event.column, width) !== null)
259
+ this.#toggleFlag(row);
260
+ return { consumed: true };
261
+ }
262
+ return { consumed: true, render: false };
263
+ }
264
+ const row = this.#frameRows.find(candidate => candidate.screenRow === event.row - 1);
265
+ const previousKey = this.#hoverKey;
266
+ const previousRegion = this.#hoverRegion;
267
+ if (row === undefined) {
268
+ this.#hoverKey = null;
269
+ this.#hoverRegion = "label";
270
+ return { consumed: event.kind !== "motion", render: previousKey !== null };
271
+ }
272
+ this.#hoverKey = row.key;
273
+ this.#hoverRegion = regionAt(row, event.column);
274
+ if (event.kind === "press") {
275
+ const rows = this.#rows();
276
+ const index = rows.findIndex(candidate => rowKey(candidate) === row.key);
277
+ if (index >= 0) {
278
+ // The pointer acts where it points; the arrow belongs to the keyboard.
279
+ this.#notice = null;
280
+ if (this.#hoverRegion === "minus")
281
+ this.#cycle(rows, index, -1);
282
+ else if (this.#hoverRegion === "plus")
283
+ this.#cycle(rows, index, 1);
284
+ else if (this.#hoverRegion === "value")
285
+ this.#openMenu(rows, index);
286
+ }
287
+ return { consumed: true };
288
+ }
289
+ if (event.kind === "wheel-up" || event.kind === "wheel-down") {
290
+ this.#scroll = Math.max(0, this.#scroll + (event.kind === "wheel-down" ? 3 : -3));
291
+ return { consumed: true };
292
+ }
293
+ const changed = previousKey !== this.#hoverKey || previousRegion !== this.#hoverRegion;
294
+ return { consumed: event.kind !== "motion", render: changed };
295
+ }
296
+ #openMenu(rows, selected) {
297
+ const row = rows[selected];
298
+ if (row === undefined || row.kind !== "element")
299
+ return;
300
+ const entry = row.value;
301
+ if (entry.structured) {
302
+ this.#openStructured(entry);
303
+ return;
304
+ }
305
+ const shown = this.#shownValue(entry);
306
+ // A number is stepped, not picked from a list: it has its own two controls,
307
+ // and pointing at it is not a request for anything else.
308
+ if (typeof shown === "number")
309
+ return;
310
+ if (!entry.editable || entry.choices === null || entry.choices.length === 0) {
311
+ this.#notice = `${labelOf(entry)} cannot be changed here`;
312
+ return;
313
+ }
314
+ const current = shown === null ? 0 : Math.max(0, entry.choices.indexOf(shown));
315
+ // Nothing is highlighted until the pointer or a key picks a row, so opening
316
+ // the menu does not flash a highlight the reader did not ask for.
317
+ this.#menu = { entry, current, anchorKey: `${entry.backend}:${entry.id}`, choices: entry.choices, index: -1 };
318
+ }
319
+ /**
320
+ * A structured setting is a set of flags, so it opens as its own list rather
321
+ * than as a value menu: each row names one flag and toggles it in place.
322
+ */
323
+ #openStructured(entry) {
324
+ // The flags come from the declaration, not from the stored value: an unset
325
+ // flag still has a row, showing the default the source would apply.
326
+ if (entry.flags.length === 0) {
327
+ this.#notice = `${labelOf(entry)} has nothing to configure`;
328
+ return;
329
+ }
330
+ const stored = typeof entry.rawValue === "object" && entry.rawValue !== null && !Array.isArray(entry.rawValue)
331
+ ? entry.rawValue
332
+ : {};
333
+ const record = {};
334
+ for (const flag of entry.flags) {
335
+ const value = stored[flag.key];
336
+ record[flag.key] = typeof value === "boolean" ? value : flag.fallback;
337
+ }
338
+ // The dialog takes the screen: the row it was opened from stops being the
339
+ // thing under the pointer, so it stops looking like it.
340
+ this.#hoverKey = null;
341
+ this.#hoverRegion = "label";
342
+ this.#structured = { entry, flags: entry.flags.map(flag => flag.key), index: 0, record };
343
+ }
344
+ #structuredKey(data) {
345
+ const open = this.#structured;
346
+ if (open === null)
347
+ return { consumed: false };
348
+ switch (SETTINGS_SHORTCUTS.resolve(KEYS[data] ?? data, DIALOG_SCOPE)) {
349
+ case "close":
350
+ this.#structured = null;
351
+ return { consumed: true };
352
+ case "part-previous":
353
+ open.index = Math.max(0, open.index - 1);
354
+ return { consumed: true };
355
+ case "part-next":
356
+ open.index = Math.min(open.flags.length - 1, open.index + 1);
357
+ return { consumed: true };
358
+ case "part-change":
359
+ this.#toggleFlag(open.index);
360
+ return { consumed: true };
361
+ default:
362
+ return { consumed: true, render: false };
363
+ }
364
+ }
365
+ #toggleFlag(index) {
366
+ const open = this.#structured;
367
+ const flag = open?.flags[index];
368
+ if (open === null || open === undefined || flag === undefined)
369
+ return;
370
+ open.record[flag] = !(open.record[flag] ?? false);
371
+ const next = { ...open.record };
372
+ void this.#session.changeStructured(open.entry.backend, open.entry.id, next).then(outcome => {
373
+ this.#notice = outcome.failure === null ? null : `Could not save ${labelOf(open.entry)}: ${outcome.failure}`;
374
+ });
375
+ }
376
+ #menuKey(data) {
377
+ const menu = this.#menu;
378
+ if (menu === null)
379
+ return { consumed: false };
380
+ const key = KEYS[data] ?? data;
381
+ if (key === "escape") {
382
+ this.#menu = null;
383
+ return { consumed: true };
384
+ }
385
+ if (key === "up" || key === "down") {
386
+ // The keyboard starts from the value in effect rather than from the top.
387
+ menu.index = menu.index < 0
388
+ ? menu.current
389
+ : Math.min(menu.choices.length - 1, Math.max(0, menu.index + (key === "down" ? 1 : -1)));
390
+ }
391
+ else if (key === "enter") {
392
+ const value = menu.choices[menu.index];
393
+ this.#menu = null;
394
+ if (value !== undefined)
395
+ this.#apply(menu.entry, value);
396
+ }
397
+ return { consumed: true };
398
+ }
399
+ #filterKey(data) {
400
+ const input = this.#filter;
401
+ if (input === null)
402
+ return { consumed: false };
403
+ const key = KEYS[data];
404
+ if (key === "up" || key === "down" || key === "shift+up" || key === "shift+down") {
405
+ const rows = this.#rows();
406
+ // Nothing found means nothing to move through; the key is still swallowed
407
+ // rather than typed into the search.
408
+ if (selectableIndexes(rows).length === 0)
409
+ return { consumed: true, render: false };
410
+ const selected = indexOfKey(rows, this.#selectedKey);
411
+ const forward = key === "down" || key === "shift+down";
412
+ if (key === "shift+up" || key === "shift+down") {
413
+ const target = blockJumpTarget(rows, selected, forward ? 1 : -1);
414
+ if (target !== undefined)
415
+ this.#jump(rows, target);
416
+ return { consumed: true };
417
+ }
418
+ this.#select(rows, moveSelection(rows, selected, forward ? 1 : -1));
419
+ return { consumed: true };
420
+ }
421
+ const outcome = handleLineInputKey(input, data);
422
+ if (outcome.kind === "cancelled")
423
+ this.#filter = null;
424
+ this.#scroll = 0;
425
+ return { consumed: true };
426
+ }
427
+ #cycle(rows, selected, delta) {
428
+ const row = rows[selected];
429
+ if (row === undefined || row.kind !== "element")
430
+ return;
431
+ const entry = row.value;
432
+ if (!entry.editable) {
433
+ this.#notice = `${labelOf(entry)} cannot be changed here`;
434
+ return;
435
+ }
436
+ const shown = this.#shownValue(entry);
437
+ if (typeof shown === "number") {
438
+ // At the end of the range there is nothing to say: the arrow already reads
439
+ // as unavailable, so a message would only repeat it.
440
+ const next = steppedValue(rangeOf(entry), shown, delta);
441
+ if (next !== null)
442
+ this.#apply(entry, next);
443
+ return;
444
+ }
445
+ const choices = entry.choices;
446
+ if (choices === null || choices.length === 0) {
447
+ this.#notice = `${labelOf(entry)} cannot be changed here`;
448
+ return;
449
+ }
450
+ const current = shown === null ? -1 : choices.indexOf(shown);
451
+ const next = choices[current < 0
452
+ ? (delta > 0 ? 0 : choices.length - 1)
453
+ : (current + delta + choices.length) % choices.length];
454
+ if (next !== undefined)
455
+ this.#apply(entry, next);
456
+ }
457
+ #apply(entry, value) {
458
+ const key = `${entry.backend}:${entry.id}`;
459
+ // Shown immediately so the row never lags a keypress, and so the next press
460
+ // steps from here rather than from a value the source has not caught up to.
461
+ this.#pending.set(key, value);
462
+ void this.#session.change(entry.backend, entry.id, value).then(outcome => {
463
+ if (outcome.failure !== null) {
464
+ this.#pending.delete(key);
465
+ this.#notice = `Could not save ${labelOf(entry)}: ${outcome.failure}`;
466
+ return;
467
+ }
468
+ // A later press may have moved on; only the last request clears itself.
469
+ if (this.#pending.get(key) === value)
470
+ this.#pending.delete(key);
471
+ this.#notice = outcome.pendingRestart ? `${labelOf(entry)} applies on the next start` : null;
472
+ });
473
+ }
474
+ /** What the row shows: the value asked for if one is outstanding, else the source's. */
475
+ #shownValue(entry) {
476
+ return this.#pending.get(`${entry.backend}:${entry.id}`) ?? entry.value;
477
+ }
478
+ #jump(rows, target) {
479
+ this.#select(rows, target);
480
+ this.#reveal = blockRowSpan(rows, target);
481
+ }
482
+ #select(rows, index) {
483
+ if (index < 0)
484
+ return;
485
+ this.#selectedKey = rowKey(rows[index]);
486
+ this.#notice = null;
487
+ }
488
+ #rows() {
489
+ const needle = this.#filter?.value.trim().toLowerCase() ?? "";
490
+ const matches = (entry) => needle.length === 0
491
+ || entry.id.toLowerCase().includes(needle)
492
+ || labelOf(entry).toLowerCase().includes(needle);
493
+ const rows = [];
494
+ for (const section of this.#session.sections()) {
495
+ // A section named by the search is what the reader asked for, so it arrives
496
+ // whole rather than narrowed to the entries that happen to repeat its name.
497
+ const named = needle.length > 0 && section.title.toLowerCase().includes(needle);
498
+ const entries = named ? section.entries : section.entries.filter(matches);
499
+ if (needle.length > 0 && entries.length === 0)
500
+ continue;
501
+ if (rows.length > 0)
502
+ rows.push({ kind: "spacer" });
503
+ rows.push({ kind: "group", group: section.id, title: section.title });
504
+ if (section.unavailableReason !== null) {
505
+ rows.push({ kind: "note", group: section.id, text: section.unavailableReason });
506
+ continue;
507
+ }
508
+ if (section.readOnlyReason !== null && (needle.length === 0 || named)) {
509
+ rows.push({ kind: "note", group: section.id, text: section.readOnlyReason });
510
+ }
511
+ // Presented in the order the source reports, which is the order the
512
+ // pinned engine shows and is neither declaration order nor alphabetical.
513
+ for (const entry of entries) {
514
+ rows.push({
515
+ kind: "element",
516
+ group: section.id,
517
+ key: `${entry.backend}:${entry.id}`,
518
+ selectable: entry.editable,
519
+ value: entry,
520
+ });
521
+ }
522
+ }
523
+ return rows;
524
+ }
525
+ #valueColumn(rows) {
526
+ const shown = rows.flatMap(row => (row.kind === "element" ? [this.#viewRow(row.value)] : []));
527
+ return valueColumnFor(shown);
528
+ }
529
+ #header(title, theme, width) {
530
+ return renderGroupHeader(humanizeTitle(title), width, theme);
531
+ }
532
+ #renderRow(row, selected, width, valueColumn, theme) {
533
+ if (row === undefined || row.kind === "spacer")
534
+ return "";
535
+ if (row.kind === "group")
536
+ return this.#header(row.title, theme, width);
537
+ if (row.kind === "note")
538
+ return renderNote(row.text, width, theme);
539
+ const entry = row.value;
540
+ const key = `${entry.backend}:${entry.id}`;
541
+ const hovered = this.#hoverKey === key;
542
+ return renderListRow(this.#viewRow(entry), { selected, hovered, region: this.#hoverRegion }, valueColumn, width, theme);
543
+ }
544
+ /** What the list view needs to draw a setting: its words, and where it can go. */
545
+ #viewRow(entry) {
546
+ const shown = this.#shownValue(entry);
547
+ const value = entry.structured
548
+ ? CONFIGURE
549
+ : shown === null ? describeRaw(entry.rawValue) : displayValue(shown);
550
+ const range = rangeOf(entry);
551
+ return {
552
+ key: `${entry.backend}:${entry.id}`,
553
+ label: labelOf(entry),
554
+ value,
555
+ ...(entry.origin === "default" ? { suffix: "(default)" } : {}),
556
+ ...(typeof shown === "number" && entry.editable ? { stepper: stepperEnds(range, shown) } : {}),
557
+ };
558
+ }
559
+ #withMenu(lines, _selected, _layout, valueColumn, theme, rect) {
560
+ const menu = this.#menu;
561
+ const anchor = menu === null ? undefined : this.#frameRows.find(candidate => candidate.key === menu.anchorKey);
562
+ if (menu === null || anchor === undefined) {
563
+ this.#menuFrame = null;
564
+ return lines;
565
+ }
566
+ const state = {
567
+ choices: menu.choices.map(choice => displayValue(choice)),
568
+ current: this.#shownValue(menu.entry) === null ? null : displayValue(this.#shownValue(menu.entry)),
569
+ index: menu.index,
570
+ };
571
+ const frame = valueMenuFrame(state, { screenRow: anchor.screenRow, valueColumn }, {
572
+ bodyHeight: lines.length - this.#footerHeight,
573
+ surfaceWidth: rect.width,
574
+ reservedRight: RAIL_COLUMNS,
575
+ });
576
+ this.#menuFrame = frame;
577
+ return renderValueMenu(lines, state, frame, theme);
578
+ }
579
+ /** What the panel needs to present a structured setting's parts. */
580
+ #dialogLines(open, width, theme) {
581
+ const rows = open.flags.map(key => {
582
+ const declared = open.entry.flags.find(flag => flag.key === key);
583
+ return {
584
+ label: declared?.label ?? humanizeLabel(key),
585
+ // The engine writes these as the booleans they are rather than as yes/no.
586
+ value: (open.record[key] ?? false) ? "true" : "false",
587
+ ...(declared?.description === undefined ? {} : { description: declared.description }),
588
+ };
589
+ });
590
+ this.#dialogValueColumn = dialogValueColumn(rows);
591
+ this.#panelTop = this.#panelTopForFrame;
592
+ return renderDialogPanel({ rows, index: open.index, hint: SETTINGS_SHORTCUTS.hint(DIALOG_SCOPE) }, width, theme);
593
+ }
594
+ #footerLines(width, theme) {
595
+ const open = this.#structured;
596
+ if (open !== null)
597
+ return this.#dialogLines(open, width, theme);
598
+ // The hint is the declarations, so a key cannot be described here and bound
599
+ // to something else.
600
+ const hint = this.#interruptArmed ? "press ctrl+c again to exit A1" : SETTINGS_SHORTCUTS.hint(SCOPE);
601
+ const status = renderStatusLine({ hint, report: this.#notice }, width, theme);
602
+ const input = this.#filter;
603
+ if (input === null)
604
+ return [status];
605
+ return [...renderInputRow(input, width, { placeholder: SEARCH_PLACEHOLDER }).lines, status];
606
+ }
607
+ }
608
+ /** The source's own wording when it has one, otherwise the id made readable. */
609
+ function labelOf(entry) {
610
+ return entry.label ?? humanizeLabel(entry.id);
611
+ }
612
+ function isStepper(entry, shown = entry.value) {
613
+ return typeof shown === "number" && entry.editable;
614
+ }
615
+ /** Where a setting's number may go: what the engine states, or what it offers. */
616
+ function rangeOf(entry) {
617
+ return { minimum: entry.minimum, maximum: entry.maximum, values: numericValues(entry.choices) };
618
+ }
619
+ /** Booleans read as yes and no; everything else prints as itself. */
620
+ function displayValue(value) {
621
+ if (typeof value === "boolean")
622
+ return value ? "yes" : "no";
623
+ return String(value);
624
+ }
625
+ function describeRaw(value) {
626
+ if (value === null || value === undefined)
627
+ return "unset";
628
+ if (typeof value === "object")
629
+ return Array.isArray(value) ? `${value.length} items` : "structured value";
630
+ return String(value);
631
+ }
@@ -53,6 +53,32 @@ export interface AgentSettingDescriptor {
53
53
  readonly valueType: "boolean" | "number" | "string" | "enum" | "json";
54
54
  readonly writable: boolean;
55
55
  readonly choices?: readonly AgentJsonValue[];
56
+ /** Label the engine shows for this setting, when it has one. */
57
+ readonly label?: string;
58
+ /** One-line explanation the engine shows for this setting. */
59
+ readonly description?: string;
60
+ /**
61
+ * True when the values this setting offers are resolved as it is read rather
62
+ * than declared: what is installed, what the running session supports. The
63
+ * boundary resolves them; nothing above it supplies them.
64
+ */
65
+ readonly resolvedWhenRead?: boolean;
66
+ /** Smallest value a numeric setting accepts, when the engine states one. */
67
+ readonly minimum?: number;
68
+ /** Largest value a numeric setting accepts, when the engine states one. */
69
+ readonly maximum?: number;
70
+ /**
71
+ * Flags a structured setting offers. Declared by the source rather than read
72
+ * from the stored value, so an unset flag still has a row and a default.
73
+ */
74
+ readonly flags?: readonly AgentSettingFlag[];
75
+ }
76
+ export interface AgentSettingFlag {
77
+ readonly key: string;
78
+ readonly label: string;
79
+ readonly description: string;
80
+ /** Value to show when the stored object says nothing about this flag. */
81
+ readonly fallback: boolean;
56
82
  }
57
83
  export interface AgentResourceDescriptor {
58
84
  readonly id: string;
@@ -0,0 +1 @@
1
+ export * from "./main.js";
@@ -0,0 +1 @@
1
+ export * from "./main.js";