@kud/gh-ink 0.25.0 → 0.26.0
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 +51 -4
- 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
|
}
|
|
@@ -1048,6 +1053,18 @@ var moveCursor = (items, current2, dir) => {
|
|
|
1048
1053
|
if (next < 0 || next >= items.length) return current2;
|
|
1049
1054
|
return next;
|
|
1050
1055
|
};
|
|
1056
|
+
var budgetNotice = (budget, now = Date.now()) => {
|
|
1057
|
+
if (!budget) return null;
|
|
1058
|
+
if (new Date(budget.resetAt).getTime() <= now) return null;
|
|
1059
|
+
const per = Math.max(budget.cost, 1);
|
|
1060
|
+
const fetches = Math.floor(budget.remaining / per);
|
|
1061
|
+
if (fetches > 8) return null;
|
|
1062
|
+
const mins = Math.max(
|
|
1063
|
+
1,
|
|
1064
|
+
Math.round((new Date(budget.resetAt).getTime() - now) / 6e4)
|
|
1065
|
+
);
|
|
1066
|
+
return fetches <= 0 ? { label: `\u26A1 API budget spent \xB7 ${mins}m`, critical: true } : { label: `\u26A1 ${fetches} fetch${fetches === 1 ? "" : "es"} left`, critical: false };
|
|
1067
|
+
};
|
|
1051
1068
|
var gapsAbove = (items, i) => {
|
|
1052
1069
|
const item = items[i];
|
|
1053
1070
|
if (!item || i === 0) return false;
|
|
@@ -1626,6 +1643,8 @@ var InboxHeader = ({
|
|
|
1626
1643
|
login,
|
|
1627
1644
|
brand,
|
|
1628
1645
|
scopeLabel,
|
|
1646
|
+
budgetLabel,
|
|
1647
|
+
budgetCritical,
|
|
1629
1648
|
loading,
|
|
1630
1649
|
quiet,
|
|
1631
1650
|
refreshing,
|
|
@@ -1637,17 +1656,19 @@ var InboxHeader = ({
|
|
|
1637
1656
|
const countSeg = loading ? " loading\u2026 " : quiet ? " " : ` ${String(total).padStart(3)} item${total !== 1 ? "s" : ""} \xB7 `;
|
|
1638
1657
|
const userSeg = loading || quiet ? "" : `@${login} `;
|
|
1639
1658
|
const workLabel = scopeLabel ? ` ${scopeLabel} ` : "";
|
|
1659
|
+
const budgetSeg = budgetLabel ? budgetLabel + " " : "";
|
|
1640
1660
|
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
1661
|
const statusSeg = statusText ? statusText + " " : "";
|
|
1642
1662
|
const fill = Math.max(
|
|
1643
1663
|
4,
|
|
1644
|
-
COLS - brand.length - countSeg.length - userSeg.length - workLabel.length - statusSeg.length
|
|
1664
|
+
COLS - brand.length - countSeg.length - userSeg.length - workLabel.length - budgetSeg.length - statusSeg.length
|
|
1645
1665
|
);
|
|
1646
1666
|
return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
1647
1667
|
/* @__PURE__ */ jsx(Text2, { color: "#FF8700", bold: true, children: brand }),
|
|
1648
1668
|
/* @__PURE__ */ jsx(Text2, { dimColor: true, children: countSeg }),
|
|
1649
1669
|
userSeg ? /* @__PURE__ */ jsx(Text2, { children: userSeg }) : null,
|
|
1650
1670
|
scopeLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: workLabel }) : null,
|
|
1671
|
+
budgetLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: budgetCritical ? "#FF5F5F" : "#FF8700", children: budgetSeg }) : null,
|
|
1651
1672
|
statusText ? /* @__PURE__ */ jsx(
|
|
1652
1673
|
Text2,
|
|
1653
1674
|
{
|
|
@@ -2152,6 +2173,8 @@ var BrowseScreen = ({
|
|
|
2152
2173
|
ciJob,
|
|
2153
2174
|
tabHelp,
|
|
2154
2175
|
origin,
|
|
2176
|
+
budget,
|
|
2177
|
+
skippedForBudget,
|
|
2155
2178
|
brand,
|
|
2156
2179
|
mergedUrls,
|
|
2157
2180
|
transients,
|
|
@@ -2611,6 +2634,8 @@ var BrowseScreen = ({
|
|
|
2611
2634
|
sections: localSections,
|
|
2612
2635
|
login,
|
|
2613
2636
|
scopeLabel: origin?.label,
|
|
2637
|
+
budgetLabel: budgetNotice(budget)?.label,
|
|
2638
|
+
budgetCritical: budgetNotice(budget)?.critical,
|
|
2614
2639
|
refreshing,
|
|
2615
2640
|
hasPending,
|
|
2616
2641
|
pendingSummary,
|
|
@@ -2618,6 +2643,10 @@ var BrowseScreen = ({
|
|
|
2618
2643
|
}
|
|
2619
2644
|
),
|
|
2620
2645
|
ciStatusState ? /* @__PURE__ */ jsx(CiStatusLine, { state: ciStatusState, job: ciJob }) : null,
|
|
2646
|
+
skippedForBudget ? /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
2647
|
+
/* @__PURE__ */ jsx(Text2, { bold: true, color: "#FF8700", children: " \u26A1 auto-refresh paused to save API budget" }),
|
|
2648
|
+
/* @__PURE__ */ jsx(Text2, { dimColor: true, children: " r refreshes anyway" })
|
|
2649
|
+
] }) : null,
|
|
2621
2650
|
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(
|
|
2622
2651
|
Tabs,
|
|
2623
2652
|
{
|
|
@@ -2816,9 +2845,21 @@ var App = ({
|
|
|
2816
2845
|
) : "",
|
|
2817
2846
|
[pending]
|
|
2818
2847
|
);
|
|
2848
|
+
const canAffordAuto = () => {
|
|
2849
|
+
const known = budget ?? (cacheKey ? readCache(cacheKey)?.budget : null);
|
|
2850
|
+
if (!known) return true;
|
|
2851
|
+
if (new Date(known.resetAt).getTime() <= Date.now()) return true;
|
|
2852
|
+
return known.remaining >= Math.max(known.cost, 1) * 2;
|
|
2853
|
+
};
|
|
2819
2854
|
const revalidate = (manual = false) => {
|
|
2855
|
+
if (!manual && !canAffordAuto()) {
|
|
2856
|
+
setSkippedForBudget(true);
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
setSkippedForBudget(false);
|
|
2820
2860
|
if (manual) setRefreshing(true);
|
|
2821
2861
|
fetcher().then((fresh) => {
|
|
2862
|
+
if (fresh.budget) setBudget(fresh.budget);
|
|
2822
2863
|
if (cacheKey) writeCache(cacheKey, fresh);
|
|
2823
2864
|
setRefreshing(false);
|
|
2824
2865
|
setFetchedAt(Date.now());
|
|
@@ -2929,6 +2970,10 @@ var App = ({
|
|
|
2929
2970
|
watcher.close();
|
|
2930
2971
|
};
|
|
2931
2972
|
}, [watchPath, watchDebounceMs]);
|
|
2973
|
+
const [budget, setBudget] = useState(
|
|
2974
|
+
() => cacheKey ? readCache(cacheKey)?.budget ?? null : null
|
|
2975
|
+
);
|
|
2976
|
+
const [skippedForBudget, setSkippedForBudget] = useState(false);
|
|
2932
2977
|
const [mergedUrls, setMergedUrls] = useState([]);
|
|
2933
2978
|
const mergeTimers = useRef([]);
|
|
2934
2979
|
useEffect(() => () => mergeTimers.current.forEach(clearTimeout), []);
|
|
@@ -3009,6 +3054,8 @@ var App = ({
|
|
|
3009
3054
|
fetchedAt,
|
|
3010
3055
|
refreshError: refreshError ?? void 0,
|
|
3011
3056
|
origin,
|
|
3057
|
+
budget,
|
|
3058
|
+
skippedForBudget,
|
|
3012
3059
|
hidden: overlay !== null,
|
|
3013
3060
|
mergedUrls,
|
|
3014
3061
|
transients,
|
|
@@ -3065,4 +3112,4 @@ var matchesFilter = (repo, filter) => {
|
|
|
3065
3112
|
};
|
|
3066
3113
|
var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
|
|
3067
3114
|
|
|
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 };
|
|
3115
|
+
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.0",
|
|
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
|
},
|