From 62d2171f4dbf6cbb7ec1879e4078659ffcd9a3aa Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:30:06 -0400 Subject: [PATCH 1/7] fix: make home's ask-here hints and the /help digits name what the keys do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - alt+enter on home belongs to the router (the composer layer); drop the unreachable alt+enter arm from home's ask-here case and keep ctrl+enter. - An errand row's foot says `enter or → answer this ask here` (and `enter answer this ask here · esc clear` over a draft); tab on the row is the next place. - The pane's `tab or esc back to the list` stays the last clause, so a 60-column frame keeps it; `/ commands` is not named while the pane has the keyboard. - /help numbers each place (`1 home · 2 AI teams · …`) instead of calling placeOrder the tab bar's order. - The manual pages say the same: ctrl+enter vs alt+enter, the row and pane feet, and which digit opens which place. Co-Authored-By: Claude Opus 5.5 --- internal/e2e/tmux_test.go | 13 +- internal/e2e/tuiwords_test.go | 5 +- internal/manual/chat/asking-from-home.md | 58 ++++++-- internal/manual/chat/commands.md | 2 +- internal/manual/chat/home.md | 26 ++-- internal/manual/chat/keys.md | 39 ++++-- internal/manual/chat/places.md | 7 +- internal/manual/chat/screen.md | 11 +- internal/manual/chat/what-i-remember.md | 2 +- internal/manual/chat_test.go | 12 ++ internal/tui3/commands.go | 8 +- internal/tui3/helpdoors_test.go | 16 ++- internal/tui3/home.go | 53 +++++-- internal/tui3/homedraft.go | 9 +- internal/tui3/homeexchange.go | 34 +++-- internal/tui3/homeexchange_test.go | 170 +++++++++++++++++++++-- internal/tui3/pages.go | 13 +- internal/tui3/placeeveryone_test.go | 4 +- internal/tui3/placekeys.go | 8 +- 19 files changed, 381 insertions(+), 109 deletions(-) diff --git a/internal/e2e/tmux_test.go b/internal/e2e/tmux_test.go index 7464ebe30d..9000071f14 100644 --- a/internal/e2e/tmux_test.go +++ b/internal/e2e/tmux_test.go @@ -496,14 +496,15 @@ func (r *rig) lit(text string) { // ctrlEnter is `ask here` without leaving the box. // -// TWO SPELLINGS, BOTH SENT AS THE BYTES A TERMINAL SENDS. home.go binds -// "ctrl+enter" and "alt+enter" to one call and says why: ctrl+enter only -// reaches the app on a terminal that can tell it from a plain enter. Bubble Tea -// v2 asks for the kitty protocol and modifyOtherKeys unconditionally, so -// CSI 13;5u decodes as ctrl+enter — this suite sends that one, and alt+enter -// (ESC CR) is the fallback every terminal delivers. +// IT IS SENT AS THE BYTES A TERMINAL SENDS. ctrl+enter only reaches the app on +// a terminal that can tell it from a plain enter. Bubble Tea v2 asks for the +// kitty protocol and modifyOtherKeys unconditionally, so CSI 13;5u decodes as +// ctrl+enter, which home binds to `ask here`. func (r *rig) ctrlEnter() { r.lit("\x1b[13;5u") } +// altEnter is ESC CR, which every terminal delivers. It is NOT a second +// spelling of ctrlEnter: on home the router takes alt+enter for the composer +// layer, whose second press sends the task (internal/tui3's composerlayer.go). func (r *rig) altEnter() { r.lit("\x1b\r") } // mouseTo moves the pointer to a 1-based cell with an SGR motion report, which diff --git a/internal/e2e/tuiwords_test.go b/internal/e2e/tuiwords_test.go index 6a13bfb558..25594117ce 100644 --- a/internal/e2e/tuiwords_test.go +++ b/internal/e2e/tuiwords_test.go @@ -344,8 +344,9 @@ var tuiWords = map[string]tuiWord{ why: "both ways out of the pane, named because on a narrow frame the pane is the whole screen", }, "homeAnswerHint": { - screen: "enter or tab answer this ", - why: "the hint while the cursor stands on an errand row that is asking something", + screen: "enter or → answer this ", + why: "the hint while the cursor stands on an errand row with an empty box. It said `enter or tab` " + + "until `tab` on the row had long been the next place; `→` is the way into the pane that replaced it", }, "standRemindYes": { screen: "Remind me", diff --git a/internal/manual/chat/asking-from-home.md b/internal/manual/chat/asking-from-home.md index 50cee1db55..a160c043bb 100644 --- a/internal/manual/chat/asking-from-home.md +++ b/internal/manual/chat/asking-from-home.md @@ -46,9 +46,14 @@ done**. They sit at the very **top of home's `threads` panel**, above every conversation — an errand is a thing you asked for a minute ago. `enter` or `→` on the row hands the keyboard to the pane. The hint under the box says so: -`↑↓ move · enter or tab answer this ask here · esc close`. (`tab` on the row was the way in -until the places arrived and took that key for the next place; `→` points at the column the -pane is drawn in, which is where the gesture went.) +`↑↓ move · enter or → answer this ask here · esc close`. With something typed in home's box +it reads `↑↓ move · enter answer this ask here · esc clear` instead, because `→` then moves +the caret and `esc` clears what you typed before it closes anything. + +**`tab` on the row is not a way in.** It goes to the next place, as it does on every place. +`tab` was the way in until the places arrived and took that key; `→` points at the column +the pane is drawn in, which is where the gesture went. The hint went on saying +`enter or tab` long after, and it was wrong. ## How do I know ask here is doing something — the spinner, the clock and the live strip @@ -177,8 +182,8 @@ it: in; **`→` on the exchange's row** takes it back in. (Both directions used to be `tab`; a place's `tab` is the way to the next place, and a box that has taken the keyboard keeps its own keys, which is why one half of the toggle stayed and the other moved to the arrow - that points at the pane.) It works from - `continue as a conversation` row, an open card. It never loses what is in the pane. + that points at the pane.) It works from the box, the `continue as a conversation` row + and an open card alike. It never loses what is in the pane. - **`esc`** in the pane hands the keyboard to the list. One layer at a time: if you have half a follow-up typed, the first `esc` clears that and the second one leaves. - **`enter`** on the exchange's row in the list hands the keyboard to its pane. @@ -193,9 +198,14 @@ column exactly as they do with no exchange on screen, and `enter` opens the row cursor. **The pane follows the cursor** — walk off the exchange and the row you land on draws its own card again, where the frame is wide enough to have one. +## What the line under the ask here pane says — the answers, a follow-up and the way back + While the pane has the keyboard the hint reads `enter sends a follow-up · tab or esc back to the list`, with the card's own answers in front of it when a card is up and -`↓ continue as a conversation` after it when that row is on screen. +`↓ continue as a conversation` before the way out when that row is on screen. Home's +`/ commands` is not on it: a `/` typed in the pane starts a follow-up and opens no list. +`tab or esc back to the list` is always the last clause, and on a narrow window it is the +one that stays (*Ask here on a narrow window*, below). **The answers in that line are the ones the card actually drew, and never one more.** A card that offers all three reads @@ -219,7 +229,17 @@ of sat side by side: - the **exchange** is the screen while it holds the keyboard — the same pane, the same card, the same live strip, drawn at the full width; - **`esc`** or **`tab`** brings the list back, with the exchange's row still on it wearing - its tail, and **`enter`** on that row opens it again. + its tail, and **`enter`** or **`→`** on that row opens it again. + +**The way back is always on the line under the box**, at the narrowest width this layout +has, sixty columns. `tab or esc back to the list` is the last clause of that line and the +one that stays; the clauses in front of it give way first, nearest first — home's chords +such as `alt+e effort`, then `↓ continue as a conversation`, then `enter sends a follow-up`, +then the card's answers from the last one back — so a card at sixty columns reads +`1 Remind me at 6 today · tab or esc back to the list`, and its other answers are still on +the card's own rows. It used to be the other way round: the way back +was the first thing a narrow window dropped, and the stacked pane, which is the whole +screen, never said how to leave it. `ask here` itself opens straight into the stacked pane, because it selects the new row and gives it the keyboard. Everything answers the same keys it answers on a wide frame. @@ -240,14 +260,6 @@ A click on one of the card's answers in the pane answers the card, the same way answers it in a conversation. Each answer owns its whole row, so there is no gap between two of them to miss. -**There is a chord for it and the foot does not name it.** `ctrl+enter` is still bound to -`ask here`, and it only reaches codeaf on a terminal that can tell it apart from a plain -`enter` — the kitty keyboard protocol, Windows terminals. `alt+enter` is **not** a second -spelling of it: on home as on every place, that chord opens the composer layer and sends -what you typed off as a **task** (the places page). So the arrow is the gesture the foot -names, because the arrow is the one every terminal has. This page used to say `alt+enter` was -bound to the same thing, and it was not. - If this window was launched with no way to open a second session, the row refuses in one line: `this window cannot ask from home`, and nothing is created. The window that gets that line is one attached to another machine — `--host` or `--at`: the errand's folder and the @@ -256,6 +268,22 @@ them on a server's disk. An ordinary `codeaf` in a folder asks from home whether project's engine is holding the conversation, because that engine is a process on the same machine as the folder. +## Is there a shortcut for ask here — ctrl+enter, and what alt+enter does on home + +**`ctrl+enter` asks what you typed here without walking to the row**, and the foot does not +name it: it only reaches codeaf on a terminal that can tell it apart from a plain `enter` — +the kitty keyboard protocol, Windows terminals. Elsewhere it usually arrives as a plain +`enter`, which on the typing rows starts a new conversation. So the gesture the foot names +is `↑ ask here`, because every terminal has the arrow. + +**`alt+enter` is not a second spelling of it.** On home it opens the **composer layer**, +where you check the project, the model the work runs on and the spending cap; a second +`alt+enter` sends it off as a task, and the task lands as the same kind of `?` row with its +pane, carrying those three settings (the places page). On every other place the chord does +nothing. This page once said `alt+enter` was bound to `ask here` itself, and home's own key +handler listed it beside `ctrl+enter` — but the layer always took the key first, so that +line never ran, and it is gone. + ## What is ask here — and how is it different from starting a conversation They send the same words to the same kind of model. The difference is **what is left behind diff --git a/internal/manual/chat/commands.md b/internal/manual/chat/commands.md index 540c986564..ceb04c4e2f 100644 --- a/internal/manual/chat/commands.md +++ b/internal/manual/chat/commands.md @@ -1299,7 +1299,7 @@ box, opens it on a one-conversation machine and on an empty one alike, and over it opens the far machine's. There is no argument form. There are three other ways in: **`alt+1`**, home being the first -of the six words on the tab bar; **`space` twice** on an empty box; and **`tab`** from any +of the seven words on the tab bar; **`space` twice** on an empty box; and **`tab`** from any other place. **It is seven panels**, in one column under 110 cells, two from 110 and three from 170, diff --git a/internal/manual/chat/home.md b/internal/manual/chat/home.md index c2f2f4714f..69c475eacb 100644 --- a/internal/manual/chat/home.md +++ b/internal/manual/chat/home.md @@ -517,7 +517,7 @@ own tab stack — so going back is `enter`. A window that has held only one conv **On a launch** — home greeting you — the cursor is on the conversation this window is holding, the row `esc` drops back into. -**`↑` off the top row of home stays on it.** The tab bar, the row of six words, is +**`↑` off the top row of home stays on it.** The tab bar, the row of seven words, is reached by clicking a word, by `tab`, or by a place's own chord (`alt+4` and the rest); on every other place `↑` off the top row still walks up onto it. On the bar `←` and `→` walk along the words without opening anything, `enter` or `↓` goes into the one under the @@ -642,17 +642,20 @@ another window asks can take up to ten seconds to reach a chat's top line. **The money on this line is the money on the spend place and the `spend` panel**, to the cent — one reading of one file, wherever you are standing. -## Where did standing and memory go: the six words on the tab bar +## Where did standing and memory go: the seven words on the tab bar -The top line reads `home chats AI teams activity memory spend settings`. `alt+1` through -`alt+6` go to each; `chats` (`alt+3`) returns to your conversation. +The top line reads `home chats AI teams activity memory spend settings` — memory is the +fifth word, and standing is the one place not on it. **The numbers are not the bar's order**: +each place kept the number it had before the bar was regrouped — `alt+1` home, `alt+2` +AI teams, `alt+3` chats (back to your conversation), `alt+4` activity, `alt+5` spend, +`alt+6` settings, `alt+7` standing, `alt+8` memory. -- `/standing` (or `/orders`) and `alt+7` open Standing. -- `/memory` (or `/memories`) and `alt+8` open Memory. +- `/standing` (or `/orders`) and `alt+7` open Standing. Its word appears after the seven + while it is open, and `tab` from there returns to Home. +- `/memory` (or `/memories`), `alt+8` and the word `memory` on the bar open Memory. -Their words appear after the six while open. `tab` returns to Home; `alt+.` shows -all eight numbered destinations. Search is on Home: type into its box to find a -conversation. There is no separate Search tab or `/search` command. +`alt+.` shows all eight numbered destinations. Search is on Home: type into its box to find +a conversation. There is no separate Search tab or `/search` command. ## Why did a dashboard open when I started codeaf — home greets you @@ -1627,8 +1630,9 @@ this folder as the first row of `projects`. ## Is there a key for home? Three of them. **`alt+1`** goes straight there from anywhere — home is the first of the -six words on the tab bar, and each answers to its own position, `alt+1` through `alt+6` -(standing and memory, the two places off the bar, answer `alt+7` and `alt+8`). +seven words on the tab bar. The other numbers do not follow the bar: each still opens the +place it opened before the bar was regrouped, so memory, the fifth word, is `alt+8`, and +standing, which is off the bar, is `alt+7`; `alt+.` draws every place with its number. **Space twice on an empty box** goes there from inside a conversation, and **`tab`** walks to it from any other place. `/home` opens it too. diff --git a/internal/manual/chat/keys.md b/internal/manual/chat/keys.md index d6994dc775..688f28cb8d 100644 --- a/internal/manual/chat/keys.md +++ b/internal/manual/chat/keys.md @@ -2331,13 +2331,15 @@ to filter, `↑↓` to walk, `enter` to use it, `esc` to go back to the layer. **Press the space bar twice with an empty message box.** That is the way back to home from inside a conversation, and `/home` opens it too. -**There is also a number: `alt+1` (`opt+1` on a Mac).** Home is the first of the seven main destinations on -the top line, `home chats AI teams activity memory spend settings`, and each answers to its position there, -`alt+1` through `alt+6`. **`alt+7` and `alt+8` are kept**, on the two places that -are off the bar — standing and memory — so those keys still open a room rather than -doing nothing; `alt+.` draws them all with their numbers. `alt+2` is the teams page, and -`alt+3` is `chats`, the way back to the conversation in front (a new chat when none is open); -it is not a room, so `tab` steps over it. Hold +**There is also a number: `alt+1` (`opt+1` on a Mac).** Home is the first of the seven words on +the top line, `home chats AI teams activity memory spend settings`, and `alt+1` opens it. +**The other numbers do not follow the bar's order** — they kept the destinations they had +before the bar was regrouped: `alt+2` AI teams, `alt+3` chats, `alt+4` activity, `alt+5` +spend, `alt+6` settings, `alt+7` standing, `alt+8` memory. So memory is the fifth word on the +bar and `alt+8`, and standing, which is off the bar, is `alt+7`. `alt+.` draws every place +with its number, and `/help` lists them the same way, digit by digit. `alt+3` is `chats`, +the way back to the conversation in front (a new chat when none is open); it is not a room, +so `tab` steps over it. Hold `alt` and press the digit. On macOS codeaf draws the modifier as `opt+`, after the name on that keycap; it is the same key and the same chord, and on Linux and on Windows it is drawn `alt+`. It arrives in every terminal codeaf runs in, which is why the numbers are on `alt` @@ -2422,6 +2424,8 @@ what each holds), and its keys are a small grammar: | `enter` | acts on the row under the cursor: a conversation opens, a `since you left` line opens its record, file or place, a `standing` row opens standing, a fold line opens or shuts its panel. `projects` and `spend` rows are not stops, so the cursor never reaches them; a click on a project picks the folder for the next message | | `pgup` / `pgdown` | jump a screenful | | `tab` | **the next place** on the bar | +| `alt+enter` | with something typed: opens the **composer layer**, and a second press sends it off as a task (*Keys in the composer layer*). It is not a spelling of `ask here` | +| `ctrl+enter` | with something typed: asks it here at once, without walking to the `ask here` row — only on a terminal that can tell it from a plain `enter`, so the foot names `↑ ask here` instead | | `esc` | clears the box if anything is in it, and closes home otherwise | | `alt+.` | the map | | `backspace`, `ctrl+u`, `ctrl+w`, `ctrl+b`, `ctrl+f` | edit the box | @@ -2515,20 +2519,25 @@ one `↑` reaches `ask here` and a second lands on the strongest match. Clearing the panels back. On a frame 136 columns or wider a card about the match under the cursor stands to the right of the list while you type; at rest there is no card. -**On an `ask here` row** — the `?` rows an errand leaves at the end of the conversation list — -the line under the box reads -`↑↓ move · enter or tab answer this ask here · esc close`. `enter` or `tab` hands the -keyboard to the exchange's pane, where it reads `enter sends a follow-up · tab or esc back to the list`; -`esc` or `tab` hands it back (*Asking from home*). - Home is modal like the panels above: while it is up, every chord except `ctrl+c` belongs to it. `ctrl+c` does not close home — it quits codeaf, with home still up. +## Keys on an ask here row — enter or → go in, and tab is the next place + +**On an `ask here` row** — the `?` row an errand leaves on home — the line under the box +reads `↑↓ move · enter or → answer this ask here · esc close`, or +`↑↓ move · enter answer this ask here · esc clear` while something is typed, when `→` moves +the caret instead. `enter` or `→` hands the keyboard to the exchange's pane; **`tab` on the +row is the next place**, not a way in. The pane reads +`enter sends a follow-up · tab or esc back to the list`, and `tab` or `esc` there hands the +keyboard back. The way back is that line's last clause and the one it keeps at sixty +columns (*Asking from home*). + ## The tab bar is a row the cursor can stand on — ↑ off the top row, and ←/→ along the words **On every place, `↑` from the first row of the page lands the cursor on the tab bar**, -the six place words on the top line after the `codeaf` wordmark (seven while you stand in -standing or memory, whose word is drawn after the six). The chat tab strip is +the seven words on the top line after the `codeaf` wordmark (eight while you stand in +standing, whose word is drawn after the seven). The chat tab strip is not on a place. It is the row under that top line only while a conversation is in front. The word you are standing in wears the cursor's band there instead of its usual mark, and five keys mean something on that row: diff --git a/internal/manual/chat/places.md b/internal/manual/chat/places.md index 35ba2f4c60..1d0b130f77 100644 --- a/internal/manual/chat/places.md +++ b/internal/manual/chat/places.md @@ -623,7 +623,7 @@ The standing orders page has the whole of it. ## memory — what is held true What codeaf holds true about you and this machine, with what kind of thing each line is, how -it has done, `helped 19 · bore on 3`, and how old it is out at the right. `/memory` and `/memories` open it, and so does `alt+8`. It is not on the tab bar. +it has done, `helped 19 · bore on 3`, and how old it is out at the right. `/memory` and `/memories` open it, and so do `alt+8` and `memory`, the fifth word on the tab bar — the numbers kept their old places, so the fifth word is not `alt+5`. The page is **shelves** — you, this project, this machine — biggest first, with the biggest one open and the rest rolled up. Type to filter what is already on the page, **every letter @@ -754,8 +754,9 @@ anywhere off the menu, puts it away and leaves everything else as it was. **The wordmark and the place you are standing in never fold**, and neither does the word the bar's cursor is on or a place wearing a count. `alt+1`…`alt+8` still go straight to a place -whether or not its word is on the row, and the numbers never move: the six on the bar are -`alt+1`…`alt+6`, then standing and memory are `alt+7`…`alt+8`. +whether or not its word is on the row, and the numbers never move — nor do they follow the +bar's order: home `alt+1`, AI teams `alt+2`, chats `alt+3`, activity `alt+4`, spend `alt+5`, +settings `alt+6`, standing `alt+7`, memory `alt+8`. The tab strip, drawn only inside a chat, keeps its own narrowing: long names are cut at a word, the names shrink until every tab fits, then the strip scrolls with `‹` `›`, and `+3` counts diff --git a/internal/manual/chat/screen.md b/internal/manual/chat/screen.md index 311a05d040..908488179d 100644 --- a/internal/manual/chat/screen.md +++ b/internal/manual/chat/screen.md @@ -47,11 +47,12 @@ column closed the conversation is laid out at the full width of the terminal, ru still draws the strip along the top, and the keys row under the box reads `alt+l tasks` once the session has tasks to come back to and no running-turn line owns that row. -**Seven places take the whole frame instead of sharing it**, at every width: home, teams, -sessions, standing, memory, spend and settings. Five are on the tab bar with the way back to the chats, `home teams chats -sessions spend settings`, and `tab` walks the five rooms; `alt+1` … `alt+8` (`opt+1` … `opt+8` on a Mac) jump -straight to any of those seven or back to the chats from wherever you are standing, a place or a conversation, -and each +**Seven places take the whole frame instead of sharing it**, at every width: home, AI teams, +activity, standing, memory, spend and settings. Six are on the tab bar with the way back to the chats, +`home chats AI teams activity memory spend settings`, and `tab` walks those six rooms; standing is +off the bar. `alt+1` … `alt+8` (`opt+1` … `opt+8` on a Mac) jump straight to any of those seven or back to +the chats from wherever you are standing, a place or a conversation — by number and not by position on +the bar, so memory, the fifth word, is `alt+8` — and each has commands of its own (`/home`, `/teams`, `/history`, `/standing`, `/memory`, `/settings`). The rewind timeline (`/rewind`) takes the frame the same way and is deliberately not one of the seven: it is something you do to this conversation rather than diff --git a/internal/manual/chat/what-i-remember.md b/internal/manual/chat/what-i-remember.md index 6dfb0cfbf7..b0c2dfbc08 100644 --- a/internal/manual/chat/what-i-remember.md +++ b/internal/manual/chat/what-i-remember.md @@ -204,7 +204,7 @@ they are gone; nothing else keeps a second copy. ## How do I see what codeaf remembers about me? -Open `/memory`, or `/memories`, or `alt+8` from anywhere (memory is not on the tab bar). **Memory is a +Open `/memory`, or `/memories`, or `alt+8` from anywhere, or click `memory`, the fifth word on the tab bar (its number is 8 all the same). **Memory is a place**, one of seven, taking the whole screen with the tab bar above it and a composer at the foot. diff --git a/internal/manual/chat_test.go b/internal/manual/chat_test.go index 04b2cdcb9a..fe0926f534 100644 --- a/internal/manual/chat_test.go +++ b/internal/manual/chat_test.go @@ -2530,6 +2530,14 @@ func TestTheChatManualAnswersTheQuestionsPeopleAsk(t *testing.T) { {"why is the answer from home showing asterisks and hashes", "asking-from-home"}, {"does the ask here pane format the answer", "asking-from-home"}, {"can I ask two things from home at once", "asking-from-home"}, + // The wave that made home's hints name only keys that do what they say. + // The pages said `alt+enter` was ask here's second chord, the row's foot + // said `enter or tab` when `tab` was the next place, and a narrow pane + // dropped the way back to the list first. + {"is there a shortcut for ask here", "asking-from-home"}, + {"does alt+enter ask here", "asking-from-home"}, + {"why does tab leave home from an ask here row", "keys"}, + {"how do I leave the ask here pane on a narrow window", "asking-from-home"}, {"why did it set my reminder for a time that already passed", "keeping-an-eye"}, {"why is there no once on my reminder card", "keeping-an-eye"}, // The e2e suite found the firing that reached a conversation and was @@ -2669,6 +2677,10 @@ func TestTheChatManualAnswersTheQuestionsPeopleAsk(t *testing.T) { {"how do I expand a panel on home to see the rest", "home"}, {"where did the card on home go", "home"}, {"where did standing and memory go from the tab bar", "home"}, + // /help said the numbers came "in the tab bar's own order", and memory is + // the fifth word on the bar and `alt+8`. + {"why is memory alt+8 when it is fifth on the tab bar", "home"}, + {"do the alt numbers follow the tab bar", "home"}, {"can I stop another window task from home", "home"}, {"is there a something is wrong panel", "home"}, // The wave that stopped guessing at the terminal's background and asked diff --git a/internal/tui3/commands.go b/internal/tui3/commands.go index cf9a4835e8..57e67df4ff 100644 --- a/internal/tui3/commands.go +++ b/internal/tui3/commands.go @@ -1032,7 +1032,13 @@ func helpText(file string, chords chordSpelling) string { // The three rows are spelled through [chordSpelling.say] like the // `alt+enter` row above them, so a Mac reads `opt+1…opt+8` and a Linux box // reads what is authored here — one substitution, one door (chords.go). - helpKeyRow(chords.say(chordJumpWords), "go to a place · in the tab bar's own order: "+placeWordList()), + // + // THE DIGITS ARE NAMED ONE BY ONE, AND NOT AS THE BAR'S ORDER. This row + // said "in the tab bar's own order" over a list in [placeOrder]'s order, + // and the two have differed since the bar was regrouped: memory is fifth + // on the bar and `alt+8` ([placeWordList] says why each word carries its + // number). + helpKeyRow(chords.say(chordJumpWords), "go to a place · "+placeWordList()), helpKeyRow(chords.say(placeMapKey), "on a place: what else is here · every key that place has, drawn"), " on a place, tab is the next place · esc back", // THE CHORD IS SPELLED FOR THIS TERMINAL AND THEN PADDED, in that order, diff --git a/internal/tui3/helpdoors_test.go b/internal/tui3/helpdoors_test.go index d45b47fe32..d2e3ebd5e4 100644 --- a/internal/tui3/helpdoors_test.go +++ b/internal/tui3/helpdoors_test.go @@ -24,12 +24,24 @@ func TestTheKeySheetNamesTheWayToEveryPlace(t *testing.T) { t.Errorf("/help never names %q — a person who reads the whole sheet still does not know the places exist:\n%s", want, sheet) } } - // AND IT SAYS WHICH DIGIT IS WHICH, in the tab bar's own order and read off - // [placeOrder] rather than typed out a second time. + // AND IT SAYS WHICH DIGIT IS WHICH, read off [placeOrder] rather than typed + // out a second time. if !strings.Contains(sheet, placeWordList()) { t.Errorf("/help names %s but never says which place each digit opens (want the list %q):\n%s", chordJumpWords, placeWordList(), sheet) } + // DIGIT BY DIGIT, AND NEVER AS THE BAR'S ORDER. The sheet said the list was + // "in the tab bar's own order", and since [placeBarOrder] regrouped the bar + // it is not: memory is fifth on the bar and `alt+8`. So every place is named + // beside the digit that reaches it, and the claim is gone. + for _, id := range placeOrder { + if want := itoa(placeDigitOf(id)) + " " + id.word(); !strings.Contains(sheet, want) { + t.Errorf("/help never pairs %s's digit with its word (want %q):\n%s", id.word(), want, sheet) + } + } + if strings.Contains(sheet, "bar's own order") { + t.Errorf("/help still says the digits follow the tab bar, which they do not:\n%s", sheet) + } // AND WHAT `tab` MEANS ONCE YOU ARE ON ONE. The sheet's own `tab` row is // about the last conversation, which is true where it is read and says // nothing about the seven. diff --git a/internal/tui3/home.go b/internal/tui3/home.go index 4bf8352be4..ce5e0515ff 100644 --- a/internal/tui3/home.go +++ b/internal/tui3/home.go @@ -2691,14 +2691,20 @@ func (a *app) homeKey(msg tea.KeyPressMsg) tea.Cmd { case "enter": return a.homeEnter() - case "ctrl+enter", "alt+enter": - // `ask here` WITHOUT LEAVING THE BOX. Two spellings because terminals - // disagree about which one they can send — the same law input.go states - // for alt+enter and ctrl+j — and ctrl+enter reaches this switch only on a - // terminal that can distinguish it from a plain enter at all (the kitty - // protocol, win32-input). alt+enter is the one that survives everywhere, - // and the hint line names ctrl+enter because it is the one a hand - // reaches for. + case "ctrl+enter": + // `ask here` WITHOUT LEAVING THE BOX, on a terminal that can tell + // ctrl+enter from a plain enter at all (the kitty protocol, win32-input). + // The foot does not name it, because most terminals cannot send it; the + // `↑` onto the row is the gesture every terminal has (homeHintWords). + // + // `alt+enter` IS NOT A SECOND SPELLING OF IT, AND IT USED TO BE LISTED + // HERE AS ONE. The router above takes that chord on every place + // (placekeys.go) — on home it opens the composer layer, whose second + // press sends the sentence through this same errand door with a project, + // a model and a cap settled (composerlayer.go's [app.composerSend]) — so + // an `alt+enter` arm in this switch was a branch no key could reach. Home + // is the only place that layer opens; a home that took the chord back + // would leave the layer reachable from nowhere. return a.askHere(strings.TrimSpace(h.box.String())) // ── THE CARD'S OWN KEYS ARE CHORDS ───────────────────────────────────── @@ -3224,8 +3230,9 @@ func (a *app) homeEnter() tea.Cmd { // ENTER ON AN ERRAND HANDS IT THE KEYBOARD. There is nothing to open — // the exchange is already drawn beside the row, or on a narrow frame is // about to take the whole screen — so the one thing enter can mean here - // is "I am talking to this one now", which is what tab means from the - // same row and what a second click on it means. + // is "I am talking to this one now", which is what `→` means from the + // same row and what a second click on it means. (`tab` meant it once; + // it is the next place now, on home as on every place.) if line.ex != nil { line.ex.focused = true } @@ -5601,12 +5608,24 @@ func withChords(hint, chords string) string { if chords == "" { return hint } - if at := strings.LastIndex(hint, railSep); at >= 0 && strings.HasPrefix(hint[at+len(railSep):], "esc") { + if at := strings.LastIndex(hint, railSep); at >= 0 && footWayOut(hint[at+len(railSep):]) { return hint[:at] + railSep + chords + hint[at:] } return dotted(hint, chords) } +// footWayOut reports whether one clause is a foot's way out: a clause that +// begins with `esc`, or the errand pane's [exchangeBackWord], whose way out is +// two keys and names the zone toggle first. +// +// IT HAS TO KNOW THE SECOND SHAPE. Read as "begins with esc" alone, the pane's +// way out was not one, so the chords went on after it, [hintFit] protected the +// last of them, and the way back to the list was the first clause a narrow +// frame gave up. +func footWayOut(clause string) bool { + return strings.HasPrefix(clause, "esc") || clause == exchangeBackWord +} + // homeVerbsWord is how the CARD advertises the strip. It names the key and the // noun, in the hint slot's own grammar (render.go's [app.hintWord]), and never // the letters themselves — those are drawn on the strip and nowhere else, which @@ -5633,7 +5652,17 @@ func (a *app) homeHintWords() string { // standing beside the column with no line saying how to reach it is the // half of the toggle nobody finds; the list's own verbs come first, // because that is the zone the hand is in. - return "↑↓ move · enter or tab answer this " + homeAskHereWord + " · esc close" + // + // AND IT NAMES ONLY THE KEYS THAT GO IN. It said `enter or tab` long + // after `tab` became the next place on every place (placekeys.go), so + // the line sent a person who believed it off home altogether. `→` is + // the way in that replaced it ([app.homeKey]'s `right` arm), and only + // over an empty box: with a draft the arrow is the caret's, and `esc` + // clears the draft before it closes anything. + if !a.home.box.empty() { + return "↑↓ move · enter answer this " + homeAskHereWord + " · esc clear" + } + return "↑↓ move · enter or → answer this " + homeAskHereWord + " · esc close" } line, _ := a.home.focusedLine() switch { diff --git a/internal/tui3/homedraft.go b/internal/tui3/homedraft.go index 7051e55893..f3e89e91d1 100644 --- a/internal/tui3/homedraft.go +++ b/internal/tui3/homedraft.go @@ -181,7 +181,8 @@ const ( // targetChordWords is the draft's chords as a clause on home's foot // (footswap.go: the lowest line is for keys): the folder chord where there is // somewhere to walk to, approvals where the control can act, the switcher -// where there is anywhere to go, and `/ commands` while the box is empty. +// where there is anywhere to go, and `/ commands` while home's own box is empty +// and has the keyboard. // Nothing while the model list // is up — it has the whole keyboard (SCREEN 3a's clause: no key does anything // that is not drawn on screen right now), and the foot is already saying the @@ -215,7 +216,11 @@ func (a *app) targetChordWords() string { // looking at is furniture (render.go's [microcopy] holds the original). It // is home's clause alone: on the other places a `/` is a character in the // box and opens no list (homeslash.go is home's). - if a.at(pageHome) && a.home.box.empty() { + // + // AND IT IS HOME'S OWN BOX'S CLAUSE, so it goes while an errand's pane holds + // the keyboard. The pane types into a box of its own ([placeHome.box]), where + // a `/` is the first character of a follow-up and no list opens over it. + if a.at(pageHome) && a.placeBox() == &a.home.box && a.home.box.empty() { right = dotted(right, microcopy) } return right diff --git a/internal/tui3/homeexchange.go b/internal/tui3/homeexchange.go index 479bbc174c..7e9e47a6d3 100644 --- a/internal/tui3/homeexchange.go +++ b/internal/tui3/homeexchange.go @@ -16,9 +16,10 @@ package tui3 // // - A second action row, `ask here`, drawn directly above // `start a new conversation` while something is typed. It is one ↑ away and -// it is also a chord — ctrl+enter, with alt+enter as the spelling terminals -// that cannot send the first one do send (input.go's alt+enter/ctrl+j pair -// is the same law about the same key). +// it is also a chord — ctrl+enter, on the terminals that can send it. It +// has no second spelling: `alt+enter` is the router's on every place, and +// on home it opens the composer layer, whose second press comes through +// this file's door with the three facts settled ([app.askHereWith]). // - A REAL session.Agent behind it, built through the door's own seam // ([Options.Errand]) with its transcript in a folder under the standing // root rather than under v3/projects — so home never lists it, and the @@ -117,6 +118,11 @@ const ( homeAskStoodWord = "kept · this exchange is filed under it" // homeAskPromotedWord is the refusal for a second promotion of one folder. homeAskPromotedWord = "this exchange is already a conversation" + // exchangeBackWord is the pane's way out, the last clause of its foot + // ([exchangeHint]). It is named because two readers have to recognise it: + // the foot that writes it, and home's [withChords], which puts the draft's + // chords in front of a foot's way out rather than behind it. + exchangeBackWord = "tab or esc back to the list" ) // What an exchange's ROW says it is doing, and what the pane says while a turn @@ -276,10 +282,11 @@ type homeExchange struct { chips []chip // focused says the keyboard belongs to the exchange rather than to the // list. THERE ARE TWO ZONES WHILE AN EXCHANGE IS UP — the list and this pane - // — and this bool is which of them has the hand. `tab` toggles it, esc hands - // it to the list, a click on a row takes it to the list and a click in the - // pane brings it back, and a yes on the card gives it to the list by itself - // (home.go's [app.homeKey] states the whole model). + // — and this bool is which of them has the hand. `tab` or esc in the pane + // hands it to the list and `enter` or `→` on the row hands it back (`tab` + // on the row is the next place), a click on a row takes it to the list and + // a click in the pane brings it back, and a yes on the card gives it to the + // list by itself (home.go's [app.homeKey] states the whole model). focused bool // onOffer says the cursor inside the pane is on `continue as a // conversation` rather than in the box. ↓ puts it there and ↑ takes it back, @@ -681,7 +688,7 @@ func (a *app) answerExchangeCard(ex *homeExchange, notice *session.StandingNotic // AND THE KEYBOARD GOES BACK TO THE LIST on a yes and on a no alike. The // question is over either way, and a hand left in a pane with nothing // left to answer is how the arrows stop moving the column. The exchange - // stays alive beside it — tab or a click brings it back for a follow-up. + // stays alive beside it — enter, → or a click brings it back for a follow-up. ex.focused, ex.onOffer = false, false } return sent @@ -836,7 +843,7 @@ func exchangeStandWords(args string) string { // // AND THE KEYBOARD GOES BACK TO THE LIST. The thing they asked for now exists; // the list is where a person goes next, and the exchange stays alive beside it -// for a follow-up that tab or a click reaches. +// for a follow-up that enter, → or a click on its row reaches. func (a *app) errandUpdated(ex *homeExchange, notice session.StandingNotice) tea.Cmd { if text := strings.TrimSpace(notice.Text); text != "" { ex.rows = append(ex.rows, exchangeRow{kind: exchangeNote, text: text}) @@ -2205,6 +2212,13 @@ func exchangeHint(ex *homeExchange) string { // half of the way back to the list. On a narrow frame this pane is the whole // screen and the same two keys are how the list comes back, which is why the // sentence says "the list" rather than "the column". - parts = append(parts, "tab or esc back to the list") + // + // AND IT IS THE LAST CLAUSE, WHICH IS WHAT KEEPS IT ON A NARROW FRAME. + // [hintFit] protects a foot's last clause and gives up the ones in front of + // it, so the answers and the follow-up go before this does. It used to lose: + // home put its draft chords AFTER it, because [withChords] only knew a way + // out that begins with `esc` — and at sixty columns, where this pane is the + // whole screen, the foot kept `/ commands` and never said how to leave. + parts = append(parts, exchangeBackWord) return strings.Join(parts, " · ") } diff --git a/internal/tui3/homeexchange_test.go b/internal/tui3/homeexchange_test.go index afe66f9dcb..fd1a38530e 100644 --- a/internal/tui3/homeexchange_test.go +++ b/internal/tui3/homeexchange_test.go @@ -299,17 +299,21 @@ func TestAskHereMakesItsFolderOutsideTheProjectsAndSendsTheSentence(t *testing.T } } -// TestTheChordAsksHereWithoutWalkingToTheRow pins the second way in. Both -// spellings are bound because terminals disagree about which one they can send -// (home.go's ctrl+enter case says so); alt+enter is the one every terminal here -// delivers, so it is the one the test presses. +// TestTheChordAsksHereWithoutWalkingToTheRow pins the two chords that reach the +// errand door without the cursor ever leaving the row it was on, and which of +// them is home's own. // -// IT IS TWO PRESSES NOW AND THE LAW IT PINS IS UNCHANGED: the chord reaches the -// errand door without the cursor ever leaving the row it was on. The first press -// opens the composer layer, where the three facts a task needs are settled -// (composerlayer.go, SCREEN 2e); the second is the send. What would break this -// test is the chord walking somebody onto the `ask here` row to work — which is -// exactly what it never did and still never does. +// `alt+enter` IS THE ROUTER'S, on home as on every place (placekeys.go). Its +// first press opens the composer layer, where the three facts a task needs are +// settled (composerlayer.go, SCREEN 2e), and asks nothing; its second is the +// send, through the same door `ask here` uses. home.go used to list it beside +// ctrl+enter as a second spelling of `ask here` in its own switch, an arm no key +// could reach because the router takes the chord first — so the first press is +// asserted to have asked NOTHING, which is what that arm would have done. +// +// `ctrl+enter` IS HOME'S, and it asks at once. It reaches home only on a +// terminal that can tell it from a plain enter, and where it does it must mean +// this and not the layer. func TestTheChordAsksHereWithoutWalkingToTheRow(t *testing.T) { lab := newErrandLab(t) mine := lab.session("-tmp-alpha", "aaaa000000000001", "pricing research", "/tmp/alpha", time.Now()) @@ -321,21 +325,25 @@ func TestTheChordAsksHereWithoutWalkingToTheRow(t *testing.T) { if !a.composerShowing() { t.Fatal("the first alt+enter opened no composer layer") } + if len(lab.agent.sent) != 0 || theExchange(a) != nil { + t.Fatalf("the first alt+enter asked here instead of opening the layer, the agent saw %v", lab.agent.sent) + } drive(t, a, tea.KeyPressMsg{Code: tea.KeyEnter, Mod: tea.ModAlt}) if len(lab.agent.sent) != 1 { - t.Fatalf("alt+enter should have asked here, the agent saw %v", lab.agent.sent) + t.Fatalf("the layer's second alt+enter should have asked here, the agent saw %v", lab.agent.sent) } if theExchange(a) == nil { t.Fatal("alt+enter left no exchange in the pane") } - // AND ctrl+enter IS THE SAME DOOR. It reaches the router only on a terminal - // that can tell it from a plain enter, and where it does it must mean this. second := lab.app(mine, []session.Event{{Kind: session.EventTurnDone}}) second.openHome() typeHome(second, "tell me when CI goes red") drive(t, second, tea.KeyPressMsg{Code: tea.KeyEnter, Mod: tea.ModCtrl}) - if len(lab.agent.sent) != 1 { + if second.composerShowing() { + t.Fatal("ctrl+enter opened the composer layer; it is home's own `ask here` chord") + } + if len(lab.agent.sent) != 1 || theExchange(second) == nil { t.Fatalf("ctrl+enter should have asked here, the agent saw %v", lab.agent.sent) } } @@ -779,7 +787,7 @@ func TestEscLeavesTheExchangeAliveAndTheListMoving(t *testing.T) { if theExchange(a).focused { t.Fatal("esc left the keyboard in the pane") } - // AND ITS ROW IS STILL ON HOME, where enter or tab hands it the keyboard + // AND ITS ROW IS STILL ON HOME, where enter or → hands it the keyboard // again — the grid puts the list back rather than a pane beside it. if exchangeRowAt(a, theExchange(a)) < 0 { t.Fatalf("esc took the exchange's row off home:\n%s", homeText(a)) @@ -874,6 +882,77 @@ func TestTabTogglesTheListAndThePaneAtHome(t *testing.T) { } } +// TestTheErrandRowFootNamesOnlyTheKeysThatGoIn pins the line under the box +// while the cursor stands on an errand's row and the list has the keyboard. +// +// IT SAID `enter or tab answer this ask here` for as long as `tab` had been the +// next place on every place (placekeys.go), so the one key it named besides +// enter walked a person off home. Every key the line names is pressed here and +// does what the line says, over an empty box and over a draft — and `tab` is +// pressed last, to show why the line may not name it. +func TestTheErrandRowFootNamesOnlyTheKeysThatGoIn(t *testing.T) { + _, a := exchangeLab(t) + ex := theExchange(a) + foot := func() string { + rows := errandRows(a) + return rows[len(rows)-1] + } + drive(t, a, key("esc")) + if ex.focused || a.paneExchange() != ex { + t.Fatal("esc should leave the cursor on the errand's row with the keyboard on the list") + } + if line := foot(); !strings.Contains(line, "enter or → answer this "+homeAskHereWord+" · ") || + strings.Contains(line, "or tab") { + t.Fatalf("over an empty box the row's foot should name enter and →, and never tab:\n%s", line) + } + drive(t, a, key("right")) + if !ex.focused || !a.at(pageHome) { + t.Fatalf("→ on the row did not hand the pane the keyboard (focused %v, on %q)", ex.focused, a.page.word()) + } + drive(t, a, key("tab"), key("enter")) + if !ex.focused { + t.Fatal("enter on the row did not hand the pane the keyboard") + } + + // OVER A DRAFT THE ARROW IS THE CARET'S AND esc CLEARS FIRST, so the line + // names enter alone and says what esc will do to the draft. The words have + // to match a conversation for the drop-up to draw a block for the errand's + // row to stand in. + // The row is walked onto with ↑, as a person reaches it: a drop-up keeps a + // cursor through a rebuild only on a row somebody chose ([homeView.build]). + drive(t, a, key("tab")) + typeHome(a, "pricing") + for step := 0; step < len(a.home.lines) && a.paneExchange() != ex; step++ { + drive(t, a, key("up")) + } + if a.paneExchange() != ex { + t.Fatalf("↑ never reached the errand's row on the drop-up:\n%s", homeText(a)) + } + if line := foot(); !strings.Contains(line, "enter answer this "+homeAskHereWord+" · ") || + !strings.Contains(line, "esc clear") || strings.Contains(line, "→") { + t.Fatalf("over a draft the row's foot should name enter alone and esc clear:\n%s", line) + } + drive(t, a, key("right")) + if ex.focused { + t.Fatal("→ over a draft took the pane instead of moving the caret") + } + drive(t, a, key("enter")) + if !ex.focused { + t.Fatal("enter over a draft did not hand the pane the keyboard") + } + + // AND `tab` ON THE ROW IS THE NEXT PLACE. Inside the pane it hands the + // keyboard back, which the pane's own foot says; on the row it leaves home. + drive(t, a, key("tab")) + if ex.focused || !a.at(pageHome) { + t.Fatalf("tab in the pane should hand the keyboard to the list (focused %v, on %q)", ex.focused, a.page.word()) + } + drive(t, a, key("tab")) + if ex.focused || a.at(pageHome) { + t.Fatalf("tab on the row should be the next place (focused %v, on %q)", ex.focused, a.page.word()) + } +} + // TestTheListWalksWhileAnExchangeIsAliveAndTheRowKeepsIt is the complaint in // one test: with the keyboard on the column every walking key moves the column, // and the exchange keeps its own row on it the whole time. @@ -1658,3 +1737,64 @@ func TestANarrowWindowStacksTheExchangeOverTheList(t *testing.T) { t.Fatalf("the card could not be answered on a narrow frame, the agent saw %v", lab.agent.answered) } } + +// TestThePaneKeepsItsWayBackAtEveryWidth pins the last clause of the pane's +// foot. On a narrow frame the pane IS the screen, so the way back to the list +// is the one clause it may never give up — and at sixty columns it was the +// first one it gave up: home put its draft chords AFTER the way back, and +// [hintFit], which protects a foot's last clause, kept `/ commands` and dropped +// `tab or esc back to the list`. A card or the `continue` row lost it the same +// way at every width up to 140. +// +// `/ commands` IS NOT NAMED IN THE PANE AT ALL. The pane types into a box of its +// own, where a `/` is the first character of a follow-up and opens no list. +func TestThePaneKeepsItsWayBackAtEveryWidth(t *testing.T) { + panes := []struct { + what string + turns [][]session.Event + check func(*homeExchange) bool + }{ + {"before any reply", nil, func(ex *homeExchange) bool { return !ex.offering() && !ex.asking() }}, + {"with a reply and the continue row", [][]session.Event{{ + text(session.EventTextDelta, "I will remind you at 6."), + {Kind: session.EventTurnDone}, + }}, func(ex *homeExchange) bool { return ex.offering() }}, + {"with a card up", [][]session.Event{{ + text(session.EventTextDelta, "Here is what I will do."), + standingProposal(7, "remind me at 6 to leave"), + {Kind: session.EventTurnDone}, + }}, func(ex *homeExchange) bool { return ex.asking() }}, + } + for _, width := range []int{60, 72, 80, 100, 140, 200} { + for _, pane := range panes { + lab := newErrandLab(t) + mine := lab.session("-tmp-alpha", "aaaa000000000001", "pricing research", "/tmp/alpha", time.Now()) + a := lab.app(mine, pane.turns...) + a.width, a.height = width, 30 + a.openHome() + typeHome(a, "remind me at 6 to leave") + drive(t, a, key("up"), key("enter")) + ex := theExchange(a) + if ex == nil || !ex.focused || !pane.check(ex) { + t.Fatalf("at %d columns the pane %s never held the keyboard", width, pane.what) + } + rows := errandRows(a) + foot := rows[len(rows)-1] + if !strings.Contains(foot, exchangeBackWord) { + t.Errorf("at %d columns the pane %s does not say how to get back to the list:\n%s", width, pane.what, foot) + } + if strings.Contains(foot, microcopy) { + t.Errorf("at %d columns the pane %s names %q, which its box does not do:\n%s", width, pane.what, microcopy, foot) + } + } + } + // AND THE DRAFT'S CHORDS GO IN FRONT OF IT, which is the mechanism. A lab + // with one project draws no `alt+p`, so the order is pinned on the line. + line := withChords("enter sends a follow-up · "+exchangeBackWord, "alt+p project · alt+e effort") + if want := "enter sends a follow-up · alt+p project · alt+e effort · " + exchangeBackWord; line != want { + t.Fatalf("the chords went on the pane's foot as %q, want them before the way out: %q", line, want) + } + if fitted := hintFit(line, 58); !strings.HasSuffix(fitted, exchangeBackWord) { + t.Fatalf("at sixty columns the pane's foot with chords on it reads %q, without the way back", fitted) + } +} diff --git a/internal/tui3/pages.go b/internal/tui3/pages.go index ed4a220e62..b18bff274d 100644 --- a/internal/tui3/pages.go +++ b/internal/tui3/pages.go @@ -464,18 +464,25 @@ func pages() []page { return rooms } -// placeWordList is the seven words in digit order, for the one sentence on the -// key sheet that has to say which digit is which (commands.go). +// placeWordList is every place's word with the digit that reaches it, in digit +// order — `1 home · 2 AI teams · 3 chats · …` — for the one sentence on the key +// sheet that has to say which digit is which (commands.go). // // IT IS READ OFF [placeOrder] AND NOT TYPED OUT, because a hand-written list on // the help sheet is a second answer to what `alt+3` opens — and the day a place // is added or the order changes, the sheet is the last thing anybody would think // to edit. One source of truth (CLAUDE.md's design laws). +// +// AND EVERY WORD CARRIES ITS DIGIT, because the digits are NOT the tab bar's +// order. [placeBarOrder] regrouped the bar and kept every number where it was, +// so memory is the fifth word on the bar and `alt+8`. A bare list in digit order +// is a list a reader counts along against the bar they can see, which is how +// the sheet came to call this list "the tab bar's own order". func placeWordList() string { words := make([]string, 0, len(placeOrder)) for _, id := range placeOrder { if pl, ok := placeRegistry[id]; ok { - words = append(words, pl.word()) + words = append(words, itoa(placeDigitOf(id))+" "+pl.word()) } } return strings.Join(words, " · ") diff --git a/internal/tui3/placeeveryone_test.go b/internal/tui3/placeeveryone_test.go index c601f76759..e5837b5db1 100644 --- a/internal/tui3/placeeveryone_test.go +++ b/internal/tui3/placeeveryone_test.go @@ -289,8 +289,8 @@ func TestTabLeavesEveryPlaceAndComesBack(t *testing.T) { } } -// alt+1…9 JUMPS FROM EVERY PLACE. The numbers are the bar's own order and they -// mean the same thing wherever you are standing — or, for a room with nothing +// alt+1…8 JUMPS FROM EVERY PLACE. The numbers are [placeOrder]'s, not the bar's, +// and they mean the same thing wherever you are standing — or, for a room with nothing // in it, they say why and leave you where you were. What they may never do is // nothing at all. func TestTheNumbersJumpFromEveryPlace(t *testing.T) { diff --git a/internal/tui3/placekeys.go b/internal/tui3/placekeys.go index 2fb3a3c974..8f1be7c4b9 100644 --- a/internal/tui3/placekeys.go +++ b/internal/tui3/placekeys.go @@ -315,7 +315,7 @@ func (a *app) placeKey(msg tea.KeyPressMsg) (tea.Cmd, bool) { return nil, false } -// placeJumpKey is `alt+1`…`alt+7` ALONE, and it is its own function because it +// placeJumpKey is `alt+1`…`alt+8` ALONE, and it is its own function because it // is the one class of the six that belongs to no place. // // THE CONVERSATION IS A SURFACE THE NUMBERS HAVE TO WORK ON. Every other class @@ -355,7 +355,7 @@ func (a *app) placeJumpKey(msg tea.KeyPressMsg) (tea.Cmd, bool) { // else is here". const placeMapKey = "alt+." -// placeDigit is `alt+1`…`alt+7`: the place at that position in [pages]. +// placeDigit is `alt+1`…`alt+8`: the place at that position in [placeOrder]. // // WHY alt IS THE ONE THIS PROGRAM PROMISES EVERYWHERE: `alt+1` arrives as // esc-then-1 and has for forty years, while `ctrl+1` has no legacy encoding at @@ -371,7 +371,9 @@ func placeDigitAt(prefix, key string) (page, bool) { return 0, false } at := int(key[len(prefix)] - '1') - // The digits are the BAR's order, the chats included (place_chats.go). + // The digits are [placeOrder]'s, the chats included (place_chats.go) — and + // NOT the bar's: [placeBarOrder] regrouped the words and kept every number, + // so memory is fifth on the bar and `alt+8`. all := placeOrder if at < 0 || at >= len(all) { return 0, false From 7848c0a036d7f211ac44e0508c080d366e36a02a Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:33:56 -0400 Subject: [PATCH 2/7] fix: give the ask-here pane's box its own words, and raise its sheet on a phone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - While an errand's pane holds the keyboard, the box under it says `type a follow-up` (or `type the change` after `o` on a card) instead of home's `type to search or start something new`. - On a phone-width window an errand's sheet now follows the pane's focus, so `ask here`, its chord and `→` on the row show the box the keys type into; before, only `enter` raised it and typed letters went nowhere visible. - The phone sheet's box draws the same sentence at rest. - Manual: the pane's box, ask here on a phone-width window, and the card's correction key (`o`, not `c`). Co-Authored-By: Claude Opus 5.5 --- internal/manual/chat/asking-from-home.md | 29 +++++++++-- internal/manual/chat/home.md | 5 ++ internal/manual/chat_test.go | 5 ++ internal/tui3/homeexchange.go | 24 +++++++++ internal/tui3/homeexchange_test.go | 65 ++++++++++++++++++++++++ internal/tui3/homephone_test.go | 51 ++++++++++++++++++- internal/tui3/homesheet.go | 30 ++++++++--- internal/tui3/pages.go | 12 ++++- 8 files changed, 207 insertions(+), 14 deletions(-) diff --git a/internal/manual/chat/asking-from-home.md b/internal/manual/chat/asking-from-home.md index a160c043bb..ff24b7d3f2 100644 --- a/internal/manual/chat/asking-from-home.md +++ b/internal/manual/chat/asking-from-home.md @@ -198,7 +198,7 @@ column exactly as they do with no exchange on screen, and `enter` opens the row cursor. **The pane follows the cursor** — walk off the exchange and the row you land on draws its own card again, where the frame is wide enough to have one. -## What the line under the ask here pane says — the answers, a follow-up and the way back +## What the line under the ask here pane says — the box, the answers, a follow-up and the way back While the pane has the keyboard the hint reads `enter sends a follow-up · tab or esc back to the list`, with the card's own answers in front of it when a card is up and @@ -207,6 +207,12 @@ the list`, with the card's own answers in front of it when a card is up and `tab or esc back to the list` is always the last clause, and on a narrow window it is the one that stays (*Ask here on a narrow window*, below). +**The box above that line says what typing there does**: `› type a follow-up`, and +`› type the change` once `o` on a card has asked for a correction. Home's own +`› type to search or start something new` belongs to home's box and comes back the moment the +list has the keyboard. The pane's box used to wear home's sentence, which promised a search +and a new conversation that typing in the pane does not do. + **The answers in that line are the ones the card actually drew, and never one more.** A card that offers all three reads `1 Set it up · Mondays at 9am · 3 Only now, don't repeat · 0 Don't set it up · o Change…`; @@ -248,6 +254,19 @@ Resizing between the two shapes costs nothing: it is the same exchange and the s keyboard, drawn in whichever geometry fits. Drag a window narrow with the pane open and the pane fills the frame; drag it wide again and it goes back beside the list. +## Ask here on a phone-width window — the errand is a sheet + +Under 60 columns, where home is an inbox, the stacked pane is a **sheet** over the whole +frame: `‹ back` on the top row, the exchange under it, its box, and `‹ back · send · more` +as the bar. **The sheet is up whenever the pane has the keyboard**, so `ask here` opens +straight into it, and so does anything else that hands the pane the keyboard — its chord, +`enter` or `→` on the errand's row. `tab` or `esc` puts the inbox back, with the errand's +row still on it and anything half-typed still in its box. + +It used to open on `enter` alone. `ask here` on a phone-width window left the inbox drawn +while the keys went into the pane's box, so what you typed appeared nowhere, under a box +that still said `type to search or start something new`. + ## Why can't I click a row while asking — you can, and it opens it You can, and it does. A click on any row of the list puts the cursor on it, gives @@ -312,10 +331,10 @@ the pane with your own words, when it would wake, and what it would cost per run created until you answer it: - `1` — yes. It stands as proposed, and the keyboard goes back to the list. -- `c` — change it. The pane says `type the change and press enter`; write the correction in - your own words ("make it 8pm", "every weekday") and the model proposes again. Nothing is - created by a change. (This was `2` before the card's answers moved onto the question every - screen here draws; `c` is that question's own key for "not as it stands".) +- `o` — change it. The pane says `type the change and press enter` and its box + `type the change`; write the correction in your own words ("make it 8pm", "every + weekday") and the model proposes again. Nothing is created by a change. (It was `2`, + and this page called it `c` long after the key became `o`.) - `3`. Once. The action runs now and nothing standing is created. On a repeating check the button reads `Only now, don't repeat`. **Not every card offers it**: a one-off reminder draws no once chip, because doing "remind me at six" now says the diff --git a/internal/manual/chat/home.md b/internal/manual/chat/home.md index 69c475eacb..617746faaa 100644 --- a/internal/manual/chat/home.md +++ b/internal/manual/chat/home.md @@ -2417,6 +2417,11 @@ Keys on the sheet: | `→` or `m` | open everything behind `▸ more` | | `→` then `p` / `s`, or `ctrl+e` / `ctrl+x` | on a standing item: pause it, stop it | +An `ask here` errand's row opens a sheet too — the exchange and its box — and it also opens +by itself whenever the errand takes the keyboard: `ask here`, its chord, `→` on the row. Its +keys are the pane's own: `tab` or `esc` back to the inbox, and `enter` sends what you typed +(*Asking from home*). + A standing item's sheet carries your words, where it is, when it goes off, what it has done, `4 runs · spent $0.08`, `ran 3 times this week · $0.04`, and how hard it thinks when you have said. A frame too short for all the bands **drops them from the bottom** — never diff --git a/internal/manual/chat_test.go b/internal/manual/chat_test.go index fe0926f534..673aa0a1bd 100644 --- a/internal/manual/chat_test.go +++ b/internal/manual/chat_test.go @@ -2538,6 +2538,11 @@ func TestTheChatManualAnswersTheQuestionsPeopleAsk(t *testing.T) { {"does alt+enter ask here", "asking-from-home"}, {"why does tab leave home from an ask here row", "keys"}, {"how do I leave the ask here pane on a narrow window", "asking-from-home"}, + // And the box under the pane, which wore home's own sentence, and on a + // phone-width window was not drawn at all while it had the keyboard. + {"why does the ask here box say type a follow-up", "asking-from-home"}, + {"I typed in ask here on my phone and nothing showed up", "asking-from-home"}, + {"ask here on a phone", "asking-from-home"}, {"why did it set my reminder for a time that already passed", "keeping-an-eye"}, {"why is there no once on my reminder card", "keeping-an-eye"}, // The e2e suite found the firing that reached a conversation and was diff --git a/internal/tui3/homeexchange.go b/internal/tui3/homeexchange.go index 7e9e47a6d3..3491ef26cf 100644 --- a/internal/tui3/homeexchange.go +++ b/internal/tui3/homeexchange.go @@ -123,6 +123,14 @@ const ( // the foot that writes it, and home's [withChords], which puts the draft's // chords in front of a foot's way out rather than behind it. exchangeBackWord = "tab or esc back to the list" + // homeAskRestWord is what the pane's box says with nothing typed into it, + // where home's own box says [placeRestWord]. Typing there writes a follow-up + // to this errand; nothing is searched and nothing new is started. + homeAskRestWord = "type a follow-up" + // homeAskChangeRestWord is the same box once the card's correction key has + // armed it: what is typed now is the change the card is answered with, not + // a follow-up ([homeExchange.changing]). + homeAskChangeRestWord = "type the change" ) // What an exchange's ROW says it is doing, and what the pane says while a turn @@ -2222,3 +2230,19 @@ func exchangeHint(ex *homeExchange) string { parts = append(parts, exchangeBackWord) return strings.Join(parts, " · ") } + +// exchangeRestWord is what the pane's box says with nothing typed in it: a +// follow-up, or — once the card's correction key has armed it — the change the +// card is answered with. It is the box's half of [exchangeHint], drawn by both +// frames that draw this box: the foot under a stacked or side-by-side pane +// ([app.placeRestWord]) and the phone's sheet ([app.homeErrandSheet]). +// +// IT USED TO BE HOME'S OWN SENTENCE, `type to search or start something new`, +// drawn over a box that does neither. The foot draws whichever box has the +// keyboard ([placeHome.box]), and asked home for its words whatever box it was. +func exchangeRestWord(ex *homeExchange) string { + if ex.changing { + return homeAskChangeRestWord + } + return homeAskRestWord +} diff --git a/internal/tui3/homeexchange_test.go b/internal/tui3/homeexchange_test.go index fd1a38530e..c9cbbaf42e 100644 --- a/internal/tui3/homeexchange_test.go +++ b/internal/tui3/homeexchange_test.go @@ -1798,3 +1798,68 @@ func TestThePaneKeepsItsWayBackAtEveryWidth(t *testing.T) { t.Fatalf("at sixty columns the pane's foot with chords on it reads %q, without the way back", fitted) } } + +// TestThePanesBoxSaysWhatTypingThereDoes pins the sentence the box rests on +// while the errand's pane holds the keyboard. +// +// IT SAID HOME'S OWN `type to search or start something new`, over a box that +// does neither: the foot draws whichever box has the keyboard ([placeHome.box]) +// and asked home for its words whatever box that was. Typing there writes a +// follow-up — or, once the card's correction key is pressed, the change the +// card is answered with — so that is what it says now, and home's sentence +// comes back the moment the list has the keyboard again. +func TestThePanesBoxSaysWhatTypingThereDoes(t *testing.T) { + for _, width := range []int{60, 140} { + lab := newErrandLab(t) + mine := lab.session("-tmp-alpha", "aaaa000000000001", "pricing research", "/tmp/alpha", time.Now()) + a := lab.app(mine, []session.Event{ + text(session.EventTextDelta, "Here is what I will do."), + standingProposal(7, "remind me at 6 to leave"), + {Kind: session.EventTurnDone}, + }) + a.width, a.height = width, 30 + a.openHome() + typeHome(a, "remind me at 6 to leave") + drive(t, a, key("up"), key("enter")) + ex := theExchange(a) + if ex == nil || !ex.focused || !ex.asking() { + t.Fatalf("at %d columns the pane never held the keyboard with a card up", width) + } + // THE BOX'S ROW IS THE ONE THAT OPENS WITH `› `, which is how a sentence + // in the box is told apart from the same words in the hint under it. + rests := func(want string) { + t.Helper() + frame := homeText(a) + if !strings.Contains(frame, "› "+want) { + t.Fatalf("at %d columns the box does not rest on %q:\n%s", width, want, frame) + } + for _, other := range []string{placeRestWord, homeAskRestWord, homeAskChangeRestWord} { + if other != want && strings.Contains(frame, "› "+other) { + t.Fatalf("at %d columns the box says %q where it should say %q:\n%s", width, other, want, frame) + } + } + } + rests(homeAskRestWord) + + // THE CORRECTION KEY CHANGES WHAT THE BOX IS FOR, and the box says so. + drive(t, a, key(questionCommentKey)) + if !ex.changing { + t.Fatalf("%s on the card did not arm the correction", questionCommentKey) + } + rests(homeAskChangeRestWord) + drive(t, a, key("x")) + if frame := homeText(a); !strings.Contains(frame, "› x") || strings.Contains(frame, "› "+homeAskChangeRestWord) { + t.Fatalf("at %d columns the letter typed is not in the box:\n%s", width, frame) + } + + // esc CLEARS WHAT WAS TYPED AND THE SENTENCE COMES BACK; esc again gives + // the list the keyboard, and home's own sentence comes back with it. + drive(t, a, key("esc")) + rests(homeAskChangeRestWord) + drive(t, a, key("esc")) + if ex.focused { + t.Fatalf("at %d columns the second esc left the keyboard in the pane", width) + } + rests(placeRestWord) + } +} diff --git a/internal/tui3/homephone_test.go b/internal/tui3/homephone_test.go index 9212490cfa..3c6f18446d 100644 --- a/internal/tui3/homephone_test.go +++ b/internal/tui3/homephone_test.go @@ -609,7 +609,9 @@ func TestAnErrandOnAPhoneIsTheSameSheet(t *testing.T) { if ex == nil { t.Fatal("ask here made no errand") } - a.home.sheet = homeSheet{open: true} + // NOTHING RAISES THE SHEET BY HAND. This test used to set the open flag + // itself, which hid that `ask here` at this tier never did (homesheet.go's + // [app.homeSheetShowing]): the pane having the keyboard IS the sheet. if !a.homeSheetShowing() { t.Fatal("the errand is not showing as a sheet") } @@ -627,6 +629,53 @@ func TestAnErrandOnAPhoneIsTheSameSheet(t *testing.T) { } } +// THE SHEET'S BOX IS THE BOX THE KEYS GO TO, AND IT SAYS WHAT TYPING THERE +// DOES. At this tier the sheet used to be raised by `enter` on the row alone, so +// `ask here` — and its chord, and `→` on the row — handed the pane the keyboard +// with the inbox still drawn: what was typed went into a box nobody could see, +// under home's own `type to search or start something new`. +func TestAskHereOnAPhoneTypesIntoTheSheetItRaised(t *testing.T) { + lab := newErrandLab(t) + mine := lab.session("-tmp-alpha", "aaaa000000000001", "pricing research", "/tmp/alpha", time.Now()) + a := lab.app(mine, []session.Event{ + text(session.EventTextDelta, "I will remind you at 6."), + {Kind: session.EventTurnDone}, + }) + a.width, a.height = phoneWidth, 30 + a.openHome() + typeHome(a, "remind me at 6") + drive(t, a, key("up"), key("enter")) + ex := theExchange(a) + if ex == nil || !ex.focused { + t.Fatal("ask here on a phone did not hand the pane the keyboard") + } + screen := func() string { return strings.Join(phoneRows(a), "\n") } + if !a.homeSheetShowing() { + t.Fatalf("ask here on a phone left the inbox up:\n%s", screen()) + } + if got := screen(); !strings.Contains(got, "› "+homeAskRestWord) || strings.Contains(got, placeRestWord) { + t.Fatalf("the sheet's box should rest on %q:\n%s", homeAskRestWord, got) + } + drive(t, a, key("x")) + if got := screen(); !strings.Contains(got, "› x") { + t.Fatalf("the letter typed is not on the screen:\n%s", got) + } + // AND OUT AND BACK IN: tab gives the list the keyboard and puts the inbox + // back, and → on the errand's row raises the sheet again with the + // half-typed follow-up still in its box. + drive(t, a, key("tab")) + if ex.focused || a.homeSheetShowing() { + t.Fatalf("tab did not put the inbox back:\n%s", screen()) + } + drive(t, a, key("right")) + if !ex.focused || !a.homeSheetShowing() { + t.Fatalf("→ on the errand's row did not raise its sheet:\n%s", screen()) + } + if got := screen(); !strings.Contains(got, "› x") { + t.Fatalf("the half-typed follow-up is gone from the sheet's box:\n%s", got) + } +} + // TestAPhoneInboxDrawsAStandingItemOnlyOnce is homephone.go's second law — A ROW // APPEARS ONCE — held for the third kind of row on that screen. // diff --git a/internal/tui3/homesheet.go b/internal/tui3/homesheet.go index 946b07933c..19041d7c75 100644 --- a/internal/tui3/homesheet.go +++ b/internal/tui3/homesheet.go @@ -119,21 +119,30 @@ type homeSheet struct { // homeSheetShowing reports whether the sheet is on the frame RIGHT NOW, which // is what every router below asks. // -// It is the open flag AND the tier AND — for an errand — the keyboard, for +// It is the tier AND the open flag — or, for an errand, the keyboard — for // [app.expandShowing]'s reason: a terminal dragged wider while the sheet is up // has stopped being the tier this sheet exists for, and the card is beside the // list from that moment exactly as it would have been had it been opened there. func (a *app) homeSheetShowing() bool { - if !a.home.sheet.open || !a.homePhone() { + if !a.homePhone() { return false } if ex, ok := a.homeSheetErrand(); ok { - // AN ERRAND'S SHEET IS ITS FOCUS. `tab` and `esc` hand the keyboard back - // to the list from inside the pane (homeexchange.go), and that IS the way - // out of this sheet — one gesture, not two flags to keep in step. + // AN ERRAND'S SHEET IS ITS FOCUS, BOTH WAYS. `tab` and `esc` hand the + // keyboard back to the list from inside the pane (homeexchange.go), and + // that IS the way out of this sheet — one gesture, not two flags to keep + // in step. The way in is the same fact: whatever handed the pane the + // keyboard raised the sheet. + // + // IT USED TO NEED THE OPEN FLAG AS WELL, which only `enter` on the row + // set. `ask here`, its chord and `→` on the row all hand the pane the + // keyboard without it, so at this tier they put the keys in a box the + // inbox does not draw: letters typed went nowhere a person could see, + // under a box still saying home's `type to search or start something + // new`. return ex.focused } - return true + return a.home.sheet.open } // homeSheetErrand is the exchange the sheet is about, when it is about one. @@ -590,7 +599,14 @@ func (a *app) homeErrandSheet(width, height int) ([]string, []int, int, int) { add(pal.dim(rule(width)), -1, homeSheetHit{}) text := ex.box.String() - add(" "+pal.muted("› ")+pal.ink(fit(text, width-4)), -1, homeSheetHit{}) + box := pal.ink(fit(text, width-4)) + if len(ex.box.value) == 0 { + // AT REST THE SHEET'S BOX SAYS WHAT TYPING THERE DOES, as the box under + // a stacked pane does ([app.placeRestWord]), with the caret on its + // first letter — the same box, at the tier with the least room. + box = pal.dim(fit(exchangeRestWord(ex), width-4)) + } + add(" "+pal.muted("› ")+box, -1, homeSheetHit{}) caretX, caretY := 3+ansi.StringWidth(text), len(lines)-1 if caretX > width-1 { caretX = width - 1 diff --git a/internal/tui3/pages.go b/internal/tui3/pages.go index b18bff274d..26ae05b10f 100644 --- a/internal/tui3/pages.go +++ b/internal/tui3/pages.go @@ -1626,7 +1626,17 @@ func boxFloor(height int) int { // placeRestWord is what home's box row says with nothing typed in it — the // design's own sentence (SCREEN 2b). It is home's alone now: no other place // draws a box ([place.box]). -func (a *app) placeRestWord() string { return "› " + placeRestWord } +// +// AND AN ERRAND'S PANE SAYS ITS OWN. While the pane holds the keyboard the box +// at the foot is the pane's ([placeHome.box]), and typing there writes a +// follow-up — so home's sentence over it promised a search and a new start the +// box under the keys does not do ([exchangeRestWord]). +func (a *app) placeRestWord() string { + if ex := a.paneExchange(); ex != nil && ex.focused { + return "› " + exchangeRestWord(ex) + } + return "› " + placeRestWord +} // placeHintSaid is the line under the composer, IN THE ONE SPELLING EVERY // CONSTANT ON THIS SURFACE IS AUTHORED IN. Home writes its own sentence for From 9dbc95ecbd84bdc2ab0b6e9d8f3206d87e9f10c9 Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:44:38 -0400 Subject: [PATCH 3/7] =?UTF-8?q?docs:=20home's=20arrows=20never=20cross=20c?= =?UTF-8?q?olumns;=20say=20what=20=E2=86=92=20opens=20instead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - keys.md, home.md, commands.md and places.md said ←/→ cross home's columns. Since 2026-09-17 every stop is in the field's one column and the rail is never walked: → opens the row's verbs, ← closes them, and on an ask-here row → hands the pane the keyboard. - The strip's words now match the code (x delete, p pause, s stop it), and the project row's `its chats` verb (removed in #1422) is no longer promised. - The phone inbox's folded project opens with → and folds with ←. - Comments in home.go, placekeys.go and verbstrip.go no longer cite the deleted homeView.crossColumns. - A truth test fails if any section repeats the crossing claims; three probes. Co-Authored-By: Claude Opus 5.5 --- internal/manual/chat/commands.md | 4 ++-- internal/manual/chat/home.md | 17 +++++++++------- internal/manual/chat/keys.md | 26 ++++++++++++++---------- internal/manual/chat/places.md | 12 ++++++----- internal/manual/chat_test.go | 5 +++++ internal/manual/chatdisplaytruth_test.go | 26 ++++++++++++++++++++++++ internal/tui3/home.go | 19 +++++++++-------- internal/tui3/placekeys.go | 8 ++++---- internal/tui3/verbstrip.go | 3 ++- 9 files changed, 81 insertions(+), 39 deletions(-) diff --git a/internal/manual/chat/commands.md b/internal/manual/chat/commands.md index ceb04c4e2f..3924691e41 100644 --- a/internal/manual/chat/commands.md +++ b/internal/manual/chat/commands.md @@ -1313,8 +1313,8 @@ at the left, and the **rail** at the right holds `projects` and `spend` at its t quiet panels under them. An empty panel keeps its heading and one dim line naming what arrives there. -`↑`/`↓` walk a column, `←`/`→` cross columns, `enter` opens, `esc` closes back into the -conversation you came from. **Typing does two things at once**: what you type is a new +`↑`/`↓` walk the field, `→` opens a row's verbs and `←` closes them (no arrow crosses +columns), `enter` opens, `esc` closes back into the conversation you came from. **Typing does two things at once**: what you type is a new conversation waiting to be sent AND a live search over every project on the machine — the panels give way to the matches, with `start a new conversation: "…"` directly above the box holding the cursor, so type-and-enter still starts a chat. **A line that starts with `/` is diff --git a/internal/manual/chat/home.md b/internal/manual/chat/home.md index 617746faaa..cf1deda73d 100644 --- a/internal/manual/chat/home.md +++ b/internal/manual/chat/home.md @@ -422,7 +422,8 @@ no right side at all on the resting screen. Where to go for the whole figure instead: -- **`→` then `its chats`** on the row lists every conversation in the project as a search; +- **typing the project's name** into the box lists every conversation in it as a search + (a project's row is not a stop, so `→` has nothing to open there); - **the activity page** for the work a project has run, which is the record rather than a count; - **the spend place** for what it has cost. @@ -473,7 +474,7 @@ every conversation in it, and `ctrl+t` on one of them starts a new one there. ones out of the way: `needs you` and `sessions` hold what wants you or is moving, and the conversation list shows the fifteen most recent conversations, with closed ones dimmed. A short frame folds what does not fit behind `N more`. Typing finds any conversation on the machine. -## Which column am I in — move between the columns on home: ↑↓ walk a panel, ←→ cross columns +## Which column am I in — move between the columns on home: ↑↓ walk the field, ←→ open a row's verbs **`↑` and `↓` walk the field**, from one panel into the next at its ends, and stop at both ends: `↑` off the top row stays on it and does not climb onto the tab bar. **`←` and @@ -487,8 +488,9 @@ inside it. The conversation and question rows have no heading. The row you are o wears the same ground and its title goes bold; the row under your mouse pointer wears it too while the pointer is on it. -**`→` opens a row's verbs, on every row and at every width**, and the chords work without -the strip: `ctrl+o` opens a conversation's workspace, the workspace a standing order stands +**`→` opens a row's verbs, on every row that has any and at every width** — on an `ask here` +row it hands the errand's pane the keyboard instead — and the chords work without the +strip: `ctrl+o` opens a conversation's workspace, the workspace a standing order stands over, or the conversation a `since you left` line happened in; `ctrl+y` copies the path, `ctrl+e` asks to delete a conversation or pauses a standing item, `ctrl+x` stops. The foot does not change from row to row and names none of them; `alt+.` draws the map when you want the rest. @@ -737,7 +739,8 @@ goes empty on the `start a new conversation` row, which is a chat that does not ## Switch between sessions — enter on home -`↑`/`↓` (or `ctrl+p`/`ctrl+n`) walk a column and `←`/`→` cross between columns. +`↑`/`↓` (or `ctrl+p`/`ctrl+n`) walk the rows; `←`/`→` never cross to another column — `→` +opens the row's verbs and `←` closes them, and with something typed they move the caret. **One click is `enter`.** A click on a row opens it, and a click on a fold that names a place opens that place. A `projects` row is the one exception: a click on it picks that @@ -2375,8 +2378,8 @@ Top to bottom: 3. Home ask exchanges retain their own answer rows. 4. `since you left` — what landed while you were not in the room. 5. **The projects.** This window's own project is drawn open with its remaining rows; every - other project is one folded line — `▸ wisp 6 · 2d` — that `enter` or a tap opens in - place. + other project is one folded line — `▸ wisp 6 · 2d` — that `enter`, `→` or a tap opens + in place, and `←` folds back. Each section shows **three rows** and folds the rest into `▸ …N more`; `enter` or a tap on that line opens it in place. A section with nothing in it is not drawn at all. **A row diff --git a/internal/manual/chat/keys.md b/internal/manual/chat/keys.md index 688f28cb8d..8d110b8e2c 100644 --- a/internal/manual/chat/keys.md +++ b/internal/manual/chat/keys.md @@ -2419,7 +2419,7 @@ what each holds), and its keys are a small grammar: | Key | On home | | --- | --- | | `↑` / `↓` (`ctrl+p` / `ctrl+n`) | walk the field, from one panel into the next, and stop at both ends: `↑` off the top row stays there and does not climb onto the tab bar (reach the bar with a click, `tab`, or a place's chord) | -| `←` / `→` | cross to the next column, onto the row nearest the one you left — only into a column with a row to stand on | +| `←` / `→` | never cross to another column: `→` opens the row's verbs on a strip under it and `←` closes them, and on an `ask here` row `→` hands its pane the keyboard. With something typed they move the caret | | a digit, or a question's own key | answers **the one row of `needs you` that is drawing its answers**, from anywhere on home, with no cursor move — the row under the cursor when it can take one, the top answerable row otherwise. A question's chips are `1 allow once 2 always 3 deny`; a landing in `unread` offers `1 accept 2 not right`, its own `[a]`/`[n]` being letters and letters always type on home | | `enter` | acts on the row under the cursor: a conversation opens, a `since you left` line opens its record, file or place, a `standing` row opens standing, a fold line opens or shuts its panel. `projects` and `spend` rows are not stops, so the cursor never reaches them; a click on a project picks the folder for the next message | | `pgup` / `pgdown` | jump a screenful | @@ -2434,19 +2434,23 @@ what each holds), and its keys are a small grammar: **Opening home puts the cursor on the chat you were in before this one**, so `space` `space` then `enter` is a switch back; a window with only one conversation opens on its own row, which says `here`. The panel holding the cursor marks its heading with the cursor's ground, -which is how you tell which column your arrows are in. **`alt+g` and `alt+q` are unbound on +which is how you tell which panel your arrows are in. **`alt+g` and `alt+q` are unbound on home** — there is no list left to group or thin — and so is the answer strip above the box: the answers are on the `needs you` row itself. -**`←` `→` cross home's columns first**; where no column with rows lies to the right, **`→` -opens the row's verbs** on a strip drawn **directly under that row**, pushing the rest -of the list down by its own height, and while that strip is drawn its letters are the verbs -and the box is asleep — a question's own answer keys and words, `x close`, `c copy name`, `n new in project`, -`o open folder`, `p pause it` or `r resume it` on a standing item. `esc` or -`←` closes it, `enter` still opens the row, and walking off the row closes it too. The arrows -never leave home's field, and on a panel's fold line (`N more`) `enter` -opens the panel and shows the rest; on `N fewer` it folds them again. -While something is typed the two arrows move the caret in the box instead. +**`←` and `→` never cross home's columns.** Every row the cursor can stand on is in the +field at the left; the rail at the right — `projects`, `spend` and the quiet panels — is +read and clicked, never walked, so there is no column to cross to. (The arrows used to +cross into the neighbouring column; that ended on 2026-09-17.) **`→` opens the row's +verbs** on a strip drawn **directly under that row**, pushing the rest of the list down by +its own height, and while that strip is drawn its letters are the verbs and the box is +asleep — a question's own answer keys and words; `x delete`, `c copy name`, +`n new in project` and `o open folder` on a conversation; `p pause` or `r resume it` on a +standing item; `s stop it` on work this window is running. `esc` or `←` closes it, `enter` +still opens the row, and walking off the row closes it too. A row with no verbs draws no +strip: on an `ask here` row `→` hands the errand's pane the keyboard instead, and on a +panel's fold line (`N more`) `enter` opens the panel and shows the rest; on `N fewer` it +folds them again. While something is typed the two arrows move the caret in the box instead. **A letter always types**, unless the verb strip that names it is on screen — that visible strip is the one state where a printable key is a verb, and it is why it has to be drawn. diff --git a/internal/manual/chat/places.md b/internal/manual/chat/places.md index 1d0b130f77..33f521159a 100644 --- a/internal/manual/chat/places.md +++ b/internal/manual/chat/places.md @@ -431,10 +431,12 @@ p pause s stop n not here ``` On memory they are `c open the card`, `e fix the wording` and `f forget it`. On home the -verbs are the row's own — a question's first two answers on its own answer keys, `x delete`, `c copy name`, `n new in project`, `o open folder`, and -`p pause it` or `r resume it` on a standing item, `s stop` on a task this window runs, -`its chats` and `open folder` on a project. On home, `→` opens the selected row's -options at every width; the arrows stay in the list. +verbs are the row's own — a question's first two answers on its own answer keys; +`x delete`, `c copy name`, `n new in project` and `o open folder` on a conversation; +`p pause` or `r resume it` on a standing item; `s stop it` on a task this window runs. A +project's row is not a stop, so it has none: a click on it picks the folder for the next +message. On home, `→` opens the selected row's options at every width; the arrows stay in +the list and never cross to another column. While those options are drawn, **those letters are the verbs** and the composer is asleep. On wide home layouts they appear below the selected description in the middle column, @@ -536,7 +538,7 @@ the left, and the **rail** at the right holds `projects` and `spend` at its top them, whichever panels are quiet today. An empty panel keeps its heading and one dim line naming what arrives there. -Its own keys are in the **Home** page: `↑↓` walk a panel, `←→` cross columns, a digit +Its own keys are in the **Home** page: `↑↓` walk the field, `→` opens a row's verbs and `←` closes them, a digit answers the question row drawing its answers, and `enter` opens a row. `tab` is the way to the next place, and the errand pane is taken into with `→` rather than `tab`. Home uses `alt+p` for the next conversation's project, `alt+e` for effort, `alt+a` for diff --git a/internal/manual/chat_test.go b/internal/manual/chat_test.go index 673aa0a1bd..a4ef5c996e 100644 --- a/internal/manual/chat_test.go +++ b/internal/manual/chat_test.go @@ -2676,6 +2676,11 @@ func TestTheChatManualAnswersTheQuestionsPeopleAsk(t *testing.T) { {"why is the home screen empty", "home"}, {"what does the dim line under a panel heading mean", "home"}, {"how do I move between the columns on home", "home"}, + // The arrows stopped crossing columns on 2026-09-17, and four pages went + // on saying they did; `→` opens the row's verbs and nothing crosses. + {"can I walk to the projects column with the arrow keys", "home"}, + {"do the arrow keys cross columns on home", "places"}, + {"what does the right arrow do on home", "places"}, {"why is the cursor on my previous chat when I open home", "home"}, {"why did home cut off panels on a small terminal", "home"}, {"what does 3 more mean on home", "home"}, diff --git a/internal/manual/chatdisplaytruth_test.go b/internal/manual/chatdisplaytruth_test.go index 2e63ae4471..7aeef84cf7 100644 --- a/internal/manual/chatdisplaytruth_test.go +++ b/internal/manual/chatdisplaytruth_test.go @@ -92,6 +92,32 @@ func TestNoSectionRepeatsStaleHelpSettingsGreetingOrManagerClaims(t *testing.T) } } +// HOME'S ARROWS STOPPED CROSSING COLUMNS ON 2026-09-17, when every row the cursor +// can stand on moved into the field's one column and the rail stopped being +// walked (internal/tui3's homegrid.go, THE ARROWS STAY IN THEIR COLUMN). Five +// sections went on saying `←`/`→` cross columns, and two promised a project +// row's `its chats` verb, which #1422 removed along with the row's stop. +func TestNoSectionSaysHomesArrowsCrossColumns(t *testing.T) { + stale := []string{ + "←→ cross columns", + "`←→` cross columns", + "`←`/`→` cross columns", + "`←`/`→` cross between columns", + "cross to the next column", + "cross home's columns first", + "`→` then `its chats`", + "`its chats` and `open folder` on a project", + } + for _, section := range Chat().Sections() { + text := flatten(section.Title + " " + section.Body) + for _, claim := range stale { + if strings.Contains(text, claim) { + t.Errorf("%s: %s still says %q", section.Page, section.Title, claim) + } + } + } +} + func TestManualStatesHelpSettingsGreetingAndManagerDisplayTruth(t *testing.T) { for _, test := range []struct { page, title string diff --git a/internal/tui3/home.go b/internal/tui3/home.go index ce5e0515ff..09b700f498 100644 --- a/internal/tui3/home.go +++ b/internal/tui3/home.go @@ -2631,11 +2631,11 @@ func (a *app) homeKey(msg tea.KeyPressMsg) tea.Cmd { // errand everywhere else; it is THE WAY TO THE NEXT PLACE now, on every // place, because a key meaning "next section" here and "next place" on the // other six is exactly the per-place grammar the router exists to end - // (placekeys.go). Both of the things it did are still reachable and neither - // lost a gesture: the zones are crossed into with `←` and back out with `→` - // ([homeView.crossColumns], which the file already described as "tab's - // circle, unrolled onto the two keys that already point the way"), and the - // errand in the pane is taken into with `→` from its own row. + // (placekeys.go). Neither of the things it did needs it now. There are no + // zones left to cycle: every row the cursor can stand on is in the field, + // which is one column at every width, and no arrow crosses columns any more + // (homegrid.go's THE ARROWS STAY IN THEIR COLUMN). And the errand in the + // pane is taken into with `→` from its own row. case "esc": // ONE LAYER AT A TIME, the settings panel's rule: a box with something // in it is cleared first, and the second esc leaves. A person who typed @@ -2890,10 +2890,11 @@ func (a *app) homeKey(msg tea.KeyPressMsg) tea.Cmd { case "right": // AN ERRAND IN THE PANE IS TAKEN INTO WITH `→`, AND IT USED TO BE `tab`. // `tab` is the way to the next place now (pages.go), so the toggle moved - // onto the arrow that already points at the column the errand is drawn in - // — the same law [homeView.crossColumns] follows two clauses down, where → - // walks from the zones into the list. `esc` still hands the keyboard back, - // which is the half of the toggle that never moved. + // onto the arrow that already points at the column the errand is drawn + // in. The arrow is free on that row: an errand's row has no verbs for the + // router to open (placekeys.go), and no arrow crosses columns any more + // (homegrid.go). `esc` still hands the keyboard back, which is the half of + // the toggle that never moved. if h.box.empty() { if ex := a.paneExchange(); ex != nil && !ex.focused { ex.focused = true diff --git a/internal/tui3/placekeys.go b/internal/tui3/placekeys.go index 8f1be7c4b9..42e66a091d 100644 --- a/internal/tui3/placekeys.go +++ b/internal/tui3/placekeys.go @@ -259,10 +259,10 @@ func (a *app) placeKey(msg tea.KeyPressMsg) (tea.Cmd, bool) { return nil, true } // `→` OPENS THE ROW'S VERBS, and only when the row has any. Where it does - // not, the arrow keeps every meaning it already had on that place — the - // fold ladder and the walk across the columns on home, the caret's step - // inside a box everywhere — which is what makes this a new claim on the - // key rather than a seizure of it. + // not, the arrow keeps every meaning it already had on that place — on + // home the way into an errand's pane and the fold ladder of the phone's + // inbox, the caret's step inside a box everywhere — which is what makes + // this a new claim on the key rather than a seizure of it. if a.openStrip() { a.touch() return nil, true diff --git a/internal/tui3/verbstrip.go b/internal/tui3/verbstrip.go index f9a57b3176..7b86a40a8f 100644 --- a/internal/tui3/verbstrip.go +++ b/internal/tui3/verbstrip.go @@ -77,7 +77,8 @@ func (a *app) closeStrip() { // openStrip is `→` on a row that has verbs. It answers false when the row has // none, and the arrow keeps every other meaning it already had on that place — -// which on home is the fold ladder and the walk across the columns. +// which on home is the way into an errand's pane and the fold ladder of the +// phone's inbox. func (a *app) openStrip() bool { verbs := a.rowVerbs() if len(verbs) == 0 { From 82ce90c40b2d8e12c35f5b1fe4a0d1c7f1d93e61 Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:53:45 -0400 Subject: [PATCH 4/7] docs: home's ctrl+e asks to delete, it does not close a tab - keys.md said ctrl+e closes, archives and reopens a conversation, that the strip's verb is `x close`, and that the card legend says `ctrl+e close`. Since #1750 ctrl+e on a conversation row opens the delete confirmation (legend `ctrl+e delete`), and enter reopens a dimmed row. - homeArchiveRow's comment says no key reaches it; tests still use it to make closed rows, so it stays. - A truth test fails if a section repeats the old claims; one probe, and the places heading for the box's rule gains the asker's words. Co-Authored-By: Claude Opus 5.5 --- internal/manual/chat/keys.md | 33 +++++++++++++----------- internal/manual/chat/places.md | 2 +- internal/manual/chat_test.go | 3 +++ internal/manual/chatdisplaytruth_test.go | 21 +++++++++++++++ internal/tui3/place_home.go | 6 +++++ 5 files changed, 49 insertions(+), 16 deletions(-) diff --git a/internal/manual/chat/keys.md b/internal/manual/chat/keys.md index 8d110b8e2c..bb3bfb1f7d 100644 --- a/internal/manual/chat/keys.md +++ b/internal/manual/chat/keys.md @@ -850,19 +850,22 @@ home's box — which is the first box most people type into. `home` and `end` ar the exception and stay with the box that owns them: on the settings panel, the task page and the rewind sheet they move the **list**, not the caret. -## cmd+right on home no longer puts a conversation away +## cmd+right on home no longer acts on the row — ctrl+e asks to delete, from the end of the line -`ctrl+e` sets the row under the cursor aside on home — a conversation goes to the -archive, a standing item is paused, and the card's legend says `ctrl+e close`. -`cmd+→` arrives as `ctrl+e` on a Mac, so reaching for the end of a sentence used -to archive whatever the cursor was resting on. +`ctrl+e` acts on the row under the cursor on home: on a conversation it **asks to delete +it** — the confirmation *Permanently deleting a conversation from Home* describes, and +nothing is deleted until you confirm — and on a standing item it pauses it. The card's +legend says `ctrl+e delete` or `ctrl+e pause`. `cmd+→` arrives as `ctrl+e` on a Mac, so +reaching for the end of a sentence used to act on whatever the cursor was resting on. **It reads the caret now.** With the caret somewhere before the end of what you typed, `ctrl+e` moves it to the end of the line and leaves the row alone; from the -end of the line — and over an empty box — it is the put-away key the legend names. -So one press is never destructive, and the way back out of the archive still -works: type the name of a row you put away, the list finds it, and `ctrl+e` from -there brings it back. +end of the line — and over an empty box — it is the row's key the legend names. +So one press never reaches the row. + +It used to put a conversation away in an archive, and `ctrl+e` on that dimmed row brought +it back. Since 2026-10-07 it asks to delete instead, and **`enter` on a dimmed row** is what +reopens a closed conversation. ## Click the box to put the caret there — in the conversation and on home @@ -2264,9 +2267,9 @@ rather than the same one, and several go in a row. Its conversation, its work an draft are untouched; `→` reaches it and `enter` brings it back. **Tab closing does not archive the saved conversation.** Home shows up to three -recently closed tabs as dimmed rows. `ctrl+e` or `→`, then `x close`, on Home also -archives the conversation and closes its tab, removing it from the default chats -list. Both routes keep work and drafts. Enter on a dimmed Home row reopens it. +recently closed tabs as dimmed rows, and `enter` on one reopens it. Home has no archive key +any more: `ctrl+e`, or `→` then `x delete`, on a conversation's row asks to delete it +permanently, and nothing goes until you confirm. On the row marked `you are here`, the card closes and the window selects the most recently used remaining tab, or Home when none remain. This is the same action as @@ -2456,9 +2459,9 @@ folds them again. While something is typed the two arrows move the caret in the strip is the one state where a printable key is a verb, and it is why it has to be drawn. Everywhere else "make me a site" comes out whole wherever the cursor is resting. The row's actions otherwise ride chords, which can never begin a word, and they work from any column: -**`ctrl+e` closes the conversation tab** — it leaves the open Home list and the -default chats menu. Up to three closed rows stay dimmed on Home; typing finds older -ones. Enter or `ctrl+e` on a closed row reopens it. **`ctrl+o`** opens +**`ctrl+e` asks to delete the conversation** — the same confirmation `→` then `x delete` +opens, and nothing is deleted until you confirm. Up to three closed tabs stay dimmed on +Home; typing finds older ones, and `enter` on a dimmed row reopens it. **`ctrl+o`** opens its folder and **`ctrl+y`** copies its path. **`ctrl+t`** on a conversation's row starts a new one in that row's folder; a click on a row of the `projects` panel instead picks the folder for the next message sent from home. On a standing item's row — in `needs you` while it asks, in diff --git a/internal/manual/chat/places.md b/internal/manual/chat/places.md index 33f521159a..7451f9166e 100644 --- a/internal/manual/chat/places.md +++ b/internal/manual/chat/places.md @@ -268,7 +268,7 @@ conversation with no work stands under `completed`; the extra section can put th unused row below the visible screen. Use `↑` and `↓` to reach it. The count does not promise that every row fits on the screen at once. -## The rule above home's box — where it lands, the model, thinking, approvals, what happened to the here ~/codeaf chip +## The rule above home's box — where it lands, the model, thinking, approvals, what happened to the here ~/codeaf chip next to the box The line over home's box is the same shape as the line over a conversation's own message box: diff --git a/internal/manual/chat_test.go b/internal/manual/chat_test.go index a4ef5c996e..521186982e 100644 --- a/internal/manual/chat_test.go +++ b/internal/manual/chat_test.go @@ -2681,6 +2681,9 @@ func TestTheChatManualAnswersTheQuestionsPeopleAsk(t *testing.T) { {"can I walk to the projects column with the arrow keys", "home"}, {"do the arrow keys cross columns on home", "places"}, {"what does the right arrow do on home", "places"}, + // And ctrl+e, which the keys page said closed a tab after #1750 made it + // the delete confirmation. + {"what does ctrl+e do on home", "keys"}, {"why is the cursor on my previous chat when I open home", "home"}, {"why did home cut off panels on a small terminal", "home"}, {"what does 3 more mean on home", "home"}, diff --git a/internal/manual/chatdisplaytruth_test.go b/internal/manual/chatdisplaytruth_test.go index 7aeef84cf7..8cc72076dd 100644 --- a/internal/manual/chatdisplaytruth_test.go +++ b/internal/manual/chatdisplaytruth_test.go @@ -118,6 +118,27 @@ func TestNoSectionSaysHomesArrowsCrossColumns(t *testing.T) { } } +// HOME'S ctrl+e STOPPED CLOSING TABS ON 2026-10-07 (#1750): on a conversation's +// row it opens the delete confirmation, and the strip's `x` is `x delete`. Three +// sections of the keys page went on saying it closes, archives and reopens. +func TestNoSectionSaysHomesCtrlEClosesATab(t *testing.T) { + stale := []string{ + "`ctrl+e` closes the conversation tab", + "Enter or `ctrl+e` on a closed row reopens it", + "the card's legend says `ctrl+e close`", + "`x close`", + "it is the put-away key the legend names", + } + for _, section := range Chat().Sections() { + text := flatten(section.Title + " " + section.Body) + for _, claim := range stale { + if strings.Contains(text, claim) { + t.Errorf("%s: %s still says %q", section.Page, section.Title, claim) + } + } + } +} + func TestManualStatesHelpSettingsGreetingAndManagerDisplayTruth(t *testing.T) { for _, test := range []struct { page, title string diff --git a/internal/tui3/place_home.go b/internal/tui3/place_home.go index 5d837c66f4..60027d1678 100644 --- a/internal/tui3/place_home.go +++ b/internal/tui3/place_home.go @@ -263,6 +263,12 @@ func (a *app) homeSwitchVerb(line homeLine, row switcherRow, v switcherVerb) ver // homeArchiveRow closes the same tab the row names, keeping its work and draft. // Reopening returns through the normal conversation door, restoring the tab too. +// +// NO KEY REACHES IT ANY MORE. `ctrl+e` and the strip's `x` on a conversation's +// row became the delete confirmation (#1750, [app.conversationDeleteOpen]), and +// home has had no archive key since. It stays because the tests make their +// closed rows with it — a closed row is still a state home draws, dimmed, and +// still reopens on `enter` ([app.homeOpenLine]). func (a *app) homeArchiveRow(row session.SessionRow) tea.Cmd { if a.homeConversationClosed(row) { return a.homeOpenLine(homeLine{kind: homeSession, row: row}) From 06aff011e7865f0915e9ce3b64906f737a986ef6 Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:55:58 -0400 Subject: [PATCH 5/7] changelog: entry for #1834 Co-Authored-By: Claude Opus 5.5 --- .../unreleased/1834-home-hints-match-keys.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/changes/unreleased/1834-home-hints-match-keys.md diff --git a/docs/changes/unreleased/1834-home-hints-match-keys.md b/docs/changes/unreleased/1834-home-hints-match-keys.md new file mode 100644 index 0000000000..d3f708274a --- /dev/null +++ b/docs/changes/unreleased/1834-home-hints-match-keys.md @@ -0,0 +1,15 @@ +--- +kind: fixed +title: Home's ask-here hints, the pane's box and /help's place numbers say only what the keys do +pr: 1834 +surface: [chat, docs] +invalidates: + - "An errand row's foot on home said `enter or tab answer this ask here`, and `tab` there goes to the next place. It says `enter or → answer this ask here`, or `enter answer this ask here · esc clear` over a draft." + - "The manual and home.go called `alt+enter` a second spelling of `ask here` on home. It opens the composer layer and its second press sends a task; `ctrl+enter` is the only ask-here chord, and home's unreachable `alt+enter` arm is gone." + - "At 60 columns the ask-here pane's foot dropped `tab or esc back to the list` first, and with a card up it lost it up to 140 columns. It is the last clause and always kept; `/ commands` is no longer named while the pane has the keyboard." + - "While the ask-here pane held the keyboard its box said home's `type to search or start something new`. It says `type a follow-up`, or `type the change` after `o` on a card." + - "On a phone-width window only `enter` on the row raised an errand's sheet, so after `ask here` typed letters went into a box that was not drawn. The sheet now shows whenever the pane has the keyboard." + - "The manual said home's `←`/`→` cross to the next column. They have not since 2026-09-17: every stop is in the field, `→` opens the row's verbs and `←` closes them." + - "The keys page said home's `ctrl+e` closes a conversation tab and reopens a closed row. Since #1750 it opens the delete confirmation; `enter` reopens a dimmed row." + - "/help said the alt digits run in the tab bar's own order. They follow placeOrder: memory is the fifth word on the bar and `alt+8`. /help now pairs each digit with its place, and the manual no longer says memory is off the bar." +--- From 93c3cdb4b28489f3183126ef3cbcd2bfb8221363 Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:27:56 -0400 Subject: [PATCH 6/7] review: /help's digit row in commands.md, the o key, and leftovers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - commands.md quoted /help's old `established shortcut order` row; it now quotes the numbered list, and a truth test refuses the old phrases. - asking-from-home: the way back is the last clause except while a change is being typed after `o`. - Comments call the correction key `o`, and the ctrl+x comment no longer says → crosses columns. - Remove the e2e rig's unused altEnter. Co-Authored-By: Claude Opus 5.5 --- internal/e2e/tmux_test.go | 5 ----- internal/manual/chat/asking-from-home.md | 6 ++++-- internal/manual/chat/commands.md | 2 +- internal/manual/chatdisplaytruth_test.go | 4 ++++ internal/tui3/home.go | 4 ++-- internal/tui3/homeexchange.go | 4 ++-- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/internal/e2e/tmux_test.go b/internal/e2e/tmux_test.go index 9000071f14..3a4649ba63 100644 --- a/internal/e2e/tmux_test.go +++ b/internal/e2e/tmux_test.go @@ -502,11 +502,6 @@ func (r *rig) lit(text string) { // ctrl+enter, which home binds to `ask here`. func (r *rig) ctrlEnter() { r.lit("\x1b[13;5u") } -// altEnter is ESC CR, which every terminal delivers. It is NOT a second -// spelling of ctrlEnter: on home the router takes alt+enter for the composer -// layer, whose second press sends the task (internal/tui3's composerlayer.go). -func (r *rig) altEnter() { r.lit("\x1b\r") } - // mouseTo moves the pointer to a 1-based cell with an SGR motion report, which // is what the all-motion mode the app turns on in View() asks for. func (r *rig) mouseTo(col, row int) { diff --git a/internal/manual/chat/asking-from-home.md b/internal/manual/chat/asking-from-home.md index ff24b7d3f2..e4bab2a57d 100644 --- a/internal/manual/chat/asking-from-home.md +++ b/internal/manual/chat/asking-from-home.md @@ -205,7 +205,8 @@ the list`, with the card's own answers in front of it when a card is up and `↓ continue as a conversation` before the way out when that row is on screen. Home's `/ commands` is not on it: a `/` typed in the pane starts a follow-up and opens no list. `tab or esc back to the list` is always the last clause, and on a narrow window it is the -one that stays (*Ask here on a narrow window*, below). +one that stays (*Ask here on a narrow window*, below) — except while a change is being +typed after `o`, when the line is `type the change and press enter · esc clear`. **The box above that line says what typing there does**: `› type a follow-up`, and `› type the change` once `o` on a card has asked for a correction. Home's own @@ -238,7 +239,8 @@ of sat side by side: its tail, and **`enter`** or **`→`** on that row opens it again. **The way back is always on the line under the box**, at the narrowest width this layout -has, sixty columns. `tab or esc back to the list` is the last clause of that line and the +has, sixty columns, except while a change is being typed after `o` (that line ends +`esc clear`). `tab or esc back to the list` is the last clause of that line and the one that stays; the clauses in front of it give way first, nearest first — home's chords such as `alt+e effort`, then `↓ continue as a conversation`, then `enter sends a follow-up`, then the card's answers from the last one back — so a card at sixty columns reads diff --git a/internal/manual/chat/commands.md b/internal/manual/chat/commands.md index 3924691e41..5e6f5201ff 100644 --- a/internal/manual/chat/commands.md +++ b/internal/manual/chat/commands.md @@ -370,7 +370,7 @@ Under the table `/help` prints the keys that have no slash command, including rows, directly under the `tab` row: ``` -alt+1…8 go to a place · established shortcut order: home · AI teams · chats · activity · spend · settings · standing · memory +alt+1…8 go to a place · 1 home · 2 AI teams · 3 chats · 4 activity · 5 spend · 6 settings · 7 standing · 8 memory alt+. on a place: what else is here · every key that place has, drawn on a place, tab is the next place · esc back ``` diff --git a/internal/manual/chatdisplaytruth_test.go b/internal/manual/chatdisplaytruth_test.go index 8cc72076dd..e781211276 100644 --- a/internal/manual/chatdisplaytruth_test.go +++ b/internal/manual/chatdisplaytruth_test.go @@ -58,6 +58,10 @@ func TestNoSectionRepeatsAStaleDisplayClaim(t *testing.T) { "| `auditor` | checker |", "draws one tab per open conversation, and", "names every open conversation, and", + // /help's digit row said it was in the tab bar's order, then in the + // "established shortcut order"; it pairs each digit with its place now. + "established shortcut order", + "in the tab bar's own order", } for _, section := range Chat().Sections() { body := flatten(section.Body) diff --git a/internal/tui3/home.go b/internal/tui3/home.go index 09b700f498..93a1bf9179 100644 --- a/internal/tui3/home.go +++ b/internal/tui3/home.go @@ -2831,8 +2831,8 @@ func (a *app) homeKey(msg tea.KeyPressMsg) tea.Cmd { } // AND IT STOPS A PIECE OF WORK THIS WINDOW HOLDS, through the stop card, // which asks first — the same verb the row's strip offers - // (homepanel_running.go's [app.runningVerbs]), reached where `→` crosses - // columns instead of opening the strip. + // (homepanel_running.go's [app.runningVerbs]), reached without opening + // the strip. if ok { for _, v := range a.runningVerbs(line) { if v.key == 's' { diff --git a/internal/tui3/homeexchange.go b/internal/tui3/homeexchange.go index 3491ef26cf..1693368de7 100644 --- a/internal/tui3/homeexchange.go +++ b/internal/tui3/homeexchange.go @@ -1325,7 +1325,7 @@ func (a *app) exchangeKey(ex *homeExchange, msg tea.KeyPressMsg) tea.Cmd { // hands the keyboard back to the list, and a card left standing on the // column is not an answer. // - // AND `c` ARMS THE CORRECTION, which is what `2 change when or where` + // AND `o` ARMS THE CORRECTION, which is what `2 change when or where` // used to be — a chip that asked for the box rather than an answer. It is // the same key on every question this surface asks. if ex.asking() && ex.ask != nil { @@ -2198,7 +2198,7 @@ func exchangeHint(ex *homeExchange) string { if ex.ask != nil { parts = append(parts, exchangeAnswerWords(ex.ask.question)) // AND THE KEY THAT ASKS FOR THE BOX, which is the one answer with no - // row on the card: `c` says "I will take one of these, but not as it + // row on the card: `o` says "I will take one of these, but not as it // stands", and the sentence that follows is the correction. // // ITS WORD IS THE KEY TABLE'S OWN (questionkeys.go) and not this From 14428e37bf8fd95d714136db975c514f64638a51 Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:26:50 -0400 Subject: [PATCH 7/7] test: let the pane's card settle before the box-sentence test presses o #1833's settle guard drops a key that lands with the card, so the test pins the clock and moves it past questionSettle once the card has been drawn. Co-Authored-By: Claude Opus 5.5 --- internal/tui3/homeexchange_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/tui3/homeexchange_test.go b/internal/tui3/homeexchange_test.go index fe7bd5ea3e..0d2318ffd4 100644 --- a/internal/tui3/homeexchange_test.go +++ b/internal/tui3/homeexchange_test.go @@ -1838,6 +1838,10 @@ func TestThePanesBoxSaysWhatTypingThereDoes(t *testing.T) { {Kind: session.EventTurnDone}, }) a.width, a.height = width, 30 + // THE CARD TAKES NO KEY UNTIL IT HAS BEEN READ (the settle guard), so the + // clock is pinned and moved past it once the card has been drawn. + now := time.Now() + a.clock = func() time.Time { return now } a.openHome() typeHome(a, "remind me at 6 to leave") drive(t, a, key("up"), key("enter")) @@ -1860,6 +1864,7 @@ func TestThePanesBoxSaysWhatTypingThereDoes(t *testing.T) { } } rests(homeAskRestWord) + now = now.Add(questionSettle) // THE CORRECTION KEY CHANGES WHAT THE BOX IS FOR, and the box says so. drive(t, a, key(questionCommentKey))