opencode-git-plugin 0.2.0 → 0.3.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/README.md +8 -3
- package/dist/tui.js +2123 -0
- package/package.json +5 -4
- package/src/config/constants.ts +0 -16
- package/src/config/index.ts +0 -2
- package/src/config/options.ts +0 -8
- package/src/diff/body.tsx +0 -90
- package/src/diff/header.tsx +0 -77
- package/src/diff/index.ts +0 -2
- package/src/diff/keymap.ts +0 -113
- package/src/diff/open-diff.ts +0 -57
- package/src/diff/panel.tsx +0 -156
- package/src/diff/search.tsx +0 -38
- package/src/git/diff-parse.ts +0 -35
- package/src/git/index.ts +0 -3
- package/src/git/porcelain.ts +0 -10
- package/src/git/runner.ts +0 -45
- package/src/sidebar/file-list.tsx +0 -47
- package/src/sidebar/focus.tsx +0 -37
- package/src/sidebar/header.tsx +0 -55
- package/src/sidebar/index.ts +0 -1
- package/src/sidebar/keymap.ts +0 -143
- package/src/sidebar/list-navigation.tsx +0 -96
- package/src/sidebar/staging.tsx +0 -108
- package/src/sidebar/status.tsx +0 -29
- package/src/sidebar/view.tsx +0 -159
- package/src/sidebar/working-copy.tsx +0 -81
- package/src/state/diff-store.ts +0 -34
- package/src/state/index.ts +0 -1
- package/src/tui.tsx +0 -25
- package/src/types.ts +0 -63
- package/src/ui/format.ts +0 -67
- package/src/ui/index.ts +0 -2
- package/src/ui/syntax.ts +0 -61
- package/tui.ts +0 -1
package/src/git/runner.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { Context } from "@opencode/plugin/tui/context"
|
|
2
|
-
import { GIT_POLL_MS, GIT_TIMEOUT_MS } from "../config"
|
|
3
|
-
|
|
4
|
-
export function shellQuote(s: string): string {
|
|
5
|
-
return `'${s.replace(/'/g, `'\\''`)}'`
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type GitRunner = {
|
|
9
|
-
enqueue<T>(op: () => Promise<T>): Promise<T>
|
|
10
|
-
run(args: string): Promise<{ exit: number | undefined; output: string }>
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function createGitRunner(ctx: Context, directory: () => string): GitRunner {
|
|
14
|
-
let queue: Promise<void> = Promise.resolve()
|
|
15
|
-
const enqueue = <T,>(op: () => Promise<T>): Promise<T> => {
|
|
16
|
-
const run = queue.then(op)
|
|
17
|
-
queue = run.then(
|
|
18
|
-
() => undefined,
|
|
19
|
-
() => undefined,
|
|
20
|
-
)
|
|
21
|
-
return run
|
|
22
|
-
}
|
|
23
|
-
const run = async (args: string): Promise<{ exit: number | undefined; output: string }> => {
|
|
24
|
-
const location = { directory: directory() }
|
|
25
|
-
const created = await ctx.client.shell.create({
|
|
26
|
-
location,
|
|
27
|
-
command: `git ${args}`,
|
|
28
|
-
cwd: location.directory,
|
|
29
|
-
})
|
|
30
|
-
const id = created.data.id
|
|
31
|
-
try {
|
|
32
|
-
let final = await ctx.client.shell.get({ id, location })
|
|
33
|
-
for (let elapsed = 0; final.data.status === "running" && elapsed < GIT_TIMEOUT_MS; elapsed += GIT_POLL_MS) {
|
|
34
|
-
await new Promise((resolve) => setTimeout(resolve, GIT_POLL_MS))
|
|
35
|
-
final = await ctx.client.shell.get({ id, location })
|
|
36
|
-
}
|
|
37
|
-
if (final.data.status === "running") throw new Error(`Git command timed out after ${GIT_TIMEOUT_MS / 1000}s`)
|
|
38
|
-
const out = await ctx.client.shell.output({ id, location })
|
|
39
|
-
return { exit: final.data.exit, output: out.data.output }
|
|
40
|
-
} finally {
|
|
41
|
-
await ctx.client.shell.remove({ id, location }).catch(() => {})
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return { enqueue, run }
|
|
45
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { For } from "solid-js"
|
|
3
|
-
import type { Context } from "@opencode/plugin/tui/context"
|
|
4
|
-
import { FILE_ROW_BADGE_WIDTH } from "../config"
|
|
5
|
-
import { filePathMaxWidth, statusBadge, truncatePath } from "../ui"
|
|
6
|
-
import type { ChangeFile } from "../types"
|
|
7
|
-
|
|
8
|
-
export function FileList(props: {
|
|
9
|
-
ctx: Context
|
|
10
|
-
files: ChangeFile[]
|
|
11
|
-
staged: Set<string>
|
|
12
|
-
selected: number
|
|
13
|
-
navMode: boolean
|
|
14
|
-
width: number
|
|
15
|
-
onSelect: (file: ChangeFile, index: number) => void
|
|
16
|
-
}) {
|
|
17
|
-
const ctx = props.ctx
|
|
18
|
-
return (
|
|
19
|
-
<box flexDirection="column" gap={0}>
|
|
20
|
-
<For each={props.files}>
|
|
21
|
-
{(f, i) => {
|
|
22
|
-
const b = statusBadge(f.status, ctx.theme)
|
|
23
|
-
const label = () => (props.staged.has(f.file) ? `[${b.label}]` : ` ${b.label} `)
|
|
24
|
-
return (
|
|
25
|
-
<box flexDirection="row" gap={1} onMouseUp={() => props.onSelect(f, i())}>
|
|
26
|
-
<text fg={b.color} width={FILE_ROW_BADGE_WIDTH}>
|
|
27
|
-
<b>{label()}</b>
|
|
28
|
-
</text>
|
|
29
|
-
<text
|
|
30
|
-
fg={props.navMode && i() === props.selected ? ctx.theme.text.base : ctx.theme.text.muted}
|
|
31
|
-
wrapMode="none"
|
|
32
|
-
>
|
|
33
|
-
{truncatePath(
|
|
34
|
-
f.file,
|
|
35
|
-
filePathMaxWidth(props.width, { additions: f.additions, deletions: f.deletions }),
|
|
36
|
-
)}
|
|
37
|
-
</text>
|
|
38
|
-
<box flexGrow={1} />
|
|
39
|
-
<text fg={ctx.theme.diff.text.added}>+{f.additions}</text>
|
|
40
|
-
<text fg={ctx.theme.diff.text.removed}>-{f.deletions}</text>
|
|
41
|
-
</box>
|
|
42
|
-
)
|
|
43
|
-
}}
|
|
44
|
-
</For>
|
|
45
|
-
</box>
|
|
46
|
-
)
|
|
47
|
-
}
|
package/src/sidebar/focus.tsx
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { onCleanup } from "solid-js"
|
|
2
|
-
import type { Context } from "@opencode/plugin/tui/context"
|
|
3
|
-
import { DIFF_PANEL_NAME } from "../config"
|
|
4
|
-
import type { RendererFocusEvents, RenderNode } from "../types"
|
|
5
|
-
|
|
6
|
-
export function bindSidebarFocus(
|
|
7
|
-
ctx: Context,
|
|
8
|
-
opts: { container: () => RenderNode | undefined; active: () => boolean; exit: () => void },
|
|
9
|
-
): void {
|
|
10
|
-
const isInsideSidebar = (node: RenderNode | null | undefined) => {
|
|
11
|
-
const container = opts.container()
|
|
12
|
-
let current = node
|
|
13
|
-
while (current) {
|
|
14
|
-
if (current === container) return true
|
|
15
|
-
current = current.parent
|
|
16
|
-
}
|
|
17
|
-
return false
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const rendererEvents = ctx.renderer as unknown as RendererFocusEvents
|
|
21
|
-
const handleFocusedRenderable = (current: RenderNode | null) => {
|
|
22
|
-
if (!opts.active()) return
|
|
23
|
-
if (ctx.ui.panel.current()?.name === DIFF_PANEL_NAME) return
|
|
24
|
-
if (!isInsideSidebar(current)) opts.exit()
|
|
25
|
-
}
|
|
26
|
-
const handleFocusedEditor = (editor: unknown) => {
|
|
27
|
-
if (editor == null) return
|
|
28
|
-
if (opts.active()) opts.exit()
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
rendererEvents.on("focused_renderable", handleFocusedRenderable)
|
|
32
|
-
rendererEvents.on("focused_editor", handleFocusedEditor)
|
|
33
|
-
onCleanup(() => {
|
|
34
|
-
rendererEvents.off("focused_renderable", handleFocusedRenderable)
|
|
35
|
-
rendererEvents.off("focused_editor", handleFocusedEditor)
|
|
36
|
-
})
|
|
37
|
-
}
|
package/src/sidebar/header.tsx
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { Show } from "solid-js"
|
|
3
|
-
import type { Context } from "@opencode/plugin/tui/context"
|
|
4
|
-
|
|
5
|
-
export function SidebarHeader(props: {
|
|
6
|
-
ctx: Context
|
|
7
|
-
branch: string
|
|
8
|
-
fileCount: number
|
|
9
|
-
ready: boolean
|
|
10
|
-
additions: number
|
|
11
|
-
deletions: number
|
|
12
|
-
navMode: boolean
|
|
13
|
-
stageMode: boolean
|
|
14
|
-
hasStaged: boolean
|
|
15
|
-
onBranchClick: () => void
|
|
16
|
-
onCommit: () => void
|
|
17
|
-
}) {
|
|
18
|
-
const ctx = props.ctx
|
|
19
|
-
return (
|
|
20
|
-
<box flexDirection="column" gap={0}>
|
|
21
|
-
<box flexDirection="row" gap={1}>
|
|
22
|
-
<text fg={ctx.theme.text.base}>
|
|
23
|
-
<b>Git Panel</b>
|
|
24
|
-
</text>
|
|
25
|
-
<box flexGrow={1} />
|
|
26
|
-
<Show when={props.branch && props.fileCount > 0}>
|
|
27
|
-
<text fg={ctx.theme.text.muted} onMouseUp={() => props.onBranchClick()}>
|
|
28
|
-
▶ {props.branch}{" "}
|
|
29
|
-
</text>
|
|
30
|
-
<text fg={ctx.theme.text.base}>({props.fileCount})</text>
|
|
31
|
-
</Show>
|
|
32
|
-
<Show when={props.ready && props.fileCount > 0}>
|
|
33
|
-
<text fg={ctx.theme.diff.text.added}>+{props.additions}</text>
|
|
34
|
-
<text fg={ctx.theme.diff.text.removed}>-{props.deletions}</text>
|
|
35
|
-
</Show>
|
|
36
|
-
</box>
|
|
37
|
-
<Show when={props.ready && props.fileCount > 0 && props.navMode}>
|
|
38
|
-
<box flexDirection="row" gap={1}>
|
|
39
|
-
<box flexGrow={1} />
|
|
40
|
-
<Show
|
|
41
|
-
when={props.stageMode}
|
|
42
|
-
fallback={<text fg={ctx.theme.text.muted}>[← → : stage/unstage]</text>}
|
|
43
|
-
>
|
|
44
|
-
<text fg={ctx.theme.text.feedback.warning.base}>[click : stage/unstage]</text>
|
|
45
|
-
</Show>
|
|
46
|
-
<Show when={props.hasStaged}>
|
|
47
|
-
<text fg={ctx.theme.text.feedback.success.base} onMouseUp={() => props.onCommit()}>
|
|
48
|
-
[c : Commit]
|
|
49
|
-
</text>
|
|
50
|
-
</Show>
|
|
51
|
-
</box>
|
|
52
|
-
</Show>
|
|
53
|
-
</box>
|
|
54
|
-
)
|
|
55
|
-
}
|
package/src/sidebar/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { GitChangesView } from "./view"
|
package/src/sidebar/keymap.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import type { Accessor } from "solid-js"
|
|
2
|
-
import type { Context, KeymapCommand } from "@opencode/plugin/tui/context"
|
|
3
|
-
import { LIST_NAV_MODE } from "../config"
|
|
4
|
-
import type { ChangeFile, FileBinds } from "../types"
|
|
5
|
-
import type { ListNavigation } from "./list-navigation"
|
|
6
|
-
|
|
7
|
-
export type FileActions = {
|
|
8
|
-
next(): void
|
|
9
|
-
prev(): void
|
|
10
|
-
open(): void
|
|
11
|
-
setStaged(stage: boolean): void
|
|
12
|
-
commit(): void
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function buildFileCommands(actions: FileActions, binds: FileBinds, palette: boolean, hasFiles: boolean): KeymapCommand[] {
|
|
16
|
-
return [
|
|
17
|
-
{
|
|
18
|
-
id: "git-panel.next",
|
|
19
|
-
title: "Git Panel: Next File",
|
|
20
|
-
group: "Git Panel",
|
|
21
|
-
bind: binds.next,
|
|
22
|
-
enabled: hasFiles,
|
|
23
|
-
run: () => actions.next(),
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
id: "git-panel.prev",
|
|
27
|
-
title: "Git Panel: Previous File",
|
|
28
|
-
group: "Git Panel",
|
|
29
|
-
bind: binds.prev,
|
|
30
|
-
enabled: hasFiles,
|
|
31
|
-
run: () => actions.prev(),
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
id: "git-panel.open",
|
|
35
|
-
title: "Git Panel: Open Diff",
|
|
36
|
-
group: "Git Panel",
|
|
37
|
-
bind: binds.open,
|
|
38
|
-
palette: palette ? true : undefined,
|
|
39
|
-
enabled: hasFiles,
|
|
40
|
-
run: () => actions.open(),
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
id: "git-panel.stage",
|
|
44
|
-
title: "Git Panel: Stage File",
|
|
45
|
-
group: "Git Panel",
|
|
46
|
-
bind: binds.stage,
|
|
47
|
-
palette: palette ? true : undefined,
|
|
48
|
-
enabled: hasFiles,
|
|
49
|
-
run: () => actions.setStaged(true),
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
id: "git-panel.unstage",
|
|
53
|
-
title: "Git Panel: Unstage File",
|
|
54
|
-
group: "Git Panel",
|
|
55
|
-
bind: binds.unstage,
|
|
56
|
-
palette: palette ? true : undefined,
|
|
57
|
-
enabled: hasFiles,
|
|
58
|
-
run: () => actions.setStaged(false),
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: "git-panel.commit",
|
|
62
|
-
title: "Git Panel: Commit Staged",
|
|
63
|
-
group: "Git Panel",
|
|
64
|
-
bind: binds.commit,
|
|
65
|
-
palette: palette ? true : undefined,
|
|
66
|
-
run: () => actions.commit(),
|
|
67
|
-
},
|
|
68
|
-
]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function registerSidebarKeymaps(
|
|
72
|
-
ctx: Context,
|
|
73
|
-
deps: {
|
|
74
|
-
files: Accessor<ChangeFile[]>
|
|
75
|
-
nav: ListNavigation
|
|
76
|
-
diffOpen: () => boolean
|
|
77
|
-
actions: FileActions
|
|
78
|
-
requestRefresh: () => void
|
|
79
|
-
},
|
|
80
|
-
): void {
|
|
81
|
-
const { files, nav, actions } = deps
|
|
82
|
-
|
|
83
|
-
ctx.keymap.layer(() => {
|
|
84
|
-
const hasFiles = files().length > 0
|
|
85
|
-
return {
|
|
86
|
-
mode: "global",
|
|
87
|
-
bindings: ["git-panel.focus-list", "git-panel.next", "git-panel.prev", "git-panel.open", "git-panel.stage", "git-panel.unstage", "git-panel.commit"],
|
|
88
|
-
commands: [
|
|
89
|
-
{
|
|
90
|
-
id: "git-panel.refresh",
|
|
91
|
-
title: "Git Panel: Refresh",
|
|
92
|
-
group: "Git Panel",
|
|
93
|
-
palette: true,
|
|
94
|
-
run: () => deps.requestRefresh(),
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: "git-panel.focus-list",
|
|
98
|
-
title: "Git Panel: Focus List",
|
|
99
|
-
group: "Git Panel",
|
|
100
|
-
bind: "<leader>G",
|
|
101
|
-
palette: true,
|
|
102
|
-
enabled: hasFiles,
|
|
103
|
-
run: () => nav.enter(),
|
|
104
|
-
},
|
|
105
|
-
...buildFileCommands(actions, {}, true, hasFiles),
|
|
106
|
-
],
|
|
107
|
-
}
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
ctx.keymap.layer(() => {
|
|
111
|
-
const hasFiles = files().length > 0
|
|
112
|
-
const canExitNav = nav.navMode() && !deps.diffOpen() && !nav.stageMode()
|
|
113
|
-
return {
|
|
114
|
-
mode: LIST_NAV_MODE,
|
|
115
|
-
priority: 10,
|
|
116
|
-
bindings: ["git-panel.next", "git-panel.prev", "git-panel.open", "git-panel.stage", "git-panel.unstage", "git-panel.commit", "git-panel.exit-stage-mode", "git-panel.unfocus-list"],
|
|
117
|
-
commands: [
|
|
118
|
-
...buildFileCommands(
|
|
119
|
-
actions,
|
|
120
|
-
{ next: "down", prev: "up", open: "return", stage: "right", unstage: "left", commit: "c" },
|
|
121
|
-
false,
|
|
122
|
-
hasFiles,
|
|
123
|
-
),
|
|
124
|
-
{
|
|
125
|
-
id: "git-panel.exit-stage-mode",
|
|
126
|
-
title: "Git Panel: Exit Stage Mode",
|
|
127
|
-
group: "Git Panel",
|
|
128
|
-
bind: "escape",
|
|
129
|
-
enabled: nav.stageMode(),
|
|
130
|
-
run: () => nav.setStageMode(false),
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
id: "git-panel.unfocus-list",
|
|
134
|
-
title: "Git Panel: Exit List Navigation",
|
|
135
|
-
group: "Git Panel",
|
|
136
|
-
bind: "escape",
|
|
137
|
-
enabled: canExitNav,
|
|
138
|
-
run: () => nav.exit(),
|
|
139
|
-
},
|
|
140
|
-
],
|
|
141
|
-
}
|
|
142
|
-
})
|
|
143
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { createSignal, onCleanup, type Accessor } from "solid-js"
|
|
2
|
-
import type { Context } from "@opencode/plugin/tui/context"
|
|
3
|
-
import { LIST_NAV_MODE } from "../config"
|
|
4
|
-
import type { ChangeFile } from "../types"
|
|
5
|
-
|
|
6
|
-
export type ListNavigation = ReturnType<typeof createListNavigation>
|
|
7
|
-
|
|
8
|
-
export function createListNavigation(ctx: Context, files: Accessor<ChangeFile[]>) {
|
|
9
|
-
const [navMode, setNavMode] = createSignal(false)
|
|
10
|
-
const [stageMode, setStageMode] = createSignal(false)
|
|
11
|
-
const [selected, setSelected] = createSignal(0)
|
|
12
|
-
|
|
13
|
-
let popListNavMode: (() => void) | null = null
|
|
14
|
-
|
|
15
|
-
const pushListNavMode = () => {
|
|
16
|
-
popListNavMode = ctx.keymap.mode.push(LIST_NAV_MODE)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const enter = () => {
|
|
20
|
-
if (popListNavMode) {
|
|
21
|
-
try {
|
|
22
|
-
if (ctx.keymap.mode.current() === LIST_NAV_MODE) return
|
|
23
|
-
} catch {
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
popListNavMode = null
|
|
27
|
-
}
|
|
28
|
-
pushListNavMode()
|
|
29
|
-
setNavMode(true)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const exit = () => {
|
|
33
|
-
try {
|
|
34
|
-
if (ctx.keymap.mode.current() === LIST_NAV_MODE) popListNavMode?.()
|
|
35
|
-
} catch {
|
|
36
|
-
}
|
|
37
|
-
popListNavMode = null
|
|
38
|
-
setNavMode(false)
|
|
39
|
-
setStageMode(false)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const ensure = () => {
|
|
43
|
-
if (!navMode()) return
|
|
44
|
-
try {
|
|
45
|
-
if (ctx.keymap.mode.current() !== LIST_NAV_MODE) {
|
|
46
|
-
popListNavMode = null
|
|
47
|
-
pushListNavMode()
|
|
48
|
-
}
|
|
49
|
-
} catch {
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const selectNext = () => {
|
|
54
|
-
const count = files().length
|
|
55
|
-
if (count > 0) setSelected((prev) => (prev + 1) % count)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const selectPrevious = () => {
|
|
59
|
-
const count = files().length
|
|
60
|
-
if (count > 0) setSelected((prev) => (prev - 1 + count) % count)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const clampSelection = (count: number) => {
|
|
64
|
-
setSelected((prev) => Math.min(prev, Math.max(0, count - 1)))
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const selectedFile = (): ChangeFile | undefined => {
|
|
68
|
-
const list = files()
|
|
69
|
-
return list[Math.min(selected(), list.length - 1)]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const toggleStageMode = () => {
|
|
73
|
-
if (files().length === 0) return
|
|
74
|
-
const next = !stageMode()
|
|
75
|
-
setStageMode(next)
|
|
76
|
-
if (next) enter()
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
onCleanup(exit)
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
navMode,
|
|
83
|
-
stageMode,
|
|
84
|
-
setStageMode,
|
|
85
|
-
selected,
|
|
86
|
-
setSelected,
|
|
87
|
-
enter,
|
|
88
|
-
exit,
|
|
89
|
-
ensure,
|
|
90
|
-
selectNext,
|
|
91
|
-
selectPrevious,
|
|
92
|
-
clampSelection,
|
|
93
|
-
selectedFile,
|
|
94
|
-
toggleStageMode,
|
|
95
|
-
}
|
|
96
|
-
}
|
package/src/sidebar/staging.tsx
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { createMemo } from "solid-js"
|
|
2
|
-
import type { Context } from "@opencode/plugin/tui/context"
|
|
3
|
-
import { STAGED_STORE_KEY } from "../config"
|
|
4
|
-
import { parseStagedPorcelain, shellQuote, type GitRunner } from "../git"
|
|
5
|
-
import { friendlyError } from "../ui"
|
|
6
|
-
|
|
7
|
-
export type Staging = ReturnType<typeof createStaging>
|
|
8
|
-
|
|
9
|
-
export function createStaging(
|
|
10
|
-
ctx: Context,
|
|
11
|
-
git: GitRunner,
|
|
12
|
-
location: () => { directory: string },
|
|
13
|
-
refreshWorkingCopy: () => Promise<void>,
|
|
14
|
-
) {
|
|
15
|
-
const [stagedMem, setStagedMem] = ctx.storage.memory(STAGED_STORE_KEY, {
|
|
16
|
-
initial: { directory: "", files: [] as string[] },
|
|
17
|
-
})
|
|
18
|
-
const staged = createMemo(() => new Set(stagedMem.files))
|
|
19
|
-
|
|
20
|
-
if (stagedMem.directory !== location().directory) {
|
|
21
|
-
setStagedMem((draft) => {
|
|
22
|
-
draft.directory = location().directory
|
|
23
|
-
draft.files = []
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let refreshTask: Promise<void> | undefined
|
|
28
|
-
|
|
29
|
-
const fetchStatuses = async () => {
|
|
30
|
-
try {
|
|
31
|
-
const res = await git.run("status --porcelain=v1 -z -uall --no-renames")
|
|
32
|
-
if (res.exit === 0) {
|
|
33
|
-
const files = [...parseStagedPorcelain(res.output)]
|
|
34
|
-
setStagedMem((draft) => {
|
|
35
|
-
draft.files = files
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
} catch {
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const scheduleRefresh = async () => {
|
|
43
|
-
if (refreshTask) return refreshTask
|
|
44
|
-
refreshTask = git.enqueue(fetchStatuses).finally(() => {
|
|
45
|
-
refreshTask = undefined
|
|
46
|
-
})
|
|
47
|
-
return refreshTask
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const applyChange = (file: string, stage: boolean) => {
|
|
51
|
-
setStagedMem((draft) => {
|
|
52
|
-
const next = new Set(draft.files)
|
|
53
|
-
if (stage) next.add(file)
|
|
54
|
-
else next.delete(file)
|
|
55
|
-
draft.files = [...next]
|
|
56
|
-
})
|
|
57
|
-
void git.enqueue(async () => {
|
|
58
|
-
try {
|
|
59
|
-
let res = await git.run(stage ? `add -- ${shellQuote(file)}` : `restore --staged -- ${shellQuote(file)}`)
|
|
60
|
-
if (!stage && res.exit !== 0 && /could not resolve HEAD|ambiguous argument ['"]?HEAD|invalid object name ['"]?HEAD/i.test(res.output)) {
|
|
61
|
-
res = await git.run(`rm --cached -- ${shellQuote(file)}`)
|
|
62
|
-
}
|
|
63
|
-
if (res.exit !== 0) {
|
|
64
|
-
ctx.ui.toast.show({
|
|
65
|
-
message: `git ${stage ? "add" : "restore"} failed: ${res.output.trim().slice(0, 200)}`,
|
|
66
|
-
variant: "error",
|
|
67
|
-
})
|
|
68
|
-
await fetchStatuses()
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
await fetchStatuses()
|
|
72
|
-
} catch (e) {
|
|
73
|
-
ctx.ui.toast.show({ message: `git failed: ${friendlyError(e)}`, variant: "error" })
|
|
74
|
-
await fetchStatuses()
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const toggle = (file: string) => {
|
|
81
|
-
applyChange(file, !staged().has(file))
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const commit = async () => {
|
|
85
|
-
await scheduleRefresh()
|
|
86
|
-
if (staged().size === 0) {
|
|
87
|
-
ctx.ui.toast.show({ message: "Nothing staged — stage files with → first", variant: "warning" })
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
const message = await ctx.ui.dialog.prompt({ title: "Commit message", placeholder: "Describe the change" })
|
|
91
|
-
if (!message) return
|
|
92
|
-
await git.enqueue(async () => {
|
|
93
|
-
try {
|
|
94
|
-
const res = await git.run(`commit -m ${shellQuote(message)}`)
|
|
95
|
-
if (res.exit !== 0) {
|
|
96
|
-
ctx.ui.toast.show({ message: `git commit failed: ${res.output.trim().slice(0, 200)}`, variant: "error" })
|
|
97
|
-
} else {
|
|
98
|
-
ctx.ui.toast.show({ message: "Committed", variant: "success" })
|
|
99
|
-
}
|
|
100
|
-
} catch (e) {
|
|
101
|
-
ctx.ui.toast.show({ message: `git commit failed: ${friendlyError(e)}`, variant: "error" })
|
|
102
|
-
}
|
|
103
|
-
await refreshWorkingCopy()
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return { staged, scheduleRefresh, applyChange, toggle, commit }
|
|
108
|
-
}
|
package/src/sidebar/status.tsx
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { Show } from "solid-js"
|
|
3
|
-
import type { Context } from "@opencode/plugin/tui/context"
|
|
4
|
-
|
|
5
|
-
export function SidebarStatus(props: {
|
|
6
|
-
ctx: Context
|
|
7
|
-
loading: boolean
|
|
8
|
-
error: string | undefined
|
|
9
|
-
hasFiles: boolean
|
|
10
|
-
branch: string
|
|
11
|
-
}) {
|
|
12
|
-
const ctx = props.ctx
|
|
13
|
-
return (
|
|
14
|
-
<>
|
|
15
|
-
<Show when={props.loading}>
|
|
16
|
-
<text fg={ctx.theme.text.muted}>Loading…</text>
|
|
17
|
-
</Show>
|
|
18
|
-
<Show when={!props.loading && props.error}>
|
|
19
|
-
<text fg={ctx.theme.text.feedback.error.base}>{props.error}</text>
|
|
20
|
-
</Show>
|
|
21
|
-
<Show when={!props.loading && !props.error && !props.hasFiles && !props.branch}>
|
|
22
|
-
<text fg={ctx.theme.text.muted}>Empty repo — no commits yet</text>
|
|
23
|
-
</Show>
|
|
24
|
-
<Show when={!props.loading && !props.error && !props.hasFiles && !!props.branch}>
|
|
25
|
-
<text fg={ctx.theme.text.muted}>Working tree clean</text>
|
|
26
|
-
</Show>
|
|
27
|
-
</>
|
|
28
|
-
)
|
|
29
|
-
}
|