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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/desktop/src/main/todo-agent/schedule-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ class TodoScheduleStore {
maxIterations: input.maxIterations,
maxWallClockSec: input.maxWallClockSec,
customSystemPrompt: input.customSystemPrompt ?? null,
claudeModel: input.claudeModel ?? null,
claudeEffort: input.claudeEffort ?? null,
overlapMode: input.overlapMode,
autoSyncBeforeFire: input.autoSyncBeforeFire,
nextRunAt: input.nextRunAt,
Expand Down Expand Up @@ -103,6 +105,10 @@ class TodoScheduleStore {
patch.maxWallClockSec = rest.maxWallClockSec;
if (rest.customSystemPrompt !== undefined)
patch.customSystemPrompt = rest.customSystemPrompt ?? null;
if (rest.claudeModel !== undefined)
patch.claudeModel = rest.claudeModel ?? null;
if (rest.claudeEffort !== undefined)
patch.claudeEffort = rest.claudeEffort ?? null;
if (rest.overlapMode !== undefined) patch.overlapMode = rest.overlapMode;
if (rest.autoSyncBeforeFire !== undefined)
patch.autoSyncBeforeFire = rest.autoSyncBeforeFire;
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main/todo-agent/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,8 @@ class TodoScheduler {
maxIterations: schedule.maxIterations,
maxWallClockSec: schedule.maxWallClockSec,
customSystemPrompt: schedule.customSystemPrompt,
claudeModel: schedule.claudeModel,
claudeEffort: schedule.claudeEffort,
artifactPath,
});
supervisor.prepareArtifacts(inserted);
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/todo-agent/session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ class TodoSessionStore {
maxIterations: number;
maxWallClockSec: number;
customSystemPrompt?: string | null;
claudeModel?: string | null;
claudeEffort?: string | null;
artifactPath: string;
}): SelectTodoSession {
return this.insert({
Expand All @@ -275,6 +277,8 @@ class TodoSessionStore {
pendingIntervention: null,
startHeadSha: null,
customSystemPrompt: template.customSystemPrompt ?? null,
claudeModel: template.claudeModel ?? null,
claudeEffort: template.claudeEffort ?? null,
verdictPassed: null,
verdictReason: null,
verdictFailingTest: null,
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main/todo-agent/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const DEFAULT_SETTINGS: TodoSettings = {
defaultMaxWallClockMin: 30,
maxConcurrentTasks: 1,
sessionRetentionDays: 0,
defaultClaudeModel: null,
defaultClaudeEffort: null,
};

let cached: TodoSettings | null = null;
Expand Down
51 changes: 50 additions & 1 deletion apps/desktop/src/main/todo-agent/supervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { getCurrentHeadSha } from "./git-status";
import { getTodoSessionStore, resolveWorktreePath } from "./session-store";
import { getTodoSettings } from "./settings";
import type { TodoStreamEventKind } from "./types";
import { TODO_ARTIFACT_SUBDIR } from "./types";
import {
CLAUDE_EFFORT_OPTIONS,
CLAUDE_MODEL_OPTIONS,
TODO_ARTIFACT_SUBDIR,
} from "./types";

/**
* Headless Claude Code driver for TODO autonomous sessions.
Expand Down Expand Up @@ -318,6 +322,13 @@ class TodoSupervisor {
`${preview}${session0.customSystemPrompt.trim().length > 200 ? "…" : ""}`,
);
}
if (session0.claudeModel || session0.claudeEffort) {
const parts: string[] = [];
if (session0.claudeModel) parts.push(`model: ${session0.claudeModel}`);
if (session0.claudeEffort)
parts.push(`effort: ${session0.claudeEffort}`);
appendSetupEvent(sessionId, "Claude 設定", parts.join(" / "));
}
appendSetupEvent(
sessionId,
"Claude",
Expand Down Expand Up @@ -436,6 +447,8 @@ class TodoSupervisor {
prompt,
resumeSessionId: claudeSessionId,
customSystemPrompt: currentSession.customSystemPrompt ?? null,
claudeModel: currentSession.claudeModel ?? null,
claudeEffort: currentSession.claudeEffort ?? null,
signal: ac.signal,
onChild: (child) => {
run.currentChild = child;
Expand Down Expand Up @@ -663,6 +676,8 @@ class TodoSupervisor {
prompt: string;
resumeSessionId: string | null;
customSystemPrompt: string | null;
claudeModel: string | null;
claudeEffort: string | null;
signal: AbortSignal;
onChild: (child: ChildProcess) => void;
}): Promise<{
Expand Down Expand Up @@ -704,6 +719,40 @@ class TodoSupervisor {
if (params.customSystemPrompt) {
args.push("--append-system-prompt", params.customSystemPrompt);
}
// Per-session Claude Code overrides. Passing `--model` /
// `--effort` only when set keeps Claude Code's own default
// resolution path intact for users who haven't picked one.
//
// Defense-in-depth whitelist: the UI already constrains
// values via `CLAUDE_*_OPTIONS`, but that validation happens
// on the render side. A corrupted / migrated row could still
// persist an unexpected string. We refuse to forward anything
// that isn't in the allow-list so the spawn call can't be
// steered by a malformed DB value.
if (
params.claudeModel &&
(CLAUDE_MODEL_OPTIONS as readonly string[]).includes(params.claudeModel)
) {
args.push("--model", params.claudeModel);
} else if (params.claudeModel) {
console.warn(
"[todo-supervisor] ignoring unknown claudeModel:",
params.claudeModel,
);
}
if (
params.claudeEffort &&
(CLAUDE_EFFORT_OPTIONS as readonly string[]).includes(
params.claudeEffort,
)
) {
args.push("--effort", params.claudeEffort);
} else if (params.claudeEffort) {
console.warn(
"[todo-supervisor] ignoring unknown claudeEffort:",
params.claudeEffort,
);
}
if (params.resumeSessionId) {
args.push("--resume", params.resumeSessionId);
}
Expand Down
18 changes: 18 additions & 0 deletions apps/desktop/src/main/todo-agent/trpc-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ export const createTodoAgentRouter = () => {
workspaceId: input.workspaceId,
});

// Fall through to the user's configured defaults when the
// composer did not pick an explicit model / effort. Null
// at both levels means "use Claude Code's own default"
// (we simply omit the CLI flag at spawn time).
const settings = getTodoSettings();
const resolvedModel =
input.claudeModel !== undefined
? input.claudeModel
: (settings.defaultClaudeModel ?? null);
const resolvedEffort =
input.claudeEffort !== undefined
? input.claudeEffort
: (settings.defaultClaudeEffort ?? null);

const session = store.insertQueuedFromTemplate({
id: sessionId,
projectId: input.projectId ?? null,
Expand All @@ -114,6 +128,8 @@ export const createTodoAgentRouter = () => {
maxIterations: input.maxIterations,
maxWallClockSec: input.maxWallClockSec,
customSystemPrompt: input.customSystemPrompt,
claudeModel: resolvedModel,
claudeEffort: resolvedEffort,
artifactPath,
});

Expand Down Expand Up @@ -391,6 +407,8 @@ export const createTodoAgentRouter = () => {
pendingIntervention: null,
startHeadSha: null,
customSystemPrompt: source.customSystemPrompt,
claudeModel: source.claudeModel,
claudeEffort: source.claudeEffort,
verdictPassed: null,
verdictReason: null,
verdictFailingTest: null,
Expand Down
58 changes: 58 additions & 0 deletions apps/desktop/src/main/todo-agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,50 @@ export interface TodoSessionListEntry extends SelectTodoSession {
projectName: string | null;
}

/**
* Claude Code `--model` values we allow the user to pick from the UI.
* Aliases cover "latest of this tier"; full model names pin a specific
* release. Kept open-ended (plus a default `null` in the storage layer)
* so new models do not require a migration. `default` is the UI-side
* sentinel that maps to `null` (don't pass `--model` at all; let Claude
* Code use whatever the user's own config / ~/.claude.json chose).
*/
export const CLAUDE_MODEL_OPTIONS = [
"opus",
"sonnet",
"haiku",
"claude-opus-4-7",
"claude-sonnet-4-6",
"claude-haiku-4-5-20251001",
] as const;

export type TodoClaudeModel = (typeof CLAUDE_MODEL_OPTIONS)[number];

export const todoClaudeModelSchema = z.enum(CLAUDE_MODEL_OPTIONS);

/**
* Claude Code `--effort` levels. `default` is the UI-side sentinel for
* "don't pass the flag"; actual persisted values are `low`..`max` or
* null.
*
* Thinking support is model-gated in Claude Code; the CLI rejects an
* incompatible effort level at launch. We intentionally don't duplicate
* that matrix here so adding a new model tier on the CLI side doesn't
* require a fork update. The UI surfaces a warning but allows the
* combination; the supervisor forwards whatever the user picked.
*/
export const CLAUDE_EFFORT_OPTIONS = [
"low",
"medium",
"high",
"xhigh",
"max",
] as const;

export type TodoClaudeEffort = (typeof CLAUDE_EFFORT_OPTIONS)[number];

export const todoClaudeEffortSchema = z.enum(CLAUDE_EFFORT_OPTIONS);

export const todoCreateInputSchema = z.object({
workspaceId: z.string().min(1),
projectId: z.string().optional(),
Expand Down Expand Up @@ -57,6 +101,10 @@ export const todoCreateInputSchema = z.object({
.max(20_000)
.optional()
.transform((v) => (v && v.length > 0 ? v : undefined)),
// Optional per-session Claude Code CLI overrides. Null / undefined
// means "use the user's configured default" (see todoSettingsSchema).
claudeModel: todoClaudeModelSchema.nullish(),
claudeEffort: todoClaudeEffortSchema.nullish(),
});

export const todoPresetKindSchema = z.enum(["system", "description", "goal"]);
Expand Down Expand Up @@ -93,6 +141,12 @@ export const todoSettingsSchema = z.object({
// 0 = 無制限 (手動削除のみ). 1-365 = その日数より古い終了済み
// セッションを起動時に自動削除する (queued / running / paused は対象外)。
sessionRetentionDays: z.number().int().min(0).max(365).default(0),
// Global defaults used when the TODO composer / ScheduleEditor does
// not override them. Null = let Claude Code resolve its own default
// (user config cascade). Stored as nullable so the user can pick
// "default" in the settings UI.
defaultClaudeModel: todoClaudeModelSchema.nullish().default(null),
defaultClaudeEffort: todoClaudeEffortSchema.nullish().default(null),
});

export type TodoSettings = z.infer<typeof todoSettingsSchema>;
Expand Down Expand Up @@ -236,6 +290,8 @@ export const todoScheduleCreateInputSchema = z
.max(60 * 60 * 4)
.default(1800),
customSystemPrompt: z.string().trim().max(20_000).nullish(),
claudeModel: todoClaudeModelSchema.nullish(),
claudeEffort: todoClaudeEffortSchema.nullish(),
overlapMode: todoScheduleOverlapModeSchema.default("skip"),
autoSyncBeforeFire: z.boolean().default(false),
})
Expand Down Expand Up @@ -275,6 +331,8 @@ const todoScheduleBaseSchema = z.object({
.min(60)
.max(60 * 60 * 4),
customSystemPrompt: z.string().trim().max(20_000).nullish(),
claudeModel: todoClaudeModelSchema.nullish(),
claudeEffort: todoClaudeEffortSchema.nullish(),
overlapMode: todoScheduleOverlapModeSchema,
autoSyncBeforeFire: z.boolean(),
});
Expand Down
Loading
Loading