@columenlabs/lumen-ui 0.1.286 → 0.1.288

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.
@@ -52,6 +52,11 @@ export interface AssistantAdapter {
52
52
  steer?(chatId: string, message: string): Promise<{
53
53
  steered: boolean;
54
54
  }>;
55
+ /**
56
+ * Stops the answer running in `chatId` on the server: what it said and made so far is kept, and
57
+ * the answer's stream ends with `stopped`. Without it Stop only stops reading the stream.
58
+ */
59
+ stop?(chatId: string): Promise<void>;
55
60
  settleDraft(chatId: string, draftId: string, action: 'save' | 'dismiss'): Promise<DraftItem>;
56
61
  /** Takes back a change the assistant made at once. */
57
62
  undoWrite(chatId: string, stepId: string): Promise<StepItem>;
@@ -76,6 +76,9 @@ export function assistantClient(base) {
76
76
  ...(amount !== undefined ? { amount } : {}),
77
77
  }),
78
78
  steer: (chatId, message) => send('POST', `/chats/${encodeURIComponent(chatId)}/steer`, { message }),
79
+ async stop(chatId) {
80
+ await send('POST', `/chats/${encodeURIComponent(chatId)}/stop`);
81
+ },
79
82
  async send(msg, onEvent, signal) {
80
83
  const url = `${root}/chat`;
81
84
  const res = await fetch(url, {
@@ -72,6 +72,11 @@ function itemMarkdown(i, depth, name) {
72
72
  ]
73
73
  .filter(Boolean)
74
74
  .join('\n\n');
75
+ case 'task':
76
+ return [
77
+ `${h} ${i.label} (${i.state})`,
78
+ ...i.items.map((x) => itemMarkdown(x, depth + 1, name)),
79
+ ].join('\n\n');
75
80
  case 'advice':
76
81
  return `_${i.error ? 'Could not ask' : 'Asked'} ${i.label} for advice._`;
77
82
  case 'fresh':
@@ -1,4 +1,4 @@
1
- import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantQuote, type AssistantRefNote, type AssistantStatus } from '@columenlabs/lumen-ui-types/assistant';
1
+ import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantQuote, type AssistantRefNote, type AssistantStatus, type AssistantTask, type AssistantTaskState } from '@columenlabs/lumen-ui-types/assistant';
2
2
  import { type LucideIcon } from 'lucide-react';
3
3
  import { type ReactNode } from 'react';
4
4
  import { type LumenOption } from '../lumen-selector.js';
@@ -63,6 +63,16 @@ export interface LumenAssistantProps {
63
63
  * yellow at 75% of it and red at it. The person's Auto-compact (status `compact_at`) wins; 150k by default.
64
64
  */
65
65
  compactAt?: number;
66
+ /**
67
+ * A task in the chat (`AssistantTask`, the app's work in its own session): the buttons in its
68
+ * block's header, such as Keep and Drop, from the app.
69
+ */
70
+ taskActions?: (task: AssistantTask) => ReactNode;
71
+ /**
72
+ * The app's word on where a task stands, ahead of the chat's: it took or dropped the task's result
73
+ * in its own screens. A task no longer open drops its chip over the box.
74
+ */
75
+ taskState?: (task: AssistantTask) => AssistantTaskState | undefined;
66
76
  }
67
77
  /** A composer mode: its toggle's icon and label (also its tooltip and aria-label). */
68
78
  export interface AssistantMode {
@@ -84,7 +94,7 @@ export interface AssistantCommand {
84
94
  run: (args: string) => string | undefined | Promise<string | undefined>;
85
95
  }
86
96
  /** The assistant's chat: header, messages and composer. */
87
- export declare function LumenAssistant({ client, context, onSettings, keepDays, chatScope, onClose, chat: outside, commands, sources, images, modes, imageTool, compactAt, }: LumenAssistantProps): import("react").JSX.Element;
97
+ export declare function LumenAssistant({ client, context, onSettings, keepDays, chatScope, onClose, chat: outside, commands, sources, images, modes, imageTool, compactAt, taskActions, taskState, }: LumenAssistantProps): import("react").JSX.Element;
88
98
  /** An allowance in its unit: `$4.20`, or `350 credits`. */
89
99
  export declare const allowanceAmount: (u: AllowanceUnit, n: number) => string;
90
100
  /** What the composer sends with the text: the tier picked with `#` and the things pointed at with `@`. */
@@ -49,7 +49,7 @@ export function useAssistantStatus(client) {
49
49
  export { usd };
50
50
  const seconds = (ms) => (ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`);
51
51
  /** The assistant's chat: header, messages and composer. */
52
- export function LumenAssistant({ client, context, onSettings, keepDays = 30, chatScope, onClose, chat: outside, commands = [], sources = [], images = false, modes = [], imageTool, compactAt = COMPACT_AT, }) {
52
+ export function LumenAssistant({ client, context, onSettings, keepDays = 30, chatScope, onClose, chat: outside, commands = [], sources = [], images = false, modes = [], imageTool, compactAt = COMPACT_AT, taskActions, taskState, }) {
53
53
  const own = useAssistantChat(client);
54
54
  const chat = outside ?? own;
55
55
  const qc = useQueryClient();
@@ -80,9 +80,16 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
80
80
  // each new screen call runs here once; how it ended goes with the next message.
81
81
  const screen = useAssistantScreen();
82
82
  const screenOn = !!screen && (s?.screen ?? 'move') === 'move';
83
+ // The open task's chip over the box: the next message goes to the task (the app decides) until
84
+ // the person steps out with its ×, or the task ends.
85
+ const [stepped, setStepped] = useState(null);
86
+ const openTask = chat.task && (taskState?.(chat.task) ?? chat.task.state) === 'open' && stepped !== chat.task.id
87
+ ? chat.task
88
+ : null;
83
89
  const extra = () => ({
84
90
  ...(context ? { context } : {}),
85
91
  ...(screenOn && screen ? { screen: screenMapFor(screen) } : {}),
92
+ ...(openTask ? { task: openTask.id } : {}),
86
93
  });
87
94
  const ran = useRef(new Set());
88
95
  const ended = chat.screenEnded;
@@ -191,7 +198,7 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
191
198
  title: c.title,
192
199
  updated_at: new Date(c.at).toISOString(),
193
200
  ...(chatScope && allScopes && c.scope ? { where: chatScope.name?.(c.scope) } : {}),
194
- })) ?? (chats.isFetched ? [] : null), value: chat.chatId ?? undefined, toggle: chatScope && { label: chatScope.label, on: allScopes, onChange: setAllScopes }, note: `Chats are kept ${keepDays} days after their last message.`, onRefresh: () => void chats.refetch(), onOpen: (c) => void chat.open(c.id) }), _jsx(IconButton, { icon: CopyIcon, label: "Copy chat", disabled: !chat.items.length, onClick: () => copyMarkdown(chatMarkdown(chat.title, chat.items, name), 'Chat') }), _jsx(IconButton, { icon: PlusIcon, label: "New chat", disabled: !chat.items.length, onClick: chat.reset }), onClose && (_jsx(IconButton, { icon: XIcon, label: `Close ${s?.name ?? 'assistant'}`, onClick: onClose }))] }), _jsx(AssistantName.Provider, { value: name, children: _jsx(Screen.Provider, { value: controls, children: _jsx(ImageTool.Provider, { value: imageTool ?? null, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings, onRetry: (at, user) => chat.retry(at, user, extra()), onFresh: (choice) => chat.fresh(choice, extra()) }) }) }) }), _jsx(LumenComposer, { name: s?.name ?? 'the assistant', busy: chat.busy, queued: chat.queued, queue: chat.queue, canSteer: chat.canSteer, onSteerQueued: chat.steerQueued, onUnqueue: chat.unqueue, disabled: !s || !!s.wait, commands: allCommands, tiers: tiers, sources: allSources, page: context?.label, images: images, modes: modes, onSend: (text, more, how) => chat.send(text, { ...extra(), ...more }, how), onStop: chat.stop, meter: _jsx(ContextDonut, { usage: chat.usage, compactAt: s?.compact_at ?? compactAt, items: chat.items }) })] }));
201
+ })) ?? (chats.isFetched ? [] : null), value: chat.chatId ?? undefined, toggle: chatScope && { label: chatScope.label, on: allScopes, onChange: setAllScopes }, note: `Chats are kept ${keepDays} days after their last message.`, onRefresh: () => void chats.refetch(), onOpen: (c) => void chat.open(c.id) }), _jsx(IconButton, { icon: CopyIcon, label: "Copy chat", disabled: !chat.items.length, onClick: () => copyMarkdown(chatMarkdown(chat.title, chat.items, name), 'Chat') }), _jsx(IconButton, { icon: PlusIcon, label: "New chat", disabled: !chat.items.length, onClick: chat.reset }), onClose && (_jsx(IconButton, { icon: XIcon, label: `Close ${s?.name ?? 'assistant'}`, onClick: onClose }))] }), _jsx(AssistantName.Provider, { value: name, children: _jsx(Screen.Provider, { value: controls, children: _jsx(ImageTool.Provider, { value: imageTool ?? null, children: _jsx(TaskSlots.Provider, { value: { actions: taskActions, state: taskState }, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings, onRetry: (at, user) => chat.retry(at, user, extra()), onFresh: (choice) => chat.fresh(choice, extra()) }) }) }) }) }), openTask && (_jsx("div", { className: "flex px-3 pt-1", children: _jsx(TaskChip, { task: openTask, onLeave: () => setStepped(openTask.id) }) })), _jsx(LumenComposer, { name: s?.name ?? 'the assistant', busy: chat.busy, queued: chat.queued, queue: chat.queue, canSteer: chat.canSteer, onSteerQueued: chat.steerQueued, onUnqueue: chat.unqueue, disabled: !s || !!s.wait, commands: allCommands, tiers: tiers, sources: allSources, page: context?.label, images: images, modes: modes, onSend: (text, more, how) => chat.send(text, { ...extra(), ...more }, how), onStop: chat.stop, meter: _jsx(ContextDonut, { usage: chat.usage, compactAt: s?.compact_at ?? compactAt, items: chat.items }) })] }));
195
202
  }
196
203
  function Messages({ chat, status, onSettings, onRetry, onFresh, }) {
197
204
  const end = useRef(null);
@@ -214,7 +221,7 @@ function Messages({ chat, status, onSettings, onRetry, onFresh, }) {
214
221
  ? 'Request more'
215
222
  : 'Open Settings' }))] })] })), chat.loading ? (_jsxs("p", { className: "flex items-center gap-2 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Loading the chat\u2026"] })) : chat.items.length === 0 && !status?.wait ? (_jsxs("p", { className: "pt-8 text-center text-muted-foreground text-sm", children: ["Ask ", status?.name ?? 'the assistant', " anything."] })) : (_jsx(TierNames.Provider, { value: (id) => status?.tiers.find((t) => t.id === id)?.label ?? id, children: _jsx(UndoWrite.Provider, { value: chat.undo, children: _jsx(RetryTurn.Provider, { value: chat.busy || status?.wait ? null : onRetry, children: _jsx(FreshChoice.Provider, { value: chat.busy ? null : onFresh, children: _jsx(Items, { items: chat.items, onSettle: chat.settle, busy: chat.busy, turns: true }) }) }) }) })), chat.busy && !chat.items.some(isLive) && (_jsxs("p", { className: "flex items-center gap-2 py-1 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Thinking\u2026"] })), _jsx("div", { ref: end })] }));
216
223
  }
217
- const isLive = (i) => !!i.live || (i.kind === 'handoff' && i.items.some(isLive));
224
+ const isLive = (i) => !!i.live || ((i.kind === 'handoff' || i.kind === 'task') && i.items.some(isLive));
218
225
  const WAIT = {
219
226
  not_connected: (admin, provider) => admin
220
227
  ? `Add an ${provider} key in Settings ▸ Model providers, or pick another model in Models.`
@@ -310,6 +317,8 @@ function Item({ item, onSettle, busy, }) {
310
317
  return _jsx(ScreenChip, { item: item });
311
318
  case 'handoff':
312
319
  return _jsx(HandOff, { item: item, onSettle: onSettle, busy: busy });
320
+ case 'task':
321
+ return _jsx(TaskBlock, { item: item, onSettle: onSettle, busy: busy });
313
322
  case 'advice':
314
323
  return (_jsxs("p", { className: "flex items-center gap-1.5 text-muted-foreground text-xs", children: [item.live ? _jsx(Spinner, { className: "size-3" }) : _jsx(LightbulbIcon, { className: "size-3.5" }), item.live
315
324
  ? `Asking ${item.label}…`
@@ -528,6 +537,29 @@ const WHY = {
528
537
  function HandOff({ item, onSettle, busy, }) {
529
538
  return (_jsxs("section", { className: "flex flex-col gap-2 border-l-2 border-primary/40 pl-3", children: [_jsxs("p", { className: "flex items-center gap-1.5 text-xs", children: [_jsx(CornerDownRightIcon, { className: "size-3.5 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: item.label }), _jsxs("span", { className: "text-muted-foreground", children: ["\u00B7 ", WHY[item.why]] })] }), item.task && _jsx("p", { className: "text-muted-foreground text-xs", children: item.task }), _jsx(Items, { items: item.items, onSettle: onSettle, busy: busy })] }));
530
539
  }
540
+ /** The app's parts of a task's block (`taskActions`, `taskState`). */
541
+ const TaskSlots = createContext({});
542
+ const TASK_STATE = {
543
+ open: 'Open',
544
+ done: 'Kept',
545
+ dropped: 'Dropped',
546
+ };
547
+ /**
548
+ * A task's turns in the chat (`TaskItem`): one block with its label and state; while open the
549
+ * app's buttons (`taskActions`) sit in its header, once ended it folds to that one line.
550
+ */
551
+ function TaskBlock({ item, onSettle, busy, }) {
552
+ const slots = useContext(TaskSlots);
553
+ const state = slots.state?.(item) ?? item.state;
554
+ const live = item.items.some(isLive);
555
+ const [open, setOpen] = useState(null);
556
+ const shown = open ?? (state === 'open' || live);
557
+ return (_jsxs("section", { "data-slot": "chat-task", "data-state": state, className: "flex min-w-0 flex-col gap-2 rounded-md border border-primary/30 bg-primary/5 px-3 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1.5 text-xs", children: [_jsx(WrenchIcon, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("button", { type: "button", className: "min-w-0 flex-1 truncate text-left font-medium", title: item.label, "aria-expanded": shown, onClick: () => setOpen(!shown), children: item.label }), state === 'open' ? (!live && slots.actions?.(item)) : (_jsxs("span", { className: "shrink-0 text-muted-foreground", children: [state === 'done' ? _jsx(CheckIcon, { className: "inline size-3.5", "aria-hidden": true }) : null, ' ', TASK_STATE[state]] }))] }), shown && _jsx(Items, { items: item.items, onSettle: onSettle, busy: busy })] }));
558
+ }
559
+ /** The open task's chip over the box: the next message goes to it; its × steps out. */
560
+ function TaskChip({ task, onLeave }) {
561
+ return (_jsxs("span", { "data-slot": "chat-task-chip", className: "inline-flex min-w-0 max-w-full items-center gap-1 rounded-md border border-primary/30 bg-primary/5 px-1.5 py-0.5 text-xs", title: `Your next message goes to ${task.label}`, children: [_jsx(WrenchIcon, { className: "size-3 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("span", { className: "truncate", children: task.label }), _jsx(IconButton, { icon: XIcon, label: "Step out of this task", size: "icon-xs", onClick: onLeave })] }));
562
+ }
531
563
  /** The composer's triggers: `@` points at a thing (the `sources`), `#` picks the tier for one message. */
532
564
  const POINT = '@';
533
565
  const TIER = '#';
@@ -1,4 +1,4 @@
1
- import type { AssistantEvent, AssistantMessage, AssistantScreenOutcome, ChatItem, ChatUsage, DraftItem, StepItem } from '@columenlabs/lumen-ui-types/assistant';
1
+ import type { AssistantEvent, AssistantMessage, AssistantScreenOutcome, AssistantTask, ChatItem, ChatUsage, DraftItem, StepItem } from '@columenlabs/lumen-ui-types/assistant';
2
2
  import type { AssistantAdapter } from './client.js';
3
3
  /**
4
4
  * An item as the panel draws it: a text still streaming, a step still running. `writing` marks a
@@ -32,6 +32,8 @@ export interface AssistantChatState {
32
32
  canSteer: boolean;
33
33
  /** The chat is loading. */
34
34
  loading: boolean;
35
+ /** The newest task open in this chat (`AssistantTask`): the composer offers to keep the next message on it. */
36
+ task: AssistantTask | null;
35
37
  /**
36
38
  * Sends now, or while an answer runs: `steer` (the default where the adapter can) into it at its
37
39
  * next step, else queued for after it.
@@ -46,8 +46,13 @@ function resendOf(user) {
46
46
  ...(images.length ? { images } : {}),
47
47
  };
48
48
  }
49
- /** Where an event lands: the chat, or the last hand-off section when it came from inside one. */
50
- function sinkOf(items, handoff) {
49
+ /**
50
+ * Where an event lands: the chat, the task block this answer runs in, or the last hand-off section
51
+ * when it came from inside one.
52
+ */
53
+ function sinkOf(all, handoff) {
54
+ const top = all[all.length - 1];
55
+ const items = top?.kind === 'task' && top.live ? top.items : all;
51
56
  if (!handoff)
52
57
  return items;
53
58
  for (let i = items.length - 1; i >= 0; i--) {
@@ -59,7 +64,10 @@ function sinkOf(items, handoff) {
59
64
  }
60
65
  /** A copy of the items with one event applied (hand-off sections copied too). */
61
66
  export function applyEvent(prev, e) {
62
- const items = prev.map((i) => i.kind === 'handoff' ? { ...i, items: [...i.items] } : i);
67
+ const copy = (i) => i.kind === 'handoff' || i.kind === 'task'
68
+ ? { ...i, items: i.items.map(copy) }
69
+ : i;
70
+ const items = prev.map(copy);
63
71
  const inside = 'handoff' in e.data && e.data.handoff === true;
64
72
  const sink = sinkOf(items, inside);
65
73
  const last = sink[sink.length - 1];
@@ -115,10 +123,31 @@ export function applyEvent(prev, e) {
115
123
  break;
116
124
  }
117
125
  case 'handoff':
118
- items.push({ ...e.data, items: [] });
126
+ sink.push({ ...e.data, items: [] });
119
127
  break;
128
+ case 'task': {
129
+ // This answer runs in a task: the message just sent joins the task's block when the block is
130
+ // right above it, else a new block opens under it; what the answer says lands in the block.
131
+ const user = items.findLastIndex((i) => i.kind === 'user' && !i.steered);
132
+ const above = user > 0 ? items[user - 1] : undefined;
133
+ if (user >= 0 &&
134
+ user === items.length - 1 &&
135
+ above?.kind === 'task' &&
136
+ above.id === e.data.id) {
137
+ const u = items.splice(user, 1)[0];
138
+ items[user - 1] = {
139
+ ...above,
140
+ ...e.data,
141
+ items: [...above.items, u],
142
+ live: true,
143
+ };
144
+ }
145
+ else
146
+ items.push({ kind: 'task', ...e.data, items: [], live: true });
147
+ break;
148
+ }
120
149
  case 'handoff_done': {
121
- const h = [...items].reverse().find((i) => i.kind === 'handoff');
150
+ const h = [...sink].reverse().find((i) => i.kind === 'handoff');
122
151
  if (h)
123
152
  h.usage = e.data.usage;
124
153
  break;
@@ -164,16 +193,23 @@ export function applyEvent(prev, e) {
164
193
  const settled = (items) => items.map((i) => {
165
194
  if (i.kind === 'handoff')
166
195
  return { ...i, items: settled(i.items) };
196
+ if (i.kind === 'task') {
197
+ const { live: _l, ...rest } = i;
198
+ return { ...rest, items: settled(i.items) };
199
+ }
167
200
  if (!i.live || i.kind === 'screen')
168
201
  return i;
169
202
  const { live: _l, writing: _w, ...rest } = i;
170
203
  return rest;
171
204
  });
205
+ /** How long Stop waits for the server to end the answer before it stops reading the stream. */
206
+ const STOP_WAIT_MS = 8_000;
172
207
  export function useAssistantChat(client) {
173
208
  const [chatId, setChatId] = useState(null);
174
209
  const [title, setTitle] = useState('');
175
210
  const [items, setItems] = useState([]);
176
211
  const [usage, setUsage] = useState(null);
212
+ const [task, setTask] = useState(null);
177
213
  const [busy, setBusy] = useState(false);
178
214
  const [loading, setLoading] = useState(false);
179
215
  const [queue, setQueue] = useState([]);
@@ -244,6 +280,10 @@ export function useAssistantChat(client) {
244
280
  }
245
281
  else if (e.event === 'usage')
246
282
  setUsage(e.data);
283
+ else if (e.event === 'task') {
284
+ setTask(e.data.state === 'open' ? e.data : null);
285
+ setItems((p) => applyEvent(p, e));
286
+ }
247
287
  else if (e.event === 'done')
248
288
  setUsage(e.data.usage);
249
289
  else if (e.event === 'steered') {
@@ -351,6 +391,7 @@ export function useAssistantChat(client) {
351
391
  setTitle('');
352
392
  setItems([]);
353
393
  setUsage(null);
394
+ setTask(null);
354
395
  void run(user.text, { ...extra, ...resendOf(user) });
355
396
  }
356
397
  else {
@@ -377,7 +418,26 @@ export function useAssistantChat(client) {
377
418
  setQueue((q) => q.filter((x) => x.id !== id));
378
419
  return m;
379
420
  }, []);
380
- const stop = useCallback(() => abort.current?.abort(), []);
421
+ // Stop asks the server to end the answer, and the stream ends there with what was said and made
422
+ // so far, so nothing it finished is lost. An app with no stop route (or one that does not
423
+ // answer in a few seconds) stops reading the stream instead.
424
+ const stop = useCallback(() => {
425
+ const ctl = abort.current;
426
+ const id = idRef.current;
427
+ if (!ctl)
428
+ return;
429
+ if (!client.stop || !id)
430
+ return void ctl.abort();
431
+ const late = setTimeout(() => {
432
+ if (abort.current === ctl)
433
+ ctl.abort();
434
+ }, STOP_WAIT_MS);
435
+ client.stop(id).catch(() => {
436
+ clearTimeout(late);
437
+ if (abort.current === ctl)
438
+ ctl.abort();
439
+ });
440
+ }, [client]);
381
441
  const reset = useCallback(() => {
382
442
  abort.current?.abort();
383
443
  setQueue([]);
@@ -386,6 +446,7 @@ export function useAssistantChat(client) {
386
446
  setTitle('');
387
447
  setItems([]);
388
448
  setUsage(null);
449
+ setTask(null);
389
450
  }, []);
390
451
  const open = useCallback(async (id) => {
391
452
  abort.current?.abort();
@@ -405,6 +466,7 @@ export function useAssistantChat(client) {
405
466
  : i;
406
467
  }));
407
468
  setUsage(c.usage ?? null);
469
+ setTask(c.task ?? null);
408
470
  }
409
471
  catch (err) {
410
472
  setItems([{ kind: 'error', text: errorText(err) }]);
@@ -417,7 +479,7 @@ export function useAssistantChat(client) {
417
479
  const replace = useCallback((next) => {
418
480
  const swap = (list) => list.map((i) => i.kind === next.kind && i.id === next.id
419
481
  ? next
420
- : i.kind === 'handoff'
482
+ : i.kind === 'handoff' || i.kind === 'task'
421
483
  ? { ...i, items: swap(i.items) }
422
484
  : i);
423
485
  setItems(swap);
@@ -441,7 +503,7 @@ export function useAssistantChat(client) {
441
503
  const outcome = { status: o.status, ...(o.text ? { text: o.text } : {}) };
442
504
  const mark = (list) => list.map((i) => i.kind === 'screen' && i.id === o.id
443
505
  ? { kind: 'screen', id: i.id, label: i.label, moves: i.moves, outcome }
444
- : i.kind === 'handoff'
506
+ : i.kind === 'handoff' || i.kind === 'task'
445
507
  ? { ...i, items: mark(i.items) }
446
508
  : i);
447
509
  setItems(mark);
@@ -456,6 +518,7 @@ export function useAssistantChat(client) {
456
518
  queue,
457
519
  canSteer,
458
520
  loading,
521
+ task,
459
522
  send,
460
523
  retry,
461
524
  steerQueued,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.286",
3
+ "version": "0.1.288",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "@codemirror/lang-javascript": "6.2.5",
50
50
  "@codemirror/lang-json": "6.0.2",
51
51
  "@codemirror/lang-sql": "6.10.0",
52
- "@columenlabs/lumen-ui-types": "0.1.286",
52
+ "@columenlabs/lumen-ui-types": "0.1.288",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",