@xingular/harness 1.0.0 → 1.1.1

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 (257) hide show
  1. package/CHANGELOG.md +33 -5
  2. package/README.md +70 -19
  3. package/dist/cli.js +10321 -5785
  4. package/dist/sdk/agents/builtin.d.ts +2 -0
  5. package/dist/sdk/agents/builtin.js +34 -0
  6. package/dist/sdk/agents/narrow.d.ts +13 -0
  7. package/dist/sdk/agents/narrow.js +29 -0
  8. package/dist/sdk/agents/profiles.d.ts +80 -0
  9. package/dist/sdk/agents/profiles.js +221 -0
  10. package/dist/sdk/annotations/bridge.d.ts +42 -0
  11. package/dist/sdk/annotations/bridge.js +264 -0
  12. package/dist/sdk/annotations/capture.d.ts +116 -0
  13. package/dist/sdk/annotations/capture.js +308 -0
  14. package/dist/sdk/annotations/delivery.d.ts +72 -0
  15. package/dist/sdk/annotations/delivery.js +130 -0
  16. package/dist/sdk/annotations/inspect.d.ts +10 -0
  17. package/dist/sdk/annotations/inspect.js +33 -0
  18. package/dist/sdk/annotations/isolated.d.ts +6 -0
  19. package/dist/sdk/annotations/isolated.js +51 -0
  20. package/dist/sdk/annotations/pageScript.d.ts +28 -0
  21. package/dist/sdk/annotations/pageScript.js +570 -0
  22. package/dist/sdk/annotations/part.d.ts +10 -0
  23. package/dist/sdk/annotations/part.js +144 -0
  24. package/dist/sdk/annotations/queue.d.ts +4 -0
  25. package/dist/sdk/annotations/queue.js +40 -0
  26. package/dist/sdk/annotations/serverMethods.d.ts +3 -0
  27. package/dist/sdk/annotations/serverMethods.js +62 -0
  28. package/dist/sdk/annotations/store.d.ts +160 -0
  29. package/dist/sdk/annotations/store.js +386 -0
  30. package/dist/sdk/browser/index.d.ts +14 -3
  31. package/dist/sdk/browser/index.js +11 -4
  32. package/dist/sdk/browser/serverMethods.d.ts +3 -0
  33. package/dist/sdk/browser/serverMethods.js +7 -0
  34. package/dist/sdk/browser/session.d.ts +1 -0
  35. package/dist/sdk/browser/session.js +45 -9
  36. package/dist/sdk/cli.js +19 -11
  37. package/dist/sdk/core/bus.js +1 -1
  38. package/dist/sdk/core/env.d.ts +1 -1
  39. package/dist/sdk/core/env.js +1 -1
  40. package/dist/sdk/doctor/serverMethods.d.ts +3 -0
  41. package/dist/sdk/doctor/serverMethods.js +8 -0
  42. package/dist/sdk/harness.d.ts +15 -0
  43. package/dist/sdk/harness.js +44 -5
  44. package/dist/sdk/history/recall.js +8 -8
  45. package/dist/sdk/history/serverMethods.d.ts +3 -0
  46. package/dist/sdk/history/serverMethods.js +8 -0
  47. package/dist/sdk/hub/cli.d.ts +1 -1
  48. package/dist/sdk/hub/cli.js +1 -1
  49. package/dist/sdk/hub/schema.d.ts +1 -1
  50. package/dist/sdk/hub/serverMethods.d.ts +3 -0
  51. package/dist/sdk/hub/serverMethods.js +11 -0
  52. package/dist/sdk/index.d.ts +10 -0
  53. package/dist/sdk/index.js +7 -0
  54. package/dist/sdk/mcp/connections.js +14 -7
  55. package/dist/sdk/mcp/serverMethods.d.ts +3 -0
  56. package/dist/sdk/mcp/serverMethods.js +22 -0
  57. package/dist/sdk/plugins/builtin.d.ts +1 -0
  58. package/dist/sdk/plugins/builtin.js +128 -33
  59. package/dist/sdk/plugins/builtinRegistry.d.ts +53 -0
  60. package/dist/sdk/plugins/builtinRegistry.js +49 -0
  61. package/dist/sdk/plugins/cli.d.ts +1 -1
  62. package/dist/sdk/plugins/cli.js +4 -3
  63. package/dist/sdk/prompt/compile.d.ts +5 -2
  64. package/dist/sdk/prompt/compile.js +25 -8
  65. package/dist/sdk/refine/cli.d.ts +1 -1
  66. package/dist/sdk/refine/cli.js +2 -2
  67. package/dist/sdk/refine/serverMethods.d.ts +3 -0
  68. package/dist/sdk/refine/serverMethods.js +20 -0
  69. package/dist/sdk/server/methods.js +9 -86
  70. package/dist/sdk/server/params.d.ts +4 -0
  71. package/dist/sdk/server/params.js +11 -0
  72. package/dist/sdk/sessions/contracts.d.ts +17 -0
  73. package/dist/sdk/sessions/contracts.js +42 -3
  74. package/dist/sdk/sessions/planMode.d.ts +5 -2
  75. package/dist/sdk/sessions/planMode.js +12 -3
  76. package/dist/sdk/sessions/reasoningSummaries.d.ts +2 -2
  77. package/dist/sdk/sessions/reasoningSummaries.js +3 -3
  78. package/dist/sdk/sessions/registry.d.ts +1 -0
  79. package/dist/sdk/sessions/registry.js +4 -1
  80. package/dist/sdk/sessions/runIndex.js +7 -7
  81. package/dist/sdk/sessions/runStore.js +3 -1
  82. package/dist/sdk/sessions/runner.js +2 -2
  83. package/dist/sdk/sessions/sessions.d.ts +17 -5
  84. package/dist/sdk/sessions/sessions.js +79 -12
  85. package/dist/sdk/sessions/sideQuestionServerMethods.d.ts +3 -0
  86. package/dist/sdk/sessions/sideQuestionServerMethods.js +19 -0
  87. package/dist/sdk/sessions/sideQuestions.d.ts +66 -0
  88. package/dist/sdk/sessions/sideQuestions.js +290 -0
  89. package/dist/sdk/sessions/subagentServerMethods.d.ts +3 -0
  90. package/dist/sdk/sessions/subagentServerMethods.js +41 -0
  91. package/dist/sdk/sessions/subagentService.d.ts +13 -2
  92. package/dist/sdk/sessions/subagentService.js +34 -16
  93. package/dist/sdk/sessions/subagents.d.ts +9 -1
  94. package/dist/sdk/sessions/subagents.js +19 -0
  95. package/dist/sdk/terminal/browser.d.ts +62 -0
  96. package/dist/sdk/terminal/browser.js +41 -0
  97. package/dist/sdk/terminal/browserControl.d.ts +29 -0
  98. package/dist/sdk/terminal/browserControl.js +186 -0
  99. package/dist/sdk/terminal/commandHelp.d.ts +17 -0
  100. package/dist/sdk/terminal/commandHelp.js +20 -0
  101. package/dist/sdk/terminal/controller.d.ts +7 -1
  102. package/dist/sdk/terminal/controller.js +34 -2
  103. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +28 -0
  104. package/dist/sdk/terminal/opentui/subagentsWidget.js +52 -0
  105. package/dist/sdk/terminal/piExtension.js +33 -48
  106. package/dist/sdk/terminal/socket.d.ts +6 -3
  107. package/dist/sdk/terminal/socket.js +40 -9
  108. package/dist/sdk/terminal/standalone.d.ts +2 -0
  109. package/dist/sdk/terminal/standalone.js +50 -10
  110. package/dist/sdk/terminal/subagentMonitor.d.ts +38 -0
  111. package/dist/sdk/terminal/subagentMonitor.js +121 -0
  112. package/dist/sdk/terminal/terminals.d.ts +42 -6
  113. package/dist/sdk/terminal/terminals.js +359 -18
  114. package/dist/sdk/terminal/trajectory.js +2 -2
  115. package/dist/sdk/terminal/tui/annotateCommand.d.ts +3 -0
  116. package/dist/sdk/terminal/tui/annotateCommand.js +22 -0
  117. package/dist/sdk/terminal/tui/commandPages.d.ts +5 -1
  118. package/dist/sdk/terminal/tui/commandPages.js +54 -6
  119. package/dist/sdk/terminal/tui/commands.d.ts +8 -4
  120. package/dist/sdk/terminal/tui/commands.js +181 -58
  121. package/dist/sdk/terminal/tui/dialogGeometry.d.ts +2 -2
  122. package/dist/sdk/terminal/tui/dialogGeometry.js +4 -2
  123. package/dist/sdk/terminal/tui/dialogs.d.ts +6 -1
  124. package/dist/sdk/terminal/tui/dialogs.js +9 -2
  125. package/dist/sdk/terminal/tui/modelPicker.js +1 -6
  126. package/dist/sdk/terminal/tui/page.d.ts +3 -0
  127. package/dist/sdk/terminal/tui/pageOverlay.js +17 -6
  128. package/dist/sdk/terminal/tui/pages/accountsPage.d.ts +26 -4
  129. package/dist/sdk/terminal/tui/pages/accountsPage.js +264 -36
  130. package/dist/sdk/terminal/tui/pages/agentsPage.d.ts +74 -0
  131. package/dist/sdk/terminal/tui/pages/agentsPage.js +190 -0
  132. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +94 -0
  133. package/dist/sdk/terminal/tui/pages/annotatePage.js +341 -0
  134. package/dist/sdk/terminal/tui/pages/browserPage.d.ts +87 -0
  135. package/dist/sdk/terminal/tui/pages/browserPage.js +738 -0
  136. package/dist/sdk/terminal/tui/pages/doctorPage.d.ts +2 -0
  137. package/dist/sdk/terminal/tui/pages/doctorPage.js +103 -15
  138. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +75 -0
  139. package/dist/sdk/terminal/tui/pages/notesPage.js +201 -0
  140. package/dist/sdk/terminal/tui/pages/settingsPage.d.ts +8 -0
  141. package/dist/sdk/terminal/tui/pages/settingsPage.js +16 -1
  142. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +23 -0
  143. package/dist/sdk/terminal/tui/pages/subagentsPage.js +70 -0
  144. package/dist/sdk/terminal/tui/pages/trajectoryPage.d.ts +12 -5
  145. package/dist/sdk/terminal/tui/pages/trajectoryPage.js +156 -83
  146. package/dist/sdk/terminal/tui/pages/worktreesPage.d.ts +72 -0
  147. package/dist/sdk/terminal/tui/pages/worktreesPage.js +203 -0
  148. package/dist/sdk/terminal/tui/planMode.d.ts +1 -1
  149. package/dist/sdk/terminal/tui/planMode.js +1 -1
  150. package/dist/sdk/terminal/tui/pluginCommands.d.ts +14 -0
  151. package/dist/sdk/terminal/tui/pluginCommands.js +27 -0
  152. package/dist/sdk/terminal/tui/questions.d.ts +2 -0
  153. package/dist/sdk/terminal/tui/runtime.js +4 -2
  154. package/dist/sdk/terminal/tui/settingsModel.d.ts +2 -0
  155. package/dist/sdk/terminal/tui/settingsModel.js +32 -2
  156. package/dist/sdk/terminal/tui/sideQuestion.d.ts +26 -26
  157. package/dist/sdk/terminal/tui/sideQuestion.js +131 -98
  158. package/dist/sdk/terminal/tui/ui.d.ts +2 -0
  159. package/dist/sdk/terminal/tui/ui.js +1 -0
  160. package/dist/sdk/tools/askUser.js +26 -8
  161. package/dist/sdk/tools/checklist.js +2 -2
  162. package/dist/sdk/tools/harnessTools.d.ts +2 -0
  163. package/dist/sdk/tools/harnessTools.js +1 -1
  164. package/dist/sdk/tools/spawnSubagent.js +13 -4
  165. package/dist/sdk/web/part.d.ts +10 -0
  166. package/dist/sdk/web/part.js +6 -0
  167. package/dist/sdk/web/serverMethods.d.ts +3 -0
  168. package/dist/sdk/web/serverMethods.js +7 -0
  169. package/dist/sdk/wiki/serverMethods.d.ts +3 -0
  170. package/dist/sdk/wiki/serverMethods.js +26 -0
  171. package/dist/sdk/worktrees/cli.d.ts +11 -0
  172. package/dist/sdk/worktrees/cli.js +146 -0
  173. package/dist/sdk/worktrees/copy.d.ts +9 -0
  174. package/dist/sdk/worktrees/copy.js +111 -0
  175. package/dist/sdk/worktrees/git.d.ts +16 -0
  176. package/dist/sdk/worktrees/git.js +81 -0
  177. package/dist/sdk/worktrees/serverMethods.d.ts +3 -0
  178. package/dist/sdk/worktrees/serverMethods.js +30 -0
  179. package/dist/sdk/worktrees/service.d.ts +73 -0
  180. package/dist/sdk/worktrees/service.js +483 -0
  181. package/dist/sdk/worktrees/setup.d.ts +23 -0
  182. package/dist/sdk/worktrees/setup.js +65 -0
  183. package/dist/sdk/worktrees/types.d.ts +91 -0
  184. package/dist/sdk/worktrees/types.js +1 -0
  185. package/dist/sdk/world/cli.d.ts +2 -2
  186. package/dist/sdk/world/cli.js +6 -6
  187. package/dist/sdk/world/world.d.ts +3 -1
  188. package/dist/sdk/world/world.js +8 -9
  189. package/dist/terminal-opentui.js +14540 -7374
  190. package/dist/terminal-pi.js +285 -666
  191. package/docs/README.md +22 -2
  192. package/docs/architecture/00-overview.md +28 -16
  193. package/docs/architecture/02-sessions-and-runs.md +72 -15
  194. package/docs/architecture/03-variants.md +10 -2
  195. package/docs/architecture/04-prompt.md +26 -6
  196. package/docs/architecture/06-plugins-and-capabilities.md +39 -3
  197. package/docs/architecture/07-tools-and-gate.md +21 -6
  198. package/docs/architecture/09-accounts-and-connectors.md +8 -0
  199. package/docs/architecture/10-server-protocol.md +111 -2
  200. package/docs/architecture/11-terminal.md +185 -5
  201. package/docs/architecture/12-storage.md +15 -1
  202. package/docs/architecture/13-world-tools.md +28 -6
  203. package/docs/architecture/14-surfaces-and-parity.md +89 -0
  204. package/docs/architecture/README.md +15 -4
  205. package/docs/architecture/proposals/PROP-0006-run-checklist.md +4 -2
  206. package/docs/architecture/proposals/PROP-0013-resource-browser.md +58 -0
  207. package/docs/architecture/proposals/PROP-0014-fullscreen-browser-tabs.md +58 -0
  208. package/docs/architecture/proposals/PROP-0015-one-capability-surface.md +88 -0
  209. package/docs/architecture/proposals/PROP-0016-managed-worktrees.md +156 -0
  210. package/docs/architecture/proposals/PROP-0017-page-annotations.md +127 -0
  211. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +179 -0
  212. package/docs/architecture/proposals/PROP-0019-contextual-side-questions.md +122 -0
  213. package/docs/architecture/proposals/PROP-0020-builtin-feature-plugins.md +105 -0
  214. package/docs/architecture/proposals/PROP-0021-command-namespaces-and-duplicates.md +99 -0
  215. package/docs/architecture/proposals/PROP-0022-plan-only-questions.md +102 -0
  216. package/docs/architecture/proposals/PROP-0023-xagent-cli-name.md +61 -0
  217. package/docs/architecture/proposals/PROP-0024-question-sketch-previews.md +130 -0
  218. package/docs/architecture/proposals/PROP-0025-contextual-question-wireframes.md +49 -0
  219. package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +94 -0
  220. package/docs/architecture/proposals/PROP-0027-xingular-agent-product-identity.md +60 -0
  221. package/docs/architecture/proposals/PROP-0028-normalize-question-wireframes.md +55 -0
  222. package/docs/architecture/proposals/PROP-0029-agent-profiles.md +124 -0
  223. package/docs/architecture/proposals/PROP-0030-subagent-title.md +86 -0
  224. package/docs/architecture/proposals/README.md +25 -0
  225. package/docs/decisions/ADR-0009-xharness-command.md +6 -5
  226. package/docs/decisions/ADR-0014-public-release-baseline.md +3 -2
  227. package/docs/decisions/ADR-0015-resource-browser.md +28 -0
  228. package/docs/decisions/ADR-0016-fullscreen-browser-tabs.md +31 -0
  229. package/docs/decisions/ADR-0017-one-capability-surface.md +34 -0
  230. package/docs/decisions/ADR-0018-builtin-feature-plugins.md +35 -0
  231. package/docs/decisions/ADR-0019-managed-git-worktrees.md +31 -0
  232. package/docs/decisions/ADR-0020-page-annotations.md +37 -0
  233. package/docs/decisions/ADR-0021-required-command-namespaces.md +40 -0
  234. package/docs/decisions/ADR-0022-plan-only-question-tools.md +34 -0
  235. package/docs/decisions/ADR-0023-contextual-side-question-runs.md +35 -0
  236. package/docs/decisions/ADR-0024-xagent-command.md +40 -0
  237. package/docs/decisions/ADR-0025-question-choice-sketches.md +49 -0
  238. package/docs/decisions/ADR-0026-contextual-question-wireframes.md +58 -0
  239. package/docs/decisions/ADR-0027-connected-sketch-display.md +60 -0
  240. package/docs/decisions/ADR-0028-xingular-agent-product-identity.md +56 -0
  241. package/docs/decisions/ADR-0029-normalized-question-wireframe-borders.md +33 -0
  242. package/docs/decisions/ADR-0030-agent-profiles.md +43 -0
  243. package/docs/doctor.md +10 -4
  244. package/docs/features/README.md +26 -0
  245. package/docs/features/interfaces/README.md +1 -1
  246. package/docs/future-work.md +48 -0
  247. package/docs/parity.md +78 -0
  248. package/docs/releases/1.0.0.md +2 -2
  249. package/docs/releases/4.0.0.md +46 -0
  250. package/docs/releases/README.md +20 -15
  251. package/docs/study/00-index.md +5 -0
  252. package/docs/study/autonomous-engineering-sources.md +277 -0
  253. package/docs/study/autonomy-sources.md +147 -0
  254. package/docs/study/proactive-autonomy.md +408 -0
  255. package/docs/study/tui-resource-browser.md +78 -0
  256. package/docs/terminal-tui.md +325 -93
  257. package/package.json +3 -3
