Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
84 changes: 81 additions & 3 deletions src/main/providerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ import { TodoContextProvider } from "./context/todoProvider";
import { NotesContextProvider } from "./context/notesProvider";
import { buildSystemPrompt } from "./context/assembler";
import {
extractUrls,
isWeatherQuery,
parseActions,
prepareMessagesForAPI,
requiresLiveWebSearch,
truncateContext
} from "./providerServiceUtils";
import { ToolRound, StreamReplyResult } from "./tools/types";
Expand Down Expand Up @@ -99,6 +102,8 @@ const MODEL_SETUP_WARNING =
"You need to configure a model to use Robin. Download a local model or add a cloud provider key in Settings.";
const LOCAL_IMAGE_UNSUPPORTED_WARNING =
"Image input is not supported in Local mode yet. Switch to a cloud model.";
const LOCAL_LIVE_DATA_SEARCH_REQUIRED_WARNING =
"Local mode needs Web Search enabled with a Brave Search API key for live/current questions. Enable it in Settings or switch to a cloud search model.";

export class ProviderService {
private readonly ollama = new OllamaProvider();
Expand Down Expand Up @@ -529,6 +534,9 @@ export class ProviderService {
]);
const toolExecutors = buildToolExecutors(braveApiKey, settings.toolToggles);
const toolDefs = getToolDefinitions(toolExecutors);
const toolExecutorByName = new Map(
toolExecutors.map((executor) => [executor.definition.name, executor])
);

