From 79078cec5fd1e149e7e11b27e17985cfd29c9ca7 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 21 Jan 2026 09:08:13 -0800 Subject: [PATCH 01/11] Ensure all LLM providers can handle tool results --- backend/src/ai/fsm.rs | 25 +++++++++---------------- backend/src/ai/session.rs | 7 +------ 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/backend/src/ai/fsm.rs b/backend/src/ai/fsm.rs index 918b7801..8b5b3e90 100644 --- a/backend/src/ai/fsm.rs +++ b/backend/src/ai/fsm.rs @@ -257,23 +257,16 @@ impl Agent { } /// Push accumulated tool results to conversation as tool response messages. + /// Each tool response becomes a separate message with ChatRole::Tool. fn push_tool_results_to_conversation(&mut self) { - if !self.context.tool_results.is_empty() { - let tool_result_parts = self - .context - .tool_results - .drain(..) - .map(|result| { - let result_str = match result.output { - ToolOutput::Success(s) => s, - ToolOutput::Error(e) => format!("Error: {}", e), - }; - ContentPart::ToolResponse(ToolResponse::new(result.call_id, result_str)) - }) - .collect::>(); - let tool_result_content = MessageContent::from_parts(tool_result_parts); - let tool_result_message = ChatMessage::user(tool_result_content); - self.context.conversation.push(tool_result_message); + for result in self.context.tool_results.drain(..) { + let result_str = match result.output { + ToolOutput::Success(s) => s, + ToolOutput::Error(e) => format!("Error: {}", e), + }; + let tool_response = ToolResponse::new(result.call_id, result_str); + // ChatMessage::from(ToolResponse) creates a message with ChatRole::Tool + self.context.conversation.push(ChatMessage::from(tool_response)); } } diff --git a/backend/src/ai/session.rs b/backend/src/ai/session.rs index f1308cd4..a3a3a396 100644 --- a/backend/src/ai/session.rs +++ b/backend/src/ai/session.rs @@ -598,14 +598,9 @@ impl AISession { Ok(ChatStreamEvent::ThoughtSignatureChunk(_)) => { log::trace!("Session {} received thought signature chunk", session_id,); } - Ok(ChatStreamEvent::ToolCallChunk(tc_chunk)) => { + Ok(ChatStreamEvent::ToolCallChunk(_tc_chunk)) => { // Tool call chunks are accumulated by genai internally // We'll get the complete tool calls in the End event - log::trace!( - "Session {} received tool call chunk: {:?}", - session_id, - tc_chunk - ); } Ok(ChatStreamEvent::ReasoningChunk(_)) => { log::trace!("Session {} received reasoning chunk", session_id); From afc21055a916d540f46b4353d7670a832c041de2 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 21 Jan 2026 09:15:39 -0800 Subject: [PATCH 02/11] Allow using Ollama for AI model --- backend/src/ai/session.rs | 34 +++-- backend/src/ai/types.rs | 2 +- backend/src/commands/ai.rs | 3 +- src/components/Settings/Settings.tsx | 131 +++++++++++++++--- .../runbooks/editor/ui/AIAssistant.tsx | 21 ++- src/lib/ai/commands.ts | 4 +- src/lib/ai/useAIChat.ts | 7 +- src/state/settings.ts | 24 ++++ src/state/settings_ai.ts | 62 +++++++++ 9 files changed, 251 insertions(+), 37 deletions(-) create mode 100644 src/state/settings_ai.ts diff --git a/backend/src/ai/session.rs b/backend/src/ai/session.rs index a3a3a396..512ffbc8 100644 --- a/backend/src/ai/session.rs +++ b/backend/src/ai/session.rs @@ -117,7 +117,17 @@ impl SessionHandle { } /// Send a user message to the session. - pub async fn send_user_message(&self, content: String) -> Result<(), AISessionError> { + pub async fn send_user_message( + &self, + content: String, + model: ModelSelection, + ) -> Result<(), AISessionError> { + let msg = Event::ModelChange(model); + self.event_tx + .send(msg) + .await + .map_err(|_| AISessionError::ChannelClosed)?; + let msg = ChatMessage::user(content); self.event_tx .send(Event::UserMessage(msg)) @@ -204,12 +214,16 @@ fn resolve_service_target( } }; - let auth = AuthData::Key( - AISession::get_api_key(adapter_kind, parts[0] == "atuinhub") - .await - .map_err(|e| genai::resolver::Error::Custom(e.to_string()))?, - ); - service_target.auth = auth; + let key = AISession::get_api_key(adapter_kind, parts[0] == "atuinhub") + .await + .map_err(|e| genai::resolver::Error::Custom(e.to_string()))?; + + if let Some(key) = key { + let auth = AuthData::Key(key); + service_target.auth = auth; + } else { + service_target.auth = AuthData::Key("".to_string()); + } let model_id = ModelIden::new(adapter_kind, parts[1]); service_target.model = model_id; @@ -354,12 +368,12 @@ impl AISession { async fn get_api_key( _adapter_kind: AdapterKind, is_hub: bool, - ) -> Result { + ) -> Result, AISessionError> { if is_hub { - return Ok("".to_string()); + return Ok(None); } - Ok("".to_string()) + Ok(None) } /// Run the session event loop. diff --git a/backend/src/ai/types.rs b/backend/src/ai/types.rs index ccbc0621..678eafb8 100644 --- a/backend/src/ai/types.rs +++ b/backend/src/ai/types.rs @@ -28,7 +28,7 @@ impl fmt::Display for ModelSelection { }, ModelSelection::Ollama { model, uri } => match uri { Some(uri) => write!(f, "ollama::{model}::{}", uri.deref()), - None => write!(f, "ollama::{model}::default"), + None => write!(f, "ollama::{model}::http://localhost:11434/v1"), }, } } diff --git a/backend/src/commands/ai.rs b/backend/src/commands/ai.rs index 988fa5d3..4d265753 100644 --- a/backend/src/commands/ai.rs +++ b/backend/src/commands/ai.rs @@ -300,6 +300,7 @@ pub async fn ai_send_message( state: tauri::State<'_, AtuinState>, session_id: Uuid, message: String, + model: ModelSelection, ) -> Result<(), String> { let sessions = state.ai_sessions.read().await; let handle = sessions @@ -307,7 +308,7 @@ pub async fn ai_send_message( .ok_or_else(|| format!("Session {} not found", session_id))?; handle - .send_user_message(message) + .send_user_message(message, model) .await .map_err(|e| e.to_string()) } diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index b3ab365e..2f37b193 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -39,6 +39,7 @@ import handleDeepLink from "@/routes/root/deep"; import * as api from "@/api/api"; import InterpreterSelector from "@/lib/blocks/common/InterpreterSelector"; import AtuinEnv from "@/atuin_env"; +import { OllamaSettings, useAIProviderSettings } from "@/state/settings_ai"; async function loadFonts(): Promise { const fonts = await invoke("list_fonts"); @@ -49,7 +50,7 @@ async function loadFonts(): Promise { } // Custom hook for managing settings -const useSettingsState = ( +export const useSettingsState = ( _key: any, initialValue: any, settingsGetter: any, @@ -109,6 +110,7 @@ interface SettingsSwitchProps { onValueChange: (e: boolean) => void; description: string; className?: string; + isDisabled?: boolean; } const SettingSwitch = ({ @@ -117,11 +119,13 @@ const SettingSwitch = ({ onValueChange, description, className, + isDisabled, }: SettingsSwitchProps) => (
{label} @@ -1190,29 +1194,122 @@ const AISettings = () => { const setAiEnabled = useStore((state) => state.setAiEnabled); const setAiShareContext = useStore((state) => state.setAiShareContext); + return ( + <> + + +

AI

+

+ Configure AI-powered features in runbooks +

+ + + + {aiEnabled && ( + + )} +
+
+ {aiEnabled && ( + <> + + + + )} + + ); +}; + +const AgentSettings = () => { + const providers = [ + ["Atuin Hub", "atuinhub"], + ["Ollama", "ollama"] + ] + + const [aiProvider, setAiProvider, aiProviderLoading] = useSettingsState( + "ai_provider", + "atuinhub", + Settings.aiAgentProvider, + Settings.aiAgentProvider, + ); + + const handleProviderChange = (keys: SharedSelection) => { + const key = keys.currentKey as string; + if (key) { + setAiProvider(key); + } + }; + return ( -

AI

-

- Configure AI-powered features in runbooks -

+

AI Agent

+ + +
+
+ ); +}; + +const AIOllamaSettings = () => { + const [ollamaSettings, setOllamaSettings, isLoading] = useAIProviderSettings("ollama", { + enabled: false, + endpoint: "http://localhost:11434", + model: "", + }); + + return ( + + +

Ollama

setOllamaSettings({ ...ollamaSettings, enabled })} + description="Toggle to use Ollama as the AI provider." /> - {aiEnabled && ( - + {ollamaSettings.enabled && ( +
+ setOllamaSettings({ ...ollamaSettings, endpoint: value })} + isDisabled={isLoading} + /> + + setOllamaSettings({ ...ollamaSettings, model: value })} + isDisabled={isLoading} + /> +
)}
diff --git a/src/components/runbooks/editor/ui/AIAssistant.tsx b/src/components/runbooks/editor/ui/AIAssistant.tsx index c6396435..341752fc 100644 --- a/src/components/runbooks/editor/ui/AIAssistant.tsx +++ b/src/components/runbooks/editor/ui/AIAssistant.tsx @@ -46,6 +46,8 @@ import { Settings } from "@/state/settings"; import { useStore } from "@/state/store"; import { ChargeTarget } from "@/rs-bindings/ChargeTarget"; import AtuinEnv from "@/atuin_env"; +import { getAIProviderSettings, getModelSelection } from "@/state/settings_ai"; +import { DialogBuilder } from "@/components/Dialogs/dialog"; const ALL_TOOL_NAMES = [ "get_runbook_document", @@ -628,11 +630,24 @@ export default function AIAssistant({ } }, [isOpen, sessionId]); - const handleSend = useCallback(() => { + const handleSend = useCallback(async () => { if (!inputValue.trim() || isStreaming || !sessionId) return; - // TODO: Allow buffering one message while streaming - sendMessage(inputValue.trim()); + + const input = inputValue.trim(); setInputValue(""); + + const aiProvider = await Settings.aiAgentProvider(); + const modelSelection = await getModelSelection(aiProvider); + if (modelSelection.isNone()) { + await new DialogBuilder().title("AI Provider Not Configured") + .message("Please configure your selected AI provider in the settings.") + .action({ label: "OK", value: undefined, variant: "flat" }) + .build(); + return; + } + + // TODO: Allow buffering one message while streaming + sendMessage(input, modelSelection.unwrap()); }, [inputValue, isStreaming, sessionId, sendMessage]); const handleKeyDown = (e: React.KeyboardEvent) => { diff --git a/src/lib/ai/commands.ts b/src/lib/ai/commands.ts index 83ef10fc..da6afd9f 100644 --- a/src/lib/ai/commands.ts +++ b/src/lib/ai/commands.ts @@ -70,8 +70,8 @@ export async function changeUser(sessionId: string, user: string): Promise /** * Send a user message to an AI session. */ -export async function sendMessage(sessionId: string, message: string): Promise { - await invoke("ai_send_message", { sessionId, message }); +export async function sendMessage(sessionId: string, message: string, model?: ModelSelection): Promise { + await invoke("ai_send_message", { sessionId, message, model }); } /** diff --git a/src/lib/ai/useAIChat.ts b/src/lib/ai/useAIChat.ts index 76aa7bce..26ab2fe9 100644 --- a/src/lib/ai/useAIChat.ts +++ b/src/lib/ai/useAIChat.ts @@ -9,6 +9,7 @@ import { } from "./commands"; import { useCallback, useEffect, useMemo, useState } from "react"; import { State } from "@/rs-bindings/State"; +import { ModelSelection } from "@/rs-bindings/ModelSelection"; export interface AIChatAPI { sessionId: string; @@ -19,7 +20,7 @@ export interface AIChatAPI { pendingToolCalls: Array; error: string | null; state: State["type"]; - sendMessage: (message: string) => void; + sendMessage: (message: string, model?: ModelSelection) => void; addToolOutput: (output: AIToolOutput) => void; cancel: () => void; } @@ -153,7 +154,7 @@ export default function useAIChat(sessionId: string): AIChatAPI { }, [sessionId]); const sendMessage = useCallback( - async (message: string) => { + async (message: string, model?: ModelSelection) => { const userMessage: AIMessage = { role: "user", content: { parts: [{ type: "text", data: message }] }, @@ -168,7 +169,7 @@ export default function useAIChat(sessionId: string): AIChatAPI { } setError(null); - await sendMessageCommand(sessionId, message); + await sendMessageCommand(sessionId, message, model); }, [sessionId, isIdle], ); diff --git a/src/state/settings.ts b/src/state/settings.ts index 76c09470..b9cdfc78 100644 --- a/src/state/settings.ts +++ b/src/state/settings.ts @@ -40,6 +40,8 @@ const NOTIFICATIONS_SERIAL_PAUSED_DURATION = "settings.notifications.serial.paus const NOTIFICATIONS_SERIAL_PAUSED_SOUND = "settings.notifications.serial.paused.sound"; const NOTIFICATIONS_SERIAL_PAUSED_OS = "settings.notifications.serial.paused.os"; +const AI_AGENT_PROVIDER = "settings.ai.agent.provider"; + export class Settings { public static DEFAULT_FONT = "FiraCode"; public static DEFAULT_FONT_SIZE = 14; @@ -407,4 +409,26 @@ export class Settings { } return await this.getSystemDefaultShell(); } + + public static async aiAgentProvider(val: string | null = null): Promise { + let store = await KVStore.open_default(); + + if (val !== null) { + await store.set(AI_AGENT_PROVIDER, val); + return val; + } + + return await store.get(AI_AGENT_PROVIDER) ?? "atuinhub"; + } + + public static async aiProviderSettings>(provider: string, settings: T | null = null): Promise { + let store = await KVStore.open_default(); + + if (settings !== null) { + await store.set(`ai.provider.${provider}.settings`, settings); + return settings; + } + + return await store.get(`ai.provider.${provider}.settings`) ?? {} as T; + } } diff --git a/src/state/settings_ai.ts b/src/state/settings_ai.ts new file mode 100644 index 00000000..0da5ec61 --- /dev/null +++ b/src/state/settings_ai.ts @@ -0,0 +1,62 @@ +import AtuinEnv from "@/atuin_env"; +import { useSettingsState } from "@/components/Settings/Settings"; +import { ModelSelection } from "@/rs-bindings/ModelSelection"; +import { Settings } from "@/state/settings"; + +export interface OllamaSettings { + enabled: boolean; + endpoint: string; + model: string; +} + +export function useAIProviderSettings>(provider: string, defaultValue: T): [T, (settings: T) => void, boolean] { + const [settings, setSettings, isLoading] = useSettingsState( + `ai.provider.${provider}.settings`, + defaultValue as T, + () => Settings.aiProviderSettings(provider), + (settings: T) => Settings.aiProviderSettings(provider, settings), + ); + return [settings, setSettings, isLoading]; +}; + +export async function getAIProviderSettings>(provider: string): Promise { + const value = await Settings.aiProviderSettings(provider); + return value as T; +} + +export async function getModelSelection(provider: string): Promise> { + if (provider === "atuinhub") { + return Some({ + type: "atuinHub", + data: { + model: "claude-opus-4-5-20251101", + uri: AtuinEnv.url("/api/ai/proxy/"), + } + }) as Option + } else if (provider === "ollama") { + const settings = await getAIProviderSettings("ollama"); + if (!settings.enabled) { + return None + } + + return Some({ + type: "ollama", + data: { + model: settings.model, + uri: joinUrlParts([settings.endpoint, "v1/"]), + } + }) as Option + } else { + return Some({ + type: "atuinHub", + data: { + model: "claude-opus-4-5-20251101", + uri: AtuinEnv.url("/api/ai/proxy/"), + } + }) as Option + } +} + +function joinUrlParts(parts: string[]): string { + return parts.join("/").replace(/\/+/g, "/"); +} From 1e877136f0bf6d05f09fe7285992903a463abfa4 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 21 Jan 2026 09:30:35 -0800 Subject: [PATCH 03/11] Fix LLM FSM test --- backend/src/ai/fsm.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/ai/fsm.rs b/backend/src/ai/fsm.rs index 8b5b3e90..7e2771f5 100644 --- a/backend/src/ai/fsm.rs +++ b/backend/src/ai/fsm.rs @@ -266,7 +266,9 @@ impl Agent { }; let tool_response = ToolResponse::new(result.call_id, result_str); // ChatMessage::from(ToolResponse) creates a message with ChatRole::Tool - self.context.conversation.push(ChatMessage::from(tool_response)); + self.context + .conversation + .push(ChatMessage::from(tool_response)); } } @@ -817,7 +819,7 @@ mod tests { assert_eq!(t.effects, vec![Effect::Cancelled]); assert!(agent.context().pending_tools.is_empty()); // Tool results were pushed to conversation as error responses - // Conversation: user msg, assistant msg, tool response (with 2 cancelled results) - assert_eq!(agent.context().conversation.len(), 3); + // Conversation: user msg, assistant msg, tool response, tool response + assert_eq!(agent.context().conversation.len(), 4); } } From e8d209b53434ed5082b33a6aaea27638c28a2f34 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 21 Jan 2026 09:33:18 -0800 Subject: [PATCH 04/11] Remove unused import --- src/components/runbooks/editor/ui/AIAssistant.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/runbooks/editor/ui/AIAssistant.tsx b/src/components/runbooks/editor/ui/AIAssistant.tsx index 341752fc..f46f910a 100644 --- a/src/components/runbooks/editor/ui/AIAssistant.tsx +++ b/src/components/runbooks/editor/ui/AIAssistant.tsx @@ -46,7 +46,7 @@ import { Settings } from "@/state/settings"; import { useStore } from "@/state/store"; import { ChargeTarget } from "@/rs-bindings/ChargeTarget"; import AtuinEnv from "@/atuin_env"; -import { getAIProviderSettings, getModelSelection } from "@/state/settings_ai"; +import { getModelSelection } from "@/state/settings_ai"; import { DialogBuilder } from "@/components/Dialogs/dialog"; const ALL_TOOL_NAMES = [ From 7e4ae6beb6950e36d1d3bd0643a0ee85d2ecb8dd Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 21 Jan 2026 11:28:26 -0800 Subject: [PATCH 05/11] Update genai fork dependency version --- Cargo.lock | 2 +- backend/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6df38e98..884ae4b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3213,7 +3213,7 @@ dependencies = [ [[package]] name = "genai" version = "0.5.0-alpha.10-WIP" -source = "git+https://github.com/BinaryMuse/rust-genai?rev=674535905a966b44104a327dd1e2ca80f4b4a444#674535905a966b44104a327dd1e2ca80f4b4a444" +source = "git+https://github.com/BinaryMuse/rust-genai?rev=ce7feec4ae112cc9ad442841c6b49961a599580e#ce7feec4ae112cc9ad442841c6b49961a599580e" dependencies = [ "base64 0.22.1", "bytes", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index a70ef8e0..583bbd83 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -88,7 +88,7 @@ log = { workspace = true } dirs = { workspace = true } tempfile = { workspace = true } config = "0.15.19" -genai = { git = "https://github.com/BinaryMuse/rust-genai", rev = "674535905a966b44104a327dd1e2ca80f4b4a444" } +genai = { git = "https://github.com/BinaryMuse/rust-genai", rev = "ce7feec4ae112cc9ad442841c6b49961a599580e" } futures-util = "0.3.31" indoc = "2.0.7" From 02551ea51a071a7bc7f8bcd6b112f33f83de23e3 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 21 Jan 2026 11:35:29 -0800 Subject: [PATCH 06/11] Improve error message when AI provider isn't setup properly --- .../runbooks/editor/ui/AIAssistant.tsx | 9 ++++++--- src/state/settings_ai.ts | 19 +++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/components/runbooks/editor/ui/AIAssistant.tsx b/src/components/runbooks/editor/ui/AIAssistant.tsx index f46f910a..bc0090e0 100644 --- a/src/components/runbooks/editor/ui/AIAssistant.tsx +++ b/src/components/runbooks/editor/ui/AIAssistant.tsx @@ -638,9 +638,12 @@ export default function AIAssistant({ const aiProvider = await Settings.aiAgentProvider(); const modelSelection = await getModelSelection(aiProvider); - if (modelSelection.isNone()) { - await new DialogBuilder().title("AI Provider Not Configured") - .message("Please configure your selected AI provider in the settings.") + if (modelSelection.isErr()) { + const err = modelSelection.unwrapErr(); + await new DialogBuilder() + .title("AI Provider Error") + .icon("error") + .message("There was an error setting up your selected AI provider: " + err) .action({ label: "OK", value: undefined, variant: "flat" }) .build(); return; diff --git a/src/state/settings_ai.ts b/src/state/settings_ai.ts index 0da5ec61..f27834e8 100644 --- a/src/state/settings_ai.ts +++ b/src/state/settings_ai.ts @@ -24,36 +24,39 @@ export async function getAIProviderSettings>(provi return value as T; } -export async function getModelSelection(provider: string): Promise> { +export async function getModelSelection(provider: string): Promise> { if (provider === "atuinhub") { - return Some({ + return Ok({ type: "atuinHub", data: { model: "claude-opus-4-5-20251101", uri: AtuinEnv.url("/api/ai/proxy/"), } - }) as Option + }) as Result } else if (provider === "ollama") { const settings = await getAIProviderSettings("ollama"); if (!settings.enabled) { - return None + return Err("Ollama is not enabled in settings"); + } + if (!settings.model) { + return Err("Ollama model is not set in settings"); } - return Some({ + return Ok({ type: "ollama", data: { model: settings.model, uri: joinUrlParts([settings.endpoint, "v1/"]), } - }) as Option + }) as Result } else { - return Some({ + return Ok({ type: "atuinHub", data: { model: "claude-opus-4-5-20251101", uri: AtuinEnv.url("/api/ai/proxy/"), } - }) as Option + }) as Result } } From ddc96b8c62c4c87afd772c4c304d3a3ecf8bc276 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 21 Jan 2026 11:40:37 -0800 Subject: [PATCH 07/11] Fix URL joining in AI settings Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/state/settings_ai.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state/settings_ai.ts b/src/state/settings_ai.ts index 0da5ec61..e3503ac6 100644 --- a/src/state/settings_ai.ts +++ b/src/state/settings_ai.ts @@ -58,5 +58,5 @@ export async function getModelSelection(provider: string): Promise p.replace(/\/+$/, '')).join('/').replace(/([^:]\/)\/+/g, '$1'); } From 71f98b28f42b7255a71173ee0f4a90d276220be7 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 21 Jan 2026 11:44:57 -0800 Subject: [PATCH 08/11] Re-add trace debug message --- backend/src/ai/session.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/ai/session.rs b/backend/src/ai/session.rs index 512ffbc8..2d3cbc35 100644 --- a/backend/src/ai/session.rs +++ b/backend/src/ai/session.rs @@ -615,6 +615,7 @@ impl AISession { Ok(ChatStreamEvent::ToolCallChunk(_tc_chunk)) => { // Tool call chunks are accumulated by genai internally // We'll get the complete tool calls in the End event + log::trace!("Session {} received tool call chunk", session_id); } Ok(ChatStreamEvent::ReasoningChunk(_)) => { log::trace!("Session {} received reasoning chunk", session_id); From f5c3b4eac4700cbc7533393761ed43ba22b4dc6b Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 21 Jan 2026 11:45:31 -0800 Subject: [PATCH 09/11] Don't include 'v1/' in default Ollama endpoint --- backend/src/ai/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ai/types.rs b/backend/src/ai/types.rs index 678eafb8..48d8a926 100644 --- a/backend/src/ai/types.rs +++ b/backend/src/ai/types.rs @@ -28,7 +28,7 @@ impl fmt::Display for ModelSelection { }, ModelSelection::Ollama { model, uri } => match uri { Some(uri) => write!(f, "ollama::{model}::{}", uri.deref()), - None => write!(f, "ollama::{model}::http://localhost:11434/v1"), + None => write!(f, "ollama::{model}::http://localhost:11434"), }, } } From 283e204044ee4f6af2d6ed8e714acf12880a1f8b Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 21 Jan 2026 11:50:20 -0800 Subject: [PATCH 10/11] Update AI settings labels --- src/components/Settings/Settings.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index 2f37b193..ddc9b2df 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -1295,7 +1295,7 @@ const AIOllamaSettings = () => { {ollamaSettings.enabled && (
setOllamaSettings({ ...ollamaSettings, endpoint: value })} @@ -1303,7 +1303,7 @@ const AIOllamaSettings = () => { /> setOllamaSettings({ ...ollamaSettings, model: value })} @@ -1332,7 +1332,7 @@ const UserSettings = () => { const deepLink = `atuin://register-token/${token}`; // token submit deep link doesn't require a runbook activation, // so passing an empty function for simplicity - handleDeepLink(deepLink, () => {}); + handleDeepLink(deepLink, () => { }); } let content; From 5fb5ea2019c629f5697e3869ecd789ae42f3cc13 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 21 Jan 2026 12:17:04 -0800 Subject: [PATCH 11/11] Update Ollama settings wording --- src/components/Settings/Settings.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index ddc9b2df..13fc05d8 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -1257,11 +1257,11 @@ const AgentSettings = () => {

AI Agent

setOllamaSettings({ ...ollamaSettings, model: value })}