claw-design 1.0.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/dist/cli/commands/start.d.ts +7 -0
  4. package/dist/cli/commands/start.d.ts.map +1 -0
  5. package/dist/cli/commands/start.js +176 -0
  6. package/dist/cli/commands/start.js.map +1 -0
  7. package/dist/cli/index.d.ts +3 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +20 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/cli/utils/claude.d.ts +21 -0
  12. package/dist/cli/utils/claude.d.ts.map +1 -0
  13. package/dist/cli/utils/claude.js +42 -0
  14. package/dist/cli/utils/claude.js.map +1 -0
  15. package/dist/cli/utils/dev-server.d.ts +14 -0
  16. package/dist/cli/utils/dev-server.d.ts.map +1 -0
  17. package/dist/cli/utils/dev-server.js +57 -0
  18. package/dist/cli/utils/dev-server.js.map +1 -0
  19. package/dist/cli/utils/electron.d.ts +7 -0
  20. package/dist/cli/utils/electron.d.ts.map +1 -0
  21. package/dist/cli/utils/electron.js +36 -0
  22. package/dist/cli/utils/electron.js.map +1 -0
  23. package/dist/cli/utils/output.d.ts +6 -0
  24. package/dist/cli/utils/output.d.ts.map +1 -0
  25. package/dist/cli/utils/output.js +21 -0
  26. package/dist/cli/utils/output.js.map +1 -0
  27. package/dist/cli/utils/port-detect.d.ts +30 -0
  28. package/dist/cli/utils/port-detect.d.ts.map +1 -0
  29. package/dist/cli/utils/port-detect.js +95 -0
  30. package/dist/cli/utils/port-detect.js.map +1 -0
  31. package/dist/cli/utils/preflight.d.ts +20 -0
  32. package/dist/cli/utils/preflight.d.ts.map +1 -0
  33. package/dist/cli/utils/preflight.js +33 -0
  34. package/dist/cli/utils/preflight.js.map +1 -0
  35. package/dist/cli/utils/process.d.ts +23 -0
  36. package/dist/cli/utils/process.d.ts.map +1 -0
  37. package/dist/cli/utils/process.js +57 -0
  38. package/dist/cli/utils/process.js.map +1 -0
  39. package/out/main/index.js +1123 -0
  40. package/out/preload/overlay.cjs +56 -0
  41. package/out/preload/sidebar.cjs +29 -0
  42. package/out/renderer/assets/overlay-Bsx1u_qg.css +449 -0
  43. package/out/renderer/assets/overlay-DZl3I3jq.js +689 -0
  44. package/out/renderer/assets/sidebar-Bt34gvPU.js +563 -0
  45. package/out/renderer/assets/sidebar-BxEPS84k.css +515 -0
  46. package/out/renderer/assets/toast-CLlgwMU_.js +110 -0
  47. package/out/renderer/overlay.html +131 -0
  48. package/out/renderer/sidebar.html +64 -0
  49. package/package.json +67 -0
  50. package/resources/icon.icns +0 -0
  51. package/resources/icon.png +0 -0
  52. package/scripts/postinstall.cjs +56 -0
@@ -0,0 +1,1123 @@
1
+ import { BaseWindow, WebContentsView, shell, ipcMain, Menu, app, nativeImage } from "electron";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import { randomUUID } from "node:crypto";
5
+ import { query } from "@anthropic-ai/claude-agent-sdk";
6
+ import __cjs_mod__ from "node:module";
7
+ const __filename = import.meta.filename;
8
+ const __dirname = import.meta.dirname;
9
+ const require2 = __cjs_mod__.createRequire(import.meta.url);
10
+ const VIEWPORT_PRESETS = {
11
+ desktop: { width: 1280, height: 800 },
12
+ tablet: { width: 768, height: 1024 },
13
+ mobile: { width: 375, height: 812 }
14
+ };
15
+ function computeSiteViewBounds(preset, windowWidth, windowHeight) {
16
+ if (preset === "desktop" || !(preset in VIEWPORT_PRESETS)) {
17
+ return { x: 0, y: 0, width: windowWidth, height: windowHeight };
18
+ }
19
+ const vp = VIEWPORT_PRESETS[preset];
20
+ if (windowWidth <= vp.width && windowHeight <= vp.height) {
21
+ return { x: 0, y: 0, width: windowWidth, height: windowHeight };
22
+ }
23
+ const w = Math.min(vp.width, windowWidth);
24
+ const h = Math.min(vp.height, windowHeight);
25
+ const x = Math.round((windowWidth - w) / 2);
26
+ const y = Math.round((windowHeight - h) / 2);
27
+ return { x, y, width: w, height: h };
28
+ }
29
+ function animateBounds(view, from, to, durationMs) {
30
+ return new Promise((resolve) => {
31
+ const startTime = Date.now();
32
+ function easeInOut(t) {
33
+ return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
34
+ }
35
+ function lerp(a, b, t) {
36
+ return Math.round(a + (b - a) * t);
37
+ }
38
+ function step() {
39
+ const elapsed = Date.now() - startTime;
40
+ const rawT = Math.min(elapsed / durationMs, 1);
41
+ const t = easeInOut(rawT);
42
+ const bounds = {
43
+ x: lerp(from.x, to.x, t),
44
+ y: lerp(from.y, to.y, t),
45
+ width: lerp(from.width, to.width, t),
46
+ height: lerp(from.height, to.height, t)
47
+ };
48
+ view.setBounds(bounds);
49
+ if (rawT >= 1) {
50
+ resolve();
51
+ } else {
52
+ setTimeout(step, 16);
53
+ }
54
+ }
55
+ step();
56
+ });
57
+ }
58
+ function createMainWindow(url2, projectName2, port2) {
59
+ const win = new BaseWindow({
60
+ width: 1280,
61
+ height: 800,
62
+ center: true,
63
+ title: `Claw Design — ${projectName2} — localhost:${port2}`
64
+ });
65
+ win.contentView.setBackgroundColor("#1a1a1a");
66
+ const siteView = new WebContentsView({
67
+ webPreferences: {
68
+ contextIsolation: true,
69
+ sandbox: true,
70
+ nodeIntegration: false,
71
+ webSecurity: true,
72
+ allowRunningInsecureContent: false
73
+ }
74
+ });
75
+ win.contentView.addChildView(siteView);
76
+ const splashHtml = `<!DOCTYPE html>
77
+ <html>
78
+ <head>
79
+ <meta charset="UTF-8">
80
+ <style>
81
+ * { margin: 0; padding: 0; box-sizing: border-box; }
82
+ body {
83
+ background: #1a1a1a;
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ height: 100vh;
88
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
89
+ -webkit-font-smoothing: antialiased;
90
+ -moz-osx-font-smoothing: grayscale;
91
+ }
92
+ .splash {
93
+ display: flex;
94
+ flex-direction: column;
95
+ align-items: center;
96
+ gap: 16px;
97
+ }
98
+ .splash__brand {
99
+ font-size: 16px;
100
+ font-weight: 600;
101
+ color: rgba(255, 255, 255, 0.9);
102
+ }
103
+ .splash__spinner {
104
+ width: 24px;
105
+ height: 24px;
106
+ border: 2px solid transparent;
107
+ border-top-color: rgba(138, 180, 248, 0.8);
108
+ border-radius: 50%;
109
+ animation: spin 800ms linear infinite;
110
+ }
111
+ @keyframes spin {
112
+ to { transform: rotate(360deg); }
113
+ }
114
+ .splash__url {
115
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
116
+ font-size: 12px;
117
+ color: rgba(255, 255, 255, 0.4);
118
+ }
119
+ @media (prefers-reduced-motion: reduce) {
120
+ .splash__spinner { animation: none; border-top-color: rgba(138, 180, 248, 0.5); }
121
+ }
122
+ </style>
123
+ </head>
124
+ <body>
125
+ <div class="splash">
126
+ <div class="splash__brand">Claw Design</div>
127
+ <div class="splash__spinner" aria-label="Loading application"></div>
128
+ <div class="splash__url">Loading localhost:${port2}...</div>
129
+ </div>
130
+ </body>
131
+ </html>`;
132
+ siteView.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(splashHtml)}`);
133
+ const overlayView = new WebContentsView({
134
+ webPreferences: {
135
+ contextIsolation: true,
136
+ sandbox: true,
137
+ nodeIntegration: false,
138
+ webSecurity: true,
139
+ preload: path.join(__dirname, "../preload/overlay.cjs")
140
+ }
141
+ });
142
+ overlayView.setBackgroundColor("#00000000");
143
+ win.contentView.addChildView(overlayView);
144
+ overlayView.webContents.loadFile(
145
+ path.join(__dirname, "../renderer/overlay.html")
146
+ );
147
+ const sidebarView = new WebContentsView({
148
+ webPreferences: {
149
+ contextIsolation: true,
150
+ sandbox: true,
151
+ nodeIntegration: false,
152
+ webSecurity: true,
153
+ preload: path.join(__dirname, "../preload/sidebar.cjs")
154
+ }
155
+ });
156
+ sidebarView.setBackgroundColor("#00000000");
157
+ win.contentView.addChildView(sidebarView);
158
+ sidebarView.webContents.loadFile(
159
+ path.join(__dirname, "../renderer/sidebar.html")
160
+ );
161
+ sidebarView.setBounds({ x: 0, y: 0, width: 0, height: 0 });
162
+ let overlayIsActive = false;
163
+ let sidebarState = "hidden";
164
+ let toolbarPosition = null;
165
+ let sidebarUserPosition = null;
166
+ let currentViewport = "desktop";
167
+ function applySidebarBounds() {
168
+ const { width, height } = win.getContentBounds();
169
+ const SIDEBAR_WIDTH = 300;
170
+ const SIDEBAR_MAX_HEIGHT = 480;
171
+ const MARGIN = 16;
172
+ switch (sidebarState) {
173
+ case "hidden":
174
+ sidebarView.setBounds({ x: 0, y: 0, width: 0, height: 0 });
175
+ break;
176
+ case "minimized": {
177
+ const mW = 52;
178
+ const mH = 80;
179
+ if (sidebarUserPosition) {
180
+ const x = Math.max(0, Math.min(width - mW, sidebarUserPosition.x));
181
+ const y = Math.max(0, Math.min(height - mH, sidebarUserPosition.y));
182
+ sidebarView.setBounds({ x, y, width: mW, height: mH });
183
+ } else {
184
+ sidebarView.setBounds({
185
+ x: width - mW,
186
+ y: Math.round(height / 2) - 18,
187
+ width: mW,
188
+ height: mH
189
+ });
190
+ }
191
+ break;
192
+ }
193
+ case "expanded": {
194
+ const panelHeight = Math.min(SIDEBAR_MAX_HEIGHT, height - MARGIN * 2);
195
+ if (sidebarUserPosition) {
196
+ const x = Math.max(0, Math.min(width - SIDEBAR_WIDTH, sidebarUserPosition.x));
197
+ const y = Math.max(0, Math.min(height - panelHeight, sidebarUserPosition.y));
198
+ sidebarView.setBounds({ x, y, width: SIDEBAR_WIDTH, height: panelHeight });
199
+ } else {
200
+ sidebarView.setBounds({
201
+ x: width - SIDEBAR_WIDTH - MARGIN,
202
+ y: MARGIN,
203
+ width: SIDEBAR_WIDTH,
204
+ height: panelHeight
205
+ });
206
+ }
207
+ break;
208
+ }
209
+ }
210
+ }
211
+ function setSidebarState(state) {
212
+ sidebarState = state;
213
+ applySidebarBounds();
214
+ sidebarView.webContents.send("sidebar:state-change", state);
215
+ }
216
+ function syncBounds() {
217
+ const { width, height } = win.getContentBounds();
218
+ const siteBounds = computeSiteViewBounds(currentViewport, width, height);
219
+ siteView.setBounds(siteBounds);
220
+ if (overlayIsActive) {
221
+ overlayView.setBounds({ x: 0, y: 0, width, height });
222
+ } else {
223
+ setOverlayInactive(overlayView, win);
224
+ }
225
+ applySidebarBounds();
226
+ }
227
+ async function setViewportImpl(preset) {
228
+ if (!VIEWPORT_PRESETS[preset]) return;
229
+ const { width, height } = win.getContentBounds();
230
+ const from = siteView.getBounds();
231
+ currentViewport = preset;
232
+ const to = computeSiteViewBounds(preset, width, height);
233
+ await animateBounds(siteView, from, to, 250);
234
+ overlayView.webContents.send("viewport:changed", { preset });
235
+ }
236
+ win.on("resize", syncBounds);
237
+ syncBounds();
238
+ return {
239
+ window: win,
240
+ siteView,
241
+ overlayView,
242
+ sidebarView,
243
+ setOverlayIsActive: (active) => {
244
+ overlayIsActive = active;
245
+ },
246
+ setSidebarState,
247
+ getSidebarState: () => sidebarState,
248
+ setToolbarPosition: (x, y) => {
249
+ toolbarPosition = { x, y };
250
+ },
251
+ getToolbarPosition: () => toolbarPosition,
252
+ setSidebarUserPosition: (x, y) => {
253
+ sidebarUserPosition = { x, y };
254
+ },
255
+ getSidebarUserPosition: () => sidebarUserPosition,
256
+ setViewport: setViewportImpl,
257
+ getViewport: () => currentViewport,
258
+ navigateToSite: () => {
259
+ siteView.webContents.loadURL(url2);
260
+ }
261
+ };
262
+ }
263
+ function setOverlayInactive(overlayView, win, components) {
264
+ const { width, height } = win.getContentBounds();
265
+ const toolbarWidth = 52;
266
+ const toolbarHeight = 305;
267
+ const margin = 16;
268
+ const tooltipAllowance = 160;
269
+ const viewW = toolbarWidth + margin + tooltipAllowance;
270
+ const viewH = toolbarHeight + margin;
271
+ const userPos = components?.getToolbarPosition?.();
272
+ if (userPos) {
273
+ const x = Math.max(0, Math.min(width - viewW, userPos.x));
274
+ const y = Math.max(0, Math.min(height - viewH, userPos.y));
275
+ overlayView.setBounds({ x, y, width: viewW, height: viewH });
276
+ } else {
277
+ overlayView.setBounds({
278
+ x: width - toolbarWidth - margin - tooltipAllowance,
279
+ y: height - toolbarHeight - margin,
280
+ width: viewW,
281
+ height: viewH
282
+ });
283
+ }
284
+ components?.setOverlayIsActive(false);
285
+ }
286
+ function setOverlayActive(overlayView, win, components) {
287
+ const { width, height } = win.getContentBounds();
288
+ if (components?.getSidebarState && components.getSidebarState() === "expanded") {
289
+ components.setSidebarState("minimized");
290
+ }
291
+ overlayView.setBounds({ x: 0, y: 0, width, height });
292
+ components?.setOverlayIsActive(true);
293
+ }
294
+ function setupNavigation(siteView, allowedOrigin) {
295
+ siteView.webContents.on("will-navigate", (event, navigationUrl) => {
296
+ try {
297
+ const parsed = new URL(navigationUrl);
298
+ if (parsed.origin !== allowedOrigin) {
299
+ event.preventDefault();
300
+ shell.openExternal(navigationUrl);
301
+ }
302
+ } catch {
303
+ event.preventDefault();
304
+ console.warn(
305
+ `[claw-design] Blocked navigation to malformed URL: ${navigationUrl}`
306
+ );
307
+ }
308
+ });
309
+ siteView.webContents.setWindowOpenHandler(({ url: url2 }) => {
310
+ try {
311
+ const parsed = new URL(url2);
312
+ if (parsed.origin !== allowedOrigin) {
313
+ shell.openExternal(url2);
314
+ }
315
+ } catch {
316
+ console.warn(
317
+ `[claw-design] Blocked window.open for malformed URL: ${url2}`
318
+ );
319
+ }
320
+ return { action: "deny" };
321
+ });
322
+ }
323
+ async function captureRegion(siteView, cssRect) {
324
+ const rect = {
325
+ x: Math.max(0, Math.round(cssRect.x)),
326
+ y: Math.max(0, Math.round(cssRect.y)),
327
+ width: Math.max(1, Math.round(cssRect.width)),
328
+ height: Math.max(1, Math.round(cssRect.height))
329
+ };
330
+ const image = await siteView.webContents.capturePage(rect);
331
+ return image.toPNG();
332
+ }
333
+ function buildDomExtractionScript(rect) {
334
+ return `(function() {
335
+ var rect = { x: ${rect.x}, y: ${rect.y}, w: ${rect.width}, h: ${rect.height} };
336
+ var elements = [];
337
+ var allEls = document.querySelectorAll('*');
338
+
339
+ function getElementPath(el) {
340
+ var parts = [];
341
+ var current = el;
342
+ while (current && current !== document.body && current !== document.documentElement) {
343
+ var selector = current.tagName.toLowerCase();
344
+ if (current.id) {
345
+ selector += '#' + current.id;
346
+ } else if (current.className && typeof current.className === 'string') {
347
+ var cls = current.className.split(' ').filter(function(c) { return c.length > 0; })[0];
348
+ if (cls) selector += '.' + cls;
349
+ }
350
+ parts.unshift(selector);
351
+ current = current.parentElement;
352
+ }
353
+ return parts.join(' > ');
354
+ }
355
+
356
+ for (var i = 0; i < allEls.length; i++) {
357
+ var el = allEls[i];
358
+ var elRect = el.getBoundingClientRect();
359
+
360
+ // Check overlap with selection rect
361
+ if (elRect.right < rect.x || elRect.left > rect.x + rect.w) continue;
362
+ if (elRect.bottom < rect.y || elRect.top > rect.y + rect.h) continue;
363
+
364
+ // Skip invisible elements
365
+ var style = window.getComputedStyle(el);
366
+ if (style.display === 'none' || style.visibility === 'hidden') continue;
367
+
368
+ var textContent = el.textContent ? el.textContent.trim().substring(0, 200) : undefined;
369
+
370
+ elements.push({
371
+ tag: el.tagName.toLowerCase(),
372
+ id: el.id || undefined,
373
+ classes: el.className && typeof el.className === 'string'
374
+ ? el.className.split(' ').filter(function(c) { return c.length > 0; })
375
+ : [],
376
+ text: textContent || undefined,
377
+ bounds: {
378
+ x: Math.round(elRect.x),
379
+ y: Math.round(elRect.y),
380
+ width: Math.round(elRect.width),
381
+ height: Math.round(elRect.height)
382
+ },
383
+ path: getElementPath(el)
384
+ });
385
+ }
386
+
387
+ return {
388
+ elements: elements,
389
+ viewport: { width: window.innerWidth, height: window.innerHeight }
390
+ };
391
+ })()`;
392
+ }
393
+ function registerIpcHandlers(components, agentManager) {
394
+ ipcMain.handle("overlay:activate-selection", async () => {
395
+ if (components.getSidebarState() === "expanded") {
396
+ components.setSidebarState("minimized");
397
+ }
398
+ const preExpansionBounds = components.overlayView.getBounds();
399
+ setOverlayActive(components.overlayView, components.window, components);
400
+ components.overlayView.webContents.send(
401
+ "overlay:mode-change",
402
+ "selection"
403
+ );
404
+ return preExpansionBounds;
405
+ });
406
+ ipcMain.handle("overlay:deactivate-selection", async () => {
407
+ setOverlayInactive(components.overlayView, components.window, components);
408
+ components.overlayView.webContents.send("overlay:mode-change", "inactive");
409
+ });
410
+ ipcMain.handle(
411
+ "overlay:get-element-at-point",
412
+ async (_event, x, y) => {
413
+ const result = await components.siteView.webContents.executeJavaScript(`
414
+ (function() {
415
+ const el = document.elementFromPoint(${x}, ${y});
416
+ if (!el) return null;
417
+ const rect = el.getBoundingClientRect();
418
+ return {
419
+ x: Math.round(rect.x),
420
+ y: Math.round(rect.y),
421
+ width: Math.round(rect.width),
422
+ height: Math.round(rect.height)
423
+ };
424
+ })()
425
+ `);
426
+ return result;
427
+ }
428
+ );
429
+ ipcMain.handle(
430
+ "overlay:capture-screenshot",
431
+ async (_event, cssRect) => {
432
+ return captureRegion(components.siteView, cssRect);
433
+ }
434
+ );
435
+ ipcMain.handle(
436
+ "overlay:extract-dom",
437
+ async (_event, cssRect) => {
438
+ const script = buildDomExtractionScript(cssRect);
439
+ const result = await components.siteView.webContents.executeJavaScript(
440
+ script
441
+ );
442
+ return result;
443
+ }
444
+ );
445
+ ipcMain.handle(
446
+ "overlay:submit-instruction",
447
+ async (_event, data) => {
448
+ const screenshotBuffer = Buffer.from(data.screenshot);
449
+ const refImages = data.referenceImages?.map((b) => Buffer.from(b));
450
+ const taskId = await agentManager.submitTask({
451
+ instruction: data.instruction,
452
+ screenshot: screenshotBuffer,
453
+ dom: data.dom,
454
+ bounds: data.bounds,
455
+ referenceImages: refImages,
456
+ model: data.model
457
+ });
458
+ setOverlayInactive(components.overlayView, components.window, components);
459
+ components.overlayView.webContents.send("overlay:mode-change", "inactive");
460
+ return taskId;
461
+ }
462
+ );
463
+ ipcMain.handle("viewport:set", async (_event, data) => {
464
+ const validPresets = ["desktop", "tablet", "mobile"];
465
+ if (!validPresets.includes(data.preset)) return;
466
+ await components.setViewport(data.preset);
467
+ });
468
+ ipcMain.handle("toast:dismiss", async (_event, data) => {
469
+ components.overlayView.webContents.send("toast:dismiss", data);
470
+ });
471
+ ipcMain.handle("sidebar:expand", async () => {
472
+ components.setSidebarState("expanded");
473
+ });
474
+ ipcMain.handle("sidebar:collapse", async () => {
475
+ components.setSidebarState("minimized");
476
+ });
477
+ ipcMain.handle("sidebar:task-dismiss", async (_event, data) => {
478
+ agentManager.dismissTask(data.id);
479
+ if (agentManager.getAllTasks().length === 0) {
480
+ components.setSidebarState("hidden");
481
+ }
482
+ });
483
+ ipcMain.handle("sidebar:task-logs", async (_event, data) => {
484
+ return agentManager.getTaskLogs(data.id);
485
+ });
486
+ ipcMain.handle("sidebar:task-retry", async (_event, data) => {
487
+ const task = agentManager.getTask(data.id);
488
+ if (!task) return;
489
+ const instruction = task.instruction;
490
+ agentManager.dismissTask(data.id);
491
+ if (agentManager.getAllTasks().length === 0) {
492
+ components.setSidebarState("hidden");
493
+ }
494
+ components.overlayView.webContents.send("overlay:prefill-instruction", {
495
+ instruction
496
+ });
497
+ });
498
+ ipcMain.handle("sidebar:task-undo", async (_event, data) => {
499
+ await agentManager.undoTask(data.id);
500
+ });
501
+ ipcMain.handle("sidebar:drag-delta", async (_event, data) => {
502
+ const current = components.sidebarView.getBounds();
503
+ const { width: winW, height: winH } = components.window.getContentBounds();
504
+ const newX = Math.max(0, Math.min(winW - current.width, current.x + data.dx));
505
+ const newY = Math.max(0, Math.min(winH - current.height, current.y + data.dy));
506
+ components.sidebarView.setBounds({ ...current, x: newX, y: newY });
507
+ components.setSidebarUserPosition(newX, newY);
508
+ return { x: newX, y: newY };
509
+ });
510
+ ipcMain.handle("sidebar:set-position", async (_event, data) => {
511
+ const current = components.sidebarView.getBounds();
512
+ const { width: winW, height: winH } = components.window.getContentBounds();
513
+ const x = Math.max(0, Math.min(winW - current.width, data.x));
514
+ const y = Math.max(0, Math.min(winH - current.height, data.y));
515
+ components.sidebarView.setBounds({ ...current, x, y });
516
+ components.setSidebarUserPosition(x, y);
517
+ });
518
+ ipcMain.handle("overlay:drag-toolbar", async (_event, data) => {
519
+ const current = components.overlayView.getBounds();
520
+ const { width: winW, height: winH } = components.window.getContentBounds();
521
+ const newX = Math.max(0, Math.min(winW - current.width, current.x + data.dx));
522
+ const newY = Math.max(0, Math.min(winH - current.height, current.y + data.dy));
523
+ components.overlayView.setBounds({ ...current, x: newX, y: newY });
524
+ components.setToolbarPosition(newX, newY);
525
+ return { x: newX, y: newY };
526
+ });
527
+ ipcMain.handle("overlay:set-toolbar-position", async (_event, data) => {
528
+ const current = components.overlayView.getBounds();
529
+ const { width: winW, height: winH } = components.window.getContentBounds();
530
+ const x = Math.max(0, Math.min(winW - current.width, data.x));
531
+ const y = Math.max(0, Math.min(winH - current.height, data.y));
532
+ components.overlayView.setBounds({ ...current, x, y });
533
+ components.setToolbarPosition(x, y);
534
+ });
535
+ }
536
+ function assemblePrompt(instruction, screenshotBuffer, domContext, bounds, referenceImages) {
537
+ const primaryElement = domContext.elements.length > 0 ? domContext.elements.reduce((best, el) => {
538
+ const area = el.bounds.width * el.bounds.height;
539
+ const bestArea = best.bounds.width * best.bounds.height;
540
+ return area > bestArea ? el : best;
541
+ }) : null;
542
+ let contextSuffix = "";
543
+ if (primaryElement) {
544
+ contextSuffix += `
545
+
546
+ **Selected element:** \`<${primaryElement.tag}>\` at \`${primaryElement.path}\``;
547
+ if (primaryElement.text) {
548
+ contextSuffix += ` containing "${primaryElement.text.slice(0, 100)}"`;
549
+ }
550
+ contextSuffix += "\n\nApply the instruction to this element specifically. The screenshot shows exactly what the user selected.";
551
+ }
552
+ if (bounds) {
553
+ contextSuffix += `
554
+
555
+ **Selection bounds:** x=${bounds.x}, y=${bounds.y}, ${bounds.width}x${bounds.height}px`;
556
+ }
557
+ const contentBlocks = [];
558
+ const hasInlineRefs = referenceImages && referenceImages.length > 0 && /\[Image #\d+\]/.test(instruction);
559
+ if (hasInlineRefs && referenceImages) {
560
+ const parts = instruction.split(/(\[Image #\d+\])/);
561
+ let headerAdded = false;
562
+ for (const part of parts) {
563
+ const match = part.match(/^\[Image #(\d+)\]$/);
564
+ if (match) {
565
+ const idx = parseInt(match[1], 10) - 1;
566
+ if (idx >= 0 && idx < referenceImages.length) {
567
+ if (!headerAdded) {
568
+ contentBlocks.unshift({ type: "text", text: "## Change Instruction\n\n" });
569
+ headerAdded = true;
570
+ }
571
+ contentBlocks.push({
572
+ type: "image",
573
+ source: {
574
+ type: "base64",
575
+ media_type: "image/png",
576
+ data: referenceImages[idx].toString("base64")
577
+ }
578
+ });
579
+ }
580
+ } else if (part) {
581
+ if (!headerAdded) {
582
+ contentBlocks.push({ type: "text", text: "## Change Instruction\n\n" + part });
583
+ headerAdded = true;
584
+ } else {
585
+ contentBlocks.push({ type: "text", text: part });
586
+ }
587
+ }
588
+ }
589
+ if (contextSuffix) {
590
+ contentBlocks.push({ type: "text", text: contextSuffix });
591
+ }
592
+ } else {
593
+ contentBlocks.push({
594
+ type: "text",
595
+ text: "## Change Instruction\n\n" + instruction + contextSuffix
596
+ });
597
+ }
598
+ contentBlocks.push({
599
+ type: "image",
600
+ source: {
601
+ type: "base64",
602
+ media_type: "image/png",
603
+ data: screenshotBuffer.toString("base64")
604
+ }
605
+ });
606
+ if (referenceImages && referenceImages.length > 0 && !hasInlineRefs) {
607
+ contentBlocks.push({
608
+ type: "text",
609
+ text: `## Reference Image${referenceImages.length > 1 ? "s" : ""}
610
+
611
+ The user pasted ${referenceImages.length > 1 ? "these images as" : "this image as a"} reference for what the result should look like:`
612
+ });
613
+ for (const buf of referenceImages) {
614
+ contentBlocks.push({
615
+ type: "image",
616
+ source: { type: "base64", media_type: "image/png", data: buf.toString("base64") }
617
+ });
618
+ }
619
+ }
620
+ contentBlocks.push({
621
+ type: "text",
622
+ text: "## DOM Context\n\nElements in the selected region:\n```json\n" + JSON.stringify(domContext, null, 2) + "\n```"
623
+ });
624
+ const userMessage = {
625
+ type: "user",
626
+ message: {
627
+ role: "user",
628
+ content: contentBlocks
629
+ },
630
+ parent_tool_use_id: null
631
+ };
632
+ return (async function* () {
633
+ yield userMessage;
634
+ })();
635
+ }
636
+ const FATAL_ASSISTANT_ERRORS = /* @__PURE__ */ new Set([
637
+ "authentication_failed",
638
+ "billing_error",
639
+ "invalid_request"
640
+ ]);
641
+ function humanReadableError(errors) {
642
+ const joined = errors.join(" ");
643
+ if (joined.includes("authentication_failed") || joined.includes("Invalid API key")) {
644
+ return 'Not authenticated. Run "claude login" in your terminal to sign in.';
645
+ }
646
+ if (joined.includes("rate_limit")) {
647
+ return "Rate limit reached. Retry in a moment.";
648
+ }
649
+ if (joined.includes("billing_error")) {
650
+ return "Billing issue. Check your Claude account.";
651
+ }
652
+ if (joined.includes("server_error")) {
653
+ return "Claude server error. Retry in a moment.";
654
+ }
655
+ if (joined.includes("invalid_request")) {
656
+ return "Invalid request. Check your Claude Code installation.";
657
+ }
658
+ if (joined.length > 0) {
659
+ return joined.slice(0, 200);
660
+ }
661
+ return "Something went wrong. Retry or dismiss to continue.";
662
+ }
663
+ function describeToolUse(toolName, input) {
664
+ const filePath = input?.file_path;
665
+ const short = filePath ? filePath.replace(/^.*\//, "") : void 0;
666
+ switch (toolName) {
667
+ case "Read":
668
+ return short ? `Reading ${short}` : "Reading file...";
669
+ case "Write":
670
+ return short ? `Writing ${short}` : "Writing file...";
671
+ case "Edit":
672
+ return short ? `Editing ${short}` : "Editing file...";
673
+ case "Glob":
674
+ return "Searching for files...";
675
+ case "Grep":
676
+ return "Searching code...";
677
+ case "Bash": {
678
+ const cmd = input?.command;
679
+ return cmd ? `Running: ${cmd.slice(0, 60)}` : "Running command...";
680
+ }
681
+ default:
682
+ return `Using ${toolName}...`;
683
+ }
684
+ }
685
+ class AgentManager {
686
+ tasks = /* @__PURE__ */ new Map();
687
+ activeCount = 0;
688
+ maxParallel = 3;
689
+ projectDir;
690
+ onTaskUpdate = null;
691
+ constructor(projectDir) {
692
+ this.projectDir = projectDir;
693
+ }
694
+ setOnTaskUpdate(cb) {
695
+ this.onTaskUpdate = cb;
696
+ }
697
+ /**
698
+ * Submit a new task for Claude to execute.
699
+ * Returns the generated task ID.
700
+ */
701
+ async submitTask(input) {
702
+ const id = randomUUID();
703
+ const task = {
704
+ id,
705
+ instruction: input.instruction,
706
+ status: "queued",
707
+ screenshot: input.screenshot,
708
+ dom: input.dom,
709
+ bounds: input.bounds,
710
+ referenceImages: input.referenceImages,
711
+ model: input.model,
712
+ logs: [],
713
+ fatalErrors: []
714
+ };
715
+ this.tasks.set(id, task);
716
+ this.emitUpdate(task);
717
+ this.processQueue();
718
+ return id;
719
+ }
720
+ /**
721
+ * Retry a task re-using its original screenshot, DOM context, and bounds.
722
+ * Dismisses the old task and creates a new one.
723
+ */
724
+ async retryTask(id) {
725
+ const original = this.tasks.get(id);
726
+ if (!original) {
727
+ throw new Error(`Task ${id} not found`);
728
+ }
729
+ const { instruction, screenshot, dom, bounds } = original;
730
+ this.dismissTask(id);
731
+ return this.submitTask({ instruction, screenshot, dom, bounds });
732
+ }
733
+ /**
734
+ * Undo a completed task by submitting a new task that asks Claude
735
+ * to revert the change, using the original screenshot/DOM as reference
736
+ * for what "before" looked like.
737
+ */
738
+ async undoTask(id) {
739
+ const task = this.tasks.get(id);
740
+ if (!task) throw new Error("Task not found");
741
+ if (task.status !== "done") throw new Error("Only completed tasks can be undone");
742
+ this.dismissTask(id);
743
+ return this.submitTask({
744
+ instruction: `Undo the previous change: "${task.instruction}". Revert the code back to match the screenshot and DOM context shown here (this is what it looked like BEFORE the change was made).`,
745
+ screenshot: task.screenshot,
746
+ dom: task.dom,
747
+ bounds: task.bounds
748
+ });
749
+ }
750
+ /**
751
+ * Dismiss a task, removing it from the internal map.
752
+ * If the task is active (sending/editing), aborts the query.
753
+ */
754
+ dismissTask(id) {
755
+ const task = this.tasks.get(id);
756
+ if (!task) return;
757
+ if ((task.status === "sending" || task.status === "editing") && task.abortController) {
758
+ task.abortController.abort();
759
+ }
760
+ this.tasks.delete(id);
761
+ }
762
+ /**
763
+ * Shut down all active queries and clean up resources.
764
+ */
765
+ shutdown() {
766
+ for (const task of this.tasks.values()) {
767
+ if ((task.status === "sending" || task.status === "editing") && task.abortController) {
768
+ task.abortController.abort();
769
+ }
770
+ if (task.queryRef) {
771
+ task.queryRef.close();
772
+ }
773
+ }
774
+ }
775
+ /**
776
+ * Get a task by ID (public view without internal fields).
777
+ */
778
+ getTask(id) {
779
+ const task = this.tasks.get(id);
780
+ if (!task) return void 0;
781
+ return {
782
+ id: task.id,
783
+ instruction: task.instruction,
784
+ status: task.status,
785
+ error: task.error,
786
+ screenshot: task.screenshot,
787
+ dom: task.dom,
788
+ bounds: task.bounds
789
+ };
790
+ }
791
+ /**
792
+ * Get all tasks (public view).
793
+ */
794
+ getAllTasks() {
795
+ return Array.from(this.tasks.values()).map((t) => ({
796
+ id: t.id,
797
+ instruction: t.instruction,
798
+ status: t.status,
799
+ error: t.error,
800
+ screenshot: t.screenshot,
801
+ dom: t.dom,
802
+ bounds: t.bounds
803
+ }));
804
+ }
805
+ // ---- Private ----
806
+ processQueue() {
807
+ if (this.activeCount >= this.maxParallel) return;
808
+ for (const task of this.tasks.values()) {
809
+ if (task.status === "queued") {
810
+ this.activeCount++;
811
+ task.status = "sending";
812
+ this.emitUpdate(task);
813
+ this.executeTask(task);
814
+ return;
815
+ }
816
+ }
817
+ }
818
+ async executeTask(task) {
819
+ const abortController = new AbortController();
820
+ task.abortController = abortController;
821
+ const prompt = assemblePrompt(
822
+ task.instruction,
823
+ task.screenshot,
824
+ task.dom,
825
+ task.bounds,
826
+ task.referenceImages
827
+ );
828
+ const cleanEnv = { ...process.env };
829
+ delete cleanEnv.ANTHROPIC_API_KEY;
830
+ const q = query({
831
+ prompt,
832
+ options: {
833
+ abortController,
834
+ cwd: this.projectDir,
835
+ env: cleanEnv,
836
+ systemPrompt: {
837
+ type: "preset",
838
+ preset: "claude_code",
839
+ append: "You are used by claw-design. The user selected a region of their website and provided a screenshot, DOM context, and change instruction. Edit the source code to implement the change. Be concise in your response."
840
+ },
841
+ allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
842
+ permissionMode: "acceptEdits",
843
+ settingSources: ["user", "project"],
844
+ persistSession: false,
845
+ maxTurns: 20,
846
+ ...task.model ? { model: task.model } : {}
847
+ }
848
+ });
849
+ task.queryRef = q;
850
+ try {
851
+ for await (const message of q) {
852
+ const msg = message;
853
+ if (msg.type === "system" && msg.subtype === "init") {
854
+ task.status = "editing";
855
+ this.addLog(task, "status", "Claude connected, starting edits...");
856
+ this.emitUpdate(task);
857
+ } else if (msg.type === "auth_status") {
858
+ const authMsg = message;
859
+ if (authMsg.error) {
860
+ task.fatalErrors.push(authMsg.error);
861
+ this.addLog(task, "status", `Auth error: ${authMsg.error}`);
862
+ }
863
+ } else if (msg.type === "assistant") {
864
+ const assistantMsg = message;
865
+ if (assistantMsg.error && FATAL_ASSISTANT_ERRORS.has(assistantMsg.error)) {
866
+ task.fatalErrors.push(assistantMsg.error);
867
+ this.addLog(
868
+ task,
869
+ "status",
870
+ `API error: ${assistantMsg.error}`
871
+ );
872
+ }
873
+ this.extractActivity(task, msg);
874
+ } else if (msg.type === "tool_use_summary") {
875
+ const summary = msg.summary;
876
+ if (summary) {
877
+ task.activity = summary;
878
+ this.addLog(task, "text", summary);
879
+ this.emitUpdate(task);
880
+ }
881
+ } else if (msg.type === "result") {
882
+ if (task.fatalErrors.length > 0) {
883
+ task.status = "error";
884
+ task.error = humanReadableError(task.fatalErrors);
885
+ task.activity = void 0;
886
+ this.addLog(task, "status", `Error: ${task.error}`);
887
+ this.emitUpdate(task);
888
+ } else if (msg.subtype === "success") {
889
+ task.status = "done";
890
+ task.activity = void 0;
891
+ this.addLog(task, "status", "Completed");
892
+ this.emitUpdate(task);
893
+ } else {
894
+ const errors = msg.errors || [];
895
+ task.status = "error";
896
+ task.error = humanReadableError(errors);
897
+ task.activity = void 0;
898
+ this.addLog(task, "status", `Error: ${task.error}`);
899
+ this.emitUpdate(task);
900
+ }
901
+ }
902
+ }
903
+ } catch (err) {
904
+ if (task.status !== "done" && task.status !== "error") {
905
+ task.status = "error";
906
+ const errMsg = err instanceof Error ? err.message : String(err);
907
+ task.error = errMsg || "Something went wrong. Retry or dismiss to continue.";
908
+ this.addLog(task, "status", `Error: ${errMsg}`);
909
+ this.emitUpdate(task);
910
+ }
911
+ } finally {
912
+ try {
913
+ q.close();
914
+ } catch {
915
+ }
916
+ this.activeCount--;
917
+ this.processQueue();
918
+ }
919
+ }
920
+ /**
921
+ * Extract activity text from assistant messages containing tool_use blocks.
922
+ */
923
+ extractActivity(task, msg) {
924
+ const assistantMsg = msg.message;
925
+ if (!assistantMsg?.content) return;
926
+ for (const block of assistantMsg.content) {
927
+ if (block.type === "tool_use") {
928
+ const toolName = block.name;
929
+ const input = block.input;
930
+ const activity = describeToolUse(toolName, input);
931
+ task.activity = activity;
932
+ this.addLog(task, "tool", activity);
933
+ this.emitUpdate(task);
934
+ } else if (block.type === "text" && typeof block.text === "string") {
935
+ const text = block.text.slice(0, 500);
936
+ if (text.trim()) {
937
+ this.addLog(task, "text", text);
938
+ }
939
+ }
940
+ }
941
+ }
942
+ addLog(task, type, content) {
943
+ task.logs.push({ timestamp: Date.now(), type, content });
944
+ }
945
+ /**
946
+ * Get logs for a task by ID.
947
+ */
948
+ getTaskLogs(id) {
949
+ return this.tasks.get(id)?.logs ?? [];
950
+ }
951
+ emitUpdate(task) {
952
+ if (!this.onTaskUpdate) return;
953
+ this.onTaskUpdate({
954
+ id: task.id,
955
+ instruction: task.instruction,
956
+ status: task.status,
957
+ error: task.error,
958
+ activity: task.activity,
959
+ model: task.model
960
+ });
961
+ }
962
+ }
963
+ function setupApplicationMenu() {
964
+ const appName = "Claw Design";
965
+ const isMac = process.platform === "darwin";
966
+ const template = [
967
+ // macOS app menu (first menu item becomes the app-name menu)
968
+ ...isMac ? [
969
+ {
970
+ label: appName,
971
+ submenu: [
972
+ { role: "about", label: `About ${appName}` },
973
+ { type: "separator" },
974
+ { role: "services" },
975
+ { type: "separator" },
976
+ { role: "hide", label: `Hide ${appName}` },
977
+ { role: "hideOthers" },
978
+ { role: "unhide" },
979
+ { type: "separator" },
980
+ { role: "quit", label: `Quit ${appName}` }
981
+ ]
982
+ }
983
+ ] : [],
984
+ // Edit menu
985
+ {
986
+ label: "Edit",
987
+ submenu: [
988
+ { role: "undo" },
989
+ { role: "redo" },
990
+ { type: "separator" },
991
+ { role: "cut" },
992
+ { role: "copy" },
993
+ { role: "paste" },
994
+ { role: "selectAll" }
995
+ ]
996
+ },
997
+ // View menu
998
+ {
999
+ label: "View",
1000
+ submenu: [
1001
+ { role: "reload" },
1002
+ { role: "forceReload" },
1003
+ { role: "toggleDevTools" },
1004
+ { type: "separator" },
1005
+ { role: "resetZoom" },
1006
+ { role: "zoomIn" },
1007
+ { role: "zoomOut" },
1008
+ { type: "separator" },
1009
+ { role: "togglefullscreen" }
1010
+ ]
1011
+ },
1012
+ // Window menu
1013
+ {
1014
+ label: "Window",
1015
+ submenu: [
1016
+ { role: "minimize" },
1017
+ { role: "zoom" },
1018
+ ...isMac ? [
1019
+ { type: "separator" },
1020
+ { role: "front" }
1021
+ ] : [{ role: "close" }]
1022
+ ]
1023
+ },
1024
+ // Help menu
1025
+ {
1026
+ label: "Help",
1027
+ submenu: [
1028
+ {
1029
+ label: `${appName} on GitHub`,
1030
+ click: async () => {
1031
+ await shell.openExternal(
1032
+ "https://github.com/prodoxx/claw-design"
1033
+ );
1034
+ }
1035
+ }
1036
+ ]
1037
+ }
1038
+ ];
1039
+ const menu = Menu.buildFromTemplate(template);
1040
+ Menu.setApplicationMenu(menu);
1041
+ }
1042
+ const url = process.env.CLAW_URL ?? "http://localhost:3000";
1043
+ const projectName = process.env.CLAW_PROJECT_NAME ?? "unknown";
1044
+ const port = parseInt(url.match(/:(\d+)/)?.[1] ?? "3000", 10);
1045
+ app.setName("Claw Design");
1046
+ const iconPath = path.join(__dirname, "../../resources/icon.png");
1047
+ let appVersion = "1.0.0";
1048
+ try {
1049
+ const pkgPath = path.join(__dirname, "../../package.json");
1050
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
1051
+ appVersion = pkg.version ?? appVersion;
1052
+ } catch {
1053
+ }
1054
+ app.whenReady().then(() => {
1055
+ let aboutIcon = nativeImage.createEmpty();
1056
+ try {
1057
+ const iconBuffer = fs.readFileSync(iconPath);
1058
+ aboutIcon = nativeImage.createFromBuffer(iconBuffer);
1059
+ } catch {
1060
+ }
1061
+ app.setAboutPanelOptions({
1062
+ applicationName: "Claw Design",
1063
+ applicationVersion: appVersion,
1064
+ version: "",
1065
+ copyright: "MIT License",
1066
+ ...process.platform === "darwin" && !aboutIcon.isEmpty() ? { icon: aboutIcon } : {},
1067
+ ...process.platform === "linux" || process.platform === "win32" ? { iconPath } : {}
1068
+ });
1069
+ if (process.platform === "darwin" && app.dock && !aboutIcon.isEmpty()) {
1070
+ app.dock.setIcon(aboutIcon);
1071
+ }
1072
+ setupApplicationMenu();
1073
+ const components = createMainWindow(url, projectName, port);
1074
+ components.navigateToSite();
1075
+ components.siteView.webContents.on("did-finish-load", () => {
1076
+ components.overlayView.webContents.send("site:loaded");
1077
+ });
1078
+ const allowedOrigin = new URL(url).origin;
1079
+ setupNavigation(components.siteView, allowedOrigin);
1080
+ const projectDir = process.env.CLAW_PROJECT_DIR ?? process.cwd();
1081
+ const agentManager = new AgentManager(projectDir);
1082
+ agentManager.setOnTaskUpdate((update) => {
1083
+ components.sidebarView.webContents.send("sidebar:task-update", update);
1084
+ if (components.getSidebarState() === "hidden") {
1085
+ components.setSidebarState("expanded");
1086
+ }
1087
+ });
1088
+ registerIpcHandlers(components, agentManager);
1089
+ setOverlayInactive(components.overlayView, components.window);
1090
+ let devServerCrashNotified = false;
1091
+ components.siteView.webContents.on("did-fail-load", (_event, _errorCode, _errorDescription, validatedURL) => {
1092
+ if (!validatedURL.includes("localhost") && !validatedURL.includes("127.0.0.1")) return;
1093
+ if (devServerCrashNotified) return;
1094
+ devServerCrashNotified = true;
1095
+ components.overlayView.webContents.send("toast:show", {
1096
+ id: "dev-server-crash",
1097
+ severity: "error",
1098
+ title: "Dev server disconnected",
1099
+ message: "The dev server process exited unexpectedly. Restart clawdesign to continue.",
1100
+ persistent: true
1101
+ });
1102
+ });
1103
+ components.siteView.webContents.on("render-process-gone", (_event, _details) => {
1104
+ if (devServerCrashNotified) return;
1105
+ devServerCrashNotified = true;
1106
+ components.overlayView.webContents.send("toast:show", {
1107
+ id: "dev-server-crash",
1108
+ severity: "error",
1109
+ title: "Connection lost",
1110
+ message: `Cannot reach localhost:${port}. Check if the dev server is still running.`,
1111
+ persistent: true
1112
+ });
1113
+ });
1114
+ components.window.on("closed", () => {
1115
+ app.quit();
1116
+ });
1117
+ app.on("before-quit", () => {
1118
+ agentManager.shutdown();
1119
+ });
1120
+ });
1121
+ app.on("window-all-closed", () => {
1122
+ app.quit();
1123
+ });