opencami 1.7.0 → 1.8.3

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 (83) hide show
  1. package/README.md +167 -7
  2. package/bin/opencami.js +15 -6
  3. package/dist/client/assets/{CSPContext-a-MQmQQt.js → CSPContext-DeJH85nm.js} +1 -1
  4. package/dist/client/assets/{DirectionContext-DRcND-Cm.js → DirectionContext-CxhRpXkm.js} +1 -1
  5. package/dist/client/assets/_sessionKey-CQE0brGK.js +23 -0
  6. package/dist/client/assets/agents-CMTFd_sG.js +2 -0
  7. package/dist/client/assets/agents-screen-BNQGEqcW.js +1 -0
  8. package/dist/client/assets/bots-B6oGzCxP.js +2 -0
  9. package/dist/client/assets/bots-screen-Be3cfGgq.js +1 -0
  10. package/dist/client/assets/button-D9Plv7hu.js +1 -0
  11. package/dist/client/assets/composite-B2KCZKKL.js +1 -0
  12. package/dist/client/assets/{connect-BX1MWO82.js → connect-DuJfnyNK.js} +1 -1
  13. package/dist/client/assets/dashboard-00GpXm5V.js +1 -0
  14. package/dist/client/assets/event-DD8Cz4O9.js +1 -0
  15. package/dist/client/assets/file-explorer-screen-CxwemBES.js +1 -0
  16. package/dist/client/assets/files-DyBJVXBu.js +2 -0
  17. package/dist/client/assets/{index-BlC2sH55.js → index-DtGzE-ea.js} +1 -1
  18. package/dist/client/assets/{index-Dg0mbvtv.js → index-Yo5UhdZV.js} +1 -1
  19. package/dist/client/assets/keyboard-shortcuts-dialog-BZwd-iyV.js +1 -0
  20. package/dist/client/assets/{main-CEuT8-Qi.js → main-CgwdHc9W.js} +16 -8
  21. package/dist/client/assets/{markdown-DKD6ZLRJ.js → markdown-DtWnt4NA.js} +1 -1
  22. package/dist/client/assets/memory-l756yiNq.js +2 -0
  23. package/dist/client/assets/memory-screen-BQtVRuzE.js +1 -0
  24. package/dist/client/assets/menu-BsS6CDf_.js +1 -0
  25. package/dist/client/assets/{opencami-logo-DA69yVKc.js → opencami-logo-Bmge6-FB.js} +1 -1
  26. package/dist/client/assets/popupStateMapping-D0ZbJR_o.js +1 -0
  27. package/dist/client/assets/{proxy-Cawf6X0W.js → proxy-CYZeDXoy.js} +1 -1
  28. package/dist/client/assets/{react-K9goXsVv.js → react-DODKNyyU.js} +1 -1
  29. package/dist/client/assets/search-dialog-DW91SK30.js +1 -0
  30. package/dist/client/assets/session-export-dialog-CliO9Ob-.js +1 -0
  31. package/dist/client/assets/settings-dialog-C1u52aju.js +1 -0
  32. package/dist/client/assets/skills-8T_avaVb.js +2 -0
  33. package/dist/client/assets/{skills-panel-DFL-3BRH.js → skills-panel-DSiH-DLs.js} +1 -1
  34. package/dist/client/assets/styles-DvaLh0o1.css +1 -0
  35. package/dist/client/assets/switch-DbgQPO6i.js +1 -0
  36. package/dist/client/assets/tabs-BsAvZnlD.js +1 -0
  37. package/dist/client/assets/tooltip-DLmutB5C.js +1 -0
  38. package/dist/client/assets/use-file-explorer-state-Cg_yDYJl.js +12 -0
  39. package/dist/client/assets/useBaseUiId-KQTzRPLp.js +1 -0
  40. package/dist/client/assets/useCompositeItem-BPY2_hF_.js +1 -0
  41. package/dist/client/assets/{useControlled-Cl9XA2_f.js → useControlled-B5pEEz2V.js} +1 -1
  42. package/dist/client/assets/{useMutation-C5bTdeC1.js → useMutation-BsQD6FKe.js} +1 -1
  43. package/dist/client/assets/useQuery-CmAJuY2W.js +1 -0
  44. package/dist/client/assets/visuallyHidden-COI6QeQH.js +1 -0
  45. package/dist/client/sw.js +5 -164
  46. package/dist/server/assets/{_sessionKey-BBG3ZUlo.js → _sessionKey-C9o7YfxA.js} +878 -755
  47. package/dist/server/assets/_tanstack-start-manifest_v-BMCAWon2.js +4 -0
  48. package/dist/server/assets/dashboard-GCKodTiJ.js +214 -0
  49. package/dist/server/assets/{index-BgMPaOsU.js → index-Bw-bA_2M.js} +4 -3
  50. package/dist/server/assets/{router-Bl2uabfY.js → router-DCjikH21.js} +704 -207
  51. package/dist/server/assets/{search-dialog-BtSQW9SR.js → search-dialog-BnwiXpdA.js} +5 -4
  52. package/dist/server/assets/settings-dialog-ClKFnZ1x.js +1511 -0
  53. package/dist/server/server.js +2 -2
  54. package/package.json +1 -1
  55. package/dist/client/assets/_sessionKey-BAmpzUOP.js +0 -23
  56. package/dist/client/assets/agents-BkeWu_3a.js +0 -2
  57. package/dist/client/assets/agents-screen-Cb76bcxn.js +0 -1
  58. package/dist/client/assets/bots-CyJwr-JU.js +0 -2
  59. package/dist/client/assets/bots-screen-CzNjLsQH.js +0 -1
  60. package/dist/client/assets/button-DNC5N25i.js +0 -1
  61. package/dist/client/assets/composite-Bliqcmg4.js +0 -1
  62. package/dist/client/assets/file-explorer-screen-CpY1O_ag.js +0 -1
  63. package/dist/client/assets/files-HiN5rXWq.js +0 -2
  64. package/dist/client/assets/keyboard-shortcuts-dialog-C2Hq19LN.js +0 -1
  65. package/dist/client/assets/memory-lhzf-8Q4.js +0 -2
  66. package/dist/client/assets/memory-screen-Zq9qfnJK.js +0 -1
  67. package/dist/client/assets/menu-47ooFeSm.js +0 -1
  68. package/dist/client/assets/owner-CFRNz_Tp.js +0 -1
  69. package/dist/client/assets/popupStateMapping-D5k-jOeY.js +0 -1
  70. package/dist/client/assets/search-dialog-C5Yae9rb.js +0 -1
  71. package/dist/client/assets/session-export-dialog-CBeTfbll.js +0 -1
  72. package/dist/client/assets/settings-dialog-CoeG9M1b.js +0 -1
  73. package/dist/client/assets/skills-BEkw619A.js +0 -2
  74. package/dist/client/assets/styles-D4EBtWYc.css +0 -1
  75. package/dist/client/assets/switch-DAFvLxNX.js +0 -1
  76. package/dist/client/assets/tabs-B2Y_7MvG.js +0 -1
  77. package/dist/client/assets/tooltip-D57Pal0B.js +0 -1
  78. package/dist/client/assets/use-file-explorer-state-DppKEjcl.js +0 -12
  79. package/dist/client/assets/useButton-DVAfkehQ.js +0 -1
  80. package/dist/client/assets/useCompositeItem-CzdGhGcj.js +0 -1
  81. package/dist/client/assets/visuallyHidden-CO3ZD5AQ.js +0 -1
  82. package/dist/server/assets/_tanstack-start-manifest_v-DmMFarHb.js +0 -4
  83. package/dist/server/assets/settings-dialog-D3fOAswX.js +0 -1173
@@ -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, File01Icon, ArtificialIntelligence02Icon, CommandIcon, Attachment01Icon, 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, u as useFileExplorerState } 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-Bl2uabfY.js";
22
+ import { a as Route } from "./router-DCjikH21.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
- }))
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 {
395
+ }
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
373
414
  };
374
- return JSON.stringify(data, null, 2);
375
415
  }
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("");
390
- }
391
- }
392
- return lines.join("\n");
416
+ let pendingSend = null;
417
+ let pendingGeneration = false;
418
+ let recentSession = null;
419
+ function stashPendingSend(payload) {
420
+ pendingSend = payload;
393
421
  }
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);
422
+ function hasPendingSend() {
423
+ return pendingSend !== null;
419
424
  }
420
- function sanitizeFilename(filename) {
421
- return filename.replace(/[^a-z0-9-_\s]/gi, "").replace(/\s+/g, "-").toLowerCase().slice(0, 50) || "conversation";
425
+ function setPendingGeneration(value) {
426
+ pendingGeneration = value;
422
427
  }
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);
428
+ function hasPendingGeneration() {
429
+ return pendingGeneration;
433
430
  }
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
- ] }) }) });
431
+ function resetPendingSend() {
432
+ pendingSend = null;
433
+ pendingGeneration = false;
474
434
  }
475
- function AlertDialogRoot({ children, ...props }) {
476
- return /* @__PURE__ */ jsx(AlertDialog.Root, { ...props, children });
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
+ }
477
444
  }
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
- ] });
445
+ function setRecentSession(friendlyId) {
446
+ recentSession = { friendlyId, at: Date.now() };
496
447
  }
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
- );
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;
505
453
  }
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
- );
517
- }
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
- }) {
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 }) {
620
748
  return /* @__PURE__ */ jsx(
621
- ScrollArea.Scrollbar,
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)) {
@@ -1196,473 +1488,200 @@ ${failedSessionLabels.join("\n")}`
1196
1488
  selectionMode,
1197
1489
  selected: selectedSessionKeys.has(session.key),
1198
1490
  onToggleSelect: handleToggleSelect,
1199
- onSelect,
1200
- onTogglePin: handleTogglePin,
1201
- onRename,
1202
- onDelete,
1203
- onExport
1204
- },
1205
- session.key
1206
- )) })
1207
- ]
1208
- }
1209
- );
1210
- }
1211
- return /* @__PURE__ */ jsxs(
1212
- Collapsible,
1213
- {
1214
- className: "flex h-full flex-col flex-1 min-h-0 w-full",
1215
- defaultOpen,
1216
- children: [
1217
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pr-2 shrink-0", children: [
1218
- /* @__PURE__ */ jsxs(CollapsibleTrigger, { className: "w-fit pl-1.5 text-balance", children: [
1219
- "Sessions",
1220
- /* @__PURE__ */ jsx("span", { className: "opacity-0 transition-opacity duration-150 group-hover:opacity-100", children: /* @__PURE__ */ jsx(
1221
- HugeiconsIcon,
1222
- {
1223
- icon: ArrowRight01Icon,
1224
- className: "size-3 transition-transform duration-150 group-data-panel-open:rotate-90"
1225
- }
1226
- ) })
1227
- ] }),
1228
- /* @__PURE__ */ jsx(
1229
- "button",
1230
- {
1231
- type: "button",
1232
- onClick: handleSelectionModeToggle,
1233
- className: cn(
1234
- "text-[11px] font-medium px-1.5 py-0.5 rounded-md transition-colors",
1235
- selectionMode ? "text-primary-900 bg-primary-200" : "text-primary-500 hover:text-primary-700 hover:bg-primary-100"
1236
- ),
1237
- children: selectionMode ? "Done" : "Select"
1238
- }
1239
- )
1240
- ] }),
1241
- /* @__PURE__ */ jsx(
1242
- CollapsiblePanel,
1243
- {
1244
- className: "w-full flex-1 min-h-0 h-auto data-starting-style:h-0 data-ending-style:h-0",
1245
- contentClassName: "flex flex-1 min-h-0 flex-col overflow-y-auto",
1246
- children: /* @__PURE__ */ jsxs(ScrollAreaRoot, { className: "flex-1 min-h-0", children: [
1247
- /* @__PURE__ */ jsx(ScrollAreaViewport, { className: "min-h-0", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 pl-2 pr-2", children: [
1248
- pinnedSessions.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-px", children: pinnedSessions.map((session) => /* @__PURE__ */ jsx(
1249
- SessionItem,
1250
- {
1251
- session,
1252
- active: isSessionActive(session, activeFriendlyId, activeSessionKey),
1253
- isGenerating: isSessionGenerating(session, activeFriendlyId, activeSessionKey, isStreaming),
1254
- isPinned: true,
1255
- selectionMode,
1256
- selected: selectedSessionKeys.has(session.key),
1257
- onToggleSelect: handleToggleSelect,
1258
- onSelect,
1259
- onTogglePin: handleTogglePin,
1260
- onRename,
1261
- onDelete,
1262
- onExport
1263
- },
1264
- session.key
1265
- )) }) : null,
1266
- showDivider ? /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-primary-200/80" }) : null,
1267
- groupedSessions.chat.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-px", children: [
1268
- /* @__PURE__ */ jsx(
1269
- "div",
1270
- {
1271
- className: cn(
1272
- "border-l-2 border-primary-200/70 px-2 py-1 text-[11px] font-medium text-primary-500/80 text-balance"
1273
- ),
1274
- children: "💬 Chats"
1275
- }
1276
- ),
1277
- /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-px", children: groupedSessions.chat.map((session) => /* @__PURE__ */ jsx(
1278
- SessionItem,
1279
- {
1280
- session,
1281
- active: isSessionActive(session, activeFriendlyId, activeSessionKey),
1282
- 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
- );
1491
+ onSelect,
1492
+ onTogglePin: handleTogglePin,
1493
+ onRename,
1494
+ onDelete,
1495
+ onExport
1496
+ },
1497
+ session.key
1498
+ )) })
1499
+ ]
1644
1500
  }
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);
1501
+ );
1502
+ }
1503
+ return /* @__PURE__ */ jsxs(
1504
+ Collapsible,
1505
+ {
1506
+ className: "flex h-full flex-col flex-1 min-h-0 w-full",
1507
+ defaultOpen,
1508
+ children: [
1509
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pr-2 shrink-0", children: [
1510
+ /* @__PURE__ */ jsxs(CollapsibleTrigger, { className: "w-fit pl-1.5 text-balance", children: [
1511
+ "Sessions",
1512
+ /* @__PURE__ */ jsx("span", { className: "opacity-0 transition-opacity duration-150 group-hover:opacity-100", children: /* @__PURE__ */ jsx(
1513
+ HugeiconsIcon,
1514
+ {
1515
+ icon: ArrowRight01Icon,
1516
+ className: "size-3 transition-transform duration-150 group-data-panel-open:rotate-90"
1517
+ }
1518
+ ) })
1519
+ ] }),
1520
+ /* @__PURE__ */ jsx(
1521
+ "button",
1522
+ {
1523
+ type: "button",
1524
+ onClick: handleSelectionModeToggle,
1525
+ className: cn(
1526
+ "text-[11px] font-medium px-1.5 py-0.5 rounded-md transition-colors",
1527
+ selectionMode ? "text-primary-900 bg-primary-200" : "text-primary-500 hover:text-primary-700 hover:bg-primary-100"
1528
+ ),
1529
+ children: selectionMode ? "Done" : "Select"
1530
+ }
1531
+ )
1532
+ ] }),
1533
+ /* @__PURE__ */ jsx(
1534
+ CollapsiblePanel,
1535
+ {
1536
+ className: "w-full flex-1 min-h-0 h-auto data-starting-style:h-0 data-ending-style:h-0",
1537
+ contentClassName: "flex flex-1 min-h-0 flex-col overflow-y-auto",
1538
+ children: /* @__PURE__ */ jsxs(ScrollAreaRoot, { className: "flex-1 min-h-0", children: [
1539
+ /* @__PURE__ */ jsx(ScrollAreaViewport, { className: "min-h-0", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 pl-2 pr-2", children: [
1540
+ pinnedSessions.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-px", children: pinnedSessions.map((session) => /* @__PURE__ */ jsx(
1541
+ SessionItem,
1542
+ {
1543
+ session,
1544
+ active: isSessionActive(session, activeFriendlyId, activeSessionKey),
1545
+ isGenerating: isSessionGenerating(session, activeFriendlyId, activeSessionKey, isStreaming),
1546
+ isPinned: true,
1547
+ selectionMode,
1548
+ selected: selectedSessionKeys.has(session.key),
1549
+ onToggleSelect: handleToggleSelect,
1550
+ onSelect,
1551
+ onTogglePin: handleTogglePin,
1552
+ onRename,
1553
+ onDelete,
1554
+ onExport
1555
+ },
1556
+ session.key
1557
+ )) }) : null,
1558
+ showDivider ? /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-primary-200/80" }) : null,
1559
+ groupedSessions.chat.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-px", children: [
1560
+ /* @__PURE__ */ jsx(
1561
+ "div",
1562
+ {
1563
+ className: cn(
1564
+ "border-l-2 border-primary-200/70 px-2 py-1 text-[11px] font-medium text-primary-500/80 text-balance"
1565
+ ),
1566
+ children: "💬 Chats"
1567
+ }
1568
+ ),
1569
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-px", children: groupedSessions.chat.map((session) => /* @__PURE__ */ jsx(
1570
+ SessionItem,
1571
+ {
1572
+ session,
1573
+ active: isSessionActive(session, activeFriendlyId, activeSessionKey),
1574
+ isGenerating: isSessionGenerating(session, activeFriendlyId, activeSessionKey, isStreaming),
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-D3fOAswX.js").then((m) => ({ default: m.SettingsDialog }))
1684
+ () => import("./settings-dialog-ClKFnZ1x.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
  }
@@ -3378,6 +3439,10 @@ const LLM_PROVIDER_DEFAULTS = {
3378
3439
  baseUrl: "https://openrouter.ai/api/v1",
3379
3440
  model: "openai/gpt-oss-120b"
3380
3441
  },
3442
+ kilocode: {
3443
+ baseUrl: "https://api.kilo.ai/api/gateway",
3444
+ model: "google/gemini-2.5-flash"
3445
+ },
3381
3446
  ollama: {
3382
3447
  baseUrl: "http://localhost:11434/v1",
3383
3448
  model: "llama3.2"
@@ -3409,7 +3474,7 @@ function getEffectiveLlmModel(settings) {
3409
3474
  function getAvailability(settings, hasEnvKey) {
3410
3475
  if (settings.llmProvider === "ollama") return true;
3411
3476
  if (settings.llmProvider === "custom") {
3412
- return Boolean(settings.llmApiKey.trim()) || settings.llmBaseUrl.trim() && settings.llmModel.trim();
3477
+ return Boolean(settings.llmApiKey.trim()) || Boolean(settings.llmBaseUrl.trim() && settings.llmModel.trim());
3413
3478
  }
3414
3479
  return hasEnvKey || Boolean(settings.llmApiKey.trim());
3415
3480
  }
@@ -3458,6 +3523,7 @@ function useLlmSettings() {
3458
3523
  const [status, setStatus] = useState({
3459
3524
  hasEnvKey: false,
3460
3525
  hasOpenRouterKey: false,
3526
+ hasKilocodeKey: false,
3461
3527
  hasUserKey: Boolean(settings.llmApiKey),
3462
3528
  isAvailable: getAvailability(settings, false),
3463
3529
  isLoading: true,
@@ -3472,10 +3538,11 @@ function useLlmSettings() {
3472
3538
  const data = await res.json();
3473
3539
  if (cancelled) return;
3474
3540
  const hasUserKey = Boolean(settings.llmApiKey);
3475
- 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;
3476
3542
  setStatus({
3477
3543
  hasEnvKey: data.hasEnvKey,
3478
3544
  hasOpenRouterKey: Boolean(data.hasOpenRouterKey),
3545
+ hasKilocodeKey: Boolean(data.hasKilocodeKey),
3479
3546
  hasUserKey,
3480
3547
  isAvailable: getAvailability(settings, hasProviderKey),
3481
3548
  isLoading: false,
@@ -4072,13 +4139,14 @@ function ChatMessageListComponent({
4072
4139
  return sources;
4073
4140
  }, [displayMessages, toolResultsByCallId]);
4074
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();
4075
4143
  const lastUserIndex = displayMessages.map((message, index) => ({ message, index })).filter(({ message }) => message.role === "user").map(({ index }) => index).pop();
4076
4144
  const showTypingIndicator = waitingForResponse && (typeof lastUserIndex !== "number" || typeof lastAssistantIndex !== "number" || lastAssistantIndex < lastUserIndex);
4077
4145
  const groupStartIndex = typeof lastUserIndex === "number" ? lastUserIndex : -1;
4078
4146
  const hasGroup = pinToTop && groupStartIndex >= 0;
4079
- const lastAssistantMessage = typeof lastAssistantIndex === "number" ? displayMessages[lastAssistantIndex] : void 0;
4147
+ const lastAssistantMessage = typeof lastTextAssistantIndex === "number" ? displayMessages[lastTextAssistantIndex] : void 0;
4080
4148
  const lastAssistantText = lastAssistantMessage ? textFromMessage(lastAssistantMessage) : "";
4081
- 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);
4082
4150
  useLayoutEffect(() => {
4083
4151
  if (loading) return;
4084
4152
  if (pinToTop) {
@@ -6152,13 +6220,56 @@ const INITIAL_STATE = {
6152
6220
  function useStreaming(options) {
6153
6221
  const [state, setState] = useState(INITIAL_STATE);
6154
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);
6155
6227
  const onDoneRef = useRef(options.onDone);
6156
6228
  const onErrorRef = useRef(options.onError);
6157
6229
  const onAssistantDeltaRef = useRef(options.onAssistantDelta);
6158
6230
  onDoneRef.current = options.onDone;
6159
6231
  onErrorRef.current = options.onError;
6160
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
+ }
6161
6270
  const stop = useCallback((options2) => {
6271
+ doneRef.current = true;
6272
+ clearPolling();
6162
6273
  if (eventSourceRef.current) {
6163
6274
  eventSourceRef.current.close();
6164
6275
  eventSourceRef.current = null;
@@ -6170,7 +6281,10 @@ function useStreaming(options) {
6170
6281
  setState(INITIAL_STATE);
6171
6282
  }, []);
6172
6283
  const start = useCallback(
6173
- (sessionKey) => {
6284
+ function start2(sessionKey) {
6285
+ doneRef.current = false;
6286
+ clearPolling();
6287
+ streamStartRef.current = Date.now();
6174
6288
  if (eventSourceRef.current) {
6175
6289
  eventSourceRef.current.close();
6176
6290
  eventSourceRef.current = null;
@@ -6213,6 +6327,8 @@ function useStreaming(options) {
6213
6327
  es.addEventListener("done", (e) => {
6214
6328
  try {
6215
6329
  const data = JSON.parse(e.data);
6330
+ doneRef.current = true;
6331
+ clearPolling();
6216
6332
  es.close();
6217
6333
  eventSourceRef.current = null;
6218
6334
  setState((prev) => ({ ...prev, active: false }));
@@ -6228,6 +6344,11 @@ function useStreaming(options) {
6228
6344
  onErrorRef.current?.("Stream connection lost");
6229
6345
  }
6230
6346
  };
6347
+ pollingTimeoutRef.current = window.setTimeout(() => {
6348
+ if (doneRef.current) return;
6349
+ const startedAt = streamStartRef.current ?? Date.now();
6350
+ startPolling(sessionKey, startedAt);
6351
+ }, 3e3);
6231
6352
  },
6232
6353
  []
6233
6354
  );
@@ -6475,7 +6596,7 @@ const KeyboardShortcutsDialog = lazy(
6475
6596
  }))
6476
6597
  );
6477
6598
  const SearchDialog = lazy(
6478
- () => import("./search-dialog-BtSQW9SR.js").then((m) => ({
6599
+ () => import("./search-dialog-BnwiXpdA.js").then((m) => ({
6479
6600
  default: m.SearchDialog
6480
6601
  }))
6481
6602
  );
@@ -6759,9 +6880,9 @@ function ChatScreen({
6759
6880
  const hideUi = shouldRedirectToNew || isRedirecting;
6760
6881
  const pollingPhaseRef = useRef("fast");
6761
6882
  useEffect(() => {
6762
- const latestMessage = historyMessages[historyMessages.length - 1];
6763
- if (!latestMessage || latestMessage.role !== "assistant") return;
6764
- 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)}`;
6765
6886
  if (signature !== lastAssistantSignature.current) {
6766
6887
  lastAssistantSignature.current = signature;
6767
6888
  if (pollingPhaseRef.current !== "fast" && streamTimer.current) {
@@ -6941,7 +7062,6 @@ function ChatScreen({
6941
7062
  content: a.base64
6942
7063
  }));
6943
7064
  streamingNotificationTextRef.current = "";
6944
- startStream(sessionKey);
6945
7065
  streamStart();
6946
7066
  fetch("/api/send", {
6947
7067
  method: "POST",
@@ -6957,6 +7077,9 @@ function ChatScreen({
6957
7077
  })
6958
7078
  }).then(async (res) => {
6959
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);
6960
7083
  }).catch((err) => {
6961
7084
  const messageText = err instanceof Error ? err.message : String(err);
6962
7085
  if (isMissingGatewayAuth(messageText)) {