clairo 1.0.7 → 1.0.8

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.
Files changed (2) hide show
  1. package/dist/cli.js +1135 -976
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4,14 +4,84 @@
4
4
  import meow from "meow";
5
5
 
6
6
  // src/app.tsx
7
- import { useCallback as useCallback9, useMemo as useMemo2, useState as useState16 } from "react";
7
+ import { useCallback as useCallback10, useMemo as useMemo2, useState as useState17 } from "react";
8
8
  import { Box as Box16, useApp, useInput as useInput13 } from "ink";
9
9
 
10
10
  // src/components/github/GitHubView.tsx
11
- import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef3, useState as useState7 } from "react";
11
+ import { useCallback as useCallback9, useEffect as useEffect9, useRef as useRef5, useState as useState12 } from "react";
12
12
  import { TitledBox as TitledBox3 } from "@mishieck/ink-titled-box";
13
13
  import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
14
14
 
15
+ // src/hooks/github/useGitRepo.ts
16
+ import { useCallback, useEffect as useEffect2, useMemo, useState as useState2 } from "react";
17
+
18
+ // src/lib/duckEvents.ts
19
+ var listeners = /* @__PURE__ */ new Set();
20
+ var duckEvents = {
21
+ emit: (event) => {
22
+ listeners.forEach((fn) => fn(event));
23
+ },
24
+ subscribe: (fn) => {
25
+ listeners.add(fn);
26
+ return () => {
27
+ listeners.delete(fn);
28
+ };
29
+ }
30
+ };
31
+
32
+ // src/lib/config/index.ts
33
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
34
+ import { homedir } from "os";
35
+ import { dirname, join } from "path";
36
+ var CONFIG_PATH = join(homedir(), ".clairo", "config.json");
37
+ var DEFAULT_CONFIG = {};
38
+ function loadConfig() {
39
+ try {
40
+ if (!existsSync(CONFIG_PATH)) {
41
+ return DEFAULT_CONFIG;
42
+ }
43
+ const content = readFileSync(CONFIG_PATH, "utf-8");
44
+ return JSON.parse(content);
45
+ } catch {
46
+ return DEFAULT_CONFIG;
47
+ }
48
+ }
49
+ function saveConfig(config) {
50
+ const dir = dirname(CONFIG_PATH);
51
+ if (!existsSync(dir)) {
52
+ mkdirSync(dir, { recursive: true });
53
+ }
54
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
55
+ }
56
+
57
+ // src/lib/github/config.ts
58
+ function getRepoConfig(repoPath) {
59
+ const config = loadConfig();
60
+ const repos = config.repositories ?? {};
61
+ return repos[repoPath] ?? {};
62
+ }
63
+ function updateRepoConfig(repoPath, updates) {
64
+ const config = loadConfig();
65
+ if (!config.repositories) {
66
+ config.repositories = {};
67
+ }
68
+ config.repositories[repoPath] = {
69
+ ...config.repositories[repoPath],
70
+ ...updates
71
+ };
72
+ saveConfig(config);
73
+ }
74
+ function getSelectedRemote(repoPath, availableRemotes) {
75
+ const repoConfig = getRepoConfig(repoPath);
76
+ if (repoConfig.selectedRemote && availableRemotes.includes(repoConfig.selectedRemote)) {
77
+ return repoConfig.selectedRemote;
78
+ }
79
+ if (availableRemotes.includes("origin")) {
80
+ return "origin";
81
+ }
82
+ return availableRemotes[0] ?? null;
83
+ }
84
+
15
85
  // src/lib/github/git.ts
16
86
  import { execSync } from "child_process";
17
87
  function isGitRepo() {
@@ -122,9 +192,7 @@ async function isGhAuthenticated() {
122
192
  function getRepoFromRemote(remoteUrl) {
123
193
  const sshMatch = remoteUrl.match(/git@github\.com:(.+?)(?:\.git)?$/);
124
194
  if (sshMatch) return sshMatch[1].replace(/\.git$/, "");
125
- const httpsMatch = remoteUrl.match(
126
- /https:\/\/github\.com\/(.+?)(?:\.git)?$/
127
- );
195
+ const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/(.+?)(?:\.git)?$/);
128
196
  if (httpsMatch) return httpsMatch[1].replace(/\.git$/, "");
129
197
  return null;
130
198
  }
@@ -145,9 +213,7 @@ async function listPRsForBranch(branch, repo) {
145
213
  }
146
214
  const fields = "number,title,state,author,createdAt,isDraft";
147
215
  try {
148
- const { stdout } = await execAsync(
149
- `gh pr view --json ${fields} 2>/dev/null`
150
- );
216
+ const { stdout } = await execAsync(`gh pr view --json ${fields} 2>/dev/null`);
151
217
  const pr = JSON.parse(stdout);
152
218
  return { success: true, data: [pr] };
153
219
  } catch {
@@ -157,10 +223,8 @@ async function listPRsForBranch(branch, repo) {
157
223
  `gh pr list --state open --json ${fields},headRefName --repo "${repo}" 2>/dev/null`
158
224
  );
159
225
  const allPrs = JSON.parse(stdout);
160
- const prs = allPrs.filter(
161
- (pr) => pr.headRefName === branch || pr.headRefName.endsWith(`:${branch}`)
162
- );
163
- const result = prs.map(({ headRefName: _, ...rest }) => rest);
226
+ const prs = allPrs.filter((pr) => pr.headRefName === branch || pr.headRefName.endsWith(`:${branch}`));
227
+ const result = prs.map(({ headRefName: _headRefName, ...rest }) => rest);
164
228
  return { success: true, data: result };
165
229
  } catch {
166
230
  return { success: false, error: "Failed to fetch PRs", errorType: "api_error" };
@@ -200,9 +264,7 @@ async function getPRDetails(prNumber, repo) {
200
264
  "reviews",
201
265
  "statusCheckRollup"
202
266
  ].join(",");
203
- const { stdout } = await execAsync(
204
- `gh pr view ${prNumber} --json ${fields} --repo "${repo}"`
205
- );
267
+ const { stdout } = await execAsync(`gh pr view ${prNumber} --json ${fields} --repo "${repo}"`);
206
268
  const pr = JSON.parse(stdout);
207
269
  return { success: true, data: pr };
208
270
  } catch {
@@ -221,99 +283,307 @@ function openPRCreationPage(owner, branch, onComplete) {
221
283
  });
222
284
  }
223
285
 
224
- // src/lib/jira/parser.ts
225
- var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
226
- function isValidTicketKeyFormat(key) {
227
- return TICKET_KEY_PATTERN.test(key.toUpperCase());
228
- }
229
- function parseTicketKey(input) {
230
- const trimmed = input.trim();
231
- const urlMatch = trimmed.match(/\/browse\/([A-Za-z][A-Za-z0-9]+-\d+)/i);
232
- if (urlMatch) {
233
- return urlMatch[1].toUpperCase();
234
- }
235
- const upperInput = trimmed.toUpperCase();
236
- if (isValidTicketKeyFormat(upperInput)) {
237
- return upperInput;
238
- }
239
- return null;
240
- }
241
- function extractTicketKey(text) {
242
- const match = text.match(/([A-Za-z][A-Za-z0-9]+-\d+)/);
243
- if (match) {
244
- const candidate = match[1].toUpperCase();
245
- if (isValidTicketKeyFormat(candidate)) {
246
- return candidate;
247
- }
248
- }
249
- return null;
286
+ // src/hooks/useTerminalFocus.ts
287
+ import { useEffect, useState } from "react";
288
+ function useTerminalFocus() {
289
+ const [isFocused, setIsFocused] = useState(null);
290
+ const [focusCount, setFocusCount] = useState(0);
291
+ useEffect(() => {
292
+ process.stdout.write("\x1B[?1004h");
293
+ const handleData = (data) => {
294
+ const str = data.toString();
295
+ if (str.includes("\x1B[I")) {
296
+ setIsFocused(true);
297
+ setFocusCount((c) => c + 1);
298
+ }
299
+ if (str.includes("\x1B[O")) {
300
+ setIsFocused(false);
301
+ }
302
+ };
303
+ process.stdin.on("data", handleData);
304
+ return () => {
305
+ process.stdout.write("\x1B[?1004l");
306
+ process.stdin.off("data", handleData);
307
+ };
308
+ }, []);
309
+ return { isFocused, focusCount };
250
310
  }
251
311
 
252
- // src/lib/config/index.ts
253
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
254
- import { homedir } from "os";
255
- import { dirname, join } from "path";
256
- var CONFIG_PATH = join(homedir(), ".clairo", "config.json");
257
- var DEFAULT_CONFIG = {};
258
- function loadConfig() {
259
- try {
260
- if (!existsSync(CONFIG_PATH)) {
261
- return DEFAULT_CONFIG;
312
+ // src/hooks/github/useGitRepo.ts
313
+ function useGitRepo() {
314
+ const [isRepo, setIsRepo] = useState2(null);
315
+ const [repoPath, setRepoPath] = useState2(null);
316
+ const [remotes, setRemotes] = useState2([]);
317
+ const [currentBranch, setCurrentBranch] = useState2(null);
318
+ const [selectedRemote, setSelectedRemote] = useState2(null);
319
+ const [loading, setLoading] = useState2(true);
320
+ const [error, setError] = useState2(void 0);
321
+ const { focusCount } = useTerminalFocus();
322
+ const currentRepoSlug = useMemo(() => {
323
+ if (!selectedRemote) return null;
324
+ const remote = remotes.find((r) => r.name === selectedRemote);
325
+ if (!remote) return null;
326
+ return getRepoFromRemote(remote.url);
327
+ }, [selectedRemote, remotes]);
328
+ useEffect2(() => {
329
+ const gitRepoCheck = isGitRepo();
330
+ setIsRepo(gitRepoCheck);
331
+ if (!gitRepoCheck) {
332
+ setLoading(false);
333
+ setError("Not a git repository");
334
+ duckEvents.emit("error");
335
+ return;
262
336
  }
263
- const content = readFileSync(CONFIG_PATH, "utf-8");
264
- return JSON.parse(content);
265
- } catch {
266
- return DEFAULT_CONFIG;
267
- }
268
- }
269
- function saveConfig(config) {
270
- const dir = dirname(CONFIG_PATH);
271
- if (!existsSync(dir)) {
272
- mkdirSync(dir, { recursive: true });
273
- }
274
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
275
- }
276
-
277
- // src/lib/github/config.ts
278
- function getRepoConfig(repoPath) {
279
- const config = loadConfig();
280
- const repos = config.repositories ?? {};
281
- return repos[repoPath] ?? {};
282
- }
283
- function updateRepoConfig(repoPath, updates) {
284
- const config = loadConfig();
285
- if (!config.repositories) {
286
- config.repositories = {};
287
- }
288
- config.repositories[repoPath] = {
289
- ...config.repositories[repoPath],
290
- ...updates
337
+ const rootResult = getRepoRoot();
338
+ if (rootResult.success) {
339
+ setRepoPath(rootResult.data);
340
+ }
341
+ const branchResult = getCurrentBranch();
342
+ if (branchResult.success) {
343
+ setCurrentBranch(branchResult.data);
344
+ }
345
+ const remotesResult = listRemotes();
346
+ if (!remotesResult.success) {
347
+ setError(remotesResult.error);
348
+ duckEvents.emit("error");
349
+ setLoading(false);
350
+ return;
351
+ }
352
+ setRemotes(remotesResult.data);
353
+ const remoteNames = remotesResult.data.map((r) => r.name);
354
+ const defaultRemote = getSelectedRemote(rootResult.success ? rootResult.data : "", remoteNames);
355
+ setSelectedRemote(defaultRemote);
356
+ setLoading(false);
357
+ }, []);
358
+ useEffect2(() => {
359
+ if (!isRepo || focusCount === 0) return;
360
+ const result = getCurrentBranch();
361
+ if (result.success && result.data !== currentBranch) {
362
+ setCurrentBranch(result.data);
363
+ }
364
+ }, [isRepo, focusCount]);
365
+ const selectRemote = useCallback(
366
+ (remoteName) => {
367
+ setSelectedRemote(remoteName);
368
+ if (repoPath) {
369
+ updateRepoConfig(repoPath, { selectedRemote: remoteName });
370
+ }
371
+ },
372
+ [repoPath]
373
+ );
374
+ const refreshBranch = useCallback(() => {
375
+ const branchResult = getCurrentBranch();
376
+ if (branchResult.success) {
377
+ setCurrentBranch(branchResult.data);
378
+ return branchResult.data;
379
+ }
380
+ return null;
381
+ }, []);
382
+ return {
383
+ isRepo,
384
+ repoPath,
385
+ remotes,
386
+ currentBranch,
387
+ selectedRemote,
388
+ currentRepoSlug,
389
+ selectRemote,
390
+ refreshBranch,
391
+ loading,
392
+ error
291
393
  };
292
- saveConfig(config);
293
- }
294
- function getSelectedRemote(repoPath, availableRemotes) {
295
- const repoConfig = getRepoConfig(repoPath);
296
- if (repoConfig.selectedRemote && availableRemotes.includes(repoConfig.selectedRemote)) {
297
- return repoConfig.selectedRemote;
298
- }
299
- if (availableRemotes.includes("origin")) {
300
- return "origin";
301
- }
302
- return availableRemotes[0] ?? null;
303
394
  }
304
395
 
305
- // src/lib/jira/config.ts
306
- function isJiraConfigured(repoPath) {
307
- const config = getRepoConfig(repoPath);
308
- return !!(config.jiraSiteUrl && config.jiraEmail && config.jiraApiToken);
309
- }
310
- function getJiraSiteUrl(repoPath) {
311
- const config = getRepoConfig(repoPath);
312
- return config.jiraSiteUrl ?? null;
313
- }
314
- function setJiraSiteUrl(repoPath, siteUrl) {
315
- updateRepoConfig(repoPath, { jiraSiteUrl: siteUrl });
316
- }
396
+ // src/hooks/github/usePRPolling.ts
397
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef, useState as useState3 } from "react";
398
+ function usePRPolling() {
399
+ const prNumbersBeforeCreate = useRef(/* @__PURE__ */ new Set());
400
+ const pollingIntervalRef = useRef(null);
401
+ const [isPolling, setIsPolling] = useState3(false);
402
+ const stopPolling = useCallback2(() => {
403
+ if (pollingIntervalRef.current) {
404
+ clearInterval(pollingIntervalRef.current);
405
+ pollingIntervalRef.current = null;
406
+ }
407
+ setIsPolling(false);
408
+ }, []);
409
+ const startPolling = useCallback2(
410
+ (options) => {
411
+ const {
412
+ branch,
413
+ repoSlug,
414
+ existingPRNumbers,
415
+ onNewPR,
416
+ onPRsUpdated,
417
+ maxAttempts = 24,
418
+ pollInterval = 5e3
419
+ } = options;
420
+ stopPolling();
421
+ prNumbersBeforeCreate.current = new Set(existingPRNumbers);
422
+ let attempts = 0;
423
+ setIsPolling(true);
424
+ pollingIntervalRef.current = setInterval(async () => {
425
+ attempts++;
426
+ if (attempts > maxAttempts) {
427
+ stopPolling();
428
+ return;
429
+ }
430
+ const result = await listPRsForBranch(branch, repoSlug);
431
+ if (result.success) {
432
+ onPRsUpdated(result.data);
433
+ const newPR = result.data.find((pr) => !prNumbersBeforeCreate.current.has(pr.number));
434
+ if (newPR) {
435
+ stopPolling();
436
+ onNewPR(newPR);
437
+ }
438
+ }
439
+ }, pollInterval);
440
+ },
441
+ [stopPolling]
442
+ );
443
+ useEffect3(() => {
444
+ return () => {
445
+ if (pollingIntervalRef.current) {
446
+ clearInterval(pollingIntervalRef.current);
447
+ }
448
+ };
449
+ }, []);
450
+ return {
451
+ startPolling,
452
+ stopPolling,
453
+ isPolling
454
+ };
455
+ }
456
+
457
+ // src/hooks/github/usePullRequests.ts
458
+ import { useCallback as useCallback3, useState as useState4 } from "react";
459
+ function usePullRequests() {
460
+ const [prs, setPrs] = useState4([]);
461
+ const [selectedPR, setSelectedPR] = useState4(null);
462
+ const [prDetails, setPrDetails] = useState4(null);
463
+ const [loading, setLoading] = useState4({
464
+ prs: false,
465
+ details: false
466
+ });
467
+ const [errors, setErrors] = useState4({});
468
+ const refreshPRs = useCallback3(async (branch, repoSlug) => {
469
+ setLoading((prev) => ({ ...prev, prs: true }));
470
+ setPrs([]);
471
+ setSelectedPR(null);
472
+ setPrDetails(null);
473
+ try {
474
+ const result = await listPRsForBranch(branch, repoSlug);
475
+ if (result.success) {
476
+ setPrs(result.data);
477
+ setErrors((prev) => ({ ...prev, prs: void 0 }));
478
+ return result.data[0] ?? null;
479
+ } else {
480
+ setErrors((prev) => ({ ...prev, prs: result.error }));
481
+ return null;
482
+ }
483
+ } catch (err) {
484
+ setErrors((prev) => ({ ...prev, prs: String(err) }));
485
+ duckEvents.emit("error");
486
+ return null;
487
+ } finally {
488
+ setLoading((prev) => ({ ...prev, prs: false }));
489
+ }
490
+ }, []);
491
+ const refreshDetails = useCallback3(async (pr, repoSlug) => {
492
+ setLoading((prev) => ({ ...prev, details: true }));
493
+ try {
494
+ const result = await getPRDetails(pr.number, repoSlug);
495
+ if (result.success) {
496
+ setPrDetails(result.data);
497
+ setErrors((prev) => ({ ...prev, details: void 0 }));
498
+ } else {
499
+ setErrors((prev) => ({ ...prev, details: result.error }));
500
+ }
501
+ } catch (err) {
502
+ setErrors((prev) => ({ ...prev, details: String(err) }));
503
+ duckEvents.emit("error");
504
+ } finally {
505
+ setLoading((prev) => ({ ...prev, details: false }));
506
+ }
507
+ }, []);
508
+ const fetchPRsAndDetails = useCallback3(
509
+ async (branch, repoSlug) => {
510
+ const firstPR = await refreshPRs(branch, repoSlug);
511
+ if (firstPR) {
512
+ setSelectedPR(firstPR);
513
+ refreshDetails(firstPR, repoSlug);
514
+ }
515
+ },
516
+ [refreshPRs, refreshDetails]
517
+ );
518
+ const selectPR = useCallback3(
519
+ (pr, repoSlug) => {
520
+ setSelectedPR(pr);
521
+ if (repoSlug) {
522
+ refreshDetails(pr, repoSlug);
523
+ }
524
+ },
525
+ [refreshDetails]
526
+ );
527
+ const setError = useCallback3((key, message) => {
528
+ setErrors((prev) => ({ ...prev, [key]: message }));
529
+ }, []);
530
+ return {
531
+ prs,
532
+ selectedPR,
533
+ prDetails,
534
+ refreshPRs,
535
+ refreshDetails,
536
+ fetchPRsAndDetails,
537
+ selectPR,
538
+ loading,
539
+ errors,
540
+ setError,
541
+ // Expose setters for cases where external code needs to update state directly
542
+ setPrs,
543
+ setSelectedPR
544
+ };
545
+ }
546
+
547
+ // src/lib/jira/parser.ts
548
+ var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
549
+ function isValidTicketKeyFormat(key) {
550
+ return TICKET_KEY_PATTERN.test(key.toUpperCase());
551
+ }
552
+ function parseTicketKey(input) {
553
+ const trimmed = input.trim();
554
+ const urlMatch = trimmed.match(/\/browse\/([A-Za-z][A-Za-z0-9]+-\d+)/i);
555
+ if (urlMatch) {
556
+ return urlMatch[1].toUpperCase();
557
+ }
558
+ const upperInput = trimmed.toUpperCase();
559
+ if (isValidTicketKeyFormat(upperInput)) {
560
+ return upperInput;
561
+ }
562
+ return null;
563
+ }
564
+ function extractTicketKey(text) {
565
+ const match = text.match(/([A-Za-z][A-Za-z0-9]+-\d+)/);
566
+ if (match) {
567
+ const candidate = match[1].toUpperCase();
568
+ if (isValidTicketKeyFormat(candidate)) {
569
+ return candidate;
570
+ }
571
+ }
572
+ return null;
573
+ }
574
+
575
+ // src/lib/jira/config.ts
576
+ function isJiraConfigured(repoPath) {
577
+ const config = getRepoConfig(repoPath);
578
+ return !!(config.jiraSiteUrl && config.jiraEmail && config.jiraApiToken);
579
+ }
580
+ function getJiraSiteUrl(repoPath) {
581
+ const config = getRepoConfig(repoPath);
582
+ return config.jiraSiteUrl ?? null;
583
+ }
584
+ function setJiraSiteUrl(repoPath, siteUrl) {
585
+ updateRepoConfig(repoPath, { jiraSiteUrl: siteUrl });
586
+ }
317
587
  function getJiraCredentials(repoPath) {
318
588
  const config = getRepoConfig(repoPath);
319
589
  return {
@@ -498,10 +768,10 @@ async function applyTransition(auth, ticketKey, transitionId) {
498
768
  }
499
769
 
500
770
  // src/lib/logs/index.ts
501
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, appendFileSync, writeFileSync as writeFileSync2 } from "fs";
771
+ import { spawnSync } from "child_process";
772
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, writeFileSync as writeFileSync2 } from "fs";
502
773
  import { homedir as homedir2 } from "os";
503
774
  import { join as join2 } from "path";
504
- import { spawnSync } from "child_process";
505
775
  var LOGS_DIRECTORY = join2(homedir2(), ".clairo", "logs");
506
776
  function ensureLogsDirectory() {
507
777
  if (!existsSync2(LOGS_DIRECTORY)) {
@@ -567,319 +837,63 @@ function createEmptyLog(date) {
567
837
  }
568
838
  const header = `# Log - ${date}
569
839
  `;
570
- writeFileSync2(filePath, header);
571
- }
572
- function appendToLog(date, entry) {
573
- ensureLogsDirectory();
574
- const filePath = getLogFilePath(date);
575
- if (!existsSync2(filePath)) {
576
- const header = `# Log - ${date}
577
-
578
- `;
579
- writeFileSync2(filePath, header);
580
- }
581
- appendFileSync(filePath, entry);
582
- }
583
- function openLogInEditor(date) {
584
- const filePath = getLogFilePath(date);
585
- if (!existsSync2(filePath)) {
586
- return false;
587
- }
588
- const timestamp = formatTimestamp();
589
- appendFileSync(filePath, `
590
- ## ${timestamp}
591
-
592
- `);
593
- const editor = process.env.VISUAL || process.env.EDITOR || "vi";
594
- const result = spawnSync(editor, [filePath], {
595
- stdio: "inherit"
596
- });
597
- process.stdout.write("\x1B[2J\x1B[H");
598
- process.stdout.emit("resize");
599
- return result.status === 0;
600
- }
601
-
602
- // src/lib/logs/logger.ts
603
- function logPRCreated(prNumber, title, jiraTickets) {
604
- const timestamp = formatTimestamp();
605
- const today = getTodayDate();
606
- let entry = `## ${timestamp} - Created PR #${prNumber}
607
-
608
- ${title}
609
- `;
610
- if (jiraTickets.length > 0) {
611
- entry += `Jira: ${jiraTickets.join(", ")}
612
- `;
613
- }
614
- entry += "\n";
615
- appendToLog(today, entry);
616
- }
617
- function logJiraStatusChanged(ticketKey, ticketName, oldStatus, newStatus) {
618
- const timestamp = formatTimestamp();
619
- const today = getTodayDate();
620
- const entry = `## ${timestamp} - Updated Jira ticket
621
-
622
- ${ticketKey}: ${ticketName}
623
- ${oldStatus} \u2192 ${newStatus}
624
-
625
- `;
626
- appendToLog(today, entry);
627
- }
628
-
629
- // src/hooks/github/useGitRepo.ts
630
- import { useCallback, useEffect as useEffect2, useMemo, useState as useState2 } from "react";
631
-
632
- // src/hooks/useTerminalFocus.ts
633
- import { useEffect, useState } from "react";
634
- function useTerminalFocus() {
635
- const [isFocused, setIsFocused] = useState(null);
636
- const [focusCount, setFocusCount] = useState(0);
637
- useEffect(() => {
638
- process.stdout.write("\x1B[?1004h");
639
- const handleData = (data) => {
640
- const str = data.toString();
641
- if (str.includes("\x1B[I")) {
642
- setIsFocused(true);
643
- setFocusCount((c) => c + 1);
644
- }
645
- if (str.includes("\x1B[O")) {
646
- setIsFocused(false);
647
- }
648
- };
649
- process.stdin.on("data", handleData);
650
- return () => {
651
- process.stdout.write("\x1B[?1004l");
652
- process.stdin.off("data", handleData);
653
- };
654
- }, []);
655
- return { isFocused, focusCount };
656
- }
657
-
658
- // src/hooks/github/useGitRepo.ts
659
- function useGitRepo() {
660
- const [isRepo, setIsRepo] = useState2(null);
661
- const [repoPath, setRepoPath] = useState2(null);
662
- const [remotes, setRemotes] = useState2([]);
663
- const [currentBranch, setCurrentBranch] = useState2(null);
664
- const [selectedRemote, setSelectedRemote] = useState2(null);
665
- const [loading, setLoading] = useState2(true);
666
- const [error, setError] = useState2(void 0);
667
- const { focusCount } = useTerminalFocus();
668
- const currentRepoSlug = useMemo(() => {
669
- if (!selectedRemote) return null;
670
- const remote = remotes.find((r) => r.name === selectedRemote);
671
- if (!remote) return null;
672
- return getRepoFromRemote(remote.url);
673
- }, [selectedRemote, remotes]);
674
- useEffect2(() => {
675
- const gitRepoCheck = isGitRepo();
676
- setIsRepo(gitRepoCheck);
677
- if (!gitRepoCheck) {
678
- setLoading(false);
679
- setError("Not a git repository");
680
- return;
681
- }
682
- const rootResult = getRepoRoot();
683
- if (rootResult.success) {
684
- setRepoPath(rootResult.data);
685
- }
686
- const branchResult = getCurrentBranch();
687
- if (branchResult.success) {
688
- setCurrentBranch(branchResult.data);
689
- }
690
- const remotesResult = listRemotes();
691
- if (!remotesResult.success) {
692
- setError(remotesResult.error);
693
- setLoading(false);
694
- return;
695
- }
696
- setRemotes(remotesResult.data);
697
- const remoteNames = remotesResult.data.map((r) => r.name);
698
- const defaultRemote = getSelectedRemote(rootResult.success ? rootResult.data : "", remoteNames);
699
- setSelectedRemote(defaultRemote);
700
- setLoading(false);
701
- }, []);
702
- useEffect2(() => {
703
- if (!isRepo || focusCount === 0) return;
704
- const result = getCurrentBranch();
705
- if (result.success && result.data !== currentBranch) {
706
- setCurrentBranch(result.data);
707
- }
708
- }, [isRepo, focusCount]);
709
- const selectRemote = useCallback(
710
- (remoteName) => {
711
- setSelectedRemote(remoteName);
712
- if (repoPath) {
713
- updateRepoConfig(repoPath, { selectedRemote: remoteName });
714
- }
715
- },
716
- [repoPath]
717
- );
718
- const refreshBranch = useCallback(() => {
719
- const branchResult = getCurrentBranch();
720
- if (branchResult.success) {
721
- setCurrentBranch(branchResult.data);
722
- return branchResult.data;
723
- }
724
- return null;
725
- }, []);
726
- return {
727
- isRepo,
728
- repoPath,
729
- remotes,
730
- currentBranch,
731
- selectedRemote,
732
- currentRepoSlug,
733
- selectRemote,
734
- refreshBranch,
735
- loading,
736
- error
737
- };
738
- }
739
-
740
- // src/hooks/github/usePRPolling.ts
741
- import { useRef, useEffect as useEffect3, useState as useState3, useCallback as useCallback2 } from "react";
742
- function usePRPolling() {
743
- const prNumbersBeforeCreate = useRef(/* @__PURE__ */ new Set());
744
- const pollingIntervalRef = useRef(null);
745
- const [isPolling, setIsPolling] = useState3(false);
746
- const stopPolling = useCallback2(() => {
747
- if (pollingIntervalRef.current) {
748
- clearInterval(pollingIntervalRef.current);
749
- pollingIntervalRef.current = null;
750
- }
751
- setIsPolling(false);
752
- }, []);
753
- const startPolling = useCallback2(
754
- (options) => {
755
- const {
756
- branch,
757
- repoSlug,
758
- existingPRNumbers,
759
- onNewPR,
760
- onPRsUpdated,
761
- maxAttempts = 24,
762
- pollInterval = 5e3
763
- } = options;
764
- stopPolling();
765
- prNumbersBeforeCreate.current = new Set(existingPRNumbers);
766
- let attempts = 0;
767
- setIsPolling(true);
768
- pollingIntervalRef.current = setInterval(async () => {
769
- attempts++;
770
- if (attempts > maxAttempts) {
771
- stopPolling();
772
- return;
773
- }
774
- const result = await listPRsForBranch(branch, repoSlug);
775
- if (result.success) {
776
- onPRsUpdated(result.data);
777
- const newPR = result.data.find(
778
- (pr) => !prNumbersBeforeCreate.current.has(pr.number)
779
- );
780
- if (newPR) {
781
- stopPolling();
782
- onNewPR(newPR);
783
- }
784
- }
785
- }, pollInterval);
786
- },
787
- [stopPolling]
788
- );
789
- useEffect3(() => {
790
- return () => {
791
- if (pollingIntervalRef.current) {
792
- clearInterval(pollingIntervalRef.current);
793
- }
794
- };
795
- }, []);
796
- return {
797
- startPolling,
798
- stopPolling,
799
- isPolling
800
- };
801
- }
802
-
803
- // src/hooks/github/usePullRequests.ts
804
- import { useCallback as useCallback3, useState as useState4 } from "react";
805
- function usePullRequests() {
806
- const [prs, setPrs] = useState4([]);
807
- const [selectedPR, setSelectedPR] = useState4(null);
808
- const [prDetails, setPrDetails] = useState4(null);
809
- const [loading, setLoading] = useState4({
810
- prs: false,
811
- details: false
812
- });
813
- const [errors, setErrors] = useState4({});
814
- const refreshPRs = useCallback3(async (branch, repoSlug) => {
815
- setLoading((prev) => ({ ...prev, prs: true }));
816
- setPrs([]);
817
- setSelectedPR(null);
818
- setPrDetails(null);
819
- try {
820
- const result = await listPRsForBranch(branch, repoSlug);
821
- if (result.success) {
822
- setPrs(result.data);
823
- setErrors((prev) => ({ ...prev, prs: void 0 }));
824
- return result.data[0] ?? null;
825
- } else {
826
- setErrors((prev) => ({ ...prev, prs: result.error }));
827
- return null;
828
- }
829
- } catch (err) {
830
- setErrors((prev) => ({ ...prev, prs: String(err) }));
831
- return null;
832
- } finally {
833
- setLoading((prev) => ({ ...prev, prs: false }));
834
- }
835
- }, []);
836
- const refreshDetails = useCallback3(async (pr, repoSlug) => {
837
- setLoading((prev) => ({ ...prev, details: true }));
838
- try {
839
- const result = await getPRDetails(pr.number, repoSlug);
840
- if (result.success) {
841
- setPrDetails(result.data);
842
- setErrors((prev) => ({ ...prev, details: void 0 }));
843
- } else {
844
- setErrors((prev) => ({ ...prev, details: result.error }));
845
- }
846
- } catch (err) {
847
- setErrors((prev) => ({ ...prev, details: String(err) }));
848
- } finally {
849
- setLoading((prev) => ({ ...prev, details: false }));
850
- }
851
- }, []);
852
- const fetchPRsAndDetails = useCallback3(async (branch, repoSlug) => {
853
- const firstPR = await refreshPRs(branch, repoSlug);
854
- if (firstPR) {
855
- setSelectedPR(firstPR);
856
- refreshDetails(firstPR, repoSlug);
857
- }
858
- }, [refreshPRs, refreshDetails]);
859
- const selectPR = useCallback3((pr, repoSlug) => {
860
- setSelectedPR(pr);
861
- if (repoSlug) {
862
- refreshDetails(pr, repoSlug);
863
- }
864
- }, [refreshDetails]);
865
- const setError = useCallback3((key, message) => {
866
- setErrors((prev) => ({ ...prev, [key]: message }));
867
- }, []);
868
- return {
869
- prs,
870
- selectedPR,
871
- prDetails,
872
- refreshPRs,
873
- refreshDetails,
874
- fetchPRsAndDetails,
875
- selectPR,
876
- loading,
877
- errors,
878
- setError,
879
- // Expose setters for cases where external code needs to update state directly
880
- setPrs,
881
- setSelectedPR
882
- };
840
+ writeFileSync2(filePath, header);
841
+ }
842
+ function appendToLog(date, entry) {
843
+ ensureLogsDirectory();
844
+ const filePath = getLogFilePath(date);
845
+ if (!existsSync2(filePath)) {
846
+ const header = `# Log - ${date}
847
+
848
+ `;
849
+ writeFileSync2(filePath, header);
850
+ }
851
+ appendFileSync(filePath, entry);
852
+ }
853
+ function openLogInEditor(date) {
854
+ const filePath = getLogFilePath(date);
855
+ if (!existsSync2(filePath)) {
856
+ return false;
857
+ }
858
+ const timestamp = formatTimestamp();
859
+ appendFileSync(filePath, `
860
+ ## ${timestamp}
861
+
862
+ `);
863
+ const editor = process.env.VISUAL || process.env.EDITOR || "vi";
864
+ const result = spawnSync(editor, [filePath], {
865
+ stdio: "inherit"
866
+ });
867
+ process.stdout.write("\x1B[2J\x1B[H");
868
+ process.stdout.emit("resize");
869
+ return result.status === 0;
870
+ }
871
+
872
+ // src/lib/logs/logger.ts
873
+ function logPRCreated(prNumber, title, jiraTickets) {
874
+ const timestamp = formatTimestamp();
875
+ const today = getTodayDate();
876
+ let entry = `## ${timestamp} - Created PR #${prNumber}
877
+
878
+ ${title}
879
+ `;
880
+ if (jiraTickets.length > 0) {
881
+ entry += `Jira: ${jiraTickets.join(", ")}
882
+ `;
883
+ }
884
+ entry += "\n";
885
+ appendToLog(today, entry);
886
+ }
887
+ function logJiraStatusChanged(ticketKey, ticketName, oldStatus, newStatus) {
888
+ const timestamp = formatTimestamp();
889
+ const today = getTodayDate();
890
+ const entry = `## ${timestamp} - Updated Jira ticket
891
+
892
+ ${ticketKey}: ${ticketName}
893
+ ${oldStatus} \u2192 ${newStatus}
894
+
895
+ `;
896
+ appendToLog(today, entry);
883
897
  }
884
898
 
885
899
  // src/components/github/PRDetailsBox.tsx
@@ -889,10 +903,10 @@ import { Box as Box2, Text as Text2, useInput, useStdout } from "ink";
889
903
  import { ScrollView } from "ink-scroll-view";
890
904
 
891
905
  // src/components/ui/Markdown.tsx
906
+ import Table from "cli-table3";
907
+ import { marked } from "marked";
892
908
  import { Box, Text } from "ink";
893
909
  import Link from "ink-link";
894
- import { marked } from "marked";
895
- import Table from "cli-table3";
896
910
  import { jsx, jsxs } from "react/jsx-runtime";
897
911
  function Markdown({ children }) {
898
912
  const tokens = marked.lexer(children);
@@ -904,10 +918,12 @@ function TokenRenderer({ token }) {
904
918
  case "heading":
905
919
  return /* @__PURE__ */ jsx(Box, { marginTop: token.depth === 1 ? 0 : 1, children: /* @__PURE__ */ jsx(Text, { bold: true, underline: token.depth === 1, children: renderInline(token.tokens) }) });
906
920
  case "paragraph": {
907
- const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some((t) => {
908
- var _a2;
909
- return t.type === "link" || t.type === "strong" && "tokens" in t && ((_a2 = t.tokens) == null ? void 0 : _a2.some((st) => st.type === "link"));
910
- });
921
+ const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some(
922
+ (t) => {
923
+ var _a2;
924
+ return t.type === "link" || t.type === "strong" && "tokens" in t && ((_a2 = t.tokens) == null ? void 0 : _a2.some((st) => st.type === "link"));
925
+ }
926
+ );
911
927
  if (hasLinks) {
912
928
  return /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: renderInline(token.tokens) });
913
929
  }
@@ -1153,10 +1169,387 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
1153
1169
  ] });
1154
1170
  }
1155
1171
 
1156
- // src/components/github/PullRequestsBox.tsx
1157
- import { useEffect as useEffect4, useState as useState5 } from "react";
1158
- import { TitledBox } from "@mishieck/ink-titled-box";
1159
- import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
1172
+ // src/components/github/PullRequestsBox.tsx
1173
+ import open2 from "open";
1174
+ import { useEffect as useEffect7, useState as useState10 } from "react";
1175
+ import { TitledBox } from "@mishieck/ink-titled-box";
1176
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
1177
+ import { ScrollView as ScrollView2 } from "ink-scroll-view";
1178
+
1179
+ // src/hooks/jira/useJiraTickets.ts
1180
+ import { useCallback as useCallback4, useState as useState5 } from "react";
1181
+ function useJiraTickets() {
1182
+ const [jiraState, setJiraState] = useState5("not_configured");
1183
+ const [tickets, setTickets] = useState5([]);
1184
+ const [loading, setLoading] = useState5({ configure: false, link: false });
1185
+ const [errors, setErrors] = useState5({});
1186
+ const initializeJiraState = useCallback4(async (repoPath, currentBranch, repoSlug) => {
1187
+ if (!isJiraConfigured(repoPath)) {
1188
+ setJiraState("not_configured");
1189
+ setTickets([]);
1190
+ return;
1191
+ }
1192
+ const linkedTickets = getLinkedTickets(repoPath, currentBranch);
1193
+ if (linkedTickets.length > 0) {
1194
+ setTickets(linkedTickets);
1195
+ setJiraState("has_tickets");
1196
+ return;
1197
+ }
1198
+ let ticketKey = extractTicketKey(currentBranch);
1199
+ if (!ticketKey && repoSlug) {
1200
+ const prResult = await listPRsForBranch(currentBranch, repoSlug);
1201
+ if (prResult.success && prResult.data.length > 0) {
1202
+ ticketKey = extractTicketKey(prResult.data[0].title);
1203
+ }
1204
+ }
1205
+ if (!ticketKey) {
1206
+ setTickets([]);
1207
+ setJiraState("no_tickets");
1208
+ return;
1209
+ }
1210
+ const siteUrl = getJiraSiteUrl(repoPath);
1211
+ const creds = getJiraCredentials(repoPath);
1212
+ if (!siteUrl || !creds.email || !creds.apiToken) {
1213
+ setTickets([]);
1214
+ setJiraState("no_tickets");
1215
+ return;
1216
+ }
1217
+ const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
1218
+ const result = await getIssue(auth, ticketKey);
1219
+ if (result.success) {
1220
+ const linkedTicket = {
1221
+ key: result.data.key,
1222
+ summary: result.data.fields.summary,
1223
+ status: result.data.fields.status.name,
1224
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1225
+ };
1226
+ addLinkedTicket(repoPath, currentBranch, linkedTicket);
1227
+ setTickets([linkedTicket]);
1228
+ setJiraState("has_tickets");
1229
+ } else {
1230
+ setTickets([]);
1231
+ setJiraState("no_tickets");
1232
+ }
1233
+ }, []);
1234
+ const refreshTickets = useCallback4((repoPath, currentBranch) => {
1235
+ const linkedTickets = getLinkedTickets(repoPath, currentBranch);
1236
+ setTickets(linkedTickets);
1237
+ setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
1238
+ }, []);
1239
+ const configureJira = useCallback4(
1240
+ async (repoPath, siteUrl, email, apiToken) => {
1241
+ setLoading((prev) => ({ ...prev, configure: true }));
1242
+ setErrors((prev) => ({ ...prev, configure: void 0 }));
1243
+ const auth = { siteUrl, email, apiToken };
1244
+ const result = await validateCredentials(auth);
1245
+ if (!result.success) {
1246
+ setErrors((prev) => ({ ...prev, configure: result.error }));
1247
+ duckEvents.emit("error");
1248
+ setLoading((prev) => ({ ...prev, configure: false }));
1249
+ return false;
1250
+ }
1251
+ setJiraSiteUrl(repoPath, siteUrl);
1252
+ setJiraCredentials(repoPath, email, apiToken);
1253
+ setJiraState("no_tickets");
1254
+ duckEvents.emit("jira:configured");
1255
+ setLoading((prev) => ({ ...prev, configure: false }));
1256
+ return true;
1257
+ },
1258
+ []
1259
+ );
1260
+ const linkTicket = useCallback4(
1261
+ async (repoPath, currentBranch, ticketInput) => {
1262
+ setLoading((prev) => ({ ...prev, link: true }));
1263
+ setErrors((prev) => ({ ...prev, link: void 0 }));
1264
+ const ticketKey = parseTicketKey(ticketInput);
1265
+ if (!ticketKey) {
1266
+ setErrors((prev) => ({ ...prev, link: "Invalid ticket format. Use PROJ-123 or a Jira URL." }));
1267
+ duckEvents.emit("error");
1268
+ setLoading((prev) => ({ ...prev, link: false }));
1269
+ return false;
1270
+ }
1271
+ const siteUrl = getJiraSiteUrl(repoPath);
1272
+ const creds = getJiraCredentials(repoPath);
1273
+ if (!siteUrl || !creds.email || !creds.apiToken) {
1274
+ setErrors((prev) => ({ ...prev, link: "Jira not configured" }));
1275
+ duckEvents.emit("error");
1276
+ setLoading((prev) => ({ ...prev, link: false }));
1277
+ return false;
1278
+ }
1279
+ const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
1280
+ const result = await getIssue(auth, ticketKey);
1281
+ if (!result.success) {
1282
+ setErrors((prev) => ({ ...prev, link: result.error }));
1283
+ duckEvents.emit("error");
1284
+ setLoading((prev) => ({ ...prev, link: false }));
1285
+ return false;
1286
+ }
1287
+ const linkedTicket = {
1288
+ key: result.data.key,
1289
+ summary: result.data.fields.summary,
1290
+ status: result.data.fields.status.name,
1291
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1292
+ };
1293
+ addLinkedTicket(repoPath, currentBranch, linkedTicket);
1294
+ const newTickets = getLinkedTickets(repoPath, currentBranch);
1295
+ setTickets(newTickets);
1296
+ setJiraState("has_tickets");
1297
+ duckEvents.emit("jira:linked");
1298
+ setLoading((prev) => ({ ...prev, link: false }));
1299
+ return true;
1300
+ },
1301
+ []
1302
+ );
1303
+ const unlinkTicket = useCallback4((repoPath, currentBranch, ticketKey) => {
1304
+ removeLinkedTicket(repoPath, currentBranch, ticketKey);
1305
+ }, []);
1306
+ const clearError = useCallback4((key) => {
1307
+ setErrors((prev) => ({ ...prev, [key]: void 0 }));
1308
+ }, []);
1309
+ return {
1310
+ jiraState,
1311
+ tickets,
1312
+ loading,
1313
+ errors,
1314
+ initializeJiraState,
1315
+ refreshTickets,
1316
+ configureJira,
1317
+ linkTicket,
1318
+ unlinkTicket,
1319
+ clearError
1320
+ };
1321
+ }
1322
+
1323
+ // src/hooks/logs/useLogs.ts
1324
+ import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef3, useState as useState6 } from "react";
1325
+ function useLogs() {
1326
+ const [logFiles, setLogFiles] = useState6([]);
1327
+ const [selectedDate, setSelectedDate] = useState6(null);
1328
+ const [logContent, setLogContent] = useState6(null);
1329
+ const [highlightedIndex, setHighlightedIndex] = useState6(0);
1330
+ const initializedRef = useRef3(false);
1331
+ const loadLogContent = useCallback5((date) => {
1332
+ if (!date) {
1333
+ setLogContent(null);
1334
+ return null;
1335
+ }
1336
+ const content = readLog(date);
1337
+ setLogContent(content);
1338
+ return content;
1339
+ }, []);
1340
+ const refreshLogFiles = useCallback5(() => {
1341
+ const files = listLogFiles();
1342
+ setLogFiles(files);
1343
+ return files;
1344
+ }, []);
1345
+ const initialize = useCallback5(() => {
1346
+ const files = listLogFiles();
1347
+ setLogFiles(files);
1348
+ if (files.length === 0) return;
1349
+ const today = getTodayDate();
1350
+ const todayFile = files.find((f) => f.date === today);
1351
+ if (todayFile) {
1352
+ setSelectedDate(todayFile.date);
1353
+ const idx = files.findIndex((f) => f.date === today);
1354
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1355
+ loadLogContent(todayFile.date);
1356
+ } else {
1357
+ setSelectedDate(files[0].date);
1358
+ setHighlightedIndex(0);
1359
+ loadLogContent(files[0].date);
1360
+ }
1361
+ }, [loadLogContent]);
1362
+ useEffect4(() => {
1363
+ if (initializedRef.current) return;
1364
+ initializedRef.current = true;
1365
+ initialize();
1366
+ }, [initialize]);
1367
+ const selectDate = useCallback5(
1368
+ (date) => {
1369
+ setSelectedDate(date);
1370
+ loadLogContent(date);
1371
+ },
1372
+ [loadLogContent]
1373
+ );
1374
+ const refresh = useCallback5(() => {
1375
+ refreshLogFiles();
1376
+ if (selectedDate) {
1377
+ loadLogContent(selectedDate);
1378
+ }
1379
+ }, [refreshLogFiles, selectedDate, loadLogContent]);
1380
+ const handleExternalLogUpdate = useCallback5(() => {
1381
+ const files = listLogFiles();
1382
+ setLogFiles(files);
1383
+ const today = getTodayDate();
1384
+ if (selectedDate === today) {
1385
+ loadLogContent(today);
1386
+ } else if (!selectedDate && files.length > 0) {
1387
+ const todayFile = files.find((f) => f.date === today);
1388
+ if (todayFile) {
1389
+ setSelectedDate(today);
1390
+ const idx = files.findIndex((f) => f.date === today);
1391
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1392
+ loadLogContent(today);
1393
+ }
1394
+ }
1395
+ }, [selectedDate, loadLogContent]);
1396
+ const handleLogCreated = useCallback5(() => {
1397
+ const files = listLogFiles();
1398
+ setLogFiles(files);
1399
+ const today = getTodayDate();
1400
+ setSelectedDate(today);
1401
+ const idx = files.findIndex((f) => f.date === today);
1402
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1403
+ loadLogContent(today);
1404
+ }, [loadLogContent]);
1405
+ return {
1406
+ logFiles,
1407
+ selectedDate,
1408
+ logContent,
1409
+ highlightedIndex,
1410
+ setHighlightedIndex,
1411
+ selectDate,
1412
+ refresh,
1413
+ handleExternalLogUpdate,
1414
+ handleLogCreated
1415
+ };
1416
+ }
1417
+
1418
+ // src/hooks/useModal.ts
1419
+ import { useCallback as useCallback6, useState as useState7 } from "react";
1420
+ function useModal() {
1421
+ const [modalType, setModalType] = useState7("none");
1422
+ const open4 = useCallback6((type) => setModalType(type), []);
1423
+ const close = useCallback6(() => setModalType("none"), []);
1424
+ const isOpen = modalType !== "none";
1425
+ return {
1426
+ type: modalType,
1427
+ isOpen,
1428
+ open: open4,
1429
+ close
1430
+ };
1431
+ }
1432
+
1433
+ // src/hooks/useListNavigation.ts
1434
+ import { useCallback as useCallback7, useState as useState8 } from "react";
1435
+ function useListNavigation(length) {
1436
+ const [index, setIndex] = useState8(0);
1437
+ const prev = useCallback7(() => {
1438
+ setIndex((i) => Math.max(0, i - 1));
1439
+ }, []);
1440
+ const next = useCallback7(() => {
1441
+ setIndex((i) => Math.min(length - 1, i + 1));
1442
+ }, [length]);
1443
+ const clampedIndex = Math.min(index, Math.max(0, length - 1));
1444
+ const reset = useCallback7(() => setIndex(0), []);
1445
+ return {
1446
+ index: length === 0 ? 0 : clampedIndex,
1447
+ prev,
1448
+ next,
1449
+ reset,
1450
+ setIndex
1451
+ };
1452
+ }
1453
+
1454
+ // src/hooks/useScrollToIndex.ts
1455
+ import { useEffect as useEffect5, useRef as useRef4 } from "react";
1456
+ function useScrollToIndex(index) {
1457
+ const scrollRef = useRef4(null);
1458
+ useEffect5(() => {
1459
+ const ref = scrollRef.current;
1460
+ if (!ref) return;
1461
+ const pos = ref.getItemPosition(index);
1462
+ const viewportHeight = ref.getViewportHeight();
1463
+ const scrollOffset = ref.getScrollOffset();
1464
+ if (!pos) return;
1465
+ if (pos.top < scrollOffset) {
1466
+ ref.scrollTo(pos.top);
1467
+ } else if (pos.top + pos.height > scrollOffset + viewportHeight) {
1468
+ ref.scrollTo(pos.top + pos.height - viewportHeight);
1469
+ }
1470
+ }, [index]);
1471
+ return scrollRef;
1472
+ }
1473
+
1474
+ // src/hooks/useRubberDuck.ts
1475
+ import { useCallback as useCallback8, useEffect as useEffect6, useState as useState9 } from "react";
1476
+ var DUCK_MESSAGES = [
1477
+ "Quack.",
1478
+ "Quack quack quack.",
1479
+ "Have you tried explaining it out loud?",
1480
+ "It's always DNS.",
1481
+ "Did you check the logs?",
1482
+ "Maybe add a console.log?",
1483
+ "Is it plugged in?",
1484
+ "Works on my machine.",
1485
+ "Have you tried reading the error message?",
1486
+ "I believe in you!",
1487
+ "It's probably a race condition.",
1488
+ "Have you tried turning it off and on again?",
1489
+ "Are you sure it compiled?",
1490
+ "It's not a bug, it's a feature.",
1491
+ "Did you clear the cache?",
1492
+ "Try deleting node_modules.",
1493
+ "That's quackers!",
1494
+ "Rubber duck debugging, activate!",
1495
+ "*supportive quacking*"
1496
+ ];
1497
+ var REACTION_MESSAGES = {
1498
+ "pr:merged": ["Quack! It shipped!", "Merged!", "To production we go!"],
1499
+ "pr:opened": ["A new PR! Exciting!", "Time for review!", "Fresh code incoming!"],
1500
+ "pr:reviewed": ["Feedback time!", "Reviews are in!", "*attentive quacking*"],
1501
+ "pr:approved": ["Approved!", "LGTM!", "Ship it!"],
1502
+ "pr:changes-requested": ["Some changes needed...", "Back to the drawing board!", "Iterate iterate!"],
1503
+ error: ["Uh oh...", "There there...", "*concerned quacking*", "Quack... not good."],
1504
+ "jira:transition": ["Ticket moving!", "Progress!", "Workflow in motion!"],
1505
+ "jira:linked": ["Ticket linked!", "Jira connection made!", "Tracking enabled!"],
1506
+ "jira:configured": ["Jira ready!", "Integration complete!", "Connected to Jira!"]
1507
+ };
1508
+ function useRubberDuck() {
1509
+ const [state, setState] = useState9({
1510
+ visible: false,
1511
+ message: DUCK_MESSAGES[0]
1512
+ });
1513
+ const getRandomMessage = useCallback8(() => {
1514
+ const index = Math.floor(Math.random() * DUCK_MESSAGES.length);
1515
+ return DUCK_MESSAGES[index];
1516
+ }, []);
1517
+ const toggleDuck = useCallback8(() => {
1518
+ setState((prev) => ({
1519
+ ...prev,
1520
+ visible: !prev.visible,
1521
+ message: !prev.visible ? getRandomMessage() : prev.message
1522
+ }));
1523
+ }, [getRandomMessage]);
1524
+ const quack = useCallback8(() => {
1525
+ if (state.visible) {
1526
+ setState((prev) => ({
1527
+ ...prev,
1528
+ message: getRandomMessage()
1529
+ }));
1530
+ }
1531
+ }, [state.visible, getRandomMessage]);
1532
+ const getReactionMessage = useCallback8((event) => {
1533
+ const messages = REACTION_MESSAGES[event];
1534
+ return messages[Math.floor(Math.random() * messages.length)];
1535
+ }, []);
1536
+ useEffect6(() => {
1537
+ const unsubscribe = duckEvents.subscribe((event) => {
1538
+ setState((prev) => ({
1539
+ ...prev,
1540
+ visible: true,
1541
+ message: getReactionMessage(event)
1542
+ }));
1543
+ });
1544
+ return unsubscribe;
1545
+ }, [getReactionMessage]);
1546
+ return {
1547
+ visible: state.visible,
1548
+ message: state.message,
1549
+ toggleDuck,
1550
+ quack
1551
+ };
1552
+ }
1160
1553
 
1161
1554
  // src/lib/clipboard.ts
1162
1555
  import { exec as exec2 } from "child_process";
@@ -1180,7 +1573,7 @@ async function copyToClipboard(text) {
1180
1573
  }
1181
1574
 
1182
1575
  // src/components/github/PullRequestsBox.tsx
1183
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1576
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1184
1577
  function PullRequestsBox({
1185
1578
  prs,
1186
1579
  selectedPR,
@@ -1192,9 +1585,11 @@ function PullRequestsBox({
1192
1585
  repoSlug,
1193
1586
  isFocused
1194
1587
  }) {
1195
- const [highlightedIndex, setHighlightedIndex] = useState5(0);
1588
+ const [highlightedIndex, setHighlightedIndex] = useState10(0);
1589
+ const [copied, setCopied] = useState10(false);
1590
+ const scrollRef = useScrollToIndex(highlightedIndex);
1196
1591
  const totalItems = prs.length + 1;
1197
- useEffect4(() => {
1592
+ useEffect7(() => {
1198
1593
  const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
1199
1594
  if (idx >= 0) setHighlightedIndex(idx);
1200
1595
  }, [selectedPR, prs]);
@@ -1218,54 +1613,74 @@ function PullRequestsBox({
1218
1613
  const pr = prs[highlightedIndex];
1219
1614
  const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
1220
1615
  copyToClipboard(url);
1616
+ setCopied(true);
1617
+ setTimeout(() => setCopied(false), 1500);
1618
+ }
1619
+ if (input === "o" && repoSlug && prs[highlightedIndex]) {
1620
+ const pr = prs[highlightedIndex];
1621
+ const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
1622
+ open2(url).catch(() => {
1623
+ });
1221
1624
  }
1222
1625
  },
1223
1626
  { isActive: isFocused }
1224
1627
  );
1225
1628
  const title = "[2] Pull Requests";
1226
1629
  const subtitle = branch ? ` (${branch})` : "";
1630
+ const copiedIndicator = copied ? " [Copied!]" : "";
1227
1631
  const borderColor = isFocused ? "yellow" : void 0;
1228
- return /* @__PURE__ */ jsx3(TitledBox, { borderStyle: "round", titles: [`${title}${subtitle}`], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
1229
- loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
1230
- error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
1231
- !loading && !error && /* @__PURE__ */ jsxs3(Fragment2, { children: [
1232
- prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }),
1233
- prs.map((pr, idx) => {
1234
- const isHighlighted = isFocused && idx === highlightedIndex;
1235
- const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
1236
- const cursor = isHighlighted ? ">" : " ";
1237
- const indicator = isSelected ? " *" : "";
1238
- return /* @__PURE__ */ jsxs3(Box3, { children: [
1239
- /* @__PURE__ */ jsxs3(Text3, { color: isHighlighted ? "yellow" : void 0, children: [
1240
- cursor,
1241
- " "
1242
- ] }),
1243
- /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
1244
- "#",
1245
- pr.number,
1246
- " ",
1247
- pr.isDraft ? "[Draft] " : "",
1248
- pr.title
1249
- ] }),
1250
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: indicator })
1251
- ] }, pr.number);
1252
- })
1253
- ] }),
1254
- /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
1255
- isFocused && highlightedIndex === prs.length ? "> " : " ",
1256
- "+ Create new PR"
1257
- ] })
1258
- ] }) });
1632
+ return /* @__PURE__ */ jsx3(
1633
+ TitledBox,
1634
+ {
1635
+ borderStyle: "round",
1636
+ titles: [`${title}${subtitle}${copiedIndicator}`],
1637
+ borderColor,
1638
+ height: 5,
1639
+ children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
1640
+ loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
1641
+ error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
1642
+ !loading && !error && /* @__PURE__ */ jsxs3(ScrollView2, { ref: scrollRef, children: [
1643
+ prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }, "empty"),
1644
+ prs.map((pr, idx) => {
1645
+ const isHighlighted = isFocused && idx === highlightedIndex;
1646
+ const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
1647
+ const cursor = isHighlighted ? ">" : " ";
1648
+ const indicator = isSelected ? " *" : "";
1649
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
1650
+ /* @__PURE__ */ jsxs3(Text3, { color: isHighlighted ? "yellow" : void 0, children: [
1651
+ cursor,
1652
+ " "
1653
+ ] }),
1654
+ /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
1655
+ "#",
1656
+ pr.number,
1657
+ " ",
1658
+ pr.isDraft ? "[Draft] " : "",
1659
+ pr.title
1660
+ ] }),
1661
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: indicator })
1662
+ ] }, pr.number);
1663
+ }),
1664
+ /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
1665
+ isFocused && highlightedIndex === prs.length ? "> " : " ",
1666
+ "+ Create new PR"
1667
+ ] }, "create")
1668
+ ] })
1669
+ ] })
1670
+ }
1671
+ );
1259
1672
  }
1260
1673
 
1261
1674
  // src/components/github/RemotesBox.tsx
1262
- import { useEffect as useEffect5, useState as useState6 } from "react";
1675
+ import { useEffect as useEffect8, useState as useState11 } from "react";
1263
1676
  import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
1264
1677
  import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
1678
+ import { ScrollView as ScrollView3 } from "ink-scroll-view";
1265
1679
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1266
1680
  function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
1267
- const [highlightedIndex, setHighlightedIndex] = useState6(0);
1268
- useEffect5(() => {
1681
+ const [highlightedIndex, setHighlightedIndex] = useState11(0);
1682
+ const scrollRef = useScrollToIndex(highlightedIndex);
1683
+ useEffect8(() => {
1269
1684
  const idx = remotes.findIndex((r) => r.name === selectedRemote);
1270
1685
  if (idx >= 0) setHighlightedIndex(idx);
1271
1686
  }, [selectedRemote, remotes]);
@@ -1286,11 +1701,11 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
1286
1701
  );
1287
1702
  const title = "[1] Remotes";
1288
1703
  const borderColor = isFocused ? "yellow" : void 0;
1289
- return /* @__PURE__ */ jsx4(TitledBox2, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
1704
+ return /* @__PURE__ */ jsx4(TitledBox2, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
1290
1705
  loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
1291
1706
  error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
1292
1707
  !loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
1293
- !loading && !error && remotes.map((remote, idx) => {
1708
+ !loading && !error && remotes.length > 0 && /* @__PURE__ */ jsx4(ScrollView3, { ref: scrollRef, children: remotes.map((remote, idx) => {
1294
1709
  const isHighlighted = isFocused && idx === highlightedIndex;
1295
1710
  const isSelected = remote.name === selectedRemote;
1296
1711
  const cursor = isHighlighted ? ">" : " ";
@@ -1308,7 +1723,7 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
1308
1723
  ] }),
1309
1724
  /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: indicator })
1310
1725
  ] }, remote.name);
1311
- })
1726
+ }) })
1312
1727
  ] }) });
1313
1728
  }
1314
1729
 
@@ -1318,9 +1733,9 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1318
1733
  const repo = useGitRepo();
1319
1734
  const pullRequests = usePullRequests();
1320
1735
  const polling = usePRPolling();
1321
- const [focusedBox, setFocusedBox] = useState7("remotes");
1322
- const lastFetchedRef = useRef3(null);
1323
- useEffect6(() => {
1736
+ const [focusedBox, setFocusedBox] = useState12("remotes");
1737
+ const lastFetchedRef = useRef5(null);
1738
+ useEffect9(() => {
1324
1739
  if (repo.loading || !repo.currentBranch || !repo.currentRepoSlug) return;
1325
1740
  const current = { branch: repo.currentBranch, repoSlug: repo.currentRepoSlug };
1326
1741
  const last = lastFetchedRef.current;
@@ -1328,15 +1743,15 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1328
1743
  lastFetchedRef.current = current;
1329
1744
  pullRequests.fetchPRsAndDetails(repo.currentBranch, repo.currentRepoSlug);
1330
1745
  }, [repo.loading, repo.currentBranch, repo.currentRepoSlug, pullRequests.fetchPRsAndDetails]);
1331
- useEffect6(() => {
1746
+ useEffect9(() => {
1332
1747
  if (isFocused) {
1333
1748
  repo.refreshBranch();
1334
1749
  }
1335
1750
  }, [isFocused, repo.refreshBranch]);
1336
- useEffect6(() => {
1751
+ useEffect9(() => {
1337
1752
  onFocusedBoxChange == null ? void 0 : onFocusedBoxChange(focusedBox);
1338
1753
  }, [focusedBox, onFocusedBoxChange]);
1339
- const handleRemoteSelect = useCallback4(
1754
+ const handleRemoteSelect = useCallback9(
1340
1755
  (remoteName) => {
1341
1756
  repo.selectRemote(remoteName);
1342
1757
  const remote = repo.remotes.find((r) => r.name === remoteName);
@@ -1348,28 +1763,31 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1348
1763
  },
1349
1764
  [repo.selectRemote, repo.remotes, repo.currentBranch, pullRequests.fetchPRsAndDetails]
1350
1765
  );
1351
- const handlePRSelect = useCallback4(
1766
+ const handlePRSelect = useCallback9(
1352
1767
  (pr) => {
1353
1768
  pullRequests.selectPR(pr, repo.currentRepoSlug);
1354
1769
  },
1355
1770
  [pullRequests.selectPR, repo.currentRepoSlug]
1356
1771
  );
1357
- const createPRContext = useRef3({ repo, pullRequests, onLogUpdated });
1772
+ const createPRContext = useRef5({ repo, pullRequests, onLogUpdated });
1358
1773
  createPRContext.current = { repo, pullRequests, onLogUpdated };
1359
- const handleCreatePR = useCallback4(() => {
1774
+ const handleCreatePR = useCallback9(() => {
1360
1775
  const { repo: repo2, pullRequests: pullRequests2 } = createPRContext.current;
1361
1776
  if (!repo2.currentBranch) {
1362
1777
  pullRequests2.setError("prs", "No branch detected");
1778
+ duckEvents.emit("error");
1363
1779
  return;
1364
1780
  }
1365
1781
  const remoteResult = findRemoteWithBranch(repo2.currentBranch);
1366
1782
  if (!remoteResult.success) {
1367
1783
  pullRequests2.setError("prs", "Push your branch to a remote first");
1784
+ duckEvents.emit("error");
1368
1785
  return;
1369
1786
  }
1370
1787
  openPRCreationPage(remoteResult.data.owner, repo2.currentBranch, (error) => {
1371
1788
  if (error) {
1372
1789
  pullRequests2.setError("prs", `Failed to create PR: ${error.message}`);
1790
+ duckEvents.emit("error");
1373
1791
  }
1374
1792
  });
1375
1793
  if (!repo2.currentRepoSlug) return;
@@ -1385,6 +1803,7 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1385
1803
  const ctx = createPRContext.current;
1386
1804
  const tickets = ctx.repo.repoPath && ctx.repo.currentBranch ? getLinkedTickets(ctx.repo.repoPath, ctx.repo.currentBranch).map((t) => t.key) : [];
1387
1805
  logPRCreated(newPR.number, newPR.title, tickets);
1806
+ duckEvents.emit("pr:opened");
1388
1807
  (_a = ctx.onLogUpdated) == null ? void 0 : _a.call(ctx);
1389
1808
  ctx.pullRequests.setSelectedPR(newPR);
1390
1809
  if (ctx.repo.currentRepoSlug) {
@@ -1455,296 +1874,95 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1455
1874
  }
1456
1875
 
1457
1876
  // src/components/jira/JiraView.tsx
1458
- import { useEffect as useEffect9, useRef as useRef5 } from "react";
1459
- import open2 from "open";
1460
- import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
1461
- import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
1877
+ import open3 from "open";
1878
+ import { useEffect as useEffect11, useRef as useRef6 } from "react";
1462
1879
 
1463
- // src/hooks/jira/useJiraTickets.ts
1464
- import { useCallback as useCallback5, useState as useState8 } from "react";
1465
- function useJiraTickets() {
1466
- const [jiraState, setJiraState] = useState8("not_configured");
1467
- const [tickets, setTickets] = useState8([]);
1468
- const [loading, setLoading] = useState8({ configure: false, link: false });
1469
- const [errors, setErrors] = useState8({});
1470
- const initializeJiraState = useCallback5(
1471
- async (repoPath, currentBranch, repoSlug) => {
1472
- if (!isJiraConfigured(repoPath)) {
1473
- setJiraState("not_configured");
1474
- setTickets([]);
1475
- return;
1476
- }
1477
- const linkedTickets = getLinkedTickets(repoPath, currentBranch);
1478
- if (linkedTickets.length > 0) {
1479
- setTickets(linkedTickets);
1480
- setJiraState("has_tickets");
1481
- return;
1482
- }
1483
- let ticketKey = extractTicketKey(currentBranch);
1484
- if (!ticketKey && repoSlug) {
1485
- const prResult = await listPRsForBranch(currentBranch, repoSlug);
1486
- if (prResult.success && prResult.data.length > 0) {
1487
- ticketKey = extractTicketKey(prResult.data[0].title);
1880
+ // src/components/jira/LinkTicketModal.tsx
1881
+ import { useState as useState13 } from "react";
1882
+ import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
1883
+
1884
+ // src/components/ui/TextInput.tsx
1885
+ import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
1886
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1887
+ function TextInput({ value, onChange, placeholder, isActive, mask }) {
1888
+ useInput5(
1889
+ (input, key) => {
1890
+ if (key.backspace || key.delete) {
1891
+ if (value.length > 0) {
1892
+ onChange(value.slice(0, -1));
1488
1893
  }
1489
- }
1490
- if (!ticketKey) {
1491
- setTickets([]);
1492
- setJiraState("no_tickets");
1493
- return;
1494
- }
1495
- const siteUrl = getJiraSiteUrl(repoPath);
1496
- const creds = getJiraCredentials(repoPath);
1497
- if (!siteUrl || !creds.email || !creds.apiToken) {
1498
- setTickets([]);
1499
- setJiraState("no_tickets");
1500
1894
  return;
1501
1895
  }
1502
- const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
1503
- const result = await getIssue(auth, ticketKey);
1504
- if (result.success) {
1505
- const linkedTicket = {
1506
- key: result.data.key,
1507
- summary: result.data.fields.summary,
1508
- status: result.data.fields.status.name,
1509
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1510
- };
1511
- addLinkedTicket(repoPath, currentBranch, linkedTicket);
1512
- setTickets([linkedTicket]);
1513
- setJiraState("has_tickets");
1514
- } else {
1515
- setTickets([]);
1516
- setJiraState("no_tickets");
1517
- }
1518
- },
1519
- []
1520
- );
1521
- const refreshTickets = useCallback5((repoPath, currentBranch) => {
1522
- const linkedTickets = getLinkedTickets(repoPath, currentBranch);
1523
- setTickets(linkedTickets);
1524
- setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
1525
- }, []);
1526
- const configureJira = useCallback5(
1527
- async (repoPath, siteUrl, email, apiToken) => {
1528
- setLoading((prev) => ({ ...prev, configure: true }));
1529
- setErrors((prev) => ({ ...prev, configure: void 0 }));
1530
- const auth = { siteUrl, email, apiToken };
1531
- const result = await validateCredentials(auth);
1532
- if (!result.success) {
1533
- setErrors((prev) => ({ ...prev, configure: result.error }));
1534
- setLoading((prev) => ({ ...prev, configure: false }));
1535
- return false;
1536
- }
1537
- setJiraSiteUrl(repoPath, siteUrl);
1538
- setJiraCredentials(repoPath, email, apiToken);
1539
- setJiraState("no_tickets");
1540
- setLoading((prev) => ({ ...prev, configure: false }));
1541
- return true;
1542
- },
1543
- []
1544
- );
1545
- const linkTicket = useCallback5(
1546
- async (repoPath, currentBranch, ticketInput) => {
1547
- setLoading((prev) => ({ ...prev, link: true }));
1548
- setErrors((prev) => ({ ...prev, link: void 0 }));
1549
- const ticketKey = parseTicketKey(ticketInput);
1550
- if (!ticketKey) {
1551
- setErrors((prev) => ({ ...prev, link: "Invalid ticket format. Use PROJ-123 or a Jira URL." }));
1552
- setLoading((prev) => ({ ...prev, link: false }));
1553
- return false;
1554
- }
1555
- const siteUrl = getJiraSiteUrl(repoPath);
1556
- const creds = getJiraCredentials(repoPath);
1557
- if (!siteUrl || !creds.email || !creds.apiToken) {
1558
- setErrors((prev) => ({ ...prev, link: "Jira not configured" }));
1559
- setLoading((prev) => ({ ...prev, link: false }));
1560
- return false;
1561
- }
1562
- const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
1563
- const result = await getIssue(auth, ticketKey);
1564
- if (!result.success) {
1565
- setErrors((prev) => ({ ...prev, link: result.error }));
1566
- setLoading((prev) => ({ ...prev, link: false }));
1567
- return false;
1568
- }
1569
- const linkedTicket = {
1570
- key: result.data.key,
1571
- summary: result.data.fields.summary,
1572
- status: result.data.fields.status.name,
1573
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1574
- };
1575
- addLinkedTicket(repoPath, currentBranch, linkedTicket);
1576
- const newTickets = getLinkedTickets(repoPath, currentBranch);
1577
- setTickets(newTickets);
1578
- setJiraState("has_tickets");
1579
- setLoading((prev) => ({ ...prev, link: false }));
1580
- return true;
1581
- },
1582
- []
1583
- );
1584
- const unlinkTicket = useCallback5((repoPath, currentBranch, ticketKey) => {
1585
- removeLinkedTicket(repoPath, currentBranch, ticketKey);
1586
- }, []);
1587
- const clearError = useCallback5((key) => {
1588
- setErrors((prev) => ({ ...prev, [key]: void 0 }));
1589
- }, []);
1590
- return {
1591
- jiraState,
1592
- tickets,
1593
- loading,
1594
- errors,
1595
- initializeJiraState,
1596
- refreshTickets,
1597
- configureJira,
1598
- linkTicket,
1599
- unlinkTicket,
1600
- clearError
1601
- };
1602
- }
1603
-
1604
- // src/hooks/logs/useLogs.ts
1605
- import { useCallback as useCallback6, useEffect as useEffect7, useRef as useRef4, useState as useState9 } from "react";
1606
- function useLogs() {
1607
- const [logFiles, setLogFiles] = useState9([]);
1608
- const [selectedDate, setSelectedDate] = useState9(null);
1609
- const [logContent, setLogContent] = useState9(null);
1610
- const [highlightedIndex, setHighlightedIndex] = useState9(0);
1611
- const initializedRef = useRef4(false);
1612
- const loadLogContent = useCallback6((date) => {
1613
- if (!date) {
1614
- setLogContent(null);
1615
- return null;
1616
- }
1617
- const content = readLog(date);
1618
- setLogContent(content);
1619
- return content;
1620
- }, []);
1621
- const refreshLogFiles = useCallback6(() => {
1622
- const files = listLogFiles();
1623
- setLogFiles(files);
1624
- return files;
1625
- }, []);
1626
- const initialize = useCallback6(() => {
1627
- const files = listLogFiles();
1628
- setLogFiles(files);
1629
- if (files.length === 0) return;
1630
- const today = getTodayDate();
1631
- const todayFile = files.find((f) => f.date === today);
1632
- if (todayFile) {
1633
- setSelectedDate(todayFile.date);
1634
- const idx = files.findIndex((f) => f.date === today);
1635
- setHighlightedIndex(idx >= 0 ? idx : 0);
1636
- loadLogContent(todayFile.date);
1637
- } else {
1638
- setSelectedDate(files[0].date);
1639
- setHighlightedIndex(0);
1640
- loadLogContent(files[0].date);
1641
- }
1642
- }, [loadLogContent]);
1643
- useEffect7(() => {
1644
- if (initializedRef.current) return;
1645
- initializedRef.current = true;
1646
- initialize();
1647
- }, [initialize]);
1648
- const selectDate = useCallback6((date) => {
1649
- setSelectedDate(date);
1650
- loadLogContent(date);
1651
- }, [loadLogContent]);
1652
- const refresh = useCallback6(() => {
1653
- refreshLogFiles();
1654
- if (selectedDate) {
1655
- loadLogContent(selectedDate);
1656
- }
1657
- }, [refreshLogFiles, selectedDate, loadLogContent]);
1658
- const handleExternalLogUpdate = useCallback6(() => {
1659
- const files = listLogFiles();
1660
- setLogFiles(files);
1661
- const today = getTodayDate();
1662
- if (selectedDate === today) {
1663
- loadLogContent(today);
1664
- } else if (!selectedDate && files.length > 0) {
1665
- const todayFile = files.find((f) => f.date === today);
1666
- if (todayFile) {
1667
- setSelectedDate(today);
1668
- const idx = files.findIndex((f) => f.date === today);
1669
- setHighlightedIndex(idx >= 0 ? idx : 0);
1670
- loadLogContent(today);
1671
- }
1672
- }
1673
- }, [selectedDate, loadLogContent]);
1674
- const handleLogCreated = useCallback6(() => {
1675
- const files = listLogFiles();
1676
- setLogFiles(files);
1677
- const today = getTodayDate();
1678
- setSelectedDate(today);
1679
- const idx = files.findIndex((f) => f.date === today);
1680
- setHighlightedIndex(idx >= 0 ? idx : 0);
1681
- loadLogContent(today);
1682
- }, [loadLogContent]);
1683
- return {
1684
- logFiles,
1685
- selectedDate,
1686
- logContent,
1687
- highlightedIndex,
1688
- setHighlightedIndex,
1689
- selectDate,
1690
- refresh,
1691
- handleExternalLogUpdate,
1692
- handleLogCreated
1693
- };
1896
+ if (key.return || key.escape || key.upArrow || key.downArrow || key.tab) {
1897
+ return;
1898
+ }
1899
+ if (input && input.length === 1 && input.charCodeAt(0) >= 32) {
1900
+ onChange(value + input);
1901
+ }
1902
+ },
1903
+ { isActive }
1904
+ );
1905
+ const displayValue = mask ? "*".repeat(value.length) : value;
1906
+ const showPlaceholder = value.length === 0 && placeholder;
1907
+ return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { children: [
1908
+ showPlaceholder ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx6(Text6, { children: displayValue }),
1909
+ isActive && /* @__PURE__ */ jsx6(Text6, { backgroundColor: "yellow", children: " " })
1910
+ ] }) });
1694
1911
  }
1695
1912
 
1696
- // src/hooks/useModal.ts
1697
- import { useCallback as useCallback7, useState as useState10 } from "react";
1698
- function useModal() {
1699
- const [modalType, setModalType] = useState10("none");
1700
- const open3 = useCallback7((type) => setModalType(type), []);
1701
- const close = useCallback7(() => setModalType("none"), []);
1702
- const isOpen = modalType !== "none";
1703
- return {
1704
- type: modalType,
1705
- isOpen,
1706
- open: open3,
1707
- close
1708
- };
1913
+ // src/components/jira/LinkTicketModal.tsx
1914
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1915
+ function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
1916
+ const [ticketInput, setTicketInput] = useState13("");
1917
+ const canSubmit = ticketInput.trim().length > 0;
1918
+ useInput6(
1919
+ (_input, key) => {
1920
+ if (loading) return;
1921
+ if (key.escape) {
1922
+ onCancel();
1923
+ return;
1924
+ }
1925
+ if (key.return && canSubmit) {
1926
+ onSubmit(ticketInput.trim());
1927
+ }
1928
+ },
1929
+ { isActive: !loading }
1930
+ );
1931
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
1932
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "yellow", children: "Link Jira Ticket" }),
1933
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Type ticket ID, Enter to submit, Esc to cancel" }),
1934
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
1935
+ error && /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: error }) }),
1936
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1937
+ /* @__PURE__ */ jsx7(Text7, { color: "blue", children: "Ticket: " }),
1938
+ /* @__PURE__ */ jsx7(TextInput, { value: ticketInput, onChange: setTicketInput, placeholder: "PROJ-123", isActive: !loading })
1939
+ ] }),
1940
+ loading && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "Fetching ticket..." }) }),
1941
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Examples: PROJ-123 or https://company.atlassian.net/browse/PROJ-123" }) })
1942
+ ] });
1709
1943
  }
