opencami 1.3.0 → 1.3.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 (34) hide show
  1. package/README.md +1 -1
  2. package/dist/client/assets/{_sessionKey-CaDTbjXx.js → _sessionKey-Z6Wcnj0N.js} +26 -26
  3. package/dist/client/assets/{agents-CGtqdyBi.js → agents-D8ZHVQ1Z.js} +2 -2
  4. package/dist/client/assets/{agents-screen-DzIMcmHe.js → agents-screen-BVK0QTRH.js} +1 -1
  5. package/dist/client/assets/{button-BcnCTjUT.js → button-CuH8u1uR.js} +1 -1
  6. package/dist/client/assets/{connect-B8sfEQyb.js → connect-CtDn993i.js} +1 -1
  7. package/dist/client/assets/file-explorer-screen-DMUuR1uG.js +1 -0
  8. package/dist/client/assets/{files-BHXmS1J5.js → files-CDMLoJ0u.js} +2 -2
  9. package/dist/client/assets/{index-CxJ4zG_W.js → index-IGP-Igwt.js} +1 -1
  10. package/dist/client/assets/{index-BfhiL2JN.js → index-N9-2R5hZ.js} +1 -1
  11. package/dist/client/assets/{keyboard-shortcuts-dialog-DEknElTu.js → keyboard-shortcuts-dialog-DW--4YLF.js} +1 -1
  12. package/dist/client/assets/{main-BlX7CS2-.js → main-DrVY5UJU.js} +2 -2
  13. package/dist/client/assets/{opencami-logo-5KynvViW.js → opencami-logo-ChD2XR2j.js} +1 -1
  14. package/dist/client/assets/{react-ONC2UaeC.js → react-CzqI3gbN.js} +1 -1
  15. package/dist/client/assets/search-dialog-DYyFYJmw.js +1 -0
  16. package/dist/client/assets/{session-export-dialog-DSRWU2YO.js → session-export-dialog-BFizBmGb.js} +1 -1
  17. package/dist/client/assets/settings-dialog-UvgVi2It.js +1 -0
  18. package/dist/client/assets/{switch-dAvZcYA-.js → switch-DxW2OWPG.js} +1 -1
  19. package/dist/client/assets/{use-file-explorer-state-C6lX-h0n.js → use-file-explorer-state-Cii59H70.js} +1 -1
  20. package/dist/client/assets/{useButton-DhneLsMA.js → useButton-hZdvKtl_.js} +1 -1
  21. package/dist/server/assets/{_sessionKey-ZF1_Jqbt.js → _sessionKey-DVNrEYFh.js} +38 -9
  22. package/dist/server/assets/_tanstack-start-manifest_v-PwRq_yJS.js +4 -0
  23. package/dist/server/assets/{file-explorer-screen-BzvgvV8m.js → file-explorer-screen-Cx0jiLRU.js} +72 -15
  24. package/dist/server/assets/{files-BxvRDIWU.js → files-Trs1M5ba.js} +1 -1
  25. package/dist/server/assets/{index-CTTNe_Sf.js → index-Dv2RXDa2.js} +1 -1
  26. package/dist/server/assets/{router-D5D0udHV.js → router-OoQe2c20.js} +3 -3
  27. package/dist/server/assets/{search-dialog-C8Oy-FBs.js → search-dialog-Bq0Pnxdb.js} +95 -52
  28. package/dist/server/assets/{settings-dialog-CQFuAt9B.js → settings-dialog-BsJsnMiu.js} +11 -5
  29. package/dist/server/server.js +2 -2
  30. package/package.json +1 -1
  31. package/dist/client/assets/file-explorer-screen-hjggspl-.js +0 -1
  32. package/dist/client/assets/search-dialog-40gW31ca.js +0 -1
  33. package/dist/client/assets/settings-dialog-AsvYzSBc.js +0 -1
  34. package/dist/server/assets/_tanstack-start-manifest_v-C2FijMus.js +0 -4
