Skip to content

Threads 18: Show parallel response lifecycle states - #247

Open
FranciscoMoretti wants to merge 3 commits into
codex/threads-v2-13-stop-provisioningfrom
codex/threads-v2-14-parallel-card-status
Open

Threads 18: Show parallel response lifecycle states#247
FranciscoMoretti wants to merge 3 commits into
codex/threads-v2-13-stop-provisioningfrom
codex/threads-v2-14-parallel-card-status

Conversation

@FranciscoMoretti

@FranciscoMoretti FranciscoMoretti commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Distinguishes queued, generating, and complete parallel responses.
  • Keeps loading UI and the stop control active for optimistic and streaming cards.
  • Hides copy, feedback, retry, and follow-up actions until the selected response settles.
  • Synchronizes the compatibility store to submitted at the app submission boundary.

Behavior

Parallel cards no longer flash Task completed, show premature actions, or expose a send icon while work is pending.

Verification

  • Browser verified first-message and follow-up parallel cards with GPT-5 mini and GPT-5 nano.
  • Browser verified selected-run stop isolation.
  • 114 app unit tests pass at the stack tip.

Screenshot

Parallel response cards

Review focus

The single response-lifecycle rule used by cards, actions, loading, and the composer.


Summary by cubic

Shows accurate lifecycle states for parallel responses (queued, generating, complete) in cards and loading UI. Disables inputs and actions until the active response settles; Stop is instant for the selected response and skips server calls for pending:* streams.

  • New Features

    • Added helpers: getParallelResponseLifecycle, getStatusLabel, getResponseAwareStatus, clearResponseActiveStream, isPendingResponseStream.
    • Cards use lifecycle-driven spinner/labels; queued slots are clickable, keep selection, and auto-navigate when streaming begins.
    • Composer, paste, uploads, and attachments gate via response-aware status using last-message metadata; set status("submitted") at provisional chat start.
  • Bug Fixes and Refactors

    • Stop settles immediately: locally clear only the selected response’s activeStreamId; skip server stop/reconnect for pending:* streams.
    • Hide assistant actions while any response is active; keep loading skeleton for queued/pending/submitted; show completion only after the stream marker clears; added unit tests for lifecycle and stop behavior.
    • Removed legacy tree state actions from useChatActions and a redundant thread hook.

Written for commit 72a3612. Summary will update on new commits.

Review in cubic

Stack

  1. Threads 0: Define the useThread target architecture #233
  2. Threads 22: Adopt AI SDK response identity lifecycle #258
  3. Threads 1: Define useThread package contracts #234
  4. Threads 2: Add the canonical message tree #235
  5. Threads 3: Adapt isolated AI SDK runs #236
  6. Threads 4: Orchestrate concurrent tree runs #237
  7. Threads 5: Prove AI SDK behavioral parity #238
  8. Threads 6: Expose the useChat-compatible useThread hook #239
  9. Threads 7: Rename the thread controller #261
  10. Threads 10: Store ordered thread snapshots in ChatJS #240
  11. Threads 8: Add externally owned thread state #262
  12. Threads 9: Align Thread with AI SDK request semantics #266
  13. Threads 10: Back threads with canonical Zustand state #267
  14. Threads 11: Mount useThread in ChatJS #241
  15. Threads 12: Add branch navigation and retry #242
  16. Threads 13: Isolate branch stream lifecycles #243
  17. Threads 14: Stream follow-up parallel responses #244
  18. Threads 15: Add cancellable request gates #263
  19. Threads 16: Gate first-message parallel runs #245
  20. Threads 17: Stop newly provisioned thread runs #246
  21. Threads 18: Show parallel response lifecycle states #247 👈 current
  22. Threads 19: Publish installable thread sources #248
  23. Threads 20: Add the thread playground model #249
  24. Threads 21: Add the interactive thread playground #250
  25. Threads 22: Publish the threads product page #251
  26. Threads 23: Add the value-first package guide #252
  27. Threads 24: Document ChatJS threaded behavior #253
  28. Threads 25: Prepare the thread package release #254

@cursor

cursor Bot commented Jul 16, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat-js-docs Ready Ready Preview Aug 11, 2026 7:33am
chat-js-site Ready Ready Preview Aug 11, 2026 7:33am
sparka Error Error Aug 11, 2026 7:33am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 771ce08f-94d7-4d4b-8fe7-9712a73b1c5e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/threads-v2-14-parallel-card-status

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai 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.

Sorry @FranciscoMoretti, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@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: 67a87b78b9

ℹ️ 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".

