@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 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.
@@ -0,0 +1,6 @@
1
+ # 手动安装时追加到 $DSH_HOME/profiles/web/cordis.patch.yml(用户补丁层)
2
+ # 每行 id 用于后续覆盖/禁用;host 行与客户端图在 dsh web 重启后生效。
3
+ # 注意:bundle 安装(dsh plugin add)不需要这一行 —— 补丁层会自动注册。
4
+ - insert:
5
+ - id: git-gui
6
+ name: '@dsh-xhl/dsh-git-gui'
@@ -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'
@@ -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
+ }