@@ -21,7 +21,7 @@ import { r as resolveLanguage, u as useChatSettingsStore, C as CodeBlock, a as u
21
21
  import { create } from "zustand";
22
22
  import { persist } from "zustand/middleware";
23
23
  import { createPortal } from "react-dom";
24
- import { a as Route } from "./router-D5D0udHV.js";
24
+ import { a as Route } from "./router-OoQe2c20.js";
25
25
  function deriveFriendlyIdFromKey(key) {
26
26
  if (!key) return "main";
27
27
  const trimmed = key.trim();
@@ -1649,7 +1649,7 @@ function useRenameSession() {
1649
1649
  return { renameSession, renaming, error };
1650
1650
  }
1651
1651
  const SettingsDialog = lazy(
1652
- () => import("./settings-dialog-CQFuAt9B.js").then((m) => ({ default: m.SettingsDialog }))
1652
+ () => import("./settings-dialog-BsJsnMiu.js").then((m) => ({ default: m.SettingsDialog }))
1653
1653
  );
1654
1654
  const SessionExportDialog = lazy(
1655
1655
  () => import("./session-export-dialog-DRVbC8Q-.js").then((m) => ({
@@ -2353,6 +2353,7 @@ function MessageActionsBar({
2353
2353
  const [audioState, setAudioState] = useState("idle");
2354
2354
  const audioRef = useRef(null);
2355
2355
  const objectUrlRef = useRef(null);
2356
+ const ttsAbortControllerRef = useRef(null);
2356
2357
  const [ttsEnabled, setTtsEnabled] = useState(() => {
2357
2358
  if (typeof window === "undefined") return true;
2358
2359
  try {
@@ -2373,6 +2374,7 @@ function MessageActionsBar({
2373
2374
  }, []);
2374
2375
  useEffect(() => {
2375
2376
  return () => {
2377
+ ttsAbortControllerRef.current?.abort();
2376
2378
  if (audioRef.current) {
2377
2379
  audioRef.current.pause();
2378
2380
  audioRef.current = null;
@@ -2394,6 +2396,7 @@ function MessageActionsBar({
2394
2396
  };
2395
2397
  const handleTTS = async () => {
2396
2398
  if (audioState === "playing") {
2399
+ ttsAbortControllerRef.current?.abort();
2397
2400
  if (audioRef.current) {
2398
2401
  audioRef.current.pause();
2399
2402
  audioRef.current = null;
@@ -2405,12 +2408,16 @@ function MessageActionsBar({
2405
2408
  setAudioState("idle");
2406
2409
  return;
2407
2410
  }
2411
+ ttsAbortControllerRef.current?.abort();
2412
+ const controller = new AbortController();
2413
+ ttsAbortControllerRef.current = controller;
2408
2414
  setAudioState("loading");
2409
2415
  try {
2410
2416
  const res = await fetch("/api/tts", {
2411
2417
  method: "POST",
2412
2418
  headers: { "Content-Type": "application/json" },
2413
- body: JSON.stringify({ text })
2419
+ body: JSON.stringify({ text }),
2420
+ signal: controller.signal
2414
2421
  });
2415
2422
  if (!res.ok) throw new Error("TTS failed");
2416
2423
  const blob = await res.blob();
@@ -2436,7 +2443,11 @@ function MessageActionsBar({
2436
2443
  };
2437
2444
  await audio.play();
2438
2445
  setAudioState("playing");
2439
- } catch {
2446
+ } catch (error) {
2447
+ if (error instanceof Error && error.name === "AbortError") {
2448
+ setAudioState("idle");
2449
+ return;
2450
+ }
2440
2451
  setAudioState("idle");
2441
2452
  if (objectUrlRef.current) {
2442
2453
  URL.revokeObjectURL(objectUrlRef.current);
@@ -4448,13 +4459,17 @@ function ModelSelector({ className, onModelChange }) {
4448
4459
  const [selectedModel, setSelectedModel] = useState("");
4449
4460
  const [isLoading, setIsLoading] = useState(true);
4450
4461
  const [error, setError] = useState(null);
4462
+ const abortControllerRef = useRef(null);
4451
4463
  useEffect(() => {
4452
4464
  let mounted = true;
4453
4465
  async function fetchModels() {
4466
+ abortControllerRef.current?.abort();
4467
+ const controller = new AbortController();
4468
+ abortControllerRef.current = controller;
4454
4469
  try {
4455
4470
  setIsLoading(true);
4456
4471
  setError(null);
4457
- const response = await fetch("/api/models");
4472
+ const response = await fetch("/api/models", { signal: controller.signal });
4458
4473
  if (!response.ok) {
4459
4474
  throw new Error("Failed to fetch models");
4460
4475
  }
@@ -4470,6 +4485,7 @@ function ModelSelector({ className, onModelChange }) {
4470
4485
  throw new Error("No models available");
4471
4486
  }
4472
4487
  } catch (err) {
4488
+ if (err instanceof Error && err.name === "AbortError") return;
4473
4489
  if (!mounted) return;
4474
4490
  console.error("[model-selector] Error fetching models:", err);
4475
4491
  setError(err instanceof Error ? err.message : "Failed to load models");
@@ -4484,6 +4500,7 @@ function ModelSelector({ className, onModelChange }) {
4484
4500
  fetchModels();
4485
4501
  return () => {
4486
4502
  mounted = false;
4503
+ abortControllerRef.current?.abort();
4487
4504
  };
4488
4505
  }, [onModelChange]);
4489
4506
  function handleModelSelect(modelId) {
@@ -4598,6 +4615,7 @@ function PersonaPicker({ className, onSelect }) {
4598
4615
  const [isLoading, setIsLoading] = useState(true);
4599
4616
  const [available, setAvailable] = useState(false);
4600
4617
  const [enabled, setEnabled] = useState(isPersonasEnabled);
4618
+ const abortControllerRef = useRef(null);
4601
4619
  useEffect(() => {
4602
4620
  const handleStorage = (e) => {
4603
4621
  if (e.key === PERSONAS_ENABLED_KEY) {
@@ -4610,9 +4628,12 @@ function PersonaPicker({ className, onSelect }) {
4610
4628
  useEffect(() => {
4611
4629
  let mounted = true;
4612
4630
  async function fetchPersonas() {
4631
+ abortControllerRef.current?.abort();
4632
+ const controller = new AbortController();
4633
+ abortControllerRef.current = controller;
4613
4634
  try {
4614
4635
  setIsLoading(true);
4615
- const response = await fetch("/api/personas");
4636
+ const response = await fetch("/api/personas", { signal: controller.signal });
4616
4637
  if (!response.ok) {
4617
4638
  setAvailable(false);
4618
4639
  return;
@@ -4626,7 +4647,8 @@ function PersonaPicker({ className, onSelect }) {
4626
4647
  } else {
4627
4648
  setAvailable(false);
4628
4649
  }
4629
- } catch {
4650
+ } catch (error) {
4651
+ if (error instanceof Error && error.name === "AbortError") return;
4630
4652
  if (!mounted) return;
4631
4653
  setAvailable(false);
4632
4654
  } finally {
@@ -4638,6 +4660,7 @@ function PersonaPicker({ className, onSelect }) {
4638
4660
  fetchPersonas();
4639
4661
  return () => {
4640
4662
  mounted = false;
4663
+ abortControllerRef.current?.abort();
4641
4664
  };
4642
4665
  }, []);
4643
4666
  const handleSelectPersona = useCallback(
@@ -5250,6 +5273,7 @@ function ChatComposerComponent({
5250
5273
  const mediaRecorderRef = useRef(null);
5251
5274
  const recordingChunksRef = useRef([]);
5252
5275
  const recordingTimerRef = useRef(null);
5276
+ const sttAbortControllerRef = useRef(null);
5253
5277
  const webSpeechRef = useRef(null);
5254
5278
  const promptRef = useRef(null);
5255
5279
  const showSlashCommands = useMemo(() => /^\/\S*$/.test(value) && !slashMenuDismissed, [value, slashMenuDismissed]);
@@ -5401,6 +5425,7 @@ function ChatComposerComponent({
5401
5425
  );
5402
5426
  useEffect(() => {
5403
5427
  return () => {
5428
+ sttAbortControllerRef.current?.abort();
5404
5429
  if (recordingTimerRef.current) clearInterval(recordingTimerRef.current);
5405
5430
  if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
5406
5431
  mediaRecorderRef.current.stop();
@@ -5497,7 +5522,10 @@ function ChatComposerComponent({
5497
5522
  const formData = new FormData();
5498
5523
  formData.append("audio", audioBlob, `recording.${mimeType === "audio/webm" ? "webm" : "mp4"}`);
5499
5524
  if (provider !== "auto") formData.append("provider", provider);
5500
- const res = await fetch("/api/stt", { method: "POST", body: formData });
5525
+ sttAbortControllerRef.current?.abort();
5526
+ const controller = new AbortController();
5527
+ sttAbortControllerRef.current = controller;
5528
+ const res = await fetch("/api/stt", { method: "POST", body: formData, signal: controller.signal });
5501
5529
  const data = await res.json();
5502
5530
  if (data.ok && data.text) {
5503
5531
  setValue((prev) => prev + (prev ? " " : "") + data.text);
@@ -5507,6 +5535,7 @@ function ChatComposerComponent({
5507
5535
  alert(data.error || "Speech-to-text failed. Try the Browser provider in Settings.");
5508
5536
  }
5509
5537
  } catch (err) {
5538
+ if (err instanceof Error && err.name === "AbortError") return;
5510
5539
  console.warn("STT request failed:", err);
5511
5540
  alert("Could not reach speech-to-text service.");
5512
5541
  } finally {
@@ -6241,7 +6270,7 @@ const KeyboardShortcutsDialog = lazy(
6241
6270
  }))
6242
6271
  );
6243
6272
  const SearchDialog = lazy(
6244
- () => import("./search-dialog-C8Oy-FBs.js").then((m) => ({
6273
+ () => import("./search-dialog-Bq0Pnxdb.js").then((m) => ({
6245
6274
  default: m.SearchDialog
6246
6275
  }))
6247
6276
  );
@@ -0,0 +1,4 @@
1
+ const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "/home/runner/work/opencami/opencami/src/routes/__root.tsx", "children": ["/", "/agents", "/connect", "/files", "/new", "/api/agents", "/api/follow-ups", "/api/history", "/api/llm-features", "/api/models", "/api/paths", "/api/personas", "/api/ping", "/api/send", "/api/sessions", "/api/stream", "/api/stt", "/api/tts", "/chat/$sessionKey", "/api/files/delete", "/api/files/download", "/api/files/info", "/api/files/list", "/api/files/mkdir", "/api/files/read", "/api/files/rename", "/api/files/save", "/api/files/upload"], "preloads": ["/assets/main-DrVY5UJU.js"], "assets": [] }, "/": { "filePath": "/home/runner/work/opencami/opencami/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-N9-2R5hZ.js"] }, "/agents": { "filePath": "/home/runner/work/opencami/opencami/src/routes/agents.tsx", "assets": [], "preloads": ["/assets/agents-D8ZHVQ1Z.js"] }, "/connect": { "filePath": "/home/runner/work/opencami/opencami/src/routes/connect.tsx", "assets": [], "preloads": ["/assets/connect-CtDn993i.js", "/assets/index-IGP-Igwt.js", "/assets/button-CuH8u1uR.js", "/assets/react-CzqI3gbN.js"] }, "/files": { "filePath": "/home/runner/work/opencami/opencami/src/routes/files.tsx", "assets": [], "preloads": ["/assets/files-CDMLoJ0u.js"] }, "/new": { "filePath": "/home/runner/work/opencami/opencami/src/routes/new.tsx", "assets": [], "preloads": ["/assets/new-DLlBm66g.js"] }, "/api/agents": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/agents.ts" }, "/api/follow-ups": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/follow-ups.ts" }, "/api/history": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/history.ts" }, "/api/llm-features": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/llm-features.ts" }, "/api/models": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/models.ts" }, "/api/paths": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/paths.ts" }, "/api/personas": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/personas.ts" }, "/api/ping": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/ping.ts" }, "/api/send": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/send.ts" }, "/api/sessions": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/sessions.ts" }, "/api/stream": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/stream.ts" }, "/api/stt": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/stt.ts" }, "/api/tts": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/tts.ts" }, "/chat/$sessionKey": { "filePath": "/home/runner/work/opencami/opencami/src/routes/chat/$sessionKey.tsx", "assets": [], "preloads": ["/assets/_sessionKey-Z6Wcnj0N.js", "/assets/useButton-hZdvKtl_.js", "/assets/button-CuH8u1uR.js", "/assets/use-file-explorer-state-Cii59H70.js", "/assets/opencami-logo-ChD2XR2j.js", "/assets/index-IGP-Igwt.js", "/assets/react-CzqI3gbN.js"] }, "/api/files/delete": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/files/delete.ts" }, "/api/files/download": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/files/download.ts" }, "/api/files/info": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/files/info.ts" }, "/api/files/list": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/files/list.ts" }, "/api/files/mkdir": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/files/mkdir.ts" }, "/api/files/read": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/files/read.ts" }, "/api/files/rename": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/files/rename.ts" }, "/api/files/save": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/files/save.ts" }, "/api/files/upload": { "filePath": "/home/runner/work/opencami/opencami/src/routes/api/files/upload.ts" } }, "clientEntry": "/assets/main-DrVY5UJU.js" });
2
+ export {
3
+ tsrStartManifest
4
+ };
@@ -1,5 +1,5 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
- import { memo, useMemo, useCallback, useEffect, useState, useRef } from "react";
2
+ import { memo, useMemo, useCallback, useRef, useEffect, useState } from "react";
3
3
  import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
4
4
  import { HugeiconsIcon } from "@hugeicons/react";
5
5
  import { Home09Icon, SidebarLeft01Icon, ArrowLeft01Icon, Settings01Icon, ArrowRight01Icon, Home02Icon, Menu01Icon, GridViewIcon, ArrowUp01Icon, ArrowDown01Icon, Upload04Icon, FolderAddIcon, FolderOpenIcon, Download04Icon, FileEditIcon, Edit02Icon, Delete02Icon, Folder01Icon, Image01Icon, Video01Icon, MusicNote01Icon, Archive01Icon, Doc01Icon, CodeIcon, File01Icon, FloppyDiskIcon, Cancel01Icon } from "@hugeicons/core-free-icons";
@@ -653,8 +653,8 @@ const fileQueryKeys = {
653
653
  function useFileContent(path) {
654
654
  return useQuery({
655
655
  queryKey: fileQueryKeys.content(path || ""),
656
- queryFn: async () => {
657
- const response = await fetch(`/api/files/read?path=${encodeURIComponent(path)}`);
656
+ queryFn: async ({ signal }) => {
657
+ const response = await fetch(`/api/files/read?path=${encodeURIComponent(path)}`, { signal });
658
658
  if (!response.ok) {
659
659
  const error = await response.json().catch(() => ({ error: "Failed to read file" }));
660
660
  throw new Error(error.error || "Failed to read file");
@@ -669,12 +669,22 @@ function useFileContent(path) {
669
669
  }
670
670
  function useFileSave() {
671
671
  const queryClient = useQueryClient();
672
+ const abortControllerRef = useRef(null);
673
+ useEffect(() => {
674
+ return () => {
675
+ abortControllerRef.current?.abort();
676
+ };
677
+ }, []);
672
678
  return useMutation({
673
679
  mutationFn: async ({ path, content }) => {
680
+ abortControllerRef.current?.abort();
681
+ const controller = new AbortController();
682
+ abortControllerRef.current = controller;
674
683
  const response = await fetch("/api/files/save", {
675
684
  method: "POST",
676
685
  headers: { "Content-Type": "application/json" },
677
- body: JSON.stringify({ path, content })
686
+ body: JSON.stringify({ path, content }),
687
+ signal: controller.signal
678
688
  });
679
689
  if (!response.ok) {
680
690
  const error = await response.json().catch(() => ({ error: "Failed to save file" }));
@@ -696,8 +706,8 @@ function useFileSave() {
696
706
  function useFileListing(path) {
697
707
  return useQuery({
698
708
  queryKey: fileQueryKeys.listing(path),
699
- queryFn: async () => {
700
- const response = await fetch(`/api/files/list?path=${encodeURIComponent(path)}`);
709
+ queryFn: async ({ signal }) => {
710
+ const response = await fetch(`/api/files/list?path=${encodeURIComponent(path)}`, { signal });
701
711
  if (!response.ok) {
702
712
  const error = await response.text();
703
713
  throw new Error(`Failed to load files: ${error}`);
@@ -708,23 +718,25 @@ function useFileListing(path) {
708
718
  // 5 minutes
709
719
  gcTime: 10 * 60 * 1e3,
710
720
  // 10 minutes
711
- retry: 3,
712
- onError: (error) => {
713
- console.error("Failed to load file listing:", error);
714
- }
721
+ retry: 3
715
722
  });
716
723
  }
717
724
  function useFileDownload() {
718
725
  const urls = /* @__PURE__ */ new Set();
726
+ const abortControllerRef = useRef(null);
719
727
  useEffect(() => {
720
728
  return () => {
729
+ abortControllerRef.current?.abort();
721
730
  urls.forEach((url) => URL.revokeObjectURL(url));
722
731
  urls.clear();
723
732
  };
724
733
  }, []);
725
734
  return useMutation({
726
735
  mutationFn: async (filePath) => {
727
- const response = await fetch(`/api/files/download?path=${encodeURIComponent(filePath)}`);
736
+ abortControllerRef.current?.abort();
737
+ const controller = new AbortController();
738
+ abortControllerRef.current = controller;
739
+ const response = await fetch(`/api/files/download?path=${encodeURIComponent(filePath)}`, { signal: controller.signal });
728
740
  if (!response.ok) {
729
741
  const error = await response.text();
730
742
  throw new Error(`Download failed: ${error}`);
@@ -747,14 +759,24 @@ function useFileDownload() {
747
759
  }, 100);
748
760
  },
749
761
  onError: (error) => {
762
+ if (error instanceof Error && error.name === "AbortError") return;
750
763
  console.error("Download failed:", error);
751
764
  }
752
765
  });
753
766
  }
754
767
  function useFileUpload() {
755
768
  const queryClient = useQueryClient();
769
+ const abortControllerRef = useRef(null);
770
+ useEffect(() => {
771
+ return () => {
772
+ abortControllerRef.current?.abort();
773
+ };
774
+ }, []);
756
775
  return useMutation({
757
776
  mutationFn: async ({ files, path }) => {
777
+ abortControllerRef.current?.abort();
778
+ const controller = new AbortController();
779
+ abortControllerRef.current = controller;
758
780
  const results = [];
759
781
  for (let i = 0; i < files.length; i++) {
760
782
  const formData = new FormData();
@@ -762,7 +784,8 @@ function useFileUpload() {
762
784
  formData.append("file", files[i]);
763
785
  const response = await fetch("/api/files/upload", {
764
786
  method: "POST",
765
- body: formData
787
+ body: formData,
788
+ signal: controller.signal
766
789
  });
767
790
  if (!response.ok) {
768
791
  const error = await response.text();
@@ -778,16 +801,27 @@ function useFileUpload() {
778
801
  });
779
802
  },
780
803
  onError: (error) => {
804
+ if (error instanceof Error && error.name === "AbortError") return;
781
805
  console.error("Upload failed:", error);
782
806
  }
783
807
  });
784
808
  }
785
809
  function useFileDelete() {
786
810
  const queryClient = useQueryClient();
811
+ const abortControllerRef = useRef(null);
812
+ useEffect(() => {
813
+ return () => {
814
+ abortControllerRef.current?.abort();
815
+ };
816
+ }, []);
787
817
  return useMutation({
788
818
  mutationFn: async (filePath) => {
819
+ abortControllerRef.current?.abort();
820
+ const controller = new AbortController();
821
+ abortControllerRef.current = controller;
789
822
  const response = await fetch(`/api/files/delete?path=${encodeURIComponent(filePath)}`, {
790
- method: "DELETE"
823
+ method: "DELETE",
824
+ signal: controller.signal
791
825
  });
792
826
  if (!response.ok) {
793
827
  const error = await response.text();
@@ -802,18 +836,29 @@ function useFileDelete() {
802
836
  });
803
837
  },
804
838
  onError: (error) => {
839
+ if (error instanceof Error && error.name === "AbortError") return;
805
840
  console.error("Delete failed:", error);
806
841
  }
807
842
  });
808
843
  }
809
844
  function useCreateFolder() {
810
845
  const queryClient = useQueryClient();
846
+ const abortControllerRef = useRef(null);
847
+ useEffect(() => {
848
+ return () => {
849
+ abortControllerRef.current?.abort();
850
+ };
851
+ }, []);
811
852
  return useMutation({
812
853
  mutationFn: async ({ path, name }) => {
854
+ abortControllerRef.current?.abort();
855
+ const controller = new AbortController();
856
+ abortControllerRef.current = controller;
813
857
  const response = await fetch("/api/files/mkdir", {
814
858
  method: "POST",
815
859
  headers: { "Content-Type": "application/json" },
816
- body: JSON.stringify({ path: `${path}/${name}` })
860
+ body: JSON.stringify({ path: `${path}/${name}` }),
861
+ signal: controller.signal
817
862
  });
818
863
  if (!response.ok) {
819
864
  const error = await response.text();
@@ -827,20 +872,31 @@ function useCreateFolder() {
827
872
  });
828
873
  },
829
874
  onError: (error) => {
875
+ if (error instanceof Error && error.name === "AbortError") return;
830
876
  console.error("Create folder failed:", error);
831
877
  }
832
878
  });
833
879
  }
834
880
  function useFileRename() {
835
881
  const queryClient = useQueryClient();
882
+ const abortControllerRef = useRef(null);
883
+ useEffect(() => {
884
+ return () => {
885
+ abortControllerRef.current?.abort();
886
+ };
887
+ }, []);
836
888
  return useMutation({
837
889
  mutationFn: async ({ oldPath, newName }) => {
890
+ abortControllerRef.current?.abort();
891
+ const controller = new AbortController();
892
+ abortControllerRef.current = controller;
838
893
  const parentPath = oldPath.split("/").slice(0, -1).join("/");
839
894
  const newPath = `${parentPath}/${newName}`;
840
895
  const response = await fetch("/api/files/rename", {
841
896
  method: "POST",
842
897
  headers: { "Content-Type": "application/json" },
843
- body: JSON.stringify({ src: oldPath, dst: newPath })
898
+ body: JSON.stringify({ src: oldPath, dst: newPath }),
899
+ signal: controller.signal
844
900
  });
845
901
  if (!response.ok) {
846
902
  const error = await response.text();
@@ -855,6 +911,7 @@ function useFileRename() {
855
911
  });
856
912
  },
857
913
  onError: (error) => {
914
+ if (error instanceof Error && error.name === "AbortError") return;
858
915
  console.error("Rename failed:", error);
859
916
  }
860
917
  });
@@ -1,6 +1,6 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { Suspense, lazy } from "react";
3
- const FileExplorerScreen = lazy(() => import("./file-explorer-screen-BzvgvV8m.js").then((m) => ({
3
+ const FileExplorerScreen = lazy(() => import("./file-explorer-screen-Cx0jiLRU.js").then((m) => ({
4
4
  default: m.FileExplorerScreen
5
5
  })));
6
6
  function FilesRoute() {
@@ -1,6 +1,6 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { useEffect } from "react";
3
- import { R as Route } from "./router-D5D0udHV.js";
3
+ import { R as Route } from "./router-OoQe2c20.js";
4
4
  import "@tanstack/react-router";
5
5
  import "@tanstack/react-query";
6
6
  import "node:crypto";
@@ -196,7 +196,7 @@ const Route$r = createFileRoute("/new")({
196
196
  },
197
197
  component: lazyRouteComponent($$splitComponentImporter$5, "component")
198
198
  });
199
- const $$splitComponentImporter$4 = () => import("./files-BxvRDIWU.js");
199
+ const $$splitComponentImporter$4 = () => import("./files-Trs1M5ba.js");
200
200
  const Route$q = createFileRoute("/files")({
201
201
  component: lazyRouteComponent($$splitComponentImporter$4, "component")
202
202
  });
@@ -208,11 +208,11 @@ const $$splitComponentImporter$2 = () => import("./agents-Dz_i76VW.js");
208
208
  const Route$o = createFileRoute("/agents")({
209
209
  component: lazyRouteComponent($$splitComponentImporter$2, "component")
210
210
  });
211
- const $$splitComponentImporter$1 = () => import("./index-CTTNe_Sf.js");
211
+ const $$splitComponentImporter$1 = () => import("./index-Dv2RXDa2.js");
212
212
  const Route$n = createFileRoute("/")({
213
213
  component: lazyRouteComponent($$splitComponentImporter$1, "component")
214
214
  });
215
- const $$splitComponentImporter = () => import("./_sessionKey-ZF1_Jqbt.js").then((n) => n.$);
215
+ const $$splitComponentImporter = () => import("./_sessionKey-DVNrEYFh.js").then((n) => n.$);
216
216
  const Route$m = createFileRoute("/chat/$sessionKey")({
217
217
  component: lazyRouteComponent($$splitComponentImporter, "component")
218
218
  });
@@ -1,11 +1,11 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useState, useCallback, useRef, useMemo, useEffect } from "react";
2
+ import { useState, useRef, useEffect, useCallback, useMemo } from "react";
3
3
  import { useNavigate } from "@tanstack/react-router";
4
4
  import { HugeiconsIcon } from "@hugeicons/react";
5
5
  import { Search01Icon, Cancel01Icon, Loading03Icon } from "@hugeicons/core-free-icons";
6
6
  import { D as DialogRoot, a as DialogContent } from "./use-file-explorer-state-DMHdtb7D.js";
7
7
  import { useQueryClient } from "@tanstack/react-query";
8
- import { c as chatQueryKeys } from "./_sessionKey-ZF1_Jqbt.js";
8
+ import { c as chatQueryKeys } from "./_sessionKey-DVNrEYFh.js";
9
9
  import { c as cn } from "./button-DtQ3rV1m.js";
10
10
  import "@base-ui/react/dialog";
11
11
  import "@base-ui/react/menu";
@@ -57,7 +57,7 @@ import "@shikijs/langs/xml";
57
57
  import "@shikijs/langs/yaml";
58
58
  import "zustand/middleware";
59
59
  import "react-dom";
60
- import "./router-D5D0udHV.js";
60
+ import "./router-OoQe2c20.js";
61
61
  import "node:crypto";
62
62
  import "ws";
63
63
  import "node:fs";
@@ -93,6 +93,13 @@ function useSearch({ sessions, currentFriendlyId, currentSessionKey }) {
93
93
  const [query, setQuery] = useState("");
94
94
  const [isSearching, setIsSearching] = useState(false);
95
95
  const [globalResults, setGlobalResults] = useState([]);
96
+ const globalSearchControllerRef = useRef(null);
97
+ useEffect(() => {
98
+ return () => {
99
+ globalSearchControllerRef.current?.abort();
100
+ globalSearchControllerRef.current = null;
101
+ };
102
+ }, []);
96
103
  const searchCurrentConversation = useCallback(
97
104
  (searchQuery) => {
98
105
  if (!searchQuery.trim() || !currentFriendlyId || !currentSessionKey) {
@@ -130,66 +137,100 @@ function useSearch({ sessions, currentFriendlyId, currentSessionKey }) {
130
137
  );
131
138
  const searchAllSessions = useCallback(
132
139
  async (searchQuery) => {
133
- if (!searchQuery.trim()) {
140
+ const trimmedQuery = searchQuery.trim();
141
+ if (!trimmedQuery) {
142
+ globalSearchControllerRef.current?.abort();
143
+ globalSearchControllerRef.current = null;
134
144
  setGlobalResults([]);
135
145
  return [];
136
146
  }
147
+ globalSearchControllerRef.current?.abort();
148
+ const controller = new AbortController();
149
+ globalSearchControllerRef.current = controller;
137
150
  setIsSearching(true);
138
- const normalizedQuery = searchQuery.toLowerCase();
151
+ setGlobalResults([]);
152
+ const normalizedQuery = trimmedQuery.toLowerCase();
139
153
  const allResults = [];
154
+ const BATCH_SIZE = 10;
140
155
  try {
141
- await Promise.all(
142
- sessions.map(async (session) => {
143
- try {
144
- const historyKey = chatQueryKeys.history(session.friendlyId, session.key);
145
- let historyData = queryClient.getQueryData(historyKey);
146
- if (!historyData) {
147
- const params = new URLSearchParams({
148
- sessionKey: session.key,
149
- friendlyId: session.friendlyId,
150
- limit: "200"
151
- });
152
- const res = await fetch(`/api/history?${params.toString()}`);
153
- if (res.ok) {
154
- historyData = await res.json();
155
- queryClient.setQueryData(historyKey, historyData);
156
- }
157
- }
158
- if (!historyData?.messages) return;
159
- const sessionTitle = session.label || session.title || session.derivedTitle || session.friendlyId;
160
- historyData.messages.forEach((message, index) => {
161
- const text = extractTextFromMessage(message);
162
- if (!text) return;
163
- const lowerText = text.toLowerCase();
164
- const matchIndex = lowerText.indexOf(normalizedQuery);
165
- if (matchIndex !== -1) {
166
- allResults.push({
156
+ for (let i = 0; i < sessions.length; i += BATCH_SIZE) {
157
+ if (controller.signal.aborted) {
158
+ throw new DOMException("Search aborted", "AbortError");
159
+ }
160
+ const batch = sessions.slice(i, i + BATCH_SIZE);
161
+ const batchResults = await Promise.all(
162
+ batch.map(async (session) => {
163
+ try {
164
+ const historyKey = chatQueryKeys.history(session.friendlyId, session.key);
165
+ let historyData = queryClient.getQueryData(historyKey);
166
+ if (!historyData) {
167
+ const params = new URLSearchParams({
167
168
  sessionKey: session.key,
168
169
  friendlyId: session.friendlyId,
169
- sessionTitle,
170
- messageIndex: index,
171
- messageRole: message.role || "unknown",
172
- messageText: text,
173
- matchStart: matchIndex,
174
- matchEnd: matchIndex + searchQuery.length,
175
- timestamp: message.timestamp
170
+ limit: "200"
176
171
  });
172
+ const res = await fetch(`/api/history?${params.toString()}`, {
173
+ signal: controller.signal
174
+ });
175
+ if (res.ok) {
176
+ historyData = await res.json();
177
+ queryClient.setQueryData(historyKey, historyData);
178
+ }
177
179
  }
178
- });
179
- } catch {
180
- }
181
- })
182
- );
183
- allResults.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
184
- setGlobalResults(allResults);
180
+ if (!historyData?.messages) return [];
181
+ const sessionTitle = session.label || session.title || session.derivedTitle || session.friendlyId;
182
+ const sessionResults = [];
183
+ historyData.messages.forEach((message, index) => {
184
+ const text = extractTextFromMessage(message);
185
+ if (!text) return;
186
+ const lowerText = text.toLowerCase();
187
+ const matchIndex = lowerText.indexOf(normalizedQuery);
188
+ if (matchIndex !== -1) {
189
+ sessionResults.push({
190
+ sessionKey: session.key,
191
+ friendlyId: session.friendlyId,
192
+ sessionTitle,
193
+ messageIndex: index,
194
+ messageRole: message.role || "unknown",
195
+ messageText: text,
196
+ matchStart: matchIndex,
197
+ matchEnd: matchIndex + trimmedQuery.length,
198
+ timestamp: message.timestamp
199
+ });
200
+ }
201
+ });
202
+ return sessionResults;
203
+ } catch (error) {
204
+ if (error instanceof DOMException && error.name === "AbortError") {
205
+ throw error;
206
+ }
207
+ return [];
208
+ }
209
+ })
210
+ );
211
+ allResults.push(...batchResults.flat());
212
+ allResults.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
213
+ setGlobalResults([...allResults]);
214
+ }
185
215
  return allResults;
216
+ } catch (error) {
217
+ if (!(error instanceof DOMException && error.name === "AbortError")) {
218
+ throw error;
219
+ }
220
+ return [];
186
221
  } finally {
187
- setIsSearching(false);
222
+ if (globalSearchControllerRef.current === controller) {
223
+ globalSearchControllerRef.current = null;
224
+ setIsSearching(false);
225
+ }
188
226
  }
189
227
  },
190
228
  [sessions, queryClient]
191
229
  );
192
230
  const clearSearch = useCallback(() => {
231
+ globalSearchControllerRef.current?.abort();
232
+ globalSearchControllerRef.current = null;
233
+ setIsSearching(false);
193
234
  setQuery("");
194
235
  setGlobalResults([]);
195
236
  }, []);
@@ -272,14 +313,16 @@ function SearchDialog({
272
313
  return () => clearTimeout(timer);
273
314
  }, [localQuery, mode, searchAllSessions, clearSearch]);
274
315
  useEffect(() => {
275
- if (open) {
276
- setLocalQuery("");
277
- setSelectedIndex(0);
316
+ if (!open) {
278
317
  clearSearch();
279
- setTimeout(() => {
280
- inputRef.current?.focus();
281
- }, 50);
318
+ return;
282
319
  }
320
+ setLocalQuery("");
321
+ setSelectedIndex(0);
322
+ clearSearch();
323
+ setTimeout(() => {
324
+ inputRef.current?.focus();
325
+ }, 50);
283
326
  }, [open, clearSearch]);
284
327
  useEffect(() => {
285
328
  setSelectedIndex(0);