Repository navigation
io-cli 0.32.0 - #82
Merged
Merged
Conversation
`Screen::replace` and `Screen::rewind` shared an identical tail — erase from a row down, give the terminal back, take a fresh viewport, and fall back to the session's own height if the new one cannot be placed. Both lived on the stdout-backed impl, because both built their replacement with `attach_with`, which enables raw mode and asks a real tty where its cursor is. Nothing under `tests/` could reach either, so no test in the suite had ever executed a viewport re-placement — the one operation 0.32.0's growing viewport is made of, and the one N5 is about. `Screen::replace_from(row, height, attach)` takes the constructor as an argument and holds the whole sequence once, on the generic impl. `Screen::replace` and `Screen::rewind` become its two callers. `tests/support::replace` supplies a recorder-backed constructor sharing the same `Recorder`, which is what makes the property observable: N5 is about the byte stream across a growth, and a fresh recorder per screen would throw away the only evidence there is. `tests/viewport.rs` asserts it over a growth, a shrink back to the floor, a grow-to-grow that never passes through the floor, a terminal resize while grown, the erase ordering as the literal CUP+ED bytes, and the ceiling on an 80x24. Committed rows survive all six exactly once. Also adds `SCROLLBACK_RESERVE`: the rows of conversation that stay visible above a grown viewport. Four, so an 80x24 keeps a twenty-row viewport with the last exchange still on screen.
Every overlay in this product measured itself wrong, and all of them the same way. `Intent::render` and `Review::render` push logical lines, take `lines.len()` as the height they occupy, and hand them to a `Paragraph` **with wrapping switched on**. A question, a context line or a plan step longer than the terminal is wide occupies more rows than it is lines, so the paragraph consumes the whole area while the count still says there is room — which loses the choices and the footer, and then draws the composer on top of rows the paragraph has already painted. The correction is not a more careful count of lines. `rows::wrapped` asks ratatui itself, through the same `Paragraph::line_count` that `Screen::commit` has always used to size `insert_before`. That method is the wrapper rather than a model of it, so the measurement and the render cannot disagree. The feature it needs was already enabled and already used here; no dependency moves. `rows::elide` keeps what fits and ends with a count of the rows dropped — rows, not items, because a wrapped step that is not drawn is three rows missing from the screen, and "1 more step" understates a surface the operator is being asked to approve. The search starts at `room` lines rather than at the end, since every line takes at least one row, which bounds it by the height of a viewport instead of the length of the content. Deliberately not a fourth spelling of an idiom there are already three of. `approval.rs` carries two and `queue.rs` a third; all three keep their own, because their tests assert their exact shapes and none of them wraps. What is new here is the wrapped case none of them handles.
Two lines were why every surface in this release drew into eight rows. `App::viewport_wanted` returned `VIEWPORT_HEIGHT` outright whenever a turn was running or an overlay was open, and `paint_picker` re-placed the viewport only when no picker was up. A question with five choices, a twelve-step plan, four queued messages and a four-hundred-row picker all arrive during a turn, so all four were clipped by the same guard. The re-placement itself is not new: the composer has grown the viewport since 0.7.0. What is new is letting the surfaces that need rows ask for them. `App::viewport_wanted` is now the one owner of the demand and `term::viewport_for` the one owner of the limit, so the session's own sizing and the driver's picker path cannot disagree about the ceiling. That ceiling is the terminal's height less `SCROLLBACK_RESERVE`, which exists so the exchange a surface is about stays visible, not to ration rows. The ordinary session's demand is derived from the layout rather than guessed, and reproduces the old constant exactly at an idle prompt. `App::viewport_height` is deleted. It returned the constant, had no production caller, and with a viewport that moves it was misleading. `n2_a_queue_of_any_depth_leaves_the_frame_the_same_height_as_an_empty_one` asserted the opposite and is replaced. It was 0.17.0's guard against growth walking the scrollback; that reason is now discharged against the byte stream in tests/viewport.rs, so what is left is the cost it bought, which was the operator not seeing what they had queued. The first queued message is still free — the layout releases the blank above the activity line, which is where 0.17.0's one visible row came from.
The operator saw a question, some inert bullets, and nothing to type into. `Intent::render` drew the composer only `if area.height > head`, and a question with a context line and five choices filled all eight rows — so on a real session the text area was never rendered at all. The offers and the free-text answer are now rows of the same `Picker`. `Enter` on an offer sends that string verbatim, from the caller's own unfiltered rows rather than re-read off the screen. Taking the marker to the last row unfolds the composer directly beneath it, and `Enter` there sends what was typed. One marker, one list, both ways to answer. The unfold is a general `Picker` mechanic — a row may open a block beneath itself while it holds the marker, and the widget records where it put it, because only the widget knows where the row landed once the list has scrolled. It ships with its one caller rather than ahead of one. Typing is always the answer and the marker follows it. That is the one place this surface differs from every other picker, and it is deliberate: filtering is worth something over four hundred models and nothing over five offers, where the expensive act is answering. The marker opens on the free-text row, never on the agent's first offer. An overlay that opened with an offer marked would turn a reflexive Enter — the key just pressed to submit the prompt that started the turn — into silent agreement with a suggestion nobody had read. `Tab` accepts the marked row and `Shift+Tab` steps back, in every picker in the product. Both spellings of shift-tab are matched, because a picker reads raw key events and never goes through the binding table. Until now both fell into the catch-all and did nothing. A picker also says how many rows it did not draw. Thirty-six commands in eight rows drew five and said nothing, so a list filtered to nothing looked like one scrolled to its end.
`triage.rs` marked `question_asked` a committed line while marking `approval_requested` and `plan_proposed` silent, so the transcript wrote the question and the overlay redrew it through `Tone::Warning` — whose word is literally "warning". Two renderers, neither aware of the other, and the operator asked twice with the second copy mislabelled. `question_asked` becomes silent, like its two siblings. The transcript line survives where nothing else will draw it, and that condition is two facts rather than one. An `Answerer` is attached to every contract io-cli builds, so "no responder is registered" never happens in io-harness's sense; what varies is whether this process kept the receiver. The resume continuation drops it and then drives a real turn, so a resumed run that asks a new question has no overlay anywhere and the durable line is the only thing that renders it. A `--plain`-only gate would have made that question invisible, which is a worse defect than printing it twice. So `Events` gains `answering`, defaulting to false. A path that forgets to set it prints the question twice; a path that wrongly claimed an overlay would not print it at all, and the default is the direction that fails safely. Also corrects this file's own doc, which named io-harness 0.69 while the pin has been 0.71 for six releases.
`Tone::Refused`'s own doc says it means an act the permission boundary turned down, and it is the one tone whose word an operator has to be able to trust. It was being spent on a mistyped `/effort` argument, on a run that simply is not in the store, on three watch errors, and on this crate's own `/memory` bookkeeping — none of which went near a boundary. Corrected, each with the prose that argued for it: the plan proposal and question headers stop being warnings; a declined question and a plan sent back for revision stop being warnings, because declining is a real answer and a revision is the surface working; a run absent from the store becomes a fact and the watch errors beside it become errors; and the profile-switch rejection becomes a refusal like all seven sibling arms in its own match, where it was the one drawn as a program failure. `/profile`'s unknown verb keeps `Tone::Refused`, and the comment now says why the two differ rather than citing `/effort` as its precedent: a wrong `/effort` word buys the wrong amount of reasoning, a wrong `/profile` verb deletes something. `outcome_help` told an operator that a run awaiting an answer or a plan could not be answered by this release and to say it in their next prompt. `Intent::resumed`, `Review::resumed` and the `/resume` continuation have all existed since 0.23.0, so the product had spent nine releases documenting the absence of a capability it shipped, in the one place an operator reads when they are stuck. The arm is split, because `/resume` answers a parked plan and an approval belongs to a turn that is over. The two tests that pinned the old wording are re-pointed at the real bytes and one now also forbids the sentence that was false.
`io_harness::NAMESPACE` is `__`, and for a skill that name reaches the
model: `Skills::catalog` puts it in the system prompt and `read_skill`
resolves it by equality, so the model can only ask for the string it was
shown. Renaming the separator would move the system prompt, the tool
dispatch, every event target, both crates' suites and every consent surface
at once.
So nothing is renamed. `naming::display` translates at the moment a name is
drawn and `naming::wire` translates back a name an operator typed, and
between them `__` never reaches a person and `:` never reaches io-harness.
First occurrence only in both directions, because a bundle id cannot
contain the separator — the first one is always the join.
Applied to every name io-harness qualified, not to skills alone. `/plugin`
lists a bundle's agents, MCP servers and policy layers; `/status` lists its
policy layers. None of those are skills, and all of them were showing the
operator a separator that appears in no documentation they will ever read.
`invoke_skill` was the reason a skill was not a command: it wrote
`use the {name} skill: ` into the composer, and with no leading slash that
submits as an ordinary prompt — so whether the skill ran depended on how
the model read an English request. It now writes `/bundle:skill`, and
`commands::parse` resolves that back to the catalogue name.
The parse needs no skills list. No command contains a colon, so the shape
is unambiguous; the parse recognises the shape and the driver, which
re-walks the inventory every turn boundary, resolves it. A skill whose name
collides with a command resolves to the command, because the static table
is matched first.
`Chosen::Skill` keeps the wire name, so the translation lives at the two
edges and nowhere in between.
A prompt typed mid-turn was queued and then nothing happened to it until the turn had ended. A correction typed thirty seconds into a ten-minute turn waited nine and a half minutes and then ran as a turn of its own, against the work it was meant to change. Both turn arms have carried a `SteerInbox` since 0.17.0, so this needed no io-harness entry point — only a default. The queue is now handed to the running turn at each step boundary. At the step boundary rather than on the keystroke, and that is not just where the contract puts it: flushing the moment the operator pressed Enter would empty the queue into the inbox instantly, and the queue surface — their only view of what they have typed and not yet had answered — would never have anything in it. Held until a step lands, the list is visible for exactly as long as it is genuinely waiting. `App::deliver_queued` holds the loop, the ordering and the transcript records. It is in the library because nothing under tests/ links the driver, and this loop decides whether an operator's words are kept. `/steer` and the automatic drain are its two callers, so they cannot diverge; `/steer`'s own sentences are unchanged. On a refusal the message goes back to the FRONT of the queue. Putting it back with `queue_prompt` appends, which would have sent everything queued behind it first and had the agent read the operator's corrections in an order nobody wrote. A message delivered into a turn that reaches no further step boundary is still in the inbox, and the existing post-loop drain re-queues it to run as its own turn. `f5_the_queue_is_sent_on_the_operators_word_and_not_before` asserted the opposite and was rewritten, not deleted. It counted `steer.say(` sites, and the second one this adds is exactly the sabotage it existed to catch — so it worked. Its "no echo" argument is answered, because every delivered message is now recorded into the transcript; its "no undo" argument was already true of the queue, since an unsent line ran as its own turn a moment later either way. It now counts two named sites so a third is still caught.
The mid-turn loop refused every slash but `/compact` and `/steer`, so `/status` was declined for the same reason `/clear` is and the sentence told the operator to interrupt their turn in order to read a report. And `/` and `@` reached `App::key`, where they are ordinary characters, so the palette typed a literal slash and said nothing. Ten commands now run mid-turn: /status /context /cost /stats /help /theme /copy /expand /fleet /image. The decision is `commands::runs_mid_turn` in the library, with the partition written out in both directions and checked to cover COMMANDS exactly, because nothing under tests/ links the driver and this is a list about what may happen while a turn holds the session, the store and the provider. `/config` is refused in every form. Its bare picker carries a row that re-reads the provider catalogue, writes prices into a scope file and reassigns the running turn's Config; filtering that one row out is the bare-from-argued split the contract refuses to make. Recorded as an iteration with the developer's approval. Neither `opens_palette` nor `complete::opens` changes — only the call sites. The pickers reachable mid-turn are exactly those whose outcome touches nothing the turn holds: the palette and path completion write into the composer, and `/theme` resolves a theme. A turn in flight holds `&mut Session` for the whole select loop, so five of the ten could not have read it. Their facts are captured before the turn borrows the session, into `TurnFacts`. That is not an approximation: the root does not move, and the head and the last on-path turn are both the turn before this one until this one commits, so mid-turn the snapshot is the live reading. `status::committed` takes root/id/head, and expand, step_detail and to_copy take the captured turn — better signatures regardless, since a report has no business holding a session it might mutate. `commit_image` becomes one implementation behind both doors. This product has twice shipped a surface answered one way by its key and another by its name; `/fleet` was the standing instance, and this release closes it.
`EventKind::Token` carries text and no count — providers do not bill per chunk — so nothing moved the token field until a step committed. The one number telling an operator what a turn was costing them sat still for the whole of it, beside a clock that ran. It is now accumulated from the streamed deltas through io-harness's own `estimate_tokens`, the same estimator `/context` already uses, so io-cli holds no tokenizer and nothing enters the tree. `App::tick` already redraws on the hundred-millisecond tick, so the figure moves with the clock without a second timer. Drawn `~1.2k tok` while it is an estimate and `1.2k tok` once it is not. One column, in both glyph sets, legible under NO_COLOR — a number that reads as settled and is not would be worse than the frozen count it replaces. The tilde says approximately rather than still-arriving, which is the honest claim: what is uncertain is the figure, not whether more is coming. The estimate is discarded at Step and Finished, never added to a settled number, and cleared in `forget_run` so it cannot survive into a conversation it was not measured in. All three token renderers go through one helper. `Status::line` and `Status::footer` are separate and the binary draws the footer on any terminal seven rows or taller, so a field added to one is a field nobody sees — 0.8.0's spend field and 0.12.0's planning field both paid for that.
Four surfaces dropped content silently, and on three of them the silence is indistinguishable from there being nothing more. `/stats` capped its per-table breakdown at a bare literal 5 and said nothing. That is not a viewport problem at all — it is a committed page with unlimited rows, so a database with nine tables had four of them dropped on a page with room for every one. The cap is now named and the page says how many smaller tables it held back. The fleet view dropped rows with no count, and messages sort after every child so they are the first thing off the bottom — on the one surface whose entire job is saying what a fan-out is doing. It now counts them, and asks the viewport for the rows it needs so that on a real session it does not have to. `f5_the_view_takes_the_composers_rows_and_not_the_status_line` keeps its fixed four rows after an attempt to grow it: raising the viewport flips the status line onto its three-row renderer, which is a different code path, and the test would have stopped asserting the small-terminal layout it is named for. It now also asserts the count, and the demand side has a test of its own.
`Config::plugins()` looks like an accessor and is not: it is `Plugins::load(&self.plugin_decls, &self.dir)`, a fresh read of every declared bundle's manifest off disk, parsed, validated and trust-checked, on every call. io-cli made that call twice on the build path of every turn, twice more each time `/plugin` opened, and again for `/skills` — and `Skills::discover` beside it reads the whole body of every skill file to keep a name and a description, then throws the body away. io-harness says once per run and is right. Every io-cli turn is a run, so a session paid the cost on every message, scaling with the number of installed bundles that the last three releases made it easy to grow. `src/resolved.rs` holds the set. `contract::configured`, `contract::hooks`, `contract::session`, `contract::server_notices` and `pluginview::view` all take the resolved `&Plugins` instead of reaching for it. The driver resolves at startup under `block_in_place`, before the event loop exists, and again only when the declared manifests' stamps have moved. The call is confined by exact path to that module, in the shape this suite already uses for the two permitted process spawns. The property is not "there is a faster way" but "there is no other way" — a cache in front of a call anyone may still make stops being one the first time somebody reaches past it. Asserted as a call count and never as a duration: a wall-clock assertion is flaky and says nothing about why a path is fast. The gate found a real redundancy while being written — `drive` resolved for the session and then `server_notices` resolved again a few lines below — and the confinement caught a `#[cfg(test)]` module reaching around it. `Resolved::loaded()` is not named `plugins()` on purpose: the gate bans the bare method call by name, and an accessor sharing it would make the ban unenforceable without knowing every receiver in the crate. The freshness rule is stated rather than implied. Two writes inside one second that leave the length unchanged are not distinguishable by a stat, and that limit is asserted, not just documented.
This release is entirely an interface one, and nothing proves that except asserting it. O15 is a source property rather than a byte diff, and the test says why: a byte comparison of two binaries' NDJSON needs a provider and this suite has none. What makes the output identical is that none of this release is reachable from `src/exec.rs` — no display translation, no wrapped-row measurement, no token estimate, no mid-turn list, no viewport — and that is checkable exactly. The line is still io-harness's own `RunEvent` serialized whole. The end-to-end comparison belongs to the live suite. The headless diff bears it out: twenty-two lines, all of them the plugin threading, and `src/bridge.rs` untouched. N6 extends the narrow suite to the two overlays this release rebuilt, at 80x24 and at the floor, in both glyph sets — the elision is one character in Unicode and three in ASCII, so a surface reserving the wrong number of cells clips in exactly one of them. The question overlay must keep something to type into even at the floor, because an overlay that cannot be typed into is the defect this release exists to fix.
`docs/CONTRACT.md` gains four things a script may depend on: the ten commands that run mid-turn and the rule that decides the set, `/config` refused in every form and why, the provisional token figure's spelling and the fact that the estimate and the settled number are never summed, and the display-versus-wire split — including that `io exec`, `io plugin` and `io skill` report the wire name, because a script addresses the wire. Four guide pages follow the code: keys.md gains a section naming both halves of the mid-turn partition, skills.md the colon form and the round trip, the-session.md the growing viewport with its four-row reserve stated as a number, and plugins.md when io re-reads bundles and the same-second limit it cannot see. The key table is generated from `commands::KEYS` and gated in both directions, so `Tab` went into the shipped table, the guide and the coverage list together — the gate caught them disagreeing on the first attempt.
Both were written, documented and green, and neither had a test that could fail. The pass is what found them. O3 is the release's headline fix — a question drawn once rather than twice, and never as a warning. Replacing its condition with `if true`, which is exactly the defect it guards, failed nothing in a suite of 1,718. It is now asserted in both directions, and the second matters more: suppressing a question everywhere is worse than printing it twice, so a resumed run and a plain session must both still commit the line. O11 is `Tab`. It was bound in `Picker::key`, listed in the shipped key table, and written into the guide — and removing the arm entirely broke no test. Both spellings of shift-tab are asserted too, because a picker reads raw key events rather than going through the binding table, so a terminal sending `BackTab` and one sending `Tab`+SHIFT both arrive here as themselves. Eleven arms in total now, every one with a real site, every one killing the gate it was aimed at and nothing else.
`cargo fmt --all` over the release's own changes, and `Cargo.lock` picking up the 0.32.0 bump from the manifest. CI runs `cargo fmt --all -- --check`, so this is the difference between a green gate and a red one rather than a matter of taste.
Two reviewers, split by area. Both independently found the skill regression and the false freshness claim, which is the strongest signal either could have given. HIGH — mid-turn `/` and `@` ran in front of `App::key`, which is what hands a keystroke to an open overlay. The three modal overlays each own their own composer, so `app.composer` stays empty and `armed()` stays false for their whole lifetime: answering an agent's question with a path, or any text containing `@`, opened the palette instead and the question vanished. That is the release's headline feature, broken by the release's own new branch. `App::modal()` exists for exactly this and was not consulted. HIGH — choosing a non-bundle skill from the palette became unreachable. `invoke_skill` now writes `/<name>`, and `parse` recognises a skill only by its colon — which bundle skills have and io-cli's own and the operator's own do not. Those printed "there is no /x" plus the whole command listing. It worked in 0.31.0, when the sentence submitted as an ordinary prompt. The driver now resolves any word against the inventory, with `commands::names_a_command` as the precedence rule so a skill can never take a command's name. HIGH — the display translation was applied to every tool-call target on the reasoning that no path contains `__`. `__init__.py`, `__pycache__`, `__tests__`, `__mocks__`, `__snapshots__`. `read src/__init__.py` was drawn `read src/:init__.py`, a path that does not exist, in the one place an operator checks what the agent touched — on the first turn in any Python or Jest repository. Gated on `read_skill`, whose target is a name. `set_answering(true)` was never restored, so after the first turn the durable question line was suppressed for the rest of the process — including `watch_child`, which draws no overlay at all, and the post-loop drain. A question reached no surface anywhere. The flag's own doc names `false` as the safe direction. `Resolved::stale` called `config.plugins()`, which is the full read. The release halved a cost it claimed to have removed, and said so in three places. It now re-stats only the paths it recorded — each manifest, and every file `Config::sources` names, so a bundle declared or undeclared is still seen. The token estimate outlived its turn: cleared at `Step` and `Finished`, but a provider error mid-stream emits neither, so a tilde-marked guess sat on an idle prompt and bled into the next turn. The question overlay's row demand oscillated by the composer's height on every arrow key, and the driver re-places the viewport whenever the demand moves — a terminal tear-down and cursor query per keystroke, on a surface open while a turn streams. The block is reserved whenever it is configured. `rows_wanted`'s chrome disagreed with what `render` reserves whenever the fleet was open, masked by `Fleet::rows_wanted` counting its summary row twice. Two compensating errors are not a correct answer. `Intent`'s `Cancelled` arm was dead and its comment contradicted `App::key` about what `Ctrl+C` does. `rows::elide` now measures its own suffix rather than assuming one row, and says so when there is room for nothing else. `cost_field` has its own doc back.
0.13.0 removed viewport growth from the palette on purpose: re-placing an inline viewport asks the terminal where its cursor is, and that round trip lands on `/` — the fastest thing an operator does — with a two-second wait on a terminal that never answers. Its own note says the fallback is not to restore it. 0.32.0 restored it, and the live suite refused: `live_f6_the_palette_opens_ without_asking_the_terminal_anything`, 35 passed / 1 failed. That test drives the real binary through a pty and reads the byte stream. It is the only gate that could see this — `Screen::replace` re-attaches to a real terminal, so the `Fixed` backend every unit test uses observes nothing, and the decision lives in the driver, which nothing links. 1,724 offline tests were green over it. The adversarial review predicted it from the source alone, and added the part the live test does not reach: a picker's demand is `matches.len()`, which shrinks as a query is typed — so filtering four hundred models would have issued a cursor query on nearly every keystroke. The three overlays still grow; they are drawn inside the session's own frame and re-place at most once as they open and close. A picker gets the elision this release added instead, which says how many rows it did not draw and costs nothing. `Picker::rows_wanted` stays. It is what the elision measures against, and a later release that finds an affordable way to grow has a correct number waiting for it. O16 is narrowed to the three overlays by US-IO-CLI-0.32.0-I12, and the driver-text assertion in tests/viewport.rs is what keeps the round trip from coming back.
Two count assertions in `rows.rs` used `contains` on a stringified number, which "116" satisfies for "16" and "16" for "6" — a count assertion that passes on a wrong count is exactly the vacuity this product keeps finding in its own gates. They compare the whole rendered row now. The streaming estimate's doc said "tokens the step now streaming looks like it has spent". It counts the completion only, because the assistant's text is the sole per-chunk signal, while the provider's total is prompt plus completion — so on a large prompt the provisional figure is a small fraction that then jumps. The tilde already made it honest; the doc now says what is actually measured. `io exec` resolves the plugin set twice per run, not once as its comment claimed: the contract and the hooks are built at different points and threading one resolution between them would reshape the headless construction for a process that runs one goal and exits. Bounded and stated rather than claimed away.
io-cli 0.32.0
Found by sweeping the shipped prose for claims this release made false — the sweep that has caught stale text in three previous releases, and which no gate performs because a `contains` assertion cannot notice a sentence that is merely no longer true. Eight rows is the floor now, not the size. The paragraph says so, and says what the four reserved rows at the top are for.
The same sweep, carried further. `docs/guide/skills.md` said `/skills` and the palette spell a bundle's skill `<bundle>__<name>` — which this release changed, and which left that page contradicting a section added to it by this very release. `docs/guide/plugins.md` said `/plugin` draws the namespaced string "unchanged". Both now say what is true: qualified, drawn with a colon, addressed with io-harness's separator, and reported in the wire form by `io plugin list` and `io exec` because a script addresses the wire. A file that disagrees with itself is worse than one that is merely out of date, and only a by-hand reading finds either.
io-cli 0.32.0 — the README no longer says the viewport is eight rows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The release PR for io-cli 0.32.0.
The agent could ask you a question and you could not answer it. The answer composer was drawn only where there was room left over, and a question with a context line and five choices filled all eight rows of the viewport — so what you saw was a question, some inert bullets, and nothing to type into. The same question reached the screen twice, and the second copy was labelled
warning:.That fixed eight-row viewport is one cause behind four defects, so the viewport becomes the size of what it has to show. Everything else follows from it.
Verified before the seal
cargo test— 101 binaries, 1,726 passed, 0 failed, 36 ignoredcargo test --doc— 1 passedcargo fmt --all -- --check,cargo clippy --all-targets -- -D warnings— clean, CI's exact flagsEach gate layer caught something the others could not
if truein place of O3's condition failed nothing in 1,718 tests.Recorded as iterations, with the developer's approval
US-IO-CLI-0.32.0-I11—/configcannot run mid-turn in any form; O12 is ten commandsUS-IO-CLI-0.32.0-I12— O16 narrows to the three overlaysThe sealed record, its lock pin and
validateexit 0 over 96 checks rode the feature branch into develop, before this merge.