clairo 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1318 -877
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4,15 +4,31 @@
4
4
  import meow from "meow";
5
5
 
6
6
  // src/app.tsx
7
- import { useCallback as useCallback4, useState as useState10 } 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 { exec as exec3 } from "child_process";
12
- import { useCallback, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
11
+ import { useCallback as useCallback9, useEffect as useEffect9, useRef as useRef5, useState as useState12 } from "react";
13
12
  import { TitledBox as TitledBox3 } from "@mishieck/ink-titled-box";
14
13
  import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
15
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
+
16
32
  // src/lib/config/index.ts
17
33
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
18
34
  import { homedir } from "os";
@@ -176,9 +192,7 @@ async function isGhAuthenticated() {
176
192
  function getRepoFromRemote(remoteUrl) {
177
193
  const sshMatch = remoteUrl.match(/git@github\.com:(.+?)(?:\.git)?$/);
178
194
  if (sshMatch) return sshMatch[1].replace(/\.git$/, "");
179
- const httpsMatch = remoteUrl.match(
180
- /https:\/\/github\.com\/(.+?)(?:\.git)?$/
181
- );
195
+ const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/(.+?)(?:\.git)?$/);
182
196
  if (httpsMatch) return httpsMatch[1].replace(/\.git$/, "");
183
197
  return null;
184
198
  }
@@ -199,9 +213,7 @@ async function listPRsForBranch(branch, repo) {
199
213
  }
200
214
  const fields = "number,title,state,author,createdAt,isDraft";
201
215
  try {
202
- const { stdout } = await execAsync(
203
- `gh pr view --json ${fields} 2>/dev/null`
204
- );
216
+ const { stdout } = await execAsync(`gh pr view --json ${fields} 2>/dev/null`);
205
217
  const pr = JSON.parse(stdout);
206
218
  return { success: true, data: [pr] };
207
219
  } catch {
@@ -211,10 +223,8 @@ async function listPRsForBranch(branch, repo) {
211
223
  `gh pr list --state open --json ${fields},headRefName --repo "${repo}" 2>/dev/null`
212
224
  );
213
225
  const allPrs = JSON.parse(stdout);
214
- const prs = allPrs.filter(
215
- (pr) => pr.headRefName === branch || pr.headRefName.endsWith(`:${branch}`)
216
- );
217
- 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);
218
228
  return { success: true, data: result };
219
229
  } catch {
220
230
  return { success: false, error: "Failed to fetch PRs", errorType: "api_error" };
@@ -254,9 +264,7 @@ async function getPRDetails(prNumber, repo) {
254
264
  "reviews",
255
265
  "statusCheckRollup"
256
266
  ].join(",");
257
- const { stdout } = await execAsync(
258
- `gh pr view ${prNumber} --json ${fields} --repo "${repo}"`
259
- );
267
+ const { stdout } = await execAsync(`gh pr view ${prNumber} --json ${fields} --repo "${repo}"`);
260
268
  const pr = JSON.parse(stdout);
261
269
  return { success: true, data: pr };
262
270
  } catch {
@@ -267,6 +275,274 @@ async function getPRDetails(prNumber, repo) {
267
275
  };
268
276
  }
269
277
  }
278
+ function openPRCreationPage(owner, branch, onComplete) {
279
+ const headFlag = `${owner}:${branch}`;
280
+ exec(`gh pr create --web --head "${headFlag}"`, (error) => {
281
+ process.stdout.emit("resize");
282
+ onComplete == null ? void 0 : onComplete(error);
283
+ });
284
+ }
285
+
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 };
310
+ }
311
+
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;
336
+ }
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
393
+ };
394
+ }
395
+
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
+ }
270
546
 
271
547
  // src/lib/jira/parser.ts
272
548
  var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
@@ -285,8 +561,8 @@ function parseTicketKey(input) {
285
561
  }
286
562
  return null;
287
563
  }
288
- function extractTicketKeyFromBranch(branchName) {
289
- const match = branchName.match(/([A-Za-z][A-Za-z0-9]+-\d+)/);
564
+ function extractTicketKey(text) {
565
+ const match = text.match(/([A-Za-z][A-Za-z0-9]+-\d+)/);
290
566
  if (match) {
291
567
  const candidate = match[1].toUpperCase();
292
568
  if (isValidTicketKeyFormat(candidate)) {
@@ -492,10 +768,10 @@ async function applyTransition(auth, ticketKey, transitionId) {
492
768
  }
493
769
 
494
770
  // src/lib/logs/index.ts
495
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, appendFileSync, writeFileSync as writeFileSync2 } from "fs";
771
+ import { spawnSync } from "child_process";
772
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, writeFileSync as writeFileSync2 } from "fs";
496
773
  import { homedir as homedir2 } from "os";
497
774
  import { join as join2 } from "path";
498
- import { spawnSync } from "child_process";
499
775
  var LOGS_DIRECTORY = join2(homedir2(), ".clairo", "logs");
500
776
  function ensureLogsDirectory() {
501
777
  if (!existsSync2(LOGS_DIRECTORY)) {
@@ -622,15 +898,15 @@ ${oldStatus} \u2192 ${newStatus}
622
898
 
623
899
  // src/components/github/PRDetailsBox.tsx
624
900
  import open from "open";
625
- import { useRef } from "react";
901
+ import { useRef as useRef2 } from "react";
626
902
  import { Box as Box2, Text as Text2, useInput, useStdout } from "ink";
627
903
  import { ScrollView } from "ink-scroll-view";
628
904
 
629
905
  // src/components/ui/Markdown.tsx
906
+ import Table from "cli-table3";
907
+ import { marked } from "marked";
630
908
  import { Box, Text } from "ink";
631
909
  import Link from "ink-link";
632
- import { marked } from "marked";
633
- import Table from "cli-table3";
634
910
  import { jsx, jsxs } from "react/jsx-runtime";
635
911
  function Markdown({ children }) {
636
912
  const tokens = marked.lexer(children);
@@ -642,10 +918,12 @@ function TokenRenderer({ token }) {
642
918
  case "heading":
643
919
  return /* @__PURE__ */ jsx(Box, { marginTop: token.depth === 1 ? 0 : 1, children: /* @__PURE__ */ jsx(Text, { bold: true, underline: token.depth === 1, children: renderInline(token.tokens) }) });
644
920
  case "paragraph": {
645
- const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some((t) => {
646
- var _a2;
647
- return t.type === "link" || t.type === "strong" && "tokens" in t && ((_a2 = t.tokens) == null ? void 0 : _a2.some((st) => st.type === "link"));
648
- });
921
+ const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some(
922
+ (t) => {
923
+ var _a2;
924
+ return t.type === "link" || t.type === "strong" && "tokens" in t && ((_a2 = t.tokens) == null ? void 0 : _a2.some((st) => st.type === "link"));
925
+ }
926
+ );
649
927
  if (hasLinks) {
650
928
  return /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: renderInline(token.tokens) });
651
929
  }
@@ -768,7 +1046,7 @@ function getCheckSortOrder(check) {
768
1046
  }
769
1047
  function PRDetailsBox({ pr, loading, error, isFocused }) {
770
1048
  var _a, _b, _c, _d, _e, _f, _g;
771
- const scrollRef = useRef(null);
1049
+ const scrollRef = useRef2(null);
772
1050
  const title = "[3] PR Details";
773
1051
  const borderColor = isFocused ? "yellow" : void 0;
774
1052
  const displayTitle = pr ? `${title} - #${pr.number}` : title;
@@ -892,118 +1170,517 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
892
1170
  }
893
1171
 
894
1172
  // src/components/github/PullRequestsBox.tsx
895
- import { useEffect, useState } from "react";
1173
+ import open2 from "open";
1174
+ import { useEffect as useEffect7, useState as useState10 } from "react";
896
1175
  import { TitledBox } from "@mishieck/ink-titled-box";
897
1176
  import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
1177
+ import { ScrollView as ScrollView2 } from "ink-scroll-view";
898
1178
 
899
- // src/lib/clipboard.ts
900
- import { exec as exec2 } from "child_process";
901
- async function copyToClipboard(text) {
902
- var _a, _b;
903
- const command = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
904
- try {
905
- const child = exec2(command);
906
- (_a = child.stdin) == null ? void 0 : _a.write(text);
907
- (_b = child.stdin) == null ? void 0 : _b.end();
908
- await new Promise((resolve, reject) => {
909
- child.on("close", (code) => {
910
- if (code === 0) resolve();
911
- else reject(new Error(`Clipboard command exited with code ${code}`));
912
- });
913
- });
914
- return true;
915
- } catch {
916
- return false;
917
- }
918
- }
919
-
920
- // src/components/github/PullRequestsBox.tsx
921
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
922
- function PullRequestsBox({
923
- prs,
924
- selectedPR,
925
- onSelect,
926
- onCreatePR,
927
- loading,
928
- error,
929
- branch,
930
- repoSlug,
931
- isFocused
932
- }) {
933
- const [highlightedIndex, setHighlightedIndex] = useState(0);
934
- const totalItems = prs.length + 1;
935
- useEffect(() => {
936
- const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
937
- if (idx >= 0) setHighlightedIndex(idx);
938
- }, [selectedPR, prs]);
939
- useInput2(
940
- (input, key) => {
941
- if (!isFocused) return;
942
- if (key.upArrow || input === "k") {
943
- setHighlightedIndex((prev) => Math.max(0, prev - 1));
944
- }
945
- if (key.downArrow || input === "j") {
946
- setHighlightedIndex((prev) => Math.min(totalItems - 1, prev + 1));
947
- }
948
- if (input === " ") {
949
- if (highlightedIndex === prs.length) {
950
- onCreatePR();
951
- } else if (prs[highlightedIndex]) {
952
- onSelect(prs[highlightedIndex]);
953
- }
1179
+ // src/hooks/jira/useJiraTickets.ts
1180
+ import { useCallback as useCallback4, useState as useState5 } from "react";
1181
+ function useJiraTickets() {
1182
+ const [jiraState, setJiraState] = useState5("not_configured");
1183
+ const [tickets, setTickets] = useState5([]);
1184
+ const [loading, setLoading] = useState5({ configure: false, link: false });
1185
+ const [errors, setErrors] = useState5({});
1186
+ const initializeJiraState = useCallback4(async (repoPath, currentBranch, repoSlug) => {
1187
+ if (!isJiraConfigured(repoPath)) {
1188
+ setJiraState("not_configured");
1189
+ setTickets([]);
1190
+ return;
1191
+ }
1192
+ const linkedTickets = getLinkedTickets(repoPath, currentBranch);
1193
+ if (linkedTickets.length > 0) {
1194
+ setTickets(linkedTickets);
1195
+ setJiraState("has_tickets");
1196
+ return;
1197
+ }
1198
+ let ticketKey = extractTicketKey(currentBranch);
1199
+ if (!ticketKey && repoSlug) {
1200
+ const prResult = await listPRsForBranch(currentBranch, repoSlug);
1201
+ if (prResult.success && prResult.data.length > 0) {
1202
+ ticketKey = extractTicketKey(prResult.data[0].title);
954
1203
  }
955
- if (input === "y" && repoSlug && prs[highlightedIndex]) {
956
- const pr = prs[highlightedIndex];
957
- const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
958
- copyToClipboard(url);
1204
+ }
1205
+ if (!ticketKey) {
1206
+ setTickets([]);
1207
+ setJiraState("no_tickets");
1208
+ return;
1209
+ }
1210
+ const siteUrl = getJiraSiteUrl(repoPath);
1211
+ const creds = getJiraCredentials(repoPath);
1212
+ if (!siteUrl || !creds.email || !creds.apiToken) {
1213
+ setTickets([]);
1214
+ setJiraState("no_tickets");
1215
+ return;
1216
+ }
1217
+ const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
1218
+ const result = await getIssue(auth, ticketKey);
1219
+ if (result.success) {
1220
+ const linkedTicket = {
1221
+ key: result.data.key,
1222
+ summary: result.data.fields.summary,
1223
+ status: result.data.fields.status.name,
1224
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1225
+ };
1226
+ addLinkedTicket(repoPath, currentBranch, linkedTicket);
1227
+ setTickets([linkedTicket]);
1228
+ setJiraState("has_tickets");
1229
+ } else {
1230
+ setTickets([]);
1231
+ setJiraState("no_tickets");
1232
+ }
1233
+ }, []);
1234
+ const refreshTickets = useCallback4((repoPath, currentBranch) => {
1235
+ const linkedTickets = getLinkedTickets(repoPath, currentBranch);
1236
+ setTickets(linkedTickets);
1237
+ setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
1238
+ }, []);
1239
+ const configureJira = useCallback4(
1240
+ async (repoPath, siteUrl, email, apiToken) => {
1241
+ setLoading((prev) => ({ ...prev, configure: true }));
1242
+ setErrors((prev) => ({ ...prev, configure: void 0 }));
1243
+ const auth = { siteUrl, email, apiToken };
1244
+ const result = await validateCredentials(auth);
1245
+ if (!result.success) {
1246
+ setErrors((prev) => ({ ...prev, configure: result.error }));
1247
+ duckEvents.emit("error");
1248
+ setLoading((prev) => ({ ...prev, configure: false }));
1249
+ return false;
959
1250
  }
1251
+ setJiraSiteUrl(repoPath, siteUrl);
1252
+ setJiraCredentials(repoPath, email, apiToken);
1253
+ setJiraState("no_tickets");
1254
+ duckEvents.emit("jira:configured");
1255
+ setLoading((prev) => ({ ...prev, configure: false }));
1256
+ return true;
960
1257
  },
961
- { isActive: isFocused }
1258
+ []
1259
+ );
1260
+ const linkTicket = useCallback4(
1261
+ async (repoPath, currentBranch, ticketInput) => {
1262
+ setLoading((prev) => ({ ...prev, link: true }));
1263
+ setErrors((prev) => ({ ...prev, link: void 0 }));
1264
+ const ticketKey = parseTicketKey(ticketInput);
1265
+ if (!ticketKey) {
1266
+ setErrors((prev) => ({ ...prev, link: "Invalid ticket format. Use PROJ-123 or a Jira URL." }));
1267
+ duckEvents.emit("error");
1268
+ setLoading((prev) => ({ ...prev, link: false }));
1269
+ return false;
1270
+ }
1271
+ const siteUrl = getJiraSiteUrl(repoPath);
1272
+ const creds = getJiraCredentials(repoPath);
1273
+ if (!siteUrl || !creds.email || !creds.apiToken) {
1274
+ setErrors((prev) => ({ ...prev, link: "Jira not configured" }));
1275
+ duckEvents.emit("error");
1276
+ setLoading((prev) => ({ ...prev, link: false }));
1277
+ return false;
1278
+ }
1279
+ const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
1280
+ const result = await getIssue(auth, ticketKey);
1281
+ if (!result.success) {
1282
+ setErrors((prev) => ({ ...prev, link: result.error }));
1283
+ duckEvents.emit("error");
1284
+ setLoading((prev) => ({ ...prev, link: false }));
1285
+ return false;
1286
+ }
1287
+ const linkedTicket = {
1288
+ key: result.data.key,
1289
+ summary: result.data.fields.summary,
1290
+ status: result.data.fields.status.name,
1291
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1292
+ };
1293
+ addLinkedTicket(repoPath, currentBranch, linkedTicket);
1294
+ const newTickets = getLinkedTickets(repoPath, currentBranch);
1295
+ setTickets(newTickets);
1296
+ setJiraState("has_tickets");
1297
+ duckEvents.emit("jira:linked");
1298
+ setLoading((prev) => ({ ...prev, link: false }));
1299
+ return true;
1300
+ },
1301
+ []
1302
+ );
1303
+ const unlinkTicket = useCallback4((repoPath, currentBranch, ticketKey) => {
1304
+ removeLinkedTicket(repoPath, currentBranch, ticketKey);
1305
+ }, []);
1306
+ const clearError = useCallback4((key) => {
1307
+ setErrors((prev) => ({ ...prev, [key]: void 0 }));
1308
+ }, []);
1309
+ return {
1310
+ jiraState,
1311
+ tickets,
1312
+ loading,
1313
+ errors,
1314
+ initializeJiraState,
1315
+ refreshTickets,
1316
+ configureJira,
1317
+ linkTicket,
1318
+ unlinkTicket,
1319
+ clearError
1320
+ };
1321
+ }
1322
+
1323
+ // src/hooks/logs/useLogs.ts
1324
+ import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef3, useState as useState6 } from "react";
1325
+ function useLogs() {
1326
+ const [logFiles, setLogFiles] = useState6([]);
1327
+ const [selectedDate, setSelectedDate] = useState6(null);
1328
+ const [logContent, setLogContent] = useState6(null);
1329
+ const [highlightedIndex, setHighlightedIndex] = useState6(0);
1330
+ const initializedRef = useRef3(false);
1331
+ const loadLogContent = useCallback5((date) => {
1332
+ if (!date) {
1333
+ setLogContent(null);
1334
+ return null;
1335
+ }
1336
+ const content = readLog(date);
1337
+ setLogContent(content);
1338
+ return content;
1339
+ }, []);
1340
+ const refreshLogFiles = useCallback5(() => {
1341
+ const files = listLogFiles();
1342
+ setLogFiles(files);
1343
+ return files;
1344
+ }, []);
1345
+ const initialize = useCallback5(() => {
1346
+ const files = listLogFiles();
1347
+ setLogFiles(files);
1348
+ if (files.length === 0) return;
1349
+ const today = getTodayDate();
1350
+ const todayFile = files.find((f) => f.date === today);
1351
+ if (todayFile) {
1352
+ setSelectedDate(todayFile.date);
1353
+ const idx = files.findIndex((f) => f.date === today);
1354
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1355
+ loadLogContent(todayFile.date);
1356
+ } else {
1357
+ setSelectedDate(files[0].date);
1358
+ setHighlightedIndex(0);
1359
+ loadLogContent(files[0].date);
1360
+ }
1361
+ }, [loadLogContent]);
1362
+ useEffect4(() => {
1363
+ if (initializedRef.current) return;
1364
+ initializedRef.current = true;
1365
+ initialize();
1366
+ }, [initialize]);
1367
+ const selectDate = useCallback5(
1368
+ (date) => {
1369
+ setSelectedDate(date);
1370
+ loadLogContent(date);
1371
+ },
1372
+ [loadLogContent]
1373
+ );
1374
+ const refresh = useCallback5(() => {
1375
+ refreshLogFiles();
1376
+ if (selectedDate) {
1377
+ loadLogContent(selectedDate);
1378
+ }
1379
+ }, [refreshLogFiles, selectedDate, loadLogContent]);
1380
+ const handleExternalLogUpdate = useCallback5(() => {
1381
+ const files = listLogFiles();
1382
+ setLogFiles(files);
1383
+ const today = getTodayDate();
1384
+ if (selectedDate === today) {
1385
+ loadLogContent(today);
1386
+ } else if (!selectedDate && files.length > 0) {
1387
+ const todayFile = files.find((f) => f.date === today);
1388
+ if (todayFile) {
1389
+ setSelectedDate(today);
1390
+ const idx = files.findIndex((f) => f.date === today);
1391
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1392
+ loadLogContent(today);
1393
+ }
1394
+ }
1395
+ }, [selectedDate, loadLogContent]);
1396
+ const handleLogCreated = useCallback5(() => {
1397
+ const files = listLogFiles();
1398
+ setLogFiles(files);
1399
+ const today = getTodayDate();
1400
+ setSelectedDate(today);
1401
+ const idx = files.findIndex((f) => f.date === today);
1402
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1403
+ loadLogContent(today);
1404
+ }, [loadLogContent]);
1405
+ return {
1406
+ logFiles,
1407
+ selectedDate,
1408
+ logContent,
1409
+ highlightedIndex,
1410
+ setHighlightedIndex,
1411
+ selectDate,
1412
+ refresh,
1413
+ handleExternalLogUpdate,
1414
+ handleLogCreated
1415
+ };
1416
+ }
1417
+
1418
+ // src/hooks/useModal.ts
1419
+ import { useCallback as useCallback6, useState as useState7 } from "react";
1420
+ function useModal() {
1421
+ const [modalType, setModalType] = useState7("none");
1422
+ const open4 = useCallback6((type) => setModalType(type), []);
1423
+ const close = useCallback6(() => setModalType("none"), []);
1424
+ const isOpen = modalType !== "none";
1425
+ return {
1426
+ type: modalType,
1427
+ isOpen,
1428
+ open: open4,
1429
+ close
1430
+ };
1431
+ }
1432
+
1433
+ // src/hooks/useListNavigation.ts
1434
+ import { useCallback as useCallback7, useState as useState8 } from "react";
1435
+ function useListNavigation(length) {
1436
+ const [index, setIndex] = useState8(0);
1437
+ const prev = useCallback7(() => {
1438
+ setIndex((i) => Math.max(0, i - 1));
1439
+ }, []);
1440
+ const next = useCallback7(() => {
1441
+ setIndex((i) => Math.min(length - 1, i + 1));
1442
+ }, [length]);
1443
+ const clampedIndex = Math.min(index, Math.max(0, length - 1));
1444
+ const reset = useCallback7(() => setIndex(0), []);
1445
+ return {
1446
+ index: length === 0 ? 0 : clampedIndex,
1447
+ prev,
1448
+ next,
1449
+ reset,
1450
+ setIndex
1451
+ };
1452
+ }
1453
+
1454
+ // src/hooks/useScrollToIndex.ts
1455
+ import { useEffect as useEffect5, useRef as useRef4 } from "react";
1456
+ function useScrollToIndex(index) {
1457
+ const scrollRef = useRef4(null);
1458
+ useEffect5(() => {
1459
+ const ref = scrollRef.current;
1460
+ if (!ref) return;
1461
+ const pos = ref.getItemPosition(index);
1462
+ const viewportHeight = ref.getViewportHeight();
1463
+ const scrollOffset = ref.getScrollOffset();
1464
+ if (!pos) return;
1465
+ if (pos.top < scrollOffset) {
1466
+ ref.scrollTo(pos.top);
1467
+ } else if (pos.top + pos.height > scrollOffset + viewportHeight) {
1468
+ ref.scrollTo(pos.top + pos.height - viewportHeight);
1469
+ }
1470
+ }, [index]);
1471
+ return scrollRef;
1472
+ }
1473
+
1474
+ // src/hooks/useRubberDuck.ts
1475
+ import { useCallback as useCallback8, useEffect as useEffect6, useState as useState9 } from "react";
1476
+ var DUCK_MESSAGES = [
1477
+ "Quack.",
1478
+ "Quack quack quack.",
1479
+ "Have you tried explaining it out loud?",
1480
+ "It's always DNS.",
1481
+ "Did you check the logs?",
1482
+ "Maybe add a console.log?",
1483
+ "Is it plugged in?",
1484
+ "Works on my machine.",
1485
+ "Have you tried reading the error message?",
1486
+ "I believe in you!",
1487
+ "It's probably a race condition.",
1488
+ "Have you tried turning it off and on again?",
1489
+ "Are you sure it compiled?",
1490
+ "It's not a bug, it's a feature.",
1491
+ "Did you clear the cache?",
1492
+ "Try deleting node_modules.",
1493
+ "That's quackers!",
1494
+ "Rubber duck debugging, activate!",
1495
+ "*supportive quacking*"
1496
+ ];
1497
+ var REACTION_MESSAGES = {
1498
+ "pr:merged": ["Quack! It shipped!", "Merged!", "To production we go!"],
1499
+ "pr:opened": ["A new PR! Exciting!", "Time for review!", "Fresh code incoming!"],
1500
+ "pr:reviewed": ["Feedback time!", "Reviews are in!", "*attentive quacking*"],
1501
+ "pr:approved": ["Approved!", "LGTM!", "Ship it!"],
1502
+ "pr:changes-requested": ["Some changes needed...", "Back to the drawing board!", "Iterate iterate!"],
1503
+ error: ["Uh oh...", "There there...", "*concerned quacking*", "Quack... not good."],
1504
+ "jira:transition": ["Ticket moving!", "Progress!", "Workflow in motion!"],
1505
+ "jira:linked": ["Ticket linked!", "Jira connection made!", "Tracking enabled!"],
1506
+ "jira:configured": ["Jira ready!", "Integration complete!", "Connected to Jira!"]
1507
+ };
1508
+ function useRubberDuck() {
1509
+ const [state, setState] = useState9({
1510
+ visible: false,
1511
+ message: DUCK_MESSAGES[0]
1512
+ });
1513
+ const getRandomMessage = useCallback8(() => {
1514
+ const index = Math.floor(Math.random() * DUCK_MESSAGES.length);
1515
+ return DUCK_MESSAGES[index];
1516
+ }, []);
1517
+ const toggleDuck = useCallback8(() => {
1518
+ setState((prev) => ({
1519
+ ...prev,
1520
+ visible: !prev.visible,
1521
+ message: !prev.visible ? getRandomMessage() : prev.message
1522
+ }));
1523
+ }, [getRandomMessage]);
1524
+ const quack = useCallback8(() => {
1525
+ if (state.visible) {
1526
+ setState((prev) => ({
1527
+ ...prev,
1528
+ message: getRandomMessage()
1529
+ }));
1530
+ }
1531
+ }, [state.visible, getRandomMessage]);
1532
+ const getReactionMessage = useCallback8((event) => {
1533
+ const messages = REACTION_MESSAGES[event];
1534
+ return messages[Math.floor(Math.random() * messages.length)];
1535
+ }, []);
1536
+ useEffect6(() => {
1537
+ const unsubscribe = duckEvents.subscribe((event) => {
1538
+ setState((prev) => ({
1539
+ ...prev,
1540
+ visible: true,
1541
+ message: getReactionMessage(event)
1542
+ }));
1543
+ });
1544
+ return unsubscribe;
1545
+ }, [getReactionMessage]);
1546
+ return {
1547
+ visible: state.visible,
1548
+ message: state.message,
1549
+ toggleDuck,
1550
+ quack
1551
+ };
1552
+ }
1553
+
1554
+ // src/lib/clipboard.ts
1555
+ import { exec as exec2 } from "child_process";
1556
+ async function copyToClipboard(text) {
1557
+ var _a, _b;
1558
+ const command = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
1559
+ try {
1560
+ const child = exec2(command);
1561
+ (_a = child.stdin) == null ? void 0 : _a.write(text);
1562
+ (_b = child.stdin) == null ? void 0 : _b.end();
1563
+ await new Promise((resolve, reject) => {
1564
+ child.on("close", (code) => {
1565
+ if (code === 0) resolve();
1566
+ else reject(new Error(`Clipboard command exited with code ${code}`));
1567
+ });
1568
+ });
1569
+ return true;
1570
+ } catch {
1571
+ return false;
1572
+ }
1573
+ }
1574
+
1575
+ // src/components/github/PullRequestsBox.tsx
1576
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1577
+ function PullRequestsBox({
1578
+ prs,
1579
+ selectedPR,
1580
+ onSelect,
1581
+ onCreatePR,
1582
+ loading,
1583
+ error,
1584
+ branch,
1585
+ repoSlug,
1586
+ isFocused
1587
+ }) {
1588
+ const [highlightedIndex, setHighlightedIndex] = useState10(0);
1589
+ const [copied, setCopied] = useState10(false);
1590
+ const scrollRef = useScrollToIndex(highlightedIndex);
1591
+ const totalItems = prs.length + 1;
1592
+ useEffect7(() => {
1593
+ const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
1594
+ if (idx >= 0) setHighlightedIndex(idx);
1595
+ }, [selectedPR, prs]);
1596
+ useInput2(
1597
+ (input, key) => {
1598
+ if (!isFocused) return;
1599
+ if (key.upArrow || input === "k") {
1600
+ setHighlightedIndex((prev) => Math.max(0, prev - 1));
1601
+ }
1602
+ if (key.downArrow || input === "j") {
1603
+ setHighlightedIndex((prev) => Math.min(totalItems - 1, prev + 1));
1604
+ }
1605
+ if (input === " ") {
1606
+ if (highlightedIndex === prs.length) {
1607
+ onCreatePR();
1608
+ } else if (prs[highlightedIndex]) {
1609
+ onSelect(prs[highlightedIndex]);
1610
+ }
1611
+ }
1612
+ if (input === "y" && repoSlug && prs[highlightedIndex]) {
1613
+ const pr = prs[highlightedIndex];
1614
+ const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
1615
+ copyToClipboard(url);
1616
+ setCopied(true);
1617
+ setTimeout(() => setCopied(false), 1500);
1618
+ }
1619
+ if (input === "o" && repoSlug && prs[highlightedIndex]) {
1620
+ const pr = prs[highlightedIndex];
1621
+ const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
1622
+ open2(url).catch(() => {
1623
+ });
1624
+ }
1625
+ },
1626
+ { isActive: isFocused }
1627
+ );
1628
+ const title = "[2] Pull Requests";
1629
+ const subtitle = branch ? ` (${branch})` : "";
1630
+ const copiedIndicator = copied ? " [Copied!]" : "";
1631
+ const borderColor = isFocused ? "yellow" : void 0;
1632
+ return /* @__PURE__ */ jsx3(
1633
+ TitledBox,
1634
+ {
1635
+ borderStyle: "round",
1636
+ titles: [`${title}${subtitle}${copiedIndicator}`],
1637
+ borderColor,
1638
+ height: 5,
1639
+ children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
1640
+ loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
1641
+ error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
1642
+ !loading && !error && /* @__PURE__ */ jsxs3(ScrollView2, { ref: scrollRef, children: [
1643
+ prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }, "empty"),
1644
+ prs.map((pr, idx) => {
1645
+ const isHighlighted = isFocused && idx === highlightedIndex;
1646
+ const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
1647
+ const cursor = isHighlighted ? ">" : " ";
1648
+ const indicator = isSelected ? " *" : "";
1649
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
1650
+ /* @__PURE__ */ jsxs3(Text3, { color: isHighlighted ? "yellow" : void 0, children: [
1651
+ cursor,
1652
+ " "
1653
+ ] }),
1654
+ /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
1655
+ "#",
1656
+ pr.number,
1657
+ " ",
1658
+ pr.isDraft ? "[Draft] " : "",
1659
+ pr.title
1660
+ ] }),
1661
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: indicator })
1662
+ ] }, pr.number);
1663
+ }),
1664
+ /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
1665
+ isFocused && highlightedIndex === prs.length ? "> " : " ",
1666
+ "+ Create new PR"
1667
+ ] }, "create")
1668
+ ] })
1669
+ ] })
1670
+ }
962
1671
  );
