@dsh-xhl/dsh-git-gui 0.1.3
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 +110 -0
- package/cordis.patch.example.yml +6 -0
- package/cordis.patch.yml +10 -0
- package/lib/activity.js +140 -0
- package/lib/client.js +2222 -0
- package/lib/index.js +24 -0
- package/lib/parse.js +298 -0
- package/lib/runner.js +294 -0
- package/lib/service.js +932 -0
- package/package.json +68 -0
- package/scripts/build-client.mjs +110 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lovetree128
|
|
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,110 @@
|
|
|
1
|
+
# `dsh-git-gui`
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="./README.md">English</a> ·
|
|
5
|
+
<a href="./docs/README.zh-CN.md">简体中文</a>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<img alt="Version 0.1.2" src="https://img.shields.io/badge/version-0.1.2-blue">
|
|
10
|
+
<img alt="DeepSeek Harness rc.6" src="https://img.shields.io/badge/dsh-0.1.0--rc.6-skyblue">
|
|
11
|
+
<img alt="MIT License" src="https://img.shields.io/badge/license-MIT-green">
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
A **Git graphical interface plugin** for the DeepSeek Harness Web UI: view, stage, commit, and revert project changes directly in the browser, while seeing every modification the Agent (harness) makes to workspace files in real time.
|
|
15
|
+
|
|
16
|
+
> **Current version: v0.1.2** — Visual operations for viewing changes, committing/reverting, remote synchronization, and more are now available. Branch management, stash, and the AI modification timeline are planned to be completed and released in v0.2.0. See the [Roadmap](#roadmap) below for details.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Changes View**: Changes are grouped into "Staged / Modified / Untracked / Merge Conflicts". Click to view line-level diffs, stage/unstage changes with one click, and discard changes or delete untracked files with confirmation dialogs.
|
|
21
|
+
- **File Tree View**: Browse all files in the workspace (automatically respecting `.gitignore`). File names are color-coded by status — unmodified files use the default text color (black in light mode / white in dark mode), modified but uncommitted files are **blue**, and untracked files are **red**. Supports directory collapsing and name filtering. Click an unmodified file to preview its contents directly.
|
|
22
|
+
- **Log View**: View commit history (hash / reference badges / author / relative time), with support for reverting individual commits using `revert`.
|
|
23
|
+
- **Nested Repository Detection**: When the session workspace itself is not a Git repository, the plugin automatically searches subdirectories for a single nested repository and uses it as the operation root (for example, when the repository is located in the `dsh-git-gui/` subdirectory of the workspace). If multiple nested repositories are found, the plugin explicitly prompts the user instead of guessing which one to use.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
### GitHub Source
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
dsh plugin --profile web add github:wojiaoxiaomayun/dsh-git-gui
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### npm Source
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
dsh plugin --profile web add @dsh-xhl/dsh-git-gui
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
After installation, restart `dsh web` and refresh the browser page.
|
|
40
|
+
|
|
41
|
+
## Update
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
dsh plugin --profile web update @dsh-xhl/dsh-git-gui
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Uninstall
|
|
48
|
+
|
|
49
|
+
```powershell
|
|
50
|
+
dsh plugin --profile web remove @dsh-xhl/dsh-git-gui
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Troubleshooting
|
|
54
|
+
|
|
55
|
+
**The Git panel is stuck on "Detecting repository…"?**
|
|
56
|
+
|
|
57
|
+
This is a known issue when the plugin is installed as a `link:` symlink (e.g. `dsh plugin --profile <name> add <local path>`): the host half resolves `@deepseek-ai/*` from the plugin repo's own `node_modules` instead of the DSH runtime's copies, so the `git/*` endpoint registration is invisible to the gateway.
|
|
58
|
+
|
|
59
|
+
Use the official install methods instead (a real install automatically links the host's packages):
|
|
60
|
+
|
|
61
|
+
```powershell
|
|
62
|
+
dsh plugin --profile web add github:wojiaoxiaomayun/dsh-git-gui
|
|
63
|
+
# or
|
|
64
|
+
dsh plugin --profile web add @dsh-xhl/dsh-git-gui
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To mimic a real install locally, pack a tarball and install that (**do not** add a local directory):
|
|
68
|
+
|
|
69
|
+
```powershell
|
|
70
|
+
npm pack
|
|
71
|
+
dsh plugin --profile web add .\dsh-xhl-dsh-git-gui-0.1.3.tgz
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If you do want a `link:` dev loop, run `scripts/link-host-deps.ps1` after every `npm install` / `pnpm install`, then fully restart DSH.
|
|
75
|
+
|
|
76
|
+
## Where to start
|
|
77
|
+
|
|
78
|
+
After installing the plugin, open the DeepSeek Harness Web UI and click the added button in the bottom-left corner to open the sidebar. Then just enjoy the version control.
|
|
79
|
+
|
|
80
|
+
## Structure
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
Host (Node): GitService (@Remote, namespace `git`, Typert SRC mode → git/* endpoints)
|
|
84
|
+
├─ runner.js spawn git (no shell) + per-workspace queue + output limit + error classification
|
|
85
|
+
├─ parse.js porcelain v2 / unified diff / log / refs / stash parsing
|
|
86
|
+
└─ activity.js session/event → (session, turn, tool, file) timeline (Stage 2)
|
|
87
|
+
|
|
88
|
+
Client (Browser): sidebar.footer.action entry button (uncommitted-count badge) + shell.overlay floating panel
|
|
89
|
+
├─ control.js session cwd synchronization + polling + operation runner + confirmation dialogs
|
|
90
|
+
├─ v-*.js view components (React.createElement, no JSX)
|
|
91
|
+
└─ styles.js theme tokens (--dsw-alias-*) and styles, adaptive to light/dark mode
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Roadmap
|
|
95
|
+
|
|
96
|
+
| Stage | Content | Status |
|
|
97
|
+
|--------------|---|---|
|
|
98
|
+
| v0.1.1 | Changes / file tree / log + commit, stage, discard, revert, **Pull/Push/Fetch + Add Remote**, dark mode, nested repository detection | ✅ Current version |
|
|
99
|
+
| v0.2.0 (unreleased) | **Branch management** (local/remote lists, switch/create, merge, ahead/behind) | 🔧 In progress |
|
|
100
|
+
| v0.2.0 (unreleased) | **Stash** (push/pop/apply/drop) | 🔧 In progress |
|
|
101
|
+
| v0.2.0 (unreleased) | **AI modification timeline** (session × turn × tool → file attribution view) | 🔧 In progress |
|
|
102
|
+
| Future ideas | Reset entry point, file history (`git log -- <path>`), settings card (polling interval / Git path / identity fallback), interactive rebase (PTY) | 💡 To be designed |
|
|
103
|
+
|
|
104
|
+
## Limitations
|
|
105
|
+
|
|
106
|
+
- When panel operations and Agent edits occur concurrently, Git's own state is treated as authoritative. A warning bar is displayed while the session is running, and operations such as discarding changes require confirmation.
|
|
107
|
+
- Untracked file content previews are limited to 512 KiB; diff output is limited to 4 MiB (truncation is marked); the file tree supports a maximum of 3,000 files.
|
|
108
|
+
- Credentials rely on existing Git credential helpers (WinCred / SSH agent). Interactive password input is disabled (`GIT_TERMINAL_PROMPT=0`) to ensure operations do not hang.
|
|
109
|
+
- On Windows, if the repository's `.git` directory was created by a process running with administrator privileges, tools running with normal privileges, such as PyCharm, need to execute `git config --global --add safe.directory '<path>'` (Git 2.35+ hijacking protection).
|
|
110
|
+
- The plugin has not yet been tested on macOS or Linux. Please report any issues you encounter.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @dsh-xhl/dsh-git-gui bundle 补丁层。
|
|
2
|
+
#
|
|
3
|
+
# 当本包出现在 profile 的 dsh.profile.bundles 中时(经
|
|
4
|
+
# `dsh plugin --profile <name> add @dsh-xhl/dsh-git-gui` 由 pnpm 安装后自动对账加入),
|
|
5
|
+
# dsh 启动时会把本补丁叠到配置树上,下面的 insert 行即注册该插件。
|
|
6
|
+
# 未走 dsh plugin 安装(例如 npm --prefix 直接装)时,本层不会被应用,此时由
|
|
7
|
+
# postinstall 钩子把同一行写入 profile 的 cordis.patch.yml 兜底。
|
|
8
|
+
- insert:
|
|
9
|
+
- id: git-gui
|
|
10
|
+
name: '@dsh-xhl/dsh-git-gui'
|
package/lib/activity.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity tracker: attributes filesystem mutations made by the agent to
|
|
3
|
+
* (session, turn, tool) tuples by listening to the session event log.
|
|
4
|
+
*
|
|
5
|
+
* Only the native fs tools (`write`, `edit`) carry exact paths. Shell
|
|
6
|
+
* mutations (bash) cannot be attributed reliably and are intentionally left
|
|
7
|
+
* out — the git status view is the ground truth for those, and the tracker
|
|
8
|
+
* exists to answer "which session/turn touched which file".
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
|
|
13
|
+
const MAX_ENTRIES = 400
|
|
14
|
+
const MAX_ENTRIES_PER_CWD = 200
|
|
15
|
+
|
|
16
|
+
export class ActivityTracker {
|
|
17
|
+
/** @type {Map<string, Array<object>>} key = normalized cwd */
|
|
18
|
+
entries = new Map()
|
|
19
|
+
/** @type {Map<string, object>} pending tool/call by callId */
|
|
20
|
+
pending = new Map()
|
|
21
|
+
|
|
22
|
+
constructor(ctx) {
|
|
23
|
+
this.ctx = ctx
|
|
24
|
+
// `global: true` so the root-level listener also sees events emitted on
|
|
25
|
+
// per-session isolate contexts (every session's tool calls).
|
|
26
|
+
const dispose = ctx.on('session/event', (session, event) => this.onEvent(session, event), { global: true })
|
|
27
|
+
this.dispose = dispose
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onEvent(session, event) {
|
|
31
|
+
if (event === null || typeof event !== 'object') return
|
|
32
|
+
if (event.type === 'tool/call') {
|
|
33
|
+
this.onToolCall(session, event)
|
|
34
|
+
} else if (event.type === 'tool/result') {
|
|
35
|
+
this.onToolResult(event)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onToolCall(session, event) {
|
|
40
|
+
const { name, callId, turn } = event
|
|
41
|
+
if (name !== 'write' && name !== 'edit') return
|
|
42
|
+
let args = null
|
|
43
|
+
try {
|
|
44
|
+
args = JSON.parse(event.arguments ?? '{}')
|
|
45
|
+
} catch {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (args === null || typeof args !== 'object') return
|
|
49
|
+
const filePath = typeof args.file_path === 'string' ? args.file_path : undefined
|
|
50
|
+
if (!filePath) return
|
|
51
|
+
const cwd = this.sessionCwd(session)
|
|
52
|
+
this.pending.set(callId, {
|
|
53
|
+
sessionId: this.sessionId(session),
|
|
54
|
+
turn,
|
|
55
|
+
tool: name,
|
|
56
|
+
filePath,
|
|
57
|
+
cwd,
|
|
58
|
+
at: Date.now(),
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onToolResult(event) {
|
|
63
|
+
const callId = event.callId
|
|
64
|
+
if (callId === undefined) return
|
|
65
|
+
const pending = this.pending.get(callId)
|
|
66
|
+
if (pending === undefined) return
|
|
67
|
+
this.pending.delete(callId)
|
|
68
|
+
if (event.error !== undefined && event.error !== null) return
|
|
69
|
+
const entry = {
|
|
70
|
+
...pending,
|
|
71
|
+
path: this.resolvePath(pending.filePath, pending.cwd),
|
|
72
|
+
error: false,
|
|
73
|
+
}
|
|
74
|
+
delete entry.filePath
|
|
75
|
+
if (pending.cwd) {
|
|
76
|
+
const key = normalizeCwd(pending.cwd)
|
|
77
|
+
const list = this.entries.get(key) ?? []
|
|
78
|
+
list.push(entry)
|
|
79
|
+
if (list.length > MAX_ENTRIES_PER_CWD) list.splice(0, list.length - MAX_ENTRIES_PER_CWD)
|
|
80
|
+
this.entries.set(key, list)
|
|
81
|
+
}
|
|
82
|
+
// global cap across workspaces
|
|
83
|
+
let total = 0
|
|
84
|
+
for (const list of this.entries.values()) total += list.length
|
|
85
|
+
if (total > MAX_ENTRIES) {
|
|
86
|
+
for (const [key, list] of this.entries) {
|
|
87
|
+
const overflow = total - MAX_ENTRIES
|
|
88
|
+
if (overflow <= 0) break
|
|
89
|
+
const drop = Math.min(list.length, overflow)
|
|
90
|
+
list.splice(0, drop)
|
|
91
|
+
total -= drop
|
|
92
|
+
if (list.length === 0) this.entries.delete(key)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
sessionId(session) {
|
|
98
|
+
return typeof session?.id === 'string' ? session.id : 'unknown'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
sessionCwd(session) {
|
|
102
|
+
const cwd = session?.header?.cwd
|
|
103
|
+
return typeof cwd === 'string' && cwd !== '' ? cwd : undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
resolvePath(filePath, cwd) {
|
|
107
|
+
if (cwd && !path.isAbsolute(filePath)) return path.join(cwd, filePath)
|
|
108
|
+
return filePath
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Timeline entries for one workspace (most recent first).
|
|
113
|
+
* @param {string} cwd
|
|
114
|
+
* @param {number} limit
|
|
115
|
+
* @returns {Array<object>}
|
|
116
|
+
*/
|
|
117
|
+
list(cwd, limit = 100) {
|
|
118
|
+
const list = this.entries.get(normalizeCwd(cwd)) ?? []
|
|
119
|
+
const capped = Math.max(1, Math.min(Number(limit) || 100, 200))
|
|
120
|
+
return [...list].reverse().slice(0, capped).map((e) => ({
|
|
121
|
+
sessionId: e.sessionId,
|
|
122
|
+
turn: e.turn,
|
|
123
|
+
tool: e.tool,
|
|
124
|
+
path: e.path,
|
|
125
|
+
at: e.at,
|
|
126
|
+
}))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
dispose() {
|
|
130
|
+
this.dispose?.()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeCwd(cwd) {
|
|
135
|
+
try {
|
|
136
|
+
return path.resolve(cwd).toLowerCase()
|
|
137
|
+
} catch {
|
|
138
|
+
return String(cwd).toLowerCase()
|
|
139
|
+
}
|
|
140
|
+
}
|