Skip to content

feat(desktop): TODO Agent に Crush CLI 対応を追加#386

Merged
MocA-Love merged 5 commits intomainfrom
feat/crush-todo-agent
Apr 23, 2026
Merged

feat(desktop): TODO Agent に Crush CLI 対応を追加#386
MocA-Love merged 5 commits intomainfrom
feat/crush-todo-agent

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

@MocA-Love MocA-Love commented Apr 23, 2026

Summary

  • TODO Agent の自律実行エンジンを Claude Code / Codex CLI に続く3つ目のエージェント として Charmbracelet Crush CLI (crush run) に対応
  • Crush は構造化ストリーミング出力をサポートしないため、プロジェクトローカルの SQLite DB (.crush/crush.db) を 250ms ポーリング してメッセージを取得し、既存の TodoStreamEvent 形式に変換する方式を採用
  • モデル選択は crush models (276+ モデル) を動的フェッチし、provider ごとにグループ化して UI 表示。ハードコードなし。Crush には effort の概念がないため、モデル選択のみ

変更点

コアロジック

  • types.tsAgentKind enum に "crush" を追加、crushModel フィールドをスキーマに追加
  • crush-turn-runner.ts (新規) — crush run --yolo を spawn → .crush/crush.db をポーリング → parts JSON を TodoStreamEvent に変換
    • textassistant_text
    • tool_calltool_use
    • tool_resulttool_result
    • finish (reason による) → result / error
    • reasoning / binary → スキップ
  • supervisor-engine.tsrunAgentTurn()agentKind === "crush" ルートを追加

データ層

  • DB schema — todo_sessions / todo_schedulescrush_model カラム追加
  • マイグレーション — 0069_add_crush_model.sql
  • tRPC — crushModels クエリ追加 (crush models の出力をキャッシュ付きで返す)

UI

  • AgentRuntimePicker.tsx — Agent セレクタに "Crush" 追加。モデルリストは provider ごとにグループ化
  • claudeRuntimeOptions.tsCrushModelPick 型 + 永続化ヘルパー
  • TodoModal.tsxcrushModel state + フェッチ + 送信

設計上の決定

  • 後方互換性: agent_kind のデフォルトは引き続き "claude"。既存セッションへの影響なし
  • SQLite ポーリング: Claude PTY runner の JSONL ポーリングと同じパターン
  • 動的モデルリスト: crush models からランタイムで取得。276+ モデルをハードコードしない
  • Effort なし: Crush CLI に effort/reasoning の概念がないため省略

Test plan

  • bun run typecheck が通ること(27/27 tasks)
  • bun run lint が通ること(warnings only, errors 0)
  • 既存の Claude Code セッションが agent_kind = "claude" で正常動作すること
  • Agent セレクタから "Crush" を選択し、モデル一覧が表示されること
  • Crush CLI がインストール済みの環境で新規セッションを作成・実行できること
  • crush run --yolo が spawn され、DB ポーリングでストリーミング表示されること
  • セッションの再実行(rerun)が crush run --session で正しく動作すること

💘 Generated with Crush

Assisted-by: GLM-5 via Crush crush@charm.land

Summary by CodeRabbit

リリースノート

  • New Features
    • 新しいエージェントタイプ「Crush」をサポートしました
    • Crushエージェント実行時のモデル選択機能を追加しました
    • グローバル設定でデフォルトCrushモデルを指定できるようになりました
    • セッション単位でCrushモデルを上書きできる機能を実装しました

TODO Agent の自律実行エンジンを拡張し、Charmbracelet Crush CLI (`crush run`) を
バックエンドとして選択可能にした。Claude Code / Codex CLI に続く3つ目のエージェント対応。

Crush は stdout に構造化ストリーミングを出力しないため、プロジェクトローカルの
SQLite データベース (`<project>/.crush/crush.db`) をポーリングしてメッセージを
取得し、既存の TodoStreamEvent 形式に変換する方式を採用。
Claude PTY runner の JSONL ポーリングと同じパターン。

モデル選択は `crush models` の出力(276+)を動的フェッチし、
provider ごとにグループ化して UI に表示。ハードコードなし。

## 変更点

### コアロジック
- `types.ts` — `AgentKind` enum に `"crush"` を追加。`crushModel` フィールドを
  `todoCreateInputSchema` / `todoSettingsSchema` に追加
- `crush-turn-runner.ts` (新規) — `crush run --yolo` を spawn → `.crush/crush.db`
  を 250ms 間隔でポーリング → messages テーブルの parts JSON をパースして
  TodoStreamEvent に変換。system_init / assistant_text / tool_use / tool_result /
  result / error の全 kind をマッピング
- `supervisor-engine.ts` — `runAgentTurn()` に `agentKind === "crush"` ルートを追加

### データ層
- DB schema — `todo_sessions` / `todo_schedules` に `crush_model` カラムを追加
- マイグレーション — `0069_add_crush_model.sql` 自動生成
- session-store / tRPC router — 作成・再実行時に crushModel を永続化
- tRPC — `crushModels` クエリ追加(`crush models` の出力をキャッシュ付きで返す)

### UI
- `AgentRuntimePicker.tsx` — Agent セレクタに "Crush" を追加。Crush 選択時は
  モデルのみのセクションを表示(Effort は Crush CLI に概念がないため省略)。
  モデルリストは provider ごとにグループ化
- `claudeRuntimeOptions.ts` — `CrushModelPick` 型 + 永続化ヘルパー
- `TodoModal.tsx` — `crushModel` state + `crushModels` フェッチ + 送信

## 設計上の決定

- **後方互換性**: `agent_kind` のデフォルトは引き続き `"claude"`。
  既存セッションへの影響なし
- **SQLite ポーリング**: Crush は NDJSON 出力をサポートしないため、DB ポーリング
  を選択。Claude PTY runner の JSONL ポーリングと同じアプローチ
- **動的モデルリスト**: 276+ のモデルをハードコードせず `crush models` から動的取得
- **Effort なし**: Crush CLI に effort/reasoning の概念がないため省略

💘 Generated with Crush

Assisted-by: GLM-5 via Crush <crush@charm.land>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@MocA-Love has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 18 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 11 minutes and 18 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c0fe4537-9d91-401d-9bbf-66e2cb096686

📥 Commits

Reviewing files that changed from the base of the PR and between 572562b and 4b80f48.

📒 Files selected for processing (5)
  • apps/desktop/src/main/todo-agent/trpc-router.ts
  • apps/desktop/src/main/todo-daemon/crush-turn-runner.ts
  • apps/desktop/src/main/todo-daemon/supervisor-engine.ts
  • apps/desktop/src/renderer/features/todo-agent/TodoManager/PresetsDialog/PresetsDialog.tsx
  • apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx
📝 Walkthrough

Walkthrough

このプルリクエストは、新しい「Crush」CLIエージェント型を導入し、セッション・スケジュール・設定にcrushModelフィールドを追加し、Crushターン実行エンジンを実装し、Crushモデル選択UIを統合し、利用可能なCrushモデルを取得するTRPCエンドポイントを追加し、データベーススキーマを更新しています。

Changes

Cohort / File(s) Summary
Crush Agent Backend Logic
apps/desktop/src/main/todo-daemon/crush-turn-runner.ts, apps/desktop/src/main/todo-daemon/supervisor-engine.ts
runCrushTurn関数を実装し、Crushバイナリを実行、SQLiteデータベースをポーリングしてストリームイベントをキャプチャ、セッション状態を管理。スーパーバイザーエンジンに"crush"エージェント実行パスを追加し、モデルパラメータとターン結果を処理。
Session・Settings Configuration
apps/desktop/src/main/todo-agent/session-store.ts, apps/desktop/src/main/todo-agent/settings.ts, apps/desktop/src/main/todo-agent/types.ts
セッション作成時にcrushModelフィールドをサポート、デフォルト設定にdefaultCrushModelプロパティを追加、エージェント種別に"crush"を追加し、スキーマ検証を拡張。
TRPC API Endpoints
apps/desktop/src/main/todo-agent/trpc-router.ts
セッション作成・再実行時のCrushモデル解決ロジックを追加、crushModelsパブリッククエリエンドポイントを実装してCrushバイナリから利用可能モデル一覧を取得(10秒タイムアウト付き)。
UI Components
apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/AgentRuntimePicker.tsx, apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/claudeRuntimeOptions.ts, apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/index.ts, apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx
AgentRuntimePickerにCrushモデル選択セクションを追加、プロバイダー別グループ化とデフォルト選択肢をサポート。TodoModalでCrushモデル状態を追跡し、crushModelsクエリを実行してmutationペイロードに含める。
Database Schema
packages/local-db/drizzle/0069_add_crush_model.sql, packages/local-db/drizzle/meta/0069_snapshot.json, packages/local-db/drizzle/meta/_journal.json, packages/local-db/src/schema/todo-schedules.ts, packages/local-db/src/schema/todo-sessions.ts
todo_sessionstodo_schedulesテーブルにcrush_modelテキスト列を追加、Drizzleマイグレーション(バージョン6)とスナップショット、ジャーナルエントリを生成。

Sequence Diagram(s)

sequenceDiagram
    participant User as TodoModal
    participant Supervisor as SupervisorEngine
    participant CrushRunner as runCrushTurn
    participant CrushBinary as crush binary
    participant SQLite as SQLite DB<br/>(.crush/crush.db)
    
    User->>Supervisor: runAgentTurn(crushModel, ...)
    Supervisor->>CrushRunner: runCrushTurn(sessionId, prompt, crushModel, ...)
    
    CrushRunner->>CrushBinary: spawn crush run --yolo<br/>(with model, session resume)
    CrushBinary-->>CrushRunner: emit stderr (session_id)
    CrushRunner->>CrushRunner: capture sessionId
    
    par Database Polling
        CrushRunner->>SQLite: poll messages table<br/>(where session_id, created_at)
        SQLite-->>CrushRunner: new message rows
        CrushRunner->>CrushRunner: parse parts JSON<br/>classifyPart()
        CrushRunner-->>User: emit CrushStreamEvent<br/>(text, tool_call, etc.)
    end
    
    CrushBinary->>CrushBinary: process iteration
    CrushBinary-->>CrushRunner: exit
    
    CrushRunner->>SQLite: final DB sweep<br/>(fetch cost from sessions)
    SQLite-->>CrushRunner: cost data
    CrushRunner-->>Supervisor: return CrushTurnResult<br/>(result, sessionId, cost, etc.)
    Supervisor-->>User: append stream events
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Crushの新しい道をひらき
モデル選択で自由に翔ぶ
SQLiteからイベント舞い降り
セッション、スケジュール、すべて繋がる
四つのエージェント、今ここに揃う ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed タイトルは、Crush CLI対応というPRの主要な変更を簡潔に説明しており、変更内容と関連性がある。
Description check ✅ Passed PRの説明は、テンプレートの主要セクション(説明、関連Issues、変更タイプ、テスト)を部分的に満たしており、技術的詳細が十分に記載されている。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/crush-todo-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 572562b76e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/main/todo-daemon/crush-turn-runner.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/desktop/src/main/todo-daemon/supervisor-engine.ts (1)

278-279: ⚠️ Potential issue | 🟡 Minor

agentKind の型注釈が "crush" を含んでいません。

行 450-451 の runAgentTurn 呼び出し側では "claude" | "codex" | "crush" | null にキャストしているのに対し、ここでは "claude" | "codex" | null のままです。DB 上に "crush" が保存された場合、この変数は型としては正しくないキャストになります(ランタイムは動きますが、以降 agentKind === "codex" 等の分岐で "crush" が常に else に落ちることを型システムで追跡できません)。両者を揃えることをお勧めします。

🔧 差分案
-			const agentKind =
-				(session0.agentKind as "claude" | "codex" | null) ?? "claude";
+			const agentKind =
+				(session0.agentKind as "claude" | "codex" | "crush" | null) ?? "claude";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/todo-daemon/supervisor-engine.ts` around lines 278 -
279, The local variable agentKind is annotated as (session0.agentKind as
"claude" | "codex" | null) but elsewhere (see runAgentTurn call around lines
450-451) the code expects "claude" | "codex" | "crush" | null; update the
annotation here to include "crush" so the union matches the caller: change the
cast on session0.agentKind to "claude" | "codex" | "crush" | null and keep the
same default value logic (e.g., ?? "claude") so the type system correctly tracks
the "crush" branch.
apps/desktop/src/main/todo-agent/types.ts (1)

379-424: ⚠️ Potential issue | 🟡 Minor

スケジュール入力スキーマが crushModel (および codexModel / agentKind) を受け付けません。

todoScheduleCreateInputSchema / todoScheduleBaseSchemaclaudeModel / claudeEffort のみを受け付けており、agentKindcodexModel / crushModel が存在しません。PR の説明では todo_schedules にも crush_model カラムが追加されるとのことなので、DB カラムを活かすためにはスケジュール経由で作成されるセッションで Crush を選択できるよう、将来的にこのスキーマにも agentKind / codexModel / crushModel を追加する想定と思われます。本 PR のスコープ外であれば問題ありませんが、Crush カラムが書き込まれるパスが現状「セッション作成経由のみ」で、スケジュール経由では常に null 固定になる点は留意してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/todo-agent/types.ts` around lines 379 - 424, The
schedule input schemas (todoScheduleCreateInputSchema and
todoScheduleBaseSchema) currently only accept claudeModel/claudeEffort and must
be extended to accept agentKind, codexModel and crushModel so schedules can
create sessions with Crush; add fields agentKind (use the same enum/schema used
elsewhere for session agent kind), codexModel and crushModel to both
todoScheduleBaseSchema and todoScheduleCreateInputSchema (use the same
types/nullish/defaults as used for session inputs), and update any .refine or
validation logic to account for these new fields (e.g., ensure model presence
when agentKind equals the corresponding value) so schedule-created sessions can
persist crush_model in the DB.
🧹 Nitpick comments (6)
packages/local-db/src/schema/todo-sessions.ts (1)

100-114: agentKind のコメントにも crush を反映してください。

Line 100 の説明が "claude" / "codex" のままなので、新しい crushModel コメントと食い違っています。

修正例
-		// Which agent CLI to use: "claude" (Claude Code) or "codex" (Codex CLI).
+		// Which agent CLI to use: "claude" (Claude Code), "codex" (Codex CLI),
+		// or "crush" (Crush CLI).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/local-db/src/schema/todo-sessions.ts` around lines 100 - 114, Update
the comment for the agentKind field to include the new "crush" option so it no
longer contradicts crushModel; specifically edit the comment above agentKind
(symbol: agentKind) to list the valid values "claude", "codex", and "crush",
keep the default as "claude" for backward compatibility, and mention that this
determines which turn runner the supervisor engine dispatches to (so it aligns
with the crushModel comment and the crushModel symbol).
apps/desktop/src/main/todo-agent/trpc-router.ts (1)

551-551: 不要な any cast を外してください。

SelectTodoSessioncrushModel が追加されているため、ここは型安全に参照できます。

修正例
-					crushModel: (source as any).crushModel ?? null,
+					crushModel: source.crushModel ?? null,

As per coding guidelines, **/*.{ts,tsx}: Avoid any type unless necessary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/todo-agent/trpc-router.ts` at line 551, Remove the
unnecessary any cast and access crushModel in a type-safe way: replace (source
as any).crushModel ?? null with a typed reference (e.g. (source as
SelectTodoSession).crushModel ?? null) or, better, ensure the surrounding
code/parameter is typed as SelectTodoSession so you can simply use
source.crushModel ?? null; target the crushModel property on SelectTodoSession
to remove the any cast.
packages/local-db/src/schema/todo-schedules.ts (1)

65-75: schedule 側の agentKind コメントも更新してください。

Line 65 が "claude" / "codex" のみを列挙しており、追加された crushModel と整合していません。

修正例
-		// Which agent CLI to use: "claude" or "codex". Defaults to "claude".
+		// Which agent CLI to use: "claude", "codex", or "crush".
+		// Defaults to "claude".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/local-db/src/schema/todo-schedules.ts` around lines 65 - 75, Update
the comment for the agentKind column in
packages/local-db/src/schema/todo-schedules.ts to reflect the newly supported
agent types: include "crush" alongside "claude" and "codex" so it matches the
added crushModel field; locate the agentKind declaration (agentKind:
text("agent_kind").notNull().default("claude")) and expand the inline comment to
enumerate "claude", "codex", and "crush" and optionally note which model
overrides (codexModel, codexEffort, crushModel) apply to each agentKind.
apps/desktop/src/main/todo-daemon/supervisor-engine.ts (1)

265-308: Crush 用のセットアップバナーがないため、選択モデルが UI 上で可視化されません。

Claude / Codex はそれぞれ session0.claudeModel / session0.codexModel を読んで appendSetupEvent("Claude 設定", ...) / appendSetupEvent("Codex 設定", ...) を出力していますが、Crush には同等の表示がありません。agentKind === "crush" の場合に「Crush」エンジンを起動する旨のバナーと、選択モデルの表示を追加すると、既存エージェントとの一貫性が保たれユーザーが設定ミスを発見しやすくなります。

♻️ 差分案(概略)
 			if (session0.codexModel || session0.codexEffort) {
 				const parts: string[] = [];
 				if (session0.codexModel) parts.push(`model: ${session0.codexModel}`);
 				if (session0.codexEffort) parts.push(`effort: ${session0.codexEffort}`);
 				appendSetupEvent(sessionId, "Codex 設定", parts.join(" / "));
 			}
+			const crushModel = (session0 as SelectTodoSession & {
+				crushModel?: string | null;
+			}).crushModel;
+			if (crushModel) {
+				appendSetupEvent(sessionId, "Crush 設定", `model: ${crushModel}`);
+			}
@@
-			if (agentKind === "codex") {
+			if (agentKind === "codex") {
 				appendSetupEvent(
 					sessionId,
 					"Codex",
 					"codex exec --json --full-auto を起動します",
 				);
 				...
+			} else if (agentKind === "crush") {
+				appendSetupEvent(
+					sessionId,
+					"Crush",
+					"crush run --yolo を起動します",
+				);
 			} else {

加えて const agentKind の型注釈(行 279)も "claude" | "codex" | "crush" | null に拡張する必要があります。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/todo-daemon/supervisor-engine.ts` around lines 265 -
308, The code never emits a setup banner for the "crush" agent so selected Crush
model/config isn't shown in the UI; update the agentKind type annotation on the
const agentKind (currently "claude" | "codex" | null) to include "crush", then
add a branch alongside the existing agentKind === "codex" / else handling that
calls appendSetupEvent(sessionId, "Crush", "<Crush startup message>") and also
emits a Crush 設定 setup event when session0.crushModel or session0.crushEffort
are present (mirroring the pattern used for session0.claudeModel /
session0.codexModel and appendSetupEvent("Claude 設定"/"Codex 設定")). Ensure the
willUsePty / remoteControlEnabled logic remains unchanged and reuse the same
parts.join(" / ") formatting for Crush setup events.
apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/AgentRuntimePicker.tsx (1)

260-280: SelectGroup / SelectLabel でプロバイダーごとにグルーピングし視覚的な区切りを追加する

groupByProvider で分類したのに、UI 上はフラットな SelectItem の連続になっており、プロバイダーごとの区切りが見出しとして表示されていません。200+ モデルを扱う場合、SelectGroupSelectLabel を使うと探しやすくなります。

♻️ 差分案
-import {
-	Select,
-	SelectContent,
-	SelectItem,
-	SelectTrigger,
-	SelectValue,
-} from "@superset/ui/select";
+import {
+	Select,
+	SelectContent,
+	SelectGroup,
+	SelectItem,
+	SelectLabel,
+	SelectTrigger,
+	SelectValue,
+} from "@superset/ui/select";
@@
-				<SelectContent className="max-h-64">
-					<SelectItem value={DEFAULT_SENTINEL}>
-						<span className="text-xs font-medium">デフォルト</span>
-					</SelectItem>
-					{Object.entries(grouped).map(([provider, models]) =>
-						models.map((model) => (
-							<SelectItem key={model} value={model}>
-								<span className="text-xs font-medium">
-									{model.replace(`${provider}/`, "")}
-								</span>
-								<span className="text-[10px] text-muted-foreground ml-1">
-									{provider}
-								</span>
-							</SelectItem>
-						)),
-					)}
-				</SelectContent>
+				<SelectContent className="max-h-64">
+					<SelectItem value={DEFAULT_SENTINEL}>
+						<span className="text-xs font-medium">デフォルト</span>
+					</SelectItem>
+					{Object.entries(grouped).map(([provider, models]) => (
+						<SelectGroup key={provider}>
+							<SelectLabel className="text-[10px] uppercase tracking-wide text-muted-foreground">
+								{provider}
+							</SelectLabel>
+							{models.map((model) => (
+								<SelectItem key={model} value={model}>
+									<span className="text-xs font-medium">
+										{model.slice(provider.length + 1) || model}
+									</span>
+								</SelectItem>
+							))}
+						</SelectGroup>
+					))}
+				</SelectContent>

合わせて model.replace から slice に切り替えると、意図の明確化につながります。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/AgentRuntimePicker.tsx`
around lines 260 - 280, The UI currently renders a flat list of SelectItem from
grouped but should use SelectGroup and SelectLabel to visually separate
providers; update the render inside SelectContent to iterate
Object.entries(grouped) and for each provider render a SelectGroup with a
SelectLabel showing the provider and map its models to SelectItem; also change
model.replace(`${provider}/`, "") to use model.slice(provider.length + 1) (or
equivalent) to make the trimming intent clearer; keep DEFAULT_SENTINEL handling
as a top-level SelectItem.
apps/desktop/src/main/todo-daemon/crush-turn-runner.ts (1)

99-118: stderr から session_id を抽出するパース方法は脆弱です。

Crush の公式ドキュメントでは、非対話型実行時に stderr へ session_id を出力することが明記されていません。このため、ログフォーマットの更新があると正規表現が一致しなくなり、crushSessionId が永遠に null のままとなります。その場合、pollDbForEventsgetSessionId() が null で continue し続け、DB へのクエリが一度も発行されずイベントが UI に届きません。

フォールバックとして、起動後一定時間 session_id が取れなければ DB 側で sessions テーブルを created_at の大きい順に最新 1 件 look-up する、または少なくとも一定時間経過後に警告イベントを emit してユーザーに気付かせる仕組みがあると、障害対応しやすくなります。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/todo-daemon/crush-turn-runner.ts` around lines 99 -
118, stderr からの正規表現抽出だけに頼る現在の crushSessionId ロジックは壊れやすいので、一定時間(例: 起動後 5–10 秒)以内に
crushSessionId が取得できなければフォールバックを行うように修正してください: pollDbForEvents / getSessionId
の呼び出し経路にタイムアウト判定を追加して(crushSessionId が null のまま一定時間経過したら)① DB の sessions テーブルを
created_at DESC で最新 1 件を look-up して session_id を取得して使う処理を実装するか、② DB lookup
ができない/失敗した場合は params.emit を使って system_warn(例: "Crush セッションID未取得のため DB を参照しました" や
"セッションID未取得")を送出するようにし、crushSessionId 変数と pollDbForEvents のループが永久に続かないようにしてください。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/todo-agent/trpc-router.ts`:
- Around line 1039-1057: The crushModels query currently spawns `crush models`
on every request; add a module-level cache (use an interface like
CrushModelsCache with fields values, expiresAt, inflight and constants like
CRUSH_MODELS_CACHE_TTL_MS and a crushModelsCache instance) and modify the
crushModels publicProcedure.query to: return cached values if not expired, if
expired but crushModelsCache.inflight exists await it, otherwise set
crushModelsCache.inflight to a Promise that runs the execFileAsync call, updates
crushModelsCache.values and expiresAt on success, clears inflight and returns
the values; on error clear inflight and return an empty array (or existing
values) so parallel requests are deduped and results are TTL-cached.

In `@apps/desktop/src/main/todo-daemon/crush-turn-runner.ts`:
- Around line 276-289: The block that checks for p.type === "finish" with reason
"end_turn" or "stop" is a no-op and should either be removed or mark the session
as finished; update it to set the existing settled flag and exit the poll so
polling stops. Locate the code in pollDbForEvents (crush-turn-runner.ts) where
safeParseJson(lastRow.parts) is iterated, and inside the branch for p.type ===
"finish" && (p.data?.reason === "end_turn" || p.data?.reason === "stop") assign
settled = true and then break/return so the polling loop exits (matching the
earlier critical fix that uses settled). Ensure you use the same settled
identifier used elsewhere in this file so state remains consistent.
- Around line 405-415: In killProcess, avoid calling taskkill with "undefined"
by checking child.pid before converting it to a string and before spawning; if
child.pid is undefined, return early (or fall back to child.kill) instead of
calling spawn("taskkill", [String(child.pid), ...]); similarly ensure the
non-Windows branch only calls process.kill when child.pid is present; keep the
try/catch so the existing child.kill("SIGKILL") fallback remains on errors.
- Around line 256-294: The code assumes safeParseJson(...) returns an array but
may return non-array values, causing silent TypeErrors and endless retries;
update the logic around safeParseJson(row.parts) (used in the parts loop where
classifyPart(...) is called) and safeParseJson(lastRow.parts) (used where
lastParts is iterated) to explicitly check Array.isArray(parts) /
Array.isArray(lastParts) before using for...of, and when the value is not an
array skip processing (and optionally emit a warning via params.emit or a
logger) so non-array JSON will not throw inside the polling try/catch.
- Around line 127-155: The poll loop in pollDbForEvents never stops because the
local `const settled = false` is never updated; modify pollDbForEvents to accept
an external "done" flag or callback (e.g., a boolean reference or onDone
function) and break the loop when that notifier is set, and at the call site
where you create `pollPromise = pollDbForEvents(params, () => crushSessionId)`
update the call to pass a notifier you can flip from the child handlers (use the
existing `child` variable and its "close" handler to set interrupted/childExited
and call the notifier or set the flag); ensure the "error" and "abort" paths
also trigger the same notifier so pollDbForEvents always exits after the child
closes.
- Around line 161-175: The SELECT result from SQLite may contain NULL for the
cost field; update the block that reads from Database (use symbols
crushSessionId, findCrushDb, Database, sessions, costUsd, and row.cost) to
defensively validate the value before assigning: after fetching row (which may
be undefined), check that row.cost is neither null nor undefined and that typeof
row.cost === "number" (or coerce/parse safely) and only then set costUsd =
row.cost; otherwise leave costUsd unchanged (or handle fallback). Ensure this
check sits inside the existing try block where the Database is opened and the
row is retrieved.

In `@apps/desktop/src/main/todo-daemon/supervisor-engine.ts`:
- Line 456: Remove the unnecessary "as any" cast and access the crushModel
property directly: replace "(currentSession as any).crushModel ?? null" with
"currentSession.crushModel ?? null" and ensure the variable currentSession is
typed (or inferred) as SelectTodoSession so TypeScript knows crushModel exists
(or add an explicit type annotation of SelectTodoSession to currentSession). If
currentSession can be undefined, guard or narrow its type before accessing
crushModel.

---

Outside diff comments:
In `@apps/desktop/src/main/todo-agent/types.ts`:
- Around line 379-424: The schedule input schemas (todoScheduleCreateInputSchema
and todoScheduleBaseSchema) currently only accept claudeModel/claudeEffort and
must be extended to accept agentKind, codexModel and crushModel so schedules can
create sessions with Crush; add fields agentKind (use the same enum/schema used
elsewhere for session agent kind), codexModel and crushModel to both
todoScheduleBaseSchema and todoScheduleCreateInputSchema (use the same
types/nullish/defaults as used for session inputs), and update any .refine or
validation logic to account for these new fields (e.g., ensure model presence
when agentKind equals the corresponding value) so schedule-created sessions can
persist crush_model in the DB.

In `@apps/desktop/src/main/todo-daemon/supervisor-engine.ts`:
- Around line 278-279: The local variable agentKind is annotated as
(session0.agentKind as "claude" | "codex" | null) but elsewhere (see
runAgentTurn call around lines 450-451) the code expects "claude" | "codex" |
"crush" | null; update the annotation here to include "crush" so the union
matches the caller: change the cast on session0.agentKind to "claude" | "codex"
| "crush" | null and keep the same default value logic (e.g., ?? "claude") so
the type system correctly tracks the "crush" branch.

---

Nitpick comments:
In `@apps/desktop/src/main/todo-agent/trpc-router.ts`:
- Line 551: Remove the unnecessary any cast and access crushModel in a type-safe
way: replace (source as any).crushModel ?? null with a typed reference (e.g.
(source as SelectTodoSession).crushModel ?? null) or, better, ensure the
surrounding code/parameter is typed as SelectTodoSession so you can simply use
source.crushModel ?? null; target the crushModel property on SelectTodoSession
to remove the any cast.

In `@apps/desktop/src/main/todo-daemon/crush-turn-runner.ts`:
- Around line 99-118: stderr からの正規表現抽出だけに頼る現在の crushSessionId
ロジックは壊れやすいので、一定時間(例: 起動後 5–10 秒)以内に crushSessionId
が取得できなければフォールバックを行うように修正してください: pollDbForEvents / getSessionId
の呼び出し経路にタイムアウト判定を追加して(crushSessionId が null のまま一定時間経過したら)① DB の sessions テーブルを
created_at DESC で最新 1 件を look-up して session_id を取得して使う処理を実装するか、② DB lookup
ができない/失敗した場合は params.emit を使って system_warn(例: "Crush セッションID未取得のため DB を参照しました" や
"セッションID未取得")を送出するようにし、crushSessionId 変数と pollDbForEvents のループが永久に続かないようにしてください。

In `@apps/desktop/src/main/todo-daemon/supervisor-engine.ts`:
- Around line 265-308: The code never emits a setup banner for the "crush" agent
so selected Crush model/config isn't shown in the UI; update the agentKind type
annotation on the const agentKind (currently "claude" | "codex" | null) to
include "crush", then add a branch alongside the existing agentKind === "codex"
/ else handling that calls appendSetupEvent(sessionId, "Crush", "<Crush startup
message>") and also emits a Crush 設定 setup event when session0.crushModel or
session0.crushEffort are present (mirroring the pattern used for
session0.claudeModel / session0.codexModel and appendSetupEvent("Claude
設定"/"Codex 設定")). Ensure the willUsePty / remoteControlEnabled logic remains
unchanged and reuse the same parts.join(" / ") formatting for Crush setup
events.

In
`@apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/AgentRuntimePicker.tsx`:
- Around line 260-280: The UI currently renders a flat list of SelectItem from
grouped but should use SelectGroup and SelectLabel to visually separate
providers; update the render inside SelectContent to iterate
Object.entries(grouped) and for each provider render a SelectGroup with a
SelectLabel showing the provider and map its models to SelectItem; also change
model.replace(`${provider}/`, "") to use model.slice(provider.length + 1) (or
equivalent) to make the trimming intent clearer; keep DEFAULT_SENTINEL handling
as a top-level SelectItem.

In `@packages/local-db/src/schema/todo-schedules.ts`:
- Around line 65-75: Update the comment for the agentKind column in
packages/local-db/src/schema/todo-schedules.ts to reflect the newly supported
agent types: include "crush" alongside "claude" and "codex" so it matches the
added crushModel field; locate the agentKind declaration (agentKind:
text("agent_kind").notNull().default("claude")) and expand the inline comment to
enumerate "claude", "codex", and "crush" and optionally note which model
overrides (codexModel, codexEffort, crushModel) apply to each agentKind.

In `@packages/local-db/src/schema/todo-sessions.ts`:
- Around line 100-114: Update the comment for the agentKind field to include the
new "crush" option so it no longer contradicts crushModel; specifically edit the
comment above agentKind (symbol: agentKind) to list the valid values "claude",
"codex", and "crush", keep the default as "claude" for backward compatibility,
and mention that this determines which turn runner the supervisor engine
dispatches to (so it aligns with the crushModel comment and the crushModel
symbol).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d1173ec-f7bb-4f4b-b6d4-13e15edff246

📥 Commits

Reviewing files that changed from the base of the PR and between eac37de and 572562b.

📒 Files selected for processing (15)
  • apps/desktop/src/main/todo-agent/session-store.ts
  • apps/desktop/src/main/todo-agent/settings.ts
  • apps/desktop/src/main/todo-agent/trpc-router.ts
  • apps/desktop/src/main/todo-agent/types.ts
  • apps/desktop/src/main/todo-daemon/crush-turn-runner.ts
  • apps/desktop/src/main/todo-daemon/supervisor-engine.ts
  • apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/AgentRuntimePicker.tsx
  • apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/claudeRuntimeOptions.ts
  • apps/desktop/src/renderer/features/todo-agent/ClaudeRuntimePicker/index.ts
  • apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx
  • packages/local-db/drizzle/0069_add_crush_model.sql
  • packages/local-db/drizzle/meta/0069_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/src/schema/todo-schedules.ts
  • packages/local-db/src/schema/todo-sessions.ts

Comment thread apps/desktop/src/main/todo-agent/trpc-router.ts
Comment thread apps/desktop/src/main/todo-daemon/crush-turn-runner.ts Outdated
Comment thread apps/desktop/src/main/todo-daemon/crush-turn-runner.ts
Comment thread apps/desktop/src/main/todo-daemon/crush-turn-runner.ts
Comment thread apps/desktop/src/main/todo-daemon/crush-turn-runner.ts
Comment thread apps/desktop/src/main/todo-daemon/crush-turn-runner.ts
Comment thread apps/desktop/src/main/todo-daemon/supervisor-engine.ts Outdated
`pollDbForEvents` の `settled` が `const false` だったため、正常終了時に
`await pollPromise` が永久にブロックしセッションが running でスタックしていた。

修正:
- `settled` を `let` に変更し、DB で `finish` part (end_turn/stop) を
  検知したら `settled = true` にする
- `isChildExited` コールバックを追加し、子プロセス終了後もポーリングが
  停止するようにした(DB に finish が書かれないエラー/シグナルケース対応)

Codex review P1 指摘への対応。

💘 Generated with Crush

Assisted-by: GLM-5 via Crush <crush@charm.land>
- crush-turn-runner: `safeParseJson` で `Array.isArray` チェックを追加し、
  非 JSON 配列が渡った際の TypeError を防止
- crush-turn-runner: `killProcess` で `child.pid` の undefined ガードを追加
- crush-turn-runner: `cost` 取得時の NULL チェックを追加
  (`typeof === "number" && Number.isFinite`)
- supervisor-engine: `(currentSession as any).crushModel` の `as any` を削除
- trpc-router: `(source as any).crushModel` の `as any` を削除
- trpc-router: `crushModels` クエリに TTL キャッシュ (5分) と
  inflight 重複排除を追加

💘 Generated with Crush

Assisted-by: GLM-5 via Crush <crush@charm.land>
PresetsDialog (設定タブ) と TodoManager (インラインコンポーザー) に
Crush CLI 向けモデル選択 UI を追加。PR #387 で Codex に対応した
両コンポーネントで Crush の抜けがあったのを補完。

💘 Generated with Crush

Assisted-by: GLM-5 via Crush <crush@charm.land>
@MocA-Love MocA-Love merged commit 4973a9d into main Apr 23, 2026
13 of 14 checks passed
@MocA-Love MocA-Love deleted the feat/crush-todo-agent branch April 23, 2026 04:02
MocA-Love added a commit that referenced this pull request Apr 23, 2026
@MocA-Love MocA-Love restored the feat/crush-todo-agent branch April 23, 2026 04:07
@MocA-Love MocA-Love deleted the feat/crush-todo-agent branch April 23, 2026 04:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant