vibe-widget 0.2.3__py3-none-any.whl

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 (40) hide show
  1. vibe_widget/AppWrapper/AppWrapper.js +340 -0
  2. vibe_widget/AppWrapper/components/AuditNotice.js +81 -0
  3. vibe_widget/AppWrapper/components/EditPromptPanel.js +232 -0
  4. vibe_widget/AppWrapper/components/FloatingMenu.js +121 -0
  5. vibe_widget/AppWrapper/components/LoadingOverlay.js +37 -0
  6. vibe_widget/AppWrapper/components/ProgressMap.js +179 -0
  7. vibe_widget/AppWrapper/components/SandboxedRunner.js +101 -0
  8. vibe_widget/AppWrapper/components/SelectionOverlay.js +135 -0
  9. vibe_widget/AppWrapper/components/editor/AuditPanel.js +195 -0
  10. vibe_widget/AppWrapper/components/editor/CodeEditor.js +205 -0
  11. vibe_widget/AppWrapper/components/editor/EditorViewer.js +996 -0
  12. vibe_widget/AppWrapper/components/editor/MessageEditor.js +119 -0
  13. vibe_widget/AppWrapper/hooks/useKeyboardShortcuts.js +17 -0
  14. vibe_widget/AppWrapper/hooks/useModelSync.js +75 -0
  15. vibe_widget/AppWrapper/utils/dom.ts +116 -0
  16. vibe_widget/AppWrapper/utils/styles.ts +18 -0
  17. vibe_widget/AppWrapper.bundle.js +49007 -0
  18. vibe_widget/__init__.py +24 -0
  19. vibe_widget/api.py +139 -0
  20. vibe_widget/config.py +469 -0
  21. vibe_widget/core.py +1934 -0
  22. vibe_widget/llm/__init__.py +9 -0
  23. vibe_widget/llm/agentic.py +264 -0
  24. vibe_widget/llm/providers/__init__.py +3 -0
  25. vibe_widget/llm/providers/base.py +582 -0
  26. vibe_widget/llm/providers/openrouter_provider.py +211 -0
  27. vibe_widget/llm/tools/base.py +102 -0
  28. vibe_widget/llm/tools/code_tools.py +126 -0
  29. vibe_widget/llm/tools/data_tools.py +540 -0
  30. vibe_widget/llm/tools/execution_tools.py +340 -0
  31. vibe_widget/models_manifest.json +15 -0
  32. vibe_widget/themes.py +928 -0
  33. vibe_widget/utils/audit_store.py +279 -0
  34. vibe_widget/utils/code_parser.py +272 -0
  35. vibe_widget/utils/util.py +92 -0
  36. vibe_widget/utils/widget_store.py +421 -0
  37. vibe_widget-0.2.3.dist-info/METADATA +94 -0
  38. vibe_widget-0.2.3.dist-info/RECORD +40 -0
  39. vibe_widget-0.2.3.dist-info/WHEEL +4 -0
  40. vibe_widget-0.2.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,340 @@
1
+ import * as React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import htm from "htm";
4
+
5
+ import { ensureGlobalStyles } from "./utils/styles";
6
+ import SandboxedRunner from "./components/SandboxedRunner";
7
+ import FloatingMenu from "./components/FloatingMenu";
8
+ import LoadingOverlay from "./components/LoadingOverlay";
9
+ import SelectionOverlay from "./components/SelectionOverlay";
10
+ import EditPromptPanel from "./components/EditPromptPanel";
11
+ import AuditNotice from "./components/AuditNotice";
12
+ import EditorViewer from "./components/editor/EditorViewer";
13
+ import useModelSync from "./hooks/useModelSync";
14
+ import useKeyboardShortcuts from "./hooks/useKeyboardShortcuts";
15
+
16
+ const html = htm.bind(React.createElement);
17
+
18
+ ensureGlobalStyles();
19
+
20
+ const AUDIT_ACK_KEY = "vibe_widget_audit_ack";
21
+ const AUDIT_AUTORUN_KEY = "vibe_widget_audit_autorun";
22
+
23
+ function AppWrapper({ model }) {
24
+ const {
25
+ status,
26
+ logs,
27
+ code,
28
+ errorMessage,
29
+ auditStatus,
30
+ auditResponse,
31
+ auditError,
32
+ auditApplyStatus,
33
+ auditApplyResponse,
34
+ auditApplyError,
35
+ executionMode,
36
+ executionApproved
37
+ } = useModelSync(model);
38
+ const [isMenuOpen, setMenuOpen] = React.useState(false);
39
+ const [grabMode, setGrabMode] = React.useState(null);
40
+ const [promptCache, setPromptCache] = React.useState({});
41
+ const [showSource, setShowSource] = React.useState(false);
42
+ const [sourceError, setSourceError] = React.useState("");
43
+ const [renderCode, setRenderCode] = React.useState(code || "");
44
+ const [lastGoodCode, setLastGoodCode] = React.useState(code || "");
45
+ const [applyState, setApplyState] = React.useState({
46
+ pending: false,
47
+ previousCode: "",
48
+ nextCode: ""
49
+ });
50
+ const containerRef = React.useRef(null);
51
+ const [minHeight, setMinHeight] = React.useState(0);
52
+ const [hasAuditAck, setHasAuditAck] = React.useState(() => {
53
+ try {
54
+ return sessionStorage.getItem(AUDIT_ACK_KEY) === "true";
55
+ } catch (err) {
56
+ return false;
57
+ }
58
+ });
59
+ const [showAudit, setShowAudit] = React.useState(false);
60
+ const [hasAutoRunAudit, setHasAutoRunAudit] = React.useState(() => {
61
+ try {
62
+ return sessionStorage.getItem(AUDIT_AUTORUN_KEY) === "true";
63
+ } catch (err) {
64
+ return false;
65
+ }
66
+ });
67
+
68
+ const handleGrabStart = () => {
69
+ setMenuOpen(false);
70
+ setGrabMode("selecting");
71
+ };
72
+
73
+ const handleViewSource = () => {
74
+ setMenuOpen(false);
75
+ setShowSource(true);
76
+ };
77
+
78
+ const handleElementSelect = (elementDescription, elementBounds) => {
79
+ const elementKey = `${elementDescription.tag}-${elementDescription.classes}-${elementDescription.text?.slice(0, 20)}`;
80
+ setGrabMode({ element: elementDescription, bounds: elementBounds, elementKey });
81
+ };
82
+
83
+ const handleEditSubmit = (prompt) => {
84
+ model.set("grab_edit_request", {
85
+ element: grabMode.element,
86
+ prompt: prompt,
87
+ });
88
+ model.save_changes();
89
+ setPromptCache((prev) => ({ ...prev, [grabMode.elementKey]: prompt }));
90
+ setGrabMode(null);
91
+ };
92
+
93
+ const handleCancel = (currentPrompt) => {
94
+ if (grabMode?.elementKey && currentPrompt) {
95
+ setPromptCache((prev) => ({ ...prev, [grabMode.elementKey]: currentPrompt }));
96
+ }
97
+ setGrabMode(null);
98
+ };
99
+
100
+ const isLoading = status === "generating";
101
+ const hasCode = renderCode && renderCode.length > 0;
102
+ const approvalMode = executionMode === "approve";
103
+ const isApproved = executionApproved || !approvalMode;
104
+ const shouldRenderWidget = hasCode && isApproved;
105
+
106
+ useKeyboardShortcuts({ isLoading, hasCode, grabMode, onGrabStart: handleGrabStart });
107
+ React.useEffect(() => {
108
+ if (!containerRef.current || typeof ResizeObserver === "undefined") return;
109
+ const observer = new ResizeObserver((entries) => {
110
+ for (const entry of entries) {
111
+ const height = Math.round(entry.contentRect.height || 0);
112
+ if (height > 50) {
113
+ setMinHeight(height);
114
+ }
115
+ }
116
+ });
117
+ observer.observe(containerRef.current);
118
+ return () => observer.disconnect();
119
+ }, []);
120
+ React.useEffect(() => {
121
+ if (approvalMode) {
122
+ setShowAudit(false);
123
+ return;
124
+ }
125
+ if (!hasAuditAck && !isLoading && hasCode) {
126
+ setShowAudit(true);
127
+ }
128
+ }, [approvalMode, hasAuditAck, isLoading, hasCode]);
129
+
130
+ React.useEffect(() => {
131
+ if (hasAutoRunAudit) return;
132
+ if (!approvalMode) return;
133
+ if (status !== "ready") return;
134
+ if (!code) return;
135
+ if (auditStatus === "running") return;
136
+ handleAuditRequest("fast");
137
+ try {
138
+ sessionStorage.setItem(AUDIT_AUTORUN_KEY, "true");
139
+ } catch (err) {
140
+ // Ignore storage failures and only track in memory.
141
+ }
142
+ setHasAutoRunAudit(true);
143
+ }, [approvalMode, hasAutoRunAudit, status, code, auditStatus]);
144
+
145
+ React.useEffect(() => {
146
+ if (!applyState.pending) return;
147
+ if (errorMessage) {
148
+ setApplyState({ pending: false, previousCode: "", nextCode: "" });
149
+ setSourceError(errorMessage);
150
+ setShowSource(false);
151
+ return;
152
+ }
153
+ if (code === applyState.nextCode && status === "ready") {
154
+ setApplyState({ pending: false, previousCode: "", nextCode: "" });
155
+ setRenderCode(code);
156
+ setLastGoodCode(code);
157
+ setSourceError("");
158
+ }
159
+ }, [applyState, code, errorMessage, model]);
160
+
161
+ React.useEffect(() => {
162
+ if (!sourceError || status === "generating" || showSource) return;
163
+ if (renderCode !== code) {
164
+ setShowSource(true);
165
+ }
166
+ }, [sourceError, status, showSource, renderCode, code]);
167
+
168
+ React.useEffect(() => {
169
+ if (!approvalMode) {
170
+ return;
171
+ }
172
+ if (executionApproved) {
173
+ setShowSource(false);
174
+ return;
175
+ }
176
+ if (status === "ready" && hasCode) {
177
+ setShowSource(true);
178
+ }
179
+ }, [approvalMode, executionApproved, status, hasCode]);
180
+
181
+ React.useEffect(() => {
182
+ if (applyState.pending) return;
183
+ if (status !== "ready") return;
184
+ if (!code) return;
185
+ setRenderCode(code);
186
+ setLastGoodCode(code);
187
+ }, [applyState.pending, status, code]);
188
+
189
+ React.useEffect(() => {
190
+ if (!renderCode && lastGoodCode) {
191
+ setRenderCode(lastGoodCode);
192
+ }
193
+ }, [renderCode, lastGoodCode]);
194
+
195
+ const handleApplySource = (payload) => {
196
+ if (payload && payload.type === "audit_apply") {
197
+ model.set("audit_apply_request", {
198
+ changes: payload.changes || [],
199
+ base_code: payload.baseCode || ""
200
+ });
201
+ model.save_changes();
202
+ setShowSource(false);
203
+ return;
204
+ }
205
+ const nextCode = payload;
206
+ setApplyState({
207
+ pending: true,
208
+ previousCode: code,
209
+ nextCode
210
+ });
211
+ setShowSource(false);
212
+ model.set("error_message", "");
213
+ model.set("code", nextCode);
214
+ model.save_changes();
215
+ };
216
+
217
+ const handleAuditRequest = (level) => {
218
+ model.set("audit_request", {
219
+ level: level || "fast",
220
+ request_id: `${Date.now()}-${Math.random().toString(16).slice(2)}`
221
+ });
222
+ model.save_changes();
223
+ };
224
+
225
+ const handleApproveRun = () => {
226
+ model.set("execution_approved", true);
227
+ model.save_changes();
228
+ setShowSource(false);
229
+ setShowAudit(false);
230
+ };
231
+
232
+ const auditReport = auditResponse?.report_yaml || "";
233
+ const auditMeta = auditResponse && !auditResponse.error ? auditResponse : null;
234
+ const auditData = auditResponse?.report || null;
235
+ const auditConcerns = auditData?.fast_audit?.concerns || [];
236
+ const highAuditCount = auditConcerns.filter((concern) => concern?.impact === "high").length;
237
+
238
+ const handleAuditAccept = () => {
239
+ try {
240
+ sessionStorage.setItem(AUDIT_ACK_KEY, "true");
241
+ } catch (err) {
242
+ // Allow dismissal without persistence if session storage is blocked.
243
+ }
244
+ setHasAuditAck(true);
245
+ setShowAudit(false);
246
+ };
247
+
248
+ return html`
249
+ <div
250
+ class="vibe-container"
251
+ ref=${containerRef}
252
+ style=${{
253
+ position: "relative",
254
+ width: "100%",
255
+ minHeight: minHeight ? `${minHeight}px` : "220px"
256
+ }}
257
+ >
258
+ ${shouldRenderWidget && html`
259
+ <div style=${{
260
+ opacity: isLoading ? 0.4 : 1,
261
+ pointerEvents: isLoading ? "none" : "auto",
262
+ transition: "opacity 0.3s ease",
263
+ }}>
264
+ <${SandboxedRunner} code=${renderCode} model=${model} />
265
+ </div>
266
+ `}
267
+
268
+ ${isLoading && html`
269
+ <${LoadingOverlay}
270
+ logs=${logs}
271
+ hasExistingWidget=${hasCode}
272
+ />
273
+ `}
274
+
275
+ ${!isLoading && shouldRenderWidget && html`
276
+ <${FloatingMenu}
277
+ isOpen=${isMenuOpen}
278
+ onToggle=${() => setMenuOpen(!isMenuOpen)}
279
+ onGrabModeStart=${handleGrabStart}
280
+ onViewSource=${handleViewSource}
281
+ highAuditCount=${highAuditCount}
282
+ isEditMode=${!!grabMode}
283
+ />
284
+ `}
285
+
286
+ ${grabMode === "selecting" && html`
287
+ <${SelectionOverlay}
288
+ onElementSelect=${handleElementSelect}
289
+ onCancel=${handleCancel}
290
+ />
291
+ `}
292
+
293
+ ${grabMode && grabMode !== "selecting" && html`
294
+ <${EditPromptPanel}
295
+ elementBounds=${grabMode.bounds}
296
+ elementDescription=${grabMode.element}
297
+ initialPrompt=${promptCache[grabMode.elementKey] || ""}
298
+ onSubmit=${handleEditSubmit}
299
+ onCancel=${handleCancel}
300
+ />
301
+ `}
302
+
303
+ ${showAudit && html`
304
+ <${AuditNotice} onAccept=${handleAuditAccept} />
305
+ `}
306
+
307
+ ${showSource && html`
308
+ <${EditorViewer}
309
+ code=${code}
310
+ errorMessage=${sourceError}
311
+ auditStatus=${auditStatus}
312
+ auditReport=${auditReport}
313
+ auditError=${auditError || auditResponse?.error}
314
+ auditMeta=${auditMeta}
315
+ auditData=${auditData}
316
+ auditApplyStatus=${auditApplyStatus}
317
+ auditApplyResponse=${auditApplyResponse}
318
+ auditApplyError=${auditApplyError}
319
+ onAudit=${handleAuditRequest}
320
+ onApply=${handleApplySource}
321
+ onClose=${() => setShowSource(false)}
322
+ approvalMode=${approvalMode}
323
+ isApproved=${isApproved}
324
+ onApprove=${handleApproveRun}
325
+ />
326
+ `}
327
+ </div>
328
+ `;
329
+ }
330
+
331
+ let rootInstance = null;
332
+
333
+ function render({ model, el }) {
334
+ if (!rootInstance) {
335
+ rootInstance = createRoot(el);
336
+ }
337
+ rootInstance.render(html`<${AppWrapper} model=${model} />`);
338
+ }
339
+
340
+ export default { render };
@@ -0,0 +1,81 @@
1
+ import * as React from "react";
2
+ import htm from "htm";
3
+
4
+ const html = htm.bind(React.createElement);
5
+
6
+ export default function AuditNotice({ onAccept }) {
7
+ return html`
8
+ <div class="audit-overlay">
9
+ <style>
10
+ .audit-overlay {
11
+ position: absolute;
12
+ inset: 0;
13
+ z-index: 1200;
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ background: rgba(6, 6, 6, 0.72);
18
+ backdrop-filter: blur(4px);
19
+ }
20
+ .audit-card {
21
+ width: min(520px, 92%);
22
+ background: #0f172a;
23
+ color: #e2e8f0;
24
+ border: 2px solid rgba(248, 113, 113, 0.65);
25
+ border-radius: 12px;
26
+ padding: 20px;
27
+ box-shadow: 0 18px 45px rgba(0, 0, 0, 0.4);
28
+ font-family: "JetBrains Mono", "Space Mono", ui-monospace, SFMono-Regular,
29
+ Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
30
+ }
31
+ .audit-title {
32
+ font-size: 14px;
33
+ text-transform: uppercase;
34
+ letter-spacing: 0.08em;
35
+ color: #fca5a5;
36
+ margin-bottom: 12px;
37
+ }
38
+ .audit-body {
39
+ font-size: 13px;
40
+ line-height: 1.5;
41
+ color: #e2e8f0;
42
+ margin-bottom: 16px;
43
+ }
44
+ .audit-body strong {
45
+ color: #fef2f2;
46
+ }
47
+ .audit-actions {
48
+ display: flex;
49
+ justify-content: flex-end;
50
+ gap: 10px;
51
+ }
52
+ .audit-accept {
53
+ background: #f97316;
54
+ color: #0b0b0b;
55
+ border: none;
56
+ border-radius: 8px;
57
+ padding: 8px 14px;
58
+ font-size: 12px;
59
+ font-weight: 600;
60
+ cursor: pointer;
61
+ }
62
+ .audit-accept:hover {
63
+ background: #fb923c;
64
+ }
65
+ </style>
66
+ <div class="audit-card" role="dialog" aria-live="polite">
67
+ <div class="audit-title">Audit Required</div>
68
+ <div class="audit-body">
69
+ Vibe widgets are <strong>LLM-generated code</strong>. Before using results,
70
+ review the widget for correctness, data handling, and safety.
71
+ By continuing, you acknowledge the need to audit outputs.
72
+ </div>
73
+ <div class="audit-actions">
74
+ <button class="audit-accept" onClick=${onAccept}>
75
+ I Understand
76
+ </button>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ `;
81
+ }
@@ -0,0 +1,232 @@
1
+ import * as React from "react";
2
+ import htm from "htm";
3
+
4
+ const html = htm.bind(React.createElement);
5
+
6
+ export default function EditPromptPanel({ elementBounds, elementDescription, initialPrompt, onSubmit, onCancel }) {
7
+ const [prompt, setPrompt] = React.useState(initialPrompt || "");
8
+ const panelRef = React.useRef(null);
9
+ const textareaRef = React.useRef(null);
10
+ const [position, setPosition] = React.useState({ top: 0, left: 0 });
11
+ const [textareaHeight, setTextareaHeight] = React.useState(40);
12
+ const [panelHovered, setPanelHovered] = React.useState(false);
13
+ const [buttonHovered, setButtonHovered] = React.useState(false);
14
+
15
+ const PANEL_WIDTH = 320;
16
+ const MIN_TEXTAREA_HEIGHT = 40;
17
+ const MAX_TEXTAREA_HEIGHT = 288;
18
+ const LINE_HEIGHT = 24;
19
+ const PADDING = 12;
20
+ const GAP = 8;
21
+
22
+ const getButtonOpacity = () => {
23
+ if (buttonHovered) return 1;
24
+ if (panelHovered) return 0.3;
25
+ return 0.1;
26
+ };
27
+
28
+ React.useEffect(() => {
29
+ const handleClickOutside = (e) => {
30
+ if (panelRef.current && !panelRef.current.contains(e.target)) {
31
+ onCancel(prompt);
32
+ }
33
+ };
34
+ document.addEventListener("mousedown", handleClickOutside);
35
+ return () => document.removeEventListener("mousedown", handleClickOutside);
36
+ }, [onCancel, prompt]);
37
+
38
+ React.useEffect(() => {
39
+ if (textareaRef.current) {
40
+ textareaRef.current.style.height = MIN_TEXTAREA_HEIGHT + "px";
41
+ const scrollHeight = textareaRef.current.scrollHeight;
42
+ const newHeight = Math.min(Math.max(scrollHeight, MIN_TEXTAREA_HEIGHT), MAX_TEXTAREA_HEIGHT);
43
+ setTextareaHeight(newHeight);
44
+ textareaRef.current.style.height = newHeight + "px";
45
+ }
46
+ }, [prompt]);
47
+
48
+ React.useEffect(() => {
49
+ if (!elementBounds) return;
50
+
51
+ const viewport = {
52
+ width: window.innerWidth,
53
+ height: window.innerHeight,
54
+ };
55
+
56
+ const panelHeight = textareaHeight + 56;
57
+
58
+ const elCenterX = elementBounds.left + elementBounds.width / 2;
59
+ const elCenterY = elementBounds.top + elementBounds.height / 2;
60
+
61
+ let top = elementBounds.bottom + GAP;
62
+ let left = elCenterX - PANEL_WIDTH / 2;
63
+
64
+ if (top + panelHeight + PADDING > viewport.height) {
65
+ top = elementBounds.top - panelHeight - GAP;
66
+ }
67
+
68
+ if (top < PADDING) {
69
+ top = Math.max(PADDING, elCenterY - panelHeight / 2);
70
+ if (elementBounds.right + GAP + PANEL_WIDTH + PADDING < viewport.width) {
71
+ left = elementBounds.right + GAP;
72
+ } else {
73
+ left = elementBounds.left - PANEL_WIDTH - GAP;
74
+ }
75
+ }
76
+
77
+ left = Math.max(PADDING, Math.min(left, viewport.width - PANEL_WIDTH - PADDING));
78
+ top = Math.max(PADDING, Math.min(top, viewport.height - panelHeight - PADDING));
79
+
80
+ setPosition({ top, left });
81
+ }, [elementBounds, textareaHeight]);
82
+
83
+ const handleKeyDown = (e) => {
84
+ if (e.key === "Enter") {
85
+ if (e.shiftKey) {
86
+ e.stopPropagation();
87
+ } else if (prompt.trim()) {
88
+ e.preventDefault();
89
+ e.stopPropagation();
90
+ onSubmit(prompt);
91
+ }
92
+ } else if (e.key === "Escape") {
93
+ onCancel(prompt);
94
+ }
95
+ };
96
+
97
+ return html`
98
+ <div>
99
+ ${elementBounds && html`
100
+ <div class="selected-element-highlight" style=${{
101
+ position: "fixed",
102
+ left: elementBounds.left + "px",
103
+ top: elementBounds.top + "px",
104
+ width: elementBounds.width + "px",
105
+ height: elementBounds.height + "px",
106
+ outline: "2px solid #f97316",
107
+ outlineOffset: "0px",
108
+ background: "rgba(249, 115, 22, 0.1)",
109
+ borderRadius: "2px",
110
+ pointerEvents: "none",
111
+ zIndex: 10000,
112
+ boxSizing: "border-box",
113
+ }}>
114
+ ${elementDescription?.tag && html`
115
+ <div style=${{
116
+ position: "absolute",
117
+ top: "-18px",
118
+ left: "-2px",
119
+ background: "#f97316",
120
+ color: "white",
121
+ fontSize: "10px",
122
+ fontWeight: 600,
123
+ padding: "1px 5px",
124
+ borderRadius: "3px 3px 0 0",
125
+ whiteSpace: "nowrap",
126
+ fontFamily: "ui-monospace, monospace",
127
+ }}>
128
+ ${elementDescription.tag.toLowerCase()}
129
+ </div>
130
+ `}
131
+ </div>
132
+ `}
133
+ <div
134
+ ref=${panelRef}
135
+ class="edit-panel"
136
+ onMouseEnter=${() => setPanelHovered(true)}
137
+ onMouseLeave=${() => setPanelHovered(false)}
138
+ style=${{
139
+ position: "fixed",
140
+ top: position.top + "px",
141
+ left: position.left + "px",
142
+ width: PANEL_WIDTH + "px",
143
+ background: "rgba(26, 26, 26, 0.95)",
144
+ border: "1px solid rgba(255, 255, 255, 0.15)",
145
+ borderRadius: "10px",
146
+ padding: "8px",
147
+ zIndex: 10001,
148
+ backdropFilter: "blur(8px)",
149
+ boxShadow: "0 4px 20px rgba(0, 0, 0, 0.4)",
150
+ display: "flex",
151
+ flexDirection: "column",
152
+ gap: "6px",
153
+ }}
154
+ >
155
+ <div style=${{ position: "relative", width: "100%" }}>
156
+ <textarea
157
+ ref=${textareaRef}
158
+ value=${prompt}
159
+ onInput=${(e) => setPrompt(e.target.value)}
160
+ onKeyDown=${handleKeyDown}
161
+ placeholder="What would you like to change?"
162
+ rows="1"
163
+ style=${{
164
+ width: "100%",
165
+ minHeight: MIN_TEXTAREA_HEIGHT + "px",
166
+ maxHeight: MAX_TEXTAREA_HEIGHT + "px",
167
+ padding: "10px 50px 10px 12px",
168
+ borderRadius: "6px",
169
+ border: "1px solid rgba(255,255,255,0.1)",
170
+ background: "rgba(255,255,255,0.05)",
171
+ color: "white",
172
+ fontSize: "13px",
173
+ lineHeight: "20px",
174
+ outline: "none",
175
+ resize: "none",
176
+ overflowY: textareaHeight >= MAX_TEXTAREA_HEIGHT ? "auto" : "hidden",
177
+ boxSizing: "border-box",
178
+ fontFamily: "inherit",
179
+ }}
180
+ autoFocus
181
+ />
182
+ <button
183
+ onClick=${() => prompt.trim() ? onSubmit(prompt) : onCancel(prompt)}
184
+ onMouseEnter=${() => setButtonHovered(true)}
185
+ onMouseLeave=${() => setButtonHovered(false)}
186
+ style=${{
187
+ position: "absolute",
188
+ top: "6px",
189
+ right: textareaHeight >= MAX_TEXTAREA_HEIGHT ? "16px" : "8px",
190
+ width: "28px",
191
+ height: "28px",
192
+ borderRadius: "6px",
193
+ background: prompt.trim()
194
+ ? "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
195
+ : "rgba(255,255,255,0.15)",
196
+ color: "white",
197
+ border: "none",
198
+ cursor: "pointer",
199
+ fontSize: "14px",
200
+ display: "flex",
201
+ alignItems: "center",
202
+ justifyContent: "center",
203
+ transition: "opacity 0.2s, background 0.2s",
204
+ padding: 0,
205
+ opacity: getButtonOpacity(),
206
+ }}
207
+ title=${prompt.trim() ? "Send (Enter)" : "Cancel (Esc)"}
208
+ >
209
+ ${prompt.trim() ? html`
210
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
211
+ <line x1="12" y1="19" x2="12" y2="5"></line>
212
+ <polyline points="5 12 12 5 19 12"></polyline>
213
+ </svg>
214
+ ` : html`
215
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
216
+ <line x1="18" y1="6" x2="6" y2="18"></line>
217
+ <line x1="6" y1="6" x2="18" y2="18"></line>
218
+ </svg>
219
+ `}
220
+ </button>
221
+ </div>
222
+ <div style=${{
223
+ fontSize: "11px",
224
+ color: "rgba(255,255,255,0.4)",
225
+ paddingLeft: "4px",
226
+ }}>
227
+ Enter to send, Shift+Enter for new line
228
+ </div>
229
+ </div>
230
+ </div>
231
+ `;
232
+ }