@xingular/harness 1.1.1 → 1.3.1-unstable.0

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 (264) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +36 -28
  3. package/dist/cli.js +10837 -6159
  4. package/dist/sdk/accounts/accounts.d.ts +27 -3
  5. package/dist/sdk/accounts/accounts.js +78 -24
  6. package/dist/sdk/accounts/secrets.d.ts +12 -0
  7. package/dist/sdk/accounts/secrets.js +114 -7
  8. package/dist/sdk/accounts/store.d.ts +26 -1
  9. package/dist/sdk/accounts/store.js +1 -1
  10. package/dist/sdk/agents/builtin.js +18 -4
  11. package/dist/sdk/agents/planResearch.d.ts +27 -0
  12. package/dist/sdk/agents/planResearch.js +82 -0
  13. package/dist/sdk/agents/profiles.d.ts +5 -1
  14. package/dist/sdk/agents/profiles.js +21 -5
  15. package/dist/sdk/annotations/pageScript.js +349 -48
  16. package/dist/sdk/annotations/part.js +34 -23
  17. package/dist/sdk/annotations/store.d.ts +7 -0
  18. package/dist/sdk/annotations/store.js +13 -5
  19. package/dist/sdk/browser/backend.d.ts +16 -0
  20. package/dist/sdk/browser/backend.js +17 -0
  21. package/dist/sdk/browser/contracts.d.ts +19 -0
  22. package/dist/sdk/browser/contracts.js +116 -0
  23. package/dist/sdk/browser/tools.d.ts +1 -4
  24. package/dist/sdk/browser/tools.js +14 -91
  25. package/dist/sdk/capabilities/runSnapshot.js +10 -4
  26. package/dist/sdk/cli.js +5 -4
  27. package/dist/sdk/commands/list.d.ts +3 -0
  28. package/dist/sdk/commands/list.js +75 -0
  29. package/dist/sdk/commands/registry.d.ts +258 -0
  30. package/dist/sdk/commands/registry.js +48 -0
  31. package/dist/sdk/commands/routes.d.ts +16 -0
  32. package/dist/sdk/commands/routes.js +33 -0
  33. package/dist/sdk/core/commandBudget.d.ts +23 -0
  34. package/dist/sdk/core/commandBudget.js +57 -0
  35. package/dist/sdk/core/index.js +17 -1
  36. package/dist/sdk/core/interactiveProfile.d.ts +46 -0
  37. package/dist/sdk/core/interactiveProfile.js +143 -0
  38. package/dist/sdk/core/shell.d.ts +20 -0
  39. package/dist/sdk/core/shell.js +73 -0
  40. package/dist/sdk/core/workspace.d.ts +13 -0
  41. package/dist/sdk/core/workspace.js +88 -0
  42. package/dist/sdk/core/workspaceBashTool.d.ts +20 -0
  43. package/dist/sdk/core/workspaceBashTool.js +50 -0
  44. package/dist/sdk/core/workspaceSandbox.d.ts +41 -0
  45. package/dist/sdk/core/workspaceSandbox.js +360 -0
  46. package/dist/sdk/core/workspaceWriteOperations.d.ts +33 -0
  47. package/dist/sdk/core/workspaceWriteOperations.js +224 -0
  48. package/dist/sdk/doctor/checks.js +71 -12
  49. package/dist/sdk/doctor/types.d.ts +2 -2
  50. package/dist/sdk/harness.d.ts +7 -0
  51. package/dist/sdk/harness.js +42 -2
  52. package/dist/sdk/hub/hub.js +5 -3
  53. package/dist/sdk/index.d.ts +8 -3
  54. package/dist/sdk/index.js +4 -1
  55. package/dist/sdk/pi/runtime.d.ts +2 -1
  56. package/dist/sdk/pi/runtime.js +2 -2
  57. package/dist/sdk/plugins/builtin.js +32 -15
  58. package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
  59. package/dist/sdk/plugins/builtinRegistry.js +30 -8
  60. package/dist/sdk/plugins/catalogue.js +2 -1
  61. package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
  62. package/dist/sdk/plugins/commandPrefix.js +21 -0
  63. package/dist/sdk/plugins/firstParty.d.ts +1 -1
  64. package/dist/sdk/plugins/firstParty.js +8 -2
  65. package/dist/sdk/plugins/install.d.ts +6 -2
  66. package/dist/sdk/plugins/install.js +29 -5
  67. package/dist/sdk/plugins/inventory.js +6 -5
  68. package/dist/sdk/plugins/manifest.d.ts +2 -0
  69. package/dist/sdk/plugins/manifest.js +2 -1
  70. package/dist/sdk/plugins/surfaces.d.ts +5 -0
  71. package/dist/sdk/plugins/surfaces.js +11 -0
  72. package/dist/sdk/plugins/types.d.ts +15 -0
  73. package/dist/sdk/prompt/compile.d.ts +14 -0
  74. package/dist/sdk/prompt/compile.js +65 -31
  75. package/dist/sdk/prompt/standard.d.ts +17 -0
  76. package/dist/sdk/prompt/standard.js +21 -0
  77. package/dist/sdk/server/methods.js +22 -2
  78. package/dist/sdk/server/remote.js +36 -11
  79. package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
  80. package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
  81. package/dist/sdk/sessions/continuation.d.ts +7 -2
  82. package/dist/sdk/sessions/continuation.js +11 -6
  83. package/dist/sdk/sessions/contracts.d.ts +190 -4
  84. package/dist/sdk/sessions/contracts.js +71 -8
  85. package/dist/sdk/sessions/control.d.ts +72 -0
  86. package/dist/sdk/sessions/control.js +158 -0
  87. package/dist/sdk/sessions/initialBash.d.ts +7 -0
  88. package/dist/sdk/sessions/initialBash.js +7 -0
  89. package/dist/sdk/sessions/planMode.d.ts +14 -3
  90. package/dist/sdk/sessions/planMode.js +74 -17
  91. package/dist/sdk/sessions/registry.d.ts +1 -1
  92. package/dist/sdk/sessions/registry.js +2 -2
  93. package/dist/sdk/sessions/runStore.d.ts +14 -0
  94. package/dist/sdk/sessions/runStore.js +38 -1
  95. package/dist/sdk/sessions/runner.d.ts +40 -2
  96. package/dist/sdk/sessions/runner.js +161 -13
  97. package/dist/sdk/sessions/sessions.d.ts +23 -3
  98. package/dist/sdk/sessions/sessions.js +225 -22
  99. package/dist/sdk/sessions/subagentService.d.ts +24 -0
  100. package/dist/sdk/sessions/subagentService.js +225 -26
  101. package/dist/sdk/sessions/subagents.d.ts +10 -1
  102. package/dist/sdk/sessions/subagents.js +4 -0
  103. package/dist/sdk/sessions/workSlice.d.ts +106 -0
  104. package/dist/sdk/sessions/workSlice.js +329 -0
  105. package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
  106. package/dist/sdk/sessions/workspaceBoundary.js +46 -0
  107. package/dist/sdk/terminal/browserControl.js +2 -1
  108. package/dist/sdk/terminal/controller.d.ts +31 -3
  109. package/dist/sdk/terminal/controller.js +81 -29
  110. package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
  111. package/dist/sdk/terminal/opentui/ansi.js +192 -0
  112. package/dist/sdk/terminal/opentui/model.d.ts +163 -0
  113. package/dist/sdk/terminal/opentui/model.js +482 -0
  114. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
  115. package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
  116. package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
  117. package/dist/sdk/terminal/opentui/theme.js +88 -0
  118. package/dist/sdk/terminal/piExtension.d.ts +6 -1
  119. package/dist/sdk/terminal/piExtension.js +177 -63
  120. package/dist/sdk/terminal/standalone.d.ts +7 -0
  121. package/dist/sdk/terminal/standalone.js +72 -13
  122. package/dist/sdk/terminal/subagentMonitor.d.ts +28 -6
  123. package/dist/sdk/terminal/subagentMonitor.js +61 -9
  124. package/dist/sdk/terminal/terminals.d.ts +26 -2
  125. package/dist/sdk/terminal/terminals.js +325 -43
  126. package/dist/sdk/terminal/trajectory.d.ts +6 -1
  127. package/dist/sdk/terminal/trajectory.js +8 -0
  128. package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
  129. package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
  130. package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
  131. package/dist/sdk/terminal/tui/chrome.js +13 -0
  132. package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
  133. package/dist/sdk/terminal/tui/commandPages.js +17 -4
  134. package/dist/sdk/terminal/tui/commands.d.ts +6 -0
  135. package/dist/sdk/terminal/tui/commands.js +213 -124
  136. package/dist/sdk/terminal/tui/list.d.ts +14 -1
  137. package/dist/sdk/terminal/tui/list.js +96 -14
  138. package/dist/sdk/terminal/tui/pageOverlay.js +21 -5
  139. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
  140. package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
  141. package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
  142. package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
  143. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
  144. package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
  145. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -6
  146. package/dist/sdk/terminal/tui/pages/pluginsPage.js +206 -28
  147. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
  148. package/dist/sdk/terminal/tui/pages/subagentsPage.js +104 -19
  149. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
  150. package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
  151. package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
  152. package/dist/sdk/terminal/tui/piAdapter.js +3 -5
  153. package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
  154. package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
  155. package/dist/sdk/terminal/tui/runtime.js +23 -3
  156. package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
  157. package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
  158. package/dist/sdk/tools/checkpoint.d.ts +27 -0
  159. package/dist/sdk/tools/checkpoint.js +154 -0
  160. package/dist/sdk/tools/getCapabilities.d.ts +31 -0
  161. package/dist/sdk/tools/getCapabilities.js +34 -0
  162. package/dist/sdk/tools/sessionName.d.ts +2 -0
  163. package/dist/sdk/tools/sessionName.js +5 -3
  164. package/dist/sdk/tools/spawnSubagent.js +11 -8
  165. package/dist/sdk/tools/todo.d.ts +15 -0
  166. package/dist/sdk/tools/todo.js +64 -0
  167. package/dist/sdk/usage/analytics.d.ts +20 -0
  168. package/dist/sdk/usage/analytics.js +151 -0
  169. package/dist/sdk/usage/ledger.d.ts +38 -0
  170. package/dist/sdk/usage/ledger.js +142 -0
  171. package/dist/sdk/usage/limits/adapters.d.ts +11 -0
  172. package/dist/sdk/usage/limits/adapters.js +349 -0
  173. package/dist/sdk/usage/limits/capture.d.ts +8 -0
  174. package/dist/sdk/usage/limits/capture.js +47 -0
  175. package/dist/sdk/usage/limits/http.d.ts +12 -0
  176. package/dist/sdk/usage/limits/http.js +51 -0
  177. package/dist/sdk/usage/limits/normalize.d.ts +15 -0
  178. package/dist/sdk/usage/limits/normalize.js +211 -0
  179. package/dist/sdk/usage/limits/service.d.ts +30 -0
  180. package/dist/sdk/usage/limits/service.js +176 -0
  181. package/dist/sdk/usage/limits/types.d.ts +65 -0
  182. package/dist/sdk/usage/limits/types.js +1 -0
  183. package/dist/sdk/usage/serverMethods.d.ts +3 -0
  184. package/dist/sdk/usage/serverMethods.js +22 -0
  185. package/dist/sdk/usage/service.d.ts +32 -0
  186. package/dist/sdk/usage/service.js +66 -0
  187. package/dist/sdk/usage/settings.d.ts +7 -0
  188. package/dist/sdk/usage/settings.js +28 -0
  189. package/dist/sdk/usage/types.d.ts +60 -0
  190. package/dist/sdk/usage/types.js +4 -0
  191. package/dist/terminal-opentui.js +9204 -4812
  192. package/dist/terminal-pi.js +1848 -272
  193. package/docs/architecture/00-overview.md +1 -1
  194. package/docs/architecture/01-extension-points.md +1 -1
  195. package/docs/architecture/02-sessions-and-runs.md +169 -36
  196. package/docs/architecture/04-prompt.md +55 -15
  197. package/docs/architecture/06-plugins-and-capabilities.md +36 -15
  198. package/docs/architecture/07-tools-and-gate.md +131 -25
  199. package/docs/architecture/08-memory-and-history.md +4 -2
  200. package/docs/architecture/09-accounts-and-connectors.md +39 -3
  201. package/docs/architecture/10-server-protocol.md +71 -9
  202. package/docs/architecture/11-terminal.md +148 -60
  203. package/docs/architecture/12-storage.md +30 -2
  204. package/docs/architecture/13-world-tools.md +22 -1
  205. package/docs/architecture/14-surfaces-and-parity.md +58 -8
  206. package/docs/architecture/README.md +20 -17
  207. package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
  208. package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
  209. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
  210. package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
  211. package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
  212. package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
  213. package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
  214. package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
  215. package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
  216. package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
  217. package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
  218. package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
  219. package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
  220. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +258 -0
  221. package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
  222. package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
  223. package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
  224. package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
  225. package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
  226. package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
  227. package/docs/architecture/proposals/README.md +24 -6
  228. package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
  229. package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
  230. package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
  231. package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
  232. package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
  233. package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
  234. package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
  235. package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
  236. package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
  237. package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
  238. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
  239. package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
  240. package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
  241. package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
  242. package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
  243. package/docs/doctor.md +14 -1
  244. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
  245. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
  246. package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
  247. package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
  248. package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
  249. package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
  250. package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
  251. package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
  252. package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
  253. package/docs/parity.md +83 -27
  254. package/docs/releases/1.1.1.md +28 -0
  255. package/docs/releases/1.2.0.md +49 -0
  256. package/docs/releases/1.3.0.md +21 -0
  257. package/docs/releases/4.0.0.md +10 -9
  258. package/docs/releases/README.md +35 -30
  259. package/docs/study/00-index.md +3 -0
  260. package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
  261. package/docs/terminal-tui.md +162 -82
  262. package/package.json +4 -3
  263. package/dist/sdk/tools/checklist.d.ts +0 -9
  264. package/dist/sdk/tools/checklist.js +0 -54
@@ -1,4 +1,4 @@
1
- import { fuzzyFilter, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
1
+ import { fuzzyFilter, matchesKey, parseColor, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
2
2
  import { safeText, selectedRow, selectionMuted, selectionText } from "./chrome.js";
3
3
  import { SEARCH_FIELD_ROWS, SearchField, searchFieldRowsFor } from "./search.js";
4
4
  import { isPrimaryClick } from "./mouse.js";
@@ -25,7 +25,10 @@ export class FilterList {
25
25
  get total() { return this.rows.length; }
26
26
  get shown() { return this.visible.length; }
27
27
  /** Rows the list would use if nothing cut it: the search field, headings and rows. */
28
- get height() { return (this.searchVisible() ? this.renderedSearchRows : 0) + this.entries().length; }
28
+ get height() {
29
+ return (this.searchVisible() ? this.renderedSearchRows : 0)
30
+ + this.entries().reduce((height, entry) => height + (entry.row?.subtitle === undefined ? 1 : 2), 0);
31
+ }
29
32
  /** Rows at the top of `render` that a frame must keep when it has to cut the list. */
30
33
  get pinnedRows() { return this.searchVisible() ? this.renderedSearchRows : 0; }
31
34
  setRows(rows) {
@@ -61,6 +64,10 @@ export class FilterList {
61
64
  }
62
65
  invalidate() { this.search.invalidate(); }
63
66
  limit() { return Math.max(1, Math.floor(this.options.limit?.() ?? Number.POSITIVE_INFINITY)); }
67
+ rowGap() {
68
+ const gap = this.options.rowGap ?? 0;
69
+ return Number.isFinite(gap) ? Math.max(0, Math.floor(gap)) : 0;
70
+ }
64
71
  mode() { return this.options.search ?? 'always'; }
65
72
  canSearch() { return this.mode() === 'always'; }
66
73
  searchVisible() { return this.canSearch() || this.query.length > 0; }
@@ -68,7 +75,7 @@ export class FilterList {
68
75
  refilter() {
69
76
  const query = this.query.trim();
70
77
  this.visible = query
71
- ? fuzzyFilter(this.rows, query, row => row.searchText ?? `${row.label} ${row.detail ?? ''} ${row.group ?? ''}`)
78
+ ? fuzzyFilter(this.rows, query, row => row.searchText ?? `${row.label} ${row.subtitle ?? ''} ${row.detail ?? ''} ${row.group ?? ''}`)
72
79
  : this.rows;
73
80
  if (!this.visible.some(row => row.id === this.selectedId && !row.disabled))
74
81
  this.selectedId = this.firstSelectable()?.id;
@@ -136,15 +143,20 @@ export class FilterList {
136
143
  entries() {
137
144
  const entries = [];
138
145
  let last;
139
- for (const row of this.visible) {
146
+ const gap = this.rowGap();
147
+ for (const [index, row] of this.visible.entries()) {
140
148
  if (!this.query && row.group && row.group !== last)
141
149
  entries.push({ heading: row.group });
142
150
  last = row.group;
143
151
  entries.push({ row });
152
+ if (index < this.visible.length - 1) {
153
+ for (let line = 0; line < gap; line += 1)
154
+ entries.push({ spacer: true });
155
+ }
144
156
  }
145
157
  return entries;
146
158
  }
147
- line(row, width) {
159
+ lines(row, width) {
148
160
  const theme = this.options.theme();
149
161
  const selected = row.id === this.selectedId;
150
162
  const detail = row.detail ? safeText(row.detail) : '';
@@ -153,10 +165,40 @@ export class FilterList {
153
165
  const label = truncateToWidth(safeText(row.label), Math.max(0, room - (showDetail ? visibleWidth(detail) + 2 : 0)), '…');
154
166
  const gap = ' '.repeat(Math.max(0, room - visibleWidth(label) - (showDetail ? visibleWidth(detail) : 0)));
155
167
  const tone = (text) => theme.fg(row.tone ?? 'muted', text);
156
- const main = (selected ? '❯ ' : ' ') + label;
157
- if (selected)
158
- return selectedRow(selectionText(theme, main) + gap + (showDetail ? selectionMuted(theme, detail) : ''), width, theme);
159
- return (row.disabled ? theme.fg('dim', main) : main) + gap + (showDetail ? tone(detail) : '');
168
+ const prefix = selected ? '❯ ' : ' ';
169
+ const labelText = selected || row.disabled || !row.labelParts?.length ? label : this.labelParts(row.labelParts, label, theme);
170
+ const main = prefix + label;
171
+ const primary = selected
172
+ ? selectedRow(selectionText(theme, main) + gap + (showDetail ? selectionMuted(theme, detail) : ''), width, theme)
173
+ : (row.disabled ? theme.fg('dim', main) : prefix + labelText) + gap + (showDetail ? tone(detail) : '');
174
+ if (row.subtitle === undefined)
175
+ return [primary];
176
+ const subtitle = truncateToWidth(` ${safeText(row.subtitle)}`, width, '…');
177
+ const secondary = selected
178
+ ? selectedRow(selectionMuted(theme, subtitle), width, theme)
179
+ : theme.fg(row.disabled ? 'dim' : 'muted', subtitle);
180
+ return [primary, secondary];
181
+ }
182
+ labelParts(parts, visibleLabel, theme) {
183
+ const source = parts.map(part => safeText(part.text)).join('');
184
+ const truncated = visibleWidth(source) > visibleWidth(visibleLabel);
185
+ let remaining = Math.max(0, visibleWidth(visibleLabel) - (truncated ? 1 : 0));
186
+ const output = [];
187
+ for (const part of parts) {
188
+ if (remaining <= 0)
189
+ break;
190
+ const text = safeText(part.text);
191
+ const fitted = truncateToWidth(text, remaining, '');
192
+ if (fitted) {
193
+ output.push(part.color ? theme.style(fitted, { fg: parseColor(part.color) }) : part.tone ? theme.fg(part.tone, fitted) : fitted);
194
+ }
195
+ remaining -= visibleWidth(fitted);
196
+ if (visibleWidth(fitted) < visibleWidth(text))
197
+ break;
198
+ }
199
+ if (truncated)
200
+ output.push('…');
201
+ return output.join('');
160
202
  }
161
203
  /** `rows` is the most the list may use, the search row included. */
162
204
  render(width, rows) {
@@ -186,11 +228,51 @@ export class FilterList {
186
228
  return lines.map(line => truncateToWidth(line, width, ''));
187
229
  }
188
230
  const at = Math.max(0, entries.findIndex(entry => entry.row?.id === this.selectedId));
189
- const start = Math.max(0, Math.min(Math.max(0, entries.length - budget), at - Math.floor(budget / 2)));
190
- for (const entry of entries.slice(start, start + budget)) {
191
- lines.push(entry.row ? this.line(entry.row, width) : theme.fg('dim', safeText(entry.heading ?? '')));
192
- this.renderedRows.push(entry.row);
231
+ let start = at;
232
+ let end = at + 1;
233
+ let used = entries[at]?.row?.subtitle === undefined ? 1 : 2;
234
+ let preferBefore = true;
235
+ while (start > 0 || end < entries.length) {
236
+ const before = entries[start - 1];
237
+ const after = entries[end];
238
+ const beforeHeight = before ? (before.row?.subtitle === undefined ? 1 : 2) : Number.POSITIVE_INFINITY;
239
+ const afterHeight = after ? (after.row?.subtitle === undefined ? 1 : 2) : Number.POSITIVE_INFINITY;
240
+ if (preferBefore && before && used + beforeHeight <= budget) {
241
+ start -= 1;
242
+ used += beforeHeight;
243
+ }
244
+ else if (after && used + afterHeight <= budget) {
245
+ end += 1;
246
+ used += afterHeight;
247
+ }
248
+ else if (before && used + beforeHeight <= budget) {
249
+ start -= 1;
250
+ used += beforeHeight;
251
+ }
252
+ else
253
+ break;
254
+ preferBefore = !preferBefore;
255
+ }
256
+ if (entries[start]?.spacer)
257
+ start += 1;
258
+ if (entries[end - 1]?.spacer)
259
+ end -= 1;
260
+ for (const entry of entries.slice(start, end)) {
261
+ if (entry.row) {
262
+ const rendered = this.lines(entry.row, width);
263
+ lines.push(...rendered);
264
+ this.renderedRows.push(...rendered.map(() => entry.row));
265
+ }
266
+ else if (entry.spacer) {
267
+ lines.push('');
268
+ this.renderedRows.push(undefined);
269
+ }
270
+ else {
271
+ lines.push(theme.fg('dim', safeText(entry.heading ?? '')));
272
+ this.renderedRows.push(undefined);
273
+ }
193
274
  }
194
- return lines.map(line => truncateToWidth(line, width, ''));
275
+ this.renderedRows = this.renderedRows.slice(0, rowBudget);
276
+ return lines.slice(0, rowBudget).map(line => truncateToWidth(line, width, ''));
195
277
  }
196
278
  }
@@ -1,4 +1,4 @@
1
- import { matchesKey } from '@earendil-works/pi-tui';
1
+ import { matchesKey, visibleWidth } from '@earendil-works/pi-tui';
2
2
  import { CENTER_DIALOG_PAD, CHROME_PAD, centerDialogBodyRows, centerDialogContentWidth, centerDialogFrame, centerDialogVerticalPad, contentWidth, pageBodyRows, pageFrame, tabIndexAt, tabStrip } from "./chrome.js";
3
3
  import { centeredDialogHost, dialogFits, pageRows } from "./dialogGeometry.js";
4
4
  export class PageOverlay {
@@ -132,11 +132,27 @@ export class PageOverlay {
132
132
  contentWidth: contentWidth(width),
133
133
  centered
134
134
  };
135
- const footer = page.ownsNavigation ? `Esc back/close · ${page.hint()}` : [
136
- page.hint(),
137
- tabs.length > 1 && !page.ownsNavigation ? 'Tab/Shift+Tab sections' : '',
138
- 'Esc close'
135
+ const pageHint = page.hint();
136
+ const returnsToPage = pageHint.split(' · ').includes('Esc back');
137
+ const escapeHint = page.ownsNavigation ? 'Esc back/close' : returnsToPage ? 'Esc back' : 'Esc close';
138
+ const fullFooter = page.ownsNavigation ? `${escapeHint} · ${pageHint}` : [
139
+ pageHint,
140
+ tabs.length > 1 ? 'Tab/Shift+Tab sections' : '',
141
+ returnsToPage ? '' : escapeHint
139
142
  ].filter(Boolean).join(' · ');
143
+ const footerWidth = Math.max(0, width - CHROME_PAD * 2);
144
+ const primaryHint = pageHint.split(' · ')[0] ?? '';
145
+ const compactFooter = [
146
+ primaryHint,
147
+ !page.ownsNavigation && tabs.length > 1 ? 'Tab/⇧Tab' : '',
148
+ escapeHint
149
+ ].filter(Boolean).join(' · ');
150
+ const navigationFooter = [!page.ownsNavigation && tabs.length > 1 ? 'Tab/⇧Tab' : '', escapeHint].filter(Boolean).join(' · ');
151
+ const footer = visibleWidth(fullFooter) <= footerWidth
152
+ ? fullFooter
153
+ : visibleWidth(compactFooter) <= footerWidth
154
+ ? compactFooter
155
+ : visibleWidth(navigationFooter) <= footerWidth ? navigationFooter : escapeHint;
140
156
  return pageFrame({
141
157
  title: '',
142
158
  headerLine: tabStrip(tabs, page.activeTab(), inner, theme, 'top', this.hoveredTab),
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The annotate pane (`/plugin:annotate`, PROP-0018): the person works in a browser window or an app's own
2
+ * The annotate pane (`/annotate:` and `/annotate:annotate`, PROP-0018): the person works in a browser window or an app's own
3
3
  * windows, and this page is the panel beside it. With the inspector on, hovering outlines a
4
4
  * component and a click opens a comment popover with three choices: keep it as a note, add it to
5
5
  * this session's queue, or send it to the agent now. The page lists the queue, what was sent and
@@ -194,10 +194,11 @@ export class BrowserPage {
194
194
  if (row.hub) {
195
195
  const entry = row.hub;
196
196
  const installed = this.snapshot?.hub?.installed.some(receipt => receipt.entry.id === entry.id && receipt.entry.version === entry.version);
197
- actions.push({ id: 'preview', label: 'Preview reviewed content', run: async () => {
198
- const page = await this.options.call('terminal.browser.hubPreview', { id: entry.id, version: entry.version });
199
- this.showContent(page.content + (page.done ? '' : '\n\n[Preview limited to first page.]'));
200
- } });
197
+ if (entry.delivery.kind !== 'pi-package')
198
+ actions.push({ id: 'preview', label: 'Preview reviewed content', run: async () => {
199
+ const page = await this.options.call('terminal.browser.hubPreview', { id: entry.id, version: entry.version });
200
+ this.showContent(page.content + (page.done ? '' : '\n\n[Preview limited to first page.]'));
201
+ } });
201
202
  if (!installed)
202
203
  actions.push({ id: 'install', label: `Install ${entry.version}`, run: async () => {
203
204
  if (!await this.options.ui.confirm(`Install ${entry.title}?`, `${entry.description}\nPublisher: ${entry.publisher}\nLicense: ${entry.license}\nSource: ${entry.sourceUrl}\nVersion: ${entry.version}\nInstallation does not grant trust, enablement, credentials or permissions.`))
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The Hub page: the reviewed catalog of skills, plugins and reusable documents. Installing asks first,
2
+ * The Hub page: the Community catalog and all installed Hub receipts. Installing asks first,
3
3
  * installs exactly the listed version, and leaves code switched off and untrusted.
4
4
  */
5
5
  import type { Theme } from '@earendil-works/pi-coding-agent';
@@ -15,12 +15,20 @@ export interface HubPageOptions {
15
15
  }
16
16
  export declare class HubPage implements TerminalPage {
17
17
  private readonly options;
18
+ readonly presentation = "fullscreen";
18
19
  private catalog;
19
20
  private section;
20
21
  private error;
22
+ private refreshError;
23
+ private refreshing;
24
+ private previewTitle;
25
+ private previewText;
26
+ private previewOffset;
27
+ private previewRows;
21
28
  private busy;
22
29
  private disposed;
23
30
  private readonly list;
31
+ private pointerLayout?;
24
32
  constructor(options: HubPageOptions);
25
33
  get focused(): boolean;
26
34
  set focused(value: boolean);
@@ -29,11 +37,16 @@ export declare class HubPage implements TerminalPage {
29
37
  tabs(): readonly TabInfo[];
30
38
  activeTab(): string;
31
39
  hint(): string;
40
+ private refreshOnOpen;
32
41
  private load;
33
42
  private fill;
34
43
  private rows;
35
44
  private activateRow;
45
+ private previewEntry;
46
+ private manageReceipt;
47
+ private status;
36
48
  handleMouse(event: TerminalMouseInput): import("@earendil-works/pi-tui").TuiMouseEventResult | undefined;
37
49
  handleInput(data: string): boolean;
50
+ private entryInspector;
38
51
  render(width: number, rows: number): string[];
39
52
  }
@@ -1,47 +1,87 @@
1
1
  import { matchesKey, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
2
2
  import { installHubEntry } from "../../hubMenu.js";
3
- import { safeText } from "../chrome.js";
3
+ import { inspectorListWidth, safeText, splitColumns } from "../chrome.js";
4
4
  import { FilterList } from "../list.js";
5
5
  const SECTIONS = [
6
6
  { id: 'catalog', title: 'Catalog' },
7
7
  { id: 'installed', title: 'Installed' }
8
8
  ];
9
9
  const REFRESH = 'action:refresh';
10
+ const installedId = (id, version) => `installed:${id}@${version}`;
10
11
  export class HubPage {
11
12
  options;
13
+ presentation = 'fullscreen';
12
14
  catalog;
13
15
  section = 'catalog';
14
16
  error;
17
+ refreshError;
18
+ refreshing = false;
19
+ previewTitle;
20
+ previewText;
21
+ previewOffset = 0;
22
+ previewRows = 1;
15
23
  busy = false;
16
24
  disposed = false;
17
25
  list;
26
+ pointerLayout;
18
27
  constructor(options) {
19
28
  this.options = options;
20
29
  this.list = new FilterList({
21
- theme: options.theme, search: 'always', noun: ['entry', 'entries'], empty: 'The catalog has no entries.',
30
+ theme: options.theme, search: 'always', noun: ['entry', 'entries'], empty: 'No Community entries have been published yet.',
22
31
  onActivate: row => { void this.activateRow(row.id); }
23
32
  });
24
33
  }
25
34
  get focused() { return this.list.focused; }
26
35
  set focused(value) { this.list.focused = value; }
27
- activate() { if (!this.catalog)
28
- void this.load(false); }
36
+ activate() { void this.refreshOnOpen(); }
29
37
  dispose() { this.disposed = true; }
30
38
  tabs() { return SECTIONS; }
31
39
  activeTab() { return this.section; }
32
40
  hint() {
33
- return this.list.query ? 'Enter open · Esc clear' : this.section === 'catalog' ? 'Enter review and install · type to search' : 'Select an installed entry · type to search';
41
+ if (this.previewText !== undefined)
42
+ return '↑↓ scroll · PgUp/PgDn page · Esc back';
43
+ if (this.list.query)
44
+ return 'Enter open · Esc clear';
45
+ if (this.section === 'installed')
46
+ return 'Enter manage receipt · type to search';
47
+ const selectedId = this.list.selected?.id;
48
+ const entry = this.catalog?.entries.find(candidate => candidate.channel === 'community' && candidate.id + '@' + candidate.version === selectedId);
49
+ if (entry?.delivery.kind === 'pi-package')
50
+ return 'Enter install exact version · type to search';
51
+ return entry ? 'Enter preview or install · type to search' : 'Enter refresh catalog · type to search';
52
+ }
53
+ async refreshOnOpen() {
54
+ await this.load(false);
55
+ if (!this.disposed)
56
+ await this.load(true);
34
57
  }
35
58
  async load(refresh) {
59
+ if (refresh) {
60
+ this.refreshing = true;
61
+ this.options.changed();
62
+ }
36
63
  try {
37
64
  const catalog = await this.options.call('hub.list', refresh ? { refresh: true } : {});
38
65
  this.catalog = catalog;
39
66
  this.error = undefined;
40
- if (refresh && catalog.error)
41
- this.options.ui.notify(`${catalog.error} Using ${catalog.origin} content.`, 'warning');
67
+ if (refresh) {
68
+ this.refreshError = catalog.error ?? undefined;
69
+ if (catalog.error)
70
+ this.options.ui.notify(`${catalog.error} Using ${catalog.origin} content.`, 'warning');
71
+ }
72
+ else if (catalog.error) {
73
+ this.refreshError = catalog.error;
74
+ }
42
75
  }
43
76
  catch (error) {
44
- this.error = error instanceof Error ? error.message : String(error);
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ if (refresh)
79
+ this.refreshError = message;
80
+ else if (!this.catalog)
81
+ this.error = message;
82
+ }
83
+ finally {
84
+ this.refreshing = false;
45
85
  }
46
86
  if (this.disposed)
47
87
  return;
@@ -53,22 +93,24 @@ export class HubPage {
53
93
  }
54
94
  rows() {
55
95
  if (this.section === 'installed') {
96
+ const receipts = this.catalog?.installed ?? [];
56
97
  return [
57
- ...(this.catalog?.installed ?? []).map(receipt => ({
58
- id: 'installed:' + receipt.entry.id + '@' + receipt.entry.version,
98
+ ...receipts.map(receipt => ({
99
+ id: installedId(receipt.entry.id, receipt.entry.version),
59
100
  label: safeText(receipt.entry.title),
60
- group: receipt.entry.category,
61
- detail: (receipt.state ?? 'installed') + ' · ' + receipt.entry.version,
101
+ group: safeText(receipt.entry.category),
102
+ detail: `${receipt.entry.channel} · ${receipt.state ?? 'installed'} · ${receipt.entry.version}`,
62
103
  tone: receipt.state === 'installed' ? 'success' : 'warning',
63
- searchText: receipt.entry.title + ' ' + receipt.entry.category + ' ' + receipt.entry.id + ' ' + receipt.entry.publisher + ' ' + (receipt.state ?? '')
64
- }))
104
+ searchText: `${receipt.entry.title} ${receipt.entry.category} ${receipt.entry.id} ${receipt.entry.publisher} ${receipt.entry.channel} ${receipt.state ?? ''}`
105
+ })),
106
+ { id: REFRESH, label: '⟳ Refresh catalog', group: 'Actions', tone: 'accent' }
65
107
  ];
66
108
  }
67
- const entries = [...(this.catalog?.entries ?? [])].sort((left, right) => left.category.localeCompare(right.category));
109
+ const entries = (this.catalog?.entries ?? []).filter(entry => entry.channel === 'community').sort((left, right) => left.category.localeCompare(right.category));
68
110
  return [
69
111
  ...entries.map(entry => ({
70
112
  id: entry.id + '@' + entry.version, label: safeText(entry.title), group: safeText(entry.category),
71
- detail: entry.version, tone: 'muted', searchText: entry.title + ' ' + entry.category + ' ' + entry.id + ' ' + entry.publisher
113
+ detail: entry.version, tone: 'muted', searchText: `${entry.title} ${entry.category} ${entry.id} ${entry.publisher} ${entry.tags.join(' ')}`
72
114
  })),
73
115
  { id: REFRESH, label: '⟳ Refresh catalog', group: 'Actions', tone: 'accent' }
74
116
  ];
@@ -82,20 +124,103 @@ export class HubPage {
82
124
  await this.load(true);
83
125
  return;
84
126
  }
85
- if (id.startsWith('installed:'))
127
+ if (id.startsWith('installed:')) {
128
+ const receipt = this.catalog?.installed.find(candidate => installedId(candidate.entry.id, candidate.entry.version) === id);
129
+ if (receipt)
130
+ await this.manageReceipt(receipt);
86
131
  return;
87
- const entry = this.catalog?.entries.find(candidate => candidate.id + '@' + candidate.version === id);
88
- if (entry)
89
- await installHubEntry(this.options.ui, this.options.call, entry);
132
+ }
133
+ const entry = this.catalog?.entries.find(candidate => candidate.channel === 'community' && candidate.id + '@' + candidate.version === id);
134
+ if (entry) {
135
+ const installAction = `Install exact version ${entry.version}`;
136
+ const actions = entry.delivery.kind === 'pi-package'
137
+ ? [installAction, 'Back']
138
+ : ['Preview reviewed content', installAction, 'Back'];
139
+ const action = await this.options.ui.select(`${safeText(entry.title)} · ${entry.version}`, actions);
140
+ if (action === 'Preview reviewed content')
141
+ await this.previewEntry(entry);
142
+ else if (action === installAction) {
143
+ await installHubEntry(this.options.ui, this.options.call, entry);
144
+ await this.load(false);
145
+ }
146
+ }
90
147
  }
91
148
  finally {
92
149
  this.busy = false;
93
150
  }
94
151
  }
152
+ async previewEntry(entry) {
153
+ if (entry.delivery.kind === 'pi-package')
154
+ return;
155
+ try {
156
+ const page = await this.options.call('terminal.browser.hubPreview', { id: entry.id, version: entry.version });
157
+ this.previewTitle = safeText(entry.title);
158
+ this.previewText = page.content + (page.done ? '' : '\n\n[Preview limited to first page.]');
159
+ this.previewOffset = 0;
160
+ this.pointerLayout = undefined;
161
+ this.options.changed();
162
+ }
163
+ catch (error) {
164
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), 'error');
165
+ }
166
+ }
167
+ async manageReceipt(receipt) {
168
+ const action = await this.options.ui.select(`${receipt.entry.title} · ${receipt.entry.channel} · ${receipt.entry.version}`, ['Uninstall this version', 'Back']);
169
+ if (action !== 'Uninstall this version')
170
+ return;
171
+ const confirmed = await this.options.ui.confirm(`Uninstall ${receipt.entry.title}?`, `This removes only resources recorded by the ${receipt.entry.channel} receipt ${receipt.entry.id}@${receipt.entry.version} and revokes their attachment choices.`);
172
+ if (!confirmed)
173
+ return;
174
+ try {
175
+ await this.options.call('hub.uninstall', { id: receipt.entry.id, version: receipt.entry.version });
176
+ this.options.ui.notify(`Uninstalled ${receipt.entry.title} ${receipt.entry.version}.`, 'info');
177
+ await this.load(false);
178
+ }
179
+ catch (error) {
180
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), 'error');
181
+ }
182
+ }
183
+ status() {
184
+ const catalog = this.catalog;
185
+ const error = this.refreshError ?? catalog.error;
186
+ const lastRefresh = catalog.refreshedAt ? ` · last refreshed ${catalog.refreshedAt}` : '';
187
+ if (this.refreshing)
188
+ return { text: `${catalog.origin} catalog · refreshing…${lastRefresh}`, tone: 'muted' };
189
+ if (error)
190
+ return { text: `${catalog.origin} catalog · refresh failed: ${error}${lastRefresh}`, tone: 'warning' };
191
+ return { text: `${catalog.origin} catalog${catalog.refreshedAt ? ` · refreshed ${catalog.refreshedAt}` : ''}`, tone: 'dim' };
192
+ }
95
193
  handleMouse(event) {
96
- return this.list.handleMouse(event);
194
+ if (this.previewText !== undefined)
195
+ return { handled: true, render: false };
196
+ const layout = this.pointerLayout;
197
+ if (!layout)
198
+ return undefined;
199
+ if (event.x < 0 || event.x >= layout.width || event.y < layout.top || event.y >= layout.top + layout.rows)
200
+ return { handled: true, render: false };
201
+ return this.list.handleMouse({ ...event, y: event.y - layout.top });
97
202
  }
98
203
  handleInput(data) {
204
+ if (this.previewText !== undefined) {
205
+ if (matchesKey(data, 'escape')) {
206
+ this.previewTitle = undefined;
207
+ this.previewText = undefined;
208
+ this.previewOffset = 0;
209
+ this.options.changed();
210
+ return true;
211
+ }
212
+ const page = Math.max(1, this.previewRows);
213
+ const delta = matchesKey(data, 'down') ? 1 : matchesKey(data, 'up') ? -1
214
+ : matchesKey(data, 'pageDown') ? page : matchesKey(data, 'pageUp') ? -page : 0;
215
+ if (matchesKey(data, 'home'))
216
+ this.previewOffset = 0;
217
+ else if (matchesKey(data, 'end'))
218
+ this.previewOffset = Number.MAX_SAFE_INTEGER;
219
+ else
220
+ this.previewOffset += delta;
221
+ this.options.changed();
222
+ return true;
223
+ }
99
224
  if (matchesKey(data, 'tab') || matchesKey(data, 'shift+tab')) {
100
225
  if (!this.list.query) {
101
226
  this.section = this.section === 'catalog' ? 'installed' : 'catalog';
@@ -105,22 +230,61 @@ export class HubPage {
105
230
  }
106
231
  return this.list.handleInput(data);
107
232
  }
233
+ entryInspector(entry, installed, width, rows) {
234
+ const theme = this.options.theme();
235
+ if (width < 1 || rows < 1)
236
+ return [];
237
+ if (!entry)
238
+ return [theme.fg('muted', 'Select an entry to review its publisher, license, and version.')].slice(0, rows);
239
+ const lines = [theme.bold(safeText(entry.title)), '', ...wrapTextWithAnsi(safeText(entry.description), width), ''];
240
+ lines.push(`Category: ${safeText(entry.category)}`, `Publisher: ${safeText(entry.publisher)}`, `License: ${safeText(entry.license)}`, `Version: ${safeText(entry.version)}`, `Channel: ${safeText(entry.channel)}`, `ID: ${safeText(entry.id)}`);
241
+ lines.push('', ...wrapTextWithAnsi(theme.fg('dim', `Source: ${safeText(entry.sourceUrl)}`), width));
242
+ if (installed)
243
+ lines.push('', `Installed: ${safeText(installed.installedAt)}`, `Receipt: ${safeText(installed.state ?? 'installed')} · ${safeText(installed.entry.channel)}`);
244
+ if (entry.delivery.kind === 'pi-package')
245
+ lines.push('', theme.fg('warning', 'Package code stays switched off and untrusted until reviewed.'));
246
+ return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
247
+ }
108
248
  render(width, rows) {
109
249
  const theme = this.options.theme();
110
250
  if (width < 1 || rows < 1)
111
251
  return [];
112
- if (!this.catalog)
113
- return [theme.fg(this.error ? 'error' : 'dim', this.error ? 'Could not read the catalog: ' + this.error : 'Reading the catalog…')];
252
+ if (this.previewText !== undefined) {
253
+ const content = this.previewText.split('\n').flatMap(line => wrapTextWithAnsi(safeText(line), width));
254
+ this.previewRows = Math.max(1, rows - 1);
255
+ this.previewOffset = Math.min(this.previewOffset, Math.max(0, content.length - this.previewRows));
256
+ const header = theme.bold(`Preview · ${this.previewTitle ?? 'Hub entry'} · ${this.previewOffset + 1}/${Math.max(1, content.length)}`);
257
+ return [header, ...content.slice(this.previewOffset, this.previewOffset + this.previewRows)]
258
+ .slice(0, rows).map(line => truncateToWidth(line, width, ''));
259
+ }
260
+ if (!this.catalog) {
261
+ this.pointerLayout = undefined;
262
+ return [theme.fg(this.error ? 'error' : 'dim', this.error ? 'Could not read the catalog: ' + safeText(this.error) : 'Reading the catalog…')];
263
+ }
114
264
  const selectedId = this.list.selected?.id;
115
265
  const installed = this.section === 'installed'
116
- ? this.catalog.installed.find(receipt => 'installed:' + receipt.entry.id + '@' + receipt.entry.version === selectedId)
266
+ ? this.catalog.installed.find(receipt => installedId(receipt.entry.id, receipt.entry.version) === selectedId)
117
267
  : undefined;
118
- const entry = installed?.entry ?? this.catalog.entries.find(candidate => candidate.id + '@' + candidate.version === selectedId);
268
+ const entry = installed?.entry ?? this.catalog.entries.find(candidate => candidate.channel === 'community' && candidate.id + '@' + candidate.version === selectedId);
269
+ const status = this.status();
270
+ const statusLine = theme.fg(status.tone, truncateToWidth(safeText(status.text), width, ''));
271
+ const paneWidth = inspectorListWidth(width);
272
+ if (paneWidth !== undefined) {
273
+ const bodyRows = Math.max(0, rows - 1);
274
+ const rightWidth = Math.max(0, width - paneWidth - 3);
275
+ const listLines = this.list.render(paneWidth, bodyRows);
276
+ this.pointerLayout = { width: paneWidth, top: 1, rows: listLines.length };
277
+ return [statusLine, ...splitColumns(listLines, this.entryInspector(entry, installed, rightWidth, bodyRows), paneWidth, width, bodyRows, theme)]
278
+ .slice(0, rows).map(line => truncateToWidth(line, width, ''));
279
+ }
119
280
  const detail = entry && rows >= 9
120
- ? [...wrapTextWithAnsi(safeText(entry.description), width).slice(0, 3), entry.publisher + ' · ' + entry.license + ' · ' + entry.id,
121
- ...(installed ? ['Installed ' + installed.installedAt + ' · ' + (installed.state ?? 'installed')] : [])]
281
+ ? [...wrapTextWithAnsi(safeText(entry.description), width).slice(0, 3), `${entry.publisher} · ${entry.license} · ${entry.id}`,
282
+ ...(installed ? [`Installed ${installed.installedAt} · ${installed.entry.channel} · ${installed.state ?? 'installed'}`] : [])]
122
283
  : [];
123
- const lines = this.list.render(width, Math.max(2, rows - (detail.length ? detail.length + 1 : 0)));
284
+ const detailHeight = detail.length ? detail.length + 1 : 0;
285
+ const listLines = this.list.render(width, Math.max(1, rows - 1 - detailHeight));
286
+ this.pointerLayout = { width, top: 1, rows: Math.min(listLines.length, Math.max(0, rows - 1)) };
287
+ const lines = [statusLine, ...listLines];
124
288
  if (detail.length)
125
289
  lines.push('', ...detail.map(line => theme.fg('muted', safeText(line))));
126
290
  return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The Page notes page (`/plugin:notes`, `/plugin:annotate [url]`): notes a person keeps about web pages. A new note
2
+ * The Web annotations page (`/annotate:notes`, `/annotate:annotate [url]`): comments a person keeps about web pages. A new annotation
3
3
  * is captured in a visible browser window: the whole page, text the person selects, or an element
4
- * they click. A note is private until it is shared with this session, whose agent can then read it
4
+ * they click. An annotation is private until it is shared with this session, whose agent can then read it
5
5
  * with `page_notes_read` (PROP-0017).
6
6
  */
7
7
  import type { Theme } from '@earendil-works/pi-coding-agent';
@@ -18,7 +18,7 @@ export interface NotesPageOptions {
18
18
  input(title: string, placeholder?: string): Promise<string | undefined>;
19
19
  };
20
20
  changed(): void;
21
- /** Start a new note as soon as the page opens (`/plugin:annotate [url]`), on this address when given. */
21
+ /** Start a new annotation as soon as the page opens (`/annotate:annotate [url]`), on this address when given. */
22
22
  start?: {
23
23
  url?: string;
24
24
  };
@@ -36,7 +36,7 @@ export declare const NOTE_KINDS: {
36
36
  };
37
37
  export declare const NOTE_ACTIONS: {
38
38
  readonly reveal: "Show it in the browser window";
39
- readonly edit: "Edit the note";
39
+ readonly edit: "Edit the annotation";
40
40
  readonly resolve: "Mark resolved";
41
41
  readonly reopen: "Reopen";
42
42
  readonly share: "Let this session's agent read it";
@@ -65,7 +65,7 @@ export declare class NotesPage implements TerminalPage {
65
65
  rows(): ListRow<Row>[];
66
66
  private attempt;
67
67
  activateRow(row: Row): Promise<void>;
68
- /** Opens the capture window, takes what the person points at, and saves their note. */
68
+ /** Opens the capture window, takes what the person points at, and saves their annotation. */
69
69
  newNote(startUrl?: string): Promise<void>;
70
70
  private noteMenu;
71
71
  handleMouse(event: TerminalMouseInput): import("@earendil-works/pi-tui").TuiMouseEventResult | undefined;