opencami 1.6.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +4 -2
  2. package/dist/client/assets/{CSPContext-CCZJblsW.js → CSPContext-DeJH85nm.js} +1 -1
  3. package/dist/client/assets/{DirectionContext-CAS2SgNN.js → DirectionContext-CxhRpXkm.js} +1 -1
  4. package/dist/client/assets/_sessionKey-CQE0brGK.js +23 -0
  5. package/dist/client/assets/agents-CMTFd_sG.js +2 -0
  6. package/dist/client/assets/agents-screen-BNQGEqcW.js +1 -0
  7. package/dist/client/assets/bots-B6oGzCxP.js +2 -0
  8. package/dist/client/assets/bots-screen-Be3cfGgq.js +1 -0
  9. package/dist/client/assets/button-D9Plv7hu.js +1 -0
  10. package/dist/client/assets/composite-B2KCZKKL.js +1 -0
  11. package/dist/client/assets/{connect-9Vns9nqk.js → connect-DuJfnyNK.js} +1 -1
  12. package/dist/client/assets/dashboard-00GpXm5V.js +1 -0
  13. package/dist/client/assets/event-DD8Cz4O9.js +1 -0
  14. package/dist/client/assets/file-explorer-screen-CxwemBES.js +1 -0
  15. package/dist/client/assets/files-DyBJVXBu.js +2 -0
  16. package/dist/client/assets/{index-BvN0Cj0s.js → index-DtGzE-ea.js} +1 -1
  17. package/dist/client/assets/{index-CRgPm0pb.js → index-Yo5UhdZV.js} +1 -1
  18. package/dist/client/assets/keyboard-shortcuts-dialog-BZwd-iyV.js +1 -0
  19. package/dist/client/assets/{main-ZPHfbCuz.js → main-CgwdHc9W.js} +27 -10
  20. package/dist/client/assets/{markdown-1-xJ1B1H.js → markdown-DtWnt4NA.js} +1 -1
  21. package/dist/client/assets/memory-l756yiNq.js +2 -0
  22. package/dist/client/assets/memory-screen-BQtVRuzE.js +1 -0
  23. package/dist/client/assets/menu-BsS6CDf_.js +1 -0
  24. package/dist/client/assets/{opencami-logo-CxC-KcS9.js → opencami-logo-Bmge6-FB.js} +1 -1
  25. package/dist/client/assets/popupStateMapping-D0ZbJR_o.js +1 -0
  26. package/dist/client/assets/{proxy-aJlrsYWj.js → proxy-CYZeDXoy.js} +1 -1
  27. package/dist/client/assets/{react-DuiuJSA1.js → react-DODKNyyU.js} +1 -1
  28. package/dist/client/assets/search-dialog-DW91SK30.js +1 -0
  29. package/dist/client/assets/session-export-dialog-CliO9Ob-.js +1 -0
  30. package/dist/client/assets/settings-dialog-C1u52aju.js +1 -0
  31. package/dist/client/assets/skills-8T_avaVb.js +2 -0
  32. package/dist/client/assets/{skills-panel-oNmWCyiv.js → skills-panel-DSiH-DLs.js} +1 -1
  33. package/dist/client/assets/styles-DvaLh0o1.css +1 -0
  34. package/dist/client/assets/switch-DbgQPO6i.js +1 -0
  35. package/dist/client/assets/tabs-BsAvZnlD.js +1 -0
  36. package/dist/client/assets/tooltip-DLmutB5C.js +1 -0
  37. package/dist/client/assets/use-file-explorer-state-Cg_yDYJl.js +12 -0
  38. package/dist/client/assets/useBaseUiId-KQTzRPLp.js +1 -0
  39. package/dist/client/assets/useCompositeItem-BPY2_hF_.js +1 -0
  40. package/dist/client/assets/{useControlled-byIifl1i.js → useControlled-B5pEEz2V.js} +1 -1
  41. package/dist/client/assets/{useMutation-D5JCmjGc.js → useMutation-BsQD6FKe.js} +1 -1
  42. package/dist/client/assets/useQuery-CmAJuY2W.js +1 -0
  43. package/dist/client/assets/visuallyHidden-COI6QeQH.js +1 -0
  44. package/dist/client/sw.js +5 -164
  45. package/dist/server/assets/{_sessionKey-BCdHKOq7.js → _sessionKey-Bq_fl7uv.js} +1070 -695
  46. package/dist/server/assets/_tanstack-start-manifest_v-BMCAWon2.js +4 -0
  47. package/dist/server/assets/dashboard-GCKodTiJ.js +214 -0
  48. package/dist/server/assets/{index-r4KOEzK3.js → index-C2hVqxBl.js} +2 -1
  49. package/dist/server/assets/{router-CkcOXH0V.js → router-bN_iTo0B.js} +518 -193
  50. package/dist/server/assets/{search-dialog-CuuZvlyq.js → search-dialog-DReM5ZD2.js} +3 -2
  51. package/dist/server/assets/settings-dialog-BUOrQN3Z.js +1511 -0
  52. package/dist/server/server.js +2 -2
  53. package/package.json +1 -1
  54. package/dist/client/assets/_sessionKey-f5UgmhlK.js +0 -14
  55. package/dist/client/assets/agents-DBx41wBT.js +0 -2
  56. package/dist/client/assets/agents-screen-CjqM2uOb.js +0 -1
  57. package/dist/client/assets/bots-BqD0EQcs.js +0 -2
  58. package/dist/client/assets/bots-screen-ivWEgfTv.js +0 -1
  59. package/dist/client/assets/button-DzGUU5Tc.js +0 -1
  60. package/dist/client/assets/composite-afaLtloO.js +0 -1
  61. package/dist/client/assets/file-explorer-screen-Di0232pk.js +0 -1
  62. package/dist/client/assets/files-BlMyTMb7.js +0 -2
  63. package/dist/client/assets/keyboard-shortcuts-dialog-C5kjcplH.js +0 -1
  64. package/dist/client/assets/memory-rR6pG57i.js +0 -2
  65. package/dist/client/assets/memory-screen-BV4YRKim.js +0 -1
  66. package/dist/client/assets/menu-9GT8MQlt.js +0 -1
  67. package/dist/client/assets/owner-Bm20thei.js +0 -1
  68. package/dist/client/assets/popupStateMapping-C797EAip.js +0 -1
  69. package/dist/client/assets/search-dialog-DD_ESAph.js +0 -1
  70. package/dist/client/assets/session-export-dialog-5X5WKL4z.js +0 -1
  71. package/dist/client/assets/settings-dialog-Bd0RXSk3.js +0 -1
  72. package/dist/client/assets/skills-DAKbQw5l.js +0 -2
  73. package/dist/client/assets/styles-Ecf_rLDJ.css +0 -1
  74. package/dist/client/assets/switch-BJY4FNiL.js +0 -1
  75. package/dist/client/assets/tabs-DBIBgGyG.js +0 -1
  76. package/dist/client/assets/tooltip-BoKzJ7ag.js +0 -1
  77. package/dist/client/assets/use-file-explorer-state-EK2HTABk.js +0 -12
  78. package/dist/client/assets/useButton-Bh6gEhdL.js +0 -1
  79. package/dist/client/assets/useCompositeItem-BR34_3gK.js +0 -1
  80. package/dist/client/assets/visuallyHidden-ChUPUs-q.js +0 -1
  81. package/dist/server/assets/_tanstack-start-manifest_v-XVyva8_b.js +0 -4
  82. package/dist/server/assets/settings-dialog-S2HICL7l.js +0 -1166
@@ -1,15 +1,15 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { Link, useNavigate } from "@tanstack/react-router";
3
3
  import * as React from "react";
4
- import React__default, { memo, useDeferredValue, useState, useMemo, useCallback, Suspense, lazy, useRef, useEffect, useLayoutEffect, createContext, useContext } from "react";
4
+ import React__default, { useState, useCallback, memo, useDeferredValue, useMemo, Suspense, lazy, useRef, useEffect, useLayoutEffect, createContext, useContext } from "react";
5
5
  import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query";
6
6
  import { T as TooltipProvider, a as TooltipRoot, b as TooltipTrigger, c as TooltipContent, s as setChatUiState, d as chatUiQueryKey, g as getChatUiState } from "./tooltip-DgsSPocE.js";
7
+ import { Tick01Icon, Cancel01Icon, MoreHorizontalIcon, Pen01Icon, Upload01Icon, Delete01Icon, BotIcon, Clock01Icon, Chat01Icon, ArrowRight01Icon, SidebarLeft01Icon, PencilEdit02Icon, Folder01Icon, AiBrain01Icon, PackageOpenIcon, SmartPhone01Icon, DashboardCircleIcon, Search01Icon, Settings01Icon, Menu01Icon, Tick02Icon, Copy01Icon, Loading02Icon, StopIcon, VolumeHighIcon, ArrowDown01Icon, Loading03Icon, File01Icon, ArtificialIntelligence02Icon, CommandIcon, Attachment01Icon, Mic02Icon, ArrowUp02Icon } from "@hugeicons/core-free-icons";
7
8
  import { HugeiconsIcon } from "@hugeicons/react";
8
- import { Tick01Icon, Cancel01Icon, MoreHorizontalIcon, Pen01Icon, Upload01Icon, Delete01Icon, BotIcon, Clock01Icon, Chat01Icon, ArrowRight01Icon, SidebarLeft01Icon, PencilEdit02Icon, Folder01Icon, AiBrain01Icon, PackageOpenIcon, SmartPhone01Icon, Search01Icon, Settings01Icon, Menu01Icon, Tick02Icon, Copy01Icon, Loading02Icon, StopIcon, VolumeHighIcon, ArrowDown01Icon, Loading03Icon, ArtificialIntelligence02Icon, CommandIcon, Attachment01Icon, File01Icon, Mic02Icon, ArrowUp02Icon } from "@hugeicons/core-free-icons";
9
9
  import { motion, AnimatePresence } from "motion/react";
10
- import { D as DialogRoot, a as DialogContent, b as DialogTitle, c as DialogDescription, d as DialogClose } from "./use-file-explorer-state-s7CS50ho.js";
11
- import { B as Button, c as cn, b as buttonVariants } from "./button-CwY2OHFj.js";
12
10
  import { AlertDialog } from "@base-ui/react/alert-dialog";
11
+ import { c as cn, B as Button, b as buttonVariants } from "./button-CwY2OHFj.js";
12
+ import { D as DialogRoot, a as DialogContent, b as DialogTitle, c as DialogDescription, d as DialogClose, u as useFileExplorerState } from "./use-file-explorer-state-s7CS50ho.js";
13
13
  import { Collapsible as Collapsible$1 } from "@base-ui/react/collapsible";
14
14
  import { ScrollArea } from "@base-ui/react/scroll-area";
15
15
  import { M as MenuRoot, a as MenuTrigger, b as MenuContent, c as MenuItem } from "./menu-D90CDTi2.js";
@@ -19,7 +19,7 @@ import { u as useChatSettings$1 } from "./index-Dl2BOKP7.js";
19
19
  import { create } from "zustand";
20
20
  import { persist } from "zustand/middleware";
21
21
  import { createPortal } from "react-dom";
22
- import { a as Route } from "./router-CkcOXH0V.js";
22
+ import { a as Route } from "./router-bN_iTo0B.js";
23
23
  function deriveFriendlyIdFromKey(key) {
24
24
  if (!key) return "main";
25
25
  const trimmed = key.trim();
@@ -29,9 +29,27 @@ function deriveFriendlyIdFromKey(key) {
29
29
  const tailTrimmed = tail.trim();
30
30
  return tailTrimmed.length > 0 ? tailTrimmed : trimmed;
31
31
  }
32
+ const INBOUND_META_FENCED_REGEX = /^Conversation info \(untrusted metadata\):\s*```(?:json)?\s*[\s\S]*?```\s*/;
33
+ const INBOUND_META_INLINE_REGEX = /^Conversation info \(untrusted metadata\):\s*\{[\s\S]*?\}\s*/;
34
+ const INBOUND_META_TIMESTAMP_REGEX = /^\[(?:[A-Za-z]{3}\s+)?\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+GMT[+-]?\d+\]\s*/;
35
+ function stripInboundMeta(text) {
36
+ let s = text;
37
+ const hasFenced = INBOUND_META_FENCED_REGEX.test(s);
38
+ const hasInline = !hasFenced && INBOUND_META_INLINE_REGEX.test(s);
39
+ if (hasFenced) {
40
+ s = s.replace(INBOUND_META_FENCED_REGEX, "");
41
+ } else if (hasInline) {
42
+ s = s.replace(INBOUND_META_INLINE_REGEX, "");
43
+ }
44
+ if (hasFenced || hasInline) {
45
+ s = s.replace(INBOUND_META_TIMESTAMP_REGEX, "");
46
+ }
47
+ return s.trim();
48
+ }
32
49
  function textFromMessage(msg) {
33
50
  const parts = Array.isArray(msg.content) ? msg.content : [];
34
- return parts.map((part) => part.type === "text" ? String(part.text ?? "") : "").join("").trim();
51
+ const raw = parts.map((part) => part.type === "text" ? String(part.text ?? "") : "").join("").trim();
52
+ return stripInboundMeta(raw);
35
53
  }
36
54
  function getToolCallsFromMessage(msg) {
37
55
  const parts = Array.isArray(msg.content) ? msg.content : [];
@@ -337,332 +355,606 @@ async function updateSessionLabel(queryClient, sessionKey, friendlyId, label) {
337
355
  console.error("[updateSessionLabel] Network error:", err);
338
356
  }
339
357
  }
340
- function extractMessageText(message) {
341
- if (!message.content || !Array.isArray(message.content)) {
342
- return "";
343
- }
344
- return message.content.filter((item) => item.type === "text").map((item) => item.text || "").join("\n");
345
- }
346
- function formatAsMarkdown(title, messages, date) {
347
- const lines = [];
348
- lines.push(`# Conversation: ${title}`);
349
- lines.push(`Date: ${date.toLocaleString()}`);
350
- lines.push("");
351
- for (const message of messages) {
352
- const role = message.role || "unknown";
353
- const text = extractMessageText(message);
354
- if (text) {
355
- const displayRole = role === "user" ? "User" : "Assistant";
356
- lines.push(`## ${displayRole}`);
357
- lines.push("");
358
- lines.push(text);
359
- lines.push("");
358
+ function useChatSettings() {
359
+ const [settingsOpen, setSettingsOpen] = useState(false);
360
+ const [pathsLoading, setPathsLoading] = useState(false);
361
+ const [pathsError, setPathsError] = useState(null);
362
+ const [paths, setPaths] = useState(null);
363
+ const openSettings = useCallback(async () => {
364
+ setSettingsOpen(true);
365
+ setPathsError(null);
366
+ if (pathsLoading || paths) return;
367
+ setPathsLoading(true);
368
+ try {
369
+ const res = await fetch("/api/paths");
370
+ if (!res.ok) throw new Error(await readError(res));
371
+ const data = await res.json();
372
+ setPaths({
373
+ agentId: String(data.agentId ?? "main"),
374
+ stateDir: String(data.stateDir ?? ""),
375
+ sessionsDir: String(data.sessionsDir ?? ""),
376
+ storePath: String(data.storePath ?? "")
377
+ });
378
+ } catch (err) {
379
+ setPathsError(err instanceof Error ? err.message : String(err));
380
+ } finally {
381
+ setPathsLoading(false);
360
382
  }
361
- }
362
- return lines.join("\n");
363
- }
364
- function formatAsJson(title, messages, date) {
365
- const data = {
366
- title,
367
- exportDate: date.toISOString(),
368
- messages: messages.map((message) => ({
369
- role: message.role,
370
- content: message.content,
371
- timestamp: message.timestamp
372
- }))
373
- };
374
- return JSON.stringify(data, null, 2);
375
- }
376
- function formatAsPlainText(title, messages, date) {
377
- const lines = [];
378
- lines.push(`Conversation: ${title}`);
379
- lines.push(`Date: ${date.toLocaleString()}`);
380
- lines.push("─".repeat(60));
381
- lines.push("");
382
- for (const message of messages) {
383
- const role = message.role || "unknown";
384
- const text = extractMessageText(message);
385
- if (text) {
386
- const displayRole = role === "user" ? "User" : "Assistant";
387
- lines.push(`${displayRole}:`);
388
- lines.push(text);
389
- lines.push("");
383
+ }, [paths, pathsLoading]);
384
+ const handleOpenSettings = useCallback(() => {
385
+ void openSettings();
386
+ }, [openSettings]);
387
+ const closeSettings = useCallback(() => {
388
+ setSettingsOpen(false);
389
+ }, []);
390
+ const copySessionsDir = useCallback(() => {
391
+ if (!paths?.sessionsDir) return;
392
+ try {
393
+ void navigator.clipboard.writeText(paths.sessionsDir);
394
+ } catch {
390
395
  }
391
- }
392
- return lines.join("\n");
396
+ }, [paths]);
397
+ const copyStorePath = useCallback(() => {
398
+ if (!paths?.storePath) return;
399
+ try {
400
+ void navigator.clipboard.writeText(paths.storePath);
401
+ } catch {
402
+ }
403
+ }, [paths]);
404
+ return {
405
+ settingsOpen,
406
+ setSettingsOpen,
407
+ pathsLoading,
408
+ pathsError,
409
+ paths,
410
+ handleOpenSettings,
411
+ closeSettings,
412
+ copySessionsDir,
413
+ copyStorePath
414
+ };
393
415
  }
394
- function exportConversation(title, messages, format) {
395
- const date = /* @__PURE__ */ new Date();
396
- let content;
397
- let filename;
398
- let mimeType;
399
- switch (format) {
400
- case "markdown":
401
- content = formatAsMarkdown(title, messages, date);
402
- filename = `${sanitizeFilename(title)}.md`;
403
- mimeType = "text/markdown";
404
- break;
405
- case "json":
406
- content = formatAsJson(title, messages, date);
407
- filename = `${sanitizeFilename(title)}.json`;
408
- mimeType = "application/json";
409
- break;
410
- case "txt":
411
- content = formatAsPlainText(title, messages, date);
412
- filename = `${sanitizeFilename(title)}.txt`;
413
- mimeType = "text/plain";
414
- break;
415
- default:
416
- throw new Error(`Unsupported format: ${format}`);
417
- }
418
- downloadFile(content, filename, mimeType);
416
+ let pendingSend = null;
417
+ let pendingGeneration = false;
418
+ let recentSession = null;
419
+ function stashPendingSend(payload) {
420
+ pendingSend = payload;
419
421
  }
420
- function sanitizeFilename(filename) {
421
- return filename.replace(/[^a-z0-9-_\s]/gi, "").replace(/\s+/g, "-").toLowerCase().slice(0, 50) || "conversation";
422
+ function hasPendingSend() {
423
+ return pendingSend !== null;
422
424
  }
423
- function downloadFile(content, filename, mimeType) {
424
- const blob = new Blob([content], { type: mimeType });
425
- const url = URL.createObjectURL(blob);
426
- const link = document.createElement("a");
427
- link.href = url;
428
- link.download = filename;
429
- document.body.appendChild(link);
430
- link.click();
431
- document.body.removeChild(link);
432
- URL.revokeObjectURL(url);
425
+ function setPendingGeneration(value) {
426
+ pendingGeneration = value;
433
427
  }
434
- function SessionRenameDialog({
435
- open,
436
- onOpenChange,
437
- sessionTitle,
438
- onSave,
439
- onCancel
440
- }) {
441
- return /* @__PURE__ */ jsx(DialogRoot, { open, onOpenChange, children: /* @__PURE__ */ jsx(DialogContent, { children: /* @__PURE__ */ jsxs("div", { className: "p-4", children: [
442
- /* @__PURE__ */ jsx(DialogTitle, { className: "mb-1", children: "Rename" }),
443
- /* @__PURE__ */ jsx(DialogDescription, { className: "mb-4", children: "Enter a new name for this session." }),
444
- /* @__PURE__ */ jsx(
445
- "input",
446
- {
447
- type: "text",
448
- defaultValue: sessionTitle,
449
- onKeyDown: (e) => {
450
- if (e.key === "Enter") {
451
- e.preventDefault();
452
- onSave(e.currentTarget.value);
453
- }
454
- },
455
- className: "w-full rounded-lg border border-primary-200 bg-primary-50 px-3 py-2 text-sm text-primary-900 outline-none focus:border-primary-400",
456
- placeholder: "Session name",
457
- autoFocus: true
458
- }
459
- ),
460
- /* @__PURE__ */ jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
461
- /* @__PURE__ */ jsx(DialogClose, { onClick: onCancel, children: "Cancel" }),
462
- /* @__PURE__ */ jsx(
463
- Button,
464
- {
465
- onClick: (e) => {
466
- const input = e.currentTarget.parentElement?.previousElementSibling;
467
- onSave(input.value);
468
- },
469
- children: "Save"
470
- }
471
- )
472
- ] })
473
- ] }) }) });
428
+ function hasPendingGeneration() {
429
+ return pendingGeneration;
474
430
  }
475
- function AlertDialogRoot({ children, ...props }) {
476
- return /* @__PURE__ */ jsx(AlertDialog.Root, { ...props, children });
431
+ function resetPendingSend() {
432
+ pendingSend = null;
433
+ pendingGeneration = false;
477
434
  }
478
- function AlertDialogContent({ className, children }) {
479
- return /* @__PURE__ */ jsxs(AlertDialog.Portal, { children: [
480
- /* @__PURE__ */ jsx(AlertDialog.Backdrop, { className: "fixed inset-0 bg-primary-950/20 transition-all duration-150 data-[state=open]:opacity-100 data-[state=closed]:opacity-0" }),
481
- /* @__PURE__ */ jsx(
482
- AlertDialog.Popup,
483
- {
484
- className: cn(
485
- "fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2",
486
- "w-[min(400px,92vw)] rounded-xl border border-primary-200 bg-primary-50 p-0 shadow-xl",
487
- "transition-all duration-150",
488
- "data-[state=open]:opacity-100 data-[state=closed]:opacity-0",
489
- "data-[state=open]:scale-100 data-[state=closed]:scale-95",
490
- className
491
- ),
492
- children
493
- }
494
- )
495
- ] });
435
+ function clearPendingSendForSession(sessionKey, friendlyId) {
436
+ if (!pendingSend) return;
437
+ if (sessionKey && pendingSend.sessionKey === sessionKey) {
438
+ resetPendingSend();
439
+ return;
440
+ }
441
+ if (friendlyId && pendingSend.friendlyId === friendlyId) {
442
+ resetPendingSend();
443
+ }
496
444
  }
497
- function AlertDialogTitle({ className, ...props }) {
498
- return /* @__PURE__ */ jsx(
499
- AlertDialog.Title,
500
- {
501
- className: cn("text-lg font-medium text-primary-900", className),
502
- ...props
503
- }
504
- );
445
+ function setRecentSession(friendlyId) {
446
+ recentSession = { friendlyId, at: Date.now() };
505
447
  }
506
- function AlertDialogDescription({
507
- className,
508
- ...props
509
- }) {
510
- return /* @__PURE__ */ jsx(
511
- AlertDialog.Description,
512
- {
513
- className: cn("text-sm text-primary-600", className),
514
- ...props
515
- }
516
- );
448
+ function isRecentSession(friendlyId, maxAgeMs = 15e3) {
449
+ if (!recentSession) return false;
450
+ if (recentSession.friendlyId !== friendlyId) return false;
451
+ if (Date.now() - recentSession.at > maxAgeMs) return false;
452
+ return true;
517
453
  }
518
- function AlertDialogCancel({ className, ...props }) {
519
- return /* @__PURE__ */ jsx(
520
- AlertDialog.Close,
521
- {
522
- render: /* @__PURE__ */ jsx(Button, { variant: "outline", className: cn(className) }),
523
- ...props
524
- }
525
- );
526
- }
527
- function AlertDialogAction({ className, ...props }) {
528
- return /* @__PURE__ */ jsx(
529
- AlertDialog.Close,
530
- {
531
- render: /* @__PURE__ */ jsx(Button, { variant: "destructive", className: cn(className) }),
532
- ...props
533
- }
534
- );
454
+ function consumePendingSend(sessionKey, friendlyId) {
455
+ if (!pendingSend) return null;
456
+ if (sessionKey && pendingSend.sessionKey === sessionKey) {
457
+ const payload = pendingSend;
458
+ pendingSend = null;
459
+ return payload;
460
+ }
461
+ if (friendlyId && pendingSend.friendlyId === friendlyId) {
462
+ const payload = pendingSend;
463
+ pendingSend = null;
464
+ return payload;
465
+ }
466
+ return null;
535
467
  }
536
- function SessionDeleteDialog({
537
- open,
538
- onOpenChange,
539
- sessionTitle,
540
- onConfirm,
541
- onCancel
542
- }) {
543
- return /* @__PURE__ */ jsx(AlertDialogRoot, { open, onOpenChange, children: /* @__PURE__ */ jsx(AlertDialogContent, { children: /* @__PURE__ */ jsxs("div", { className: "p-4", children: [
544
- /* @__PURE__ */ jsx(AlertDialogTitle, { className: "mb-1", children: "Delete Session" }),
545
- /* @__PURE__ */ jsxs(AlertDialogDescription, { className: "mb-4", children: [
546
- 'Are you sure you want to delete "',
547
- sessionTitle,
548
- '"? This action cannot be undone.'
549
- ] }),
550
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
551
- /* @__PURE__ */ jsx(AlertDialogCancel, { onClick: onCancel, children: "Cancel" }),
552
- /* @__PURE__ */ jsx(AlertDialogAction, { onClick: onConfirm, children: "Delete" })
553
- ] })
554
- ] }) }) });
468
+ const TOMBSTONE_TTL_MS = 8e3;
469
+ const tombstones = /* @__PURE__ */ new Map();
470
+ function markSessionDeleted(id) {
471
+ if (!id) return;
472
+ tombstones.set(id, { id, expiresAt: Date.now() + TOMBSTONE_TTL_MS });
555
473
  }
556
- function Collapsible(props) {
557
- return /* @__PURE__ */ jsx(Collapsible$1.Root, { ...props });
474
+ function clearSessionDeleted(id) {
475
+ if (!id) return;
476
+ tombstones.delete(id);
558
477
  }
559
- function CollapsibleTrigger({
560
- className,
561
- ...props
562
- }) {
563
- return /* @__PURE__ */ jsx(
564
- Collapsible$1.Trigger,
565
- {
566
- className: cn(
567
- "group inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-left text-xs font-medium text-primary-500 transition-colors hover:bg-primary-100 hover:text-primary-700 data-panel-open:text-primary-700",
568
- className
569
- ),
570
- ...props
478
+ function filterSessionsWithTombstones(sessions) {
479
+ if (tombstones.size === 0) return sessions;
480
+ const now = Date.now();
481
+ let changed = false;
482
+ const next = sessions.filter((session) => {
483
+ const keyTombstone = tombstones.get(session.key);
484
+ const friendlyTombstone = tombstones.get(session.friendlyId);
485
+ const isExpired = keyTombstone && keyTombstone.expiresAt <= now || friendlyTombstone && friendlyTombstone.expiresAt <= now;
486
+ if (isExpired) {
487
+ if (keyTombstone && keyTombstone.expiresAt <= now) {
488
+ tombstones.delete(session.key);
489
+ }
490
+ if (friendlyTombstone && friendlyTombstone.expiresAt <= now) {
491
+ tombstones.delete(session.friendlyId);
492
+ }
493
+ return true;
571
494
  }
572
- );
573
- }
574
- function CollapsiblePanel({
575
- className,
576
- contentClassName,
577
- children,
578
- ...props
579
- }) {
580
- return /* @__PURE__ */ jsx(
581
- Collapsible$1.Panel,
582
- {
583
- className: cn(
584
- 'flex h-(--collapsible-panel-height) flex-col overflow-hidden text-sm transition-all duration-150 ease-out data-ending-style:h-0 data-starting-style:h-0 [&[hidden]:not([hidden="until-found"])]:hidden',
585
- className
586
- ),
587
- ...props,
588
- children: /* @__PURE__ */ jsx("div", { className: cn("pt-1", contentClassName), children })
495
+ if (keyTombstone || friendlyTombstone) {
496
+ changed = true;
497
+ return false;
589
498
  }
590
- );
499
+ return true;
500
+ });
501
+ return changed ? next : sessions;
591
502
  }
592
- function ScrollAreaRoot({ className, ...props }) {
593
- return /* @__PURE__ */ jsx(
594
- ScrollArea.Root,
595
- {
596
- className: cn(
597
- "group/scroll-area relative outline-none focus-visible:outline-none",
598
- className
599
- ),
600
- ...props
503
+ function useDeleteSession() {
504
+ const queryClient = useQueryClient();
505
+ const [deleting, setDeleting] = useState(false);
506
+ const [error, setError] = useState(null);
507
+ const mutation = useMutation({
508
+ mutationFn: async function deleteSessionRequest2(payload) {
509
+ const query = new URLSearchParams();
510
+ if (payload.sessionKey) query.set("sessionKey", payload.sessionKey);
511
+ if (payload.friendlyId) query.set("friendlyId", payload.friendlyId);
512
+ const res = await fetch(`/api/sessions?${query.toString()}`, {
513
+ method: "DELETE"
514
+ });
515
+ if (!res.ok) throw new Error(await readError(res));
516
+ return payload;
517
+ },
518
+ onMutate: async function onMutate(payload) {
519
+ setError(null);
520
+ markSessionDeleted(payload.sessionKey || payload.friendlyId);
521
+ clearPendingSendForSession(payload.sessionKey, payload.friendlyId);
522
+ await queryClient.cancelQueries({ queryKey: chatQueryKeys.sessions });
523
+ const previousSessions = queryClient.getQueryData(chatQueryKeys.sessions);
524
+ removeSessionFromCache(
525
+ queryClient,
526
+ payload.sessionKey,
527
+ payload.friendlyId
528
+ );
529
+ if (payload.isActive && (payload.sessionKey || payload.friendlyId)) {
530
+ clearHistoryMessages(
531
+ queryClient,
532
+ payload.friendlyId || payload.sessionKey,
533
+ payload.sessionKey || payload.friendlyId
534
+ );
535
+ }
536
+ return { previousSessions, isActive: payload.isActive };
537
+ },
538
+ onError: function onError(err, _payload, context) {
539
+ if (context?.previousSessions) {
540
+ queryClient.setQueryData(
541
+ chatQueryKeys.sessions,
542
+ context.previousSessions
543
+ );
544
+ }
545
+ clearSessionDeleted(_payload.sessionKey || _payload.friendlyId);
546
+ setError(err instanceof Error ? err.message : String(err));
547
+ },
548
+ onSuccess: function onSuccess(payload) {
549
+ if (payload.isActive) {
550
+ resetPendingSend();
551
+ }
552
+ queryClient.invalidateQueries({ queryKey: chatQueryKeys.sessions });
553
+ },
554
+ onSettled: function onSettled() {
555
+ setDeleting(false);
601
556
  }
557
+ });
558
+ const deleteSession = useCallback(
559
+ async (sessionKey, friendlyId, isActive) => {
560
+ if (!sessionKey && !friendlyId) return;
561
+ setDeleting(true);
562
+ await mutation.mutateAsync({ sessionKey, friendlyId, isActive });
563
+ },
564
+ [mutation]
602
565
  );
566
+ return { deleteSession, deleting, error };
603
567
  }
604
- function ScrollAreaViewport({ className, ...props }) {
605
- return /* @__PURE__ */ jsx(
606
- ScrollArea.Viewport,
607
- {
608
- className: cn(
609
- "h-full w-full outline-none focus-visible:outline-none",
610
- className
611
- ),
612
- ...props
568
+ function useRenameSession() {
569
+ const queryClient = useQueryClient();
570
+ const [renaming, setRenaming] = useState(false);
571
+ const [error, setError] = useState(null);
572
+ const mutation = useMutation({
573
+ mutationFn: async function renameSessionRequest(payload) {
574
+ const res = await fetch("/api/sessions", {
575
+ method: "PATCH",
576
+ headers: { "content-type": "application/json" },
577
+ body: JSON.stringify({
578
+ sessionKey: payload.sessionKey,
579
+ label: payload.newTitle
580
+ })
581
+ });
582
+ if (!res.ok) throw new Error(await readError(res));
583
+ return payload;
584
+ },
585
+ onMutate: async function onMutate(payload) {
586
+ setError(null);
587
+ await queryClient.cancelQueries({ queryKey: chatQueryKeys.sessions });
588
+ const previousSessions = queryClient.getQueryData(chatQueryKeys.sessions);
589
+ queryClient.setQueryData(
590
+ chatQueryKeys.sessions,
591
+ function update(sessions) {
592
+ if (!Array.isArray(sessions)) return sessions;
593
+ return sessions.map((session) => {
594
+ if (session.key !== payload.sessionKey) return session;
595
+ return {
596
+ ...session,
597
+ label: payload.newTitle,
598
+ title: payload.newTitle
599
+ };
600
+ });
601
+ }
602
+ );
603
+ return { previousSessions };
604
+ },
605
+ onError: function onError(err, _payload, context) {
606
+ if (context?.previousSessions) {
607
+ queryClient.setQueryData(
608
+ chatQueryKeys.sessions,
609
+ context.previousSessions
610
+ );
611
+ }
612
+ setError(err instanceof Error ? err.message : String(err));
613
+ },
614
+ onSuccess: function onSuccess() {
615
+ queryClient.invalidateQueries({ queryKey: chatQueryKeys.sessions });
616
+ },
617
+ onSettled: function onSettled() {
618
+ setRenaming(false);
613
619
  }
620
+ });
621
+ const renameSession = useCallback(
622
+ async (sessionKey, newTitle) => {
623
+ if (!sessionKey || !newTitle.trim()) return;
624
+ setRenaming(true);
625
+ await mutation.mutateAsync({ sessionKey, newTitle: newTitle.trim() });
626
+ },
627
+ [mutation]
614
628
  );
629
+ return { renameSession, renaming, error };
615
630
  }
616
- function ScrollAreaScrollbar({
617
- className,
618
- ...props
619
- }) {
620
- return /* @__PURE__ */ jsx(
621
- ScrollArea.Scrollbar,
631
+ function extractMessageText(message) {
632
+ if (!message.content || !Array.isArray(message.content)) {
633
+ return "";
634
+ }
635
+ return message.content.filter((item) => item.type === "text").map((item) => item.text || "").join("\n");
636
+ }
637
+ function formatAsMarkdown(title, messages, date) {
638
+ const lines = [];
639
+ lines.push(`# Conversation: ${title}`);
640
+ lines.push(`Date: ${date.toLocaleString()}`);
641
+ lines.push("");
642
+ for (const message of messages) {
643
+ const role = message.role || "unknown";
644
+ const text = extractMessageText(message);
645
+ if (text) {
646
+ const displayRole = role === "user" ? "User" : "Assistant";
647
+ lines.push(`## ${displayRole}`);
648
+ lines.push("");
649
+ lines.push(text);
650
+ lines.push("");
651
+ }
652
+ }
653
+ return lines.join("\n");
654
+ }
655
+ function formatAsJson(title, messages, date) {
656
+ const data = {
657
+ title,
658
+ exportDate: date.toISOString(),
659
+ messages: messages.map((message) => ({
660
+ role: message.role,
661
+ content: message.content,
662
+ timestamp: message.timestamp
663
+ }))
664
+ };
665
+ return JSON.stringify(data, null, 2);
666
+ }
667
+ function formatAsPlainText(title, messages, date) {
668
+ const lines = [];
669
+ lines.push(`Conversation: ${title}`);
670
+ lines.push(`Date: ${date.toLocaleString()}`);
671
+ lines.push("─".repeat(60));
672
+ lines.push("");
673
+ for (const message of messages) {
674
+ const role = message.role || "unknown";
675
+ const text = extractMessageText(message);
676
+ if (text) {
677
+ const displayRole = role === "user" ? "User" : "Assistant";
678
+ lines.push(`${displayRole}:`);
679
+ lines.push(text);
680
+ lines.push("");
681
+ }
682
+ }
683
+ return lines.join("\n");
684
+ }
685
+ function exportConversation(title, messages, format) {
686
+ const date = /* @__PURE__ */ new Date();
687
+ let content;
688
+ let filename;
689
+ let mimeType;
690
+ switch (format) {
691
+ case "markdown":
692
+ content = formatAsMarkdown(title, messages, date);
693
+ filename = `${sanitizeFilename(title)}.md`;
694
+ mimeType = "text/markdown";
695
+ break;
696
+ case "json":
697
+ content = formatAsJson(title, messages, date);
698
+ filename = `${sanitizeFilename(title)}.json`;
699
+ mimeType = "application/json";
700
+ break;
701
+ case "txt":
702
+ content = formatAsPlainText(title, messages, date);
703
+ filename = `${sanitizeFilename(title)}.txt`;
704
+ mimeType = "text/plain";
705
+ break;
706
+ default:
707
+ throw new Error(`Unsupported format: ${format}`);
708
+ }
709
+ downloadFile(content, filename, mimeType);
710
+ }
711
+ function sanitizeFilename(filename) {
712
+ return filename.replace(/[^a-z0-9-_\s]/gi, "").replace(/\s+/g, "-").toLowerCase().slice(0, 50) || "conversation";
713
+ }
714
+ function downloadFile(content, filename, mimeType) {
715
+ const blob = new Blob([content], { type: mimeType });
716
+ const url = URL.createObjectURL(blob);
717
+ const link = document.createElement("a");
718
+ link.href = url;
719
+ link.download = filename;
720
+ document.body.appendChild(link);
721
+ link.click();
722
+ document.body.removeChild(link);
723
+ URL.revokeObjectURL(url);
724
+ }
725
+ function AlertDialogRoot({ children, ...props }) {
726
+ return /* @__PURE__ */ jsx(AlertDialog.Root, { ...props, children });
727
+ }
728
+ function AlertDialogContent({ className, children }) {
729
+ return /* @__PURE__ */ jsxs(AlertDialog.Portal, { children: [
730
+ /* @__PURE__ */ jsx(AlertDialog.Backdrop, { className: "fixed inset-0 bg-primary-950/20 transition-all duration-150 data-[state=open]:opacity-100 data-[state=closed]:opacity-0" }),
731
+ /* @__PURE__ */ jsx(
732
+ AlertDialog.Popup,
733
+ {
734
+ className: cn(
735
+ "fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2",
736
+ "w-[min(400px,92vw)] rounded-xl border border-primary-200 bg-primary-50 p-0 shadow-xl",
737
+ "transition-all duration-150",
738
+ "data-[state=open]:opacity-100 data-[state=closed]:opacity-0",
739
+ "data-[state=open]:scale-100 data-[state=closed]:scale-95",
740
+ className
741
+ ),
742
+ children
743
+ }
744
+ )
745
+ ] });
746
+ }
747
+ function AlertDialogTitle({ className, ...props }) {
748
+ return /* @__PURE__ */ jsx(
749
+ AlertDialog.Title,
750
+ {
751
+ className: cn("text-lg font-medium text-primary-900", className),
752
+ ...props
753
+ }
754
+ );
755
+ }
756
+ function AlertDialogDescription({
757
+ className,
758
+ ...props
759
+ }) {
760
+ return /* @__PURE__ */ jsx(
761
+ AlertDialog.Description,
762
+ {
763
+ className: cn("text-sm text-primary-600", className),
764
+ ...props
765
+ }
766
+ );
767
+ }
768
+ function AlertDialogCancel({ className, ...props }) {
769
+ return /* @__PURE__ */ jsx(
770
+ AlertDialog.Close,
771
+ {
772
+ render: /* @__PURE__ */ jsx(Button, { variant: "outline", className: cn(className) }),
773
+ ...props
774
+ }
775
+ );
776
+ }
777
+ function AlertDialogAction({ className, ...props }) {
778
+ return /* @__PURE__ */ jsx(
779
+ AlertDialog.Close,
780
+ {
781
+ render: /* @__PURE__ */ jsx(Button, { variant: "destructive", className: cn(className) }),
782
+ ...props
783
+ }
784
+ );
785
+ }
786
+ function SessionDeleteDialog({
787
+ open,
788
+ onOpenChange,
789
+ sessionTitle,
790
+ onConfirm,
791
+ onCancel
792
+ }) {
793
+ return /* @__PURE__ */ jsx(AlertDialogRoot, { open, onOpenChange, children: /* @__PURE__ */ jsx(AlertDialogContent, { children: /* @__PURE__ */ jsxs("div", { className: "p-4", children: [
794
+ /* @__PURE__ */ jsx(AlertDialogTitle, { className: "mb-1", children: "Delete Session" }),
795
+ /* @__PURE__ */ jsxs(AlertDialogDescription, { className: "mb-4", children: [
796
+ 'Are you sure you want to delete "',
797
+ sessionTitle,
798
+ '"? This action cannot be undone.'
799
+ ] }),
800
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
801
+ /* @__PURE__ */ jsx(AlertDialogCancel, { onClick: onCancel, children: "Cancel" }),
802
+ /* @__PURE__ */ jsx(AlertDialogAction, { onClick: onConfirm, children: "Delete" })
803
+ ] })
804
+ ] }) }) });
805
+ }
806
+ function SessionRenameDialog({
807
+ open,
808
+ onOpenChange,
809
+ sessionTitle,
810
+ onSave,
811
+ onCancel
812
+ }) {
813
+ return /* @__PURE__ */ jsx(DialogRoot, { open, onOpenChange, children: /* @__PURE__ */ jsx(DialogContent, { children: /* @__PURE__ */ jsxs("div", { className: "p-4", children: [
814
+ /* @__PURE__ */ jsx(DialogTitle, { className: "mb-1", children: "Rename" }),
815
+ /* @__PURE__ */ jsx(DialogDescription, { className: "mb-4", children: "Enter a new name for this session." }),
816
+ /* @__PURE__ */ jsx(
817
+ "input",
818
+ {
819
+ type: "text",
820
+ defaultValue: sessionTitle,
821
+ onKeyDown: (e) => {
822
+ if (e.key === "Enter") {
823
+ e.preventDefault();
824
+ onSave(e.currentTarget.value);
825
+ }
826
+ },
827
+ className: "w-full rounded-lg border border-primary-200 bg-primary-50 px-3 py-2 text-sm text-primary-900 outline-none focus:border-primary-400",
828
+ placeholder: "Session name",
829
+ autoFocus: true
830
+ }
831
+ ),
832
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
833
+ /* @__PURE__ */ jsx(DialogClose, { onClick: onCancel, children: "Cancel" }),
834
+ /* @__PURE__ */ jsx(
835
+ Button,
836
+ {
837
+ onClick: (e) => {
838
+ const input = e.currentTarget.parentElement?.previousElementSibling;
839
+ onSave(input.value);
840
+ },
841
+ children: "Save"
842
+ }
843
+ )
844
+ ] })
845
+ ] }) }) });
846
+ }
847
+ function Collapsible(props) {
848
+ return /* @__PURE__ */ jsx(Collapsible$1.Root, { ...props });
849
+ }
850
+ function CollapsibleTrigger({
851
+ className,
852
+ ...props
853
+ }) {
854
+ return /* @__PURE__ */ jsx(
855
+ Collapsible$1.Trigger,
622
856
  {
623
857
  className: cn(
624
- "flex w-2 touch-none select-none p-0.5 outline-none focus-visible:outline-none",
625
- "opacity-0 transition-opacity duration-150",
626
- "data-hovering:opacity-100 data-scrolling:opacity-100 group-hover/scroll-area:opacity-100",
858
+ "group inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-left text-xs font-medium text-primary-500 transition-colors hover:bg-primary-100 hover:text-primary-700 data-panel-open:text-primary-700",
627
859
  className
628
860
  ),
629
861
  ...props
630
862
  }
631
863
  );
632
864
  }
633
- function ScrollAreaThumb({ className, ...props }) {
865
+ function CollapsiblePanel({
866
+ className,
867
+ contentClassName,
868
+ children,
869
+ ...props
870
+ }) {
634
871
  return /* @__PURE__ */ jsx(
635
- ScrollArea.Thumb,
872
+ Collapsible$1.Panel,
636
873
  {
637
874
  className: cn(
638
- "flex-1 rounded-full bg-primary-500 outline-none focus-visible:outline-none",
875
+ 'flex h-(--collapsible-panel-height) flex-col overflow-hidden text-sm transition-all duration-150 ease-out data-ending-style:h-0 data-starting-style:h-0 [&[hidden]:not([hidden="until-found"])]:hidden',
639
876
  className
640
877
  ),
641
- ...props
878
+ ...props,
879
+ children: /* @__PURE__ */ jsx("div", { className: cn("pt-1", contentClassName), children })
642
880
  }
643
881
  );
644
882
  }
645
- function ScrollAreaCorner({ className, ...props }) {
883
+ function ScrollAreaRoot({ className, ...props }) {
646
884
  return /* @__PURE__ */ jsx(
647
- ScrollArea.Corner,
885
+ ScrollArea.Root,
648
886
  {
649
887
  className: cn(
650
- "bg-primary-100 outline-none focus-visible:outline-none",
888
+ "group/scroll-area relative outline-none focus-visible:outline-none",
651
889
  className
652
890
  ),
653
891
  ...props
654
892
  }
655
893
  );
656
894
  }
657
- function getKindIcon(kind) {
658
- if (kind === "subagent") return BotIcon;
659
- if (kind === "cron") return Clock01Icon;
660
- return Chat01Icon;
661
- }
662
- function previewFromMessage(message) {
663
- if (!message || !Array.isArray(message.content)) return "";
664
- const text = message.content.map((part) => part.type === "text" ? String(part.text ?? "") : "").join(" ").replace(/\s+/g, " ").trim();
665
- return text;
895
+ function ScrollAreaViewport({ className, ...props }) {
896
+ return /* @__PURE__ */ jsx(
897
+ ScrollArea.Viewport,
898
+ {
899
+ className: cn(
900
+ "h-full w-full outline-none focus-visible:outline-none",
901
+ className
902
+ ),
903
+ ...props
904
+ }
905
+ );
906
+ }
907
+ function ScrollAreaScrollbar({
908
+ className,
909
+ ...props
910
+ }) {
911
+ return /* @__PURE__ */ jsx(
912
+ ScrollArea.Scrollbar,
913
+ {
914
+ className: cn(
915
+ "flex w-2 touch-none select-none p-0.5 outline-none focus-visible:outline-none",
916
+ "opacity-0 transition-opacity duration-150",
917
+ "data-hovering:opacity-100 data-scrolling:opacity-100 group-hover/scroll-area:opacity-100",
918
+ className
919
+ ),
920
+ ...props
921
+ }
922
+ );
923
+ }
924
+ function ScrollAreaThumb({ className, ...props }) {
925
+ return /* @__PURE__ */ jsx(
926
+ ScrollArea.Thumb,
927
+ {
928
+ className: cn(
929
+ "flex-1 rounded-full bg-primary-500 outline-none focus-visible:outline-none",
930
+ className
931
+ ),
932
+ ...props
933
+ }
934
+ );
935
+ }
936
+ function ScrollAreaCorner({ className, ...props }) {
937
+ return /* @__PURE__ */ jsx(
938
+ ScrollArea.Corner,
939
+ {
940
+ className: cn(
941
+ "bg-primary-100 outline-none focus-visible:outline-none",
942
+ className
943
+ ),
944
+ ...props
945
+ }
946
+ );
947
+ }
948
+ function getKindIcon(kind) {
949
+ if (kind === "subagent") return BotIcon;
950
+ if (kind === "cron") return Clock01Icon;
951
+ return Chat01Icon;
952
+ }
953
+ function previewFromMessage(message) {
954
+ if (!message || !Array.isArray(message.content)) return "";
955
+ const text = message.content.map((part) => part.type === "text" ? String(part.text ?? "") : "").join(" ");
956
+ const cleaned = stripInboundMeta(text).replace(/\s+/g, " ").trim();
957
+ return cleaned;
666
958
  }
667
959
  function normalizeTimestamp$1(value) {
668
960
  if (typeof value === "number" && Number.isFinite(value)) {
@@ -1280,389 +1572,116 @@ ${failedSessionLabels.join("\n")}`
1280
1572
  session,
1281
1573
  active: isSessionActive(session, activeFriendlyId, activeSessionKey),
1282
1574
  isGenerating: isSessionGenerating(session, activeFriendlyId, activeSessionKey, isStreaming),
1283
- isPinned: false,
1284
- selectionMode,
1285
- selected: selectedSessionKeys.has(session.key),
1286
- onToggleSelect: handleToggleSelect,
1287
- onSelect,
1288
- onTogglePin: handleTogglePin,
1289
- onRename,
1290
- onDelete,
1291
- onExport
1292
- },
1293
- session.key
1294
- )) })
1295
- ] }) : null,
1296
- renderFolderGroup(
1297
- "webchat",
1298
- "🦎 OpenCami",
1299
- groupedSessions.webchat
1300
- ),
1301
- renderFolderGroup(
1302
- "subagent",
1303
- "🤖 Sub-agents",
1304
- groupedSessions.subagent
1305
- ),
1306
- renderFolderGroup(
1307
- "cron",
1308
- "⏰ Cron / Isolated",
1309
- groupedSessions.cron
1310
- ),
1311
- renderFolderGroup("other", "📁 Other", groupedSessions.other)
1312
- ] }) }),
1313
- /* @__PURE__ */ jsx(ScrollAreaScrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollAreaThumb, {}) })
1314
- ] })
1315
- }
1316
- ),
1317
- selectionMode ? /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-t border-primary-200 bg-surface px-2 py-2 flex flex-col gap-1.5", children: [
1318
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
1319
- /* @__PURE__ */ jsxs("span", { className: "text-xs text-primary-600 font-medium", children: [
1320
- selectedCount,
1321
- " selected"
1322
- ] }),
1323
- /* @__PURE__ */ jsx(
1324
- "button",
1325
- {
1326
- type: "button",
1327
- onClick: handleSelectAll,
1328
- className: "text-[11px] font-medium text-primary-600 hover:text-primary-800",
1329
- children: "Select all"
1330
- }
1331
- )
1332
- ] }),
1333
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
1334
- /* @__PURE__ */ jsxs(
1335
- "button",
1336
- {
1337
- type: "button",
1338
- onClick: handleDeleteSelected,
1339
- disabled: selectedCount === 0,
1340
- className: cn(
1341
- "flex-1 inline-flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-colors",
1342
- selectedCount > 0 ? "bg-red-100 text-red-700 hover:bg-red-200" : "bg-primary-100 text-primary-400 cursor-not-allowed"
1343
- ),
1344
- children: [
1345
- /* @__PURE__ */ jsx(HugeiconsIcon, { icon: Delete01Icon, size: 14, strokeWidth: 1.5 }),
1346
- "Delete"
1347
- ]
1348
- }
1349
- ),
1350
- /* @__PURE__ */ jsx(
1351
- "button",
1352
- {
1353
- type: "button",
1354
- onClick: handleCancelSelection,
1355
- className: "flex-1 inline-flex items-center justify-center rounded-md px-2 py-1.5 text-xs font-medium bg-primary-100 text-primary-700 hover:bg-primary-200 transition-colors",
1356
- children: "Cancel"
1357
- }
1358
- )
1359
- ] })
1360
- ] }) : null
1361
- ]
1362
- }
1363
- );
1364
- }, areSidebarSessionsEqual);
1365
- function areSidebarSessionsEqual(prev, next) {
1366
- if (prev.activeFriendlyId !== next.activeFriendlyId) return false;
1367
- if (prev.activeSessionKey !== next.activeSessionKey) return false;
1368
- if (prev.isStreaming !== next.isStreaming) return false;
1369
- if (prev.defaultOpen !== next.defaultOpen) return false;
1370
- if (prev.onSelect !== next.onSelect) return false;
1371
- if (prev.onRename !== next.onRename) return false;
1372
- if (prev.onDelete !== next.onDelete) return false;
1373
- if (prev.onExport !== next.onExport) return false;
1374
- if (prev.sessions === next.sessions) return true;
1375
- if (prev.sessions.length !== next.sessions.length) return false;
1376
- for (let i = 0; i < prev.sessions.length; i += 1) {
1377
- const prevSession = prev.sessions[i];
1378
- const nextSession = next.sessions[i];
1379
- if (prevSession.key !== nextSession.key) return false;
1380
- if (prevSession.friendlyId !== nextSession.friendlyId) return false;
1381
- if (prevSession.label !== nextSession.label) return false;
1382
- if (prevSession.title !== nextSession.title) return false;
1383
- if (prevSession.derivedTitle !== nextSession.derivedTitle) return false;
1384
- if (prevSession.updatedAt !== nextSession.updatedAt) return false;
1385
- if (prevSession.kind !== nextSession.kind) return false;
1386
- if (prevSession.status !== nextSession.status) return false;
1387
- if (prevSession.lastMessage !== nextSession.lastMessage) return false;
1388
- }
1389
- return true;
1390
- }
1391
- function useChatSettings() {
1392
- const [settingsOpen, setSettingsOpen] = useState(false);
1393
- const [pathsLoading, setPathsLoading] = useState(false);
1394
- const [pathsError, setPathsError] = useState(null);
1395
- const [paths, setPaths] = useState(null);
1396
- const openSettings = useCallback(async () => {
1397
- setSettingsOpen(true);
1398
- setPathsError(null);
1399
- if (pathsLoading || paths) return;
1400
- setPathsLoading(true);
1401
- try {
1402
- const res = await fetch("/api/paths");
1403
- if (!res.ok) throw new Error(await readError(res));
1404
- const data = await res.json();
1405
- setPaths({
1406
- agentId: String(data.agentId ?? "main"),
1407
- stateDir: String(data.stateDir ?? ""),
1408
- sessionsDir: String(data.sessionsDir ?? ""),
1409
- storePath: String(data.storePath ?? "")
1410
- });
1411
- } catch (err) {
1412
- setPathsError(err instanceof Error ? err.message : String(err));
1413
- } finally {
1414
- setPathsLoading(false);
1415
- }
1416
- }, [paths, pathsLoading]);
1417
- const handleOpenSettings = useCallback(() => {
1418
- void openSettings();
1419
- }, [openSettings]);
1420
- const closeSettings = useCallback(() => {
1421
- setSettingsOpen(false);
1422
- }, []);
1423
- const copySessionsDir = useCallback(() => {
1424
- if (!paths?.sessionsDir) return;
1425
- try {
1426
- void navigator.clipboard.writeText(paths.sessionsDir);
1427
- } catch {
1428
- }
1429
- }, [paths]);
1430
- const copyStorePath = useCallback(() => {
1431
- if (!paths?.storePath) return;
1432
- try {
1433
- void navigator.clipboard.writeText(paths.storePath);
1434
- } catch {
1435
- }
1436
- }, [paths]);
1437
- return {
1438
- settingsOpen,
1439
- setSettingsOpen,
1440
- pathsLoading,
1441
- pathsError,
1442
- paths,
1443
- handleOpenSettings,
1444
- closeSettings,
1445
- copySessionsDir,
1446
- copyStorePath
1447
- };
1448
- }
1449
- let pendingSend = null;
1450
- let pendingGeneration = false;
1451
- let recentSession = null;
1452
- function stashPendingSend(payload) {
1453
- pendingSend = payload;
1454
- }
1455
- function hasPendingSend() {
1456
- return pendingSend !== null;
1457
- }
1458
- function setPendingGeneration(value) {
1459
- pendingGeneration = value;
1460
- }
1461
- function hasPendingGeneration() {
1462
- return pendingGeneration;
1463
- }
1464
- function resetPendingSend() {
1465
- pendingSend = null;
1466
- pendingGeneration = false;
1467
- }
1468
- function clearPendingSendForSession(sessionKey, friendlyId) {
1469
- if (!pendingSend) return;
1470
- if (sessionKey && pendingSend.sessionKey === sessionKey) {
1471
- resetPendingSend();
1472
- return;
1473
- }
1474
- if (friendlyId && pendingSend.friendlyId === friendlyId) {
1475
- resetPendingSend();
1476
- }
1477
- }
1478
- function setRecentSession(friendlyId) {
1479
- recentSession = { friendlyId, at: Date.now() };
1480
- }
1481
- function isRecentSession(friendlyId, maxAgeMs = 15e3) {
1482
- if (!recentSession) return false;
1483
- if (recentSession.friendlyId !== friendlyId) return false;
1484
- if (Date.now() - recentSession.at > maxAgeMs) return false;
1485
- return true;
1486
- }
1487
- function consumePendingSend(sessionKey, friendlyId) {
1488
- if (!pendingSend) return null;
1489
- if (sessionKey && pendingSend.sessionKey === sessionKey) {
1490
- const payload = pendingSend;
1491
- pendingSend = null;
1492
- return payload;
1493
- }
1494
- if (friendlyId && pendingSend.friendlyId === friendlyId) {
1495
- const payload = pendingSend;
1496
- pendingSend = null;
1497
- return payload;
1498
- }
1499
- return null;
1500
- }
1501
- const TOMBSTONE_TTL_MS = 8e3;
1502
- const tombstones = /* @__PURE__ */ new Map();
1503
- function markSessionDeleted(id) {
1504
- if (!id) return;
1505
- tombstones.set(id, { id, expiresAt: Date.now() + TOMBSTONE_TTL_MS });
1506
- }
1507
- function clearSessionDeleted(id) {
1508
- if (!id) return;
1509
- tombstones.delete(id);
1510
- }
1511
- function filterSessionsWithTombstones(sessions) {
1512
- if (tombstones.size === 0) return sessions;
1513
- const now = Date.now();
1514
- let changed = false;
1515
- const next = sessions.filter((session) => {
1516
- const keyTombstone = tombstones.get(session.key);
1517
- const friendlyTombstone = tombstones.get(session.friendlyId);
1518
- const isExpired = keyTombstone && keyTombstone.expiresAt <= now || friendlyTombstone && friendlyTombstone.expiresAt <= now;
1519
- if (isExpired) {
1520
- if (keyTombstone && keyTombstone.expiresAt <= now) {
1521
- tombstones.delete(session.key);
1522
- }
1523
- if (friendlyTombstone && friendlyTombstone.expiresAt <= now) {
1524
- tombstones.delete(session.friendlyId);
1525
- }
1526
- return true;
1527
- }
1528
- if (keyTombstone || friendlyTombstone) {
1529
- changed = true;
1530
- return false;
1531
- }
1532
- return true;
1533
- });
1534
- return changed ? next : sessions;
1535
- }
1536
- function useDeleteSession() {
1537
- const queryClient = useQueryClient();
1538
- const [deleting, setDeleting] = useState(false);
1539
- const [error, setError] = useState(null);
1540
- const mutation = useMutation({
1541
- mutationFn: async function deleteSessionRequest2(payload) {
1542
- const query = new URLSearchParams();
1543
- if (payload.sessionKey) query.set("sessionKey", payload.sessionKey);
1544
- if (payload.friendlyId) query.set("friendlyId", payload.friendlyId);
1545
- const res = await fetch(`/api/sessions?${query.toString()}`, {
1546
- method: "DELETE"
1547
- });
1548
- if (!res.ok) throw new Error(await readError(res));
1549
- return payload;
1550
- },
1551
- onMutate: async function onMutate(payload) {
1552
- setError(null);
1553
- markSessionDeleted(payload.sessionKey || payload.friendlyId);
1554
- clearPendingSendForSession(payload.sessionKey, payload.friendlyId);
1555
- await queryClient.cancelQueries({ queryKey: chatQueryKeys.sessions });
1556
- const previousSessions = queryClient.getQueryData(chatQueryKeys.sessions);
1557
- removeSessionFromCache(
1558
- queryClient,
1559
- payload.sessionKey,
1560
- payload.friendlyId
1561
- );
1562
- if (payload.isActive && (payload.sessionKey || payload.friendlyId)) {
1563
- clearHistoryMessages(
1564
- queryClient,
1565
- payload.friendlyId || payload.sessionKey,
1566
- payload.sessionKey || payload.friendlyId
1567
- );
1568
- }
1569
- return { previousSessions, isActive: payload.isActive };
1570
- },
1571
- onError: function onError(err, _payload, context) {
1572
- if (context?.previousSessions) {
1573
- queryClient.setQueryData(
1574
- chatQueryKeys.sessions,
1575
- context.previousSessions
1576
- );
1577
- }
1578
- clearSessionDeleted(_payload.sessionKey || _payload.friendlyId);
1579
- setError(err instanceof Error ? err.message : String(err));
1580
- },
1581
- onSuccess: function onSuccess(payload) {
1582
- if (payload.isActive) {
1583
- resetPendingSend();
1584
- }
1585
- queryClient.invalidateQueries({ queryKey: chatQueryKeys.sessions });
1586
- },
1587
- onSettled: function onSettled() {
1588
- setDeleting(false);
1589
- }
1590
- });
1591
- const deleteSession = useCallback(
1592
- async (sessionKey, friendlyId, isActive) => {
1593
- if (!sessionKey && !friendlyId) return;
1594
- setDeleting(true);
1595
- await mutation.mutateAsync({ sessionKey, friendlyId, isActive });
1596
- },
1597
- [mutation]
1598
- );
1599
- return { deleteSession, deleting, error };
1600
- }
1601
- function useRenameSession() {
1602
- const queryClient = useQueryClient();
1603
- const [renaming, setRenaming] = useState(false);
1604
- const [error, setError] = useState(null);
1605
- const mutation = useMutation({
1606
- mutationFn: async function renameSessionRequest(payload) {
1607
- const res = await fetch("/api/sessions", {
1608
- method: "PATCH",
1609
- headers: { "content-type": "application/json" },
1610
- body: JSON.stringify({
1611
- sessionKey: payload.sessionKey,
1612
- label: payload.newTitle
1613
- })
1614
- });
1615
- if (!res.ok) throw new Error(await readError(res));
1616
- return payload;
1617
- },
1618
- onMutate: async function onMutate(payload) {
1619
- setError(null);
1620
- await queryClient.cancelQueries({ queryKey: chatQueryKeys.sessions });
1621
- const previousSessions = queryClient.getQueryData(chatQueryKeys.sessions);
1622
- queryClient.setQueryData(
1623
- chatQueryKeys.sessions,
1624
- function update(sessions) {
1625
- if (!Array.isArray(sessions)) return sessions;
1626
- return sessions.map((session) => {
1627
- if (session.key !== payload.sessionKey) return session;
1628
- return {
1629
- ...session,
1630
- label: payload.newTitle,
1631
- title: payload.newTitle
1632
- };
1633
- });
1634
- }
1635
- );
1636
- return { previousSessions };
1637
- },
1638
- onError: function onError(err, _payload, context) {
1639
- if (context?.previousSessions) {
1640
- queryClient.setQueryData(
1641
- chatQueryKeys.sessions,
1642
- context.previousSessions
1643
- );
1644
- }
1645
- setError(err instanceof Error ? err.message : String(err));
1646
- },
1647
- onSuccess: function onSuccess() {
1648
- queryClient.invalidateQueries({ queryKey: chatQueryKeys.sessions });
1649
- },
1650
- onSettled: function onSettled() {
1651
- setRenaming(false);
1575
+ isPinned: false,
1576
+ selectionMode,
1577
+ selected: selectedSessionKeys.has(session.key),
1578
+ onToggleSelect: handleToggleSelect,
1579
+ onSelect,
1580
+ onTogglePin: handleTogglePin,
1581
+ onRename,
1582
+ onDelete,
1583
+ onExport
1584
+ },
1585
+ session.key
1586
+ )) })
1587
+ ] }) : null,
1588
+ renderFolderGroup(
1589
+ "webchat",
1590
+ "🦎 OpenCami",
1591
+ groupedSessions.webchat
1592
+ ),
1593
+ renderFolderGroup(
1594
+ "subagent",
1595
+ "🤖 Sub-agents",
1596
+ groupedSessions.subagent
1597
+ ),
1598
+ renderFolderGroup(
1599
+ "cron",
1600
+ "⏰ Cron / Isolated",
1601
+ groupedSessions.cron
1602
+ ),
1603
+ renderFolderGroup("other", "📁 Other", groupedSessions.other)
1604
+ ] }) }),
1605
+ /* @__PURE__ */ jsx(ScrollAreaScrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollAreaThumb, {}) })
1606
+ ] })
1607
+ }
1608
+ ),
1609
+ selectionMode ? /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-t border-primary-200 bg-surface px-2 py-2 flex flex-col gap-1.5", children: [
1610
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
1611
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-primary-600 font-medium", children: [
1612
+ selectedCount,
1613
+ " selected"
1614
+ ] }),
1615
+ /* @__PURE__ */ jsx(
1616
+ "button",
1617
+ {
1618
+ type: "button",
1619
+ onClick: handleSelectAll,
1620
+ className: "text-[11px] font-medium text-primary-600 hover:text-primary-800",
1621
+ children: "Select all"
1622
+ }
1623
+ )
1624
+ ] }),
1625
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
1626
+ /* @__PURE__ */ jsxs(
1627
+ "button",
1628
+ {
1629
+ type: "button",
1630
+ onClick: handleDeleteSelected,
1631
+ disabled: selectedCount === 0,
1632
+ className: cn(
1633
+ "flex-1 inline-flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-colors",
1634
+ selectedCount > 0 ? "bg-red-100 text-red-700 hover:bg-red-200" : "bg-primary-100 text-primary-400 cursor-not-allowed"
1635
+ ),
1636
+ children: [
1637
+ /* @__PURE__ */ jsx(HugeiconsIcon, { icon: Delete01Icon, size: 14, strokeWidth: 1.5 }),
1638
+ "Delete"
1639
+ ]
1640
+ }
1641
+ ),
1642
+ /* @__PURE__ */ jsx(
1643
+ "button",
1644
+ {
1645
+ type: "button",
1646
+ onClick: handleCancelSelection,
1647
+ className: "flex-1 inline-flex items-center justify-center rounded-md px-2 py-1.5 text-xs font-medium bg-primary-100 text-primary-700 hover:bg-primary-200 transition-colors",
1648
+ children: "Cancel"
1649
+ }
1650
+ )
1651
+ ] })
1652
+ ] }) : null
1653
+ ]
1652
1654
  }
1653
- });
1654
- const renameSession = useCallback(
1655
- async (sessionKey, newTitle) => {
1656
- if (!sessionKey || !newTitle.trim()) return;
1657
- setRenaming(true);
1658
- await mutation.mutateAsync({ sessionKey, newTitle: newTitle.trim() });
1659
- },
1660
- [mutation]
1661
1655
  );
1662
- return { renameSession, renaming, error };
1656
+ }, areSidebarSessionsEqual);
1657
+ function areSidebarSessionsEqual(prev, next) {
1658
+ if (prev.activeFriendlyId !== next.activeFriendlyId) return false;
1659
+ if (prev.activeSessionKey !== next.activeSessionKey) return false;
1660
+ if (prev.isStreaming !== next.isStreaming) return false;
1661
+ if (prev.defaultOpen !== next.defaultOpen) return false;
1662
+ if (prev.onSelect !== next.onSelect) return false;
1663
+ if (prev.onRename !== next.onRename) return false;
1664
+ if (prev.onDelete !== next.onDelete) return false;
1665
+ if (prev.onExport !== next.onExport) return false;
1666
+ if (prev.sessions === next.sessions) return true;
1667
+ if (prev.sessions.length !== next.sessions.length) return false;
1668
+ for (let i = 0; i < prev.sessions.length; i += 1) {
1669
+ const prevSession = prev.sessions[i];
1670
+ const nextSession = next.sessions[i];
1671
+ if (prevSession.key !== nextSession.key) return false;
1672
+ if (prevSession.friendlyId !== nextSession.friendlyId) return false;
1673
+ if (prevSession.label !== nextSession.label) return false;
1674
+ if (prevSession.title !== nextSession.title) return false;
1675
+ if (prevSession.derivedTitle !== nextSession.derivedTitle) return false;
1676
+ if (prevSession.updatedAt !== nextSession.updatedAt) return false;
1677
+ if (prevSession.kind !== nextSession.kind) return false;
1678
+ if (prevSession.status !== nextSession.status) return false;
1679
+ if (prevSession.lastMessage !== nextSession.lastMessage) return false;
1680
+ }
1681
+ return true;
1663
1682
  }
1664
1683
  const SettingsDialog = lazy(
1665
- () => import("./settings-dialog-S2HICL7l.js").then((m) => ({ default: m.SettingsDialog }))
1684
+ () => import("./settings-dialog-BUOrQN3Z.js").then((m) => ({ default: m.SettingsDialog }))
1666
1685
  );
1667
1686
  const SessionExportDialog = lazy(
1668
1687
  () => import("./session-export-dialog-C53RRAah.js").then((m) => ({
@@ -2081,6 +2100,48 @@ function ChatSidebarComponent({
2081
2100
  }
2082
2101
  ) }),
2083
2102
  isCollapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Cron Jobs" })
2103
+ ] }) }),
2104
+ (() => {
2105
+ try {
2106
+ return localStorage.getItem("feature_dashboard") === "true";
2107
+ } catch {
2108
+ return false;
2109
+ }
2110
+ })() && /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(TooltipRoot, { children: [
2111
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
2112
+ Link,
2113
+ {
2114
+ to: "/dashboard",
2115
+ className: cn(
2116
+ buttonVariants({ variant: "ghost", size: "sm" }),
2117
+ "w-full pl-1.5 justify-start"
2118
+ ),
2119
+ onClick: onSelectSession,
2120
+ children: [
2121
+ /* @__PURE__ */ jsx(
2122
+ HugeiconsIcon,
2123
+ {
2124
+ icon: DashboardCircleIcon,
2125
+ size: 20,
2126
+ strokeWidth: 1.5,
2127
+ className: "min-w-5"
2128
+ }
2129
+ ),
2130
+ /* @__PURE__ */ jsx(AnimatePresence, { initial: false, mode: "wait", children: !isCollapsed && /* @__PURE__ */ jsx(
2131
+ motion.span,
2132
+ {
2133
+ initial: { opacity: 0 },
2134
+ animate: { opacity: 1 },
2135
+ exit: { opacity: 0 },
2136
+ transition,
2137
+ className: "overflow-hidden whitespace-nowrap",
2138
+ children: "Dashboard"
2139
+ }
2140
+ ) })
2141
+ ]
2142
+ }
2143
+ ) }),
2144
+ isCollapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Dashboard" })
2084
2145
  ] }) })
2085
2146
  ]
2086
2147
  }
@@ -2998,6 +3059,73 @@ function imagesFromMessage(msg) {
2998
3059
  }
2999
3060
  return images;
3000
3061
  }
3062
+ const UPLOADED_FILE_LINE_REGEX = /^📎 Uploaded file:\s*(\/uploads\/\S+)\s*$/;
3063
+ function formatFileSize$1(bytes) {
3064
+ if (bytes === null || Number.isNaN(bytes)) return "Unknown size";
3065
+ if (bytes < 1024) return `${bytes} B`;
3066
+ const units = ["KB", "MB", "GB", "TB"];
3067
+ let value = bytes / 1024;
3068
+ let unitIndex = 0;
3069
+ while (value >= 1024 && unitIndex < units.length - 1) {
3070
+ value /= 1024;
3071
+ unitIndex++;
3072
+ }
3073
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
3074
+ }
3075
+ function getLeadingUploadedFileLines(text) {
3076
+ if (!text) return [];
3077
+ const trimmedStart = text.trimStart();
3078
+ if (trimmedStart.startsWith(">") || trimmedStart.startsWith("```")) {
3079
+ return [];
3080
+ }
3081
+ const lines = text.split("\n");
3082
+ const uploadedLines = [];
3083
+ for (const line of lines) {
3084
+ if (UPLOADED_FILE_LINE_REGEX.test(line)) {
3085
+ uploadedLines.push(line);
3086
+ continue;
3087
+ }
3088
+ if (uploadedLines.length > 0 && line.trim() === "") {
3089
+ uploadedLines.push(line);
3090
+ continue;
3091
+ }
3092
+ break;
3093
+ }
3094
+ const firstLine = lines[0] ?? "";
3095
+ if (!UPLOADED_FILE_LINE_REGEX.test(firstLine)) {
3096
+ return [];
3097
+ }
3098
+ return uploadedLines.filter((line) => UPLOADED_FILE_LINE_REGEX.test(line));
3099
+ }
3100
+ function parseUploadedFileReferences(text) {
3101
+ const refs = [];
3102
+ const seen = /* @__PURE__ */ new Set();
3103
+ for (const line of getLeadingUploadedFileLines(text)) {
3104
+ const match = line.match(UPLOADED_FILE_LINE_REGEX);
3105
+ const filePath = match?.[1];
3106
+ if (!filePath || seen.has(filePath)) continue;
3107
+ seen.add(filePath);
3108
+ refs.push({
3109
+ path: filePath,
3110
+ filename: filePath.split("/").pop() || filePath
3111
+ });
3112
+ }
3113
+ return refs;
3114
+ }
3115
+ function stripUploadedFileLines(text) {
3116
+ const lines = text.split("\n");
3117
+ if (!UPLOADED_FILE_LINE_REGEX.test(lines[0] ?? "")) {
3118
+ return text.trim();
3119
+ }
3120
+ let index = 0;
3121
+ while (index < lines.length && UPLOADED_FILE_LINE_REGEX.test(lines[index])) {
3122
+ index++;
3123
+ }
3124
+ while (index < lines.length && lines[index].trim() === "") {
3125
+ index++;
3126
+ }
3127
+ return lines.slice(index).join("\n").replace(/\n{3,}/g, "\n\n").trim();
3128
+ }
3001
3129
  function MessageItemComponent({
3002
3130
  message,
3003
3131
  toolResultsByCallId,
@@ -3018,6 +3146,47 @@ function MessageItemComponent({
3018
3146
  const images = imagesFromMessage(message);
3019
3147
  const isUser = role === "user";
3020
3148
  const timestamp = getMessageTimestamp(message);
3149
+ const navigate = useNavigate();
3150
+ const openInEditor = useFileExplorerState((state) => state.openInEditor);
3151
+ const uploadedFileRefs = useMemo(() => parseUploadedFileReferences(text), [text]);
3152
+ const [fileSizes, setFileSizes] = useState({});
3153
+ const displayText = useMemo(() => stripUploadedFileLines(text), [text]);
3154
+ useEffect(() => {
3155
+ let cancelled = false;
3156
+ async function loadSizes() {
3157
+ const nextSizes = {};
3158
+ await Promise.all(
3159
+ uploadedFileRefs.map(async (ref) => {
3160
+ try {
3161
+ const response = await fetch(`/api/files/info?path=${encodeURIComponent(ref.path)}`);
3162
+ if (!response.ok) {
3163
+ nextSizes[ref.path] = null;
3164
+ return;
3165
+ }
3166
+ const data = await response.json();
3167
+ nextSizes[ref.path] = typeof data.size === "number" ? data.size : null;
3168
+ } catch {
3169
+ nextSizes[ref.path] = null;
3170
+ }
3171
+ })
3172
+ );
3173
+ if (!cancelled) {
3174
+ setFileSizes(nextSizes);
3175
+ }
3176
+ }
3177
+ if (uploadedFileRefs.length > 0) {
3178
+ void loadSizes();
3179
+ } else {
3180
+ setFileSizes({});
3181
+ }
3182
+ return () => {
3183
+ cancelled = true;
3184
+ };
3185
+ }, [uploadedFileRefs]);
3186
+ const handleOpenFile = async (filePath) => {
3187
+ openInEditor(filePath);
3188
+ await navigate({ to: "/files" });
3189
+ };
3021
3190
  const toolCalls = role === "assistant" ? getToolCallsFromMessage(message) : [];
3022
3191
  const hasToolCalls = toolCalls.length > 0;
3023
3192
  const searchSources = isLastAssistant && !isStreaming && settings.showSearchSources && aggregatedSearchSources ? aggregatedSearchSources : [];
@@ -3054,6 +3223,24 @@ function MessageItemComponent({
3054
3223
  },
3055
3224
  idx
3056
3225
  )) }),
3226
+ uploadedFileRefs.length > 0 && /* @__PURE__ */ jsx("div", { className: cn("mb-2 flex w-full flex-col gap-2", isUser ? "items-end" : "items-start"), children: uploadedFileRefs.map((fileRef) => /* @__PURE__ */ jsxs(
3227
+ "button",
3228
+ {
3229
+ type: "button",
3230
+ onClick: () => {
3231
+ void handleOpenFile(fileRef.path);
3232
+ },
3233
+ className: "flex max-w-full items-center gap-3 rounded-xl border border-primary-200 bg-primary-50 px-3 py-2 text-left hover:bg-primary-100",
3234
+ children: [
3235
+ /* @__PURE__ */ jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-primary-100", children: /* @__PURE__ */ jsx(HugeiconsIcon, { icon: File01Icon, size: 18, className: "text-primary-600" }) }),
3236
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
3237
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-primary-900", children: fileRef.filename }),
3238
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-primary-600", children: formatFileSize$1(fileSizes[fileRef.path] ?? null) })
3239
+ ] })
3240
+ ]
3241
+ },
3242
+ fileRef.path
3243
+ )) }),
3057
3244
  /* @__PURE__ */ jsx(Message, { className: cn("min-w-0 max-w-full", isUser ? "flex-row-reverse" : ""), children: /* @__PURE__ */ jsx(
3058
3245
  MessageContent,
3059
3246
  {
@@ -3063,7 +3250,7 @@ function MessageItemComponent({
3063
3250
  isUser ? "opencami-message-user bg-primary-100 px-4 py-[var(--opencami-user-bubble-py)] max-w-[85%]" : "opencami-message-assistant bg-transparent w-full",
3064
3251
  !isUser && isStreaming && "stream-fade-in"
3065
3252
  ),
3066
- children: text
3253
+ children: displayText
3067
3254
  }
3068
3255
  ) }),
3069
3256
  hasToolCalls && settings.showToolMessages && /* @__PURE__ */ jsx("div", { className: "mt-2 flex w-full min-w-0 max-w-[var(--opencami-chat-width)] flex-col gap-3 overflow-x-hidden", children: toolCalls.map((toolCall) => {
@@ -3252,6 +3439,10 @@ const LLM_PROVIDER_DEFAULTS = {
3252
3439
  baseUrl: "https://openrouter.ai/api/v1",
3253
3440
  model: "openai/gpt-oss-120b"
3254
3441
  },
3442
+ kilocode: {
3443
+ baseUrl: "https://api.kilo.ai/api/gateway",
3444
+ model: "google/gemini-2.5-flash"
3445
+ },
3255
3446
  ollama: {
3256
3447
  baseUrl: "http://localhost:11434/v1",
3257
3448
  model: "llama3.2"
@@ -3283,7 +3474,7 @@ function getEffectiveLlmModel(settings) {
3283
3474
  function getAvailability(settings, hasEnvKey) {
3284
3475
  if (settings.llmProvider === "ollama") return true;
3285
3476
  if (settings.llmProvider === "custom") {
3286
- return Boolean(settings.llmApiKey.trim()) || settings.llmBaseUrl.trim() && settings.llmModel.trim();
3477
+ return Boolean(settings.llmApiKey.trim()) || Boolean(settings.llmBaseUrl.trim() && settings.llmModel.trim());
3287
3478
  }
3288
3479
  return hasEnvKey || Boolean(settings.llmApiKey.trim());
3289
3480
  }
@@ -3332,6 +3523,7 @@ function useLlmSettings() {
3332
3523
  const [status, setStatus] = useState({
3333
3524
  hasEnvKey: false,
3334
3525
  hasOpenRouterKey: false,
3526
+ hasKilocodeKey: false,
3335
3527
  hasUserKey: Boolean(settings.llmApiKey),
3336
3528
  isAvailable: getAvailability(settings, false),
3337
3529
  isLoading: true,
@@ -3346,10 +3538,11 @@ function useLlmSettings() {
3346
3538
  const data = await res.json();
3347
3539
  if (cancelled) return;
3348
3540
  const hasUserKey = Boolean(settings.llmApiKey);
3349
- const hasProviderKey = settings.llmProvider === "openrouter" ? Boolean(data.hasOpenRouterKey) : data.hasEnvKey;
3541
+ const hasProviderKey = settings.llmProvider === "openrouter" ? Boolean(data.hasOpenRouterKey) : settings.llmProvider === "kilocode" ? Boolean(data.hasKilocodeKey) : data.hasEnvKey;
3350
3542
  setStatus({
3351
3543
  hasEnvKey: data.hasEnvKey,
3352
3544
  hasOpenRouterKey: Boolean(data.hasOpenRouterKey),
3545
+ hasKilocodeKey: Boolean(data.hasKilocodeKey),
3353
3546
  hasUserKey,
3354
3547
  isAvailable: getAvailability(settings, hasProviderKey),
3355
3548
  isLoading: false,
@@ -3946,13 +4139,14 @@ function ChatMessageListComponent({
3946
4139
  return sources;
3947
4140
  }, [displayMessages, toolResultsByCallId]);
3948
4141
  const lastAssistantIndex = displayMessages.map((message, index) => ({ message, index })).filter(({ message }) => message.role !== "user").map(({ index }) => index).pop();
4142
+ const lastTextAssistantIndex = displayMessages.map((message, index) => ({ message, index })).filter(({ message }) => message.role === "assistant").map(({ index }) => index).pop();
3949
4143
  const lastUserIndex = displayMessages.map((message, index) => ({ message, index })).filter(({ message }) => message.role === "user").map(({ index }) => index).pop();
3950
4144
  const showTypingIndicator = waitingForResponse && (typeof lastUserIndex !== "number" || typeof lastAssistantIndex !== "number" || lastAssistantIndex < lastUserIndex);
3951
4145
  const groupStartIndex = typeof lastUserIndex === "number" ? lastUserIndex : -1;
3952
4146
  const hasGroup = pinToTop && groupStartIndex >= 0;
3953
- const lastAssistantMessage = typeof lastAssistantIndex === "number" ? displayMessages[lastAssistantIndex] : void 0;
4147
+ const lastAssistantMessage = typeof lastTextAssistantIndex === "number" ? displayMessages[lastTextAssistantIndex] : void 0;
3954
4148
  const lastAssistantText = lastAssistantMessage ? textFromMessage(lastAssistantMessage) : "";
3955
- const showFollowUps = !waitingForResponse && !isStreaming && lastAssistantText.length > 0 && onFollowUpClick !== void 0 && (typeof lastUserIndex !== "number" || typeof lastAssistantIndex !== "number" || lastAssistantIndex > lastUserIndex);
4149
+ const showFollowUps = !waitingForResponse && !isStreaming && lastAssistantText.length > 0 && onFollowUpClick !== void 0 && (typeof lastUserIndex !== "number" || typeof lastTextAssistantIndex !== "number" || lastTextAssistantIndex > lastUserIndex);
3956
4150
  useLayoutEffect(() => {
3957
4151
  if (loading) return;
3958
4152
  if (pinToTop) {
@@ -4690,7 +4884,30 @@ const MAX_IMAGE_DIMENSION = 1280;
4690
4884
  const IMAGE_QUALITY = 0.75;
4691
4885
  const TARGET_IMAGE_SIZE = 300 * 1024;
4692
4886
  const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
4693
- const ACCEPTED_EXTENSIONS = ".png,.jpg,.jpeg,.gif,.webp";
4887
+ const ACCEPTED_NON_IMAGE_EXTENSIONS = [
4888
+ "pdf",
4889
+ "txt",
4890
+ "md",
4891
+ "csv",
4892
+ "json",
4893
+ "xml",
4894
+ "yaml",
4895
+ "yml",
4896
+ "log",
4897
+ "py",
4898
+ "js",
4899
+ "ts",
4900
+ "html",
4901
+ "css"
4902
+ ];
4903
+ const ACCEPTED_EXTENSIONS = [
4904
+ ".png",
4905
+ ".jpg",
4906
+ ".jpeg",
4907
+ ".gif",
4908
+ ".webp",
4909
+ ...ACCEPTED_NON_IMAGE_EXTENSIONS.map((ext) => `.${ext}`)
4910
+ ].join(",");
4694
4911
  function isCanvasSupported() {
4695
4912
  if (typeof document === "undefined") return false;
4696
4913
  try {
@@ -4766,6 +4983,10 @@ async function compressImage(file) {
4766
4983
  function isAcceptedImage(file) {
4767
4984
  return ACCEPTED_IMAGE_TYPES.includes(file.type);
4768
4985
  }
4986
+ function isAcceptedNonImage(file) {
4987
+ const extension = file.name.includes(".") ? file.name.split(".").pop()?.toLowerCase() : "";
4988
+ return Boolean(extension && ACCEPTED_NON_IMAGE_EXTENSIONS.includes(extension));
4989
+ }
4769
4990
  async function createAttachmentFromFile(file) {
4770
4991
  const id = crypto.randomUUID();
4771
4992
  if (!isAcceptedImage(file)) {
@@ -4773,9 +4994,9 @@ async function createAttachmentFromFile(file) {
4773
4994
  id,
4774
4995
  file,
4775
4996
  preview: null,
4776
- type: "image",
4997
+ type: "file",
4777
4998
  base64: null,
4778
- error: "Unsupported file type. Please use PNG, JPG, GIF, or WebP images."
4999
+ error: "Unsupported image type. Please use PNG, JPG, GIF, or WebP images."
4779
5000
  };
4780
5001
  }
4781
5002
  if (file.size > MAX_FILE_SIZE) {
@@ -4810,7 +5031,7 @@ async function createAttachmentFromFile(file) {
4810
5031
  }
4811
5032
  }
4812
5033
  function AttachmentButton({
4813
- onFileSelect,
5034
+ onFilesSelect,
4814
5035
  disabled = false,
4815
5036
  className
4816
5037
  }) {
@@ -4820,13 +5041,12 @@ function AttachmentButton({
4820
5041
  }, []);
4821
5042
  const handleFileChange = useCallback(
4822
5043
  async (event) => {
4823
- const file = event.target.files?.[0];
4824
- if (!file) return;
5044
+ const files = Array.from(event.target.files ?? []);
5045
+ if (files.length === 0) return;
4825
5046
  event.target.value = "";
4826
- const attachment = await createAttachmentFromFile(file);
4827
- onFileSelect(attachment);
5047
+ onFilesSelect(files);
4828
5048
  },
4829
- [onFileSelect]
5049
+ [onFilesSelect]
4830
5050
  );
4831
5051
  return /* @__PURE__ */ jsxs(Fragment, { children: [
4832
5052
  /* @__PURE__ */ jsx(
@@ -4835,6 +5055,7 @@ function AttachmentButton({
4835
5055
  ref: inputRef,
4836
5056
  type: "file",
4837
5057
  accept: ACCEPTED_EXTENSIONS,
5058
+ multiple: true,
4838
5059
  onChange: handleFileChange,
4839
5060
  className: "hidden",
4840
5061
  "aria-hidden": "true"
@@ -4848,7 +5069,7 @@ function AttachmentButton({
4848
5069
  onClick: handleClick,
4849
5070
  disabled,
4850
5071
  className,
4851
- "aria-label": "Attach image",
5072
+ "aria-label": "Attach file",
4852
5073
  type: "button",
4853
5074
  children: /* @__PURE__ */ jsx(HugeiconsIcon, { icon: Attachment01Icon, size: 18, strokeWidth: 1.8 })
4854
5075
  }
@@ -4877,6 +5098,36 @@ function AttachmentPreview({
4877
5098
  };
4878
5099
  }, [attachment.preview]);
4879
5100
  const hasError = Boolean(attachment.error);
5101
+ if (attachment.type === "file" && !hasError) {
5102
+ return /* @__PURE__ */ jsxs(
5103
+ "div",
5104
+ {
5105
+ className: cn(
5106
+ "inline-flex max-w-full items-center gap-2 rounded-full border border-primary-200 bg-primary-50 px-3 py-1.5 text-xs text-primary-800",
5107
+ className
5108
+ ),
5109
+ children: [
5110
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
5111
+ "📄 ",
5112
+ attachment.file.name,
5113
+ " (",
5114
+ formatFileSize(attachment.file.size),
5115
+ ")"
5116
+ ] }),
5117
+ /* @__PURE__ */ jsx(
5118
+ "button",
5119
+ {
5120
+ onClick: () => onRemove(attachment.id),
5121
+ className: "shrink-0 text-primary-500 hover:text-primary-800",
5122
+ "aria-label": "Remove attachment",
5123
+ type: "button",
5124
+ children: "✕"
5125
+ }
5126
+ )
5127
+ ]
5128
+ }
5129
+ );
5130
+ }
4880
5131
  return /* @__PURE__ */ jsxs(
4881
5132
  "div",
4882
5133
  {
@@ -4932,7 +5183,7 @@ function AttachmentPreviewList({
4932
5183
  className
4933
5184
  }) {
4934
5185
  if (attachments.length === 0) return null;
4935
- return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col gap-2 px-4", className), children: attachments.map((attachment) => /* @__PURE__ */ jsx(
5186
+ return /* @__PURE__ */ jsx("div", { className: cn("flex flex-wrap gap-2 px-4", className), children: attachments.map((attachment) => /* @__PURE__ */ jsx(
4936
5187
  AttachmentPreview,
4937
5188
  {
4938
5189
  attachment,
@@ -5162,9 +5413,82 @@ function ChatComposerComponent({
5162
5413
  setSlashMenuDismissed(false);
5163
5414
  focusPrompt();
5164
5415
  }, [focusPrompt]);
5165
- const handleFileSelect = useCallback((file) => {
5166
- setAttachments((prev) => [...prev, file]);
5416
+ const appendUploadedFilePrompt = useCallback((uploadedPath) => {
5417
+ setValue((prev) => {
5418
+ const template = `📎 Uploaded file: ${uploadedPath}
5419
+
5420
+ Please analyze this file.`;
5421
+ if (!prev.trim()) return template;
5422
+ return `${prev.trim()}
5423
+
5424
+ ${template}`;
5425
+ });
5167
5426
  }, []);
5427
+ const ensureUploadsDirectory = useCallback(async () => {
5428
+ await fetch("/api/files/mkdir", {
5429
+ method: "POST",
5430
+ headers: { "Content-Type": "application/json" },
5431
+ body: JSON.stringify({ path: "/uploads" })
5432
+ });
5433
+ }, []);
5434
+ const uploadAttachmentFile = useCallback(async (file) => {
5435
+ const id = crypto.randomUUID();
5436
+ if (!isAcceptedNonImage(file)) {
5437
+ return {
5438
+ id,
5439
+ file,
5440
+ preview: null,
5441
+ type: "file",
5442
+ base64: null,
5443
+ error: "Unsupported file type for upload."
5444
+ };
5445
+ }
5446
+ try {
5447
+ await ensureUploadsDirectory();
5448
+ const formData = new FormData();
5449
+ formData.append("path", "/uploads");
5450
+ formData.append("file", file);
5451
+ const response = await fetch("/api/files/upload", {
5452
+ method: "POST",
5453
+ body: formData
5454
+ });
5455
+ if (!response.ok) {
5456
+ const message = await response.text();
5457
+ throw new Error(message || "File upload failed");
5458
+ }
5459
+ const data = await response.json();
5460
+ const uploadedPath = data.files?.[0]?.path;
5461
+ if (!uploadedPath) {
5462
+ throw new Error("Upload succeeded but no path was returned");
5463
+ }
5464
+ appendUploadedFilePrompt(uploadedPath);
5465
+ return {
5466
+ id,
5467
+ file,
5468
+ preview: null,
5469
+ type: "file",
5470
+ base64: null,
5471
+ uploadedPath
5472
+ };
5473
+ } catch (err) {
5474
+ return {
5475
+ id,
5476
+ file,
5477
+ preview: null,
5478
+ type: "file",
5479
+ base64: null,
5480
+ error: err instanceof Error ? err.message : "File upload failed"
5481
+ };
5482
+ }
5483
+ }, [appendUploadedFilePrompt, ensureUploadsDirectory]);
5484
+ const handleFilesSelect = useCallback(async (files) => {
5485
+ if (files.length === 0) return;
5486
+ const nextAttachments = await Promise.all(
5487
+ files.map((file) => isAcceptedImage(file) ? createAttachmentFromFile(file) : uploadAttachmentFile(file))
5488
+ );
5489
+ setAttachments((prev) => [...prev, ...nextAttachments]);
5490
+ focusPrompt();
5491
+ }, [focusPrompt, uploadAttachmentFile]);
5168
5492
  const handleRemoveAttachment = useCallback((id) => {
5169
5493
  setAttachments((prev) => prev.filter((a) => a.id !== id));
5170
5494
  }, []);
@@ -5186,12 +5510,8 @@ function ChatComposerComponent({
5186
5510
  setIsDragActive(false);
5187
5511
  const files = Array.from(event.dataTransfer.files ?? []);
5188
5512
  if (files.length === 0) return;
5189
- const imageFiles = files.filter((file) => isAcceptedImage(file));
5190
- if (imageFiles.length === 0) return;
5191
- const newAttachments = await Promise.all(imageFiles.map((file) => createAttachmentFromFile(file)));
5192
- setAttachments((prev) => [...prev, ...newAttachments]);
5193
- focusPrompt();
5194
- }, [focusPrompt]);
5513
+ await handleFilesSelect(files);
5514
+ }, [handleFilesSelect]);
5195
5515
  const setComposerValue = useCallback(
5196
5516
  (nextValue) => {
5197
5517
  setValue(nextValue);
@@ -5443,7 +5763,7 @@ function ChatComposerComponent({
5443
5763
  onDragLeave: handleDragLeave,
5444
5764
  onDrop: handleDrop,
5445
5765
  children: [
5446
- isDragActive && /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-2 z-20 flex items-center justify-center rounded-2xl border-2 border-dashed border-primary-400 bg-primary-50/90 text-sm font-medium text-primary-700", children: "Drop image here" }),
5766
+ isDragActive && /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-2 z-20 flex items-center justify-center rounded-2xl border-2 border-dashed border-primary-400 bg-primary-50/90 text-sm font-medium text-primary-700", children: "Drop files here" }),
5447
5767
  /* @__PURE__ */ jsxs(
5448
5768
  PromptInput,
5449
5769
  {
@@ -5487,7 +5807,7 @@ function ChatComposerComponent({
5487
5807
  /* @__PURE__ */ jsx(
5488
5808
  AttachmentButton,
5489
5809
  {
5490
- onFileSelect: handleFileSelect,
5810
+ onFilesSelect: handleFilesSelect,
5491
5811
  disabled
5492
5812
  }
5493
5813
  ),
@@ -5900,13 +6220,56 @@ const INITIAL_STATE = {
5900
6220
  function useStreaming(options) {
5901
6221
  const [state, setState] = useState(INITIAL_STATE);
5902
6222
  const eventSourceRef = useRef(null);
6223
+ const pollingRef = useRef(null);
6224
+ const pollingTimeoutRef = useRef(null);
6225
+ const streamStartRef = useRef(null);
6226
+ const doneRef = useRef(false);
5903
6227
  const onDoneRef = useRef(options.onDone);
5904
6228
  const onErrorRef = useRef(options.onError);
5905
6229
  const onAssistantDeltaRef = useRef(options.onAssistantDelta);
5906
6230
  onDoneRef.current = options.onDone;
5907
6231
  onErrorRef.current = options.onError;
5908
6232
  onAssistantDeltaRef.current = options.onAssistantDelta;
6233
+ function clearPolling() {
6234
+ if (pollingTimeoutRef.current) {
6235
+ window.clearTimeout(pollingTimeoutRef.current);
6236
+ pollingTimeoutRef.current = null;
6237
+ }
6238
+ if (pollingRef.current) {
6239
+ window.clearInterval(pollingRef.current);
6240
+ pollingRef.current = null;
6241
+ }
6242
+ }
6243
+ function startPolling(sessionKey, startedAt) {
6244
+ if (pollingRef.current) return;
6245
+ pollingRef.current = window.setInterval(async () => {
6246
+ try {
6247
+ const res = await fetch(
6248
+ `/api/history?sessionKey=${encodeURIComponent(sessionKey)}`
6249
+ );
6250
+ if (!res.ok) return;
6251
+ const data = await res.json();
6252
+ const messages = Array.isArray(data.messages) ? data.messages : [];
6253
+ const hasNewAssistant = messages.some((message) => {
6254
+ if (!message || message.role !== "assistant") return false;
6255
+ if (typeof message.timestamp !== "number") return false;
6256
+ return message.timestamp > startedAt - 3e3;
6257
+ });
6258
+ if (!hasNewAssistant) return;
6259
+ if (eventSourceRef.current) {
6260
+ eventSourceRef.current.close();
6261
+ eventSourceRef.current = null;
6262
+ }
6263
+ clearPolling();
6264
+ setState((prev) => ({ ...prev, active: false }));
6265
+ onDoneRef.current(sessionKey);
6266
+ } catch {
6267
+ }
6268
+ }, 2e3);
6269
+ }
5909
6270
  const stop = useCallback((options2) => {
6271
+ doneRef.current = true;
6272
+ clearPolling();
5910
6273
  if (eventSourceRef.current) {
5911
6274
  eventSourceRef.current.close();
5912
6275
  eventSourceRef.current = null;
@@ -5918,7 +6281,10 @@ function useStreaming(options) {
5918
6281
  setState(INITIAL_STATE);
5919
6282
  }, []);
5920
6283
  const start = useCallback(
5921
- (sessionKey) => {
6284
+ function start2(sessionKey) {
6285
+ doneRef.current = false;
6286
+ clearPolling();
6287
+ streamStartRef.current = Date.now();
5922
6288
  if (eventSourceRef.current) {
5923
6289
  eventSourceRef.current.close();
5924
6290
  eventSourceRef.current = null;
@@ -5961,6 +6327,8 @@ function useStreaming(options) {
5961
6327
  es.addEventListener("done", (e) => {
5962
6328
  try {
5963
6329
  const data = JSON.parse(e.data);
6330
+ doneRef.current = true;
6331
+ clearPolling();
5964
6332
  es.close();
5965
6333
  eventSourceRef.current = null;
5966
6334
  setState((prev) => ({ ...prev, active: false }));
@@ -5976,6 +6344,11 @@ function useStreaming(options) {
5976
6344
  onErrorRef.current?.("Stream connection lost");
5977
6345
  }
5978
6346
  };
6347
+ pollingTimeoutRef.current = window.setTimeout(() => {
6348
+ if (doneRef.current) return;
6349
+ const startedAt = streamStartRef.current ?? Date.now();
6350
+ startPolling(sessionKey, startedAt);
6351
+ }, 3e3);
5979
6352
  },
5980
6353
  []
5981
6354
  );
@@ -6223,7 +6596,7 @@ const KeyboardShortcutsDialog = lazy(
6223
6596
  }))
6224
6597
  );
6225
6598
  const SearchDialog = lazy(
6226
- () => import("./search-dialog-CuuZvlyq.js").then((m) => ({
6599
+ () => import("./search-dialog-DReM5ZD2.js").then((m) => ({
6227
6600
  default: m.SearchDialog
6228
6601
  }))
6229
6602
  );
@@ -6507,9 +6880,9 @@ function ChatScreen({
6507
6880
  const hideUi = shouldRedirectToNew || isRedirecting;
6508
6881
  const pollingPhaseRef = useRef("fast");
6509
6882
  useEffect(() => {
6510
- const latestMessage = historyMessages[historyMessages.length - 1];
6511
- if (!latestMessage || latestMessage.role !== "assistant") return;
6512
- const signature = `${historyMessages.length}:${textFromMessage(latestMessage).slice(-64)}`;
6883
+ const lastAssistantMessage = [...historyMessages].reverse().find((message) => message.role === "assistant");
6884
+ if (!lastAssistantMessage) return;
6885
+ const signature = `${historyMessages.length}:${textFromMessage(lastAssistantMessage).slice(-64)}`;
6513
6886
  if (signature !== lastAssistantSignature.current) {
6514
6887
  lastAssistantSignature.current = signature;
6515
6888
  if (pollingPhaseRef.current !== "fast" && streamTimer.current) {
@@ -6689,7 +7062,6 @@ function ChatScreen({
6689
7062
  content: a.base64
6690
7063
  }));
6691
7064
  streamingNotificationTextRef.current = "";
6692
- startStream(sessionKey);
6693
7065
  streamStart();
6694
7066
  fetch("/api/send", {
6695
7067
  method: "POST",
@@ -6705,6 +7077,9 @@ function ChatScreen({
6705
7077
  })
6706
7078
  }).then(async (res) => {
6707
7079
  if (!res.ok) throw new Error(await readError(res));
7080
+ const data = await res.json();
7081
+ const resolvedKey = data.sessionKey || sessionKey || friendlyId;
7082
+ startStream(resolvedKey);
6708
7083
  }).catch((err) => {
6709
7084
  const messageText = err instanceof Error ? err.message : String(err);
6710
7085
  if (isMissingGatewayAuth(messageText)) {