@nemo-cli/ui 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { ProgressBar, Spinner, ThemeProvider, defaultTheme, extendTheme } from "@inkjs/ui";
2
4
  import { Box, Static, Text, render, useApp, useInput } from "ink";
3
5
  import InkBigText from "ink-big-text";
4
6
  import Gradient from "ink-gradient";
5
- import { useCallback, useEffect, useMemo, useState } from "react";
6
7
  import { x, xASync } from "@nemo-cli/shared";
7
- import { ProgressBar, Spinner, ThemeProvider, defaultTheme, extendTheme } from "@inkjs/ui";
8
8
 
9
9
  //#region \0rolldown/runtime.js
10
10
  var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -219,46 +219,1365 @@ var require_react_jsx_runtime_development = /* @__PURE__ */ __commonJSMin(((expo
219
219
  function validateChildKeys(node) {
220
220
  isValidElement(node) ? node._store && (node._store.validated = 1) : "object" === typeof node && null !== node && node.$$typeof === REACT_LAZY_TYPE && ("fulfilled" === node._payload.status ? isValidElement(node._payload.value) && node._payload.value._store && (node._payload.value._store.validated = 1) : node._store && (node._store.validated = 1));
221
221
  }
222
- function isValidElement(object) {
223
- return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE;
222
+ function isValidElement(object) {
223
+ return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE;
224
+ }
225
+ var React = __require("react"), REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, hasOwnProperty = Object.prototype.hasOwnProperty, isArrayImpl = Array.isArray, createTask = console.createTask ? console.createTask : function() {
226
+ return null;
227
+ };
228
+ React = { react_stack_bottom_frame: function(callStackForError) {
229
+ return callStackForError();
230
+ } };
231
+ var specialPropKeyWarningShown;
232
+ var didWarnAboutElementRef = {};
233
+ var unknownOwnerDebugStack = React.react_stack_bottom_frame.bind(React, UnknownOwner)();
234
+ var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));
235
+ var didWarnAboutKeySpread = {};
236
+ exports.Fragment = REACT_FRAGMENT_TYPE;
237
+ exports.jsx = function(type, config, maybeKey) {
238
+ var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
239
+ return jsxDEVImpl(type, config, maybeKey, !1, trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
240
+ };
241
+ exports.jsxs = function(type, config, maybeKey) {
242
+ var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
243
+ return jsxDEVImpl(type, config, maybeKey, !0, trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
244
+ };
245
+ })();
246
+ }));
247
+
248
+ //#endregion
249
+ //#region ../../node_modules/.pnpm/react@19.2.4/node_modules/react/jsx-runtime.js
250
+ var require_jsx_runtime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
251
+ if (process.env.NODE_ENV === "production") module.exports = require_react_jsx_runtime_production();
252
+ else module.exports = require_react_jsx_runtime_development();
253
+ }));
254
+
255
+ //#endregion
256
+ //#region src/components/provider/index.tsx
257
+ var import_jsx_runtime = require_jsx_runtime();
258
+ const customTheme = extendTheme(defaultTheme, { components: {
259
+ ProgressBar: { styles: {
260
+ completed: () => ({ color: "green" }),
261
+ remaining: () => ({ backgroundColor: "#fff" })
262
+ } },
263
+ Spinner: { styles: { frame: () => ({ color: "#fff" }) } }
264
+ } });
265
+ const Provider = ({ children }) => {
266
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThemeProvider, {
267
+ theme: customTheme,
268
+ children
269
+ });
270
+ };
271
+
272
+ //#endregion
273
+ //#region src/components/ai-progress-viewer.tsx
274
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
275
+ const renderBar = (completed, total, width) => {
276
+ if (total <= 0) return "[----------]";
277
+ const ratio = clamp(completed / total, 0, 1);
278
+ const filled = Math.round(ratio * width);
279
+ const empty = Math.max(0, width - filled);
280
+ return `[${"#".repeat(filled)}${"-".repeat(empty)}]`;
281
+ };
282
+ const AiProgressViewer = ({ title, onStart, onExit }) => {
283
+ const [update, setUpdate] = useState({
284
+ total: 0,
285
+ completed: 0,
286
+ status: "pending"
287
+ });
288
+ const [messages, setMessages] = useState([]);
289
+ const [error, setError] = useState(null);
290
+ const [done, setDone] = useState(false);
291
+ const controllerRef = useRef(null);
292
+ const { exit } = useApp();
293
+ useEffect(() => {
294
+ const controller = new AbortController();
295
+ controllerRef.current = controller;
296
+ const emit = (next) => {
297
+ setUpdate(next);
298
+ if (next.message) setMessages((prev) => [...prev.slice(-4), next.message ?? ""]);
299
+ };
300
+ const start = async () => {
301
+ try {
302
+ await onStart(emit, controller.signal);
303
+ setDone(true);
304
+ } catch (err) {
305
+ setError(err instanceof Error ? err.message : "Unknown error");
306
+ }
307
+ };
308
+ start();
309
+ }, [onStart]);
310
+ useInput((input, key) => {
311
+ if (input === "q" || key.escape) {
312
+ controllerRef.current?.abort();
313
+ onExit?.();
314
+ exit();
315
+ }
316
+ });
317
+ useEffect(() => {
318
+ if (!done) return;
319
+ const timer = setTimeout(() => {
320
+ onExit?.();
321
+ exit();
322
+ }, 300);
323
+ return () => clearTimeout(timer);
324
+ }, [
325
+ done,
326
+ exit,
327
+ onExit
328
+ ]);
329
+ const barWidth = useMemo(() => {
330
+ return clamp((process.stdout.columns || 80) - 20, 10, 40);
331
+ }, []);
332
+ if (error) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
333
+ paddingX: 1,
334
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
335
+ color: "red",
336
+ children: ["Error: ", error]
337
+ })
338
+ });
339
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
340
+ borderStyle: "single",
341
+ flexDirection: "column",
342
+ width: "100%",
343
+ children: [
344
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
345
+ paddingX: 1,
346
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
347
+ bold: true,
348
+ color: "cyan",
349
+ children: title
350
+ })
351
+ }),
352
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
353
+ flexDirection: "column",
354
+ paddingX: 1,
355
+ children: [
356
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
357
+ alignItems: "center",
358
+ gap: 1,
359
+ children: [!done ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
360
+ color: "green",
361
+ children: "Done"
362
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: update.current ?? "Preparing..." })]
363
+ }),
364
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
365
+ marginTop: 1,
366
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
367
+ renderBar(update.completed, update.total, barWidth),
368
+ " ",
369
+ update.completed,
370
+ "/",
371
+ update.total
372
+ ] })
373
+ }),
374
+ messages.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
375
+ flexDirection: "column",
376
+ marginTop: 1,
377
+ children: messages.map((message, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
378
+ dimColor: true,
379
+ children: message
380
+ }, index))
381
+ }) : null
382
+ ]
383
+ }),
384
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
385
+ borderBottom: false,
386
+ borderColor: "gray",
387
+ borderLeft: false,
388
+ borderRight: false,
389
+ borderStyle: "single",
390
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
391
+ dimColor: true,
392
+ children: " q: Quit"
393
+ })
394
+ })
395
+ ]
396
+ }) });
397
+ };
398
+ const renderAiProgressViewer = (props) => {
399
+ return new Promise((resolve) => {
400
+ const { unmount, waitUntilExit } = render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AiProgressViewer, { ...props }));
401
+ waitUntilExit().then(() => {
402
+ unmount();
403
+ resolve();
404
+ });
405
+ });
406
+ };
407
+
408
+ //#endregion
409
+ //#region src/components/big-text.tsx
410
+ const BigText = ({ text }) => render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Gradient, {
411
+ name: "passion",
412
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InkBigText, { text })
413
+ }));
414
+
415
+ //#endregion
416
+ //#region src/hooks/useRawMode.ts
417
+ /**
418
+ * Hook to manage stdin raw mode for interactive terminal components.
419
+ *
420
+ * Raw mode is required for proper keyboard input handling (like j/k navigation).
421
+ * This hook automatically enables raw mode when mounted and restores normal mode on unmount.
422
+ *
423
+ * @example
424
+ * ```tsx
425
+ * export const MyComponent: FC = () => {
426
+ * useRawMode() // Enable raw mode
427
+ *
428
+ * useInput((input, key) => {
429
+ * if (input === 'q') {
430
+ * exit()
431
+ * }
432
+ * })
433
+ *
434
+ * return <Box>...</Box>
435
+ * }
436
+ * ```
437
+ */
438
+ const useRawMode = () => {
439
+ const stdin = useApp().stdin;
440
+ useEffect(() => {
441
+ if (stdin && typeof stdin.setRawMode === "function") {
442
+ stdin.setRawMode(true);
443
+ return () => {
444
+ stdin.setRawMode(false);
445
+ };
446
+ }
447
+ }, [stdin]);
448
+ };
449
+
450
+ //#endregion
451
+ //#region src/components/branch-viewer.tsx
452
+ const TERMINAL_HEIGHT_RESERVED$1 = 8;
453
+ const MIN_VIEW_HEIGHT$1 = 10;
454
+ const PANEL_LOCAL = "local";
455
+ const PANEL_REMOTE = "remote";
456
+ const formatBranchName = (branch) => branch.trim().replace(/^origin\//, "");
457
+ const cleanBranchList = (lines) => lines.filter((line) => line.trim() && !line.includes("->")).map((line) => line.trim().replace(/^\*\s*/, "").trim());
458
+ /**
459
+ * BranchViewer Component
460
+ *
461
+ * Displays local and remote git branches in a dual-panel interactive viewer.
462
+ * Supports vim-style keyboard navigation and independent panel scrolling.
463
+ *
464
+ * @param maxCount - Optional limit on the number of branches to display
465
+ *
466
+ * @example
467
+ * ```tsx
468
+ * <BranchViewer maxCount={20} />
469
+ * ```
470
+ */
471
+ const BranchViewer = ({ maxCount }) => {
472
+ const [localBranchData, setLocalBranchData] = useState({
473
+ branches: [],
474
+ currentBranch: "",
475
+ loading: true,
476
+ error: null
477
+ });
478
+ const [remoteBranchData, setRemoteBranchData] = useState({
479
+ branches: [],
480
+ currentBranch: "",
481
+ loading: true,
482
+ error: null
483
+ });
484
+ const [focusPanel, setFocusPanel] = useState("local");
485
+ const [localScrollTop, setLocalScrollTop] = useState(0);
486
+ const [remoteScrollTop, setRemoteScrollTop] = useState(0);
487
+ const { exit } = useApp();
488
+ useRawMode();
489
+ const terminalHeight = process.stdout.rows || 24;
490
+ const viewHeight = Math.max(MIN_VIEW_HEIGHT$1, terminalHeight - TERMINAL_HEIGHT_RESERVED$1);
491
+ useEffect(() => {
492
+ const fetchLocalBranches = async () => {
493
+ try {
494
+ const args = ["branch", "--sort=-committerdate"];
495
+ if (maxCount) args.push(`-${maxCount}`);
496
+ const [error, result] = await xASync("git", args, { quiet: true });
497
+ if (error) {
498
+ setLocalBranchData({
499
+ branches: [],
500
+ currentBranch: "",
501
+ loading: false,
502
+ error: `Failed to fetch local branches: ${error.message || "Unknown error"}`
503
+ });
504
+ return;
505
+ }
506
+ const lines = result.stdout.split("\n");
507
+ const currentBranch = (lines.find((line) => line.includes("*")) || "*").replace(/^\*\s*/, "").trim();
508
+ setLocalBranchData({
509
+ branches: cleanBranchList(lines),
510
+ currentBranch,
511
+ loading: false,
512
+ error: null
513
+ });
514
+ } catch (err) {
515
+ setLocalBranchData({
516
+ branches: [],
517
+ currentBranch: "",
518
+ loading: false,
519
+ error: err instanceof Error ? err.message : "Unknown error"
520
+ });
521
+ }
522
+ };
523
+ fetchLocalBranches();
524
+ }, [maxCount]);
525
+ useEffect(() => {
526
+ const fetchRemoteBranches = async () => {
527
+ try {
528
+ const args = [
529
+ "branch",
530
+ "-r",
531
+ "--sort=-committerdate"
532
+ ];
533
+ if (maxCount) args.push(`-${maxCount}`);
534
+ const [error, result] = await xASync("git", args, { quiet: true });
535
+ if (error) {
536
+ setRemoteBranchData({
537
+ branches: [],
538
+ currentBranch: "",
539
+ loading: false,
540
+ error: `Failed to fetch remote branches: ${error.message || "Unknown error"}`
541
+ });
542
+ return;
543
+ }
544
+ setRemoteBranchData({
545
+ branches: result.stdout.split("\n").filter((line) => line.trim() && !line.includes("->")).map(formatBranchName),
546
+ currentBranch: "",
547
+ loading: false,
548
+ error: null
549
+ });
550
+ } catch (err) {
551
+ setRemoteBranchData({
552
+ branches: [],
553
+ currentBranch: "",
554
+ loading: false,
555
+ error: err instanceof Error ? err.message : "Unknown error"
556
+ });
557
+ }
558
+ };
559
+ fetchRemoteBranches();
560
+ }, [maxCount]);
561
+ useInput((input, key) => {
562
+ if (key.return || input === "q") {
563
+ exit();
564
+ return;
565
+ }
566
+ if (key.leftArrow || input === "h") {
567
+ if (focusPanel === "remote") setFocusPanel("local");
568
+ return;
569
+ }
570
+ if (key.rightArrow || input === "l") {
571
+ if (focusPanel === "local") setFocusPanel("remote");
572
+ return;
573
+ }
574
+ if (focusPanel === "local") {
575
+ const maxScroll = Math.max(0, localBranchData.branches.length - viewHeight);
576
+ if (key.upArrow || input === "k") setLocalScrollTop((prev) => Math.max(0, prev - 1));
577
+ else if (key.downArrow || input === "j") setLocalScrollTop((prev) => Math.min(maxScroll, prev + 1));
578
+ else if (key.pageUp) setLocalScrollTop((prev) => Math.max(0, prev - viewHeight));
579
+ else if (key.pageDown) setLocalScrollTop((prev) => Math.min(maxScroll, prev + viewHeight));
580
+ } else {
581
+ const maxScroll = Math.max(0, remoteBranchData.branches.length - viewHeight);
582
+ if (key.upArrow || input === "k") setRemoteScrollTop((prev) => Math.max(0, prev - 1));
583
+ else if (key.downArrow || input === "j") setRemoteScrollTop((prev) => Math.min(maxScroll, prev + 1));
584
+ else if (key.pageUp) setRemoteScrollTop((prev) => Math.max(0, prev - viewHeight));
585
+ else if (key.pageDown) setRemoteScrollTop((prev) => Math.min(maxScroll, prev + viewHeight));
586
+ }
587
+ });
588
+ const renderBranchList = (data, scrollTop, panelType) => {
589
+ const title = panelType === PANEL_LOCAL ? "Local" : "Remote";
590
+ if (data.loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
591
+ paddingX: 1,
592
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
593
+ dimColor: true,
594
+ children: "Loading..."
595
+ })
596
+ });
597
+ if (data.error) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
598
+ paddingX: 1,
599
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
600
+ color: "red",
601
+ children: ["Error: ", data.error]
602
+ })
603
+ });
604
+ if (data.branches.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
605
+ paddingX: 1,
606
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
607
+ color: "yellow",
608
+ children: "No branches found"
609
+ })
610
+ });
611
+ const visibleBranches = data.branches.slice(scrollTop, scrollTop + viewHeight);
612
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
613
+ flexDirection: "column",
614
+ paddingX: 1,
615
+ children: [
616
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
617
+ marginBottom: 1,
618
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
619
+ bold: true,
620
+ color: focusPanel === panelType ? "green" : "blue",
621
+ children: [
622
+ title,
623
+ " Branches (",
624
+ data.branches.length,
625
+ ")",
626
+ focusPanel === panelType && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
627
+ dimColor: true,
628
+ children: " ◀"
629
+ })
630
+ ]
631
+ })
632
+ }),
633
+ visibleBranches.map((branch, index) => {
634
+ const isCurrent = branch === data.currentBranch;
635
+ scrollTop + index;
636
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
637
+ backgroundColor: isCurrent ? "gray" : void 0,
638
+ marginBottom: index < visibleBranches.length - 1 ? 1 : 0,
639
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
640
+ color: isCurrent ? "green" : "white",
641
+ children: [isCurrent ? "* " : " ", branch]
642
+ })
643
+ }, branch);
644
+ }),
645
+ scrollTop > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
646
+ color: "gray",
647
+ dimColor: true,
648
+ children: "▲"
649
+ }),
650
+ scrollTop + viewHeight < data.branches.length && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
651
+ color: "gray",
652
+ dimColor: true,
653
+ children: "▼"
654
+ })
655
+ ]
656
+ });
657
+ };
658
+ if (localBranchData.loading || remoteBranchData.loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
659
+ paddingX: 1,
660
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
661
+ dimColor: true,
662
+ children: "Loading branch information..."
663
+ })
664
+ });
665
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
666
+ flexDirection: "column",
667
+ height: terminalHeight,
668
+ width: "100%",
669
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
670
+ flexDirection: "row",
671
+ flexGrow: 1,
672
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
673
+ borderColor: focusPanel === "local" ? "green" : "gray",
674
+ borderStyle: "single",
675
+ flexDirection: "column",
676
+ width: "50%",
677
+ children: renderBranchList(localBranchData, localScrollTop, PANEL_LOCAL)
678
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
679
+ borderColor: focusPanel === "remote" ? "green" : "gray",
680
+ borderStyle: "single",
681
+ flexDirection: "column",
682
+ width: "50%",
683
+ children: renderBranchList(remoteBranchData, remoteScrollTop, PANEL_REMOTE)
684
+ })]
685
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
686
+ borderBottom: false,
687
+ borderColor: "gray",
688
+ borderLeft: false,
689
+ borderRight: false,
690
+ borderStyle: "single",
691
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
692
+ dimColor: true,
693
+ children: [
694
+ " ",
695
+ "←→/hl: Switch Panel | ↑↓/jk: Scroll | PgUp/PgDn | q: Quit | Local: ",
696
+ localScrollTop + 1,
697
+ "-",
698
+ Math.min(localScrollTop + viewHeight, localBranchData.branches.length),
699
+ "/",
700
+ localBranchData.branches.length,
701
+ " ",
702
+ "Remote: ",
703
+ remoteScrollTop + 1,
704
+ "-",
705
+ Math.min(remoteScrollTop + viewHeight, remoteBranchData.branches.length),
706
+ "/",
707
+ remoteBranchData.branches.length
708
+ ]
709
+ })
710
+ })]
711
+ });
712
+ };
713
+ /**
714
+ * Renders the BranchViewer component using Ink's render function.
715
+ *
716
+ * @param maxCount - Optional limit on the number of branches to display
717
+ * @returns A promise that resolves when the viewer exits
718
+ *
719
+ * @example
720
+ * ```typescript
721
+ * await renderBranchViewer(20)
722
+ * ```
723
+ */
724
+ const renderBranchViewer = (maxCount) => {
725
+ const { waitUntilExit } = render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(BranchViewer, { maxCount }));
726
+ return waitUntilExit();
727
+ };
728
+
729
+ //#endregion
730
+ //#region src/components/commit-detail.tsx
731
+ const DiffViewer$2 = ({ commitHash, filePath, scrollTop, visibleLines }) => {
732
+ const [diffLines, setDiffLines] = useState([]);
733
+ const [loading, setLoading] = useState(true);
734
+ useEffect(() => {
735
+ const fetchDiff = async () => {
736
+ try {
737
+ const [error, result] = await xASync("git", [
738
+ "show",
739
+ commitHash,
740
+ "--",
741
+ filePath
742
+ ], { quiet: true });
743
+ if (error) {
744
+ setDiffLines(["Error loading diff"]);
745
+ setLoading(false);
746
+ return;
747
+ }
748
+ setDiffLines(result.stdout.split("\n"));
749
+ } catch (error) {
750
+ console.error("Failed to fetch diff:", error);
751
+ setDiffLines(["Error loading diff"]);
752
+ } finally {
753
+ setLoading(false);
754
+ }
755
+ };
756
+ fetchDiff();
757
+ }, [commitHash, filePath]);
758
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
759
+ paddingX: 1,
760
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
761
+ dimColor: true,
762
+ children: "Loading diff..."
763
+ })
764
+ });
765
+ if (diffLines.length === 0 || diffLines.length === 1 && diffLines[0] === "") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
766
+ paddingX: 1,
767
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
768
+ dimColor: true,
769
+ children: "No diff available"
770
+ })
771
+ });
772
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
773
+ flexDirection: "column",
774
+ paddingX: 1,
775
+ children: [
776
+ diffLines.slice(scrollTop, scrollTop + visibleLines).map((line, index) => {
777
+ let color = "white";
778
+ if (line.startsWith("+") && !line.startsWith("+++")) color = "green";
779
+ else if (line.startsWith("-") && !line.startsWith("---")) color = "red";
780
+ else if (line.startsWith("@@")) color = "cyan";
781
+ else if (line.startsWith("diff")) color = "yellow";
782
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
783
+ color,
784
+ children: line
785
+ }, index);
786
+ }),
787
+ scrollTop > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
788
+ color: "gray",
789
+ dimColor: true,
790
+ children: "▲"
791
+ }),
792
+ scrollTop + visibleLines < diffLines.length && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
793
+ color: "gray",
794
+ dimColor: true,
795
+ children: "▼"
796
+ })
797
+ ]
798
+ });
799
+ };
800
+ const CommitDetail = ({ commitHash, onExit }) => {
801
+ const [commitInfo, setCommitInfo] = useState(null);
802
+ const [loading, setLoading] = useState(true);
803
+ const [error, setError] = useState(null);
804
+ const [selectedIndex, setSelectedIndex] = useState(0);
805
+ const [focusPanel, setFocusPanel] = useState("files");
806
+ const [diffScrollTop, setDiffScrollTop] = useState(0);
807
+ const app = useApp();
808
+ const { exit } = app;
809
+ useRawMode();
810
+ const terminalHeight = app.stdout?.rows || 24;
811
+ const visibleLines = Math.max(10, terminalHeight - 6);
812
+ const fileVisibleCount = Math.max(5, terminalHeight - 4);
813
+ useEffect(() => {
814
+ const fetchCommitDetail = async () => {
815
+ try {
816
+ const [gitError, result] = await xASync("git", [
817
+ "show",
818
+ commitHash,
819
+ "--name-only",
820
+ "--pretty=format:%H|%an|%ad|%s"
821
+ ], { quiet: true });
822
+ if (gitError) {
823
+ setError(`Failed to fetch commit ${commitHash}`);
824
+ setLoading(false);
825
+ return;
826
+ }
827
+ const lines = result.stdout.split("\n");
828
+ const firstLine = lines[0];
829
+ if (!firstLine) {
830
+ setError("Invalid git output: missing commit info");
831
+ setLoading(false);
832
+ return;
833
+ }
834
+ const parts = firstLine.split("|");
835
+ if (parts.length < 4) {
836
+ setError("Invalid git output: malformed commit info");
837
+ setLoading(false);
838
+ return;
839
+ }
840
+ const [hash, author, date, message] = parts;
841
+ const files = lines.slice(1).filter(Boolean).map((path) => ({
842
+ path,
843
+ status: "M"
844
+ }));
845
+ setCommitInfo({
846
+ hash,
847
+ shortHash: hash.slice(0, 7),
848
+ author,
849
+ date,
850
+ message,
851
+ files
852
+ });
853
+ } catch (err) {
854
+ setError(err instanceof Error ? err.message : "Unknown error");
855
+ } finally {
856
+ setLoading(false);
857
+ }
858
+ };
859
+ fetchCommitDetail();
860
+ }, [commitHash]);
861
+ useEffect(() => {
862
+ setDiffScrollTop(0);
863
+ }, [selectedIndex]);
864
+ const getVisibleFiles = useCallback(() => {
865
+ if (!commitInfo) return [];
866
+ const start = Math.max(0, selectedIndex - Math.floor(fileVisibleCount / 2));
867
+ const end = Math.min(commitInfo.files.length, start + fileVisibleCount);
868
+ return commitInfo.files.slice(start, end);
869
+ }, [
870
+ commitInfo,
871
+ selectedIndex,
872
+ fileVisibleCount
873
+ ]);
874
+ useInput((input, key) => {
875
+ if (key.return || input === "q") {
876
+ onExit();
877
+ exit();
878
+ return;
879
+ }
880
+ if (loading || !commitInfo || commitInfo.files.length === 0) return;
881
+ if (focusPanel === "files") {
882
+ if (key.upArrow || input === "k") setSelectedIndex((prev) => Math.max(0, prev - 1));
883
+ else if (key.downArrow || input === "j") setSelectedIndex((prev) => Math.min(commitInfo.files.length - 1, prev + 1));
884
+ else if (key.rightArrow || input === "l") setFocusPanel("diff");
885
+ } else if (key.upArrow || input === "k") setDiffScrollTop((prev) => Math.max(0, prev - 1));
886
+ else if (key.downArrow || input === "j") setDiffScrollTop((prev) => prev + 1);
887
+ else if (key.leftArrow || input === "h") setFocusPanel("files");
888
+ });
889
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
890
+ paddingX: 1,
891
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
892
+ dimColor: true,
893
+ children: "Loading commit detail..."
894
+ })
895
+ });
896
+ if (error) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
897
+ paddingX: 1,
898
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
899
+ color: "red",
900
+ children: ["Error: ", error]
901
+ })
902
+ });
903
+ if (!commitInfo) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
904
+ paddingX: 1,
905
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
906
+ color: "red",
907
+ children: "Error: No commit info loaded"
908
+ })
909
+ });
910
+ if (commitInfo.files.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
911
+ flexDirection: "column",
912
+ paddingX: 1,
913
+ paddingY: 1,
914
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
915
+ color: "green",
916
+ children: "✓ No files changed in this commit"
917
+ })
918
+ });
919
+ const selectedFile = commitInfo.files[selectedIndex];
920
+ const visibleFiles = getVisibleFiles();
921
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
922
+ flexDirection: "row",
923
+ height: terminalHeight,
924
+ width: "100%",
925
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
926
+ borderColor: focusPanel === "files" ? "green" : "gray",
927
+ borderStyle: "single",
928
+ flexDirection: "column",
929
+ paddingX: 1,
930
+ width: "30%",
931
+ children: [
932
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
933
+ marginBottom: 1,
934
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
935
+ bold: true,
936
+ color: focusPanel === "files" ? "green" : "blue",
937
+ children: [
938
+ "Changed Files (",
939
+ commitInfo.files.length,
940
+ ")",
941
+ focusPanel === "files" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
942
+ dimColor: true,
943
+ children: " ◀"
944
+ })
945
+ ]
946
+ })
947
+ }),
948
+ visibleFiles.map((file, index) => {
949
+ const isSelected = commitInfo.files.indexOf(file) === selectedIndex;
950
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
951
+ backgroundColor: isSelected ? "gray" : void 0,
952
+ marginBottom: index < visibleFiles.length - 1 ? 1 : 0,
953
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
954
+ color: isSelected ? "white" : "yellow",
955
+ children: [isSelected ? "●" : " ", " "]
956
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
957
+ color: isSelected ? "white" : "white",
958
+ children: file.path
959
+ })]
960
+ }, file.path);
961
+ }),
962
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
963
+ marginTop: 1,
964
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
965
+ dimColor: true,
966
+ children: focusPanel === "files" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: "↑↓/jk Navigate | →/l Diff | Enter/q Quit" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: "↑↓/jk Scroll | ←/h Files | Enter/q Quit" })
967
+ })
968
+ })
969
+ ]
970
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
971
+ borderColor: focusPanel === "diff" ? "green" : "blue",
972
+ borderStyle: "single",
973
+ flexDirection: "column",
974
+ width: "70%",
975
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
976
+ borderBottom: true,
977
+ borderColor: focusPanel === "diff" ? "green" : "blue",
978
+ paddingX: 1,
979
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
980
+ bold: true,
981
+ color: "cyan",
982
+ children: [
983
+ commitInfo.shortHash,
984
+ " ",
985
+ commitInfo.message
986
+ ]
987
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
988
+ dimColor: true,
989
+ children: [
990
+ " ",
991
+ "(",
992
+ commitInfo.author,
993
+ ", ",
994
+ commitInfo.date,
995
+ ")",
996
+ focusPanel === "diff" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
997
+ dimColor: true,
998
+ children: " ▶"
999
+ })
1000
+ ]
1001
+ })]
1002
+ }), selectedFile && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiffViewer$2, {
1003
+ commitHash,
1004
+ filePath: selectedFile.path,
1005
+ scrollTop: diffScrollTop,
1006
+ visibleLines
1007
+ })]
1008
+ })]
1009
+ });
1010
+ };
1011
+ const renderCommitDetail = (commitHash) => {
1012
+ const { waitUntilExit } = render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommitDetail, {
1013
+ commitHash,
1014
+ onExit: () => {}
1015
+ }));
1016
+ return waitUntilExit();
1017
+ };
1018
+
1019
+ //#endregion
1020
+ //#region src/components/commit-viewer.tsx
1021
+ const CommitViewer = ({ maxCount = 20, onSelect, onExit }) => {
1022
+ const [commits, setCommits] = useState([]);
1023
+ const [loading, setLoading] = useState(true);
1024
+ const [error, setError] = useState(null);
1025
+ const [selectedIndex, setSelectedIndex] = useState(0);
1026
+ const [scrollTop, setScrollTop] = useState(0);
1027
+ const { exit } = useApp();
1028
+ useRawMode();
1029
+ const terminalHeight = process.stdout.rows || 24;
1030
+ const viewHeight = Math.max(5, terminalHeight - 4);
1031
+ useEffect(() => {
1032
+ const fetchCommits = async () => {
1033
+ try {
1034
+ const [gitError, result] = await xASync("git", [
1035
+ "log",
1036
+ `-n ${maxCount}`,
1037
+ "--pretty=format:%H|%an|%ad|%s",
1038
+ "--date=relative"
1039
+ ], { quiet: true });
1040
+ if (gitError) {
1041
+ setError(`Failed to fetch commit history: ${gitError.message || "Unknown error"}`);
1042
+ setLoading(false);
1043
+ return;
1044
+ }
1045
+ setCommits(result.stdout.split("\n").filter(Boolean).map((line) => {
1046
+ const parts = line.split("|");
1047
+ if (parts.length < 4) return {
1048
+ hash: line,
1049
+ shortHash: line.slice(0, 7),
1050
+ author: "Unknown",
1051
+ date: "",
1052
+ message: line
1053
+ };
1054
+ const [hash, author, date, message] = parts;
1055
+ return {
1056
+ hash,
1057
+ shortHash: hash.slice(0, 7),
1058
+ author,
1059
+ date,
1060
+ message
1061
+ };
1062
+ }));
1063
+ } catch (err) {
1064
+ setError(err instanceof Error ? err.message : "Unknown error");
1065
+ } finally {
1066
+ setLoading(false);
1067
+ }
1068
+ };
1069
+ fetchCommits();
1070
+ }, [maxCount]);
1071
+ useEffect(() => {
1072
+ if (commits.length === 0) return;
1073
+ Math.floor(viewHeight / 2);
1074
+ if (selectedIndex < scrollTop + 2) setScrollTop(Math.max(0, selectedIndex - 2));
1075
+ else if (selectedIndex > scrollTop + viewHeight - 3) setScrollTop(Math.min(commits.length - viewHeight, selectedIndex - viewHeight + 3));
1076
+ }, [
1077
+ selectedIndex,
1078
+ commits.length,
1079
+ viewHeight
1080
+ ]);
1081
+ const visibleCommits = useMemo(() => {
1082
+ if (commits.length === 0) return [];
1083
+ return commits.slice(scrollTop, scrollTop + viewHeight);
1084
+ }, [
1085
+ commits,
1086
+ scrollTop,
1087
+ viewHeight
1088
+ ]);
1089
+ useInput((input, key) => {
1090
+ if (input === "q") {
1091
+ onExit();
1092
+ exit();
1093
+ return;
1094
+ }
1095
+ if (loading || commits.length === 0) return;
1096
+ if (key.return) {
1097
+ if (commits[selectedIndex]) onSelect(commits[selectedIndex].hash);
1098
+ return;
1099
+ }
1100
+ if (key.upArrow || input === "k") setSelectedIndex((prev) => Math.max(0, prev - 1));
1101
+ else if (key.downArrow || input === "j") setSelectedIndex((prev) => Math.min(commits.length - 1, prev + 1));
1102
+ });
1103
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1104
+ paddingX: 1,
1105
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1106
+ dimColor: true,
1107
+ children: "Loading commit history..."
1108
+ })
1109
+ });
1110
+ if (error) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1111
+ paddingX: 1,
1112
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1113
+ color: "red",
1114
+ children: ["Error: ", error]
1115
+ })
1116
+ });
1117
+ if (commits.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1118
+ paddingX: 1,
1119
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1120
+ color: "yellow",
1121
+ children: "No commits found."
1122
+ })
1123
+ });
1124
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1125
+ borderStyle: "single",
1126
+ flexDirection: "column",
1127
+ width: "100%",
1128
+ children: [
1129
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1130
+ paddingX: 1,
1131
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1132
+ bold: true,
1133
+ color: "cyan",
1134
+ children: [
1135
+ "Recent Commits (",
1136
+ commits.length,
1137
+ ")"
1138
+ ]
1139
+ })
1140
+ }),
1141
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1142
+ flexDirection: "column",
1143
+ height: viewHeight,
1144
+ paddingX: 1,
1145
+ children: visibleCommits.map((commit) => {
1146
+ const isSelected = commits.indexOf(commit) === selectedIndex;
1147
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1148
+ backgroundColor: isSelected ? "gray" : void 0,
1149
+ marginBottom: 1,
1150
+ children: [
1151
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1152
+ color: isSelected ? "white" : "yellow",
1153
+ children: [isSelected ? "●" : " ", " "]
1154
+ }),
1155
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1156
+ bold: true,
1157
+ color: isSelected ? "white" : "cyan",
1158
+ children: commit.shortHash
1159
+ }),
1160
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: " " }),
1161
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1162
+ color: isSelected ? "white" : "green",
1163
+ children: commit.message
1164
+ }),
1165
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1166
+ dimColor: true,
1167
+ children: [
1168
+ " ",
1169
+ "(",
1170
+ commit.author,
1171
+ ", ",
1172
+ commit.date,
1173
+ ")"
1174
+ ]
1175
+ })
1176
+ ]
1177
+ }, commit.hash);
1178
+ })
1179
+ }),
1180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1181
+ borderBottom: false,
1182
+ borderColor: "gray",
1183
+ borderLeft: false,
1184
+ borderRight: false,
1185
+ borderStyle: "single",
1186
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1187
+ dimColor: true,
1188
+ children: " ↑↓/jk: Navigate | Enter: Select | q: Quit"
1189
+ })
1190
+ })
1191
+ ]
1192
+ });
1193
+ };
1194
+ const renderCommitViewer = (maxCount) => {
1195
+ return new Promise((resolve) => {
1196
+ const { unmount } = render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommitViewer, {
1197
+ maxCount,
1198
+ onExit: () => {
1199
+ unmount();
1200
+ resolve(void 0);
1201
+ },
1202
+ onSelect: (hash) => {
1203
+ unmount();
1204
+ resolve(hash);
1205
+ }
1206
+ }));
1207
+ });
1208
+ };
1209
+
1210
+ //#endregion
1211
+ //#region src/components/diff-viewer.tsx
1212
+ const TERMINAL_HEIGHT_RESERVED = 8;
1213
+ const MIN_VIEW_HEIGHT = 10;
1214
+ const PANEL_FILES = "files";
1215
+ const PANEL_DIFF = "diff";
1216
+ const PANEL_WIDTH = "50%";
1217
+ const COLOR_FOCUSED = "green";
1218
+ const COLOR_UNFOCUSED = "blue";
1219
+ const COLOR_SELECTED = "green";
1220
+ const COLOR_NORMAL = "white";
1221
+ const COLOR_ADDED = "green";
1222
+ const COLOR_REMOVED = "red";
1223
+ const COLOR_HUNK_HEADER = "cyan";
1224
+ const COLOR_BORDER_FOCUSED = "green";
1225
+ const COLOR_BORDER_UNFOCUSED = "gray";
1226
+ const COLOR_SCROLL_INDICATOR = "gray";
1227
+ /**
1228
+ * DiffViewer Component
1229
+ *
1230
+ * Displays git diff in a dual-panel interactive viewer.
1231
+ * Left panel shows changed files, right panel shows diff content for selected file.
1232
+ * Supports vim-style keyboard navigation and independent panel scrolling.
1233
+ *
1234
+ * @param targetBranch - Optional target branch to diff against (defaults to working directory)
1235
+ *
1236
+ * @example
1237
+ * ```tsx
1238
+ * <DiffViewer targetBranch="main" />
1239
+ * ```
1240
+ */
1241
+ const DiffViewer = ({ targetBranch }) => {
1242
+ const [fileData, setFileData] = useState({
1243
+ files: [],
1244
+ loading: true,
1245
+ error: null
1246
+ });
1247
+ const [selectedFileIndex, setSelectedFileIndex] = useState(0);
1248
+ const [diffContent, setDiffContent] = useState("");
1249
+ const [diffLoading, setDiffLoading] = useState(false);
1250
+ const [focusPanel, setFocusPanel] = useState("files");
1251
+ const [filesScrollTop, setFilesScrollTop] = useState(0);
1252
+ const [diffScrollTop, setDiffScrollTop] = useState(0);
1253
+ const { exit } = useApp();
1254
+ useRawMode();
1255
+ const terminalHeight = process.stdout.rows || 24;
1256
+ const viewHeight = Math.max(MIN_VIEW_HEIGHT, terminalHeight - TERMINAL_HEIGHT_RESERVED);
1257
+ useEffect(() => {
1258
+ if (fileData.files.length === 0) {
1259
+ setSelectedFileIndex(0);
1260
+ return;
1261
+ }
1262
+ const clampedIndex = Math.min(selectedFileIndex, fileData.files.length - 1);
1263
+ if (clampedIndex !== selectedFileIndex) {
1264
+ setSelectedFileIndex(clampedIndex);
1265
+ return;
224
1266
  }
225
- var React = __require("react"), REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, hasOwnProperty = Object.prototype.hasOwnProperty, isArrayImpl = Array.isArray, createTask = console.createTask ? console.createTask : function() {
226
- return null;
1267
+ const maxScroll = Math.max(0, fileData.files.length - viewHeight);
1268
+ const padding = 2;
1269
+ const minVisible = filesScrollTop + padding;
1270
+ const maxVisible = filesScrollTop + viewHeight - padding - 1;
1271
+ if (selectedFileIndex < minVisible) setFilesScrollTop((_prev) => Math.max(0, selectedFileIndex - padding));
1272
+ else if (selectedFileIndex > maxVisible) setFilesScrollTop((_prev) => Math.min(maxScroll, selectedFileIndex - viewHeight + padding + 1));
1273
+ }, [
1274
+ selectedFileIndex,
1275
+ fileData.files.length,
1276
+ viewHeight,
1277
+ filesScrollTop
1278
+ ]);
1279
+ useEffect(() => {
1280
+ let cancelled = false;
1281
+ const fetchFiles = async () => {
1282
+ try {
1283
+ const [error, result] = await xASync("git", targetBranch ? [
1284
+ "diff",
1285
+ "--name-only",
1286
+ `${targetBranch}...HEAD`
1287
+ ] : ["diff", "--name-only"], { quiet: true });
1288
+ if (cancelled) return;
1289
+ if (error) {
1290
+ setFileData({
1291
+ files: [],
1292
+ loading: false,
1293
+ error: `Failed to fetch changed files: ${error.message || "Unknown error"}`
1294
+ });
1295
+ return;
1296
+ }
1297
+ const files = result.stdout.split("\n").filter((line) => line.trim()).map((line) => line.trim());
1298
+ if (cancelled) return;
1299
+ setFileData({
1300
+ files,
1301
+ loading: false,
1302
+ error: null
1303
+ });
1304
+ } catch (err) {
1305
+ if (cancelled) return;
1306
+ setFileData({
1307
+ files: [],
1308
+ loading: false,
1309
+ error: err instanceof Error ? err.message : "Unknown error"
1310
+ });
1311
+ }
227
1312
  };
228
- React = { react_stack_bottom_frame: function(callStackForError) {
229
- return callStackForError();
230
- } };
231
- var specialPropKeyWarningShown;
232
- var didWarnAboutElementRef = {};
233
- var unknownOwnerDebugStack = React.react_stack_bottom_frame.bind(React, UnknownOwner)();
234
- var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));
235
- var didWarnAboutKeySpread = {};
236
- exports.Fragment = REACT_FRAGMENT_TYPE;
237
- exports.jsx = function(type, config, maybeKey) {
238
- var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
239
- return jsxDEVImpl(type, config, maybeKey, !1, trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
1313
+ fetchFiles();
1314
+ return () => {
1315
+ cancelled = true;
240
1316
  };
241
- exports.jsxs = function(type, config, maybeKey) {
242
- var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
243
- return jsxDEVImpl(type, config, maybeKey, !0, trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
1317
+ }, [targetBranch]);
1318
+ useEffect(() => {
1319
+ let cancelled = false;
1320
+ const fetchDiff = async () => {
1321
+ if (fileData.files.length === 0 || selectedFileIndex >= fileData.files.length) {
1322
+ setDiffContent("");
1323
+ return;
1324
+ }
1325
+ const selectedFile = fileData.files[selectedFileIndex];
1326
+ if (!selectedFile) {
1327
+ setDiffContent("");
1328
+ return;
1329
+ }
1330
+ setDiffLoading(true);
1331
+ try {
1332
+ const [error, result] = await xASync("git", targetBranch ? [
1333
+ "diff",
1334
+ `${targetBranch}...HEAD`,
1335
+ "--",
1336
+ selectedFile
1337
+ ] : [
1338
+ "diff",
1339
+ "--",
1340
+ selectedFile
1341
+ ], { quiet: true });
1342
+ if (cancelled) return;
1343
+ if (error) {
1344
+ setDiffContent(`Error loading diff for "${selectedFile}": ${error.message || "Unknown error"}`);
1345
+ return;
1346
+ }
1347
+ setDiffContent(result.stdout || "No changes in this file");
1348
+ } catch (err) {
1349
+ if (cancelled) return;
1350
+ setDiffContent(`Error loading diff for "${selectedFile}": ${err instanceof Error ? err.message : "Unknown error"}`);
1351
+ } finally {
1352
+ if (!cancelled) setDiffLoading(false);
1353
+ }
244
1354
  };
245
- })();
246
- }));
247
-
248
- //#endregion
249
- //#region ../../node_modules/.pnpm/react@19.2.4/node_modules/react/jsx-runtime.js
250
- var require_jsx_runtime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
251
- if (process.env.NODE_ENV === "production") module.exports = require_react_jsx_runtime_production();
252
- else module.exports = require_react_jsx_runtime_development();
253
- }));
254
-
255
- //#endregion
256
- //#region src/components/big-text.tsx
257
- var import_jsx_runtime = require_jsx_runtime();
258
- const BigText = ({ text }) => render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Gradient, {
259
- name: "passion",
260
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InkBigText, { text })
261
- }));
1355
+ fetchDiff();
1356
+ return () => {
1357
+ cancelled = true;
1358
+ };
1359
+ }, [
1360
+ selectedFileIndex,
1361
+ fileData.files,
1362
+ targetBranch
1363
+ ]);
1364
+ const diffLines = useMemo(() => {
1365
+ if (!diffContent) return [];
1366
+ return diffContent.split("\n");
1367
+ }, [diffContent]);
1368
+ Math.max(0, fileData.files.length - viewHeight);
1369
+ const maxDiffScroll = Math.max(0, diffLines.length - viewHeight);
1370
+ useInput((input, key) => {
1371
+ if (key.return || input === "q") {
1372
+ exit();
1373
+ return;
1374
+ }
1375
+ if (fileData.loading || fileData.files.length === 0) return;
1376
+ if (key.leftArrow || input === "h") {
1377
+ if (focusPanel === "diff") setFocusPanel("files");
1378
+ return;
1379
+ }
1380
+ if (key.rightArrow || input === "l") {
1381
+ if (focusPanel === "files" && fileData.files.length > 0) setFocusPanel("diff");
1382
+ return;
1383
+ }
1384
+ if (focusPanel === "files") {
1385
+ if (key.upArrow || input === "k") setSelectedFileIndex((prev) => Math.max(0, prev - 1));
1386
+ else if (key.downArrow || input === "j") setSelectedFileIndex((prev) => Math.min(fileData.files.length - 1, prev + 1));
1387
+ else if (key.pageUp) setSelectedFileIndex((prev) => Math.max(0, prev - viewHeight));
1388
+ else if (key.pageDown) setSelectedFileIndex((prev) => Math.min(fileData.files.length - 1, prev + viewHeight));
1389
+ } else if (key.upArrow || input === "k") setDiffScrollTop((prev) => Math.max(0, prev - 1));
1390
+ else if (key.downArrow || input === "j") setDiffScrollTop((prev) => Math.min(maxDiffScroll, prev + 1));
1391
+ else if (key.pageUp) setDiffScrollTop((prev) => Math.max(0, prev - viewHeight));
1392
+ else if (key.pageDown) setDiffScrollTop((prev) => Math.min(maxDiffScroll, prev + viewHeight));
1393
+ });
1394
+ if (fileData.loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1395
+ paddingX: 1,
1396
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1397
+ dimColor: true,
1398
+ children: "Loading changed files..."
1399
+ })
1400
+ });
1401
+ if (fileData.error) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1402
+ paddingX: 1,
1403
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1404
+ color: "red",
1405
+ children: ["Error: ", fileData.error]
1406
+ })
1407
+ });
1408
+ if (fileData.files.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1409
+ paddingX: 1,
1410
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1411
+ color: "yellow",
1412
+ children: "No changed files found."
1413
+ })
1414
+ });
1415
+ const renderFileList = () => {
1416
+ const visibleFiles = fileData.files.slice(filesScrollTop, filesScrollTop + viewHeight);
1417
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1418
+ flexDirection: "column",
1419
+ paddingX: 1,
1420
+ children: [
1421
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1422
+ marginBottom: 1,
1423
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1424
+ bold: true,
1425
+ color: focusPanel === PANEL_FILES ? COLOR_FOCUSED : COLOR_UNFOCUSED,
1426
+ children: [
1427
+ "Changed Files (",
1428
+ fileData.files.length,
1429
+ ")",
1430
+ focusPanel === PANEL_FILES && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1431
+ dimColor: true,
1432
+ children: " ◀"
1433
+ })
1434
+ ]
1435
+ })
1436
+ }),
1437
+ visibleFiles.map((file, index) => {
1438
+ const isSelected = filesScrollTop + index === selectedFileIndex;
1439
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1440
+ backgroundColor: isSelected ? "gray" : void 0,
1441
+ marginBottom: index < visibleFiles.length - 1 ? 1 : 0,
1442
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1443
+ color: isSelected ? COLOR_SELECTED : COLOR_NORMAL,
1444
+ children: isSelected ? "> " : " "
1445
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1446
+ color: isSelected ? COLOR_SELECTED : COLOR_NORMAL,
1447
+ children: file
1448
+ })]
1449
+ }, file);
1450
+ }),
1451
+ filesScrollTop > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1452
+ color: COLOR_SCROLL_INDICATOR,
1453
+ dimColor: true,
1454
+ children: "▲"
1455
+ }),
1456
+ filesScrollTop + viewHeight < fileData.files.length && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1457
+ color: COLOR_SCROLL_INDICATOR,
1458
+ dimColor: true,
1459
+ children: "▼"
1460
+ })
1461
+ ]
1462
+ });
1463
+ };
1464
+ const renderDiffContent = () => {
1465
+ if (diffLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1466
+ paddingX: 1,
1467
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1468
+ dimColor: true,
1469
+ children: "Loading diff..."
1470
+ })
1471
+ });
1472
+ if (!diffContent) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1473
+ paddingX: 1,
1474
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1475
+ dimColor: true,
1476
+ children: "Select a file to view diff"
1477
+ })
1478
+ });
1479
+ const visibleLines = diffLines.slice(diffScrollTop, diffScrollTop + viewHeight);
1480
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1481
+ flexDirection: "column",
1482
+ paddingX: 1,
1483
+ children: [
1484
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1485
+ marginBottom: 1,
1486
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1487
+ bold: true,
1488
+ color: focusPanel === PANEL_DIFF ? COLOR_FOCUSED : COLOR_UNFOCUSED,
1489
+ children: ["Diff Content", focusPanel === PANEL_DIFF && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1490
+ dimColor: true,
1491
+ children: " ◀"
1492
+ })]
1493
+ })
1494
+ }),
1495
+ visibleLines.map((line, index) => {
1496
+ let lineColor = COLOR_NORMAL;
1497
+ if (line.startsWith("+") && !line.startsWith("+++")) lineColor = COLOR_ADDED;
1498
+ else if (line.startsWith("-") && !line.startsWith("---")) lineColor = COLOR_REMOVED;
1499
+ else if (line.startsWith("@@")) lineColor = COLOR_HUNK_HEADER;
1500
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1501
+ color: lineColor,
1502
+ children: line || " "
1503
+ }, index);
1504
+ }),
1505
+ diffScrollTop > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1506
+ color: COLOR_SCROLL_INDICATOR,
1507
+ dimColor: true,
1508
+ children: "▲"
1509
+ }),
1510
+ diffScrollTop + viewHeight < diffLines.length && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1511
+ color: COLOR_SCROLL_INDICATOR,
1512
+ dimColor: true,
1513
+ children: "▼"
1514
+ })
1515
+ ]
1516
+ });
1517
+ };
1518
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1519
+ flexDirection: "column",
1520
+ height: terminalHeight,
1521
+ width: "100%",
1522
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1523
+ flexDirection: "row",
1524
+ flexGrow: 1,
1525
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1526
+ borderColor: focusPanel === "files" ? COLOR_BORDER_FOCUSED : COLOR_BORDER_UNFOCUSED,
1527
+ borderStyle: "single",
1528
+ flexDirection: "column",
1529
+ width: PANEL_WIDTH,
1530
+ children: renderFileList()
1531
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1532
+ borderColor: focusPanel === "diff" ? COLOR_BORDER_FOCUSED : COLOR_BORDER_UNFOCUSED,
1533
+ borderStyle: "single",
1534
+ flexDirection: "column",
1535
+ width: PANEL_WIDTH,
1536
+ children: renderDiffContent()
1537
+ })]
1538
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1539
+ borderBottom: false,
1540
+ borderColor: COLOR_BORDER_UNFOCUSED,
1541
+ borderLeft: false,
1542
+ borderRight: false,
1543
+ borderStyle: "single",
1544
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1545
+ dimColor: true,
1546
+ children: [
1547
+ " ",
1548
+ "←→/hl: Switch Panel | ↑↓/jk: Scroll | PgUp/PgDn | q: Quit | Files ",
1549
+ filesScrollTop + 1,
1550
+ "-",
1551
+ Math.min(filesScrollTop + viewHeight, fileData.files.length),
1552
+ "/",
1553
+ fileData.files.length,
1554
+ " Diff",
1555
+ " ",
1556
+ diffScrollTop + 1,
1557
+ "-",
1558
+ Math.min(diffScrollTop + viewHeight, diffLines.length),
1559
+ "/",
1560
+ diffLines.length
1561
+ ]
1562
+ })
1563
+ })]
1564
+ });
1565
+ };
1566
+ /**
1567
+ * Renders the DiffViewer component using Ink's render function.
1568
+ *
1569
+ * @param targetBranch - Optional target branch to diff against
1570
+ * @returns A promise that resolves when the viewer exits
1571
+ *
1572
+ * @example
1573
+ * ```typescript
1574
+ * await renderDiffViewer('main')
1575
+ * ```
1576
+ */
1577
+ const renderDiffViewer = (targetBranch) => {
1578
+ const { waitUntilExit } = render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiffViewer, { targetBranch }));
1579
+ return waitUntilExit();
1580
+ };
262
1581
 
