Skip to content

fix: 質問待ちの Running 表示を解消し、AskUserQuestion に「自分で入力する」「これについて相談する」を追加 - #42

Merged
takecchi merged 2 commits into
mainfrom
codiva/askuserquestion-running-askuserquestion
Jul 20, 2026
Merged

takecchi merged 2 commits into
mainfrom
codiva/askuserquestion-running-askuserquestion

Conversation

@takecchi

Copy link
Copy Markdown
Owner

概要

AskUserQuestion(質問)まわりの 2 点を対応しました。

  1. 質問が出ているのにステータスが「Running」のままになる残存ケースを修正
  2. AskUserQuestion ダイアログに Claude Code 同様の 2 つの導線を追加
    • 「自分で入力する」(Type something.)
    • 「これについて相談する」(Chat about this)

1. ステータスバグの修正

#37reduceAssistant / reduceStreamEventpendingPermission がある間
running へ降格しないようガード済みでしたが、running を設定する経路が他に 2 つ
残っていました
。どちらも pendingPermission を残したまま statusrunning
してしまうため、質問ダイアログが表示されているのにバッジが「Running」になります。

経路 ファイル 修正
user_input src/core/status-reducer.ts 保留中に追加指示を送っても awaiting_* を維持
system/init src/core/sdk-parse.ts 保留中に init が来ても awaiting_* を維持

これで running へ遷移する全経路(reduceAssistant / reduceStreamEvent /
user_input / system/init)が pendingPermission をガードし、pending を解除するのは
permission_resolved のみとなるため、「pending あり かつ running」という不整合な
状態は発生しなくなります(reducer レベルで再現テスト済み)。

2. ダイアログへの選択肢追加

実選択肢の末尾に、常に次の 2 つを表示します(Claude Code の AskUserQuestion に準拠)。

1. Hoge
2. Fuga
   自分で入力する        ← 追加(自由記述へ)
──────────────────────────
   これについて相談する   ← 追加(区切り線の下・スキップして会話へ)
  • 自分で入力する: 選ぶと自由記述モードに入り、入力テキストがその質問の回答に
    なります。空欄で Backspace すると選択に戻ります(Esc は背後の一覧/詳細 view が
    先取りするため、あえて割り当てていません)。
  • これについて相談する: 質問をスキップしてツールを拒否(onDeny)し、通常の会話へ
    戻します。モデルには「選択肢を選ばず相談を選んだ」旨が伝わります。

表示文字列はすべて i18n カタログ(ja / en 両方)へ追加しています。

テスト計画

  • npm test(661 passed / core・utils カバレッジ 80% 以上を維持)
  • npm run typecheck(クリーン)
  • npm run lint(クリーン)
  • npm run build(成功)
  • 実セッションで AskUserQuestion 発火時に「質問あり」バッジへ切り替わること、
    および「自分で入力する」「これについて相談する」が期待通り動くことを手動確認

追加テスト

  • 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 で選択へ戻る、を検証

takecchi added 2 commits July 20, 2026 05:09
#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
takecchi merged commit d85eb06 into main Jul 20, 2026
1 check passed
@takecchi
takecchi deleted the codiva/askuserquestion-running-askuserquestion branch July 20, 2026 05:51
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)
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` が残る実測メモ
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