@kud/gh-ink 0.23.3 → 0.25.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 +14 -4
- package/dist/index.js +87 -31
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -199,8 +199,8 @@ declare const sortItems: (items: GHItem[]) => GHItem[];
|
|
|
199
199
|
declare const sortByRecency: (items: GHItem[]) => GHItem[];
|
|
200
200
|
declare const insertRepoHeaders: (items: GHItem[]) => AnyItem[];
|
|
201
201
|
type Standing = "authored" | "queued" | "spoken";
|
|
202
|
-
declare const whoseMove: (health: Health, sectionId: string, standing?: Standing) => "you" | "them";
|
|
203
|
-
declare const layoutGHItems: (items: GHItem[], sectionId: string) => AnyItem[];
|
|
202
|
+
declare const whoseMove: (health: Health, sectionId: string, standing?: Standing, theySpokeLast?: boolean) => "you" | "them";
|
|
203
|
+
declare const layoutGHItems: (items: GHItem[], sectionId: string, login?: string) => AnyItem[];
|
|
204
204
|
/**
|
|
205
205
|
* A two-way repo split, when a host has one. `undefined` — the ordinary case —
|
|
206
206
|
* means no split at all: every row stands.
|
|
@@ -270,6 +270,7 @@ declare const runHere: (cmd: string) => void;
|
|
|
270
270
|
declare const COLS: number;
|
|
271
271
|
declare const topLevelCount: (s: Section) => number;
|
|
272
272
|
declare const drillCmd: (item: AnyItem) => string | null;
|
|
273
|
+
declare const explainGhAction: (e: unknown) => string;
|
|
273
274
|
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
275
|
extensions?: InboxExtension[];
|
|
275
276
|
onOpenExt?: (id: string, target: ExtensionTarget) => void;
|
|
@@ -314,7 +315,7 @@ declare const ActionMenu: ({ item, actions, cursor, }: {
|
|
|
314
315
|
cursor: number;
|
|
315
316
|
}) => React.JSX.Element;
|
|
316
317
|
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, }: {
|
|
318
|
+
declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jiraKeyRe, jiraTransitions, hasCiStatus, ciJob, ciFetcher, ciPollMs, watchPath, watchDebounceMs, watchJitterMs, extensions, tabHelp, emptyHint, }: {
|
|
318
319
|
fetcher: () => Promise<{
|
|
319
320
|
sections: Section[];
|
|
320
321
|
login: string;
|
|
@@ -342,6 +343,15 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jir
|
|
|
342
343
|
watchPath?: string;
|
|
343
344
|
/** Bursts of writes to collapse into one refetch. */
|
|
344
345
|
watchDebounceMs?: number;
|
|
346
|
+
/**
|
|
347
|
+
* Random spread added to `watchDebounceMs`, so several cockpits woken by one
|
|
348
|
+
* signal take turns instead of stampeding. The first to wake fetches and
|
|
349
|
+
* writes the shared cache; the others adopt it and pay nothing.
|
|
350
|
+
*
|
|
351
|
+
* Defaults to 0 — no randomness unless a host asks for it. Set it to comfortably
|
|
352
|
+
* more than one round trip when you expect several instances to be open.
|
|
353
|
+
*/
|
|
354
|
+
watchJitterMs?: number;
|
|
345
355
|
extensions?: InboxExtension[];
|
|
346
356
|
}) => React.JSX.Element;
|
|
347
357
|
|
|
@@ -443,4 +453,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
|
|
|
443
453
|
*/
|
|
444
454
|
declare const parsePatterns: (value: string) => string[];
|
|
445
455
|
|
|
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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -919,18 +919,38 @@ var YOURS = {
|
|
|
919
919
|
queued: ["waiting", "pending", "threads", "approved"],
|
|
920
920
|
spoken: ["threads"]
|
|
921
921
|
};
|
|
922
|
-
var whoseMove = (health, sectionId, standing) =>
|
|
922
|
+
var whoseMove = (health, sectionId, standing, theySpokeLast) => (
|
|
923
|
+
// Somebody else having the last word outranks every review state below. It is
|
|
924
|
+
// the plainest claim on you there is — a question asked, an objection raised,
|
|
925
|
+
// a "can you rebase" — and none of it shows up as a health, because a bare
|
|
926
|
+
// comment approves nothing, fails nothing and opens no thread.
|
|
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
|
+
);
|
|
923
938
|
var BAND_LABEL = {
|
|
924
939
|
you: "Your move",
|
|
925
940
|
them: "Their move"
|
|
926
941
|
};
|
|
927
|
-
var layoutGHItems = (items, sectionId) => {
|
|
942
|
+
var layoutGHItems = (items, sectionId, login) => {
|
|
928
943
|
if (sectionId === "done") return insertRepoHeaders(sortByRecency(items));
|
|
929
944
|
const sorted = sortItems(items);
|
|
930
945
|
const bands = ["you", "them"].map((side) => ({
|
|
931
946
|
side,
|
|
932
947
|
rows: sorted.filter(
|
|
933
|
-
(i) => whoseMove(
|
|
948
|
+
(i) => whoseMove(
|
|
949
|
+
i.health,
|
|
950
|
+
sectionId,
|
|
951
|
+
i.standing,
|
|
952
|
+
!!login && !!i.lastActor && i.lastActor !== login
|
|
953
|
+
) === side
|
|
934
954
|
)
|
|
935
955
|
}));
|
|
936
956
|
const filled = bands.filter((b) => b.rows.length > 0);
|
|
@@ -1297,9 +1317,22 @@ var UNSUBSCRIBE_MUTATION = `
|
|
|
1297
1317
|
}
|
|
1298
1318
|
}
|
|
1299
1319
|
`;
|
|
1320
|
+
var GH_ACTION_FAILURES = [
|
|
1321
|
+
[/rate limit|RATE_LIMIT/i, "GitHub rate limit spent \u2014 it resets within the hour"],
|
|
1322
|
+
[/\b401\b|not logged in|authentication/i, "gh is not authenticated"],
|
|
1323
|
+
[/\b403\b|must have admin|not authorized|permission/i, "not permitted on this repo"],
|
|
1324
|
+
[/\b50[0234]\b/, "GitHub's API is failing (5xx)"],
|
|
1325
|
+
[/ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNREFUSED/i, "no route to GitHub"]
|
|
1326
|
+
];
|
|
1327
|
+
var explainGhAction = (e) => {
|
|
1328
|
+
const err = e;
|
|
1329
|
+
const raw = (err.stderr ?? "").trim().split("\n").filter(Boolean).pop() ?? err.message ?? "no reason reported";
|
|
1330
|
+
const human = GH_ACTION_FAILURES.find(([re]) => re.test(raw))?.[1];
|
|
1331
|
+
return human ? `${human} (${raw})` : raw;
|
|
1332
|
+
};
|
|
1300
1333
|
var unsubscribeFrom = async (item) => {
|
|
1301
|
-
const
|
|
1302
|
-
const found = await quietly`gh
|
|
1334
|
+
const route = item.kind === "pr" ? "pulls" : "issues";
|
|
1335
|
+
const found = await quietly`gh api repos/${item.repo}/${route}/${item.number} --jq .node_id`;
|
|
1303
1336
|
const id = found.stdout.trim();
|
|
1304
1337
|
if (!id) throw new Error(`no node id for ${item.repo}#${item.number}`);
|
|
1305
1338
|
await quietly`gh api graphql -f query=${UNSUBSCRIBE_MUTATION} -f id=${id}`;
|
|
@@ -1399,8 +1432,8 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
|
|
|
1399
1432
|
void quietly`gh pr edit ${item.number} --repo ${item.repo} --remove-reviewer ${login}`.then(() => {
|
|
1400
1433
|
showFlash(`\u2713 Removed you as reviewer on #${item.number}`);
|
|
1401
1434
|
ext?.onActed?.();
|
|
1402
|
-
}).catch(() => {
|
|
1403
|
-
showFlash(`\u2717
|
|
1435
|
+
}).catch((e) => {
|
|
1436
|
+
showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`);
|
|
1404
1437
|
onRefresh?.();
|
|
1405
1438
|
});
|
|
1406
1439
|
}
|
|
@@ -1411,7 +1444,9 @@ var buildActions = (item, login, showFlash, jiraBase, jiraKeyRe, jiraTransitions
|
|
|
1411
1444
|
hint: "u",
|
|
1412
1445
|
run: () => {
|
|
1413
1446
|
showFlash(`\u22EF Unsubscribing from #${item.number}\u2026`);
|
|
1414
|
-
void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch(
|
|
1447
|
+
void unsubscribeFrom(item).then(() => showFlash(`\u2713 Unsubscribed from #${item.number}`)).catch(
|
|
1448
|
+
(e) => showFlash(`\u2717 #${item.number}: ${explainGhAction(e)}`)
|
|
1449
|
+
);
|
|
1415
1450
|
}
|
|
1416
1451
|
});
|
|
1417
1452
|
if (item.kind === "pr" && item.branch) {
|
|
@@ -2446,7 +2481,9 @@ var BrowseScreen = ({
|
|
|
2446
2481
|
}
|
|
2447
2482
|
if (input === "u" && (activeItem.kind === "pr" || activeItem.kind === "issue")) {
|
|
2448
2483
|
showFlash(`\u22EF Unsubscribing from #${activeItem.number}\u2026`);
|
|
2449
|
-
void unsubscribeFrom(activeItem).then(() => showFlash(`\u2713 Unsubscribed from #${activeItem.number}`)).catch(
|
|
2484
|
+
void unsubscribeFrom(activeItem).then(() => showFlash(`\u2713 Unsubscribed from #${activeItem.number}`)).catch(
|
|
2485
|
+
(e) => showFlash(`\u2717 #${activeItem.number}: ${explainGhAction(e)}`)
|
|
2486
|
+
);
|
|
2450
2487
|
return;
|
|
2451
2488
|
}
|
|
2452
2489
|
if (input === "x" && activeItem.kind === "pr" && activeItem.standing === "queued" && login) {
|
|
@@ -2455,8 +2492,8 @@ var BrowseScreen = ({
|
|
|
2455
2492
|
void quietly`gh pr edit ${activeItem.number} --repo ${activeItem.repo} --remove-reviewer ${login}`.then(() => {
|
|
2456
2493
|
showFlash(`\u2713 Removed you as reviewer on #${activeItem.number}`);
|
|
2457
2494
|
onActed?.();
|
|
2458
|
-
}).catch(() => {
|
|
2459
|
-
showFlash(`\u2717
|
|
2495
|
+
}).catch((e) => {
|
|
2496
|
+
showFlash(`\u2717 #${activeItem.number}: ${explainGhAction(e)}`);
|
|
2460
2497
|
onRefresh?.();
|
|
2461
2498
|
});
|
|
2462
2499
|
return;
|
|
@@ -2685,6 +2722,11 @@ var App = ({
|
|
|
2685
2722
|
ciPollMs = 6e4,
|
|
2686
2723
|
watchPath,
|
|
2687
2724
|
watchDebounceMs = 400,
|
|
2725
|
+
// Zero by default, deliberately. A package that adds randomness to its own
|
|
2726
|
+
// timing makes every host's tests flaky — this one's went green locally on a
|
|
2727
|
+
// lucky draw and red in CI. It is also a host concern: only somebody running
|
|
2728
|
+
// several cockpits at once needs the stagger, and only they know how many.
|
|
2729
|
+
watchJitterMs = 0,
|
|
2688
2730
|
extensions,
|
|
2689
2731
|
tabHelp,
|
|
2690
2732
|
emptyHint
|
|
@@ -2782,24 +2824,7 @@ var App = ({
|
|
|
2782
2824
|
setFetchedAt(Date.now());
|
|
2783
2825
|
setRefreshError(null);
|
|
2784
2826
|
if (hasCiStatus) applyCiStatus(fresh.ciStatus ?? null);
|
|
2785
|
-
|
|
2786
|
-
if (!displayedKey.current) {
|
|
2787
|
-
if (fresh.sections.length === 0) {
|
|
2788
|
-
setState({ phase: "empty" });
|
|
2789
|
-
return;
|
|
2790
|
-
}
|
|
2791
|
-
showData(fresh.sections, fresh.login);
|
|
2792
|
-
} else if (freshKey !== displayedKey.current) {
|
|
2793
|
-
const { counts } = diffSections(
|
|
2794
|
-
displayedSections.current,
|
|
2795
|
-
fresh.sections
|
|
2796
|
-
);
|
|
2797
|
-
if (counts.added + counts.removed + counts.changed === 0)
|
|
2798
|
-
showData(fresh.sections, fresh.login);
|
|
2799
|
-
else setPending(fresh);
|
|
2800
|
-
} else {
|
|
2801
|
-
setPending(null);
|
|
2802
|
-
}
|
|
2827
|
+
receive(fresh);
|
|
2803
2828
|
}).catch((err) => {
|
|
2804
2829
|
setRefreshing(false);
|
|
2805
2830
|
const message = err.message;
|
|
@@ -2810,6 +2835,26 @@ var App = ({
|
|
|
2810
2835
|
setRefreshError({ message, at: Date.now() });
|
|
2811
2836
|
});
|
|
2812
2837
|
};
|
|
2838
|
+
const receive = (fresh) => {
|
|
2839
|
+
const freshKey = signatureOf(fresh.sections);
|
|
2840
|
+
if (!displayedKey.current) {
|
|
2841
|
+
if (fresh.sections.length === 0) {
|
|
2842
|
+
setState({ phase: "empty" });
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
showData(fresh.sections, fresh.login);
|
|
2846
|
+
} else if (freshKey !== displayedKey.current) {
|
|
2847
|
+
const { counts } = diffSections(
|
|
2848
|
+
displayedSections.current,
|
|
2849
|
+
fresh.sections
|
|
2850
|
+
);
|
|
2851
|
+
if (counts.added + counts.removed + counts.changed === 0)
|
|
2852
|
+
showData(fresh.sections, fresh.login);
|
|
2853
|
+
else setPending(fresh);
|
|
2854
|
+
} else {
|
|
2855
|
+
setPending(null);
|
|
2856
|
+
}
|
|
2857
|
+
};
|
|
2813
2858
|
const applyOrRefresh = () => {
|
|
2814
2859
|
if (pending) showData(pending.sections, pending.login);
|
|
2815
2860
|
else revalidate(true);
|
|
@@ -2862,7 +2907,18 @@ var App = ({
|
|
|
2862
2907
|
watcher = watch(dir, (_event, changed) => {
|
|
2863
2908
|
if (!live || changed && changed !== name) return;
|
|
2864
2909
|
if (timer) clearTimeout(timer);
|
|
2865
|
-
|
|
2910
|
+
const firedAt = Date.now();
|
|
2911
|
+
const delay = watchDebounceMs + Math.floor(Math.random() * watchJitterMs);
|
|
2912
|
+
timer = setTimeout(() => {
|
|
2913
|
+
if (!live) return;
|
|
2914
|
+
const shared = cacheKey ? readCache(cacheKey) : null;
|
|
2915
|
+
if (shared && shared.at > firedAt) {
|
|
2916
|
+
receive({ sections: shared.sections, login: shared.login });
|
|
2917
|
+
setFetchedAt(shared.at);
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2920
|
+
revalidate();
|
|
2921
|
+
}, delay);
|
|
2866
2922
|
});
|
|
2867
2923
|
} catch {
|
|
2868
2924
|
return;
|
|
@@ -3009,4 +3065,4 @@ var matchesFilter = (repo, filter) => {
|
|
|
3009
3065
|
};
|
|
3010
3066
|
var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
|
|
3011
3067
|
|
|
3012
|
-
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 };
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kud/gh-ink",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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",
|