clairo 1.0.7 → 1.0.9

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 +1231 -959
  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,327 @@ 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 getExistingJiraConfigs(excludeRepoPath) {
577
+ const config = loadConfig();
578
+ const repos = config.repositories ?? {};
579
+ const configs = [];
580
+ const seen = /* @__PURE__ */ new Set();
581
+ for (const [repoPath, repoConfig] of Object.entries(repos)) {
582
+ if (repoPath === excludeRepoPath) continue;
583
+ if (!repoConfig.jiraSiteUrl || !repoConfig.jiraEmail || !repoConfig.jiraApiToken) continue;
584
+ const key = `${repoConfig.jiraSiteUrl}|${repoConfig.jiraEmail}`;
585
+ if (seen.has(key)) continue;
586
+ seen.add(key);
587
+ configs.push({
588
+ repoPath,
589
+ siteUrl: repoConfig.jiraSiteUrl,
590
+ email: repoConfig.jiraEmail,
591
+ apiToken: repoConfig.jiraApiToken
592
+ });
593
+ }
594
+ return configs;
595
+ }
596
+ function isJiraConfigured(repoPath) {
597
+ const config = getRepoConfig(repoPath);
598
+ return !!(config.jiraSiteUrl && config.jiraEmail && config.jiraApiToken);
599
+ }
600
+ function getJiraSiteUrl(repoPath) {
601
+ const config = getRepoConfig(repoPath);
602
+ return config.jiraSiteUrl ?? null;
603
+ }
604
+ function setJiraSiteUrl(repoPath, siteUrl) {
605
+ updateRepoConfig(repoPath, { jiraSiteUrl: siteUrl });
606
+ }
317
607
  function getJiraCredentials(repoPath) {
318
608
  const config = getRepoConfig(repoPath);
319
609
  return {
@@ -324,6 +614,13 @@ function getJiraCredentials(repoPath) {
324
614
  function setJiraCredentials(repoPath, email, apiToken) {
325
615
  updateRepoConfig(repoPath, { jiraEmail: email, jiraApiToken: apiToken });
326
616
  }
617
+ function clearJiraConfig(repoPath) {
618
+ updateRepoConfig(repoPath, {
619
+ jiraSiteUrl: void 0,
620
+ jiraEmail: void 0,
621
+ jiraApiToken: void 0
622
+ });
623
+ }
327
624
  function getLinkedTickets(repoPath, branch) {
328
625
  var _a;
329
626
  const config = getRepoConfig(repoPath);
@@ -498,10 +795,10 @@ async function applyTransition(auth, ticketKey, transitionId) {
498
795
  }
499
796
 
500
797
  // src/lib/logs/index.ts
501
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, appendFileSync, writeFileSync as writeFileSync2 } from "fs";
798
+ import { spawnSync } from "child_process";
799
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, writeFileSync as writeFileSync2 } from "fs";
502
800
  import { homedir as homedir2 } from "os";
503
801
  import { join as join2 } from "path";
504
- import { spawnSync } from "child_process";
505
802
  var LOGS_DIRECTORY = join2(homedir2(), ".clairo", "logs");
506
803
  function ensureLogsDirectory() {
507
804
  if (!existsSync2(LOGS_DIRECTORY)) {
@@ -581,305 +878,49 @@ function appendToLog(date, entry) {
581
878
  appendFileSync(filePath, entry);
582
879
  }
583
880
  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
- };
881
+ const filePath = getLogFilePath(date);
882
+ if (!existsSync2(filePath)) {
883
+ return false;
884
+ }
885
+ const timestamp = formatTimestamp();
886
+ appendFileSync(filePath, `
887
+ ## ${timestamp}
888
+
889
+ `);
890
+ const editor = process.env.VISUAL || process.env.EDITOR || "vi";
891
+ const result = spawnSync(editor, [filePath], {
892
+ stdio: "inherit"
893
+ });
894
+ process.stdout.write("\x1B[2J\x1B[H");
895
+ process.stdout.emit("resize");
896
+ return result.status === 0;
897
+ }
898
+
899
+ // src/lib/logs/logger.ts
900
+ function logPRCreated(prNumber, title, jiraTickets) {
901
+ const timestamp = formatTimestamp();
902
+ const today = getTodayDate();
903
+ let entry = `## ${timestamp} - Created PR #${prNumber}
904
+
905
+ ${title}
906
+ `;
907
+ if (jiraTickets.length > 0) {
908
+ entry += `Jira: ${jiraTickets.join(", ")}
909
+ `;
910
+ }
911
+ entry += "\n";
912
+ appendToLog(today, entry);
913
+ }
914
+ function logJiraStatusChanged(ticketKey, ticketName, oldStatus, newStatus) {
915
+ const timestamp = formatTimestamp();
916
+ const today = getTodayDate();
917
+ const entry = `## ${timestamp} - Updated Jira ticket
918
+
919
+ ${ticketKey}: ${ticketName}
920
+ ${oldStatus} \u2192 ${newStatus}
921
+
922
+ `;
923
+ appendToLog(today, entry);
883
924
  }
884
925
 
885
926
  // src/components/github/PRDetailsBox.tsx
@@ -889,10 +930,10 @@ import { Box as Box2, Text as Text2, useInput, useStdout } from "ink";
889
930
  import { ScrollView } from "ink-scroll-view";
890
931
 
891
932
  // src/components/ui/Markdown.tsx
933
+ import Table from "cli-table3";
934
+ import { marked } from "marked";
892
935
  import { Box, Text } from "ink";
893
936
  import Link from "ink-link";
894
- import { marked } from "marked";
895
- import Table from "cli-table3";
896
937
  import { jsx, jsxs } from "react/jsx-runtime";
897
938
  function Markdown({ children }) {
898
939
  const tokens = marked.lexer(children);
@@ -904,10 +945,12 @@ function TokenRenderer({ token }) {
904
945
  case "heading":
905
946
  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
947
  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
- });
948
+ const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some(
949
+ (t) => {
950
+ var _a2;
951
+ return t.type === "link" || t.type === "strong" && "tokens" in t && ((_a2 = t.tokens) == null ? void 0 : _a2.some((st) => st.type === "link"));
952
+ }
953
+ );
911
954
  if (hasLinks) {
912
955
  return /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: renderInline(token.tokens) });
913
956
  }
@@ -1153,10 +1196,387 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
1153
1196
  ] });
1154
1197
  }
