@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
@@ -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;
@@ -4,6 +4,8 @@ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import terminalExtension from "../piExtension.js";
5
5
  import { normalModeTools, planModeTools, QUESTION_TOOL_NAMES } from "./planMode.js";
6
6
  import { claimTerminalControl, controlCall } from "../control.js";
7
+ import { observeProviderResponses } from "../../usage/limits/capture.js";
8
+ import { limitHeaders } from "../../usage/limits/normalize.js";
7
9
  import { loadPiAdapter } from "./piAdapter.js";
8
10
  import { loadPi } from "../../pi/runtime.js";
9
11
  import { sessionUiContext } from "./sessionUi.js";
@@ -101,8 +103,16 @@ export async function createTerminalRuntime(args, host) {
101
103
  const identity = await controlCall('accounts.session').catch(() => null);
102
104
  const account = identity && typeof identity.accountId === 'string' && typeof identity.label === 'string'
103
105
  ? Object.freeze({ accountId: identity.accountId, label: identity.label }) : null;
104
- const adapter = await loadPiAdapter();
106
+ await loadPiAdapter();
105
107
  const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, 'auth.json'), modelsPath: join(agentDir, 'models.json') });
108
+ // This process makes the model requests, so it is where response headers are seen. Only the
109
+ // limit-bearing headers cross the socket, and the parent throttles what it stores.
110
+ observeProviderResponses(modelRuntime, (provider, response) => {
111
+ const headers = limitHeaders(response.headers);
112
+ if (!Object.keys(headers).length)
113
+ return;
114
+ void controlCall('terminal.usage.headers', { provider, status: response.status, headers }).catch(() => undefined);
115
+ });
106
116
  const listeners = new Set();
107
117
  const changes = new Set();
108
118
  const beforeChanges = new Set();
@@ -157,11 +167,21 @@ export async function createTerminalRuntime(args, host) {
157
167
  additionalSkillPaths: prepared.skills, additionalPromptTemplatePaths: prepared.prompts,
158
168
  additionalThemePaths: prepared.themes,
159
169
  extensionFactories: [
160
- ...adapter.builtins,
161
170
  { name: 'harness-terminal-bridge', factory: pi => terminalExtension(pi, planRun, () => native.session), hidden: true },
162
171
  // A model picked in the terminal reaches Pi as this event. A restore is not a pick.
163
172
  { name: 'harness-mode-choices', factory: pi => { pi.on('model_select', event => { if (event.source !== 'restore')
164
- void modeChoices.record(recordingMode(), { model: `${event.model.provider}/${event.model.id}` }); }); }, hidden: true }
173
+ void modeChoices.record(recordingMode(), { model: `${event.model.provider}/${event.model.id}` }); }); }, hidden: true },
174
+ // The bridge registers parent capabilities when delegation is available. Runs without
175
+ // delegation still need the same frozen snapshot, after the bridge has begun the run.
176
+ { name: 'harness-run-capabilities', factory: pi => {
177
+ pi.on('before_agent_start', async () => {
178
+ if (!pi.getAllTools().some(tool => tool.name === 'spawn_subagent')) {
179
+ const snapshot = await controlCall('terminal.subagents.register', { tools: pi.getActiveTools(), plan: planRun.active });
180
+ if (Array.isArray(snapshot.connectors))
181
+ planRun.connectors = snapshot.connectors;
182
+ }
183
+ });
184
+ }, hidden: true }
165
185
  ],
166
186
  appendSystemPromptOverride: (base) => [...base, ...prepared.appendSystemPrompt]
167
187
  });
@@ -0,0 +1,40 @@
1
+ import type { SessionInfo, Theme } from '@earendil-works/pi-coding-agent';
2
+ import { type TuiMouseEventResult } from '@earendil-works/pi-tui';
3
+ import type { TerminalPage } from "./page.js";
4
+ import type { TerminalMouseInput } from "./mouse.js";
5
+ export declare function formatSessionRelativeTime(value: Date, now?: number): string;
6
+ export declare function formatSessionSize(bytes: number | undefined): string;
7
+ export interface ResumeSessionsPageOptions {
8
+ theme: () => Theme;
9
+ cwd(): string;
10
+ branch(): string | null;
11
+ listSessions(signal: AbortSignal): Promise<SessionInfo[]>;
12
+ select(path: string): void;
13
+ cancel(): void;
14
+ changed(): void;
15
+ }
16
+ export declare class ResumeSessionsPage implements TerminalPage {
17
+ private readonly options;
18
+ readonly presentation: "fullscreen";
19
+ private readonly list;
20
+ private loading;
21
+ private loaded;
22
+ private disposed;
23
+ private error;
24
+ private abort?;
25
+ constructor(options: ResumeSessionsPageOptions);
26
+ get focused(): boolean;
27
+ set focused(value: boolean);
28
+ tabs(): {
29
+ id: string;
30
+ title: string;
31
+ }[];
32
+ activeTab(): string;
33
+ hint(): string;
34
+ activate(): void;
35
+ dispose(): void;
36
+ private load;
37
+ handleInput(data: string): boolean;
38
+ handleMouse(event: TerminalMouseInput): TuiMouseEventResult | undefined;
39
+ render(width: number, rows: number): string[];
40
+ }