@columenlabs/lumen-ui 0.1.190 → 0.1.192

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.
@@ -56,6 +56,8 @@ function itemMarkdown(i, depth, name) {
56
56
  ]
57
57
  .filter(Boolean)
58
58
  .join('\n\n');
59
+ case 'screen':
60
+ return `_Screen: ${i.label}${i.outcome ? ` (${i.outcome.status})` : ''}._`;
59
61
  case 'handoff':
60
62
  return [
61
63
  `${h} ${i.label} (${i.why})`,
@@ -1,8 +1,9 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useQuery, useQueryClient } from '@tanstack/react-query';
3
- import { ArrowUpIcon, CheckIcon, ChevronRightIcon, CircleAlertIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, LightbulbIcon, PencilIcon, PlusIcon, SparklesIcon, SquareIcon, Undo2Icon, WrenchIcon, XIcon, } from 'lucide-react';
3
+ import { ArrowUpIcon, CheckIcon, ChevronRightIcon, CircleAlertIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, LightbulbIcon, MonitorIcon, PencilIcon, PlusIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, } from 'lucide-react';
4
4
  import { createContext, useContext, useEffect, useRef, useState } from 'react';
5
5
  import { LumenAssistantButton, LumenAssistantChats, LumenAssistantPanel, assistantOpen, useAssistantHotkey, } from '../assistant-panel.js';
6
+ import { screenMapFor, useAssistantScreen } from '../assistant-screen.js';
6
7
  import { IconButton } from '../icon-button.js';
7
8
  import { errorText } from '../integrations.js';
8
9
  import { lazyPart } from '../lazy-part.js';
@@ -68,6 +69,43 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, onC
68
69
  lumenToast.error(errorText(e));
69
70
  }
70
71
  };
72
+ // The assistant moves the screen (Settings ▸ Screen): the page map goes with each message, and
73
+ // each new screen call runs here once; how it ended goes with the next message.
74
+ const screen = useAssistantScreen();
75
+ const screenOn = !!screen && (s?.screen ?? 'move') === 'move';
76
+ const extra = () => ({
77
+ ...(context ? { context } : {}),
78
+ ...(screenOn && screen ? { screen: screenMapFor(screen) } : {}),
79
+ });
80
+ const ran = useRef(new Set());
81
+ const ended = chat.screenEnded;
82
+ useEffect(() => {
83
+ for (const i of screenCalls(chat.items)) {
84
+ if (!i.live || ran.current.has(i.id))
85
+ continue;
86
+ ran.current.add(i.id);
87
+ if (!screen) {
88
+ ended({ id: i.id, status: 'failed', text: 'this page cannot be moved' });
89
+ continue;
90
+ }
91
+ void screen.run(i.id, i.moves).then((o) => {
92
+ ended({ id: i.id, ...o });
93
+ // On a phone the panel covers the page: it steps aside so the move shows.
94
+ if (o.status === 'done' && onClose && window.matchMedia('(max-width: 767px)').matches)
95
+ onClose();
96
+ });
97
+ }
98
+ }, [chat.items, screen, ended, onClose]);
99
+ const controls = screen
100
+ ? {
101
+ canUndo: (id) => screen.canUndo(id),
102
+ undo: (id) => void screen.undo(id).then((o) => ended({ id, ...o })),
103
+ go: (item) => {
104
+ ran.current.add(item.id);
105
+ void screen.run(item.id, item.moves, true).then((o) => ended({ id: item.id, ...o }));
106
+ },
107
+ }
108
+ : null;
71
109
  const tiers = s?.tiers ?? [];
72
110
  const above = tiers.slice(tiers.findIndex((t) => t.id === s?.tier) + 1);
73
111
  // The kit's commands; the app's own (`commands`, then the server's) come first and win a name.
@@ -121,7 +159,7 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, onC
121
159
  args: '<task>',
122
160
  run: (task) => {
123
161
  if (task.trim())
124
- chat.send(task, { ...(context ? { context } : {}), tier: above[0]?.id });
162
+ chat.send(task, { ...extra(), tier: above[0]?.id });
125
163
  return undefined;
126
164
  },
127
165
  },
@@ -142,7 +180,7 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, onC
142
180
  id: c.id,
143
181
  title: c.title,
144
182
  updated_at: new Date(c.at).toISOString(),
145
- })) ?? (chats.isFetched ? [] : null), value: chat.chatId ?? undefined, 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(Messages, { chat: chat, status: s, onSettings: onSettings }) }), _jsx(LumenComposer, { name: s?.name ?? 'the assistant', busy: chat.busy, queued: chat.queued, disabled: !s || !!s.wait, commands: allCommands, tiers: tiers, sources: allSources, page: context?.label, onSend: (text, extra) => chat.send(text, { ...(context ? { context } : {}), ...extra }), onStop: chat.stop }), _jsx(UsageLine, { usage: chat.usage })] }));
183
+ })) ?? (chats.isFetched ? [] : null), value: chat.chatId ?? undefined, 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(Messages, { chat: chat, status: s, onSettings: onSettings }) }) }), _jsx(LumenComposer, { name: s?.name ?? 'the assistant', busy: chat.busy, queued: chat.queued, disabled: !s || !!s.wait, commands: allCommands, tiers: tiers, sources: allSources, page: context?.label, onSend: (text, more) => chat.send(text, { ...extra(), ...more }), onStop: chat.stop }), _jsx(UsageLine, { usage: chat.usage })] }));
146
184
  }
147
185
  function Messages({ chat, status, onSettings, }) {
148
186
  const end = useRef(null);
@@ -223,6 +261,8 @@ function Item({ item, onSettle, busy, }) {
223
261
  return _jsx(Step, { step: item, live: !!item.live });
224
262
  case 'draft':
225
263
  return _jsx(Draft, { draft: item, onSettle: onSettle, busy: busy });
264
+ case 'screen':
265
+ return _jsx(ScreenChip, { item: item });
226
266
  case 'handoff':
227
267
  return _jsx(HandOff, { item: item, onSettle: onSettle, busy: busy });
228
268
  case 'advice':
@@ -239,6 +279,40 @@ function Item({ item, onSettle, busy, }) {
239
279
  return (_jsxs("p", { role: "alert", className: "flex items-start gap-1.5 text-loss text-sm", children: [_jsx(CircleAlertIcon, { className: "mt-0.5 size-4 shrink-0" }), item.text] }));
240
280
  }
241
281
  }
282
+ /** Every screen call in the chat, hand-off sections included. */
283
+ function screenCalls(items) {
284
+ return items.flatMap((i) => i.kind === 'screen' ? [i] : i.kind === 'handoff' ? screenCalls(i.items) : []);
285
+ }
286
+ const Screen = createContext(null);
287
+ /** A screen call's moves in words, for the chip's hover. */
288
+ const movesText = (moves) => moves
289
+ .map((m) => 'go' in m
290
+ ? `Go to ${m.go}`
291
+ : 'settings' in m
292
+ ? `Settings ▸ ${m.settings}${m.row ? ` ▸ ${m.row}` : ''}`
293
+ : 'chart' in m
294
+ ? `Chart ${[m.chart].flat().join(', ')}`
295
+ : 'open' in m
296
+ ? `Open ${m.open}`
297
+ : `Point at ${m.point.replace(/^card:/, '')}`)
298
+ .join(' · ');
299
+ /**
300
+ * A screen call in the chat: ✓ and what is on screen, with Undo; why it failed; or, while the
301
+ * person was mid-edit, Go now.
302
+ */
303
+ function ScreenChip({ item }) {
304
+ const c = useContext(Screen);
305
+ // No outcome and not running: it ran in another tab or before a reload, which kept no result.
306
+ const status = item.outcome?.status ?? (item.live ? 'running' : 'sent');
307
+ const said = status === 'failed'
308
+ ? `Couldn't move · ${item.outcome?.text ?? ''}`
309
+ : status === 'waiting'
310
+ ? 'Waits while you edit'
311
+ : status === 'undone'
312
+ ? 'Undone'
313
+ : null;
314
+ return (_jsxs("div", { "data-slot": "assistant-screen", "data-status": status, className: "flex min-w-0 items-center gap-2 rounded-lg border bg-muted/30 px-3 py-1.5 text-muted-foreground text-xs", children: [status === 'running' ? (_jsx(Spinner, { className: "size-3.5" })) : status === 'done' ? (_jsx(CheckIcon, { className: "size-3.5 shrink-0 text-edge" })) : status === 'failed' ? (_jsx(TriangleAlertIcon, { className: "size-3.5 shrink-0 text-loss" })) : status === 'undone' ? (_jsx(Undo2Icon, { className: "size-3.5 shrink-0" })) : (_jsx(MonitorIcon, { className: "size-3.5 shrink-0" })), _jsxs("span", { className: "min-w-0 flex-1 truncate", title: `${item.label} · ${movesText(item.moves)}`, children: [_jsx("span", { className: cn(status === 'undone' ? 'line-through' : 'text-foreground'), children: item.label }), said && _jsxs("span", { children: [" \u00B7 ", said] })] }), status === 'done' && c?.canUndo(item.id) && (_jsx(IconButton, { icon: Undo2Icon, label: "Undo", size: "icon-xs", onClick: () => c.undo(item.id) })), status === 'waiting' && c && (_jsx(IconButton, { icon: MonitorIcon, label: "Go now", variant: "outline", size: "icon-xs", onClick: () => c.go(item) }))] }));
315
+ }
242
316
  const Rule = ({ children }) => (_jsx("p", { className: "flex items-center gap-2 text-muted-foreground text-xs before:h-px before:flex-1 before:bg-border after:h-px after:flex-1 after:bg-border", children: children }));
243
317
  function Step({ step, live }) {
244
318
  const undo = useContext(UndoWrite);
@@ -85,7 +85,10 @@ function General({ client }) {
85
85
  value: t.id,
86
86
  label: t.label,
87
87
  })),
