amicus 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +477 -0
  4. package/bin/amicus.js +382 -0
  5. package/electron/assets/icon.png +0 -0
  6. package/electron/assets/icon.svg +5 -0
  7. package/electron/fold.js +163 -0
  8. package/electron/ipc-setup.js +176 -0
  9. package/electron/load-failsafe.js +85 -0
  10. package/electron/main.js +468 -0
  11. package/electron/preload-setup.js +38 -0
  12. package/electron/preload.js +33 -0
  13. package/electron/setup-ui-alias-script.js +218 -0
  14. package/electron/setup-ui-aliases.js +85 -0
  15. package/electron/setup-ui-keys-script.js +115 -0
  16. package/electron/setup-ui-keys.js +97 -0
  17. package/electron/setup-ui-model.js +138 -0
  18. package/electron/setup-ui-styles.js +327 -0
  19. package/electron/setup-ui.js +465 -0
  20. package/electron/summary.js +118 -0
  21. package/electron/toolbar.js +229 -0
  22. package/electron/window-position.js +35 -0
  23. package/package.json +98 -0
  24. package/scripts/postinstall.js +193 -0
  25. package/scripts/setup-hooks.js +42 -0
  26. package/skill/SKILL.md +976 -0
  27. package/skills/second-opinion/COUNCIL-DESIGN.md +227 -0
  28. package/skills/second-opinion/MODEL-NOTES.md +104 -0
  29. package/skills/second-opinion/SKILL.md +389 -0
  30. package/src/cli-handlers.js +188 -0
  31. package/src/cli.js +400 -0
  32. package/src/conflict.js +144 -0
  33. package/src/context-compression.js +102 -0
  34. package/src/context.js +199 -0
  35. package/src/drift.js +144 -0
  36. package/src/environment.js +157 -0
  37. package/src/headless.js +742 -0
  38. package/src/index.js +106 -0
  39. package/src/jsonl-parser.js +180 -0
  40. package/src/mcp-server.js +625 -0
  41. package/src/mcp-tools.js +407 -0
  42. package/src/opencode-client.js +615 -0
  43. package/src/prompt-builder.js +355 -0
  44. package/src/prompts/cowork-agent-prompt.js +118 -0
  45. package/src/session-manager.js +414 -0
  46. package/src/session.js +180 -0
  47. package/src/sidecar/context-builder.js +297 -0
  48. package/src/sidecar/continue.js +212 -0
  49. package/src/sidecar/crash-handler.js +56 -0
  50. package/src/sidecar/fanout-leg.js +107 -0
  51. package/src/sidecar/fanout-output.js +46 -0
  52. package/src/sidecar/fanout.js +236 -0
  53. package/src/sidecar/interactive.js +217 -0
  54. package/src/sidecar/models.js +135 -0
  55. package/src/sidecar/progress.js +218 -0
  56. package/src/sidecar/read.js +183 -0
  57. package/src/sidecar/resume.js +221 -0
  58. package/src/sidecar/session-utils.js +288 -0
  59. package/src/sidecar/setup-window.js +79 -0
  60. package/src/sidecar/setup.js +280 -0
  61. package/src/sidecar/start.js +251 -0
  62. package/src/utils/agent-mapping.js +138 -0
  63. package/src/utils/alias-audit.js +98 -0
  64. package/src/utils/alias-resolver.js +77 -0
  65. package/src/utils/api-key-store.js +259 -0
  66. package/src/utils/api-key-validation.js +97 -0
  67. package/src/utils/auth-json.js +109 -0
  68. package/src/utils/config.js +291 -0
  69. package/src/utils/curated-models.js +82 -0
  70. package/src/utils/env-compat.js +38 -0
  71. package/src/utils/env-loader.js +54 -0
  72. package/src/utils/idle-watchdog.js +225 -0
  73. package/src/utils/input-validators.js +127 -0
  74. package/src/utils/lifecycle.js +43 -0
  75. package/src/utils/logger.js +84 -0
  76. package/src/utils/mcp-discovery.js +194 -0
  77. package/src/utils/mcp-validators.js +78 -0
  78. package/src/utils/model-catalog.js +103 -0
  79. package/src/utils/model-fetcher.js +179 -0
  80. package/src/utils/model-validator.js +207 -0
  81. package/src/utils/path-setup.js +41 -0
  82. package/src/utils/port-pid.js +39 -0
  83. package/src/utils/prompt-source.js +53 -0
  84. package/src/utils/result-schema.js +261 -0
  85. package/src/utils/server-setup.js +93 -0
  86. package/src/utils/session-abort.js +53 -0
  87. package/src/utils/session-lock.js +95 -0
  88. package/src/utils/shared-server.js +216 -0
  89. package/src/utils/start-helpers.js +76 -0
  90. package/src/utils/thinking-validators.js +92 -0
  91. package/src/utils/update-notifier-loader.js +18 -0
  92. package/src/utils/updater.js +157 -0
  93. package/src/utils/validators.js +300 -0
package/bin/amicus.js ADDED
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Amicus CLI Entry Point
5
+ *
6
+ * Spec Reference: §4 CLI Interface
7
+ * Routes commands to appropriate handlers.
8
+ */
9
+
10
+ // Load API keys from all sources: process.env > sidecar .env > auth.json
11
+ const { loadCredentials } = require('../src/utils/env-loader');
12
+ loadCredentials();
13
+
14
+ const { parseArgs, validateStartArgs, getUsage } = require('../src/cli');
15
+ const { validateTaskId } = require('../src/utils/validators');
16
+ const { resolveModelFromArgs, validateFallbackModel } = require('../src/utils/start-helpers');
17
+ const { handleSetup, handleAbort, handleUpdate, handleMcp } = require('../src/cli-handlers');
18
+ const { isOneShotCommand, armExitWatchdog } = require('../src/utils/lifecycle');
19
+ const { logger } = require('../src/utils/logger');
20
+
21
+ const VERSION = require('../package.json').version;
22
+
23
+ async function main() {
24
+ const args = parseArgs(process.argv.slice(2));
25
+ const command = args._[0];
26
+
27
+ // Install crash handler for MCP-spawned processes (have --task-id)
28
+ if (args['task-id'] && (command === 'start' || command === 'continue')) {
29
+ const { installCrashHandler } = require('../src/sidecar/crash-handler');
30
+ const project = args.cwd || process.cwd();
31
+ const handler = installCrashHandler(args['task-id'], project);
32
+ process.on('uncaughtException', (err) => {
33
+ handler(err);
34
+ process.exit(1);
35
+ });
36
+ process.on('unhandledRejection', (reason) => {
37
+ handler(reason instanceof Error ? reason : new Error(String(reason)));
38
+ process.exit(1);
39
+ });
40
+ }
41
+
42
+ // Non-interactive update check (skip for mcp, --version, --help)
43
+ if (command !== 'mcp' && !args.version && !args.help) {
44
+ const { initUpdateCheck, getUpdateInfo, notifyUpdate } = require('../src/utils/updater');
45
+ await initUpdateCheck();
46
+ // Pass update info to Electron child process via env var,
47
+ // because update-notifier deletes the cache entry after reading it.
48
+ const cliUpdateInfo = getUpdateInfo();
49
+ if (cliUpdateInfo) {
50
+ process.env.AMICUS_UPDATE_INFO = JSON.stringify(cliUpdateInfo);
51
+ process.on('exit', () => {
52
+ process.stderr.write(
53
+ `\n Update available: v${cliUpdateInfo.current} → v${cliUpdateInfo.latest}\n` +
54
+ ' Run `npm install -g amicus` to upgrade.\n\n'
55
+ );
56
+ });
57
+ }
58
+ }
59
+
60
+ // Handle --version
61
+ if (args.version) {
62
+ console.log(`amicus v${VERSION}`);
63
+ process.exit(0);
64
+ }
65
+
66
+ // Handle --help or no command
67
+ if (args.help || args._.length === 0) {
68
+ console.log(getUsage());
69
+ process.exit(0);
70
+ }
71
+
72
+ let exitCode = 0;
73
+ try {
74
+ switch (command) {
75
+ case 'start':
76
+ await handleStart(args);
77
+ break;
78
+ case 'fanout':
79
+ exitCode = await handleFanout(args);
80
+ break;
81
+ case 'list':
82
+ await handleList(args);
83
+ break;
84
+ case 'resume':
85
+ await handleResume(args);
86
+ break;
87
+ case 'continue':
88
+ await handleContinue(args);
89
+ break;
90
+ case 'read':
91
+ await handleRead(args);
92
+ break;
93
+ case 'models': {
94
+ const { handleModels } = require('../src/sidecar/models');
95
+ exitCode = await handleModels(args);
96
+ break;
97
+ }
98
+ case 'setup':
99
+ await handleSetup(args);
100
+ break;
101
+ case 'abort':
102
+ await handleAbort(args);
103
+ break;
104
+ case 'mcp':
105
+ await handleMcp();
106
+ break;
107
+ case 'update':
108
+ await handleUpdate();
109
+ break;
110
+ default:
111
+ console.error(`Unknown command: ${command}`);
112
+ console.log(getUsage());
113
+ process.exit(1);
114
+ }
115
+ } catch (err) {
116
+ console.error(`Error: ${err.message}`);
117
+ process.exit(1);
118
+ }
119
+
120
+ // F3 #15: one-shot commands must not hang on a lingering handle. The work is
121
+ // done here; give natural drain a brief grace, then force-exit as a net.
122
+ // (mcp is long-lived and never reaches this point.)
123
+ if (exitCode) { process.exitCode = exitCode; }
124
+ if (isOneShotCommand(command)) {
125
+ armExitWatchdog(exitCode, 1500, { log: (m, meta) => logger.debug(m, meta) });
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Handle 'sidecar start' command
131
+ * Spec Reference: §4.1
132
+ */
133
+ async function handleStart(args) {
134
+ // F4: --prompt-file support (XOR --prompt) and --json gating
135
+ if (args.prompt !== undefined || args['prompt-file'] !== undefined) {
136
+ const { resolvePromptSource } = require('../src/utils/prompt-source');
137
+ const promptRes = resolvePromptSource(args);
138
+ if (promptRes.error) {
139
+ console.error(promptRes.error);
140
+ process.exit(1);
141
+ }
142
+ args.prompt = promptRes.prompt;
143
+ }
144
+ if (args.json && !args['no-ui']) {
145
+ console.error('Error: --json requires --no-ui');
146
+ process.exit(1);
147
+ }
148
+
149
+ const { model, alias } = resolveModelFromArgs(args);
150
+ args.model = model;
151
+ args.model = await validateFallbackModel(args, alias);
152
+
153
+ // Normalize agent: --agent takes precedence, otherwise use --mode
154
+ args.agent = args.agent || args.mode;
155
+
156
+ const validation = validateStartArgs(args);
157
+ if (!validation.valid) {
158
+ console.error(validation.error);
159
+ process.exit(1);
160
+ }
161
+
162
+ const { startSidecar } = require('../src/index');
163
+
164
+ await startSidecar({
165
+ taskId: args['task-id'],
166
+ model: args.model,
167
+ prompt: args.prompt,
168
+ sessionId: args['session-id'],
169
+ cwd: args.cwd,
170
+ contextTurns: args['context-turns'],
171
+ contextSince: args['context-since'],
172
+ contextMaxTokens: args['context-max-tokens'],
173
+ noUi: args['no-ui'],
174
+ timeout: args.timeout,
175
+ agent: args.agent,
176
+ mcp: args.mcp,
177
+ mcpConfig: args['mcp-config'],
178
+ thinking: args.thinking,
179
+ summaryLength: args['summary-length'],
180
+ client: args.client,
181
+ sessionDir: args['session-dir'],
182
+ foldShortcut: args['fold-shortcut'],
183
+ opencodePort: args['opencode-port'],
184
+ noMcp: args['no-mcp'],
185
+ excludeMcp: args['exclude-mcp'],
186
+ coworkProcess: args['cowork-process'],
187
+ position: args.position,
188
+ json: !!args.json,
189
+ modelInput: alias || null,
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Handle 'amicus fanout' command (F4).
195
+ * Returns the wave exit code: 0 all complete, 2 partial, 1 none/hard failure,
196
+ * 130/143 when the wave was signal-aborted.
197
+ */
198
+ async function handleFanout(args) {
199
+ const { resolvePromptSource } = require('../src/utils/prompt-source');
200
+ const promptRes = resolvePromptSource(args);
201
+ if (promptRes.error) {
202
+ console.error(promptRes.error);
203
+ process.exit(1);
204
+ }
205
+ if (typeof args.models !== 'string' || !args.models.trim()) {
206
+ console.error('Error: --models is required (comma-separated aliases or provider/model IDs)');
207
+ process.exit(1);
208
+ }
209
+ if (args['wave-id']) {
210
+ const check = validateTaskId(String(args['wave-id']));
211
+ if (!check.valid) {
212
+ console.error(check.error);
213
+ process.exit(1);
214
+ }
215
+ }
216
+ if (args.agent && String(args.agent).toLowerCase() === 'chat') {
217
+ console.error('Error: --agent chat is interactive-only; fanout is headless');
218
+ process.exit(1);
219
+ }
220
+ if (args.timeout !== undefined && args.timeout <= 0) {
221
+ console.error('Error: --timeout must be a positive number');
222
+ process.exit(1);
223
+ }
224
+ const { parseModelsList } = require('../src/sidecar/fanout');
225
+ if (parseModelsList(args.models).length === 0) {
226
+ console.error('Error: --models must contain at least one non-empty entry');
227
+ process.exit(1);
228
+ }
229
+
230
+ // Direct require — the src/index.js public re-export is added later (Task 13)
231
+ const { runFanout } = require('../src/sidecar/fanout');
232
+ const { exitCode } = await runFanout({
233
+ models: args.models,
234
+ prompt: promptRes.prompt,
235
+ promptMeta: promptRes.promptMeta,
236
+ waveId: args['wave-id'],
237
+ project: args.cwd || process.cwd(),
238
+ agent: args.agent || args.mode,
239
+ thinking: args.thinking,
240
+ timeout: args.timeout,
241
+ summaryLength: args['summary-length'],
242
+ includeContext: !args['no-context'],
243
+ sessionId: args['session-id'],
244
+ contextTurns: args['context-turns'],
245
+ contextSince: args['context-since'],
246
+ contextMaxTokens: args['context-max-tokens'],
247
+ mcp: args.mcp,
248
+ mcpConfig: args['mcp-config'],
249
+ noMcp: args['no-mcp'],
250
+ excludeMcp: args['exclude-mcp'],
251
+ noValidateModel: args['no-validate-model'],
252
+ json: !!args.json,
253
+ client: args.client,
254
+ });
255
+ return exitCode;
256
+ }
257
+
258
+ /**
259
+ * Handle 'sidecar list' command
260
+ * Spec Reference: §4.2
261
+ */
262
+ async function handleList(args) {
263
+ const { listSidecars } = require('../src/index');
264
+
265
+ await listSidecars({
266
+ status: args.status,
267
+ all: args.all,
268
+ json: args.json,
269
+ project: args.cwd
270
+ });
271
+ }
272
+
273
+ /**
274
+ * Handle 'sidecar resume' command
275
+ * Spec Reference: §4.3
276
+ */
277
+ async function handleResume(args) {
278
+ const taskId = args._[1];
279
+
280
+ if (!taskId) {
281
+ console.error('Error: task_id is required for resume');
282
+ console.error('Usage: sidecar resume <task_id>');
283
+ process.exit(1);
284
+ }
285
+
286
+ const taskIdCheck = validateTaskId(taskId);
287
+ if (!taskIdCheck.valid) {
288
+ console.error(taskIdCheck.error);
289
+ process.exit(1);
290
+ }
291
+
292
+ const { resumeSidecar } = require('../src/index');
293
+
294
+ await resumeSidecar({
295
+ taskId,
296
+ project: args.cwd,
297
+ headless: args['no-ui'],
298
+ timeout: args.timeout
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Handle 'sidecar continue' command
304
+ * Spec Reference: §4.4
305
+ */
306
+ async function handleContinue(args) {
307
+ const taskId = args._[1];
308
+
309
+ if (!taskId) {
310
+ console.error('Error: task_id is required for continue');
311
+ console.error('Usage: sidecar continue <task_id> --prompt "..."');
312
+ process.exit(1);
313
+ }
314
+
315
+ const taskIdCheck = validateTaskId(taskId);
316
+ if (!taskIdCheck.valid) {
317
+ console.error(taskIdCheck.error);
318
+ process.exit(1);
319
+ }
320
+
321
+ if (!args.prompt && !args.briefing) {
322
+ console.error('Error: --prompt is required for continue');
323
+ process.exit(1);
324
+ }
325
+
326
+ // F5: an explicitly passed --model gets the same resolution+validation as start.
327
+ if (args.model !== undefined) {
328
+ const { model, alias } = resolveModelFromArgs(args);
329
+ args.model = model;
330
+ args.model = await validateFallbackModel(args, alias);
331
+ }
332
+
333
+ const { continueSidecar } = require('../src/index');
334
+
335
+ await continueSidecar({
336
+ taskId,
337
+ newTaskId: args['task-id'],
338
+ briefing: args.prompt || args.briefing,
339
+ model: args.model,
340
+ project: args.cwd,
341
+ contextTurns: args['context-turns'],
342
+ contextMaxTokens: args['context-max-tokens'],
343
+ headless: args['no-ui'],
344
+ timeout: args.timeout
345
+ });
346
+ }
347
+
348
+ /**
349
+ * Handle 'sidecar read' command
350
+ * Spec Reference: §4.5
351
+ */
352
+ async function handleRead(args) {
353
+ const taskId = args._[1];
354
+
355
+ if (!taskId) {
356
+ console.error('Error: task_id is required for read');
357
+ console.error('Usage: sidecar read <task_id> [--summary|--conversation]');
358
+ process.exit(1);
359
+ }
360
+
361
+ const taskIdCheck = validateTaskId(taskId);
362
+ if (!taskIdCheck.valid) {
363
+ console.error(taskIdCheck.error);
364
+ process.exit(1);
365
+ }
366
+
367
+ const { readSidecar } = require('../src/index');
368
+
369
+ await readSidecar({
370
+ taskId,
371
+ conversation: args.conversation,
372
+ metadata: args.metadata,
373
+ json: args.json,
374
+ project: args.cwd
375
+ });
376
+ }
377
+
378
+ // Run main
379
+ main().catch(err => {
380
+ console.error(`Fatal error: ${err.message}`);
381
+ process.exit(1);
382
+ });
Binary file
@@ -0,0 +1,5 @@
1
+ <svg width="1024" height="1024" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="16" height="16" rx="3" fill="#2D2B2A"/>
3
+ <path d="M3 2v12" stroke="#D97757" stroke-width="2" stroke-linecap="round"/>
4
+ <path d="M10 2v5c0 2-3 3-7 5" stroke="#D97757" stroke-width="2" stroke-linecap="round" stroke-opacity="0.6"/>
5
+ </svg>
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Fold Logic
3
+ *
4
+ * Handles the fold action: showing overlay, requesting summary,
5
+ * outputting fold data to stdout, and closing the window.
6
+ */
7
+
8
+ const { logger } = require('../src/utils/logger');
9
+ const { requestSummaryFromModel } = require('./summary');
10
+ const { getSummaryTemplate } = require('../src/prompt-builder');
11
+
12
+ /**
13
+ * Create a fold handler bound to the window state
14
+ * @param {object} state - Shared state object
15
+ * @param {string} state.model - Model name
16
+ * @param {string} state.client - Client type
17
+ * @param {string} state.cwd - Working directory
18
+ * @param {string} state.sessionId - OpenCode session ID
19
+ * @param {string} state.taskId - Sidecar task ID
20
+ * @param {number} state.port - OpenCode server port
21
+ * @returns {{ triggerFold: Function, hasFolded: Function }}
22
+ */
23
+ function createFoldHandler(state) {
24
+ let folded = false;
25
+
26
+ async function triggerFold(mainWindow, contentView) {
27
+ if (folded) { return; }
28
+ folded = true;
29
+
30
+ // Show fold progress in toolbar and content overlay
31
+ showFoldOverlay(mainWindow, contentView);
32
+
33
+ try {
34
+ // Ask the model to generate a structured summary
35
+ let summary = '';
36
+ try {
37
+ summary = await requestSummaryFromModel(
38
+ state.sessionId, state.port, getSummaryTemplate
39
+ );
40
+ } catch (err) {
41
+ logger.warn('Failed to get model summary', { error: err.message });
42
+ }
43
+
44
+ const output = [
45
+ '[SIDECAR_FOLD]',
46
+ `Model: ${state.model}`,
47
+ `Session: ${state.sessionId || state.taskId}`,
48
+ `Client: ${state.client}`,
49
+ `CWD: ${state.cwd}`,
50
+ `Mode: interactive`,
51
+ '---',
52
+ summary || 'Session ended without summary.'
53
+ ].join('\n');
54
+
55
+ process.stdout.write(output + '\n');
56
+ logger.info('Fold completed', { taskId: state.taskId });
57
+
58
+ // Show nudge overlay before closing
59
+ if (contentView) {
60
+ await contentView.webContents.executeJavaScript(`
61
+ (function() {
62
+ var overlay = document.getElementById('sidecar-fold-overlay');
63
+ if (overlay) {
64
+ while (overlay.firstChild) { overlay.removeChild(overlay.firstChild); }
65
+ var msg = document.createElement('div');
66
+ msg.style.cssText = 'color:#E8E0D8;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:15px;font-weight:500;text-align:center;max-width:320px;';
67
+ msg.textContent = 'Summary saved. Tell Claude you\\u2019re done with the Amicus session so it can read the results.';
68
+ overlay.appendChild(msg);
69
+ }
70
+ })();
71
+ `).catch(() => {});
72
+ }
73
+
74
+ // Wait 2.5s for user to read the nudge, then close
75
+ await new Promise(resolve => setTimeout(resolve, 2500));
76
+ } catch (err) {
77
+ logger.error('Fold failed', { error: err.message });
78
+ folded = false;
79
+ return;
80
+ }
81
+
82
+ // Close the window after fold
83
+ const { app } = require('electron');
84
+ if (mainWindow) {
85
+ mainWindow.close();
86
+ } else {
87
+ app.quit();
88
+ }
89
+ }
90
+
91
+ function hasFolded() {
92
+ return folded;
93
+ }
94
+
95
+ return { triggerFold, hasFolded };
96
+ }
97
+
98
+ /**
99
+ * Show fold progress overlay in both the toolbar and the content view.
100
+ * Note: The JS strings below contain only hardcoded markup (no user input),
101
+ * so there is no XSS risk from DOM manipulation.
102
+ */
103
+ function showFoldOverlay(mainWindow, contentView) {
104
+ if (mainWindow) {
105
+ mainWindow.webContents.executeJavaScript(`
106
+ (function() {
107
+ var btn = document.getElementById('fold-btn');
108
+ if (btn) {
109
+ btn.textContent = '';
110
+ var span = document.createElement('span');
111
+ span.style.cssText = 'display:inline-flex;align-items:center;gap:6px;';
112
+ var spinner = document.createElement('span');
113
+ spinner.style.cssText = 'width:12px;height:12px;border:2px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:spin 0.8s linear infinite;display:inline-block;';
114
+ span.appendChild(spinner);
115
+ span.appendChild(document.createTextNode('Generating summary\\u2026'));
116
+ btn.appendChild(span);
117
+ btn.disabled = true;
118
+ btn.style.opacity = '0.85';
119
+ btn.style.cursor = 'default';
120
+ }
121
+ if (!document.getElementById('fold-spin-style')) {
122
+ var style = document.createElement('style');
123
+ style.id = 'fold-spin-style';
124
+ style.textContent = '@keyframes spin { to { transform: rotate(360deg); } }';
125
+ document.head.appendChild(style);
126
+ }
127
+ })();
128
+ `).catch(() => {});
129
+ }
130
+ if (contentView) {
131
+ contentView.webContents.executeJavaScript(`
132
+ (function() {
133
+ if (!document.getElementById('fold-spin-style')) {
134
+ var style = document.createElement('style');
135
+ style.id = 'fold-spin-style';
136
+ style.textContent = '@keyframes spin { to { transform: rotate(360deg); } }';
137
+ document.head.appendChild(style);
138
+ }
139
+ var overlay = document.createElement('div');
140
+ overlay.id = 'sidecar-fold-overlay';
141
+ overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.55);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:99999;';
142
+
143
+ var spinDiv = document.createElement('div');
144
+ spinDiv.style.cssText = 'width:32px;height:32px;border:3px solid rgba(217,119,87,0.3);border-top-color:#D97757;border-radius:50%;animation:spin 0.8s linear infinite;margin-bottom:16px;';
145
+ overlay.appendChild(spinDiv);
146
+
147
+ var titleDiv = document.createElement('div');
148
+ titleDiv.style.cssText = 'color:#E8E0D8;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:15px;font-weight:500;';
149
+ titleDiv.textContent = 'Generating summary\\u2026';
150
+ overlay.appendChild(titleDiv);
151
+
152
+ var subtitleDiv = document.createElement('div');
153
+ subtitleDiv.style.cssText = 'color:#7A756F;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:12px;margin-top:6px;';
154
+ subtitleDiv.textContent = 'Folding session back to Claude Code';
155
+ overlay.appendChild(subtitleDiv);
156
+
157
+ document.body.appendChild(overlay);
158
+ })();
159
+ `).catch(() => {});
160
+ }
161
+ }
162
+
163
+ module.exports = { createFoldHandler, showFoldOverlay };