refactor: extract the Jobs view state, track selection by job ID

Phase 10 of the whole-project review (findings 5.1 and 5.2), folded into
the ROADMAP file-split item as that plan asks.

5.2 was a real defect. `selected` was an index into a snapshot of the jobs
slice, and every path that changed the slice patched it by hand. The one
path that could not — adopting a different jobs file, where the Service
replaces the whole list and the view only hears about it through the
refresh JobsLoaded triggers — left the details pane redrawing from an
index that belonged to the previous list, describing whichever job now sat
there (or clearing when the new list was shorter) while the list highlight
stayed put. The selection is now a job ID; rows are derived from it at
render time, and refresh ends by pointing the highlight at the selected
job, so the two can no longer disagree.

5.1: newJobsView was one 330-line constructor whose dozen closures shared
seven mutable locals. It is now a jobsView struct over a jobsViewState
that owns the snapshot, the folder filter, and the selection — the
invariant that used to be maintained by hand in five places lives in one
place — split across jobs_view.go (construction, refresh, layout),
jobs_view_state.go, jobs_view_list.go, and jobs_view_toolbar.go. The
folder-option rebuild that appeared verbatim in three handlers is one
method.

Behaviour that changed beyond the fix: switching the folder filter keeps
the current selection when the new filter still shows it, instead of
always jumping to the folder's first job.

Docs: ARCHITECTURE records the new file layout and the selection-by-ID
contract; ROADMAP drops jobs_view.go from the over-guideline table and
refreshes the other five numbers (finding 2.4); TESTS documents the new
state test file and the adoption regression test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 22:38:47 +03:00
parent ca2a8c8aa7
commit bd7ebde68e
10 changed files with 913 additions and 355 deletions
+16 -4
View File
@@ -270,16 +270,28 @@ the moment the window opens.
### `jobs_view.go` file structure
The size guideline for a file in this project is ~250 lines.
`src/ui/jobs_view.go` is split across three files along these seams; the view
file itself has grown back over the guideline since — see the split item in
[ROADMAP.md](ROADMAP.md), which tracks every file currently over it:
`src/ui/jobs_view.go` is split across five files along these seams:
| File | Contents |
|------|----------|
| `jobs_view.go` | `newJobsView` — list, toolbar, button wiring, and layout |
| `jobs_view.go` | `jobsView` struct — construction, `refresh`, `updateDetails`, the pause control, and layout assembly |
| `jobs_view_state.go` | `jobsViewState` — the jobs/runtime snapshot, the folder filter, and the selection |
| `jobs_view_list.go` | The sidebar list: row template, row rendering, row mode, and the compact/detailed toggle |
| `jobs_view_toolbar.go` | The per-job button row — new, edit, run, pause, delete |
| `jobs_view_details.go` | `detailsPanel` struct — widget creation, `update`, `clear`, `container` |
| `jobs_view_helpers.go` | Pure helpers — `filteredJobIndexes`, `folderOptions`, `filterValue`, `indexOfID`, `lastJobLogs`, `nextJobListView`, `viewToggleText` |
The widgets hold no job state of their own: they read `jobsViewState`, which is
the only thing that reads the Service. The **selection is a job ID, not a row
index.** Every path that changes the job list replaces the state's snapshot —
create, delete, and edit from this view's own handlers, adopting a different
jobs file from the Service, which the view only learns about through the refresh
`JobsLoaded` triggers. An index that outlives its snapshot points at whichever
job now sits there, so the details pane would describe one job while the list
highlighted another. Rows are derived from the ID at render time
(`selectedIndex`, `displayRow`), and `jobsView.refresh` ends by pointing the
list's highlight at the selected job.
### `settings_view.go` file structure
`src/ui/settings_view.go` is split across three files the same way, once its