263
1582
  //#endregion
264
1583
  //#region src/components/hist-viewer.tsx
@@ -269,6 +1588,7 @@ const HistViewer = ({ maxCount }) => {
269
1588
  const [scrollTop, setScrollTop] = useState(0);
270
1589
  const [lastKey, setLastKey] = useState("");
271
1590
  const { exit } = useApp();
1591
+ useRawMode();
272
1592
  const terminalHeight = process.stdout.rows || 24;
273
1593
  const viewHeight = Math.max(10, terminalHeight - 8);
274
1594
  useEffect(() => {
@@ -457,22 +1777,6 @@ const ErrorMessage = ({ text, colors, ...props }) => {
457
1777
  }));
458
1778
  };
459
1779
 
460
- //#endregion
461
- //#region src/components/provider/index.tsx
462
- const customTheme = extendTheme(defaultTheme, { components: {
463
- ProgressBar: { styles: {
464
- completed: () => ({ color: "green" }),
465
- remaining: () => ({ backgroundColor: "#fff" })
466
- } },
467
- Spinner: { styles: { frame: () => ({ color: "#fff" }) } }
468
- } });
469
- const Provider = ({ children }) => {
470
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThemeProvider, {
471
- theme: customTheme,
472
- children
473
- });
474
- };
475
-
476
1780
  //#endregion