1155
1198
 
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";
1199
+ // src/components/github/PullRequestsBox.tsx
1200
+ import open2 from "open";
1201
+ import { useEffect as useEffect7, useState as useState10 } from "react";
1202
+ import { TitledBox } from "@mishieck/ink-titled-box";
1203
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
1204
+ import { ScrollView as ScrollView2 } from "ink-scroll-view";
1205
+
1206
+ // src/hooks/jira/useJiraTickets.ts
1207
+ import { useCallback as useCallback4, useState as useState5 } from "react";
1208
+ function useJiraTickets() {
1209
+ const [jiraState, setJiraState] = useState5("not_configured");
1210
+ const [tickets, setTickets] = useState5([]);
1211
+ const [loading, setLoading] = useState5({ configure: false, link: false });
1212
+ const [errors, setErrors] = useState5({});
1213
+ const initializeJiraState = useCallback4(async (repoPath, currentBranch, repoSlug) => {
1214
+ if (!isJiraConfigured(repoPath)) {
1215
+ setJiraState("not_configured");
1216
+ setTickets([]);
1217
+ return;
1218
+ }
1219
+ const linkedTickets = getLinkedTickets(repoPath, currentBranch);
1220
+ if (linkedTickets.length > 0) {
1221
+ setTickets(linkedTickets);
1222
+ setJiraState("has_tickets");
1223
+ return;
1224
+ }
1225
+ let ticketKey = extractTicketKey(currentBranch);
1226
+ if (!ticketKey && repoSlug) {
1227
+ const prResult = await listPRsForBranch(currentBranch, repoSlug);
1228
+ if (prResult.success && prResult.data.length > 0) {
1229
+ ticketKey = extractTicketKey(prResult.data[0].title);
1230
+ }
1231
+ }
1232
+ if (!ticketKey) {
1233
+ setTickets([]);
1234
+ setJiraState("no_tickets");
1235
+ return;
1236
+ }
1237
+ const siteUrl = getJiraSiteUrl(repoPath);
1238
+ const creds = getJiraCredentials(repoPath);
1239
+ if (!siteUrl || !creds.email || !creds.apiToken) {
1240
+ setTickets([]);
1241
+ setJiraState("no_tickets");
1242
+ return;
1243
+ }
1244
+ const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
1245
+ const result = await getIssue(auth, ticketKey);
1246
+ if (result.success) {
1247
+ const linkedTicket = {
1248
+ key: result.data.key,
1249
+ summary: result.data.fields.summary,
1250
+ status: result.data.fields.status.name,
1251
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1252
+ };
1253
+ addLinkedTicket(repoPath, currentBranch, linkedTicket);
1254
+ setTickets([linkedTicket]);
1255
+ setJiraState("has_tickets");
1256
+ } else {
1257
+ setTickets([]);
1258
+ setJiraState("no_tickets");
1259
+ }
1260
+ }, []);
1261
+ const refreshTickets = useCallback4((repoPath, currentBranch) => {
1262
+ const linkedTickets = getLinkedTickets(repoPath, currentBranch);
1263
+ setTickets(linkedTickets);
1264
+ setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
1265
+ }, []);
1266
+ const configureJira = useCallback4(
1267
+ async (repoPath, siteUrl, email, apiToken) => {
1268
+ setLoading((prev) => ({ ...prev, configure: true }));
1269
+ setErrors((prev) => ({ ...prev, configure: void 0 }));
1270
+ const auth = { siteUrl, email, apiToken };
1271
+ const result = await validateCredentials(auth);
1272
+ if (!result.success) {
1273
+ setErrors((prev) => ({ ...prev, configure: result.error }));
1274
+ duckEvents.emit("error");
1275
+ setLoading((prev) => ({ ...prev, configure: false }));
1276
+ return false;
1277
+ }
1278
+ setJiraSiteUrl(repoPath, siteUrl);
1279
+ setJiraCredentials(repoPath, email, apiToken);
1280
+ setJiraState("no_tickets");
1281
+ duckEvents.emit("jira:configured");
1282
+ setLoading((prev) => ({ ...prev, configure: false }));
1283
+ return true;
1284
+ },
1285
+ []
1286
+ );
1287
+ const linkTicket = useCallback4(
1288
+ async (repoPath, currentBranch, ticketInput) => {
1289
+ setLoading((prev) => ({ ...prev, link: true }));
1290
+ setErrors((prev) => ({ ...prev, link: void 0 }));
1291
+ const ticketKey = parseTicketKey(ticketInput);
1292
+ if (!ticketKey) {
1293
+ setErrors((prev) => ({ ...prev, link: "Invalid ticket format. Use PROJ-123 or a Jira URL." }));
1294
+ duckEvents.emit("error");
1295
+ setLoading((prev) => ({ ...prev, link: false }));
1296
+ return false;
1297
+ }
1298
+ const siteUrl = getJiraSiteUrl(repoPath);
1299
+ const creds = getJiraCredentials(repoPath);
1300
+ if (!siteUrl || !creds.email || !creds.apiToken) {
1301
+ setErrors((prev) => ({ ...prev, link: "Jira not configured" }));
1302
+ duckEvents.emit("error");
1303
+ setLoading((prev) => ({ ...prev, link: false }));
1304
+ return false;
1305
+ }
1306
+ const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
1307
+ const result = await getIssue(auth, ticketKey);
1308
+ if (!result.success) {
1309
+ setErrors((prev) => ({ ...prev, link: result.error }));
1310
+ duckEvents.emit("error");
1311
+ setLoading((prev) => ({ ...prev, link: false }));
1312
+ return false;
1313
+ }
1314
+ const linkedTicket = {
1315
+ key: result.data.key,
1316
+ summary: result.data.fields.summary,
1317
+ status: result.data.fields.status.name,
1318
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1319
+ };
1320
+ addLinkedTicket(repoPath, currentBranch, linkedTicket);
1321
+ const newTickets = getLinkedTickets(repoPath, currentBranch);
1322
+ setTickets(newTickets);
1323
+ setJiraState("has_tickets");
1324
+ duckEvents.emit("jira:linked");
1325
+ setLoading((prev) => ({ ...prev, link: false }));
1326
+ return true;
1327
+ },
1328
+ []
1329
+ );
1330
+ const unlinkTicket = useCallback4((repoPath, currentBranch, ticketKey) => {
1331
+ removeLinkedTicket(repoPath, currentBranch, ticketKey);
1332
+ }, []);
1333
+ const clearError = useCallback4((key) => {
1334
+ setErrors((prev) => ({ ...prev, [key]: void 0 }));
1335
+ }, []);
1336
+ return {
1337
+ jiraState,
1338
+ tickets,
1339
+ loading,
1340
+ errors,
1341
+ initializeJiraState,
1342
+ refreshTickets,
1343
+ configureJira,
1344
+ linkTicket,
1345
+ unlinkTicket,
1346
+ clearError
1347
+ };
1348
+ }
1349
+
1350
+ // src/hooks/logs/useLogs.ts
1351
+ import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef3, useState as useState6 } from "react";
1352
+ function useLogs() {
1353
+ const [logFiles, setLogFiles] = useState6([]);
1354
+ const [selectedDate, setSelectedDate] = useState6(null);
1355
+ const [logContent, setLogContent] = useState6(null);
1356
+ const [highlightedIndex, setHighlightedIndex] = useState6(0);
1357
+ const initializedRef = useRef3(false);
1358
+ const loadLogContent = useCallback5((date) => {
1359
+ if (!date) {
1360
+ setLogContent(null);
1361
+ return null;
1362
+ }
1363
+ const content = readLog(date);
1364
+ setLogContent(content);
1365
+ return content;
1366
+ }, []);
1367
+ const refreshLogFiles = useCallback5(() => {
1368
+ const files = listLogFiles();
1369
+ setLogFiles(files);
1370
+ return files;
1371
+ }, []);
1372
+ const initialize = useCallback5(() => {
1373
+ const files = listLogFiles();
1374
+ setLogFiles(files);
1375
+ if (files.length === 0) return;
1376
+ const today = getTodayDate();
1377
+ const todayFile = files.find((f) => f.date === today);
1378
+ if (todayFile) {
1379
+ setSelectedDate(todayFile.date);
1380
+ const idx = files.findIndex((f) => f.date === today);
1381
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1382
+ loadLogContent(todayFile.date);
1383
+ } else {
1384
+ setSelectedDate(files[0].date);
1385
+ setHighlightedIndex(0);
1386
+ loadLogContent(files[0].date);
1387
+ }
1388
+ }, [loadLogContent]);
1389
+ useEffect4(() => {
1390
+ if (initializedRef.current) return;
1391
+ initializedRef.current = true;
1392
+ initialize();
1393
+ }, [initialize]);
1394
+ const selectDate = useCallback5(
1395
+ (date) => {
1396
+ setSelectedDate(date);
1397
+ loadLogContent(date);
1398
+ },
1399
+ [loadLogContent]
1400
+ );
1401
+ const refresh = useCallback5(() => {
1402
+ refreshLogFiles();
1403
+ if (selectedDate) {
1404
+ loadLogContent(selectedDate);
1405
+ }
1406
+ }, [refreshLogFiles, selectedDate, loadLogContent]);
1407
+ const handleExternalLogUpdate = useCallback5(() => {
1408
+ const files = listLogFiles();
1409
+ setLogFiles(files);
1410
+ const today = getTodayDate();
1411
+ if (selectedDate === today) {
1412
+ loadLogContent(today);
1413
+ } else if (!selectedDate && files.length > 0) {
1414
+ const todayFile = files.find((f) => f.date === today);
1415
+ if (todayFile) {
1416
+ setSelectedDate(today);
1417
+ const idx = files.findIndex((f) => f.date === today);
1418
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1419
+ loadLogContent(today);
1420
+ }
1421
+ }
1422
+ }, [selectedDate, loadLogContent]);
1423
+ const handleLogCreated = useCallback5(() => {
1424
+ const files = listLogFiles();
1425
+ setLogFiles(files);
1426
+ const today = getTodayDate();
1427
+ setSelectedDate(today);
1428
+ const idx = files.findIndex((f) => f.date === today);
1429
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1430
+ loadLogContent(today);
1431
+ }, [loadLogContent]);
1432
+ return {
1433
+ logFiles,
1434
+ selectedDate,
1435
+ logContent,
1436
+ highlightedIndex,
1437
+ setHighlightedIndex,
1438
+ selectDate,
1439
+ refresh,
1440
+ handleExternalLogUpdate,
1441
+ handleLogCreated
1442
+ };
1443
+ }
1444
+
1445
+ // src/hooks/useModal.ts
1446
+ import { useCallback as useCallback6, useState as useState7 } from "react";
1447
+ function useModal() {
1448
+ const [modalType, setModalType] = useState7("none");
1449
+ const open4 = useCallback6((type) => setModalType(type), []);
1450
+ const close = useCallback6(() => setModalType("none"), []);
1451
+ const isOpen = modalType !== "none";
1452
+ return {
1453
+ type: modalType,
1454
+ isOpen,
1455
+ open: open4,
1456
+ close
1457
+ };
1458
+ }
1459
+
1460
+ // src/hooks/useListNavigation.ts
1461
+ import { useCallback as useCallback7, useState as useState8 } from "react";
1462
+ function useListNavigation(length) {
1463
+ const [index, setIndex] = useState8(0);
1464
+ const prev = useCallback7(() => {
1465
+ setIndex((i) => Math.max(0, i - 1));
1466
+ }, []);
1467
+ const next = useCallback7(() => {
1468
+ setIndex((i) => Math.min(length - 1, i + 1));
1469
+ }, [length]);
1470
+ const clampedIndex = Math.min(index, Math.max(0, length - 1));
1471
+ const reset = useCallback7(() => setIndex(0), []);
1472
+ return {
1473
+ index: length === 0 ? 0 : clampedIndex,
1474
+ prev,
1475
+ next,
1476
+ reset,
1477
+ setIndex
1478
+ };
1479
+ }
1480
+
1481
+ // src/hooks/useScrollToIndex.ts
1482
+ import { useEffect as useEffect5, useRef as useRef4 } from "react";
1483
+ function useScrollToIndex(index) {
1484
+ const scrollRef = useRef4(null);
1485
+ useEffect5(() => {
1486
+ const ref = scrollRef.current;
1487
+ if (!ref) return;
1488
+ const pos = ref.getItemPosition(index);
1489
+ const viewportHeight = ref.getViewportHeight();
1490
+ const scrollOffset = ref.getScrollOffset();
1491
+ if (!pos) return;
1492
+ if (pos.top < scrollOffset) {
1493
+ ref.scrollTo(pos.top);
1494
+ } else if (pos.top + pos.height > scrollOffset + viewportHeight) {
1495
+ ref.scrollTo(pos.top + pos.height - viewportHeight);
1496
+ }
1497
+ }, [index]);
1498
+ return scrollRef;
1499
+ }
1500
+
1501
+ // src/hooks/useRubberDuck.ts
1502
+ import { useCallback as useCallback8, useEffect as useEffect6, useState as useState9 } from "react";
1503
+ var DUCK_MESSAGES = [
1504
+ "Quack.",
1505
+ "Quack quack quack.",
1506
+ "Have you tried explaining it out loud?",
1507
+ "It's always DNS.",
1508
+ "Did you check the logs?",
1509
+ "Maybe add a console.log?",
1510
+ "Is it plugged in?",
1511
+ "Works on my machine.",
1512
+ "Have you tried reading the error message?",
1513
+ "I believe in you!",
1514
+ "It's probably a race condition.",
1515
+ "Have you tried turning it off and on again?",
1516
+ "Are you sure it compiled?",
1517
+ "It's not a bug, it's a feature.",
1518
+ "Did you clear the cache?",
1519
+ "Try deleting node_modules.",
1520
+ "That's quackers!",
1521
+ "Rubber duck debugging, activate!",
1522
+ "*supportive quacking*"
1523
+ ];
1524
+ var REACTION_MESSAGES = {
1525
+ "pr:merged": ["Quack! It shipped!", "Merged!", "To production we go!"],
1526
+ "pr:opened": ["A new PR! Exciting!", "Time for review!", "Fresh code incoming!"],
1527
+ "pr:reviewed": ["Feedback time!", "Reviews are in!", "*attentive quacking*"],
1528
+ "pr:approved": ["Approved!", "LGTM!", "Ship it!"],
1529
+ "pr:changes-requested": ["Some changes needed...", "Back to the drawing board!", "Iterate iterate!"],
1530
+ error: ["Uh oh...", "There there...", "*concerned quacking*", "Quack... not good."],
1531
+ "jira:transition": ["Ticket moving!", "Progress!", "Workflow in motion!"],
1532
+ "jira:linked": ["Ticket linked!", "Jira connection made!", "Tracking enabled!"],
1533
+ "jira:configured": ["Jira ready!", "Integration complete!", "Connected to Jira!"]
1534
+ };
1535
+ function useRubberDuck() {
1536
+ const [state, setState] = useState9({
1537
+ visible: false,
1538
+ message: DUCK_MESSAGES[0]
1539
+ });
1540
+ const getRandomMessage = useCallback8(() => {
1541
+ const index = Math.floor(Math.random() * DUCK_MESSAGES.length);
1542
+ return DUCK_MESSAGES[index];
1543
+ }, []);
1544
+ const toggleDuck = useCallback8(() => {
1545
+ setState((prev) => ({
1546
+ ...prev,
1547
+ visible: !prev.visible,
1548
+ message: !prev.visible ? getRandomMessage() : prev.message
1549
+ }));
1550
+ }, [getRandomMessage]);
1551
+ const quack = useCallback8(() => {
1552
+ if (state.visible) {
1553
+ setState((prev) => ({
1554
+ ...prev,
1555
+ message: getRandomMessage()
1556
+ }));
1557
+ }
1558
+ }, [state.visible, getRandomMessage]);
1559
+ const getReactionMessage = useCallback8((event) => {
1560
+ const messages = REACTION_MESSAGES[event];
1561
+ return messages[Math.floor(Math.random() * messages.length)];
1562
+ }, []);
1563
+ useEffect6(() => {
1564
+ const unsubscribe = duckEvents.subscribe((event) => {
1565
+ setState((prev) => ({
1566
+ ...prev,
1567
+ visible: true,
1568
+ message: getReactionMessage(event)
1569
+ }));
1570
+ });
1571
+ return unsubscribe;
1572
+ }, [getReactionMessage]);
1573
+ return {
1574
+ visible: state.visible,
1575
+ message: state.message,
1576
+ toggleDuck,
1577
+ quack
1578
+ };
1579
+ }
1160
1580
 
1161
1581
  // src/lib/clipboard.ts
1162
1582
  import { exec as exec2 } from "child_process";
@@ -1180,7 +1600,7 @@ async function copyToClipboard(text) {
1180
1600
  }
1181
1601
 
1182
1602
  // src/components/github/PullRequestsBox.tsx
1183
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1603
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1184
1604
  function PullRequestsBox({
1185
1605
  prs,
1186
1606
  selectedPR,
@@ -1192,9 +1612,11 @@ function PullRequestsBox({
1192
1612
  repoSlug,
1193
1613
  isFocused
1194
1614
  }) {
1195
- const [highlightedIndex, setHighlightedIndex] = useState5(0);
1615
+ const [highlightedIndex, setHighlightedIndex] = useState10(0);
1616
+ const [copied, setCopied] = useState10(false);
1617
+ const scrollRef = useScrollToIndex(highlightedIndex);
1196
1618
  const totalItems = prs.length + 1;
1197
- useEffect4(() => {
1619
+ useEffect7(() => {
1198
1620
  const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
1199
1621
  if (idx >= 0) setHighlightedIndex(idx);
1200
1622
  }, [selectedPR, prs]);
@@ -1218,54 +1640,74 @@ function PullRequestsBox({
1218
1640
  const pr = prs[highlightedIndex];
1219
1641
  const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
1220
1642
  copyToClipboard(url);
1643
+ setCopied(true);
1644
+ setTimeout(() => setCopied(false), 1500);
1645
+ }
1646
+ if (input === "o" && repoSlug && prs[highlightedIndex]) {
1647
+ const pr = prs[highlightedIndex];
1648
+ const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
1649
+ open2(url).catch(() => {
1650
+ });
1221
1651
  }
1222
1652
  },
1223
1653
  { isActive: isFocused }
1224
1654
  );
1225
1655
  const title = "[2] Pull Requests";
1226
1656
  const subtitle = branch ? ` (${branch})` : "";
1657
+ const copiedIndicator = copied ? " [Copied!]" : "";
1227
1658
  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
- ] }) });
1659
+ return /* @__PURE__ */ jsx3(
1660
+ TitledBox,
1661
+ {
1662
+ borderStyle: "round",
1663
+ titles: [`${title}${subtitle}${copiedIndicator}`],
1664
+ borderColor,
1665
+ height: 5,
1666
+ children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
1667
+ loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
1668
+ error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
1669
+ !loading && !error && /* @__PURE__ */ jsxs3(ScrollView2, { ref: scrollRef, children: [
1670
+ prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }, "empty"),
1671
+ prs.map((pr, idx) => {
1672
+ const isHighlighted = isFocused && idx === highlightedIndex;
1673
+ const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
1674
+ const cursor = isHighlighted ? ">" : " ";
1675
+ const indicator = isSelected ? " *" : "";
1676
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
1677
+ /* @__PURE__ */ jsxs3(Text3, { color: isHighlighted ? "yellow" : void 0, children: [
1678
+ cursor,
1679
+ " "
1680
+ ] }),
1681
+ /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
1682
+ "#",
1683
+ pr.number,
1684
+ " ",
1685
+ pr.isDraft ? "[Draft] " : "",
1686
+ pr.title
1687
+ ] }),
1688
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: indicator })
1689
+ ] }, pr.number);
1690
+ }),
1691
+ /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
1692
+ isFocused && highlightedIndex === prs.length ? "> " : " ",
1693
+ "+ Create new PR"
1694
+ ] }, "create")
1695
+ ] })
1696
+ ] })
1697
+ }
1698
+ );
1259
1699
  }