963
- const title = "[2] Pull Requests";
964
- const subtitle = branch ? ` (${branch})` : "";
965
- const borderColor = isFocused ? "yellow" : void 0;
966
- return /* @__PURE__ */ jsx3(TitledBox, { borderStyle: "round", titles: [`${title}${subtitle}`], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
967
- loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
968
- error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
969
- !loading && !error && /* @__PURE__ */ jsxs3(Fragment2, { children: [
970
- prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }),
971
- prs.map((pr, idx) => {
972
- const isHighlighted = isFocused && idx === highlightedIndex;
973
- const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
974
- const cursor = isHighlighted ? ">" : " ";
975
- const indicator = isSelected ? " *" : "";
976
- return /* @__PURE__ */ jsxs3(Box3, { children: [
977
- /* @__PURE__ */ jsxs3(Text3, { color: isHighlighted ? "yellow" : void 0, children: [
978
- cursor,
979
- " "
980
- ] }),
981
- /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
982
- "#",
983
- pr.number,
984
- " ",
985
- pr.isDraft ? "[Draft] " : "",
986
- pr.title
987
- ] }),
988
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: indicator })
989
- ] }, pr.number);
990
- })
991
- ] }),
992
- /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
993
- isFocused && highlightedIndex === prs.length ? "> " : " ",
994
- "+ Create new PR"
995
- ] })
996
- ] }) });
997
1672
  }
998
1673
 
999
1674
  // src/components/github/RemotesBox.tsx
1000
- import { useEffect as useEffect2, useState as useState2 } from "react";
1675
+ import { useEffect as useEffect8, useState as useState11 } from "react";
1001
1676
  import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
1002
1677
  import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
1678
+ import { ScrollView as ScrollView3 } from "ink-scroll-view";
1003
1679
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1004
1680
  function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
1005
- const [highlightedIndex, setHighlightedIndex] = useState2(0);
1006
- useEffect2(() => {
1681
+ const [highlightedIndex, setHighlightedIndex] = useState11(0);
1682
+ const scrollRef = useScrollToIndex(highlightedIndex);
1683
+ useEffect8(() => {
1007
1684
  const idx = remotes.findIndex((r) => r.name === selectedRemote);
1008
1685
  if (idx >= 0) setHighlightedIndex(idx);
1009
1686
  }, [selectedRemote, remotes]);
@@ -1024,11 +1701,11 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
1024
1701
  );
1025
1702
  const title = "[1] Remotes";
1026
1703
  const borderColor = isFocused ? "yellow" : void 0;
1027
- return /* @__PURE__ */ jsx4(TitledBox2, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
1704
+ return /* @__PURE__ */ jsx4(TitledBox2, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
1028
1705
  loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
1029
1706
  error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
1030
1707
  !loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
1031
- !loading && !error && remotes.map((remote, idx) => {
1708
+ !loading && !error && remotes.length > 0 && /* @__PURE__ */ jsx4(ScrollView3, { ref: scrollRef, children: remotes.map((remote, idx) => {
1032
1709
  const isHighlighted = isFocused && idx === highlightedIndex;
1033
1710
  const isSelected = remote.name === selectedRemote;
1034
1711
  const cursor = isHighlighted ? ">" : " ";
@@ -1046,215 +1723,108 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
1046
1723
  ] }),
1047
1724
  /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: indicator })
1048
1725
  ] }, remote.name);
1049
- })
1726
+ }) })
1050
1727
  ] }) });
1051
1728
  }
1052
1729
 
1053
1730
  // src/components/github/GitHubView.tsx
