@drewswiredin/backstage-plugin-assistants 0.6.1 → 0.7.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.
@@ -1,34 +1,14 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { Badge } from '@material-ui/core';
3
- import { makeStyles } from '@material-ui/core/styles';
4
2
  import { RiRobot2Line } from '@remixicon/react';
5
3
  import { useApi } from '@backstage/core-plugin-api';
6
4
  import { assistantsApiRef } from './api/AssistantsApi.esm.js';
7
5
  import { useThreadStatus } from './collapsible/useThreadStatus.esm.js';
6
+ import { StatusDot } from './collapsible/StatusDot.esm.js';
8
7
 
9
- const useStyles = makeStyles({
10
- "@keyframes auiNavPulse": {
11
- "0%": { transform: "scale(1)", opacity: 1 },
12
- "50%": { transform: "scale(1.5)", opacity: 0.45 },
13
- "100%": { transform: "scale(1)", opacity: 1 }
14
- },
15
- pulseDot: { animation: "$auiNavPulse 1.2s ease-in-out infinite" }
16
- });
17
8
  function AssistantsNavIcon() {
18
- const classes = useStyles();
19
9
  const api = useApi(assistantsApiRef);
20
10
  const { overallStatus } = useThreadStatus(api, void 0);
21
- return /* @__PURE__ */ jsx(
22
- Badge,
23
- {
24
- color: overallStatus === "working" ? "primary" : "error",
25
- variant: "dot",
26
- overlap: "circular",
27
- invisible: overallStatus === "read",
28
- classes: overallStatus === "working" ? { dot: classes.pulseDot } : void 0,
29
- children: /* @__PURE__ */ jsx(RiRobot2Line, {})
30
- }
31
- );
11
+ return /* @__PURE__ */ jsx(StatusDot, { status: overallStatus, children: /* @__PURE__ */ jsx(RiRobot2Line, {}) });
32
12
  }
33
13
 
