@volter-ai-dev/supercode-ui 0.1.42 → 0.1.43
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/components.mjs +55 -7
- package/composer.mjs +22 -1
- package/embed.mjs +55 -7
- package/index.d.ts +8 -1
- package/messenger.mjs +55 -7
- package/package.json +1 -1
- package/react/components.mjs +55 -7
- package/react/composer.mjs +22 -1
- package/react/index.d.ts +4 -1
- package/react/messenger.mjs +55 -7
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/components.mjs
CHANGED
|
@@ -1317,7 +1317,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1317
1317
|
] })
|
|
1318
1318
|
] });
|
|
1319
1319
|
}
|
|
1320
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1320
|
+
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
1321
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
1322
1322
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
1323
1323
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -1329,6 +1329,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1329
1329
|
const [dragging, setDragging] = useState2(false);
|
|
1330
1330
|
const [pickerError, setPickerError] = useState2(null);
|
|
1331
1331
|
const textarea = useRef2(null);
|
|
1332
|
+
const lastCommand = useRef2(null);
|
|
1332
1333
|
useAutosizeTextarea(textarea, draft);
|
|
1333
1334
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
1334
1335
|
useEffect2(() => {
|
|
@@ -1370,6 +1371,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1370
1371
|
textarea.current?.focus({ preventScroll: true });
|
|
1371
1372
|
onDraftRestored?.(restoreDraft.id);
|
|
1372
1373
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
1374
|
+
useEffect2(() => {
|
|
1375
|
+
if (!command || command.id === lastCommand.current) return;
|
|
1376
|
+
lastCommand.current = command.id;
|
|
1377
|
+
if (command.action === "attach") {
|
|
1378
|
+
try {
|
|
1379
|
+
const attachments = partitionAttachments(command.attachments);
|
|
1380
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1381
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1382
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
1383
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
1384
|
+
setContext(nextContext);
|
|
1385
|
+
setImages(nextImages);
|
|
1386
|
+
setPickerError(null);
|
|
1387
|
+
remember(draft, nextContext, nextImages, queue);
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
textarea.current?.focus({ preventScroll: true });
|
|
1393
|
+
}, [command?.id]);
|
|
1373
1394
|
useEffect2(() => {
|
|
1374
1395
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
1375
1396
|
return () => clearTimeout(timer);
|
|
@@ -2696,7 +2717,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
|
|
|
2696
2717
|
onClose ? /* @__PURE__ */ jsx10("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx10(UiIcon, { name: "close", size: 18 }) }) : null
|
|
2697
2718
|
] });
|
|
2698
2719
|
}
|
|
2699
|
-
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation }) {
|
|
2720
|
+
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
|
|
2700
2721
|
const Header = slots.header;
|
|
2701
2722
|
const HeaderActions = slots.headerActions;
|
|
2702
2723
|
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
@@ -2823,11 +2844,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2823
2844
|
/* @__PURE__ */ jsx10(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
|
|
2824
2845
|
/* @__PURE__ */ jsx10(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
|
|
2825
2846
|
/* @__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,
|
|
2847
|
+
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
2848
|
settings ? /* @__PURE__ */ jsx10(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
|
|
2828
2849
|
] });
|
|
2829
2850
|
}
|
|
2830
|
-
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation }) {
|
|
2851
|
+
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand }) {
|
|
2831
2852
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2832
2853
|
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2833
2854
|
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
@@ -2841,6 +2862,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2841
2862
|
const [dragging, setDragging] = useState6(false);
|
|
2842
2863
|
const [pickerError, setPickerError] = useState6(null);
|
|
2843
2864
|
const startSequence = useRef7(0);
|
|
2865
|
+
const lastComposerCommand = useRef7(null);
|
|
2844
2866
|
const textarea = useRef7(null);
|
|
2845
2867
|
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2846
2868
|
const Picker = components.HarnessPicker ?? HarnessPicker;
|
|
@@ -2884,6 +2906,32 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2884
2906
|
remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
|
|
2885
2907
|
textarea.current?.focus({ preventScroll: true });
|
|
2886
2908
|
}, [navigation?.id]);
|
|
2909
|
+
useEffect8(() => {
|
|
2910
|
+
if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
|
|
2911
|
+
lastComposerCommand.current = composerCommand.id;
|
|
2912
|
+
if (composerCommand.action === "attach") {
|
|
2913
|
+
if (mode === "terminal" && !launchModes.includes("headless")) {
|
|
2914
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2915
|
+
} else {
|
|
2916
|
+
try {
|
|
2917
|
+
const attachments = partitionAttachments(composerCommand.attachments);
|
|
2918
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2919
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2920
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
2921
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
2922
|
+
const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
|
|
2923
|
+
if (nextModes !== modes) setModes(nextModes);
|
|
2924
|
+
setContext(nextContext);
|
|
2925
|
+
setImages(nextImages);
|
|
2926
|
+
setPickerError(null);
|
|
2927
|
+
remember({ context: nextContext, images: nextImages, modes: nextModes });
|
|
2928
|
+
} catch (error) {
|
|
2929
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
textarea.current?.focus({ preventScroll: true });
|
|
2934
|
+
}, [composerCommand?.id]);
|
|
2887
2935
|
const pickContext = () => {
|
|
2888
2936
|
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2889
2937
|
setPicking(true);
|
|
@@ -3041,7 +3089,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
3041
3089
|
] })
|
|
3042
3090
|
] });
|
|
3043
3091
|
}
|
|
3044
|
-
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3092
|
+
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3045
3093
|
const state = useMemo3(() => normalizeUiState(stateInput), [stateInput]);
|
|
3046
3094
|
const contextCandidates = useMemo3(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
|
|
3047
3095
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
@@ -3112,7 +3160,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3112
3160
|
setNewNavigation(null);
|
|
3113
3161
|
setView("new");
|
|
3114
3162
|
}, 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,
|
|
3163
|
+
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
3164
|
view === "chat" ? /* @__PURE__ */ jsx10(Chat, { state, adapter, onBack: () => {
|
|
3117
3165
|
setListFocus(state.attached?.key ?? listFocus);
|
|
3118
3166
|
setView("list");
|
|
@@ -3121,7 +3169,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3121
3169
|
setChatNavigation(null);
|
|
3122
3170
|
setNewNavigation(null);
|
|
3123
3171
|
setView("new");
|
|
3124
|
-
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null }) : null,
|
|
3172
|
+
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null, composerCommand }) : null,
|
|
3125
3173
|
opening ? /* @__PURE__ */ jsxs9("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
3126
3174
|
/* @__PURE__ */ jsx10(HarnessLogo, { id: opening.harness, size: 34 }),
|
|
3127
3175
|
/* @__PURE__ */ jsxs9("span", { children: [
|
package/composer.mjs
CHANGED
|
@@ -318,7 +318,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
318
318
|
] })
|
|
319
319
|
] });
|
|
320
320
|
}
|
|
321
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
321
|
+
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
322
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
323
323
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
324
324
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -330,6 +330,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
330
330
|
const [dragging, setDragging] = useState2(false);
|
|
331
331
|
const [pickerError, setPickerError] = useState2(null);
|
|
332
332
|
const textarea = useRef2(null);
|
|
333
|
+
const lastCommand = useRef2(null);
|
|
333
334
|
useAutosizeTextarea(textarea, draft);
|
|
334
335
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
335
336
|
useEffect2(() => {
|
|
@@ -371,6 +372,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
371
372
|
textarea.current?.focus({ preventScroll: true });
|
|
372
373
|
onDraftRestored?.(restoreDraft.id);
|
|
373
374
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
375
|
+
useEffect2(() => {
|
|
376
|
+
if (!command || command.id === lastCommand.current) return;
|
|
377
|
+
lastCommand.current = command.id;
|
|
378
|
+
if (command.action === "attach") {
|
|
379
|
+
try {
|
|
380
|
+
const attachments = partitionAttachments(command.attachments);
|
|
381
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
382
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
383
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
384
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
385
|
+
setContext(nextContext);
|
|
386
|
+
setImages(nextImages);
|
|
387
|
+
setPickerError(null);
|
|
388
|
+
remember(draft, nextContext, nextImages, queue);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
textarea.current?.focus({ preventScroll: true });
|
|
394
|
+
}, [command?.id]);
|
|
374
395
|
useEffect2(() => {
|
|
375
396
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
376
397
|
return () => clearTimeout(timer);
|
package/embed.mjs
CHANGED
|
@@ -1283,7 +1283,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1283
1283
|
] })
|
|
1284
1284
|
] });
|
|
1285
1285
|
}
|
|
1286
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1286
|
+
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
1287
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
1288
1288
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
1289
1289
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -1295,6 +1295,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1295
1295
|
const [dragging, setDragging] = useState2(false);
|
|
1296
1296
|
const [pickerError, setPickerError] = useState2(null);
|
|
1297
1297
|
const textarea = useRef2(null);
|
|
1298
|
+
const lastCommand = useRef2(null);
|
|
1298
1299
|
useAutosizeTextarea(textarea, draft);
|
|
1299
1300
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
1300
1301
|
useEffect2(() => {
|
|
@@ -1336,6 +1337,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1336
1337
|
textarea.current?.focus({ preventScroll: true });
|
|
1337
1338
|
onDraftRestored?.(restoreDraft.id);
|
|
1338
1339
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
1340
|
+
useEffect2(() => {
|
|
1341
|
+
if (!command || command.id === lastCommand.current) return;
|
|
1342
|
+
lastCommand.current = command.id;
|
|
1343
|
+
if (command.action === "attach") {
|
|
1344
|
+
try {
|
|
1345
|
+
const attachments = partitionAttachments(command.attachments);
|
|
1346
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1347
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1348
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
1349
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
1350
|
+
setContext(nextContext);
|
|
1351
|
+
setImages(nextImages);
|
|
1352
|
+
setPickerError(null);
|
|
1353
|
+
remember(draft, nextContext, nextImages, queue);
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
textarea.current?.focus({ preventScroll: true });
|
|
1359
|
+
}, [command?.id]);
|
|
1339
1360
|
useEffect2(() => {
|
|
1340
1361
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
1341
1362
|
return () => clearTimeout(timer);
|
|
@@ -2616,7 +2637,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
|
|
|
2616
2637
|
onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
|
|
2617
2638
|
] });
|
|
2618
2639
|
}
|
|
2619
|
-
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation }) {
|
|
2640
|
+
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
|
|
2620
2641
|
const Header = slots.header;
|
|
2621
2642
|
const HeaderActions = slots.headerActions;
|
|
2622
2643
|
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
@@ -2743,11 +2764,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2743
2764
|
/* @__PURE__ */ jsx9(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
|
|
2744
2765
|
/* @__PURE__ */ jsx9(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
|
|
2745
2766
|
/* @__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,
|
|
2767
|
+
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
2768
|
settings ? /* @__PURE__ */ jsx9(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
|
|
2748
2769
|
] });
|
|
2749
2770
|
}
|
|
2750
|
-
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation }) {
|
|
2771
|
+
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand }) {
|
|
2751
2772
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2752
2773
|
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2753
2774
|
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
@@ -2761,6 +2782,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2761
2782
|
const [dragging, setDragging] = useState6(false);
|
|
2762
2783
|
const [pickerError, setPickerError] = useState6(null);
|
|
2763
2784
|
const startSequence = useRef7(0);
|
|
2785
|
+
const lastComposerCommand = useRef7(null);
|
|
2764
2786
|
const textarea = useRef7(null);
|
|
2765
2787
|
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2766
2788
|
const Picker = components.HarnessPicker ?? HarnessPicker;
|
|
@@ -2804,6 +2826,32 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2804
2826
|
remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
|
|
2805
2827
|
textarea.current?.focus({ preventScroll: true });
|
|
2806
2828
|
}, [navigation?.id]);
|
|
2829
|
+
useEffect8(() => {
|
|
2830
|
+
if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
|
|
2831
|
+
lastComposerCommand.current = composerCommand.id;
|
|
2832
|
+
if (composerCommand.action === "attach") {
|
|
2833
|
+
if (mode === "terminal" && !launchModes.includes("headless")) {
|
|
2834
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2835
|
+
} else {
|
|
2836
|
+
try {
|
|
2837
|
+
const attachments = partitionAttachments(composerCommand.attachments);
|
|
2838
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2839
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2840
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
2841
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
2842
|
+
const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
|
|
2843
|
+
if (nextModes !== modes) setModes(nextModes);
|
|
2844
|
+
setContext(nextContext);
|
|
2845
|
+
setImages(nextImages);
|
|
2846
|
+
setPickerError(null);
|
|
2847
|
+
remember({ context: nextContext, images: nextImages, modes: nextModes });
|
|
2848
|
+
} catch (error) {
|
|
2849
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
textarea.current?.focus({ preventScroll: true });
|
|
2854
|
+
}, [composerCommand?.id]);
|
|
2807
2855
|
const pickContext = () => {
|
|
2808
2856
|
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2809
2857
|
setPicking(true);
|
|
@@ -2961,7 +3009,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2961
3009
|
] })
|
|
2962
3010
|
] });
|
|
2963
3011
|
}
|
|
2964
|
-
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3012
|
+
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
2965
3013
|
const state = useMemo3(() => normalizeUiState(stateInput), [stateInput]);
|
|
2966
3014
|
const contextCandidates = useMemo3(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
|
|
2967
3015
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
@@ -3032,7 +3080,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3032
3080
|
setNewNavigation(null);
|
|
3033
3081
|
setView("new");
|
|
3034
3082
|
}, 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,
|
|
3083
|
+
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
3084
|
view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
|
|
3037
3085
|
setListFocus(state.attached?.key ?? listFocus);
|
|
3038
3086
|
setView("list");
|
|
@@ -3041,7 +3089,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3041
3089
|
setChatNavigation(null);
|
|
3042
3090
|
setNewNavigation(null);
|
|
3043
3091
|
setView("new");
|
|
3044
|
-
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null }) : null,
|
|
3092
|
+
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null, composerCommand }) : null,
|
|
3045
3093
|
opening ? /* @__PURE__ */ jsxs8("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
3046
3094
|
/* @__PURE__ */ jsx9(HarnessLogo, { id: opening.harness, size: 34 }),
|
|
3047
3095
|
/* @__PURE__ */ jsxs8("span", { children: [
|
package/index.d.ts
CHANGED
|
@@ -483,6 +483,11 @@ export type MessengerNavigation =
|
|
|
483
483
|
| { id: string | number; view: 'chat'; sessionKey: string; draft?: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
|
|
484
484
|
| { id: string | number; view: 'new'; harness?: HarnessId; draft?: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> };
|
|
485
485
|
|
|
486
|
+
/** Event-like host command that focuses the active composer or appends context without replacing its draft. */
|
|
487
|
+
export type MessengerComposerCommand =
|
|
488
|
+
| { id: string | number; action: 'focus' }
|
|
489
|
+
| { id: string | number; action: 'attach'; attachments: TranscriptAttachment | TranscriptAttachment[] };
|
|
490
|
+
|
|
486
491
|
export interface MessengerProps {
|
|
487
492
|
state: SupercodeUiState | unknown;
|
|
488
493
|
adapter: UiAdapter;
|
|
@@ -490,6 +495,8 @@ export interface MessengerProps {
|
|
|
490
495
|
initialView?: 'list' | 'chat' | 'new';
|
|
491
496
|
/** Event-like host navigation. Change `id` to issue a new request. */
|
|
492
497
|
navigation?: MessengerNavigation | null;
|
|
498
|
+
/** Event-like composer command. Change `id` to focus or append attachments to the current draft. */
|
|
499
|
+
composerCommand?: MessengerComposerCommand | null;
|
|
493
500
|
onViewChange?(view: 'list' | 'chat' | 'new'): void;
|
|
494
501
|
/** Deliberate host-provided items that can be attached without opening a picker. */
|
|
495
502
|
contextCandidates?: TranscriptAttachment[];
|
|
@@ -549,7 +556,7 @@ export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapte
|
|
|
549
556
|
export function SessionRow(props: { row: SessionRowModel; state: SupercodeUiState; adapter: UiAdapter; now?: number; onOpen(row: SessionRowModel): void }): VNode;
|
|
550
557
|
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
558
|
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;
|
|
559
|
+
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
560
|
export function SupercodeMessenger(props: MessengerProps): VNode;
|
|
554
561
|
|
|
555
562
|
export interface MountedMessenger {
|
package/messenger.mjs
CHANGED
|
@@ -1280,7 +1280,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1280
1280
|
] })
|
|
1281
1281
|
] });
|
|
1282
1282
|
}
|
|
1283
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1283
|
+
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
1284
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
1285
1285
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
1286
1286
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -1292,6 +1292,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1292
1292
|
const [dragging, setDragging] = useState2(false);
|
|
1293
1293
|
const [pickerError, setPickerError] = useState2(null);
|
|
1294
1294
|
const textarea = useRef2(null);
|
|
1295
|
+
const lastCommand = useRef2(null);
|
|
1295
1296
|
useAutosizeTextarea(textarea, draft);
|
|
1296
1297
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
1297
1298
|
useEffect2(() => {
|
|
@@ -1333,6 +1334,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1333
1334
|
textarea.current?.focus({ preventScroll: true });
|
|
1334
1335
|
onDraftRestored?.(restoreDraft.id);
|
|
1335
1336
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
1337
|
+
useEffect2(() => {
|
|
1338
|
+
if (!command || command.id === lastCommand.current) return;
|
|
1339
|
+
lastCommand.current = command.id;
|
|
1340
|
+
if (command.action === "attach") {
|
|
1341
|
+
try {
|
|
1342
|
+
const attachments = partitionAttachments(command.attachments);
|
|
1343
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1344
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1345
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
1346
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
1347
|
+
setContext(nextContext);
|
|
1348
|
+
setImages(nextImages);
|
|
1349
|
+
setPickerError(null);
|
|
1350
|
+
remember(draft, nextContext, nextImages, queue);
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
textarea.current?.focus({ preventScroll: true });
|
|
1356
|
+
}, [command?.id]);
|
|
1336
1357
|
useEffect2(() => {
|
|
1337
1358
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
1338
1359
|
return () => clearTimeout(timer);
|
|
@@ -2613,7 +2634,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
|
|
|
2613
2634
|
onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
|
|
2614
2635
|
] });
|
|
2615
2636
|
}
|
|
2616
|
-
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation }) {
|
|
2637
|
+
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
|
|
2617
2638
|
const Header = slots.header;
|
|
2618
2639
|
const HeaderActions = slots.headerActions;
|
|
2619
2640
|
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
@@ -2740,11 +2761,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2740
2761
|
/* @__PURE__ */ jsx9(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
|
|
2741
2762
|
/* @__PURE__ */ jsx9(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
|
|
2742
2763
|
/* @__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,
|
|
2764
|
+
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
2765
|
settings ? /* @__PURE__ */ jsx9(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
|
|
2745
2766
|
] });
|
|
2746
2767
|
}
|
|
2747
|
-
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation }) {
|
|
2768
|
+
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand }) {
|
|
2748
2769
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2749
2770
|
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2750
2771
|
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
@@ -2758,6 +2779,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2758
2779
|
const [dragging, setDragging] = useState6(false);
|
|
2759
2780
|
const [pickerError, setPickerError] = useState6(null);
|
|
2760
2781
|
const startSequence = useRef7(0);
|
|
2782
|
+
const lastComposerCommand = useRef7(null);
|
|
2761
2783
|
const textarea = useRef7(null);
|
|
2762
2784
|
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2763
2785
|
const Picker = components.HarnessPicker ?? HarnessPicker;
|
|
@@ -2801,6 +2823,32 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2801
2823
|
remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
|
|
2802
2824
|
textarea.current?.focus({ preventScroll: true });
|
|
2803
2825
|
}, [navigation?.id]);
|
|
2826
|
+
useEffect8(() => {
|
|
2827
|
+
if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
|
|
2828
|
+
lastComposerCommand.current = composerCommand.id;
|
|
2829
|
+
if (composerCommand.action === "attach") {
|
|
2830
|
+
if (mode === "terminal" && !launchModes.includes("headless")) {
|
|
2831
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2832
|
+
} else {
|
|
2833
|
+
try {
|
|
2834
|
+
const attachments = partitionAttachments(composerCommand.attachments);
|
|
2835
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2836
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2837
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
2838
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
2839
|
+
const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
|
|
2840
|
+
if (nextModes !== modes) setModes(nextModes);
|
|
2841
|
+
setContext(nextContext);
|
|
2842
|
+
setImages(nextImages);
|
|
2843
|
+
setPickerError(null);
|
|
2844
|
+
remember({ context: nextContext, images: nextImages, modes: nextModes });
|
|
2845
|
+
} catch (error) {
|
|
2846
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
textarea.current?.focus({ preventScroll: true });
|
|
2851
|
+
}, [composerCommand?.id]);
|
|
2804
2852
|
const pickContext = () => {
|
|
2805
2853
|
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2806
2854
|
setPicking(true);
|
|
@@ -2958,7 +3006,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2958
3006
|
] })
|
|
2959
3007
|
] });
|
|
2960
3008
|
}
|
|
2961
|
-
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3009
|
+
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
2962
3010
|
const state = useMemo3(() => normalizeUiState(stateInput), [stateInput]);
|
|
2963
3011
|
const contextCandidates = useMemo3(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
|
|
2964
3012
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
@@ -3029,7 +3077,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3029
3077
|
setNewNavigation(null);
|
|
3030
3078
|
setView("new");
|
|
3031
3079
|
}, 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,
|
|
3080
|
+
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
3081
|
view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
|
|
3034
3082
|
setListFocus(state.attached?.key ?? listFocus);
|
|
3035
3083
|
setView("list");
|
|
@@ -3038,7 +3086,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3038
3086
|
setChatNavigation(null);
|
|
3039
3087
|
setNewNavigation(null);
|
|
3040
3088
|
setView("new");
|
|
3041
|
-
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null }) : null,
|
|
3089
|
+
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null, composerCommand }) : null,
|
|
3042
3090
|
opening ? /* @__PURE__ */ jsxs8("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
3043
3091
|
/* @__PURE__ */ jsx9(HarnessLogo, { id: opening.harness, size: 34 }),
|
|
3044
3092
|
/* @__PURE__ */ jsxs8("span", { children: [
|
package/package.json
CHANGED
package/react/components.mjs
CHANGED
|
@@ -1317,7 +1317,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1317
1317
|
] })
|
|
1318
1318
|
] });
|
|
1319
1319
|
}
|
|
1320
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1320
|
+
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
1321
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
1322
1322
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
1323
1323
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -1329,6 +1329,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1329
1329
|
const [dragging, setDragging] = useState2(false);
|
|
1330
1330
|
const [pickerError, setPickerError] = useState2(null);
|
|
1331
1331
|
const textarea = useRef2(null);
|
|
1332
|
+
const lastCommand = useRef2(null);
|
|
1332
1333
|
useAutosizeTextarea(textarea, draft);
|
|
1333
1334
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
1334
1335
|
useEffect2(() => {
|
|
@@ -1370,6 +1371,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1370
1371
|
textarea.current?.focus({ preventScroll: true });
|
|
1371
1372
|
onDraftRestored?.(restoreDraft.id);
|
|
1372
1373
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
1374
|
+
useEffect2(() => {
|
|
1375
|
+
if (!command || command.id === lastCommand.current) return;
|
|
1376
|
+
lastCommand.current = command.id;
|
|
1377
|
+
if (command.action === "attach") {
|
|
1378
|
+
try {
|
|
1379
|
+
const attachments = partitionAttachments(command.attachments);
|
|
1380
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1381
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1382
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
1383
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
1384
|
+
setContext(nextContext);
|
|
1385
|
+
setImages(nextImages);
|
|
1386
|
+
setPickerError(null);
|
|
1387
|
+
remember(draft, nextContext, nextImages, queue);
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
textarea.current?.focus({ preventScroll: true });
|
|
1393
|
+
}, [command?.id]);
|
|
1373
1394
|
useEffect2(() => {
|
|
1374
1395
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
1375
1396
|
return () => clearTimeout(timer);
|
|
@@ -2696,7 +2717,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
|
|
|
2696
2717
|
onClose ? /* @__PURE__ */ jsx10("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx10(UiIcon, { name: "close", size: 18 }) }) : null
|
|
2697
2718
|
] });
|
|
2698
2719
|
}
|
|
2699
|
-
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation }) {
|
|
2720
|
+
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
|
|
2700
2721
|
const Header = slots.header;
|
|
2701
2722
|
const HeaderActions = slots.headerActions;
|
|
2702
2723
|
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
@@ -2823,11 +2844,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2823
2844
|
/* @__PURE__ */ jsx10(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
|
|
2824
2845
|
/* @__PURE__ */ jsx10(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
|
|
2825
2846
|
/* @__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,
|
|
2847
|
+
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
2848
|
settings ? /* @__PURE__ */ jsx10(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
|
|
2828
2849
|
] });
|
|
2829
2850
|
}
|
|
2830
|
-
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation }) {
|
|
2851
|
+
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand }) {
|
|
2831
2852
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2832
2853
|
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2833
2854
|
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
@@ -2841,6 +2862,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2841
2862
|
const [dragging, setDragging] = useState6(false);
|
|
2842
2863
|
const [pickerError, setPickerError] = useState6(null);
|
|
2843
2864
|
const startSequence = useRef7(0);
|
|
2865
|
+
const lastComposerCommand = useRef7(null);
|
|
2844
2866
|
const textarea = useRef7(null);
|
|
2845
2867
|
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2846
2868
|
const Picker = components.HarnessPicker ?? HarnessPicker;
|
|
@@ -2884,6 +2906,32 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2884
2906
|
remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
|
|
2885
2907
|
textarea.current?.focus({ preventScroll: true });
|
|
2886
2908
|
}, [navigation?.id]);
|
|
2909
|
+
useEffect8(() => {
|
|
2910
|
+
if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
|
|
2911
|
+
lastComposerCommand.current = composerCommand.id;
|
|
2912
|
+
if (composerCommand.action === "attach") {
|
|
2913
|
+
if (mode === "terminal" && !launchModes.includes("headless")) {
|
|
2914
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2915
|
+
} else {
|
|
2916
|
+
try {
|
|
2917
|
+
const attachments = partitionAttachments(composerCommand.attachments);
|
|
2918
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2919
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2920
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
2921
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
2922
|
+
const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
|
|
2923
|
+
if (nextModes !== modes) setModes(nextModes);
|
|
2924
|
+
setContext(nextContext);
|
|
2925
|
+
setImages(nextImages);
|
|
2926
|
+
setPickerError(null);
|
|
2927
|
+
remember({ context: nextContext, images: nextImages, modes: nextModes });
|
|
2928
|
+
} catch (error) {
|
|
2929
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
textarea.current?.focus({ preventScroll: true });
|
|
2934
|
+
}, [composerCommand?.id]);
|
|
2887
2935
|
const pickContext = () => {
|
|
2888
2936
|
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2889
2937
|
setPicking(true);
|
|
@@ -3041,7 +3089,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
3041
3089
|
] })
|
|
3042
3090
|
] });
|
|
3043
3091
|
}
|
|
3044
|
-
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3092
|
+
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3045
3093
|
const state = useMemo3(() => normalizeUiState(stateInput), [stateInput]);
|
|
3046
3094
|
const contextCandidates = useMemo3(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
|
|
3047
3095
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
@@ -3112,7 +3160,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3112
3160
|
setNewNavigation(null);
|
|
3113
3161
|
setView("new");
|
|
3114
3162
|
}, 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,
|
|
3163
|
+
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
3164
|
view === "chat" ? /* @__PURE__ */ jsx10(Chat, { state, adapter, onBack: () => {
|
|
3117
3165
|
setListFocus(state.attached?.key ?? listFocus);
|
|
3118
3166
|
setView("list");
|
|
@@ -3121,7 +3169,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3121
3169
|
setChatNavigation(null);
|
|
3122
3170
|
setNewNavigation(null);
|
|
3123
3171
|
setView("new");
|
|
3124
|
-
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null }) : null,
|
|
3172
|
+
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null, composerCommand }) : null,
|
|
3125
3173
|
opening ? /* @__PURE__ */ jsxs9("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
3126
3174
|
/* @__PURE__ */ jsx10(HarnessLogo, { id: opening.harness, size: 34 }),
|
|
3127
3175
|
/* @__PURE__ */ jsxs9("span", { children: [
|
package/react/composer.mjs
CHANGED
|
@@ -318,7 +318,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
318
318
|
] })
|
|
319
319
|
] });
|
|
320
320
|
}
|
|
321
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
321
|
+
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
322
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
323
323
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
324
324
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -330,6 +330,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
330
330
|
const [dragging, setDragging] = useState2(false);
|
|
331
331
|
const [pickerError, setPickerError] = useState2(null);
|
|
332
332
|
const textarea = useRef2(null);
|
|
333
|
+
const lastCommand = useRef2(null);
|
|
333
334
|
useAutosizeTextarea(textarea, draft);
|
|
334
335
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
335
336
|
useEffect2(() => {
|
|
@@ -371,6 +372,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
371
372
|
textarea.current?.focus({ preventScroll: true });
|
|
372
373
|
onDraftRestored?.(restoreDraft.id);
|
|
373
374
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
375
|
+
useEffect2(() => {
|
|
376
|
+
if (!command || command.id === lastCommand.current) return;
|
|
377
|
+
lastCommand.current = command.id;
|
|
378
|
+
if (command.action === "attach") {
|
|
379
|
+
try {
|
|
380
|
+
const attachments = partitionAttachments(command.attachments);
|
|
381
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
382
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
383
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
384
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
385
|
+
setContext(nextContext);
|
|
386
|
+
setImages(nextImages);
|
|
387
|
+
setPickerError(null);
|
|
388
|
+
remember(draft, nextContext, nextImages, queue);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
textarea.current?.focus({ preventScroll: true });
|
|
394
|
+
}, [command?.id]);
|
|
374
395
|
useEffect2(() => {
|
|
375
396
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
376
397
|
return () => clearTimeout(timer);
|
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
|
@@ -1280,7 +1280,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1280
1280
|
] })
|
|
1281
1281
|
] });
|
|
1282
1282
|
}
|
|
1283
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
1283
|
+
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
1284
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
1285
1285
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
1286
1286
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
@@ -1292,6 +1292,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1292
1292
|
const [dragging, setDragging] = useState2(false);
|
|
1293
1293
|
const [pickerError, setPickerError] = useState2(null);
|
|
1294
1294
|
const textarea = useRef2(null);
|
|
1295
|
+
const lastCommand = useRef2(null);
|
|
1295
1296
|
useAutosizeTextarea(textarea, draft);
|
|
1296
1297
|
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
1297
1298
|
useEffect2(() => {
|
|
@@ -1333,6 +1334,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
1333
1334
|
textarea.current?.focus({ preventScroll: true });
|
|
1334
1335
|
onDraftRestored?.(restoreDraft.id);
|
|
1335
1336
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
1337
|
+
useEffect2(() => {
|
|
1338
|
+
if (!command || command.id === lastCommand.current) return;
|
|
1339
|
+
lastCommand.current = command.id;
|
|
1340
|
+
if (command.action === "attach") {
|
|
1341
|
+
try {
|
|
1342
|
+
const attachments = partitionAttachments(command.attachments);
|
|
1343
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1344
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1345
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
1346
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
1347
|
+
setContext(nextContext);
|
|
1348
|
+
setImages(nextImages);
|
|
1349
|
+
setPickerError(null);
|
|
1350
|
+
remember(draft, nextContext, nextImages, queue);
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
textarea.current?.focus({ preventScroll: true });
|
|
1356
|
+
}, [command?.id]);
|
|
1336
1357
|
useEffect2(() => {
|
|
1337
1358
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
1338
1359
|
return () => clearTimeout(timer);
|
|
@@ -2613,7 +2634,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
|
|
|
2613
2634
|
onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
|
|
2614
2635
|
] });
|
|
2615
2636
|
}
|
|
2616
|
-
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation }) {
|
|
2637
|
+
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
|
|
2617
2638
|
const Header = slots.header;
|
|
2618
2639
|
const HeaderActions = slots.headerActions;
|
|
2619
2640
|
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
@@ -2740,11 +2761,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2740
2761
|
/* @__PURE__ */ jsx9(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
|
|
2741
2762
|
/* @__PURE__ */ jsx9(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
|
|
2742
2763
|
/* @__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,
|
|
2764
|
+
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
2765
|
settings ? /* @__PURE__ */ jsx9(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
|
|
2745
2766
|
] });
|
|
2746
2767
|
}
|
|
2747
|
-
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation }) {
|
|
2768
|
+
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand }) {
|
|
2748
2769
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2749
2770
|
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2750
2771
|
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
@@ -2758,6 +2779,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2758
2779
|
const [dragging, setDragging] = useState6(false);
|
|
2759
2780
|
const [pickerError, setPickerError] = useState6(null);
|
|
2760
2781
|
const startSequence = useRef7(0);
|
|
2782
|
+
const lastComposerCommand = useRef7(null);
|
|
2761
2783
|
const textarea = useRef7(null);
|
|
2762
2784
|
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2763
2785
|
const Picker = components.HarnessPicker ?? HarnessPicker;
|
|
@@ -2801,6 +2823,32 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2801
2823
|
remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
|
|
2802
2824
|
textarea.current?.focus({ preventScroll: true });
|
|
2803
2825
|
}, [navigation?.id]);
|
|
2826
|
+
useEffect8(() => {
|
|
2827
|
+
if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
|
|
2828
|
+
lastComposerCommand.current = composerCommand.id;
|
|
2829
|
+
if (composerCommand.action === "attach") {
|
|
2830
|
+
if (mode === "terminal" && !launchModes.includes("headless")) {
|
|
2831
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2832
|
+
} else {
|
|
2833
|
+
try {
|
|
2834
|
+
const attachments = partitionAttachments(composerCommand.attachments);
|
|
2835
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2836
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2837
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
2838
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
2839
|
+
const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
|
|
2840
|
+
if (nextModes !== modes) setModes(nextModes);
|
|
2841
|
+
setContext(nextContext);
|
|
2842
|
+
setImages(nextImages);
|
|
2843
|
+
setPickerError(null);
|
|
2844
|
+
remember({ context: nextContext, images: nextImages, modes: nextModes });
|
|
2845
|
+
} catch (error) {
|
|
2846
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
textarea.current?.focus({ preventScroll: true });
|
|
2851
|
+
}, [composerCommand?.id]);
|
|
2804
2852
|
const pickContext = () => {
|
|
2805
2853
|
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2806
2854
|
setPicking(true);
|
|
@@ -2958,7 +3006,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2958
3006
|
] })
|
|
2959
3007
|
] });
|
|
2960
3008
|
}
|
|
2961
|
-
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
3009
|
+
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
2962
3010
|
const state = useMemo3(() => normalizeUiState(stateInput), [stateInput]);
|
|
2963
3011
|
const contextCandidates = useMemo3(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
|
|
2964
3012
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
@@ -3029,7 +3077,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3029
3077
|
setNewNavigation(null);
|
|
3030
3078
|
setView("new");
|
|
3031
3079
|
}, 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,
|
|
3080
|
+
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
3081
|
view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
|
|
3034
3082
|
setListFocus(state.attached?.key ?? listFocus);
|
|
3035
3083
|
setView("list");
|
|
@@ -3038,7 +3086,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
3038
3086
|
setChatNavigation(null);
|
|
3039
3087
|
setNewNavigation(null);
|
|
3040
3088
|
setView("new");
|
|
3041
|
-
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null }) : null,
|
|
3089
|
+
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null, composerCommand }) : null,
|
|
3042
3090
|
opening ? /* @__PURE__ */ jsxs8("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
3043
3091
|
/* @__PURE__ */ jsx9(HarnessLogo, { id: opening.harness, size: 34 }),
|
|
3044
3092
|
/* @__PURE__ */ jsxs8("span", { children: [
|