Skip to content
Open
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
18 changes: 18 additions & 0 deletions package-lock.json

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

49 changes: 38 additions & 11 deletions src/main/agent/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { createDeepAgent } from "deepagents"
import { getDefaultModel } from "../ipc/models"
import { getDefaultModel, AVAILABLE_MODELS } from "../ipc/models"
import { getApiKey, getThreadCheckpointPath } from "../storage"
import { ChatAnthropic } from "@langchain/anthropic"
import { ChatOpenAI } from "@langchain/openai"
Expand Down Expand Up @@ -65,8 +65,12 @@ function getModelInstance(
const model = modelId || getDefaultModel()
console.log("[Runtime] Using model:", model)

// Determine provider from model ID
if (model.startsWith("claude")) {
// Check if model is in our known cloud provider list
const knownModel = AVAILABLE_MODELS.find((m) => m.id === model || m.model === model)

// 1. ANTHROPIC
// Check known model OR fallback prefix check (for backwards compat)
if (knownModel?.provider === "anthropic" || (!knownModel && model.startsWith("claude"))) {
const apiKey = getApiKey("anthropic")
console.log("[Runtime] Anthropic API key present:", !!apiKey)
if (!apiKey) {
Expand All @@ -76,11 +80,20 @@ function getModelInstance(
model,
anthropicApiKey: apiKey
})
} else if (
model.startsWith("gpt") ||
model.startsWith("o1") ||
model.startsWith("o3") ||
model.startsWith("o4")
}

// 2. OPENAI
// Only use OpenAI provider if it's a configured cloud model OR matches known prefixes AND isn't excluded
else if (
knownModel?.provider === "openai" ||
(!knownModel &&
// More specific checks for official OpenAI models to avoid capturing local "gpt-*" models
((model.startsWith("gpt-") && !model.includes("oss") && !model.includes("local")) ||
model.startsWith("o1-") ||
model.startsWith("o1") ||
model.startsWith("o3-") ||
model.startsWith("o3") ||
model.startsWith("o4-")))
) {
const apiKey = getApiKey("openai")
console.log("[Runtime] OpenAI API key present:", !!apiKey)
Expand All @@ -91,7 +104,10 @@ function getModelInstance(
model,
openAIApiKey: apiKey
})
} else if (model.startsWith("gemini")) {
}

// 3. GOOGLE
else if (knownModel?.provider === "google" || (!knownModel && model.startsWith("gemini"))) {
const apiKey = getApiKey("google")
console.log("[Runtime] Google API key present:", !!apiKey)
if (!apiKey) {
Expand All @@ -103,8 +119,19 @@ function getModelInstance(
})
}

// Default to model string (let deepagents handle it)
return model
// 4. FALLBACK -> LOCAL / OLLAMA
else {
// Assume Ollama / Local OpenAI-compatible model
// This allows using models like "gpt-oss:...", "llama3", etc.
console.log("[Runtime] Using local/Ollama model:", model)
return new ChatOpenAI({
model,
configuration: {
baseURL: "http://127.0.0.1:11434/v1"
},
apiKey: "ollama" // Required by SDK but ignored by Ollama
})
}
}

export interface CreateAgentRuntimeOptions {
Expand Down
6 changes: 6 additions & 0 deletions src/main/agent/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Be concise and direct. Answer in fewer than 4 lines unless the user asks for det
After working on a file, just stop - don't explain what you did unless asked.
Avoid unnecessary introductions or conclusions.

**CRITICAL: When asked to write code, create an app, or modify files, YOU MUST USE THE write_file OR edit_file TOOLS. DO NOT just print the code in a markdown block. The user expects the files to be created on their disk.**

When you run non-trivial bash commands, briefly explain what they do.

## Proactiveness
Expand Down Expand Up @@ -99,6 +101,10 @@ Respect the user's decisions and work with them collaboratively.

## Todo List Management

When using the write_todos tool, ensure you use the correct schema:
write_todos(todos: { content: string, status: "pending" | "in_progress" | "completed" }[])
**IMPORTANT: Use "content" for the task description, NOT "task".**

When using the write_todos tool:
1. Keep the todo list MINIMAL - aim for 3-6 items maximum
2. Only create todos for complex, multi-step tasks that truly need tracking
Expand Down
46 changes: 43 additions & 3 deletions src/main/ipc/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const PROVIDERS: Omit<Provider, "hasApiKey">[] = [
]

// Available models configuration (updated Jan 2026)
const AVAILABLE_MODELS: ModelConfig[] = [
export const AVAILABLE_MODELS: ModelConfig[] = [
// Anthropic Claude 4.5 series (latest as of Jan 2026)
{
id: "claude-opus-4-5-20251101",
Expand Down Expand Up @@ -207,11 +207,34 @@ const AVAILABLE_MODELS: ModelConfig[] = [
export function registerModelHandlers(ipcMain: IpcMain): void {
// List available models
ipcMain.handle("models:list", async () => {
// Fetch Ollama models
let ollamaModels: ModelConfig[] = []
try {
const response = await fetch("http://127.0.0.1:11434/api/tags")
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = (await response.json()) as any
if (data && data.models) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ollamaModels = data.models.map((m: any) => ({
id: m.name,
name: m.name,
provider: "ollama",
model: m.name,
description: `Local Ollama model`,
available: true
}))
}
} catch (error) {
// Ollama not running or not reachable, just ignore
}

// Check which models have API keys configured
return AVAILABLE_MODELS.map((model) => ({
const standardModels = AVAILABLE_MODELS.map((model) => ({
...model,
available: hasApiKey(model.provider)
}))

return [...standardModels, ...ollamaModels]
})

// Get default model
Expand Down Expand Up @@ -241,10 +264,19 @@ export function registerModelHandlers(ipcMain: IpcMain): void {

// List providers with their API key status
ipcMain.handle("models:listProviders", async () => {
return PROVIDERS.map((provider) => ({
const providers = PROVIDERS.map((provider) => ({
...provider,
hasApiKey: hasApiKey(provider.id)
}))

// Add Ollama provider
providers.push({
id: "ollama" as any, // Cast to any if ProviderId type is strict in main process context
name: "Ollama (Local)",
hasApiKey: true
})

return providers
})

// Sync version info
Expand Down Expand Up @@ -357,9 +389,17 @@ export function registerModelHandlers(ipcMain: IpcMain): void {

// Recursively read directory
async function readDir(dirPath: string, relativePath: string = ""): Promise<void> {
// If dirPath doesn't exist, we can't read it
try {
await fs.access(dirPath)
} catch {
return
}

const entries = await fs.readdir(dirPath, { withFileTypes: true })

for (const entry of entries) {

// Skip hidden files and common non-project files
if (entry.name.startsWith(".") || entry.name === "node_modules") {
continue
Expand Down
25 changes: 17 additions & 8 deletions src/main/services/workspace-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import * as path from "path"
import { BrowserWindow } from "electron"

// Store active watchers by thread ID
const activeWatchers = new Map<string, fs.FSWatcher>()
const activeWatchers = new Map<string, { watcher: fs.FSWatcher; timer?: NodeJS.Timeout }>()

// Debounce timers to prevent rapid-fire updates
const debounceTimers = new Map<string, NodeJS.Timeout>()

const DEBOUNCE_DELAY = 500 // ms
const POLLING_INTERVAL = 4000 // 4s safety poll for reliable updates

/**
* Start watching a workspace directory for file changes.
Expand All @@ -31,8 +32,9 @@ export function startWatching(threadId: string, workspacePath: string): void {
}

try {
// 1. Native FS Watcher
// Use recursive watching (supported on macOS and Windows)
const watcher = fs.watch(workspacePath, { recursive: true }, (eventType, filename) => {
const fsWatcher = fs.watch(workspacePath, { recursive: true }, (eventType, filename) => {
// Skip hidden files and common non-project files
if (filename) {
const parts = filename.split(path.sep)
Expand All @@ -57,12 +59,18 @@ export function startWatching(threadId: string, workspacePath: string): void {
debounceTimers.set(threadId, timer)
})

watcher.on("error", (error) => {
fsWatcher.on("error", (error) => {
console.error(`[WorkspaceWatcher] Error watching ${workspacePath}:`, error)
stopWatching(threadId)
// Don't fully stop on error, the polling will pick it up
})

activeWatchers.set(threadId, watcher)
// 2. Safety Polling Interval
// Ensures UI updates even if fs.watch fails (common on network/virtual drives like A:\)
const pollTimer = setInterval(() => {
notifyRenderer(threadId, workspacePath)
}, POLLING_INTERVAL)

activeWatchers.set(threadId, { watcher: fsWatcher, timer: pollTimer })
console.log(`[WorkspaceWatcher] Started watching ${workspacePath} for thread ${threadId}`)
} catch (e) {
console.error(`[WorkspaceWatcher] Failed to start watching ${workspacePath}:`, e)
Expand All @@ -73,9 +81,10 @@ export function startWatching(threadId: string, workspacePath: string): void {
* Stop watching the workspace for a specific thread.
*/
export function stopWatching(threadId: string): void {
const watcher = activeWatchers.get(threadId)
if (watcher) {
watcher.close()
const active = activeWatchers.get(threadId)
if (active) {
active.watcher.close()
if (active.timer) clearInterval(active.timer)
activeWatchers.delete(threadId)
console.log(`[WorkspaceWatcher] Stopped watching for thread ${threadId}`)
}
Expand Down
Loading