Skip to content

feat(desktop): TODO Agent に PTY エンジン + Remote Control 統合を追加 (opt-in)#278

Merged
MocA-Love merged 7 commits intomainfrom
feat/todo-agent-remote-control
Apr 17, 2026
Merged

feat(desktop): TODO Agent に PTY エンジン + Remote Control 統合を追加 (opt-in)#278
MocA-Love merged 7 commits intomainfrom
feat/todo-agent-remote-control

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

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

概要

TODO Agent のセッションを Claude 公式の Remote Control (claude.ai/code / Claude iOS・Android アプリ) から閲覧・操作できるようにする opt-in 実装。

従来の claude -p --output-format stream-json ヘッドレス経路は interactive TUI を持たないため /remote-control を受けられないので、node-pty で interactive claude を起動し、~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl を tail するサブエンジンを追加した。既存の -p 経路はそのまま残し、環境変数 + per-session チェックで段階的に移行する。

設計メモ: apps/desktop/plans/20260417-todo-agent-remote-control.md

主要変更

バックエンド (daemon)

  • apps/desktop/src/main/todo-daemon/pty-turn-runner.ts を新規追加
    • node-pty で interactive claude を起動し、--settings に Stop hook を inline 注入してターン終了を検知
    • ~/.claude/projects/<encoded-cwd>/*.jsonl を polling + offset で tail、assistant / user(tool_result) / assistant(tool_use) / assistant(thinking) を既存 TodoStreamEvent にマッピング
    • Remote Control 有効時は PTY に /remote-control を送信、stdout を ANSI ストリップして https://claude.ai/code/session_... を抽出 → stream event (remote_control kind) として UI に流す
    • 失敗時 (未ログイン / プラン未対応) は remote_control_error kind のイベントを emit
    • interventions / abort / signal 連携は supervisor 側の既存 flow に追従
  • supervisor-engine.tsrunClaudeTurn を feature flag で dispatch
    • TODO_ENGINE=pty が設定されている or セッション側の remoteControlEnabled が true のとき、PTY 経路へ
    • それ以外は従来の -p ヘッドレス経路 (runClaudeTurnHeadless に分離)
    • セットアップバナーに起動方式と Remote Control 状態を表示

DB / API

  • todo_sessions.remote_control_enabled 列を追加 (migration 0059_todo_remote_control_enabled.sql)
  • tRPC todoAgent.create 入力に remoteControlEnabled (optional, default false) を追加
  • session-store insertQueuedFromTemplate / trpc recreate が RC フラグを引き継ぐ
  • TodoStreamEventKindremote_control / remote_control_error を追加

UI

  • TodoModal に「Remote Control を有効化」チェックボックスを追加 (indigo 系ハイライト、Pro/Max 必須の注意書き付き)
  • TodoManager ライブストリームで remote_control イベントを indigo バッジ + 接続 URL リンク (クリックで外部ブラウザ) として描画、remote_control_error は amber バッジでエラー理由を表示

運用方針 (dogfood 期間)

条件 挙動
TODO_ENGINE 未設定 + RC チェック OFF 従来通り -p ヘッドレス
TODO_ENGINE=pty + RC チェック OFF PTY 経路 (RC なし)
TODO_ENGINE 未設定 + RC チェック ON PTY 経路 (/remote-control 発行)
TODO_ENGINE=pty + RC チェック ON PTY 経路 (/remote-control 発行)

既存 -p 経路は残してあり、問題があれば RC チェックを外すだけで従来動作に戻る。dogfood で安定確認ができたら後続 PR で -p 経路を撤去予定。

既知の制約 / フォローアップ

  • per-token streaming は無し: JSONL には text_delta が書かれないので、PTY 経路では whole message 単位のライブ表示になる
  • ScheduleEditorDialog には RC トグル未実装: スケジュール経由のセッションは常に RC off で作成される (UI を追加する後続 PR 予定)
  • Remote Control URL の永続化無し: daemon 再起動で RC セッションは切れるので URL は stream event のみで表現。再接続導線は後続 PR
  • 並列実行時の JSONL race: fs.watch ではなく polling で project dir の新規ファイルを検出している。同一 cwd で 2 セッション同時起動した場合の race は理論上あるが worktree 分離が主用途なので実害は小
  • ネイティブ rebuild: node-pty は既に terminal-host で使用中のため電池同梱

動作確認 (手元 POC + 想定フロー)

手元の /tmp/claude-poc-rc/poc.mjs で以下を事前検証済み:

  • PTY spawn → JSONL 生成 → Stop hook 発火 → assistant 応答取得まで end-to-end で通る
  • tool_use / tool_result / thinking を JSONL tail で観測可能
  • /remote-controlhttps://claude.ai/code/session_01LGA... を ANSI stripped stdout から抽出成功
  • mid-session で追加プロンプトも PTY stdin で送れる

本ブランチの daemon 統合版の最終確認 (dev server 起動 → TODO 作成 → RC チェック → claude.ai/code 接続) はマージ後の dogfood で行う前提。

事前要件 (Remote Control を使うユーザー向け)

  • claude auth login 済み (claude.ai OAuth)
  • Claude Code v2.1.51 以上
  • Pro / Max / Team / Enterprise プラン (Team/Enterprise は admin による Remote Control トグル有効化が必要)

Test plan

  • 既存 TODO セッション作成 (RC OFF) が従来通り動くこと
  • RC ON で新規 TODO を作成 → ライブストリームに indigo バッジと URL が出ること
  • URL をクリックで外部ブラウザが開くこと
  • claude.ai/code から該当セッションに接続できること (Pro/Max 必須)
  • 未ログイン時に remote_control_error バッジが出ること

Summary by CodeRabbit

リリースノート

  • 新機能

    • TODO セッション作成時に Remote Control を有効にするオプションを追加
    • Remote Control セッション中、UIにライブバッジとリンクを表示
    • Remote Control エラーの通知機能を実装
  • ドキュメント

    • Remote Control 機能の実装計画書を追加
  • Chores

    • .gitignoreに一時ファイルディレクトリを追加

PTY + JSONL tail ベースの代替エンジンで Claude Code Remote Control
(claude.ai/code や iOS/Android アプリからの接続) を opt-in 提供する
ための設計ドキュメント。POC で検証済の挙動と feature flag 方針を記録。
PTY エンジン経由で起動された TODO セッションが Remote Control を
opt-in で有効化できるようにするための列。既存セッションは
default false で影響なし。URL 側は永続化せず stream event で表現する。
- `apps/desktop/src/main/todo-daemon/pty-turn-runner.ts` を新規追加し、
  node-pty で interactive claude を起動、~/.claude/projects/.../*.jsonl
  を tail して TodoStreamEvent に変換する
- `--settings` に inline で Stop hook を仕込み、ターン終了を検出
- Remote Control 有効時は PTY に `/remote-control` を送信し、
  stdout から `https://claude.ai/code/session_...` を抽出して
  stream event (`remote_control` kind) として UI に通知
- `supervisor-engine.ts` は `TODO_ENGINE=pty` 環境変数または
  セッションの remote_control_enabled フラグで PTY 経路に dispatch。
  従来の `-p` ヘッドレス経路はそのまま残る (dogfood 後に削除予定)
- tRPC todoAgent.create / todoCreateInputSchema に
  remoteControlEnabled を追加
- TodoStreamEventKind に `remote_control` / `remote_control_error` を追加
- TodoModal: 「Remote Control を有効化」チェックボックスを追加。
  ON で作成したセッションは PTY エンジン経路で起動し、
  /remote-control を発行する
- TodoManager ライブストリーム: remote_control 種別のイベントを
  indigo バッジ + 接続 URL (クリックで外部ブラウザ) として描画。
  エラー時は amber バッジで理由を表示
- store.insertQueuedFromTemplate / trpc todoAgent.recreate で
  remoteControlEnabled を受け渡し、スキーマと整合
- typecheck / lint は成功 (PTY 経路の型エラーと ANSI 正規表現の
  biome-ignore コメント位置を修正)
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Warning

Rate limit exceeded

@MocA-Love has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 38 minutes and 11 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 38 minutes and 11 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: 7c62386b-c455-480d-a4d6-aa332cfd36d8

📥 Commits

Reviewing files that changed from the base of the PR and between b2725a3 and 261a45d.

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

概要

TODO AgentにClaude Code「リモートコントロール」機能を実装するための変更。PTY(疑似ターミナル)ベースのターンランナーを導入し、既存のヘッドレスclaude -p --output-format stream-jsonアプローチに代わるインタラクティブな実行モードを追加。remoteControlEnabledフラグをセッションレベルで管理し、UI・データベース・tRPCパイプラインを通じてサポートを追加。

変更内容

コホート / ファイル 説明
設定管理
.gitignore
temp/ディレクトリをGit追跡から除外。
計画ドキュメント
apps/desktop/plans/20260417-todo-agent-remote-control.md
Remote Control統合の設計計画を新規追加。POC検証、提案アーキテクチャ、データベーススキーマ変更、UI実装手順を記載。
セッション永続化
apps/desktop/src/main/todo-agent/session-store.ts, packages/local-db/src/schema/todo-sessions.ts
TodoSessionStore.insertQueuedFromTemplateremoteControlEnabledフィールドを追加。デフォルト値はfalse。データベーススキーマにも対応カラムを定義。
型定義・スキーマ拡張
apps/desktop/src/main/todo-agent/types.ts
todoCreateInputSchemaremoteControlEnabled: boolean(オプション、デフォルトfalse)を追加。TodoStreamEventKind"remote_control""remote_control_error"イベント型を追加。
tRPCルータ連携
apps/desktop/src/main/todo-agent/trpc-router.ts
セッション作成・再実行時にremoteControlEnabledをペイロードに含める。入力値を新規セッションへ伝播。
PTYターンランナー実装
apps/desktop/src/main/todo-daemon/pty-turn-runner.ts
新規モジュールとしてrunClaudeTurnPty関数を実装。Node PTYでclaude TUIを起動、対話的にプロンプト送信、JSONL出力をリアルタイム解析、Remote Control URLを検出。セッション中止時はPTYを強制終了。extractAttachmentPathsヘルパーも提供。
スーパーバイザーエンジン統合
apps/desktop/src/main/todo-daemon/supervisor-engine.ts
PTY_ENGINE_ENABLEDフィーチャーフラグを導入。runClaudeTurnをasync化し、remoteControlEnabledパラメータを受け取ってPTYランナーへ条件付きでルート。既存ヘッドレスランナーへのフォールバック処理を維持。
UIコンポーネント
apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx, apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx
TodoModalにリモートコントロール有効化チェックボックスを追加。TodoManagerremote_control/remote_control_errorイベントを処理し、URLをバッジとしてレンダリング、エラーは琥珀色スタイルで表示。
データベースマイグレーション
packages/local-db/drizzle/0059_todo_remote_control_enabled.sql, packages/local-db/drizzle/meta/0059_snapshot.json, packages/local-db/drizzle/meta/_journal.json
todo_sessionsテーブルにremote_control_enabledカラムを追加(INTEGER、非NULL、デフォルトfalse)。Drizzleスキーマスナップショット・ジャーナルを更新。

シーケンス図

sequenceDiagram
    participant User as ユーザー (UI)
    participant Modal as TodoModal
    participant tRPC as tRPC Router
    participant SessionStore as SessionStore
    participant Supervisor as Supervisor
    participant PTY as PTY Runner
    participant Claude as Claude TUI
    participant Stream as JSONL Stream

    User->>Modal: remoteControlEnabledフラグをON
    User->>Modal: セッション作成を送信
    Modal->>tRPC: create(remoteControlEnabled: true)
    tRPC->>SessionStore: insertQueuedFromTemplate({ remoteControlEnabled: true })
    SessionStore->>SessionStore: DBに remoteControlEnabled=true で行を作成
    Note over Supervisor: 監視ループ起動
    Supervisor->>Supervisor: remoteControlEnabled を確認
    Supervisor->>PTY: runClaudeTurnPty({ remoteControlEnabled: true, ... })
    PTY->>Claude: node-ptyで claude TUI を起動
    PTY->>Claude: /remote-control 有効化
    User->>Claude: (ブラウザで Remote Control URL にアクセス)
    Claude->>Claude: Claude Code で対話実行
    Claude->>Stream: session.jsonl に イベント追記
    PTY->>Stream: JSONLを監視、イベント行をパース
    Stream-->>PTY: remote_control イベント(URLを含む)
    PTY->>SessionStore: イベントをセッションストアに記録
    SessionStore-->>Supervisor: イベント完了通知
    Supervisor->>User: TodoManager に remote_control イベント表示
    User->>User: バッジと URL リンクが表示される
Loading

推定コードレビュー工数

🎯 4 (複雑) | ⏱️ ~50分

関連する可能性のあるPR

🐰 ターミナルの窓を開けて、
Claude Code とウサギが手をつなぎ
リモートコントロール、ぴょんぴょん走る 🌙
データベースはくすくす笑い
TODOたちが生きが良くなった~ ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed タイトルは主要な変更内容「PTY エンジン + Remote Control 統合を opt-in で追加」を明確かつ簡潔に表現している。
Description check ✅ Passed 説明は構造化されており、概要、主要変更、運用方針、既知制約、事前要件、テストプランを含む包括的な内容となっている。

✏️ 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/todo-agent-remote-control

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: b2725a36af

ℹ️ 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 on lines +539 to +541
const script = `#!/bin/sh
set -e
EVENT="$1"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make PTY Stop-hook command work on Windows

This hook sink always generates a POSIX shell script (#!/bin/sh) and registers it as the Stop hook command, but there is no Windows-specific path. In PTY mode on Windows (either TODO_ENGINE=pty or a session with remoteControlEnabled), the hook command cannot execute, so hasStopEvent() never flips and the turn loop waits until the 30-minute timeout or manual abort. That makes PTY turns effectively hang on every iteration for Windows users.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

P1 指摘を対応しました (be8a7c6)。JSONL race は SessionStart hook で runtime session_id を受け取って <session_id>.jsonl にバインドする形に変更。Windows 非対応は Stop/SessionStart hook を Node.js スクリプトにしてクロスプラットフォーム化しました。

Comment on lines +269 to +270
} else if (added.length > 0) {
discovered = added[0];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bind each runner to the correct JSONL file

For non-resume runs, the discovery logic picks added[0] from the project directory. If two PTY sessions start in the same workspace concurrently, both can observe multiple new JSONLs and choose the same first entry, causing one session to tail another session’s transcript, adopt the wrong Claude session id, and emit incorrect stream events/results. The file selection needs a session-specific handshake instead of “first new file.”

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

P1 指摘 (JSONL file race) を対応しました (be8a7c6)。新実装では SessionStart hook で Claude Code が生成する runtime session_id を受け取り、<session_id>.jsonl を明示的にバインドする形に変更。first-new-file ヒューリスティックを廃止したので同一 cwd で同時起動しても transcript を取り違えません。

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

🧹 Nitpick comments (6)
apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx (1)

288-313: TODO_ENGINE=pty が無効な環境でもトグルを操作できてしまいます(任意)

packages/local-db/src/schema/todo-sessions.ts のコメントには「TODO_ENGINE=pty でない場合 UI 側でトグルを無効化する」と書かれていますが、ここではチェックボックスが常時有効です。PTY エンジンが無効な状態で Remote Control を有効にしてセッションを作成すると、backend 側で無視される/期待通りに動かない可能性があります。

対応案としては、(a) todoAgent.settings などでエンジン種別を露出して disabled をかける、(b) スキーマ側のコメントを実装実態に合わせて「エンジンが PTY のときだけ効果があり、それ以外では無視される」旨に更新する、のいずれかで整合を取るのがよいと思います。Pro/Max の注意書きは良い UX です。

🤖 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/TodoModal/TodoModal.tsx` around
lines 288 - 313, The Remote Control checkbox (Checkbox with id
"todo-remote-control", state remoteControlEnabled and setter
setRemoteControlEnabled) is always interactive even when TODO_ENGINE is not
"pty"; either disable the control or reflect backend behavior. Expose the engine
type from todoAgent.settings (or props/state that represent the session engine)
and use it to set the Checkbox disabled prop and adjust the label styling when
engine !== "pty", and ensure onCheckedChange no-ops or prevents enabling when
disabled; alternatively, update the schema comment if you intentionally want the
UI to be permissive but non-effective.
apps/desktop/src/main/todo-agent/trpc-router.ts (1)

418-418: ?? false は実質冗長です(任意)

todo_sessions.remote_control_enablednotNull().default(false) として定義されているため、source.remoteControlEnabled は常に booleannull/undefined を返しません。防御的コードとしては問題ありませんが、他フィールド(source.customSystemPrompt 等の nullable 列)とは扱いが一段異なるので、意図的であることが分かるようにコメントを添えるか、単に source.remoteControlEnabled にしてもよいと思います。

🤖 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 418, The line
setting remoteControlEnabled uses a redundant nullish coalescing fallback
(source.remoteControlEnabled ?? false) because the DB column
todo_sessions.remote_control_enabled is defined as notNull().default(false) so
source.remoteControlEnabled is already boolean; either remove the "?? false" and
use source.remoteControlEnabled directly, or keep it but add a short inline
comment referencing todo_sessions.remote_control_enabled to make the defensive
intent explicit (refer to the remoteControlEnabled property and
source.remoteControlEnabled).
apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx (1)

2256-2285: URL 正規表現が session id 部分しか許容していません(任意)

/https:\/\/claude\.ai\/code\/session_[A-Za-z0-9_-]+/ は現行の出力には合致しますが、将来クエリ文字列やフラグメント(?foo=bar, #...)が付与された場合に末尾を取りこぼし、リンクが壊れたセッション URL を指す可能性があります。取り込みやすい範囲で \S+ に緩めるか、末尾の句読点だけ除外する正規表現にしておくとロバストです。

🤖 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/TodoManager/TodoManager.tsx`
around lines 2256 - 2285, The regex used to extract the Claude session URL (in
the event handling branch where event.kind === "remote_control" and variables
urlMatch/url are computed from event.text) only matches the session id portion
and will drop query strings or fragments; update the pattern to capture the
entire URL (allow trailing non-space chars) while avoiding trailing
punctuation—e.g., change the current
/https:\/\/claude\.ai\/code\/session_[A-Za-z0-9_-]+/ usage to a more permissive
pattern like matching https://claude.ai/code/session_ followed by \S+ but strip
trailing punctuation characters before assigning url so the anchor href and
displayed url remain correct.
apps/desktop/plans/20260417-todo-agent-remote-control.md (1)

34-34: Fenced code block に言語タグを付けると markdownlint (MD040) が黙ります

静的解析ヒント通りの軽微なもの。アーキ図のブロックなので text で十分です。

提案 diff
-```
+```text
 [daemon]
  ├── supervisor-engine.ts  (従来 -p エンジン / 既定)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/plans/20260417-todo-agent-remote-control.md` at line 34, Add the
language tag "text" to the fenced code block that contains the architecture
diagram so markdownlint MD040 is satisfied; locate the fenced block containing
the daemon diagram (the triple-backtick block showing "[daemon] ├──
supervisor-engine.ts (従来 -p エンジン / 既定)") and change the opening fence to include
"text" (i.e., ```text) so the block is treated as plain text.
apps/desktop/src/main/todo-daemon/pty-turn-runner.ts (2)

215-247: abort シグナル時に interrupted を立てずに終わる — 呼び出し側で turnResult.interrupted が false のまま通常終了パスに入る可能性

pollStateparams.signal.aborted を検知したら safeKill(ptyProcess) して return false しますが、interrupted フラグは介入時にしか立てません。abort 時は supervisor の ac.signal.aborted チェック (supervisor-engine.ts Line 417) で早期 return するので実害は出ませんが、ptyStatus.alive が false になった後、最後の tailJsonl で偶然 lastAssistantText を拾うと result に値が残ってしまい、ログ上の見え方がノイズになる恐れがあります。

params.signal.aborted 時は ptyExitError にも行かず、明示的に interrupted: true or error: "aborted" で返すほうが呼び出し側と整合が取れます。

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

In `@apps/desktop/src/main/todo-daemon/pty-turn-runner.ts` around lines 215 - 247,
pollState currently kills the PTY on params.signal.aborted but doesn't set the
interrupted flag, which can leave turnResult.interrupted false; update the
params.signal.aborted branch inside pollState to set interrupted = true (and/or
mark an explicit abort error marker that will be returned) before calling
safeKill(ptyProcess) so callers like supervisor-engine see interrupted/error
consistently; also ensure the abortHandler (abortHandler) behavior aligns with
this change so both synchronous abort paths set the same interrupted state used
by turnResult.

62-90: CLAUDE_BIN が PATH 依存になるケースで node-pty が spawn に失敗する可能性

process.env.TODO_CLAUDE_BIN || process.env.CLAUDE_BIN || "claude" で、デフォルトは単なる "claude" です。Electron の本番ビルドでは daemon プロセスの PATH が開発時と大きく異なる(例: macOS の launchd 経由、packaged app 起動で /usr/local/bin が見えないことがある)ため、headless の spawn("claude", ...) と同じトラブルが起きます。

  • 既存の headless -p パスと症状を合わせる選択なら OK ですが、PTY 側は失敗時の diagnostic が claude を PTY 起動できませんでした: spawn ENOENT 程度しか出ません。
  • which claude 相当を起動時に一度解決してキャッシュする、もしくはエラーメッセージで「claude auth login 済みか / PATH にあるか」を案内してあげるとユーザが詰まりません。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/todo-daemon/pty-turn-runner.ts` around lines 62 - 90,
CLAUDE_BIN currently defaults to the literal "claude" which can cause spawn
ENOENT in packaged Electron; at startup resolve the actual executable path
(e.g., run child_process.execSync("command -v <name>" or "which <name>") for
process.env.TODO_CLAUDE_BIN || process.env.CLAUDE_BIN || "claude"), cache that
resolved path into a new symbol (e.g., CLAUDE_BIN_RESOLVED) and use it when
spawning the PTY, and if resolution/spawn fails enhance the error path where you
handle spawn errors to include actionable diagnostics ("executable not found on
PATH; try `claude auth login` or add to PATH") plus the original ENOENT details
so users can diagnose missing PATH vs auth issues.
🤖 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-daemon/pty-turn-runner.ts`:
- Around line 699-707: The current logic uses buf.toString("utf8") and then
computes a rewind using Buffer.byteLength(lastLine, "utf8"), which breaks when
the buffer ends in the middle of a UTF-8 multibyte sequence; instead detect the
last newline at the raw-byte level and rewind by the exact number of trailing
bytes after that newline: use buf.lastIndexOf(0x0A) to find the last newline
byte, set state.pendingLine to the string for the partial tail (prepend it next
read), and decrement state.jsonlReadOffset by (buf.length - (lastNewlineIndex +
1)) when lastNewlineIndex >= 0 (or handle the entire buffer as pending when
lastNewlineIndex === -1); update the code around the existing buf / text / lines
handling (the block using buf.toString, lines, lastLine, and
state.jsonlReadOffset) to use this byte-level calculation so multibyte
characters are preserved.
- Around line 665-697: The try/catch/finally double-closes the same fd in
tailJsonl (fd variable) — modify the error handling so the descriptor is only
closed once: either remove the fs.closeSync(fd) call from the catch block and
let the finally block always close the fd, or keep the close in the catch but
set fd = null immediately after closing so the finally block skips it; update
the catch accordingly around the fs.openSync/fs.readSync block (referencing fd
and the surrounding try/catch/finally) to ensure a single close.
- Around line 776-782: In pty-turn-runner.ts the JSONL fields are being read
with camelCase causing sessionId and parentToolUseId to always be
null/undefined; update the checks that set sessionId and parentToolUseId to read
rec.session_id and rec.parent_tool_use_id (respectively) instead of
rec.sessionId / rec.parentToolUseId, preserving the same typeof string guards
and fallback behavior so the variables (sessionId, parentToolUseId) mirror
supervisor-engine.ts’s snake_case handling.
- Around line 495-507: The Stop hook command interpolates hookScriptPath
directly in buildSettingsJson causing shell-escaping issues; wrap the path with
the existing escapeShell helper so the command becomes
`${escapeShell(hookScriptPath)} Stop` (i.e., update buildSettingsJson to call
escapeShell on hookScriptPath when constructing the command string) to safely
handle spaces and special characters for the POSIX shell execution.

In `@apps/desktop/src/main/todo-daemon/supervisor-engine.ts`:
- Around line 917-950: The shim created by buildChildProcessShim currently makes
once/on no-ops and never updates shim.signalCode/exitCode, so abort's
child.once("close", ...) never fires and the SIGKILL timer cannot be cleared;
modify buildChildProcessShim to store listeners registered via once/on (at least
for the "close" event) and provide a way for the real process handle to notify
the shim of exit (e.g., add an exit callback on the passed-in handle like
handle.onExit or call a supplied notifyExit function) so that when the PTY/real
child exits you invoke the stored "close" callbacks and set
shim.exitCode/shim.signalCode appropriately; also ensure abort path updates
shim.signalCode (either by routing abort's process.kill through handle.kill or
by setting shim.signalCode when the handle reports the SIGINT) so the timer
check (child.exitCode == null && child.signalCode == null) correctly observes
the child state and cancels the SIGKILL.

In `@apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx`:
- Around line 2268-2282: Replace the renderer use of window.open in the anchor
onClick with the tRPC procedure electronTrpc.external.openUrl(url): keep the
href and e.preventDefault(), then call await
electronTrpc.external.openUrl.mutate({ url }) (or .query/.call per your client)
instead of window.open; update the onClick handler in the JSX where url is
referenced and remove any reliance on window.open so the renderer uses the
existing electronTrpc.external.openUrl IPC path.

---

Nitpick comments:
In `@apps/desktop/plans/20260417-todo-agent-remote-control.md`:
- Line 34: Add the language tag "text" to the fenced code block that contains
the architecture diagram so markdownlint MD040 is satisfied; locate the fenced
block containing the daemon diagram (the triple-backtick block showing "[daemon]
├── supervisor-engine.ts (従来 -p エンジン / 既定)") and change the opening fence to
include "text" (i.e., ```text) so the block is treated as plain text.

In `@apps/desktop/src/main/todo-agent/trpc-router.ts`:
- Line 418: The line setting remoteControlEnabled uses a redundant nullish
coalescing fallback (source.remoteControlEnabled ?? false) because the DB column
todo_sessions.remote_control_enabled is defined as notNull().default(false) so
source.remoteControlEnabled is already boolean; either remove the "?? false" and
use source.remoteControlEnabled directly, or keep it but add a short inline
comment referencing todo_sessions.remote_control_enabled to make the defensive
intent explicit (refer to the remoteControlEnabled property and
source.remoteControlEnabled).

In `@apps/desktop/src/main/todo-daemon/pty-turn-runner.ts`:
- Around line 215-247: pollState currently kills the PTY on
params.signal.aborted but doesn't set the interrupted flag, which can leave
turnResult.interrupted false; update the params.signal.aborted branch inside
pollState to set interrupted = true (and/or mark an explicit abort error marker
that will be returned) before calling safeKill(ptyProcess) so callers like
supervisor-engine see interrupted/error consistently; also ensure the
abortHandler (abortHandler) behavior aligns with this change so both synchronous
abort paths set the same interrupted state used by turnResult.
- Around line 62-90: CLAUDE_BIN currently defaults to the literal "claude" which
can cause spawn ENOENT in packaged Electron; at startup resolve the actual
executable path (e.g., run child_process.execSync("command -v <name>" or "which
<name>") for process.env.TODO_CLAUDE_BIN || process.env.CLAUDE_BIN || "claude"),
cache that resolved path into a new symbol (e.g., CLAUDE_BIN_RESOLVED) and use
it when spawning the PTY, and if resolution/spawn fails enhance the error path
where you handle spawn errors to include actionable diagnostics ("executable not
found on PATH; try `claude auth login` or add to PATH") plus the original ENOENT
details so users can diagnose missing PATH vs auth issues.

In `@apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx`:
- Around line 2256-2285: The regex used to extract the Claude session URL (in
the event handling branch where event.kind === "remote_control" and variables
urlMatch/url are computed from event.text) only matches the session id portion
and will drop query strings or fragments; update the pattern to capture the
entire URL (allow trailing non-space chars) while avoiding trailing
punctuation—e.g., change the current
/https:\/\/claude\.ai\/code\/session_[A-Za-z0-9_-]+/ usage to a more permissive
pattern like matching https://claude.ai/code/session_ followed by \S+ but strip
trailing punctuation characters before assigning url so the anchor href and
displayed url remain correct.

In `@apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx`:
- Around line 288-313: The Remote Control checkbox (Checkbox with id
"todo-remote-control", state remoteControlEnabled and setter
setRemoteControlEnabled) is always interactive even when TODO_ENGINE is not
"pty"; either disable the control or reflect backend behavior. Expose the engine
type from todoAgent.settings (or props/state that represent the session engine)
and use it to set the Checkbox disabled prop and adjust the label styling when
engine !== "pty", and ensure onCheckedChange no-ops or prevents enabling when
disabled; alternatively, update the schema comment if you intentionally want the
UI to be permissive but non-effective.
🪄 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: c8d03b76-fe42-4e9d-a37f-f9e0f85cc646

📥 Commits

Reviewing files that changed from the base of the PR and between 02fb002 and b2725a3.

📒 Files selected for processing (13)
  • .gitignore
  • apps/desktop/plans/20260417-todo-agent-remote-control.md
  • apps/desktop/src/main/todo-agent/session-store.ts
  • apps/desktop/src/main/todo-agent/trpc-router.ts
  • apps/desktop/src/main/todo-agent/types.ts
  • apps/desktop/src/main/todo-daemon/pty-turn-runner.ts
  • apps/desktop/src/main/todo-daemon/supervisor-engine.ts
  • apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx
  • apps/desktop/src/renderer/features/todo-agent/TodoModal/TodoModal.tsx
  • packages/local-db/drizzle/0059_todo_remote_control_enabled.sql
  • packages/local-db/drizzle/meta/0059_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/src/schema/todo-sessions.ts

Comment thread apps/desktop/src/main/todo-daemon/pty-turn-runner.ts Outdated
Comment thread apps/desktop/src/main/todo-daemon/pty-turn-runner.ts
Comment thread apps/desktop/src/main/todo-daemon/pty-turn-runner.ts Outdated
Comment thread apps/desktop/src/main/todo-daemon/pty-turn-runner.ts Outdated
Comment thread apps/desktop/src/main/todo-daemon/supervisor-engine.ts
Comment thread apps/desktop/src/main/todo-daemon/supervisor-engine.ts
Comment thread apps/desktop/src/renderer/features/todo-agent/TodoManager/TodoManager.tsx Outdated
1. JSONL file race (#278 codex review)
   同一 cwd で 2 セッションを同時起動した場合、旧実装は
   project dir の新規 .jsonl の「先頭」を自セッションとみなして
   いたため、互いの transcript を tail する可能性があった。
   SessionStart hook でランタイム session_id を受け取って
   <session_id>.jsonl にバインドする形に変更。

2. Windows 非対応 (#278 codex review)
   旧 Stop hook は `#!/bin/sh` シェルスクリプトで、Windows では
   実行できず 30 分タイムアウトまでハングしていた。
   hook を Node.js スクリプト (cross-platform) に置き換え、
   `node <script> <event>` コマンドとして登録する。

合わせて:
- buildSettingsJson に SessionStart フックを追加
- 未使用になった existingJsonl / mostRecentFile ヘルパを削除
- JSONL 発見失敗時のエラーメッセージを SessionStart 未発火
  パターンに対応できるよう拡張
Critical:
- classifyJsonlRecord が camelCase (`sessionId` / `parentToolUseId`)
  しか見ていなかったため、snake_case (`session_id` / `parent_tool_use_id`)
  で書く Claude Code バージョンでは常に null になっていた。両方を
  読むフォールバックを追加。

Major:
- TodoManager の Remote Control バッジが `window.open` で URL を
  開こうとしていたが、Electron renderer では外部ブラウザに飛ば
  ない可能性がある。`electronTrpc.external.openUrl` に切り替え、
  RemoteControlBadge を独立コンポーネントに分離。

Minor:
- tailJsonl の catch ブロックで `fs.closeSync(fd)` を呼んだ後、
  finally でも同じ fd を閉じていた二重 close を解消 (catch 側を
  削除し finally 一本に)。
- tailJsonl の行バッファリングが `buf.toString("utf8")` の置換に
  依存していたため、チャンク末尾が UTF-8 マルチバイトの途中で切れると
  `Buffer.byteLength(lastLine)` が元バイト数と合わず、日本語ログで
  rewind がずれる問題を修正。`buf.lastIndexOf(0x0a)` で最後の LF を
  バイト境界で見つけて消費バイト数ぶんだけ進める実装に変更。
- PTY runner の onChild handle に `onExit(cb)` サブスクライブ口を
  追加。supervisor-engine の shim はこれを受けて `once("close", ...)`
  を記録し、PTY 終了時に発火。abort 時の SIGKILL タイマーの
  clearTimeout が効くようになり、死んだ/再利用 PID への不要な
  SIGKILL を避ける。
- PTY モードでは cost_usd が JSONL に載らないため totalCostUsd が
  常に null/0 になる旨をセットアップバナーに明示。
@MocA-Love MocA-Love merged commit 9c7a3de into main Apr 17, 2026
14 checks passed
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