feat: Repos タブにリポジトリ一覧のサイドバーを足す - #71
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
golangci-lint flags unused fields and funcs; row.prs/issues/counted and Model.rowNames belong to the tasks that will read them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Repos tab now shows a left-hand sidebar listing every configured repository with an open-item badge, alongside the existing table. The sidebar folds away under 100 columns, keeping the current repository's name in the header. h/l move focus between the sidebar and the table; j/k drive whichever pane has focus. The table's columns now measure against the width left over once the sidebar has taken its share. Regenerates the app package's golden files: the Repos tab they render now includes the sidebar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The counts fetch that Refresh triggers had a test; the one Init triggers did not, and the doc comment claiming otherwise sat on a model with no sidebar rows to fetch counts for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sidebarRowAt checked only the top and the total repository count, not sidebarRows(): with more repositories than fit on screen, a click on the footer (or lower) mapped to a row past what was actually drawn and selected it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Refresh() and showTab() set loading and called fetchList regardless of whether there were any rows. With no rows, fetchList asks for "" and gh pr list falls back to the working directory's repository; when that call fails, the errMsg becomes repo.ErrorMsg and app.go drops the whole UI onto its error screen. This hit two real paths: pressing r on an empty Repos tab, and app.refreshLists calling repo.Refresh unconditionally after any review submission or merge. Guard both the same way Init already does, on len(m.rows) == 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SetCurrent rebuilds m.rows from the settings file's strings, which resets every row's badge to uncounted, but only returned selectRow's fetchList command. On the ordinary --repo-less startup path, RepoCounts (one GraphQL call) almost always answers before resolveRepo (gh repo view, 6+ seconds cold), so SetCurrent's rebuild landed after counts had already arrived and wiped every badge back to "—" until the user pressed r. Have SetCurrent batch a fresh fetchCounts alongside selectRow's fetch, and add a test on the app side confirming RepoCounts is actually called once the lookup resolves the current repository (fakeSource.RepoCounts previously returned nil, nil unconditionally and recorded nothing). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
prListMsg/issueListMsg dropped an answer whose repo did not string-match the selected row's name. repoCountsMsg rewrites a row's name to GitHub's own spelling, and RepoCounts (one GraphQL call) usually answers before a just-started ListPRs/ListIssues does, so a settings file spelled "KUKV/Octoscope" got its row renamed to "kukv/octoscope" out from under the fetch already in flight for the old spelling: the answer's repo no longer matched, was dropped, and the spinner never stopped. A GitHub-side rename (old/name -> new/name, which GitHub redirects) hits the same path. errMsg carried no guard at all: a fetch for a row the cursor moved past could fail seconds later and drop the whole UI onto app.fail's screen, unrelated to whatever the cursor is on by then -- routine with the 20-50 rows a real settings file has (design §2). Replace the name comparison with a generation counter selectRow bumps each time the cursor moves to a different row. fetchList and openWeb take the generation their caller was in; Update drops any prListMsg, issueListMsg or errMsg whose generation no longer matches, which no longer depends on a row's spelling and closes the errMsg gap the same way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TestMovingTheSidebarClearsTheOldList and TestSetCurrentClearsAndRefetches asserted "first pr" is absent from View() to guard selectRow's m.prs, m.issues = nil, nil. That assertion cannot fail: the same selectRow call also sets m.loading[m.tab] = true, and body() draws only a spinner line while loading, so a stale m.prs never reaches View() whether or not it was cleared. What the clear actually protects is SelectedRef() and selectedURL(): they read m.prs/m.issues and m.cursors directly, ignoring m.loading, and pair whatever they find there with the newly selected row's name. Without the clear, enter/o/d/s pressed while the new row is still loading would open the previous repository's item under the new repository's name. Assert on SelectedRef() instead, which does fail when the clear is removed (verified by hand before writing this commit). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop item 5 (Refresh/showTab fetching with no rows): fixed as C1 in this
fix wave, so it is no longer carried forward. Its old reasoning ("no real
harm") was itself wrong -- the failure reaches app.fail's full-screen
error via repo.ErrorMsg.
Rewrite item 4's reasoning: the rule column and its click handling did not
exist before this slice, so "an extension of pre-existing behavior" does
not hold. The real reason to carry it forward is that a miss on that one
column only nudges the selection to the table's first row, not a broken
or misdirected action.
Rewrite item 6's reasoning (startup flicker): "momentary" and "in theory"
understated it. With an empty settings file, repos.none is shown for the
lookup's entire span -- up to repoLookupTimeout (20s), 6+ seconds cold --
not a flash. Flag it for higher priority next slice.
Add re-evaluation notes to items 3 and 7 (renumbered from 3 and 7): I1
changed the assumption item 3's reasoning rests on (name matching is gone,
replaced by a generation counter), and C2 changes what item 7's "—" badge
can mean (never fetched vs. fetched and failed), so both need a fresh look
against the fixed code, not just repeating the old text.
Add two new items found while fixing this wave: sidebar navigation
starting one gh subprocess per row with no cancellation or debouncing
(the new generation counter gives a cancellation context something to key
off, left for the next slice's design call), and the pane-switch key hint
staying in the footer when the sidebar is folded away or empty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code Metrics Report
Code coverage of files in pull request scope (86.3%, patch 88.8%)
Reported by octocov |
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.
Phase 4 スライス 2-2。Repos タブを常設にし、設定ファイルの
repositoriesを左ペインのサイドバーとして描き、選択した行のリポジトリで右ペインを引き直せるようにする。
設計:
docs/superpowers/specs/2026-09-08-phase4-design.md§4・§8・§9画面:
docs/superpowers/specs/2026-09-05-octoscope-standalone-design.md§4.0・§4.2・§4.6実装計画:
docs/superpowers/plans/2026-09-09-phase4-repos-sidebar.md入ったもの
app.Options.HasRepoをRepo/Repositoriesに解体。カレントのリポジトリが一覧に無ければ一時行として先頭に出て、設定ファイルには書かない
20〜50 件でもスクロールする
GitHub が解決した綴りを採用する。引けなかった行は「—」で残る
h/lでペイン移動、j/kでフォーカス側のカーソル移動。 マウスも対応(サイドバーのクリック・ホイール、右ペインの X オフセット)
repos.none。 初期投入の導線は 2-3入っていないもの(2-3)
追加ダイアログ(
a)、削除(x)、初期投入の導線、config.Save。実測(設計 §2 に追記)
自分の公開リポジトリ 30 件を
buildRepoCountsQueryと同じ形(alias を並べて 1 リクエスト)で組み、
gh api graphqlで 1 回計測。8.13 秒、30 alias 全件解決・errors無し。1 リクエストで足りる。30 件超は未計測。
レビューで見つけて直したもの
タスクごとのレビュー 8 回に加えて全ブランチレビューを 1 回。後者でしか出なかったもの:
rを押す、またはレビュー提出後の自動 refresh で全画面エラーに落ちる退行(
Refresh()/showTab()に行数ガードが無かった)SetCurrentがバッジを全消しして引き直さない ため、--repo無しの既定の起動でバッジが常に「—」だった
KUKV/Octoscope)で飛行中の一覧が破棄され、スピナーが止まらなくなる問題。名前の完全一致を世代カウンタに置き換えて解決
errMsgにガードが無く、離れた行の失敗が全画面エラーを起こす問題(同じ世代カウンタで解決)loadingがスピナーを出すためView()ベースの主張が落ちなかった)をSelectedRef()ベースに書き直し積み残し
docs/superpowers/2026-09-09-phase4-repos-sidebar-followups.mdに 9 件、すべて理由つき。実端末での確認が要る(TTY の無い環境では代行できない):
サイドバー付きの Repos タブ、
--lang jaの 80 桁、h/lとj/k、マウスでのサイドバー選択、設定が空のときの
repos.none。検証
make check緑(1044 tests、lint 0 issues)。golden は en / ja × 80 / 120 / 160。🤖 Generated with Claude Code