megit-app 0.5.0 → 0.5.2

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/CHANGELOG.md CHANGED
@@ -9,41 +9,48 @@ surface, and the HTTP API may change in any minor release.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.5.2] - 2026-08-12
13
+
14
+ ### Fixed
15
+
16
+ - Ubuntu Mono is bundled instead of fetched from Google Fonts. Every page load used to
17
+ hand your IP and User-Agent to a third party, and the UI fell back to a system font
18
+ when offline — neither matched what the README promised.
19
+
20
+ ### Changed
21
+
22
+ - README now states that avatar lookups reach `api.github.com` (with your `gh` token,
23
+ when present) as well as Gravatar. That was already true and undocumented.
24
+
25
+ ## [0.5.1] - 2026-08-10
26
+
27
+ ### Changed
28
+
29
+ - Express is gone; routing and static serving are now ~150 lines on `node:http`. A megit install
30
+ pulls 3 packages instead of 70, and the server starts in about half the time.
31
+ - The `postinstall` script is gone. node-pty's `spawn-helper` is made executable on the first
32
+ terminal you open instead, so installing megit runs no scripts of its own.
33
+
12
34
  ## [0.5.0] - 2026-08-08
13
35
 
14
36
  ### Added
15
37
 
16
- - Merge conflicts can be resolved inside megit. A merge, rebase, cherry-pick or revert that
17
- stops on a conflict now shows a banner above the graph naming the operation and the number of
18
- conflicts left, with Abort (confirmed first) and Continue (enabled once nothing is unmerged).
19
- Conflicted files get their own "Merge Changes" section in the commit panel no Stage and no
20
- Discard, because staging a file that still holds `<<<<<<<` commits the markers and opening
21
- one replaces the diff with a picker: every conflict block renders ours above theirs with Use
22
- ours / Use theirs / Use both / Reset. Deciding the last block writes the file and stages it.
23
- Files with nothing to pick — binary, delete/modify, submodule — get Keep ours / Keep theirs /
24
- Delete instead, and a file fixed by hand in the terminal can be accepted with Mark resolved.
25
- Picks are held in the client and written only on full resolution, so Reset costs nothing and
26
- the watcher stays quiet. Stash-pop conflicts get the picker but no banner: git leaves no state
27
- file to detect and has no `--continue`.
28
- - A Rendered/Source toggle on `.svg` diffs. SVG is text rendered as a picture, which hides
29
- exactly the edits worth reviewing — a changed `viewBox`, a renamed `id`, a `stroke-width`
30
- tweak below visual threshold. Source mode shows the real patch with Unified/Split; the choice
31
- is sticky across files and reloads. Raster images are deliberately excluded — their source
32
- diff is `Binary files … differ`, a control that leads nowhere.
38
+ - Merge conflict resolution. A conflicted merge, rebase, cherry-pick or revert shows a banner
39
+ with Abort and Continue; conflicted files open a per-block picker (Use ours / theirs / both /
40
+ Reset) instead of a diff, and hand-fixed files take Mark resolved. Stash-pop conflicts get the
41
+ picker but no banner git leaves no state to detect and has no `--continue`.
42
+ - Rendered/Source toggle on `.svg` diffs, so a changed `viewBox`, `id` or `stroke-width` is
43
+ reviewable as text. Sticky across files; raster images stay excluded.
33
44
 
34
45
  ### Changed
35
46
 
36
- - The add-repo dialog is now two panes: recent repositories on the left, the current
37
- directory's path header and folder list on the right. Recent entries show just the
38
- repository name with a dot when it is already open and a highlight on the active one; folder
39
- rows carry a chevron, and the path header bolds the directory you are in.
47
+ - The add-repo dialog is two panes: recent repositories on the left, directory browser on the
48
+ right.
40
49
 
41
50
  ### Fixed
42
51
 
43
- - "Stage all" and "Discard all" no longer touch unmerged paths. `git add -A` swept conflicted
44
- files markers and all into the index, and `git restore --worktree -- .` failed outright on
45
- paths with no stage-0 entry to restore from, taking the whole batch with it. Both now exclude
46
- every unmerged path by pathspec.
52
+ - "Stage all" and "Discard all" skip unmerged paths. `git add -A` staged conflict markers, and
53
+ `git restore --worktree -- .` failed on paths with no stage-0 entry, killing the whole batch.
47
54
 
48
55
  ## [0.4.1] - 2026-08-05
49
56
 
@@ -60,38 +67,53 @@ surface, and the HTTP API may change in any minor release.
60
67
 
61
68
  ### Changed
62
69
 
63
- - "Discard all" in the commit panel now discards only unstaged changes: tracked files go back to their staged content and untracked files are deleted, while anything staged survives. It previously reset both sides to HEAD, so staging a hunk you wanted to keep and discarding the rest threw away the part you had just protected. The button counts unstaged entries and does nothing when there are none.
70
+ - "Discard all" discards only unstaged changes tracked files go back to their staged content,
71
+ untracked files are deleted, staged work survives. It previously reset both sides to HEAD,
72
+ throwing away the hunk you had just staged to protect.
64
73
 
65
74
  ### Fixed
66
75
 
67
- - The search bar (<kbd>⌘F</kbd>) no longer slides sideways when the commit panel opens or closes. It was centred on `.graph-pane`, which shrinks with the panel, so a 50% anchor moved on every selection change; it is now pinned to the start of the subject column, which only moves when a splitter is dragged.
76
+ - The search bar (<kbd>⌘F</kbd>) no longer slides sideways when the commit panel opens. It was
77
+ centred on `.graph-pane`, which shrinks with the panel; it is now pinned to the subject column.
68
78
 
69
79
  ## [0.3.0] - 2026-08-01
70
80
 
71
81
  ### Added
72
82
 
73
- - Split terminal panes: <kbd>⌘D</kbd> (or the split button in the panel header) divides the terminal panel into up to four side-by-side shells, each its own PTY with its own scrollback. A pane goes away when its shell exits (`exit`, <kbd>⌃D</kbd>) or via the ✕ in its top-right corner, which kills the shell rather than orphaning it; closing the last one closes the panel. The layout is remembered per repository, so switching tabs and coming back reattaches every pane. The four-pane cap is enforced server-side, not just in the UI — the WebSocket spawns login shells, so an unbounded pane index would be an unbounded shell factory.
83
+ - Split terminal panes: <kbd>⌘D</kbd> divides the terminal into up to four side-by-side shells,
84
+ each its own PTY and scrollback, remembered per repository. The four-pane cap is enforced
85
+ server-side — the WebSocket spawns login shells, so an unbounded pane index would be an
86
+ unbounded shell factory.
74
87
 
75
88
  ### Changed
76
89
 
77
- - The published package now requires Node ≥ 22 instead of ≥ 24, so `npx megit-app` no longer prints an `EBADENGINE` warning on Node 22 or 23. Node 24 was only ever needed to run `server/*.ts` directly in development; the package ships `dist-server/` compiled to ES2022, whose real floor is `import.meta.dirname` (Node 20.11). Development and CI stay on Node 24.
90
+ - The package now requires Node ≥ 22 instead of ≥ 24, so `npx megit-app` no longer warns
91
+ EBADENGINE on Node 22 or 23. Node 24 was only needed to run `server/*.ts` in development.
78
92
 
79
93
  ### Fixed
80
94
 
81
- - A stash newer than every commit no longer sinks to its base commit's row. Stash placement snaps a stash down to its base when the first lane clear of solid graph lines sits more than one lane past the insertion row's own traffic — a guard against a stash square floating alone past the right edge. On a busy history that fired far too eagerly: a stash taken minutes ago landed eleven rows down, next to a day-old base. The guard now also keeps the chronological row whenever the clear lane fits inside the width the graph already draws, which is the width the original check was really about.
95
+ - A stash newer than every commit no longer sinks to its base commit's row. The guard that
96
+ snaps a stash down to its base fired on any busy history — it now keeps the chronological row
97
+ whenever the clear lane fits inside the width the graph already draws.
82
98
 
83
99
  ## [0.2.0] - 2026-07-30
84
100
 
85
101
  ### Added
86
102
 
