Skip to content

fix(web,mobile): Send now tells you why it could not send - #31283

Open
juliusmarminge wants to merge 5 commits into
mainfrom
fix/issue-31236
Open

juliusmarminge wants to merge 5 commits into
mainfrom
fix/issue-31236

Conversation

@juliusmarminge

Copy link
Copy Markdown
Member

Problem

Clicking "Send now" on a queued message could do nothing visible. When the server refuses the send (for example, the active turn is stopping, or the queued message is /compact, which must run as its own turn), the web and mobile clients dropped the failure: it only reached console.warn, no message appeared, and the queued row stayed as it was.

Reproduced on main (935818e) in the real web app: a Codex thread runs sleep 600, a follow-up is queued with Ctrl+Enter, then Stop is clicked and "Send now" ~30 ms later. The server rejects queued-message.promote-to-steer with "Target run … is stopping and cannot be steered.", and the page shows nothing. The same silent refusal happens on iOS and Android when "Send now" is tapped on a queued /compact during an active turn.

Change

QueuedRunsControl (web and desktop) and the mobile queue sheet now keep the result of the "Send now" command (start the queued turn when idle, or steer it into the active run) instead of ignoring it. On a failure that is not an interruption, web shows an error toast "Could not send queued message" with the server's reason; mobile shows the same title in its native alert. The button, keyboard shortcut and mobile row menu all go through these handlers. Remove, Edit and Resume queue are unchanged.

Scope and approval

Fixes #31236

Verification

  • apps/web/src/components/chat/QueuedRunsControl.browser.test.tsx, new test "tells the user why Send now was rejected": the promote command fails with the server's OrchestrationV2DispatchCommandError; fails on main (reason never appears), passes on this branch. 4/4 tests in the file pass.
  • vp run check:changed: format, lint, knip, typecheck (web, mobile and dependents) and tests beside the change.

Evidence

All captures use fresh databases created by each side's own dev server (T3CODE_DEV_SEED=0) and synthetic data. Before = 935818e422, after = 84a4f8f38b (the fix before the merge of main). The composer's model selector is masked in web captures. These are recordings of an interaction, so there are no still pairs and no pixel diff images (Diff0/DiffT: N/A). Native before/after use different synthetic histories, so they are behaviour comparisons, not pixel-matched frames.

Web, 1280×800 dark, real app (Chromium, DPR 2, default fonts). Steps: send "Run the shell command sleep 600 and then reply done. Do not modify files."; wait for the running shell; queue "Evidence follow-up: reply queued done." with Ctrl+Enter; start recording; one script clicks Stop, then Send now ~30 ms later. Both sides receive the same server refusal. Before: nothing changes. After: the toast appears at 1.3 s.

Before (935818e) After (84a4f8f)
Web before: rejected Send now shows nothing Web after: error toast with the server reason

Android, Pixel 9 portrait (411×923 logical, DPR 2.625), real app. A /compact message queued behind an active turn; tap Send now (0.8 s), OK (6.6 s), long-press the row and choose Send now (9.0 s / 12.6 s), OK; dark then light. Server reason: "Context compaction must run as a separate turn. Queue it or wait for the active turn to finish."

Before (935818e) After (84a4f8f)
Android before: Send now refusal is silent Android after: alert with the server reason

iOS, iPhone 17 Pro portrait (402×874 logical, DPR 3), real app. Same /compact refusal: direct Send now, then the row menu; dark then light.

Before (935818e) After (84a4f8f)
iOS before: Send now refusal is silent iOS after: native alert with the server reason
Supplementary captures (keyboard shortcut, narrow viewport, harness controls)

Rows marked harness replace the command result in the running app (fallback text, pending → interruption, injected failures); everything else is the real server.

Web keyboard shortcut, after only, 1280×800 dark, real app: Ctrl+Shift+Enter on a queued /compact during an active turn (0–7.0 s), then dismiss and retry (7.0–15.7 s).

Web after: keyboard Send now shows the refusal toast

Web 390×844 light, after only, real reason text:

Web after, narrow viewport: toast with the full reason

Web controls, 1280×800 dark (harness + real): non-Error failure shows the fallback text; a pending send disables the row's buttons and accepts one command; an interruption shows nothing and the row can retry; real promote and idle resume succeed with no toast.

Before After
Web controls before, dark Web controls after, dark

Web controls, light (harness): idle start failure via click and Ctrl+Shift+Enter at 1280×800; a 273-character reason at 390×844 with Copy error and dismiss.

Before After
Web controls before, light Web controls after, light

iOS controls (harness + real): fallback text, pending → interruption → retry, a failed Remove shows no Send now alert; real idle Send now, real menu promote and real Remove succeed with no alert.

Before After
iOS controls before iOS controls after

Android controls, after only (harness + real): the same sequence as iOS.

Android controls after

What else changed: only the intended feedback: the toast or alert (with its copy, dismiss or OK control, and the alert's dimmed background) appears after a refused Send now. Elapsed-time counters, clocks and the agent's reply wording differ between runs; native histories differ between sides.

Driver scripts (scratch, not committed)

Web race, evaluated in the paired app after queuing the follow-up:

new Promise((resolve) => {
  const stop = document.querySelector('button[aria-label="Stop generation"]');
  const send = [...document.querySelectorAll("button")].find((b) => b.textContent.trim() === "Send now");
  if (!stop || !send) throw new Error("Fixture is not ready");
  const started = performance.now();
  stop.click();
  setTimeout(() => {
    send.click();
    resolve({ delayMs: performance.now() - started });
  }, 30);
});

Queuing /compact behind an active turn through the app's own RPC (evaluated in the dev app; CHECKOUT is the checkout path):

const [{ runAtomCommand }, { appAtomRegistry }, { orchestrationEnvironment }] = await Promise.all([
  import("/@fs" + CHECKOUT + "/packages/client-runtime/src/state/runtime.ts"),
  import("/src/rpc/atomRegistry.ts"),
  import("/src/state/orchestration.ts"),
]);
const environmentId = decodeURIComponent(location.pathname.split("/")[1]);
await runAtomCommand(appAtomRegistry, orchestrationEnvironment.v2.dispatchCommand, {
  environmentId,
  input: {
    type: "message.dispatch", commandId: crypto.randomUUID(), threadId, messageId: crypto.randomUUID(),
    text: "/compact", attachments: [], createdBy: "user", creationSource: "web",
    dispatchMode: { type: "queue_after_active" },
  },
});

Native taps (agent-device on a pool device, dev client from vp run mobile:install):

agent-device press 'label="Send now, queued message 1"' --settle --session "$s"
agent-device press 'text="OK"' --settle --session "$s"
agent-device longpress 'label="/compact"' --settle --session "$s"
agent-device press 'text="Send now"' --settle --session "$s"
agent-device press 'text="OK"' --settle --session "$s"

Not checked: the macOS desktop app the reporter used (it renders the same web component; checked in Chromium on Linux). The reporter's exact model pair (a Codex turn with Claude selected next) could not be run on this machine; the same state with another provider sent successfully, so the reason their sends were refused is not confirmed. Not captured: swipe-to-remove on native, the Android controls recovering after an injected Remove failure, a final settled frame after the last iOS retry, Android before recordings for the harness controls, and a before recording of the keyboard shortcut.

AI-assisted; verified with the tests and tools listed above.

🤖 Generated with Claude Code

juliusmarminge and others added 4 commits November 5, 2026 02:12
A queued message's "Send now" whose promote command is rejected by the
server (e.g. "Target run ... is stopping and cannot be steered.") leaves
the row unchanged and tells the user nothing. This browser test fails on
main until the reason is shown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the server rejected "Send now" on a queued message (for example,
the active run is stopping and cannot be steered), the row stayed put and
the reason only reached the console.

Web now shows the server's message in an error toast; mobile shows it in
the existing Dialog.alert. Interrupted commands stay silent, and a short
generic fallback covers failures without a message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Nov 5, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 8e9cfb1

Macroscope's review found this PR approvable — Queued-message send failures now show an explanation on web and mobile, while interrupted commands remain silent. Existing send behavior is unchanged, keeping the impact limited to error feedback.

No code changes detected at edbf5c6. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The web and mobile queued-message controls now report failed send-now commands unless the command was interrupted. The web control shows a toast, and the mobile control shows an alert. A browser test checks that the web toast displays the rejection reason.

Changes: Queued-message send failure feedback

Layer / File(s) Summary
Handle send-now failures across clients
apps/web/src/components/chat/QueuedRunsControl.tsx, apps/web/src/components/chat/QueuedRunsControl.browser.test.tsx, apps/mobile/src/features/threads/ThreadQueueControl.tsx
The web control shows an error toast, and the mobile control shows an alert, for non-interrupted send-now failures. Each uses the error message when available and a generic fallback otherwise. The browser test verifies that the web toast displays the rejection reason.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related issues

Possibly related PRs

  • pingdotgg/t3code#13764 — Moves queued-message dispatch into sendQueuedMessage and adds failure feedback for that send path.
  • pingdotgg/t3code#25846 — Adds the Send now action and routes it through the queue send flow that this change updates.
  • pingdotgg/t3code#25035 — Exposes rejection reasons for queued standalone commands, which this change reports when Send now fails.

Suggested reviewers: yash-singh1

Merge Risk: Minimal · up to edbf5

Send now displays the available server refusal reason on web and mobile. No actionable merge-blocking risk remains after normal checks.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title clearly summarizes the main change: Send now reports why a queued message could not be sent.
Description check Passed The description covers the problem and reproduction, the web and mobile changes, the linked issue, focused verification, and UI evidence. It also identifies checks that were not completed.
Linked Issues check Passed Issue #31236 requires visible feedback when “Send now” cannot proceed. The web and desktop queue control now shows an error toast with the failure reason. The mobile queue sheet shows a native alert w…
Out of Scope Changes check Passed All changed files support issue #31236. The web queue control implements the feedback, the mobile queue sheet applies the same feedback behavior, and the browser test verifies the rejection message. N…
Full details: Linked Issues check

Explanation

Issue #31236 requires visible feedback when “Send now” cannot proceed. The web and desktop queue control now shows an error toast with the failure reason. The mobile queue sheet shows a native alert with the failure reason. The browser test verifies that a rejected promotion displays the server reason. The handlers suppress interruption-only results; the issue concerns unexplained send refusals, and the reviewed evidence does not show that this exception leaves those refusals silent.

Full details: Out of Scope Changes check

Explanation

All changed files support issue #31236. The web queue control implements the feedback, the mobile queue sheet applies the same feedback behavior, and the browser test verifies the rejection message. No unrelated changes appear in the whole-PR diff.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Usage-based review receipt

Note

This review was completed with usage-based billing: files reviewed beyond your plan's included limits are billed at $0.25/file. View usage-based billing.

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 8e9cfb1 and edbf5c6.

Files selected for processing: 2

  • apps/mobile/src/features/threads/ThreadQueueControl.tsx
  • apps/web/src/components/chat/QueuedRunsControl.tsx

Limit details: You’ve used all 10 included reviews currently available.

@juliusmarminge juliusmarminge added bugfix Fix PR opened by an issue lead; enters the PR loop pr:gate-failed A land gate failed labels Nov 5, 2026
@shivamhwp

Copy link
Copy Markdown
Collaborator

[GPT-6.1 Sol][Codex] on behalf of shivam.

Issue watch picked up #31236 and found this PR already owns the fix. The report names the macOS desktop app; no independent reproduction or verification was started in the issue-watch thread. No code changes or competing PR were made. Handing off to this PR and stopping the issue-watch work under fleet's Julius takeover rule.

# Conflicts:
#	apps/mobile/src/features/threads/ThreadQueueControl.tsx

Copy link
Copy Markdown
Member Author

Evidence

Fresh isolated databases, identical synthetic inputs, separate base/head worktrees. Web: 1280×800, dark theme. Native iOS: iPhone 17 Pro, 402×874 logical viewport, light theme. Every recording includes a successful retry of the retained queued message.

  1. Stopping rejection: run sleep 600, queue a follow-up with Ctrl+Enter, then Stop → Send now with a scripted 30 ms delay. Both real servers reject steering because the run is stopping. Base is silent; head explains the failure.
  2. Resume rejection: race another queued start against Send now from the stopped view. Both servers reject the second start. Base is silent; head explains the failure.
  3. Interruption: cancel the actual Send now RPC using its standard Interrupt message. Both clients correctly stay silent and permit retry.
  4. Native iOS: delay subscription updates, stop from web, then tap native Send now from the stale running view. Both servers reject steering of the interrupted run. Only head displays the native alert. Restore updates and retry successfully.

The diagnostic strip below each recording is added presentation of captured real RPC results, separate from application UI. It makes the otherwise silent base rejection inspectable. Requests and server failure results were not mocked.

Before After
1a2a4a5 edbf5c6
Web stopping rejection: Recording · RPC transcript Web error explanation: Recording · RPC transcript
Web resume rejection: Recording · RPC transcript Web resume explanation: Recording · RPC transcript
Web interruption stays silent: Recording · RPC transcript Web interruption stays silent: Recording · RPC transcript
Native iOS silent rejection: Recording · RPC transcript Native iOS alert: Recording · RPC transcript

Reproduction material: stopping-race driver, resume-race driver, interruption driver, native driver and seed instructions, subscription-delay proxy.

AI-assisted; verified with the tests and tools listed above. Runtime recordings and actual RPC responses are the evidence; tests were not used as proof.

Not shown:

  • Android native rendering was not captured: the pool worker was unreachable and subsequently reported adb missing from PATH after retry. Native alert behavior was demonstrated on iOS.
  • The native resume-failure branch and native pure-command-interruption guard were not separately captured. Both corresponding web paths were captured; native steering failure and successful resume retry were captured.
  • The original approximately 30 ms Stop → Send now interaction was demonstrated on web. Native iOS used the documented cross-client stale-subscription variant with delayed updates, producing an interrupted-run rejection rather than a stopping-run rejection.
  • The Electron wrapper was not launched; the shared web implementation was exercised in the browser.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop pr:gate-failed A land gate failed size:M 30-99 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Send now does nothing

2 participants