-
Notifications
You must be signed in to change notification settings - Fork 8
feat(tui): unified spinner slot — Thinking / Working / Executing + regression fixtures #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
d4c15cd
feat(tui): show 'Thinking...' on spinner during reasoning & unify pen…
minpeter 4667b41
refactor(tui): extract shared spinner primitive; auto-show Executing.…
minpeter 9a6721e
fix(tui): revive spinner for second reasoning span after tool call
minpeter c62cd5a
fix(tui): place Executing... indicator directly under tool block
minpeter 4fe9079
fix(tui): move Executing... to foreground spinner slot (matches Think…
minpeter a5228e8
fix(tui): remove duplicate Executing... in pretty block + kill extra …
minpeter 1cf85c4
test(tui): lock every spinner/layout regression surfaced in this PR b…
minpeter 13090b3
chore(lint): fix ultracite check errors in CI
minpeter bc92e44
fix(tui): release spinner on approval gate; don't overwrite Thinking...
minpeter 1a99304
fix(tui): onReasoningEnd keeps Executing... while tools are still pen…
minpeter f2b7ba8
fix(tui): transfer spinner ownership when tool ends mid-reasoning
minpeter d4e6a64
fix(tui): track pending tool-call ids so unmatched terminals can't co…
minpeter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| --- | ||
| "@ai-sdk-tool/tui": patch | ||
| "plugsuits": patch | ||
| --- | ||
|
|
||
| Route all agent pending states (`Thinking...` / `Working...` / `Executing...`) through the same foreground status spinner slot above the prompt editor, unify their visual language via a shared primitive, and lock every surfaced regression behind fixture tests. | ||
|
|
||
| - TUI: new shared primitive `pending-spinner.ts` exposing `PENDING_SPINNER_FRAMES` (braille `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`), `PENDING_SPINNER_INTERVAL_MS` (80ms), `stylePendingIndicator(frame, message)` (cyan frame + dim message), and `createSpinnerTicker(onFrame, options?)`. `StatusSpinner`, `FooterStatusBar`, and CEA's internal `pi-tui-stream-renderer.ts` tool view all route through it so any future palette / cadence / glyph change lands in one place. Previously tool-pending blocks rendered plain ASCII `- \ | /` frames with no color. | ||
| - TUI: `PiTuiStreamState` gains optional `onReasoningStart` / `onReasoningEnd` callbacks. `handleReasoningStart` fires `onReasoningStart`; a new `handleReasoningEnd` handler fires `onReasoningEnd` (previously `reasoning-end` was silently dropped via `IGNORE_PART_TYPES`). `isVisibleStreamPart` now treats `reasoning-start` / `reasoning-delta` / `reasoning-end` as non-visible, so the first-visible-part spinner clear only fires on real text / tool output. | ||
| - TUI: the foreground spinner label swaps to `Thinking...` for reasoning spans and restores the caller-provided base label (`Working...`) on `reasoning-end`. When a visible part arrives before the second reasoning span of a turn (post tool-call), `foregroundStatus` has already been cleared, so `onReasoningStart` revives the spinner via `showLoader("Thinking...")` and `onReasoningEnd` tears down only the spinner it revived — ordinary flows that kept the base loader alive restore that label unchanged. | ||
| - TUI: `PiTuiStreamState` gains optional `onToolPendingStart` / `onToolPendingEnd` callbacks. `onToolPendingStart` fires from `handleToolCall`; `onToolPendingEnd` fires from `handleToolResult`, `handleToolError`, `handleToolOutputDenied`, and `handleToolApprovalRequest` (approval pauses execution so the spinner must release). `onToolPendingEnd` runs unconditionally — independent of `showToolResults` — so the spinner is always restored even when the tool result itself is not rendered visually. `renderAgentStream` wires these into the same foreground spinner slot (`showLoader("Executing...")` with revive-if-null semantics and a counter so parallel tool calls only restore the base label once every pending call resolves). `Executing...` sits directly above the prompt, identical in placement to `Thinking...`. | ||
| - TUI: overlapping reasoning / tool lifecycles are handled safely. `onToolPendingEnd` skips restoring the base label when reasoning is currently active (`reasoningRevivedSpinner === true`), so a tool result arriving mid-reasoning does not overwrite the live `Thinking...` label. | ||
| - TUI: `BaseToolCallView` no longer renders any inline pending indicator — the foreground spinner owns the pending affordance. `setPrettyBlock(header, body, options?)` now writes the body text through unchanged regardless of `options.isPending`, so consumers can pass a non-empty body alongside `isPending: true` (e.g. `edit_file`'s live diff preview) and have it remain visible while the tool runs. All of the old pretty-block spinner plumbing (`startPendingSpinner`, `stopPendingSpinner`, `paintPendingBodyFrame`, `pendingSpinnerTicker`, `pendingTemplate`, `lastPendingFrame`, `PENDING_MESSAGE`, `PENDING_MARKER`) is removed. | ||
| - TUI: fix the 2-blank-line gap above the foreground spinner during pretty-block pending state. `ensurePrettyBlockComponents` used to add a standalone `new Spacer(1)` between `readHeader` and `readBody`, which always emitted `[""]` even when `readBody` rendered to `[]` (empty-text short-circuit). Combined with `StatusSpinner.render()`'s own leading `""`, callers saw two blank rows above `Executing...`. The explicit `Spacer(1)` is removed; `BackgroundBody.render()` now prepends its own leading `""` only when the body has content, which preserves the one-blank separator between header and body in non-pending mode and eliminates the stray blank in pending mode. `Executing...` now sits with a single leading blank line, identical to `Thinking...` / `Working...`. | ||
| - CEA: the internal `pi-tui-stream-renderer.ts` pending spinner is kept in sync via the shared primitive. `setPrettyBlock` is simplified to always write the body text through (pending branch deleted); the dead `startPendingSpinner` / `stopPendingSpinner` / `paintPendingFrame` / `TOOL_PENDING_SPINNER_FRAMES` / `TOOL_PENDING_MESSAGE` / `TOOL_PENDING_MARKER` / `SpinnerTicker` state is removed. `renderPendingOutput()` now returns `""` so pretty-rendered tools show the header only while pending (no leftover marker text). The companion `preserves requestRender this-context for pending spinner updates` test is removed (its machinery no longer exists); five other tests that locked in the old in-block `Executing...` painting are inverted to assert the absence of that text. | ||
| - TUI: comprehensive regression fixture tests lock every bug surfaced during this PR: | ||
| - `pending-spinner.test.ts` (new) — `PENDING_SPINNER_FRAMES`, `PENDING_SPINNER_INTERVAL_MS`, the exact `stylePendingIndicator` ANSI byte sequence, `createSpinnerTicker` lifecycle (initial frame emission, 80ms cadence, frame wraparound, `stop()` idempotency, `emitInitialFrame: false`, custom `intervalMs`). | ||
| - `stream-handlers.test.ts` (extended) — parametric proof that all three reasoning parts are invisible under any flag combination (the invariant that keeps `Thinking...` alive); `IGNORE_PART_TYPES` does not contain `reasoning-start` / `reasoning-end`; `STREAM_HANDLERS` has a handler for every known part type; reasoning / tool lifecycle callbacks dispatch correctly; parallel-tool-call counter semantics; `onToolPendingEnd` fires on approval-gate transition. | ||
| - `tool-call-view.test.ts` (extended) — inline `toMatchInlineSnapshot` fixtures for pretty-block pending (header-only, no trailing blank) and non-pending (header / blank / body) render shapes. Assertions that `BaseToolCallView` never emits `Executing` or any braille spinner glyph. Raw fallback trailing-blank-free lock. | ||
| - `spinner-layout.test.ts` (new) — end-to-end layout invariant: tool block + foreground spinner has exactly one blank line between them across raw fallback, pretty-block pending, and pretty-block non-pending modes. Parametric trailing-blank-free assertions across all four rendering modes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.