@volter-ai-dev/supercode-ui 0.1.17 → 0.1.20

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
@@ -82,6 +82,7 @@ function boundedSet(map, key, value) {
82
82
  // src/icon.jsx
83
83
  import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
84
84
  var ICONS = {
85
+ attach: () => /* @__PURE__ */ jsx("path", { d: "M6.25 9.75 10.6 5.4a2.1 2.1 0 0 1 2.97 2.97l-5.4 5.4a3.3 3.3 0 0 1-4.67-4.66l5.52-5.52" }),
85
86
  back: () => /* @__PURE__ */ jsx("path", { d: "m11.5 4.5-4.5 4.5 4.5 4.5" }),
86
87
  check: () => /* @__PURE__ */ jsx("path", { d: "m4 9 3.25 3.25L14 5.5" }),
87
88
  chevron: () => /* @__PURE__ */ jsx("path", { d: "m7 4.5 4.5 4.5L7 13.5" }),
@@ -122,6 +123,44 @@ function UiIcon({ name, size = 16, class: className = "" }) {
122
123
  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, {}) });
123
124
  }
124
125
 
126
+ // src/context.jsx
127
+ import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
128
+ var MAX_CONTEXT_ITEMS = 32;
129
+ function normalizeContext(value) {
130
+ return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
131
+ if (!item || typeof item.label !== "string" || typeof item.detail !== "string") return [];
132
+ const label = item.label.trim().slice(0, 200);
133
+ const detail = item.detail.slice(0, 2e4);
134
+ if (!label || !detail) return [];
135
+ return [{
136
+ ...typeof item.id === "string" && item.id ? { id: item.id.slice(0, 2e3) } : {},
137
+ ...typeof item.kind === "string" && item.kind ? { kind: item.kind.slice(0, 100) } : {},
138
+ label,
139
+ detail
140
+ }];
141
+ }).slice(0, MAX_CONTEXT_ITEMS);
142
+ }
143
+ function mergeContext(current, picked) {
144
+ const next = [...current];
145
+ const seen = new Set(current.map((item) => item.id ? `id:${item.id}` : `value:${item.kind ?? ""}\0${item.label}\0${item.detail}`));
146
+ for (const item of normalizeContext(picked)) {
147
+ const key = item.id ? `id:${item.id}` : `value:${item.kind ?? ""}\0${item.label}\0${item.detail}`;
148
+ if (seen.has(key)) continue;
149
+ seen.add(key);
150
+ next.push(item);
151
+ if (next.length === MAX_CONTEXT_ITEMS) break;
152
+ }
153
+ return next;
154
+ }
155
+ function ContextTray({ items, onRemove }) {
156
+ if (!items.length) return null;
157
+ return /* @__PURE__ */ jsx2("div", { class: "scui-compose-context", "aria-label": "Attached context", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
158
+ /* @__PURE__ */ jsx2(UiIcon, { name: "attach", size: 12 }),
159
+ /* @__PURE__ */ jsx2("strong", { children: item.label }),
160
+ /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove context ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) })
161
+ ] }, item.id ?? `${item.label}:${index}`)) });
162
+ }
163
+
125
164
  // src/textarea.js
126
165
  import { useLayoutEffect } from "preact/hooks";
127
166
  function useAutosizeTextarea(ref, value) {
@@ -137,7 +176,7 @@ function useAutosizeTextarea(ref, value) {
137
176
  }
138
177
 
139
178
  // src/composer.jsx
140
- import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
179
+ import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
141
180
  var composerMemory = /* @__PURE__ */ new Map();
142
181
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
143
182
  if (state.mode !== "mirror" || state.canSend) return null;
@@ -147,29 +186,32 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
147
186
  const resume = canContinueHere(state);
148
187
  const join = state.canAttach;
149
188
  const branch = state.canBranch;
150
- if (!resume && !join && !branch) return /* @__PURE__ */ jsx2("div", { class: "scui-continuation", children: /* @__PURE__ */ jsxs2("span", { children: [
151
- /* @__PURE__ */ jsx2("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
152
- /* @__PURE__ */ jsx2("small", { children: "This session cannot be continued by an available harness." })
189
+ if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { class: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
190
+ /* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
191
+ /* @__PURE__ */ jsx3("small", { children: "This session cannot be continued by an available harness." })
153
192
  ] }) });
154
- return /* @__PURE__ */ jsxs2("div", { class: "scui-continuation", children: [
155
- /* @__PURE__ */ jsxs2("span", { children: [
156
- /* @__PURE__ */ jsx2("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
157
- /* @__PURE__ */ jsx2("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
193
+ return /* @__PURE__ */ jsxs3("div", { class: "scui-continuation", children: [
194
+ /* @__PURE__ */ jsxs3("span", { children: [
195
+ /* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
196
+ /* @__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." })
158
197
  ] }),
159
- /* @__PURE__ */ jsx2("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter.onIntent(join ? { action: "join" } : resume ? { action: "resume" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
198
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter.onIntent(join ? { action: "join" } : resume ? { action: "resume" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
160
199
  ] });
161
200
  }
162
201
  function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
163
- const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, queue: [] };
202
+ const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], queue: [] };
164
203
  const [draft, setDraft] = useState(remembered.draft);
204
+ const [context, setContext] = useState(remembered.context);
165
205
  const [queue, setQueue] = useState(remembered.queue);
166
206
  const [dispatching, setDispatching] = useState(false);
207
+ const [picking, setPicking] = useState(false);
208
+ const [pickerError, setPickerError] = useState(null);
167
209
  const textarea = useRef(null);
168
210
  useAutosizeTextarea(textarea, draft);
169
- const remember = (nextDraft, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, queue: nextQueue });
211
+ const remember = (nextDraft, nextContext, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, queue: nextQueue });
170
212
  const updateQueue = (update) => setQueue((items) => {
171
213
  const next = update(items);
172
- remember(draft, next);
214
+ remember(draft, context, next);
173
215
  return next;
174
216
  });
175
217
  const queueBlocked = state.busy || pendingStatus !== null || dispatching;
@@ -179,9 +221,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
179
221
  const [next, ...rest] = queue;
180
222
  setDispatching(true);
181
223
  setQueue(rest);
182
- remember(draft, rest);
183
- onPending?.(next);
184
- adapter.onIntent({ action: "send", text: next });
224
+ remember(draft, context, rest);
225
+ onPending?.(next.text, next.context);
226
+ adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {} });
185
227
  }
186
228
  }, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
187
229
  useEffect(() => {
@@ -193,7 +235,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
193
235
  useEffect(() => {
194
236
  if (!restoreDraft) return;
195
237
  setDraft(restoreDraft.text);
196
- remember(restoreDraft.text, queue);
238
+ const restoredContext = normalizeContext(restoreDraft.context);
239
+ setContext(restoredContext);
240
+ remember(restoreDraft.text, restoredContext, queue);
197
241
  textarea.current?.focus({ preventScroll: true });
198
242
  onDraftRestored?.(restoreDraft.id);
199
243
  }, [onDraftRestored, restoreDraft?.id]);
@@ -201,43 +245,70 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
201
245
  const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
202
246
  return () => clearTimeout(timer);
203
247
  }, [adapter, draft]);
248
+ const pickContext = () => {
249
+ if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS) return;
250
+ setPicking(true);
251
+ setPickerError(null);
252
+ Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
253
+ setContext((current) => {
254
+ const next = mergeContext(current, picked);
255
+ remember(draft, next, queue);
256
+ return next;
257
+ });
258
+ }, (error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
259
+ };
204
260
  const send = () => {
205
261
  const text = draft.trim();
206
262
  if (!text) return;
207
- if (queuesNewMessage) updateQueue((items) => [...items, text]);
263
+ const message = { text, context };
264
+ if (queuesNewMessage) updateQueue((items) => [...items, message]);
208
265
  else if (state.canSend) {
209
266
  if (onPending) setDispatching(true);
210
- onPending?.(text);
211
- adapter.onIntent({ action: "send", text });
267
+ onPending?.(text, context);
268
+ adapter.onIntent({ action: "send", text, ...context.length ? { context } : {} });
212
269
  } else return;
213
270
  setDraft("");
214
- remember("", queuesNewMessage ? [...queue, text] : queue);
271
+ setContext([]);
272
+ remember("", [], queuesNewMessage ? [...queue, message] : queue);
215
273
  };
216
- return /* @__PURE__ */ jsxs2("div", { class: "scui-compose", children: [
217
- queue.length ? /* @__PURE__ */ jsxs2("div", { class: "scui-queue", children: [
218
- /* @__PURE__ */ jsxs2("strong", { children: [
274
+ return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
275
+ queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
276
+ /* @__PURE__ */ jsxs3("strong", { children: [
219
277
  queue.length,
220
278
  " queued"
221
279
  ] }),
222
- queue.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
223
- item,
224
- /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove queued message ${index + 1}`, onClick: () => updateQueue((items) => items.filter((_, itemIndex) => itemIndex !== index)), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 13 }) })
225
- ] }, `${index}:${item}`))
280
+ queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
281
+ /* @__PURE__ */ jsxs3("span", { children: [
282
+ item.text,
283
+ item.context.length ? /* @__PURE__ */ jsxs3("small", { children: [
284
+ item.context.length,
285
+ " attached"
286
+ ] }) : null
287
+ ] }),
288
+ /* @__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 }) })
289
+ ] }, `${index}:${item.text}`))
226
290
  ] }) : null,
227
- /* @__PURE__ */ jsxs2("div", { class: "scui-envelope", children: [
228
- /* @__PURE__ */ jsx2("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, onInput: (event) => {
291
+ /* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
292
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
293
+ remember(draft, next, queue);
294
+ return next;
295
+ }) }),
296
+ pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
297
+ /* @__PURE__ */ jsxs3("div", { class: "scui-envelope", children: [
298
+ adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { class: "scui-attach", type: "button", "aria-label": "Attach context", disabled: picking || context.length >= MAX_CONTEXT_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
299
+ /* @__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, onInput: (event) => {
229
300
  const value = event.currentTarget.value;
230
301
  setDraft(value);
231
- remember(value, queue);
302
+ remember(value, context, queue);
232
303
  }, onKeyDown: (event) => {
233
304
  if (isSendKey(event)) {
234
305
  event.preventDefault();
235
306
  send();
236
307
  }
237
308
  } }),
238
- /* @__PURE__ */ jsxs2("span", { children: [
239
- state.busy ? /* @__PURE__ */ jsx2("button", { class: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: /* @__PURE__ */ jsx2(UiIcon, { name: "stop", size: 15 }) }) : null,
240
- /* @__PURE__ */ jsx2("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx2(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
309
+ /* @__PURE__ */ jsxs3("span", { children: [
310
+ 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,
311
+ /* @__PURE__ */ jsx3("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx3(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
241
312
  ] })
242
313
  ] })
243
314
  ] });
package/controller.mjs CHANGED
@@ -141,6 +141,11 @@ function projectConversationEntry(entry, maxEntryChars) {
141
141
  };
142
142
  projected.presentation = createToolPresentation({
143
143
  ...projected,
144
+ // Presentation is a small semantic projection, so derive it before the raw native
145
+ // envelope is bounded for transport. Large Agent prompts and patches routinely exceed the
146
+ // widget cap; parsing the truncated JSON made their useful description/path disappear.
147
+ arguments: (entry.arguments ?? '').trim(),
148
+ resultText: (entry.resultText ?? '').trim(),
144
149
  resultContent: entry.resultContent,
145
150
  metadata: entry.metadata,
146
151
  });
@@ -371,10 +376,10 @@ async function dispatchStandard(controller, intent, options) {
371
376
  const active = snapshot.activeSessionKey;
372
377
  if (intent.action === 'mounted') return;
373
378
  if (intent.action === 'attach') return controller.dispatch({ type: 'observe', sessionKey: intent.key });
374
- if (intent.action === 'send') return controller.dispatch({ type: 'send', text: intent.text });
379
+ if (intent.action === 'send') return controller.dispatch({ type: 'send', text: intent.text, ...(intent.context?.length ? { context: intent.context } : {}) });
375
380
  if (intent.action === 'new') {
376
381
  await controller.dispatch({ type: 'start', harness: intent.harness });
377
- return controller.dispatch({ type: 'send', text: intent.text });
382
+ return controller.dispatch({ type: 'send', text: intent.text, ...(intent.context?.length ? { context: intent.context } : {}) });
378
383
  }
379
384
  if (intent.action === 'resume' && active) return controller.dispatch({ type: 'resume', sessionKey: active });
380
385
  if (intent.action === 'join' && active) return controller.dispatch({ type: 'attach', sessionKey: active });
package/conversation.mjs CHANGED
@@ -102,6 +102,12 @@ function sourceString(source, keys) {
102
102
  const match = new RegExp(`(?:^|[,{\\s])["']?(?:${names})["']?\\s*:\\s*("(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|\`(?:\\\\.|[^\`\\\\])*\`)`).exec(source);
103
103
  return decodedLiteral(match?.[1]);
104
104
  }
105
+ function assignedString(source, keys) {
106
+ if (!source) return "";
107
+ const names = keys.join("|");
108
+ const match = new RegExp(`\\b(?:${names})\\s*=\\s*("(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|\`(?:\\\\.|[^\`\\\\])*\`)`).exec(source);
109
+ return decodedLiteral(match?.[1]);
110
+ }
105
111
  function callArgumentSource(source, open) {
106
112
  let depth = 1;
107
113
  let quote = "";
@@ -243,6 +249,13 @@ function planItems(args) {
243
249
  return [{ label: boundedString(label, 300), status: firstString(value, ["status"]) }];
244
250
  }).slice(0, 12);
245
251
  }
252
+ function agentItems(name, resultText) {
253
+ if (!/list.?agents/i.test(name)) return [];
254
+ return (resultText ?? "").split("\n").flatMap((line) => {
255
+ const parts = line.trim().split(/\s+·\s+/).filter(Boolean);
256
+ return parts.length > 1 ? [{ label: boundedString(parts[0], 120), status: boundedString(parts.slice(1).join(" \xB7 "), 180) }] : [];
257
+ }).slice(0, 12);
258
+ }
246
259
  function editPreview(args, resultText, source) {
247
260
  const direct = firstString(args, ["patch", "diff"]);
248
261
  if (direct) return direct;
@@ -260,7 +273,19 @@ function editPreview(args, resultText, source) {
260
273
  }
261
274
  function toolResultEnvelope(resultText) {
262
275
  const match = /^Script (?:completed|failed)\r?\nWall time ([0-9.]+) seconds\r?\nOutput:\r?\n([\s\S]*)$/.exec(resultText ?? "");
263
- if (!match) return { preview: resultText ?? "", value: null, durationMs: null };
276
+ if (!match) {
277
+ try {
278
+ const value = JSON.parse(resultText ?? "");
279
+ const object = record(value);
280
+ return {
281
+ preview: typeof value === "string" ? value : firstString(object, ["output", "message", "text", "summary", "result"]) || resultText || "",
282
+ value: object,
283
+ durationMs: null
284
+ };
285
+ } catch {
286
+ return { preview: resultText ?? "", value: null, durationMs: null };
287
+ }
288
+ }
264
289
  const durationMs = Number(match[1]) * 1e3;
265
290
  try {
266
291
  const value = record(JSON.parse(match[2]));
@@ -273,6 +298,15 @@ function toolResultEnvelope(resultText) {
273
298
  return { preview: match[2], value: null, durationMs: Number.isFinite(durationMs) ? durationMs : null };
274
299
  }
275
300
  }
301
+ function semanticAgentPreview(name, status, outcome) {
302
+ const normalized = name.toLocaleLowerCase();
303
+ if (status === "error") return outcome.preview;
304
+ if (/^(?:agent|task)$|spawn.?agent/.test(normalized) && outcome.preview) return "Agent is working in the background.";
305
+ if (/send.?message|followup.?task/.test(normalized) && outcome.preview) {
306
+ return /resumed from transcript|resumedAgentId/i.test(outcome.preview) ? "Agent resumed in the background." : "Message delivered.";
307
+ }
308
+ return outcome.preview;
309
+ }
276
310
  function verificationCommand(command) {
277
311
  let shell = "";
278
312
  let quote = "";
@@ -305,12 +339,13 @@ function verificationCommand(command) {
305
339
  function classifyTool(name, command) {
306
340
  const normalized = name.toLocaleLowerCase();
307
341
  if (/write_stdin|^wait$/.test(normalized)) return "command";
308
- if (/update.?plan|todo|checklist|taskcreate|taskupdate/.test(normalized)) return "plan";
342
+ if (/update.?plan|todo|checklist|taskcreate|taskupdate|create.?goal|update.?goal/.test(normalized)) return "plan";
309
343
  if (/search.?replace|edit|write|patch|replace|create_file|apply_patch/.test(normalized)) return "edit";
310
344
  if (/read|view|open_file|list_dir/.test(normalized)) return "read";
345
+ if (/web.?search|web.?fetch|fetch.?url/.test(normalized)) return "web";
311
346
  if (/search|find|grep|glob|toolsearch/.test(normalized)) return "search";
312
347
  if (/browser|web|fetch|url/.test(normalized)) return "web";
313
- if (/agent|subagent|sendmessage|delegate|^task$/.test(normalized)) return "agent";
348
+ if (/agent|subagent|send.?message|delegate|followup.?task|taskstop|taskoutput|^task$/.test(normalized)) return "agent";
314
349
  if (/test|typecheck|lint|build/.test(normalized)) return "test";
315
350
  if (/terminal|bash|shell|command|exec|write_stdin|^wait$/.test(normalized)) return verificationCommand(command) ? "test" : "command";
316
351
  return "other";
@@ -319,9 +354,17 @@ function toolAction(status, category, name, tools) {
319
354
  const position = status === "pending" ? 0 : status === "error" ? 2 : 1;
320
355
  if (tools.length > 1) return [`Running ${tools.length} actions`, `Ran ${tools.length} actions`, `${tools.length} actions failed`][position];
321
356
  const normalized = name.toLocaleLowerCase();
322
- if (/sendmessage/.test(normalized)) return ["Messaging agent", "Messaged agent", "Agent message failed"][position];
323
- if (/kill_command_or_subagent/.test(normalized)) return ["Stopping", "Stopped", "Stop failed"][position];
357
+ if (/send.?message|followup.?task/.test(normalized)) return ["Messaging agent", "Messaged agent", "Agent message failed"][position];
358
+ if (/taskstop|interrupt.?agent|kill_command_or_subagent/.test(normalized)) return ["Stopping agent", "Stopped agent", "Stop failed"][position];
359
+ if (/list.?agents|taskoutput|wait.?agent/.test(normalized)) return ["Checking agents", "Checked agents", "Agent check failed"][position];
324
360
  if (/get_command_or_subagent_output|write_stdin|^wait$/.test(normalized)) return ["Waiting for", "Checked", "Check failed"][position];
361
+ if (/web.?search/.test(normalized)) return ["Searching web", "Searched web", "Web search failed"][position];
362
+ if (/web.?fetch|fetch.?url/.test(normalized)) return ["Fetching page", "Fetched page", "Page fetch failed"][position];
363
+ if (/^skill$|use.?skill|load.?skill/.test(normalized)) return ["Loading skill", "Loaded skill", "Skill load failed"][position];
364
+ if (/taskcreate/.test(normalized)) return ["Adding task", "Added task", "Task creation failed"][position];
365
+ if (/taskupdate/.test(normalized)) return ["Updating task", "Updated task", "Task update failed"][position];
366
+ if (/create.?goal/.test(normalized)) return ["Creating goal", "Created goal", "Goal creation failed"][position];
367
+ if (/update.?goal/.test(normalized)) return ["Updating goal", "Updated goal", "Goal update failed"][position];
325
368
  const actions = {
326
369
  read: ["Reading", "Read", "Read failed"],
327
370
  search: ["Searching", "Searched", "Search failed"],
@@ -338,18 +381,21 @@ function createToolPresentation(entry) {
338
381
  const envelope = toolEnvelope(entry);
339
382
  const args = envelope.args;
340
383
  const outcome = toolResultEnvelope(entry.resultText);
384
+ const patchSource = assignedString(envelope.source, ["patch"]) || envelope.source;
341
385
  const command = firstString(args, ["command", "cmd"]) || sourceString(envelope.callSource, ["command", "cmd"]);
342
386
  const category = classifyTool(envelope.name, command || envelope.source || entry.arguments || "");
343
- const path = firstString(args, ["file_path", "target_file", "target_directory", "path"]) || sourceString(envelope.callSource, ["file_path", "target_file", "target_directory", "path"]) || patchPath(envelope.source);
387
+ const path = firstString(args, ["file_path", "target_file", "target_directory", "path"]) || sourceString(envelope.callSource, ["file_path", "target_file", "target_directory", "path"]) || patchPath(patchSource);
344
388
  const query = firstString(args, ["query", "pattern"]) || sourceString(envelope.callSource, ["query", "pattern", "q"]);
345
389
  const url = firstString(args, ["url"]) || sourceString(envelope.callSource, ["url", "ref_id"]);
346
- const subject = firstString(args, ["subject", "description", "summary", "task", "prompt"]) || sourceString(envelope.callSource, ["subject", "description", "summary", "task", "prompt"]);
390
+ const subject = firstString(args, ["subject", "description", "summary", "task", "objective", "prompt"]) || sourceString(envelope.callSource, ["subject", "description", "summary", "task", "objective", "prompt"]);
391
+ const agentTarget = category === "agent" ? firstString(record(outcome.value), ["command", "name"]) || firstString(args, ["target", "task_name", "taskId", "task_id", "agentId", "agent_id", "resume", "team_name"]) || sourceString(envelope.callSource, ["target", "task_name", "taskId", "task_id", "agentId", "agent_id", "resume", "team_name"]) : "";
392
+ const skillTarget = /^skill$|use.?skill|load.?skill/i.test(envelope.name) ? firstString(args, ["skill", "name"]) || sourceString(envelope.callSource, ["skill", "name"]) : "";
347
393
  const background = /get_command_or_subagent_output|kill_command_or_subagent/i.test(envelope.name) ? "background task" : /write_stdin|^wait$/i.test(envelope.name) ? "background command" : "";
348
- const items = planItems(args);
394
+ const items = category === "agent" ? agentItems(envelope.name, outcome.preview) : planItems(args);
349
395
  const taskId = firstString(args, ["taskId", "task_id"]);
350
396
  const planTarget = category === "plan" ? items.length ? `${items.length} ${items.length === 1 ? "item" : "items"}` : taskId ? `task ${taskId}` : "" : "";
351
- const target = path || command || query || url || subject || background || planTarget || (envelope.tools.length > 1 ? `${envelope.tools.length} coordinated actions` : toolTarget(entry.arguments));
352
- const previewSource = category === "edit" ? editPreview(args, outcome.preview, envelope.source) : outcome.preview;
397
+ const target = path || command || query || url || subject || agentTarget || skillTarget || background || planTarget || (envelope.tools.length > 1 ? `${envelope.tools.length} coordinated actions` : toolTarget(entry.arguments));
398
+ const previewSource = category === "edit" ? editPreview(args, outcome.preview, patchSource) : category === "agent" ? semanticAgentPreview(envelope.name, entry.status ?? "completed", outcome) : outcome.preview;
353
399
  const result = record(entry.resultContent);
354
400
  const metadata = record(entry.metadata);
355
401
  const resultMetadata = record(result?.metadata);
@@ -529,6 +575,7 @@ function boundedSet(map, key, value) {
529
575
  // src/icon.jsx
530
576
  import { Fragment, jsx as jsx2, jsxs } from "preact/jsx-runtime";
531
577
  var ICONS = {
578
+ attach: () => /* @__PURE__ */ jsx2("path", { d: "M6.25 9.75 10.6 5.4a2.1 2.1 0 0 1 2.97 2.97l-5.4 5.4a3.3 3.3 0 0 1-4.67-4.66l5.52-5.52" }),
532
579
  back: () => /* @__PURE__ */ jsx2("path", { d: "m11.5 4.5-4.5 4.5 4.5 4.5" }),
533
580
  check: () => /* @__PURE__ */ jsx2("path", { d: "m4 9 3.25 3.25L14 5.5" }),
534
581
  chevron: () => /* @__PURE__ */ jsx2("path", { d: "m7 4.5 4.5 4.5L7 13.5" }),
@@ -799,7 +846,10 @@ function ToolPreview({ presentation, entry }) {
799
846
  presentation.url ? /* @__PURE__ */ jsx3("code", { children: presentation.url }) : null,
800
847
  presentation.preview ? /* @__PURE__ */ jsx3("p", { children: stripAnsi(presentation.preview) }) : /* @__PURE__ */ jsx3("small", { children: pending ? "Waiting for the page" : "No page summary returned" })
801
848
  ] });
802
- if (presentation.detail === "agent") return /* @__PURE__ */ jsx3("section", { class: "scui-agent-preview", children: presentation.preview ? /* @__PURE__ */ jsx3("pre", { children: stripAnsi(presentation.preview) }) : /* @__PURE__ */ jsx3("small", { children: pending ? "Agent is working" : "No textual handoff returned" }) });
849
+ if (presentation.detail === "agent") return /* @__PURE__ */ jsx3("section", { class: "scui-agent-preview", children: presentation.items?.length ? /* @__PURE__ */ jsx3("ol", { class: "scui-agent-roster", children: presentation.items.map((item, index) => /* @__PURE__ */ jsxs2("li", { children: [
850
+ /* @__PURE__ */ jsx3("strong", { children: item.label }),
851
+ /* @__PURE__ */ jsx3("small", { children: item.status })
852
+ ] }, `${item.label}:${index}`)) }) : presentation.preview ? /* @__PURE__ */ jsx3("pre", { children: stripAnsi(presentation.preview) }) : /* @__PURE__ */ jsx3("small", { children: pending ? "Agent is working" : "No textual handoff returned" }) });
803
853
  if (presentation.detail === "plan") return /* @__PURE__ */ jsx3("ol", { class: "scui-plan-preview", children: presentation.items?.map((item, index) => /* @__PURE__ */ jsxs2("li", { "data-status": item.status, children: [
804
854
  /* @__PURE__ */ jsx3("i", { "aria-hidden": "true" }),
805
855
  /* @__PURE__ */ jsx3("span", { children: item.label })
@@ -1017,18 +1067,23 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1017
1067
  const element = scroller.current;
1018
1068
  if (!element) return;
1019
1069
  const anchor = earlierAnchor.current;
1020
- if (anchor && state.transcript.length > anchor.entries) {
1021
- element.scrollTop = anchor.top + (element.scrollHeight - anchor.height);
1022
- earlierAnchor.current = null;
1023
- remember({ top: element.scrollTop, atBottom: false });
1024
- return;
1070
+ if (anchor) {
1071
+ if (state.operation === "loadEarlier") anchor.seenOperation = true;
1072
+ const prepended = state.transcript.length > anchor.entries && state.transcript[0]?.id !== anchor.firstId;
1073
+ if (prepended) {
1074
+ element.scrollTop = anchor.top + (element.scrollHeight - anchor.height);
1075
+ earlierAnchor.current = null;
1076
+ remember({ top: element.scrollTop, atBottom: false });
1077
+ return;
1078
+ }
1079
+ if (state.error || anchor.seenOperation && state.operation !== "loadEarlier") earlierAnchor.current = null;
1025
1080
  }
1026
1081
  if (!restored.current) {
1027
1082
  restored.current = true;
1028
1083
  if (remembered.top !== null && !remembered.atBottom) element.scrollTop = remembered.top;
1029
1084
  else pin();
1030
1085
  } else if (atBottom) pin();
1031
- }, [memoryKey, state.transcript, state.busy, state.operation, pendingMessage?.text, pendingMessage?.status]);
1086
+ }, [memoryKey, state.transcript, state.busy, state.operation, state.error, pendingMessage?.text, pendingMessage?.status]);
1032
1087
  return /* @__PURE__ */ jsxs2("div", { class: "scui-conversation-wrap", children: [
1033
1088
  /* @__PURE__ */ jsx3(ConversationAnnouncements, { state }),
1034
1089
  /* @__PURE__ */ jsx3("div", { class: "scui-conversation", ref: scroller, tabIndex: 0, "aria-label": "Conversation", onScroll: (event) => {
@@ -1042,7 +1097,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1042
1097
  /* @__PURE__ */ jsx3(SessionDetails, { semantics: state.semantics }),
1043
1098
  state.history.hasEarlier ? /* @__PURE__ */ jsx3("button", { class: "scui-load", type: "button", disabled: Boolean(state.operation), onClick: () => {
1044
1099
  const element = scroller.current;
1045
- if (element) earlierAnchor.current = { height: element.scrollHeight, top: element.scrollTop, entries: state.transcript.length };
1100
+ if (element) earlierAnchor.current = { height: element.scrollHeight, top: element.scrollTop, entries: state.transcript.length, firstId: state.transcript[0]?.id, seenOperation: false };
1046
1101
  setAtBottom(false);
1047
1102
  adapter.onIntent({ action: "loadEarlier" });
1048
1103
  }, children: "Load earlier messages" }) : null,
@@ -1054,6 +1109,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1054
1109
  ] }, block.id)),
1055
1110
  pendingMessage ? /* @__PURE__ */ jsxs2("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, "aria-label": "Your pending message", children: [
1056
1111
  /* @__PURE__ */ jsx3(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
1112
+ /* @__PURE__ */ jsx3(ContextDisclosure, { context: pendingMessage.context }),
1057
1113
  /* @__PURE__ */ jsxs2("footer", { children: [
1058
1114
  /* @__PURE__ */ jsx3("small", { children: pendingMessage.status === "failed" ? "Not sent" : "Sending\u2026" }),
1059
1115
  pendingMessage.status === "failed" ? /* @__PURE__ */ jsxs2("span", { children: [
package/core.mjs CHANGED
@@ -131,6 +131,13 @@ function sourceString(source, keys) {
131
131
  return decodedLiteral(match?.[1]);
132
132
  }
133
133
 
134
+ function assignedString(source, keys) {
135
+ if (!source) return '';
136
+ const names = keys.join('|');
137
+ const match = new RegExp(`\\b(?:${names})\\s*=\\s*(\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|\`(?:\\\\.|[^\`\\\\])*\`)`).exec(source);
138
+ return decodedLiteral(match?.[1]);
139
+ }
140
+
134
141
  function callArgumentSource(source, open) {
135
142
  let depth = 1;
136
143
  let quote = '';
@@ -243,6 +250,14 @@ function planItems(args) {
243
250
  }).slice(0, 12);
244
251
  }
245
252
 
253
+ function agentItems(name, resultText) {
254
+ if (!/list.?agents/i.test(name)) return [];
255
+ return (resultText ?? '').split('\n').flatMap((line) => {
256
+ const parts = line.trim().split(/\s+·\s+/).filter(Boolean);
257
+ return parts.length > 1 ? [{ label: boundedString(parts[0], 120), status: boundedString(parts.slice(1).join(' · '), 180) }] : [];
258
+ }).slice(0, 12);
259
+ }
260
+
246
261
  function editPreview(args, resultText, source) {
247
262
  const direct = firstString(args, ['patch', 'diff']);
248
263
  if (direct) return direct;
@@ -261,7 +276,19 @@ function editPreview(args, resultText, source) {
261
276
 
262
277
  function toolResultEnvelope(resultText) {
263
278
  const match = /^Script (?:completed|failed)\r?\nWall time ([0-9.]+) seconds\r?\nOutput:\r?\n([\s\S]*)$/.exec(resultText ?? '');
264
- if (!match) return { preview: resultText ?? '', value: null, durationMs: null };
279
+ if (!match) {
280
+ try {
281
+ const value = JSON.parse(resultText ?? '');
282
+ const object = record(value);
283
+ return {
284
+ preview: typeof value === 'string' ? value : firstString(object, ['output', 'message', 'text', 'summary', 'result']) || resultText || '',
285
+ value: object,
286
+ durationMs: null,
287
+ };
288
+ } catch {
289
+ return { preview: resultText ?? '', value: null, durationMs: null };
290
+ }
291
+ }
265
292
  const durationMs = Number(match[1]) * 1_000;
266
293
  try {
267
294
  const value = record(JSON.parse(match[2]));
@@ -275,6 +302,16 @@ function toolResultEnvelope(resultText) {
275
302
  }
276
303
  }
277
304
 
305
+ function semanticAgentPreview(name, status, outcome) {
306
+ const normalized = name.toLocaleLowerCase();
307
+ if (status === 'error') return outcome.preview;
308
+ if (/^(?:agent|task)$|spawn.?agent/.test(normalized) && outcome.preview) return 'Agent is working in the background.';
309
+ if (/send.?message|followup.?task/.test(normalized) && outcome.preview) {
310
+ return /resumed from transcript|resumedAgentId/i.test(outcome.preview) ? 'Agent resumed in the background.' : 'Message delivered.';
311
+ }
312
+ return outcome.preview;
313
+ }
314
+
278
315
  function verificationCommand(command) {
279
316
  let shell = '';
280
317
  let quote = '';
@@ -292,12 +329,13 @@ function verificationCommand(command) {
292
329
  function classifyTool(name, command) {
293
330
  const normalized = name.toLocaleLowerCase();
294
331
  if (/write_stdin|^wait$/.test(normalized)) return 'command';
295
- if (/update.?plan|todo|checklist|taskcreate|taskupdate/.test(normalized)) return 'plan';
332
+ if (/update.?plan|todo|checklist|taskcreate|taskupdate|create.?goal|update.?goal/.test(normalized)) return 'plan';
296
333
  if (/search.?replace|edit|write|patch|replace|create_file|apply_patch/.test(normalized)) return 'edit';
297
334
  if (/read|view|open_file|list_dir/.test(normalized)) return 'read';
335
+ if (/web.?search|web.?fetch|fetch.?url/.test(normalized)) return 'web';
298
336
  if (/search|find|grep|glob|toolsearch/.test(normalized)) return 'search';
299
337
  if (/browser|web|fetch|url/.test(normalized)) return 'web';
300
- if (/agent|subagent|sendmessage|delegate|^task$/.test(normalized)) return 'agent';
338
+ if (/agent|subagent|send.?message|delegate|followup.?task|taskstop|taskoutput|^task$/.test(normalized)) return 'agent';
301
339
  if (/test|typecheck|lint|build/.test(normalized)) return 'test';
302
340
  if (/terminal|bash|shell|command|exec|write_stdin|^wait$/.test(normalized)) return verificationCommand(command) ? 'test' : 'command';
303
341
  return 'other';
@@ -307,9 +345,17 @@ function toolAction(status, category, name, tools) {
307
345
  const position = status === 'pending' ? 0 : status === 'error' ? 2 : 1;
308
346
  if (tools.length > 1) return [`Running ${tools.length} actions`, `Ran ${tools.length} actions`, `${tools.length} actions failed`][position];
309
347
  const normalized = name.toLocaleLowerCase();
310
- if (/sendmessage/.test(normalized)) return ['Messaging agent', 'Messaged agent', 'Agent message failed'][position];
311
- if (/kill_command_or_subagent/.test(normalized)) return ['Stopping', 'Stopped', 'Stop failed'][position];
348
+ if (/send.?message|followup.?task/.test(normalized)) return ['Messaging agent', 'Messaged agent', 'Agent message failed'][position];
349
+ if (/taskstop|interrupt.?agent|kill_command_or_subagent/.test(normalized)) return ['Stopping agent', 'Stopped agent', 'Stop failed'][position];
350
+ if (/list.?agents|taskoutput|wait.?agent/.test(normalized)) return ['Checking agents', 'Checked agents', 'Agent check failed'][position];
312
351
  if (/get_command_or_subagent_output|write_stdin|^wait$/.test(normalized)) return ['Waiting for', 'Checked', 'Check failed'][position];
352
+ if (/web.?search/.test(normalized)) return ['Searching web', 'Searched web', 'Web search failed'][position];
353
+ if (/web.?fetch|fetch.?url/.test(normalized)) return ['Fetching page', 'Fetched page', 'Page fetch failed'][position];
354
+ if (/^skill$|use.?skill|load.?skill/.test(normalized)) return ['Loading skill', 'Loaded skill', 'Skill load failed'][position];
355
+ if (/taskcreate/.test(normalized)) return ['Adding task', 'Added task', 'Task creation failed'][position];
356
+ if (/taskupdate/.test(normalized)) return ['Updating task', 'Updated task', 'Task update failed'][position];
357
+ if (/create.?goal/.test(normalized)) return ['Creating goal', 'Created goal', 'Goal creation failed'][position];
358
+ if (/update.?goal/.test(normalized)) return ['Updating goal', 'Updated goal', 'Goal update failed'][position];
313
359
  const actions = {
314
360
  read: ['Reading', 'Read', 'Read failed'],
315
361
  search: ['Searching', 'Searched', 'Search failed'],
@@ -327,18 +373,25 @@ export function createToolPresentation(entry) {
327
373
  const envelope = toolEnvelope(entry);
328
374
  const args = envelope.args;
329
375
  const outcome = toolResultEnvelope(entry.resultText);
376
+ const patchSource = assignedString(envelope.source, ['patch']) || envelope.source;
330
377
  const command = firstString(args, ['command', 'cmd']) || sourceString(envelope.callSource, ['command', 'cmd']);
331
378
  const category = classifyTool(envelope.name, command || envelope.source || entry.arguments || '');
332
- const path = firstString(args, ['file_path', 'target_file', 'target_directory', 'path']) || sourceString(envelope.callSource, ['file_path', 'target_file', 'target_directory', 'path']) || patchPath(envelope.source);
379
+ const path = firstString(args, ['file_path', 'target_file', 'target_directory', 'path']) || sourceString(envelope.callSource, ['file_path', 'target_file', 'target_directory', 'path']) || patchPath(patchSource);
333
380
  const query = firstString(args, ['query', 'pattern']) || sourceString(envelope.callSource, ['query', 'pattern', 'q']);
334
381
  const url = firstString(args, ['url']) || sourceString(envelope.callSource, ['url', 'ref_id']);
335
- const subject = firstString(args, ['subject', 'description', 'summary', 'task', 'prompt']) || sourceString(envelope.callSource, ['subject', 'description', 'summary', 'task', 'prompt']);
382
+ const subject = firstString(args, ['subject', 'description', 'summary', 'task', 'objective', 'prompt']) || sourceString(envelope.callSource, ['subject', 'description', 'summary', 'task', 'objective', 'prompt']);
383
+ const agentTarget = category === 'agent'
384
+ ? firstString(record(outcome.value), ['command', 'name']) || firstString(args, ['target', 'task_name', 'taskId', 'task_id', 'agentId', 'agent_id', 'resume', 'team_name']) || sourceString(envelope.callSource, ['target', 'task_name', 'taskId', 'task_id', 'agentId', 'agent_id', 'resume', 'team_name'])
385
+ : '';
386
+ const skillTarget = /^skill$|use.?skill|load.?skill/i.test(envelope.name)
387
+ ? firstString(args, ['skill', 'name']) || sourceString(envelope.callSource, ['skill', 'name'])
388
+ : '';
336
389
  const background = /get_command_or_subagent_output|kill_command_or_subagent/i.test(envelope.name) ? 'background task' : /write_stdin|^wait$/i.test(envelope.name) ? 'background command' : '';
337
- const items = planItems(args);
390
+ const items = category === 'agent' ? agentItems(envelope.name, outcome.preview) : planItems(args);
338
391
  const taskId = firstString(args, ['taskId', 'task_id']);
339
392
  const planTarget = category === 'plan' ? items.length ? `${items.length} ${items.length === 1 ? 'item' : 'items'}` : taskId ? `task ${taskId}` : '' : '';
340
- const target = path || command || query || url || subject || background || planTarget || (envelope.tools.length > 1 ? `${envelope.tools.length} coordinated actions` : toolTarget(entry.arguments));
341
- const previewSource = category === 'edit' ? editPreview(args, outcome.preview, envelope.source) : outcome.preview;
393
+ const target = path || command || query || url || subject || agentTarget || skillTarget || background || planTarget || (envelope.tools.length > 1 ? `${envelope.tools.length} coordinated actions` : toolTarget(entry.arguments));
394
+ const previewSource = category === 'edit' ? editPreview(args, outcome.preview, patchSource) : category === 'agent' ? semanticAgentPreview(envelope.name, entry.status ?? 'completed', outcome) : outcome.preview;
342
395
  const result = record(entry.resultContent);
343
396
  const metadata = record(entry.metadata);
344
397
  const resultMetadata = record(result?.metadata);
@@ -668,7 +721,7 @@ export function sessionActivity(state, row) {
668
721
  export function filterSessions(rows, query) {
669
722
  const needle = query.trim().toLocaleLowerCase();
670
723
  if (!needle) return [...rows];
671
- return rows.filter((row) => [row.name, row.title, row.cwd, row.harness, harnessDisplayName(row.harness)].some((value) => value.toLocaleLowerCase().includes(needle)));
724
+ return rows.filter((row) => [row.name, row.title, row.preview, row.cwd, row.harness, harnessDisplayName(row.harness)].some((value) => typeof value === 'string' && value.toLocaleLowerCase().includes(needle)));
672
725
  }
673
726
 
674
727
  export function groupConversation(entries) {