const onDelta = (delta: string) => {
assistantMessage.content += delta;
Expand Down Expand Up @@ -672,6 +680,70 @@ export class ProviderService {
if ((request.attachments?.length ?? 0) > 0) {
throw new Error(LOCAL_IMAGE_UNSUPPORTED_WARNING);
}
const localPrompt = request.prompt.trim();
const explicitUrls = extractUrls(localPrompt);
const needsLiveData = requiresLiveWebSearch(localPrompt);
const supplementalSections: string[] = [];

if (explicitUrls.length > 0) {
const fetchUrlExecutor = toolExecutorByName.get("fetch_url");
if (!fetchUrlExecutor) {
throw new Error(
"Local mode cannot read shared links because Fetch URL is disabled in Settings."
);
}

emitToolStatus("fetch_url", "calling");
const fetchedContent = await fetchUrlExecutor.execute({
url: explicitUrls[0]
});
emitToolStatus("fetch_url", "complete");

if (fetchedContent.startsWith("Error:")) {
throw new Error(fetchedContent.replace(/^Error:\s*/i, ""));
}

supplementalSections.push(
[
`## Source Content (${explicitUrls[0]})`,
"Use this fetched page content when answering the user's question.",
fetchedContent
].join("\n")
);
}

if (needsLiveData) {
const webSearchExecutor = toolExecutorByName.get("web_search");
if (!webSearchExecutor) {
throw new Error(LOCAL_LIVE_DATA_SEARCH_REQUIRED_WARNING);
}

emitToolStatus("web_search", "calling");
const searchResults = await webSearchExecutor.execute({
query: localPrompt,
count: isWeatherQuery(localPrompt) ? 3 : 5
});
emitToolStatus("web_search", "complete");

if (searchResults.startsWith("Error:")) {
throw new Error(searchResults.replace(/^Error:\s*/i, ""));
}

supplementalSections.push(
[
"## Live Web Context",
"Use these search results as the authoritative source for current facts.",
searchResults
].join("\n")
);

if (isWeatherQuery(localPrompt)) {
supplementalSections.push(
"## Weather Guard\nWhen the local time is nighttime, do not describe current conditions as sunny. Prefer neutral wording like clear, warm, humid, cloudy, or rainy unless the source explicitly describes the current moment otherwise."
);
}
}

const ollamaStatus = await this.ollama.detect(
providers.ollama.baseUrl,
providers.ollama.model || undefined
Expand Down Expand Up @@ -699,7 +771,10 @@ export class ProviderService {
(message) => message.id !== assistantMessage.id
)
),
systemPrompt: systemPrompt || undefined,
systemPrompt:
[systemPrompt, ...supplementalSections]
.filter((section) => Boolean(section && section.trim()))
.join("\n\n") || undefined,
tools: toolDefs.length > 0 ? toolDefs : undefined,
toolHistory: toolHistory.length > 0 ? toolHistory : undefined,
onDelta
Expand Down Expand Up @@ -760,8 +835,11 @@ export class ProviderService {
});
} catch (error) {
assistantMessage.status = "error";
assistantMessage.content =
assistantMessage.content || "I hit a snag before I could finish that.";
if (assistantMessage.content.trim().length === 0) {
thread.messages = thread.messages.filter(
(message) => message.id !== assistantMessage.id
);
}
thread.updatedAt = isoNow();
await this.storage.upsertThread(thread);
emit({
Expand Down
28 changes: 28 additions & 0 deletions src/main/providerServiceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,31 @@ export function truncateContext(

return result;
}

export function extractUrls(content: string): string[] {
const matches = content.match(/https?:\/\/[^\s)]+/gi) ?? [];
return Array.from(
new Set(matches.map((entry) => entry.trim().replace(/[.,!?;:]+$/, "")))
);
}

export function isWeatherQuery(content: string): boolean {
return /\b(weather|forecast|temperature|rain|humidity|wind)\b/i.test(content);
}

export function requiresLiveWebSearch(content: string): boolean {
const normalized = content.trim().toLowerCase();
if (!normalized) {
return false;
}

if (
/\b(latest|current|currently|right now|today|tonight|this morning|this evening|now|news|forecast|live score|stock price|price today)\b/.test(
normalized
)
) {
return true;
}

return isWeatherQuery(normalized);
}
90 changes: 75 additions & 15 deletions src/main/providers/curatedCloudModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,85 @@ export const CURATED_CLOUD_MODELS: Record<
CloudModelCatalogItem[]
> = {
openai: [
{ id: "gpt-5.2-codex", modes: ["low", "medium", "high", "xhigh"] },
{ id: "gpt-5.2", modes: ["none", "low", "medium", "high", "xhigh"] },
{ id: "gpt-5.2-mini", modes: ["none", "low", "medium", "high", "xhigh"] },
{ id: "gpt-5.2-nano", modes: ["none", "low", "medium", "high", "xhigh"] },
{ id: "gpt-5.1", modes: ["none", "low", "medium", "high"] },
{ id: "gpt-5.1-mini", modes: ["none", "low", "medium", "high"] },
{ id: "gpt-5.1-nano", modes: ["none", "low", "medium", "high"] },
{ id: "gpt-5-pro", modes: ["high"] },
{ id: "o4-mini", modes: ["low", "medium", "high"] },
{ id: "o3", modes: ["low", "medium", "high"] }
{
id: "gpt-5.2-codex",
modes: ["low", "medium", "high", "xhigh"],
capabilities: { image: true, tools: true, search: false }
},
{
id: "gpt-5.2",
modes: ["none", "low", "medium", "high", "xhigh"],
capabilities: { image: true, tools: true, search: false }
},
{
id: "gpt-5.2-mini",
modes: ["none", "low", "medium", "high", "xhigh"],
capabilities: { image: true, tools: true, search: false }
},
{
id: "gpt-5.2-nano",
modes: ["none", "low", "medium", "high", "xhigh"],
capabilities: { image: true, tools: true, search: false }
},
{
id: "gpt-5.1",
modes: ["none", "low", "medium", "high"],
capabilities: { image: true, tools: true, search: false }
},
{
id: "gpt-5.1-mini",
modes: ["none", "low", "medium", "high"],
capabilities: { image: true, tools: true, search: false }
},
{
id: "gpt-5.1-nano",
modes: ["none", "low", "medium", "high"],
capabilities: { image: true, tools: true, search: false }
},
{
id: "gpt-5-pro",
modes: ["high"],
capabilities: { image: true, tools: true, search: false }
},
{
id: "o4-mini",
modes: ["low", "medium", "high"],
capabilities: { image: true, tools: true, search: false }
},
{
id: "o3",
modes: ["low", "medium", "high"],
capabilities: { image: true, tools: true, search: false }
}
],
google: [
{ id: "gemini-2.5-pro", modes: [] },
{ id: "gemini-2.5-flash", modes: [] },
{ id: "gemini-2.5-flash-lite", modes: [] }
{
id: "gemini-2.5-pro",
modes: [],
capabilities: { image: true, tools: true, search: false }
},
{
id: "gemini-2.5-flash",
modes: [],
capabilities: { image: true, tools: true, search: false }
},
{
id: "gemini-2.5-flash-lite",
modes: [],
capabilities: { image: true, tools: true, search: false }
}
],
perplexity: [
{ id: "sonar-pro", modes: [] },
{ id: "sonar", modes: [] }
{
id: "sonar-pro",
modes: [],
capabilities: { image: false, tools: false, search: true }
},
{
id: "sonar",
modes: [],
capabilities: { image: false, tools: false, search: true }
}
],
openrouter: []
};
Loading
Loading