34
14
  export { AssistantsNavIcon };
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantsNavIcon.esm.js","sources":["../src/AssistantsNavIcon.tsx"],"sourcesContent":["import { Badge } from '@material-ui/core';\nimport { makeStyles } from '@material-ui/core/styles';\nimport { RiRobot2Line } from '@remixicon/react';\nimport { useApi } from '@backstage/core-plugin-api';\nimport { assistantsApiRef } from './api';\nimport { useThreadStatus } from './collapsible/useThreadStatus';\n\nconst useStyles = makeStyles({\n '@keyframes auiNavPulse': {\n '0%': { transform: 'scale(1)', opacity: 1 },\n '50%': { transform: 'scale(1.5)', opacity: 0.45 },\n '100%': { transform: 'scale(1)', opacity: 1 },\n },\n pulseDot: { animation: '$auiNavPulse 1.2s ease-in-out infinite' },\n});\n\n/**\n * The Assistants nav-rail icon with a live status dot: a pulsing dot if any\n * conversation (across all assistants) is generating, else a solid red dot if\n * any is unread, else nothing — generating wins. Derived from the same\n * Signals-backed status store as the in-page indicators, so they never disagree.\n *\n * @public\n */\nexport function AssistantsNavIcon() {\n const classes = useStyles();\n const api = useApi(assistantsApiRef);\n // No focused conversation in the nav context, so pass undefined.\n const { overallStatus } = useThreadStatus(api, undefined);\n\n return (\n <Badge\n color={overallStatus === 'working' ? 'primary' : 'error'}\n variant=\"dot\"\n overlap=\"circular\"\n invisible={overallStatus === 'read'}\n classes={\n overallStatus === 'working' ? { dot: classes.pulseDot } : undefined\n }\n >\n <RiRobot2Line />\n </Badge>\n );\n}\n"],"names":[],"mappings":";;;;;;;;AAOA,MAAM,YAAY,UAAA,CAAW;AAAA,EAC3B,wBAAA,EAA0B;AAAA,IACxB,IAAA,EAAM,EAAE,SAAA,EAAW,UAAA,EAAY,SAAS,CAAA,EAAE;AAAA,IAC1C,KAAA,EAAO,EAAE,SAAA,EAAW,YAAA,EAAc,SAAS,IAAA,EAAK;AAAA,IAChD,MAAA,EAAQ,EAAE,SAAA,EAAW,UAAA,EAAY,SAAS,CAAA;AAAE,GAC9C;AAAA,EACA,QAAA,EAAU,EAAE,SAAA,EAAW,wCAAA;AACzB,CAAC,CAAA;AAUM,SAAS,iBAAA,GAAoB;AAClC,EAAA,MAAM,UAAU,SAAA,EAAU;AAC1B,EAAA,MAAM,GAAA,GAAM,OAAO,gBAAgB,CAAA;AAEnC,EAAA,MAAM,EAAE,aAAA,EAAc,GAAI,eAAA,CAAgB,KAAK,MAAS,CAAA;AAExD,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,KAAA,EAAO,aAAA,KAAkB,SAAA,GAAY,SAAA,GAAY,OAAA;AAAA,MACjD,OAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAQ,UAAA;AAAA,MACR,WAAW,aAAA,KAAkB,MAAA;AAAA,MAC7B,SACE,aAAA,KAAkB,SAAA,GAAY,EAAE,GAAA,EAAK,OAAA,CAAQ,UAAS,GAAI,MAAA;AAAA,MAG5D,8BAAC,YAAA,EAAA,EAAa;AAAA;AAAA,GAChB;AAEJ;;;;"}
1
+ {"version":3,"file":"AssistantsNavIcon.esm.js","sources":["../src/AssistantsNavIcon.tsx"],"sourcesContent":["import { RiRobot2Line } from '@remixicon/react';\nimport { useApi } from '@backstage/core-plugin-api';\nimport { assistantsApiRef } from './api';\nimport { useThreadStatus } from './collapsible/useThreadStatus';\nimport { StatusDot } from './collapsible/StatusDot';\n\n/**\n * The Assistants nav-rail icon with a live status dot: amber pulsing if any\n * conversation (across all assistants) is working, else solid red if any is\n * unread, else nothing — working wins. Shown on every Backstage page (it's a\n * sidebar item), and derived from the same server status snapshot as the in-page\n * indicators, so they never disagree.\n *\n * @public\n */\nexport function AssistantsNavIcon() {\n const api = useApi(assistantsApiRef);\n // No focused conversation in the nav context, so pass undefined.\n const { overallStatus } = useThreadStatus(api, undefined);\n\n return (\n <StatusDot status={overallStatus}>\n <RiRobot2Line />\n </StatusDot>\n );\n}\n"],"names":[],"mappings":";;;;;;;AAeO,SAAS,iBAAA,GAAoB;AAClC,EAAA,MAAM,GAAA,GAAM,OAAO,gBAAgB,CAAA;AAEnC,EAAA,MAAM,EAAE,aAAA,EAAc,GAAI,eAAA,CAAgB,KAAK,MAAS,CAAA;AAExD,EAAA,2BACG,SAAA,EAAA,EAAU,MAAA,EAAQ,aAAA,EACjB,QAAA,kBAAA,GAAA,CAAC,gBAAa,CAAA,EAChB,CAAA;AAEJ;;;;"}
@@ -7,12 +7,12 @@ import ListItemIcon from '@material-ui/core/ListItemIcon';
7
7
  import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
8
8
  import ListItemText from '@material-ui/core/ListItemText';
9
9
  import IconButton from '@material-ui/core/IconButton';
10
- import Badge from '@material-ui/core/Badge';
11
10
  import Tooltip from '@material-ui/core/Tooltip';
12
11
  import Typography from '@material-ui/core/Typography';
13
12
  import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined';
14
13
  import { AssistantAvatar } from './surface/AssistantAvatar.esm.js';
15
14
  import { AssistantDetailDialog } from './AssistantDetailDialog.esm.js';
15
+ import { StatusDot } from './StatusDot.esm.js';
16
16
 
17
17
  const useStyles = makeStyles((theme) => ({
18
18
  root: {
@@ -79,17 +79,7 @@ const AssistantsList = ({ assistants, activeId, onSelect, agentStatus }) => {
79
79
  ContainerProps: { className: classes.container },
80
80
  onClick: () => onSelect(a.id),
81
81
  children: [
82
- /* @__PURE__ */ jsx(ListItemIcon, { className: classes.icon, children: /* @__PURE__ */ jsx(
83
- Badge,
84
- {
85
- color: agentStatus?.(a.id) === "working" ? "primary" : "error",
86
- variant: "dot",
87
- overlap: "circular",
88
- invisible: (agentStatus?.(a.id) ?? "read") === "read",
89
- classes: agentStatus?.(a.id) === "working" ? { dot: classes.pulseDot } : void 0,
90
- children: /* @__PURE__ */ jsx(AssistantAvatar, { color: a.color, size: 22 })
91
- }
92
- ) }),
82
+ /* @__PURE__ */ jsx(ListItemIcon, { className: classes.icon, children: /* @__PURE__ */ jsx(StatusDot, { status: agentStatus?.(a.id) ?? "read", children: /* @__PURE__ */ jsx(AssistantAvatar, { color: a.color, size: 22 }) }) }),
93
83
  /* @__PURE__ */ jsx(
94
84
  ListItemText,
95
85
  {
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantsList.esm.js","sources":["../../src/collapsible/AssistantsList.tsx"],"sourcesContent":["import { FC, MouseEvent, useState } from 'react';\nimport { makeStyles } from '@material-ui/core/styles';\nimport List from '@material-ui/core/List';\nimport ListItem from '@material-ui/core/ListItem';\nimport ListItemIcon from '@material-ui/core/ListItemIcon';\nimport ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';\nimport ListItemText from '@material-ui/core/ListItemText';\nimport IconButton from '@material-ui/core/IconButton';\nimport Badge from '@material-ui/core/Badge';\nimport Tooltip from '@material-ui/core/Tooltip';\nimport Typography from '@material-ui/core/Typography';\nimport InfoOutlinedIcon from '@material-ui/icons/InfoOutlined';\nimport { AssistantSummary } from '@drewswiredin/backstage-plugin-assistants-common';\nimport { AssistantAvatar } from './surface/AssistantAvatar';\nimport { AssistantDetailDialog } from './AssistantDetailDialog';\nimport type { ConvStatus } from './useThreadStatus';\n\nconst useStyles = makeStyles(theme => ({\n root: {\n display: 'flex',\n flexDirection: 'column',\n padding: theme.spacing(1, 1.5, 0),\n },\n label: {\n color: theme.palette.text.secondary,\n fontWeight: 600,\n textTransform: 'uppercase',\n letterSpacing: 0.5,\n padding: theme.spacing(0, 0.5, 0.5),\n },\n item: {\n borderRadius: theme.shape.borderRadius,\n },\n // The hover rule lives on the container <li> (ListItem's ContainerProps), which\n // wraps BOTH the row and the secondary action. The action is a *sibling* of the\n // ListItem root, so a :hover rule on the row itself can't reach it.\n container: {\n '&:hover $infoAction': {\n opacity: 1,\n pointerEvents: 'auto',\n },\n },\n icon: {\n minWidth: 32,\n display: 'inline-flex',\n alignItems: 'center',\n },\n // Hidden until row hover (opacity + pointer-events so it's not clickable while\n // hidden); no reserved gutter so the title uses full width and truncates later.\n infoAction: {\n opacity: 0,\n pointerEvents: 'none',\n color: theme.palette.text.secondary,\n transition: theme.transitions.create('opacity'),\n },\n '@keyframes auiPulse': {\n '0%': { transform: 'scale(1)', opacity: 1 },\n '50%': { transform: 'scale(1.5)', opacity: 0.45 },\n '100%': { transform: 'scale(1)', opacity: 1 },\n },\n pulseDot: {\n animation: '$auiPulse 1.2s ease-in-out infinite',\n },\n}));\n\n/**\n * The in-page assistant switcher (top tier of the left sidebar). Lists every\n * accessible assistant; selecting one drives `?assistant=<id>` via `onSelect`,\n * which remounts the chat onto that assistant's (siloed) conversation set. The\n * active assistant is highlighted. A hover-revealed info button opens the\n * {@link AssistantDetailDialog} (description, tools, models).\n *\n * @public\n */\nexport const AssistantsList: FC<{\n assistants: AssistantSummary[];\n activeId: string;\n onSelect: (id: string) => void;\n /** Per-assistant rollup status (working/unread/read) for the rail dots. */\n agentStatus?: (assistantId: string) => ConvStatus;\n}> = ({ assistants, activeId, onSelect, agentStatus }) => {\n const classes = useStyles();\n const [detailId, setDetailId] = useState<string | null>(null);\n\n const openDetail = (e: MouseEvent<HTMLElement>, id: string) => {\n e.stopPropagation();\n setDetailId(id);\n };\n\n return (\n <div className={classes.root}>\n <Typography variant=\"caption\" className={classes.label}>\n Assistants\n </Typography>\n <List dense disablePadding>\n {assistants.map(a => (\n <ListItem\n key={a.id}\n button\n selected={a.id === activeId}\n className={classes.item}\n ContainerProps={{ className: classes.container }}\n onClick={() => onSelect(a.id)}\n >\n <ListItemIcon className={classes.icon}>\n <Badge\n color={agentStatus?.(a.id) === 'working' ? 'primary' : 'error'}\n variant=\"dot\"\n overlap=\"circular\"\n invisible={(agentStatus?.(a.id) ?? 'read') === 'read'}\n classes={\n agentStatus?.(a.id) === 'working'\n ? { dot: classes.pulseDot }\n : undefined\n }\n >\n <AssistantAvatar color={a.color} size={22} />\n </Badge>\n </ListItemIcon>\n <ListItemText\n primary={a.title}\n primaryTypographyProps={{ variant: 'body2', noWrap: true }}\n />\n <ListItemSecondaryAction className={classes.infoAction}>\n <Tooltip title=\"Details\">\n <IconButton\n edge=\"end\"\n size=\"small\"\n aria-label={`${a.title} details`}\n onClick={e => openDetail(e, a.id)}\n >\n <InfoOutlinedIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n </ListItemSecondaryAction>\n </ListItem>\n ))}\n </List>\n\n <AssistantDetailDialog\n assistant={assistants.find(a => a.id === detailId)}\n open={detailId !== null}\n onClose={() => setDetailId(null)}\n />\n </div>\n );\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AssistantsList.esm.js","sources":["../../src/collapsible/AssistantsList.tsx"],"sourcesContent":["import { FC, MouseEvent, useState } from 'react';\nimport { makeStyles } from '@material-ui/core/styles';\nimport List from '@material-ui/core/List';\nimport ListItem from '@material-ui/core/ListItem';\nimport ListItemIcon from '@material-ui/core/ListItemIcon';\nimport ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';\nimport ListItemText from '@material-ui/core/ListItemText';\nimport IconButton from '@material-ui/core/IconButton';\nimport Tooltip from '@material-ui/core/Tooltip';\nimport Typography from '@material-ui/core/Typography';\nimport InfoOutlinedIcon from '@material-ui/icons/InfoOutlined';\nimport { AssistantSummary } from '@drewswiredin/backstage-plugin-assistants-common';\nimport { AssistantAvatar } from './surface/AssistantAvatar';\nimport { AssistantDetailDialog } from './AssistantDetailDialog';\nimport { StatusDot } from './StatusDot';\nimport type { ConvStatus } from './useThreadStatus';\n\nconst useStyles = makeStyles(theme => ({\n root: {\n display: 'flex',\n flexDirection: 'column',\n padding: theme.spacing(1, 1.5, 0),\n },\n label: {\n color: theme.palette.text.secondary,\n fontWeight: 600,\n textTransform: 'uppercase',\n letterSpacing: 0.5,\n padding: theme.spacing(0, 0.5, 0.5),\n },\n item: {\n borderRadius: theme.shape.borderRadius,\n },\n // The hover rule lives on the container <li> (ListItem's ContainerProps), which\n // wraps BOTH the row and the secondary action. The action is a *sibling* of the\n // ListItem root, so a :hover rule on the row itself can't reach it.\n container: {\n '&:hover $infoAction': {\n opacity: 1,\n pointerEvents: 'auto',\n },\n },\n icon: {\n minWidth: 32,\n display: 'inline-flex',\n alignItems: 'center',\n },\n // Hidden until row hover (opacity + pointer-events so it's not clickable while\n // hidden); no reserved gutter so the title uses full width and truncates later.\n infoAction: {\n opacity: 0,\n pointerEvents: 'none',\n color: theme.palette.text.secondary,\n transition: theme.transitions.create('opacity'),\n },\n '@keyframes auiPulse': {\n '0%': { transform: 'scale(1)', opacity: 1 },\n '50%': { transform: 'scale(1.5)', opacity: 0.45 },\n '100%': { transform: 'scale(1)', opacity: 1 },\n },\n pulseDot: {\n animation: '$auiPulse 1.2s ease-in-out infinite',\n },\n}));\n\n/**\n * The in-page assistant switcher (top tier of the left sidebar). Lists every\n * accessible assistant; selecting one drives `?assistant=<id>` via `onSelect`,\n * which remounts the chat onto that assistant's (siloed) conversation set. The\n * active assistant is highlighted. A hover-revealed info button opens the\n * {@link AssistantDetailDialog} (description, tools, models).\n *\n * @public\n */\nexport const AssistantsList: FC<{\n assistants: AssistantSummary[];\n activeId: string;\n onSelect: (id: string) => void;\n /** Per-assistant rollup status (working/unread/read) for the rail dots. */\n agentStatus?: (assistantId: string) => ConvStatus;\n}> = ({ assistants, activeId, onSelect, agentStatus }) => {\n const classes = useStyles();\n const [detailId, setDetailId] = useState<string | null>(null);\n\n const openDetail = (e: MouseEvent<HTMLElement>, id: string) => {\n e.stopPropagation();\n setDetailId(id);\n };\n\n return (\n <div className={classes.root}>\n <Typography variant=\"caption\" className={classes.label}>\n Assistants\n </Typography>\n <List dense disablePadding>\n {assistants.map(a => (\n <ListItem\n key={a.id}\n button\n selected={a.id === activeId}\n className={classes.item}\n ContainerProps={{ className: classes.container }}\n onClick={() => onSelect(a.id)}\n >\n <ListItemIcon className={classes.icon}>\n <StatusDot status={agentStatus?.(a.id) ?? 'read'}>\n <AssistantAvatar color={a.color} size={22} />\n </StatusDot>\n </ListItemIcon>\n <ListItemText\n primary={a.title}\n primaryTypographyProps={{ variant: 'body2', noWrap: true }}\n />\n <ListItemSecondaryAction className={classes.infoAction}>\n <Tooltip title=\"Details\">\n <IconButton\n edge=\"end\"\n size=\"small\"\n aria-label={`${a.title} details`}\n onClick={e => openDetail(e, a.id)}\n >\n <InfoOutlinedIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n </ListItemSecondaryAction>\n </ListItem>\n ))}\n </List>\n\n <AssistantDetailDialog\n assistant={assistants.find(a => a.id === detailId)}\n open={detailId !== null}\n onClose={() => setDetailId(null)}\n />\n </div>\n );\n};\n"],"names":[],"mappings":"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"}
@@ -1,13 +1,13 @@
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 { useRef, useMemo, useState, useEffect, useCallback } from 'react';
4
+ import { useRef, useMemo, useState, useCallback, useEffect } 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';
8
8
  import { Progress, ResponseErrorPanel, Content } from '@backstage/core-components';
9
9
  import { makeStyles } from '@material-ui/core/styles';
10
- import { FormControl, Select, ListSubheader, MenuItem, ListItemIcon, Tooltip, IconButton, Badge, Typography } from '@material-ui/core';
10
+ import { FormControl, Select, ListSubheader, MenuItem, ListItemIcon, Tooltip, IconButton, Typography, Button } from '@material-ui/core';
11
11
  import AddIcon from '@material-ui/icons/Add';
12
12
  import ChatBubbleOutlineIcon from '@material-ui/icons/ChatBubbleOutline';
13
13
  import CheckIcon from '@material-ui/icons/Check';
@@ -20,8 +20,10 @@ import { AssistantAvatar } from './surface/AssistantAvatar.esm.js';
20
20
  import { SidePane } from './SidePane.esm.js';
21
21
  import { FullHeightRegion } from './FullHeightRegion.esm.js';
22
22
  import { createThreadListAdapter, patchThread } from './threadListAdapter.esm.js';
23
+ import { signalApiRef } from '@backstage/plugin-signals-react';
23
24
  import { makeRuntimeHook } from './useAssistantRuntime.esm.js';
24
- import { useThreadStatus } from './useThreadStatus.esm.js';
25
+ import { StatusDot } from './StatusDot.esm.js';
26
+ import { useThreadStatus, toConvStatus } from './useThreadStatus.esm.js';
25
27
 
26
28
  const SIDEPANE_COLLAPSED_KEY = "ai-chat-sidepane-collapsed";
27
29
  function loadSidePaneCollapsed() {
@@ -214,14 +216,24 @@ const useStyles = makeStyles((theme) => ({
214
216
  display: "flex",
215
217
  flexDirection: "column"
216
218
  },
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 }
219
+ // Bare-agent empty state (no conversation selected; new chats are "+"-only).
220
+ emptyState: {
221
+ display: "flex",
222
+ flexDirection: "column",
223
+ alignItems: "center",
224
+ justifyContent: "center",
225
+ gap: theme.spacing(1),
226
+ height: "100%",
227
+ textAlign: "center",
228
+ color: theme.palette.text.secondary,
229
+ padding: theme.spacing(3)
230
+ },
231
+ emptyTitle: {
232
+ fontWeight: 600,
233
+ color: theme.palette.text.primary
222
234
  },
223
- pulseDot: {
224
- animation: "$auiPulse 1.2s ease-in-out infinite"
235
+ emptyButton: {
236
+ marginTop: theme.spacing(1)
225
237
  }
226
238
  }));
227
239
  function splitModel(id, pool) {
@@ -259,12 +271,24 @@ function CollapsiblePage() {
259
271
  }
260
272
  );
261
273
  }
262
- const assistant = assistants.find((a) => a.id === requestedAssistant) ?? assistants[0];
263
- return /* @__PURE__ */ jsx(CollapsibleChat, { status: status.value, assistant }, assistant.id);
274
+ const initialAssistantId = assistants.find((a) => a.id === requestedAssistant)?.id ?? assistants[0].id;
275
+ return /* @__PURE__ */ jsx(
276
+ CollapsibleChat,
277
+ {
278
+ status: status.value,
279
+ initialAssistantId
280
+ }
281
+ );
282
+ }
283
+ function mostRecentThreadFor(assistantId, threadList) {
284
+ const updatedAt = (id) => threadList.threadItems[id]?.custom?.updatedAt ?? "";
285
+ return [...threadList.threadIds].filter(
286
+ (id) => threadList.threadItems[id]?.custom?.assistantId === assistantId
287
+ ).sort((a, b) => updatedAt(a) < updatedAt(b) ? 1 : -1)[0];
264
288
  }
265
289
  function CollapsibleChat({
266
290
  status,
267
- assistant
291
+ initialAssistantId
268
292
  }) {
269
293
  const api = useApi(assistantsApiRef);
270
294
  const baseUrl = useAsync(() => api.getBaseUrl(), [api]);
@@ -283,50 +307,71 @@ function CollapsibleChat({
283
307
  ChatRuntime,
284
308
  {
285
309
  status,
286
- assistant,
287
310
  api,
288
- baseUrl: baseUrl.value
311
+ baseUrl: baseUrl.value,
312
+ initialAssistantId
289
313
  }
290
314
  );
291
315
  }
292
316
  function ChatRuntime({
293
317
  status,
294
- assistant,
295
318
  api,
296
- baseUrl
319
+ baseUrl,
320
+ initialAssistantId
297
321
  }) {
298
- const defaultModel = assistant.defaultModel ?? status.defaultModel;
299
- const modelIdRef = useRef(defaultModel);
322
+ const activeAssistantIdRef = useRef(initialAssistantId);
323
+ const modelIdRef = useRef(status.defaultModel);
300
324
  const adapter = useMemo(
301
- () => createThreadListAdapter(api, assistant.id),
302
- [api, assistant.id]
325
+ () => createThreadListAdapter(api, () => activeAssistantIdRef.current),
326
+ [api]
303
327
  );
304
328
  const runtimeHook = useMemo(
305
- () => makeRuntimeHook({ api, baseUrl, assistantId: assistant.id, modelIdRef }),
306
- [api, baseUrl, assistant.id]
329
+ () => makeRuntimeHook({
330
+ api,
331
+ baseUrl,
332
+ getActiveAssistantId: () => activeAssistantIdRef.current,
333
+ modelIdRef
334
+ }),
335
+ [api, baseUrl]
307
336
  );
308
337
  const runtime = useRemoteThreadListRuntime({ adapter, runtimeHook });
309
338
  return /* @__PURE__ */ jsx(AssistantRuntimeProvider, { runtime, children: /* @__PURE__ */ jsx(
310
339
  ChatChrome,
311
340
  {
312
341
  status,
313
- assistant,
314
342
  api,
315
343
  modelIdRef,
316
- defaultModel
344
+ activeAssistantIdRef,
345
+ initialAssistantId
317
346
  }
318
347
  ) });
319
348
  }
320
349
  function ChatChrome({
321
350
  status,
322
- assistant,
323
351
  api,
324
352
  modelIdRef,
325
- defaultModel
353
+ activeAssistantIdRef,
354
+ initialAssistantId
326
355
  }) {
327
356
  const classes = useStyles();
328
357
  const runtime = useAssistantRuntime();
358
+ const signals = useApi(signalApiRef);
329
359
  const [, setSearchParams] = useSearchParams();
360
+ const [activeAssistantId, setActiveAssistantIdState] = useState(
361
+ initialAssistantId
362
+ );
363
+ const setActiveAssistantId = useCallback(
364
+ (id) => {
365
+ activeAssistantIdRef.current = id;
366
+ setActiveAssistantIdState(id);
367
+ },
368
+ [activeAssistantIdRef]
369
+ );
370
+ const assistant = useMemo(
371
+ () => status.assistants.find((a) => a.id === activeAssistantId) ?? status.assistants[0],
372
+ [status.assistants, activeAssistantId]
373
+ );
374
+ const defaultModel = assistant.defaultModel ?? status.defaultModel;
330
375
  const [threadList, setThreadList] = useState(
331
376
  () => runtime.threads.getState()
332
377
  );
@@ -340,44 +385,52 @@ function ChatChrome({
340
385
  const activeItem = threadList.threadItems[activeId];
341
386
  const activeRemoteId = activeItem?.remoteId;
342
387
  const activeTitle = activeItem?.title ?? "";
343
- const { statusOf, agentStatus, markRead, finishedTick } = useThreadStatus(
344
- api,
345
- activeRemoteId
346
- );
388
+ const isBlankDraft = activeId === threadList.newThreadId && !activeRemoteId;
389
+ const { statusOf, agentStatus } = useThreadStatus(api, activeRemoteId);
347
390
  const didSelectInitial = useRef(false);
348
391
  useEffect(() => {
349
392
  if (didSelectInitial.current || threadList.isLoading) return;
350
393
  didSelectInitial.current = true;
351
394
  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
- }
395
+ const mostRecent = mostRecentThreadFor(
396
+ activeAssistantIdRef.current,
397
+ threadList
398
+ );
399
+ if (mostRecent) void runtime.threads.switchToThread(mostRecent);
359
400
  }, [threadList.isLoading]);
360
401
  const conversations = useMemo(
361
- () => threadList.threadIds.map((id) => {
402
+ () => threadList.threadIds.filter((id) => {
403
+ if (id === activeId) return true;
404
+ if (id === threadList.newThreadId) return false;
405
+ const aid = threadList.threadItems[id]?.custom?.assistantId;
406
+ return aid === activeAssistantId;
407
+ }).map((id) => {
362
408
  const item = threadList.threadItems[id];
363
409
  const custom = item?.custom;
364
- const st = id === activeId ? "read" : statusOf(item?.remoteId);
410
+ const st = statusOf(item?.remoteId);
365
411
  return {
366
412
  id,
367
413
  remoteId: item?.remoteId,
368
414
  title: item?.title ?? "New Chat",
369
415
  pinned: custom?.pinned ?? false,
370
416
  unread: st === "unread",
371
- generating: st === "working"
417
+ working: st === "working"
372
418
  };
373
419
  }),
374
- [threadList, activeId, statusOf]
420
+ [threadList, statusOf, activeAssistantId, activeId]
375
421
  );
376
422
  useEffect(() => {
377
- if (finishedTick > 0) {
378
- void runtime.threads.reload();
379
- }
380
- }, [finishedTick, runtime]);
423
+ let timer;
424
+ const sub = signals.subscribe("assistants:threads", (msg) => {
425
+ if (msg.type !== "updated") return;
426
+ if (timer) clearTimeout(timer);
427
+ timer = setTimeout(() => void runtime.threads.reload(), 150);
428
+ });
429
+ return () => {
430
+ if (timer) clearTimeout(timer);
431
+ sub.unsubscribe();
432
+ };
433
+ }, [signals, runtime]);
381
434
  const allowedModels = useMemo(
382
435
  () => assistant.models ?? status.models.map((m) => m.id),
383
436
  [assistant.models, status.models]
@@ -405,9 +458,6 @@ function ChatChrome({
405
458
  setModelId(next);
406
459
  modelIdRef.current = next;
407
460
  }, [activeId]);
408
- useEffect(() => {
409
- if (activeRemoteId) markRead(activeRemoteId);
410
- }, [activeRemoteId]);
411
461
  const [sidePaneCollapsed, setSidePaneCollapsed] = useState(loadSidePaneCollapsed);
412
462
  useEffect(() => {
413
463
  try {
@@ -417,9 +467,14 @@ function ChatChrome({
417
467
  }, [sidePaneCollapsed]);
418
468
  const handleSelectAssistant = useCallback(
419
469
  (id) => {
420
- if (id !== assistant.id) setSearchParams({ assistant: id });
470
+ if (id === activeAssistantId) return;
471
+ setActiveAssistantId(id);
472
+ setSearchParams({ assistant: id });
473
+ const mostRecent = mostRecentThreadFor(id, threadList);
474
+ if (mostRecent) void runtime.threads.switchToThread(mostRecent);
475
+ else void runtime.threads.switchToNewThread();
421
476
  },
422
- [assistant.id, setSearchParams]
477
+ [activeAssistantId, setActiveAssistantId, setSearchParams, threadList, runtime]
423
478
  );
424
479
  const handleNew = useCallback(() => {
425
480
  void (async () => {
@@ -448,9 +503,20 @@ function ChatChrome({
448
503
  );
449
504
  const handleDelete = useCallback(
450
505
  (id) => {
451
- void runtime.threads.getItemById(id).delete();
506
+ void (async () => {
507
+ const wasActive = id === activeId;
508
+ await runtime.threads.getItemById(id).delete();
509
+ await runtime.threads.reload();
510
+ if (!wasActive) return;
511
+ const remaining = mostRecentThreadFor(
512
+ activeAssistantIdRef.current,
513
+ runtime.threads.getState()
514
+ );
515
+ if (remaining) await runtime.threads.switchToThread(remaining);
516
+ else await runtime.threads.switchToNewThread();
517
+ })();
452
518
  },
453
- [runtime]
519
+ [runtime, activeId, activeAssistantIdRef]
454
520
  );
455
521
  const handlePin = useCallback(
456
522
  async (id) => {
@@ -537,17 +603,7 @@ function ChatChrome({
537
603
  className: `${classes.sidePaneRailButton} ${a.id === assistant.id ? classes.sidePaneRailButtonActive : ""}`,
538
604
  "aria-label": a.title,
539
605
  onClick: () => handleSelectAssistant(a.id),
540
- children: /* @__PURE__ */ jsx(
541
- Badge,
542
- {
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 })
549
- }
550
- )
606
+ children: /* @__PURE__ */ jsx(StatusDot, { status: agentStatus(a.id), children: /* @__PURE__ */ jsx(AssistantAvatar, { color: a.color, size: 24 }) })
551
607
  }
552
608
  ) }, a.id)) }),
553
609
  /* @__PURE__ */ jsx("div", { className: classes.sidePaneRailDivider }),
@@ -579,13 +635,12 @@ function ChatChrome({
579
635
  "aria-label": conversation.title,
580
636
  onClick: () => handleSelect(conversation.id),
581
637
  children: /* @__PURE__ */ jsx(
582
- Badge,
638
+ StatusDot,
583
639
  {
584
- color: conversation.generating ? "primary" : "error",
585
- variant: "dot",
586
- overlap: "circular",
587
- invisible: !conversation.generating && !conversation.unread,
588
- classes: conversation.generating ? { dot: classes.pulseDot } : void 0,
640
+ status: toConvStatus(
641
+ conversation.working,
642
+ conversation.unread
643
+ ),
589
644
  children: /* @__PURE__ */ jsx(ChatBubbleOutlineIcon, { fontSize: "small" })
590
645
  }
591
646
  )
@@ -635,7 +690,25 @@ function ChatChrome({
635
690
  ] }),
636
691
  modelPicker
637
692
  ] }),
638
- /* @__PURE__ */ jsx("div", { className: classes.threadBody, children: /* @__PURE__ */ jsx(
693
+ /* @__PURE__ */ jsx("div", { className: classes.threadBody, children: isBlankDraft ? /* @__PURE__ */ jsxs("div", { className: classes.emptyState, children: [
694
+ /* @__PURE__ */ jsx(Typography, { variant: "body1", className: classes.emptyTitle, children: "No conversation yet" }),
695
+ /* @__PURE__ */ jsxs(Typography, { variant: "body2", color: "textSecondary", children: [
696
+ "Start a new chat with ",
697
+ assistant.title,
698
+ "."
699
+ ] }),
700
+ /* @__PURE__ */ jsx(
701
+ Button,
702
+ {
703
+ variant: "outlined",
704
+ size: "small",
705
+ startIcon: /* @__PURE__ */ jsx(AddIcon, {}),
706
+ onClick: handleNew,
707
+ className: classes.emptyButton,
708
+ children: "New chat"
709
+ }
710
+ )
711
+ ] }) : /* @__PURE__ */ jsx(
639
712
  ConversationSurface,
640
713
  {
641
714
  composerPlaceholder: assistant.ui?.composer?.placeholder,