@columenlabs/lumen-ui 0.1.281 → 0.1.282

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.
@@ -1,7 +1,7 @@
1
1
  export { type AssistantAdapter, assistantClient } from './client.js';
2
2
  export { ChatImage } from './chat-image.js';
3
3
  export { type AssistantCommand, type AssistantMode, type ComposerExtra, LumenAssistant, type LumenAssistantProps, LumenComposer, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
4
- export { assistantAdminSections, assistantSettingsSections } from './settings.js';
4
+ export { AutoCompactField, assistantAdminSections, assistantSettingsSections } from './settings.js';
5
5
  export { AssistantChatCost, AssistantUsageTable, UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
6
6
  export { type AssistantSource, type AssistantSourceItem, type ComposeRequest, type PageSelection, composeAssistant, selectionRef, useAssistantSource, usePageSelection, useShownAssistantSources, } from './sources.js';
7
7
  export { type AssistantChatState, type LiveItem, applyEvent, useAssistantChat, } from './use-assistant-chat.js';
@@ -4,7 +4,7 @@
4
4
  export { assistantClient } from './client.js';
5
5
  export { ChatImage } from './chat-image.js';
6
6
  export { LumenAssistant, LumenComposer, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
7
- export { assistantAdminSections, assistantSettingsSections } from './settings.js';
7
+ export { AutoCompactField, assistantAdminSections, assistantSettingsSections } from './settings.js';
8
8
  export { AssistantChatCost, AssistantUsageTable, UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
9
9
  export { composeAssistant, selectionRef, useAssistantSource, usePageSelection, useShownAssistantSources, } from './sources.js';
10
10
  export { applyEvent, useAssistantChat, } from './use-assistant-chat.js';
@@ -59,8 +59,8 @@ export interface LumenAssistantProps {
59
59
  mode?: string;
60
60
  };
61
61
  /**
62
- * Where the server compacts the chat, in tokens (the agent's `compactAt()`), named in the context
63
- * donut's hover. 150k by default.
62
+ * Where the server compacts the chat, in tokens (the agent's `compactAt()`): the context donut turns
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
66
  }
@@ -4,7 +4,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
4
4
  // (the person's messages, the answer as it streams, each tool call as a line, drafted changes as
5
5
  // cards to Save or Dismiss, work handed to a higher tier in its own section); and the composer,
6
6
  // with Stop, a queued message and the chat's cost and context under it.
7
- import { ASSISTANT_IMAGES_MAX, ASSISTANT_IMAGE_CHARS, } from '@columenlabs/lumen-ui-types/assistant';
7
+ import { ASSISTANT_IMAGES_MAX, ASSISTANT_IMAGE_CHARS, COMPACT_AT, } from '@columenlabs/lumen-ui-types/assistant';
8
8
  import { useQuery, useQueryClient } from '@tanstack/react-query';
9
9
  import { ArrowUpIcon, CheckIcon, ChevronRightIcon, CircleAlertIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, GlobeIcon, LightbulbIcon, ListPlusIcon, MonitorIcon, PaperclipIcon, PencilIcon, PlusIcon, RefreshCwIcon, ReplyIcon, SearchIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, } from 'lucide-react';
10
10
  import { Fragment, createContext, useContext, useEffect, useRef, useState, } from 'react';
@@ -36,7 +36,6 @@ import { useAssistantChat, } from './use-assistant-chat.js';
36
36
  const ChatMarkdown = lazyPart(() => import('./chat-markdown.js').then((m) => m.default), null, ({ children }) => _jsx("p", { className: "whitespace-pre-wrap", children: children }));
37
37
  /** The main model's context drawn full (tokens); the mark sits where the assistant compacts. */
38
38
  const CONTEXT_MAX = 200_000;
39
- const COMPACT_AT = 150_000;
40
39
  export const assistantStatusKey = (client) => [
41
40
  'lumen-assistant',
42
41
  client.key,
@@ -191,7 +190,7 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
191
190
  title: c.title,
192
191
  updated_at: new Date(c.at).toISOString(),
193
192
  ...(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: compactAt }) })] }));
193
+ })) ?? (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 }) })] }));
195
194
  }
196
195
  function Messages({ chat, status, onSettings, onRetry, onFresh, }) {
197
196
  const end = useRef(null);
@@ -929,30 +928,67 @@ const pointedRef = (p) => p.kind === 'card'
929
928
  text: textOf(p.el),
930
929
  }
931
930
  : { kind: p.section.kind, label: p.section.label, id: p.section.id, text: textOf(p.el) };
932
- /** Context tokens from which the donut turns yellow, and red. */
933
- const CONTEXT_WATCH = 75_000;
934
- const CONTEXT_ALERT = 100_000;
931
+ /** Share of the compact point from which the donut turns yellow; red at the compact point itself. */
932
+ const CONTEXT_WATCH = 0.75;
933
+ /** What the context pop-up counts from the chat: its turns, and the images and pointed-at things sent. */
934
+ function chatCounts(items) {
935
+ let turns = 0;
936
+ let attached = 0;
937
+ for (const i of items) {
938
+ if (i.kind !== 'user' || i.steered)
939
+ continue;
940
+ turns += 1;
941
+ attached += (i.images?.length ?? 0) + (i.refs?.length ?? 0);
942
+ }
943
+ return { turns, attached };
944
+ }
935
945
  /**
936
- * How full the chat's context is, as a small donut in the composer's icon row (after the paperclip and the modes):
937
- * it fills against `CONTEXT_MAX`, neutral below 75k, yellow from 75k, red from 100k. The hover gives
938
- * the tokens used, where the chat compacts, and how much of the last turn came from the prompt cache.
939
- * What a chat cost is not shown here: a person reads their use in Settings.
946
+ * How full the chat's context is, as a small donut in the composer's icon row (after the paperclip and
947
+ * the modes). It fills against `CONTEXT_MAX`: neutral, yellow from 75% of the compact point, red at it
948
+ * (75k and 100k when chats compact at 100k). Hover, focus or a tap (touch has no hover) opens its pop-up:
949
+ * context used of the most with a bar, that the chat compacts on its own, its turns, the images and
950
+ * attachments sent, and how much of the last turn came from the prompt cache. No price: a person reads
951
+ * their use in Settings.
940
952
  */
