@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 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. Context candidates use the exact
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter-ai-dev/supercode-ui",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "type": "module",
5
5
  "description": "Composable default UI kit for Supercode-powered coding-agent experiences",
6
6
  "exports": {
@@ -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: [
@@ -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;
@@ -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: [