Conversation
Region safety. tuish_clear_to_eol is ESC[K — erase to the end of the PHYSICAL line, with no region clamp — and every example we newly made hostable called it, so an embedded app erased its host's chrome: the hosted editor in cooperative.sh emitted ESC[K at the region's left column and wiped through to the terminal edge, eating the editor box's own right border. Add a bounded tuish_clear_to_edge ROW [COL] and switch the examples to clear-then-print. The editor had three more of the same family — its mode toggle, resize handler and reverse-video status bar all sized themselves to TUISH_COLUMNS, so in a region the status bar painted a full-terminal-width bar across the page; they use TUISH_VIEW_COLS now. The raw ESC[2J/2K/K/1K primitives stay for root-owned apps and are marked not-region-safe. Idle-tick negotiation. A driven child's tuish_idle_interval was inert: the HOST's loop owns the idle wait, so a mounted game asking for 0.02s silently ran at its host's 0.26s. Children can want different clocks, and one loop has one poll rate, so two rules pull in opposite directions — the fast child must not be slowed, the slow one must not be sped up. tuish_ctx_sync_interval adopts the fastest tick among the host and its children; tuish_ctx_tick accumulates that tick per child and only drives it once ITS OWN interval has elapsed. Over 2.0s of virtual time a 50Hz child gets 100 idles and a 1Hz child gets 2. Replaces TUISH_MOUNT_INTERVAL, which only ever half-wired this. Dead code. The _tuish_yield/_x/_y trio was written and read by nobody: the coop router handles out-of-region mouse by not forwarding it, and the modal path's behaviour came entirely from the tuish_quit call. Drop them (and the unused tuish_ctx_deactivate) and correct the comments that described a re-dispatch that never existed. Duplication. The region-reseat both cooperative.sh and the website hand-copied — each admitting as much in a comment, while poking ten private fields — is now tuish_ctx_reseat. Tests. test_hosting's scroll assertion was vacuous: the panel shows six rows from the top, so its three j presses never scrolled and R03 was already on screen. It now scrolls past the fold. New test_context.sh (21 assertions) pins region seating, the bounded erase, and the negotiation. Docs. The whole context/hosting/cooperative surface was undocumented and six pages made claims that are now false (callbacks as override-only, TUISH_MOUSE_X as a raw terminal column, vmove as row-only, VIEW_COLS == COLUMNS, fini as "restores the terminal", a flat bind table). Add docs/hosting.md and correct them.
Groundwork for live documentation — doc pages that embed RUNNING examples, and snippets you can edit in place with editor.sh. CLIPPED REGIONS. A live widget in a scrolling document has to slide under the pane's edge. tuish_vmove already clips per cell, but _tuish_ctx_seat conflated three separate things: the child's layout size, its origin, and what it may draw through. Split them. A child can now be seated with its origin ABOVE the pane and a clip window covering only the visible slice: it keeps thinking it is full size, so it does not reflow — it is genuinely occluded. tuish_ctx_reseat takes an optional clip window to do this. tuish_canvas OVERWROTE the transform's clip bounds instead of intersecting them with the context's base. A canvas could therefore draw outside its host's region entirely — a latent escape on main, not just under the new clipping (the new test fails three ways against the old canvas.sh). It intersects now; the two clips live in different coordinate spaces, so it is a conversion, not a min/max. PASTE. tuish recognized only the ESC[200~ / ESC[201~ markers and let the body fall through to the key decoder — so a paste arrived as a burst of key events: a pasted newline fired the app's `enter` BINDING, a tab its `tab` binding (expanding to four spaces), and every character forced its own render+flush. _tuish_capture_paste consumes the body instead and delivers one atomic `paste` event with the text in TUISH_PASTE. The markers still fire, so apps that track them keep working. CLIPBOARD. New src/clip.sh: OSC 52 copy-out, with a fork-free base64 built on ord.sh's tables (verified byte-for-byte against coreutils, including UTF-8 and every padding case). Clipboard READ is deliberately absent — terminals and browsers block it, and inbound text is bracketed paste's job. editor.sh gains the selected-text extractor it never had (selection could only be rendered or deleted), an internal register, ctrl-c/ctrl-x/ctrl-v, and atomic multi-line paste insertion. Its hard-coded buffer name is now _ed_buf, and _ed_text serializes the buffer, so a host can seed the editor with a snippet and read the edit back. ord.sh built its char table with $(printf ...) on busybox/ksh93/mksh, and command substitution strips TRAILING NEWLINES — so _tuish_chr_10 was the empty string on exactly those shells. Nothing read it until paste capture needed a newline, at which point pasted line breaks vanished on busybox, the browser/wasm target. A sentinel byte fixes it. The tests assert the character is real first, because otherwise every newline assertion compares '' to '' and passes vacuously.
Follow-ups from putting live widgets in a scrolling document. tuish_ctx_mount does not merely create a context — it RUNS the child's setup and paints it. So a widget mounted while partly scrolled past its host's pane drew over the host's chrome once, before any tuish_ctx_reseat could bound it. Add TUISH_MOUNT_CLIP: a clip window applied before the child's first paint. tuish_ctx_render repaints a mounted child on demand. A host that MOVES a child (scrolling) needs it redrawn into its new rectangle now, not on its next idle tick — at a lazy interval that leaves a torn widget on screen for a whole tick. boxes.sh called tuish_vmove and then printed REGARDLESS of the result. vmove refuses a clipped cell, and printing anyway drops the text wherever the cursor last sat — standalone that almost never bites (a cell is refused only off-screen), but in a clipped region it fires constantly and smears stray labels across the host's chrome. A row-bounds test is no substitute: TUISH_VIEW_ROWS is the layout height, which stays full-size while the visible clip shrinks. Only vmove knows. All of them now go through one guarded helper; the standalone render is byte-identical to its fixture.
_ed_buf becomes a per-context field, so a host can mount editor.sh on a buffer it already populated — a documentation snippet — and read the edit back, without two mounted editors fighting over one target. This is what makes a doc snippet editable: the snippet's text already lives in a line buffer, and a line buffer is precisely what editor.sh edits. "Edit this snippet" is then just "mount the real editor on that buffer".
hosting.md: how a live child scrolls under a pane edge — a region is three independent things (layout size, origin, visible clip), and only keeping them apart lets a child be occluded rather than reflowed. Plus TUISH_MOUNT_CLIP (a mount PAINTS, so a child must be clipped before its first paint), tuish_ctx_render, and the rule a clipped app has to follow: honour tuish_vmove's return value, since it refuses a clipped cell and printing anyway smears text across the host's chrome. hid.md: the `paste` event. The body is not delivered as keystrokes — it is text, and feeding it to the key decoder fires the app's `enter` and `tab` BINDINGS and renders once per character. clip.md: OSC 52 copy, why there is deliberately no clipboard READ (terminals refuse it and browsers block it — it would let anything in your terminal exfiltrate what you last copied), and the consequence: an app that wants copy AND paste within itself must keep its own register.
A host that routes the mouse by position gets the wheel wrong: park the pointer over an embedded widget that scrolls nothing and the page stops scrolling entirely — and never recovers, because nothing then moves the widget out from under the pointer. tuish_dispatch now marks an event handled BEFORE running the action, so an action that turns out to be inert can hand it back with tuish_pass. A host reads the result as TUISH_CTX_HANDLED after tuish_ctx_dispatch and chains what the child declined, the way a nested scroller does in a browser. The editor's wheel bindings pass at the top and bottom of their buffer (and stop reading a stale TUISH_BUF_COUNT from whatever counted last), and its catch-all passes instead of silently eating every event a host offers it.
_tuish_buf is a PER-CONTEXT frame, so a host cannot buffer everything that happens: the moment it activates a child it is looking at the child's buffer, and the child's own tuish_end goes straight to the terminal. A page with three live widgets emitted four writes, and the terminal drew each one — the prose landed at its new scroll offset a frame before the widgets did, which reads on screen as a shimmer trailing the text. tuish_ctx_render now splices into the host's buffer when the host has a frame open, and tuish_ctx_mount holds the child's first paint the same way, so mounting a widget on a click does not flash it up a frame early. Both fall back to writing for themselves when no host frame is open. Every flush goes through _tuish_sink, which is the one place that can hold them. Measured on the site: 3 writes per scroll tick -> 1, and 2 -> 1 on a click that mounts an editor.
tuish_begin unconditionally reset the buffer. But the framework opens a frame BEFORE it calls your code and puts things in it — the caret hide that precedes every deferred render — so any app that buffered inside its own render handler silently threw those away. Every host does. That is why the caret stayed on, blinking wherever the last cell was drawn. _tuish_buffering is a depth now: only the outermost begin clears and only the outermost end writes. tuish_end below zero clamps rather than underflowing, and the dispatcher restores the depth it entered at, so one unbalanced app costs a frame instead of wedging the loop into never flushing again. It repairs the splice, too: a child whose own render used begin/end was flushing straight to the terminal mid-host-frame, and the host's background fill then landed on top of it. tuish_ctx_render now also HOLDS, which catches children that tuish_flush inside their render (canvas_demo does). TUISH_MOUNT_CLIP — a write-once global, consumed by the next mount — becomes tuish_ctx_clip R C W H: a host declares the pane its children are seen through once, and mount and reseat both honour it. The two spellings for one concept are gone, and so is forgetting one of them, which was a silent one-frame corruption.
The website grew a slot engine — a child table, mount/reseat/unmount on
scroll, clipping, focus, mouse routing, per-child ticks — and none of it was
in the toolkit. cooperative.sh had independently hand-rolled the same ideas
and got less of it right: its own rect test, its own per-child mouse routing
unrolled, its own tick loop, its own reseat-on-resize, and no focus model at
all (the keyboard was wired to the editor).
host.sh takes the child table and the rules. A host declares its children
(begin/slot/commit) and says where they go; host.sh reconciles. Three things
it gets right that a hand-rolled host does not:
- the id is NOT the rectangle, so scrolling reseats children rather than
remounting them — and a mount paints, so remounting would redraw the page
widget by widget before the real repaint even started;
- the FOCUSED child paints last, which is the only reason the caret ends up
where you are typing;
- an event a child declines comes back to the host (scroll chaining).
cooperative.sh's router is now three lines and it gained focus for free.
Catch-alls that bound ':' silently ATE every event a host offered — the swallow
bug that froze the site's scrolling. They pass now.
game.sh registers no render handler: it is real-time, so it paints every frame directly rather than through the coalescing rAF path (which would freeze it while a key is held). But tuish_on_redraw is also how a HOST repaints a child — when it assembles a frame, and when it moves one — and an app that registers none paints nothing where the host put it. Standalone that never showed. Hosted, the platformer scribbled its sprites over whatever the page had left on screen, and the reader saw a level with no level in it. Register a handler that forces a full repaint, and say so in hosting.md: a render handler is not optional, even if you never ask for a redraw yourself.
A widget narrower than its pane leaves columns beside it that belong to the host, and two widgets side by side leave a gap between them that does too. tuish_host_row_span answered with the outer bounds of the children, which cannot express that gap: the host never repaints it, and last frame's text just stays there. That is the bug the span was added to fix, one case over. So ask the question the caller actually has. tuish_host_row_free ROW C W hands back the runs of the row nobody is standing on, as "C W" pairs, spent straight on tuish_draw_fill. However many children share the row, whatever the gaps between them. tuish_host_focus, meanwhile, was the only function in the toolkit that returned its answer by PRINTING it — and a shell has no way to read that back but a subshell. Every host forked to ask who was focused: once per idle tick in the website, once per rendered frame in cooperative.sh, in a toolkit whose whole argument is that it does not fork. Focus is state, not a query: it is TUISH_HOST_FOCUS now, read like TUISH_EVENT or TUISH_MOUSE_X. Underneath, the same clipped-rectangle scan was written three times and the id lookup three more, so the mouse router walked the child table three times per click. One _th_rect, one _th_find, and the pane kept in fields instead of re-parsed out of a string by two functions that had drifted apart. And three things that were quietly wrong: `modal` was documented as 1 and compared against the word, so the documented spelling produced a child that was not modal; tuish_host_pane with no arguments stored a rectangle of spaces, which is not empty, so every later query parsed blanks and compared them as numbers; and host.sh was the only module with no load guard, where a second source would reset the child table out from under its children. 37 host tests, up from 20. The one that matters asserts the gap between two children is the host's to paint.
…es not own One process, one set of global shell variables, one terminal — and now several independent contexts living inside all three. Everywhere the singular substrate leaked through the per-context abstraction, it did so as a silent corruption. Three of those leaks, closed. `local` is a lie on ksh93, and it is the expensive kind. There the alias is `typeset`, which scopes only in a ksh-STYLE function; every function here is POSIX-style, so every `local` in tuish is really a GLOBAL. A framework helper's scratch variable overwrites any caller variable that shares its name. It was not hypothetical: `_base` in the event loop overwrote the driver's own `_base`, turning `E 91 49 …` into `E 91 1 …` on the second iteration and taking 176 modifier tests with it; draw.sh's `_top` overwrote a host's layout row and handed "╭────────╮" to tuish_text as a coordinate. compat.sh now re-declares every framework function ksh-style after the fact (`typeset -f`, one fork, not one per function) so `local` finally scopes — with a load-bearing skip list for the trap path, the reflective name-deref helpers, and the blocking loop, each of which a ksh scope would break. The functions that must stay POSIX get namespaced locals instead (`_tuish_`-prefixed), because a framework local is a framework global on one of our five shells; name it like one. test_scope and test_namespace pin both halves. An app's state is plain globals, and a shell has exactly one of each, so mounting the editor twice meant two widgets sharing one cursor. tuish_ctx_declare lets an app declare its state as a field SET at source time; each mounted instance gets its own copy, marshalled with the context. examples/twins.sh is the proof — the same editor mounted twice, each with its own buffer and caret. test_fields covers it. And the device is singular where the region is not. `_tuish_hosted` conflated "I draw into a region" (true for everyone, including the root) with "I own the terminal" (true for exactly one context) — so a child unmounted from a rectangle it never owned would put the alt-screen, the mouse mode, or the scroll region back for EVERYBODY. It is `_tuish_owns_dev` now, and only the framework branches on it; `tuish_hosted` stays as the documented predicate. Device state that an app REQUESTS and only the device layer restores: the mouse already gated this way, and detailed mode and the caret shape now join it. The caret's shape rides with the caret and is re-asserted every frame, so a child mounted inside a host's event handler — whose frame the rAF path discards — no longer loses its `ESC[6 q` into a buffer that was never written (a thin bar in the terminal, a fat block on the website, from one line of code). event.sh forgets the shape cache wherever it throws frame content away, so it can never claim bytes that never landed. The root seats itself too now: its region is the whole screen, refusing the malformed escapes it used to wave through for out-of-bounds rows and columns. test_device draws the line between the two kinds of state. REPORT.md (a review of this branch) is intentionally left untracked.
_tuish_raf_inhibit says "do not peek at the input right now: the next sequence's ESC byte has already been read, so a peek would eat its body". It was a context register, and it says nothing about any context — there is one input stream, one _tuish_pending_byte, one reader. The bug that hides there needs a host to show itself, which is why the escape-burst tests never caught it. tuish_run raises the flag in ITS context mid-sequence, then a cooperative host routes the event onward with tuish_ctx_dispatch — which activates the child, whose saved frame carries its own copy of the flag: 0. The child requests a redraw, reaches the rAF check, sees no inhibit, and peeks. That peek stashes a byte in _tuish_pending_byte while tuish_run's escape loop is reading the tty directly, so the sequence body it was assembling loses a byte to a replay queue the loop will not drain until later. The bytes come back out reordered: exactly the failure the flag exists to prevent, entered through the front door. Same reasoning as TUISH_HANDLED, one line below it: state that describes the DEVICE, or the single event in flight, must not be marshalled per context. The redraw REQUEST stays per-context — a child's pending frame is not its host's — and the tests now pin both, so the two cannot drift back together.
tuish_request_redraw defers the render while input is pending, which is right for an editor and coalesces an autorepeat burst into one paint. But "input is pending" is a -t0 peek: it does not say a burst is coming, it says we are BEHIND — a byte is already buffered. Deferring on that with no bound is a livelock. Once a frame costs more than the terminal's autorepeat interval, bytes queue faster than they drain, the peek never comes back empty, and the screen is withheld until the key is RELEASED. Every app has this; game.sh only meets it first, which is why it opted out of the scheduler and paints by hand. Two clauses fix it. An IDLE never defers. Idle MEANS the input was exhausted — the reader let a whole interval elapse with nothing arriving to produce the event — so a byte landing while the handler runs does not undo the wait already paid. It is also the safest place to render: not testing means not peeking, and the peek is the only thing there that can reorder a byte. And the deferral gets a budget: hold at most TUISH_DEFER_MAX events, then paint regardless. The budget is a trade, and the direction that bites is the unobvious one — each forced render costs a frame the backlog must chew through, so too SMALL a budget drains slower than input arrives, the queue grows without bound, and letting go of the key leaves the app still acting on it a second later. It must stay above autorepeat_rate x frame_cost; 8 carries a 266ms frame against VT's ~30/s, and around 4 it inverts. The condition asks _class rather than latching TUISH_EVENT_KIND before the handler. The latch cost ~8% of every dispatch, measured, to answer a question only a pending redraw ever asks — and _class is the better answer anyway: it is the descriptor we were woken by, where TUISH_EVENT_KIND is a context field holding whatever the handler last left there.
An idle event is not a timer. It is the reader's `read -t<interval>` timing out — so the tick fires while a key is held if and only if the idle interval is SHORTER than the terminal's autorepeat interval. Above it a byte is always waiting before the timeout can land, the read never times out, and a real-time app's clock stops dead for as long as the key is down. Nothing stated that constraint, and the margin is thinner than it looks: the platformer polls at 20ms against a typical ~33ms autorepeat, so it survives on 13ms of slack. `xset r rate 250 50` is a common setting and erases it outright — hold a movement key and the player takes one step and freezes until release, standalone and hosted alike, with nothing in the code to say why. The demo's own claim that it "plays at the same speed for any TUISH_IDLE_TIMEOUT" is true for gravity and false for a held key, where a coarse enough interval means no movement at all. So tuish_run counts complete events since a read last timed out, and injects the tick the reader is not delivering. Healthy interleaving is `idle, byte, idle, byte`: the count never passes 1, nothing is injected, and the clock is exactly what it always was — that case is pinned by a test, because a guarantee that taxes the healthy path is not worth having. Reaching 2 means two events with no timeout between them, which IS a starved tick, so we owe one. A threshold of 1 would fire after every byte and double the rate. The check sits where the loop is between complete events, before the escape FSM starts reading a body — an idle injected mid-sequence would split an ESC from its parameters. It counts events, not bytes, so a held arrow banks credit like a held letter. This buys LIVENESS, not fidelity: under starvation the tick fires per-N-events rather than per-wall-ms, so game-time dilates. It never stops, which is what it did before. Fidelity needs a wall clock, and reading one costs a fork per tick on the shells this targets.
A plain VT reports no key release. Hold a key and the tty just repeats the same event at the OS autorepeat rate, so "is A down?" has no direct answer — an app that wants one infers it from recency, and every real-time app has to. examples/game.sh already did, by hand, as MOVE_TTL_US. The inference has one number in it and that number has to land between two timescales the app does not choose: above the ~33ms autorepeat interval, or a held key looks released in the gaps between repeats; below the ~500ms initial repeat delay, or a single tap is still "down" when the first repeat lands and a tap becomes a hold. Miss on either side and it is not subtly wrong, it is the wrong feature. So the framework keeps the window. tuish_key_track declares the keys, tuish_key_down asks, tuish_key_ttl sets the window. Each matching event refills, each idle tick drains it by TUISH_TICK_US — which is the other half of why this cannot live in an app: a hosted child is ticked at its own negotiated rate, and it cannot learn that rate without asking whether it is hosted, which is a smell. Driving the decay from the parse means a child gets it at its own rate for free. TUISH_KEY_REPEAT comes with it, and it is the part that makes the API usable rather than merely present. It is the press/repeat split kitty reports natively and VT does not, synthesized from the window already being kept: the slot was empty, so this is a press; the slot was open, so the key never came up. Without it an app cannot edge-trigger, because the stamp deliberately lands BEFORE dispatch — a handler must be able to ask about the key that just arrived — so by the time a binding runs, tuish_key_down says "down" either way. Kitty sharpens this and is never required: under tuish_detailed_on a -rel empties the window at once instead of waiting it out. That path also routes around hid.sh spelling a press `char a` but its repeat `a-rep`, which would otherwise make a physically-held key read as up the moment any app enabled detailed mode. The set is one string in one context field, not a table of per-context variables like the bind table: that table costs tuish_ctx_destroy a hardcoded block reaching into keybind.sh's internals to unset by name, and a second one is not worth an eval per key per tick to decrement four integers. Both hot-path hooks are guarded by one comparison, and the bench says an app that tracks nothing pays nothing.
The game carried its own "is a movement key still held" window, because a plain VT never says so: pressing jump stops the movement key's repeats, and there is no release event to miss, so a running jump had to be told from a standing one by recency alone. That window is now the library's (tuish_key_track / tuish_key_down / tuish_key_ttl), and the demo just asks. The value and the reasoning are unchanged — 0.15s, above the autorepeat gap and below the initial repeat delay — but the decay was the part the game could no longer do for itself. It aged the window by TICK_DT on its own idle tick, which is right exactly while it owns the clock; hosted, its tick is negotiated with the host and divided down per child, and the number it would have to age against is one it does not get to know. Driving the decay from the parse means a hosted game gets it at its own rate without asking whether it is hosted. Declaring the set is also how it clears, so the respawn paths re-declare where they used to zero the counter — a stale hold must not survive a death for the next jump to carry. Movement is deliberately untouched: _walk still steps a tile per accepted repeat, and _step_acc still throttles the held run to RUN_SPEED. The dash this branch set out to fix was never a movement bug — the clock was stopping under a held key, and the tick guarantee fixed it with this file byte-identical. Rewriting the cadence on top of that would be changing a feel that is already right. Verified in a terminal, both directions: a standing jump goes straight up, a running jump carries.
Three things the code now does that nobody could have guessed from the docs, and one of them is the constraint this whole branch came out of. tui.md: an idle event is `read -t<interval>` timing out. Nothing schedules it. So while a key is held the tick fires only if the interval is SHORTER than the terminal's autorepeat interval — above it the read never times out and a real-time app's clock stops for the whole hold. The margin is thin (20ms against ~33ms) and a common `xset r rate` erases it. The library now refuses to let the clock stop outright, but that is a floor and not a substitute for picking a sane interval, so both halves are written down. Also retires the claim that a demo plays the same at any TUISH_IDLE_TIMEOUT: true for anything the tick integrates, false for anything paced by a held key. event.md: the rAF deferral is bounded. "Input is pending" is a zero-timeout peek — it says we are behind, not that a burst is coming — and deferring on it forever withholds the screen until the key is released. The budget's floor gets said out loud too, because it is the counter-intuitive end: too small and the queue outgrows the drain. hid.md: the held-key API, and the three timescales it lives between — above the autorepeat gap, below the initial repeat delay. Miss either side and it is not subtly wrong, it is the wrong feature, so the numbers are in a table rather than in someone's head. Kitty is documented as what it is: an accuracy upgrade to the same calls, never a requirement. The platformer's header loses its own version of the same lie — it claimed the throttle kept the idle tick "never starved", which is exactly the assumption that broke.
It can, and the table saying otherwise cost real time: chasing a report of a held key running slow in the browser, "busybox sh = second resolution, 1s minimum" made a 1s idle timeout the obvious suspect. It was never there. busybox ash reads fractional timeouts whenever it is built with FEATURE_SH_READ_FRAC, which is usual, and probes `sub` — natively and in the wasm build the browser runs. The table was also the wrong shape for the question. Resolution is PROBED, not implied by the shell's name: _tuish_init_timing runs `read -t0.01` and believes the answer, so the row that matters is "a shell whose read -t rejects a fraction", not a list of names that drifts out of date the moment a build flag changes. Say that, and point at TUISH_TIMING. While here: note that an idle tick is that read TIMING OUT, and link the constraint that falls out of it — an app whose interval exceeds the terminal's autorepeat interval gets no idle at all while a key is held. That is the thing the table sent me looking away from.
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.
No description provided.