1710
1944
 
1711
- // src/hooks/useListNavigation.ts
1712
- import { useCallback as useCallback8, useState as useState11 } from "react";
1713
- function useListNavigation(length) {
1714
- const [index, setIndex] = useState11(0);
1715
- const prev = useCallback8(() => {
1716
- setIndex((i) => Math.max(0, i - 1));
1717
- }, []);
1718
- const next = useCallback8(() => {
1719
- setIndex((i) => Math.min(length - 1, i + 1));
1720
- }, [length]);
1721
- const clampedIndex = Math.min(index, Math.max(0, length - 1));
1722
- const reset = useCallback8(() => setIndex(0), []);
1723
- return {
1724
- index: length === 0 ? 0 : clampedIndex,
1725
- prev,
1726
- next,
1727
- reset,
1728
- setIndex
1729
- };
1730
- }
1945
+ // src/components/jira/JiraView.tsx
1946
+ import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
1947
+ import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
1731
1948
 
1732
1949
  // src/components/jira/ChangeStatusModal.tsx
1733
- import { useEffect as useEffect8, useState as useState12 } from "react";
1734
- import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
1950
+ import { useEffect as useEffect10, useState as useState14 } from "react";
1951
+ import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
1735
1952
  import SelectInput from "ink-select-input";
1736
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1953
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1737
1954
  function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onCancel }) {
1738
- const [transitions, setTransitions] = useState12([]);
1739
- const [loading, setLoading] = useState12(true);
1740
- const [applying, setApplying] = useState12(false);
1741
- const [error, setError] = useState12(null);
1742
- useEffect8(() => {
1955
+ const [transitions, setTransitions] = useState14([]);
1956
+ const [loading, setLoading] = useState14(true);
1957
+ const [applying, setApplying] = useState14(false);
1958
+ const [error, setError] = useState14(null);
1959
+ useEffect10(() => {
1743
1960
  const fetchTransitions = async () => {
1744
1961
  const siteUrl = getJiraSiteUrl(repoPath);
1745
1962
  const creds = getJiraCredentials(repoPath);
1746
1963
  if (!siteUrl || !creds.email || !creds.apiToken) {
1747
1964
  setError("Jira not configured");
1965
+ duckEvents.emit("error");
1748
1966
  setLoading(false);
1749
1967
  return;
1750
1968
  }
@@ -1754,6 +1972,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1754
1972
  setTransitions(result.data);
1755
1973
  } else {
1756
1974
  setError(result.error);
1975
+ duckEvents.emit("error");
1757
1976
  }
1758
1977
  setLoading(false);
1759
1978
  };
@@ -1766,6 +1985,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1766
1985
  const creds = getJiraCredentials(repoPath);
1767
1986
  if (!siteUrl || !creds.email || !creds.apiToken) {
1768
1987
  setError("Jira not configured");
1988
+ duckEvents.emit("error");
1769
1989
  setApplying(false);
1770
1990
  return;
1771
1991
  }
@@ -1774,13 +1994,15 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1774
1994
  if (result.success) {
1775
1995
  const transition = transitions.find((t) => t.id === item.value);
1776
1996
  const newStatus = (transition == null ? void 0 : transition.to.name) ?? item.label;
1997
+ duckEvents.emit("jira:transition");
1777
1998
  onComplete(newStatus);
1778
1999
  } else {
1779
2000
  setError(result.error);
2001
+ duckEvents.emit("error");
1780
2002
  setApplying(false);
1781
2003
  }
1782
2004
  };
1783
- useInput5(
2005
+ useInput7(
1784
2006
  (_input, key) => {
1785
2007
  if (key.escape && !applying) {
1786
2008
  onCancel();
@@ -1792,24 +2014,27 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1792
2014
  label: t.name,
1793
2015
  value: t.id
1794
2016
  }));
1795
- const initialIndex = Math.max(0, transitions.findIndex((t) => t.to.name === currentStatus));
1796
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
1797
- /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "yellow", children: [
2017
+ const initialIndex = Math.max(
2018
+ 0,
2019
+ transitions.findIndex((t) => t.to.name === currentStatus)
2020
+ );
2021
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
2022
+ /* @__PURE__ */ jsxs8(Text8, { bold: true, color: "yellow", children: [
1798
2023
  "Change Status: ",
1799
2024
  ticketKey
1800
2025
  ] }),
1801
- loading && /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Loading transitions..." }),
1802
- error && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: error }) }),
1803
- !loading && !error && transitions.length === 0 && /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No available transitions" }),
1804
- !loading && !error && transitions.length > 0 && !applying && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx6(SelectInput, { items, initialIndex, onSelect: handleSelect }) }),
1805
- applying && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "Updating status..." }) }),
1806
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Esc to cancel" }) })
2026
+ loading && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Loading transitions..." }),
2027
+ error && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "red", children: error }) }),
2028
+ !loading && !error && transitions.length === 0 && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "No available transitions" }),
2029
+ !loading && !error && transitions.length > 0 && !applying && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx8(SelectInput, { items, initialIndex, onSelect: handleSelect }) }),
2030
+ applying && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Updating status..." }) }),
2031
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Esc to cancel" }) })
1807
2032
  ] });
