Skip to content

feat(tui): take the shared key rows and Claude opener from gotermboard - #22

Merged
datapointchris merged 4 commits into
mainfrom
gotermboard-host-exports
Nov 8, 2026
Merged

datapointchris merged 4 commits into
mainfrom
gotermboard-host-exports

Conversation

@datapointchris

@datapointchris datapointchris commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

The board retyped two pieces of gotermboard. Its key table copied the row shape, the filter on
applies and the universal rows j k g G ctrl+d ctrl+u J K [ ] ? q. Its C
copied slate's Claude session opener, and outside tmux it ran tea.ExecProcess, so a failed session
said only exit status 1. gotermboard now exports both, so the copies go. The board's Enter had
also drifted from the fleet's: it always opened the full-screen reader, while its in-place text was
the note alone.

Session: 86a41f0d-4642-4e49-bf73-b5688a1bb146

What to look at

  1. tui/key_host.go in b4cfcde. It is App as gotermboard's Host. Check that MoveBy and
    HalfScreen act on the pane the key works in: the map's, the project list's or the item list's.
    Check that Toggles is false where the board's own enter and space act.
  2. tookDetail in the same file. Enter reads the detail, then opens it full screen where it runs
    past the pane, or toggles it in place. A detail open in place is read again after each items read.
  3. actions in tui/action_table.go. The board's own rows go in a layer ahead of
    UniversalActions, so a project's enter and space and a task's space win where they apply.
    In c571ea4, runKey, footerHints and helpLines read those rows through gotermboard's
    RunKey, FooterHints and BySection.
  4. tui/claude_session.go in 5cc96bd, and tookClaudeOutcome.
  5. go.mod pins gotermboard to the head of feat: export the key table and the Claude session opener for a host gotermboard#18. It moves to that pull
    request's release tag before this merges.

How it was verified

  • go test ./... passes, with every tuidrive flow replayed against the built binary.
  • detail-in-place replaces note-in-place. On "Pick a theme", Enter opens a three-line detail in
    place and Enter closes it. On the export item, Enter opens the reader, because its note runs past
    the pane. Space then opens the same detail in place, and ctrl+d and J scroll through it with
    the item's line held under the band.
  • The help flow's frame shows the overlay grouped under Moving, On a project, Views and filters and
    Everywhere, with the universal rows under Moving and Everywhere.
  • TestStatusBar_NeverWiderThanTheWindow checks that ? keys survives clipping at every width, and
    that each hint shown is a whole hint from the footer.
  • Every other frame change was read in the diff. The footer words changed, every item row gained a
    chevron, and the newer gotermboard's markdown renderer changed some note styling.

What changes

  • Enter on an item opens its detail in place where it fits the pane, and full screen where it does
    not. It always opened full screen before.
  • Space on an item opens the detail, rather than the note alone. So every item opens, and every
    item's row carries a chevron.
  • The footer's hints read c complete a add n note where they read [c]omplete [a]dd [n]ote.
    The status bar's right side reads ? keys where it read [?]help.
  • The footer now names j/k move, [/] jump, enter open, space fold and q quit after the
    board's own hints. A 100-column window clips most of them.
  • Outside tmux, a failed Claude session says the last line it wrote to stderr. A session that held
    the terminal reads the board again when it ends.
  • Inside tmux, the flash says a window opened for the Claude session, where it said a session
    opened, because tmux answers before claude starts.

Decisions, and what they rejected

  • The modal lists keep their own movement rows. The project picker, the move and the dependency
    links each move a cursor of their own, and bind no ? or q. Rejected: running the universal
    rows there too, which would add q quitting from inside a picker.
  • The footer takes gotermboard's key word form. Rejected: keeping [a]dd beside gotermboard's
    enter open, which would mix two forms in one line. Also rejected: rewriting gotermboard's
    footers into brackets, where [/] jump has no bracketed form.
  • The board's own rows come ahead of the universal ones, as a gotermboard board's do. So a
    narrow footer clips the universal hints first.

Risk and rollback

This releases the CLI and the API on one version. A revert puts both copies back and needs the
gotermboard v0.31.0 pin it had.

… an item's detail in place

The board retyped gotermboard's key table: the row shape, the filter on
applies, and j k g G ctrl+d ctrl+u J K [ ] ? q with near-identical
labels. gotermboard now exports them, so the board's table is its own
rows layered ahead of gotermboard's UniversalActions, acting on App as a
Host. The modal lists, the picker, the move and the dependency links,
keep their own movement, because each moves a cursor of its own.

Enter and space are the shared rows too. Space opens an item's detail
under its row however tall, and Enter opens it there where it fits the
pane and full screen where it does not, because both show one document.
The detail is the item's projects, created date, what blocks it and what
it blocks, its tasks and its note, where it was the note alone, so every
item now carries a chevron. A detail open in place is read again when
the board reads its items.

The footer's hints take gotermboard's words, such as "a add" for
"[a]dd", two spaces apart, and the status bar pins "? keys" at its
right.

Agent-Name: claude-code-unattended
Agent-Session: 86a41f0d-4642-4e49-bf73-b5688a1bb146
Agent-Run: 01a1bc5d-2bb7-7e54-bc32-13efbb7743af
Worklog: 2605.1
The board copied slate's opener step for step, and outside tmux it ran
tea.ExecProcess, so a failed session said only how it exited, such as
"exit status 1". gotermboard now opens the session for a board and for
a program that draws its own screen alike, so the copy goes, and a
failed session's footer says the last line it wrote to stderr.

A session that held the terminal reads the board again when it ends, in
case it changed the items.

Agent-Name: claude-code-unattended
Agent-Session: 86a41f0d-4642-4e49-bf73-b5688a1bb146
Agent-Run: 01a1bc5d-2bb7-7e54-bc32-13efbb7743af
Worklog: 2605.1
The key handler, the footer's hints and the ? overlay's sections come
from gotermboard's RunKey, FooterHints and BySection, so the board keeps
no copy of how a table's rows are run, named on the footer or grouped
under their sections. The pin moves to the gotermboard head that
exports them.

Agent-Name: claude-code-unattended
Agent-Session: 86a41f0d-4642-4e49-bf73-b5688a1bb146
Agent-Run: 01a1bc88-08a6-79cb-9f61-c8b272c4f88b
Worklog: 2605.1
datapointchris added a commit to datapointchris/gotermboard that referenced this pull request Nov 8, 2026
)

A program that hosts only the `Reader` and draws its own panes had to
retype two pieces of this
package. Its key table copied `Action`'s shape, the filter on `Applies`,
the universal rows and the
loops that run a key, name it on the footer and group it in the `?`
overlay, because `Action` is
written against `*Model`. Its `C` copied the Claude session opener step
for step, and outside tmux
it ran `tea.ExecProcess`, so a failed session said only `exit status 1`.
The stderr-keeping
handover is unexported, so the host could not reach it.

This exports both. `KeyAction[H]` is a key-table row for a screen of any
type, with `Action` as its
`*Model` instance and the `Reader`'s rows as `KeyAction[*Reader]`.
`UniversalActions` is the rows
every board binds, written against a `Host` interface, and `ActionsNow`
layers a host's own rows
ahead of them. `RunKey`, `FooterHints` and `BySection` read those rows
for a key handler, a footer
and an overlay, and a `Model` and the `Reader` read theirs through the
same three.
`OpenClaudeSession` opens `C`'s session as one `tea.Cmd` for a `Model`
and a host alike, with an
environment that reaches a tmux window too. It ends in a
`ClaudeSessionOutcome` carrying a failed
session's last stderr line.

Session: 86a41f0d-4642-4e49-bf73-b5688a1bb146

## What to look at

1. `key_actions.go` in 4d07353 and f38ce85. Check that `modelActions`
gives a `Model` the same
rows, keys, footers, labels and order `actionTable` had: the universal
rows, then `v` `f` `p`
`/` `R`. Check that `g`/`G` moving by `pastEitherEnd` stops where moving
by the node count did.
2. The `Host` interface in `key_actions.go`. It is what a host
implements, so check that each
   method is one a host can answer without knowing about a `Model`.
3. `reader.go` in f38ce85. The `Reader`'s rows changed type, and its
handler, footer and overlay
read them through `ActionsNow`, `RunKey`, `FooterHints` and `keyLine`.
4. `claude_session.go` in 975c465 and e4a0f79. Check the brief's words,
the sentences each path
   says, and that `Env` reaches the session inside tmux and out.
5. The `ClaudeSessionOutcome` case in `Model.update`. It reaches only a
model that is not hidden.
That stands in for the starting tab, because a session holding the
terminal leaves no key to
   change tabs, and tmux makes the window it opens current.

## How it was verified

- `go test ./...` passes with every board and reader test unchanged.
Those tests pin the footer
  and overlay a `Model` and the `Reader` draw from their tables.
- `GOARCH=386 go test .` passes. At 3992c2f it failed two tests, because
`G` wrapped to the top.
-
`TestASectionSplitAcrossLayersHeadsTheOverlayOnceWhereItsFirstRowStands`
layers a host's
`Moving` row ahead of the universal ones, and checks `BySection` heads
`Moving` once.
-
`TestAClaudeSessionInTmuxOpensAWindowNamedForItsRecordWithTheBriefWhole`
runs the command with a
stand-in `tmux`, `env` and `claude` on `PATH`. It checks the refusal
with no `claude`, then the
`new-window -n "claude 12" -c <dir> env WORK=… claude <brief>`
arguments, with the fallback
  sentence ending the brief.
-
`TestAClaudeSessionOutsideTmuxHoldsTheTerminalAndItsTabSaysItsLastStderrLine`
runs two tabs under
a real `tea.Program`, with a stand-in `claude` that writes two stderr
lines and exits 3. It checks
the session ran in `Dir` with the brief. Only the showing tab's board
received the outcome, and
  its footer carried the last stderr line, `no credits left`.
- fleet's and prs's suites pass unchanged against this branch through a
`go.work`. Both are
  `Model` boards.

## What changes

- `Action` is now an alias of `KeyAction[*Model]`. A literal with field
names compiles as before.
  It gains a `Section` field, which a `Model`'s overlay ignores.
- A `Model` handles `ClaudeSessionOutcome` itself: the tab showing says
`Said` on its footer, then
  hands the outcome to its `ReceivingBoard`.
- A board's frames and keys are unchanged, and so are the `Reader`'s.

## Decisions, and what they rejected

- **A `Model` adapts to `Host` through an unexported type** rather than
exporting `MoveBy`,
`ShowKeys`, `Quit` and the rest on `Model`. Exporting them would give
every board nine new
  methods to read past, for no caller that needs them.
- **A `Model`'s longer labels for `enter` and `?` come from an
unexported builder that takes
them.** The `?` overlay lists every read, and the side panel can already
show a row's document.
Rejected: changing a `Model`'s labels to the generic ones, which would
change every board's `?`
  overlay.
- **`g` and `G` move by `1 << 30` rows.** Every host adds the move to
its cursor before clamping,
so the bound sits in the one place they all read. Rejected: a clamp in
`Model.move` alone, which
  leaves each other host to wrap on a 32-bit build.
- **The package exports the readers' data and keeps the drawing.**
`RunKey`, `FooterHints` and
`BySection` return what a screen draws. Rejected: a `FooterLine(rows,
width)` and an
`OverlayLines(rows)`, because a host that draws its own screen cuts and
styles its own lines.
- **`Host` gains no method.** A later universal key asks a host for more
through an interface of
  its own, so a host built against this one keeps building.
- **The outcome reaches the tab showing rather than the tab that started
it.** `HandedBack` routes
by a model pointer that `Model.HandOver` captures. A function that
serves a host too has no
model to capture. Rejected: a second, `Model`-only entry point, which
would be the second copy
  this change exists to remove.
- **The directory, its fallback and the session's environment stay the
caller's.** Each caller
reads its own repo registry and knows its own telemetry, so the function
takes the directory,
  the reason and `Env` rather than looking any of them up.

## Risk and rollback

A release of this is a minor tag. A board that never calls
`OpenClaudeSession` or
`UniversalActions` draws as before. A revert removes the exports, so a
caller pinned to them stops
building until it pins the tag before.

