TUIダッシュボードのパニッククラッシュをnil安全ガードで修正

basecamp/once

別ターミナルから once teardown を実行した際にTUIがパニックしてクラッシュする問題を、scrollToSelection 関数へのnil安全ガードの追加で修正しました。

背景

once teardown を別ターミナルから実行すると、TUIを開いているターミナルでパニックが発生してクラッシュするバグが報告されていました(#49)。パニックのスタックトレースによると、クラッシュは dashboard.go:359scrollToSelection 関数内で発生しており、index out of range [0] with length 0 というエラーが原因でした。

teardown によってアプリが全て削除されると、m.panels スライスが空になります。しかし scrollToSelection はパネルが存在することを前提としており、空スライスに対してインデックスアクセスを試みた結果、ランタイムパニックが発生していました。

技術的な変更

修正は scrollToSelection 関数の冒頭に空スライスの早期リターンガードを追加するという、最小限の変更です。

変更前:

func (m *Dashboard) scrollToSelection() {
    panelTop := 0
    for i := range m.selectedIndex {
        panelTop += m.panels[i].Height(dashboardShowDetails)

変更後:

func (m *Dashboard) scrollToSelection() {
    if len(m.panels) == 0 {
        return
    }

    panelTop := 0
    for i := range m.selectedIndex {
        panelTop += m.panels[i].Height(dashboardShowDetails)

合わせて dashboard_test.go にリグレッションテストが追加されました。testDashboard(3) で3アプリのダッシュボードを構築した後、d.apps = nil および d.buildPanels() を呼び出してteardown後の状態を再現し、selectPanel(0) がパニックしないことを assert.NotPanics で検証しています。

設計判断

修正箇所を selectPanel ではなく scrollToSelection に限定した ことが今回の設計判断の核心です。スタックトレースを見ると selectPanelscrollToSelection の呼び出し順になっており、パニックの根本原因は scrollToSelection 内のインデックスアクセスにあります。ガードを呼び出し元の selectPanel に置く選択肢もありましたが、問題の責務を持つ関数自身がその前提条件を検査する方式が採用されています。

テストでは d.apps = nil という形でteardownのシナリオを模擬しています。実際の操作(別ターミナルからの once teardown)がアプリリストを空にすることで m.panels も空になるという因果関係を、テストコードのコメント // Simulate all apps disappearing (e.g. once teardown in another shell) が明示しており、将来の読者への文脈提供も兼ねています。

まとめ

4行の早期リターンガードと17行のリグレッションテストという最小限の変更で、並行操作によるUIクラッシュが解消されました。問題の発生箇所に責務を持たせてガードを配置し、再現シナリオを明示したテストで保護する——このアプローチは、TUIのような非同期・並行的な状態遷移が起きやすい環境でのバグ修正の手本といえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
b38e5b3b

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術的変更・設計判断(各論)、まとめ(結論)という3部構成が明確に適用されており、理想的な記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```go:internal/ui/dashboard.go)およびGitHubのPR・Issueへのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

TUI、パニック、リグレッションテストといった専門用語を適切に使用し、専門知識を持つエンジニアという対象読者に適合した内容になっています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが「総論→各論」で構成され、各パラグラフはトピックセンテンスで始まり、1段落1トピックの原則が守られています。段落の長さも適切で可読性が高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

`dashboard.go`に追加されたnilガードと、`dashboard_test.go`に追加されたリグレッションテストのコード引用・説明が、提供されたDiff情報と完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「パニック」「nil安全ガード」「リグレッションテスト」「スタックトレース」など、Go言語とソフトウェアテストの文脈で技術用語が正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「`teardown`により`m.panels`が空になり、インデックスアクセスでパニックする」という原因の説明は、PR情報とDiffから論理的に導かれ、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張はPRのDescriptionとDiffのコード変更によって裏付けられています。特に、「設計判断」セクションはPRに明記されていないものの、コードから読み取れる妥当な洞察であり、ハルシネーションではありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#50)とIssue番号(#49)が正確に記載・リンクされています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「TUIダッシュボードのパニッククラッシュをnil安全ガードで修正」は、PRのタイトル「Fix UI crash on teardown」の内容をより具体的に表現しており、主題と完全に一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事には、PR情報に基づかない外部知識(バージョンサポート状況、リリース日程など)の捏造は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

記事全体が過去のバグ修正について述べており、時間表現の歪曲や誤用は見られません。