git-viewer 14.0.0 → 15.0.0

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.
@@ -1607,6 +1607,45 @@ async function fetchDiff(sha, signal) {
1607
1607
  let res = await fetch(`/api/diff/${sha}`, { signal });
1608
1608
  return res.json();
1609
1609
  }
1610
+ async function fetchStatus(signal) {
1611
+ let res = await fetch("/api/status", { signal });
1612
+ return res.json();
1613
+ }
1614
+ async function stageFiles(paths) {
1615
+ await fetch("/api/stage", {
1616
+ method: "POST",
1617
+ headers: { "Content-Type": "application/json" },
1618
+ body: JSON.stringify({ paths })
1619
+ });
1620
+ }
1621
+ async function unstageFiles(paths) {
1622
+ await fetch("/api/unstage", {
1623
+ method: "POST",
1624
+ headers: { "Content-Type": "application/json" },
1625
+ body: JSON.stringify({ paths })
1626
+ });
1627
+ }
1628
+ async function commitChanges(message, amend) {
1629
+ await fetch("/api/commit", {
1630
+ method: "POST",
1631
+ headers: { "Content-Type": "application/json" },
1632
+ body: JSON.stringify({ message, amend })
1633
+ });
1634
+ }
1635
+ async function fetchLastCommit(signal) {
1636
+ let res = await fetch("/api/last-commit", { signal });
1637
+ return res.json();
1638
+ }
1639
+ async function fetchWorkingDiff(path, signal) {
1640
+ let params = new URLSearchParams({ path });
1641
+ let res = await fetch(`/api/working-diff?${params}`, { signal });
1642
+ return res.json();
1643
+ }
1644
+ async function fetchStagedDiff(path, signal) {
1645
+ let params = new URLSearchParams({ path });
1646
+ let res = await fetch(`/api/staged-diff?${params}`, { signal });
1647
+ return res.json();
1648
+ }
1610
1649
  var AppStore = class extends TypedEventTarget {
1611
1650
  constructor() {
1612
1651
  super(...arguments);
@@ -1615,6 +1654,8 @@ var AppStore = class extends TypedEventTarget {
1615
1654
  __publicField(this, "search", "");
1616
1655
  __publicField(this, "selectedCommit", null);
1617
1656
  __publicField(this, "fullscreenDiff", false);
1657
+ __publicField(this, "view", "commits");
1658
+ __publicField(this, "status", null);
1618
1659
  }
1619
1660
  setRefs(refs) {
1620
1661
  this.refs = refs;
@@ -1638,6 +1679,14 @@ var AppStore = class extends TypedEventTarget {
1638
1679
  this.dispatchEvent(new Event("fullscreenDiff"));
1639
1680
  });
1640
1681
  }
1682
+ setView(view) {
1683
+ this.view = view;
1684
+ this.dispatchEvent(new Event("view"));
1685
+ }
1686
+ setStatus(status) {
1687
+ this.status = status;
1688
+ this.dispatchEvent(new Event("status"));
1689
+ }
1641
1690
  };
