@xingular/harness 1.2.0 → 1.3.1-unstable.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 (186) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +26 -17
  3. package/builtin/skills/plugin-authoring/SKILL.md +34 -0
  4. package/dist/cli.js +7362 -6239
  5. package/dist/sdk/agents/builtin.js +3 -3
  6. package/dist/sdk/agents/planResearch.d.ts +23 -1
  7. package/dist/sdk/agents/planResearch.js +71 -8
  8. package/dist/sdk/agents/profiles.d.ts +16 -4
  9. package/dist/sdk/agents/profiles.js +75 -12
  10. package/dist/sdk/annotations/bridge.d.ts +2 -0
  11. package/dist/sdk/annotations/bridge.js +4 -0
  12. package/dist/sdk/annotations/capture.d.ts +44 -2
  13. package/dist/sdk/annotations/capture.js +104 -3
  14. package/dist/sdk/annotations/delivery.d.ts +24 -17
  15. package/dist/sdk/annotations/delivery.js +82 -49
  16. package/dist/sdk/annotations/inspect.js +7 -6
  17. package/dist/sdk/annotations/pageScript.js +4 -4
  18. package/dist/sdk/annotations/part.d.ts +0 -2
  19. package/dist/sdk/annotations/part.js +4 -33
  20. package/dist/sdk/annotations/queue.d.ts +2 -2
  21. package/dist/sdk/annotations/queue.js +12 -7
  22. package/dist/sdk/annotations/serverMethods.js +11 -7
  23. package/dist/sdk/annotations/store.d.ts +13 -21
  24. package/dist/sdk/annotations/store.js +47 -37
  25. package/dist/sdk/capabilities/runSnapshot.js +10 -4
  26. package/dist/sdk/commands/list.js +2 -0
  27. package/dist/sdk/commands/registry.d.ts +11 -3
  28. package/dist/sdk/commands/registry.js +3 -2
  29. package/dist/sdk/core/links.d.ts +51 -0
  30. package/dist/sdk/core/links.js +155 -0
  31. package/dist/sdk/core/shell.d.ts +20 -0
  32. package/dist/sdk/core/shell.js +73 -0
  33. package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
  34. package/dist/sdk/core/workspaceBashTool.js +16 -2
  35. package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
  36. package/dist/sdk/core/workspaceSandbox.js +52 -12
  37. package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
  38. package/dist/sdk/core/workspaceWriteOperations.js +26 -8
  39. package/dist/sdk/doctor/checks.js +71 -12
  40. package/dist/sdk/harness.d.ts +5 -0
  41. package/dist/sdk/harness.js +11 -6
  42. package/dist/sdk/hub/hub.js +5 -3
  43. package/dist/sdk/index.d.ts +3 -2
  44. package/dist/sdk/index.js +2 -1
  45. package/dist/sdk/plugins/builtin.js +14 -9
  46. package/dist/sdk/plugins/builtinRegistry.d.ts +3 -1
  47. package/dist/sdk/plugins/builtinRegistry.js +9 -3
  48. package/dist/sdk/plugins/discover.d.ts +7 -1
  49. package/dist/sdk/plugins/discover.js +41 -2
  50. package/dist/sdk/plugins/identity.js +1 -1
  51. package/dist/sdk/plugins/install.d.ts +6 -2
  52. package/dist/sdk/plugins/install.js +29 -5
  53. package/dist/sdk/plugins/inventory.js +3 -3
  54. package/dist/sdk/plugins/manifest.d.ts +2 -0
  55. package/dist/sdk/plugins/manifest.js +21 -4
  56. package/dist/sdk/plugins/plugins.d.ts +2 -0
  57. package/dist/sdk/plugins/plugins.js +1 -0
  58. package/dist/sdk/plugins/proposals.d.ts +17 -0
  59. package/dist/sdk/plugins/proposals.js +147 -0
  60. package/dist/sdk/plugins/types.d.ts +1 -1
  61. package/dist/sdk/plugins/types.js +1 -1
  62. package/dist/sdk/prompt/compile.d.ts +13 -4
  63. package/dist/sdk/prompt/compile.js +46 -20
  64. package/dist/sdk/prompt/standard.d.ts +1 -1
  65. package/dist/sdk/prompt/standard.js +1 -1
  66. package/dist/sdk/server/methods.js +14 -2
  67. package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
  68. package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
  69. package/dist/sdk/sessions/continuation.d.ts +4 -2
  70. package/dist/sdk/sessions/continuation.js +7 -6
  71. package/dist/sdk/sessions/contracts.d.ts +22 -4
  72. package/dist/sdk/sessions/contracts.js +12 -7
  73. package/dist/sdk/sessions/planMode.js +2 -2
  74. package/dist/sdk/sessions/runStore.js +3 -1
  75. package/dist/sdk/sessions/runner.js +2 -2
  76. package/dist/sdk/sessions/sessionState.d.ts +13 -0
  77. package/dist/sdk/sessions/sessionState.js +31 -0
  78. package/dist/sdk/sessions/sessions.d.ts +7 -0
  79. package/dist/sdk/sessions/sessions.js +73 -15
  80. package/dist/sdk/sessions/subagentService.d.ts +15 -0
  81. package/dist/sdk/sessions/subagentService.js +195 -55
  82. package/dist/sdk/sessions/subagents.d.ts +10 -1
  83. package/dist/sdk/sessions/subagents.js +4 -0
  84. package/dist/sdk/sessions/workSlice.d.ts +8 -5
  85. package/dist/sdk/sessions/workSlice.js +77 -45
  86. package/dist/sdk/terminal/controller.d.ts +20 -0
  87. package/dist/sdk/terminal/controller.js +43 -18
  88. package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
  89. package/dist/sdk/terminal/opentui/ansi.js +192 -0
  90. package/dist/sdk/terminal/opentui/model.d.ts +163 -0
  91. package/dist/sdk/terminal/opentui/model.js +482 -0
  92. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
  93. package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
  94. package/dist/sdk/terminal/opentui/theme.d.ts +2 -0
  95. package/dist/sdk/terminal/opentui/theme.js +2 -0
  96. package/dist/sdk/terminal/piExtension.js +98 -15
  97. package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
  98. package/dist/sdk/terminal/subagentMonitor.js +29 -8
  99. package/dist/sdk/terminal/terminals.d.ts +16 -0
  100. package/dist/sdk/terminal/terminals.js +130 -26
  101. package/dist/sdk/terminal/trajectory.d.ts +6 -1
  102. package/dist/sdk/terminal/trajectory.js +8 -0
  103. package/dist/sdk/terminal/tui/annotateCommand.d.ts +5 -1
  104. package/dist/sdk/terminal/tui/annotateCommand.js +21 -7
  105. package/dist/sdk/terminal/tui/annotateSend.d.ts +30 -0
  106. package/dist/sdk/terminal/tui/annotateSend.js +40 -0
  107. package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
  108. package/dist/sdk/terminal/tui/chrome.js +13 -0
  109. package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
  110. package/dist/sdk/terminal/tui/commandPages.js +16 -11
  111. package/dist/sdk/terminal/tui/commands.d.ts +4 -0
  112. package/dist/sdk/terminal/tui/commands.js +12 -2
  113. package/dist/sdk/terminal/tui/dialogs.js +1 -0
  114. package/dist/sdk/terminal/tui/pageOverlay.d.ts +3 -0
  115. package/dist/sdk/terminal/tui/pageOverlay.js +25 -7
  116. package/dist/sdk/terminal/tui/pages/agentsPage.js +5 -2
  117. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +70 -16
  118. package/dist/sdk/terminal/tui/pages/annotatePage.js +349 -115
  119. package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
  120. package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
  121. package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
  122. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +14 -1
  123. package/dist/sdk/terminal/tui/pages/pluginsPage.js +295 -25
  124. package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
  125. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +4 -2
  126. package/dist/sdk/terminal/tui/pages/usagePage.js +39 -12
  127. package/dist/sdk/terminal/tui/pluginCommands.d.ts +1 -1
  128. package/dist/sdk/terminal/tui/pluginCommands.js +13 -3
  129. package/dist/sdk/tools/defaultSkills.d.ts +4 -0
  130. package/dist/sdk/tools/defaultSkills.js +20 -0
  131. package/dist/sdk/tools/definitions.d.ts +3 -1
  132. package/dist/sdk/tools/definitions.js +17 -1
  133. package/dist/sdk/tools/dispatch.js +12 -0
  134. package/dist/sdk/tools/harnessTools.js +1 -1
  135. package/dist/sdk/tools/sessionName.d.ts +9 -0
  136. package/dist/sdk/tools/sessionName.js +58 -11
  137. package/dist/sdk/tools/spawnSubagent.js +9 -7
  138. package/dist/sdk/tools/todo.d.ts +15 -0
  139. package/dist/sdk/tools/todo.js +64 -0
  140. package/dist/sdk/world/world.d.ts +0 -2
  141. package/dist/terminal-opentui.js +3331 -1584
  142. package/dist/terminal-pi.js +421 -133
  143. package/docs/README.md +3 -0
  144. package/docs/architecture/00-overview.md +1 -1
  145. package/docs/architecture/01-extension-points.md +1 -1
  146. package/docs/architecture/02-sessions-and-runs.md +73 -36
  147. package/docs/architecture/03-variants.md +1 -3
  148. package/docs/architecture/04-prompt.md +24 -13
  149. package/docs/architecture/06-plugins-and-capabilities.md +46 -10
  150. package/docs/architecture/07-tools-and-gate.md +49 -8
  151. package/docs/architecture/09-accounts-and-connectors.md +8 -2
  152. package/docs/architecture/10-server-protocol.md +68 -16
  153. package/docs/architecture/11-terminal.md +94 -45
  154. package/docs/architecture/12-storage.md +1 -0
  155. package/docs/architecture/13-world-tools.md +8 -6
  156. package/docs/architecture/14-surfaces-and-parity.md +4 -1
  157. package/docs/architecture/README.md +20 -18
  158. package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
  159. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +6 -2
  160. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
  161. package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
  162. package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
  163. package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
  164. package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
  165. package/docs/architecture/proposals/PROP-0048-simple-annotate.md +85 -0
  166. package/docs/architecture/proposals/README.md +11 -6
  167. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
  168. package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
  169. package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
  170. package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
  171. package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
  172. package/docs/decisions/ADR-0044-annotations-pending-or-sent.md +24 -0
  173. package/docs/decisions/ADR-0045-default-plugin-authoring-skill.md +9 -0
  174. package/docs/decisions/ADR-0046-hash-bound-plugin-proposals.md +20 -0
  175. package/docs/decisions/ADR-0047-links-open-with-the-os-handler.md +47 -0
  176. package/docs/doctor.md +14 -1
  177. package/docs/parity.md +16 -13
  178. package/docs/releases/1.3.0.md +21 -0
  179. package/docs/releases/1.3.1-unstable.1.md +8 -0
  180. package/docs/releases/README.md +9 -6
  181. package/docs/terminal-tui.md +127 -65
  182. package/package.json +1 -1
  183. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +0 -75
  184. package/dist/sdk/terminal/tui/pages/notesPage.js +0 -201
  185. package/dist/sdk/tools/checklist.d.ts +0 -9
  186. package/dist/sdk/tools/checklist.js +0 -55
@@ -1,14 +1,22 @@
1
- import { truncateToWidth } from '@earendil-works/pi-tui';
1
+ import { Input, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
2
2
  import { FilterList } from "../list.js";
3
- const TABS = [{ id: 'annotate', title: 'Annotate' }];
3
+ import { normalizeAddress, normalizeDevTools } from "../annotateCommand.js";
4
+ import { describeSend, sendUnsent, unsent } from "../annotateSend.js";
5
+ const TABS = [{ id: 'annotate', title: 'Annotate' }, { id: 'all', title: 'All annotations' }];
6
+ const FIELDS = {
7
+ address: { label: 'Page address', placeholder: 'http://localhost:5173' },
8
+ port: { label: 'DevTools port', placeholder: '9222' },
9
+ path: { label: 'App to start', placeholder: '/Applications/MyApp.app' }
10
+ };
4
11
  export const ANNOTATE_ACTIONS = {
5
12
  reveal: 'Show it in the window',
6
- send: 'Send to the agent now',
7
- confirm: 'Confirm sending it (it came from the page)',
8
- queue: 'Ask the agent to fix it (add to queue)',
9
- dequeue: 'Take it out of the queue',
13
+ send: 'Send it now',
14
+ confirm: 'Confirm and send it (it came from the page)',
10
15
  edit: 'Edit the comment',
11
16
  resolve: 'Resolve',
17
+ reopen: 'Reopen',
18
+ share: 'Share it with this session',
19
+ unshare: 'Stop sharing it with this session',
12
20
  delete: 'Delete'
13
21
  };
14
22
  const clip = (text, size) => (text.length > size ? `${text.slice(0, size - 1)}…` : text);
@@ -19,45 +27,84 @@ export function describeTarget(target) {
19
27
  return `<${target.tag}> ${clip(target.name || target.text || target.selector, 40)}`;
20
28
  return 'whole page';
21
29
  }
22
- /** Which section of the panel a note belongs in, for this session; null when it is not shown. */
30
+ /** Which group of the Annotate tab a note belongs in, for this session; null when it is not shown there. */
23
31
  export function section(note, sessionId) {
24
- if (note.status === 'resolved')
32
+ if (note.status === 'resolved' || !note.delivery || note.delivery.sessionId !== sessionId)
25
33
  return null;
26
- const delivery = note.delivery;
27
- if (delivery && delivery.sessionId === sessionId)
28
- return delivery.state === 'queued' ? 'Queue' : 'Sent and fixed';
29
- if (!delivery)
30
- return 'Saved';
31
- return null;
34
+ return note.delivery.state === 'pending' ? 'Unsent' : 'Sent';
32
35
  }
33
36
  export class AnnotatePage {
34
37
  options;
35
38
  presentation = 'fullscreen';
39
+ tab;
40
+ view = 'notes';
41
+ field = null;
36
42
  notes;
37
43
  sessionId = null;
38
44
  target = null;
45
+ apps = null;
39
46
  bridgeSnippet = null;
40
47
  error;
41
48
  busy = false;
42
49
  disposed = false;
43
50
  polling = false;
51
+ focusedValue = false;
44
52
  timer = null;
45
53
  refreshes = 0;
46
- list;
54
+ listTop = 0;
55
+ noteList;
56
+ appList;
57
+ allList;
47
58
  constructor(options) {
48
59
  this.options = options;
49
- this.list = new FilterList({
50
- theme: options.theme, search: 'never', noun: ['note', 'notes'], empty: 'Nothing here yet. Press i and click a component in the window.',
60
+ this.tab = options.initialTab ?? 'annotate';
61
+ this.noteList = new FilterList({
62
+ theme: options.theme, search: 'never', noun: ['annotation', 'annotations'],
63
+ empty: 'No annotations yet. Press i and click a component in the window.',
64
+ onActivate: row => { if (row.data)
65
+ void this.noteMenu(row.data.note, 'annotate'); }
66
+ });
67
+ this.appList = new FilterList({
68
+ theme: options.theme, search: 'never', noun: ['choice', 'choices'],
51
69
  onActivate: row => { if (row.data)
52
- void this.noteMenu(row.data.note); }
70
+ void this.chooseApp(row.data); }
53
71
  });
72
+ this.allList = new FilterList({
73
+ theme: options.theme, search: 'always', noun: ['annotation', 'annotations'], empty: 'No web annotations yet',
74
+ onActivate: row => { if (row.data)
75
+ void this.noteMenu(row.data.note, 'all'); }
76
+ });
77
+ }
78
+ get focused() { return this.focusedValue; }
79
+ set focused(value) {
80
+ this.focusedValue = value;
81
+ this.syncFocus();
82
+ }
83
+ syncFocus() {
84
+ const value = this.focusedValue;
85
+ this.noteList.focused = value && this.tab === 'annotate' && this.view === 'notes' && !this.field;
86
+ this.appList.focused = value && this.tab === 'annotate' && this.view === 'apps' && !this.field;
87
+ this.allList.focused = value && this.tab === 'all';
88
+ if (this.field)
89
+ this.field.input.focused = value && this.tab === 'annotate';
54
90
  }
55
- get focused() { return this.list.focused; }
56
- set focused(value) { this.list.focused = value; }
57
91
  tabs() { return TABS; }
58
- activeTab() { return 'annotate'; }
92
+ activeTab() { return this.tab; }
93
+ selectTab(id) {
94
+ if (id !== 'annotate' && id !== 'all')
95
+ return;
96
+ this.tab = id;
97
+ this.syncFocus();
98
+ this.options.changed();
99
+ }
59
100
  hint() {
60
- return `${this.target?.inspecting ? 'i stop inspecting' : 'i inspect'} · s send queue · x unqueue · t window · Enter note · Esc close`;
101
+ if (this.tab === 'all')
102
+ return 'Enter choose · type to search · Tab Annotate · Esc close';
103
+ if (this.field)
104
+ return 'Enter go · Esc cancel';
105
+ if (this.view === 'apps')
106
+ return 'Enter choose · r look again · Esc back';
107
+ return `${this.target?.inspecting ? 'i stop inspecting' : 'i inspect'} · s send unsent · o page · a app · t window · Enter note · Tab all · Esc close`;
61
108
  }
62
109
  activate() {
63
110
  void this.start();
@@ -70,32 +117,158 @@ export class AnnotatePage {
70
117
  void this.options.call('terminal.annotations.stopInspect').catch(() => undefined);
71
118
  }
72
119
  async start() {
73
- const { call } = this.options;
74
120
  const start = this.options.start;
75
- await this.attempt(async () => {
76
- if (start.kind === 'web') {
77
- const url = start.url ?? await this.options.ui.input('Page or dev server address', 'http://localhost:5173');
78
- if (!url)
79
- return;
80
- await call('terminal.annotations.open', { url });
81
- }
82
- else if (start.kind === 'cdp') {
83
- await call('terminal.annotations.attach', { kind: 'cdp', endpoint: start.endpoint, ...(start.name ? { name: start.name } : {}) });
84
- }
85
- else {
86
- const attached = await call('terminal.annotations.attach', { kind: 'bridge', ...(start.name ? { name: start.name } : {}) });
87
- this.bridgeSnippet = attached.bridge?.snippet ?? null;
88
- }
89
- await this.refreshTarget();
90
- if (this.target?.windows.length || start.kind !== 'bridge')
91
- await this.toggleInspect(true);
92
- return start.kind === 'bridge' && !this.target?.windows.length ? 'Load the bridge script in the app (shown at the top), then reload the app.' : undefined;
93
- });
121
+ await this.reload();
122
+ if (start.kind === 'web' && !start.url) {
123
+ // The capture target outlives the page: reopening returns to the window still open.
124
+ if (!await this.restore())
125
+ this.openField('address');
126
+ }
127
+ else if (start.kind === 'apps')
128
+ await this.showApps();
129
+ else
130
+ await this.attempt(async () => {
131
+ if (start.kind === 'web')
132
+ await this.openPage(start.url);
133
+ else if (start.kind === 'cdp')
134
+ await this.attachApp(start.endpoint, start.name);
135
+ else if (start.kind === 'bridge')
136
+ return this.attachBridge(start.name);
137
+ });
94
138
  this.schedule();
95
139
  }
140
+ /** Shows the target that is still open, with the inspector on again; false when no window is open. */
141
+ async restore() {
142
+ try {
143
+ await this.refreshTarget();
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ const target = this.target;
149
+ if (!target || (target.kind === 'web' && !target.windows.length))
150
+ return false;
151
+ this.view = 'notes';
152
+ this.bridgeSnippet = target.bridge?.snippet ?? null;
153
+ this.syncFocus();
154
+ if (target.windows.length)
155
+ await this.toggleInspect(true).catch(() => undefined);
156
+ this.options.changed();
157
+ return true;
158
+ }
159
+ // ---- The target: a page, an app over its DevTools port, or the page bridge ----------------------
96
160
  async refreshTarget() {
97
161
  this.target = await this.options.call('terminal.annotations.target');
98
162
  }
163
+ async openPage(url) {
164
+ await this.options.call('terminal.annotations.open', { url });
165
+ await this.refreshTarget();
166
+ await this.toggleInspect(true);
167
+ }
168
+ async attachApp(endpoint, name) {
169
+ await this.options.call('terminal.annotations.attach', { kind: 'cdp', endpoint, ...(name ? { name } : {}) });
170
+ await this.refreshTarget();
171
+ this.view = 'notes';
172
+ this.syncFocus();
173
+ await this.toggleInspect(true);
174
+ }
175
+ async attachBridge(name) {
176
+ const attached = await this.options.call('terminal.annotations.attach', { kind: 'bridge', ...(name ? { name } : {}) });
177
+ this.bridgeSnippet = attached.bridge?.snippet ?? null;
178
+ await this.refreshTarget();
179
+ this.view = 'notes';
180
+ this.syncFocus();
181
+ if (this.target?.windows.length) {
182
+ await this.toggleInspect(true);
183
+ return undefined;
184
+ }
185
+ return 'Load the bridge script in the app (shown at the top), then reload the app.';
186
+ }
187
+ openField(kind) {
188
+ const input = new Input({ prompt: '', placeholder: FIELDS[kind].placeholder, placeholderStyle: text => this.options.theme().fg('dim', text) });
189
+ this.field = { kind, input };
190
+ this.syncFocus();
191
+ this.options.changed();
192
+ }
193
+ closeField() {
194
+ this.field = null;
195
+ this.syncFocus();
196
+ this.options.changed();
197
+ }
198
+ submitField() {
199
+ const field = this.field;
200
+ if (!field)
201
+ return;
202
+ const value = field.input.getValue().trim();
203
+ if (!value)
204
+ return;
205
+ this.closeField();
206
+ void this.attempt(async () => {
207
+ if (field.kind === 'address')
208
+ return this.openPage(normalizeAddress(value));
209
+ if (field.kind === 'port')
210
+ return this.attachApp(normalizeDevTools(value));
211
+ const launched = await this.options.call('terminal.annotations.launch', { path: value });
212
+ await this.refreshTarget();
213
+ this.view = 'notes';
214
+ this.syncFocus();
215
+ await this.toggleInspect(true);
216
+ return `Started ${launched.app?.name ?? 'the app'} and attached to its windows.`;
217
+ });
218
+ }
219
+ async showApps() {
220
+ this.view = 'apps';
221
+ this.syncFocus();
222
+ this.appList.setRows(this.appRows());
223
+ this.options.changed();
224
+ try {
225
+ this.apps = (await this.options.call('terminal.annotations.discover')).apps;
226
+ }
227
+ catch (error) {
228
+ this.apps = [];
229
+ this.error = error instanceof Error ? error.message : String(error);
230
+ }
231
+ if (this.disposed)
232
+ return;
233
+ this.appList.setRows(this.appRows());
234
+ // The first app found is the likely choice.
235
+ const first = this.apps[0];
236
+ this.appList.select(first ? `app:${first.port}` : 'launch');
237
+ this.options.changed();
238
+ }
239
+ appRows() {
240
+ const rows = [];
241
+ const group = 'Apps with a DevTools port on this computer';
242
+ if (!this.apps)
243
+ rows.push({ id: 'looking', label: 'Looking for apps…', group, disabled: true });
244
+ else if (!this.apps.length)
245
+ rows.push({ id: 'none', label: 'None found on ports 9222–9230', group, detail: 'start one below', disabled: true, tone: 'dim' });
246
+ for (const app of this.apps ?? []) {
247
+ rows.push({
248
+ id: `app:${app.port}`, label: app.name, group, tone: 'accent', data: { kind: 'app', app },
249
+ detail: `${app.browser} · port ${app.port} · ${app.windows} window${app.windows === 1 ? '' : 's'}`
250
+ });
251
+ }
252
+ rows.push({ id: 'launch', label: 'Start an app with a DevTools port…', group: 'Other ways', detail: 'Electron or Chromium app', data: { kind: 'launch' } }, { id: 'port', label: 'Type a DevTools port…', group: 'Other ways', detail: 'an app started with --remote-debugging-port', data: { kind: 'port' } }, { id: 'bridge', label: 'Use the page bridge…', group: 'Other ways', detail: 'a web view with no DevTools port', data: { kind: 'bridge' } }, { id: 'web', label: 'Annotate a web page instead…', group: 'Other ways', detail: 'opens a browser window', data: { kind: 'web' } });
253
+ return rows;
254
+ }
255
+ chooseApp(row) {
256
+ if (row.kind === 'port')
257
+ return this.openField('port');
258
+ if (row.kind === 'launch')
259
+ return this.openField('path');
260
+ if (row.kind === 'web') {
261
+ this.view = 'notes';
262
+ return this.openField('address');
263
+ }
264
+ if (row.kind === 'bridge')
265
+ return this.attempt(() => this.attachBridge());
266
+ return this.attempt(async () => {
267
+ await this.attachApp(row.app.endpoint, row.app.name);
268
+ return `Attached to ${row.app.name}.`;
269
+ });
270
+ }
271
+ // ---- Notes ------------------------------------------------------------------------------------
99
272
  async reload() {
100
273
  try {
101
274
  const result = await this.options.call('terminal.annotations.list');
@@ -108,30 +281,38 @@ export class AnnotatePage {
108
281
  }
109
282
  if (this.disposed)
110
283
  return;
111
- this.list.setRows(this.rows());
284
+ this.noteList.setRows(this.rows());
285
+ this.allList.setRows(this.allRows());
112
286
  this.options.changed();
113
287
  }
288
+ /** The Annotate tab: this session's unsent and sent annotations. */
114
289
  rows() {
115
- const order = ['Queue', 'Sent and fixed', 'Saved'];
116
- const rows = [];
117
- for (const group of order) {
118
- for (const note of (this.notes ?? []).filter(entry => section(entry, this.sessionId) === group)) {
119
- const delivery = note.delivery;
120
- const state = !delivery ? describeTarget(note.target)
121
- : delivery.state === 'queued' ? `${describeTarget(note.target)}${delivery.confirm ? ' · confirm to send' : delivery.autoSend ? ' · sends when idle' : ''}`
122
- : delivery.state === 'fixed' ? `fixed: ${clip(delivery.summary ?? '', 50)}` : `sent · ${describeTarget(note.target)}`;
123
- rows.push({
124
- id: `note:${note.id}`, label: clip(note.body.replace(/\s+/g, ' '), 60), group: group === 'Queue' ? `Queue (${this.queued().length})` : group,
125
- detail: state,
126
- tone: delivery?.state === 'fixed' ? 'success' : delivery?.confirm ? 'warning' : delivery?.state === 'queued' ? 'accent' : 'muted',
127
- data: { kind: 'note', note }
128
- });
129
- }
130
- }
131
- return rows;
290
+ const waiting = unsent(this.notes ?? [], this.sessionId);
291
+ const sent = (this.notes ?? []).filter(note => section(note, this.sessionId) === 'Sent');
292
+ const row = (note, group) => {
293
+ const delivery = note.delivery;
294
+ const state = delivery.state === 'sent' ? `sent · ${describeTarget(note.target)}`
295
+ : `${describeTarget(note.target)}${delivery.confirm ? ' · confirm before sending' : delivery.autoSend ? ' · sends when the agent finishes' : ''}`;
296
+ return {
297
+ id: `note:${note.id}`, label: clip(note.body.replace(/\s+/g, ' '), 60), group, detail: state,
298
+ tone: delivery.state === 'sent' ? 'muted' : delivery.confirm ? 'warning' : 'accent',
299
+ data: { kind: 'note', note }
300
+ };
301
+ };
302
+ return [...waiting.map(note => row(note, `Unsent (${waiting.length})`)), ...sent.map(note => row(note, 'Sent'))];
132
303
  }
133
- queued() {
134
- return (this.notes ?? []).filter(note => section(note, this.sessionId) === 'Queue');
304
+ shared(note) {
305
+ return note.grants.some(grant => grant.principal.sessionId && grant.principal.sessionId === this.sessionId);
306
+ }
307
+ /** The All annotations tab: every saved annotation. */
308
+ allRows() {
309
+ return (this.notes ?? []).map(note => ({
310
+ id: `note:${note.id}`, label: clip(note.body.replace(/\s+/g, ' '), 60), group: 'Annotations',
311
+ detail: `${describeTarget(note.target)} · ${note.status}${this.shared(note) ? ' · shared here' : ''}`,
312
+ tone: note.status === 'resolved' ? 'dim' : this.shared(note) ? 'success' : 'muted',
313
+ searchText: `${note.body} ${note.source.url} ${note.source.title} ${note.tags.join(' ')}`,
314
+ data: { kind: 'note', note }
315
+ }));
135
316
  }
136
317
  /** Polls the inspector for the person's next comment, and the notes every few seconds. */
137
318
  schedule() {
@@ -184,9 +365,12 @@ export class AnnotatePage {
184
365
  await this.refreshTarget();
185
366
  this.options.changed();
186
367
  }
368
+ /** Runs one action at a time; a key pressed meanwhile is answered, never dropped silently. */
187
369
  async attempt(work) {
188
- if (this.busy)
370
+ if (this.busy) {
371
+ this.options.ui.notify('Still working on the last step. Try again in a moment.', 'info');
189
372
  return;
373
+ }
190
374
  this.busy = true;
191
375
  try {
192
376
  const message = await work();
@@ -202,21 +386,8 @@ export class AnnotatePage {
202
386
  }
203
387
  await this.reload();
204
388
  }
205
- sendQueue() {
206
- return this.attempt(async () => {
207
- const waiting = this.queued();
208
- if (!waiting.length)
209
- return 'The queue is empty.';
210
- const unconfirmed = waiting.filter(note => note.delivery?.confirm);
211
- if (unconfirmed.length) {
212
- if (!await this.options.ui.confirm(`Send ${waiting.length} note(s) to the agent?`, `${unconfirmed.length} came from the page's own scripts. Check that you wrote them.`))
213
- return;
214
- for (const note of unconfirmed)
215
- await this.options.call('terminal.annotations.confirm', { id: note.id, expectedVersion: note.version });
216
- }
217
- const result = await this.options.call('terminal.annotations.send');
218
- return result.state === 'sent' ? `Sent ${result.ids.length} note(s) to the agent.` : result.state === 'busy' ? 'The agent is working; the queue goes out when it finishes.' : 'Nothing was sent.';
219
- });
389
+ sendAll() {
390
+ return this.attempt(() => sendUnsent(this.options.call, this.options.ui));
220
391
  }
221
392
  chooseWindow() {
222
393
  return this.attempt(async () => {
@@ -233,16 +404,24 @@ export class AnnotatePage {
233
404
  await this.refreshTarget();
234
405
  });
235
406
  }
236
- async noteMenu(note) {
407
+ async noteMenu(note, from) {
237
408
  const { call, ui } = this.options;
238
- const where = section(note, this.sessionId);
239
- const choices = [
240
- ANNOTATE_ACTIONS.reveal,
241
- ...(where === 'Queue' ? [note.delivery?.confirm ? ANNOTATE_ACTIONS.confirm : ANNOTATE_ACTIONS.send, ANNOTATE_ACTIONS.dequeue] : [ANNOTATE_ACTIONS.queue]),
242
- ANNOTATE_ACTIONS.edit,
243
- ANNOTATE_ACTIONS.resolve,
244
- ANNOTATE_ACTIONS.delete
245
- ];
409
+ const pendingHere = section(note, this.sessionId) === 'Unsent';
410
+ const choices = from === 'annotate'
411
+ ? [
412
+ ANNOTATE_ACTIONS.reveal,
413
+ ...(pendingHere ? [note.delivery?.confirm ? ANNOTATE_ACTIONS.confirm : ANNOTATE_ACTIONS.send] : []),
414
+ ANNOTATE_ACTIONS.edit,
415
+ ANNOTATE_ACTIONS.resolve,
416
+ ANNOTATE_ACTIONS.delete
417
+ ]
418
+ : [
419
+ ANNOTATE_ACTIONS.reveal,
420
+ ANNOTATE_ACTIONS.edit,
421
+ note.status === 'open' ? ANNOTATE_ACTIONS.resolve : ANNOTATE_ACTIONS.reopen,
422
+ ...(note.status === 'open' ? [this.shared(note) ? ANNOTATE_ACTIONS.unshare : ANNOTATE_ACTIONS.share] : []),
423
+ ANNOTATE_ACTIONS.delete
424
+ ];
246
425
  const choice = await ui.select(`${clip(note.source.app?.name ?? note.source.title ?? note.source.url, 50)} · ${describeTarget(note.target)}`, choices);
247
426
  const id = note.id;
248
427
  if (choice === ANNOTATE_ACTIONS.reveal)
@@ -250,21 +429,11 @@ export class AnnotatePage {
250
429
  const result = await call('terminal.annotations.reveal', { id });
251
430
  return result.state === 'matched' ? 'Marked in the window.' : `The target is ${result.state}${result.reason ? `: ${result.reason}` : ''}.`;
252
431
  });
253
- if (choice === ANNOTATE_ACTIONS.queue)
254
- return this.attempt(async () => {
255
- await call('terminal.annotations.queue', { id, expectedVersion: note.version });
256
- return 'Added to the queue.';
257
- });
258
- if (choice === ANNOTATE_ACTIONS.dequeue)
259
- return this.attempt(async () => {
260
- await call('terminal.annotations.dequeue', { id, expectedVersion: note.version });
261
- });
262
432
  if (choice === ANNOTATE_ACTIONS.confirm || choice === ANNOTATE_ACTIONS.send)
263
433
  return this.attempt(async () => {
264
434
  if (choice === ANNOTATE_ACTIONS.confirm)
265
435
  await call('terminal.annotations.confirm', { id, expectedVersion: note.version });
266
- const result = await call('terminal.annotations.send', { ids: [id] });
267
- return result.state === 'sent' ? 'Sent to the agent.' : result.state === 'busy' ? 'It goes to the agent when it finishes.' : undefined;
436
+ return describeSend(await call('terminal.annotations.send', { ids: [id] }));
268
437
  });
269
438
  if (choice === ANNOTATE_ACTIONS.edit)
270
439
  return this.attempt(async () => {
@@ -273,58 +442,116 @@ export class AnnotatePage {
273
442
  return;
274
443
  await call('terminal.annotations.update', { id, expectedVersion: note.version, patch: { body } });
275
444
  });
276
- if (choice === ANNOTATE_ACTIONS.resolve)
445
+ if (choice === ANNOTATE_ACTIONS.resolve || choice === ANNOTATE_ACTIONS.reopen)
446
+ return this.attempt(async () => {
447
+ await call('terminal.annotations.update', { id, expectedVersion: note.version, patch: { status: choice === ANNOTATE_ACTIONS.resolve ? 'resolved' : 'open' } });
448
+ return choice === ANNOTATE_ACTIONS.resolve ? 'Resolved.' : 'Reopened.';
449
+ });
450
+ if (choice === ANNOTATE_ACTIONS.share || choice === ANNOTATE_ACTIONS.unshare)
277
451
  return this.attempt(async () => {
278
- await call('terminal.annotations.update', { id, expectedVersion: note.version, patch: { status: 'resolved' } });
279
- return 'Resolved.';
452
+ await call(choice === ANNOTATE_ACTIONS.share ? 'terminal.annotations.share' : 'terminal.annotations.unshare', { id });
453
+ return choice === ANNOTATE_ACTIONS.share ? 'Shared with this session. It is unsent until you send it.' : 'No longer shared with this session.';
280
454
  });
281
455
  if (choice === ANNOTATE_ACTIONS.delete)
282
456
  return this.attempt(async () => {
283
- if (!await ui.confirm('Delete this note?', 'It cannot be restored.'))
457
+ if (!await ui.confirm('Delete this annotation?', 'It cannot be restored.'))
284
458
  return;
285
459
  await call('terminal.annotations.delete', { id, expectedVersion: note.version });
286
460
  });
287
461
  }
462
+ // ---- Input and rendering -------------------------------------------------------------------------
463
+ currentList() {
464
+ if (this.tab === 'all')
465
+ return this.allList;
466
+ return this.view === 'apps' ? this.appList : this.noteList;
467
+ }
288
468
  handleMouse(event) {
289
- return this.list.handleMouse(event);
469
+ return this.currentList().handleMouse({ ...event, y: event.y - this.listTop });
290
470
  }
291
471
  handleInput(data) {
472
+ if (this.tab === 'all') {
473
+ if (matchesKey(data, 'tab') || matchesKey(data, 'shift+tab')) {
474
+ this.selectTab('annotate');
475
+ return true;
476
+ }
477
+ if (matchesKey(data, 'escape'))
478
+ return this.allList.query ? this.allList.handleInput(data) : false;
479
+ return this.allList.handleInput(data);
480
+ }
481
+ if (this.field) {
482
+ if (matchesKey(data, 'escape')) {
483
+ this.closeField();
484
+ return true;
485
+ }
486
+ if (matchesKey(data, 'enter')) {
487
+ this.submitField();
488
+ return true;
489
+ }
490
+ this.field.input.handleInput(data);
491
+ return true;
492
+ }
493
+ if (matchesKey(data, 'tab') || matchesKey(data, 'shift+tab')) {
494
+ this.selectTab('all');
495
+ return true;
496
+ }
497
+ if (this.view === 'apps') {
498
+ if (matchesKey(data, 'escape')) {
499
+ this.view = 'notes';
500
+ this.syncFocus();
501
+ return true;
502
+ }
503
+ if (data === 'r') {
504
+ void this.showApps();
505
+ return true;
506
+ }
507
+ return this.appList.handleInput(data);
508
+ }
292
509
  if (data === 'i') {
293
510
  void this.attempt(() => this.toggleInspect(!this.target?.inspecting));
294
511
  return true;
295
512
  }
296
513
  if (data === 's') {
297
- void this.sendQueue();
514
+ void this.sendAll();
298
515
  return true;
299
516
  }
300
517
  if (data === 't') {
301
518
  void this.chooseWindow();
302
519
  return true;
303
520
  }
304
- if (data === 'x') {
305
- const row = this.list.selected?.data;
306
- if (row && section(row.note, this.sessionId) === 'Queue')
307
- void this.attempt(async () => { await this.options.call('terminal.annotations.dequeue', { id: row.note.id, expectedVersion: row.note.version }); });
521
+ if (data === 'o') {
522
+ this.openField('address');
523
+ return true;
524
+ }
525
+ if (data === 'a') {
526
+ void this.showApps();
308
527
  return true;
309
528
  }
310
- return this.list.handleInput(data);
529
+ return this.noteList.handleInput(data);
311
530
  }
312
531
  header(width) {
313
532
  const theme = this.options.theme();
314
533
  const target = this.target;
315
534
  const current = target?.windows.find(window => window.current);
316
- const kind = !target ? 'Starting…' : target.kind === 'web' ? 'Browser window' : target.kind === 'cdp' ? `App · ${target.app?.name ?? ''} (DevTools)` : `App · ${target.app?.name ?? ''} (page bridge)`;
535
+ const kind = !target ? 'No window yet' : target.kind === 'web' ? 'Browser window' : target.kind === 'cdp' ? `App · ${target.app?.name ?? ''} (DevTools)` : `App · ${target.app?.name ?? ''} (page bridge)`;
317
536
  const lines = [
318
537
  `${theme.bold(kind)}${current ? theme.fg('muted', ` · ${clip(current.title || current.url, 40)} · ${clip(current.url, 60)}`) : ''}`,
319
- target?.inspecting
320
- ? theme.fg('success', '● Inspecting: hover a component in the window and click it to comment.')
321
- : theme.fg('dim', '○ Inspector off. Press i to turn it on.')
538
+ !target
539
+ ? theme.fg('dim', 'Type a page address, or press a to annotate a desktop app.')
540
+ : target.inspecting
541
+ ? theme.fg('success', '● Inspecting: hover a component in the window and click it. The agent can read every annotation.')
542
+ : theme.fg('dim', '○ Inspector off. Press i to turn it on.')
322
543
  ];
323
544
  if (this.bridgeSnippet && target?.kind === 'bridge') {
324
545
  lines.push(theme.fg(target.windows.length ? 'dim' : 'warning', `Load in the app while developing: ${this.bridgeSnippet}`));
325
546
  }
326
547
  if (this.error)
327
548
  lines.push(theme.fg('error', this.error));
549
+ if (this.field) {
550
+ const { label } = FIELDS[this.field.kind];
551
+ const prefix = `${theme.fg('accent', '›')} ${theme.bold(label)} `;
552
+ const room = Math.max(1, width - visibleWidth(prefix));
553
+ lines.push('', prefix + (this.field.input.render(room)[0] ?? ''));
554
+ }
328
555
  lines.push('');
329
556
  return lines.map(line => truncateToWidth(line, width, ''));
330
557
  }
@@ -332,10 +559,17 @@ export class AnnotatePage {
332
559
  const theme = this.options.theme();
333
560
  if (width < 1 || rows < 1)
334
561
  return [];
562
+ if (this.tab === 'all') {
563
+ this.listTop = 0;
564
+ if (!this.notes)
565
+ return [theme.fg('dim', this.error ? `Could not read web annotations: ${this.error}` : 'Reading web annotations…')];
566
+ return this.allList.render(width, rows).slice(0, rows).map(line => truncateToWidth(line, width, ''));
567
+ }
335
568
  const header = this.header(width);
569
+ this.listTop = header.length;
336
570
  if (!this.notes)
337
- return [...header, theme.fg('dim', 'Reading page notes…')].slice(0, rows);
338
- const body = this.list.render(width, Math.max(1, rows - header.length));
571
+ return [...header, theme.fg('dim', 'Reading annotations…')].slice(0, rows);
572
+ const body = this.currentList().render(width, Math.max(1, rows - header.length));
339
573
  return [...header, ...body].slice(0, rows).map(line => truncateToWidth(line, width, ''));
340
574
  }
341
575
  }
@@ -194,10 +194,11 @@ export class BrowserPage {
194
194
  if (row.hub) {
195
195
  const entry = row.hub;
196
196
  const installed = this.snapshot?.hub?.installed.some(receipt => receipt.entry.id === entry.id && receipt.entry.version === entry.version);
197
- actions.push({ id: 'preview', label: 'Preview reviewed content', run: async () => {
198
- const page = await this.options.call('terminal.browser.hubPreview', { id: entry.id, version: entry.version });
199
- this.showContent(page.content + (page.done ? '' : '\n\n[Preview limited to first page.]'));
200
- } });
197
+ if (entry.delivery.kind !== 'pi-package')
198
+ actions.push({ id: 'preview', label: 'Preview reviewed content', run: async () => {
199
+ const page = await this.options.call('terminal.browser.hubPreview', { id: entry.id, version: entry.version });
200
+ this.showContent(page.content + (page.done ? '' : '\n\n[Preview limited to first page.]'));
201
+ } });
201
202
  if (!installed)
202
203
  actions.push({ id: 'install', label: `Install ${entry.version}`, run: async () => {
203
204
  if (!await this.options.ui.confirm(`Install ${entry.title}?`, `${entry.description}\nPublisher: ${entry.publisher}\nLicense: ${entry.license}\nSource: ${entry.sourceUrl}\nVersion: ${entry.version}\nInstallation does not grant trust, enablement, credentials or permissions.`))