@drewswiredin/backstage-plugin-assistants 0.5.4 → 0.6.0

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.
Files changed (28) hide show
  1. package/dist/AssistantsNavIcon.esm.js +35 -0
  2. package/dist/AssistantsNavIcon.esm.js.map +1 -0
  3. package/dist/collapsible/AssistantsList.esm.js +12 -5
  4. package/dist/collapsible/AssistantsList.esm.js.map +1 -1
  5. package/dist/collapsible/CollapsiblePage.esm.js +269 -342
  6. package/dist/collapsible/CollapsiblePage.esm.js.map +1 -1
  7. package/dist/collapsible/ConversationsPanel.esm.js +13 -7
  8. package/dist/collapsible/ConversationsPanel.esm.js.map +1 -1
  9. package/dist/collapsible/SidePane.esm.js +3 -2
  10. package/dist/collapsible/SidePane.esm.js.map +1 -1
  11. package/dist/collapsible/surface/ConversationSurface.esm.js +2 -12
  12. package/dist/collapsible/surface/ConversationSurface.esm.js.map +1 -1
  13. package/dist/collapsible/surface/parts.esm.js +35 -8
  14. package/dist/collapsible/surface/parts.esm.js.map +1 -1
  15. package/dist/collapsible/threadListAdapter.esm.js +153 -0
  16. package/dist/collapsible/threadListAdapter.esm.js.map +1 -0
  17. package/dist/collapsible/useAssistantRuntime.esm.js +66 -0
  18. package/dist/collapsible/useAssistantRuntime.esm.js.map +1 -0
  19. package/dist/collapsible/useThreadStatus.esm.js +108 -0
  20. package/dist/collapsible/useThreadStatus.esm.js.map +1 -0
  21. package/dist/index.d.ts +12 -1
  22. package/dist/index.esm.js +1 -0
  23. package/dist/index.esm.js.map +1 -1
  24. package/package.json +4 -2
  25. package/dist/collapsible/unreadStore.esm.js +0 -79
  26. package/dist/collapsible/unreadStore.esm.js.map +0 -1
  27. package/dist/collapsible/useConversations.esm.js +0 -171
  28. package/dist/collapsible/useConversations.esm.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import './surface/styles/assistant-ui.css.esm.js';
3
3
  import './surface/styles/assistant-ui-markdown.css.esm.js';
4
- import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
4
+ import { useRef, useMemo, useState, useEffect, useCallback } from 'react';
5
5
  import { useSearchParams } from 'react-router-dom';
6
6
  import useAsync from 'react-use/lib/useAsync';
7
7
  import { useApi } from '@backstage/core-plugin-api';
@@ -13,16 +13,15 @@ import ChatBubbleOutlineIcon from '@material-ui/icons/ChatBubbleOutline';
13
13
  import CheckIcon from '@material-ui/icons/Check';
14
14
  import ChevronRightIcon from '@material-ui/icons/ChevronRight';
15
15
  import StarIcon from '@material-ui/icons/Star';
16
- import { AssistantRuntimeProvider, useThread } from '@assistant-ui/react';
17
- import { useChatRuntime } from '@assistant-ui/react-ai-sdk';
18
- import { DefaultChatTransport } from 'ai';
16
+ import { useRemoteThreadListRuntime, AssistantRuntimeProvider, useAssistantRuntime } from '@assistant-ui/react';
19
17
  import { assistantsApiRef } from '../api/AssistantsApi.esm.js';
20
18
  import { ConversationSurface } from './surface/ConversationSurface.esm.js';
21
19
  import { AssistantAvatar } from './surface/AssistantAvatar.esm.js';
22
20
  import { SidePane } from './SidePane.esm.js';
23
21
  import { FullHeightRegion } from './FullHeightRegion.esm.js';
24
- import { useConversations, persistConversationMessages } from './useConversations.esm.js';
25
- import { useUnreadVersion, markUnread, clearUnread, hasUnread, isConversationUnread } from './unreadStore.esm.js';
22
+ import { createThreadListAdapter, patchThread } from './threadListAdapter.esm.js';
23
+ import { makeRuntimeHook } from './useAssistantRuntime.esm.js';
24
+ import { useThreadStatus } from './useThreadStatus.esm.js';
26
25
 
27
26
  const SIDEPANE_COLLAPSED_KEY = "ai-chat-sidepane-collapsed";
28
27
  function loadSidePaneCollapsed() {
@@ -33,8 +32,6 @@ function loadSidePaneCollapsed() {
33
32
  }
34
33
  }