88
- ], onChange: (advice) => void set({ advice }) }) }))] }));
88
+ ], onChange: (advice) => void set({ advice }) }) })), _jsx(Row, { 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: [
89
+ { value: 'move', label: 'Move' },
90
+ { value: 'off', label: 'Off' },
91
+ ], onChange: (screen) => void set({ screen: screen }) }) })] }));
89
92
  } }));
90
93
  }
91
94
  // ---------------------------------------------------------------- model providers
@@ -1,4 +1,4 @@
1
- import type { AssistantEvent, AssistantMessage, ChatItem, ChatUsage, DraftItem, StepItem } from '@columenlabs/lumen-ui-types/assistant';
1
+ import type { AssistantEvent, AssistantMessage, AssistantScreenOutcome, ChatItem, ChatUsage, DraftItem, StepItem } from '@columenlabs/lumen-ui-types/assistant';
2
2
  import type { AssistantAdapter } from './client.js';
3
3
  /** An item as the panel draws it: a text still streaming, a step still running. */
4
4
  export type LiveItem = ChatItem & {
@@ -22,6 +22,8 @@ export interface AssistantChatState {
22
22
  settle: (draft: DraftItem, action: 'save' | 'dismiss') => Promise<void>;
23
23
  /** Takes back a change the assistant made at once (Undo on its line). */
24
24
  undo: (step: StepItem) => Promise<void>;
25
+ /** A screen call ended in the browser: shown on its chip, and sent with the next message. */
26
+ screenEnded: (outcome: AssistantScreenOutcome) => void;
25
27
  }
26
28
  /** A copy of the items with one event applied (hand-off sections copied too). */
27
29
  export declare function applyEvent(prev: LiveItem[], e: AssistantEvent): LiveItem[];
@@ -9,6 +9,27 @@ function browserZone() {
9
9
  return undefined;
10
10
  }
11
11
  }
12
+ // Screen calls' outcomes not yet sent (they go with the next message), kept for this tab so a
13
+ // reload neither loses them nor leaves their chips without a result.
14
+ const SCREEN_KEY = 'lumen-assistant:screen';
15
+ function readOutcomes() {
16
+ try {
17
+ const m = new Map(JSON.parse(sessionStorage.getItem(SCREEN_KEY) ?? '[]'));
18
+ return m;
19
+ }
20
+ catch {
21
+ return new Map();
22
+ }
23
+ }
24
+ function keepOutcomes(m) {
25
+ try {
26
+ if (m.size)
27
+ sessionStorage.setItem(SCREEN_KEY, JSON.stringify([...m].slice(-50)));
28
+ else
29
+ sessionStorage.removeItem(SCREEN_KEY);
30
+ }
31
+ catch { }
32
+ }
12
33
  /** Where an event lands: the chat, or the last hand-off section when it came from inside one. */
