From a761701a8f52e13739420d8dc609c8d5d21307c5 Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Sat, 21 Mar 2026 23:00:57 +0530 Subject: [PATCH 1/7] Handle legacy warnings without sticky errors --- src/renderer/App.tsx | 96 ++++++++++++---- src/renderer/components/TransientWarning.tsx | 83 ++++++++++++++ src/renderer/styles.css | 111 +++++++++++++++++++ 3 files changed, 270 insertions(+), 20 deletions(-) create mode 100644 src/renderer/components/TransientWarning.tsx diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 14a7524..7a2969e 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -23,6 +23,7 @@ import { } from "./components/icons"; import { SidebarFooter } from "./components/SidebarFooter"; import { DropdownOption, ThemedDropdown } from "./components/ThemedDropdown"; +import { TransientWarning } from "./components/TransientWarning"; import { buildCloudProviderStateMap, buildProviderDrafts, @@ -201,6 +202,12 @@ function getRobinBridge(): RobinBridge { } export function App() { + const [warning, setWarning] = useState<{ + id: number; + message: string; + startedAt: number; + durationMs: number; + } | null>(null); const [profileName, setProfileName] = useState("there"); const [screen, setScreen] = useState<"chat" | "settings">("chat"); const [settingsMode, setSettingsMode] = useState("cloud"); @@ -282,12 +289,35 @@ export function App() { const chatEndRef = useRef(null); const imageInputRef = useRef(null); const streamWatchdogRef = useRef | null>(null); + const warningTimeoutRef = useRef | null>(null); const streamSequenceRef = useRef(0); const activeStreamIdRef = useRef(null); const pendingDeltasRef = useRef(new Map()); const deltaFrameRef = useRef(null); + const warnedAboutLegacyToolSettingsRef = useRef(false); const messages = activeThread?.messages ?? []; + function dismissWarning() { + if (warningTimeoutRef.current) { + clearTimeout(warningTimeoutRef.current); + warningTimeoutRef.current = null; + } + setWarning(null); + } + + function showWarning(message: string, durationMs = 5000) { + const startedAt = performance.now(); + const id = startedAt; + if (warningTimeoutRef.current) { + clearTimeout(warningTimeoutRef.current); + } + setWarning({ id, message, startedAt, durationMs }); + warningTimeoutRef.current = setTimeout(() => { + setWarning((current) => (current?.id === id ? null : current)); + warningTimeoutRef.current = null; + }, durationMs); + } + function applyPendingDeltas() { if (pendingDeltasRef.current.size === 0) { return; @@ -435,6 +465,10 @@ export function App() { ]); const nextStatus: ProviderStatus = { ...rawStatus, + toolToggles: { + fetchUrl: rawStatus.toolToggles?.fetchUrl !== false, + webSearch: rawStatus.toolToggles?.webSearch !== false + }, cloudProviderKeys: normalizeCloudProviderKeys( rawStatus.cloudProviderKeys ), @@ -443,6 +477,12 @@ export function App() { rawStatus.selectedCloudModels ) }; + if (!rawStatus.toolToggles && !warnedAboutLegacyToolSettingsRef.current) { + warnedAboutLegacyToolSettingsRef.current = true; + showWarning( + "Some older settings were missing. URL tool preferences were reset to defaults." + ); + } setStatus(nextStatus); setProviderKeyDrafts(nextStatus.providerApiKeys); setSelectedCloudModelsDraft(nextStatus.selectedCloudModels); @@ -748,6 +788,10 @@ export function App() { return () => { isActive = false; + if (warningTimeoutRef.current) { + clearTimeout(warningTimeoutRef.current); + warningTimeoutRef.current = null; + } }; }, []); @@ -1021,7 +1065,7 @@ export function App() { next.model && !localModelSet.has(next.model.toLowerCase()) ) { - setError( + showWarning( `${next.model} is not downloaded yet. Download it first, then activate.` ); return; @@ -1032,7 +1076,7 @@ export function App() { (providerId) => providerId === next.model.trim().toLowerCase() ); if (!nextCloudProvider) { - setError("Select a valid cloud provider."); + showWarning("Select a valid cloud provider."); return; } const nextCloudModel = @@ -1054,7 +1098,7 @@ export function App() { async function applyComposerSelection(nextValue: string) { const parsedSelection = parseComposerValue(nextValue); if (parsedSelection.mode === "unknown") { - setError("Select a valid model."); + showWarning("Select a valid model."); return; } @@ -1196,7 +1240,7 @@ export function App() { const result = await getRobinBridge().app.setShortcut(shortcut); setShortcutDraft(result.shortcut); if (!result.success) { - setError("Shortcut in use."); + showWarning("Shortcut in use."); } } catch (shortcutError) { setError(errorMessage(shortcutError, "Could not set shortcut.")); @@ -1234,7 +1278,7 @@ export function App() { setLocalModelNotice(null); if (ollamaStatus?.state === "not_installed") { - setError("Ollama is not installed yet. Opening download page."); + showWarning("Ollama is not installed yet. Opening download page."); try { await getRobinBridge().app.openExternal(OLLAMA_DOWNLOAD_URL); } catch { @@ -1330,13 +1374,13 @@ export function App() { file.type.toLowerCase().startsWith("image/") ); if (imageFiles.length === 0) { - setError("Only image files are supported."); + showWarning("Only image files are supported."); return; } const remainingSlots = MAX_IMAGE_ATTACHMENTS - pendingAttachments.length; if (remainingSlots <= 0) { - setError(`You can attach up to ${MAX_IMAGE_ATTACHMENTS} images.`); + showWarning(`You can attach up to ${MAX_IMAGE_ATTACHMENTS} images.`); return; } @@ -1345,7 +1389,7 @@ export function App() { for (const file of accepted) { if (file.size > MAX_IMAGE_BYTES) { - setError( + showWarning( `"${file.name}" is too large. Use images under ${Math.round(MAX_IMAGE_BYTES / (1024 * 1024))}MB.` ); continue; @@ -1359,7 +1403,7 @@ export function App() { dataUrl }); } catch { - setError(`Could not read "${file.name}".`); + showWarning(`Could not read "${file.name}".`); } } @@ -1369,7 +1413,9 @@ export function App() { } if (imageFiles.length > accepted.length) { - setError(`Only ${MAX_IMAGE_ATTACHMENTS} images can be attached at once.`); + showWarning( + `Only ${MAX_IMAGE_ATTACHMENTS} images can be attached at once.` + ); } } @@ -1382,7 +1428,7 @@ export function App() { async function sendPrompt() { if (!prompt.trim() && pendingAttachments.length === 0) return; if (isStreaming) { - setError("Response in progress. Press Stop first."); + showWarning("Response in progress. Press Stop first."); return; } const parsed = parseModelKey(activeModelDraft); @@ -1394,23 +1440,23 @@ export function App() { : undefined; if (parsed.mode === "search" && !selectedCloudProvider) { - setError("Select a cloud model first."); + showWarning("Select a cloud model first."); return; } if (selectedCloudProvider) { const selectedModels = selectedCloudModelsDraft[selectedCloudProvider] ?? []; if (selectedModels.length === 0) { - setError("Select at least one cloud model in Settings first."); + showWarning("Select at least one cloud model in Settings first."); return; } if (!cloudModelDraft || !selectedModels.includes(cloudModelDraft)) { - setError("Pick one of your selected cloud models."); + showWarning("Pick one of your selected cloud models."); return; } } if (parsed.mode === "local" && ollamaStatus?.state === "not_installed") { - setError("Ollama is not installed yet. Opening download page."); + showWarning("Ollama is not installed yet. Opening download page."); try { await getRobinBridge().app.openExternal(OLLAMA_DOWNLOAD_URL); } catch { @@ -1419,11 +1465,11 @@ export function App() { return; } if (parsed.mode === "local" && !parsed.model) { - setError("Select or download a local model first."); + showWarning("Select or download a local model first."); return; } if (parsed.mode === "local" && pendingAttachments.length > 0) { - setError("Image input is not supported in Local mode yet."); + showWarning("Image input is not supported in Local mode yet."); return; } setError(null); @@ -1433,9 +1479,8 @@ export function App() { if (streamWatchdogRef.current) clearTimeout(streamWatchdogRef.current); streamWatchdogRef.current = setTimeout(() => { if (streamToken !== streamSequenceRef.current) return; - void stopPendingResponse( - "Model response timed out. Press Stop and retry." - ); + showWarning("Model response timed out. Press Stop and retry."); + void stopPendingResponse(); }, 60000); }; resetWatchdog(); @@ -1765,6 +1810,17 @@ export function App() { + {warning ? ( +
+ +
+ ) : null} +
- {todos.length > 0 ? ( - todos.map((todo) => ( + {sortedTodos.length > 0 ? ( + sortedTodos.map((todo) => (
setDraggedTodoId(todo.id)} + className={`todo-main-item${dragOverTodoId === todo.id ? " todo-main-item-drag-over" : ""}${draggedTodoId === todo.id ? " todo-main-item-dragging" : ""}`} onDragOver={(e) => { e.preventDefault(); setDragOverTodoId(todo.id); @@ -2678,6 +2674,20 @@ export function App() { setDragOverTodoId(null); }} > +
) : null} diff --git a/src/renderer/styles.css b/src/renderer/styles.css index f196aca..e66dc16 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -1691,6 +1691,15 @@ button:focus-visible { padding: 6px 8px; } +.themed-dropdown-footer { + margin-top: 4px; + padding: 8px 8px 4px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + font-size: 11px; + line-height: 1.4; + color: var(--t3); +} + .settings-cloud-dropdown { max-width: 320px; margin-bottom: 8px; From fc1ea11c49ece26f9a8c2eeb5d4baa7c560b7ff3 Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Sat, 21 Mar 2026 23:42:54 +0530 Subject: [PATCH 5/7] Add labeled capability badges beside model selector --- src/renderer/App.tsx | 27 +++++++++++++++------------ src/renderer/styles.css | 28 +++++++++++++++++++--------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index bda9d78..a0b9de7 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -3042,18 +3042,6 @@ export function App() { void applyComposerSelection(nextValue); }} /> - {shouldShowCloudModeSelector ? ( - - ) : null}
+ Images + Tools + Search
+ {shouldShowCloudModeSelector ? ( + + ) : null} -
+ ); } diff --git a/src/renderer/components/StatusBanner.tsx b/src/renderer/components/StatusBanner.tsx new file mode 100644 index 0000000..f979313 --- /dev/null +++ b/src/renderer/components/StatusBanner.tsx @@ -0,0 +1,76 @@ +import { useEffect, useState } from "react"; +import { IconClose } from "./icons"; + +type StatusBannerVariant = "warning" | "error"; + +export function StatusBanner({ + variant, + label, + message, + onClose, + role, + ariaLive, + startedAt, + durationMs, + fullWidth = false +}: { + variant: StatusBannerVariant; + label: string; + message: string; + onClose: () => void; + role: "alert" | "status"; + ariaLive?: "assertive" | "polite" | "off"; + startedAt?: number; + durationMs?: number; + fullWidth?: boolean; +}) { + const [remainingMs, setRemainingMs] = useState(durationMs ?? 0); + + useEffect(() => { + if (!startedAt || !durationMs) { + setRemainingMs(0); + return; + } + + const tick = () => { + setRemainingMs(Math.max(0, startedAt + durationMs - performance.now())); + }; + + tick(); + const timer = window.setInterval(tick, 200); + return () => window.clearInterval(timer); + }, [durationMs, startedAt]); + + const seconds = + startedAt && durationMs ? Math.max(1, Math.ceil(remainingMs / 1000)) : null; + + return ( +
+
+

{label}

+

{message}

+
+ {seconds !== null ? ( + + {seconds} + + ) : null} + +
+ ); +} diff --git a/src/renderer/components/TransientWarning.tsx b/src/renderer/components/TransientWarning.tsx index 091822d..b3fe785 100644 --- a/src/renderer/components/TransientWarning.tsx +++ b/src/renderer/components/TransientWarning.tsx @@ -1,8 +1,4 @@ -import { useEffect, useState } from "react"; -import { IconClose } from "./icons"; - -const RADIUS = 12; -const CIRCUMFERENCE = 2 * Math.PI * RADIUS; +import { StatusBanner } from "./StatusBanner"; export function TransientWarning({ message, @@ -15,69 +11,16 @@ export function TransientWarning({ durationMs: number; onClose: () => void; }) { - const [remainingMs, setRemainingMs] = useState(durationMs); - - useEffect(() => { - let frameId = 0; - - const tick = () => { - const nextRemaining = Math.max( - 0, - startedAt + durationMs - performance.now() - ); - setRemainingMs(nextRemaining); - if (nextRemaining > 0) { - frameId = window.requestAnimationFrame(tick); - } - }; - - frameId = window.requestAnimationFrame(tick); - return () => window.cancelAnimationFrame(frameId); - }, [durationMs, startedAt]); - - const progress = durationMs > 0 ? remainingMs / durationMs : 0; - const dashOffset = CIRCUMFERENCE * (1 - progress); - const seconds = Math.max(1, Math.ceil(remainingMs / 1000)); - return ( -
-
-

Warning

-

{message}

-
-
-
- - - - - {seconds} -
- -
-
+ ); } diff --git a/src/renderer/styles.css b/src/renderer/styles.css index c7633f9..43a9a60 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -536,11 +536,8 @@ button:focus-visible { } .todo-main-item-dragging { - opacity: 0.74; -} - -.todo-main-item-drag-over { - border-top: 2px solid var(--accent); + opacity: 0.42; + background: var(--raised); } .todo-drag-handle { @@ -731,166 +728,127 @@ button:focus-visible { /* ── Error ──────────────────────────── */ .warning-rail { - padding: 0 20px 10px; + padding: 0 16px 8px; flex-shrink: 0; } -.transient-warning { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; - padding: 12px 14px; - border-radius: 18px; - border: 1px solid var(--warning-border); - background: linear-gradient(180deg, rgba(232, 152, 58, 0.12), var(--warning-bg)); - box-shadow: 0 14px 32px rgba(0, 0, 0, 0.18); -} - -.transient-warning-copy { - min-width: 0; -} - -.transient-warning-label { - color: var(--warning-text); - font-size: 11px; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.transient-warning-text { - margin-top: 2px; - color: #f7dfbf; - font-size: 14px; - line-height: 1.4; +.status-banner { + display: inline-flex; + align-self: flex-start; + align-items: flex-start; + gap: 10px; + margin: 6px 16px 0; + padding: 9px 10px; + max-width: min(520px, calc(100% - 32px)); + border-radius: 10px; + border: 1px solid var(--border); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.018), rgba(255, 255, 255, 0.01)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02); } -.transient-warning-actions { +.status-banner-full-width { display: flex; - align-items: center; - gap: 10px; - flex-shrink: 0; + align-self: stretch; + max-width: none; + width: calc(100% - 32px); } -.transient-warning-countdown { - position: relative; - width: 28px; - height: 28px; - display: grid; - place-items: center; +.warning-rail .status-banner { + margin: 0; + max-width: 100%; } -.transient-warning-ring { - position: absolute; - inset: 0; - width: 28px; - height: 28px; - transform: rotate(-90deg); +.status-banner::before { + content: ""; + width: 2px; + min-width: 2px; + align-self: stretch; + border-radius: 999px; + background: currentColor; } -.transient-warning-ring-track, -.transient-warning-ring-progress { - fill: none; - stroke-width: 2.5; +.status-banner-warning { + color: var(--warning-text); + border-color: rgba(232, 152, 58, 0.18); + background: linear-gradient(180deg, rgba(232, 152, 58, 0.09), rgba(232, 152, 58, 0.04)); } -.transient-warning-ring-track { - stroke: rgba(243, 188, 114, 0.18); +.status-banner-error { + color: var(--red); + border-color: rgba(228, 88, 88, 0.18); + background: linear-gradient(180deg, rgba(228, 88, 88, 0.08), rgba(228, 88, 88, 0.04)); } -.transient-warning-ring-progress { - stroke: var(--warning-text); - stroke-linecap: round; - transition: stroke-dashoffset 120ms linear; +.status-banner-copy { + flex: 1; + min-width: 0; } -.transient-warning-count { - position: relative; - z-index: 1; - color: var(--warning-text); +.status-banner-countdown { + min-width: 22px; + padding-top: 1px; + color: currentColor; font-size: 11px; font-weight: 700; line-height: 1; + text-align: center; + opacity: 0.92; } -.transient-warning-close { - width: 18px; - height: 18px; - display: grid; - place-items: center; - border: none; - background: transparent; - color: rgba(243, 188, 114, 0.8); - padding: 0; - cursor: pointer; - transition: color 120ms ease, transform 120ms var(--ease); -} - -.transient-warning-close svg { - width: 14px; - height: 14px; +.status-banner-label { color: currentColor; + font-size: 9px; + font-weight: 600; + letter-spacing: 0.09em; + text-transform: uppercase; } -.transient-warning-close:hover { - color: #ffd39f; - transform: translateY(-1px); -} - -.error-banner { - margin: 8px 16px 0; - padding: 8px 10px 8px 14px; +.status-banner-text { + margin-top: 2px; + color: var(--t1); font-size: 12px; - color: var(--red); - background: var(--danger-bg); - border: 1px solid var(--danger-border); - border-radius: 12px; - display: flex; - align-items: flex-start; - gap: 8px; - max-height: min(30vh, 220px); + line-height: 1.35; + max-height: min(24vh, 160px); overflow-y: auto; } -.error-banner-text { - flex: 1; - min-width: 0; - line-height: 1.45; +.status-banner-text { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; } -.error-banner-close { - width: 20px; - height: 20px; +.status-banner-close { + width: 16px; + height: 16px; border: none; - border-radius: 6px; + border-radius: 4px; background: transparent; - color: var(--red); + color: currentColor; display: grid; place-items: center; cursor: pointer; flex-shrink: 0; - transition: background 100ms ease, color 100ms ease; + opacity: 0.76; + transition: background 100ms ease, opacity 100ms ease; } -.error-banner-close:hover { - background: rgba(228, 88, 88, 0.12); +.status-banner-close:hover { + background: rgba(255, 255, 255, 0.06); + opacity: 1; } -.error-banner-close svg { - width: 12px; - height: 12px; +.status-banner-close svg { + width: 10px; + height: 10px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; } -.error-banner-close:focus, -.error-banner-close:focus-visible { +.status-banner-close:focus, +.status-banner-close:focus-visible { outline: none; box-shadow: none; } @@ -2367,34 +2325,37 @@ button:focus-visible { min-width: 0; } .cap-badge { - display: flex; - align-items: center; - gap: 5px; + position: relative; + display: grid; + place-items: center; height: 24px; - padding: 0 8px; - border-radius: 999px; - border: 1px solid rgba(255, 255, 255, 0.06); - background: rgba(255, 255, 255, 0.02); - color: var(--text-dim); - opacity: 0.52; + width: 24px; + color: var(--t3); + opacity: 0.68; cursor: default; - transition: opacity 0.15s, color 0.15s, border-color 0.15s, background 0.15s; - white-space: nowrap; + transition: opacity 0.15s, color 0.15s; +} +.cap-badge svg { + width: 14px; + height: 14px; } .cap-badge.cap-active { - opacity: 0.9; + opacity: 0.96; color: var(--accent); - border-color: rgba(196, 164, 108, 0.22); - background: rgba(196, 164, 108, 0.08); +} +.cap-badge.cap-unsupported::after { + content: ""; + position: absolute; + width: 16px; + height: 1.5px; + border-radius: 999px; + background: currentColor; + transform: rotate(-42deg); + opacity: 0.95; } .cap-badge:hover { opacity: 1; } -.cap-badge-label { - font-size: 10px; - font-weight: 600; - letter-spacing: 0.01em; -} /* ── Toggle Switch ───────────────────────────────────── */ .toggle-switch { diff --git a/src/shared/contracts.ts b/src/shared/contracts.ts index b7fa89c..f5868f1 100644 --- a/src/shared/contracts.ts +++ b/src/shared/contracts.ts @@ -120,6 +120,11 @@ export interface ProviderStatus { export interface CloudModelCatalogItem { id: string; modes: string[]; + capabilities: { + image: boolean; + tools: boolean; + search: boolean; + }; } export interface CloudModelCatalogResult { diff --git a/test/main/ollamaProvider.test.ts b/test/main/ollamaProvider.test.ts new file mode 100644 index 0000000..c24dc2b --- /dev/null +++ b/test/main/ollamaProvider.test.ts @@ -0,0 +1,82 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { OllamaProvider } from "../../src/main/providers/ollamaProvider"; + +const ORIGINAL_FETCH = globalThis.fetch; + +function createStreamResponse(chunks: string[]): Response { + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + start(controller) { + for (const chunk of chunks) { + controller.enqueue(encoder.encode(chunk)); + } + controller.close(); + } + }); + + return new Response(stream, { + status: 200, + headers: { "Content-Type": "application/x-ndjson" } + }); +} + +test.afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; +}); + +test("ollama provider accepts buffered JSON chat replies", async () => { + globalThis.fetch = async () => + new Response( + JSON.stringify({ + message: { content: "hello from ollama" }, + done: true + }), + { + status: 200, + headers: { "Content-Type": "application/json" } + } + ); + + const provider = new OllamaProvider(); + const deltas: string[] = []; + const result = await provider.streamReply({ + baseUrl: "http://localhost:11434", + model: "llama3.2", + messages: [{ id: "1", role: "user", content: "hi", createdAt: "now" }], + onDelta: (delta) => { + deltas.push(delta); + } + }); + + assert.deepEqual(deltas, ["hello from ollama"]); + assert.deepEqual(result.toolCalls, []); +}); + +test("ollama provider accepts streaming ndjson chat replies", async () => { + globalThis.fetch = async () => + createStreamResponse([ + JSON.stringify({ + message: { content: "hello " }, + done: false + }) + "\n", + JSON.stringify({ + message: { content: "world" }, + done: true + }) + ]); + + const provider = new OllamaProvider(); + const deltas: string[] = []; + const result = await provider.streamReply({ + baseUrl: "http://localhost:11434", + model: "llama3.2", + messages: [{ id: "1", role: "user", content: "hi", createdAt: "now" }], + onDelta: (delta) => { + deltas.push(delta); + } + }); + + assert.deepEqual(deltas, ["hello ", "world"]); + assert.deepEqual(result.toolCalls, []); +}); diff --git a/test/main/providerServiceUtils.test.ts b/test/main/providerServiceUtils.test.ts index ceb5aa0..eb2296d 100644 --- a/test/main/providerServiceUtils.test.ts +++ b/test/main/providerServiceUtils.test.ts @@ -2,8 +2,11 @@ import assert from "node:assert/strict"; import test from "node:test"; import { ChatMessage } from "../../src/shared/contracts"; import { + extractUrls, + isWeatherQuery, parseActions, prepareMessagesForAPI, + requiresLiveWebSearch, truncateContext } from "../../src/main/providerServiceUtils"; @@ -77,3 +80,26 @@ test("truncateContext drops oldest messages but preserves at least four", () => ["3", "4", "5", "6"] ); }); + +test("extractUrls returns unique cleaned urls", () => { + assert.deepEqual( + extractUrls( + "Read https://example.com/foo, then compare with https://example.com/foo." + ), + ["https://example.com/foo"] + ); +}); + +test("requiresLiveWebSearch detects live/current-info prompts", () => { + assert.equal( + requiresLiveWebSearch("What is the weather in Calcutta right now?"), + true + ); + assert.equal(requiresLiveWebSearch("Give me the latest Apple news"), true); + assert.equal(requiresLiveWebSearch("Rewrite this paragraph"), false); +}); + +test("isWeatherQuery detects weather prompts only", () => { + assert.equal(isWeatherQuery("What is the temperature in Delhi?"), true); + assert.equal(isWeatherQuery("Summarize my meeting notes"), false); +}); From 07056ddd784a3705ae7ab4ddc27ffbd55e9a4fc9 Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Sun, 22 Mar 2026 02:02:09 +0530 Subject: [PATCH 7/7] more provider related fixes --- package-lock.json | 4 ++-- package.json | 2 +- src/main/providerServiceUtils.ts | 4 +--- src/main/providers/ollamaProvider.ts | 4 +--- src/renderer/App.tsx | 5 +++-- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index e9fa34c..a7e8edf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "robin-sidekick", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "robin-sidekick", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "@fontsource/capriola": "^5.2.7", diff --git a/package.json b/package.json index be19021..3e7a4e6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "robin-sidekick", "productName": "Robin", - "version": "0.3.0", + "version": "0.4.0", "description": "A mac-first menu bar sidekick for web-grounded chat and local AI.", "main": ".webpack/main", "scripts": { diff --git a/src/main/providerServiceUtils.ts b/src/main/providerServiceUtils.ts index 2a75b77..ab5e321 100644 --- a/src/main/providerServiceUtils.ts +++ b/src/main/providerServiceUtils.ts @@ -85,9 +85,7 @@ export function extractUrls(content: string): string[] { } export function isWeatherQuery(content: string): boolean { - return /\b(weather|forecast|temperature|rain|humidity|wind)\b/i.test( - content - ); + return /\b(weather|forecast|temperature|rain|humidity|wind)\b/i.test(content); } export function requiresLiveWebSearch(content: string): boolean { diff --git a/src/main/providers/ollamaProvider.ts b/src/main/providers/ollamaProvider.ts index 92154c9..26ee846 100644 --- a/src/main/providers/ollamaProvider.ts +++ b/src/main/providers/ollamaProvider.ts @@ -137,9 +137,7 @@ async function formatStreamingError( return `Ollama replied from ${location}, but the response was not streamed. Check the base URL and any proxy in front of Ollama.`; } -function mapOllamaToolCalls( - toolCalls?: OllamaToolCallPayload[] -): ToolCall[] { +function mapOllamaToolCalls(toolCalls?: OllamaToolCallPayload[]): ToolCall[] { if (!toolCalls?.length) { return []; } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 04c3447..c101d48 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1717,7 +1717,7 @@ export function App() { const modelCapabilities = parsed.mode === "local" ? NO_MODEL_CAPABILITIES - : selectedCloudModel?.capabilities ?? NO_MODEL_CAPABILITIES; + : (selectedCloudModel?.capabilities ?? NO_MODEL_CAPABILITIES); const cloudModeOptions: DropdownOption[] = ( selectedCloudModel?.modes ?? [] ).map((mode) => ({ @@ -2737,7 +2737,8 @@ export function App() { onDragStart={(event) => { event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", todo.id); - const row = event.currentTarget.closest(".todo-main-item"); + const row = + event.currentTarget.closest(".todo-main-item"); if (row instanceof HTMLElement) { event.dataTransfer.setDragImage(row, 20, 20); }