@kud/gh-ink 0.70.0 → 0.73.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 CHANGED
@@ -468,6 +468,16 @@ interface InboxExtension {
468
468
  drills?: AnyItem["kind"][];
469
469
  body: (onExit: () => void, target?: ExtensionTarget) => ReactNode;
470
470
  commands?: (target: ExtensionTarget) => Command[];
471
+ resolve?: (input: string) => (() => Promise<AnyItem>) | null;
472
+ }
473
+ /**
474
+ * How a `resolve` thunk says "nothing by that name": the launcher draws its
475
+ * message muted, the way it draws a query that matched nothing, and keeps the
476
+ * input open. Any other rejection is a failure and is drawn in the error tone —
477
+ * the difference a reader needs between "you typed it wrong" and "try again".
478
+ */
479
+ declare class LookupMiss extends Error {
480
+ constructor(message: string);
471
481
  }
472
482
 
473
483
  /**
@@ -594,10 +604,31 @@ type JiraTransition = {
594
604
  type JiraAvailableTransition = {
595
605
  id: string;
596
606
  name: string;
607
+ /**
608
+ * The status the transition lands on. `category` is Jira's status category
609
+ * (`new`, `indeterminate`, `done`) where the host has it — the one field a
610
+ * `tabForStatus` can map without knowing every workflow's status names.
611
+ */
597
612
  to: {
598
613
  name: string;
614
+ category?: string;
599
615
  };
600
616
  };
617
+ /**
618
+ * Which tab a ticket belongs in once it lands on `to`, as the host lays its
619
+ * tabs out — the section id, or null when the ticket leaves the board or the
620
+ * host cannot say. Only the host knows: one board files by status category,
621
+ * another by a status-to-column table, and the inbox has neither.
622
+ *
623
+ * Supplying it makes a move optimistic: the row goes to that tab the moment
624
+ * the move is chosen instead of at the next fetch. Null keeps the move
625
+ * pessimistic, exactly as it is without the prop — a row guessed off the board
626
+ * that turns out to still be on it is worse than one that waits for the fetch.
627
+ */
628
+ type TabForStatus = (to: {
629
+ name: string;
630
+ category?: string;
631
+ }) => string | null;
601
632
  /**
602
633
  * Resolve the live transitions for a ticket, called lazily when its move
603
634
  * submenu opens — never at board fetch, where one call per visible ticket
@@ -800,7 +831,7 @@ declare const ActionMenu: ({ item, actions, cursor, cols, }: {
800
831
  cols?: number;
801
832
  }) => React.JSX.Element;
802
833
  declare const signatureOf: (sections: Section[]) => string;
803
- declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jiraKeyRe, jiraSetupHint, jiraTransitions, jiraTransitionsFor, hasCiStatus, ciJob, ciFetcher, ciPollMs, stripFetcher, stripLabel, stripPollMs, watchPath, watchDebounceMs, watchJitterMs, extensions, tabHelp, emptyHint, liveLabel, onSettled, }: {
834
+ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jiraKeyRe, jiraSetupHint, jiraTransitions, jiraTransitionsFor, tabForStatus, hasCiStatus, ciJob, ciFetcher, ciPollMs, stripFetcher, stripLabel, stripPollMs, watchPath, watchDebounceMs, watchJitterMs, extensions, tabHelp, emptyHint, liveLabel, onSettled, }: {
804
835
  fetcher: () => Promise<{
805
836
  sections: Section[];
806
837
  login: string;
@@ -841,6 +872,12 @@ declare const App: ({ fetcher, cacheKey, title, detailFor, origin, jiraBase, jir
841
872
  jiraSetupHint?: string;
842
873
  jiraTransitions?: JiraTransition[];
843
874
  jiraTransitionsFor?: JiraTransitionsFor;
875
+ /**
876
+ * Which tab a moved ticket lands in — see TabForStatus. Supplying it makes
877
+ * moves through `jiraTransitionsFor` optimistic; leaving it out keeps every
878
+ * move waiting on the next fetch, as before.
879
+ */
880
+ tabForStatus?: TabForStatus;
844
881
  hasCiStatus?: boolean;
845
882
  ciJob?: string;
846
883
  ciFetcher?: () => Promise<CiStatus | null>;
@@ -909,4 +946,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
909
946
  */
910
947
  declare const parsePatterns: (value: string) => string[];
911
948
 
912
- export { type Action, type ActionGroup, ActionMenu, App, COLS, type ChromeHints, type ChromeSpec, type ChromeState, CiStatusLine, type CiStatusState, type Command, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, HealthPanel, type HealthPanelProps, type InboxBudget, type InboxExtension, type JiraAvailableTransition, type JiraTransition, type JiraTransitionsFor, type LiveLabel, MIN_SIDEBAR_COLS, type MenuGap, type MenuRow, PrRow, type PrRowProps, type Rails, type RepoFilter, type RowAnnouncement, type RowMotion, SIDEBAR_COLS, type SettledResult, SidePanel, type Sidebar, type SidebarRow, type StatusStrip, StatusStripLine, type StripItem, type StripState, type StripStatusState, type TrailingColumns, type TrailingShed, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, isMenuGap, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, menuRowsOf, menuTone, moveCursor, numberColumnsOf, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, useChrome, windowCount, withHeaders, withoutItem, workCount, writeCache };
949
+ export { type Action, type ActionGroup, ActionMenu, App, COLS, type ChromeHints, type ChromeSpec, type ChromeState, CiStatusLine, type CiStatusState, type Command, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, HealthPanel, type HealthPanelProps, type InboxBudget, type InboxExtension, type JiraAvailableTransition, type JiraTransition, type JiraTransitionsFor, type LiveLabel, LookupMiss, MIN_SIDEBAR_COLS, type MenuGap, type MenuRow, PrRow, type PrRowProps, type Rails, type RepoFilter, type RowAnnouncement, type RowMotion, SIDEBAR_COLS, type SettledResult, SidePanel, type Sidebar, type SidebarRow, type StatusStrip, StatusStripLine, type StripItem, type StripState, type StripStatusState, type TabForStatus, type TrailingColumns, type TrailingShed, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, isMenuGap, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, menuRowsOf, menuTone, moveCursor, numberColumnsOf, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, useChrome, windowCount, withHeaders, withoutItem, workCount, writeCache };
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import React2, { createContext, useState, useRef, useContext, useEffect, useCallback, useMemo } from 'react';
2
2
  import { useWindowSize, useInput, Text, Box } from 'ink';
