@columenlabs/lumen-ui 0.1.189 → 0.1.191

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());
@@ -1,7 +1,10 @@
1
- export declare function FrameScriptDialog({ api, frame, name, open, onOpenChange, }: {
1
+ import type { FrameRun } from '@columenlabs/lumen-ui-types/frames';
2
+ export declare function FrameScriptDialog({ api, frame, name, runs, open, onOpenChange, }: {
2
3
  api: string;
3
4
  frame: string;
4
5
  name: string;
6
+ /** The last runs, newest first: when, how many rows, how long, who, and why one failed. */
7
+ runs?: FrameRun[];
5
8
  open: boolean;
6
9
  onOpenChange: (open: boolean) => void;
7
10
  }): import("react").JSX.Element;
@@ -47,7 +47,7 @@ function parseScript(was, text) {
47
47
  args: (o.args ?? {}),
48
48
  };
49
49
  }
50
- export function FrameScriptDialog({ api, frame, name, open, onOpenChange, }) {
50
+ export function FrameScriptDialog({ api, frame, name, runs, open, onOpenChange, }) {
51
51
  const qc = useQueryClient();
52
52
  const assistant = useAssistantName();
53
53
  const read = useQuery({
@@ -96,8 +96,17 @@ export function FrameScriptDialog({ api, frame, name, open, onOpenChange, }) {
96
96
  setError(e.message);
97
97
  }
98
98
  };
99
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "flex h-[min(44rem,calc(100dvh-2rem))] flex-col sm:max-w-4xl", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Script \u00B7 ", name] }), _jsxs(DialogDescription, { children: ["What ", assistant, " runs to make these rows. Saved only if it runs."] })] }), read.isPending ? (_jsx(Skeleton, { className: "min-h-0 flex-1" })) : read.error ? (_jsxs(Alert, { variant: "destructive", children: [_jsx(AlertTitle, { children: "Couldn't read the script" }), _jsx(AlertDescription, { children: read.error.message })] })) : !script ? (_jsxs(Alert, { children: [_jsx(AlertTitle, { children: "No script" }), _jsxs(AlertDescription, { children: [assistant, " wrote these rows once: nothing runs to make them."] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-end gap-1", children: [lang && (_jsx(Badge, { variant: "secondary", className: "font-mono", children: lang })), _jsx(IconButton, { icon: WrapTextIcon, label: wrap ? 'Stop wrapping lines' : 'Wrap long lines', pressed: wrap, onClick: () => setWrap(!wrap) })] }), _jsx(LumenCodeEditor, { className: "flex-1", value: text, lang: lang, wrap: wrap, onChange: setText }), error && (_jsxs(Alert, { variant: "destructive", className: "max-h-32 overflow-y-auto", children: [_jsx(AlertTitle, { children: "Didn't save" }), _jsx(AlertDescription, { className: "break-words font-mono text-xs", children: error })] }))] })), _jsxs(DialogFooter, { children: [_jsx(IconButton, { icon: RotateCcwIcon, label: "Undo my changes", disabled: !changed || save.isPending, onClick: () => {
99
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "flex h-[min(44rem,calc(100dvh-2rem))] flex-col sm:max-w-4xl", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Script \u00B7 ", name] }), _jsxs(DialogDescription, { children: ["What ", assistant, " runs to make these rows. Saved only if it runs."] })] }), read.isPending ? (_jsx(Skeleton, { className: "min-h-0 flex-1" })) : read.error ? (_jsxs(Alert, { variant: "destructive", children: [_jsx(AlertTitle, { children: "Couldn't read the script" }), _jsx(AlertDescription, { children: read.error.message })] })) : !script ? (_jsxs(Alert, { children: [_jsx(AlertTitle, { children: "No script" }), _jsxs(AlertDescription, { children: [assistant, " wrote these rows once: nothing runs to make them."] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-end gap-1", children: [lang && (_jsx(Badge, { variant: "secondary", className: "font-mono", children: lang })), _jsx(IconButton, { icon: WrapTextIcon, label: wrap ? 'Stop wrapping lines' : 'Wrap long lines', pressed: wrap, onClick: () => setWrap(!wrap) })] }), _jsx(LumenCodeEditor, { className: "flex-1", value: text, lang: lang, wrap: wrap, onChange: setText }), runs && runs.length > 0 && _jsx(RunList, { runs: runs, assistant: assistant }), error && (_jsxs(Alert, { variant: "destructive", className: "max-h-32 overflow-y-auto", children: [_jsx(AlertTitle, { children: "Didn't save" }), _jsx(AlertDescription, { className: "break-words font-mono text-xs", children: error })] }))] })), _jsxs(DialogFooter, { children: [_jsx(IconButton, { icon: RotateCcwIcon, label: "Undo my changes", disabled: !changed || save.isPending, onClick: () => {
100
100
  setText(original);
101
101
  setError(null);
102
102
  } }), _jsxs(Button, { disabled: !script || save.isPending, onClick: run, children: [save.isPending ? _jsx(Spinner, {}) : _jsx(PlayIcon, {}), " Run and save"] })] })] }) }));
