Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/changes/unreleased/1487-space-space-home-beside-the-tip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
kind: changed
title: a conversation's tip covers the project on the keys row, with a cross, after 15 quiet seconds
pr: 1487
surface: [chat, docs]
invalidates:
- "A conversation's earned tip was the keys row's lowest rung and replaced the whole row, so from the first exchange onward the controls and `space space home` were gone while a tip stood. The controls keep their place now, and the tip stands at the keys row's right end, covering `project: <path>` while it is up, with home's bulb and a cross; the project is back when it goes."
- "A conversation's tip had no clock and no cross. It waits for 15 seconds with no key, click, scroll or paste and none since a turn ended or codeaf opened, and its cross puts it away (giving the project back) until the conversation is left (home, a place, another conversation) and come back to. Home's row does not wait."
- "The Workspace tab's `disable hints` row explained itself in a sentence about which rows it silences. The line under it reads `disable💡 tips everywhere (requires restart)`. It still silences both rows and the what's-new lines."
- "A conversation's tip was counted as shown when its slot took it, once per session. It is counted when the keys row first draws it, so a tip hidden behind the 15-second wait all session costs none of its six showings."
- "The tip list was twenty-two rows. It is twenty-three: `space space takes you back to home`, the first row a conversation earns by talking (ahead of `/task`), armed only in a conversation whose door home opens and never on home, and retired by the two-space gesture itself (the new `home-gesture` event) — not by `/home` or the tab."
---
The controls are the keys that work right now and a tip is only a suggestion,
so the tip takes only the project's place, and only once somebody has stopped working.
7 changes: 2 additions & 5 deletions internal/config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -2687,11 +2687,8 @@ func (s *Settings) build() []Setting {
Setting{
Key: KeyHints, Category: CategoryInterface, Kind: SettingBool,
Label: "disable hints",
Hint: "on silences the one-line tips — the keys row's in a conversation and the " +
"row above the rule on home — and the what's-new line a new build may say " +
"with them. Off, the default, shows each tip until the key or command it " +
"names has been used once. A change lands at the end of the next turn.",
read: func() string { return formatBool(!HintsAt(dir)) },
Hint: "disable💡 tips everywhere (requires restart)",
read: func() string { return formatBool(!HintsAt(dir)) },
write: func(raw string) error {
disabled, err := parseBool(raw)
if err != nil {
Expand Down
62 changes: 45 additions & 17 deletions internal/manual/chat/hints-and-tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,53 @@ The dim row under your message box — the last row of the frame — mostly name
work right now: `esc interrupt` while an answer is coming, `y allow · n deny · a always`
while codeaf is asking you something, `space space home` when there is a home to go to,
`/ commands` when nothing else is true. Once you have used codeaf a little, that idle line
sometimes carries a **tip** instead — one sentence naming a key or a command you have not
sometimes carries a **tip** as well — one sentence naming a key or a command you have not
used yet, and what it does, for example `esc esc or /rewind takes back an earlier message`
or `/files finds files codeaf wrote for you`. It reads the way every hint on this surface
does: the key or the command first, then what it does.

**In a conversation the tip is the keys row's lowest rung.** It takes that row from the
rest state — the line a newcomer reads when nothing is happening — and every state with
keys of its own outranks it: a running turn, a list, a panel, a room, and a box with so
much as one letter in it. Empty the box and it is back. There is no clock over it and no
cross on it: the event that makes a tip true puts it there, and it stays until something
truer takes the row or you use what it teaches.
**In a conversation the tip stands at the right end of the keys row, covering the
project** (`project: <path>`) while it is up — led by a bulb (💡) and closed by a small
cross (`✕`), as home's is. The controls keep their place at the row's left:

```
─ glm-5.3-flash:auto · ◇ asks ────────────────────────────────────── $0.00 idle ─
›
opt+e effort · / commands · space space home 💡 /files finds files codeaf wrote for you ✕
```

When the tip goes, `project:` is back in its place. When the row is too narrow for the
whole sentence it is cut short with `…`, and where there is no room for a word of it the
project stays.

**It waits for 15 seconds of quiet**, counted from when codeaf opened at the earliest.
Any key, click, scroll or paste hides it and starts the 15 seconds again, and so does an
answer finishing — the tip never appears under an
answer you have just started reading or between two things you are typing. A running turn,
a list, a panel, a room, or a box with so much as one letter in it also keep it down. It
shows whether the task column is open or put away with `ctrl+g`.

**The cross puts it away**, and gives the project back, until you leave the conversation —
for home, a place, or another conversation — and come back; it does not retire the tip,
which comes round again later.

**Home says its tips differently, and the two are not the same row.** Home's is the dim
line **directly above the rule** over its box, right-aligned, led by a bulb and closed by a
small cross. It rotates on every visit and every two minutes at rest, and the cross blanks
it until you leave home and come back. A conversation is a screen you sit in and home is a
screen you pass through, so the tip worth saying differs: a conversation gets the most
urgent thing that is true right now, and home gets everything in turn.
urgent thing that is true right now, and home gets everything in turn. Home's row does not
wait for quiet; a conversation's waits 15 seconds.

Both rows draw from **one list** of twenty-two tips (below), and using a gesture on
Both rows draw from **one list** of twenty-three tips (below), and using a gesture on
either retires it on both.

**A conversation's tip has changed places twice.** It was the keys row's lowest rung until
**A conversation's tip has moved several times.** It was the keys row's lowest rung until
2026-09-22, moved up to a row of its own over the rule that day — with a quiet minute
before it appeared, a two-minute rotation and a cross — and moved back to the keys row the
same day, which is where it is now.
same day, replacing the controls there. On 2026-09-24 it stopped replacing the controls,
spent part of that day above the rule again, and settled at the keys row's right end over
the project, with a cross and a 15-second wait, which is where it is now.

On a Mac the row says `opt` where the table below says `alt`, exactly as the keys row does.

Expand Down Expand Up @@ -65,7 +86,8 @@ because they behave differently.

- **In a conversation, a showing is one session.** However many times the tip comes and
goes on the keys row while you work, that is one showing, counted the first time the row
takes it. Six sessions of never acting on it and it is done.
actually draws it — after its 15-second wait — so a tip you never paused long enough to
see is not counted. Six sessions of never acting on it and it is done.
- **On home, a showing is a tip that stood twenty seconds or more on a row you could see.**
Passing through home for a second or two is not a showing, however many times you do it,
and a row deciding while home is not in front is not one either. Until 2026-09-22 every
Expand Down Expand Up @@ -99,7 +121,7 @@ later when the ring comes round. It jumps once and then takes its turn like the

## Every hint codeaf can show, and what makes each one go away

There are twenty-two, one list for both boxes. Each one says the moment it first appears
There are twenty-three, one list for both boxes. Each one says the moment it first appears
and the gesture that retires it. The list is the program's own table (the surface refuses to
build if the two disagree), so a tip you saw is on it word for word.

Expand All @@ -120,6 +142,10 @@ build if the two disagree), so a tip you saw is on it word for word.
a conversation. Retired when you run `/resume`.
- `/standing turns a message into a rule work must follow` — once this directory has three or more earlier
conversations. Retired when a standing order is made or the standing page opened.
- `space space takes you back to home` — after the first exchange, in a conversation only
(never on home itself). Retired the first time two spaces in an empty box open home,
from a conversation or from a place; reaching home by `/home` or the tab does not retire
it. It is the first tip a conversation says, ahead of `/task`.
- `/task starts a single-shot task on the side` — after the first exchange. Retired when
`/task` is typed, bare or with a brief.
- `ctrl+enter makes your message a rule instead of a request` — retired when a standing
Expand Down Expand Up @@ -210,12 +236,14 @@ for a chord that works only in a conversation and only over a making-shaped sent
## Turn off hints — stop showing tips, disable the hints, the disable hints row

Open the settings panel with `/settings` (or `ctrl+,`), go to the **Workspace** tab, and flip
the **disable hints** row on. Enter or space toggles it; it is off by default, which means
the tips show. (Until 2026-09-22 it was a **hints** row on the Display tab, on by default.)
The change lands at the end of the next turn. On silences the tips — over a conversation's
the **disable hints** row on. The line under the row reads
`disable💡 tips everywhere (requires restart)`. Enter or space toggles it; it is off by
default, which means the tips show. (Until 2026-09-22 it was a **hints** row on the Display
tab, on by default.) Inside a running codeaf the change lands at the end of the next turn,
in every conversation and on home alike; restarting codeaf is the only way to have it at once. On silences the tips — over a conversation's
box and over home's alike — and the what's-new lines together; it does not touch the keys
row's own words for a live state — `esc interrupt` and the rest are not hints and cannot
be turned off. From the terminal, `codeaf config` shows the same row under the same name.
be turned off.

Turning the row back off shows whatever is due. Tips you had already retired stay retired.

Expand Down
11 changes: 8 additions & 3 deletions internal/manual/chat/home.md
Original file line number Diff line number Diff line change
Expand Up @@ -1413,9 +1413,9 @@ home and every two minutes at rest, and each tip goes away for good the first ti
what it names. It sits at the right, led by a bulb and closed by a small cross: clicking it
means ENOUGH FOR NOW, and the row stays blank until you leave home and come back, when a
different tip is there. The tip you put away keeps its whole allowance and comes round
again. **A conversation says its tips differently**: there the tip is the lowest rung of
the keys row at the very foot, with no bulb, no cross and no clock — it is simply there
whenever nothing else is happening. Both rows draw from the same one list. The whole list,
again. **A conversation says its tips differently**: its tip stands at the right end of
the keys row, over the project, with the bulb and the cross, and it appears only after
15 seconds in which you have not pressed, clicked, scrolled or pasted anything. Both rows draw from the same one list. The whole list,
what makes each one appear and disappear, and the **disable hints** row on the Workspace
tab that turns them off, are on the *hints and tips* page.

Expand Down Expand Up @@ -1475,6 +1475,11 @@ and not decoration. It also goes while a turn is running, where the same row has
more urgent to say (`esc interrupt`); the gesture still works then, it is just not being
advertised. (Until 2026-09-17 these words were the right end of the rule above the box.)

**It stays when a tip is showing.** A conversation's tip covers the project at the keys
row's right end, never the keys. Until 2026-09-24 a tip took the whole row and the
door went with it. One of the tips is about the door itself:
`space space takes you back to home`.

**You can click it.** A press on the words `space space home` opens home; a press on the
rule beside them is a press on a rule.

Expand Down
3 changes: 2 additions & 1 deletion internal/manual/chat/keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -2203,7 +2203,8 @@ back in it.

When the box is empty, the keys row under the box says so:
`/ commands · space space home`, after any effort, approvals and chats hints. Clicking
`space space home` opens home; that clause vanishes as soon as you type.
`space space home` opens home; that clause vanishes as soon as you type. A tip never takes
its place: a conversation's tip covers the project at the row's right end instead.

**The door does not ask what the machine holds.** It is open on a machine with only this
conversation and on one with none, from the first minute, and starting a second
Expand Down
6 changes: 3 additions & 3 deletions internal/manual/chat/places.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,9 @@ on home and in conversations. On both boxes `project: <path>` is at the right en
conversation opens, a conversation's names its own workspace. The keys keep their room: a
long path truncates on the right, and the field disappears if there is less than a word
of room. **On home** the dim line above the rule, when there is one, is a tip (see *hints
and tips*), there from the first minute. **In a conversation** the tip is not that row at
all: it is the lowest rung of the keys row itself, taking the slot from the rest state
whenever nothing else is happening. The bottom row names the available project, effort
and tips*), there from the first minute. **In a conversation** the tip covers `project:` at the
keys row's right end, bulb and cross included, after 15 quiet seconds, and the project is
back when it goes. The bottom row names the available project, effort
and approval controls; the cells can also be pressed:

| cell | chord | or |
Expand Down
47 changes: 47 additions & 0 deletions internal/tui3/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2311,6 +2311,16 @@ type app struct {
footRow int
tipRow int
tipCloseSpan hudSpan
// chatTipClose is the columns of the cross on a conversation's tip, at the
// right end of its keys row (footswap.go's [app.hintRow]). tipQuietFrom is
// when this window last stirred, and tipAlarm the one pending alarm of the
// conversation's quiet clock, or zero (notice.go's THE CONVERSATION'S TIP).
// chatTipDrawn is the id of the tip the keys row last drew, or "", which is
// what a showing is counted from ([app.chatTipCount]).
chatTipClose hudSpan
chatTipDrawn string
tipQuietFrom time.Time
tipAlarm time.Time
// targetEffortSpan and targetApprovalSpan are the rung's and the gate's
// columns on that same line — the draft's twins of [app.seamEffortSpan] and
// [app.seamApprovalSpan] (boxseam.go), recorded on the same bargain.
Expand Down Expand Up @@ -3186,6 +3196,10 @@ func (a *app) Init() tea.Cmd {
// replaying the events that made them, so no arrival ever fires for them
// (imagepreview.go's [app.learnShownPictures], learned.go).
a.learnShownPictures()
// AND THE WINDOW HAS JUST OPENED, which is when somebody starts reading
// it: a conversation's tip waits out its quiet from here, as it does after
// an answer (notice.go's THE CONVERSATION'S TIP).
a.stirred()
// The repository is asked ONCE here and then only at turn ends. A branch is
// a fact that changes when a person changes it, and a person who checks out
// a branch mid-turn is between two turns by the time it matters.
Expand Down Expand Up @@ -3318,6 +3332,14 @@ func (a *app) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if a.levelsWaiting() || a.waiting() || a.formingCardLive() {
cmd = tea.Batch(cmd, a.wake())
}
// AND THE CONVERSATION'S TIP WAITS OUT ITS QUIET HERE, on the one line every
// message passes: whatever the message did — a key restarting the wait, a
// turn ending, the alarm itself landing early — this is where the next
// alarm is set, and only ever one (notice.go's [app.chatTipAlarm]).
if alarm := a.chatTipAlarm(); alarm != nil {
cmd = tea.Batch(cmd, alarm)
}
a.chatTipCount()
// AND A CLOCK SOMEBODY STOPPED IS TOLD TO THE ENGINE HERE, from a command
// rather than from inside the key routine that took the key
// (questionhold.go's [app.takeQuestionHolds] says why it is this line).
Expand Down Expand Up @@ -3403,8 +3425,17 @@ func (a *app) route(msg tea.Msg) (tea.Model, tea.Cmd) {
// on the way out.
return a, a.quit()

case chatTipDueMsg:
// THE QUIET CLOCK'S ALARM. It carries nothing: the frame drawn after it
// asks the clock afresh, and a wait a key restarted meanwhile is set
// again for what is left of it ([app.chatTipAlarm], in [app.Update]).
a.tipAlarm = time.Time{}
a.touch()
return a, nil

case tea.KeyPressMsg:
a.sawAPerson()
a.stirred()
// AND THE HAND IS STAMPED HERE, because this is the only line every
// keypress passes through, and what the question block needs to know
// is whether somebody is at the keyboard at all:
Expand Down Expand Up @@ -3541,6 +3572,7 @@ func (a *app) route(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, nil

case tea.PasteMsg:
a.stirred()
// Bracketed paste, whole, in one message — the parser coalesced the keys
// between the brackets for us, so the newlines inside it are text and not
// a stack of enters. Inside an open bracket it JOINS what the bracket has
Expand Down Expand Up @@ -3668,6 +3700,7 @@ func (a *app) route(msg tea.Msg) (tea.Model, tea.Cmd) {

case tea.MouseWheelMsg:
a.clearPlaceRowHover()
a.stirred()
a.placePointer.suspended = true
// THE CONTEXT CHOOSER OWNS THE WHEEL WHILE IT IS UP, and it owns it over
// the WHOLE screen: the conversation under a modal is not live, so a wheel
Expand Down Expand Up @@ -3874,6 +3907,11 @@ func (a *app) route(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.MouseClickMsg:
a.clearPlaceRowHover()
a.sawAPerson()
// THE QUIET CLOCK RESTARTS AFTER THE PRESS IS ANSWERED, not before. A
// press is resolved against the chrome laid out afresh ([app.chromeAt]),
// and a clock restarted first would have taken the tip row — and the
// cross being pressed — off that layout (notice.go's [app.chatTipPress]).
defer a.stirred()
// AND IT OWNS THE PRESS, on the same terms and for a sharper reason: a
// press that fell through a modal would switch a tab, open a tool call or
// answer a question behind a sheet somebody is looking at
Expand Down Expand Up @@ -4062,6 +4100,9 @@ func (a *app) route(msg tea.Msg) (tea.Model, tea.Cmd) {
// rather than in the box, and column-aware for the same reason: the
// row it rides is empty everywhere else, and empty space on this
// surface is not a gesture (jumpchip.go).
if a.chatTipPress(msg.Mouse().X, msg.Mouse().Y) {
return a, nil
}
if a.jumpPress(msg.Mouse().X, msg.Mouse().Y) {
return a, nil
}
Expand Down Expand Up @@ -5825,6 +5866,9 @@ func (a *app) settle() tea.Cmd {
a.hopQuick = config.QuickSwitchAt(a.profileDir)
a.askWait = a.consentWait()
a.notices.enabled = config.HintsAt(a.profileDir)
// AND THE ANSWER HAS JUST ARRIVED, which is when somebody starts reading
// it: the conversation's tip waits its quiet out from here (notice.go).
a.stirred()
// A turn ending is the moment most hints become true — the answer was long,
// the window is half full, the money is real — so it is the event they are
// decided on, and it is the turn [noticeGap] is counted in (notice.go).
Expand Down Expand Up @@ -7487,6 +7531,9 @@ func (a *app) openConversation(file string) (Conversation, bool, error) {
// draft and the trio. That is what `whole` says, and it is the caller's own fact
// rather than something guessed from the fields.
func (a *app) takeUp(conv Conversation, whole bool) {
// ANOTHER CONVERSATION IS ANOTHER ROW: a cross pressed over the one being
// left does not blank the one arriving (notice.go's [app.liftChatTipCross]).
a.liftChatTipCross()
if conv.Agent != nil {
a.agent = conv.Agent
// AND EVERY DOOR ASKED OF THE CONVERSATION BEING PUT DOWN IS NOW A DOOR
Expand Down
Loading
Loading