@kud/gh-ink 0.44.1 → 0.44.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import React, { ReactNode } from 'react';
2
2
  import { PrComments, PrHealthData, PrCheck, Health } from '@kud/gh';
3
- import { StyledLine, PillVariant } from '@kud/ink-ui';
3
+ import { StyledLine } from '@kud/ink-ui';
4
+ import { AnyItem, Section, CiStatus, GHItem, OriginSplit } from '@kud/gh-workflow';
5
+ export { AnyItem, CiStatus, GHDetail, GHItem, InboxConfig, OriginSplit, PIN_MARK, RepoHeader, RepoProfile, Section, ShowLess, ShowMore, Standing, SubgroupHeader, TaskRow, checkoutDirs, configureInbox, filterByOrigin, filterByRepos, filterBySearch, inboxConfig, insertRepoHeaders, labelPriority, layoutGHItems, profileOf, relativeTime, repoPriority, resetInboxConfig, sortByRecency, sortItems, whoseMove } from '@kud/gh-workflow';
4
6
 
5
7
  type CommentsPanelProps = {
6
8
  repo: string;
@@ -183,210 +185,6 @@ declare const writeCache: (key: string, data: {
183
185
  budget?: InboxBudget;
184
186
  }) => void;
185
187
 
186
- type GHDetail = {
187
- reviewDecision?: string;
188
- mergeable?: string;
189
- checksPass: number;
190
- checksFail: number;
191
- checksPending: number;
192
- checksStale?: number;
193
- threadsTotal: number;
194
- lastCommitAt?: string;
195
- lastEventAt?: string;
196
- };
197
- type GHItem = {
198
- kind: "pr" | "issue";
199
- number: number;
200
- title: string;
201
- repo: string;
202
- url: string;
203
- branch?: string;
204
- health: Health;
205
- author?: string;
206
- age: string;
207
- activityAge?: string;
208
- ts: number;
209
- unresolved: number;
210
- conversation: number;
211
- lastActor?: string;
212
- /**
213
- * The item's labels, unordered and by name only — which is all GitHub gives
214
- * without the timeline API. `labels(first: N)` orders by when the label was
215
- * created IN THE REPO, never by when it was applied here, so "the newest two"
216
- * is not derivable and the row ranks them against the host's `labelPriority`
217
- * instead.
218
- *
219
- * Optional because a `minimal` fetch omits the selection entirely — these
220
- * fields vanish rather than degrade — so undefined means "not asked for",
221
- * which the row draws as nothing at all rather than as an unlabelled item.
222
- */
223
- labels?: readonly string[];
224
- detail?: GHDetail;
225
- /**
226
- * Where YOU stand on this row, when the host knows it per row rather than per
227
- * tab. Set it and the whose-move band reads it instead of inferring from the
228
- * section id — which is what lets one tab hold rows of mixed standing: a
229
- * review asked of you and one you have already given differ only in whether
230
- * the ball comes back, and that is a fact about the search a row came from,
231
- * never about the PR. Left unset, the section id decides as before.
232
- */
233
- standing?: Standing;
234
- /**
235
- * A turn the viewer has claimed BY HAND, outranking everything inferred.
236
- *
237
- * The inference below is good and still gets things wrong in one direction it
238
- * cannot see: a row can be genuinely yours while every signal says otherwise —
239
- * nothing red, no unresolved thread, nobody waiting on a word from you — and
240
- * no amount of reading the PR harder will discover it. That knowledge lives
241
- * with the viewer, so this is the field where they put it.
242
- *
243
- * It is a PIN, not a correction: it applies whether or not the row was already
244
- * yours, which is why it does not pair with an opposite. The other direction —
245
- * "no reply is owed for this particular comment" — is a fact about one event
246
- * and belongs where the events are read, not here; a host expresses it by
247
- * handing back a `lastActor` that no longer claims a turn.
248
- */
249
- pinned?: boolean;
250
- /**
251
- * What this row STANDS FOR, when that differs from what it is. Absent means
252
- * the row is a unit of work and is counted as one.
253
- *
254
- * `"container"` marks a row that exists to carry context rather than to be
255
- * done: the initiative a story hangs under, drawn as a real row because it is
256
- * still selectable, openable, and worth seeing until it closes — but not
257
- * itself a thing on anyone's plate. Four stories under one container is four
258
- * items, not five.
259
- *
260
- * Deliberately not `uncounted`. A host knows what a row IS; it should not have
261
- * to know what the tab badge does with that, and a field named after one
262
- * consumer starts lying the moment a second one reads it.
263
- *
264
- * Orthogonal to `depth`, which says where the row hangs rather than what it
265
- * stands for. A container is a genuine depth-0 row with genuine depth-1
266
- * children; spelling it as a depth would mean lying about the tree to fix a
267
- * number, and every site that draws indentation reads that lie as truth.
268
- *
269
- * A union with one member rather than a boolean. `indent` was a boolean that
270
- * turned out to need a scalar, and widening it cost a deprecation that is
271
- * still in this file. Adding a second role here is additive; turning a boolean
272
- * into a union is not.
273
- */
274
- role?: "container";
275
- /**
276
- * How deep this row hangs in the tree: 0 top level, 1 a child, 2 a
277
- * grandchild. Absent means 0.
278
- *
279
- * Always read it through `depthOf`, never off the row — `indent` is still a
280
- * legal way to spell "depth 1", and that helper is the only place that knows
281
- * both spellings. A site that reads `item.indent` directly prices a `depth: 2`
282
- * row as top level, which is the bug this field exists to end rather than a
283
- * new one to introduce.
284
- */
285
- depth?: number;
286
- /**
287
- * @deprecated Legacy spelling of `depth: 1`. Write `depth`; this is kept so
288
- * every existing producer still typechecks, and it is read in exactly one
289
- * place (`depthOf`). Due to be removed at the next major.
290
- */
291
- indent?: boolean;
292
- };
293
- type TaskRow = {
294
- kind: "task";
295
- key: string;
296
- summary: string;
297
- url: string;
298
- status: string;
299
- age: string;
300
- /** What this row stands for. See `GHItem.role`. */
301
- role?: "container";
302
- /** How deep this row hangs. See `GHItem.depth`; read it through `depthOf`. */
303
- depth?: number;
304
- /** @deprecated Legacy spelling of `depth: 1`. See `GHItem.indent`. */
305
- indent?: boolean;
306
- instanceKey?: string;
307
- /**
308
- * Trailing annotation, rendered dim after the summary — a recurrence marker,
309
- * a source hint, anything secondary to the title. Its own node rather than
310
- * part of `summary` so it can be dimmed, and so its width is measured
311
- * separately instead of being smuggled past the truncation maths.
312
- */
313
- note?: string;
314
- /**
315
- * A category this row BELONGS to — `epic`, `blocked`, `spike` — drawn as a
316
- * filled pill after the summary.
317
- *
318
- * Deliberately not a second spelling of `note`, which the epic marker used to
319
- * borrow. The two want opposite weights: a pill says the word itself is the
320
- * information, while `note` is for a reference the reader follows — a parent
321
- * ticket key, a source. Filling a breadcrumb gives it a weight it has not
322
- * earned, and a row can legitimately carry both (a story under someone else's
323
- * epic shows its parent's key AND, one day, a status of its own).
324
- */
325
- pill?: string;
326
- /** Which fill the pill takes. See `@kud/ink-ui`'s `PillVariant`. */
327
- pillVariant?: PillVariant;
328
- /**
329
- * The Jira issue key behind this row, when one exists. Its PRESENCE is what
330
- * turns on the ticket affordances — ↵ opens a menu led by `jira issue view`,
331
- * and `t` transitions the issue. A row without it is just a task: ↵ opens its
332
- * URL and nothing here shells out to `jira`.
333
- *
334
- * Separate from `key` because the two are not the same thing. `key` is the
335
- * left column, and a caller is free to put anything legible there: cockpit
336
- * puts the ticket key, `life` puts a Todoist project name. Reading the drill
337
- * off `key` is what made `life` run `jira issue view "Maison "` and
338
- * label it "View ticket" on a surface that has never touched Jira.
339
- */
340
- ticket?: string;
341
- };
342
- type RepoHeader = {
343
- kind: "repo-header";
344
- repo: string;
345
- age: string;
346
- /** How deep this row hangs. See `GHItem.depth`; read it through `depthOf`. */
347
- depth?: number;
348
- /** @deprecated Legacy spelling of `depth: 1`. See `GHItem.indent`. */
349
- indent?: boolean;
350
- };
351
- type ShowMore = {
352
- kind: "show-more";
353
- hidden: GHItem[];
354
- /** How deep this row hangs. See `GHItem.depth`; read it through `depthOf`. */
355
- depth?: number;
356
- /** @deprecated Legacy spelling of `depth: 1`. See `GHItem.indent`. */
357
- indent?: boolean;
358
- };
359
- type ShowLess = {
360
- kind: "show-less";
361
- toHide: GHItem[];
362
- /** How deep this row hangs. See `GHItem.depth`; read it through `depthOf`. */
363
- depth?: number;
364
- /** @deprecated Legacy spelling of `depth: 1`. See `GHItem.indent`. */
365
- indent?: boolean;
366
- };
367
- type SubgroupHeader = {
368
- kind: "subgroup-header";
369
- label: string;
370
- age: string;
371
- /** How deep this row hangs. See `GHItem.depth`; read it through `depthOf`. */
372
- depth?: number;
373
- /** @deprecated Legacy spelling of `depth: 1`. See `GHItem.indent`. */
374
- indent?: boolean;
375
- };
376
- type AnyItem = GHItem | TaskRow | RepoHeader | SubgroupHeader | ShowMore | ShowLess;
377
- type CiStatus = {
378
- job: string;
379
- buildNumber: number;
380
- result: string;
381
- building: boolean;
382
- url: string;
383
- age: string;
384
- };
385
- type Section = {
386
- id: string;
387
- label: string;
388
- items: AnyItem[];
389
- };
390
188
  type DetailContext = {
391
189
  item: GHItem;
392
190
  kind: "pr" | "issue";
@@ -424,55 +222,11 @@ type JiraTransition = {
424
222
  /** Offered as a second step when the transition asks for a resolution. */
425
223
  resolutions?: string[];
426
224
  };
427
- declare const relativeTime: (iso: string) => string;
428
225
  type ExplainSection = {
429
226
  heading: string;
430
227
  lines: string[];
431
228
  };
432
229
  declare const explainItem: (item: GHItem, login: string) => ExplainSection[];
433
- declare const repoPriority: (repo: string) => number;
434
- declare const labelPriority: (name: string) => number;
435
- declare const sortItems: (items: GHItem[]) => GHItem[];
436
- declare const sortByRecency: (items: GHItem[]) => GHItem[];
437
- declare const insertRepoHeaders: (items: GHItem[]) => AnyItem[];
438
- type Standing = "authored" | "queued" | "spoken";
439
- declare const whoseMove: (health: Health, sectionId: string, standing?: Standing, theySpokeLast?: boolean, pinned?: boolean) => "you" | "them";
440
- declare const PIN_MARK = "+";
441
- declare const layoutGHItems: (items: GHItem[], sectionId: string, login?: string) => AnyItem[];
442
- /**
443
- * A two-way repo split, when a host has one. `undefined` — the ordinary case —
444
- * means no split at all: every row stands.
445
- *
446
- * The package deliberately does not NAME the sides. It hardcoded "work" and
447
- * "home" until 2026-08-27: one reader's two lives compiled into a library
448
- * anyone can install, printed in a header where no other host would recognise
449
- * the word. The predicate, the side, and the word are all yours.
450
- *
451
- * Chosen once, by the host, before the first paint. There is no in-app toggle,
452
- * because a key that flipped it could only ever put the inbox out of step with
453
- * the scope the command was started in, with nothing on screen to explain the
454
- * disagreement.
455
- */
456
- type OriginSplit = {
457
- /** Rows whose repo this matches are one side; everything else is the other. */
458
- match: (repo: string) => boolean;
459
- /** Which side to show. */
460
- show: "matched" | "rest";
461
- /** What to call the visible side in the header. Omitted, nothing is shown. */
462
- label?: string;
463
- };
464
- /**
465
- * Keep only one side of a two-way repo split, leaving non-GH rows (task)
466
- * untouched. Mirrors filterByRepos' gh/other split.
467
- *
468
- * `keep` is positional — "the ones that matched" or "the rest" — and never a
469
- * name. This took the literal strings "work" and "home" until 2026-08-27, which
470
- * is one reader's life compiled into a published library: no other host has
471
- * those two categories, and several have none at all.
472
- */
473
- declare const filterByOrigin: (sections: Section[], keep: "matched" | "rest", match: (repo: string) => boolean) => Section[];
474
- declare const filterBySearch: (sections: Section[], query: string) => Section[];
475
- declare const filterByRepos: (sections: Section[], repos: Set<string>) => Section[];
476
230
  declare const withoutItem: (sections: Section[], target: GHItem) => Section[];
477
231
  declare const reposInSections: (sections: Section[]) => string[];
478
232
  /**
@@ -645,88 +399,6 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jir
645
399
  extensions?: InboxExtension[];
646
400
  }) => React.JSX.Element;
647
401
 
648
- /**
649
- * A named slice of the reader's repos. Two or more turn on the in-app toggle;
650
- * one or none means an undivided set and no toggle at all.
651
- *
652
- * Named rather than boolean because the distinction was never a property of a
653
- * repo — it is whatever separation the reader actually keeps, and a boolean
654
- * leaves a third slice nowhere to go.
655
- */
656
- type RepoProfile = {
657
- /** Stable id — what `--profile=<name>` selects. */
658
- name: string;
659
- /** Shown on the toggle. Defaults to `name`. */
660
- label?: string;
661
- /** Which repos belong to this profile. */
662
- match: (repo: string) => boolean;
663
- /** Where this profile's checkouts live, if it keeps its own directory. */
664
- checkoutDir?: string;
665
- };
666
- type InboxConfig = {
667
- /**
668
- * Repo ranking, best first. An entry ending in `/` matches by owner prefix,
669
- * anything else must equal `owner/name` exactly — so a single repo can outrank
670
- * the owner that contains it. Repos matching nothing sort last, together.
671
- */
672
- repoPriority: readonly string[];
673
- /**
674
- * Label ranking, best first, deciding which two labels a row shows when it
675
- * carries more than two. An entry ending in `*` matches by prefix; anything
676
- * else must equal the label name exactly. Labels matching nothing share the
677
- * last rank and are ordered among themselves by name.
678
- *
679
- * Empty like everything else here, and that is load-bearing rather than
680
- * merely consistent: `plan`, `spike` and `app:*` are one reader's filing
681
- * conventions, invented in their own notes, and a library that shipped them
682
- * as defaults would be ranking a stranger's labels by a scheme they have
683
- * never seen. Empty means a labelled row shows its labels in name order,
684
- * which is honest about knowing nothing.
685
- */
686
- labelPriority: readonly string[];
687
- profiles: readonly RepoProfile[];
688
- /**
689
- * Fallback directory holding checkouts, for repos in no profile that names one
690
- * of its own. Unset means local checkouts are not resolved at all, which is
691
- * correct for a host that only ever opens things in a browser.
692
- */
693
- checkoutDir?: string;
694
- /**
695
- * Directory name the on-disk cache lives under, inside the platform cache
696
- * root. Defaults to this package's name — a library has no business claiming a
697
- * directory named after whichever tool happens to embed it, and two hosts on
698
- * one machine would otherwise share a cache keyed only by section name.
699
- */
700
- cacheNamespace: string;
701
- /**
702
- * How long a cached inbox is trusted before a launch refetches. Every launch
703
- * past it pays the full query — eight searches with check rollups — so this is
704
- * the single knob between "always current" and "always slow, and drawing 502s
705
- * from the API".
706
- *
707
- * It can be generous because staleness is bounded from the other end: acting
708
- * on a row drops the entry outright, and `r` refetches on demand. What the TTL
709
- * actually governs is how long a change made ELSEWHERE — someone approving
710
- * your PR while you were not looking — can go unseen, which is a glance
711
- * arriving late, not a wrong action taken.
712
- */
713
- cacheTtlMs: number;
714
- };
715
- /** Supply the host's opinions. Call once, before rendering. */
716
- declare const configureInbox: (config: Partial<InboxConfig>) => void;
717
- declare const inboxConfig: () => InboxConfig;
718
- /** Test seam — restores the empty defaults. */
719
- declare const resetInboxConfig: () => void;
720
- /** The profile a repo belongs to, or null when none claims it. */
721
- declare const profileOf: (repo: string) => RepoProfile | null;
722
- /**
723
- * Where checkouts are searched, in order: every profile that names its own
724
- * directory, then the fallback. Deduped, since the ordinary single-directory
725
- * case points them all at the same place. Empty when nothing is configured —
726
- * callers treat that as "not checked out locally", which it truthfully is.
727
- */
728
- declare const checkoutDirs: () => string[];
729
-
730
402
  type RepoFilter = {
731
403
  /** Show only repos matching one of these. Empty or absent means show all. */
732
404
  include?: readonly string[];
@@ -746,4 +418,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
746
418
  */
747
419
  declare const parsePatterns: (value: string) => string[];
748
420
 
749
- export { type Action, ActionMenu, type AnyItem, App, COLS, type CiStatus, CiStatusLine, type CiStatusState, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, type GHDetail, type GHItem, HealthPanel, type HealthPanelProps, type InboxBudget, type InboxConfig, type InboxExtension, type JiraTransition, type OriginSplit, PIN_MARK, type RepoFilter, type RepoHeader, type RepoProfile, SIDEBAR_COLS, type Section, type ShowLess, type ShowMore, SidePanel, type Sidebar, type SidebarRow, type Standing, type SubgroupHeader, type TaskRow, budgetNotice, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, counts, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, labelPriority, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, railCapacity, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
421
+ export { type Action, ActionMenu, App, COLS, CiStatusLine, type CiStatusState, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, HealthPanel, type HealthPanelProps, type InboxBudget, type InboxExtension, type JiraTransition, type RepoFilter, SIDEBAR_COLS, SidePanel, type Sidebar, type SidebarRow, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, railCapacity, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, toCiStatusState, topLevelCount, truncate, useActionMenu, windowCount, withHeaders, withoutItem, writeCache };
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ import { spawn } from 'child_process';
8
8
  import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, statSync, watch, rmSync } from 'fs';
9
9
  import { join, dirname, basename } from 'path';
10
10
  import { homedir } from 'os';
11
+ import { depthOf, checkoutDirs, profileOf, inboxConfig, relativeTime, filterBySearch, filterByRepos, filterByOrigin, PIN_MARK, labelPriority } from '@kud/gh-workflow';
12
+ export { PIN_MARK, checkoutDirs, configureInbox, filterByOrigin, filterByRepos, filterBySearch, inboxConfig, insertRepoHeaders, labelPriority, layoutGHItems, profileOf, relativeTime, repoPriority, resetInboxConfig, sortByRecency, sortItems, whoseMove } from '@kud/gh-workflow';
11
13
 
12
14
  // src/components/comments-panel.tsx
13
15
  var ENTITIES = {
@@ -699,30 +701,6 @@ var healthLegend = [
699
701
  ["closed", "Closed"]
700
702
  ];
701
703
 
702
- // src/inbox/config.ts
703
- var EMPTY = {
704
- repoPriority: [],
705
- labelPriority: [],
706
- profiles: [],
707
- cacheNamespace: "gh-ink",
708
- cacheTtlMs: 6e5
709
- };
710
- var current = EMPTY;
711
- var configureInbox = (config) => {
712
- current = { ...EMPTY, ...config };
713
- };
714
- var inboxConfig = () => current;
715
- var resetInboxConfig = () => {
716
- current = EMPTY;
717
- };
718
- var profileOf = (repo) => current.profiles.find((p) => p.match(repo)) ?? null;
719
- var checkoutDirs = () => [
720
- .../* @__PURE__ */ new Set([
721
- ...current.profiles.flatMap((p) => p.checkoutDir ?? []),
722
- ...current.checkoutDir ? [current.checkoutDir] : []
723
- ])
724
- ];
725
-
726
704
  // src/inbox/cache.ts
727
705
  var cacheTtlMs = () => inboxConfig().cacheTtlMs;
728
706
  var CACHE_VERSION = 3;
@@ -903,18 +881,6 @@ var tabsOfMarks = (sections, marks) => {
903
881
  return out;
904
882
  };
905
883
  var quietly = $({ quiet: true });
906
- var depthOf = (item) => {
907
- if (!item) return 0;
908
- if (typeof item.depth === "number") return Math.max(0, Math.floor(item.depth));
909
- return item.indent === true ? 1 : 0;
910
- };
911
- var relativeTime = (iso) => {
912
- const diff = (Date.now() - new Date(iso).getTime()) / 1e3;
913
- if (diff < 3600) return `${Math.floor(diff / 60)}m`;
914
- if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
915
- if (diff < 604800) return `${Math.floor(diff / 86400)}d`;
916
- return `${Math.floor(diff / 604800)}w`;
917
- };
918
884
  var healthSentence = (item) => {
919
885
  const glyph = healthDisplay[item.health].glyph.trim();
920
886
  const d = item.detail;
@@ -1010,160 +976,6 @@ var explainItem = (item, login) => {
1010
976
  { heading: "Whose turn", lines: turnSentences(item, login) }
1011
977
  ];
1012
978
  };
1013
- var repoPriority = (repo) => {
1014
- const order = inboxConfig().repoPriority;
1015
- const i = order.findIndex(
1016
- (p) => p.endsWith("/") ? repo.startsWith(p) : repo === p
1017
- );
1018
- return i === -1 ? order.length : i;
1019
- };
1020
- var labelPriority = (name) => {
1021
- const order = inboxConfig().labelPriority;
1022
- const i = order.findIndex(
1023
- (p) => p.endsWith("*") ? name.startsWith(p.slice(0, -1)) : name === p
1024
- );
1025
- return i === -1 ? Infinity : i;
1026
- };
1027
- var sortItems = (items) => [...items].sort((a, b) => {
1028
- const pd = repoPriority(a.repo) - repoPriority(b.repo);
1029
- if (pd !== 0) return pd;
1030
- const rd = a.repo.localeCompare(b.repo);
1031
- if (rd !== 0) return rd;
1032
- const dd = Number(a.health === "draft") - Number(b.health === "draft");
1033
- return dd !== 0 ? dd : b.ts - a.ts;
1034
- });
1035
- var sortByRecency = (items) => [...items].sort((a, b) => b.ts - a.ts);
1036
- var insertRepoHeaders = (items) => {
1037
- const result = [];
1038
- let lastRepo = "";
1039
- for (const item of items) {
1040
- if (depthOf(item) === 0 && item.repo !== lastRepo) {
1041
- lastRepo = item.repo;
1042
- result.push({
1043
- kind: "repo-header",
1044
- repo: item.repo,
1045
- age: "",
1046
- indent: false
1047
- });
1048
- }
1049
- result.push(item);
1050
- }
1051
- return result;
1052
- };
1053
- var STANDING = {
1054
- mine: "authored",
1055
- // `open` and `draft` predate `mine` and are the same standing: a host that
1056
- // still splits its own PRs by draft-ness keeps working, and one that folds
1057
- // them into a single tab (as cockpit does — the band already sinks a draft,
1058
- // so the split said it twice) gets the same reading.
1059
- open: "authored",
1060
- draft: "authored",
1061
- assigned: "authored",
1062
- issues: "authored",
1063
- review: "queued",
1064
- incoming: "queued",
1065
- reviewed: "spoken"
1066
- };
1067
- var YOURS = {
1068
- authored: [
1069
- "ci-fail",
1070
- "conflict",
1071
- "changes-req",
1072
- "threads",
1073
- "approved",
1074
- "draft"
1075
- ],
1076
- queued: ["waiting", "pending", "threads", "approved"],
1077
- spoken: ["threads"]
1078
- };
1079
- var whoseMove = (health, sectionId, standing, theySpokeLast, pinned) => {
1080
- const position = standing ?? STANDING[sectionId] ?? "queued";
1081
- if (pinned) return "you";
1082
- if (theySpokeLast && position === "authored") return "you";
1083
- return YOURS[position].includes(health) ? "you" : "them";
1084
- };
1085
- var PIN_MARK = "+";
1086
- var BAND_LABEL = {
1087
- you: "Your move",
1088
- them: "Their move"
1089
- };
1090
- var layoutGHItems = (items, sectionId, login) => {
1091
- if (sectionId === "done") return insertRepoHeaders(sortByRecency(items));
1092
- const sorted = sortItems(items);
1093
- const bands = ["you", "them"].map((side) => ({
1094
- side,
1095
- rows: sorted.filter(
1096
- (i) => whoseMove(
1097
- i.health,
1098
- sectionId,
1099
- i.standing,
1100
- !!login && !!i.lastActor && i.lastActor !== login,
1101
- i.pinned
1102
- ) === side
1103
- )
1104
- }));
1105
- const filled = bands.filter((b) => b.rows.length > 0);
1106
- if (filled.length < 2) return insertRepoHeaders(sorted);
1107
- return filled.flatMap(({ side, rows }) => [
1108
- {
1109
- kind: "subgroup-header",
1110
- label: `${BAND_LABEL[side]} (${rows.length})`,
1111
- age: "",
1112
- indent: false
1113
- },
1114
- ...insertRepoHeaders(rows)
1115
- ]);
1116
- };
1117
- var filterByOrigin = (sections, keep, match) => sections.map((s) => {
1118
- const kept = s.items.filter(
1119
- (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && i.kind !== "show-more" && i.kind !== "show-less" && (i.kind === "pr" || i.kind === "issue" ? keep === "matched" ? match(i.repo) : !match(i.repo) : true)
1120
- );
1121
- const gh = kept.filter(
1122
- (i) => i.kind === "pr" || i.kind === "issue"
1123
- );
1124
- const other = kept.filter((i) => i.kind !== "pr" && i.kind !== "issue");
1125
- return { ...s, items: [...layoutGHItems(gh, s.id), ...other] };
1126
- }).filter(
1127
- (s) => s.items.some(
1128
- (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header"
1129
- )
1130
- );
1131
- var searchText = (i) => i.kind === "pr" || i.kind === "issue" ? `${i.title} ${i.repo} #${i.number}` : i.kind === "task" ? `${i.summary} ${i.key}` : "";
1132
- var filterBySearch = (sections, query) => {
1133
- const q = query.trim().toLowerCase();
1134
- if (!q) return sections;
1135
- return sections.map((s) => {
1136
- const kept = s.items.filter(
1137
- (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && i.kind !== "show-more" && i.kind !== "show-less" && searchText(i).toLowerCase().includes(q)
1138
- );
1139
- const gh = kept.filter(
1140
- (i) => i.kind === "pr" || i.kind === "issue"
1141
- );
1142
- const other = kept.filter((i) => i.kind !== "pr" && i.kind !== "issue");
1143
- return { ...s, items: [...layoutGHItems(gh, s.id), ...other] };
1144
- }).filter(
1145
- (s) => s.items.some(
1146
- (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header"
1147
- )
1148
- );
1149
- };
1150
- var filterByRepos = (sections, repos) => {
1151
- if (repos.size === 0) return sections;
1152
- return sections.map((s) => {
1153
- const kept = s.items.filter(
1154
- (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header" && i.kind !== "show-more" && i.kind !== "show-less" && (i.kind === "pr" || i.kind === "issue" ? repos.has(i.repo) : true)
1155
- );
1156
- const gh = kept.filter(
1157
- (i) => i.kind === "pr" || i.kind === "issue"
1158
- );
1159
- const other = kept.filter((i) => i.kind !== "pr" && i.kind !== "issue");
1160
- return { ...s, items: [...layoutGHItems(gh, s.id), ...other] };
1161
- }).filter(
1162
- (s) => s.items.some(
1163
- (i) => i.kind !== "repo-header" && i.kind !== "subgroup-header"
1164
- )
1165
- );
1166
- };
1167
979
  var isHeader = (i) => i.kind === "repo-header" || i.kind === "subgroup-header";
1168
980
  var headerOwnsContent = (items, idx) => {
1169
981
  const kind = items[idx].kind;
@@ -1190,11 +1002,11 @@ var reposInSections = (sections) => [
1190
1002
  sections.flatMap((s) => s.items).filter((i) => i.kind === "pr" || i.kind === "issue").map((i) => i.repo)
1191
1003
  )
1192
1004
  ].sort();
1193
- var moveCursor = (items, current2, dir) => {
1194
- let next = current2 + dir;
1005
+ var moveCursor = (items, current, dir) => {
1006
+ let next = current + dir;
1195
1007
  while (next >= 0 && next < items.length && items[next].kind === "subgroup-header")
1196
1008
  next += dir;
1197
- if (next < 0 || next >= items.length) return current2;
1009
+ if (next < 0 || next >= items.length) return current;
1198
1010
  return next;
1199
1011
  };
1200
1012
  var budgetNotice = (budget, now = Date.now()) => {
@@ -1501,10 +1313,21 @@ var COLS = (process.stdout.columns ?? 120) - FRAME_CHROME_COLS;
1501
1313
  var MODAL_CHROME_COLS = 2 + 1 * 2;
1502
1314
  var OVERLAY_BG = "#22222e";
1503
1315
  var DimContext = createContext(false);
1316
+ var BACKDROP_FG = "#5a5a68";
1317
+ var BACKDROP_FG_RECESSED = "#3a3a46";
1318
+ var backdropStyle = (props) => ({
1319
+ color: props.dimColor ? BACKDROP_FG_RECESSED : BACKDROP_FG,
1320
+ dimColor: false,
1321
+ bold: false,
1322
+ italic: false,
1323
+ inverse: false,
1324
+ backgroundColor: void 0
1325
+ });
1504
1326
  var Text3 = ({ children, ...props }) => {
1505
1327
  const dimmed = useContext(DimContext);
1506
- return dimmed ? /* @__PURE__ */ jsx(Text, { ...props, bold: false, dimColor: true, children }) : /* @__PURE__ */ jsx(Text, { ...props, children });
1328
+ return dimmed ? /* @__PURE__ */ jsx(Text, { ...props, ...backdropStyle(props), children }) : /* @__PURE__ */ jsx(Text, { ...props, children });
1507
1329
  };
1330
+ var useBackdropped = () => useContext(DimContext);
1508
1331
  var Backdrop = ({
1509
1332
  dimmed,
1510
1333
  absolute,
@@ -1981,7 +1804,7 @@ var isDeparture = (t) => t === "out" || t === "moved-out";
1981
1804
  var isArrival = (t) => t === "in" || t === "moved-in";
1982
1805
  var LEAVING_HOLD_MS = 2500;
1983
1806
  var tabMarker = (marked, id, frame = 0) => marked.has(id) ? `${TAB_PULSE[frame % TAB_PULSE.length]} ` : " ";
1984
- var TAB_PULSE = ["\xB7", "\u25CB", "\u25CE", "\u25C9"];
1807
+ var TAB_PULSE = ["\xB7", "\u25CB", "\u25CE", "\u25C9", "\u25CE", "\u25CB"];
1985
1808
  var ItemRow = ({
1986
1809
  item,
1987
1810
  active,
@@ -1995,6 +1818,7 @@ var ItemRow = ({
1995
1818
  sparkFrame = 0,
1996
1819
  cols = COLS
1997
1820
  }) => {
1821
+ const backdropped = useBackdropped();
1998
1822
  const transient = leaving ? "out" : refreshMark;
1999
1823
  const transitFrame = Math.floor(sparkFrame / TRANSIT_FRAME_TICKS);
2000
1824
  const farewellLabel = leaving ? TRANSIT_LABEL.out : "";
@@ -2041,12 +1865,12 @@ var ItemRow = ({
2041
1865
  children: truncate2(item.summary, titleMax2)
2042
1866
  }
2043
1867
  ),
2044
- item.pill ? /* @__PURE__ */ jsxs(Fragment, { children: [
1868
+ item.pill && !backdropped ? /* @__PURE__ */ jsxs(Fragment, { children: [
2045
1869
  /* @__PURE__ */ jsx(Text3, { children: " " }),
2046
1870
  /* @__PURE__ */ jsx(Pill, { variant: item.pillVariant ?? "muted", children: item.pill })
2047
1871
  ] }) : null,
2048
1872
  note ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: ` ${note}` }) : null,
2049
- farewellLabel ? /* @__PURE__ */ jsxs(Fragment, { children: [
1873
+ farewellLabel && !backdropped ? /* @__PURE__ */ jsxs(Fragment, { children: [
2050
1874
  /* @__PURE__ */ jsx(Text3, { children: " " }),
2051
1875
  /* @__PURE__ */ jsx(Pill, { color: TRANSIT_COLOUR.out, children: farewellLabel })
2052
1876
  ] }) : null
@@ -2133,11 +1957,11 @@ var ItemRow = ({
2133
1957
  unresolvedLabel && !givingUp.threads ? /* @__PURE__ */ jsx(Text3, { bold: !spokeLast, color: spokeLast ? "#888888" : "#FF8700", children: " " + unresolvedLabel }) : null,
2134
1958
  showAuthor && !givingUp.author ? /* @__PURE__ */ jsx(Text3, { dimColor: true, italic: true, children: " by " + item.author }) : null,
2135
1959
  ageLabel && !givingUp.age ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: " " + ageLabel }) : null,
2136
- mergedLabel ? /* @__PURE__ */ jsxs(Fragment, { children: [
1960
+ mergedLabel && !backdropped ? /* @__PURE__ */ jsxs(Fragment, { children: [
2137
1961
  /* @__PURE__ */ jsx(Text3, { children: " " }),
2138
1962
  /* @__PURE__ */ jsx(Pill, { color: MERGED_COLOUR, children: mergedLabel })
2139
1963
  ] }) : null,
2140
- farewellLabel ? /* @__PURE__ */ jsxs(Fragment, { children: [
1964
+ farewellLabel && !backdropped ? /* @__PURE__ */ jsxs(Fragment, { children: [
2141
1965
  /* @__PURE__ */ jsx(Text3, { children: " " }),
2142
1966
  /* @__PURE__ */ jsx(Pill, { color: TRANSIT_COLOUR.out, children: farewellLabel })
2143
1967
  ] }) : null
@@ -3093,11 +2917,9 @@ var BrowseScreen = ({
3093
2917
  count: topLevelCount(s),
3094
2918
  // Its own cell, always two wide, so news arriving never slides the
3095
2919
  // bar — and free to animate for exactly that reason.
3096
- marker: tabMarker(
3097
- markedTabs,
3098
- s.id,
3099
- Math.floor(sparkFrame / TRANSIT_FRAME_TICKS)
3100
- ),
2920
+ // The ticker undivided — see TAB_PULSE. The row marks' /3 buys quiet
2921
+ // for motion sitting beside text, and this is not that.
2922
+ marker: tabMarker(markedTabs, s.id, sparkFrame),
3101
2923
  markerColor: "#FF8700"
3102
2924
  }))
3103
2925
  }
@@ -3625,4 +3447,4 @@ var matchesFilter = (repo, filter) => {
3625
3447
  };
3626
3448
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
3627
3449
 
3628
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, PIN_MARK, SIDEBAR_COLS, SidePanel, budgetNotice, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, counts, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, labelPriority, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, railCapacity, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate2 as truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
3450
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, SIDEBAR_COLS, SidePanel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, railCapacity, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, toCiStatusState, topLevelCount, truncate2 as truncate, useActionMenu, windowCount, withHeaders, withoutItem, writeCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.44.1",
3
+ "version": "0.44.3",
4
4
  "description": "Ink components for rendering GitHub PR review comments and health — controlled, presentation-only, built on @kud/ink-ui and fed by @kud/gh.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -48,9 +48,10 @@
48
48
  "react": ">=19"
49
49
  },
50
50
  "dependencies": {
51
- "@kud/gh": "0.9.0",
51
+ "@kud/gh": "0.11.0",
52
52
  "@kud/ink-ui": "0.22.0",
53
- "zx": "8.8.5"
53
+ "zx": "8.8.5",
54
+ "@kud/gh-workflow": "0.2.0"
54
55
  },
55
56
  "devDependencies": {
56
57
  "@types/node": "22.20.1",