@ondrej-svec/hog 1.2.0 → 1.3.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/cli.js CHANGED
@@ -9,6 +9,166 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/ai.ts
13
+ async function parseHeuristic(input2, today = /* @__PURE__ */ new Date()) {
14
+ let remaining = input2;
15
+ const labelMatches = [...remaining.matchAll(/#([\w:/-]+)/g)];
16
+ const rawLabels = labelMatches.map((m) => (m[1] ?? "").toLowerCase());
17
+ remaining = remaining.replace(/#[\w:/-]+/g, "").trim();
18
+ const assigneeMatches = [...remaining.matchAll(/@([\w-]+)/g)];
19
+ const assignee = assigneeMatches.length > 0 ? assigneeMatches[assigneeMatches.length - 1]?.[1] ?? null : null;
20
+ remaining = remaining.replace(/@[\w-]+/g, "").trim();
21
+ let dueDate = null;
22
+ const dueMatch = remaining.match(/\bdue\s+(.+?)(?:\s+#|\s+@|$)/i);
23
+ if (dueMatch?.[1]) {
24
+ const { parse } = await import("chrono-node");
25
+ const results = parse(dueMatch[1], { instant: today }, { forwardDate: true });
26
+ const first = results[0];
27
+ if (first) {
28
+ let date = first.date();
29
+ if (date < today) {
30
+ date = new Date(date);
31
+ date.setFullYear(date.getFullYear() + 1);
32
+ }
33
+ const yyyy = date.getFullYear();
34
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
35
+ const dd = String(date.getDate()).padStart(2, "0");
36
+ dueDate = `${yyyy}-${mm}-${dd}`;
37
+ }
38
+ remaining = remaining.slice(0, dueMatch.index ?? 0).trim();
39
+ }
40
+ const title = remaining.replace(/\s+/g, " ").trim();
41
+ if (!title) return null;
42
+ return { title, labels: rawLabels, assignee, dueDate };
43
+ }
44
+ function detectProvider() {
45
+ const orKey = process.env["OPENROUTER_API_KEY"];
46
+ if (orKey) return { provider: "openrouter", apiKey: orKey };
47
+ const antKey = process.env["ANTHROPIC_API_KEY"];
48
+ if (antKey) return { provider: "anthropic", apiKey: antKey };
49
+ return null;
50
+ }
51
+ async function callLLM(userText, validLabels, today, providerConfig) {
52
+ const { provider, apiKey } = providerConfig;
53
+ const todayStr = today.toISOString().slice(0, 10);
54
+ const systemPrompt = `Extract GitHub issue fields. Today is ${todayStr}. Return JSON with: title (string), labels (string[]), due_date (YYYY-MM-DD or null), assignee (string or null).`;
55
+ const escapedText = userText.replace(/<\/input>/gi, "< /input>");
56
+ const userContent = `<input>${escapedText}</input>
57
+ <valid_labels>${validLabels.join(",")}</valid_labels>`;
58
+ const jsonSchema = {
59
+ name: "issue",
60
+ schema: {
61
+ type: "object",
62
+ properties: {
63
+ title: { type: "string" },
64
+ labels: { type: "array", items: { type: "string" } },
65
+ due_date: { type: ["string", "null"] },
66
+ assignee: { type: ["string", "null"] }
67
+ },
68
+ required: ["title", "labels", "due_date", "assignee"],
69
+ additionalProperties: false
70
+ }
71
+ };
72
+ try {
73
+ let response;
74
+ if (provider === "openrouter") {
75
+ response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
76
+ method: "POST",
77
+ headers: {
78
+ "Content-Type": "application/json",
79
+ Authorization: `Bearer ${apiKey}`
80
+ },
81
+ body: JSON.stringify({
82
+ model: "google/gemini-2.5-flash",
83
+ messages: [
84
+ { role: "system", content: systemPrompt },
85
+ { role: "user", content: userContent }
86
+ ],
87
+ response_format: { type: "json_schema", json_schema: jsonSchema },
88
+ max_tokens: 256,
89
+ temperature: 0
90
+ }),
91
+ signal: AbortSignal.timeout(5e3)
92
+ });
93
+ } else {
94
+ response = await fetch("https://api.anthropic.com/v1/messages", {
95
+ method: "POST",
96
+ headers: {
97
+ "Content-Type": "application/json",
98
+ "x-api-key": apiKey,
99
+ "anthropic-version": "2023-06-01"
100
+ },
101
+ body: JSON.stringify({
102
+ model: "claude-haiku-4-5-20251001",
103
+ system: systemPrompt,
104
+ messages: [{ role: "user", content: userContent }],
105
+ max_tokens: 256
106
+ }),
107
+ signal: AbortSignal.timeout(5e3)
108
+ });
109
+ }
110
+ if (!response.ok) return null;
111
+ const json = await response.json();
112
+ let raw;
113
+ if (provider === "openrouter") {
114
+ const choicesRaw = json["choices"];
115
+ if (!Array.isArray(choicesRaw)) return null;
116
+ const firstChoice = choicesRaw[0];
117
+ const content = firstChoice?.message?.content;
118
+ if (!content) return null;
119
+ raw = JSON.parse(content);
120
+ } else {
121
+ const contentRaw = json["content"];
122
+ if (!Array.isArray(contentRaw)) return null;
123
+ const firstItem = contentRaw[0];
124
+ const text = firstItem?.text;
125
+ if (!text) return null;
126
+ raw = JSON.parse(text);
127
+ }
128
+ if (!raw || typeof raw !== "object") return null;
129
+ const r = raw;
130
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
131
+ return {
132
+ title: typeof r["title"] === "string" ? r["title"] : "",
133
+ labels: Array.isArray(r["labels"]) ? r["labels"].filter((l) => typeof l === "string") : [],
134
+ due_date: typeof r["due_date"] === "string" && ISO_DATE_RE.test(r["due_date"]) ? r["due_date"] : null,
135
+ assignee: typeof r["assignee"] === "string" ? r["assignee"] : null
136
+ };
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+ async function extractIssueFields(input2, options = {}) {
142
+ const today = options.today ?? /* @__PURE__ */ new Date();
143
+ const heuristic = await parseHeuristic(input2, today);
144
+ if (!heuristic) return null;
145
+ const providerConfig = detectProvider();
146
+ if (!providerConfig) return heuristic;
147
+ const llmResult = await callLLM(input2, options.validLabels ?? [], today, providerConfig);
148
+ if (!llmResult) {
149
+ options.onLlmFallback?.("AI parsing unavailable, used keyword matching");
150
+ return heuristic;
151
+ }
152
+ const merged = {
153
+ ...llmResult,
154
+ // Heuristic explicit tokens always win
155
+ labels: heuristic.labels.length > 0 ? heuristic.labels : llmResult.labels,
156
+ assignee: heuristic.assignee ?? llmResult.assignee,
157
+ dueDate: heuristic.dueDate ?? llmResult.due_date,
158
+ // LLM title is used only if heuristic left explicit tokens
159
+ title: heuristic.labels.length > 0 || heuristic.assignee || heuristic.dueDate ? llmResult.title || heuristic.title : heuristic.title
160
+ };
161
+ return merged;
162
+ }
163
+ function hasLlmApiKey() {
164
+ return detectProvider() !== null;
165
+ }
166
+ var init_ai = __esm({
167
+ "src/ai.ts"() {
168
+ "use strict";
169
+ }
170
+ });
171
+
12
172
  // src/api.ts
13
173
  var BASE_URL, TickTickClient;
14
174
  var init_api = __esm({
@@ -461,6 +621,21 @@ function fetchProjectStatusOptions(repo, projectNumber, _statusFieldId) {
461
621
  function addLabel(repo, issueNumber, label) {
462
622
  runGh(["issue", "edit", String(issueNumber), "--repo", repo, "--add-label", label]);
463
623
  }
624
+ async function fetchRepoLabelsAsync(repo) {
625
+ try {
626
+ const result = await runGhJsonAsync([
627
+ "label",
628
+ "list",
629
+ "--repo",
630
+ repo,
631
+ "--json",
632
+ "name,color"
633
+ ]);
634
+ return Array.isArray(result) ? result : [];
635
+ } catch {
636
+ return [];
637
+ }
638
+ }
464
639
  function updateProjectItemStatus(repo, issueNumber, projectConfig) {
465
640
  const [owner, repoName] = repo.split("/");
466
641
  const findItemQuery = `
@@ -678,6 +853,21 @@ var init_sync_state = __esm({
678
853
  }
679
854
  });
680
855
 
856
+ // src/clipboard.ts
857
+ function getClipboardArgs() {
858
+ if (process.platform === "darwin") return ["pbcopy"];
859
+ if (process.platform === "win32") return ["clip"];
860
+ if (process.env["WSL_DISTRO_NAME"] ?? process.env["WSL_INTEROP"]) return ["clip.exe"];
861
+ if (process.env["WAYLAND_DISPLAY"]) return ["wl-copy"];
862
+ if (process.env["DISPLAY"]) return ["xsel", "--clipboard", "--input"];
863
+ return null;
864
+ }
865
+ var init_clipboard = __esm({
866
+ "src/clipboard.ts"() {
867
+ "use strict";
868
+ }
869
+ });
870
+
681
871
  // src/pick.ts
682
872
  var pick_exports = {};
683
873
  __export(pick_exports, {
@@ -1034,6 +1224,26 @@ function useActions({
1034
1224
  },
1035
1225
  [toast, refresh, onOverlayDone]
1036
1226
  );
1227
+ const handleLabelChange = useCallback(
1228
+ (addLabels, removeLabels) => {
1229
+ const ctx = findIssueContext(reposRef.current, selectedIdRef.current, configRef.current);
1230
+ if (!(ctx.issue && ctx.repoName)) return;
1231
+ const { issue, repoName } = ctx;
1232
+ const args = ["issue", "edit", String(issue.number), "--repo", repoName];
1233
+ for (const label of addLabels) args.push("--add-label", label);
1234
+ for (const label of removeLabels) args.push("--remove-label", label);
1235
+ const t = toast.loading("Updating labels...");
1236
+ execFileAsync2("gh", args, { encoding: "utf-8", timeout: 3e4 }).then(() => {
1237
+ t.resolve(`Labels updated on #${issue.number}`);
1238
+ refresh();
1239
+ onOverlayDone();
1240
+ }).catch((err) => {
1241
+ t.reject(`Label update failed: ${err instanceof Error ? err.message : String(err)}`);
1242
+ onOverlayDone();
1243
+ });
1244
+ },
1245
+ [toast, refresh, onOverlayDone]
1246
+ );
1037
1247
  const handleBulkAssign = useCallback(
1038
1248
  async (ids) => {
1039
1249
  const failed = [];
@@ -1164,6 +1374,7 @@ function useActions({
1164
1374
  handleStatusChange,
1165
1375
  handleAssign,
1166
1376
  handleUnassign,
1377
+ handleLabelChange,
1167
1378
  handleCreateIssue,
1168
1379
  handleBulkAssign,
1169
1380
  handleBulkUnassign,
@@ -1299,7 +1510,13 @@ function useData(config2, options, refreshIntervalMs) {
1299
1510
  return { ...prev, data: fn(prev.data) };
1300
1511
  });
1301
1512
  }, []);
1302
- return { ...state, refresh, mutateData };
1513
+ const pauseAutoRefresh = useCallback2(() => {
1514
+ setState((prev) => ({ ...prev, autoRefreshPaused: true }));
1515
+ }, []);
1516
+ const resumeAutoRefresh = useCallback2(() => {
1517
+ setState((prev) => ({ ...prev, autoRefreshPaused: false }));
1518
+ }, []);
1519
+ return { ...state, refresh, mutateData, pauseAutoRefresh, resumeAutoRefresh };
1303
1520
  }
1304
1521
  var INITIAL_STATE, STALE_THRESHOLDS, MAX_REFRESH_FAILURES;
1305
1522
  var init_use_data = __esm({
@@ -1325,14 +1542,234 @@ var init_use_data = __esm({
1325
1542
  }
1326
1543
  });
1327
1544
 
1545
+ // src/board/hooks/use-keyboard.ts
1546
+ import { useInput } from "ink";
1547
+ import { useCallback as useCallback3 } from "react";
1548
+ function isHeaderId(id) {
1549
+ return id != null && (id.startsWith("header:") || id.startsWith("sub:"));
1550
+ }
1551
+ function useKeyboard({
1552
+ ui,
1553
+ nav,
1554
+ multiSelect,
1555
+ selectedIssue,
1556
+ selectedRepoStatusOptionsLength,
1557
+ actions,
1558
+ onSearchEscape
1559
+ }) {
1560
+ const {
1561
+ exit,
1562
+ refresh,
1563
+ handleSlack,
1564
+ handleCopyLink,
1565
+ handleOpen,
1566
+ handleEnterFocus,
1567
+ handlePick,
1568
+ handleAssign,
1569
+ handleUnassign,
1570
+ handleEnterLabel,
1571
+ handleEnterCreateNl,
1572
+ handleErrorAction,
1573
+ toastInfo
1574
+ } = actions;
1575
+ const handleInput = useCallback3(
1576
+ (input2, key) => {
1577
+ if (input2 === "?") {
1578
+ ui.toggleHelp();
1579
+ return;
1580
+ }
1581
+ if (key.escape && ui.state.mode !== "focus") {
1582
+ if (ui.state.mode === "multiSelect") {
1583
+ multiSelect.clear();
1584
+ }
1585
+ ui.exitOverlay();
1586
+ return;
1587
+ }
1588
+ if (ui.canNavigate) {
1589
+ if (input2 === "j" || key.downArrow) {
1590
+ nav.moveDown();
1591
+ return;
1592
+ }
1593
+ if (input2 === "k" || key.upArrow) {
1594
+ nav.moveUp();
1595
+ return;
1596
+ }
1597
+ if (key.tab) {
1598
+ if (ui.state.mode === "multiSelect") {
1599
+ multiSelect.clear();
1600
+ ui.clearMultiSelect();
1601
+ }
1602
+ key.shift ? nav.prevSection() : nav.nextSection();
1603
+ return;
1604
+ }
1605
+ }
1606
+ if (ui.state.mode === "multiSelect") {
1607
+ if (input2 === " ") {
1608
+ const id = nav.selectedId;
1609
+ if (id && !isHeaderId(id)) {
1610
+ multiSelect.toggle(id);
1611
+ }
1612
+ return;
1613
+ }
1614
+ if (key.return) {
1615
+ if (multiSelect.count > 0) {
1616
+ ui.enterBulkAction();
1617
+ }
1618
+ return;
1619
+ }
1620
+ if (input2 === "m" && multiSelect.count > 0) {
1621
+ ui.enterBulkAction();
1622
+ return;
1623
+ }
1624
+ return;
1625
+ }
1626
+ if (input2 === "d") {
1627
+ if (handleErrorAction("dismiss")) return;
1628
+ }
1629
+ if (input2 === "r" && handleErrorAction("retry")) return;
1630
+ if (ui.canAct) {
1631
+ if (input2 === "/") {
1632
+ multiSelect.clear();
1633
+ ui.enterSearch();
1634
+ return;
1635
+ }
1636
+ if (input2 === "q") {
1637
+ exit();
1638
+ return;
1639
+ }
1640
+ if (input2 === "r" || input2 === "R") {
1641
+ multiSelect.clear();
1642
+ refresh();
1643
+ return;
1644
+ }
1645
+ if (input2 === "s") {
1646
+ handleSlack();
1647
+ return;
1648
+ }
1649
+ if (input2 === "y") {
1650
+ handleCopyLink();
1651
+ return;
1652
+ }
1653
+ if (input2 === "p") {
1654
+ handlePick();
1655
+ return;
1656
+ }
1657
+ if (input2 === "a") {
1658
+ handleAssign();
1659
+ return;
1660
+ }
1661
+ if (input2 === "u") {
1662
+ handleUnassign();
1663
+ return;
1664
+ }
1665
+ if (input2 === "c") {
1666
+ if (selectedIssue) {
1667
+ multiSelect.clear();
1668
+ ui.enterComment();
1669
+ }
1670
+ return;
1671
+ }
1672
+ if (input2 === "m") {
1673
+ if (selectedIssue && selectedRepoStatusOptionsLength > 0) {
1674
+ multiSelect.clear();
1675
+ ui.enterStatus();
1676
+ } else if (selectedIssue) {
1677
+ toastInfo("Issue not in a project board");
1678
+ }
1679
+ return;
1680
+ }
1681
+ if (input2 === "n") {
1682
+ multiSelect.clear();
1683
+ ui.enterCreate();
1684
+ return;
1685
+ }
1686
+ if (input2 === "f") {
1687
+ handleEnterFocus();
1688
+ return;
1689
+ }
1690
+ if (input2 === "C") {
1691
+ nav.collapseAll();
1692
+ return;
1693
+ }
1694
+ if (input2 === "l") {
1695
+ if (selectedIssue) {
1696
+ multiSelect.clear();
1697
+ handleEnterLabel();
1698
+ }
1699
+ return;
1700
+ }
1701
+ if (input2 === "I") {
1702
+ handleEnterCreateNl();
1703
+ return;
1704
+ }
1705
+ if (input2 === " ") {
1706
+ const id = nav.selectedId;
1707
+ if (id && !isHeaderId(id)) {
1708
+ multiSelect.toggle(id);
1709
+ ui.enterMultiSelect();
1710
+ } else if (isHeaderId(nav.selectedId)) {
1711
+ nav.toggleSection();
1712
+ }
1713
+ return;
1714
+ }
1715
+ if (key.return) {
1716
+ if (isHeaderId(nav.selectedId)) {
1717
+ nav.toggleSection();
1718
+ return;
1719
+ }
1720
+ handleOpen();
1721
+ return;
1722
+ }
1723
+ }
1724
+ },
1725
+ [
1726
+ ui,
1727
+ nav,
1728
+ exit,
1729
+ refresh,
1730
+ handleSlack,
1731
+ handleCopyLink,
1732
+ handleOpen,
1733
+ handlePick,
1734
+ handleAssign,
1735
+ handleUnassign,
1736
+ handleEnterLabel,
1737
+ handleEnterCreateNl,
1738
+ selectedIssue,
1739
+ selectedRepoStatusOptionsLength,
1740
+ toastInfo,
1741
+ nav.selectedId,
1742
+ multiSelect,
1743
+ handleEnterFocus,
1744
+ handleErrorAction
1745
+ ]
1746
+ );
1747
+ const inputActive = ui.state.mode === "normal" || ui.state.mode === "multiSelect" || ui.state.mode === "focus";
1748
+ useInput(handleInput, { isActive: inputActive });
1749
+ const handleSearchEscape = useCallback3(
1750
+ (_input, key) => {
1751
+ if (key.escape) {
1752
+ onSearchEscape();
1753
+ }
1754
+ },
1755
+ [onSearchEscape]
1756
+ );
1757
+ useInput(handleSearchEscape, { isActive: ui.state.mode === "search" });
1758
+ }
1759
+ var init_use_keyboard = __esm({
1760
+ "src/board/hooks/use-keyboard.ts"() {
1761
+ "use strict";
1762
+ }
1763
+ });
1764
+
1328
1765
  // src/board/hooks/use-multi-select.ts
1329
- import { useCallback as useCallback3, useRef as useRef3, useState as useState2 } from "react";
1766
+ import { useCallback as useCallback4, useRef as useRef3, useState as useState2 } from "react";
1330
1767
  function useMultiSelect(getRepoForId) {
1331
1768
  const [selected, setSelected] = useState2(/* @__PURE__ */ new Set());
1332
1769
  const repoRef = useRef3(null);
1333
1770
  const getRepoRef = useRef3(getRepoForId);
1334
1771
  getRepoRef.current = getRepoForId;
1335
- const toggle = useCallback3((id) => {
1772
+ const toggle = useCallback4((id) => {
1336
1773
  setSelected((prev) => {
1337
1774
  const repo = getRepoRef.current(id);
1338
1775
  if (!repo) return prev;
@@ -1350,11 +1787,11 @@ function useMultiSelect(getRepoForId) {
1350
1787
  return next;
1351
1788
  });
1352
1789
  }, []);
1353
- const clear = useCallback3(() => {
1790
+ const clear = useCallback4(() => {
1354
1791
  setSelected(/* @__PURE__ */ new Set());
1355
1792
  repoRef.current = null;
1356
1793
  }, []);
1357
- const prune = useCallback3((validIds) => {
1794
+ const prune = useCallback4((validIds) => {
1358
1795
  setSelected((prev) => {
1359
1796
  const next = /* @__PURE__ */ new Set();
1360
1797
  for (const id of prev) {
@@ -1365,7 +1802,7 @@ function useMultiSelect(getRepoForId) {
1365
1802
  return next;
1366
1803
  });
1367
1804
  }, []);
1368
- const isSelected = useCallback3((id) => selected.has(id), [selected]);
1805
+ const isSelected = useCallback4((id) => selected.has(id), [selected]);
1369
1806
  return {
1370
1807
  selected,
1371
1808
  count: selected.size,
@@ -1383,7 +1820,7 @@ var init_use_multi_select = __esm({
1383
1820
  });
1384
1821
 
1385
1822
  // src/board/hooks/use-navigation.ts
1386
- import { useCallback as useCallback4, useMemo, useReducer, useRef as useRef4 } from "react";
1823
+ import { useCallback as useCallback5, useMemo, useReducer, useRef as useRef4 } from "react";
1387
1824
  function arraysEqual(a, b) {
1388
1825
  if (a.length !== b.length) return false;
1389
1826
  for (let i = 0; i < a.length; i++) {
@@ -1405,7 +1842,7 @@ function navReducer(state, action) {
1405
1842
  case "SET_ITEMS": {
1406
1843
  const sections = [...new Set(action.items.map((i) => i.section))];
1407
1844
  const isFirstLoad = state.sections.length === 0;
1408
- const collapsedSections = isFirstLoad ? new Set(sections) : state.collapsedSections;
1845
+ const collapsedSections = isFirstLoad ? new Set(sections.filter((s) => s === "activity")) : state.collapsedSections;
1409
1846
  const selectionValid = state.selectedId != null && action.items.some((i) => i.id === state.selectedId);
1410
1847
  if (!isFirstLoad && selectionValid && arraysEqual(sections, state.sections)) {
1411
1848
  return state;
@@ -1443,6 +1880,9 @@ function navReducer(state, action) {
1443
1880
  }
1444
1881
  return { ...state, collapsedSections: next };
1445
1882
  }
1883
+ case "COLLAPSE_ALL": {
1884
+ return { ...state, collapsedSections: new Set(state.sections) };
1885
+ }
1446
1886
  default:
1447
1887
  return state;
1448
1888
  }
@@ -1477,17 +1917,17 @@ function useNavigation(allItems) {
1477
1917
  const idx = visibleItems.findIndex((i) => i.id === state.selectedId);
1478
1918
  return idx >= 0 ? idx : 0;
1479
1919
  }, [state.selectedId, visibleItems]);
1480
- const moveUp = useCallback4(() => {
1920
+ const moveUp = useCallback5(() => {
1481
1921
  const newIdx = Math.max(0, selectedIndex - 1);
1482
1922
  const item = visibleItems[newIdx];
1483
1923
  if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
1484
1924
  }, [selectedIndex, visibleItems]);
1485
- const moveDown = useCallback4(() => {
1925
+ const moveDown = useCallback5(() => {
1486
1926
  const newIdx = Math.min(visibleItems.length - 1, selectedIndex + 1);
1487
1927
  const item = visibleItems[newIdx];
1488
1928
  if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
1489
1929
  }, [selectedIndex, visibleItems]);
1490
- const nextSection = useCallback4(() => {
1930
+ const nextSection = useCallback5(() => {
1491
1931
  const currentItem = visibleItems[selectedIndex];
1492
1932
  if (!currentItem) return;
1493
1933
  const currentSectionIdx = state.sections.indexOf(currentItem.section);
@@ -1496,7 +1936,7 @@ function useNavigation(allItems) {
1496
1936
  const header = visibleItems.find((i) => i.section === nextSectionId && i.type === "header");
1497
1937
  if (header) dispatch({ type: "SELECT", id: header.id, section: header.section });
1498
1938
  }, [selectedIndex, visibleItems, state.sections]);
1499
- const prevSection = useCallback4(() => {
1939
+ const prevSection = useCallback5(() => {
1500
1940
  const currentItem = visibleItems[selectedIndex];
1501
1941
  if (!currentItem) return;
1502
1942
  const currentSectionIdx = state.sections.indexOf(currentItem.section);
@@ -1505,19 +1945,22 @@ function useNavigation(allItems) {
1505
1945
  const header = visibleItems.find((i) => i.section === prevSectionId && i.type === "header");
1506
1946
  if (header) dispatch({ type: "SELECT", id: header.id, section: header.section });
1507
1947
  }, [selectedIndex, visibleItems, state.sections]);
1508
- const toggleSection = useCallback4(() => {
1948
+ const toggleSection = useCallback5(() => {
1509
1949
  const currentItem = visibleItems[selectedIndex];
1510
1950
  if (!currentItem) return;
1511
1951
  const key = currentItem.type === "subHeader" ? currentItem.id : currentItem.section;
1512
1952
  dispatch({ type: "TOGGLE_SECTION", section: key });
1513
1953
  }, [selectedIndex, visibleItems]);
1954
+ const collapseAll = useCallback5(() => {
1955
+ dispatch({ type: "COLLAPSE_ALL" });
1956
+ }, []);
1514
1957
  const allItemsRef = useRef4(allItems);
1515
1958
  allItemsRef.current = allItems;
1516
- const select2 = useCallback4((id) => {
1959
+ const select2 = useCallback5((id) => {
1517
1960
  const item = allItemsRef.current.find((i) => i.id === id);
1518
1961
  dispatch({ type: "SELECT", id, section: item?.section });
1519
1962
  }, []);
1520
- const isCollapsed = useCallback4(
1963
+ const isCollapsed = useCallback5(
1521
1964
  (section) => state.collapsedSections.has(section),
1522
1965
  [state.collapsedSections]
1523
1966
  );
@@ -1530,6 +1973,7 @@ function useNavigation(allItems) {
1530
1973
  nextSection,
1531
1974
  prevSection,
1532
1975
  toggleSection,
1976
+ collapseAll,
1533
1977
  select: select2,
1534
1978
  isCollapsed
1535
1979
  };
@@ -1541,25 +1985,25 @@ var init_use_navigation = __esm({
1541
1985
  });
1542
1986
 
1543
1987
  // src/board/hooks/use-toast.ts
1544
- import { useCallback as useCallback5, useRef as useRef5, useState as useState3 } from "react";
1988
+ import { useCallback as useCallback6, useRef as useRef5, useState as useState3 } from "react";
1545
1989
  function useToast() {
1546
1990
  const [toasts, setToasts] = useState3([]);
1547
1991
  const timersRef = useRef5(/* @__PURE__ */ new Map());
1548
- const clearTimer = useCallback5((id) => {
1992
+ const clearTimer = useCallback6((id) => {
1549
1993
  const timer = timersRef.current.get(id);
1550
1994
  if (timer) {
1551
1995
  clearTimeout(timer);
1552
1996
  timersRef.current.delete(id);
1553
1997
  }
1554
1998
  }, []);
1555
- const removeToast = useCallback5(
1999
+ const removeToast = useCallback6(
1556
2000
  (id) => {
1557
2001
  clearTimer(id);
1558
2002
  setToasts((prev) => prev.filter((t) => t.id !== id));
1559
2003
  },
1560
2004
  [clearTimer]
1561
2005
  );
1562
- const addToast = useCallback5(
2006
+ const addToast = useCallback6(
1563
2007
  (t) => {
1564
2008
  const id = `toast-${++nextId}`;
1565
2009
  const newToast = { ...t, id, createdAt: Date.now() };
@@ -1588,25 +2032,25 @@ function useToast() {
1588
2032
  [removeToast, clearTimer]
1589
2033
  );
1590
2034
  const toast = {
1591
- info: useCallback5(
2035
+ info: useCallback6(
1592
2036
  (message) => {
1593
2037
  addToast({ type: "info", message });
1594
2038
  },
1595
2039
  [addToast]
1596
2040
  ),
1597
- success: useCallback5(
2041
+ success: useCallback6(
1598
2042
  (message) => {
1599
2043
  addToast({ type: "success", message });
1600
2044
  },
1601
2045
  [addToast]
1602
2046
  ),
1603
- error: useCallback5(
2047
+ error: useCallback6(
1604
2048
  (message, retry) => {
1605
2049
  addToast(retry ? { type: "error", message, retry } : { type: "error", message });
1606
2050
  },
1607
2051
  [addToast]
1608
2052
  ),
1609
- loading: useCallback5(
2053
+ loading: useCallback6(
1610
2054
  (message) => {
1611
2055
  const id = addToast({ type: "loading", message });
1612
2056
  return {
@@ -1623,20 +2067,20 @@ function useToast() {
1623
2067
  [addToast, removeToast]
1624
2068
  )
1625
2069
  };
1626
- const dismiss = useCallback5(
2070
+ const dismiss = useCallback6(
1627
2071
  (id) => {
1628
2072
  removeToast(id);
1629
2073
  },
1630
2074
  [removeToast]
1631
2075
  );
1632
- const dismissAll = useCallback5(() => {
2076
+ const dismissAll = useCallback6(() => {
1633
2077
  for (const timer of timersRef.current.values()) {
1634
2078
  clearTimeout(timer);
1635
2079
  }
1636
2080
  timersRef.current.clear();
1637
2081
  setToasts([]);
1638
2082
  }, []);
1639
- const handleErrorAction = useCallback5(
2083
+ const handleErrorAction = useCallback6(
1640
2084
  (action) => {
1641
2085
  const errorToast = toasts.find((t) => t.type === "error");
1642
2086
  if (!errorToast) return false;
@@ -1666,7 +2110,7 @@ var init_use_toast = __esm({
1666
2110
  });
1667
2111
 
1668
2112
  // src/board/hooks/use-ui-state.ts
1669
- import { useCallback as useCallback6, useReducer as useReducer2 } from "react";
2113
+ import { useCallback as useCallback7, useReducer as useReducer2 } from "react";
1670
2114
  function uiReducer(state, action) {
1671
2115
  switch (action.type) {
1672
2116
  case "ENTER_SEARCH":
@@ -1685,6 +2129,12 @@ function uiReducer(state, action) {
1685
2129
  case "ENTER_CREATE":
1686
2130
  if (state.mode !== "normal") return state;
1687
2131
  return { ...state, mode: "overlay:create", previousMode: "normal" };
2132
+ case "ENTER_CREATE_NL":
2133
+ if (state.mode !== "normal") return state;
2134
+ return { ...state, mode: "overlay:createNl", previousMode: "normal" };
2135
+ case "ENTER_LABEL":
2136
+ if (state.mode !== "normal") return state;
2137
+ return { ...state, mode: "overlay:label", previousMode: "normal" };
1688
2138
  case "ENTER_MULTI_SELECT":
1689
2139
  if (state.mode !== "normal" && state.mode !== "multiSelect") return state;
1690
2140
  return { ...state, mode: "multiSelect", previousMode: "normal" };
@@ -1728,18 +2178,20 @@ function useUIState() {
1728
2178
  const [state, dispatch] = useReducer2(uiReducer, INITIAL_STATE2);
1729
2179
  return {
1730
2180
  state,
1731
- enterSearch: useCallback6(() => dispatch({ type: "ENTER_SEARCH" }), []),
1732
- enterComment: useCallback6(() => dispatch({ type: "ENTER_COMMENT" }), []),
1733
- enterStatus: useCallback6(() => dispatch({ type: "ENTER_STATUS" }), []),
1734
- enterCreate: useCallback6(() => dispatch({ type: "ENTER_CREATE" }), []),
1735
- enterMultiSelect: useCallback6(() => dispatch({ type: "ENTER_MULTI_SELECT" }), []),
1736
- enterBulkAction: useCallback6(() => dispatch({ type: "ENTER_BULK_ACTION" }), []),
1737
- enterConfirmPick: useCallback6(() => dispatch({ type: "ENTER_CONFIRM_PICK" }), []),
1738
- enterFocus: useCallback6(() => dispatch({ type: "ENTER_FOCUS" }), []),
1739
- toggleHelp: useCallback6(() => dispatch({ type: "TOGGLE_HELP" }), []),
1740
- exitOverlay: useCallback6(() => dispatch({ type: "EXIT_OVERLAY" }), []),
1741
- exitToNormal: useCallback6(() => dispatch({ type: "EXIT_TO_NORMAL" }), []),
1742
- clearMultiSelect: useCallback6(() => dispatch({ type: "CLEAR_MULTI_SELECT" }), []),
2181
+ enterSearch: useCallback7(() => dispatch({ type: "ENTER_SEARCH" }), []),
2182
+ enterComment: useCallback7(() => dispatch({ type: "ENTER_COMMENT" }), []),
2183
+ enterStatus: useCallback7(() => dispatch({ type: "ENTER_STATUS" }), []),
2184
+ enterCreate: useCallback7(() => dispatch({ type: "ENTER_CREATE" }), []),
2185
+ enterCreateNl: useCallback7(() => dispatch({ type: "ENTER_CREATE_NL" }), []),
2186
+ enterLabel: useCallback7(() => dispatch({ type: "ENTER_LABEL" }), []),
2187
+ enterMultiSelect: useCallback7(() => dispatch({ type: "ENTER_MULTI_SELECT" }), []),
2188
+ enterBulkAction: useCallback7(() => dispatch({ type: "ENTER_BULK_ACTION" }), []),
2189
+ enterConfirmPick: useCallback7(() => dispatch({ type: "ENTER_CONFIRM_PICK" }), []),
2190
+ enterFocus: useCallback7(() => dispatch({ type: "ENTER_FOCUS" }), []),
2191
+ toggleHelp: useCallback7(() => dispatch({ type: "TOGGLE_HELP" }), []),
2192
+ exitOverlay: useCallback7(() => dispatch({ type: "EXIT_OVERLAY" }), []),
2193
+ exitToNormal: useCallback7(() => dispatch({ type: "EXIT_TO_NORMAL" }), []),
2194
+ clearMultiSelect: useCallback7(() => dispatch({ type: "CLEAR_MULTI_SELECT" }), []),
1743
2195
  canNavigate: canNavigate(state),
1744
2196
  canAct: canAct(state),
1745
2197
  isOverlay: isOverlay(state)
@@ -1757,37 +2209,205 @@ var init_use_ui_state = __esm({
1757
2209
  }
1758
2210
  });
1759
2211
 
1760
- // src/board/components/bulk-action-menu.tsx
1761
- import { Box, Text, useInput } from "ink";
1762
- import { useState as useState4 } from "react";
1763
- import { jsx, jsxs } from "react/jsx-runtime";
1764
- function getMenuItems(selectionType) {
1765
- if (selectionType === "github") {
1766
- return [
1767
- { label: "Assign all to me", action: { type: "assign" } },
1768
- { label: "Unassign all from me", action: { type: "unassign" } },
1769
- { label: "Move status (all)", action: { type: "statusChange" } }
1770
- ];
1771
- }
1772
- if (selectionType === "ticktick") {
1773
- return [
1774
- { label: "Complete all", action: { type: "complete" } },
1775
- { label: "Delete all", action: { type: "delete" } }
1776
- ];
1777
- }
1778
- return [];
2212
+ // src/board/components/detail-panel.tsx
2213
+ import { Box, Text } from "ink";
2214
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2215
+ function truncateLines(text, maxLines) {
2216
+ const lines = text.split("\n").slice(0, maxLines);
2217
+ return lines.join("\n");
1779
2218
  }
1780
- function BulkActionMenu({ count, selectionType, onSelect, onCancel }) {
1781
- const items = getMenuItems(selectionType);
1782
- const [selectedIdx, setSelectedIdx] = useState4(0);
1783
- useInput((input2, key) => {
1784
- if (key.escape) return onCancel();
1785
- if (key.return) {
1786
- const item = items[selectedIdx];
1787
- if (item) onSelect(item.action);
1788
- return;
1789
- }
1790
- if (input2 === "j" || key.downArrow) {
2219
+ function stripMarkdown(text) {
2220
+ return text.replace(/^#{1,6}\s+/gm, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/_(.+?)_/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`{1,3}[^`]*`{1,3}/g, (m) => m.replace(/`/g, "")).replace(/^\s*[-*+]\s+/gm, " - ").replace(/^\s*\d+\.\s+/gm, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "[$1]").replace(/^>\s+/gm, " ").replace(/---+/g, "").replace(/\n{3,}/g, "\n\n").trim();
2221
+ }
2222
+ function formatBody(body, maxLines) {
2223
+ const plain = stripMarkdown(body);
2224
+ const lines = plain.split("\n");
2225
+ const truncated = lines.slice(0, maxLines).join("\n");
2226
+ return { text: truncated, remaining: Math.max(0, lines.length - maxLines) };
2227
+ }
2228
+ function countSlackLinks(body) {
2229
+ if (!body) return 0;
2230
+ return (body.match(SLACK_URL_RE) ?? []).length;
2231
+ }
2232
+ function BodySection({
2233
+ body,
2234
+ issueNumber
2235
+ }) {
2236
+ const { text, remaining } = formatBody(body, 15);
2237
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2238
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2239
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "--- Description ---" }),
2240
+ /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: text }),
2241
+ remaining > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2242
+ "... (",
2243
+ remaining,
2244
+ " more lines \u2014 gh issue view ",
2245
+ issueNumber,
2246
+ " for full)"
2247
+ ] }) : null
2248
+ ] });
2249
+ }
2250
+ function DetailPanel({ issue, task: task2, width }) {
2251
+ if (!(issue || task2)) {
2252
+ return /* @__PURE__ */ jsx(
2253
+ Box,
2254
+ {
2255
+ width,
2256
+ borderStyle: "single",
2257
+ borderColor: "gray",
2258
+ flexDirection: "column",
2259
+ paddingX: 1,
2260
+ children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "No item selected" })
2261
+ }
2262
+ );
2263
+ }
2264
+ if (issue) {
2265
+ return /* @__PURE__ */ jsxs(
2266
+ Box,
2267
+ {
2268
+ width,
2269
+ borderStyle: "single",
2270
+ borderColor: "cyan",
2271
+ flexDirection: "column",
2272
+ paddingX: 1,
2273
+ children: [
2274
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
2275
+ "#",
2276
+ issue.number,
2277
+ " ",
2278
+ issue.title
2279
+ ] }),
2280
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2281
+ /* @__PURE__ */ jsxs(Box, { children: [
2282
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "State: " }),
2283
+ /* @__PURE__ */ jsx(Text, { color: issue.state === "open" ? "green" : "red", children: issue.state })
2284
+ ] }),
2285
+ (issue.assignees ?? []).length > 0 ? /* @__PURE__ */ jsxs(Box, { children: [
2286
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Assignees: " }),
2287
+ /* @__PURE__ */ jsx(Text, { children: (issue.assignees ?? []).map((a) => a.login).join(", ") })
2288
+ ] }) : null,
2289
+ issue.labels.length > 0 ? /* @__PURE__ */ jsxs(Box, { children: [
2290
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Labels: " }),
2291
+ /* @__PURE__ */ jsx(Text, { children: issue.labels.map((l) => l.name).join(", ") })
2292
+ ] }) : null,
2293
+ issue.projectStatus ? /* @__PURE__ */ jsxs(Box, { children: [
2294
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Status: " }),
2295
+ /* @__PURE__ */ jsx(Text, { color: "magenta", children: issue.projectStatus })
2296
+ ] }) : null,
2297
+ issue.targetDate ? /* @__PURE__ */ jsxs(Box, { children: [
2298
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Target: " }),
2299
+ /* @__PURE__ */ jsx(Text, { children: issue.targetDate })
2300
+ ] }) : null,
2301
+ /* @__PURE__ */ jsxs(Box, { children: [
2302
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Updated: " }),
2303
+ /* @__PURE__ */ jsx(Text, { children: new Date(issue.updatedAt).toLocaleString() })
2304
+ ] }),
2305
+ issue.slackThreadUrl ? /* @__PURE__ */ jsxs(Box, { children: [
2306
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Slack: " }),
2307
+ /* @__PURE__ */ jsx(Text, { color: "blue", children: countSlackLinks(issue.body) > 1 ? `${countSlackLinks(issue.body)} links (s opens first)` : "thread (s to open)" })
2308
+ ] }) : null,
2309
+ issue.body ? /* @__PURE__ */ jsx(BodySection, { body: issue.body, issueNumber: issue.number }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2310
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2311
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "(no description)" })
2312
+ ] }),
2313
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2314
+ /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: issue.url })
2315
+ ]
2316
+ }
2317
+ );
2318
+ }
2319
+ const t = task2;
2320
+ return /* @__PURE__ */ jsxs(
2321
+ Box,
2322
+ {
2323
+ width,
2324
+ borderStyle: "single",
2325
+ borderColor: "yellow",
2326
+ flexDirection: "column",
2327
+ paddingX: 1,
2328
+ children: [
2329
+ /* @__PURE__ */ jsx(Text, { color: "yellow", bold: true, children: t.title }),
2330
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2331
+ /* @__PURE__ */ jsxs(Box, { children: [
2332
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Priority: " }),
2333
+ /* @__PURE__ */ jsx(Text, { children: PRIORITY_LABELS2[t.priority] ?? "None" })
2334
+ ] }),
2335
+ t.dueDate ? /* @__PURE__ */ jsxs(Box, { children: [
2336
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Due: " }),
2337
+ /* @__PURE__ */ jsx(Text, { children: new Date(t.dueDate).toLocaleDateString() })
2338
+ ] }) : null,
2339
+ (t.tags ?? []).length > 0 ? /* @__PURE__ */ jsxs(Box, { children: [
2340
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Tags: " }),
2341
+ /* @__PURE__ */ jsx(Text, { children: t.tags.join(", ") })
2342
+ ] }) : null,
2343
+ t.content ? /* @__PURE__ */ jsxs(Fragment, { children: [
2344
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2345
+ /* @__PURE__ */ jsx(Text, { children: truncateLines(t.content, 8) })
2346
+ ] }) : null,
2347
+ (t.items ?? []).length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
2348
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2349
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Checklist:" }),
2350
+ t.items.slice(0, 5).map((item) => /* @__PURE__ */ jsxs(Text, { children: [
2351
+ item.status === 2 ? "\u2611" : "\u2610",
2352
+ " ",
2353
+ item.title
2354
+ ] }, item.id)),
2355
+ t.items.length > 5 ? /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
2356
+ "...and ",
2357
+ t.items.length - 5,
2358
+ " more"
2359
+ ] }) : null
2360
+ ] }) : null
2361
+ ]
2362
+ }
2363
+ );
2364
+ }
2365
+ var SLACK_URL_RE, PRIORITY_LABELS2;
2366
+ var init_detail_panel = __esm({
2367
+ "src/board/components/detail-panel.tsx"() {
2368
+ "use strict";
2369
+ init_types();
2370
+ SLACK_URL_RE = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/gi;
2371
+ PRIORITY_LABELS2 = {
2372
+ [5 /* High */]: "High",
2373
+ [3 /* Medium */]: "Medium",
2374
+ [1 /* Low */]: "Low",
2375
+ [0 /* None */]: "None"
2376
+ };
2377
+ }
2378
+ });
2379
+
2380
+ // src/board/components/bulk-action-menu.tsx
2381
+ import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
2382
+ import { useState as useState4 } from "react";
2383
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2384
+ function getMenuItems(selectionType) {
2385
+ if (selectionType === "github") {
2386
+ return [
2387
+ { label: "Assign all to me", action: { type: "assign" } },
2388
+ { label: "Unassign all from me", action: { type: "unassign" } },
2389
+ { label: "Move status (all)", action: { type: "statusChange" } }
2390
+ ];
2391
+ }
2392
+ if (selectionType === "ticktick") {
2393
+ return [
2394
+ { label: "Complete all", action: { type: "complete" } },
2395
+ { label: "Delete all", action: { type: "delete" } }
2396
+ ];
2397
+ }
2398
+ return [];
2399
+ }
2400
+ function BulkActionMenu({ count, selectionType, onSelect, onCancel }) {
2401
+ const items = getMenuItems(selectionType);
2402
+ const [selectedIdx, setSelectedIdx] = useState4(0);
2403
+ useInput2((input2, key) => {
2404
+ if (key.escape) return onCancel();
2405
+ if (key.return) {
2406
+ const item = items[selectedIdx];
2407
+ if (item) onSelect(item.action);
2408
+ return;
2409
+ }
2410
+ if (input2 === "j" || key.downArrow) {
1791
2411
  setSelectedIdx((i) => Math.min(i + 1, items.length - 1));
1792
2412
  }
1793
2413
  if (input2 === "k" || key.upArrow) {
@@ -1795,13 +2415,13 @@ function BulkActionMenu({ count, selectionType, onSelect, onCancel }) {
1795
2415
  }
1796
2416
  });
1797
2417
  if (items.length === 0) {
1798
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1799
- /* @__PURE__ */ jsx(Text, { color: "yellow", children: "No bulk actions for mixed selection types." }),
1800
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Esc to cancel" })
2418
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2419
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "No bulk actions for mixed selection types." }),
2420
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Esc to cancel" })
1801
2421
  ] });
1802
2422
  }
1803
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1804
- /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
2423
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2424
+ /* @__PURE__ */ jsxs2(Text2, { color: "cyan", bold: true, children: [
1805
2425
  "Bulk action (",
1806
2426
  count,
1807
2427
  " selected):"
@@ -1809,12 +2429,12 @@ function BulkActionMenu({ count, selectionType, onSelect, onCancel }) {
1809
2429
  items.map((item, i) => {
1810
2430
  const isSelected = i === selectedIdx;
1811
2431
  const prefix = isSelected ? "> " : " ";
1812
- return /* @__PURE__ */ jsxs(Text, { ...isSelected ? { color: "cyan" } : {}, children: [
2432
+ return /* @__PURE__ */ jsxs2(Text2, { ...isSelected ? { color: "cyan" } : {}, children: [
1813
2433
  prefix,
1814
2434
  item.label
1815
2435
  ] }, item.action.type);
1816
2436
  }),
1817
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "j/k:navigate Enter:select Esc:cancel" })
2437
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "j/k:navigate Enter:select Esc:cancel" })
1818
2438
  ] });
1819
2439
  }
1820
2440
  var init_bulk_action_menu = __esm({
@@ -1823,27 +2443,113 @@ var init_bulk_action_menu = __esm({
1823
2443
  }
1824
2444
  });
1825
2445
 
2446
+ // src/board/ink-instance.ts
2447
+ function setInkInstance(instance) {
2448
+ _instance = instance;
2449
+ }
2450
+ function getInkInstance() {
2451
+ return _instance;
2452
+ }
2453
+ var _instance;
2454
+ var init_ink_instance = __esm({
2455
+ "src/board/ink-instance.ts"() {
2456
+ "use strict";
2457
+ _instance = null;
2458
+ }
2459
+ });
2460
+
1826
2461
  // src/board/components/comment-input.tsx
2462
+ import { spawnSync } from "child_process";
2463
+ import { mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "fs";
2464
+ import { tmpdir } from "os";
2465
+ import { join as join3 } from "path";
1827
2466
  import { TextInput } from "@inkjs/ui";
1828
- import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
1829
- import { useState as useState5 } from "react";
1830
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1831
- function CommentInput({ issueNumber, onSubmit, onCancel }) {
2467
+ import { Box as Box3, Text as Text3, useInput as useInput3, useStdin } from "ink";
2468
+ import { useEffect as useEffect2, useRef as useRef6, useState as useState5 } from "react";
2469
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2470
+ function CommentInput({
2471
+ issueNumber,
2472
+ onSubmit,
2473
+ onCancel,
2474
+ onPauseRefresh,
2475
+ onResumeRefresh
2476
+ }) {
1832
2477
  const [value, setValue] = useState5("");
1833
- useInput2((_input, key) => {
1834
- if (key.escape) onCancel();
2478
+ const [editing, setEditing] = useState5(false);
2479
+ const { setRawMode } = useStdin();
2480
+ const onSubmitRef = useRef6(onSubmit);
2481
+ const onCancelRef = useRef6(onCancel);
2482
+ const onPauseRef = useRef6(onPauseRefresh);
2483
+ const onResumeRef = useRef6(onResumeRefresh);
2484
+ onSubmitRef.current = onSubmit;
2485
+ onCancelRef.current = onCancel;
2486
+ onPauseRef.current = onPauseRefresh;
2487
+ onResumeRef.current = onResumeRefresh;
2488
+ useInput3((_input, key) => {
2489
+ if (editing) return;
2490
+ if (key.escape) {
2491
+ onCancel();
2492
+ return;
2493
+ }
2494
+ if (_input === "") {
2495
+ setEditing(true);
2496
+ }
1835
2497
  });
1836
- return /* @__PURE__ */ jsxs2(Box2, { children: [
1837
- /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
2498
+ useEffect2(() => {
2499
+ if (!editing) return;
2500
+ const editorEnv = process.env["VISUAL"] ?? process.env["EDITOR"] ?? "vi";
2501
+ const [cmd, ...extraArgs] = editorEnv.split(" ").filter(Boolean);
2502
+ if (!cmd) {
2503
+ setEditing(false);
2504
+ return;
2505
+ }
2506
+ let tmpDir = null;
2507
+ let tmpFile = null;
2508
+ try {
2509
+ onPauseRef.current?.();
2510
+ tmpDir = mkdtempSync(join3(tmpdir(), "hog-comment-"));
2511
+ tmpFile = join3(tmpDir, "comment.md");
2512
+ writeFileSync3(tmpFile, value);
2513
+ const inkInstance = getInkInstance();
2514
+ inkInstance?.clear();
2515
+ setRawMode(false);
2516
+ spawnSync(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
2517
+ const content = readFileSync3(tmpFile, "utf-8").trim();
2518
+ setRawMode(true);
2519
+ if (content) {
2520
+ onSubmitRef.current(content);
2521
+ } else {
2522
+ onCancelRef.current();
2523
+ }
2524
+ } finally {
2525
+ onResumeRef.current?.();
2526
+ if (tmpFile) {
2527
+ try {
2528
+ rmSync(tmpDir, { recursive: true, force: true });
2529
+ } catch {
2530
+ }
2531
+ }
2532
+ setEditing(false);
2533
+ }
2534
+ }, [editing, value, setRawMode]);
2535
+ if (editing) {
2536
+ return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
2537
+ "Opening editor for #",
2538
+ issueNumber,
2539
+ "\u2026"
2540
+ ] }) });
2541
+ }
2542
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
2543
+ /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
1838
2544
  "comment #",
1839
2545
  issueNumber,
1840
2546
  ": "
1841
2547
  ] }),
1842
- /* @__PURE__ */ jsx2(
2548
+ /* @__PURE__ */ jsx3(
1843
2549
  TextInput,
1844
2550
  {
1845
2551
  defaultValue: value,
1846
- placeholder: "type comment, Enter to post...",
2552
+ placeholder: "type comment (ctrl+e for editor), Enter to post...",
1847
2553
  onChange: setValue,
1848
2554
  onSubmit: (text) => {
1849
2555
  if (text.trim()) onSubmit(text.trim());
@@ -1856,20 +2562,21 @@ function CommentInput({ issueNumber, onSubmit, onCancel }) {
1856
2562
  var init_comment_input = __esm({
1857
2563
  "src/board/components/comment-input.tsx"() {
1858
2564
  "use strict";
2565
+ init_ink_instance();
1859
2566
  }
1860
2567
  });
1861
2568
 
1862
2569
  // src/board/components/confirm-prompt.tsx
1863
- import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
1864
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2570
+ import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
2571
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1865
2572
  function ConfirmPrompt({ message, onConfirm, onCancel }) {
1866
- useInput3((input2, key) => {
2573
+ useInput4((input2, key) => {
1867
2574
  if (input2 === "y" || input2 === "Y") return onConfirm();
1868
2575
  if (input2 === "n" || input2 === "N" || key.escape) return onCancel();
1869
2576
  });
1870
- return /* @__PURE__ */ jsxs3(Box3, { children: [
1871
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: message }),
1872
- /* @__PURE__ */ jsx3(Text3, { color: "gray", children: " (y/n)" })
2577
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
2578
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: message }),
2579
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " (y/n)" })
1873
2580
  ] });
1874
2581
  }
1875
2582
  var init_confirm_prompt = __esm({
@@ -1878,20 +2585,146 @@ var init_confirm_prompt = __esm({
1878
2585
  }
1879
2586
  });
1880
2587
 
2588
+ // src/board/components/label-picker.tsx
2589
+ import { Spinner } from "@inkjs/ui";
2590
+ import { Box as Box5, Text as Text5, useInput as useInput5 } from "ink";
2591
+ import { useEffect as useEffect3, useRef as useRef7, useState as useState6 } from "react";
2592
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2593
+ function LabelPicker({
2594
+ repo,
2595
+ currentLabels,
2596
+ labelCache,
2597
+ onConfirm,
2598
+ onCancel,
2599
+ onError
2600
+ }) {
2601
+ const [labels, setLabels] = useState6(labelCache[repo] ?? null);
2602
+ const [loading, setLoading] = useState6(labels === null);
2603
+ const [fetchAttempted, setFetchAttempted] = useState6(false);
2604
+ const [selected, setSelected] = useState6(new Set(currentLabels));
2605
+ const [cursor, setCursor] = useState6(0);
2606
+ const submittedRef = useRef7(false);
2607
+ useEffect3(() => {
2608
+ if (labels !== null || fetchAttempted) return;
2609
+ setFetchAttempted(true);
2610
+ setLoading(true);
2611
+ let canceled = false;
2612
+ fetchRepoLabelsAsync(repo).then((fetched) => {
2613
+ if (canceled) return;
2614
+ labelCache[repo] = fetched;
2615
+ setLabels(fetched);
2616
+ setLoading(false);
2617
+ }).catch(() => {
2618
+ if (canceled) return;
2619
+ setLoading(false);
2620
+ onError(`Could not fetch labels for ${repo}`);
2621
+ });
2622
+ return () => {
2623
+ canceled = true;
2624
+ };
2625
+ }, [repo, fetchAttempted, labelCache, onError]);
2626
+ useInput5((input2, key) => {
2627
+ if (loading) return;
2628
+ if (key.escape) {
2629
+ onCancel();
2630
+ return;
2631
+ }
2632
+ if (key.return) {
2633
+ if (submittedRef.current) return;
2634
+ submittedRef.current = true;
2635
+ const allLabels2 = labels ?? [];
2636
+ const add = [...selected].filter((l) => !currentLabels.includes(l));
2637
+ const remove = currentLabels.filter((l) => {
2638
+ const exists = allLabels2.some((rl) => rl.name === l);
2639
+ return exists && !selected.has(l);
2640
+ });
2641
+ onConfirm(add, remove);
2642
+ return;
2643
+ }
2644
+ if (input2 === " ") {
2645
+ const allLabels2 = labels ?? [];
2646
+ const item = allLabels2[cursor];
2647
+ if (!item) return;
2648
+ setSelected((prev) => {
2649
+ const next = new Set(prev);
2650
+ if (next.has(item.name)) {
2651
+ next.delete(item.name);
2652
+ } else {
2653
+ next.add(item.name);
2654
+ }
2655
+ return next;
2656
+ });
2657
+ return;
2658
+ }
2659
+ if (input2 === "j" || key.downArrow) {
2660
+ setCursor((i) => Math.min(i + 1, (labels?.length ?? 1) - 1));
2661
+ }
2662
+ if (input2 === "k" || key.upArrow) {
2663
+ setCursor((i) => Math.max(i - 1, 0));
2664
+ }
2665
+ });
2666
+ if (loading) {
2667
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Spinner, { label: "Fetching labels..." }) });
2668
+ }
2669
+ const allLabels = labels ?? [];
2670
+ if (allLabels.length === 0) {
2671
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2672
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", bold: true, children: "Labels:" }),
2673
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No labels in this repo" }),
2674
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Esc:cancel" })
2675
+ ] });
2676
+ }
2677
+ const repoLabelNames = new Set(allLabels.map((l) => l.name));
2678
+ const orphanedLabels = currentLabels.filter((l) => !repoLabelNames.has(l));
2679
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2680
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", bold: true, children: "Labels (Space:toggle Enter:confirm Esc:cancel):" }),
2681
+ orphanedLabels.map((name) => /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2682
+ selected.has(name) ? "[x]" : "[ ]",
2683
+ " ",
2684
+ name,
2685
+ " (orphaned)"
2686
+ ] }, `orphan:${name}`)),
2687
+ allLabels.map((label, i) => {
2688
+ const isSel = i === cursor;
2689
+ const isChecked = selected.has(label.name);
2690
+ return /* @__PURE__ */ jsxs5(Text5, { ...isSel ? { color: "cyan" } : {}, children: [
2691
+ isSel ? ">" : " ",
2692
+ " ",
2693
+ isChecked ? "[x]" : "[ ]",
2694
+ " ",
2695
+ label.name
2696
+ ] }, label.name);
2697
+ })
2698
+ ] });
2699
+ }
2700
+ var init_label_picker = __esm({
2701
+ "src/board/components/label-picker.tsx"() {
2702
+ "use strict";
2703
+ init_github();
2704
+ }
2705
+ });
2706
+
1881
2707
  // src/board/components/create-issue-form.tsx
1882
2708
  import { TextInput as TextInput2 } from "@inkjs/ui";
1883
- import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
1884
- import { useState as useState6 } from "react";
1885
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1886
- function CreateIssueForm({ repos, defaultRepo, onSubmit, onCancel }) {
2709
+ import { Box as Box6, Text as Text6, useInput as useInput6 } from "ink";
2710
+ import { useState as useState7 } from "react";
2711
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2712
+ function CreateIssueForm({
2713
+ repos,
2714
+ defaultRepo,
2715
+ onSubmit,
2716
+ onCancel,
2717
+ labelCache
2718
+ }) {
1887
2719
  const defaultRepoIdx = defaultRepo ? Math.max(
1888
2720
  0,
1889
2721
  repos.findIndex((r) => r.name === defaultRepo)
1890
2722
  ) : 0;
1891
- const [repoIdx, setRepoIdx] = useState6(defaultRepoIdx);
1892
- const [title, setTitle] = useState6("");
1893
- const [field, setField] = useState6("title");
1894
- useInput4((input2, key) => {
2723
+ const [repoIdx, setRepoIdx] = useState7(defaultRepoIdx);
2724
+ const [title, setTitle] = useState7("");
2725
+ const [field, setField] = useState7("title");
2726
+ useInput6((input2, key) => {
2727
+ if (field === "labels") return;
1895
2728
  if (key.escape) return onCancel();
1896
2729
  if (field === "repo") {
1897
2730
  if (input2 === "j" || key.downArrow) {
@@ -1905,12 +2738,40 @@ function CreateIssueForm({ repos, defaultRepo, onSubmit, onCancel }) {
1905
2738
  }
1906
2739
  });
1907
2740
  const selectedRepo = repos[repoIdx];
1908
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
1909
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "Create Issue" }),
1910
- /* @__PURE__ */ jsxs4(Box4, { children: [
1911
- /* @__PURE__ */ jsx4(Text4, { dimColor: field !== "repo", children: "Repo: " }),
1912
- repos.map((r, i) => /* @__PURE__ */ jsx4(
1913
- Text4,
2741
+ if (field === "labels" && selectedRepo) {
2742
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
2743
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", bold: true, children: "Create Issue \u2014 Add Labels (optional)" }),
2744
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2745
+ "Repo: ",
2746
+ selectedRepo.shortName,
2747
+ " Title: ",
2748
+ title
2749
+ ] }),
2750
+ /* @__PURE__ */ jsx6(
2751
+ LabelPicker,
2752
+ {
2753
+ repo: selectedRepo.name,
2754
+ currentLabels: [],
2755
+ labelCache: labelCache ?? {},
2756
+ onConfirm: (addLabels) => {
2757
+ onSubmit(selectedRepo.name, title, addLabels.length > 0 ? addLabels : void 0);
2758
+ },
2759
+ onCancel: () => {
2760
+ onSubmit(selectedRepo.name, title);
2761
+ },
2762
+ onError: () => {
2763
+ onSubmit(selectedRepo.name, title);
2764
+ }
2765
+ }
2766
+ )
2767
+ ] });
2768
+ }
2769
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
2770
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", bold: true, children: "Create Issue" }),
2771
+ /* @__PURE__ */ jsxs6(Box6, { children: [
2772
+ /* @__PURE__ */ jsx6(Text6, { dimColor: field !== "repo", children: "Repo: " }),
2773
+ repos.map((r, i) => /* @__PURE__ */ jsx6(
2774
+ Text6,
1914
2775
  {
1915
2776
  ...i === repoIdx ? { color: "cyan", bold: true } : {},
1916
2777
  dimColor: field !== "repo",
@@ -1918,215 +2779,53 @@ function CreateIssueForm({ repos, defaultRepo, onSubmit, onCancel }) {
1918
2779
  },
1919
2780
  r.name
1920
2781
  )),
1921
- field === "repo" ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " j/k:select Tab:next" }) : null
2782
+ field === "repo" ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " j/k:select Tab:next" }) : null
1922
2783
  ] }),
1923
- /* @__PURE__ */ jsxs4(Box4, { children: [
1924
- /* @__PURE__ */ jsx4(Text4, { dimColor: field !== "title", children: "Title: " }),
1925
- field === "title" ? /* @__PURE__ */ jsx4(
2784
+ /* @__PURE__ */ jsxs6(Box6, { children: [
2785
+ /* @__PURE__ */ jsx6(Text6, { dimColor: field !== "title", children: "Title: " }),
2786
+ field === "title" ? /* @__PURE__ */ jsx6(
1926
2787
  TextInput2,
1927
2788
  {
1928
2789
  defaultValue: title,
1929
2790
  placeholder: "issue title...",
1930
2791
  onChange: setTitle,
1931
2792
  onSubmit: (text) => {
1932
- if (text.trim() && selectedRepo) {
1933
- onSubmit(selectedRepo.name, text.trim());
2793
+ const trimmed = text.trim();
2794
+ if (!(trimmed && selectedRepo)) return;
2795
+ if (labelCache !== void 0) {
2796
+ setTitle(trimmed);
2797
+ setField("labels");
2798
+ } else {
2799
+ onSubmit(selectedRepo.name, trimmed);
1934
2800
  }
1935
2801
  }
1936
2802
  }
1937
- ) : /* @__PURE__ */ jsx4(Text4, { children: title || "(empty)" })
2803
+ ) : /* @__PURE__ */ jsx6(Text6, { children: title || "(empty)" })
1938
2804
  ] }),
1939
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Tab:switch fields Enter:submit Esc:cancel" })
2805
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Tab:switch fields Enter:next Esc:cancel" })
1940
2806
  ] });
1941
2807
  }
1942
2808
  var init_create_issue_form = __esm({
1943
2809
  "src/board/components/create-issue-form.tsx"() {
1944
2810
  "use strict";
1945
- }
1946
- });
1947
-
1948
- // src/board/components/detail-panel.tsx
1949
- import { Box as Box5, Text as Text5 } from "ink";
1950
- import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1951
- function truncateLines(text, maxLines) {
1952
- const lines = text.split("\n").slice(0, maxLines);
1953
- return lines.join("\n");
1954
- }
1955
- function stripMarkdown(text) {
1956
- return text.replace(/^#{1,6}\s+/gm, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/_(.+?)_/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`{1,3}[^`]*`{1,3}/g, (m) => m.replace(/`/g, "")).replace(/^\s*[-*+]\s+/gm, " - ").replace(/^\s*\d+\.\s+/gm, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "[$1]").replace(/^>\s+/gm, " ").replace(/---+/g, "").replace(/\n{3,}/g, "\n\n").trim();
1957
- }
1958
- function formatBody(body, maxLines) {
1959
- const plain = stripMarkdown(body);
1960
- const lines = plain.split("\n");
1961
- const truncated = lines.slice(0, maxLines).join("\n");
1962
- return { text: truncated, remaining: Math.max(0, lines.length - maxLines) };
1963
- }
1964
- function countSlackLinks(body) {
1965
- if (!body) return 0;
1966
- return (body.match(SLACK_URL_RE) ?? []).length;
1967
- }
1968
- function BodySection({
1969
- body,
1970
- issueNumber
1971
- }) {
1972
- const { text, remaining } = formatBody(body, 15);
1973
- return /* @__PURE__ */ jsxs5(Fragment, { children: [
1974
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
1975
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "--- Description ---" }),
1976
- /* @__PURE__ */ jsx5(Text5, { wrap: "wrap", children: text }),
1977
- remaining > 0 ? /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1978
- "... (",
1979
- remaining,
1980
- " more lines \u2014 gh issue view ",
1981
- issueNumber,
1982
- " for full)"
1983
- ] }) : null
1984
- ] });
1985
- }
1986
- function DetailPanel({ issue, task: task2, width }) {
1987
- if (!(issue || task2)) {
1988
- return /* @__PURE__ */ jsx5(
1989
- Box5,
1990
- {
1991
- width,
1992
- borderStyle: "single",
1993
- borderColor: "gray",
1994
- flexDirection: "column",
1995
- paddingX: 1,
1996
- children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "No item selected" })
1997
- }
1998
- );
1999
- }
2000
- if (issue) {
2001
- return /* @__PURE__ */ jsxs5(
2002
- Box5,
2003
- {
2004
- width,
2005
- borderStyle: "single",
2006
- borderColor: "cyan",
2007
- flexDirection: "column",
2008
- paddingX: 1,
2009
- children: [
2010
- /* @__PURE__ */ jsxs5(Text5, { color: "cyan", bold: true, children: [
2011
- "#",
2012
- issue.number,
2013
- " ",
2014
- issue.title
2015
- ] }),
2016
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2017
- /* @__PURE__ */ jsxs5(Box5, { children: [
2018
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "State: " }),
2019
- /* @__PURE__ */ jsx5(Text5, { color: issue.state === "open" ? "green" : "red", children: issue.state })
2020
- ] }),
2021
- (issue.assignees ?? []).length > 0 ? /* @__PURE__ */ jsxs5(Box5, { children: [
2022
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Assignees: " }),
2023
- /* @__PURE__ */ jsx5(Text5, { children: (issue.assignees ?? []).map((a) => a.login).join(", ") })
2024
- ] }) : null,
2025
- issue.labels.length > 0 ? /* @__PURE__ */ jsxs5(Box5, { children: [
2026
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Labels: " }),
2027
- /* @__PURE__ */ jsx5(Text5, { children: issue.labels.map((l) => l.name).join(", ") })
2028
- ] }) : null,
2029
- issue.projectStatus ? /* @__PURE__ */ jsxs5(Box5, { children: [
2030
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Status: " }),
2031
- /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: issue.projectStatus })
2032
- ] }) : null,
2033
- issue.targetDate ? /* @__PURE__ */ jsxs5(Box5, { children: [
2034
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Target: " }),
2035
- /* @__PURE__ */ jsx5(Text5, { children: issue.targetDate })
2036
- ] }) : null,
2037
- /* @__PURE__ */ jsxs5(Box5, { children: [
2038
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Updated: " }),
2039
- /* @__PURE__ */ jsx5(Text5, { children: new Date(issue.updatedAt).toLocaleString() })
2040
- ] }),
2041
- issue.slackThreadUrl ? /* @__PURE__ */ jsxs5(Box5, { children: [
2042
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Slack: " }),
2043
- /* @__PURE__ */ jsx5(Text5, { color: "blue", children: countSlackLinks(issue.body) > 1 ? `${countSlackLinks(issue.body)} links (s opens first)` : "thread (s to open)" })
2044
- ] }) : null,
2045
- issue.body ? /* @__PURE__ */ jsx5(BodySection, { body: issue.body, issueNumber: issue.number }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
2046
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2047
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "(no description)" })
2048
- ] }),
2049
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2050
- /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: issue.url })
2051
- ]
2052
- }
2053
- );
2054
- }
2055
- const t = task2;
2056
- return /* @__PURE__ */ jsxs5(
2057
- Box5,
2058
- {
2059
- width,
2060
- borderStyle: "single",
2061
- borderColor: "yellow",
2062
- flexDirection: "column",
2063
- paddingX: 1,
2064
- children: [
2065
- /* @__PURE__ */ jsx5(Text5, { color: "yellow", bold: true, children: t.title }),
2066
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2067
- /* @__PURE__ */ jsxs5(Box5, { children: [
2068
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Priority: " }),
2069
- /* @__PURE__ */ jsx5(Text5, { children: PRIORITY_LABELS2[t.priority] ?? "None" })
2070
- ] }),
2071
- t.dueDate ? /* @__PURE__ */ jsxs5(Box5, { children: [
2072
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Due: " }),
2073
- /* @__PURE__ */ jsx5(Text5, { children: new Date(t.dueDate).toLocaleDateString() })
2074
- ] }) : null,
2075
- (t.tags ?? []).length > 0 ? /* @__PURE__ */ jsxs5(Box5, { children: [
2076
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Tags: " }),
2077
- /* @__PURE__ */ jsx5(Text5, { children: t.tags.join(", ") })
2078
- ] }) : null,
2079
- t.content ? /* @__PURE__ */ jsxs5(Fragment, { children: [
2080
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2081
- /* @__PURE__ */ jsx5(Text5, { children: truncateLines(t.content, 8) })
2082
- ] }) : null,
2083
- (t.items ?? []).length > 0 ? /* @__PURE__ */ jsxs5(Fragment, { children: [
2084
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2085
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Checklist:" }),
2086
- t.items.slice(0, 5).map((item) => /* @__PURE__ */ jsxs5(Text5, { children: [
2087
- item.status === 2 ? "\u2611" : "\u2610",
2088
- " ",
2089
- item.title
2090
- ] }, item.id)),
2091
- t.items.length > 5 ? /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
2092
- "...and ",
2093
- t.items.length - 5,
2094
- " more"
2095
- ] }) : null
2096
- ] }) : null
2097
- ]
2098
- }
2099
- );
2100
- }
2101
- var SLACK_URL_RE, PRIORITY_LABELS2;
2102
- var init_detail_panel = __esm({
2103
- "src/board/components/detail-panel.tsx"() {
2104
- "use strict";
2105
- init_types();
2106
- SLACK_URL_RE = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/gi;
2107
- PRIORITY_LABELS2 = {
2108
- [5 /* High */]: "High",
2109
- [3 /* Medium */]: "Medium",
2110
- [1 /* Low */]: "Low",
2111
- [0 /* None */]: "None"
2112
- };
2811
+ init_label_picker();
2113
2812
  }
2114
2813
  });
2115
2814
 
2116
2815
  // src/board/components/focus-mode.tsx
2117
- import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
2118
- import { useCallback as useCallback7, useEffect as useEffect2, useRef as useRef6, useState as useState7 } from "react";
2119
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2816
+ import { Box as Box7, Text as Text7, useInput as useInput7 } from "ink";
2817
+ import { useCallback as useCallback8, useEffect as useEffect4, useRef as useRef8, useState as useState8 } from "react";
2818
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2120
2819
  function formatTime(secs) {
2121
2820
  const m = Math.floor(secs / 60);
2122
2821
  const s = secs % 60;
2123
2822
  return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
2124
2823
  }
2125
2824
  function FocusMode({ label, durationSec, onExit, onEndAction }) {
2126
- const [remaining, setRemaining] = useState7(durationSec);
2127
- const [timerDone, setTimerDone] = useState7(false);
2128
- const bellSentRef = useRef6(false);
2129
- useEffect2(() => {
2825
+ const [remaining, setRemaining] = useState8(durationSec);
2826
+ const [timerDone, setTimerDone] = useState8(false);
2827
+ const bellSentRef = useRef8(false);
2828
+ useEffect4(() => {
2130
2829
  if (timerDone) return;
2131
2830
  const interval = setInterval(() => {
2132
2831
  setRemaining((prev) => {
@@ -2140,13 +2839,13 @@ function FocusMode({ label, durationSec, onExit, onEndAction }) {
2140
2839
  }, 1e3);
2141
2840
  return () => clearInterval(interval);
2142
2841
  }, [timerDone]);
2143
- useEffect2(() => {
2842
+ useEffect4(() => {
2144
2843
  if (timerDone && !bellSentRef.current) {
2145
2844
  bellSentRef.current = true;
2146
2845
  process.stdout.write("\x07");
2147
2846
  }
2148
2847
  }, [timerDone]);
2149
- const handleInput = useCallback7(
2848
+ const handleInput = useCallback8(
2150
2849
  (input2, key) => {
2151
2850
  if (key.escape) {
2152
2851
  if (timerDone) {
@@ -2171,25 +2870,25 @@ function FocusMode({ label, durationSec, onExit, onEndAction }) {
2171
2870
  },
2172
2871
  [timerDone, onExit, onEndAction]
2173
2872
  );
2174
- useInput5(handleInput);
2873
+ useInput7(handleInput);
2175
2874
  if (timerDone) {
2176
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
2177
- /* @__PURE__ */ jsxs6(Box6, { children: [
2178
- /* @__PURE__ */ jsx6(Text6, { color: "green", bold: true, children: "Focus complete!" }),
2179
- /* @__PURE__ */ jsxs6(Text6, { color: "gray", children: [
2875
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
2876
+ /* @__PURE__ */ jsxs7(Box7, { children: [
2877
+ /* @__PURE__ */ jsx7(Text7, { color: "green", bold: true, children: "Focus complete!" }),
2878
+ /* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
2180
2879
  " ",
2181
2880
  label
2182
2881
  ] })
2183
2882
  ] }),
2184
- /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
2185
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "[c]" }),
2186
- /* @__PURE__ */ jsx6(Text6, { children: " Continue " }),
2187
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "[b]" }),
2188
- /* @__PURE__ */ jsx6(Text6, { children: " Break " }),
2189
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "[d]" }),
2190
- /* @__PURE__ */ jsx6(Text6, { children: " Done " }),
2191
- /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "[Esc]" }),
2192
- /* @__PURE__ */ jsx6(Text6, { children: " Exit" })
2883
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
2884
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "[c]" }),
2885
+ /* @__PURE__ */ jsx7(Text7, { children: " Continue " }),
2886
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "[b]" }),
2887
+ /* @__PURE__ */ jsx7(Text7, { children: " Break " }),
2888
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "[d]" }),
2889
+ /* @__PURE__ */ jsx7(Text7, { children: " Done " }),
2890
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "[Esc]" }),
2891
+ /* @__PURE__ */ jsx7(Text7, { children: " Exit" })
2193
2892
  ] })
2194
2893
  ] });
2195
2894
  }
@@ -2197,21 +2896,21 @@ function FocusMode({ label, durationSec, onExit, onEndAction }) {
2197
2896
  const barWidth = 20;
2198
2897
  const filled = Math.round(progress * barWidth);
2199
2898
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
2200
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
2201
- /* @__PURE__ */ jsxs6(Box6, { children: [
2202
- /* @__PURE__ */ jsxs6(Text6, { color: "magenta", bold: true, children: [
2899
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
2900
+ /* @__PURE__ */ jsxs7(Box7, { children: [
2901
+ /* @__PURE__ */ jsxs7(Text7, { color: "magenta", bold: true, children: [
2203
2902
  "Focus:",
2204
2903
  " "
2205
2904
  ] }),
2206
- /* @__PURE__ */ jsx6(Text6, { children: label })
2905
+ /* @__PURE__ */ jsx7(Text7, { children: label })
2207
2906
  ] }),
2208
- /* @__PURE__ */ jsxs6(Box6, { children: [
2209
- /* @__PURE__ */ jsx6(Text6, { color: "magenta", children: bar }),
2210
- /* @__PURE__ */ jsx6(Text6, { children: " " }),
2211
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: formatTime(remaining) }),
2212
- /* @__PURE__ */ jsx6(Text6, { color: "gray", children: " remaining" })
2907
+ /* @__PURE__ */ jsxs7(Box7, { children: [
2908
+ /* @__PURE__ */ jsx7(Text7, { color: "magenta", children: bar }),
2909
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
2910
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: formatTime(remaining) }),
2911
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", children: " remaining" })
2213
2912
  ] }),
2214
- /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: "Esc to exit focus" })
2913
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: "Esc to exit focus" })
2215
2914
  ] });
2216
2915
  }
2217
2916
  var init_focus_mode = __esm({
@@ -2221,29 +2920,29 @@ var init_focus_mode = __esm({
2221
2920
  });
2222
2921
 
2223
2922
  // src/board/components/help-overlay.tsx
2224
- import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
2225
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2923
+ import { Box as Box8, Text as Text8, useInput as useInput8 } from "ink";
2924
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2226
2925
  function HelpOverlay({ currentMode, onClose }) {
2227
- useInput6((_input, key) => {
2926
+ useInput8((_input, key) => {
2228
2927
  if (key.escape) onClose();
2229
2928
  });
2230
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2231
- /* @__PURE__ */ jsxs7(Box7, { justifyContent: "space-between", children: [
2232
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", bold: true, children: "Keyboard Shortcuts" }),
2233
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2929
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2930
+ /* @__PURE__ */ jsxs8(Box8, { justifyContent: "space-between", children: [
2931
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", bold: true, children: "Keyboard Shortcuts" }),
2932
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2234
2933
  "mode: ",
2235
2934
  currentMode
2236
2935
  ] })
2237
2936
  ] }),
2238
- /* @__PURE__ */ jsx7(Text7, { children: " " }),
2239
- SHORTCUTS.map((group) => /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
2240
- /* @__PURE__ */ jsx7(Text7, { color: "yellow", bold: true, children: group.category }),
2241
- group.items.map((item) => /* @__PURE__ */ jsxs7(Box7, { children: [
2242
- /* @__PURE__ */ jsx7(Box7, { width: 16, children: /* @__PURE__ */ jsx7(Text7, { color: "green", children: item.key }) }),
2243
- /* @__PURE__ */ jsx7(Text7, { children: item.desc })
2937
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
2938
+ SHORTCUTS.map((group) => /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, children: [
2939
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", bold: true, children: group.category }),
2940
+ group.items.map((item) => /* @__PURE__ */ jsxs8(Box8, { children: [
2941
+ /* @__PURE__ */ jsx8(Box8, { width: 16, children: /* @__PURE__ */ jsx8(Text8, { color: "green", children: item.key }) }),
2942
+ /* @__PURE__ */ jsx8(Text8, { children: item.desc })
2244
2943
  ] }, item.key))
2245
2944
  ] }, group.category)),
2246
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press ? or Esc to close" })
2945
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Press ? or Esc to close" })
2247
2946
  ] });
2248
2947
  }
2249
2948
  var SHORTCUTS;
@@ -2271,31 +2970,447 @@ var init_help_overlay = __esm({
2271
2970
  ]
2272
2971
  },
2273
2972
  {
2274
- category: "Actions",
2275
- items: [
2276
- { key: "p", desc: "Pick issue (assign + TickTick)" },
2277
- { key: "a", desc: "Assign to self" },
2278
- { key: "u", desc: "Unassign self" },
2279
- { key: "c", desc: "Comment on issue" },
2280
- { key: "m", desc: "Move status" },
2281
- { key: "s", desc: "Open Slack thread" },
2282
- { key: "n", desc: "Create new issue" }
2283
- ]
2973
+ category: "Actions",
2974
+ items: [
2975
+ { key: "p", desc: "Pick issue (assign + TickTick)" },
2976
+ { key: "a", desc: "Assign to self" },
2977
+ { key: "u", desc: "Unassign self" },
2978
+ { key: "c", desc: "Comment on issue" },
2979
+ { key: "m", desc: "Move status" },
2980
+ { key: "s", desc: "Open Slack thread" },
2981
+ { key: "y", desc: "Copy issue link to clipboard" },
2982
+ { key: "n", desc: "Create new issue" }
2983
+ ]
2984
+ },
2985
+ {
2986
+ category: "Board",
2987
+ items: [
2988
+ { key: "r", desc: "Refresh data" },
2989
+ { key: "q", desc: "Quit" }
2990
+ ]
2991
+ }
2992
+ ];
2993
+ }
2994
+ });
2995
+
2996
+ // src/board/components/nl-create-overlay.tsx
2997
+ import { Spinner as Spinner2, TextInput as TextInput3 } from "@inkjs/ui";
2998
+ import { Box as Box9, Text as Text9, useInput as useInput9 } from "ink";
2999
+ import { useCallback as useCallback9, useEffect as useEffect5, useRef as useRef9, useState as useState9 } from "react";
3000
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
3001
+ function NlCreateOverlay({
3002
+ repos,
3003
+ defaultRepoName,
3004
+ labelCache,
3005
+ onSubmit,
3006
+ onCancel,
3007
+ onLlmFallback
3008
+ }) {
3009
+ const [, setInput] = useState9("");
3010
+ const [isParsing, setIsParsing] = useState9(false);
3011
+ const [parsed, setParsed] = useState9(null);
3012
+ const [parseError, setParseError] = useState9(null);
3013
+ const [createError, setCreateError] = useState9(null);
3014
+ const submittedRef = useRef9(false);
3015
+ const parseParamsRef = useRef9(null);
3016
+ const defaultRepoIdx = defaultRepoName ? Math.max(
3017
+ 0,
3018
+ repos.findIndex((r) => r.name === defaultRepoName)
3019
+ ) : 0;
3020
+ const [repoIdx, setRepoIdx] = useState9(defaultRepoIdx);
3021
+ const selectedRepo = repos[repoIdx];
3022
+ useInput9((inputChar, key) => {
3023
+ if (isParsing) return;
3024
+ if (key.escape) {
3025
+ onCancel();
3026
+ return;
3027
+ }
3028
+ if (parsed) {
3029
+ if (key.return) {
3030
+ if (submittedRef.current) return;
3031
+ submittedRef.current = true;
3032
+ if (!selectedRepo) return;
3033
+ setCreateError(null);
3034
+ const labels = buildLabelList(parsed);
3035
+ onSubmit(selectedRepo.name, parsed.title, labels.length > 0 ? labels : void 0);
3036
+ return;
3037
+ }
3038
+ if (inputChar === "r") {
3039
+ setRepoIdx((i) => (i + 1) % repos.length);
3040
+ return;
3041
+ }
3042
+ }
3043
+ });
3044
+ const handleInputSubmit = useCallback9(
3045
+ (text) => {
3046
+ const trimmed = text.trim();
3047
+ if (!trimmed) return;
3048
+ const validLabels = selectedRepo ? (labelCache[selectedRepo.name] ?? []).map((l) => l.name) : [];
3049
+ parseParamsRef.current = { input: trimmed, validLabels };
3050
+ setInput(trimmed);
3051
+ setParseError(null);
3052
+ setIsParsing(true);
3053
+ },
3054
+ [selectedRepo, labelCache]
3055
+ );
3056
+ useEffect5(() => {
3057
+ if (!(isParsing && parseParamsRef.current)) return;
3058
+ const { input: capturedInput, validLabels } = parseParamsRef.current;
3059
+ extractIssueFields(capturedInput, {
3060
+ validLabels,
3061
+ onLlmFallback
3062
+ }).then((result) => {
3063
+ if (!result) {
3064
+ setParseError("Title is required");
3065
+ setIsParsing(false);
3066
+ return;
3067
+ }
3068
+ const filteredLabels = validLabels.length > 0 ? result.labels.filter((l) => validLabels.includes(l)) : result.labels;
3069
+ setParsed({ ...result, labels: filteredLabels });
3070
+ setIsParsing(false);
3071
+ }).catch(() => {
3072
+ setParseError("Parsing failed \u2014 please try again");
3073
+ setIsParsing(false);
3074
+ });
3075
+ }, [isParsing, onLlmFallback]);
3076
+ if (isParsing) {
3077
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
3078
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: "\u2728 Creating Issue" }),
3079
+ /* @__PURE__ */ jsx9(Spinner2, { label: "Parsing..." })
3080
+ ] });
3081
+ }
3082
+ if (parsed) {
3083
+ const labels = buildLabelList(parsed);
3084
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
3085
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: "\u2728 Creating Issue" }),
3086
+ /* @__PURE__ */ jsxs9(Box9, { children: [
3087
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Repo: " }),
3088
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: selectedRepo?.shortName ?? "(none)" }),
3089
+ repos.length > 1 ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " r:cycle" }) : null
3090
+ ] }),
3091
+ /* @__PURE__ */ jsxs9(Box9, { children: [
3092
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Title: " }),
3093
+ /* @__PURE__ */ jsx9(Text9, { children: parsed.title })
3094
+ ] }),
3095
+ labels.length > 0 ? /* @__PURE__ */ jsxs9(Box9, { children: [
3096
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Labels: " }),
3097
+ /* @__PURE__ */ jsx9(Text9, { children: labels.join(", ") })
3098
+ ] }) : null,
3099
+ parsed.assignee ? /* @__PURE__ */ jsxs9(Box9, { children: [
3100
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Assignee: " }),
3101
+ /* @__PURE__ */ jsxs9(Text9, { children: [
3102
+ "@",
3103
+ parsed.assignee
3104
+ ] })
3105
+ ] }) : null,
3106
+ parsed.dueDate ? /* @__PURE__ */ jsxs9(Box9, { children: [
3107
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Due: " }),
3108
+ /* @__PURE__ */ jsx9(Text9, { children: formatDue(parsed.dueDate) })
3109
+ ] }) : null,
3110
+ parsed.dueDate && selectedRepo && !hasDueLabelInCache(labelCache, selectedRepo.name) ? /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u26A0 No due:* label in this repo \u2014 will try to create label on submit" }) : null,
3111
+ createError ? /* @__PURE__ */ jsx9(Text9, { color: "red", children: createError }) : null,
3112
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Enter:create Esc:cancel" })
3113
+ ] });
3114
+ }
3115
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
3116
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: "\u2728 What do you need to do?" }),
3117
+ /* @__PURE__ */ jsxs9(Box9, { children: [
3118
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "> " }),
3119
+ /* @__PURE__ */ jsx9(
3120
+ TextInput3,
3121
+ {
3122
+ placeholder: "fix login bug #bug #priority:high @me due friday",
3123
+ onChange: setInput,
3124
+ onSubmit: handleInputSubmit
3125
+ }
3126
+ )
3127
+ ] }),
3128
+ parseError ? /* @__PURE__ */ jsx9(Text9, { color: "red", children: parseError }) : null,
3129
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Tip: #label @user due <date> Enter:parse Esc:cancel" })
3130
+ ] });
3131
+ }
3132
+ function buildLabelList(parsed) {
3133
+ const labels = [...parsed.labels];
3134
+ if (parsed.dueDate) {
3135
+ labels.push(`due:${parsed.dueDate}`);
3136
+ }
3137
+ return labels;
3138
+ }
3139
+ function hasDueLabelInCache(labelCache, repoName) {
3140
+ return (labelCache[repoName] ?? []).some((l) => l.name.startsWith("due:"));
3141
+ }
3142
+ function formatDue(dueDate) {
3143
+ const d = /* @__PURE__ */ new Date(`${dueDate}T12:00:00`);
3144
+ const human = d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
3145
+ return `${human} (label: due:${dueDate})`;
3146
+ }
3147
+ var init_nl_create_overlay = __esm({
3148
+ "src/board/components/nl-create-overlay.tsx"() {
3149
+ "use strict";
3150
+ init_ai();
3151
+ }
3152
+ });
3153
+
3154
+ // src/board/components/search-bar.tsx
3155
+ import { TextInput as TextInput4 } from "@inkjs/ui";
3156
+ import { Box as Box10, Text as Text10 } from "ink";
3157
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
3158
+ function SearchBar({ defaultValue, onChange, onSubmit }) {
3159
+ return /* @__PURE__ */ jsxs10(Box10, { children: [
3160
+ /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: "/" }),
3161
+ /* @__PURE__ */ jsx10(
3162
+ TextInput4,
3163
+ {
3164
+ defaultValue,
3165
+ placeholder: "search...",
3166
+ onChange,
3167
+ onSubmit
3168
+ }
3169
+ )
3170
+ ] });
3171
+ }
3172
+ var init_search_bar = __esm({
3173
+ "src/board/components/search-bar.tsx"() {
3174
+ "use strict";
3175
+ }
3176
+ });
3177
+
3178
+ // src/board/components/status-picker.tsx
3179
+ import { Box as Box11, Text as Text11, useInput as useInput10 } from "ink";
3180
+ import { useRef as useRef10, useState as useState10 } from "react";
3181
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3182
+ function isTerminal(name) {
3183
+ return TERMINAL_STATUS_RE2.test(name);
3184
+ }
3185
+ function StatusPicker({
3186
+ options,
3187
+ currentStatus,
3188
+ onSelect,
3189
+ onCancel,
3190
+ showTerminalStatuses = true
3191
+ }) {
3192
+ const [selectedIdx, setSelectedIdx] = useState10(() => {
3193
+ const idx = options.findIndex((o) => o.name === currentStatus);
3194
+ return idx >= 0 ? idx : 0;
3195
+ });
3196
+ const [confirmingTerminal, setConfirmingTerminal] = useState10(false);
3197
+ const submittedRef = useRef10(false);
3198
+ useInput10((input2, key) => {
3199
+ if (confirmingTerminal) {
3200
+ if (input2 === "y" || input2 === "Y") {
3201
+ if (submittedRef.current) return;
3202
+ submittedRef.current = true;
3203
+ const opt = options[selectedIdx];
3204
+ if (opt) onSelect(opt.id);
3205
+ return;
3206
+ }
3207
+ if (input2 === "n" || input2 === "N" || key.escape) {
3208
+ setConfirmingTerminal(false);
3209
+ return;
3210
+ }
3211
+ return;
3212
+ }
3213
+ if (key.escape) return onCancel();
3214
+ if (key.return) {
3215
+ if (submittedRef.current) return;
3216
+ const opt = options[selectedIdx];
3217
+ if (!opt) return;
3218
+ if (isTerminal(opt.name) && showTerminalStatuses) {
3219
+ setConfirmingTerminal(true);
3220
+ return;
3221
+ }
3222
+ submittedRef.current = true;
3223
+ onSelect(opt.id);
3224
+ return;
3225
+ }
3226
+ if (input2 === "j" || key.downArrow) {
3227
+ setSelectedIdx((i) => Math.min(i + 1, options.length - 1));
3228
+ }
3229
+ if (input2 === "k" || key.upArrow) {
3230
+ setSelectedIdx((i) => Math.max(i - 1, 0));
3231
+ }
3232
+ });
3233
+ if (confirmingTerminal) {
3234
+ const opt = options[selectedIdx];
3235
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
3236
+ /* @__PURE__ */ jsxs11(Text11, { color: "yellow", bold: true, children: [
3237
+ "Mark as ",
3238
+ opt?.name,
3239
+ "?"
3240
+ ] }),
3241
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "This will close the issue on GitHub." }),
3242
+ /* @__PURE__ */ jsx11(Text11, { children: "Continue? [y/n]" })
3243
+ ] });
3244
+ }
3245
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
3246
+ /* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: "Move to status:" }),
3247
+ options.map((opt, i) => {
3248
+ const isCurrent = opt.name === currentStatus;
3249
+ const isSelected = i === selectedIdx;
3250
+ const terminal = isTerminal(opt.name) && showTerminalStatuses;
3251
+ const prefix = isSelected ? "> " : " ";
3252
+ const suffix = isCurrent ? " (current)" : terminal ? " (Done)" : "";
3253
+ return /* @__PURE__ */ jsxs11(
3254
+ Text11,
3255
+ {
3256
+ ...isSelected ? { color: "cyan" } : terminal ? { color: "yellow" } : {},
3257
+ dimColor: isCurrent,
3258
+ children: [
3259
+ prefix,
3260
+ opt.name,
3261
+ suffix
3262
+ ]
3263
+ },
3264
+ opt.id
3265
+ );
3266
+ }),
3267
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "j/k:navigate Enter:select Esc:cancel" })
3268
+ ] });
3269
+ }
3270
+ var TERMINAL_STATUS_RE2;
3271
+ var init_status_picker = __esm({
3272
+ "src/board/components/status-picker.tsx"() {
3273
+ "use strict";
3274
+ TERMINAL_STATUS_RE2 = /^(done|shipped|won't|wont|closed|complete|completed)$/i;
3275
+ }
3276
+ });
3277
+
3278
+ // src/board/components/overlay-renderer.tsx
3279
+ import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
3280
+ function OverlayRenderer({
3281
+ uiState,
3282
+ config: config2,
3283
+ selectedRepoStatusOptions,
3284
+ currentStatus,
3285
+ onStatusSelect,
3286
+ onExitOverlay,
3287
+ defaultRepo,
3288
+ onCreateIssue,
3289
+ onConfirmPick,
3290
+ onCancelPick,
3291
+ multiSelectCount,
3292
+ multiSelectType,
3293
+ onBulkAction,
3294
+ focusLabel,
3295
+ focusKey,
3296
+ onFocusExit,
3297
+ onFocusEndAction,
3298
+ searchQuery,
3299
+ onSearchChange,
3300
+ onSearchSubmit,
3301
+ selectedIssue,
3302
+ onComment,
3303
+ onPauseRefresh,
3304
+ onResumeRefresh,
3305
+ onToggleHelp,
3306
+ labelCache,
3307
+ onLabelConfirm,
3308
+ onLabelError,
3309
+ onLlmFallback
3310
+ }) {
3311
+ const { mode, helpVisible } = uiState;
3312
+ return /* @__PURE__ */ jsxs12(Fragment2, { children: [
3313
+ helpVisible ? /* @__PURE__ */ jsx12(HelpOverlay, { currentMode: mode, onClose: onToggleHelp }) : null,
3314
+ mode === "overlay:status" && selectedRepoStatusOptions.length > 0 ? /* @__PURE__ */ jsx12(
3315
+ StatusPicker,
3316
+ {
3317
+ options: selectedRepoStatusOptions,
3318
+ currentStatus,
3319
+ onSelect: onStatusSelect,
3320
+ onCancel: onExitOverlay
3321
+ }
3322
+ ) : null,
3323
+ mode === "overlay:create" ? /* @__PURE__ */ jsx12(
3324
+ CreateIssueForm,
3325
+ {
3326
+ repos: config2.repos,
3327
+ defaultRepo,
3328
+ onSubmit: onCreateIssue,
3329
+ onCancel: onExitOverlay,
3330
+ labelCache
3331
+ }
3332
+ ) : null,
3333
+ mode === "overlay:confirmPick" ? /* @__PURE__ */ jsx12(
3334
+ ConfirmPrompt,
3335
+ {
3336
+ message: "Pick this issue?",
3337
+ onConfirm: onConfirmPick,
3338
+ onCancel: onCancelPick
3339
+ }
3340
+ ) : null,
3341
+ mode === "overlay:bulkAction" ? /* @__PURE__ */ jsx12(
3342
+ BulkActionMenu,
3343
+ {
3344
+ count: multiSelectCount,
3345
+ selectionType: multiSelectType,
3346
+ onSelect: onBulkAction,
3347
+ onCancel: onExitOverlay
3348
+ }
3349
+ ) : null,
3350
+ mode === "focus" && focusLabel ? /* @__PURE__ */ jsx12(
3351
+ FocusMode,
3352
+ {
3353
+ label: focusLabel,
3354
+ durationSec: config2.board.focusDuration ?? 1500,
3355
+ onExit: onFocusExit,
3356
+ onEndAction: onFocusEndAction
2284
3357
  },
3358
+ focusKey
3359
+ ) : null,
3360
+ mode === "overlay:label" && selectedIssue && defaultRepo ? /* @__PURE__ */ jsx12(
3361
+ LabelPicker,
2285
3362
  {
2286
- category: "Board",
2287
- items: [
2288
- { key: "r", desc: "Refresh data" },
2289
- { key: "q", desc: "Quit" }
2290
- ]
3363
+ repo: defaultRepo,
3364
+ currentLabels: selectedIssue.labels.map((l) => l.name),
3365
+ labelCache,
3366
+ onConfirm: onLabelConfirm,
3367
+ onCancel: onExitOverlay,
3368
+ onError: onLabelError
2291
3369
  }
2292
- ];
3370
+ ) : null,
3371
+ mode === "search" ? /* @__PURE__ */ jsx12(SearchBar, { defaultValue: searchQuery, onChange: onSearchChange, onSubmit: onSearchSubmit }) : null,
3372
+ mode === "overlay:comment" && selectedIssue ? /* @__PURE__ */ jsx12(
3373
+ CommentInput,
3374
+ {
3375
+ issueNumber: selectedIssue.number,
3376
+ onSubmit: onComment,
3377
+ onCancel: onExitOverlay,
3378
+ onPauseRefresh,
3379
+ onResumeRefresh
3380
+ }
3381
+ ) : null,
3382
+ mode === "overlay:createNl" ? /* @__PURE__ */ jsx12(
3383
+ NlCreateOverlay,
3384
+ {
3385
+ repos: config2.repos,
3386
+ defaultRepoName: defaultRepo,
3387
+ labelCache,
3388
+ onSubmit: onCreateIssue,
3389
+ onCancel: onExitOverlay,
3390
+ onLlmFallback
3391
+ }
3392
+ ) : null
3393
+ ] });
3394
+ }
3395
+ var init_overlay_renderer = __esm({
3396
+ "src/board/components/overlay-renderer.tsx"() {
3397
+ "use strict";
3398
+ init_bulk_action_menu();
3399
+ init_comment_input();
3400
+ init_confirm_prompt();
3401
+ init_create_issue_form();
3402
+ init_focus_mode();
3403
+ init_help_overlay();
3404
+ init_label_picker();
3405
+ init_nl_create_overlay();
3406
+ init_search_bar();
3407
+ init_status_picker();
2293
3408
  }
2294
3409
  });
2295
3410
 
2296
3411
  // src/board/components/issue-row.tsx
2297
- import { Box as Box8, Text as Text8 } from "ink";
2298
- import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
3412
+ import { Box as Box12, Text as Text12 } from "ink";
3413
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2299
3414
  function truncate(s, max) {
2300
3415
  return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
2301
3416
  }
@@ -2336,30 +3451,30 @@ function IssueRow({ issue, selfLogin, isSelected }) {
2336
3451
  const labels = (issue.labels ?? []).slice(0, 2);
2337
3452
  const target = formatTargetDate(issue.targetDate);
2338
3453
  const titleStr = truncate(issue.title, 42).padEnd(42);
2339
- return /* @__PURE__ */ jsxs8(Box8, { children: [
2340
- isSelected ? /* @__PURE__ */ jsx8(Text8, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx8(Text8, { children: " " }),
2341
- /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
3454
+ return /* @__PURE__ */ jsxs13(Box12, { children: [
3455
+ isSelected ? /* @__PURE__ */ jsx13(Text12, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx13(Text12, { children: " " }),
3456
+ /* @__PURE__ */ jsxs13(Text12, { color: "cyan", children: [
2342
3457
  "#",
2343
3458
  String(issue.number).padEnd(5)
2344
3459
  ] }),
2345
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2346
- isSelected ? /* @__PURE__ */ jsx8(Text8, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx8(Text8, { children: titleStr }),
2347
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2348
- /* @__PURE__ */ jsx8(Box8, { width: LABEL_COL_WIDTH, children: labels.map((l, i) => /* @__PURE__ */ jsxs8(Text8, { children: [
3460
+ /* @__PURE__ */ jsx13(Text12, { children: " " }),
3461
+ isSelected ? /* @__PURE__ */ jsx13(Text12, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx13(Text12, { children: titleStr }),
3462
+ /* @__PURE__ */ jsx13(Text12, { children: " " }),
3463
+ /* @__PURE__ */ jsx13(Box12, { width: LABEL_COL_WIDTH, children: labels.map((l, i) => /* @__PURE__ */ jsxs13(Text12, { children: [
2349
3464
  i > 0 ? " " : "",
2350
- /* @__PURE__ */ jsxs8(Text8, { color: labelColor(l.name), children: [
3465
+ /* @__PURE__ */ jsxs13(Text12, { color: labelColor(l.name), children: [
2351
3466
  "[",
2352
3467
  truncate(l.name, 12),
2353
3468
  "]"
2354
3469
  ] })
2355
3470
  ] }, l.name)) }),
2356
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2357
- /* @__PURE__ */ jsx8(Text8, { color: assigneeColor, children: assigneeText.padEnd(14) }),
2358
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2359
- /* @__PURE__ */ jsx8(Text8, { color: "gray", children: timeAgo(issue.updatedAt).padStart(4) }),
2360
- target.text ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
2361
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2362
- /* @__PURE__ */ jsx8(Text8, { color: target.color, children: target.text })
3471
+ /* @__PURE__ */ jsx13(Text12, { children: " " }),
3472
+ /* @__PURE__ */ jsx13(Text12, { color: assigneeColor, children: assigneeText.padEnd(14) }),
3473
+ /* @__PURE__ */ jsx13(Text12, { children: " " }),
3474
+ /* @__PURE__ */ jsx13(Text12, { color: "gray", children: timeAgo(issue.updatedAt).padStart(4) }),
3475
+ target.text ? /* @__PURE__ */ jsxs13(Fragment3, { children: [
3476
+ /* @__PURE__ */ jsx13(Text12, { children: " " }),
3477
+ /* @__PURE__ */ jsx13(Text12, { color: target.color, children: target.text })
2363
3478
  ] }) : null
2364
3479
  ] });
2365
3480
  }
@@ -2382,90 +3497,13 @@ var init_issue_row = __esm({
2382
3497
  }
2383
3498
  });
2384
3499
 
2385
- // src/board/components/search-bar.tsx
2386
- import { TextInput as TextInput3 } from "@inkjs/ui";
2387
- import { Box as Box9, Text as Text9 } from "ink";
2388
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2389
- function SearchBar({ defaultValue, onChange, onSubmit }) {
2390
- return /* @__PURE__ */ jsxs9(Box9, { children: [
2391
- /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "/" }),
2392
- /* @__PURE__ */ jsx9(
2393
- TextInput3,
2394
- {
2395
- defaultValue,
2396
- placeholder: "search...",
2397
- onChange,
2398
- onSubmit
2399
- }
2400
- )
2401
- ] });
2402
- }
2403
- var init_search_bar = __esm({
2404
- "src/board/components/search-bar.tsx"() {
2405
- "use strict";
2406
- }
2407
- });
2408
-
2409
- // src/board/components/status-picker.tsx
2410
- import { Box as Box10, Text as Text10, useInput as useInput7 } from "ink";
2411
- import { useState as useState8 } from "react";
2412
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2413
- function StatusPicker({ options, currentStatus, onSelect, onCancel }) {
2414
- const [selectedIdx, setSelectedIdx] = useState8(() => {
2415
- const idx = options.findIndex((o) => o.name === currentStatus);
2416
- return idx >= 0 ? idx : 0;
2417
- });
2418
- useInput7((input2, key) => {
2419
- if (key.escape) return onCancel();
2420
- if (key.return) {
2421
- const opt = options[selectedIdx];
2422
- if (opt) onSelect(opt.id);
2423
- return;
2424
- }
2425
- if (input2 === "j" || key.downArrow) {
2426
- setSelectedIdx((i) => Math.min(i + 1, options.length - 1));
2427
- }
2428
- if (input2 === "k" || key.upArrow) {
2429
- setSelectedIdx((i) => Math.max(i - 1, 0));
2430
- }
2431
- });
2432
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
2433
- /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: "Move to status:" }),
2434
- options.map((opt, i) => {
2435
- const isCurrent = opt.name === currentStatus;
2436
- const isSelected = i === selectedIdx;
2437
- const prefix = isSelected ? "> " : " ";
2438
- const suffix = isCurrent ? " (current)" : "";
2439
- return /* @__PURE__ */ jsxs10(
2440
- Text10,
2441
- {
2442
- ...isSelected ? { color: "cyan" } : {},
2443
- dimColor: isCurrent,
2444
- children: [
2445
- prefix,
2446
- opt.name,
2447
- suffix
2448
- ]
2449
- },
2450
- opt.id
2451
- );
2452
- }),
2453
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "j/k:navigate Enter:select Esc:cancel" })
2454
- ] });
2455
- }
2456
- var init_status_picker = __esm({
2457
- "src/board/components/status-picker.tsx"() {
2458
- "use strict";
2459
- }
2460
- });
2461
-
2462
3500
  // src/board/components/task-row.tsx
2463
- import { Box as Box11, Text as Text11 } from "ink";
2464
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3501
+ import { Box as Box13, Text as Text13 } from "ink";
3502
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2465
3503
  function truncate2(s, max) {
2466
3504
  return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
2467
3505
  }
2468
- function formatDue(dateStr) {
3506
+ function formatDue2(dateStr) {
2469
3507
  if (!dateStr) return { text: "", color: "gray" };
2470
3508
  const d = new Date(dateStr);
2471
3509
  const days = Math.ceil((d.getTime() - Date.now()) / 864e5);
@@ -2480,15 +3518,15 @@ function formatDue(dateStr) {
2480
3518
  }
2481
3519
  function TaskRow({ task: task2, isSelected }) {
2482
3520
  const pri = PRIORITY_INDICATORS[task2.priority] ?? DEFAULT_PRIORITY;
2483
- const due = formatDue(task2.dueDate);
3521
+ const due = formatDue2(task2.dueDate);
2484
3522
  const titleStr = truncate2(task2.title, 45).padEnd(45);
2485
- return /* @__PURE__ */ jsxs11(Box11, { children: [
2486
- isSelected ? /* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx11(Text11, { children: " " }),
2487
- /* @__PURE__ */ jsx11(Text11, { color: pri.color, children: pri.text }),
2488
- /* @__PURE__ */ jsx11(Text11, { children: " " }),
2489
- isSelected ? /* @__PURE__ */ jsx11(Text11, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx11(Text11, { children: titleStr }),
2490
- /* @__PURE__ */ jsx11(Text11, { children: " " }),
2491
- /* @__PURE__ */ jsx11(Text11, { color: due.color, children: due.text })
3523
+ return /* @__PURE__ */ jsxs14(Box13, { children: [
3524
+ isSelected ? /* @__PURE__ */ jsx14(Text13, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx14(Text13, { children: " " }),
3525
+ /* @__PURE__ */ jsx14(Text13, { color: pri.color, children: pri.text }),
3526
+ /* @__PURE__ */ jsx14(Text13, { children: " " }),
3527
+ isSelected ? /* @__PURE__ */ jsx14(Text13, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx14(Text13, { children: titleStr }),
3528
+ /* @__PURE__ */ jsx14(Text13, { children: " " }),
3529
+ /* @__PURE__ */ jsx14(Text13, { color: due.color, children: due.text })
2492
3530
  ] });
2493
3531
  }
2494
3532
  var PRIORITY_INDICATORS, DEFAULT_PRIORITY;
@@ -2506,23 +3544,128 @@ var init_task_row = __esm({
2506
3544
  }
2507
3545
  });
2508
3546
 
3547
+ // src/board/components/row-renderer.tsx
3548
+ import { Box as Box14, Text as Text14 } from "ink";
3549
+ import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
3550
+ function RowRenderer({ row, selectedId, selfLogin, isMultiSelected }) {
3551
+ switch (row.type) {
3552
+ case "sectionHeader": {
3553
+ const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
3554
+ const isSel = selectedId === row.navId;
3555
+ return /* @__PURE__ */ jsxs15(Box14, { children: [
3556
+ /* @__PURE__ */ jsxs15(Text14, { color: isSel ? "cyan" : "white", bold: true, children: [
3557
+ arrow,
3558
+ " ",
3559
+ row.label
3560
+ ] }),
3561
+ /* @__PURE__ */ jsxs15(Text14, { color: "gray", children: [
3562
+ " ",
3563
+ "(",
3564
+ row.count,
3565
+ " ",
3566
+ row.countLabel,
3567
+ ")"
3568
+ ] })
3569
+ ] });
3570
+ }
3571
+ case "subHeader": {
3572
+ if (row.navId) {
3573
+ const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
3574
+ const isSel = selectedId === row.navId;
3575
+ return /* @__PURE__ */ jsxs15(Box14, { children: [
3576
+ /* @__PURE__ */ jsxs15(Text14, { color: isSel ? "cyan" : "gray", children: [
3577
+ " ",
3578
+ arrow,
3579
+ " ",
3580
+ row.text
3581
+ ] }),
3582
+ /* @__PURE__ */ jsxs15(Text14, { color: "gray", children: [
3583
+ " (",
3584
+ row.count,
3585
+ ")"
3586
+ ] })
3587
+ ] });
3588
+ }
3589
+ return /* @__PURE__ */ jsxs15(Text14, { color: "gray", children: [
3590
+ " ",
3591
+ row.text
3592
+ ] });
3593
+ }
3594
+ case "issue": {
3595
+ const checkbox2 = isMultiSelected != null ? isMultiSelected ? "\u2611 " : "\u2610 " : "";
3596
+ return /* @__PURE__ */ jsxs15(Box14, { children: [
3597
+ checkbox2 ? /* @__PURE__ */ jsx15(Text14, { color: isMultiSelected ? "cyan" : "gray", children: checkbox2 }) : null,
3598
+ /* @__PURE__ */ jsx15(IssueRow, { issue: row.issue, selfLogin, isSelected: selectedId === row.navId })
3599
+ ] });
3600
+ }
3601
+ case "task": {
3602
+ const checkbox2 = isMultiSelected != null ? isMultiSelected ? "\u2611 " : "\u2610 " : "";
3603
+ return /* @__PURE__ */ jsxs15(Box14, { children: [
3604
+ checkbox2 ? /* @__PURE__ */ jsx15(Text14, { color: isMultiSelected ? "cyan" : "gray", children: checkbox2 }) : null,
3605
+ /* @__PURE__ */ jsx15(TaskRow, { task: row.task, isSelected: selectedId === row.navId })
3606
+ ] });
3607
+ }
3608
+ case "activity": {
3609
+ const ago = timeAgo2(row.event.timestamp);
3610
+ return /* @__PURE__ */ jsxs15(Text14, { dimColor: true, children: [
3611
+ " ",
3612
+ ago,
3613
+ ": ",
3614
+ /* @__PURE__ */ jsxs15(Text14, { color: "gray", children: [
3615
+ "@",
3616
+ row.event.actor
3617
+ ] }),
3618
+ " ",
3619
+ row.event.summary,
3620
+ " ",
3621
+ /* @__PURE__ */ jsxs15(Text14, { dimColor: true, children: [
3622
+ "(",
3623
+ row.event.repoShortName,
3624
+ ")"
3625
+ ] })
3626
+ ] });
3627
+ }
3628
+ case "error":
3629
+ return /* @__PURE__ */ jsxs15(Text14, { color: "red", children: [
3630
+ " Error: ",
3631
+ row.text
3632
+ ] });
3633
+ case "gap":
3634
+ return /* @__PURE__ */ jsx15(Text14, { children: "" });
3635
+ }
3636
+ }
3637
+ function timeAgo2(date) {
3638
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
3639
+ if (seconds < 10) return "just now";
3640
+ if (seconds < 60) return `${seconds}s ago`;
3641
+ const minutes = Math.floor(seconds / 60);
3642
+ return `${minutes}m ago`;
3643
+ }
3644
+ var init_row_renderer = __esm({
3645
+ "src/board/components/row-renderer.tsx"() {
3646
+ "use strict";
3647
+ init_issue_row();
3648
+ init_task_row();
3649
+ }
3650
+ });
3651
+
2509
3652
  // src/board/components/toast-container.tsx
2510
- import { Spinner } from "@inkjs/ui";
2511
- import { Box as Box12, Text as Text12 } from "ink";
2512
- import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
3653
+ import { Spinner as Spinner3 } from "@inkjs/ui";
3654
+ import { Box as Box15, Text as Text15 } from "ink";
3655
+ import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
2513
3656
  function ToastContainer({ toasts }) {
2514
3657
  if (toasts.length === 0) return null;
2515
- return /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", children: toasts.map((t) => /* @__PURE__ */ jsx12(Box12, { children: t.type === "loading" ? /* @__PURE__ */ jsxs12(Fragment3, { children: [
2516
- /* @__PURE__ */ jsx12(Spinner, { label: "" }),
2517
- /* @__PURE__ */ jsxs12(Text12, { color: "cyan", children: [
3658
+ return /* @__PURE__ */ jsx16(Box15, { flexDirection: "column", children: toasts.map((t) => /* @__PURE__ */ jsx16(Box15, { children: t.type === "loading" ? /* @__PURE__ */ jsxs16(Fragment4, { children: [
3659
+ /* @__PURE__ */ jsx16(Spinner3, { label: "" }),
3660
+ /* @__PURE__ */ jsxs16(Text15, { color: "cyan", children: [
2518
3661
  " ",
2519
3662
  t.message
2520
3663
  ] })
2521
- ] }) : /* @__PURE__ */ jsxs12(Text12, { color: TYPE_COLORS[t.type], children: [
3664
+ ] }) : /* @__PURE__ */ jsxs16(Text15, { color: TYPE_COLORS[t.type], children: [
2522
3665
  TYPE_PREFIXES[t.type],
2523
3666
  " ",
2524
3667
  t.message,
2525
- t.type === "error" ? /* @__PURE__ */ jsx12(Text12, { color: "gray", children: t.retry ? " [r]etry [d]ismiss" : " [d]ismiss" }) : null
3668
+ t.type === "error" ? /* @__PURE__ */ jsx16(Text15, { color: "gray", children: t.retry ? " [r]etry [d]ismiss" : " [d]ismiss" }) : null
2526
3669
  ] }) }, t.id)) });
2527
3670
  }
2528
3671
  var TYPE_COLORS, TYPE_PREFIXES;
@@ -2544,13 +3687,13 @@ var init_toast_container = __esm({
2544
3687
  });
2545
3688
 
2546
3689
  // src/board/components/dashboard.tsx
2547
- import { execFileSync as execFileSync3 } from "child_process";
2548
- import { Spinner as Spinner2 } from "@inkjs/ui";
2549
- import { Box as Box13, Text as Text13, useApp, useInput as useInput8, useStdout } from "ink";
2550
- import { useCallback as useCallback8, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef7, useState as useState9 } from "react";
2551
- import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
3690
+ import { execFileSync as execFileSync3, spawnSync as spawnSync2 } from "child_process";
3691
+ import { Spinner as Spinner4 } from "@inkjs/ui";
3692
+ import { Box as Box16, Text as Text16, useApp, useStdout } from "ink";
3693
+ import { useCallback as useCallback10, useEffect as useEffect6, useMemo as useMemo2, useRef as useRef11, useState as useState11 } from "react";
3694
+ import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs17 } from "react/jsx-runtime";
2552
3695
  function isTerminalStatus(status) {
2553
- return TERMINAL_STATUS_RE2.test(status);
3696
+ return TERMINAL_STATUS_RE3.test(status);
2554
3697
  }
2555
3698
  function resolveStatusGroups(statusOptions, configuredGroups) {
2556
3699
  if (configuredGroups && configuredGroups.length > 0) {
@@ -2773,7 +3916,7 @@ function buildFlatRows(repos, tasks, activity, isCollapsed) {
2773
3916
  }
2774
3917
  return rows;
2775
3918
  }
2776
- function timeAgo2(date) {
3919
+ function timeAgo3(date) {
2777
3920
  const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
2778
3921
  if (seconds < 10) return "just now";
2779
3922
  if (seconds < 60) return `${seconds}s ago`;
@@ -2786,114 +3929,27 @@ function openInBrowser(url) {
2786
3929
  } catch {
2787
3930
  }
2788
3931
  }
2789
- function findSelectedUrl(repos, selectedId) {
2790
- if (!selectedId?.startsWith("gh:")) return null;
2791
- for (const rd of repos) {
2792
- for (const issue of rd.issues) {
2793
- if (`gh:${rd.repo.name}:${issue.number}` === selectedId) return issue.url;
2794
- }
2795
- }
2796
- return null;
2797
- }
2798
- function findSelectedIssueWithRepo(repos, selectedId) {
2799
- if (!selectedId?.startsWith("gh:")) return null;
2800
- for (const rd of repos) {
2801
- for (const issue of rd.issues) {
2802
- if (`gh:${rd.repo.name}:${issue.number}` === selectedId)
2803
- return { issue, repoName: rd.repo.name };
2804
- }
2805
- }
2806
- return null;
2807
- }
2808
- function isHeaderId(id) {
2809
- return id != null && (id.startsWith("header:") || id.startsWith("sub:"));
2810
- }
2811
- function RowRenderer({ row, selectedId, selfLogin, isMultiSelected }) {
2812
- switch (row.type) {
2813
- case "sectionHeader": {
2814
- const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
2815
- const isSel = selectedId === row.navId;
2816
- return /* @__PURE__ */ jsxs13(Box13, { children: [
2817
- /* @__PURE__ */ jsxs13(Text13, { color: isSel ? "cyan" : "white", bold: true, children: [
2818
- arrow,
2819
- " ",
2820
- row.label
2821
- ] }),
2822
- /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2823
- " ",
2824
- "(",
2825
- row.count,
2826
- " ",
2827
- row.countLabel,
2828
- ")"
2829
- ] })
2830
- ] });
2831
- }
2832
- case "subHeader": {
2833
- if (row.navId) {
2834
- const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
2835
- const isSel = selectedId === row.navId;
2836
- return /* @__PURE__ */ jsxs13(Box13, { children: [
2837
- /* @__PURE__ */ jsxs13(Text13, { color: isSel ? "cyan" : "gray", children: [
2838
- " ",
2839
- arrow,
2840
- " ",
2841
- row.text
2842
- ] }),
2843
- /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2844
- " (",
2845
- row.count,
2846
- ")"
2847
- ] })
2848
- ] });
2849
- }
2850
- return /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2851
- " ",
2852
- row.text
2853
- ] });
2854
- }
2855
- case "issue": {
2856
- const checkbox2 = isMultiSelected != null ? isMultiSelected ? "\u2611 " : "\u2610 " : "";
2857
- return /* @__PURE__ */ jsxs13(Box13, { children: [
2858
- checkbox2 ? /* @__PURE__ */ jsx13(Text13, { color: isMultiSelected ? "cyan" : "gray", children: checkbox2 }) : null,
2859
- /* @__PURE__ */ jsx13(IssueRow, { issue: row.issue, selfLogin, isSelected: selectedId === row.navId })
2860
- ] });
2861
- }
2862
- case "task": {
2863
- const checkbox2 = isMultiSelected != null ? isMultiSelected ? "\u2611 " : "\u2610 " : "";
2864
- return /* @__PURE__ */ jsxs13(Box13, { children: [
2865
- checkbox2 ? /* @__PURE__ */ jsx13(Text13, { color: isMultiSelected ? "cyan" : "gray", children: checkbox2 }) : null,
2866
- /* @__PURE__ */ jsx13(TaskRow, { task: row.task, isSelected: selectedId === row.navId })
2867
- ] });
3932
+ function findSelectedUrl(repos, selectedId) {
3933
+ if (!selectedId?.startsWith("gh:")) return null;
3934
+ for (const rd of repos) {
3935
+ for (const issue of rd.issues) {
3936
+ if (`gh:${rd.repo.name}:${issue.number}` === selectedId) return issue.url;
2868
3937
  }
2869
- case "activity": {
2870
- const ago = timeAgo2(row.event.timestamp);
2871
- return /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
2872
- " ",
2873
- ago,
2874
- ": ",
2875
- /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2876
- "@",
2877
- row.event.actor
2878
- ] }),
2879
- " ",
2880
- row.event.summary,
2881
- " ",
2882
- /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
2883
- "(",
2884
- row.event.repoShortName,
2885
- ")"
2886
- ] })
2887
- ] });
3938
+ }
3939
+ return null;
3940
+ }
3941
+ function findSelectedIssueWithRepo(repos, selectedId) {
3942
+ if (!selectedId?.startsWith("gh:")) return null;
3943
+ for (const rd of repos) {
3944
+ for (const issue of rd.issues) {
3945
+ if (`gh:${rd.repo.name}:${issue.number}` === selectedId)
3946
+ return { issue, repoName: rd.repo.name };
2888
3947
  }
2889
- case "error":
2890
- return /* @__PURE__ */ jsxs13(Text13, { color: "red", children: [
2891
- " Error: ",
2892
- row.text
2893
- ] });
2894
- case "gap":
2895
- return /* @__PURE__ */ jsx13(Text13, { children: "" });
2896
3948
  }
3949
+ return null;
3950
+ }
3951
+ function isHeaderId2(id) {
3952
+ return id != null && (id.startsWith("header:") || id.startsWith("sub:"));
2897
3953
  }
2898
3954
  function Dashboard({ config: config2, options, activeProfile }) {
2899
3955
  const { exit } = useApp();
@@ -2907,7 +3963,9 @@ function Dashboard({ config: config2, options, activeProfile }) {
2907
3963
  consecutiveFailures,
2908
3964
  autoRefreshPaused,
2909
3965
  refresh,
2910
- mutateData
3966
+ mutateData,
3967
+ pauseAutoRefresh,
3968
+ resumeAutoRefresh
2911
3969
  } = useData(config2, options, refreshMs);
2912
3970
  const allRepos = useMemo2(() => data?.repos ?? [], [data?.repos]);
2913
3971
  const allTasks = useMemo2(
@@ -2916,10 +3974,10 @@ function Dashboard({ config: config2, options, activeProfile }) {
2916
3974
  );
2917
3975
  const allActivity = useMemo2(() => data?.activity ?? [], [data?.activity]);
2918
3976
  const ui = useUIState();
2919
- const [searchQuery, setSearchQuery] = useState9("");
3977
+ const [searchQuery, setSearchQuery] = useState11("");
2920
3978
  const { toasts, toast, handleErrorAction } = useToast();
2921
- const [, setTick] = useState9(0);
2922
- useEffect3(() => {
3979
+ const [, setTick] = useState11(0);
3980
+ useEffect6(() => {
2923
3981
  const id = setInterval(() => setTick((t) => t + 1), 1e4);
2924
3982
  return () => clearInterval(id);
2925
3983
  }, []);
@@ -2938,7 +3996,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
2938
3996
  [repos, tasks, allActivity.length]
2939
3997
  );
2940
3998
  const nav = useNavigation(navItems);
2941
- const getRepoForId = useCallback8((id) => {
3999
+ const getRepoForId = useCallback10((id) => {
2942
4000
  if (id.startsWith("gh:")) {
2943
4001
  const parts = id.split(":");
2944
4002
  return parts.length >= 3 ? `${parts[1]}` : null;
@@ -2947,7 +4005,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
2947
4005
  return null;
2948
4006
  }, []);
2949
4007
  const multiSelect = useMultiSelect(getRepoForId);
2950
- useEffect3(() => {
4008
+ useEffect6(() => {
2951
4009
  if (multiSelect.count === 0) return;
2952
4010
  const validIds = new Set(navItems.map((i) => i.id));
2953
4011
  multiSelect.prune(validIds);
@@ -2961,8 +4019,9 @@ function Dashboard({ config: config2, options, activeProfile }) {
2961
4019
  mutateData,
2962
4020
  onOverlayDone: ui.exitOverlay
2963
4021
  });
2964
- const pendingPickRef = useRef7(null);
2965
- const handleCreateIssueWithPrompt = useCallback8(
4022
+ const pendingPickRef = useRef11(null);
4023
+ const labelCacheRef = useRef11({});
4024
+ const handleCreateIssueWithPrompt = useCallback10(
2966
4025
  (repo, title, labels) => {
2967
4026
  actions.handleCreateIssue(repo, title, labels).then((result) => {
2968
4027
  if (result) {
@@ -2973,7 +4032,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
2973
4032
  },
2974
4033
  [actions, ui]
2975
4034
  );
2976
- const handleConfirmPick = useCallback8(() => {
4035
+ const handleConfirmPick = useCallback10(() => {
2977
4036
  const pending = pendingPickRef.current;
2978
4037
  pendingPickRef.current = null;
2979
4038
  ui.exitOverlay();
@@ -2991,14 +4050,14 @@ function Dashboard({ config: config2, options, activeProfile }) {
2991
4050
  })
2992
4051
  );
2993
4052
  }, [config2, toast, refresh, ui]);
2994
- const handleCancelPick = useCallback8(() => {
4053
+ const handleCancelPick = useCallback10(() => {
2995
4054
  pendingPickRef.current = null;
2996
4055
  ui.exitOverlay();
2997
4056
  }, [ui]);
2998
- const [focusLabel, setFocusLabel] = useState9(null);
2999
- const handleEnterFocus = useCallback8(() => {
4057
+ const [focusLabel, setFocusLabel] = useState11(null);
4058
+ const handleEnterFocus = useCallback10(() => {
3000
4059
  const id = nav.selectedId;
3001
- if (!id || isHeaderId(id)) return;
4060
+ if (!id || isHeaderId2(id)) return;
3002
4061
  let label = "";
3003
4062
  if (id.startsWith("gh:")) {
3004
4063
  const found = findSelectedIssueWithRepo(repos, id);
@@ -3015,11 +4074,11 @@ function Dashboard({ config: config2, options, activeProfile }) {
3015
4074
  setFocusLabel(label);
3016
4075
  ui.enterFocus();
3017
4076
  }, [nav.selectedId, repos, tasks, config2.repos, ui]);
3018
- const handleFocusExit = useCallback8(() => {
4077
+ const handleFocusExit = useCallback10(() => {
3019
4078
  setFocusLabel(null);
3020
4079
  ui.exitToNormal();
3021
4080
  }, [ui]);
3022
- const handleFocusEndAction = useCallback8(
4081
+ const handleFocusEndAction = useCallback10(
3023
4082
  (action) => {
3024
4083
  switch (action) {
3025
4084
  case "restart":
@@ -3045,13 +4104,13 @@ function Dashboard({ config: config2, options, activeProfile }) {
3045
4104
  },
3046
4105
  [toast, ui]
3047
4106
  );
3048
- const [focusKey, setFocusKey] = useState9(0);
4107
+ const [focusKey, setFocusKey] = useState11(0);
3049
4108
  const { stdout } = useStdout();
3050
- const [termSize, setTermSize] = useState9({
4109
+ const [termSize, setTermSize] = useState11({
3051
4110
  cols: stdout?.columns ?? 80,
3052
4111
  rows: stdout?.rows ?? 24
3053
4112
  });
3054
- useEffect3(() => {
4113
+ useEffect6(() => {
3055
4114
  if (!stdout) return;
3056
4115
  const onResize = () => setTermSize({ cols: stdout.columns, rows: stdout.rows });
3057
4116
  stdout.on("resize", onResize);
@@ -3068,7 +4127,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3068
4127
  () => buildFlatRows(repos, tasks, allActivity, nav.isCollapsed),
3069
4128
  [repos, tasks, allActivity, nav.isCollapsed]
3070
4129
  );
3071
- const scrollRef = useRef7(0);
4130
+ const scrollRef = useRef11(0);
3072
4131
  const selectedRowIdx = flatRows.findIndex((r) => r.navId === nav.selectedId);
3073
4132
  if (selectedRowIdx >= 0) {
3074
4133
  if (selectedRowIdx < scrollRef.current) {
@@ -3086,7 +4145,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3086
4145
  const belowCount = flatRows.length - scrollRef.current - viewportHeight;
3087
4146
  const selectedItem = useMemo2(() => {
3088
4147
  const id = nav.selectedId;
3089
- if (!id || isHeaderId(id)) return { issue: null, task: null, repoName: null };
4148
+ if (!id || isHeaderId2(id)) return { issue: null, task: null, repoName: null };
3090
4149
  if (id.startsWith("gh:")) {
3091
4150
  for (const rd of repos) {
3092
4151
  for (const issue of rd.issues) {
@@ -3106,17 +4165,42 @@ function Dashboard({ config: config2, options, activeProfile }) {
3106
4165
  const repoName = multiSelect.count > 0 ? multiSelect.constrainedRepo : selectedItem.repoName;
3107
4166
  if (!repoName || repoName === "ticktick") return [];
3108
4167
  const rd = repos.find((r) => r.repo.name === repoName);
3109
- return rd?.statusOptions.filter((o) => !isTerminalStatus(o.name)) ?? [];
4168
+ return rd?.statusOptions ?? [];
3110
4169
  }, [selectedItem.repoName, repos, multiSelect.count, multiSelect.constrainedRepo]);
3111
- const handleOpen = useCallback8(() => {
4170
+ const handleOpen = useCallback10(() => {
3112
4171
  const url = findSelectedUrl(repos, nav.selectedId);
3113
4172
  if (url) openInBrowser(url);
3114
4173
  }, [repos, nav.selectedId]);
3115
- const handleSlack = useCallback8(() => {
4174
+ const handleSlack = useCallback10(() => {
3116
4175
  const found = findSelectedIssueWithRepo(repos, nav.selectedId);
3117
4176
  if (!found?.issue.slackThreadUrl) return;
3118
4177
  openInBrowser(found.issue.slackThreadUrl);
3119
4178
  }, [repos, nav.selectedId]);
4179
+ const handleCopyLink = useCallback10(() => {
4180
+ const found = findSelectedIssueWithRepo(repos, nav.selectedId);
4181
+ if (!found) return;
4182
+ const rc = config2.repos.find((r) => r.name === found.repoName);
4183
+ const label = `${rc?.shortName ?? found.repoName}#${found.issue.number}`;
4184
+ const clipArgs = getClipboardArgs();
4185
+ if (clipArgs) {
4186
+ const [cmd, ...args] = clipArgs;
4187
+ if (!cmd) {
4188
+ toast.info(`${label} \u2014 ${found.issue.url}`);
4189
+ return;
4190
+ }
4191
+ const result = spawnSync2(cmd, args, {
4192
+ input: found.issue.url,
4193
+ stdio: ["pipe", "pipe", "pipe"]
4194
+ });
4195
+ if (result.status === 0) {
4196
+ toast.success(`Copied ${label} to clipboard`);
4197
+ } else {
4198
+ toast.info(`${label} \u2014 ${found.issue.url}`);
4199
+ }
4200
+ } else {
4201
+ toast.info(`${label} \u2014 ${found.issue.url}`);
4202
+ }
4203
+ }, [repos, nav.selectedId, config2.repos, toast]);
3120
4204
  const multiSelectType = useMemo2(() => {
3121
4205
  let hasGh = false;
3122
4206
  let hasTt = false;
@@ -3128,7 +4212,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3128
4212
  if (hasTt) return "ticktick";
3129
4213
  return "github";
3130
4214
  }, [multiSelect.selected]);
3131
- const handleBulkAction = useCallback8(
4215
+ const handleBulkAction = useCallback10(
3132
4216
  (action) => {
3133
4217
  const ids = multiSelect.selected;
3134
4218
  switch (action.type) {
@@ -3172,7 +4256,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3172
4256
  },
3173
4257
  [multiSelect, actions, ui, toast]
3174
4258
  );
3175
- const handleBulkStatusSelect = useCallback8(
4259
+ const handleBulkStatusSelect = useCallback10(
3176
4260
  (optionId) => {
3177
4261
  const ids = multiSelect.selected;
3178
4262
  ui.exitOverlay();
@@ -3188,168 +4272,35 @@ function Dashboard({ config: config2, options, activeProfile }) {
3188
4272
  },
3189
4273
  [multiSelect, actions, ui]
3190
4274
  );
3191
- const handleInput = useCallback8(
3192
- (input2, key) => {
3193
- if (input2 === "?") {
3194
- ui.toggleHelp();
3195
- return;
3196
- }
3197
- if (key.escape && ui.state.mode !== "focus") {
3198
- if (ui.state.mode === "multiSelect") {
3199
- multiSelect.clear();
3200
- }
3201
- ui.exitOverlay();
3202
- return;
3203
- }
3204
- if (ui.canNavigate) {
3205
- if (input2 === "j" || key.downArrow) {
3206
- nav.moveDown();
3207
- return;
3208
- }
3209
- if (input2 === "k" || key.upArrow) {
3210
- nav.moveUp();
3211
- return;
3212
- }
3213
- if (key.tab) {
3214
- if (ui.state.mode === "multiSelect") {
3215
- multiSelect.clear();
3216
- ui.clearMultiSelect();
3217
- }
3218
- key.shift ? nav.prevSection() : nav.nextSection();
3219
- return;
3220
- }
3221
- }
3222
- if (ui.state.mode === "multiSelect") {
3223
- if (input2 === " ") {
3224
- const id = nav.selectedId;
3225
- if (id && !isHeaderId(id)) {
3226
- multiSelect.toggle(id);
3227
- }
3228
- return;
3229
- }
3230
- if (key.return) {
3231
- if (multiSelect.count > 0) {
3232
- ui.enterBulkAction();
3233
- }
3234
- return;
3235
- }
3236
- if (input2 === "m" && multiSelect.count > 0) {
3237
- ui.enterBulkAction();
3238
- return;
3239
- }
3240
- return;
3241
- }
3242
- if (input2 === "d") {
3243
- if (handleErrorAction("dismiss")) return;
3244
- }
3245
- if (input2 === "r" && handleErrorAction("retry")) return;
3246
- if (ui.canAct) {
3247
- if (input2 === "/") {
3248
- multiSelect.clear();
3249
- ui.enterSearch();
3250
- return;
3251
- }
3252
- if (input2 === "q") {
3253
- exit();
3254
- return;
3255
- }
3256
- if (input2 === "r" || input2 === "R") {
3257
- multiSelect.clear();
3258
- refresh();
3259
- return;
3260
- }
3261
- if (input2 === "s") {
3262
- handleSlack();
3263
- return;
3264
- }
3265
- if (input2 === "p") {
3266
- actions.handlePick();
3267
- return;
3268
- }
3269
- if (input2 === "a") {
3270
- actions.handleAssign();
3271
- return;
3272
- }
3273
- if (input2 === "u") {
3274
- actions.handleUnassign();
3275
- return;
3276
- }
3277
- if (input2 === "c") {
3278
- if (selectedItem.issue) {
3279
- multiSelect.clear();
3280
- ui.enterComment();
3281
- }
3282
- return;
3283
- }
3284
- if (input2 === "m") {
3285
- if (selectedItem.issue && selectedRepoStatusOptions.length > 0) {
3286
- multiSelect.clear();
3287
- ui.enterStatus();
3288
- } else if (selectedItem.issue) {
3289
- toast.info("Issue not in a project board");
3290
- }
3291
- return;
3292
- }
3293
- if (input2 === "n") {
3294
- multiSelect.clear();
3295
- ui.enterCreate();
3296
- return;
3297
- }
3298
- if (input2 === "f") {
3299
- handleEnterFocus();
3300
- return;
3301
- }
3302
- if (input2 === " ") {
3303
- const id = nav.selectedId;
3304
- if (id && !isHeaderId(id)) {
3305
- multiSelect.toggle(id);
3306
- ui.enterMultiSelect();
3307
- } else if (isHeaderId(nav.selectedId)) {
3308
- nav.toggleSection();
3309
- }
3310
- return;
3311
- }
3312
- if (key.return) {
3313
- if (isHeaderId(nav.selectedId)) {
3314
- nav.toggleSection();
3315
- return;
3316
- }
3317
- handleOpen();
3318
- return;
3319
- }
3320
- }
3321
- },
3322
- [
3323
- ui,
3324
- nav,
4275
+ const onSearchEscape = useCallback10(() => {
4276
+ ui.exitOverlay();
4277
+ setSearchQuery("");
4278
+ }, [ui]);
4279
+ useKeyboard({
4280
+ ui,
4281
+ nav,
4282
+ multiSelect,
4283
+ selectedIssue: selectedItem.issue,
4284
+ selectedRepoStatusOptionsLength: selectedRepoStatusOptions.length,
4285
+ actions: {
3325
4286
  exit,
3326
4287
  refresh,
3327
4288
  handleSlack,
4289
+ handleCopyLink,
3328
4290
  handleOpen,
3329
- actions,
3330
- selectedItem.issue,
3331
- selectedRepoStatusOptions.length,
3332
- toast,
3333
- nav.selectedId,
3334
- multiSelect,
3335
4291
  handleEnterFocus,
3336
- handleErrorAction
3337
- ]
3338
- );
3339
- const inputActive = ui.state.mode === "normal" || ui.state.mode === "multiSelect" || ui.state.mode === "focus";
3340
- useInput8(handleInput, { isActive: inputActive });
3341
- const handleSearchEscape = useCallback8(
3342
- (_input, key) => {
3343
- if (key.escape) {
3344
- ui.exitOverlay();
3345
- setSearchQuery("");
3346
- }
4292
+ handlePick: actions.handlePick,
4293
+ handleAssign: actions.handleAssign,
4294
+ handleUnassign: actions.handleUnassign,
4295
+ handleEnterLabel: ui.enterLabel,
4296
+ handleEnterCreateNl: ui.enterCreateNl,
4297
+ handleErrorAction,
4298
+ toastInfo: toast.info
3347
4299
  },
3348
- [ui]
3349
- );
3350
- useInput8(handleSearchEscape, { isActive: ui.state.mode === "search" });
4300
+ onSearchEscape
4301
+ });
3351
4302
  if (status === "loading" && !data) {
3352
- return /* @__PURE__ */ jsx13(Box13, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx13(Spinner2, { label: "Loading dashboard..." }) });
4303
+ return /* @__PURE__ */ jsx17(Box16, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx17(Spinner4, { label: "Loading dashboard..." }) });
3353
4304
  }
3354
4305
  const now = data?.fetchedAt ?? /* @__PURE__ */ new Date();
3355
4306
  const dateStr = now.toLocaleDateString("en-US", {
@@ -3357,93 +4308,81 @@ function Dashboard({ config: config2, options, activeProfile }) {
3357
4308
  day: "numeric",
3358
4309
  year: "numeric"
3359
4310
  });
3360
- return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
3361
- /* @__PURE__ */ jsxs13(Box13, { children: [
3362
- /* @__PURE__ */ jsx13(Text13, { color: "cyan", bold: true, children: "HOG BOARD" }),
3363
- activeProfile ? /* @__PURE__ */ jsxs13(Text13, { color: "yellow", children: [
4311
+ return /* @__PURE__ */ jsxs17(Box16, { flexDirection: "column", paddingX: 1, children: [
4312
+ /* @__PURE__ */ jsxs17(Box16, { children: [
4313
+ /* @__PURE__ */ jsx17(Text16, { color: "cyan", bold: true, children: "HOG BOARD" }),
4314
+ activeProfile ? /* @__PURE__ */ jsxs17(Text16, { color: "yellow", children: [
3364
4315
  " [",
3365
4316
  activeProfile,
3366
4317
  "]"
3367
4318
  ] }) : null,
3368
- /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
4319
+ /* @__PURE__ */ jsxs17(Text16, { color: "gray", children: [
3369
4320
  " ",
3370
4321
  "\u2014",
3371
4322
  " ",
3372
4323
  dateStr
3373
4324
  ] }),
3374
- /* @__PURE__ */ jsx13(Text13, { children: " " }),
3375
- isRefreshing ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
3376
- /* @__PURE__ */ jsx13(Spinner2, { label: "" }),
3377
- /* @__PURE__ */ jsx13(Text13, { color: "cyan", children: " Refreshing..." })
3378
- ] }) : lastRefresh ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
3379
- /* @__PURE__ */ jsxs13(Text13, { color: refreshAgeColor(lastRefresh), children: [
4325
+ /* @__PURE__ */ jsx17(Text16, { children: " " }),
4326
+ isRefreshing ? /* @__PURE__ */ jsxs17(Fragment5, { children: [
4327
+ /* @__PURE__ */ jsx17(Spinner4, { label: "" }),
4328
+ /* @__PURE__ */ jsx17(Text16, { color: "cyan", children: " Refreshing..." })
4329
+ ] }) : lastRefresh ? /* @__PURE__ */ jsxs17(Fragment5, { children: [
4330
+ /* @__PURE__ */ jsxs17(Text16, { color: refreshAgeColor(lastRefresh), children: [
3380
4331
  "Updated ",
3381
- timeAgo2(lastRefresh)
4332
+ timeAgo3(lastRefresh)
3382
4333
  ] }),
3383
- consecutiveFailures > 0 ? /* @__PURE__ */ jsx13(Text13, { color: "red", children: " (!)" }) : null
4334
+ consecutiveFailures > 0 ? /* @__PURE__ */ jsx17(Text16, { color: "red", children: " (!)" }) : null
3384
4335
  ] }) : null,
3385
- autoRefreshPaused ? /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: " Auto-refresh paused \u2014 press r to retry" }) : null
4336
+ autoRefreshPaused ? /* @__PURE__ */ jsx17(Text16, { color: "yellow", children: " Auto-refresh paused \u2014 press r to retry" }) : null
3386
4337
  ] }),
3387
- error ? /* @__PURE__ */ jsxs13(Text13, { color: "red", children: [
4338
+ error ? /* @__PURE__ */ jsxs17(Text16, { color: "red", children: [
3388
4339
  "Error: ",
3389
4340
  error
3390
4341
  ] }) : null,
3391
- ui.state.helpVisible ? /* @__PURE__ */ jsx13(HelpOverlay, { currentMode: ui.state.mode, onClose: ui.toggleHelp }) : null,
3392
- ui.state.mode === "overlay:status" && selectedRepoStatusOptions.length > 0 ? /* @__PURE__ */ jsx13(
3393
- StatusPicker,
4342
+ /* @__PURE__ */ jsx17(
4343
+ OverlayRenderer,
3394
4344
  {
3395
- options: selectedRepoStatusOptions,
4345
+ uiState: ui.state,
4346
+ config: config2,
4347
+ selectedRepoStatusOptions,
3396
4348
  currentStatus: multiSelect.count > 0 ? void 0 : selectedItem.issue?.projectStatus,
3397
- onSelect: multiSelect.count > 0 ? handleBulkStatusSelect : actions.handleStatusChange,
3398
- onCancel: ui.exitOverlay
3399
- }
3400
- ) : null,
3401
- ui.state.mode === "overlay:create" ? /* @__PURE__ */ jsx13(
3402
- CreateIssueForm,
3403
- {
3404
- repos: config2.repos,
4349
+ onStatusSelect: multiSelect.count > 0 ? handleBulkStatusSelect : actions.handleStatusChange,
4350
+ onExitOverlay: ui.exitOverlay,
3405
4351
  defaultRepo: selectedItem.repoName,
3406
- onSubmit: handleCreateIssueWithPrompt,
3407
- onCancel: ui.exitOverlay
3408
- }
3409
- ) : null,
3410
- ui.state.mode === "overlay:confirmPick" ? /* @__PURE__ */ jsx13(
3411
- ConfirmPrompt,
3412
- {
3413
- message: "Pick this issue?",
3414
- onConfirm: handleConfirmPick,
3415
- onCancel: handleCancelPick
3416
- }
3417
- ) : null,
3418
- ui.state.mode === "overlay:bulkAction" ? /* @__PURE__ */ jsx13(
3419
- BulkActionMenu,
3420
- {
3421
- count: multiSelect.count,
3422
- selectionType: multiSelectType,
3423
- onSelect: handleBulkAction,
3424
- onCancel: ui.exitOverlay
4352
+ onCreateIssue: handleCreateIssueWithPrompt,
4353
+ onConfirmPick: handleConfirmPick,
4354
+ onCancelPick: handleCancelPick,
4355
+ multiSelectCount: multiSelect.count,
4356
+ multiSelectType,
4357
+ onBulkAction: handleBulkAction,
4358
+ focusLabel,
4359
+ focusKey,
4360
+ onFocusExit: handleFocusExit,
4361
+ onFocusEndAction: handleFocusEndAction,
4362
+ searchQuery,
4363
+ onSearchChange: setSearchQuery,
4364
+ onSearchSubmit: ui.exitOverlay,
4365
+ selectedIssue: selectedItem.issue,
4366
+ onComment: actions.handleComment,
4367
+ onPauseRefresh: pauseAutoRefresh,
4368
+ onResumeRefresh: resumeAutoRefresh,
4369
+ onToggleHelp: ui.toggleHelp,
4370
+ labelCache: labelCacheRef.current,
4371
+ onLabelConfirm: actions.handleLabelChange,
4372
+ onLabelError: (msg) => toast.error(msg),
4373
+ onLlmFallback: (msg) => toast.info(msg)
3425
4374
  }
3426
- ) : null,
3427
- ui.state.mode === "focus" && focusLabel ? /* @__PURE__ */ jsx13(
3428
- FocusMode,
3429
- {
3430
- label: focusLabel,
3431
- durationSec: config2.board.focusDuration ?? 1500,
3432
- onExit: handleFocusExit,
3433
- onEndAction: handleFocusEndAction
3434
- },
3435
- focusKey
3436
- ) : null,
3437
- !ui.state.helpVisible && ui.state.mode !== "overlay:status" && ui.state.mode !== "overlay:create" && ui.state.mode !== "overlay:bulkAction" && ui.state.mode !== "overlay:confirmPick" && ui.state.mode !== "focus" ? /* @__PURE__ */ jsxs13(Box13, { height: viewportHeight, children: [
3438
- /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", flexGrow: 1, children: [
3439
- hasMoreAbove ? /* @__PURE__ */ jsxs13(Text13, { color: "gray", dimColor: true, children: [
4375
+ ),
4376
+ !ui.state.helpVisible && ui.state.mode !== "overlay:status" && ui.state.mode !== "overlay:create" && ui.state.mode !== "overlay:createNl" && ui.state.mode !== "overlay:bulkAction" && ui.state.mode !== "overlay:confirmPick" && ui.state.mode !== "focus" ? /* @__PURE__ */ jsxs17(Box16, { height: viewportHeight, children: [
4377
+ /* @__PURE__ */ jsxs17(Box16, { flexDirection: "column", flexGrow: 1, children: [
4378
+ hasMoreAbove ? /* @__PURE__ */ jsxs17(Text16, { color: "gray", dimColor: true, children: [
3440
4379
  " ",
3441
4380
  "\u25B2",
3442
4381
  " ",
3443
4382
  aboveCount,
3444
4383
  " more above"
3445
4384
  ] }) : null,
3446
- visibleRows.map((row) => /* @__PURE__ */ jsx13(
4385
+ visibleRows.map((row) => /* @__PURE__ */ jsx17(
3447
4386
  RowRenderer,
3448
4387
  {
3449
4388
  row,
@@ -3453,7 +4392,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3453
4392
  },
3454
4393
  row.key
3455
4394
  )),
3456
- hasMoreBelow ? /* @__PURE__ */ jsxs13(Text13, { color: "gray", dimColor: true, children: [
4395
+ hasMoreBelow ? /* @__PURE__ */ jsxs17(Text16, { color: "gray", dimColor: true, children: [
3457
4396
  " ",
3458
4397
  "\u25BC",
3459
4398
  " ",
@@ -3461,7 +4400,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3461
4400
  " more below"
3462
4401
  ] }) : null
3463
4402
  ] }),
3464
- showDetailPanel ? /* @__PURE__ */ jsx13(Box13, { marginLeft: 1, width: detailPanelWidth, children: /* @__PURE__ */ jsx13(
4403
+ showDetailPanel ? /* @__PURE__ */ jsx17(Box16, { marginLeft: 1, width: detailPanelWidth, children: /* @__PURE__ */ jsx17(
3465
4404
  DetailPanel,
3466
4405
  {
3467
4406
  issue: selectedItem.issue,
@@ -3470,25 +4409,16 @@ function Dashboard({ config: config2, options, activeProfile }) {
3470
4409
  }
3471
4410
  ) }) : null
3472
4411
  ] }) : null,
3473
- ui.state.mode === "search" ? /* @__PURE__ */ jsx13(SearchBar, { defaultValue: searchQuery, onChange: setSearchQuery, onSubmit: ui.exitOverlay }) : null,
3474
- ui.state.mode === "overlay:comment" && selectedItem.issue ? /* @__PURE__ */ jsx13(
3475
- CommentInput,
3476
- {
3477
- issueNumber: selectedItem.issue.number,
3478
- onSubmit: actions.handleComment,
3479
- onCancel: ui.exitOverlay
3480
- }
3481
- ) : null,
3482
- /* @__PURE__ */ jsx13(ToastContainer, { toasts }),
3483
- /* @__PURE__ */ jsx13(Box13, { children: ui.state.mode === "multiSelect" ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
3484
- /* @__PURE__ */ jsxs13(Text13, { color: "cyan", bold: true, children: [
4412
+ /* @__PURE__ */ jsx17(ToastContainer, { toasts }),
4413
+ /* @__PURE__ */ jsx17(Box16, { children: ui.state.mode === "multiSelect" ? /* @__PURE__ */ jsxs17(Fragment5, { children: [
4414
+ /* @__PURE__ */ jsxs17(Text16, { color: "cyan", bold: true, children: [
3485
4415
  multiSelect.count,
3486
4416
  " selected"
3487
4417
  ] }),
3488
- /* @__PURE__ */ jsx13(Text13, { color: "gray", children: " Space:toggle Enter:actions Esc:cancel" })
3489
- ] }) : ui.state.mode === "focus" ? /* @__PURE__ */ jsx13(Text13, { color: "magenta", bold: true, children: "Focus mode \u2014 Esc to exit" }) : /* @__PURE__ */ jsxs13(Fragment4, { children: [
3490
- /* @__PURE__ */ jsx13(Text13, { color: "gray", children: "j/k:nav Tab:section Enter:open Space:select /:search p:pick c:comment m:status a/u:assign s:slack n:new f:focus ?:help q:quit" }),
3491
- searchQuery && ui.state.mode !== "search" ? /* @__PURE__ */ jsxs13(Text13, { color: "yellow", children: [
4418
+ /* @__PURE__ */ jsx17(Text16, { color: "gray", children: " Space:toggle Enter:actions Esc:cancel" })
4419
+ ] }) : ui.state.mode === "focus" ? /* @__PURE__ */ jsx17(Text16, { color: "magenta", bold: true, children: "Focus mode \u2014 Esc to exit" }) : /* @__PURE__ */ jsxs17(Fragment5, { children: [
4420
+ /* @__PURE__ */ jsx17(Text16, { color: "gray", children: "j/k:nav Tab:section Enter:open Space:select /:search p:pick c:comment m:status a/u:assign s:slack y:copy l:labels n:new I:nlcreate C:collapse f:focus ?:help q:quit" }),
4421
+ searchQuery && ui.state.mode !== "search" ? /* @__PURE__ */ jsxs17(Text16, { color: "yellow", children: [
3492
4422
  ' filter: "',
3493
4423
  searchQuery,
3494
4424
  '"'
@@ -3496,29 +4426,23 @@ function Dashboard({ config: config2, options, activeProfile }) {
3496
4426
  ] }) })
3497
4427
  ] });
3498
4428
  }
3499
- var TERMINAL_STATUS_RE2, PRIORITY_RANK, CHROME_ROWS;
4429
+ var TERMINAL_STATUS_RE3, PRIORITY_RANK, CHROME_ROWS;
3500
4430
  var init_dashboard = __esm({
3501
4431
  "src/board/components/dashboard.tsx"() {
3502
4432
  "use strict";
4433
+ init_clipboard();
3503
4434
  init_use_actions();
3504
4435
  init_use_data();
4436
+ init_use_keyboard();
3505
4437
  init_use_multi_select();
3506
4438
  init_use_navigation();
3507
4439
  init_use_toast();
3508
4440
  init_use_ui_state();
3509
- init_bulk_action_menu();
3510
- init_comment_input();
3511
- init_confirm_prompt();
3512
- init_create_issue_form();
3513
4441
  init_detail_panel();
3514
- init_focus_mode();
3515
- init_help_overlay();
3516
- init_issue_row();
3517
- init_search_bar();
3518
- init_status_picker();
3519
- init_task_row();
4442
+ init_overlay_renderer();
4443
+ init_row_renderer();
3520
4444
  init_toast_container();
3521
- TERMINAL_STATUS_RE2 = /^(done|shipped|won't|wont|closed|complete|completed)$/i;
4445
+ TERMINAL_STATUS_RE3 = /^(done|shipped|won't|wont|closed|complete|completed)$/i;
3522
4446
  PRIORITY_RANK = {
3523
4447
  "priority:critical": 0,
3524
4448
  "priority:high": 1,
@@ -3535,17 +4459,19 @@ __export(live_exports, {
3535
4459
  runLiveDashboard: () => runLiveDashboard
3536
4460
  });
3537
4461
  import { render } from "ink";
3538
- import { jsx as jsx14 } from "react/jsx-runtime";
4462
+ import { jsx as jsx18 } from "react/jsx-runtime";
3539
4463
  async function runLiveDashboard(config2, options, activeProfile) {
3540
- const { waitUntilExit } = render(
3541
- /* @__PURE__ */ jsx14(Dashboard, { config: config2, options, activeProfile: activeProfile ?? null })
4464
+ const instance = render(
4465
+ /* @__PURE__ */ jsx18(Dashboard, { config: config2, options, activeProfile: activeProfile ?? null })
3542
4466
  );
3543
- await waitUntilExit();
4467
+ setInkInstance(instance);
4468
+ await instance.waitUntilExit();
3544
4469
  }
3545
4470
  var init_live = __esm({
3546
4471
  "src/board/live.tsx"() {
3547
4472
  "use strict";
3548
4473
  init_dashboard();
4474
+ init_ink_instance();
3549
4475
  }
3550
4476
  });
3551
4477
 
@@ -3911,8 +4837,10 @@ var init_format_static = __esm({
3911
4837
  });
3912
4838
 
3913
4839
  // src/cli.ts
4840
+ init_ai();
3914
4841
  init_api();
3915
4842
  init_config();
4843
+ import { execFileSync as execFileSync5 } from "child_process";
3916
4844
  import { Command } from "commander";
3917
4845
 
3918
4846
  // src/init.ts
@@ -4582,7 +5510,7 @@ function resolveProjectId(projectId) {
4582
5510
  process.exit(1);
4583
5511
  }
4584
5512
  var program = new Command();
4585
- program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.2.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
5513
+ program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.3.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
4586
5514
  const opts = thisCommand.opts();
4587
5515
  if (opts.json) setFormat("json");
4588
5516
  if (opts.human) setFormat("human");
@@ -4931,6 +5859,53 @@ config.command("profile:default [name]").description("Set or show the default bo
4931
5859
  printSuccess(`Default profile set to "${name}".`);
4932
5860
  }
4933
5861
  });
5862
+ var issueCommand = new Command("issue").description("GitHub issue utilities");
5863
+ issueCommand.command("create <text>").description("Create a GitHub issue from natural language text").option("--repo <repo>", "Target repository (owner/name)").option("--dry-run", "Print parsed fields without creating the issue").action(async (text, opts) => {
5864
+ const config2 = loadFullConfig();
5865
+ const repo = opts.repo ?? config2.repos[0]?.name;
5866
+ if (!repo) {
5867
+ console.error(
5868
+ "Error: no repo specified. Use --repo owner/name or configure repos in hog init."
5869
+ );
5870
+ process.exit(1);
5871
+ }
5872
+ if (hasLlmApiKey()) {
5873
+ console.error("[info] LLM parsing enabled");
5874
+ }
5875
+ const parsed = await extractIssueFields(text, {
5876
+ onLlmFallback: (msg) => console.error(`[warn] ${msg}`)
5877
+ });
5878
+ if (!parsed) {
5879
+ console.error(
5880
+ "Error: could not parse a title from input. Ensure your text has a non-empty title."
5881
+ );
5882
+ process.exit(1);
5883
+ }
5884
+ const labels = [...parsed.labels];
5885
+ if (parsed.dueDate) labels.push(`due:${parsed.dueDate}`);
5886
+ console.error(`Title: ${parsed.title}`);
5887
+ if (labels.length > 0) console.error(`Labels: ${labels.join(", ")}`);
5888
+ if (parsed.assignee) console.error(`Assignee: @${parsed.assignee}`);
5889
+ if (parsed.dueDate) console.error(`Due: ${parsed.dueDate}`);
5890
+ console.error(`Repo: ${repo}`);
5891
+ if (opts.dryRun) {
5892
+ console.error("[dry-run] Skipping issue creation.");
5893
+ return;
5894
+ }
5895
+ const args = ["issue", "create", "--repo", repo, "--title", parsed.title];
5896
+ for (const label of labels) {
5897
+ args.push("--label", label);
5898
+ }
5899
+ try {
5900
+ execFileSync5("gh", args, { stdio: "inherit" });
5901
+ } catch (err) {
5902
+ console.error(
5903
+ `Error: gh issue create failed: ${err instanceof Error ? err.message : String(err)}`
5904
+ );
5905
+ process.exit(1);
5906
+ }
5907
+ });
5908
+ program.addCommand(issueCommand);
4934
5909
  program.parseAsync().catch((err) => {
4935
5910
  const message = err instanceof Error ? err.message : String(err);
4936
5911
  console.error(`Error: ${message}`);