@volter-ai-dev/supercode-ui 0.1.42 → 0.1.44

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