sparkecode-devtools 0.1.41 → 0.1.42

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/README.md CHANGED
@@ -109,16 +109,37 @@ You can also send copy events to any endpoint:
109
109
 
110
110
  ## Controls
111
111
 
112
- - Click the floating button to toggle selection
112
+ - Click the floating button to toggle selection mode
113
+ - Click the panel icon (right side of button) to open the SparkeCode web UI
113
114
  - Drag the button to reposition
114
- - Press `Cmd/Ctrl+Shift+S` to toggle
115
+ - Press `Cmd/Ctrl+Shift+S` to toggle selection mode
115
116
  - Press `Esc` to exit selection mode
116
117
 
118
+ ## Floating Web UI Panel
119
+
120
+ Click the panel icon on the button to open a draggable, collapsible panel showing the full SparkeCode web interface. This lets you see the agent's responses and chat history while working on your app.
121
+
122
+ - Drag the header to move the panel
123
+ - Click the minus button to collapse
124
+ - Click the X to close
125
+
126
+ Configure the web UI URL:
127
+
128
+ ```bash
129
+ # In your .env.local
130
+ NEXT_PUBLIC_SPARKECODER_WEB_UI_URL=http://localhost:6969
131
+ ```
132
+
133
+ Or via prop:
134
+ ```tsx
135
+ <SparkeCodeSelect webUiUrl="http://localhost:6969" />
136
+ ```
137
+
117
138
  ## Auto-send to Agent
118
139
 
119
140
  When connected to a running Sparkecoder instance, clicking a component will:
120
141
 
121
- 1. Show a prompt input modal
142
+ 1. Show a prompt input
122
143
  2. Let you type what you want to do with the component (e.g., "make this button red")
123
144
  3. Press Enter to send the request + component context directly to the agent
124
145
  4. The agent will start working immediately
package/next.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface SparkeCodeSelectConfig {
10
10
  enabled?: boolean;
11
11
  apiBase?: string | null;
12
12
  sessionId?: string | null;
13
+ webUiUrl?: string | null;
13
14
  };
14
15
  }
15
16
 
@@ -20,10 +21,16 @@ export interface SparkeCodeSelectProps {
20
21
  sparkecoderEnabled?: boolean;
21
22
  strategy?: 'afterInteractive' | 'lazyOnload' | 'beforeInteractive' | 'worker';
22
23
  /**
23
- * Override the Sparkecoder server URL.
24
+ * Override the Sparkecoder server URL (API).
24
25
  * Defaults to NEXT_PUBLIC_SPARKECODER_DEV_TOOLS_SERVER_URL or SPARKECODER_DEV_TOOLS_SERVER_URL env variable.
25
26
  */
26
27
  serverUrl?: string;
28
+ /**
29
+ * Override the Sparkecoder Web UI URL.
30
+ * Defaults to NEXT_PUBLIC_SPARKECODER_WEB_UI_URL or SPARKECODER_WEB_UI_URL env variable.
31
+ * Falls back to http://localhost:6969 if not set.
32
+ */
33
+ webUiUrl?: string;
27
34
  }
28
35
 
29
36
  export function SparkeCodeSelect(props: SparkeCodeSelectProps): JSX.Element | null;
package/next.js CHANGED
@@ -5,12 +5,16 @@ import { useEffect, useMemo } from 'react';
5
5
 
6
6
  const DEFAULT_SCRIPT_URL = 'https://unpkg.com/sparkecode-devtools/sparkecode-select.js';
7
7
 
8
- // Check for env variable to override the default Sparkecoder server URL
9
- // This needs to be prefixed with NEXT_PUBLIC_ to be available in the browser
8
+ // Check for env variables to override defaults
9
+ // These need to be prefixed with NEXT_PUBLIC_ to be available in the browser
10
10
  const ENV_SERVER_URL = typeof process !== 'undefined'
11
11
  ? (process.env.NEXT_PUBLIC_SPARKECODER_DEV_TOOLS_SERVER_URL || process.env.SPARKECODER_DEV_TOOLS_SERVER_URL)
12
12
  : undefined;
13
13
 
14
+ const ENV_WEB_UI_URL = typeof process !== 'undefined'
15
+ ? (process.env.NEXT_PUBLIC_SPARKECODER_WEB_UI_URL || process.env.SPARKECODER_WEB_UI_URL)
16
+ : undefined;
17
+
14
18
  export function SparkeCodeSelect({
15
19
  enabled = process.env.NODE_ENV !== 'production',
16
20
  scriptUrl = DEFAULT_SCRIPT_URL,
@@ -18,16 +22,18 @@ export function SparkeCodeSelect({
18
22
  sparkecoderEnabled = true,
19
23
  strategy = 'afterInteractive',
20
24
  serverUrl = ENV_SERVER_URL,
25
+ webUiUrl = ENV_WEB_UI_URL,
21
26
  }) {
22
27
  const mergedConfig = useMemo(() => ({
23
28
  ...(config || {}),
24
29
  sparkecoder: {
25
30
  enabled: sparkecoderEnabled,
26
- // Use serverUrl prop, which defaults to env variable if set
31
+ // Use props which default to env variables if set
27
32
  ...(serverUrl ? { apiBase: serverUrl } : {}),
33
+ ...(webUiUrl ? { webUiUrl } : {}),
28
34
  ...(config && config.sparkecoder ? config.sparkecoder : {}),
29
35
  },
30
- }), [config, sparkecoderEnabled, serverUrl]);
36
+ }), [config, sparkecoderEnabled, serverUrl, webUiUrl]);
31
37
 
32
38
  useEffect(() => {
33
39
  if (!enabled) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sparkecode-devtools",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "description": "Sparkecode Dev Tools for selecting and sharing components.",
5
5
  "type": "module",
6
6
  "main": "sparkecode-select.js",
@@ -25,6 +25,7 @@
25
25
  enabled: false,
26
26
  apiBase: null,
27
27
  sessionId: null,
28
+ webUiUrl: null, // Web UI URL (default: http://localhost:6969)
28
29
  },
29
30
  };
30
31
 
@@ -338,7 +339,7 @@
338
339
 
339
340
  async function runAgentWithPrompt(promptText) {
340
341
  const connection = await ensureSparkecoderConnection();
341
- if (!connection) return false;
342
+ if (!connection) return { success: false };
342
343
 
343
344
  try {
344
345
  // Post to agents run endpoint to actually run the agent
@@ -350,10 +351,13 @@
350
351
  body: JSON.stringify({ prompt: promptText }),
351
352
  }
352
353
  );
353
- return res.ok;
354
+ if (res.ok) {
355
+ return { success: true, sessionId: connection.sessionId };
356
+ }
357
+ return { success: false };
354
358
  } catch (e) {
355
359
  console.warn('[SparkeCode] Failed to run agent:', e.message);
356
- return false;
360
+ return { success: false };
357
361
  }
358
362
  }
359
363
 
@@ -461,87 +465,139 @@
461
465
  function createFloatingButton() {
462
466
  floatingButton = document.createElement('div');
463
467
  floatingButton.id = 'sparkecode-button';
464
- // Clean pointer icon + text
465
- floatingButton.innerHTML = `
466
- <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" style="flex-shrink: 0;">
467
- <path d="M3 1.5L3 12.5L6.5 9L10 14L12 13L8.5 8L13 7.5L3 1.5Z"/>
468
- </svg>
469
- <span style="font-weight: 500; font-size: 12px;">SparkeCode Select</span>
470
- `;
471
468
  floatingButton.style.cssText = `
472
469
  position: fixed;
473
470
  top: ${buttonPos.y}px;
474
471
  left: ${buttonPos.x}px;
475
472
  height: 32px;
476
- padding: 0 10px;
477
- gap: 6px;
478
473
  background: ${config.bgColor};
479
474
  border: 1px solid ${config.borderColor};
480
475
  border-radius: 6px;
481
476
  display: flex;
482
477
  align-items: center;
483
- justify-content: center;
484
- cursor: grab;
485
478
  z-index: 999998;
486
479
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
487
480
  transition: all 0.15s ease;
488
- color: ${config.primaryColor};
489
481
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
490
482
  user-select: none;
483
+ overflow: hidden;
491
484
  `;
492
- floatingButton.title = 'Click to toggle • Drag to move';
485
+
486
+ // Main button area (select toggle)
487
+ const mainBtn = document.createElement('div');
488
+ mainBtn.id = 'sparkecode-button-main';
489
+ mainBtn.innerHTML = `
490
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" style="flex-shrink: 0;">
491
+ <path d="M3 1.5L3 12.5L6.5 9L10 14L12 13L8.5 8L13 7.5L3 1.5Z"/>
492
+ </svg>
493
+ <span style="font-weight: 500; font-size: 12px;">SparkeCode Select</span>
494
+ `;
495
+ mainBtn.style.cssText = `
496
+ display: flex;
497
+ align-items: center;
498
+ gap: 6px;
499
+ padding: 0 10px;
500
+ height: 100%;
501
+ cursor: grab;
502
+ color: ${config.primaryColor};
503
+ `;
504
+ mainBtn.title = 'Click to toggle select mode • Drag to move';
505
+
506
+ // Expand button (show web UI)
507
+ const expandBtn = document.createElement('div');
508
+ expandBtn.id = 'sparkecode-button-expand';
509
+ expandBtn.innerHTML = `
510
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
511
+ <rect x="3" y="3" width="18" height="18" rx="2"/>
512
+ <path d="M9 3v18"/>
513
+ </svg>
514
+ `;
515
+ expandBtn.style.cssText = `
516
+ display: flex;
517
+ align-items: center;
518
+ justify-content: center;
519
+ width: 32px;
520
+ height: 100%;
521
+ cursor: pointer;
522
+ color: ${config.mutedColor};
523
+ border-left: 1px solid ${config.borderColor};
524
+ transition: background 0.15s, color 0.15s;
525
+ `;
526
+ expandBtn.title = 'Open SparkeCode panel';
527
+ expandBtn.addEventListener('mouseenter', () => {
528
+ expandBtn.style.background = 'rgba(255,255,255,0.05)';
529
+ expandBtn.style.color = config.primaryColor;
530
+ });
531
+ expandBtn.addEventListener('mouseleave', () => {
532
+ expandBtn.style.background = 'transparent';
533
+ expandBtn.style.color = config.mutedColor;
534
+ });
535
+ expandBtn.addEventListener('click', (e) => {
536
+ e.stopPropagation();
537
+ if (isWebFrameVisible()) {
538
+ hideWebFrame();
539
+ } else {
540
+ showWebFrame();
541
+ }
542
+ });
543
+
544
+ floatingButton.appendChild(mainBtn);
545
+ floatingButton.appendChild(expandBtn);
493
546
 
494
- // Drag handling
495
- floatingButton.addEventListener('mousedown', startDrag);
496
- floatingButton.addEventListener('touchstart', startDrag, { passive: false });
547
+ // Drag handling - only on main button area
548
+ mainBtn.addEventListener('mousedown', startDrag);
549
+ mainBtn.addEventListener('touchstart', startDrag, { passive: false });
497
550
 
498
551
  document.body.appendChild(floatingButton);
499
552
  }
500
553
 
501
554
  function startDrag(e) {
502
- if (e.target.closest('#sparkecode-button')) {
503
- const clientX = e.touches ? e.touches[0].clientX : e.clientX;
504
- const clientY = e.touches ? e.touches[0].clientY : e.clientY;
555
+ // Only handle drag on main button, not expand
556
+ if (e.target.closest('#sparkecode-button-expand')) return;
557
+
558
+ const clientX = e.touches ? e.touches[0].clientX : e.clientX;
559
+ const clientY = e.touches ? e.touches[0].clientY : e.clientY;
560
+
561
+ dragOffset.x = clientX - buttonPos.x;
562
+ dragOffset.y = clientY - buttonPos.y;
563
+ isDragging = false;
564
+
565
+ const startX = clientX;
566
+ const startY = clientY;
567
+ const mainBtn = document.getElementById('sparkecode-button-main');
568
+
569
+ const onMove = (e) => {
570
+ const moveX = e.touches ? e.touches[0].clientX : e.clientX;
571
+ const moveY = e.touches ? e.touches[0].clientY : e.clientY;
505
572
 
506
- dragOffset.x = clientX - buttonPos.x;
507
- dragOffset.y = clientY - buttonPos.y;
508
- isDragging = false;
573
+ // Only start dragging if moved more than 5px
574
+ if (!isDragging && (Math.abs(moveX - startX) > 5 || Math.abs(moveY - startY) > 5)) {
575
+ isDragging = true;
576
+ if (mainBtn) mainBtn.style.cursor = 'grabbing';
577
+ }
509
578
 
510
- const startX = clientX;
511
- const startY = clientY;
579
+ if (isDragging) {
580
+ e.preventDefault();
581
+ buttonPos.x = Math.max(0, Math.min(window.innerWidth - 200, moveX - dragOffset.x));
582
+ buttonPos.y = Math.max(0, Math.min(window.innerHeight - 50, moveY - dragOffset.y));
583
+ floatingButton.style.left = buttonPos.x + 'px';
584
+ floatingButton.style.top = buttonPos.y + 'px';
585
+ }
586
+ };
587
+
588
+ const onUp = () => {
589
+ document.removeEventListener('mousemove', onMove);
590
+ document.removeEventListener('mouseup', onUp);
591
+ document.removeEventListener('touchmove', onMove);
592
+ document.removeEventListener('touchend', onUp);
512
593
 
513
- const onMove = (e) => {
514
- const moveX = e.touches ? e.touches[0].clientX : e.clientX;
515
- const moveY = e.touches ? e.touches[0].clientY : e.clientY;
516
-
517
- // Only start dragging if moved more than 5px
518
- if (!isDragging && (Math.abs(moveX - startX) > 5 || Math.abs(moveY - startY) > 5)) {
519
- isDragging = true;
520
- floatingButton.style.cursor = 'grabbing';
521
- }
522
-
523
- if (isDragging) {
524
- e.preventDefault();
525
- buttonPos.x = Math.max(0, Math.min(window.innerWidth - 200, moveX - dragOffset.x));
526
- buttonPos.y = Math.max(0, Math.min(window.innerHeight - 50, moveY - dragOffset.y));
527
- floatingButton.style.left = buttonPos.x + 'px';
528
- floatingButton.style.top = buttonPos.y + 'px';
529
- }
530
- };
594
+ if (mainBtn) mainBtn.style.cursor = 'grab';
531
595
 
532
- const onUp = () => {
533
- document.removeEventListener('mousemove', onMove);
534
- document.removeEventListener('mouseup', onUp);
535
- document.removeEventListener('touchmove', onMove);
536
- document.removeEventListener('touchend', onUp);
537
-
538
- floatingButton.style.cursor = 'grab';
539
-
540
- // If not dragging, it was a click
541
- if (!isDragging) {
542
- toggleActive();
543
- }
544
- isDragging = false;
596
+ // If not dragging, it was a click - toggle select mode
597
+ if (!isDragging) {
598
+ toggleActive();
599
+ }
600
+ isDragging = false;
545
601
  };
546
602
 
547
603
  document.addEventListener('mousemove', onMove);
@@ -877,10 +933,15 @@
877
933
  hidePromptBar();
878
934
  showToast('Sending...');
879
935
 
880
- const success = await runAgentWithPrompt(fullPrompt);
936
+ const result = await runAgentWithPrompt(fullPrompt);
881
937
 
882
- if (success) {
938
+ if (result.success) {
883
939
  showToast('Sent ✓');
940
+ // Auto-show the web frame to see the agent working
941
+ if (result.sessionId) {
942
+ sparkecoderState.sessionId = result.sessionId;
943
+ showWebFrame();
944
+ }
884
945
  } else {
885
946
  await copyToClipboard(fullPrompt);
886
947
  showToast('Copied (no connection)');
@@ -890,10 +951,284 @@
890
951
  component: pendingComponentInfo?.name,
891
952
  fileName: pendingComponentInfo?.fileName,
892
953
  hasUserMessage: !!userMessage,
893
- success,
954
+ success: result.success,
955
+ });
956
+ }
957
+
958
+ // ============================================
959
+ // Floating Web UI Frame
960
+ // ============================================
961
+
962
+ const DEFAULT_WEB_UI_URL = 'http://localhost:6969';
963
+ let webFrame = null;
964
+ let webFrameCollapsed = false;
965
+ let webFramePos = { x: window.innerWidth - 420, y: 60 };
966
+ let webFrameSize = { width: 400, height: 600 };
967
+ let isWebFrameDragging = false;
968
+ let webFrameDragOffset = { x: 0, y: 0 };
969
+
970
+ function getWebUiUrl() {
971
+ return config.sparkecoder?.webUiUrl || DEFAULT_WEB_UI_URL;
972
+ }
973
+
974
+ function createWebFrame() {
975
+ if (webFrame) return;
976
+
977
+ webFrame = document.createElement('div');
978
+ webFrame.id = 'sparkecode-webframe';
979
+ webFrame.style.cssText = `
980
+ position: fixed;
981
+ top: ${webFramePos.y}px;
982
+ right: 16px;
983
+ width: ${webFrameSize.width}px;
984
+ height: ${webFrameSize.height}px;
985
+ background: ${config.bgColor};
986
+ border: 1px solid ${config.borderColor};
987
+ border-radius: 12px;
988
+ z-index: 999997;
989
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
990
+ display: flex;
991
+ flex-direction: column;
992
+ overflow: hidden;
993
+ opacity: 0;
994
+ transform: scale(0.95) translateY(10px);
995
+ transition: opacity 0.2s ease, transform 0.2s ease;
996
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
997
+ `;
998
+
999
+ // Header
1000
+ const header = document.createElement('div');
1001
+ header.id = 'sparkecode-webframe-header';
1002
+ header.style.cssText = `
1003
+ display: flex;
1004
+ align-items: center;
1005
+ justify-content: space-between;
1006
+ padding: 10px 12px;
1007
+ background: rgba(255,255,255,0.03);
1008
+ border-bottom: 1px solid ${config.borderColor};
1009
+ cursor: grab;
1010
+ user-select: none;
1011
+ `;
1012
+
1013
+ const title = document.createElement('div');
1014
+ title.style.cssText = `
1015
+ font-size: 12px;
1016
+ font-weight: 500;
1017
+ color: ${config.mutedColor};
1018
+ display: flex;
1019
+ align-items: center;
1020
+ gap: 6px;
1021
+ `;
1022
+ title.innerHTML = `
1023
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="${config.primaryColor}">
1024
+ <path d="M3 1.5L3 12.5L6.5 9L10 14L12 13L8.5 8L13 7.5L3 1.5Z"/>
1025
+ </svg>
1026
+ <span>SparkeCode</span>
1027
+ `;
1028
+
1029
+ const buttons = document.createElement('div');
1030
+ buttons.style.cssText = `display: flex; gap: 6px;`;
1031
+
1032
+ // Collapse button
1033
+ const collapseBtn = document.createElement('button');
1034
+ collapseBtn.id = 'sparkecode-webframe-collapse';
1035
+ collapseBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>`;
1036
+ collapseBtn.style.cssText = `
1037
+ width: 24px;
1038
+ height: 24px;
1039
+ border: none;
1040
+ background: transparent;
1041
+ color: ${config.mutedColor};
1042
+ cursor: pointer;
1043
+ border-radius: 4px;
1044
+ display: flex;
1045
+ align-items: center;
1046
+ justify-content: center;
1047
+ transition: background 0.15s;
1048
+ `;
1049
+ collapseBtn.addEventListener('mouseenter', () => collapseBtn.style.background = 'rgba(255,255,255,0.1)');
1050
+ collapseBtn.addEventListener('mouseleave', () => collapseBtn.style.background = 'transparent');
1051
+ collapseBtn.addEventListener('click', toggleWebFrameCollapse);
1052
+
1053
+ // Close button
1054
+ const closeBtn = document.createElement('button');
1055
+ closeBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>`;
1056
+ closeBtn.style.cssText = `
1057
+ width: 24px;
1058
+ height: 24px;
1059
+ border: none;
1060
+ background: transparent;
1061
+ color: ${config.mutedColor};
1062
+ cursor: pointer;
1063
+ border-radius: 4px;
1064
+ display: flex;
1065
+ align-items: center;
1066
+ justify-content: center;
1067
+ transition: background 0.15s;
1068
+ `;
1069
+ closeBtn.addEventListener('mouseenter', () => closeBtn.style.background = 'rgba(255,255,255,0.1)');
1070
+ closeBtn.addEventListener('mouseleave', () => closeBtn.style.background = 'transparent');
1071
+ closeBtn.addEventListener('click', hideWebFrame);
1072
+
1073
+ buttons.appendChild(collapseBtn);
1074
+ buttons.appendChild(closeBtn);
1075
+ header.appendChild(title);
1076
+ header.appendChild(buttons);
1077
+
1078
+ // Drag handling for header
1079
+ header.addEventListener('mousedown', startWebFrameDrag);
1080
+
1081
+ // Iframe container
1082
+ const iframeContainer = document.createElement('div');
1083
+ iframeContainer.id = 'sparkecode-webframe-content';
1084
+ iframeContainer.style.cssText = `
1085
+ flex: 1;
1086
+ overflow: hidden;
1087
+ background: #0A0A0A;
1088
+ `;
1089
+
1090
+ // Loading state
1091
+ const loading = document.createElement('div');
1092
+ loading.id = 'sparkecode-webframe-loading';
1093
+ loading.style.cssText = `
1094
+ position: absolute;
1095
+ inset: 0;
1096
+ display: flex;
1097
+ align-items: center;
1098
+ justify-content: center;
1099
+ color: ${config.mutedColor};
1100
+ font-size: 13px;
1101
+ `;
1102
+ loading.textContent = 'Loading...';
1103
+ iframeContainer.appendChild(loading);
1104
+
1105
+ // Iframe
1106
+ const iframe = document.createElement('iframe');
1107
+ iframe.id = 'sparkecode-webframe-iframe';
1108
+ iframe.style.cssText = `
1109
+ width: 100%;
1110
+ height: 100%;
1111
+ border: none;
1112
+ opacity: 0;
1113
+ transition: opacity 0.2s;
1114
+ `;
1115
+ iframe.addEventListener('load', () => {
1116
+ loading.style.display = 'none';
1117
+ iframe.style.opacity = '1';
1118
+ });
1119
+ iframeContainer.appendChild(iframe);
1120
+
1121
+ webFrame.appendChild(header);
1122
+ webFrame.appendChild(iframeContainer);
1123
+ document.body.appendChild(webFrame);
1124
+ }
1125
+
1126
+ function startWebFrameDrag(e) {
1127
+ if (e.target.closest('button')) return;
1128
+
1129
+ isWebFrameDragging = true;
1130
+ const rect = webFrame.getBoundingClientRect();
1131
+ webFrameDragOffset.x = e.clientX - rect.left;
1132
+ webFrameDragOffset.y = e.clientY - rect.top;
1133
+
1134
+ const header = document.getElementById('sparkecode-webframe-header');
1135
+ if (header) header.style.cursor = 'grabbing';
1136
+
1137
+ const onMove = (e) => {
1138
+ if (!isWebFrameDragging) return;
1139
+ const x = Math.max(0, Math.min(window.innerWidth - webFrameSize.width, e.clientX - webFrameDragOffset.x));
1140
+ const y = Math.max(0, Math.min(window.innerHeight - 100, e.clientY - webFrameDragOffset.y));
1141
+ webFrame.style.left = x + 'px';
1142
+ webFrame.style.right = 'auto';
1143
+ webFrame.style.top = y + 'px';
1144
+ webFramePos = { x, y };
1145
+ };
1146
+
1147
+ const onUp = () => {
1148
+ isWebFrameDragging = false;
1149
+ const header = document.getElementById('sparkecode-webframe-header');
1150
+ if (header) header.style.cursor = 'grab';
1151
+ document.removeEventListener('mousemove', onMove);
1152
+ document.removeEventListener('mouseup', onUp);
1153
+ };
1154
+
1155
+ document.addEventListener('mousemove', onMove);
1156
+ document.addEventListener('mouseup', onUp);
1157
+ }
1158
+
1159
+ function showWebFrame() {
1160
+ createWebFrame();
1161
+
1162
+ const iframe = document.getElementById('sparkecode-webframe-iframe');
1163
+ const loading = document.getElementById('sparkecode-webframe-loading');
1164
+
1165
+ // Build embed URL with session ID
1166
+ let url = getWebUiUrl();
1167
+ if (sparkecoderState.sessionId) {
1168
+ url += `/embed/${sparkecoderState.sessionId}`;
1169
+ } else {
1170
+ url += '/embed';
1171
+ }
1172
+
1173
+ if (iframe && iframe.src !== url) {
1174
+ if (loading) loading.style.display = 'flex';
1175
+ iframe.style.opacity = '0';
1176
+ iframe.src = url;
1177
+ }
1178
+
1179
+ webFrame.style.display = 'flex';
1180
+ webFrameCollapsed = false;
1181
+ updateWebFrameSize();
1182
+
1183
+ requestAnimationFrame(() => {
1184
+ webFrame.style.opacity = '1';
1185
+ webFrame.style.transform = 'scale(1) translateY(0)';
894
1186
  });
895
1187
  }
896
1188
 
1189
+ function hideWebFrame() {
1190
+ if (!webFrame) return;
1191
+
1192
+ webFrame.style.opacity = '0';
1193
+ webFrame.style.transform = 'scale(0.95) translateY(10px)';
1194
+
1195
+ setTimeout(() => {
1196
+ if (webFrame) webFrame.style.display = 'none';
1197
+ // Clear iframe to stop any running content
1198
+ const iframe = document.getElementById('sparkecode-webframe-iframe');
1199
+ if (iframe) iframe.src = 'about:blank';
1200
+ }, 200);
1201
+ }
1202
+
1203
+ function toggleWebFrameCollapse() {
1204
+ webFrameCollapsed = !webFrameCollapsed;
1205
+ updateWebFrameSize();
1206
+
1207
+ const collapseBtn = document.getElementById('sparkecode-webframe-collapse');
1208
+ if (collapseBtn) {
1209
+ collapseBtn.innerHTML = webFrameCollapsed
1210
+ ? `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>`
1211
+ : `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>`;
1212
+ }
1213
+ }
1214
+
1215
+ function updateWebFrameSize() {
1216
+ if (!webFrame) return;
1217
+
1218
+ const content = document.getElementById('sparkecode-webframe-content');
1219
+ if (webFrameCollapsed) {
1220
+ webFrame.style.height = '44px';
1221
+ if (content) content.style.display = 'none';
1222
+ } else {
1223
+ webFrame.style.height = webFrameSize.height + 'px';
1224
+ if (content) content.style.display = 'block';
1225
+ }
1226
+ }
1227
+
1228
+ function isWebFrameVisible() {
1229
+ return webFrame && webFrame.style.display !== 'none' && webFrame.style.opacity !== '0';
1230
+ }
1231
+
897
1232
  // ============================================
898
1233
  // Event Handlers
899
1234
  // ============================================
@@ -959,12 +1294,18 @@
959
1294
  page: getPagePath(),
960
1295
  });
961
1296
 
962
- // If Sparkecoder is enabled and connected, show the prompt bar
1297
+ // If Sparkecoder is enabled and connected, open embed with context
963
1298
  if (config.sparkecoder?.enabled) {
964
1299
  const connection = await ensureSparkecoderConnection();
965
1300
  if (connection) {
966
1301
  deactivate();
967
- showPromptBar(content, info);
1302
+
1303
+ // Send component context to pending-input so it appears in the embed's input
1304
+ await sendToSparkecoder(content);
1305
+ showToast(`Selected <${info?.name || 'Component'}>`);
1306
+
1307
+ // Open the embed panel
1308
+ showWebFrame();
968
1309
  return;
969
1310
  }
970
1311
  }