3
3
  import { colors, ScrollView, TextInput, Pill, getIconMode, FooterHints, useAppKeys, LoadingScreen, StatusMessage, useFilterMode, useListCursor, CommandPalette, Tabs, ProgressBar, pillWidth } from '@kud/ink-ui';
4
- import { isPassCheck, isFailCheck, isInconclusiveCheck, resolveThread, unresolveThread, replyToThread, rerunFailedRun, mergePr, reRequestReviewer } from '@kud/gh';
4
+ import { isPassCheck, isFailCheck, isInconclusiveCheck, resolveThread, unresolveThread, replyToThread, rerunFailedRun, mergePr, reRequestReviewer, parseRef, formatRef, fetchItemNode } from '@kud/gh';
5
5
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
- import { PIN_MARK, sizeOf, sizePartsOf, depthOf, impliedLabels, labelPriority, checkoutDirs, profileOf, inboxConfig, relativeTime, filterBySearch, filterByRepos, filterByOrigin } from '@kud/gh-workflow';
6
+ import { PIN_MARK, sizeOf, sizePartsOf, depthOf, impliedLabels, labelPriority, checkoutDirs, profileOf, inboxConfig, relativeTime, filterBySearch, filterByRepos, filterByOrigin, refCandidates, resolveRef } from '@kud/gh-workflow';
7
7
  export { NEEDS_YOU_SECTION, PIN_MARK, bandOf, checkoutDirs, configureInbox, filterByOrigin, filterByRepos, filterBySearch, impliedLabels, inboxConfig, insertRepoHeaders, labelPriority, layoutGHItems, profileOf, relativeTime, repoPriority, resetInboxConfig, sortByRecency, sortItems, whoseMove } from '@kud/gh-workflow';
8
8
  import { $ } from 'zx';
9
9
  import { spawn } from 'child_process';
@@ -1180,7 +1180,7 @@ var searchBarState = ({
1180
1180
  glyph: searchGlyph(nerd),
1181
1181
  glyphColor: typing ? colors.info : colors.muted,
1182
1182
  caret: typing,
1183
- hints: typing ? "\u2191\u2193 move \xB7 \u21B5 keep \xB7 esc clear" : "filtered \xB7 keys active \xB7 esc clear"
1183
+ hints: typing ? "\u2191\u2193 move \xB7 \u21B5/esc done \xB7 \u2303u clear" : "filtered \xB7 keys active \xB7 esc clear"
1184
1184
  });
1185
1185
  var InboxChromeContext = createContext(null);
1186
1186
  var useChrome = (spec) => {
@@ -1194,6 +1194,189 @@ var useChrome = (spec) => {
1194
1194
  }, [setChrome, scope, hintsKey]);
1195
1195
  return chrome !== null;
1196
1196
  };
