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 +21 -0
- package/README.md +147 -0
- package/docs/diff.png +0 -0
- package/docs/interactive.png +0 -0
- package/docs/panel.png +0 -0
- package/package.json +62 -0
- package/src/config/constants.ts +16 -0
- package/src/config/index.ts +2 -0
- package/src/config/options.ts +8 -0
- package/src/diff/body.tsx +90 -0
- package/src/diff/header.tsx +77 -0
- package/src/diff/index.ts +2 -0
- package/src/diff/keymap.ts +113 -0
- package/src/diff/open-diff.ts +57 -0
- package/src/diff/panel.tsx +156 -0
- package/src/diff/search.tsx +38 -0
- package/src/git/diff-parse.ts +35 -0
- package/src/git/index.ts +3 -0
- package/src/git/porcelain.ts +10 -0
- package/src/git/runner.ts +45 -0
- package/src/sidebar/file-list.tsx +47 -0
- package/src/sidebar/focus.tsx +37 -0
- package/src/sidebar/header.tsx +55 -0
- package/src/sidebar/index.ts +1 -0
- package/src/sidebar/keymap.ts +143 -0
- package/src/sidebar/list-navigation.tsx +96 -0
- package/src/sidebar/staging.tsx +108 -0
- package/src/sidebar/status.tsx +29 -0
- package/src/sidebar/view.tsx +159 -0
- package/src/sidebar/working-copy.tsx +81 -0
- package/src/state/diff-store.ts +34 -0
- package/src/state/index.ts +1 -0
- package/src/tui.tsx +25 -0
- package/src/types.ts +63 -0
- package/src/ui/format.ts +67 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/syntax.ts +61 -0
- package/tui.ts +1 -0
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
|
+
|  |  |  |
|
|
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,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,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
|
+
}
|