@tangle-network/agent-app 0.43.40 → 0.43.41

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chat-routes/wire.ts","../src/chat-routes/file-index.ts"],"sourcesContent":["/**\n * Wire contract between the chat client (composer + `streamChatTurn`) and the\n * assembled server vertical (`createChatTurnRoutes`). Import-free on purpose:\n * `/web-react` re-exports these types into browser bundles, so nothing here may\n * reach a Node builtin or an engine package.\n *\n * The part shape mirrors the sandbox SDK's `PromptInputPart` structurally\n * (text | image | file with filename/mediaType/url/path/content) — derived\n * here, not imported, so the client bundle never touches the SDK.\n */\n\nexport interface ChatTurnTextPartInput {\n type: 'text'\n text: string\n}\n\n/** A non-text prompt part the upload route hands back and the client echoes\n * on send. `url` carries an inline `data:` URI for small files; `path` is a\n * sandbox workspace reference for large ones (the >1 MiB gateway body cap\n * makes the two-step upload mandatory). */\nexport interface ChatTurnFilePartInput {\n type: 'image' | 'file'\n filename?: string\n mediaType?: string\n url?: string\n path?: string\n content?: string\n}\n\nexport type ChatTurnPartInput = ChatTurnTextPartInput | ChatTurnFilePartInput\n\n/** POST body for the turn route. `content` may be empty when `parts` carry the\n * message (an image-only send). Product routing fields (workspaceId etc.) ride\n * alongside and are read by the product's `authorize` seam. */\nexport interface ChatTurnRequestPayload {\n threadId: string\n content?: string\n /** Non-text parts from the upload route, echoed back verbatim. */\n parts?: ChatTurnFilePartInput[]\n model?: string\n effort?: 'auto' | 'low' | 'medium' | 'high'\n harness?: string\n /** Client-generated idempotency key for the logical turn (retry-safe). */\n turnId?: string\n [key: string]: unknown\n}\n\n/** `fetch` init for the turn route — the one place the client wire shape is\n * serialized, so composer glue and products never drift from the server's\n * parser. */\nexport function chatTurnRequestInit(payload: ChatTurnRequestPayload): RequestInit {\n return {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }\n}\n\n// ── inline-part byte budget ─────────────────────────────────────────────────\n//\n// The sandbox gateway caps request bodies at 1 MiB; a turn body whose inline\n// `data:` parts exceed it dies at the gateway with an opaque 413. Enforce the\n// budget at the route boundary instead, with headroom for the JSON envelope\n// (same fail-loud-at-the-choke-point style as /sandbox's provision-payload and\n// env-size gates).\n\nexport const INLINE_PARTS_MAX_BYTES = 950_000\n\nexport class ChatTurnInputError extends Error {\n constructor(message: string, readonly status = 400, readonly code = 'INVALID_CHAT_TURN') {\n super(message)\n this.name = 'ChatTurnInputError'\n }\n}\n\nfunction partByteSize(part: ChatTurnPartInput): number {\n let bytes = 0\n if (part.type === 'text') return part.text.length\n if (part.url) bytes += part.url.length\n if (part.content) bytes += part.content.length\n if (part.path) bytes += part.path.length\n return bytes\n}\n\nexport function promptPartsByteSize(parts: ChatTurnPartInput[]): number {\n return parts.reduce((total, part) => total + partByteSize(part), 0)\n}\n\n/** Throws `ChatTurnInputError` (413) when the parts' inline payload would blow\n * the gateway cap. Path-ref parts are tiny by construction and always pass. */\nexport function assertPromptPartsWithinCap(\n parts: ChatTurnPartInput[],\n maxBytes = INLINE_PARTS_MAX_BYTES,\n): void {\n const total = promptPartsByteSize(parts)\n if (total <= maxBytes) return\n const largest = [...parts].sort((a, b) => partByteSize(b) - partByteSize(a))[0]\n const largestName = largest && largest.type !== 'text' ? largest.filename ?? largest.path ?? largest.type : 'text'\n throw new ChatTurnInputError(\n `Inline prompt parts total ${total}B, over the ${maxBytes}B budget (largest: ${largestName}, ${largest ? partByteSize(largest) : 0}B). ` +\n 'Upload large files through the upload route so they travel as sandbox path references.',\n 413,\n 'PROMPT_PARTS_TOO_LARGE',\n )\n}\n\n// ── file mentions ────────────────────────────────────────────────────────\n//\n// A file mention (`@`-picked in the composer, sandbox-ui#184) is a path\n// reference into the workspace sandbox — no byte upload. These helpers turn\n// a resolved mention list into wire parts and the prompt pointer block that\n// tells the agent where to read them from.\n\n/** A file mention resolved from the composer's `@`-picker: the\n * workspace-relative path plus enough metadata to build a prompt part and\n * pointer text. `path` is the canonical identity — the mention pill's\n * `MentionItem.id` for the file kind (`/web-react`'s `useFileMentions`). */\nexport interface FileMention {\n path: string\n name: string\n size?: number\n}\n\nconst MENTION_IMAGE_MEDIA_TYPES: ReadonlyMap<string, string> = new Map([\n ['.png', 'image/png'],\n ['.jpg', 'image/jpeg'],\n ['.jpeg', 'image/jpeg'],\n ['.gif', 'image/gif'],\n ['.webp', 'image/webp'],\n ['.svg', 'image/svg+xml'],\n ['.bmp', 'image/bmp'],\n ['.heic', 'image/heic'],\n ['.heif', 'image/heif'],\n ['.avif', 'image/avif'],\n])\n\nfunction extensionOf(path: string): string {\n const base = path.split('/').filter(Boolean).pop() ?? path\n const dot = base.lastIndexOf('.')\n return dot > 0 ? base.slice(dot).toLowerCase() : ''\n}\n\n/** The `image/*` mime for a mention path by extension, or `undefined` for\n * anything not in the known image set (dispatched as `type: 'file'`). */\nexport function mediaTypeForMentionPath(path: string): string | undefined {\n return MENTION_IMAGE_MEDIA_TYPES.get(extensionOf(path))\n}\n\nexport interface FileMentionsToPartsOptions {\n /** Resolve a mention's workspace-relative path to the absolute path the\n * dispatched part should carry (e.g. a host prefixing the in-box vault\n * root). Default: identity — the path travels unchanged. */\n resolvePath?: (path: string) => string\n}\n\n/** Maps resolved file mentions to path-only `ChatTurnFilePartInput`s —\n * `image` vs `file` by extension, and always a `path`, never a `url` (the\n * url/path XOR invariant: a mention is a sandbox path reference, never\n * inline bytes). */\nexport function fileMentionsToParts(\n mentions: readonly FileMention[],\n opts: FileMentionsToPartsOptions = {},\n): ChatTurnFilePartInput[] {\n const resolvePath = opts.resolvePath ?? ((path: string) => path)\n return mentions.map((mention) => {\n const mediaType = mediaTypeForMentionPath(mention.path)\n const part: ChatTurnFilePartInput = {\n type: mediaType ? 'image' : 'file',\n filename: mention.name,\n path: resolvePath(mention.path),\n }\n if (mediaType) part.mediaType = mediaType\n return part\n })\n}\n\n/** The agent-facing pointer block appended to the dispatched prompt — never\n * persisted in message `content`. Empty array → `''` so callers can append\n * unconditionally. This is the sole producer of that text: the current\n * turn's dispatch and any history projection built from the same mention\n * list both route through here, so the two can't drift apart. */\nexport function buildMentionPromptBlock(\n mentions: readonly Pick<FileMention, 'name' | 'path'>[],\n): string {\n if (mentions.length === 0) return ''\n const lines = mentions.map((m) => `- ${m.name} (${m.path})`)\n return `\\n\\nMentioned files — read them from these paths:\\n${lines.join('\\n')}`\n}\n\n/** Validates the untyped `parts` array off the wire. Returns the typed parts\n * or throws `ChatTurnInputError` (400) naming the offending entry. */\nexport function parseChatTurnParts(raw: unknown): ChatTurnFilePartInput[] {\n if (raw === undefined || raw === null) return []\n if (!Array.isArray(raw)) throw new ChatTurnInputError('parts must be an array')\n return raw.map((entry, index) => {\n const part = entry as Record<string, unknown> | null\n if (!part || typeof part !== 'object') {\n throw new ChatTurnInputError(`parts[${index}] must be an object`)\n }\n if (part.type !== 'image' && part.type !== 'file') {\n throw new ChatTurnInputError(`parts[${index}].type must be 'image' or 'file'`)\n }\n for (const key of ['filename', 'mediaType', 'url', 'path', 'content'] as const) {\n if (part[key] !== undefined && typeof part[key] !== 'string') {\n throw new ChatTurnInputError(`parts[${index}].${key} must be a string`)\n }\n }\n if (!part.url && !part.path && !part.content) {\n throw new ChatTurnInputError(`parts[${index}] needs a url, path, or content`)\n }\n return {\n type: part.type,\n ...(part.filename !== undefined ? { filename: part.filename as string } : {}),\n ...(part.mediaType !== undefined ? { mediaType: part.mediaType as string } : {}),\n ...(part.url !== undefined ? { url: part.url as string } : {}),\n ...(part.path !== undefined ? { path: part.path as string } : {}),\n ...(part.content !== undefined ? { content: part.content as string } : {}),\n }\n })\n}\n","/**\n * `createSandboxFileIndexRoute` — server side of `@`-file-mentions\n * (companion to sandbox-ui#184's composer mention primitive). Serves a flat,\n * ignore-filtered listing of the workspace sandbox so `useFileMentions`\n * (`/web-react`) can filter it client-side without a round trip per\n * keystroke.\n *\n * Same seam style as `createUploadRoute`: `authorize({ request })` resolves a\n * structural `{ tree(path, opts) }` handle (the shape of the sandbox SDK's\n * `box.fs.tree`) — no SDK import here. `authorize` also carries the\n * cold-box signal: a sandbox that isn't running yet answers `{ status:\n * 'warming' }` directly, never provisions-and-waits inside this route.\n */\n\nimport type { FileMention } from './wire'\n\n/** One entry from a structural `tree()` scan. Mirrors the sandbox SDK's\n * `FileTreeFile` (`path`, `size`, `mtime`) — `mtime` is unused here so it's\n * omitted from the structural match. */\nexport interface SandboxTreeFile {\n path: string\n size: number\n}\n\n/** Structural match of the sandbox SDK's `box.fs.tree` result shape\n * (`FileTreeResult`). `stats.truncated` is the only stat this route reads;\n * the rest ride through unread on the real SDK type. */\nexport interface SandboxTreeResult {\n root: string\n files: SandboxTreeFile[]\n stats: { truncated: boolean }\n}\n\n/** Structural match of the sandbox SDK's `box.fs` tree surface. */\nexport interface SandboxFileTreeSource {\n tree(path: string, options?: { maxDepth?: number }): Promise<SandboxTreeResult>\n}\n\nexport interface FileIndexReadyResponse {\n status: 'ready'\n /** Workspace-relative entries. Same shape as `FileMention` (`./wire`) so a\n * client can hand a response entry straight to `fileMentionsToParts` /\n * `buildMentionPromptBlock` without remapping. */\n files: FileMention[]\n /** True when either the underlying scan truncated (SDK-side cap) or this\n * route's own `maxEntries` cap trimmed the filtered list. The client\n * should show \"showing first N files\" rather than imply completeness. */\n truncated: boolean\n generatedAt: string\n}\n\n/** Cold-box answer: no provisioning happened, no files were scanned. The\n * client shows a warming state and retries — this route never blocks on a\n * box coming up. */\nexport interface FileIndexWarmingResponse {\n status: 'warming'\n}\n\nexport type FileIndexResponse = FileIndexReadyResponse | FileIndexWarmingResponse\n\n/** Short-TTL cache seam so repeat popover opens in the same session don't\n * re-scan the workspace. Host-provided (e.g. a KV binding); `key` is\n * whatever `authorize` returns as `cacheKey` — this route treats it opaquely. */\nexport interface FileIndexCache {\n get(key: string): Promise<FileIndexReadyResponse | null> | FileIndexReadyResponse | null\n put(key: string, value: FileIndexReadyResponse, options?: { ttlSeconds?: number }): Promise<void> | void\n}\n\nexport type FileIndexAuthorization =\n | {\n status: 'ready'\n /** Structural sandbox `fs` handle, usually `ensureWorkspaceSandbox(...)` → `box.fs`. */\n fs: SandboxFileTreeSource\n /** Workspace root to index (e.g. `/home/agent`). */\n root: string\n /** Extra ignore segments for this request, merged with the route's\n * defaults + `CreateSandboxFileIndexRouteOptions.ignore`. */\n ignore?: string[]\n /** Opaque cache key for the optional cache seam. Omit to skip caching\n * for this request (e.g. a workspace the host chooses not to cache). */\n cacheKey?: string\n }\n | { status: 'warming' }\n | { status: 'denied'; response: Response }\n\nexport interface CreateSandboxFileIndexRouteOptions {\n /** Authenticate the caller, resolve the sandbox `fs` handle, and signal a\n * cold box — never provisions or waits. */\n authorize(args: { request: Request }): Promise<FileIndexAuthorization>\n /** Extra ignore segments beyond the route's defaults (node_modules, .git,\n * dotfiles/dot-dirs, common build dirs). Matched as exact path-segment\n * names, same rule as the defaults. */\n ignore?: string[]\n /** Passed to `fs.tree` as `options.maxDepth`. Default 12. */\n maxDepth?: number\n /** Hard cap on entries returned after filtering. Default 5000. */\n maxEntries?: number\n /** Optional host-provided cache seam. */\n cache?: FileIndexCache\n /** Cache TTL in seconds when `cache` is set. Default 20. */\n cacheTtlSeconds?: number\n}\n\n/** Segment names ignored anywhere in a path, beyond the generic dotfile rule\n * below. Intentionally small and language/framework-agnostic — callers\n * extend it via `ignore` for anything domain-specific (e.g. a vault's\n * `uploads` dir). */\nconst DEFAULT_IGNORE_SEGMENTS = [\n 'node_modules',\n 'dist',\n 'build',\n 'out',\n 'coverage',\n 'target',\n '__pycache__',\n 'venv',\n]\n\n/** A path segment starting with `.` (`.git`, `.env`, `.next`, `.cache`, …) is\n * always ignored — this single rule covers most dot-prefixed VCS/tooling\n * dirs and dotfiles without enumerating them. */\nfunction isIgnored(relPath: string, ignoreSegments: ReadonlySet<string>): boolean {\n for (const segment of relPath.split('/')) {\n if (!segment) continue\n if (segment.startsWith('.')) return true\n if (ignoreSegments.has(segment)) return true\n }\n return false\n}\n\n/** Strips the tree result's echoed `root` prefix so entries are always\n * workspace-relative, whichever convention the structural `fs.tree` uses\n * (root-relative already, or root-prefixed). */\nfunction relativeTo(root: string, path: string): string {\n const prefix = root.endsWith('/') ? root : `${root}/`\n if (path.startsWith(prefix)) return path.slice(prefix.length)\n if (path === root) return ''\n return path\n}\n\nfunction basename(path: string): string {\n const segments = path.split('/').filter(Boolean)\n return segments[segments.length - 1] ?? path\n}\n\nexport function createSandboxFileIndexRoute(\n options: CreateSandboxFileIndexRouteOptions,\n): (request: Request) => Promise<Response> {\n const maxDepth = options.maxDepth ?? 12\n const maxEntries = options.maxEntries ?? 5000\n const cacheTtlSeconds = options.cacheTtlSeconds ?? 20\n const staticIgnore = new Set([...DEFAULT_IGNORE_SEGMENTS, ...(options.ignore ?? [])])\n\n return async function fileIndex(request: Request): Promise<Response> {\n const auth = await options.authorize({ request })\n if (auth.status === 'denied') return auth.response\n if (auth.status === 'warming') {\n return Response.json({ status: 'warming' } satisfies FileIndexWarmingResponse)\n }\n\n const cache = options.cache\n if (cache && auth.cacheKey) {\n const cached = await cache.get(auth.cacheKey)\n if (cached) return Response.json(cached)\n }\n\n const ignoreSegments = auth.ignore?.length\n ? new Set([...staticIgnore, ...auth.ignore])\n : staticIgnore\n\n const scan = await auth.fs.tree(auth.root, { maxDepth })\n const filtered = scan.files.filter((f) => !isIgnored(relativeTo(scan.root, f.path), ignoreSegments))\n const truncated = scan.stats.truncated || filtered.length > maxEntries\n const files: FileMention[] = filtered.slice(0, maxEntries).map((f) => {\n const path = relativeTo(scan.root, f.path)\n const entry: FileMention = { path, name: basename(path) }\n if (typeof f.size === 'number') entry.size = f.size\n return entry\n })\n\n const body: FileIndexReadyResponse = {\n status: 'ready',\n files,\n truncated,\n generatedAt: new Date().toISOString(),\n }\n\n if (cache && auth.cacheKey) await cache.put(auth.cacheKey, body, { ttlSeconds: cacheTtlSeconds })\n\n return Response.json(body)\n }\n}\n"],"mappings":"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","names":[]}
@@ -15,7 +15,7 @@ import {
15
15
  } from "./chunk-E7QYOOON.js";
