@volter-ai-dev/supercode-ui 0.1.21 → 0.1.22
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 +3 -2
- package/components.mjs +63 -19
- package/composer.mjs +32 -10
- package/embed.mjs +63 -19
- package/messenger.mjs +63 -19
- package/package.json +1 -1
- package/styles.css +1 -0
package/README.md
CHANGED
|
@@ -126,8 +126,9 @@ The browser cannot supply locators, credentials, policy, environment variables,
|
|
|
126
126
|
materialization paths. Session keys and target harnesses must be revalidated by the host.
|
|
127
127
|
`pickContext` is optional and host-owned: the default composer shows one attachment control when
|
|
128
128
|
this callback exists and accepts either typed text context (`detail`) or native image inputs
|
|
129
|
-
(`url`). Images can also be pasted directly
|
|
130
|
-
|
|
129
|
+
(`url`). Images can also be pasted or dropped directly, and an image-only turn is sent without
|
|
130
|
+
inventing fallback prompt text. The composer bounds images to four supported browser formats at
|
|
131
|
+
5 MB each, previews them locally, and keeps both attachment kinds associated with queued
|
|
131
132
|
messages, retry, edit, and new-chat recovery. The host still chooses how explicit file selection
|
|
132
133
|
is exposed and must revalidate every returned item.
|
|
133
134
|
|
package/components.mjs
CHANGED
|
@@ -967,6 +967,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
967
967
|
const [queue, setQueue] = useState((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
|
|
968
968
|
const [dispatching, setDispatching] = useState(false);
|
|
969
969
|
const [picking, setPicking] = useState(false);
|
|
970
|
+
const [dragging, setDragging] = useState(false);
|
|
970
971
|
const [pickerError, setPickerError] = useState(null);
|
|
971
972
|
const textarea = useRef(null);
|
|
972
973
|
useAutosizeTextarea(textarea, draft);
|
|
@@ -1032,13 +1033,17 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1032
1033
|
});
|
|
1033
1034
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
1034
1035
|
};
|
|
1035
|
-
const
|
|
1036
|
-
const
|
|
1037
|
-
if (!
|
|
1038
|
-
|
|
1036
|
+
const addImageFiles = (value, source) => {
|
|
1037
|
+
const allFiles = Array.from(value ?? []);
|
|
1038
|
+
if (!allFiles.length) return false;
|
|
1039
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
1040
|
+
if (files.length !== allFiles.length) {
|
|
1041
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1039
1044
|
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
1040
1045
|
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1041
|
-
return;
|
|
1046
|
+
return true;
|
|
1042
1047
|
}
|
|
1043
1048
|
setPicking(true);
|
|
1044
1049
|
setPickerError(null);
|
|
@@ -1046,11 +1051,19 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1046
1051
|
const next = mergeImages(current, picked);
|
|
1047
1052
|
remember(draft, context, next, queue);
|
|
1048
1053
|
return next;
|
|
1049
|
-
}), (error) => setPickerError(error instanceof Error ? error.message :
|
|
1054
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
1055
|
+
return true;
|
|
1056
|
+
};
|
|
1057
|
+
const pasteImages = (event) => {
|
|
1058
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
1059
|
+
};
|
|
1060
|
+
const dropImages = (event) => {
|
|
1061
|
+
setDragging(false);
|
|
1062
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
1050
1063
|
};
|
|
1051
1064
|
const send = () => {
|
|
1052
1065
|
const text = draft.trim();
|
|
1053
|
-
if (!text) return;
|
|
1066
|
+
if (!text && !images.length) return;
|
|
1054
1067
|
const message = { text, context, images };
|
|
1055
1068
|
if (queuesNewMessage) updateQueue((items) => [...items, message]);
|
|
1056
1069
|
else if (state.canSend) {
|
|
@@ -1071,7 +1084,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1071
1084
|
] }),
|
|
1072
1085
|
queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
|
|
1073
1086
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
1074
|
-
item.text,
|
|
1087
|
+
item.text || "Image attachment",
|
|
1075
1088
|
item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
1076
1089
|
item.context.length + item.images.length,
|
|
1077
1090
|
" attached"
|
|
@@ -1091,7 +1104,16 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1091
1104
|
return next;
|
|
1092
1105
|
}) }),
|
|
1093
1106
|
pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
1094
|
-
/* @__PURE__ */ jsxs3("div", { class:
|
|
1107
|
+
/* @__PURE__ */ jsxs3("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
1108
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
1109
|
+
event.preventDefault();
|
|
1110
|
+
setDragging(true);
|
|
1111
|
+
}
|
|
1112
|
+
}, onDragOver: (event) => {
|
|
1113
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
1114
|
+
}, onDragLeave: (event) => {
|
|
1115
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1116
|
+
}, onDrop: dropImages, children: [
|
|
1095
1117
|
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
1096
1118
|
/* @__PURE__ */ jsx3("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" : pendingStatus === "failed" ? "Retry or edit the unsent message\u2026" : pendingStatus === "editing" ? "Edit and resend\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
|
|
1097
1119
|
const value = event.currentTarget.value;
|
|
@@ -1105,7 +1127,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1105
1127
|
} }),
|
|
1106
1128
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
1107
1129
|
state.busy ? /* @__PURE__ */ jsx3("button", { class: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: /* @__PURE__ */ jsx3(UiIcon, { name: "stop", size: 15 }) }) : null,
|
|
1108
|
-
/* @__PURE__ */ jsx3("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx3(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
|
|
1130
|
+
/* @__PURE__ */ jsx3("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() && !images.length || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx3(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
|
|
1109
1131
|
] })
|
|
1110
1132
|
] })
|
|
1111
1133
|
] });
|
|
@@ -2140,6 +2162,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2140
2162
|
const [images, setImages] = useState4(remembered.images ?? []);
|
|
2141
2163
|
const [starting, setStarting] = useState4(null);
|
|
2142
2164
|
const [picking, setPicking] = useState4(false);
|
|
2165
|
+
const [dragging, setDragging] = useState4(false);
|
|
2143
2166
|
const [pickerError, setPickerError] = useState4(null);
|
|
2144
2167
|
const startSequence = useRef5(0);
|
|
2145
2168
|
const textarea = useRef5(null);
|
|
@@ -2184,13 +2207,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2184
2207
|
});
|
|
2185
2208
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
2186
2209
|
};
|
|
2187
|
-
const
|
|
2188
|
-
const
|
|
2189
|
-
if (!
|
|
2190
|
-
|
|
2210
|
+
const addImageFiles = (value, source) => {
|
|
2211
|
+
const allFiles = Array.from(value ?? []);
|
|
2212
|
+
if (!allFiles.length) return false;
|
|
2213
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
2214
|
+
if (files.length !== allFiles.length) {
|
|
2215
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
2216
|
+
return true;
|
|
2217
|
+
}
|
|
2191
2218
|
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
2192
2219
|
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2193
|
-
return;
|
|
2220
|
+
return true;
|
|
2194
2221
|
}
|
|
2195
2222
|
setPicking(true);
|
|
2196
2223
|
setPickerError(null);
|
|
@@ -2198,11 +2225,19 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2198
2225
|
const next = mergeImages(current, picked);
|
|
2199
2226
|
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2200
2227
|
return next;
|
|
2201
|
-
}), (error) => setPickerError(error instanceof Error ? error.message :
|
|
2228
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
2229
|
+
return true;
|
|
2230
|
+
};
|
|
2231
|
+
const pasteImages = (event) => {
|
|
2232
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
2233
|
+
};
|
|
2234
|
+
const dropImages = (event) => {
|
|
2235
|
+
setDragging(false);
|
|
2236
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
2202
2237
|
};
|
|
2203
2238
|
const send = () => {
|
|
2204
2239
|
const text = draft.trim();
|
|
2205
|
-
if (!text || !harness || starting) return;
|
|
2240
|
+
if (!text && !images.length || !harness || starting) return;
|
|
2206
2241
|
const id = startSequence.current + 1;
|
|
2207
2242
|
startSequence.current = id;
|
|
2208
2243
|
setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
@@ -2253,7 +2288,16 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2253
2288
|
return next;
|
|
2254
2289
|
}) }),
|
|
2255
2290
|
pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2256
|
-
/* @__PURE__ */ jsxs7("div", { class:
|
|
2291
|
+
/* @__PURE__ */ jsxs7("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
2292
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
2293
|
+
event.preventDefault();
|
|
2294
|
+
setDragging(true);
|
|
2295
|
+
}
|
|
2296
|
+
}, onDragOver: (event) => {
|
|
2297
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
2298
|
+
}, onDragLeave: (event) => {
|
|
2299
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
2300
|
+
}, onDrop: dropImages, children: [
|
|
2257
2301
|
adapter.pickContext ? /* @__PURE__ */ jsx8("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2258
2302
|
/* @__PURE__ */ jsx8("textarea", { ref: textarea, rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || Boolean(starting), onPaste: pasteImages, onInput: (event) => {
|
|
2259
2303
|
const value = event.currentTarget.value;
|
|
@@ -2265,7 +2309,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2265
2309
|
send();
|
|
2266
2310
|
}
|
|
2267
2311
|
} }),
|
|
2268
|
-
/* @__PURE__ */ jsx8("span", { children: /* @__PURE__ */ jsx8("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "send", size: 17 }) }) })
|
|
2312
|
+
/* @__PURE__ */ jsx8("span", { children: /* @__PURE__ */ jsx8("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "send", size: 17 }) }) })
|
|
2269
2313
|
] })
|
|
2270
2314
|
] })
|
|
2271
2315
|
] });
|
package/composer.mjs
CHANGED
|
@@ -274,6 +274,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
274
274
|
const [queue, setQueue] = useState((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
|
|
275
275
|
const [dispatching, setDispatching] = useState(false);
|
|
276
276
|
const [picking, setPicking] = useState(false);
|
|
277
|
+
const [dragging, setDragging] = useState(false);
|
|
277
278
|
const [pickerError, setPickerError] = useState(null);
|
|
278
279
|
const textarea = useRef(null);
|
|
279
280
|
useAutosizeTextarea(textarea, draft);
|
|
@@ -339,13 +340,17 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
339
340
|
});
|
|
340
341
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
341
342
|
};
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
if (!
|
|
345
|
-
|
|
343
|
+
const addImageFiles = (value, source) => {
|
|
344
|
+
const allFiles = Array.from(value ?? []);
|
|
345
|
+
if (!allFiles.length) return false;
|
|
346
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
347
|
+
if (files.length !== allFiles.length) {
|
|
348
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
346
351
|
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
347
352
|
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
348
|
-
return;
|
|
353
|
+
return true;
|
|
349
354
|
}
|
|
350
355
|
setPicking(true);
|
|
351
356
|
setPickerError(null);
|
|
@@ -353,11 +358,19 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
353
358
|
const next = mergeImages(current, picked);
|
|
354
359
|
remember(draft, context, next, queue);
|
|
355
360
|
return next;
|
|
356
|
-
}), (error) => setPickerError(error instanceof Error ? error.message :
|
|
361
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
362
|
+
return true;
|
|
363
|
+
};
|
|
364
|
+
const pasteImages = (event) => {
|
|
365
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
366
|
+
};
|
|
367
|
+
const dropImages = (event) => {
|
|
368
|
+
setDragging(false);
|
|
369
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
357
370
|
};
|
|
358
371
|
const send = () => {
|
|
359
372
|
const text = draft.trim();
|
|
360
|
-
if (!text) return;
|
|
373
|
+
if (!text && !images.length) return;
|
|
361
374
|
const message = { text, context, images };
|
|
362
375
|
if (queuesNewMessage) updateQueue((items) => [...items, message]);
|
|
363
376
|
else if (state.canSend) {
|
|
@@ -378,7 +391,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
378
391
|
] }),
|
|
379
392
|
queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
|
|
380
393
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
381
|
-
item.text,
|
|
394
|
+
item.text || "Image attachment",
|
|
382
395
|
item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
383
396
|
item.context.length + item.images.length,
|
|
384
397
|
" attached"
|
|
@@ -398,7 +411,16 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
398
411
|
return next;
|
|
399
412
|
}) }),
|
|
400
413
|
pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
401
|
-
/* @__PURE__ */ jsxs3("div", { class:
|
|
414
|
+
/* @__PURE__ */ jsxs3("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
415
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
416
|
+
event.preventDefault();
|
|
417
|
+
setDragging(true);
|
|
418
|
+
}
|
|
419
|
+
}, onDragOver: (event) => {
|
|
420
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
421
|
+
}, onDragLeave: (event) => {
|
|
422
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
423
|
+
}, onDrop: dropImages, children: [
|
|
402
424
|
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
403
425
|
/* @__PURE__ */ jsx3("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" : pendingStatus === "failed" ? "Retry or edit the unsent message\u2026" : pendingStatus === "editing" ? "Edit and resend\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
|
|
404
426
|
const value = event.currentTarget.value;
|
|
@@ -412,7 +434,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
412
434
|
} }),
|
|
413
435
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
414
436
|
state.busy ? /* @__PURE__ */ jsx3("button", { class: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: /* @__PURE__ */ jsx3(UiIcon, { name: "stop", size: 15 }) }) : null,
|
|
415
|
-
/* @__PURE__ */ jsx3("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx3(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
|
|
437
|
+
/* @__PURE__ */ jsx3("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() && !images.length || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx3(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
|
|
416
438
|
] })
|
|
417
439
|
] })
|
|
418
440
|
] });
|
package/embed.mjs
CHANGED
|
@@ -973,6 +973,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
973
973
|
const [queue, setQueue] = useState((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
|
|
974
974
|
const [dispatching, setDispatching] = useState(false);
|
|
975
975
|
const [picking, setPicking] = useState(false);
|
|
976
|
+
const [dragging, setDragging] = useState(false);
|
|
976
977
|
const [pickerError, setPickerError] = useState(null);
|
|
977
978
|
const textarea = useRef(null);
|
|
978
979
|
useAutosizeTextarea(textarea, draft);
|
|
@@ -1038,13 +1039,17 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1038
1039
|
});
|
|
1039
1040
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
1040
1041
|
};
|
|
1041
|
-
const
|
|
1042
|
-
const
|
|
1043
|
-
if (!
|
|
1044
|
-
|
|
1042
|
+
const addImageFiles = (value, source) => {
|
|
1043
|
+
const allFiles = Array.from(value ?? []);
|
|
1044
|
+
if (!allFiles.length) return false;
|
|
1045
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
1046
|
+
if (files.length !== allFiles.length) {
|
|
1047
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1045
1050
|
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
1046
1051
|
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1047
|
-
return;
|
|
1052
|
+
return true;
|
|
1048
1053
|
}
|
|
1049
1054
|
setPicking(true);
|
|
1050
1055
|
setPickerError(null);
|
|
@@ -1052,11 +1057,19 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1052
1057
|
const next = mergeImages(current, picked);
|
|
1053
1058
|
remember(draft, context, next, queue);
|
|
1054
1059
|
return next;
|
|
1055
|
-
}), (error) => setPickerError(error instanceof Error ? error.message :
|
|
1060
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
1061
|
+
return true;
|
|
1062
|
+
};
|
|
1063
|
+
const pasteImages = (event) => {
|
|
1064
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
1065
|
+
};
|
|
1066
|
+
const dropImages = (event) => {
|
|
1067
|
+
setDragging(false);
|
|
1068
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
1056
1069
|
};
|
|
1057
1070
|
const send = () => {
|
|
1058
1071
|
const text = draft.trim();
|
|
1059
|
-
if (!text) return;
|
|
1072
|
+
if (!text && !images.length) return;
|
|
1060
1073
|
const message = { text, context, images };
|
|
1061
1074
|
if (queuesNewMessage) updateQueue((items) => [...items, message]);
|
|
1062
1075
|
else if (state.canSend) {
|
|
@@ -1077,7 +1090,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1077
1090
|
] }),
|
|
1078
1091
|
queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
|
|
1079
1092
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
1080
|
-
item.text,
|
|
1093
|
+
item.text || "Image attachment",
|
|
1081
1094
|
item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
1082
1095
|
item.context.length + item.images.length,
|
|
1083
1096
|
" attached"
|
|
@@ -1097,7 +1110,16 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1097
1110
|
return next;
|
|
1098
1111
|
}) }),
|
|
1099
1112
|
pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
1100
|
-
/* @__PURE__ */ jsxs3("div", { class:
|
|
1113
|
+
/* @__PURE__ */ jsxs3("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
1114
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
1115
|
+
event.preventDefault();
|
|
1116
|
+
setDragging(true);
|
|
1117
|
+
}
|
|
1118
|
+
}, onDragOver: (event) => {
|
|
1119
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
1120
|
+
}, onDragLeave: (event) => {
|
|
1121
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1122
|
+
}, onDrop: dropImages, children: [
|
|
1101
1123
|
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
1102
1124
|
/* @__PURE__ */ jsx3("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" : pendingStatus === "failed" ? "Retry or edit the unsent message\u2026" : pendingStatus === "editing" ? "Edit and resend\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
|
|
1103
1125
|
const value = event.currentTarget.value;
|
|
@@ -1111,7 +1133,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1111
1133
|
} }),
|
|
1112
1134
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
1113
1135
|
state.busy ? /* @__PURE__ */ jsx3("button", { class: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: /* @__PURE__ */ jsx3(UiIcon, { name: "stop", size: 15 }) }) : null,
|
|
1114
|
-
/* @__PURE__ */ jsx3("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx3(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
|
|
1136
|
+
/* @__PURE__ */ jsx3("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() && !images.length || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx3(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
|
|
1115
1137
|
] })
|
|
1116
1138
|
] })
|
|
1117
1139
|
] });
|
|
@@ -2140,6 +2162,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2140
2162
|
const [images, setImages] = useState4(remembered.images ?? []);
|
|
2141
2163
|
const [starting, setStarting] = useState4(null);
|
|
2142
2164
|
const [picking, setPicking] = useState4(false);
|
|
2165
|
+
const [dragging, setDragging] = useState4(false);
|
|
2143
2166
|
const [pickerError, setPickerError] = useState4(null);
|
|
2144
2167
|
const startSequence = useRef5(0);
|
|
2145
2168
|
const textarea = useRef5(null);
|
|
@@ -2184,13 +2207,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2184
2207
|
});
|
|
2185
2208
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
2186
2209
|
};
|
|
2187
|
-
const
|
|
2188
|
-
const
|
|
2189
|
-
if (!
|
|
2190
|
-
|
|
2210
|
+
const addImageFiles = (value, source) => {
|
|
2211
|
+
const allFiles = Array.from(value ?? []);
|
|
2212
|
+
if (!allFiles.length) return false;
|
|
2213
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
2214
|
+
if (files.length !== allFiles.length) {
|
|
2215
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
2216
|
+
return true;
|
|
2217
|
+
}
|
|
2191
2218
|
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
2192
2219
|
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2193
|
-
return;
|
|
2220
|
+
return true;
|
|
2194
2221
|
}
|
|
2195
2222
|
setPicking(true);
|
|
2196
2223
|
setPickerError(null);
|
|
@@ -2198,11 +2225,19 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2198
2225
|
const next = mergeImages(current, picked);
|
|
2199
2226
|
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2200
2227
|
return next;
|
|
2201
|
-
}), (error) => setPickerError(error instanceof Error ? error.message :
|
|
2228
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
2229
|
+
return true;
|
|
2230
|
+
};
|
|
2231
|
+
const pasteImages = (event) => {
|
|
2232
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
2233
|
+
};
|
|
2234
|
+
const dropImages = (event) => {
|
|
2235
|
+
setDragging(false);
|
|
2236
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
2202
2237
|
};
|
|
2203
2238
|
const send = () => {
|
|
2204
2239
|
const text = draft.trim();
|
|
2205
|
-
if (!text || !harness || starting) return;
|
|
2240
|
+
if (!text && !images.length || !harness || starting) return;
|
|
2206
2241
|
const id = startSequence.current + 1;
|
|
2207
2242
|
startSequence.current = id;
|
|
2208
2243
|
setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
@@ -2253,7 +2288,16 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2253
2288
|
return next;
|
|
2254
2289
|
}) }),
|
|
2255
2290
|
pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2256
|
-
/* @__PURE__ */ jsxs7("div", { class:
|
|
2291
|
+
/* @__PURE__ */ jsxs7("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
2292
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
2293
|
+
event.preventDefault();
|
|
2294
|
+
setDragging(true);
|
|
2295
|
+
}
|
|
2296
|
+
}, onDragOver: (event) => {
|
|
2297
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
2298
|
+
}, onDragLeave: (event) => {
|
|
2299
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
2300
|
+
}, onDrop: dropImages, children: [
|
|
2257
2301
|
adapter.pickContext ? /* @__PURE__ */ jsx8("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2258
2302
|
/* @__PURE__ */ jsx8("textarea", { ref: textarea, rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || Boolean(starting), onPaste: pasteImages, onInput: (event) => {
|
|
2259
2303
|
const value = event.currentTarget.value;
|
|
@@ -2265,7 +2309,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2265
2309
|
send();
|
|
2266
2310
|
}
|
|
2267
2311
|
} }),
|
|
2268
|
-
/* @__PURE__ */ jsx8("span", { children: /* @__PURE__ */ jsx8("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "send", size: 17 }) }) })
|
|
2312
|
+
/* @__PURE__ */ jsx8("span", { children: /* @__PURE__ */ jsx8("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "send", size: 17 }) }) })
|
|
2269
2313
|
] })
|
|
2270
2314
|
] })
|
|
2271
2315
|
] });
|
package/messenger.mjs
CHANGED
|
@@ -970,6 +970,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
970
970
|
const [queue, setQueue] = useState((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
|
|
971
971
|
const [dispatching, setDispatching] = useState(false);
|
|
972
972
|
const [picking, setPicking] = useState(false);
|
|
973
|
+
const [dragging, setDragging] = useState(false);
|
|
973
974
|
const [pickerError, setPickerError] = useState(null);
|
|
974
975
|
const textarea = useRef(null);
|
|
975
976
|
useAutosizeTextarea(textarea, draft);
|
|
@@ -1035,13 +1036,17 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1035
1036
|
});
|
|
1036
1037
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
1037
1038
|
};
|
|
1038
|
-
const
|
|
1039
|
-
const
|
|
1040
|
-
if (!
|
|
1041
|
-
|
|
1039
|
+
const addImageFiles = (value, source) => {
|
|
1040
|
+
const allFiles = Array.from(value ?? []);
|
|
1041
|
+
if (!allFiles.length) return false;
|
|
1042
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
1043
|
+
if (files.length !== allFiles.length) {
|
|
1044
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1042
1047
|
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
1043
1048
|
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1044
|
-
return;
|
|
1049
|
+
return true;
|
|
1045
1050
|
}
|
|
1046
1051
|
setPicking(true);
|
|
1047
1052
|
setPickerError(null);
|
|
@@ -1049,11 +1054,19 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1049
1054
|
const next = mergeImages(current, picked);
|
|
1050
1055
|
remember(draft, context, next, queue);
|
|
1051
1056
|
return next;
|
|
1052
|
-
}), (error) => setPickerError(error instanceof Error ? error.message :
|
|
1057
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
1058
|
+
return true;
|
|
1059
|
+
};
|
|
1060
|
+
const pasteImages = (event) => {
|
|
1061
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
1062
|
+
};
|
|
1063
|
+
const dropImages = (event) => {
|
|
1064
|
+
setDragging(false);
|
|
1065
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
1053
1066
|
};
|
|
1054
1067
|
const send = () => {
|
|
1055
1068
|
const text = draft.trim();
|
|
1056
|
-
if (!text) return;
|
|
1069
|
+
if (!text && !images.length) return;
|
|
1057
1070
|
const message = { text, context, images };
|
|
1058
1071
|
if (queuesNewMessage) updateQueue((items) => [...items, message]);
|
|
1059
1072
|
else if (state.canSend) {
|
|
@@ -1074,7 +1087,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1074
1087
|
] }),
|
|
1075
1088
|
queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
|
|
1076
1089
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
1077
|
-
item.text,
|
|
1090
|
+
item.text || "Image attachment",
|
|
1078
1091
|
item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
1079
1092
|
item.context.length + item.images.length,
|
|
1080
1093
|
" attached"
|
|
@@ -1094,7 +1107,16 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1094
1107
|
return next;
|
|
1095
1108
|
}) }),
|
|
1096
1109
|
pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
1097
|
-
/* @__PURE__ */ jsxs3("div", { class:
|
|
1110
|
+
/* @__PURE__ */ jsxs3("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
1111
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
1112
|
+
event.preventDefault();
|
|
1113
|
+
setDragging(true);
|
|
1114
|
+
}
|
|
1115
|
+
}, onDragOver: (event) => {
|
|
1116
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
1117
|
+
}, onDragLeave: (event) => {
|
|
1118
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1119
|
+
}, onDrop: dropImages, children: [
|
|
1098
1120
|
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
1099
1121
|
/* @__PURE__ */ jsx3("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" : pendingStatus === "failed" ? "Retry or edit the unsent message\u2026" : pendingStatus === "editing" ? "Edit and resend\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
|
|
1100
1122
|
const value = event.currentTarget.value;
|
|
@@ -1108,7 +1130,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1108
1130
|
} }),
|
|
1109
1131
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
1110
1132
|
state.busy ? /* @__PURE__ */ jsx3("button", { class: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: /* @__PURE__ */ jsx3(UiIcon, { name: "stop", size: 15 }) }) : null,
|
|
1111
|
-
/* @__PURE__ */ jsx3("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx3(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
|
|
1133
|
+
/* @__PURE__ */ jsx3("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() && !images.length || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx3(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
|
|
1112
1134
|
] })
|
|
1113
1135
|
] })
|
|
1114
1136
|
] });
|
|
@@ -2137,6 +2159,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2137
2159
|
const [images, setImages] = useState4(remembered.images ?? []);
|
|
2138
2160
|
const [starting, setStarting] = useState4(null);
|
|
2139
2161
|
const [picking, setPicking] = useState4(false);
|
|
2162
|
+
const [dragging, setDragging] = useState4(false);
|
|
2140
2163
|
const [pickerError, setPickerError] = useState4(null);
|
|
2141
2164
|
const startSequence = useRef5(0);
|
|
2142
2165
|
const textarea = useRef5(null);
|
|
@@ -2181,13 +2204,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2181
2204
|
});
|
|
2182
2205
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
2183
2206
|
};
|
|
2184
|
-
const
|
|
2185
|
-
const
|
|
2186
|
-
if (!
|
|
2187
|
-
|
|
2207
|
+
const addImageFiles = (value, source) => {
|
|
2208
|
+
const allFiles = Array.from(value ?? []);
|
|
2209
|
+
if (!allFiles.length) return false;
|
|
2210
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
2211
|
+
if (files.length !== allFiles.length) {
|
|
2212
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
2213
|
+
return true;
|
|
2214
|
+
}
|
|
2188
2215
|
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
2189
2216
|
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2190
|
-
return;
|
|
2217
|
+
return true;
|
|
2191
2218
|
}
|
|
2192
2219
|
setPicking(true);
|
|
2193
2220
|
setPickerError(null);
|
|
@@ -2195,11 +2222,19 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2195
2222
|
const next = mergeImages(current, picked);
|
|
2196
2223
|
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2197
2224
|
return next;
|
|
2198
|
-
}), (error) => setPickerError(error instanceof Error ? error.message :
|
|
2225
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
2226
|
+
return true;
|
|
2227
|
+
};
|
|
2228
|
+
const pasteImages = (event) => {
|
|
2229
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
2230
|
+
};
|
|
2231
|
+
const dropImages = (event) => {
|
|
2232
|
+
setDragging(false);
|
|
2233
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
2199
2234
|
};
|
|
2200
2235
|
const send = () => {
|
|
2201
2236
|
const text = draft.trim();
|
|
2202
|
-
if (!text || !harness || starting) return;
|
|
2237
|
+
if (!text && !images.length || !harness || starting) return;
|
|
2203
2238
|
const id = startSequence.current + 1;
|
|
2204
2239
|
startSequence.current = id;
|
|
2205
2240
|
setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
@@ -2250,7 +2285,16 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2250
2285
|
return next;
|
|
2251
2286
|
}) }),
|
|
2252
2287
|
pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2253
|
-
/* @__PURE__ */ jsxs7("div", { class:
|
|
2288
|
+
/* @__PURE__ */ jsxs7("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
2289
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
2290
|
+
event.preventDefault();
|
|
2291
|
+
setDragging(true);
|
|
2292
|
+
}
|
|
2293
|
+
}, onDragOver: (event) => {
|
|
2294
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
2295
|
+
}, onDragLeave: (event) => {
|
|
2296
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
2297
|
+
}, onDrop: dropImages, children: [
|
|
2254
2298
|
adapter.pickContext ? /* @__PURE__ */ jsx8("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2255
2299
|
/* @__PURE__ */ jsx8("textarea", { ref: textarea, rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || Boolean(starting), onPaste: pasteImages, onInput: (event) => {
|
|
2256
2300
|
const value = event.currentTarget.value;
|
|
@@ -2262,7 +2306,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2262
2306
|
send();
|
|
2263
2307
|
}
|
|
2264
2308
|
} }),
|
|
2265
|
-
/* @__PURE__ */ jsx8("span", { children: /* @__PURE__ */ jsx8("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "send", size: 17 }) }) })
|
|
2309
|
+
/* @__PURE__ */ jsx8("span", { children: /* @__PURE__ */ jsx8("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "send", size: 17 }) }) })
|
|
2266
2310
|
] })
|
|
2267
2311
|
] })
|
|
2268
2312
|
] });
|
package/package.json
CHANGED
package/styles.css
CHANGED
|
@@ -171,6 +171,7 @@
|
|
|
171
171
|
.scui-continuation { display:flex; align-items:center; gap:8px; padding:7px 9px; border-top:1px solid var(--scui-border); background:var(--scui-bg-raised) }.scui-continuation > span { display:grid; flex:1 }.scui-continuation small { color:var(--scui-muted); font-size:10px }.scui-continuation button { padding:5px 8px; border:1px solid var(--scui-accent); border-radius:7px; background:transparent; color:var(--scui-accent); cursor:pointer }
|
|
172
172
|
.scui-compose { flex:none; padding:8px; border-top:1px solid var(--scui-border); background:var(--scui-bg-raised) }
|
|
173
173
|
.scui-envelope { display:flex; align-items:flex-end; gap:7px; padding:7px; border:1px solid var(--scui-border-strong); border-radius:16px; background:var(--scui-bg) }.scui-envelope textarea { flex:1; min-width:0; min-height:34px; max-height:150px; resize:none; overflow-y:hidden; border:0; outline:0; background:transparent; color:var(--scui-fg) }.scui-envelope > span { display:flex; gap:4px }
|
|
174
|
+
.scui-envelope.scui-drop-target { border-color:var(--scui-accent); background:color-mix(in srgb,var(--scui-accent) 7%,var(--scui-bg)); box-shadow:0 0 0 2px color-mix(in srgb,var(--scui-accent) 16%,transparent) }
|
|
174
175
|
.scui-attach { display:grid; flex:0 0 34px; width:34px; height:34px; place-items:center; border:0; border-radius:50%; background:transparent; color:var(--scui-muted) }.scui-attach:hover:not(:disabled) { background:var(--scui-fill); color:var(--scui-fg) }
|
|
175
176
|
.scui-compose-context { display:flex; gap:5px; margin-bottom:6px; overflow-x:auto; scrollbar-width:thin }.scui-compose-context > span { display:flex; flex:0 0 auto; max-width:210px; align-items:center; gap:5px; padding:4px 5px 4px 7px; border:1px solid var(--scui-border); border-radius:999px; background:var(--scui-fill); font-size:10px }.scui-compose-context strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-compose-context button { display:grid; width:18px; height:18px; padding:0; place-items:center; border:0; border-radius:50%; background:transparent }.scui-compose-context button:hover { background:color-mix(in srgb,var(--scui-fg) 9%,transparent) }.scui-context-error { display:block; margin:0 2px 6px; color:var(--scui-danger) }
|
|
176
177
|
.scui-compose-images { display:flex; gap:6px; margin-bottom:6px; overflow-x:auto; scrollbar-width:thin }.scui-compose-images > span { position:relative; display:grid; flex:0 0 58px; width:58px; height:58px; overflow:hidden; border:1px solid var(--scui-border); border-radius:8px; background:var(--scui-fill); place-items:center }.scui-compose-images img { width:100%; height:100%; object-fit:cover }.scui-compose-images strong { position:absolute; right:2px; bottom:2px; left:2px; overflow:hidden; padding:2px 3px; border-radius:4px; background:rgba(0,0,0,.65); color:#fff; font-size:8px; font-weight:500; text-overflow:ellipsis; white-space:nowrap }.scui-compose-images button { position:absolute; z-index:1; top:2px; right:2px; display:grid; width:19px; height:19px; padding:0; border:1px solid rgba(255,255,255,.32); border-radius:50%; background:rgba(0,0,0,.68); color:#fff; place-items:center; cursor:pointer }
|