diff --git a/apps/desktop/src/main/todo-agent/session-store.ts b/apps/desktop/src/main/todo-agent/session-store.ts index 179d51531b3..3476449b30a 100644 --- a/apps/desktop/src/main/todo-agent/session-store.ts +++ b/apps/desktop/src/main/todo-agent/session-store.ts @@ -287,6 +287,7 @@ class TodoSessionStore { agentKind?: string | null; codexModel?: string | null; codexEffort?: string | null; + crushModel?: string | null; remoteControlEnabled?: boolean; artifactPath: string; }): SelectTodoSession { @@ -317,6 +318,7 @@ class TodoSessionStore { agentKind: template.agentKind ?? "claude", codexModel: template.codexModel ?? null, codexEffort: template.codexEffort ?? null, + crushModel: template.crushModel ?? null, remoteControlEnabled: template.remoteControlEnabled ?? false, verdictPassed: null, verdictReason: null, diff --git a/apps/desktop/src/main/todo-agent/settings.ts b/apps/desktop/src/main/todo-agent/settings.ts index fea368ab2ac..5f5bd6a551b 100644 --- a/apps/desktop/src/main/todo-agent/settings.ts +++ b/apps/desktop/src/main/todo-agent/settings.ts @@ -71,6 +71,7 @@ const DEFAULT_SETTINGS: TodoSettings = { defaultClaudeEffort: null, defaultCodexModel: null, defaultCodexEffort: null, + defaultCrushModel: null, }; let cached: TodoSettings | null = null; diff --git a/apps/desktop/src/main/todo-agent/trpc-router.ts b/apps/desktop/src/main/todo-agent/trpc-router.ts index 60aaa2d096e..f1b1336485e 100644 --- a/apps/desktop/src/main/todo-agent/trpc-router.ts +++ b/apps/desktop/src/main/todo-agent/trpc-router.ts @@ -48,6 +48,19 @@ import { * * Exposed as `todoAgent.*` on the app router. */ +interface CrushModelsCache { + values: string[]; + expiresAt: number; + inflight: Promise | null; +} + +const CRUSH_MODELS_CACHE_TTL_MS = 5 * 60_000; +const crushModelsCache: CrushModelsCache = { + values: [], + expiresAt: 0, + inflight: null, +}; + export const createTodoAgentRouter = () => { return router({ create: publicProcedure @@ -152,6 +165,10 @@ export const createTodoAgentRouter = () => { : (settings.defaultCodexEffort ?? null); const resolvedAgentKind = input.agentKind ?? settings.defaultAgentKind ?? "claude"; + const resolvedCrushModel = + input.crushModel !== undefined + ? input.crushModel + : (settings.defaultCrushModel ?? null); const session = store.insertQueuedFromTemplate({ id: sessionId, @@ -172,6 +189,8 @@ export const createTodoAgentRouter = () => { resolvedAgentKind === "codex" ? resolvedCodexModel : null, codexEffort: resolvedAgentKind === "codex" ? resolvedCodexEffort : null, + crushModel: + resolvedAgentKind === "crush" ? resolvedCrushModel : null, remoteControlEnabled: input.remoteControlEnabled, artifactPath, }); @@ -542,6 +561,7 @@ export const createTodoAgentRouter = () => { agentKind: source.agentKind ?? "claude", codexModel: source.codexModel, codexEffort: source.codexEffort, + crushModel: source.crushModel ?? null, remoteControlEnabled: source.remoteControlEnabled ?? false, verdictPassed: null, verdictReason: null, @@ -1029,6 +1049,43 @@ export const createTodoAgentRouter = () => { }), ), }), + crushModels: publicProcedure.query(async () => { + const now = Date.now(); + if (crushModelsCache.expiresAt > now) { + return crushModelsCache.values; + } + if (crushModelsCache.inflight) { + return crushModelsCache.inflight; + } + + crushModelsCache.inflight = (async () => { + const { execFile } = await import("node:child_process"); + const { promisify } = await import("node:util"); + const execFileAsync = promisify(execFile); + const bin = + process.env.TODO_CRUSH_BIN || process.env.CRUSH_BIN || "crush"; + try { + const { stdout } = await execFileAsync(bin, ["models"], { + timeout: 10_000, + }); + const values = stdout + .trim() + .split("\n") + .map((l) => l.trim()) + .filter(Boolean); + crushModelsCache.values = values; + crushModelsCache.expiresAt = Date.now() + CRUSH_MODELS_CACHE_TTL_MS; + return values; + } catch { + return crushModelsCache.values.length > 0 + ? crushModelsCache.values + : []; + } finally { + crushModelsCache.inflight = null; + } + })(); + return crushModelsCache.inflight; + }), }); }; diff --git a/apps/desktop/src/main/todo-agent/types.ts b/apps/desktop/src/main/todo-agent/types.ts index 3b58b8a209b..96613ed375c 100644 --- a/apps/desktop/src/main/todo-agent/types.ts +++ b/apps/desktop/src/main/todo-agent/types.ts @@ -19,7 +19,7 @@ export interface TodoSessionListEntry extends SelectTodoSession { // ---- Agent kind ---- -export const AGENT_KIND_OPTIONS = ["claude", "codex"] as const; +export const AGENT_KIND_OPTIONS = ["claude", "codex", "crush"] as const; export type AgentKind = (typeof AGENT_KIND_OPTIONS)[number]; export const agentKindSchema = z.enum(AGENT_KIND_OPTIONS); export const DEFAULT_AGENT_KIND: AgentKind = "claude"; @@ -161,6 +161,12 @@ export const todoCreateInputSchema = z.object({ // "codex"; ignored for Claude sessions. codexModel: todoCodexModelSchema.nullish(), codexEffort: todoCodexEffortSchema.nullish(), + // Optional per-session Crush CLI model override. Null / undefined means + // "use the user's configured default". Only read when agentKind is + // "crush"; ignored for Claude / Codex sessions. The value is a free-form + // string in the form "provider/model" (e.g. "openai/gpt-5.4") resolved + // dynamically from `crush models`. No effort option — Crush CLI lacks one. + crushModel: z.string().trim().max(200).nullish(), // Beta escape hatch: opt a single TODO into the interactive PTY // engine without flipping the whole app over from headless `-p`. // Persisted in the artifact runtime config, not the DB row. @@ -218,6 +224,8 @@ export const todoSettingsSchema = z.object({ // Global defaults for Codex sessions. defaultCodexModel: todoCodexModelSchema.nullish().default(null), defaultCodexEffort: todoCodexEffortSchema.nullish().default(null), + // Global default for Crush sessions. Free-form string ("provider/model"). + defaultCrushModel: z.string().trim().max(200).nullish().default(null), }); export type TodoSettings = z.infer; diff --git a/apps/desktop/src/main/todo-daemon/crush-turn-runner.ts b/apps/desktop/src/main/todo-daemon/crush-turn-runner.ts new file mode 100644 index 00000000000..14f975ab774 --- /dev/null +++ b/apps/desktop/src/main/todo-daemon/crush-turn-runner.ts @@ -0,0 +1,482 @@ +import { type ChildProcess, spawn } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import Database from "better-sqlite3"; +import type { TodoStreamEventKind } from "main/todo-agent/types"; + +/** + * Crush CLI (`crush run`) turn runner. + * + * Runs a single Crush iteration via `crush run --yolo` and monitors + * progress by polling the project-local SQLite database + * (`/.crush/crush.db`). Crush does not emit structured + * streaming events on stdout (unlike Claude Code or Codex), but it + * writes every message — including tool calls, tool results, and + * finish events — into `crush.db` in real time. We poll this DB and + * convert new rows into the same `TodoStreamEvent` shape the + * supervisor engine uses. + * + * Session resume is supported via `crush run --session `. + * + * See: https://github.com/charmbracelet/crush + */ + +export interface CrushTurnParams { + sessionId: string; + iteration: number; + cwd: string; + prompt: string; + resumeSessionId: string | null; + customSystemPrompt: string | null; + crushModel: string | null; + signal: AbortSignal; + onChild: (child: ChildProcess) => void; + emit: (event: CrushStreamEvent) => void; +} + +export interface CrushTurnResult { + result: string | null; + sessionId: string | null; + costUsd: number | null; + numTurns: number | null; + error: string | null; + interrupted: boolean; +} + +export interface CrushStreamEvent { + id: string; + ts: number; + iteration: number; + kind: TodoStreamEventKind; + label: string; + text: string; + toolUseId?: string; +} + +const CRUSH_BIN = + process.env.TODO_CRUSH_BIN || process.env.CRUSH_BIN || "crush"; + +const POLL_INTERVAL_MS = 250; + +// ---- Main entry point ---- + +export async function runCrushTurn( + params: CrushTurnParams, +): Promise { + const args = buildArgs(params); + + let child: ChildProcess; + try { + child = spawn(CRUSH_BIN, args, { + cwd: params.cwd, + env: { ...process.env }, + detached: process.platform !== "win32", + }); + } catch (error) { + return { + result: null, + sessionId: null, + costUsd: null, + numTurns: null, + error: + error instanceof Error + ? `crush を起動できませんでした: ${error.message}` + : "crush を起動できませんでした", + interrupted: false, + }; + } + + params.onChild(child); + + let crushSessionId: string | null = null; + let resultText: string | null = null; + let costUsd: number | null = null; + let numTurns = 0; + let errorText: string | null = null; + let interrupted = false; + + // Extract session id from stderr: + // "INFO Created session for non-interactive run session_id=..." + let stderrBuffer = ""; + child.stderr?.on("data", (chunk: Buffer) => { + stderrBuffer += chunk.toString("utf8"); + const match = stderrBuffer.match( + /session_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/, + ); + if (match && !crushSessionId) { + crushSessionId = match[1]; + params.emit({ + id: randomUUID(), + ts: Date.now(), + iteration: params.iteration, + kind: "system_init", + label: "Crush", + text: `Crush セッション開始 (id: ${crushSessionId.slice(0, 8)}...)`, + }); + } + }); + + // Collect stdout (final result text) + let stdoutBuffer = ""; + child.stdout?.on("data", (chunk: Buffer) => { + stdoutBuffer += chunk.toString("utf8"); + }); + + // Signal that the child has exited so the DB poll loop can stop. + let childExited = false; + + // Start DB polling in parallel with process execution + const pollPromise = pollDbForEvents( + params, + () => crushSessionId, + () => childExited, + ); + + // Wait for child to exit + const exitCode = await new Promise((resolve) => { + child.on("close", (code) => { + childExited = true; + resolve(code); + }); + child.on("error", (err) => { + errorText = `crush プロセスエラー: ${err.message}`; + childExited = true; + resolve(null); + }); + + if (params.signal.aborted) { + killProcess(child); + interrupted = true; + childExited = true; + resolve(null); + return; + } + params.signal.addEventListener( + "abort", + () => { + interrupted = true; + killProcess(child); + }, + { once: true }, + ); + }); + + // Give DB polling one final sweep after process exits + const pollResult = await pollPromise; + + // Collect final result + resultText = stdoutBuffer.trim() || pollResult.lastAssistantText || null; + numTurns = pollResult.numTurns; + + // Read cost from DB if available + if (crushSessionId) { + const dbPath = findCrushDb(params.cwd); + if (dbPath) { + try { + const db = new Database(dbPath, { readonly: true }); + const row = db + .prepare("SELECT cost FROM sessions WHERE id = ?") + .get(crushSessionId) as { cost: number } | undefined; + if (row && typeof row.cost === "number" && Number.isFinite(row.cost)) { + costUsd = row.cost; + } + db.close(); + } catch { + // best-effort + } + } + } + + if (exitCode !== 0 && exitCode !== null && !interrupted) { + errorText = errorText ?? `crush が終了コード ${exitCode} で終了しました`; + } + + return { + result: resultText, + sessionId: crushSessionId, + costUsd, + numTurns: numTurns || null, + error: errorText, + interrupted, + }; +} + +// ---- Arg builder ---- + +function buildArgs(params: CrushTurnParams): string[] { + const args = ["run", "--yolo"]; + + if (params.crushModel) { + args.push("--model", params.crushModel); + } + + if (params.resumeSessionId) { + args.push("--session", params.resumeSessionId); + } + + // Prepend system instructions since Crush `run` has no dedicated flag. + const promptParts: string[] = []; + if (params.customSystemPrompt) { + promptParts.push( + `[System Instructions]\n${params.customSystemPrompt}\n[End System Instructions]\n\n`, + ); + } + promptParts.push(params.prompt); + args.push(promptParts.join("")); + + return args; +} + +// ---- DB polling ---- + +interface PollResult { + lastAssistantText: string | null; + numTurns: number; +} + +async function pollDbForEvents( + params: CrushTurnParams, + getSessionId: () => string | null, + isChildExited: () => boolean, +): Promise { + let lastSeenCreatedAt = 0; + let lastAssistantText: string | null = null; + let numTurns = 0; + let settled = false; + + while (!settled && !params.signal.aborted) { + await sleep(POLL_INTERVAL_MS); + + const sessionId = getSessionId(); + if (!sessionId) continue; + + const dbPath = findCrushDb(params.cwd); + if (!dbPath) continue; + + let db: Database.Database | null = null; + try { + db = new Database(dbPath, { readonly: true }); + const rows = db + .prepare( + "SELECT id, role, parts, created_at FROM messages WHERE session_id = ? AND created_at > ? ORDER BY created_at ASC", + ) + .all(sessionId, lastSeenCreatedAt) as Array<{ + id: string; + role: string; + parts: string; + created_at: number; + }>; + + for (const row of rows) { + lastSeenCreatedAt = Math.max(lastSeenCreatedAt, row.created_at); + const parts = safeParseJson(row.parts); + if (!parts) continue; + + for (const part of parts) { + const events = classifyPart(part, row.role, params.iteration); + for (const evt of events) { + params.emit(evt); + if (evt.kind === "assistant_text" && evt.text) { + lastAssistantText = evt.text; + } + if (evt.kind === "result") { + numTurns++; + } + } + } + } + + // Check if session has finished (finish reason in last assistant message) + if (rows.length > 0) { + const lastRow = rows[rows.length - 1]; + const lastParts = safeParseJson(lastRow.parts); + if (lastParts) { + for (const p of lastParts) { + if ( + p.type === "finish" && + (p.data?.reason === "end_turn" || p.data?.reason === "stop") + ) { + settled = true; + } + } + } + } + } catch { + // DB may be locked or not yet created — retry + } finally { + db?.close(); + } + + // Also stop polling once the child process has exited. + // This handles cases where the DB never gets a finish event + // (e.g. error, signal, or DB race). + if (isChildExited()) { + settled = true; + } + } + + return { lastAssistantText, numTurns }; +} + +// ---- Event classification ---- + +function classifyPart( + part: PartData, + role: string, + iteration: number, +): CrushStreamEvent[] { + const events: CrushStreamEvent[] = []; + const ts = Date.now(); + + if (part.type === "text" && role === "assistant") { + const text = part.data?.text ?? ""; + if (text.length > 0) { + events.push({ + id: randomUUID(), + ts, + iteration, + kind: "assistant_text", + label: "Crush", + text: truncate(text, 4000), + }); + } + } else if (part.type === "tool_call") { + const name = part.data?.name ?? "unknown"; + const input = part.data?.input ?? ""; + const id = part.data?.id; + events.push({ + id: randomUUID(), + ts, + iteration, + kind: "tool_use", + label: toolLabel(name), + text: truncate( + typeof input === "string" ? input : JSON.stringify(input), + 2000, + ), + toolUseId: id, + }); + } else if (part.type === "tool_result") { + const content = part.data?.content ?? ""; + const isError = part.data?.is_error ?? false; + const toolCallId = part.data?.tool_call_id; + events.push({ + id: randomUUID(), + ts, + iteration, + kind: isError ? "error" : "tool_result", + label: isError ? "Error" : "Result", + text: truncate( + typeof content === "string" ? content : JSON.stringify(content), + 4000, + ), + toolUseId: toolCallId, + }); + } else if (part.type === "finish" && role === "assistant") { + const reason = part.data?.reason ?? "unknown"; + if ( + reason === "error" || + reason === "canceled" || + reason === "permission_denied" + ) { + events.push({ + id: randomUUID(), + ts, + iteration, + kind: "error", + label: "Crush", + text: `終了理由: ${reason}`, + }); + } else if (reason === "end_turn" || reason === "stop") { + events.push({ + id: randomUUID(), + ts, + iteration, + kind: "result", + label: "Crush", + text: `ターン完了`, + }); + } + } + // "reasoning" and "binary" parts are intentionally skipped + + return events; +} + +function toolLabel(name: string): string { + const labels: Record = { + bash: "Bash", + edit: "Edit", + write: "Write", + view: "Read", + glob: "Glob", + grep: "Grep", + ls: "LS", + agent: "Agent", + fetch: "Fetch", + sourcegraph: "Sourcegraph", + multiedit: "MultiEdit", + todos: "Todos", + }; + return labels[name] ?? name; +} + +// ---- Helpers ---- + +function killProcess(child: ChildProcess) { + if (!child.pid) return; + try { + if (process.platform === "win32") { + spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"]); + } else { + process.kill(-child.pid, "SIGKILL"); + } + } catch { + child.kill("SIGKILL"); + } +} + +function findCrushDb(cwd: string): string | null { + const dbPath = path.join(cwd, ".crush", "crush.db"); + try { + fs.accessSync(dbPath); + return dbPath; + } catch { + return null; + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function safeParseJson(text: string): PartData[] | null { + try { + const parsed = JSON.parse(text); + return Array.isArray(parsed) ? (parsed as PartData[]) : null; + } catch { + return null; + } +} + +function truncate(text: string, maxLen: number): string { + if (text.length <= maxLen) return text; + return `${text.slice(0, maxLen)}…`; +} + +interface PartData { + type: string; + data?: { + text?: string; + thinking?: string; + id?: string; + name?: string; + input?: string; + tool_call_id?: string; + content?: string; + is_error?: boolean; + reason?: string; + [key: string]: unknown; + }; +} diff --git a/apps/desktop/src/main/todo-daemon/supervisor-engine.ts b/apps/desktop/src/main/todo-daemon/supervisor-engine.ts index 16bc6b27973..e6da4c8db43 100644 --- a/apps/desktop/src/main/todo-daemon/supervisor-engine.ts +++ b/apps/desktop/src/main/todo-daemon/supervisor-engine.ts @@ -14,6 +14,7 @@ import { type TodoStreamEventKind, } from "main/todo-agent/types"; import { runCodexTurn } from "./codex-turn-runner"; +import { runCrushTurn } from "./crush-turn-runner"; import { runClaudeTurnPty } from "./pty-turn-runner"; /** @@ -446,11 +447,13 @@ export class TodoSupervisorEngine { resumeSessionId: claudeSessionId, customSystemPrompt: currentSession.customSystemPrompt ?? null, agentKind: - (currentSession.agentKind as "claude" | "codex" | null) ?? "claude", + (currentSession.agentKind as "claude" | "codex" | "crush" | null) ?? + "claude", claudeModel: currentSession.claudeModel ?? null, claudeEffort: currentSession.claudeEffort ?? null, codexModel: currentSession.codexModel ?? null, codexEffort: currentSession.codexEffort ?? null, + crushModel: currentSession.crushModel ?? null, signal: ac.signal, usePty: willUsePty, remoteControlEnabled, @@ -650,11 +653,12 @@ export class TodoSupervisorEngine { prompt: string; resumeSessionId: string | null; customSystemPrompt: string | null; - agentKind: "claude" | "codex"; + agentKind: "claude" | "codex" | "crush"; claudeModel: string | null; claudeEffort: string | null; codexModel: string | null; codexEffort: string | null; + crushModel: string | null; signal: AbortSignal; usePty: boolean; onChild: (child: ChildProcess) => void; @@ -692,6 +696,42 @@ export class TodoSupervisorEngine { }; } + if (params.agentKind === "crush") { + const crushResult = await runCrushTurn({ + sessionId: params.sessionId, + iteration: params.iteration, + cwd: params.cwd, + prompt: params.prompt, + resumeSessionId: params.resumeSessionId, + customSystemPrompt: params.customSystemPrompt, + crushModel: params.crushModel, + signal: params.signal, + onChild: params.onChild, + emit: (evt) => { + getTodoSessionStore().appendStreamEvents(params.sessionId, [ + { + id: evt.id, + ts: evt.ts, + iteration: evt.iteration, + kind: evt.kind, + label: evt.label, + text: evt.text, + ...(evt.toolUseId ? { toolUseId: evt.toolUseId } : {}), + }, + ]); + }, + }); + return { + result: crushResult.result, + sessionId: crushResult.sessionId, + costUsd: crushResult.costUsd, + numTurns: crushResult.numTurns, + error: crushResult.error, + interrupted: crushResult.interrupted, + scheduledWakeup: null, + }; + } + // Claude Code path if (params.usePty) { return runClaudeTurnPty({ diff --git a/apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/AgentRuntimePicker.tsx b/apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/AgentRuntimePicker.tsx index 61037a7f7cb..8af3ccfa16f 100644 --- a/apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/AgentRuntimePicker.tsx +++ b/apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/AgentRuntimePicker.tsx @@ -17,6 +17,8 @@ import { CODEX_MODEL_SELECT_OPTIONS, type CodexEffortPick, type CodexModelPick, + type CrushModelPick, + DEFAULT_SENTINEL, } from "./claudeRuntimeOptions"; interface AgentRuntimePickerProps { @@ -30,6 +32,9 @@ interface AgentRuntimePickerProps { codexEffort: CodexEffortPick; onCodexModelChange: (value: CodexModelPick) => void; onCodexEffortChange: (value: CodexEffortPick) => void; + crushModel: CrushModelPick; + onCrushModelChange: (value: CrushModelPick) => void; + crushModels: string[]; disabled?: boolean; layout?: "stacked" | "row"; compact?: boolean; @@ -50,6 +55,11 @@ const AGENT_KIND_OPTIONS: Array<{ label: "Codex CLI", description: "OpenAI Codex CLI (codex exec)", }, + { + value: "crush", + label: "Crush", + description: "Charmbracelet Crush CLI (crush run)", + }, ]; export function AgentRuntimePicker({ @@ -63,14 +73,98 @@ export function AgentRuntimePicker({ codexEffort, onCodexModelChange, onCodexEffortChange, + crushModel, + onCrushModelChange, + crushModels, disabled, layout = "row", compact = true, }: AgentRuntimePickerProps) { const labelClass = compact ? "text-xs" : "text-sm"; const triggerClass = compact ? "h-8 text-xs" : ""; - const isClaude = agentKind === "claude"; + return ( +
+
+ + +
+ + {agentKind === "crush" ? ( + + ) : ( + + )} +
+ ); +} + +function ClaudeCodexModelSection({ + agentKind, + claudeModel, + claudeEffort, + onClaudeModelChange, + onClaudeEffortChange, + codexModel, + codexEffort, + onCodexModelChange, + onCodexEffortChange, + disabled, + layout, + labelClass, + triggerClass, +}: { + agentKind: "claude" | "codex"; + claudeModel: ClaudeModelPick; + claudeEffort: ClaudeEffortPick; + onClaudeModelChange: (v: ClaudeModelPick) => void; + onClaudeEffortChange: (v: ClaudeEffortPick) => void; + codexModel: CodexModelPick; + codexEffort: CodexEffortPick; + onCodexModelChange: (v: CodexModelPick) => void; + onCodexEffortChange: (v: CodexEffortPick) => void; + disabled?: boolean; + layout?: "stacked" | "row"; + labelClass: string; + triggerClass: string; +}) { + const isClaude = agentKind === "claude"; const modelOptions = isClaude ? CLAUDE_MODEL_SELECT_OPTIONS : CODEX_MODEL_SELECT_OPTIONS; @@ -83,19 +177,26 @@ export function AgentRuntimePicker({ const onEffortChange = isClaude ? onClaudeEffortChange : onCodexEffortChange; return ( -
+
- +
-
-
- - -
-
- - -
+
+ +
); } + +function CrushModelSection({ + crushModel, + onCrushModelChange, + crushModels, + disabled, + labelClass, + triggerClass, +}: { + crushModel: CrushModelPick; + onCrushModelChange: (v: CrushModelPick) => void; + crushModels: string[]; + disabled?: boolean; + labelClass: string; + triggerClass: string; +}) { + const grouped = groupByProvider(crushModels); + + return ( +
+ + +
+ ); +} + +function groupByProvider(models: string[]): Record { + const result: Record = {}; + for (const model of models) { + const sep = model.indexOf("/"); + const provider = sep > 0 ? model.slice(0, sep) : "other"; + if (!result[provider]) result[provider] = []; + result[provider].push(model); + } + return result; +} diff --git a/apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/claudeRuntimeOptions.ts b/apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/claudeRuntimeOptions.ts index cde15944e3f..9c924ac2377 100644 --- a/apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/claudeRuntimeOptions.ts +++ b/apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/claudeRuntimeOptions.ts @@ -366,3 +366,29 @@ export function getCodexEffortLabel( persisted ); } + +// ---- Crush CLI options ---- +// +// Crush has 200+ models and no effort concept. The model list is dynamic +// (fetched from `crush models` via tRPC) so we use free-form strings +// instead of a const array. Only the default sentinel is static. + +export type CrushModelPick = typeof DEFAULT_SENTINEL | string; + +export function toPersistedCrushModel(pick: CrushModelPick): string | null { + return pick === DEFAULT_SENTINEL ? null : pick; +} + +export function fromPersistedCrushModel( + persisted: string | null | undefined, +): CrushModelPick { + if (persisted == null) return DEFAULT_SENTINEL; + return persisted; +} + +export function getCrushModelLabel( + persisted: string | null | undefined, +): string { + if (persisted == null) return "デフォルト"; + return persisted; +} diff --git a/apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/index.ts b/apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/index.ts index d9d43dcff4b..43d175affe2 100644 --- a/apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/index.ts +++ b/apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/index.ts @@ -9,17 +9,21 @@ export { CODEX_MODEL_SELECT_OPTIONS, type CodexEffortPick, type CodexModelPick, + type CrushModelPick, DEFAULT_SENTINEL, fromPersistedCodexEffort, fromPersistedCodexModel, + fromPersistedCrushModel, fromPersistedEffort, fromPersistedModel, getClaudeEffortLabel, getClaudeModelLabel, getCodexEffortLabel, getCodexModelLabel, + getCrushModelLabel, toPersistedCodexEffort, toPersistedCodexModel, + toPersistedCrushModel, toPersistedEffort, toPersistedModel, } from "./claudeRuntimeOptions"; diff --git a/apps/desktop/src/renderer/features/todo-agent/TodoManager/PresetsDialog/PresetsDialog.tsx b/apps/desktop/src/renderer/features/todo-agent/TodoManager/PresetsDialog/PresetsDialog.tsx index 32479aaef90..58ca2c7c489 100644 --- a/apps/desktop/src/renderer/features/todo-agent/TodoManager/PresetsDialog/PresetsDialog.tsx +++ b/apps/desktop/src/renderer/features/todo-agent/TodoManager/PresetsDialog/PresetsDialog.tsx @@ -22,11 +22,14 @@ import { type ClaudeModelPick, type CodexEffortPick, type CodexModelPick, + type CrushModelPick, DEFAULT_SENTINEL, + fromPersistedCrushModel, fromPersistedEffort, fromPersistedModel, toPersistedCodexEffort, toPersistedCodexModel, + toPersistedCrushModel, toPersistedEffort, toPersistedModel, } from "../../ClaudeRuntimePicker"; @@ -111,6 +114,8 @@ function SettingsTab() { const { data: settings } = electronTrpc.todoAgent.settings.get.useQuery(); const updateMut = electronTrpc.todoAgent.settings.update.useMutation(); const utils = electronTrpc.useUtils(); + const { data: crushModelsData } = + electronTrpc.todoAgent.crushModels.useQuery(undefined); const [maxIter, setMaxIter] = useState(10); const [maxMin, setMaxMin] = useState(30); @@ -126,6 +131,8 @@ function SettingsTab() { useState(DEFAULT_SENTINEL); const [defaultCodexEffort, setDefaultCodexEffort] = useState(DEFAULT_SENTINEL); + const [defaultCrushModel, setDefaultCrushModel] = + useState(DEFAULT_SENTINEL); // Hydrate form state the first time settings arrive from the main // process. A React Query background refetch (window focus, etc.) @@ -143,6 +150,9 @@ function SettingsTab() { setRetentionDays(settings.sessionRetentionDays); setDefaultModel(fromPersistedModel(settings.defaultClaudeModel ?? null)); setDefaultEffort(fromPersistedEffort(settings.defaultClaudeEffort ?? null)); + setDefaultCrushModel( + fromPersistedCrushModel(settings.defaultCrushModel ?? null), + ); hydratedRef.current = true; }, [settings]); @@ -160,7 +170,9 @@ function SettingsTab() { toPersistedCodexModel(defaultCodexModel) !== (settings.defaultCodexModel ?? null) || toPersistedCodexEffort(defaultCodexEffort) !== - (settings.defaultCodexEffort ?? null)); + (settings.defaultCodexEffort ?? null) || + toPersistedCrushModel(defaultCrushModel) !== + (settings.defaultCrushModel ?? null)); const handleSave = useCallback(async () => { try { @@ -174,6 +186,7 @@ function SettingsTab() { defaultAgentKind, defaultCodexModel: toPersistedCodexModel(defaultCodexModel), defaultCodexEffort: toPersistedCodexEffort(defaultCodexEffort), + defaultCrushModel: toPersistedCrushModel(defaultCrushModel), }); await utils.todoAgent.settings.get.invalidate(); toast.success("設定を保存しました"); @@ -186,6 +199,7 @@ function SettingsTab() { defaultAgentKind, defaultCodexEffort, defaultCodexModel, + defaultCrushModel, defaultEffort, defaultModel, maxIter, @@ -276,6 +290,9 @@ function SettingsTab() { codexEffort={defaultCodexEffort} onCodexModelChange={setDefaultCodexModel} onCodexEffortChange={setDefaultCodexEffort} + crushModel={defaultCrushModel} + onCrushModelChange={setDefaultCrushModel} + crushModels={crushModelsData ?? []} compact={false} />

diff --git a/apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx b/apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx index 8aaa8d4efb9..17ce8cb2b98 100644 --- a/apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx +++ b/apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx @@ -82,15 +82,18 @@ import { ClaudeRuntimePicker, type CodexEffortPick, type CodexModelPick, + type CrushModelPick, DEFAULT_SENTINEL, fromPersistedCodexEffort, fromPersistedCodexModel, + fromPersistedCrushModel, fromPersistedEffort, fromPersistedModel, getClaudeEffortLabel, getClaudeModelLabel, toPersistedCodexEffort, toPersistedCodexModel, + toPersistedCrushModel, toPersistedEffort, toPersistedModel, } from "../ClaudeRuntimePicker"; @@ -2766,8 +2769,13 @@ function TodoComposer({ useState(DEFAULT_SENTINEL); const [codexEffort, setCodexEffort] = useState(DEFAULT_SENTINEL); + const [crushModel, setCrushModel] = + useState(DEFAULT_SENTINEL); const [submitting, setSubmitting] = useState(false); + const { data: crushModelsData } = + electronTrpc.todoAgent.crushModels.useQuery(undefined); + useEffect(() => { if (!projectId && defaultProjectId) setProjectId(defaultProjectId); }, [projectId, defaultProjectId]); @@ -2790,6 +2798,9 @@ function TodoComposer({ setCodexEffort( fromPersistedCodexEffort(todoSettings.defaultCodexEffort ?? null), ); + setCrushModel( + fromPersistedCrushModel(todoSettings.defaultCrushModel ?? null), + ); claudeSeededRef.current = true; }, [todoSettings]); @@ -2926,6 +2937,7 @@ function TodoComposer({ claudeEffort: toPersistedEffort(claudeEffort), codexModel: toPersistedCodexModel(codexModel), codexEffort: toPersistedCodexEffort(codexEffort), + crushModel: toPersistedCrushModel(crushModel), ptyEnabled, remoteControlEnabled, }); @@ -3006,6 +3018,7 @@ function TodoComposer({ agentKind, codexEffort, codexModel, + crushModel, remoteControlEnabled, scopedPresets.system, selectedPresetId, @@ -3234,6 +3247,9 @@ function TodoComposer({ codexEffort={codexEffort} onCodexModelChange={setCodexModel} onCodexEffortChange={setCodexEffort} + crushModel={crushModel} + onCrushModelChange={setCrushModel} + crushModels={crushModelsData ?? []} disabled={submitting} /> diff --git a/apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx b/apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx index 042dd6a7e62..9b551f36ab1 100644 --- a/apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx +++ b/apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx @@ -30,13 +30,16 @@ import { type ClaudeModelPick, type CodexEffortPick, type CodexModelPick, + type CrushModelPick, DEFAULT_SENTINEL, fromPersistedCodexEffort, fromPersistedCodexModel, + fromPersistedCrushModel, fromPersistedEffort, fromPersistedModel, toPersistedCodexEffort, toPersistedCodexModel, + toPersistedCrushModel, toPersistedEffort, toPersistedModel, } from "../ClaudeRuntimePicker"; @@ -96,6 +99,8 @@ export function TodoModal({ useState(DEFAULT_SENTINEL); const [codexEffort, setCodexEffort] = useState(DEFAULT_SENTINEL); + const [crushModel, setCrushModel] = + useState(DEFAULT_SENTINEL); // Seed the picker from the global defaults only once per modal // opening. Without this guard, a React Query background refetch or // a settings update fired while the user is picking would overwrite @@ -124,6 +129,9 @@ export function TodoModal({ setCodexEffort( fromPersistedCodexEffort(todoSettings.defaultCodexEffort ?? null), ); + setCrushModel( + fromPersistedCrushModel(todoSettings.defaultCrushModel ?? null), + ); seededRef.current = true; }, [open, todoSettings]); @@ -140,6 +148,12 @@ export function TodoModal({ enabled: open, }, ); + const { data: crushModelsData } = electronTrpc.todoAgent.crushModels.useQuery( + undefined, + { + enabled: open, + }, + ); const selectedPreset = useMemo( () => (presets ?? []).find((p) => p.id === selectedPresetId) ?? null, [presets, selectedPresetId], @@ -164,6 +178,9 @@ export function TodoModal({ setClaudeEffort( fromPersistedEffort(todoSettings?.defaultClaudeEffort ?? null), ); + setCrushModel( + fromPersistedCrushModel(todoSettings?.defaultCrushModel ?? null), + ); setSubmitting(false); }, [todoSettings]); @@ -262,6 +279,7 @@ export function TodoModal({ claudeEffort: toPersistedEffort(claudeEffort), codexModel: toPersistedCodexModel(codexModel), codexEffort: toPersistedCodexEffort(codexEffort), + crushModel: toPersistedCrushModel(crushModel), ptyEnabled, remoteControlEnabled, }); @@ -342,6 +360,7 @@ export function TodoModal({ agentKind, codexEffort, codexModel, + crushModel, ]); return ( @@ -532,6 +551,9 @@ export function TodoModal({ codexEffort={codexEffort} onCodexModelChange={setCodexModel} onCodexEffortChange={setCodexEffort} + crushModel={crushModel} + onCrushModelChange={setCrushModel} + crushModels={crushModelsData ?? []} disabled={submitting} /> diff --git a/packages/local-db/drizzle/0069_add_crush_model.sql b/packages/local-db/drizzle/0069_add_crush_model.sql new file mode 100644 index 00000000000..0b4e615e685 --- /dev/null +++ b/packages/local-db/drizzle/0069_add_crush_model.sql @@ -0,0 +1,2 @@ +ALTER TABLE `todo_schedules` ADD `crush_model` text;--> statement-breakpoint +ALTER TABLE `todo_sessions` ADD `crush_model` text; \ No newline at end of file diff --git a/packages/local-db/drizzle/meta/0069_snapshot.json b/packages/local-db/drizzle/meta/0069_snapshot.json new file mode 100644 index 00000000000..3e2ac270629 --- /dev/null +++ b/packages/local-db/drizzle/meta/0069_snapshot.json @@ -0,0 +1,2390 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "1742b9fd-913c-4233-ba5b-22f5b9bb1206", + "prevId": "a11e0cbf-22c7-4355-a1f9-691112b9872d", + "tables": { + "browser_history": { + "name": "browser_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_visited_at": { + "name": "last_visited_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visit_count": { + "name": "visit_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "browser_history_url_unique": { + "name": "browser_history_url_unique", + "columns": [ + "url" + ], + "isUnique": true + }, + "browser_history_url_idx": { + "name": "browser_history_url_idx", + "columns": [ + "url" + ], + "isUnique": false + }, + "browser_history_last_visited_at_idx": { + "name": "browser_history_last_visited_at_idx", + "columns": [ + "last_visited_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "browser_site_permissions": { + "name": "browser_site_permissions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ask'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "browser_site_permissions_origin_idx": { + "name": "browser_site_permissions_origin_idx", + "columns": [ + "origin" + ], + "isUnique": false + }, + "browser_site_permissions_origin_kind_unique": { + "name": "browser_site_permissions_origin_kind_unique", + "columns": [ + "origin", + "kind" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization_members": { + "name": "organization_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_members_organization_id_idx": { + "name": "organization_members_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "organization_members_user_id_idx": { + "name": "organization_members_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "organization_members_organization_id_organizations_id_fk": { + "name": "organization_members_organization_id_organizations_id_fk", + "tableFrom": "organization_members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_members_user_id_users_id_fk": { + "name": "organization_members_user_id_users_id_fk", + "tableFrom": "organization_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "clerk_org_id": { + "name": "clerk_org_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organizations_clerk_org_id_unique": { + "name": "organizations_clerk_org_id_unique", + "columns": [ + "clerk_org_id" + ], + "isUnique": true + }, + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + "slug" + ], + "isUnique": false + }, + "organizations_clerk_org_id_idx": { + "name": "organizations_clerk_org_id_idx", + "columns": [ + "clerk_org_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "main_repo_path": { + "name": "main_repo_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_toast_dismissed": { + "name": "config_toast_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_base_branch": { + "name": "workspace_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_owner": { + "name": "github_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_mode": { + "name": "branch_prefix_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_custom": { + "name": "branch_prefix_custom", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "worktree_base_dir": { + "name": "worktree_base_dir", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hide_image": { + "name": "hide_image", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "neon_project_id": { + "name": "neon_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_app": { + "name": "default_app", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_import_external_worktrees": { + "name": "auto_import_external_worktrees", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_remove_missing_worktrees": { + "name": "auto_remove_missing_worktrees", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "projects_main_repo_path_idx": { + "name": "projects_main_repo_path_idx", + "columns": [ + "main_repo_path" + ], + "isUnique": false + }, + "projects_last_opened_at_idx": { + "name": "projects_last_opened_at_idx", + "columns": [ + "last_opened_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_presets": { + "name": "terminal_presets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_presets_initialized": { + "name": "terminal_presets_initialized", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_preset_overrides": { + "name": "agent_preset_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_custom_definitions": { + "name": "agent_custom_definitions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selected_ringtone_id": { + "name": "selected_ringtone_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_on_quit": { + "name": "confirm_on_quit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_link_behavior": { + "name": "terminal_link_behavior", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "persist_terminal": { + "name": "persist_terminal", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "auto_apply_default_preset": { + "name": "auto_apply_default_preset", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_mode": { + "name": "branch_prefix_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_custom": { + "name": "branch_prefix_custom", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notification_sounds_muted": { + "name": "notification_sounds_muted", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notification_volume": { + "name": "notification_volume", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aivis_enabled": { + "name": "aivis_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aivis_api_key": { + "name": "aivis_api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aivis_model_uuid": { + "name": "aivis_model_uuid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aivis_format": { + "name": "aivis_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aivis_format_permission": { + "name": "aivis_format_permission", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aivis_user_dictionary_uuid": { + "name": "aivis_user_dictionary_uuid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aivis_volume": { + "name": "aivis_volume", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aivis_speaking_rate": { + "name": "aivis_speaking_rate", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aivis_model_presets": { + "name": "aivis_model_presets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prevent_agent_sleep": { + "name": "prevent_agent_sleep", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delete_local_branch": { + "name": "delete_local_branch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_open_mode": { + "name": "file_open_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_drag_behavior": { + "name": "file_drag_behavior", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "right_sidebar_open_view_width": { + "name": "right_sidebar_open_view_width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_presets_bar": { + "name": "show_presets_bar", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_compact_terminal_add_button": { + "name": "use_compact_terminal_add_button", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_font_family": { + "name": "terminal_font_family", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_font_size": { + "name": "terminal_font_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "editor_font_family": { + "name": "editor_font_family", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "editor_font_size": { + "name": "editor_font_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_resource_monitor": { + "name": "show_resource_monitor", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "worktree_base_dir": { + "name": "worktree_base_dir", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "open_links_in_app": { + "name": "open_links_in_app", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_editor": { + "name": "default_editor", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "indent_rainbow_enabled": { + "name": "indent_rainbow_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "indent_rainbow_colors": { + "name": "indent_rainbow_colors", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trailing_spaces_enabled": { + "name": "trailing_spaces_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trailing_spaces_color": { + "name": "trailing_spaces_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_graph_enabled": { + "name": "reference_graph_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expose_host_service_via_relay": { + "name": "expose_host_service_via_relay", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_smart_commit": { + "name": "enable_smart_commit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "smart_commit_changes": { + "name": "smart_commit_changes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_stash": { + "name": "auto_stash", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_sort_order": { + "name": "branch_sort_order", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_default_branch": { + "name": "pin_default_branch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "post_commit_command": { + "name": "post_commit_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tasks": { + "name": "tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_color": { + "name": "status_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_type": { + "name": "status_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_position": { + "name": "status_position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "assignee_id": { + "name": "assignee_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "estimate": { + "name": "estimate", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_provider": { + "name": "external_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_key": { + "name": "external_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_url": { + "name": "external_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tasks_slug_unique": { + "name": "tasks_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "tasks_slug_idx": { + "name": "tasks_slug_idx", + "columns": [ + "slug" + ], + "isUnique": false + }, + "tasks_organization_id_idx": { + "name": "tasks_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "tasks_assignee_id_idx": { + "name": "tasks_assignee_id_idx", + "columns": [ + "assignee_id" + ], + "isUnique": false + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "tasks_created_at_idx": { + "name": "tasks_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tasks_organization_id_organizations_id_fk": { + "name": "tasks_organization_id_organizations_id_fk", + "tableFrom": "tasks", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_assignee_id_users_id_fk": { + "name": "tasks_assignee_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_creator_id_users_id_fk": { + "name": "tasks_creator_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "clerk_id": { + "name": "clerk_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "columns": [ + "clerk_id" + ], + "isUnique": true + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + "email" + ], + "isUnique": false + }, + "users_clerk_id_idx": { + "name": "users_clerk_id_idx", + "columns": [ + "clerk_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workspace_sections": { + "name": "workspace_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_collapsed": { + "name": "is_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "workspace_sections_project_id_idx": { + "name": "workspace_sections_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "workspace_sections_project_id_projects_id_fk": { + "name": "workspace_sections_project_id_projects_id_fk", + "tableFrom": "workspace_sections", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workspaces": { + "name": "workspaces", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worktree_id": { + "name": "worktree_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_unread": { + "name": "is_unread", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "is_unnamed": { + "name": "is_unnamed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "deleting_at": { + "name": "deleting_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_base": { + "name": "port_base", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "workspaces_project_id_idx": { + "name": "workspaces_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "workspaces_worktree_id_idx": { + "name": "workspaces_worktree_id_idx", + "columns": [ + "worktree_id" + ], + "isUnique": false + }, + "workspaces_last_opened_at_idx": { + "name": "workspaces_last_opened_at_idx", + "columns": [ + "last_opened_at" + ], + "isUnique": false + }, + "workspaces_section_id_idx": { + "name": "workspaces_section_id_idx", + "columns": [ + "section_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "workspaces_project_id_projects_id_fk": { + "name": "workspaces_project_id_projects_id_fk", + "tableFrom": "workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspaces_worktree_id_worktrees_id_fk": { + "name": "workspaces_worktree_id_worktrees_id_fk", + "tableFrom": "workspaces", + "tableTo": "worktrees", + "columnsFrom": [ + "worktree_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspaces_section_id_workspace_sections_id_fk": { + "name": "workspaces_section_id_workspace_sections_id_fk", + "tableFrom": "workspaces", + "tableTo": "workspace_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "worktrees": { + "name": "worktrees", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "git_status": { + "name": "git_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_status": { + "name": "github_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_superset": { + "name": "created_by_superset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "worktrees_project_id_idx": { + "name": "worktrees_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "worktrees_branch_idx": { + "name": "worktrees_branch_idx", + "columns": [ + "branch" + ], + "isUnique": false + } + }, + "foreignKeys": { + "worktrees_project_id_projects_id_fk": { + "name": "worktrees_project_id_projects_id_fk", + "tableFrom": "worktrees", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "browser_automation_bindings": { + "name": "browser_automation_bindings", + "columns": { + "pane_id": { + "name": "pane_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_kind": { + "name": "session_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'todo-agent'" + }, + "connected_at": { + "name": "connected_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "todo_prompt_presets": { + "name": "todo_prompt_presets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "todo_prompt_presets_name_idx": { + "name": "todo_prompt_presets_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "todo_prompt_presets_updated_at_idx": { + "name": "todo_prompt_presets_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "todo_prompt_presets_kind_idx": { + "name": "todo_prompt_presets_kind_idx", + "columns": [ + "kind" + ], + "isUnique": false + }, + "todo_prompt_presets_workspace_idx": { + "name": "todo_prompt_presets_workspace_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "todo_schedules": { + "name": "todo_schedules", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "frequency": { + "name": "frequency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "minute": { + "name": "minute", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hour": { + "name": "hour", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weekday": { + "name": "weekday", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthday": { + "name": "monthday", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cron_expr": { + "name": "cron_expr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goal": { + "name": "goal", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "verify_command": { + "name": "verify_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_iterations": { + "name": "max_iterations", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "max_wall_clock_sec": { + "name": "max_wall_clock_sec", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1800 + }, + "custom_system_prompt": { + "name": "custom_system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claude_model": { + "name": "claude_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claude_effort": { + "name": "claude_effort", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_kind": { + "name": "agent_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'claude'" + }, + "codex_model": { + "name": "codex_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "codex_effort": { + "name": "codex_effort", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crush_model": { + "name": "crush_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overlap_mode": { + "name": "overlap_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'skip'" + }, + "auto_sync_before_fire": { + "name": "auto_sync_before_fire", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_session_id": { + "name": "last_run_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "todo_schedules_project_idx": { + "name": "todo_schedules_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "todo_schedules_workspace_idx": { + "name": "todo_schedules_workspace_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + }, + "todo_schedules_enabled_next_run_idx": { + "name": "todo_schedules_enabled_next_run_idx", + "columns": [ + "enabled", + "next_run_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "todo_schedules_project_id_projects_id_fk": { + "name": "todo_schedules_project_id_projects_id_fk", + "tableFrom": "todo_schedules", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "todo_schedules_workspace_id_workspaces_id_fk": { + "name": "todo_schedules_workspace_id_workspaces_id_fk", + "tableFrom": "todo_schedules", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "todo_sessions": { + "name": "todo_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goal": { + "name": "goal", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "verify_command": { + "name": "verify_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_iterations": { + "name": "max_iterations", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "max_wall_clock_sec": { + "name": "max_wall_clock_sec", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1800 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'queued'" + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "iteration": { + "name": "iteration", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "attached_pane_id": { + "name": "attached_pane_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attached_tab_id": { + "name": "attached_tab_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claude_session_id": { + "name": "claude_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "final_assistant_text": { + "name": "final_assistant_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_cost_usd": { + "name": "total_cost_usd", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_num_turns": { + "name": "total_num_turns", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_intervention": { + "name": "pending_intervention", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_head_sha": { + "name": "start_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_system_prompt": { + "name": "custom_system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claude_model": { + "name": "claude_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claude_effort": { + "name": "claude_effort", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_kind": { + "name": "agent_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'claude'" + }, + "codex_model": { + "name": "codex_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "codex_effort": { + "name": "codex_effort", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crush_model": { + "name": "crush_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "verdict_passed": { + "name": "verdict_passed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "verdict_failing_test": { + "name": "verdict_failing_test", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_path": { + "name": "artifact_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remote_control_enabled": { + "name": "remote_control_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "waiting_until": { + "name": "waiting_until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "waiting_reason": { + "name": "waiting_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "todo_sessions_workspace_idx": { + "name": "todo_sessions_workspace_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + }, + "todo_sessions_status_idx": { + "name": "todo_sessions_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "todo_sessions_created_at_idx": { + "name": "todo_sessions_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "todo_sessions_project_id_projects_id_fk": { + "name": "todo_sessions_project_id_projects_id_fk", + "tableFrom": "todo_sessions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "todo_sessions_workspace_id_workspaces_id_fk": { + "name": "todo_sessions_workspace_id_workspaces_id_fk", + "tableFrom": "todo_sessions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/local-db/drizzle/meta/_journal.json b/packages/local-db/drizzle/meta/_journal.json index c6886654b11..4384e30ff16 100644 --- a/packages/local-db/drizzle/meta/_journal.json +++ b/packages/local-db/drizzle/meta/_journal.json @@ -484,6 +484,13 @@ "when": 1776884577928, "tag": "0068_add_agent_kind_codex_fields", "breakpoints": true + }, + { + "idx": 69, + "version": "6", + "when": 1776912086245, + "tag": "0069_add_crush_model", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/local-db/src/schema/todo-schedules.ts b/packages/local-db/src/schema/todo-schedules.ts index de987436560..300bc6f2261 100644 --- a/packages/local-db/src/schema/todo-schedules.ts +++ b/packages/local-db/src/schema/todo-schedules.ts @@ -70,6 +70,10 @@ export const todoSchedules = sqliteTable( codexModel: text("codex_model"), codexEffort: text("codex_effort"), + // Optional Crush CLI model override for sessions this schedule + // creates. Only read when agentKind is "crush". + crushModel: text("crush_model"), + // How to behave when the previous session from this schedule is // still running at fire time. overlapMode: text("overlap_mode", { enum: ["skip", "queue"] }) diff --git a/packages/local-db/src/schema/todo-sessions.ts b/packages/local-db/src/schema/todo-sessions.ts index 6ade505cef5..174824f5018 100644 --- a/packages/local-db/src/schema/todo-sessions.ts +++ b/packages/local-db/src/schema/todo-sessions.ts @@ -108,6 +108,11 @@ export const todoSessions = sqliteTable( codexModel: text("codex_model"), codexEffort: text("codex_effort"), + // Optional per-session Crush CLI model override. Only read when + // agentKind is "crush". Free-form string in "provider/model" format + // (e.g. "openai/gpt-5.4"), resolved dynamically from `crush models`. + crushModel: text("crush_model"), + verdictPassed: integer("verdict_passed", { mode: "boolean" }), verdictReason: text("verdict_reason"), verdictFailingTest: text("verdict_failing_test"),