@athenaintel/react 0.40.0 → 0.40.2
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.
- package/README.md +36 -2
- package/dist/chat/AthenaChat.cjs +25 -11
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +26 -12
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/run-activity.cjs +58 -13
- package/dist/chat/run-activity.cjs.map +1 -1
- package/dist/chat/run-activity.d.ts +52 -6
- package/dist/chat/run-activity.js +59 -14
- package/dist/chat/run-activity.js.map +1 -1
- package/dist/lib/use-document-active.cjs +40 -0
- package/dist/lib/use-document-active.cjs.map +1 -0
- package/dist/lib/use-document-active.d.ts +9 -0
- package/dist/lib/use-document-active.js +40 -0
- package/dist/lib/use-document-active.js.map +1 -0
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/threads/Inbox.cjs +6 -2
- package/dist/threads/Inbox.cjs.map +1 -1
- package/dist/threads/Inbox.js +6 -2
- package/dist/threads/Inbox.js.map +1 -1
- package/dist/threads/mark-viewed-thread-read.cjs +97 -0
- package/dist/threads/mark-viewed-thread-read.cjs.map +1 -0
- package/dist/threads/mark-viewed-thread-read.d.ts +7 -0
- package/dist/threads/mark-viewed-thread-read.js +97 -0
- package/dist/threads/mark-viewed-thread-read.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Inbox.cjs","sources":["../../src/threads/Inbox.tsx"],"sourcesContent":["/**\n * `Inbox` and `InboxButton` — the Devin-style notifications inbox over the\n * session list, with no endpoint of its own:\n *\n * - **What is in it.** The viewer's own sessions, newest activity first, minus\n * the ones dismissed from the inbox — the same set Athena's inbox shows —\n * read through `POST /api/conversations/threads/list` with\n * `created_by_me` + `exclude_dismissed` (+ `unread_only` for the Unread\n * tab). Each row is the session's title, its latest message, a state chip\n * (Needs you / Working / PR merged / Answered …) and an unread dot.\n * - **Live.** The rows fold the per-user conversation index like every other\n * list under the provider, so a session flipping to \"needs you\" or getting\n * a new message moves in place.\n * - **Writes.** Opening a row (or \"Mark read\") stores the read receipt through\n * `POST /api/conversations/threads/read-state`; \"Dismiss\" uses\n * `POST /api/conversations/threads/notifications/dismiss` (EDIT on the\n * session — a dismissed session comes back with its next message).\n * - **Counts.** The unread badge counts the unread page (50 rows); past that\n * it reads `50+`.\n *\n * Renders nothing outside `<AthenaProvider enableThreadList>`.\n */\nimport { BellIcon, CheckCheckIcon, MailIcon, MailOpenIcon, XIcon } from 'lucide-react';\nimport { useMutation } from '@tanstack/react-query';\nimport type { FC } from 'react';\nimport { useContext, useMemo, useState } from 'react';\nimport { cn } from '../lib/utils';\nimport { useAthenaQueryClient } from '../lib/use-athena-rest-query';\nimport { withCredentialRenewal } from '../runtime/credential-renewal';\nimport { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';\nimport { dismissThreadFromInbox } from './api';\nimport { SessionListSkeleton, type SessionOpenTarget } from './SessionList';\nimport { relativeTime } from './session-list-model';\nimport { FOCUS_RING_CLASS, RowIconButton, StatusChip } from './session-row-parts';\nimport { inboxStateChip, plainExcerpt } from './sessions-page-model';\nimport type { AthenaSessionsQuery } from './sessions-query';\nimport {\n AthenaSessionsLiveContext,\n type AthenaStatewireThreadListState,\n type AthenaStatewireThreadSummary,\n useAthenaStatewireThreadList,\n} from './statewire-thread-list';\nimport { type AthenaSessionsList, useAthenaSessions } from './use-athena-sessions';\n\n/** The inbox's sessions: the viewer's own, not dismissed, newest activity first. */\nexport const INBOX_QUERY: AthenaSessionsQuery = { createdByMe: true, excludeDismissed: true };\n/** The Unread tab, and what the badge counts. */\nexport const INBOX_UNREAD_QUERY: AthenaSessionsQuery = { ...INBOX_QUERY, unreadOnly: true };\n\n/** Rows per inbox page — and the badge's ceiling before it reads `50+`. */\nexport const INBOX_PAGE_SIZE = 50;\n\nexport type InboxTab = 'all' | 'unread';\n\nexport interface AthenaInboxUnreadCount {\n /** Unread sessions loaded (at most one page). */\n count: number;\n /** More unread sessions exist beyond `count`. */\n hasMore: boolean;\n /** `count` with `+` when there are more, `''` for none. */\n label: string;\n isAvailable: boolean;\n}\n\n/** The unread count the inbox badge shows; shares the Unread tab's cache. */\nexport function useAthenaInboxUnreadCount(): AthenaInboxUnreadCount {\n const unread = useAthenaSessions({ query: INBOX_UNREAD_QUERY, pageSize: INBOX_PAGE_SIZE });\n const count = unread.rows.length;\n const hasMore = unread.hasNextPage;\n return {\n count,\n hasMore,\n label: count === 0 ? '' : `${count}${hasMore ? '+' : ''}`,\n isAvailable: unread.isAvailable,\n };\n}\n\n/** Dismiss from the inbox: hidden at once, back on failure, the inbox lists refreshed after. */\nfunction useInboxDismiss(refresh: () => Promise<void>) {\n const live = useContext(AthenaSessionsLiveContext);\n const queryClient = useAthenaQueryClient();\n const [hidden, setHidden] = useState<ReadonlySet<string>>(() => new Set());\n const mutation = useMutation(\n {\n mutationKey: ['athena-sdk', 'inbox-dismiss', live?.backendUrl ?? '', live?.principal ?? ''],\n mutationFn: async (threadId: string) => {\n if (!live) throw new Error('[AthenaSDK] The inbox needs enableThreadList.');\n return withCredentialRenewal(live.getAuth, (auth) =>\n dismissThreadFromInbox(live.backendUrl, auth, { threadId, dismissed: true }),\n );\n },\n onMutate: (threadId) => setHidden((current) => new Set(current).add(threadId)),\n // The local hide only bridges the write and the refresh after it: once\n // the inbox has re-read, the server decides — a dismissed session comes\n // back with its next message, and a failed dismiss shows it again.\n onSettled: async (_result, _error, threadId) => {\n try {\n await refresh();\n } finally {\n setHidden((current) => {\n if (!current.has(threadId)) return current;\n const next = new Set(current);\n next.delete(threadId);\n return next;\n });\n }\n },\n },\n queryClient,\n );\n return { hidden, dismiss: mutation.mutate, pending: mutation.isPending };\n}\n\nconst InboxRow: FC<{\n row: AthenaStatewireThreadSummary;\n isActive: boolean;\n list: AthenaStatewireThreadListState;\n onOpen: (row: AthenaStatewireThreadSummary) => void;\n onDismiss: ((threadId: string) => void) | null;\n}> = ({ row, isActive, list, onOpen, onDismiss }) => {\n const chip = inboxStateChip(row);\n const excerpt = plainExcerpt(row.preview);\n return (\n <li\n data-thread-id={row.id}\n data-unread={row.isUnread || undefined}\n className={cn(\n 'group relative flex items-start gap-2 rounded-lg transition-colors hover:bg-muted/40',\n isActive && 'bg-muted',\n )}\n >\n <button\n type=\"button\"\n onClick={() => onOpen(row)}\n className={cn('flex min-w-0 flex-1 items-start gap-2 rounded-lg p-2 pl-2.5 text-left', FOCUS_RING_CLASS)}\n >\n <span\n role={row.isUnread ? 'img' : undefined}\n aria-label={row.isUnread ? 'Unread' : undefined}\n aria-hidden={row.isUnread ? undefined : true}\n className={cn('mt-1.5 size-2 shrink-0 rounded-full', row.isUnread ? 'bg-primary' : 'bg-transparent')}\n />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-center gap-2\">\n <span\n data-title\n className={cn('min-w-0 flex-1 truncate text-foreground text-sm', row.isUnread ? 'font-semibold' : 'font-medium')}\n >\n {row.title ?? 'Untitled'}\n </span>\n <span className=\"shrink-0 text-[11px] text-muted-foreground\">{relativeTime(row.updatedAt)}</span>\n </span>\n {excerpt ? (\n <span data-excerpt className=\"mt-0.5 line-clamp-2 break-words text-muted-foreground text-xs\">\n {excerpt}\n </span>\n ) : null}\n <span className=\"mt-1 flex\">\n <StatusChip label={chip.label} tone={chip.tone} detail={row.nextAction} />\n </span>\n </span>\n </button>\n <div data-row-actions className=\"flex shrink-0 items-center gap-0.5 py-2 pr-2\">\n <RowIconButton\n label={row.isUnread ? 'Mark read' : 'Mark unread'}\n action=\"read-state\"\n pending={list.readStatePendingThreadIds.has(row.id)}\n onClick={() =>\n list.setReadState({ threadId: row.id, isUnread: !row.isUnread, updatedAt: row.updatedAt })\n }\n >\n {row.isUnread ? <MailOpenIcon className=\"size-3.5\" aria-hidden /> : <MailIcon className=\"size-3.5\" aria-hidden />}\n </RowIconButton>\n {onDismiss ? (\n <RowIconButton label=\"Dismiss from inbox\" action=\"dismiss\" onClick={() => onDismiss(row.id)}>\n <XIcon className=\"size-3.5\" aria-hidden />\n </RowIconButton>\n ) : null}\n </div>\n </li>\n );\n};\n\nexport interface InboxProps {\n className?: string;\n /** Heading (default \"Inbox\"). */\n title?: string;\n /** The tab it opens on (default `all`). */\n defaultTab?: InboxTab;\n /** Called after a row is opened (selected and marked read), so a host can show its chat. */\n onOpenSession?: (target: SessionOpenTarget) => void;\n}\n\nfunction InboxBody({\n className,\n title = 'Inbox',\n defaultTab = 'all',\n onOpenSession,\n list,\n}: InboxProps & { list: AthenaStatewireThreadListState }) {\n const [tab, setTab] = useState<InboxTab>(defaultTab);\n const all = useAthenaSessions({ query: INBOX_QUERY, pageSize: INBOX_PAGE_SIZE });\n const unread = useAthenaSessions({ query: INBOX_UNREAD_QUERY, pageSize: INBOX_PAGE_SIZE });\n const shown: AthenaSessionsList = tab === 'unread' ? unread : all;\n const refreshBoth = async () => {\n await Promise.all([all.refresh(), unread.refresh()]);\n };\n const dismissal = useInboxDismiss(refreshBoth);\n // Dismissal is only offered where the server can leave dismissed sessions\n // out of the inbox; elsewhere a dismissed row would stay in the list.\n const canDismiss = shown.supportedFilters?.has('exclude_dismissed') ?? false;\n const rows = useMemo(\n () => shown.rows.filter((row) => !dismissal.hidden.has(row.id)),\n [shown.rows, dismissal.hidden],\n );\n const unreadRows = unread.rows.filter((row) => !dismissal.hidden.has(row.id));\n const unreadLabel = `${unreadRows.length}${unread.hasNextPage ? '+' : ''}`;\n const allLabel = `${all.rows.filter((row) => !dismissal.hidden.has(row.id)).length}${all.hasNextPage ? '+' : ''}`;\n\n const open = (row: AthenaStatewireThreadSummary) => {\n list.selectThread(row.id);\n if (row.isUnread) {\n list.setReadState({ threadId: row.id, isUnread: false, updatedAt: row.updatedAt });\n }\n onOpenSession?.({ threadId: row.id, title: row.title });\n };\n const markAllRead = () => {\n for (const row of unreadRows) {\n list.setReadState({ threadId: row.id, isUnread: false, updatedAt: row.updatedAt });\n }\n };\n const showSkeleton = rows.length === 0 && (!shown.hasResolvedFirstPage || shown.isFetching);\n const notice =\n shown.unavailableFields.length > 0\n ? 'This Athena server cannot narrow the inbox to your own, undismissed sessions yet, so it lists every session you can see.'\n : null;\n\n const tabClass = (active: boolean) =>\n cn(\n 'inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 font-medium text-xs transition-colors',\n active ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground',\n FOCUS_RING_CLASS,\n );\n\n return (\n <div data-testid=\"inbox\" className={cn('athena-sdk-chrome flex min-h-0 flex-col gap-2', className)}>\n <div className=\"flex items-center gap-2 px-1\">\n <h2 className=\"font-semibold text-foreground text-sm\">{title}</h2>\n <div role=\"tablist\" aria-label=\"Inbox\" className=\"flex items-center gap-0.5\">\n <button type=\"button\" role=\"tab\" aria-selected={tab === 'all'} onClick={() => setTab('all')} className={tabClass(tab === 'all')}>\n All\n <span data-testid=\"inbox-all-count\" className=\"text-muted-foreground\">\n {allLabel}\n </span>\n </button>\n <button type=\"button\" role=\"tab\" aria-selected={tab === 'unread'} onClick={() => setTab('unread')} className={tabClass(tab === 'unread')}>\n Unread\n <span data-testid=\"inbox-unread-count\" className=\"text-muted-foreground\">\n {unreadLabel}\n </span>\n </button>\n </div>\n <div className=\"flex-1\" />\n <button\n type=\"button\"\n onClick={markAllRead}\n disabled={unreadRows.length === 0}\n className={cn(\n 'inline-flex h-7 items-center gap-1 rounded-md px-2 text-muted-foreground text-xs transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50',\n FOCUS_RING_CLASS,\n )}\n title=\"Mark the loaded unread sessions read\"\n >\n <CheckCheckIcon className=\"size-3.5\" aria-hidden />\n Mark all read\n </button>\n </div>\n {notice ? (\n <p role=\"status\" data-testid=\"inbox-degraded\" className=\"mx-1 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5 text-[11px] text-amber-900\">\n {notice}\n </p>\n ) : null}\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {showSkeleton ? (\n <SessionListSkeleton rows={4} />\n ) : rows.length === 0 ? (\n <p data-testid=\"inbox-empty\" className=\"px-3 py-8 text-center text-muted-foreground text-xs\">\n {tab === 'unread' ? 'You are all caught up.' : 'Nothing in your inbox.'}\n </p>\n ) : (\n <ul className=\"flex flex-col gap-0.5\" aria-busy={shown.isFetching || undefined}>\n {rows.map((row) => (\n <InboxRow\n key={row.id}\n row={row}\n isActive={row.id === list.activeThreadId}\n list={list}\n onOpen={open}\n onDismiss={canDismiss ? dismissal.dismiss : null}\n />\n ))}\n </ul>\n )}\n {shown.hasNextPage && !showSkeleton ? (\n <button\n type=\"button\"\n onClick={shown.fetchNextPage}\n disabled={shown.isFetchingNextPage}\n className={cn('mt-1 w-full rounded-md py-2 text-muted-foreground text-xs transition-colors hover:bg-muted disabled:opacity-50', FOCUS_RING_CLASS)}\n >\n {shown.isFetchingNextPage ? 'Loading…' : 'Load more'}\n </button>\n ) : null}\n </div>\n </div>\n );\n}\n\n/**\n * The notifications inbox: All / Unread tabs with counts, rows of title,\n * latest message, state chip and unread dot, mark read / mark all read and\n * dismiss. Renders nothing outside `<AthenaProvider enableThreadList>`.\n */\nexport function Inbox(props: InboxProps) {\n const list = useAthenaStatewireThreadList();\n const live = useContext(AthenaSessionsLiveContext);\n if (!list || !live) return null;\n return <InboxBody {...props} list={list} />;\n}\n\nexport interface InboxButtonProps extends Omit<InboxProps, 'className'> {\n className?: string;\n /** Classes for the popover panel. */\n panelClassName?: string;\n align?: 'start' | 'center' | 'end';\n}\n\nfunction InboxButtonBody({ className, panelClassName, align = 'end', onOpenSession, ...inboxProps }: InboxButtonProps) {\n const [open, setOpen] = useState(false);\n const unread = useAthenaInboxUnreadCount();\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label={unread.count > 0 ? `Inbox, ${unread.label} unread` : 'Inbox'}\n data-testid=\"inbox-button\"\n className={cn(\n 'relative inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground',\n FOCUS_RING_CLASS,\n className,\n )}\n >\n <BellIcon className=\"size-4\" aria-hidden />\n {unread.count > 0 ? (\n <span\n data-testid=\"inbox-badge\"\n className=\"absolute -top-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 font-semibold text-[10px] text-primary-foreground leading-none\"\n >\n {unread.label}\n </span>\n ) : null}\n </button>\n </PopoverTrigger>\n <PopoverContent align={align} className={cn('flex max-h-[70vh] w-96 flex-col p-2', panelClassName)}>\n <Inbox\n {...inboxProps}\n onOpenSession={(target) => {\n setOpen(false);\n onOpenSession?.(target);\n }}\n />\n </PopoverContent>\n </Popover>\n );\n}\n\n/**\n * A bell with the unread count that opens `Inbox` in a popover. Renders\n * nothing outside `<AthenaProvider enableThreadList>`.\n */\nexport function InboxButton(props: InboxButtonProps) {\n const live = useContext(AthenaSessionsLiveContext);\n if (!live) return null;\n return <InboxButtonBody {...props} />;\n}\n"],"names":["useAthenaSessions","useContext","AthenaSessionsLiveContext","useAthenaQueryClient","useState","useMutation","withCredentialRenewal","dismissThreadFromInbox","inboxStateChip","plainExcerpt","jsxs","cn","FOCUS_RING_CLASS","jsx","relativeTime","StatusChip","RowIconButton","MailOpenIcon","MailIcon","XIcon","useMemo","CheckCheckIcon","SessionListSkeleton","useAthenaStatewireThreadList","Popover","PopoverTrigger","BellIcon","PopoverContent"],"mappings":";;;;;;;;;;;;;;;;;AA6CO,MAAM,cAAmC,EAAE,aAAa,MAAM,kBAAkB,KAAA;AAEhF,MAAM,qBAA0C,EAAE,GAAG,aAAa,YAAY,KAAA;AAG9E,MAAM,kBAAkB;AAexB,SAAS,4BAAoD;AAClE,QAAM,SAASA,kBAAAA,kBAAkB,EAAE,OAAO,oBAAoB,UAAU,iBAAiB;AACzF,QAAM,QAAQ,OAAO,KAAK;AAC1B,QAAM,UAAU,OAAO;AACvB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,OAAO,UAAU,IAAI,KAAK,GAAG,KAAK,GAAG,UAAU,MAAM,EAAE;AAAA,IACvD,aAAa,OAAO;AAAA,EAAA;AAExB;AAGA,SAAS,gBAAgB,SAA8B;AACrD,QAAM,OAAOC,MAAAA,WAAWC,6CAAyB;AACjD,QAAM,cAAcC,mBAAAA,qBAAA;AACpB,QAAM,CAAC,QAAQ,SAAS,IAAIC,MAAAA,SAA8B,MAAM,oBAAI,KAAK;AACzE,QAAM,WAAWC,WAAAA;AAAAA,IACf;AAAA,MACE,aAAa,CAAC,cAAc,kBAAiB,6BAAM,eAAc,KAAI,6BAAM,cAAa,EAAE;AAAA,MAC1F,YAAY,OAAO,aAAqB;AACtC,YAAI,CAAC,KAAM,OAAM,IAAI,MAAM,+CAA+C;AAC1E,eAAOC,kBAAAA;AAAAA,UAAsB,KAAK;AAAA,UAAS,CAAC,SAC1CC,IAAAA,uBAAuB,KAAK,YAAY,MAAM,EAAE,UAAU,WAAW,KAAA,CAAM;AAAA,QAAA;AAAA,MAE/E;AAAA,MACA,UAAU,CAAC,aAAa,UAAU,CAAC,YAAY,IAAI,IAAI,OAAO,EAAE,IAAI,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA,MAI7E,WAAW,OAAO,SAAS,QAAQ,aAAa;AAC9C,YAAI;AACF,gBAAM,QAAA;AAAA,QACR,UAAA;AACE,oBAAU,CAAC,YAAY;AACrB,gBAAI,CAAC,QAAQ,IAAI,QAAQ,EAAG,QAAO;AACnC,kBAAM,OAAO,IAAI,IAAI,OAAO;AAC5B,iBAAK,OAAO,QAAQ;AACpB,mBAAO;AAAA,UACT,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IAAA;AAAA,IAEF;AAAA,EAAA;AAEF,SAAO,EAAE,QAAQ,SAAS,SAAS,QAAQ,SAAS,SAAS,UAAA;AAC/D;AAEA,MAAM,WAMD,CAAC,EAAE,KAAK,UAAU,MAAM,QAAQ,gBAAgB;AACnD,QAAM,OAAOC,kBAAAA,eAAe,GAAG;AAC/B,QAAM,UAAUC,kBAAAA,aAAa,IAAI,OAAO;AACxC,SACEC,2BAAAA;AAAAA,IAAC;AAAA,IAAA;AAAA,MACC,kBAAgB,IAAI;AAAA,MACpB,eAAa,IAAI,YAAY;AAAA,MAC7B,WAAWC,OAAAA;AAAAA,QACT;AAAA,QACA,YAAY;AAAA,MAAA;AAAA,MAGd,UAAA;AAAA,QAAAD,2BAAAA;AAAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM,OAAO,GAAG;AAAA,YACzB,WAAWC,OAAAA,GAAG,yEAAyEC,gCAAgB;AAAA,YAEvG,UAAA;AAAA,cAAAC,2BAAAA;AAAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAM,IAAI,WAAW,QAAQ;AAAA,kBAC7B,cAAY,IAAI,WAAW,WAAW;AAAA,kBACtC,eAAa,IAAI,WAAW,SAAY;AAAA,kBACxC,WAAWF,OAAAA,GAAG,uCAAuC,IAAI,WAAW,eAAe,gBAAgB;AAAA,gBAAA;AAAA,cAAA;AAAA,cAErGD,2BAAAA,KAAC,QAAA,EAAK,WAAU,kBACd,UAAA;AAAA,gBAAAA,2BAAAA,KAAC,QAAA,EAAK,WAAU,2BACd,UAAA;AAAA,kBAAAG,2BAAAA;AAAAA,oBAAC;AAAA,oBAAA;AAAA,sBACC,cAAU;AAAA,sBACV,WAAWF,OAAAA,GAAG,mDAAmD,IAAI,WAAW,kBAAkB,aAAa;AAAA,sBAE9G,cAAI,SAAS;AAAA,oBAAA;AAAA,kBAAA;AAAA,iDAEf,QAAA,EAAK,WAAU,8CAA8C,UAAAG,iBAAAA,aAAa,IAAI,SAAS,EAAA,CAAE;AAAA,gBAAA,GAC5F;AAAA,gBACC,yCACE,QAAA,EAAK,gBAAY,MAAC,WAAU,iEAC1B,mBACH,IACE;AAAA,gBACJD,2BAAAA,IAAC,QAAA,EAAK,WAAU,aACd,yCAACE,gBAAAA,YAAA,EAAW,OAAO,KAAK,OAAO,MAAM,KAAK,MAAM,QAAQ,IAAI,YAAY,EAAA,CAC1E;AAAA,cAAA,EAAA,CACF;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAEFL,2BAAAA,KAAC,OAAA,EAAI,oBAAgB,MAAC,WAAU,gDAC9B,UAAA;AAAA,UAAAG,2BAAAA;AAAAA,YAACG,gBAAAA;AAAAA,YAAA;AAAA,cACC,OAAO,IAAI,WAAW,cAAc;AAAA,cACpC,QAAO;AAAA,cACP,SAAS,KAAK,0BAA0B,IAAI,IAAI,EAAE;AAAA,cAClD,SAAS,MACP,KAAK,aAAa,EAAE,UAAU,IAAI,IAAI,UAAU,CAAC,IAAI,UAAU,WAAW,IAAI,WAAW;AAAA,cAG1F,UAAA,IAAI,WAAWH,2BAAAA,IAACI,YAAAA,cAAA,EAAa,WAAU,YAAW,eAAW,KAAA,CAAC,IAAKJ,2BAAAA,IAACK,YAAAA,UAAA,EAAS,WAAU,YAAW,eAAW,KAAA,CAAC;AAAA,YAAA;AAAA,UAAA;AAAA,UAEhH,2CACEF,gBAAAA,eAAA,EAAc,OAAM,sBAAqB,QAAO,WAAU,SAAS,MAAM,UAAU,IAAI,EAAE,GACxF,yCAACG,YAAAA,OAAA,EAAM,WAAU,YAAW,eAAW,MAAC,GAC1C,IACE;AAAA,QAAA,EAAA,CACN;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;AAYA,SAAS,UAAU;AAAA,EACjB;AAAA,EACA,QAAQ;AAAA,EACR,aAAa;AAAA,EACb;AAAA,EACA;AACF,GAA0D;;AACxD,QAAM,CAAC,KAAK,MAAM,IAAIf,MAAAA,SAAmB,UAAU;AACnD,QAAM,MAAMJ,kBAAAA,kBAAkB,EAAE,OAAO,aAAa,UAAU,iBAAiB;AAC/E,QAAM,SAASA,kBAAAA,kBAAkB,EAAE,OAAO,oBAAoB,UAAU,iBAAiB;AACzF,QAAM,QAA4B,QAAQ,WAAW,SAAS;AAC9D,QAAM,cAAc,YAAY;AAC9B,UAAM,QAAQ,IAAI,CAAC,IAAI,WAAW,OAAO,QAAA,CAAS,CAAC;AAAA,EACrD;AACA,QAAM,YAAY,gBAAgB,WAAW;AAG7C,QAAM,eAAa,WAAM,qBAAN,mBAAwB,IAAI,yBAAwB;AACvE,QAAM,OAAOoB,MAAAA;AAAAA,IACX,MAAM,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,UAAU,OAAO,IAAI,IAAI,EAAE,CAAC;AAAA,IAC9D,CAAC,MAAM,MAAM,UAAU,MAAM;AAAA,EAAA;AAE/B,QAAM,aAAa,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,UAAU,OAAO,IAAI,IAAI,EAAE,CAAC;AAC5E,QAAM,cAAc,GAAG,WAAW,MAAM,GAAG,OAAO,cAAc,MAAM,EAAE;AACxE,QAAM,WAAW,GAAG,IAAI,KAAK,OAAO,CAAC,QAAQ,CAAC,UAAU,OAAO,IAAI,IAAI,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,cAAc,MAAM,EAAE;AAE/G,QAAM,OAAO,CAAC,QAAsC;AAClD,SAAK,aAAa,IAAI,EAAE;AACxB,QAAI,IAAI,UAAU;AAChB,WAAK,aAAa,EAAE,UAAU,IAAI,IAAI,UAAU,OAAO,WAAW,IAAI,UAAA,CAAW;AAAA,IACnF;AACA,mDAAgB,EAAE,UAAU,IAAI,IAAI,OAAO,IAAI;EACjD;AACA,QAAM,cAAc,MAAM;AACxB,eAAW,OAAO,YAAY;AAC5B,WAAK,aAAa,EAAE,UAAU,IAAI,IAAI,UAAU,OAAO,WAAW,IAAI,UAAA,CAAW;AAAA,IACnF;AAAA,EACF;AACA,QAAM,eAAe,KAAK,WAAW,MAAM,CAAC,MAAM,wBAAwB,MAAM;AAChF,QAAM,SACJ,MAAM,kBAAkB,SAAS,IAC7B,6HACA;AAEN,QAAM,WAAW,CAAC,WAChBT,OAAAA;AAAAA,IACE;AAAA,IACA,SAAS,6BAA6B;AAAA,IACtCC,gBAAAA;AAAAA,EAAA;AAGJ,SACEF,gCAAC,SAAI,eAAY,SAAQ,WAAWC,OAAAA,GAAG,iDAAiD,SAAS,GAC/F,UAAA;AAAA,IAAAD,2BAAAA,KAAC,OAAA,EAAI,WAAU,gCACb,UAAA;AAAA,MAAAG,2BAAAA,IAAC,MAAA,EAAG,WAAU,yCAAyC,UAAA,OAAM;AAAA,sCAC5D,OAAA,EAAI,MAAK,WAAU,cAAW,SAAQ,WAAU,6BAC/C,UAAA;AAAA,QAAAH,gCAAC,YAAO,MAAK,UAAS,MAAK,OAAM,iBAAe,QAAQ,OAAO,SAAS,MAAM,OAAO,KAAK,GAAG,WAAW,SAAS,QAAQ,KAAK,GAAG,UAAA;AAAA,UAAA;AAAA,yCAE9H,QAAA,EAAK,eAAY,mBAAkB,WAAU,yBAC3C,UAAA,SAAA,CACH;AAAA,QAAA,GACF;AAAA,wCACC,UAAA,EAAO,MAAK,UAAS,MAAK,OAAM,iBAAe,QAAQ,UAAU,SAAS,MAAM,OAAO,QAAQ,GAAG,WAAW,SAAS,QAAQ,QAAQ,GAAG,UAAA;AAAA,UAAA;AAAA,yCAEvI,QAAA,EAAK,eAAY,sBAAqB,WAAU,yBAC9C,UAAA,YAAA,CACH;AAAA,QAAA,EAAA,CACF;AAAA,MAAA,GACF;AAAA,MACAG,2BAAAA,IAAC,OAAA,EAAI,WAAU,SAAA,CAAS;AAAA,MACxBH,2BAAAA;AAAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS;AAAA,UACT,UAAU,WAAW,WAAW;AAAA,UAChC,WAAWC,OAAAA;AAAAA,YACT;AAAA,YACAC,gBAAAA;AAAAA,UAAA;AAAA,UAEF,OAAM;AAAA,UAEN,UAAA;AAAA,YAAAC,2BAAAA,IAACQ,YAAAA,gBAAA,EAAe,WAAU,YAAW,eAAW,MAAC;AAAA,YAAE;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,IAErD,GACF;AAAA,IACC,SACCR,2BAAAA,IAAC,KAAA,EAAE,MAAK,UAAS,eAAY,kBAAiB,WAAU,gGACrD,UAAA,OAAA,CACH,IACE;AAAA,IACJH,2BAAAA,KAAC,OAAA,EAAI,WAAU,kCACZ,UAAA;AAAA,MAAA,eACCG,2BAAAA,IAACS,iCAAA,EAAoB,MAAM,EAAA,CAAG,IAC5B,KAAK,WAAW,IAClBT,2BAAAA,IAAC,KAAA,EAAE,eAAY,eAAc,WAAU,uDACpC,UAAA,QAAQ,WAAW,2BAA2B,0BACjD,IAEAA,2BAAAA,IAAC,MAAA,EAAG,WAAU,yBAAwB,aAAW,MAAM,cAAc,QAClE,UAAA,KAAK,IAAI,CAAC,QACTA,2BAAAA;AAAAA,QAAC;AAAA,QAAA;AAAA,UAEC;AAAA,UACA,UAAU,IAAI,OAAO,KAAK;AAAA,UAC1B;AAAA,UACA,QAAQ;AAAA,UACR,WAAW,aAAa,UAAU,UAAU;AAAA,QAAA;AAAA,QALvC,IAAI;AAAA,MAAA,CAOZ,GACH;AAAA,MAED,MAAM,eAAe,CAAC,eACrBA,2BAAAA;AAAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,MAAM;AAAA,UACf,UAAU,MAAM;AAAA,UAChB,WAAWF,OAAAA,GAAG,kHAAkHC,gCAAgB;AAAA,UAE/I,UAAA,MAAM,qBAAqB,aAAa;AAAA,QAAA;AAAA,MAAA,IAEzC;AAAA,IAAA,EAAA,CACN;AAAA,EAAA,GACF;AAEJ;AAOO,SAAS,MAAM,OAAmB;AACvC,QAAM,OAAOW,oBAAAA,6BAAA;AACb,QAAM,OAAOtB,MAAAA,WAAWC,6CAAyB;AACjD,MAAI,CAAC,QAAQ,CAAC,KAAM,QAAO;AAC3B,SAAOW,2BAAAA,IAAC,WAAA,EAAW,GAAG,OAAO,KAAA,CAAY;AAC3C;AASA,SAAS,gBAAgB,EAAE,WAAW,gBAAgB,QAAQ,OAAO,eAAe,GAAG,cAAgC;AACrH,QAAM,CAAC,MAAM,OAAO,IAAIT,MAAAA,SAAS,KAAK;AACtC,QAAM,SAAS,0BAAA;AACf,SACEM,2BAAAA,KAACc,QAAAA,SAAA,EAAQ,MAAY,cAAc,SACjC,UAAA;AAAA,IAAAX,2BAAAA,IAACY,QAAAA,gBAAA,EAAe,SAAO,MACrB,UAAAf,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,cAAY,OAAO,QAAQ,IAAI,UAAU,OAAO,KAAK,YAAY;AAAA,QACjE,eAAY;AAAA,QACZ,WAAWC,OAAAA;AAAAA,UACT;AAAA,UACAC,gBAAAA;AAAAA,UACA;AAAA,QAAA;AAAA,QAGF,UAAA;AAAA,UAAAC,2BAAAA,IAACa,YAAAA,UAAA,EAAS,WAAU,UAAS,eAAW,MAAC;AAAA,UACxC,OAAO,QAAQ,IACdb,2BAAAA;AAAAA,YAAC;AAAA,YAAA;AAAA,cACC,eAAY;AAAA,cACZ,WAAU;AAAA,cAET,UAAA,OAAO;AAAA,YAAA;AAAA,UAAA,IAER;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAER;AAAA,mCACCc,QAAAA,gBAAA,EAAe,OAAc,WAAWhB,OAAAA,GAAG,uCAAuC,cAAc,GAC/F,UAAAE,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACE,GAAG;AAAA,QACJ,eAAe,CAAC,WAAW;AACzB,kBAAQ,KAAK;AACb,yDAAgB;AAAA,QAClB;AAAA,MAAA;AAAA,IAAA,EACF,CACF;AAAA,EAAA,GACF;AAEJ;AAMO,SAAS,YAAY,OAAyB;AACnD,QAAM,OAAOZ,MAAAA,WAAWC,6CAAyB;AACjD,MAAI,CAAC,KAAM,QAAO;AAClB,SAAOW,+BAAC,iBAAA,EAAiB,GAAG,MAAA,CAAO;AACrC;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"Inbox.cjs","sources":["../../src/threads/Inbox.tsx"],"sourcesContent":["/**\n * `Inbox` and `InboxButton` — the Devin-style notifications inbox over the\n * session list, with no endpoint of its own:\n *\n * - **What is in it.** The viewer's own sessions, newest activity first, minus\n * the ones dismissed from the inbox — the same set Athena's inbox shows —\n * read through `POST /api/conversations/threads/list` with\n * `created_by_me` + `exclude_dismissed` (+ `unread_only` for the Unread\n * tab). Each row is the session's title, its latest message, a state chip\n * (Needs you / Working / PR merged / Answered …) and an unread dot.\n * - **Live.** The rows fold the per-user conversation index like every other\n * list under the provider, so a session flipping to \"needs you\" or getting\n * a new message moves in place.\n * - **Writes.** Opening a row (or \"Mark read\") stores the read receipt through\n * `POST /api/conversations/threads/read-state`; \"Dismiss\" uses\n * `POST /api/conversations/threads/notifications/dismiss` (EDIT on the\n * session — a dismissed session comes back with its next message).\n * - **Counts.** The unread badge counts the unread page (50 rows); past that\n * it reads `50+`.\n *\n * Renders nothing outside `<AthenaProvider enableThreadList>`.\n */\nimport { BellIcon, CheckCheckIcon, MailIcon, MailOpenIcon, XIcon } from 'lucide-react';\nimport { useMutation } from '@tanstack/react-query';\nimport type { FC } from 'react';\nimport { useContext, useMemo, useState } from 'react';\nimport { cn } from '../lib/utils';\nimport { useAthenaQueryClient } from '../lib/use-athena-rest-query';\nimport { withCredentialRenewal } from '../runtime/credential-renewal';\nimport { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';\nimport { dismissThreadFromInbox } from './api';\nimport { SessionListSkeleton, type SessionOpenTarget } from './SessionList';\nimport { relativeTime } from './session-list-model';\nimport { FOCUS_RING_CLASS, RowIconButton, StatusChip } from './session-row-parts';\nimport { inboxStateChip, plainExcerpt } from './sessions-page-model';\nimport type { AthenaSessionsQuery } from './sessions-query';\nimport {\n AthenaSessionsLiveContext,\n type AthenaStatewireThreadListState,\n type AthenaStatewireThreadSummary,\n useAthenaStatewireThreadList,\n} from './statewire-thread-list';\nimport { type AthenaSessionsList, useAthenaSessions } from './use-athena-sessions';\n\n/** The inbox's sessions: the viewer's own, not dismissed, newest activity first. */\nexport const INBOX_QUERY: AthenaSessionsQuery = { createdByMe: true, excludeDismissed: true };\n/** The Unread tab, and what the badge counts. */\nexport const INBOX_UNREAD_QUERY: AthenaSessionsQuery = { ...INBOX_QUERY, unreadOnly: true };\n\n/** Rows per inbox page — and the badge's ceiling before it reads `50+`. */\nexport const INBOX_PAGE_SIZE = 50;\n\nexport type InboxTab = 'all' | 'unread';\n\nexport interface AthenaInboxUnreadCount {\n /** Unread sessions loaded (at most one page). */\n count: number;\n /** More unread sessions exist beyond `count`. */\n hasMore: boolean;\n /** `count` with `+` when there are more, `''` for none. */\n label: string;\n isAvailable: boolean;\n}\n\n/** The unread count the inbox badge shows; shares the Unread tab's cache. */\nexport function useAthenaInboxUnreadCount(): AthenaInboxUnreadCount {\n const unread = useAthenaSessions({ query: INBOX_UNREAD_QUERY, pageSize: INBOX_PAGE_SIZE });\n const count = unread.rows.length;\n const hasMore = unread.hasNextPage;\n return {\n count,\n hasMore,\n label: count === 0 ? '' : `${count}${hasMore ? '+' : ''}`,\n isAvailable: unread.isAvailable,\n };\n}\n\n/** Dismiss from the inbox: hidden at once, back on failure, the inbox lists refreshed after. */\nfunction useInboxDismiss(refresh: () => Promise<void>) {\n const live = useContext(AthenaSessionsLiveContext);\n const queryClient = useAthenaQueryClient();\n const [hidden, setHidden] = useState<ReadonlySet<string>>(() => new Set());\n const mutation = useMutation(\n {\n mutationKey: ['athena-sdk', 'inbox-dismiss', live?.backendUrl ?? '', live?.principal ?? ''],\n mutationFn: async (threadId: string) => {\n if (!live) throw new Error('[AthenaSDK] The inbox needs enableThreadList.');\n return withCredentialRenewal(live.getAuth, (auth) =>\n dismissThreadFromInbox(live.backendUrl, auth, { threadId, dismissed: true }),\n );\n },\n onMutate: (threadId) => setHidden((current) => new Set(current).add(threadId)),\n // The local hide only bridges the write and the refresh after it: once\n // the inbox has re-read, the server decides — a dismissed session comes\n // back with its next message, and a failed dismiss shows it again.\n onSettled: async (_result, _error, threadId) => {\n try {\n await refresh();\n } finally {\n setHidden((current) => {\n if (!current.has(threadId)) return current;\n const next = new Set(current);\n next.delete(threadId);\n return next;\n });\n }\n },\n },\n queryClient,\n );\n return { hidden, dismiss: mutation.mutate, pending: mutation.isPending };\n}\n\nconst InboxRow: FC<{\n row: AthenaStatewireThreadSummary;\n isActive: boolean;\n list: AthenaStatewireThreadListState;\n onOpen: (row: AthenaStatewireThreadSummary) => void;\n onDismiss: ((threadId: string) => void) | null;\n}> = ({ row, isActive, list, onOpen, onDismiss }) => {\n const chip = inboxStateChip(row);\n const excerpt = plainExcerpt(row.preview);\n return (\n <li\n data-thread-id={row.id}\n data-unread={row.isUnread || undefined}\n className={cn(\n 'group relative flex items-start gap-2 rounded-lg transition-colors hover:bg-muted/40',\n isActive && 'bg-muted',\n )}\n >\n <button\n type=\"button\"\n onClick={() => onOpen(row)}\n className={cn('flex min-w-0 flex-1 items-start gap-2 rounded-lg p-2 pl-2.5 text-left', FOCUS_RING_CLASS)}\n >\n <span\n role={row.isUnread ? 'img' : undefined}\n aria-label={row.isUnread ? 'Unread' : undefined}\n aria-hidden={row.isUnread ? undefined : true}\n className={cn('mt-1.5 size-2 shrink-0 rounded-full', row.isUnread ? 'bg-primary' : 'bg-transparent')}\n />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-center gap-2\">\n <span\n data-title\n className={cn('min-w-0 flex-1 truncate text-foreground text-sm', row.isUnread ? 'font-semibold' : 'font-medium')}\n >\n {row.title ?? 'Untitled'}\n </span>\n <span className=\"shrink-0 text-[11px] text-muted-foreground\">{relativeTime(row.updatedAt)}</span>\n </span>\n {excerpt ? (\n <span data-excerpt className=\"mt-0.5 line-clamp-2 break-words text-muted-foreground text-xs\">\n {excerpt}\n </span>\n ) : null}\n <span className=\"mt-1 flex\">\n <StatusChip label={chip.label} tone={chip.tone} detail={row.nextAction} />\n </span>\n </span>\n </button>\n <div data-row-actions className=\"flex shrink-0 items-center gap-0.5 py-2 pr-2\">\n <RowIconButton\n label={row.isUnread ? 'Mark read' : 'Mark unread'}\n action=\"read-state\"\n pending={list.readStatePendingThreadIds.has(row.id)}\n onClick={() =>\n list.setReadState({ threadId: row.id, isUnread: !row.isUnread, updatedAt: row.updatedAt })\n }\n >\n {row.isUnread ? <MailOpenIcon className=\"size-3.5\" aria-hidden /> : <MailIcon className=\"size-3.5\" aria-hidden />}\n </RowIconButton>\n {onDismiss ? (\n <RowIconButton label=\"Dismiss from inbox\" action=\"dismiss\" onClick={() => onDismiss(row.id)}>\n <XIcon className=\"size-3.5\" aria-hidden />\n </RowIconButton>\n ) : null}\n </div>\n </li>\n );\n};\n\nexport interface InboxProps {\n className?: string;\n /** Heading (default \"Inbox\"). */\n title?: string;\n /** The tab it opens on (default `all`). */\n defaultTab?: InboxTab;\n /** Called after a row is opened (selected and marked read), so a host can show its chat. */\n onOpenSession?: (target: SessionOpenTarget) => void;\n}\n\nfunction InboxBody({\n className,\n title = 'Inbox',\n defaultTab = 'all',\n onOpenSession,\n list,\n}: InboxProps & { list: AthenaStatewireThreadListState }) {\n const [tab, setTab] = useState<InboxTab>(defaultTab);\n const all = useAthenaSessions({ query: INBOX_QUERY, pageSize: INBOX_PAGE_SIZE });\n const unread = useAthenaSessions({ query: INBOX_UNREAD_QUERY, pageSize: INBOX_PAGE_SIZE });\n const shown: AthenaSessionsList = tab === 'unread' ? unread : all;\n const refreshBoth = async () => {\n await Promise.all([all.refresh(), unread.refresh()]);\n };\n const dismissal = useInboxDismiss(refreshBoth);\n // Dismissal is only offered where the server can leave dismissed sessions\n // out of the inbox; elsewhere a dismissed row would stay in the list.\n const canDismiss = shown.supportedFilters?.has('exclude_dismissed') ?? false;\n const rows = useMemo(\n () => shown.rows.filter((row) => !dismissal.hidden.has(row.id)),\n [shown.rows, dismissal.hidden],\n );\n const unreadRows = unread.rows.filter((row) => !dismissal.hidden.has(row.id));\n // A tab shows its count only once its list has answered: until then its\n // rows are empty because nothing has been read, not because there are none.\n const tabCount = ({ tabList, count }: { tabList: AthenaSessionsList; count: number }) =>\n tabList.hasResolvedFirstPage ? `${count}${tabList.hasNextPage ? '+' : ''}` : '';\n const unreadLabel = tabCount({ tabList: unread, count: unreadRows.length });\n const allLabel = tabCount({\n tabList: all,\n count: all.rows.filter((row) => !dismissal.hidden.has(row.id)).length,\n });\n\n const open = (row: AthenaStatewireThreadSummary) => {\n list.selectThread(row.id);\n if (row.isUnread) {\n list.setReadState({ threadId: row.id, isUnread: false, updatedAt: row.updatedAt });\n }\n onOpenSession?.({ threadId: row.id, title: row.title });\n };\n const markAllRead = () => {\n for (const row of unreadRows) {\n list.setReadState({ threadId: row.id, isUnread: false, updatedAt: row.updatedAt });\n }\n };\n const showSkeleton = rows.length === 0 && (!shown.hasResolvedFirstPage || shown.isFetching);\n const notice =\n shown.unavailableFields.length > 0\n ? 'This Athena server cannot narrow the inbox to your own, undismissed sessions yet, so it lists every session you can see.'\n : null;\n\n const tabClass = (active: boolean) =>\n cn(\n 'inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 font-medium text-xs transition-colors',\n active ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground',\n FOCUS_RING_CLASS,\n );\n\n return (\n <div data-testid=\"inbox\" className={cn('athena-sdk-chrome flex min-h-0 flex-col gap-2', className)}>\n <div className=\"flex items-center gap-2 px-1\">\n <h2 className=\"font-semibold text-foreground text-sm\">{title}</h2>\n <div role=\"tablist\" aria-label=\"Inbox\" className=\"flex items-center gap-0.5\">\n <button type=\"button\" role=\"tab\" aria-selected={tab === 'all'} onClick={() => setTab('all')} className={tabClass(tab === 'all')}>\n All\n <span data-testid=\"inbox-all-count\" className=\"text-muted-foreground\">\n {allLabel}\n </span>\n </button>\n <button type=\"button\" role=\"tab\" aria-selected={tab === 'unread'} onClick={() => setTab('unread')} className={tabClass(tab === 'unread')}>\n Unread\n <span data-testid=\"inbox-unread-count\" className=\"text-muted-foreground\">\n {unreadLabel}\n </span>\n </button>\n </div>\n <div className=\"flex-1\" />\n <button\n type=\"button\"\n onClick={markAllRead}\n disabled={unreadRows.length === 0}\n className={cn(\n 'inline-flex h-7 items-center gap-1 rounded-md px-2 text-muted-foreground text-xs transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50',\n FOCUS_RING_CLASS,\n )}\n title=\"Mark the loaded unread sessions read\"\n >\n <CheckCheckIcon className=\"size-3.5\" aria-hidden />\n Mark all read\n </button>\n </div>\n {notice ? (\n <p role=\"status\" data-testid=\"inbox-degraded\" className=\"mx-1 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5 text-[11px] text-amber-900\">\n {notice}\n </p>\n ) : null}\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {showSkeleton ? (\n <SessionListSkeleton rows={4} />\n ) : rows.length === 0 ? (\n <p data-testid=\"inbox-empty\" className=\"px-3 py-8 text-center text-muted-foreground text-xs\">\n {tab === 'unread' ? 'You are all caught up.' : 'Nothing in your inbox.'}\n </p>\n ) : (\n <ul className=\"flex flex-col gap-0.5\" aria-busy={shown.isFetching || undefined}>\n {rows.map((row) => (\n <InboxRow\n key={row.id}\n row={row}\n isActive={row.id === list.activeThreadId}\n list={list}\n onOpen={open}\n onDismiss={canDismiss ? dismissal.dismiss : null}\n />\n ))}\n </ul>\n )}\n {shown.hasNextPage && !showSkeleton ? (\n <button\n type=\"button\"\n onClick={shown.fetchNextPage}\n disabled={shown.isFetchingNextPage}\n className={cn('mt-1 w-full rounded-md py-2 text-muted-foreground text-xs transition-colors hover:bg-muted disabled:opacity-50', FOCUS_RING_CLASS)}\n >\n {shown.isFetchingNextPage ? 'Loading…' : 'Load more'}\n </button>\n ) : null}\n </div>\n </div>\n );\n}\n\n/**\n * The notifications inbox: All / Unread tabs with counts, rows of title,\n * latest message, state chip and unread dot, mark read / mark all read and\n * dismiss. Renders nothing outside `<AthenaProvider enableThreadList>`.\n */\nexport function Inbox(props: InboxProps) {\n const list = useAthenaStatewireThreadList();\n const live = useContext(AthenaSessionsLiveContext);\n if (!list || !live) return null;\n return <InboxBody {...props} list={list} />;\n}\n\nexport interface InboxButtonProps extends Omit<InboxProps, 'className'> {\n className?: string;\n /** Classes for the popover panel. */\n panelClassName?: string;\n align?: 'start' | 'center' | 'end';\n}\n\nfunction InboxButtonBody({ className, panelClassName, align = 'end', onOpenSession, ...inboxProps }: InboxButtonProps) {\n const [open, setOpen] = useState(false);\n const unread = useAthenaInboxUnreadCount();\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label={unread.count > 0 ? `Inbox, ${unread.label} unread` : 'Inbox'}\n data-testid=\"inbox-button\"\n className={cn(\n 'relative inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground',\n FOCUS_RING_CLASS,\n className,\n )}\n >\n <BellIcon className=\"size-4\" aria-hidden />\n {unread.count > 0 ? (\n <span\n data-testid=\"inbox-badge\"\n className=\"absolute -top-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 font-semibold text-[10px] text-primary-foreground leading-none\"\n >\n {unread.label}\n </span>\n ) : null}\n </button>\n </PopoverTrigger>\n <PopoverContent align={align} className={cn('flex max-h-[70vh] w-96 flex-col p-2', panelClassName)}>\n <Inbox\n {...inboxProps}\n onOpenSession={(target) => {\n setOpen(false);\n onOpenSession?.(target);\n }}\n />\n </PopoverContent>\n </Popover>\n );\n}\n\n/**\n * A bell with the unread count that opens `Inbox` in a popover. Renders\n * nothing outside `<AthenaProvider enableThreadList>`.\n */\nexport function InboxButton(props: InboxButtonProps) {\n const live = useContext(AthenaSessionsLiveContext);\n if (!live) return null;\n return <InboxButtonBody {...props} />;\n}\n"],"names":["useAthenaSessions","useContext","AthenaSessionsLiveContext","useAthenaQueryClient","useState","useMutation","withCredentialRenewal","dismissThreadFromInbox","inboxStateChip","plainExcerpt","jsxs","cn","FOCUS_RING_CLASS","jsx","relativeTime","StatusChip","RowIconButton","MailOpenIcon","MailIcon","XIcon","useMemo","CheckCheckIcon","SessionListSkeleton","useAthenaStatewireThreadList","Popover","PopoverTrigger","BellIcon","PopoverContent"],"mappings":"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"}
|
package/dist/threads/Inbox.js
CHANGED
|
@@ -148,8 +148,12 @@ function InboxBody({
|
|
|
148
148
|
[shown.rows, dismissal.hidden]
|
|
149
149
|
);
|
|
150
150
|
const unreadRows = unread.rows.filter((row) => !dismissal.hidden.has(row.id));
|
|
151
|
-
const
|
|
152
|
-
const
|
|
151
|
+
const tabCount = ({ tabList, count }) => tabList.hasResolvedFirstPage ? `${count}${tabList.hasNextPage ? "+" : ""}` : "";
|
|
152
|
+
const unreadLabel = tabCount({ tabList: unread, count: unreadRows.length });
|
|
153
|
+
const allLabel = tabCount({
|
|
154
|
+
tabList: all,
|
|
155
|
+
count: all.rows.filter((row) => !dismissal.hidden.has(row.id)).length
|
|
156
|
+
});
|
|
153
157
|
const open = (row) => {
|
|
154
158
|
list.selectThread(row.id);
|
|
155
159
|
if (row.isUnread) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Inbox.js","sources":["../../src/threads/Inbox.tsx"],"sourcesContent":["/**\n * `Inbox` and `InboxButton` — the Devin-style notifications inbox over the\n * session list, with no endpoint of its own:\n *\n * - **What is in it.** The viewer's own sessions, newest activity first, minus\n * the ones dismissed from the inbox — the same set Athena's inbox shows —\n * read through `POST /api/conversations/threads/list` with\n * `created_by_me` + `exclude_dismissed` (+ `unread_only` for the Unread\n * tab). Each row is the session's title, its latest message, a state chip\n * (Needs you / Working / PR merged / Answered …) and an unread dot.\n * - **Live.** The rows fold the per-user conversation index like every other\n * list under the provider, so a session flipping to \"needs you\" or getting\n * a new message moves in place.\n * - **Writes.** Opening a row (or \"Mark read\") stores the read receipt through\n * `POST /api/conversations/threads/read-state`; \"Dismiss\" uses\n * `POST /api/conversations/threads/notifications/dismiss` (EDIT on the\n * session — a dismissed session comes back with its next message).\n * - **Counts.** The unread badge counts the unread page (50 rows); past that\n * it reads `50+`.\n *\n * Renders nothing outside `<AthenaProvider enableThreadList>`.\n */\nimport { BellIcon, CheckCheckIcon, MailIcon, MailOpenIcon, XIcon } from 'lucide-react';\nimport { useMutation } from '@tanstack/react-query';\nimport type { FC } from 'react';\nimport { useContext, useMemo, useState } from 'react';\nimport { cn } from '../lib/utils';\nimport { useAthenaQueryClient } from '../lib/use-athena-rest-query';\nimport { withCredentialRenewal } from '../runtime/credential-renewal';\nimport { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';\nimport { dismissThreadFromInbox } from './api';\nimport { SessionListSkeleton, type SessionOpenTarget } from './SessionList';\nimport { relativeTime } from './session-list-model';\nimport { FOCUS_RING_CLASS, RowIconButton, StatusChip } from './session-row-parts';\nimport { inboxStateChip, plainExcerpt } from './sessions-page-model';\nimport type { AthenaSessionsQuery } from './sessions-query';\nimport {\n AthenaSessionsLiveContext,\n type AthenaStatewireThreadListState,\n type AthenaStatewireThreadSummary,\n useAthenaStatewireThreadList,\n} from './statewire-thread-list';\nimport { type AthenaSessionsList, useAthenaSessions } from './use-athena-sessions';\n\n/** The inbox's sessions: the viewer's own, not dismissed, newest activity first. */\nexport const INBOX_QUERY: AthenaSessionsQuery = { createdByMe: true, excludeDismissed: true };\n/** The Unread tab, and what the badge counts. */\nexport const INBOX_UNREAD_QUERY: AthenaSessionsQuery = { ...INBOX_QUERY, unreadOnly: true };\n\n/** Rows per inbox page — and the badge's ceiling before it reads `50+`. */\nexport const INBOX_PAGE_SIZE = 50;\n\nexport type InboxTab = 'all' | 'unread';\n\nexport interface AthenaInboxUnreadCount {\n /** Unread sessions loaded (at most one page). */\n count: number;\n /** More unread sessions exist beyond `count`. */\n hasMore: boolean;\n /** `count` with `+` when there are more, `''` for none. */\n label: string;\n isAvailable: boolean;\n}\n\n/** The unread count the inbox badge shows; shares the Unread tab's cache. */\nexport function useAthenaInboxUnreadCount(): AthenaInboxUnreadCount {\n const unread = useAthenaSessions({ query: INBOX_UNREAD_QUERY, pageSize: INBOX_PAGE_SIZE });\n const count = unread.rows.length;\n const hasMore = unread.hasNextPage;\n return {\n count,\n hasMore,\n label: count === 0 ? '' : `${count}${hasMore ? '+' : ''}`,\n isAvailable: unread.isAvailable,\n };\n}\n\n/** Dismiss from the inbox: hidden at once, back on failure, the inbox lists refreshed after. */\nfunction useInboxDismiss(refresh: () => Promise<void>) {\n const live = useContext(AthenaSessionsLiveContext);\n const queryClient = useAthenaQueryClient();\n const [hidden, setHidden] = useState<ReadonlySet<string>>(() => new Set());\n const mutation = useMutation(\n {\n mutationKey: ['athena-sdk', 'inbox-dismiss', live?.backendUrl ?? '', live?.principal ?? ''],\n mutationFn: async (threadId: string) => {\n if (!live) throw new Error('[AthenaSDK] The inbox needs enableThreadList.');\n return withCredentialRenewal(live.getAuth, (auth) =>\n dismissThreadFromInbox(live.backendUrl, auth, { threadId, dismissed: true }),\n );\n },\n onMutate: (threadId) => setHidden((current) => new Set(current).add(threadId)),\n // The local hide only bridges the write and the refresh after it: once\n // the inbox has re-read, the server decides — a dismissed session comes\n // back with its next message, and a failed dismiss shows it again.\n onSettled: async (_result, _error, threadId) => {\n try {\n await refresh();\n } finally {\n setHidden((current) => {\n if (!current.has(threadId)) return current;\n const next = new Set(current);\n next.delete(threadId);\n return next;\n });\n }\n },\n },\n queryClient,\n );\n return { hidden, dismiss: mutation.mutate, pending: mutation.isPending };\n}\n\nconst InboxRow: FC<{\n row: AthenaStatewireThreadSummary;\n isActive: boolean;\n list: AthenaStatewireThreadListState;\n onOpen: (row: AthenaStatewireThreadSummary) => void;\n onDismiss: ((threadId: string) => void) | null;\n}> = ({ row, isActive, list, onOpen, onDismiss }) => {\n const chip = inboxStateChip(row);\n const excerpt = plainExcerpt(row.preview);\n return (\n <li\n data-thread-id={row.id}\n data-unread={row.isUnread || undefined}\n className={cn(\n 'group relative flex items-start gap-2 rounded-lg transition-colors hover:bg-muted/40',\n isActive && 'bg-muted',\n )}\n >\n <button\n type=\"button\"\n onClick={() => onOpen(row)}\n className={cn('flex min-w-0 flex-1 items-start gap-2 rounded-lg p-2 pl-2.5 text-left', FOCUS_RING_CLASS)}\n >\n <span\n role={row.isUnread ? 'img' : undefined}\n aria-label={row.isUnread ? 'Unread' : undefined}\n aria-hidden={row.isUnread ? undefined : true}\n className={cn('mt-1.5 size-2 shrink-0 rounded-full', row.isUnread ? 'bg-primary' : 'bg-transparent')}\n />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-center gap-2\">\n <span\n data-title\n className={cn('min-w-0 flex-1 truncate text-foreground text-sm', row.isUnread ? 'font-semibold' : 'font-medium')}\n >\n {row.title ?? 'Untitled'}\n </span>\n <span className=\"shrink-0 text-[11px] text-muted-foreground\">{relativeTime(row.updatedAt)}</span>\n </span>\n {excerpt ? (\n <span data-excerpt className=\"mt-0.5 line-clamp-2 break-words text-muted-foreground text-xs\">\n {excerpt}\n </span>\n ) : null}\n <span className=\"mt-1 flex\">\n <StatusChip label={chip.label} tone={chip.tone} detail={row.nextAction} />\n </span>\n </span>\n </button>\n <div data-row-actions className=\"flex shrink-0 items-center gap-0.5 py-2 pr-2\">\n <RowIconButton\n label={row.isUnread ? 'Mark read' : 'Mark unread'}\n action=\"read-state\"\n pending={list.readStatePendingThreadIds.has(row.id)}\n onClick={() =>\n list.setReadState({ threadId: row.id, isUnread: !row.isUnread, updatedAt: row.updatedAt })\n }\n >\n {row.isUnread ? <MailOpenIcon className=\"size-3.5\" aria-hidden /> : <MailIcon className=\"size-3.5\" aria-hidden />}\n </RowIconButton>\n {onDismiss ? (\n <RowIconButton label=\"Dismiss from inbox\" action=\"dismiss\" onClick={() => onDismiss(row.id)}>\n <XIcon className=\"size-3.5\" aria-hidden />\n </RowIconButton>\n ) : null}\n </div>\n </li>\n );\n};\n\nexport interface InboxProps {\n className?: string;\n /** Heading (default \"Inbox\"). */\n title?: string;\n /** The tab it opens on (default `all`). */\n defaultTab?: InboxTab;\n /** Called after a row is opened (selected and marked read), so a host can show its chat. */\n onOpenSession?: (target: SessionOpenTarget) => void;\n}\n\nfunction InboxBody({\n className,\n title = 'Inbox',\n defaultTab = 'all',\n onOpenSession,\n list,\n}: InboxProps & { list: AthenaStatewireThreadListState }) {\n const [tab, setTab] = useState<InboxTab>(defaultTab);\n const all = useAthenaSessions({ query: INBOX_QUERY, pageSize: INBOX_PAGE_SIZE });\n const unread = useAthenaSessions({ query: INBOX_UNREAD_QUERY, pageSize: INBOX_PAGE_SIZE });\n const shown: AthenaSessionsList = tab === 'unread' ? unread : all;\n const refreshBoth = async () => {\n await Promise.all([all.refresh(), unread.refresh()]);\n };\n const dismissal = useInboxDismiss(refreshBoth);\n // Dismissal is only offered where the server can leave dismissed sessions\n // out of the inbox; elsewhere a dismissed row would stay in the list.\n const canDismiss = shown.supportedFilters?.has('exclude_dismissed') ?? false;\n const rows = useMemo(\n () => shown.rows.filter((row) => !dismissal.hidden.has(row.id)),\n [shown.rows, dismissal.hidden],\n );\n const unreadRows = unread.rows.filter((row) => !dismissal.hidden.has(row.id));\n const unreadLabel = `${unreadRows.length}${unread.hasNextPage ? '+' : ''}`;\n const allLabel = `${all.rows.filter((row) => !dismissal.hidden.has(row.id)).length}${all.hasNextPage ? '+' : ''}`;\n\n const open = (row: AthenaStatewireThreadSummary) => {\n list.selectThread(row.id);\n if (row.isUnread) {\n list.setReadState({ threadId: row.id, isUnread: false, updatedAt: row.updatedAt });\n }\n onOpenSession?.({ threadId: row.id, title: row.title });\n };\n const markAllRead = () => {\n for (const row of unreadRows) {\n list.setReadState({ threadId: row.id, isUnread: false, updatedAt: row.updatedAt });\n }\n };\n const showSkeleton = rows.length === 0 && (!shown.hasResolvedFirstPage || shown.isFetching);\n const notice =\n shown.unavailableFields.length > 0\n ? 'This Athena server cannot narrow the inbox to your own, undismissed sessions yet, so it lists every session you can see.'\n : null;\n\n const tabClass = (active: boolean) =>\n cn(\n 'inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 font-medium text-xs transition-colors',\n active ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground',\n FOCUS_RING_CLASS,\n );\n\n return (\n <div data-testid=\"inbox\" className={cn('athena-sdk-chrome flex min-h-0 flex-col gap-2', className)}>\n <div className=\"flex items-center gap-2 px-1\">\n <h2 className=\"font-semibold text-foreground text-sm\">{title}</h2>\n <div role=\"tablist\" aria-label=\"Inbox\" className=\"flex items-center gap-0.5\">\n <button type=\"button\" role=\"tab\" aria-selected={tab === 'all'} onClick={() => setTab('all')} className={tabClass(tab === 'all')}>\n All\n <span data-testid=\"inbox-all-count\" className=\"text-muted-foreground\">\n {allLabel}\n </span>\n </button>\n <button type=\"button\" role=\"tab\" aria-selected={tab === 'unread'} onClick={() => setTab('unread')} className={tabClass(tab === 'unread')}>\n Unread\n <span data-testid=\"inbox-unread-count\" className=\"text-muted-foreground\">\n {unreadLabel}\n </span>\n </button>\n </div>\n <div className=\"flex-1\" />\n <button\n type=\"button\"\n onClick={markAllRead}\n disabled={unreadRows.length === 0}\n className={cn(\n 'inline-flex h-7 items-center gap-1 rounded-md px-2 text-muted-foreground text-xs transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50',\n FOCUS_RING_CLASS,\n )}\n title=\"Mark the loaded unread sessions read\"\n >\n <CheckCheckIcon className=\"size-3.5\" aria-hidden />\n Mark all read\n </button>\n </div>\n {notice ? (\n <p role=\"status\" data-testid=\"inbox-degraded\" className=\"mx-1 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5 text-[11px] text-amber-900\">\n {notice}\n </p>\n ) : null}\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {showSkeleton ? (\n <SessionListSkeleton rows={4} />\n ) : rows.length === 0 ? (\n <p data-testid=\"inbox-empty\" className=\"px-3 py-8 text-center text-muted-foreground text-xs\">\n {tab === 'unread' ? 'You are all caught up.' : 'Nothing in your inbox.'}\n </p>\n ) : (\n <ul className=\"flex flex-col gap-0.5\" aria-busy={shown.isFetching || undefined}>\n {rows.map((row) => (\n <InboxRow\n key={row.id}\n row={row}\n isActive={row.id === list.activeThreadId}\n list={list}\n onOpen={open}\n onDismiss={canDismiss ? dismissal.dismiss : null}\n />\n ))}\n </ul>\n )}\n {shown.hasNextPage && !showSkeleton ? (\n <button\n type=\"button\"\n onClick={shown.fetchNextPage}\n disabled={shown.isFetchingNextPage}\n className={cn('mt-1 w-full rounded-md py-2 text-muted-foreground text-xs transition-colors hover:bg-muted disabled:opacity-50', FOCUS_RING_CLASS)}\n >\n {shown.isFetchingNextPage ? 'Loading…' : 'Load more'}\n </button>\n ) : null}\n </div>\n </div>\n );\n}\n\n/**\n * The notifications inbox: All / Unread tabs with counts, rows of title,\n * latest message, state chip and unread dot, mark read / mark all read and\n * dismiss. Renders nothing outside `<AthenaProvider enableThreadList>`.\n */\nexport function Inbox(props: InboxProps) {\n const list = useAthenaStatewireThreadList();\n const live = useContext(AthenaSessionsLiveContext);\n if (!list || !live) return null;\n return <InboxBody {...props} list={list} />;\n}\n\nexport interface InboxButtonProps extends Omit<InboxProps, 'className'> {\n className?: string;\n /** Classes for the popover panel. */\n panelClassName?: string;\n align?: 'start' | 'center' | 'end';\n}\n\nfunction InboxButtonBody({ className, panelClassName, align = 'end', onOpenSession, ...inboxProps }: InboxButtonProps) {\n const [open, setOpen] = useState(false);\n const unread = useAthenaInboxUnreadCount();\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label={unread.count > 0 ? `Inbox, ${unread.label} unread` : 'Inbox'}\n data-testid=\"inbox-button\"\n className={cn(\n 'relative inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground',\n FOCUS_RING_CLASS,\n className,\n )}\n >\n <BellIcon className=\"size-4\" aria-hidden />\n {unread.count > 0 ? (\n <span\n data-testid=\"inbox-badge\"\n className=\"absolute -top-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 font-semibold text-[10px] text-primary-foreground leading-none\"\n >\n {unread.label}\n </span>\n ) : null}\n </button>\n </PopoverTrigger>\n <PopoverContent align={align} className={cn('flex max-h-[70vh] w-96 flex-col p-2', panelClassName)}>\n <Inbox\n {...inboxProps}\n onOpenSession={(target) => {\n setOpen(false);\n onOpenSession?.(target);\n }}\n />\n </PopoverContent>\n </Popover>\n );\n}\n\n/**\n * A bell with the unread count that opens `Inbox` in a popover. Renders\n * nothing outside `<AthenaProvider enableThreadList>`.\n */\nexport function InboxButton(props: InboxButtonProps) {\n const live = useContext(AthenaSessionsLiveContext);\n if (!live) return null;\n return <InboxButtonBody {...props} />;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Inbox.js","sources":["../../src/threads/Inbox.tsx"],"sourcesContent":["/**\n * `Inbox` and `InboxButton` — the Devin-style notifications inbox over the\n * session list, with no endpoint of its own:\n *\n * - **What is in it.** The viewer's own sessions, newest activity first, minus\n * the ones dismissed from the inbox — the same set Athena's inbox shows —\n * read through `POST /api/conversations/threads/list` with\n * `created_by_me` + `exclude_dismissed` (+ `unread_only` for the Unread\n * tab). Each row is the session's title, its latest message, a state chip\n * (Needs you / Working / PR merged / Answered …) and an unread dot.\n * - **Live.** The rows fold the per-user conversation index like every other\n * list under the provider, so a session flipping to \"needs you\" or getting\n * a new message moves in place.\n * - **Writes.** Opening a row (or \"Mark read\") stores the read receipt through\n * `POST /api/conversations/threads/read-state`; \"Dismiss\" uses\n * `POST /api/conversations/threads/notifications/dismiss` (EDIT on the\n * session — a dismissed session comes back with its next message).\n * - **Counts.** The unread badge counts the unread page (50 rows); past that\n * it reads `50+`.\n *\n * Renders nothing outside `<AthenaProvider enableThreadList>`.\n */\nimport { BellIcon, CheckCheckIcon, MailIcon, MailOpenIcon, XIcon } from 'lucide-react';\nimport { useMutation } from '@tanstack/react-query';\nimport type { FC } from 'react';\nimport { useContext, useMemo, useState } from 'react';\nimport { cn } from '../lib/utils';\nimport { useAthenaQueryClient } from '../lib/use-athena-rest-query';\nimport { withCredentialRenewal } from '../runtime/credential-renewal';\nimport { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';\nimport { dismissThreadFromInbox } from './api';\nimport { SessionListSkeleton, type SessionOpenTarget } from './SessionList';\nimport { relativeTime } from './session-list-model';\nimport { FOCUS_RING_CLASS, RowIconButton, StatusChip } from './session-row-parts';\nimport { inboxStateChip, plainExcerpt } from './sessions-page-model';\nimport type { AthenaSessionsQuery } from './sessions-query';\nimport {\n AthenaSessionsLiveContext,\n type AthenaStatewireThreadListState,\n type AthenaStatewireThreadSummary,\n useAthenaStatewireThreadList,\n} from './statewire-thread-list';\nimport { type AthenaSessionsList, useAthenaSessions } from './use-athena-sessions';\n\n/** The inbox's sessions: the viewer's own, not dismissed, newest activity first. */\nexport const INBOX_QUERY: AthenaSessionsQuery = { createdByMe: true, excludeDismissed: true };\n/** The Unread tab, and what the badge counts. */\nexport const INBOX_UNREAD_QUERY: AthenaSessionsQuery = { ...INBOX_QUERY, unreadOnly: true };\n\n/** Rows per inbox page — and the badge's ceiling before it reads `50+`. */\nexport const INBOX_PAGE_SIZE = 50;\n\nexport type InboxTab = 'all' | 'unread';\n\nexport interface AthenaInboxUnreadCount {\n /** Unread sessions loaded (at most one page). */\n count: number;\n /** More unread sessions exist beyond `count`. */\n hasMore: boolean;\n /** `count` with `+` when there are more, `''` for none. */\n label: string;\n isAvailable: boolean;\n}\n\n/** The unread count the inbox badge shows; shares the Unread tab's cache. */\nexport function useAthenaInboxUnreadCount(): AthenaInboxUnreadCount {\n const unread = useAthenaSessions({ query: INBOX_UNREAD_QUERY, pageSize: INBOX_PAGE_SIZE });\n const count = unread.rows.length;\n const hasMore = unread.hasNextPage;\n return {\n count,\n hasMore,\n label: count === 0 ? '' : `${count}${hasMore ? '+' : ''}`,\n isAvailable: unread.isAvailable,\n };\n}\n\n/** Dismiss from the inbox: hidden at once, back on failure, the inbox lists refreshed after. */\nfunction useInboxDismiss(refresh: () => Promise<void>) {\n const live = useContext(AthenaSessionsLiveContext);\n const queryClient = useAthenaQueryClient();\n const [hidden, setHidden] = useState<ReadonlySet<string>>(() => new Set());\n const mutation = useMutation(\n {\n mutationKey: ['athena-sdk', 'inbox-dismiss', live?.backendUrl ?? '', live?.principal ?? ''],\n mutationFn: async (threadId: string) => {\n if (!live) throw new Error('[AthenaSDK] The inbox needs enableThreadList.');\n return withCredentialRenewal(live.getAuth, (auth) =>\n dismissThreadFromInbox(live.backendUrl, auth, { threadId, dismissed: true }),\n );\n },\n onMutate: (threadId) => setHidden((current) => new Set(current).add(threadId)),\n // The local hide only bridges the write and the refresh after it: once\n // the inbox has re-read, the server decides — a dismissed session comes\n // back with its next message, and a failed dismiss shows it again.\n onSettled: async (_result, _error, threadId) => {\n try {\n await refresh();\n } finally {\n setHidden((current) => {\n if (!current.has(threadId)) return current;\n const next = new Set(current);\n next.delete(threadId);\n return next;\n });\n }\n },\n },\n queryClient,\n );\n return { hidden, dismiss: mutation.mutate, pending: mutation.isPending };\n}\n\nconst InboxRow: FC<{\n row: AthenaStatewireThreadSummary;\n isActive: boolean;\n list: AthenaStatewireThreadListState;\n onOpen: (row: AthenaStatewireThreadSummary) => void;\n onDismiss: ((threadId: string) => void) | null;\n}> = ({ row, isActive, list, onOpen, onDismiss }) => {\n const chip = inboxStateChip(row);\n const excerpt = plainExcerpt(row.preview);\n return (\n <li\n data-thread-id={row.id}\n data-unread={row.isUnread || undefined}\n className={cn(\n 'group relative flex items-start gap-2 rounded-lg transition-colors hover:bg-muted/40',\n isActive && 'bg-muted',\n )}\n >\n <button\n type=\"button\"\n onClick={() => onOpen(row)}\n className={cn('flex min-w-0 flex-1 items-start gap-2 rounded-lg p-2 pl-2.5 text-left', FOCUS_RING_CLASS)}\n >\n <span\n role={row.isUnread ? 'img' : undefined}\n aria-label={row.isUnread ? 'Unread' : undefined}\n aria-hidden={row.isUnread ? undefined : true}\n className={cn('mt-1.5 size-2 shrink-0 rounded-full', row.isUnread ? 'bg-primary' : 'bg-transparent')}\n />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-center gap-2\">\n <span\n data-title\n className={cn('min-w-0 flex-1 truncate text-foreground text-sm', row.isUnread ? 'font-semibold' : 'font-medium')}\n >\n {row.title ?? 'Untitled'}\n </span>\n <span className=\"shrink-0 text-[11px] text-muted-foreground\">{relativeTime(row.updatedAt)}</span>\n </span>\n {excerpt ? (\n <span data-excerpt className=\"mt-0.5 line-clamp-2 break-words text-muted-foreground text-xs\">\n {excerpt}\n </span>\n ) : null}\n <span className=\"mt-1 flex\">\n <StatusChip label={chip.label} tone={chip.tone} detail={row.nextAction} />\n </span>\n </span>\n </button>\n <div data-row-actions className=\"flex shrink-0 items-center gap-0.5 py-2 pr-2\">\n <RowIconButton\n label={row.isUnread ? 'Mark read' : 'Mark unread'}\n action=\"read-state\"\n pending={list.readStatePendingThreadIds.has(row.id)}\n onClick={() =>\n list.setReadState({ threadId: row.id, isUnread: !row.isUnread, updatedAt: row.updatedAt })\n }\n >\n {row.isUnread ? <MailOpenIcon className=\"size-3.5\" aria-hidden /> : <MailIcon className=\"size-3.5\" aria-hidden />}\n </RowIconButton>\n {onDismiss ? (\n <RowIconButton label=\"Dismiss from inbox\" action=\"dismiss\" onClick={() => onDismiss(row.id)}>\n <XIcon className=\"size-3.5\" aria-hidden />\n </RowIconButton>\n ) : null}\n </div>\n </li>\n );\n};\n\nexport interface InboxProps {\n className?: string;\n /** Heading (default \"Inbox\"). */\n title?: string;\n /** The tab it opens on (default `all`). */\n defaultTab?: InboxTab;\n /** Called after a row is opened (selected and marked read), so a host can show its chat. */\n onOpenSession?: (target: SessionOpenTarget) => void;\n}\n\nfunction InboxBody({\n className,\n title = 'Inbox',\n defaultTab = 'all',\n onOpenSession,\n list,\n}: InboxProps & { list: AthenaStatewireThreadListState }) {\n const [tab, setTab] = useState<InboxTab>(defaultTab);\n const all = useAthenaSessions({ query: INBOX_QUERY, pageSize: INBOX_PAGE_SIZE });\n const unread = useAthenaSessions({ query: INBOX_UNREAD_QUERY, pageSize: INBOX_PAGE_SIZE });\n const shown: AthenaSessionsList = tab === 'unread' ? unread : all;\n const refreshBoth = async () => {\n await Promise.all([all.refresh(), unread.refresh()]);\n };\n const dismissal = useInboxDismiss(refreshBoth);\n // Dismissal is only offered where the server can leave dismissed sessions\n // out of the inbox; elsewhere a dismissed row would stay in the list.\n const canDismiss = shown.supportedFilters?.has('exclude_dismissed') ?? false;\n const rows = useMemo(\n () => shown.rows.filter((row) => !dismissal.hidden.has(row.id)),\n [shown.rows, dismissal.hidden],\n );\n const unreadRows = unread.rows.filter((row) => !dismissal.hidden.has(row.id));\n // A tab shows its count only once its list has answered: until then its\n // rows are empty because nothing has been read, not because there are none.\n const tabCount = ({ tabList, count }: { tabList: AthenaSessionsList; count: number }) =>\n tabList.hasResolvedFirstPage ? `${count}${tabList.hasNextPage ? '+' : ''}` : '';\n const unreadLabel = tabCount({ tabList: unread, count: unreadRows.length });\n const allLabel = tabCount({\n tabList: all,\n count: all.rows.filter((row) => !dismissal.hidden.has(row.id)).length,\n });\n\n const open = (row: AthenaStatewireThreadSummary) => {\n list.selectThread(row.id);\n if (row.isUnread) {\n list.setReadState({ threadId: row.id, isUnread: false, updatedAt: row.updatedAt });\n }\n onOpenSession?.({ threadId: row.id, title: row.title });\n };\n const markAllRead = () => {\n for (const row of unreadRows) {\n list.setReadState({ threadId: row.id, isUnread: false, updatedAt: row.updatedAt });\n }\n };\n const showSkeleton = rows.length === 0 && (!shown.hasResolvedFirstPage || shown.isFetching);\n const notice =\n shown.unavailableFields.length > 0\n ? 'This Athena server cannot narrow the inbox to your own, undismissed sessions yet, so it lists every session you can see.'\n : null;\n\n const tabClass = (active: boolean) =>\n cn(\n 'inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 font-medium text-xs transition-colors',\n active ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground',\n FOCUS_RING_CLASS,\n );\n\n return (\n <div data-testid=\"inbox\" className={cn('athena-sdk-chrome flex min-h-0 flex-col gap-2', className)}>\n <div className=\"flex items-center gap-2 px-1\">\n <h2 className=\"font-semibold text-foreground text-sm\">{title}</h2>\n <div role=\"tablist\" aria-label=\"Inbox\" className=\"flex items-center gap-0.5\">\n <button type=\"button\" role=\"tab\" aria-selected={tab === 'all'} onClick={() => setTab('all')} className={tabClass(tab === 'all')}>\n All\n <span data-testid=\"inbox-all-count\" className=\"text-muted-foreground\">\n {allLabel}\n </span>\n </button>\n <button type=\"button\" role=\"tab\" aria-selected={tab === 'unread'} onClick={() => setTab('unread')} className={tabClass(tab === 'unread')}>\n Unread\n <span data-testid=\"inbox-unread-count\" className=\"text-muted-foreground\">\n {unreadLabel}\n </span>\n </button>\n </div>\n <div className=\"flex-1\" />\n <button\n type=\"button\"\n onClick={markAllRead}\n disabled={unreadRows.length === 0}\n className={cn(\n 'inline-flex h-7 items-center gap-1 rounded-md px-2 text-muted-foreground text-xs transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50',\n FOCUS_RING_CLASS,\n )}\n title=\"Mark the loaded unread sessions read\"\n >\n <CheckCheckIcon className=\"size-3.5\" aria-hidden />\n Mark all read\n </button>\n </div>\n {notice ? (\n <p role=\"status\" data-testid=\"inbox-degraded\" className=\"mx-1 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5 text-[11px] text-amber-900\">\n {notice}\n </p>\n ) : null}\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {showSkeleton ? (\n <SessionListSkeleton rows={4} />\n ) : rows.length === 0 ? (\n <p data-testid=\"inbox-empty\" className=\"px-3 py-8 text-center text-muted-foreground text-xs\">\n {tab === 'unread' ? 'You are all caught up.' : 'Nothing in your inbox.'}\n </p>\n ) : (\n <ul className=\"flex flex-col gap-0.5\" aria-busy={shown.isFetching || undefined}>\n {rows.map((row) => (\n <InboxRow\n key={row.id}\n row={row}\n isActive={row.id === list.activeThreadId}\n list={list}\n onOpen={open}\n onDismiss={canDismiss ? dismissal.dismiss : null}\n />\n ))}\n </ul>\n )}\n {shown.hasNextPage && !showSkeleton ? (\n <button\n type=\"button\"\n onClick={shown.fetchNextPage}\n disabled={shown.isFetchingNextPage}\n className={cn('mt-1 w-full rounded-md py-2 text-muted-foreground text-xs transition-colors hover:bg-muted disabled:opacity-50', FOCUS_RING_CLASS)}\n >\n {shown.isFetchingNextPage ? 'Loading…' : 'Load more'}\n </button>\n ) : null}\n </div>\n </div>\n );\n}\n\n/**\n * The notifications inbox: All / Unread tabs with counts, rows of title,\n * latest message, state chip and unread dot, mark read / mark all read and\n * dismiss. Renders nothing outside `<AthenaProvider enableThreadList>`.\n */\nexport function Inbox(props: InboxProps) {\n const list = useAthenaStatewireThreadList();\n const live = useContext(AthenaSessionsLiveContext);\n if (!list || !live) return null;\n return <InboxBody {...props} list={list} />;\n}\n\nexport interface InboxButtonProps extends Omit<InboxProps, 'className'> {\n className?: string;\n /** Classes for the popover panel. */\n panelClassName?: string;\n align?: 'start' | 'center' | 'end';\n}\n\nfunction InboxButtonBody({ className, panelClassName, align = 'end', onOpenSession, ...inboxProps }: InboxButtonProps) {\n const [open, setOpen] = useState(false);\n const unread = useAthenaInboxUnreadCount();\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label={unread.count > 0 ? `Inbox, ${unread.label} unread` : 'Inbox'}\n data-testid=\"inbox-button\"\n className={cn(\n 'relative inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground',\n FOCUS_RING_CLASS,\n className,\n )}\n >\n <BellIcon className=\"size-4\" aria-hidden />\n {unread.count > 0 ? (\n <span\n data-testid=\"inbox-badge\"\n className=\"absolute -top-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 font-semibold text-[10px] text-primary-foreground leading-none\"\n >\n {unread.label}\n </span>\n ) : null}\n </button>\n </PopoverTrigger>\n <PopoverContent align={align} className={cn('flex max-h-[70vh] w-96 flex-col p-2', panelClassName)}>\n <Inbox\n {...inboxProps}\n onOpenSession={(target) => {\n setOpen(false);\n onOpenSession?.(target);\n }}\n />\n </PopoverContent>\n </Popover>\n );\n}\n\n/**\n * A bell with the unread count that opens `Inbox` in a popover. Renders\n * nothing outside `<AthenaProvider enableThreadList>`.\n */\nexport function InboxButton(props: InboxButtonProps) {\n const live = useContext(AthenaSessionsLiveContext);\n if (!live) return null;\n return <InboxButtonBody {...props} />;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
const react$1 = require("@assistant-ui/react");
|
|
5
|
+
const react = require("react");
|
|
6
|
+
const AthenaAuthContext = require("../auth/AthenaAuthContext.cjs");
|
|
7
|
+
const lib_ownerDocument = require("../lib/owner-document.cjs");
|
|
8
|
+
const useDocumentActive = require("../lib/use-document-active.cjs");
|
|
9
|
+
const statewireThreadList = require("./statewire-thread-list.cjs");
|
|
10
|
+
const threadIdContext = require("./thread-id-context.cjs");
|
|
11
|
+
const MARK_VIEWED_THREAD_READ_DELAY_MS = 1e3;
|
|
12
|
+
function lastMessageKey(messages) {
|
|
13
|
+
const last = messages == null ? void 0 : messages.at(-1);
|
|
14
|
+
return last ? `${last.id}#${last.content.length}` : null;
|
|
15
|
+
}
|
|
16
|
+
function useOnScreen(element) {
|
|
17
|
+
const [onScreen, setOnScreen] = react.useState(true);
|
|
18
|
+
react.useEffect(() => {
|
|
19
|
+
var _a;
|
|
20
|
+
const Observer = (_a = element == null ? void 0 : element.ownerDocument.defaultView) == null ? void 0 : _a.IntersectionObserver;
|
|
21
|
+
if (!element || !Observer) return;
|
|
22
|
+
const observer = new Observer((entries) => {
|
|
23
|
+
const latest = entries.at(-1);
|
|
24
|
+
if (latest) setOnScreen(latest.isIntersecting);
|
|
25
|
+
});
|
|
26
|
+
observer.observe(element);
|
|
27
|
+
return () => observer.disconnect();
|
|
28
|
+
}, [element]);
|
|
29
|
+
return onScreen;
|
|
30
|
+
}
|
|
31
|
+
function MarkViewedThreadRead({
|
|
32
|
+
delayMs = MARK_VIEWED_THREAD_READ_DELAY_MS
|
|
33
|
+
}) {
|
|
34
|
+
var _a, _b, _c;
|
|
35
|
+
const list = statewireThreadList.useAthenaStatewireThreadList();
|
|
36
|
+
const threadId = react.useContext(threadIdContext.AthenaStatewireThreadIdContext);
|
|
37
|
+
const principal = ((_b = (_a = react.useContext(AthenaAuthContext.AthenaAuthContext)) == null ? void 0 : _a.user) == null ? void 0 : _b.userId) ?? "";
|
|
38
|
+
const messageKey = react$1.useAuiState((s) => {
|
|
39
|
+
var _a2;
|
|
40
|
+
return lastMessageKey((_a2 = s.optional.thread) == null ? void 0 : _a2.messages);
|
|
41
|
+
});
|
|
42
|
+
const isRunning = react$1.useAuiState((s) => {
|
|
43
|
+
var _a2;
|
|
44
|
+
return ((_a2 = s.optional.thread) == null ? void 0 : _a2.isRunning) ?? false;
|
|
45
|
+
});
|
|
46
|
+
const windowActive = useDocumentActive.useDocumentActive(lib_ownerDocument.useOwnerDocument());
|
|
47
|
+
const [sentinel, setSentinel] = react.useState(null);
|
|
48
|
+
const onScreen = useOnScreen(sentinel);
|
|
49
|
+
const inView = windowActive && onScreen;
|
|
50
|
+
const row = threadId !== null && ((_c = list == null ? void 0 : list.activeThread) == null ? void 0 : _c.id) === threadId ? list.activeThread : null;
|
|
51
|
+
const isUnread = (row == null ? void 0 : row.isUnread) ?? false;
|
|
52
|
+
const writeInFlight = threadId !== null && ((list == null ? void 0 : list.readStatePendingThreadIds.has(threadId)) ?? false);
|
|
53
|
+
const setReadState = list == null ? void 0 : list.setReadState;
|
|
54
|
+
const visitKey = `${principal}\0${threadId ?? ""}`;
|
|
55
|
+
const rowRef = react.useRef(row);
|
|
56
|
+
rowRef.current = row;
|
|
57
|
+
const visitRef = react.useRef({
|
|
58
|
+
key: visitKey,
|
|
59
|
+
settledMessageKey: null,
|
|
60
|
+
inFlightMessageKey: null
|
|
61
|
+
});
|
|
62
|
+
react.useEffect(() => {
|
|
63
|
+
if (visitRef.current.key !== visitKey) {
|
|
64
|
+
visitRef.current = { key: visitKey, settledMessageKey: null, inFlightMessageKey: null };
|
|
65
|
+
}
|
|
66
|
+
const visit = visitRef.current;
|
|
67
|
+
if (threadId === null || setReadState === void 0 || messageKey === null) return;
|
|
68
|
+
if (writeInFlight) {
|
|
69
|
+
if (visit.inFlightMessageKey === null) visit.inFlightMessageKey = messageKey;
|
|
70
|
+
visit.settledMessageKey = visit.inFlightMessageKey;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
visit.inFlightMessageKey = null;
|
|
74
|
+
if (visit.settledMessageKey === messageKey) return;
|
|
75
|
+
if (!inView || isRunning || !isUnread) return;
|
|
76
|
+
const timer = setTimeout(() => {
|
|
77
|
+
const current = rowRef.current;
|
|
78
|
+
if (current === null || !current.isUnread) return;
|
|
79
|
+
visit.settledMessageKey = messageKey;
|
|
80
|
+
setReadState({ threadId, isUnread: false, updatedAt: current.updatedAt });
|
|
81
|
+
}, delayMs);
|
|
82
|
+
return () => clearTimeout(timer);
|
|
83
|
+
}, [visitKey, threadId, messageKey, writeInFlight, inView, isRunning, isUnread, setReadState, delayMs]);
|
|
84
|
+
if (list === null) return null;
|
|
85
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
86
|
+
"span",
|
|
87
|
+
{
|
|
88
|
+
ref: setSentinel,
|
|
89
|
+
"aria-hidden": "true",
|
|
90
|
+
"data-slot": "viewed-thread-sentinel",
|
|
91
|
+
className: "pointer-events-none absolute inset-0"
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
exports.MARK_VIEWED_THREAD_READ_DELAY_MS = MARK_VIEWED_THREAD_READ_DELAY_MS;
|
|
96
|
+
exports.MarkViewedThreadRead = MarkViewedThreadRead;
|
|
97
|
+
//# sourceMappingURL=mark-viewed-thread-read.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mark-viewed-thread-read.cjs","sources":["../../src/threads/mark-viewed-thread-read.tsx"],"sourcesContent":["/**\n * `MarkViewedThreadRead` — the session `AthenaChat` shows is marked read for\n * the viewer, as Olympus marks every session it shows\n * (`useMarkCurrentThreadAsRead`) and Devin clears a session's unread dot when\n * it is opened. Without it, a session opened from the session list or a link\n * stayed unread for good: only the inbox's own open and its Mark read wrote\n * the receipt, so unread dots and the inbox's Unread count only grew.\n *\n * The open session is marked read once all of these hold:\n *\n * - the transcript is in front of the person: its window is visible and\n * focused (the window it renders into, a pop-out's included), and the chat\n * itself is on screen (not hidden, as `AthenaLayout` hides it behind a\n * maximized workbench, nor scrolled out of the page);\n * - its history is on screen (the thread has a last message);\n * - no run is going (a running session is marked when its run ends, never per\n * streamed chunk);\n * - its row is unread (a read row is never written).\n *\n * They must hold for {@link MARK_VIEWED_THREAD_READ_DELAY_MS}; the write is\n * the thread list's `setReadState` (`POST /api/conversations/threads/read-state`),\n * with the row's `updatedAt`, so the next message ends the override and shows\n * the row unread again. One write per (viewer, session, last message as it\n * stands — its id and how many parts it has, since the agent's further\n * messages in a turn join that turn's message): what arrives while the person\n * watches is marked too, nothing polls, and a failed write is not repeated\n * for the same message. A read-state write in flight for the open session\n * (this one's, or the person's own Mark read / Mark unread) settles the\n * message that was on screen when it began, so the person's choice stands\n * until something new arrives or they open the session again.\n *\n * Renders an invisible, inert sentinel over the chat panel (it must sit in\n * the panel, which is `relative`); nothing outside\n * `<AthenaProvider enableThreadList>`.\n */\nimport { useAuiState } from '@assistant-ui/react';\nimport { type ReactNode, useContext, useEffect, useRef, useState } from 'react';\nimport { AthenaAuthContext } from '../auth/AthenaAuthContext';\nimport { useOwnerDocument } from '../lib/owner-document';\nimport { useDocumentActive } from '../lib/use-document-active';\nimport { useAthenaStatewireThreadList } from './statewire-thread-list';\nimport { AthenaStatewireThreadIdContext } from './thread-id-context';\n\n/** How long the open session must stay unread and in view before it is marked read. */\nexport const MARK_VIEWED_THREAD_READ_DELAY_MS = 1_000;\n\ninterface ViewedThreadVisit {\n /** Who views which session: the provider's read state is scoped to the viewer. */\n key: string;\n /** The last message (as {@link lastMessageKey} reads it) this visit has settled. */\n settledMessageKey: string | null;\n /** The message on screen when the read-state write now in flight began. */\n inFlightMessageKey: string | null;\n}\n\n/**\n * The thread's last message as it stands: its id and part count. A turn's\n * further assistant messages join that turn's message, so its id alone does\n * not move when the agent writes more; the part count does — once per part,\n * never per streamed token. `null` until the history is on screen.\n */\nfunction lastMessageKey(\n messages: readonly { readonly id: string; readonly content: readonly unknown[] }[] | undefined,\n): string | null {\n const last = messages?.at(-1);\n return last ? `${last.id}#${last.content.length}` : null;\n}\n\n/**\n * Whether `element` is on screen: laid out (no `display: none` on it or an\n * ancestor) and inside the viewport, read by an IntersectionObserver of the\n * element's own window. `true` until the observer's first answer, which\n * arrives within a frame, and where there is no observer.\n */\nfunction useOnScreen(element: Element | null): boolean {\n const [onScreen, setOnScreen] = useState(true);\n useEffect(() => {\n const Observer = element?.ownerDocument.defaultView?.IntersectionObserver;\n if (!element || !Observer) return;\n const observer = new Observer((entries) => {\n const latest = entries.at(-1);\n if (latest) setOnScreen(latest.isIntersecting);\n });\n observer.observe(element);\n return () => observer.disconnect();\n }, [element]);\n return onScreen;\n}\n\nexport function MarkViewedThreadRead({\n delayMs = MARK_VIEWED_THREAD_READ_DELAY_MS,\n}: {\n /** How long the conditions must hold before the write. */\n delayMs?: number;\n}): ReactNode {\n const list = useAthenaStatewireThreadList();\n // The thread the statewire scope publishes WITH its client, so the messages\n // read here are always this thread's: the list's selection runs one commit\n // ahead of the client during a switch.\n const threadId = useContext(AthenaStatewireThreadIdContext);\n const principal = useContext(AthenaAuthContext)?.user?.userId ?? '';\n // Primitive reads: a streamed token changes neither, so it never re-renders this.\n const messageKey = useAuiState((s) => lastMessageKey(s.optional.thread?.messages));\n const isRunning = useAuiState((s) => s.optional.thread?.isRunning ?? false);\n const windowActive = useDocumentActive(useOwnerDocument());\n const [sentinel, setSentinel] = useState<HTMLSpanElement | null>(null);\n const onScreen = useOnScreen(sentinel);\n const inView = windowActive && onScreen;\n\n const row = threadId !== null && list?.activeThread?.id === threadId ? list.activeThread : null;\n const isUnread = row?.isUnread ?? false;\n const writeInFlight = threadId !== null && (list?.readStatePendingThreadIds.has(threadId) ?? false);\n const setReadState = list?.setReadState;\n const visitKey = `${principal}\\u0000${threadId ?? ''}`;\n\n // The row as it stands when the wait ends: the receipt covers its `updatedAt`.\n const rowRef = useRef(row);\n rowRef.current = row;\n const visitRef = useRef<ViewedThreadVisit>({\n key: visitKey,\n settledMessageKey: null,\n inFlightMessageKey: null,\n });\n\n useEffect(() => {\n if (visitRef.current.key !== visitKey) {\n visitRef.current = { key: visitKey, settledMessageKey: null, inFlightMessageKey: null };\n }\n const visit = visitRef.current;\n if (threadId === null || setReadState === undefined || messageKey === null) return;\n if (writeInFlight) {\n // A write settles the message on screen when it began. One that arrives\n // while it is in flight is weighed once it has answered.\n if (visit.inFlightMessageKey === null) visit.inFlightMessageKey = messageKey;\n visit.settledMessageKey = visit.inFlightMessageKey;\n return;\n }\n visit.inFlightMessageKey = null;\n if (visit.settledMessageKey === messageKey) return;\n if (!inView || isRunning || !isUnread) return;\n const timer = setTimeout(() => {\n // Read again: a render may have taken the row away or marked it read\n // before React ran this effect's cleanup.\n const current = rowRef.current;\n if (current === null || !current.isUnread) return;\n visit.settledMessageKey = messageKey;\n setReadState({ threadId, isUnread: false, updatedAt: current.updatedAt });\n }, delayMs);\n return () => clearTimeout(timer);\n }, [visitKey, threadId, messageKey, writeInFlight, inView, isRunning, isUnread, setReadState, delayMs]);\n\n if (list === null) return null;\n return (\n <span\n ref={setSentinel}\n aria-hidden=\"true\"\n data-slot=\"viewed-thread-sentinel\"\n className=\"pointer-events-none absolute inset-0\"\n />\n );\n}\n"],"names":["useState","useEffect","useAthenaStatewireThreadList","useContext","AthenaStatewireThreadIdContext","AthenaAuthContext","useAuiState","_a","useDocumentActive","useOwnerDocument","useRef","jsx"],"mappings":";;;;;;;;;;AA4CO,MAAM,mCAAmC;AAiBhD,SAAS,eACP,UACe;AACf,QAAM,OAAO,qCAAU,GAAG;AAC1B,SAAO,OAAO,GAAG,KAAK,EAAE,IAAI,KAAK,QAAQ,MAAM,KAAK;AACtD;AAQA,SAAS,YAAY,SAAkC;AACrD,QAAM,CAAC,UAAU,WAAW,IAAIA,MAAAA,SAAS,IAAI;AAC7CC,QAAAA,UAAU,MAAM;;AACd,UAAM,YAAW,wCAAS,cAAc,gBAAvB,mBAAoC;AACrD,QAAI,CAAC,WAAW,CAAC,SAAU;AAC3B,UAAM,WAAW,IAAI,SAAS,CAAC,YAAY;AACzC,YAAM,SAAS,QAAQ,GAAG,EAAE;AAC5B,UAAI,OAAQ,aAAY,OAAO,cAAc;AAAA,IAC/C,CAAC;AACD,aAAS,QAAQ,OAAO;AACxB,WAAO,MAAM,SAAS,WAAA;AAAA,EACxB,GAAG,CAAC,OAAO,CAAC;AACZ,SAAO;AACT;AAEO,SAAS,qBAAqB;AAAA,EACnC,UAAU;AACZ,GAGc;;AACZ,QAAM,OAAOC,oBAAAA,6BAAA;AAIb,QAAM,WAAWC,MAAAA,WAAWC,8CAA8B;AAC1D,QAAM,cAAYD,iBAAAA,WAAWE,kBAAAA,iBAAiB,MAA5BF,mBAA+B,SAA/BA,mBAAqC,WAAU;AAEjE,QAAM,aAAaG,oBAAY,CAAC,MAAA;;AAAM,2BAAeC,MAAA,EAAE,SAAS,WAAX,gBAAAA,IAAmB,QAAQ;AAAA,GAAC;AACjF,QAAM,YAAYD,QAAAA,YAAY,CAAC,MAAA;;AAAM,aAAAC,MAAA,EAAE,SAAS,WAAX,gBAAAA,IAAmB,cAAa;AAAA,GAAK;AAC1E,QAAM,eAAeC,oCAAkBC,kBAAAA,kBAAkB;AACzD,QAAM,CAAC,UAAU,WAAW,IAAIT,MAAAA,SAAiC,IAAI;AACrE,QAAM,WAAW,YAAY,QAAQ;AACrC,QAAM,SAAS,gBAAgB;AAE/B,QAAM,MAAM,aAAa,UAAQ,kCAAM,iBAAN,mBAAoB,QAAO,WAAW,KAAK,eAAe;AAC3F,QAAM,YAAW,2BAAK,aAAY;AAClC,QAAM,gBAAgB,aAAa,UAAS,6BAAM,0BAA0B,IAAI,cAAa;AAC7F,QAAM,eAAe,6BAAM;AAC3B,QAAM,WAAW,GAAG,SAAS,KAAS,YAAY,EAAE;AAGpD,QAAM,SAASU,MAAAA,OAAO,GAAG;AACzB,SAAO,UAAU;AACjB,QAAM,WAAWA,MAAAA,OAA0B;AAAA,IACzC,KAAK;AAAA,IACL,mBAAmB;AAAA,IACnB,oBAAoB;AAAA,EAAA,CACrB;AAEDT,QAAAA,UAAU,MAAM;AACd,QAAI,SAAS,QAAQ,QAAQ,UAAU;AACrC,eAAS,UAAU,EAAE,KAAK,UAAU,mBAAmB,MAAM,oBAAoB,KAAA;AAAA,IACnF;AACA,UAAM,QAAQ,SAAS;AACvB,QAAI,aAAa,QAAQ,iBAAiB,UAAa,eAAe,KAAM;AAC5E,QAAI,eAAe;AAGjB,UAAI,MAAM,uBAAuB,KAAM,OAAM,qBAAqB;AAClE,YAAM,oBAAoB,MAAM;AAChC;AAAA,IACF;AACA,UAAM,qBAAqB;AAC3B,QAAI,MAAM,sBAAsB,WAAY;AAC5C,QAAI,CAAC,UAAU,aAAa,CAAC,SAAU;AACvC,UAAM,QAAQ,WAAW,MAAM;AAG7B,YAAM,UAAU,OAAO;AACvB,UAAI,YAAY,QAAQ,CAAC,QAAQ,SAAU;AAC3C,YAAM,oBAAoB;AAC1B,mBAAa,EAAE,UAAU,UAAU,OAAO,WAAW,QAAQ,WAAW;AAAA,IAC1E,GAAG,OAAO;AACV,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,UAAU,UAAU,YAAY,eAAe,QAAQ,WAAW,UAAU,cAAc,OAAO,CAAC;AAEtG,MAAI,SAAS,KAAM,QAAO;AAC1B,SACEU,2BAAAA;AAAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK;AAAA,MACL,eAAY;AAAA,MACZ,aAAU;AAAA,MACV,WAAU;AAAA,IAAA;AAAA,EAAA;AAGhB;;;"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** How long the open session must stay unread and in view before it is marked read. */
|
|
3
|
+
export declare const MARK_VIEWED_THREAD_READ_DELAY_MS = 1000;
|
|
4
|
+
export declare function MarkViewedThreadRead({ delayMs, }: {
|
|
5
|
+
/** How long the conditions must hold before the write. */
|
|
6
|
+
delayMs?: number;
|
|
7
|
+
}): ReactNode;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useAuiState } from "@assistant-ui/react";
|
|
3
|
+
import { useContext, useState, useRef, useEffect } from "react";
|
|
4
|
+
import { AthenaAuthContext } from "../auth/AthenaAuthContext.js";
|
|
5
|
+
import { useOwnerDocument } from "../lib/owner-document.js";
|
|
6
|
+
import { useDocumentActive } from "../lib/use-document-active.js";
|
|
7
|
+
import { useAthenaStatewireThreadList } from "./statewire-thread-list.js";
|
|
8
|
+
import { AthenaStatewireThreadIdContext } from "./thread-id-context.js";
|
|
9
|
+
const MARK_VIEWED_THREAD_READ_DELAY_MS = 1e3;
|
|
10
|
+
function lastMessageKey(messages) {
|
|
11
|
+
const last = messages == null ? void 0 : messages.at(-1);
|
|
12
|
+
return last ? `${last.id}#${last.content.length}` : null;
|
|
13
|
+
}
|
|
14
|
+
function useOnScreen(element) {
|
|
15
|
+
const [onScreen, setOnScreen] = useState(true);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
var _a;
|
|
18
|
+
const Observer = (_a = element == null ? void 0 : element.ownerDocument.defaultView) == null ? void 0 : _a.IntersectionObserver;
|
|
19
|
+
if (!element || !Observer) return;
|
|
20
|
+
const observer = new Observer((entries) => {
|
|
21
|
+
const latest = entries.at(-1);
|
|
22
|
+
if (latest) setOnScreen(latest.isIntersecting);
|
|
23
|
+
});
|
|
24
|
+
observer.observe(element);
|
|
25
|
+
return () => observer.disconnect();
|
|
26
|
+
}, [element]);
|
|
27
|
+
return onScreen;
|
|
28
|
+
}
|
|
29
|
+
function MarkViewedThreadRead({
|
|
30
|
+
delayMs = MARK_VIEWED_THREAD_READ_DELAY_MS
|
|
31
|
+
}) {
|
|
32
|
+
var _a, _b, _c;
|
|
33
|
+
const list = useAthenaStatewireThreadList();
|
|
34
|
+
const threadId = useContext(AthenaStatewireThreadIdContext);
|
|
35
|
+
const principal = ((_b = (_a = useContext(AthenaAuthContext)) == null ? void 0 : _a.user) == null ? void 0 : _b.userId) ?? "";
|
|
36
|
+
const messageKey = useAuiState((s) => {
|
|
37
|
+
var _a2;
|
|
38
|
+
return lastMessageKey((_a2 = s.optional.thread) == null ? void 0 : _a2.messages);
|
|
39
|
+
});
|
|
40
|
+
const isRunning = useAuiState((s) => {
|
|
41
|
+
var _a2;
|
|
42
|
+
return ((_a2 = s.optional.thread) == null ? void 0 : _a2.isRunning) ?? false;
|
|
43
|
+
});
|
|
44
|
+
const windowActive = useDocumentActive(useOwnerDocument());
|
|
45
|
+
const [sentinel, setSentinel] = useState(null);
|
|
46
|
+
const onScreen = useOnScreen(sentinel);
|
|
47
|
+
const inView = windowActive && onScreen;
|
|
48
|
+
const row = threadId !== null && ((_c = list == null ? void 0 : list.activeThread) == null ? void 0 : _c.id) === threadId ? list.activeThread : null;
|
|
49
|
+
const isUnread = (row == null ? void 0 : row.isUnread) ?? false;
|
|
50
|
+
const writeInFlight = threadId !== null && ((list == null ? void 0 : list.readStatePendingThreadIds.has(threadId)) ?? false);
|
|
51
|
+
const setReadState = list == null ? void 0 : list.setReadState;
|
|
52
|
+
const visitKey = `${principal}\0${threadId ?? ""}`;
|
|
53
|
+
const rowRef = useRef(row);
|
|
54
|
+
rowRef.current = row;
|
|
55
|
+
const visitRef = useRef({
|
|
56
|
+
key: visitKey,
|
|
57
|
+
settledMessageKey: null,
|
|
58
|
+
inFlightMessageKey: null
|
|
59
|
+
});
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (visitRef.current.key !== visitKey) {
|
|
62
|
+
visitRef.current = { key: visitKey, settledMessageKey: null, inFlightMessageKey: null };
|
|
63
|
+
}
|
|
64
|
+
const visit = visitRef.current;
|
|
65
|
+
if (threadId === null || setReadState === void 0 || messageKey === null) return;
|
|
66
|
+
if (writeInFlight) {
|
|
67
|
+
if (visit.inFlightMessageKey === null) visit.inFlightMessageKey = messageKey;
|
|
68
|
+
visit.settledMessageKey = visit.inFlightMessageKey;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
visit.inFlightMessageKey = null;
|
|
72
|
+
if (visit.settledMessageKey === messageKey) return;
|
|
73
|
+
if (!inView || isRunning || !isUnread) return;
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
const current = rowRef.current;
|
|
76
|
+
if (current === null || !current.isUnread) return;
|
|
77
|
+
visit.settledMessageKey = messageKey;
|
|
78
|
+
setReadState({ threadId, isUnread: false, updatedAt: current.updatedAt });
|
|
79
|
+
}, delayMs);
|
|
80
|
+
return () => clearTimeout(timer);
|
|
81
|
+
}, [visitKey, threadId, messageKey, writeInFlight, inView, isRunning, isUnread, setReadState, delayMs]);
|
|
82
|
+
if (list === null) return null;
|
|
83
|
+
return /* @__PURE__ */ jsx(
|
|
84
|
+
"span",
|
|
85
|
+
{
|
|
86
|
+
ref: setSentinel,
|
|
87
|
+
"aria-hidden": "true",
|
|
88
|
+
"data-slot": "viewed-thread-sentinel",
|
|
89
|
+
className: "pointer-events-none absolute inset-0"
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
MARK_VIEWED_THREAD_READ_DELAY_MS,
|
|
95
|
+
MarkViewedThreadRead
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=mark-viewed-thread-read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mark-viewed-thread-read.js","sources":["../../src/threads/mark-viewed-thread-read.tsx"],"sourcesContent":["/**\n * `MarkViewedThreadRead` — the session `AthenaChat` shows is marked read for\n * the viewer, as Olympus marks every session it shows\n * (`useMarkCurrentThreadAsRead`) and Devin clears a session's unread dot when\n * it is opened. Without it, a session opened from the session list or a link\n * stayed unread for good: only the inbox's own open and its Mark read wrote\n * the receipt, so unread dots and the inbox's Unread count only grew.\n *\n * The open session is marked read once all of these hold:\n *\n * - the transcript is in front of the person: its window is visible and\n * focused (the window it renders into, a pop-out's included), and the chat\n * itself is on screen (not hidden, as `AthenaLayout` hides it behind a\n * maximized workbench, nor scrolled out of the page);\n * - its history is on screen (the thread has a last message);\n * - no run is going (a running session is marked when its run ends, never per\n * streamed chunk);\n * - its row is unread (a read row is never written).\n *\n * They must hold for {@link MARK_VIEWED_THREAD_READ_DELAY_MS}; the write is\n * the thread list's `setReadState` (`POST /api/conversations/threads/read-state`),\n * with the row's `updatedAt`, so the next message ends the override and shows\n * the row unread again. One write per (viewer, session, last message as it\n * stands — its id and how many parts it has, since the agent's further\n * messages in a turn join that turn's message): what arrives while the person\n * watches is marked too, nothing polls, and a failed write is not repeated\n * for the same message. A read-state write in flight for the open session\n * (this one's, or the person's own Mark read / Mark unread) settles the\n * message that was on screen when it began, so the person's choice stands\n * until something new arrives or they open the session again.\n *\n * Renders an invisible, inert sentinel over the chat panel (it must sit in\n * the panel, which is `relative`); nothing outside\n * `<AthenaProvider enableThreadList>`.\n */\nimport { useAuiState } from '@assistant-ui/react';\nimport { type ReactNode, useContext, useEffect, useRef, useState } from 'react';\nimport { AthenaAuthContext } from '../auth/AthenaAuthContext';\nimport { useOwnerDocument } from '../lib/owner-document';\nimport { useDocumentActive } from '../lib/use-document-active';\nimport { useAthenaStatewireThreadList } from './statewire-thread-list';\nimport { AthenaStatewireThreadIdContext } from './thread-id-context';\n\n/** How long the open session must stay unread and in view before it is marked read. */\nexport const MARK_VIEWED_THREAD_READ_DELAY_MS = 1_000;\n\ninterface ViewedThreadVisit {\n /** Who views which session: the provider's read state is scoped to the viewer. */\n key: string;\n /** The last message (as {@link lastMessageKey} reads it) this visit has settled. */\n settledMessageKey: string | null;\n /** The message on screen when the read-state write now in flight began. */\n inFlightMessageKey: string | null;\n}\n\n/**\n * The thread's last message as it stands: its id and part count. A turn's\n * further assistant messages join that turn's message, so its id alone does\n * not move when the agent writes more; the part count does — once per part,\n * never per streamed token. `null` until the history is on screen.\n */\nfunction lastMessageKey(\n messages: readonly { readonly id: string; readonly content: readonly unknown[] }[] | undefined,\n): string | null {\n const last = messages?.at(-1);\n return last ? `${last.id}#${last.content.length}` : null;\n}\n\n/**\n * Whether `element` is on screen: laid out (no `display: none` on it or an\n * ancestor) and inside the viewport, read by an IntersectionObserver of the\n * element's own window. `true` until the observer's first answer, which\n * arrives within a frame, and where there is no observer.\n */\nfunction useOnScreen(element: Element | null): boolean {\n const [onScreen, setOnScreen] = useState(true);\n useEffect(() => {\n const Observer = element?.ownerDocument.defaultView?.IntersectionObserver;\n if (!element || !Observer) return;\n const observer = new Observer((entries) => {\n const latest = entries.at(-1);\n if (latest) setOnScreen(latest.isIntersecting);\n });\n observer.observe(element);\n return () => observer.disconnect();\n }, [element]);\n return onScreen;\n}\n\nexport function MarkViewedThreadRead({\n delayMs = MARK_VIEWED_THREAD_READ_DELAY_MS,\n}: {\n /** How long the conditions must hold before the write. */\n delayMs?: number;\n}): ReactNode {\n const list = useAthenaStatewireThreadList();\n // The thread the statewire scope publishes WITH its client, so the messages\n // read here are always this thread's: the list's selection runs one commit\n // ahead of the client during a switch.\n const threadId = useContext(AthenaStatewireThreadIdContext);\n const principal = useContext(AthenaAuthContext)?.user?.userId ?? '';\n // Primitive reads: a streamed token changes neither, so it never re-renders this.\n const messageKey = useAuiState((s) => lastMessageKey(s.optional.thread?.messages));\n const isRunning = useAuiState((s) => s.optional.thread?.isRunning ?? false);\n const windowActive = useDocumentActive(useOwnerDocument());\n const [sentinel, setSentinel] = useState<HTMLSpanElement | null>(null);\n const onScreen = useOnScreen(sentinel);\n const inView = windowActive && onScreen;\n\n const row = threadId !== null && list?.activeThread?.id === threadId ? list.activeThread : null;\n const isUnread = row?.isUnread ?? false;\n const writeInFlight = threadId !== null && (list?.readStatePendingThreadIds.has(threadId) ?? false);\n const setReadState = list?.setReadState;\n const visitKey = `${principal}\\u0000${threadId ?? ''}`;\n\n // The row as it stands when the wait ends: the receipt covers its `updatedAt`.\n const rowRef = useRef(row);\n rowRef.current = row;\n const visitRef = useRef<ViewedThreadVisit>({\n key: visitKey,\n settledMessageKey: null,\n inFlightMessageKey: null,\n });\n\n useEffect(() => {\n if (visitRef.current.key !== visitKey) {\n visitRef.current = { key: visitKey, settledMessageKey: null, inFlightMessageKey: null };\n }\n const visit = visitRef.current;\n if (threadId === null || setReadState === undefined || messageKey === null) return;\n if (writeInFlight) {\n // A write settles the message on screen when it began. One that arrives\n // while it is in flight is weighed once it has answered.\n if (visit.inFlightMessageKey === null) visit.inFlightMessageKey = messageKey;\n visit.settledMessageKey = visit.inFlightMessageKey;\n return;\n }\n visit.inFlightMessageKey = null;\n if (visit.settledMessageKey === messageKey) return;\n if (!inView || isRunning || !isUnread) return;\n const timer = setTimeout(() => {\n // Read again: a render may have taken the row away or marked it read\n // before React ran this effect's cleanup.\n const current = rowRef.current;\n if (current === null || !current.isUnread) return;\n visit.settledMessageKey = messageKey;\n setReadState({ threadId, isUnread: false, updatedAt: current.updatedAt });\n }, delayMs);\n return () => clearTimeout(timer);\n }, [visitKey, threadId, messageKey, writeInFlight, inView, isRunning, isUnread, setReadState, delayMs]);\n\n if (list === null) return null;\n return (\n <span\n ref={setSentinel}\n aria-hidden=\"true\"\n data-slot=\"viewed-thread-sentinel\"\n className=\"pointer-events-none absolute inset-0\"\n />\n );\n}\n"],"names":["_a"],"mappings":"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"}
|