941
- function ContextDonut({ usage, compactAt }) {
953
+ function ContextDonut({ usage, compactAt, items, }) {
954
+ const [open, setOpen] = useState(false);
955
+ const touch = useRef(false);
956
+ const wasOpen = useRef(false);
942
957
  const ctx = usage?.context ?? 0;
943
958
  if (ctx <= 0)
944
959
  return null;
945
960
  const share = Math.min(1, ctx / CONTEXT_MAX);
946
961
  const r = 6;
947
962
  const round = 2 * Math.PI * r;
948
- const tone = ctx >= CONTEXT_ALERT
963
+ const level = ctx >= compactAt ? 'alert' : ctx >= compactAt * CONTEXT_WATCH ? 'watch' : 'ok';
964
+ const tone = level === 'alert'
949
965
  ? 'text-destructive'
950
- : ctx >= CONTEXT_WATCH
966
+ : level === 'watch'
951
967
  ? 'text-warning'
952
968
  : 'text-muted-foreground';
953
969
  const cached = usage && usage.in > 0 ? Math.round((usage.read / usage.in) * 100) : null;
970
+ const { turns, attached } = chatCounts(items);
954
971
  const said = `Context ${tokens(ctx)} of ${tokens(CONTEXT_MAX)}`;
955
- return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": said, "data-testid": "context-donut", className: cn('grid size-6 place-items-center rounded-md hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', tone), children: _jsxs("svg", { viewBox: "0 0 16 16", className: "size-4 -rotate-90", "aria-hidden": "true", children: [_jsx("title", { children: said }), _jsx("circle", { cx: "8", cy: "8", r: r, fill: "none", strokeWidth: "2.5", className: "stroke-muted-foreground/25" }), _jsx("circle", { cx: "8", cy: "8", r: r, fill: "none", strokeWidth: "2.5", stroke: "currentColor", strokeDasharray: `${share * round} ${round}` })] }) }) }), _jsxs(TooltipContent, { className: "flex flex-col items-start gap-0.5 text-left", children: [_jsx("span", { className: "num", children: said }), _jsxs("span", { className: "num text-muted-foreground", children: ["Compacts at ", tokens(compactAt)] }), cached !== null && (_jsxs("span", { className: "num text-muted-foreground", children: [cached, "% of the last turn read from cache"] }))] })] }));
972
+ const rows = [
973
+ ['This chat', `${turns} ${turns === 1 ? 'turn' : 'turns'}`],
974
+ ['Images and attachments', String(attached)],
975
+ ...(cached !== null ? [['Cache read', `${cached}%`]] : []),
976
+ ];
977
+ return (_jsxs(Tooltip, { open: open, onOpenChange: setOpen, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": said, "aria-expanded": open, "data-testid": "context-donut", "data-level": level,
978
+ // A tap opens or closes the pop-up (touch has no hover); a mouse click leaves it open.
979
+ // The press itself counts as outside the pop-up and closes it, so the tap reads the state before.
980
+ onPointerDown: (e) => {
981
+ touch.current = e.pointerType !== 'mouse';
982
+ wasOpen.current = open;
983
+ e.preventDefault();
984
+ }, onClick: (e) => {
985
+ e.preventDefault();
986
+ setOpen(touch.current ? !wasOpen.current : true);
987
+ }, className: cn('grid size-6 place-items-center rounded-md hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', tone), children: _jsxs("svg", { viewBox: "0 0 16 16", className: "size-4 -rotate-90", "aria-hidden": "true", children: [_jsx("title", { children: said }), _jsx("circle", { cx: "8", cy: "8", r: r, fill: "none", strokeWidth: "2.5", className: "stroke-muted-foreground/25" }), _jsx("circle", { cx: "8", cy: "8", r: r, fill: "none", strokeWidth: "2.5", stroke: "currentColor", strokeDasharray: `${share * round} ${round}` })] }) }) }), _jsxs(TooltipContent, { "data-testid": "context-popup", className: "flex w-56 flex-col items-stretch gap-1.5 px-3 py-2.5 text-left font-sans tabular-nums", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [_jsx("span", { className: "font-medium", children: "Context" }), _jsxs("span", { className: "whitespace-nowrap", children: [tokens(ctx), " ", _jsxs("span", { className: "text-muted-foreground", children: ["/ ", tokens(CONTEXT_MAX)] })] })] }), _jsx("div", { className: "h-1 overflow-hidden rounded-full bg-muted-foreground/20", children: _jsx("div", { className: cn('h-full rounded-full', level === 'alert'
988
+ ? 'bg-destructive'
989
+ : level === 'watch'
990
+ ? 'bg-warning'
991
+ : 'bg-foreground/60'), style: { width: `${Math.max(2, share * 100)}%` } }) }), _jsx("span", { className: "text-muted-foreground", children: "Auto-compacts as needed" }), _jsx("div", { className: "my-0.5 border-t" }), rows.map(([k, v]) => (_jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [_jsx("span", { className: "truncate text-muted-foreground", children: k }), _jsx("span", { className: "whitespace-nowrap", children: v })] }, k)))] })] }));
956
992
  }
