@volter-ai-dev/supercode-ui 0.1.36 → 0.1.38

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/composer.mjs CHANGED
@@ -32,6 +32,7 @@ var EMPTY_UI_STATE = Object.freeze({
32
32
  mode: "none",
33
33
  strategy: null,
34
34
  canSend: false,
35
+ canSteer: false,
35
36
  canResume: false,
36
37
  continuationModes: Object.freeze([]),
37
38
  canBranch: false,
@@ -67,6 +68,16 @@ var EMPTY_UI_STATE = Object.freeze({
67
68
  function harnessDisplayName(id) {
68
69
  return HARNESS_NAMES[id] ?? id;
69
70
  }
71
+ var ACTIVITY_PRIORITY = Object.freeze({
72
+ "needs-input": 70,
73
+ failed: 60,
74
+ working: 50,
75
+ unseen: 40,
76
+ finished: 30,
77
+ running: 20,
78
+ recent: 10,
79
+ idle: 0
80
+ });
70
81
  function canContinueHere(state) {
71
82
  if (state.mode !== "mirror" || state.canSend) return false;
72
83
  const row = state.attached?.key ? state.sessions.find((item) => item.key === state.attached.key) : null;
@@ -130,7 +141,7 @@ var ICONS = {
130
141
  function UiIcon({ name, size = 16, class: className = "" }) {
131
142
  const Glyph = ICONS[name];
132
143
  if (!Glyph) return null;
133
- return /* @__PURE__ */ jsx("svg", { class: `scui-icon ${className}`, style: { "--scui-icon-size": `${size}px` }, viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx(Glyph, {}) });
144
+ return /* @__PURE__ */ jsx("svg", { className: `scui-icon ${className}`, style: { "--scui-icon-size": `${size}px` }, viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx(Glyph, {}) });
134
145
  }
135
146
 
136
147
  // src/context.jsx
@@ -204,6 +215,26 @@ function partitionAttachments(value) {
204
215
  }
205
216
  return { context, images };
206
217
  }
218
+ function attachmentKey(item) {
219
+ if (item.id) return `id:${item.id}`;
220
+ return "detail" in item ? `context:${item.kind ?? ""}\0${item.label}\0${item.detail}` : `image:${item.url}`;
221
+ }
222
+ function ContextCandidate({ attachment, attached, onAttach }) {
223
+ const image = "url" in attachment;
224
+ return /* @__PURE__ */ jsxs2("button", { type: "button", disabled: attached, "aria-label": `${attached ? "Attached" : "Attach"} ${attachment.label}`, onClick: () => onAttach(attachment), children: [
225
+ image ? /* @__PURE__ */ jsx2("img", { src: attachment.url, alt: "" }) : /* @__PURE__ */ jsx2(UiIcon, { name: "attach", size: 13 }),
226
+ /* @__PURE__ */ jsxs2("span", { children: [
227
+ /* @__PURE__ */ jsx2("strong", { children: attachment.label }),
228
+ /* @__PURE__ */ jsx2("small", { children: image ? "Image" : attachment.kind || "Context" })
229
+ ] }),
230
+ attached ? /* @__PURE__ */ jsx2(UiIcon, { name: "check", size: 13 }) : /* @__PURE__ */ jsx2(UiIcon, { name: "plus", size: 13 })
231
+ ] });
232
+ }
233
+ function ContextCandidates({ items, context, images, state, adapter, onAttach, component: Candidate = ContextCandidate }) {
234
+ if (!items.length) return null;
235
+ const attached = new Set([...context, ...images].map(attachmentKey));
236
+ return /* @__PURE__ */ jsx2("div", { className: "scui-context-candidates", "aria-label": "Available context", children: items.map((attachment, index) => /* @__PURE__ */ jsx2(Candidate, { value: attachment, attachment, attached: attached.has(attachmentKey(attachment)), state, adapter, onAttach, index }, attachmentKey(attachment))) });
237
+ }
207
238
  async function imageAttachmentsFromFiles(value) {
208
239
  const files = Array.from(value ?? []).filter((file) => file?.type?.startsWith("image/"));
209
240
  if (files.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images at a time.`);
@@ -221,7 +252,7 @@ async function imageAttachmentsFromFiles(value) {
221
252
  }
222
253
  function ContextTray({ items, onRemove }) {
223
254
  if (!items.length) return null;
224
- return /* @__PURE__ */ jsx2("div", { class: "scui-compose-context", "aria-label": "Attached context", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
255
+ return /* @__PURE__ */ jsx2("div", { className: "scui-compose-context", "aria-label": "Attached context", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
225
256
  /* @__PURE__ */ jsx2(UiIcon, { name: "attach", size: 12 }),
226
257
  /* @__PURE__ */ jsx2("strong", { children: item.label }),
227
258
  /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove context ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) })
@@ -229,13 +260,23 @@ function ContextTray({ items, onRemove }) {
229
260
  }
230
261
  function ImageTray({ items, onRemove }) {
231
262
  if (!items.length) return null;
232
- return /* @__PURE__ */ jsx2("div", { class: "scui-compose-images", "aria-label": "Attached images", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
263
+ return /* @__PURE__ */ jsx2("div", { className: "scui-compose-images", "aria-label": "Attached images", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
233
264
  /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }),
234
265
  /* @__PURE__ */ jsx2("strong", { children: item.label }),
235
266
  onRemove ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove image ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) }) : null
236
267
  ] }, item.id ?? `${item.label}:${index}`)) });
237
268
  }
238
269
 
270
+ // src/intent.js
271
+ var CONFIRMABLE_ACTIONS = /* @__PURE__ */ new Set(["resume", "join", "branch", "reduce", "export"]);
272
+ async function dispatchConfirmedIntent(adapter, intent) {
273
+ if (CONFIRMABLE_ACTIONS.has(intent.action) && adapter.confirmIntent) {
274
+ const confirmed = await adapter.confirmIntent(intent);
275
+ if (!confirmed) return;
276
+ }
277
+ return adapter.onIntent(intent);
278
+ }
279
+
239
280
  // src/textarea.js
240
281
  import { useLayoutEffect } from "preact/hooks";
241
282
  function useAutosizeTextarea(ref, value) {
@@ -262,28 +303,29 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
262
303
  const terminal = resume && state.continuationModes?.includes("terminal");
263
304
  const join = state.canAttach;
264
305
  const branch = state.canBranch;
265
- if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { class: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
306
+ if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
266
307
  /* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
267
308
  /* @__PURE__ */ jsx3("small", { children: "This session cannot be continued by an available harness." })
268
309
  ] }) });
269
- return /* @__PURE__ */ jsxs3("div", { class: "scui-continuation", children: [
310
+ return /* @__PURE__ */ jsxs3("div", { className: "scui-continuation", children: [
270
311
  /* @__PURE__ */ jsxs3("span", { children: [
271
312
  /* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
272
313
  /* @__PURE__ */ jsx3("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
273
314
  ] }),
274
- /* @__PURE__ */ jsxs3("span", { class: "scui-continuation-actions", children: [
275
- /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter.onIntent(join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
276
- terminal ? /* @__PURE__ */ jsx3("button", { type: "button", class: "scui-secondary", disabled: Boolean(state.operation), onClick: () => adapter.onIntent({ action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
315
+ /* @__PURE__ */ jsxs3("span", { className: "scui-continuation-actions", children: [
316
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
317
+ terminal ? /* @__PURE__ */ jsx3("button", { type: "button", className: "scui-secondary", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, { action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
277
318
  ] })
278
319
  ] });
279
320
  }
280
- function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
321
+ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
281
322
  const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
282
323
  const [draft, setDraft] = useState2(remembered.draft);
283
324
  const [context, setContext] = useState2(remembered.context ?? []);
284
325
  const [images, setImages] = useState2(remembered.images ?? []);
285
326
  const [queue, setQueue] = useState2((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
286
327
  const [dispatching, setDispatching] = useState2(false);
328
+ const [steering, setSteering] = useState2(false);
287
329
  const [picking, setPicking] = useState2(false);
288
330
  const [dragging, setDragging] = useState2(false);
289
331
  const [pickerError, setPickerError] = useState2(null);
@@ -298,8 +340,10 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
298
340
  remember(draft, context, images, next);
299
341
  return next;
300
342
  });
301
- const queueBlocked = state.busy || pendingStatus !== null || dispatching;
302
- const queuesNewMessage = state.busy || pendingStatus === "sending" || pendingStatus === "failed" || dispatching;
343
+ const queueBlocked = state.busy || pendingStatus !== null || dispatching || steering;
344
+ const queuesNewMessage = state.busy || pendingStatus === "sending" || pendingStatus === "failed" || dispatching || steering;
345
+ const steerAvailable = state.busy && state.canSteer && !steering && pendingStatus === null;
346
+ const canSteerDraft = steerAvailable && Boolean(draft.trim()) && context.length === 0 && images.length === 0;
303
347
  useEffect2(() => {
304
348
  if (!queueBlocked && state.canSend && queue.length) {
305
349
  const [next, ...rest] = queue;
@@ -351,6 +395,14 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
351
395
  });
352
396
  }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
353
397
  };
398
+ const attachCandidate = (picked) => {
399
+ const attachments = partitionAttachments(picked);
400
+ const nextContext = mergeContext(context, attachments.context);
401
+ const nextImages = mergeImages(images, attachments.images);
402
+ setContext(nextContext);
403
+ setImages(nextImages);
404
+ remember(draft, nextContext, nextImages, queue);
405
+ };
354
406
  const addImageFiles = (value, source) => {
355
407
  const allFiles = Array.from(value ?? []);
356
408
  if (!allFiles.length) return false;
@@ -379,9 +431,17 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
379
431
  setDragging(false);
380
432
  if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
381
433
  };
382
- const send = () => {
434
+ const send = (forceQueue = false) => {
383
435
  const text = draft.trim();
384
436
  if (!text && !images.length) return;
437
+ if (canSteerDraft && !forceQueue) {
438
+ setSteering(true);
439
+ Promise.resolve(adapter.onIntent({ action: "steer", text })).then(() => {
440
+ setDraft("");
441
+ remember("", context, images, queue);
442
+ }, (error) => setPickerError(error instanceof Error ? error.message : "Could not steer the active turn.")).finally(() => setSteering(false));
443
+ return;
444
+ }
385
445
  const message = { text, context, images };
386
446
  if (queuesNewMessage) updateQueue((items) => [...items, message]);
387
447
  else if (state.canSend) {
@@ -394,8 +454,8 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
394
454
  setImages([]);
395
455
  remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
396
456
  };
397
- return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
398
- queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
457
+ return /* @__PURE__ */ jsxs3("div", { className: "scui-compose", children: [
458
+ queue.length ? /* @__PURE__ */ jsxs3("div", { className: "scui-queue", children: [
399
459
  /* @__PURE__ */ jsxs3("strong", { children: [
400
460
  queue.length,
401
461
  " queued"
@@ -411,6 +471,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
411
471
  /* @__PURE__ */ jsx3("button", { type: "button", "aria-label": `Remove queued message ${index + 1}`, onClick: () => updateQueue((items) => items.filter((_, itemIndex) => itemIndex !== index)), children: /* @__PURE__ */ jsx3(UiIcon, { name: "close", size: 13 }) })
412
472
  ] }, `${index}:${item.text}`))
413
473
  ] }) : null,
474
+ /* @__PURE__ */ jsx3(ContextCandidates, { items: contextCandidates, context, images, state, adapter, onAttach: attachCandidate, component: components.ContextCandidate }),
414
475
  /* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
415
476
  const next = items.filter((_, itemIndex) => itemIndex !== index);
416
477
  remember(draft, context, next, queue);
@@ -421,8 +482,8 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
421
482
  remember(draft, next, images, queue);
422
483
  return next;
423
484
  }) }),
424
- pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
425
- /* @__PURE__ */ jsxs3("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
485
+ pickerError ? /* @__PURE__ */ jsx3("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
486
+ /* @__PURE__ */ jsxs3("div", { className: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
426
487
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
427
488
  event.preventDefault();
428
489
  setDragging(true);
@@ -432,8 +493,8 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
432
493
  }, onDragLeave: (event) => {
433
494
  if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
434
495
  }, onDrop: dropImages, children: [
435
- adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
436
- /* @__PURE__ */ jsx3("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" : pendingStatus === "failed" ? "Retry or edit the unsent message\u2026" : pendingStatus === "editing" ? "Edit and resend\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
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,
497
+ /* @__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) => {
437
498
  const value = event.currentTarget.value;
438
499
  setDraft(value);
439
500
  remember(value, context, images, queue);
@@ -444,8 +505,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
444
505
  }
445
506
  } }),
446
507
  /* @__PURE__ */ jsxs3("span", { children: [
447
- state.busy ? /* @__PURE__ */ jsx3("button", { class: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: /* @__PURE__ */ jsx3(UiIcon, { name: "stop", size: 15 }) }) : null,
448
- /* @__PURE__ */ jsx3("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() && !images.length || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx3(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
508
+ state.busy ? /* @__PURE__ */ jsx3("button", { className: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: /* @__PURE__ */ jsx3(UiIcon, { name: "stop", size: 15 }) }) : null,
509
+ canSteerDraft ? /* @__PURE__ */ jsx3("button", { className: "scui-queue-send", type: "button", "aria-label": "Queue follow-up instead", onClick: () => send(true), children: /* @__PURE__ */ jsx3(UiIcon, { name: "plus", size: 15 }) }) : null,
510
+ /* @__PURE__ */ jsx3("button", { className: "scui-send", type: "button", "aria-label": canSteerDraft ? "Steer current turn" : queuesNewMessage ? "Queue message" : "Send message", disabled: steering || !draft.trim() && !images.length || !queuesNewMessage && !state.canSend, onClick: () => send(), children: steering ? /* @__PURE__ */ jsx3("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: canSteerDraft ? "send" : queuesNewMessage ? "plus" : "send", size: 17 }) })
449
511
  ] })
450
512
  ] })
451
513
  ] });
package/controller.d.ts CHANGED
@@ -115,6 +115,10 @@ export interface ControllerBindingOptions {
115
115
  onResumeTerminal?: (intent: Extract<SupercodeUiIntent, { action: 'resume' }>) => void | Promise<void>;
116
116
  copyText?: UiAdapter['copyText'];
117
117
  resolveImage?: UiAdapter['resolveImage'];
118
+ confirmIntent?: UiAdapter['confirmIntent'];
119
+ pickContext?: UiAdapter['pickContext'];
120
+ onClose?: UiAdapter['onClose'];
121
+ onOpen?: UiAdapter['onOpen'];
118
122
  }
119
123
 
120
124
  export interface ControllerIntentTarget {
package/controller.mjs CHANGED
@@ -275,12 +275,13 @@ function projectConversationEntry(entry, maxEntryChars, imageRegistry) {
275
275
  if (entry.kind === 'tool') {
276
276
  const argumentsText = truncate((entry.arguments ?? '').trim(), maxEntryChars);
277
277
  const result = truncate((entry.resultText ?? '').trim(), maxEntryChars);
278
- const primary = entry.status === 'pending' ? argumentsText : result;
279
278
  const projected = {
280
279
  id: entry.id,
281
280
  role: 'tool',
282
281
  ...(Number.isSafeInteger(entry.messageIndex) && entry.messageIndex > 0 ? { messageIndex: entry.messageIndex } : {}),
283
- text: primary.text,
282
+ // Tool payloads already have dedicated bounded fields. Repeating the
283
+ // result in `text` nearly doubles transcript transport and storage.
284
+ text: '',
284
285
  ts: timestampFromMetadata(entry.metadata),
285
286
  truncated: argumentsText.truncated || result.truncated,
286
287
  label: entry.name ?? 'tool',
@@ -470,6 +471,7 @@ function projectClientSnapshotInternal(snapshot, options, imageRegistry) {
470
471
  mode: snapshot.connection?.mode ?? 'none',
471
472
  strategy: snapshot.connection?.strategy ?? null,
472
473
  canSend: actions.send === true,
474
+ canSteer: actions.steer === true,
473
475
  canResume: actions.resume === true,
474
476
  continuationModes: options.continuationModes ?? (actions.resume === true ? ['headless'] : []),
475
477
  canBranch: actions.branch === true,
@@ -508,6 +510,19 @@ function projectClientSnapshotInternal(snapshot, options, imageRegistry) {
508
510
  installed: harness.installed === true,
509
511
  startable: harness.availableActions?.start === true,
510
512
  reason: harness.reason ?? null,
513
+ auth: harness.auth,
514
+ runtime: harness.runtime,
515
+ protocol: harness.protocol,
516
+ repair: harness.repair ?? null,
517
+ capabilities: {
518
+ start: harness.availableActions?.start === true,
519
+ resume: harness.availableActions?.resume === true,
520
+ attach: harness.availableActions?.attach === true,
521
+ send: harness.availableActions?.send === true,
522
+ interrupt: harness.availableActions?.interrupt === true,
523
+ steer: harness.availableActions?.steer === true,
524
+ respond: harness.availableActions?.respond === true,
525
+ },
511
526
  })),
512
527
  history: {
513
528
  sessionLimit: options.history?.sessionLimit ?? sessions.length,
@@ -552,6 +567,7 @@ export async function dispatchControllerIntent(controller, intent, options = {})
552
567
  if (intent.action === 'mounted') return;
553
568
  if (intent.action === 'attach') return controller.dispatch({ type: 'observe', sessionKey: intent.key });
554
569
  if (intent.action === 'send') return controller.dispatch({ type: 'send', text: intent.text, ...(intent.context?.length ? { context: intent.context } : {}), ...(intent.images?.length ? { images: intent.images } : {}) });
570
+ if (intent.action === 'steer') return controller.dispatch({ type: 'steer', text: intent.text });
555
571
  if (intent.action === 'new') {
556
572
  if (intent.mode === 'terminal') {
557
573
  return options.onStartTerminal
@@ -608,6 +624,10 @@ export function createControllerBinding(controller, options = {}) {
608
624
  },
609
625
  ...(options.copyText ? { copyText: options.copyText } : {}),
610
626
  ...(options.resolveImage ? { resolveImage: options.resolveImage } : {}),
627
+ ...(options.confirmIntent ? { confirmIntent: options.confirmIntent } : {}),
628
+ ...(options.pickContext ? { pickContext: options.pickContext } : {}),
629
+ ...(options.onClose ? { onClose: options.onClose } : {}),
630
+ ...(options.onOpen ? { onOpen: options.onOpen } : {}),
611
631
  };
612
632
  return {
613
633
  adapter,