Skip to content

Groom: TUI question component UX — no navigation between questions, long pause after final question, awkward up/down arrow behavior #830

Closed

Description

@santoshkumarradha

Summary

The question component (the TUI surface where the agent asks the person questions during a task) has several UX problems that need grooming before any implementation work starts.

Observed problems

  1. No way to move between questions — once a question is shown there is no way to go back to a previous question or jump forward. Answering wrong means living with it or restarting the whole flow.
  2. Long pause after the final question — after answering the last question, the TUI sits for a noticeable period with no visible feedback before anything happens. Feels frozen; we should show what is happening (or fix the latency itself).
  3. Up/down arrow behavior is weird — arrow-key navigation doesn't behave the way a person expects (selection highlight, wrap-around, or focus semantics are off). Needs a defined key contract.

What this issue is

A grooming item, not a task: the expected behavior for each of the three problems has to be decided (navigation model, post-final-question feedback, arrow-key contract) before anyone breaks it into tasks.

Grooming questions to settle

  • Should people be able to navigate back and edit earlier answers? If yes, what happens to downstream answers?
  • What should happen between the final answer and the next visible state — spinner, progress, or eliminating the pause?
  • What is the intended key contract for the question list (up/down, tab, vim keys)? Should it match the rest of the TUI conventions?

Notes

  • Reported by hand during use of the TUI; no replication steps beyond "answer questions in a chat/task flow."
  • Area: internal/tui3 question component.

Drafted with CodeAF · reviewed and owned by the author