1642
1691
  var colors = {
1643
1692
  bg: "#ffffff",
@@ -1697,47 +1746,104 @@ function App(handle) {
1697
1746
  function Sidebar(handle) {
1698
1747
  let store = handle.context.get(App);
1699
1748
  handle.on(store, {
1700
- refs: () => handle.update()
1749
+ refs: () => handle.update(),
1750
+ status: () => handle.update(),
1751
+ view: () => handle.update()
1701
1752
  });
1702
- return () => /* @__PURE__ */ jsx(
1703
- "div",
1704
- {
1705
- css: {
1706
- borderRight: `1px solid ${colors.border}`,
1707
- display: "flex",
1708
- flexDirection: "column",
1709
- overflow: "hidden"
1710
- },
1711
- children: [
1712
- /* @__PURE__ */ jsx(
1713
- "div",
1714
- {
1715
- css: {
1716
- padding: "12px",
1717
- fontWeight: 600,
1718
- textTransform: "uppercase",
1719
- letterSpacing: "0.5px",
1720
- color: colors.textMuted,
1721
- borderBottom: `1px solid ${colors.border}`
1722
- },
1723
- children: "Git Tree Viewer"
1724
- }
1725
- ),
1726
- /* @__PURE__ */ jsx("div", { css: { flex: 1, overflow: "auto", padding: "8px 0" }, children: store.refs && /* @__PURE__ */ jsx(Fragment, { children: [
1727
- /* @__PURE__ */ jsx(RefSection, { title: "LOCAL", nodes: store.refs.local }),
1728
- Object.entries(store.refs.remotes).map(([remote, nodes]) => /* @__PURE__ */ jsx(
1729
- RefSection,
1753
+ handle.queueTask(async (signal) => {
1754
+ let status = await fetchStatus(signal);
1755
+ store.setStatus(status);
1756
+ });
1757
+ return () => {
1758
+ let totalChanges = store.status ? store.status.staged.length + store.status.unstaged.length : 0;
1759
+ let isStageView = store.view === "stage";
1760
+ return /* @__PURE__ */ jsx(
1761
+ "div",
1762
+ {
1763
+ css: {
1764
+ borderRight: `1px solid ${colors.border}`,
1765
+ display: "flex",
1766
+ flexDirection: "column",
1767
+ overflow: "hidden"
1768
+ },
1769
+ children: [
1770
+ /* @__PURE__ */ jsx(
1771
+ "div",
1730
1772
  {
1731
- title: remote.toUpperCase(),
1732
- nodes,
1733
- initialExpanded: false
1734
- },
1735
- remote
1736
- ))
1737
- ] }) })
1738
- ]
1739
- }
1740
- );
1773
+ css: {
1774
+ padding: "12px",
1775
+ fontWeight: 600,
1776
+ textTransform: "uppercase",
1777
+ letterSpacing: "0.5px",
1778
+ color: colors.textMuted,
1779
+ borderBottom: `1px solid ${colors.border}`
1780
+ },
1781
+ children: "Git Tree Viewer"
1782
+ }
1783
+ ),
1784
+ /* @__PURE__ */ jsx("div", { css: { flex: 1, overflow: "auto", padding: "8px 0" }, children: [
1785
+ /* @__PURE__ */ jsx(
1786
+ "div",
1787
+ {
1788
+ css: {
1789
+ padding: "6px 12px",
1790
+ marginBottom: "8px",
1791
+ borderRadius: "3px",
1792
+ marginRight: "8px",
1793
+ marginLeft: "8px",
1794
+ display: "flex",
1795
+ alignItems: "center",
1796
+ justifyContent: "space-between",
1797
+ background: isStageView ? colors.accentDim : "transparent",
1798
+ color: isStageView ? colors.accent : colors.text,
1799
+ fontWeight: 600,
1800
+ userSelect: "none",
1801
+ "&:hover": {
1802
+ background: isStageView ? colors.accentDim : colors.bgLighter
1803
+ }
1804
+ },
1805
+ on: {
1806
+ click: () => {
1807
+ store.setFilter("");
1808
+ store.setView("stage");
1809
+ }
1810
+ },
1811
+ children: [
1812
+ /* @__PURE__ */ jsx("span", { children: "stage" }),
1813
+ totalChanges > 0 && /* @__PURE__ */ jsx(
1814
+ "span",
1815
+ {
1816
+ css: {
1817
+ background: colors.accent,
1818
+ color: "#fff",
1819
+ fontSize: "10px",
1820
+ padding: "2px 6px",
1821
+ borderRadius: "10px",
1822
+ fontWeight: 600
1823
+ },
1824
+ children: totalChanges
1825
+ }
1826
+ )
1827
+ ]
1828
+ }
1829
+ ),
1830
+ store.refs && /* @__PURE__ */ jsx(Fragment, { children: [
1831
+ /* @__PURE__ */ jsx(RefSection, { title: "LOCAL", nodes: store.refs.local }),
1832
+ Object.entries(store.refs.remotes).map(([remote, nodes]) => /* @__PURE__ */ jsx(
1833
+ RefSection,
1834
+ {
1835
+ title: remote.toUpperCase(),
1836
+ nodes,
1837
+ initialExpanded: false
1838
+ },
1839
+ remote
1840
+ ))
1841
+ ] })
1842
+ ] })
1843
+ ]
1844
+ }
1845
+ );
1846
+ };
1741
1847
  }
1742
1848
  function RefSection(handle) {
1743
1849
  let expanded = null;
@@ -1758,7 +1864,7 @@ function RefSection(handle) {
1758
1864
  textTransform: "uppercase",
1759
1865
  letterSpacing: "0.5px",
1760
1866
  color: colors.textMuted,
1761
- cursor: "pointer",
1867
+ userSelect: "none",
1762
1868
  "&:hover": { color: colors.text }
1763
1869
  },
1764
1870
  on: {
@@ -1794,10 +1900,10 @@ function RefNodeItem(handle) {
1794
1900
  css: {
1795
1901
  padding: `3px 12px`,
1796
1902
  paddingLeft: `${paddingLeft}px`,
1797
- cursor: "pointer",
1798
1903
  color: colors.textMuted,
1799
1904
  fontSize: "12px",
1800
1905
  whiteSpace: "nowrap",
1906
+ userSelect: "none",
1801
1907
  "&:hover": { color: colors.text }
1802
1908
  },
1803
1909
  on: {
@@ -1824,18 +1930,23 @@ function RefNodeItem(handle) {
1824
1930
  css: {
1825
1931
  padding: `3px 12px`,
1826
1932
  paddingLeft: `${paddingLeft}px`,
1827
- cursor: "pointer",
1828
1933
  borderRadius: "3px",
1829
1934
  marginRight: "8px",
1830
1935
  background: isSelected ? colors.accentDim : "transparent",
1831
1936
  color: node.current ? colors.accent : colors.text,
1832
1937
  fontWeight: node.current ? 600 : 400,
1833
1938
  whiteSpace: "nowrap",
1939
+ userSelect: "none",
1834
1940
  "&:hover": {
1835
1941
  background: isSelected ? colors.accentDim : colors.bgLighter
1836
1942
  }
1837
1943
  },
1838
- on: { click: () => store.setFilter(node.fullName) },
1944
+ on: {
1945
+ click: () => {
1946
+ store.setFilter(node.fullName);
1947
+ store.setView("commits");
1948
+ }
1949
+ },
1839
1950
  children: [
1840
1951
  node.current && /* @__PURE__ */ jsx("span", { css: { fontSize: "8px", marginRight: "4px" }, children: "\u25CF" }),
1841
1952
  node.name
@@ -1844,44 +1955,63 @@ function RefNodeItem(handle) {
1844
1955
  );
1845
1956
  };
1846
1957
  }
1847
- function MainPanel() {
1848
- return () => /* @__PURE__ */ jsx(
1849
- "div",
1850
- {
1851
- css: {
1852
- flex: 1,
1853
- display: "flex",
1854
- flexDirection: "column",
1855
- overflow: "hidden"
1856
- },
1857
- children: [
1858
- /* @__PURE__ */ jsx(CommitList, {}),
1859
- /* @__PURE__ */ jsx(DiffPanel, {})
1860
- ]
1958
+ function MainPanel(handle) {
1959
+ let store = handle.context.get(App);
1960
+ handle.on(store, {
1961
+ view: () => handle.update()
1962
+ });
1963
+ return () => {
1964
+ if (store.view === "stage") {
1965
+ return /* @__PURE__ */ jsx(
1966
+ "div",
1967
+ {
1968
+ css: {
1969
+ flex: 1,
1970
+ display: "flex",
1971
+ flexDirection: "column",
1972
+ overflow: "hidden"
1973
+ },
1974
+ children: /* @__PURE__ */ jsx(StagePanel, {})
1975
+ }
1976
+ );
1861
1977
  }
1862
- );
1978
+ return /* @__PURE__ */ jsx(
1979
+ "div",
1980
+ {
1981
+ css: {
1982
+ flex: 1,
1983
+ display: "flex",
1984
+ flexDirection: "column",
1985
+ overflow: "hidden"
1986
+ },
1987
+ children: [
1988
+ /* @__PURE__ */ jsx(CommitList, {}),
1989
+ /* @__PURE__ */ jsx(DiffPanel, {})
1990
+ ]
1991
+ }
1992
+ );
1993
+ };
1863
1994
  }
1864
1995
  function CommitList(handle) {
1865
1996
  let store = handle.context.get(App);
1866
1997
  let commits = [];
1867
1998
  let loading = true;
1868
- async function loadCommits(signal) {
1869
- loading = true;
1870
- handle.update();
1999
+ async function doLoadCommits(signal) {
1871
2000
  let ref = store.filter === "all" ? "all" : store.filter === "local" ? store.refs?.currentBranch : store.filter;
1872
2001
  let result = await fetchCommits(ref, store.search, signal);
1873
2002
  commits = result.commits;
1874
2003
  loading = false;
1875
2004
  handle.update();
1876
2005
  }
2006
+ function loadCommits() {
2007
+ loading = true;
2008
+ handle.update(doLoadCommits);
2009
+ }
1877
2010
  handle.on(store, {
1878
- refs(_, signal) {
1879
- loadCommits(signal);
1880
- },
1881
- filter(_, signal) {
1882
- loadCommits(signal);
1883
- }
2011
+ refs: loadCommits,
2012
+ filter: loadCommits
1884
2013
  });
2014
+ handle.queueTask(doLoadCommits);
1885
2015
  return () => /* @__PURE__ */ jsx(
1886
2016
  "div",
1887
2017
  {
@@ -2008,7 +2138,7 @@ function FilterButton(handle) {
2008
2138
  background: isActive ? colors.accentDim : "transparent",
2009
2139
  color: isActive ? colors.accent : colors.text,
2010
2140
  fontSize: "12px",
2011
- cursor: "pointer",
2141
+ userSelect: "none",
2012
2142
  "&:hover": { borderColor: colors.accent }
2013
2143
  },
2014
2144
  on: { click: () => store.setFilter(filter) },
@@ -2035,8 +2165,8 @@ function CommitRow(handle) {
2035
2165
  "tr",
2036
2166
  {
2037
2167
  css: {
2038
- cursor: "pointer",
2039
- background: isSelected ? colors.accentDim : "transparent"
2168
+ background: isSelected ? colors.accentDim : "transparent",
2169
+ userSelect: "none"
2040
2170
  },
2041
2171
  on: { click: () => store.selectCommit(commit) },
2042
2172
  children: [
@@ -2271,7 +2401,6 @@ function DiffPanel(handle) {
2271
2401
  background: colors.bg,
2272
2402
  color: colors.text,
2273
2403
  fontSize: "12px",
2274
- cursor: "pointer",
2275
2404
  whiteSpace: "nowrap",
2276
2405
  "&:hover": {
2277
2406
  background: colors.bgLighter,
@@ -2409,7 +2538,7 @@ function FileListItem() {
2409
2538
  {
2410
2539
  css: {
2411
2540
  padding: "6px 12px",
2412
- cursor: "pointer",
2541
+ userSelect: "none",
2413
2542
  "&:hover": {
2414
2543
  background: colors.bgLighter
2415
2544
  }
@@ -2535,4 +2664,561 @@ function FileListItem() {
2535
2664
  );
2536
2665
  };
2537
2666
  }
2667
+ function StagePanel(handle) {
2668
+ let store = handle.context.get(App);
2669
+ let selectedFile = null;
2670
+ let diffHtml = null;
2671
+ let commitMessage = "";
2672
+ let savedMessage = "";
2673
+ let amend = false;
2674
+ let lastCommit = null;
2675
+ let loading = false;
2676
+ async function loadStatus(signal) {
2677
+ let status = await fetchStatus(signal);
2678
+ store.setStatus(status);
2679
+ }
2680
+ async function loadDiff(path, type, signal) {
2681
+ try {
2682
+ let result = type === "unstaged" ? await fetchWorkingDiff(path, signal) : await fetchStagedDiff(path, signal);
2683
+ if (signal.aborted) return;
2684
+ diffHtml = result.diffHtml;
2685
+ } catch {
2686
+ if (signal.aborted) return;
2687
+ diffHtml = "";
2688
+ }
2689
+ handle.update();
2690
+ }
2691
+ async function handleStage(paths) {
2692
+ loading = true;
2693
+ handle.update();
2694
+ await stageFiles(paths);
2695
+ let status = await fetchStatus();
2696
+ store.setStatus(status);
2697
+ if (selectedFile && selectedFile.type === "unstaged" && paths.includes(selectedFile.path)) {
2698
+ selectedFile = { path: selectedFile.path, type: "staged" };
2699
+ }
2700
+ loading = false;
2701
+ handle.update();
2702
+ }
2703
+ async function handleUnstage(paths) {
2704
+ loading = true;
2705
+ handle.update();
2706
+ await unstageFiles(paths);
2707
+ let status = await fetchStatus();
2708
+ store.setStatus(status);
2709
+ if (selectedFile && selectedFile.type === "staged" && paths.includes(selectedFile.path)) {
2710
+ selectedFile = { path: selectedFile.path, type: "unstaged" };
2711
+ }
2712
+ loading = false;
2713
+ handle.update();
2714
+ }
2715
+ async function handleCommit() {
2716
+ if (!commitMessage.trim()) return;
2717
+ loading = true;
2718
+ handle.update();
2719
+ await commitChanges(commitMessage, amend);
2720
+ commitMessage = "";
2721
+ amend = false;
2722
+ lastCommit = null;
2723
+ let status = await fetchStatus();
2724
+ store.setStatus(status);
2725
+ selectedFile = null;
2726
+ diffHtml = null;
2727
+ loading = false;
2728
+ handle.update();
2729
+ }
2730
+ async function toggleAmend(checked) {
2731
+ amend = checked;
2732
+ if (checked) {
2733
+ savedMessage = commitMessage;
2734
+ lastCommit = await fetchLastCommit();
2735
+ commitMessage = lastCommit.subject + (lastCommit.body ? "\n\n" + lastCommit.body : "");
2736
+ } else {
2737
+ commitMessage = savedMessage;
2738
+ lastCommit = null;
2739
+ }
2740
+ handle.update();
2741
+ }
2742
+ handle.on(store, {
2743
+ status: () => handle.update()
2744
+ });
2745
+ handle.queueTask(loadStatus);
2746
+ async function selectFile(path, type, signal) {
2747
+ if (selectedFile?.path === path && selectedFile?.type === type) {
2748
+ return;
2749
+ }
2750
+ selectedFile = { path, type };
2751
+ handle.update();
2752
+ await loadDiff(path, type, signal);
2753
+ }
2754
+ return () => {
2755
+ let staged = store.status?.staged ?? [];
2756
+ let unstaged = store.status?.unstaged ?? [];
2757
+ let displayStaged = staged;
2758
+ if (amend && lastCommit) {
2759
+ let stagedPaths = new Set(staged.map((f) => f.path));
2760
+ let amendFiles = lastCommit.files.filter((f) => !stagedPaths.has(f.path));
2761
+ displayStaged = [...staged, ...amendFiles];
2762
+ }
2763
+ return /* @__PURE__ */ jsx(
2764
+ "div",
2765
+ {
2766
+ css: {
2767
+ flex: 1,
2768
+ display: "flex",
2769
+ flexDirection: "column",
2770
+ overflow: "hidden"
2771
+ },
2772
+ children: [
2773
+ /* @__PURE__ */ jsx(
2774
+ "div",
2775
+ {
2776
+ css: {
2777
+ flex: "1 1 60%",
2778
+ display: "flex",
2779
+ flexDirection: "column",
2780
+ borderBottom: `1px solid ${colors.border}`,
2781
+ overflow: "hidden"
2782
+ },
2783
+ children: [
2784
+ /* @__PURE__ */ jsx(
2785
+ "div",
2786
+ {
2787
+ css: {
2788
+ padding: "8px 12px",
2789
+ borderBottom: `1px solid ${colors.border}`,
2790
+ background: colors.bgLight,
2791
+ fontSize: "12px",
2792
+ fontWeight: 600
2793
+ },
2794
+ children: selectedFile ? `${selectedFile.type === "unstaged" ? "Unstaged" : "Staged"} changes for ${selectedFile.path}` : "Select a file to view changes"
2795
+ }
2796
+ ),
2797
+ /* @__PURE__ */ jsx("div", { css: { flex: 1, overflow: "auto", background: colors.bg }, children: diffHtml ? /* @__PURE__ */ jsx(
2798
+ "section",
2799
+ {
2800
+ css: {
2801
+ "& .d2h-wrapper": { background: "transparent" },
2802
+ "& .d2h-file-header": {
2803
+ background: colors.bgLighter,
2804
+ borderBottom: `1px solid ${colors.border}`,
2805
+ padding: "8px 12px",
2806
+ position: "sticky",
2807
+ top: 0,
2808
+ zIndex: 1
2809
+ },
2810
+ "& .d2h-file-name": { color: colors.text },
2811
+ "& .d2h-code-line": { padding: "0 8px" },
2812
+ "& .d2h-code-line-ctn": { color: colors.text },
2813
+ "& .d2h-ins": { background: "#dafbe1" },
2814
+ "& .d2h-del": { background: "#ffebe9" },
2815
+ "& .d2h-ins .d2h-code-line-ctn": { color: colors.green },
2816
+ "& .d2h-del .d2h-code-line-ctn": { color: colors.red },
2817
+ "& .d2h-code-linenumber": {
2818
+ color: colors.textMuted,
2819
+ borderRight: `1px solid ${colors.border}`
2820
+ },
2821
+ "& .d2h-file-diff": {
2822
+ borderBottom: `1px solid ${colors.border}`
2823
+ },
2824
+ "& .d2h-diff-tbody": { position: "relative" }
2825
+ },
2826
+ innerHTML: diffHtml
2827
+ }
2828
+ ) : selectedFile ? /* @__PURE__ */ jsx(
2829
+ "div",
2830
+ {
2831
+ css: {
2832
+ padding: "20px",
2833
+ textAlign: "center",
2834
+ color: colors.textMuted
2835
+ },
2836
+ children: "Loading diff..."
2837
+ }
2838
+ ) : /* @__PURE__ */ jsx(
2839
+ "div",
2840
+ {
2841
+ css: {
2842
+ padding: "20px",
2843
+ textAlign: "center",
2844
+ color: colors.textMuted
2845
+ },
2846
+ children: "Select a file to view its diff"
2847
+ }
2848
+ ) })
2849
+ ]
2850
+ }
2851
+ ),
2852
+ /* @__PURE__ */ jsx(
2853
+ "div",
2854
+ {
2855
+ css: {
2856
+ flex: "0 0 300px",
2857
+ display: "flex",
2858
+ gap: "1px",
2859
+ background: colors.border,
2860
+ minHeight: "200px"
2861
+ },
2862
+ children: [
2863
+ /* @__PURE__ */ jsx(
2864
+ "div",
2865
+ {
2866
+ css: {
2867
+ flex: 1,
2868
+ display: "flex",
2869
+ flexDirection: "column",
2870
+ background: colors.bg,
2871
+ overflow: "hidden"
2872
+ },
2873
+ children: [
2874
+ /* @__PURE__ */ jsx(
2875
+ "div",
2876
+ {
2877
+ css: {
2878
+ padding: "8px 12px",
2879
+ borderBottom: `1px solid ${colors.border}`,
2880
+ fontSize: "11px",
2881
+ fontWeight: 600,
2882
+ textTransform: "uppercase",
2883
+ letterSpacing: "0.5px",
2884
+ color: colors.textMuted,
2885
+ display: "flex",
2886
+ justifyContent: "space-between",
2887
+ alignItems: "center"
2888
+ },
2889
+ children: /* @__PURE__ */ jsx("span", { children: [
2890
+ "Unstaged (",
2891
+ unstaged.length,
2892
+ ")"
2893
+ ] })
2894
+ }
2895
+ ),
2896
+ /* @__PURE__ */ jsx("div", { css: { flex: 1, overflow: "auto" }, children: [
2897
+ unstaged.map((file) => /* @__PURE__ */ jsx(
2898
+ StatusFileItem,
2899
+ {
2900
+ file,
2901
+ isSelected: selectedFile?.path === file.path && selectedFile?.type === "unstaged",
2902
+ onSelect: (signal) => selectFile(file.path, "unstaged", signal),
2903
+ onDoubleClick: () => handleStage([file.path])
2904
+ },
2905
+ file.path
2906
+ )),
2907
+ unstaged.length === 0 && /* @__PURE__ */ jsx(
2908
+ "div",
2909
+ {
2910
+ css: {
2911
+ padding: "12px",
2912
+ color: colors.textMuted,
2913
+ fontSize: "12px",
2914
+ textAlign: "center"
2915
+ },
2916
+ children: "No unstaged changes"
2917
+ }
2918
+ )
2919
+ ] })
2920
+ ]
2921
+ }
2922
+ ),
2923
+ /* @__PURE__ */ jsx(
2924
+ "div",
2925
+ {
2926
+ css: {
2927
+ flex: 1,
2928
+ display: "flex",
2929
+ flexDirection: "column",
2930
+ background: colors.bg,
2931
+ overflow: "hidden"
2932
+ },
2933
+ children: [
2934
+ /* @__PURE__ */ jsx(
2935
+ "div",
2936
+ {
2937
+ css: {
2938
+ padding: "8px 12px",
2939
+ borderBottom: `1px solid ${colors.border}`,
2940
+ fontSize: "11px",
2941
+ fontWeight: 600,
2942
+ textTransform: "uppercase",
2943
+ letterSpacing: "0.5px",
2944
+ color: colors.textMuted
2945
+ },
2946
+ children: "Commit Message"
2947
+ }
2948
+ ),
2949
+ /* @__PURE__ */ jsx(
2950
+ "div",
2951
+ {
2952
+ css: {
2953
+ flex: 1,
2954
+ display: "flex",
2955
+ flexDirection: "column",
2956
+ padding: "12px"
2957
+ },
2958
+ children: [
2959
+ /* @__PURE__ */ jsx(
2960
+ "textarea",
2961
+ {
2962
+ css: {
2963
+ flex: 1,
2964
+ resize: "none",
2965
+ border: `1px solid ${colors.border}`,
2966
+ borderRadius: "4px",
2967
+ padding: "8px",
2968
+ fontSize: "13px",
2969
+ fontFamily: "sf-mono, monospace",
2970
+ background: colors.bg,
2971
+ color: colors.text,
2972
+ "&:focus": {
2973
+ outline: "none",
2974
+ borderColor: colors.accent
2975
+ },
2976
+ "&::placeholder": {
2977
+ color: colors.textMuted
2978
+ }
2979
+ },
2980
+ placeholder: "Enter commit message...",
2981
+ value: commitMessage,
2982
+ on: {
2983
+ input: (e) => {
2984
+ commitMessage = e.currentTarget.value;
2985
+ handle.update();
2986
+ },
2987
+ keydown: (e) => {
2988
+ if (e.metaKey && e.key === "Enter") {
2989
+ e.preventDefault();
2990
+ handleCommit();
2991
+ }
2992
+ }
2993
+ }
2994
+ }
2995
+ ),
2996
+ /* @__PURE__ */ jsx(
2997
+ "div",
2998
+ {
2999
+ css: {
3000
+ display: "flex",
3001
+ alignItems: "center",
3002
+ justifyContent: "space-between",
3003
+ marginTop: "12px",
3004
+ gap: "12px"
3005
+ },
3006
+ children: [
3007
+ /* @__PURE__ */ jsx(
3008
+ "label",
3009
+ {
3010
+ css: {
3011
+ display: "flex",
3012
+ alignItems: "center",
3013
+ gap: "6px",
3014
+ fontSize: "12px"
3015
+ },
3016
+ children: [
3017
+ /* @__PURE__ */ jsx(
3018
+ "input",
3019
+ {
3020
+ type: "checkbox",
3021
+ checked: amend,
3022
+ on: {
3023
+ change: (e) => toggleAmend(e.currentTarget.checked)
3024
+ }
3025
+ }
3026
+ ),
3027
+ "Amend"
3028
+ ]
3029
+ }
3030
+ ),
3031
+ /* @__PURE__ */ jsx(
3032
+ "button",
3033
+ {
3034
+ css: {
3035
+ padding: "6px 16px",
3036
+ border: "none",
3037
+ borderRadius: "4px",
3038
+ background: displayStaged.length > 0 && commitMessage.trim() ? colors.accent : colors.bgLighter,
3039
+ color: displayStaged.length > 0 && commitMessage.trim() ? "#fff" : colors.textMuted,
3040
+ fontSize: "12px",
3041
+ fontWeight: 600,
3042
+ cursor: displayStaged.length > 0 && commitMessage.trim() ? "pointer" : "not-allowed",
3043
+ "&:hover": {
3044
+ background: displayStaged.length > 0 && commitMessage.trim() ? "#0860ca" : colors.bgLighter
3045
+ }
3046
+ },
3047
+ disabled: displayStaged.length === 0 || !commitMessage.trim() || loading,
3048
+ on: { click: handleCommit },
3049
+ children: loading ? "..." : "Commit"
3050
+ }
3051
+ )
3052
+ ]
3053
+ }
3054
+ )
3055
+ ]
3056
+ }
3057
+ )
3058
+ ]
3059
+ }
3060
+ ),
3061
+ /* @__PURE__ */ jsx(
3062
+ "div",
3063
+ {
3064
+ css: {
3065
+ flex: 1,
3066
+ display: "flex",
3067
+ flexDirection: "column",
3068
+ background: colors.bg,
3069
+ overflow: "hidden"
3070
+ },
3071
+ children: [
3072
+ /* @__PURE__ */ jsx(
3073
+ "div",
3074
+ {
3075
+ css: {
3076
+ padding: "8px 12px",
3077
+ borderBottom: `1px solid ${colors.border}`,
3078
+ fontSize: "11px",
3079
+ fontWeight: 600,
3080
+ textTransform: "uppercase",
3081
+ letterSpacing: "0.5px",
3082
+ color: colors.textMuted
3083
+ },
3084
+ children: [
3085
+ "Staged (",
3086
+ displayStaged.length,
3087
+ ")"
3088
+ ]
3089
+ }
3090
+ ),
3091
+ /* @__PURE__ */ jsx("div", { css: { flex: 1, overflow: "auto" }, children: [
3092
+ displayStaged.map((file) => /* @__PURE__ */ jsx(
3093
+ StatusFileItem,
3094
+ {
3095
+ file,
3096
+ isSelected: selectedFile?.path === file.path && selectedFile?.type === "staged",
3097
+ onSelect: (signal) => selectFile(file.path, "staged", signal),
3098
+ onDoubleClick: () => handleUnstage([file.path])
3099
+ },
3100
+ file.path
3101
+ )),
3102
+ displayStaged.length === 0 && /* @__PURE__ */ jsx(
3103
+ "div",
3104
+ {
3105
+ css: {
3106
+ padding: "12px",
3107
+ color: colors.textMuted,
3108
+ fontSize: "12px",
3109
+ textAlign: "center"
3110
+ },
3111
+ children: "No staged changes"
3112
+ }
3113
+ )
3114
+ ] })
3115
+ ]
3116
+ }
3117
+ )
3118
+ ]
3119
+ }
3120
+ )
3121
+ ]
3122
+ }
3123
+ );
3124
+ };
3125
+ }
3126
+ function StatusFileItem() {
3127
+ return ({
3128
+ file,
3129
+ isSelected,
3130
+ onSelect,
3131
+ onDoubleClick
3132
+ }) => {
3133
+ let displayName = file.path.split("/").pop() ?? file.path;
3134
+ let statusLabel = {
3135
+ M: "MOD",
3136
+ A: "ADD",
3137
+ D: "DEL",
3138
+ R: "REN",
3139
+ "?": "NEW"
3140
+ }[file.status] ?? file.status;
3141
+ let statusColor = {
3142
+ M: colors.accent,
3143
+ A: colors.green,
3144
+ D: colors.red,
3145
+ R: colors.accent,
3146
+ "?": colors.green
3147
+ }[file.status] ?? colors.textMuted;
3148
+ return /* @__PURE__ */ jsx(
3149
+ "div",
3150
+ {
3151
+ css: {
3152
+ padding: "6px 12px",
3153
+ background: isSelected ? colors.accentDim : "transparent",
3154
+ userSelect: "none",
3155
+ "&:hover": {
3156
+ background: isSelected ? colors.accentDim : colors.bgLighter
3157
+ }
3158
+ },
3159
+ on: {
3160
+ click: (_, signal) => onSelect(signal),
3161
+ dblclick: onDoubleClick
3162
+ },
3163
+ children: [
3164
+ /* @__PURE__ */ jsx(
3165
+ "div",
3166
+ {
3167
+ css: {
3168
+ display: "flex",
3169
+ alignItems: "center",
3170
+ gap: "8px"
3171
+ },
3172
+ children: [
3173
+ /* @__PURE__ */ jsx(
3174
+ "span",
3175
+ {
3176
+ css: {
3177
+ fontSize: "9px",
3178
+ fontWeight: 600,
3179
+ padding: "2px 4px",
3180
+ borderRadius: "3px",
3181
+ background: statusColor,
3182
+ color: "#fff"
3183
+ },
3184
+ children: statusLabel
3185
+ }
3186
+ ),
3187
+ /* @__PURE__ */ jsx(
3188
+ "span",
3189
+ {
3190
+ css: {
3191
+ flex: 1,
3192
+ overflow: "hidden",
3193
+ textOverflow: "ellipsis",
3194
+ whiteSpace: "nowrap",
3195
+ fontSize: "12px"
3196
+ },
3197
+ title: file.path,
3198
+ children: displayName
3199
+ }
3200
+ )
3201
+ ]
3202
+ }
3203
+ ),
3204
+ file.path !== displayName && /* @__PURE__ */ jsx(
3205
+ "div",
3206
+ {
3207
+ css: {
3208
+ fontSize: "10px",
3209
+ color: colors.textMuted,
3210
+ marginTop: "2px",
3211
+ marginLeft: "32px",
3212
+ overflow: "hidden",
3213
+ textOverflow: "ellipsis",
3214
+ whiteSpace: "nowrap"
3215
+ },
3216
+ children: file.path
3217
+ }
3218
+ )
3219
+ ]
3220
+ }
3221
+ );
3222
+ };
3223
+ }
2538
3224
  createRoot(document.body).render(/* @__PURE__ */ jsx(App, {}));
package/dist/server.js CHANGED
@@ -9033,6 +9033,112 @@ function formatDate(date) {
9033
9033
  minute: "2-digit"
9034
9034
  });
9035
9035
  }
9036
+ async function getStatus() {
9037
+ let output = await git("status --porcelain=v1");
9038
+ let lines = output ? output.split("\n").filter(Boolean) : [];
9039
+ let staged = [];
9040
+ let unstaged = [];
9041
+ for (let line of lines) {
9042
+ let indexStatus = line[0];
9043
+ let workTreeStatus = line[1];
9044
+ let path4 = line.slice(3);
9045
+ if (path4.includes(" -> ")) {
9046
+ path4 = path4.split(" -> ")[1];
9047
+ }
9048
+ if (indexStatus !== " " && indexStatus !== "?") {
9049
+ staged.push({ path: path4, status: indexStatus });
9050
+ }
9051
+ if (workTreeStatus !== " ") {
9052
+ let status = workTreeStatus === "?" ? "?" : workTreeStatus;
9053
+ unstaged.push({ path: path4, status });
9054
+ }
9055
+ }
9056
+ return { staged, unstaged };
9057
+ }
9058
+ async function stageFiles(paths) {
9059
+ if (paths.length === 0) return;
9060
+ let escaped = paths.map((p) => `"${p}"`).join(" ");
9061
+ await git(`add ${escaped}`);
9062
+ }
9063
+ async function unstageFiles(paths) {
9064
+ if (paths.length === 0) return;
9065
+ let escaped = paths.map((p) => `"${p}"`).join(" ");
9066
+ await git(`restore --staged ${escaped}`);
9067
+ }
9068
+ async function commitChanges(message, amend) {
9069
+ let escapedMessage = message.replace(/"/g, '\\"');
9070
+ let args = `commit -m "${escapedMessage}"`;
9071
+ if (amend) {
9072
+ args += " --amend";
9073
+ }
9074
+ await git(args);
9075
+ }
9076
+ async function getLastCommit() {
9077
+ let format = "%H%x1f%h%x1f%s%x1f%b";
9078
+ let metaOutput = await git(`log -1 --format="${format}"`);
9079
+ let [sha, shortSha, subject, body] = metaOutput.split("");
9080
+ let filesOutput = await git("diff-tree --no-commit-id --name-status -r HEAD");
9081
+ let files = [];
9082
+ if (filesOutput) {
9083
+ for (let line of filesOutput.split("\n").filter(Boolean)) {
9084
+ let [status, ...pathParts] = line.split(" ");
9085
+ let path4 = pathParts.join(" ");
9086
+ if (pathParts.length > 1) {
9087
+ path4 = pathParts[1];
9088
+ }
9089
+ files.push({ path: path4, status: status[0] });
9090
+ }
9091
+ }
9092
+ return {
9093
+ sha,
9094
+ shortSha,
9095
+ subject,
9096
+ body: body ? body.trimEnd() : "",
9097
+ files
9098
+ };
9099
+ }
9100
+ async function getWorkingDiff(filePath) {
9101
+ let output = await git(`diff -- "${filePath}"`);
9102
+ if (output) {
9103
+ return output;
9104
+ }
9105
+ try {
9106
+ let fileContent = fs2.readFileSync(path3.join(repoDir, filePath), "utf-8");
9107
+ let lines = fileContent.split("\n");
9108
+ let diffLines = [
9109
+ `diff --git a/${filePath} b/${filePath}`,
9110
+ `new file mode 100644`,
9111
+ `--- /dev/null`,
9112
+ `+++ b/${filePath}`,
9113
+ `@@ -0,0 +1,${lines.length} @@`,
9114
+ ...lines.map((line) => `+${line}`)
9115
+ ];
9116
+ return diffLines.join("\n");
9117
+ } catch {
9118
+ return "";
9119
+ }
9120
+ }
9121
+ async function getStagedDiff(filePath) {
9122
+ let output = await git(`diff --cached -- "${filePath}"`);
9123
+ if (output) {
9124
+ return output;
9125
+ }
9126
+ try {
9127
+ let fileContent = await git(`show :${filePath}`);
9128
+ let lines = fileContent.split("\n");
9129
+ let diffLines = [
9130
+ `diff --git a/${filePath} b/${filePath}`,
9131
+ `new file mode 100644`,
9132
+ `--- /dev/null`,
9133
+ `+++ b/${filePath}`,
9134
+ `@@ -0,0 +1,${lines.length} @@`,
9135
+ ...lines.map((line) => `+${line}`)
9136
+ ];
9137
+ return diffLines.join("\n");
9138
+ } catch {
9139
+ return "";
9140
+ }
9141
+ }
9036
9142
  async function getDiff(sha) {
9037
9143
  let format = "%H%x1f%h%x1f%s%x1f%b%x1f%an%x1f%ai%x1f%P";
9038
9144
  let metaOutput = await git(`show -z --format="${format}" -s ${sha}`);
@@ -9102,9 +9208,102 @@ router.get("/api/diff/:sha", async ({ params }) => {
9102
9208
  return Response.json({ error: "Failed to get diff" }, { status: 500 });
9103
9209
  }
9104
9210
  });
9211
+ router.get("/api/status", async () => {
9212
+ try {
9213
+ let status = await getStatus();
9214
+ return Response.json(status);
9215
+ } catch (error) {
9216
+ console.error("Error getting status:", error);
9217
+ return Response.json({ error: "Failed to get status" }, { status: 500 });
9218
+ }
9219
+ });
9220
+ router.post("/api/stage", async ({ request }) => {
9221
+ try {
9222
+ let { paths } = await request.json();
9223
+ await stageFiles(paths);
9224
+ return Response.json({ success: true });
9225
+ } catch (error) {
9226
+ console.error("Error staging files:", error);
9227
+ return Response.json({ error: "Failed to stage files" }, { status: 500 });
9228
+ }
9229
+ });
9230
+ router.post("/api/unstage", async ({ request }) => {
9231
+ try {
9232
+ let { paths } = await request.json();
9233
+ await unstageFiles(paths);
9234
+ return Response.json({ success: true });
9235
+ } catch (error) {
9236
+ console.error("Error unstaging files:", error);
9237
+ return Response.json({ error: "Failed to unstage files" }, { status: 500 });
9238
+ }
9239
+ });
9240
+ router.post("/api/commit", async ({ request }) => {
9241
+ try {
9242
+ let { message, amend } = await request.json();
9243
+ await commitChanges(message, amend || false);
9244
+ return Response.json({ success: true });
9245
+ } catch (error) {
9246
+ console.error("Error committing:", error);
9247
+ return Response.json({ error: "Failed to commit" }, { status: 500 });
9248
+ }
9249
+ });
9250
+ router.get("/api/last-commit", async () => {
9251
+ try {
9252
+ let lastCommit = await getLastCommit();
9253
+ return Response.json(lastCommit);
9254
+ } catch (error) {
9255
+ console.error("Error getting last commit:", error);
9256
+ return Response.json({ error: "Failed to get last commit" }, { status: 500 });
9257
+ }
9258
+ });
9259
+ router.get("/api/working-diff", async ({ url }) => {
9260
+ try {
9261
+ let filePath = url.searchParams.get("path");
9262
+ if (!filePath) {
9263
+ return Response.json({ error: "Missing path parameter" }, { status: 400 });
9264
+ }
9265
+ let diffOutput = await getWorkingDiff(filePath);
9266
+ let parsedDiff = (0, import_diff2html.parse)(diffOutput);
9267
+ let diffHtml = (0, import_diff2html.html)(parsedDiff, {
9268
+ drawFileList: false,
9269
+ outputFormat: "line-by-line",
9270
+ matching: "lines"
9271
+ });
9272
+ return Response.json({ diffHtml });
9273
+ } catch (error) {
9274
+ console.error("Error getting working diff:", error);
9275
+ return Response.json({ error: "Failed to get working diff" }, { status: 500 });
9276
+ }
9277
+ });
9278
+ router.get("/api/staged-diff", async ({ url }) => {
9279
+ try {
9280
+ let filePath = url.searchParams.get("path");
9281
+ if (!filePath) {
9282
+ return Response.json({ error: "Missing path parameter" }, { status: 400 });
9283
+ }
9284
+ let diffOutput = await getStagedDiff(filePath);
9285
+ let parsedDiff = (0, import_diff2html.parse)(diffOutput);
9286
+ let diffHtml = (0, import_diff2html.html)(parsedDiff, {
9287
+ drawFileList: false,
9288
+ outputFormat: "line-by-line",
9289
+ matching: "lines"
9290
+ });
9291
+ return Response.json({ diffHtml });
9292
+ } catch (error) {
9293
+ console.error("Error getting staged diff:", error);
9294
+ return Response.json({ error: "Failed to get staged diff" }, { status: 500 });
9295
+ }
9296
+ });
9105
9297
  var server = http.createServer(
9106
9298
  createRequestListener(async (request) => {
9107
- return await router.fetch(request);
9299
+ let response = await router.fetch(request);
9300
+ let headers = new Headers(response.headers);
9301
+ headers.set("Cache-Control", "no-store");
9302
+ return new Response(response.body, {
9303
+ status: response.status,
9304
+ statusText: response.statusText,
9305
+ headers
9306
+ });
9108
9307
  })
9109
9308
  );
9110
9309
  var startPort = 44100;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-viewer",
3
- "version": "14.0.0",
3
+ "version": "15.0.0",
4
4
  "description": "Visual git log viewer with branch graph and diff display",
5
5
  "repository": {
6
6
  "type": "git",