Skip to content

🤖 feat: continue interrupted stream by clicking the interrupted splitter#3634

Open
ethanndickson wants to merge 4 commits into
mainfrom
interrupt-dvhd
Open

🤖 feat: continue interrupted stream by clicking the interrupted splitter#3634
ethanndickson wants to merge 4 commits into
mainfrom
interrupt-dvhd

Conversation

@ethanndickson

@ethanndickson ethanndickson commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

Clicking the "interrupted" splitter now continues the stream from where it stopped, identical to the existing RetryBarrier "Retry" button and the backend auto-retry path. Previously the splitter was purely decorative, which left user-initiated (Esc) interrupts with no continue affordance at all — the warning RetryBarrier is intentionally suppressed for reason: "user" aborts, so only the subtle "interrupted" line showed and there was no way to resume short of typing a new message.

Background

When a stream is interrupted, the backend keeps the partial assistant turn in history. Both manual Retry and auto-retry converge on resumeStream, which does no history shaping; the model simply continues its partial turn (plus an ephemeral [CONTINUE] sentinel injected at transform time, never persisted). The continue behavior already existed — it just wasn't reachable after a user-initiated Esc, because the RetryBarrier only mounts for system/error interrupts.

Implementation

  • Extracts the manual-retry/resume flow into a shared useResumeStream(workspaceId) hook. This includes the temporary setAutoRetryEnabled({ persist: false }) enable plus the rollback dance that restores the user's auto-retry preference once the resumed attempt reaches a terminal outcome (so a user who disabled auto-retry isn't silently re-opted-in).
  • RetryBarrier now consumes the hook instead of owning that logic inline, so both entry points share one implementation and can't drift.
  • BaseBarrier gains an optional onClick. When provided, only the centered label becomes a <button> with a hover underline and pointer cursor; the gradient lines and every other visual stay exactly as before. Barriers without onClick (e.g. EditCutoffBarrier) are unchanged.
  • InterruptedBarrier accepts workspaceId and wires its label to resume(). resume() self-guards against re-entrancy, so the label stays mounted (no button↔div remount flicker).

Visual contract

No visual change except: hovering the "interrupted" label underlines it and shows a pointer cursor. The gradient lines are inert and hovering them does nothing.

Risks

Low. The resume/rollback logic is moved verbatim into the hook (covered by the existing RetryBarrier tests, which still pass against the refactored component), and the new affordance reuses the same backend path. The only new surface is the conditional <button> rendering in BaseBarrier, scoped to callers that pass onClick.

Follow-ups (not in scope)

  • No keyboard shortcut for resume yet (Esc has no inverse). AGENTS.md wants every operation to have one; a RESUME_STREAM keybind could call the same hook from useAIViewKeybinds.
  • Resume errors triggered from the splitter are currently swallowed (the splitter has no chrome to surface them); the user can click again. The RetryBarrier still surfaces its own errors.

Generated with mux • Model: anthropic:claude-opus-4-8 • Thinking: xhigh • Cost: $5.84

Clicking the 'interrupted' splitter now resumes the stream from where it
stopped, identical to the RetryBarrier's Retry button and the backend
auto-retry path. This is the only continue affordance for user-initiated
(Esc) interrupts, where the RetryBarrier is intentionally suppressed.

Extracts the manual-retry/resume flow (including the temporary
auto-retry enable + rollback dance) into a shared useResumeStream hook so
RetryBarrier and InterruptedBarrier share one implementation. BaseBarrier
gains an optional onClick that turns only the centered label into a
button with a hover underline + pointer cursor; the gradient lines and
all other visuals are unchanged.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7e6eaab08

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/Messages/ChatBarrier/InterruptedBarrier.tsx Outdated
Comment thread src/browser/components/ChatPane/ChatPane.tsx Outdated
P2-1: the interrupted divider opts out of the temporary auto-retry enable
(autoRetryOnFailure:false). ChatPane unmounts the divider once auto-retry
becomes active, so owning the rollback there would cancel an in-flight
scheduled retry on unmount. A user-initiated Esc means 'continue once', and
auto-retry-enabled users still get backend recovery (the backend consults the
persisted preference on failure regardless), so this is both safe and the
correct semantic.

P2-2: only the divider on the resume target (history tail,
lastRetryCandidateMessage) is clickable. resumeStream always continues the
tail, so older partial dividers would otherwise resume the wrong turn; they now
stay decorative.
@ethanndickson

Copy link
Copy Markdown
Member Author

Addressed both Codex P2 findings in d89eba2.

P2-1 — Keep resume rollback outside the transient divider (InterruptedBarrier.tsx): the divider now resumes via useResumeStream(workspaceId, { autoRetryOnFailure: false }). It no longer enables auto-retry or owns rollback refs, so the unmount that happens once isAutoRetryActive becomes true can't cancel an in-flight scheduled retry. A user-initiated Esc means "continue once"; auto-retry-enabled users still get backend recovery because the backend consults the persisted preference on failure regardless.

P2-2 — Limit clicks to the resumable interrupted turn (ChatPane.tsx): the divider is only clickable when it sits on the current resume target (message.id === lastRetryCandidateMessage?.id). Since resumeStream always continues the history tail, older partial dividers now stay decorative instead of resuming the wrong turn.

@ethanndickson

Copy link
Copy Markdown
Member Author

@codex review

Addressed both P2 findings (auto-retry rollback ownership + clickable-only-on-resume-target). Please take another look.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d89eba2017

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/Messages/ChatBarrier/InterruptedBarrier.tsx Outdated
The divider is the only continue affordance for a user-aborted stream, so a
resume failure before any stream-error (e.g. disconnected API or a
SendMessageError from resumeStream) must be visible — otherwise the click looks
like a no-op. Renders the hook's error inline below the resumable divider,
mirroring RetryBarrier's error treatment. Idle/hover appearance is unchanged.
@ethanndickson

Copy link
Copy Markdown
Member Author

Addressed in 9364f85.

P2 — Surface interrupted-resume failures (InterruptedBarrier.tsx): the resumable divider now renders the hook's error inline below it ("Couldn't continue: …"), mirroring RetryBarrier's error treatment. A resume failure before any stream-error (disconnected API, SendMessageError from resumeStream) is now visible instead of looking like a no-op. The idle/hover appearance is unchanged, and the label stays clickable to try again.

@ethanndickson

Copy link
Copy Markdown
Member Author

@codex review

Addressed the resume-failure feedback finding (errors now surface inline on the resumable divider). Please take another look.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9364f85358

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/components/ChatPane/ChatPane.tsx Outdated
Transcript-only (archived, local checkout removed) workspaces are read-only:
the composer is replaced and edit/send are disabled. Gate the divider's
resumable flag on !transcriptOnly so archived interrupted transcripts render a
decorative divider instead of a live continue button that would call
resumeStream against a no-checkout workspace.
@ethanndickson

Copy link
Copy Markdown
Member Author

Addressed in c4d3d05.

P2 — Disable resume in transcript-only chats (ChatPane.tsx): the divider's resumable flag is now gated on !transcriptOnly. Archived/no-checkout workspaces (where the composer is already replaced with a read-only notice) render a decorative divider instead of a live continue button, so they stay non-mutating.

@ethanndickson

Copy link
Copy Markdown
Member Author

@codex review

Gated the divider's resume on !transcriptOnly so archived transcripts stay read-only. Please take another look.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4d3d054fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

text="interrupted"
color="var(--color-interrupted)"
className={props.className}
onClick={props.resumable ? () => void resume() : undefined}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add a shortcut for continuing interrupted responses

When props.resumable is true, this introduces a new continue/resume operation that is only exposed through the divider button. /workspace/mux/AGENTS.md requires that every operation have a keyboard shortcut, so keyboard-driven users have no direct shortcut/command-palette path to resume a user-aborted stream without tabbing to this small label. Please wire the same resume() path into the app keybind/command flow, with any visible hint hidden on mobile per the guideline.

Useful? React with 👍 / 👎.

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