103
103
  }
104
+ /** The frame's last runs, one line each; a failure's reason in its hover. */
105
+ function RunList({ runs, assistant }) {
106
+ return (_jsx("ul", { className: "flex max-h-28 flex-col gap-0.5 overflow-y-auto text-xs text-muted-foreground", children: runs.map((r) => (_jsxs("li", { className: "flex min-w-0 items-center gap-2", title: r.error, children: [_jsx("span", { className: `size-1.5 shrink-0 rounded-full ${r.ok ? 'bg-[var(--meter-ok)]' : 'bg-destructive'}` }), _jsx("time", { className: "shrink-0", dateTime: r.at, children: new Date(r.at).toLocaleString('en-US', {
107
+ month: 'short',
108
+ day: 'numeric',
109
+ hour: 'numeric',
110
+ minute: '2-digit',
111
+ }) }), _jsx("span", { className: "num shrink-0", children: r.ok ? `${r.rows.toLocaleString('en-US')} rows` : 'failed' }), _jsxs("span", { className: "num shrink-0", children: [(r.ms / 1000).toFixed(1), " s"] }), _jsx("span", { className: "min-w-0 truncate", children: r.ok ? `by ${r.by === 'you' ? 'you' : assistant}` : r.error })] }, r.at))) }));
112
+ }
package/dist/index.d.ts CHANGED
@@ -102,10 +102,10 @@ 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
- export { type WorkspaceApp, type WorkspaceDraftProps, WorkspacePage, type WorkspacesConfig, WorkspacesProvider, WorkspacesSection, } from './workspaces.js';
108
+ export { type WorkspaceApp, type WorkspaceDraftProps, WorkspacePage, type WorkspaceRecipe, type WorkspacesConfig, WorkspacesProvider, WorkspacesSection, } from './workspaces.js';
109
109
  export { CHART_INDICATORS, type ChartIndicator, type IndicatorValues, LINE_COLOR, computeIndicators, formulaIndicator, lineColor, resolveIndicator, runFormula, } from './market/indicators.js';
110
110
  export { IndicatorPicker } from './market/indicator-picker.js';
111
111
  export { type OptionChainField, type OptionChainRow, OptionChainTable, } from './market/option-chain.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';
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, }) {
@@ -1,3 +1,4 @@
1
+ import { type FrameScript } from '@columenlabs/lumen-ui-types/frames';
1
2
  import { type CustomViewConfig } from '@columenlabs/lumen-ui-types/views';
2
3
  import { type ComponentType, type ReactNode } from 'react';
3
4
  import { type Crumb } from './page.js';
@@ -24,6 +25,20 @@ export interface WorkspacesConfig {
24
25
  defaultConfig?: (domain: string) => CustomViewConfig | null;
25
26
  /** The empty workspace's example ask, on an app: `put my holdings with their earnings dates in`. */
26
27
  example?: (app: string) => string;
28
+ /** Recipes a person picks to start a tab by hand (+ in the tab bar); none hides the +. */
29
+ recipes?: WorkspaceRecipe[];
30
+ }
31
+ /** A saved data pull a person can start a tab from: one call to one of the product's read tools. */
32
+ export interface WorkspaceRecipe {
33
+ id: string;
34
+ /** The tab's name and the picker's row: `Jobs scoring 70+`. */
35
+ label: string;
36
+ /** When to pick it, under the label. */
37
+ description?: string;
38
+ /** The picker's section: the app the rows come from. */
39
+ group?: string;
40
+ icon?: Icon;
41
+ recipe: FrameScript;
27
42
  }
28
43
  /** The product's apps and domains for its workspaces. */
29
44
  export declare const WorkspacesProvider: import("react").Provider<WorkspacesConfig>;
@@ -4,7 +4,7 @@ import { customViewConfigSchema, frameSpace, } from '@columenlabs/lumen-ui-types
4
4
  import { workspacePath, } from '@columenlabs/lumen-ui-types/workspaces';
5
5
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
6
6
  import { useNavigate, useParams, useSearch } from '@tanstack/react-router';
7
- import { ArrowRightLeftIcon, CalendarIcon, ExternalLinkIcon, HashIcon, LayersIcon, PencilIcon, SparklesIcon, TableIcon, TextCursorInputIcon, Trash2Icon, TypeIcon, } from 'lucide-react';
7
+ import { ArrowRightLeftIcon, CalendarIcon, ExternalLinkIcon, HashIcon, LayersIcon, PencilIcon, PlusIcon, RefreshCwIcon, SparklesIcon, TableIcon, TextCursorInputIcon, Trash2Icon, TypeIcon, } from 'lucide-react';
8
8
  import { createContext, useContext, useEffect, useMemo, useState, } from 'react';
9
9
  import { apiJson } from './api-fetch.js';
10
10
  import { useAssistant, useAssistantName, useAssistantPending } from './assistant-context.js';
@@ -206,9 +206,48 @@ function WorkspaceBody({ api, w, tab, draft, }) {
206
206
  }).then(() => qc.invalidateQueries({ queryKey: WORKSPACES_KEY }));
