@windypro-rourou/dsh-code-studio 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/lib/client.js +78 -5
  2. package/lib/index.js +114 -16
  3. package/package.json +56 -56
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.0",
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": "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.2",
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
+ }