@volter-ai-dev/supercode-ui 0.1.42 → 0.1.44
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 +10 -1
- package/activity.mjs +1 -0
- package/components.mjs +58 -9
- package/composer.mjs +24 -2
- package/conversation.mjs +1 -0
- package/core.mjs +1 -0
- package/embed.mjs +58 -9
- package/index.d.ts +9 -1
- package/messenger.mjs +58 -9
- package/package.json +1 -1
- package/react/activity.mjs +1 -0
- package/react/components.mjs +58 -9
- package/react/composer.mjs +24 -2
- package/react/conversation.mjs +1 -0
- package/react/index.d.ts +4 -1
- package/react/messenger.mjs +58 -9
- package/react/sessions.mjs +1 -0
- package/react/settings.mjs +1 -0
- package/sessions.mjs +1 -0
- package/settings.mjs +1 -0
package/README.md
CHANGED
|
@@ -128,6 +128,11 @@ Hosts do not need to fork the messenger to connect their own object model:
|
|
|
128
128
|
draft: 'Inspect this selection',
|
|
129
129
|
context: [selectedObject.asTranscriptContext()],
|
|
130
130
|
}}
|
|
131
|
+
composerCommand={{
|
|
132
|
+
id: browserCapture.revision,
|
|
133
|
+
action: 'attach',
|
|
134
|
+
attachments: browserCapture.asTranscriptAttachment(),
|
|
135
|
+
}}
|
|
131
136
|
contextCandidates={visibleObjects.map(asTranscriptContext)}
|
|
132
137
|
components={{ ContextCandidate: ObjectPreview }}
|
|
133
138
|
slots={{ beforeSessions: ProjectThreads }}
|
|
@@ -135,7 +140,11 @@ Hosts do not need to fork the messenger to connect their own object model:
|
|
|
135
140
|
```
|
|
136
141
|
|
|
137
142
|
`navigation` is an event, not duplicated router state: change its `id` to open a conversation or
|
|
138
|
-
prefill either an existing or new one, while `onViewChange` observes user navigation.
|
|
143
|
+
prefill either an existing or new one, while `onViewChange` observes user navigation.
|
|
144
|
+
`composerCommand` is a separate event-like host seam: `focus` focuses the active composer and
|
|
145
|
+
`attach` appends bounded context without replacing its draft or existing attachments. On New Chat,
|
|
146
|
+
an attachment selects the compatible headless lane when the remembered terminal lane cannot carry
|
|
147
|
+
structured context. Context candidates use the exact
|
|
139
148
|
bounded `TranscriptAttachment` envelope sent to the harness; a custom candidate component changes
|
|
140
149
|
presentation without inventing a second context protocol. `beforeSessions` and `afterSessions`
|
|
141
150
|
place host-owned rows inside the same searchable scroller. `confirmIntent` runs before resume,
|
package/activity.mjs
CHANGED
|
@@ -13,6 +13,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
13
13
|
newChat: "New chat",
|
|
14
14
|
searchChats: "Search chats",
|
|
15
15
|
askAgent: "Ask your agent\u2026",
|
|
16
|
+
attachContext: "Attach files or images",
|
|
16
17
|
continueHere: "Continue here",
|
|
17
18
|
continueWithTerminal: "Continue with terminal",
|
|
18
19
|
joinLive: "Join live",
|
package/components.mjs
CHANGED
|
@@ -16,6 +16,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
16
16
|
newChat: "New chat",
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
|
+
attachContext: "Attach files or images",
|
|
19
20
|
continueHere: "Continue here",
|
|
20
21
|
continueWithTerminal: "Continue with terminal",
|
|
21
22
|
joinLive: "Join live",
|
|
@@ -1317,7 +1318,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1317
1318
|
] })
|
|
1318
1319
|
] });
|
|
1319
1320
|
}
|
|
1320
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1321
|
+
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, command = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1321
1322
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
1322
1323
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
1323
1324
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -1329,6 +1330,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1329
1330
|
const [dragging, setDragging] = useState2(false);
|
|
1330
1331
|
const [pickerError, setPickerError] = useState2(null);
|
|
1331
1332
|
const textarea = useRef2(null);
|
|
1333
|
+
const lastCommand = useRef2(null);
|
|
1332
1334
|
useAutosizeTextarea(textarea, draft);
|
|
1333
1335
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
1334
1336
|
useEffect2(() => {
|
|
@@ -1370,6 +1372,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1370
1372
|
textarea.current?.focus({ preventScroll: true });
|
|
1371
1373
|
onDraftRestored?.(restoreDraft.id);
|
|
1372
1374
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
1375
|
+
useEffect2(() => {
|
|
1376
|
+
if (!command || command.id === lastCommand.current) return;
|
|
1377
|
+
lastCommand.current = command.id;
|
|
1378
|
+
if (command.action === "attach") {
|
|
1379
|
+
try {
|
|
1380
|
+
const attachments = partitionAttachments(command.attachments);
|
|
1381
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1382
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1383
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
1384
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
1385
|
+
setContext(nextContext);
|
|
1386
|
+
setImages(nextImages);
|
|
1387
|
+
setPickerError(null);
|
|
1388
|
+
remember(draft, nextContext, nextImages, queue);
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
textarea.current?.focus({ preventScroll: true });
|
|
1394
|
+
}, [command?.id]);
|
|
1373
1395
|
useEffect2(() => {
|
|
1374
1396
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
1375
1397
|
return () => clearTimeout(timer);
|
|
@@ -1492,7 +1514,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1492
1514
|
}, onDragLeave: (event) => {
|
|
1493
1515
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1494
1516
|
}, onDrop: dropImages, children: [
|
|
1495
|
-
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label":
|
|
1517
|
+
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
1496
1518
|
/* @__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" : steerAvailable && context.length === 0 && images.length === 0 ? "Redirect the current turn\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
|
|
1497
1519
|
const value = event.currentTarget.value;
|
|
1498
1520
|
setDraft(value);
|
|
@@ -2696,7 +2718,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
|
|
|
2696
2718
|
onClose ? /* @__PURE__ */ jsx10("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx10(UiIcon, { name: "close", size: 18 }) }) : null
|
|
2697
2719
|
] });
|
|
2698
2720
|
}
|
|
2699
|
-
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation }) {
|
|
2721
|
+
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
|
|
2700
2722
|
const Header = slots.header;
|
|
2701
2723
|
const HeaderActions = slots.headerActions;
|
|
2702
2724
|
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
@@ -2823,11 +2845,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2823
2845
|
/* @__PURE__ */ jsx10(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
|
|
2824
2846
|
/* @__PURE__ */ jsx10(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
|
|
2825
2847
|
/* @__PURE__ */ jsx10(Advisory, { state: actionState, adapter: trackedAdapter, value: state.interopSettings?.advisories[0] ?? null, onReview: (recommendedChange) => setSettings({ recommendedChange }) }),
|
|
2826
|
-
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx10(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null,
|
|
2848
|
+
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx10(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, command: composerCommand, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null,
|
|
2827
2849
|
settings ? /* @__PURE__ */ jsx10(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
|
|
2828
2850
|
] });
|
|
2829
2851
|
}
|
|
2830
|
-
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation }) {
|
|
2852
|
+
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand }) {
|
|
2831
2853
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2832
2854
|
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2833
2855
|
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
@@ -2841,6 +2863,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2841
2863
|
const [dragging, setDragging] = useState6(false);
|
|
2842
2864
|
const [pickerError, setPickerError] = useState6(null);
|
|
2843
2865
|
const startSequence = useRef7(0);
|
|
2866
|
+
const lastComposerCommand = useRef7(null);
|
|
2844
2867
|
const textarea = useRef7(null);
|
|
2845
2868
|
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2846
2869
|
const Picker = components.HarnessPicker ?? HarnessPicker;
|
|
@@ -2884,6 +2907,32 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2884
2907
|
remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
|
|
2885
2908
|
textarea.current?.focus({ preventScroll: true });
|
|
2886
2909
|
}, [navigation?.id]);
|
|
2910
|
+
useEffect8(() => {
|
|
2911
|
+
if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
|
|
2912
|
+
lastComposerCommand.current = composerCommand.id;
|
|
2913
|
+
if (composerCommand.action === "attach") {
|
|
2914
|
+
if (mode === "terminal" && !launchModes.includes("headless")) {
|
|
2915
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2916
|
+
} else {
|
|
2917
|
+
try {
|
|
2918
|
+
const attachments = partitionAttachments(composerCommand.attachments);
|
|
2919
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2920
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2921
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
2922
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
2923
|
+
const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
|
|
2924
|
+
if (nextModes !== modes) setModes(nextModes);
|
|
2925
|
+
setContext(nextContext);
|
|
2926
|
+
setImages(nextImages);
|
|
2927
|
+
setPickerError(null);
|
|
2928
|
+
remember({ context: nextContext, images: nextImages, modes: nextModes });
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
textarea.current?.focus({ preventScroll: true });
|
|
2935
|
+
}, [composerCommand?.id]);
|
|
2887
2936
|
const pickContext = () => {
|
|
2888
2937
|
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2889
2938
|
setPicking(true);
|
|
@@ -3030,7 +3079,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
3030
3079
|
}
|
|
3031
3080
|
} }),
|
|
3032
3081
|
/* @__PURE__ */ jsxs9("footer", { className: "scui-new-envelope-controls", children: [
|
|
3033
|
-
adapter.pickContext ? /* @__PURE__ */ jsx10("button", { className: "scui-attach", type: "button", "aria-label":
|
|
3082
|
+
adapter.pickContext ? /* @__PURE__ */ jsx10("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: mode === "terminal" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx10("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx10(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
3034
3083
|
/* @__PURE__ */ jsx10(Picker, { harnesses: state.harnesses, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
|
|
3035
3084
|
setHarness(value);
|
|
3036
3085
|
remember({ harness: value });
|
|
@@ -3041,7 +3090,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
3041
3090
|
] })
|
|
3042
3091
|
] });
|
|
3043
3092
|
}
|
|
3044
|
-
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3093
|
+
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3045
3094
|
const state = useMemo3(() => normalizeUiState(stateInput), [stateInput]);
|
|
3046
3095
|
const contextCandidates = useMemo3(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
|
|
3047
3096
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
@@ -3112,7 +3161,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3112
3161
|
setNewNavigation(null);
|
|
3113
3162
|
setView("new");
|
|
3114
3163
|
}, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey, slots, headerActions: HeaderActions }) : null,
|
|
3115
|
-
view === "new" ? /* @__PURE__ */ jsx10(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation: newNavigation }) : null,
|
|
3164
|
+
view === "new" ? /* @__PURE__ */ jsx10(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation: newNavigation, composerCommand }) : null,
|
|
3116
3165
|
view === "chat" ? /* @__PURE__ */ jsx10(Chat, { state, adapter, onBack: () => {
|
|
3117
3166
|
setListFocus(state.attached?.key ?? listFocus);
|
|
3118
3167
|
setView("list");
|
|
@@ -3121,7 +3170,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3121
3170
|
setChatNavigation(null);
|
|
3122
3171
|
setNewNavigation(null);
|
|
3123
3172
|
setView("new");
|
|
3124
|
-
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null }) : null,
|
|
3173
|
+
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null, composerCommand }) : null,
|
|
3125
3174
|
opening ? /* @__PURE__ */ jsxs9("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
3126
3175
|
/* @__PURE__ */ jsx10(HarnessLogo, { id: opening.harness, size: 34 }),
|
|
3127
3176
|
/* @__PURE__ */ jsxs9("span", { children: [
|
package/composer.mjs
CHANGED
|
@@ -16,6 +16,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
16
16
|
newChat: "New chat",
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
|
+
attachContext: "Attach files or images",
|
|
19
20
|
continueHere: "Continue here",
|
|
20
21
|
continueWithTerminal: "Continue with terminal",
|
|
21
22
|
joinLive: "Join live",
|
|
@@ -318,7 +319,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
318
319
|
] })
|
|
319
320
|
] });
|
|
320
321
|
}
|
|
321
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
322
|
+
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, command = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
322
323
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
323
324
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
324
325
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -330,6 +331,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
330
331
|
const [dragging, setDragging] = useState2(false);
|
|
331
332
|
const [pickerError, setPickerError] = useState2(null);
|
|
332
333
|
const textarea = useRef2(null);
|
|
334
|
+
const lastCommand = useRef2(null);
|
|
333
335
|
useAutosizeTextarea(textarea, draft);
|
|
334
336
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
335
337
|
useEffect2(() => {
|
|
@@ -371,6 +373,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
371
373
|
textarea.current?.focus({ preventScroll: true });
|
|
372
374
|
onDraftRestored?.(restoreDraft.id);
|
|
373
375
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
376
|
+
useEffect2(() => {
|
|
377
|
+
if (!command || command.id === lastCommand.current) return;
|
|
378
|
+
lastCommand.current = command.id;
|
|
379
|
+
if (command.action === "attach") {
|
|
380
|
+
try {
|
|
381
|
+
const attachments = partitionAttachments(command.attachments);
|
|
382
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
383
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
384
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
385
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
386
|
+
setContext(nextContext);
|
|
387
|
+
setImages(nextImages);
|
|
388
|
+
setPickerError(null);
|
|
389
|
+
remember(draft, nextContext, nextImages, queue);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
textarea.current?.focus({ preventScroll: true });
|
|
395
|
+
}, [command?.id]);
|
|
374
396
|
useEffect2(() => {
|
|
375
397
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
376
398
|
return () => clearTimeout(timer);
|
|
@@ -493,7 +515,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
493
515
|
}, onDragLeave: (event) => {
|
|
494
516
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
495
517
|
}, onDrop: dropImages, children: [
|
|
496
|
-
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label":
|
|
518
|
+
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
497
519
|
/* @__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" : steerAvailable && context.length === 0 && images.length === 0 ? "Redirect the current turn\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
|
|
498
520
|
const value = event.currentTarget.value;
|
|
499
521
|
setDraft(value);
|
package/conversation.mjs
CHANGED
|
@@ -17,6 +17,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
17
17
|
newChat: "New chat",
|
|
18
18
|
searchChats: "Search chats",
|
|
19
19
|
askAgent: "Ask your agent\u2026",
|
|
20
|
+
attachContext: "Attach files or images",
|
|
20
21
|
continueHere: "Continue here",
|
|
21
22
|
continueWithTerminal: "Continue with terminal",
|
|
22
23
|
joinLive: "Join live",
|
package/core.mjs
CHANGED
|
@@ -13,6 +13,7 @@ export const DEFAULT_LABELS = Object.freeze({
|
|
|
13
13
|
newChat: 'New chat',
|
|
14
14
|
searchChats: 'Search chats',
|
|
15
15
|
askAgent: 'Ask your agent…',
|
|
16
|
+
attachContext: 'Attach files or images',
|
|
16
17
|
continueHere: 'Continue here',
|
|
17
18
|
continueWithTerminal: 'Continue with terminal',
|
|
18
19
|
joinLive: 'Join live',
|
package/embed.mjs
CHANGED
|
@@ -19,6 +19,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
19
19
|
newChat: "New chat",
|
|
20
20
|
searchChats: "Search chats",
|
|
21
21
|
askAgent: "Ask your agent\u2026",
|
|
22
|
+
attachContext: "Attach files or images",
|
|
22
23
|
continueHere: "Continue here",
|
|
23
24
|
continueWithTerminal: "Continue with terminal",
|
|
24
25
|
joinLive: "Join live",
|
|
@@ -1283,7 +1284,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1283
1284
|
] })
|
|
1284
1285
|
] });
|
|
1285
1286
|
}
|
|
1286
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1287
|
+
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, command = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1287
1288
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
1288
1289
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
1289
1290
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -1295,6 +1296,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1295
1296
|
const [dragging, setDragging] = useState2(false);
|
|
1296
1297
|
const [pickerError, setPickerError] = useState2(null);
|
|
1297
1298
|
const textarea = useRef2(null);
|
|
1299
|
+
const lastCommand = useRef2(null);
|
|
1298
1300
|
useAutosizeTextarea(textarea, draft);
|
|
1299
1301
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
1300
1302
|
useEffect2(() => {
|
|
@@ -1336,6 +1338,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1336
1338
|
textarea.current?.focus({ preventScroll: true });
|
|
1337
1339
|
onDraftRestored?.(restoreDraft.id);
|
|
1338
1340
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
1341
|
+
useEffect2(() => {
|
|
1342
|
+
if (!command || command.id === lastCommand.current) return;
|
|
1343
|
+
lastCommand.current = command.id;
|
|
1344
|
+
if (command.action === "attach") {
|
|
1345
|
+
try {
|
|
1346
|
+
const attachments = partitionAttachments(command.attachments);
|
|
1347
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1348
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1349
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
1350
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
1351
|
+
setContext(nextContext);
|
|
1352
|
+
setImages(nextImages);
|
|
1353
|
+
setPickerError(null);
|
|
1354
|
+
remember(draft, nextContext, nextImages, queue);
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
textarea.current?.focus({ preventScroll: true });
|
|
1360
|
+
}, [command?.id]);
|
|
1339
1361
|
useEffect2(() => {
|
|
1340
1362
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
1341
1363
|
return () => clearTimeout(timer);
|
|
@@ -1458,7 +1480,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1458
1480
|
}, onDragLeave: (event) => {
|
|
1459
1481
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1460
1482
|
}, onDrop: dropImages, children: [
|
|
1461
|
-
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label":
|
|
1483
|
+
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
1462
1484
|
/* @__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" : steerAvailable && context.length === 0 && images.length === 0 ? "Redirect the current turn\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
|
|
1463
1485
|
const value = event.currentTarget.value;
|
|
1464
1486
|
setDraft(value);
|
|
@@ -2616,7 +2638,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
|
|
|
2616
2638
|
onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
|
|
2617
2639
|
] });
|
|
2618
2640
|
}
|
|
2619
|
-
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation }) {
|
|
2641
|
+
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
|
|
2620
2642
|
const Header = slots.header;
|
|
2621
2643
|
const HeaderActions = slots.headerActions;
|
|
2622
2644
|
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
@@ -2743,11 +2765,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2743
2765
|
/* @__PURE__ */ jsx9(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
|
|
2744
2766
|
/* @__PURE__ */ jsx9(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
|
|
2745
2767
|
/* @__PURE__ */ jsx9(Advisory, { state: actionState, adapter: trackedAdapter, value: state.interopSettings?.advisories[0] ?? null, onReview: (recommendedChange) => setSettings({ recommendedChange }) }),
|
|
2746
|
-
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx9(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null,
|
|
2768
|
+
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx9(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, command: composerCommand, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null,
|
|
2747
2769
|
settings ? /* @__PURE__ */ jsx9(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
|
|
2748
2770
|
] });
|
|
2749
2771
|
}
|
|
2750
|
-
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation }) {
|
|
2772
|
+
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand }) {
|
|
2751
2773
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2752
2774
|
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2753
2775
|
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
@@ -2761,6 +2783,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2761
2783
|
const [dragging, setDragging] = useState6(false);
|
|
2762
2784
|
const [pickerError, setPickerError] = useState6(null);
|
|
2763
2785
|
const startSequence = useRef7(0);
|
|
2786
|
+
const lastComposerCommand = useRef7(null);
|
|
2764
2787
|
const textarea = useRef7(null);
|
|
2765
2788
|
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2766
2789
|
const Picker = components.HarnessPicker ?? HarnessPicker;
|
|
@@ -2804,6 +2827,32 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2804
2827
|
remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
|
|
2805
2828
|
textarea.current?.focus({ preventScroll: true });
|
|
2806
2829
|
}, [navigation?.id]);
|
|
2830
|
+
useEffect8(() => {
|
|
2831
|
+
if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
|
|
2832
|
+
lastComposerCommand.current = composerCommand.id;
|
|
2833
|
+
if (composerCommand.action === "attach") {
|
|
2834
|
+
if (mode === "terminal" && !launchModes.includes("headless")) {
|
|
2835
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2836
|
+
} else {
|
|
2837
|
+
try {
|
|
2838
|
+
const attachments = partitionAttachments(composerCommand.attachments);
|
|
2839
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2840
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2841
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
2842
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
2843
|
+
const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
|
|
2844
|
+
if (nextModes !== modes) setModes(nextModes);
|
|
2845
|
+
setContext(nextContext);
|
|
2846
|
+
setImages(nextImages);
|
|
2847
|
+
setPickerError(null);
|
|
2848
|
+
remember({ context: nextContext, images: nextImages, modes: nextModes });
|
|
2849
|
+
} catch (error) {
|
|
2850
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
textarea.current?.focus({ preventScroll: true });
|
|
2855
|
+
}, [composerCommand?.id]);
|
|
2807
2856
|
const pickContext = () => {
|
|
2808
2857
|
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2809
2858
|
setPicking(true);
|
|
@@ -2950,7 +2999,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2950
2999
|
}
|
|
2951
3000
|
} }),
|
|
2952
3001
|
/* @__PURE__ */ jsxs8("footer", { className: "scui-new-envelope-controls", children: [
|
|
2953
|
-
adapter.pickContext ? /* @__PURE__ */ jsx9("button", { className: "scui-attach", type: "button", "aria-label":
|
|
3002
|
+
adapter.pickContext ? /* @__PURE__ */ jsx9("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: mode === "terminal" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2954
3003
|
/* @__PURE__ */ jsx9(Picker, { harnesses: state.harnesses, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
|
|
2955
3004
|
setHarness(value);
|
|
2956
3005
|
remember({ harness: value });
|
|
@@ -2961,7 +3010,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2961
3010
|
] })
|
|
2962
3011
|
] });
|
|
2963
3012
|
}
|
|
2964
|
-
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3013
|
+
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
2965
3014
|
const state = useMemo3(() => normalizeUiState(stateInput), [stateInput]);
|
|
2966
3015
|
const contextCandidates = useMemo3(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
|
|
2967
3016
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
@@ -3032,7 +3081,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3032
3081
|
setNewNavigation(null);
|
|
3033
3082
|
setView("new");
|
|
3034
3083
|
}, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey, slots, headerActions: HeaderActions }) : null,
|
|
3035
|
-
view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation: newNavigation }) : null,
|
|
3084
|
+
view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation: newNavigation, composerCommand }) : null,
|
|
3036
3085
|
view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
|
|
3037
3086
|
setListFocus(state.attached?.key ?? listFocus);
|
|
3038
3087
|
setView("list");
|
|
@@ -3041,7 +3090,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3041
3090
|
setChatNavigation(null);
|
|
3042
3091
|
setNewNavigation(null);
|
|
3043
3092
|
setView("new");
|
|
3044
|
-
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null }) : null,
|
|
3093
|
+
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null, composerCommand }) : null,
|
|
3045
3094
|
opening ? /* @__PURE__ */ jsxs8("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
3046
3095
|
/* @__PURE__ */ jsx9(HarnessLogo, { id: opening.harness, size: 34 }),
|
|
3047
3096
|
/* @__PURE__ */ jsxs8("span", { children: [
|
package/index.d.ts
CHANGED
|
@@ -350,6 +350,7 @@ export interface MessengerLabels {
|
|
|
350
350
|
newChat: string;
|
|
351
351
|
searchChats: string;
|
|
352
352
|
askAgent: string;
|
|
353
|
+
attachContext?: string;
|
|
353
354
|
continueHere: string;
|
|
354
355
|
continueWithTerminal?: string;
|
|
355
356
|
joinLive: string;
|
|
@@ -483,6 +484,11 @@ export type MessengerNavigation =
|
|
|
483
484
|
| { id: string | number; view: 'chat'; sessionKey: string; draft?: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
|
|
484
485
|
| { id: string | number; view: 'new'; harness?: HarnessId; draft?: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> };
|
|
485
486
|
|
|
487
|
+
/** Event-like host command that focuses the active composer or appends context without replacing its draft. */
|
|
488
|
+
export type MessengerComposerCommand =
|
|
489
|
+
| { id: string | number; action: 'focus' }
|
|
490
|
+
| { id: string | number; action: 'attach'; attachments: TranscriptAttachment | TranscriptAttachment[] };
|
|
491
|
+
|
|
486
492
|
export interface MessengerProps {
|
|
487
493
|
state: SupercodeUiState | unknown;
|
|
488
494
|
adapter: UiAdapter;
|
|
@@ -490,6 +496,8 @@ export interface MessengerProps {
|
|
|
490
496
|
initialView?: 'list' | 'chat' | 'new';
|
|
491
497
|
/** Event-like host navigation. Change `id` to issue a new request. */
|
|
492
498
|
navigation?: MessengerNavigation | null;
|
|
499
|
+
/** Event-like composer command. Change `id` to focus or append attachments to the current draft. */
|
|
500
|
+
composerCommand?: MessengerComposerCommand | null;
|
|
493
501
|
onViewChange?(view: 'list' | 'chat' | 'new'): void;
|
|
494
502
|
/** Deliberate host-provided items that can be attached without opening a picker. */
|
|
495
503
|
contextCandidates?: TranscriptAttachment[];
|
|
@@ -549,7 +557,7 @@ export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapte
|
|
|
549
557
|
export function SessionRow(props: { row: SessionRowModel; state: SupercodeUiState; adapter: UiAdapter; now?: number; onOpen(row: SessionRowModel): void }): VNode;
|
|
550
558
|
export function SessionList(props: { state: SupercodeUiState; adapter: UiAdapter; onOpen(row: SessionRowModel): void; onNew?(): void; onClose?(): void; components?: MessengerComponents; slots?: MessengerSlots; labels?: MessengerLabels; focusKey?: string | null; memoryKey?: string; headerActions?: MessengerSlots['headerActions'] }): VNode;
|
|
551
559
|
export function ContinuationBar(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels }): VNode | null;
|
|
552
|
-
export function Composer(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels; memoryKey?: string; pendingStatus?: PendingMessageModel['status'] | 'editing' | null; restoreDraft?: { id: string | number; text: string; context?: TranscriptContext[]; images?: TranscriptImage[] } | null; contextCandidates?: TranscriptAttachment[]; components?: Pick<MessengerComponents, 'ContextCandidate'>; onPending?(text: string, context?: TranscriptContext[], images?: TranscriptImage[]): void; onDraftRestored?(id: string | number): void }): VNode;
|
|
560
|
+
export function Composer(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels; memoryKey?: string; pendingStatus?: PendingMessageModel['status'] | 'editing' | null; restoreDraft?: { id: string | number; text: string; context?: TranscriptContext[]; images?: TranscriptImage[] } | null; command?: MessengerComposerCommand | null; contextCandidates?: TranscriptAttachment[]; components?: Pick<MessengerComponents, 'ContextCandidate'>; onPending?(text: string, context?: TranscriptContext[], images?: TranscriptImage[]): void; onDraftRestored?(id: string | number): void }): VNode;
|
|
553
561
|
export function SupercodeMessenger(props: MessengerProps): VNode;
|
|
554
562
|
|
|
555
563
|
export interface MountedMessenger {
|
package/messenger.mjs
CHANGED
|
@@ -16,6 +16,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
16
16
|
newChat: "New chat",
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
|
+
attachContext: "Attach files or images",
|
|
19
20
|
continueHere: "Continue here",
|
|
20
21
|
continueWithTerminal: "Continue with terminal",
|
|
21
22
|
joinLive: "Join live",
|
|
@@ -1280,7 +1281,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1280
1281
|
] })
|
|
1281
1282
|
] });
|
|
1282
1283
|
}
|
|
1283
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1284
|
+
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, command = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1284
1285
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
1285
1286
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
1286
1287
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -1292,6 +1293,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1292
1293
|
const [dragging, setDragging] = useState2(false);
|
|
1293
1294
|
const [pickerError, setPickerError] = useState2(null);
|
|
1294
1295
|
const textarea = useRef2(null);
|
|
1296
|
+
const lastCommand = useRef2(null);
|
|
1295
1297
|
useAutosizeTextarea(textarea, draft);
|
|
1296
1298
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
1297
1299
|
useEffect2(() => {
|
|
@@ -1333,6 +1335,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1333
1335
|
textarea.current?.focus({ preventScroll: true });
|
|
1334
1336
|
onDraftRestored?.(restoreDraft.id);
|
|
1335
1337
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
1338
|
+
useEffect2(() => {
|
|
1339
|
+
if (!command || command.id === lastCommand.current) return;
|
|
1340
|
+
lastCommand.current = command.id;
|
|
1341
|
+
if (command.action === "attach") {
|
|
1342
|
+
try {
|
|
1343
|
+
const attachments = partitionAttachments(command.attachments);
|
|
1344
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1345
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1346
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
1347
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
1348
|
+
setContext(nextContext);
|
|
1349
|
+
setImages(nextImages);
|
|
1350
|
+
setPickerError(null);
|
|
1351
|
+
remember(draft, nextContext, nextImages, queue);
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
textarea.current?.focus({ preventScroll: true });
|
|
1357
|
+
}, [command?.id]);
|
|
1336
1358
|
useEffect2(() => {
|
|
1337
1359
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
1338
1360
|
return () => clearTimeout(timer);
|
|
@@ -1455,7 +1477,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1455
1477
|
}, onDragLeave: (event) => {
|
|
1456
1478
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1457
1479
|
}, onDrop: dropImages, children: [
|
|
1458
|
-
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label":
|
|
1480
|
+
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
1459
1481
|
/* @__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" : steerAvailable && context.length === 0 && images.length === 0 ? "Redirect the current turn\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
|
|
1460
1482
|
const value = event.currentTarget.value;
|
|
1461
1483
|
setDraft(value);
|
|
@@ -2613,7 +2635,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
|
|
|
2613
2635
|
onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
|
|
2614
2636
|
] });
|
|
2615
2637
|
}
|
|
2616
|
-
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation }) {
|
|
2638
|
+
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
|
|
2617
2639
|
const Header = slots.header;
|
|
2618
2640
|
const HeaderActions = slots.headerActions;
|
|
2619
2641
|
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
@@ -2740,11 +2762,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2740
2762
|
/* @__PURE__ */ jsx9(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
|
|
2741
2763
|
/* @__PURE__ */ jsx9(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
|
|
2742
2764
|
/* @__PURE__ */ jsx9(Advisory, { state: actionState, adapter: trackedAdapter, value: state.interopSettings?.advisories[0] ?? null, onReview: (recommendedChange) => setSettings({ recommendedChange }) }),
|
|
2743
|
-
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx9(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null,
|
|
2765
|
+
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx9(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, command: composerCommand, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null,
|
|
2744
2766
|
settings ? /* @__PURE__ */ jsx9(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
|
|
2745
2767
|
] });
|
|
2746
2768
|
}
|
|
2747
|
-
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation }) {
|
|
2769
|
+
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand }) {
|
|
2748
2770
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2749
2771
|
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2750
2772
|
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
@@ -2758,6 +2780,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2758
2780
|
const [dragging, setDragging] = useState6(false);
|
|
2759
2781
|
const [pickerError, setPickerError] = useState6(null);
|
|
2760
2782
|
const startSequence = useRef7(0);
|
|
2783
|
+
const lastComposerCommand = useRef7(null);
|
|
2761
2784
|
const textarea = useRef7(null);
|
|
2762
2785
|
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2763
2786
|
const Picker = components.HarnessPicker ?? HarnessPicker;
|
|
@@ -2801,6 +2824,32 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2801
2824
|
remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
|
|
2802
2825
|
textarea.current?.focus({ preventScroll: true });
|
|
2803
2826
|
}, [navigation?.id]);
|
|
2827
|
+
useEffect8(() => {
|
|
2828
|
+
if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
|
|
2829
|
+
lastComposerCommand.current = composerCommand.id;
|
|
2830
|
+
if (composerCommand.action === "attach") {
|
|
2831
|
+
if (mode === "terminal" && !launchModes.includes("headless")) {
|
|
2832
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2833
|
+
} else {
|
|
2834
|
+
try {
|
|
2835
|
+
const attachments = partitionAttachments(composerCommand.attachments);
|
|
2836
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2837
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2838
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
2839
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
2840
|
+
const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
|
|
2841
|
+
if (nextModes !== modes) setModes(nextModes);
|
|
2842
|
+
setContext(nextContext);
|
|
2843
|
+
setImages(nextImages);
|
|
2844
|
+
setPickerError(null);
|
|
2845
|
+
remember({ context: nextContext, images: nextImages, modes: nextModes });
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
textarea.current?.focus({ preventScroll: true });
|
|
2852
|
+
}, [composerCommand?.id]);
|
|
2804
2853
|
const pickContext = () => {
|
|
2805
2854
|
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2806
2855
|
setPicking(true);
|
|
@@ -2947,7 +2996,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2947
2996
|
}
|
|
2948
2997
|
} }),
|
|
2949
2998
|
/* @__PURE__ */ jsxs8("footer", { className: "scui-new-envelope-controls", children: [
|
|
2950
|
-
adapter.pickContext ? /* @__PURE__ */ jsx9("button", { className: "scui-attach", type: "button", "aria-label":
|
|
2999
|
+
adapter.pickContext ? /* @__PURE__ */ jsx9("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: mode === "terminal" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2951
3000
|
/* @__PURE__ */ jsx9(Picker, { harnesses: state.harnesses, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
|
|
2952
3001
|
setHarness(value);
|
|
2953
3002
|
remember({ harness: value });
|
|
@@ -2958,7 +3007,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2958
3007
|
] })
|
|
2959
3008
|
] });
|
|
2960
3009
|
}
|
|
2961
|
-
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3010
|
+
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
2962
3011
|
const state = useMemo3(() => normalizeUiState(stateInput), [stateInput]);
|
|
2963
3012
|
const contextCandidates = useMemo3(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
|
|
2964
3013
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
@@ -3029,7 +3078,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3029
3078
|
setNewNavigation(null);
|
|
3030
3079
|
setView("new");
|
|
3031
3080
|
}, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey, slots, headerActions: HeaderActions }) : null,
|
|
3032
|
-
view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation: newNavigation }) : null,
|
|
3081
|
+
view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation: newNavigation, composerCommand }) : null,
|
|
3033
3082
|
view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
|
|
3034
3083
|
setListFocus(state.attached?.key ?? listFocus);
|
|
3035
3084
|
setView("list");
|
|
@@ -3038,7 +3087,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3038
3087
|
setChatNavigation(null);
|
|
3039
3088
|
setNewNavigation(null);
|
|
3040
3089
|
setView("new");
|
|
3041
|
-
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null }) : null,
|
|
3090
|
+
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null, composerCommand }) : null,
|
|
3042
3091
|
opening ? /* @__PURE__ */ jsxs8("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
3043
3092
|
/* @__PURE__ */ jsx9(HarnessLogo, { id: opening.harness, size: 34 }),
|
|
3044
3093
|
/* @__PURE__ */ jsxs8("span", { children: [
|
package/package.json
CHANGED
package/react/activity.mjs
CHANGED
|
@@ -13,6 +13,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
13
13
|
newChat: "New chat",
|
|
14
14
|
searchChats: "Search chats",
|
|
15
15
|
askAgent: "Ask your agent\u2026",
|
|
16
|
+
attachContext: "Attach files or images",
|
|
16
17
|
continueHere: "Continue here",
|
|
17
18
|
continueWithTerminal: "Continue with terminal",
|
|
18
19
|
joinLive: "Join live",
|
package/react/components.mjs
CHANGED
|
@@ -16,6 +16,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
16
16
|
newChat: "New chat",
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
|
+
attachContext: "Attach files or images",
|
|
19
20
|
continueHere: "Continue here",
|
|
20
21
|
continueWithTerminal: "Continue with terminal",
|
|
21
22
|
joinLive: "Join live",
|
|
@@ -1317,7 +1318,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1317
1318
|
] })
|
|
1318
1319
|
] });
|
|
1319
1320
|
}
|
|
1320
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1321
|
+
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, command = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1321
1322
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
1322
1323
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
1323
1324
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -1329,6 +1330,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1329
1330
|
const [dragging, setDragging] = useState2(false);
|
|
1330
1331
|
const [pickerError, setPickerError] = useState2(null);
|
|
1331
1332
|
const textarea = useRef2(null);
|
|
1333
|
+
const lastCommand = useRef2(null);
|
|
1332
1334
|
useAutosizeTextarea(textarea, draft);
|
|
1333
1335
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
1334
1336
|
useEffect2(() => {
|
|
@@ -1370,6 +1372,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1370
1372
|
textarea.current?.focus({ preventScroll: true });
|
|
1371
1373
|
onDraftRestored?.(restoreDraft.id);
|
|
1372
1374
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
1375
|
+
useEffect2(() => {
|
|
1376
|
+
if (!command || command.id === lastCommand.current) return;
|
|
1377
|
+
lastCommand.current = command.id;
|
|
1378
|
+
if (command.action === "attach") {
|
|
1379
|
+
try {
|
|
1380
|
+
const attachments = partitionAttachments(command.attachments);
|
|
1381
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1382
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1383
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
1384
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
1385
|
+
setContext(nextContext);
|
|
1386
|
+
setImages(nextImages);
|
|
1387
|
+
setPickerError(null);
|
|
1388
|
+
remember(draft, nextContext, nextImages, queue);
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
textarea.current?.focus({ preventScroll: true });
|
|
1394
|
+
}, [command?.id]);
|
|
1373
1395
|
useEffect2(() => {
|
|
1374
1396
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
1375
1397
|
return () => clearTimeout(timer);
|
|
@@ -1492,7 +1514,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1492
1514
|
}, onDragLeave: (event) => {
|
|
1493
1515
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1494
1516
|
}, onDrop: dropImages, children: [
|
|
1495
|
-
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label":
|
|
1517
|
+
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
1496
1518
|
/* @__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" : steerAvailable && context.length === 0 && images.length === 0 ? "Redirect the current turn\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
|
|
1497
1519
|
const value = event.currentTarget.value;
|
|
1498
1520
|
setDraft(value);
|
|
@@ -2696,7 +2718,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
|
|
|
2696
2718
|
onClose ? /* @__PURE__ */ jsx10("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx10(UiIcon, { name: "close", size: 18 }) }) : null
|
|
2697
2719
|
] });
|
|
2698
2720
|
}
|
|
2699
|
-
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation }) {
|
|
2721
|
+
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
|
|
2700
2722
|
const Header = slots.header;
|
|
2701
2723
|
const HeaderActions = slots.headerActions;
|
|
2702
2724
|
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
@@ -2823,11 +2845,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2823
2845
|
/* @__PURE__ */ jsx10(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
|
|
2824
2846
|
/* @__PURE__ */ jsx10(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
|
|
2825
2847
|
/* @__PURE__ */ jsx10(Advisory, { state: actionState, adapter: trackedAdapter, value: state.interopSettings?.advisories[0] ?? null, onReview: (recommendedChange) => setSettings({ recommendedChange }) }),
|
|
2826
|
-
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx10(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null,
|
|
2848
|
+
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx10(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, command: composerCommand, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null,
|
|
2827
2849
|
settings ? /* @__PURE__ */ jsx10(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
|
|
2828
2850
|
] });
|
|
2829
2851
|
}
|
|
2830
|
-
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation }) {
|
|
2852
|
+
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand }) {
|
|
2831
2853
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2832
2854
|
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2833
2855
|
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
@@ -2841,6 +2863,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2841
2863
|
const [dragging, setDragging] = useState6(false);
|
|
2842
2864
|
const [pickerError, setPickerError] = useState6(null);
|
|
2843
2865
|
const startSequence = useRef7(0);
|
|
2866
|
+
const lastComposerCommand = useRef7(null);
|
|
2844
2867
|
const textarea = useRef7(null);
|
|
2845
2868
|
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2846
2869
|
const Picker = components.HarnessPicker ?? HarnessPicker;
|
|
@@ -2884,6 +2907,32 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2884
2907
|
remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
|
|
2885
2908
|
textarea.current?.focus({ preventScroll: true });
|
|
2886
2909
|
}, [navigation?.id]);
|
|
2910
|
+
useEffect8(() => {
|
|
2911
|
+
if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
|
|
2912
|
+
lastComposerCommand.current = composerCommand.id;
|
|
2913
|
+
if (composerCommand.action === "attach") {
|
|
2914
|
+
if (mode === "terminal" && !launchModes.includes("headless")) {
|
|
2915
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2916
|
+
} else {
|
|
2917
|
+
try {
|
|
2918
|
+
const attachments = partitionAttachments(composerCommand.attachments);
|
|
2919
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2920
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2921
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
2922
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
2923
|
+
const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
|
|
2924
|
+
if (nextModes !== modes) setModes(nextModes);
|
|
2925
|
+
setContext(nextContext);
|
|
2926
|
+
setImages(nextImages);
|
|
2927
|
+
setPickerError(null);
|
|
2928
|
+
remember({ context: nextContext, images: nextImages, modes: nextModes });
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
textarea.current?.focus({ preventScroll: true });
|
|
2935
|
+
}, [composerCommand?.id]);
|
|
2887
2936
|
const pickContext = () => {
|
|
2888
2937
|
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2889
2938
|
setPicking(true);
|
|
@@ -3030,7 +3079,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
3030
3079
|
}
|
|
3031
3080
|
} }),
|
|
3032
3081
|
/* @__PURE__ */ jsxs9("footer", { className: "scui-new-envelope-controls", children: [
|
|
3033
|
-
adapter.pickContext ? /* @__PURE__ */ jsx10("button", { className: "scui-attach", type: "button", "aria-label":
|
|
3082
|
+
adapter.pickContext ? /* @__PURE__ */ jsx10("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: mode === "terminal" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx10("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx10(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
3034
3083
|
/* @__PURE__ */ jsx10(Picker, { harnesses: state.harnesses, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
|
|
3035
3084
|
setHarness(value);
|
|
3036
3085
|
remember({ harness: value });
|
|
@@ -3041,7 +3090,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
3041
3090
|
] })
|
|
3042
3091
|
] });
|
|
3043
3092
|
}
|
|
3044
|
-
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3093
|
+
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3045
3094
|
const state = useMemo3(() => normalizeUiState(stateInput), [stateInput]);
|
|
3046
3095
|
const contextCandidates = useMemo3(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
|
|
3047
3096
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
@@ -3112,7 +3161,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3112
3161
|
setNewNavigation(null);
|
|
3113
3162
|
setView("new");
|
|
3114
3163
|
}, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey, slots, headerActions: HeaderActions }) : null,
|
|
3115
|
-
view === "new" ? /* @__PURE__ */ jsx10(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation: newNavigation }) : null,
|
|
3164
|
+
view === "new" ? /* @__PURE__ */ jsx10(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation: newNavigation, composerCommand }) : null,
|
|
3116
3165
|
view === "chat" ? /* @__PURE__ */ jsx10(Chat, { state, adapter, onBack: () => {
|
|
3117
3166
|
setListFocus(state.attached?.key ?? listFocus);
|
|
3118
3167
|
setView("list");
|
|
@@ -3121,7 +3170,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3121
3170
|
setChatNavigation(null);
|
|
3122
3171
|
setNewNavigation(null);
|
|
3123
3172
|
setView("new");
|
|
3124
|
-
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null }) : null,
|
|
3173
|
+
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null, composerCommand }) : null,
|
|
3125
3174
|
opening ? /* @__PURE__ */ jsxs9("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
3126
3175
|
/* @__PURE__ */ jsx10(HarnessLogo, { id: opening.harness, size: 34 }),
|
|
3127
3176
|
/* @__PURE__ */ jsxs9("span", { children: [
|
package/react/composer.mjs
CHANGED
|
@@ -16,6 +16,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
16
16
|
newChat: "New chat",
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
|
+
attachContext: "Attach files or images",
|
|
19
20
|
continueHere: "Continue here",
|
|
20
21
|
continueWithTerminal: "Continue with terminal",
|
|
21
22
|
joinLive: "Join live",
|
|
@@ -318,7 +319,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
318
319
|
] })
|
|
319
320
|
] });
|
|
320
321
|
}
|
|
321
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
322
|
+
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, command = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
322
323
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
323
324
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
324
325
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -330,6 +331,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
330
331
|
const [dragging, setDragging] = useState2(false);
|
|
331
332
|
const [pickerError, setPickerError] = useState2(null);
|
|
332
333
|
const textarea = useRef2(null);
|
|
334
|
+
const lastCommand = useRef2(null);
|
|
333
335
|
useAutosizeTextarea(textarea, draft);
|
|
334
336
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
335
337
|
useEffect2(() => {
|
|
@@ -371,6 +373,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
371
373
|
textarea.current?.focus({ preventScroll: true });
|
|
372
374
|
onDraftRestored?.(restoreDraft.id);
|
|
373
375
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
376
|
+
useEffect2(() => {
|
|
377
|
+
if (!command || command.id === lastCommand.current) return;
|
|
378
|
+
lastCommand.current = command.id;
|
|
379
|
+
if (command.action === "attach") {
|
|
380
|
+
try {
|
|
381
|
+
const attachments = partitionAttachments(command.attachments);
|
|
382
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
383
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
384
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
385
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
386
|
+
setContext(nextContext);
|
|
387
|
+
setImages(nextImages);
|
|
388
|
+
setPickerError(null);
|
|
389
|
+
remember(draft, nextContext, nextImages, queue);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
textarea.current?.focus({ preventScroll: true });
|
|
395
|
+
}, [command?.id]);
|
|
374
396
|
useEffect2(() => {
|
|
375
397
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
376
398
|
return () => clearTimeout(timer);
|
|
@@ -493,7 +515,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
493
515
|
}, onDragLeave: (event) => {
|
|
494
516
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
495
517
|
}, onDrop: dropImages, children: [
|
|
496
|
-
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label":
|
|
518
|
+
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
497
519
|
/* @__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" : steerAvailable && context.length === 0 && images.length === 0 ? "Redirect the current turn\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
|
|
498
520
|
const value = event.currentTarget.value;
|
|
499
521
|
setDraft(value);
|
package/react/conversation.mjs
CHANGED
|
@@ -17,6 +17,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
17
17
|
newChat: "New chat",
|
|
18
18
|
searchChats: "Search chats",
|
|
19
19
|
askAgent: "Ask your agent\u2026",
|
|
20
|
+
attachContext: "Attach files or images",
|
|
20
21
|
continueHere: "Continue here",
|
|
21
22
|
continueWithTerminal: "Continue with terminal",
|
|
22
23
|
joinLive: "Join live",
|
package/react/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
HarnessSettingsPanelProps,
|
|
12
12
|
HarnessOption,
|
|
13
13
|
MessengerLabels,
|
|
14
|
+
MessengerComposerCommand,
|
|
14
15
|
MessengerNavigation,
|
|
15
16
|
PendingMessageModel,
|
|
16
17
|
SessionActivity,
|
|
@@ -41,6 +42,7 @@ export type {
|
|
|
41
42
|
HarnessSettingsPanelProps,
|
|
42
43
|
HarnessOption,
|
|
43
44
|
MessengerLabels,
|
|
45
|
+
MessengerComposerCommand,
|
|
44
46
|
MessengerNavigation,
|
|
45
47
|
PendingMessageModel,
|
|
46
48
|
SessionActivity,
|
|
@@ -90,6 +92,7 @@ export interface MessengerProps {
|
|
|
90
92
|
class?: string;
|
|
91
93
|
initialView?: 'list' | 'chat' | 'new';
|
|
92
94
|
navigation?: MessengerNavigation | null;
|
|
95
|
+
composerCommand?: MessengerComposerCommand | null;
|
|
93
96
|
onViewChange?(view: 'list' | 'chat' | 'new'): void;
|
|
94
97
|
contextCandidates?: TranscriptAttachment[];
|
|
95
98
|
labels?: Partial<MessengerLabels>;
|
|
@@ -122,5 +125,5 @@ export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapte
|
|
|
122
125
|
export function SessionRow(props: { row: SessionRowModel; state: SupercodeUiState; adapter: UiAdapter; now?: number; onOpen(row: SessionRowModel): void }): ReactElement;
|
|
123
126
|
export function SessionList(props: { state: SupercodeUiState; adapter: UiAdapter; onOpen(row: SessionRowModel): void; onNew?(): void; onClose?(): void; components?: MessengerComponents; slots?: MessengerSlots; labels?: MessengerLabels; focusKey?: string | null; memoryKey?: string; headerActions?: MessengerSlots['headerActions'] }): ReactElement;
|
|
124
127
|
export function ContinuationBar(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels }): ReactElement | null;
|
|
125
|
-
export function Composer(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels; memoryKey?: string; pendingStatus?: PendingMessageModel['status'] | 'editing' | null; restoreDraft?: { id: string | number; text: string; context?: TranscriptContext[]; images?: TranscriptImage[] } | null; contextCandidates?: TranscriptAttachment[]; components?: Pick<MessengerComponents, 'ContextCandidate'>; onPending?(text: string, context?: TranscriptContext[], images?: TranscriptImage[]): void; onDraftRestored?(id: string | number): void }): ReactElement;
|
|
128
|
+
export function Composer(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels; memoryKey?: string; pendingStatus?: PendingMessageModel['status'] | 'editing' | null; restoreDraft?: { id: string | number; text: string; context?: TranscriptContext[]; images?: TranscriptImage[] } | null; command?: MessengerComposerCommand | null; contextCandidates?: TranscriptAttachment[]; components?: Pick<MessengerComponents, 'ContextCandidate'>; onPending?(text: string, context?: TranscriptContext[], images?: TranscriptImage[]): void; onDraftRestored?(id: string | number): void }): ReactElement;
|
|
126
129
|
export function SupercodeMessenger(props: MessengerProps): ReactElement;
|
package/react/messenger.mjs
CHANGED
|
@@ -16,6 +16,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
16
16
|
newChat: "New chat",
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
|
+
attachContext: "Attach files or images",
|
|
19
20
|
continueHere: "Continue here",
|
|
20
21
|
continueWithTerminal: "Continue with terminal",
|
|
21
22
|
joinLive: "Join live",
|
|
@@ -1280,7 +1281,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1280
1281
|
] })
|
|
1281
1282
|
] });
|
|
1282
1283
|
}
|
|
1283
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1284
|
+
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, command = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1284
1285
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
1285
1286
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
1286
1287
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -1292,6 +1293,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1292
1293
|
const [dragging, setDragging] = useState2(false);
|
|
1293
1294
|
const [pickerError, setPickerError] = useState2(null);
|
|
1294
1295
|
const textarea = useRef2(null);
|
|
1296
|
+
const lastCommand = useRef2(null);
|
|
1295
1297
|
useAutosizeTextarea(textarea, draft);
|
|
1296
1298
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
1297
1299
|
useEffect2(() => {
|
|
@@ -1333,6 +1335,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1333
1335
|
textarea.current?.focus({ preventScroll: true });
|
|
1334
1336
|
onDraftRestored?.(restoreDraft.id);
|
|
1335
1337
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
1338
|
+
useEffect2(() => {
|
|
1339
|
+
if (!command || command.id === lastCommand.current) return;
|
|
1340
|
+
lastCommand.current = command.id;
|
|
1341
|
+
if (command.action === "attach") {
|
|
1342
|
+
try {
|
|
1343
|
+
const attachments = partitionAttachments(command.attachments);
|
|
1344
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1345
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1346
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
1347
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
1348
|
+
setContext(nextContext);
|
|
1349
|
+
setImages(nextImages);
|
|
1350
|
+
setPickerError(null);
|
|
1351
|
+
remember(draft, nextContext, nextImages, queue);
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
textarea.current?.focus({ preventScroll: true });
|
|
1357
|
+
}, [command?.id]);
|
|
1336
1358
|
useEffect2(() => {
|
|
1337
1359
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
1338
1360
|
return () => clearTimeout(timer);
|
|
@@ -1455,7 +1477,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1455
1477
|
}, onDragLeave: (event) => {
|
|
1456
1478
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1457
1479
|
}, onDrop: dropImages, children: [
|
|
1458
|
-
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label":
|
|
1480
|
+
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
1459
1481
|
/* @__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" : steerAvailable && context.length === 0 && images.length === 0 ? "Redirect the current turn\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
|
|
1460
1482
|
const value = event.currentTarget.value;
|
|
1461
1483
|
setDraft(value);
|
|
@@ -2613,7 +2635,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
|
|
|
2613
2635
|
onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
|
|
2614
2636
|
] });
|
|
2615
2637
|
}
|
|
2616
|
-
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation }) {
|
|
2638
|
+
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
|
|
2617
2639
|
const Header = slots.header;
|
|
2618
2640
|
const HeaderActions = slots.headerActions;
|
|
2619
2641
|
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
@@ -2740,11 +2762,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2740
2762
|
/* @__PURE__ */ jsx9(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
|
|
2741
2763
|
/* @__PURE__ */ jsx9(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
|
|
2742
2764
|
/* @__PURE__ */ jsx9(Advisory, { state: actionState, adapter: trackedAdapter, value: state.interopSettings?.advisories[0] ?? null, onReview: (recommendedChange) => setSettings({ recommendedChange }) }),
|
|
2743
|
-
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx9(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null,
|
|
2765
|
+
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx9(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, command: composerCommand, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null,
|
|
2744
2766
|
settings ? /* @__PURE__ */ jsx9(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
|
|
2745
2767
|
] });
|
|
2746
2768
|
}
|
|
2747
|
-
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation }) {
|
|
2769
|
+
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand }) {
|
|
2748
2770
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2749
2771
|
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2750
2772
|
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
@@ -2758,6 +2780,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2758
2780
|
const [dragging, setDragging] = useState6(false);
|
|
2759
2781
|
const [pickerError, setPickerError] = useState6(null);
|
|
2760
2782
|
const startSequence = useRef7(0);
|
|
2783
|
+
const lastComposerCommand = useRef7(null);
|
|
2761
2784
|
const textarea = useRef7(null);
|
|
2762
2785
|
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2763
2786
|
const Picker = components.HarnessPicker ?? HarnessPicker;
|
|
@@ -2801,6 +2824,32 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2801
2824
|
remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
|
|
2802
2825
|
textarea.current?.focus({ preventScroll: true });
|
|
2803
2826
|
}, [navigation?.id]);
|
|
2827
|
+
useEffect8(() => {
|
|
2828
|
+
if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
|
|
2829
|
+
lastComposerCommand.current = composerCommand.id;
|
|
2830
|
+
if (composerCommand.action === "attach") {
|
|
2831
|
+
if (mode === "terminal" && !launchModes.includes("headless")) {
|
|
2832
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2833
|
+
} else {
|
|
2834
|
+
try {
|
|
2835
|
+
const attachments = partitionAttachments(composerCommand.attachments);
|
|
2836
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2837
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2838
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
2839
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
2840
|
+
const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
|
|
2841
|
+
if (nextModes !== modes) setModes(nextModes);
|
|
2842
|
+
setContext(nextContext);
|
|
2843
|
+
setImages(nextImages);
|
|
2844
|
+
setPickerError(null);
|
|
2845
|
+
remember({ context: nextContext, images: nextImages, modes: nextModes });
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
textarea.current?.focus({ preventScroll: true });
|
|
2852
|
+
}, [composerCommand?.id]);
|
|
2804
2853
|
const pickContext = () => {
|
|
2805
2854
|
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2806
2855
|
setPicking(true);
|
|
@@ -2947,7 +2996,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2947
2996
|
}
|
|
2948
2997
|
} }),
|
|
2949
2998
|
/* @__PURE__ */ jsxs8("footer", { className: "scui-new-envelope-controls", children: [
|
|
2950
|
-
adapter.pickContext ? /* @__PURE__ */ jsx9("button", { className: "scui-attach", type: "button", "aria-label":
|
|
2999
|
+
adapter.pickContext ? /* @__PURE__ */ jsx9("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: mode === "terminal" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2951
3000
|
/* @__PURE__ */ jsx9(Picker, { harnesses: state.harnesses, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
|
|
2952
3001
|
setHarness(value);
|
|
2953
3002
|
remember({ harness: value });
|
|
@@ -2958,7 +3007,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2958
3007
|
] })
|
|
2959
3008
|
] });
|
|
2960
3009
|
}
|
|
2961
|
-
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3010
|
+
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
2962
3011
|
const state = useMemo3(() => normalizeUiState(stateInput), [stateInput]);
|
|
2963
3012
|
const contextCandidates = useMemo3(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
|
|
2964
3013
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
@@ -3029,7 +3078,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3029
3078
|
setNewNavigation(null);
|
|
3030
3079
|
setView("new");
|
|
3031
3080
|
}, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey, slots, headerActions: HeaderActions }) : null,
|
|
3032
|
-
view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation: newNavigation }) : null,
|
|
3081
|
+
view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation: newNavigation, composerCommand }) : null,
|
|
3033
3082
|
view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
|
|
3034
3083
|
setListFocus(state.attached?.key ?? listFocus);
|
|
3035
3084
|
setView("list");
|
|
@@ -3038,7 +3087,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3038
3087
|
setChatNavigation(null);
|
|
3039
3088
|
setNewNavigation(null);
|
|
3040
3089
|
setView("new");
|
|
3041
|
-
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null }) : null,
|
|
3090
|
+
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null, composerCommand }) : null,
|
|
3042
3091
|
opening ? /* @__PURE__ */ jsxs8("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
3043
3092
|
/* @__PURE__ */ jsx9(HarnessLogo, { id: opening.harness, size: 34 }),
|
|
3044
3093
|
/* @__PURE__ */ jsxs8("span", { children: [
|
package/react/sessions.mjs
CHANGED
|
@@ -16,6 +16,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
16
16
|
newChat: "New chat",
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
|
+
attachContext: "Attach files or images",
|
|
19
20
|
continueHere: "Continue here",
|
|
20
21
|
continueWithTerminal: "Continue with terminal",
|
|
21
22
|
joinLive: "Join live",
|
package/react/settings.mjs
CHANGED
|
@@ -16,6 +16,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
16
16
|
newChat: "New chat",
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
|
+
attachContext: "Attach files or images",
|
|
19
20
|
continueHere: "Continue here",
|
|
20
21
|
continueWithTerminal: "Continue with terminal",
|
|
21
22
|
joinLive: "Join live",
|
package/sessions.mjs
CHANGED
|
@@ -16,6 +16,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
16
16
|
newChat: "New chat",
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
|
+
attachContext: "Attach files or images",
|
|
19
20
|
continueHere: "Continue here",
|
|
20
21
|
continueWithTerminal: "Continue with terminal",
|
|
21
22
|
joinLive: "Join live",
|
package/settings.mjs
CHANGED
|
@@ -16,6 +16,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
16
16
|
newChat: "New chat",
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
|
+
attachContext: "Attach files or images",
|
|
19
20
|
continueHere: "Continue here",
|
|
20
21
|
continueWithTerminal: "Continue with terminal",
|
|
21
22
|
joinLive: "Join live",
|