ChiGaoandQwen-Coder b0afb6c196 fix(cli): close twelve OpenTUI parity gaps found against ink (#11806)
* fix(cli): draw OpenTUI tool confirmations inline as ink does

ink renders a tool confirmation inline in the transcript, as a sibling
directly below the row of the tool call that is waiting, with no border, no
title row and no navigation hint; the awaiting call is marked by an arrow
appended to its own row. This renderer instead boxed the same content at
full width, gave it a title, added a separate row announcing that the call
was awaiting approval, and kept a hint about navigating a list. Approving a
command therefore read as answering a modal rather than as continuing the
transcript, and the box pushed the question and its options several columns
past where ink puts them.

The border, the title and the hint are gone. The body, the question and the
outcome list now sit at the columns ink uses, the awaiting arrow rides the
tool card's own row instead of taking one, and while a call is parked the
spinner swaps to ink's waiting phrase and drops the elapsed-time, token and
cancel suffix — there is no in-flight request to cancel and no tokens to
count. That waiting row is rendered only for a tool confirmation: the MCP
startup approval, the shell gate and the action confirmations all arrive
while idle, where ink's indicator renders nothing, and the captured frames
agree.

The row budgets that reserved space for the removed chrome shrink with it.
Those bounds turn out to be inert at the viewport heights the harness uses,
so the change was verified against ink's own frames at a hundred columns
rather than against the arithmetic: the confirmation's row sequence now
matches ink line for line. What still differs is where the block sits
vertically — this renderer anchors it to the bottom of the viewport while
ink flows it straight after the tool row — which is a separate known
divergence and is not addressed here.

* fix(cli): let a plain-text tool update replace OpenTUI's structured payload

A tool card prefers a structured payload over its flattened text and does so
unconditionally, which is what lets a shell run paint a styled token grid, a
diff render its coloured lines and a todo write render its status list. The
history fold that feeds the card only ever added those payloads, though, and
never cleared one, so the preference could outlive the payload it was chosen
for.

A shell command hits exactly that: it streams styled output while it runs and,
if the stream then trips binary detection, the accumulated output is replaced
by a plain string. The card kept the last grid it saw and showed it forever,
including after the call settled, so the user was told nothing about the
binary output and never saw the byte counts that followed. The same shape
applies to any tool whose display changes kind mid-flight, since a result
carries at most one structured payload.

Each event carries the whole display, so the structured fields are now derived
from the event rather than accumulated onto the card. The three producers of
these events all pick the structured form or the flattened form and never emit
both for one result, and the trailing flush that could have delivered a late
update after completion is cancelled before the call returns, so nothing
regains a payload that a later event legitimately dropped. The egress
disclosure keeps its existing sticky behaviour: it is an additional notice
under the result rather than an alternative rendering of it, and it takes no
part in the card's preference.

* fix(cli): port ink's question dialog to OpenTUI in full

The ask_user_question confirmation had been reduced to its literal options: no
free-text row, no description under any option, no digit keys, and a full-width
box around a dialog that ink draws inline under its own transcript entry. A
selection therefore cost a navigation and a confirmation instead of one
keypress, and an answer the model had not happened to invent could not be given
at all.

The dialog is now ported whole from ink's own component: a chip row of question
headers with a review-and-submit tab after them, numbered options carrying
their descriptions, multi-select checkboxes, and a trailing free-text row that
takes a bracketed paste of its own, since the composer that would otherwise
consume one is unmounted while a confirmation owns the screen. The chip row's
width is derived rather than guessed, by reusing the water-filling helper ink
sizes it with and subtracting every cell the row spends outside the header text
from the width a tool confirmation actually gets in the transcript.

One divergence is deliberate. ink subscribes to Enter twice on the free-text
row, once from the dialog's key handler and once from the input widget it
mounts, and its key layer has no focus stack to arbitrate between them, so a
typed answer advances two tabs and silently skips the question after it. That
was observed on a real run rather than inferred: the acceptance leg timed out
waiting for a question that was never drawn, and the review tab reported it as
unanswered. Every key here goes through one handler, so Enter advances exactly
one tab. Fixing it in ink would move the baseline this sweep measures itself
against, so the defect and the divergence are both recorded in the design doc.

Typing also builds each next value from a mirror written synchronously rather
than from the value the registering render captured, because keystrokes out of
one pty read can share a React batch and only the last of them would otherwise
survive. Removing the mirror fails exactly the test that pins it, with a
five-character answer rendering as a single character.

The acceptance harness needed a fix of its own before this could be measured.
It waited on the byte stream, and a renderer that repaints only the cells that
changed never puts a line edited in place into that stream as one contiguous
string, so a leg timed out on a screen that was already correct. Waits now poll
the reconstruction as well, less often than the stream because reading it
flushes the pty and walks the whole buffer.

Verified on a real machine against ink over eight checkpoints of a
three-question dialog with a multi-select and a twenty-character typed answer,
plus the two checkpoints of the single-question scenario. Every dialog row
matches on text and on indentation, including the mark appearing on the header
chip as soon as its free-text box is checked and the review tab dropping the
cancel clause from its hint. The rows that still differ on those frames are the
long context-file path wrapping at a different column, the spinner's braille
phase, and this renderer's deliberately reworded update notice. Across the
whole matrix the single-question pair drops from twelve and eighteen differing
rows to four and five, and no other pair moves at all.

Not verified: colour, since the reconstruction is text; the vertical position
of the block, which bottom anchoring moves by construction; and whether the
free-text cursor's background-filled cell survives layout, which a text frame
cannot show.

* fix(cli): keep a keystroke burst intact in OpenTUI's auth inputs

The shared line-input helper behind the endpoint and API-key steps built each
next value from the value the registering render had captured. Keystrokes read
out of one pty buffer can be delivered inside a single React batch, where no
render separates them, so every character in the burst appended to the same
stale snapshot and the input ended up holding only the last one. A pasted
string was safe because a bracketed paste arrives as one event carrying the
whole text, and ordinary typing at human speed was safe because each keystroke
flushed a render first, which is why the existing tests, one act per character,
never saw it.

The helper now keeps a mirror of the value that is written synchronously on
every change and re-synced on render, so each event appends to what the
previous one produced while a value set from anywhere else is still picked up.
Both steps reach it through plain state setters that store exactly what they
are given, so the mirror converges with the parent on the next render and the
fix stays inside the helper, leaving ink's own wizard untouched.

Pinned by dispatching a whole burst inside one act and asserting the input
holds all of it and carries it into the next step. Taking the mirror out fails
that test alone, with a twelve-character key rendering as its final character.

This is correctness by construction: no truncation was observed on a real
machine, and no acceptance scenario drives the authentication wizard, so the
fix has unit coverage only.

* fix(cli): draw ink's inline tool arguments row in OpenTUI

With `ui.showToolCallArgs` on, ink writes a call's raw arguments on a dim row
under the card header, capped to two wrapped rows with the remainder collapsed
into `… +N chars (ctrl+o)` and printed in full once ctrl+O puts the session in
its detail state. The setting had no reader here, so the row existed nowhere
and a call's payload stayed invisible outside its one-line summary.

The policy is not reimplemented. Serialisation, the deduplication against a
description that already is the payload, the character budget scaled by the
header's own inner width, the surrogate-safe walk and the prompt-injection
sanitisation all come from the helper ink uses, which is split so that both
renderers read one source of truth for the row.

Investigation corrected the premise before a line was written: the live path
already carried the arguments, on the event that opens the card, and that is
the same value ink's setting reads. Only two carriers were missing — the
steering card raised by an `@`-mentioned file, and a resumed session's
transcript — and the row still drew nowhere because nothing read the field off
the item. A first attempt added a fourth source in the scheduler loop and was
reverted once the tests showed it emitting the same row twice per call; what it
exposed was that the pre-existing carrier had no test at all, so a fold that
dropped it would have passed. That carrier is pinned now, along with the two
that were filled in.

The arrow that marks an awaiting call moved onto ink's gate with it. It was
drawn on every pending card, while ink draws it on the first one — this
renderer shows only that call's dialog, so every other arrow marked a call the
user could not act on.

Frame-verified against ink on a real machine while the call is pending, after
it settles, and in the untruncated state, at 100 columns. Two divergences are
recorded rather than closed: the row costs this renderer one more physical row,
which a control arm raising the same header with the row switched off reproduces
unchanged, so it belongs to the wrap rule and not to this row; and ink's
per-group height reservation has no counterpart where every card is handed the
same per-item ceiling.

* docs: correct the row-budget claim the viewport fix overturned

Drawing the confirmation inline was recorded as making the row budget that
reserved space for the removed chrome inert at the heights the harness uses.
It is not inert, and the harness never reaches the height where it binds: the
reserve was raised upstream after a long confirmation on an 80-row terminal
pushed the payload's tail and the outcome list past the bottom edge, and this
branch was rebased onto that fix. Both language versions now record the
re-derivation and the two arms that bound the value — the scenario is green at
the shipped reserve and times out waiting for the tail at zero, but stays green
four rows lower, so the arithmetic is what pins it.

* docs: retire the two card gaps this change closed

The follow-up list still recorded the missing inline arguments row and the
pending indicator on every card, both of which the last commit on this branch
drew. Lead the item with the gap that stays open — a settled card keeps the
position it was created at — and say what closed the other two, so a reader
does not implement a fix that has landed.

* fix(cli): keep shell output within ink's history retention

ink compacts a shell card's display through the shared history retention
policy while handing the model the verbatim output; the OpenTUI shell path
skipped the compaction on both the streamed snapshots and the final result,
so a long-running command could pin its whole output in the transcript.

* fix(cli): keep OpenTUI's conversation inside the terminal

The app column was laid out at the height of its content, so a conversation
longer than the terminal pushed the composer, the waiting row and the footer
past the last row, where nothing repaints them — a second turn left the
reconstructed frame byte-identical until the checkpoint timed out, and a dialog
taller than what was left lost its closing border the same way. ink never sees
this because its permanent output goes into the terminal's own scrollback; the
library's split-footer equivalent bounds the render tree to the footer band and
feeds the region above from captured stdout, so it cannot carry transcript items
authored in the tree.

Everything that flows — banner, transcript, the two notice rows — now lives in a
scroll region bounded by the terminal, pinned to its last row until the user
scrolls away from the bottom, while the rows below it are locked against
shrinking. Without that lock the layout spreads the region's content height over
the column and squeezes the composer's three border rows into one row painted
three times. The overflow arm then matches ink row for row, differing only by
the scrollbar indicator, and the wheel was measured on that arm: twenty notches
move the region back twenty rows with the chrome unmoved, sixty the other way let
the anchor take the tail again. Two divergences this document had recorded as
deliberate — a persistent banner, and a dialog that did not reflow the
conversation — close as a side effect.

* docs: record the shell-output compaction this branch already ships

The follow-up list still described the shell card as keeping its whole
output on screen, although the compaction landed on this branch, and it
claimed ink discards shell progress — ink renders the streamed output as
a pending item, which is what the compacted snapshot beside it is for.
Both languages now carry the decision instead, with the two unit arms
that pin it and the mutation showing they fail without the compaction.

* docs: attribute the startup update-check notice to the runtime

A control arm — ink under the runtime the OpenTUI leg uses — reproduces the
notice on ink, so its absence from ink's frames is not a renderer choice. The
explanation this document recorded instead (a subscription race, with ink
showing a later hard failure) is retracted: no arm showed a hard failure.

* fix(cli): freeze the waiting row's spinner while a confirmation is open

ink's responding spinner animates only while a turn is responding and draws one
static frame while a call is parked on a confirmation, so its waiting row emits
no bytes at all until the user answers. This renderer's spinner owned an
unconditional interval, so the row kept repainting: measured on a real machine at
about twelve bytes a second against ink's zero, holding a different frame at
every one-second sample. The acceptance harness decides a leg has settled by
watching its output go quiet, so every parked leg of a confirmation scenario also
burned the full sixty-second idle timeout.

Port the gate, and move the frame it freezes on out of ink's literal into the
spinner constants both renderers already read from one place.

* fix(cli): cancel a pending tab swap when a second answer lands inside the pause

Answering a question schedules the swap after ink's 150ms pause. A second
answer inside that window overwrote the timer handle without clearing it, so
both timers fired and the question between them was skipped without ever being
drawn — the same failure the ported dialog exists to remove, reached by another
route. The pause was carried over from ink, which keeps no handle to its timer
and so cannot cancel it either.

Also restates the sweep's remaining residuals against the matrix re-run on the
frozen waiting row: the parked spinner frame and the startup update notice are
no longer open items, leaving the long-path wrap break and vertical anchoring.

* fix(cli): drop the footer while a gated server holds the dialog slot

ink's layout swaps the whole Composer — footer included — for the dialog mount
whenever it counts any dialog visible, and the startup server approval is one of
them. This renderer gave the approval the Composer's slot while leaving the
footer on a gate of its own that knew about the shell confirmation, the action
modal, the tool confirmation and the completion list, but not about the new
occupant — so two rows ink does not draw stayed painted under the box. The line
by line read of that frame is what turned it up; the frame now differs only by
the two residuals already recorded for it.

* fix(cli): hold ink's pending glyph on a call queued behind another approval

A turn that returns two calls needing approval parks the first in the
scheduler's 'scheduled' status until the second's answer arrives. ink reads
that status and keeps its pending glyph on the card; this renderer went
straight to the executing one, drawing work in progress on a call waiting for
a person. The status was already on the batch update this renderer consumes, so
this reads it there rather than touching the scheduler, and emits it only when
it changes.

Found by a new acceptance scenario that captures the screen between the two
approvals, and confirmed on the machine with a single-variable comparison: the
row pairs off and the checkpoint drops from four divergent rows to three.

* docs: record the inline-confirmation row reserves as named debt

A review pass noted that the two reserves sizing the conversation around an
expanded confirmation would rot silently if that geometry ever changed: they
are hand-counted from a row inventory, the live scenario separates the shipped
value from zero but not from four rows smaller, and the unit tests assert the
arithmetic the constants feed rather than the screen. Both languages of the
design doc now say so where the follow-ups are kept.

* fix(cli): list mid-turn queued prompts above the composer

A prompt submitted while a turn was running was counted here but never shown:
its length reached the footer badge and the composer could pop it back for
editing, but the texts themselves were readable only by draining the queue,
which empties it. ink lists them above the composer instead — three at a time,
each collapsed to one line, an overflow row and a fading hint — so this ports
that component row for row.

The queue stays in a ref, because a turn drains it and puts it back within one
tick and a ref is what that needs; the five sites that move it now copy it into
state besides, which is also why a cleared screen sheds the rows with it. No
frame can show this one: both renderers steer a plain-Enter submission at the
next sampling boundary, so a queue that outlives the turn needs the deferral ink
puts behind Ctrl+Q, a key this renderer does not bind. Coverage is therefore
unit-level, and the design doc records that the ported hint now prints that
unbound key on screen.

* docs: correct the mutation counts recorded for the queued prompt rows

The recorded coverage claimed nine mutations, one per component behaviour and
per mirror site, each failing its own test alone. Re-running the set says
otherwise: eleven mutations, and the mirror sites do not map one to one because
a list never given its first entry, or never emptied, is asserted turns later.

* docs: state the queued row hint's cut-off and its assertion layers exactly

The coverage sentence read as if the hint survived its fourth fill, and listed
the transcript reset twice: once among the five sites that mirror the queue and
again as an extra assertion layer.

* fix(cli): record the model dialog's outcome like ink does

The row a `/model` dialog leaves behind reached the transcript but not the
session log, so resuming a session dropped the model a pick settled on or the
one an escape kept, where ink replays it. The dispatcher cannot carry it: its
result phase closes while the dialog is still open, with an empty output list,
and ink writes that same empty pair. Ink then pairs each of its three outcome
sites with a second result-phase record naming the command and carrying that
one row, which is what this commit adds here.

The pairing goes over one object, so the row shown and the row recorded are the
same value built once and cannot drift apart. Two silences are deliberate: a
pick that fails to apply records nothing, because ink keeps the dialog open with
the error and a recorded row would replay a switch that never happened; and the
dialog adds no invocation-phase record, because the bare command's own
invocation is already written, hidden, by the dispatcher.

Three existing cases in the dialog suite now assert the recorder alongside the
row and a new one covers the failed pick. Five mutations — no record, wrong
phase, wrong command, empty payload, a failure that reports anyway — each fail
at least one of them. On a real machine the slash-dialog scenario was run on
both legs: the two session logs now hold nine `slash_command` records in the
same order with the same flags, the fifth being the dialog's own result carrying
`Kept model as fake-model` on both sides. What is not shown on screen is the
replay itself; no scenario resumed a session, so that step rests on source
reading, and the design doc records the boundary.

The sweep also surfaced two parity classes left open rather than widened into
here: most dialogs answer through a notice slot that the next submit clears, so
their outcomes never reach history or the log at all, and an auxiliary model
pick is a success row in ink where every model outcome here is an info row. Both
are documented as follow-ups.

* fix(cli): let OpenTUI dialog fields move the caret like ink's

Every text field in these dialogs kept its value in the dialog's own state and
only ever appended to it, so the keyboard could add a character and drop the
last one: the arrows, Home and End did nothing, and a mistyped character in the
middle of a value could only be repaired by erasing everything after it. ink
hands the same fields to one text input component whose editing lives in a
buffer that measures the caret in code points, so those keys move it there.

The buffer is not a rewrite of what ink's ought to do. It is a one-line slice of
ink's own operations, and its differential suite holds no hand-written
expectations: each fixture drives the same keystrokes through ink's reducer and
this model and compares the text and the caret offset after every step — across
a paste that leaves a line break behind, across code points ink weighs as one
cell or two, and across both walls when the caret is already at one. Word
segmentation is reused as ink's, with two helpers that were module-private now
exported so what ctrl+W erases is what ink erases; the key order is ink's too,
taking only the keys a one-line field can receive and handing every other key
back to the dialog, which is what ink needs because its dialog stops handling the
arrows once a field owns the row. Word jumps, delete-word-right, kill-line and
undo/redo are recorded as not ported, and ink binds all four. Two absences are
forced: ctrl+D belongs to the app's global exit binding, and a field whose value
is replaced wholesale pulls its caret back inside the string rather than leaving
it past the end.

Three rendering differences are kept and documented: ink windows the field at a
fixed column count and shows only the caret's line, so a multi-line paste hides
what it cannot see, while these rows render the whole value; ink blinks the
cursor cell every 530 ms, where a steady cell spares the dialog a repaint timer —
measurable, not just source, since in a scenario parked on one of these rows
ink's leg wrote 270 KB per minute sitting there and burned four of seven idle
waits in full, taking 251 s against this renderer's 14; and the cell carries the
theme accent, as the composer's cursor does, where ink paints gray read from the
terminal background.

Mount semantics differ per field because ink's do. The question dialog mounts the
row's input only while its option is selected, at the end of the stored value, so
this port treats the row becoming selected again as a fresh mount and drops a
caret left mid-value; the wizard's custom-model field is not unmounted when the
list below takes focus, so Tab returns it to where it was left.

One field's text is not the model's either. The context-window setter keeps
digits only and this port renders what it is handed, so a character that setter
rejects — a letter, or the Space that step binds — moves the caret and never
appears in the row, where ink's buffer is the source of truth and shows what its
own flow discarded. Both land the caret in the same column, and that display gap
predates this change, so it is recorded as follow-up work rather than copied.

Coverage is twenty-two new cases across three suites — thirteen the model itself,
against ink, the rest using its fields — plus seven mutations, each failing the
tests that own the behaviour: the left arrow stops moving, the context-row input
branch, both end-of-line bindings, forward delete, a modified Delete that erases
a character instead of passing through, ctrl+W, and a row that stops
re-mounting. On a real machine a new scenario answers one question from the free
text row, and only one, so the tab clamping and the double trigger recorded
earlier cannot be the source of the frame: type six characters, step the caret
back two, insert one. Both legs show `> abcdXef` at the same column of the same
row and both record `abcdXef` as the answer. The styled capture pins that down —
in the frame both legs caught together, ink's cursor cell and this renderer's sit
on the same character in the same column, differing in two colours only; in the
two frames before it ink's cell is absent at all, which is its blink interval and
not a missing cursor. No scenario reaches the auth wizard, as before: its four
fields' editing rests on the unit suites alone. One test-runtime note is recorded
with it, because it cost a debugging round: the DOM runtime used by the field
tests keeps stale renders of a re-rendered row mounted beside the live one, so the
probe reads the last cursor cell — the same JSX shape driven through five state
changes in the real renderer holds one row, so this is scaffolding, not the dialog.

The full ink/OpenTUI matrix was rerun on this tree: twenty-six scenarios, no
scenario errors, twenty-two of seventy-seven checkpoints byte-identical, and the
frames of the three earlier dialog scenarios unchanged against the previous run.

* docs: correct the cursor-cell frames recorded for the dialog caret

The coverage paragraph claimed ink's cursor cell was absent in the two frames
before the one where both legs show it, and attributed that to the blink
interval. Reading the captures says otherwise. The cell is on screen at two
checkpoints, not one — over the same character in the same column on both legs
each time — and in the frame between them the caret rests past the last
character, so the cell each leg would paint is a trailing blank the capture
drops and neither side shows one. The frame where the two legs really do differ
is the first, where the field is empty and this renderer paints the cursor over
the placeholder's first character while ink shows the placeholder whole; that one
is the blink phase, and it is the only absence the paragraph should lean on.

The same re-read puts this renderer's leg of that scenario at 13 s rather than
the 14 recorded, against ink's 251.

The commit message of the change these notes were written for carries the earlier
wording; it is left as pushed, and this is the correction to the record.

* fix(cli): point the awaiting arrow at the call the queue can answer

The marker that says "this card is the one on screen asking for you" was
derived from the transcript: the first row that is a pending, unfinished tool
card. That is not the same call as the one whose confirmation is showing. A
PreToolUse ask hook re-arms a call that was already approved by putting it at
the back of the waiting queue, while its own card goes back to pending where it
already sits in the transcript — so the two orders disagree, and the arrow
landed on a card with nothing behind it to answer while the card that was
actually waiting showed none.

The queue knows which call is on screen, so the view is told its identifier and
draws the marker only on a row that both carries it and is still an unanswered
pending card. Keeping the second condition is what preserves the one-arrow rule:
a card the queue names but the transcript has already settled shows nothing,
exactly as before. Where there is no queue at all, no row is marked.

* fix(cli): keep a keystroke burst intact across the dialog fields and rows

The first review round found, for the shared line-input helper, the same defect
that had already been closed for the composer: a dialog read the value out of
its own state, so a keystroke handled after another one in the same batch
appended to what the last render had drawn and the earlier character was lost.
That class was not confined to one helper. Every dialog row that answers from
the position it last drew has it, and a terminal delivers three ordinary things
as one burst: a held arrow repeats about every thirty milliseconds, a bracketed
paste arrives with the Enter its terminal appends, and a fast typist's keys land
together.

The question dialog now keeps its tab, its cursor row, its checked options and
each free-text entry in a mirror written as the key is handled, and derives what
a key acts on from the burst's live position rather than from the render that
installed the handler. So a held arrow walks every row it repeats through and a
Space ticks the one it reached, and an Enter that follows a typed answer within
one read commits what was typed instead of refusing the empty value it was
showing when the read began. The same fix in the authentication wizard carries
the endpoint typed in one batch with its Enter all the way to the saved plan,
where it used to be discarded and replaced by the protocol default.

Two consequences of making a mid-read submit work are closed with it. A field
now stops taking keys once the Enter it handled has moved the step on, so the
rest of that read cannot write into the step the burst already left behind; a
refused Enter, which leaves the step mounted, keeps taking them. And a manual
tab move calls off the swap the answer scheduled a beat earlier, which otherwise
fired one tab further on and skipped a question nobody had answered.

A caret the value's owner refused now falls back to the last offset the accepted
text agrees with, so a character a field strips leaves no trace at all:
previously it stepped the caret right, and the next erasure deleted a character
the user never typed. Along with it, a field that has lost the focus prints the
character the caret is on rather than dropping it — the highlight is what says
which row owns the cursor, and omitting the character printed a value one
shorter than the one held.

None of this reaches a machine: the acceptance harness writes one character per
write and never pastes into a dialog, so every case here is a unit test, and
each of the behaviours above is pinned by a mutation that fails exactly the test
owning it.

* docs: record the keystroke-burst decisions and correct what the sweep claimed

Five entries are added for this round: why a burst of keystrokes is handled by
the render it started in and what the dialogs now mirror against that, why a
manual tab move calls off a scheduled swap where the reference implementation
lets both fire, why a caret the owner refused falls back and a field out of
focus still prints its character, why the marker for an awaiting call is named
by the queue rather than by transcript order, and what makes a field stop taking
keys after its own read has submitted it.

The coverage notes are corrected where they overclaimed. The reference
implementation's own wizard suite was recorded as failing a fixed set of tests
here; six runs at this head say the count moves with the machine, and a
one-variable control says the shared provider-setup hook edited this round is
not what fails them. Two gaps that this round leaves open are written down as
well: the stop that settles a field does not settle a step, and it stops keys
but not a paste that shares its read.

* fix(cli): read the loading-phrase setting for the waiting row

ink's Composer drops the phrase, not the row, when
ui.accessibility.enableLoadingPhrases is off; this renderer ignored the
setting. The parked-row tests now read the mechanism too — that no timer is
left running, and that no cancel affordance is offered for a request that is
not in flight — rather than only the glyph the cell happens to show.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): sanitize a queued prompt before the waiting row shows it

