@iblai/web-utils 1.11.4 → 1.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9395,6 +9395,14 @@ async function validateJwtToken(storageService) {
9395
9395
  return false;
9396
9396
  }
9397
9397
  }
9398
+ /**
9399
+ * How long (ms) a redirect may be "in progress" before we assume the
9400
+ * navigation failed and allow a fresh attempt. Must be longer than the 3s
9401
+ * safety timer in `safeRedirectToAuthSpa` so the two don't fight; this is
9402
+ * the backstop for when that timer is lost because the page began
9403
+ * unloading and then survived (bfcache restore, aborted navigation).
9404
+ */
9405
+ const REDIRECT_STUCK_MS = 5000;
9398
9406
  function useAuthProvider({ middleware = new Map(), onAuthSuccess, onAuthFailure, redirectToAuthSpa, username, pathname, storageService, skipAuthCheck, token, enableStorageSync = true, }) {
9399
9407
  const [isAuthenticating, setIsAuthenticating] = React.useState(true);
9400
9408
  const [userIsAccessingPublicRoute, setUserIsAccessingPublicRoute] = React.useState(false);
@@ -9402,21 +9410,51 @@ function useAuthProvider({ middleware = new Map(), onAuthSuccess, onAuthFailure,
9402
9410
  const cookieCheckIntervalRef = React.useRef(null);
9403
9411
  const lastLogoutTimestampRef = React.useRef(null);
9404
9412
  const lastLoginTimestampRef = React.useRef(null);
9405
- // Guard to prevent poll from firing after a redirect has been initiated.
9406
- // Once set, no further redirects or cookie syncs will happen.
9413
+ // Guard to prevent the poll from firing redundant redirects while a
9414
+ // navigation to the auth SPA is already underway.
9407
9415
  const isRedirectingRef = React.useRef(false);
9416
+ // Wall-clock time (ms) at which the in-progress redirect was started.
9417
+ // Used to self-heal a stuck guard: the browser discards our 3s safety
9418
+ // timer the instant navigation begins, so if navigation never actually
9419
+ // completes (page restored from bfcache, redirect aborted/slow, timers
9420
+ // throttled during unload) the boolean alone would latch ON forever and
9421
+ // suppress every future redirect. This timestamp lets the poll detect
9422
+ // that case and reset the guard. See `redirectInProgress`.
9423
+ const redirectStartedAtRef = React.useRef(0);
9408
9424
  // RTK Query hook for refreshing JWT token
9409
9425
  const [refreshJwtToken] = dataLayer.useLazyRefreshJwtTokenQuery();
9410
9426
  /**
9411
- * Wrapper around redirectToAuthSpa that stops polling immediately
9412
- * and prevents any further redirect calls from racing.
9427
+ * Whether a redirect is genuinely still in progress.
9428
+ *
9429
+ * Returns false (and resets the guard) when the redirect was started more
9430
+ * than REDIRECT_STUCK_MS ago, since at that point the page is clearly
9431
+ * still alive and the navigation must have failed. This is what lets the
9432
+ * app recover on its own instead of latching the guard ON forever and
9433
+ * needing a manual cookie clear / hard reload.
9434
+ */
9435
+ const redirectInProgress = () => {
9436
+ if (!isRedirectingRef.current)
9437
+ return false;
9438
+ const elapsed = Date.now() - redirectStartedAtRef.current;
9439
+ if (elapsed > REDIRECT_STUCK_MS) {
9440
+ console.log("[AuthProvider] redirect guard stuck for", elapsed, "ms — navigation never completed, resetting so we can retry");
9441
+ isRedirectingRef.current = false;
9442
+ redirectStartedAtRef.current = 0;
9443
+ return false;
9444
+ }
9445
+ return true;
9446
+ };
9447
+ /**
9448
+ * Wrapper around redirectToAuthSpa that prevents redundant redirect calls
9449
+ * from racing while a navigation is already underway.
9413
9450
  */
9414
9451
  const safeRedirectToAuthSpa = (...args) => {
9415
- if (isRedirectingRef.current) {
9452
+ if (redirectInProgress()) {
9416
9453
  console.log("[AuthProvider] Redirect already in progress, skipping");
9417
9454
  return;
9418
9455
  }
9419
9456
  isRedirectingRef.current = true;
9457
+ redirectStartedAtRef.current = Date.now();
9420
9458
  // NOTE: we intentionally do NOT clear the interval here.
9421
9459
  // The isRedirectingRef guard prevents redundant redirects while navigation
9422
9460
  // is in progress. Clearing the interval preemptively can permanently stop
@@ -9431,6 +9469,7 @@ function useAuthProvider({ middleware = new Map(), onAuthSuccess, onAuthFailure,
9431
9469
  if (isRedirectingRef.current) {
9432
9470
  console.log("[AuthProvider] safeRedirectToAuthSpa: navigation did not occur, resetting redirect guard");
9433
9471
  isRedirectingRef.current = false;
9472
+ redirectStartedAtRef.current = 0;
9434
9473
  }
9435
9474
  }, 3000);
9436
9475
  };
@@ -9457,6 +9496,7 @@ function useAuthProvider({ middleware = new Map(), onAuthSuccess, onAuthFailure,
9457
9496
  }
9458
9497
  // Reset redirect guard on effect setup (new mount / dep change)
9459
9498
  isRedirectingRef.current = false;
9499
+ redirectStartedAtRef.current = 0;
9460
9500
  // Initial sync on mount
9461
9501
  async function initialSync() {
9462
9502
  console.log("[AuthProvider] initialSync starting");
@@ -9487,8 +9527,11 @@ function useAuthProvider({ middleware = new Map(), onAuthSuccess, onAuthFailure,
9487
9527
  initialSync();
9488
9528
  // Poll for cookie changes every 2 seconds to detect cross-SPA updates
9489
9529
  cookieCheckIntervalRef.current = setInterval(async () => {
9490
- // If a redirect is already in progress, skip entirely
9491
- if (isRedirectingRef.current) {
9530
+ // If a redirect is genuinely still in progress, skip entirely.
9531
+ // redirectInProgress() self-heals a guard that got stuck because a
9532
+ // navigation was initiated but the page survived (bfcache, aborted
9533
+ // redirect), so this no longer latches ON forever.
9534
+ if (redirectInProgress()) {
9492
9535
  console.log("[AuthProvider] interval: redirect already in progress, skipping");
9493
9536
  return;
9494
9537
  }
@@ -9561,6 +9604,27 @@ function useAuthProvider({ middleware = new Map(), onAuthSuccess, onAuthFailure,
9561
9604
  window.addEventListener("storage", handleStorageChange);
9562
9605
  return () => window.removeEventListener("storage", handleStorageChange);
9563
9606
  }, [storageService, enableStorageSync]);
9607
+ /**
9608
+ * Reset the redirect guard when the page is restored from the back/forward
9609
+ * cache (web only). When a redirect calls window.location.href the browser
9610
+ * may freeze and bfcache the page rather than destroy it; navigating back
9611
+ * then restores it with isRedirectingRef still latched ON and the 3s safety
9612
+ * timer discarded. Clearing the guard here lets redirects fire again
9613
+ * without a manual cookie clear / hard reload.
9614
+ */
9615
+ React.useEffect(() => {
9616
+ if (!isWeb$1() || skipAuthCheck)
9617
+ return;
9618
+ const handlePageShow = (event) => {
9619
+ if (event.persisted) {
9620
+ console.log("[AuthProvider] pageshow from bfcache — resetting redirect guard");
9621
+ isRedirectingRef.current = false;
9622
+ redirectStartedAtRef.current = 0;
9623
+ }
9624
+ };
9625
+ window.addEventListener("pageshow", handlePageShow);
9626
+ return () => window.removeEventListener("pageshow", handlePageShow);
9627
+ }, [skipAuthCheck]);
9564
9628
  /**
9565
9629
  * Performs the authentication check by:
9566
9630
  * 1. Validating the auth token
@@ -12365,7 +12429,35 @@ const selectEnableChatActionsPopup = (state) => state.chat.enableChatActionsPopu
12365
12429
  * because browsers block mixed content (HTTPS page -> HTTP localhost).
12366
12430
  */
12367
12431
  const OLLAMA_BASE_URL = "http://localhost:11434";
12432
+ /**
12433
+ * Ollama-compatible MCP/tool proxy. Chat for tool-capable models is routed here
12434
+ * (instead of plain Ollama) so the model gets MCP tools. The host (ghost-os)
12435
+ * owns the real routing for the Tauri path; this is the fetch-path equivalent.
12436
+ */
12437
+ const TOOL_BRIDGE_BASE_URL = "http://localhost:8000";
12368
12438
  const OLLAMA_MODEL = "phi3:mini";
12439
+ const LOCAL_LLM_MODEL_KEY = "ibl_local_llm_model";
12440
+ const LOCAL_LLM_TOOL_SUPPORT_KEY = "ibl_local_llm_tool_support";
12441
+ /**
12442
+ * The model the user has chosen to chat with (persisted by the Local Models
12443
+ * settings UI), falling back to the default model.
12444
+ */
12445
+ function getSelectedModel() {
12446
+ if (typeof window === "undefined")
12447
+ return OLLAMA_MODEL;
12448
+ return localStorage.getItem(LOCAL_LLM_MODEL_KEY) || OLLAMA_MODEL;
12449
+ }
12450
+ /**
12451
+ * Whether the selected model supports tools / function calling, persisted by the
12452
+ * Local Models settings UI (`setLocalLLMToolSupport` in @iblai/web-containers).
12453
+ * Read directly from localStorage to avoid a web-utils → web-containers import
12454
+ * cycle — same approach as {@link getSelectedModel}.
12455
+ */
12456
+ function getSelectedModelToolSupport() {
12457
+ if (typeof window === "undefined")
12458
+ return false;
12459
+ return localStorage.getItem(LOCAL_LLM_TOOL_SUPPORT_KEY) === "true";
12460
+ }
12369
12461
  /**
12370
12462
  * Check if running in Tauri
12371
12463
  */
@@ -12449,11 +12541,17 @@ async function streamOllamaChatViaTauri(messages, callbacks, generationId) {
12449
12541
  }
12450
12542
  });
12451
12543
  try {
12452
- // Call the Tauri command to start streaming
12544
+ const model = getSelectedModel();
12545
+ const toolSupport = getSelectedModelToolSupport();
12546
+ console.log("[LocalLLM] Chatting with model:", model, "toolSupport:", toolSupport);
12547
+ // Call the Tauri command to start streaming. `toolSupport` (→ Rust
12548
+ // `tool_support`) lets the host route to the MCP/tool proxy (:8000) vs
12549
+ // plain Ollama (:11434); the host owns the actual routing decision.
12453
12550
  await invoke("ollama_chat_stream", {
12454
12551
  messages,
12455
- model: OLLAMA_MODEL,
12552
+ model,
12456
12553
  generationId,
12554
+ toolSupport,
12457
12555
  });
12458
12556
  }
12459
12557
  finally {
@@ -12476,13 +12574,19 @@ async function streamOllamaChatViaFetch(messages, callbacks, generationId, abort
12476
12574
  try {
12477
12575
  // Notify start
12478
12576
  callbacks.onStart(generationId);
12479
- const response = await fetch(`${OLLAMA_BASE_URL}/api/chat`, {
12577
+ const model = getSelectedModel();
12578
+ const toolSupport = getSelectedModelToolSupport();
12579
+ // Tool-capable models stream from the MCP/tool proxy (:8000); others from
12580
+ // plain Ollama (:11434).
12581
+ const base = toolSupport ? TOOL_BRIDGE_BASE_URL : OLLAMA_BASE_URL;
12582
+ console.log("[LocalLLM] Chatting with model:", model, "toolSupport:", toolSupport, "via", base);
12583
+ const response = await fetch(`${base}/api/chat`, {
12480
12584
  method: "POST",
12481
12585
  headers: {
12482
12586
  "Content-Type": "application/json",
12483
12587
  },
12484
12588
  body: JSON.stringify({
12485
- model: OLLAMA_MODEL,
12589
+ model,
12486
12590
  messages,
12487
12591
  stream: true,
12488
12592
  }),