@nemo-cli/ui 0.1.3 → 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, 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);
@@ -216,49 +216,1368 @@ var require_react_jsx_runtime_development = /* @__PURE__ */ __commonJSMin(((expo
216
216
  children && defineKeyPropWarningGetter(maybeKey, "function" === typeof type ? type.displayName || type.name || "Unknown" : type);
217
217
  return ReactElement(type, children, maybeKey, getOwner(), debugStack, debugTask);
218
218
  }
219
- function validateChildKeys(node) {
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));
219
+ function validateChildKeys(node) {
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
+ }
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;
1266
+ }
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
+ }
1312
+ };
1313
+ fetchFiles();
1314
+ return () => {
1315
+ cancelled = true;
1316
+ };
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
+ }
1354
+ };
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;
221
1379
  }
222
- function isValidElement(object) {
223
- return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE;
1380
+ if (key.rightArrow || input === "l") {
1381
+ if (focusPanel === "files" && fileData.files.length > 0) setFocusPanel("diff");
1382
+ return;
224
1383
  }
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/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
- }));
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
@@ -266,6 +1585,12 @@ const HistViewer = ({ maxCount }) => {
266
1585
  const [output, setOutput] = useState("");
267
1586
  const [loading, setLoading] = useState(true);
268
1587
  const [error, setError] = useState(null);
1588
+ const [scrollTop, setScrollTop] = useState(0);
1589
+ const [lastKey, setLastKey] = useState("");
1590
+ const { exit } = useApp();
1591
+ useRawMode();
1592
+ const terminalHeight = process.stdout.rows || 24;
1593
+ const viewHeight = Math.max(10, terminalHeight - 8);
269
1594
  useEffect(() => {
270
1595
  const fetchHistory = async () => {
271
1596
  try {
@@ -274,20 +1599,18 @@ const HistViewer = ({ maxCount }) => {
274
1599
  "--graph",
275
1600
  "--abbrev-commit",
276
1601
  "--date=format:%Y-%m-%d %H:%M:%S",
277
- "--format=%C(bold cyan)%h%Creset - %Cgreen%ad%Creset %C(magenta)[%an]%Creset%n%s %C(yellow)%d",
1602
+ "--format=%C(bold cyan)%h%Creset %Cgreen%ad%Creset %C(magenta)[%an]%Creset%C(yellow)%d%Creset%n %s",
278
1603
  "--color=always"
279
1604
  ];
280
1605
  if (maxCount) args.splice(1, 0, `-${maxCount}`);
281
1606
  const [gitError, result] = await xASync("git", args, { quiet: true });
282
1607
  if (gitError) {
283
- console.error("Git error:", gitError);
284
- setError("Failed to fetch git history");
1608
+ setError(`Failed to fetch git history: ${gitError.message || "Unknown error"}`);
285
1609
  setLoading(false);
286
1610
  return;
287
1611
  }
288
1612
  setOutput(result.stdout);
289
1613
  } catch (err) {
290
- console.error("Error:", err);
291
1614
  setError(err instanceof Error ? err.message : "Unknown error");
292
1615
  } finally {
293
1616
  setLoading(false);
@@ -295,21 +1618,91 @@ const HistViewer = ({ maxCount }) => {
295
1618
  };
296
1619
  fetchHistory();
297
1620
  }, [maxCount]);
298
- if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
299
- dimColor: true,
300
- children: "Loading git history..."
301
- }) });
302
- if (error) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
303
- color: "red",
304
- children: ["Error: ", error]
305
- }) });
306
- if (!output.trim()) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
307
- color: "yellow",
308
- children: "No git history found."
309
- }) });
310
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1621
+ const lines = useMemo(() => {
1622
+ if (!output) return [];
1623
+ return output.split("\n");
1624
+ }, [output]);
1625
+ const maxScroll = Math.max(0, lines.length - viewHeight);
1626
+ useEffect(() => {
1627
+ if (lastKey) {
1628
+ const timer = setTimeout(() => setLastKey(""), 500);
1629
+ return () => clearTimeout(timer);
1630
+ }
1631
+ }, [lastKey]);
1632
+ useInput((input, key) => {
1633
+ if (key.return || input === "q") {
1634
+ exit();
1635
+ return;
1636
+ }
1637
+ if (loading || lines.length === 0) return;
1638
+ if (input === "g") {
1639
+ if (lastKey === "g") {
1640
+ setScrollTop(0);
1641
+ setLastKey("");
1642
+ return;
1643
+ }
1644
+ setLastKey("g");
1645
+ return;
1646
+ }
1647
+ if (lastKey) setLastKey("");
1648
+ if (key.upArrow || input === "k") setScrollTop((prev) => Math.max(0, prev - 1));
1649
+ else if (key.downArrow || input === "j") setScrollTop((prev) => Math.min(maxScroll, prev + 1));
1650
+ else if (input === "G") setScrollTop(maxScroll);
1651
+ else if (key.pageUp) setScrollTop((prev) => Math.max(0, prev - viewHeight));
1652
+ else if (key.pageDown) setScrollTop((prev) => Math.min(maxScroll, prev + viewHeight));
1653
+ });
1654
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1655
+ paddingX: 1,
1656
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1657
+ dimColor: true,
1658
+ children: "Loading git history..."
1659
+ })
1660
+ });
1661
+ if (error) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1662
+ paddingX: 1,
1663
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1664
+ color: "red",
1665
+ children: ["Error: ", error]
1666
+ })
1667
+ });
1668
+ if (!output.trim()) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1669
+ paddingX: 1,
1670
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1671
+ color: "yellow",
1672
+ children: "No git history found."
1673
+ })
1674
+ });
1675
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1676
+ borderStyle: "single",
311
1677
  flexDirection: "column",
312
- children: output.split("\n").map((line, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: line }, index))
1678
+ width: "100%",
1679
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1680
+ flexDirection: "column",
1681
+ height: viewHeight,
1682
+ paddingX: 1,
1683
+ children: lines.slice(scrollTop, scrollTop + viewHeight).map((line, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1684
+ wrap: "truncate-end",
1685
+ children: line || " "
1686
+ }, index))
1687
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1688
+ borderBottom: false,
1689
+ borderColor: "gray",
1690
+ borderLeft: false,
1691
+ borderRight: false,
1692
+ borderStyle: "single",
1693
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1694
+ dimColor: true,
1695
+ children: [
1696
+ " ",
1697
+ "↑↓/jk: Scroll | gg/G: Top/Bottom | PgUp/PgDn | q: Quit | Lines ",
1698
+ scrollTop + 1,
1699
+ "-",
1700
+ Math.min(scrollTop + viewHeight, lines.length),
1701
+ "/",
1702
+ lines.length
1703
+ ]
1704
+ })
1705
+ })]
313
1706
  });
314
1707
  };
315
1708
  const renderHistViewer = (maxCount) => {
@@ -384,23 +1777,6 @@ const ErrorMessage = ({ text, colors, ...props }) => {
384
1777
  }));
385
1778
  };
386
1779
 
387
- //#endregion
388
- //#region src/components/provider/index.tsx
389
- const customTheme = extendTheme(defaultTheme, { components: {
390
- ProgressBar: { styles: {
391
- completed: () => ({ color: "green" }),
392
- remaining: () => ({ backgroundColor: "#fff" })
393
- } },
394
- Spinner: { styles: { frame: () => ({ color: "#fff" }) } }
395
- } });
396
- const Provider = ({ children }) => {
397
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThemeProvider, {
398
- theme: customTheme,
399
- children
400
- });
401
- };
402
- var provider_default = Provider;
403
-
404
1780
  //#endregion
405
1781
  //#region src/components/process-message.tsx
406
1782
  const ProcessMessageUI = ({ command, commandArgs, onSuccess, onError }) => {
@@ -430,7 +1806,7 @@ const ProcessMessageUI = ({ command, commandArgs, onSuccess, onError }) => {
430
1806
  useEffect(() => {
431
1807
  executeCommand();
432
1808
  }, []);
433
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(provider_default, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1809
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
434
1810
  flexDirection: "column",
435
1811
  children: [
436
1812
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
@@ -472,6 +1848,146 @@ const ProcessMessage = ({ command, commandArgs, onSuccess, onError }) => {
472
1848
  }));
473
1849
  };
474
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
+
475
1991
  //#endregion
476
1992
  //#region src/components/stash-list.tsx
477
1993
  const StashCard = ({ stash, index }) => {
@@ -574,7 +2090,7 @@ const renderStashList = (stashes) => {
574
2090
 
575
2091
  //#endregion
576
2092
  //#region src/components/status-viewer.tsx
577
- const DiffViewer = ({ filePath, scrollTop, visibleLines }) => {
2093
+ const DiffViewer$1 = ({ filePath, scrollTop, visibleLines }) => {
578
2094
  const [diffLines, setDiffLines] = useState([]);
579
2095
  const [loading, setLoading] = useState(true);
580
2096
  useEffect(() => {
@@ -644,15 +2160,7 @@ const StatusViewer = ({ files, onExit }) => {
644
2160
  const [diffScrollTop, setDiffScrollTop] = useState(0);
645
2161
  const app = useApp();
646
2162
  const { exit } = app;
647
- const stdin = app.stdin;
648
- useEffect(() => {
649
- if (stdin && typeof stdin.setRawMode === "function") {
650
- stdin.setRawMode(true);
651
- return () => {
652
- stdin.setRawMode(false);
653
- };
654
- }
655
- }, [stdin]);
2163
+ useRawMode();
656
2164
  const terminalHeight = app.stdout?.rows || 24;
657
2165
  const visibleLines = Math.max(10, terminalHeight - 6);
658
2166
  const fileVisibleCount = Math.max(5, terminalHeight - 4);
@@ -775,7 +2283,7 @@ const StatusViewer = ({ files, onExit }) => {
775
2283
  })
776
2284
  ]
777
2285
  })]
778
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiffViewer, {
2286
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiffViewer$1, {
779
2287
  filePath: selectedFile.path,
780
2288
  scrollTop: diffScrollTop,
781
2289
  visibleLines
@@ -795,5 +2303,5 @@ const renderStatusViewer = (files) => {
795
2303
  };
796
2304
 
797
2305
  //#endregion
798
- 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 };
799
2307
  //# sourceMappingURL=index.js.map