@xingular/harness 1.2.0 → 1.3.1-unstable.1

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 (186) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +26 -17
  3. package/builtin/skills/plugin-authoring/SKILL.md +34 -0
  4. package/dist/cli.js +7362 -6239
  5. package/dist/sdk/agents/builtin.js +3 -3
  6. package/dist/sdk/agents/planResearch.d.ts +23 -1
  7. package/dist/sdk/agents/planResearch.js +71 -8
  8. package/dist/sdk/agents/profiles.d.ts +16 -4
  9. package/dist/sdk/agents/profiles.js +75 -12
  10. package/dist/sdk/annotations/bridge.d.ts +2 -0
  11. package/dist/sdk/annotations/bridge.js +4 -0
  12. package/dist/sdk/annotations/capture.d.ts +44 -2
  13. package/dist/sdk/annotations/capture.js +104 -3
  14. package/dist/sdk/annotations/delivery.d.ts +24 -17
  15. package/dist/sdk/annotations/delivery.js +82 -49
  16. package/dist/sdk/annotations/inspect.js +7 -6
  17. package/dist/sdk/annotations/pageScript.js +4 -4
  18. package/dist/sdk/annotations/part.d.ts +0 -2
  19. package/dist/sdk/annotations/part.js +4 -33
  20. package/dist/sdk/annotations/queue.d.ts +2 -2
  21. package/dist/sdk/annotations/queue.js +12 -7
  22. package/dist/sdk/annotations/serverMethods.js +11 -7
  23. package/dist/sdk/annotations/store.d.ts +13 -21
  24. package/dist/sdk/annotations/store.js +47 -37
  25. package/dist/sdk/capabilities/runSnapshot.js +10 -4
  26. package/dist/sdk/commands/list.js +2 -0
  27. package/dist/sdk/commands/registry.d.ts +11 -3
  28. package/dist/sdk/commands/registry.js +3 -2
  29. package/dist/sdk/core/links.d.ts +51 -0
  30. package/dist/sdk/core/links.js +155 -0
  31. package/dist/sdk/core/shell.d.ts +20 -0
  32. package/dist/sdk/core/shell.js +73 -0
  33. package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
  34. package/dist/sdk/core/workspaceBashTool.js +16 -2
  35. package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
  36. package/dist/sdk/core/workspaceSandbox.js +52 -12
  37. package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
  38. package/dist/sdk/core/workspaceWriteOperations.js +26 -8
  39. package/dist/sdk/doctor/checks.js +71 -12
  40. package/dist/sdk/harness.d.ts +5 -0
  41. package/dist/sdk/harness.js +11 -6
  42. package/dist/sdk/hub/hub.js +5 -3
  43. package/dist/sdk/index.d.ts +3 -2
  44. package/dist/sdk/index.js +2 -1
  45. package/dist/sdk/plugins/builtin.js +14 -9
  46. package/dist/sdk/plugins/builtinRegistry.d.ts +3 -1
  47. package/dist/sdk/plugins/builtinRegistry.js +9 -3
  48. package/dist/sdk/plugins/discover.d.ts +7 -1
  49. package/dist/sdk/plugins/discover.js +41 -2
  50. package/dist/sdk/plugins/identity.js +1 -1
  51. package/dist/sdk/plugins/install.d.ts +6 -2
  52. package/dist/sdk/plugins/install.js +29 -5
  53. package/dist/sdk/plugins/inventory.js +3 -3
  54. package/dist/sdk/plugins/manifest.d.ts +2 -0
  55. package/dist/sdk/plugins/manifest.js +21 -4
  56. package/dist/sdk/plugins/plugins.d.ts +2 -0
  57. package/dist/sdk/plugins/plugins.js +1 -0
  58. package/dist/sdk/plugins/proposals.d.ts +17 -0
  59. package/dist/sdk/plugins/proposals.js +147 -0
  60. package/dist/sdk/plugins/types.d.ts +1 -1
  61. package/dist/sdk/plugins/types.js +1 -1
  62. package/dist/sdk/prompt/compile.d.ts +13 -4
  63. package/dist/sdk/prompt/compile.js +46 -20
  64. package/dist/sdk/prompt/standard.d.ts +1 -1
  65. package/dist/sdk/prompt/standard.js +1 -1
  66. package/dist/sdk/server/methods.js +14 -2
  67. package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
  68. package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
  69. package/dist/sdk/sessions/continuation.d.ts +4 -2
  70. package/dist/sdk/sessions/continuation.js +7 -6
  71. package/dist/sdk/sessions/contracts.d.ts +22 -4
  72. package/dist/sdk/sessions/contracts.js +12 -7
  73. package/dist/sdk/sessions/planMode.js +2 -2
  74. package/dist/sdk/sessions/runStore.js +3 -1
  75. package/dist/sdk/sessions/runner.js +2 -2
  76. package/dist/sdk/sessions/sessionState.d.ts +13 -0
  77. package/dist/sdk/sessions/sessionState.js +31 -0
  78. package/dist/sdk/sessions/sessions.d.ts +7 -0
  79. package/dist/sdk/sessions/sessions.js +73 -15
  80. package/dist/sdk/sessions/subagentService.d.ts +15 -0
  81. package/dist/sdk/sessions/subagentService.js +195 -55
  82. package/dist/sdk/sessions/subagents.d.ts +10 -1
  83. package/dist/sdk/sessions/subagents.js +4 -0
  84. package/dist/sdk/sessions/workSlice.d.ts +8 -5
  85. package/dist/sdk/sessions/workSlice.js +77 -45
  86. package/dist/sdk/terminal/controller.d.ts +20 -0
  87. package/dist/sdk/terminal/controller.js +43 -18
  88. package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
  89. package/dist/sdk/terminal/opentui/ansi.js +192 -0
  90. package/dist/sdk/terminal/opentui/model.d.ts +163 -0
  91. package/dist/sdk/terminal/opentui/model.js +482 -0
  92. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
  93. package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
  94. package/dist/sdk/terminal/opentui/theme.d.ts +2 -0
  95. package/dist/sdk/terminal/opentui/theme.js +2 -0
  96. package/dist/sdk/terminal/piExtension.js +98 -15
  97. package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
  98. package/dist/sdk/terminal/subagentMonitor.js +29 -8
  99. package/dist/sdk/terminal/terminals.d.ts +16 -0
  100. package/dist/sdk/terminal/terminals.js +130 -26
  101. package/dist/sdk/terminal/trajectory.d.ts +6 -1
  102. package/dist/sdk/terminal/trajectory.js +8 -0
  103. package/dist/sdk/terminal/tui/annotateCommand.d.ts +5 -1
  104. package/dist/sdk/terminal/tui/annotateCommand.js +21 -7
  105. package/dist/sdk/terminal/tui/annotateSend.d.ts +30 -0
  106. package/dist/sdk/terminal/tui/annotateSend.js +40 -0
  107. package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
  108. package/dist/sdk/terminal/tui/chrome.js +13 -0
  109. package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
  110. package/dist/sdk/terminal/tui/commandPages.js +16 -11
  111. package/dist/sdk/terminal/tui/commands.d.ts +4 -0
  112. package/dist/sdk/terminal/tui/commands.js +12 -2
  113. package/dist/sdk/terminal/tui/dialogs.js +1 -0
  114. package/dist/sdk/terminal/tui/pageOverlay.d.ts +3 -0
  115. package/dist/sdk/terminal/tui/pageOverlay.js +25 -7
  116. package/dist/sdk/terminal/tui/pages/agentsPage.js +5 -2
  117. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +70 -16
  118. package/dist/sdk/terminal/tui/pages/annotatePage.js +349 -115
  119. package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
  120. package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
  121. package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
  122. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +14 -1
  123. package/dist/sdk/terminal/tui/pages/pluginsPage.js +295 -25
  124. package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
  125. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +4 -2
  126. package/dist/sdk/terminal/tui/pages/usagePage.js +39 -12
  127. package/dist/sdk/terminal/tui/pluginCommands.d.ts +1 -1
  128. package/dist/sdk/terminal/tui/pluginCommands.js +13 -3
  129. package/dist/sdk/tools/defaultSkills.d.ts +4 -0
  130. package/dist/sdk/tools/defaultSkills.js +20 -0
  131. package/dist/sdk/tools/definitions.d.ts +3 -1
  132. package/dist/sdk/tools/definitions.js +17 -1
  133. package/dist/sdk/tools/dispatch.js +12 -0
  134. package/dist/sdk/tools/harnessTools.js +1 -1
  135. package/dist/sdk/tools/sessionName.d.ts +9 -0
  136. package/dist/sdk/tools/sessionName.js +58 -11
  137. package/dist/sdk/tools/spawnSubagent.js +9 -7
  138. package/dist/sdk/tools/todo.d.ts +15 -0
  139. package/dist/sdk/tools/todo.js +64 -0
  140. package/dist/sdk/world/world.d.ts +0 -2
  141. package/dist/terminal-opentui.js +3331 -1584
  142. package/dist/terminal-pi.js +421 -133
  143. package/docs/README.md +3 -0
  144. package/docs/architecture/00-overview.md +1 -1
  145. package/docs/architecture/01-extension-points.md +1 -1
  146. package/docs/architecture/02-sessions-and-runs.md +73 -36
  147. package/docs/architecture/03-variants.md +1 -3
  148. package/docs/architecture/04-prompt.md +24 -13
  149. package/docs/architecture/06-plugins-and-capabilities.md +46 -10
  150. package/docs/architecture/07-tools-and-gate.md +49 -8
  151. package/docs/architecture/09-accounts-and-connectors.md +8 -2
  152. package/docs/architecture/10-server-protocol.md +68 -16
  153. package/docs/architecture/11-terminal.md +94 -45
  154. package/docs/architecture/12-storage.md +1 -0
  155. package/docs/architecture/13-world-tools.md +8 -6
  156. package/docs/architecture/14-surfaces-and-parity.md +4 -1
  157. package/docs/architecture/README.md +20 -18
  158. package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
  159. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +6 -2
  160. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
  161. package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
  162. package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
  163. package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
  164. package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
  165. package/docs/architecture/proposals/PROP-0048-simple-annotate.md +85 -0
  166. package/docs/architecture/proposals/README.md +11 -6
  167. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
  168. package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
  169. package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
  170. package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
  171. package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
  172. package/docs/decisions/ADR-0044-annotations-pending-or-sent.md +24 -0
  173. package/docs/decisions/ADR-0045-default-plugin-authoring-skill.md +9 -0
  174. package/docs/decisions/ADR-0046-hash-bound-plugin-proposals.md +20 -0
  175. package/docs/decisions/ADR-0047-links-open-with-the-os-handler.md +47 -0
  176. package/docs/doctor.md +14 -1
  177. package/docs/parity.md +16 -13
  178. package/docs/releases/1.3.0.md +21 -0
  179. package/docs/releases/1.3.1-unstable.1.md +8 -0
  180. package/docs/releases/README.md +9 -6
  181. package/docs/terminal-tui.md +127 -65
  182. package/package.json +1 -1
  183. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +0 -75
  184. package/dist/sdk/terminal/tui/pages/notesPage.js +0 -201
  185. package/dist/sdk/tools/checklist.d.ts +0 -9
  186. package/dist/sdk/tools/checklist.js +0 -55
@@ -6,28 +6,42 @@
6
6
  import { describePlugin, surfaceBadge } from "../../../plugins/surfaces.js";
7
7
  import { matchesKey, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
8
8
  import { BACK, applyPluginAction, menuRows, rowActions } from "../../pluginMenu.js";
9
- import { safeText } from "../chrome.js";
9
+ import { inspectorListWidth, safeText, splitColumns } from "../chrome.js";
10
10
  import { FilterList } from "../list.js";
11
11
  const SECTIONS = [
12
12
  { id: 'plugins', title: 'Plugins' },
13
- { id: 'trust', title: 'Trust' }
13
+ { id: 'trust', title: 'Trust' },
14
+ { id: 'packages', title: 'Packages' },
15
+ { id: 'proposals', title: 'Proposals' }
14
16
  ];
15
17
  const SETTINGS_ROW = 'settings:';
18
+ const PACKAGE_ROW = 'package:';
19
+ const INSTALL_PACKAGE = 'action:install-package';
16
20
  const isSwitch = (setting) => setting.min === 0 && setting.max === 1;
17
21
  const settingValueText = (setting) => isSwitch(setting)
18
22
  ? setting.value === 1 ? 'on' : 'off'
19
23
  : `${setting.value.toLocaleString('en-US')}${setting.value === setting.default ? ' · default' : ''}`;
20
24
  const settingsOf = (plugin) => plugin.settings ?? [];
25
+ // Never execute or silently hide terminal controls in code being reviewed.
26
+ const reviewText = (line) => line.replace(/[\x00-\x1f\x7f-\x9f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
21
27
  export class PluginsPage {
22
28
  options;
29
+ presentation = 'fullscreen';
23
30
  plugins;
31
+ proposals = [];
32
+ proposalError;
33
+ preview;
34
+ previewOffset = 0;
35
+ previewRows = 1;
24
36
  section = 'plugins';
25
37
  projectId = null;
38
+ hubManagedIds = new Set();
26
39
  error;
27
40
  restart = false;
28
41
  busy = false;
29
42
  disposed = false;
30
43
  list;
44
+ pointerLayout;
31
45
  /** The plugin whose settings are open, or null on the list. */
32
46
  settingsFor = null;
33
47
  saved = false;
@@ -57,6 +71,8 @@ export class PluginsPage {
57
71
  }
58
72
  activeTab() { return this.settingsPlugin() ? 'settings' : this.section; }
59
73
  hint() {
74
+ if (this.preview)
75
+ return this.preview.status === 'pending' ? '↑↓ scroll · PgUp/PgDn page · Enter decide · Esc back' : '↑↓ scroll · PgUp/PgDn page · Esc back';
60
76
  if (this.settingsPlugin())
61
77
  return this.settingsList.query ? 'Enter change · Esc clear' : 'Enter change · Esc back';
62
78
  return this.list.query ? 'Enter actions · Esc clear' : 'Enter actions · type to search';
@@ -70,6 +86,20 @@ export class PluginsPage {
70
86
  this.projectId = (await call('plugins.session').catch(() => ({ projectId: null }))).projectId;
71
87
  this.plugins = await call('plugins.list');
72
88
  this.error = undefined;
89
+ try {
90
+ this.proposals = await call('plugins.proposals.list');
91
+ this.proposalError = undefined;
92
+ }
93
+ catch (error) {
94
+ this.proposals = [];
95
+ this.proposalError = error instanceof Error ? error.message : String(error);
96
+ }
97
+ this.hubManagedIds = new Set();
98
+ try {
99
+ const overview = await call('hub.list');
100
+ this.hubManagedIds = new Set(overview.installed.filter(receipt => receipt.entry.delivery.kind === 'pi-package').flatMap(receipt => receipt.resourceIds));
101
+ }
102
+ catch { /* Package actions still go through the receipt-aware Harness service. */ }
73
103
  }
74
104
  catch (error) {
75
105
  this.error = error instanceof Error ? error.message : String(error);
@@ -91,8 +121,23 @@ export class PluginsPage {
91
121
  searchText: `${setting.label} ${setting.description}`
92
122
  })));
93
123
  }
94
- /** The menu rows, with a Settings row after the last row of each plugin that has settings. */
124
+ /** Switch, trust, settings and package actions stay grouped under their owning plugin. */
95
125
  rows() {
126
+ if (this.section === 'proposals')
127
+ return this.proposals.map(proposal => ({
128
+ id: `proposal:${proposal.id}`, label: safeText(proposal.name), detail: proposal.status,
129
+ tone: proposal.status === 'pending' ? 'warning' : 'muted',
130
+ searchText: `${proposal.name} ${proposal.description} ${proposal.status}`
131
+ }));
132
+ if (this.section === 'packages')
133
+ return [
134
+ ...(this.plugins ?? []).filter(plugin => plugin.install).map(plugin => {
135
+ const source = plugin.install.kind === 'folder' ? 'local folder' : 'package';
136
+ const hub = this.hubManagedIds.has(plugin.id);
137
+ return { id: `${PACKAGE_ROW}${plugin.id}`, group: safeText(plugin.title), label: 'Manage package', detail: `${source} · ${hub ? 'Hub' : 'local'} · ${plugin.version ?? plugin.install.source}`, tone: 'muted', searchText: `${plugin.title} ${plugin.install.source} ${plugin.version ?? ''} package ${hub ? 'Hub' : 'local'}` };
138
+ }),
139
+ { id: INSTALL_PACKAGE, group: 'Packages', label: 'Install package…', detail: 'Folder, npm or Git', tone: 'accent', searchText: 'install add package plugin npm Git folder' }
140
+ ];
96
141
  const menu = this.menu();
97
142
  const out = [];
98
143
  const placed = new Set();
@@ -107,18 +152,19 @@ export class PluginsPage {
107
152
  if (this.section === 'plugins' && last && settingsOf(row.plugin).length)
108
153
  out.push(settingsRow(row.plugin));
109
154
  });
110
- if (this.section === 'plugins') {
155
+ if (this.section === 'plugins')
111
156
  for (const plugin of this.plugins ?? [])
112
157
  if (settingsOf(plugin).length && !placed.has(plugin.id))
113
158
  out.push(settingsRow(plugin));
114
- }
115
159
  return out;
116
160
  }
117
161
  menu() {
118
162
  const rows = menuRows(this.plugins ?? [], this.projectId);
119
- return this.section === 'trust'
120
- ? rows.filter(row => row.whole && Boolean(row.plugin.code))
121
- : rows.filter(row => rowActions(row, this.projectId).some(action => action.startsWith('Switch ')));
163
+ if (this.section === 'trust')
164
+ return rows.filter(row => row.whole && Boolean(row.plugin.code));
165
+ if (this.section === 'packages')
166
+ return [];
167
+ return rows.filter(row => rowActions(row, this.projectId).some(action => action.startsWith('Switch ')));
122
168
  }
123
169
  key(row) { return `${row.whole ? 'plugin' : 'item'}:${row.id}`; }
124
170
  toRow(row) {
@@ -130,6 +176,24 @@ export class PluginsPage {
130
176
  };
131
177
  }
132
178
  async open(id) {
179
+ if (this.section === 'proposals' && id.startsWith('proposal:')) {
180
+ if (this.busy)
181
+ return;
182
+ this.busy = true;
183
+ try {
184
+ this.preview = await this.options.call('plugins.proposals.preview', { id: id.slice('proposal:'.length) });
185
+ this.previewOffset = 0;
186
+ this.pointerLayout = undefined;
187
+ }
188
+ catch (error) {
189
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), 'error');
190
+ }
191
+ finally {
192
+ this.busy = false;
193
+ this.options.changed();
194
+ }
195
+ return;
196
+ }
133
197
  if (id.startsWith(SETTINGS_ROW)) {
134
198
  this.settingsFor = id.slice(SETTINGS_ROW.length);
135
199
  this.saved = false;
@@ -137,6 +201,16 @@ export class PluginsPage {
137
201
  this.options.changed();
138
202
  return;
139
203
  }
204
+ if (id === INSTALL_PACKAGE) {
205
+ await this.installPackage();
206
+ return;
207
+ }
208
+ if (id.startsWith(PACKAGE_ROW)) {
209
+ const plugin = this.plugins?.find(candidate => candidate.id === id.slice(PACKAGE_ROW.length));
210
+ if (plugin?.install)
211
+ await this.managePackage(plugin);
212
+ return;
213
+ }
140
214
  const row = this.menu().find(candidate => this.key(candidate) === id);
141
215
  if (!row || this.busy)
142
216
  return;
@@ -151,6 +225,104 @@ export class PluginsPage {
151
225
  }
152
226
  await this.reload();
153
227
  }
228
+ async decideProposal() {
229
+ const preview = this.preview;
230
+ if (!preview || preview.status !== 'pending' || this.busy)
231
+ return;
232
+ this.busy = true;
233
+ try {
234
+ const action = await this.options.ui.select(`Proposal ${safeText(preview.name)} · ${preview.hash}`, ['Accept proposal', 'Reject proposal', BACK]);
235
+ if (action !== 'Accept proposal' && action !== 'Reject proposal')
236
+ return;
237
+ const accept = action === 'Accept proposal';
238
+ if (!await this.options.ui.confirm(action + '?', accept ? 'Install this reviewed proposal switched off and untrusted? Review and trust code separately.' : 'Reject this reviewed proposal?'))
239
+ return;
240
+ await this.options.call(accept ? 'plugins.proposals.accept' : 'plugins.proposals.reject', { id: preview.id, hash: preview.hash });
241
+ this.preview = undefined;
242
+ if (accept)
243
+ this.restart = true;
244
+ this.options.ui.notify(accept ? 'Accepted switched off and untrusted. Changes apply after restart.' : 'Proposal rejected.', 'info');
245
+ await this.reload();
246
+ }
247
+ catch (error) {
248
+ this.preview = undefined;
249
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), 'error');
250
+ await this.reload();
251
+ }
252
+ finally {
253
+ this.busy = false;
254
+ if (!this.disposed)
255
+ this.options.changed();
256
+ }
257
+ }
258
+ async installPackage() {
259
+ if (this.busy)
260
+ return;
261
+ this.busy = true;
262
+ try {
263
+ const source = await this.options.ui.input('Install local plugin package', 'npm:package@version, Git address, or absolute folder path');
264
+ if (!source?.trim())
265
+ return;
266
+ const confirmed = await this.options.ui.confirm('Install package?', `${source.trim()} will be installed without running package code. It arrives switched off and code remains untrusted until reviewed.`);
267
+ if (!confirmed)
268
+ return;
269
+ await this.options.call('plugins.install', { source: source.trim() });
270
+ this.restart = true;
271
+ this.options.ui.notify('Installed switched off. Changes apply when this session restarts.', 'info');
272
+ await this.reload();
273
+ }
274
+ catch (error) {
275
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), 'error');
276
+ }
277
+ finally {
278
+ this.busy = false;
279
+ if (!this.disposed)
280
+ this.options.changed();
281
+ }
282
+ }
283
+ async managePackage(plugin) {
284
+ if (this.busy || !plugin.install)
285
+ return;
286
+ const hubManaged = this.hubManagedIds.has(plugin.id);
287
+ const actions = [
288
+ ...(hubManaged || plugin.install.kind === 'folder' ? [] : ['Update package']),
289
+ hubManaged ? 'Uninstall Hub version' : 'Remove package',
290
+ BACK
291
+ ];
292
+ this.busy = true;
293
+ try {
294
+ const action = await this.options.ui.select(`${plugin.title}: ${plugin.install.source}`, actions);
295
+ if (!action || action === BACK)
296
+ return;
297
+ if (action === 'Update package') {
298
+ const confirmed = await this.options.ui.confirm(`Update ${plugin.title}?`, 'Fetches a newer package version without running its scripts. Changed code remains subject to trust review.');
299
+ if (!confirmed)
300
+ return;
301
+ await this.options.call('plugins.update', { pluginId: plugin.id });
302
+ this.options.ui.notify('Updated. Changes apply when this session restarts.', 'info');
303
+ }
304
+ else {
305
+ const title = hubManaged ? `Uninstall ${plugin.title} from Hub?` : `Remove ${plugin.title}?`;
306
+ const message = hubManaged
307
+ ? 'This removes the exact Hub receipt and only its recorded package resources.'
308
+ : 'This removes the installed package. A local folder is moved to the Harness trash.';
309
+ if (!await this.options.ui.confirm(title, message))
310
+ return;
311
+ await this.options.call('plugins.remove', { pluginId: plugin.id });
312
+ this.options.ui.notify(hubManaged ? 'Hub package uninstalled.' : 'Package removed.', 'info');
313
+ }
314
+ this.restart = true;
315
+ await this.reload();
316
+ }
317
+ catch (error) {
318
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), 'error');
319
+ }
320
+ finally {
321
+ this.busy = false;
322
+ if (!this.disposed)
323
+ this.options.changed();
324
+ }
325
+ }
154
326
  back() {
155
327
  this.settingsFor = null;
156
328
  this.saved = false;
@@ -196,9 +368,30 @@ export class PluginsPage {
196
368
  }
197
369
  }
198
370
  handleMouse(event) {
199
- return this.settingsPlugin() ? this.settingsList.handleMouse(event) : this.list.handleMouse(event);
371
+ if (this.preview)
372
+ return { handled: true, render: false };
373
+ const layout = this.pointerLayout;
374
+ if (!layout)
375
+ return undefined;
376
+ if (event.x < 0 || event.x >= layout.width || event.y < layout.top || event.y >= layout.top + layout.rows)
377
+ return { handled: true, render: false };
378
+ return layout.list.handleMouse({ ...event, y: event.y - layout.top });
200
379
  }
201
380
  handleInput(data) {
381
+ if (this.preview) {
382
+ if (matchesKey(data, 'escape'))
383
+ this.preview = undefined;
384
+ else if (matchesKey(data, 'enter'))
385
+ void this.decideProposal();
386
+ else if (matchesKey(data, 'home'))
387
+ this.previewOffset = 0;
388
+ else if (matchesKey(data, 'end'))
389
+ this.previewOffset = Number.MAX_SAFE_INTEGER;
390
+ else
391
+ this.previewOffset = Math.max(0, this.previewOffset + (matchesKey(data, 'down') ? 1 : matchesKey(data, 'up') ? -1 : matchesKey(data, 'pageDown') ? this.previewRows : matchesKey(data, 'pageUp') ? -this.previewRows : 0));
392
+ this.options.changed();
393
+ return true;
394
+ }
202
395
  if (this.settingsPlugin()) {
203
396
  if (matchesKey(data, 'tab') || matchesKey(data, 'shift+tab'))
204
397
  return true;
@@ -206,44 +399,121 @@ export class PluginsPage {
206
399
  }
207
400
  if (matchesKey(data, 'tab') || matchesKey(data, 'shift+tab')) {
208
401
  if (!this.list.query) {
209
- this.section = this.section === 'plugins' ? 'trust' : 'plugins';
402
+ const direction = matchesKey(data, 'shift+tab') ? -1 : 1;
403
+ const current = SECTIONS.findIndex(section => section.id === this.section);
404
+ this.section = SECTIONS[(current + direction + SECTIONS.length) % SECTIONS.length].id;
210
405
  this.fill();
211
406
  }
212
407
  return true;
213
408
  }
214
409
  return this.list.handleInput(data);
215
410
  }
411
+ selectedPlugin() {
412
+ const selectedId = this.list.selected?.id;
413
+ return selectedId?.startsWith(SETTINGS_ROW)
414
+ ? this.plugins?.find(candidate => `${SETTINGS_ROW}${candidate.id}` === selectedId)
415
+ : selectedId?.startsWith(PACKAGE_ROW)
416
+ ? this.plugins?.find(candidate => `${PACKAGE_ROW}${candidate.id}` === selectedId)
417
+ : this.menu().find(candidate => this.key(candidate) === selectedId)?.plugin;
418
+ }
419
+ pluginInspector(plugin, width, rows) {
420
+ const theme = this.options.theme();
421
+ if (width < 1 || rows < 1)
422
+ return [];
423
+ if (!plugin) {
424
+ const action = this.list.selected?.id === INSTALL_PACKAGE;
425
+ return [theme.fg('muted', action ? 'Add a package from npm, Git, or a local folder.' : 'Select a plugin to inspect it.')].slice(0, rows).map(line => truncateToWidth(line, width, ''));
426
+ }
427
+ const selectedId = this.list.selected?.id;
428
+ const row = this.menu().find(candidate => this.key(candidate) === selectedId);
429
+ const title = row ? row.whole ? row.plugin.title : row.name : plugin.title;
430
+ const lines = [theme.bold(safeText(title))];
431
+ if (title !== plugin.title)
432
+ lines.push(theme.fg('dim', `Part of ${safeText(plugin.title)}`));
433
+ lines.push('', ...wrapTextWithAnsi(safeText(describePlugin(plugin)), width));
434
+ if (row) {
435
+ const status = this.section === 'trust' && row.whole
436
+ ? plugin.trust === 'trusted' ? 'Code trusted' : 'Code not trusted'
437
+ : row.on ? 'Enabled' : 'Disabled';
438
+ lines.push('', theme.fg(row.on ? 'success' : 'dim', status));
439
+ }
440
+ if (plugin.code) {
441
+ lines.push(theme.fg('muted', `Code trust: ${plugin.trust === 'trusted' ? 'trusted' : 'not trusted'}`));
442
+ lines.push(theme.fg('muted', `Fingerprint: ${plugin.code.hash.slice(0, 12)}`));
443
+ }
444
+ if (plugin.install) {
445
+ lines.push('', theme.fg('muted', `Package: ${this.hubManagedIds.has(plugin.id) ? 'Hub receipt' : 'local'}`));
446
+ lines.push(...wrapTextWithAnsi(theme.fg('dim', safeText(plugin.install.source)), width));
447
+ }
448
+ if (settingsOf(plugin).length)
449
+ lines.push('', theme.fg('muted', `${settingsOf(plugin).length} settings`));
450
+ return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
451
+ }
216
452
  render(width, rows) {
217
453
  const theme = this.options.theme();
218
454
  if (width < 1 || rows < 1)
219
455
  return [];
220
- if (!this.plugins)
456
+ if (this.preview) {
457
+ const proposal = this.preview;
458
+ const text = [`Name: ${proposal.name}`, `Description: ${proposal.description}`, `Status: ${proposal.status}`, `SHA-256: ${proposal.hash}`, '', '--- package.json ---', proposal.manifest, ...Object.entries(proposal.files).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).flatMap(([path, content]) => ['', `--- ${path} ---`, content])].join('\n');
459
+ const content = text.split('\n').flatMap(line => wrapTextWithAnsi(reviewText(line), width));
460
+ this.previewRows = Math.max(1, rows - 1);
461
+ this.previewOffset = Math.min(this.previewOffset, Math.max(0, content.length - this.previewRows));
462
+ return [theme.bold(`Proposal preview · ${this.previewOffset + 1}/${Math.max(1, content.length)}`), ...content.slice(this.previewOffset, this.previewOffset + this.previewRows)].slice(0, rows).map(line => truncateToWidth(line, width, ''));
463
+ }
464
+ if (this.section === 'proposals' && this.proposalError)
465
+ return [theme.fg('error', `Could not read proposals: ${safeText(this.proposalError)}`)];
466
+ if (!this.plugins) {
467
+ this.pointerLayout = undefined;
221
468
  return [theme.fg(this.error ? 'error' : 'dim', this.error ? `Could not read plugins: ${this.error}` : 'Reading plugins…')];
469
+ }
222
470
  const plugin = this.settingsPlugin();
223
471
  if (plugin)
224
472
  return this.renderSettings(plugin, width, rows);
225
- const selectedId = this.list.selected?.id;
226
- const selected = selectedId?.startsWith(SETTINGS_ROW)
227
- ? this.plugins.find(candidate => `${SETTINGS_ROW}${candidate.id}` === selectedId)
228
- : this.menu().find(candidate => this.key(candidate) === selectedId)?.plugin;
473
+ const note = this.restart ? [theme.fg('warning', 'Saved. Changes apply when this session restarts.')] : [];
474
+ const paneWidth = inspectorListWidth(width);
475
+ if (paneWidth !== undefined) {
476
+ const bodyRows = Math.max(0, rows - note.length);
477
+ const rightWidth = Math.max(0, width - paneWidth - 3);
478
+ const listLines = this.list.render(paneWidth, bodyRows);
479
+ this.pointerLayout = { list: this.list, width: paneWidth, top: note.length, rows: listLines.length };
480
+ return [...note, ...splitColumns(listLines, this.pluginInspector(this.selectedPlugin(), rightWidth, bodyRows), paneWidth, width, bodyRows, theme)]
481
+ .slice(0, rows).map(line => truncateToWidth(line, width, ''));
482
+ }
483
+ const selected = this.selectedPlugin();
229
484
  const detail = selected && rows >= 8 ? wrapTextWithAnsi(safeText(describePlugin(selected)), width).slice(0, 3) : [];
230
- const note = this.restart ? ['Saved. Changes apply when this session restarts.'] : [];
231
- const extra = detail.length + note.length;
232
- const lines = this.list.render(width, Math.max(2, rows - (extra ? extra + 1 : 0)));
233
- if (extra)
234
- lines.push('', ...detail.map(line => theme.fg('muted', line)), ...note.map(line => theme.fg('warning', line)));
485
+ const listRows = Math.max(2, rows - note.length - (detail.length ? detail.length + 1 : 0));
486
+ const listLines = this.list.render(width, listRows);
487
+ this.pointerLayout = { list: this.list, width, top: note.length, rows: Math.min(listLines.length, Math.max(0, rows - note.length)) };
488
+ const lines = [...note, ...listLines];
489
+ if (detail.length)
490
+ lines.push('', ...detail.map(line => theme.fg('muted', line)));
235
491
  return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
236
492
  }
237
493
  renderSettings(plugin, width, rows) {
238
494
  const theme = this.options.theme();
239
495
  const setting = settingsOf(plugin).find(candidate => candidate.key === this.settingsList.selected?.id);
240
496
  const range = setting && !isSwitch(setting) ? [`From ${setting.min.toLocaleString('en-US')} to ${setting.max.toLocaleString('en-US')} · default ${setting.default.toLocaleString('en-US')}`] : [];
497
+ const note = this.saved ? [theme.fg('success', 'Saved.')] : [];
498
+ const paneWidth = inspectorListWidth(width);
499
+ if (paneWidth !== undefined) {
500
+ const bodyRows = Math.max(0, rows - note.length);
501
+ const rightWidth = Math.max(0, width - paneWidth - 3);
502
+ const listLines = this.settingsList.render(paneWidth, bodyRows);
503
+ const details = setting
504
+ ? [theme.bold(safeText(setting.label)), '', ...wrapTextWithAnsi(safeText(setting.description), rightWidth), '', ...range.map(line => theme.fg('muted', line)), theme.fg('dim', `Current value: ${settingValueText(setting)}`)]
505
+ : [theme.fg('muted', 'Select a setting to review its value and description.')];
506
+ this.pointerLayout = { list: this.settingsList, width: paneWidth, top: note.length, rows: listLines.length };
507
+ return [...note, ...splitColumns(listLines, details, paneWidth, width, bodyRows, theme)]
508
+ .slice(0, rows).map(line => truncateToWidth(line, width, ''));
509
+ }
241
510
  const detail = setting && rows >= 8 ? [...wrapTextWithAnsi(safeText(setting.description), width).slice(0, 3), ...range] : [];
242
- const note = this.saved ? ['Saved.'] : [];
243
- const extra = detail.length + note.length;
244
- const lines = this.settingsList.render(width, Math.max(2, rows - (extra ? extra + 1 : 0)));
245
- if (extra)
246
- lines.push('', ...detail.map(line => theme.fg('muted', line)), ...note.map(line => theme.fg('success', line)));
511
+ const listRows = Math.max(2, rows - note.length - (detail.length ? detail.length + 1 : 0));
512
+ const listLines = this.settingsList.render(width, listRows);
513
+ this.pointerLayout = { list: this.settingsList, width, top: note.length, rows: Math.min(listLines.length, Math.max(0, rows - note.length)) };
514
+ const lines = [...note, ...listLines];
515
+ if (detail.length)
516
+ lines.push('', ...detail.map(line => theme.fg('muted', line)));
247
517
  return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
248
518
  }
