@xingular/harness 1.1.1 → 1.2.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 (223) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +28 -25
  3. package/dist/cli.js +6675 -2501
  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 +15 -1
  11. package/dist/sdk/agents/planResearch.d.ts +5 -0
  12. package/dist/sdk/agents/planResearch.js +19 -0
  13. package/dist/sdk/agents/profiles.d.ts +2 -1
  14. package/dist/sdk/agents/profiles.js +9 -4
  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/cli.js +5 -4
  26. package/dist/sdk/commands/list.d.ts +3 -0
  27. package/dist/sdk/commands/list.js +75 -0
  28. package/dist/sdk/commands/registry.d.ts +250 -0
  29. package/dist/sdk/commands/registry.js +47 -0
  30. package/dist/sdk/commands/routes.d.ts +16 -0
  31. package/dist/sdk/commands/routes.js +33 -0
  32. package/dist/sdk/core/commandBudget.d.ts +23 -0
  33. package/dist/sdk/core/commandBudget.js +57 -0
  34. package/dist/sdk/core/index.js +17 -1
  35. package/dist/sdk/core/interactiveProfile.d.ts +46 -0
  36. package/dist/sdk/core/interactiveProfile.js +143 -0
  37. package/dist/sdk/core/workspace.d.ts +13 -0
  38. package/dist/sdk/core/workspace.js +88 -0
  39. package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
  40. package/dist/sdk/core/workspaceBashTool.js +36 -0
  41. package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
  42. package/dist/sdk/core/workspaceSandbox.js +320 -0
  43. package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
  44. package/dist/sdk/core/workspaceWriteOperations.js +206 -0
  45. package/dist/sdk/doctor/types.d.ts +2 -2
  46. package/dist/sdk/harness.d.ts +7 -0
  47. package/dist/sdk/harness.js +42 -2
  48. package/dist/sdk/index.d.ts +6 -1
  49. package/dist/sdk/index.js +3 -0
  50. package/dist/sdk/pi/runtime.d.ts +2 -1
  51. package/dist/sdk/pi/runtime.js +2 -2
  52. package/dist/sdk/plugins/builtin.js +29 -12
  53. package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
  54. package/dist/sdk/plugins/builtinRegistry.js +30 -8
  55. package/dist/sdk/plugins/catalogue.js +2 -1
  56. package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
  57. package/dist/sdk/plugins/commandPrefix.js +21 -0
  58. package/dist/sdk/plugins/firstParty.d.ts +1 -1
  59. package/dist/sdk/plugins/firstParty.js +8 -2
  60. package/dist/sdk/plugins/inventory.js +4 -3
  61. package/dist/sdk/plugins/manifest.d.ts +2 -0
  62. package/dist/sdk/plugins/manifest.js +2 -1
  63. package/dist/sdk/plugins/surfaces.d.ts +5 -0
  64. package/dist/sdk/plugins/surfaces.js +11 -0
  65. package/dist/sdk/plugins/types.d.ts +15 -0
  66. package/dist/sdk/prompt/compile.d.ts +14 -0
  67. package/dist/sdk/prompt/compile.js +65 -29
  68. package/dist/sdk/prompt/standard.d.ts +17 -0
  69. package/dist/sdk/prompt/standard.js +21 -0
  70. package/dist/sdk/server/methods.js +18 -0
  71. package/dist/sdk/server/remote.js +36 -11
  72. package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
  73. package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
  74. package/dist/sdk/sessions/continuation.d.ts +3 -0
  75. package/dist/sdk/sessions/continuation.js +5 -1
  76. package/dist/sdk/sessions/contracts.d.ts +168 -0
  77. package/dist/sdk/sessions/contracts.js +59 -1
  78. package/dist/sdk/sessions/control.d.ts +72 -0
  79. package/dist/sdk/sessions/control.js +158 -0
  80. package/dist/sdk/sessions/initialBash.d.ts +7 -0
  81. package/dist/sdk/sessions/initialBash.js +7 -0
  82. package/dist/sdk/sessions/planMode.d.ts +14 -3
  83. package/dist/sdk/sessions/planMode.js +73 -16
  84. package/dist/sdk/sessions/registry.d.ts +1 -1
  85. package/dist/sdk/sessions/registry.js +2 -2
  86. package/dist/sdk/sessions/runStore.d.ts +14 -0
  87. package/dist/sdk/sessions/runStore.js +35 -0
  88. package/dist/sdk/sessions/runner.d.ts +40 -2
  89. package/dist/sdk/sessions/runner.js +161 -13
  90. package/dist/sdk/sessions/sessions.d.ts +23 -3
  91. package/dist/sdk/sessions/sessions.js +202 -17
  92. package/dist/sdk/sessions/subagentService.d.ts +9 -0
  93. package/dist/sdk/sessions/subagentService.js +63 -4
  94. package/dist/sdk/sessions/workSlice.d.ts +103 -0
  95. package/dist/sdk/sessions/workSlice.js +297 -0
  96. package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
  97. package/dist/sdk/sessions/workspaceBoundary.js +46 -0
  98. package/dist/sdk/terminal/browserControl.js +2 -1
  99. package/dist/sdk/terminal/controller.d.ts +11 -3
  100. package/dist/sdk/terminal/controller.js +40 -13
  101. package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
  102. package/dist/sdk/terminal/opentui/theme.js +87 -0
  103. package/dist/sdk/terminal/piExtension.d.ts +6 -1
  104. package/dist/sdk/terminal/piExtension.js +154 -54
  105. package/dist/sdk/terminal/standalone.d.ts +7 -0
  106. package/dist/sdk/terminal/standalone.js +72 -13
  107. package/dist/sdk/terminal/subagentMonitor.d.ts +11 -2
  108. package/dist/sdk/terminal/subagentMonitor.js +32 -1
  109. package/dist/sdk/terminal/terminals.d.ts +19 -2
  110. package/dist/sdk/terminal/terminals.js +245 -35
  111. package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
  112. package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
  113. package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
  114. package/dist/sdk/terminal/tui/commandPages.js +3 -0
  115. package/dist/sdk/terminal/tui/commands.d.ts +2 -0
  116. package/dist/sdk/terminal/tui/commands.js +205 -124
  117. package/dist/sdk/terminal/tui/list.d.ts +14 -1
  118. package/dist/sdk/terminal/tui/list.js +96 -14
  119. package/dist/sdk/terminal/tui/pageOverlay.js +13 -4
  120. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
  121. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
  122. package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
  123. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
  124. package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
  125. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
  126. package/dist/sdk/terminal/tui/pages/subagentsPage.js +98 -16
  127. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
  128. package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
  129. package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
  130. package/dist/sdk/terminal/tui/piAdapter.js +3 -5
  131. package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
  132. package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
  133. package/dist/sdk/terminal/tui/runtime.js +23 -3
  134. package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
  135. package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
  136. package/dist/sdk/tools/checklist.js +1 -0
  137. package/dist/sdk/tools/checkpoint.d.ts +27 -0
  138. package/dist/sdk/tools/checkpoint.js +154 -0
  139. package/dist/sdk/tools/getCapabilities.d.ts +31 -0
  140. package/dist/sdk/tools/getCapabilities.js +34 -0
  141. package/dist/sdk/tools/spawnSubagent.js +4 -3
  142. package/dist/sdk/usage/analytics.d.ts +20 -0
  143. package/dist/sdk/usage/analytics.js +151 -0
  144. package/dist/sdk/usage/ledger.d.ts +38 -0
  145. package/dist/sdk/usage/ledger.js +142 -0
  146. package/dist/sdk/usage/limits/adapters.d.ts +11 -0
  147. package/dist/sdk/usage/limits/adapters.js +349 -0
  148. package/dist/sdk/usage/limits/capture.d.ts +8 -0
  149. package/dist/sdk/usage/limits/capture.js +47 -0
  150. package/dist/sdk/usage/limits/http.d.ts +12 -0
  151. package/dist/sdk/usage/limits/http.js +51 -0
  152. package/dist/sdk/usage/limits/normalize.d.ts +15 -0
  153. package/dist/sdk/usage/limits/normalize.js +211 -0
  154. package/dist/sdk/usage/limits/service.d.ts +30 -0
  155. package/dist/sdk/usage/limits/service.js +176 -0
  156. package/dist/sdk/usage/limits/types.d.ts +65 -0
  157. package/dist/sdk/usage/limits/types.js +1 -0
  158. package/dist/sdk/usage/serverMethods.d.ts +3 -0
  159. package/dist/sdk/usage/serverMethods.js +22 -0
  160. package/dist/sdk/usage/service.d.ts +32 -0
  161. package/dist/sdk/usage/service.js +66 -0
  162. package/dist/sdk/usage/settings.d.ts +7 -0
  163. package/dist/sdk/usage/settings.js +28 -0
  164. package/dist/sdk/usage/types.d.ts +60 -0
  165. package/dist/sdk/usage/types.js +4 -0
  166. package/dist/terminal-opentui.js +5207 -1883
  167. package/dist/terminal-pi.js +1658 -271
  168. package/docs/architecture/02-sessions-and-runs.md +128 -26
  169. package/docs/architecture/04-prompt.md +50 -12
  170. package/docs/architecture/06-plugins-and-capabilities.md +24 -7
  171. package/docs/architecture/07-tools-and-gate.md +104 -24
  172. package/docs/architecture/08-memory-and-history.md +4 -2
  173. package/docs/architecture/09-accounts-and-connectors.md +31 -1
  174. package/docs/architecture/10-server-protocol.md +58 -3
  175. package/docs/architecture/11-terminal.md +105 -42
  176. package/docs/architecture/12-storage.md +30 -2
  177. package/docs/architecture/13-world-tools.md +22 -1
  178. package/docs/architecture/14-surfaces-and-parity.md +54 -7
  179. package/docs/architecture/README.md +3 -1
  180. package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
  181. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
  182. package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
  183. package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
  184. package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
  185. package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
  186. package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
  187. package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
  188. package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
  189. package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
  190. package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
  191. package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
  192. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
  193. package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
  194. package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
  195. package/docs/architecture/proposals/README.md +14 -0
  196. package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
  197. package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
  198. package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
  199. package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
  200. package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
  201. package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
  202. package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
  203. package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
  204. package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
  205. package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
  206. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
  207. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
  208. package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
  209. package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
  210. package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
  211. package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
  212. package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
  213. package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
  214. package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
  215. package/docs/parity.md +80 -24
  216. package/docs/releases/1.1.1.md +28 -0
  217. package/docs/releases/1.2.0.md +49 -0
  218. package/docs/releases/4.0.0.md +10 -9
  219. package/docs/releases/README.md +35 -30
  220. package/docs/study/00-index.md +3 -0
  221. package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
  222. package/docs/terminal-tui.md +100 -59
  223. package/package.json +4 -3
@@ -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,20 @@ export class PageOverlay {
132
132
  contentWidth: contentWidth(width),
133
133
  centered
134
134
  };
135
- const footer = page.ownsNavigation ? `Esc back/close · ${page.hint()}` : [
135
+ const escapeHint = page.ownsNavigation ? 'Esc back/close' : 'Esc close';
136
+ const fullFooter = page.ownsNavigation ? `${escapeHint} · ${page.hint()}` : [
136
137
  page.hint(),
137
- tabs.length > 1 && !page.ownsNavigation ? 'Tab/Shift+Tab sections' : '',
138
- 'Esc close'
138
+ tabs.length > 1 ? 'Tab/Shift+Tab sections' : '',
139
+ escapeHint
139
140
  ].filter(Boolean).join(' · ');
141
+ const footerWidth = Math.max(0, width - CHROME_PAD * 2);
142
+ const primaryHint = page.hint().split(' · ')[0] ?? '';
143
+ const compactFooter = [
144
+ primaryHint,
145
+ !page.ownsNavigation && tabs.length > 1 ? 'Tab/⇧Tab' : '',
146
+ escapeHint
147
+ ].filter(Boolean).join(' · ');
148
+ const footer = visibleWidth(fullFooter) <= footerWidth ? fullFooter : compactFooter;
140
149
  return pageFrame({
141
150
  title: '',
142
151
  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
@@ -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;
@@ -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";
@@ -1,3 +1,9 @@
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
9
  import { safeText } from "../chrome.js";
@@ -119,7 +125,7 @@ export class PluginsPage {
119
125
  const kind = row.whole ? row.note : row.note.replace(/ in .*$/, '');
120
126
  return {
121
127
  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',
128
+ detail: `${row.on ? 'on' : 'off'}${kind ? ` · ${kind}` : ''}${row.whole && surfaceBadge(row.plugin.surfaces) ? ` · ${surfaceBadge(row.plugin.surfaces)}` : ''}`, tone: row.on ? 'success' : 'dim',
123
129
  searchText: `${row.name} ${row.plugin.title} ${row.note}`
124
130
  };
125
131
  }
@@ -136,7 +142,7 @@ export class PluginsPage {
136
142
  return;
137
143
  this.busy = true;
138
144
  try {
139
- const action = await this.options.ui.select(`${row.plugin.title}: ${row.plugin.description || 'No description.'}`, rowActions(row, this.projectId));
145
+ const action = await this.options.ui.select(`${row.plugin.title}: ${describePlugin(row.plugin)}`, rowActions(row, this.projectId));
140
146
  if (action && action !== BACK && await applyPluginAction(this.options.ui, this.options.call, row, action))
141
147
  this.restart = true;
142
148
  }
@@ -220,7 +226,7 @@ export class PluginsPage {
220
226
  const selected = selectedId?.startsWith(SETTINGS_ROW)
221
227
  ? this.plugins.find(candidate => `${SETTINGS_ROW}${candidate.id}` === selectedId)
222
228
  : 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) : [];
229
+ const detail = selected && rows >= 8 ? wrapTextWithAnsi(safeText(describePlugin(selected)), width).slice(0, 3) : [];
224
230
  const note = this.restart ? ['Saved. Changes apply when this session restarts.'] : [];
225
231
  const extra = detail.length + note.length;
226
232
  const lines = this.list.render(width, Math.max(2, rows - (extra ? extra + 1 : 0)));
@@ -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;
@@ -1,12 +1,76 @@
1
1
  import { matchesKey, stripTerminalSequences, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
2
- import { headerRow, neutralRule, padded } from "../chrome.js";
2
+ import { CHROME_PAD, headerRow, neutralRule, padded } from "../chrome.js";
3
3
  import { FilterList } from "../list.js";
4
- import { ACTIVE_SUBAGENT_STATUSES, terminalSubagentDuration, terminalSubagentTone } from "../../subagentMonitor.js";
4
+ import { ACTIVE_SUBAGENT_STATUSES, terminalSubagentDuration } from "../../subagentMonitor.js";
5
+ import { profileColor, opentuiTheme } from "../../opentui/theme.js";
5
6
  import { subagentTasks } from "../../opentui/subagentsWidget.js";
7
+ import { formatTokenCount } from "../status.js";
8
+ import { getWorkingSpinnerFrames, SPINNER_INTERVAL_MS, spinnerFrameAt } from "../workingStatus.js";
6
9
  const TABS = [{ id: 'subagents', title: 'Subagents' }];
7
10
  function clean(value) {
8
11
  return stripTerminalSequences(value).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, ' ').replace(/\s+/g, ' ').trim();
9
12
  }
13
+ function statusGlyph(item, now) {
14
+ if (item.status === 'running') {
15
+ const started = Date.parse(item.startedAt);
16
+ return spinnerFrameAt(now - (Number.isFinite(started) ? started : now), getWorkingSpinnerFrames(process.env.TERM), SPINNER_INTERVAL_MS);
17
+ }
18
+ switch (item.status) {
19
+ case 'queued': return '○';
20
+ case 'waiting_for_approval':
21
+ case 'waiting_for_input': return '●';
22
+ case 'completed': return '✓';
23
+ case 'failed': return '✕';
24
+ case 'interrupted': return '!';
25
+ }
26
+ }
27
+ function statusGlyphTone(item) {
28
+ if (item.status === 'completed')
29
+ return 'success';
30
+ if (item.status === 'failed')
31
+ return 'error';
32
+ if (item.status === 'waiting_for_approval' || item.status === 'waiting_for_input')
33
+ return 'accent';
34
+ if (item.status === 'running')
35
+ return 'warning';
36
+ return 'muted';
37
+ }
38
+ function effectiveModel(item) {
39
+ const model = item.usage?.model;
40
+ if (model)
41
+ return `${item.usage?.provider ? `${item.usage.provider}/` : ''}${model}`;
42
+ return item.model ?? '—';
43
+ }
44
+ function usageSummary(item) {
45
+ const usage = item.usage;
46
+ if (!usage)
47
+ return 'usage —';
48
+ const cache = [usage.cacheReadTokens, usage.cacheWriteTokens]
49
+ .filter((value) => typeof value === 'number' && value > 0)
50
+ .map(formatTokenCount);
51
+ const cost = typeof usage.costUsd === 'number'
52
+ ? `${usage.costSource === 'estimate' ? '~' : ''}$${usage.costUsd.toFixed(usage.costUsd > 0 && usage.costUsd < 0.001 ? 5 : usage.costUsd < 0.01 ? 4 : 2).replace(/0+$/u, '').replace(/\.$/u, '')}`
53
+ : '';
54
+ const fields = [
55
+ typeof usage.inputTokens === 'number' ? `↑${formatTokenCount(usage.inputTokens)}` : '',
56
+ typeof usage.outputTokens === 'number' ? `↓${formatTokenCount(usage.outputTokens)}` : '',
57
+ cache.some(Boolean) ? `c${cache.join('/')}` : '',
58
+ cost,
59
+ typeof usage.contextTokens === 'number' ? `ctx${formatTokenCount(usage.contextTokens)}${typeof usage.contextWindow === 'number' ? `/${formatTokenCount(usage.contextWindow)}` : ''}` : '',
60
+ typeof usage.requests === 'number' ? `req${usage.requests}` : ''
61
+ ].filter(Boolean);
62
+ return fields.length ? fields.join(' ') : 'usage —';
63
+ }
64
+ function rowSubtitle(item, width) {
65
+ const available = Math.max(1, width - 2);
66
+ const verbose = width >= 96;
67
+ const account = clean(item.accountLabel ?? '') || '—';
68
+ const model = truncateToWidth(effectiveModel(item), 24, '…');
69
+ const details = verbose
70
+ ? [`acct ${account}`, `model ${model}`, `eff ${item.effort ?? '—'}`, usageSummary(item)].join(' · ')
71
+ : [`a:${account}`, `m:${model}`, `e:${item.effort ?? '—'}`, usageSummary(item)].join(' · ');
72
+ return truncateToWidth(details, available, '…');
73
+ }
10
74
  export class SubagentsPage {
11
75
  options;
12
76
  presentation = 'fullscreen';
@@ -14,7 +78,8 @@ export class SubagentsPage {
14
78
  constructor(options) {
15
79
  this.options = options;
16
80
  this.list = new FilterList({
17
- theme: options.theme, search: 'always', noun: ['subagent', 'subagents'], empty: 'No subagents have started in this turn'
81
+ theme: options.theme, search: 'always', noun: ['subagent', 'subagents'], rowGap: 1,
82
+ empty: 'No subagents have started in the latest run'
18
83
  });
19
84
  }
20
85
  get focused() { return this.list.focused; }
@@ -22,18 +87,33 @@ export class SubagentsPage {
22
87
  tabs() { return TABS; }
23
88
  activeTab() { return 'subagents'; }
24
89
  hint() { return '↑↓ select · type to search'; }
25
- rows() {
26
- return this.options.snapshot().map(item => ({
27
- id: item.id,
28
- label: `${clean(item.profile?.name ?? '') || 'subagent'} · ${clean(item.title) || 'Subagent'}`,
29
- detail: [item.status.replaceAll('_', ' '), subagentTasks(item.checklist), terminalSubagentDuration(item)].filter(Boolean).join(' · '),
30
- tone: terminalSubagentTone(item.status),
31
- searchText: `${item.profile?.name ?? ''} ${item.title} ${item.goal ?? ''} ${item.status} ${item.activity}`,
32
- data: item
33
- }));
90
+ rows(width = 80, now = this.options.now?.() ?? Date.now()) {
91
+ const ordered = [...this.options.snapshot()].sort((left, right) => {
92
+ 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);
94
+ });
95
+ return ordered.map(item => {
96
+ const profile = clean(item.profile?.name ?? '') || 'subagent';
97
+ const title = clean(item.title) || 'Subagent';
98
+ const color = item.profile?.color ? profileColor(opentuiTheme, item.profile.color) : null;
99
+ const glyph = statusGlyph(item, now);
100
+ const labelParts = [
101
+ { text: `${glyph} `, ...(item.status === 'running' && color ? { color } : { tone: statusGlyphTone(item) }) },
102
+ { text: profile, ...(color ? { color } : { tone: 'muted' }) },
103
+ { text: ` ${title}` }
104
+ ];
105
+ return {
106
+ id: item.id,
107
+ label: labelParts.map(part => part.text).join(''),
108
+ labelParts,
109
+ subtitle: rowSubtitle(item, width),
110
+ searchText: `${profile} ${title} ${item.goal ?? ''} ${item.status} ${item.activity} ${item.accountLabel ?? ''} ${effectiveModel(item)} ${item.effort ?? ''} ${usageSummary(item)}`,
111
+ data: item
112
+ };
113
+ });
34
114
  }
35
115
  render(width, rows) {
36
- const items = this.rows();
116
+ const items = this.rows(width, this.options.now?.() ?? Date.now());
37
117
  this.list.setRows(items);
38
118
  const active = items.filter(item => item.data && ACTIVE_SUBAGENT_STATUSES.has(item.data.status)).length;
39
119
  const selected = this.list.selected?.data;
@@ -43,14 +123,16 @@ export class SubagentsPage {
43
123
  const output = [header, '', ...this.list.render(width, listRows), ''];
44
124
  if (selected) {
45
125
  const theme = this.options.theme();
46
- const textWidth = Math.max(1, width - 4);
126
+ const detailIndent = ' '.repeat(CHROME_PAD * 2);
127
+ const detailLines = (text) => wrapTextWithAnsi(theme.fg('text', clean(text)), Math.max(1, width - detailIndent.length))
128
+ .map(line => `${detailIndent}${line}`);
47
129
  output.push(neutralRule(width, theme));
48
130
  output.push(padded(theme.fg('muted', `Profile: ${clean(selected.profile?.name ?? '') || 'copy of the parent'}`), width));
49
131
  output.push(padded(theme.fg('accent', 'Task'), width));
50
- output.push(...wrapTextWithAnsi(theme.fg('text', clean(selected.goal || 'Subagent')), textWidth));
132
+ output.push(...detailLines(selected.goal || 'Subagent'));
51
133
  output.push(padded(theme.fg('muted', `Status: ${[selected.status.replaceAll('_', ' '), subagentTasks(selected.checklist), terminalSubagentDuration(selected)].filter(Boolean).join(' · ')}`), width));
52
134
  output.push(padded(theme.fg('accent', 'Current activity'), width));
53
- output.push(...wrapTextWithAnsi(theme.fg('text', clean(selected.activity)), textWidth));
135
+ output.push(...detailLines(selected.activity));
54
136
  }
55
137
  else {
56
138
  output.push(padded(this.options.theme().fg('muted', 'The selected subagent’s task and current activity appear here.'), width));
@@ -0,0 +1,43 @@
1
+ import type { Theme } from '@earendil-works/pi-coding-agent';
2
+ import type { UsagePeriod } from "../../../usage/types.js";
3
+ import type { TabInfo } from "../chrome.js";
4
+ import type { TerminalPage } from "../page.js";
5
+ type UsageTab = 'analysis' | 'limits';
6
+ interface UsagePageOptions {
7
+ theme(): Theme;
8
+ call(method: string, params?: Record<string, unknown>): Promise<unknown>;
9
+ changed(): void;
10
+ initialTab?: UsageTab;
11
+ initialPeriod?: UsagePeriod;
12
+ }
13
+ export declare class UsagePage implements TerminalPage {
14
+ private readonly options;
15
+ presentation: "centered";
16
+ focused: boolean;
17
+ private tab;
18
+ private period;
19
+ private scope;
20
+ private summary?;
21
+ private limitsSummary?;
22
+ private limits?;
23
+ private busy;
24
+ private error?;
25
+ private refreshLimits;
26
+ private offset;
27
+ private scrollRows;
28
+ private maxOffset;
29
+ private disposed;
30
+ constructor(options: UsagePageOptions);
31
+ tabs(): readonly TabInfo[];
32
+ activeTab(): string;
33
+ selectTab(id: string): void;
34
+ hint(): string;
35
+ activate(): void;
36
+ dispose(): void;
37
+ private load;
38
+ handleInput(data: string): boolean;
39
+ private analysisContent;
40
+ private limitsContent;
41
+ render(width: number, rows: number): string[];
42
+ }
43
+ export {};