@windypro-rourou/dsh-code-studio 0.1.1 → 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/README.md +32 -31
- package/lib/client.js +78 -5
- package/lib/index.js +114 -16
- package/package.json +56 -56
package/README.md
CHANGED
|
@@ -1,42 +1,43 @@
|
|
|
1
|
-
# Code Studio
|
|
1
|
+
# Code Studio — DSH Web UI 文件修改监视器 · 代码工作台
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
> A **file-change monitor & code editor** inside the DeepSeek Harness Web UI.
|
|
4
|
+
> Watch agent edits land as line-by-line diffs in real time, right beside your
|
|
5
|
+
> conversation — then open, edit and save any workspace file without leaving
|
|
6
|
+
> the browser.
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
## 瀹夎锛堝凡瑁呭ソ锛?
|
|
8
|
-
鎻掍欢宸插畨瑁呭埌 web profile锛?DSH_HOME/profiles/web锛夛細
|
|
9
|
-
- 鍖呬綅浜?profiles/web/node_modules/@windypro-rourou/dsh-code-studio
|
|
10
|
-
- cordis.patch.yml 宸叉彃鍏?code-studio 琛?- package.json 宸插姞鍏?link:F:/CycleMaster/dsh-code-studio 渚濊禆
|
|
8
|
+
在 DeepSeek Harness 的 Web 界面里,实时监视 Agent 对文件的每一次修改:**改动落盘的瞬间**,逐行 Diff(`+` 新增 / `−` 删除 / `~` 修改)自动浮现在你的会话旁;内置语法高亮编辑器,随时查看和修改工作区文件。**不用离开 DSH,Agent 改了哪里、改了什么,一目了然。**
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
### 鎵嬪姩瀹夎锛堝叾浠栨満鍣級
|
|
10
|
+
## 核心价值
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
- 🔭 **实时文件修改监视**:基于会话工具事件直推,Agent 每次 write / edit 完成立即推送 Diff —— 不依赖文件系统轮询,不漏报、不延迟。
|
|
13
|
+
- 🗂 **多工作区覆盖**:自动监视所有会话的工作区目录,Agent 在任意目录改文件都能捕获。
|
|
14
|
+
- 🔒 **按会话隔离**:每个 Code Studio 只响应当前会话的修改,切换会话互不干扰。
|
|
15
|
+
- 🧠 **会话状态记忆**:变更列表、打开的标签、面板宽度按会话分别保留(本地持久化)。
|
|
16
|
+
- 📝 **语法高亮编辑器**:类编辑器体验,行号、光标、滚动同步;`Ctrl+S` 保存,`Ctrl+D` 查看 Diff。
|
|
17
|
+
- 📐 **可拖拽面板**:宽度自由调整并记住;UI 与 DSH 主题完全一致(`--dsw-alias-*` 令牌)。
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
# - id: code-studio
|
|
21
|
-
# name: '@windypro-rourou/dsh-code-studio'
|
|
22
|
-
```
|
|
19
|
+
## 安装
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
```sh
|
|
22
|
+
dsh plugin --profile web add @windypro-rourou/dsh-code-studio
|
|
23
|
+
# 或使用 GitHub 源
|
|
24
|
+
dsh plugin --profile web add github:WindyPro-rourou/dsh-code-studio
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
重启 `dsh web` 后,左侧边栏出现 **Code Studio** 入口。
|
|
28
|
+
|
|
29
|
+
## 使用
|
|
30
|
+
|
|
31
|
+
1. 打开 Code Studio(右侧面板,可拖左缘调宽)。
|
|
32
|
+
2. 让 Agent 修改代码 —— 面板**自动浮现**该文件的逐行 Diff:行号前 `+`(绿)/ `−`(红)/ `~`(黄),未改动大段自动折叠。
|
|
33
|
+
3. 「文件」页签:浏览工作区、打开文件、直接编辑保存(`Ctrl+S`)。
|
|
34
|
+
|
|
35
|
+
## 技术说明
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
| 鎸夐敭 | 鍔熻兘 |
|
|
34
|
-
| --- | --- |
|
|
35
|
-
| Ctrl+S | 淇濆瓨褰撳墠鏂囦欢 |
|
|
36
|
-
| Ctrl+D | 鍒囨崲 缂栬緫 / Diff 瑙嗗浘 |
|
|
37
|
+
- **Host**(`lib/index.js`):监听 `session/event` 工具事件(`tool/call` ↔ `tool/result` 配对),对写文件工具即时读取并推送 before/after;递归文件监视 + mtime 轮询兜底;`/api/code-studio/*` REST + SSE。
|
|
38
|
+
- **Client**(`lib/client.js`):浏览器 bundle,仅依赖 react;LCS 行级 Diff 引擎、语法高亮、按会话状态管理。
|
|
37
39
|
|
|
38
|
-
##
|
|
39
|
-
- Host锛歭ib/index.js 鈥?webServer.register 娉ㄥ唽 6 涓矾鐢憋紱fs.watch(recursive) + 1500ms mtime 杞锛涘姣忎釜鏂囦欢缁存姢鍩虹嚎蹇収锛屼粠鑰屼骇鍑虹簿纭殑 before/after銆?- Client锛歭ib/client.js 鈥?绾祻瑙堝櫒 bundle锛坵indow.__ModuleLoader__.load锛夛紝浠呬緷璧?react/react-dom锛涜嚜甯?LCS 琛岀骇 diff 寮曟搸涓庢牱寮忋€?- 璺緞瀹夊叏锛欰PI 浠呮帴鍙?loopback + 鍚屾簮鏍囪璇锋眰銆?
|
|
40
|
-
## 宸茬煡闄愬埗
|
|
40
|
+
## 已知限制
|
|
41
41
|
|
|
42
|
-
-
|
|
42
|
+
- 单文件 > 512KB 不读取内容(防浏览器卡顿)。
|
|
43
|
+
- 通过 bash/pwsh 等非文件工具写入的变更依赖文件监视兜底(仍会捕获,可能有少量延迟)。
|
package/lib/client.js
CHANGED
|
@@ -166,6 +166,28 @@ window.__ModuleLoader__.load({
|
|
|
166
166
|
}
|
|
167
167
|
function subscribePanel(fn) { panel.listeners.add(fn); return () => panel.listeners.delete(fn); }
|
|
168
168
|
|
|
169
|
+
// current session id (per-session change isolation)
|
|
170
|
+
let currentSessionId = null;
|
|
171
|
+
const sessListeners = new Set();
|
|
172
|
+
function setSessionId(v) {
|
|
173
|
+
currentSessionId = v;
|
|
174
|
+
for (const fn of sessListeners) fn(v);
|
|
175
|
+
}
|
|
176
|
+
function subscribeSession(fn) { sessListeners.add(fn); return () => sessListeners.delete(fn); }
|
|
177
|
+
|
|
178
|
+
// per-session UI state memory (changes, tabs, width), persisted locally
|
|
179
|
+
const SESS_STATE_KEY = "cs.sessionStates.v1";
|
|
180
|
+
function loadSessionStates() {
|
|
181
|
+
try {
|
|
182
|
+
const raw = globalThis.localStorage?.getItem(SESS_STATE_KEY);
|
|
183
|
+
if (raw) return JSON.parse(raw);
|
|
184
|
+
} catch { /* ignore */ }
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
function persistSessionStates(map) {
|
|
188
|
+
try { globalThis.localStorage?.setItem(SESS_STATE_KEY, JSON.stringify(map)); } catch { /* ignore */ }
|
|
189
|
+
}
|
|
190
|
+
|
|
169
191
|
// ============================ CSS ============================
|
|
170
192
|
const CSS = [
|
|
171
193
|
".cs-root{position:absolute;top:0;right:0;bottom:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1,#151922);color:var(--dsw-alias-label-primary,#d7dde6);border-left:1px solid var(--dsw-alias-border-l1,#262a33);font:13px/1.55 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;z-index:30;box-shadow:-12px 0 32px rgba(0,0,0,.18);animation:csSlideIn .18s ease-out;--cs-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,\"Liberation Mono\",monospace}",
|
|
@@ -408,6 +430,32 @@ window.__ModuleLoader__.load({
|
|
|
408
430
|
});
|
|
409
431
|
const dragRef = React.useRef(null);
|
|
410
432
|
const changedSet = React.useMemo(() => new Set(changes.map((c) => c.path)), [changes]);
|
|
433
|
+
// current session for per-session isolation
|
|
434
|
+
const [sessId, setSessId] = React.useState(currentSessionId);
|
|
435
|
+
const sessIdRef = React.useRef(currentSessionId);
|
|
436
|
+
// per-session UI memory: save current snapshot on switch, restore target's
|
|
437
|
+
const sessStatesRef = React.useRef(loadSessionStates());
|
|
438
|
+
const snapRef = React.useRef({ changes, expandedPath, editor, tab, width });
|
|
439
|
+
snapRef.current = { changes, expandedPath, editor, tab, width };
|
|
440
|
+
React.useEffect(() => subscribeSession((v) => {
|
|
441
|
+
const cur = sessIdRef.current;
|
|
442
|
+
if (cur && cur !== v) {
|
|
443
|
+
sessStatesRef.current[cur] = snapRef.current;
|
|
444
|
+
persistSessionStates(sessStatesRef.current);
|
|
445
|
+
}
|
|
446
|
+
sessIdRef.current = v;
|
|
447
|
+
setSessId(v);
|
|
448
|
+
const st = sessStatesRef.current[v];
|
|
449
|
+
if (st) {
|
|
450
|
+
setChanges(st.changes ?? []);
|
|
451
|
+
setExpandedPath(st.expandedPath ?? null);
|
|
452
|
+
setEditor(st.editor ?? null);
|
|
453
|
+
setTab(st.tab ?? "changes");
|
|
454
|
+
setWidth(st.width ?? 400);
|
|
455
|
+
} else {
|
|
456
|
+
setChanges([]); setExpandedPath(null); setEditor(null); setTab("changes");
|
|
457
|
+
}
|
|
458
|
+
}), []);
|
|
411
459
|
|
|
412
460
|
React.useEffect(() => subscribePanel(setOpen), []);
|
|
413
461
|
React.useEffect(() => {
|
|
@@ -435,11 +483,16 @@ window.__ModuleLoader__.load({
|
|
|
435
483
|
try {
|
|
436
484
|
const ev = JSON.parse(msg.data);
|
|
437
485
|
if (!ev || typeof ev.path !== "string") return;
|
|
486
|
+
// per-session isolation: only react to changes owned by the active session
|
|
487
|
+
// (or unattributed ones, treated as the current session's/user's own edits)
|
|
488
|
+
// per-session isolation: only filter when we KNOW the active session.
|
|
489
|
+
// If current is null, do not filter - otherwise edits would never appear.
|
|
490
|
+
if (ev.sessionId && sessIdRef.current && ev.sessionId !== sessIdRef.current) return;
|
|
438
491
|
setConn("ok"); setConnText("已连接");
|
|
439
492
|
const name = ev.path.split(/[\\/]/).pop() || ev.path;
|
|
440
493
|
setChanges((prev) => {
|
|
441
494
|
const next = prev.filter((c) => c.path !== ev.path);
|
|
442
|
-
next.unshift({ path: ev.path, name, before: ev.before, after: ev.after, deleted: !!ev.deleted, ts: ev.ts });
|
|
495
|
+
next.unshift({ path: ev.path, name, before: ev.before, after: ev.after, deleted: !!ev.deleted, ts: ev.ts, sessionId: ev.sessionId });
|
|
443
496
|
return next.slice(0, 50);
|
|
444
497
|
});
|
|
445
498
|
setPanelOpen(true);
|
|
@@ -487,12 +540,17 @@ window.__ModuleLoader__.load({
|
|
|
487
540
|
setTab("changes");
|
|
488
541
|
setExpandedPath(path);
|
|
489
542
|
historyApi(path).then((data) => {
|
|
490
|
-
const
|
|
491
|
-
if (
|
|
543
|
+
const events = data.events && data.events.length > 0 ? data.events : [];
|
|
544
|
+
if (events.length === 0) return;
|
|
545
|
+
// prefer the latest event owned by the active session; fall back to the latest overall
|
|
546
|
+
let last = events[events.length - 1];
|
|
547
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
548
|
+
if (events[i].sessionId && events[i].sessionId === sessIdRef.current) { last = events[i]; break; }
|
|
549
|
+
}
|
|
492
550
|
const name = path.split(/[\\/]/).pop() || path;
|
|
493
551
|
setChanges((prev) => {
|
|
494
552
|
if (prev.some((c) => c.path === path)) return prev;
|
|
495
|
-
return [{ path, name, before: last.before, after: last.after, deleted: !!last.deleted, ts: last.ts }, ...prev];
|
|
553
|
+
return [{ path, name, before: last.before, after: last.after, deleted: !!last.deleted, ts: last.ts, sessionId: last.sessionId }, ...prev];
|
|
496
554
|
});
|
|
497
555
|
}).catch(() => {});
|
|
498
556
|
};
|
|
@@ -513,7 +571,10 @@ window.__ModuleLoader__.load({
|
|
|
513
571
|
if (!dragRef.current) return;
|
|
514
572
|
dragRef.current = null;
|
|
515
573
|
e.currentTarget.classList.remove("cs-drag");
|
|
516
|
-
|
|
574
|
+
if (sessIdRef.current) {
|
|
575
|
+
sessStatesRef.current[sessIdRef.current] = { ...snapRef.current, width };
|
|
576
|
+
persistSessionStates(sessStatesRef.current);
|
|
577
|
+
}
|
|
517
578
|
};
|
|
518
579
|
|
|
519
580
|
if (!open) {
|
|
@@ -596,6 +657,7 @@ window.__ModuleLoader__.load({
|
|
|
596
657
|
el("span", { className: "cs-sdot cs-" + conn }),
|
|
597
658
|
el("span", null, connText),
|
|
598
659
|
el("span", { style: { marginLeft: "auto" } }, "变更 " + changes.length + " 文件"),
|
|
660
|
+
sessId ? el("span", { title: "仅显示当前会话的变更" }, "会话 " + String(sessId).slice(-6)) : null,
|
|
599
661
|
el("span", null, editor && editor.dirty ? "未保存" : "就绪")
|
|
600
662
|
)
|
|
601
663
|
);
|
|
@@ -662,6 +724,17 @@ window.__ModuleLoader__.load({
|
|
|
662
724
|
} catch (error) {
|
|
663
725
|
console.error("[code-studio] sidebar entry failed", error);
|
|
664
726
|
}
|
|
727
|
+
// track the active session so the panel only reacts to this session's edits
|
|
728
|
+
try {
|
|
729
|
+
const sessions = ctx.get("sessions");
|
|
730
|
+
if (sessions && typeof sessions.list?.getSnapshot === "function" && typeof sessions.list.subscribe === "function") {
|
|
731
|
+
const sync = () => { try { setSessionId(sessions.list.getSnapshot()?.current ?? null); } catch { /* ignore */ } };
|
|
732
|
+
disposers.push(sessions.list.subscribe(sync));
|
|
733
|
+
sync();
|
|
734
|
+
}
|
|
735
|
+
} catch (error) {
|
|
736
|
+
console.error("[code-studio] session tracking failed", error);
|
|
737
|
+
}
|
|
665
738
|
try {
|
|
666
739
|
const slots = ctx.get("slots");
|
|
667
740
|
if (slots !== void 0) {
|
package/lib/index.js
CHANGED
|
@@ -61,7 +61,9 @@ function safePath(raw) {
|
|
|
61
61
|
|
|
62
62
|
class FileLedger {
|
|
63
63
|
constructor(root) {
|
|
64
|
-
|
|
64
|
+
/** watched workspace roots (one recursive watcher each) */
|
|
65
|
+
this.roots = new Set();
|
|
66
|
+
this.watchers = new Map();
|
|
65
67
|
/** baseline cache: absolute path -> { content, mtimeMs, size } */
|
|
66
68
|
this.baseline = new Map();
|
|
67
69
|
/** change history: absolute path -> [{ ts, before, after, deleted }] */
|
|
@@ -69,7 +71,41 @@ class FileLedger {
|
|
|
69
71
|
/** SSE subscribers */
|
|
70
72
|
this.subscribers = new Set();
|
|
71
73
|
this.debounce = new Map();
|
|
72
|
-
|
|
74
|
+
/** file path -> owning session { sessionId, ts } (learnt from tool/call events) */
|
|
75
|
+
this.owners = new Map();
|
|
76
|
+
if (root) this.addRoot(root);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Add one workspace root (recursive watcher). Idempotent. */
|
|
80
|
+
addRoot(root) {
|
|
81
|
+
if (typeof root !== "string" || root === "") return;
|
|
82
|
+
let resolved;
|
|
83
|
+
try { resolved = realpathSync(root); } catch { return; }
|
|
84
|
+
if (this.roots.has(resolved)) return;
|
|
85
|
+
this.roots.add(resolved);
|
|
86
|
+
try {
|
|
87
|
+
const watcher = fsWatch(resolved, { recursive: true }, (_eventType, filename) => {
|
|
88
|
+
if (typeof filename !== "string") return;
|
|
89
|
+
this.schedule(join(resolved, filename));
|
|
90
|
+
});
|
|
91
|
+
watcher.on?.("error", () => { /* poll covers us */ });
|
|
92
|
+
this.watchers.set(resolved, watcher);
|
|
93
|
+
} catch { /* poll covers us */ }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Attribute a file to a session (from a session tool/call event). */
|
|
97
|
+
claim(path, sessionId) {
|
|
98
|
+
if (typeof path !== "string" || path === "" || typeof sessionId !== "string") return;
|
|
99
|
+
this.owners.set(path, { sessionId, ts: Date.now() });
|
|
100
|
+
if (this.owners.size > 2000) {
|
|
101
|
+
const oldest = [...this.owners.entries()].sort((a, b) => a[1].ts - b[1].ts)[0];
|
|
102
|
+
if (oldest) this.owners.delete(oldest[0]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Owning session for a path, if known. */
|
|
107
|
+
ownerOf(path) {
|
|
108
|
+
return this.owners.get(path)?.sessionId ?? null;
|
|
73
109
|
}
|
|
74
110
|
|
|
75
111
|
snapshot(path) {
|
|
@@ -111,8 +147,9 @@ class FileLedger {
|
|
|
111
147
|
if (error.code === "ENOENT") {
|
|
112
148
|
const before = this.baseline.get(path);
|
|
113
149
|
this.baseline.delete(path);
|
|
114
|
-
|
|
115
|
-
this.
|
|
150
|
+
const ev = { path, ts: Date.now(), before: before?.content ?? null, after: null, deleted: true, sessionId: this.ownerOf(path) };
|
|
151
|
+
this.push(ev);
|
|
152
|
+
this.addHistory(path, ev);
|
|
116
153
|
return;
|
|
117
154
|
}
|
|
118
155
|
return;
|
|
@@ -121,7 +158,7 @@ class FileLedger {
|
|
|
121
158
|
const before = prior && !prior.tooLarge ? prior.content : null;
|
|
122
159
|
this.baseline.set(path, { ...current });
|
|
123
160
|
if (prior && prior.mtimeMs === current.mtimeMs && prior.size === current.size && prior.content === current.content) return;
|
|
124
|
-
const event = { path, ts: Date.now(), before, after: current.tooLarge ? null : current.content, deleted: false, tooLarge: current.tooLarge };
|
|
161
|
+
const event = { path, ts: Date.now(), before, after: current.tooLarge ? null : current.content, deleted: false, tooLarge: current.tooLarge, sessionId: this.ownerOf(path) };
|
|
125
162
|
this.push(event);
|
|
126
163
|
this.addHistory(path, event);
|
|
127
164
|
}
|
|
@@ -163,21 +200,14 @@ class FileLedger {
|
|
|
163
200
|
}
|
|
164
201
|
|
|
165
202
|
start() {
|
|
166
|
-
|
|
167
|
-
this.watcher = fsWatch(this.root, { recursive: true }, (_eventType, filename) => {
|
|
168
|
-
if (typeof filename !== "string") return;
|
|
169
|
-
this.schedule(join(this.root, filename));
|
|
170
|
-
});
|
|
171
|
-
this.watcher.on?.("error", () => { /* recursive watch may fail on some mounts; poll covers us */ });
|
|
172
|
-
} catch (error) {
|
|
173
|
-
this.watcher = void 0;
|
|
174
|
-
}
|
|
203
|
+
for (const root of this.roots) this.addRoot(root);
|
|
175
204
|
}
|
|
176
205
|
|
|
177
206
|
stop() {
|
|
178
207
|
for (const timer of this.debounce.values()) clearTimeout(timer);
|
|
179
208
|
this.debounce.clear();
|
|
180
|
-
|
|
209
|
+
for (const watcher of this.watchers.values()) { try { watcher.close(); } catch { /* ignore */ } }
|
|
210
|
+
this.watchers.clear();
|
|
181
211
|
for (const res of this.subscribers) { try { res.destroy(); } catch { /* ignore */ } }
|
|
182
212
|
this.subscribers.clear();
|
|
183
213
|
}
|
|
@@ -277,7 +307,8 @@ function makeRoutes(ledger, root, cwd) {
|
|
|
277
307
|
before: ledger.snapshot(path)?.content ?? null,
|
|
278
308
|
after: body.content,
|
|
279
309
|
deleted: false,
|
|
280
|
-
source: "user"
|
|
310
|
+
source: "user",
|
|
311
|
+
sessionId: ledger.ownerOf(path)
|
|
281
312
|
};
|
|
282
313
|
ledger.recordBaseline(path, body.content, mtimeMs, body.content.length);
|
|
283
314
|
ledger.push(event);
|
|
@@ -322,6 +353,21 @@ function makeRoutes(ledger, root, cwd) {
|
|
|
322
353
|
|
|
323
354
|
/* ---------- plugin ---------- */
|
|
324
355
|
|
|
356
|
+
/** Tool names whose arguments carry a file path we can attribute to a session. */
|
|
357
|
+
const PATH_TOOLS = new Set(["write", "edit", "str-replace", "apply-patch", "fs-write", "write-file"]);
|
|
358
|
+
const PATH_ARGS = ["file_path", "path", "filePath", "target", "file"];
|
|
359
|
+
|
|
360
|
+
/** Resolve a tool argument to an absolute path, anchoring relative paths at the session cwd. */
|
|
361
|
+
function toolPath(args, sessionCwd) {
|
|
362
|
+
for (const key of PATH_ARGS) {
|
|
363
|
+
const p = args?.[key];
|
|
364
|
+
if (typeof p === "string" && p !== "") {
|
|
365
|
+
return isAbsolute(p) ? resolve(p) : resolve(sessionCwd ?? process.cwd(), p);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return void 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
325
371
|
function apply(ctx, config) {
|
|
326
372
|
const root = realpathSync((config?.root && config.root !== "" ? config.root : process.env.DSH_WORKSPACE ?? process.cwd()));
|
|
327
373
|
const cwd = process.cwd();
|
|
@@ -335,6 +381,58 @@ function apply(ctx, config) {
|
|
|
335
381
|
const timer = setInterval(() => ledger.poll(), pollIntervalMs);
|
|
336
382
|
timer.unref();
|
|
337
383
|
disposers.push(() => clearInterval(timer));
|
|
384
|
+
// Watch every session's workspace, not just our cwd: agents edit files
|
|
385
|
+
// in whichever directory their session was started in.
|
|
386
|
+
const addSessionRoot = (session) => {
|
|
387
|
+
if (session?.header?.cwd) ledger.addRoot(session.header.cwd);
|
|
388
|
+
};
|
|
389
|
+
try {
|
|
390
|
+
const sessions = ctx.get("sessions");
|
|
391
|
+
if (sessions && typeof sessions.list === "function") {
|
|
392
|
+
for (const session of sessions.list()) addSessionRoot(session);
|
|
393
|
+
}
|
|
394
|
+
} catch { /* ignore */ }
|
|
395
|
+
// ---- reliable real-time change detection from tool events ----
|
|
396
|
+
// tool/call records { name, arguments, callId } per session; tool/result
|
|
397
|
+
// pairs by callId. On a file-writing tool completing, read the file NOW
|
|
398
|
+
// and push the change immediately (fs.watch on atomic writes/renames is
|
|
399
|
+
// unreliable and late), attributing it to the owning session.
|
|
400
|
+
const pendingCalls = new Map(); // callId -> { name, args, sessionId, cwd }
|
|
401
|
+
const offSession = ctx.on("session/event", (session, event) => {
|
|
402
|
+
addSessionRoot(session);
|
|
403
|
+
const data = event?.data;
|
|
404
|
+
if (!data || typeof data !== "object") return;
|
|
405
|
+
if (event.type === "tool/call") {
|
|
406
|
+
if (typeof data.name === "string" && data.arguments && typeof data.arguments === "object") {
|
|
407
|
+
pendingCalls.set(String(data.callId), {
|
|
408
|
+
name: data.name,
|
|
409
|
+
args: data.arguments,
|
|
410
|
+
sessionId: session?.id,
|
|
411
|
+
cwd: session?.header?.cwd
|
|
412
|
+
});
|
|
413
|
+
if (pendingCalls.size > 500) {
|
|
414
|
+
const oldest = pendingCalls.keys().next().value;
|
|
415
|
+
if (oldest !== void 0) pendingCalls.delete(oldest);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (event.type === "tool/result") {
|
|
421
|
+
const callId = data.message?.source?.callId ?? data.callId ?? data.call_id;
|
|
422
|
+
const call = callId !== void 0 ? pendingCalls.get(String(callId)) : void 0;
|
|
423
|
+
if (call === void 0) return;
|
|
424
|
+
pendingCalls.delete(String(callId));
|
|
425
|
+
if (!PATH_TOOLS.has(call.name)) return;
|
|
426
|
+
const abs = toolPath(call.args, call.cwd);
|
|
427
|
+
if (abs === void 0) return;
|
|
428
|
+
ledger.claim(abs, call.sessionId);
|
|
429
|
+
// proactive immediate read+push; fs.watch remains a fallback for
|
|
430
|
+
// writes that bypass tools (bash, user edits)
|
|
431
|
+
void ledger.handleChange(abs);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
disposers.push(offSession);
|
|
338
436
|
} catch (error) {
|
|
339
437
|
for (const dispose of disposers) dispose();
|
|
340
438
|
ledger.stop();
|
package/package.json
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@windypro-rourou/dsh-code-studio",
|
|
3
|
+
"description": "File-change monitor \u0026 code editor for the DSH Web UI: watch agent edits land as line-by-line diffs in real time, right beside your conversation, then view and edit any workspace file without leaving the browser.",
|
|
4
|
+
"version": "0.1.3",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"types": "./lib/types/client/index.d.ts",
|
|
15
|
+
"default": "./lib/client.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"dsh": {
|
|
20
|
+
"bundle": {
|
|
21
|
+
"patch": "./cordis.patch.yml"
|
|
22
|
+
},
|
|
23
|
+
"client": {
|
|
24
|
+
"inject": [
|
|
25
|
+
"@deepseek-ai/dsh-client-runtime"
|
|
26
|
+
],
|
|
27
|
+
"platform": "web"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"dsh-plugin",
|
|
32
|
+
"dsh",
|
|
33
|
+
"deepseek-harness",
|
|
34
|
+
"code-studio",
|
|
35
|
+
"cline",
|
|
36
|
+
"diff",
|
|
37
|
+
"editor",
|
|
38
|
+
"web-ui"
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/windypro-rourou/dsh-code-studio.git"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
46
|
+
"react": "^18.2.0"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"lib/**/*.js",
|
|
50
|
+
"lib/**/*.d.ts",
|
|
51
|
+
"cordis.patch.yml",
|
|
52
|
+
"README.md",
|
|
53
|
+
"scripts"
|
|
54
|
+
],
|
|
55
|
+
"license": "MIT"
|
|
56
|
+
}
|