Version Packages - #121
Merged
Merged
Version Packages#121
Conversation
github-actions
Bot
force-pushed
the
changeset-release/main
branch
5 times, most recently
from
September 15, 2026 20:41
d4101d7 to
870adc0
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
September 15, 2026 20:45
870adc0 to
ec1a045
Compare
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.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@kud/gh@0.17.0
Minor Changes
c1115f3: The inbox windows
reviewThreadsatlast: 20and selectstotalCountbeside it, taking the whole query from 114 points to 54.The window was
first: 50because fifty is a round number. It is also the dominant cost on every PR-bearing source, because it multiplies beneath five searches at once — the same multiplication themyPRscap and theminimalshape were both introduced to fight, left untouched in the one selection that spends the most. Measured 2026-09-09 on a live account, the whole document, same selections that ship: a window of 50 cost 114 points at 27,230 nodes, 20 cost 54, and 10 cost 34. The anchor is free —first: 20andlast: 20both measure 8 points onmyPRs, so taking the window from the other end costs nothing. Against 5,000 points an hour that is 92 loads where there were 44, on a query a naive poll has previously killed the board by refreshing.The data never justified fifty. Across 13 pull-request rows on that account the deepest carried two review threads and the median carried none — nothing above ten on either PR source. Twenty keeps ten times it — the step down to ten buys three more points and spends the only headroom there is against the next pull request that gets busy, and
kud/ambre#69carried 67 threads while it was open.totalCountis what makes the narrower window safe rather than merely cheap, and it is the half that has to ship with it. A window smaller than the world is the trapsourceCoveragealready exists for one level up: count what came back, call it the total, and a truncated row reads as complete.@kud/gh-workflow'sthreadsTotalnow reads the scalar, falling back tonodes.lengthonly for a caller whose own query omits it — so a PR carrying twenty-four threads reports twenty-four rather than twenty. The coverage this buys is on the COUNT alone:isResolvedbeyond the window is still unseen, so a PR past twenty threads can under-report unresolved ones tocomputeHealth. That is the direction to fail in, and it is why the window keeps room for a busy PR rather than for a quiet week.4c8bc86: The PR summary line draws
→ baseonly when the base is not the repo's default branch, and draws it a tier brighter than the provenance around it. The arrow's presence is now the signal.A pull request onto
develop, or onto a stacked base, read identically to an ordinary one in every other cell on that line — which made it exactly the fact you can be wrong about and never notice. The line exists to answer "what IS this pull request" before you start reading CI results, and it was silent on the one thing that changes the answer.Suppression alone would not have done it. Presence cannot fire from inside the dim tier:
→ developwedged between two branch-shaped tokens, in a run already littered with·, at identical luminance, gives the eye no reason to stop — and this module's own header defines dim as "provenance you look at deliberately or not at all". Sobasecomes back as its own cell and is drawn at the plain tier while the head branch stays dim. Two channels, presence and luminance, both already in this screen's vocabulary, and no hue spent: there is no "notable" colour token, a PR ontodevelopon a repo with a develop flow is entirely correct rather than wrong, and colour is the one channel a colourblind reader cannot use alone.Why suppress rather than always draw it brighter: the draft marker's own rationale says a cell that is usually empty teaches you to skip past it. A cell that is usually identical teaches the same skip, faster.
→ mainon every pull request is the most efficient way there is to train a reader out of looking at that cell, so by the time it saysdevelopthey stopped weeks ago. Always-shown-and-dim was not the neutral option — it manufactured the blindness.@kud/ghgainsfetchDefaultBranch, agh repo view --json defaultBranchRefcall in its own module. It is a second call rather than a field on the health projection becausegh pr view --jsonhas no default-branch field at all — checked against the live field list on gh 2.100.0 — and because it is a per-repo fact that caches on a different key from anything per-PR. Cockpit keys it by repo through the drill cache, so the answer paints from disk immediately, revalidates behind it, and a repo that renames its default heals itself on the next drill-in. The call is mounted beside the health fetch and adds no wall clock.It throws on a failed lookup rather than resolving
undefined, and the distinction is load-bearing.undefinedalready means draw the base, so against a caller that revalidates on every mount a swallowed failure would redraw a cell that had been correctly suppressed — one network blip flickering→ mainback onto a pull request it had been absent from. Answered-with-no-default and did-not-answer are now different outcomes.The line was deliberately not given a new overflow ladder. It still has the one rung it has always had — the head branch goes, everything else stays — which remains right now that
→ baseappears only when it is notable. That the ladder is one step at all, so a narrow enough terminal wraps a line the module claims never wraps, is true today with no suppression anywhere near it, and is tracked separately.cac544b: The review-thread window is anchored at the newest threads. It was taking the oldest, which made a busy pull request read as clear while a reviewer was waiting.
first: Non a Relay connection returns the OLDEST N, and the inbox had been asking that way since the window was introduced. Verified onkud/ambre#69, which carried 67 review threads and was open for eleven days:reviewThreads(first: 3)returns threads first commented on at 2025-08-12, whilelast: 3returns 2025-08-20 and 2025-08-23. For the whole time that pull request sat in the inbox, the mapper was handed the oldest 50 and never saw the newest 17 — and its newest ten alone carry eight unresolved threads.Both consumers in
@kud/gh-workflow'smap.tswere wrong, in different directions and both in the unsafe one.conversationOfbuildslastEventAtas a max over thread comment times, so the whose-move clock read stale by days on exactly the pull requests with live discussion. Worse,computeHealthtestsunresolvedThreads > 0— and threads get RESOLVED over time, so sampling the oldest end systematically sampled the threads most likely to be resolved already. The token fell through towaitingorapprovedand the row read clear. A falsethreadscosts a glance; a false clear costs a missed review.lastbiases both the other way, and costs exactly whatfirstcost at the same window size. The query-side pin lives in@kud/gh'sinbox.test.tsand the consumer-side pin in@kud/gh-workflow'smap.test.ts, so they fail together if the anchor is ever swapped back for symmetry with the other windows in that file — a change that would compile cleanly and silently reintroduce the false clear.GHDetailalso gainsthreadsSampled, how many threads the fetch actually returned, beside thethreadsTotalthat now reads the connection's own count. Two numbers rather than a truncation flag, so a consumer can say how many it is not showing rather than only that something was cut. One honest limit remains:totalCountcounts resolved and unresolved alike and GitHub offers noisResolvedargument, sounresolvedThreadspast the window is permanently a sample.@kud/gh-cockpit@0.6.0
Minor Changes
2367989: The PR summary line degrades by dropping the least valuable cell rather than the only elastic one, so a narrow terminal no longer wraps a line the module says never wraps.
The header has always claimed "the numbers are never truncated and the line never wraps". The first half held; the second did not. The ladder was one rung — the head branch went, and everything else stayed — so there was no step at all between fitting and wrapping: once
kud · opened 3 months agoexceeded the width on its own, nothing could give and the line wrapped.The ranking, most expendable first, which is now the order cells are dropped: the author, because it is
kudon very nearly every row of a solo cockpit;opened Nd ago, because staleness is the inbox's question and by the time you have drilled in you have already decided to look; then the head branch; and→ baseis kept longest, because since the previous release it is drawn only when it is not the repo default — so its presence already means it is the notable fact on the line.That inverts what the old single rung sacrificed. It gave up the branch name and kept both provenance cells, which is backwards: it protected the two facts you can most afford to lose.
The size, file count and draft marker are never dropped. The numbers are never truncated, and a draft changes the meaning of the whole panel below the line.
trailon the returnedSummarysplits intoauthorandopened, since dropping them independently is the whole point.4c8bc86: The PR summary line draws
→ baseonly when the base is not the repo's default branch, and draws it a tier brighter than the provenance around it. The arrow's presence is now the signal.A pull request onto
develop, or onto a stacked base, read identically to an ordinary one in every other cell on that line — which made it exactly the fact you can be wrong about and never notice. The line exists to answer "what IS this pull request" before you start reading CI results, and it was silent on the one thing that changes the answer.Suppression alone would not have done it. Presence cannot fire from inside the dim tier:
→ developwedged between two branch-shaped tokens, in a run already littered with·, at identical luminance, gives the eye no reason to stop — and this module's own header defines dim as "provenance you look at deliberately or not at all". Sobasecomes back as its own cell and is drawn at the plain tier while the head branch stays dim. Two channels, presence and luminance, both already in this screen's vocabulary, and no hue spent: there is no "notable" colour token, a PR ontodevelopon a repo with a develop flow is entirely correct rather than wrong, and colour is the one channel a colourblind reader cannot use alone.Why suppress rather than always draw it brighter: the draft marker's own rationale says a cell that is usually empty teaches you to skip past it. A cell that is usually identical teaches the same skip, faster.
→ mainon every pull request is the most efficient way there is to train a reader out of looking at that cell, so by the time it saysdevelopthey stopped weeks ago. Always-shown-and-dim was not the neutral option — it manufactured the blindness.@kud/ghgainsfetchDefaultBranch, agh repo view --json defaultBranchRefcall in its own module. It is a second call rather than a field on the health projection becausegh pr view --jsonhas no default-branch field at all — checked against the live field list on gh 2.100.0 — and because it is a per-repo fact that caches on a different key from anything per-PR. Cockpit keys it by repo through the drill cache, so the answer paints from disk immediately, revalidates behind it, and a repo that renames its default heals itself on the next drill-in. The call is mounted beside the health fetch and adds no wall clock.It throws on a failed lookup rather than resolving
undefined, and the distinction is load-bearing.undefinedalready means draw the base, so against a caller that revalidates on every mount a swallowed failure would redraw a cell that had been correctly suppressed — one network blip flickering→ mainback onto a pull request it had been absent from. Answered-with-no-default and did-not-answer are now different outcomes.The line was deliberately not given a new overflow ladder. It still has the one rung it has always had — the head branch goes, everything else stays — which remains right now that
→ baseappears only when it is notable. That the ladder is one step at all, so a narrow enough terminal wraps a line the module claims never wraps, is true today with no suppression anywhere near it, and is tracked separately.2e307ca: The drill views hand
qandescback to the app.escgoes back one level,qquits from any depth, and the footers say so.This is the second half of the navigation contract.
@kud/ink-ui's manual puts it plainly: a view exported from a*-inkpackage takesonBackand never bindsescorqitself — the host's peel routes to it. Five views were binding both, which is whyqinside a drill went back rather than quitting, and why leaving the app from three levels down meant three presses of a key labelled "quit".Each drill now publishes a peel through
DetailContext.registerPeel— its own layers, innermost first, reporting whether there was one to close. When a drill says no, closing the drill is the root's next layer out.FilePickerandCheckLogViewpublish nothing at all, because they are leaves: they push no layers, so the drill above closes them.AiLauncheris the exception that proves the shape. It is not a leaf — agent → placement is two screens — so it publishes just the step it can pop itself, and backing out of the placement returns to the agent list rather than throwing the launcher away. Itsstepstays inside it, where it belongs; lifting it into both callers would have put the launcher's internal state in two places that do not own it.What you will notice:
qin a drill now quits instead of going back, and every drill footer readsesc backrather thanq/esc back. A reply box is unaffected — the root stands its keys down while a text field has focus, soqtypes aqand backspace deletes.The
process.exit(0)afterrunHere()in the AI panel is deliberately untouched. It is not a quit binding: a shell command is taking the terminal over, and Ink's async unmount would race the handover.Patch Changes
@kud/gh-ink@0.57.0
Minor Changes
4d5f45b:
q,escandbackspacebelong to the app now, bound once at the root through@kud/ink-ui'suseAppKeys. The browse screen publishes a peel instead of claiming the keys itself.Ink runs every active
useInputon every key, with no order and no propagation — so "who getsesc" cannot be settled by layering handlers. It is settled by there being one claimant, and there were five forqand eight foresc.A hole this closes for free: during the cold-fetch loading phase nothing bound
qat all.Appreturns above the browse screen and the empty/failed screen only mounts once a fetch has resolved, so a slow first load could be left only with ctrl+c. The hook sits above every phase, so it now answers there too.Two behaviour changes you will notice.
qin the repo picker quits, where it previously did nothing at all — the picker's branch returned unconditionally and swallowed it. Andescwith the repo picker open over a search now closes the picker; before, the arms were written in source order rather than priority order, so an inner layer plus a filter meant one press cleared the filter and left the inner layer standing.The peel is a plain function per screen — a priority order over that screen's own booleans, published through a ref — and deliberately not a back stack, which
@kud/ink-ui's own manual is explicit about. The search field has no arm in it: the root hook is inactive while a text field has focus, soescthere never reaches the peel and the field's own handler stays live. That inactivity is also what keepsqa letter while you are typing.hiddenkeeps both of its jobs — don't render, and don't act on keys. It looks like one boolean doing two things, but the listen half is not simplyisActive: four lines above that guard re-arm the idle pulse deliberately, because a key pressed at a hidden tab is still evidence of a person at the keyboard. Gating the handler itself would have stopped an idle return from re-arming it.The drill views are untouched in this release and still own their own
esc; the root defers to them and behaviour inside a drill is unchanged.2e307ca: The drill views hand
qandescback to the app.escgoes back one level,qquits from any depth, and the footers say so.This is the second half of the navigation contract.
@kud/ink-ui's manual puts it plainly: a view exported from a*-inkpackage takesonBackand never bindsescorqitself — the host's peel routes to it. Five views were binding both, which is whyqinside a drill went back rather than quitting, and why leaving the app from three levels down meant three presses of a key labelled "quit".Each drill now publishes a peel through
DetailContext.registerPeel— its own layers, innermost first, reporting whether there was one to close. When a drill says no, closing the drill is the root's next layer out.FilePickerandCheckLogViewpublish nothing at all, because they are leaves: they push no layers, so the drill above closes them.AiLauncheris the exception that proves the shape. It is not a leaf — agent → placement is two screens — so it publishes just the step it can pop itself, and backing out of the placement returns to the agent list rather than throwing the launcher away. Itsstepstays inside it, where it belongs; lifting it into both callers would have put the launcher's internal state in two places that do not own it.What you will notice:
qin a drill now quits instead of going back, and every drill footer readsesc backrather thanq/esc back. A reply box is unaffected — the root stands its keys down while a text field has focus, soqtypes aqand backspace deletes.The
process.exit(0)afterrunHere()in the AI panel is deliberately untouched. It is not a quit binding: a shell command is taking the terminal over, and Ink's async unmount would race the handover.7f5a616: Three cells on the inbox row stop spending the middle tier on nothing: a label every row in the section carries is suppressed, the age pair reads
6d (1w)rather than6d · 1w, and the repo name in a section rule steps out of the furniture tier.The row has exactly three neutrals — default for the answer,
secondaryfor context,dimColorfor furniture — and the trailing band had stopped reading as three of anything. Each of these is the same fault: a tier spent on something that says nothing, or withheld from something that does.The label cell.
impliedLabelsalready suppresses a label a repo's convention puts on every issue, because a label on every row is the group header repeated. It is keyed on REPO, which is right for a repo convention and blind to uniformity that comes from the QUERY — alabel:planview spanning five repos drawsplanon all sixty rows while only the one repo in the config is exempt. The section axis is now measured too, oversection.itemsrather than the visible window (a cell that appears as you scroll is worse than one always there) and only where the section holds more than one row (one row makes every label trivially uniform, and suppressing there would hide the only classification on screen). What this costs when it is wrong is not a wasted cell but a wasted tier: a tone the eye meets on every single row is calibrated to and filed as background, so a uniform label teaches the reader thatsecondarymeans nothing — and every varied label further down the list inherits that.The age pair. It was
6d · 1w, on the argument that the left value is by construction the smaller of the two and that the invariant teaches the order without a legend, a colour or a second glyph column. That fails twice. Knowing which value is smaller is not knowing which value is which — monotonicity establishes that an ordering exists and says nothing about what the two quantities are. And it only reads as ordered inside one unit:0m · 1dis obviously ordered, while6d · 1wneeds weeks converted to days before the ordering is even visible, and cross-unit pairs are the common case rather than the edge, because GitHub ages cross units within a fortnight. So the one worked example that would teach the pattern is the one almost never on screen. A parenthetical is read as subordinate to the number beside it by everyone, which kills the "two peers separated by a dot" reading: the bare value is the age, the parenthetical the lifetime. It costs nothing — both forms are seven columns — and it frees the·to mean one thing everywhere else on the row.The pair is also tiered now. Both halves were
dimColor, which said "you may skip this" about the half you are actually scanning for. Last-activity takessecondary, the parenthetical stays furniture. The parentheses carry the meaning alone for a reader who sees no colour; the tier only reinforces them.The section rule.
── kud/gh ───drew the repo name and the dashes at the same tier, so the one word on that line that answers "what am I looking at" was painted as skippable. The name takessecondary; the rules stay furniture. An active header keeps its own colour and bold.No new token and no new hue anywhere in this — every change moves a cell between the three neutrals the row already has.
Patch Changes
a59a289: The turn column draws no rightward arrow.
→is a blank now,←is kept, and the row's colour literals go through the design system's tokens.The cursor sits at column 0 and the turn cell at column 4, and both were small rightward points. The collision is not that two marks are close together — it is that they pointed the same way while only one of them is on every row. At scan speed the eye is asking "which row am I on", and a rightward mark four columns in, present on some rows and not others, was a second candidate answer to that question.
Substituting a different rightward glyph would patch the symptom and land on a different neighbour —
▸beside◆is two filled blobs in adjacent cells. Blanking separates the pair by direction, which is a shape channel and therefore survives the colourblind invarianthealth-display.tsexists to enforce; a hue change would not. Nothing else on the row is a horizontal arrow — not the health map, the transit frames, the merge sparkle, the thread glyph or the tag — and the tree run└─is furniture two tiers down and present on every nested row, which is what makes it scenery rather than a competitor.Nothing the cell was carrying is lost.
→said "you spoke last, nothing is being asked of you", which is the absence of a claim — and absence already draws as a blank here, exactly asnonehealth does. The band header says it in words, the unresolved-thread cell is already quiet in that state, and the explain action has room for a sentence. What it buys is a sparse column whose only ink is←, the one state that is a claim on you.The accepted cost, stated rather than discovered: "you spoke last" and "we never learned who spoke" now draw alike. The second is a fetch fact rather than a domain one, and neither is actionable, so it is not worth a column in the aligned zone.
Three text sites moved with it, or the interface would go on teaching a glyph it no longer draws: the two explain lines lose their
(→), and the?legend drops that row. The pin gains a legend row for the first time — it has always sat in this column while the modal documented ten health states, two arrows, and nothing about the+beside them. A stale reference in the pin's own explain line, still naming the!it was moved off when it collided withconflict, is corrected to+.Separately, fifteen hardcoded colour literals in the row —
"cyan","red","green"— now go throughcolors.info,colors.errorandcolors.success. They render identically today; the point is that a literal stops tracking the token the moment the token moves.Updated dependencies [c1115f3]
Updated dependencies [4c8bc86]
Updated dependencies [cac544b]
@kud/gh-pr-comments@0.1.109
Patch Changes
@kud/gh-pr-health@0.1.109
Patch Changes
@kud/gh-webhook-replay@0.1.27
Patch Changes
@kud/gh-workflow@0.12.1
Patch Changes
c1115f3: The inbox windows
reviewThreadsatlast: 20and selectstotalCountbeside it, taking the whole query from 114 points to 54.The window was
first: 50because fifty is a round number. It is also the dominant cost on every PR-bearing source, because it multiplies beneath five searches at once — the same multiplication themyPRscap and theminimalshape were both introduced to fight, left untouched in the one selection that spends the most. Measured 2026-09-09 on a live account, the whole document, same selections that ship: a window of 50 cost 114 points at 27,230 nodes, 20 cost 54, and 10 cost 34. The anchor is free —first: 20andlast: 20both measure 8 points onmyPRs, so taking the window from the other end costs nothing. Against 5,000 points an hour that is 92 loads where there were 44, on a query a naive poll has previously killed the board by refreshing.The data never justified fifty. Across 13 pull-request rows on that account the deepest carried two review threads and the median carried none — nothing above ten on either PR source. Twenty keeps ten times it — the step down to ten buys three more points and spends the only headroom there is against the next pull request that gets busy, and
kud/ambre#69carried 67 threads while it was open.totalCountis what makes the narrower window safe rather than merely cheap, and it is the half that has to ship with it. A window smaller than the world is the trapsourceCoveragealready exists for one level up: count what came back, call it the total, and a truncated row reads as complete.@kud/gh-workflow'sthreadsTotalnow reads the scalar, falling back tonodes.lengthonly for a caller whose own query omits it — so a PR carrying twenty-four threads reports twenty-four rather than twenty. The coverage this buys is on the COUNT alone:isResolvedbeyond the window is still unseen, so a PR past twenty threads can under-report unresolved ones tocomputeHealth. That is the direction to fail in, and it is why the window keeps room for a busy PR rather than for a quiet week.cac544b: The review-thread window is anchored at the newest threads. It was taking the oldest, which made a busy pull request read as clear while a reviewer was waiting.
first: Non a Relay connection returns the OLDEST N, and the inbox had been asking that way since the window was introduced. Verified onkud/ambre#69, which carried 67 review threads and was open for eleven days:reviewThreads(first: 3)returns threads first commented on at 2025-08-12, whilelast: 3returns 2025-08-20 and 2025-08-23. For the whole time that pull request sat in the inbox, the mapper was handed the oldest 50 and never saw the newest 17 — and its newest ten alone carry eight unresolved threads.Both consumers in
@kud/gh-workflow'smap.tswere wrong, in different directions and both in the unsafe one.conversationOfbuildslastEventAtas a max over thread comment times, so the whose-move clock read stale by days on exactly the pull requests with live discussion. Worse,computeHealthtestsunresolvedThreads > 0— and threads get RESOLVED over time, so sampling the oldest end systematically sampled the threads most likely to be resolved already. The token fell through towaitingorapprovedand the row read clear. A falsethreadscosts a glance; a false clear costs a missed review.lastbiases both the other way, and costs exactly whatfirstcost at the same window size. The query-side pin lives in@kud/gh'sinbox.test.tsand the consumer-side pin in@kud/gh-workflow'smap.test.ts, so they fail together if the anchor is ever swapped back for symmetry with the other windows in that file — a change that would compile cleanly and silently reintroduce the false clear.GHDetailalso gainsthreadsSampled, how many threads the fetch actually returned, beside thethreadsTotalthat now reads the connection's own count. Two numbers rather than a truncation flag, so a consumer can say how many it is not showing rather than only that something was cut. One honest limit remains:totalCountcounts resolved and unresolved alike and GitHub offers noisResolvedargument, sounresolvedThreadspast the window is permanently a sample.Updated dependencies [c1115f3]
Updated dependencies [4c8bc86]
Updated dependencies [cac544b]