@columenlabs/lumen-ui 0.1.307 → 0.1.308

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.
@@ -197,18 +197,38 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
197
197
  ...(chatScope && allScopes && c.scope ? { where: chatScope.name?.(c.scope) } : {}),
198
198
  })) ?? (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, view: taskView, stop: chat.stop }, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings, onRetry: (at, user) => chat.retry(at, user, extra()), onFresh: (choice) => chat.fresh(choice, extra()), onAnswer: (text) => chat.send(text, 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 }) })] }));
199
199
  }
200
+ /** How near the end of the chat (px) still counts as at the end, so it keeps following. */
201
+ const FOLLOW_PX = 48;
200
202
  function Messages({ chat, status, onSettings, onRetry, onFresh, onAnswer, }) {
201
203
  const end = useRef(null);
202
204
  const box = useRef(null);
203
205
  const count = JSON.stringify(chat.items).length;
206
+ // Follow the chat as it grows only while the person is at its end: once they scroll up to read,
207
+ // a streaming answer or a card that changes leaves their place alone. Sending a message, or
208
+ // opening another chat, goes to the end again.
209
+ const pinned = useRef(true);
210
+ const onScroll = () => {
211
+ const el = box.current;
212
+ if (el)
213
+ pinned.current = el.scrollHeight - el.scrollTop - el.clientHeight < FOLLOW_PX;
214
+ };
215
+ const last = chat.items[chat.items.length - 1];
216
+ const sent = last?.kind === 'user' ? last.at : undefined;
217
+ // biome-ignore lint/correctness/useExhaustiveDependencies: a new message or chat pins the end again
218
+ useEffect(() => {
219
+ pinned.current = true;
220
+ }, [sent, chat.chatId]);
204
221
  // biome-ignore lint/correctness/useExhaustiveDependencies: scroll to the end as the chat grows
205
222
  useEffect(() => {
206
- end.current?.scrollIntoView({ block: 'end' });
207
- }, [count]);
223
+ const el = box.current;
224
+ // The list's own scroll only: scrollIntoView would move the page and every scroll box around it.
225
+ if (el && pinned.current)
226
+ el.scrollTop = el.scrollHeight;
227
+ }, [count, sent, chat.chatId, chat.loading]);
208
228
  // The app's own link to Settings, else the shell's, at the section that answers the wait.
209
229
  const shellSettings = useOpenSettings();
210
230
  const settings = onSettings ? (_section) => onSettings() : shellSettings;
211
- return (_jsxs("div", { ref: box, className: "relative min-h-0 flex-1 overflow-y-auto px-3 py-3", children: [_jsx(ReplyToSelection, { box: box }), status?.wait && (_jsxs(Alert, { className: "mb-3", children: [_jsx(CircleAlertIcon, {}), _jsxs(AlertDescription, { className: "flex flex-col items-start gap-2", children: [status.wait === 'app_cap' && status.allowance
231
+ return (_jsxs("div", { ref: box, onScroll: onScroll, className: "relative min-h-0 flex-1 overflow-y-auto px-3 py-3", children: [_jsx(ReplyToSelection, { box: box }), status?.wait && (_jsxs(Alert, { className: "mb-3", children: [_jsx(CircleAlertIcon, {}), _jsxs(AlertDescription, { className: "flex flex-col items-start gap-2", children: [status.wait === 'app_cap' && status.allowance
212
232
  ? allowanceWait(status.allowance)
213
233
  : WAIT[status.wait](status.admin, status.providers.find((p) => p.id === status.wait_provider)?.label ?? 'API'), settings && (_jsx(Button, { size: "sm", variant: "outline", onClick: () => settings(status.wait === 'app_cap' && status.allowance
214
234
  ? 'assistant-allowance'
@@ -451,7 +451,10 @@ export function useAssistantChat(client) {
451
451
  const open = useCallback(async (id) => {
452
452
  abort.current?.abort();
453
453
  setQueue([]);
454
- setLoading(true);
454
+ // Reading the chat that is on screen again (a task ended, a change was kept) swaps its items in
455
+ // place: the list stays, so the person's place in it stays. Only another chat shows Loading.
456
+ if (idRef.current !== id)
457
+ setLoading(true);
455
458
  try {
456
459
  const c = await client.chat(id);
457
460
  idRef.current = c.id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.307",
3
+ "version": "0.1.308",
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.307",
52
+ "@columenlabs/lumen-ui-types": "0.1.308",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",