13
34
  function sinkOf(items, handoff) {
14
35
  if (!handoff)
@@ -62,6 +83,12 @@ export function applyEvent(prev, e) {
62
83
  sink.push(draft);
63
84
  break;
64
85
  }
86
+ case 'screen': {
87
+ // Live until the browser runs it: the panel runs a screen call once, while it is new.
88
+ const { handoff: _h, ...call } = e.data;
89
+ sink.push({ ...call, live: true });
90
+ break;
91
+ }
65
92
  case 'handoff':
66
93
  items.push({ ...e.data, items: [] });
67
94
  break;
@@ -102,11 +129,14 @@ export function applyEvent(prev, e) {
102
129
  }
103
130
  return items;
104
131
  }
105
- /** Nothing streams any more: a text or step cut by Stop ends where it was. */
132
+ /**
133
+ * Nothing streams any more: a text or step cut by Stop ends where it was. A screen call stays live
134
+ * until the browser has run it (`screenEnded`).
135
+ */
106
136
  const settled = (items) => items.map((i) => {
107
137
  if (i.kind === 'handoff')
108
138
  return { ...i, items: settled(i.items) };
109
- if (!i.live)
139
+ if (!i.live || i.kind === 'screen')
110
140
  return i;
111
141
  const { live: _l, ...rest } = i;
112
142
  return rest;
@@ -120,6 +150,8 @@ export function useAssistantChat(client) {
120
150
  const [loading, setLoading] = useState(false);
121
151
  const [queued, setQueued] = useState(null);
122
152
  const abort = useRef(null);
153
+ const outcomes = useRef(null);
154
+ outcomes.current ??= readOutcomes();
123
155
  const idRef = useRef(null);
124
156
  idRef.current = chatId;
125
157
  const run = useCallback(async (message, extra) => {
@@ -138,12 +170,17 @@ export function useAssistantChat(client) {
138
170
  ...(extra?.tier ? { tier: extra.tier } : {}),
139
171
  },
140
172
  ]);
173
+ // Every screen outcome not yet sent goes with this message; kept again if it fails.
174
+ const sent = [...(outcomes.current?.values() ?? [])];
175
+ outcomes.current?.clear();
176
+ keepOutcomes(new Map());
141
177
  try {
142
178
  await client.send({
143
179
  message,
144
180
  ...(idRef.current ? { chat_id: idRef.current } : {}),
145
181
  ...(browserZone() ? { time_zone: browserZone() } : {}),
146
182
  ...extra,
183
+ ...(sent.length ? { outcomes: sent } : {}),
147
184
  }, (e) => {
148
185
  if (e.event === 'chat') {
149
186
  idRef.current = e.data.id;
@@ -159,6 +196,11 @@ export function useAssistantChat(client) {
159
196
  }, ctl.signal);
160
197
  }
161
198
  catch (err) {
199
+ for (const o of sent)
200
+ if (!outcomes.current?.has(o.id))
201
+ outcomes.current?.set(o.id, o);
202
+ if (outcomes.current)
203
+ keepOutcomes(outcomes.current);
162
204
  if (ctl.signal.aborted)
163
205
  setItems((p) => [...p, { kind: 'stopped' }]);
164
206
  else
@@ -205,7 +247,14 @@ export function useAssistantChat(client) {
205
247
  idRef.current = c.id;
206
248
  setChatId(c.id);
207
249
  setTitle(c.title);
208
- setItems(c.items);
250
+ // A call that ended here but whose outcome is not sent yet shows it already.
251
+ const kept = outcomes.current ?? new Map();
252
+ setItems(c.items.map((i) => {
253
+ const o = i.kind === 'screen' && !i.outcome ? kept.get(i.id) : undefined;
254
+ return o && i.kind === 'screen'
255
+ ? { ...i, outcome: { status: o.status, ...(o.text ? { text: o.text } : {}) } }
256
+ : i;
257
+ }));
209
258
  setUsage(c.usage ?? null);
210
259
  }
211
260
  catch (err) {
@@ -236,6 +285,18 @@ export function useAssistantChat(client) {
236
285
  return;
237
286
  replace(await client.undoWrite(id, step.id));
238
287
  }, [client, replace]);
288
+ const screenEnded = useCallback((o) => {
289
+ outcomes.current?.set(o.id, o);
290
+ if (outcomes.current)
291
+ keepOutcomes(outcomes.current);
292
+ const outcome = { status: o.status, ...(o.text ? { text: o.text } : {}) };
293
+ const mark = (list) => list.map((i) => i.kind === 'screen' && i.id === o.id
294
+ ? { kind: 'screen', id: i.id, label: i.label, moves: i.moves, outcome }
295
+ : i.kind === 'handoff'
296
+ ? { ...i, items: mark(i.items) }
297
+ : i);
298
+ setItems(mark);
299
+ }, []);
239
300
  return {
240
301
  chatId,
241
302
  title,
@@ -250,5 +311,6 @@ export function useAssistantChat(client) {
250
311
  reset,
251
312
  settle,
252
313
  undo,
314
+ screenEnded,
253
315
  };
254
316
  }
@@ -1,3 +1,4 @@
1
+ import type { AssistantScreenMap, AssistantScreenMove, AssistantScreenStatus } from '@columenlabs/lumen-ui-types/assistant';
1
2
  import type { IndicatorSpec } from '@columenlabs/lumen-ui-types/chart-formula';
2
3
  import { type ReactNode } from 'react';
3
4
  import { type CardOnScreen } from './spotlight.js';
@@ -28,20 +29,8 @@ export interface AssistantMoveHandlers {
28
29
  restore?: (snap: unknown) => void;
29
30
  }
30
31
  /** One move, as the server checked it. */
31
- export type AssistantMove = {
32
- go: string;
33
- } | {
34
- settings: string;
35
- row?: string;
36
- } | {
37
- chart: string | string[];
38
- indicators?: IndicatorSpec[];
39
- } | {
40
- open: string;
41
- } | {
42
- point: string;
43
- };
44
- export type ScreenStatus = 'done' | 'failed' | 'waiting' | 'undone';
32
+ export type AssistantMove = AssistantScreenMove;
33
+ export type ScreenStatus = AssistantScreenStatus;
45
34
  export interface ScreenOutcome {
46
35
  status: ScreenStatus;
47
36
  text?: string;
@@ -86,7 +75,7 @@ export interface ScreenMap {
86
75
  cards: CardOnScreen[];
87
76
  workspace?: WorkspaceOnScreen;
88
77
  }
89
- interface ScreenApi {
78
+ export interface ScreenApi {
90
79
  register: (slot: string, h: AssistantMoveHandlers | null) => void;
91
80
  pages: (key: string, items: AssistantPage[] | null) => void;
92
81
  /** The workspace page says which workspace and tabs are on screen while it shows. */
@@ -121,4 +110,8 @@ export declare function useAssistantPages(items: {
121
110
  export declare function AssistantScreenProvider({ children }: {
122
111
  children: ReactNode;
123
112
  }): import("react").JSX.Element;
124
- export {};
113
+ /**
114
+ * The page map as the assistant's server reads it (`AssistantMessage.screen`): routes, named pages,
115
+ * Settings sections, the cards on screen and the rows the view can open.
116
+ */
117
+ export declare function screenMapFor(api: ScreenApi): AssistantScreenMap;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useRouter } from '@tanstack/react-router';
3
3
  import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, } from 'react';
4
4
  import { cardsOnScreen } from './spotlight.js';
@@ -88,6 +88,31 @@ const here = () => `${window.location.pathname}${window.location.search}${window
88
88
  const cardOf = (id) => document.querySelector(`[data-slot="lumen-card"][data-card="${CSS.escape(id)}"]`);
89
89
  /** Holds what each part of the screen takes, and runs the assistant's moves over them. */
90
90
  export function AssistantScreenProvider({ children }) {
91
+ // One per app: a shell inside an app's own provider uses that one.
92
+ if (useContext(ScreenCtx))
93
+ return _jsx(_Fragment, { children: children });
94
+ return _jsx(ScreenProvider, { children: children });
95
+ }
96
+ /**
97
+ * The page map as the assistant's server reads it (`AssistantMessage.screen`): routes, named pages,
98
+ * Settings sections, the cards on screen and the rows the view can open.
99
+ */
100
+ export function screenMapFor(api) {
101
+ const m = api.map();
102
+ const rows = api.can()?.rows ?? [];
103
+ return {
104
+ routes: m.routes,
105
+ pages: m.pages.map((p) => ({
106
+ path: p.path,
107
+ title: p.title,
108
+ ...(p.section ? { section: p.section } : {}),
109
+ })),
110
+ settings: m.settings,
111
+ cards: m.cards.map((c) => ({ id: c.id, title: c.title, kind: c.kind })),
112
+ ...(rows.length ? { rows } : {}),
113
+ };
114
+ }
115
+ function ScreenProvider({ children }) {
91
116
  const router = useRouter();
92
117
  const slots = useRef(new Map());
93
118
  const navPages = useRef(new Map());
package/dist/index.d.ts CHANGED
@@ -102,7 +102,7 @@ export { useTell } from './hooks/use-tell.js';
102
102
  export { useBackClose } from './hooks/use-back-close.js';
103
103
  export { VIEW_ICONS, iconOf, viewIcon } from './view-icons.js';
104
104
  export { type AppViewState, useAppView } from './app-views.js';
105
- export { type ScreenMap, type ScreenOutcome, type ScreenStatus, type WorkspaceOnScreen, type AssistantMove, type AssistantMoveHandlers, type AssistantPage, type AssistantPageType, AssistantScreenProvider, useAssistantMoves, useAssistantPages, useAssistantScreen, useAssistantWorkspace, } from './assistant-screen.js';
105
+ export { type ScreenApi, type ScreenMap, type ScreenOutcome, type ScreenStatus, type WorkspaceOnScreen, type AssistantMove, type AssistantMoveHandlers, type AssistantPage, type AssistantPageType, AssistantScreenProvider, screenMapFor, useAssistantMoves, useAssistantPages, useAssistantScreen, useAssistantWorkspace, } from './assistant-screen.js';
106
106
  export { type AssistantActions, AssistantContext, type AssistantNote, type AssistantRef, type AssistantSlot, useAssistant, AssistantPendingContext, useAssistantName, useAssistantNote, useAssistantPending, } from './assistant-context.js';
107
107
  export { WORKSPACES_KEY, frameKey, workspacesListQuery } from './workspace-keys.js';
108
108
  export { type WorkspaceApp, type WorkspaceDraftProps, WorkspacePage, type WorkspaceRecipe, type WorkspacesConfig, WorkspacesProvider, WorkspacesSection, } from './workspaces.js';
package/dist/index.js CHANGED
@@ -109,7 +109,7 @@ export { useTell } from './hooks/use-tell.js';
109
109
  export { useBackClose } from './hooks/use-back-close.js';
110
110
  export { VIEW_ICONS, iconOf, viewIcon } from './view-icons.js';
111
111
  export { useAppView } from './app-views.js';
112
- export { AssistantScreenProvider, useAssistantMoves, useAssistantPages, useAssistantScreen, useAssistantWorkspace, } from './assistant-screen.js';
112
+ export { AssistantScreenProvider, screenMapFor, useAssistantMoves, useAssistantPages, useAssistantScreen, useAssistantWorkspace, } from './assistant-screen.js';
113
113
  export { AssistantContext, useAssistant, AssistantPendingContext, useAssistantName, useAssistantNote, useAssistantPending, } from './assistant-context.js';
114
114
  export { WORKSPACES_KEY, frameKey, workspacesListQuery } from './workspace-keys.js';
115
115
  export { WorkspacePage, WorkspacesProvider, WorkspacesSection, } from './workspaces.js';
@@ -18,7 +18,9 @@ import { InputGroup, InputGroupAddon, InputGroupButton } from './ui/input-group.
18
18
  import { Label } from './ui/label.js';
19
19
  /** One setting inside an item: its label, then its control group, then an optional switch at the end. */
20
20
  export function SettingField({ label, info, htmlFor, end, stack = false, className, children, }) {
21
- return (_jsxs("div", { "data-slot": "setting-field", "data-stack": stack || undefined, className: cn('flex min-h-10 min-w-0 items-center gap-3 border-t border-border/60 py-1.5 first:border-t-0', stack && 'max-sm:flex-wrap max-sm:gap-y-1.5', className), children: [_jsxs("div", { className: cn('flex w-32 shrink-0 items-center gap-1.5 max-sm:w-24', stack && 'max-sm:w-auto max-sm:flex-1'), children: [_jsx(Label, { htmlFor: htmlFor, className: "truncate font-normal text-muted-foreground", title: label, children: label }), info && _jsx(InfoTip, { label: `About ${label}`, children: info })] }), children !== undefined && (_jsx("div", { className: cn('flex min-w-0 max-w-md flex-1 items-center gap-2', stack && 'max-sm:order-2 max-sm:max-w-none max-sm:basis-full'), children: children })), end && _jsx("div", { className: cn('ml-auto shrink-0', stack && 'max-sm:order-1'), children: end })] }));
21
+ return (_jsxs("div", { "data-slot": "setting-field", "data-stack": stack || undefined, className: cn('flex min-h-10 min-w-0 items-center gap-3 border-t border-border/60 py-1.5 first:border-t-0', stack && 'max-sm:flex-wrap max-sm:gap-y-1.5', className), children: [_jsxs("div", { className: cn('flex w-32 shrink-0 items-center gap-1.5 max-sm:w-24', stack && 'max-sm:w-auto max-sm:flex-1',
22
+ // Nothing beside it but `end`: the label takes the row, so a short name never cuts off.
23
+ children === undefined && 'w-auto min-w-0 flex-1 shrink max-sm:w-auto'), children: [_jsx(Label, { htmlFor: htmlFor, className: "truncate font-normal text-muted-foreground", title: label, children: label }), info && _jsx(InfoTip, { label: `About ${label}`, children: info })] }), children !== undefined && (_jsx("div", { className: cn('flex min-w-0 max-w-md flex-1 items-center gap-2', stack && 'max-sm:order-2 max-sm:max-w-none max-sm:basis-full'), children: children })), end && _jsx("div", { className: cn('ml-auto shrink-0', stack && 'max-sm:order-1'), children: end })] }));
22
24
  }
23
25
  /** A value read in place: cut to one line with the full text on hover, with Copy and Replace inside. */
24
26
  export function SettingValue({ value, label, mono = false, copy = false, onReplace, replaceLabel = 'Replace', replaceIcon: ReplaceIcon = PencilIcon, }) {
package/dist/shell.js CHANGED
@@ -7,6 +7,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
7
7
  import { useRouterState } from '@tanstack/react-router';
8
8
  import { ChevronsUpDownIcon, LogOutIcon, ScrollTextIcon, SettingsIcon, SunMoonIcon, } from 'lucide-react';
9
9
  import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
10
+ import { AssistantScreenProvider } from './assistant-screen.js';
10
11
  import { lazyPart } from './lazy-part.js';
11
12
  import { cn } from './lib/utils.js';
12
13
  import { LumenFinder } from './lumen-finder.js';
@@ -133,18 +134,18 @@ export function LumenShell({ app, appLabel, brand, nav, tabs, bar, status, healt
133
134
  title: s.title,
134
135
  })));
135
136
  const findSettings = useMemo(() => JSON.parse(sectionNames), [sectionNames]);
136
- return (_jsx(Ctx.Provider, { value: {
137
- app,
138
- appLabel,
139
- statusSlot,
140
- openSettings,
141
- closeSettings: () => setSettingsAt(null),
142
- hasSettings: (section) => findSettings.some((s) => s.id === section),
143
- refresh: onRefresh ? refresh : null,
144
- refreshing,
145
- hideAmounts,
146
- headerActions,
147
- }, children: _jsx(StatusBarProvider, { children: _jsx(DockSlot.Provider, { value: dockSlot, children: _jsx(StatusSignals, { app: app, model: bar, openSettings: openSettings, children: _jsx(SourceHealthProvider, { children: wrap(_jsx(TooltipProvider, { delayDuration: 200, children: _jsx(SidebarProvider, { defaultOpen: sidebarOpen(), children: _jsxs(LumenFinder, { app: app, settings: findSettings, openSettings: openSettings, config: finder, children: [_jsx(CloseDrawerOnNavigate, {}), _jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: brand }), _jsx(SidebarContent, { children: nav }), _jsx(SidebarFooter, { children: _jsx(NavUser, { who: user, signOutUrl: signOutUrl, onSettings: () => openSettings(), onLogs: onLogs }) }), _jsx(SidebarRail, {})] }), _jsxs(SidebarInset, { className: "flex min-h-svh min-w-0 flex-col", children: [_jsx(SignalBanners, {}), _jsx("div", { className: "flex min-w-0 flex-1 flex-col", children: children }, epoch), _jsx(LumenStatusBar, { app: app, model: bar, legacy: status, slot: setStatusSlot, end: _jsxs(_Fragment, { children: [health, releases && _jsx(WhatsNew, { api: releases, apps: releaseApps }), statusEnd, _jsx(StatusTimeZone, {})] }) }), tabs && _jsx(LumenTabBar, { items: tabs })] }), _jsx("div", { ref: setDockSlot, "data-slot": "shell-dock", className: "contents" }), panel, _jsx(LumenSettings, { open: settingsAt !== null, section: settingsAt ?? lastSection, onOpen: openSettings, onClose: () => setSettingsAt(null), appLabel: appLabel, profile: profile, security: security, sections: settings, shared: shared, sharedLabel: sharedLabel, groups: settingsGroups }), _jsx(Toaster, { position: "bottom-right" })] }) }) })) }) }) }) }) }));
137
+ return (_jsx(AssistantScreenProvider, { children: _jsx(Ctx.Provider, { value: {
138
+ app,
139
+ appLabel,
140
+ statusSlot,
141
+ openSettings,
142
+ closeSettings: () => setSettingsAt(null),
143
+ hasSettings: (section) => findSettings.some((s) => s.id === section),
144
+ refresh: onRefresh ? refresh : null,
145
+ refreshing,
146
+ hideAmounts,
147
+ headerActions,
148
+ }, children: _jsx(StatusBarProvider, { children: _jsx(DockSlot.Provider, { value: dockSlot, children: _jsx(StatusSignals, { app: app, model: bar, openSettings: openSettings, children: _jsx(SourceHealthProvider, { children: wrap(_jsx(TooltipProvider, { delayDuration: 200, children: _jsx(SidebarProvider, { defaultOpen: sidebarOpen(), children: _jsxs(LumenFinder, { app: app, settings: findSettings, openSettings: openSettings, config: finder, children: [_jsx(CloseDrawerOnNavigate, {}), _jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: brand }), _jsx(SidebarContent, { children: nav }), _jsx(SidebarFooter, { children: _jsx(NavUser, { who: user, signOutUrl: signOutUrl, onSettings: () => openSettings(), onLogs: onLogs }) }), _jsx(SidebarRail, {})] }), _jsxs(SidebarInset, { className: "flex min-h-svh min-w-0 flex-col", children: [_jsx(SignalBanners, {}), _jsx("div", { className: "flex min-w-0 flex-1 flex-col", children: children }, epoch), _jsx(LumenStatusBar, { app: app, model: bar, legacy: status, slot: setStatusSlot, end: _jsxs(_Fragment, { children: [health, releases && _jsx(WhatsNew, { api: releases, apps: releaseApps }), statusEnd, _jsx(StatusTimeZone, {})] }) }), tabs && _jsx(LumenTabBar, { items: tabs })] }), _jsx("div", { ref: setDockSlot, "data-slot": "shell-dock", className: "contents" }), panel, _jsx(LumenSettings, { open: settingsAt !== null, section: settingsAt ?? lastSection, onOpen: openSettings, onClose: () => setSettingsAt(null), appLabel: appLabel, profile: profile, security: security, sections: settings, shared: shared, sharedLabel: sharedLabel, groups: settingsGroups }), _jsx(Toaster, { position: "bottom-right" })] }) }) })) }) }) }) }) }) }));
148
149
  }
149
150
  /** A status bar entry: an optional health dot, a label and its value ("Mail · Up to date"). */
150
151
  export function StatusItem({ label, value, ok, className, }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.190",
3
+ "version": "0.1.192",
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.190",
52
+ "@columenlabs/lumen-ui-types": "0.1.192",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",