Skip to content

Make the dashboard clickable - #17

Merged
tdwd merged 5 commits into
mainfrom
mouse-navigation
Oct 1, 2026
Merged

tdwd merged 5 commits into
mainfrom
mouse-navigation

Conversation

@tdwd

@tdwd tdwd commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

A click on a project or a session selects it, and a second press on the same row opens it, as ↵ does. A click on a tab switches to it, and the wheel moves the cursor in the column under the pointer. Closes backlog entry 9, built without bubblezone.

Five commits, reviewable one at a time. Only the last is the mouse:

  1. Give the dashboard's cursor and the list windowing their own files. A refactor with no behaviour change; the dashboard renders byte for byte as before. It also removes a second copy of the list-centring rule from picker.go.
  2. Keep the dashboard frame to the terminal's height. A frame taller than the terminal lost its top rows. That happened with a running session at 80 columns, a newline or a tab in a name, a two-line error in the footer, or the empty-store placeholder in a small column. truncate now turns newlines and tabs into spaces, since every caller fills one row. This also fixes a panic at widths 21 to 24.
  3. Land on a new project with the projects list focused. Adding a project from the sessions list left focus on an empty list.
  4. Report a failed write on every path into the pane. The wheel and ^g ^g discarded write errors; keystrokes did not.
  5. Make the dashboard clickable. Clicks resolve against the lines the renderer drew, not against a second copy of the layout arithmetic. docs/architecture.md has the reasoning under "A click resolves against what was drawn".

Behaviour worth knowing

  • One click never opens anything. At launch the first project already has the cursor, and opening it on a first click would start an agent from a click meant to select.
  • The arrow keys now scroll the project list line by line instead of re-centring, so the list does not move under the pointer between the two presses of a double-click.

Checked

  • go test ./... and make race pass on the final commit, and every commit builds and passes the suite.
  • The dashboard renders identically to main in the states compared, except two frames that were taller than the terminal on main and now fit.
  • Each fix and each rule has a test, and the tests were checked by deliberate breakage: every breakage applied was caught.
  • Driven through a real terminal with SGR mouse input against a throwaway demo workspace: a click selects, a second click opens, a tab click switches, the wheel scrolls, and a double-click in a scrolled list opens the row clicked.
  • Three rounds of independent review; everything they found that held up is fixed here.

Not in this PR

  • The session view's sidebar does not take clicks.
  • Heights 2 and 3 still overflow. The Sessions tab does not scroll, so its cursor can fall below the visible rows. Both behave the same on main.

No behaviour changes; the dashboard renders byte for byte as before.

The dashboard's cursor moves out of selection.go into dashcursor.go, and
the rules two of its moves share become one call each: a new project or
a new section puts the session cursor back at the top (selectProject,
selectTab), and an empty session list takes no focus (hasSessionList).
The mouse is about to make these moves as well, and a rule stated at
each call site is a rule that drifts.

The scroll windowing moves out of util.go into window.go and splits into
its layout half (windowed) and its drawing half (fill). picker.go had a
second copy of the same centring in windowIndexes; that now reads
windowed, so the rule has one owner.
A frame taller than the terminal loses its top rows: Bubble Tea keeps the
bottom of an over-tall frame, so the header disappeared and every row
under it was drawn a line higher than it was placed. Four things made
the dashboard frame too tall:

- a detail line wider than its column, which lipgloss wraps. A running
  session's "Active · 1 running" was enough at 80 columns;
- a newline in anything cut to one row, such as a project name, a
  description, or a two-line error in the footer;
- a tab, which the cell count reads as no cells and lipgloss draws as
  several, so a name cut to fit was drawn wider than its slot;
- the empty-store placeholder, whose lines were not cut to the column
  and whose height was not cut to the space.

truncate and truncateStyled now turn a newline or a tab into a space,
since every caller fills a one-row slot; that covers the session view's
footer, sidebar and pane messages too. Each detail line is cut to the
column, and the placeholder is cut to its width and height. It is only
cut, not padded, so Place still centres it.

The rule under the tabs also no longer panics at widths 21 to 24, where
the detail column is narrower than its margins.
Adding a project from the sessions list left focus there, on the new
project's session list, which is empty, with the session cursor still
at its old index past the end. That is the state toggleColumn refuses to
create: a focused list with no rows gives the arrows nothing to move and
shows no cursor.

addProject now selects the new project through selectProject, which puts
the session cursor back at the top, and moves focus to the projects list.
A keystroke into an agent pane reported a failed write and dropped the
attachment. The wheel and the literal prefix (^g ^g) wrote to the same
pane and discarded the error, so writing to an agent that had gone did
nothing and said nothing.

sendToAgent is now the one place that writes into a pane: it reports a
failure, as a keystroke always did, and drops the attachment with it.
Keys, the literal prefix and the wheel all go through it.
A click on a project or a session puts the cursor and the focus on it,
and a second press on the same row, with no key between, opens it as ↵
does. A click on a tab switches to it, and the wheel moves the cursor
in the column under the pointer. In the session view the wheel still
sends ↑ and ↓ to the attached agent; the sidebar does not take clicks.

A click resolves against what was drawn, not against a second copy of
the layout. Both dashboard columns are now built as lines that record
what each part of them stands for (hittest.go); the renderers draw from
those builders and dashboardHit reads the same ones. dashboardLayout is
the one place the columns' sizes and positions come from.

Three decisions worth keeping:

- Two presses in a row open, not "a click on the row with the cursor".
  At launch the first project already has the cursor and the focus, and
  opening it on a first click would start an agent from a click meant
  to select. Any key between the presses breaks the pair.
- The project list stays still while its cursor is on a shown line, and
  otherwise moves only as far as it takes. It used to re-centre on every
  move, so after a first press the list scrolled under the pointer and
  the second press selected whichever project had moved there. The
  arrow keys now scroll it line by line too. Model.projectTop carries
  the start between frames, recorded by Update once the size is known,
  so the first frame still centres as launch always has.
- bubblezone, the planned route in backlog 9, was not needed: the
  builders already know what every line stands for, so recording it
  costs no dependency.

Only presses act, a press cancels an armed ^g, and a modal takes no
mouse input, because it replaces the frame. The mouse routing moves
from input.go to mouse.go; the literal-prefix test moves into the write
test beside the wheel cases.

The click tests aim at the frame as the terminal shows it: they find a
label in View and click that cell, and fail if the frame is not exactly
the terminal's height.
@tdwd
tdwd merged commit d9482ed into main Oct 1, 2026
3 checks passed
@tdwd
tdwd deleted the mouse-navigation branch October 1, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant