@tangle-network/agent-app 0.44.48 → 0.44.50
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/dist/assistant/index.js +2 -2
- package/dist/chat-routes/index.d.ts +55 -2
- package/dist/chat-routes/index.js +79 -9
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chat-store/index.d.ts +12 -0
- package/dist/chat-store/index.js +12 -0
- package/dist/chat-store/index.js.map +1 -1
- package/dist/{chunk-2EJFIQUV.js → chunk-6VWA26BV.js} +57 -79
- package/dist/chunk-6VWA26BV.js.map +1 -0
- package/dist/{chunk-T3TYG3VW.js → chunk-D3GK2IPA.js} +2 -2
- package/dist/chunk-JEZJ6HTF.js +77 -0
- package/dist/chunk-JEZJ6HTF.js.map +1 -0
- package/dist/{chunk-3E4MW5LR.js → chunk-YOSSGDIG.js} +25 -16
- package/dist/chunk-YOSSGDIG.js.map +1 -0
- package/dist/session-shell/index.d.ts +11 -2
- package/dist/session-shell/index.js +1 -1
- package/dist/stream/index.d.ts +1 -1
- package/dist/stream/index.js +11 -5
- package/dist/{turn-buffer-BcPfhr1_.d.ts → turn-buffer-CFKQlnxf.d.ts} +24 -6
- package/dist/turn-stream/index.d.ts +3 -1
- package/dist/turn-stream/index.js +8 -1
- package/dist/turn-stream/index.js.map +1 -1
- package/dist/web-react/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-2EJFIQUV.js.map +0 -1
- package/dist/chunk-3E4MW5LR.js.map +0 -1
- /package/dist/{chunk-T3TYG3VW.js.map → chunk-D3GK2IPA.js.map} +0 -0
|
@@ -12,28 +12,37 @@ function buildSessionSubItems({
|
|
|
12
12
|
prefetch = "intent",
|
|
13
13
|
overflow
|
|
14
14
|
}) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
unread: Boolean(session.unread),
|
|
22
|
-
actions: actions?.canEdit ? [
|
|
23
|
-
{
|
|
15
|
+
const rowActions = (session) => {
|
|
16
|
+
if (!actions?.canEdit) return void 0;
|
|
17
|
+
const built = [];
|
|
18
|
+
const { onRename, onDelete } = actions;
|
|
19
|
+
if (onRename) {
|
|
20
|
+
built.push({
|
|
24
21
|
id: "rename",
|
|
25
22
|
label: actions.renameLabel ?? "Rename",
|
|
26
23
|
icon: actions.renameIcon,
|
|
27
|
-
onSelect: () =>
|
|
28
|
-
}
|
|
29
|
-
|
|
24
|
+
onSelect: () => onRename(session)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (onDelete) {
|
|
28
|
+
built.push({
|
|
30
29
|
id: "delete",
|
|
31
30
|
label: actions.deleteLabel ?? "Delete",
|
|
32
31
|
icon: actions.deleteIcon,
|
|
33
32
|
destructive: true,
|
|
34
|
-
onSelect: () =>
|
|
35
|
-
}
|
|
36
|
-
|
|
33
|
+
onSelect: () => onDelete(session)
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return built.length ? built : void 0;
|
|
37
|
+
};
|
|
38
|
+
const rows = sessions.map((session) => ({
|
|
39
|
+
id: session.id,
|
|
40
|
+
label: sessionLabel(session, untitledLabel),
|
|
41
|
+
href: hrefForSession(session.id),
|
|
42
|
+
prefetch,
|
|
43
|
+
isLoading: respondingSessionIds?.has(session.id) ?? false,
|
|
44
|
+
unread: Boolean(session.unread),
|
|
45
|
+
actions: rowActions(session)
|
|
37
46
|
}));
|
|
38
47
|
if (!overflow) return rows;
|
|
39
48
|
return [
|
|
@@ -201,4 +210,4 @@ export {
|
|
|
201
210
|
railCollapsedCookie,
|
|
202
211
|
writeRailCollapsedCookie
|
|
203
212
|
};
|
|
204
|
-
//# sourceMappingURL=chunk-
|
|
213
|
+
//# sourceMappingURL=chunk-YOSSGDIG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/session-shell/index.ts"],"sourcesContent":["/**\n * Session shell — the app-shell mechanism every agent product needs around the\n * chat surface: a list of past sessions in the rail, an entry point for a new\n * one, and a paged history view behind it.\n *\n * `/web-react` already owns the chat SURFACE (composer, transcript, cards); it\n * owned no session SHELL, so all four products hand-rolled one and drifted.\n * This module is the shell's pure half: no React, no DOM, no peer imports, so a\n * server loader can call `readRailCollapsedCookie` without dragging React into\n * a worker bundle (`/web-react` holds the rendered half).\n *\n * Domain stays a parameter. A \"session\" here is only an id, a title and a\n * timestamp — a gtm thread, a tax session and a legal matter are all the same\n * shape to the shell, and the product supplies routing through `hrefForSession`\n * rather than the shell knowing any URL.\n */\n\n/** One session as the shell needs to see it. Products map their own row\n * (thread / session / matter) onto this before handing it over. */\nexport interface SessionSummary {\n id: string\n /** `null`/empty renders as the untitled placeholder rather than a blank row. */\n title: string | null\n /** ISO-8601. `null` when the product has no timestamp to show. */\n updatedAt: string | null\n isPinned?: boolean\n /** Unread for the viewer. Use `resolveSessionUnread` to fold live overlays in. */\n unread?: boolean\n /** Free-form product label (gtm categories, legal matter types). Passed\n * through untouched — the shell never interprets it. */\n category?: string | null\n}\n\n/** One fetched page of sessions with an optional continuation cursor. */\nexport interface SessionPage {\n items: SessionSummary[]\n /** Opaque continuation token; absent/null ⇒ no further pages. */\n nextCursor?: string | null\n}\n\n/** Sort order for the history view. The product's fetcher decides what these\n * mean against its own storage; the shell only round-trips the value. */\nexport type SessionSort = 'newest' | 'oldest'\n\n// ---------------------------------------------------------------------------\n// Rail items — structurally assignable to sandbox-ui's SidebarLayout types\n// ---------------------------------------------------------------------------\n\n/**\n * These mirror `@tangle-network/sandbox-ui/dashboard`'s `SidebarLayoutNavItem`\n * / `RailExpandableSubItem` STRUCTURALLY rather than importing them, so this\n * module stays free of the optional peer (invariant 3 — structural over\n * hard-dep when the surface is small). `tests/session-shell/rail-contract.test.ts`\n * assigns the builder output to the real sandbox-ui types, so a drift in either\n * direction fails CI instead of silently dropping a field at runtime.\n *\n * `TIcon` is the product's icon component type (lucide, custom, anything) —\n * generic so this file needs no React types.\n */\nexport interface SessionRailAction<TIcon = unknown> {\n id: string\n label: string\n icon?: TIcon\n destructive?: boolean\n onSelect: () => void\n}\n\nexport type RailPrefetch = 'none' | 'intent' | 'render' | 'viewport'\n\nexport interface SessionRailSubItem<TIcon = unknown> {\n id: string\n label: string\n href: string\n prefetch?: RailPrefetch\n /** Live working indicator — the session is mid-turn. */\n isLoading?: boolean\n /** Bold + leading dot. sandbox-ui suppresses it while `isLoading`. */\n unread?: boolean\n /** Emphasised row, used for the trailing \"view all\" overflow link. */\n emphasis?: boolean\n actions?: SessionRailAction<TIcon>[]\n}\n\nexport interface SessionRailNavItem<TIcon = unknown> {\n id: string\n /** REQUIRED, mirroring sandbox-ui — the rail renders `<Icon />` unguarded, so\n * an omitted icon is a blank/crashing row rather than a styling nit. */\n icon: TIcon\n label: string\n href: string\n badge?: number\n expandable?: boolean\n defaultOpen?: boolean\n subItems?: SessionRailSubItem<TIcon>[]\n subActiveIds?: string[]\n emptyLabel?: string\n prefetch?: RailPrefetch\n}\n\n/** Per-row rename/delete wiring. Supplied by the layout that owns the dialogs;\n * omitted (or `canEdit: false`) leaves rows read-only. */\nexport interface SessionRowActions<TIcon = unknown> {\n canEdit: boolean\n renameIcon?: TIcon\n deleteIcon?: TIcon\n renameLabel?: string\n deleteLabel?: string\n /**\n * Omit when the product cannot rename a session — the row then offers delete\n * only, instead of a menu item that does nothing.\n *\n * Independently optional, matching `SessionHistoryPanel`, which has always\n * rendered whichever of the two it was given. The rail builder used to demand\n * both, so a product with archive-but-no-rename (tax) could either fake a\n * rename or ship no row actions at all.\n */\n onRename?: (session: SessionSummary) => void\n onDelete?: (session: SessionSummary) => void\n}\n\nexport const UNTITLED_SESSION_LABEL = 'Untitled chat'\n\n/** Display title for a session row — trims, and falls back rather than\n * rendering an empty row the user cannot aim at. */\nexport function sessionLabel(session: SessionSummary, untitled = UNTITLED_SESSION_LABEL): string {\n return session.title?.trim() || untitled\n}\n\nexport interface BuildSessionSubItemsOptions<TIcon = unknown> {\n sessions: SessionSummary[]\n /** The product's route for one session. The shell never builds a URL itself. */\n hrefForSession: (sessionId: string) => string\n /** Ids currently mid-turn — renders the working indicator. */\n respondingSessionIds?: ReadonlySet<string>\n actions?: SessionRowActions<TIcon>\n untitledLabel?: string\n prefetch?: RailPrefetch\n /** Trailing \"view all\" row, appended when the capped list hides sessions. */\n overflow?: { href: string; label?: string }\n}\n\n/** Session rows for the rail's expandable history item. */\nexport function buildSessionSubItems<TIcon = unknown>({\n sessions,\n hrefForSession,\n respondingSessionIds,\n actions,\n untitledLabel = UNTITLED_SESSION_LABEL,\n prefetch = 'intent',\n overflow,\n}: BuildSessionSubItemsOptions<TIcon>): SessionRailSubItem<TIcon>[] {\n /**\n * Only the handlers the product actually supplied. An empty result becomes\n * `undefined` rather than `[]`, because sandbox-ui renders the kebab trigger\n * whenever `actions` is an array — an empty one is a button that opens an\n * empty menu.\n */\n const rowActions = (session: SessionSummary): SessionRailAction<TIcon>[] | undefined => {\n if (!actions?.canEdit) return undefined\n const built: SessionRailAction<TIcon>[] = []\n const { onRename, onDelete } = actions\n if (onRename) {\n built.push({\n id: 'rename',\n label: actions.renameLabel ?? 'Rename',\n icon: actions.renameIcon,\n onSelect: () => onRename(session),\n })\n }\n if (onDelete) {\n built.push({\n id: 'delete',\n label: actions.deleteLabel ?? 'Delete',\n icon: actions.deleteIcon,\n destructive: true,\n onSelect: () => onDelete(session),\n })\n }\n return built.length ? built : undefined\n }\n\n const rows: SessionRailSubItem<TIcon>[] = sessions.map((session) => ({\n id: session.id,\n label: sessionLabel(session, untitledLabel),\n href: hrefForSession(session.id),\n prefetch,\n isLoading: respondingSessionIds?.has(session.id) ?? false,\n unread: Boolean(session.unread),\n actions: rowActions(session),\n }))\n if (!overflow) return rows\n return [\n ...rows,\n {\n id: 'view-all',\n label: overflow.label ?? 'View all chats',\n href: overflow.href,\n prefetch,\n emphasis: true,\n },\n ]\n}\n\nexport interface BuildSessionNavItemOptions<TIcon = unknown>\n extends BuildSessionSubItemsOptions<TIcon> {\n /** Nav id the product highlights against (`activeNavId === id`). */\n id?: string\n label?: string\n /** The product's icon component. Required — see `SessionRailNavItem.icon`. */\n icon: TIcon\n /** The expandable row's own destination — the full history page. */\n href: string\n /** Session currently open, highlighted inside the expandable. */\n activeSessionId?: string | null\n emptyLabel?: string\n defaultOpen?: boolean\n}\n\n/**\n * The rail's session entry: one expandable nav row whose sub-items are the\n * recent sessions. This is the structure the owner asked for — history lives IN\n * the rail, not in a second sidebar panel beside it.\n */\nexport function buildSessionNavItem<TIcon = unknown>({\n id = 'history',\n label = 'History',\n icon,\n href,\n activeSessionId,\n emptyLabel = 'No chats yet',\n defaultOpen = true,\n ...subItemOptions\n}: BuildSessionNavItemOptions<TIcon>): SessionRailNavItem<TIcon> {\n return {\n id,\n icon,\n label,\n href,\n expandable: true,\n defaultOpen,\n subItems: buildSessionSubItems<TIcon>(subItemOptions),\n subActiveIds: activeSessionId ? [activeSessionId] : undefined,\n emptyLabel,\n prefetch: subItemOptions.prefetch ?? 'intent',\n }\n}\n\n// ---------------------------------------------------------------------------\n// Routing / selection\n// ---------------------------------------------------------------------------\n\nfunction stripTrailingSlashes(value: string): string {\n return value.replace(/\\/+$/, '')\n}\n\n/** Bare segment name, so `reserved` accepts `'/settings'` or `'settings'`. */\nfunction stripSlashes(value: string): string {\n return value.replace(/^\\/+|\\/+$/g, '')\n}\n\n/** Path with query + fragment removed and trailing slashes trimmed. A caller\n * passing a full href instead of a pathname would otherwise match nothing. */\nfunction normalizePath(pathname: string): string {\n const withoutHash = pathname.split('#')[0] ?? ''\n const withoutQuery = withoutHash.split('?')[0] ?? ''\n return stripTrailingSlashes(withoutQuery)\n}\n\n/** True when `path` is `prefix` or a segment-aligned descendant of it, so\n * `/vault` never claims `/vault-archive`. */\nfunction isUnderPrefix(path: string, prefix: string): boolean {\n const p = stripTrailingSlashes(prefix)\n if (p === '') return true\n return path === p || path.startsWith(`${p}/`)\n}\n\nexport interface ActiveSessionIdOptions {\n pathname: string\n /** Workspace-scoped route base, e.g. `/app/ws_123`. */\n base: string\n /** Route segment sessions live under. Default `chat` ⇒ `${base}/chat/:id`.\n * Pass `''` when sessions sit DIRECTLY under the base (`/app/:sessionId`),\n * which is how one product routes them — then `reserved` is mandatory. */\n segment?: string\n /** Segment that means \"composing a new session\", not an id. Default `new`. */\n newSegment?: string\n /**\n * First segments that are OTHER routes, not session ids. Only meaningful\n * with `segment: ''`, where `/app/settings` is otherwise indistinguishable\n * from a session called `settings` — and resolving it as one would highlight\n * and prefetch a session that does not exist. Pass the product's own nav\n * paths; unknown-but-reserved is a routing bug, so this fails closed.\n */\n reserved?: readonly string[]\n}\n\n/**\n * The session id the current route has open, or `null` on the new-session\n * composer / anywhere else.\n *\n * Anchored at `base` on purpose. A bare `/\\/chat\\/([^/]+)/` scan — the shape\n * three products shipped — matches the FIRST `/chat/` anywhere in the path, so\n * a workspace or vault folder named `chat` resolves a neighbouring segment as a\n * session id and the rail highlights (and prefetches) a session the user is not\n * in. Same class as attaching to a stale box: it looks right and points at the\n * wrong row.\n */\nexport function activeSessionIdFromPath({\n pathname,\n base,\n segment = 'chat',\n newSegment = 'new',\n reserved,\n}: ActiveSessionIdOptions): string | null {\n const path = normalizePath(pathname)\n const root = stripTrailingSlashes(base)\n const prefix = segment ? `${root}/${segment}` : root\n if (!isUnderPrefix(path, prefix) || path === prefix) return null\n const id = path.slice(prefix.length + 1).split('/')[0] ?? ''\n if (!id || id === newSegment) return null\n // `/app/settings` under a segment-less route is a sibling page, not a\n // session named \"settings\".\n if (reserved?.some((name) => stripSlashes(name) === id)) return null\n return decodeURIComponent(id)\n}\n\n/** One rail destination. `path` is relative to the workspace base. */\nexport interface NavRouteDef {\n id: string\n path: string\n}\n\nexport interface ResolveActiveNavIdOptions {\n pathname: string\n base: string\n /** The product's rail rows, in any order — resolution is longest-prefix. */\n routes: NavRouteDef[]\n /** Extra prefixes that light an existing row: `{ '/agents': 'integrations' }`.\n * Participates in the same longest-prefix contest. */\n aliases?: Record<string, string>\n /** Prefixes that deliberately highlight NOTHING, beating any shorter match.\n * gtm uses this so an open chat lights no rail row while `/chat/new` still\n * lights \"New\". */\n claimsNothing?: string[]\n}\n\n/**\n * The rail row to highlight for the current route.\n *\n * Longest-prefix wins, so declaration order cannot change the answer. The\n * per-product versions this replaces were first-match over an array, which made\n * `/chat/new` vs `/chat` an ordering accident rather than a rule.\n */\nexport function resolveActiveNavId({\n pathname,\n base,\n routes,\n aliases,\n claimsNothing,\n}: ResolveActiveNavIdOptions): string | undefined {\n const path = normalizePath(pathname)\n const root = stripTrailingSlashes(base)\n let bestLength = -1\n let bestId: string | undefined\n const consider = (relative: string, id: string | undefined, winsTies = false) => {\n const full = stripTrailingSlashes(`${root}${relative}`)\n if (!isUnderPrefix(path, full)) return\n if (full.length > bestLength || (winsTies && full.length === bestLength)) {\n bestLength = full.length\n bestId = id\n }\n }\n for (const route of routes) consider(route.path, route.id)\n for (const [prefix, id] of Object.entries(aliases ?? {})) consider(prefix, id)\n // Declared last and wins an exact-length tie: naming a prefix in\n // `claimsNothing` is a deliberate override of the row that owns it, so\n // `claimsNothing: ['/chat']` beats a `{ id: 'chat', path: '/chat' }` row while\n // a longer `/chat/new` still wins on specificity.\n for (const prefix of claimsNothing ?? []) consider(prefix, undefined, true)\n return bestId\n}\n\n// ---------------------------------------------------------------------------\n// Sidebar list composition\n// ---------------------------------------------------------------------------\n\nexport interface ResolveSessionUnreadOptions {\n sessionId: string\n /** Server-computed unread from the route loader. */\n loaderUnread: boolean\n /** Live \"went unread\" ids from the workspace channel. */\n liveUnreadIds?: ReadonlySet<string>\n /** Ids this tab has already opened since the loader ran. */\n locallyReadIds?: ReadonlySet<string>\n /** The open session is never unread to its own viewer. */\n currentSessionId?: string | null\n}\n\n/**\n * Effective unread for one row. The loader's value can be stale — a layout\n * loader that survives same-workspace navigation keeps reporting a session as\n * unread after the user opened it — so live and local overlays win over it, and\n * the currently-open session always reads as read.\n */\nexport function resolveSessionUnread({\n sessionId,\n loaderUnread,\n liveUnreadIds,\n locallyReadIds,\n currentSessionId,\n}: ResolveSessionUnreadOptions): boolean {\n if (sessionId === currentSessionId) return false\n if (liveUnreadIds?.has(sessionId)) return true\n if (locallyReadIds?.has(sessionId)) return false\n return loaderUnread\n}\n\nexport interface ComposeSidebarSessionsOptions {\n /** Server-rendered rows, already ordered by the product's query. */\n loaderSessions: SessionSummary[]\n /** Optimistic rows from the live channel (a chat created in another tab). */\n optimisticSessions?: SessionSummary[]\n /** Rail cap. The full list lives on the history page. */\n limit: number\n /** Total sessions the product holds, used to decide the overflow row. */\n totalCount?: number\n liveUnreadIds?: ReadonlySet<string>\n locallyReadIds?: ReadonlySet<string>\n currentSessionId?: string | null\n}\n\nexport interface ComposedSidebarSessions {\n sessions: SessionSummary[]\n /** More sessions exist than the rail shows ⇒ render the \"view all\" row. */\n hasMore: boolean\n}\n\n/**\n * The rail's session list: optimistic rows first, then the loader's, capped,\n * with unread resolved per row.\n *\n * Optimistic rows are deduped against the loader by id — once a revalidation\n * brings a live-created session back from the server it must not appear twice\n * (duplicate React keys, and the row's actions would target the same session\n * from two places).\n */\nexport function composeSidebarSessions({\n loaderSessions,\n optimisticSessions = [],\n limit,\n totalCount,\n liveUnreadIds,\n locallyReadIds,\n currentSessionId,\n}: ComposeSidebarSessionsOptions): ComposedSidebarSessions {\n const loaderIds = new Set(loaderSessions.map((session) => session.id))\n const pendingNew = optimisticSessions.filter((session) => !loaderIds.has(session.id))\n const merged = [...pendingNew, ...loaderSessions]\n const sessions = merged.slice(0, Math.max(0, limit)).map((session) => ({\n ...session,\n unread: resolveSessionUnread({\n sessionId: session.id,\n loaderUnread: Boolean(session.unread),\n liveUnreadIds,\n locallyReadIds,\n currentSessionId,\n }),\n }))\n const known = (totalCount ?? loaderSessions.length) + pendingNew.length\n return { sessions, hasMore: known > sessions.length }\n}\n\n/**\n * Append a fetched page to held rows, dropping ids already shown. A session\n * bumped to the top between two page fetches otherwise arrives twice — once in\n * the page it moved out of and once in the page it moved into.\n */\nexport function mergeSessionPages(\n existing: SessionSummary[],\n incoming: SessionSummary[],\n): SessionSummary[] {\n const seen = new Set(existing.map((session) => session.id))\n return [...existing, ...incoming.filter((session) => !seen.has(session.id))]\n}\n\n// ---------------------------------------------------------------------------\n// Rail collapse cookie (SSR-seeded so the first paint matches the client)\n// ---------------------------------------------------------------------------\n\nexport const DEFAULT_RAIL_COOKIE_NAME = 'agent-sidebar-rail-collapsed'\n\n/**\n * Read the persisted rail-collapse state from a request's `Cookie` header, so\n * the server renders the rail in the state the user left it and the first\n * client render does not re-flow.\n *\n * Parses the header rather than building a `RegExp` from the cookie name (the\n * shape the products shipped): a name containing a regex metacharacter would\n * silently match the wrong cookie or none at all.\n */\nexport function readRailCollapsedCookie(\n cookieHeader: string | null | undefined,\n name: string = DEFAULT_RAIL_COOKIE_NAME,\n): boolean {\n for (const pair of (cookieHeader ?? '').split(';')) {\n const eq = pair.indexOf('=')\n if (eq === -1) continue\n if (pair.slice(0, eq).trim() !== name) continue\n return pair.slice(eq + 1).trim() === '1'\n }\n return false\n}\n\nexport interface RailCookieOptions {\n name?: string\n /** Seconds. Default one year. */\n maxAge?: number\n path?: string\n /** Omit to auto-detect: `secure` on https, off on http://localhost — a Secure\n * cookie is dropped there and the rail state would not persist in dev. */\n secure?: boolean\n}\n\n/** The cookie string for a collapse state. Usable as `document.cookie` or as a\n * `Set-Cookie` value. Exported separately so it is testable without a DOM. */\nexport function railCollapsedCookie(\n collapsed: boolean,\n { name = DEFAULT_RAIL_COOKIE_NAME, maxAge = 31_536_000, path = '/', secure }: RailCookieOptions = {},\n): string {\n const isSecure =\n secure ?? (typeof location !== 'undefined' && location.protocol === 'https:')\n return `${name}=${collapsed ? '1' : '0'}; path=${path}; max-age=${maxAge}; samesite=lax${isSecure ? '; secure' : ''}`\n}\n\n/** Persist the rail-collapse state from the browser. No-op without a document\n * so a shared toggle handler is safe to call during SSR. */\nexport function writeRailCollapsedCookie(collapsed: boolean, options: RailCookieOptions = {}): void {\n if (typeof document === 'undefined') return\n document.cookie = railCollapsedCookie(collapsed, options)\n}\n"],"mappings":"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","names":[]}
|
|
@@ -93,8 +93,17 @@ interface SessionRowActions<TIcon = unknown> {
|
|
|
93
93
|
deleteIcon?: TIcon;
|
|
94
94
|
renameLabel?: string;
|
|
95
95
|
deleteLabel?: string;
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Omit when the product cannot rename a session — the row then offers delete
|
|
98
|
+
* only, instead of a menu item that does nothing.
|
|
99
|
+
*
|
|
100
|
+
* Independently optional, matching `SessionHistoryPanel`, which has always
|
|
101
|
+
* rendered whichever of the two it was given. The rail builder used to demand
|
|
102
|
+
* both, so a product with archive-but-no-rename (tax) could either fake a
|
|
103
|
+
* rename or ship no row actions at all.
|
|
104
|
+
*/
|
|
105
|
+
onRename?: (session: SessionSummary) => void;
|
|
106
|
+
onDelete?: (session: SessionSummary) => void;
|
|
98
107
|
}
|
|
99
108
|
declare const UNTITLED_SESSION_LABEL = "Untitled chat";
|
|
100
109
|
/** Display title for a session row — trims, and falls back rather than
|
package/dist/stream/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { J as JsonRecord } from '../stream-normalizer-CnPnMaTp.js';
|
|
2
2
|
export { M as MISSING_TOOL_TERMINAL_ERROR, b as MISSING_TOOL_TERMINAL_REASON, S as StreamEvent, c as asRecord, d as asString, a as attachmentPartKey, e as collapseRedundantTextParts, f as draftAssistantParts, g as encodeEvent, h as finalizeAssistantParts, i as finalizePendingInteractionParts, j as getPartKey, m as mergePersistedPart, n as normalizePersistedPart, k as normalizeTime, l as normalizeToolEvent, r as resolveToolId, o as resolveToolName, t as terminalizeDanglingAssistantToolUpdates, p as terminalizeDanglingToolPart, q as terminalizeDanglingToolParts } from '../stream-normalizer-CnPnMaTp.js';
|
|
3
|
-
export { B as BufferedTurnEvent, a as BufferedTurnOptions, b as BufferedTurnTap, D as D1LikeForTurns, P as PumpBufferedTurnOptions, R as ReplayTurnEventsOptions,
|
|
3
|
+
export { B as BufferedTurnEvent, a as BufferedTurnOptions, b as BufferedTurnTap, D as D1LikeForTurns, c as DEFAULT_RUNNING_TURN_LEASE_MS, d as DEFAULT_RUNNING_TURN_RENEW_INTERVAL_MS, P as PumpBufferedTurnOptions, R as ReplayTurnEventsOptions, e as TURN_EVENTS_MIGRATION_SQL, f as TURN_STATUS_SCOPE_MIGRATION_SQL, T as TurnEventStore, g as TurnEventStoreOptions, h as TurnStatus, i as coalesceChatStreamEvents, j as coalesceDeltas, k as createBufferedTurnTap, l as createD1TurnEventStore, m as createMemoryTurnEventStore, p as pumpBufferedTurn, r as replayTurnEvents, s as stampReplaySeq } from '../turn-buffer-CFKQlnxf.js';
|
|
4
4
|
import '../contract-CQNvv5th.js';
|
|
5
5
|
import '@tangle-network/agent-interface';
|
|
6
6
|
|
package/dist/stream/index.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildUserTextParts,
|
|
3
|
+
messageHasTurnId,
|
|
4
|
+
normalizeClientTurnId,
|
|
5
|
+
resolveChatTurn
|
|
6
|
+
} from "../chunk-JEZJ6HTF.js";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_RUNNING_TURN_LEASE_MS,
|
|
9
|
+
DEFAULT_RUNNING_TURN_RENEW_INTERVAL_MS,
|
|
2
10
|
TURN_EVENTS_MIGRATION_SQL,
|
|
3
11
|
TURN_STATUS_SCOPE_MIGRATION_SQL,
|
|
4
|
-
buildUserTextParts,
|
|
5
12
|
coalesceChatStreamEvents,
|
|
6
13
|
coalesceDeltas,
|
|
7
14
|
createBufferedTurnTap,
|
|
8
15
|
createD1TurnEventStore,
|
|
9
16
|
createMemoryTurnEventStore,
|
|
10
|
-
messageHasTurnId,
|
|
11
|
-
normalizeClientTurnId,
|
|
12
17
|
pumpBufferedTurn,
|
|
13
18
|
replayTurnEvents,
|
|
14
|
-
resolveChatTurn,
|
|
15
19
|
stampReplaySeq
|
|
16
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-6VWA26BV.js";
|
|
17
21
|
import {
|
|
18
22
|
MISSING_TOOL_TERMINAL_ERROR,
|
|
19
23
|
MISSING_TOOL_TERMINAL_REASON,
|
|
@@ -39,6 +43,8 @@ import {
|
|
|
39
43
|
import "../chunk-3ZK5IJSW.js";
|
|
40
44
|
import "../chunk-YJMCRXQQ.js";
|
|
41
45
|
export {
|
|
46
|
+
DEFAULT_RUNNING_TURN_LEASE_MS,
|
|
47
|
+
DEFAULT_RUNNING_TURN_RENEW_INTERVAL_MS,
|
|
42
48
|
MISSING_TOOL_TERMINAL_ERROR,
|
|
43
49
|
MISSING_TOOL_TERMINAL_REASON,
|
|
44
50
|
TURN_EVENTS_MIGRATION_SQL,
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* deltas with identical concatenation.
|
|
17
17
|
*/
|
|
18
18
|
type TurnStatus = 'running' | 'complete' | 'error';
|
|
19
|
+
/** A running row is a renewable lease, not permanent truth. If the process
|
|
20
|
+
* driving a turn dies before writing a terminal status, reconnect discovery
|
|
21
|
+
* must eventually stop returning that abandoned row. */
|
|
22
|
+
declare const DEFAULT_RUNNING_TURN_LEASE_MS: number;
|
|
23
|
+
/** Keep a healthy turn's lease comfortably ahead of expiry without turning
|
|
24
|
+
* per-token streaming into status-write traffic. */
|
|
25
|
+
declare const DEFAULT_RUNNING_TURN_RENEW_INTERVAL_MS = 30000;
|
|
19
26
|
/** Represent a buffered turn event with a sequence number and serialized event data */
|
|
20
27
|
interface BufferedTurnEvent {
|
|
21
28
|
seq: number;
|
|
@@ -31,11 +38,19 @@ interface TurnEventStore {
|
|
|
31
38
|
* loses the turnId; stores that don't track scope ignore it. */
|
|
32
39
|
setStatus(turnId: string, status: TurnStatus, scopeId?: string): Promise<void>;
|
|
33
40
|
getStatus(turnId: string): Promise<TurnStatus | null>;
|
|
34
|
-
/**
|
|
35
|
-
* lost) can find and resume the in-flight turn
|
|
36
|
-
*
|
|
41
|
+
/** Unexpired running turnIds for a scope, newest first — so a reloaded client
|
|
42
|
+
* (clientRunId lost) can find and resume the in-flight turn without reviving
|
|
43
|
+
* a row abandoned by a dead process. Optional: a store records it only if
|
|
44
|
+
* `setStatus` was given a `scopeId`. */
|
|
37
45
|
listRunning?(scopeId: string): Promise<string[]>;
|
|
38
46
|
}
|
|
47
|
+
/** Configure running-turn lease evaluation. The clock is injectable so store
|
|
48
|
+
* contract tests do not sleep. Keep the default in production unless a
|
|
49
|
+
* deployment also tunes the buffer's renewal interval. */
|
|
50
|
+
interface TurnEventStoreOptions {
|
|
51
|
+
runningTurnLeaseMs?: number;
|
|
52
|
+
now?: () => number;
|
|
53
|
+
}
|
|
39
54
|
/** Merge consecutive text/reasoning deltas of the same type into one event.
|
|
40
55
|
* Concatenation-preserving: replaying the coalesced stream produces the same
|
|
41
56
|
* accumulated text as the original. */
|
|
@@ -72,6 +87,9 @@ interface BufferedTurnOptions {
|
|
|
72
87
|
/** Optional scope (thread/session id) recorded with the turn status, so
|
|
73
88
|
* {@link TurnEventStore.listRunning} can find this turn after a reload. */
|
|
74
89
|
scopeId?: string;
|
|
90
|
+
/** How often to renew the running-turn lease while a producer is alive.
|
|
91
|
+
* Default {@link DEFAULT_RUNNING_TURN_RENEW_INTERVAL_MS}. */
|
|
92
|
+
runningTurnRenewIntervalMs?: number;
|
|
75
93
|
}
|
|
76
94
|
/** A push-driven buffer for a turn whose producer the caller does NOT own. */
|
|
77
95
|
interface BufferedTurnTap {
|
|
@@ -166,8 +184,8 @@ declare const TURN_EVENTS_MIGRATION_SQL = "\nCREATE TABLE IF NOT EXISTS turn_eve
|
|
|
166
184
|
* deployments). SQLite ignores a duplicate-add error if already applied. */
|
|
167
185
|
declare const TURN_STATUS_SCOPE_MIGRATION_SQL = "ALTER TABLE turn_status ADD COLUMN scopeId TEXT;";
|
|
168
186
|
/** Resolve a TurnEventStore that appends and reads turn events using a D1-like database interface */
|
|
169
|
-
declare function createD1TurnEventStore(db: D1LikeForTurns): TurnEventStore;
|
|
187
|
+
declare function createD1TurnEventStore(db: D1LikeForTurns, options?: TurnEventStoreOptions): TurnEventStore;
|
|
170
188
|
/** In-memory store for tests and keyless local dev. */
|
|
171
|
-
declare function createMemoryTurnEventStore(): TurnEventStore;
|
|
189
|
+
declare function createMemoryTurnEventStore(options?: TurnEventStoreOptions): TurnEventStore;
|
|
172
190
|
|
|
173
|
-
export { type BufferedTurnEvent as B, type D1LikeForTurns as D, type PumpBufferedTurnOptions as P, type ReplayTurnEventsOptions as R, type TurnEventStore as T, type BufferedTurnOptions as a, type BufferedTurnTap as b,
|
|
191
|
+
export { type BufferedTurnEvent as B, type D1LikeForTurns as D, type PumpBufferedTurnOptions as P, type ReplayTurnEventsOptions as R, type TurnEventStore as T, type BufferedTurnOptions as a, type BufferedTurnTap as b, DEFAULT_RUNNING_TURN_LEASE_MS as c, DEFAULT_RUNNING_TURN_RENEW_INTERVAL_MS as d, TURN_EVENTS_MIGRATION_SQL as e, TURN_STATUS_SCOPE_MIGRATION_SQL as f, type TurnEventStoreOptions as g, type TurnStatus as h, coalesceChatStreamEvents as i, coalesceDeltas as j, createBufferedTurnTap as k, createD1TurnEventStore as l, createMemoryTurnEventStore as m, pumpBufferedTurn as p, replayTurnEvents as r, stampReplaySeq as s };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { R as ReconcileStaleTurnLockOptions } from '../stale-turn-lock-DucQzvXu.js';
|
|
2
|
-
import { T as TurnEventStore } from '../turn-buffer-
|
|
2
|
+
import { T as TurnEventStore } from '../turn-buffer-CFKQlnxf.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* `/turn-stream` core — the pure, substrate-free half of the shared durable
|
|
@@ -375,6 +375,8 @@ interface TurnStreamDOOptions {
|
|
|
375
375
|
maxSegmentEvents?: number;
|
|
376
376
|
/** Override {@link ACTIVITY_TTL_MS}. */
|
|
377
377
|
activityTtlMs?: number;
|
|
378
|
+
/** How long an unrenewed running turn remains discoverable. */
|
|
379
|
+
runningTurnLeaseMs?: number;
|
|
378
380
|
}
|
|
379
381
|
/** Manage per-turn segments and track active threads with durable event storage */
|
|
380
382
|
declare class TurnStreamDO {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
reconcileStaleTurnLock
|
|
3
3
|
} from "../chunk-CCVWR33L.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_RUNNING_TURN_LEASE_MS
|
|
6
|
+
} from "../chunk-6VWA26BV.js";
|
|
4
7
|
|
|
5
8
|
// src/turn-stream/core.ts
|
|
6
9
|
function isTerminalRunEvent(type) {
|
|
@@ -486,7 +489,11 @@ var TurnStreamDO = class {
|
|
|
486
489
|
}
|
|
487
490
|
async handleScopeRunningList() {
|
|
488
491
|
const index = await this.state.storage.get(TURN_STREAM_STORAGE_KEYS.turnScope) ?? {};
|
|
489
|
-
const
|
|
492
|
+
const cutoff = Date.now() - Math.max(
|
|
493
|
+
1,
|
|
494
|
+
this.options.runningTurnLeaseMs ?? DEFAULT_RUNNING_TURN_LEASE_MS
|
|
495
|
+
);
|
|
496
|
+
const running = Object.entries(index).filter(([, entry]) => entry.status === "running" && entry.updatedAt >= cutoff).sort((a, b) => b[1].updatedAt - a[1].updatedAt).map(([turnId]) => turnId);
|
|
490
497
|
return jsonResponse({ running });
|
|
491
498
|
}
|
|
492
499
|
};
|