@ondrej-svec/hog 1.2.0 → 1.4.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,70 +9,6 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/api.ts
13
- var BASE_URL, TickTickClient;
14
- var init_api = __esm({
15
- "src/api.ts"() {
16
- "use strict";
17
- BASE_URL = "https://api.ticktick.com/open/v1";
18
- TickTickClient = class {
19
- token;
20
- constructor(token) {
21
- this.token = token;
22
- }
23
- async request(method, path, body) {
24
- const url = `${BASE_URL}${path}`;
25
- const init = {
26
- method,
27
- headers: {
28
- Authorization: `Bearer ${this.token}`,
29
- "Content-Type": "application/json"
30
- }
31
- };
32
- if (body !== void 0) {
33
- init.body = JSON.stringify(body);
34
- }
35
- const res = await fetch(url, init);
36
- if (!res.ok) {
37
- const text2 = await res.text();
38
- throw new Error(`TickTick API error ${res.status}: ${text2}`);
39
- }
40
- const text = await res.text();
41
- if (!text) return void 0;
42
- return JSON.parse(text);
43
- }
44
- async listProjects() {
45
- return this.request("GET", "/project");
46
- }
47
- async getProject(projectId) {
48
- return this.request("GET", `/project/${projectId}`);
49
- }
50
- async getProjectData(projectId) {
51
- return this.request("GET", `/project/${projectId}/data`);
52
- }
53
- async listTasks(projectId) {
54
- const data = await this.getProjectData(projectId);
55
- return data.tasks ?? [];
56
- }
57
- async getTask(projectId, taskId) {
58
- return this.request("GET", `/project/${projectId}/task/${taskId}`);
59
- }
60
- async createTask(input2) {
61
- return this.request("POST", "/task", input2);
62
- }
63
- async updateTask(input2) {
64
- return this.request("POST", `/task/${input2.id}`, input2);
65
- }
66
- async completeTask(projectId, taskId) {
67
- await this.request("POST", `/project/${projectId}/task/${taskId}/complete`);
68
- }
69
- async deleteTask(projectId, taskId) {
70
- await this.request("DELETE", `/project/${projectId}/task/${taskId}`);
71
- }
72
- };
73
- }
74
- });
75
-
76
12
  // src/config.ts
77
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
78
14
  import { homedir } from "os";
@@ -164,6 +100,29 @@ function getAuth() {
164
100
  return null;
165
101
  }
166
102
  }
103
+ function saveAuth(data) {
104
+ ensureDir();
105
+ writeFileSync(AUTH_FILE, `${JSON.stringify(data, null, 2)}
106
+ `, {
107
+ mode: 384
108
+ });
109
+ }
110
+ function getLlmAuth() {
111
+ const auth = getAuth();
112
+ if (auth?.openrouterApiKey) return { provider: "openrouter", apiKey: auth.openrouterApiKey };
113
+ return null;
114
+ }
115
+ function saveLlmAuth(openrouterApiKey) {
116
+ const existing = getAuth();
117
+ const updated = existing ? { ...existing, openrouterApiKey } : { accessToken: "", clientId: "", clientSecret: "", openrouterApiKey };
118
+ saveAuth(updated);
119
+ }
120
+ function clearLlmAuth() {
121
+ const existing = getAuth();
122
+ if (!existing) return;
123
+ const { openrouterApiKey: _, ...rest } = existing;
124
+ saveAuth(rest);
125
+ }
167
126
  function getConfig() {
168
127
  if (!existsSync(CONFIG_FILE)) return {};
169
128
  try {
@@ -235,6 +194,231 @@ var init_config = __esm({
235
194
  }
236
195
  });
237
196
 
197
+ // src/ai.ts
198
+ async function parseHeuristic(input2, today = /* @__PURE__ */ new Date()) {
199
+ let remaining = input2;
200
+ const labelMatches = [...remaining.matchAll(/#([\w:/-]+)/g)];
201
+ const rawLabels = labelMatches.map((m) => (m[1] ?? "").toLowerCase());
202
+ remaining = remaining.replace(/#[\w:/-]+/g, "").trim();
203
+ const assigneeMatches = [...remaining.matchAll(/@([\w-]+)/g)];
204
+ const assignee = assigneeMatches.length > 0 ? assigneeMatches[assigneeMatches.length - 1]?.[1] ?? null : null;
205
+ remaining = remaining.replace(/@[\w-]+/g, "").trim();
206
+ let dueDate = null;
207
+ const dueMatch = remaining.match(/\bdue\s+(.+?)(?:\s+#|\s+@|$)/i);
208
+ if (dueMatch?.[1]) {
209
+ const { parse } = await import("chrono-node");
210
+ const results = parse(dueMatch[1], { instant: today }, { forwardDate: true });
211
+ const first = results[0];
212
+ if (first) {
213
+ let date = first.date();
214
+ if (date < today) {
215
+ date = new Date(date);
216
+ date.setFullYear(date.getFullYear() + 1);
217
+ }
218
+ const yyyy = date.getFullYear();
219
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
220
+ const dd = String(date.getDate()).padStart(2, "0");
221
+ dueDate = `${yyyy}-${mm}-${dd}`;
222
+ }
223
+ remaining = remaining.slice(0, dueMatch.index ?? 0).trim();
224
+ }
225
+ const title = remaining.replace(/\s+/g, " ").trim();
226
+ if (!title) return null;
227
+ return { title, labels: rawLabels, assignee, dueDate };
228
+ }
229
+ function detectProvider() {
230
+ const orKey = process.env["OPENROUTER_API_KEY"];
231
+ if (orKey) return { provider: "openrouter", apiKey: orKey };
232
+ const antKey = process.env["ANTHROPIC_API_KEY"];
233
+ if (antKey) return { provider: "anthropic", apiKey: antKey };
234
+ return getLlmAuth();
235
+ }
236
+ async function callLLM(userText, validLabels, today, providerConfig) {
237
+ const { provider, apiKey } = providerConfig;
238
+ const todayStr = today.toISOString().slice(0, 10);
239
+ 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).`;
240
+ const escapedText = userText.replace(/<\/input>/gi, "< /input>");
241
+ const userContent = `<input>${escapedText}</input>
242
+ <valid_labels>${validLabels.join(",")}</valid_labels>`;
243
+ const jsonSchema = {
244
+ name: "issue",
245
+ schema: {
246
+ type: "object",
247
+ properties: {
248
+ title: { type: "string" },
249
+ labels: { type: "array", items: { type: "string" } },
250
+ due_date: { type: ["string", "null"] },
251
+ assignee: { type: ["string", "null"] }
252
+ },
253
+ required: ["title", "labels", "due_date", "assignee"],
254
+ additionalProperties: false
255
+ }
256
+ };
257
+ try {
258
+ let response;
259
+ if (provider === "openrouter") {
260
+ response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
261
+ method: "POST",
262
+ headers: {
263
+ "Content-Type": "application/json",
264
+ Authorization: `Bearer ${apiKey}`
265
+ },
266
+ body: JSON.stringify({
267
+ model: "google/gemini-2.5-flash",
268
+ messages: [
269
+ { role: "system", content: systemPrompt },
270
+ { role: "user", content: userContent }
271
+ ],
272
+ response_format: { type: "json_schema", json_schema: jsonSchema },
273
+ max_tokens: 256,
274
+ temperature: 0
275
+ }),
276
+ signal: AbortSignal.timeout(5e3)
277
+ });
278
+ } else {
279
+ response = await fetch("https://api.anthropic.com/v1/messages", {
280
+ method: "POST",
281
+ headers: {
282
+ "Content-Type": "application/json",
283
+ "x-api-key": apiKey,
284
+ "anthropic-version": "2023-06-01"
285
+ },
286
+ body: JSON.stringify({
287
+ model: "claude-haiku-4-5-20251001",
288
+ system: systemPrompt,
289
+ messages: [{ role: "user", content: userContent }],
290
+ max_tokens: 256
291
+ }),
292
+ signal: AbortSignal.timeout(5e3)
293
+ });
294
+ }
295
+ if (!response.ok) return null;
296
+ const json = await response.json();
297
+ let raw;
298
+ if (provider === "openrouter") {
299
+ const choicesRaw = json["choices"];
300
+ if (!Array.isArray(choicesRaw)) return null;
301
+ const firstChoice = choicesRaw[0];
302
+ const content = firstChoice?.message?.content;
303
+ if (!content) return null;
304
+ raw = JSON.parse(content);
305
+ } else {
306
+ const contentRaw = json["content"];
307
+ if (!Array.isArray(contentRaw)) return null;
308
+ const firstItem = contentRaw[0];
309
+ const text = firstItem?.text;
310
+ if (!text) return null;
311
+ raw = JSON.parse(text);
312
+ }
313
+ if (!raw || typeof raw !== "object") return null;
314
+ const r = raw;
315
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
316
+ return {
317
+ title: typeof r["title"] === "string" ? r["title"] : "",
318
+ labels: Array.isArray(r["labels"]) ? r["labels"].filter((l) => typeof l === "string") : [],
319
+ due_date: typeof r["due_date"] === "string" && ISO_DATE_RE.test(r["due_date"]) ? r["due_date"] : null,
320
+ assignee: typeof r["assignee"] === "string" ? r["assignee"] : null
321
+ };
322
+ } catch {
323
+ return null;
324
+ }
325
+ }
326
+ async function extractIssueFields(input2, options = {}) {
327
+ const today = options.today ?? /* @__PURE__ */ new Date();
328
+ const heuristic = await parseHeuristic(input2, today);
329
+ if (!heuristic) return null;
330
+ const providerConfig = detectProvider();
331
+ if (!providerConfig) return heuristic;
332
+ const llmResult = await callLLM(input2, options.validLabels ?? [], today, providerConfig);
333
+ if (!llmResult) {
334
+ options.onLlmFallback?.("AI parsing unavailable, used keyword matching");
335
+ return heuristic;
336
+ }
337
+ const merged = {
338
+ ...llmResult,
339
+ // Heuristic explicit tokens always win
340
+ labels: heuristic.labels.length > 0 ? heuristic.labels : llmResult.labels,
341
+ assignee: heuristic.assignee ?? llmResult.assignee,
342
+ dueDate: heuristic.dueDate ?? llmResult.due_date,
343
+ // LLM title is used only if heuristic left explicit tokens
344
+ title: heuristic.labels.length > 0 || heuristic.assignee || heuristic.dueDate ? llmResult.title || heuristic.title : heuristic.title
345
+ };
346
+ return merged;
347
+ }
348
+ function hasLlmApiKey() {
349
+ return detectProvider() !== null;
350
+ }
351
+ var init_ai = __esm({
352
+ "src/ai.ts"() {
353
+ "use strict";
354
+ init_config();
355
+ }
356
+ });
357
+
358
+ // src/api.ts
359
+ var BASE_URL, TickTickClient;
360
+ var init_api = __esm({
361
+ "src/api.ts"() {
362
+ "use strict";
363
+ BASE_URL = "https://api.ticktick.com/open/v1";
364
+ TickTickClient = class {
365
+ token;
366
+ constructor(token) {
367
+ this.token = token;
368
+ }
369
+ async request(method, path, body) {
370
+ const url = `${BASE_URL}${path}`;
371
+ const init = {
372
+ method,
373
+ headers: {
374
+ Authorization: `Bearer ${this.token}`,
375
+ "Content-Type": "application/json"
376
+ }
377
+ };
378
+ if (body !== void 0) {
379
+ init.body = JSON.stringify(body);
380
+ }
381
+ const res = await fetch(url, init);
382
+ if (!res.ok) {
383
+ const text2 = await res.text();
384
+ throw new Error(`TickTick API error ${res.status}: ${text2}`);
385
+ }
386
+ const text = await res.text();
387
+ if (!text) return void 0;
388
+ return JSON.parse(text);
389
+ }
390
+ async listProjects() {
391
+ return this.request("GET", "/project");
392
+ }
393
+ async getProject(projectId) {
394
+ return this.request("GET", `/project/${projectId}`);
395
+ }
396
+ async getProjectData(projectId) {
397
+ return this.request("GET", `/project/${projectId}/data`);
398
+ }
399
+ async listTasks(projectId) {
400
+ const data = await this.getProjectData(projectId);
401
+ return data.tasks ?? [];
402
+ }
403
+ async getTask(projectId, taskId) {
404
+ return this.request("GET", `/project/${projectId}/task/${taskId}`);
405
+ }
406
+ async createTask(input2) {
407
+ return this.request("POST", "/task", input2);
408
+ }
409
+ async updateTask(input2) {
410
+ return this.request("POST", `/task/${input2.id}`, input2);
411
+ }
412
+ async completeTask(projectId, taskId) {
413
+ await this.request("POST", `/project/${projectId}/task/${taskId}/complete`);
414
+ }
415
+ async deleteTask(projectId, taskId) {
416
+ await this.request("DELETE", `/project/${projectId}/task/${taskId}`);
417
+ }
418
+ };
419
+ }
420
+ });
421
+
238
422
  // src/types.ts
239
423
  var init_types = __esm({
240
424
  "src/types.ts"() {
@@ -461,6 +645,21 @@ function fetchProjectStatusOptions(repo, projectNumber, _statusFieldId) {
461
645
  function addLabel(repo, issueNumber, label) {
462
646
  runGh(["issue", "edit", String(issueNumber), "--repo", repo, "--add-label", label]);
463
647
  }
648
+ async function fetchRepoLabelsAsync(repo) {
649
+ try {
650
+ const result = await runGhJsonAsync([
651
+ "label",
652
+ "list",
653
+ "--repo",
654
+ repo,
655
+ "--json",
656
+ "name,color"
657
+ ]);
658
+ return Array.isArray(result) ? result : [];
659
+ } catch {
660
+ return [];
661
+ }
662
+ }
464
663
  function updateProjectItemStatus(repo, issueNumber, projectConfig) {
465
664
  const [owner, repoName] = repo.split("/");
466
665
  const findItemQuery = `
@@ -678,6 +877,21 @@ var init_sync_state = __esm({
678
877
  }
679
878
  });
680
879
 
880
+ // src/clipboard.ts
881
+ function getClipboardArgs() {
882
+ if (process.platform === "darwin") return ["pbcopy"];
883
+ if (process.platform === "win32") return ["clip"];
884
+ if (process.env["WSL_DISTRO_NAME"] ?? process.env["WSL_INTEROP"]) return ["clip.exe"];
885
+ if (process.env["WAYLAND_DISPLAY"]) return ["wl-copy"];
886
+ if (process.env["DISPLAY"]) return ["xsel", "--clipboard", "--input"];
887
+ return null;
888
+ }
889
+ var init_clipboard = __esm({
890
+ "src/clipboard.ts"() {
891
+ "use strict";
892
+ }
893
+ });
894
+
681
895
  // src/pick.ts
682
896
  var pick_exports = {};
683
897
  __export(pick_exports, {
@@ -1034,6 +1248,26 @@ function useActions({
1034
1248
  },
1035
1249
  [toast, refresh, onOverlayDone]
1036
1250
  );
1251
+ const handleLabelChange = useCallback(
1252
+ (addLabels, removeLabels) => {
1253
+ const ctx = findIssueContext(reposRef.current, selectedIdRef.current, configRef.current);
1254
+ if (!(ctx.issue && ctx.repoName)) return;
1255
+ const { issue, repoName } = ctx;
1256
+ const args = ["issue", "edit", String(issue.number), "--repo", repoName];
1257
+ for (const label of addLabels) args.push("--add-label", label);
1258
+ for (const label of removeLabels) args.push("--remove-label", label);
1259
+ const t = toast.loading("Updating labels...");
1260
+ execFileAsync2("gh", args, { encoding: "utf-8", timeout: 3e4 }).then(() => {
1261
+ t.resolve(`Labels updated on #${issue.number}`);
1262
+ refresh();
1263
+ onOverlayDone();
1264
+ }).catch((err) => {
1265
+ t.reject(`Label update failed: ${err instanceof Error ? err.message : String(err)}`);
1266
+ onOverlayDone();
1267
+ });
1268
+ },
1269
+ [toast, refresh, onOverlayDone]
1270
+ );
1037
1271
  const handleBulkAssign = useCallback(
1038
1272
  async (ids) => {
1039
1273
  const failed = [];
@@ -1164,6 +1398,7 @@ function useActions({
1164
1398
  handleStatusChange,
1165
1399
  handleAssign,
1166
1400
  handleUnassign,
1401
+ handleLabelChange,
1167
1402
  handleCreateIssue,
1168
1403
  handleBulkAssign,
1169
1404
  handleBulkUnassign,
@@ -1299,7 +1534,13 @@ function useData(config2, options, refreshIntervalMs) {
1299
1534
  return { ...prev, data: fn(prev.data) };
1300
1535
  });
1301
1536
  }, []);
1302
- return { ...state, refresh, mutateData };
1537
+ const pauseAutoRefresh = useCallback2(() => {
1538
+ setState((prev) => ({ ...prev, autoRefreshPaused: true }));
1539
+ }, []);
1540
+ const resumeAutoRefresh = useCallback2(() => {
1541
+ setState((prev) => ({ ...prev, autoRefreshPaused: false }));
1542
+ }, []);
1543
+ return { ...state, refresh, mutateData, pauseAutoRefresh, resumeAutoRefresh };
1303
1544
  }
1304
1545
  var INITIAL_STATE, STALE_THRESHOLDS, MAX_REFRESH_FAILURES;
1305
1546
  var init_use_data = __esm({
@@ -1325,47 +1566,267 @@ var init_use_data = __esm({
1325
1566
  }
1326
1567
  });
1327
1568
 
1328
- // src/board/hooks/use-multi-select.ts
1329
- import { useCallback as useCallback3, useRef as useRef3, useState as useState2 } from "react";
1330
- function useMultiSelect(getRepoForId) {
1331
- const [selected, setSelected] = useState2(/* @__PURE__ */ new Set());
1332
- const repoRef = useRef3(null);
1333
- const getRepoRef = useRef3(getRepoForId);
1334
- getRepoRef.current = getRepoForId;
1335
- const toggle = useCallback3((id) => {
1336
- setSelected((prev) => {
1337
- const repo = getRepoRef.current(id);
1338
- if (!repo) return prev;
1339
- const next = new Set(prev);
1340
- if (next.has(id)) {
1341
- next.delete(id);
1342
- if (next.size === 0) repoRef.current = null;
1343
- } else {
1344
- if (repoRef.current && repoRef.current !== repo) {
1345
- next.clear();
1569
+ // src/board/hooks/use-keyboard.ts
1570
+ import { useInput } from "ink";
1571
+ import { useCallback as useCallback3 } from "react";
1572
+ function isHeaderId(id) {
1573
+ return id != null && (id.startsWith("header:") || id.startsWith("sub:"));
1574
+ }
1575
+ function useKeyboard({
1576
+ ui,
1577
+ nav,
1578
+ multiSelect,
1579
+ selectedIssue,
1580
+ selectedRepoStatusOptionsLength,
1581
+ actions,
1582
+ onSearchEscape
1583
+ }) {
1584
+ const {
1585
+ exit,
1586
+ refresh,
1587
+ handleSlack,
1588
+ handleCopyLink,
1589
+ handleOpen,
1590
+ handleEnterFocus,
1591
+ handlePick,
1592
+ handleAssign,
1593
+ handleUnassign,
1594
+ handleEnterLabel,
1595
+ handleEnterCreateNl,
1596
+ handleErrorAction,
1597
+ toastInfo
1598
+ } = actions;
1599
+ const handleInput = useCallback3(
1600
+ (input2, key) => {
1601
+ if (input2 === "?") {
1602
+ ui.toggleHelp();
1603
+ return;
1604
+ }
1605
+ if (key.escape && ui.state.mode !== "focus") {
1606
+ if (ui.state.mode === "multiSelect") {
1607
+ multiSelect.clear();
1346
1608
  }
1347
- repoRef.current = repo;
1348
- next.add(id);
1609
+ ui.exitOverlay();
1610
+ return;
1349
1611
  }
1350
- return next;
1351
- });
1352
- }, []);
1353
- const clear = useCallback3(() => {
1354
- setSelected(/* @__PURE__ */ new Set());
1355
- repoRef.current = null;
1356
- }, []);
1357
- const prune = useCallback3((validIds) => {
1358
- setSelected((prev) => {
1359
- const next = /* @__PURE__ */ new Set();
1360
- for (const id of prev) {
1361
- if (validIds.has(id)) next.add(id);
1612
+ if (ui.canNavigate) {
1613
+ if (input2 === "j" || key.downArrow) {
1614
+ nav.moveDown();
1615
+ return;
1616
+ }
1617
+ if (input2 === "k" || key.upArrow) {
1618
+ nav.moveUp();
1619
+ return;
1620
+ }
1621
+ if (key.tab) {
1622
+ if (ui.state.mode === "multiSelect") {
1623
+ multiSelect.clear();
1624
+ ui.clearMultiSelect();
1625
+ }
1626
+ key.shift ? nav.prevSection() : nav.nextSection();
1627
+ return;
1628
+ }
1362
1629
  }
1363
- if (next.size === prev.size) return prev;
1364
- if (next.size === 0) repoRef.current = null;
1365
- return next;
1366
- });
1630
+ if (ui.state.mode === "multiSelect") {
1631
+ if (input2 === " ") {
1632
+ const id = nav.selectedId;
1633
+ if (id && !isHeaderId(id)) {
1634
+ multiSelect.toggle(id);
1635
+ }
1636
+ return;
1637
+ }
1638
+ if (key.return) {
1639
+ if (multiSelect.count > 0) {
1640
+ ui.enterBulkAction();
1641
+ }
1642
+ return;
1643
+ }
1644
+ if (input2 === "m" && multiSelect.count > 0) {
1645
+ ui.enterBulkAction();
1646
+ return;
1647
+ }
1648
+ return;
1649
+ }
1650
+ if (input2 === "d") {
1651
+ if (handleErrorAction("dismiss")) return;
1652
+ }
1653
+ if (input2 === "r" && handleErrorAction("retry")) return;
1654
+ if (ui.canAct) {
1655
+ if (input2 === "/") {
1656
+ multiSelect.clear();
1657
+ ui.enterSearch();
1658
+ return;
1659
+ }
1660
+ if (input2 === "q") {
1661
+ exit();
1662
+ return;
1663
+ }
1664
+ if (input2 === "r" || input2 === "R") {
1665
+ multiSelect.clear();
1666
+ refresh();
1667
+ return;
1668
+ }
1669
+ if (input2 === "s") {
1670
+ handleSlack();
1671
+ return;
1672
+ }
1673
+ if (input2 === "y") {
1674
+ handleCopyLink();
1675
+ return;
1676
+ }
1677
+ if (input2 === "p") {
1678
+ handlePick();
1679
+ return;
1680
+ }
1681
+ if (input2 === "a") {
1682
+ handleAssign();
1683
+ return;
1684
+ }
1685
+ if (input2 === "u") {
1686
+ handleUnassign();
1687
+ return;
1688
+ }
1689
+ if (input2 === "c") {
1690
+ if (selectedIssue) {
1691
+ multiSelect.clear();
1692
+ ui.enterComment();
1693
+ }
1694
+ return;
1695
+ }
1696
+ if (input2 === "m") {
1697
+ if (selectedIssue && selectedRepoStatusOptionsLength > 0) {
1698
+ multiSelect.clear();
1699
+ ui.enterStatus();
1700
+ } else if (selectedIssue) {
1701
+ toastInfo("Issue not in a project board");
1702
+ }
1703
+ return;
1704
+ }
1705
+ if (input2 === "n") {
1706
+ multiSelect.clear();
1707
+ ui.enterCreate();
1708
+ return;
1709
+ }
1710
+ if (input2 === "f") {
1711
+ handleEnterFocus();
1712
+ return;
1713
+ }
1714
+ if (input2 === "C") {
1715
+ nav.collapseAll();
1716
+ return;
1717
+ }
1718
+ if (input2 === "l") {
1719
+ if (selectedIssue) {
1720
+ multiSelect.clear();
1721
+ handleEnterLabel();
1722
+ }
1723
+ return;
1724
+ }
1725
+ if (input2 === "I") {
1726
+ handleEnterCreateNl();
1727
+ return;
1728
+ }
1729
+ if (input2 === " ") {
1730
+ const id = nav.selectedId;
1731
+ if (id && !isHeaderId(id)) {
1732
+ multiSelect.toggle(id);
1733
+ ui.enterMultiSelect();
1734
+ } else if (isHeaderId(nav.selectedId)) {
1735
+ nav.toggleSection();
1736
+ }
1737
+ return;
1738
+ }
1739
+ if (key.return) {
1740
+ if (isHeaderId(nav.selectedId)) {
1741
+ nav.toggleSection();
1742
+ return;
1743
+ }
1744
+ handleOpen();
1745
+ return;
1746
+ }
1747
+ }
1748
+ },
1749
+ [
1750
+ ui,
1751
+ nav,
1752
+ exit,
1753
+ refresh,
1754
+ handleSlack,
1755
+ handleCopyLink,
1756
+ handleOpen,
1757
+ handlePick,
1758
+ handleAssign,
1759
+ handleUnassign,
1760
+ handleEnterLabel,
1761
+ handleEnterCreateNl,
1762
+ selectedIssue,
1763
+ selectedRepoStatusOptionsLength,
1764
+ toastInfo,
1765
+ nav.selectedId,
1766
+ multiSelect,
1767
+ handleEnterFocus,
1768
+ handleErrorAction
1769
+ ]
1770
+ );
1771
+ const inputActive = ui.state.mode === "normal" || ui.state.mode === "multiSelect" || ui.state.mode === "focus";
1772
+ useInput(handleInput, { isActive: inputActive });
1773
+ const handleSearchEscape = useCallback3(
1774
+ (_input, key) => {
1775
+ if (key.escape) {
1776
+ onSearchEscape();
1777
+ }
1778
+ },
1779
+ [onSearchEscape]
1780
+ );
1781
+ useInput(handleSearchEscape, { isActive: ui.state.mode === "search" });
1782
+ }
1783
+ var init_use_keyboard = __esm({
1784
+ "src/board/hooks/use-keyboard.ts"() {
1785
+ "use strict";
1786
+ }
1787
+ });
1788
+
1789
+ // src/board/hooks/use-multi-select.ts
1790
+ import { useCallback as useCallback4, useRef as useRef3, useState as useState2 } from "react";
1791
+ function useMultiSelect(getRepoForId) {
1792
+ const [selected, setSelected] = useState2(/* @__PURE__ */ new Set());
1793
+ const repoRef = useRef3(null);
1794
+ const getRepoRef = useRef3(getRepoForId);
1795
+ getRepoRef.current = getRepoForId;
1796
+ const toggle = useCallback4((id) => {
1797
+ setSelected((prev) => {
1798
+ const repo = getRepoRef.current(id);
1799
+ if (!repo) return prev;
1800
+ const next = new Set(prev);
1801
+ if (next.has(id)) {
1802
+ next.delete(id);
1803
+ if (next.size === 0) repoRef.current = null;
1804
+ } else {
1805
+ if (repoRef.current && repoRef.current !== repo) {
1806
+ next.clear();
1807
+ }
1808
+ repoRef.current = repo;
1809
+ next.add(id);
1810
+ }
1811
+ return next;
1812
+ });
1813
+ }, []);
1814
+ const clear = useCallback4(() => {
1815
+ setSelected(/* @__PURE__ */ new Set());
1816
+ repoRef.current = null;
1817
+ }, []);
1818
+ const prune = useCallback4((validIds) => {
1819
+ setSelected((prev) => {
1820
+ const next = /* @__PURE__ */ new Set();
1821
+ for (const id of prev) {
1822
+ if (validIds.has(id)) next.add(id);
1823
+ }
1824
+ if (next.size === prev.size) return prev;
1825
+ if (next.size === 0) repoRef.current = null;
1826
+ return next;
1827
+ });
1367
1828
  }, []);
1368
- const isSelected = useCallback3((id) => selected.has(id), [selected]);
1829
+ const isSelected = useCallback4((id) => selected.has(id), [selected]);
1369
1830
  return {
1370
1831
  selected,
1371
1832
  count: selected.size,
@@ -1383,7 +1844,7 @@ var init_use_multi_select = __esm({
1383
1844
  });
1384
1845
 
1385
1846
  // src/board/hooks/use-navigation.ts
1386
- import { useCallback as useCallback4, useMemo, useReducer, useRef as useRef4 } from "react";
1847
+ import { useCallback as useCallback5, useMemo, useReducer, useRef as useRef4 } from "react";
1387
1848
  function arraysEqual(a, b) {
1388
1849
  if (a.length !== b.length) return false;
1389
1850
  for (let i = 0; i < a.length; i++) {
@@ -1405,7 +1866,7 @@ function navReducer(state, action) {
1405
1866
  case "SET_ITEMS": {
1406
1867
  const sections = [...new Set(action.items.map((i) => i.section))];
1407
1868
  const isFirstLoad = state.sections.length === 0;
1408
- const collapsedSections = isFirstLoad ? new Set(sections) : state.collapsedSections;
1869
+ const collapsedSections = isFirstLoad ? new Set(sections.filter((s) => s === "activity")) : state.collapsedSections;
1409
1870
  const selectionValid = state.selectedId != null && action.items.some((i) => i.id === state.selectedId);
1410
1871
  if (!isFirstLoad && selectionValid && arraysEqual(sections, state.sections)) {
1411
1872
  return state;
@@ -1443,6 +1904,9 @@ function navReducer(state, action) {
1443
1904
  }
1444
1905
  return { ...state, collapsedSections: next };
1445
1906
  }
1907
+ case "COLLAPSE_ALL": {
1908
+ return { ...state, collapsedSections: new Set(state.sections) };
1909
+ }
1446
1910
  default:
1447
1911
  return state;
1448
1912
  }
@@ -1477,17 +1941,17 @@ function useNavigation(allItems) {
1477
1941
  const idx = visibleItems.findIndex((i) => i.id === state.selectedId);
1478
1942
  return idx >= 0 ? idx : 0;
1479
1943
  }, [state.selectedId, visibleItems]);
1480
- const moveUp = useCallback4(() => {
1944
+ const moveUp = useCallback5(() => {
1481
1945
  const newIdx = Math.max(0, selectedIndex - 1);
1482
1946
  const item = visibleItems[newIdx];
1483
1947
  if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
1484
1948
  }, [selectedIndex, visibleItems]);
1485
- const moveDown = useCallback4(() => {
1949
+ const moveDown = useCallback5(() => {
1486
1950
  const newIdx = Math.min(visibleItems.length - 1, selectedIndex + 1);
1487
1951
  const item = visibleItems[newIdx];
1488
1952
  if (item) dispatch({ type: "SELECT", id: item.id, section: item.section });
1489
1953
  }, [selectedIndex, visibleItems]);
1490
- const nextSection = useCallback4(() => {
1954
+ const nextSection = useCallback5(() => {
1491
1955
  const currentItem = visibleItems[selectedIndex];
1492
1956
  if (!currentItem) return;
1493
1957
  const currentSectionIdx = state.sections.indexOf(currentItem.section);
@@ -1496,7 +1960,7 @@ function useNavigation(allItems) {
1496
1960
  const header = visibleItems.find((i) => i.section === nextSectionId && i.type === "header");
1497
1961
  if (header) dispatch({ type: "SELECT", id: header.id, section: header.section });
1498
1962
  }, [selectedIndex, visibleItems, state.sections]);
1499
- const prevSection = useCallback4(() => {
1963
+ const prevSection = useCallback5(() => {
1500
1964
  const currentItem = visibleItems[selectedIndex];
1501
1965
  if (!currentItem) return;
1502
1966
  const currentSectionIdx = state.sections.indexOf(currentItem.section);
@@ -1505,19 +1969,22 @@ function useNavigation(allItems) {
1505
1969
  const header = visibleItems.find((i) => i.section === prevSectionId && i.type === "header");
1506
1970
  if (header) dispatch({ type: "SELECT", id: header.id, section: header.section });
1507
1971
  }, [selectedIndex, visibleItems, state.sections]);
1508
- const toggleSection = useCallback4(() => {
1972
+ const toggleSection = useCallback5(() => {
1509
1973
  const currentItem = visibleItems[selectedIndex];
1510
1974
  if (!currentItem) return;
1511
1975
  const key = currentItem.type === "subHeader" ? currentItem.id : currentItem.section;
1512
1976
  dispatch({ type: "TOGGLE_SECTION", section: key });
1513
1977
  }, [selectedIndex, visibleItems]);
1978
+ const collapseAll = useCallback5(() => {
1979
+ dispatch({ type: "COLLAPSE_ALL" });
1980
+ }, []);
1514
1981
  const allItemsRef = useRef4(allItems);
1515
1982
  allItemsRef.current = allItems;
1516
- const select2 = useCallback4((id) => {
1983
+ const select2 = useCallback5((id) => {
1517
1984
  const item = allItemsRef.current.find((i) => i.id === id);
1518
1985
  dispatch({ type: "SELECT", id, section: item?.section });
1519
1986
  }, []);
1520
- const isCollapsed = useCallback4(
1987
+ const isCollapsed = useCallback5(
1521
1988
  (section) => state.collapsedSections.has(section),
1522
1989
  [state.collapsedSections]
1523
1990
  );
@@ -1530,6 +1997,7 @@ function useNavigation(allItems) {
1530
1997
  nextSection,
1531
1998
  prevSection,
1532
1999
  toggleSection,
2000
+ collapseAll,
1533
2001
  select: select2,
1534
2002
  isCollapsed
1535
2003
  };
@@ -1541,25 +2009,25 @@ var init_use_navigation = __esm({
1541
2009
  });
1542
2010
 
1543
2011
  // src/board/hooks/use-toast.ts
1544
- import { useCallback as useCallback5, useRef as useRef5, useState as useState3 } from "react";
2012
+ import { useCallback as useCallback6, useRef as useRef5, useState as useState3 } from "react";
1545
2013
  function useToast() {
1546
2014
  const [toasts, setToasts] = useState3([]);
1547
2015
  const timersRef = useRef5(/* @__PURE__ */ new Map());
1548
- const clearTimer = useCallback5((id) => {
2016
+ const clearTimer = useCallback6((id) => {
1549
2017
  const timer = timersRef.current.get(id);
1550
2018
  if (timer) {
1551
2019
  clearTimeout(timer);
1552
2020
  timersRef.current.delete(id);
1553
2021
  }
1554
2022
  }, []);
1555
- const removeToast = useCallback5(
2023
+ const removeToast = useCallback6(
1556
2024
  (id) => {
1557
2025
  clearTimer(id);
1558
2026
  setToasts((prev) => prev.filter((t) => t.id !== id));
1559
2027
  },
1560
2028
  [clearTimer]
1561
2029
  );
1562
- const addToast = useCallback5(
2030
+ const addToast = useCallback6(
1563
2031
  (t) => {
1564
2032
  const id = `toast-${++nextId}`;
1565
2033
  const newToast = { ...t, id, createdAt: Date.now() };
@@ -1588,25 +2056,25 @@ function useToast() {
1588
2056
  [removeToast, clearTimer]
1589
2057
  );
1590
2058
  const toast = {
1591
- info: useCallback5(
2059
+ info: useCallback6(
1592
2060
  (message) => {
1593
2061
  addToast({ type: "info", message });
1594
2062
  },
1595
2063
  [addToast]
1596
2064
  ),
1597
- success: useCallback5(
2065
+ success: useCallback6(
1598
2066
  (message) => {
1599
2067
  addToast({ type: "success", message });
1600
2068
  },
1601
2069
  [addToast]
1602
2070
  ),
1603
- error: useCallback5(
2071
+ error: useCallback6(
1604
2072
  (message, retry) => {
1605
2073
  addToast(retry ? { type: "error", message, retry } : { type: "error", message });
1606
2074
  },
1607
2075
  [addToast]
1608
2076
  ),
1609
- loading: useCallback5(
2077
+ loading: useCallback6(
1610
2078
  (message) => {
1611
2079
  const id = addToast({ type: "loading", message });
1612
2080
  return {
@@ -1623,20 +2091,20 @@ function useToast() {
1623
2091
  [addToast, removeToast]
1624
2092
  )
1625
2093
  };
1626
- const dismiss = useCallback5(
2094
+ const dismiss = useCallback6(
1627
2095
  (id) => {
1628
2096
  removeToast(id);
1629
2097
  },
1630
2098
  [removeToast]
1631
2099
  );
1632
- const dismissAll = useCallback5(() => {
2100
+ const dismissAll = useCallback6(() => {
1633
2101
  for (const timer of timersRef.current.values()) {
1634
2102
  clearTimeout(timer);
1635
2103
  }
1636
2104
  timersRef.current.clear();
1637
2105
  setToasts([]);
1638
2106
  }, []);
1639
- const handleErrorAction = useCallback5(
2107
+ const handleErrorAction = useCallback6(
1640
2108
  (action) => {
1641
2109
  const errorToast = toasts.find((t) => t.type === "error");
1642
2110
  if (!errorToast) return false;
@@ -1666,7 +2134,7 @@ var init_use_toast = __esm({
1666
2134
  });
1667
2135
 
1668
2136
  // src/board/hooks/use-ui-state.ts
1669
- import { useCallback as useCallback6, useReducer as useReducer2 } from "react";
2137
+ import { useCallback as useCallback7, useReducer as useReducer2 } from "react";
1670
2138
  function uiReducer(state, action) {
1671
2139
  switch (action.type) {
1672
2140
  case "ENTER_SEARCH":
@@ -1685,6 +2153,12 @@ function uiReducer(state, action) {
1685
2153
  case "ENTER_CREATE":
1686
2154
  if (state.mode !== "normal") return state;
1687
2155
  return { ...state, mode: "overlay:create", previousMode: "normal" };
2156
+ case "ENTER_CREATE_NL":
2157
+ if (state.mode !== "normal") return state;
2158
+ return { ...state, mode: "overlay:createNl", previousMode: "normal" };
2159
+ case "ENTER_LABEL":
2160
+ if (state.mode !== "normal") return state;
2161
+ return { ...state, mode: "overlay:label", previousMode: "normal" };
1688
2162
  case "ENTER_MULTI_SELECT":
1689
2163
  if (state.mode !== "normal" && state.mode !== "multiSelect") return state;
1690
2164
  return { ...state, mode: "multiSelect", previousMode: "normal" };
@@ -1728,18 +2202,20 @@ function useUIState() {
1728
2202
  const [state, dispatch] = useReducer2(uiReducer, INITIAL_STATE2);
1729
2203
  return {
1730
2204
  state,
1731
- enterSearch: useCallback6(() => dispatch({ type: "ENTER_SEARCH" }), []),
1732
- enterComment: useCallback6(() => dispatch({ type: "ENTER_COMMENT" }), []),
1733
- enterStatus: useCallback6(() => dispatch({ type: "ENTER_STATUS" }), []),
1734
- enterCreate: useCallback6(() => dispatch({ type: "ENTER_CREATE" }), []),
1735
- enterMultiSelect: useCallback6(() => dispatch({ type: "ENTER_MULTI_SELECT" }), []),
1736
- enterBulkAction: useCallback6(() => dispatch({ type: "ENTER_BULK_ACTION" }), []),
1737
- enterConfirmPick: useCallback6(() => dispatch({ type: "ENTER_CONFIRM_PICK" }), []),
1738
- enterFocus: useCallback6(() => dispatch({ type: "ENTER_FOCUS" }), []),
1739
- toggleHelp: useCallback6(() => dispatch({ type: "TOGGLE_HELP" }), []),
1740
- exitOverlay: useCallback6(() => dispatch({ type: "EXIT_OVERLAY" }), []),
1741
- exitToNormal: useCallback6(() => dispatch({ type: "EXIT_TO_NORMAL" }), []),
1742
- clearMultiSelect: useCallback6(() => dispatch({ type: "CLEAR_MULTI_SELECT" }), []),
2205
+ enterSearch: useCallback7(() => dispatch({ type: "ENTER_SEARCH" }), []),
2206
+ enterComment: useCallback7(() => dispatch({ type: "ENTER_COMMENT" }), []),
2207
+ enterStatus: useCallback7(() => dispatch({ type: "ENTER_STATUS" }), []),
2208
+ enterCreate: useCallback7(() => dispatch({ type: "ENTER_CREATE" }), []),
2209
+ enterCreateNl: useCallback7(() => dispatch({ type: "ENTER_CREATE_NL" }), []),
2210
+ enterLabel: useCallback7(() => dispatch({ type: "ENTER_LABEL" }), []),
2211
+ enterMultiSelect: useCallback7(() => dispatch({ type: "ENTER_MULTI_SELECT" }), []),
2212
+ enterBulkAction: useCallback7(() => dispatch({ type: "ENTER_BULK_ACTION" }), []),
2213
+ enterConfirmPick: useCallback7(() => dispatch({ type: "ENTER_CONFIRM_PICK" }), []),
2214
+ enterFocus: useCallback7(() => dispatch({ type: "ENTER_FOCUS" }), []),
2215
+ toggleHelp: useCallback7(() => dispatch({ type: "TOGGLE_HELP" }), []),
2216
+ exitOverlay: useCallback7(() => dispatch({ type: "EXIT_OVERLAY" }), []),
2217
+ exitToNormal: useCallback7(() => dispatch({ type: "EXIT_TO_NORMAL" }), []),
2218
+ clearMultiSelect: useCallback7(() => dispatch({ type: "CLEAR_MULTI_SELECT" }), []),
1743
2219
  canNavigate: canNavigate(state),
1744
2220
  canAct: canAct(state),
1745
2221
  isOverlay: isOverlay(state)
@@ -1757,93 +2233,347 @@ var init_use_ui_state = __esm({
1757
2233
  }
1758
2234
  });
1759
2235
 
1760
- // src/board/components/bulk-action-menu.tsx
1761
- import { Box, Text, useInput } from "ink";
1762
- import { useState as useState4 } from "react";
1763
- import { jsx, jsxs } from "react/jsx-runtime";
1764
- function getMenuItems(selectionType) {
1765
- if (selectionType === "github") {
1766
- return [
1767
- { label: "Assign all to me", action: { type: "assign" } },
1768
- { label: "Unassign all from me", action: { type: "unassign" } },
1769
- { label: "Move status (all)", action: { type: "statusChange" } }
1770
- ];
1771
- }
1772
- if (selectionType === "ticktick") {
1773
- return [
1774
- { label: "Complete all", action: { type: "complete" } },
1775
- { label: "Delete all", action: { type: "delete" } }
1776
- ];
1777
- }
1778
- return [];
2236
+ // src/board/components/detail-panel.tsx
2237
+ import { Box, Text } from "ink";
2238
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2239
+ function truncateLines(text, maxLines) {
2240
+ const lines = text.split("\n").slice(0, maxLines);
2241
+ return lines.join("\n");
1779
2242
  }
1780
- function BulkActionMenu({ count, selectionType, onSelect, onCancel }) {
1781
- const items = getMenuItems(selectionType);
1782
- const [selectedIdx, setSelectedIdx] = useState4(0);
1783
- useInput((input2, key) => {
1784
- if (key.escape) return onCancel();
1785
- if (key.return) {
1786
- const item = items[selectedIdx];
1787
- if (item) onSelect(item.action);
1788
- return;
1789
- }
1790
- if (input2 === "j" || key.downArrow) {
1791
- setSelectedIdx((i) => Math.min(i + 1, items.length - 1));
1792
- }
1793
- if (input2 === "k" || key.upArrow) {
1794
- setSelectedIdx((i) => Math.max(i - 1, 0));
1795
- }
1796
- });
1797
- if (items.length === 0) {
1798
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1799
- /* @__PURE__ */ jsx(Text, { color: "yellow", children: "No bulk actions for mixed selection types." }),
1800
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Esc to cancel" })
1801
- ] });
1802
- }
1803
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1804
- /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
1805
- "Bulk action (",
1806
- count,
1807
- " selected):"
1808
- ] }),
1809
- items.map((item, i) => {
1810
- const isSelected = i === selectedIdx;
1811
- const prefix = isSelected ? "> " : " ";
1812
- return /* @__PURE__ */ jsxs(Text, { ...isSelected ? { color: "cyan" } : {}, children: [
1813
- prefix,
1814
- item.label
1815
- ] }, item.action.type);
1816
- }),
1817
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "j/k:navigate Enter:select Esc:cancel" })
2243
+ function stripMarkdown(text) {
2244
+ 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();
2245
+ }
2246
+ function formatBody(body, maxLines) {
2247
+ const plain = stripMarkdown(body);
2248
+ const lines = plain.split("\n");
2249
+ const truncated = lines.slice(0, maxLines).join("\n");
2250
+ return { text: truncated, remaining: Math.max(0, lines.length - maxLines) };
2251
+ }
2252
+ function countSlackLinks(body) {
2253
+ if (!body) return 0;
2254
+ return (body.match(SLACK_URL_RE) ?? []).length;
2255
+ }
2256
+ function BodySection({
2257
+ body,
2258
+ issueNumber
2259
+ }) {
2260
+ const { text, remaining } = formatBody(body, 15);
2261
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2262
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2263
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "--- Description ---" }),
2264
+ /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: text }),
2265
+ remaining > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2266
+ "... (",
2267
+ remaining,
2268
+ " more lines \u2014 gh issue view ",
2269
+ issueNumber,
2270
+ " for full)"
2271
+ ] }) : null
1818
2272
  ] });
1819
2273
  }
1820
- var init_bulk_action_menu = __esm({
1821
- "src/board/components/bulk-action-menu.tsx"() {
1822
- "use strict";
2274
+ function DetailPanel({ issue, task: task2, width }) {
2275
+ if (!(issue || task2)) {
2276
+ return /* @__PURE__ */ jsx(
2277
+ Box,
2278
+ {
2279
+ width,
2280
+ borderStyle: "single",
2281
+ borderColor: "gray",
2282
+ flexDirection: "column",
2283
+ paddingX: 1,
2284
+ children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "No item selected" })
2285
+ }
2286
+ );
1823
2287
  }
1824
- });
1825
-
1826
- // src/board/components/comment-input.tsx
1827
- import { TextInput } from "@inkjs/ui";
2288
+ if (issue) {
2289
+ return /* @__PURE__ */ jsxs(
2290
+ Box,
2291
+ {
2292
+ width,
2293
+ borderStyle: "single",
2294
+ borderColor: "cyan",
2295
+ flexDirection: "column",
2296
+ paddingX: 1,
2297
+ children: [
2298
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
2299
+ "#",
2300
+ issue.number,
2301
+ " ",
2302
+ issue.title
2303
+ ] }),
2304
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2305
+ /* @__PURE__ */ jsxs(Box, { children: [
2306
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "State: " }),
2307
+ /* @__PURE__ */ jsx(Text, { color: issue.state === "open" ? "green" : "red", children: issue.state })
2308
+ ] }),
2309
+ (issue.assignees ?? []).length > 0 ? /* @__PURE__ */ jsxs(Box, { children: [
2310
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Assignees: " }),
2311
+ /* @__PURE__ */ jsx(Text, { children: (issue.assignees ?? []).map((a) => a.login).join(", ") })
2312
+ ] }) : null,
2313
+ issue.labels.length > 0 ? /* @__PURE__ */ jsxs(Box, { children: [
2314
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Labels: " }),
2315
+ /* @__PURE__ */ jsx(Text, { children: issue.labels.map((l) => l.name).join(", ") })
2316
+ ] }) : null,
2317
+ issue.projectStatus ? /* @__PURE__ */ jsxs(Box, { children: [
2318
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Status: " }),
2319
+ /* @__PURE__ */ jsx(Text, { color: "magenta", children: issue.projectStatus })
2320
+ ] }) : null,
2321
+ issue.targetDate ? /* @__PURE__ */ jsxs(Box, { children: [
2322
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Target: " }),
2323
+ /* @__PURE__ */ jsx(Text, { children: issue.targetDate })
2324
+ ] }) : null,
2325
+ /* @__PURE__ */ jsxs(Box, { children: [
2326
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Updated: " }),
2327
+ /* @__PURE__ */ jsx(Text, { children: new Date(issue.updatedAt).toLocaleString() })
2328
+ ] }),
2329
+ issue.slackThreadUrl ? /* @__PURE__ */ jsxs(Box, { children: [
2330
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Slack: " }),
2331
+ /* @__PURE__ */ jsx(Text, { color: "blue", children: countSlackLinks(issue.body) > 1 ? `${countSlackLinks(issue.body)} links (s opens first)` : "thread (s to open)" })
2332
+ ] }) : null,
2333
+ issue.body ? /* @__PURE__ */ jsx(BodySection, { body: issue.body, issueNumber: issue.number }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2334
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2335
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "(no description)" })
2336
+ ] }),
2337
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2338
+ /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: issue.url })
2339
+ ]
2340
+ }
2341
+ );
2342
+ }
2343
+ const t = task2;
2344
+ return /* @__PURE__ */ jsxs(
2345
+ Box,
2346
+ {
2347
+ width,
2348
+ borderStyle: "single",
2349
+ borderColor: "yellow",
2350
+ flexDirection: "column",
2351
+ paddingX: 1,
2352
+ children: [
2353
+ /* @__PURE__ */ jsx(Text, { color: "yellow", bold: true, children: t.title }),
2354
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2355
+ /* @__PURE__ */ jsxs(Box, { children: [
2356
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Priority: " }),
2357
+ /* @__PURE__ */ jsx(Text, { children: PRIORITY_LABELS2[t.priority] ?? "None" })
2358
+ ] }),
2359
+ t.dueDate ? /* @__PURE__ */ jsxs(Box, { children: [
2360
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Due: " }),
2361
+ /* @__PURE__ */ jsx(Text, { children: new Date(t.dueDate).toLocaleDateString() })
2362
+ ] }) : null,
2363
+ (t.tags ?? []).length > 0 ? /* @__PURE__ */ jsxs(Box, { children: [
2364
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Tags: " }),
2365
+ /* @__PURE__ */ jsx(Text, { children: t.tags.join(", ") })
2366
+ ] }) : null,
2367
+ t.content ? /* @__PURE__ */ jsxs(Fragment, { children: [
2368
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2369
+ /* @__PURE__ */ jsx(Text, { children: truncateLines(t.content, 8) })
2370
+ ] }) : null,
2371
+ (t.items ?? []).length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
2372
+ /* @__PURE__ */ jsx(Text, { children: "" }),
2373
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Checklist:" }),
2374
+ t.items.slice(0, 5).map((item) => /* @__PURE__ */ jsxs(Text, { children: [
2375
+ item.status === 2 ? "\u2611" : "\u2610",
2376
+ " ",
2377
+ item.title
2378
+ ] }, item.id)),
2379
+ t.items.length > 5 ? /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
2380
+ "...and ",
2381
+ t.items.length - 5,
2382
+ " more"
2383
+ ] }) : null
2384
+ ] }) : null
2385
+ ]
2386
+ }
2387
+ );
2388
+ }
2389
+ var SLACK_URL_RE, PRIORITY_LABELS2;
2390
+ var init_detail_panel = __esm({
2391
+ "src/board/components/detail-panel.tsx"() {
2392
+ "use strict";
2393
+ init_types();
2394
+ SLACK_URL_RE = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/gi;
2395
+ PRIORITY_LABELS2 = {
2396
+ [5 /* High */]: "High",
2397
+ [3 /* Medium */]: "Medium",
2398
+ [1 /* Low */]: "Low",
2399
+ [0 /* None */]: "None"
2400
+ };
2401
+ }
2402
+ });
2403
+
2404
+ // src/board/components/bulk-action-menu.tsx
1828
2405
  import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
1829
- import { useState as useState5 } from "react";
2406
+ import { useState as useState4 } from "react";
1830
2407
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1831
- function CommentInput({ issueNumber, onSubmit, onCancel }) {
2408
+ function getMenuItems(selectionType) {
2409
+ if (selectionType === "github") {
2410
+ return [
2411
+ { label: "Assign all to me", action: { type: "assign" } },
2412
+ { label: "Unassign all from me", action: { type: "unassign" } },
2413
+ { label: "Move status (all)", action: { type: "statusChange" } }
2414
+ ];
2415
+ }
2416
+ if (selectionType === "ticktick") {
2417
+ return [
2418
+ { label: "Complete all", action: { type: "complete" } },
2419
+ { label: "Delete all", action: { type: "delete" } }
2420
+ ];
2421
+ }
2422
+ return [];
2423
+ }
2424
+ function BulkActionMenu({ count, selectionType, onSelect, onCancel }) {
2425
+ const items = getMenuItems(selectionType);
2426
+ const [selectedIdx, setSelectedIdx] = useState4(0);
2427
+ useInput2((input2, key) => {
2428
+ if (key.escape) return onCancel();
2429
+ if (key.return) {
2430
+ const item = items[selectedIdx];
2431
+ if (item) onSelect(item.action);
2432
+ return;
2433
+ }
2434
+ if (input2 === "j" || key.downArrow) {
2435
+ setSelectedIdx((i) => Math.min(i + 1, items.length - 1));
2436
+ }
2437
+ if (input2 === "k" || key.upArrow) {
2438
+ setSelectedIdx((i) => Math.max(i - 1, 0));
2439
+ }
2440
+ });
2441
+ if (items.length === 0) {
2442
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2443
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "No bulk actions for mixed selection types." }),
2444
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Esc to cancel" })
2445
+ ] });
2446
+ }
2447
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2448
+ /* @__PURE__ */ jsxs2(Text2, { color: "cyan", bold: true, children: [
2449
+ "Bulk action (",
2450
+ count,
2451
+ " selected):"
2452
+ ] }),
2453
+ items.map((item, i) => {
2454
+ const isSelected = i === selectedIdx;
2455
+ const prefix = isSelected ? "> " : " ";
2456
+ return /* @__PURE__ */ jsxs2(Text2, { ...isSelected ? { color: "cyan" } : {}, children: [
2457
+ prefix,
2458
+ item.label
2459
+ ] }, item.action.type);
2460
+ }),
2461
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "j/k:navigate Enter:select Esc:cancel" })
2462
+ ] });
2463
+ }
2464
+ var init_bulk_action_menu = __esm({
2465
+ "src/board/components/bulk-action-menu.tsx"() {
2466
+ "use strict";
2467
+ }
2468
+ });
2469
+
2470
+ // src/board/ink-instance.ts
2471
+ function setInkInstance(instance) {
2472
+ _instance = instance;
2473
+ }
2474
+ function getInkInstance() {
2475
+ return _instance;
2476
+ }
2477
+ var _instance;
2478
+ var init_ink_instance = __esm({
2479
+ "src/board/ink-instance.ts"() {
2480
+ "use strict";
2481
+ _instance = null;
2482
+ }
2483
+ });
2484
+
2485
+ // src/board/components/comment-input.tsx
2486
+ import { spawnSync } from "child_process";
2487
+ import { mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "fs";
2488
+ import { tmpdir } from "os";
2489
+ import { join as join3 } from "path";
2490
+ import { TextInput } from "@inkjs/ui";
2491
+ import { Box as Box3, Text as Text3, useInput as useInput3, useStdin } from "ink";
2492
+ import { useEffect as useEffect2, useRef as useRef6, useState as useState5 } from "react";
2493
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2494
+ function CommentInput({
2495
+ issueNumber,
2496
+ onSubmit,
2497
+ onCancel,
2498
+ onPauseRefresh,
2499
+ onResumeRefresh
2500
+ }) {
1832
2501
  const [value, setValue] = useState5("");
1833
- useInput2((_input, key) => {
1834
- if (key.escape) onCancel();
2502
+ const [editing, setEditing] = useState5(false);
2503
+ const { setRawMode } = useStdin();
2504
+ const onSubmitRef = useRef6(onSubmit);
2505
+ const onCancelRef = useRef6(onCancel);
2506
+ const onPauseRef = useRef6(onPauseRefresh);
2507
+ const onResumeRef = useRef6(onResumeRefresh);
2508
+ onSubmitRef.current = onSubmit;
2509
+ onCancelRef.current = onCancel;
2510
+ onPauseRef.current = onPauseRefresh;
2511
+ onResumeRef.current = onResumeRefresh;
2512
+ useInput3((_input, key) => {
2513
+ if (editing) return;
2514
+ if (key.escape) {
2515
+ onCancel();
2516
+ return;
2517
+ }
2518
+ if (_input === "") {
2519
+ setEditing(true);
2520
+ }
1835
2521
  });
1836
- return /* @__PURE__ */ jsxs2(Box2, { children: [
1837
- /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
2522
+ useEffect2(() => {
2523
+ if (!editing) return;
2524
+ const editorEnv = process.env["VISUAL"] ?? process.env["EDITOR"] ?? "vi";
2525
+ const [cmd, ...extraArgs] = editorEnv.split(" ").filter(Boolean);
2526
+ if (!cmd) {
2527
+ setEditing(false);
2528
+ return;
2529
+ }
2530
+ let tmpDir = null;
2531
+ let tmpFile = null;
2532
+ try {
2533
+ onPauseRef.current?.();
2534
+ tmpDir = mkdtempSync(join3(tmpdir(), "hog-comment-"));
2535
+ tmpFile = join3(tmpDir, "comment.md");
2536
+ writeFileSync3(tmpFile, value);
2537
+ const inkInstance = getInkInstance();
2538
+ inkInstance?.clear();
2539
+ setRawMode(false);
2540
+ spawnSync(cmd, [...extraArgs, tmpFile], { stdio: "inherit" });
2541
+ const content = readFileSync3(tmpFile, "utf-8").trim();
2542
+ setRawMode(true);
2543
+ if (content) {
2544
+ onSubmitRef.current(content);
2545
+ } else {
2546
+ onCancelRef.current();
2547
+ }
2548
+ } finally {
2549
+ onResumeRef.current?.();
2550
+ if (tmpFile) {
2551
+ try {
2552
+ rmSync(tmpDir, { recursive: true, force: true });
2553
+ } catch {
2554
+ }
2555
+ }
2556
+ setEditing(false);
2557
+ }
2558
+ }, [editing, value, setRawMode]);
2559
+ if (editing) {
2560
+ return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
2561
+ "Opening editor for #",
2562
+ issueNumber,
2563
+ "\u2026"
2564
+ ] }) });
2565
+ }
2566
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
2567
+ /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
1838
2568
  "comment #",
1839
2569
  issueNumber,
1840
2570
  ": "
1841
2571
  ] }),
1842
- /* @__PURE__ */ jsx2(
2572
+ /* @__PURE__ */ jsx3(
1843
2573
  TextInput,
1844
2574
  {
1845
2575
  defaultValue: value,
1846
- placeholder: "type comment, Enter to post...",
2576
+ placeholder: "type comment (ctrl+e for editor), Enter to post...",
1847
2577
  onChange: setValue,
1848
2578
  onSubmit: (text) => {
1849
2579
  if (text.trim()) onSubmit(text.trim());
@@ -1856,20 +2586,21 @@ function CommentInput({ issueNumber, onSubmit, onCancel }) {
1856
2586
  var init_comment_input = __esm({
1857
2587
  "src/board/components/comment-input.tsx"() {
1858
2588
  "use strict";
2589
+ init_ink_instance();
1859
2590
  }
1860
2591
  });
1861
2592
 
1862
2593
  // src/board/components/confirm-prompt.tsx
1863
- import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
1864
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2594
+ import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
2595
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1865
2596
  function ConfirmPrompt({ message, onConfirm, onCancel }) {
1866
- useInput3((input2, key) => {
2597
+ useInput4((input2, key) => {
1867
2598
  if (input2 === "y" || input2 === "Y") return onConfirm();
1868
2599
  if (input2 === "n" || input2 === "N" || key.escape) return onCancel();
1869
2600
  });
1870
- return /* @__PURE__ */ jsxs3(Box3, { children: [
1871
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: message }),
1872
- /* @__PURE__ */ jsx3(Text3, { color: "gray", children: " (y/n)" })
2601
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
2602
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: message }),
2603
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " (y/n)" })
1873
2604
  ] });
1874
2605
  }
1875
2606
  var init_confirm_prompt = __esm({
@@ -1878,20 +2609,146 @@ var init_confirm_prompt = __esm({
1878
2609
  }
1879
2610
  });
1880
2611
 
2612
+ // src/board/components/label-picker.tsx
2613
+ import { Spinner } from "@inkjs/ui";
2614
+ import { Box as Box5, Text as Text5, useInput as useInput5 } from "ink";
2615
+ import { useEffect as useEffect3, useRef as useRef7, useState as useState6 } from "react";
2616
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2617
+ function LabelPicker({
2618
+ repo,
2619
+ currentLabels,
2620
+ labelCache,
2621
+ onConfirm,
2622
+ onCancel,
2623
+ onError
2624
+ }) {
2625
+ const [labels, setLabels] = useState6(labelCache[repo] ?? null);
2626
+ const [loading, setLoading] = useState6(labels === null);
2627
+ const [fetchAttempted, setFetchAttempted] = useState6(false);
2628
+ const [selected, setSelected] = useState6(new Set(currentLabels));
2629
+ const [cursor, setCursor] = useState6(0);
2630
+ const submittedRef = useRef7(false);
2631
+ useEffect3(() => {
2632
+ if (labels !== null || fetchAttempted) return;
2633
+ setFetchAttempted(true);
2634
+ setLoading(true);
2635
+ let canceled = false;
2636
+ fetchRepoLabelsAsync(repo).then((fetched) => {
2637
+ if (canceled) return;
2638
+ labelCache[repo] = fetched;
2639
+ setLabels(fetched);
2640
+ setLoading(false);
2641
+ }).catch(() => {
2642
+ if (canceled) return;
2643
+ setLoading(false);
2644
+ onError(`Could not fetch labels for ${repo}`);
2645
+ });
2646
+ return () => {
2647
+ canceled = true;
2648
+ };
2649
+ }, [repo, fetchAttempted, labelCache, onError]);
2650
+ useInput5((input2, key) => {
2651
+ if (loading) return;
2652
+ if (key.escape) {
2653
+ onCancel();
2654
+ return;
2655
+ }
2656
+ if (key.return) {
2657
+ if (submittedRef.current) return;
2658
+ submittedRef.current = true;
2659
+ const allLabels2 = labels ?? [];
2660
+ const add = [...selected].filter((l) => !currentLabels.includes(l));
2661
+ const remove = currentLabels.filter((l) => {
2662
+ const exists = allLabels2.some((rl) => rl.name === l);
2663
+ return exists && !selected.has(l);
2664
+ });
2665
+ onConfirm(add, remove);
2666
+ return;
2667
+ }
2668
+ if (input2 === " ") {
2669
+ const allLabels2 = labels ?? [];
2670
+ const item = allLabels2[cursor];
2671
+ if (!item) return;
2672
+ setSelected((prev) => {
2673
+ const next = new Set(prev);
2674
+ if (next.has(item.name)) {
2675
+ next.delete(item.name);
2676
+ } else {
2677
+ next.add(item.name);
2678
+ }
2679
+ return next;
2680
+ });
2681
+ return;
2682
+ }
2683
+ if (input2 === "j" || key.downArrow) {
2684
+ setCursor((i) => Math.min(i + 1, (labels?.length ?? 1) - 1));
2685
+ }
2686
+ if (input2 === "k" || key.upArrow) {
2687
+ setCursor((i) => Math.max(i - 1, 0));
2688
+ }
2689
+ });
2690
+ if (loading) {
2691
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Spinner, { label: "Fetching labels..." }) });
2692
+ }
2693
+ const allLabels = labels ?? [];
2694
+ if (allLabels.length === 0) {
2695
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2696
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", bold: true, children: "Labels:" }),
2697
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No labels in this repo" }),
2698
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Esc:cancel" })
2699
+ ] });
2700
+ }
2701
+ const repoLabelNames = new Set(allLabels.map((l) => l.name));
2702
+ const orphanedLabels = currentLabels.filter((l) => !repoLabelNames.has(l));
2703
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2704
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", bold: true, children: "Labels (Space:toggle Enter:confirm Esc:cancel):" }),
2705
+ orphanedLabels.map((name) => /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2706
+ selected.has(name) ? "[x]" : "[ ]",
2707
+ " ",
2708
+ name,
2709
+ " (orphaned)"
2710
+ ] }, `orphan:${name}`)),
2711
+ allLabels.map((label, i) => {
2712
+ const isSel = i === cursor;
2713
+ const isChecked = selected.has(label.name);
2714
+ return /* @__PURE__ */ jsxs5(Text5, { ...isSel ? { color: "cyan" } : {}, children: [
2715
+ isSel ? ">" : " ",
2716
+ " ",
2717
+ isChecked ? "[x]" : "[ ]",
2718
+ " ",
2719
+ label.name
2720
+ ] }, label.name);
2721
+ })
2722
+ ] });
2723
+ }
2724
+ var init_label_picker = __esm({
2725
+ "src/board/components/label-picker.tsx"() {
2726
+ "use strict";
2727
+ init_github();
2728
+ }
2729
+ });
2730
+
1881
2731
  // src/board/components/create-issue-form.tsx
1882
2732
  import { TextInput as TextInput2 } from "@inkjs/ui";
1883
- import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
1884
- import { useState as useState6 } from "react";
1885
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1886
- function CreateIssueForm({ repos, defaultRepo, onSubmit, onCancel }) {
2733
+ import { Box as Box6, Text as Text6, useInput as useInput6 } from "ink";
2734
+ import { useState as useState7 } from "react";
2735
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2736
+ function CreateIssueForm({
2737
+ repos,
2738
+ defaultRepo,
2739
+ onSubmit,
2740
+ onCancel,
2741
+ labelCache
2742
+ }) {
1887
2743
  const defaultRepoIdx = defaultRepo ? Math.max(
1888
2744
  0,
1889
2745
  repos.findIndex((r) => r.name === defaultRepo)
1890
2746
  ) : 0;
1891
- const [repoIdx, setRepoIdx] = useState6(defaultRepoIdx);
1892
- const [title, setTitle] = useState6("");
1893
- const [field, setField] = useState6("title");
1894
- useInput4((input2, key) => {
2747
+ const [repoIdx, setRepoIdx] = useState7(defaultRepoIdx);
2748
+ const [title, setTitle] = useState7("");
2749
+ const [field, setField] = useState7("title");
2750
+ useInput6((input2, key) => {
2751
+ if (field === "labels") return;
1895
2752
  if (key.escape) return onCancel();
1896
2753
  if (field === "repo") {
1897
2754
  if (input2 === "j" || key.downArrow) {
@@ -1905,12 +2762,40 @@ function CreateIssueForm({ repos, defaultRepo, onSubmit, onCancel }) {
1905
2762
  }
1906
2763
  });
1907
2764
  const selectedRepo = repos[repoIdx];
1908
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
1909
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "Create Issue" }),
1910
- /* @__PURE__ */ jsxs4(Box4, { children: [
1911
- /* @__PURE__ */ jsx4(Text4, { dimColor: field !== "repo", children: "Repo: " }),
1912
- repos.map((r, i) => /* @__PURE__ */ jsx4(
1913
- Text4,
2765
+ if (field === "labels" && selectedRepo) {
2766
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
2767
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", bold: true, children: "Create Issue \u2014 Add Labels (optional)" }),
2768
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2769
+ "Repo: ",
2770
+ selectedRepo.shortName,
2771
+ " Title: ",
2772
+ title
2773
+ ] }),
2774
+ /* @__PURE__ */ jsx6(
2775
+ LabelPicker,
2776
+ {
2777
+ repo: selectedRepo.name,
2778
+ currentLabels: [],
2779
+ labelCache: labelCache ?? {},
2780
+ onConfirm: (addLabels) => {
2781
+ onSubmit(selectedRepo.name, title, addLabels.length > 0 ? addLabels : void 0);
2782
+ },
2783
+ onCancel: () => {
2784
+ onSubmit(selectedRepo.name, title);
2785
+ },
2786
+ onError: () => {
2787
+ onSubmit(selectedRepo.name, title);
2788
+ }
2789
+ }
2790
+ )
2791
+ ] });
2792
+ }
2793
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
2794
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", bold: true, children: "Create Issue" }),
2795
+ /* @__PURE__ */ jsxs6(Box6, { children: [
2796
+ /* @__PURE__ */ jsx6(Text6, { dimColor: field !== "repo", children: "Repo: " }),
2797
+ repos.map((r, i) => /* @__PURE__ */ jsx6(
2798
+ Text6,
1914
2799
  {
1915
2800
  ...i === repoIdx ? { color: "cyan", bold: true } : {},
1916
2801
  dimColor: field !== "repo",
@@ -1918,215 +2803,53 @@ function CreateIssueForm({ repos, defaultRepo, onSubmit, onCancel }) {
1918
2803
  },
1919
2804
  r.name
1920
2805
  )),
1921
- field === "repo" ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " j/k:select Tab:next" }) : null
2806
+ field === "repo" ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " j/k:select Tab:next" }) : null
1922
2807
  ] }),
1923
- /* @__PURE__ */ jsxs4(Box4, { children: [
1924
- /* @__PURE__ */ jsx4(Text4, { dimColor: field !== "title", children: "Title: " }),
1925
- field === "title" ? /* @__PURE__ */ jsx4(
2808
+ /* @__PURE__ */ jsxs6(Box6, { children: [
2809
+ /* @__PURE__ */ jsx6(Text6, { dimColor: field !== "title", children: "Title: " }),
2810
+ field === "title" ? /* @__PURE__ */ jsx6(
1926
2811
  TextInput2,
1927
2812
  {
1928
2813
  defaultValue: title,
1929
2814
  placeholder: "issue title...",
1930
2815
  onChange: setTitle,
1931
2816
  onSubmit: (text) => {
1932
- if (text.trim() && selectedRepo) {
1933
- onSubmit(selectedRepo.name, text.trim());
2817
+ const trimmed = text.trim();
2818
+ if (!(trimmed && selectedRepo)) return;
2819
+ if (labelCache !== void 0) {
2820
+ setTitle(trimmed);
2821
+ setField("labels");
2822
+ } else {
2823
+ onSubmit(selectedRepo.name, trimmed);
1934
2824
  }
1935
2825
  }
1936
2826
  }
1937
- ) : /* @__PURE__ */ jsx4(Text4, { children: title || "(empty)" })
2827
+ ) : /* @__PURE__ */ jsx6(Text6, { children: title || "(empty)" })
1938
2828
  ] }),
1939
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Tab:switch fields Enter:submit Esc:cancel" })
2829
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Tab:switch fields Enter:next Esc:cancel" })
1940
2830
  ] });
1941
2831
  }
1942
2832
  var init_create_issue_form = __esm({
1943
2833
  "src/board/components/create-issue-form.tsx"() {
1944
2834
  "use strict";
1945
- }
1946
- });
1947
-
1948
- // src/board/components/detail-panel.tsx
1949
- import { Box as Box5, Text as Text5 } from "ink";
1950
- import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1951
- function truncateLines(text, maxLines) {
1952
- const lines = text.split("\n").slice(0, maxLines);
1953
- return lines.join("\n");
1954
- }
1955
- function stripMarkdown(text) {
1956
- return text.replace(/^#{1,6}\s+/gm, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/_(.+?)_/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`{1,3}[^`]*`{1,3}/g, (m) => m.replace(/`/g, "")).replace(/^\s*[-*+]\s+/gm, " - ").replace(/^\s*\d+\.\s+/gm, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "[$1]").replace(/^>\s+/gm, " ").replace(/---+/g, "").replace(/\n{3,}/g, "\n\n").trim();
1957
- }
1958
- function formatBody(body, maxLines) {
1959
- const plain = stripMarkdown(body);
1960
- const lines = plain.split("\n");
1961
- const truncated = lines.slice(0, maxLines).join("\n");
1962
- return { text: truncated, remaining: Math.max(0, lines.length - maxLines) };
1963
- }
1964
- function countSlackLinks(body) {
1965
- if (!body) return 0;
1966
- return (body.match(SLACK_URL_RE) ?? []).length;
1967
- }
1968
- function BodySection({
1969
- body,
1970
- issueNumber
1971
- }) {
1972
- const { text, remaining } = formatBody(body, 15);
1973
- return /* @__PURE__ */ jsxs5(Fragment, { children: [
1974
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
1975
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "--- Description ---" }),
1976
- /* @__PURE__ */ jsx5(Text5, { wrap: "wrap", children: text }),
1977
- remaining > 0 ? /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1978
- "... (",
1979
- remaining,
1980
- " more lines \u2014 gh issue view ",
1981
- issueNumber,
1982
- " for full)"
1983
- ] }) : null
1984
- ] });
1985
- }
1986
- function DetailPanel({ issue, task: task2, width }) {
1987
- if (!(issue || task2)) {
1988
- return /* @__PURE__ */ jsx5(
1989
- Box5,
1990
- {
1991
- width,
1992
- borderStyle: "single",
1993
- borderColor: "gray",
1994
- flexDirection: "column",
1995
- paddingX: 1,
1996
- children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "No item selected" })
1997
- }
1998
- );
1999
- }
2000
- if (issue) {
2001
- return /* @__PURE__ */ jsxs5(
2002
- Box5,
2003
- {
2004
- width,
2005
- borderStyle: "single",
2006
- borderColor: "cyan",
2007
- flexDirection: "column",
2008
- paddingX: 1,
2009
- children: [
2010
- /* @__PURE__ */ jsxs5(Text5, { color: "cyan", bold: true, children: [
2011
- "#",
2012
- issue.number,
2013
- " ",
2014
- issue.title
2015
- ] }),
2016
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2017
- /* @__PURE__ */ jsxs5(Box5, { children: [
2018
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "State: " }),
2019
- /* @__PURE__ */ jsx5(Text5, { color: issue.state === "open" ? "green" : "red", children: issue.state })
2020
- ] }),
2021
- (issue.assignees ?? []).length > 0 ? /* @__PURE__ */ jsxs5(Box5, { children: [
2022
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Assignees: " }),
2023
- /* @__PURE__ */ jsx5(Text5, { children: (issue.assignees ?? []).map((a) => a.login).join(", ") })
2024
- ] }) : null,
2025
- issue.labels.length > 0 ? /* @__PURE__ */ jsxs5(Box5, { children: [
2026
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Labels: " }),
2027
- /* @__PURE__ */ jsx5(Text5, { children: issue.labels.map((l) => l.name).join(", ") })
2028
- ] }) : null,
2029
- issue.projectStatus ? /* @__PURE__ */ jsxs5(Box5, { children: [
2030
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Status: " }),
2031
- /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: issue.projectStatus })
2032
- ] }) : null,
2033
- issue.targetDate ? /* @__PURE__ */ jsxs5(Box5, { children: [
2034
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Target: " }),
2035
- /* @__PURE__ */ jsx5(Text5, { children: issue.targetDate })
2036
- ] }) : null,
2037
- /* @__PURE__ */ jsxs5(Box5, { children: [
2038
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Updated: " }),
2039
- /* @__PURE__ */ jsx5(Text5, { children: new Date(issue.updatedAt).toLocaleString() })
2040
- ] }),
2041
- issue.slackThreadUrl ? /* @__PURE__ */ jsxs5(Box5, { children: [
2042
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Slack: " }),
2043
- /* @__PURE__ */ jsx5(Text5, { color: "blue", children: countSlackLinks(issue.body) > 1 ? `${countSlackLinks(issue.body)} links (s opens first)` : "thread (s to open)" })
2044
- ] }) : null,
2045
- issue.body ? /* @__PURE__ */ jsx5(BodySection, { body: issue.body, issueNumber: issue.number }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
2046
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2047
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "(no description)" })
2048
- ] }),
2049
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2050
- /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: issue.url })
2051
- ]
2052
- }
2053
- );
2054
- }
2055
- const t = task2;
2056
- return /* @__PURE__ */ jsxs5(
2057
- Box5,
2058
- {
2059
- width,
2060
- borderStyle: "single",
2061
- borderColor: "yellow",
2062
- flexDirection: "column",
2063
- paddingX: 1,
2064
- children: [
2065
- /* @__PURE__ */ jsx5(Text5, { color: "yellow", bold: true, children: t.title }),
2066
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2067
- /* @__PURE__ */ jsxs5(Box5, { children: [
2068
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Priority: " }),
2069
- /* @__PURE__ */ jsx5(Text5, { children: PRIORITY_LABELS2[t.priority] ?? "None" })
2070
- ] }),
2071
- t.dueDate ? /* @__PURE__ */ jsxs5(Box5, { children: [
2072
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Due: " }),
2073
- /* @__PURE__ */ jsx5(Text5, { children: new Date(t.dueDate).toLocaleDateString() })
2074
- ] }) : null,
2075
- (t.tags ?? []).length > 0 ? /* @__PURE__ */ jsxs5(Box5, { children: [
2076
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Tags: " }),
2077
- /* @__PURE__ */ jsx5(Text5, { children: t.tags.join(", ") })
2078
- ] }) : null,
2079
- t.content ? /* @__PURE__ */ jsxs5(Fragment, { children: [
2080
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2081
- /* @__PURE__ */ jsx5(Text5, { children: truncateLines(t.content, 8) })
2082
- ] }) : null,
2083
- (t.items ?? []).length > 0 ? /* @__PURE__ */ jsxs5(Fragment, { children: [
2084
- /* @__PURE__ */ jsx5(Text5, { children: "" }),
2085
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Checklist:" }),
2086
- t.items.slice(0, 5).map((item) => /* @__PURE__ */ jsxs5(Text5, { children: [
2087
- item.status === 2 ? "\u2611" : "\u2610",
2088
- " ",
2089
- item.title
2090
- ] }, item.id)),
2091
- t.items.length > 5 ? /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
2092
- "...and ",
2093
- t.items.length - 5,
2094
- " more"
2095
- ] }) : null
2096
- ] }) : null
2097
- ]
2098
- }
2099
- );
2100
- }
2101
- var SLACK_URL_RE, PRIORITY_LABELS2;
2102
- var init_detail_panel = __esm({
2103
- "src/board/components/detail-panel.tsx"() {
2104
- "use strict";
2105
- init_types();
2106
- SLACK_URL_RE = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/gi;
2107
- PRIORITY_LABELS2 = {
2108
- [5 /* High */]: "High",
2109
- [3 /* Medium */]: "Medium",
2110
- [1 /* Low */]: "Low",
2111
- [0 /* None */]: "None"
2112
- };
2835
+ init_label_picker();
2113
2836
  }
2114
2837
  });
2115
2838
 
2116
2839
  // src/board/components/focus-mode.tsx
2117
- import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
2118
- import { useCallback as useCallback7, useEffect as useEffect2, useRef as useRef6, useState as useState7 } from "react";
2119
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2840
+ import { Box as Box7, Text as Text7, useInput as useInput7 } from "ink";
2841
+ import { useCallback as useCallback8, useEffect as useEffect4, useRef as useRef8, useState as useState8 } from "react";
2842
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2120
2843
  function formatTime(secs) {
2121
2844
  const m = Math.floor(secs / 60);
2122
2845
  const s = secs % 60;
2123
2846
  return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
2124
2847
  }
2125
2848
  function FocusMode({ label, durationSec, onExit, onEndAction }) {
2126
- const [remaining, setRemaining] = useState7(durationSec);
2127
- const [timerDone, setTimerDone] = useState7(false);
2128
- const bellSentRef = useRef6(false);
2129
- useEffect2(() => {
2849
+ const [remaining, setRemaining] = useState8(durationSec);
2850
+ const [timerDone, setTimerDone] = useState8(false);
2851
+ const bellSentRef = useRef8(false);
2852
+ useEffect4(() => {
2130
2853
  if (timerDone) return;
2131
2854
  const interval = setInterval(() => {
2132
2855
  setRemaining((prev) => {
@@ -2140,13 +2863,13 @@ function FocusMode({ label, durationSec, onExit, onEndAction }) {
2140
2863
  }, 1e3);
2141
2864
  return () => clearInterval(interval);
2142
2865
  }, [timerDone]);
2143
- useEffect2(() => {
2866
+ useEffect4(() => {
2144
2867
  if (timerDone && !bellSentRef.current) {
2145
2868
  bellSentRef.current = true;
2146
2869
  process.stdout.write("\x07");
2147
2870
  }
2148
2871
  }, [timerDone]);
2149
- const handleInput = useCallback7(
2872
+ const handleInput = useCallback8(
2150
2873
  (input2, key) => {
2151
2874
  if (key.escape) {
2152
2875
  if (timerDone) {
@@ -2171,25 +2894,25 @@ function FocusMode({ label, durationSec, onExit, onEndAction }) {
2171
2894
  },
2172
2895
  [timerDone, onExit, onEndAction]
2173
2896
  );
2174
- useInput5(handleInput);
2897
+ useInput7(handleInput);
2175
2898
  if (timerDone) {
2176
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
2177
- /* @__PURE__ */ jsxs6(Box6, { children: [
2178
- /* @__PURE__ */ jsx6(Text6, { color: "green", bold: true, children: "Focus complete!" }),
2179
- /* @__PURE__ */ jsxs6(Text6, { color: "gray", children: [
2899
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
2900
+ /* @__PURE__ */ jsxs7(Box7, { children: [
2901
+ /* @__PURE__ */ jsx7(Text7, { color: "green", bold: true, children: "Focus complete!" }),
2902
+ /* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
2180
2903
  " ",
2181
2904
  label
2182
2905
  ] })
2183
2906
  ] }),
2184
- /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
2185
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "[c]" }),
2186
- /* @__PURE__ */ jsx6(Text6, { children: " Continue " }),
2187
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "[b]" }),
2188
- /* @__PURE__ */ jsx6(Text6, { children: " Break " }),
2189
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "[d]" }),
2190
- /* @__PURE__ */ jsx6(Text6, { children: " Done " }),
2191
- /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "[Esc]" }),
2192
- /* @__PURE__ */ jsx6(Text6, { children: " Exit" })
2907
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
2908
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "[c]" }),
2909
+ /* @__PURE__ */ jsx7(Text7, { children: " Continue " }),
2910
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "[b]" }),
2911
+ /* @__PURE__ */ jsx7(Text7, { children: " Break " }),
2912
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "[d]" }),
2913
+ /* @__PURE__ */ jsx7(Text7, { children: " Done " }),
2914
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "[Esc]" }),
2915
+ /* @__PURE__ */ jsx7(Text7, { children: " Exit" })
2193
2916
  ] })
2194
2917
  ] });
2195
2918
  }
@@ -2197,21 +2920,21 @@ function FocusMode({ label, durationSec, onExit, onEndAction }) {
2197
2920
  const barWidth = 20;
2198
2921
  const filled = Math.round(progress * barWidth);
2199
2922
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
2200
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
2201
- /* @__PURE__ */ jsxs6(Box6, { children: [
2202
- /* @__PURE__ */ jsxs6(Text6, { color: "magenta", bold: true, children: [
2923
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
2924
+ /* @__PURE__ */ jsxs7(Box7, { children: [
2925
+ /* @__PURE__ */ jsxs7(Text7, { color: "magenta", bold: true, children: [
2203
2926
  "Focus:",
2204
2927
  " "
2205
2928
  ] }),
2206
- /* @__PURE__ */ jsx6(Text6, { children: label })
2929
+ /* @__PURE__ */ jsx7(Text7, { children: label })
2207
2930
  ] }),
2208
- /* @__PURE__ */ jsxs6(Box6, { children: [
2209
- /* @__PURE__ */ jsx6(Text6, { color: "magenta", children: bar }),
2210
- /* @__PURE__ */ jsx6(Text6, { children: " " }),
2211
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: formatTime(remaining) }),
2212
- /* @__PURE__ */ jsx6(Text6, { color: "gray", children: " remaining" })
2931
+ /* @__PURE__ */ jsxs7(Box7, { children: [
2932
+ /* @__PURE__ */ jsx7(Text7, { color: "magenta", children: bar }),
2933
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
2934
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: formatTime(remaining) }),
2935
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", children: " remaining" })
2213
2936
  ] }),
2214
- /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: "Esc to exit focus" })
2937
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: "Esc to exit focus" })
2215
2938
  ] });
2216
2939
  }
2217
2940
  var init_focus_mode = __esm({
@@ -2221,29 +2944,29 @@ var init_focus_mode = __esm({
2221
2944
  });
2222
2945
 
2223
2946
  // src/board/components/help-overlay.tsx
2224
- import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
2225
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2947
+ import { Box as Box8, Text as Text8, useInput as useInput8 } from "ink";
2948
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2226
2949
  function HelpOverlay({ currentMode, onClose }) {
2227
- useInput6((_input, key) => {
2950
+ useInput8((_input, key) => {
2228
2951
  if (key.escape) onClose();
2229
2952
  });
2230
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2231
- /* @__PURE__ */ jsxs7(Box7, { justifyContent: "space-between", children: [
2232
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", bold: true, children: "Keyboard Shortcuts" }),
2233
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2953
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2954
+ /* @__PURE__ */ jsxs8(Box8, { justifyContent: "space-between", children: [
2955
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", bold: true, children: "Keyboard Shortcuts" }),
2956
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2234
2957
  "mode: ",
2235
2958
  currentMode
2236
2959
  ] })
2237
2960
  ] }),
2238
- /* @__PURE__ */ jsx7(Text7, { children: " " }),
2239
- SHORTCUTS.map((group) => /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
2240
- /* @__PURE__ */ jsx7(Text7, { color: "yellow", bold: true, children: group.category }),
2241
- group.items.map((item) => /* @__PURE__ */ jsxs7(Box7, { children: [
2242
- /* @__PURE__ */ jsx7(Box7, { width: 16, children: /* @__PURE__ */ jsx7(Text7, { color: "green", children: item.key }) }),
2243
- /* @__PURE__ */ jsx7(Text7, { children: item.desc })
2961
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
2962
+ SHORTCUTS.map((group) => /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, children: [
2963
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", bold: true, children: group.category }),
2964
+ group.items.map((item) => /* @__PURE__ */ jsxs8(Box8, { children: [
2965
+ /* @__PURE__ */ jsx8(Box8, { width: 16, children: /* @__PURE__ */ jsx8(Text8, { color: "green", children: item.key }) }),
2966
+ /* @__PURE__ */ jsx8(Text8, { children: item.desc })
2244
2967
  ] }, item.key))
2245
2968
  ] }, group.category)),
2246
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press ? or Esc to close" })
2969
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Press ? or Esc to close" })
2247
2970
  ] });
2248
2971
  }
2249
2972
  var SHORTCUTS;
@@ -2271,31 +2994,447 @@ var init_help_overlay = __esm({
2271
2994
  ]
2272
2995
  },
2273
2996
  {
2274
- category: "Actions",
2275
- items: [
2276
- { key: "p", desc: "Pick issue (assign + TickTick)" },
2277
- { key: "a", desc: "Assign to self" },
2278
- { key: "u", desc: "Unassign self" },
2279
- { key: "c", desc: "Comment on issue" },
2280
- { key: "m", desc: "Move status" },
2281
- { key: "s", desc: "Open Slack thread" },
2282
- { key: "n", desc: "Create new issue" }
2283
- ]
2284
- },
2997
+ category: "Actions",
2998
+ items: [
2999
+ { key: "p", desc: "Pick issue (assign + TickTick)" },
3000
+ { key: "a", desc: "Assign to self" },
3001
+ { key: "u", desc: "Unassign self" },
3002
+ { key: "c", desc: "Comment on issue" },
3003
+ { key: "m", desc: "Move status" },
3004
+ { key: "s", desc: "Open Slack thread" },
3005
+ { key: "y", desc: "Copy issue link to clipboard" },
3006
+ { key: "n", desc: "Create new issue" }
3007
+ ]
3008
+ },
3009
+ {
3010
+ category: "Board",
3011
+ items: [
3012
+ { key: "r", desc: "Refresh data" },
3013
+ { key: "q", desc: "Quit" }
3014
+ ]
3015
+ }
3016
+ ];
3017
+ }
3018
+ });
3019
+
3020
+ // src/board/components/nl-create-overlay.tsx
3021
+ import { Spinner as Spinner2, TextInput as TextInput3 } from "@inkjs/ui";
3022
+ import { Box as Box9, Text as Text9, useInput as useInput9 } from "ink";
3023
+ import { useCallback as useCallback9, useEffect as useEffect5, useRef as useRef9, useState as useState9 } from "react";
3024
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
3025
+ function NlCreateOverlay({
3026
+ repos,
3027
+ defaultRepoName,
3028
+ labelCache,
3029
+ onSubmit,
3030
+ onCancel,
3031
+ onLlmFallback
3032
+ }) {
3033
+ const [, setInput] = useState9("");
3034
+ const [isParsing, setIsParsing] = useState9(false);
3035
+ const [parsed, setParsed] = useState9(null);
3036
+ const [parseError, setParseError] = useState9(null);
3037
+ const [createError, setCreateError] = useState9(null);
3038
+ const submittedRef = useRef9(false);
3039
+ const parseParamsRef = useRef9(null);
3040
+ const defaultRepoIdx = defaultRepoName ? Math.max(
3041
+ 0,
3042
+ repos.findIndex((r) => r.name === defaultRepoName)
3043
+ ) : 0;
3044
+ const [repoIdx, setRepoIdx] = useState9(defaultRepoIdx);
3045
+ const selectedRepo = repos[repoIdx];
3046
+ useInput9((inputChar, key) => {
3047
+ if (isParsing) return;
3048
+ if (key.escape) {
3049
+ onCancel();
3050
+ return;
3051
+ }
3052
+ if (parsed) {
3053
+ if (key.return) {
3054
+ if (submittedRef.current) return;
3055
+ submittedRef.current = true;
3056
+ if (!selectedRepo) return;
3057
+ setCreateError(null);
3058
+ const labels = buildLabelList(parsed);
3059
+ onSubmit(selectedRepo.name, parsed.title, labels.length > 0 ? labels : void 0);
3060
+ return;
3061
+ }
3062
+ if (inputChar === "r") {
3063
+ setRepoIdx((i) => (i + 1) % repos.length);
3064
+ return;
3065
+ }
3066
+ }
3067
+ });
3068
+ const handleInputSubmit = useCallback9(
3069
+ (text) => {
3070
+ const trimmed = text.trim();
3071
+ if (!trimmed) return;
3072
+ const validLabels = selectedRepo ? (labelCache[selectedRepo.name] ?? []).map((l) => l.name) : [];
3073
+ parseParamsRef.current = { input: trimmed, validLabels };
3074
+ setInput(trimmed);
3075
+ setParseError(null);
3076
+ setIsParsing(true);
3077
+ },
3078
+ [selectedRepo, labelCache]
3079
+ );
3080
+ useEffect5(() => {
3081
+ if (!(isParsing && parseParamsRef.current)) return;
3082
+ const { input: capturedInput, validLabels } = parseParamsRef.current;
3083
+ extractIssueFields(capturedInput, {
3084
+ validLabels,
3085
+ onLlmFallback
3086
+ }).then((result) => {
3087
+ if (!result) {
3088
+ setParseError("Title is required");
3089
+ setIsParsing(false);
3090
+ return;
3091
+ }
3092
+ const filteredLabels = validLabels.length > 0 ? result.labels.filter((l) => validLabels.includes(l)) : result.labels;
3093
+ setParsed({ ...result, labels: filteredLabels });
3094
+ setIsParsing(false);
3095
+ }).catch(() => {
3096
+ setParseError("Parsing failed \u2014 please try again");
3097
+ setIsParsing(false);
3098
+ });
3099
+ }, [isParsing, onLlmFallback]);
3100
+ if (isParsing) {
3101
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
3102
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: "\u2728 Creating Issue" }),
3103
+ /* @__PURE__ */ jsx9(Spinner2, { label: "Parsing..." })
3104
+ ] });
3105
+ }
3106
+ if (parsed) {
3107
+ const labels = buildLabelList(parsed);
3108
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
3109
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: "\u2728 Creating Issue" }),
3110
+ /* @__PURE__ */ jsxs9(Box9, { children: [
3111
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Repo: " }),
3112
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: selectedRepo?.shortName ?? "(none)" }),
3113
+ repos.length > 1 ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " r:cycle" }) : null
3114
+ ] }),
3115
+ /* @__PURE__ */ jsxs9(Box9, { children: [
3116
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Title: " }),
3117
+ /* @__PURE__ */ jsx9(Text9, { children: parsed.title })
3118
+ ] }),
3119
+ labels.length > 0 ? /* @__PURE__ */ jsxs9(Box9, { children: [
3120
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Labels: " }),
3121
+ /* @__PURE__ */ jsx9(Text9, { children: labels.join(", ") })
3122
+ ] }) : null,
3123
+ parsed.assignee ? /* @__PURE__ */ jsxs9(Box9, { children: [
3124
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Assignee: " }),
3125
+ /* @__PURE__ */ jsxs9(Text9, { children: [
3126
+ "@",
3127
+ parsed.assignee
3128
+ ] })
3129
+ ] }) : null,
3130
+ parsed.dueDate ? /* @__PURE__ */ jsxs9(Box9, { children: [
3131
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Due: " }),
3132
+ /* @__PURE__ */ jsx9(Text9, { children: formatDue(parsed.dueDate) })
3133
+ ] }) : null,
3134
+ 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,
3135
+ createError ? /* @__PURE__ */ jsx9(Text9, { color: "red", children: createError }) : null,
3136
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Enter:create Esc:cancel" })
3137
+ ] });
3138
+ }
3139
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
3140
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: "\u2728 What do you need to do?" }),
3141
+ /* @__PURE__ */ jsxs9(Box9, { children: [
3142
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "> " }),
3143
+ /* @__PURE__ */ jsx9(
3144
+ TextInput3,
3145
+ {
3146
+ placeholder: "fix login bug #bug #priority:high @me due friday",
3147
+ onChange: setInput,
3148
+ onSubmit: handleInputSubmit
3149
+ }
3150
+ )
3151
+ ] }),
3152
+ parseError ? /* @__PURE__ */ jsx9(Text9, { color: "red", children: parseError }) : null,
3153
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Tip: #label @user due <date> Enter:parse Esc:cancel" })
3154
+ ] });
3155
+ }
3156
+ function buildLabelList(parsed) {
3157
+ const labels = [...parsed.labels];
3158
+ if (parsed.dueDate) {
3159
+ labels.push(`due:${parsed.dueDate}`);
3160
+ }
3161
+ return labels;
3162
+ }
3163
+ function hasDueLabelInCache(labelCache, repoName) {
3164
+ return (labelCache[repoName] ?? []).some((l) => l.name.startsWith("due:"));
3165
+ }
3166
+ function formatDue(dueDate) {
3167
+ const d = /* @__PURE__ */ new Date(`${dueDate}T12:00:00`);
3168
+ const human = d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
3169
+ return `${human} (label: due:${dueDate})`;
3170
+ }
3171
+ var init_nl_create_overlay = __esm({
3172
+ "src/board/components/nl-create-overlay.tsx"() {
3173
+ "use strict";
3174
+ init_ai();
3175
+ }
3176
+ });
3177
+
3178
+ // src/board/components/search-bar.tsx
3179
+ import { TextInput as TextInput4 } from "@inkjs/ui";
3180
+ import { Box as Box10, Text as Text10 } from "ink";
3181
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
3182
+ function SearchBar({ defaultValue, onChange, onSubmit }) {
3183
+ return /* @__PURE__ */ jsxs10(Box10, { children: [
3184
+ /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: "/" }),
3185
+ /* @__PURE__ */ jsx10(
3186
+ TextInput4,
3187
+ {
3188
+ defaultValue,
3189
+ placeholder: "search...",
3190
+ onChange,
3191
+ onSubmit
3192
+ }
3193
+ )
3194
+ ] });
3195
+ }
3196
+ var init_search_bar = __esm({
3197
+ "src/board/components/search-bar.tsx"() {
3198
+ "use strict";
3199
+ }
3200
+ });
3201
+
3202
+ // src/board/components/status-picker.tsx
3203
+ import { Box as Box11, Text as Text11, useInput as useInput10 } from "ink";
3204
+ import { useRef as useRef10, useState as useState10 } from "react";
3205
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3206
+ function isTerminal(name) {
3207
+ return TERMINAL_STATUS_RE2.test(name);
3208
+ }
3209
+ function StatusPicker({
3210
+ options,
3211
+ currentStatus,
3212
+ onSelect,
3213
+ onCancel,
3214
+ showTerminalStatuses = true
3215
+ }) {
3216
+ const [selectedIdx, setSelectedIdx] = useState10(() => {
3217
+ const idx = options.findIndex((o) => o.name === currentStatus);
3218
+ return idx >= 0 ? idx : 0;
3219
+ });
3220
+ const [confirmingTerminal, setConfirmingTerminal] = useState10(false);
3221
+ const submittedRef = useRef10(false);
3222
+ useInput10((input2, key) => {
3223
+ if (confirmingTerminal) {
3224
+ if (input2 === "y" || input2 === "Y") {
3225
+ if (submittedRef.current) return;
3226
+ submittedRef.current = true;
3227
+ const opt = options[selectedIdx];
3228
+ if (opt) onSelect(opt.id);
3229
+ return;
3230
+ }
3231
+ if (input2 === "n" || input2 === "N" || key.escape) {
3232
+ setConfirmingTerminal(false);
3233
+ return;
3234
+ }
3235
+ return;
3236
+ }
3237
+ if (key.escape) return onCancel();
3238
+ if (key.return) {
3239
+ if (submittedRef.current) return;
3240
+ const opt = options[selectedIdx];
3241
+ if (!opt) return;
3242
+ if (isTerminal(opt.name) && showTerminalStatuses) {
3243
+ setConfirmingTerminal(true);
3244
+ return;
3245
+ }
3246
+ submittedRef.current = true;
3247
+ onSelect(opt.id);
3248
+ return;
3249
+ }
3250
+ if (input2 === "j" || key.downArrow) {
3251
+ setSelectedIdx((i) => Math.min(i + 1, options.length - 1));
3252
+ }
3253
+ if (input2 === "k" || key.upArrow) {
3254
+ setSelectedIdx((i) => Math.max(i - 1, 0));
3255
+ }
3256
+ });
3257
+ if (confirmingTerminal) {
3258
+ const opt = options[selectedIdx];
3259
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
3260
+ /* @__PURE__ */ jsxs11(Text11, { color: "yellow", bold: true, children: [
3261
+ "Mark as ",
3262
+ opt?.name,
3263
+ "?"
3264
+ ] }),
3265
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "This will close the issue on GitHub." }),
3266
+ /* @__PURE__ */ jsx11(Text11, { children: "Continue? [y/n]" })
3267
+ ] });
3268
+ }
3269
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
3270
+ /* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: "Move to status:" }),
3271
+ options.map((opt, i) => {
3272
+ const isCurrent = opt.name === currentStatus;
3273
+ const isSelected = i === selectedIdx;
3274
+ const terminal = isTerminal(opt.name) && showTerminalStatuses;
3275
+ const prefix = isSelected ? "> " : " ";
3276
+ const suffix = isCurrent ? " (current)" : terminal ? " (Done)" : "";
3277
+ return /* @__PURE__ */ jsxs11(
3278
+ Text11,
3279
+ {
3280
+ ...isSelected ? { color: "cyan" } : terminal ? { color: "yellow" } : {},
3281
+ dimColor: isCurrent,
3282
+ children: [
3283
+ prefix,
3284
+ opt.name,
3285
+ suffix
3286
+ ]
3287
+ },
3288
+ opt.id
3289
+ );
3290
+ }),
3291
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "j/k:navigate Enter:select Esc:cancel" })
3292
+ ] });
3293
+ }
3294
+ var TERMINAL_STATUS_RE2;
3295
+ var init_status_picker = __esm({
3296
+ "src/board/components/status-picker.tsx"() {
3297
+ "use strict";
3298
+ TERMINAL_STATUS_RE2 = /^(done|shipped|won't|wont|closed|complete|completed)$/i;
3299
+ }
3300
+ });
3301
+
3302
+ // src/board/components/overlay-renderer.tsx
3303
+ import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
3304
+ function OverlayRenderer({
3305
+ uiState,
3306
+ config: config2,
3307
+ selectedRepoStatusOptions,
3308
+ currentStatus,
3309
+ onStatusSelect,
3310
+ onExitOverlay,
3311
+ defaultRepo,
3312
+ onCreateIssue,
3313
+ onConfirmPick,
3314
+ onCancelPick,
3315
+ multiSelectCount,
3316
+ multiSelectType,
3317
+ onBulkAction,
3318
+ focusLabel,
3319
+ focusKey,
3320
+ onFocusExit,
3321
+ onFocusEndAction,
3322
+ searchQuery,
3323
+ onSearchChange,
3324
+ onSearchSubmit,
3325
+ selectedIssue,
3326
+ onComment,
3327
+ onPauseRefresh,
3328
+ onResumeRefresh,
3329
+ onToggleHelp,
3330
+ labelCache,
3331
+ onLabelConfirm,
3332
+ onLabelError,
3333
+ onLlmFallback
3334
+ }) {
3335
+ const { mode, helpVisible } = uiState;
3336
+ return /* @__PURE__ */ jsxs12(Fragment2, { children: [
3337
+ helpVisible ? /* @__PURE__ */ jsx12(HelpOverlay, { currentMode: mode, onClose: onToggleHelp }) : null,
3338
+ mode === "overlay:status" && selectedRepoStatusOptions.length > 0 ? /* @__PURE__ */ jsx12(
3339
+ StatusPicker,
3340
+ {
3341
+ options: selectedRepoStatusOptions,
3342
+ currentStatus,
3343
+ onSelect: onStatusSelect,
3344
+ onCancel: onExitOverlay
3345
+ }
3346
+ ) : null,
3347
+ mode === "overlay:create" ? /* @__PURE__ */ jsx12(
3348
+ CreateIssueForm,
3349
+ {
3350
+ repos: config2.repos,
3351
+ defaultRepo,
3352
+ onSubmit: onCreateIssue,
3353
+ onCancel: onExitOverlay,
3354
+ labelCache
3355
+ }
3356
+ ) : null,
3357
+ mode === "overlay:confirmPick" ? /* @__PURE__ */ jsx12(
3358
+ ConfirmPrompt,
3359
+ {
3360
+ message: "Pick this issue?",
3361
+ onConfirm: onConfirmPick,
3362
+ onCancel: onCancelPick
3363
+ }
3364
+ ) : null,
3365
+ mode === "overlay:bulkAction" ? /* @__PURE__ */ jsx12(
3366
+ BulkActionMenu,
3367
+ {
3368
+ count: multiSelectCount,
3369
+ selectionType: multiSelectType,
3370
+ onSelect: onBulkAction,
3371
+ onCancel: onExitOverlay
3372
+ }
3373
+ ) : null,
3374
+ mode === "focus" && focusLabel ? /* @__PURE__ */ jsx12(
3375
+ FocusMode,
3376
+ {
3377
+ label: focusLabel,
3378
+ durationSec: config2.board.focusDuration ?? 1500,
3379
+ onExit: onFocusExit,
3380
+ onEndAction: onFocusEndAction
3381
+ },
3382
+ focusKey
3383
+ ) : null,
3384
+ mode === "overlay:label" && selectedIssue && defaultRepo ? /* @__PURE__ */ jsx12(
3385
+ LabelPicker,
3386
+ {
3387
+ repo: defaultRepo,
3388
+ currentLabels: selectedIssue.labels.map((l) => l.name),
3389
+ labelCache,
3390
+ onConfirm: onLabelConfirm,
3391
+ onCancel: onExitOverlay,
3392
+ onError: onLabelError
3393
+ }
3394
+ ) : null,
3395
+ mode === "search" ? /* @__PURE__ */ jsx12(SearchBar, { defaultValue: searchQuery, onChange: onSearchChange, onSubmit: onSearchSubmit }) : null,
3396
+ mode === "overlay:comment" && selectedIssue ? /* @__PURE__ */ jsx12(
3397
+ CommentInput,
2285
3398
  {
2286
- category: "Board",
2287
- items: [
2288
- { key: "r", desc: "Refresh data" },
2289
- { key: "q", desc: "Quit" }
2290
- ]
3399
+ issueNumber: selectedIssue.number,
3400
+ onSubmit: onComment,
3401
+ onCancel: onExitOverlay,
3402
+ onPauseRefresh,
3403
+ onResumeRefresh
2291
3404
  }
2292
- ];
3405
+ ) : null,
3406
+ mode === "overlay:createNl" ? /* @__PURE__ */ jsx12(
3407
+ NlCreateOverlay,
3408
+ {
3409
+ repos: config2.repos,
3410
+ defaultRepoName: defaultRepo,
3411
+ labelCache,
3412
+ onSubmit: onCreateIssue,
3413
+ onCancel: onExitOverlay,
3414
+ onLlmFallback
3415
+ }
3416
+ ) : null
3417
+ ] });
3418
+ }
3419
+ var init_overlay_renderer = __esm({
3420
+ "src/board/components/overlay-renderer.tsx"() {
3421
+ "use strict";
3422
+ init_bulk_action_menu();
3423
+ init_comment_input();
3424
+ init_confirm_prompt();
3425
+ init_create_issue_form();
3426
+ init_focus_mode();
3427
+ init_help_overlay();
3428
+ init_label_picker();
3429
+ init_nl_create_overlay();
3430
+ init_search_bar();
3431
+ init_status_picker();
2293
3432
  }
2294
3433
  });
2295
3434
 
2296
3435
  // src/board/components/issue-row.tsx
2297
- import { Box as Box8, Text as Text8 } from "ink";
2298
- import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
3436
+ import { Box as Box12, Text as Text12 } from "ink";
3437
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2299
3438
  function truncate(s, max) {
2300
3439
  return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
2301
3440
  }
@@ -2336,30 +3475,30 @@ function IssueRow({ issue, selfLogin, isSelected }) {
2336
3475
  const labels = (issue.labels ?? []).slice(0, 2);
2337
3476
  const target = formatTargetDate(issue.targetDate);
2338
3477
  const titleStr = truncate(issue.title, 42).padEnd(42);
2339
- return /* @__PURE__ */ jsxs8(Box8, { children: [
2340
- isSelected ? /* @__PURE__ */ jsx8(Text8, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx8(Text8, { children: " " }),
2341
- /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
3478
+ return /* @__PURE__ */ jsxs13(Box12, { children: [
3479
+ isSelected ? /* @__PURE__ */ jsx13(Text12, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx13(Text12, { children: " " }),
3480
+ /* @__PURE__ */ jsxs13(Text12, { color: "cyan", children: [
2342
3481
  "#",
2343
3482
  String(issue.number).padEnd(5)
2344
3483
  ] }),
2345
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2346
- isSelected ? /* @__PURE__ */ jsx8(Text8, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx8(Text8, { children: titleStr }),
2347
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2348
- /* @__PURE__ */ jsx8(Box8, { width: LABEL_COL_WIDTH, children: labels.map((l, i) => /* @__PURE__ */ jsxs8(Text8, { children: [
3484
+ /* @__PURE__ */ jsx13(Text12, { children: " " }),
3485
+ isSelected ? /* @__PURE__ */ jsx13(Text12, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx13(Text12, { children: titleStr }),
3486
+ /* @__PURE__ */ jsx13(Text12, { children: " " }),
3487
+ /* @__PURE__ */ jsx13(Box12, { width: LABEL_COL_WIDTH, children: labels.map((l, i) => /* @__PURE__ */ jsxs13(Text12, { children: [
2349
3488
  i > 0 ? " " : "",
2350
- /* @__PURE__ */ jsxs8(Text8, { color: labelColor(l.name), children: [
3489
+ /* @__PURE__ */ jsxs13(Text12, { color: labelColor(l.name), children: [
2351
3490
  "[",
2352
3491
  truncate(l.name, 12),
2353
3492
  "]"
2354
3493
  ] })
2355
3494
  ] }, l.name)) }),
2356
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2357
- /* @__PURE__ */ jsx8(Text8, { color: assigneeColor, children: assigneeText.padEnd(14) }),
2358
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2359
- /* @__PURE__ */ jsx8(Text8, { color: "gray", children: timeAgo(issue.updatedAt).padStart(4) }),
2360
- target.text ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
2361
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
2362
- /* @__PURE__ */ jsx8(Text8, { color: target.color, children: target.text })
3495
+ /* @__PURE__ */ jsx13(Text12, { children: " " }),
3496
+ /* @__PURE__ */ jsx13(Text12, { color: assigneeColor, children: assigneeText.padEnd(14) }),
3497
+ /* @__PURE__ */ jsx13(Text12, { children: " " }),
3498
+ /* @__PURE__ */ jsx13(Text12, { color: "gray", children: timeAgo(issue.updatedAt).padStart(4) }),
3499
+ target.text ? /* @__PURE__ */ jsxs13(Fragment3, { children: [
3500
+ /* @__PURE__ */ jsx13(Text12, { children: " " }),
3501
+ /* @__PURE__ */ jsx13(Text12, { color: target.color, children: target.text })
2363
3502
  ] }) : null
2364
3503
  ] });
2365
3504
  }
@@ -2382,90 +3521,13 @@ var init_issue_row = __esm({
2382
3521
  }
2383
3522
  });
2384
3523
 
2385
- // src/board/components/search-bar.tsx
2386
- import { TextInput as TextInput3 } from "@inkjs/ui";
2387
- import { Box as Box9, Text as Text9 } from "ink";
2388
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2389
- function SearchBar({ defaultValue, onChange, onSubmit }) {
2390
- return /* @__PURE__ */ jsxs9(Box9, { children: [
2391
- /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "/" }),
2392
- /* @__PURE__ */ jsx9(
2393
- TextInput3,
2394
- {
2395
- defaultValue,
2396
- placeholder: "search...",
2397
- onChange,
2398
- onSubmit
2399
- }
2400
- )
2401
- ] });
2402
- }
2403
- var init_search_bar = __esm({
2404
- "src/board/components/search-bar.tsx"() {
2405
- "use strict";
2406
- }
2407
- });
2408
-
2409
- // src/board/components/status-picker.tsx
2410
- import { Box as Box10, Text as Text10, useInput as useInput7 } from "ink";
2411
- import { useState as useState8 } from "react";
2412
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2413
- function StatusPicker({ options, currentStatus, onSelect, onCancel }) {
2414
- const [selectedIdx, setSelectedIdx] = useState8(() => {
2415
- const idx = options.findIndex((o) => o.name === currentStatus);
2416
- return idx >= 0 ? idx : 0;
2417
- });
2418
- useInput7((input2, key) => {
2419
- if (key.escape) return onCancel();
2420
- if (key.return) {
2421
- const opt = options[selectedIdx];
2422
- if (opt) onSelect(opt.id);
2423
- return;
2424
- }
2425
- if (input2 === "j" || key.downArrow) {
2426
- setSelectedIdx((i) => Math.min(i + 1, options.length - 1));
2427
- }
2428
- if (input2 === "k" || key.upArrow) {
2429
- setSelectedIdx((i) => Math.max(i - 1, 0));
2430
- }
2431
- });
2432
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
2433
- /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: "Move to status:" }),
2434
- options.map((opt, i) => {
2435
- const isCurrent = opt.name === currentStatus;
2436
- const isSelected = i === selectedIdx;
2437
- const prefix = isSelected ? "> " : " ";
2438
- const suffix = isCurrent ? " (current)" : "";
2439
- return /* @__PURE__ */ jsxs10(
2440
- Text10,
2441
- {
2442
- ...isSelected ? { color: "cyan" } : {},
2443
- dimColor: isCurrent,
2444
- children: [
2445
- prefix,
2446
- opt.name,
2447
- suffix
2448
- ]
2449
- },
2450
- opt.id
2451
- );
2452
- }),
2453
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "j/k:navigate Enter:select Esc:cancel" })
2454
- ] });
2455
- }
2456
- var init_status_picker = __esm({
2457
- "src/board/components/status-picker.tsx"() {
2458
- "use strict";
2459
- }
2460
- });
2461
-
2462
3524
  // src/board/components/task-row.tsx
2463
- import { Box as Box11, Text as Text11 } from "ink";
2464
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3525
+ import { Box as Box13, Text as Text13 } from "ink";
3526
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2465
3527
  function truncate2(s, max) {
2466
3528
  return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
2467
3529
  }
2468
- function formatDue(dateStr) {
3530
+ function formatDue2(dateStr) {
2469
3531
  if (!dateStr) return { text: "", color: "gray" };
2470
3532
  const d = new Date(dateStr);
2471
3533
  const days = Math.ceil((d.getTime() - Date.now()) / 864e5);
@@ -2480,15 +3542,15 @@ function formatDue(dateStr) {
2480
3542
  }
2481
3543
  function TaskRow({ task: task2, isSelected }) {
2482
3544
  const pri = PRIORITY_INDICATORS[task2.priority] ?? DEFAULT_PRIORITY;
2483
- const due = formatDue(task2.dueDate);
3545
+ const due = formatDue2(task2.dueDate);
2484
3546
  const titleStr = truncate2(task2.title, 45).padEnd(45);
2485
- return /* @__PURE__ */ jsxs11(Box11, { children: [
2486
- isSelected ? /* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx11(Text11, { children: " " }),
2487
- /* @__PURE__ */ jsx11(Text11, { color: pri.color, children: pri.text }),
2488
- /* @__PURE__ */ jsx11(Text11, { children: " " }),
2489
- isSelected ? /* @__PURE__ */ jsx11(Text11, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx11(Text11, { children: titleStr }),
2490
- /* @__PURE__ */ jsx11(Text11, { children: " " }),
2491
- /* @__PURE__ */ jsx11(Text11, { color: due.color, children: due.text })
3547
+ return /* @__PURE__ */ jsxs14(Box13, { children: [
3548
+ isSelected ? /* @__PURE__ */ jsx14(Text13, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx14(Text13, { children: " " }),
3549
+ /* @__PURE__ */ jsx14(Text13, { color: pri.color, children: pri.text }),
3550
+ /* @__PURE__ */ jsx14(Text13, { children: " " }),
3551
+ isSelected ? /* @__PURE__ */ jsx14(Text13, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx14(Text13, { children: titleStr }),
3552
+ /* @__PURE__ */ jsx14(Text13, { children: " " }),
3553
+ /* @__PURE__ */ jsx14(Text13, { color: due.color, children: due.text })
2492
3554
  ] });
2493
3555
  }
2494
3556
  var PRIORITY_INDICATORS, DEFAULT_PRIORITY;
@@ -2506,23 +3568,128 @@ var init_task_row = __esm({
2506
3568
  }
2507
3569
  });
2508
3570
 
3571
+ // src/board/components/row-renderer.tsx
3572
+ import { Box as Box14, Text as Text14 } from "ink";
3573
+ import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
3574
+ function RowRenderer({ row, selectedId, selfLogin, isMultiSelected }) {
3575
+ switch (row.type) {
3576
+ case "sectionHeader": {
3577
+ const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
3578
+ const isSel = selectedId === row.navId;
3579
+ return /* @__PURE__ */ jsxs15(Box14, { children: [
3580
+ /* @__PURE__ */ jsxs15(Text14, { color: isSel ? "cyan" : "white", bold: true, children: [
3581
+ arrow,
3582
+ " ",
3583
+ row.label
3584
+ ] }),
3585
+ /* @__PURE__ */ jsxs15(Text14, { color: "gray", children: [
3586
+ " ",
3587
+ "(",
3588
+ row.count,
3589
+ " ",
3590
+ row.countLabel,
3591
+ ")"
3592
+ ] })
3593
+ ] });
3594
+ }
3595
+ case "subHeader": {
3596
+ if (row.navId) {
3597
+ const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
3598
+ const isSel = selectedId === row.navId;
3599
+ return /* @__PURE__ */ jsxs15(Box14, { children: [
3600
+ /* @__PURE__ */ jsxs15(Text14, { color: isSel ? "cyan" : "gray", children: [
3601
+ " ",
3602
+ arrow,
3603
+ " ",
3604
+ row.text
3605
+ ] }),
3606
+ /* @__PURE__ */ jsxs15(Text14, { color: "gray", children: [
3607
+ " (",
3608
+ row.count,
3609
+ ")"
3610
+ ] })
3611
+ ] });
3612
+ }
3613
+ return /* @__PURE__ */ jsxs15(Text14, { color: "gray", children: [
3614
+ " ",
3615
+ row.text
3616
+ ] });
3617
+ }
3618
+ case "issue": {
3619
+ const checkbox2 = isMultiSelected != null ? isMultiSelected ? "\u2611 " : "\u2610 " : "";
3620
+ return /* @__PURE__ */ jsxs15(Box14, { children: [
3621
+ checkbox2 ? /* @__PURE__ */ jsx15(Text14, { color: isMultiSelected ? "cyan" : "gray", children: checkbox2 }) : null,
3622
+ /* @__PURE__ */ jsx15(IssueRow, { issue: row.issue, selfLogin, isSelected: selectedId === row.navId })
3623
+ ] });
3624
+ }
3625
+ case "task": {
3626
+ const checkbox2 = isMultiSelected != null ? isMultiSelected ? "\u2611 " : "\u2610 " : "";
3627
+ return /* @__PURE__ */ jsxs15(Box14, { children: [
3628
+ checkbox2 ? /* @__PURE__ */ jsx15(Text14, { color: isMultiSelected ? "cyan" : "gray", children: checkbox2 }) : null,
3629
+ /* @__PURE__ */ jsx15(TaskRow, { task: row.task, isSelected: selectedId === row.navId })
3630
+ ] });
3631
+ }
3632
+ case "activity": {
3633
+ const ago = timeAgo2(row.event.timestamp);
3634
+ return /* @__PURE__ */ jsxs15(Text14, { dimColor: true, children: [
3635
+ " ",
3636
+ ago,
3637
+ ": ",
3638
+ /* @__PURE__ */ jsxs15(Text14, { color: "gray", children: [
3639
+ "@",
3640
+ row.event.actor
3641
+ ] }),
3642
+ " ",
3643
+ row.event.summary,
3644
+ " ",
3645
+ /* @__PURE__ */ jsxs15(Text14, { dimColor: true, children: [
3646
+ "(",
3647
+ row.event.repoShortName,
3648
+ ")"
3649
+ ] })
3650
+ ] });
3651
+ }
3652
+ case "error":
3653
+ return /* @__PURE__ */ jsxs15(Text14, { color: "red", children: [
3654
+ " Error: ",
3655
+ row.text
3656
+ ] });
3657
+ case "gap":
3658
+ return /* @__PURE__ */ jsx15(Text14, { children: "" });
3659
+ }
3660
+ }
3661
+ function timeAgo2(date) {
3662
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
3663
+ if (seconds < 10) return "just now";
3664
+ if (seconds < 60) return `${seconds}s ago`;
3665
+ const minutes = Math.floor(seconds / 60);
3666
+ return `${minutes}m ago`;
3667
+ }
3668
+ var init_row_renderer = __esm({
3669
+ "src/board/components/row-renderer.tsx"() {
3670
+ "use strict";
3671
+ init_issue_row();
3672
+ init_task_row();
3673
+ }
3674
+ });
3675
+
2509
3676
  // src/board/components/toast-container.tsx
2510
- import { Spinner } from "@inkjs/ui";
2511
- import { Box as Box12, Text as Text12 } from "ink";
2512
- import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
3677
+ import { Spinner as Spinner3 } from "@inkjs/ui";
3678
+ import { Box as Box15, Text as Text15 } from "ink";
3679
+ import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
2513
3680
  function ToastContainer({ toasts }) {
2514
3681
  if (toasts.length === 0) return null;
2515
- return /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", children: toasts.map((t) => /* @__PURE__ */ jsx12(Box12, { children: t.type === "loading" ? /* @__PURE__ */ jsxs12(Fragment3, { children: [
2516
- /* @__PURE__ */ jsx12(Spinner, { label: "" }),
2517
- /* @__PURE__ */ jsxs12(Text12, { color: "cyan", children: [
3682
+ return /* @__PURE__ */ jsx16(Box15, { flexDirection: "column", children: toasts.map((t) => /* @__PURE__ */ jsx16(Box15, { children: t.type === "loading" ? /* @__PURE__ */ jsxs16(Fragment4, { children: [
3683
+ /* @__PURE__ */ jsx16(Spinner3, { label: "" }),
3684
+ /* @__PURE__ */ jsxs16(Text15, { color: "cyan", children: [
2518
3685
  " ",
2519
3686
  t.message
2520
3687
  ] })
2521
- ] }) : /* @__PURE__ */ jsxs12(Text12, { color: TYPE_COLORS[t.type], children: [
3688
+ ] }) : /* @__PURE__ */ jsxs16(Text15, { color: TYPE_COLORS[t.type], children: [
2522
3689
  TYPE_PREFIXES[t.type],
2523
3690
  " ",
2524
3691
  t.message,
2525
- t.type === "error" ? /* @__PURE__ */ jsx12(Text12, { color: "gray", children: t.retry ? " [r]etry [d]ismiss" : " [d]ismiss" }) : null
3692
+ t.type === "error" ? /* @__PURE__ */ jsx16(Text15, { color: "gray", children: t.retry ? " [r]etry [d]ismiss" : " [d]ismiss" }) : null
2526
3693
  ] }) }, t.id)) });
2527
3694
  }
2528
3695
  var TYPE_COLORS, TYPE_PREFIXES;
@@ -2544,13 +3711,13 @@ var init_toast_container = __esm({
2544
3711
  });
2545
3712
 
2546
3713
  // src/board/components/dashboard.tsx
2547
- import { execFileSync as execFileSync3 } from "child_process";
2548
- import { Spinner as Spinner2 } from "@inkjs/ui";
2549
- import { Box as Box13, Text as Text13, useApp, useInput as useInput8, useStdout } from "ink";
2550
- import { useCallback as useCallback8, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef7, useState as useState9 } from "react";
2551
- import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
3714
+ import { execFileSync as execFileSync3, spawnSync as spawnSync2 } from "child_process";
3715
+ import { Spinner as Spinner4 } from "@inkjs/ui";
3716
+ import { Box as Box16, Text as Text16, useApp, useStdout } from "ink";
3717
+ import { useCallback as useCallback10, useEffect as useEffect6, useMemo as useMemo2, useRef as useRef11, useState as useState11 } from "react";
3718
+ import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs17 } from "react/jsx-runtime";
2552
3719
  function isTerminalStatus(status) {
2553
- return TERMINAL_STATUS_RE2.test(status);
3720
+ return TERMINAL_STATUS_RE3.test(status);
2554
3721
  }
2555
3722
  function resolveStatusGroups(statusOptions, configuredGroups) {
2556
3723
  if (configuredGroups && configuredGroups.length > 0) {
@@ -2773,7 +3940,7 @@ function buildFlatRows(repos, tasks, activity, isCollapsed) {
2773
3940
  }
2774
3941
  return rows;
2775
3942
  }
2776
- function timeAgo2(date) {
3943
+ function timeAgo3(date) {
2777
3944
  const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
2778
3945
  if (seconds < 10) return "just now";
2779
3946
  if (seconds < 60) return `${seconds}s ago`;
@@ -2799,101 +3966,14 @@ function findSelectedIssueWithRepo(repos, selectedId) {
2799
3966
  if (!selectedId?.startsWith("gh:")) return null;
2800
3967
  for (const rd of repos) {
2801
3968
  for (const issue of rd.issues) {
2802
- if (`gh:${rd.repo.name}:${issue.number}` === selectedId)
2803
- return { issue, repoName: rd.repo.name };
2804
- }
2805
- }
2806
- return null;
2807
- }
2808
- function isHeaderId(id) {
2809
- return id != null && (id.startsWith("header:") || id.startsWith("sub:"));
2810
- }
2811
- function RowRenderer({ row, selectedId, selfLogin, isMultiSelected }) {
2812
- switch (row.type) {
2813
- case "sectionHeader": {
2814
- const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
2815
- const isSel = selectedId === row.navId;
2816
- return /* @__PURE__ */ jsxs13(Box13, { children: [
2817
- /* @__PURE__ */ jsxs13(Text13, { color: isSel ? "cyan" : "white", bold: true, children: [
2818
- arrow,
2819
- " ",
2820
- row.label
2821
- ] }),
2822
- /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2823
- " ",
2824
- "(",
2825
- row.count,
2826
- " ",
2827
- row.countLabel,
2828
- ")"
2829
- ] })
2830
- ] });
2831
- }
2832
- case "subHeader": {
2833
- if (row.navId) {
2834
- const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
2835
- const isSel = selectedId === row.navId;
2836
- return /* @__PURE__ */ jsxs13(Box13, { children: [
2837
- /* @__PURE__ */ jsxs13(Text13, { color: isSel ? "cyan" : "gray", children: [
2838
- " ",
2839
- arrow,
2840
- " ",
2841
- row.text
2842
- ] }),
2843
- /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2844
- " (",
2845
- row.count,
2846
- ")"
2847
- ] })
2848
- ] });
2849
- }
2850
- return /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2851
- " ",
2852
- row.text
2853
- ] });
2854
- }
2855
- case "issue": {
2856
- const checkbox2 = isMultiSelected != null ? isMultiSelected ? "\u2611 " : "\u2610 " : "";
2857
- return /* @__PURE__ */ jsxs13(Box13, { children: [
2858
- checkbox2 ? /* @__PURE__ */ jsx13(Text13, { color: isMultiSelected ? "cyan" : "gray", children: checkbox2 }) : null,
2859
- /* @__PURE__ */ jsx13(IssueRow, { issue: row.issue, selfLogin, isSelected: selectedId === row.navId })
2860
- ] });
2861
- }
2862
- case "task": {
2863
- const checkbox2 = isMultiSelected != null ? isMultiSelected ? "\u2611 " : "\u2610 " : "";
2864
- return /* @__PURE__ */ jsxs13(Box13, { children: [
2865
- checkbox2 ? /* @__PURE__ */ jsx13(Text13, { color: isMultiSelected ? "cyan" : "gray", children: checkbox2 }) : null,
2866
- /* @__PURE__ */ jsx13(TaskRow, { task: row.task, isSelected: selectedId === row.navId })
2867
- ] });
2868
- }
2869
- case "activity": {
2870
- const ago = timeAgo2(row.event.timestamp);
2871
- return /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
2872
- " ",
2873
- ago,
2874
- ": ",
2875
- /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
2876
- "@",
2877
- row.event.actor
2878
- ] }),
2879
- " ",
2880
- row.event.summary,
2881
- " ",
2882
- /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
2883
- "(",
2884
- row.event.repoShortName,
2885
- ")"
2886
- ] })
2887
- ] });
3969
+ if (`gh:${rd.repo.name}:${issue.number}` === selectedId)
3970
+ return { issue, repoName: rd.repo.name };
2888
3971
  }
2889
- case "error":
2890
- return /* @__PURE__ */ jsxs13(Text13, { color: "red", children: [
2891
- " Error: ",
2892
- row.text
2893
- ] });
2894
- case "gap":
2895
- return /* @__PURE__ */ jsx13(Text13, { children: "" });
2896
3972
  }
3973
+ return null;
3974
+ }
3975
+ function isHeaderId2(id) {
3976
+ return id != null && (id.startsWith("header:") || id.startsWith("sub:"));
2897
3977
  }
2898
3978
  function Dashboard({ config: config2, options, activeProfile }) {
2899
3979
  const { exit } = useApp();
@@ -2907,7 +3987,9 @@ function Dashboard({ config: config2, options, activeProfile }) {
2907
3987
  consecutiveFailures,
2908
3988
  autoRefreshPaused,
2909
3989
  refresh,
2910
- mutateData
3990
+ mutateData,
3991
+ pauseAutoRefresh,
3992
+ resumeAutoRefresh
2911
3993
  } = useData(config2, options, refreshMs);
2912
3994
  const allRepos = useMemo2(() => data?.repos ?? [], [data?.repos]);
2913
3995
  const allTasks = useMemo2(
@@ -2916,10 +3998,10 @@ function Dashboard({ config: config2, options, activeProfile }) {
2916
3998
  );
2917
3999
  const allActivity = useMemo2(() => data?.activity ?? [], [data?.activity]);
2918
4000
  const ui = useUIState();
2919
- const [searchQuery, setSearchQuery] = useState9("");
4001
+ const [searchQuery, setSearchQuery] = useState11("");
2920
4002
  const { toasts, toast, handleErrorAction } = useToast();
2921
- const [, setTick] = useState9(0);
2922
- useEffect3(() => {
4003
+ const [, setTick] = useState11(0);
4004
+ useEffect6(() => {
2923
4005
  const id = setInterval(() => setTick((t) => t + 1), 1e4);
2924
4006
  return () => clearInterval(id);
2925
4007
  }, []);
@@ -2938,7 +4020,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
2938
4020
  [repos, tasks, allActivity.length]
2939
4021
  );
2940
4022
  const nav = useNavigation(navItems);
2941
- const getRepoForId = useCallback8((id) => {
4023
+ const getRepoForId = useCallback10((id) => {
2942
4024
  if (id.startsWith("gh:")) {
2943
4025
  const parts = id.split(":");
2944
4026
  return parts.length >= 3 ? `${parts[1]}` : null;
@@ -2947,7 +4029,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
2947
4029
  return null;
2948
4030
  }, []);
2949
4031
  const multiSelect = useMultiSelect(getRepoForId);
2950
- useEffect3(() => {
4032
+ useEffect6(() => {
2951
4033
  if (multiSelect.count === 0) return;
2952
4034
  const validIds = new Set(navItems.map((i) => i.id));
2953
4035
  multiSelect.prune(validIds);
@@ -2961,8 +4043,9 @@ function Dashboard({ config: config2, options, activeProfile }) {
2961
4043
  mutateData,
2962
4044
  onOverlayDone: ui.exitOverlay
2963
4045
  });
2964
- const pendingPickRef = useRef7(null);
2965
- const handleCreateIssueWithPrompt = useCallback8(
4046
+ const pendingPickRef = useRef11(null);
4047
+ const labelCacheRef = useRef11({});
4048
+ const handleCreateIssueWithPrompt = useCallback10(
2966
4049
  (repo, title, labels) => {
2967
4050
  actions.handleCreateIssue(repo, title, labels).then((result) => {
2968
4051
  if (result) {
@@ -2973,7 +4056,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
2973
4056
  },
2974
4057
  [actions, ui]
2975
4058
  );
2976
- const handleConfirmPick = useCallback8(() => {
4059
+ const handleConfirmPick = useCallback10(() => {
2977
4060
  const pending = pendingPickRef.current;
2978
4061
  pendingPickRef.current = null;
2979
4062
  ui.exitOverlay();
@@ -2991,14 +4074,14 @@ function Dashboard({ config: config2, options, activeProfile }) {
2991
4074
  })
2992
4075
  );
2993
4076
  }, [config2, toast, refresh, ui]);
2994
- const handleCancelPick = useCallback8(() => {
4077
+ const handleCancelPick = useCallback10(() => {
2995
4078
  pendingPickRef.current = null;
2996
4079
  ui.exitOverlay();
2997
4080
  }, [ui]);
2998
- const [focusLabel, setFocusLabel] = useState9(null);
2999
- const handleEnterFocus = useCallback8(() => {
4081
+ const [focusLabel, setFocusLabel] = useState11(null);
4082
+ const handleEnterFocus = useCallback10(() => {
3000
4083
  const id = nav.selectedId;
3001
- if (!id || isHeaderId(id)) return;
4084
+ if (!id || isHeaderId2(id)) return;
3002
4085
  let label = "";
3003
4086
  if (id.startsWith("gh:")) {
3004
4087
  const found = findSelectedIssueWithRepo(repos, id);
@@ -3015,11 +4098,11 @@ function Dashboard({ config: config2, options, activeProfile }) {
3015
4098
  setFocusLabel(label);
3016
4099
  ui.enterFocus();
3017
4100
  }, [nav.selectedId, repos, tasks, config2.repos, ui]);
3018
- const handleFocusExit = useCallback8(() => {
4101
+ const handleFocusExit = useCallback10(() => {
3019
4102
  setFocusLabel(null);
3020
4103
  ui.exitToNormal();
3021
4104
  }, [ui]);
3022
- const handleFocusEndAction = useCallback8(
4105
+ const handleFocusEndAction = useCallback10(
3023
4106
  (action) => {
3024
4107
  switch (action) {
3025
4108
  case "restart":
@@ -3045,13 +4128,13 @@ function Dashboard({ config: config2, options, activeProfile }) {
3045
4128
  },
3046
4129
  [toast, ui]
3047
4130
  );
3048
- const [focusKey, setFocusKey] = useState9(0);
4131
+ const [focusKey, setFocusKey] = useState11(0);
3049
4132
  const { stdout } = useStdout();
3050
- const [termSize, setTermSize] = useState9({
4133
+ const [termSize, setTermSize] = useState11({
3051
4134
  cols: stdout?.columns ?? 80,
3052
4135
  rows: stdout?.rows ?? 24
3053
4136
  });
3054
- useEffect3(() => {
4137
+ useEffect6(() => {
3055
4138
  if (!stdout) return;
3056
4139
  const onResize = () => setTermSize({ cols: stdout.columns, rows: stdout.rows });
3057
4140
  stdout.on("resize", onResize);
@@ -3068,7 +4151,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3068
4151
  () => buildFlatRows(repos, tasks, allActivity, nav.isCollapsed),
3069
4152
  [repos, tasks, allActivity, nav.isCollapsed]
3070
4153
  );
3071
- const scrollRef = useRef7(0);
4154
+ const scrollRef = useRef11(0);
3072
4155
  const selectedRowIdx = flatRows.findIndex((r) => r.navId === nav.selectedId);
3073
4156
  if (selectedRowIdx >= 0) {
3074
4157
  if (selectedRowIdx < scrollRef.current) {
@@ -3086,7 +4169,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3086
4169
  const belowCount = flatRows.length - scrollRef.current - viewportHeight;
3087
4170
  const selectedItem = useMemo2(() => {
3088
4171
  const id = nav.selectedId;
3089
- if (!id || isHeaderId(id)) return { issue: null, task: null, repoName: null };
4172
+ if (!id || isHeaderId2(id)) return { issue: null, task: null, repoName: null };
3090
4173
  if (id.startsWith("gh:")) {
3091
4174
  for (const rd of repos) {
3092
4175
  for (const issue of rd.issues) {
@@ -3106,17 +4189,42 @@ function Dashboard({ config: config2, options, activeProfile }) {
3106
4189
  const repoName = multiSelect.count > 0 ? multiSelect.constrainedRepo : selectedItem.repoName;
3107
4190
  if (!repoName || repoName === "ticktick") return [];
3108
4191
  const rd = repos.find((r) => r.repo.name === repoName);
3109
- return rd?.statusOptions.filter((o) => !isTerminalStatus(o.name)) ?? [];
4192
+ return rd?.statusOptions ?? [];
3110
4193
  }, [selectedItem.repoName, repos, multiSelect.count, multiSelect.constrainedRepo]);
3111
- const handleOpen = useCallback8(() => {
4194
+ const handleOpen = useCallback10(() => {
3112
4195
  const url = findSelectedUrl(repos, nav.selectedId);
3113
4196
  if (url) openInBrowser(url);
3114
4197
  }, [repos, nav.selectedId]);
3115
- const handleSlack = useCallback8(() => {
4198
+ const handleSlack = useCallback10(() => {
3116
4199
  const found = findSelectedIssueWithRepo(repos, nav.selectedId);
3117
4200
  if (!found?.issue.slackThreadUrl) return;
3118
4201
  openInBrowser(found.issue.slackThreadUrl);
3119
4202
  }, [repos, nav.selectedId]);
4203
+ const handleCopyLink = useCallback10(() => {
4204
+ const found = findSelectedIssueWithRepo(repos, nav.selectedId);
4205
+ if (!found) return;
4206
+ const rc = config2.repos.find((r) => r.name === found.repoName);
4207
+ const label = `${rc?.shortName ?? found.repoName}#${found.issue.number}`;
4208
+ const clipArgs = getClipboardArgs();
4209
+ if (clipArgs) {
4210
+ const [cmd, ...args] = clipArgs;
4211
+ if (!cmd) {
4212
+ toast.info(`${label} \u2014 ${found.issue.url}`);
4213
+ return;
4214
+ }
4215
+ const result = spawnSync2(cmd, args, {
4216
+ input: found.issue.url,
4217
+ stdio: ["pipe", "pipe", "pipe"]
4218
+ });
4219
+ if (result.status === 0) {
4220
+ toast.success(`Copied ${label} to clipboard`);
4221
+ } else {
4222
+ toast.info(`${label} \u2014 ${found.issue.url}`);
4223
+ }
4224
+ } else {
4225
+ toast.info(`${label} \u2014 ${found.issue.url}`);
4226
+ }
4227
+ }, [repos, nav.selectedId, config2.repos, toast]);
3120
4228
  const multiSelectType = useMemo2(() => {
3121
4229
  let hasGh = false;
3122
4230
  let hasTt = false;
@@ -3128,7 +4236,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3128
4236
  if (hasTt) return "ticktick";
3129
4237
  return "github";
3130
4238
  }, [multiSelect.selected]);
3131
- const handleBulkAction = useCallback8(
4239
+ const handleBulkAction = useCallback10(
3132
4240
  (action) => {
3133
4241
  const ids = multiSelect.selected;
3134
4242
  switch (action.type) {
@@ -3172,7 +4280,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3172
4280
  },
3173
4281
  [multiSelect, actions, ui, toast]
3174
4282
  );
3175
- const handleBulkStatusSelect = useCallback8(
4283
+ const handleBulkStatusSelect = useCallback10(
3176
4284
  (optionId) => {
3177
4285
  const ids = multiSelect.selected;
3178
4286
  ui.exitOverlay();
@@ -3188,168 +4296,35 @@ function Dashboard({ config: config2, options, activeProfile }) {
3188
4296
  },
3189
4297
  [multiSelect, actions, ui]
3190
4298
  );
3191
- const handleInput = useCallback8(
3192
- (input2, key) => {
3193
- if (input2 === "?") {
3194
- ui.toggleHelp();
3195
- return;
3196
- }
3197
- if (key.escape && ui.state.mode !== "focus") {
3198
- if (ui.state.mode === "multiSelect") {
3199
- multiSelect.clear();
3200
- }
3201
- ui.exitOverlay();
3202
- return;
3203
- }
3204
- if (ui.canNavigate) {
3205
- if (input2 === "j" || key.downArrow) {
3206
- nav.moveDown();
3207
- return;
3208
- }
3209
- if (input2 === "k" || key.upArrow) {
3210
- nav.moveUp();
3211
- return;
3212
- }
3213
- if (key.tab) {
3214
- if (ui.state.mode === "multiSelect") {
3215
- multiSelect.clear();
3216
- ui.clearMultiSelect();
3217
- }
3218
- key.shift ? nav.prevSection() : nav.nextSection();
3219
- return;
3220
- }
3221
- }
3222
- if (ui.state.mode === "multiSelect") {
3223
- if (input2 === " ") {
3224
- const id = nav.selectedId;
3225
- if (id && !isHeaderId(id)) {
3226
- multiSelect.toggle(id);
3227
- }
3228
- return;
3229
- }
3230
- if (key.return) {
3231
- if (multiSelect.count > 0) {
3232
- ui.enterBulkAction();
3233
- }
3234
- return;
3235
- }
3236
- if (input2 === "m" && multiSelect.count > 0) {
3237
- ui.enterBulkAction();
3238
- return;
3239
- }
3240
- return;
3241
- }
3242
- if (input2 === "d") {
3243
- if (handleErrorAction("dismiss")) return;
3244
- }
3245
- if (input2 === "r" && handleErrorAction("retry")) return;
3246
- if (ui.canAct) {
3247
- if (input2 === "/") {
3248
- multiSelect.clear();
3249
- ui.enterSearch();
3250
- return;
3251
- }
3252
- if (input2 === "q") {
3253
- exit();
3254
- return;
3255
- }
3256
- if (input2 === "r" || input2 === "R") {
3257
- multiSelect.clear();
3258
- refresh();
3259
- return;
3260
- }
3261
- if (input2 === "s") {
3262
- handleSlack();
3263
- return;
3264
- }
3265
- if (input2 === "p") {
3266
- actions.handlePick();
3267
- return;
3268
- }
3269
- if (input2 === "a") {
3270
- actions.handleAssign();
3271
- return;
3272
- }
3273
- if (input2 === "u") {
3274
- actions.handleUnassign();
3275
- return;
3276
- }
3277
- if (input2 === "c") {
3278
- if (selectedItem.issue) {
3279
- multiSelect.clear();
3280
- ui.enterComment();
3281
- }
3282
- return;
3283
- }
3284
- if (input2 === "m") {
3285
- if (selectedItem.issue && selectedRepoStatusOptions.length > 0) {
3286
- multiSelect.clear();
3287
- ui.enterStatus();
3288
- } else if (selectedItem.issue) {
3289
- toast.info("Issue not in a project board");
3290
- }
3291
- return;
3292
- }
3293
- if (input2 === "n") {
3294
- multiSelect.clear();
3295
- ui.enterCreate();
3296
- return;
3297
- }
3298
- if (input2 === "f") {
3299
- handleEnterFocus();
3300
- return;
3301
- }
3302
- if (input2 === " ") {
3303
- const id = nav.selectedId;
3304
- if (id && !isHeaderId(id)) {
3305
- multiSelect.toggle(id);
3306
- ui.enterMultiSelect();
3307
- } else if (isHeaderId(nav.selectedId)) {
3308
- nav.toggleSection();
3309
- }
3310
- return;
3311
- }
3312
- if (key.return) {
3313
- if (isHeaderId(nav.selectedId)) {
3314
- nav.toggleSection();
3315
- return;
3316
- }
3317
- handleOpen();
3318
- return;
3319
- }
3320
- }
3321
- },
3322
- [
3323
- ui,
3324
- nav,
4299
+ const onSearchEscape = useCallback10(() => {
4300
+ ui.exitOverlay();
4301
+ setSearchQuery("");
4302
+ }, [ui]);
4303
+ useKeyboard({
4304
+ ui,
4305
+ nav,
4306
+ multiSelect,
4307
+ selectedIssue: selectedItem.issue,
4308
+ selectedRepoStatusOptionsLength: selectedRepoStatusOptions.length,
4309
+ actions: {
3325
4310
  exit,
3326
4311
  refresh,
3327
4312
  handleSlack,
4313
+ handleCopyLink,
3328
4314
  handleOpen,
3329
- actions,
3330
- selectedItem.issue,
3331
- selectedRepoStatusOptions.length,
3332
- toast,
3333
- nav.selectedId,
3334
- multiSelect,
3335
4315
  handleEnterFocus,
3336
- handleErrorAction
3337
- ]
3338
- );
3339
- const inputActive = ui.state.mode === "normal" || ui.state.mode === "multiSelect" || ui.state.mode === "focus";
3340
- useInput8(handleInput, { isActive: inputActive });
3341
- const handleSearchEscape = useCallback8(
3342
- (_input, key) => {
3343
- if (key.escape) {
3344
- ui.exitOverlay();
3345
- setSearchQuery("");
3346
- }
4316
+ handlePick: actions.handlePick,
4317
+ handleAssign: actions.handleAssign,
4318
+ handleUnassign: actions.handleUnassign,
4319
+ handleEnterLabel: ui.enterLabel,
4320
+ handleEnterCreateNl: ui.enterCreateNl,
4321
+ handleErrorAction,
4322
+ toastInfo: toast.info
3347
4323
  },
3348
- [ui]
3349
- );
3350
- useInput8(handleSearchEscape, { isActive: ui.state.mode === "search" });
4324
+ onSearchEscape
4325
+ });
3351
4326
  if (status === "loading" && !data) {
3352
- return /* @__PURE__ */ jsx13(Box13, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx13(Spinner2, { label: "Loading dashboard..." }) });
4327
+ return /* @__PURE__ */ jsx17(Box16, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx17(Spinner4, { label: "Loading dashboard..." }) });
3353
4328
  }
3354
4329
  const now = data?.fetchedAt ?? /* @__PURE__ */ new Date();
3355
4330
  const dateStr = now.toLocaleDateString("en-US", {
@@ -3357,93 +4332,81 @@ function Dashboard({ config: config2, options, activeProfile }) {
3357
4332
  day: "numeric",
3358
4333
  year: "numeric"
3359
4334
  });
3360
- return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
3361
- /* @__PURE__ */ jsxs13(Box13, { children: [
3362
- /* @__PURE__ */ jsx13(Text13, { color: "cyan", bold: true, children: "HOG BOARD" }),
3363
- activeProfile ? /* @__PURE__ */ jsxs13(Text13, { color: "yellow", children: [
4335
+ return /* @__PURE__ */ jsxs17(Box16, { flexDirection: "column", paddingX: 1, children: [
4336
+ /* @__PURE__ */ jsxs17(Box16, { children: [
4337
+ /* @__PURE__ */ jsx17(Text16, { color: "cyan", bold: true, children: "HOG BOARD" }),
4338
+ activeProfile ? /* @__PURE__ */ jsxs17(Text16, { color: "yellow", children: [
3364
4339
  " [",
3365
4340
  activeProfile,
3366
4341
  "]"
3367
4342
  ] }) : null,
3368
- /* @__PURE__ */ jsxs13(Text13, { color: "gray", children: [
4343
+ /* @__PURE__ */ jsxs17(Text16, { color: "gray", children: [
3369
4344
  " ",
3370
4345
  "\u2014",
3371
4346
  " ",
3372
4347
  dateStr
3373
4348
  ] }),
3374
- /* @__PURE__ */ jsx13(Text13, { children: " " }),
3375
- isRefreshing ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
3376
- /* @__PURE__ */ jsx13(Spinner2, { label: "" }),
3377
- /* @__PURE__ */ jsx13(Text13, { color: "cyan", children: " Refreshing..." })
3378
- ] }) : lastRefresh ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
3379
- /* @__PURE__ */ jsxs13(Text13, { color: refreshAgeColor(lastRefresh), children: [
4349
+ /* @__PURE__ */ jsx17(Text16, { children: " " }),
4350
+ isRefreshing ? /* @__PURE__ */ jsxs17(Fragment5, { children: [
4351
+ /* @__PURE__ */ jsx17(Spinner4, { label: "" }),
4352
+ /* @__PURE__ */ jsx17(Text16, { color: "cyan", children: " Refreshing..." })
4353
+ ] }) : lastRefresh ? /* @__PURE__ */ jsxs17(Fragment5, { children: [
4354
+ /* @__PURE__ */ jsxs17(Text16, { color: refreshAgeColor(lastRefresh), children: [
3380
4355
  "Updated ",
3381
- timeAgo2(lastRefresh)
4356
+ timeAgo3(lastRefresh)
3382
4357
  ] }),
3383
- consecutiveFailures > 0 ? /* @__PURE__ */ jsx13(Text13, { color: "red", children: " (!)" }) : null
4358
+ consecutiveFailures > 0 ? /* @__PURE__ */ jsx17(Text16, { color: "red", children: " (!)" }) : null
3384
4359
  ] }) : null,
3385
- autoRefreshPaused ? /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: " Auto-refresh paused \u2014 press r to retry" }) : null
4360
+ autoRefreshPaused ? /* @__PURE__ */ jsx17(Text16, { color: "yellow", children: " Auto-refresh paused \u2014 press r to retry" }) : null
3386
4361
  ] }),
3387
- error ? /* @__PURE__ */ jsxs13(Text13, { color: "red", children: [
4362
+ error ? /* @__PURE__ */ jsxs17(Text16, { color: "red", children: [
3388
4363
  "Error: ",
3389
4364
  error
3390
4365
  ] }) : null,
3391
- ui.state.helpVisible ? /* @__PURE__ */ jsx13(HelpOverlay, { currentMode: ui.state.mode, onClose: ui.toggleHelp }) : null,
3392
- ui.state.mode === "overlay:status" && selectedRepoStatusOptions.length > 0 ? /* @__PURE__ */ jsx13(
3393
- StatusPicker,
4366
+ /* @__PURE__ */ jsx17(
4367
+ OverlayRenderer,
3394
4368
  {
3395
- options: selectedRepoStatusOptions,
4369
+ uiState: ui.state,
4370
+ config: config2,
4371
+ selectedRepoStatusOptions,
3396
4372
  currentStatus: multiSelect.count > 0 ? void 0 : selectedItem.issue?.projectStatus,
3397
- onSelect: multiSelect.count > 0 ? handleBulkStatusSelect : actions.handleStatusChange,
3398
- onCancel: ui.exitOverlay
3399
- }
3400
- ) : null,
3401
- ui.state.mode === "overlay:create" ? /* @__PURE__ */ jsx13(
3402
- CreateIssueForm,
3403
- {
3404
- repos: config2.repos,
4373
+ onStatusSelect: multiSelect.count > 0 ? handleBulkStatusSelect : actions.handleStatusChange,
4374
+ onExitOverlay: ui.exitOverlay,
3405
4375
  defaultRepo: selectedItem.repoName,
3406
- onSubmit: handleCreateIssueWithPrompt,
3407
- onCancel: ui.exitOverlay
3408
- }
3409
- ) : null,
3410
- ui.state.mode === "overlay:confirmPick" ? /* @__PURE__ */ jsx13(
3411
- ConfirmPrompt,
3412
- {
3413
- message: "Pick this issue?",
3414
- onConfirm: handleConfirmPick,
3415
- onCancel: handleCancelPick
3416
- }
3417
- ) : null,
3418
- ui.state.mode === "overlay:bulkAction" ? /* @__PURE__ */ jsx13(
3419
- BulkActionMenu,
3420
- {
3421
- count: multiSelect.count,
3422
- selectionType: multiSelectType,
3423
- onSelect: handleBulkAction,
3424
- onCancel: ui.exitOverlay
4376
+ onCreateIssue: handleCreateIssueWithPrompt,
4377
+ onConfirmPick: handleConfirmPick,
4378
+ onCancelPick: handleCancelPick,
4379
+ multiSelectCount: multiSelect.count,
4380
+ multiSelectType,
4381
+ onBulkAction: handleBulkAction,
4382
+ focusLabel,
4383
+ focusKey,
4384
+ onFocusExit: handleFocusExit,
4385
+ onFocusEndAction: handleFocusEndAction,
4386
+ searchQuery,
4387
+ onSearchChange: setSearchQuery,
4388
+ onSearchSubmit: ui.exitOverlay,
4389
+ selectedIssue: selectedItem.issue,
4390
+ onComment: actions.handleComment,
4391
+ onPauseRefresh: pauseAutoRefresh,
4392
+ onResumeRefresh: resumeAutoRefresh,
4393
+ onToggleHelp: ui.toggleHelp,
4394
+ labelCache: labelCacheRef.current,
4395
+ onLabelConfirm: actions.handleLabelChange,
4396
+ onLabelError: (msg) => toast.error(msg),
4397
+ onLlmFallback: (msg) => toast.info(msg)
3425
4398
  }
3426
- ) : null,
3427
- ui.state.mode === "focus" && focusLabel ? /* @__PURE__ */ jsx13(
3428
- FocusMode,
3429
- {
3430
- label: focusLabel,
3431
- durationSec: config2.board.focusDuration ?? 1500,
3432
- onExit: handleFocusExit,
3433
- onEndAction: handleFocusEndAction
3434
- },
3435
- focusKey
3436
- ) : null,
3437
- !ui.state.helpVisible && ui.state.mode !== "overlay:status" && ui.state.mode !== "overlay:create" && ui.state.mode !== "overlay:bulkAction" && ui.state.mode !== "overlay:confirmPick" && ui.state.mode !== "focus" ? /* @__PURE__ */ jsxs13(Box13, { height: viewportHeight, children: [
3438
- /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", flexGrow: 1, children: [
3439
- hasMoreAbove ? /* @__PURE__ */ jsxs13(Text13, { color: "gray", dimColor: true, children: [
4399
+ ),
4400
+ !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: [
4401
+ /* @__PURE__ */ jsxs17(Box16, { flexDirection: "column", flexGrow: 1, children: [
4402
+ hasMoreAbove ? /* @__PURE__ */ jsxs17(Text16, { color: "gray", dimColor: true, children: [
3440
4403
  " ",
3441
4404
  "\u25B2",
3442
4405
  " ",
3443
4406
  aboveCount,
3444
4407
  " more above"
3445
4408
  ] }) : null,
3446
- visibleRows.map((row) => /* @__PURE__ */ jsx13(
4409
+ visibleRows.map((row) => /* @__PURE__ */ jsx17(
3447
4410
  RowRenderer,
3448
4411
  {
3449
4412
  row,
@@ -3453,7 +4416,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3453
4416
  },
3454
4417
  row.key
3455
4418
  )),
3456
- hasMoreBelow ? /* @__PURE__ */ jsxs13(Text13, { color: "gray", dimColor: true, children: [
4419
+ hasMoreBelow ? /* @__PURE__ */ jsxs17(Text16, { color: "gray", dimColor: true, children: [
3457
4420
  " ",
3458
4421
  "\u25BC",
3459
4422
  " ",
@@ -3461,7 +4424,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
3461
4424
  " more below"
3462
4425
  ] }) : null
3463
4426
  ] }),
3464
- showDetailPanel ? /* @__PURE__ */ jsx13(Box13, { marginLeft: 1, width: detailPanelWidth, children: /* @__PURE__ */ jsx13(
4427
+ showDetailPanel ? /* @__PURE__ */ jsx17(Box16, { marginLeft: 1, width: detailPanelWidth, children: /* @__PURE__ */ jsx17(
3465
4428
  DetailPanel,
3466
4429
  {
3467
4430
  issue: selectedItem.issue,
@@ -3470,25 +4433,16 @@ function Dashboard({ config: config2, options, activeProfile }) {
3470
4433
  }
3471
4434
  ) }) : null
3472
4435
  ] }) : null,
3473
- ui.state.mode === "search" ? /* @__PURE__ */ jsx13(SearchBar, { defaultValue: searchQuery, onChange: setSearchQuery, onSubmit: ui.exitOverlay }) : null,
3474
- ui.state.mode === "overlay:comment" && selectedItem.issue ? /* @__PURE__ */ jsx13(
3475
- CommentInput,
3476
- {
3477
- issueNumber: selectedItem.issue.number,
3478
- onSubmit: actions.handleComment,
3479
- onCancel: ui.exitOverlay
3480
- }
3481
- ) : null,
3482
- /* @__PURE__ */ jsx13(ToastContainer, { toasts }),
3483
- /* @__PURE__ */ jsx13(Box13, { children: ui.state.mode === "multiSelect" ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
3484
- /* @__PURE__ */ jsxs13(Text13, { color: "cyan", bold: true, children: [
4436
+ /* @__PURE__ */ jsx17(ToastContainer, { toasts }),
4437
+ /* @__PURE__ */ jsx17(Box16, { children: ui.state.mode === "multiSelect" ? /* @__PURE__ */ jsxs17(Fragment5, { children: [
4438
+ /* @__PURE__ */ jsxs17(Text16, { color: "cyan", bold: true, children: [
3485
4439
  multiSelect.count,
3486
4440
  " selected"
3487
4441
  ] }),
3488
- /* @__PURE__ */ jsx13(Text13, { color: "gray", children: " Space:toggle Enter:actions Esc:cancel" })
3489
- ] }) : ui.state.mode === "focus" ? /* @__PURE__ */ jsx13(Text13, { color: "magenta", bold: true, children: "Focus mode \u2014 Esc to exit" }) : /* @__PURE__ */ jsxs13(Fragment4, { children: [
3490
- /* @__PURE__ */ jsx13(Text13, { color: "gray", children: "j/k:nav Tab:section Enter:open Space:select /:search p:pick c:comment m:status a/u:assign s:slack n:new f:focus ?:help q:quit" }),
3491
- searchQuery && ui.state.mode !== "search" ? /* @__PURE__ */ jsxs13(Text13, { color: "yellow", children: [
4442
+ /* @__PURE__ */ jsx17(Text16, { color: "gray", children: " Space:toggle Enter:actions Esc:cancel" })
4443
+ ] }) : ui.state.mode === "focus" ? /* @__PURE__ */ jsx17(Text16, { color: "magenta", bold: true, children: "Focus mode \u2014 Esc to exit" }) : /* @__PURE__ */ jsxs17(Fragment5, { children: [
4444
+ /* @__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" }),
4445
+ searchQuery && ui.state.mode !== "search" ? /* @__PURE__ */ jsxs17(Text16, { color: "yellow", children: [
3492
4446
  ' filter: "',
3493
4447
  searchQuery,
3494
4448
  '"'
@@ -3496,29 +4450,23 @@ function Dashboard({ config: config2, options, activeProfile }) {
3496
4450
  ] }) })
3497
4451
  ] });
3498
4452
  }
3499
- var TERMINAL_STATUS_RE2, PRIORITY_RANK, CHROME_ROWS;
4453
+ var TERMINAL_STATUS_RE3, PRIORITY_RANK, CHROME_ROWS;
3500
4454
  var init_dashboard = __esm({
3501
4455
  "src/board/components/dashboard.tsx"() {
3502
4456
  "use strict";
4457
+ init_clipboard();
3503
4458
  init_use_actions();
3504
4459
  init_use_data();
4460
+ init_use_keyboard();
3505
4461
  init_use_multi_select();
3506
4462
  init_use_navigation();
3507
4463
  init_use_toast();
3508
4464
  init_use_ui_state();
3509
- init_bulk_action_menu();
3510
- init_comment_input();
3511
- init_confirm_prompt();
3512
- init_create_issue_form();
3513
4465
  init_detail_panel();
3514
- init_focus_mode();
3515
- init_help_overlay();
3516
- init_issue_row();
3517
- init_search_bar();
3518
- init_status_picker();
3519
- init_task_row();
4466
+ init_overlay_renderer();
4467
+ init_row_renderer();
3520
4468
  init_toast_container();
3521
- TERMINAL_STATUS_RE2 = /^(done|shipped|won't|wont|closed|complete|completed)$/i;
4469
+ TERMINAL_STATUS_RE3 = /^(done|shipped|won't|wont|closed|complete|completed)$/i;
3522
4470
  PRIORITY_RANK = {
3523
4471
  "priority:critical": 0,
3524
4472
  "priority:high": 1,
@@ -3535,17 +4483,19 @@ __export(live_exports, {
3535
4483
  runLiveDashboard: () => runLiveDashboard
3536
4484
  });
3537
4485
  import { render } from "ink";
3538
- import { jsx as jsx14 } from "react/jsx-runtime";
4486
+ import { jsx as jsx18 } from "react/jsx-runtime";
3539
4487
  async function runLiveDashboard(config2, options, activeProfile) {
3540
- const { waitUntilExit } = render(
3541
- /* @__PURE__ */ jsx14(Dashboard, { config: config2, options, activeProfile: activeProfile ?? null })
4488
+ const instance = render(
4489
+ /* @__PURE__ */ jsx18(Dashboard, { config: config2, options, activeProfile: activeProfile ?? null })
3542
4490
  );
3543
- await waitUntilExit();
4491
+ setInkInstance(instance);
4492
+ await instance.waitUntilExit();
3544
4493
  }
3545
4494
  var init_live = __esm({
3546
4495
  "src/board/live.tsx"() {
3547
4496
  "use strict";
3548
4497
  init_dashboard();
4498
+ init_ink_instance();
3549
4499
  }
3550
4500
  });
3551
4501
 
@@ -3911,8 +4861,10 @@ var init_format_static = __esm({
3911
4861
  });
3912
4862
 
3913
4863
  // src/cli.ts
4864
+ init_ai();
3914
4865
  init_api();
3915
4866
  init_config();
4867
+ import { execFileSync as execFileSync5 } from "child_process";
3916
4868
  import { Command } from "commander";
3917
4869
 
3918
4870
  // src/init.ts
@@ -4160,6 +5112,28 @@ Configuring ${repoName}...`);
4160
5112
  message: " Focus timer duration (seconds):",
4161
5113
  default: "1500"
4162
5114
  });
5115
+ console.log("\nAI-enhanced issue creation (optional):");
5116
+ console.log(
5117
+ ' Press I on the board to create issues with natural language (e.g. "fix login bug #backend @alice due friday").'
5118
+ );
5119
+ console.log(" Without a key the heuristic parser still works \u2014 labels, assignee, and due dates");
5120
+ console.log(" are extracted from #, @, and due tokens. An OpenRouter key enables richer title");
5121
+ console.log(" cleanup and inference for ambiguous input.");
5122
+ const setupLlm = await confirm({
5123
+ message: " Set up an OpenRouter API key now?",
5124
+ default: false
5125
+ });
5126
+ if (setupLlm) {
5127
+ console.log(" Get a free key at https://openrouter.ai/keys");
5128
+ const llmKey = await input({
5129
+ message: " OpenRouter API key:",
5130
+ validate: (v) => v.trim().startsWith("sk-or-") ? true : 'Key must start with "sk-or-"'
5131
+ });
5132
+ saveLlmAuth(llmKey.trim());
5133
+ console.log(" OpenRouter key saved to ~/.config/hog/auth.json");
5134
+ } else {
5135
+ console.log(" Skipped. You can add it later: hog config ai:set-key");
5136
+ }
4163
5137
  const existingConfig = configExists ? loadFullConfig() : void 0;
4164
5138
  const config2 = {
4165
5139
  version: 3,
@@ -4582,7 +5556,7 @@ function resolveProjectId(projectId) {
4582
5556
  process.exit(1);
4583
5557
  }
4584
5558
  var program = new Command();
4585
- program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.2.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
5559
+ program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.4.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
4586
5560
  const opts = thisCommand.opts();
4587
5561
  if (opts.json) setFormat("json");
4588
5562
  if (opts.human) setFormat("human");
@@ -4861,6 +5835,61 @@ config.command("ticktick:disable").description("Disable TickTick integration in
4861
5835
  printSuccess("TickTick integration disabled. Board will no longer show TickTick tasks.");
4862
5836
  }
4863
5837
  });
5838
+ config.command("ai:set-key <key>").description("Store an OpenRouter API key for AI-enhanced issue creation (I key on board)").action((key) => {
5839
+ if (!key.startsWith("sk-or-")) {
5840
+ console.error('Error: key must start with "sk-or-". Get one at https://openrouter.ai/keys');
5841
+ process.exit(1);
5842
+ }
5843
+ saveLlmAuth(key);
5844
+ if (useJson()) {
5845
+ jsonOut({ ok: true, message: "OpenRouter key saved" });
5846
+ } else {
5847
+ printSuccess("OpenRouter key saved to ~/.config/hog/auth.json");
5848
+ console.log(" Press I on the board to create issues with natural language.");
5849
+ }
5850
+ });
5851
+ config.command("ai:clear-key").description("Remove the stored OpenRouter API key").action(() => {
5852
+ const existing = getLlmAuth();
5853
+ if (!existing) {
5854
+ if (useJson()) {
5855
+ jsonOut({ ok: true, message: "No key was stored" });
5856
+ } else {
5857
+ console.log("No OpenRouter key stored.");
5858
+ }
5859
+ return;
5860
+ }
5861
+ clearLlmAuth();
5862
+ if (useJson()) {
5863
+ jsonOut({ ok: true, message: "OpenRouter key removed" });
5864
+ } else {
5865
+ printSuccess("OpenRouter key removed from ~/.config/hog/auth.json");
5866
+ }
5867
+ });
5868
+ config.command("ai:status").description("Show whether AI-enhanced issue creation is available and which source provides it").action(() => {
5869
+ const envOr = process.env["OPENROUTER_API_KEY"];
5870
+ const envAnt = process.env["ANTHROPIC_API_KEY"];
5871
+ const stored = getLlmAuth();
5872
+ if (useJson()) {
5873
+ jsonOut({
5874
+ ok: true,
5875
+ data: {
5876
+ active: !!(envOr ?? envAnt ?? stored),
5877
+ source: envOr ? "env:OPENROUTER_API_KEY" : envAnt ? "env:ANTHROPIC_API_KEY" : stored ? "config:auth.json" : null,
5878
+ provider: envOr ? "openrouter" : envAnt ? "anthropic" : stored ? "openrouter" : null
5879
+ }
5880
+ });
5881
+ } else if (envOr) {
5882
+ console.log("AI: active (source: OPENROUTER_API_KEY env var, provider: openrouter)");
5883
+ } else if (envAnt) {
5884
+ console.log("AI: active (source: ANTHROPIC_API_KEY env var, provider: anthropic)");
5885
+ } else if (stored) {
5886
+ console.log("AI: active (source: ~/.config/hog/auth.json, provider: openrouter)");
5887
+ } else {
5888
+ console.log("AI: off \u2014 heuristic-only mode");
5889
+ console.log(" Enable with: hog config ai:set-key <sk-or-...>");
5890
+ console.log(" Or set env: export OPENROUTER_API_KEY=sk-or-...");
5891
+ }
5892
+ });
4864
5893
  config.command("profile:create <name>").description("Create a board profile (copies current top-level config)").action((name) => {
4865
5894
  const cfg = loadFullConfig();
4866
5895
  if (cfg.profiles[name]) {
@@ -4931,6 +5960,53 @@ config.command("profile:default [name]").description("Set or show the default bo
4931
5960
  printSuccess(`Default profile set to "${name}".`);
4932
5961
  }
4933
5962
  });
5963
+ var issueCommand = new Command("issue").description("GitHub issue utilities");
5964
+ 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) => {
5965
+ const config2 = loadFullConfig();
5966
+ const repo = opts.repo ?? config2.repos[0]?.name;
5967
+ if (!repo) {
5968
+ console.error(
5969
+ "Error: no repo specified. Use --repo owner/name or configure repos in hog init."
5970
+ );
5971
+ process.exit(1);
5972
+ }
5973
+ if (hasLlmApiKey()) {
5974
+ console.error("[info] LLM parsing enabled");
5975
+ }
5976
+ const parsed = await extractIssueFields(text, {
5977
+ onLlmFallback: (msg) => console.error(`[warn] ${msg}`)
5978
+ });
5979
+ if (!parsed) {
5980
+ console.error(
5981
+ "Error: could not parse a title from input. Ensure your text has a non-empty title."
5982
+ );
5983
+ process.exit(1);
5984
+ }
5985
+ const labels = [...parsed.labels];
5986
+ if (parsed.dueDate) labels.push(`due:${parsed.dueDate}`);
5987
+ console.error(`Title: ${parsed.title}`);
5988
+ if (labels.length > 0) console.error(`Labels: ${labels.join(", ")}`);
5989
+ if (parsed.assignee) console.error(`Assignee: @${parsed.assignee}`);
5990
+ if (parsed.dueDate) console.error(`Due: ${parsed.dueDate}`);
5991
+ console.error(`Repo: ${repo}`);
5992
+ if (opts.dryRun) {
5993
+ console.error("[dry-run] Skipping issue creation.");
5994
+ return;
5995
+ }
5996
+ const args = ["issue", "create", "--repo", repo, "--title", parsed.title];
5997
+ for (const label of labels) {
5998
+ args.push("--label", label);
5999
+ }
6000
+ try {
6001
+ execFileSync5("gh", args, { stdio: "inherit" });
6002
+ } catch (err) {
6003
+ console.error(
6004
+ `Error: gh issue create failed: ${err instanceof Error ? err.message : String(err)}`
6005
+ );
6006
+ process.exit(1);
6007
+ }
6008
+ });
6009
+ program.addCommand(issueCommand);
4934
6010
  program.parseAsync().catch((err) => {
4935
6011
  const message = err instanceof Error ? err.message : String(err);
4936
6012
  console.error(`Error: ${message}`);