87
- - Merge and Rebase in the right-click menu of a remote branch chip, not only a local one. When a local branch has diverged from its upstream — a local commit here, a new commit on the remote — `origin/x` is drawn as its own chip, and that chip is exactly what you want to merge in or rebase onto. The actions address the branch by its full remote-tracking ref, so a diverged `origin/main` can never resolve to the local `main`.
88
- - A build badge in the top-right of the tab bar: `[DEV]` when running the Vite dev server, the package version (`v0.2.0`) in a production build. Baked in at build time, so it costs no request and the unused branch is dropped from the production bundle.
103
+ - Merge and Rebase in the right-click menu of a remote branch chip, not only a local one. The
104
+ actions address the branch by its full remote-tracking ref, so a diverged origin/main can
105
+ never resolve to the local main.
106
+ - A build badge in the tab bar: [DEV] under the Vite dev server, the package version in a
107
+ production build. Baked in at build time, so it costs no request.
89
108
 
90
109
  ### Fixed
91
110
 
92
- - Refresh (⟳ button and <kbd>r</kbd>) now fetches from the remote before re-reading the repository. It previously only read local git, so commits pushed by someone else — and the Pull/Push badge counts — stayed stale until you hit Pull. Auto-refresh over SSE and the initial load of a tab remain local-only, so neither costs a network round-trip; a fetch that fails is ignored and the local refresh still happens.
93
- - Ref chips are ordered branches-first, tags-last, instead of taking whatever order git listed them in. The sort is stable, so git's ordering still decides within each group.
94
- - The tab close button is a real 18×18 target with a hover ring and a `Close <repo>` label for screen readers, and its cross is an SVG rather than a `×` glyph — a text glyph is positioned from the font's baseline, so flex centring aligned its line box and left the visible ink off-centre.
111
+ - Refresh (⟳ and <kbd>r</kbd>) fetches from the remote before re-reading the repository, so
112
+ commits pushed by someone else and the Pull/Push counts no longer stay stale. Auto-refresh and
113
+ initial tab load remain local-only; a failed fetch is ignored.
114
+ - Ref chips are ordered branches-first, tags-last, instead of git's listing order.
115
+ - The tab close button is a real 18×18 target with a hover ring and a Close <repo> label for
116
+ screen readers; its cross is an SVG, since a × glyph centres on its line box, not its ink.
95
117
 
96
118
  ## [0.1.0] - 2026-07-28
97
119
 
@@ -122,7 +144,7 @@ First public release.
122
144
 
123
145
  - <kbd>⌘F</kbd> find bar filtering the loaded rows as you type, matching commit message, author name, email, hash prefix, or ref name — no request, and immune to going stale across a background refresh
124
146
  - <kbd>Enter</kbd> / <kbd>Shift+Enter</kbd> step through matches, wrapping at both ends
125
- - Full-history search on demand: three `git log` passes (message, author, hash) unioned into one date-ordered list, capped at 500 results, with the query treated as a literal so a typed `(` or `.` is not a regex
147
+ - Full-history search on demand: three `git log` passes (message, author, hash) unioned into one date-ordered list, capped at 500 results, with the query treated as a literal so a typed ( or . is not a regex
126
148
  - A match below the loaded window pulls the graph down to it
127
149
 
128
150
  ### Working tree
@@ -155,9 +177,9 @@ First public release.
155
177
 
156
178
  ### Security
157
179
 
158
- - Server binds `127.0.0.1` only
159
- - `Host` header pinned to loopback names, closing DNS-rebinding access
160
- - WebSocket `Origin` checked before handing out a PTY
180
+ - Server binds 127.0.0.1 only
181
+ - Host header pinned to loopback names, closing DNS-rebinding access
182
+ - WebSocket Origin checked before handing out a PTY
161
183
  - Repositories are only reachable once registered in `~/.config/megit/config.json`
162
184
  - Client-supplied revs whitelisted rather than escaped, so a leading-dash rev cannot smuggle in a git option
163
185
  - Git invocations cannot block on credential prompts
@@ -166,7 +188,7 @@ First public release.
166
188
 
167
189
  - macOS (arm64, x64): full support, and the only platform verified by hand
168
190
  - Windows (arm64, x64): builds and runs, but untested on real hardware — recursive `fs.watch` crashes the test worker there, so auto-refresh is not covered by CI
169
- - Linux: everything except the terminal — `node-pty` is an `optionalDependency` with no Linux prebuild, and the terminal button is hidden when it is unavailable
191
+ - Linux: everything except the terminal — node-pty is an optionalDependency with no Linux prebuild, and the terminal button is hidden when it is unavailable
170
192
 
171
193
  [Unreleased]: https://github.com/vuongvu1/megit/compare/v0.5.0...HEAD
172
194
  [0.5.0]: https://github.com/vuongvu1/megit/compare/v0.4.1...v0.5.0
@@ -174,4 +196,4 @@ First public release.
174
196
  [0.4.0]: https://github.com/vuongvu1/megit/compare/v0.3.0...v0.4.0
175
197
  [0.3.0]: https://github.com/vuongvu1/megit/compare/v0.2.0...v0.3.0
176
198
  [0.2.0]: https://github.com/vuongvu1/megit/compare/v0.1.0...v0.2.0
177
- [0.1.0]: https://github.com/vuongvu1/megit/releases/tag/v0.1.0
199
+ [0.1.0]: https://github.com/vuongvu1/megit/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,16 +1,16 @@
1
1
  # <img src="public/logo.svg" width="28"/> megit
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/megit-app)](https://www.npmjs.com/package/megit-app) [![CI](https://github.com/vuongvu1/megit/actions/workflows/ci.yml/badge.svg)](https://github.com/vuongvu1/megit/actions/workflows/ci.yml) [![Socket](https://badge.socket.dev/npm/package/megit-app/latest)](https://socket.dev/npm/package/megit-app)
4
+
3
5
  A git repository viewer that runs in your browser. Commit graph with real branch lanes, diffs including your uncommitted work, stashes, staging, search, and a shell — pointed at as many local repos as you like, each in its own tab.
4
6
 
5
7
  It writes, too: stage/unstage/discard, commit and amend, branch and tag create/delete, stash push/pop/drop, checkout (with auto-stash when the worktree is dirty), plus revert, reset, cherry-pick, merge, rebase, pull and push.
6
8
 
7
- Nothing leaves your machine: the server binds `127.0.0.1`, shells out to your own `git`, and has no account, telemetry, or network dependency beyond optional Gravatar avatars.
8
-
9
9
  ![megit — commit graph with branch lanes, stash rows and the sticky WIP row](docs/graph-dark.png)
10
10
 
11
11
  ## Getting started
12
12
 
13
- Requires Node ≥ 22. The package is 1.7 MB, with `express` and `ws` as its only runtime dependencies.
13
+ Requires Node ≥ 22. The package is 1.8 MB, with `ws` as its only runtime dependency and no install scripts.
14
14
 
15
15
  **Run it once, without installing:**
16
16
 
@@ -140,7 +140,7 @@ Requires Node ≥ 24 and pnpm — a development-only floor, since the server run
140
140
 
141
141
  ```bash
142
142
  pnpm install
143
- pnpm dev # Express API on :4500 + Vite dev server on :4000
143
+ pnpm dev # API on :4500 + Vite dev server on :4000
144
144
  ```
145
145
 
146
146
  Production build:
@@ -1 +1 @@
1
- import{a as e,i as t,n,r}from"./index-Bfrt9LxG.js";var i=e(),a=/^<<<<<<< ?(.*)$/,o=/^\|\|\|\|\|\|\| ?(.*)$/,s=/^=======$/,c=/^>>>>>>> ?(.*)$/,l=e=>e.replace(/\r?\n$/,``);function u(e){let t=e.split(/(?<=\n)/),n=[],r=[],i=null,u=`context`,d=()=>{r.length&&n.push({kind:`context`,lines:r}),r=[]};for(let e of t){let t=l(e),f=a.exec(t);if(f){if(u!==`context`)return null;d(),i={ours:[],base:null,theirs:[],oursLabel:f[1].trim(),theirsLabel:``},u=`ours`;continue}if(u===`context`){r.push(e);continue}if(o.exec(t)){if(u!==`ours`)return null;i.base=[],u=`base`;continue}if(s.test(t)){if(u!==`ours`&&u!==`base`)return null;u=`theirs`;continue}let p=c.exec(t);if(p){if(u!==`theirs`)return null;i.theirsLabel=p[1].trim(),n.push({kind:`conflict`,block:i}),i=null,u=`context`;continue}u===`ours`?i.ours.push(e):u===`base`?i.base.push(e):i.theirs.push(e)}return u===`context`?(d(),n.some(e=>e.kind===`conflict`)?n:null):null}function d(e,t){let n=[];return e.forEach((e,r)=>{if(e.kind===`context`){n.push(...e.lines);return}let i=t.get(r);if(!i)throw Error(`no pick for conflict at segment ${r}`);(i===`ours`||i===`both`)&&n.push(...e.block.ours),(i===`theirs`||i===`both`)&&n.push(...e.block.theirs)}),n.join(``)}var f=n(),p=[[`ours`,`Use ours`],[`theirs`,`Use theirs`],[`both`,`Use both`]];function m({note:e,busy:t,onPick:n,onMarkResolved:r}){return(0,f.jsxs)(`div`,{className:`cf-card`,children:[(0,f.jsx)(`div`,{className:`cf-card-note`,children:e}),(0,f.jsxs)(`div`,{className:`cf-card-actions`,children:[r&&(0,f.jsx)(`button`,{className:`primary`,disabled:t,onClick:r,children:`Mark resolved`}),(0,f.jsx)(`button`,{disabled:t,onClick:()=>n(`ours`),children:`Keep ours`}),(0,f.jsx)(`button`,{disabled:t,onClick:()=>n(`theirs`),children:`Keep theirs`}),(0,f.jsx)(`button`,{className:`danger`,disabled:t,onClick:()=>n(`delete`),children:`Delete file`})]})]})}function h({repo:e,file:n,onResolved:a}){let[o,s]=(0,i.useState)(null),[c,l]=(0,i.useState)(new Map),[h,g]=(0,i.useState)(``),[_,v]=(0,i.useState)(!1),y=`repo=${encodeURIComponent(e)}`;(0,i.useEffect)(()=>{s(null),l(new Map),g(``),r(`/api/conflict?${y}&file=${encodeURIComponent(n)}`).then(s).catch(e=>g(e.message))},[e,n]);let b=(0,i.useMemo)(()=>o?.content?u(o.content):null,[o]),x=(0,i.useMemo)(()=>b?.filter(e=>e.kind===`conflict`).length??0,[b]);(0,i.useEffect)(()=>{!b||!x||c.size<x||(v(!0),r(`/api/conflict?${y}`,t(`POST`,{action:`resolve`,file:n,content:d(b,c)})).then(a).catch(e=>{g(e.message),v(!1)}))},[c,b,x]);let S=e=>{v(!0),r(`/api/conflict?${y}`,t(`POST`,{action:e,file:n})).then(a).catch(e=>{g(e.message),v(!1)})},C=()=>{v(!0),r(`/api/conflict?${y}`,t(`POST`,{action:`resolve`,file:n,content:o?.content??``})).then(a).catch(e=>{g(e.message),v(!1)})},w=(e,t)=>l(n=>new Map(n).set(e,t)),T=e=>l(t=>{let n=new Map(t);return n.delete(e),n});return h?(0,f.jsx)(`div`,{className:`diffview error`,children:h}):o?o.tooLarge?(0,f.jsxs)(`div`,{className:`diffview empty`,children:[`File too large to resolve here (`,Math.round((o.size??0)/1024),` KB) — use the terminal`]}):o.binary?(0,f.jsx)(`div`,{className:`cf-view`,children:(0,f.jsx)(m,{note:`Binary file — there is nothing to merge line by line.`,busy:_,onPick:S})}):o.missing?(0,f.jsx)(`div`,{className:`cf-view`,children:(0,f.jsx)(m,{note:`Deleted on one side and modified on the other.`,busy:_,onPick:S})}):b?(0,f.jsxs)(`div`,{className:`cf-view`,children:[(0,f.jsxs)(`div`,{className:`cf-progress`,children:[c.size,` of `,x,` resolved`]}),b.map((e,t)=>{if(e.kind===`context`)return(0,f.jsx)(`pre`,{className:`cf-context`,children:e.lines.join(``)},t);let n=c.get(t);return(0,f.jsxs)(`div`,{className:`cf-block${n?` picked`:``}`,children:[(0,f.jsxs)(`div`,{className:`cf-bar`,children:[(0,f.jsx)(`span`,{className:`cf-side-name`,children:e.block.oursLabel||`ours`}),n?(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(`span`,{className:`cf-chosen`,children:[`took `,n]}),(0,f.jsx)(`button`,{disabled:_,onClick:()=>T(t),children:`Reset`})]}):p.map(([e,n])=>(0,f.jsx)(`button`,{disabled:_,onClick:()=>w(t,e),children:n},e))]}),(!n||n===`ours`||n===`both`)&&(0,f.jsx)(`pre`,{className:`cf-ours`,children:e.block.ours.join(``)}),!n&&(0,f.jsx)(`div`,{className:`cf-mid`,children:e.block.theirsLabel||`theirs`}),(!n||n===`theirs`||n===`both`)&&(0,f.jsx)(`pre`,{className:`cf-theirs`,children:e.block.theirs.join(``)})]},t)})]}):(0,f.jsx)(`div`,{className:`cf-view`,children:(0,f.jsx)(m,{note:`No conflict markers left in this file — it looks already resolved.`,busy:_,onPick:S,onMarkResolved:C})}):(0,f.jsx)(`div`,{className:`diffview empty`,children:`Loading…`})}export{h as default};
1
+ import{a as e,i as t,n,r}from"./index-BZBjYCd4.js";var i=e(),a=/^<<<<<<< ?(.*)$/,o=/^\|\|\|\|\|\|\| ?(.*)$/,s=/^=======$/,c=/^>>>>>>> ?(.*)$/,l=e=>e.replace(/\r?\n$/,``);function u(e){let t=e.split(/(?<=\n)/),n=[],r=[],i=null,u=`context`,d=()=>{r.length&&n.push({kind:`context`,lines:r}),r=[]};for(let e of t){let t=l(e),f=a.exec(t);if(f){if(u!==`context`)return null;d(),i={ours:[],base:null,theirs:[],oursLabel:f[1].trim(),theirsLabel:``},u=`ours`;continue}if(u===`context`){r.push(e);continue}if(o.exec(t)){if(u!==`ours`)return null;i.base=[],u=`base`;continue}if(s.test(t)){if(u!==`ours`&&u!==`base`)return null;u=`theirs`;continue}let p=c.exec(t);if(p){if(u!==`theirs`)return null;i.theirsLabel=p[1].trim(),n.push({kind:`conflict`,block:i}),i=null,u=`context`;continue}u===`ours`?i.ours.push(e):u===`base`?i.base.push(e):i.theirs.push(e)}return u===`context`?(d(),n.some(e=>e.kind===`conflict`)?n:null):null}function d(e,t){let n=[];return e.forEach((e,r)=>{if(e.kind===`context`){n.push(...e.lines);return}let i=t.get(r);if(!i)throw Error(`no pick for conflict at segment ${r}`);(i===`ours`||i===`both`)&&n.push(...e.block.ours),(i===`theirs`||i===`both`)&&n.push(...e.block.theirs)}),n.join(``)}var f=n(),p=[[`ours`,`Use ours`],[`theirs`,`Use theirs`],[`both`,`Use both`]];function m({note:e,busy:t,onPick:n,onMarkResolved:r}){return(0,f.jsxs)(`div`,{className:`cf-card`,children:[(0,f.jsx)(`div`,{className:`cf-card-note`,children:e}),(0,f.jsxs)(`div`,{className:`cf-card-actions`,children:[r&&(0,f.jsx)(`button`,{className:`primary`,disabled:t,onClick:r,children:`Mark resolved`}),(0,f.jsx)(`button`,{disabled:t,onClick:()=>n(`ours`),children:`Keep ours`}),(0,f.jsx)(`button`,{disabled:t,onClick:()=>n(`theirs`),children:`Keep theirs`}),(0,f.jsx)(`button`,{className:`danger`,disabled:t,onClick:()=>n(`delete`),children:`Delete file`})]})]})}function h({repo:e,file:n,onResolved:a}){let[o,s]=(0,i.useState)(null),[c,l]=(0,i.useState)(new Map),[h,g]=(0,i.useState)(``),[_,v]=(0,i.useState)(!1),y=`repo=${encodeURIComponent(e)}`;(0,i.useEffect)(()=>{s(null),l(new Map),g(``),r(`/api/conflict?${y}&file=${encodeURIComponent(n)}`).then(s).catch(e=>g(e.message))},[e,n]);let b=(0,i.useMemo)(()=>o?.content?u(o.content):null,[o]),x=(0,i.useMemo)(()=>b?.filter(e=>e.kind===`conflict`).length??0,[b]);(0,i.useEffect)(()=>{!b||!x||c.size<x||(v(!0),r(`/api/conflict?${y}`,t(`POST`,{action:`resolve`,file:n,content:d(b,c)})).then(a).catch(e=>{g(e.message),v(!1)}))},[c,b,x]);let S=e=>{v(!0),r(`/api/conflict?${y}`,t(`POST`,{action:e,file:n})).then(a).catch(e=>{g(e.message),v(!1)})},C=()=>{v(!0),r(`/api/conflict?${y}`,t(`POST`,{action:`resolve`,file:n,content:o?.content??``})).then(a).catch(e=>{g(e.message),v(!1)})},w=(e,t)=>l(n=>new Map(n).set(e,t)),T=e=>l(t=>{let n=new Map(t);return n.delete(e),n});return h?(0,f.jsx)(`div`,{className:`diffview error`,children:h}):o?o.tooLarge?(0,f.jsxs)(`div`,{className:`diffview empty`,children:[`File too large to resolve here (`,Math.round((o.size??0)/1024),` KB) — use the terminal`]}):o.binary?(0,f.jsx)(`div`,{className:`cf-view`,children:(0,f.jsx)(m,{note:`Binary file — there is nothing to merge line by line.`,busy:_,onPick:S})}):o.missing?(0,f.jsx)(`div`,{className:`cf-view`,children:(0,f.jsx)(m,{note:`Deleted on one side and modified on the other.`,busy:_,onPick:S})}):b?(0,f.jsxs)(`div`,{className:`cf-view`,children:[(0,f.jsxs)(`div`,{className:`cf-progress`,children:[c.size,` of `,x,` resolved`]}),b.map((e,t)=>{if(e.kind===`context`)return(0,f.jsx)(`pre`,{className:`cf-context`,children:e.lines.join(``)},t);let n=c.get(t);return(0,f.jsxs)(`div`,{className:`cf-block${n?` picked`:``}`,children:[(0,f.jsxs)(`div`,{className:`cf-bar`,children:[(0,f.jsx)(`span`,{className:`cf-side-name`,children:e.block.oursLabel||`ours`}),n?(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(`span`,{className:`cf-chosen`,children:[`took `,n]}),(0,f.jsx)(`button`,{disabled:_,onClick:()=>T(t),children:`Reset`})]}):p.map(([e,n])=>(0,f.jsx)(`button`,{disabled:_,onClick:()=>w(t,e),children:n},e))]}),(!n||n===`ours`||n===`both`)&&(0,f.jsx)(`pre`,{className:`cf-ours`,children:e.block.ours.join(``)}),!n&&(0,f.jsx)(`div`,{className:`cf-mid`,children:e.block.theirsLabel||`theirs`}),(!n||n===`theirs`||n===`both`)&&(0,f.jsx)(`pre`,{className:`cf-theirs`,children:e.block.theirs.join(``)})]},t)})]}):(0,f.jsx)(`div`,{className:`cf-view`,children:(0,f.jsx)(m,{note:`No conflict markers left in this file — it looks already resolved.`,busy:_,onPick:S,onMarkResolved:C})}):(0,f.jsx)(`div`,{className:`diffview empty`,children:`Loading…`})}export{h as default};