Repository navigation
feat(tui): take the shared key rows and Claude opener from gotermboard - #22
Merged
Merged
Conversation
… 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
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.
The board retyped two pieces of gotermboard. Its key table copied the row shape, the filter on
appliesand the universal rowsjkgGctrl+dctrl+uJK[]?q. ItsCcopied slate's Claude session opener, and outside tmux it ran
tea.ExecProcess, so a failed sessionsaid only
exit status 1. gotermboard now exports both, so the copies go. The board's Enter hadalso 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
tui/key_host.goin b4cfcde. It isAppas gotermboard'sHost. Check thatMoveByandHalfScreenact on the pane the key works in: the map's, the project list's or the item list's.Check that
Togglesis false where the board's own enter and space act.tookDetailin the same file. Enter reads the detail, then opens it full screen where it runspast the pane, or toggles it in place. A detail open in place is read again after each items read.
actionsintui/action_table.go. The board's own rows go in a layer ahead ofUniversalActions, so a project's enter and space and a task's space win where they apply.In c571ea4,
runKey,footerHintsandhelpLinesread those rows through gotermboard'sRunKey,FooterHintsandBySection.tui/claude_session.goin 5cc96bd, andtookClaudeOutcome.go.modpins gotermboard to the head of feat: export the key table and the Claude session opener for a host gotermboard#18. It moves to that pullrequest's release tag before this merges.
How it was verified
go test ./...passes, with every tuidrive flow replayed against the built binary.detail-in-placereplacesnote-in-place. On "Pick a theme", Enter opens a three-line detail inplace 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+dandJscroll through it withthe item's line held under the band.
Everywhere, with the universal rows under Moving and Everywhere.
TestStatusBar_NeverWiderThanTheWindowchecks that? keyssurvives clipping at every width, andthat each hint shown is a whole hint from the footer.
chevron, and the newer gotermboard's markdown renderer changed some note styling.
What changes
not. It always opened full screen before.
item's row carries a chevron.
c complete a add n notewhere they read[c]omplete [a]dd [n]ote.The status bar's right side reads
? keyswhere it read[?]help.j/k move,[/] jump,enter open,space foldandq quitafter theboard's own hints. A 100-column window clips most of them.
the terminal reads the board again when it ends.
opened, because tmux answers before
claudestarts.Decisions, and what they rejected
links each move a cursor of their own, and bind no
?orq. Rejected: running the universalrows there too, which would add
qquitting from inside a picker.key wordform. Rejected: keeping[a]ddbeside gotermboard'senter open, which would mix two forms in one line. Also rejected: rewriting gotermboard'sfooters into brackets, where
[/] jumphas no bracketed form.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.