1260
1700
 
1261
1701
  // src/components/github/RemotesBox.tsx
1262
- import { useEffect as useEffect5, useState as useState6 } from "react";
1702
+ import { useEffect as useEffect8, useState as useState11 } from "react";
1263
1703
  import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
1264
1704
  import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
1705
+ import { ScrollView as ScrollView3 } from "ink-scroll-view";
1265
1706
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1266
1707
  function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
1267
- const [highlightedIndex, setHighlightedIndex] = useState6(0);
1268
- useEffect5(() => {
1708
+ const [highlightedIndex, setHighlightedIndex] = useState11(0);
1709
+ const scrollRef = useScrollToIndex(highlightedIndex);
1710
+ useEffect8(() => {
1269
1711
  const idx = remotes.findIndex((r) => r.name === selectedRemote);
1270
1712
  if (idx >= 0) setHighlightedIndex(idx);
1271
1713
  }, [selectedRemote, remotes]);
@@ -1286,11 +1728,11 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
1286
1728
  );
1287
1729
  const title = "[1] Remotes";
1288
1730
  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: [
1731
+ 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
1732
  loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
1291
1733
  error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
1292
1734
  !loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
1293
- !loading && !error && remotes.map((remote, idx) => {
1735
+ !loading && !error && remotes.length > 0 && /* @__PURE__ */ jsx4(ScrollView3, { ref: scrollRef, children: remotes.map((remote, idx) => {
1294
1736
  const isHighlighted = isFocused && idx === highlightedIndex;
1295
1737
  const isSelected = remote.name === selectedRemote;
1296
1738
  const cursor = isHighlighted ? ">" : " ";
@@ -1308,7 +1750,7 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
1308
1750
  ] }),
1309
1751
  /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: indicator })
1310
1752
  ] }, remote.name);
1311
- })
1753
+ }) })
1312
1754
  ] }) });
1313
1755
  }
1314
1756
 
@@ -1318,9 +1760,9 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1318
1760
  const repo = useGitRepo();
1319
1761
  const pullRequests = usePullRequests();
1320
1762
  const polling = usePRPolling();