35
34
  const useStyles = makeStyles((theme) => ({
36
- // Flex-column fill inside the measured FullHeightRegion (mirrors the native
37
- // page): lets the shell own the remaining height without a nested <Page>.
38
35
  content: {
39
36
  flex: 1,
40
37
  minHeight: 0,
@@ -47,10 +44,6 @@ const useStyles = makeStyles((theme) => ({
47
44
  minHeight: 0,
48
45
  overflow: "hidden",
49
46
  backgroundColor: theme.palette.background.default,
50
- // No top gutter so the card tucks flush under the page header (reclaims the
51
- // gap there); 8px right/bottom keep it enclosed on those sides, 0 left since
52
- // it abuts the sidebar. Composer breathing room lives on the Thread's own
53
- // footer (see ConversationSurface) — same bg, no seam.
54
47
  paddingTop: 0,
55
48
  paddingRight: theme.spacing(1),
56
49
  paddingBottom: theme.spacing(1),
@@ -137,7 +130,6 @@ const useStyles = makeStyles((theme) => ({
137
130
  boxShadow: theme.shadows[1],
138
131
  overflow: "hidden"
139
132
  },
140
- // Slim, solid header.
141
133
  threadHeader: {
142
134
  display: "flex",
143
135
  alignItems: "center",
@@ -194,11 +186,6 @@ const useStyles = makeStyles((theme) => ({
194
186
  right: theme.spacing(0.5)
195
187
  }
196
188
  },
197
- // Trigger label: vendor in muted text, model name emphasized.
198
- modelTriggerVendor: {
199
- color: theme.palette.text.hint,
200
- marginRight: theme.spacing(0.5)
201
- },
202
189
  modelGroupLabel: {
203
190
  lineHeight: 2,
204
191
  fontSize: theme.typography.caption.fontSize,
@@ -226,25 +213,17 @@ const useStyles = makeStyles((theme) => ({
226
213
  minHeight: 0,
227
214
  display: "flex",
228
215
  flexDirection: "column"
229
- // NOTE: no paddingBottom here — the Thread fills this box with its own
230
- // --aui-background; padding would expose the card's paper bg and create a
231
- // two-tone seam. Composer breathing room lives on .aui-thread-viewport-footer
232
- // (same bg) in ConversationSurface.
233
216
  },
234
- // Background thread: kept mounted (stream alive) but out of view/layout.
235
- threadPaneHidden: {
236
- display: "none"
217
+ // The "generating" indicator: a pulsing dot, distinct from the solid unread dot.
218
+ "@keyframes auiPulse": {
219
+ "0%": { transform: "scale(1)", opacity: 1 },
220
+ "50%": { transform: "scale(1.5)", opacity: 0.45 },
221
+ "100%": { transform: "scale(1)", opacity: 1 }
222
+ },
223
+ pulseDot: {
224
+ animation: "$auiPulse 1.2s ease-in-out infinite"
237
225
  }
238
226
  }));
239
- function RunningReporter({
240
- onRunningChange
241
- }) {
242
- const isRunning = useThread((t) => t.isRunning);
243
- useEffect(() => {
244
- onRunningChange?.(isRunning);
245
- }, [isRunning, onRunningChange]);
246
- return null;
247
- }
248
227
  function splitModel(id, pool) {
249
228
  const opt = pool.find((m) => m.id === id);
250
229
  const raw = opt?.model ?? id;
@@ -257,262 +236,245 @@ function splitModel(id, pool) {
257
236
  function modelLabel(id, pool) {
258
237
  return splitModel(id, pool).name;
259
238
  }
260
- function ChatThread({
261
- baseUrl,
262
- authFetch,
263
- assistantId,
264
- modelId,
265
- title,
266
- assistantName,
267
- initialMessages,
268
- onFinish,
269
- composerPlaceholder,
270
- suggestions,
271
- assistantColor,
272
- headerRight,
273
- hidden,
274
- onRunningChange
239
+ function CollapsiblePage() {
240
+ const api = useApi(assistantsApiRef);
241
+ const [searchParams] = useSearchParams();
242
+ const requestedAssistant = searchParams.get("assistant");
243
+ const status = useAsync(() => api.getStatus(), [api]);
244
+ if (status.loading) {
245
+ return /* @__PURE__ */ jsx(Progress, {});
246
+ }
247
+ if (status.error) {
248
+ return /* @__PURE__ */ jsx(ResponseErrorPanel, { error: status.error });
249
+ }
250
+ if (!status.value) {
251
+ return null;
252
+ }
253
+ const assistants = status.value.assistants;
254
+ if (assistants.length === 0) {
255
+ return /* @__PURE__ */ jsx(
256
+ ResponseErrorPanel,
257
+ {
258
+ error: new Error("No assistants are available to you.")
259
+ }
260
+ );
261
+ }
262
+ const assistant = assistants.find((a) => a.id === requestedAssistant) ?? assistants[0];
263
+ return /* @__PURE__ */ jsx(CollapsibleChat, { status: status.value, assistant }, assistant.id);
264
+ }
265
+ function CollapsibleChat({
266
+ status,
267
+ assistant
275
268
  }) {
276
- const classes = useStyles();
277
- const selectionRef = useRef({ assistantId, modelId });
278
- selectionRef.current = { assistantId, modelId };
279
- const transport = useMemo(
280
- () => new DefaultChatTransport({
281
- api: `${baseUrl}/chat`,
282
- fetch: authFetch,
283
- body: () => ({
284
- assistantId: selectionRef.current.assistantId,
285
- modelId: selectionRef.current.modelId
286
- })
287
- }),
288
- [authFetch, baseUrl]
289
- );
290
- const onFinishRef = useRef(onFinish);
291
- onFinishRef.current = onFinish;
292
- const stableOnFinish = useCallback(({ messages }) => {
293
- onFinishRef.current?.(messages);
294
- }, []);
295
- const runtime = useChatRuntime({
296
- transport,
297
- messages: initialMessages,
298
- onFinish: stableOnFinish
299
- });
300
- return /* @__PURE__ */ jsxs(AssistantRuntimeProvider, { runtime, children: [
301
- /* @__PURE__ */ jsx(RunningReporter, { onRunningChange }),
302
- /* @__PURE__ */ jsxs(
303
- "main",
269
+ const api = useApi(assistantsApiRef);
270
+ const baseUrl = useAsync(() => api.getBaseUrl(), [api]);
271
+ if (baseUrl.loading) {
272
+ return /* @__PURE__ */ jsx(Progress, {});
273
+ }
274
+ if (baseUrl.error || !baseUrl.value) {
275
+ return /* @__PURE__ */ jsx(
276
+ ResponseErrorPanel,
304
277
  {
305
- className: hidden ? `${classes.threadPane} ${classes.threadPaneHidden}` : classes.threadPane,
306
- "aria-label": "AI chat thread",
307
- "aria-hidden": hidden,
308
- children: [
309
- /* @__PURE__ */ jsxs("div", { className: classes.threadHeader, children: [
310
- /* @__PURE__ */ jsxs("div", { className: classes.threadIdentity, children: [
311
- /* @__PURE__ */ jsx(AssistantAvatar, { color: assistantColor, size: 22 }),
312
- /* @__PURE__ */ jsx(Typography, { variant: "subtitle2", className: classes.assistantName, children: assistantName }),
313
- title && /* @__PURE__ */ jsxs(
314
- Typography,
315
- {
316
- variant: "body2",
317
- className: classes.threadTitle,
318
- title,
319
- children: [
320
- "\xB7 ",
321
- title
322
- ]
323
- }
324
- )
325
- ] }),
326
- headerRight
327
- ] }),
328
- /* @__PURE__ */ jsx("div", { className: classes.threadBody, children: /* @__PURE__ */ jsx(
329
- ConversationSurface,
330
- {
331
- composerPlaceholder,
332
- suggestions,
333
- assistantColor
334
- }
335
- ) })
336
- ]
278
+ error: baseUrl.error ?? new Error("Failed to resolve backend URL")
337
279
  }
338
- )
339
- ] });
280
+ );
281
+ }
282
+ return /* @__PURE__ */ jsx(
283
+ ChatRuntime,
284
+ {
285
+ status,
286
+ assistant,
287
+ api,
288
+ baseUrl: baseUrl.value
289
+ }
290
+ );
340
291
  }
341
- function useLiveThreads(active) {
342
- const [mounted, setMounted] = useState([]);
343
- const [running, setRunning] = useState(
344
- () => /* @__PURE__ */ new Set()
292
+ function ChatRuntime({
293
+ status,
294
+ assistant,
295
+ api,
296
+ baseUrl
297
+ }) {
298
+ const defaultModel = assistant.defaultModel ?? status.defaultModel;
299
+ const modelIdRef = useRef(defaultModel);
300
+ const adapter = useMemo(
301
+ () => createThreadListAdapter(api, assistant.id),
302
+ [api, assistant.id]
345
303
  );
346
- const activeRef = useRef(active);
347
- activeRef.current = active;
348
- useEffect(() => {
349
- const a = activeRef.current;
350
- setMounted((prev) => {
351
- const byId = new Map(prev.map((d) => [d.convId, d]));
352
- if (a && !byId.has(a.convId)) {
353
- byId.set(a.convId, a);
354
- }
355
- const keep = new Set(running);
356
- if (a) {
357
- keep.add(a.convId);
358
- }
359
- const next = [];
360
- for (const d of prev) {
361
- if (keep.has(d.convId)) {
362
- next.push(byId.get(d.convId) ?? d);
363
- keep.delete(d.convId);
364
- }
365
- }
366
- for (const id of keep) {
367
- const d = byId.get(id);
368
- if (d) {
369
- next.push(d);
370
- }
371
- }
372
- const unchanged = next.length === prev.length && next.every((d, i) => d.convId === prev[i].convId);
373
- return unchanged ? prev : next;
374
- });
375
- }, [active?.convId, running]);
376
- const setThreadRunning = useCallback((convId, isRunning) => {
377
- setRunning((prev) => {
378
- const has = prev.has(convId);
379
- if (isRunning === has) {
380
- return prev;
381
- }
382
- const next = new Set(prev);
383
- if (isRunning) {
384
- next.add(convId);
385
- } else {
386
- next.delete(convId);
387
- }
388
- return next;
389
- });
390
- }, []);
391
- const threads = active && !mounted.some((d) => d.convId === active.convId) ? [...mounted, active] : mounted;
392
- return { threads, setThreadRunning };
304
+ const runtimeHook = useMemo(
305
+ () => makeRuntimeHook({ api, baseUrl, assistantId: assistant.id, modelIdRef }),
306
+ [api, baseUrl, assistant.id]
307
+ );
308
+ const runtime = useRemoteThreadListRuntime({ adapter, runtimeHook });
309
+ return /* @__PURE__ */ jsx(AssistantRuntimeProvider, { runtime, children: /* @__PURE__ */ jsx(
310
+ ChatChrome,
311
+ {
312
+ status,
313
+ assistant,
314
+ api,
315
+ modelIdRef,
316
+ defaultModel
317
+ }
318
+ ) });
393
319
  }
394
- function CollapsibleChat({ status, assistant }) {
320
+ function ChatChrome({
321
+ status,
322
+ assistant,
323
+ api,
324
+ modelIdRef,
325
+ defaultModel
326
+ }) {
395
327
  const classes = useStyles();
396
- const api = useApi(assistantsApiRef);
397
- useUnreadVersion();
328
+ const runtime = useAssistantRuntime();
398
329
  const [, setSearchParams] = useSearchParams();
399
- const handleSelectAssistant = useCallback(
400
- (id) => {
401
- if (id !== assistant.id) {
402
- setSearchParams({ assistant: id });
403
- }
404
- },
405
- [assistant.id, setSearchParams]
330
+ const [threadList, setThreadList] = useState(
331
+ () => runtime.threads.getState()
406
332
  );
407
- const baseUrl = useAsync(() => api.getBaseUrl(), [api]);
333
+ useEffect(() => {
334
+ setThreadList(runtime.threads.getState());
335
+ return runtime.threads.subscribe(
336
+ () => setThreadList(runtime.threads.getState())
337
+ );
338
+ }, [runtime]);
339
+ const activeId = threadList.mainThreadId;
340
+ const activeItem = threadList.threadItems[activeId];
341
+ const activeRemoteId = activeItem?.remoteId;
342
+ const activeTitle = activeItem?.title ?? "";
343
+ const { statusOf, agentStatus, markRead, finishedTick } = useThreadStatus(
344
+ api,
345
+ activeRemoteId
346
+ );
347
+ const didSelectInitial = useRef(false);
348
+ useEffect(() => {
349
+ if (didSelectInitial.current || threadList.isLoading) return;
350
+ didSelectInitial.current = true;
351
+ if (activeId !== threadList.newThreadId) return;
352
+ const mostRecent = [...threadList.threadIds].map((id) => ({
353
+ id,
354
+ updatedAt: threadList.threadItems[id]?.custom?.updatedAt ?? ""
355
+ })).sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1)[0]?.id;
356
+ if (mostRecent) {
357
+ void runtime.threads.switchToThread(mostRecent);
358
+ }
359
+ }, [threadList.isLoading]);
360
+ const conversations = useMemo(
361
+ () => threadList.threadIds.map((id) => {
362
+ const item = threadList.threadItems[id];
363
+ const custom = item?.custom;
364
+ const st = id === activeId ? "read" : statusOf(item?.remoteId);
365
+ return {
366
+ id,
367
+ remoteId: item?.remoteId,
368
+ title: item?.title ?? "New Chat",
369
+ pinned: custom?.pinned ?? false,
370
+ unread: st === "unread",
371
+ generating: st === "working"
372
+ };
373
+ }),
374
+ [threadList, activeId, statusOf]
375
+ );
376
+ useEffect(() => {
377
+ if (finishedTick > 0) {
378
+ void runtime.threads.reload();
379
+ }
380
+ }, [finishedTick, runtime]);
408
381
  const allowedModels = useMemo(
409
382
  () => assistant.models ?? status.models.map((m) => m.id),
410
383
  [assistant.models, status.models]
411
384
  );
412
- const defaultModel = assistant.defaultModel ?? status.defaultModel;
413
385
  const modelGroups = useMemo(() => {
414
386
  const byVendor = /* @__PURE__ */ new Map();
415
387
  for (const id of allowedModels) {
416
388
  const { vendor } = splitModel(id, status.models);
417
389
  const list = byVendor.get(vendor);
418
- if (list) {
419
- list.push(id);
420
- } else {
421
- byVendor.set(vendor, [id]);
422
- }
390
+ if (list) list.push(id);
391
+ else byVendor.set(vendor, [id]);
423
392
  }
424
393
  return [...byVendor.entries()];
425
394
  }, [allowedModels, status.models]);
426
- const convState = useConversations(assistant.id);
427
395
  const resolveModel = useCallback(
428
396
  (stored) => stored && allowedModels.includes(stored) ? stored : defaultModel,
429
397
  [allowedModels, defaultModel]
430
398
  );
431
399
  const [modelId, setModelId] = useState(
432
- () => resolveModel(convState.activeConversation?.model)
400
+ () => resolveModel(activeItem?.custom?.model)
433
401
  );
434
402
  useEffect(() => {
435
- setModelId(resolveModel(convState.activeConversation?.model));
436
- }, [convState.activeId]);
437
- const handleModelChange = useCallback(
438
- (next) => {
439
- setModelId(next);
440
- if (convState.activeId) {
441
- convState.setConversationModel(convState.activeId, next);
442
- }
443
- },
444
- [convState]
445
- );
446
- const [sidePaneCollapsed, setSidePaneCollapsed] = useState(
447
- loadSidePaneCollapsed
448
- );
403
+ const stored = threadList.threadItems[activeId]?.custom?.model;
404
+ const next = resolveModel(stored);
405
+ setModelId(next);
406
+ modelIdRef.current = next;
407
+ }, [activeId]);
408
+ useEffect(() => {
409
+ if (activeRemoteId) markRead(activeRemoteId);
410
+ }, [activeRemoteId]);
411
+ const [sidePaneCollapsed, setSidePaneCollapsed] = useState(loadSidePaneCollapsed);
449
412
  useEffect(() => {
450
413
  try {
451
414
  localStorage.setItem(SIDEPANE_COLLAPSED_KEY, String(sidePaneCollapsed));
452
415
  } catch {
453
416
  }
454
417
  }, [sidePaneCollapsed]);
418
+ const handleSelectAssistant = useCallback(
419
+ (id) => {
420
+ if (id !== assistant.id) setSearchParams({ assistant: id });
421
+ },
422
+ [assistant.id, setSearchParams]
423
+ );
455
424
  const handleNew = useCallback(() => {
456
- convState.createConversation();
457
- }, [convState]);
458
- const handleThreadFinish = useCallback(
459
- (descriptor, messages) => {
460
- const sameAgent = descriptor.agentId === assistant.id;
461
- const viewing = sameAgent && descriptor.convId === convState.activeId;
462
- if (sameAgent) {
463
- convState.updateMessages(descriptor.convId, messages);
464
- } else {
465
- persistConversationMessages(
466
- descriptor.agentId,
467
- descriptor.convId,
468
- messages
469
- );
425
+ void (async () => {
426
+ await runtime.threads.switchToNewThread();
427
+ try {
428
+ await runtime.threads.mainItem.initialize();
429
+ await runtime.threads.reload();
430
+ } catch {
470
431
  }
471
- if (!viewing) {
472
- markUnread(descriptor.agentId, descriptor.convId);
432
+ })();
433
+ }, [runtime]);
434
+ const handleSelect = useCallback(
435
+ (id) => {
436
+ if (id) void runtime.threads.switchToThread(id);
437
+ },
438
+ [runtime]
439
+ );
440
+ const handleRename = useCallback(
441
+ (id, title) => {
442
+ void (async () => {
443
+ await runtime.threads.getItemById(id).rename(title);
444
+ await runtime.threads.reload();
445
+ })();
446
+ },
447
+ [runtime]
448
+ );
449
+ const handleDelete = useCallback(
450
+ (id) => {
451
+ void runtime.threads.getItemById(id).delete();
452
+ },
453
+ [runtime]
454
+ );
455
+ const handlePin = useCallback(
456
+ async (id) => {
457
+ const item = threadList.threadItems[id];
458
+ if (!item?.remoteId) return;
459
+ const pinned = item.custom?.pinned ?? false;
460
+ try {
461
+ await patchThread(api, item.remoteId, { pinned: !pinned });
462
+ await runtime.threads.reload();
463
+ } catch {
473
464
  }
474
- if (sameAgent) {
475
- const conv = convState.conversations.find(
476
- (c) => c.id === descriptor.convId
477
- );
478
- const shouldTitle = conv?.title === "New Chat" && messages.some((m) => m.role === "user") && messages.some((m) => m.role === "assistant");
479
- if (shouldTitle) {
480
- api.getTitle({
481
- assistantId: descriptor.agentId,
482
- modelId: descriptor.modelId,
483
- messages
484
- }).then((generated) => {
485
- if (generated && generated !== "New Chat") {
486
- convState.renameConversation(descriptor.convId, generated);
487
- }
488
- }).catch(() => {
489
- });
490
- }
465
+ },
466
+ [api, runtime, threadList]
467
+ );
468
+ const handleModelChange = useCallback(
469
+ (next) => {
470
+ setModelId(next);
471
+ modelIdRef.current = next;
472
+ if (activeRemoteId) {
473
+ void patchThread(api, activeRemoteId, { model: next });
491
474
  }
492
475
  },
493
- [api, assistant.id, convState]
476
+ [api, activeRemoteId, modelIdRef]
494
477
  );
495
- useEffect(() => {
496
- if (!convState.activeId) {
497
- convState.createConversation();
498
- }
499
- }, []);
500
- useEffect(() => {
501
- if (convState.activeId) {
502
- clearUnread(assistant.id, convState.activeId);
503
- }
504
- }, [assistant.id, convState.activeId]);
505
- const activeDescriptor = convState.activeId ? {
506
- convId: convState.activeId,
507
- agentId: assistant.id,
508
- assistantTitle: assistant.title,
509
- assistantColor: assistant.color,
510
- modelId,
511
- initialMessages: convState.activeConversation?.messages,
512
- composerPlaceholder: assistant.ui?.composer?.placeholder,
513
- suggestions: assistant.ui?.suggestions
514
- } : null;
515
- const { threads, setThreadRunning } = useLiveThreads(activeDescriptor);
516
478
  const modelPicker = /* @__PURE__ */ jsx(FormControl, { children: /* @__PURE__ */ jsx(
517
479
  Select,
518
480
  {
@@ -551,17 +513,6 @@ function CollapsibleChat({ status, assistant }) {
551
513
  ])
552
514
  }
553
515
  ) });
554
- if (baseUrl.loading) {
555
- return /* @__PURE__ */ jsx(Progress, {});
556
- }
557
- if (baseUrl.error || !baseUrl.value) {
558
- return /* @__PURE__ */ jsx(
559
- ResponseErrorPanel,
560
- {
561
- error: baseUrl.error ?? new Error("Failed to resolve backend URL")
562
- }
563
- );
564
- }
565
516
  return /* @__PURE__ */ jsx(FullHeightRegion, { children: /* @__PURE__ */ jsx(Content, { noPadding: true, className: classes.content, children: /* @__PURE__ */ jsxs("div", { className: classes.shell, children: [
566
517
  sidePaneCollapsed ? /* @__PURE__ */ jsxs(
567
518
  "aside",
@@ -579,32 +530,26 @@ function CollapsibleChat({ status, assistant }) {
579
530
  children: /* @__PURE__ */ jsx(ChevronRightIcon, { fontSize: "small" })
580
531
  }
581
532
  ) }) }),
582
- /* @__PURE__ */ jsx(
583
- "nav",
533
+ /* @__PURE__ */ jsx("nav", { className: classes.sidePaneRailAssistants, "aria-label": "Assistants", children: status.assistants.map((a) => /* @__PURE__ */ jsx(Tooltip, { title: a.title, placement: "right", children: /* @__PURE__ */ jsx(
534
+ IconButton,
584
535
  {
585
- className: classes.sidePaneRailAssistants,
586
- "aria-label": "Assistants",
587
- children: status.assistants.map((a) => /* @__PURE__ */ jsx(Tooltip, { title: a.title, placement: "right", children: /* @__PURE__ */ jsx(
588
- IconButton,
536
+ size: "small",
537
+ className: `${classes.sidePaneRailButton} ${a.id === assistant.id ? classes.sidePaneRailButtonActive : ""}`,
538
+ "aria-label": a.title,
539
+ onClick: () => handleSelectAssistant(a.id),
540
+ children: /* @__PURE__ */ jsx(
541
+ Badge,
589
542
  {
590
- size: "small",
591
- className: `${classes.sidePaneRailButton} ${a.id === assistant.id ? classes.sidePaneRailButtonActive : ""}`,
592
- "aria-label": a.title,
593
- onClick: () => handleSelectAssistant(a.id),
594
- children: /* @__PURE__ */ jsx(
595
- Badge,
596
- {
597
- color: "error",
598
- variant: "dot",
599
- overlap: "circular",
600
- invisible: !hasUnread(a.id),
601
- children: /* @__PURE__ */ jsx(AssistantAvatar, { color: a.color, size: 24 })
602
- }
603
- )
543
+ color: agentStatus(a.id) === "working" ? "primary" : "error",
544
+ variant: "dot",
545
+ overlap: "circular",
546
+ invisible: agentStatus(a.id) === "read",
547
+ classes: agentStatus(a.id) === "working" ? { dot: classes.pulseDot } : void 0,
548
+ children: /* @__PURE__ */ jsx(AssistantAvatar, { color: a.color, size: 24 })
604
549
  }
605
- ) }, a.id))
550
+ )
606
551
  }
607
- ),
552
+ ) }, a.id)) }),
608
553
  /* @__PURE__ */ jsx("div", { className: classes.sidePaneRailDivider }),
609
554
  /* @__PURE__ */ jsx("div", { className: classes.sidePaneRailControls, children: /* @__PURE__ */ jsx(Tooltip, { title: "New Chat", placement: "right", children: /* @__PURE__ */ jsx(
610
555
  IconButton,
@@ -621,7 +566,7 @@ function CollapsibleChat({ status, assistant }) {
621
566
  {
622
567
  className: classes.sidePaneRailChats,
623
568
  "aria-label": "AI chat conversations",
624
- children: convState.conversations.map((conversation) => /* @__PURE__ */ jsx(
569
+ children: conversations.map((conversation) => /* @__PURE__ */ jsx(
625
570
  Tooltip,
626
571
  {
627
572
  title: conversation.title,
@@ -630,16 +575,17 @@ function CollapsibleChat({ status, assistant }) {
630
575
  IconButton,
631
576
  {
632
577
  size: "small",
633
- className: `${classes.sidePaneRailButton} ${conversation.id === convState.activeId ? classes.sidePaneRailButtonActive : ""}`,
578
+ className: `${classes.sidePaneRailButton} ${conversation.id === activeId ? classes.sidePaneRailButtonActive : ""}`,
634
579
  "aria-label": conversation.title,
635
- onClick: () => convState.selectConversation(conversation.id),
580
+ onClick: () => handleSelect(conversation.id),
636
581
  children: /* @__PURE__ */ jsx(
637
582
  Badge,
638
583
  {
639
- color: "error",
584
+ color: conversation.generating ? "primary" : "error",
640
585
  variant: "dot",
641
586
  overlap: "circular",
642
- invisible: conversation.id === convState.activeId || !isConversationUnread(assistant.id, conversation.id),
587
+ invisible: !conversation.generating && !conversation.unread,
588
+ classes: conversation.generating ? { dot: classes.pulseDot } : void 0,
643
589
  children: /* @__PURE__ */ jsx(ChatBubbleOutlineIcon, { fontSize: "small" })
644
590
  }
645
591
  )
@@ -658,67 +604,48 @@ function CollapsibleChat({ status, assistant }) {
658
604
  assistants: status.assistants,
659
605
  activeAssistantId: assistant.id,
660
606
  onSelectAssistant: handleSelectAssistant,
661
- conversations: convState.conversations,
662
- activeId: convState.activeId,
607
+ agentStatus,
608
+ conversations,
609
+ activeId,
663
610
  onNew: handleNew,
664
- onSelect: convState.selectConversation,
665
- onRename: convState.renameConversation,
666
- onPin: convState.pinConversation,
667
- onDelete: convState.deleteConversation,
611
+ onSelect: handleSelect,
612
+ onRename: handleRename,
613
+ onPin: handlePin,
614
+ onDelete: handleDelete,
668
615
  onCollapse: () => setSidePaneCollapsed(true)
669
616
  }
670
617
  ) }),
671
- threads.map((d) => {
672
- const isActive = d.agentId === assistant.id && d.convId === convState.activeId;
673
- return /* @__PURE__ */ jsx(
674
- ChatThread,
618
+ /* @__PURE__ */ jsxs("main", { className: classes.threadPane, "aria-label": "AI chat thread", children: [
619
+ /* @__PURE__ */ jsxs("div", { className: classes.threadHeader, children: [
620
+ /* @__PURE__ */ jsxs("div", { className: classes.threadIdentity, children: [
621
+ /* @__PURE__ */ jsx(AssistantAvatar, { color: assistant.color, size: 22 }),
622
+ /* @__PURE__ */ jsx(Typography, { variant: "subtitle2", className: classes.assistantName, children: assistant.title }),
623
+ activeTitle && /* @__PURE__ */ jsxs(
624
+ Typography,
625
+ {
626
+ variant: "body2",
627
+ className: classes.threadTitle,
628
+ title: activeTitle,
629
+ children: [
630
+ "\xB7 ",
631
+ activeTitle
632
+ ]
633
+ }
634
+ )
635
+ ] }),
636
+ modelPicker
637
+ ] }),
638
+ /* @__PURE__ */ jsx("div", { className: classes.threadBody, children: /* @__PURE__ */ jsx(
639
+ ConversationSurface,
675
640
  {
676
- hidden: !isActive,
677
- baseUrl: baseUrl.value,
678
- authFetch: api.fetch,
679
- assistantId: d.agentId,
680
- modelId: isActive ? modelId : d.modelId,
681
- title: isActive ? convState.activeConversation?.title ?? "" : "",
682
- assistantName: isActive ? assistant.title : d.assistantTitle,
683
- initialMessages: d.initialMessages,
684
- onFinish: (messages) => handleThreadFinish(d, messages),
685
- onRunningChange: (running) => setThreadRunning(d.convId, running),
686
- composerPlaceholder: d.composerPlaceholder,
687
- suggestions: d.suggestions,
688
- assistantColor: isActive ? assistant.color : d.assistantColor,
689
- headerRight: isActive ? modelPicker : void 0
690
- },
691
- d.convId
692
- );
693
- })
641
+ composerPlaceholder: assistant.ui?.composer?.placeholder,
642
+ suggestions: assistant.ui?.suggestions,
643
+ assistantColor: assistant.color
644
+ }
645
+ ) })
646
+ ] })
694
647
  ] }) }) });
695
648
  }
696
- function CollapsiblePage() {
697
- const api = useApi(assistantsApiRef);
698
- const [searchParams] = useSearchParams();
699
- const requestedAssistant = searchParams.get("assistant");
700
- const status = useAsync(() => api.getStatus(), [api]);
701
- if (status.loading) {
702
- return /* @__PURE__ */ jsx(Progress, {});
703
- }
704
- if (status.error) {
705
- return /* @__PURE__ */ jsx(ResponseErrorPanel, { error: status.error });
706
- }
707
- if (!status.value) {
708
- return null;
709
- }
710
- const assistants = status.value.assistants;
711
- if (assistants.length === 0) {
712
- return /* @__PURE__ */ jsx(
713
- ResponseErrorPanel,
714
- {
715
- error: new Error("No assistants are available to you.")
716
- }
717
- );
718
- }
719
- const assistant = assistants.find((a) => a.id === requestedAssistant) ?? assistants[0];
720
- return /* @__PURE__ */ jsx(CollapsibleChat, { status: status.value, assistant });
721
- }
722
649
 
723
650
  export { CollapsiblePage };
724
651
  //# sourceMappingURL=CollapsiblePage.esm.js.map