@kud/gh-ink 0.24.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 +72 -13
- package/dist/index.js +114 -31
- 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
|
*
|
|
@@ -270,6 +323,7 @@ declare const runHere: (cmd: string) => void;
|
|
|
270
323
|
declare const COLS: number;
|
|
271
324
|
declare const topLevelCount: (s: Section) => number;
|
|
272
325
|
declare const drillCmd: (item: AnyItem) => string | null;
|
|
326
|
+
declare const explainGhAction: (e: unknown) => string;
|
|
273
327
|
declare const buildActions: (item: AnyItem, login: string, showFlash: (msg: string) => void, jiraBase?: string, jiraKeyRe?: RegExp, jiraTransitions?: JiraTransition[], onRefresh?: () => void, onRemove?: (item: GHItem) => void, onOpenView?: (item: AnyItem) => boolean, ext?: {
|
|
274
328
|
extensions?: InboxExtension[];
|
|
275
329
|
onOpenExt?: (id: string, target: ExtensionTarget) => void;
|
|
@@ -314,11 +368,18 @@ declare const ActionMenu: ({ item, actions, cursor, }: {
|
|
|
314
368
|
cursor: number;
|
|
315
369
|
}) => React.JSX.Element;
|
|
316
370
|
declare const signatureOf: (sections: Section[]) => string;
|
|
317
|
-
declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jiraKeyRe, jiraTransitions, hasCiStatus, ciJob, ciFetcher, ciPollMs, watchPath, watchDebounceMs, extensions, tabHelp, emptyHint, }: {
|
|
371
|
+
declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jiraKeyRe, jiraTransitions, hasCiStatus, ciJob, ciFetcher, ciPollMs, watchPath, watchDebounceMs, watchJitterMs, extensions, tabHelp, emptyHint, }: {
|
|
318
372
|
fetcher: () => Promise<{
|
|
319
373
|
sections: Section[];
|
|
320
374
|
login: string;
|
|
321
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;
|
|
322
383
|
}>;
|
|
323
384
|
cacheKey?: string;
|
|
324
385
|
title?: string;
|
|
@@ -342,20 +403,18 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jir
|
|
|
342
403
|
watchPath?: string;
|
|
343
404
|
/** Bursts of writes to collapse into one refetch. */
|
|
344
405
|
watchDebounceMs?: number;
|
|
406
|
+
/**
|
|
407
|
+
* Random spread added to `watchDebounceMs`, so several cockpits woken by one
|
|
408
|
+
* signal take turns instead of stampeding. The first to wake fetches and
|
|
409
|
+
* writes the shared cache; the others adopt it and pay nothing.
|
|
410
|
+
*
|
|
411
|
+
* Defaults to 0 — no randomness unless a host asks for it. Set it to comfortably
|
|
412
|
+
* more than one round trip when you expect several instances to be open.
|
|
413
|
+
*/
|
|
414
|
+
watchJitterMs?: number;
|
|
345
415
|
extensions?: InboxExtension[];
|
|
346
416
|
}) => React.JSX.Element;
|
|
347
417
|
|
|
348
|
-
type CachedCockpit = {
|
|
349
|
-
sections: Section[];
|
|
350
|
-
login: string;
|
|
351
|
-
at: number;
|
|
352
|
-
};
|
|
353
|
-
declare const readCache: (key: string) => CachedCockpit | null;
|
|
354
|
-
declare const writeCache: (key: string, data: {
|
|
355
|
-
sections: Section[];
|
|
356
|
-
login: string;
|
|
357
|
-
}) => void;
|
|
358
|
-
|
|
359
418
|
/**
|
|
360
419
|
* A named slice of the reader's repos. Two or more turn on the in-app toggle;
|
|
361
420
|
* one or none means an undivided set and no toggle at all.
|
|
@@ -443,4 +502,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
|
|
|
443
502
|
*/
|
|
444
503
|
declare const parsePatterns: (value: string) => string[];
|
|
445
504
|
|
|
446
|
-
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, 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;
|
|
@@ -1317,9 +1334,22 @@ var UNSUBSCRIBE_MUTATION = `
|
|
|
1317
1334
|
}
|
|
1318
1335
|
}
|
|
1319
1336
|
`;
|
|
1337
|
+
var GH_ACTION_FAILURES = [
|
|
1338
|
+
[/rate limit|RATE_LIMIT/i, "GitHub rate limit spent \u2014 it resets within the hour"],
|
|
1339
|
+
[/\b401\b|not logged in|authentication/i, "gh is not authenticated"],
|
|
1340
|
+
[/\b403\b|must have admin|not authorized|permission/i, "not permitted on this repo"],
|
|
1341
|
+
[/\b50[0234]\b/, "GitHub's API is failing (5xx)"],
|
|
1342
|
+
[/ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNREFUSED/i, "no route to GitHub"]
|
|
1343
|
+
];
|
|
1344
|
+
var explainGhAction = (e) => {
|
|
1345
|
+
const err = e;
|
|
1346
|
+
const raw = (err.stderr ?? "").trim().split("\n").filter(Boolean).pop() ?? err.message ?? "no reason reported";
|
|
1347
|
+
const human = GH_ACTION_FAILURES.find(([re]) => re.test(raw))?.[1];
|
|
1348
|
+
return human ? `${human} (${raw})` : raw;
|
|
1349
|
+
};
|
|
1320
1350
|
var unsubscribeFrom = async (item) => {
|
|
1321
|
-
const
|
|
1322
|
-
const found = await quietly`gh
|
|
1351
|
+
const route = item.kind === "pr" ? "pulls" : "issues";
|
|
1352
|
+
const found = await quietly`gh api repos/${item.repo}/${route}/${item.number} --jq .node_id`;
|
|
1323
1353
|
const id = found.stdout.trim();
|
|
1324
1354
|
if (!id) throw new Error(`no node id for ${item.repo}#${item.number}`);
|
|
1325
1355
|
await quietly`gh api graphql -f query=${UNSUBSCRIBE_MUTATION} -f id=${id}`;
|
|
@@ -1419,8 +1449,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
|
|
|
1419
1449
|
void quietly`gh pr edit ${item.number} --repo ${item.repo} --remove-reviewer ${login}`.then(() => {
|
|
1420
1450
|
showFlash(`\u2713 Removed you as reviewer on #${item.number}`);
|
|
1421
1451
|
ext?.onActed?.();
|
|
1422
|
-
}).catch(() => {
|
|
1423
|
-
showFlash(`\u2717
|
|
1452
|
+
}).catch((e) => {
|
|
1453
|
+
showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`);
|
|
1424
1454
|
onRefresh?.();
|
|
1425
1455
|
});
|
|
1426
1456
|
}
|
|
@@ -1431,7 +1461,9 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
|
|
|
1431
1461
|
hint: "u",
|
|
1432
1462
|
run: () => {
|
|
1433
1463
|
showFlash(`\u22EF Unsubscribing from #${item.number}\u2026`);
|
|
1434
|
-
void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch(
|
|
1464
|
+
void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch(
|
|
1465
|
+
(e) => showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`)
|
|
1466
|
+
);
|
|
1435
1467
|
}
|
|
1436
1468
|
});
|
|
1437
1469
|
if (item.kind === "pr" && item.branch) {
|
|
@@ -1611,6 +1643,8 @@ var InboxHeader = ({
|
|
|
1611
1643
|
login,
|
|
1612
1644
|
brand,
|
|
1613
1645
|
scopeLabel,
|
|
1646
|
+
budgetLabel,
|
|
1647
|
+
budgetCritical,
|
|
1614
1648
|
loading,
|
|
1615
1649
|
quiet,
|
|
1616
1650
|
refreshing,
|
|
@@ -1622,17 +1656,19 @@ var InboxHeader = ({
|
|
|
1622
1656
|
const countSeg = loading ? " loading\u2026 " : quiet ? " " : ` ${String(total).padStart(3)} item${total !== 1 ? "s" : ""} \xB7 `;
|
|
1623
1657
|
const userSeg = loading || quiet ? "" : `@${login} `;
|
|
1624
1658
|
const workLabel = scopeLabel ? ` ${scopeLabel} ` : "";
|
|
1659
|
+
const budgetSeg = budgetLabel ? budgetLabel + " " : "";
|
|
1625
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];
|
|
1626
1661
|
const statusSeg = statusText ? statusText + " " : "";
|
|
1627
1662
|
const fill = Math.max(
|
|
1628
1663
|
4,
|
|
1629
|
-
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
|
|
1630
1665
|
);
|
|
1631
1666
|
return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
1632
1667
|
/* @__PURE__ */ jsx(Text2, { color: "#FF8700", bold: true, children: brand }),
|
|
1633
1668
|
/* @__PURE__ */ jsx(Text2, { dimColor: true, children: countSeg }),
|
|
1634
1669
|
userSeg ? /* @__PURE__ */ jsx(Text2, { children: userSeg }) : null,
|
|
1635
1670
|
scopeLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: workLabel }) : null,
|
|
1671
|
+
budgetLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: budgetCritical ? "#FF5F5F" : "#FF8700", children: budgetSeg }) : null,
|
|
1636
1672
|
statusText ? /* @__PURE__ */ jsx(
|
|
1637
1673
|
Text2,
|
|
1638
1674
|
{
|
|
@@ -2137,6 +2173,8 @@ var BrowseScreen = ({
|
|
|
2137
2173
|
ciJob,
|
|
2138
2174
|
tabHelp,
|
|
2139
2175
|
origin,
|
|
2176
|
+
budget,
|
|
2177
|
+
skippedForBudget,
|
|
2140
2178
|
brand,
|
|
2141
2179
|
mergedUrls,
|
|
2142
2180
|
transients,
|
|
@@ -2466,7 +2504,9 @@ var BrowseScreen = ({
|
|
|
2466
2504
|
}
|
|
2467
2505
|
if (input === "u" && (activeItem.kind === "pr" || activeItem.kind === "issue")) {
|
|
2468
2506
|
showFlash(`\u22EF Unsubscribing from #${activeItem.number}\u2026`);
|
|
2469
|
-
void unsubscribeFrom(activeItem).then(() => showFlash(`\u2713 Unsubscribed from #${activeItem.number}`)).catch(
|
|
2507
|
+
void unsubscribeFrom(activeItem).then(() => showFlash(`\u2713 Unsubscribed from #${activeItem.number}`)).catch(
|
|
2508
|
+
(e) => showFlash(`\u2717 #${activeItem.number}: ${explainGhAction(e)}`)
|
|
2509
|
+
);
|
|
2470
2510
|
return;
|
|
2471
2511
|
}
|
|
2472
2512
|
if (input === "x" && activeItem.kind === "pr" && activeItem.standing === "queued" && login) {
|
|
@@ -2475,8 +2515,8 @@ var BrowseScreen = ({
|
|
|
2475
2515
|
void quietly`gh pr edit ${activeItem.number} --repo ${activeItem.repo} --remove-reviewer ${login}`.then(() => {
|
|
2476
2516
|
showFlash(`\u2713 Removed you as reviewer on #${activeItem.number}`);
|
|
2477
2517
|
onActed?.();
|
|
2478
|
-
}).catch(() => {
|
|
2479
|
-
showFlash(`\u2717
|
|
2518
|
+
}).catch((e) => {
|
|
2519
|
+
showFlash(`\u2717 #${activeItem.number}: ${explainGhAction(e)}`);
|
|
2480
2520
|
onRefresh?.();
|
|
2481
2521
|
});
|
|
2482
2522
|
return;
|
|
@@ -2594,6 +2634,8 @@ var BrowseScreen = ({
|
|
|
2594
2634
|
sections: localSections,
|
|
2595
2635
|
login,
|
|
2596
2636
|
scopeLabel: origin?.label,
|
|
2637
|
+
budgetLabel: budgetNotice(budget)?.label,
|
|
2638
|
+
budgetCritical: budgetNotice(budget)?.critical,
|
|
2597
2639
|
refreshing,
|
|
2598
2640
|
hasPending,
|
|
2599
2641
|
pendingSummary,
|
|
@@ -2601,6 +2643,10 @@ var BrowseScreen = ({
|
|
|
2601
2643
|
}
|
|
2602
2644
|
),
|
|
2603
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,
|
|
2604
2650
|
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(
|
|
2605
2651
|
Tabs,
|
|
2606
2652
|
{
|
|
@@ -2705,6 +2751,11 @@ var App = ({
|
|
|
2705
2751
|
ciPollMs = 6e4,
|
|
2706
2752
|
watchPath,
|
|
2707
2753
|
watchDebounceMs = 400,
|
|
2754
|
+
// Zero by default, deliberately. A package that adds randomness to its own
|
|
2755
|
+
// timing makes every host's tests flaky — this one's went green locally on a
|
|
2756
|
+
// lucky draw and red in CI. It is also a host concern: only somebody running
|
|
2757
|
+
// several cockpits at once needs the stagger, and only they know how many.
|
|
2758
|
+
watchJitterMs = 0,
|
|
2708
2759
|
extensions,
|
|
2709
2760
|
tabHelp,
|
|
2710
2761
|
emptyHint
|
|
@@ -2794,32 +2845,27 @@ var App = ({
|
|
|
2794
2845
|
) : "",
|
|
2795
2846
|
[pending]
|
|
2796
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
|
+
};
|
|
2797
2854
|
const revalidate = (manual = false) => {
|
|
2855
|
+
if (!manual && !canAffordAuto()) {
|
|
2856
|
+
setSkippedForBudget(true);
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
setSkippedForBudget(false);
|
|
2798
2860
|
if (manual) setRefreshing(true);
|
|
2799
2861
|
fetcher().then((fresh) => {
|
|
2862
|
+
if (fresh.budget) setBudget(fresh.budget);
|
|
2800
2863
|
if (cacheKey) writeCache(cacheKey, fresh);
|
|
2801
2864
|
setRefreshing(false);
|
|
2802
2865
|
setFetchedAt(Date.now());
|
|
2803
2866
|
setRefreshError(null);
|
|
2804
2867
|
if (hasCiStatus) applyCiStatus(fresh.ciStatus ?? null);
|
|
2805
|
-
|
|
2806
|
-
if (!displayedKey.current) {
|
|
2807
|
-
if (fresh.sections.length === 0) {
|
|
2808
|
-
setState({ phase: "empty" });
|
|
2809
|
-
return;
|
|
2810
|
-
}
|
|
2811
|
-
showData(fresh.sections, fresh.login);
|
|
2812
|
-
} else if (freshKey !== displayedKey.current) {
|
|
2813
|
-
const { counts } = diffSections(
|
|
2814
|
-
displayedSections.current,
|
|
2815
|
-
fresh.sections
|
|
2816
|
-
);
|
|
2817
|
-
if (counts.added + counts.removed + counts.changed === 0)
|
|
2818
|
-
showData(fresh.sections, fresh.login);
|
|
2819
|
-
else setPending(fresh);
|
|
2820
|
-
} else {
|
|
2821
|
-
setPending(null);
|
|
2822
|
-
}
|
|
2868
|
+
receive(fresh);
|
|
2823
2869
|
}).catch((err) => {
|
|
2824
2870
|
setRefreshing(false);
|
|
2825
2871
|
const message = err.message;
|
|
@@ -2830,6 +2876,26 @@ var App = ({
|
|
|
2830
2876
|
setRefreshError({ message, at: Date.now() });
|
|
2831
2877
|
});
|
|
2832
2878
|
};
|
|
2879
|
+
const receive = (fresh) => {
|
|
2880
|
+
const freshKey = signatureOf(fresh.sections);
|
|
2881
|
+
if (!displayedKey.current) {
|
|
2882
|
+
if (fresh.sections.length === 0) {
|
|
2883
|
+
setState({ phase: "empty" });
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
showData(fresh.sections, fresh.login);
|
|
2887
|
+
} else if (freshKey !== displayedKey.current) {
|
|
2888
|
+
const { counts } = diffSections(
|
|
2889
|
+
displayedSections.current,
|
|
2890
|
+
fresh.sections
|
|
2891
|
+
);
|
|
2892
|
+
if (counts.added + counts.removed + counts.changed === 0)
|
|
2893
|
+
showData(fresh.sections, fresh.login);
|
|
2894
|
+
else setPending(fresh);
|
|
2895
|
+
} else {
|
|
2896
|
+
setPending(null);
|
|
2897
|
+
}
|
|
2898
|
+
};
|
|
2833
2899
|
const applyOrRefresh = () => {
|
|
2834
2900
|
if (pending) showData(pending.sections, pending.login);
|
|
2835
2901
|
else revalidate(true);
|
|
@@ -2882,7 +2948,18 @@ var App = ({
|
|
|
2882
2948
|
watcher = watch(dir, (_event, changed) => {
|
|
2883
2949
|
if (!live || changed && changed !== name) return;
|
|
2884
2950
|
if (timer) clearTimeout(timer);
|
|
2885
|
-
|
|
2951
|
+
const firedAt = Date.now();
|
|
2952
|
+
const delay = watchDebounceMs + Math.floor(Math.random() * watchJitterMs);
|
|
2953
|
+
timer = setTimeout(() => {
|
|
2954
|
+
if (!live) return;
|
|
2955
|
+
const shared = cacheKey ? readCache(cacheKey) : null;
|
|
2956
|
+
if (shared && shared.at > firedAt) {
|
|
2957
|
+
receive({ sections: shared.sections, login: shared.login });
|
|
2958
|
+
setFetchedAt(shared.at);
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
revalidate();
|
|
2962
|
+
}, delay);
|
|
2886
2963
|
});
|
|
2887
2964
|
} catch {
|
|
2888
2965
|
return;
|
|
@@ -2893,6 +2970,10 @@ var App = ({
|
|
|
2893
2970
|
watcher.close();
|
|
2894
2971
|
};
|
|
2895
2972
|
}, [watchPath, watchDebounceMs]);
|
|
2973
|
+
const [budget, setBudget] = useState(
|
|
2974
|
+
() => cacheKey ? readCache(cacheKey)?.budget ?? null : null
|
|
2975
|
+
);
|
|
2976
|
+
const [skippedForBudget, setSkippedForBudget] = useState(false);
|
|
2896
2977
|
const [mergedUrls, setMergedUrls] = useState([]);
|
|
2897
2978
|
const mergeTimers = useRef([]);
|
|
2898
2979
|
useEffect(() => () => mergeTimers.current.forEach(clearTimeout), []);
|
|
@@ -2973,6 +3054,8 @@ var App = ({
|
|
|
2973
3054
|
fetchedAt,
|
|
2974
3055
|
refreshError: refreshError ?? void 0,
|
|
2975
3056
|
origin,
|
|
3057
|
+
budget,
|
|
3058
|
+
skippedForBudget,
|
|
2976
3059
|
hidden: overlay !== null,
|
|
2977
3060
|
mergedUrls,
|
|
2978
3061
|
transients,
|
|
@@ -3029,4 +3112,4 @@ var matchesFilter = (repo, filter) => {
|
|
|
3029
3112
|
};
|
|
3030
3113
|
var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
|
|
3031
3114
|
|
|
3032
|
-
export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, 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
|
},
|