# fix(terminal): そっと動かしたトラックパッドのスクロールが1行も動かない (#1200)

## 症状

Claude / Codex セルで、トラックパッドを**そっと**動かしても画面が一切動かない。強く動かすと動く。
#1200 の「部分的なスクロールが画面に反映されず、スクロールバーが一番上に到達したときだけ反映される」
という報告と一致する。

## 原因

`guardMouseWheel`（`src/composables/terminalMouseInput.ts`）は、mouse tracking 中のホイールを
xterm から奪って SGR レポートに変換する（#737）。その際 `wheelNotches`（`mouseReports.ts`）が
delta を **行数に変換して端数を `residual` に貯め、整数ノッチになった分だけ**報告する（#978）。

問題は、**0ノッチのイベントも `preventDefault()` して消費する**こと。これ自体は正しい —
xterm に返すと alt buffer での ↑/↓ 変換（#737 が置き換えた挙動）が復活するため。

結果として、**1ノッチに満たない小さなジェスチャは、アプリにも xterm にも届かず完全に無反応**になる。
端数は貯まっているので失われてはいないが、次に十分な量を動かすまで画面は動かない。

実測（`cellHeight=17px`, `speed=1`）:

| ジェスチャ | イベント数 | ノッチ | 結果 |
|---|---|---|---|
| ホイール 3ノッチ (120px×3) | 3 | 21 | 動く |
| トラックパッド 勢いよく (8px×30) | 30 | 21 | 動く |
| トラックパッド そっと (2px×4) | 4 | **0** | **無反応**（residual 0.706） |
| トラックパッド ごく僅か (1px×6) | 6 | **0** | **無反応**（residual 0.529） |

`reportsMouseToApp` は「alternate buffer かつ mouse tracking 有効」のときだけ true なので、
**この無反応帯は Claude / Codex セルにしか存在しない**（shell セルは xterm がそのまま処理するので
小さい動きでも滑らかに動く）。#1200 が「Claudeセルで」と書いているのと整合する。

## 直しかた（A案: ジェスチャ終端でのフラッシュ）

ジェスチャが終わった時点で、貯まっている端数を四捨五入して 1 ノッチ出す。

- 最後のホイールイベントから `GESTURE_END_MS` 無音ならジェスチャ終了とみなす。
- `|residual| >= 0.5` なら 1 ノッチ、それ未満なら 0（＝ごく僅かな動きでは動かさない）。
- 出したら bank は 0 に戻す。

**採らなかった案:**

- `preventDefault` をやめる → #737 の ↑/↓ 変換が復活するので不可。
- 「ジェスチャ最初のイベントを切り上げ」→ タイマー不要だが、通常のスワイプ全部の出だしが
  1 ノッチ余分に動く。差し引くと residual が負になり、既存の符号反転ガードと噛み合わない。
- 次のイベント到着時に前ジェスチャ分をフラッシュ → 「そのまま二度と動かさない」ケースが直らない。
  さらに前ジェスチャの端数を次のジェスチャに払い出すのは、既存コメントが避けたい形。

## 設計

計算は純粋関数に、タイマーは DOM 側に置く（既存の分担を保つ）。

| 置き場所 | 追加するもの |
|---|---|
| `common` ではなく `src/composables/mouseReports.ts` | `flushWheelResidual(ticker): number` — 端数を四捨五入して返し、bank を 0 にする純粋関数 |
| `src/composables/terminalMouseInput.ts` | `GESTURE_END_MS` のタイマー、直近のセル座標の保持、tracking 終了時のキャンセル |

**破棄済み端末への配慮**: `dispose()` 後の xterm 6.0 は `element` が `undefined`、
`buffer.active.type` が `"normal"` を返し、**どちらも throw しない**ことを実測で確認した。
よってフラッシュ側で `reportsMouseToApp` を再確認すれば、端末より長生きしたタイマーは
何もせずに終わる。try/catch は不要。

**タイマー長**: 100ms。慣性スクロールの尾はイベント間隔が広がっていくので、短すぎると
ジェスチャ途中でフラッシュしてしまう（害は 1 行未満のズレ）。長すぎると、そっと動かしたときの
反応が遅れて感じられる。

## テスト

- `mouseReports.spec.ts` — `flushWheelResidual` が四捨五入して bank を空にすること。
- `terminalMouseInput.spec.ts` — 実 xterm 上で、そっとした 5 イベントが**その場では無音のまま**、
  ジェスチャ終端で 1 レポート出ること（fake timers）。tracking が止まったらフラッシュしないこと。
  既存の「burst を貯める」「小さすぎるイベントを消費する」テストは意味が変わらないので残す。

## #1200 との関係（PR に明記する）

これは #1200 の症状の**実証できる原因の一つ**を直すもので、報告全体の説明にはなっていない。

- 報告者の環境は **WSL2 / Linux** で、macOS のトラックパッドではない（Windows 精密タッチパッドなら
  同じ形になり得るが未確認）。
- 「別セッションに切り替えて戻ると直ることがある」は、端数の貯金では説明できない。
  こちらは mouse-mode ゲート（#1073 系）の状態の話に見える。

つまり**別々の 2 つの問題が同じ症状に見えている可能性がある**。今回は再現・実証できる方を直し、
#1200 は close する。再発したら新しい issue として報告してもらう（同梱 FAQ もその案内になっている）。