249
519
  }
@@ -88,9 +88,11 @@ export class SubagentsPage {
88
88
  activeTab() { return 'subagents'; }
89
89
  hint() { return '↑↓ select · type to search'; }
90
90
  rows(width = 80, now = this.options.now?.() ?? Date.now()) {
91
+ // The main agent is pinned first so its children read against it.
91
92
  const ordered = [...this.options.snapshot()].sort((left, right) => {
93
+ const main = Number(right.relation === 'main') - Number(left.relation === 'main');
92
94
  const active = Number(ACTIVE_SUBAGENT_STATUSES.has(right.status)) - Number(ACTIVE_SUBAGENT_STATUSES.has(left.status));
93
- return active || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id);
95
+ return main || active || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id);
94
96
  });
95
97
  return ordered.map(item => {
96
98
  const profile = clean(item.profile?.name ?? '') || 'subagent';
@@ -115,9 +117,10 @@ export class SubagentsPage {
115
117
  render(width, rows) {
116
118
  const items = this.rows(width, this.options.now?.() ?? Date.now());
117
119
  this.list.setRows(items);
118
- const active = items.filter(item => item.data && ACTIVE_SUBAGENT_STATUSES.has(item.data.status)).length;
120
+ const children = items.filter(item => item.data?.relation !== 'main');
121
+ const active = children.filter(item => item.data && ACTIVE_SUBAGENT_STATUSES.has(item.data.status)).length;
119
122
  const selected = this.list.selected?.data;
120
- const header = headerRow('Subagents', `${active} active · ${items.length} total`, width, this.options.theme());
123
+ const header = headerRow('Subagents', `${active} active · ${children.length} total`, width, this.options.theme());
121
124
  const bodyRows = Math.max(0, rows - 7);
122
125
  const listRows = Math.max(1, Math.min(this.list.height, Math.floor(bodyRows * 0.6)));
123
126
  const output = [header, '', ...this.list.render(width, listRows), ''];
@@ -129,8 +132,8 @@ export class SubagentsPage {
129
132
  output.push(neutralRule(width, theme));
130
133
  output.push(padded(theme.fg('muted', `Profile: ${clean(selected.profile?.name ?? '') || 'copy of the parent'}`), width));
131
134
  output.push(padded(theme.fg('accent', 'Task'), width));
132
- output.push(...detailLines(selected.goal || 'Subagent'));
133
- output.push(padded(theme.fg('muted', `Status: ${[selected.status.replaceAll('_', ' '), subagentTasks(selected.checklist), terminalSubagentDuration(selected)].filter(Boolean).join(' · ')}`), width));
135
+ output.push(...detailLines(selected.goal || (selected.relation === 'main' ? 'Main agent' : 'Subagent')));
136
+ output.push(padded(theme.fg('muted', `Status: ${[selected.status.replaceAll('_', ' '), subagentTasks(selected.todo), terminalSubagentDuration(selected)].filter(Boolean).join(' · ')}`), width));
134
137
  output.push(padded(theme.fg('accent', 'Current activity'), width));
135
138
  output.push(...detailLines(selected.activity));
136
139
  }
@@ -2,7 +2,7 @@ import type { Theme } from '@earendil-works/pi-coding-agent';
2
2
  import type { UsagePeriod } from "../../../usage/types.js";
3
3
  import type { TabInfo } from "../chrome.js";
4
4
  import type { TerminalPage } from "../page.js";
5
- type UsageTab = 'analysis' | 'limits';
5
+ type UsageTab = 'analysis' | 'limits' | 'accounts';
6
6
  interface UsagePageOptions {
7
7
  theme(): Theme;
8
8
  call(method: string, params?: Record<string, unknown>): Promise<unknown>;
@@ -12,7 +12,7 @@ interface UsagePageOptions {
12
12
  }
13
13
  export declare class UsagePage implements TerminalPage {
14
14
  private readonly options;
15
- presentation: "centered";
15
+ presentation: "fullscreen";
16
16
  focused: boolean;
17
17
  private tab;
18
18
  private period;
@@ -20,7 +20,9 @@ export declare class UsagePage implements TerminalPage {
20
20
  private summary?;
21
21
  private limitsSummary?;
22
22
  private limits?;
23
+ private allLimits?;
23
24
  private busy;
25
+ private reloadPending;
24
26
  private error?;
25
27
  private refreshLimits;
26
28
  private offset;
@@ -118,7 +118,7 @@ function spread(left, right, width) {
118
118
  }
119
119
  export class UsagePage {
120
120
  options;
121
- presentation = 'centered';
121
+ presentation = 'fullscreen';
122
122
  focused = false;
123
123
  tab;
124
124
  period = '7d';
@@ -126,7 +126,9 @@ export class UsagePage {
126
126
  summary;
127
127
  limitsSummary;
128
128
  limits;
129
+ allLimits;
129
130
  busy = false;
131
+ reloadPending = false;
130
132
  error;
131
133
  refreshLimits;
132
134
  offset = 0;
@@ -140,17 +142,19 @@ export class UsagePage {
140
142
  this.refreshLimits = this.tab === 'limits';
141
143
  }
142
144
  tabs() {
143
- return [{ id: 'analysis', title: 'Analysis' }, { id: 'limits', title: 'Limits' }];
145
+ return [{ id: 'analysis', title: 'Analysis' }, { id: 'limits', title: 'Limits' }, { id: 'accounts', title: 'All accounts' }];
144
146
  }
145
147
  activeTab() {
146
148
  return this.tab;
147
149
  }
148
150
  selectTab(id) {
149
- if ((id === 'analysis' || id === 'limits') && id !== this.tab) {
151
+ if ((id === 'analysis' || id === 'limits' || id === 'accounts') && id !== this.tab) {
150
152
  this.tab = id;
151
153
  this.offset = 0;
152
154
  this.options.changed();
153
- if (id === 'limits' && !this.limits) {
155
+ if (id === 'accounts')
156
+ void this.load();
157
+ else if (id === 'limits' && !this.limits) {
154
158
  this.refreshLimits = true;
155
159
  void this.load();
156
160
  }
@@ -162,19 +166,23 @@ export class UsagePage {
162
166
  }
163
167
  hint() {
164
168
  return this.tab === 'analysis'
165
- ? '↑/↓ scroll · PgUp/PgDn page · 1 today 2 7d 3 30d 0 all · c account s session p project a all accounts · r refresh · Tab limits · Esc close'
166
- : '↑/↓ scroll · PgUp/PgDn page · r read provider limits · Tab analysis · Esc close';
169
+ ? '↑/↓ scroll · PgUp/PgDn page · 1 today 2 7d 3 30d 0 all · c account s session p project a all accounts · r refresh'
170
+ : '↑/↓ scroll · PgUp/PgDn page · r read provider limits';
167
171
  }
168
172
  activate() {
169
- if (!this.summary && !this.limits)
173
+ if (!this.summary && !this.limits && !this.allLimits)
170
174
  void this.load();
171
175
  }
172
176
  dispose() {
173
177
  this.disposed = true;
174
178
  }
175
179
  async load() {
176
- if (this.busy || this.disposed)
180
+ if (this.disposed)
181
+ return;
182
+ if (this.busy) {
183
+ this.reloadPending = true;
177
184
  return;
185
+ }
178
186
  this.busy = true;
179
187
  this.error = undefined;
180
188
  this.options.changed();
@@ -186,6 +194,14 @@ export class UsagePage {
186
194
  timezoneOffsetMinutes: -new Date().getTimezoneOffset()
187
195
  });
188
196
  }
197
+ else if (this.tab === 'accounts') {
198
+ if (!this.allLimits) {
199
+ this.allLimits = await this.options.call('terminal.usage.allLimits', { refresh: false });
200
+ if (!this.disposed)
201
+ this.options.changed();
202
+ }
203
+ this.allLimits = await this.options.call('terminal.usage.allLimits', { refresh: true });
204
+ }
189
205
  else {
190
206
  // The provider's windows and this app's own ledger answer different questions; show both.
191
207
  const [limits, summary] = await Promise.all([
@@ -207,12 +223,23 @@ export class UsagePage {
207
223
  }
208
224
  finally {
209
225
  this.busy = false;
210
- if (!this.disposed)
226
+ if (!this.disposed) {
211
227
  this.options.changed();
228
+ if (this.reloadPending) {
229
+ this.reloadPending = false;
230
+ void this.load();
231
+ }
232
+ }
212
233
  }
213
234
  }
214
235
  handleInput(data) {
215
236
  const page = Math.max(1, this.scrollRows - 1);
237
+ if (matchesKey(data, 'tab') || matchesKey(data, 'shift+tab')) {
238
+ const tabs = ['analysis', 'limits', 'accounts'];
239
+ const direction = matchesKey(data, 'shift+tab') ? -1 : 1;
240
+ this.selectTab(tabs[(tabs.indexOf(this.tab) + direction + tabs.length) % tabs.length]);
241
+ return true;
242
+ }
216
243
  if (matchesKey(data, 'up'))
217
244
  this.offset = Math.max(0, this.offset - 1);
218
245
  else if (matchesKey(data, 'down'))
@@ -298,7 +325,7 @@ export class UsagePage {
298
325
  return { header, body };
299
326
  }
300
327
  limitsContent(width, theme) {
301
- const report = this.limits;
328
+ const report = this.tab === 'accounts' ? this.allLimits : this.limits;
302
329
  const header = [];
303
330
  if (!report) {
304
331
  header.push(theme.fg(this.error ? 'error' : 'dim', this.error ?? (this.busy ? 'Reading provider limits…' : 'No limit readings yet.')));
@@ -318,7 +345,7 @@ export class UsagePage {
318
345
  }
319
346
  for (const provider of account.providers) {
320
347
  body.push('');
321
- const source = provider.source === 'provider' ? '' : provider.source;
348
+ const source = this.tab === 'accounts' ? `cached · ${provider.source}` : provider.source === 'provider' ? '' : provider.source;
322
349
  const meta = [source, ago(provider.observedAt)].filter(Boolean).join(' · ');
323
350
  const tone = provider.source === 'unavailable' ? 'warning' : 'muted';
324
351
  body.push(...wrapTextWithAnsi(theme.fg(tone, ` ${provider.provider}${meta ? ` · ${meta}` : ''}${provider.reason ? ` · ${oneLine(provider.reason)}` : ''}`), width));
@@ -348,7 +375,7 @@ export class UsagePage {
348
375
  }
349
376
  if (!report.accounts.length)
350
377
  body.push(theme.fg('dim', 'No accounts.'));
351
- const local = this.limitsSummary?.totals;
378
+ const local = this.tab === 'limits' ? this.limitsSummary?.totals : undefined;
352
379
  if (local) {
353
380
  if (body.length)
354
381
  body.push('');
@@ -6,7 +6,7 @@ type PageOptions = {
6
6
  runId?: string;
7
7
  url?: string;
8
8
  annotate?: AnnotateStart;
9
- usageTab?: 'analysis' | 'limit';
9
+ usageTab?: 'analysis' | 'limit' | 'accounts';
10
10
  usagePeriod?: 'today' | '7d' | '30d' | 'all';
11
11
  };
12
12
  type OpenPage = (id: CommandPageId, options?: PageOptions) => Promise<void>;