1197
+ var LookupMiss = class extends Error {
1198
+ constructor(message) {
1199
+ super(message);
1200
+ this.name = "LookupMiss";
1201
+ }
1202
+ };
1203
+ var isGH = (item) => item.kind === "pr" || item.kind === "issue";
1204
+ var listedAt = (sections, repo, number) => {
1205
+ const want = repo.toLowerCase();
1206
+ for (const section of sections)
1207
+ for (const item of section.items)
1208
+ if (isGH(item) && item.number === number && item.repo.toLowerCase() === want)
1209
+ return item;
1210
+ return void 0;
1211
+ };
1212
+ var listedCopy = (sections, item) => {
1213
+ if (isGH(item)) return listedAt(sections, item.repo, item.number);
1214
+ const url = "url" in item ? item.url : void 0;
1215
+ if (!url) return void 0;
1216
+ for (const section of sections)
1217
+ for (const row of section.items)
1218
+ if (row.kind === item.kind && "url" in row && row.url === url) return row;
1219
+ return void 0;
1220
+ };
1221
+ var settle = (sections, item) => listedCopy(sections, item) ?? (isGH(item) ? { ...item, notInInbox: true } : item);
1222
+ var githubClaim = (query, { activeRepo, repos, sections }) => {
1223
+ const ref = parseRef(query);
1224
+ if (!ref) return null;
1225
+ const near = ref.repo ? listedAt(sections, ref.repo, ref.number) : activeRepo ? listedAt(sections, activeRepo, ref.number) : void 0;
1226
+ const label = near ? `${near.repo}#${near.number}` : formatRef(ref);
1227
+ const repo = ref.repo ?? refCandidates(activeRepo, repos)[0];
1228
+ const text = query.trim();
1229
+ const browserUrl = near?.url ?? (/^https?:/i.test(text) ? text : repo ? `https://github.com/${repo}/issues/${ref.number}` : void 0);
1230
+ return {
1231
+ label,
1232
+ browser: browserUrl ? {
1233
+ label: near || !repo ? label : `${repo}#${ref.number}`,
1234
+ url: browserUrl
1235
+ } : void 0,
1236
+ fetch: async () => {
1237
+ if (near) return near;
1238
+ const item = await resolveRef(text, {
1239
+ fetchNode: (r, n) => fetchItemNode(r, n),
1240
+ activeRepo,
1241
+ repos
1242
+ });
1243
+ if (!item) throw new LookupMiss(`nothing matches "${label}"`);
1244
+ return item;
1245
+ }
1246
+ };
1247
+ };
1248
+ var claimFor = (query, context, extensions = []) => {
1249
+ if (!query.trim()) return null;
1250
+ const github = githubClaim(query, context);
1251
+ if (github) return github;
1252
+ for (const ext of extensions) {
1253
+ const fetch = ext.resolve?.(query);
1254
+ if (fetch) return { label: query.trim(), fetch };
1255
+ }
1256
+ return null;
1257
+ };
1258
+ var reasonOf = (error) => {
1259
+ const e = error;
1260
+ const text = typeof e?.stderr === "string" && e.stderr.trim() || typeof e?.message === "string" && e.message || String(error);
1261
+ const line = text.split("\n").find((l) => l.trim()) ?? text;
1262
+ return line.replace(/^gh:\s*/, "").trim();
1263
+ };
1264
+ var useLauncherLookup = (palette, open) => {
1265
+ const [lookup, setLookup] = useState(null);
1266
+ const seq = useRef(0);
1267
+ const latest = useRef({ palette, open });
1268
+ latest.current = { palette, open };
1269
+ const start = (query, claim) => {
1270
+ const id = ++seq.current;
1271
+ const live = () => id === seq.current && latest.current.palette === query;
1272
+ setLookup({ query, phase: "loading", label: claim.label });
1273
+ claim.fetch().then(
1274
+ (item) => {
1275
+ if (!live()) return;
1276
+ setLookup(null);
1277
+ if (!latest.current.open(item))
1278
+ setLookup({
1279
+ query,
1280
+ phase: "failed",
1281
+ label: claim.label,
1282
+ message: `nothing here can open ${claim.label}`
1283
+ });
1284
+ },
1285
+ (error) => {
1286
+ if (!live()) return;
1287
+ setLookup(
1288
+ error instanceof LookupMiss ? {
1289
+ query,
1290
+ phase: "missed",
1291
+ label: claim.label,
1292
+ message: error.message
1293
+ } : {
1294
+ query,
1295
+ phase: "failed",
1296
+ label: claim.label,
1297
+ message: reasonOf(error)
1298
+ }
1299
+ );
1300
+ }
1301
+ );
1302
+ };
1303
+ const reset = () => {
1304
+ seq.current++;
1305
+ setLookup(null);
1306
+ };
1307
+ return {
1308
+ lookup: lookup && lookup.query === palette ? lookup : null,
1309
+ start,
1310
+ reset
1311
+ };
1312
+ };
1313
+ var lookupRows = (claim, lookup, query, start, openUrl) => {
1314
+ const ref = (s) => /* @__PURE__ */ jsx(Text3, { color: colors.accent, children: s });
1315
+ if (lookup?.phase === "loading")
1316
+ return {
1317
+ exclusive: true,
1318
+ rows: [
1319
+ {
1320
+ id: "launcher:lookup",
1321
+ title: `Looking up ${claim.label}\u2026`,
1322
+ label: /* @__PURE__ */ jsxs(Text3, { dimColor: true, children: [
1323
+ "Looking up ",
1324
+ ref(claim.label),
1325
+ "\u2026"
1326
+ ] }),
1327
+ run: () => {
1328
+ },
1329
+ keepOpen: true
1330
+ }
1331
+ ]
1332
+ };
1333
+ if (lookup?.phase === "missed")
1334
+ return { exclusive: true, rows: [], message: lookup.message };
1335
+ if (lookup?.phase === "failed")
1336
+ return {
1337
+ exclusive: true,
1338
+ rows: [
1339
+ {
1340
+ id: "launcher:retry",
1341
+ title: `Couldn't look up ${claim.label}: ${lookup.message}`,
1342
+ label: /* @__PURE__ */ jsxs(Text3, { color: colors.error, children: [
1343
+ "\u2717 couldn't look up ",
1344
+ claim.label,
1345
+ " \xB7 ",
1346
+ lookup.message
1347
+ ] }),
1348
+ run: () => start(query, claim),
1349
+ keepOpen: true
1350
+ }
1351
+ ]
1352
+ };
1353
+ const rows = [
1354
+ {
1355
+ id: "launcher:open",
1356
+ title: `\u21B5 opens ${claim.label}`,
1357
+ label: /* @__PURE__ */ jsxs(Text3, { children: [
1358
+ "\u21B5 opens ",
1359
+ ref(claim.label)
1360
+ ] }),
1361
+ run: () => start(query, claim),
1362
+ keepOpen: true
1363
+ }
1364
+ ];
1365
+ const browser = claim.browser;
1366
+ if (browser)
1367
+ rows.push({
1368
+ id: "launcher:browser",
1369
+ title: `Open ${browser.label} in browser`,
1370
+ label: /* @__PURE__ */ jsxs(Text3, { children: [
1371
+ "Open ",
1372
+ ref(browser.label),
1373
+ " ",
1374
+ /* @__PURE__ */ jsx(Text3, { color: colors.info, children: "in browser" })
1375
+ ] }),
1376
+ run: () => openUrl(browser.url, browser.label)
1377
+ });
1378
+ return { exclusive: false, rows };
1379
+ };
1197
1380
 
1198
1381
  // src/inbox/cache.ts
1199
1382
  var cacheTtlMs = () => inboxConfig().cacheTtlMs;
@@ -1285,11 +1468,11 @@ var indexRows = (sections) => {
1285
1468
  };
1286
1469
  var rowsOf = (index) => {
1287
1470
  const out = /* @__PURE__ */ new Map();
1288
- for (const [mark, placed] of index) {
1471
+ for (const [mark, placed2] of index) {
1289
1472
  const key = rowKeyOfMark(mark);
1290
1473
  const seen = out.get(key);
1291
- if (seen) seen.add(placed.sectionId);
1292
- else out.set(key, /* @__PURE__ */ new Set([placed.sectionId]));
1474
+ if (seen) seen.add(placed2.sectionId);
1475
+ else out.set(key, /* @__PURE__ */ new Set([placed2.sectionId]));
1293
1476
  }
1294
1477
  return out;
1295
1478
  };
@@ -1444,6 +1627,73 @@ var reconcile = (prev, fresh, now, holdMs = HOLD_MS) => {
1444
1627
  }
1445
1628
  return out;
1446
1629
  };