1321
- const [focusedBox, setFocusedBox] = useState7("remotes");
1322
- const lastFetchedRef = useRef3(null);
1323
- useEffect6(() => {
1763
+ const [focusedBox, setFocusedBox] = useState12("remotes");
1764
+ const lastFetchedRef = useRef5(null);
1765
+ useEffect9(() => {
1324
1766
  if (repo.loading || !repo.currentBranch || !repo.currentRepoSlug) return;
1325
1767
  const current = { branch: repo.currentBranch, repoSlug: repo.currentRepoSlug };
1326
1768
  const last = lastFetchedRef.current;
@@ -1328,15 +1770,15 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1328
1770
  lastFetchedRef.current = current;
1329
1771
  pullRequests.fetchPRsAndDetails(repo.currentBranch, repo.currentRepoSlug);
1330
1772
  }, [repo.loading, repo.currentBranch, repo.currentRepoSlug, pullRequests.fetchPRsAndDetails]);
1331
- useEffect6(() => {
1773
+ useEffect9(() => {
1332
1774
  if (isFocused) {
1333
1775
  repo.refreshBranch();
1334
1776
  }
1335
1777
  }, [isFocused, repo.refreshBranch]);
1336
- useEffect6(() => {
1778
+ useEffect9(() => {
1337
1779
  onFocusedBoxChange == null ? void 0 : onFocusedBoxChange(focusedBox);
1338
1780
  }, [focusedBox, onFocusedBoxChange]);
1339
- const handleRemoteSelect = useCallback4(
1781
+ const handleRemoteSelect = useCallback9(
1340
1782
  (remoteName) => {
1341
1783
  repo.selectRemote(remoteName);
1342
1784
  const remote = repo.remotes.find((r) => r.name === remoteName);
@@ -1348,28 +1790,31 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1348
1790
  },
1349
1791
  [repo.selectRemote, repo.remotes, repo.currentBranch, pullRequests.fetchPRsAndDetails]
1350
1792
  );
1351
- const handlePRSelect = useCallback4(
1793
+ const handlePRSelect = useCallback9(
1352
1794
  (pr) => {
1353
1795
  pullRequests.selectPR(pr, repo.currentRepoSlug);
1354
1796
  },
1355
1797
  [pullRequests.selectPR, repo.currentRepoSlug]
1356
1798
  );
1357
- const createPRContext = useRef3({ repo, pullRequests, onLogUpdated });
1799
+ const createPRContext = useRef5({ repo, pullRequests, onLogUpdated });
1358
1800
  createPRContext.current = { repo, pullRequests, onLogUpdated };
1359
- const handleCreatePR = useCallback4(() => {
1801
+ const handleCreatePR = useCallback9(() => {
1360
1802
  const { repo: repo2, pullRequests: pullRequests2 } = createPRContext.current;
1361
1803
  if (!repo2.currentBranch) {
1362
1804
  pullRequests2.setError("prs", "No branch detected");
1805
+ duckEvents.emit("error");
1363
1806
  return;
1364
1807
  }
1365
1808
  const remoteResult = findRemoteWithBranch(repo2.currentBranch);
1366
1809
  if (!remoteResult.success) {
1367
1810
  pullRequests2.setError("prs", "Push your branch to a remote first");
1811
+ duckEvents.emit("error");
1368
1812
  return;
1369
1813
  }
1370
1814
  openPRCreationPage(remoteResult.data.owner, repo2.currentBranch, (error) => {
1371
1815
  if (error) {
1372
1816
  pullRequests2.setError("prs", `Failed to create PR: ${error.message}`);
1817
+ duckEvents.emit("error");
1373
1818
  }
1374
1819
  });
1375
1820
  if (!repo2.currentRepoSlug) return;
@@ -1385,6 +1830,7 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1385
1830
  const ctx = createPRContext.current;
1386
1831
  const tickets = ctx.repo.repoPath && ctx.repo.currentBranch ? getLinkedTickets(ctx.repo.repoPath, ctx.repo.currentBranch).map((t) => t.key) : [];
1387
1832
  logPRCreated(newPR.number, newPR.title, tickets);
1833
+ duckEvents.emit("pr:opened");
1388
1834
  (_a = ctx.onLogUpdated) == null ? void 0 : _a.call(ctx);
1389
1835
  ctx.pullRequests.setSelectedPR(newPR);
1390
1836
  if (ctx.repo.currentRepoSlug) {
@@ -1455,296 +1901,95 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1455
1901
  }
1456
1902
 
1457
1903
  // 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";
1904
+ import open3 from "open";
1905
+ import { useEffect as useEffect11, useRef as useRef6 } from "react";
1462
1906
 
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);
1907
+ // src/components/jira/LinkTicketModal.tsx
1908
+ import { useState as useState13 } from "react";
1909
+ import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
1910
+
1911
+ // src/components/ui/TextInput.tsx
1912
+ import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
1913
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1914
+ function TextInput({ value, onChange, placeholder, isActive, mask }) {
1915
+ useInput5(
1916
+ (input, key) => {
1917
+ if (key.backspace || key.delete) {
1918
+ if (value.length > 0) {
1919
+ onChange(value.slice(0, -1));
1488
1920
  }
1489
- }
1490
- if (!ticketKey) {
1491
- setTickets([]);
1492
- setJiraState("no_tickets");
1493
1921
  return;
1494
1922
  }
1495
- const siteUrl = getJiraSiteUrl(repoPath);
1496
- const creds = getJiraCredentials(repoPath);
1497
- if (!siteUrl || !creds.email || !creds.apiToken) {
1498
- setTickets([]);
1499
- setJiraState("no_tickets");
1923
+ if (key.return || key.escape || key.upArrow || key.downArrow || key.tab) {
1500
1924
  return;
1501
1925
  }
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;
1926
+ if (input && input.length === 1 && input.charCodeAt(0) >= 32) {
1927
+ onChange(value + input);
1568
1928
  }
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
1929
  },
1582
- []
1930
+ { isActive }
1583
1931
  );
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
- };
1932
+ const displayValue = mask ? "*".repeat(value.length) : value;
1933
+ const showPlaceholder = value.length === 0 && placeholder;
1934
+ return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { children: [
1935
+ showPlaceholder ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx6(Text6, { children: displayValue }),
1936
+ isActive && /* @__PURE__ */ jsx6(Text6, { backgroundColor: "yellow", children: " " })
1937
+ ] }) });
1694
1938
  }
1695
1939
 
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
- };
1940
+ // src/components/jira/LinkTicketModal.tsx
1941
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1942
+ function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
1943
+ const [ticketInput, setTicketInput] = useState13("");
1944
+ const canSubmit = ticketInput.trim().length > 0;
1945
+ useInput6(
1946
+ (_input, key) => {
1947
+ if (loading) return;
1948
+ if (key.escape) {
1949
+ onCancel();
1950
+ return;
1951
+ }
1952
+ if (key.return && canSubmit) {
1953
+ onSubmit(ticketInput.trim());
1954
+ }
1955
+ },
1956
+ { isActive: !loading }
1957
+ );
1958
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
1959
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "yellow", children: "Link Jira Ticket" }),
1960
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Type ticket ID, Enter to submit, Esc to cancel" }),
1961
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
1962
+ error && /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: error }) }),
1963
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1964
+ /* @__PURE__ */ jsx7(Text7, { color: "blue", children: "Ticket: " }),
1965
+ /* @__PURE__ */ jsx7(TextInput, { value: ticketInput, onChange: setTicketInput, placeholder: "PROJ-123", isActive: !loading })
1966
+ ] }),
1967
+ loading && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "Fetching ticket..." }) }),
1968
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Examples: PROJ-123 or https://company.atlassian.net/browse/PROJ-123" }) })
1969
+ ] });
1709
1970
  }
1710
1971
 
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
- }
1972
+ // src/components/jira/JiraView.tsx
1973
+ import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
1974
+ import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
1731
1975
 
1732
1976
  // 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";
1977
+ import { useEffect as useEffect10, useState as useState14 } from "react";
1978
+ import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
1735
1979
  import SelectInput from "ink-select-input";
1736
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1980
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1737
1981
  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(() => {
1982
+ const [transitions, setTransitions] = useState14([]);
1983
+ const [loading, setLoading] = useState14(true);
1984
+ const [applying, setApplying] = useState14(false);
1985
+ const [error, setError] = useState14(null);
1986
+ useEffect10(() => {
1743
1987
  const fetchTransitions = async () => {
1744
1988
  const siteUrl = getJiraSiteUrl(repoPath);
1745
1989
  const creds = getJiraCredentials(repoPath);
1746
1990
  if (!siteUrl || !creds.email || !creds.apiToken) {
1747
1991
  setError("Jira not configured");
1992
+ duckEvents.emit("error");
1748
1993
  setLoading(false);
1749
1994
  return;
1750
1995
  }
@@ -1754,6 +1999,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1754
1999
  setTransitions(result.data);
1755
2000
  } else {
1756
2001
  setError(result.error);
2002
+ duckEvents.emit("error");
1757
2003
  }
1758
2004
  setLoading(false);
1759
2005
  };
@@ -1766,6 +2012,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1766
2012
  const creds = getJiraCredentials(repoPath);
1767
2013
  if (!siteUrl || !creds.email || !creds.apiToken) {
1768
2014
  setError("Jira not configured");
2015
+ duckEvents.emit("error");
1769
2016
  setApplying(false);
1770
2017
  return;
1771
2018
  }
@@ -1774,13 +2021,15 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1774
2021
  if (result.success) {
1775
2022
  const transition = transitions.find((t) => t.id === item.value);
1776
2023
  const newStatus = (transition == null ? void 0 : transition.to.name) ?? item.label;
2024
+ duckEvents.emit("jira:transition");
1777
2025
  onComplete(newStatus);
1778
2026
  } else {
1779
2027
  setError(result.error);
2028
+ duckEvents.emit("error");
1780
2029
  setApplying(false);
1781
2030
  }
1782
2031
  };
1783
- useInput5(
2032
+ useInput7(
1784
2033
  (_input, key) => {
1785
2034
  if (key.escape && !applying) {
1786
2035
  onCancel();
@@ -1792,24 +2041,28 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1792
2041
  label: t.name,
1793
2042
  value: t.id
1794
2043
  }));
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: [
2044
+ const initialIndex = Math.max(
2045
+ 0,
2046
+ transitions.findIndex((t) => t.to.name === currentStatus)
2047
+ );
2048
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
2049
+ /* @__PURE__ */ jsxs8(Text8, { bold: true, color: "yellow", children: [
1798
2050
  "Change Status: ",
1799
2051
  ticketKey
1800
2052
  ] }),
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" }) })
2053
+ loading && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Loading transitions..." }),
2054
+ error && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "red", children: error }) }),
2055
+ !loading && !error && transitions.length === 0 && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "No available transitions" }),
2056
+ !loading && !error && transitions.length > 0 && !applying && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx8(SelectInput, { items, initialIndex, onSelect: handleSelect }) }),
2057
+ applying && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Updating status..." }) }),
2058
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Esc to cancel" }) })
1807
2059
  ] });
1808
2060
  }
1809
2061
 
1810
2062
  // 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";
2063
+ import { useState as useState15 } from "react";
2064
+ import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
2065
+ import { ScrollView as ScrollView4 } from "ink-scroll-view";
1813
2066
 
1814
2067
  // src/lib/editor.ts
1815
2068
  import { spawnSync as spawnSync2 } from "child_process";
@@ -1840,28 +2093,58 @@ function openInEditor(content, filename) {
1840
2093
  }
1841
2094
 
1842
2095
  // src/components/jira/ConfigureJiraSiteModal.tsx