@@ -1,7 +1,5 @@
1
- import { matchesKey, stripTerminalSequences, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
2
- import { selectedRow, selectionText } from "../chrome.js";
1
+ import { matchesKey, stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
3
2
  import { FilterList } from "../list.js";
4
- import { isPrimaryClick } from "../mouse.js";
5
3
  const ACTIVE = new Set(['queued', 'running', 'waiting_for_approval', 'waiting_for_input']);
6
4
  const payloadOf = (event) => event.payload && typeof event.payload === 'object' ? event.payload : {};
7
5
  const stringOf = (value) => typeof value === 'string' ? value : undefined;
@@ -68,17 +66,20 @@ const SECTIONS = [
68
66
  ];
69
67
  export class TrajectoryPage {
70
68
  options;
69
+ presentation = 'fullscreen';
71
70
  runs = [];
72
71
  section = 'runs';
73
72
  runList;
73
+ eventList;
74
74
  runIndex = 0;
75
75
  run;
76
76
  events = [];
77
- selected = 0;
78
77
  detailOffset = 0;
79
- eventRowsStart = -1;
80
- eventRowsFirst = 0;
81
- eventRowsCount = 0;
78
+ listStart = 0;
79
+ listEnd = 0;
80
+ listWidth = 0;
81
+ detailsStart = -1;
82
+ wideLayout = false;
82
83
  loading = false;
83
84
  loaded = false;
84
85
  disposed = false;
@@ -87,15 +88,19 @@ export class TrajectoryPage {
87
88
  constructor(options) {
88
89
  this.options = options;
89
90
  this.runList = new FilterList({
90
- theme: options.theme, search: 'never', noun: ['run', 'runs'], empty: 'No runs recorded for this native session yet',
91
+ theme: options.theme, search: 'always', noun: ['run', 'runs'], empty: 'No runs recorded for this native session yet',
91
92
  onActivate: row => { if (row.data)
92
93
  void this.selectRun(row.data); }
93
94
  });
95
+ this.eventList = new FilterList({
96
+ theme: options.theme, search: 'always', noun: ['event', 'events'], empty: 'No recorded events',
97
+ onActivate: () => { this.detailOffset = 0; this.options.changed(); }
98
+ });
94
99
  }
95
100
  get selectedRunId() { return this.run?.id; }
96
- get selectedEvent() { return this.events[this.selected]; }
97
- get focused() { return this.runList.focused; }
98
- set focused(value) { this.runList.focused = value; }
101
+ get selectedEvent() { return this.eventList.selected?.data; }
102
+ get focused() { return this.section === 'runs' ? this.runList.focused : this.eventList.focused; }
103
+ set focused(value) { this.runList.focused = value; this.eventList.focused = value; }
99
104
  tabs() { return SECTIONS; }
100
105
  activeTab() { return this.section; }
101
106
  activate() {
@@ -153,8 +158,11 @@ export class TrajectoryPage {
153
158
  return;
154
159
  this.run = snapshot.run;
155
160
  this.events = snapshot.events.slice().sort(bySequence);
161
+ this.eventList.setRows(this.eventRows());
162
+ const latest = this.events.at(-1);
163
+ if (latest)
164
+ this.eventList.select(String(latest.sequence));
156
165
  this.runList.select(run.id);
157
- this.selected = Math.max(0, this.events.length - 1);
158
166
  this.detailOffset = 0;
159
167
  }
160
168
  async refresh() {
@@ -162,7 +170,8 @@ export class TrajectoryPage {
162
170
  return;
163
171
  this.loading = true;
164
172
  const lastSequence = this.events.at(-1)?.sequence;
165
- const followedLatest = this.selected === this.events.length - 1;
173
+ const selectedBefore = this.selectedEvent;
174
+ const followedLatest = !selectedBefore || selectedBefore.sequence === lastSequence;
166
175
  try {
167
176
  const snapshot = await this.options.read(this.run.id, lastSequence);
168
177
  if (this.disposed)
@@ -176,8 +185,14 @@ export class TrajectoryPage {
176
185
  const existing = new Set(this.events.map(event => event.sequence));
177
186
  this.events = [...this.events, ...snapshot.events.filter(event => !existing.has(event.sequence))].sort(bySequence);
178
187
  }
179
- if (followedLatest && this.events.length)
180
- this.selected = this.events.length - 1;
188
+ this.eventList.setRows(this.eventRows());
189
+ if (followedLatest) {
190
+ const latest = this.events.at(-1);
191
+ if (latest)
192
+ this.eventList.select(String(latest.sequence));
193
+ }
194
+ else if (selectedBefore)
195
+ this.eventList.select(String(selectedBefore.sequence));
181
196
  this.error = undefined;
182
197
  }
183
198
  catch (error) {
@@ -201,6 +216,18 @@ export class TrajectoryPage {
201
216
  data: run
202
217
  }));
203
218
  }
219
+ eventRows() {
220
+ return this.events.map(event => ({
221
+ id: String(event.sequence),
222
+ label: `${timeOf(event.timestamp)} ${event.type}`,
223
+ detail: eventDescription(event),
224
+ searchText: `${event.timestamp} ${event.type} ${eventDescription(event)} ${safe(JSON.stringify(payloadOf(event)))}`,
225
+ data: event
226
+ }));
227
+ }
228
+ eventJson(event) {
229
+ return JSON.stringify({ payload: event.payload, type: event.type, sequence: event.sequence, timestamp: event.timestamp }, null, 2);
230
+ }
204
231
  async selectRun(run) {
205
232
  const at = this.runs.findIndex(candidate => candidate.id === run.id);
206
233
  if (at < 0 || this.loading)
@@ -209,7 +236,7 @@ export class TrajectoryPage {
209
236
  this.runList.select(run.id);
210
237
  this.run = run;
211
238
  this.events = [];
212
- this.selected = 0;
239
+ this.eventList.setRows([]);
213
240
  this.detailOffset = 0;
214
241
  this.error = undefined;
215
242
  this.loading = true;
@@ -236,7 +263,7 @@ export class TrajectoryPage {
236
263
  this.run = this.runs[next];
237
264
  this.runList.select(this.run.id);
238
265
  this.events = [];
239
- this.selected = 0;
266
+ this.eventList.setRows([]);
240
267
  this.detailOffset = 0;
241
268
  this.error = undefined;
242
269
  this.loading = true;
@@ -255,21 +282,38 @@ export class TrajectoryPage {
255
282
  }
256
283
  }
257
284
  hint() {
258
- return this.section === 'runs' ? '↑/↓ runs · Enter inspect' : '[ ] runs · ↑/↓ events · PgUp/PgDn details · c copy';
285
+ return this.section === 'runs'
286
+ ? '↑/↓ runs · type to search · Enter inspect'
287
+ : '[ ] runs · ↑/↓ events · type to search · PgUp/PgDn details · Shift+C copy';
259
288
  }
260
289
  handleMouse(event) {
290
+ if (event.type === 'wheel') {
291
+ const down = (event.wheelDelta ?? 1) >= 0;
292
+ if (this.section === 'events' && this.detailsStart >= 0 && (this.wideLayout ? event.x >= this.listWidth + 3 : event.y >= this.detailsStart)) {
293
+ this.detailOffset = Math.max(0, this.detailOffset + (down ? 4 : -4));
294
+ this.options.changed();
295
+ return { handled: true, render: true };
296
+ }
297
+ if (this.section === 'runs')
298
+ this.runList.handleInput(down ? '\x1b[B' : '\x1b[A');
299
+ else
300
+ this.eventList.handleInput(down ? '\x1b[B' : '\x1b[A');
301
+ this.detailOffset = 0;
302
+ this.options.changed();
303
+ return { handled: true, render: true };
304
+ }
261
305
  if (event.type === 'click' && event.button !== 'left')
262
306
  return undefined;
263
- if (this.section === 'runs')
264
- return this.runList.handleMouse({ ...event, y: event.y - 1 });
265
- const offset = event.y - this.eventRowsStart;
266
- if (this.eventRowsStart < 0 || offset < 0 || offset >= this.eventRowsCount)
307
+ if (event.type !== 'click' && event.type !== 'move')
267
308
  return undefined;
268
- const next = this.eventRowsFirst + offset;
269
- const changed = this.selected !== next;
270
- this.selected = next;
271
- this.detailOffset = 0;
272
- return { handled: true, render: changed || isPrimaryClick(event) };
309
+ if (this.section === 'runs') {
310
+ if (event.y < this.listStart || event.y >= this.listEnd)
311
+ return undefined;
312
+ return this.runList.handleMouse({ ...event, y: event.y - this.listStart });
313
+ }
314
+ if (event.y < this.listStart || event.y >= this.listEnd || (this.wideLayout && event.x >= this.listWidth + 3))
315
+ return undefined;
316
+ return this.eventList.handleMouse({ ...event, y: event.y - this.listStart });
273
317
  }
274
318
  handleInput(data) {
275
319
  if (matchesKey(data, 'tab') || matchesKey(data, 'shift+tab')) {
@@ -288,16 +332,6 @@ export class TrajectoryPage {
288
332
  void this.changeRun(-1);
289
333
  return true;
290
334
  }
291
- if (matchesKey(data, 'up')) {
292
- this.selected = Math.max(0, this.selected - 1);
293
- this.detailOffset = 0;
294
- return true;
295
- }
296
- if (matchesKey(data, 'down')) {
297
- this.selected = this.events.length ? Math.min(this.events.length - 1, this.selected + 1) : 0;
298
- this.detailOffset = 0;
299
- return true;
300
- }
301
335
  if (matchesKey(data, 'pageUp')) {
302
336
  this.detailOffset = Math.max(0, this.detailOffset - 8);
303
337
  return true;
@@ -306,71 +340,110 @@ export class TrajectoryPage {
306
340
  this.detailOffset += 8;
307
341
  return true;
308
342
  }
309
- if (data.toLowerCase() === 'c' && this.selectedEvent) {
310
- const text = JSON.stringify(this.selectedEvent, null, 2);
343
+ if (data === 'C' && this.selectedEvent) {
344
+ const text = this.eventJson(this.selectedEvent);
311
345
  void this.options.copy(text).then(() => this.options.notify('Copied selected trajectory event')).catch(error => this.options.notify(String(error), 'error'));
312
346
  return true;
313
347
  }
314
- return false;
348
+ const selected = this.selectedEvent?.sequence;
349
+ if (!this.eventList.handleInput(data))
350
+ return false;
351
+ if (this.selectedEvent?.sequence !== selected)
352
+ this.detailOffset = 0;
353
+ return true;
354
+ }
355
+ runSummary(width, rows) {
356
+ if (!this.run || rows < 1)
357
+ return [];
358
+ const runPosition = `${this.runIndex + 1}/${this.runs.length}`;
359
+ const modelEvent = [...this.events].reverse().find(event => event.type === 'usage.updated');
360
+ const usage = modelEvent ? payloadOf(modelEvent) : {};
361
+ const model = stringOf(usage.model) ?? 'model unavailable';
362
+ const theme = this.options.theme();
363
+ const lines = [theme.bold(theme.fg('accent', `Run ${runPosition} · ${this.run.id} · ${this.run.status}${this.loading ? ' · loading' : ''}`))];
364
+ if (rows >= 3)
365
+ lines.push(theme.fg('muted', `${model} · ${durationLine(this.run)} · ${this.run.mode}`));
366
+ if (rows >= 4)
367
+ lines.push(theme.fg('muted', usageLine(this.events)));
368
+ if (rows >= 6)
369
+ lines.push(theme.fg('dim', this.run.goal ? `Prompt: ${this.run.goal.replace(/\s+/g, ' ').slice(0, 140)}` : 'Prompt text unavailable'));
370
+ if (this.error)
371
+ lines.push(theme.fg('error', `Read error: ${this.error}`));
372
+ return lines.slice(0, Math.min(rows, 5)).map(line => truncateToWidth(line, width, ''));
373
+ }
374
+ detailLines(width, rows) {
375
+ if (rows < 1)
376
+ return [];
377
+ const event = this.selectedEvent;
378
+ const theme = this.options.theme();
379
+ const heading = theme.bold(theme.fg('accent', event ? `Event JSON · ${event.type} · #${event.sequence}` : 'Event JSON'));
380
+ const json = event
381
+ ? wrapTextWithAnsi(safe(this.eventJson(event)), Math.max(1, width))
382
+ : [this.events.length ? 'No event selected' : this.loading ? 'Loading recorded events…' : 'No recorded events'];
383
+ const visibleRows = Math.max(0, rows - 1);
384
+ this.detailOffset = Math.min(this.detailOffset, Math.max(0, json.length - visibleRows));
385
+ return [heading, ...json.slice(this.detailOffset, this.detailOffset + visibleRows)
386
+ .map(line => this.options.theme().fg('dim', line))].slice(0, rows).map(line => truncateToWidth(line, width, ''));
315
387
  }
316
388
  render(width, rows) {
317
389
  const theme = this.options.theme();
318
390
  if (width < 1 || rows < 1)
319
391
  return [];
320
392
  if (this.section === 'runs') {
321
- this.eventRowsStart = -1;
322
- this.eventRowsCount = 0;
393
+ this.wideLayout = false;
394
+ this.detailsStart = -1;
323
395
  if (this.error && !this.runs.length)
324
396
  return [theme.fg('error', 'Read error: ' + this.error)];
325
- const lines = [theme.fg('dim', !this.loaded ? 'Loading recorded runs…' : this.runs.length + ' recorded runs')];
326
- lines.push(...this.runList.render(width, Math.max(0, rows - lines.length)));
397
+ const lines = [theme.fg('dim', !this.loaded ? 'Loading recorded runs…' : `${this.runs.length} recorded runs`)];
398
+ this.listStart = lines.length;
399
+ const list = this.runList.render(width, Math.max(0, rows - lines.length));
400
+ this.listEnd = this.listStart + list.length;
401
+ lines.push(...list);
327
402
  return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
328
403
  }
329
404
  if (!this.run) {
330
- this.eventRowsStart = -1;
331
- this.eventRowsCount = 0;
405
+ this.listStart = this.listEnd = 0;
406
+ this.detailsStart = -1;
332
407
  if (this.error)
333
408
  return [theme.fg('error', 'Read error: ' + this.error)];
334
409
  return [theme.fg('dim', this.loading || !this.loaded ? 'Loading recorded runs…' : 'No run selected. Choose a run in the Runs tab.')];
335
410
  }
336
- const runPosition = (this.runIndex + 1) + '/' + this.runs.length;
337
- const modelEvent = [...this.events].reverse().find(event => event.type === 'usage.updated');
338
- const usage = modelEvent ? payloadOf(modelEvent) : {};
339
- const model = stringOf(usage.model) ?? 'model unavailable';
340
- const lines = [theme.bold(theme.fg('accent', 'Run ' + runPosition + ' · ' + this.run.id + ' · ' + this.run.status + (this.loading ? ' · loading' : '')))];
341
- if (rows >= 9) {
342
- lines.push(theme.fg('muted', model + ' · ' + durationLine(this.run) + ' · ' + this.run.mode));
343
- lines.push(theme.fg('muted', usageLine(this.events)));
344
- lines.push(this.run.goal ? theme.fg('dim', 'Prompt: ' + this.run.goal.replace(/\s+/g, ' ').slice(0, 140)) : theme.fg('dim', 'Prompt text unavailable'));
345
- }
346
- else if (rows >= 6) {
347
- lines.push(theme.fg('muted', model + ' · ' + durationLine(this.run)));
348
- }
349
- lines.push(theme.bold('Events'));
350
- if (this.error)
351
- lines.push(theme.fg('error', 'Read error: ' + this.error));
352
- if (!this.events.length)
353
- lines.push(theme.fg('dim', this.loading ? 'Loading recorded events…' : 'No recorded events'));
354
- const available = Math.max(0, rows - lines.length - 1);
355
- const ledgerBudget = this.events.length ? Math.min(this.events.length, Math.max(1, Math.floor(available * 0.4))) : 0;
356
- const detailBudget = Math.max(0, available - ledgerBudget);
357
- const firstEvent = Math.max(0, Math.min(this.events.length - ledgerBudget, this.selected - Math.floor(ledgerBudget / 2)));
358
- this.eventRowsStart = lines.length;
359
- this.eventRowsFirst = firstEvent;
360
- this.eventRowsCount = ledgerBudget;
361
- for (let offset = 0; offset < ledgerBudget; offset++) {
362
- const index = firstEvent + offset;
363
- const event = this.events[index];
364
- const prefix = index === this.selected ? '❯' : ' ';
365
- const label = truncateToWidth(prefix + ' ' + timeOf(event.timestamp) + ' ' + event.type + ' ' + eventDescription(event), width, '');
366
- lines.push(index === this.selected ? selectedRow(selectionText(theme, label), width, theme) : theme.fg('muted', label));
411
+ const lines = this.runSummary(width, rows);
412
+ const bodyRows = Math.max(0, rows - lines.length);
413
+ const wide = width >= 72;
414
+ this.wideLayout = wide;
415
+ if (wide) {
416
+ const separatorWidth = 3;
417
+ const leftWidth = Math.max(24, Math.floor((width - separatorWidth) * 0.42));
418
+ const rightWidth = Math.max(1, width - leftWidth - separatorWidth);
419
+ const paneStart = lines.length;
420
+ this.listWidth = leftWidth;
421
+ const events = [theme.bold(theme.fg('accent', 'Events')), ...this.eventList.render(leftWidth, Math.max(0, bodyRows - 1))];
422
+ const details = this.detailLines(rightWidth, bodyRows);
423
+ this.listStart = paneStart + 1;
424
+ this.listEnd = this.listStart + events.length - 1;
425
+ this.detailsStart = paneStart;
426
+ const pane = Array.from({ length: Math.max(events.length, details.length) }, (_, index) => {
427
+ const left = truncateToWidth(events[index] ?? '', leftWidth, '');
428
+ const gap = ' '.repeat(Math.max(0, leftWidth - visibleWidth(left)));
429
+ return left + gap + theme.fg('dim', ' │ ') + (details[index] ?? '');
430
+ });
431
+ lines.push(...pane);
367
432
  }
368
- const selected = this.selectedEvent;
369
- lines.push(theme.bold(selected ? 'Selected event · ' + selected.type + ' · #' + selected.sequence : 'Selected event'));
370
- if (selected) {
371
- const details = wrapTextWithAnsi(safe(JSON.stringify({ payload: selected.payload, type: selected.type, sequence: selected.sequence, timestamp: selected.timestamp }, null, 2)), width);
372
- this.detailOffset = Math.min(this.detailOffset, Math.max(0, details.length - detailBudget));
373
- lines.push(...details.slice(this.detailOffset, this.detailOffset + detailBudget).map(line => theme.fg('dim', line)));
433
+ else {
434
+ lines.push(theme.bold(theme.fg('accent', 'Events')));
435
+ this.listStart = lines.length;
436
+ this.listWidth = width;
437
+ const listRows = Math.min(Math.max(0, bodyRows - 1), Math.max(1, Math.floor(bodyRows * 0.58)));
438
+ const events = this.eventList.render(width, listRows);
439
+ this.listEnd = this.listStart + events.length;
440
+ lines.push(...events);
441
+ const hasRoomForDetails = lines.length < rows;
442
+ if (hasRoomForDetails)
443
+ lines.push('');
444
+ this.detailsStart = lines.length;
445
+ const detailRows = Math.max(0, rows - lines.length);
446
+ lines.push(...this.detailLines(width, detailRows));
374
447
  }
375
448
  return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
376
449
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The Worktrees page (`/worktree`): the managed worktrees of the repository this session works in.
3
+ * A person makes, switches to, branches, hands off, removes, restores and prunes them; switching
4
+ * restarts the terminal in the chosen folder as a new conversation (PROP-0016).
5
+ */
6
+ import type { Theme } from '@earendil-works/pi-coding-agent';
7
+ import type { MenuCall, MenuUi } from "../../pluginMenu.js";
8
+ import type { TabInfo } from "../chrome.js";
9
+ import { type ListRow } from "../list.js";
10
+ import type { TerminalMouseInput } from "../mouse.js";
11
+ import type { TerminalPage } from "../page.js";
12
+ import type { WorktreeRecord, WorktreeStatus } from "../../../worktrees/types.js";
13
+ export interface WorktreeOverview {
14
+ repo: string | null;
15
+ current: string | null;
16
+ worktrees: WorktreeStatus[];
17
+ removed: WorktreeRecord[];
18
+ reason: string | null;
19
+ }
20
+ export interface WorktreesPageOptions {
21
+ theme(): Theme;
22
+ call: MenuCall;
23
+ ui: MenuUi & {
24
+ input(title: string, placeholder?: string): Promise<string | undefined>;
25
+ };
26
+ /** Ends the frontend so the host restarts it in the chosen folder. */
27
+ relaunch(): void;
28
+ changed(): void;
29
+ }
30
+ type Row = {
31
+ kind: 'new' | 'permanent' | 'prune' | 'trust' | 'main';
32
+ } | {
33
+ kind: 'worktree';
34
+ status: WorktreeStatus;
35
+ } | {
36
+ kind: 'removed';
37
+ record: WorktreeRecord;
38
+ };
39
+ export declare const WORKTREE_ACTIONS: {
40
+ readonly switch: "Switch this terminal here (new conversation)";
41
+ readonly branch: "Create branch here";
42
+ readonly toSource: "Hand off its changes to the main checkout";
43
+ readonly toWorktree: "Bring the main checkout's changes here";
44
+ readonly remove: "Remove";
45
+ readonly removeForce: "Remove, keeping a snapshot of unsaved work";
46
+ };
47
+ export declare class WorktreesPage implements TerminalPage {
48
+ private readonly options;
49
+ presentation: "centered";
50
+ private overview;
51
+ private error;
52
+ private busy;
53
+ private disposed;
54
+ private readonly list;
55
+ constructor(options: WorktreesPageOptions);
56
+ get focused(): boolean;
57
+ set focused(value: boolean);
58
+ activate(): void;
59
+ dispose(): void;
60
+ tabs(): readonly TabInfo[];
61
+ activeTab(): string;
62
+ hint(): string;
63
+ reload(): Promise<void>;
64
+ rows(): ListRow<Row>[];
65
+ private attempt;
66
+ activateRow(row: Row): Promise<void>;
67
+ private worktreeMenu;
68
+ handleMouse(event: TerminalMouseInput): import("@earendil-works/pi-tui").TuiMouseEventResult | undefined;
69
+ handleInput(data: string): boolean;
70
+ render(width: number, rows: number): string[];
71
+ }
72
+ export {};
@@ -0,0 +1,203 @@
1
+ import { truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
2
+ import { FilterList } from "../list.js";
3
+ const TABS = [{ id: 'worktrees', title: 'Worktrees' }];
4
+ export const WORKTREE_ACTIONS = {
5
+ switch: 'Switch this terminal here (new conversation)',
6
+ branch: 'Create branch here',
7
+ toSource: 'Hand off its changes to the main checkout',
8
+ toWorktree: "Bring the main checkout's changes here",
9
+ remove: 'Remove',
10
+ removeForce: 'Remove, keeping a snapshot of unsaved work'
11
+ };
12
+ export class WorktreesPage {
13
+ options;
14
+ presentation = 'centered';
15
+ overview;
16
+ error;
17
+ busy = false;
18
+ disposed = false;
19
+ list;
20
+ constructor(options) {
21
+ this.options = options;
22
+ this.list = new FilterList({
23
+ theme: options.theme, search: 'always', noun: ['worktree', 'worktrees'], empty: 'No worktrees yet',
24
+ onActivate: row => { if (row.data)
25
+ void this.activateRow(row.data); }
26
+ });
27
+ }
28
+ get focused() { return this.list.focused; }
29
+ set focused(value) { this.list.focused = value; }
30
+ activate() { void this.reload(); }
31
+ dispose() { this.disposed = true; }
32
+ tabs() { return TABS; }
33
+ activeTab() { return 'worktrees'; }
34
+ hint() { return 'Enter choose · type to search · Esc close'; }
35
+ async reload() {
36
+ try {
37
+ this.overview = await this.options.call('terminal.worktrees.list');
38
+ this.error = undefined;
39
+ }
40
+ catch (error) {
41
+ this.error = error instanceof Error ? error.message : String(error);
42
+ }
43
+ if (this.disposed)
44
+ return;
45
+ this.list.setRows(this.rows());
46
+ this.options.changed();
47
+ }
48
+ rows() {
49
+ const overview = this.overview;
50
+ if (!overview?.repo)
51
+ return [];
52
+ const rows = [
53
+ { id: 'new', label: 'New managed worktree', group: 'Actions', detail: 'detached, carries your changes', tone: 'accent', data: { kind: 'new' } },
54
+ { id: 'permanent', label: 'New permanent worktree', group: 'Actions', detail: 'named, never pruned', tone: 'muted', data: { kind: 'permanent' } }
55
+ ];
56
+ if (overview.current)
57
+ rows.push({ id: 'main', label: 'Switch back to the main checkout', group: 'Actions', tone: 'muted', data: { kind: 'main' } });
58
+ rows.push({ id: 'prune', label: 'Prune older managed worktrees', group: 'Actions', tone: 'muted', data: { kind: 'prune' } }, { id: 'trust', label: 'Trust the setup script as it is now', group: 'Actions', tone: 'muted', data: { kind: 'trust' } });
59
+ for (const status of overview.worktrees) {
60
+ const record = status.record;
61
+ const here = record.id === overview.current ? ' · this terminal' : record.lease ? ' · in use' : '';
62
+ const changes = status.changes ? `${status.changes} changed` : 'clean';
63
+ rows.push({
64
+ id: `worktree:${record.id}`, label: record.name, group: 'Worktrees',
65
+ detail: `${record.branch ?? 'detached'} · ${changes}${status.ahead ? ` · ${status.ahead} commit(s)` : ''} · ${record.kind}${here}`,
66
+ tone: record.id === overview.current ? 'success' : status.changes ? 'warning' : 'muted',
67
+ searchText: `${record.name} ${record.branch ?? ''} ${record.path}`,
68
+ data: { kind: 'worktree', status }
69
+ });
70
+ }
71
+ for (const record of overview.removed) {
72
+ rows.push({
73
+ id: `removed:${record.id}`, label: record.name, group: 'Removed (restorable)',
74
+ detail: record.removed?.snapshot ? 'snapshot kept' : 'clean when removed', tone: 'dim', data: { kind: 'removed', record }
75
+ });
76
+ }
77
+ return rows;
78
+ }
79
+ async attempt(work) {
80
+ if (this.busy)
81
+ return;
82
+ this.busy = true;
83
+ try {
84
+ const message = await work();
85
+ if (message)
86
+ this.options.ui.notify(message, 'info');
87
+ }
88
+ catch (error) {
89
+ this.options.ui.notify(error instanceof Error ? error.message : String(error), 'error');
90
+ }
91
+ finally {
92
+ this.busy = false;
93
+ }
94
+ await this.reload();
95
+ }
96
+ async activateRow(row) {
97
+ const call = this.options.call;
98
+ const ui = this.options.ui;
99
+ switch (row.kind) {
100
+ case 'new':
101
+ case 'permanent':
102
+ return this.attempt(async () => {
103
+ const name = row.kind === 'permanent' ? await ui.input('Worktree name', 'lowercase-with-dashes') : undefined;
104
+ if (row.kind === 'permanent' && !name)
105
+ return;
106
+ const result = await call('terminal.worktrees.create', { kind: row.kind === 'permanent' ? 'permanent' : 'managed', ...(name ? { name } : {}) });
107
+ if (result.setup.state === 'untrusted')
108
+ ui.notify('The repository has a setup script you have not trusted; it did not run.', 'warning');
109
+ if (await ui.confirm(`Worktree ${result.worktree.name} is ready`, 'Switch this terminal there now? It starts a new conversation in that folder.')) {
110
+ await call('terminal.worktrees.switch', { id: result.worktree.id });
111
+ this.options.relaunch();
112
+ }
113
+ });
114
+ case 'main':
115
+ return this.attempt(async () => {
116
+ await call('terminal.worktrees.switch', { main: true });
117
+ this.options.relaunch();
118
+ });
119
+ case 'prune':
120
+ return this.attempt(async () => {
121
+ const result = await call('terminal.worktrees.prune');
122
+ return `Removed ${result.removed.length} older worktree(s); unsaved work was kept as snapshots.`;
123
+ });
124
+ case 'trust':
125
+ return this.attempt(async () => {
126
+ if (!await ui.confirm('Trust the setup script?', 'Read .xharness/worktree-setup first. It will run in every new worktree of this repository until its text changes.'))
127
+ return;
128
+ await call('terminal.worktrees.trustSetup');
129
+ return 'Setup script trusted.';
130
+ });
131
+ case 'removed':
132
+ return this.attempt(async () => {
133
+ await call('terminal.worktrees.restore', { id: row.record.id });
134
+ return `Restored ${row.record.name}.`;
135
+ });
136
+ case 'worktree':
137
+ return this.worktreeMenu(row.status);
138
+ }
139
+ }
140
+ async worktreeMenu(status) {
141
+ const record = status.record;
142
+ const current = this.overview?.current === record.id;
143
+ const choices = [
144
+ ...(current ? [] : [WORKTREE_ACTIONS.switch]),
145
+ ...(record.branch ? [] : [WORKTREE_ACTIONS.branch]),
146
+ WORKTREE_ACTIONS.toSource,
147
+ WORKTREE_ACTIONS.toWorktree,
148
+ ...(current ? [] : [status.changes || status.ahead ? WORKTREE_ACTIONS.removeForce : WORKTREE_ACTIONS.remove])
149
+ ];
150
+ const choice = await this.options.ui.select(`${record.name} · ${record.path}`, choices);
151
+ const call = this.options.call;
152
+ const id = record.id;
153
+ if (choice === WORKTREE_ACTIONS.switch)
154
+ return this.attempt(async () => {
155
+ await call('terminal.worktrees.switch', { id });
156
+ this.options.relaunch();
157
+ });
158
+ if (choice === WORKTREE_ACTIONS.branch)
159
+ return this.attempt(async () => {
160
+ const name = await this.options.ui.input('Branch name', 'my-change');
161
+ if (!name)
162
+ return;
163
+ const updated = await call('terminal.worktrees.branch', { id, name });
164
+ return `${record.name} is on branch ${updated.branch}.`;
165
+ });
166
+ if (choice === WORKTREE_ACTIONS.toSource || choice === WORKTREE_ACTIONS.toWorktree)
167
+ return this.attempt(async () => {
168
+ const direction = choice === WORKTREE_ACTIONS.toSource ? 'toSource' : 'toWorktree';
169
+ const result = await call('terminal.worktrees.handoff', { id, direction });
170
+ if (!result.patched && !result.copied.length)
171
+ return 'Nothing to hand off.';
172
+ return `Handed off.${result.stash ? ' The origin keeps a copy in git stash.' : ''}`;
173
+ });
174
+ if (choice === WORKTREE_ACTIONS.remove || choice === WORKTREE_ACTIONS.removeForce)
175
+ return this.attempt(async () => {
176
+ const force = choice === WORKTREE_ACTIONS.removeForce;
177
+ if (force && !await this.options.ui.confirm(`Remove ${record.name}?`, 'Its unsaved changes and commits are kept as a snapshot you can restore from this page.'))
178
+ return;
179
+ await call('terminal.worktrees.remove', { id, force });
180
+ return `Removed ${record.name}.`;
181
+ });
182
+ }
183
+ handleMouse(event) {
184
+ return this.list.handleMouse(event);
185
+ }
186
+ handleInput(data) {
187
+ return this.list.handleInput(data);
188
+ }
189
+ render(width, rows) {
190
+ const theme = this.options.theme();
191
+ if (width < 1 || rows < 1)
192
+ return [];
193
+ if (this.error)
194
+ return [theme.fg('error', `Could not read worktrees: ${this.error}`)];
195
+ if (!this.overview)
196
+ return [theme.fg('dim', 'Reading worktrees…')];
197
+ if (!this.overview.repo) {
198
+ return wrapTextWithAnsi(`Worktrees need a git repository. ${this.overview.reason ?? ''}`.trim(), width).slice(0, rows).map(line => theme.fg('dim', line));
199
+ }
200
+ const header = truncateToWidth(theme.fg('dim', `Repository ${this.overview.repo}`), width, '');
201
+ return [header, '', ...this.list.render(width, Math.max(1, rows - 2))].slice(0, rows).map(line => truncateToWidth(line, width, ''));
202
+ }
203
+ }
@@ -1,2 +1,2 @@
1
- export { parseProposedPlan, planModeTools } from "../../sessions/planMode.js";
1
+ export { normalModeTools, parseProposedPlan, planModeTools, QUESTION_TOOL_NAMES } from "../../sessions/planMode.js";
2
2
  export type { ProposedPlan } from "../../sessions/planMode.js";
@@ -1,2 +1,2 @@
1
1
  // The terminal's plan mode uses the session's read-only tool set.
2
- export { parseProposedPlan, planModeTools } from "../../sessions/planMode.js";
2
+ export { normalModeTools, parseProposedPlan, planModeTools, QUESTION_TOOL_NAMES } from "../../sessions/planMode.js";
@@ -0,0 +1,14 @@
1
+ import type { AnnotateStart } from "./pages/annotatePage.js";
2
+ import type { CommandPageId } from "./commandPages.js";
3
+ import type { TerminalCommandHost } from "./commands.js";
4
+ import { parseAnnotate } from "./annotateCommand.js";
5
+ type PageOptions = {
6
+ runId?: string;
7
+ url?: string;
8
+ annotate?: AnnotateStart;
9
+ };
10
+ type OpenPage = (id: CommandPageId, options?: PageOptions) => Promise<void>;
11
+ type Handler = (argument: string) => Promise<void>;
12
+ /** Terminal adapters for commands declared by Harness-owned feature plugins. */
13
+ export declare function createBuiltinPluginCommandHandlers(host: TerminalCommandHost, openPage: OpenPage): Map<string, Handler>;
14
+ export { parseAnnotate };