1630
+
1631
+ // src/inbox/overrides.ts
1632
+ var OVERRIDE_HOLD_MS = 3e4;
1633
+ var withFields = (item, fields) => fields ? { ...item, ...fields } : item;
1634
+ var placed = (item, fields) => {
1635
+ const row = withFields(item, fields);
1636
+ return { ...row, depth: 0, indent: false };
1637
+ };
1638
+ var applyOne = (sections, key, patch) => {
1639
+ const found = sections.flatMap((s) => s.items).find((item) => keyOf(item) === key);
1640
+ if (!found) return sections;
1641
+ if (patch.kind === "mutate")
1642
+ return sections.map((s) => ({
1643
+ ...s,
1644
+ items: s.items.map(
1645
+ (item) => keyOf(item) === key ? withFields(item, patch.fields) : item
1646
+ )
1647
+ }));
1648
+ const without = sections.map((s) => ({
1649
+ ...s,
1650
+ items: s.items.filter((item) => keyOf(item) !== key)
1651
+ }));
1652
+ if (patch.kind === "remove") return without;
1653
+ if (!without.some((s) => s.id === patch.toSection))
1654
+ return sections.map((s) => ({
1655
+ ...s,
1656
+ items: s.items.map(
1657
+ (item) => keyOf(item) === key ? withFields(item, patch.fields) : item
1658
+ )
1659
+ }));
1660
+ return without.map(
1661
+ (s) => s.id === patch.toSection ? { ...s, items: [...s.items, placed(found, patch.fields)] } : s
1662
+ );
1663
+ };
1664
+ var applyOverrides = (sections, overrides) => [...overrides].reduce(
1665
+ (out, [key, override]) => applyOne(out, key, override.patch),
1666
+ sections
1667
+ );
1668
+ var fieldsMatch = (item, fields) => !fields || Object.entries(fields).every(
1669
+ ([name, value]) => item[name] === value
1670
+ );
1671
+ var agrees = (sections, key, patch) => {
1672
+ const at = sections.flatMap(
1673
+ (s) => s.items.filter((item) => keyOf(item) === key).map((item) => ({ s, item }))
1674
+ );
1675
+ if (patch.kind === "remove") return at.length === 0;
1676
+ if (at.length === 0) return false;
1677
+ if (patch.kind === "mutate")
1678
+ return at.every(({ item }) => fieldsMatch(item, patch.fields));
1679
+ return at.some(
1680
+ ({ s, item }) => s.id === patch.toSection && fieldsMatch(item, patch.fields)
1681
+ );
1682
+ };
1683
+ var pruneOverrides = (overrides, fresh, now, holdMs = OVERRIDE_HOLD_MS) => new Map(
1684
+ [...overrides].filter(([key, override]) => {
1685
+ if (override.phase !== "sent") return true;
1686
+ if (now - override.since > holdMs) return false;
1687
+ return !agrees(fresh, key, override.patch);
1688
+ })
1689
+ );
1690
+ var failedOverrides = (overrides) => [...overrides].filter(([, o]) => o.phase === "failed").sort(([, a], [, b]) => (b.failure?.seq ?? 0) - (a.failure?.seq ?? 0));
1691
+ var failureNotice = (failed) => {
1692
+ const newest = failed[0]?.[1];
1693
+ if (!newest) return null;
1694
+ const more = failed.length > 1 ? ` +${failed.length - 1} more` : "";
1695
+ return `\u2717 Couldn't ${newest.action} ${newest.label}: ${newest.failure?.reason ?? "no reason reported"}${more} w restore`;
1696
+ };
1447
1697
  var quietly = $({ quiet: true });
1448
1698
  var isMenuGap = (row) => "gap" in row;
1449
1699
  var jiraMovesCache = /* @__PURE__ */ new Map();
@@ -1463,43 +1713,56 @@ var orderJiraMoves = (available, configured) => {
1463
1713
  if (!live) continue;
1464
1714
  known.add(live.name);
1465
1715
  ordered.push(
1466
- resolutions && resolutions.length > 0 ? { label, transition: live.name, resolutions } : { label, transition: live.name }
1716
+ resolutions && resolutions.length > 0 ? { label, transition: live.name, resolutions, to: live.to } : { label, transition: live.name, to: live.to }
1467
1717
  );
1468
1718
  }
1469
- const rest = available.filter((t) => !known.has(t.name)).sort((a, b) => a.name.localeCompare(b.name)).map((t) => ({ label: t.name, transition: t.name }));
1719
+ const rest = available.filter((t) => !known.has(t.name)).sort((a, b) => a.name.localeCompare(b.name)).map((t) => ({ label: t.name, transition: t.name, to: t.to }));
1470
1720
  return [...ordered, ...rest];
1471
1721
  };
1472
- var jiraMoveActions = (ticket, moves, showFlash, onActed) => moves.map(
1473
- ({ label, transition, resolutions }) => resolutions && resolutions.length > 0 ? {
1474
- label,
1475
- hint: "",
1476
- run: () => {
1477
- },
1478
- subActions: resolutions.map((resolution) => ({
1479
- label: resolution,
1722
+ var jiraMoveActions = (ticket, moves, showFlash, onActed, optimistic) => {
1723
+ const execute = (to, request, done, failed) => {
1724
+ const held = to ? optimistic?.(to) : void 0;
1725
+ void request().then(() => {
1726
+ invalidateJiraMoves(ticket);
1727
+ held?.settled();
1728
+ showFlash(done);
1729
+ onActed?.();
1730
+ }).catch((err) => held ? held.failed(err) : showFlash(failed));
1731
+ };
1732
+ return moves.map(
1733
+ ({ label, transition, resolutions, to }) => resolutions && resolutions.length > 0 ? {
1734
+ label,
1480
1735
  hint: "",
1481
1736
  run: () => {
1482
- showFlash(`\u22EF ${label} \xB7 ${resolution}\u2026`);
1483
- void quietly`jira issue move ${ticket} ${transition} --resolution ${resolution}`.then(() => {
1484
- invalidateJiraMoves(ticket);
1485
- showFlash(`\u2713 ${label} \xB7 ${resolution}`);
1486
- onActed?.();
1487
- }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
1737
+ },
1738
+ subActions: resolutions.map((resolution) => ({
1739
+ label: resolution,
1740
+ hint: "",
1741
+ run: () => {
1742
+ showFlash(`\u22EF ${label} \xB7 ${resolution}\u2026`);
1743
+ execute(
1744
+ to,
1745
+ () => quietly`jira issue move ${ticket} ${transition} --resolution ${resolution}`,
1746
+ `\u2713 ${label} \xB7 ${resolution}`,
1747
+ `\u2717 Move to ${label} failed`
1748
+ );
1749
+ }
1750
+ }))
1751
+ } : {
1752
+ label,
1753
+ hint: "",
1754
+ run: () => {
1755
+ showFlash(`\u22EF Moving to ${label}\u2026`);
1756
+ execute(
1757
+ to,
1758
+ () => quietly`jira issue move ${ticket} ${transition}`,
1759
+ `\u2713 Moved to ${label}`,
1760
+ `\u2717 Move to ${label} failed`
1761
+ );
1488
1762
  }
1489
- }))
1490
- } : {
1491
- label,
1492
- hint: "",
1493
- run: () => {
1494
- showFlash(`\u22EF Moving to ${label}\u2026`);
1495
- void quietly`jira issue move ${ticket} ${transition}`.then(() => {
1496
- invalidateJiraMoves(ticket);
1497
- showFlash(`\u2713 Moved to ${label}`);
1498
- onActed?.();
1499
- }).catch(() => showFlash(`\u2717 Move to ${label} failed`));
1500
1763
  }
1501
- }
1502
- );
1764
+ );
1765
+ };
1503
1766
  var jiraLoadingActions = () => [
1504
1767
  { label: "Loading transitions\u2026", hint: "", tone: "dim", run: () => {
1505
1768
  } }
@@ -1513,12 +1776,13 @@ var openJiraMoves = (deps) => {
1513
1776
  open,
1514
1777
  isOpen,
1515
1778
  showFlash,
1516
- onActed
1779
+ onActed,
1780
+ optimistic
1517
1781
  } = deps;
1518
1782
  const openOrdered = (moves) => {
1519
1783
  const ordered = orderJiraMoves(moves, configured);
1520
1784
  open(
1521
- ordered.length > 0 ? jiraMoveActions(ticket, ordered, showFlash, onActed) : [
1785
+ ordered.length > 0 ? jiraMoveActions(ticket, ordered, showFlash, onActed, optimistic) : [
1522
1786
  {
1523
1787
  label: "No transitions available",
1524
1788
  hint: "",
@@ -2011,7 +2275,7 @@ var ROW_MAX_COLS = 140;
2011
2275
  var OVERLAY_BG = "#22222e";
2012
2276
  var SELECTION_BG = "#16161f";
2013
2277
  var selectionBackground = (active, backdropped) => active && !backdropped ? SELECTION_BG : void 0;
2014
- var TICKET_KEY_COLOUR = "#C792EA";
2278
+ var TICKET_KEY_COLOUR = colors.ticket;
2015
2279
  var taskKeyStyle = () => ({
2016
2280
  color: TICKET_KEY_COLOUR,
2017
2281
  bold: true
@@ -2595,6 +2859,8 @@ var PULSE_IDLE_MS = 6e4;
2595
2859
  var PULSE_SETTLED_FRAME = 3;
2596
2860
  var NO_TRANSIENTS = /* @__PURE__ */ new Map();
2597
2861
  var NO_HOLDS = /* @__PURE__ */ new Map();
2862
+ var PENDING_GLYPH = "\u25CC";
2863
+ var FAILURE_NOTICE_MS = 8e3;
2598
2864
  var TRANSIT_OUT_FRAMES = ["\u25C9", "\u25CE", "\u25CB", "\xB7"];
2599
2865
  var TRANSIT_IN_FRAMES = ["\xB7", "\u25CB", "\u25CE", "\u25C9"];
2600
2866
  var TRANSIT_COLOUR = {
@@ -2623,6 +2889,7 @@ var ItemRow = ({
2623
2889
  merged,
2624
2890
  transient: refreshMark,
2625
2891
  leaving,
2892
+ pending,
2626
2893
  transitAt,
2627
2894
  prefix = "",
2628
2895
  parent,
@@ -2673,7 +2940,7 @@ var ItemRow = ({
2673
2940
  ] });
2674
2941
  if (item.kind === "task") {
2675
2942
  const note = item.note ?? "";
2676
- const transitIcon = !transient ? " " : isDeparture(transient) ? TRANSIT_OUT_FRAMES[rampAt(TRANSIT_OUT_FRAMES.length)] : isArrival(transient) ? TRANSIT_IN_FRAMES[rampAt(TRANSIT_IN_FRAMES.length)] : "\u25C9";
2943
+ const transitIcon = pending ? PENDING_GLYPH : !transient ? " " : isDeparture(transient) ? TRANSIT_OUT_FRAMES[rampAt(TRANSIT_OUT_FRAMES.length)] : isArrival(transient) ? TRANSIT_IN_FRAMES[rampAt(TRANSIT_IN_FRAMES.length)] : "\u25C9";
2677
2944
  const titleMax = Math.max(
2678
2945
  1,
2679
2946
  cols - item.key.length - note.length - (item.pill ? pillWidth(item.pill) + 1 : 0) - (farewellLabel ? pillWidth(farewellLabel) + 2 : 0) - prefix.length - (markerCols > 0 ? markerCols + 1 : 0) - 12
@@ -2682,7 +2949,7 @@ var ItemRow = ({
2682
2949
  /* @__PURE__ */ jsx(Text3, { color: colors.info, children: active ? "\u276F " : " " }),
2683
2950
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: prefix }),
2684
2951
  /* @__PURE__ */ jsx(Text3, { dimColor: true, children: parent ? "\u252C " : " " }),
2685
- /* @__PURE__ */ jsx(Text3, { bold: true, color: transient ? TRANSIT_COLOUR[transient] : void 0, children: transitIcon + " " }),
2952
+ pending ? /* @__PURE__ */ jsx(Text3, { dimColor: true, children: transitIcon + " " }) : /* @__PURE__ */ jsx(Text3, { bold: true, color: transient ? TRANSIT_COLOUR[transient] : void 0, children: transitIcon + " " }),
2686
2953
  markerCols > 0 ? /* @__PURE__ */ jsx(Text3, { color: item.markerColor, children: (item.marker ?? "").padEnd(markerCols) + " " }) : null,
2687
2954
  /* @__PURE__ */ jsx(Text3, { ...taskKeyStyle(), children: item.key + " " }),
2688
2955
  /* @__PURE__ */ jsx(
@@ -2970,12 +3237,13 @@ var HelpModal = ({
2970
3237
  ["u", "unsubscribe from this item"],
2971
3238
  ["x", "remove yourself as reviewer"],
2972
3239
  ...hasJira ? [
2973
- ["t", "Jira: move / open ticket"],
2974
- ["\u2303K", "launch \xB7 jump to a ticket by key"]
3240
+ ["t", "Jira: move / open ticket"]
2975
3241
  ] : [],
3242
+ ["\u2303K", "launch \xB7 open a ticket or PR by name"],
2976
3243
  ["/", "search"],
2977
3244
  ["f", "filter by repo"],
2978
3245
  ["r", "refresh"],
3246
+ ["w", "restore a change that failed"],
2979
3247
  ...extensionLegend(extensions),
2980
3248
  ["?", "this help"],
2981
3249
  ["q", "quit"]
@@ -3183,7 +3451,11 @@ var BrowseScreen = ({
3183
3451
  transients,
3184
3452
  onTabChange,
3185
3453
  sidebar,
3186
- liveLabel
3454
+ liveLabel,
3455
+ optimisticMove,
3456
+ pendingKeys,
3457
+ failure,
3458
+ onRestore
3187
3459
  }) => {
3188
3460
  const { rows } = useWindowSize();
3189
3461
  const applyWork = (secs) => origin ? filterByOrigin(secs, origin.show, origin.match, login) : secs;
@@ -3298,6 +3570,14 @@ var BrowseScreen = ({
3298
3570
  if (flashTimer.current) clearTimeout(flashTimer.current);
3299
3571
  flashTimer.current = setTimeout(() => setFlash(null), 1500);
3300
3572
  };
3573
+ const [dismissedSeq, setDismissedSeq] = useState(null);
3574
+ const noticeUp = !!failure && dismissedSeq !== failure.seq;
3575
+ useEffect(() => {
3576
+ if (!failure) return;
3577
+ const seq = failure.seq;
3578
+ const timer = setTimeout(() => setDismissedSeq(seq), FAILURE_NOTICE_MS);
3579
+ return () => clearTimeout(timer);
3580
+ }, [failure?.seq]);
3301
3581
  const markedTabs = useMemo(() => {
3302
3582
  const out = /* @__PURE__ */ new Set();
3303
3583
  if (!transients?.size) return out;
@@ -3323,6 +3603,7 @@ var BrowseScreen = ({
3323
3603
  if (repoPicker) return setRepoPicker(false), true;
3324
3604
  if (railActive) return setRailFocus(false), true;
3325
3605
  if (menu.actions !== null) return menu.close(), true;
3606
+ if (noticeUp && failure) return setDismissedSeq(failure.seq), true;
3326
3607
  if (search != null) return filter.clear(), true;
3327
3608
  if (repoFilter.size > 0) return setRepoFilter(/* @__PURE__ */ new Set()), true;
3328
3609
  return false;
@@ -3428,7 +3709,8 @@ var BrowseScreen = ({
3428
3709
  open: (rows2) => menu.open(rows2),
3429
3710
  isOpen: () => menuActionsRef.current !== null,
3430
3711
  showFlash,
3431
- onActed
3712
+ onActed,
3713
+ optimistic: optimisticMove ? (to) => optimisticMove(task, to) : void 0
3432
3714
  });
3433
3715
  };
3434
3716
  const openMenu = () => {
@@ -3504,6 +3786,10 @@ var BrowseScreen = ({
3504
3786
  return;
3505
3787
  }
3506
3788
  if (searchInput && !key.upArrow && !key.downArrow) return;
3789
+ if (input === "w" && failure && onRestore) {
3790
+ onRestore();
3791
+ return;
3792
+ }
3507
3793
  if (input === "f" && allRepos.length > 0) {
3508
3794
  setRepoCursor(0);
3509
3795
  setRepoPicker(true);
@@ -3861,9 +4147,10 @@ var BrowseScreen = ({
3861
4147
  ["\u2190\u2192", "tab"],
3862
4148
  ["\u21B5/d", "open"],
3863
4149
  ["m", "actions"],
3864
- // Only where Jira is configured: without it the launcher can only say
3865
- // so, and a footer key that opens an apology is a broken feature.
3866
- ...jiraBase ? [["\u2303K", "launch"]] : [],
4150
+ // Always, now that the launcher opens a pasted PR or issue too. It was
4151
+ // shown only where Jira was configured, when a ticket key was all it
4152
+ // could take and without one it could only apologise.
4153
+ ["\u2303K", "launch"],
3867
4154
  // Advertised only where it does something, and named for what it
3868
4155
  // shows rather than for the furniture: nobody wants "a sidebar".
3869
4156
  ...showRail ? [["\u21E5", railTitle]] : hasRail ? [["i", railTitle]] : [],
@@ -3877,10 +4164,16 @@ var BrowseScreen = ({
3877
4164
  typing: searchInput,
3878
4165
  nerd: getIconMode() === "nerd"
3879
4166
  });
4167
+ const launcherLookup = useLauncherLookup(palette, (item) => {
4168
+ if (!openDrillView(settle(localSections, item))) return false;
4169
+ setPalette(null);
4170
+ return true;
4171
+ });
3880
4172
  const launcher = useMemo(() => {
3881
4173
  const run = /* @__PURE__ */ new Map();
4174
+ const keepOpen = /* @__PURE__ */ new Set();
3882
4175
  if (palette === null)
3883
- return { items: [], run, message: void 0 };
4176
+ return { items: [], run, keepOpen, message: void 0 };
3884
4177
  const query = palette.trim();
3885
4178
  const ticketKey = jiraKeyRe ? query.toUpperCase().match(jiraKeyRe)?.[0] : void 0;
3886
4179
  const commands = [];
@@ -3891,6 +4184,31 @@ var BrowseScreen = ({
3891
4184
  /* @__PURE__ */ jsx(Text3, { color: colors.info, children: where })
3892
4185
  ] });
3893
4186
  const labels = /* @__PURE__ */ new Map();
4187
+ const claim = claimFor(
4188
+ query,
4189
+ {
4190
+ activeRepo: activeItem?.kind === "pr" || activeItem?.kind === "issue" ? activeItem.repo : void 0,
4191
+ repos: reposInSections(localSections),
4192
+ sections: localSections
4193
+ },
4194
+ extensions
4195
+ );
4196
+ const claimed = claim ? lookupRows(
4197
+ claim,
4198
+ launcherLookup.lookup,
4199
+ palette,
4200
+ launcherLookup.start,
4201
+ (url, label) => {
4202
+ quietly`open ${url}`.catch(() => {
4203
+ });
4204
+ showFlash(`\u2197 Opened ${label} in browser`);
4205
+ }
4206
+ ) : void 0;
4207
+ for (const row of claimed?.rows ?? []) {
4208
+ commands.push({ id: row.id, title: row.title, run: row.run });
4209
+ labels.set(row.id, row.label);
4210
+ if (row.keepOpen) keepOpen.add(row.id);
4211
+ }
3894
4212
  if (ticketKey && jiraBase) {
3895
4213
  const row = {
3896
4214
  kind: "task",
@@ -3926,16 +4244,26 @@ var BrowseScreen = ({
3926
4244
  commands.push({ ...command, id: `${ext.id}:${command.id}` });
3927
4245
  }
3928
4246
  }
4247
+ if (claimed?.exclusive) commands.splice(claimed.rows.length);
3929
4248
  for (const command of commands) run.set(command.id, command.run);
3930
- const message = !jiraBase ? `Jira not configured${jiraSetupHint ? ` \xB7 ${jiraSetupHint}` : ""}` : query && !ticketKey ? `no ticket matches "${query}"` : void 0;
4249
+ const message = claimed ? claimed.message : !jiraBase ? `Jira not configured${jiraSetupHint ? ` \xB7 ${jiraSetupHint}` : ""}` : query && !ticketKey ? `nothing matches "${query}"` : void 0;
3931
4250
  const items = commands.map((command) => ({
3932
4251
  id: command.id,
3933
4252
  title: command.title,
3934
4253
  group: command.group,
3935
4254
  label: labels.get(command.id)
3936
4255
  }));
3937
- return { items, run, message };
3938
- }, [palette, activeItem, extensions, jiraBase, jiraKeyRe, jiraSetupHint]);
4256
+ return { items, run, keepOpen, message };
4257
+ }, [
4258
+ palette,
4259
+ activeItem,
4260
+ extensions,
4261
+ jiraBase,
4262
+ jiraKeyRe,
4263
+ jiraSetupHint,
4264
+ localSections,
4265
+ launcherLookup.lookup
4266
+ ]);
3939
4267
  const chrome = useContext(InboxChromeContext);
3940
4268
  const overlayOpen = overlayBody != null;
3941
4269
  const overlay = palette !== null ? /* @__PURE__ */ jsx(
@@ -3943,15 +4271,18 @@ var BrowseScreen = ({
3943
4271
  {
3944
4272
  items: launcher.items,
3945
4273
  query: palette,
3946
- onQueryChange: setPalette,
4274
+ onQueryChange: (query) => {
4275
+ launcherLookup.reset();
4276
+ setPalette(query);
4277
+ },
3947
4278
  onSelect: (id) => {
3948
4279
  const run = launcher.run.get(id);
3949
- setPalette(null);
4280
+ if (!launcher.keepOpen.has(id)) setPalette(null);
3950
4281
  void run?.();
3951
4282
  },
3952
4283
  onClose: () => setPalette(null),
3953
4284
  message: launcher.message,
3954
- placeholder: "ticket key\u2026",
4285
+ placeholder: "ticket, PR, owner/repo#N or URL\u2026",
3955
4286
  width: Math.min(60, Math.max(30, listCols - 2)),
3956
4287
  maxRows: Math.max(3, listHeight - 6),
3957
4288
  hints: [
@@ -4054,7 +4385,8 @@ var BrowseScreen = ({
4054
4385
  s.id,
4055
4386
  pulseSettled ? PULSE_SETTLED_FRAME : sparkFrame
4056
4387
  ),
4057
- markerColor: colors.accent
4388
+ markerColor: colors.accent,
4389
+ group: s.group
4058
4390
  }))
4059
4391
  }
4060
4392
  ) }),
@@ -4105,6 +4437,7 @@ var BrowseScreen = ({
4105
4437
  merged: (item.kind === "pr" || item.kind === "issue") && !!mergedUrls?.includes(item.url),
4106
4438
  leaving: (item.kind === "pr" || item.kind === "issue") && !!leavingUrls?.includes(item.url),
4107
4439
  transient: transientOf(transients, item, section.id),
4440
+ pending: !!pendingKeys?.has(keyOf(item) ?? ""),
4108
4441
  transitAt: "url" in item ? transitSince?.get(item.url) : void 0,
4109
4442
  prefix: treePrefix(section.items, viewStart + i),
4110
4443
  parent: item.kind === "task" && depthOf(section.items[viewStart + i + 1]) > depthOf(item),
@@ -4153,7 +4486,7 @@ var BrowseScreen = ({
4153
4486
  }
4154
4487
  ) : null
4155
4488
  ] }),
4156
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text3, { color: colors.success, children: flash }) : overlayOpen ? (
4489
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: noticeUp && failure ? /* @__PURE__ */ jsx(Text3, { color: colors.error, children: failure.notice }) : flash ? /* @__PURE__ */ jsx(Text3, { color: colors.success, children: flash }) : overlayOpen ? (
4157
4490
  // The overlay's own verbs, plus the way back. `page="nested"` is what
4158
4491
  // appends `⌫ back` and `q quit`, so a body passes only its own keys
4159
4492
  // through `useChrome` — the same contract ink-ui's `Page` offers its
@@ -4202,6 +4535,7 @@ var App = ({
4202
4535
  jiraSetupHint,
4203
4536
  jiraTransitions,
4204
4537
  jiraTransitionsFor,
4538
+ tabForStatus,
4205
4539
  hasCiStatus,
4206
4540
  ciJob,
4207
4541
  ciFetcher,
@@ -4277,12 +4611,14 @@ var App = ({
4277
4611
  const transitTabs = useRef(/* @__PURE__ */ new Map());
4278
4612
  const [heldSince, setHeldSince] = useState(NO_HOLDS);
4279
4613
  const [visibleTab, setVisibleTab] = useState("");
4280
- const showData = (sections, login) => {
4614
+ const showData = (sections, login, fetched = true) => {
4281
4615
  const before = displayedSections.current;
4282
4616
  displayedSections.current = sections;
4283
4617
  displayedKey.current = signatureOf(sections);
4284
- setPending(null);
4285
- setFetchedAt(Date.now());
4618
+ if (fetched) {
4619
+ setPending(null);
4620
+ setFetchedAt(Date.now());
4621
+ }
4286
4622
  if (before.length === 0) {
4287
4623
  transitTabs.current = /* @__PURE__ */ new Map();
4288
4624
  setTransients(NO_TRANSIENTS);
@@ -4371,7 +4707,7 @@ var App = ({
4371
4707
  if (new Date(known.resetAt).getTime() <= Date.now()) return true;
4372
4708
  return known.remaining >= Math.max(known.cost, 1) * 2;
4373
4709
  };
4374
- const revalidate = (manual = false) => {
4710
+ const revalidate = (manual = false, apply = false) => {
4375
4711
  const gated = !manual && !!displayedKey.current;
4376
4712
  if (gated && !canAffordAuto()) {
4377
4713
  setSkippedForBudget(true);
@@ -4395,7 +4731,7 @@ var App = ({
4395
4731
  setFetchedAt(Date.now());
4396
4732
  setRefreshError(null);
4397
4733
  if (hasCiStatus) applyCiStatus(fresh.ciStatus ?? null);
4398
- receive(fresh);
4734
+ receive(fresh, apply);
4399
4735
  }).catch((err) => {
4400
4736
  setRefreshing(false);
4401
4737
  const message = explainGhAction(err);
@@ -4406,8 +4742,22 @@ var App = ({
4406
4742
  setRefreshError({ message, at: Date.now() });
4407
4743
  });
4408
4744
  };
4409
- const receive = (fresh) => {
4745
+ const receive = (truth, apply = false) => {
4746
+ overrides.current = pruneOverrides(
4747
+ overrides.current,
4748
+ truth.sections,
4749
+ Date.now()
4750
+ );
4751
+ setOverrideRev((n) => n + 1);
4752
+ const fresh = {
4753
+ ...truth,
4754
+ sections: applyOverrides(truth.sections, overrides.current)
4755
+ };
4410
4756
  const freshKey = signatureOf(fresh.sections);
4757
+ if (apply && displayedKey.current) {
4758
+ showData(fresh.sections, fresh.login);
4759
+ return;
4760
+ }
4411
4761
  if (!displayedKey.current) {
4412
4762
  if (fresh.sections.length === 0) {
4413
4763
  setState({ phase: "empty" });
@@ -4427,6 +4777,74 @@ var App = ({
4427
4777
  if (pending) showData(pending.sections, pending.login);
4428
4778
  else revalidate(true);
4429
4779
  };
4780
+ const overrides = useRef(/* @__PURE__ */ new Map());
4781
+ const [overrideRev, setOverrideRev] = useState(0);
4782
+ const failureSeq = useRef(0);
4783
+ const pendingKeys = useMemo(
4784
+ () => new Set(
4785
+ [...overrides.current].filter(([, o]) => o.phase === "inflight").map(([key]) => key)
4786
+ ),
4787
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4788
+ [overrideRev]
4789
+ );
4790
+ const failure = useMemo(() => {
4791
+ const failed = failedOverrides(overrides.current);
4792
+ const notice = failureNotice(failed);
4793
+ const seq = failed[0]?.[1].failure?.seq;
4794
+ return notice && seq != null ? { notice, seq } : void 0;
4795
+ }, [overrideRev]);
4796
+ const hold = (key, override) => {
4797
+ overrides.current = new Map(overrides.current).set(key, override);
4798
+ setOverrideRev((n) => n + 1);
4799
+ const one = /* @__PURE__ */ new Map([[key, override]]);
4800
+ if (state.phase === "browse")
4801
+ showData(
4802
+ applyOverrides(displayedSections.current, one),
4803
+ state.login,
4804
+ false
4805
+ );
4806
+ setPending(
4807
+ (p) => p ? { ...p, sections: applyOverrides(p.sections, one) } : p
4808
+ );
4809
+ const answer = (next) => {
4810
+ if (overrides.current.get(key) !== override) return;
4811
+ overrides.current = new Map(overrides.current).set(key, {
4812
+ ...override,
4813
+ ...next
4814
+ });
4815
+ setOverrideRev((n) => n + 1);
4816
+ };
4817
+ return {
4818
+ // Answered yes: only now does the hold clock matter — see OVERRIDE_HOLD_MS.
4819
+ settled: () => answer({ phase: "sent", since: Date.now() }),
4820
+ failed: (err) => answer({
4821
+ phase: "failed",
4822
+ failure: { reason: explainGhAction(err), seq: ++failureSeq.current }
4823
+ })
4824
+ };
4825
+ };
4826
+ const optimisticMove = tabForStatus ? (task, to) => {
4827
+ const key = keyOf(task);
4828
+ const toSection = tabForStatus(to);
4829
+ if (!key || !toSection) return void 0;
4830
+ return hold(key, {
4831
+ patch: { kind: "move", toSection, fields: { status: to.name } },
4832
+ action: "move",
4833
+ label: task.ticket ?? task.key,
4834
+ since: Date.now(),
4835
+ phase: "inflight"
4836
+ });
4837
+ } : void 0;
4838
+ const restore = () => {
4839
+ const newest = failedOverrides(overrides.current)[0];
4840
+ if (!newest) return;
4841
+ const next = new Map(overrides.current);
4842
+ next.delete(newest[0]);
4843
+ overrides.current = next;
4844
+ setOverrideRev((n) => n + 1);
4845
+ if (cacheKey) invalidateCache(cacheKey);
4846
+ revalidate(true, true);
4847
+ };
4430
4848
  const onActed = () => {
4431
4849
  if (cacheKey) invalidateCache(cacheKey);
4432
4850
  setTimeout(() => applyOrRefresh(), 1500);
@@ -4651,6 +5069,10 @@ var App = ({
4651
5069
  jiraSetupHint,
4652
5070
  jiraTransitions,
4653
5071
  jiraTransitionsFor,
5072
+ optimisticMove,
5073
+ pendingKeys,
5074
+ failure,
5075
+ onRestore: restore,
4654
5076
  onRefresh: applyOrRefresh,
4655
5077
  onActed,
4656
5078
  onSettled,
@@ -4727,4 +5149,4 @@ var matchesFilter = (repo, filter) => {
4727
5149
  };
4728
5150
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
4729
5151
 
4730
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, MIN_SIDEBAR_COLS, PrRow, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, isMenuGap, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, menuRowsOf, menuTone, moveCursor, numberColumnsOf, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, useChrome, windowCount, withHeaders, withoutItem, workCount, writeCache };
5152
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, LookupMiss, MIN_SIDEBAR_COLS, PrRow, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, budgetNotice, buildActions, buildCheckoutCmd, clipboard, counts, drillCmd, explainGhAction, explainItem, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, isMenuGap, itermRun, jumpToRepo, jumpToRepoPane, matchesFilter, maxViewStart, menuRowsOf, menuTone, moveCursor, numberColumnsOf, openInTab, parsePatterns, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, renderMarkdown, reposInSections, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, useChrome, windowCount, withHeaders, withoutItem, workCount, writeCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.70.0",
3
+ "version": "0.73.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",
@@ -44,17 +44,17 @@
44
44
  "access": "public"
45
45
  },
46
46
  "peerDependencies": {
47
- "@kud/ink-ui": ">=0.33.0 <1",
47
+ "@kud/ink-ui": ">=0.36.0 <1",
48
48
  "ink": ">=7",
49
49
  "react": ">=19"
50
50
  },
51
51
  "dependencies": {
52
- "@kud/gh": "0.20.0",
53
- "@kud/gh-workflow": "0.14.1",
52
+ "@kud/gh": "0.21.0",
53
+ "@kud/gh-workflow": "0.16.0",
54
54
  "zx": "8.8.5"
55
55
  },
56
56
  "devDependencies": {
57
- "@kud/ink-ui": "0.34.0",
57
+ "@kud/ink-ui": "0.36.0",
58
58
  "@types/node": "22.20.1",
59
59
  "@types/react": "19.2.17",
60
60
  "ink": "7.1.0",