message: ParallelResponseStatusMessage | null
): ChatStatus {
const activeStreamId = message?.metadata.activeStreamId;
if (!activeStreamId || status === "submitted" || status === "streaming") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve request errors despite active stream markers

When a parallel request fails after its pending or real activeStreamId has been attached, the selected run reports error, but this branch converts that status back to submitted or streaming. The primary error path only shows a toast and does not clear or refresh the local placeholder, so the composer remains a stop button, submissions stay disabled, and the response continues to appear active until the page is reloaded. Return error unchanged, as is already done for the two active statuses.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a 3-state parallel response lifecycle (queued, generating, complete) to eliminate premature "Task completed" flashes and misplaced action buttons in parallel-model card UIs. It wires the new getResponseAwareStatus helper into the composer, uploads, and paste handlers, and adds optimistic Stop handling that skips server calls for unstarted (pending:*) streams.

  • parallel-response-status.ts / parallel-response-cards.tsx: New getParallelResponseLifecycle and getStatusLabel helpers replace the old boolean isStreaming flag; cards show a spinner for both queued and generating states, and a pendingParallelIndex state auto-navigates to a queued card once its message arrives.
  • stop-response.ts: Adds clearResponseActiveStream (optimistic local clear for the selected message only) and isPendingResponseStream (guards against sending a server stop for not-yet-started streams).
  • start-provisional-chat.ts: Calls setStatus(\"submitted\") at the app submission boundary so the composer is gated immediately during the provisional-chat navigation.

Confidence Score: 4/5

Safe to merge with one fix: the pending-response guard in handleStop reads stream state from the throttled selector while the target message ID comes from the raw store, creating a narrow window where the check evaluates the wrong message.

The core lifecycle logic is sound and the new helpers are well-tested. The one concrete defect is in handleStop: useLastMessageMetadata pulls from getThrottledMessages() while useLastMessageId uses raw state.messages, so between a new message landing and the throttle firing, isPendingResponseStream checks the previous message's activeStreamId rather than the newly-added pending one. A stop action during that window skips the guard and incorrectly sends a server stop request for a message that has no server-side stream yet.

Files Needing Attention: apps/chat/components/multimodal-input.tsx — the handleStop callback's pending-response guard reads from a throttled metadata hook while the message ID comes from the raw store.

Important Files Changed

Filename Overview
apps/chat/components/multimodal-input.tsx Integrates response-aware status for gating input controls and a new optimistic Stop path; the pending-response guard in handleStop reads metadata from the throttled selector while lastMessageId comes from the raw store, creating a window where the wrong message's stream state is checked.
apps/chat/components/parallel-response-cards.tsx Replaces boolean isStreaming with a 3-state lifecycle and adds pendingParallelIndex for auto-navigation on stream start; the stale-pending state on failure was flagged in a previous review round.
apps/chat/components/parallel-response-status.ts New module providing getParallelResponseLifecycle, getStatusLabel, and getResponseAwareStatus helpers; logic is clean and well-tested, but the error guard gap was already flagged in a previous review round.
apps/chat/lib/stop-response.ts Adds clearResponseActiveStream and isPendingResponseStream helpers; straightforward immutable update and predicate, well-covered by unit tests.
apps/chat/lib/stores/hooks-base.ts Adds useLastMessageMetadata hook using throttled messages; missing shallow equality was flagged in a previous review round.
apps/chat/lib/start-provisional-chat.ts Eagerly calls setStatus("submitted") before parallel requests to gate the input; the catch handler not resetting status on total failure was flagged in a previous review round.
apps/chat/components/assistant-message.tsx Extends shouldHideCompletionActions to cover hasActiveResponse and skips reconnection for pending: streams; logic is straightforward and correct.
apps/chat/components/partial-message-loading.tsx Extends the isLoading predicate to show the skeleton for pending: streams regardless of chat status; change is correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User submits message] --> B[setStatus submitted]
    B --> C[runParallelThreadRequestSpecs]
    C --> D{For each model slot}
    D --> E[activeStreamId = pending:X]
    E --> F[getParallelResponseLifecycle to queued]
    F --> G[Card: spinner + Generating...]
    G --> H{Server stream starts?}
    H -- Yes --> I[activeStreamId = stream-X]
    I --> J[getParallelResponseLifecycle to generating]
    J --> K[Card: spinner + Generating...]
    K --> L[Stream ends: activeStreamId = null]
    L --> M[getParallelResponseLifecycle to complete]
    M --> N[Card: Task completed or Selected]
    H -- User clicks Stop --> O{isPendingResponseStream?}
    O -- Yes pending --> P[Skip server stop and clearResponseActiveStream locally]
    O -- No real stream --> Q[stopStreamMutation.mutate and clearResponseActiveStream locally]
    P --> M
    Q --> M
    N --> R[shouldHideCompletionActions = false and Actions visible]
Loading

Reviews (31): Last reviewed commit: "fix(chat): settle stopped response state..." | Re-trigger Greptile

message: ParallelResponseStatusMessage | null
): ChatStatus {
const activeStreamId = message?.metadata.activeStreamId;
if (!activeStreamId || status === "submitted" || status === "streaming") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 "error" not in the short-circuit guard — error state is overridden by a lingering stream ID

When status === "error" (e.g., the primary request fails) and a parallel response's activeStreamId is still set, the early-return guard only catches "submitted" and "streaming", so the function falls through and returns "submitted" or "streaming" instead of "error". In multimodal-input.tsx, the send-button check treats both those values as "still busy", so the composer stays disabled with the misleading "Please wait for the model to finish" message even though the request actually errored and the user should be allowed to retry or submit a new message.

Comment on lines +99 to +100
export const useLastMessageMetadata = () =>
useBaseChatStore((state) => state.getThrottledMessages().at(-1)?.metadata);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 useLastMessageMetadata is the only metadata hook in this file that doesn't use shallow equality. Without it the selector returns a new object reference on every store tick, causing PureMultimodalInput to re-render even when metadata content is unchanged — opposite to the throttling pattern used by every other hook here.

Suggested change
export const useLastMessageMetadata = () =>
useBaseChatStore((state) => state.getThrottledMessages().at(-1)?.metadata);
export const useLastMessageMetadata = () =>
useBaseChatStore(
(state) => state.getThrottledMessages().at(-1)?.metadata,
shallow
);

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +41 to +57
it("keeps selected pending responses stoppable", () => {
assert.equal(
getResponseAwareStatus(
"ready",
createAssistantMessage("pending:assistant-1")
),
"submitted"
);
assert.equal(
getResponseAwareStatus("ready", createAssistantMessage("stream-1")),
"streaming"
);
assert.equal(
getResponseAwareStatus("ready", createAssistantMessage(null)),
"ready"
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 No test coverage for getResponseAwareStatus when status is "error"

The test suite exercises "ready""submitted" / "streaming" / "ready" transitions, but not what happens when the AI SDK sets status === "error" while an activeStreamId is still present. That is precisely the boundary where the short-circuit guard in getResponseAwareStatus matters most; adding a case like getResponseAwareStatus("error", createAssistantMessage("stream-1")) === "error" would pin the intended behaviour and prevent a silent regression.

@FranciscoMoretti
FranciscoMoretti force-pushed the codex/threads-v2-14-parallel-card-status branch from 67a87b7 to 8586679 Compare July 16, 2026 18:25
Comment on lines +123 to +137
useEffect(() => {
if (pendingParallelIndex === null) {
return;
}

const response = cardSlots.find(
(slot) => slot.parallelIndex === pendingParallelIndex
)?.message;
if (!response) {
return;
}

setPendingParallelIndex(null);
navigateToMessage(response.id);
}, [cardSlots, navigateToMessage, pendingParallelIndex]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 pendingParallelIndex never cleared on failed response

When a user clicks a queued card (no message yet), pendingParallelIndex is set to that slot's index. The useEffect only clears it when cardSlots.find(…)?.message is truthy — i.e., when an assistant message with that parallelIndex actually arrives. If the request fails before any message is created for that slot, cardSlots will always show slot.message === null for that index, the if (!response) return short-circuits on every tick, and pendingParallelIndex is never reset.

The visible result: selectedParallelIndex is stuck returning pendingParallelIndex (line 107-109 priority check), so the failed card stays permanently shown as "Selected" with a spinning loader and "Generating…" label. The only escape is clicking a different card whose slot.message is non-null, which calls setPendingParallelIndex(null) via the if (slot.message) branch. If all other slots also fail to produce messages, there is no way to clear the state.

Comment on lines 643 to +648
stopHelper?.();
}, [chatId, lastMessageId, session?.user, stopHelper, stopStreamMutation]);
if (lastMessageId) {
setMessages(
clearResponseActiveStream(storeApi.getState().messages, lastMessageId)
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 clearResponseActiveStream is called unconditionally even when the active response is still in pending state. clearResponseActiveStream sets activeStreamId to null on the last message in the store, which causes getParallelResponseLifecycle to return "complete" for that message. The parallel card then displays "Task completed" for an assistant message that has no content yet, and shouldHideCompletionActions in assistant-message.tsx lets copy/retry/feedback actions appear for the empty message. Guarding the call with !isPendingResponse preserves the "queued" lifecycle until the server either delivers content or the cancellation propagates.

Suggested change
stopHelper?.();
}, [chatId, lastMessageId, session?.user, stopHelper, stopStreamMutation]);
if (lastMessageId) {
setMessages(
clearResponseActiveStream(storeApi.getState().messages, lastMessageId)
);
}
stopHelper?.();
if (lastMessageId && !isPendingResponse) {
setMessages(
clearResponseActiveStream(storeApi.getState().messages, lastMessageId)
);
}

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