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.
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +477 -0
- package/bin/amicus.js +382 -0
- package/electron/assets/icon.png +0 -0
- package/electron/assets/icon.svg +5 -0
- package/electron/fold.js +163 -0
- package/electron/ipc-setup.js +176 -0
- package/electron/load-failsafe.js +85 -0
- package/electron/main.js +468 -0
- package/electron/preload-setup.js +38 -0
- package/electron/preload.js +33 -0
- package/electron/setup-ui-alias-script.js +218 -0
- package/electron/setup-ui-aliases.js +85 -0
- package/electron/setup-ui-keys-script.js +115 -0
- package/electron/setup-ui-keys.js +97 -0
- package/electron/setup-ui-model.js +138 -0
- package/electron/setup-ui-styles.js +327 -0
- package/electron/setup-ui.js +465 -0
- package/electron/summary.js +118 -0
- package/electron/toolbar.js +229 -0
- package/electron/window-position.js +35 -0
- package/package.json +98 -0
- package/scripts/postinstall.js +193 -0
- package/scripts/setup-hooks.js +42 -0
- package/skill/SKILL.md +976 -0
- package/skills/second-opinion/COUNCIL-DESIGN.md +227 -0
- package/skills/second-opinion/MODEL-NOTES.md +104 -0
- package/skills/second-opinion/SKILL.md +389 -0
- package/src/cli-handlers.js +188 -0
- package/src/cli.js +400 -0
- package/src/conflict.js +144 -0
- package/src/context-compression.js +102 -0
- package/src/context.js +199 -0
- package/src/drift.js +144 -0
- package/src/environment.js +157 -0
- package/src/headless.js +742 -0
- package/src/index.js +106 -0
- package/src/jsonl-parser.js +180 -0
- package/src/mcp-server.js +625 -0
- package/src/mcp-tools.js +407 -0
- package/src/opencode-client.js +615 -0
- package/src/prompt-builder.js +355 -0
- package/src/prompts/cowork-agent-prompt.js +118 -0
- package/src/session-manager.js +414 -0
- package/src/session.js +180 -0
- package/src/sidecar/context-builder.js +297 -0
- package/src/sidecar/continue.js +212 -0
- package/src/sidecar/crash-handler.js +56 -0
- package/src/sidecar/fanout-leg.js +107 -0
- package/src/sidecar/fanout-output.js +46 -0
- package/src/sidecar/fanout.js +236 -0
- package/src/sidecar/interactive.js +217 -0
- package/src/sidecar/models.js +135 -0
- package/src/sidecar/progress.js +218 -0
- package/src/sidecar/read.js +183 -0
- package/src/sidecar/resume.js +221 -0
- package/src/sidecar/session-utils.js +288 -0
- package/src/sidecar/setup-window.js +79 -0
- package/src/sidecar/setup.js +280 -0
- package/src/sidecar/start.js +251 -0
- package/src/utils/agent-mapping.js +138 -0
- package/src/utils/alias-audit.js +98 -0
- package/src/utils/alias-resolver.js +77 -0
- package/src/utils/api-key-store.js +259 -0
- package/src/utils/api-key-validation.js +97 -0
- package/src/utils/auth-json.js +109 -0
- package/src/utils/config.js +291 -0
- package/src/utils/curated-models.js +82 -0
- package/src/utils/env-compat.js +38 -0
- package/src/utils/env-loader.js +54 -0
- package/src/utils/idle-watchdog.js +225 -0
- package/src/utils/input-validators.js +127 -0
- package/src/utils/lifecycle.js +43 -0
- package/src/utils/logger.js +84 -0
- package/src/utils/mcp-discovery.js +194 -0
- package/src/utils/mcp-validators.js +78 -0
- package/src/utils/model-catalog.js +103 -0
- package/src/utils/model-fetcher.js +179 -0
- package/src/utils/model-validator.js +207 -0
- package/src/utils/path-setup.js +41 -0
- package/src/utils/port-pid.js +39 -0
- package/src/utils/prompt-source.js +53 -0
- package/src/utils/result-schema.js +261 -0
- package/src/utils/server-setup.js +93 -0
- package/src/utils/session-abort.js +53 -0
- package/src/utils/session-lock.js +95 -0
- package/src/utils/shared-server.js +216 -0
- package/src/utils/start-helpers.js +76 -0
- package/src/utils/thinking-validators.js +92 -0
- package/src/utils/update-notifier-loader.js +18 -0
- package/src/utils/updater.js +157 -0
- package/src/utils/validators.js +300 -0
package/electron/main.js
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amicus Electron Shell - v3
|
|
3
|
+
*
|
|
4
|
+
* Uses BrowserView to split the window into two physical areas:
|
|
5
|
+
* - Top: OpenCode Web UI (gets its own viewport, no CSS conflicts)
|
|
6
|
+
* - Bottom 40px: Amicus toolbar (branding, task ID, timer, fold button)
|
|
7
|
+
*
|
|
8
|
+
* Supports two modes via AMICUS_MODE env var:
|
|
9
|
+
* - 'sidecar' (default): OpenCode conversation with fold toolbar
|
|
10
|
+
* - 'setup': API key configuration form
|
|
11
|
+
*
|
|
12
|
+
* Spec Reference: §4.4 Electron Wrapper
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { app, BrowserWindow, BrowserView, globalShortcut, ipcMain, screen } = require('electron');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { logger } = require('../src/utils/logger');
|
|
18
|
+
const { getCompatEnv } = require('../src/utils/env-compat');
|
|
19
|
+
const { buildToolbarHTML, TOOLBAR_H, getBrandName } = require('./toolbar');
|
|
20
|
+
const { createFoldHandler } = require('./fold');
|
|
21
|
+
const { registerSetupHandlers } = require('./ipc-setup');
|
|
22
|
+
const { computeWindowPosition } = require('./window-position');
|
|
23
|
+
const { attachLoadFailsafe, buildLoadErrorHTML } = require('./load-failsafe');
|
|
24
|
+
|
|
25
|
+
const ICON_PATH = path.join(__dirname, 'assets', 'icon.png');
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// EPIPE Error Handling
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
process.stdout.on('error', (err) => {
|
|
32
|
+
if (err.code === 'EPIPE') { return; }
|
|
33
|
+
console.error('stdout error:', err);
|
|
34
|
+
});
|
|
35
|
+
process.stderr.on('error', (err) => {
|
|
36
|
+
if (err.code === 'EPIPE') { return; }
|
|
37
|
+
});
|
|
38
|
+
process.on('uncaughtException', (err) => {
|
|
39
|
+
if (err.code === 'EPIPE') { return; }
|
|
40
|
+
console.error('Uncaught exception:', err);
|
|
41
|
+
});
|
|
42
|
+
process.on('unhandledRejection', (reason) => {
|
|
43
|
+
console.error('Unhandled rejection:', reason);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Configuration from Environment (set by src/sidecar/start.js)
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
const MODE = getCompatEnv('MODE') || 'sidecar';
|
|
51
|
+
const TASK_ID = getCompatEnv('TASK_ID') || 'unknown';
|
|
52
|
+
const MODEL = getCompatEnv('MODEL') || 'unknown';
|
|
53
|
+
const CWD = getCompatEnv('CWD') || process.cwd();
|
|
54
|
+
const CLIENT = getCompatEnv('CLIENT') || 'code-local';
|
|
55
|
+
const OPENCODE_PORT = parseInt(getCompatEnv('OPENCODE_PORT') || '4096', 10);
|
|
56
|
+
const OPENCODE_SESSION_ID = getCompatEnv('SESSION_ID');
|
|
57
|
+
const FOLD_SHORTCUT = getCompatEnv('FOLD_SHORTCUT') || 'CommandOrControl+Shift+F';
|
|
58
|
+
const WINDOW_POSITION = getCompatEnv('WINDOW_POSITION') || 'right';
|
|
59
|
+
|
|
60
|
+
const OPENCODE_URL = `http://localhost:${OPENCODE_PORT}`;
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// State
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
let mainWindow = null;
|
|
67
|
+
let contentView = null;
|
|
68
|
+
let currentToolbarH = TOOLBAR_H;
|
|
69
|
+
|
|
70
|
+
const foldHandler = createFoldHandler({
|
|
71
|
+
model: MODEL,
|
|
72
|
+
client: CLIENT,
|
|
73
|
+
cwd: CWD,
|
|
74
|
+
sessionId: OPENCODE_SESSION_ID,
|
|
75
|
+
taskId: TASK_ID,
|
|
76
|
+
port: OPENCODE_PORT
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Amicus Window (OpenCode + Toolbar)
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
function createAmicusWindow() {
|
|
84
|
+
const shortcutLabel = FOLD_SHORTCUT.replace('CommandOrControl', 'Cmd');
|
|
85
|
+
|
|
86
|
+
const WIN_W = 720;
|
|
87
|
+
const WIN_H = 850;
|
|
88
|
+
const { workArea } = screen.getPrimaryDisplay();
|
|
89
|
+
const { x: winX, y: winY } = computeWindowPosition(workArea, WIN_W, WIN_H, WINDOW_POSITION);
|
|
90
|
+
|
|
91
|
+
mainWindow = new BrowserWindow({
|
|
92
|
+
width: WIN_W, height: WIN_H, minWidth: 550, minHeight: 600,
|
|
93
|
+
x: winX, y: winY,
|
|
94
|
+
show: false,
|
|
95
|
+
frame: true, backgroundColor: '#2D2B2A',
|
|
96
|
+
title: CLIENT === 'cowork' ? 'Openwork Amicus' : 'Amicus',
|
|
97
|
+
icon: ICON_PATH,
|
|
98
|
+
webPreferences: {
|
|
99
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
100
|
+
contextIsolation: true, nodeIntegration: false,
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Check for updates: prefer env var from CLI (cache is one-shot), fallback to direct check
|
|
105
|
+
let updateInfo = null;
|
|
106
|
+
const updateInfoRaw = getCompatEnv('UPDATE_INFO');
|
|
107
|
+
if (updateInfoRaw) {
|
|
108
|
+
try { updateInfo = JSON.parse(updateInfoRaw); } catch (_) {}
|
|
109
|
+
}
|
|
110
|
+
if (!updateInfo) {
|
|
111
|
+
const { getUpdateInfo, initUpdateCheck } = require('../src/utils/updater');
|
|
112
|
+
// initUpdateCheck is async (update-notifier is ESM-only). Fire-and-forget
|
|
113
|
+
// here: it seeds update-notifier's cache for the next launch, and the
|
|
114
|
+
// synchronous read below still serves the mock-mode banner. Real runs get
|
|
115
|
+
// update info via the CLI env var above.
|
|
116
|
+
initUpdateCheck();
|
|
117
|
+
updateInfo = getUpdateInfo();
|
|
118
|
+
}
|
|
119
|
+
if (updateInfo) {
|
|
120
|
+
currentToolbarH = TOOLBAR_H + 32; // Expand for 32px update banner
|
|
121
|
+
logger.info('Update available', { current: updateInfo.current, latest: updateInfo.latest });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const toolbarHtml = buildToolbarHTML({
|
|
125
|
+
mode: 'sidecar', taskId: TASK_ID, foldShortcut: shortcutLabel, client: CLIENT, updateInfo
|
|
126
|
+
});
|
|
127
|
+
mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(toolbarHtml)}`);
|
|
128
|
+
mainWindow.webContents.on('page-title-updated', (e) => e.preventDefault());
|
|
129
|
+
|
|
130
|
+
// BrowserView for OpenCode content
|
|
131
|
+
contentView = new BrowserView({
|
|
132
|
+
webPreferences: {
|
|
133
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
134
|
+
contextIsolation: true, nodeIntegration: false,
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
// Load OpenCode off-screen first; only attach BrowserView after rebranding
|
|
138
|
+
// to prevent the OpenCode logo/splash from flashing during load.
|
|
139
|
+
mainWindow.on('resize', updateContentBounds);
|
|
140
|
+
|
|
141
|
+
logger.info('Loading OpenCode Web UI', {
|
|
142
|
+
url: OPENCODE_URL, sessionId: OPENCODE_SESSION_ID, taskId: TASK_ID
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Use Electron's insertCSS API on dom-ready to hide OpenCode branding.
|
|
146
|
+
// This is more reliable than preload DOM injection in BrowserView.
|
|
147
|
+
contentView.webContents.on('dom-ready', () => {
|
|
148
|
+
contentView.webContents.insertCSS(`
|
|
149
|
+
#root > div > header { display: none !important; }
|
|
150
|
+
svg[viewBox="0 0 234 42"] { visibility: hidden !important; }
|
|
151
|
+
`).catch(() => {});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Navigate directly to the session URL to bypass the project selection screen.
|
|
155
|
+
// OpenCode's router format: /<base64url(projectPath)>/session/<sessionId>
|
|
156
|
+
const contentUrl = OPENCODE_SESSION_ID
|
|
157
|
+
? `${OPENCODE_URL}/${Buffer.from(CWD).toString('base64url')}/session/${OPENCODE_SESSION_ID}`
|
|
158
|
+
: OPENCODE_URL;
|
|
159
|
+
|
|
160
|
+
// The window only becomes visible on the success path below. Without this
|
|
161
|
+
// failsafe, a failed/stalled UI load leaves an invisible window and a
|
|
162
|
+
// silently hung process (the historical "Starting up... | 0 messages" bug).
|
|
163
|
+
const failsafe = attachLoadFailsafe({
|
|
164
|
+
webContents: contentView.webContents,
|
|
165
|
+
timeoutMs: parseInt(getCompatEnv('GUI_LOAD_TIMEOUT_MS') || '', 10) || undefined,
|
|
166
|
+
onFail: ({ reason, errorCode, errorDescription, validatedURL }) => {
|
|
167
|
+
logger.error('OpenCode UI failed to load', {
|
|
168
|
+
reason, errorCode, errorDescription, validatedURL, url: contentUrl
|
|
169
|
+
});
|
|
170
|
+
if (reason === 'load-failed') {
|
|
171
|
+
const html = buildLoadErrorHTML({
|
|
172
|
+
url: validatedURL || contentUrl, errorCode, errorDescription
|
|
173
|
+
});
|
|
174
|
+
contentView.webContents
|
|
175
|
+
.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`)
|
|
176
|
+
.catch(() => {});
|
|
177
|
+
}
|
|
178
|
+
// On timeout, show whatever is in flight rather than aborting the load.
|
|
179
|
+
mainWindow.addBrowserView(contentView);
|
|
180
|
+
updateContentBounds();
|
|
181
|
+
if (!process.env.SIDECAR_HEADLESS_TEST) {
|
|
182
|
+
mainWindow.show();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
contentView.webContents.loadURL(contentUrl);
|
|
188
|
+
|
|
189
|
+
contentView.webContents.on('did-finish-load', () => {
|
|
190
|
+
// Wait for React to render, then rebrand and show window
|
|
191
|
+
setTimeout(() => {
|
|
192
|
+
rebrandUI().then(() => {
|
|
193
|
+
// Disarm only once the window is actually about to show, so a wedged
|
|
194
|
+
// rebrand/executeJavaScript is still covered by the timeout.
|
|
195
|
+
failsafe.cancel();
|
|
196
|
+
mainWindow.addBrowserView(contentView);
|
|
197
|
+
updateContentBounds();
|
|
198
|
+
if (!process.env.SIDECAR_HEADLESS_TEST) {
|
|
199
|
+
mainWindow.show();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}, 500);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
globalShortcut.register(FOLD_SHORTCUT, () => {
|
|
206
|
+
foldHandler.triggerFold(mainWindow, contentView);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Poll toolbar for button clicks (IPC doesn't work with data: URLs).
|
|
210
|
+
// Fold, settings, and update actions all use window.__amicus*Action flags.
|
|
211
|
+
const toolbarPoll = setInterval(() => {
|
|
212
|
+
if (!mainWindow || mainWindow.isDestroyed()) { clearInterval(toolbarPoll); return; }
|
|
213
|
+
mainWindow.webContents.executeJavaScript('window.__amicusToolbarAction').then(action => {
|
|
214
|
+
if (!action) { return; }
|
|
215
|
+
mainWindow.webContents.executeJavaScript('window.__amicusToolbarAction = null');
|
|
216
|
+
if (action === 'fold') {
|
|
217
|
+
foldHandler.triggerFold(mainWindow, contentView);
|
|
218
|
+
} else if (action === 'open-settings') {
|
|
219
|
+
createSettingsChildWindow();
|
|
220
|
+
}
|
|
221
|
+
}).catch(() => {});
|
|
222
|
+
}, 300);
|
|
223
|
+
|
|
224
|
+
if (updateInfo) {
|
|
225
|
+
const updatePoll = setInterval(() => {
|
|
226
|
+
if (!mainWindow || mainWindow.isDestroyed()) { clearInterval(updatePoll); return; }
|
|
227
|
+
mainWindow.webContents.executeJavaScript('window.__amicusUpdateAction').then(action => {
|
|
228
|
+
if (action === 'perform-update') {
|
|
229
|
+
mainWindow.webContents.executeJavaScript('window.__amicusUpdateAction = null');
|
|
230
|
+
const { performUpdate } = require('../src/utils/updater');
|
|
231
|
+
performUpdate().then(result => {
|
|
232
|
+
if (!mainWindow || mainWindow.isDestroyed()) { return; }
|
|
233
|
+
if (result.success) {
|
|
234
|
+
mainWindow.webContents.executeJavaScript(`
|
|
235
|
+
document.getElementById('update-text').textContent = 'Updated! Your next amicus session will use the new version.';
|
|
236
|
+
document.getElementById('update-btn').style.display = 'none';
|
|
237
|
+
document.getElementById('dismiss-btn').style.display = '';
|
|
238
|
+
`);
|
|
239
|
+
} else {
|
|
240
|
+
mainWindow.webContents.executeJavaScript(`
|
|
241
|
+
document.getElementById('update-text').textContent = 'Update failed: ${(result.error || 'unknown').replace(/'/g, "\\'")}';
|
|
242
|
+
document.getElementById('update-btn').textContent = 'Retry';
|
|
243
|
+
document.getElementById('update-btn').disabled = false;
|
|
244
|
+
document.getElementById('dismiss-btn').style.display = '';
|
|
245
|
+
`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
} else if (action === 'dismiss') {
|
|
249
|
+
mainWindow.webContents.executeJavaScript('window.__amicusUpdateAction = null');
|
|
250
|
+
currentToolbarH = TOOLBAR_H;
|
|
251
|
+
updateContentBounds();
|
|
252
|
+
clearInterval(updatePoll);
|
|
253
|
+
}
|
|
254
|
+
}).catch(() => {});
|
|
255
|
+
}, 500);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
mainWindow.on('close', () => {
|
|
259
|
+
if (!foldHandler.hasFolded() && mainWindow) { mainWindow.destroy(); }
|
|
260
|
+
});
|
|
261
|
+
mainWindow.on('closed', () => {
|
|
262
|
+
mainWindow = null;
|
|
263
|
+
contentView = null;
|
|
264
|
+
globalShortcut.unregisterAll();
|
|
265
|
+
app.quit();
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Setup Window (API Key Form)
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
function createSetupWindow() {
|
|
274
|
+
// Lazy-load setup UI to avoid loading it for sidecar mode
|
|
275
|
+
const { buildSetupHTML } = require('./setup-ui');
|
|
276
|
+
|
|
277
|
+
mainWindow = new BrowserWindow({
|
|
278
|
+
width: 560, height: 680, minWidth: 480, minHeight: 580,
|
|
279
|
+
frame: true, backgroundColor: '#2D2B2A',
|
|
280
|
+
title: `${getBrandName(CLIENT)} Setup`,
|
|
281
|
+
icon: ICON_PATH,
|
|
282
|
+
resizable: false,
|
|
283
|
+
webPreferences: {
|
|
284
|
+
preload: path.join(__dirname, 'preload-setup.js'),
|
|
285
|
+
contextIsolation: true, nodeIntegration: false,
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const html = buildSetupHTML({ client: CLIENT });
|
|
290
|
+
mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
|
|
291
|
+
mainWindow.webContents.on('page-title-updated', (e) => e.preventDefault());
|
|
292
|
+
|
|
293
|
+
mainWindow.on('closed', () => {
|
|
294
|
+
mainWindow = null;
|
|
295
|
+
app.quit();
|
|
296
|
+
});
|
|
297
|
+
mainWindow.webContents.on('render-process-gone', (_event, details) => {
|
|
298
|
+
logger.error('Setup renderer crashed', details);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ============================================================================
|
|
303
|
+
// Shared Helpers
|
|
304
|
+
// ============================================================================
|
|
305
|
+
|
|
306
|
+
function updateContentBounds() {
|
|
307
|
+
if (!mainWindow || !contentView) { return; }
|
|
308
|
+
const [w, h] = mainWindow.getContentSize();
|
|
309
|
+
contentView.setBounds({ x: 0, y: 0, width: w, height: h - currentToolbarH });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Amicus wordmark SVG in the same pixel/block art style as the OpenCode logo.
|
|
313
|
+
// Uses the same CSS variables (--icon-base, --icon-weak-base) and viewBox
|
|
314
|
+
// proportions. Built on a 6px grid: each letter 24px wide, 30px tall (y:6-36),
|
|
315
|
+
// 6px gaps between letters (letter N starts at x = N*30). 6 letters end at
|
|
316
|
+
// x=174; ~30px right padding -> viewBox 0 0 204 43.
|
|
317
|
+
const AMICUS_WORDMARK = [
|
|
318
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 204 43" fill="none" class="CLASS">',
|
|
319
|
+
'<g>',
|
|
320
|
+
// A (x:0-24): peak, walls, crossbar with lighter inner
|
|
321
|
+
'<path d="M18 30H6V24H18Z" fill="var(--icon-weak-base)"/>',
|
|
322
|
+
'<path d="M18 12H6V6H18ZM6 36H0V12H6ZM24 36H18V12H24ZM24 24H0V18H24Z" fill="var(--icon-base)"/>',
|
|
323
|
+
// M (x:30-54): full top bar, left+right walls, hanging center tooth (lighter)
|
|
324
|
+
'<path d="M45 24H39V12H45Z" fill="var(--icon-weak-base)"/>',
|
|
325
|
+
'<path d="M54 12H30V6H54ZM36 36H30V12H36ZM54 36H48V12H54Z" fill="var(--icon-base)"/>',
|
|
326
|
+
// I (x:60-84): top bar, center stem, bottom bar
|
|
327
|
+
'<path d="M84 12H60V6H84ZM78 30H66V12H78ZM84 36H60V30H84Z" fill="var(--icon-base)"/>',
|
|
328
|
+
// C (x:90-114): top bar, left wall, bottom bar
|
|
329
|
+
'<path d="M114 12H90V6H114ZM96 30H90V12H96ZM114 36H90V30H114Z" fill="var(--icon-base)"/>',
|
|
330
|
+
// U (x:120-144): left wall, right wall, bottom bar (open top)
|
|
331
|
+
'<path d="M126 30H120V6H126ZM144 30H138V6H144ZM144 36H120V30H144Z" fill="var(--icon-base)"/>',
|
|
332
|
+
// S (x:150-174): top-right bar, left arm, full middle, right arm, bottom-left bar
|
|
333
|
+
'<path d="M174 12H156V6H174ZM156 18H150V12H156ZM174 24H150V18H174ZM174 30H168V24H174ZM168 36H150V30H168Z" fill="var(--icon-base)"/>',
|
|
334
|
+
'</g></svg>',
|
|
335
|
+
].join('');
|
|
336
|
+
|
|
337
|
+
function rebrandUI() {
|
|
338
|
+
if (!contentView) { return Promise.resolve(); }
|
|
339
|
+
const brandName = getBrandName(CLIENT);
|
|
340
|
+
// The OpenCode logo may be hidden (display:none/visibility:hidden) by preload.js
|
|
341
|
+
// or insertCSS before this runs. Use a MutationObserver with a fallback timeout
|
|
342
|
+
// to catch it whenever React renders it into the DOM.
|
|
343
|
+
return contentView.webContents.executeJavaScript(`
|
|
344
|
+
(function() {
|
|
345
|
+
document.title = '${brandName}';
|
|
346
|
+
var header = document.querySelector('#root > div > header');
|
|
347
|
+
if (header) { header.style.display = 'none'; }
|
|
348
|
+
|
|
349
|
+
function replaceLogo() {
|
|
350
|
+
// Match both visible and hidden logos
|
|
351
|
+
var logo = document.querySelector('svg[viewBox="0 0 234 42"]');
|
|
352
|
+
if (!logo) { return false; }
|
|
353
|
+
var cls = logo.getAttribute('class') || '';
|
|
354
|
+
var markup = ${JSON.stringify(AMICUS_WORDMARK)}.replace('CLASS', cls);
|
|
355
|
+
logo.insertAdjacentHTML('afterend', markup);
|
|
356
|
+
logo.remove();
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Try immediately
|
|
361
|
+
if (replaceLogo()) { return 'replaced'; }
|
|
362
|
+
|
|
363
|
+
// Observe DOM for the logo appearing (React may render it after initial load)
|
|
364
|
+
return new Promise(function(resolve) {
|
|
365
|
+
var observer = new MutationObserver(function() {
|
|
366
|
+
if (replaceLogo()) {
|
|
367
|
+
observer.disconnect();
|
|
368
|
+
resolve('replaced-observed');
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
372
|
+
// Give up after 5s
|
|
373
|
+
setTimeout(function() {
|
|
374
|
+
observer.disconnect();
|
|
375
|
+
resolve(replaceLogo() ? 'replaced-late' : 'logo-not-found');
|
|
376
|
+
}, 5000);
|
|
377
|
+
});
|
|
378
|
+
})();
|
|
379
|
+
`).catch(() => {});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ============================================================================
|
|
383
|
+
// IPC Handlers
|
|
384
|
+
// ============================================================================
|
|
385
|
+
|
|
386
|
+
// Amicus mode: fold
|
|
387
|
+
ipcMain.handle('sidecar:fold', () => {
|
|
388
|
+
return foldHandler.triggerFold(mainWindow, contentView);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Amicus mode: open settings in a child window
|
|
392
|
+
ipcMain.handle('sidecar:open-settings', () => {
|
|
393
|
+
createSettingsChildWindow();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Update check
|
|
397
|
+
ipcMain.handle('sidecar:get-update-info', async () => {
|
|
398
|
+
const { getUpdateInfo, initUpdateCheck } = require('../src/utils/updater');
|
|
399
|
+
await initUpdateCheck();
|
|
400
|
+
return getUpdateInfo();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Perform update
|
|
404
|
+
ipcMain.handle('sidecar:perform-update', async () => {
|
|
405
|
+
const { performUpdate } = require('../src/utils/updater');
|
|
406
|
+
const result = await performUpdate();
|
|
407
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
408
|
+
mainWindow.webContents.send('sidecar:update-result', result);
|
|
409
|
+
}
|
|
410
|
+
return result;
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Resize toolbar area (called when update banner shows/hides)
|
|
414
|
+
ipcMain.handle('sidecar:resize-toolbar', (_event, height) => {
|
|
415
|
+
currentToolbarH = height;
|
|
416
|
+
updateContentBounds();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Setup mode: all setup IPC handlers (extracted to ipc-setup.js)
|
|
420
|
+
registerSetupHandlers(ipcMain, () => mainWindow);
|
|
421
|
+
|
|
422
|
+
// ============================================================================
|
|
423
|
+
// Settings Child Window (opened from sidecar toolbar gear button)
|
|
424
|
+
// ============================================================================
|
|
425
|
+
|
|
426
|
+
function createSettingsChildWindow() {
|
|
427
|
+
const { buildSetupHTML } = require('./setup-ui');
|
|
428
|
+
|
|
429
|
+
const settingsWin = new BrowserWindow({
|
|
430
|
+
width: 560, height: 680,
|
|
431
|
+
parent: mainWindow, modal: false,
|
|
432
|
+
frame: true, backgroundColor: '#2D2B2A',
|
|
433
|
+
title: `${getBrandName(CLIENT)} Settings`,
|
|
434
|
+
icon: ICON_PATH,
|
|
435
|
+
resizable: false,
|
|
436
|
+
webPreferences: {
|
|
437
|
+
preload: path.join(__dirname, 'preload-setup.js'),
|
|
438
|
+
contextIsolation: true, nodeIntegration: false,
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const html = buildSetupHTML({ client: CLIENT });
|
|
443
|
+
settingsWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
|
|
444
|
+
settingsWin.webContents.on('page-title-updated', (e) => e.preventDefault());
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ============================================================================
|
|
448
|
+
// App Lifecycle
|
|
449
|
+
// ============================================================================
|
|
450
|
+
|
|
451
|
+
app.whenReady().then(() => {
|
|
452
|
+
if (process.platform === 'darwin') {
|
|
453
|
+
const { nativeImage } = require('electron');
|
|
454
|
+
const icon = nativeImage.createFromPath(ICON_PATH);
|
|
455
|
+
if (!icon.isEmpty()) { app.dock.setIcon(icon); }
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (MODE === 'setup') {
|
|
459
|
+
createSetupWindow();
|
|
460
|
+
} else {
|
|
461
|
+
createAmicusWindow();
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
app.on('window-all-closed', () => {
|
|
466
|
+
globalShortcut.unregisterAll();
|
|
467
|
+
app.quit();
|
|
468
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidecar Preload - Setup Mode
|
|
3
|
+
*
|
|
4
|
+
* Exposes IPC bridge for the setup window:
|
|
5
|
+
* - invoke: Generic IPC invoke for validate-key, save-key, setup-done
|
|
6
|
+
* - openExternal: Open URLs in default browser
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { contextBridge, ipcRenderer, shell } = require('electron');
|
|
10
|
+
|
|
11
|
+
contextBridge.exposeInMainWorld('sidecarSetup', {
|
|
12
|
+
/** Generic IPC invoke for setup channels */
|
|
13
|
+
invoke: (channel, ...args) => {
|
|
14
|
+
const allowedChannels = [
|
|
15
|
+
'sidecar:validate-key',
|
|
16
|
+
'sidecar:save-key',
|
|
17
|
+
'sidecar:remove-key',
|
|
18
|
+
'sidecar:remove-from-opencode',
|
|
19
|
+
'sidecar:setup-done',
|
|
20
|
+
'sidecar:save-config',
|
|
21
|
+
'sidecar:get-config',
|
|
22
|
+
'sidecar:get-api-keys',
|
|
23
|
+
'sidecar:fetch-models',
|
|
24
|
+
'sidecar:get-catalog',
|
|
25
|
+
'sidecar:refresh-catalog'
|
|
26
|
+
];
|
|
27
|
+
if (!allowedChannels.includes(channel)) {
|
|
28
|
+
throw new Error(`IPC channel not allowed: ${channel}`);
|
|
29
|
+
}
|
|
30
|
+
return ipcRenderer.invoke(channel, ...args);
|
|
31
|
+
},
|
|
32
|
+
/** Open a URL in the default browser */
|
|
33
|
+
openExternal: (url) => {
|
|
34
|
+
if (typeof url === 'string' && url.startsWith('https://')) {
|
|
35
|
+
shell.openExternal(url);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidecar Preload - v3 Minimal
|
|
3
|
+
*
|
|
4
|
+
* Exposes only the fold IPC bridge to the renderer.
|
|
5
|
+
* OpenCode's Web UI handles all other functionality natively.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
9
|
+
|
|
10
|
+
// Inject CSS before page scripts run to hide OpenCode branding
|
|
11
|
+
// and match window background color to prevent white flash on load
|
|
12
|
+
const style = document.createElement('style');
|
|
13
|
+
style.textContent = [
|
|
14
|
+
'html, body { background-color: #2D2B2A !important; }',
|
|
15
|
+
'#root > div > header { display: none !important; }',
|
|
16
|
+
'svg[viewBox="0 0 234 42"] { display: none !important; }',
|
|
17
|
+
].join('\n');
|
|
18
|
+
document.documentElement.appendChild(style);
|
|
19
|
+
|
|
20
|
+
contextBridge.exposeInMainWorld('sidecar', {
|
|
21
|
+
/** Trigger fold: summarize and return to Claude Code */
|
|
22
|
+
fold: () => ipcRenderer.invoke('sidecar:fold'),
|
|
23
|
+
/** Open settings wizard in a child window */
|
|
24
|
+
openSettings: () => ipcRenderer.invoke('sidecar:open-settings'),
|
|
25
|
+
/** Check if an update is available */
|
|
26
|
+
getUpdateInfo: () => ipcRenderer.invoke('sidecar:get-update-info'),
|
|
27
|
+
/** Trigger the update process */
|
|
28
|
+
performUpdate: () => ipcRenderer.invoke('sidecar:perform-update'),
|
|
29
|
+
/** Listen for update result */
|
|
30
|
+
onUpdateResult: (callback) => ipcRenderer.on('sidecar:update-result', (_event, data) => callback(data)),
|
|
31
|
+
/** Notify main process to resize toolbar area */
|
|
32
|
+
resizeToolbar: (height) => ipcRenderer.invoke('sidecar:resize-toolbar', height),
|
|
33
|
+
});
|