* 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]>
The open-source AI coding agent for your terminal, editor, desktop, browser, and chat.
中文 | Deutsch | français | 日本語 | Русский | Português (Brasil) | 한국어
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.
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 serveto 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
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.

