From beabececb4983f36901218a4fa2230e51c5de571 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Fri, 25 Sep 2026 11:59:28 -0400 Subject: [PATCH 1/4] chat: groom the standing card ask and its yes clause MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lead said "wants to keep an eye on" on every standing card, which was wrong about most of them: a one-off reminder watches nothing, and a rule that never wakes watches nothing either. The lead now says what a yes binds — "wants to set this up" — for every kind. The yes clause beside the chip promised "it keeps happening until you stop it" on cards whose item runs once at a moment and then retires. The clause now reads the item: at-things get "it happens at the time, and then it retires", and every other kind keeps the old clause. The manual pages that quote the card move with it, and the tests that assert the lead's words say the new words. --- internal/manual/chat/home.md | 2 +- internal/manual/chat/keeping-an-eye.md | 24 +++++++++--------- internal/manual/chat/questions.md | 14 +++++------ internal/session/tools_standing.go | 2 +- internal/tui3/homeband_answer_test.go | 6 ++--- internal/tui3/standing.go | 34 +++++++++++++++++++++++--- 6 files changed, 53 insertions(+), 29 deletions(-) diff --git a/internal/manual/chat/home.md b/internal/manual/chat/home.md index fcb5ee87a5..3f53671100 100644 --- a/internal/manual/chat/home.md +++ b/internal/manual/chat/home.md @@ -1647,7 +1647,7 @@ The chips are the ones the question has: - It is **waiting for permission to run something**: `1 allow once 2 always 3 deny`. - It is **asking whether to start a task**: `1 yes 2 no`. -- It is **asking whether to keep something going**. The chips are that card's own +- It is **asking whether to set something up**. The chips are that card's own words: a repeating check is `1 Set it up · `, `3 Only now, don't repeat`, `0 Don't set it up`. A one-off reminder has no once, and its no is `Don't remind me`. A watch's no is `Don't watch`. A rule's no is `Don't keep it`. **`0` is how you say diff --git a/internal/manual/chat/keeping-an-eye.md b/internal/manual/chat/keeping-an-eye.md index faf713c293..7630c9b8d6 100644 --- a/internal/manual/chat/keeping-an-eye.md +++ b/internal/manual/chat/keeping-an-eye.md @@ -145,9 +145,9 @@ to leave" the whole content of the request is the **6**. Doing it now says neither of those cards offers `3`: ``` -? wants to remind you - 1 Remind me at 6 Reminds you then. Nothing repeats. - 0 Don't remind me You are not reminded. +? wants to set this up: remind me at 6 to leave + 1 yes, set it up it happens at the time, and then it retires + 0 no nothing happens, now or later ``` Two answers, `1` and `0`, and `3` does nothing. A rule is the same shape: it @@ -290,13 +290,11 @@ wake, where it reaches, what it costs. The **question sits above the message box every decision on this screen is put, and every answer carries what it costs beside it: ``` -? wants to set up a repeating check - every Monday at 9, post the standup note from the git log - Mondays at 9am · about $0.02 a run - 1 Set it up · Mondays at 9am It repeats on that cadence until you stop it. - 3 Only now, don't repeat Runs the check one time now. Nothing repeats. - 0 Don't set it up Nothing is set up, and nothing runs. - esc later · o Change… · ? clarify +? wants to set this up: every Monday at 9, post the standup note from the git log + 1 yes, set it up it keeps happening until you stop it + 3 just once it happens now, and nothing is kept + 0 no nothing happens, now or later + esc later · o other · ? clarify ``` The first line says what kind of thing it is. The second line is what it does. @@ -312,9 +310,9 @@ The third is when, and what one time costs. Each answer is a row of its own and **a click anywhere along it takes that answer**. The digit takes it too. -Answering leaves a line where the question was, the kind and the button you pressed, -and the card in the conversation settles with that button and what it came to: -`Set it up · Mondays at 9am · set up`. +Answering leaves a line where the question was — `✓ wants to set this up: … → yes, +set it up · you · 14:02` — and the card in the conversation settles with the answer and what +it came to on its bottom edge: `yes, set it up · set up`. ## How do I decline a standing card or say no to a reminder — 0, esc, or the no on the card diff --git a/internal/manual/chat/questions.md b/internal/manual/chat/questions.md index 5a21e10a42..91a33efb28 100644 --- a/internal/manual/chat/questions.md +++ b/internal/manual/chat/questions.md @@ -1270,13 +1270,13 @@ has all of the above: the digits, `esc` for later, the chip, the receipt, the settle guard, the narrow card and the phone sheet. `permissions` is its own page and states what each answer banks. -**So does the standing card.** The first line says the kind: `wants to remind you`, -`wants to set up a repeating check`, `wants to watch for something`, or -`wants to keep a rule`. Under that is what it does, then when and what one time -costs, and the `where ·` band. The answers sit above the box. A repeating check's -yes is `Set it up · `, its once is `Only now, don't repeat` (this used -to say `just once`), and its no is `Don't set it up`. A reminder and a rule have -no once. `o` starts an updated request. `enter` sends your words back to be +**So does the standing card** — `wants to set this up:` with `1 yes, set it up`, +`3 just once` where the item can be done once at all, and `0 no`. What is left in +the conversation is the card itself: your own words, the `when ·`, `where ·` and +`costs ·` bands, and the meter where the engine put a deadline on it. The answers +are up above the box with everything else you are being waited on for, and each +one says what it costs beside it. `o` starts an updated request — it turns +the box into the correction lane, and `enter` sends your words back to be re-proposed. **`esc` on a standing card means *later* now, and it used to mean no.** It is diff --git a/internal/session/tools_standing.go b/internal/session/tools_standing.go index fc91dfe966..80495e83e8 100644 --- a/internal/session/tools_standing.go +++ b/internal/session/tools_standing.go @@ -1543,7 +1543,7 @@ func (a *Agent) standingAsk(id uint64, notice StandingNotice) Question { // StandingAskLead is the old opening, kept so a reader of an older line can // find what a card used to say. New cards open with [StandingHead]. -const StandingAskLead = "wants to keep an eye on: " +const StandingAskLead = "wants to set this up: " // StandingAskReason is why the card is up, in the one sentence that is true of // every standing card there is. The when and the cost are the card's to show, in diff --git a/internal/tui3/homeband_answer_test.go b/internal/tui3/homeband_answer_test.go index ba6191743e..96d08f10e5 100644 --- a/internal/tui3/homeband_answer_test.go +++ b/internal/tui3/homeband_answer_test.go @@ -335,7 +335,7 @@ func TestHomeCanSayNoToAStandingCard(t *testing.T) { When: standing.When{Kind: standing.WhenProbe}, Does: standing.Action{Kind: standing.ActionSay}, } - lab := newAnswerLab(t, standingQuestion(9, watch, "wants to keep an eye on: tell me when ci goes red"), time.Now()) + lab := newAnswerLab(t, standingQuestion(9, watch, "wants to set this up: tell me when ci goes red"), time.Now()) text := homeText(lab.a) for _, chip := range []string{"1 Watch for it", "3 Check once now", "0 Don't watch"} { if !strings.Contains(text, chip) { @@ -370,7 +370,7 @@ func TestHomeCanSayNoToAReminderThatOffersNoOnce(t *testing.T) { When: standing.When{Kind: standing.WhenAt}, Does: standing.Action{Kind: standing.ActionSay}, } - lab := newAnswerLab(t, standingQuestion(9, reminder, "wants to keep an eye on: remind me at 6 to leave"), time.Now()) + lab := newAnswerLab(t, standingQuestion(9, reminder, "wants to set this up: remind me at 6 to leave"), time.Now()) text := homeText(lab.a) if strings.Contains(text, "Check once now") || strings.Contains(text, "Only now") { t.Fatalf("a one-off reminder was offered `once` from home:\n%s", text) @@ -397,7 +397,7 @@ func TestHomeDecliningItsOwnStandingCardSettlesItAsNotSetUp(t *testing.T) { Does: standing.Action{Kind: standing.ActionSay}, } mine := lab.session("-tmp-alpha", "aaaa000000000001", "porting the resume picker", "/tmp/alpha", now) - lab.asking("-tmp-alpha", "aaaa000000000001", standingQuestion(11, item, "wants to keep an eye on: tell me when ci goes red"), now) + lab.asking("-tmp-alpha", "aaaa000000000001", standingQuestion(11, item, "wants to set this up: tell me when ci goes red"), now) agent := &standFake{fakeAgent: &fakeAgent{model: "m"}} a := newTestApp(agent) diff --git a/internal/tui3/standing.go b/internal/tui3/standing.go index 0e91191dad..1aa6224fe7 100644 --- a/internal/tui3/standing.go +++ b/internal/tui3/standing.go @@ -182,10 +182,14 @@ const ( // bands two rows above are for. Under a card that draws both, that sentence // was the two-renderings defect one size smaller. What no band can say is how // LONG each answer lasts, so that is what is left. - standYesCost = "It repeats on that cadence until you stop it." - standOnceCost = "Runs the check one time now. Nothing repeats." - standNoCost = "Nothing is set up, and nothing runs." - standChangeCost = "Say a different time or place. Nothing is set up yet." + standYesCost = "it keeps happening until you stop it" + // standYesOnceCost is the yes clause for an AT-item, which runs once at its + // moment and then retires (internal/standing's kinds). It is a separate + // constant because [standAnswerCost] chooses it by the item. + standYesOnceCost = "it happens at the time, and then it retires" + standOnceCost = "Runs the check one time now. Nothing repeats." + standNoCost = "Nothing is set up, and nothing runs." + standChangeCost = "Say a different time or place. Nothing is set up yet." ) // The glyphs a standing row wears, and their stand-ins on a terminal that @@ -593,6 +597,28 @@ func standAnswerWord(item standing.Item, key string) string { return "" } +// standAnswerCost is what one answer costs, by the key that takes it. +// +// THE YES CLAUSE READS THE ITEM, because how long a yes lasts is a fact about +// the item and not about the key. An at-thing happens once at its moment and +// then retires — "keeps happening until you stop it" promised a forever the +// when band two rows above had already ruled out — and every other kind really +// does go on until it is stopped. +func standAnswerCost(key string, item standing.Item) string { + switch key { + case standYesKey: + if item.When.Kind == standing.WhenAt { + return standYesOnceCost + } + return standYesCost + case session.StandingOnceKey: + return standOnceCost + case session.StandingNoKey: + return standNoCost + } + return "" +} + // standYesKey is the digit that sets a standing card up. It is `1` on every // question this surface asks and is named here so the clause beside it is not // keyed off a literal ([standAnswerCost]). From 27109dd8d43f74f443e9b5a3fd61823e23ec28ee Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Sat, 26 Sep 2026 02:16:02 -0400 Subject: [PATCH 2/4] docs(changelog): add unreleased entry for PR #1520 --- .../changes/unreleased/1520-groom-standing-prompts.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/changes/unreleased/1520-groom-standing-prompts.md diff --git a/docs/changes/unreleased/1520-groom-standing-prompts.md b/docs/changes/unreleased/1520-groom-standing-prompts.md new file mode 100644 index 0000000000..0a88253a4c --- /dev/null +++ b/docs/changes/unreleased/1520-groom-standing-prompts.md @@ -0,0 +1,11 @@ +--- +kind: changed +title: groom the standing card ask and its yes clause +pr: 1520 +surface: [chat, docs] +invalidates: + - "The standing card ask led with 'wants to keep an eye on:'. It now leads with 'wants to set this up:' across all kinds." + - "The standing card yes clause unconditionally stated 'it keeps happening until you stop it'. For one-off scheduled items, it now states 'it happens at the time, and then it retires'." +--- + +The standing card lead was inaccurate for rules and reminders that do not keep an eye on anything. The lead is now generic across all kinds, and the confirmation clause branches on one-off schedules rather than promising recurring execution. From d74f5e9d3eec30bc9dba61afd01c0605be61de51 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Sun, 27 Sep 2026 10:43:14 -0400 Subject: [PATCH 3/4] docs: align standing explanations with engine-owned card wording --- .../unreleased/1520-groom-standing-prompts.md | 11 +++--- internal/manual/chat/home.md | 2 +- internal/manual/chat/keeping-an-eye.md | 24 +++++++------ internal/manual/chat/questions.md | 22 ++++++++---- internal/session/tools_standing.go | 2 +- internal/tui3/homeband_answer_test.go | 6 ++-- internal/tui3/standing.go | 34 +++---------------- 7 files changed, 41 insertions(+), 60 deletions(-) diff --git a/docs/changes/unreleased/1520-groom-standing-prompts.md b/docs/changes/unreleased/1520-groom-standing-prompts.md index 0a88253a4c..5f3e2ded93 100644 --- a/docs/changes/unreleased/1520-groom-standing-prompts.md +++ b/docs/changes/unreleased/1520-groom-standing-prompts.md @@ -1,11 +1,8 @@ --- -kind: changed -title: groom the standing card ask and its yes clause +kind: docs +title: clarify when accepting a standing card repeats work pr: 1520 -surface: [chat, docs] +surface: [docs] invalidates: - - "The standing card ask led with 'wants to keep an eye on:'. It now leads with 'wants to set this up:' across all kinds." - - "The standing card yes clause unconditionally stated 'it keeps happening until you stop it'. For one-off scheduled items, it now states 'it happens at the time, and then it retires'." + - "Accepting any standing card means a repeating job. Reminder, rule, and repeating-check cards already have different engine-owned answer consequences; the manual now explains that distinction directly." --- - -The standing card lead was inaccurate for rules and reminders that do not keep an eye on anything. The lead is now generic across all kinds, and the confirmation clause branches on one-off schedules rather than promising recurring execution. diff --git a/internal/manual/chat/home.md b/internal/manual/chat/home.md index 3f53671100..fcb5ee87a5 100644 --- a/internal/manual/chat/home.md +++ b/internal/manual/chat/home.md @@ -1647,7 +1647,7 @@ The chips are the ones the question has: - It is **waiting for permission to run something**: `1 allow once 2 always 3 deny`. - It is **asking whether to start a task**: `1 yes 2 no`. -- It is **asking whether to set something up**. The chips are that card's own +- It is **asking whether to keep something going**. The chips are that card's own words: a repeating check is `1 Set it up · `, `3 Only now, don't repeat`, `0 Don't set it up`. A one-off reminder has no once, and its no is `Don't remind me`. A watch's no is `Don't watch`. A rule's no is `Don't keep it`. **`0` is how you say diff --git a/internal/manual/chat/keeping-an-eye.md b/internal/manual/chat/keeping-an-eye.md index 7630c9b8d6..faf713c293 100644 --- a/internal/manual/chat/keeping-an-eye.md +++ b/internal/manual/chat/keeping-an-eye.md @@ -145,9 +145,9 @@ to leave" the whole content of the request is the **6**. Doing it now says neither of those cards offers `3`: ``` -? wants to set this up: remind me at 6 to leave - 1 yes, set it up it happens at the time, and then it retires - 0 no nothing happens, now or later +? wants to remind you + 1 Remind me at 6 Reminds you then. Nothing repeats. + 0 Don't remind me You are not reminded. ``` Two answers, `1` and `0`, and `3` does nothing. A rule is the same shape: it @@ -290,11 +290,13 @@ wake, where it reaches, what it costs. The **question sits above the message box every decision on this screen is put, and every answer carries what it costs beside it: ``` -? wants to set this up: every Monday at 9, post the standup note from the git log - 1 yes, set it up it keeps happening until you stop it - 3 just once it happens now, and nothing is kept - 0 no nothing happens, now or later - esc later · o other · ? clarify +? wants to set up a repeating check + every Monday at 9, post the standup note from the git log + Mondays at 9am · about $0.02 a run + 1 Set it up · Mondays at 9am It repeats on that cadence until you stop it. + 3 Only now, don't repeat Runs the check one time now. Nothing repeats. + 0 Don't set it up Nothing is set up, and nothing runs. + esc later · o Change… · ? clarify ``` The first line says what kind of thing it is. The second line is what it does. @@ -310,9 +312,9 @@ The third is when, and what one time costs. Each answer is a row of its own and **a click anywhere along it takes that answer**. The digit takes it too. -Answering leaves a line where the question was — `✓ wants to set this up: … → yes, -set it up · you · 14:02` — and the card in the conversation settles with the answer and what -it came to on its bottom edge: `yes, set it up · set up`. +Answering leaves a line where the question was, the kind and the button you pressed, +and the card in the conversation settles with that button and what it came to: +`Set it up · Mondays at 9am · set up`. ## How do I decline a standing card or say no to a reminder — 0, esc, or the no on the card diff --git a/internal/manual/chat/questions.md b/internal/manual/chat/questions.md index 91a33efb28..05ad4a6646 100644 --- a/internal/manual/chat/questions.md +++ b/internal/manual/chat/questions.md @@ -1270,13 +1270,13 @@ has all of the above: the digits, `esc` for later, the chip, the receipt, the settle guard, the narrow card and the phone sheet. `permissions` is its own page and states what each answer banks. -**So does the standing card** — `wants to set this up:` with `1 yes, set it up`, -`3 just once` where the item can be done once at all, and `0 no`. What is left in -the conversation is the card itself: your own words, the `when ·`, `where ·` and -`costs ·` bands, and the meter where the engine put a deadline on it. The answers -are up above the box with everything else you are being waited on for, and each -one says what it costs beside it. `o` starts an updated request — it turns -the box into the correction lane, and `enter` sends your words back to be +**So does the standing card.** The first line says the kind: `wants to remind you`, +`wants to set up a repeating check`, `wants to watch for something`, or +`wants to keep a rule`. Under that is what it does, then when and what one time +costs, and the `where ·` band. The answers sit above the box. A repeating check's +yes is `Set it up · `, its once is `Only now, don't repeat` (this used +to say `just once`), and its no is `Don't set it up`. A reminder and a rule have +no once. `o` starts an updated request. `enter` sends your words back to be re-proposed. **`esc` on a standing card means *later* now, and it used to mean no.** It is @@ -1398,3 +1398,11 @@ older blocks — the connect offer, with its own answers row and its own keys gone. The one card in this program that still answers to keys of its own is the intake form `/subharness` opens for a saved program, which is a fullscreen page with fields to fill in rather than a question above the box. + +## Does accepting a standing card mean it repeats? + +The answer depends on the kind shown on the card. A one-off reminder says +`Reminds you then. Nothing repeats.` A repeating check says +`It repeats on that cadence until you stop it.` A rule says +`The rule is kept until you stop it.` These are the engine's own answer +consequences, shared by the home screen and the conversation. diff --git a/internal/session/tools_standing.go b/internal/session/tools_standing.go index 80495e83e8..fc91dfe966 100644 --- a/internal/session/tools_standing.go +++ b/internal/session/tools_standing.go @@ -1543,7 +1543,7 @@ func (a *Agent) standingAsk(id uint64, notice StandingNotice) Question { // StandingAskLead is the old opening, kept so a reader of an older line can // find what a card used to say. New cards open with [StandingHead]. -const StandingAskLead = "wants to set this up: " +const StandingAskLead = "wants to keep an eye on: " // StandingAskReason is why the card is up, in the one sentence that is true of // every standing card there is. The when and the cost are the card's to show, in diff --git a/internal/tui3/homeband_answer_test.go b/internal/tui3/homeband_answer_test.go index 96d08f10e5..ba6191743e 100644 --- a/internal/tui3/homeband_answer_test.go +++ b/internal/tui3/homeband_answer_test.go @@ -335,7 +335,7 @@ func TestHomeCanSayNoToAStandingCard(t *testing.T) { When: standing.When{Kind: standing.WhenProbe}, Does: standing.Action{Kind: standing.ActionSay}, } - lab := newAnswerLab(t, standingQuestion(9, watch, "wants to set this up: tell me when ci goes red"), time.Now()) + lab := newAnswerLab(t, standingQuestion(9, watch, "wants to keep an eye on: tell me when ci goes red"), time.Now()) text := homeText(lab.a) for _, chip := range []string{"1 Watch for it", "3 Check once now", "0 Don't watch"} { if !strings.Contains(text, chip) { @@ -370,7 +370,7 @@ func TestHomeCanSayNoToAReminderThatOffersNoOnce(t *testing.T) { When: standing.When{Kind: standing.WhenAt}, Does: standing.Action{Kind: standing.ActionSay}, } - lab := newAnswerLab(t, standingQuestion(9, reminder, "wants to set this up: remind me at 6 to leave"), time.Now()) + lab := newAnswerLab(t, standingQuestion(9, reminder, "wants to keep an eye on: remind me at 6 to leave"), time.Now()) text := homeText(lab.a) if strings.Contains(text, "Check once now") || strings.Contains(text, "Only now") { t.Fatalf("a one-off reminder was offered `once` from home:\n%s", text) @@ -397,7 +397,7 @@ func TestHomeDecliningItsOwnStandingCardSettlesItAsNotSetUp(t *testing.T) { Does: standing.Action{Kind: standing.ActionSay}, } mine := lab.session("-tmp-alpha", "aaaa000000000001", "porting the resume picker", "/tmp/alpha", now) - lab.asking("-tmp-alpha", "aaaa000000000001", standingQuestion(11, item, "wants to set this up: tell me when ci goes red"), now) + lab.asking("-tmp-alpha", "aaaa000000000001", standingQuestion(11, item, "wants to keep an eye on: tell me when ci goes red"), now) agent := &standFake{fakeAgent: &fakeAgent{model: "m"}} a := newTestApp(agent) diff --git a/internal/tui3/standing.go b/internal/tui3/standing.go index 1aa6224fe7..0e91191dad 100644 --- a/internal/tui3/standing.go +++ b/internal/tui3/standing.go @@ -182,14 +182,10 @@ const ( // bands two rows above are for. Under a card that draws both, that sentence // was the two-renderings defect one size smaller. What no band can say is how // LONG each answer lasts, so that is what is left. - standYesCost = "it keeps happening until you stop it" - // standYesOnceCost is the yes clause for an AT-item, which runs once at its - // moment and then retires (internal/standing's kinds). It is a separate - // constant because [standAnswerCost] chooses it by the item. - standYesOnceCost = "it happens at the time, and then it retires" - standOnceCost = "Runs the check one time now. Nothing repeats." - standNoCost = "Nothing is set up, and nothing runs." - standChangeCost = "Say a different time or place. Nothing is set up yet." + standYesCost = "It repeats on that cadence until you stop it." + standOnceCost = "Runs the check one time now. Nothing repeats." + standNoCost = "Nothing is set up, and nothing runs." + standChangeCost = "Say a different time or place. Nothing is set up yet." ) // The glyphs a standing row wears, and their stand-ins on a terminal that @@ -597,28 +593,6 @@ func standAnswerWord(item standing.Item, key string) string { return "" } -// standAnswerCost is what one answer costs, by the key that takes it. -// -// THE YES CLAUSE READS THE ITEM, because how long a yes lasts is a fact about -// the item and not about the key. An at-thing happens once at its moment and -// then retires — "keeps happening until you stop it" promised a forever the -// when band two rows above had already ruled out — and every other kind really -// does go on until it is stopped. -func standAnswerCost(key string, item standing.Item) string { - switch key { - case standYesKey: - if item.When.Kind == standing.WhenAt { - return standYesOnceCost - } - return standYesCost - case session.StandingOnceKey: - return standOnceCost - case session.StandingNoKey: - return standNoCost - } - return "" -} - // standYesKey is the digit that sets a standing card up. It is `1` on every // question this surface asks and is named here so the clause beside it is not // keyed off a literal ([standAnswerCost]). From 82e34d5dcca434536398a08c0a2849c49a4f6ae0 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Sun, 27 Sep 2026 10:46:53 -0400 Subject: [PATCH 4/4] docs: use the supported changelog category --- docs/changes/unreleased/1520-groom-standing-prompts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes/unreleased/1520-groom-standing-prompts.md b/docs/changes/unreleased/1520-groom-standing-prompts.md index 5f3e2ded93..e97c2cb5e4 100644 --- a/docs/changes/unreleased/1520-groom-standing-prompts.md +++ b/docs/changes/unreleased/1520-groom-standing-prompts.md @@ -1,5 +1,5 @@ --- -kind: docs +kind: changed title: clarify when accepting a standing card repeats work pr: 1520 surface: [docs]