@kud/gh-ink 0.25.0 → 0.26.1
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 +61 -12
- package/dist/index.js +56 -20
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -48,6 +48,44 @@ interface InboxExtension {
|
|
|
48
48
|
body: (onExit: () => void, target?: ExtensionTarget) => ReactNode;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* What the last fetch cost and what was left afterwards, as reported INSIDE the
|
|
53
|
+
* query response by `rateLimit { cost remaining resetAt }`.
|
|
54
|
+
*
|
|
55
|
+
* Cached because the budget is shared by every instance and every other tool on
|
|
56
|
+
* the account, so the useful reading is the most recent one from anywhere — not
|
|
57
|
+
* whatever this process happens to remember. A cockpit launching cold needs it
|
|
58
|
+
* BEFORE its first fetch, which is the one moment it has no response to read it
|
|
59
|
+
* from.
|
|
60
|
+
*
|
|
61
|
+
* Never from `GET /rate_limit`. That endpoint is served from replicas that do
|
|
62
|
+
* not share state: on 2026-08-27 it reported `used: 0, remaining: 5000` while
|
|
63
|
+
* GraphQL was refusing every call, returned a `reset` exactly 3600s ahead of
|
|
64
|
+
* each read, and gave `used` values that DECREASED between two calls a second
|
|
65
|
+
* apart. This figure comes from the service that enforces the limit, in the same
|
|
66
|
+
* response as the data, so it cannot disagree with itself.
|
|
67
|
+
*/
|
|
68
|
+
type InboxBudget = {
|
|
69
|
+
/** Points left in the window. */
|
|
70
|
+
remaining: number;
|
|
71
|
+
/** What the query just cost, so a caller can price the next one. */
|
|
72
|
+
cost: number;
|
|
73
|
+
/** ISO-8601, from GitHub rather than computed locally. */
|
|
74
|
+
resetAt: string;
|
|
75
|
+
};
|
|
76
|
+
type CachedCockpit = {
|
|
77
|
+
sections: Section[];
|
|
78
|
+
login: string;
|
|
79
|
+
at: number;
|
|
80
|
+
budget?: InboxBudget;
|
|
81
|
+
};
|
|
82
|
+
declare const readCache: (key: string) => CachedCockpit | null;
|
|
83
|
+
declare const writeCache: (key: string, data: {
|
|
84
|
+
sections: Section[];
|
|
85
|
+
login: string;
|
|
86
|
+
budget?: InboxBudget;
|
|
87
|
+
}) => void;
|
|
88
|
+
|
|
51
89
|
type GHDetail = {
|
|
52
90
|
reviewDecision?: string;
|
|
53
91
|
mergeable?: string;
|
|
@@ -238,6 +276,21 @@ declare const filterByRepos: (sections: Section[], repos: Set<string>) => Sectio
|
|
|
238
276
|
declare const withoutItem: (sections: Section[], target: GHItem) => Section[];
|
|
239
277
|
declare const reposInSections: (sections: Section[]) => string[];
|
|
240
278
|
declare const moveCursor: (items: AnyItem[], current: number, dir: 1 | -1) => number;
|
|
279
|
+
/**
|
|
280
|
+
* What to say about the remaining budget, or nothing at all.
|
|
281
|
+
*
|
|
282
|
+
* Priced in whole fetches because that is the unit that runs out: the query
|
|
283
|
+
* costs ~111 points against a 5,000 pool, so "some points left" and "another
|
|
284
|
+
* fetch left" are different questions and only the second one is actionable.
|
|
285
|
+
*
|
|
286
|
+
* Silent above the threshold. A counter on a healthy account is noise in a
|
|
287
|
+
* header already carrying four things, and noise is what gets skimmed past on
|
|
288
|
+
* the one day it matters.
|
|
289
|
+
*/
|
|
290
|
+
declare const budgetNotice: (budget: InboxBudget | null | undefined, now?: number) => {
|
|
291
|
+
label: string;
|
|
292
|
+
critical: boolean;
|
|
293
|
+
} | null;
|
|
241
294
|
/**
|
|
242
295
|
* Whether this row draws a blank line above it.
|
|
243
296
|
*
|
|
@@ -320,6 +373,13 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jir
|
|
|
320
373
|
sections: Section[];
|
|
321
374
|
login: string;
|
|
322
375
|
ciStatus?: CiStatus | null;
|
|
376
|
+
/**
|
|
377
|
+
* What that fetch cost and what is left, if the host's query asked. GitHub
|
|
378
|
+
* answers `rateLimit { cost remaining resetAt }` inside the response for
|
|
379
|
+
* free, so a host that asks pays nothing to know — and an inbox that knows
|
|
380
|
+
* can decline to spend the last of it on a refresh nobody requested.
|
|
381
|
+
*/
|
|
382
|
+
budget?: InboxBudget;
|
|
323
383
|
}>;
|
|
324
384
|
cacheKey?: string;
|
|
325
385
|
title?: string;
|
|
@@ -355,17 +415,6 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jir
|
|
|
355
415
|
extensions?: InboxExtension[];
|
|
356
416
|
}) => React.JSX.Element;
|
|
357
417
|
|
|
358
|
-
type CachedCockpit = {
|
|
359
|
-
sections: Section[];
|
|
360
|
-
login: string;
|
|
361
|
-
at: number;
|
|
362
|
-
};
|
|
363
|
-
declare const readCache: (key: string) => CachedCockpit | null;
|
|
364
|
-
declare const writeCache: (key: string, data: {
|
|
365
|
-
sections: Section[];
|
|
366
|
-
login: string;
|
|
367
|
-
}) => void;
|
|
368
|
-
|
|
369
418
|
/**
|
|
370
419
|
* A named slice of the reader's repos. Two or more turn on the in-app toggle;
|
|
371
420
|
* one or none means an undivided set and no toggle at all.
|
|
@@ -453,4 +502,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
|
|
|
453
502
|
*/
|
|
454
503
|
declare const parsePatterns: (value: string) => string[];
|
|
455
504
|
|
|
456
|
-
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 InboxConfig, type InboxExtension, type JiraTransition, type OriginSplit, type RepoFilter, type RepoHeader, type RepoProfile, type Section, type ShowLess, type ShowMore, type Standing, type SubgroupHeader, type TaskRow, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
|
|
505
|
+
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, type RepoFilter, type RepoHeader, type RepoProfile, type Section, type ShowLess, type ShowMore, type Standing, type SubgroupHeader, type TaskRow, budgetNotice, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
|
package/dist/index.js
CHANGED
|
@@ -632,7 +632,7 @@ var checkoutDirs = () => [
|
|
|
632
632
|
|
|
633
633
|
// src/inbox/cache.ts
|
|
634
634
|
var cacheTtlMs = () => inboxConfig().cacheTtlMs;
|
|
635
|
-
var CACHE_VERSION =
|
|
635
|
+
var CACHE_VERSION = 3;
|
|
636
636
|
var cacheDir = () => join(
|
|
637
637
|
process.env.XDG_CACHE_HOME || join(homedir(), ".cache"),
|
|
638
638
|
inboxConfig().cacheNamespace
|
|
@@ -643,7 +643,12 @@ var readCache = (key) => {
|
|
|
643
643
|
const raw = JSON.parse(readFileSync(cacheFile(key), "utf8"));
|
|
644
644
|
if (raw?.version !== CACHE_VERSION) return null;
|
|
645
645
|
if (!Array.isArray(raw?.sections)) return null;
|
|
646
|
-
return {
|
|
646
|
+
return {
|
|
647
|
+
sections: raw.sections,
|
|
648
|
+
login: raw.login ?? "",
|
|
649
|
+
at: raw.at ?? 0,
|
|
650
|
+
budget: raw.budget
|
|
651
|
+
};
|
|
647
652
|
} catch {
|
|
648
653
|
return null;
|
|
649
654
|
}
|
|
@@ -919,22 +924,11 @@ var YOURS = {
|
|
|
919
924
|
queued: ["waiting", "pending", "threads", "approved"],
|
|
920
925
|
spoken: ["threads"]
|
|
921
926
|
};
|
|
922
|
-
var whoseMove = (health, sectionId, standing, theySpokeLast) =>
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
//
|
|
928
|
-
// The row already SAID this and the band disagreed with it: the turn arrow
|
|
929
|
-
// reads lastActor and drew `←`, and the explain panel spelled out "X spoke
|
|
930
|
-
// last, your reply is owed", while the band filed it under Their move. Two
|
|
931
|
-
// signals on one row, pointing opposite ways, until 2026-08-27.
|
|
932
|
-
//
|
|
933
|
-
// Only in that direction. YOU having spoken last does not hand the row over —
|
|
934
|
-
// red CI on your own PR is yours whether or not you commented after it — so
|
|
935
|
-
// that case falls through to the table.
|
|
936
|
-
theySpokeLast ? "you" : YOURS[standing ?? STANDING[sectionId] ?? "queued"].includes(health) ? "you" : "them"
|
|
937
|
-
);
|
|
927
|
+
var whoseMove = (health, sectionId, standing, theySpokeLast) => {
|
|
928
|
+
const position = standing ?? STANDING[sectionId] ?? "queued";
|
|
929
|
+
if (theySpokeLast && position === "authored") return "you";
|
|
930
|
+
return YOURS[position].includes(health) ? "you" : "them";
|
|
931
|
+
};
|
|
938
932
|
var BAND_LABEL = {
|
|
939
933
|
you: "Your move",
|
|
940
934
|
them: "Their move"
|
|
@@ -1048,6 +1042,18 @@ var moveCursor = (items, current2, dir) => {
|
|
|
1048
1042
|
if (next < 0 || next >= items.length) return current2;
|
|
1049
1043
|
return next;
|
|
1050
1044
|
};
|
|
1045
|
+
var budgetNotice = (budget, now = Date.now()) => {
|
|
1046
|
+
if (!budget) return null;
|
|
1047
|
+
if (new Date(budget.resetAt).getTime() <= now) return null;
|
|
1048
|
+
const per = Math.max(budget.cost, 1);
|
|
1049
|
+
const fetches = Math.floor(budget.remaining / per);
|
|
1050
|
+
if (fetches > 8) return null;
|
|
1051
|
+
const mins = Math.max(
|
|
1052
|
+
1,
|
|
1053
|
+
Math.round((new Date(budget.resetAt).getTime() - now) / 6e4)
|
|
1054
|
+
);
|
|
1055
|
+
return fetches <= 0 ? { label: `\u26A1 API budget spent \xB7 ${mins}m`, critical: true } : { label: `\u26A1 ${fetches} fetch${fetches === 1 ? "" : "es"} left`, critical: false };
|
|
1056
|
+
};
|
|
1051
1057
|
var gapsAbove = (items, i) => {
|
|
1052
1058
|
const item = items[i];
|
|
1053
1059
|
if (!item || i === 0) return false;
|
|
@@ -1626,6 +1632,8 @@ var InboxHeader = ({
|
|
|
1626
1632
|
login,
|
|
1627
1633
|
brand,
|
|
1628
1634
|
scopeLabel,
|
|
1635
|
+
budgetLabel,
|
|
1636
|
+
budgetCritical,
|
|
1629
1637
|
loading,
|
|
1630
1638
|
quiet,
|
|
1631
1639
|
refreshing,
|
|
@@ -1637,17 +1645,19 @@ var InboxHeader = ({
|
|
|
1637
1645
|
const countSeg = loading ? " loading\u2026 " : quiet ? " " : ` ${String(total).padStart(3)} item${total !== 1 ? "s" : ""} \xB7 `;
|
|
1638
1646
|
const userSeg = loading || quiet ? "" : `@${login} `;
|
|
1639
1647
|
const workLabel = scopeLabel ? ` ${scopeLabel} ` : "";
|
|
1648
|
+
const budgetSeg = budgetLabel ? budgetLabel + " " : "";
|
|
1640
1649
|
const [statusText, statusColor] = hasPending ? [`\u25CF ${pendingSummary || "new"} \xB7 r apply`, "#FF8700"] : refreshing ? ["\u21BB refreshing\u2026", "cyan"] : fetchedAt ? [`updated ${agoText(fetchedAt)}`, void 0] : ["", void 0];
|
|
1641
1650
|
const statusSeg = statusText ? statusText + " " : "";
|
|
1642
1651
|
const fill = Math.max(
|
|
1643
1652
|
4,
|
|
1644
|
-
COLS - brand.length - countSeg.length - userSeg.length - workLabel.length - statusSeg.length
|
|
1653
|
+
COLS - brand.length - countSeg.length - userSeg.length - workLabel.length - budgetSeg.length - statusSeg.length
|
|
1645
1654
|
);
|
|
1646
1655
|
return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
1647
1656
|
/* @__PURE__ */ jsx(Text2, { color: "#FF8700", bold: true, children: brand }),
|
|
1648
1657
|
/* @__PURE__ */ jsx(Text2, { dimColor: true, children: countSeg }),
|
|
1649
1658
|
userSeg ? /* @__PURE__ */ jsx(Text2, { children: userSeg }) : null,
|
|
1650
1659
|
scopeLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: workLabel }) : null,
|
|
1660
|
+
budgetLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: budgetCritical ? "#FF5F5F" : "#FF8700", children: budgetSeg }) : null,
|
|
1651
1661
|
statusText ? /* @__PURE__ */ jsx(
|
|
1652
1662
|
Text2,
|
|
1653
1663
|
{
|
|
@@ -2152,6 +2162,8 @@ var BrowseScreen = ({
|
|
|
2152
2162
|
ciJob,
|
|
2153
2163
|
tabHelp,
|
|
2154
2164
|
origin,
|
|
2165
|
+
budget,
|
|
2166
|
+
skippedForBudget,
|
|
2155
2167
|
brand,
|
|
2156
2168
|
mergedUrls,
|
|
2157
2169
|
transients,
|
|
@@ -2611,6 +2623,8 @@ var BrowseScreen = ({
|
|
|
2611
2623
|
sections: localSections,
|
|
2612
2624
|
login,
|
|
2613
2625
|
scopeLabel: origin?.label,
|
|
2626
|
+
budgetLabel: budgetNotice(budget)?.label,
|
|
2627
|
+
budgetCritical: budgetNotice(budget)?.critical,
|
|
2614
2628
|
refreshing,
|
|
2615
2629
|
hasPending,
|
|
2616
2630
|
pendingSummary,
|
|
@@ -2618,6 +2632,10 @@ var BrowseScreen = ({
|
|
|
2618
2632
|
}
|
|
2619
2633
|
),
|
|
2620
2634
|
ciStatusState ? /* @__PURE__ */ jsx(CiStatusLine, { state: ciStatusState, job: ciJob }) : null,
|
|
2635
|
+
skippedForBudget ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
2636
|
+
/* @__PURE__ */ jsx(Text2, { bold: true, color: "#FF8700", children: " \u26A1 auto-refresh paused to save API budget" }),
|
|
2637
|
+
/* @__PURE__ */ jsx(Text2, { dimColor: true, children: " r refreshes anyway" })
|
|
2638
|
+
] }) : null,
|
|
2621
2639
|
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(
|
|
2622
2640
|
Tabs,
|
|
2623
2641
|
{
|
|
@@ -2816,9 +2834,21 @@ var App = ({
|
|
|
2816
2834
|
) : "",
|
|
2817
2835
|
[pending]
|
|
2818
2836
|
);
|
|
2837
|
+
const canAffordAuto = () => {
|
|
2838
|
+
const known = budget ?? (cacheKey ? readCache(cacheKey)?.budget : null);
|
|
2839
|
+
if (!known) return true;
|
|
2840
|
+
if (new Date(known.resetAt).getTime() <= Date.now()) return true;
|
|
2841
|
+
return known.remaining >= Math.max(known.cost, 1) * 2;
|
|
2842
|
+
};
|
|
2819
2843
|
const revalidate = (manual = false) => {
|
|
2844
|
+
if (!manual && !canAffordAuto()) {
|
|
2845
|
+
setSkippedForBudget(true);
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
setSkippedForBudget(false);
|
|
2820
2849
|
if (manual) setRefreshing(true);
|
|
2821
2850
|
fetcher().then((fresh) => {
|
|
2851
|
+
if (fresh.budget) setBudget(fresh.budget);
|
|
2822
2852
|
if (cacheKey) writeCache(cacheKey, fresh);
|
|
2823
2853
|
setRefreshing(false);
|
|
2824
2854
|
setFetchedAt(Date.now());
|
|
@@ -2929,6 +2959,10 @@ var App = ({
|
|
|
2929
2959
|
watcher.close();
|
|
2930
2960
|
};
|
|
2931
2961
|
}, [watchPath, watchDebounceMs]);
|
|
2962
|
+
const [budget, setBudget] = useState(
|
|
2963
|
+
() => cacheKey ? readCache(cacheKey)?.budget ?? null : null
|
|
2964
|
+
);
|
|
2965
|
+
const [skippedForBudget, setSkippedForBudget] = useState(false);
|
|
2932
2966
|
const [mergedUrls, setMergedUrls] = useState([]);
|
|
2933
2967
|
const mergeTimers = useRef([]);
|
|
2934
2968
|
useEffect(() => () => mergeTimers.current.forEach(clearTimeout), []);
|
|
@@ -3009,6 +3043,8 @@ var App = ({
|
|
|
3009
3043
|
fetchedAt,
|
|
3010
3044
|
refreshError: refreshError ?? void 0,
|
|
3011
3045
|
origin,
|
|
3046
|
+
budget,
|
|
3047
|
+
skippedForBudget,
|
|
3012
3048
|
hidden: overlay !== null,
|
|
3013
3049
|
mergedUrls,
|
|
3014
3050
|
transients,
|
|
@@ -3065,4 +3101,4 @@ var matchesFilter = (repo, filter) => {
|
|
|
3065
3101
|
};
|
|
3066
3102
|
var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
|
|
3067
3103
|
|
|
3068
|
-
export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
|
|
3104
|
+
export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, budgetNotice, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kud/gh-ink",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
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,7 +48,7 @@
|
|
|
48
48
|
"react": ">=19"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@kud/gh": "0.
|
|
51
|
+
"@kud/gh": "0.6.0",
|
|
52
52
|
"@kud/ink-ui": "0.14.0",
|
|
53
53
|
"zx": "8.8.5"
|
|
54
54
|
},
|