477
1781
  //#region src/components/process-message.tsx
478
1782
  const ProcessMessageUI = ({ command, commandArgs, onSuccess, onError }) => {
@@ -544,6 +1848,146 @@ const ProcessMessage = ({ command, commandArgs, onSuccess, onError }) => {
544
1848
  }));
545
1849
  };
546
1850
 
1851
+ //#endregion
1852
+ //#region src/components/route-viewer.tsx
1853
+ const RouteViewer = ({ routes, onSelect, onExit }) => {
1854
+ const [selectedIndex, setSelectedIndex] = useState(0);
1855
+ const [selectedRoutes, setSelectedRoutes] = useState(/* @__PURE__ */ new Set());
1856
+ const [scrollTop, setScrollTop] = useState(0);
1857
+ const { exit } = useApp();
1858
+ useRawMode();
1859
+ const terminalHeight = process.stdout.rows || 24;
1860
+ const viewHeight = Math.max(5, terminalHeight - 4);
1861
+ useEffect(() => {
1862
+ if (routes.length === 0) return;
1863
+ if (selectedIndex < scrollTop + 2) setScrollTop(Math.max(0, selectedIndex - 2));
1864
+ else if (selectedIndex > scrollTop + viewHeight - 3) setScrollTop(Math.min(routes.length - viewHeight, selectedIndex - viewHeight + 3));
1865
+ }, [
1866
+ selectedIndex,
1867
+ routes.length,
1868
+ viewHeight,
1869
+ scrollTop
1870
+ ]);
1871
+ const visibleRoutes = useMemo(() => {
1872
+ if (routes.length === 0) return [];
1873
+ return routes.slice(scrollTop, scrollTop + viewHeight);
1874
+ }, [
1875
+ routes,
1876
+ scrollTop,
1877
+ viewHeight
1878
+ ]);
1879
+ useInput((input, key) => {
1880
+ if (input === "q") {
1881
+ onExit();
1882
+ exit();
1883
+ return;
1884
+ }
1885
+ if (routes.length === 0) return;
1886
+ if (key.return) {
1887
+ const selected = Array.from(selectedRoutes);
1888
+ if (selected.length > 0) {
1889
+ onSelect(routes.filter((route) => selected.includes(route)));
1890
+ return;
1891
+ }
1892
+ if (routes[selectedIndex]) onSelect([routes[selectedIndex]]);
1893
+ return;
1894
+ }
1895
+ if (key.upArrow || input === "k") setSelectedIndex((prev) => Math.max(0, prev - 1));
1896
+ else if (key.downArrow || input === "j") setSelectedIndex((prev) => Math.min(routes.length - 1, prev + 1));
1897
+ else if (input === " ") {
1898
+ const route = routes[selectedIndex];
1899
+ if (!route) return;
1900
+ setSelectedRoutes((prev) => {
1901
+ const next = new Set(prev);
1902
+ if (next.has(route)) next.delete(route);
1903
+ else next.add(route);
1904
+ return next;
1905
+ });
1906
+ } else if (input === "a") setSelectedRoutes((prev) => {
1907
+ if (prev.size === routes.length) return /* @__PURE__ */ new Set();
1908
+ return new Set(routes);
1909
+ });
1910
+ });
1911
+ if (routes.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1912
+ paddingX: 1,
1913
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1914
+ color: "yellow",
1915
+ children: "No routes found."
1916
+ })
1917
+ });
1918
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1919
+ borderStyle: "single",
1920
+ flexDirection: "column",
1921
+ width: "100%",
1922
+ children: [
1923
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1924
+ paddingX: 1,
1925
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1926
+ bold: true,
1927
+ color: "cyan",
1928
+ children: [
1929
+ "Select Page Route (",
1930
+ routes.length,
1931
+ ")"
1932
+ ]
1933
+ })
1934
+ }),
1935
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1936
+ flexDirection: "column",
1937
+ height: viewHeight,
1938
+ paddingX: 1,
1939
+ children: visibleRoutes.map((route, visibleIndex) => {
1940
+ const isSelected = scrollTop + visibleIndex === selectedIndex;
1941
+ const isChecked = selectedRoutes.has(route);
1942
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1943
+ backgroundColor: isSelected ? "gray" : void 0,
1944
+ children: [
1945
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1946
+ color: isSelected ? "white" : "yellow",
1947
+ children: [isSelected ? ">" : " ", " "]
1948
+ }),
1949
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1950
+ color: isChecked ? "green" : "gray",
1951
+ children: [isChecked ? "[x]" : "[ ]", " "]
1952
+ }),
1953
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1954
+ color: isSelected ? "white" : "green",
1955
+ children: route
1956
+ })
1957
+ ]
1958
+ }, route);
1959
+ })
1960
+ }),
1961
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1962
+ borderBottom: false,
1963
+ borderColor: "gray",
1964
+ borderLeft: false,
1965
+ borderRight: false,
1966
+ borderStyle: "single",
1967
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1968
+ dimColor: true,
1969
+ children: " ↑↓/jk: Navigate | Space: Toggle | a: All | Enter: Confirm | q: Quit"
1970
+ })
1971
+ })
1972
+ ]
1973
+ });
1974
+ };
1975
+ const renderRouteViewer = (routes) => {
1976
+ return new Promise((resolve) => {
1977
+ const { unmount } = render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouteViewer, {
1978
+ onExit: () => {
1979
+ unmount();
1980
+ resolve(void 0);
1981
+ },
1982
+ onSelect: (route) => {
1983
+ unmount();
1984
+ resolve(route);
1985
+ },
1986
+ routes
1987
+ }));
1988
+ });
1989
+ };
1990
+
547
1991
  //#endregion
548
1992
  //#region src/components/stash-list.tsx
549
1993
  const StashCard = ({ stash, index }) => {
@@ -646,7 +2090,7 @@ const renderStashList = (stashes) => {
646
2090
 
647
2091
  //#endregion
648
2092
  //#region src/components/status-viewer.tsx
649
- const DiffViewer = ({ filePath, scrollTop, visibleLines }) => {
2093
+ const DiffViewer$1 = ({ filePath, scrollTop, visibleLines }) => {
650
2094
  const [diffLines, setDiffLines] = useState([]);
651
2095
  const [loading, setLoading] = useState(true);
652
2096
  useEffect(() => {
@@ -716,15 +2160,7 @@ const StatusViewer = ({ files, onExit }) => {
716
2160
  const [diffScrollTop, setDiffScrollTop] = useState(0);
717
2161
  const app = useApp();
718
2162
  const { exit } = app;
719
- const stdin = app.stdin;
720
- useEffect(() => {
721
- if (stdin && typeof stdin.setRawMode === "function") {
722
- stdin.setRawMode(true);
723
- return () => {
724
- stdin.setRawMode(false);
725
- };
726
- }
727
- }, [stdin]);
2163
+ useRawMode();
728
2164
  const terminalHeight = app.stdout?.rows || 24;
729
2165
  const visibleLines = Math.max(10, terminalHeight - 6);
730
2166
  const fileVisibleCount = Math.max(5, terminalHeight - 4);
@@ -847,7 +2283,7 @@ const StatusViewer = ({ files, onExit }) => {
847
2283
  })
848
2284
  ]
849
2285
  })]
850
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiffViewer, {
2286
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiffViewer$1, {
851
2287
  filePath: selectedFile.path,
852
2288
  scrollTop: diffScrollTop,
853
2289
  visibleLines
@@ -867,5 +2303,5 @@ const renderStatusViewer = (files) => {
867
2303
  };
868
2304
 
869
2305
  //#endregion
870
- export { BigText, ErrorMessage, HistViewer, Message, ProcessMessage, StashList, StatusViewer, renderHistViewer, renderList, renderStashList, renderStatusViewer };
2306
+ export { AiProgressViewer, BigText, BranchViewer, CommitDetail, CommitViewer, DiffViewer, ErrorMessage, HistViewer, Message, ProcessMessage, RouteViewer, StashList, StatusViewer, renderAiProgressViewer, renderBranchViewer, renderCommitDetail, renderCommitViewer, renderDiffViewer, renderHistViewer, renderList, renderRouteViewer, renderStashList, renderStatusViewer };
871
2307
  //# sourceMappingURL=index.js.map