Skip to content

ux: consistent keys, confirmations, input, status and connectivity from the repository review - #606

Merged
stormer78 merged 11 commits into
mainfrom
ux/consistency-from-review
Nov 15, 2026
Merged

stormer78 merged 11 commits into
mainfrom
ux/consistency-from-review

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

One pass over the UX findings from the repository review: one confirmation rule, one key table per view (drawn as the footer, listed by a new ? overlay, checked against the handler by tests), one text-input behaviour, typed status severity, a layout that fits 80x24, honest notifications, classified connectivity failures with a retry, a wizard you can step back through, and consistent wording.

Commits are split by concern and each builds and passes the tests on its own:

  1. ux(status) — bug(ui): many failure messages render in success green — status colour is decided by a string-prefix heuristic #577
  2. ux(confirm) — ux(confirm): three different confirmation patterns, Enter commits destructive actions, and some outward-facing actions have no confirmation #579
  3. ux(keys) — ux(keys): inconsistent key map across panels, no ? help overlay, and a focusable panel with no key handler #580
  4. ux(input) — ux(input): main-page text fields have no cursor, arrow keys steal focus, Ctrl-combos insert letters; paste and Esc behave differently per screen #578 (+ a follow-up commit for setup paste)
  5. ux(layout) — ux(layout): at 80x24 the Quit menu item, the <F10> quit hint and the setup PNM commands are clipped #576
  6. ux(notifications) — ux(notifications): the Inbox badge counts the user's own outbound tasks, other panels have no badge, and a ping never reports its result #584
  7. ux(connectivity) — ux(connectivity): the header shows 17 characters of failure cause, cannot tell unreachable from rejected, and offers no retry #585
  8. ux(setup) — ux(setup): the setup wizard has no way back, and Esc clears the field instead of going back #588
  9. docs(ui) — docs(ui): mixed British/American spelling in UI strings and inconsistent names for the same concept #554
  10. docs(changelog)

Per issue

#577 — typed status (rest of the issue)

  • Every panel's status_message (communities, identity, vetting, inbox, relationships, credentials, settings, capabilities) and the device-access message are a typed Status { severity, text }. Severity/Status moved to state_handler/main_page/status.rs (re-exported where repos/rooms/elections had them) with one style table, components::status::severity_style.
  • Severity is set at each producer (~250 sites reviewed; "nothing was done, and why" is Warning, in-flight sends are Progress, refusals/failures Error). VettingAction::Status and SettingsAction::ClipboardCopied carry a Status.
  • The wording heuristic (FAILURE_MARKERS / status_style) is deleted; nothing decides colour from text any more. Forms' error fields render through push_error (the field is named for its severity).

#579 — confirmations

  • ui/pages/main/confirm.rs: is_yes (plain y only), KEYS ("y: confirm · any other key: cancel") and prompt(); every prompt uses them.
  • y-only everywhere a destructive or outward-facing action was confirmed by y or Enter: community delete/leave/cancel-join, device-access revoke, Inbox dismiss/clear-all, relationship and credential remove, invitation credential delete/purge, agent-name remove, all persona confirmations, capability toggle, vetting abandon / delete-ticket / withdraw / decline (Enter on the decline note now just leaves the note), local-face delete, make-reusable, forget-contact.
  • New confirmations: Inbox r reject (sent to the peer), Settings "Remove the unlock code", Communities m (issue VMC) and R (renew).
  • Typed words stay for WIPE, RESET and context deletion.

#580 — key map and ?

  • ui/pages/main/keymap.rs: a view declares its keys as Bindings (k("d/Del", "delete")); keymap::footer draws them in one format (key: does · …, wrapping rather than clipping) and advertises them for the frame. Labels use one vocabulary parsed into the KeyCodes they name, so a label cannot disagree with the key matched.
  • ? opens an overlay listing exactly the focused view's advertised keys plus the global table (any key closes it); the footer row and Help / Status's shortcut list are generated from the same global table.
  • A key a view does not take shows "x does nothing here — ? lists this panel's keys" in the footer row. While a text field has focus, unhandled keys stay in the form (they no longer switch panels). Ctrl/Alt chords reach a view only where it lists them.
  • Consistent keys: VTA invitations archive with x (was r), show-archived is v (was i), refresh is r (was Tab); Communities joins with n (was j) and j/k move there and in the menu. TSP Relationships gets a handler (↑/↓, Esc).
  • Silent swallows removed or listed (Inbox a, credential d, VTA u, persona d, identity read-only views and pickers).
  • keymap_tests.rs asserts, for each view, that the footer lists exactly the keys the handler takes (inbox list/detail, relationships list/detail/alias/new-request ×4 fields, credentials list/detail ×2, VTA ×4, communities ×4, settings ×2, Help/Status, TSP, all six identity tabs, capabilities loading/failed).

#578 — text input

  • ui/pages/main/text_input.rs lists every main-page string field once and edits it through tui_input (as setup/join do): visible cursor; ←/→, Ctrl+←/→, Home/End move inside the field; Backspace/Del; Ctrl+A/E/U/W/K edit; no chord types its letter (a chord the view lists, e.g. Ctrl+A "add owner", stays the view's).
  • Paste inserts at the cursor on the main page and in setup (was append / replace). Typed confirmation words take no paste. Ballot text answers get a BallotText action so they edit like any field.
  • Masked passphrase length counts characters, not bytes. The append-only edit_text and the per-panel paste code are gone.

#576 — 80x24

  • Activity log 4 rows below 32 rows; menu ≥ 23 columns and scrolls to the selection; footer row leads with F10: quit and ?: keys and keeps only whole hints.
  • Setup step 1 body scrolls (↑/↓, PgUp/PgDn) and the footer says so; the copy result takes the footer row.

#584 — notifications

  • Inbox badge counts inbound items only (not own outbound requests/pings); Communities (actions required) and Vetting (desk) get badges.
  • Activity-log entries carry a severity (errors ✗ and warnings ! in colour; routine lines dim).
  • Pong shown in the Relationships panel with latency; no pong in 30 s → a warning there and in the log, and the outbound ping task is expired.

#585 — connectivity

  • MediatorStatus::Failed(ConnectionFailure { reason, class: ErrorClass }); core's ReconnectOutcome::Failed { reason, class } (class_of the listener error; a listener that never connects is Timeout).
  • Header: "✗ mediator unreachable / refused us / incompatible / not answering … · see Help". Help / Status: class, full reason, ErrorClass::remedy(), and r reconnects. Enter on the Settings mediator row reconnects. SettingsAction::ReconnectMediator no longer #[allow(dead_code)].
  • "Messaging unavailable — cannot X right now." names the class and remedy.
  • Identity pane agent errors are typed (AgentError { text, class }); the holder-grant command is offered only for a refusal whose text names it — never for an unreachable agent (the old string-only decision).

#588 — setup

  • Esc goes back a page on every wizard page that has one (navigation::previous_page); back stops at the VTA once its one-time grant is spent; step 2 keeps its own back (it carries step 1's banner reason). Choice pages and the no-code warning say "[ESC] to go back".
  • The setup DID is minted once per run: going back and forward keeps it, so a PNM grant still applies. (Still new on each relaunch — it is never written to disk; see leftovers.)
  • One progress indicator: the header's sections. Boxes are named, not counted.

#554 — wording

  • British spelling in the American UI strings (unauthorised, recognised, Initialising, initialised, Finalised). Protocol/SDK/format text untouched.
  • The profile secret is "unlock code" in Settings → Protection and the CLI prompt/--unlock-code-file errors (export passphrases keep their name).
  • README glossary (persona, community/VTC, unlock code, Inbox, Help / Status); CONTRIBUTING wording convention. No persisted identifier or theme id renamed.

User-visible behaviour changes

  • Enter no longer confirms any destructive or outward-facing action; only y does, and any other key (Enter included) cancels.
  • New y confirmations before: rejecting an Inbox request, removing the unlock code in Settings, issuing a VMC (m) and renewing a membership (R).
  • Vetting: abandon / delete ticket / withdraw / decline are committed by y; Enter on the decline note moves off the note.
  • Identity: deleting a local face, making a value reusable, and forgetting a contact are committed by y (were Enter).
  • ? opens a key overlay on the main page (outside text fields).
  • A key that does nothing shows a one-line hint in the footer row.
  • Footers everywhere use "key: does · key: does" and the same key names (Enter, not ⏎).
  • VTA panel: archive is x (was r), show archived is v (was i), refresh is r (was Tab).
  • Communities: join is n (was j); j/k move the selection; same in the menu.
  • TSP Relationships: ↑/↓ move the cursor, Esc returns to the menu.
  • Text fields show a cursor; ←/→/Home/End move within them; Ctrl+A/E/U/W/K edit; Ctrl chords never type letters; Tab/←/→ in a field no longer switch panels.
  • Paste inserts at the cursor (main page and setup).
  • 80x24: activity log shrinks to 4 rows; the menu is wider and scrolls; the footer row starts with F10: quit · ?: keys.
  • Setup step 1 scrolls; its copy result shows in the footer row.
  • Inbox badge counts only inbound items; Communities and Vetting show badges.
  • Activity log shows errors/warnings in colour with ✗ / !.
  • Pings report their pong (or no reply after 30 s) in the Relationships panel; unanswered ping tasks expire.
  • Header shows the class of a messaging failure and "see Help"; Help / Status shows the cause, the remedy and r to reconnect; Enter on Settings' mediator row reconnects.
  • "Messaging unavailable" messages name the class and remedy.
  • Setup: Esc goes back instead of clearing; boxes no longer show "Step x/y".
  • Settings/CLI say "unlock code" for the profile secret.
  • Status lines are coloured by severity (warnings orange, successes green, errors red/bold) — no longer by wording.

Tests added

  • Status: colour follows severity not wording; every severity distinct.
  • Confirmation: table test — for 16 armed confirmations Enter never commits and y does; Inbox reject arms; remove-unlock-code arms.
  • Key map: per-view exactness (footer ⇔ handler) for the views listed under ux(keys): inconsistent key map across panels, no ? help overlay, and a focusable panel with no key handler #580; ? lists the focused view's keys and the global ones and any key closes it; an unused key gives feedback; label parsing; footer wrapping.
  • Input: insert at cursor, arrows, deletes, chords never insert, non-ASCII, cursor drawing; ← then type; paste at Home; Ctrl+U clears / Ctrl+T doesn't type.
  • Layout: 80x24 render shows every menu item incl. Quit and F10: quit/?: keys; short terminal scrolls to Quit; setup step 1 at 80x24 scrolls to the F4 command and shows the copy result in the footer.
  • Notifications: Inbox badge counts inbound only; activity log keeps severity.
  • Connectivity: header per class (unreachable / rejected / incompatible); Help / Status shows cause + remedy and r reconnects; Settings mediator row reconnects.
  • Setup: Esc walks back from step 1 to the start keeping the input.

Verified locally (rustc 1.100): cargo fmt --all --check; cargo clippy --workspace --all-targets --locked -- -D warnings and with --no-default-features; cargo clippy -p openvtc --all-targets --features dev-overrides -- -D warnings; cargo test --workspace --locked and with --no-default-features; RUSTDOCFLAGS='-D warnings' cargo doc --workspace --no-deps; cargo machete.

Not done here (follow-ups)

Closes #576
Closes #577
Closes #579
Closes #584
Closes #585
Closes #588
Part of #578
Part of #580
Part of #554

Pre-merge checklist (vti-stack-development-guide §9)

  • No new reqwest::Client::new() / bare fetch(); all clients have finite timeouts (R1.2) — no new clients; the ping reply window is a finite 30 s
  • No lock held across a network await (R1.3)
  • No local state committed before its remote effect, or the flow is resumable with an idempotency key (R2.1) — confirmations only gate sends; ping-task expiry is local bookkeeping after the window
  • Every retry is bounded + backed off; non-idempotent ops are not blind-retried (R1.4) — reconnect is operator-triggered, one at a time (existing in-flight domain)
  • Accept/poll/listen loops survive transient errors (R1.5) — unchanged
  • Acks/deletes happen only after durable handoff (R1.6) — unchanged
  • New/changed wire types: camelCase, deny_unknown_fields where security-relevant, schema registered, all consumers (incl. JS) updated (R3.*) — no wire types changed
  • Config absence = most restrictive; fail-closed if enforcement can't start (R5.*) — unchanged
  • Logs/status claim only what was verified; background-job failures are surfaced (R6.*) — R6.4: header, Help / Status, "Messaging unavailable" and the identity pane now name the failure class
  • "Process dies on the next line" answered for every mutation touched (R2.1) — ping expiry marks the config dirty after removal; nothing else persisted changed
  • Deviations from this guide flagged explicitly with rule numbers — none

…uristic (#577)

Every main-page panel's status_message (communities, identity, vetting,
inbox, relationships, credentials, settings, capabilities) and the device
access view's message are now a typed Status { severity, text }. The
producer that knows whether its operation worked sets the severity; the
renderer colours by severity only. The string-prefix/marker heuristic in
components/status.rs is deleted.

Severity and Status move from main_page/repos.rs to main_page/status.rs
(re-exported where they were) so rooms, elections and repos share one
type and one style table (severity_style). Actions that carried a status
string (VettingAction::Status, SettingsAction::ClipboardCopied) carry a
Status.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
#579)

Destructive and outward-facing actions are armed by their key and
committed by y alone; Enter, the key that opened the view, now cancels
like any other key. Shared in ui/pages/main/confirm.rs (is_yes, KEYS,
prompt) and used by every prompt, which now reads
'y: confirm · any other key: cancel'.

Changed to y-only: community delete/leave/withdraw, device-grant revoke,
inbox dismiss/clear-all, relationship and credential remove, invitation
credential delete/purge, agent-name remove, persona confirmations,
capability toggle, vetting abandon/delete-ticket/withdraw/decline.

Now confirmed (previously sent on one key): Inbox reject (sent to the
peer), Settings remove passphrase, Communities m (issue VMC) and R
(renew). Typed-word confirmation stays for WIPE and RESET.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…eedback (#580)

Every main-page view now builds its keys as a list of Bindings
(ui/pages/main/keymap.rs) and draws them with keymap::footer, which also
advertises them for the frame. The ? overlay lists exactly the advertised
keys of the focused view plus the global ones; the footer row and the
Help / Status shortcut list are generated from the same global table.

- one label vocabulary (Enter, Esc, Tab, Shift+Tab, arrows, Space, Del,
  Ctrl+X) parsed into the key codes it names, so a label cannot name a
  key it does not match; one format 'key: does · key: does'
- a key a view does not take shows 'x does nothing here — ? lists this
  panel's keys' in the footer row instead of vanishing; a text field keeps
  ←/→/Tab rather than switching panels; Ctrl/Alt chords reach a view only
  where it lists them
- consistent keys: archive is x (VTA invitations too), show archived is v
  (was i on the VTA panel), refresh is r (was Tab on the VTA panel);
  Communities joins with n, and j/k move there as in rooms, elections and
  the menu
- TSP Relationships gets a key handler (↑/↓, Esc)
- silent swallows removed (inbox a, credential d, VTA u, persona d,
  identity read-only overlays and pickers); local-face delete, make
  reusable and forget-contact now confirm with y only
- keymap_tests checks, per view, that the footer lists exactly the keys
  the handler takes (inbox, relationships, credentials, VTA, communities,
  settings, help, TSP, identity tabs, capabilities)

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Every string field on the main page (relationship request, alias and
witness; credential reason; settings name, export/import path and the
typed WIPE/RESET words; vetting text fields; the device-grant form; the
context-delete word; the repos forms; ballot text answers) is listed
once in ui/pages/main/text_input.rs and edited there through tui_input,
the editor the setup and join screens use:

- a visible cursor; ←/→, Ctrl+←/→, Home/End move within the field instead
  of switching panels; Backspace/Del delete either side; Ctrl+A/E/U/W/K
  edit; no Ctrl or Alt chord types its letter (a chord the view lists as
  its own, such as Ctrl+A 'add owner', stays the view's)
- a paste is inserted at the cursor in every field that takes one (it was
  appended on the main page); the typed confirmation words take no paste
- the masked passphrase length counts characters, not bytes
- ballot text answers get a BallotText action so they edit like any field
- the append-only edit_text helper and the per-panel paste code go

Tests: editor semantics (insert at cursor, arrows, deletes, chords,
non-ASCII), cursor drawing, ← then type, paste at Home, Ctrl+U/Ctrl+T.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
- the activity log takes 4 rows instead of 8 below 32 rows, so all twelve
  menu items fit at 24 rows; the menu also scrolls to keep the selection
  on screen on anything shorter
- the menu is at least 23 columns wide (its longest item and a badge), so
  'My Relationships', 'TSP Relationships', 'My Credentials' and
  'Help / Status' are no longer cut at 80 columns
- the footer row is built from the global key table with F10 quit and ?
  first, and keeps only whole hints that fit
- setup step 1 (Run the PNM command): the actions-and-commands body scrolls
  with ↑/↓ and PgUp/PgDn, the footer says so when it can, and the copy
  result takes the footer row instead of the end of the body
- tests: 80x24 TestBackend renders of the main page (every menu item and
  'F10: quit' visible) and setup step 1 (scrolls to the F4 command; copy
  result in the footer row)

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…g results (#584)

- the Inbox badge counts what was sent to the user (requests, issued
  credentials, information, vetting), not their own outbound requests or
  pings; Communities shows its actions-required count and Vetting the
  requests on the desk, as badges of their own
- activity-log entries carry a severity: errors (log_error) and warnings
  are drawn in their colour with a glyph (✗ / !), routine lines stay dim
- a pong is reported in the Relationships panel ('Pong from X (N ms) —
  they are reachable.'), not only in the log; a ping with no pong in 30 s
  says so there ('No reply to the ping after 30 s …'), is logged as a
  warning, and its outbound task is expired so it does not sit in the
  Inbox
- tests: badge counts inbound only; log keeps each entry's severity

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
- MediatorStatus::Failed carries a ConnectionFailure { reason, class }
  with the typed openvtc_core::errors::ErrorClass; ReconnectOutcome::Failed
  carries the class too (class_of the listener error; a listener that never
  connects is a Timeout)
- the header shows the class ('✗ mediator unreachable', 'mediator refused
  us', 'mediator incompatible', 'mediator not answering', …) and 'see
  Help' instead of 17 characters of error text; Help / Status shows the
  class, the full reason and the class's remedy, and r there reconnects
- SettingsAction::ReconnectMediator is live: Enter on the Settings mediator
  row ('[Enter to reconnect]') and r on Help / Status; its
  #[allow(dead_code)] is gone
- 'Messaging unavailable — cannot X right now.' now names the class and
  the remedy, and points at the retry
- the identity pane's agent errors are typed (AgentError { text, class }):
  the holder-grant hint is offered only for a refusal (AuthRejected /
  Refused) whose text names the holder credential, never for an
  unreachable agent; other failures show their class's remedy
- tests: header per class, Help / Status cause + r, mediator row Enter

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…#588)

- Esc goes back a page on every setup page that has one
  (setup_flow::navigation::previous_page), instead of clearing the field:
  ConfigImport and VTA DID go back to the start, step 1 (PNM command) to
  the VTA DID, the context-occupied warning to step 1, the unlock-code
  pages to the unlock-code choice, the token pages to the page before.
  Back stops at the VTA once its one-time grant is spent. Step 2 keeps its
  own back (it carries the reason for step 1's banner). Choice pages and
  the no-code warning say '[ESC] to go back'; the input pages' '[ESC] to
  clear input' now reads '[ESC] to go back'.
- going back keeps what was typed and the setup DID: the ephemeral setup
  key is minted once per run, so a PNM grant already made for it still
  applies after going back and forward (it is still new each relaunch:
  it is never written to disk)
- one progress indicator: the header's 'Section n/4'; the boxes are named
  ('Run the PNM command', 'Connect to the VTA', 'Enter unlock code', …)
  instead of carrying their own 'Step x/y' counters that disagreed with it
- test: Esc walks back from step 1 to the start keeping the context id

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
- British spelling in the user-visible strings that were American:
  unauthorised (unlock-code page), recognised (token scan),
  'Initialising:' (header, Inbox, Help / Status), 'not initialised',
  'Finalised'. Protocol and SDK text keeps its own spelling (the VTA's
  'is not authorized on' is matched verbatim; theme 'color' keys and
  'organization' JSON are formats).
- the profile's secret is the 'unlock code' everywhere: Settings →
  Protection ('Set an unlock code', 'Remove the unlock code (keyring
  only)', 'Unlock code:'), its log lines, and the CLI unlock prompt and
  --unlock-code-file errors. An export file's 'passphrase' is a different
  secret and keeps its name.
- README gains a Glossary (persona, community/VTC, unlock code, Inbox,
  Help / Status); CONTRIBUTING states the wording convention (British
  spelling, one name per concept, one key vocabulary via keymap::Binding).
- Persisted identifiers and theme ids are unchanged.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…584 #585 #588 #554)

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Setup's text fields replaced their whole content with a paste while the
main page appended it; both now insert at the cursor.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 requested a review from a team as a code owner November 15, 2026 08:01
@stormer78
stormer78 merged commit 439b512 into main Nov 15, 2026
19 checks passed
@stormer78
stormer78 deleted the ux/consistency-from-review branch November 15, 2026 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment