Skip to content

feat(chat-core): route ChatPage's send and steer through the transport - #8689

Merged
chenmingwei23 merged 1 commit into
mainfrom
feat/chat-core-p2-chatpage
Sep 5, 2026
Merged

feat(chat-core): route ChatPage's send and steer through the transport#8689
chenmingwei23 merged 1 commit into
mainfrom
feat/chat-core-p2-chatpage

Conversation

@CrysisDeu

@CrysisDeu CrysisDeu commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Problem / Motivation

ChatPage — the main chat surface — was the last user-facing composer still hand-rolling its send (App.tsx's feedback send also hand-rolls api.sendChat + readSendReceipt, with no deadline; it is not a composer and is listed below as its own slice). send() owned an AbortController and a 10 s timer, called api.sendChat, read the body through readSendReceipt, and had a catch that told an AbortError from a network failure by hand. Its mid-turn steer was a second, worse copy: a separate api.steerChat helper posting to the same /api/chat?ws=1 endpoint with steer: true, no deadline at all, and a failure that surfaced only as a console line. Every rule the transport layer exists to own — the resolves-not-rejects trap, the deadline, the unreadable-2xx classification — was re-learned here, twice.

Why it matters

This is RFC chat-core extraction P2 (transport), third and last composer slice. With ChatPane (#5909, merged) and this PR, both main-based composer sends go through website/src/chat-core/transport/sendTurn.ts; ChatEmbed (#8599) and SideChat (#8655) are in review on their own branches, and once those land every user-facing composer send is on the transport, so a change to the send contract is made once. Merge order among the three is free — this PR touches sendTurn.ts only additively (two optional flags). It also removes the last bespoke steer POST, so a steer is classified like any send — dispatched, queued (demoted) or refused — instead of "resolved or threw".

What changed (motivation → approach → change)

Transport (sendTurn.ts). SendTurnOptions gains the two flags this endpoint takes that ChatPage sets: steer ("act on this now" — inject into the running turn, or on an idle slot skip the hold that parks a message behind still-running sub-agents; a flag of the endpoint, not a new receipt shape — body.steered says which happened) and colorTheme (so a widget the turn renders inherits the active theme; only a surface that owns a theme sends it). Both are forwarded to api.sendChat and absent from the wire when unset. Contract test +1.

ChatPage send(). The controller/timer/api.sendChat/readSendReceipt/try-catch block is replaced by one await sendTurn({ message, slot, meta, steer, colorTheme }) and a status switch that maps 1:1 onto the previous branches: transport-error → the old catch branch (connection error row, running cleared, composer restored) — with both failure branches now addressed to the sending slot: a new slot-keyed endLocalTurn(slot) (the inverse of the startLocalTurn(slot) the send dispatched) replaces setSlotRunning(false), and the error row goes through appendSlotMessage({ slot }), so a failure that lands after the user switched to a running session neither clears that session's running state nor drops its error there (the old code did both); response-late → the old AbortError branch (nothing: the message was received, the WS delivers, the bubble's delivery indicator says "unconfirmed"); refused → the old refused branch, with receipt.reason now framed through pages.chatPage.send_failed_with_error ("Send failed: …") like the steer's and ChatEmbed's refusals, instead of the raw backend reason; unknown → falls through exactly as before (the body-driven steps below read ok/queued and are no-ops on an empty body); dispatched/queued → the old accepted path. The queued-send stash, steer-echo append, confirmOptimisticSend, stateless-card and folder-card retirement, and resolveAskAfterSend are byte-for-byte the previous code, now one indentation level shallower.

ChatPage steer. steerMutation's mutationFn is sendTurn({ message, slot, steer: true, meta: { sendId } }); onSuccess reads the receipt. The composer is cleared at submit and the optimistic bubble is not persisted (the next transcript rebuild drops it), so a steer that did not provably reach the gateway hands its text back — merged into the draft of the slot it was sent from (mergeRecoveredDraft, the helper every recovery site in this file uses), and every row it appends goes through appendSlotMessage({ slot }) — so a receipt that lands after the user switched sessions restores into the originating session's draft and transcript, never the one on screen. "On screen" is decided by composerSlotRef (which slot the live composer state belongs to), not activeSlotRef: during a switch the active slot flips before the composer flushes the outgoing slot's text, and a draft written in that window would be overwritten by that flush — updating the live input instead is what the flush then persists. send()'s restoreComposerAfterFailedSend takes the same ownership test in this PR, closing the same window there. Before this PR a failed steer's text was simply gone; attachments and pastes still are, as documented at the call site. refused / transport-error → the optimistic bubble is dropped (the reducer's drop arm; standing, it would be a third, false representation of the same text next to the error row and the refilled composer), then an error row (the server's reason framed through send_failed_with_error, or the cause-stating connection copy that names the restore — pages.chatPage.send_failed_connection, "Couldn't send — check your connection and try again.", new here with the same copy #8655 promotes to this key; send()'s own transport failure takes it too instead of a bare "Connection error", and so does ChatPane's reason-less transport failure — the one already-on-transport sibling that still said bare "Send failed"; the same hunk #8655 carries, so the branches merge cleanly) + restore; response-late → if the server's echo has already reconciled the bubble the steer landed and nothing happens; otherwise the optimistic bubble is removed through resolveOptimisticSteer's existing drop arm (queued; standing, the bubble would read as delivered and make "check the transcript" unanswerable — a late echo re-creates the row from the server's copy), the text is restored, and a warn-tone notice row (⚠️ lead glyph = NoticeCard's tone selector and its "Warning" screen-reader label; the glyph is added at the dispatch so the catalog string stays shared) says "Delivery not confirmed — your text is back in the composer. Check the transcript before sending it again." (new key pages.chatPage.delivery_unconfirmed, 12 locales + en-XA, the same copy #8655 promotes to this key) — the steer never had a deadline before this transport, so a stalled socket the abort kills is a new way for the text to be lost, and the notice makes a duplicate visible rather than a loss silent; unknown → nothing (accepted, receipt unreadable); steered → keep the badge; otherwise resolveOptimisticSteer to queued or turn as before.

Not in this PR (by design). No render-chain change — the 29 role === branches, the composer, the virtualizer are untouched (P5-a is the next slice). api.steerChat stays in the client: hooks/useSceneInteraction.tsx still calls it and is its own P2 item. mintSendId stays local to ChatPage here; #8599 hoists it to chat-core and this branch takes that on rebase.

Tests

  • chatCoreTransport.contract.test.ts (+1): steer and color_theme ride the wire when set and are absent when not; a steered acceptance is classified dispatched with body.steered.
  • chatSlice.test.ts (+3): endLocalTurn clears the active slot's footer and pending mark; leaves a switched-to running session alone; leaves another slot's pending mark alone.
  • ChatPane.dirSend.test.tsx (updated): a reason-less transport failure on the pane pins the connection copy.
  • ChatPage.steerQueuedReceipt.test.tsx (+4) / ChatPage.steerWithSubagents.test.tsx / SideSlashCommand.steer.test.tsx (updated): the mid-turn steer now asserts api.sendChat with the steer flag and a meta.sendId and no theme, and that api.steerChat is never called; the idle steer-flagged send asserts the theme is attached (the two paths stay distinguishable on the wire). New: a refused steer drops its bubble, restores the text and appends "Send failed: "; a never-left steer drops its bubble and restores with the cause-stating connection copy; a deadline-aborted steer removes the unconfirmed bubble, restores, and appends the ⚠️ notice; a refusal that lands after the user switched sessions restores into the originating slot's draft and transcript while the on-screen composer is untouched.
  • Compatibility evidence — unmodified: the other 61 ChatPage* test files (578 tests across all 63, plus the 22 non-ChatPage suites that mock api.sendChat) pass as-is, including every send-failure, send-confirm, knowledge-merge, create-failure and dictation suite that mocks api.sendChat with a Response-shaped value — the transport calls the same client method with the same arguments.

Local gates: tsc -b, eslint on changed files, check-i18n-strings (0 added), all ChatPage* + transport contract tests. The full vitest suite runs in CI.

Manual verification

N/A — pure transport collapse with 1:1 branch mapping and deterministic coverage; the one new visible element is the existing notice row (NoticeCard, already rendered by this surface) carrying one new sentence, on a path (a steer whose POST the deadline aborted) that needs a stalled gateway to reach.

Why no screenshot: no new component, style, layout, motion or affordance — the deadline-aborted steer reuses the surface's existing notice row renderer with new copy; everything else maps each receipt status onto the branch the surface already had.

Related Issues

Checklist

  • Single commit, conventional title
  • Tests added/updated for behaviour that changed; compatibility evidence for behaviour that did not
  • i18n: two new keys (pages.chatPage.delivery_unconfirmed, pages.chatPage.send_failed_connection), 12 locales + en-XA, style gates pass; both share their copy with feat(chat-core): route SideChat's send through the transport over a side wire #8655 so the branches converge
  • No render-chain changes

Contribution License Agreement

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@CrysisDeu
CrysisDeu requested a review from a team September 5, 2026 07:32
@CrysisDeu
CrysisDeu requested a review from a team as a code owner September 5, 2026 07:32
@CrysisDeu
CrysisDeu requested a review from bolichen97 September 5, 2026 07:32
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of 935f2801b7547651cd5fd7594e0da2c42be02a8e — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: PASS

Failed and unconfirmed steers now visibly fail, hand the text back, and say what to do next — previously they died in the console.

[UX-REVIEWED] 935f280

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of 935f2801b7547651cd5fd7594e0da2c42be02a8e — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: PASS

Additive transport flags plus per-surface receipt policy is the established series shape; the steer gains a deadline and a recovery path it never had, with each new window reasoned and tested.

[DESIGN-REVIEWED] 935f280

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 935f2801b7547651cd5fd7594e0da2c42be02a8e and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 935f280

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 935f2801b7547651cd5fd7594e0da2c42be02a8e: <one-sentence reason>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of 935f2801b7547651cd5fd7594e0da2c42be02a8e — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All mechanical checks done. Verified: steerChat keeps 1 real consumer (declared), endLocalTurn has 1 defect-derived consumer, both new transport flags map to pre-existing api.sendChat positional params, composerSlotRef pre-exists (ChatPage.tsx:1493), all 12 languages + en-XA covered, and the new key is not a second spelling of connection_error (that key's surviving consumer is upload copy). Remaining hand-rolled sendChat+readSendReceipt sites counted: 4.

First-Principles-Verdict: PASS

Migrates the last main composer's send and steer onto the existing transport, deleting the bespoke steer POST; every rider is declared with a named defect.

What this change ships

Intent: make ChatPage's send and mid-turn steer go through the shared chat-core transport so the send contract lives in one place. FIX-shaped consolidation (deletes duplicated transport logic) with declared behavior fixes riding along.

  1. ChatPage send routed through sendTurn; receipt branches map 1:1 to old paths — justified
  2. Mid-turn steer uses the same transport; failures classified instead of console-logged — justified
  3. Failed/refused/unconfirmed steer hands its text back (was silently lost) — justified, data-loss fix
  4. Unconfirmed steer shows a warn notice, new key in 13 locale files — justified, derived from the transport's deadline (a new loss window)
  5. Send/steer failures address the sending slot, not the on-screen one (endLocalTurn, 1 consumer) — justified, defect-derived
  6. Composer restore keyed to live composer ownership (composerSlotRef), closing the mid-switch clobber — justified
  7. Refused-send reason framed as "Send failed: …" like sibling surfaces — declared relabel, named confusion
  8. Connection failures state cause + retry copy; ChatPane's bare "Send failed" takes it too — rides along, declared
  9. sendTurn gains steer/colorTheme flags (2 and 1 consumers; both pre-existing endpoint params) — justified
  10. Bespoke steerChat path removed from ChatPage; helper kept for its 1 remaining consumer (useSceneInteraction.tsx:347) — justified deletion

Watch

  • The ChatPane hunk and both catalog keys are deliberately duplicated in in-review feat(chat-core): route SideChat's send through the transport over a side wire #8655 ("the same hunk feat(chat-core): route SideChat's send through the transport over a side wire #8655 carries"); whichever lands second must match byte-identically across 13 locale files or the merge conflicts. Declared, but a human should confirm the copies stayed in sync.
  • After this PR, 4 hand-rolled api.sendChat+readSendReceipt sites remain (grepped readSendReceipt|api\.sendChat\(, non-test): App.tsx:2723, hooks/useSceneInteraction.tsx:355, apps/issue-radar/lib/agentSession.ts:283, apps/auto-improvement/lib/agentSession.ts:240. The first two are declared future slices; the two app seed sends sit outside the stated "composer" scope — accepted-and-deferred, not a demand.

[FIRST-PRINCIPLES-REVIEWED] 935f280

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 935f2801b7547651cd5fd7594e0da2c42be02a8e — this comment is updated in place on each push.

Review details

No findings.

The single candidate — \u26A0\uFE0F at ChatPage.tsx:1727 as a no-emoji-as-icons violation — does not survive falsification. The glyph is never rendered as an emoji: it is the established NoticeCard tone-selector protocol, consumed and stripped by parseNotice's LEAD_EMOJI_RE (NoticeCard.tsx:20-27) which renders a Lucide TriangleAlert in the leading slot instead. It is data, not a UI icon — the same convention already carried by ChannelPage.tsx and subagentCompletion.ts, which pass the existing gate. No observable wrong outcome exists.

[OPUS-REVIEWED] 935f280

Verdict parsed from the review's SHA-scoped output markers for commit 935f2801b7547651cd5fd7594e0da2c42be02a8e.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 935f2801b7547651cd5fd7594e0da2c42be02a8e: <one-sentence reason>

@CrysisDeu
CrysisDeu force-pushed the feat/chat-core-p2-chatpage branch from 0e79150 to 05a7d0d Compare September 5, 2026 07:58
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

GPT — BLOCKING: the deadline path can discard a steer that never reached the gateway (0e7915097 → fixed in 05a7d0ddd)

Accepted — this PR gives the steer a deadline it never had, so a stalled socket the abort kills is a new loss window on text the composer already cleared, and the optimistic bubble is not persisted. Fixed with the receipt policy the rest of the series uses rather than a retain-until-echo state machine (which is #6825's bubble-fate work): refused / transport-error → text merged back into the composer (mergeRecoveredDraft, the helper every recovery site in this file uses) + the old console line; response-late → text merged back under a "Delivery not confirmed — … check the transcript before sending it again" notice row (new pages.chatPage.delivery_unconfirmed, 12 locales; the same copy #8655 promotes to this key), so a possibly-delivered steer becomes a visible duplicate the user can delete rather than a silent loss. Before this PR a failed steer's text was simply gone (documented at the call site); attachments/pastes are still not restored, as before. Tests: refused, never-left and deadline-aborted steers each restore the text; only the deadline adds the notice. Also fixed the CI red on the first head: SideSlashCommand.steer.test.tsx asserted the retired api.steerChat path and now asserts sendChat(steer=true) with meta.sendId.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

First Principles — Subtraction: delete api.steerChat when useSceneInteraction moves, rather than adapting it (0e7915097)

Agreed and recorded: useSceneInteraction.tsx is on the remaining-sites list with both its plain send and its steerChat call; that slice moves both onto sendTurn (steer: true for the latter) and deletes the helper in the same commit.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 5, 2026
@CrysisDeu
CrysisDeu force-pushed the feat/chat-core-p2-chatpage branch from 05a7d0d to cd8ef7d Compare September 5, 2026 08:26
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

GPT — BLOCKING: failed steers are restored into the wrong session / Opus — FINDING: same / UX — Suggestion: same (05a7d0ddd → fixed in cd8ef7dba)

Accepted (three lanes, one gap). The steer's onSuccess is now addressed to the sending slot the way send()'s restoreComposerAfterFailedSend and its steer-echo append already are: onScreenNow = slot === activeSlotRef.current; the text is merged into that slot's persisted draft (setDraft(drafts.current, slot, …) + saveDrafts()), the live composer is updated only when that slot is on screen, and every row goes through appendSlotMessage({ slot }). New test: steer from A, switch to B and type, A's POST fails → B's composer untouched, A's draft holds the text, the error row is in A's transcript and not in the live messages.

@github-actions github-actions Bot removed the readiness: action required A blocking check or review needs attention label Sep 5, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

UX — Watch: "Check the transcript" cannot be acted on (the optimistic bubble reads as delivered) (05a7d0ddd → fixed in cd8ef7dba)

Accepted. On response-late the bubble is now removed: resolveOptimisticSteer gains an unconfirmed outcome that splices the row while it is still optimistic (a late steer_push that does arrive re-creates it from the server's copy — reconcileOptimisticEcho appends when no row carries the sendId). And if the echo has already reconciled the bubble by the time the deadline fires, the steer landed and the handler does nothing — no restore, no notice, no duplicate invitation. Test asserts the bubble is gone alongside the restore.

@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 5, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

UX — Watch: decision-critical copy rendered as routine (info tone, nothing announced) (05a7d0ddd → fixed in cd8ef7dba)

Accepted. The dispatch prefixes \u26A0\uFE0F so parseNotice selects the warn tone (triangle icon, text-warn, "Warning" screen-reader label). Added at the dispatch rather than in the catalog string so the copy stays shared with the surfaces that render it in their own strip (#8655). Test pins the glyph prefix.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

UX — Watch: a refused steer says nothing at all (05a7d0ddd → fixed in cd8ef7dba)

Accepted. refused / transport-error now append an error row to the sending slot — the server's reason framed through pages.chatPage.send_failed_with_error ("Send failed: no running turn"), or the connection copy when there is no reason — exactly what send() shows for the same two outcomes, and then restore. Tests: refused → "Send failed: no running turn" + text back; never-left → connection copy + text back.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

GPT — BLOCKING: transport rejection does not prove the steer never arrived; treat transport-error as unconfirmed (05a7d0ddd) — disputed

The transport contract (sendTurn.ts, pinned by chatCoreTransport.contract.test.ts) defines transport-error as "the fetch itself rejected — the send never left, so restore-and-report is safe", and every caller in the series — ChatPane (#5909), ChatEmbed (#8599), SideChat (#8655) and send() in this very file, unchanged by this PR — restores and reports on it. A TCP reset after the request bytes were written is real but is the same residual send() has always carried, and re-classifying one caller's transport-error as unconfirmed would make the steer the only path in the codebase whose receipt vocabulary disagrees with the contract. The refused/transport-error branch now also appends the connection-copy error row, so the outcome is visible either way. Overriding to keep the series' contract consistent; if the contract should change, that is a transport PR, not a per-caller exception.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

Design — Suggestion: migrate useSceneInteraction's api.steerChat caller in a follow-up so the duplicate steer path can be retired (05a7d0ddd)

Agreed — recorded on the remaining-sites list (its plain send and its steer both move onto sendTurn in that slice and the helper is deleted in the same commit, per FP's round-1 subtraction).

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/ai-review override gpt cd8ef7d: transport-error is defined by the sendTurn contract (pinned by chatCoreTransport.contract.test.ts) as never-left/restore-safe, and every caller in the series incl. send() in this file treats it so; a per-caller reclassification would make the steer the one path disagreeing with the contract. The branch now also appends the connection error row, so the outcome is visible. Changing the vocabulary belongs to a transport PR.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/ai-review override gpt cd8ef7d: transport-error is defined by the sendTurn contract (pinned by chatCoreTransport.contract.test.ts) as never-left/restore-safe, and every caller in the series incl. send() in this file treats it so; a per-caller reclassification would make the steer the one path disagreeing with the contract. The branch now also appends the connection error row, so the outcome is visible. Changing the vocabulary belongs to a transport PR.

@github-actions github-actions Bot added the readiness: action required A blocking check or review needs attention label Sep 5, 2026
@CrysisDeu
CrysisDeu force-pushed the feat/chat-core-p2-chatpage branch from 663bc3a to 5f35ccc Compare September 5, 2026 10:27
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

GPT — BLOCKING: late send failures corrupt the newly active session (setSlotRunning(false) + appendMessage in both failure branches) (663bc3ab9 → fixed in 5f35ccc08)

Accepted. The old catch did the same, but with the steer now slot-addressed the send had no excuse. Both failure branches go through one failLocalTurn(message): endLocalTurn(slot) — a new slot-keyed reducer that is the exact inverse of the startLocalTurn(slot) the send dispatched (clears the pending mark only if it is this slot's, and the visible footer only if this slot is the active one) — plus appendSlotMessage({ slot, message }); the active-slot fallback is kept only for the no-slot case. chatSlice.test.ts (+3): the inverse for the active slot; a switched-to running session keeps slotRunning; another slot's pending mark survives. 846 tests green across the ChatPage, steer and slice suites.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 5, 2026
ChatPage's composer send and its mid-turn steer each hand-rolled the
POST to /api/chat: an AbortController and 10 s timer, the
resolves-not-rejects trap, readSendReceipt, and a catch that had to
tell an abort from a network failure. The steer path was worse -- a
separate api.steerChat helper with no deadline whose failure was only
a console line. Both now call sendTurn (chat-core transport) and
branch on receipt.status; the receipt policy (error row, composer
restore, optimistic confirm, card retirement, ask resolution) is
unchanged and stays on the surface.

sendTurn gains the two flags this endpoint takes that ChatPage sets:
steer (inject into the running turn / skip the sub-agent hold) and
colorTheme. api.steerChat has no send-path consumer left on this
surface (useSceneInteraction still uses it; its own P2 slice).

RFC chat-core extraction, P2 (transport), third slice: after
ChatPane (#5909), ChatEmbed (#8599) and SideChat (#8655), every
user-facing composer send is on sendTurn.
@CrysisDeu
CrysisDeu force-pushed the feat/chat-core-p2-chatpage branch from 5f35ccc to 935f280 Compare September 5, 2026 13:31
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 5, 2026

@chenmingwei23 chenmingwei23 left a comment

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.

Approved: checks green at head, readiness passed, no blocking reviewer verdict outstanding.

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.

2 participants