1843
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2096
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2097
+ var MAX_VISIBLE_ITEMS = 4;
1844
2098
  function ConfigureJiraSiteModal({
1845
2099
  initialSiteUrl,
1846
2100
  initialEmail,
2101
+ existingConfigs = [],
1847
2102
  onSubmit,
1848
2103
  onCancel,
1849
2104
  loading,
1850
2105
  error
1851
2106
  }) {
1852
- const [siteUrl, setSiteUrl] = useState13(initialSiteUrl ?? "");
1853
- const [email, setEmail] = useState13(initialEmail ?? "");
1854
- const [apiToken, setApiToken] = useState13("");
1855
- const [selectedItem, setSelectedItem] = useState13("siteUrl");
2107
+ const hasExisting = existingConfigs.length > 0;
2108
+ const [mode, setMode] = useState15(hasExisting ? "choose" : "manual");
2109
+ const [selectedExisting, setSelectedExisting] = useState15(0);
2110
+ const scrollRef = useScrollToIndex(selectedExisting);
2111
+ const [siteUrl, setSiteUrl] = useState15(initialSiteUrl ?? "");
2112
+ const [email, setEmail] = useState15(initialEmail ?? "");
2113
+ const [apiToken, setApiToken] = useState15("");
2114
+ const [selectedItem, setSelectedItem] = useState15("siteUrl");
1856
2115
  const items = ["siteUrl", "email", "apiToken", "submit"];
1857
2116
  const canSubmit = siteUrl.trim() && email.trim() && apiToken.trim();
1858
- useInput6(
2117
+ const chooseItems = existingConfigs.length + 1;
2118
+ useInput8(
1859
2119
  (input, key) => {
1860
2120
  if (loading) return;
1861
2121
  if (key.escape) {
2122
+ if (mode === "manual" && hasExisting) {
2123
+ setMode("choose");
2124
+ return;
2125
+ }
1862
2126
  onCancel();
1863
2127
  return;
1864
2128
  }
2129
+ if (mode === "choose") {
2130
+ if (key.upArrow || input === "k") {
2131
+ setSelectedExisting((prev) => Math.max(0, prev - 1));
2132
+ return;
2133
+ }
2134
+ if (key.downArrow || input === "j") {
2135
+ setSelectedExisting((prev) => Math.min(chooseItems - 1, prev + 1));
2136
+ return;
2137
+ }
2138
+ if (key.return) {
2139
+ if (selectedExisting < existingConfigs.length) {
2140
+ const config = existingConfigs[selectedExisting];
2141
+ onSubmit(config.siteUrl, config.email, config.apiToken);
2142
+ } else {
2143
+ setMode("manual");
2144
+ }
2145
+ }
2146
+ return;
2147
+ }
1865
2148
  if (key.upArrow || input === "k") {
1866
2149
  setSelectedItem((prev) => {
1867
2150
  const idx = items.indexOf(prev);
@@ -1904,104 +2187,71 @@ function ConfigureJiraSiteModal({
1904
2187
  const prefix = isSelected ? "> " : " ";
1905
2188
  const color = isSelected ? "yellow" : void 0;
1906
2189
  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: [
2190
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2191
+ /* @__PURE__ */ jsxs9(Text9, { color, bold: isSelected, children: [
1909
2192
  prefix,
1910
2193
  label
1911
2194
  ] }),
1912
- value !== void 0 && /* @__PURE__ */ jsx7(Box7, { marginLeft: 4, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: displayValue || "(empty - press Enter to edit)" }) })
2195
+ value !== void 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: displayValue || "(empty - press Enter to edit)" }) })
1913
2196
  ] });
1914
2197
  };
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 }) }),
2198
+ if (mode === "choose") {
2199
+ const totalItems = existingConfigs.length + 1;
2200
+ const listHeight = Math.min(totalItems * 2, MAX_VISIBLE_ITEMS * 2);
2201
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
2202
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: "Configure Jira Site" }),
2203
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Select an existing configuration or enter new credentials" }),
2204
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
2205
+ error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
2206
+ /* @__PURE__ */ jsx9(Box9, { height: listHeight, overflow: "hidden", children: /* @__PURE__ */ jsxs9(ScrollView4, { ref: scrollRef, children: [
2207
+ existingConfigs.map((config, idx) => {
2208
+ const isSelected = selectedExisting === idx;
2209
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2210
+ /* @__PURE__ */ jsxs9(Text9, { color: isSelected ? "yellow" : void 0, bold: isSelected, children: [
2211
+ isSelected ? "> " : " ",
2212
+ config.siteUrl
2213
+ ] }),
2214
+ /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2215
+ " ",
2216
+ config.email
2217
+ ] })
2218
+ ] }, config.siteUrl + config.email);
2219
+ }),
2220
+ /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs9(
2221
+ Text9,
2222
+ {
2223
+ color: selectedExisting === existingConfigs.length ? "yellow" : void 0,
2224
+ bold: selectedExisting === existingConfigs.length,
2225
+ children: [
2226
+ selectedExisting === existingConfigs.length ? "> " : " ",
2227
+ "Enter new credentials..."
2228
+ ]
2229
+ }
2230
+ ) })
2231
+ ] }) }),
2232
+ loading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Validating credentials..." }) })
2233
+ ] });
2234
+ }
2235
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
2236
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: "Configure Jira Site" }),
2237
+ /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2238
+ "Up/Down to select, Enter to edit, Esc to ",
2239
+ hasExisting ? "go back" : "cancel"
2240
+ ] }),
2241
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
2242
+ error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
1920
2243
  renderItem("siteUrl", "Site URL (e.g., https://company.atlassian.net)", siteUrl),
1921
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
2244
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
1922
2245
  renderItem("email", "Email", email),
1923
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
2246
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
1924
2247
  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: [
2248
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
2249
+ /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs9(Text9, { color: selectedItem === "submit" ? "green" : void 0, bold: selectedItem === "submit", children: [
1927
2250
  selectedItem === "submit" ? "> " : " ",
1928
2251
  canSubmit ? "[Save Configuration]" : "[Fill all fields first]"
1929
2252
  ] }) }),
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" }) })
2253
+ loading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Validating credentials..." }) }),
2254
+ /* @__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
2255
  ] });
2006
2256
  }
2007
2257
 
@@ -2031,8 +2281,8 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2031
2281
  const jira = useJiraTickets();
2032
2282
  const modal = useModal();
2033
2283
  const nav = useListNavigation(jira.tickets.length);
2034
- const lastInitRef = useRef5(null);
2035
- useEffect9(() => {
2284
+ const lastInitRef = useRef6(null);
2285
+ useEffect11(() => {
2036
2286
  if (repo.loading || !repo.repoPath || !repo.currentBranch) return;
2037
2287
  const current = { branch: repo.currentBranch };
2038
2288
  const last = lastInitRef.current;
@@ -2040,17 +2290,17 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2040
2290
  lastInitRef.current = current;
2041
2291
  jira.initializeJiraState(repo.repoPath, repo.currentBranch, repo.currentRepoSlug);
2042
2292
  }, [repo.loading, repo.repoPath, repo.currentBranch, repo.currentRepoSlug, jira.initializeJiraState]);
2043
- useEffect9(() => {
2293
+ useEffect11(() => {
2044
2294
  if (isFocused) {
2045
2295
  repo.refreshBranch();
2046
2296
  } else {
2047
2297
  modal.close();
2048
2298
  }
2049
2299
  }, [isFocused, repo.refreshBranch, modal.close]);
2050
- useEffect9(() => {
2300
+ useEffect11(() => {
2051
2301
  onModalChange == null ? void 0 : onModalChange(modal.isOpen);
2052
2302
  }, [modal.isOpen, onModalChange]);
2053
- useEffect9(() => {
2303
+ useEffect11(() => {
2054
2304
  onJiraStateChange == null ? void 0 : onJiraStateChange(jira.jiraState);
2055
2305
  }, [jira.jiraState, onJiraStateChange]);
2056
2306
  const handleConfigureSubmit = async (siteUrl, email, apiToken) => {
@@ -2077,7 +2327,7 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2077
2327
  const ticket = jira.tickets[nav.index];
2078
2328
  const siteUrl = getJiraSiteUrl(repo.repoPath);
2079
2329
  if (ticket && siteUrl) {
2080
- open2(`${siteUrl}/browse/${ticket.key}`).catch(() => {
2330
+ open3(`${siteUrl}/browse/${ticket.key}`).catch(() => {
2081
2331
  });
2082
2332
  }
2083
2333
  };
@@ -2099,6 +2349,11 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2099
2349
  modal.close();
2100
2350
  jira.refreshTickets(repo.repoPath, repo.currentBranch);
2101
2351
  };
2352
+ const handleRemoveConfig = () => {
2353
+ if (!repo.repoPath || !repo.currentBranch) return;
2354
+ clearJiraConfig(repo.repoPath);
2355
+ jira.initializeJiraState(repo.repoPath, repo.currentBranch, repo.currentRepoSlug);
2356
+ };
2102
2357
  useInput9(
2103
2358
  (input, key) => {
2104
2359
  if (input === "c" && jira.jiraState === "not_configured") {
@@ -2109,6 +2364,10 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2109
2364
  modal.open("link");
2110
2365
  return;
2111
2366
  }
2367
+ if (input === "r" && jira.jiraState !== "not_configured") {
2368
+ handleRemoveConfig();
2369
+ return;
2370
+ }
2112
2371
  if (jira.jiraState === "has_tickets") {
2113
2372
  if (key.upArrow || input === "k") nav.prev();
2114
2373
  if (key.downArrow || input === "j") nav.next();
@@ -2126,11 +2385,13 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2126
2385
  if (modal.type === "configure") {
2127
2386
  const siteUrl = repo.repoPath ? getJiraSiteUrl(repo.repoPath) : void 0;
2128
2387
  const creds = repo.repoPath ? getJiraCredentials(repo.repoPath) : { email: null, apiToken: null };
2388
+ const existingConfigs = getExistingJiraConfigs(repo.repoPath ?? void 0);
2129
2389
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
2130
2390
  ConfigureJiraSiteModal,
2131
2391
  {
2132
2392
  initialSiteUrl: siteUrl ?? void 0,
2133
2393
  initialEmail: creds.email ?? void 0,
2394
+ existingConfigs,
2134
2395
  onSubmit: handleConfigureSubmit,
2135
2396
  onCancel: () => {
2136
2397
  modal.close();
@@ -2187,73 +2448,14 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2187
2448
  }
2188
2449
 
2189
2450
  // src/components/logs/LogsView.tsx
2190
- import { useEffect as useEffect10 } from "react";
2451
+ import { useEffect as useEffect12 } from "react";
2191
2452
  import { Box as Box14, useInput as useInput12 } from "ink";
2192
2453
 
2193
- // src/components/logs/LogsHistoryBox.tsx
2454
+ // src/components/logs/LogViewerBox.tsx
2455
+ import { useRef as useRef7, useState as useState16 } from "react";
2194
2456
  import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
2195
2457
  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";
2458
+ import { ScrollView as ScrollView5 } from "ink-scroll-view";
2257
2459
  import TextInput2 from "ink-text-input";
2258
2460
 
2259
2461
  // src/lib/claude/api.ts
@@ -2340,18 +2542,18 @@ Generate the standup notes:`;
2340
2542
  }
2341
2543
 
2342
2544
  // src/components/logs/LogViewerBox.tsx
2343
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2545
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2344
2546
  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);
2547
+ const scrollRef = useRef7(null);
2548
+ const [isInputMode, setIsInputMode] = useState16(false);
2549
+ const [inputValue, setInputValue] = useState16("");
2550
+ const [isGeneratingStandup, setIsGeneratingStandup] = useState16(false);
2551
+ const [standupResult, setStandupResult] = useState16(null);
2552
+ const claudeProcessRef = useRef7(null);
2351
2553
  const title = "[6] Log Content";
2352
2554
  const borderColor = isFocused ? "yellow" : void 0;
2353
2555
  const displayTitle = date ? `${title} - ${date}.md` : title;
2354
- useInput11(
2556
+ useInput10(
2355
2557
  (input, key) => {
2356
2558
  var _a, _b, _c;
2357
2559
  if (key.escape && isInputMode) {
@@ -2430,14 +2632,14 @@ ${value.trim()}
2430
2632
  setIsInputMode(false);
2431
2633
  onRefresh();
2432
2634
  };
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 })
2635
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", flexGrow: 1, children: [
2636
+ /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx12(ScrollView5, { ref: scrollRef, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
2637
+ !date && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Select a log file to view" }),
2638
+ date && content === null && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Log file not found" }),
2639
+ date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Empty log file" }),
2640
+ date && content && content.trim() !== "" && /* @__PURE__ */ jsx12(Markdown, { children: content })
2439
2641
  ] }) }) }) }),
2440
- isInputMode && /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: ["Add Entry"], borderColor: "yellow", children: /* @__PURE__ */ jsx13(Box13, { paddingX: 1, children: /* @__PURE__ */ jsx13(
2642
+ isInputMode && /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: ["Add Entry"], borderColor: "yellow", children: /* @__PURE__ */ jsx12(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx12(
2441
2643
  TextInput2,
2442
2644
  {
2443
2645
  value: inputValue,
@@ -2445,30 +2647,84 @@ ${value.trim()}
2445
2647
  onSubmit: handleInputSubmit
2446
2648
  }
2447
2649
  ) }) }),
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" })
2650
+ isGeneratingStandup && /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: ["Standup Notes"], borderColor: "yellow", children: /* @__PURE__ */ jsxs12(Box12, { paddingX: 1, flexDirection: "column", children: [
2651
+ /* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "Generating standup notes..." }),
2652
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Press Esc to cancel" })
2451
2653
  ] }) }),
2452
- standupResult && /* @__PURE__ */ jsx13(
2453
- TitledBox6,
2654
+ standupResult && /* @__PURE__ */ jsx12(
2655
+ TitledBox5,
2454
2656
  {
2455
2657
  borderStyle: "round",
2456
2658
  titles: ["Standup Notes"],
2457
2659
  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" })
2660
+ children: /* @__PURE__ */ jsxs12(Box12, { paddingX: 1, flexDirection: "column", children: [
2661
+ standupResult.type === "error" ? /* @__PURE__ */ jsx12(Text12, { color: "red", children: standupResult.message }) : /* @__PURE__ */ jsx12(Markdown, { children: standupResult.message }),
2662
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Press Esc to dismiss" })
2461
2663
  ] })
2462
2664
  }
2463
2665
  )
2464
2666
  ] });
2465
2667
  }
2466
2668
 
2669
+ // src/components/logs/LogsHistoryBox.tsx
2670
+ import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
2671
+ import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
2672
+ import { ScrollView as ScrollView6 } from "ink-scroll-view";
2673
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2674
+ function LogsHistoryBox({
2675
+ logFiles,
2676
+ selectedDate,
2677
+ highlightedIndex,
2678
+ onHighlight,
2679
+ onSelect,
2680
+ isFocused
2681
+ }) {
2682
+ const scrollRef = useScrollToIndex(highlightedIndex);
2683
+ const title = "[5] Logs";
2684
+ const borderColor = isFocused ? "yellow" : void 0;
2685
+ useInput11(
2686
+ (input, key) => {
2687
+ if (logFiles.length === 0) return;
2688
+ if (key.upArrow || input === "k") {
2689
+ onHighlight(Math.max(0, highlightedIndex - 1));
2690
+ }
2691
+ if (key.downArrow || input === "j") {
2692
+ onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
2693
+ }
2694
+ if (key.return) {
2695
+ const file = logFiles[highlightedIndex];
2696
+ if (file) {
2697
+ onSelect(file.date);
2698
+ }
2699
+ }
2700
+ },
2701
+ { isActive: isFocused }
2702
+ );
2703
+ return /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
2704
+ logFiles.length === 0 && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "No logs yet" }),
2705
+ logFiles.length > 0 && /* @__PURE__ */ jsx13(ScrollView6, { ref: scrollRef, children: logFiles.map((file, idx) => {
2706
+ const isHighlighted = idx === highlightedIndex;
2707
+ const isSelected = file.date === selectedDate;
2708
+ const cursor = isHighlighted ? ">" : " ";
2709
+ const indicator = isSelected ? " *" : "";
2710
+ return /* @__PURE__ */ jsxs13(Box13, { children: [
2711
+ /* @__PURE__ */ jsxs13(Text13, { color: isHighlighted ? "yellow" : void 0, children: [
2712
+ cursor,
2713
+ " "
2714
+ ] }),
2715
+ /* @__PURE__ */ jsx13(Text13, { color: file.isToday ? "green" : void 0, bold: file.isToday, children: file.date }),
2716
+ file.isToday && /* @__PURE__ */ jsx13(Text13, { color: "green", children: " (today)" }),
2717
+ /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: indicator })
2718
+ ] }, file.date);
2719
+ }) })
2720
+ ] }) });
2721
+ }
2722
+
2467
2723
  // src/components/logs/LogsView.tsx
2468
2724
  import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2469
2725
  function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
2470
2726
  const logs = useLogs();
2471
- useEffect10(() => {
2727
+ useEffect12(() => {
2472
2728
  if (refreshKey !== void 0 && refreshKey > 0) {
2473
2729
  logs.handleExternalLogUpdate();
2474
2730
  }
@@ -2513,22 +2769,25 @@ var globalBindings = [
2513
2769
  { key: "j/k", label: "Navigate" },
2514
2770
  { key: "Ctrl+C", label: "Quit" }
2515
2771
  ];
2516
- var modalBindings = [
2517
- { key: "Esc", label: "Cancel" }
2518
- ];
2519
- function KeybindingsBar({ contextBindings = [], modalOpen = false }) {
2772
+ var modalBindings = [{ key: "Esc", label: "Cancel" }];
2773
+ var DUCK_ASCII = "<(')___";
2774
+ function KeybindingsBar({ contextBindings = [], modalOpen = false, duck }) {
2520
2775
  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)) });
2776
+ return /* @__PURE__ */ jsxs15(Box15, { flexShrink: 0, paddingX: 1, gap: 2, children: [
2777
+ allBindings.map((binding) => /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
2778
+ /* @__PURE__ */ jsx15(Text14, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
2779
+ /* @__PURE__ */ jsx15(Text14, { dimColor: true, children: binding.label })
2780
+ ] }, binding.key)),
2781
+ (duck == null ? void 0 : duck.visible) && /* @__PURE__ */ jsxs15(Box15, { flexGrow: 1, justifyContent: "flex-end", gap: 1, children: [
2782
+ /* @__PURE__ */ jsx15(Text14, { children: DUCK_ASCII }),
2783
+ /* @__PURE__ */ jsx15(Text14, { dimColor: true, children: duck.message })
2784
+ ] })
2785
+ ] });
2525
2786
  }
2526
2787
 
2527
2788
  // src/constants/github.ts
2528
2789
  var GITHUB_KEYBINDINGS = {
2529
- remotes: [
2530
- { key: "Space", label: "Select Remote" }
2531
- ],
2790
+ remotes: [{ key: "Space", label: "Select Remote" }],
2532
2791
  prs: [
2533
2792
  { key: "Space", label: "Select" },
2534
2793
  { key: "n", label: "New PR", color: "green" },
@@ -2545,21 +2804,23 @@ var GITHUB_KEYBINDINGS = {
2545
2804
  // src/constants/jira.ts
2546
2805
  var JIRA_KEYBINDINGS = {
2547
2806
  not_configured: [{ key: "c", label: "Configure Jira" }],
2548
- no_tickets: [{ key: "l", label: "Link Ticket" }],
2807
+ no_tickets: [
2808
+ { key: "l", label: "Link Ticket" },
2809
+ { key: "r", label: "Remove Config", color: "red" }
2810
+ ],
2549
2811
  has_tickets: [
2550
2812
  { key: "l", label: "Link" },
2551
2813
  { key: "s", label: "Status" },
2552
2814
  { key: "d", label: "Unlink", color: "red" },
2553
2815
  { key: "o", label: "Open", color: "green" },
2554
- { key: "y", label: "Copy Link" }
2816
+ { key: "y", label: "Copy Link" },
2817
+ { key: "r", label: "Remove Config", color: "red" }
2555
2818
  ]
2556
2819
  };
2557
2820
 
2558
2821
  // src/constants/logs.ts
2559
2822
  var LOGS_KEYBINDINGS = {
2560
- history: [
2561
- { key: "Enter", label: "Select" }
2562
- ],
2823
+ history: [{ key: "Enter", label: "Select" }],
2563
2824
  viewer: [
2564
2825
  { key: "i", label: "Add Entry" },
2565
2826
  { key: "e", label: "Edit" },
@@ -2588,12 +2849,13 @@ function computeKeybindings(focusedView, state) {
2588
2849
  import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
2589
2850
  function App() {
2590
2851
  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");
2852
+ const [focusedView, setFocusedView] = useState17("github");
2853
+ const [modalOpen, setModalOpen] = useState17(false);
2854
+ const [logRefreshKey, setLogRefreshKey] = useState17(0);
2855
+ const duck = useRubberDuck();
2856
+ const [githubFocusedBox, setGithubFocusedBox] = useState17("remotes");
2857
+ const [jiraState, setJiraState] = useState17("not_configured");
2858
+ const [logsFocusedBox, setLogsFocusedBox] = useState17("history");
2597
2859
  const keybindings = useMemo2(
2598
2860
  () => computeKeybindings(focusedView, {
2599
2861
  github: { focusedBox: githubFocusedBox },
@@ -2602,7 +2864,7 @@ function App() {
2602
2864
  }),
2603
2865
  [focusedView, githubFocusedBox, jiraState, modalOpen, logsFocusedBox]
2604
2866
  );
2605
- const handleLogUpdated = useCallback9(() => {
2867
+ const handleLogUpdated = useCallback10(() => {
2606
2868
  setLogRefreshKey((prev) => prev + 1);
2607
2869
  }, []);
2608
2870
  useInput13(
@@ -2624,6 +2886,12 @@ function App() {
2624
2886
  setFocusedView("logs");
2625
2887
  setLogsFocusedBox("viewer");
2626
2888
  }
2889
+ if (input === "d") {
2890
+ duck.toggleDuck();
2891
+ }
2892
+ if (input === "q" && duck.visible) {
2893
+ duck.quack();
2894
+ }
2627
2895
  },
2628
2896
  { isActive: !modalOpen }
2629
2897
  );
@@ -2658,7 +2926,14 @@ function App() {
2658
2926
  }
2659
2927
  ) })
2660
2928
  ] }),
2661
- /* @__PURE__ */ jsx16(KeybindingsBar, { contextBindings: keybindings, modalOpen })
2929
+ /* @__PURE__ */ jsx16(
2930
+ KeybindingsBar,
2931
+ {
2932
+ contextBindings: keybindings,
2933
+ modalOpen,
2934
+ duck: { visible: duck.visible, message: duck.message }
2935
+ }
2936
+ )
2662
2937
  ] });
2663
2938
  }
2664
2939
 
@@ -2666,17 +2941,14 @@ function App() {
2666
2941
  import { render as inkRender } from "ink";
2667
2942
 
2668
2943
  // src/lib/Screen.tsx
2944
+ import { useCallback as useCallback11, useEffect as useEffect13, useState as useState18 } from "react";
2669
2945
  import { Box as Box17, useStdout as useStdout2 } from "ink";
2670
- import { useCallback as useCallback10, useEffect as useEffect11, useState as useState17 } from "react";
2671
2946
  import { jsx as jsx17 } from "react/jsx-runtime";
2672
2947
  function Screen({ children }) {
2673
2948
  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(() => {
2949
+ const getSize = useCallback11(() => ({ height: stdout.rows, width: stdout.columns }), [stdout]);
2950
+ const [size, setSize] = useState18(getSize);
2951
+ useEffect13(() => {
2680
2952
  const onResize = () => setSize(getSize());
2681
2953
  stdout.on("resize", onResize);
2682
2954
  return () => {