A row listed the prompt raw while the same string goes through
sanitizeTerminalText once the queue is consumed, so an escape sequence typed
into the composer reached the terminal from a row that only displays text.

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(cli): drop the @-mention card's unreachable arguments row

handleAtCommand builds every display it returns as a closed literal with no
arguments, so formatToolArgs never produced a string here and the row was
never drawn. The test that covered it passed on a fake that supplied an
argument object production cannot give, which is why the suite stayed green
over dead code.

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): stop handing React a null key in the dialog harnesses

The stub renderer forwarded whatever key the element carried, and an absent
one arrived as null, which React reports as a reserved prop on every element
the two dialog suites build.

Co-authored-by: Qwen-Coder <[email protected]>

* docs(cli): correct what two source comments claim

The line editor said the global exit binding takes ctrl+D before a field sees
it; the binding acts on the key without swallowing it, and the reducer hands
it back unhandled. The expanded-window comment had the two failure directions
of its reserve swapped, and cited a line number in another file.

Co-authored-by: Qwen-Coder <[email protected]>

* docs: record the pointer divergence and the pins review measured as loose

Decision 21 now names the two deliberate diverances separately — a defect ink
has that this port does not, and the pointer affordance this port lost when the
option rows were drawn by hand. Decision 21's residual list drops vertical
anchoring, which the retraction at the top of that section already disowns, and
Decision 22 stops treating a reconstruction artefact as a frame finding.
Decision 28 says which half of its mutation pair pinned the timer, Decision 30
scopes its queue rows to the steering lane and marks the mount pin as
containment rather than order, and Decision 32 corrects the ctrl+D claim. Four
follow-ups record the unported key families, the refused-keystroke divergence,
the exit handler's missing claim, and seven pins that survive a mutation.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): keep a click on the transcript out of the composer's focus

