@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 CHANGED
@@ -1,42 +1,43 @@
1
- # Code Studio 鈥?DSH Web GUI 鐨?VS Code + Cline 娣峰悎鎻掍欢
1
+ # Code Studio DSH Web UI 文件修改监视器 · 代码工作台
2
2
 
3
- 鍦?DeepSeek Harness 鐨?Web 鐣岄潰锛坵ebui锛夐噷鐩存帴**娴忚銆佺紪杈戜唬鐮?*锛屽苟鍦?**Agent 淇敼鏂囦欢鏃跺儚 Cline 涓€鏍烽€愯鏄剧ず Diff**锛氭敼鍔ㄥ尯琛屽彿鍓嶆爣娉?`+`锛堟柊澧?缁胯壊锛夈€乣-`锛堝垹闄?绾㈣壊锛夈€乣~`锛堜慨鏀?榛勮壊锛夈€?
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
- - 鏂囦欢鏍戯細宸︿晶鎳掑姞杞界洰褰曟爲锛岀偣鍑绘枃浠舵墦寮€锛堟敮鎸佹墦寮€宸ヤ綔鍖轰换鎰忚矾寰勶級銆?- 缂栬緫鍣細甯﹁鍙风殑浠g爜缂栬緫瑙嗗浘锛汣trl+S 淇濆瓨锛孋trl+D 鍦?缂栬緫/Diff 闂村垏鎹€?- Cline 寮?Diff锛欴iff 瑙嗗浘鎸夎鏄剧ず before/after锛涜鍙峰墠鏂规湁绗﹀彿鍒楋紙+ - ~锛夛紝鏈敼鍔ㄥぇ娈佃嚜鍔ㄦ姌鍙犱负銆屸嫰 N 琛屾湭鏀瑰姩銆嶏紝鍙偣鍑诲睍寮€鍏ㄩ儴銆?- Agent 鍙樻洿璺熻釜锛欻ost 渚ч€掑綊鐩戝惉宸ヤ綔鍖猴紙fs.watch + mtime 杞鍏滃簳锛夛紝Agent 鍐欏叆鏂囦欢鍚庢祻瑙堝櫒瀹炴椂鏀跺埌浜嬩欢锛涙枃浠舵爲椤堕儴銆孉gent 鍙樻洿銆嶅垪琛ㄦ樉绀烘湁鍙樻洿鐨勬枃浠讹紙缁?鏂板缓銆侀粍=淇敼銆佺孩=鍒犻櫎锛夛紝鐐瑰嚮鍗崇湅 Diff銆?- 鍙樻洿鍘嗗彶锛氭瘡涓枃浠朵繚鐣欐渶杩?30 鏉?before/after 璁板綍锛?api/code-studio/history锛夈€?- 鍙岀鎻掍欢锛欻ost锛圢ode锛夋彁渚?/api/code-studio/* REST + SSE锛汣lient锛堟祻瑙堝櫒 bundle锛夋彁渚?UI銆?
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
- **鐢熸晥鏃舵満**锛欴SH 鐨勫鎴风鎻掍欢鍚嶅崟锛坆oot manifest锛夊湪鍚姩鏃跺浐鍖栵紝鏂版彃浠堕渶瑕?*閲嶅惎 dsh web**锛堟垨涓嬫鍚姩 harness锛夊悗鍑虹幇鍦ㄤ晶杈规爮銆傚綋鍓嶆鍦ㄨ繍琛岀殑 webui 鏃犳硶鐑姞杞芥柊鎻掍欢锛堣繖鏄钩鍙版満鍒讹紝闈炴彃浠剁己闄凤級銆?
13
- ### 鎵嬪姩瀹夎锛堝叾浠栨満鍣級
10
+ ## 核心价值
14
11
 
15
- ```powershell
16
- # 1. 鐢ㄥ畼鏂瑰懡浠ら摼鎺ユ彃浠讹紙浼氳嚜鍔ㄦ妸瀹冨姞鍏?profile bundles 灞傦級
17
- dsh plugin --profile web add link:F:/CycleMaster/dsh-code-studio
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
- # 2. 鎴栬€呭湪 profiles/web/cordis.patch.yml 杩藉姞锛?# - insert:
20
- # - id: code-studio
21
- # name: '@windypro-rourou/dsh-code-studio'
22
- ```
19
+ ## 安装
23
20
 
24
- ### 鐜板湪灏辨兂棰勮锛?
25
- ```powershell
26
- dsh web --port 3081 # 浼氱湅鍒颁晶杈规爮鍑虹幇 "Code Studio" 鍏ュ彛
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
- 1. 鍒锋柊椤甸潰鍚庯紝宸︿晶杈规爮鍑虹幇 **Code Studio** 鍏ュ彛锛堜唬鐮佸浘鏍囷級銆?2. 鐐瑰嚮鎵撳紑鍏ㄥ睆闈㈡澘锛氬乏渚ф枃浠舵爲锛屼腑闂存爣绛鹃〉缂栬緫鍣ㄣ€?3. 璁?Agent 淇敼浠g爜锛堝啓鏂囦欢/缂栬緫锛夛紝闈㈡澘椤堕儴銆孉gent 鍙樻洿銆嶅疄鏃跺垪鍑哄彉鍖栨枃浠躲€?4. 鐐瑰嚮鍙樻洿鏂囦欢 鈫?榛樿杩涘叆 Diff 瑙嗗浘锛氳鍙峰墠绗﹀彿 + 棰滆壊鑳屾櫙锛屼竴鐩簡鐒躲€?
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
- - 鍗曟枃浠?> 512KB 涓嶈鍙栧唴瀹癸紙闃叉祻瑙堝櫒鍗℃锛夛紝浣嗕細鏍囪銆屾枃浠惰繃澶с€嶃€?- Diff 瑙嗗浘閽堝銆屾墦寮€鍚?涓婃璇诲彇鍚庛€嶇殑鍙樻洿鍋氬熀绾垮姣旓紝绗﹀悎 Cline 鐨勪細璇濊涔夈€?
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 last = data.events && data.events.length > 0 ? data.events[data.events.length - 1] : null;
491
- if (!last) return;
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
- try { globalThis.localStorage?.setItem("cs.panelWidth", String(width)); } catch { /* ignore */ }
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
- this.root = root;
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
- this.watcher = void 0;
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
- this.push({ path, ts: Date.now(), before: before?.content ?? null, after: null, deleted: true });
115
- this.addHistory(path, { ts: Date.now(), before: before?.content ?? null, after: null, deleted: true });
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
- try {
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
- if (this.watcher) { try { this.watcher.close(); } catch { /* ignore */ } }
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
- "name": "@windypro-rourou/dsh-code-studio",
3
- "description": "Code Studio for DSH Web GUI a VS Code + Cline hybrid: file tree, syntax-highlighted editor, and Cline-style line-by-line diffs that auto-appear when the agent edits files.",
4
- "version": "0.1.1",
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
- }
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
+ }