Activity

  1. added
    featureWork that adds a capability; developers break it into tasks
    area:chatThe v3 surface a person sits in front of (internal/tui3)
    needs a rulingAn owner decision comes before any code — do not start
    on Sep 10, 2026
  2. santoshkumarradha commented on Sep 11, 2026

    @santoshkumarradha
    MemberAuthor

    Recon findings (2026-09-11, dev @ 39039b9)

    Four read-only passes: engine, rendering + keys, freeze/latency, design contract. Nothing changed yet. This issue tracks the redesign + fixes; close it with the PR.

    Defects — cause first

    B1. Enter → ~10 s freeze (likely a deadlock, not slowness). Enter calls ResolveQuestion over the wire synchronously inside bubbletea's Update (internal/tui3/question.go:2368). The engine wakes the turn and emits phase frames (session/loop.go:2508) that often beat the reply. The client reader hands each phase frame to a handler that calls p.Send (internal/remote/client.go:824-830 → internal/tui3/tui3.go:1325-1331, twin at 1287-1314); bubbletea v2's p.Send blocks until Update is free — and Update is waiting for the reply that same reader can no longer read. Only callDeadline = 10s (remote/client.go:79) breaks it → "the engine did not answer in time". The comment at question.go:2352-2366 already records "ten seconds to the millisecond". Dates from #747 (phase frames on the wire). Same exposure: AttachReplay on a conversation switch (observe.go:128-160), SetAutonomy, the room.
    Fix at the cause: non-blocking coalescing wake instead of p.Send in the reader (+ test: reader returns ≤50 ms while Update is busy); answer from a tea.Cmd and close the question at once (markQuestionSent exists); AST law: no wire door called directly from Update.

    B2. Every recorded answer busts the prompt cache. ResolveQuestion → recordDecision rewrites the system prompt (session/question.go:1571 → memory.go:1055), so the next request pays a cold prefix. Tool consent ("allow once") takes this road too. Also re-reads decisions.jsonl under a.mu and may call RememberScoped (a model call) synchronously.

    B3. Other post-answer waits. Landing accept runs the git merge inside the answer call (task_audit.go:2627-2687). An answer on a checkpoint round is followed by the markreader (~8 s "taking stock", checkpoint.go:2697). The reflex gate is not on this path.

    A1. Arrows that don't move (ranked):

    1. During the B1 freeze every key is dead, then lands late on whatever is next.
    2. "Away" rule (10 min without a keypress; mouse/scroll don't count, app.go:3066): a new question only notifies + bells (questiondelivery.go:158-168), is never pinned, and alt+a refuses because the count is 0 (question.go:3373). Nothing re-delivers it on return. Matches "I come back to a chat and the question has been asked".
    3. Room: ↓ moves focus but the page never scrolls to it (questionroom.go:1349/1709, app.go:3471).
    4. Batch sheet steals ↑↓/tab/digits behind a page or home — no off-screen guard (questionsheet.go:612-633).
    5. Checklist card has two pointers; arrows move one, space/tab act on the other (question.go:1308-1320, 2968).
    6. Second beat after always swallows every arrow (question.go:1684); 250 ms settle guard; text in the box → arrows go to the caret (:2677); phone width <60 moves the pointer but never draws it; no wrap at the list ends.

    Other correctness: ? ask-back on a model ask is queued as a steer that is read only after the tool returns, i.e. after the question is answered. "Make it a rule" writes a hard-coded reason into memory. Five lanes (connect, harness, subharness, subharness-ask, fuel) never emit answered/withdrawn on the questions stream → stale on other windows. Schema offers confidence low/medium/high, code reads sure/fairly/unsure → confidence never shown. Schema input.pairs has no field behind it. Click row math likely off by the receipt rows. Receipt offers c change that nothing handles. Ratify still counts in ? 1 question. A question page stays open after its question is answered or withdrawn. The first key typed into an empty box answers the question (a digit picks, d hands the call back).

    UX debt (owner's complaints, confirmed)

    • Pink = hueAsk #C08FE8 painted on whole rows: head, every label, every key, every · (styles.go:338, palette.ask/askBold, question.go:1021/1374/1476/1839). Breaks the design language's own "colour is stroke, never fill". ? is amber on the block and violet in the room; the manual says labels are "amber".
    • No hierarchy in hints: one row mixes answers, navigation, dismiss, extra verbs and the clock, all one hue and weight (questionOffer, question.go:1422). The rule above the block names the wrong keys for a model ask (esc interrupt; esc actually folds). Card key-row bolding is inverted (question.go:1843).
    • No containment / collapse: esc folds straight to the status chip; no in-place collapsed state.
    • Recommended barely marked: dim · suggested on the card, my pick · fairly sure in the room, nothing on the line, phone sheet, batch sheet, home chips, task record.
    • "Other / type your own" is hidden behind c or typing.
    • Room is one long column, top-anchored, no split; previews stack under each option, never beside.

    Architecture (for the redesign)

    • One object: session.Question (session/question.go:627) for 11 lanes: model ask, consent, task proposal, standing, landing/conflict, connect, harness, subharness, subharness-ask, fuel, recovery (borrows consent). Rich options: body, consequence, safe, widening, blocks (text/diagram/table/diff/image/layout), dimensions. Pick{key, reason, confidence, wouldChange}. Input: text/blanks/checklist/dial/secret.
    • Form (line/card/room/sheet) is a hint; the surface decides in app.questionForm (tui3/question.go:849). Every model ask with no form is a card (the gate requires a reason).
    • One ask = one question; a multi-question panel must be gathered from concurrent asks (batch sheet) or needs a schema change (prompt-diet caps apply, The prompt profile is a row, ask pays less for it, and the job footer is said once per change #844).
    • Laws a redesign keeps: never modal (box stays live), esc = later (never cancel; test-pinned), 250 ms settle, never move the box under typing, first window wins, pointer opens on the safe answer for irreversible gates.
    • A bordered panel needs an owner ruling amending docs/DESIGN-LANGUAGE.md ("What we refused: Borders"). Three dim rounded boxes already ship (/folder, ctrl+k card, onboarding) — should become one frame.
    • Redesign obligations: internal/manual/chat/questions.md (1043 lines, 55 probes), ~40 pinned strings in internal/e2e/tuiwords_test.go, the one key table internal/tui3/questionkeys.go, icon slots (pointer vs folded both ▸ today; checkbox/radio/recommended missing).

    Next: the view primitives + design prototypes, then the owner picks.

  3. santoshkumarradha commented on Sep 11, 2026

    @santoshkumarradha
    MemberAuthor

    Owner's picks (2026-09-11) → work started

    Pick sheet: https://claude.ai/code/artifact/24190a18-caa4-4a4c-9298-6917583944a4

    Decision Pick
    Frame A rounded frame hanging above the input (amends DESIGN-LANGUAGE "Borders"; one frame primitive, the three existing boxes move onto it)
    Colour C amber marks (? ▸ ◆), like home; violet leaves the question surface; words in ink
    Recommended A pointer opens on the pick, ◆ recommended at the right, reason/confidence under it only while focused (safe-answer law still wins on hands-only questions)
    Key hints A two tiers: ↑↓ choose · enter take it · esc later in the frame; secondary keys on one dim row below
    Your own answer A last row something else… becomes an input when focused
    Collapsed A folds in place to one titled rule above the input
    Evidence A split list | preview at ≥100 cols; unfolds under the row below 100
    Several questions A tabs + review, ←→ between questions, answers editable until sent
    Full page A two panes: answers left, focused answer's reasoning + evidence right
    Tool permission B small frame for every permission; irreversible: pointer on deny, no clock, no always
    After answering A instant ✓ receipt + working tail (≤50 ms)

    Also in scope (owner): every "waiting on you" surface — status chip, home "needs you" panel/strip, task rows your call, task record, tab ?, terminal title, phone sheet — one grammar, cleared the instant a question settles. Pulls in #677 (question behind the start page eats typed letters; sev:critical) and #840 (waiting chip displaces the task row's door hint).

    Lanes (Claude Opus, interactive, on the Spark; integration branch feat/question-views off dev 333acc6; lanes PR against it, then one PR to dev closes this issue):

  4. santoshkumarradha commented on Sep 11, 2026

    @santoshkumarradha
    MemberAuthor

    Owner approved five more (2026-09-11) — one mechanism, not five

    Answers are messages. Today an answer is only the return value of the tool call that asked. It becomes a message to the conversation that also releases a waiting call when there is one. Three approved ideas are cases of that one door:

    • Keep working while waiting: an ask with Blocking.Turn=false returns at once; the answer arrives at the next step boundary (or starts a turn). Builds the deferred D8 seam.
    • Visible countdown: a reversible question with a pick goes with it when its clock runs out, as a provisional decision; the clock comes from /autonomy, not a new constant; any key pauses it.
    • Undo / revise: a settled reversible decision can be revised from its receipt; the model gets a correction message; revising always revokes the rule; irreversible = not offered.

    Two more are views over what already exists:

    • "Don't ask again for this project": a scope row in the permission frame from Question.Scope (once/task/project/always).
    • Group permissions: several permission questions from one step → one frame: allow all N · one by one · deny all, fanned out as one answer per question; irreversible never grouped.

    Lane A (af-qv-A, feat/qv-A) builds the engine half, design first (rewrites D8 in docs/design/questions/DESIGN.md); P draws all five; F gives the answer door a list form and a step/batch property for grouping.

  5. santoshkumarradha commented on Sep 11, 2026

    @santoshkumarradha
    MemberAuthor

    Owner-reported 2026-09-11 15:35 — an answered question keeps coming back. Standing proposal answered 3 just once at 14:55:59 (recorded once); the card reappears on every tab/conversation switch, and pressing it again records nothing. Cause: internal/remote's held set drops a card only when its old per-lane resolve method is called (server.go:2455-2525); the chat now answers every lane through MethodQuestionResolve (server.go:2102), which never clears it, so each attach re-hands the answered card and outstanding() keeps the chat waiting · your call. Fix at the shape (held set cleared by the question lifecycle events, not by which door was called) assigned to lane F in #910.

  6. santoshkumarradha commented on Sep 11, 2026

    @santoshkumarradha
    MemberAuthor

    Plan change (owner, 2026-09-11 16:05): merge to dev piece by piece. The integration branch feat/question-views is retired; every PR retargets to dev and must stand alone there (manual true for exactly that PR). Order by readiness: #910 (engine: one lifecycle door, cache-stable answers, held-set fix for the answered question that came back) → #919 (answer road: Enter freeze, key ownership, #677) → #914 (answers are messages; rebases onto #910) → P1 (frame, amber, chooser, panel, receipt, chip #840, phone sheet, scope row, /autonomy settings row, pointer by stakes). Two new parallel lanes: R (evidence split + two-pane full page) and T (tabs + review, grouped permissions), stacked on P1. Side runs: #690 (host road drops self-started turns), #926 (keyboard-handover test red under load). This issue closes with the last piece.

  7. santoshkumarradha commented on Sep 11, 2026

    @santoshkumarradha
    MemberAuthor

    First piece merged: #910 → dev 154a0be65. The engine half: one raiseQuestion/letGo door for every lane; Question.Waiting() as the single reading of "is anything stopped on this"; the held set cleared by the question's own lifecycle (heldSet.keepOnly), which closes the owner-reported defect where an answered standing question came back on every attach; withoutSecretWords — a secret answer's words never reach the decision record, the questions lane, a --host frame or message[0] (an OpenRouter key typed into the connect box was written to decisions.jsonl before this); the decision record out of message[0] so answering is byte-stable for the cache; the override memory write registered with startMemoryJob so Close waits for it; ask-back keeps the question open; ask schema enums generated from the Go constants. bin/aforge rebuilt on the owner's laptop at 154a0be65.

    Next: #919 (rebasing), #933 (safety fix — owner ruled deny-first until the engine grades a call), #914 (rebasing onto #910), then #946 and R's page work.

  8. santoshkumarradha commented on Sep 11, 2026

    @santoshkumarradha
    MemberAuthor

    #919 merged → dev 9e803c9ec (the answer road). The ~10 s Enter freeze is gone at its cause: a non-blocking coalescing wake door (doorbell.go) so the wire reader can never be parked by the UI, and every engine door asked off the update loop (offLoop), both held by law tests. Also the one off-screen key guard that closes #677 (a question behind the New chat page ate typed letters, one of which granted always-allow), questions that arrive while away now pinned and re-delivered, the full page scrolling to the focused answer, and one answering road that takes a list.

    Merged so far: #910, #919.

  9. santoshkumarradha commented on Sep 11, 2026

    @santoshkumarradha
    MemberAuthor

    #954 and #960 merged. dev now has: #910 (engine lifecycle, secret answers, cache-stable), #919 (Enter freeze, key ownership, #677), #954 (an answer is a message: keep working while asking, opt-in countdown you can stop, change an answer), #960 (ask-back answered with the question still open; the chip counts only what waits on you). Left: #933 (views P1 — two row-count tests and a rebase), then #955 (evidence beside the answers, two-pane page) and #946 (tabs + review, grouped permissions) which are stacked on it, plus #963 (clocks let go on close).

  10. santoshkumarradha commented on Sep 11, 2026

    @santoshkumarradha
    MemberAuthor

    #933 merged → dev 23f5da1eb. The drawing you picked is live: one frame primitive (the ctrl+k card and onboarding moved onto it), amber on the marks alone with the violet retired, one chooser keyed on the question's properties, the panel with the pointer on the pick and ◆ recommended at the right edge, two tiers of keys, esc folding in place, the ✓ receipt, the chip carrying the question's words (#840), the phone sheet, the permission lifetimes row, and the /autonomy settings row (plus the wire fix that made every door onto those rules work at all — it reported "no project" on every machine).

    The permission pointer ships deny-first per the owner's ruling, with TestTheGateGradesNoCallAsIrreversibleAndMarksDenyTheSafeAnswer failing the day #953 grades a call, so the allow-once half cannot be forgotten.

    Merged: #910, #919, #954, #960, #963, #933. Left: #955 (evidence beside the answers, two-pane page) and #946 (tabs + review, grouped permissions), both rebasing onto dev now.

  11. santoshkumarradha commented on Sep 12, 2026

    @santoshkumarradha
    MemberAuthor

    Closing this. Every complaint in the opening report is fixed and on dev at 27506e80b, across eleven pull requests.

    The four things you reported

    "press enter, escape, all in the same line without any hierarchy." The keys are tiered now: the primary verbs sit on the frame's bottom edge, the rest on a second dim row under it, and a question drawn narrow drops to the rules-and-bands sheet rather than cramming. #933, #946.

    "when I press enter, there is a long gap where nothing happens. It becomes completely unresponsive." That was a deadlock, not slowness. The tui3 wire reader called a blocking p.Send while Update waited synchronously on ResolveQuestion's reply — which the same reader had to deliver. Only the 10-second call deadline broke it, which is exactly the pause you felt. Reproduced deterministically at 10.000362601s; after the fix, 181µs. Fixed with a non-blocking coalescing door (doorbell.go, offloop.go) and two law tests that fail the build if any wire door is ever called from Update again. #919.

    "the pink text… not ideal. Maybe with the border we kind of show the question there hanging." The question is a framed panel above the box now, foldable to one titled rule with esc and reopened with space. The violet is retired from both ladders: colour is stroke, never fill — amber on the three marks (?, the pointer, the recommendation) and ordinary ink on every word. A law test walks the renderers to hold it. #933.

    "turning it into a full page is also not ideal. Maybe left right part." The page is two panes: answers on the left, and on the right the case for whichever answer the pointer is on — what it means, then ·, why this one ·, would switch if, confidence ·, and anything it drew. Under ~100 columns the same lines unfold under the row instead. The panel never takes more than half the screen. #972.

    "we need to see what is the recommended option… it can say recommended or be highlighted cleverly." ◆ recommended on the row, and enter starts there — except on a permission, where it starts on the answer that loses nothing.

    The five you approved afterwards

    All five landed: the scope row (t cycles how long an answer holds), permissions from one batch as one frame, the undo window, the visible countdown (opt-in through /autonomy, per your ruling), and the engine continuing to work while a question waits.

    Two things found on the way that you did not ask for

    A key you typed into the connect box was being written to disk. An OpenRouter key reached decisions.jsonl, the questions lane, --host frames and message[0]. Fixed at one place, withoutSecretWords. #910.

    The answered question that kept coming back — the one you sent the screenshot of — was internal/remote's held set being cleared only by the old per-lane doors, so the unified resolve never cleared it and every attach re-handed the answered card. Now driven by the question's own lifecycle (heldSet.keepOnly). #910.

    What the wave left behind

    PRs: #910, #919, #933, #946, #954, #960, #963, #972, #973, #976, #985.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:chatThe v3 surface a person sits in front of (internal/tui3)featureWork that adds a capability; developers break it into tasksneeds a rulingAn owner decision comes before any code — do not start

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions