amicus 4.4.0 → 4.4.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 (75) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +32 -0
  3. package/README.md +3 -1
  4. package/docs/DISTRIBUTION.md +234 -0
  5. package/docs/ROADMAP.md +200 -0
  6. package/docs/SHIMS.md +62 -0
  7. package/docs/architecture.md +104 -0
  8. package/docs/configuration.md +371 -0
  9. package/docs/council.md +911 -0
  10. package/docs/doc-system.md +92 -0
  11. package/docs/electron-testing.md +471 -0
  12. package/docs/jsdoc-setup.md +75 -0
  13. package/docs/opencode-integration.md +114 -0
  14. package/docs/publishing.md +60 -0
  15. package/docs/schemas.md +55 -0
  16. package/docs/testing.md +589 -0
  17. package/docs/troubleshooting.md +298 -0
  18. package/docs/usage.md +699 -0
  19. package/electron/fold.js +1 -1
  20. package/electron/main.js +4 -1
  21. package/electron/setup-ui-aliases.js +6 -6
  22. package/electron/workspace-ui/live-model.js +12 -1
  23. package/electron/workspace-ui/md-lite.js +52 -8
  24. package/electron/workspace-ui/workspace-matrix.js +46 -9
  25. package/electron/workspace-ui/workspace-panels.js +14 -3
  26. package/electron/workspace-ui/workspace-render.js +7 -1
  27. package/electron/workspace-ui/workspace-verbs.js +48 -2
  28. package/package.json +8 -3
  29. package/schemas/council-run.schema.json +20 -0
  30. package/schemas/progress.schema.json +12 -0
  31. package/schemas/spend.schema.json +52 -4
  32. package/src/cli-handlers-spend.js +20 -2
  33. package/src/cli-handlers-watch.js +11 -0
  34. package/src/cli.js +4 -2
  35. package/src/council/briefings-debate.js +27 -7
  36. package/src/council/briefings-stage2.js +155 -25
  37. package/src/council/briefings.js +24 -1
  38. package/src/council/findings.js +236 -9
  39. package/src/council/parse-stage2.js +10 -2
  40. package/src/council/report.js +19 -8
  41. package/src/council/run-assemble.js +42 -1
  42. package/src/council/run-budget.js +64 -11
  43. package/src/council/run-chair.js +4 -1
  44. package/src/council/run-debate.js +4 -2
  45. package/src/council/run-finalize.js +102 -0
  46. package/src/council/run-launch.js +29 -1
  47. package/src/council/run-server.js +248 -0
  48. package/src/council/run-stage2.js +118 -0
  49. package/src/council/run-stages.js +132 -111
  50. package/src/council/run-state.js +23 -1
  51. package/src/council/run.js +44 -46
  52. package/src/council/tally.js +10 -0
  53. package/src/headless.js +175 -6
  54. package/src/observe/council-legs.js +60 -3
  55. package/src/observe/live-doc.js +18 -1
  56. package/src/observe/watch-render.js +4 -1
  57. package/src/sidecar/child-sessions.js +1 -2
  58. package/src/sidecar/fanout-leg-fallback.js +69 -21
  59. package/src/sidecar/fanout-leg.js +6 -0
  60. package/src/sidecar/fanout-signals.js +61 -0
  61. package/src/sidecar/fanout-wave-io.js +75 -0
  62. package/src/sidecar/fanout.js +61 -70
  63. package/src/sidecar/progress-fields.js +26 -4
  64. package/src/sidecar/progress.js +8 -1
  65. package/src/sidecar/session-utils.js +23 -14
  66. package/src/spend-query.js +17 -5
  67. package/src/utils/lifecycle.js +37 -1
  68. package/src/utils/path-fence.js +39 -1
  69. package/src/utils/pricing.js +26 -10
  70. package/src/utils/server-setup.js +79 -1
  71. package/src/utils/spend-ledger.js +24 -3
  72. package/src/workspace/artifact-guard.js +22 -1
  73. package/src/workspace/fold-format.js +33 -4
  74. package/src/workspace/live-normalize.js +28 -15
  75. package/src/workspace/run-detail.js +7 -1
@@ -0,0 +1,92 @@
1
+ # Documentation System
2
+
3
+ Canonical reference for the auto-documentation system that keeps CLAUDE.md in sync with the codebase.
4
+
5
+ ## Overview
6
+
7
+ CLAUDE.md uses **progressive disclosure**: a slim main file (~400 lines) with auto-generated sections and pointers to deeper topic docs. This replaces a monolithic 875-line file that was too large for effective agent context.
8
+
9
+ Inspired by [OpenAI's Harness Engineering](https://openai.com/index/harness-engineering/) approach: "give Codex a map, not a 1,000-page instruction manual."
10
+
11
+ ## Auto-Generated Sections
12
+
13
+ Sections between `<!-- AUTO:name -->` markers in CLAUDE.md are maintained by `scripts/generate-docs.js`. Do NOT edit these by hand.
14
+
15
+ ### Marker Format
16
+
17
+ ```markdown
18
+ <!-- AUTO:tree -->
19
+ (generated content here)
20
+ <!-- /AUTO:tree -->
21
+ ```
22
+
23
+ ### Current Markers
24
+
25
+ | Marker | Content | Source |
26
+ |--------|---------|--------|
27
+ | `tree` | ASCII directory tree with JSDoc annotations | Filesystem scan of `bin/`, `src/`, `electron/`, `scripts/`, `evals/` (note: `tests/` is NOT included) |
28
+ | `modules` | Markdown table of all `src/**/*.js` modules | JSDoc description + `module.exports` extraction |
29
+
30
+ ### How It Works
31
+
32
+ 1. `scripts/generate-docs.js` scans the codebase
33
+ 2. For each marker, it generates new content from the source of truth (filesystem, JSDoc)
34
+ 3. It replaces the content between the open/close marker tags
35
+ 4. If CLAUDE.md changed, it auto-stages the file with `git add`
36
+
37
+ ### Adding a New Auto-Generated Section
38
+
39
+ 1. Add a new marker pair to CLAUDE.md (short lowercase name, no hyphens required but keep it terse):
40
+ ```markdown
41
+ <!-- AUTO:my-section -->
42
+ <!-- /AUTO:my-section -->
43
+ ```
44
+ 2. Add a generator function in `scripts/generate-docs.js` (or `scripts/generate-docs-helpers.js`)
45
+ 3. Add it to the `generated` map in `main()`
46
+ 4. Add tests in `tests/scripts/generate-docs.test.js`
47
+
48
+ ## Cross-Link Validation
49
+
50
+ When running `--check` mode, the script validates every markdown link in CLAUDE.md:
51
+
52
+ - `[text](path)` links are resolved relative to the project root
53
+ - External URLs (`https://...`) are skipped
54
+ - Anchor-only links (`#section`) are skipped
55
+ - Broken links cause `--check` to exit 1
56
+
57
+ ## Plans Index
58
+
59
+ `docs/plans/index.md` is auto-generated by the same script. It lists all `.md` files in:
60
+ - `docs/plans/` (active plans)
61
+ - `docs/archive/plans/` (archived plans)
62
+
63
+ Each entry shows the filename, first heading, and date extracted from the filename.
64
+
65
+ ## Commands
66
+
67
+ ```bash
68
+ node scripts/generate-docs.js # Regenerate all auto sections + plans index
69
+ node scripts/generate-docs.js --check # Verify everything is current (CI mode)
70
+ npm run generate-docs # Alias for write mode
71
+ npm run generate-docs:check # Alias for check mode
72
+ ```
73
+
74
+ ## Pre-Commit Integration
75
+
76
+ The pre-commit hook runs `generate-docs.js` in write mode automatically. If CLAUDE.md changes, it stages the update. Developers never need to run the script manually.
77
+
78
+ Hook order:
79
+ 1. lint-staged
80
+ 2. check-secrets
81
+ 3. check-file-sizes
82
+ 4. **generate-docs.js** (auto-stages CLAUDE.md if changed)
83
+ 5. validate-docs.js (warns about manual drift)
84
+
85
+ ## Troubleshooting
86
+
87
+ | Problem | Solution |
88
+ |---------|---------|
89
+ | "Marker not found" error | Ensure `<!-- AUTO:name -->` and `<!-- /AUTO:name -->` exist in CLAUDE.md |
90
+ | Stale markers after code change | Run `node scripts/generate-docs.js` manually |
91
+ | Cross-link validation failure | Fix the broken link in CLAUDE.md or create the missing file |
92
+ | Plans index missing | Run the script; it creates `docs/plans/index.md` |
@@ -0,0 +1,471 @@
1
+ # Electron UI Testing (Chrome DevTools Protocol)
2
+
3
+ The Electron Amicus window runs with remote debugging enabled via the Chrome DevTools Protocol. This allows programmatic inspection and testing of the UI state.
4
+
5
+ ## Prerequisites
6
+
7
+ ### Debug Port Configuration
8
+
9
+ The default debug port is 9222, but **Chrome browser also uses port 9222**. If Chrome is running, Electron will silently fail to bind. Use `AMICUS_DEBUG_PORT` to set a different port:
10
+
11
+ ```bash
12
+ # Use port 9223 to avoid conflicts with Chrome
13
+ AMICUS_DEBUG_PORT=9223 amicus start --model gemini --prompt "test"
14
+ ```
15
+
16
+ Verify it's accessible:
17
+
18
+ ```bash
19
+ # Use the same port you configured (default: 9222, recommended: 9223)
20
+ curl -s http://127.0.0.1:9223/json | python3 -m json.tool
21
+ ```
22
+
23
+ ### Known Limitations
24
+
25
+ - **`contextBridge` does not work with `data:` URLs** — The toolbar is loaded via a `data:` URL in the main window. Electron's `contextBridge.exposeInMainWorld()` silently fails for `data:` origins, so `window.sidecar` is `undefined` in the toolbar. Any toolbar↔main-process communication must use `executeJavaScript()` polling instead of IPC.
26
+ - **Debug targets by URL scheme** — sidecar mode creates two pages (OpenCode content at `http://localhost:<port>`, toolbar at `data:text/html`); the Council Workspace mode (`AMICUS_MODE=council-workspace`, v4.4) creates ONE page at a `file://` URL (`electron/workspace-ui/index.html`, loaded via `loadFile`). Filter by URL prefix: `CdpClient.toolbar()` → `data:`, `CdpClient.content()` → `http://localhost`, `CdpClient.workspace()` → `file://`. The workspace e2e suite runs on port 9225 (9223 is the manual/docs port used throughout this page; 9224 belongs to the toolbar suite). Unlike the `data:`-URL toolbar, the workspace page has a **working** `contextBridge` (`window.amicusWorkspace`) because `loadFile` is a real `file://` origin — see [docs/council.md's Council Workspace section](./council.md#council-workspace-gui) for what that bridge exposes.
27
+
28
+ ## Testing UI State with Node.js
29
+
30
+ Use the WebSocket API to execute JavaScript in the Electron renderer and inspect UI state:
31
+
32
+ ```javascript
33
+ // test-electron-ui.js
34
+ const WebSocket = require('ws');
35
+
36
+ const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/<PAGE_ID>');
37
+
38
+ ws.on('open', () => {
39
+ ws.send(JSON.stringify({
40
+ id: 1,
41
+ method: 'Runtime.evaluate',
42
+ params: {
43
+ expression: `
44
+ (function() {
45
+ const messages = document.querySelectorAll('.message');
46
+ const toolCalls = document.querySelectorAll('.tool-call');
47
+ return {
48
+ sessionId: window.sessionId,
49
+ messagesCount: messages.length,
50
+ toolCallsCount: toolCalls.length,
51
+ messages: Array.from(messages).map(m => ({
52
+ class: m.className,
53
+ text: m.textContent.slice(0, 200)
54
+ }))
55
+ };
56
+ })()
57
+ `,
58
+ returnByValue: true
59
+ }
60
+ }));
61
+ });
62
+
63
+ ws.on('message', (data) => {
64
+ const response = JSON.parse(data);
65
+ if (response.id === 1) {
66
+ console.log(JSON.stringify(response.result?.result?.value, null, 2));
67
+ ws.close();
68
+ }
69
+ });
70
+ ```
71
+
72
+ ## Common UI Test Queries
73
+
74
+ **Get page ID first:**
75
+ ```bash
76
+ curl -s http://127.0.0.1:9223/json | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])"
77
+ ```
78
+
79
+ **Check UI state (inline):**
80
+ ```bash
81
+ node << 'EOF'
82
+ const WebSocket = require('ws');
83
+ const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/<PAGE_ID>');
84
+
85
+ ws.on('open', () => {
86
+ ws.send(JSON.stringify({
87
+ id: 1,
88
+ method: 'Runtime.evaluate',
89
+ params: {
90
+ expression: `({
91
+ hasConfig: !!window.sidecarConfig,
92
+ model: window.sidecarConfig?.model,
93
+ messagesCount: document.querySelectorAll('.message').length,
94
+ toolCallsCount: document.querySelectorAll('.tool-call').length,
95
+ errorMessages: Array.from(document.querySelectorAll('.error-message')).map(e => e.textContent)
96
+ })`,
97
+ returnByValue: true
98
+ }
99
+ }));
100
+ });
101
+
102
+ ws.on('message', (data) => {
103
+ const r = JSON.parse(data);
104
+ if (r.id === 1) { console.log(JSON.stringify(r.result?.result?.value, null, 2)); ws.close(); }
105
+ });
106
+
107
+ setTimeout(() => { ws.close(); process.exit(0); }, 5000);
108
+ EOF
109
+ ```
110
+
111
+ **Get tool call details:**
112
+ ```bash
113
+ node << 'EOF'
114
+ const WebSocket = require('ws');
115
+ const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/<PAGE_ID>');
116
+
117
+ ws.on('open', () => {
118
+ ws.send(JSON.stringify({
119
+ id: 1,
120
+ method: 'Runtime.evaluate',
121
+ params: {
122
+ expression: `
123
+ Array.from(document.querySelectorAll('.tool-call')).map(t => ({
124
+ class: t.className,
125
+ html: t.innerHTML.slice(0, 500)
126
+ }))
127
+ `,
128
+ returnByValue: true
129
+ }
130
+ }));
131
+ });
132
+
133
+ ws.on('message', (data) => {
134
+ const r = JSON.parse(data);
135
+ if (r.id === 1) { console.log(JSON.stringify(r.result?.result?.value, null, 2)); ws.close(); }
136
+ });
137
+
138
+ setTimeout(() => { ws.close(); process.exit(0); }, 5000);
139
+ EOF
140
+ ```
141
+
142
+ ## Expected UI Elements
143
+
144
+ When testing the Amicus UI, verify these elements:
145
+
146
+ | Selector | Description | Expected Content |
147
+ |----------|-------------|------------------|
148
+ | `.message.system` | Task briefing | "Task: {briefing}" |
149
+ | `.message.assistant` | Model response | Response text |
150
+ | `.message.user` | User input | User's message |
151
+ | `.tool-call` | Tool execution | Tool name, input, output |
152
+ | `.tool-call.completed` | Completed tool | Has ✓ status |
153
+ | `.tool-call.running` | Running tool | Has ... status |
154
+ | `.tool-status-panel` | Tool summary | "Tools: X/Y completed" |
155
+ | `.reasoning` | Model reasoning | Collapsible thinking |
156
+ | `.error-message` | Error display | Error text |
157
+
158
+ ## Debugging Tips
159
+
160
+ 1. **Get WebSocket URL**: `curl -s http://127.0.0.1:9223/json | jq '.[0].webSocketDebuggerUrl'`
161
+ 2. **Enable console capture**: Send `{"method": "Console.enable"}` first
162
+ 3. **Screenshot**: Use `Page.captureScreenshot` method
163
+ 4. **Timeout**: Always add a timeout to prevent hanging scripts
164
+
165
+ ## Quick WebSocket Testing Patterns
166
+
167
+ The WebSocket approach via Chrome DevTools Protocol is the most efficient way to test the Amicus UI programmatically. Here are streamlined patterns for common testing scenarios:
168
+
169
+ **1. Get Page ID and Check UI State (one-liner):**
170
+ ```bash
171
+ PAGE_ID=$(curl -s http://127.0.0.1:9223/json | node -e "const d=require('fs').readFileSync(0,'utf8');const p=JSON.parse(d);console.log(p[0]?.id || 'NO_ID')")
172
+ echo "Page ID: $PAGE_ID"
173
+ ```
174
+
175
+ **2. Inspect UI State:**
176
+ ```bash
177
+ node -e "
178
+ const WebSocket = require('ws');
179
+ const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/$PAGE_ID');
180
+
181
+ ws.on('open', () => {
182
+ ws.send(JSON.stringify({
183
+ id: 1,
184
+ method: 'Runtime.evaluate',
185
+ params: {
186
+ expression: \`
187
+ (function() {
188
+ const messages = document.querySelectorAll('.message');
189
+ return {
190
+ sseSubscribed: typeof sseSubscribed !== 'undefined' ? sseSubscribed : false,
191
+ messagesCount: messages.length,
192
+ messages: Array.from(messages).map(m => ({
193
+ class: m.className,
194
+ text: (m.textContent || '').slice(0, 200)
195
+ }))
196
+ };
197
+ })()
198
+ \`,
199
+ returnByValue: true
200
+ }
201
+ }));
202
+ });
203
+
204
+ ws.on('message', (data) => {
205
+ const msg = JSON.parse(data.toString());
206
+ if (msg.id === 1) {
207
+ console.log(JSON.stringify(msg.result?.result?.value, null, 2));
208
+ ws.close();
209
+ process.exit(0);
210
+ }
211
+ });
212
+
213
+ setTimeout(() => { ws.close(); process.exit(0); }, 3000);
214
+ "
215
+ ```
216
+
217
+ **3. Send a Message via UI:**
218
+ ```bash
219
+ node -e "
220
+ const WebSocket = require('ws');
221
+ const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/$PAGE_ID');
222
+
223
+ ws.on('open', () => {
224
+ ws.send(JSON.stringify({
225
+ id: 1,
226
+ method: 'Runtime.evaluate',
227
+ params: {
228
+ expression: \`
229
+ (function() {
230
+ const input = document.getElementById('message-input');
231
+ input.value = 'What is 2+2? Just give me the number.';
232
+ input.dispatchEvent(new Event('input'));
233
+ document.getElementById('send-btn').click();
234
+ return 'Message sent';
235
+ })()
236
+ \`,
237
+ returnByValue: true
238
+ }
239
+ }));
240
+ });
241
+
242
+ ws.on('message', (data) => {
243
+ const msg = JSON.parse(data.toString());
244
+ if (msg.id === 1) {
245
+ console.log(msg.result?.result?.value);
246
+ ws.close();
247
+ process.exit(0);
248
+ }
249
+ });
250
+
251
+ setTimeout(() => { ws.close(); process.exit(0); }, 3000);
252
+ "
253
+ ```
254
+
255
+ **4. Check for Errors:**
256
+ ```bash
257
+ node -e "
258
+ const WebSocket = require('ws');
259
+ const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/$PAGE_ID');
260
+
261
+ ws.on('open', () => {
262
+ ws.send(JSON.stringify({
263
+ id: 1,
264
+ method: 'Runtime.evaluate',
265
+ params: {
266
+ expression: \`({
267
+ lastError: document.querySelector('.error-message')?.textContent,
268
+ sessionId: typeof sessionId !== 'undefined' ? sessionId : 'undefined',
269
+ isWaiting: typeof isWaitingForResponse !== 'undefined' ? isWaitingForResponse : 'undefined'
270
+ })\`,
271
+ returnByValue: true
272
+ }
273
+ }));
274
+ });
275
+
276
+ ws.on('message', (data) => {
277
+ const msg = JSON.parse(data.toString());
278
+ if (msg.id === 1) {
279
+ console.log(JSON.stringify(msg.result?.result?.value, null, 2));
280
+ ws.close();
281
+ process.exit(0);
282
+ }
283
+ });
284
+
285
+ setTimeout(() => { ws.close(); process.exit(0); }, 3000);
286
+ "
287
+ ```
288
+
289
+ **Why WebSocket Testing is Efficient:**
290
+ - **No file creation**: Tests run inline without creating temporary files
291
+ - **Direct DOM access**: Query and manipulate any UI element
292
+ - **Real-time state**: Access JavaScript variables like `sessionId`, `sseSubscribed`, `isWaitingForResponse`
293
+ - **Click simulation**: Trigger button clicks and input events programmatically
294
+ - **Fast iteration**: Quickly test changes without restarting the app
295
+
296
+ **Important Notes:**
297
+ - Run commands from the amicus directory to access the `ws` module
298
+ - Page ID changes on each Electron launch - always fetch dynamically
299
+ - Add timeouts to prevent hanging on WebSocket errors
300
+ - Use `data.toString()` when parsing WebSocket messages in newer Node.js versions
301
+
302
+ ## Integration with CI
303
+
304
+ For automated testing, launch Amicus with a known task and verify UI state:
305
+
306
+ ```bash
307
+ # Launch Amicus in background
308
+ node bin/amicus.js start --model "openrouter/google/gemini-2.5-pro" \
309
+ --briefing "Echo hello" &
310
+ AMICUS_PID=$!
311
+
312
+ # Wait for window to open
313
+ sleep 5
314
+
315
+ # Get page ID and test UI
316
+ PAGE_ID=$(curl -s http://127.0.0.1:9223/json | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])")
317
+
318
+ # Run UI verification script
319
+ node scripts/verify-ui-state.js "$PAGE_ID"
320
+
321
+ # Cleanup
322
+ kill $AMICUS_PID
323
+ ```
324
+
325
+ ## Visual UI Testing with Screenshots
326
+
327
+ ### macOS (native tools)
328
+
329
+ **Launch and position Electron window:**
330
+ ```bash
331
+ # Start Amicus in background
332
+ node bin/amicus.js start --model "openrouter/google/gemini-3-flash-preview" --briefing "Test task" &
333
+ sleep 8
334
+
335
+ # Bring window to front and position it (window may open off-screen)
336
+ # Note: osascript / AppleScript is macOS-only
337
+ osascript << 'EOF'
338
+ tell application "System Events"
339
+ tell process "Electron"
340
+ set frontmost to true
341
+ set position of window 1 to {100, 100}
342
+ end tell
343
+ end tell
344
+ EOF
345
+ ```
346
+
347
+ **Take screenshot (macOS — `screencapture` is macOS-only):**
348
+ ```bash
349
+ screencapture -x /tmp/amicus-screenshot.png
350
+ ```
351
+
352
+ ### Windows (CDP-based, cross-platform)
353
+
354
+ `screencapture` and AppleScript are not available on Windows. Use CDP `Page.captureScreenshot` instead (works on all platforms):
355
+
356
+ ```javascript
357
+ const { CdpClient } = require('./tests/helpers/cdp-client');
358
+ const cdp = await CdpClient.toolbar(9223);
359
+ await cdp.screenshot('C:\\tmp\\amicus-screenshot.png');
360
+ cdp.close();
361
+ ```
362
+
363
+ To verify window visibility on Windows without a screenshot:
364
+ ```powershell
365
+ Get-Process electron | Select-Object MainWindowTitle, MainWindowHandle
366
+ ```
367
+
368
+ **Dynamic page ID retrieval (required - ID changes each session):**
369
+ ```bash
370
+ PAGE_ID=$(curl -s http://127.0.0.1:9223/json | node -e "const d=require('fs').readFileSync(0,'utf8');console.log(JSON.parse(d)[0].id)")
371
+ ```
372
+
373
+ **Click UI elements and inspect state (run from amicus directory for `ws` module):**
374
+ ```bash
375
+ cd /path/to/amicus
376
+ cat << EOF > test-ui.js
377
+ const WebSocket = require('ws');
378
+ const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/${PAGE_ID}');
379
+
380
+ ws.on('open', () => {
381
+ ws.send(JSON.stringify({
382
+ id: 1,
383
+ method: 'Runtime.evaluate',
384
+ params: {
385
+ expression: \`
386
+ (function() {
387
+ // Click model selector
388
+ document.getElementById('model-selector-display')?.click();
389
+
390
+ // Or force dropdown visible
391
+ document.getElementById('model-selector-dropdown')?.classList.add('visible');
392
+
393
+ // Return state
394
+ return Array.from(document.querySelectorAll('.model-option'))
395
+ .map(opt => ({
396
+ name: opt.querySelector('.model-name-display')?.textContent,
397
+ selected: opt.classList.contains('selected')
398
+ }));
399
+ })()
400
+ \`,
401
+ returnByValue: true
402
+ }
403
+ }));
404
+ });
405
+
406
+ ws.on('message', (data) => {
407
+ const msg = JSON.parse(data);
408
+ if (msg.id === 1) {
409
+ console.log(JSON.stringify(msg.result?.result?.value, null, 2));
410
+ ws.close();
411
+ process.exit(0);
412
+ }
413
+ });
414
+
415
+ setTimeout(() => { ws.close(); process.exit(0); }, 3000);
416
+ EOF
417
+ node test-ui.js
418
+ ```
419
+
420
+ **Common gotchas:**
421
+ - Window may open off-screen (negative Y coordinate) - use AppleScript to reposition
422
+ - Page ID changes on each Electron launch - always fetch dynamically
423
+ - Run Node.js scripts from amicus directory to access `ws` module
424
+ - Add `setTimeout` to prevent hanging on WebSocket errors
425
+ - **Always use `AMICUS_DEBUG_PORT=9223`** when Chrome is running (Chrome claims 9222)
426
+
427
+ ## Toolbar-Specific Testing
428
+
429
+ The toolbar is a `data:text/html` page — a separate debug target from the OpenCode content view.
430
+
431
+ **Find the toolbar page ID:**
432
+ ```bash
433
+ TOOLBAR_ID=$(curl -s http://127.0.0.1:9223/json | node -e "
434
+ const d=require('fs').readFileSync(0,'utf8');
435
+ const pages=JSON.parse(d);
436
+ const toolbar = pages.find(p => p.url && p.url.startsWith('data:'));
437
+ console.log(toolbar ? toolbar.id : 'NOT_FOUND');
438
+ ")
439
+ echo "Toolbar ID: $TOOLBAR_ID"
440
+ ```
441
+
442
+ **Inspect toolbar state (update banner, buttons, timer):**
443
+ ```bash
444
+ cd /path/to/amicus
445
+ node -e "
446
+ const WebSocket = require('ws');
447
+ const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/$TOOLBAR_ID');
448
+ ws.on('open', () => {
449
+ ws.send(JSON.stringify({
450
+ id: 1,
451
+ method: 'Runtime.evaluate',
452
+ params: {
453
+ expression: \`({
454
+ bannerVisible: document.getElementById('update-banner')?.style?.display === 'flex',
455
+ bannerText: document.getElementById('update-text')?.textContent,
456
+ timerText: document.getElementById('timer')?.textContent,
457
+ foldBtnText: document.getElementById('fold-btn')?.textContent
458
+ })\`,
459
+ returnByValue: true
460
+ }
461
+ }));
462
+ });
463
+ ws.on('message', (data) => {
464
+ const msg = JSON.parse(data.toString());
465
+ if (msg.id === 1) { console.log(JSON.stringify(msg.result?.result?.value, null, 2)); ws.close(); process.exit(0); }
466
+ });
467
+ setTimeout(() => { ws.close(); process.exit(0); }, 3000);
468
+ "
469
+ ```
470
+
471
+ **Note:** `window.sidecar` is `undefined` in the toolbar (see Known Limitations above). The toolbar communicates with the main process via `window.__amicusUpdateAction` polling, not IPC.
@@ -0,0 +1,75 @@
1
+ # JSDoc + TypeScript Declarations
2
+
3
+ This project uses **JSDoc comments** to provide TypeScript type information without converting to TypeScript. This gives npm consumers autocomplete and type checking.
4
+
5
+ ## JSDoc Pattern for Public APIs
6
+
7
+ ```javascript
8
+ /**
9
+ * Start a new Amicus session
10
+ * @param {Object} options - Amicus configuration
11
+ * @param {string} options.model - LLM model identifier (e.g., 'google/gemini-2.5-flash')
12
+ * @param {string} options.briefing - Task description for the Amicus session
13
+ * @param {string} [options.sessionId] - Optional Claude Code session ID
14
+ * @param {boolean} [options.headless=false] - Run without GUI
15
+ * @param {number} [options.timeout=15] - Headless timeout in minutes
16
+ * @returns {Promise<AmicusResult>} Session result with summary
17
+ */
18
+ async function startAmicus(options) {
19
+ // ...
20
+ }
21
+
22
+ /**
23
+ * @typedef {Object} AmicusResult
24
+ * @property {string} taskId - Unique session identifier
25
+ * @property {string} summary - Fold summary from Amicus
26
+ * @property {string} status - Session status (completed|timeout|error)
27
+ * @property {string[]} [conflicts] - Files with potential conflicts
28
+ */
29
+ ```
30
+
31
+ ## Generating .d.ts Files
32
+
33
+ Add to `package.json`:
34
+
35
+ ```json
36
+ {
37
+ "scripts": {
38
+ "build:types": "tsc --declaration --emitDeclarationOnly --allowJs --outDir types"
39
+ },
40
+ "types": "types/index.d.ts",
41
+ "files": ["bin/", "src/", "electron/", "types/"]
42
+ }
43
+ ```
44
+
45
+ Create `jsconfig.json`:
46
+
47
+ ```json
48
+ {
49
+ "compilerOptions": {
50
+ "checkJs": true,
51
+ "declaration": true,
52
+ "emitDeclarationOnly": true,
53
+ "allowJs": true,
54
+ "outDir": "types",
55
+ "lib": ["ES2022"],
56
+ "module": "CommonJS",
57
+ "target": "ES2022"
58
+ },
59
+ "include": ["src/**/*.js", "bin/**/*.js"],
60
+ "exclude": ["node_modules", "tests"]
61
+ }
62
+ ```
63
+
64
+ ## Pre-publish Workflow
65
+
66
+ ```bash
67
+ # Generate types before publishing
68
+ npm run build:types
69
+
70
+ # Verify types are generated
71
+ ls types/
72
+
73
+ # Publish with types
74
+ npm publish
75
+ ```