957
993
  /**
958
994
  * The assistant docked in LumenShell: put `button` in `headerActions` and `panel` in `panel`.
@@ -1,5 +1,13 @@
1
1
  import type { SettingsSectionDef } from '../settings.js';
2
2
  import type { AssistantAdapter } from './client.js';
3
+ /**
4
+ * Settings: the context size past which a chat is summarized out of sight (Auto-compact), the
5
+ * person's own; the app's default until they pick. An app that shows only some of the kit's
6
+ * sections puts this row in its own.
7
+ */
8
+ export declare function AutoCompactField({ client }: {
9
+ client: AssistantAdapter;
10
+ }): import("react").JSX.Element | null;
3
11
  /**
4
12
  * The admin's section: the app's shared keys and each person's monthly allowance on them. Put it
5
13
  * where the app keeps its admin settings, shown only to admins.
@@ -1,4 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { COMPACT_AT, COMPACT_CHOICES } from '@columenlabs/lumen-ui-types/assistant';
2
3
  import { useQuery, useQueryClient } from '@tanstack/react-query';
3
4
  import { ChartColumnIcon, CoinsIcon, GlobeIcon, InboxIcon, KeyRoundIcon, LayersIcon, Link2Icon, LogInIcon, PlugIcon, ReceiptIcon, SendIcon, SparklesIcon, WrenchIcon, } from 'lucide-react';
4
5
  import { useId, useState } from 'react';
@@ -18,7 +19,7 @@ import { Label } from '../ui/label.js';
18
19
  import { Switch } from '../ui/switch.js';
19
20
  import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group.js';
20
21
  import { allowanceAmount, assistantStatusKey, usd, useAssistantStatus } from './lumen-assistant.js';
21
- import { AssistantUsageTable, OwnKeyUsage, UsageHistoryView } from './usage-history.js';
22
+ import { AssistantUsageTable, OwnKeyUsage, UsageHistoryView, tokens } from './usage-history.js';
22
23
  const EFFORTS = [
23
24
  { value: 'default', label: 'Model default' },
24
25
  { value: 'low', label: 'Low' },
@@ -84,12 +85,30 @@ function General({ client }) {
84
85
  value: t.id,
85
86
  label: t.label,
86
87
  })),
87
- ], onChange: (advice) => void set({ advice }) }) })), _jsx(SettingField, { label: "Screen", info: "Takes you to a page, a Settings row, a chart or a card when you ask. Each move has an Undo; nothing is saved.", children: _jsx(LumenSelector, { label: "Screen", size: "sm", className: "w-44", value: s.screen ?? 'move', options: [
88
+ ], onChange: (advice) => void set({ advice }) }) })), _jsx(AutoCompactField, { client: client }), _jsx(SettingField, { label: "Screen", info: "Takes you to a page, a Settings row, a chart or a card when you ask. Each move has an Undo; nothing is saved.", children: _jsx(LumenSelector, { label: "Screen", size: "sm", className: "w-44", value: s.screen ?? 'move', options: [
88
89
  { value: 'move', label: 'Move' },
89
90
  { value: 'off', label: 'Off' },
90
91
  ], onChange: (screen) => void set({ screen: screen }) }) })] }));
91
92
  } }));
92
93
  }
94
+ /**
95
+ * Settings: the context size past which a chat is summarized out of sight (Auto-compact), the
96
+ * person's own; the app's default until they pick. An app that shows only some of the kit's
97
+ * sections puts this row in its own.
98
+ */
99
+ export function AutoCompactField({ client }) {
100
+ const { apply } = useApply(client);
101
+ const s = useAssistantStatus(client).data;
102
+ if (!s)
103
+ return null;
104
+ const now = s.compact_at ?? COMPACT_AT;
105
+ const choices = [...COMPACT_CHOICES];
106
+ if (!choices.includes(now))
107
+ choices.push(now);
108
+ return (_jsx(SettingField, { label: "Auto-compact", info: "Past this much context, the chat's older part is summarized out of sight, so long chats stay quick.", children: _jsx(LumenSelector, { label: "Auto-compact", size: "sm", className: "w-44", value: String(now), options: choices
109
+ .sort((a, b) => a - b)
110
+ .map((n) => ({ value: String(n), label: `At ${tokens(n)}` })), onChange: (v) => void apply('settings', () => client.settings({ compact_at: Number(v) })) }) }));
111
+ }
93
112
  // ---------------------------------------------------------------- model providers
94
113
  /** A dollar cap a month, saved on Enter or leaving the field. */
95
114
  function CapInput({ label, value, onSave, }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.281",
3
+ "version": "0.1.282",
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.281",
52
+ "@columenlabs/lumen-ui-types": "0.1.282",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",