## The review

Round 1:
#18 (comment)

- **G1.1** fixed in e8e4a4e. `pastEitherEnd` is `1 << 30`, and
`Host.MoveBy` says so. Every host
adds before it clamps, including projects' `moveBy` and `depTreeMove`,
so the bound fixes them
  all. `GOARCH=386 go test .` passes.
- **G1.2** fixed in f38ce85. `BySection` groups rows under their
sections where each first stands,
and `ActionsNow`'s doc says its order is the footer's. projects'
`helpLines` reads it in
  datapointchris/projects#22.
- **G1.3** fixed in f38ce85. The README names what the rows do rather
than listing their keys, and
  says `esc` stays the host's.
- **G1.4** fixed in e4a0f79, with the rewrite given.
- **useless-test.1** fixed in e4a0f79. The three `Said` equalities are
checks that the sentence
names the directory, the fallback's reason and the last stderr line. The
footer is compared with
  `Said` itself.
- **contracts.1** fixed in e4a0f79. `Brief`'s doc and the README say it
asks nothing and the opener
adds the ask. Both callers drop the sentence in
datapointchris/projects#22 and
datapointchris/slate#18. Rejected: skipping the ask where `Brief`
already ends with it, which
  would hide a stale copy rather than remove it.
- **contracts.2** fixed in e4a0f79. `Fallback`'s doc says it is a bare
clause completing
"because", such as `12 names no one repo`. slate's `sessionDirectory`
returns bare clauses in
  datapointchris/slate#18.
- **contracts.3** fixed in f38ce85. `Host`'s doc says it gains no
method, and a later key asks for
  more through an interface of its own.
- **fleet-patterns.1** fixed in e4a0f79. `ClaudeSession.Env` is the
session's environment, put
ahead of `claude` through `env` inside tmux and in the command's
environment outside it. The
package names no attribute, because any program may open a session.
slate's `C` fills it with
`OTEL_RESOURCE_ATTRIBUTES` and `BAGGAGE` in datapointchris/slate#18
(1a6d89c): its inherited
attributes, `slate.record.number`, and each inherited baggage member as
it came.
- **fleet-patterns.2** fixed — same as contracts.1.
- **fleet-patterns.3** fixed in datapointchris/projects#22. projects
writes its own hints in the
`key word` form and takes the universal rows' labels, so one footer
shows one form.
- **failure-paths.1** fixed in e4a0f79. Inside tmux the outcome says a
window opened for the
session, and the doc says tmux answers before `claude` starts. Rejected:
a wrapper that holds a
failed window open. Whether `claude` exits 0 when a person ends it is
not established, so it
  could hold the window after every ordinary end.
- **code-architecture.1** fixed in f38ce85. `readerAction` is gone, and
the `Reader`'s rows are
  `KeyAction[*Reader]` read through `ActionsNow`.
- **code-architecture.2** fixed in f38ce85. `RunKey`, `FooterHints` and
`BySection` are exported,
and a `Model` and the `Reader` call the first two and share `keyLine`.
Rejected: `FooterLine` and
  `OverlayLines`, for the reason under Decisions.
- **code-architecture.3** fixed in f38ce85. `universalActions` takes
`enter`'s and `?`'s labels,
and `modelActions` passes a `Model`'s, so no row is found by its first
key.
- **stale-prose.1** fixed in e4a0f79, with both rewrites given.
- **stale-prose.2** fixed — same as G1.3.
- **stale-prose.3** fixed in f38ce85. The docstring says the rows are
what every board binds and a
  host's table starts from.
- **synthesis.1** fixed — same as contracts.1.
- **synthesis.2** fixed — same as code-architecture.2.
- **synthesis.3** fixed — same as G1.3.
…n rows

The branch was pinned to the library pull request's head commit. v0.43.0 is the
first tag holding it.

Agent-Name: claude-code
Agent-Session: 60bee577-11ae-4c13-9517-75a25f6211a6
@datapointchris
datapointchris merged commit 0d3d62b into main Nov 8, 2026
6 checks passed
@datapointchris
datapointchris deleted the gotermboard-host-exports branch November 8, 2026 19:43
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