The transcript scroll region was focusable by class default, and the
renderer focuses the first focusable ancestor under a left mouse-down, so
one click anywhere in the conversation blurred the composer's editor for
the rest of the session. Printable keys kept arriving through the
composer's own global handler, which hid it: caret keys and pastes went
where nothing was listening, because a renderable attaches those handlers
only while focused.

* fix(cli): stop the bare End key at its own line in dialog fields

ink has two distinct "go right" keys and this renderer collapsed them into
one. Ctrl+E moves through the reducer and lands past the last code point of
the whole value, so a multi-line paste ends after its final line. A bare End
falls through to ink's text buffer instead, whose `end` move stops at the end
of the visual line the caret sits on — so typing after End on the first row of
a two-row value inserts there rather than appending at the bottom. Dispatch
the two keys to their own moves and differential-test the pair against ink's
reducer, including a bare End then a keystroke on a line break.

Separately, the queued-prompt rows and their hint counter mount inside the
composer's branch, which this renderer hands to a parked tool confirmation,
so those rows leave the screen for a state where ink keeps them. That is
recorded as a divergence rather than fixed: hoisting them above the branch
would place them over the waiting row where ink draws them under the spinner.

* fix(cli): fit the question dialog's chip row to the room it has

The header cap copied ink's overhead terms along with the width they are
subtracted from, but this row is not drawn in ink's box. It spends two columns
of margin here, and the padding it spends is already one of those overheads, so
the row was billed for eight columns it does not spend and a header was
ellipsized where the row still had room. Re-deriving the base from this row's
box was then checked against a laid-out frame rather than the props it came
from: headers print whole here where ink reaches for the ellipsis at the same
width, and that window is recorded in the design doc.

* fix(cli): erase a word for the byte a legacy terminal sends for it

ink binds ctrl+W, and this port erases a word for that and for a backspace
carrying a modifier, which is what a terminal that reports modifiers sends. A
terminal that reports none sends the bare 0x1f byte instead, and that byte
reached a dialog field and did nothing: not inserted, because a control
character is not printable, and not handled, because only the composer had been
taught it. Both field paths now ask the composer's own predicate, so the one
spelling they had not shared is shared.

* docs: correct what a resumed session brings back for a slash command

The record a model dialog leaves behind was claimed to be missing where ink
keeps it on screen. ink keeps a record of each of its outcome sites too, and
what this renderer loses on a resume is wider than that one dialog: its
transcript adapter replays a slash command's invocation phase only, so no slash
result comes back at all. That gap now reads as the app-wide one it is, and is
carried into the follow-ups list alongside the chip row's clipping window, which
was recorded the same way.

* fix(cli): keep a pasted newline inside the row the caret sits on

A bracketed paste is the only way a newline reaches a dialog field, and
the rows that draw these fields rendered the whole value. This port's
cursor is a cell set beside the text it marks, so one break grew the row
past the dialog's height budget and left the highlighted cell on another
line than the caret. The rows now draw the caret's line only, as ink's
single-line field does, while the value keeps the break so the answer
that reaches the model is the same text.

* fix(cli): hold a settled field closed and follow the tail its owner accepted

The latch a submit armed was a per-render binding, so the re-render the submit
itself scheduled cleared it and the next key of the same stdin read kept editing
a row the wizard had already left. It is a ref now, re-armed only where the
field's mirror is re-seeded, and it is checked inside the insert path so a
bracketed paste trailing the Enter is stopped the same way a key is.

The caret rule beside it capped the caret at the shared prefix of the proposed
and accepted values, which strands it on the first code point an owner dropped
out of the middle of a paste. Walking the two texts and charging the caret only
for the drops ahead of it keeps the caret following the accepted tail, and the
old cap remains the fallback for an owner that inserts or substitutes.

* fix(cli): mirror the question dialog's answers and lock a settled row

Two reads of an answer still came from rendered state, so a single stdin read
that typed into a multi-select's free-text row and then submitted left that entry
out, and one that answered a question and then submitted from the review tab left
that answer out. Both now read a mirror written synchronously, with state kept as
the fallback for the reads that run during render.

The row a pending tab swap settled is still drawn and still owns the keys, so the
rest of one read answered the same question again and quietly replaced the answer
already recorded — a digit on the option branch, or a Space that a multi-select
rebuilds its answer from at submit. Answers are locked while that swap is armed.
Navigation is not: a manual arrow still calls the swap off and takes the cursor
with it.

* fix(cli): pause the loading row's timer while a call is parked

The row already freezes its spinner and drops its suffix while a tool call waits
on a confirmation, but its elapsed counter kept running on the active flag alone.
Wiring the parked state out of that flag is the obvious fix and the wrong one:
the shared timer zeroes what it has accumulated on a false-to-true edge, so a
turn that had been running for half a minute reported itself as four seconds old
once the user answered. It is handed the timer's separate paused flag instead,
which holds the accumulated time and carries on from it.

* docs(design): record the answer lock and the parked timer, correct the counts

Adds the two behaviours this round landed to the parity sweep design in both
languages, drops a follow-up entry the mirrors closed, and replaces coverage
totals that had gone stale with counts a reader can re-measure against the
suites.

* docs(design): correct the ctrl+C actor set and the unported key families

The follow-up bullet claimed three handlers act on ctrl+C at once. The shell
mounts five exclusive branches, so only the on-screen one joins the app-wide
exit handler; the rewind overlay that binds the key has no importer. The
key-family bullet also said every operation the four families need already
exists, which is not true of undo/redo — it needs a history the line model
does not keep.

* test(cli): pin the wiring and placement the review measured as free

Every one of the six pins the last review round measured as not holding was
an assertion that read what was rendered, so a change that rendered the same
thing survived it. Each now asserts the placement, the wiring or the state
instead, and each was re-measured by removing the behaviour it names.

The round's remaining review suggestions are closed the same way: a digit on
a multi-select tab moves the cursor rather than ticking the option it names,
the burst that ends on another tab is checked for what it stored as well as
what it drew, the loading-phrase setting is pinned at both of its mounts, the
queued-call id and the arguments setting are read off the tree the entry point
renders, ctrl+E is shown crossing a line break, and the finished-call term of
the arrow gate has a witness of its own.

The design document's coverage counts and its pin register follow, in both
language versions.

* test(cli): witness the ctrl+E routing the review measured as free

Every fixture that pressed ctrl+E held a value with no line break, where the
buffer-end jump and the line-end jump land in the same column, so routing the
key to the line-scoped move survived. Press both keys through the routing on a
value that holds a break with the caret off its last line.

* fix(cli): bound the ask-user answer row and re-arm a wizard field after a failed submit