16
16
 
17
17
  // src/web-react/index.tsx
18
- import { useEffect as useEffect5, useMemo as useMemo6, useRef as useRef6, useState as useState9, memo } from "react";
18
+ import { useEffect as useEffect5, useMemo as useMemo7, useRef as useRef7, useState as useState10, memo } from "react";
19
19
 
20
20
  // src/web-react/smooth-text.ts
21
21
  import { useEffect, useRef, useState } from "react";
@@ -1441,8 +1441,137 @@ function useChatInteractions() {
1441
1441
  return { interactions, pending, upsert, applyCancel, markResolved, restore, terminalizePending, reset };
1442
1442
  }
1443
1443
 
1444
+ // src/web-react/use-file-mentions.ts
1445
+ import { useCallback as useCallback3, useMemo as useMemo5, useRef as useRef6, useState as useState7 } from "react";
1446
+ var FILE_MENTION_KIND = "file";
1447
+ function toMentionItem(file) {
1448
+ return { id: file.path, label: file.name, detail: file.path, kind: FILE_MENTION_KIND };
1449
+ }
1450
+ function toFileMention(item) {
1451
+ return { path: item.id, name: item.label };
1452
+ }
1453
+ function rankFileMentions(files, query, limit) {
1454
+ const q = query.trim().toLowerCase();
1455
+ if (!q) return files.slice(0, limit);
1456
+ const scored = [];
1457
+ for (const file of files) {
1458
+ const name = file.name.toLowerCase();
1459
+ if (name.startsWith(q)) {
1460
+ scored.push({ file, tier: 0 });
1461
+ continue;
1462
+ }
1463
+ if (name.includes(q)) {
1464
+ scored.push({ file, tier: 1 });
1465
+ continue;
1466
+ }
1467
+ if (file.path.toLowerCase().includes(q)) {
1468
+ scored.push({ file, tier: 2 });
1469
+ }
1470
+ }
1471
+ scored.sort((a, b) => {
1472
+ if (a.tier !== b.tier) return a.tier - b.tier;
1473
+ if (a.file.name.length !== b.file.name.length) return a.file.name.length - b.file.name.length;
1474
+ return a.file.path.localeCompare(b.file.path);
1475
+ });
1476
+ return scored.slice(0, limit).map((s) => s.file);
1477
+ }
1478
+ var RETRY_AFTER_MS = 3e3;
1479
+ var DEFAULT_MENTION_LIMIT = 20;
1480
+ var INDEX_REFRESH_AFTER_MS = 5 * 60 * 1e3;
1481
+ var DEFAULT_MENTION_EMPTY_TEXT = "No matching files";
1482
+ function emptyTextFor(state, fallback) {
1483
+ switch (state.kind) {
1484
+ case "idle":
1485
+ case "loading":
1486
+ return "Loading files\u2026";
1487
+ case "warming":
1488
+ return "Sandbox is starting \u2014 try again in a moment";
1489
+ case "error":
1490
+ return `Couldn't load files: ${state.message}`;
1491
+ case "ready":
1492
+ return fallback;
1493
+ }
1494
+ }
1495
+ function useFileMentions(options) {
1496
+ const {
1497
+ indexUrl,
1498
+ limit = DEFAULT_MENTION_LIMIT,
1499
+ refreshAfterMs = INDEX_REFRESH_AFTER_MS,
1500
+ emptyText = DEFAULT_MENTION_EMPTY_TEXT
1501
+ } = options;
1502
+ const fetchImpl = options.fetchImpl ?? fetch;
1503
+ const [state, setState] = useState7({ kind: "idle" });
1504
+ const stateRef = useRef6(state);
1505
+ stateRef.current = state;
1506
+ const inFlightRef = useRef6(null);
1507
+ const [mentions, setMentions] = useState7([]);
1508
+ const load = useCallback3(() => {
1509
+ if (inFlightRef.current) return inFlightRef.current;
1510
+ if (stateRef.current.kind === "idle") {
1511
+ stateRef.current = { kind: "loading" };
1512
+ setState(stateRef.current);
1513
+ }
1514
+ const attempt = (async () => {
1515
+ let next;
1516
+ try {
1517
+ const res = await fetchImpl(indexUrl);
1518
+ if (!res.ok) {
1519
+ next = { kind: "error", message: `HTTP ${res.status}`, attemptedAt: Date.now() };
1520
+ } else {
1521
+ const body = await res.json();
1522
+ next = body.status === "warming" ? { kind: "warming", attemptedAt: Date.now() } : {
1523
+ kind: "ready",
1524
+ files: body.files,
1525
+ truncated: body.truncated,
1526
+ fetchedAt: Date.now()
1527
+ };
1528
+ }
1529
+ } catch (err) {
1530
+ next = { kind: "error", message: err instanceof Error ? err.message : String(err), attemptedAt: Date.now() };
1531
+ }
1532
+ stateRef.current = next;
1533
+ setState(next);
1534
+ inFlightRef.current = null;
1535
+ return next;
1536
+ })();
1537
+ inFlightRef.current = attempt;
1538
+ return attempt;
1539
+ }, [fetchImpl, indexUrl]);
1540
+ const refresh = useCallback3(async () => {
1541
+ await load();
1542
+ }, [load]);
1543
+ const fetchItems = useCallback3(
1544
+ async (query) => {
1545
+ let current = stateRef.current;
1546
+ if (current.kind === "idle" || current.kind === "loading") {
1547
+ current = await load();
1548
+ } else if (current.kind === "ready") {
1549
+ if (Date.now() - current.fetchedAt > refreshAfterMs) void load();
1550
+ } else if (Date.now() - current.attemptedAt > RETRY_AFTER_MS) {
1551
+ void load();
1552
+ }
1553
+ if (current.kind !== "ready") return [];
1554
+ return rankFileMentions(current.files, query, limit).map(toMentionItem);
1555
+ },
1556
+ [load, limit, refreshAfterMs]
1557
+ );
1558
+ const onMentionsChange = useCallback3((items) => {
1559
+ setMentions(items.filter((item) => item.kind === void 0 || item.kind === FILE_MENTION_KIND).map(toFileMention));
1560
+ }, []);
1561
+ const clearMentions = useCallback3(() => setMentions([]), []);
1562
+ const mention = useMemo5(
1563
+ () => ({
1564
+ fetchItems,
1565
+ onMentionsChange,
1566
+ emptyText: emptyTextFor(state, emptyText)
1567
+ }),
1568
+ [fetchItems, onMentionsChange, state, emptyText]
1569
+ );
1570
+ return { mention, mentions, clearMentions, refresh };
1571
+ }
1572
+
1444
1573
  // src/web-react/mission-activity.tsx
1445
- import { useCallback as useCallback3, useEffect as useEffect4, useState as useState7 } from "react";
1574
+ import { useCallback as useCallback4, useEffect as useEffect4, useState as useState8 } from "react";
1446
1575
  import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1447
1576
  var LIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "running"]);
1448
1577
  var OK_STATUSES = /* @__PURE__ */ new Set(["completed", "done", "succeeded"]);
@@ -1502,8 +1631,8 @@ function CopyGlyph({ className }) {
1502
1631
  ] });
1503
1632
  }
1504
1633
  function TraceIdCopy({ traceId }) {
1505
- const [copied, setCopied] = useState7(false);
1506
- const copy = useCallback3(() => {
1634
+ const [copied, setCopied] = useState8(false);
1635
+ const copy = useCallback4(() => {
1507
1636
  void navigator.clipboard?.writeText(traceId).then(
1508
1637
  () => {
1509
1638
  setCopied(true);
@@ -1570,7 +1699,7 @@ function FlowWaterfall({ trace }) {
1570
1699
  ] });
1571
1700
  }
1572
1701
  function MissionActivityLane({ activity, startedAt, nowMs }) {
1573
- const [expanded, setExpanded] = useState7(false);
1702
+ const [expanded, setExpanded] = useState8(false);
1574
1703
  if (activity.length === 0) return null;
1575
1704
  return /* @__PURE__ */ jsxs6("div", { className: "mt-1 border-l border-border/50 pl-3", children: [
1576
1705
  activity.map((run) => {
@@ -1621,7 +1750,7 @@ function ActivityRow({
1621
1750
  record,
1622
1751
  renderMissionRef
1623
1752
  }) {
1624
- const [open, setOpen] = useState7(false);
1753
+ const [open, setOpen] = useState8(false);
1625
1754
  const tone = activityTone(record.status);
1626
1755
  const cost = formatActivityCost(record.costUsd);
1627
1756
  const duration = formatActivityDuration(record.durationMs);
@@ -1667,11 +1796,11 @@ function ActivityRow({
1667
1796
  ] });
1668
1797
  }
1669
1798
  function AgentActivityPanel({ fetchActivity, renderMissionRef, title = "Agent activity", emptyLabel = "No agent runs yet." }) {
1670
- const [rows, setRows] = useState7([]);
1671
- const [cursor, setCursor] = useState7(void 0);
1672
- const [loading, setLoading] = useState7(false);
1673
- const [error, setError] = useState7(null);
1674
- const load = useCallback3(
1799
+ const [rows, setRows] = useState8([]);
1800
+ const [cursor, setCursor] = useState8(void 0);
1801
+ const [loading, setLoading] = useState8(false);
1802
+ const [error, setError] = useState8(null);
1803
+ const load = useCallback4(
1675
1804
  async (from) => {
1676
1805
  setLoading(true);
1677
1806
  setError(null);
@@ -1794,7 +1923,7 @@ function SeatPaywall({
1794
1923
  }
1795
1924
 
1796
1925
  // src/web-react/agent-session-controls.tsx
1797
- import { useMemo as useMemo5, useState as useState8 } from "react";
1926
+ import { useMemo as useMemo6, useState as useState9 } from "react";
1798
1927
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1799
1928
  var HARNESS_LABELS = {
1800
1929
  opencode: "OpenCode (any model)",
@@ -1829,7 +1958,7 @@ function HarnessPicker({
1829
1958
  onChange,
1830
1959
  available
1831
1960
  }) {
1832
- const [open, setOpen] = useState8(false);
1961
+ const [open, setOpen] = useState9(false);
1833
1962
  const { containerRef, triggerProps } = usePopover(open, setOpen);
1834
1963
  const options = available ?? Object.keys(HARNESS_LABELS);
1835
1964
  return /* @__PURE__ */ jsxs8("div", { ref: containerRef, className: "relative inline-flex", children: [
@@ -1866,7 +1995,7 @@ function HarnessPicker({
1866
1995
  }
1867
1996
  function useCoherentHandlers(props) {
1868
1997
  const { model, models, harness, onModelChange, onHarnessChange } = props;
1869
- const canonicalIds = useMemo5(() => models.map((m) => m.id), [models]);
1998
+ const canonicalIds = useMemo6(() => models.map((m) => m.id), [models]);
1870
1999
  const onModel = (next) => {
1871
2000
  onModelChange(next);
1872
2001
  const nextHarness = snapHarnessToModel(harness, next);
@@ -1894,7 +2023,7 @@ function AgentSessionControls(props) {
1894
2023
  className
1895
2024
  } = props;
1896
2025
  const { onModel, onHarness } = useCoherentHandlers(props);
1897
- const [open, setOpen] = useState8(false);
2026
+ const [open, setOpen] = useState9(false);
1898
2027
  const { containerRef: popoverRef, triggerProps } = usePopover(open, setOpen);
1899
2028
  const selectedModel = models.find((m) => m.id === model);
1900
2029
  const showEffort = selectedModel?.supportsReasoning ?? true;
@@ -2158,7 +2287,7 @@ function ProposalCard({
2158
2287
  approval,
2159
2288
  renderers
2160
2289
  }) {
2161
- const [expanded, setExpanded] = useState9(false);
2290
+ const [expanded, setExpanded] = useState10(false);
2162
2291
  const { summary, meta } = proposalPreview(call);
2163
2292
  const custom = renderers?.[call.name]?.(call, message);
2164
2293
  const { pending: deciding, run: decide } = usePending();
@@ -2230,7 +2359,7 @@ function ToolCallCard({
2230
2359
  onOpenRun,
2231
2360
  renderers
2232
2361
  }) {
2233
- const [expanded, setExpanded] = useState9(false);
2362
+ const [expanded, setExpanded] = useState10(false);
2234
2363
  const pending = call.status === "done" ? pendingApprovalOf(call) : null;
2235
2364
  const kind = blockKindOf(call);
2236
2365
  const failed = call.status === "error" || toolOutcomeOf(call)?.ok === false;
@@ -2327,7 +2456,7 @@ function SegmentText({
2327
2456
  renderBody
2328
2457
  }) {
2329
2458
  const text = useSmoothText(content, streaming);
2330
- const body = useMemo6(() => renderBody(text), [renderBody, text]);
2459
+ const body = useMemo7(() => renderBody(text), [renderBody, text]);
2331
2460
  if (!content.trim() && !showCaret) return null;
2332
2461
  return /* @__PURE__ */ jsxs9("div", { className: "text-base leading-[1.75]", children: [
2333
2462
  body,
@@ -2421,12 +2550,12 @@ function AssistantMessageImpl({
2421
2550
  }) {
2422
2551
  const content = useSmoothText(msg.content, streaming);
2423
2552
  const reasoning = useSmoothText(msg.reasoning ?? "", streaming);
2424
- const body = useMemo6(() => renderBody(content), [renderBody, content]);
2553
+ const body = useMemo7(() => renderBody(content), [renderBody, content]);
2425
2554
  const segments = msg.segments;
2426
2555
  const hasAnswerText = content !== "" || (segments?.some((s) => s.kind === "text" && s.content.trim() !== "") ?? false);
2427
- const reasoningScrollRef = useRef6(null);
2428
- const thinkStartRef = useRef6(null);
2429
- const thinkMsRef = useRef6(null);
2556
+ const reasoningScrollRef = useRef7(null);
2557
+ const thinkStartRef = useRef7(null);
2558
+ const thinkMsRef = useRef7(null);
2430
2559
  if (streaming && reasoning && !hasAnswerText && thinkStartRef.current === null) {
2431
2560
  thinkStartRef.current = performance.now();
2432
2561
  }
@@ -2487,7 +2616,7 @@ function AssistantMessageImpl({
2487
2616
  }
2488
2617
  var AssistantMessage = memo(AssistantMessageImpl);
2489
2618
  function useThinkingSeconds(active) {
2490
- const [seconds, setSeconds] = useState9(0);
2619
+ const [seconds, setSeconds] = useState10(0);
2491
2620
  useEffect5(() => {
2492
2621
  if (!active) return;
2493
2622
  setSeconds(0);
@@ -2542,7 +2671,7 @@ function ChatMessages({
2542
2671
  emptyState,
2543
2672
  header
2544
2673
  }) {
2545
- const renderBody = useMemo6(
2674
+ const renderBody = useMemo7(
2546
2675
  () => renderMarkdown ?? ((content) => /* @__PURE__ */ jsx10("p", { className: "whitespace-pre-wrap", children: content })),
2547
2676
  [renderMarkdown]
2548
2677
  );
@@ -2616,6 +2745,11 @@ export {
2616
2745
  terminalizePendingChatInteractions,
2617
2746
  restoreChatInteractions,
2618
2747
  useChatInteractions,
2748
+ rankFileMentions,
2749
+ DEFAULT_MENTION_LIMIT,
2750
+ INDEX_REFRESH_AFTER_MS,
2751
+ DEFAULT_MENTION_EMPTY_TEXT,
2752
+ useFileMentions,
2619
2753
  activityTone,
2620
2754
  formatActivityCost,
2621
2755
  formatActivityDuration,
@@ -2634,4 +2768,4 @@ export {
2634
2768
  useThinkingSeconds,
2635
2769
  ChatMessages
2636
2770
  };
2637
- //# sourceMappingURL=chunk-D4HB72W2.js.map
2771
+ //# sourceMappingURL=chunk-KM766NN3.js.map