Skip to content

Version Packages - #121

Merged
kud merged 1 commit into
mainfrom
changeset-release/main
Sep 15, 2026
Merged

kud merged 1 commit into
mainfrom
changeset-release/main

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

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 reviewThreads at last: 20 and selects totalCount beside it, taking the whole query from 114 points to 54.

    The window was first: 50 because 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 the myPRs cap and the minimal shape 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: 20 and last: 20 both measure 8 points on myPRs, 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#69 carried 67 threads while it was open.

    totalCount is 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 trap sourceCoverage already exists for one level up: count what came back, call it the total, and a truncated row reads as complete. @kud/gh-workflow's threadsTotal now reads the scalar, falling back to nodes.length only 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: isResolved beyond the window is still unseen, so a PR past twenty threads can under-report unresolved ones to computeHealth. 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 → base only 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: → develop wedged 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". So base comes 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 onto develop on 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. → main on 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 says develop they stopped weeks ago. Always-shown-and-dim was not the neutral option — it manufactured the blindness.

    @kud/gh gains fetchDefaultBranch, a gh repo view --json defaultBranchRef call in its own module. It is a second call rather than a field on the health projection because gh pr view --json has 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. undefined already 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 → main back 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 → base appears 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: N on a Relay connection returns the OLDEST N, and the inbox had been asking that way since the window was introduced. Verified on kud/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, while last: 3 returns 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's map.ts were wrong, in different directions and both in the unsafe one. conversationOf builds lastEventAt as a max over thread comment times, so the whose-move clock read stale by days on exactly the pull requests with live discussion. Worse, computeHealth tests unresolvedThreads > 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 to waiting or approved and the row read clear. A false threads costs a glance; a false clear costs a missed review.

    last biases both the other way, and costs exactly what first cost at the same window size. The query-side pin lives in @kud/gh's inbox.test.ts and the consumer-side pin in @kud/gh-workflow's map.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.

    GHDetail also gains threadsSampled, how many threads the fetch actually returned, beside the threadsTotal that 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: totalCount counts resolved and unresolved alike and GitHub offers no isResolved argument, so unresolvedThreads past 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 ago exceeded 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 kud on 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 → base is 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.

    trail on the returned Summary splits into author and opened, since dropping them independently is the whole point.

  • 4c8bc86: The PR summary line draws → base only 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: → develop wedged 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". So base comes 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 onto develop on 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. → main on 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 says develop they stopped weeks ago. Always-shown-and-dim was not the neutral option — it manufactured the blindness.

    @kud/gh gains fetchDefaultBranch, a gh repo view --json defaultBranchRef call in its own module. It is a second call rather than a field on the health projection because gh pr view --json has 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. undefined already 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 → main back 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 → base appears 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 q and esc back to the app. esc goes back one level, q quits 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 *-ink package takes onBack and never binds esc or q itself — the host's peel routes to it. Five views were binding both, which is why q inside 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. FilePicker and CheckLogView publish nothing at all, because they are leaves: they push no layers, so the drill above closes them.

    AiLauncher is 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. Its step stays 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: q in a drill now quits instead of going back, and every drill footer reads esc back rather than q/esc back. A reply box is unaffected — the root stands its keys down while a text field has focus, so q types a q and backspace deletes.

    The process.exit(0) after runHere() 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

  • Updated dependencies [c1115f3]
  • Updated dependencies [4d5f45b]
  • Updated dependencies [4c8bc86]
  • Updated dependencies [2e307ca]
  • Updated dependencies [cac544b]
  • Updated dependencies [7f5a616]
  • Updated dependencies [a59a289]
    • @kud/gh@0.17.0
    • @kud/gh-workflow@0.12.1
    • @kud/gh-ink@0.57.0

@kud/gh-ink@0.57.0