207
207
  }, { id: `ws-save:${tab.id}`, duration: 4000 });
208
208
  };
209
- const intro = (_jsx(LumenTabs, { tabs: w.tabs.map((t) => ({ value: t.id, label: t.name, icon: tabIcon(t) })), value: tab?.id, onChange: (t) => void navigate({ href: workspacePath(w.id, t), replace: true }), actions: tab && _jsx(IconButton, { icon: PencilIcon, label: "Edit tab", onClick: () => setEditingTab(true) }) }));
209
+ // + in the tab bar: a tab from a recipe, run once now; ↻ in the header runs every tab again.
210
+ const addTab = useMutation({
211
+ mutationFn: (r) => json(`${api}/workspaces/${w.id}/tabs`, {
212
+ method: 'POST',
213
+ body: JSON.stringify({ name: r.label, recipe: r.recipe }),
214
+ }),
215
+ onSuccess: ({ workspace, tab: t }) => {
216
+ put(workspace);
217
+ void qc.invalidateQueries({ queryKey: [...WORKSPACES_KEY, api, 'list'] });
218
+ void navigate({ href: workspacePath(w.id, t), replace: true });
219
+ },
220
+ onError: (e) => lumenToast.error(`Couldn’t add the tab: ${e.message}`),
221
+ });
222
+ const runAll = useMutation({
223
+ mutationFn: () => json(`${api}/workspaces/${w.id}/refresh`, {
224
+ method: 'POST',
225
+ }),
226
+ onSuccess: (r) => {
227
+ void qc.invalidateQueries({ queryKey: WORKSPACES_KEY });
228
+ const ran = `Ran ${r.ran} ${r.ran === 1 ? 'tab' : 'tabs'}: ${r.rows.toLocaleString('en-US')} rows`;
229
+ if (r.failed.length)
230
+ lumenToast.warning(`${ran}. Failed: ${r.failed.join(', ')}`);
231
+ else
232
+ lumenToast.success(ran);
233
+ },
234
+ onError: (e) => lumenToast.error(`Couldn’t run the tabs: ${e.message}`),
235
+ });
236
+ const recipes = config.recipes ?? [];
237
+ const runnable = w.tabs.some((t) => 'frame' in t.source);
238
+ const intro = (_jsx(LumenTabs, { tabs: w.tabs.map((t) => ({ value: t.id, label: t.name, icon: tabIcon(t) })), value: tab?.id, onChange: (t) => void navigate({ href: workspacePath(w.id, t), replace: true }), actions: _jsxs(_Fragment, { children: [recipes.length > 0 && (_jsx(LumenSelector, { trigger: "icon", icon: PlusIcon, label: addTab.isPending ? 'Adding a tab…' : 'New tab from a recipe', align: "end", disabled: addTab.isPending, options: recipes.map((r) => ({
239
+ value: r.id,
240
+ label: r.label,
241
+ description: r.description,
242
+ group: r.group,
243
+ icon: r.icon,
244
+ })), value: null, onChange: (id) => {
245
+ const r = recipes.find((x) => x.id === id);
246
+ if (r)
247
+ addTab.mutate(r);
248
+ } })), tab && (_jsx(IconButton, { icon: PencilIcon, label: "Edit tab", onClick: () => setEditingTab(true) }))] }) }));
210
249
  const crumbs = [{ label: 'Workspaces' }, { label: w.name }];
211
- const actions = (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: TextCursorInputIcon, label: "Rename workspace", onClick: () => setNaming('workspace') }), moveTo.length > 0 && (_jsx(LumenSelector, { trigger: "icon", icon: ArrowRightLeftIcon, label: `Move to ${noun}`, align: "end", search: false, options: moveTo.map((a) => ({ value: a.id, label: a.label, icon: a.icon })), value: null, onChange: (a) => move.mutate(a) })), _jsx(IconButton, { icon: Trash2Icon, label: "Delete workspace", className: "text-muted-foreground hover:text-destructive", onClick: () => setRemoving('workspace') })] }));
250
+ const actions = (_jsxs(_Fragment, { children: [runnable && (_jsx(IconButton, { icon: RefreshCwIcon, label: runAll.isPending ? 'Running every tab…' : 'Run every tab again', disabled: runAll.isPending, className: runAll.isPending ? '[&_svg]:animate-spin' : undefined, onClick: () => runAll.mutate() })), _jsx(IconButton, { icon: TextCursorInputIcon, label: "Rename workspace", onClick: () => setNaming('workspace') }), moveTo.length > 0 && (_jsx(LumenSelector, { trigger: "icon", icon: ArrowRightLeftIcon, label: `Move to ${noun}`, align: "end", search: false, options: moveTo.map((a) => ({ value: a.id, label: a.label, icon: a.icon })), value: null, onChange: (a) => move.mutate(a) })), _jsx(IconButton, { icon: Trash2Icon, label: "Delete workspace", className: "text-muted-foreground hover:text-destructive", onClick: () => setRemoving('workspace') })] }));
212
251
  const dialogs = (_jsxs(_Fragment, { children: [_jsx(NameDialog, { open: naming !== null, title: "Rename workspace", value: w.name, onClose: () => setNaming(null), onSave: (name) => rename.mutate(name) }), tab && (_jsx(LumenTabDrawer, { open: editingTab, onClose: () => setEditingTab(false), name: tab.name, icon: tab.config?.icon, fallback: tabIcon({ ...tab, config: null }), description: "Its filters and columns save as you change them.", maxName: 60, busy: editTab.isPending, error: editTab.error?.message, onSave: ({ name, icon }) => editTab.mutate(icon === (tab.config?.icon ?? null)
213
252
  ? { name }
214
253
  : { name, config: { ...tabConfig(tab, w, config.defaultConfig), name, icon } }), remove: {
@@ -226,7 +265,9 @@ function WorkspaceBody({ api, w, tab, draft, }) {
226
265
  title: 'No tabs yet',
227
266
  icon: LayersIcon,
228
267
  kind: 'widget',
229
- children: (_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Ask ", assistant, " for data and to open it here: \u201C", config.example?.(here) ?? 'put this month’s numbers in', " ", w.name, "\u201D."] })),
268
+ children: (_jsx("p", { className: "text-sm text-muted-foreground", children: recipes.length > 0
269
+ ? 'Pick a recipe from + above: its rows open here as a tab, and run again whenever you tap ↻.'
270
+ : `Ask ${assistant} for data and to open it here: “${config.example?.(here) ?? 'put this month’s numbers in'} ${w.name}”.` })),
230
271
  },
231
272
  ] }));
232
273
  }
@@ -290,6 +331,7 @@ assistant = 'Assistant') {
290
331
  value: (r) => r.v[i] ?? null,
291
332
  format: c.format,
292
333
  amount: c.amount,
334
+ ...(c.tags ? { tags: true } : {}),
293
335
  filterable: true,
294
336
  groupable: c.type !== 'number',
295
337
  ...(c.type === 'number' ? { aggregate: c.format === 'percent' ? 'mean' : 'sum' } : {}),
@@ -438,7 +480,7 @@ function FrameTab({ api, tab, meta, crumbs, actions, intro, onChange, }) {
438
480
  day: 'numeric',
439
481
  hour: 'numeric',
440
482
  minute: '2-digit',
441
- }) }), failed ? (_jsx(StatusItem, { label: "Refresh failed", className: "min-w-0", value: _jsx("span", { className: "inline-block max-w-72 truncate align-bottom", title: failed, children: failed }), ok: false })) : null] }) }, `frame:${tab.id}`), assistantMade && (_jsx(FrameScriptDialog, { api: api, frame: current.id, name: current.name, open: scriptOpen, onOpenChange: setScriptOpen }))] }));
483
+ }) }), failed ? (_jsx(StatusItem, { label: "Refresh failed", className: "min-w-0", value: _jsx("span", { className: "inline-block max-w-72 truncate align-bottom", title: failed, children: failed }), ok: false })) : null] }) }, `frame:${tab.id}`), assistantMade && (_jsx(FrameScriptDialog, { api: api, frame: current.id, name: current.name, runs: current.runs, open: scriptOpen, onOpenChange: setScriptOpen }))] }));
442
484
  }
443
485
  /** A tab's icon: the person's pick, else the assistant's mark on a frame, else a table. */
444
486
  function tabIcon(t) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.189",
3
+ "version": "0.1.191",
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.189",
52
+ "@columenlabs/lumen-ui-types": "0.1.191",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",