fix: 質問待ちの Running 表示を解消し、AskUserQuestion に「自分で入力する」「これについて相談する」を追加 - #42
Merged
Merged
Conversation
#37 で reduceAssistant / reduceStreamEvent は pendingPermission がある間 running へ降格しないようガードしたが、running を設定する経路が他に 2 つ残って いた。どちらも pendingPermission を残したまま status を running にするため、 質問ダイアログが出ているのにバッジが「Running」になっていた。 - status-reducer.ts `user_input`: 保留中に追加指示を送っても awaiting_* を維持 - sdk-parse.ts `system/init`: 保留中に init が来ても awaiting_* を維持 これで running へ遷移する全経路(reduceAssistant / reduceStreamEvent / user_input / system/init)が pendingPermission をガードし、pending を解除する のは permission_resolved のみとなり「pending あり かつ running」は起こらない。
Claude Code の AskUserQuestion に倣い、選択肢の末尾へ常に 2 つの導線を足す。 - 自分で入力する(Type something.): 選ぶと自由記述モードに入り、入力テキストが その質問の回答になる。空欄で Backspace すると選択に戻る(Esc は背後の一覧/詳細 view が先取りするため使わない)。 - これについて相談する(Chat about this): 区切り線の下に置き、質問をスキップして ツールを拒否(onDeny)→ 通常の会話へ戻す。 文字列は i18n カタログ(ja/en 両方)へ追加。ダイアログのテスト(表示・相談で 拒否・自由記述で回答・空欄 Backspace で選択へ戻る)も追加。
takecchi
added a commit
that referenced
this pull request
Jul 31, 2026
## 背景 / 症状
1. 一覧の PR ステータス(`#<n>` やチェック)が**出るときと出ないときがある**
2. (レビュー中のご指摘)**セッション数に比例してポーリングが重くなる**/一度取得した情報がキャッシュされない
## 原因(実測で確認)
`utils/pr.ts` の `lookupPr` が **`gh` のあらゆる失敗を「PR が無い」と同じ `undefined`
に丸めていた**ため、`PrCoordinator` が `session.setPr(undefined)` を呼び、**表示中の
`#<n>` を消していた**。20 秒ごとのポーリングなので「消えて、しばらくして復活する」挙動になる。
このリポジトリで再現を確認:
```console
$ gh pr view codiva/github --json number # exit 1
GraphQL: API rate limit already exceeded for user ID …
$ gh api rate_limit --jq .resources.graphql
{"limit":5000,"remaining":0,"used":5004}
```
- `gh pr view --json mergeable` は REST ではなく **GraphQL API**(5000
ポイント/時、REST とは別枠)を消費する
- このクォータは**同一アカウントの全ツールで共有**で、**セッション内の Claude が実行する `gh`** も同じ枠を食う
- そこへ codiva が「**全セッション × 20 秒 × 2 回**」(PR 情報用と `statusCheckRollup`
用)を上乗せしていた。10 セッションで 3600 req/h = **自分で枯らして自分の表示を壊していた**
加えて「PR が無い」と「確認できていない」が画面上どちらも空セルで区別できず、PR
番号は永続化されていないため再起動直後は毎回ゼロから取得していた。
## 対応
### 1. 失敗と「PR が無い」を区別する(根本原因)
```ts
type PrLookupResult =
| { kind: 'found'; pr: PrInfo }
| { kind: 'absent' } // gh が答えた: PR は無い
| { kind: 'unavailable'; reason: PrUnavailableReason } // gh が答えられなかった
```
失敗理由を stderr の文言から分類(`cli` / `auth` / `rate_limit` / `network` /
`unknown`)。`unavailable` のときは**直前の値を保持**し、`prLookup: 'error'`
を立てるだけにする。パース失敗も `absent` ではなく `unavailable` に倒す。
### 2. 番号とステータスを分けて、番号は永続キャッシュする
```ts
pr?: PrRef; // { number, url } — ブランチに対して不変 → state.json に永続
prStatus?: PrStatus; // { mergeStatus, checks, isDraft } — 揺れる → transient
```
- **番号が分かった時点で番号を表示**: 永続するので**再起動直後からグリフ無しの `#42`** が出る(従来は毎回 `⋯` から)
- **ステータスが分かった時点で表示**: 「番号は既知・ステータス未取得」は `prPollIntervalMs` が **0**
を返して即取得 → グリフが後から付く。ステータスは永続しない(前回終了時の古いグリフを見せない)
- reducer は**半分ずつ**比較して参照を維持 → チェックの進行だけで `state.json` が再保存されない
### 3. 表示(読み込み中 / 確認不能 / チェック)
| 表示 | 意味 |
|---|---|
| `⋯` | 確認中(1 回目の問い合わせが未完了) |
| (空) | このブランチに PR は無い |
| `#12` | PR あり、**ステータス未取得**(再起動直後など) |
| `✓ #12` | マージ可能(チェックも通過) |
| `● #12` | **チェック実行中** |
| `✗ #12` | **チェック失敗** またはコンフリクト |
| `⑂ #12` | マージ済み |
| `?` | **確認できなかった**(レート制限 / オフライン / 未認証) |
draft は番号を淡色に(従来は区別なし)。`gh` 未導入のときは印を出さない(全行に警告を出しても直せない)。
### 4. API コストをセッション数から切り離す
- **陳腐化ベースのポーリング**(`core/pr-refresh.ts`)— 20 秒 tick
は**スケジューラに降格**し、実際に叩くのは期限切れのものだけ。チェック実行中 20 秒 / マージ可否計算中 60 秒 / 落ち着いた PR
**3 分** / **merged と archived は永久に問い合わせない**
- **3 件以上まとまったら `gh pr list` 1 回に畳む**(`lookupPrs`)— 各セッションの HEAD
ブランチ(ローカル `git rev-parse`、API 不要)で突き合わせ。**10 セッションでも API 1 回**。1〜2 件は
`pr view` の方が安いので閾値を置く(list は全件の rollup を運ぶ)
- チェック取得を `pr view` 1 回に統合(毎ポーリング 2 回 → 1 回)
- `rate_limit` / `auth` / `cli` を検知したら **5
分**停止(`PR_LOOKUP_BACKOFF_MS`)、サイクルの**多重実行を禁止**(`gh` が 20 秒より遅いと重なっていた)
10 セッションで概算 **3600 → 20〜60 req/h**。
## 不変条件
- 状態遷移は reducer 経由のみ(`pr_lookup` イベント追加。`pr` は「`gh` が答えた」ときだけ流すので
`prLookup` も必ずクリア)
- `prStatus` / `prLookup` は transient(`state.json` にも
`persistRelevantChanged` にも入らない)。永続するのは番号・URL だけで、untrusted JSON も検証する
- `gh` を知るのは `utils/pr.ts` だけ / 色・記号は `theme.ts` 経由 / UI 文字列の追加なし(記号のみ)
## テスト
`lint` / `typecheck` / `test` / `build` 通過(**1621 tests**、statements
95.6%)。
- `src/utils/pr.spec.ts` — 実際の `gh` 文言(レート制限・未認証・ENOENT・オフライン・no
PR)の分類テーブル、チェック集約、バッチ(1 回で解決 / 失敗時に全件 unavailable / open を closed より優先 /
ページ切り詰め時の確認)
- `src/core/pr-refresh.spec.ts`(新規)— 状態ごとの再取得間隔とキャッシュ判定のテーブル
- `src/core/pr-coordinator.spec.ts`(新規)— 失敗時に PR 保持 / バックオフ / 多重実行禁止 /
merged は打ち切り / 点滅しない / バッチ切替 など 29 件
- `src/core/status-reducer.spec.ts` — 2 分割の比較と参照維持
- `src/core/persistence.spec.ts` — 番号だけ永続し、ステータスは永続しない(壊れた JSON も除去)
- `tests/restore.test.tsx` / `tests/app.test.tsx` —
再起動で番号が残り、最初のポーリングでグリフが付くところまで通す
### 実 API での検証(`gh` の実データ)
`gh pr list --json` の形状・並び順を実測し `docs/TECH_NOTES.md` に記録。`lookupPrs` を実
API に通して確認:
| ケース | 結果 |
|---|---|
| HEAD にある PR | `found` #78 mergeable / passing |
| PR 無し | **`absent`**(`unavailable` ではない) |
| マージ済み | `found` #77 **merged**(以後ポーリングしない) |
### 手動確認のお願い
実セッション(TTY + Claude 認証)が必要な体感確認はお願いします。`gh auth logout` 状態やクォータ枯渇時に一覧へ
`?` が出て番号が消えないこと、再起動直後に `#<n>` が即出ることを見ていただければ確実です。
🤖 Generated with [Claude Code](https://claude.com/claude-code)
takecchi
added a commit
that referenced
this pull request
Aug 6, 2026
## 背景 セッションが自分で別ブランチを切って `gh pr create` すると、1 セッションに複数の PR が紐づきます。しかし一覧の PR セルは**セッションブランチ(`codiva/<slug>`)の PR しか出せず**、2 本目以降はどこにも表示されませんでした。 ## 変更内容 ### 検知(`core/pr-detect.ts` 新規・純粋) - `gh pr create` を実行した **tool_use の結果**に出る PR URL を拾う(`sdk-parse` が tool_use id を控えて tool_result と突き合わせる)。 - ログ全体から URL を拾わないのは誤検出を避けるため — `gh pr list` の出力や、`gh pr view` / WebFetch で覗いただけの他人の PR まで「このセッションの PR」になってしまう。 - MCP の `create_pull_request` 系ツールもツール名で拾う。 - 参照の追加・除去は**変化が無ければ同じ配列参照を返す**(`state.json` を無駄に書き直さない / 再描画を増やさない)。 ### 状態・永続化 - `SessionState.extraPrs`(識別のみ・**永続**)と `prCreateToolIds`(結果待ちの対応付け・transient)を追加。 - `state.json` に保存・復元(壊れた要素は 1 件ずつ落とす)。reducer はブランチの PR と重複したら畳む。 ### 表示 | 画面 | 表記 | |---|---| | 一覧 | `✓ #12 +2`(代表 + 残り件数)。PR 列は複数 PR の行があるときだけ 10 → 14 桁に広げる | | 詳細 | `PR 3 件: ✓ #12 · #13 · #14`(複数 PR のときだけ 1 行。1 本ならログの縦幅を 1 行も譲らない) | - **代表はセッションブランチの PR**。`prStatus`(グリフ)を持つ唯一の PR で、クリック / `p` で開く先でもあるため、見えている番号と開く先とグリフの意味を一致させた。 - セッションが自分で作った PR は codiva が追跡・操作しない(別ブランチのものを勝手に ready 化・マージしない)ので**番号のみ・グリフなし**。状態を知らないのに緑や赤で嘘をつかない。 - **`gh` の追加呼び出しはゼロ**(API クォータを増やさない)。 - 描画とクリック当たり判定は必ず同じ列幅(`core/list-hit.ts` の `prCellWidth`)を使う。 - `PrCell` / `prStatusBadge` は `ui/pr-cell.tsx` に共通化(一覧と詳細で同じグリフ・同じ色)。 ### ドキュメント `README.md`(利用者向けの表記説明)/ `docs/ARCHITECTURE.md`(状態フィールド・設計判断)/ `docs/TASKS.md`(Phase 27)/ `CLAUDE.md`(コードの地図)。 ## テスト - `src/core/pr-detect.spec.ts`(新規・テーブルドリブン): コマンド判定 / URL 抽出(GHE ホスト・サブパス・重複・issues リンク)/ 参照同一性 / 上限 / 表示ヘルパ。 - `src/core/sdk-parse.spec.ts`: 実フィクスチャと同じメッセージ形で tool_use → tool_result の対応、`gh pr list` を数えないこと、ブランチ PR と判明したら畳むこと。 - `src/core/persistence.spec.ts`: 往復と壊れた JSON の扱い。 - `src/core/list-hit.spec.ts`: 列幅の切替。 - `tests/app.test.tsx`: 一覧に `✓ #42 +2` が出て `#43` は出ないこと、詳細に全件が並ぶこと、1 本のときは詳細に PR 行を出さないこと。 ### 動作確認 - [x] `npm run lint` / `npm run typecheck` / `npm test`(2,232 件)/ `npm run build` - [ ] 実 TTY + Claude 認証での手動確認(非対話セッションでは実行できないため未実施)。確認観点: 複数 PR を出したセッションの行が `#12 +2` になり、PR セルのクリックで代表の PR が開く / 詳細ビューに全件が並ぶ / 再起動しても `+n` が残る。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
5 tasks
takecchi
added a commit
that referenced
this pull request
Aug 11, 2026
レビュー指摘(別リポジトリの PR を取り違える)の修正。 `extraPrs` は `https://<host>/<owner>/<repo>/pull/<n>` を受理するので、 セッションが `gh pr create -R owner/other` で**別リポジトリ**に作った PR も 保存される。ところが `knownPr` を番号で渡していたため、worktree の cwd で `gh pr view 42` を実行すると**現在リポジトリの #42** が返る: - 無関係な PR の状態・URL で `pr` を上書きする - それが draft かつ checks passing なら `gh pr ready 42` で無関係な PR を ready 化する - 現在リポジトリに同番号が無ければ、既知 PR を `absent` として消す `PrLookupOptions.knownPr` / `PrLookupTarget.knownPr` を `number` から `PrRef` に変え、フォールバックの `gh pr view` と auto-ready の両方を **PR の URL** で実行する(`gh` は URL でリポジトリごと解決する)。 回帰テストは utils / core の両方に追加: - 単体: 既知 PR が別リポジトリの `/pull/42` でも、その URL で問い合わせ、 **番号(`42`)では一度も聞かない** - バッチ: 現在リポジトリの `pr list` に同番号 #42 の行があっても、 cross-repo の既知 PR はその URL で確認し、行に引きずられない - coordinator: `markReady` が解決した PR の URL で呼ばれる
takecchi
added a commit
that referenced
this pull request
Aug 18, 2026
## 概要 マージされずに **クローズされた PR** を、専用のステータス(アイコン `⊘`)で表示するようにしました。あわせて、**クローズは「詰まっている」ではなく「終わっている」** ものとして扱い、コンフリクトや CI 失敗のような立て直し(recovery)の対象から外します。 ### これまでの問題 `gh pr view --json state,mergeable` の `state: 'CLOSED'` を無視して `mergeable` だけを見ていたため、閉じる直前の値がそのまま出ていました。 - `mergeable: MERGEABLE` のまま閉じた PR → 緑の `✓`(= マージできる) - `mergeable: CONFLICTING` のまま閉じた PR → 赤の `✗`(= 立て直せ) つまり **終わった PR が現役の PR と同じ見た目**になり、さらに後者は `autoSync` / `autoFixCi` / `Ctrl+F` の立て直し対象になって、誰も取り込む気の無いブランチにベースを push したり、CI 修正を 1 ターン課金して依頼したりしていました。 ## 変更点 | 層 | 変更 | |---|---| | `core/types.ts` | `PrMergeStatus` に `'closed'` を追加 | | `utils/pr.ts` | `toMergeStatus` は `MERGED` と同様に `state: 'CLOSED'` を `mergeable` より優先 | | `ui/theme.ts` / `ui/pr-cell.tsx` | 専用グリフ `⊘`(畳んだ灰色 = archived と同じ)。判定順は merged → closed → checks … で、閉じた PR の CI 状態には引きずられない | | `core/pr-recovery.ts` | `stuckKinds` が `closed` で空を返す(`/sync` `/fix-ci` の自動起動・`Ctrl+F` の対象外) | | `core/pr-coordinator.ts` | draft の自動 ready 化をしない(閉じた PR を勝手に ready にしない) | | `core/pr-refresh.ts` | reopen があり得るのでポーリングは止めないが、チェック実行中でも stable 間隔へ落とす | ### 色の判断 GitHub 慣習ではクローズは赤ですが、この一覧の赤は **「直せ」= 操作可能** の意味で使っています(CI 失敗・コンフリクト)。クローズはまさに何もすることが無い状態なので、archived 行と同じ畳んだ灰色にしました。 ### 立て直さない理由 閉じたのは人間の判断で、codiva が覆すべきものではありません(再開したいときは GitHub 側で reopen すれば、次のポーリングで通常の状態に戻ります)。README にもその旨を追記しました。 ## テスト - `utils/pr.spec.ts` … `state=CLOSED × mergeable=UNKNOWN / MERGEABLE / CONFLICTING` → すべて `closed` - `core/pr-recovery.spec.ts` … 「クローズ済みは(赤くても)詰まっていない」 - `core/pr-refresh.spec.ts` … クローズ済みはチェック実行中でも stable 間隔 - `ui/pr-cell.spec.tsx` … `⊘ #42`(`checks: 'failing'` でも `✗` に化けない) ### 確認方法(TODO) - [ ] `npm run lint` / `npm run typecheck` / `npm test` / `npm run build`(CI) - [ ] 実際に PR をマージせずクローズして、一覧の該当行が `⊘ #<n>` になること - [ ] その行が `Ctrl+F`(立て直し)の対象に出てこないこと ## ドキュメント - `README.md` … PR ステータス表に `⊘` の行 + 「クローズ済みは立て直さない」の説明 - `docs/ARCHITECTURE.md` … 「終わった PR は詰まっていない」の節 - `docs/TECH_NOTES.md` … `state: 'CLOSED'` でも `mergeable` が残る実測メモ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
AskUserQuestion(質問)まわりの 2 点を対応しました。
1. ステータスバグの修正
#37でreduceAssistant/reduceStreamEventはpendingPermissionがある間runningへ降格しないようガード済みでしたが、runningを設定する経路が他に 2 つ残っていました。どちらも
pendingPermissionを残したままstatusをrunningにしてしまうため、質問ダイアログが表示されているのにバッジが「Running」になります。
user_inputsrc/core/status-reducer.tsawaiting_*を維持system/initsrc/core/sdk-parse.tsawaiting_*を維持これで
runningへ遷移する全経路(reduceAssistant/reduceStreamEvent/user_input/system/init)がpendingPermissionをガードし、pending を解除するのはpermission_resolvedのみとなるため、「pending あり かつ running」という不整合な状態は発生しなくなります(reducer レベルで再現テスト済み)。
2. ダイアログへの選択肢追加
実選択肢の末尾に、常に次の 2 つを表示します(Claude Code の AskUserQuestion に準拠)。
なります。空欄で
Backspaceすると選択に戻ります(Escは背後の一覧/詳細 view が先取りするため、あえて割り当てていません)。
onDeny)し、通常の会話へ戻します。モデルには「選択肢を選ばず相談を選んだ」旨が伝わります。
表示文字列はすべて i18n カタログ(
ja/en両方)へ追加しています。テスト計画
npm test(661 passed / core・utils カバレッジ 80% 以上を維持)npm run typecheck(クリーン)npm run lint(クリーン)npm run build(成功)および「自分で入力する」「これについて相談する」が期待通り動くことを手動確認
追加テスト
status-reducer.spec.ts:user_inputが pending 中のawaiting_input/awaiting_permissionを維持することを検証sdk-parse.spec.ts:system/initが pending 中のawaiting_inputを維持することを検証permission-dialog.spec.tsx: 2 選択肢の表示 / 相談でonDeny/ 自由記述で回答 /空欄 Backspace で選択へ戻る、を検証