Minor Changes

  • 4d5f45b: q, esc and backspace belong to the app now, bound once at the root through @kud/ink-ui's useAppKeys. The browse screen publishes a peel instead of claiming the keys itself.

    Ink runs every active useInput on every key, with no order and no propagation — so "who gets esc" cannot be settled by layering handlers. It is settled by there being one claimant, and there were five for q and eight for esc.

    A hole this closes for free: during the cold-fetch loading phase nothing bound q at all. App returns 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. q in the repo picker quits, where it previously did nothing at all — the picker's branch returned unconditionally and swallowed it. And esc with 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, so esc there never reaches the peel and the field's own handler stays live. That inactivity is also what keeps q a letter while you are typing.

    hidden keeps 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 simply isActive: 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 q and esc back to the app. esc goes back one level, q quits 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 *-ink package takes onBack and never binds esc or q itself — the host's peel routes to it. Five views were binding both, which is why q inside 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. FilePicker and CheckLogView publish nothing at all, because they are leaves: they push no layers, so the drill above closes them.

    AiLauncher is 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. Its step stays 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: q in a drill now quits instead of going back, and every drill footer reads esc back rather than q/esc back. A reply box is unaffected — the root stands its keys down while a text field has focus, so q types a q and backspace deletes.

    The process.exit(0) after runHere() 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 than 6d · 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, secondary for context, dimColor for 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. impliedLabels already 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 — a label:plan view spanning five repos draws plan on all sixty rows while only the one repo in the config is exempt. The section axis is now measured too, over section.items rather 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 that secondary means 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 · 1d is obviously ordered, while 6d · 1w needs 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 takes secondary, 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 takes secondary; 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 invariant health-display.ts exists 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 as none health 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 with conflict, is corrected to +.

    Separately, fifteen hardcoded colour literals in the row — "cyan", "red", "green" — now go through colors.info, colors.error and colors.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@0.17.0
    • @kud/gh-workflow@0.12.1

@kud/gh-pr-comments@0.1.109

Patch Changes

  • Updated dependencies [c1115f3]
  • Updated dependencies [4d5f45b]
  • Updated dependencies [4c8bc86]
  • Updated dependencies [2e307ca]
  • Updated dependencies [cac544b]
  • Updated dependencies [7f5a616]
  • Updated dependencies [a59a289]
    • @kud/gh@0.17.0
    • @kud/gh-ink@0.57.0

@kud/gh-pr-health@0.1.109

Patch Changes

  • Updated dependencies [c1115f3]
  • Updated dependencies [4d5f45b]
  • Updated dependencies [4c8bc86]
  • Updated dependencies [2e307ca]
  • Updated dependencies [cac544b]
  • Updated dependencies [7f5a616]
  • Updated dependencies [a59a289]
    • @kud/gh@0.17.0
    • @kud/gh-ink@0.57.0

@kud/gh-webhook-replay@0.1.27

Patch Changes

  • Updated dependencies [c1115f3]
  • Updated dependencies [4c8bc86]
  • Updated dependencies [cac544b]
    • @kud/gh@0.17.0

@kud/gh-workflow@0.12.1

Patch Changes

  • c1115f3: The inbox windows reviewThreads at last: 20 and selects totalCount beside it, taking the whole query from 114 points to 54.

    The window was first: 50 because 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 the myPRs cap and the minimal shape 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: 20 and last: 20 both measure 8 points on myPRs, 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#69 carried 67 threads while it was open.

    totalCount is 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 trap sourceCoverage already exists for one level up: count what came back, call it the total, and a truncated row reads as complete. @kud/gh-workflow's threadsTotal now reads the scalar, falling back to nodes.length only 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: isResolved beyond the window is still unseen, so a PR past twenty threads can under-report unresolved ones to computeHealth. 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: N on a Relay connection returns the OLDEST N, and the inbox had been asking that way since the window was introduced. Verified on kud/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, while last: 3 returns 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's map.ts were wrong, in different directions and both in the unsafe one. conversationOf builds lastEventAt as a max over thread comment times, so the whose-move clock read stale by days on exactly the pull requests with live discussion. Worse, computeHealth tests unresolvedThreads > 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 to waiting or approved and the row read clear. A false threads costs a glance; a false clear costs a missed review.

    last biases both the other way, and costs exactly what first cost at the same window size. The query-side pin lives in @kud/gh's inbox.test.ts and the consumer-side pin in @kud/gh-workflow's map.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.

    GHDetail also gains threadsSampled, how many threads the fetch actually returned, beside the threadsTotal that 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: totalCount counts resolved and unresolved alike and GitHub offers no isResolved argument, so unresolvedThreads past the window is permanently a sample.

  • Updated dependencies [c1115f3]

  • Updated dependencies [4c8bc86]

  • Updated dependencies [cac544b]

    • @kud/gh@0.17.0

@github-actions
github-actions Bot force-pushed the changeset-release/main branch 5 times, most recently from d4101d7 to 870adc0 Compare September 15, 2026 20:41
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from 870adc0 to ec1a045 Compare September 15, 2026 20:45
@kud
kud merged commit bc13693 into main Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant