@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,10 +1,10 @@
1
1
  import { truncateToWidth } from '@earendil-works/pi-tui';
2
2
  import { FilterList } from "../list.js";
3
- const TABS = [{ id: 'notes', title: 'Page notes' }];
3
+ const TABS = [{ id: 'notes', title: 'Web annotations' }];
4
4
  export const NOTE_KINDS = { page: 'The whole page', text: 'Text I select in the window', element: 'An element I click in the window' };
5
5
  export const NOTE_ACTIONS = {
6
6
  reveal: 'Show it in the browser window',
7
- edit: 'Edit the note',
7
+ edit: 'Edit the annotation',
8
8
  resolve: 'Mark resolved',
9
9
  reopen: 'Reopen',
10
10
  share: 'Let this session\'s agent read it',
@@ -31,7 +31,7 @@ export class NotesPage {
31
31
  constructor(options) {
32
32
  this.options = options;
33
33
  this.list = new FilterList({
34
- theme: options.theme, search: 'always', noun: ['note', 'notes'], empty: 'No page notes yet',
34
+ theme: options.theme, search: 'always', noun: ['annotation', 'annotations'], empty: 'No web annotations yet',
35
35
  onActivate: row => { if (row.data)
36
36
  void this.activateRow(row.data); }
37
37
  });
@@ -67,10 +67,10 @@ export class NotesPage {
67
67
  return note.grants.some(grant => grant.principal.sessionId && grant.principal.sessionId === this.sessionId);
68
68
  }
69
69
  rows() {
70
- const rows = [{ id: 'new', label: 'New note on a page…', group: 'Actions', detail: 'opens a browser window', tone: 'accent', data: { kind: 'new' } }];
70
+ const rows = [{ id: 'new', label: 'New web annotation…', group: 'Actions', detail: 'opens a browser window', tone: 'accent', data: { kind: 'new' } }];
71
71
  for (const note of this.notes ?? []) {
72
72
  rows.push({
73
- id: `note:${note.id}`, label: clip(note.body.replace(/\s+/g, ' '), 60), group: 'Notes',
73
+ id: `note:${note.id}`, label: clip(note.body.replace(/\s+/g, ' '), 60), group: 'Annotations',
74
74
  detail: `${describeTarget(note.target)} · ${note.status}${this.shared(note) ? ' · shared here' : ''}`,
75
75
  tone: note.status === 'resolved' ? 'dim' : this.shared(note) ? 'success' : 'muted',
76
76
  searchText: `${note.body} ${note.source.url} ${note.source.title} ${note.tags.join(' ')}`,
@@ -101,7 +101,7 @@ export class NotesPage {
101
101
  return this.newNote();
102
102
  return this.noteMenu(row.note);
103
103
  }
104
- /** Opens the capture window, takes what the person points at, and saves their note. */
104
+ /** Opens the capture window, takes what the person points at, and saves their annotation. */
105
105
  newNote(startUrl) {
106
106
  const { call, ui } = this.options;
107
107
  return this.attempt(async () => {
@@ -109,7 +109,7 @@ export class NotesPage {
109
109
  if (!url)
110
110
  return;
111
111
  await call('terminal.annotations.open', { url });
112
- const kind = await ui.select('What does the note point at?', Object.values(NOTE_KINDS));
112
+ const kind = await ui.select('What does the annotation point at?', Object.values(NOTE_KINDS));
113
113
  if (!kind)
114
114
  return;
115
115
  let envelope;
@@ -127,18 +127,18 @@ export class NotesPage {
127
127
  }
128
128
  envelope = await call('terminal.annotations.capture', { captureId });
129
129
  if (!envelope)
130
- return 'Nothing was selected in the browser window, so no note was made.';
130
+ return 'Nothing was selected in the browser window, so no annotation was made.';
131
131
  }
132
- const body = await ui.input('Note', 'What should be remembered about it?');
132
+ const body = await ui.input('Comment', 'What should change or be remembered?');
133
133
  if (!body?.trim())
134
- return 'A note needs some text; nothing was saved.';
134
+ return 'An annotation needs a comment; nothing was saved.';
135
135
  const tags = (await ui.input('Tags (optional, comma separated)', '') ?? '').split(',').map(tag => tag.trim()).filter(Boolean);
136
- const share = await ui.confirm('Share it with this session?', 'This session\'s agent can then read the note with page_notes_read. You can stop sharing it later.');
136
+ const share = await ui.confirm('Share it with this session?', 'This session\'s agent can then read the annotation with page_notes_read. You can stop sharing it later.');
137
137
  await call('terminal.annotations.create', {
138
138
  source: { url: envelope.url, title: envelope.title, capturedAt: new Date().toISOString(), surface: 'terminal', representationHint: envelope.documentId },
139
139
  target: envelope.target, body, tags, share
140
140
  });
141
- return share ? 'Note saved and shared with this session.' : 'Note saved. It stays private until you share it.';
141
+ return share ? 'Annotation saved and shared with this session.' : 'Annotation saved. It stays private until you share it.';
142
142
  });
143
143
  }
144
144
  async noteMenu(note) {
@@ -163,7 +163,7 @@ export class NotesPage {
163
163
  if (!body?.trim() || body === note.body)
164
164
  return;
165
165
  await call('terminal.annotations.update', { id, expectedVersion: note.version, patch: { body } });
166
- return 'Note saved.';
166
+ return 'Annotation saved.';
167
167
  });
168
168
  if (choice === NOTE_ACTIONS.resolve || choice === NOTE_ACTIONS.reopen)
169
169
  return this.attempt(async () => {
@@ -172,14 +172,14 @@ export class NotesPage {
172
172
  if (choice === NOTE_ACTIONS.share || choice === NOTE_ACTIONS.unshare)
173
173
  return this.attempt(async () => {
174
174
  await call(choice === NOTE_ACTIONS.share ? 'terminal.annotations.share' : 'terminal.annotations.unshare', { id });
175
- return choice === NOTE_ACTIONS.share ? 'This session\'s agent can read the note.' : 'The note is no longer shared with this session.';
175
+ return choice === NOTE_ACTIONS.share ? 'This session\'s agent can read the annotation.' : 'The annotation is no longer shared with this session.';
176
176
  });
177
177
  if (choice === NOTE_ACTIONS.delete)
178
178
  return this.attempt(async () => {
179
- if (!await ui.confirm('Delete this note?', 'It cannot be restored.'))
179
+ if (!await ui.confirm('Delete this annotation?', 'It cannot be restored.'))
180
180
  return;
181
181
  await call('terminal.annotations.delete', { id, expectedVersion: note.version });
182
- return 'Note deleted.';
182
+ return 'Annotation deleted.';
183
183
  });
184
184
  }
185
185
  handleMouse(event) {
@@ -193,9 +193,9 @@ export class NotesPage {
193
193
  if (width < 1 || rows < 1)
194
194
  return [];
195
195
  if (this.error)
196
- return [theme.fg('error', `Could not read page notes: ${this.error}`)];
196
+ return [theme.fg('error', `Could not read web annotations: ${this.error}`)];
197
197
  if (!this.notes)
198
- return [theme.fg('dim', 'Reading page notes…')];
198
+ return [theme.fg('dim', 'Reading web annotations…')];
199
199
  return this.list.render(width, rows).slice(0, rows).map(line => truncateToWidth(line, width, ''));
200
200
  }
201
201
  }
@@ -1,8 +1,3 @@
1
- /**
2
- * The Plugins page: what this session loads, switched on or off, with trust for plugin code.
3
- * Enter opens what can be done with a row; nothing changes without a choice made here. A plugin
4
- * with settings has a Settings row that opens them as a subpage; Esc goes back to the list.
5
- */
6
1
  import type { Theme } from '@earendil-works/pi-coding-agent';
7
2
  import { type MenuCall, type MenuUi } from "../../pluginMenu.js";
8
3
  import type { TabInfo } from "../chrome.js";
@@ -18,14 +13,17 @@ export interface PluginsPageOptions {
18
13
  }
19
14
  export declare class PluginsPage implements TerminalPage {
20
15
  private readonly options;
16
+ readonly presentation = "fullscreen";
21
17
  private plugins;
22
18
  private section;
23
19
  private projectId;
20
+ private hubManagedIds;
24
21
  private error;
25
22
  private restart;
26
23
  private busy;
27
24
  private disposed;
28
25
  private readonly list;
26
+ private pointerLayout?;
29
27
  /** The plugin whose settings are open, or null on the list. */
30
28
  private settingsFor;
31
29
  private saved;
@@ -41,17 +39,21 @@ export declare class PluginsPage implements TerminalPage {
41
39
  private settingsPlugin;
42
40
  private reload;
43
41
  private fill;
44
- /** The menu rows, with a Settings row after the last row of each plugin that has settings. */
42
+ /** Switch, trust, settings and package actions stay grouped under their owning plugin. */
45
43
  private rows;
46
44
  private menu;
47
45
  private key;
48
46
  private toRow;
49
47
  private open;
48
+ private installPackage;
49
+ private managePackage;
50
50
  private back;
51
51
  /** Flips a switch, or asks for a number inside the setting's range, and saves it. */
52
52
  private change;
53
53
  handleMouse(event: TerminalMouseInput): import("@earendil-works/pi-tui").TuiMouseEventResult | undefined;
54
54
  handleInput(data: string): boolean;
55
+ private selectedPlugin;
56
+ private pluginInspector;
55
57
  render(width: number, rows: number): string[];
56
58
  private renderSettings;
57
59
  }
@@ -1,12 +1,21 @@
1
+ /**
2
+ * The Plugins page: what this session loads, switched on or off, with trust for plugin code.
3
+ * Enter opens what can be done with a row; nothing changes without a choice made here. A plugin
4
+ * with settings has a Settings row that opens them as a subpage; Esc goes back to the list.
5
+ */
6
+ import { describePlugin, surfaceBadge } from "../../../plugins/surfaces.js";
1
7
  import { matchesKey, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
2
8
  import { BACK, applyPluginAction, menuRows, rowActions } from "../../pluginMenu.js";
3
- import { safeText } from "../chrome.js";
9
+ import { inspectorListWidth, safeText, splitColumns } from "../chrome.js";
4
10
  import { FilterList } from "../list.js";
5
11
  const SECTIONS = [
6
12
  { id: 'plugins', title: 'Plugins' },
7
- { id: 'trust', title: 'Trust' }
13
+ { id: 'trust', title: 'Trust' },
14
+ { id: 'packages', title: 'Packages' }
8
15
  ];
9
16
  const SETTINGS_ROW = 'settings:';
17
+ const PACKAGE_ROW = 'package:';
18
+ const INSTALL_PACKAGE = 'action:install-package';
10
19
  const isSwitch = (setting) => setting.min === 0 && setting.max === 1;
11
20
  const settingValueText = (setting) => isSwitch(setting)
12
21
  ? setting.value === 1 ? 'on' : 'off'
@@ -14,14 +23,17 @@ const settingValueText = (setting) => isSwitch(setting)
14
23
  const settingsOf = (plugin) => plugin.settings ?? [];
15
24
  export class PluginsPage {
16
25
  options;
26
+ presentation = 'fullscreen';
17
27
  plugins;
18
28
  section = 'plugins';
19
29
  projectId = null;
30
+ hubManagedIds = new Set();
20
31
  error;
21
32
  restart = false;
22
33
  busy = false;
23
34
  disposed = false;
24
35
  list;
36
+ pointerLayout;
25
37
  /** The plugin whose settings are open, or null on the list. */
26
38
  settingsFor = null;
27
39
  saved = false;
@@ -64,6 +76,12 @@ export class PluginsPage {
64
76
  this.projectId = (await call('plugins.session').catch(() => ({ projectId: null }))).projectId;
65
77
  this.plugins = await call('plugins.list');
66
78
  this.error = undefined;
79
+ this.hubManagedIds = new Set();
80
+ try {
81
+ const overview = await call('hub.list');
82
+ this.hubManagedIds = new Set(overview.installed.filter(receipt => receipt.entry.delivery.kind === 'pi-package').flatMap(receipt => receipt.resourceIds));
83
+ }
84
+ catch { /* Package actions still go through the receipt-aware Harness service. */ }
67
85
  }
68
86
  catch (error) {
69
87
  this.error = error instanceof Error ? error.message : String(error);
@@ -85,8 +103,17 @@ export class PluginsPage {
85
103
  searchText: `${setting.label} ${setting.description}`
86
104
  })));
87
105
  }
88
- /** The menu rows, with a Settings row after the last row of each plugin that has settings. */
106
+ /** Switch, trust, settings and package actions stay grouped under their owning plugin. */
89
107
  rows() {
108
+ if (this.section === 'packages')
109
+ return [
110
+ ...(this.plugins ?? []).filter(plugin => plugin.install).map(plugin => {
111
+ const source = plugin.install.kind === 'folder' ? 'local folder' : 'package';
112
+ const hub = this.hubManagedIds.has(plugin.id);
113
+ 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'}` };
114
+ }),
115
+ { id: INSTALL_PACKAGE, group: 'Packages', label: 'Install package…', detail: 'Folder, npm or Git', tone: 'accent', searchText: 'install add package plugin npm Git folder' }
116
+ ];
90
117
  const menu = this.menu();
91
118
  const out = [];
92
119
  const placed = new Set();
@@ -101,25 +128,26 @@ export class PluginsPage {
101
128
  if (this.section === 'plugins' && last && settingsOf(row.plugin).length)
102
129
  out.push(settingsRow(row.plugin));
103
130
  });
104
- if (this.section === 'plugins') {
131
+ if (this.section === 'plugins')
105
132
  for (const plugin of this.plugins ?? [])
106
133
  if (settingsOf(plugin).length && !placed.has(plugin.id))
107
134
  out.push(settingsRow(plugin));
108
- }
109
135
  return out;
110
136
  }
111
137
  menu() {
112
138
  const rows = menuRows(this.plugins ?? [], this.projectId);
113
- return this.section === 'trust'
114
- ? rows.filter(row => row.whole && Boolean(row.plugin.code))
115
- : rows.filter(row => rowActions(row, this.projectId).some(action => action.startsWith('Switch ')));
139
+ if (this.section === 'trust')
140
+ return rows.filter(row => row.whole && Boolean(row.plugin.code));
141
+ if (this.section === 'packages')
142
+ return [];
143
+ return rows.filter(row => rowActions(row, this.projectId).some(action => action.startsWith('Switch ')));
116
144
  }
117
145
  key(row) { return `${row.whole ? 'plugin' : 'item'}:${row.id}`; }
118
146
  toRow(row) {
119
147
  const kind = row.whole ? row.note : row.note.replace(/ in .*$/, '');
120
148
  return {
121
149
  id: this.key(row), group: safeText(row.plugin.title), label: row.whole ? 'Whole plugin' : safeText(row.name),
122
- detail: `${row.on ? 'on' : 'off'}${kind ? ` · ${kind}` : ''}`, tone: row.on ? 'success' : 'dim',
150
+ detail: `${row.on ? 'on' : 'off'}${kind ? ` · ${kind}` : ''}${row.whole && surfaceBadge(row.plugin.surfaces) ? ` · ${surfaceBadge(row.plugin.surfaces)}` : ''}`, tone: row.on ? 'success' : 'dim',
123
151
  searchText: `${row.name} ${row.plugin.title} ${row.note}`
124
152
  };
125
153
  }
@@ -131,12 +159,22 @@ export class PluginsPage {
131
159
  this.options.changed();
132
160
  return;
133
161
  }
162
+ if (id === INSTALL_PACKAGE) {
163
+ await this.installPackage();
164
+ return;
165
+ }
166
+ if (id.startsWith(PACKAGE_ROW)) {
167
+ const plugin = this.plugins?.find(candidate => candidate.id === id.slice(PACKAGE_ROW.length));
168
+ if (plugin?.install)
169
+ await this.managePackage(plugin);
170
+ return;
171
+ }
134
172
  const row = this.menu().find(candidate => this.key(candidate) === id);
135
173
  if (!row || this.busy)
136
174
  return;
137
175
  this.busy = true;
138
176
  try {
139
- const action = await this.options.ui.select(`${row.plugin.title}: ${row.plugin.description || 'No description.'}`, rowActions(row, this.projectId));
177
+ const action = await this.options.ui.select(`${row.plugin.title}: ${describePlugin(row.plugin)}`, rowActions(row, this.projectId));
140
178
  if (action && action !== BACK && await applyPluginAction(this.options.ui, this.options.call, row, action))
141
179
  this.restart = true;
142
180
  }
@@ -145,6 +183,74 @@ export class PluginsPage {
145
183
  }
146
184
  await this.reload();
147
185
  }
186
+ async installPackage() {
187
+ if (this.busy)
188
+ return;
189
+ this.busy = true;
190
+ try {
191
+ const source = await this.options.ui.input('Install local plugin package', 'npm:package@version, Git address, or absolute folder path');
192
+ if (!source?.trim())
193
+ return;
194
+ 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.`);
195
+ if (!confirmed)
196
+ return;
197
+ await this.options.call('plugins.install', { source: source.trim() });
198
+ this.restart = true;
199
+ this.options.ui.notify('Installed switched off. Changes apply when this session restarts.', 'info');
200
+ await this.reload();
201
+ }
202
+ catch (error) {
203
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), 'error');
204
+ }
205
+ finally {
206
+ this.busy = false;
207
+ if (!this.disposed)
208
+ this.options.changed();
209
+ }
210
+ }
211
+ async managePackage(plugin) {
212
+ if (this.busy || !plugin.install)
213
+ return;
214
+ const hubManaged = this.hubManagedIds.has(plugin.id);
215
+ const actions = [
216
+ ...(hubManaged || plugin.install.kind === 'folder' ? [] : ['Update package']),
217
+ hubManaged ? 'Uninstall Hub version' : 'Remove package',
218
+ BACK
219
+ ];
220
+ this.busy = true;
221
+ try {
222
+ const action = await this.options.ui.select(`${plugin.title}: ${plugin.install.source}`, actions);
223
+ if (!action || action === BACK)
224
+ return;
225
+ if (action === 'Update package') {
226
+ 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.');
227
+ if (!confirmed)
228
+ return;
229
+ await this.options.call('plugins.update', { pluginId: plugin.id });
230
+ this.options.ui.notify('Updated. Changes apply when this session restarts.', 'info');
231
+ }
232
+ else {
233
+ const title = hubManaged ? `Uninstall ${plugin.title} from Hub?` : `Remove ${plugin.title}?`;
234
+ const message = hubManaged
235
+ ? 'This removes the exact Hub receipt and only its recorded package resources.'
236
+ : 'This removes the installed package. A local folder is moved to the Harness trash.';
237
+ if (!await this.options.ui.confirm(title, message))
238
+ return;
239
+ await this.options.call('plugins.remove', { pluginId: plugin.id });
240
+ this.options.ui.notify(hubManaged ? 'Hub package uninstalled.' : 'Package removed.', 'info');
241
+ }
242
+ this.restart = true;
243
+ await this.reload();
244
+ }
245
+ catch (error) {
246
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), 'error');
247
+ }
248
+ finally {
249
+ this.busy = false;
250
+ if (!this.disposed)
251
+ this.options.changed();
252
+ }
253
+ }
148
254
  back() {
149
255
  this.settingsFor = null;
150
256
  this.saved = false;
@@ -190,7 +296,12 @@ export class PluginsPage {
190
296
  }
191
297
  }
192
298
  handleMouse(event) {
193
- return this.settingsPlugin() ? this.settingsList.handleMouse(event) : this.list.handleMouse(event);
299
+ const layout = this.pointerLayout;
300
+ if (!layout)
301
+ return undefined;
302
+ if (event.x < 0 || event.x >= layout.width || event.y < layout.top || event.y >= layout.top + layout.rows)
303
+ return { handled: true, render: false };
304
+ return layout.list.handleMouse({ ...event, y: event.y - layout.top });
194
305
  }
195
306
  handleInput(data) {
196
307
  if (this.settingsPlugin()) {
@@ -200,44 +311,111 @@ export class PluginsPage {
200
311
  }
201
312
  if (matchesKey(data, 'tab') || matchesKey(data, 'shift+tab')) {
202
313
  if (!this.list.query) {
203
- this.section = this.section === 'plugins' ? 'trust' : 'plugins';
314
+ const direction = matchesKey(data, 'shift+tab') ? -1 : 1;
315
+ const current = SECTIONS.findIndex(section => section.id === this.section);
316
+ this.section = SECTIONS[(current + direction + SECTIONS.length) % SECTIONS.length].id;
204
317
  this.fill();
205
318
  }
206
319
  return true;
207
320
  }
208
321
  return this.list.handleInput(data);
209
322
  }
323
+ selectedPlugin() {
324
+ const selectedId = this.list.selected?.id;
325
+ return selectedId?.startsWith(SETTINGS_ROW)
326
+ ? this.plugins?.find(candidate => `${SETTINGS_ROW}${candidate.id}` === selectedId)
327
+ : selectedId?.startsWith(PACKAGE_ROW)
328
+ ? this.plugins?.find(candidate => `${PACKAGE_ROW}${candidate.id}` === selectedId)
329
+ : this.menu().find(candidate => this.key(candidate) === selectedId)?.plugin;
330
+ }
331
+ pluginInspector(plugin, width, rows) {
332
+ const theme = this.options.theme();
333
+ if (width < 1 || rows < 1)
334
+ return [];
335
+ if (!plugin) {
336
+ const action = this.list.selected?.id === INSTALL_PACKAGE;
337
+ 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, ''));
338
+ }
339
+ const selectedId = this.list.selected?.id;
340
+ const row = this.menu().find(candidate => this.key(candidate) === selectedId);
341
+ const title = row ? row.whole ? row.plugin.title : row.name : plugin.title;
342
+ const lines = [theme.bold(safeText(title))];
343
+ if (title !== plugin.title)
344
+ lines.push(theme.fg('dim', `Part of ${safeText(plugin.title)}`));
345
+ lines.push('', ...wrapTextWithAnsi(safeText(describePlugin(plugin)), width));
346
+ if (row) {
347
+ const status = this.section === 'trust' && row.whole
348
+ ? plugin.trust === 'trusted' ? 'Code trusted' : 'Code not trusted'
349
+ : row.on ? 'Enabled' : 'Disabled';
350
+ lines.push('', theme.fg(row.on ? 'success' : 'dim', status));
351
+ }
352
+ if (plugin.code) {
353
+ lines.push(theme.fg('muted', `Code trust: ${plugin.trust === 'trusted' ? 'trusted' : 'not trusted'}`));
354
+ lines.push(theme.fg('muted', `Fingerprint: ${plugin.code.hash.slice(0, 12)}`));
355
+ }
356
+ if (plugin.install) {
357
+ lines.push('', theme.fg('muted', `Package: ${this.hubManagedIds.has(plugin.id) ? 'Hub receipt' : 'local'}`));
358
+ lines.push(...wrapTextWithAnsi(theme.fg('dim', safeText(plugin.install.source)), width));
359
+ }
360
+ if (settingsOf(plugin).length)
361
+ lines.push('', theme.fg('muted', `${settingsOf(plugin).length} settings`));
362
+ return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
363
+ }
210
364
  render(width, rows) {
211
365
  const theme = this.options.theme();
212
366
  if (width < 1 || rows < 1)
213
367
  return [];
214
- if (!this.plugins)
368
+ if (!this.plugins) {
369
+ this.pointerLayout = undefined;
215
370
  return [theme.fg(this.error ? 'error' : 'dim', this.error ? `Could not read plugins: ${this.error}` : 'Reading plugins…')];
371
+ }
216
372
  const plugin = this.settingsPlugin();
217
373
  if (plugin)
218
374
  return this.renderSettings(plugin, width, rows);
219
- const selectedId = this.list.selected?.id;
220
- const selected = selectedId?.startsWith(SETTINGS_ROW)
221
- ? this.plugins.find(candidate => `${SETTINGS_ROW}${candidate.id}` === selectedId)
222
- : this.menu().find(candidate => this.key(candidate) === selectedId)?.plugin;
223
- const detail = selected && rows >= 8 ? wrapTextWithAnsi(safeText(selected.description || 'No description.'), width).slice(0, 2) : [];
224
- const note = this.restart ? ['Saved. Changes apply when this session restarts.'] : [];
225
- const extra = detail.length + note.length;
226
- const lines = this.list.render(width, Math.max(2, rows - (extra ? extra + 1 : 0)));
227
- if (extra)
228
- lines.push('', ...detail.map(line => theme.fg('muted', line)), ...note.map(line => theme.fg('warning', line)));
375
+ const note = this.restart ? [theme.fg('warning', 'Saved. Changes apply when this session restarts.')] : [];
376
+ const paneWidth = inspectorListWidth(width);
377
+ if (paneWidth !== undefined) {
378
+ const bodyRows = Math.max(0, rows - note.length);
379
+ const rightWidth = Math.max(0, width - paneWidth - 3);
380
+ const listLines = this.list.render(paneWidth, bodyRows);
381
+ this.pointerLayout = { list: this.list, width: paneWidth, top: note.length, rows: listLines.length };
382
+ return [...note, ...splitColumns(listLines, this.pluginInspector(this.selectedPlugin(), rightWidth, bodyRows), paneWidth, width, bodyRows, theme)]
383
+ .slice(0, rows).map(line => truncateToWidth(line, width, ''));
384
+ }
385
+ const selected = this.selectedPlugin();
386
+ const detail = selected && rows >= 8 ? wrapTextWithAnsi(safeText(describePlugin(selected)), width).slice(0, 3) : [];
387
+ const listRows = Math.max(2, rows - note.length - (detail.length ? detail.length + 1 : 0));
388
+ const listLines = this.list.render(width, listRows);
389
+ this.pointerLayout = { list: this.list, width, top: note.length, rows: Math.min(listLines.length, Math.max(0, rows - note.length)) };
390
+ const lines = [...note, ...listLines];
391
+ if (detail.length)
392
+ lines.push('', ...detail.map(line => theme.fg('muted', line)));
229
393
  return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
230
394
  }
231
395
  renderSettings(plugin, width, rows) {
232
396
  const theme = this.options.theme();
233
397
  const setting = settingsOf(plugin).find(candidate => candidate.key === this.settingsList.selected?.id);
234
398
  const range = setting && !isSwitch(setting) ? [`From ${setting.min.toLocaleString('en-US')} to ${setting.max.toLocaleString('en-US')} · default ${setting.default.toLocaleString('en-US')}`] : [];
399
+ const note = this.saved ? [theme.fg('success', 'Saved.')] : [];
400
+ const paneWidth = inspectorListWidth(width);
401
+ if (paneWidth !== undefined) {
402
+ const bodyRows = Math.max(0, rows - note.length);
403
+ const rightWidth = Math.max(0, width - paneWidth - 3);
404
+ const listLines = this.settingsList.render(paneWidth, bodyRows);
405
+ const details = setting
406
+ ? [theme.bold(safeText(setting.label)), '', ...wrapTextWithAnsi(safeText(setting.description), rightWidth), '', ...range.map(line => theme.fg('muted', line)), theme.fg('dim', `Current value: ${settingValueText(setting)}`)]
407
+ : [theme.fg('muted', 'Select a setting to review its value and description.')];
408
+ this.pointerLayout = { list: this.settingsList, width: paneWidth, top: note.length, rows: listLines.length };
409
+ return [...note, ...splitColumns(listLines, details, paneWidth, width, bodyRows, theme)]
410
+ .slice(0, rows).map(line => truncateToWidth(line, width, ''));
411
+ }
235
412
  const detail = setting && rows >= 8 ? [...wrapTextWithAnsi(safeText(setting.description), width).slice(0, 3), ...range] : [];
236
- const note = this.saved ? ['Saved.'] : [];
237
- const extra = detail.length + note.length;
238
- const lines = this.settingsList.render(width, Math.max(2, rows - (extra ? extra + 1 : 0)));
239
- if (extra)
240
- lines.push('', ...detail.map(line => theme.fg('muted', line)), ...note.map(line => theme.fg('success', line)));
413
+ const listRows = Math.max(2, rows - note.length - (detail.length ? detail.length + 1 : 0));
414
+ const listLines = this.settingsList.render(width, listRows);
415
+ this.pointerLayout = { list: this.settingsList, width, top: note.length, rows: Math.min(listLines.length, Math.max(0, rows - note.length)) };
416
+ const lines = [...note, ...listLines];
417
+ if (detail.length)
418
+ lines.push('', ...detail.map(line => theme.fg('muted', line)));
241
419
  return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
242
420
  }
243
421
  }
@@ -6,6 +6,7 @@ export interface SubagentsPageOptions {
6
6
  theme(): Theme;
7
7
  snapshot(): readonly TerminalSubagentActivity[];
8
8
  changed(): void;
9
+ now?(): number;
9
10
  }
10
11
  export declare class SubagentsPage implements TerminalPage {
11
12
  private readonly options;