Skip to content

Commit 722e494

Browse files
authored
Merge pull request #94 from watany-dev/claude/improve-review-readability-2vmY6
2 parents 9e56785 + 5ee0e4d commit 722e494

1 file changed

Lines changed: 291 additions & 0 deletions

File tree

docs/design-review-readability.md

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
# レビュー画面の可読性改善設計書
2+
3+
> **ステータス**: 設計完了(2026-04-09)
4+
>
5+
> PR詳細画面のdiff表示に行番号ガターと変更行ジャンプ(`]c`/`[c`)を追加し、コードレビュー体験を改善する。
6+
7+
## 概要
8+
9+
PR詳細画面(`PullRequestDetail`)のdiff表示に2つの機能を追加し、コードレビュー体験を改善する:
10+
1. 行番号ガターの表示
11+
2. 変更行への高速ジャンプナビゲーション(`]c`/`[c`
12+
13+
## Context
14+
15+
ユーザーがレビュー画面を使用中に「見づらい」と感じている。具体的には:
16+
1. **diffに行番号がない** — コードの位置が分からず、レビューコメントで行を参照できない
17+
2. **変更行にすぐ飛べない**`j`/`k`で1行ずつ移動するか、`n`/`N`でファイル間ジャンプしかできず、変更箇所(add/delete行)に素早く移動する手段がない
18+
19+
本設計は v0.3.0(UX強化: diff可読性・ナビゲーション向上)ロードマップに合致する。
20+
21+
## ファイル構成
22+
23+
| ファイル | 変更種別 | Iteration |
24+
|---------|---------|-----------|
25+
| `src/components/DiffLine.tsx` | 変更: 行番号ガター追加 | 1 |
26+
| `src/components/PullRequestDetail.tsx` | 変更: `]c`/`[c` キーバインド、`pendingBracket` state追加 | 2 |
27+
| `src/components/Help.tsx` | 変更: Navigation セクションに `]c`/`[c` 追加 | 2 |
28+
| `src/components/PullRequestDetail.test.tsx` | 変更: テストケース追加 | 1, 2 |
29+
30+
## コンポーネント設計
31+
32+
### Iteration 1: diff行番号の表示
33+
34+
**Tidy First?**: 不要。`DiffLine.tsx` はシンプルな switch-case で読みやすく、構造変更なしに機能を追加できる。
35+
36+
**対象ファイル:**
37+
- `src/components/DiffLine.tsx` — 行番号ガターのレンダリング追加
38+
- `src/components/PullRequestDetail.test.tsx` — テスト追加
39+
40+
**設計:**
41+
42+
`renderDiffLine` に行番号ガターを追加する。`DisplayLine` には既に `beforeLineNumber``afterLineNumber` が計算済み(`formatDiff.ts:55-56,77,94`)なので、表示するだけ。
43+
44+
表示形式(ガター幅: 12文字 = 4 + 1 + 4 + 3):
45+
```
46+
3 4 │ context line text
47+
5 │ -deleted line text
48+
6 │ +added line text
49+
```
50+
51+
- before/after それぞれ4文字幅(`padStart(4)`)で右寄せ、スペース1文字区切り、`` セパレータ付き
52+
- `<Text dimColor>` で表示し、diff本文の色(green/red/white)と競合しないようにする
53+
- `add`, `delete`, `context` 行のみガター表示
54+
- `header`, `separator`, `truncation`, `truncate-context`, `comment-header`, `comment`, `inline-comment`, `inline-reply`, `comment-reply`, `fold-indicator` はガター不要(行番号プロパティが存在しないため)
55+
- ターミナル幅80カラムの場合: カーソル2文字 + ガター12文字 = 14文字消費、残り66文字がdiff本文(十分)
56+
- 行番号が9999を超える場合: `padStart(4)` では左にはみ出すが、レイアウトが壊れるわけではなく数字が左に1文字伸びるだけ。10,000行超のファイルは稀であり、対応不要
57+
58+
**実装詳細:**
59+
60+
`DiffLine.tsx``formatGutter(line: DisplayLine): string` ヘルパーを追加:
61+
```typescript
62+
function formatGutter(line: DisplayLine): string {
63+
const before = line.beforeLineNumber ? String(line.beforeLineNumber).padStart(4) : " ";
64+
const after = line.afterLineNumber ? String(line.afterLineNumber).padStart(4) : " ";
65+
return `${before} ${after} │ `;
66+
}
67+
```
68+
69+
`add`, `delete`, `context` の各caseでガター部分を `<Text dimColor>` で先頭に追加:
70+
```tsx
71+
case "context":
72+
return (
73+
<Text bold={bold}>
74+
<Text dimColor>{formatGutter(line)}</Text>
75+
{line.text}
76+
</Text>
77+
);
78+
case "add":
79+
return (
80+
<Text color="green" bold={bold}>
81+
<Text dimColor>{formatGutter(line)}</Text>
82+
{line.text}
83+
</Text>
84+
);
85+
case "delete":
86+
return (
87+
<Text color="red" bold={bold}>
88+
<Text dimColor>{formatGutter(line)}</Text>
89+
{line.text}
90+
</Text>
91+
);
92+
```
93+
94+
**データフロー:**
95+
```
96+
formatDiff.ts: computeSimpleDiff()
97+
→ DisplayLine { beforeLineNumber: 3, afterLineNumber: 4, type: "context", text: " line1" }
98+
→ displayLines.ts: buildDisplayLines() でそのまま伝搬
99+
→ PullRequestDetail.tsx: renderDiffLine(line, isCursor) を呼び出し
100+
→ DiffLine.tsx: formatGutter(line) で " 3 4 │ " を生成、<Text dimColor> で表示
101+
```
102+
103+
---
104+
105+
### Iteration 2: 変更行ジャンプ機能の追加 (`]c`/`[c`)
106+
107+
**Tidy First?**: 不要。`useInput` ハンドラは既存のパターン(キー検出 → 状態更新 → return)で一貫しており、新キーバインドを同パターンで追加可能。
108+
109+
**対象ファイル:**
110+
- `src/components/PullRequestDetail.tsx``]c`/`[c` キーバインド追加
111+
- `src/components/PullRequestDetail.test.tsx` — テスト追加
112+
- `src/components/Help.tsx` — ヘルプ画面にキー説明追加
113+
114+
**設計:**
115+
116+
Vim diff風の `]c` (次の変更行) / `[c` (前の変更行) キーバインドを追加する。
117+
118+
変更行 = `line.type === "add" || line.type === "delete"` の行
119+
120+
**2キーシーケンスの設計判断:**
121+
122+
`design-review-keybindings.md:349` で「2ストローク操作はタイムアウトベースのキーシーケンス管理が必要」と記載されている。しかしこれは `gg` のように**1キー目が既存キーバインドと衝突する場合**`g` はリアクション操作に使用中)の話。`[`/`]` は現在どのキーバインドにも使用されていないため、押下時点で即座に `pendingBracket` state を設定する state ベースアプローチが使える。タイムアウト管理は不要。
123+
124+
**state遷移図:**
125+
```
126+
[null] ──("]"キー)──→ ["]"] ──("c"キー)──→ [null] + 次の変更行へジャンプ
127+
↑ │
128+
└──(他のキー)─────────┘ (リセット + そのキーを通常処理)
129+
130+
[null] ──("["キー)──→ ["["] ──("c"キー)──→ [null] + 前の変更行へジャンプ
131+
↑ │
132+
└──(他のキー)─────────┘ (リセット + そのキーを通常処理)
133+
```
134+
135+
**実装詳細:**
136+
137+
1. `pendingBracket` state を追加(`PullRequestDetail.tsx`、既存state群の近くに配置):
138+
```typescript
139+
const [pendingBracket, setPendingBracket] = useState<"[" | "]" | null>(null);
140+
```
141+
142+
2. `useInput` コールバック内で、**モーダルガード(line 405-416)の直後、既存キーハンドラの前**に配置:
143+
144+
```typescript
145+
// === 2-key sequence: ]c / [c for change navigation ===
146+
if (pendingBracket && input === "c") {
147+
const isNext = pendingBracket === "]";
148+
setPendingBracket(null);
149+
if (isNext) {
150+
const idx = lines.findIndex(
151+
(l, i) => i > cursorIndex && (l.type === "add" || l.type === "delete"),
152+
);
153+
if (idx !== -1) setCursorIndex(idx);
154+
} else {
155+
for (let i = cursorIndex - 1; i >= 0; i--) {
156+
if (lines[i]!.type === "add" || lines[i]!.type === "delete") {
157+
setCursorIndex(i);
158+
break;
159+
}
160+
}
161+
}
162+
return;
163+
}
164+
if (pendingBracket) {
165+
setPendingBracket(null); // 無効な2キー目: リセットしてフォールスルー
166+
}
167+
```
168+
169+
3. 既存キーハンドラの末尾付近(`G` ハンドラ等の後)に `[`/`]` キャプチャを追加:
170+
```typescript
171+
if (input === "]") {
172+
setPendingBracket("]");
173+
return;
174+
}
175+
if (input === "[") {
176+
setPendingBracket("[");
177+
return;
178+
}
179+
```
180+
181+
## エッジケース・異常系
182+
183+
| ケース | 挙動 | 根拠 |
184+
|--------|------|------|
185+
| カーソルが既に変更行上 | 次の/前の**別の**変更行にジャンプ | `i > cursorIndex` / `i < cursorIndex` で現在行を除外 |
186+
| 前方/後方に変更行がない | カーソル位置変更なし | `findIndex``-1` を返す / for ループが `break` せず終了。ラップアラウンドしない(Vimの `]c` と同じ) |
187+
| diffが空(ファイルロード中) | カーソル位置変更なし | `lines` にadd/delete行が存在しない |
188+
| モーダル表示中に `[` を押下 | `pendingBracket` は設定されない | モーダルガード(line 405-416)で早期リターンするため |
189+
| `[` 押下直後に `j` を押す | pendingBracket リセット、`j` は通常のカーソル移動 | `pendingBracket` チェックで null 化後、return せずフォールスルー |
190+
| `[` 押下直後に `q` を押す | pendingBracket リセット、`q` で画面遷移 | 同上 |
191+
| All changes / コミットビュー | 両方で動作する | diff行のtype判定は共通。`viewIndex` によるガードなし |
192+
| 連続して `]c`/`[c` を押す | 変更行を順にたどれる | 毎回 `cursorIndex` から検索するため |
193+
194+
**キーバインドの整合性:**
195+
196+
`c` は既にコメント投稿に使用中(line 519-522)。衝突しない理由:
197+
- `[` 押下 → `pendingBracket``"["` にセット、即 return
198+
- 次の入力 `c``pendingBracket` チェック(モーダルガード直後に配置)が先に発火し、変更行ジャンプ実行
199+
- `c` 単体押下(`pendingBracket` が null)→ pendingBracket チェック条件 `if (pendingBracket && input === "c")` が false → 従来通り line 519 のコメント投稿ハンドラに到達
200+
201+
**フッター更新** (`PullRequestDetail.tsx` line 905付近):
202+
`n/N file` の直後に `]c/[c change` を追加。
203+
204+
**Help.tsx更新:**
205+
Navigation セクション内(line 50-51 の `n`/`N` の直後)に追加:
206+
```tsx
207+
<KeyRow keyName="]c" description="Next change (add/delete line)" />
208+
<KeyRow keyName="[c" description="Previous change (add/delete line)" />
209+
```
210+
211+
## テスト戦略
212+
213+
### Iteration 1: 行番号テスト
214+
215+
TDDサイクル:
216+
1. **Red**: `PullRequestDetail.test.tsx` にテスト追加 — diff行に `` セパレータが含まれることを検証
217+
2. **Green**: `DiffLine.tsx``formatGutter` + 各caseの変更を実装
218+
3. **Refactor**: 必要に応じて整理
219+
220+
**既存テストへの影響:**
221+
- `toContain("line1")` (line 251): ガターが前置されても `line1` は含まれるためパス
222+
- `toMatch(/> .*new line 1/)` (line 1938): `.*` がガター文字にマッチするためパス
223+
- `toContain("src/auth.ts")` (line 225): header行にはガターが付かないためパス
224+
225+
### Iteration 2: 変更行ジャンプテスト
226+
227+
TDDサイクル:
228+
1. **Red**: テストを追加
229+
- `]c` で次のadd/delete行にジャンプすることを確認
230+
- `[c` で前のadd/delete行にジャンプすることを確認
231+
- 変更行がない場合はカーソル位置が変わらないことを確認
232+
- `[` + `j` でpendingBracketがリセットされ、`j` が通常処理されることを確認
233+
2. **Green**: `PullRequestDetail.tsx` に pendingBracket state + useInput ハンドラ実装、`Help.tsx` にキー説明追加
234+
3. **Refactor**: 必要に応じて整理
235+
236+
## 実装計画
237+
238+
各イテレーション完了時に `bun run ci` を実行して全チェックをパス:
239+
1. `lint` — oxlint
240+
2. `format:check` — Biome formatting
241+
3. `check` — TypeScript type check
242+
4. `dead-code` — knip unused export detection
243+
5. `audit` — dependency vulnerability check
244+
6. `test:coverage` — Vitest with 95% coverage threshold
245+
7. `build` — production build
246+
247+
## 対象外(今回のスコープ外)
248+
249+
- ファイル間の余白改善(セパレータ幅の動的化など)
250+
- コメントの視覚的階層改善
251+
- フッター情報量の削減(`design-review-keybindings.md` P2 として別途対応)
252+
- 表示行数のターミナル高さ連動
253+
- 要件定義書(`docs/requirements.md`)のキーバインド表への `]c`/`[c` 追記は実装完了後に `/update-docs` で対応
254+
255+
---
256+
257+
## update-plan 検証結果
258+
259+
### 設計書品質評価
260+
261+
| # | カテゴリ | 点数 | コメント |
262+
|---|---------|------|---------|
263+
| 1 | 基本構造 | 9/10 | 概要・コンポーネント設計・実装詳細・テスト・検証方法・ファイル構成が揃っている |
264+
| 2 | 目的・スコープの明確性 | 9/10 | 何を実装し何を実装しないかが明確。対象外も列挙済み |
265+
| 3 | コンポーネント設計の完全性 | 9/10 | DiffLine.tsx の全case、pendingBracket の型定義、state遷移図、データフローを記載 |
266+
| 4 | AWS SDK連携設計 | N/A | 本変更にAWS SDK連携なし |
267+
| 5 | 内部アーキテクチャ | 9/10 | ファイル構成表、データフロー図、state遷移図、`useInput` 内の配置順序を記載 |
268+
| 6 | エッジケース・異常系 | 9/10 | 8ケースの網羅的な表、行番号オーバーフロー、モーダル中の挙動を記載 |
269+
| 7 | テスト戦略 | 9/10 | TDDサイクル(Red→Green→Refactor)で構成。具体的なテストケース、既存テスト影響分析を記載 |
270+
| 8 | セキュリティ考慮 | N/A | 本変更にセキュリティ影響なし |
271+
| 9 | 技術選定の根拠 | 9/10 | stateベース vs タイムアウトの比較、`design-review-keybindings.md` との整合性説明、ユーザー選択の経緯 |
272+
| 10 | 実装計画 | 9/10 | 2イテレーション、Tidy First判定、TDDサイクル、対象ファイル/行番号が明確 |
273+
| | **合計** | **72/80 (90%)** | N/A 2項目を除外した8項目での評価 |
274+
275+
**総合判定**: 🟢 実装着手可能(90%)
276+
277+
### 整合性チェック
278+
279+
| チェック項目 | 結果 | 詳細 |
280+
|-------------|------|------|
281+
| 設計書 ↔ ソースコード || `DisplayLine``beforeLineNumber`/`afterLineNumber` が既存(`formatDiff.ts:1-24`)。`DiffLine.tsx` の switch-case 構造と一致 |
282+
| ロードマップ ↔ 設計書 ↔ 要件定義 || v0.3.0 UX強化(diff可読性・ナビゲーション向上)に合致 |
283+
| CLAUDE.md との整合性 || TDDサイクル、Tidy First判定、カバレッジ95%を担保 |
284+
| ソースコード規約 || 既存のキーハンドラパターン、`Help.tsx``SectionHeader`+`KeyRow` パターンに従う |
285+
| design-review-keybindings.md || 2ストローク操作にタイムアウト不要の根拠を記載 |
286+
287+
### 修正事項
288+
289+
- **P0**: なし
290+
- **P1**: なし
291+
- **P2**: フッターがさらに長くなる(`]c/[c change` 追加)が、`design-review-keybindings.md` P2(フッター整理)で別途対応

0 commit comments

Comments
 (0)