@tangle-network/agent-app 0.45.63 → 0.45.65
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 +3 -3
- package/dist/chat-react/index.js +1 -1
- package/dist/{chunk-FZKNVQYP.js → chunk-23J72UUA.js} +104 -15
- package/dist/chunk-23J72UUA.js.map +1 -0
- package/dist/{chunk-FOXGPGXF.js → chunk-AWM4H5XR.js} +1393 -567
- package/dist/chunk-AWM4H5XR.js.map +1 -0
- package/dist/{chunk-PC2WYTK7.js → chunk-EDTWGSQT.js} +118 -1
- package/dist/chunk-EDTWGSQT.js.map +1 -0
- package/dist/session-shell/command-palette.d.ts +88 -0
- package/dist/session-shell/index.d.ts +1 -0
- package/dist/session-shell/index.js +13 -1
- package/dist/stories/chat-controls/fixtures.d.ts +5 -1
- package/dist/web-react/chat-composer.d.ts +115 -5
- package/dist/web-react/command-palette.d.ts +44 -0
- package/dist/web-react/composer-file-accept.d.ts +77 -0
- package/dist/web-react/index.d.ts +3 -0
- package/dist/web-react/index.js +23 -3
- package/dist/web-react/insight-card.d.ts +5 -1
- package/dist/web-react/record-grid-model.d.ts +45 -0
- package/dist/web-react/record-grid.d.ts +28 -2
- package/dist/web-react/use-composer-attachments.d.ts +3 -4
- package/dist/web-react/use-dictation.d.ts +67 -0
- package/dist/workspace-react/index.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-FOXGPGXF.js.map +0 -1
- package/dist/chunk-FZKNVQYP.js.map +0 -1
- package/dist/chunk-PC2WYTK7.js.map +0 -1
|
@@ -199,6 +199,117 @@ function ignoredCount(items, options) {
|
|
|
199
199
|
return flattenItems(items).filter((item) => ignored.has(normalizePath(item.href))).length;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
// src/session-shell/command-palette.ts
|
|
203
|
+
var COMMAND_PALETTE_SESSIONS_GROUP = "Sessions";
|
|
204
|
+
var COMMAND_PALETTE_ACTIONS_GROUP = "Actions";
|
|
205
|
+
function buildCommandPaletteItems({
|
|
206
|
+
sessions = [],
|
|
207
|
+
actions = [],
|
|
208
|
+
sessionsLabel = COMMAND_PALETTE_SESSIONS_GROUP,
|
|
209
|
+
actionsLabel = COMMAND_PALETTE_ACTIONS_GROUP,
|
|
210
|
+
untitledLabel = UNTITLED_SESSION_LABEL
|
|
211
|
+
}) {
|
|
212
|
+
const ordered = [...sessions].sort((a, b) => {
|
|
213
|
+
if (!!a.isPinned !== !!b.isPinned) return a.isPinned ? -1 : 1;
|
|
214
|
+
return compareRecent(a.updatedAt, b.updatedAt);
|
|
215
|
+
});
|
|
216
|
+
const sessionItems = ordered.map((session) => ({
|
|
217
|
+
id: session.id,
|
|
218
|
+
group: sessionsLabel,
|
|
219
|
+
label: sessionLabel(session, untitledLabel),
|
|
220
|
+
keywords: session.category ? [session.category] : void 0,
|
|
221
|
+
recentAt: session.updatedAt
|
|
222
|
+
}));
|
|
223
|
+
const actionItems = actions.map((action) => ({
|
|
224
|
+
id: action.id,
|
|
225
|
+
group: actionsLabel,
|
|
226
|
+
label: action.label,
|
|
227
|
+
description: action.description,
|
|
228
|
+
hint: action.hint,
|
|
229
|
+
keywords: action.keywords
|
|
230
|
+
}));
|
|
231
|
+
return [...sessionItems, ...actionItems];
|
|
232
|
+
}
|
|
233
|
+
function compareRecent(a, b) {
|
|
234
|
+
if (a && b) return a < b ? 1 : a > b ? -1 : 0;
|
|
235
|
+
if (a) return -1;
|
|
236
|
+
if (b) return 1;
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
function normalize(text) {
|
|
240
|
+
return text.trim().toLowerCase().replace(/\s+/g, " ");
|
|
241
|
+
}
|
|
242
|
+
var WORD_SPLIT = /[^\p{L}\p{N}]+/u;
|
|
243
|
+
function wordsOf(text) {
|
|
244
|
+
return text.split(WORD_SPLIT).filter(Boolean);
|
|
245
|
+
}
|
|
246
|
+
var KEYWORD_PENALTY = 65;
|
|
247
|
+
function scoreText(text, query) {
|
|
248
|
+
if (!query) return 0;
|
|
249
|
+
if (text === query) return 100;
|
|
250
|
+
if (text.startsWith(query)) return 90;
|
|
251
|
+
const words = wordsOf(text);
|
|
252
|
+
if (words.some((word) => word.startsWith(query))) return 80;
|
|
253
|
+
const at = text.indexOf(query);
|
|
254
|
+
if (at >= 0) return 60 + Math.max(0, 19 - at);
|
|
255
|
+
const tokens = wordsOf(query);
|
|
256
|
+
if (tokens.length > 1) {
|
|
257
|
+
let atWord = 0;
|
|
258
|
+
const inOrder = tokens.every((token) => {
|
|
259
|
+
while (atWord < words.length) {
|
|
260
|
+
const word = words[atWord];
|
|
261
|
+
atWord += 1;
|
|
262
|
+
if (word !== void 0 && (word.startsWith(token) || word.includes(token))) return true;
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
});
|
|
266
|
+
if (inOrder) return 40;
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
function scoreCommandPaletteItem(item, query) {
|
|
271
|
+
const q = normalize(query);
|
|
272
|
+
const label = scoreText(normalize(item.label), q);
|
|
273
|
+
let best = label;
|
|
274
|
+
for (const keyword of item.keywords ?? []) {
|
|
275
|
+
const score = scoreText(normalize(keyword), q);
|
|
276
|
+
if (score !== null) {
|
|
277
|
+
const penalized = score - KEYWORD_PENALTY;
|
|
278
|
+
if (best === null || penalized > best) best = penalized;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return best;
|
|
282
|
+
}
|
|
283
|
+
function filterCommandPaletteItems(items, query) {
|
|
284
|
+
if (!normalize(query)) return [...items];
|
|
285
|
+
const scored = [];
|
|
286
|
+
items.forEach((item, index) => {
|
|
287
|
+
const score = scoreCommandPaletteItem(item, query);
|
|
288
|
+
if (score !== null) scored.push({ item, score, index });
|
|
289
|
+
});
|
|
290
|
+
scored.sort((a, b) => {
|
|
291
|
+
if (a.score !== b.score) return b.score - a.score;
|
|
292
|
+
const recent = compareRecent(a.item.recentAt, b.item.recentAt);
|
|
293
|
+
if (recent !== 0) return recent;
|
|
294
|
+
return a.index - b.index;
|
|
295
|
+
});
|
|
296
|
+
return scored.map(({ item }) => item);
|
|
297
|
+
}
|
|
298
|
+
function groupCommandPaletteItems(items) {
|
|
299
|
+
const groups = [];
|
|
300
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
301
|
+
for (const item of items) {
|
|
302
|
+
let group = byGroup.get(item.group);
|
|
303
|
+
if (!group) {
|
|
304
|
+
group = { group: item.group, items: [] };
|
|
305
|
+
byGroup.set(item.group, group);
|
|
306
|
+
groups.push(group);
|
|
307
|
+
}
|
|
308
|
+
group.items.push(item);
|
|
309
|
+
}
|
|
310
|
+
return groups;
|
|
311
|
+
}
|
|
312
|
+
|
|
202
313
|
// src/session-shell/index.ts
|
|
203
314
|
var UNTITLED_SESSION_LABEL = "Untitled chat";
|
|
204
315
|
function sessionLabel(session, untitled = UNTITLED_SESSION_LABEL) {
|
|
@@ -388,6 +499,12 @@ export {
|
|
|
388
499
|
flattenRouteTable,
|
|
389
500
|
checkNavHrefs,
|
|
390
501
|
assertNavHrefsRegistered,
|
|
502
|
+
COMMAND_PALETTE_SESSIONS_GROUP,
|
|
503
|
+
COMMAND_PALETTE_ACTIONS_GROUP,
|
|
504
|
+
buildCommandPaletteItems,
|
|
505
|
+
scoreCommandPaletteItem,
|
|
506
|
+
filterCommandPaletteItems,
|
|
507
|
+
groupCommandPaletteItems,
|
|
391
508
|
UNTITLED_SESSION_LABEL,
|
|
392
509
|
sessionLabel,
|
|
393
510
|
buildSessionSubItems,
|
|
@@ -402,4 +519,4 @@ export {
|
|
|
402
519
|
railCollapsedCookie,
|
|
403
520
|
writeRailCollapsedCookie
|
|
404
521
|
};
|
|
405
|
-
//# sourceMappingURL=chunk-
|
|
522
|
+
//# sourceMappingURL=chunk-EDTWGSQT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/session-shell/path.ts","../src/session-shell/nav-guard.ts","../src/session-shell/command-palette.ts","../src/session-shell/index.ts"],"sourcesContent":["/**\n * Path normalisation shared by the shell's routing helpers. Segment-aligned\n * comparison is the invariant: `/vault` must never claim `/vault-archive`, so\n * every prefix test here works on whole segments rather than string prefixes.\n */\n\nexport function stripTrailingSlashes(value: string): string {\n return value.replace(/\\/+$/, '')\n}\n\n/** Bare segment name, so a caller may pass `'/settings'` or `'settings'`. */\nexport function 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. */\nexport function 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`. */\nexport function 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\n/** Non-empty segments of a path or route pattern. Leading/trailing/duplicate\n * slashes collapse, so `/app//x/` and `app/x` compare equal. */\nexport function toSegments(value: string): string[] {\n return value.split('/').filter((segment) => segment.length > 0)\n}\n\n/** Canonical display form: rooted, no trailing slash, no empty segments. */\nexport function toRootedPath(value: string): string {\n return `/${toSegments(value).join('/')}`\n}\n","/**\n * Nav destinations, and the guard that proves every href the rail renders\n * resolves to a route the product's router actually registered.\n *\n * A rail row's href is assembled from a base plus a relative path, and nothing\n * downstream re-checks it: the sidebar renders a link, the click navigates, and\n * the router answers 404. A unit test written against the nav builder alone\n * cannot catch that — it asserts the href the builder produced, which is the\n * same wrong string the user clicks.\n *\n * Two mechanisms, meant to be used together:\n *\n * 1. `NavDestination` makes the base a REQUIRED discriminant (`scope`). An\n * optional `absolute?: boolean`-style flag has the opposite property:\n * omitting it type-checks, and the destination silently resolves under the\n * workspace prefix instead of the app-level one. A required literal union\n * turns that omission into a compile error, and widening `TScope` demands a\n * base for the new scope rather than defaulting to a wrong one.\n * 2. `assertNavHrefsRegistered` matches every resolved href against the route\n * table, so a destination the router never registered fails a test instead\n * of a user's click. It reads the product's real route table, so it cannot\n * agree with the builder's mistake the way a hand-maintained expected-href\n * list does.\n */\n\nimport { isUnderPrefix, normalizePath, stripTrailingSlashes, toRootedPath, toSegments } from './path'\n\n// ---------------------------------------------------------------------------\n// Destinations — the base is a required discriminant, never an optional flag\n// ---------------------------------------------------------------------------\n\n/** The bases a product routes rail rows under. `workspace` is the per-workspace\n * prefix (`/app/ws_123`); `app` is the account-level one (`/app`), where\n * singleton surfaces such as a shared terminal or billing live. */\nexport type NavScope = 'workspace' | 'app'\n\n/** A base path per scope. Widening `TScope` widens this record, so a product\n * that adds a scope cannot compile until it supplies that scope's base. */\nexport type NavScopeBases<TScope extends string = NavScope> = Readonly<Record<TScope, string>>\n\n/** One rail destination as the product declares it, before a base is applied. */\nexport interface NavDestination<TScope extends string = NavScope> {\n id: string\n /** Path relative to the base named by `scope`. `''` is the base itself.\n * Must be empty or start with `/` — a bare `'vault'` would concatenate into\n * `/app/ws_123vault`, so it is rejected rather than silently repaired. */\n path: string\n /** Which base `path` resolves against. Required on purpose. */\n scope: TScope\n}\n\n/** A destination with its base applied. */\nexport interface ResolvedNavDestination<TScope extends string = NavScope> {\n id: string\n href: string\n scope: TScope\n}\n\n/** Apply a destination's scope base to its path.\n *\n * Throws when the scope has no base configured — a product that assembles\n * `bases` dynamically can defeat the type-level guarantee, and a missing base\n * would otherwise produce `undefined/vault`. */\nexport function resolveNavHref<TScope extends string>(\n destination: NavDestination<TScope>,\n bases: NavScopeBases<TScope>,\n): string {\n const base = bases[destination.scope]\n if (typeof base !== 'string') {\n throw new Error(\n `Nav destination '${destination.id}' uses scope '${destination.scope}', which has no configured base`,\n )\n }\n if (destination.path !== '' && !destination.path.startsWith('/')) {\n throw new Error(\n `Nav destination '${destination.id}' path must be empty or start with '/' (got '${destination.path}')`,\n )\n }\n const rooted = `${stripTrailingSlashes(base)}${destination.path}`\n return rooted === '' ? '/' : stripTrailingSlashes(rooted)\n}\n\n/** Apply the bases to every destination, preserving declaration order. */\nexport function resolveNavDestinations<TScope extends string>(\n destinations: readonly NavDestination<TScope>[],\n bases: NavScopeBases<TScope>,\n): ResolvedNavDestination<TScope>[] {\n return destinations.map((destination) => ({\n id: destination.id,\n href: resolveNavHref(destination, bases),\n scope: destination.scope,\n }))\n}\n\nexport interface ResolveScopedActiveNavIdOptions<TScope extends string = NavScope> {\n pathname: string\n destinations: readonly NavDestination<TScope>[]\n bases: NavScopeBases<TScope>\n /** Extra ABSOLUTE prefixes that light an existing row, e.g.\n * `{ '/app/ws_1/agents': 'integrations' }`. Same longest-prefix contest. */\n aliases?: Readonly<Record<string, string>>\n /** ABSOLUTE prefixes that deliberately highlight nothing, beating any shorter\n * match. */\n claimsNothing?: readonly string[]\n}\n\n/**\n * The rail row to highlight, across scopes.\n *\n * `resolveActiveNavId` resolves rows against ONE base, so an app-level row can\n * only be highlighted by a second, hand-rolled scan — the same split that lets\n * an app-level destination render under the workspace base. This resolves the\n * hrefs first and runs a single longest-prefix contest over absolute paths, so\n * declaration order cannot change the answer and no scope needs its own pass.\n *\n * Prefixes in `aliases` / `claimsNothing` are absolute here, unlike\n * `resolveActiveNavId`'s base-relative ones, because the contest itself is\n * absolute.\n */\nexport function resolveScopedActiveNavId<TScope extends string>({\n pathname,\n destinations,\n bases,\n aliases,\n claimsNothing,\n}: ResolveScopedActiveNavIdOptions<TScope>): string | undefined {\n const path = normalizePath(pathname)\n let bestLength = -1\n let bestId: string | undefined\n const consider = (candidate: string, id: string | undefined, winsTies = false): void => {\n const full = stripTrailingSlashes(candidate)\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 resolved of resolveNavDestinations(destinations, bases)) consider(resolved.href, resolved.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 here is a\n // deliberate override of the row that owns it.\n for (const prefix of claimsNothing ?? []) consider(prefix, undefined, true)\n return bestId\n}\n\n// ---------------------------------------------------------------------------\n// Route table — structurally the product's own router config\n// ---------------------------------------------------------------------------\n\n/**\n * One entry of a registered route table. Structurally compatible with\n * react-router's `RouteConfigEntry`, so a product passes its real `routes.ts`\n * default export straight in — the point of the guard is that it reads the\n * router's own truth rather than a second list that can agree with the bug.\n */\nexport interface RegisteredRoute {\n /** Absent on a pathless layout route: its children inherit the parent path. */\n path?: string\n index?: boolean\n children?: readonly RegisteredRoute[]\n}\n\n/** A route table entry is either a bare pattern string or a router config node. */\nexport type NavRouteTable = readonly (string | RegisteredRoute)[]\n\nfunction joinPattern(parent: string, child: string): string {\n if (child.startsWith('/')) return child\n if (child === '') return parent\n return `${parent}/${child}`\n}\n\n/**\n * Every path pattern the table registers, rooted and de-duplicated.\n *\n * Parent nodes contribute their own cumulative path as well as their children's:\n * a router matches a parent route with an index child at the parent path, and a\n * parent without one still matches with an empty outlet, so treating parents as\n * unregistered would flag working hrefs.\n */\nexport function flattenRouteTable(table: NavRouteTable): string[] {\n const patterns: string[] = []\n const walk = (entries: NavRouteTable, parent: string): void => {\n for (const entry of entries) {\n if (typeof entry === 'string') {\n patterns.push(toRootedPath(joinPattern(parent, entry)))\n continue\n }\n const own = entry.path === undefined ? parent : joinPattern(parent, entry.path)\n patterns.push(toRootedPath(own))\n if (entry.children) walk(entry.children, own)\n }\n }\n walk(table, '')\n return [...new Set(patterns)]\n}\n\n/**\n * Whole-path segment match of a concrete path against one route pattern.\n *\n * Supports the three pattern forms a router uses: literal segments, `:param`\n * (exactly one non-empty segment), optional `:param?` / `segment?` (zero or\n * one), and a trailing `*` splat (zero or more). Matching is recursive because\n * an optional segment forks the walk — a linear scan silently mismatches\n * `/a/b` against `a/:x?/b`.\n */\nfunction matchesPattern(pathSegments: readonly string[], patternSegments: readonly string[], caseSensitive: boolean): boolean {\n if (patternSegments.length === 0) return pathSegments.length === 0\n const head = patternSegments[0] ?? ''\n if (head === '*') return true\n const rest = patternSegments.slice(1)\n const optional = head.endsWith('?')\n const core = optional ? head.slice(0, -1) : head\n const first = pathSegments[0]\n if (first !== undefined) {\n const hit = core.startsWith(':')\n ? first.length > 0\n : caseSensitive\n ? core === first\n : core.toLowerCase() === first.toLowerCase()\n if (hit && matchesPattern(pathSegments.slice(1), rest, caseSensitive)) return true\n }\n return optional ? matchesPattern(pathSegments, rest, caseSensitive) : false\n}\n\n// ---------------------------------------------------------------------------\n// The guard\n// ---------------------------------------------------------------------------\n\n/**\n * A nav row as the guard needs to see it. Structurally satisfied by\n * `SessionRailNavItem` / `SessionRailSubItem` and by sandbox-ui's\n * `SidebarLayoutNavItem`, so the guard runs over the builder's real output\n * rather than a re-declaration of it.\n */\nexport interface NavHrefItem {\n id: string\n href: string\n subItems?: readonly NavHrefItem[]\n}\n\nexport type NavHrefProblemReason =\n /** No registered pattern matches the resolved href. */\n | 'unregistered'\n /** Empty, fragment-only, or not rooted at `/` — the row navigates nowhere\n * predictable regardless of the route table. */\n | 'not-a-path'\n /** Leaves the router (scheme or protocol-relative) while `allowExternal` is\n * off. */\n | 'external'\n\nexport interface NavHrefProblem {\n id: string\n href: string\n reason: NavHrefProblemReason\n /** Registered patterns ending in the same segment. A destination resolved\n * under the wrong base lands here as its correctly-based twin, which is what\n * names the missing scope in the failure message. */\n nearest: string[]\n message: string\n}\n\nexport interface NavHrefReport {\n /** Hrefs examined, including nested sub-items. */\n checked: number\n problems: NavHrefProblem[]\n /** Off-router destinations accepted because `allowExternal` is on. */\n external: string[]\n /** The flattened route table the check ran against. */\n patterns: string[]\n}\n\nexport interface NavHrefCheckOptions {\n /** Hrefs to skip, compared after query/fragment removal. For a destination\n * served outside this route table (a static asset, another worker). */\n ignore?: readonly string[]\n /** Absolute URLs / `mailto:` / `tel:` are reported under `external` instead\n * of failing. Default true. */\n allowExternal?: boolean\n /** Compare literal segments case-sensitively. Default true — a router that\n * matches case-insensitively still renders a link the deploy's CDN or a\n * case-sensitive origin may not. */\n caseSensitive?: boolean\n}\n\n/** `scheme:` or `//host` — anything the router will not resolve as a path. */\nconst OFF_ROUTER_HREF = /^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i\n\nfunction flattenItems(items: readonly NavHrefItem[], out: NavHrefItem[] = []): NavHrefItem[] {\n for (const item of items) {\n out.push(item)\n if (item.subItems) flattenItems(item.subItems, out)\n }\n return out\n}\n\n/**\n * Check every nav href against the product's route table.\n *\n * Pure — returns the full report so a caller can assert on parts of it. Use\n * {@link assertNavHrefsRegistered} in tests; it turns the report into a failure\n * that names the offending row, its resolved href, and the near-miss pattern.\n */\nexport function checkNavHrefs(\n items: readonly NavHrefItem[],\n routes: NavRouteTable,\n options: NavHrefCheckOptions = {},\n): NavHrefReport {\n const { ignore, allowExternal = true, caseSensitive = true } = options\n const patterns = flattenRouteTable(routes)\n const patternSegments = patterns.map((pattern) => ({ pattern, segments: toSegments(pattern) }))\n const ignored = new Set((ignore ?? []).map((href) => normalizePath(href)))\n const problems: NavHrefProblem[] = []\n const external: string[] = []\n const flat = flattenItems(items)\n let checked = 0\n\n for (const item of flat) {\n const raw = item.href\n const path = normalizePath(raw)\n if (ignored.has(path)) continue\n if (OFF_ROUTER_HREF.test(raw)) {\n if (allowExternal) {\n external.push(raw)\n continue\n }\n // Counted as checked: it was examined and rejected, so the vacuous-pass\n // guard must not read this run as \"nothing was looked at\".\n checked += 1\n problems.push({\n id: item.id,\n href: raw,\n reason: 'external',\n nearest: [],\n message: `Nav item '${item.id}' href '${raw}' leaves the router, and external destinations are rejected`,\n })\n continue\n }\n checked += 1\n if (raw === '' || raw.startsWith('#') || !raw.startsWith('/')) {\n problems.push({\n id: item.id,\n href: raw,\n reason: 'not-a-path',\n nearest: [],\n message: `Nav item '${item.id}' href '${raw}' is not a rooted path — it cannot resolve to a registered route`,\n })\n continue\n }\n const segments = toSegments(path)\n if (patternSegments.some((candidate) => matchesPattern(segments, candidate.segments, caseSensitive))) continue\n const nearest = nearestPatterns(segments, patternSegments.map((candidate) => candidate.pattern), caseSensitive)\n problems.push({\n id: item.id,\n href: raw,\n reason: 'unregistered',\n nearest,\n message:\n `Nav item '${item.id}' href '${raw}' matches no registered route` +\n (nearest.length ? ` — nearest registered: ${nearest.join(', ')}` : ''),\n })\n }\n\n return { checked, problems, external, patterns }\n}\n\n/** Registered patterns whose last segment equals the href's last segment: the\n * same destination under a different base is the near-miss worth printing. */\nfunction nearestPatterns(segments: readonly string[], patterns: readonly string[], caseSensitive: boolean): string[] {\n const tail = segments[segments.length - 1]\n if (tail === undefined) return []\n const same = (a: string, b: string): boolean => (caseSensitive ? a === b : a.toLowerCase() === b.toLowerCase())\n return patterns\n .filter((pattern) => {\n const patternTail = toSegments(pattern).at(-1)\n return patternTail !== undefined && same(patternTail, tail)\n })\n .slice(0, 5)\n}\n\n/**\n * Fail unless every nav href resolves to a registered route.\n *\n * Throws on an empty item list or an empty route table as well: a guard that\n * examined nothing reports safety it does not provide, and both are what a\n * mis-wired import looks like.\n */\nexport function assertNavHrefsRegistered(\n items: readonly NavHrefItem[],\n routes: NavRouteTable,\n options: NavHrefCheckOptions = {},\n): void {\n if (items.length === 0) {\n throw new Error('assertNavHrefsRegistered received no nav items — the check would pass without examining anything')\n }\n const report = checkNavHrefs(items, routes, options)\n if (report.patterns.length === 0) {\n throw new Error('assertNavHrefsRegistered received an empty route table — every href would fail or nothing would be proven')\n }\n // Real problems are reported before the vacuous-pass guard: rejected external\n // hrefs are problems that were never \"checked\", and the guard's message would\n // otherwise hide them.\n if (report.problems.length > 0) {\n const detail = report.problems.map((problem) => ` - ${problem.message}`).join('\\n')\n throw new Error(\n `${report.problems.length} of ${report.checked} nav hrefs do not resolve to a registered route:\\n${detail}\\n` +\n `Registered patterns (${report.patterns.length}): ${report.patterns.join(', ')}`,\n )\n }\n if (report.checked === 0) {\n throw new Error(\n `assertNavHrefsRegistered examined 0 hrefs (${report.external.length} external, ${ignoredCount(items, options)} ignored) — the check would pass without examining anything`,\n )\n }\n}\n\nfunction ignoredCount(items: readonly NavHrefItem[], options: NavHrefCheckOptions): number {\n const ignored = new Set((options.ignore ?? []).map((href) => normalizePath(href)))\n return flattenItems(items).filter((item) => ignored.has(normalizePath(item.href))).length\n}\n","/**\n * Command palette — the React-free selection half of the Cmd/Ctrl+K surface\n * (`/web-react` holds the rendered half, `CommandPalette`).\n *\n * Pure and import-free beyond this module's own types: no React, no DOM, no\n * fuse.js. A route loader or a worker can build and rank the same items the\n * browser renders.\n *\n * Domain stays a parameter. The palette knows two kinds of row — a SESSION the\n * user can jump to and an ACTION the product offers (new chat, toggle theme,\n * open settings) — and both arrive as data. What a selection DOES is the\n * product's business; the shell only builds, ranks, and groups.\n *\n * Ranking is the documented ladder, not a fuzzy library: exact > prefix >\n * word-prefix > substring (earlier index wins) > token-order, with keyword\n * hits ranked a fixed step below the same hit on the label. Deterministic —\n * no index-building, no async, same input always sorts the same way.\n */\n\nimport { sessionLabel, UNTITLED_SESSION_LABEL, type SessionSummary } from './index'\n\n/** A product-supplied palette action. `hint` is the right-aligned affordance\n * copy (a kbd chord, a route name) — rendered verbatim, never interpreted. */\nexport interface CommandPaletteAction {\n id: string\n label: string\n description?: string\n hint?: string\n /** Extra match vocabulary that never renders (`settings` matching\n * \"preferences\"). A keyword hit ranks below the same hit on the label. */\n keywords?: string[]\n}\n\n/** One selectable row. `group` is the section header it renders under. */\nexport interface CommandPaletteItem {\n id: string\n group: string\n label: string\n description?: string\n hint?: string\n keywords?: string[]\n /** Recency key (ISO-8601). Breaks score ties and orders the unfiltered\n * list recent-first. Rows without one sort below rows with one. */\n recentAt?: string | null\n}\n\n/** One rendered section: a header plus its rows, in first-seen group order. */\nexport interface CommandPaletteGroup {\n group: string\n items: CommandPaletteItem[]\n}\n\nexport const COMMAND_PALETTE_SESSIONS_GROUP = 'Sessions'\nexport const COMMAND_PALETTE_ACTIONS_GROUP = 'Actions'\n\nexport interface BuildCommandPaletteItemsOptions {\n sessions?: readonly SessionSummary[]\n actions?: readonly CommandPaletteAction[]\n /** Section label for sessions. Default \"Sessions\". */\n sessionsLabel?: string\n /** Section label for actions. Default \"Actions\". */\n actionsLabel?: string\n /** Placeholder title for an untitled session. */\n untitledLabel?: string\n}\n\n/**\n * Flatten sessions + actions into palette items, sessions group first (the\n * jump-back-in list), actions after. Sessions order recent-first by\n * `updatedAt` — a palette with an empty query IS the recency list, so the\n * build order is the render order and the filter never has to re-derive it.\n * Pinned sessions lead the recency sort, matching the rail.\n */\nexport function buildCommandPaletteItems({\n sessions = [],\n actions = [],\n sessionsLabel = COMMAND_PALETTE_SESSIONS_GROUP,\n actionsLabel = COMMAND_PALETTE_ACTIONS_GROUP,\n untitledLabel = UNTITLED_SESSION_LABEL,\n}: BuildCommandPaletteItemsOptions): CommandPaletteItem[] {\n const ordered = [...sessions].sort((a, b) => {\n if (!!a.isPinned !== !!b.isPinned) return a.isPinned ? -1 : 1\n return compareRecent(a.updatedAt, b.updatedAt)\n })\n const sessionItems: CommandPaletteItem[] = ordered.map((session) => ({\n id: session.id,\n group: sessionsLabel,\n label: sessionLabel(session, untitledLabel),\n keywords: session.category ? [session.category] : undefined,\n recentAt: session.updatedAt,\n }))\n const actionItems: CommandPaletteItem[] = actions.map((action) => ({\n id: action.id,\n group: actionsLabel,\n label: action.label,\n description: action.description,\n hint: action.hint,\n keywords: action.keywords,\n }))\n return [...sessionItems, ...actionItems]\n}\n\n/** Newer first; an undated row sorts below every dated one. ISO strings\n * compare lexicographically. */\nfunction compareRecent(a: string | null | undefined, b: string | null | undefined): number {\n if (a && b) return a < b ? 1 : a > b ? -1 : 0\n if (a) return -1\n if (b) return 1\n return 0\n}\n\n/** Collapse whitespace, lowercase once — every comparison runs on this form. */\nfunction normalize(text: string): string {\n return text.trim().toLowerCase().replace(/\\s+/g, ' ')\n}\n\n/** Words of a normalized string: runs of letters/numbers, so \"new-chat\" and\n * \"new chat\" word-prefix identically. */\nconst WORD_SPLIT = /[^\\p{L}\\p{N}]+/u\n\nfunction wordsOf(text: string): string[] {\n return text.split(WORD_SPLIT).filter(Boolean)\n}\n\n/** How far below a label hit the same keyword hit ranks: any label match, even\n * a token-order one (40), outranks a keyword exact match (100 − 65 = 35). */\nconst KEYWORD_PENALTY = 65\n\n/**\n * Score one text against the query on the prefix > substring > token-order\n * ladder. Returns `null` for no match. Higher is better; tiers are spaced so\n * no within-tier adjustment can cross a tier boundary:\n *\n * - 100 exact\n * - 90 prefix (`text` starts with the query)\n * - 80 word-prefix (a WORD starts with the query — \"chat\" hits \"New chat\")\n * - 60–79 substring, minus the match index clamped to 19 (earlier wins)\n * - 40 token-order (every query token matches a later word, in order)\n */\nfunction scoreText(text: string, query: string): number | null {\n if (!query) return 0\n if (text === query) return 100\n if (text.startsWith(query)) return 90\n const words = wordsOf(text)\n if (words.some((word) => word.startsWith(query))) return 80\n const at = text.indexOf(query)\n if (at >= 0) return 60 + Math.max(0, 19 - at)\n const tokens = wordsOf(query)\n if (tokens.length > 1) {\n let atWord = 0\n const inOrder = tokens.every((token) => {\n while (atWord < words.length) {\n const word = words[atWord]\n atWord += 1\n // Guarded by the while condition; noUncheckedIndexedAccess can't see it.\n if (word !== undefined && (word.startsWith(token) || word.includes(token))) return true\n }\n return false\n })\n if (inOrder) return 40\n }\n return null\n}\n\n/**\n * Score an item: the best label score, or the best keyword score a fixed step\n * below. `null` when neither matches — the item is filtered out. An empty\n * query scores every item 0 (the caller keeps build order: recent-first).\n */\nexport function scoreCommandPaletteItem(item: CommandPaletteItem, query: string): number | null {\n const q = normalize(query)\n const label = scoreText(normalize(item.label), q)\n let best = label\n for (const keyword of item.keywords ?? []) {\n const score = scoreText(normalize(keyword), q)\n if (score !== null) {\n const penalized = score - KEYWORD_PENALTY\n if (best === null || penalized > best) best = penalized\n }\n }\n return best\n}\n\n/**\n * Filter + rank: an empty query returns the items untouched (build order is\n * the recency order); a real query drops non-matches and sorts by score, then\n * recency, then original position — stable and deterministic.\n */\nexport function filterCommandPaletteItems(\n items: readonly CommandPaletteItem[],\n query: string,\n): CommandPaletteItem[] {\n if (!normalize(query)) return [...items]\n const scored: Array<{ item: CommandPaletteItem; score: number; index: number }> = []\n items.forEach((item, index) => {\n const score = scoreCommandPaletteItem(item, query)\n if (score !== null) scored.push({ item, score, index })\n })\n scored.sort((a, b) => {\n if (a.score !== b.score) return b.score - a.score\n const recent = compareRecent(a.item.recentAt, b.item.recentAt)\n if (recent !== 0) return recent\n return a.index - b.index\n })\n return scored.map(({ item }) => item)\n}\n\n/**\n * Fold a flat (already ordered) item list into renderable sections. Groups\n * appear in first-seen order and each group appears ONCE — a filtered ranking\n * interleaves sessions and actions by score, and folding only consecutive runs\n * would render the same header twice. Within a group, rows keep the flat\n * order. Empty groups vanish, so a filter that leaves only actions renders no\n * \"Sessions\" header over nothing.\n */\nexport function groupCommandPaletteItems(items: readonly CommandPaletteItem[]): CommandPaletteGroup[] {\n const groups: CommandPaletteGroup[] = []\n const byGroup = new Map<string, CommandPaletteGroup>()\n for (const item of items) {\n let group = byGroup.get(item.group)\n if (!group) {\n group = { group: item.group, items: [] }\n byGroup.set(item.group, group)\n groups.push(group)\n }\n group.items.push(item)\n }\n return groups\n}\n","/**\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\nimport { isUnderPrefix, normalizePath, stripSlashes, stripTrailingSlashes } from './path'\n\nexport * from './nav-guard'\nexport * from './command-palette'\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 * Row actions this shell has no opinion about — pin, categorise, duplicate,\n * share. Evaluated per session so a label can read that row's state\n * (\"Pin\" vs \"Unpin\"), and ordered between rename and delete so the\n * destructive action stays last.\n *\n * `id` must not be `rename` or `delete`; those are the shell's own.\n */\n extraActions?: (session: SessionSummary) => SessionRailAction<TIcon>[]\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, extraActions } = 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 (extraActions) built.push(...extraActions(session))\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\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":[]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command palette — the React-free selection half of the Cmd/Ctrl+K surface
|
|
3
|
+
* (`/web-react` holds the rendered half, `CommandPalette`).
|
|
4
|
+
*
|
|
5
|
+
* Pure and import-free beyond this module's own types: no React, no DOM, no
|
|
6
|
+
* fuse.js. A route loader or a worker can build and rank the same items the
|
|
7
|
+
* browser renders.
|
|
8
|
+
*
|
|
9
|
+
* Domain stays a parameter. The palette knows two kinds of row — a SESSION the
|
|
10
|
+
* user can jump to and an ACTION the product offers (new chat, toggle theme,
|
|
11
|
+
* open settings) — and both arrive as data. What a selection DOES is the
|
|
12
|
+
* product's business; the shell only builds, ranks, and groups.
|
|
13
|
+
*
|
|
14
|
+
* Ranking is the documented ladder, not a fuzzy library: exact > prefix >
|
|
15
|
+
* word-prefix > substring (earlier index wins) > token-order, with keyword
|
|
16
|
+
* hits ranked a fixed step below the same hit on the label. Deterministic —
|
|
17
|
+
* no index-building, no async, same input always sorts the same way.
|
|
18
|
+
*/
|
|
19
|
+
import { type SessionSummary } from './index';
|
|
20
|
+
/** A product-supplied palette action. `hint` is the right-aligned affordance
|
|
21
|
+
* copy (a kbd chord, a route name) — rendered verbatim, never interpreted. */
|
|
22
|
+
export interface CommandPaletteAction {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
hint?: string;
|
|
27
|
+
/** Extra match vocabulary that never renders (`settings` matching
|
|
28
|
+
* "preferences"). A keyword hit ranks below the same hit on the label. */
|
|
29
|
+
keywords?: string[];
|
|
30
|
+
}
|
|
31
|
+
/** One selectable row. `group` is the section header it renders under. */
|
|
32
|
+
export interface CommandPaletteItem {
|
|
33
|
+
id: string;
|
|
34
|
+
group: string;
|
|
35
|
+
label: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
hint?: string;
|
|
38
|
+
keywords?: string[];
|
|
39
|
+
/** Recency key (ISO-8601). Breaks score ties and orders the unfiltered
|
|
40
|
+
* list recent-first. Rows without one sort below rows with one. */
|
|
41
|
+
recentAt?: string | null;
|
|
42
|
+
}
|
|
43
|
+
/** One rendered section: a header plus its rows, in first-seen group order. */
|
|
44
|
+
export interface CommandPaletteGroup {
|
|
45
|
+
group: string;
|
|
46
|
+
items: CommandPaletteItem[];
|
|
47
|
+
}
|
|
48
|
+
export declare const COMMAND_PALETTE_SESSIONS_GROUP = "Sessions";
|
|
49
|
+
export declare const COMMAND_PALETTE_ACTIONS_GROUP = "Actions";
|
|
50
|
+
export interface BuildCommandPaletteItemsOptions {
|
|
51
|
+
sessions?: readonly SessionSummary[];
|
|
52
|
+
actions?: readonly CommandPaletteAction[];
|
|
53
|
+
/** Section label for sessions. Default "Sessions". */
|
|
54
|
+
sessionsLabel?: string;
|
|
55
|
+
/** Section label for actions. Default "Actions". */
|
|
56
|
+
actionsLabel?: string;
|
|
57
|
+
/** Placeholder title for an untitled session. */
|
|
58
|
+
untitledLabel?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Flatten sessions + actions into palette items, sessions group first (the
|
|
62
|
+
* jump-back-in list), actions after. Sessions order recent-first by
|
|
63
|
+
* `updatedAt` — a palette with an empty query IS the recency list, so the
|
|
64
|
+
* build order is the render order and the filter never has to re-derive it.
|
|
65
|
+
* Pinned sessions lead the recency sort, matching the rail.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildCommandPaletteItems({ sessions, actions, sessionsLabel, actionsLabel, untitledLabel, }: BuildCommandPaletteItemsOptions): CommandPaletteItem[];
|
|
68
|
+
/**
|
|
69
|
+
* Score an item: the best label score, or the best keyword score a fixed step
|
|
70
|
+
* below. `null` when neither matches — the item is filtered out. An empty
|
|
71
|
+
* query scores every item 0 (the caller keeps build order: recent-first).
|
|
72
|
+
*/
|
|
73
|
+
export declare function scoreCommandPaletteItem(item: CommandPaletteItem, query: string): number | null;
|
|
74
|
+
/**
|
|
75
|
+
* Filter + rank: an empty query returns the items untouched (build order is
|
|
76
|
+
* the recency order); a real query drops non-matches and sorts by score, then
|
|
77
|
+
* recency, then original position — stable and deterministic.
|
|
78
|
+
*/
|
|
79
|
+
export declare function filterCommandPaletteItems(items: readonly CommandPaletteItem[], query: string): CommandPaletteItem[];
|
|
80
|
+
/**
|
|
81
|
+
* Fold a flat (already ordered) item list into renderable sections. Groups
|
|
82
|
+
* appear in first-seen order and each group appears ONCE — a filtered ranking
|
|
83
|
+
* interleaves sessions and actions by score, and folding only consecutive runs
|
|
84
|
+
* would render the same header twice. Within a group, rows keep the flat
|
|
85
|
+
* order. Empty groups vanish, so a filter that leaves only actions renders no
|
|
86
|
+
* "Sessions" header over nothing.
|
|
87
|
+
*/
|
|
88
|
+
export declare function groupCommandPaletteItems(items: readonly CommandPaletteItem[]): CommandPaletteGroup[];
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* rather than the shell knowing any URL.
|
|
16
16
|
*/
|
|
17
17
|
export * from './nav-guard';
|
|
18
|
+
export * from './command-palette';
|
|
18
19
|
/** One session as the shell needs to see it. Products map their own row
|
|
19
20
|
* (thread / session / matter) onto this before handing it over. */
|
|
20
21
|
export interface SessionSummary {
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
COMMAND_PALETTE_ACTIONS_GROUP,
|
|
3
|
+
COMMAND_PALETTE_SESSIONS_GROUP,
|
|
2
4
|
DEFAULT_RAIL_COOKIE_NAME,
|
|
3
5
|
UNTITLED_SESSION_LABEL,
|
|
4
6
|
activeSessionIdFromPath,
|
|
5
7
|
assertNavHrefsRegistered,
|
|
8
|
+
buildCommandPaletteItems,
|
|
6
9
|
buildSessionNavItem,
|
|
7
10
|
buildSessionSubItems,
|
|
8
11
|
checkNavHrefs,
|
|
9
12
|
composeSidebarSessions,
|
|
13
|
+
filterCommandPaletteItems,
|
|
10
14
|
flattenRouteTable,
|
|
15
|
+
groupCommandPaletteItems,
|
|
11
16
|
mergeSessionPages,
|
|
12
17
|
railCollapsedCookie,
|
|
13
18
|
readRailCollapsedCookie,
|
|
@@ -16,19 +21,25 @@ import {
|
|
|
16
21
|
resolveNavHref,
|
|
17
22
|
resolveScopedActiveNavId,
|
|
18
23
|
resolveSessionUnread,
|
|
24
|
+
scoreCommandPaletteItem,
|
|
19
25
|
sessionLabel,
|
|
20
26
|
writeRailCollapsedCookie
|
|
21
|
-
} from "../chunk-
|
|
27
|
+
} from "../chunk-EDTWGSQT.js";
|
|
22
28
|
export {
|
|
29
|
+
COMMAND_PALETTE_ACTIONS_GROUP,
|
|
30
|
+
COMMAND_PALETTE_SESSIONS_GROUP,
|
|
23
31
|
DEFAULT_RAIL_COOKIE_NAME,
|
|
24
32
|
UNTITLED_SESSION_LABEL,
|
|
25
33
|
activeSessionIdFromPath,
|
|
26
34
|
assertNavHrefsRegistered,
|
|
35
|
+
buildCommandPaletteItems,
|
|
27
36
|
buildSessionNavItem,
|
|
28
37
|
buildSessionSubItems,
|
|
29
38
|
checkNavHrefs,
|
|
30
39
|
composeSidebarSessions,
|
|
40
|
+
filterCommandPaletteItems,
|
|
31
41
|
flattenRouteTable,
|
|
42
|
+
groupCommandPaletteItems,
|
|
32
43
|
mergeSessionPages,
|
|
33
44
|
railCollapsedCookie,
|
|
34
45
|
readRailCollapsedCookie,
|
|
@@ -37,6 +48,7 @@ export {
|
|
|
37
48
|
resolveNavHref,
|
|
38
49
|
resolveScopedActiveNavId,
|
|
39
50
|
resolveSessionUnread,
|
|
51
|
+
scoreCommandPaletteItem,
|
|
40
52
|
sessionLabel,
|
|
41
53
|
writeRailCollapsedCookie
|
|
42
54
|
};
|
|
@@ -24,8 +24,12 @@ export declare const DEFAULT_MODEL_ID: string;
|
|
|
24
24
|
export declare const NON_REASONING_MODEL_ID = "deepseek/deepseek-chat";
|
|
25
25
|
/** The exact pending-file pair the playground's ComposerRoute ships. */
|
|
26
26
|
export declare const pendingComposerFiles: ComposerFile[];
|
|
27
|
-
/** Adds an errored chip so the destructive tone
|
|
27
|
+
/** Adds an errored chip so the destructive tone and its reason are covered. */
|
|
28
28
|
export declare const pendingComposerFilesWithError: ComposerFile[];
|
|
29
|
+
/** A staged image whose thumbnail identifies it — the shape a pasted
|
|
30
|
+
* screenshot arrives in, where the auto-generated name says nothing. A data
|
|
31
|
+
* URL keeps the story free of an object URL nobody would revoke. */
|
|
32
|
+
export declare const pendingComposerImageFiles: ComposerFile[];
|
|
29
33
|
export declare const fileAttachmentParts: ChatAttachmentPart[];
|
|
30
34
|
export declare const imageAttachmentParts: ChatAttachmentPart[];
|
|
31
35
|
export declare const mixedAttachmentParts: ChatAttachmentPart[];
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ChatComposer — the shared message input every agent app used to hand-roll:
|
|
3
3
|
* an auto-resizing textarea (Enter sends, Shift+Enter inserts a newline), an
|
|
4
|
-
* opt-in attach + drag-and-drop surface with pending-file
|
|
5
|
-
* Stop/Send toggle, a slot for inline controls (model
|
|
6
|
-
* effort), and a Cmd/Ctrl+L focus shortcut.
|
|
4
|
+
* opt-in attach + drag-and-drop + clipboard-paste surface with pending-file
|
|
5
|
+
* chips, a streaming Stop/Send toggle, a slot for inline controls (model
|
|
6
|
+
* picker, reasoning effort), and a Cmd/Ctrl+L focus shortcut.
|
|
7
|
+
*
|
|
8
|
+
* Files arrive by three routes — the picker dialog, a drop, and a paste — and
|
|
9
|
+
* all three funnel through `accept` (`./composer-file-accept`) before they
|
|
10
|
+
* reach `onAttach`, so a type the picker will not offer cannot get in by
|
|
11
|
+
* another route. What `accept` refuses goes to `onRejectFiles` with a reason;
|
|
12
|
+
* without that prop a refusal is silent, which is what the native picker also
|
|
13
|
+
* does. Size and count limits stay the host's job — `useComposerAttachments`
|
|
14
|
+
* owns them, because they depend on what is already staged.
|
|
7
15
|
*
|
|
8
16
|
* A REJECTED send never destroys the draft. The input clears optimistically —
|
|
9
17
|
* the composer stays editable while a turn streams precisely so the next
|
|
@@ -25,6 +33,8 @@
|
|
|
25
33
|
* fallbacks when a host hasn't defined a private chat-token set.
|
|
26
34
|
*/
|
|
27
35
|
import { type ReactNode } from 'react';
|
|
36
|
+
import { type ComposerFileRejection } from './composer-file-accept';
|
|
37
|
+
import { type DictationAudio } from './use-dictation';
|
|
28
38
|
/** Prompt-part descriptor an uploaded file carries (the upload route's
|
|
29
39
|
* `UploadedChatFile.part`), echoed back in the turn body on send. Mirrors
|
|
30
40
|
* `/chat-routes`' wire shape structurally — no server import here. */
|
|
@@ -47,6 +57,25 @@ export interface ComposerFile {
|
|
|
47
57
|
/** Uploaded part descriptor; set once the upload route returns. Only
|
|
48
58
|
* `status: 'ready'` files with a part travel on a parts-aware send. */
|
|
49
59
|
part?: ComposerFilePart;
|
|
60
|
+
/** Object URL for an image thumbnail on the chip. The host owns the URL's
|
|
61
|
+
* whole life — `URL.createObjectURL` when the file is staged,
|
|
62
|
+
* `URL.revokeObjectURL` when it leaves — and the composer only reads it.
|
|
63
|
+
* `useComposerAttachments` already does both. */
|
|
64
|
+
previewUrl?: string;
|
|
65
|
+
/** Why this file failed, shown on the chip while `status: 'error'`. Without
|
|
66
|
+
* it an error chip is red and mute, which tells the user nothing. */
|
|
67
|
+
errorMessage?: string;
|
|
68
|
+
}
|
|
69
|
+
/** A piece of context the agent will see beside the next message — an open
|
|
70
|
+
* file, a selected record, a pinned document. Rendered as its own chip row,
|
|
71
|
+
* separate from staged attachments: context is what the turn already carries,
|
|
72
|
+
* an attachment is what the user is adding to it. */
|
|
73
|
+
export interface ComposerContextItem {
|
|
74
|
+
id: string;
|
|
75
|
+
label: string;
|
|
76
|
+
icon?: ReactNode;
|
|
77
|
+
/** Omit for a chip the user cannot dismiss. */
|
|
78
|
+
onRemove?: () => void;
|
|
50
79
|
}
|
|
51
80
|
/** A send the host refused. `error` is shown verbatim in the composer's notice;
|
|
52
81
|
* omit it for the generic copy. */
|
|
@@ -105,6 +134,20 @@ export interface ComposerSendFailure {
|
|
|
105
134
|
* the notice instead. */
|
|
106
135
|
restored: boolean;
|
|
107
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* One `/` command the composer offers. Typing `/` at position 0 opens the
|
|
139
|
+
* command menu; the rest of the token filters it (the same prefix > substring
|
|
140
|
+
* > token-order ranking as the command palette). Picking a command CLEARS the
|
|
141
|
+
* token from the draft and calls `run` — what the command does (a route, a
|
|
142
|
+
* dialog, a draft transformation) is the product's business.
|
|
143
|
+
*/
|
|
144
|
+
export interface SlashCommand {
|
|
145
|
+
/** Command name without the leading slash: `model`, `clear`. */
|
|
146
|
+
name: string;
|
|
147
|
+
/** One line of what it does, rendered beside the name. */
|
|
148
|
+
description: string;
|
|
149
|
+
run: () => void;
|
|
150
|
+
}
|
|
108
151
|
export interface ChatComposerProps {
|
|
109
152
|
/** Send the trimmed, non-empty message. Attached files travel separately via
|
|
110
153
|
* `onAttach` + `pendingFiles` (the host consumes and clears them on send).
|
|
@@ -154,14 +197,81 @@ export interface ChatComposerProps {
|
|
|
154
197
|
*/
|
|
155
198
|
controlsPlacement?: 'above' | 'inline';
|
|
156
199
|
/** Attachments are opt-in: pass `onAttach` to show the attach button, accept
|
|
157
|
-
* drag-and-drop onto the input, and render
|
|
200
|
+
* drag-and-drop and clipboard paste onto the input, and render
|
|
201
|
+
* `pendingFiles` chips. */
|
|
158
202
|
onAttach?: (files: FileList) => void;
|
|
159
203
|
onAttachFolder?: (files: FileList) => void;
|
|
160
204
|
pendingFiles?: ComposerFile[];
|
|
161
205
|
onRemoveFile?: (id: string) => void;
|
|
206
|
+
/** Pass it and a chip with `status: 'error'` gains a retry button. */
|
|
207
|
+
onRetryFile?: (id: string) => void;
|
|
208
|
+
/**
|
|
209
|
+
* File types the composer takes, in the native `<input accept>` grammar.
|
|
210
|
+
* Enforced on every ingress route — the picker dialog (which the user can
|
|
211
|
+
* override with "All Files"), drag-and-drop, and clipboard paste — so a type
|
|
212
|
+
* the picker will not offer cannot arrive by another route. A non-matching
|
|
213
|
+
* file goes to `onRejectFiles` and never reaches `onAttach`. Folder attach is
|
|
214
|
+
* exempt: directory selection has no native accept semantics.
|
|
215
|
+
*/
|
|
162
216
|
accept?: string;
|
|
217
|
+
/** Called with the files `accept` removed from a pick, drop, or paste, each
|
|
218
|
+
* with a reason. Without it a refusal is silent — the same feedback the
|
|
219
|
+
* native picker gives for a type it will not offer. */
|
|
220
|
+
onRejectFiles?: (rejections: ComposerFileRejection[]) => void;
|
|
163
221
|
dropTitle?: string;
|
|
164
222
|
dropDescription?: string;
|
|
223
|
+
/** Context the agent will see beside the next message, as its own chip row
|
|
224
|
+
* above the input. */
|
|
225
|
+
contextItems?: ReadonlyArray<ComposerContextItem>;
|
|
226
|
+
/**
|
|
227
|
+
* Let a staged file stand in for message text, so the send control stays live
|
|
228
|
+
* while an upload is in flight instead of going dead with nothing to explain
|
|
229
|
+
* it. Default false, where an empty message needs a `ready` file.
|
|
230
|
+
*
|
|
231
|
+
* It does NOT make an unfinished file sendable. A turn whose only content is a
|
|
232
|
+
* file that is still uploading or has failed never reaches the send handler —
|
|
233
|
+
* it would arrive empty and the attachment would be lost. The composer
|
|
234
|
+
* refuses it and names the reason in its notice
|
|
235
|
+
* ({@link attachmentsNotReadyMessage}). So the flag decides whether the
|
|
236
|
+
* control is live, and the composer keeps the integrity gate rather than
|
|
237
|
+
* leaving each host to re-derive it.
|
|
238
|
+
*/
|
|
239
|
+
canSubmitAttachmentsOnly?: boolean;
|
|
240
|
+
/** Notice copy when a send is refused because no staged file is ready yet.
|
|
241
|
+
* Defaults to wording chosen from whether a file failed or is still
|
|
242
|
+
* uploading. */
|
|
243
|
+
attachmentsNotReadyMessage?: string;
|
|
244
|
+
/**
|
|
245
|
+
* Let Enter and Send keep firing while `isStreaming`, for a surface that
|
|
246
|
+
* queues the next turn rather than blocking on the current one. Default
|
|
247
|
+
* false. The button still flips to Stop while a turn streams, so this opens
|
|
248
|
+
* the keyboard path, not a second button.
|
|
249
|
+
*/
|
|
250
|
+
canSubmitWhileBusy?: boolean;
|
|
251
|
+
/** Focus the input on mount — for a surface whose whole job is the input
|
|
252
|
+
* (an entry/hero composer), never for one docked under a transcript. */
|
|
253
|
+
autoFocus?: boolean;
|
|
254
|
+
/** Rows the input shows before it grows. Default 2. */
|
|
255
|
+
minRows?: number;
|
|
256
|
+
/** Pixel height the input grows to before it scrolls. Default 168. */
|
|
257
|
+
maxHeight?: number;
|
|
258
|
+
/** Content between the controls slot and Send — a token meter, a cost, a
|
|
259
|
+
* status line. It sits outside the controls slot and never shrinks, so a
|
|
260
|
+
* wrapping picker set cannot push it away. */
|
|
261
|
+
trailing?: ReactNode;
|
|
262
|
+
/** `/` commands offered when the draft is exactly a leading slash token.
|
|
263
|
+
* Omit (or pass []) and `/` types as ordinary text. */
|
|
264
|
+
slashCommands?: SlashCommand[];
|
|
265
|
+
/** Dictation is opt-in: pass `onDictate` and the action row gains a mic
|
|
266
|
+
* button (browsers without `MediaRecorder`/`getUserMedia` render none).
|
|
267
|
+
* Click starts the capture; the button flips to a stop control with the
|
|
268
|
+
* running elapsed seconds; stop hands the recorded audio blob here. The
|
|
269
|
+
* composer owns capture only — turning the audio into text (e.g. the
|
|
270
|
+
* Whisper provider from `sequences-react`) is the host's. */
|
|
271
|
+
onDictate?: (audio: DictationAudio) => void;
|
|
272
|
+
/** Capture failures (a denied mic prompt, no device), after the composer has
|
|
273
|
+
* shown its own dismissible notice. For hosts that log or track. */
|
|
274
|
+
onDictateError?: (message: string) => void;
|
|
165
275
|
/** Cmd/Ctrl+L focuses the input and shows the hint. Default true. */
|
|
166
276
|
focusShortcut?: boolean;
|
|
167
277
|
/** Float the card on a soft two-layer foreground-tinted shadow (opt-in).
|
|
@@ -176,4 +286,4 @@ export interface ChatComposerProps {
|
|
|
176
286
|
sendVariant?: 'pill' | 'icon';
|
|
177
287
|
className?: string;
|
|
178
288
|
}
|
|
179
|
-
export declare function ChatComposer({ onSend, onSendParts, onSendFailed, sendFailureMessage, onCancel, isStreaming, disabled, placeholder, value, onValueChange, initialValue, seed, onSeedApplied, controls, controlsPlacement, onAttach, onAttachFolder, pendingFiles, onRemoveFile, accept, dropTitle, dropDescription, focusShortcut, floating, sendLabel, sendVariant, className, }: ChatComposerProps): import("react").JSX.Element;
|
|
289
|
+
export declare function ChatComposer({ onSend, onSendParts, onSendFailed, sendFailureMessage, onCancel, isStreaming, disabled, placeholder, value, onValueChange, initialValue, seed, onSeedApplied, controls, controlsPlacement, onAttach, onAttachFolder, pendingFiles, onRemoveFile, onRetryFile, accept, onRejectFiles, dropTitle, dropDescription, contextItems, canSubmitAttachmentsOnly, attachmentsNotReadyMessage, canSubmitWhileBusy, autoFocus, minRows, maxHeight, trailing, slashCommands, onDictate, onDictateError, focusShortcut, floating, sendLabel, sendVariant, className, }: ChatComposerProps): import("react").JSX.Element;
|