The question dialog's free-text answer is the one row a paste can make
arbitrarily long, and the confirmation it lives in has no body window of its
own, so a single long paste grew the dialog past the terminal height and pushed
the options still to be picked off screen. The row now draws inside the same
fixed window ink's TextInput gives that field, caret cell included, while the
submitted answer keeps every character. The collapsed echo of the same value is
bounded the same way.

A bracketed paste is one event with no keypress per character, so it was read
against the render that drew the row rather than the cursor the keys of the same
stdin read had already moved, and it did not ask whether the answer it would
widen had been recorded. Both are now asked of the live position.

Every text field of the provider wizard latches itself shut on the Enter that
submits it, and only a mount-key change clears that latch. On the last step the
submit is async, so a rejected install — or one that saved service models
without a conversation model — left the very same step on screen with every key
dead, and backing out of the wizard was the only way left. A retry counter now
hands the field back on both paths.

* fix(cli): settle the answer row on the verdict that records it

A submit the answer lock rejects used to settle the field before the call
that could refuse it, against the contract the shared line model states for
its own latch. The letters trailing that Enter in the same read then reached
a row that had closed, so the answer the user came back to give was the
shorter one. The call now reports whether the answer was recorded and the
field settles on that verdict.

The tab swap cancelled on the answer path goes with it. A lock added in an
earlier round drops a second answer inside the pause before it reaches that
cancel, and a live timer always belongs to the tab the cursor is still on, so
putting the call back fails no test — which is the measurement that showed it
unreachable.

The design doc is corrected in both languages for this round's findings, with
the mutation counts measured rather than restated.

* refactor(cli): collapse the answer lock to the armed timer alone

The lock that drops a second answer inside the dialog's 150 ms pause also
asked which tab the pending swap belonged to, and that clause could never be
false while a timer was armed: the only moves of the cursor's tab are the
swap's own callback, which nulls the timer first, and the two arrows, which
cancel the swap first. The ref modelling it goes with the dead cancel it
accompanied, and the whole suite is green with the clause removed — which is
the measurement that showed it unobservable.

The design doc records the collapse in both languages, as this round's tenth
mutation.

* docs: record the machine leg of the round-three fixes

The matrix was re-run at this head, so the placeholder that stood in for
its result is replaced with the measurement: fifty-four runs over
twenty-seven scenarios with no error, the checkpoint split and the
divergent-row counts, and how many captures are unchanged against the
previous head. The same pass states that none of the round's five items
moves a frame and why, so the unit suites are named as what carries them.

Two counts in the coverage boundary had gone stale against the matrix as
it runs — one scenario and one checkpoint were missing from both — and
the scenario enumeration in the method section was missing the chip-row
arm. Both languages are corrected together.

* docs: record the matrix re-run after merging main

The merge moved one thing on a frame — the version the banner prints — so
the counts Decision 39 quotes are re-measured rather than carried over, and
the byte-identity comparison states the normalisation it needed.

* fix(cli): scope the answer row's re-arm, lock, width and caret cell

A retry counter bumped by an asynchronous verdict re-seeded whichever
field happened to be mounted when the verdict landed, so a rejection
arriving after the user had backed out discarded a caret parked in a
field that never submitted. Only the step that fired the install may
bump it now.

The free-text row's editing branch was the one mutation of a
multi-select answer that never asked whether a pause was still holding
it, so letters typed inside that pause widened an answer Enter had
already recorded. The branch asks now, and only on a multi-select, so a
single-select field whose submit the pause rejected stays editable.

The caret cell was the one span of either dialog's field drawn without
sanitising, while both spans beside it went through it, so a bidi
override a paste parked in a value was emitted raw from the cell the
user is looking at and rewrote the direction of the row. Both fields
sanitise all three spans; the submitted value keeps every code point.

The row's width budget charged the confirmation box's margin and not
its padding, so on a terminal of sixty columns or narrower the last two
cells it drew — the caret cell among them — fell outside the box that
contains them.

The design document scopes Decision 39's premise to the payload the
bound actually covers and records the four fixes, their mutations and
the re-run machine leg as Decision 40.

* docs: attribute the answer lock to the branches Decision 37 put it on

Decision 40 said Decision 37 had stopped the free-text row's insert
branch on a locked answer. It had not: the lock went on the digit branch
and a multi-select's Space, Decision 39 extended it to the paste path,
and the editing branch was left to the field's own latch, which arms only
on an Enter that moved the dialog. An Enter on the option row arms the
pause without arming that latch, which is the gap this round closed.

Also re-wrap the answer-row paragraph Decision 39's scoped premise left
one line past the document's width.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-17 04:06:07 +00:00
2025-07-22 23:26:01 +08:00
2025-08-08 20:45:54 +08:00
2025-07-22 23:26:01 +08:00
2026-09-16 13:31:06 +00:00
2025-10-23 09:27:04 +08:00
2025-12-22 21:11:33 +08:00
2026-09-16 13:31:06 +00:00

npm version License Node.js Version Downloads

QwenLM%2Fqwen-code | Trendshift

The open-source AI coding agent for your terminal, editor, desktop, browser, and chat.

中文 | Deutsch | français | 日本語 | Русский | Português (Brasil) | 한국어

Qwen Code in the terminal, desktop app, browser, editor, and chat, with VS Code, Zed, JetBrains, Telegram, DingTalk, WeChat, and Feishu integrations

Why Qwen Code?

  • Agentic out of the box — Auto-Memory, Auto-Skills, SubAgents, Agent Teams, and MCP. Dynamic workflows, zero setup.
  • Open-source, inside and out — The framework and the Qwen models are open-source. They evolve together. No vendor lock-in.
  • Multi-protocol — Supports OpenAI, Anthropic, Gemini, and Qwen APIs. Any third-party provider or local model (Ollama / vLLM). Switch at runtime.
  • Beyond the terminal — IDE plugins, Desktop app, Web UI, SDKs, and chat integrations (Telegram / DingTalk / WeChat / Feishu).

Tip

Qwen Code is actively iterating on itself — using its own agent and models to file issues, submit PRs, review code, and run tests. Powered by the community, driven by AI.

Installation

Linux / macOS:

curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash

Windows:

irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.ps1 | iex

Restart your terminal after installation to ensure environment variables take effect.

NPM / Homebrew

NPM (requires Node.js 22+):

npm install -g @qwen-code/qwen-code@latest

Homebrew (macOS / Linux):

brew install qwen-code

Quick Start

Open a terminal in your project and start Qwen Code:

cd /path/to/your-project
qwen

Inside the session, run /auth to configure your provider and API key. Then try:

Explain this repository and show me where to start.

See the Authentication Guide and Settings Reference for detailed setup.

See the terminal interface

Qwen Code terminal interface

How to Use Qwen Code

Choose the interface that fits your workflow:

Interface Get started
Terminal Run qwen in your project — see Quick Start.
Desktop Download Qwen Code Desktop for macOS, Windows, and Linux.
Web Run qwen serve --open to open the Web UI in your browser. (experimental)
Editor Set up VS Code, Zed, or JetBrains.
Chat Connect Telegram, DingTalk, WeChat, or Feishu, then run qwen channel start.

For automation and custom integrations:

  • Headless — Run qwen -p "..." in scripts, CI/CD, or batch jobs.
  • SDKs — Build with TypeScript, Python, or Java.
  • Daemon — Run qwen serve to connect clients over HTTP + SSE (ACP). (experimental) Daemon guide.
SDK example (Python)
import asyncio

from qwen_code_sdk import is_sdk_result_message, query


async def main() -> None:
    result = query(
        "Summarize the repository layout.",
        {
            "cwd": "/path/to/project",
            "path_to_qwen_executable": "qwen",
        },
    )

    async for message in result:
        if is_sdk_result_message(message):
            print(message["result"])


asyncio.run(main())

Capabilities

If you know Claude Code, you already know Qwen Code — and then some. We've put significant effort into bringing Qwen Code to feature parity with Claude Code, improving both breadth and reliability across the board.

Feature Qwen Code Claude Code
SubAgents, Agent Teams, Dynamic Workflows ✓ ✓
Auto-Memory, Auto-Skills, Hooks ✓ ✓
Built-in Skills (/review, /batch, /loop, /bugfix…) ✓ ✓
MCP, Plan Mode, LSP Integration ✓ ✓
Auto Mode, Sandbox, Git Worktrees ✓ ✓
Computer Use (desktop automation) ✓ ✓
IDE Plugins (VS Code / JetBrains / Zed) ✓ ✓
SDK ✓ ✓
Headless Mode, Session Management ✓ ✓
Open-source — model and framework ✓ —
Multi-protocol (OpenAI / Anthropic / Gemini / Qwen + any provider) ✓ —
Agent Arena (multi-model head-to-head on same task) ✓ —
Daemon Mode — qwen serve (multi-client shared agent) ✓ —
IM Channels (Telegram / DingTalk / WeChat / Feishu) ✓ —

Qwen Code Evaluation

Evaluation Configuration

Configuration Value
Dataset princeton-nlp/SWE-bench_Verified, 500 cases
Runs 3 trials per version, 1,500 jobs per version; 7 Qwen Code versions
Model Qwen 3.7 Max
Sampling temperature=1, top_p=0.95
Thinking / Reasoning interleaved_thinking=true, thinking_type=adaptive, reasoning_effort=high
Context / Output Limits context_1m=true, model_context_length=1,000,000, max_tokens=64,000, model_max_tokens=65,535
Harness Limits max_iterations=500, runtime_timeout_sec=7,200, agent_timeout_multiplier=3.0

Scores by Qwen Code Version

Version Avg Score Pass Rate pass@3 pass^3
0.0.14 77.80% 84.6% 84.6% 69.2%
0.1.0 76.73% 84.6% 84.6% 68.0%
0.5.0 76.60% 83.0% 83.0% 68.4%
0.10.0 76.47% 84.4% 84.4% 67.4%
0.15.0 77.67% 84.6% 84.6% 68.6%
0.19.0 76.40% 85.0% 85.0% 65.8%
0.22.0 77.33% 84.4% 84.4% 68.0%

Avg Score Trend

Qwen Code version Avg Score trend

Ecosystem

  • Qwen Code Desktop — Official desktop app for macOS, Windows, and Linux

  • AionUi — A modern GUI for command-line AI tools including Qwen Code

  • Gemini CLI Desktop — A cross-platform desktop/web/mobile UI for Qwen Code

  • 🦞 Qwen Code Claw — Let other agents (Claude, Codex, etc.) delegate coding tasks to Qwen Code via ACP. Paste this prompt into your agent:

Use Qwen Code as a coding agent. First, fetch and read this skill reference:
https://raw.githubusercontent.com/QwenLM/qwen-code/refs/heads/main/.qwen/skills/qwen-code-claw/SKILL.md
Then follow the instructions inside to install, authenticate, and use Qwen Code via acpx for all coding tasks.
  • Aliyun Model Studio CLI — Official CLI for Aliyun's AI platform (bailian-cli). Extends Qwen Code with image/video generation, knowledge retrieval, app orchestration, and model deployment

Contributing

Contributions are welcome! See CONTRIBUTING.md for guidelines.

Acknowledgments

This project was originally based on Google Gemini CLI v0.8.2. We gratefully acknowledge the Gemini CLI team's excellent work. Starting from Qwen Code v0.1, we stopped syncing with upstream and began independent development as a multi-protocol, multi-platform agent framework with deep integrations for Qwen models and beyond.

Languages
TypeScript 76.5%
Java 11.5%
JavaScript 5.4%
Rust 5%
Python 0.5%
Other 0.9%