1054
1731
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1055
- function GitHubView({ isFocused, onKeybindingsChange, onLogUpdated }) {
1056
- const [isRepo, setIsRepo] = useState3(null);
1057
- const [repoPath, setRepoPath] = useState3(null);
1058
- const [remotes, setRemotes] = useState3([]);
1059
- const [currentBranch, setCurrentBranch] = useState3(null);
1060
- const [currentRepoSlug, setCurrentRepoSlug] = useState3(null);
1061
- const [selectedRemote, setSelectedRemote] = useState3(null);
1062
- const [selectedPR, setSelectedPR] = useState3(null);
1063
- const [prs, setPrs] = useState3([]);
1064
- const [prDetails, setPrDetails] = useState3(null);
1065
- const [loading, setLoading] = useState3({
1066
- remotes: true,
1067
- prs: false,
1068
- details: false
1069
- });
1070
- const [errors, setErrors] = useState3({});
1071
- const [focusedBox, setFocusedBox] = useState3("remotes");
1072
- useEffect3(() => {
1073
- if (!isFocused) {
1074
- onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
1075
- return;
1076
- }
1077
- const bindings = [];
1078
- if (focusedBox === "remotes") {
1079
- bindings.push({ key: "Space", label: "Select Remote" });
1080
- } else if (focusedBox === "prs") {
1081
- bindings.push({ key: "Space", label: "Select" });
1082
- bindings.push({ key: "n", label: "New PR", color: "green" });
1083
- bindings.push({ key: "r", label: "Refresh" });
1084
- bindings.push({ key: "o", label: "Open", color: "green" });
1085
- bindings.push({ key: "y", label: "Copy Link" });
1086
- } else if (focusedBox === "details") {
1087
- bindings.push({ key: "r", label: "Refresh" });
1088
- bindings.push({ key: "o", label: "Open", color: "green" });
1089
- }
1090
- onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
1091
- }, [isFocused, focusedBox, onKeybindingsChange]);
1092
- useEffect3(() => {
1093
- const gitRepoCheck = isGitRepo();
1094
- setIsRepo(gitRepoCheck);
1095
- if (!gitRepoCheck) {
1096
- setLoading((prev) => ({ ...prev, remotes: false }));
1097
- setErrors((prev) => ({ ...prev, remotes: "Not a git repository" }));
1098
- return;
1099
- }
1100
- const rootResult = getRepoRoot();
1101
- if (rootResult.success) {
1102
- setRepoPath(rootResult.data);
1103
- }
1104
- const branchResult = getCurrentBranch();
1105
- if (branchResult.success) {
1106
- setCurrentBranch(branchResult.data);
1107
- }
1108
- const remotesResult = listRemotes();
1109
- if (remotesResult.success) {
1110
- setRemotes(remotesResult.data);
1111
- const remoteNames = remotesResult.data.map((r) => r.name);
1112
- const defaultRemote = getSelectedRemote(rootResult.success ? rootResult.data : "", remoteNames);
1113
- setSelectedRemote(defaultRemote);
1114
- } else {
1115
- setErrors((prev) => ({ ...prev, remotes: remotesResult.error }));
1116
- }
1117
- setLoading((prev) => ({ ...prev, remotes: false }));
1118
- }, []);
1119
- const refreshPRs = useCallback(async () => {
1120
- if (!currentBranch || !currentRepoSlug) return;
1121
- setLoading((prev) => ({ ...prev, prs: true }));
1122
- try {
1123
- const result = await listPRsForBranch(currentBranch, currentRepoSlug);
1124
- if (result.success) {
1125
- setPrs(result.data);
1126
- if (result.data.length > 0) {
1127
- setSelectedPR((prev) => prev ?? result.data[0]);
1128
- }
1129
- setErrors((prev) => ({ ...prev, prs: void 0 }));
1130
- } else {
1131
- setErrors((prev) => ({ ...prev, prs: result.error }));
1132
- }
1133
- } catch (err) {
1134
- setErrors((prev) => ({ ...prev, prs: String(err) }));
1135
- } finally {
1136
- setLoading((prev) => ({ ...prev, prs: false }));
1137
- }
1138
- }, [currentBranch, currentRepoSlug]);
1139
- const refreshDetails = useCallback(async () => {
1140
- if (!selectedPR || !currentRepoSlug) return;
1141
- setLoading((prev) => ({ ...prev, details: true }));
1142
- try {
1143
- const result = await getPRDetails(selectedPR.number, currentRepoSlug);
1144
- if (result.success) {
1145
- setPrDetails(result.data);
1146
- setErrors((prev) => ({ ...prev, details: void 0 }));
1147
- } else {
1148
- setErrors((prev) => ({ ...prev, details: result.error }));
1149
- }
1150
- } catch (err) {
1151
- setErrors((prev) => ({ ...prev, details: String(err) }));
1152
- } finally {
1153
- setLoading((prev) => ({ ...prev, details: false }));
1154
- }
1155
- }, [selectedPR, currentRepoSlug]);
1156
- useEffect3(() => {
1157
- if (!selectedRemote || !currentBranch) return;
1158
- const remote = remotes.find((r) => r.name === selectedRemote);
1159
- if (!remote) return;
1160
- const repo = getRepoFromRemote(remote.url);
1161
- if (!repo) return;
1162
- setCurrentRepoSlug(repo);
1163
- setPrs([]);
1164
- setSelectedPR(null);
1165
- }, [selectedRemote, currentBranch, remotes]);
1166
- useEffect3(() => {
1167
- if (currentRepoSlug && currentBranch) {
1168
- refreshPRs();
1732
+ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1733
+ const repo = useGitRepo();
1734
+ const pullRequests = usePullRequests();
1735
+ const polling = usePRPolling();
1736
+ const [focusedBox, setFocusedBox] = useState12("remotes");
1737
+ const lastFetchedRef = useRef5(null);
1738
+ useEffect9(() => {
1739
+ if (repo.loading || !repo.currentBranch || !repo.currentRepoSlug) return;
1740
+ const current = { branch: repo.currentBranch, repoSlug: repo.currentRepoSlug };
1741
+ const last = lastFetchedRef.current;
1742
+ if (last && last.branch === current.branch && last.repoSlug === current.repoSlug) return;
1743
+ lastFetchedRef.current = current;
1744
+ pullRequests.fetchPRsAndDetails(repo.currentBranch, repo.currentRepoSlug);
1745
+ }, [repo.loading, repo.currentBranch, repo.currentRepoSlug, pullRequests.fetchPRsAndDetails]);
1746
+ useEffect9(() => {
1747
+ if (isFocused) {
1748
+ repo.refreshBranch();
1169
1749
  }
1170
- }, [currentRepoSlug, currentBranch, refreshPRs]);
1171
- useEffect3(() => {
1172
- if (!selectedPR || !currentRepoSlug) {
1173
- setPrDetails(null);
1174
- return;
1175
- }
1176
- refreshDetails();
1177
- }, [selectedPR, currentRepoSlug, refreshDetails]);
1178
- const handleRemoteSelect = useCallback(
1750
+ }, [isFocused, repo.refreshBranch]);
1751
+ useEffect9(() => {
1752
+ onFocusedBoxChange == null ? void 0 : onFocusedBoxChange(focusedBox);
1753
+ }, [focusedBox, onFocusedBoxChange]);
1754
+ const handleRemoteSelect = useCallback9(
1179
1755
  (remoteName) => {
1180
- setSelectedRemote(remoteName);
1181
- if (repoPath) {
1182
- updateRepoConfig(repoPath, { selectedRemote: remoteName });
1183
- }
1756
+ repo.selectRemote(remoteName);
1757
+ const remote = repo.remotes.find((r) => r.name === remoteName);
1758
+ if (!remote || !repo.currentBranch) return;
1759
+ const repoSlug = getRepoFromRemote(remote.url);
1760
+ if (!repoSlug) return;
1761
+ lastFetchedRef.current = { branch: repo.currentBranch, repoSlug };
1762
+ pullRequests.fetchPRsAndDetails(repo.currentBranch, repoSlug);
1184
1763
  },
1185
- [repoPath]
1764
+ [repo.selectRemote, repo.remotes, repo.currentBranch, pullRequests.fetchPRsAndDetails]
1186
1765
  );
1187
- const handlePRSelect = useCallback((pr) => {
1188
- setSelectedPR(pr);
1189
- }, []);
1190
- const prNumbersBeforeCreate = useRef2(/* @__PURE__ */ new Set());
1191
- const pollingIntervalRef = useRef2(null);
1192
- const handleCreatePR = useCallback(() => {
1193
- if (!currentBranch) {
1194
- setErrors((prev) => ({ ...prev, prs: "No branch detected" }));
1766
+ const handlePRSelect = useCallback9(
1767
+ (pr) => {
1768
+ pullRequests.selectPR(pr, repo.currentRepoSlug);
1769
+ },
1770
+ [pullRequests.selectPR, repo.currentRepoSlug]
1771
+ );
1772
+ const createPRContext = useRef5({ repo, pullRequests, onLogUpdated });
1773
+ createPRContext.current = { repo, pullRequests, onLogUpdated };
1774
+ const handleCreatePR = useCallback9(() => {
1775
+ const { repo: repo2, pullRequests: pullRequests2 } = createPRContext.current;
1776
+ if (!repo2.currentBranch) {
1777
+ pullRequests2.setError("prs", "No branch detected");
1778
+ duckEvents.emit("error");
1195
1779
  return;
1196
1780
  }
1197
- const remoteResult = findRemoteWithBranch(currentBranch);
1781
+ const remoteResult = findRemoteWithBranch(repo2.currentBranch);
1198
1782
  if (!remoteResult.success) {
1199
- setErrors((prev) => ({ ...prev, prs: "Push your branch to a remote first" }));
1783
+ pullRequests2.setError("prs", "Push your branch to a remote first");
1784
+ duckEvents.emit("error");
1200
1785
  return;
1201
1786
  }
1202
- prNumbersBeforeCreate.current = new Set(prs.map((pr) => pr.number));
1203
- const headFlag = `${remoteResult.data.owner}:${currentBranch}`;
1204
- exec3(`gh pr create --web --head "${headFlag}"`, (error) => {
1205
- process.stdout.emit("resize");
1787
+ openPRCreationPage(remoteResult.data.owner, repo2.currentBranch, (error) => {
1206
1788
  if (error) {
1207
- setErrors((prev) => ({ ...prev, prs: `Failed to create PR: ${error.message}` }));
1789
+ pullRequests2.setError("prs", `Failed to create PR: ${error.message}`);
1790
+ duckEvents.emit("error");
1208
1791
  }
1209
1792
  });
1210
- if (!currentRepoSlug) return;
1211
- let attempts = 0;
1212
- const maxAttempts = 24;
1213
- const pollInterval = 5e3;
1214
- if (pollingIntervalRef.current) {
1215
- clearInterval(pollingIntervalRef.current);
1216
- }
1217
- pollingIntervalRef.current = setInterval(async () => {
1218
- attempts++;
1219
- if (attempts > maxAttempts) {
1220
- if (pollingIntervalRef.current) {
1221
- clearInterval(pollingIntervalRef.current);
1222
- pollingIntervalRef.current = null;
1223
- }
1224
- return;
1225
- }
1226
- const result = await listPRsForBranch(currentBranch, currentRepoSlug);
1227
- if (result.success) {
1228
- setPrs(result.data);
1229
- const newPR = result.data.find((pr) => !prNumbersBeforeCreate.current.has(pr.number));
1230
- if (newPR) {
1231
- if (pollingIntervalRef.current) {
1232
- clearInterval(pollingIntervalRef.current);
1233
- pollingIntervalRef.current = null;
1234
- }
1235
- const tickets = repoPath && currentBranch ? getLinkedTickets(repoPath, currentBranch).map((t) => t.key) : [];
1236
- logPRCreated(newPR.number, newPR.title, tickets);
1237
- onLogUpdated == null ? void 0 : onLogUpdated();
1238
- setSelectedPR(newPR);
1793
+ if (!repo2.currentRepoSlug) return;
1794
+ polling.startPolling({
1795
+ branch: repo2.currentBranch,
1796
+ repoSlug: repo2.currentRepoSlug,
1797
+ existingPRNumbers: pullRequests2.prs.map((pr) => pr.number),
1798
+ onPRsUpdated: (prs) => {
1799
+ pullRequests2.setPrs(prs);
1800
+ },
1801
+ onNewPR: (newPR) => {
1802
+ var _a;
1803
+ const ctx = createPRContext.current;
1804
+ const tickets = ctx.repo.repoPath && ctx.repo.currentBranch ? getLinkedTickets(ctx.repo.repoPath, ctx.repo.currentBranch).map((t) => t.key) : [];
1805
+ logPRCreated(newPR.number, newPR.title, tickets);
1806
+ duckEvents.emit("pr:opened");
1807
+ (_a = ctx.onLogUpdated) == null ? void 0 : _a.call(ctx);
1808
+ ctx.pullRequests.setSelectedPR(newPR);
1809
+ if (ctx.repo.currentRepoSlug) {
1810
+ ctx.pullRequests.refreshDetails(newPR, ctx.repo.currentRepoSlug);
1239
1811
  }
1240
1812
  }
1241
- }, pollInterval);
1242
- }, [prs, currentBranch, currentRepoSlug, repoPath, onLogUpdated]);
1243
- useEffect3(() => {
1244
- return () => {
1245
- if (pollingIntervalRef.current) {
1246
- clearInterval(pollingIntervalRef.current);
1247
- }
1248
- };
1249
- }, []);
1813
+ });
1814
+ }, [polling.startPolling]);
1250
1815
  useInput4(
1251
1816
  (input) => {
1252
1817
  if (input === "1") setFocusedBox("remotes");
1253
1818
  if (input === "2") setFocusedBox("prs");
1254
1819
  if (input === "3") setFocusedBox("details");
1255
1820
  if (input === "r") {
1256
- if (focusedBox === "prs") refreshPRs();
1257
- if (focusedBox === "details") refreshDetails();
1821
+ const freshBranch = repo.refreshBranch() ?? repo.currentBranch;
1822
+ if (focusedBox === "prs" && freshBranch && repo.currentRepoSlug) {
1823
+ pullRequests.fetchPRsAndDetails(freshBranch, repo.currentRepoSlug);
1824
+ }
1825
+ if (focusedBox === "details" && pullRequests.selectedPR && repo.currentRepoSlug) {
1826
+ pullRequests.refreshDetails(pullRequests.selectedPR, repo.currentRepoSlug);
1827
+ }
1258
1828
  }
1259
1829
  if (input === "n" && focusedBox === "prs") {
1260
1830
  handleCreatePR();
@@ -1262,41 +1832,41 @@ function GitHubView({ isFocused, onKeybindingsChange, onLogUpdated }) {
1262
1832
  },
1263
1833
  { isActive: isFocused }
1264
1834
  );
1265
- if (isRepo === false) {
1835
+ if (repo.isRepo === false) {
1266
1836
  return /* @__PURE__ */ jsx5(TitledBox3, { borderStyle: "round", titles: ["Error"], flexGrow: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", children: "Current directory is not a git repository" }) });
1267
1837
  }
1268
1838
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, children: [
1269
1839
  /* @__PURE__ */ jsx5(
1270
1840
  RemotesBox,
1271
1841
  {
1272
- remotes,
1273
- selectedRemote,
1842
+ remotes: repo.remotes,
1843
+ selectedRemote: repo.selectedRemote,
1274
1844
  onSelect: handleRemoteSelect,
1275
- loading: loading.remotes,
1276
- error: errors.remotes,
1845
+ loading: repo.loading,
1846
+ error: repo.error,
1277
1847
  isFocused: isFocused && focusedBox === "remotes"
1278
1848
  }
1279
1849
  ),
1280
1850
  /* @__PURE__ */ jsx5(
1281
1851
  PullRequestsBox,
1282
1852
  {
1283
- prs,
1284
- selectedPR,
1853
+ prs: pullRequests.prs,
1854
+ selectedPR: pullRequests.selectedPR,
1285
1855
  onSelect: handlePRSelect,
1286
1856
  onCreatePR: handleCreatePR,
1287
- loading: loading.prs,
1288
- error: errors.prs,
1289
- branch: currentBranch,
1290
- repoSlug: currentRepoSlug,
1857
+ loading: pullRequests.loading.prs,
1858
+ error: pullRequests.errors.prs,
1859
+ branch: repo.currentBranch,
1860
+ repoSlug: repo.currentRepoSlug,
1291
1861
  isFocused: isFocused && focusedBox === "prs"
1292
1862
  }
1293
1863
  ),
1294
1864
  /* @__PURE__ */ jsx5(
1295
1865
  PRDetailsBox,
1296
1866
  {
1297
- pr: prDetails,
1298
- loading: loading.details,
1299
- error: errors.details,
1867
+ pr: pullRequests.prDetails,
1868
+ loading: pullRequests.loading.details,
1869
+ error: pullRequests.errors.details,
1300
1870
  isFocused: isFocused && focusedBox === "details"
1301
1871
  }
1302
1872
  )
@@ -1304,27 +1874,95 @@ function GitHubView({ isFocused, onKeybindingsChange, onLogUpdated }) {
1304
1874
  }
1305
1875
 
1306
1876
  // src/components/jira/JiraView.tsx
1307
- import { useCallback as useCallback2, useEffect as useEffect5, useState as useState7 } from "react";
1308
- import open2 from "open";
1877
+ import open3 from "open";
1878
+ import { useEffect as useEffect11, useRef as useRef6 } from "react";
1879
+
1880
+ // src/components/jira/LinkTicketModal.tsx
1881
+ import { useState as useState13 } from "react";
1882
+ import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
1883
+
1884
+ // src/components/ui/TextInput.tsx
1885
+ import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
1886
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1887
+ function TextInput({ value, onChange, placeholder, isActive, mask }) {
1888
+ useInput5(
1889
+ (input, key) => {
1890
+ if (key.backspace || key.delete) {
1891
+ if (value.length > 0) {
1892
+ onChange(value.slice(0, -1));
1893
+ }
1894
+ return;
1895
+ }
1896
+ if (key.return || key.escape || key.upArrow || key.downArrow || key.tab) {
1897
+ return;
1898
+ }
1899
+ if (input && input.length === 1 && input.charCodeAt(0) >= 32) {
1900
+ onChange(value + input);
1901
+ }
1902
+ },
1903
+ { isActive }
1904
+ );
1905
+ const displayValue = mask ? "*".repeat(value.length) : value;
1906
+ const showPlaceholder = value.length === 0 && placeholder;
1907
+ return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { children: [
1908
+ showPlaceholder ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx6(Text6, { children: displayValue }),
1909
+ isActive && /* @__PURE__ */ jsx6(Text6, { backgroundColor: "yellow", children: " " })
1910
+ ] }) });
1911
+ }
1912
+
1913
+ // src/components/jira/LinkTicketModal.tsx
1914
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1915
+ function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
1916
+ const [ticketInput, setTicketInput] = useState13("");
1917
+ const canSubmit = ticketInput.trim().length > 0;
1918
+ useInput6(
1919
+ (_input, key) => {
1920
+ if (loading) return;
1921
+ if (key.escape) {
1922
+ onCancel();
1923
+ return;
1924
+ }
1925
+ if (key.return && canSubmit) {
1926
+ onSubmit(ticketInput.trim());
1927
+ }
1928
+ },
1929
+ { isActive: !loading }
1930
+ );
1931
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
1932
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "yellow", children: "Link Jira Ticket" }),
1933
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Type ticket ID, Enter to submit, Esc to cancel" }),
1934
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
1935
+ error && /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: error }) }),
1936
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1937
+ /* @__PURE__ */ jsx7(Text7, { color: "blue", children: "Ticket: " }),
1938
+ /* @__PURE__ */ jsx7(TextInput, { value: ticketInput, onChange: setTicketInput, placeholder: "PROJ-123", isActive: !loading })
1939
+ ] }),
1940
+ loading && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "Fetching ticket..." }) }),
1941
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Examples: PROJ-123 or https://company.atlassian.net/browse/PROJ-123" }) })
1942
+ ] });
1943
+ }
1944
+
1945
+ // src/components/jira/JiraView.tsx
1309
1946
  import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
1310
1947
  import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
1311
1948
 
1312
1949
  // src/components/jira/ChangeStatusModal.tsx
1313
- import { useEffect as useEffect4, useState as useState4 } from "react";
1314
- import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
1950
+ import { useEffect as useEffect10, useState as useState14 } from "react";
1951
+ import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
1315
1952
  import SelectInput from "ink-select-input";
1316
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1953
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1317
1954
  function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onCancel }) {
1318
- const [transitions, setTransitions] = useState4([]);
1319
- const [loading, setLoading] = useState4(true);
1320
- const [applying, setApplying] = useState4(false);
1321
- const [error, setError] = useState4(null);
1322
- useEffect4(() => {
1955
+ const [transitions, setTransitions] = useState14([]);
1956
+ const [loading, setLoading] = useState14(true);
1957
+ const [applying, setApplying] = useState14(false);
1958
+ const [error, setError] = useState14(null);
1959
+ useEffect10(() => {
1323
1960
  const fetchTransitions = async () => {
1324
1961
  const siteUrl = getJiraSiteUrl(repoPath);
1325
1962
  const creds = getJiraCredentials(repoPath);
1326
1963
  if (!siteUrl || !creds.email || !creds.apiToken) {
1327
1964
  setError("Jira not configured");
1965
+ duckEvents.emit("error");
1328
1966
  setLoading(false);
1329
1967
  return;
1330
1968
  }
@@ -1334,6 +1972,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1334
1972
  setTransitions(result.data);
1335
1973
  } else {
1336
1974
  setError(result.error);
1975
+ duckEvents.emit("error");
1337
1976
  }
1338
1977
  setLoading(false);
1339
1978
  };
@@ -1346,6 +1985,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1346
1985
  const creds = getJiraCredentials(repoPath);
1347
1986
  if (!siteUrl || !creds.email || !creds.apiToken) {
1348
1987
  setError("Jira not configured");
1988
+ duckEvents.emit("error");
1349
1989
  setApplying(false);
1350
1990
  return;
1351
1991
  }
@@ -1354,13 +1994,15 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1354
1994
  if (result.success) {
1355
1995
  const transition = transitions.find((t) => t.id === item.value);
1356
1996
  const newStatus = (transition == null ? void 0 : transition.to.name) ?? item.label;
1997
+ duckEvents.emit("jira:transition");
1357
1998
  onComplete(newStatus);
1358
1999
  } else {
1359
2000
  setError(result.error);
2001
+ duckEvents.emit("error");
1360
2002
  setApplying(false);
1361
2003
  }
1362
2004
  };
1363
- useInput5(
2005
+ useInput7(
1364
2006
  (_input, key) => {
1365
2007
  if (key.escape && !applying) {
1366
2008
  onCancel();
@@ -1372,24 +2014,27 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1372
2014
  label: t.name,
1373
2015
  value: t.id
1374
2016
  }));
1375
- const initialIndex = Math.max(0, transitions.findIndex((t) => t.to.name === currentStatus));
1376
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
1377
- /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "yellow", children: [
2017
+ const initialIndex = Math.max(
2018
+ 0,
2019
+ transitions.findIndex((t) => t.to.name === currentStatus)
2020
+ );
2021
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
2022
+ /* @__PURE__ */ jsxs8(Text8, { bold: true, color: "yellow", children: [
1378
2023
  "Change Status: ",
1379
2024
  ticketKey
1380
2025
  ] }),
1381
- loading && /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Loading transitions..." }),
1382
- error && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: error }) }),
1383
- !loading && !error && transitions.length === 0 && /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No available transitions" }),
1384
- !loading && !error && transitions.length > 0 && !applying && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx6(SelectInput, { items, initialIndex, onSelect: handleSelect }) }),
1385
- applying && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "Updating status..." }) }),
1386
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Esc to cancel" }) })
2026
+ loading && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Loading transitions..." }),
2027
+ error && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "red", children: error }) }),
2028
+ !loading && !error && transitions.length === 0 && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "No available transitions" }),
2029
+ !loading && !error && transitions.length > 0 && !applying && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx8(SelectInput, { items, initialIndex, onSelect: handleSelect }) }),
2030
+ applying && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Updating status..." }) }),
2031
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Esc to cancel" }) })
1387
2032
  ] });
1388
2033
  }
1389
2034
 
1390
2035
  // src/components/jira/ConfigureJiraSiteModal.tsx
1391
- import { useState as useState5 } from "react";
1392
- import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
2036
+ import { useState as useState15 } from "react";
2037
+ import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
1393
2038
 
1394
2039
  // src/lib/editor.ts
1395
2040
  import { spawnSync as spawnSync2 } from "child_process";
@@ -1420,7 +2065,7 @@ function openInEditor(content, filename) {
1420
2065
  }
1421
2066
 
1422
2067
  // src/components/jira/ConfigureJiraSiteModal.tsx
1423
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2068
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1424
2069
  function ConfigureJiraSiteModal({
1425
2070
  initialSiteUrl,
1426
2071
  initialEmail,
@@ -1429,13 +2074,13 @@ function ConfigureJiraSiteModal({
1429
2074
  loading,
1430
2075
  error
1431
2076
  }) {
1432
- const [siteUrl, setSiteUrl] = useState5(initialSiteUrl ?? "");
1433
- const [email, setEmail] = useState5(initialEmail ?? "");
1434
- const [apiToken, setApiToken] = useState5("");
1435
- const [selectedItem, setSelectedItem] = useState5("siteUrl");
2077
+ const [siteUrl, setSiteUrl] = useState15(initialSiteUrl ?? "");
2078
+ const [email, setEmail] = useState15(initialEmail ?? "");
2079
+ const [apiToken, setApiToken] = useState15("");
2080
+ const [selectedItem, setSelectedItem] = useState15("siteUrl");
1436
2081
  const items = ["siteUrl", "email", "apiToken", "submit"];
1437
2082
  const canSubmit = siteUrl.trim() && email.trim() && apiToken.trim();
1438
- useInput6(
2083
+ useInput8(
1439
2084
  (input, key) => {
1440
2085
  if (loading) return;
1441
2086
  if (key.escape) {
@@ -1484,104 +2129,31 @@ function ConfigureJiraSiteModal({
1484
2129
  const prefix = isSelected ? "> " : " ";
1485
2130
  const color = isSelected ? "yellow" : void 0;
1486
2131
  const displayValue = isSensitive && value ? "*".repeat(Math.min(value.length, 20)) : value;
1487
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1488
- /* @__PURE__ */ jsxs7(Text7, { color, bold: isSelected, children: [
2132
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2133
+ /* @__PURE__ */ jsxs9(Text9, { color, bold: isSelected, children: [
1489
2134
  prefix,
1490
2135
  label
1491
2136
  ] }),
1492
- value !== void 0 && /* @__PURE__ */ jsx7(Box7, { marginLeft: 4, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: displayValue || "(empty - press Enter to edit)" }) })
2137
+ value !== void 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: displayValue || "(empty - press Enter to edit)" }) })
1493
2138
  ] });
1494
2139
  };
1495
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
1496
- /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "Configure Jira Site" }),
1497
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Up/Down to select, Enter to edit, Esc to cancel" }),
1498
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
1499
- error && /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: error }) }),
2140
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
2141
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: "Configure Jira Site" }),
2142
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Up/Down to select, Enter to edit, Esc to cancel" }),
2143
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
2144
+ error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
1500
2145
  renderItem("siteUrl", "Site URL (e.g., https://company.atlassian.net)", siteUrl),
1501
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
2146
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
1502
2147
  renderItem("email", "Email", email),
1503
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
2148
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
1504
2149
  renderItem("apiToken", "API Token", apiToken, true),
1505
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1 }),
1506
- /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { color: selectedItem === "submit" ? "green" : void 0, bold: selectedItem === "submit", children: [
1507
- selectedItem === "submit" ? "> " : " ",
1508
- canSubmit ? "[Save Configuration]" : "[Fill all fields first]"
1509
- ] }) }),
1510
- loading && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "Validating credentials..." }) }),
1511
- /* @__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" }) })
1512
- ] });
1513
- }
1514
-
1515
- // src/components/jira/LinkTicketModal.tsx
1516
- import { useState as useState6 } from "react";
1517
- import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
1518
-
1519
- // src/components/ui/TextInput.tsx
1520
- import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
1521
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1522
- function TextInput({ value, onChange, placeholder, isActive, mask }) {
1523
- useInput7(
1524
- (input, key) => {
1525
- if (key.backspace || key.delete) {
1526
- if (value.length > 0) {
1527
- onChange(value.slice(0, -1));
1528
- }
1529
- return;
1530
- }
1531
- if (key.return || key.escape || key.upArrow || key.downArrow || key.tab) {
1532
- return;
1533
- }
1534
- if (input && input.length === 1 && input.charCodeAt(0) >= 32) {
1535
- onChange(value + input);
1536
- }
1537
- },
1538
- { isActive }
1539
- );
1540
- const displayValue = mask ? "*".repeat(value.length) : value;
1541
- const showPlaceholder = value.length === 0 && placeholder;
1542
- return /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs8(Text8, { children: [
1543
- showPlaceholder ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx8(Text8, { children: displayValue }),
1544
- isActive && /* @__PURE__ */ jsx8(Text8, { backgroundColor: "yellow", children: " " })
1545
- ] }) });
1546
- }
1547
-
1548
- // src/components/jira/LinkTicketModal.tsx
1549
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1550
- function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
1551
- const [ticketInput, setTicketInput] = useState6("");
1552
- const canSubmit = ticketInput.trim().length > 0;
1553
- useInput8(
1554
- (_input, key) => {
1555
- if (loading) return;
1556
- if (key.escape) {
1557
- onCancel();
1558
- return;
1559
- }
1560
- if (key.return && canSubmit) {
1561
- onSubmit(ticketInput.trim());
1562
- }
1563
- },
1564
- { isActive: !loading }
1565
- );
1566
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 1, children: [
1567
- /* @__PURE__ */ jsx9(Text9, { bold: true, color: "yellow", children: "Link Jira Ticket" }),
1568
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Type ticket ID, Enter to submit, Esc to cancel" }),
1569
2150
  /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
1570
- error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
1571
- /* @__PURE__ */ jsxs9(Box9, { children: [
1572
- /* @__PURE__ */ jsx9(Text9, { color: "blue", children: "Ticket: " }),
1573
- /* @__PURE__ */ jsx9(
1574
- TextInput,
1575
- {
1576
- value: ticketInput,
1577
- onChange: setTicketInput,
1578
- placeholder: "PROJ-123",
1579
- isActive: !loading
1580
- }
1581
- )
1582
- ] }),
1583
- loading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Fetching ticket..." }) }),
1584
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Examples: PROJ-123 or https://company.atlassian.net/browse/PROJ-123" }) })
2151
+ /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs9(Text9, { color: selectedItem === "submit" ? "green" : void 0, bold: selectedItem === "submit", children: [
2152
+ selectedItem === "submit" ? "> " : " ",
2153
+ canSubmit ? "[Save Configuration]" : "[Fill all fields first]"
2154
+ ] }) }),
2155
+ loading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Validating credentials..." }) }),
2156
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Get your API token from: https://id.atlassian.com/manage-profile/security/api-tokens" }) })
1585
2157
  ] });
1586
2158
  }
1587
2159
 
@@ -1606,225 +2178,106 @@ function TicketItem({ ticketKey, summary, status, isHighlighted, isSelected }) {
1606
2178
 
1607
2179
  // src/components/jira/JiraView.tsx
1608
2180
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1609
- function JiraView({ isFocused, onModalChange, onKeybindingsChange, onLogUpdated }) {
1610
- const [repoPath, setRepoPath] = useState7(null);
1611
- const [currentBranch, setCurrentBranch] = useState7(null);
1612
- const [isRepo, setIsRepo] = useState7(null);
1613
- const [jiraState, setJiraState] = useState7("not_configured");
1614
- const [tickets, setTickets] = useState7([]);
1615
- const [highlightedIndex, setHighlightedIndex] = useState7(0);
1616
- const [showConfigureModal, setShowConfigureModal] = useState7(false);
1617
- const [showLinkModal, setShowLinkModal] = useState7(false);
1618
- const [showStatusModal, setShowStatusModal] = useState7(false);
1619
- const [loading, setLoading] = useState7({ configure: false, link: false });
1620
- const [errors, setErrors] = useState7({});
1621
- useEffect5(() => {
1622
- if (!isFocused) {
1623
- setShowConfigureModal(false);
1624
- setShowLinkModal(false);
1625
- setShowStatusModal(false);
1626
- setErrors({});
1627
- }
1628
- }, [isFocused]);
1629
- useEffect5(() => {
1630
- onModalChange == null ? void 0 : onModalChange(showConfigureModal || showLinkModal || showStatusModal);
1631
- }, [showConfigureModal, showLinkModal, showStatusModal, onModalChange]);
1632
- useEffect5(() => {
1633
- if (!isFocused || showConfigureModal || showLinkModal || showStatusModal) {
1634
- onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
1635
- return;
1636
- }
1637
- const bindings = [];
1638
- if (jiraState === "not_configured") {
1639
- bindings.push({ key: "c", label: "Configure Jira" });
1640
- } else if (jiraState === "no_tickets") {
1641
- bindings.push({ key: "l", label: "Link Ticket" });
1642
- } else if (jiraState === "has_tickets") {
1643
- bindings.push({ key: "l", label: "Link" });
1644
- bindings.push({ key: "s", label: "Status" });
1645
- bindings.push({ key: "d", label: "Unlink", color: "red" });
1646
- bindings.push({ key: "o", label: "Open", color: "green" });
1647
- bindings.push({ key: "y", label: "Copy Link" });
1648
- }
1649
- onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
1650
- }, [isFocused, jiraState, showConfigureModal, showLinkModal, showStatusModal, onKeybindingsChange]);
1651
- useEffect5(() => {
1652
- const gitRepoCheck = isGitRepo();
1653
- setIsRepo(gitRepoCheck);
1654
- if (!gitRepoCheck) return;
1655
- const rootResult = getRepoRoot();
1656
- if (rootResult.success) {
1657
- setRepoPath(rootResult.data);
1658
- }
1659
- const branchResult = getCurrentBranch();
1660
- if (branchResult.success) {
1661
- setCurrentBranch(branchResult.data);
1662
- }
1663
- }, []);
1664
- useEffect5(() => {
1665
- if (!repoPath || !currentBranch) return;
1666
- if (!isJiraConfigured(repoPath)) {
1667
- setJiraState("not_configured");
1668
- setTickets([]);
1669
- return;
2181
+ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated }) {
2182
+ const repo = useGitRepo();
2183
+ const jira = useJiraTickets();
2184
+ const modal = useModal();
2185
+ const nav = useListNavigation(jira.tickets.length);
2186
+ const lastInitRef = useRef6(null);
2187
+ useEffect11(() => {
2188
+ if (repo.loading || !repo.repoPath || !repo.currentBranch) return;
2189
+ const current = { branch: repo.currentBranch };
2190
+ const last = lastInitRef.current;
2191
+ if (last && last.branch === current.branch) return;
2192
+ lastInitRef.current = current;
2193
+ jira.initializeJiraState(repo.repoPath, repo.currentBranch, repo.currentRepoSlug);
2194
+ }, [repo.loading, repo.repoPath, repo.currentBranch, repo.currentRepoSlug, jira.initializeJiraState]);
2195
+ useEffect11(() => {
2196
+ if (isFocused) {
2197
+ repo.refreshBranch();
2198
+ } else {
2199
+ modal.close();
1670
2200
  }
1671
- const linkedTickets = getLinkedTickets(repoPath, currentBranch);
1672
- setTickets(linkedTickets);
1673
- setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
1674
- }, [repoPath, currentBranch]);
1675
- useEffect5(() => {
1676
- if (!repoPath || !currentBranch) return;
1677
- if (jiraState !== "no_tickets") return;
1678
- const ticketKey = extractTicketKeyFromBranch(currentBranch);
1679
- if (!ticketKey) return;
1680
- const existingTickets = getLinkedTickets(repoPath, currentBranch);
1681
- if (existingTickets.some((t) => t.key === ticketKey)) return;
1682
- const siteUrl = getJiraSiteUrl(repoPath);
1683
- const creds = getJiraCredentials(repoPath);
1684
- if (!siteUrl || !creds.email || !creds.apiToken) return;
1685
- const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
1686
- getIssue(auth, ticketKey).then((result) => {
1687
- if (result.success) {
1688
- const linkedTicket = {
1689
- key: result.data.key,
1690
- summary: result.data.fields.summary,
1691
- status: result.data.fields.status.name,
1692
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1693
- };
1694
- addLinkedTicket(repoPath, currentBranch, linkedTicket);
1695
- setTickets([linkedTicket]);
1696
- setJiraState("has_tickets");
1697
- }
1698
- });
1699
- }, [repoPath, currentBranch, jiraState]);
1700
- const refreshTickets = useCallback2(() => {
1701
- if (!repoPath || !currentBranch) return;
1702
- const linkedTickets = getLinkedTickets(repoPath, currentBranch);
1703
- setTickets(linkedTickets);
1704
- setJiraState(linkedTickets.length > 0 ? "has_tickets" : "no_tickets");
1705
- }, [repoPath, currentBranch]);
1706
- const handleConfigureSubmit = useCallback2(
1707
- async (siteUrl, email, apiToken) => {
1708
- if (!repoPath) return;
1709
- setLoading((prev) => ({ ...prev, configure: true }));
1710
- setErrors((prev) => ({ ...prev, configure: void 0 }));
1711
- const auth = { siteUrl, email, apiToken };
1712
- const result = await validateCredentials(auth);
1713
- if (!result.success) {
1714
- setErrors((prev) => ({ ...prev, configure: result.error }));
1715
- setLoading((prev) => ({ ...prev, configure: false }));
1716
- return;
1717
- }
1718
- setJiraSiteUrl(repoPath, siteUrl);
1719
- setJiraCredentials(repoPath, email, apiToken);
1720
- setShowConfigureModal(false);
1721
- setJiraState("no_tickets");
1722
- setLoading((prev) => ({ ...prev, configure: false }));
1723
- },
1724
- [repoPath]
1725
- );
1726
- const handleLinkSubmit = useCallback2(
1727
- async (ticketInput) => {
1728
- if (!repoPath || !currentBranch) return;
1729
- setLoading((prev) => ({ ...prev, link: true }));
1730
- setErrors((prev) => ({ ...prev, link: void 0 }));
1731
- const ticketKey = parseTicketKey(ticketInput);
1732
- if (!ticketKey) {
1733
- setErrors((prev) => ({ ...prev, link: "Invalid ticket format. Use PROJ-123 or a Jira URL." }));
1734
- setLoading((prev) => ({ ...prev, link: false }));
1735
- return;
1736
- }
1737
- const siteUrl = getJiraSiteUrl(repoPath);
1738
- const creds = getJiraCredentials(repoPath);
1739
- if (!siteUrl || !creds.email || !creds.apiToken) {
1740
- setErrors((prev) => ({ ...prev, link: "Jira not configured" }));
1741
- setLoading((prev) => ({ ...prev, link: false }));
1742
- return;
1743
- }
1744
- const auth = { siteUrl, email: creds.email, apiToken: creds.apiToken };
1745
- const result = await getIssue(auth, ticketKey);
1746
- if (!result.success) {
1747
- setErrors((prev) => ({ ...prev, link: result.error }));
1748
- setLoading((prev) => ({ ...prev, link: false }));
1749
- return;
1750
- }
1751
- const linkedTicket = {
1752
- key: result.data.key,
1753
- summary: result.data.fields.summary,
1754
- status: result.data.fields.status.name,
1755
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1756
- };
1757
- addLinkedTicket(repoPath, currentBranch, linkedTicket);
1758
- refreshTickets();
1759
- setShowLinkModal(false);
1760
- setLoading((prev) => ({ ...prev, link: false }));
1761
- },
1762
- [repoPath, currentBranch, refreshTickets]
1763
- );
1764
- const handleUnlinkTicket = useCallback2(() => {
1765
- if (!repoPath || !currentBranch || tickets.length === 0) return;
1766
- const ticket = tickets[highlightedIndex];
2201
+ }, [isFocused, repo.refreshBranch, modal.close]);
2202
+ useEffect11(() => {
2203
+ onModalChange == null ? void 0 : onModalChange(modal.isOpen);
2204
+ }, [modal.isOpen, onModalChange]);
2205
+ useEffect11(() => {
2206
+ onJiraStateChange == null ? void 0 : onJiraStateChange(jira.jiraState);
2207
+ }, [jira.jiraState, onJiraStateChange]);
2208
+ const handleConfigureSubmit = async (siteUrl, email, apiToken) => {
2209
+ if (!repo.repoPath) return;
2210
+ const success = await jira.configureJira(repo.repoPath, siteUrl, email, apiToken);
2211
+ if (success) modal.close();
2212
+ };
2213
+ const handleLinkSubmit = async (ticketInput) => {
2214
+ if (!repo.repoPath || !repo.currentBranch) return;
2215
+ const success = await jira.linkTicket(repo.repoPath, repo.currentBranch, ticketInput);
2216
+ if (success) modal.close();
2217
+ };
2218
+ const handleUnlinkTicket = () => {
2219
+ if (!repo.repoPath || !repo.currentBranch || jira.tickets.length === 0) return;
2220
+ const ticket = jira.tickets[nav.index];
1767
2221
  if (ticket) {
1768
- removeLinkedTicket(repoPath, currentBranch, ticket.key);
1769
- refreshTickets();
1770
- setHighlightedIndex((prev) => Math.max(0, prev - 1));
2222
+ jira.unlinkTicket(repo.repoPath, repo.currentBranch, ticket.key);
2223
+ jira.refreshTickets(repo.repoPath, repo.currentBranch);
2224
+ nav.prev();
1771
2225
  }
1772
- }, [repoPath, currentBranch, tickets, highlightedIndex, refreshTickets]);
1773
- const handleOpenInBrowser = useCallback2(() => {
1774
- if (!repoPath || tickets.length === 0) return;
1775
- const ticket = tickets[highlightedIndex];
1776
- const siteUrl = getJiraSiteUrl(repoPath);
2226
+ };
2227
+ const handleOpenInBrowser = () => {
2228
+ if (!repo.repoPath || jira.tickets.length === 0) return;
2229
+ const ticket = jira.tickets[nav.index];
2230
+ const siteUrl = getJiraSiteUrl(repo.repoPath);
1777
2231
  if (ticket && siteUrl) {
1778
- const url = `${siteUrl}/browse/${ticket.key}`;
1779
- open2(url).catch(() => {
2232
+ open3(`${siteUrl}/browse/${ticket.key}`).catch(() => {
1780
2233
  });
1781
2234
  }
1782
- }, [repoPath, tickets, highlightedIndex]);
2235
+ };
2236
+ const handleCopyLink = () => {
2237
+ if (!repo.repoPath || jira.tickets.length === 0) return;
2238
+ const ticket = jira.tickets[nav.index];
2239
+ const siteUrl = getJiraSiteUrl(repo.repoPath);
2240
+ if (ticket && siteUrl) {
2241
+ copyToClipboard(`${siteUrl}/browse/${ticket.key}`);
2242
+ }
2243
+ };
2244
+ const handleStatusComplete = (newStatus) => {
2245
+ if (!repo.repoPath || !repo.currentBranch) return;
2246
+ const ticket = jira.tickets[nav.index];
2247
+ if (!ticket) return;
2248
+ updateTicketStatus(repo.repoPath, repo.currentBranch, ticket.key, newStatus);
2249
+ logJiraStatusChanged(ticket.key, ticket.summary, ticket.status, newStatus);
2250
+ onLogUpdated == null ? void 0 : onLogUpdated();
2251
+ modal.close();
2252
+ jira.refreshTickets(repo.repoPath, repo.currentBranch);
2253
+ };
1783
2254
  useInput9(
1784
2255
  (input, key) => {
1785
- if (showConfigureModal || showLinkModal || showStatusModal) return;
1786
- if (input === "c" && jiraState === "not_configured") {
1787
- setShowConfigureModal(true);
2256
+ if (input === "c" && jira.jiraState === "not_configured") {
2257
+ modal.open("configure");
1788
2258
  return;
1789
2259
  }
1790
- if (input === "l" && jiraState !== "not_configured") {
1791
- setShowLinkModal(true);
2260
+ if (input === "l" && jira.jiraState !== "not_configured") {
2261
+ modal.open("link");
1792
2262
  return;
1793
2263
  }
1794
- if (jiraState === "has_tickets") {
1795
- if (key.upArrow || input === "k") {
1796
- setHighlightedIndex((prev) => Math.max(0, prev - 1));
1797
- }
1798
- if (key.downArrow || input === "j") {
1799
- setHighlightedIndex((prev) => Math.min(tickets.length - 1, prev + 1));
1800
- }
1801
- if (input === "s") {
1802
- setShowStatusModal(true);
1803
- }
1804
- if (input === "d") {
1805
- handleUnlinkTicket();
1806
- }
1807
- if (input === "o") {
1808
- handleOpenInBrowser();
1809
- }
1810
- if (input === "y" && repoPath) {
1811
- const ticket = tickets[highlightedIndex];
1812
- const siteUrl = getJiraSiteUrl(repoPath);
1813
- if (ticket && siteUrl) {
1814
- const url = `${siteUrl}/browse/${ticket.key}`;
1815
- copyToClipboard(url);
1816
- }
1817
- }
2264
+ if (jira.jiraState === "has_tickets") {
2265
+ if (key.upArrow || input === "k") nav.prev();
2266
+ if (key.downArrow || input === "j") nav.next();
2267
+ if (input === "s") modal.open("status");
2268
+ if (input === "d") handleUnlinkTicket();
2269
+ if (input === "o") handleOpenInBrowser();
2270
+ if (input === "y") handleCopyLink();
1818
2271
  }
1819
2272
  },
1820
- { isActive: isFocused && !showConfigureModal && !showLinkModal && !showStatusModal }
2273
+ { isActive: isFocused && !modal.isOpen }
1821
2274
  );
1822
- if (isRepo === false) {
2275
+ if (repo.isRepo === false) {
1823
2276
  return /* @__PURE__ */ jsx11(TitledBox4, { borderStyle: "round", titles: ["Jira"], flexShrink: 0, children: /* @__PURE__ */ jsx11(Text11, { color: "red", children: "Not a git repository" }) });
1824
2277
  }
1825
- if (showConfigureModal) {
1826
- const siteUrl = repoPath ? getJiraSiteUrl(repoPath) : void 0;
1827
- const creds = repoPath ? getJiraCredentials(repoPath) : { email: null, apiToken: null };
2278
+ if (modal.type === "configure") {
2279
+ const siteUrl = repo.repoPath ? getJiraSiteUrl(repo.repoPath) : void 0;
2280
+ const creds = repo.repoPath ? getJiraCredentials(repo.repoPath) : { email: null, apiToken: null };
1828
2281
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
1829
2282
  ConfigureJiraSiteModal,
1830
2283
  {
@@ -1832,60 +2285,53 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange, onLogUpdated
1832
2285
  initialEmail: creds.email ?? void 0,
1833
2286
  onSubmit: handleConfigureSubmit,
1834
2287
  onCancel: () => {
1835
- setShowConfigureModal(false);
1836
- setErrors((prev) => ({ ...prev, configure: void 0 }));
2288
+ modal.close();
2289
+ jira.clearError("configure");
1837
2290
  },
1838
- loading: loading.configure,
1839
- error: errors.configure
2291
+ loading: jira.loading.configure,
2292
+ error: jira.errors.configure
1840
2293
  }
1841
2294
  ) });
1842
2295
  }
1843
- if (showLinkModal) {
2296
+ if (modal.type === "link") {
1844
2297
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
1845
2298
  LinkTicketModal,
1846
2299
  {
1847
2300
  onSubmit: handleLinkSubmit,
1848
2301
  onCancel: () => {
1849
- setShowLinkModal(false);
1850
- setErrors((prev) => ({ ...prev, link: void 0 }));
2302
+ modal.close();
2303
+ jira.clearError("link");
1851
2304
  },
1852
- loading: loading.link,
1853
- error: errors.link
2305
+ loading: jira.loading.link,
2306
+ error: jira.errors.link
1854
2307
  }
1855
2308
  ) });
1856
2309
  }
1857
- if (showStatusModal && repoPath && currentBranch && tickets[highlightedIndex]) {
1858
- const ticket = tickets[highlightedIndex];
2310
+ if (modal.type === "status" && repo.repoPath && repo.currentBranch && jira.tickets[nav.index]) {
2311
+ const ticket = jira.tickets[nav.index];
1859
2312
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
1860
2313
  ChangeStatusModal,
1861
2314
  {
1862
- repoPath,
2315
+ repoPath: repo.repoPath,
1863
2316
  ticketKey: ticket.key,
1864
2317
  currentStatus: ticket.status,
1865
- onComplete: (newStatus) => {
1866
- const oldStatus = ticket.status;
1867
- updateTicketStatus(repoPath, currentBranch, ticket.key, newStatus);
1868
- logJiraStatusChanged(ticket.key, ticket.summary, oldStatus, newStatus);
1869
- onLogUpdated == null ? void 0 : onLogUpdated();
1870
- setShowStatusModal(false);
1871
- refreshTickets();
1872
- },
1873
- onCancel: () => setShowStatusModal(false)
2318
+ onComplete: handleStatusComplete,
2319
+ onCancel: modal.close
1874
2320
  }
1875
2321
  ) });
1876
2322
  }
1877
2323
  const title = "[4] Jira";
1878
2324
  const borderColor = isFocused ? "yellow" : void 0;
1879
2325
  return /* @__PURE__ */ jsx11(TitledBox4, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, children: [
1880
- jiraState === "not_configured" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No Jira site configured" }),
1881
- jiraState === "no_tickets" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No tickets linked to this branch" }),
1882
- jiraState === "has_tickets" && tickets.map((ticket, idx) => /* @__PURE__ */ jsx11(
2326
+ jira.jiraState === "not_configured" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No Jira site configured" }),
2327
+ jira.jiraState === "no_tickets" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No tickets linked to this branch" }),
2328
+ jira.jiraState === "has_tickets" && jira.tickets.map((ticket, idx) => /* @__PURE__ */ jsx11(
1883
2329
  TicketItem,
1884
2330
  {
1885
2331
  ticketKey: ticket.key,
1886
2332
  summary: ticket.summary,
1887
2333
  status: ticket.status,
1888
- isHighlighted: idx === highlightedIndex
2334
+ isHighlighted: idx === nav.index
1889
2335
  },
1890
2336
  ticket.key
1891
2337
  ))
@@ -1893,84 +2339,25 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange, onLogUpdated
1893
2339
  }
1894
2340
 
1895
2341
  // src/components/logs/LogsView.tsx
1896
- import { useCallback as useCallback3, useEffect as useEffect6, useState as useState9 } from "react";
2342
+ import { useEffect as useEffect12 } from "react";
1897
2343
  import { Box as Box14, useInput as useInput12 } from "ink";
1898
2344
 
1899
- // src/components/logs/LogsHistoryBox.tsx
2345
+ // src/components/logs/LogViewerBox.tsx
2346
+ import { useRef as useRef7, useState as useState16 } from "react";
1900
2347
  import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
1901
2348
  import { Box as Box12, Text as Text12, useInput as useInput10 } from "ink";
1902
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1903
- function LogsHistoryBox({
1904
- logFiles,
1905
- selectedDate,
1906
- highlightedIndex,
1907
- onHighlight,
1908
- onSelect,
1909
- isFocused
1910
- }) {
1911
- const title = "[5] Logs";
1912
- const borderColor = isFocused ? "yellow" : void 0;
1913
- useInput10(
1914
- (input, key) => {
1915
- if (logFiles.length === 0) return;
1916
- if (key.upArrow || input === "k") {
1917
- onHighlight(Math.max(0, highlightedIndex - 1));
1918
- }
1919
- if (key.downArrow || input === "j") {
1920
- onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
1921
- }
1922
- if (key.return) {
1923
- const file = logFiles[highlightedIndex];
1924
- if (file) {
1925
- onSelect(file.date);
1926
- }
1927
- }
1928
- },
1929
- { isActive: isFocused }
1930
- );
1931
- return /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
1932
- logFiles.length === 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "No logs yet" }),
1933
- logFiles.map((file, idx) => {
1934
- const isHighlighted = idx === highlightedIndex;
1935
- const isSelected = file.date === selectedDate;
1936
- const cursor = isHighlighted ? ">" : " ";
1937
- const indicator = isSelected ? " *" : "";
1938
- return /* @__PURE__ */ jsxs12(Box12, { children: [
1939
- /* @__PURE__ */ jsxs12(Text12, { color: isHighlighted ? "yellow" : void 0, children: [
1940
- cursor,
1941
- " "
1942
- ] }),
1943
- /* @__PURE__ */ jsx12(
1944
- Text12,
1945
- {
1946
- color: file.isToday ? "green" : void 0,
1947
- bold: file.isToday,
1948
- children: file.date
1949
- }
1950
- ),
1951
- file.isToday && /* @__PURE__ */ jsx12(Text12, { color: "green", children: " (today)" }),
1952
- /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: indicator })
1953
- ] }, file.date);
1954
- })
1955
- ] }) });
1956
- }
1957
-
1958
- // src/components/logs/LogViewerBox.tsx
1959
- import { useRef as useRef3, useState as useState8 } from "react";
1960
- import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
1961
- import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
1962
- import { ScrollView as ScrollView2 } from "ink-scroll-view";
2349
+ import { ScrollView as ScrollView4 } from "ink-scroll-view";
1963
2350
  import TextInput2 from "ink-text-input";
1964
2351
 
1965
2352
  // src/lib/claude/api.ts
1966
- import { exec as exec4 } from "child_process";
2353
+ import { exec as exec3 } from "child_process";
1967
2354
  function runClaudePrompt(prompt) {
1968
2355
  let childProcess = null;
1969
2356
  let cancelled = false;
1970
2357
  const promise = new Promise((resolve) => {
1971
2358
  const escapedPrompt = prompt.replace(/'/g, "'\\''").replace(/\n/g, "\\n");
1972
2359
  const command = `claude -p $'${escapedPrompt}' --output-format json < /dev/null`;
1973
- childProcess = exec4(command, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
2360
+ childProcess = exec3(command, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
1974
2361
  if (cancelled) {
1975
2362
  resolve({
1976
2363
  success: false,
@@ -2046,18 +2433,18 @@ Generate the standup notes:`;
2046
2433
  }
2047
2434
 
2048
2435
  // src/components/logs/LogViewerBox.tsx
2049
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2436
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2050
2437
  function LogViewerBox({ date, content, isFocused, onRefresh, onLogCreated }) {
2051
- const scrollRef = useRef3(null);
2052
- const [isInputMode, setIsInputMode] = useState8(false);
2053
- const [inputValue, setInputValue] = useState8("");
2054
- const [isGeneratingStandup, setIsGeneratingStandup] = useState8(false);
2055
- const [standupResult, setStandupResult] = useState8(null);
2056
- const claudeProcessRef = useRef3(null);
2438
+ const scrollRef = useRef7(null);
2439
+ const [isInputMode, setIsInputMode] = useState16(false);
2440
+ const [inputValue, setInputValue] = useState16("");
2441
+ const [isGeneratingStandup, setIsGeneratingStandup] = useState16(false);
2442
+ const [standupResult, setStandupResult] = useState16(null);
2443
+ const claudeProcessRef = useRef7(null);
2057
2444
  const title = "[6] Log Content";
2058
2445
  const borderColor = isFocused ? "yellow" : void 0;
2059
2446
  const displayTitle = date ? `${title} - ${date}.md` : title;
2060
- useInput11(
2447
+ useInput10(
2061
2448
  (input, key) => {
2062
2449
  var _a, _b, _c;
2063
2450
  if (key.escape && isInputMode) {
@@ -2136,14 +2523,14 @@ ${value.trim()}
2136
2523
  setIsInputMode(false);
2137
2524
  onRefresh();
2138
2525
  };
2139
- return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", flexGrow: 1, children: [
2140
- /* @__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: [
2141
- !date && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Select a log file to view" }),
2142
- date && content === null && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Log file not found" }),
2143
- date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Empty log file" }),
2144
- date && content && content.trim() !== "" && /* @__PURE__ */ jsx13(Markdown, { children: content })
2526
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", flexGrow: 1, children: [
2527
+ /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx12(ScrollView4, { ref: scrollRef, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
2528
+ !date && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Select a log file to view" }),
2529
+ date && content === null && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Log file not found" }),
2530
+ date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Empty log file" }),
2531
+ date && content && content.trim() !== "" && /* @__PURE__ */ jsx12(Markdown, { children: content })
2145
2532
  ] }) }) }) }),
2146
- isInputMode && /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: ["Add Entry"], borderColor: "yellow", children: /* @__PURE__ */ jsx13(Box13, { paddingX: 1, children: /* @__PURE__ */ jsx13(
2533
+ isInputMode && /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: ["Add Entry"], borderColor: "yellow", children: /* @__PURE__ */ jsx12(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx12(
2147
2534
  TextInput2,
2148
2535
  {
2149
2536
  value: inputValue,
@@ -2151,114 +2538,88 @@ ${value.trim()}
2151
2538
  onSubmit: handleInputSubmit
2152
2539
  }
2153
2540
  ) }) }),
2154
- isGeneratingStandup && /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: ["Standup Notes"], borderColor: "yellow", children: /* @__PURE__ */ jsxs13(Box13, { paddingX: 1, flexDirection: "column", children: [
2155
- /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: "Generating standup notes..." }),
2156
- /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Press Esc to cancel" })
2541
+ isGeneratingStandup && /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: ["Standup Notes"], borderColor: "yellow", children: /* @__PURE__ */ jsxs12(Box12, { paddingX: 1, flexDirection: "column", children: [
2542
+ /* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "Generating standup notes..." }),
2543
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Press Esc to cancel" })
2157
2544
  ] }) }),
2158
- standupResult && /* @__PURE__ */ jsx13(
2159
- TitledBox6,
2545
+ standupResult && /* @__PURE__ */ jsx12(
2546
+ TitledBox5,
2160
2547
  {
2161
2548
  borderStyle: "round",
2162
2549
  titles: ["Standup Notes"],
2163
2550
  borderColor: standupResult.type === "error" ? "red" : "green",
2164
- children: /* @__PURE__ */ jsxs13(Box13, { paddingX: 1, flexDirection: "column", children: [
2165
- standupResult.type === "error" ? /* @__PURE__ */ jsx13(Text13, { color: "red", children: standupResult.message }) : /* @__PURE__ */ jsx13(Markdown, { children: standupResult.message }),
2166
- /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Press Esc to dismiss" })
2551
+ children: /* @__PURE__ */ jsxs12(Box12, { paddingX: 1, flexDirection: "column", children: [
2552
+ standupResult.type === "error" ? /* @__PURE__ */ jsx12(Text12, { color: "red", children: standupResult.message }) : /* @__PURE__ */ jsx12(Markdown, { children: standupResult.message }),
2553
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Press Esc to dismiss" })
2167
2554
  ] })
2168
2555
  }
2169
2556
  )
2170
2557
  ] });
2171
2558
  }
2172
2559
 
2173
- // src/components/logs/LogsView.tsx
2174
- import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2175
- function LogsView({ isFocused, onKeybindingsChange, refreshKey, focusedBox, onFocusedBoxChange }) {
2176
- const [logFiles, setLogFiles] = useState9([]);
2177
- const [selectedDate, setSelectedDate] = useState9(null);
2178
- const [logContent, setLogContent] = useState9(null);
2179
- const [highlightedIndex, setHighlightedIndex] = useState9(0);
2180
- useEffect6(() => {
2181
- if (!isFocused) {
2182
- onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
2183
- return;
2184
- }
2185
- const bindings = [];
2186
- if (focusedBox === "history") {
2187
- bindings.push({ key: "Enter", label: "Select" });
2188
- } else if (focusedBox === "viewer") {
2189
- bindings.push({ key: "i", label: "Add Entry" });
2190
- bindings.push({ key: "e", label: "Edit" });
2191
- bindings.push({ key: "n", label: "New Log", color: "green" });
2192
- bindings.push({ key: "c", label: "Standup" });
2193
- bindings.push({ key: "r", label: "Refresh" });
2194
- }
2195
- onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
2196
- }, [isFocused, focusedBox, onKeybindingsChange]);
2197
- const refreshLogFiles = useCallback3(() => {
2198
- const files = listLogFiles();
2199
- setLogFiles(files);
2200
- if (files.length > 0 && !selectedDate) {
2201
- const today = getTodayDate();
2202
- const todayFile = files.find((f) => f.date === today);
2203
- if (todayFile) {
2204
- setSelectedDate(todayFile.date);
2205
- const idx = files.findIndex((f) => f.date === today);
2206
- setHighlightedIndex(idx >= 0 ? idx : 0);
2207
- } else {
2208
- setSelectedDate(files[0].date);
2209
- setHighlightedIndex(0);
2560
+ // src/components/logs/LogsHistoryBox.tsx
2561
+ import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
2562
+ import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
2563
+ import { ScrollView as ScrollView5 } from "ink-scroll-view";
2564
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2565
+ function LogsHistoryBox({
2566
+ logFiles,
2567
+ selectedDate,
2568
+ highlightedIndex,
2569
+ onHighlight,
2570
+ onSelect,
2571
+ isFocused
2572
+ }) {
2573
+ const scrollRef = useScrollToIndex(highlightedIndex);
2574
+ const title = "[5] Logs";
2575
+ const borderColor = isFocused ? "yellow" : void 0;
2576
+ useInput11(
2577
+ (input, key) => {
2578
+ if (logFiles.length === 0) return;
2579
+ if (key.upArrow || input === "k") {
2580
+ onHighlight(Math.max(0, highlightedIndex - 1));
2210
2581
  }
2211
- }
2212
- }, [selectedDate]);
2213
- useEffect6(() => {
2214
- refreshLogFiles();
2215
- }, [refreshLogFiles]);
2216
- useEffect6(() => {
2217
- if (selectedDate) {
2218
- const content = readLog(selectedDate);
2219
- setLogContent(content);
2220
- } else {
2221
- setLogContent(null);
2222
- }
2223
- }, [selectedDate]);
2224
- useEffect6(() => {
2225
- if (refreshKey !== void 0 && refreshKey > 0) {
2226
- const files = listLogFiles();
2227
- setLogFiles(files);
2228
- const today = getTodayDate();
2229
- if (selectedDate === today) {
2230
- const content = readLog(today);
2231
- setLogContent(content);
2232
- } else if (!selectedDate && files.length > 0) {
2233
- const todayFile = files.find((f) => f.date === today);
2234
- if (todayFile) {
2235
- setSelectedDate(today);
2236
- const idx = files.findIndex((f) => f.date === today);
2237
- setHighlightedIndex(idx >= 0 ? idx : 0);
2582
+ if (key.downArrow || input === "j") {
2583
+ onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
2584
+ }
2585
+ if (key.return) {
2586
+ const file = logFiles[highlightedIndex];
2587
+ if (file) {
2588
+ onSelect(file.date);
2238
2589
  }
2239
2590
  }
2591
+ },
2592
+ { isActive: isFocused }
2593
+ );
2594
+ return /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
2595
+ logFiles.length === 0 && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "No logs yet" }),
2596
+ logFiles.length > 0 && /* @__PURE__ */ jsx13(ScrollView5, { ref: scrollRef, children: logFiles.map((file, idx) => {
2597
+ const isHighlighted = idx === highlightedIndex;
2598
+ const isSelected = file.date === selectedDate;
2599
+ const cursor = isHighlighted ? ">" : " ";
2600
+ const indicator = isSelected ? " *" : "";
2601
+ return /* @__PURE__ */ jsxs13(Box13, { children: [
2602
+ /* @__PURE__ */ jsxs13(Text13, { color: isHighlighted ? "yellow" : void 0, children: [
2603
+ cursor,
2604
+ " "
2605
+ ] }),
2606
+ /* @__PURE__ */ jsx13(Text13, { color: file.isToday ? "green" : void 0, bold: file.isToday, children: file.date }),
2607
+ file.isToday && /* @__PURE__ */ jsx13(Text13, { color: "green", children: " (today)" }),
2608
+ /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: indicator })
2609
+ ] }, file.date);
2610
+ }) })
2611
+ ] }) });
2612
+ }
2613
+
2614
+ // src/components/logs/LogsView.tsx
2615
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2616
+ function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
2617
+ const logs = useLogs();
2618
+ useEffect12(() => {
2619
+ if (refreshKey !== void 0 && refreshKey > 0) {
2620
+ logs.handleExternalLogUpdate();
2240
2621
  }
2241
- }, [refreshKey, selectedDate]);
2242
- const handleSelectDate = useCallback3((date) => {
2243
- setSelectedDate(date);
2244
- }, []);
2245
- const handleRefresh = useCallback3(() => {
2246
- refreshLogFiles();
2247
- if (selectedDate) {
2248
- const content = readLog(selectedDate);
2249
- setLogContent(content);
2250
- }
2251
- }, [refreshLogFiles, selectedDate]);
2252
- const handleLogCreated = useCallback3(() => {
2253
- const files = listLogFiles();
2254
- setLogFiles(files);
2255
- const today = getTodayDate();
2256
- setSelectedDate(today);
2257
- const idx = files.findIndex((f) => f.date === today);
2258
- setHighlightedIndex(idx >= 0 ? idx : 0);
2259
- const content = readLog(today);
2260
- setLogContent(content);
2261
- }, []);
2622
+ }, [refreshKey, logs.handleExternalLogUpdate]);
2262
2623
  useInput12(
2263
2624
  (input) => {
2264
2625
  if (input === "5") onFocusedBoxChange("history");
@@ -2270,22 +2631,22 @@ function LogsView({ isFocused, onKeybindingsChange, refreshKey, focusedBox, onFo
2270
2631
  /* @__PURE__ */ jsx14(
2271
2632
  LogsHistoryBox,
2272
2633
  {
2273
- logFiles,
2274
- selectedDate,
2275
- highlightedIndex,
2276
- onHighlight: setHighlightedIndex,
2277
- onSelect: handleSelectDate,
2634
+ logFiles: logs.logFiles,
2635
+ selectedDate: logs.selectedDate,
2636
+ highlightedIndex: logs.highlightedIndex,
2637
+ onHighlight: logs.setHighlightedIndex,
2638
+ onSelect: logs.selectDate,
2278
2639
  isFocused: isFocused && focusedBox === "history"
2279
2640
  }
2280
2641
  ),
2281
2642
  /* @__PURE__ */ jsx14(
2282
2643
  LogViewerBox,
2283
2644
  {
2284
- date: selectedDate,
2285
- content: logContent,
2645
+ date: logs.selectedDate,
2646
+ content: logs.logContent,
2286
2647
  isFocused: isFocused && focusedBox === "viewer",
2287
- onRefresh: handleRefresh,
2288
- onLogCreated: handleLogCreated
2648
+ onRefresh: logs.refresh,
2649
+ onLogCreated: logs.handleLogCreated
2289
2650
  }
2290
2651
  )
2291
2652
  ] });
@@ -2299,27 +2660,98 @@ var globalBindings = [
2299
2660
  { key: "j/k", label: "Navigate" },
2300
2661
  { key: "Ctrl+C", label: "Quit" }
2301
2662
  ];
2302
- var modalBindings = [
2303
- { key: "Esc", label: "Cancel" }
2304
- ];
2305
- function KeybindingsBar({ contextBindings = [], modalOpen = false }) {
2663
+ var modalBindings = [{ key: "Esc", label: "Cancel" }];
2664
+ var DUCK_ASCII = "<(')___";
2665
+ function KeybindingsBar({ contextBindings = [], modalOpen = false, duck }) {
2306
2666
  const allBindings = modalOpen ? [...contextBindings, ...modalBindings] : [...contextBindings, ...globalBindings];
2307
- return /* @__PURE__ */ jsx15(Box15, { flexShrink: 0, paddingX: 1, gap: 2, children: allBindings.map((binding) => /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
2308
- /* @__PURE__ */ jsx15(Text14, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
2309
- /* @__PURE__ */ jsx15(Text14, { dimColor: true, children: binding.label })
2310
- ] }, binding.key)) });
2667
+ return /* @__PURE__ */ jsxs15(Box15, { flexShrink: 0, paddingX: 1, gap: 2, children: [
2668
+ allBindings.map((binding) => /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
2669
+ /* @__PURE__ */ jsx15(Text14, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
2670
+ /* @__PURE__ */ jsx15(Text14, { dimColor: true, children: binding.label })
2671
+ ] }, binding.key)),
2672
+ (duck == null ? void 0 : duck.visible) && /* @__PURE__ */ jsxs15(Box15, { flexGrow: 1, justifyContent: "flex-end", gap: 1, children: [
2673
+ /* @__PURE__ */ jsx15(Text14, { children: DUCK_ASCII }),
2674
+ /* @__PURE__ */ jsx15(Text14, { dimColor: true, children: duck.message })
2675
+ ] })
2676
+ ] });
2677
+ }
2678
+
2679
+ // src/constants/github.ts
2680
+ var GITHUB_KEYBINDINGS = {
2681
+ remotes: [{ key: "Space", label: "Select Remote" }],
2682
+ prs: [
2683
+ { key: "Space", label: "Select" },
2684
+ { key: "n", label: "New PR", color: "green" },
2685
+ { key: "r", label: "Refresh" },
2686
+ { key: "o", label: "Open", color: "green" },
2687
+ { key: "y", label: "Copy Link" }
2688
+ ],
2689
+ details: [
2690
+ { key: "r", label: "Refresh" },
2691
+ { key: "o", label: "Open", color: "green" }
2692
+ ]
2693
+ };
2694
+
2695
+ // src/constants/jira.ts
2696
+ var JIRA_KEYBINDINGS = {
2697
+ not_configured: [{ key: "c", label: "Configure Jira" }],
2698
+ no_tickets: [{ key: "l", label: "Link Ticket" }],
2699
+ has_tickets: [
2700
+ { key: "l", label: "Link" },
2701
+ { key: "s", label: "Status" },
2702
+ { key: "d", label: "Unlink", color: "red" },
2703
+ { key: "o", label: "Open", color: "green" },
2704
+ { key: "y", label: "Copy Link" }
2705
+ ]
2706
+ };
2707
+
2708
+ // src/constants/logs.ts
2709
+ var LOGS_KEYBINDINGS = {
2710
+ history: [{ key: "Enter", label: "Select" }],
2711
+ viewer: [
2712
+ { key: "i", label: "Add Entry" },
2713
+ { key: "e", label: "Edit" },
2714
+ { key: "n", label: "New Log", color: "green" },
2715
+ { key: "c", label: "Standup" },
2716
+ { key: "r", label: "Refresh" }
2717
+ ]
2718
+ };
2719
+
2720
+ // src/lib/keybindings.ts
2721
+ function computeKeybindings(focusedView, state) {
2722
+ switch (focusedView) {
2723
+ case "github":
2724
+ return GITHUB_KEYBINDINGS[state.github.focusedBox];
2725
+ case "jira":
2726
+ if (state.jira.modalOpen) return [];
2727
+ return JIRA_KEYBINDINGS[state.jira.jiraState];
2728
+ case "logs":
2729
+ return LOGS_KEYBINDINGS[state.logs.focusedBox];
2730
+ default:
2731
+ return [];
2732
+ }
2311
2733
  }
2312
2734
 
2313
2735
  // src/app.tsx
2314
2736
  import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
2315
2737
  function App() {
2316
2738
  const { exit } = useApp();
2317
- const [focusedView, setFocusedView] = useState10("github");
2318
- const [modalOpen, setModalOpen] = useState10(false);
2319
- const [contextBindings, setContextBindings] = useState10([]);
2320
- const [logRefreshKey, setLogRefreshKey] = useState10(0);
2321
- const [logsFocusedBox, setLogsFocusedBox] = useState10("history");
2322
- const handleLogUpdated = useCallback4(() => {
2739
+ const [focusedView, setFocusedView] = useState17("github");
2740
+ const [modalOpen, setModalOpen] = useState17(false);
2741
+ const [logRefreshKey, setLogRefreshKey] = useState17(0);
2742
+ const duck = useRubberDuck();
2743
+ const [githubFocusedBox, setGithubFocusedBox] = useState17("remotes");
2744
+ const [jiraState, setJiraState] = useState17("not_configured");
2745
+ const [logsFocusedBox, setLogsFocusedBox] = useState17("history");
2746
+ const keybindings = useMemo2(
2747
+ () => computeKeybindings(focusedView, {
2748
+ github: { focusedBox: githubFocusedBox },
2749
+ jira: { jiraState, modalOpen },
2750
+ logs: { focusedBox: logsFocusedBox }
2751
+ }),
2752
+ [focusedView, githubFocusedBox, jiraState, modalOpen, logsFocusedBox]
2753
+ );
2754
+ const handleLogUpdated = useCallback10(() => {
2323
2755
  setLogRefreshKey((prev) => prev + 1);
2324
2756
  }, []);
2325
2757
  useInput13(
@@ -2341,6 +2773,12 @@ function App() {
2341
2773
  setFocusedView("logs");
2342
2774
  setLogsFocusedBox("viewer");
2343
2775
  }
2776
+ if (input === "d") {
2777
+ duck.toggleDuck();
2778
+ }
2779
+ if (input === "q" && duck.visible) {
2780
+ duck.quack();
2781
+ }
2344
2782
  },
2345
2783
  { isActive: !modalOpen }
2346
2784
  );
@@ -2351,7 +2789,7 @@ function App() {
2351
2789
  GitHubView,
2352
2790
  {
2353
2791
  isFocused: focusedView === "github",
2354
- onKeybindingsChange: focusedView === "github" ? setContextBindings : void 0,
2792
+ onFocusedBoxChange: setGithubFocusedBox,
2355
2793
  onLogUpdated: handleLogUpdated
2356
2794
  }
2357
2795
  ),
@@ -2360,7 +2798,7 @@ function App() {
2360
2798
  {
2361
2799
  isFocused: focusedView === "jira",
2362
2800
  onModalChange: setModalOpen,
2363
- onKeybindingsChange: focusedView === "jira" ? setContextBindings : void 0,
2801
+ onJiraStateChange: setJiraState,
2364
2802
  onLogUpdated: handleLogUpdated
2365
2803
  }
2366
2804
  )
@@ -2369,14 +2807,20 @@ function App() {
2369
2807
  LogsView,
2370
2808
  {
2371
2809
  isFocused: focusedView === "logs",
2372
- onKeybindingsChange: focusedView === "logs" ? setContextBindings : void 0,
2373
2810
  refreshKey: logRefreshKey,
2374
2811
  focusedBox: logsFocusedBox,
2375
2812
  onFocusedBoxChange: setLogsFocusedBox
2376
2813
  }
2377
2814
  ) })
2378
2815
  ] }),
2379
- /* @__PURE__ */ jsx16(KeybindingsBar, { contextBindings, modalOpen })
2816
+ /* @__PURE__ */ jsx16(
2817
+ KeybindingsBar,
2818
+ {
2819
+ contextBindings: keybindings,
2820
+ modalOpen,
2821
+ duck: { visible: duck.visible, message: duck.message }
2822
+ }
2823
+ )
2380
2824
  ] });
2381
2825
  }
2382
2826
 
@@ -2384,17 +2828,14 @@ function App() {
2384
2828
  import { render as inkRender } from "ink";
2385
2829
 
2386
2830
  // src/lib/Screen.tsx
2831
+ import { useCallback as useCallback11, useEffect as useEffect13, useState as useState18 } from "react";
2387
2832
  import { Box as Box17, useStdout as useStdout2 } from "ink";
2388
- import { useCallback as useCallback5, useEffect as useEffect7, useState as useState11 } from "react";
2389
2833
  import { jsx as jsx17 } from "react/jsx-runtime";
2390
2834
  function Screen({ children }) {
2391
2835
  const { stdout } = useStdout2();
2392
- const getSize = useCallback5(
2393
- () => ({ height: stdout.rows, width: stdout.columns }),
2394
- [stdout]
2395
- );
2396
- const [size, setSize] = useState11(getSize);
2397
- useEffect7(() => {
2836
+ const getSize = useCallback11(() => ({ height: stdout.rows, width: stdout.columns }), [stdout]);
2837
+ const [size, setSize] = useState18(getSize);
2838
+ useEffect13(() => {
2398
2839
  const onResize = () => setSize(getSize());
2399
2840
  stdout.on("resize", onResize);
2400
2841
  return () => {