opencode-git-plugin 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 opencode-git-plugin contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # Git Panel
2
+
3
+ An OpenCode v2 TUI plugin that shows Git working-tree changes in the sidebar and opens file diffs in the native session panel.
4
+
5
+ ## Features
6
+
7
+ - Sidebar list with `A`, `M`, and `D` badges plus per-file addition/deletion counts
8
+ - Staged files marked as `[A]`, `[M]`, or `[D]`
9
+ - Stage and unstage with the keyboard, command palette, or mouse
10
+ - Commit staged files without leaving the TUI
11
+ - Scrollable diff panel with line numbers and responsive controls
12
+ - Search with highlighted matches and next/previous navigation
13
+ - Optional persisted tree-sitter syntax highlighting
14
+ - Automatic refresh from OpenCode events and a configurable polling interval
15
+ - Safe handling of Git paths containing spaces, quotes, arrows, or newlines
16
+
17
+ The plugin runs Git through OpenCode's local shell API. It does not contact an external service.
18
+
19
+ ## Screenshots
20
+
21
+ | File list | List navigation | Diff panel |
22
+ | --- | --- | --- |
23
+ | ![Git Panel sidebar showing changed files with status badges](docs/panel.png) | ![Git Panel list navigation with stage and commit controls](docs/interactive.png) | ![Git Panel diff panel with syntax highlighting and search](docs/diff.png) |
24
+
25
+ ## Requirements
26
+
27
+ - OpenCode `>=2.0.0 <3`
28
+ - A Git repository as the active session directory
29
+
30
+ ## Install
31
+
32
+ Install the published package with OpenCode:
33
+
34
+ ```sh
35
+ opencode plugin add opencode-git-plugin
36
+ ```
37
+
38
+ Git Panel is a TUI-only plugin, so `opencode plugin add` registers it in `~/.config/opencode/cli.json`. It stays active when the TUI connects to a remote server.
39
+
40
+ Restart the OpenCode TUI after installation. Verify installation with:
41
+
42
+ ```sh
43
+ opencode plugin list
44
+ ```
45
+
46
+ ### Update
47
+
48
+ ```sh
49
+ opencode plugin check
50
+ opencode plugin update
51
+ ```
52
+
53
+ `check` reports whether a newer version is available; `update` installs it. Restart the TUI afterwards.
54
+
55
+ ### Uninstall
56
+
57
+ ```sh
58
+ opencode plugin remove opencode-git-plugin
59
+ ```
60
+
61
+ ### Local development
62
+
63
+ For local development, add the package directory to `~/.config/opencode/cli.json`:
64
+
65
+ ```jsonc
66
+ {
67
+ "plugins": [
68
+ {
69
+ "package": "/absolute/path/to/git-changes",
70
+ "options": { "intervalMs": 4000 }
71
+ }
72
+ ]
73
+ }
74
+ ```
75
+
76
+ Fully quit and reopen the TUI after changing local plugin code.
77
+
78
+ Do not add the plugin to `opencode.json(c)`; that file is for server plugins and Git Panel has no server entrypoint.
79
+
80
+ ## Options
81
+
82
+ | Option | Type | Default | Description |
83
+ | --- | --- | --- | --- |
84
+ | `intervalMs` | `number` | `4000` | Refresh interval in milliseconds; values below `1000` are clamped. |
85
+
86
+ ## Usage
87
+
88
+ ### File list
89
+
90
+ - `<leader>G` (or palette: `Git Panel: Focus List`) enters list mode (yellow top/bottom border)
91
+ - `↑` / `↓` move, `Enter` opens the diff, `→` stages, `←` unstages, `c` commits staged files, `ESC` exits
92
+ - Click a row to open its diff; click the branch name to enter stage mode (every clicked file toggles staged/unstaged; branch click or `ESC` exits)
93
+ - Clicking the prompt input exits list mode so typing is never hijacked
94
+ - Staging also via palette (`Git Panel: Stage/Unstage File`)
95
+
96
+ ### Diff panel
97
+
98
+ - `?` searches (yellow match highlight + match counter, `↑` / `↓` jump between matches), `ESC` clears the search, second `ESC` (or `Backspace`) closes the panel
99
+ - `s`, the `[syntax:on/off]` header button, or palette (`Git Panel: Toggle Diff Syntax Highlighting`) toggles language syntax colors; default off, persisted across restarts
100
+ - While searching, the view temporarily falls back to plain text so highlight + jump keep working
101
+ - `j` / `k` scroll in syntax mode; mouse wheel works in both modes
102
+ - Closing the panel returns to list mode
103
+ - Plain diffs are truncated after 4,000 parsed lines to keep the TUI responsive
104
+
105
+ ### Commit
106
+
107
+ - Green `[c : Commit]` button (or `c`, or palette: `Git Panel: Commit Staged`), visible only while files are staged and list mode is active; prompts for a message, then commits
108
+
109
+ ## States
110
+
111
+ - `Loading…` while fetching
112
+ - `Working tree clean` when there is nothing to show (header stats hidden)
113
+ - `Empty repo — no commits yet` before the first commit
114
+ - `Not a git repository` outside a repo, plus raw fetch errors
115
+
116
+ ## Development
117
+
118
+ ```sh
119
+ bun install
120
+ bun run check
121
+ npm pack --dry-run
122
+ ```
123
+
124
+ The package exposes only the `./tui` export (`tui.ts` at the package root, which re-exports `src/tui.tsx`). Local directory plugins are resolved as `<dir>/tui`, published packages through the `./tui` export.
125
+
126
+ Any module that imports runtime values from `solid-js` (`createSignal`, `createMemo`, `onCleanup`, …) must be a `.tsx` file. OpenCode only redirects `solid-js` to its own runtime in `.jsx`/`.tsx` files; in a `.ts` file the import loads a second Solid instance and the UI stops updating. A test enforces this.
127
+
128
+ Before publishing, test the packed tarball rather than the working directory:
129
+
130
+ ```sh
131
+ bun pm pack
132
+ mkdir -p /tmp/git-panel-smoke && cd /tmp/git-panel-smoke
133
+ bun init -y && bun add /path/to/opencode-git-plugin-<version>.tgz
134
+ ```
135
+
136
+ ## Publish
137
+
138
+ ```sh
139
+ npm login
140
+ npm publish
141
+ ```
142
+
143
+ The `prepublishOnly` script runs type checking and tests before npm publishes the package.
144
+
145
+ ## License
146
+
147
+ MIT
package/docs/diff.png ADDED
Binary file
Binary file
package/docs/panel.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "name": "opencode-git-plugin",
4
+ "version": "0.2.0",
5
+ "description": "OpenCode v2 TUI sidebar plugin listing git working-copy changes with diff preview",
6
+ "type": "module",
7
+ "exports": {
8
+ "./tui": "./tui.ts"
9
+ },
10
+ "files": [
11
+ "tui.ts",
12
+ "src",
13
+ "docs",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "bun test",
20
+ "check": "bun run typecheck && bun test",
21
+ "prepublishOnly": "bun run check"
22
+ },
23
+ "dependencies": {
24
+ "@opencode/plugin": "^2.0.21",
25
+ "@opencode/theme": "^2.0.21"
26
+ },
27
+ "peerDependencies": {
28
+ "@opentui/core": ">=0.5.14",
29
+ "@opentui/solid": ">=0.5.14",
30
+ "solid-js": ">=1.9.0"
31
+ },
32
+ "devDependencies": {
33
+ "@opentui/core": "0.5.14",
34
+ "@opentui/solid": "0.5.14",
35
+ "solid-js": "1.9.12",
36
+ "typescript": "^5.6.0"
37
+ },
38
+ "keywords": [
39
+ "opencode",
40
+ "opencode-plugin",
41
+ "tui",
42
+ "sidebar",
43
+ "git",
44
+ "vcs"
45
+ ],
46
+ "license": "MIT",
47
+ "author": "Aktug Urun",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/aktug/opencode-git-plugin.git"
51
+ },
52
+ "homepage": "https://github.com/aktug/opencode-git-plugin#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/aktug/opencode-git-plugin/issues"
55
+ },
56
+ "engines": {
57
+ "opencode": ">=2.0.0 <3"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ }
62
+ }
@@ -0,0 +1,16 @@
1
+ export const DEFAULT_INTERVAL_MS = 4000
2
+ export const MIN_INTERVAL_MS = 1000
3
+ export const MAX_DIFF_LINES = 4000
4
+ export const GIT_TIMEOUT_MS = 120_000
5
+ export const GIT_POLL_MS = 50
6
+
7
+ export const DIFF_STORE_KEY = "git-panel.diff-view"
8
+ export const DIFF_PANEL_NAME = "git-panel.diff"
9
+ export const REFRESH_STORE_KEY = "git-panel.refresh"
10
+ export const SETTINGS_STORE_KEY = "git-panel.settings"
11
+ export const STAGED_STORE_KEY = "git-panel.staged"
12
+
13
+ export const LIST_NAV_MODE = "git-panel-list"
14
+
15
+ export const FILE_ROW_BADGE_WIDTH = 3
16
+ export const EVENT_REFRESH_DEBOUNCE_MS = 2000
@@ -0,0 +1,2 @@
1
+ export * from "./constants"
2
+ export * from "./options"
@@ -0,0 +1,8 @@
1
+ import type { Context } from "@opencode/plugin/tui/context"
2
+ import { DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS } from "./constants"
3
+
4
+ export function intervalMs(ctx: Context): number {
5
+ const raw = ctx.options.intervalMs
6
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return DEFAULT_INTERVAL_MS
7
+ return Math.max(MIN_INTERVAL_MS, Math.floor(raw))
8
+ }
@@ -0,0 +1,90 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { For, Show } from "solid-js"
3
+ import type { generateSyntax } from "@opencode/theme/tui"
4
+ import { diffLinePrefix, diffLineStyle, syntaxFiletype } from "../ui"
5
+ import type { DiffRow, DiffViewState, ScrollHandle, Theme } from "../types"
6
+
7
+ function DiffLineRow(props: { theme: Theme; row: DiffRow; index: number }) {
8
+ const theme = props.theme
9
+ const row = props.row
10
+ const style = diffLineStyle(theme, row.kind)
11
+ const isCurrent = row.state === "current"
12
+ const gutter = row.line !== undefined ? String(row.line).padStart(4, " ") : " "
13
+ const fg = isCurrent
14
+ ? theme.diff.background.context
15
+ : row.state === "hit"
16
+ ? theme.text.feedback.warning.base
17
+ : style.fg
18
+ const bg = isCurrent ? theme.text.feedback.warning.base : style.bg
19
+ return (
20
+ <box flexDirection="row" width="100%" id={`git-panel-diff-line-${props.index}`}>
21
+ <text fg={isCurrent ? theme.text.feedback.warning.base : theme.text.muted} width={5}>
22
+ {isCurrent ? `❯${gutter.slice(1)}` : `${gutter} `}
23
+ </text>
24
+ <text fg={fg} bg={bg} wrapMode="word" flexGrow={1}>
25
+ {diffLinePrefix(row.kind)}
26
+ {row.text}
27
+ </text>
28
+ </box>
29
+ )
30
+ }
31
+
32
+ export function DiffBody(props: {
33
+ theme: Theme
34
+ state: DiffViewState
35
+ rows: DiffRow[]
36
+ focused: boolean
37
+ syntaxActive: boolean
38
+ syntaxStyle: ReturnType<typeof generateSyntax> | undefined
39
+ onScrollRef: (el: ScrollHandle) => void
40
+ }) {
41
+ const theme = props.theme
42
+ return (
43
+ <>
44
+ <Show when={props.state.loading}>
45
+ <text fg={theme.text.muted}>Loading diff…</text>
46
+ </Show>
47
+ <Show when={!props.state.loading && props.state.message}>
48
+ <text fg={theme.text.muted}>{props.state.message}</text>
49
+ </Show>
50
+ <Show when={!props.state.loading && !props.state.message}>
51
+ <scrollbox
52
+ width="100%"
53
+ flexGrow={1}
54
+ flexShrink={1}
55
+ minHeight={0}
56
+ scrollY
57
+ scrollX={false}
58
+ focused={props.focused}
59
+ ref={(el) => props.onScrollRef(el)}
60
+ contentOptions={{ flexDirection: "column", gap: 0 }}
61
+ wrapperOptions={{ paddingRight: 1 }}
62
+ >
63
+ <Show when={props.syntaxActive}>
64
+ <diff
65
+ diff={props.state.patch}
66
+ view="unified"
67
+ filetype={syntaxFiletype(props.state.file)}
68
+ syntaxStyle={props.syntaxStyle}
69
+ showLineNumbers
70
+ wrapMode="word"
71
+ fg={theme.text.base}
72
+ addedBg={theme.diff.background.added}
73
+ removedBg={theme.diff.background.removed}
74
+ contextBg={theme.diff.background.context}
75
+ addedSignColor={theme.diff.text.added}
76
+ removedSignColor={theme.diff.text.removed}
77
+ lineNumberFg={theme.text.muted}
78
+ />
79
+ </Show>
80
+ <Show when={!props.syntaxActive}>
81
+ <For each={props.rows}>{(row, i) => <DiffLineRow theme={theme} row={row} index={i()} />}</For>
82
+ </Show>
83
+ <Show when={props.state.truncated}>
84
+ <text fg={theme.text.muted}>… truncated</text>
85
+ </Show>
86
+ </scrollbox>
87
+ </Show>
88
+ </>
89
+ )
90
+ }
@@ -0,0 +1,77 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { Show } from "solid-js"
3
+ import { displayWidth, maxTextWidth, truncatePath } from "../ui"
4
+ import type { Theme } from "../types"
5
+
6
+ export function DiffHeader(props: {
7
+ theme: Theme
8
+ width: number
9
+ file: string
10
+ additions: number
11
+ deletions: number
12
+ query: string
13
+ matchCursor: number
14
+ matchCount: number
15
+ syntax: boolean
16
+ onSearch: () => void
17
+ onToggleSyntax: () => void
18
+ onClose: () => void
19
+ }) {
20
+ const theme = props.theme
21
+ return (
22
+ <box flexDirection="column" gap={0} flexShrink={0}>
23
+ <box flexDirection="row" width={Math.max(1, props.width)} flexShrink={0}>
24
+ <text fg={theme.text.base} wrapMode="none" flexShrink={0}>
25
+ <b>
26
+ {truncatePath(
27
+ props.file,
28
+ maxTextWidth(
29
+ props.width,
30
+ displayWidth(`+${props.additions}`) + 1 + displayWidth(`-${props.deletions}`) + 1,
31
+ ),
32
+ )}
33
+ </b>
34
+ </text>
35
+ <box flexGrow={1} />
36
+ <box flexDirection="row" gap={1} flexShrink={0}>
37
+ <text fg={theme.diff.text.added} wrapMode="none" flexShrink={0}>
38
+ +{props.additions}
39
+ </text>
40
+ <text fg={theme.diff.text.removed} wrapMode="none" flexShrink={0}>
41
+ -{props.deletions}
42
+ </text>
43
+ </box>
44
+ </box>
45
+ <Show when={props.query}>
46
+ <text fg={theme.text.feedback.warning.base} wrapMode="none">
47
+ {props.matchCount > 0 ? `${props.matchCursor + 1}/${props.matchCount}` : "0/0"} · {props.query}
48
+ </text>
49
+ </Show>
50
+ <box height={1} flexShrink={0} />
51
+ <box flexDirection="row" flexWrap="wrap" width={Math.max(1, props.width)} gap={1} flexShrink={0}>
52
+ <text fg={theme.text.muted} wrapMode="none" flexShrink={0} onMouseUp={() => props.onSearch()}>
53
+ [? : Search]
54
+ </text>
55
+ <Show when={props.query}>
56
+ <text fg={theme.text.muted} wrapMode="none" flexShrink={0}>
57
+ [↑↓ : Jump]
58
+ </text>
59
+ </Show>
60
+ <text
61
+ fg={props.syntax ? theme.text.feedback.warning.base : theme.text.muted}
62
+ wrapMode="none"
63
+ flexShrink={0}
64
+ onMouseUp={() => props.onToggleSyntax()}
65
+ >
66
+ {props.syntax ? "[syntax:on]" : "[syntax:off]"}
67
+ </text>
68
+ <text fg={theme.text.base} wrapMode="none" flexShrink={0} onMouseUp={() => props.onClose()}>
69
+ [ Close · ESC/⌫ ]
70
+ </text>
71
+ </box>
72
+ <text fg={theme.text.muted} wrapMode="none">
73
+ {"─".repeat(Math.max(1, props.width))}
74
+ </text>
75
+ </box>
76
+ )
77
+ }
@@ -0,0 +1,2 @@
1
+ export { createDiffOpener } from "./open-diff"
2
+ export { DiffPanel } from "./panel"
@@ -0,0 +1,113 @@
1
+ import type { Context, PanelInput } from "@opencode/plugin/tui/context"
2
+ import { DIFF_PANEL_NAME } from "../config"
3
+
4
+ export function registerDiffKeymap(
5
+ ctx: Context,
6
+ deps: {
7
+ input: () => PanelInput
8
+ searching: () => boolean
9
+ hasMatches: () => boolean
10
+ syntaxActive: () => boolean
11
+ search(): void
12
+ stepMatch(dir: 1 | -1): void
13
+ clearSearch(): void
14
+ close(): void
15
+ toggleSyntax(): void
16
+ scrollBy(delta: number): void
17
+ },
18
+ ): void {
19
+ ctx.keymap.layer(() => {
20
+ const searching = deps.searching()
21
+ const hasMatches = deps.hasMatches()
22
+ const inSyntax = deps.syntaxActive()
23
+ return {
24
+ mode: "global",
25
+ priority: 20,
26
+ enabled: () => deps.input().name === DIFF_PANEL_NAME,
27
+ bindings: [
28
+ "git-panel.diff-search",
29
+ "git-panel.diff-next-match",
30
+ "git-panel.diff-prev-match",
31
+ "git-panel.diff-clear-search",
32
+ "git-panel.close-diff",
33
+ "git-panel.close-diff-backspace",
34
+ "git-panel.toggle-syntax",
35
+ "git-panel.syntax-down",
36
+ "git-panel.syntax-up",
37
+ ],
38
+ commands: [
39
+ {
40
+ id: "git-panel.diff-search",
41
+ title: "Git Panel: Search in Diff",
42
+ group: "Git Panel",
43
+ bind: "?",
44
+ run: () => deps.search(),
45
+ },
46
+ {
47
+ id: "git-panel.diff-next-match",
48
+ title: "Git Panel: Next Diff Match",
49
+ group: "Git Panel",
50
+ bind: "down",
51
+ enabled: searching && hasMatches,
52
+ run: () => deps.stepMatch(1),
53
+ },
54
+ {
55
+ id: "git-panel.diff-prev-match",
56
+ title: "Git Panel: Previous Diff Match",
57
+ group: "Git Panel",
58
+ bind: "up",
59
+ enabled: searching && hasMatches,
60
+ run: () => deps.stepMatch(-1),
61
+ },
62
+ {
63
+ id: "git-panel.diff-clear-search",
64
+ title: "Git Panel: Clear Diff Search",
65
+ group: "Git Panel",
66
+ bind: "escape",
67
+ enabled: searching,
68
+ run: () => deps.clearSearch(),
69
+ },
70
+ {
71
+ id: "git-panel.close-diff",
72
+ title: "Git Panel: Close Diff",
73
+ group: "Git Panel",
74
+ bind: "escape",
75
+ enabled: !searching,
76
+ run: () => deps.close(),
77
+ },
78
+ {
79
+ id: "git-panel.close-diff-backspace",
80
+ title: "Git Panel: Close Diff (Backspace)",
81
+ group: "Git Panel",
82
+ bind: "backspace",
83
+ enabled: !searching,
84
+ run: () => deps.close(),
85
+ },
86
+ {
87
+ id: "git-panel.toggle-syntax",
88
+ title: "Git Panel: Toggle Diff Syntax Highlighting",
89
+ group: "Git Panel",
90
+ bind: "s",
91
+ palette: true,
92
+ run: () => deps.toggleSyntax(),
93
+ },
94
+ {
95
+ id: "git-panel.syntax-down",
96
+ title: "Git Panel: Scroll Syntax Diff Down",
97
+ group: "Git Panel",
98
+ bind: "j",
99
+ enabled: inSyntax,
100
+ run: () => deps.scrollBy(3),
101
+ },
102
+ {
103
+ id: "git-panel.syntax-up",
104
+ title: "Git Panel: Scroll Syntax Diff Up",
105
+ group: "Git Panel",
106
+ bind: "k",
107
+ enabled: inSyntax,
108
+ run: () => deps.scrollBy(-3),
109
+ },
110
+ ],
111
+ }
112
+ })
113
+ }
@@ -0,0 +1,57 @@
1
+ import type { Context } from "@opencode/plugin/tui/context"
2
+ import { DIFF_PANEL_NAME, MAX_DIFF_LINES } from "../config"
3
+ import { parseDiff } from "../git"
4
+ import { useDiffStore } from "../state"
5
+ import { friendlyError } from "../ui"
6
+ import type { ChangeFile } from "../types"
7
+
8
+ export function createDiffOpener(ctx: Context, location: () => { directory: string }) {
9
+ const [diffView, setDiffState] = useDiffStore(ctx)
10
+ let activeRequestId = 0
11
+
12
+ return async (f: ChangeFile) => {
13
+ const requestId = ++activeRequestId
14
+ const isStale = () => requestId !== activeRequestId || diffView.file !== f.file || !diffView.panelOpen
15
+
16
+ setDiffState((draft) => {
17
+ draft.file = f.file
18
+ draft.additions = f.additions
19
+ draft.deletions = f.deletions
20
+ draft.lines = []
21
+ draft.patch = ""
22
+ draft.message = ""
23
+ draft.truncated = false
24
+ draft.loading = true
25
+ draft.query = ""
26
+ draft.matchCursor = 0
27
+ draft.panelOpen = true
28
+ })
29
+ ctx.ui.panel.open(DIFF_PANEL_NAME, { presentation: "panel" })
30
+ try {
31
+ const res = await ctx.client.vcs.diff({ location: location(), mode: "working", context: 3 })
32
+ if (isStale()) return
33
+ const entry = res.data.find((d: { file: string }) => d.file === f.file)
34
+ const parsed = entry ? parseDiff(entry.patch) : []
35
+ setDiffState((draft) => {
36
+ draft.loading = false
37
+ draft.patch = entry?.patch ?? ""
38
+ if (!entry) {
39
+ draft.message = "No diff available"
40
+ } else if (parsed.length === 0) {
41
+ draft.message = "No visible changes (empty or binary file)"
42
+ } else if (parsed.length > MAX_DIFF_LINES) {
43
+ draft.lines = parsed.slice(0, MAX_DIFF_LINES)
44
+ draft.truncated = true
45
+ } else {
46
+ draft.lines = parsed
47
+ }
48
+ })
49
+ } catch (e) {
50
+ if (isStale()) return
51
+ setDiffState((draft) => {
52
+ draft.loading = false
53
+ draft.message = friendlyError(e)
54
+ })
55
+ }
56
+ }
57
+ }