1808
2033
  }
1809
2034
 
1810
2035
  // src/components/jira/ConfigureJiraSiteModal.tsx
1811
- import { useState as useState13 } from "react";
1812
- import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
2036
+ import { useState as useState15 } from "react";
2037
+ import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
1813
2038
 
1814
2039
  // src/lib/editor.ts
1815
2040
  import { spawnSync as spawnSync2 } from "child_process";
@@ -1840,7 +2065,7 @@ function openInEditor(content, filename) {
1840
2065
  }
1841
2066
 
1842
2067
  // src/components/jira/ConfigureJiraSiteModal.tsx
1843
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2068
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1844
2069
  function ConfigureJiraSiteModal({
1845
2070
  initialSiteUrl,
1846
2071
  initialEmail,
@@ -1849,13 +2074,13 @@ function ConfigureJiraSiteModal({
1849
2074
  loading,
1850
2075
  error
1851
2076
  }) {
1852
- const [siteUrl, setSiteUrl] = useState13(initialSiteUrl ?? "");
1853
- const [email, setEmail] = useState13(initialEmail ?? "");
1854
- const [apiToken, setApiToken] = useState13("");
1855
- const [selectedItem, setSelectedItem] = useState13("siteUrl");
2077
+ const [siteUrl, setSiteUrl] = useState15(initialSiteUrl ?? "");
2078
+ const [email, setEmail] = useState15(initialEmail ?? "");
2079
+ const [apiToken, setApiToken] = useState15("");
2080
+ const [selectedItem, setSelectedItem] = useState15("siteUrl");
1856
2081
  const items = ["siteUrl", "email", "apiToken", "submit"];
1857
2082
  const canSubmit = siteUrl.trim() && email.trim() && apiToken.trim();
1858
- useInput6(
2083
+ useInput8(
1859
2084
  (input, key) => {
1860
2085
  if (loading) return;
1861
2086
  if (key.escape) {
@@ -1904,104 +2129,31 @@ function ConfigureJiraSiteModal({
1904
2129
  const prefix = isSelected ? "> " : " ";
1905
2130
  const color = isSelected ? "yellow" : void 0;
1906
2131
  const displayValue = isSensitive && value ? "*".repeat(Math.min(value.length, 20)) : value;
1907
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1908
- /* @__PURE__ */ jsxs7(Text7, { color, bold: isSelected, children: [
2132
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2133
+ /* @__PURE__ */ jsxs9(Text9, { color, bold: isSelected, children: [
1909
2134
  prefix,
1910
2135
  label
1911
2136
  ] }),
1912
- value !== void 0 && /* @__PURE__ */ jsx7(Box7, { marginLeft: 4, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: displayValue || "(empty - press Enter to edit)" }) })
2137
+ value !== void 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: displayValue || "(empty - press Enter to edit)" }) })
1913
2138
  ] });
1914
2139
  };
1915
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
1916
- /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "Configure Jira Site" }),
1917
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Up/Down to select, Enter to edit, Esc to cancel" }),
1918
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
1919
- error && /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: error }) }),
2140
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
2141
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: "Configure Jira Site" }),
2142
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Up/Down to select, Enter to edit, Esc to cancel" }),
2143
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
2144
+ error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
1920
2145
  renderItem("siteUrl", "Site URL (e.g., https://company.atlassian.net)", siteUrl),
1921
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
2146
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
1922
2147
  renderItem("email", "Email", email),
1923
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
2148
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
1924
2149
  renderItem("apiToken", "API Token", apiToken, true),
1925
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
1926
- /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { color: selectedItem === "submit" ? "green" : void 0, bold: selectedItem === "submit", children: [
2150
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
2151
+ /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs9(Text9, { color: selectedItem === "submit" ? "green" : void 0, bold: selectedItem === "submit", children: [
1927
2152
  selectedItem === "submit" ? "> " : " ",
1928
2153
  canSubmit ? "[Save Configuration]" : "[Fill all fields first]"
1929
2154
  ] }) }),
1930
- loading && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "Validating credentials..." }) }),
1931
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Get your API token from: https://id.atlassian.com/manage-profile/security/api-tokens" }) })
1932
- ] });
1933
- }
1934
-
1935
- // src/components/jira/LinkTicketModal.tsx
1936
- import { useState as useState14 } from "react";
1937
- import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
1938
-
1939
- // src/components/ui/TextInput.tsx
1940
- import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
1941
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1942
- function TextInput({ value, onChange, placeholder, isActive, mask }) {
1943
- useInput7(
1944
- (input, key) => {
1945
- if (key.backspace || key.delete) {
1946
- if (value.length > 0) {
1947
- onChange(value.slice(0, -1));
1948
- }
1949
- return;
1950
- }
1951
- if (key.return || key.escape || key.upArrow || key.downArrow || key.tab) {
1952
- return;
1953
- }
1954
- if (input && input.length === 1 && input.charCodeAt(0) >= 32) {
1955
- onChange(value + input);
1956
- }
1957
- },
1958
- { isActive }
1959
- );
1960
- const displayValue = mask ? "*".repeat(value.length) : value;
1961
- const showPlaceholder = value.length === 0 && placeholder;
1962
- return /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs8(Text8, { children: [
1963
- showPlaceholder ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx8(Text8, { children: displayValue }),
1964
- isActive && /* @__PURE__ */ jsx8(Text8, { backgroundColor: "yellow", children: " " })
1965
- ] }) });
1966
- }
1967
-
1968
- // src/components/jira/LinkTicketModal.tsx
1969
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1970
- function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
1971
- const [ticketInput, setTicketInput] = useState14("");
1972
- const canSubmit = ticketInput.trim().length > 0;
1973
- useInput8(
1974
- (_input, key) => {
1975
- if (loading) return;
1976
- if (key.escape) {
1977
- onCancel();
1978
- return;
1979
- }
1980
- if (key.return && canSubmit) {
1981
- onSubmit(ticketInput.trim());
1982
- }
1983
- },
1984
- { isActive: !loading }
1985
- );
1986
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
1987
- /* @__PURE__ */ jsx9(Text9, { bold: true, color: "yellow", children: "Link Jira Ticket" }),
1988
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Type ticket ID, Enter to submit, Esc to cancel" }),
1989
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
1990
- error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
1991
- /* @__PURE__ */ jsxs9(Box9, { children: [
1992
- /* @__PURE__ */ jsx9(Text9, { color: "blue", children: "Ticket: " }),
1993
- /* @__PURE__ */ jsx9(
1994
- TextInput,
1995
- {
1996
- value: ticketInput,
1997
- onChange: setTicketInput,
1998
- placeholder: "PROJ-123",
1999
- isActive: !loading
2000
- }
2001
- )
2002
- ] }),
2003
- loading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Fetching ticket..." }) }),
2004
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Examples: PROJ-123 or https://company.atlassian.net/browse/PROJ-123" }) })
2155
+ loading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Validating credentials..." }) }),
2156
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Get your API token from: https://id.atlassian.com/manage-profile/security/api-tokens" }) })
2005
2157
  ] });
2006
2158
  }
2007
2159
 
@@ -2031,8 +2183,8 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2031
2183
  const jira = useJiraTickets();
2032
2184
  const modal = useModal();
2033
2185
  const nav = useListNavigation(jira.tickets.length);
2034
- const lastInitRef = useRef5(null);
2035
- useEffect9(() => {
2186
+ const lastInitRef = useRef6(null);
2187
+ useEffect11(() => {
2036
2188
  if (repo.loading || !repo.repoPath || !repo.currentBranch) return;
2037
2189
  const current = { branch: repo.currentBranch };
2038
2190
  const last = lastInitRef.current;
@@ -2040,17 +2192,17 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2040
2192
  lastInitRef.current = current;
2041
2193
  jira.initializeJiraState(repo.repoPath, repo.currentBranch, repo.currentRepoSlug);
2042
2194
  }, [repo.loading, repo.repoPath, repo.currentBranch, repo.currentRepoSlug, jira.initializeJiraState]);
2043
- useEffect9(() => {
2195
+ useEffect11(() => {
2044
2196
  if (isFocused) {
2045
2197
  repo.refreshBranch();
2046
2198
  } else {
2047
2199
  modal.close();
2048
2200
  }
2049
2201
  }, [isFocused, repo.refreshBranch, modal.close]);
2050
- useEffect9(() => {
2202
+ useEffect11(() => {
2051
2203
  onModalChange == null ? void 0 : onModalChange(modal.isOpen);
2052
2204
  }, [modal.isOpen, onModalChange]);
2053
- useEffect9(() => {
2205
+ useEffect11(() => {
2054
2206
  onJiraStateChange == null ? void 0 : onJiraStateChange(jira.jiraState);
2055
2207
  }, [jira.jiraState, onJiraStateChange]);
2056
2208
  const handleConfigureSubmit = async (siteUrl, email, apiToken) => {
@@ -2077,7 +2229,7 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2077
2229
  const ticket = jira.tickets[nav.index];
2078
2230
  const siteUrl = getJiraSiteUrl(repo.repoPath);
2079
2231
  if (ticket && siteUrl) {
2080
- open2(`${siteUrl}/browse/${ticket.key}`).catch(() => {
2232
+ open3(`${siteUrl}/browse/${ticket.key}`).catch(() => {
2081
2233
  });
2082
2234
  }
2083
2235
  };
@@ -2187,73 +2339,14 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2187
2339
  }
2188
2340
 
2189
2341
  // src/components/logs/LogsView.tsx
2190
- import { useEffect as useEffect10 } from "react";
2342
+ import { useEffect as useEffect12 } from "react";
2191
2343
  import { Box as Box14, useInput as useInput12 } from "ink";
2192
2344
 
2193
- // src/components/logs/LogsHistoryBox.tsx
2345
+ // src/components/logs/LogViewerBox.tsx
2346
+ import { useRef as useRef7, useState as useState16 } from "react";
2194
2347
  import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
2195
2348
  import { Box as Box12, Text as Text12, useInput as useInput10 } from "ink";
2196
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2197
- function LogsHistoryBox({
2198
- logFiles,
2199
- selectedDate,
2200
- highlightedIndex,
2201
- onHighlight,
2202
- onSelect,
2203
- isFocused
2204
- }) {
2205
- const title = "[5] Logs";
2206
- const borderColor = isFocused ? "yellow" : void 0;
2207
- useInput10(
2208
- (input, key) => {
2209
- if (logFiles.length === 0) return;
2210
- if (key.upArrow || input === "k") {
2211
- onHighlight(Math.max(0, highlightedIndex - 1));
2212
- }
2213
- if (key.downArrow || input === "j") {
2214
- onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
2215
- }
2216
- if (key.return) {
2217
- const file = logFiles[highlightedIndex];
2218
- if (file) {
2219
- onSelect(file.date);
2220
- }
2221
- }
2222
- },
2223
- { isActive: isFocused }
2224
- );
2225
- return /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
2226
- logFiles.length === 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "No logs yet" }),
2227
- logFiles.map((file, idx) => {
2228
- const isHighlighted = idx === highlightedIndex;
2229
- const isSelected = file.date === selectedDate;
2230
- const cursor = isHighlighted ? ">" : " ";
2231
- const indicator = isSelected ? " *" : "";
2232
- return /* @__PURE__ */ jsxs12(Box12, { children: [
2233
- /* @__PURE__ */ jsxs12(Text12, { color: isHighlighted ? "yellow" : void 0, children: [
2234
- cursor,
2235
- " "
2236
- ] }),
2237
- /* @__PURE__ */ jsx12(
2238
- Text12,
2239
- {
2240
- color: file.isToday ? "green" : void 0,
2241
- bold: file.isToday,
2242
- children: file.date
2243
- }
2244
- ),
2245
- file.isToday && /* @__PURE__ */ jsx12(Text12, { color: "green", children: " (today)" }),
2246
- /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: indicator })
2247
- ] }, file.date);
2248
- })
2249
- ] }) });
2250
- }
2251
-
2252
- // src/components/logs/LogViewerBox.tsx
2253
- import { useRef as useRef6, useState as useState15 } from "react";
2254
- import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
2255
- import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
2256
- import { ScrollView as ScrollView2 } from "ink-scroll-view";
2349
+ import { ScrollView as ScrollView4 } from "ink-scroll-view";
2257
2350
  import TextInput2 from "ink-text-input";
2258
2351
 
2259
2352
  // src/lib/claude/api.ts
@@ -2340,18 +2433,18 @@ Generate the standup notes:`;
2340
2433
  }
2341
2434
 
2342
2435
  // src/components/logs/LogViewerBox.tsx
2343
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2436
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2344
2437
  function LogViewerBox({ date, content, isFocused, onRefresh, onLogCreated }) {
2345
- const scrollRef = useRef6(null);
2346
- const [isInputMode, setIsInputMode] = useState15(false);
2347
- const [inputValue, setInputValue] = useState15("");
2348
- const [isGeneratingStandup, setIsGeneratingStandup] = useState15(false);
2349
- const [standupResult, setStandupResult] = useState15(null);
2350
- const claudeProcessRef = useRef6(null);
2438
+ const scrollRef = useRef7(null);
2439
+ const [isInputMode, setIsInputMode] = useState16(false);
2440
+ const [inputValue, setInputValue] = useState16("");
2441
+ const [isGeneratingStandup, setIsGeneratingStandup] = useState16(false);
2442
+ const [standupResult, setStandupResult] = useState16(null);
2443
+ const claudeProcessRef = useRef7(null);
2351
2444
  const title = "[6] Log Content";
2352
2445
  const borderColor = isFocused ? "yellow" : void 0;
2353
2446
  const displayTitle = date ? `${title} - ${date}.md` : title;
2354
- useInput11(
2447
+ useInput10(
2355
2448
  (input, key) => {
2356
2449
  var _a, _b, _c;
2357
2450
  if (key.escape && isInputMode) {
@@ -2430,14 +2523,14 @@ ${value.trim()}
2430
2523
  setIsInputMode(false);
2431
2524
  onRefresh();
2432
2525
  };
2433
- return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", flexGrow: 1, children: [
2434
- /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx13(Box13, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx13(ScrollView2, { ref: scrollRef, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
2435
- !date && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Select a log file to view" }),
2436
- date && content === null && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Log file not found" }),
2437
- date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Empty log file" }),
2438
- date && content && content.trim() !== "" && /* @__PURE__ */ jsx13(Markdown, { children: content })
2526
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", flexGrow: 1, children: [
2527
+ /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx12(ScrollView4, { ref: scrollRef, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
2528
+ !date && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Select a log file to view" }),
2529
+ date && content === null && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Log file not found" }),
2530
+ date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Empty log file" }),
2531
+ date && content && content.trim() !== "" && /* @__PURE__ */ jsx12(Markdown, { children: content })
2439
2532
  ] }) }) }) }),
2440
- isInputMode && /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: ["Add Entry"], borderColor: "yellow", children: /* @__PURE__ */ jsx13(Box13, { paddingX: 1, children: /* @__PURE__ */ jsx13(
2533
+ isInputMode && /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: ["Add Entry"], borderColor: "yellow", children: /* @__PURE__ */ jsx12(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx12(
2441
2534
  TextInput2,
2442
2535
  {
2443
2536
  value: inputValue,
@@ -2445,30 +2538,84 @@ ${value.trim()}
2445
2538
  onSubmit: handleInputSubmit
2446
2539
  }
2447
2540
  ) }) }),
2448
- isGeneratingStandup && /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: ["Standup Notes"], borderColor: "yellow", children: /* @__PURE__ */ jsxs13(Box13, { paddingX: 1, flexDirection: "column", children: [
2449
- /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: "Generating standup notes..." }),
2450
- /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Press Esc to cancel" })
2541
+ isGeneratingStandup && /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: ["Standup Notes"], borderColor: "yellow", children: /* @__PURE__ */ jsxs12(Box12, { paddingX: 1, flexDirection: "column", children: [
2542
+ /* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "Generating standup notes..." }),
2543
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Press Esc to cancel" })
2451
2544
  ] }) }),
2452
- standupResult && /* @__PURE__ */ jsx13(
2453
- TitledBox6,
2545
+ standupResult && /* @__PURE__ */ jsx12(
2546
+ TitledBox5,
2454
2547
  {
2455
2548
  borderStyle: "round",
2456
2549
  titles: ["Standup Notes"],
2457
2550
  borderColor: standupResult.type === "error" ? "red" : "green",
2458
- children: /* @__PURE__ */ jsxs13(Box13, { paddingX: 1, flexDirection: "column", children: [
2459
- standupResult.type === "error" ? /* @__PURE__ */ jsx13(Text13, { color: "red", children: standupResult.message }) : /* @__PURE__ */ jsx13(Markdown, { children: standupResult.message }),
2460
- /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Press Esc to dismiss" })
2551
+ children: /* @__PURE__ */ jsxs12(Box12, { paddingX: 1, flexDirection: "column", children: [
2552
+ standupResult.type === "error" ? /* @__PURE__ */ jsx12(Text12, { color: "red", children: standupResult.message }) : /* @__PURE__ */ jsx12(Markdown, { children: standupResult.message }),
2553
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Press Esc to dismiss" })
2461
2554
  ] })
2462
2555
  }
2463
2556
  )
2464
2557
  ] });
2465
2558
  }
2466
2559
 
2560
+ // src/components/logs/LogsHistoryBox.tsx
2561
+ import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
2562
+ import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
2563
+ import { ScrollView as ScrollView5 } from "ink-scroll-view";
2564
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2565
+ function LogsHistoryBox({
2566
+ logFiles,
2567
+ selectedDate,
2568
+ highlightedIndex,
2569
+ onHighlight,
2570
+ onSelect,
2571
+ isFocused
2572
+ }) {
2573
+ const scrollRef = useScrollToIndex(highlightedIndex);
2574
+ const title = "[5] Logs";
2575
+ const borderColor = isFocused ? "yellow" : void 0;
2576
+ useInput11(
2577
+ (input, key) => {
2578
+ if (logFiles.length === 0) return;
2579
+ if (key.upArrow || input === "k") {
2580
+ onHighlight(Math.max(0, highlightedIndex - 1));
2581
+ }
2582
+ if (key.downArrow || input === "j") {
2583
+ onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
2584
+ }
2585
+ if (key.return) {
2586
+ const file = logFiles[highlightedIndex];
2587
+ if (file) {
2588
+ onSelect(file.date);
2589
+ }
2590
+ }
2591
+ },
2592
+ { isActive: isFocused }
2593
+ );
2594
+ return /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
2595
+ logFiles.length === 0 && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "No logs yet" }),
2596
+ logFiles.length > 0 && /* @__PURE__ */ jsx13(ScrollView5, { ref: scrollRef, children: logFiles.map((file, idx) => {
2597
+ const isHighlighted = idx === highlightedIndex;
2598
+ const isSelected = file.date === selectedDate;
2599
+ const cursor = isHighlighted ? ">" : " ";
2600
+ const indicator = isSelected ? " *" : "";
2601
+ return /* @__PURE__ */ jsxs13(Box13, { children: [
2602
+ /* @__PURE__ */ jsxs13(Text13, { color: isHighlighted ? "yellow" : void 0, children: [
2603
+ cursor,
2604
+ " "
2605
+ ] }),
2606
+ /* @__PURE__ */ jsx13(Text13, { color: file.isToday ? "green" : void 0, bold: file.isToday, children: file.date }),
2607
+ file.isToday && /* @__PURE__ */ jsx13(Text13, { color: "green", children: " (today)" }),
2608
+ /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: indicator })
2609
+ ] }, file.date);
2610
+ }) })
2611
+ ] }) });
2612
+ }
2613
+
2467
2614
  // src/components/logs/LogsView.tsx
2468
2615
  import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2469
2616
  function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
2470
2617
  const logs = useLogs();
2471
- useEffect10(() => {
2618
+ useEffect12(() => {
2472
2619
  if (refreshKey !== void 0 && refreshKey > 0) {
2473
2620
  logs.handleExternalLogUpdate();
2474
2621
  }
@@ -2513,22 +2660,25 @@ var globalBindings = [
2513
2660
  { key: "j/k", label: "Navigate" },
2514
2661
  { key: "Ctrl+C", label: "Quit" }
2515
2662
  ];
2516
- var modalBindings = [
2517
- { key: "Esc", label: "Cancel" }
2518
- ];
2519
- function KeybindingsBar({ contextBindings = [], modalOpen = false }) {
2663
+ var modalBindings = [{ key: "Esc", label: "Cancel" }];
2664
+ var DUCK_ASCII = "<(')___";
2665
+ function KeybindingsBar({ contextBindings = [], modalOpen = false, duck }) {
2520
2666
  const allBindings = modalOpen ? [...contextBindings, ...modalBindings] : [...contextBindings, ...globalBindings];
2521
- return /* @__PURE__ */ jsx15(Box15, { flexShrink: 0, paddingX: 1, gap: 2, children: allBindings.map((binding) => /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
2522
- /* @__PURE__ */ jsx15(Text14, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
2523
- /* @__PURE__ */ jsx15(Text14, { dimColor: true, children: binding.label })
2524
- ] }, binding.key)) });
2667
+ return /* @__PURE__ */ jsxs15(Box15, { flexShrink: 0, paddingX: 1, gap: 2, children: [
2668
+ allBindings.map((binding) => /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
2669
+ /* @__PURE__ */ jsx15(Text14, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
2670
+ /* @__PURE__ */ jsx15(Text14, { dimColor: true, children: binding.label })
2671
+ ] }, binding.key)),
2672
+ (duck == null ? void 0 : duck.visible) && /* @__PURE__ */ jsxs15(Box15, { flexGrow: 1, justifyContent: "flex-end", gap: 1, children: [
2673
+ /* @__PURE__ */ jsx15(Text14, { children: DUCK_ASCII }),
2674
+ /* @__PURE__ */ jsx15(Text14, { dimColor: true, children: duck.message })
2675
+ ] })
2676
+ ] });
2525
2677
  }
2526
2678
 
2527
2679
  // src/constants/github.ts
2528
2680
  var GITHUB_KEYBINDINGS = {
2529
- remotes: [
2530
- { key: "Space", label: "Select Remote" }
2531
- ],
2681
+ remotes: [{ key: "Space", label: "Select Remote" }],
2532
2682
  prs: [
2533
2683
  { key: "Space", label: "Select" },
2534
2684
  { key: "n", label: "New PR", color: "green" },
@@ -2557,9 +2707,7 @@ var JIRA_KEYBINDINGS = {
2557
2707
 
2558
2708
  // src/constants/logs.ts
2559
2709
  var LOGS_KEYBINDINGS = {
2560
- history: [
2561
- { key: "Enter", label: "Select" }
2562
- ],
2710
+ history: [{ key: "Enter", label: "Select" }],
2563
2711
  viewer: [
2564
2712
  { key: "i", label: "Add Entry" },
2565
2713
  { key: "e", label: "Edit" },
@@ -2588,12 +2736,13 @@ function computeKeybindings(focusedView, state) {
2588
2736
  import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
2589
2737
  function App() {
2590
2738
  const { exit } = useApp();
2591
- const [focusedView, setFocusedView] = useState16("github");
2592
- const [modalOpen, setModalOpen] = useState16(false);
2593
- const [logRefreshKey, setLogRefreshKey] = useState16(0);
2594
- const [githubFocusedBox, setGithubFocusedBox] = useState16("remotes");
2595
- const [jiraState, setJiraState] = useState16("not_configured");
2596
- const [logsFocusedBox, setLogsFocusedBox] = useState16("history");
2739
+ const [focusedView, setFocusedView] = useState17("github");
2740
+ const [modalOpen, setModalOpen] = useState17(false);
2741
+ const [logRefreshKey, setLogRefreshKey] = useState17(0);
2742
+ const duck = useRubberDuck();
2743
+ const [githubFocusedBox, setGithubFocusedBox] = useState17("remotes");
2744
+ const [jiraState, setJiraState] = useState17("not_configured");
2745
+ const [logsFocusedBox, setLogsFocusedBox] = useState17("history");
2597
2746
  const keybindings = useMemo2(
2598
2747
  () => computeKeybindings(focusedView, {
2599
2748
  github: { focusedBox: githubFocusedBox },
@@ -2602,7 +2751,7 @@ function App() {
2602
2751
  }),
2603
2752
  [focusedView, githubFocusedBox, jiraState, modalOpen, logsFocusedBox]
2604
2753
  );
2605
- const handleLogUpdated = useCallback9(() => {
2754
+ const handleLogUpdated = useCallback10(() => {
2606
2755
  setLogRefreshKey((prev) => prev + 1);
2607
2756
  }, []);
2608
2757
  useInput13(
@@ -2624,6 +2773,12 @@ function App() {
2624
2773
  setFocusedView("logs");
2625
2774
  setLogsFocusedBox("viewer");
2626
2775
  }
2776
+ if (input === "d") {
2777
+ duck.toggleDuck();
2778
+ }
2779
+ if (input === "q" && duck.visible) {
2780
+ duck.quack();
2781
+ }
2627
2782
  },
2628
2783
  { isActive: !modalOpen }
2629
2784
  );
@@ -2658,7 +2813,14 @@ function App() {
2658
2813
  }
2659
2814
  ) })
2660
2815
  ] }),
2661
- /* @__PURE__ */ jsx16(KeybindingsBar, { contextBindings: keybindings, modalOpen })
2816
+ /* @__PURE__ */ jsx16(
2817
+ KeybindingsBar,
2818
+ {
2819
+ contextBindings: keybindings,
2820
+ modalOpen,
2821
+ duck: { visible: duck.visible, message: duck.message }
2822
+ }
2823
+ )
2662
2824
  ] });
2663
2825
  }
2664
2826
 
@@ -2666,17 +2828,14 @@ function App() {
2666
2828
  import { render as inkRender } from "ink";
2667
2829
 
2668
2830
  // src/lib/Screen.tsx
2831
+ import { useCallback as useCallback11, useEffect as useEffect13, useState as useState18 } from "react";
2669
2832
  import { Box as Box17, useStdout as useStdout2 } from "ink";
2670
- import { useCallback as useCallback10, useEffect as useEffect11, useState as useState17 } from "react";
2671
2833
  import { jsx as jsx17 } from "react/jsx-runtime";
2672
2834
  function Screen({ children }) {
2673
2835
  const { stdout } = useStdout2();
2674
- const getSize = useCallback10(
2675
- () => ({ height: stdout.rows, width: stdout.columns }),
2676
- [stdout]
2677
- );
2678
- const [size, setSize] = useState17(getSize);
2679
- useEffect11(() => {
2836
+ const getSize = useCallback11(() => ({ height: stdout.rows, width: stdout.columns }), [stdout]);
2837
+ const [size, setSize] = useState18(getSize);
2838
+ useEffect13(() => {
2680
2839
  const onResize = () => setSize(getSize());
2681
2840
  stdout.on("resize", onResize);
2682
2841
  return () => {