@ondrej-svec/hog 1.1.3 → 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({
@@ -164,13 +324,6 @@ function getAuth() {
164
324
  return null;
165
325
  }
166
326
  }
167
- function saveAuth(data) {
168
- ensureDir();
169
- writeFileSync(AUTH_FILE, `${JSON.stringify(data, null, 2)}
170
- `, {
171
- mode: 384
172
- });
173
- }
174
327
  function getConfig() {
175
328
  if (!existsSync(CONFIG_FILE)) return {};
176
329
  try {
@@ -468,6 +621,21 @@ function fetchProjectStatusOptions(repo, projectNumber, _statusFieldId) {
468
621
  function addLabel(repo, issueNumber, label) {
469
622
  runGh(["issue", "edit", String(issueNumber), "--repo", repo, "--add-label", label]);
470
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
+ }
471
639
  function updateProjectItemStatus(repo, issueNumber, projectConfig) {
472
640
  const [owner, repoName] = repo.split("/");
473
641
  const findItemQuery = `
@@ -685,6 +853,21 @@ var init_sync_state = __esm({
685
853
  }
686
854
  });
687
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
+
688
871
  // src/pick.ts
689
872
  var pick_exports = {};
690
873
  __export(pick_exports, {
@@ -1041,6 +1224,26 @@ function useActions({
1041
1224
  },
1042
1225
  [toast, refresh, onOverlayDone]
1043
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
+ );
1044
1247
  const handleBulkAssign = useCallback(
1045
1248
  async (ids) => {
1046
1249
  const failed = [];
@@ -1171,6 +1374,7 @@ function useActions({
1171
1374
  handleStatusChange,
1172
1375
  handleAssign,
1173
1376
  handleUnassign,
1377
+ handleLabelChange,
1174
1378
  handleCreateIssue,
1175
1379
  handleBulkAssign,
1176
1380
  handleBulkUnassign,
@@ -1306,7 +1510,13 @@ function useData(config2, options, refreshIntervalMs) {
1306
1510
  return { ...prev, data: fn(prev.data) };
1307
1511
  });
1308
1512
  }, []);
1309
- 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 };
1310
1520
  }
1311
1521
  var INITIAL_STATE, STALE_THRESHOLDS, MAX_REFRESH_FAILURES;
1312
1522
  var init_use_data = __esm({
@@ -1332,14 +1542,234 @@ var init_use_data = __esm({
1332
1542
  }
1333
1543
  });
1334
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
+
1335
1765
  // src/board/hooks/use-multi-select.ts
1336
- 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";
1337
1767
  function useMultiSelect(getRepoForId) {
1338
1768
  const [selected, setSelected] = useState2(/* @__PURE__ */ new Set());
1339
1769
  const repoRef = useRef3(null);
1340
1770
  const getRepoRef = useRef3(getRepoForId);
1341
1771
  getRepoRef.current = getRepoForId;
1342
- const toggle = useCallback3((id) => {
1772
+ const toggle = useCallback4((id) => {
1343
1773
  setSelected((prev) => {
1344
1774
  const repo = getRepoRef.current(id);
1345
1775
  if (!repo) return prev;
@@ -1357,11 +1787,11 @@ function useMultiSelect(getRepoForId) {
1357
1787
  return next;
1358
1788
  });
1359
1789
  }, []);
1360
- const clear = useCallback3(() => {
1790
+ const clear = useCallback4(() => {
1361
1791
  setSelected(/* @__PURE__ */ new Set());
1362
1792
  repoRef.current = null;
1363
1793
  }, []);
1364
- const prune = useCallback3((validIds) => {
1794
+ const prune = useCallback4((validIds) => {
1365
1795
  setSelected((prev) => {
1366
1796
  const next = /* @__PURE__ */ new Set();
1367
1797
  for (const id of prev) {
@@ -1372,7 +1802,7 @@ function useMultiSelect(getRepoForId) {
1372
1802
  return next;
1373
1803
  });
1374
1804
  }, []);
1375
- const isSelected = useCallback3((id) => selected.has(id), [selected]);
1805
+ const isSelected = useCallback4((id) => selected.has(id), [selected]);
1376
1806
  return {
1377
1807
  selected,
1378
1808
  count: selected.size,
@@ -1390,7 +1820,7 @@ var init_use_multi_select = __esm({
1390
1820
  });
1391
1821
 
1392
1822
  // src/board/hooks/use-navigation.ts
1393
- import { useCallback as useCallback4, useMemo, useReducer, useRef as useRef4 } from "react";
1823
+ import { useCallback as useCallback5, useMemo, useReducer, useRef as useRef4 } from "react";
1394
1824
  function arraysEqual(a, b) {
1395
1825
  if (a.length !== b.length) return false;
1396
1826
  for (let i = 0; i < a.length; i++) {
@@ -1412,7 +1842,7 @@ function navReducer(state, action) {
1412
1842
  case "SET_ITEMS": {
1413
1843
  const sections = [...new Set(action.items.map((i) => i.section))];
1414
1844
  const isFirstLoad = state.sections.length === 0;
1415
- const collapsedSections = isFirstLoad ? new Set(sections) : state.collapsedSections;
1845
+ const collapsedSections = isFirstLoad ? new Set(sections.filter((s) => s === "activity")) : state.collapsedSections;
1416
1846
  const selectionValid = state.selectedId != null && action.items.some((i) => i.id === state.selectedId);
1417
1847
  if (!isFirstLoad && selectionValid && arraysEqual(sections, state.sections)) {
1418
1848
  return state;
@@ -1450,6 +1880,9 @@ function navReducer(state, action) {
1450
1880
  }
1451
1881
  return { ...state, collapsedSections: next };
1452
1882
  }
1883
+ case "COLLAPSE_ALL": {
1884
+ return { ...state, collapsedSections: new Set(state.sections) };
1885
+ }
1453
1886
  default:
1454
1887
  return state;
1455
1888
  }
@@ -1484,17 +1917,17 @@ function useNavigation(allItems) {
1484
1917
  const idx = visibleItems.findIndex((i) => i.id === state.selectedId);
1485
1918
  return idx >= 0 ? idx : 0;
1486
1919
  }, [state.selectedId, visibleItems]);
1487
- const moveUp = useCallback4(() => {
1920
+ const moveUp = useCallback5(() => {
1488
1921
  const newIdx = Math.max(0, selectedIndex - 1);
1489
1922
  const item = visibleItems[newIdx];
1490
1923
  if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
1491
1924
  }, [selectedIndex, visibleItems]);
1492
- const moveDown = useCallback4(() => {
1925
+ const moveDown = useCallback5(() => {
1493
1926
  const newIdx = Math.min(visibleItems.length - 1, selectedIndex + 1);
1494
1927
  const item = visibleItems[newIdx];
1495
1928
  if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
1496
1929
  }, [selectedIndex, visibleItems]);
1497
- const nextSection = useCallback4(() => {
1930
+ const nextSection = useCallback5(() => {
1498
1931
  const currentItem = visibleItems[selectedIndex];
1499
1932
  if (!currentItem) return;
1500
1933
  const currentSectionIdx = state.sections.indexOf(currentItem.section);
@@ -1503,7 +1936,7 @@ function useNavigation(allItems) {
1503
1936
  const header = visibleItems.find((i) => i.section === nextSectionId && i.type === "header");
1504
1937
  if (header) dispatch({ type: "SELECT", id: header.id, section: header.section });
1505
1938
  }, [selectedIndex, visibleItems, state.sections]);
1506
- const prevSection = useCallback4(() => {
1939
+ const prevSection = useCallback5(() => {
1507
1940
  const currentItem = visibleItems[selectedIndex];
1508
1941
  if (!currentItem) return;
1509
1942
  const currentSectionIdx = state.sections.indexOf(currentItem.section);
@@ -1512,19 +1945,22 @@ function useNavigation(allItems) {
1512
1945
  const header = visibleItems.find((i) => i.section === prevSectionId && i.type === "header");
1513
1946
  if (header) dispatch({ type: "SELECT", id: header.id, section: header.section });
1514
1947
  }, [selectedIndex, visibleItems, state.sections]);
1515
- const toggleSection = useCallback4(() => {
1948
+ const toggleSection = useCallback5(() => {
1516
1949
  const currentItem = visibleItems[selectedIndex];
1517
1950
  if (!currentItem) return;
1518
1951
  const key = currentItem.type === "subHeader" ? currentItem.id : currentItem.section;
1519
1952
  dispatch({ type: "TOGGLE_SECTION", section: key });
1520
1953
  }, [selectedIndex, visibleItems]);
1954
+ const collapseAll = useCallback5(() => {
1955
+ dispatch({ type: "COLLAPSE_ALL" });
1956
+ }, []);
1521
1957
  const allItemsRef = useRef4(allItems);
1522
1958
  allItemsRef.current = allItems;
1523
- const select2 = useCallback4((id) => {
1959
+ const select2 = useCallback5((id) => {
1524
1960
  const item = allItemsRef.current.find((i) => i.id === id);
1525
1961
  dispatch({ type: "SELECT", id, section: item?.section });
1526
1962
  }, []);
1527
- const isCollapsed = useCallback4(
1963
+ const isCollapsed = useCallback5(
1528
1964
  (section) => state.collapsedSections.has(section),
1529
1965
  [state.collapsedSections]
1530
1966
  );
@@ -1537,6 +1973,7 @@ function useNavigation(allItems) {
1537
1973
  nextSection,
1538
1974
  prevSection,
1539
1975
  toggleSection,
1976
+ collapseAll,
1540
1977
  select: select2,
1541
1978
  isCollapsed
1542
1979
  };
@@ -1548,25 +1985,25 @@ var init_use_navigation = __esm({
1548
1985
  });
1549
1986
 
1550
1987
  // src/board/hooks/use-toast.ts
1551
- 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";
1552
1989
  function useToast() {
1553
1990
  const [toasts, setToasts] = useState3([]);
1554
1991
  const timersRef = useRef5(/* @__PURE__ */ new Map());
1555
- const clearTimer = useCallback5((id) => {
1992
+ const clearTimer = useCallback6((id) => {
1556
1993
  const timer = timersRef.current.get(id);
1557
1994
  if (timer) {
1558
1995
  clearTimeout(timer);
1559
1996
  timersRef.current.delete(id);
1560
1997
  }
1561
1998
  }, []);
1562
- const removeToast = useCallback5(
1999
+ const removeToast = useCallback6(
1563
2000
  (id) => {
1564
2001
  clearTimer(id);
1565
2002
  setToasts((prev) => prev.filter((t) => t.id !== id));
1566
2003
  },
1567
2004
  [clearTimer]
1568
2005
  );
1569
- const addToast = useCallback5(
2006
+ const addToast = useCallback6(
1570
2007
  (t) => {
1571
2008
  const id = `toast-${++nextId}`;
1572
2009
  const newToast = { ...t, id, createdAt: Date.now() };
@@ -1595,25 +2032,25 @@ function useToast() {
1595
2032
  [removeToast, clearTimer]
1596
2033
  );
1597
2034
  const toast = {
1598
- info: useCallback5(
2035
+ info: useCallback6(
1599
2036
  (message) => {
1600
2037
  addToast({ type: "info", message });
1601
2038
  },
1602
2039
  [addToast]
1603
2040
  ),
1604
- success: useCallback5(
2041
+ success: useCallback6(
1605
2042
  (message) => {
1606
2043
  addToast({ type: "success", message });
1607
2044
  },
1608
2045
  [addToast]
1609
2046
  ),
1610
- error: useCallback5(
2047
+ error: useCallback6(
1611
2048
  (message, retry) => {
1612
2049
  addToast(retry ? { type: "error", message, retry } : { type: "error", message });
1613
2050
  },
1614
2051
  [addToast]
1615
2052
  ),
1616
- loading: useCallback5(
2053
+ loading: useCallback6(
1617
2054
  (message) => {
1618
2055
  const id = addToast({ type: "loading", message });
1619
2056
  return {
@@ -1630,20 +2067,20 @@ function useToast() {
1630
2067
  [addToast, removeToast]
1631
2068
  )
1632
2069
  };
1633
- const dismiss = useCallback5(
2070
+ const dismiss = useCallback6(
1634
2071
  (id) => {
1635
2072
  removeToast(id);
1636
2073
  },
1637
2074
  [removeToast]
1638
2075
  );
1639
- const dismissAll = useCallback5(() => {
2076
+ const dismissAll = useCallback6(() => {
1640
2077
  for (const timer of timersRef.current.values()) {
1641
2078
  clearTimeout(timer);
1642
2079
  }
1643
2080
  timersRef.current.clear();
1644
2081
  setToasts([]);
1645
2082
  }, []);
1646
- const handleErrorAction = useCallback5(
2083
+ const handleErrorAction = useCallback6(
1647
2084
  (action) => {
1648
2085
  const errorToast = toasts.find((t) => t.type === "error");
1649
2086
  if (!errorToast) return false;
@@ -1673,7 +2110,7 @@ var init_use_toast = __esm({
1673
2110
  });
1674
2111
 
1675
2112
  // src/board/hooks/use-ui-state.ts
1676
- import { useCallback as useCallback6, useReducer as useReducer2 } from "react";
2113
+ import { useCallback as useCallback7, useReducer as useReducer2 } from "react";
1677
2114
  function uiReducer(state, action) {
1678
2115
  switch (action.type) {
1679
2116
  case "ENTER_SEARCH":
@@ -1692,6 +2129,12 @@ function uiReducer(state, action) {
1692
2129
  case "ENTER_CREATE":
1693
2130
  if (state.mode !== "normal") return state;
1694
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" };
1695
2138
  case "ENTER_MULTI_SELECT":
1696
2139
  if (state.mode !== "normal" && state.mode !== "multiSelect") return state;
1697
2140
  return { ...state, mode: "multiSelect", previousMode: "normal" };
@@ -1735,18 +2178,20 @@ function useUIState() {
1735
2178
  const [state, dispatch] = useReducer2(uiReducer, INITIAL_STATE2);
1736
2179
  return {
1737
2180
  state,
1738
- enterSearch: useCallback6(() => dispatch({ type: "ENTER_SEARCH" }), []),
1739
- enterComment: useCallback6(() => dispatch({ type: "ENTER_COMMENT" }), []),
1740
- enterStatus: useCallback6(() => dispatch({ type: "ENTER_STATUS" }), []),
1741
- enterCreate: useCallback6(() => dispatch({ type: "ENTER_CREATE" }), []),
1742
- enterMultiSelect: useCallback6(() => dispatch({ type: "ENTER_MULTI_SELECT" }), []),
1743
- enterBulkAction: useCallback6(() => dispatch({ type: "ENTER_BULK_ACTION" }), []),
1744
- enterConfirmPick: useCallback6(() => dispatch({ type: "ENTER_CONFIRM_PICK" }), []),
1745
- enterFocus: useCallback6(() => dispatch({ type: "ENTER_FOCUS" }), []),
1746
- toggleHelp: useCallback6(() => dispatch({ type: "TOGGLE_HELP" }), []),
1747
- exitOverlay: useCallback6(() => dispatch({ type: "EXIT_OVERLAY" }), []),
1748
- exitToNormal: useCallback6(() => dispatch({ type: "EXIT_TO_NORMAL" }), []),
1749
- 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" }), []),
1750
2195
  canNavigate: canNavigate(state),
1751
2196
  canAct: canAct(state),
1752
2197
  isOverlay: isOverlay(state)
@@ -1764,34 +2209,202 @@ var init_use_ui_state = __esm({
1764
2209
  }
1765
2210
  });
1766
2211
 
1767
- // src/board/components/bulk-action-menu.tsx
1768
- import { Box, Text, useInput } from "ink";
1769
- import { useState as useState4 } from "react";
1770
- import { jsx, jsxs } from "react/jsx-runtime";
1771
- function getMenuItems(selectionType) {
1772
- if (selectionType === "github") {
1773
- return [
1774
- { label: "Assign all to me", action: { type: "assign" } },
1775
- { label: "Unassign all from me", action: { type: "unassign" } },
1776
- { label: "Move status (all)", action: { type: "statusChange" } }
1777
- ];
1778
- }
1779
- if (selectionType === "ticktick") {
1780
- return [
1781
- { label: "Complete all", action: { type: "complete" } },
1782
- { label: "Delete all", action: { type: "delete" } }
1783
- ];
1784
- }
1785
- 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");
1786
2218
  }
1787
- function BulkActionMenu({ count, selectionType, onSelect, onCancel }) {
1788
- const items = getMenuItems(selectionType);
1789
- const [selectedIdx, setSelectedIdx] = useState4(0);
1790
- useInput((input2, key) => {
1791
- if (key.escape) return onCancel();
1792
- if (key.return) {
1793
- const item = items[selectedIdx];
1794
- if (item) onSelect(item.action);
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);
1795
2408
  return;
1796
2409
  }
1797
2410
  if (input2 === "j" || key.downArrow) {
@@ -1802,13 +2415,13 @@ function BulkActionMenu({ count, selectionType, onSelect, onCancel }) {
1802
2415
  }
1803
2416
  });
1804
2417
  if (items.length === 0) {
1805
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1806
- /* @__PURE__ */ jsx(Text, { color: "yellow", children: "No bulk actions for mixed selection types." }),
1807
- /* @__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" })
1808
2421
  ] });
1809
2422
  }
1810
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1811
- /* @__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: [
1812
2425
  "Bulk action (",
1813
2426
  count,
1814
2427
  " selected):"
@@ -1816,12 +2429,12 @@ function BulkActionMenu({ count, selectionType, onSelect, onCancel }) {
1816
2429
  items.map((item, i) => {
1817
2430
  const isSelected = i === selectedIdx;
1818
2431
  const prefix = isSelected ? "> " : " ";
1819
- return /* @__PURE__ */ jsxs(Text, { ...isSelected ? { color: "cyan" } : {}, children: [
2432
+ return /* @__PURE__ */ jsxs2(Text2, { ...isSelected ? { color: "cyan" } : {}, children: [
1820
2433
  prefix,
1821
2434
  item.label
1822
2435
  ] }, item.action.type);
1823
2436
  }),
1824
- /* @__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" })
1825
2438
  ] });
1826
2439
  }
1827
2440
  var init_bulk_action_menu = __esm({
@@ -1830,27 +2443,113 @@ var init_bulk_action_menu = __esm({
1830
2443
  }
1831
2444
  });
1832
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
+
1833
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";
1834
2466
  import { TextInput } from "@inkjs/ui";
1835
- import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
1836
- import { useState as useState5 } from "react";
1837
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1838
- 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
+ }) {
1839
2477
  const [value, setValue] = useState5("");
1840
- useInput2((_input, key) => {
1841
- 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
+ }
1842
2497
  });
1843
- return /* @__PURE__ */ jsxs2(Box2, { children: [
1844
- /* @__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: [
1845
2544
  "comment #",
1846
2545
  issueNumber,
1847
2546
  ": "
1848
2547
  ] }),
1849
- /* @__PURE__ */ jsx2(
2548
+ /* @__PURE__ */ jsx3(
1850
2549
  TextInput,
1851
2550
  {
1852
2551
  defaultValue: value,
1853
- placeholder: "type comment, Enter to post...",
2552
+ placeholder: "type comment (ctrl+e for editor), Enter to post...",
1854
2553
  onChange: setValue,
1855
2554
  onSubmit: (text) => {
1856
2555
  if (text.trim()) onSubmit(text.trim());
@@ -1863,20 +2562,21 @@ function CommentInput({ issueNumber, onSubmit, onCancel }) {
1863
2562
  var init_comment_input = __esm({
1864
2563
  "src/board/components/comment-input.tsx"() {
1865
2564
  "use strict";
2565
+ init_ink_instance();
1866
2566
  }
1867
2567
  });
1868
2568
 
1869
2569
  // src/board/components/confirm-prompt.tsx
1870
- import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
1871
- 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";
1872
2572
  function ConfirmPrompt({ message, onConfirm, onCancel }) {
1873
- useInput3((input2, key) => {
2573
+ useInput4((input2, key) => {
1874
2574
  if (input2 === "y" || input2 === "Y") return onConfirm();
1875
2575
  if (input2 === "n" || input2 === "N" || key.escape) return onCancel();
1876
2576
  });
1877
- return /* @__PURE__ */ jsxs3(Box3, { children: [
1878
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: message }),
1879
- /* @__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)" })
1880
2580
  ] });
1881
2581
  }
1882
2582
  var init_confirm_prompt = __esm({
@@ -1885,20 +2585,146 @@ var init_confirm_prompt = __esm({
1885
2585
  }
1886
2586
  });
1887
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
+
1888
2707
  // src/board/components/create-issue-form.tsx
1889
2708
  import { TextInput as TextInput2 } from "@inkjs/ui";
1890
- import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
1891
- import { useState as useState6 } from "react";
1892
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1893
- 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
+ }) {
1894
2719
  const defaultRepoIdx = defaultRepo ? Math.max(
1895
2720
  0,
1896
2721
  repos.findIndex((r) => r.name === defaultRepo)
1897
2722
  ) : 0;
1898
- const [repoIdx, setRepoIdx] = useState6(defaultRepoIdx);
1899
- const [title, setTitle] = useState6("");
1900
- const [field, setField] = useState6("title");
1901
- 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;
1902
2728
  if (key.escape) return onCancel();
1903
2729
  if (field === "repo") {
1904
2730
  if (input2 === "j" || key.downArrow) {
@@ -1912,12 +2738,40 @@ function CreateIssueForm({ repos, defaultRepo, onSubmit, onCancel }) {
1912
2738
  }
1913
2739
  });
1914
2740
  const selectedRepo = repos[repoIdx];
1915
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
1916
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "Create Issue" }),
1917
- /* @__PURE__ */ jsxs4(Box4, { children: [
1918
- /* @__PURE__ */ jsx4(Text4, { dimColor: field !== "repo", children: "Repo: " }),
1919
- repos.map((r, i) => /* @__PURE__ */ jsx4(
1920
- 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,
1921
2775
  {
1922
2776
  ...i === repoIdx ? { color: "cyan", bold: true } : {},
1923
2777
  dimColor: field !== "repo",
@@ -1925,215 +2779,53 @@ function CreateIssueForm({ repos, defaultRepo, onSubmit, onCancel }) {
1925
2779
  },
1926
2780
  r.name
1927
2781
  )),
1928
- 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
1929
2783
  ] }),
1930
- /* @__PURE__ */ jsxs4(Box4, { children: [
1931
- /* @__PURE__ */ jsx4(Text4, { dimColor: field !== "title", children: "Title: " }),
1932
- field === "title" ? /* @__PURE__ */ jsx4(
2784
+ /* @__PURE__ */ jsxs6(Box6, { children: [
2785
+ /* @__PURE__ */ jsx6(Text6, { dimColor: field !== "title", children: "Title: " }),
2786
+ field === "title" ? /* @__PURE__ */ jsx6(
1933
2787
  TextInput2,
1934
2788
  {
1935
2789
  defaultValue: title,
1936
2790
  placeholder: "issue title...",
1937
2791
  onChange: setTitle,
1938
2792
  onSubmit: (text) => {
1939
- if (text.trim() && selectedRepo) {
1940
- 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);
1941
2800
  }
1942
2801
  }
1943
2802
  }
1944
- ) : /* @__PURE__ */ jsx4(Text4, { children: title || "(empty)" })
2803
+ ) : /* @__PURE__ */ jsx6(Text6, { children: title || "(empty)" })
1945
2804
  ] }),
1946
- /* @__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" })
1947
2806
  ] });
1948
2807
  }
1949
2808
  var init_create_issue_form = __esm({
1950
2809
  "src/board/components/create-issue-form.tsx"() {
1951
2810
  "use strict";
1952
- }
1953
- });
1954
-
1955
- // src/board/components/detail-panel.tsx
1956
- import { Box as Box5, Text as Text5 } from "ink";
1957
- import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1958
- function truncateLines(text, maxLines) {
1959
- const lines = text.split("\n").slice(0, maxLines);
1960
- return lines.join("\n");
1961
- }
1962
- function stripMarkdown(text) {
1963
- 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();
1964
- }
1965
- function formatBody(body, maxLines) {
1966
- const plain = stripMarkdown(body);
1967
- const lines = plain.split("\n");
1968
- const truncated = lines.slice(0, maxLines).join("\n");
1969
- return { text: truncated, remaining: Math.max(0, lines.length - maxLines) };
1970
- }
1971
- function countSlackLinks(body) {
1972
- if (!body) return 0;
1973
- return (body.match(SLACK_URL_RE) ?? []).length;
1974
- }
1975
- function BodySection({
1976
- body,
1977
- issueNumber
1978
- }) {
1979
- const { text, remaining } = formatBody(body, 15);
1980
- return /* @__PURE__ */ jsxs5(Fragment, { children: [
1981
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
1982
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "--- Description ---" }),
1983
- /* @__PURE__ */ jsx5(Text5, { wrap: "wrap", children: text }),
1984
- remaining > 0 ? /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1985
- "... (",
1986
- remaining,
1987
- " more lines \u2014 gh issue view ",
1988
- issueNumber,
1989
- " for full)"
1990
- ] }) : null
1991
- ] });
1992
- }
1993
- function DetailPanel({ issue, task: task2, width }) {
1994
- if (!(issue || task2)) {
1995
- return /* @__PURE__ */ jsx5(
1996
- Box5,
1997
- {
1998
- width,
1999
- borderStyle: "single",
2000
- borderColor: "gray",
2001
- flexDirection: "column",
2002
- paddingX: 1,
2003
- children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "No item selected" })
2004
- }
2005
- );
2006
- }
2007
- if (issue) {
2008
- return /* @__PURE__ */ jsxs5(
2009
- Box5,
2010
- {
2011
- width,
2012
- borderStyle: "single",
2013
- borderColor: "cyan",
2014
- flexDirection: "column",
2015
- paddingX: 1,
2016
- children: [
2017
- /* @__PURE__ */ jsxs5(Text5, { color: "cyan", bold: true, children: [
2018
- "#",
2019
- issue.number,
2020
- " ",
2021
- issue.title
2022
- ] }),
2023
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2024
- /* @__PURE__ */ jsxs5(Box5, { children: [
2025
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "State: " }),
2026
- /* @__PURE__ */ jsx5(Text5, { color: issue.state === "open" ? "green" : "red", children: issue.state })
2027
- ] }),
2028
- (issue.assignees ?? []).length > 0 ? /* @__PURE__ */ jsxs5(Box5, { children: [
2029
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Assignees: " }),
2030
- /* @__PURE__ */ jsx5(Text5, { children: (issue.assignees ?? []).map((a) => a.login).join(", ") })
2031
- ] }) : null,
2032
- issue.labels.length > 0 ? /* @__PURE__ */ jsxs5(Box5, { children: [
2033
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Labels: " }),
2034
- /* @__PURE__ */ jsx5(Text5, { children: issue.labels.map((l) => l.name).join(", ") })
2035
- ] }) : null,
2036
- issue.projectStatus ? /* @__PURE__ */ jsxs5(Box5, { children: [
2037
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Status: " }),
2038
- /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: issue.projectStatus })
2039
- ] }) : null,
2040
- issue.targetDate ? /* @__PURE__ */ jsxs5(Box5, { children: [
2041
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Target: " }),
2042
- /* @__PURE__ */ jsx5(Text5, { children: issue.targetDate })
2043
- ] }) : null,
2044
- /* @__PURE__ */ jsxs5(Box5, { children: [
2045
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Updated: " }),
2046
- /* @__PURE__ */ jsx5(Text5, { children: new Date(issue.updatedAt).toLocaleString() })
2047
- ] }),
2048
- issue.slackThreadUrl ? /* @__PURE__ */ jsxs5(Box5, { children: [
2049
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Slack: " }),
2050
- /* @__PURE__ */ jsx5(Text5, { color: "blue", children: countSlackLinks(issue.body) > 1 ? `${countSlackLinks(issue.body)} links (s opens first)` : "thread (s to open)" })
2051
- ] }) : null,
2052
- issue.body ? /* @__PURE__ */ jsx5(BodySection, { body: issue.body, issueNumber: issue.number }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
2053
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2054
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "(no description)" })
2055
- ] }),
2056
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2057
- /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: issue.url })
2058
- ]
2059
- }
2060
- );
2061
- }
2062
- const t = task2;
2063
- return /* @__PURE__ */ jsxs5(
2064
- Box5,
2065
- {
2066
- width,
2067
- borderStyle: "single",
2068
- borderColor: "yellow",
2069
- flexDirection: "column",
2070
- paddingX: 1,
2071
- children: [
2072
- /* @__PURE__ */ jsx5(Text5, { color: "yellow", bold: true, children: t.title }),
2073
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2074
- /* @__PURE__ */ jsxs5(Box5, { children: [
2075
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Priority: " }),
2076
- /* @__PURE__ */ jsx5(Text5, { children: PRIORITY_LABELS2[t.priority] ?? "None" })
2077
- ] }),
2078
- t.dueDate ? /* @__PURE__ */ jsxs5(Box5, { children: [
2079
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Due: " }),
2080
- /* @__PURE__ */ jsx5(Text5, { children: new Date(t.dueDate).toLocaleDateString() })
2081
- ] }) : null,
2082
- (t.tags ?? []).length > 0 ? /* @__PURE__ */ jsxs5(Box5, { children: [
2083
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Tags: " }),
2084
- /* @__PURE__ */ jsx5(Text5, { children: t.tags.join(", ") })
2085
- ] }) : null,
2086
- t.content ? /* @__PURE__ */ jsxs5(Fragment, { children: [
2087
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2088
- /* @__PURE__ */ jsx5(Text5, { children: truncateLines(t.content, 8) })
2089
- ] }) : null,
2090
- (t.items ?? []).length > 0 ? /* @__PURE__ */ jsxs5(Fragment, { children: [
2091
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2092
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Checklist:" }),
2093
- t.items.slice(0, 5).map((item) => /* @__PURE__ */ jsxs5(Text5, { children: [
2094
- item.status === 2 ? "\u2611" : "\u2610",
2095
- " ",
2096
- item.title
2097
- ] }, item.id)),
2098
- t.items.length > 5 ? /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
2099
- "...and ",
2100
- t.items.length - 5,
2101
- " more"
2102
- ] }) : null
2103
- ] }) : null
2104
- ]
2105
- }
2106
- );
2107
- }
2108
- var SLACK_URL_RE, PRIORITY_LABELS2;
2109
- var init_detail_panel = __esm({
2110
- "src/board/components/detail-panel.tsx"() {
2111
- "use strict";
2112
- init_types();
2113
- SLACK_URL_RE = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/gi;
2114
- PRIORITY_LABELS2 = {
2115
- [5 /* High */]: "High",
2116
- [3 /* Medium */]: "Medium",
2117
- [1 /* Low */]: "Low",
2118
- [0 /* None */]: "None"
2119
- };
2811
+ init_label_picker();
2120
2812
  }
2121
2813
  });
2122
2814
 
2123
2815
  // src/board/components/focus-mode.tsx
2124
- import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
2125
- import { useCallback as useCallback7, useEffect as useEffect2, useRef as useRef6, useState as useState7 } from "react";
2126
- 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";
2127
2819
  function formatTime(secs) {
2128
2820
  const m = Math.floor(secs / 60);
2129
2821
  const s = secs % 60;
2130
2822
  return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
2131
2823
  }
2132
2824
  function FocusMode({ label, durationSec, onExit, onEndAction }) {
2133
- const [remaining, setRemaining] = useState7(durationSec);
2134
- const [timerDone, setTimerDone] = useState7(false);
2135
- const bellSentRef = useRef6(false);
2136
- useEffect2(() => {
2825
+ const [remaining, setRemaining] = useState8(durationSec);
2826
+ const [timerDone, setTimerDone] = useState8(false);
2827
+ const bellSentRef = useRef8(false);
2828
+ useEffect4(() => {
2137
2829
  if (timerDone) return;
2138
2830
  const interval = setInterval(() => {
2139
2831
  setRemaining((prev) => {
@@ -2147,13 +2839,13 @@ function FocusMode({ label, durationSec, onExit, onEndAction }) {
2147
2839
  }, 1e3);
2148
2840
  return () => clearInterval(interval);
2149
2841
  }, [timerDone]);
2150
- useEffect2(() => {
2842
+ useEffect4(() => {
2151
2843
  if (timerDone && !bellSentRef.current) {
2152
2844
  bellSentRef.current = true;
2153
2845
  process.stdout.write("\x07");
2154
2846
  }
2155
2847
  }, [timerDone]);
2156
- const handleInput = useCallback7(
2848
+ const handleInput = useCallback8(
2157
2849
  (input2, key) => {
2158
2850
  if (key.escape) {
2159
2851
  if (timerDone) {
@@ -2178,25 +2870,25 @@ function FocusMode({ label, durationSec, onExit, onEndAction }) {
2178
2870
  },
2179
2871
  [timerDone, onExit, onEndAction]
2180
2872
  );
2181
- useInput5(handleInput);
2873
+ useInput7(handleInput);
2182
2874
  if (timerDone) {
2183
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
2184
- /* @__PURE__ */ jsxs6(Box6, { children: [
2185
- /* @__PURE__ */ jsx6(Text6, { color: "green", bold: true, children: "Focus complete!" }),
2186
- /* @__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: [
2187
2879
  " ",
2188
2880
  label
2189
2881
  ] })
2190
2882
  ] }),
2191
- /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
2192
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "[c]" }),
2193
- /* @__PURE__ */ jsx6(Text6, { children: " Continue " }),
2194
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "[b]" }),
2195
- /* @__PURE__ */ jsx6(Text6, { children: " Break " }),
2196
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "[d]" }),
2197
- /* @__PURE__ */ jsx6(Text6, { children: " Done " }),
2198
- /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "[Esc]" }),
2199
- /* @__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" })
2200
2892
  ] })
2201
2893
  ] });
2202
2894
  }
@@ -2204,21 +2896,21 @@ function FocusMode({ label, durationSec, onExit, onEndAction }) {
2204
2896
  const barWidth = 20;
2205
2897
  const filled = Math.round(progress * barWidth);
2206
2898
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
2207
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
2208
- /* @__PURE__ */ jsxs6(Box6, { children: [
2209
- /* @__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: [
2210
2902
  "Focus:",
2211
2903
  " "
2212
2904
  ] }),
2213
- /* @__PURE__ */ jsx6(Text6, { children: label })
2905
+ /* @__PURE__ */ jsx7(Text7, { children: label })
2214
2906
  ] }),
2215
- /* @__PURE__ */ jsxs6(Box6, { children: [
2216
- /* @__PURE__ */ jsx6(Text6, { color: "magenta", children: bar }),
2217
- /* @__PURE__ */ jsx6(Text6, { children: " " }),
2218
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: formatTime(remaining) }),
2219
- /* @__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" })
2220
2912
  ] }),
2221
- /* @__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" })
2222
2914
  ] });
2223
2915
  }
2224
2916
  var init_focus_mode = __esm({
@@ -2228,29 +2920,29 @@ var init_focus_mode = __esm({
2228
2920
  });
2229
2921
 
2230
2922
  // src/board/components/help-overlay.tsx
2231
- import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
2232
- 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";
2233
2925
  function HelpOverlay({ currentMode, onClose }) {
2234
- useInput6((_input, key) => {
2926
+ useInput8((_input, key) => {
2235
2927
  if (key.escape) onClose();
2236
2928
  });
2237
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2238
- /* @__PURE__ */ jsxs7(Box7, { justifyContent: "space-between", children: [
2239
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", bold: true, children: "Keyboard Shortcuts" }),
2240
- /* @__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: [
2241
2933
  "mode: ",
2242
2934
  currentMode
2243
2935
  ] })
2244
2936
  ] }),
2245
- /* @__PURE__ */ jsx7(Text7, { children: " " }),
2246
- SHORTCUTS.map((group) => /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
2247
- /* @__PURE__ */ jsx7(Text7, { color: "yellow", bold: true, children: group.category }),
2248
- group.items.map((item) => /* @__PURE__ */ jsxs7(Box7, { children: [
2249
- /* @__PURE__ */ jsx7(Box7, { width: 16, children: /* @__PURE__ */ jsx7(Text7, { color: "green", children: item.key }) }),
2250
- /* @__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 })
2251
2943
  ] }, item.key))
2252
2944
  ] }, group.category)),
2253
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press ? or Esc to close" })
2945
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Press ? or Esc to close" })
2254
2946
  ] });
2255
2947
  }
2256
2948
  var SHORTCUTS;
@@ -2286,23 +2978,439 @@ var init_help_overlay = __esm({
2286
2978
  { key: "c", desc: "Comment on issue" },
2287
2979
  { key: "m", desc: "Move status" },
2288
2980
  { key: "s", desc: "Open Slack thread" },
2981
+ { key: "y", desc: "Copy issue link to clipboard" },
2289
2982
  { key: "n", desc: "Create new issue" }
2290
2983
  ]
2291
2984
  },
2292
2985
  {
2293
- category: "Board",
2294
- items: [
2295
- { key: "r", desc: "Refresh data" },
2296
- { key: "q", desc: "Quit" }
2297
- ]
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
3357
+ },
3358
+ focusKey
3359
+ ) : null,
3360
+ mode === "overlay:label" && selectedIssue && defaultRepo ? /* @__PURE__ */ jsx12(
3361
+ LabelPicker,
3362
+ {
3363
+ repo: defaultRepo,
3364
+ currentLabels: selectedIssue.labels.map((l) => l.name),
3365
+ labelCache,
3366
+ onConfirm: onLabelConfirm,
3367
+ onCancel: onExitOverlay,
3368
+ onError: onLabelError
2298
3369
  }
2299
- ];
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();
2300
3408
  }
2301
3409
  });
2302
3410
 
2303
3411
  // src/board/components/issue-row.tsx
2304
- import { Box as Box8, Text as Text8 } from "ink";
2305
- 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";
2306
3414
  function truncate(s, max) {
2307
3415
  return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
2308
3416
  }
@@ -2343,30 +3451,30 @@ function IssueRow({ issue, selfLogin, isSelected }) {
2343
3451
  const labels = (issue.labels ?? []).slice(0, 2);
2344
3452
  const target = formatTargetDate(issue.targetDate);
2345
3453
  const titleStr = truncate(issue.title, 42).padEnd(42);
2346
- return /* @__PURE__ */ jsxs8(Box8, { children: [
2347
- isSelected ? /* @__PURE__ */ jsx8(Text8, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx8(Text8, { children: " " }),
2348
- /* @__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: [
2349
3457
  "#",
2350
3458
  String(issue.number).padEnd(5)
2351
3459
  ] }),
2352
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2353
- isSelected ? /* @__PURE__ */ jsx8(Text8, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx8(Text8, { children: titleStr }),
2354
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2355
- /* @__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: [
2356
3464
  i > 0 ? " " : "",
2357
- /* @__PURE__ */ jsxs8(Text8, { color: labelColor(l.name), children: [
3465
+ /* @__PURE__ */ jsxs13(Text12, { color: labelColor(l.name), children: [
2358
3466
  "[",
2359
3467
  truncate(l.name, 12),
2360
3468
  "]"
2361
3469
  ] })
2362
3470
  ] }, l.name)) }),
2363
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2364
- /* @__PURE__ */ jsx8(Text8, { color: assigneeColor, children: assigneeText.padEnd(14) }),
2365
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2366
- /* @__PURE__ */ jsx8(Text8, { color: "gray", children: timeAgo(issue.updatedAt).padStart(4) }),
2367
- target.text ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
2368
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2369
- /* @__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 })
2370
3478
  ] }) : null
2371
3479
  ] });
2372
3480
  }
@@ -2389,90 +3497,13 @@ var init_issue_row = __esm({
2389
3497
  }
2390
3498
  });
2391
3499
 
2392
- // src/board/components/search-bar.tsx
2393
- import { TextInput as TextInput3 } from "@inkjs/ui";
2394
- import { Box as Box9, Text as Text9 } from "ink";
2395
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2396
- function SearchBar({ defaultValue, onChange, onSubmit }) {
2397
- return /* @__PURE__ */ jsxs9(Box9, { children: [
2398
- /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "/" }),
2399
- /* @__PURE__ */ jsx9(
2400
- TextInput3,
2401
- {
2402
- defaultValue,
2403
- placeholder: "search...",
2404
- onChange,
2405
- onSubmit
2406
- }
2407
- )
2408
- ] });
2409
- }
2410
- var init_search_bar = __esm({
2411
- "src/board/components/search-bar.tsx"() {
2412
- "use strict";
2413
- }
2414
- });
2415
-
2416
- // src/board/components/status-picker.tsx
2417
- import { Box as Box10, Text as Text10, useInput as useInput7 } from "ink";
2418
- import { useState as useState8 } from "react";
2419
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2420
- function StatusPicker({ options, currentStatus, onSelect, onCancel }) {
2421
- const [selectedIdx, setSelectedIdx] = useState8(() => {
2422
- const idx = options.findIndex((o) => o.name === currentStatus);
2423
- return idx >= 0 ? idx : 0;
2424
- });
2425
- useInput7((input2, key) => {
2426
- if (key.escape) return onCancel();
2427
- if (key.return) {
2428
- const opt = options[selectedIdx];
2429
- if (opt) onSelect(opt.id);
2430
- return;
2431
- }
2432
- if (input2 === "j" || key.downArrow) {
2433
- setSelectedIdx((i) => Math.min(i + 1, options.length - 1));
2434
- }
2435
- if (input2 === "k" || key.upArrow) {
2436
- setSelectedIdx((i) => Math.max(i - 1, 0));
2437
- }
2438
- });
2439
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
2440
- /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: "Move to status:" }),
2441
- options.map((opt, i) => {
2442
- const isCurrent = opt.name === currentStatus;
2443
- const isSelected = i === selectedIdx;
2444
- const prefix = isSelected ? "> " : " ";
2445
- const suffix = isCurrent ? " (current)" : "";
2446
- return /* @__PURE__ */ jsxs10(
2447
- Text10,
2448
- {
2449
- ...isSelected ? { color: "cyan" } : {},
2450
- dimColor: isCurrent,
2451
- children: [
2452
- prefix,
2453
- opt.name,
2454
- suffix
2455
- ]
2456
- },
2457
- opt.id
2458
- );
2459
- }),
2460
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "j/k:navigate Enter:select Esc:cancel" })
2461
- ] });
2462
- }
2463
- var init_status_picker = __esm({
2464
- "src/board/components/status-picker.tsx"() {
2465
- "use strict";
2466
- }
2467
- });
2468
-
2469
3500
  // src/board/components/task-row.tsx
2470
- import { Box as Box11, Text as Text11 } from "ink";
2471
- 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";
2472
3503
  function truncate2(s, max) {
2473
3504
  return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
2474
3505
  }
2475
- function formatDue(dateStr) {
3506
+ function formatDue2(dateStr) {
2476
3507
  if (!dateStr) return { text: "", color: "gray" };
2477
3508
  const d = new Date(dateStr);
2478
3509
  const days = Math.ceil((d.getTime() - Date.now()) / 864e5);
@@ -2487,15 +3518,15 @@ function formatDue(dateStr) {
2487
3518
  }
2488
3519
  function TaskRow({ task: task2, isSelected }) {
2489
3520
  const pri = PRIORITY_INDICATORS[task2.priority] ?? DEFAULT_PRIORITY;
2490
- const due = formatDue(task2.dueDate);
3521
+ const due = formatDue2(task2.dueDate);
2491
3522
  const titleStr = truncate2(task2.title, 45).padEnd(45);
2492
- return /* @__PURE__ */ jsxs11(Box11, { children: [
2493
- isSelected ? /* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx11(Text11, { children: " " }),
2494
- /* @__PURE__ */ jsx11(Text11, { color: pri.color, children: pri.text }),
2495
- /* @__PURE__ */ jsx11(Text11, { children: " " }),
2496
- isSelected ? /* @__PURE__ */ jsx11(Text11, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx11(Text11, { children: titleStr }),
2497
- /* @__PURE__ */ jsx11(Text11, { children: " " }),
2498
- /* @__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 })
2499
3530
  ] });
2500
3531
  }
2501
3532
  var PRIORITY_INDICATORS, DEFAULT_PRIORITY;
@@ -2513,23 +3544,128 @@ var init_task_row = __esm({
2513
3544
  }
2514
3545
  });
2515
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
+
2516
3652
  // src/board/components/toast-container.tsx
2517
- import { Spinner } from "@inkjs/ui";
2518
- import { Box as Box12, Text as Text12 } from "ink";
2519
- 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";
2520
3656
  function ToastContainer({ toasts }) {
2521
3657
  if (toasts.length === 0) return null;
2522
- return /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", children: toasts.map((t) => /* @__PURE__ */ jsx12(Box12, { children: t.type === "loading" ? /* @__PURE__ */ jsxs12(Fragment3, { children: [
2523
- /* @__PURE__ */ jsx12(Spinner, { label: "" }),
2524
- /* @__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: [
2525
3661
  " ",
2526
3662
  t.message
2527
3663
  ] })
2528
- ] }) : /* @__PURE__ */ jsxs12(Text12, { color: TYPE_COLORS[t.type], children: [
3664
+ ] }) : /* @__PURE__ */ jsxs16(Text15, { color: TYPE_COLORS[t.type], children: [
2529
3665
  TYPE_PREFIXES[t.type],
2530
3666
  " ",
2531
3667
  t.message,
2532
- 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
2533
3669
  ] }) }, t.id)) });
2534
3670
  }
2535
3671
  var TYPE_COLORS, TYPE_PREFIXES;
@@ -2551,13 +3687,13 @@ var init_toast_container = __esm({
2551
3687
  });
2552
3688
 
2553
3689
  // src/board/components/dashboard.tsx
2554
- import { execFileSync as execFileSync3 } from "child_process";
2555
- import { Spinner as Spinner2 } from "@inkjs/ui";
2556
- import { Box as Box13, Text as Text13, useApp, useInput as useInput8, useStdout } from "ink";
2557
- import { useCallback as useCallback8, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef7, useState as useState9 } from "react";
2558
- 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";
2559
3695
  function isTerminalStatus(status) {
2560
- return TERMINAL_STATUS_RE2.test(status);
3696
+ return TERMINAL_STATUS_RE3.test(status);
2561
3697
  }
2562
3698
  function resolveStatusGroups(statusOptions, configuredGroups) {
2563
3699
  if (configuredGroups && configuredGroups.length > 0) {
@@ -2780,7 +3916,7 @@ function buildFlatRows(repos, tasks, activity, isCollapsed) {
2780
3916
  }
2781
3917
  return rows;
2782
3918
  }
2783
- function timeAgo2(date) {
3919
+ function timeAgo3(date) {
2784
3920
  const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
2785
3921
  if (seconds < 10) return "just now";
2786
3922
  if (seconds < 60) return `${seconds}s ago`;
@@ -2807,100 +3943,13 @@ function findSelectedIssueWithRepo(repos, selectedId) {
2807
3943
  for (const rd of repos) {
2808
3944
  for (const issue of rd.issues) {
2809
3945
  if (`gh:${rd.repo.name}:${issue.number}` === selectedId)
2810
- return { issue, repoName: rd.repo.name };
2811
- }
2812
- }
2813
- return null;
2814
- }
2815
- function isHeaderId(id) {
2816
- return id != null && (id.startsWith("header:") || id.startsWith("sub:"));
2817
- }
2818
- function RowRenderer({ row, selectedId, selfLogin, isMultiSelected }) {
2819
- switch (row.type) {
2820
- case "sectionHeader": {
2821
- const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
2822
- const isSel = selectedId === row.navId;
2823
- return /* @__PURE__ */ jsxs13(Box13, { children: [
2824
- /* @__PURE__ */ jsxs13(Text13, { color: isSel ? "cyan" : "white", bold: true, children: [
2825
- arrow,
2826
- " ",
2827
- row.label
2828
- ] }),
2829
- /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2830
- " ",
2831
- "(",
2832
- row.count,
2833
- " ",
2834
- row.countLabel,
2835
- ")"
2836
- ] })
2837
- ] });
2838
- }
2839
- case "subHeader": {
2840
- if (row.navId) {
2841
- const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
2842
- const isSel = selectedId === row.navId;
2843
- return /* @__PURE__ */ jsxs13(Box13, { children: [
2844
- /* @__PURE__ */ jsxs13(Text13, { color: isSel ? "cyan" : "gray", children: [
2845
- " ",
2846
- arrow,
2847
- " ",
2848
- row.text
2849
- ] }),
2850
- /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2851
- " (",
2852
- row.count,
2853
- ")"
2854
- ] })
2855
- ] });
2856
- }
2857
- return /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2858
- " ",
2859
- row.text
2860
- ] });
2861
- }
2862
- case "issue": {
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(IssueRow, { issue: row.issue, selfLogin, isSelected: selectedId === row.navId })
2867
- ] });
2868
- }
2869
- case "task": {
2870
- const checkbox2 = isMultiSelected != null ? isMultiSelected ? "\u2611 " : "\u2610 " : "";
2871
- return /* @__PURE__ */ jsxs13(Box13, { children: [
2872
- checkbox2 ? /* @__PURE__ */ jsx13(Text13, { color: isMultiSelected ? "cyan" : "gray", children: checkbox2 }) : null,
2873
- /* @__PURE__ */ jsx13(TaskRow, { task: row.task, isSelected: selectedId === row.navId })
2874
- ] });
2875
- }
2876
- case "activity": {
2877
- const ago = timeAgo2(row.event.timestamp);
2878
- return /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
2879
- " ",
2880
- ago,
2881
- ": ",
2882
- /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2883
- "@",
2884
- row.event.actor
2885
- ] }),
2886
- " ",
2887
- row.event.summary,
2888
- " ",
2889
- /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
2890
- "(",
2891
- row.event.repoShortName,
2892
- ")"
2893
- ] })
2894
- ] });
2895
- }
2896
- case "error":
2897
- return /* @__PURE__ */ jsxs13(Text13, { color: "red", children: [
2898
- " Error: ",
2899
- row.text
2900
- ] });
2901
- case "gap":
2902
- return /* @__PURE__ */ jsx13(Text13, { children: "" });
3946
+ return { issue, repoName: rd.repo.name };
3947
+ }
2903
3948
  }
3949
+ return null;
3950
+ }
3951
+ function isHeaderId2(id) {
3952
+ return id != null && (id.startsWith("header:") || id.startsWith("sub:"));
2904
3953
  }
2905
3954
  function Dashboard({ config: config2, options, activeProfile }) {
2906
3955
  const { exit } = useApp();
@@ -2914,7 +3963,9 @@ function Dashboard({ config: config2, options, activeProfile }) {
2914
3963
  consecutiveFailures,
2915
3964
  autoRefreshPaused,
2916
3965
  refresh,
2917
- mutateData
3966
+ mutateData,
3967
+ pauseAutoRefresh,
3968
+ resumeAutoRefresh
2918
3969
  } = useData(config2, options, refreshMs);
2919
3970
  const allRepos = useMemo2(() => data?.repos ?? [], [data?.repos]);
2920
3971
  const allTasks = useMemo2(
@@ -2923,10 +3974,10 @@ function Dashboard({ config: config2, options, activeProfile }) {
2923
3974
  );
2924
3975
  const allActivity = useMemo2(() => data?.activity ?? [], [data?.activity]);
2925
3976
  const ui = useUIState();
2926
- const [searchQuery, setSearchQuery] = useState9("");
3977
+ const [searchQuery, setSearchQuery] = useState11("");
2927
3978
  const { toasts, toast, handleErrorAction } = useToast();
2928
- const [, setTick] = useState9(0);
2929
- useEffect3(() => {
3979
+ const [, setTick] = useState11(0);
3980
+ useEffect6(() => {
2930
3981
  const id = setInterval(() => setTick((t) => t + 1), 1e4);
2931
3982
  return () => clearInterval(id);
2932
3983
  }, []);
@@ -2945,7 +3996,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
2945
3996
  [repos, tasks, allActivity.length]
2946
3997
  );
2947
3998
  const nav = useNavigation(navItems);
2948
- const getRepoForId = useCallback8((id) => {
3999
+ const getRepoForId = useCallback10((id) => {
2949
4000
  if (id.startsWith("gh:")) {
2950
4001
  const parts = id.split(":");
2951
4002
  return parts.length >= 3 ? `${parts[1]}` : null;
@@ -2954,7 +4005,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
2954
4005
  return null;
2955
4006
  }, []);
2956
4007
  const multiSelect = useMultiSelect(getRepoForId);
2957
- useEffect3(() => {
4008
+ useEffect6(() => {
2958
4009
  if (multiSelect.count === 0) return;
2959
4010
  const validIds = new Set(navItems.map((i) => i.id));
2960
4011
  multiSelect.prune(validIds);
@@ -2968,8 +4019,9 @@ function Dashboard({ config: config2, options, activeProfile }) {
2968
4019
  mutateData,
2969
4020
  onOverlayDone: ui.exitOverlay
2970
4021
  });
2971
- const pendingPickRef = useRef7(null);
2972
- const handleCreateIssueWithPrompt = useCallback8(
4022
+ const pendingPickRef = useRef11(null);
4023
+ const labelCacheRef = useRef11({});
4024
+ const handleCreateIssueWithPrompt = useCallback10(
2973
4025
  (repo, title, labels) => {
2974
4026
  actions.handleCreateIssue(repo, title, labels).then((result) => {
2975
4027
  if (result) {
@@ -2980,7 +4032,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
2980
4032
  },
2981
4033
  [actions, ui]
2982
4034
  );
2983
- const handleConfirmPick = useCallback8(() => {
4035
+ const handleConfirmPick = useCallback10(() => {
2984
4036
  const pending = pendingPickRef.current;
2985
4037
  pendingPickRef.current = null;
2986
4038
  ui.exitOverlay();
@@ -2998,14 +4050,14 @@ function Dashboard({ config: config2, options, activeProfile }) {
2998
4050
  })
2999
4051
  );
3000
4052
  }, [config2, toast, refresh, ui]);
3001
- const handleCancelPick = useCallback8(() => {
4053
+ const handleCancelPick = useCallback10(() => {
3002
4054
  pendingPickRef.current = null;
3003
4055
  ui.exitOverlay();
3004
4056
  }, [ui]);
3005
- const [focusLabel, setFocusLabel] = useState9(null);
3006
- const handleEnterFocus = useCallback8(() => {
4057
+ const [focusLabel, setFocusLabel] = useState11(null);
4058
+ const handleEnterFocus = useCallback10(() => {
3007
4059
  const id = nav.selectedId;
3008
- if (!id || isHeaderId(id)) return;
4060
+ if (!id || isHeaderId2(id)) return;
3009
4061
  let label = "";
3010
4062
  if (id.startsWith("gh:")) {
3011
4063
  const found = findSelectedIssueWithRepo(repos, id);
@@ -3022,11 +4074,11 @@ function Dashboard({ config: config2, options, activeProfile }) {
3022
4074
  setFocusLabel(label);
3023
4075
  ui.enterFocus();
3024
4076
  }, [nav.selectedId, repos, tasks, config2.repos, ui]);
3025
- const handleFocusExit = useCallback8(() => {
4077
+ const handleFocusExit = useCallback10(() => {
3026
4078
  setFocusLabel(null);
3027
4079
  ui.exitToNormal();
3028
4080
  }, [ui]);
3029
- const handleFocusEndAction = useCallback8(
4081
+ const handleFocusEndAction = useCallback10(
3030
4082
  (action) => {
3031
4083
  switch (action) {
3032
4084
  case "restart":
@@ -3052,13 +4104,13 @@ function Dashboard({ config: config2, options, activeProfile }) {
3052
4104
  },
3053
4105
  [toast, ui]
3054
4106
  );
3055
- const [focusKey, setFocusKey] = useState9(0);
4107
+ const [focusKey, setFocusKey] = useState11(0);
3056
4108
  const { stdout } = useStdout();
3057
- const [termSize, setTermSize] = useState9({
4109
+ const [termSize, setTermSize] = useState11({
3058
4110
  cols: stdout?.columns ?? 80,
3059
4111
  rows: stdout?.rows ?? 24
3060
4112
  });
3061
- useEffect3(() => {
4113
+ useEffect6(() => {
3062
4114
  if (!stdout) return;
3063
4115
  const onResize = () => setTermSize({ cols: stdout.columns, rows: stdout.rows });
3064
4116
  stdout.on("resize", onResize);
@@ -3075,7 +4127,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3075
4127
  () => buildFlatRows(repos, tasks, allActivity, nav.isCollapsed),
3076
4128
  [repos, tasks, allActivity, nav.isCollapsed]
3077
4129
  );
3078
- const scrollRef = useRef7(0);
4130
+ const scrollRef = useRef11(0);
3079
4131
  const selectedRowIdx = flatRows.findIndex((r) => r.navId === nav.selectedId);
3080
4132
  if (selectedRowIdx >= 0) {
3081
4133
  if (selectedRowIdx < scrollRef.current) {
@@ -3093,7 +4145,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3093
4145
  const belowCount = flatRows.length - scrollRef.current - viewportHeight;
3094
4146
  const selectedItem = useMemo2(() => {
3095
4147
  const id = nav.selectedId;
3096
- if (!id || isHeaderId(id)) return { issue: null, task: null, repoName: null };
4148
+ if (!id || isHeaderId2(id)) return { issue: null, task: null, repoName: null };
3097
4149
  if (id.startsWith("gh:")) {
3098
4150
  for (const rd of repos) {
3099
4151
  for (const issue of rd.issues) {
@@ -3113,17 +4165,42 @@ function Dashboard({ config: config2, options, activeProfile }) {
3113
4165
  const repoName = multiSelect.count > 0 ? multiSelect.constrainedRepo : selectedItem.repoName;
3114
4166
  if (!repoName || repoName === "ticktick") return [];
3115
4167
  const rd = repos.find((r) => r.repo.name === repoName);
3116
- return rd?.statusOptions.filter((o) => !isTerminalStatus(o.name)) ?? [];
4168
+ return rd?.statusOptions ?? [];
3117
4169
  }, [selectedItem.repoName, repos, multiSelect.count, multiSelect.constrainedRepo]);
3118
- const handleOpen = useCallback8(() => {
4170
+ const handleOpen = useCallback10(() => {
3119
4171
  const url = findSelectedUrl(repos, nav.selectedId);
3120
4172
  if (url) openInBrowser(url);
3121
4173
  }, [repos, nav.selectedId]);
3122
- const handleSlack = useCallback8(() => {
4174
+ const handleSlack = useCallback10(() => {
3123
4175
  const found = findSelectedIssueWithRepo(repos, nav.selectedId);
3124
4176
  if (!found?.issue.slackThreadUrl) return;
3125
4177
  openInBrowser(found.issue.slackThreadUrl);
3126
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]);
3127
4204
  const multiSelectType = useMemo2(() => {
3128
4205
  let hasGh = false;
3129
4206
  let hasTt = false;
@@ -3135,7 +4212,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3135
4212
  if (hasTt) return "ticktick";
3136
4213
  return "github";
3137
4214
  }, [multiSelect.selected]);
3138
- const handleBulkAction = useCallback8(
4215
+ const handleBulkAction = useCallback10(
3139
4216
  (action) => {
3140
4217
  const ids = multiSelect.selected;
3141
4218
  switch (action.type) {
@@ -3179,7 +4256,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3179
4256
  },
3180
4257
  [multiSelect, actions, ui, toast]
3181
4258
  );
3182
- const handleBulkStatusSelect = useCallback8(
4259
+ const handleBulkStatusSelect = useCallback10(
3183
4260
  (optionId) => {
3184
4261
  const ids = multiSelect.selected;
3185
4262
  ui.exitOverlay();
@@ -3195,168 +4272,35 @@ function Dashboard({ config: config2, options, activeProfile }) {
3195
4272
  },
3196
4273
  [multiSelect, actions, ui]
3197
4274
  );
3198
- const handleInput = useCallback8(
3199
- (input2, key) => {
3200
- if (input2 === "?") {
3201
- ui.toggleHelp();
3202
- return;
3203
- }
3204
- if (key.escape && ui.state.mode !== "focus") {
3205
- if (ui.state.mode === "multiSelect") {
3206
- multiSelect.clear();
3207
- }
3208
- ui.exitOverlay();
3209
- return;
3210
- }
3211
- if (ui.canNavigate) {
3212
- if (input2 === "j" || key.downArrow) {
3213
- nav.moveDown();
3214
- return;
3215
- }
3216
- if (input2 === "k" || key.upArrow) {
3217
- nav.moveUp();
3218
- return;
3219
- }
3220
- if (key.tab) {
3221
- if (ui.state.mode === "multiSelect") {
3222
- multiSelect.clear();
3223
- ui.clearMultiSelect();
3224
- }
3225
- key.shift ? nav.prevSection() : nav.nextSection();
3226
- return;
3227
- }
3228
- }
3229
- if (ui.state.mode === "multiSelect") {
3230
- if (input2 === " ") {
3231
- const id = nav.selectedId;
3232
- if (id && !isHeaderId(id)) {
3233
- multiSelect.toggle(id);
3234
- }
3235
- return;
3236
- }
3237
- if (key.return) {
3238
- if (multiSelect.count > 0) {
3239
- ui.enterBulkAction();
3240
- }
3241
- return;
3242
- }
3243
- if (input2 === "m" && multiSelect.count > 0) {
3244
- ui.enterBulkAction();
3245
- return;
3246
- }
3247
- return;
3248
- }
3249
- if (input2 === "d") {
3250
- if (handleErrorAction("dismiss")) return;
3251
- }
3252
- if (input2 === "r" && handleErrorAction("retry")) return;
3253
- if (ui.canAct) {
3254
- if (input2 === "/") {
3255
- multiSelect.clear();
3256
- ui.enterSearch();
3257
- return;
3258
- }
3259
- if (input2 === "q") {
3260
- exit();
3261
- return;
3262
- }
3263
- if (input2 === "r" || input2 === "R") {
3264
- multiSelect.clear();
3265
- refresh();
3266
- return;
3267
- }
3268
- if (input2 === "s") {
3269
- handleSlack();
3270
- return;
3271
- }
3272
- if (input2 === "p") {
3273
- actions.handlePick();
3274
- return;
3275
- }
3276
- if (input2 === "a") {
3277
- actions.handleAssign();
3278
- return;
3279
- }
3280
- if (input2 === "u") {
3281
- actions.handleUnassign();
3282
- return;
3283
- }
3284
- if (input2 === "c") {
3285
- if (selectedItem.issue) {
3286
- multiSelect.clear();
3287
- ui.enterComment();
3288
- }
3289
- return;
3290
- }
3291
- if (input2 === "m") {
3292
- if (selectedItem.issue && selectedRepoStatusOptions.length > 0) {
3293
- multiSelect.clear();
3294
- ui.enterStatus();
3295
- } else if (selectedItem.issue) {
3296
- toast.info("Issue not in a project board");
3297
- }
3298
- return;
3299
- }
3300
- if (input2 === "n") {
3301
- multiSelect.clear();
3302
- ui.enterCreate();
3303
- return;
3304
- }
3305
- if (input2 === "f") {
3306
- handleEnterFocus();
3307
- return;
3308
- }
3309
- if (input2 === " ") {
3310
- const id = nav.selectedId;
3311
- if (id && !isHeaderId(id)) {
3312
- multiSelect.toggle(id);
3313
- ui.enterMultiSelect();
3314
- } else if (isHeaderId(nav.selectedId)) {
3315
- nav.toggleSection();
3316
- }
3317
- return;
3318
- }
3319
- if (key.return) {
3320
- if (isHeaderId(nav.selectedId)) {
3321
- nav.toggleSection();
3322
- return;
3323
- }
3324
- handleOpen();
3325
- return;
3326
- }
3327
- }
3328
- },
3329
- [
3330
- ui,
3331
- 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: {
3332
4286
  exit,
3333
4287
  refresh,
3334
4288
  handleSlack,
4289
+ handleCopyLink,
3335
4290
  handleOpen,
3336
- actions,
3337
- selectedItem.issue,
3338
- selectedRepoStatusOptions.length,
3339
- toast,
3340
- nav.selectedId,
3341
- multiSelect,
3342
4291
  handleEnterFocus,
3343
- handleErrorAction
3344
- ]
3345
- );
3346
- const inputActive = ui.state.mode === "normal" || ui.state.mode === "multiSelect" || ui.state.mode === "focus";
3347
- useInput8(handleInput, { isActive: inputActive });
3348
- const handleSearchEscape = useCallback8(
3349
- (_input, key) => {
3350
- if (key.escape) {
3351
- ui.exitOverlay();
3352
- setSearchQuery("");
3353
- }
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
3354
4299
  },
3355
- [ui]
3356
- );
3357
- useInput8(handleSearchEscape, { isActive: ui.state.mode === "search" });
4300
+ onSearchEscape
4301
+ });
3358
4302
  if (status === "loading" && !data) {
3359
- 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..." }) });
3360
4304
  }
3361
4305
  const now = data?.fetchedAt ?? /* @__PURE__ */ new Date();
3362
4306
  const dateStr = now.toLocaleDateString("en-US", {
@@ -3364,93 +4308,81 @@ function Dashboard({ config: config2, options, activeProfile }) {
3364
4308
  day: "numeric",
3365
4309
  year: "numeric"
3366
4310
  });
3367
- return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
3368
- /* @__PURE__ */ jsxs13(Box13, { children: [
3369
- /* @__PURE__ */ jsx13(Text13, { color: "cyan", bold: true, children: "HOG BOARD" }),
3370
- 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: [
3371
4315
  " [",
3372
4316
  activeProfile,
3373
4317
  "]"
3374
4318
  ] }) : null,
3375
- /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
4319
+ /* @__PURE__ */ jsxs17(Text16, { color: "gray", children: [
3376
4320
  " ",
3377
4321
  "\u2014",
3378
4322
  " ",
3379
4323
  dateStr
3380
4324
  ] }),
3381
- /* @__PURE__ */ jsx13(Text13, { children: " " }),
3382
- isRefreshing ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
3383
- /* @__PURE__ */ jsx13(Spinner2, { label: "" }),
3384
- /* @__PURE__ */ jsx13(Text13, { color: "cyan", children: " Refreshing..." })
3385
- ] }) : lastRefresh ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
3386
- /* @__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: [
3387
4331
  "Updated ",
3388
- timeAgo2(lastRefresh)
4332
+ timeAgo3(lastRefresh)
3389
4333
  ] }),
3390
- consecutiveFailures > 0 ? /* @__PURE__ */ jsx13(Text13, { color: "red", children: " (!)" }) : null
4334
+ consecutiveFailures > 0 ? /* @__PURE__ */ jsx17(Text16, { color: "red", children: " (!)" }) : null
3391
4335
  ] }) : null,
3392
- 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
3393
4337
  ] }),
3394
- error ? /* @__PURE__ */ jsxs13(Text13, { color: "red", children: [
4338
+ error ? /* @__PURE__ */ jsxs17(Text16, { color: "red", children: [
3395
4339
  "Error: ",
3396
4340
  error
3397
4341
  ] }) : null,
3398
- ui.state.helpVisible ? /* @__PURE__ */ jsx13(HelpOverlay, { currentMode: ui.state.mode, onClose: ui.toggleHelp }) : null,
3399
- ui.state.mode === "overlay:status" && selectedRepoStatusOptions.length > 0 ? /* @__PURE__ */ jsx13(
3400
- StatusPicker,
4342
+ /* @__PURE__ */ jsx17(
4343
+ OverlayRenderer,
3401
4344
  {
3402
- options: selectedRepoStatusOptions,
4345
+ uiState: ui.state,
4346
+ config: config2,
4347
+ selectedRepoStatusOptions,
3403
4348
  currentStatus: multiSelect.count > 0 ? void 0 : selectedItem.issue?.projectStatus,
3404
- onSelect: multiSelect.count > 0 ? handleBulkStatusSelect : actions.handleStatusChange,
3405
- onCancel: ui.exitOverlay
3406
- }
3407
- ) : null,
3408
- ui.state.mode === "overlay:create" ? /* @__PURE__ */ jsx13(
3409
- CreateIssueForm,
3410
- {
3411
- repos: config2.repos,
4349
+ onStatusSelect: multiSelect.count > 0 ? handleBulkStatusSelect : actions.handleStatusChange,
4350
+ onExitOverlay: ui.exitOverlay,
3412
4351
  defaultRepo: selectedItem.repoName,
3413
- onSubmit: handleCreateIssueWithPrompt,
3414
- onCancel: ui.exitOverlay
3415
- }
3416
- ) : null,
3417
- ui.state.mode === "overlay:confirmPick" ? /* @__PURE__ */ jsx13(
3418
- ConfirmPrompt,
3419
- {
3420
- message: "Pick this issue?",
3421
- onConfirm: handleConfirmPick,
3422
- onCancel: handleCancelPick
3423
- }
3424
- ) : null,
3425
- ui.state.mode === "overlay:bulkAction" ? /* @__PURE__ */ jsx13(
3426
- BulkActionMenu,
3427
- {
3428
- count: multiSelect.count,
3429
- selectionType: multiSelectType,
3430
- onSelect: handleBulkAction,
3431
- 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)
3432
4374
  }
3433
- ) : null,
3434
- ui.state.mode === "focus" && focusLabel ? /* @__PURE__ */ jsx13(
3435
- FocusMode,
3436
- {
3437
- label: focusLabel,
3438
- durationSec: config2.board.focusDuration ?? 1500,
3439
- onExit: handleFocusExit,
3440
- onEndAction: handleFocusEndAction
3441
- },
3442
- focusKey
3443
- ) : null,
3444
- !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: [
3445
- /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", flexGrow: 1, children: [
3446
- 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: [
3447
4379
  " ",
3448
4380
  "\u25B2",
3449
4381
  " ",
3450
4382
  aboveCount,
3451
4383
  " more above"
3452
4384
  ] }) : null,
3453
- visibleRows.map((row) => /* @__PURE__ */ jsx13(
4385
+ visibleRows.map((row) => /* @__PURE__ */ jsx17(
3454
4386
  RowRenderer,
3455
4387
  {
3456
4388
  row,
@@ -3460,7 +4392,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3460
4392
  },
3461
4393
  row.key
3462
4394
  )),
3463
- hasMoreBelow ? /* @__PURE__ */ jsxs13(Text13, { color: "gray", dimColor: true, children: [
4395
+ hasMoreBelow ? /* @__PURE__ */ jsxs17(Text16, { color: "gray", dimColor: true, children: [
3464
4396
  " ",
3465
4397
  "\u25BC",
3466
4398
  " ",
@@ -3468,7 +4400,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3468
4400
  " more below"
3469
4401
  ] }) : null
3470
4402
  ] }),
3471
- showDetailPanel ? /* @__PURE__ */ jsx13(Box13, { marginLeft: 1, width: detailPanelWidth, children: /* @__PURE__ */ jsx13(
4403
+ showDetailPanel ? /* @__PURE__ */ jsx17(Box16, { marginLeft: 1, width: detailPanelWidth, children: /* @__PURE__ */ jsx17(
3472
4404
  DetailPanel,
3473
4405
  {
3474
4406
  issue: selectedItem.issue,
@@ -3477,25 +4409,16 @@ function Dashboard({ config: config2, options, activeProfile }) {
3477
4409
  }
3478
4410
  ) }) : null
3479
4411
  ] }) : null,
3480
- ui.state.mode === "search" ? /* @__PURE__ */ jsx13(SearchBar, { defaultValue: searchQuery, onChange: setSearchQuery, onSubmit: ui.exitOverlay }) : null,
3481
- ui.state.mode === "overlay:comment" && selectedItem.issue ? /* @__PURE__ */ jsx13(
3482
- CommentInput,
3483
- {
3484
- issueNumber: selectedItem.issue.number,
3485
- onSubmit: actions.handleComment,
3486
- onCancel: ui.exitOverlay
3487
- }
3488
- ) : null,
3489
- /* @__PURE__ */ jsx13(ToastContainer, { toasts }),
3490
- /* @__PURE__ */ jsx13(Box13, { children: ui.state.mode === "multiSelect" ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
3491
- /* @__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: [
3492
4415
  multiSelect.count,
3493
4416
  " selected"
3494
4417
  ] }),
3495
- /* @__PURE__ */ jsx13(Text13, { color: "gray", children: " Space:toggle Enter:actions Esc:cancel" })
3496
- ] }) : ui.state.mode === "focus" ? /* @__PURE__ */ jsx13(Text13, { color: "magenta", bold: true, children: "Focus mode \u2014 Esc to exit" }) : /* @__PURE__ */ jsxs13(Fragment4, { children: [
3497
- /* @__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" }),
3498
- 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: [
3499
4422
  ' filter: "',
3500
4423
  searchQuery,
3501
4424
  '"'
@@ -3503,29 +4426,23 @@ function Dashboard({ config: config2, options, activeProfile }) {
3503
4426
  ] }) })
3504
4427
  ] });
3505
4428
  }
3506
- var TERMINAL_STATUS_RE2, PRIORITY_RANK, CHROME_ROWS;
4429
+ var TERMINAL_STATUS_RE3, PRIORITY_RANK, CHROME_ROWS;
3507
4430
  var init_dashboard = __esm({
3508
4431
  "src/board/components/dashboard.tsx"() {
3509
4432
  "use strict";
4433
+ init_clipboard();
3510
4434
  init_use_actions();
3511
4435
  init_use_data();
4436
+ init_use_keyboard();
3512
4437
  init_use_multi_select();
3513
4438
  init_use_navigation();
3514
4439
  init_use_toast();
3515
4440
  init_use_ui_state();
3516
- init_bulk_action_menu();
3517
- init_comment_input();
3518
- init_confirm_prompt();
3519
- init_create_issue_form();
3520
4441
  init_detail_panel();
3521
- init_focus_mode();
3522
- init_help_overlay();
3523
- init_issue_row();
3524
- init_search_bar();
3525
- init_status_picker();
3526
- init_task_row();
4442
+ init_overlay_renderer();
4443
+ init_row_renderer();
3527
4444
  init_toast_container();
3528
- 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;
3529
4446
  PRIORITY_RANK = {
3530
4447
  "priority:critical": 0,
3531
4448
  "priority:high": 1,
@@ -3542,17 +4459,19 @@ __export(live_exports, {
3542
4459
  runLiveDashboard: () => runLiveDashboard
3543
4460
  });
3544
4461
  import { render } from "ink";
3545
- import { jsx as jsx14 } from "react/jsx-runtime";
4462
+ import { jsx as jsx18 } from "react/jsx-runtime";
3546
4463
  async function runLiveDashboard(config2, options, activeProfile) {
3547
- const { waitUntilExit } = render(
3548
- /* @__PURE__ */ jsx14(Dashboard, { config: config2, options, activeProfile: activeProfile ?? null })
4464
+ const instance = render(
4465
+ /* @__PURE__ */ jsx18(Dashboard, { config: config2, options, activeProfile: activeProfile ?? null })
3549
4466
  );
3550
- await waitUntilExit();
4467
+ setInkInstance(instance);
4468
+ await instance.waitUntilExit();
3551
4469
  }
3552
4470
  var init_live = __esm({
3553
4471
  "src/board/live.tsx"() {
3554
4472
  "use strict";
3555
4473
  init_dashboard();
4474
+ init_ink_instance();
3556
4475
  }
3557
4476
  });
3558
4477
 
@@ -3918,83 +4837,17 @@ var init_format_static = __esm({
3918
4837
  });
3919
4838
 
3920
4839
  // src/cli.ts
4840
+ init_ai();
3921
4841
  init_api();
3922
4842
  init_config();
4843
+ import { execFileSync as execFileSync5 } from "child_process";
3923
4844
  import { Command } from "commander";
3924
4845
 
3925
4846
  // src/init.ts
4847
+ init_config();
3926
4848
  import { execFileSync } from "child_process";
3927
4849
  import { existsSync as existsSync2 } from "fs";
3928
4850
  import { checkbox, confirm, input, select } from "@inquirer/prompts";
3929
-
3930
- // src/auth.ts
3931
- import { createServer } from "http";
3932
- var AUTH_URL = "https://ticktick.com/oauth/authorize";
3933
- var TOKEN_URL = "https://ticktick.com/oauth/token";
3934
- var REDIRECT_URI = "http://localhost:8080";
3935
- var SCOPE = "tasks:write tasks:read";
3936
- function getAuthorizationUrl(clientId) {
3937
- const params = new URLSearchParams({
3938
- scope: SCOPE,
3939
- client_id: clientId,
3940
- state: "hog",
3941
- redirect_uri: REDIRECT_URI,
3942
- response_type: "code"
3943
- });
3944
- return `${AUTH_URL}?${params}`;
3945
- }
3946
- async function waitForAuthCode() {
3947
- return new Promise((resolve, reject) => {
3948
- const server = createServer((req, res) => {
3949
- const url = new URL(req.url ?? "", REDIRECT_URI);
3950
- const code = url.searchParams.get("code");
3951
- if (code) {
3952
- res.writeHead(200, { "Content-Type": "text/html" });
3953
- res.end(
3954
- "<html><body><h1>Heart of Gold authenticated!</h1><p>You can close this window.</p></body></html>"
3955
- );
3956
- server.close();
3957
- resolve(code);
3958
- } else {
3959
- res.writeHead(400, { "Content-Type": "text/plain" });
3960
- res.end("Missing authorization code");
3961
- server.close();
3962
- reject(new Error("No authorization code received"));
3963
- }
3964
- });
3965
- server.listen(8080, () => {
3966
- });
3967
- server.on("error", reject);
3968
- setTimeout(() => {
3969
- server.close();
3970
- reject(new Error("Authorization timed out (2 min)"));
3971
- }, 12e4);
3972
- });
3973
- }
3974
- async function exchangeCodeForToken(clientId, clientSecret, code) {
3975
- const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
3976
- const res = await fetch(TOKEN_URL, {
3977
- method: "POST",
3978
- headers: {
3979
- Authorization: `Basic ${credentials}`,
3980
- "Content-Type": "application/x-www-form-urlencoded"
3981
- },
3982
- body: new URLSearchParams({
3983
- grant_type: "authorization_code",
3984
- code,
3985
- redirect_uri: REDIRECT_URI
3986
- })
3987
- });
3988
- if (!res.ok) {
3989
- const text = await res.text();
3990
- throw new Error(`Token exchange failed: ${text}`);
3991
- }
3992
- const data = await res.json();
3993
- return data.access_token;
3994
- }
3995
-
3996
- // src/init.ts
3997
- init_config();
3998
4851
  function ghJson(args) {
3999
4852
  const output = execFileSync("gh", args, { encoding: "utf-8", timeout: 3e4 }).trim();
4000
4853
  return JSON.parse(output);
@@ -4078,12 +4931,13 @@ function listProjectFields(owner, projectNumber) {
4078
4931
  return [];
4079
4932
  }
4080
4933
  }
4081
- function detectStatusFieldId(owner, projectNumber) {
4934
+ function detectStatusField(owner, projectNumber) {
4082
4935
  const fields = listProjectFields(owner, projectNumber);
4083
4936
  const statusField = fields.find(
4084
4937
  (f) => f.name === "Status" && f.type === "ProjectV2SingleSelectField"
4085
4938
  );
4086
- return statusField?.id ?? null;
4939
+ if (!statusField) return null;
4940
+ return { fieldId: statusField.id, options: statusField.options ?? [] };
4087
4941
  }
4088
4942
  async function runInit(opts = {}) {
4089
4943
  try {
@@ -4157,8 +5011,10 @@ Configuring ${repoName}...`);
4157
5011
  });
4158
5012
  }
4159
5013
  console.log(" Detecting status field...");
4160
- let statusFieldId = detectStatusFieldId(owner, projectNumber);
4161
- if (statusFieldId) {
5014
+ const statusInfo = detectStatusField(owner, projectNumber);
5015
+ let statusFieldId;
5016
+ if (statusInfo) {
5017
+ statusFieldId = statusInfo.fieldId;
4162
5018
  console.log(` Found status field: ${statusFieldId}`);
4163
5019
  } else {
4164
5020
  console.log(" Could not auto-detect status field.");
@@ -4167,7 +5023,7 @@ Configuring ${repoName}...`);
4167
5023
  });
4168
5024
  }
4169
5025
  const completionType = await select({
4170
- message: ` When a task is completed in TickTick, what should happen on GitHub?`,
5026
+ message: ` When a task is completed, what should happen on GitHub?`,
4171
5027
  choices: [
4172
5028
  { name: "Close the issue", value: "closeIssue" },
4173
5029
  { name: "Add a label (e.g. review:pending)", value: "addLabel" },
@@ -4182,9 +5038,21 @@ Configuring ${repoName}...`);
4182
5038
  });
4183
5039
  completionAction = { type: "addLabel", label };
4184
5040
  } else if (completionType === "updateProjectStatus") {
4185
- const optionId = await input({
4186
- message: " Status option ID to set:"
4187
- });
5041
+ const statusOptions = statusInfo?.options ?? [];
5042
+ let optionId;
5043
+ if (statusOptions.length > 0) {
5044
+ optionId = await select({
5045
+ message: " Status to set when completed:",
5046
+ choices: statusOptions.map((o) => ({
5047
+ name: o.name,
5048
+ value: o.id
5049
+ }))
5050
+ });
5051
+ } else {
5052
+ optionId = await input({
5053
+ message: " Status option ID to set:"
5054
+ });
5055
+ }
4188
5056
  completionAction = { type: "updateProjectStatus", optionId };
4189
5057
  } else {
4190
5058
  completionAction = { type: "closeIssue" };
@@ -4201,43 +5069,11 @@ Configuring ${repoName}...`);
4201
5069
  completionAction
4202
5070
  });
4203
5071
  }
4204
- const enableTickTick = await confirm({
4205
- message: "Enable TickTick integration?",
4206
- default: false
4207
- });
5072
+ const ticktickAlreadyEnabled = existsSync2(`${CONFIG_DIR}/auth.json`);
4208
5073
  let ticktickAuth = false;
4209
- if (enableTickTick) {
4210
- const hasAuth = existsSync2(`${CONFIG_DIR}/auth.json`);
4211
- if (hasAuth) {
4212
- console.log(" TickTick auth already configured.");
4213
- ticktickAuth = true;
4214
- } else {
4215
- const setupNow = await confirm({
4216
- message: " Set up TickTick OAuth now?",
4217
- default: true
4218
- });
4219
- if (setupNow) {
4220
- const clientId = await input({ message: " TickTick OAuth client ID:" });
4221
- const clientSecret = await input({ message: " TickTick OAuth client secret:" });
4222
- const url = getAuthorizationUrl(clientId);
4223
- console.log(`
4224
- Open this URL to authorize:
4225
-
4226
- ${url}
4227
- `);
4228
- try {
4229
- const { exec } = await import("child_process");
4230
- exec(`open "${url}"`);
4231
- } catch {
4232
- }
4233
- console.log(" Waiting for authorization...");
4234
- const code = await waitForAuthCode();
4235
- const accessToken = await exchangeCodeForToken(clientId, clientSecret, code);
4236
- saveAuth({ accessToken, clientId, clientSecret });
4237
- console.log(" TickTick authenticated successfully.");
4238
- ticktickAuth = true;
4239
- }
4240
- }
5074
+ if (ticktickAlreadyEnabled) {
5075
+ ticktickAuth = true;
5076
+ console.log("TickTick auth found \u2014 integration enabled.");
4241
5077
  }
4242
5078
  console.log("\nBoard settings:");
4243
5079
  const refreshInterval = await input({
@@ -4264,7 +5100,7 @@ Configuring ${repoName}...`);
4264
5100
  assignee: login,
4265
5101
  focusDuration: Number.parseInt(focusDuration, 10) || 1500
4266
5102
  },
4267
- ticktick: { enabled: enableTickTick && ticktickAuth },
5103
+ ticktick: { enabled: ticktickAuth },
4268
5104
  profiles: existingConfig?.profiles ?? {}
4269
5105
  };
4270
5106
  saveFullConfig(config2);
@@ -4674,7 +5510,7 @@ function resolveProjectId(projectId) {
4674
5510
  process.exit(1);
4675
5511
  }
4676
5512
  var program = new Command();
4677
- program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.1.3").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) => {
4678
5514
  const opts = thisCommand.opts();
4679
5515
  if (opts.json) setFormat("json");
4680
5516
  if (opts.human) setFormat("human");
@@ -5023,6 +5859,53 @@ config.command("profile:default [name]").description("Set or show the default bo
5023
5859
  printSuccess(`Default profile set to "${name}".`);
5024
5860
  }
5025
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);
5026
5909
  program.parseAsync().catch((err) => {
5027
5910
  const message = err instanceof Error ? err.message : String(err);
5028
5911
  console.error(`Error: ${message}`);