Skip to content

ui: single-line avatar (no smear), friendly errors, decode-body retryable - #52

Merged
yogthos merged 1 commit into
mainfrom
ui/avatar-and-error
May 20, 2026
Merged

ui: single-line avatar (no smear), friendly errors, decode-body retryable#52
yogthos merged 1 commit into
mainfrom
ui/avatar-and-error

Conversation

@yogthos

@yogthos yogthos commented May 20, 2026

Copy link
Copy Markdown
Collaborator

Three fixes: avatar collapsed to a single row on the input line (no more fossils in scrollback from ScrollUp), centered in the left margin between col 0 and the input prompt. Errors now show a 3-line headline + hint + raw cause instead of a stack of nested error types. error decoding response body (a common transient mid-stream failure) is now classified as Network and retried. 604 tests pass.

Three things from the visual review:

## Single-line avatar, repositioned

User preferred the simpler one-line faces:

  Idle      (o o) ↔ (- -)       blink
  Thinking  (o .) ↔ (. o)       eyes shift
  Speaking  (o o) ↔ (o O)       mouth opens
  Reading   [@ @]                 scan-eye brackets
  Writing   (>_<) ↔ (-_-)       concentration
  Bash      [$_$]                 prompt brackets
  Alert     (O_O)                 wide eyes (yellow)
  Error     (x_x)                 ko'd (red)
  Done      (^_^)                 happy (accent)

Avatar now paints a single row on the *input* row, horizontally
centered between col 0 and the input prompt's start column:

  Before (3 rows above input, smeared when chat scrolled):
       ,-,                ,-,         ← chat content
      [@ @]    >>>>>      [@ @]       ← stale avatar fossil
       \_/                 \_/        ← from ScrollUp
                          [@ @]       ← current avatar
                          ▌▌ _

  After (1 row on input line, centered in margin, never scrolled):
                          [@ @]   ▌▌ _

`AVATAR_H` is gone — single row, `AVATAR_W = 5`. The avatar lives
on `input_top` which sits *below* the chat scroll region, so
`ensure_room`'s `ScrollUp(1)` doesn't drag it into scrollback. The
ghost-avatar fossils from the screenshot are gone.

`draw_avatar` wipes the entire left margin (`cols 0..indent`) before
painting so a face from a previous state can't leave artifacts
when the indent recomputes on resize.

## Friendly error messages

`error: CompletionError: ProviderError: Http client error: error
decoding response body` was unhelpful at the chat boundary —
opaque enough to leave the user wondering what to do.

New `recovery::user_facing_error(msg, attempts)` produces a
3-line message: a plain-English headline that names what failed,
a `↳ hint:` that points at the recovery path, and a `↳ cause:`
that preserves the raw error for debugging:

  lost the response stream from the provider (truncated or malformed body)
    ↳ hint: usually transient — retry. If it persists the provider
            may be having issues or returning non-JSON (HTML error
            pages, plaintext)
    ↳ cause: CompletionError: ProviderError: Http client error: ...

Headlines per `ErrorKind`:

  Auth          → "authentication failed talking to the LLM provider"
                  hint points at the API key env var
  RateLimit     → "provider rate-limited the request"
                  hint suggests waiting / /model
  ContextLength → "conversation exceeds the model's context window"
                  hint points at /compress
  Network       → "network error reaching the LLM provider"
                  (with a special variant for decode-body errors)
  Other         → "the LLM provider returned an error we didn't recognize"
                  hint suggests /model

Wired into every runner.rs error-surfacing site (auth, ctx-length,
tool-side-effects, retries-exhausted) — they all now route through
`user_facing_error`.

## "error decoding response body" is now Network-class

Reqwest decode failures mid-stream (very common with streaming
providers) were classified as `Other` and surfaced immediately
without retrying. Now classified as `Network` so the existing
retry policy kicks in. The cause is almost always transient
(connection blip, truncated chunked response, provider hiccup);
single-attempt failure was a frequent papercut.

## Test plan

- [x] 4 new tests:
  - `test_classify_network` extended with the rig decode error
    + "decode error: EOF"
  - `user_facing_error_includes_cause` (headline + hint + raw)
  - `user_facing_error_classifies_auth` (mentions API key)
  - `user_facing_error_classifies_context_length` (mentions /compress)
- [x] `cargo test --features plugin` -> 604 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.
- [ ] Eyeball: trigger a streaming hiccup, verify the chat shows
      a friendly multi-line error and the request is retried;
      verify only one avatar visible at a time when scrolling.
@yogthos
yogthos merged commit b25d58a into main May 20, 2026
1 check passed
@yogthos
yogthos deleted the ui/avatar-and-error branch May 20, 2026 22:11
allen-munsch pushed a commit to allen-munsch/dirge that referenced this pull request Jun 3, 2026
Three things from the visual review:

## Single-line avatar, repositioned

User preferred the simpler one-line faces:

  Idle      (o o) ↔ (- -)       blink
  Thinking  (o .) ↔ (. o)       eyes shift
  Speaking  (o o) ↔ (o O)       mouth opens
  Reading   [@ @]                 scan-eye brackets
  Writing   (>_<) ↔ (-_-)       concentration
  Bash      [$_$]                 prompt brackets
  Alert     (O_O)                 wide eyes (yellow)
  Error     (x_x)                 ko'd (red)
  Done      (^_^)                 happy (accent)

Avatar now paints a single row on the *input* row, horizontally
centered between col 0 and the input prompt's start column:

  Before (3 rows above input, smeared when chat scrolled):
       ,-,                ,-,         ← chat content
      [@ @]    >>>>>      [@ @]       ← stale avatar fossil
       \_/                 \_/        ← from ScrollUp
                          [@ @]       ← current avatar
                          ▌▌ _

  After (1 row on input line, centered in margin, never scrolled):
                          [@ @]   ▌▌ _

`AVATAR_H` is gone — single row, `AVATAR_W = 5`. The avatar lives
on `input_top` which sits *below* the chat scroll region, so
`ensure_room`'s `ScrollUp(1)` doesn't drag it into scrollback. The
ghost-avatar fossils from the screenshot are gone.

`draw_avatar` wipes the entire left margin (`cols 0..indent`) before
painting so a face from a previous state can't leave artifacts
when the indent recomputes on resize.

## Friendly error messages

`error: CompletionError: ProviderError: Http client error: error
decoding response body` was unhelpful at the chat boundary —
opaque enough to leave the user wondering what to do.

New `recovery::user_facing_error(msg, attempts)` produces a
3-line message: a plain-English headline that names what failed,
a `↳ hint:` that points at the recovery path, and a `↳ cause:`
that preserves the raw error for debugging:

  lost the response stream from the provider (truncated or malformed body)
    ↳ hint: usually transient — retry. If it persists the provider
            may be having issues or returning non-JSON (HTML error
            pages, plaintext)
    ↳ cause: CompletionError: ProviderError: Http client error: ...

Headlines per `ErrorKind`:

  Auth          → "authentication failed talking to the LLM provider"
                  hint points at the API key env var
  RateLimit     → "provider rate-limited the request"
                  hint suggests waiting / /model
  ContextLength → "conversation exceeds the model's context window"
                  hint points at /compress
  Network       → "network error reaching the LLM provider"
                  (with a special variant for decode-body errors)
  Other         → "the LLM provider returned an error we didn't recognize"
                  hint suggests /model

Wired into every runner.rs error-surfacing site (auth, ctx-length,
tool-side-effects, retries-exhausted) — they all now route through
`user_facing_error`.

## "error decoding response body" is now Network-class

Reqwest decode failures mid-stream (very common with streaming
providers) were classified as `Other` and surfaced immediately
without retrying. Now classified as `Network` so the existing
retry policy kicks in. The cause is almost always transient
(connection blip, truncated chunked response, provider hiccup);
single-attempt failure was a frequent papercut.

## Test plan

- [x] 4 new tests:
  - `test_classify_network` extended with the rig decode error
    + "decode error: EOF"
  - `user_facing_error_includes_cause` (headline + hint + raw)
  - `user_facing_error_classifies_auth` (mentions API key)
  - `user_facing_error_classifies_context_length` (mentions /compress)
- [x] `cargo test --features plugin` -> 604 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.
- [ ] Eyeball: trigger a streaming hiccup, verify the chat shows
      a friendly multi-line error and the request is retried;
      verify only one avatar visible at a time when scrolling.

Co-authored-by: Yogthos <yogthos@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant