@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,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,35 +87,55 @@ 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
+ // The main agent is pinned first so its children read against it.
92
+ const ordered = [...this.options.snapshot()].sort((left, right) => {
93
+ const main = Number(right.relation === 'main') - Number(left.relation === 'main');
94
+ const active = Number(ACTIVE_SUBAGENT_STATUSES.has(right.status)) - Number(ACTIVE_SUBAGENT_STATUSES.has(left.status));
95
+ return main || active || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id);
96
+ });
97
+ return ordered.map(item => {
98
+ const profile = clean(item.profile?.name ?? '') || 'subagent';
99
+ const title = clean(item.title) || 'Subagent';
100
+ const color = item.profile?.color ? profileColor(opentuiTheme, item.profile.color) : null;
101
+ const glyph = statusGlyph(item, now);
102
+ const labelParts = [
103
+ { text: `${glyph} `, ...(item.status === 'running' && color ? { color } : { tone: statusGlyphTone(item) }) },
104
+ { text: profile, ...(color ? { color } : { tone: 'muted' }) },
105
+ { text: ` ${title}` }
106
+ ];
107
+ return {
108
+ id: item.id,
109
+ label: labelParts.map(part => part.text).join(''),
110
+ labelParts,
111
+ subtitle: rowSubtitle(item, width),
112
+ searchText: `${profile} ${title} ${item.goal ?? ''} ${item.status} ${item.activity} ${item.accountLabel ?? ''} ${effectiveModel(item)} ${item.effort ?? ''} ${usageSummary(item)}`,
113
+ data: item
114
+ };
115
+ });
34
116
  }
35
117
  render(width, rows) {
36
- const items = this.rows();
118
+ const items = this.rows(width, this.options.now?.() ?? Date.now());
37
119
  this.list.setRows(items);
38
- const active = items.filter(item => item.data && ACTIVE_SUBAGENT_STATUSES.has(item.data.status)).length;
120
+ const children = items.filter(item => item.data?.relation !== 'main');
121
+ const active = children.filter(item => item.data && ACTIVE_SUBAGENT_STATUSES.has(item.data.status)).length;
39
122
  const selected = this.list.selected?.data;
40
- const header = headerRow('Subagents', `${active} active · ${items.length} total`, width, this.options.theme());
123
+ const header = headerRow('Subagents', `${active} active · ${children.length} total`, width, this.options.theme());
41
124
  const bodyRows = Math.max(0, rows - 7);
42
125
  const listRows = Math.max(1, Math.min(this.list.height, Math.floor(bodyRows * 0.6)));
43
126
  const output = [header, '', ...this.list.render(width, listRows), ''];
44
127
  if (selected) {
45
128
  const theme = this.options.theme();
46
- const textWidth = Math.max(1, width - 4);
129
+ const detailIndent = ' '.repeat(CHROME_PAD * 2);
130
+ const detailLines = (text) => wrapTextWithAnsi(theme.fg('text', clean(text)), Math.max(1, width - detailIndent.length))
131
+ .map(line => `${detailIndent}${line}`);
47
132
  output.push(neutralRule(width, theme));
48
133
  output.push(padded(theme.fg('muted', `Profile: ${clean(selected.profile?.name ?? '') || 'copy of the parent'}`), width));
49
134
  output.push(padded(theme.fg('accent', 'Task'), width));
50
- output.push(...wrapTextWithAnsi(theme.fg('text', clean(selected.goal || 'Subagent')), textWidth));
51
- output.push(padded(theme.fg('muted', `Status: ${[selected.status.replaceAll('_', ' '), subagentTasks(selected.checklist), terminalSubagentDuration(selected)].filter(Boolean).join(' · ')}`), width));
135
+ output.push(...detailLines(selected.goal || (selected.relation === 'main' ? 'Main agent' : 'Subagent')));
136
+ output.push(padded(theme.fg('muted', `Status: ${[selected.status.replaceAll('_', ' '), subagentTasks(selected.todo), terminalSubagentDuration(selected)].filter(Boolean).join(' · ')}`), width));
52
137
  output.push(padded(theme.fg('accent', 'Current activity'), width));
53
- output.push(...wrapTextWithAnsi(theme.fg('text', clean(selected.activity)), textWidth));
138
+ output.push(...detailLines(selected.activity));
54
139
  }
55
140
  else {
56
141
  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 {};
@@ -0,0 +1,370 @@
1
+ import { matchesKey, stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
2
+ const PERIODS = [
3
+ { key: '1', period: 'today', label: 'today' },
4
+ { key: '2', period: '7d', label: '7d' },
5
+ { key: '3', period: '30d', label: '30d' },
6
+ { key: '0', period: 'all', label: 'all time' }
7
+ ];
8
+ const SCOPES = [
9
+ { key: 'c', scope: 'account', label: 'account' },
10
+ { key: 's', scope: 'session', label: 'session' },
11
+ { key: 'p', scope: 'project', label: 'project' },
12
+ { key: 'a', scope: 'all', label: 'all accounts' }
13
+ ];
14
+ const BARS = '▁▂▃▄▅▆▇█';
15
+ function clean(value) {
16
+ return stripTerminalSequences(value).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, ' ').replace(/\t/g, ' ');
17
+ }
18
+ function oneLine(value) {
19
+ return clean(value).replace(/\s+/g, ' ').trim();
20
+ }
21
+ function count(value) {
22
+ if (!Number.isFinite(value))
23
+ return '0';
24
+ if (value >= 1_000_000_000)
25
+ return `${(value / 1_000_000_000).toFixed(1)}B`;
26
+ if (value >= 1_000_000)
27
+ return `${(value / 1_000_000).toFixed(1)}M`;
28
+ if (value >= 1_000)
29
+ return `${(value / 1_000).toFixed(1)}K`;
30
+ return String(Math.round(value));
31
+ }
32
+ function money(value) {
33
+ if (value === null)
34
+ return '—';
35
+ return value >= 1 ? `$${value.toFixed(2)}` : `$${value.toFixed(3)}`;
36
+ }
37
+ function duration(milliseconds) {
38
+ if (milliseconds === null)
39
+ return '—';
40
+ if (milliseconds < 1_000)
41
+ return `${Math.round(milliseconds)}ms`;
42
+ return `${(milliseconds / 1_000).toFixed(1)}s`;
43
+ }
44
+ function until(iso, now = Date.now()) {
45
+ if (!iso)
46
+ return '';
47
+ const at = Date.parse(iso);
48
+ if (Number.isNaN(at))
49
+ return '';
50
+ const delta = at - now;
51
+ if (delta <= 0)
52
+ return 'reset due';
53
+ const minutes = Math.round(delta / 60_000);
54
+ if (minutes < 60)
55
+ return `resets in ${minutes}m`;
56
+ const hours = Math.floor(minutes / 60);
57
+ if (hours < 48)
58
+ return `resets in ${hours}h ${minutes % 60}m`;
59
+ return `resets in ${Math.floor(hours / 24)}d`;
60
+ }
61
+ function ago(iso) {
62
+ const at = Date.parse(iso);
63
+ if (Number.isNaN(at))
64
+ return 'unknown time';
65
+ const minutes = Math.max(0, Math.round((Date.now() - at) / 60_000));
66
+ if (minutes < 1)
67
+ return 'just now';
68
+ if (minutes < 60)
69
+ return `${minutes}m ago`;
70
+ const hours = Math.round(minutes / 60);
71
+ return hours < 48 ? `${hours}h ago` : `${Math.round(hours / 24)}d ago`;
72
+ }
73
+ function usageLine(totals) {
74
+ const parts = [
75
+ `${count(totals.inputTokens)} in`,
76
+ `${count(totals.cacheReadTokens)} cached`,
77
+ `${count(totals.outputTokens)} out`,
78
+ `${count(totals.requests)} requests`,
79
+ money(totals.costUsd)
80
+ ];
81
+ if (totals.unpricedRequests)
82
+ parts.push(`${totals.unpricedRequests} unpriced`);
83
+ return parts.join(' · ');
84
+ }
85
+ function share(value, total) {
86
+ if (!total)
87
+ return '0%';
88
+ return `${Math.round((value / total) * 100)}%`;
89
+ }
90
+ function tokensOf(row) {
91
+ return row.inputTokens + row.cacheReadTokens + row.cacheWriteTokens + row.outputTokens;
92
+ }
93
+ /** A quota-left bar: green with room, amber when it narrows, red when it runs out. */
94
+ function barTone(percent, rejected = false) {
95
+ if (rejected)
96
+ return 'error';
97
+ if (percent === null || percent === undefined)
98
+ return 'dim';
99
+ if (percent >= 50)
100
+ return 'success';
101
+ if (percent >= 20)
102
+ return 'warning';
103
+ return 'error';
104
+ }
105
+ function bar(percent, width, theme, rejected = false) {
106
+ const track = '░'.repeat(width);
107
+ if (percent === null || percent === undefined)
108
+ return `[${theme.fg('dim', track)}]`;
109
+ const filled = Math.max(0, Math.min(width, Math.round((Math.max(0, Math.min(100, percent)) / 100) * width)));
110
+ return `[${theme.fg(barTone(percent, rejected), '█'.repeat(filled))}${theme.fg('dim', '░'.repeat(Math.max(0, width - filled)))}]`;
111
+ }
112
+ /** A label on the left and a quiet value on the right of the same row. */
113
+ function spread(left, right, width) {
114
+ if (!right)
115
+ return left;
116
+ const rightText = truncateToWidth(right, Math.max(0, width - visibleWidth(left) - 2), '');
117
+ return left + ' '.repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(rightText))) + rightText;
118
+ }
119
+ export class UsagePage {
120
+ options;
121
+ presentation = 'centered';
122
+ focused = false;
123
+ tab;
124
+ period = '7d';
125
+ scope = 'account';
126
+ summary;
127
+ limitsSummary;
128
+ limits;
129
+ busy = false;
130
+ error;
131
+ refreshLimits;
132
+ offset = 0;
133
+ scrollRows = 1;
134
+ maxOffset = 0;
135
+ disposed = false;
136
+ constructor(options) {
137
+ this.options = options;
138
+ this.tab = options.initialTab ?? 'analysis';
139
+ this.period = options.initialPeriod ?? '7d';
140
+ this.refreshLimits = this.tab === 'limits';
141
+ }
142
+ tabs() {
143
+ return [{ id: 'analysis', title: 'Analysis' }, { id: 'limits', title: 'Limits' }];
144
+ }
145
+ activeTab() {
146
+ return this.tab;
147
+ }
148
+ selectTab(id) {
149
+ if ((id === 'analysis' || id === 'limits') && id !== this.tab) {
150
+ this.tab = id;
151
+ this.offset = 0;
152
+ this.options.changed();
153
+ if (id === 'limits' && !this.limits) {
154
+ this.refreshLimits = true;
155
+ void this.load();
156
+ }
157
+ else if (id === 'analysis' && !this.summary)
158
+ void this.load();
159
+ else if (id === 'limits' && this.refreshLimits)
160
+ void this.load();
161
+ }
162
+ }
163
+ hint() {
164
+ return this.tab === 'analysis'
165
+ ? '↑/↓ scroll · PgUp/PgDn page · 1 today 2 7d 3 30d 0 all · c account s session p project a all accounts · r refresh · Tab limits · Esc close'
166
+ : '↑/↓ scroll · PgUp/PgDn page · r read provider limits · Tab analysis · Esc close';
167
+ }
168
+ activate() {
169
+ if (!this.summary && !this.limits)
170
+ void this.load();
171
+ }
172
+ dispose() {
173
+ this.disposed = true;
174
+ }
175
+ async load() {
176
+ if (this.busy || this.disposed)
177
+ return;
178
+ this.busy = true;
179
+ this.error = undefined;
180
+ this.options.changed();
181
+ try {
182
+ if (this.tab === 'analysis') {
183
+ this.summary = await this.options.call('terminal.usage.summary', {
184
+ scope: this.scope,
185
+ period: this.period,
186
+ timezoneOffsetMinutes: -new Date().getTimezoneOffset()
187
+ });
188
+ }
189
+ else {
190
+ // The provider's windows and this app's own ledger answer different questions; show both.
191
+ const [limits, summary] = await Promise.all([
192
+ this.options.call('terminal.usage.limits', { refresh: this.refreshLimits }),
193
+ this.options.call('terminal.usage.summary', {
194
+ scope: 'account',
195
+ period: '7d',
196
+ timezoneOffsetMinutes: -new Date().getTimezoneOffset()
197
+ })
198
+ ]);
199
+ this.limits = limits;
200
+ this.limitsSummary = summary;
201
+ this.refreshLimits = false;
202
+ }
203
+ this.offset = 0;
204
+ }
205
+ catch (error) {
206
+ this.error = error instanceof Error ? error.message : 'Usage could not be read.';
207
+ }
208
+ finally {
209
+ this.busy = false;
210
+ if (!this.disposed)
211
+ this.options.changed();
212
+ }
213
+ }
214
+ handleInput(data) {
215
+ const page = Math.max(1, this.scrollRows - 1);
216
+ if (matchesKey(data, 'up'))
217
+ this.offset = Math.max(0, this.offset - 1);
218
+ else if (matchesKey(data, 'down'))
219
+ this.offset = Math.min(this.maxOffset, this.offset + 1);
220
+ else if (matchesKey(data, 'pageUp'))
221
+ this.offset = Math.max(0, this.offset - page);
222
+ else if (matchesKey(data, 'pageDown'))
223
+ this.offset = Math.min(this.maxOffset, this.offset + page);
224
+ else if (matchesKey(data, 'home'))
225
+ this.offset = 0;
226
+ else if (matchesKey(data, 'end'))
227
+ this.offset = this.maxOffset;
228
+ else if (data === 'r') {
229
+ if (this.tab === 'limits')
230
+ this.refreshLimits = true;
231
+ void this.load();
232
+ }
233
+ else if (this.tab === 'analysis' && PERIODS.some((entry) => entry.key === data)) {
234
+ this.period = PERIODS.find((entry) => entry.key === data).period;
235
+ void this.load();
236
+ }
237
+ else if (this.tab === 'analysis' && SCOPES.some((entry) => entry.key === data)) {
238
+ this.scope = SCOPES.find((entry) => entry.key === data).scope;
239
+ void this.load();
240
+ }
241
+ else
242
+ return false;
243
+ this.options.changed();
244
+ return true;
245
+ }
246
+ analysisContent(width, theme) {
247
+ const summary = this.summary;
248
+ const header = [];
249
+ if (!summary) {
250
+ header.push(theme.fg(this.error ? 'error' : 'dim', this.error ?? (this.busy ? 'Reading usage…' : 'No usage to show.')));
251
+ return { header, body: [] };
252
+ }
253
+ const scope = SCOPES.find((entry) => entry.scope === summary.scope)?.label ?? summary.scope;
254
+ const period = PERIODS.find((entry) => entry.period === summary.period)?.label ?? summary.period;
255
+ header.push(theme.bold(theme.fg('accent', `${scope} · ${period}`)));
256
+ header.push(...wrapTextWithAnsi(theme.fg('muted', usageLine(summary.totals)), width));
257
+ const details = [
258
+ summary.totals.cacheHitPercent === null ? null : `${summary.totals.cacheHitPercent}% cache hits`,
259
+ `TTFT ${duration(summary.totals.avgTtftMs)}`,
260
+ `request ${duration(summary.totals.avgDurationMs)}`,
261
+ summary.totals.estimatedCostUsd ? `${money(summary.totals.estimatedCostUsd)} estimated` : null
262
+ ].filter((entry) => Boolean(entry));
263
+ if (details.length)
264
+ header.push(...wrapTextWithAnsi(theme.fg('dim', details.join(' · ')), width));
265
+ if (this.error)
266
+ header.push(...wrapTextWithAnsi(theme.fg('error', this.error), width));
267
+ const body = [''];
268
+ const total = tokensOf(summary.totals);
269
+ const gap = () => {
270
+ if (body.length && body[body.length - 1] !== '')
271
+ body.push('');
272
+ };
273
+ if (summary.byDay.length) {
274
+ const max = Math.max(...summary.byDay.map((day) => day.inputTokens + day.cacheReadTokens + day.outputTokens), 1);
275
+ const spark = summary.byDay.slice(-14).map((day) => BARS[Math.min(BARS.length - 1, Math.round(((day.inputTokens + day.cacheReadTokens + day.outputTokens) / max) * (BARS.length - 1)))]).join('');
276
+ body.push(theme.fg('accent', spark));
277
+ for (const day of summary.byDay.slice(-3).reverse()) {
278
+ body.push(...wrapTextWithAnsi(theme.fg('muted', ` ${day.day} ${count(day.inputTokens + day.cacheReadTokens + day.outputTokens)} tokens · ${count(day.requests)} requests · ${money(day.costUsd)}`), width));
279
+ }
280
+ }
281
+ const section = (rows) => {
282
+ if (!rows.length)
283
+ return;
284
+ gap();
285
+ for (const row of rows) {
286
+ const line = ` ${oneLine(row.label)} ${share(tokensOf(row), total)} ${count(tokensOf(row))} tokens · ${count(row.requests)} requests · ${money(row.costUsd)}`;
287
+ body.push(...wrapTextWithAnsi(theme.fg('muted', line), width));
288
+ }
289
+ };
290
+ section(summary.byModel);
291
+ section(summary.byProvider);
292
+ section(summary.byAccount);
293
+ section(summary.bySession);
294
+ if (body.length === 1)
295
+ body.push(theme.fg('dim', 'No requests were recorded in this period.'));
296
+ while (body.length > 1 && body[body.length - 1] === '')
297
+ body.pop();
298
+ return { header, body };
299
+ }
300
+ limitsContent(width, theme) {
301
+ const report = this.limits;
302
+ const header = [];
303
+ if (!report) {
304
+ header.push(theme.fg(this.error ? 'error' : 'dim', this.error ?? (this.busy ? 'Reading provider limits…' : 'No limit readings yet.')));
305
+ return { header, body: [] };
306
+ }
307
+ if (this.error)
308
+ header.push(...wrapTextWithAnsi(theme.fg('error', this.error), width));
309
+ const body = [];
310
+ const barWidth = Math.max(8, Math.min(16, Math.floor(width / 6)));
311
+ for (const account of report.accounts) {
312
+ if (body.length)
313
+ body.push('');
314
+ body.push(theme.bold(oneLine(account.label)) + theme.fg('dim', `${account.plan ? ` · ${oneLine(account.plan)}` : ''} · ${account.health}`));
315
+ if (!account.providers.length) {
316
+ body.push(theme.fg('dim', ' No limit readings.'));
317
+ continue;
318
+ }
319
+ for (const provider of account.providers) {
320
+ body.push('');
321
+ const source = provider.source === 'provider' ? '' : provider.source;
322
+ const meta = [source, ago(provider.observedAt)].filter(Boolean).join(' · ');
323
+ const tone = provider.source === 'unavailable' ? 'warning' : 'muted';
324
+ body.push(...wrapTextWithAnsi(theme.fg(tone, ` ${provider.provider}${meta ? ` · ${meta}` : ''}${provider.reason ? ` · ${oneLine(provider.reason)}` : ''}`), width));
325
+ for (const limit of provider.limits) {
326
+ body.push('');
327
+ const reset = until(limit.resetsAt);
328
+ body.push(...wrapTextWithAnsi(spread(theme.bold(` ${oneLine(limit.label)}`), theme.fg('dim', reset), width), width));
329
+ const used = limit.usedPercent ?? null;
330
+ const remainingPercent = limit.remainingPercent
331
+ ?? (used === null
332
+ ? limit.remaining !== null && limit.remaining !== undefined && limit.limit !== null && limit.limit !== undefined && limit.limit > 0
333
+ ? Math.max(0, Math.min(100, (limit.remaining / limit.limit) * 100))
334
+ : null
335
+ : Math.max(0, Math.min(100, 100 - used)));
336
+ const rejected = limit.status === 'rejected';
337
+ const tone = barTone(remainingPercent, rejected);
338
+ const absolute = limit.remaining !== null && limit.remaining !== undefined
339
+ ? `${limit.unit === 'usd' ? money(limit.remaining) : limit.unit === 'currency' ? `${limit.remaining.toFixed(2)} ${limit.detail ?? ''}`.trim() : count(limit.remaining)} left`
340
+ : limit.limit !== null && limit.limit !== undefined
341
+ ? `limit ${count(limit.limit)}${limit.unit && limit.unit !== 'percent' ? ` ${limit.unit}` : ''}`
342
+ : '';
343
+ const value = remainingPercent === null ? absolute || '—' : `${Math.round(remainingPercent)}% left`;
344
+ const suffix = remainingPercent !== null && absolute ? ` ${theme.fg('dim', absolute)}` : '';
345
+ body.push(...wrapTextWithAnsi(` ${bar(remainingPercent, barWidth, theme, rejected)} ${theme.fg(tone, value)}${suffix}`, width));
346
+ }
347
+ }
348
+ }
349
+ if (!report.accounts.length)
350
+ body.push(theme.fg('dim', 'No accounts.'));
351
+ const local = this.limitsSummary?.totals;
352
+ if (local) {
353
+ if (body.length)
354
+ body.push('');
355
+ body.push(...wrapTextWithAnsi(theme.fg('dim', `7d · ${count(local.inputTokens + local.cacheReadTokens + local.outputTokens)} tokens · ${count(local.requests)} requests · ${money(local.costUsd)}`), width));
356
+ }
357
+ return { header, body };
358
+ }
359
+ render(width, rows) {
360
+ if (width < 1 || rows < 1)
361
+ return [];
362
+ const theme = this.options.theme();
363
+ const { header, body } = this.tab === 'analysis' ? this.analysisContent(width, theme) : this.limitsContent(width, theme);
364
+ const bodyRows = Math.max(0, rows - header.length);
365
+ this.scrollRows = Math.max(1, bodyRows);
366
+ this.maxOffset = Math.max(0, body.length - bodyRows);
367
+ this.offset = Math.max(0, Math.min(this.offset, this.maxOffset));
368
+ return [...header, ...body.slice(this.offset, this.offset + bodyRows)].slice(0, rows).map((line) => truncateToWidth(line, width, ''));
369
+ }
370
+ }
@@ -1,6 +1,5 @@
1
- import type { InlineExtension, Theme, ToolRenderers } from '@earendil-works/pi-coding-agent';
1
+ import type { Theme, ToolRenderers } from '@earendil-works/pi-coding-agent';
2
2
  export interface PiAdapter {
3
- builtins: InlineExtension[];
4
3
  setTheme(theme: Theme): void;
5
4
  withBuiltInRenderers(toolName: string, definition: ToolRenderers | undefined): ToolRenderers | undefined;
6
5
  readClipboardText(): Promise<string | null>;
@@ -12,18 +12,16 @@ export function loadPiAdapter() {
12
12
  const pi = await loadPi();
13
13
  assertPiAdapterVersion(pi.VERSION);
14
14
  const root = pi.getPackageDir();
15
- const [extensions, theme, renderers, clipboard, images] = await Promise.all([
16
- import(pathToFileURL(join(root, 'dist/extensions/index.js')).href),
15
+ const [theme, renderers, clipboard, images] = await Promise.all([
17
16
  import(pathToFileURL(join(root, 'dist/modes/interactive/theme/theme.js')).href),
18
17
  import(pathToFileURL(join(root, 'dist/core/tools/renderers/index.js')).href),
19
18
  import(pathToFileURL(join(root, 'dist/utils/clipboard.js')).href),
20
19
  import(pathToFileURL(join(root, 'dist/utils/clipboard-image.js')).href)
21
20
  ]);
22
- if (!Array.isArray(extensions.builtInExtensions) || typeof theme.setThemeInstance !== 'function' || typeof renderers.withBuiltInRenderers !== 'function' || typeof clipboard.readClipboardText !== 'function' || typeof clipboard.readClipboardFilePaths !== 'function' || typeof images.readClipboardImage !== 'function' || typeof images.extensionForImageMimeType !== 'function') {
23
- throw new Error('The pinned Pi terminal adapter no longer exposes its required builtin or theme hooks.');
21
+ if (typeof theme.setThemeInstance !== 'function' || typeof renderers.withBuiltInRenderers !== 'function' || typeof clipboard.readClipboardText !== 'function' || typeof clipboard.readClipboardFilePaths !== 'function' || typeof images.readClipboardImage !== 'function' || typeof images.extensionForImageMimeType !== 'function') {
22
+ throw new Error('The pinned Pi terminal adapter no longer exposes its required presentation hooks.');
24
23
  }
25
24
  adapter = {
26
- builtins: extensions.builtInExtensions,
27
25
  setTheme: theme.setThemeInstance,
28
26
  withBuiltInRenderers: renderers.withBuiltInRenderers,
29
27
  readClipboardText: clipboard.readClipboardText,
@@ -6,6 +6,8 @@ type PageOptions = {
6
6
  runId?: string;
7
7
  url?: string;
8
8
  annotate?: AnnotateStart;
9
+ usageTab?: 'analysis' | 'limit';
10
+ usagePeriod?: 'today' | '7d' | '30d' | 'all';
9
11
  };
10
12
  type OpenPage = (id: CommandPageId, options?: PageOptions) => Promise<void>;
11
13
  type Handler = (argument: string) => Promise<void>;
@@ -3,23 +3,27 @@ import { parseAnnotate } from "./annotateCommand.js";
3
3
  /** Terminal adapters for commands declared by Harness-owned feature plugins. */
4
4
  export function createBuiltinPluginCommandHandlers(host, openPage) {
5
5
  const handlers = new Map([
6
- ['doctor', async () => openPage('doctor')],
7
- ['worktree', async () => openPage('worktree')],
8
- ['trajectory', async (argument) => openPage('trajectory', { runId: argument })],
9
- ['subagents', async () => openPage('subagents')],
10
- ['agents', async () => openPage('agents')],
11
- ['notes', async () => openPage('notes')],
12
- ['annotate', async (argument) => {
6
+ ['annotations:notes', async () => openPage('notes')],
7
+ ['annotations:annotate', async (argument) => {
13
8
  const start = parseAnnotate(argument);
14
9
  if (typeof start === 'string')
15
10
  host.ui.notify(start, 'error');
16
11
  else
17
12
  await openPage('annotate', { annotate: start });
18
- }]
13
+ }],
14
+ ['usage:analysis', async (argument) => {
15
+ const period = argument.trim().toLowerCase();
16
+ if (period && !['today', '7d', '30d', 'all'].includes(period)) {
17
+ host.ui.notify('Use /usage:analysis today, 7d, 30d or all.', 'error');
18
+ return;
19
+ }
20
+ await openPage('usage', { usageTab: 'analysis', ...(period ? { usagePeriod: period } : {}) });
21
+ }],
22
+ ['usage:limit', async () => openPage('usage', { usageTab: 'limit' })]
19
23
  ]);
20
24
  const commands = BUILTIN_PLUGIN_REGISTRY.commands();
21
25
  for (const command of commands) {
22
- if (!handlers.has(command.name))
26
+ if (!handlers.has(`${command.pluginId}:${command.name}`))
23
27
  throw new Error(`No terminal handler for built-in plugin command ${command.pluginId}:${command.name}.`);
24
28
  }
25
29
  return handlers;