@dimina-kit/devtools 0.4.0-dev.20260706154125 → 0.4.0-dev.20260707110006

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 (39) hide show
  1. package/dist/main/app/app.js +1 -1
  2. package/dist/main/index.bundle.js +129 -40
  3. package/dist/main/ipc/bridge-router.d.ts +1 -1
  4. package/dist/main/ipc/bridge-router.js +14 -11
  5. package/dist/main/ipc/project-fs.d.ts +4 -0
  6. package/dist/main/ipc/project-fs.js +4 -1
  7. package/dist/main/runtime/miniapp-runtime.d.ts +4 -3
  8. package/dist/main/services/fs-watch-sse.d.ts +33 -0
  9. package/dist/main/services/fs-watch-sse.js +99 -0
  10. package/dist/main/services/http-json.d.ts +9 -0
  11. package/dist/main/services/http-json.js +6 -0
  12. package/dist/main/services/render-inspect/index.d.ts +1 -1
  13. package/dist/main/services/render-inspect/index.js +1 -1
  14. package/dist/main/services/service-host-pool/pool.d.ts +4 -4
  15. package/dist/main/services/service-host-pool/pool.js +5 -5
  16. package/dist/main/services/simulator-temp-files/disk.d.ts +1 -1
  17. package/dist/main/services/simulator-temp-files/disk.js +1 -1
  18. package/dist/main/services/simulator-temp-files/fs-channels.d.ts +1 -1
  19. package/dist/main/services/simulator-temp-files/fs-channels.js +1 -1
  20. package/dist/main/services/simulator-temp-files/request-handler.d.ts +1 -1
  21. package/dist/main/services/simulator-temp-files/request-handler.js +1 -1
  22. package/dist/main/services/workbench-coi-server.js +26 -7
  23. package/dist/native-host/render/render.js +36 -18
  24. package/dist/preload/windows/host-toolbar-runtime.cjs +1 -1
  25. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  26. package/dist/preload/windows/main.cjs.map +1 -1
  27. package/dist/shared/vpath.d.ts +1 -1
  28. package/dist/shared/vpath.js +1 -1
  29. package/dist/vscode-workbench/assets/__vite-browser-external-D6-QnwSb.js +1 -0
  30. package/dist/vscode-workbench/assets/{dist-TpGpmMGi.js → dist-OuMzPA6f.js} +3 -3
  31. package/dist/vscode-workbench/assets/fs-core-worker-urls-CLvv1Q_0.js +1 -0
  32. package/dist/vscode-workbench/assets/fs-core.worker-VoMLk-PY.js +1 -0
  33. package/dist/vscode-workbench/assets/fs-query.worker-fXLkQK9l.js +2 -0
  34. package/dist/vscode-workbench/assets/{iconv-lite-umd-C9tiCAJa.js → iconv-lite-umd-a5yQx8hi.js} +1 -1
  35. package/dist/vscode-workbench/assets/{index-DJ1HyMZ7.js → index-CVEUw5jw.js} +458 -458
  36. package/dist/vscode-workbench/assets/{jschardet-DE1jV513.js → jschardet-BYkY0M2Q.js} +1 -1
  37. package/dist/vscode-workbench/index.html +1 -1
  38. package/package.json +4 -4
  39. package/dist/vscode-workbench/assets/__vite-browser-external-CXi6Kz9W.js +0 -1
@@ -356,7 +356,7 @@ export async function createDevtoolsRuntime(config = {}) {
356
356
  const context = createContext(config, mainWindow, rendererDir);
357
357
  // Anchor the main window's renderer as the first Connection. Resources
358
358
  // scoped to the main webContents (acquired by later wiring) tear down with
359
- // it; see packages/electron-deck/docs/foundation.md §4.
359
+ // it; see packages/electron-deck/docs/foundation.md (teardown paths).
360
360
  context.connections.acquire(mainWindow.webContents);
361
361
  context.registry.add(registerAppIpc(context));
362
362
  // Sandboxed project file-system IPC (the renderer-side project:fs:* surface).
@@ -827,7 +827,7 @@ function registerProjectFsIpc(ctx) {
827
827
  // src/main/app/app.ts
828
828
  import { app as app15, BrowserWindow as BrowserWindow8, nativeImage, session as session4 } from "electron";
829
829
  import fs12 from "fs";
830
- import path23 from "path";
830
+ import path24 from "path";
831
831
 
832
832
  // src/main/utils/paths.ts
833
833
  import fs3 from "fs";
@@ -3043,8 +3043,8 @@ function createHostToolbarView(ctx, reconciler, deps) {
3043
3043
  hide() {
3044
3044
  hideHostToolbar();
3045
3045
  },
3046
- setPreloadPath(path24) {
3047
- hostToolbarPreloadOverride = path24;
3046
+ setPreloadPath(path25) {
3047
+ hostToolbarPreloadOverride = path25;
3048
3048
  },
3049
3049
  setHeightMode(mode) {
3050
3050
  if (mode !== "auto" && !(Number.isFinite(mode.fixed) && mode.fixed >= 0)) {
@@ -5298,7 +5298,7 @@ var ServiceHostPool = class {
5298
5298
  /**
5299
5299
  * Hand a ready window matching `spec` to the caller. If none is ready, fall
5300
5300
  * back to synchronously constructing a fresh window — never blocking on warm
5301
- * (doc §3.1 "绝不阻塞 acquire"). Fallback windows carry `entryId === null` and
5301
+ * (see prewarm-webview.md's invariants section). Fallback windows carry `entryId === null` and
5302
5302
  * are destroyed (not pooled) on release.
5303
5303
  *
5304
5304
  * A spec change (different `preloadPath`) tears down all *pooled* entries and
@@ -5374,7 +5374,7 @@ var ServiceHostPool = class {
5374
5374
  }
5375
5375
  /**
5376
5376
  * Re-target the pool. `target` is clamped to `[0, maxPoolSize]`. Pooled
5377
- * entries beyond the target are disposed OLDEST-FIRST (doc §3.3/§3.6); in-use
5377
+ * entries beyond the target are disposed OLDEST-FIRST (see prewarm-webview.md); in-use
5378
5378
  * entries are untouched. Does not warm new entries up (use `init` / refill).
5379
5379
  */
5380
5380
  resize(target) {
@@ -5503,7 +5503,7 @@ var ServiceHostPool = class {
5503
5503
  }
5504
5504
  /**
5505
5505
  * Reset a released window back to a clean, blank state. Order matters
5506
- * (doc §3.5.3 / §3.4「等待时序」): navigate to blank FIRST so the old document
5506
+ * (see prewarm-webview.md's state-machine and reset-ordering sections): navigate to blank FIRST so the old document
5507
5507
  * stops running and in-flight requests are aborted, THEN clear storage — so a
5508
5508
  * still-live old page cannot re-populate storage after the clear.
5509
5509
  */
@@ -10227,13 +10227,13 @@ function createRenderInspector(options = {}) {
10227
10227
  import { toDisposable as toDisposable6 } from "@dimina-kit/electron-deck/main";
10228
10228
 
10229
10229
  // src/main/services/simulator-temp-files/store.ts
10230
- function registerTempFile(store, path24, mime, bytes) {
10230
+ function registerTempFile(store, path25, mime, bytes) {
10231
10231
  const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(new Uint8Array(bytes));
10232
- store.delete(path24);
10233
- store.set(path24, { bytes: buf, mime });
10232
+ store.delete(path25);
10233
+ store.set(path25, { bytes: buf, mime });
10234
10234
  }
10235
- function revokeTempFile(store, path24) {
10236
- store.delete(path24);
10235
+ function revokeTempFile(store, path25) {
10236
+ store.delete(path25);
10237
10237
  }
10238
10238
  function revokeAllTempFiles(store) {
10239
10239
  store.clear();
@@ -10661,10 +10661,10 @@ function setupSimulatorTempFiles(simSession) {
10661
10661
  const store = /* @__PURE__ */ new Map();
10662
10662
  const pendingWaiters = /* @__PURE__ */ new Map();
10663
10663
  let disposed = false;
10664
- function drainWaiters(path24) {
10665
- const list = pendingWaiters.get(path24);
10664
+ function drainWaiters(path25) {
10665
+ const list = pendingWaiters.get(path25);
10666
10666
  if (!list) return;
10667
- pendingWaiters.delete(path24);
10667
+ pendingWaiters.delete(path25);
10668
10668
  for (const fn of list) fn();
10669
10669
  }
10670
10670
  function drainAllWaiters() {
@@ -10677,15 +10677,15 @@ function setupSimulatorTempFiles(simSession) {
10677
10677
  const registry = new IpcRegistry(simulatorOnlyPolicy);
10678
10678
  registry.on("simulator:temp-file:write", (_event, payload) => {
10679
10679
  if (disposed) return;
10680
- const { path: path24, mime, bytes } = payload;
10681
- registerTempFile(store, path24, mime, bytes);
10680
+ const { path: path25, mime, bytes } = payload;
10681
+ registerTempFile(store, path25, mime, bytes);
10682
10682
  enforceStoreCap(store);
10683
- drainWaiters(path24);
10683
+ drainWaiters(path25);
10684
10684
  });
10685
10685
  registry.on("simulator:temp-file:revoke", (_event, payload) => {
10686
10686
  if (disposed) return;
10687
- const { path: path24 } = payload;
10688
- revokeTempFile(store, path24);
10687
+ const { path: path25 } = payload;
10688
+ revokeTempFile(store, path25);
10689
10689
  });
10690
10690
  registry.on("simulator:temp-file:revoke-all", () => {
10691
10691
  if (disposed) return;
@@ -10829,9 +10829,97 @@ function setupSimulatorSessionPolicy() {
10829
10829
 
10830
10830
  // src/main/services/workbench-coi-server.ts
10831
10831
  import http2 from "node:http";
10832
- import nodeFs2 from "node:fs";
10832
+ import nodeFs3 from "node:fs";
10833
10833
  import fs11 from "node:fs/promises";
10834
+ import path23 from "node:path";
10835
+
10836
+ // src/main/services/fs-watch-sse.ts
10837
+ import nodeFs2 from "node:fs";
10834
10838
  import path22 from "node:path";
10839
+
10840
+ // src/main/services/http-json.ts
10841
+ function jsonRes(res, code, obj) {
10842
+ const body = Buffer.from(JSON.stringify(obj));
10843
+ res.writeHead(code, { "Content-Type": "application/json; charset=utf-8", "Content-Length": body.length });
10844
+ res.end(body);
10845
+ }
10846
+
10847
+ // src/main/services/fs-watch-sse.ts
10848
+ var WATCH_DEBOUNCE_MS = 80;
10849
+ function shouldReportWatchPath(rel) {
10850
+ if (rel === "" || rel.startsWith("..") || path22.isAbsolute(rel)) return false;
10851
+ return !rel.split("/").some((segment) => SKIP_DIRS.has(segment));
10852
+ }
10853
+ function handleFsWatchRequest(req, res, getProjectRoot) {
10854
+ const watchedRoot = getProjectRoot();
10855
+ if (!watchedRoot) {
10856
+ jsonRes(res, 409, { error: "No active project", code: "ENOACTIVE" });
10857
+ return;
10858
+ }
10859
+ let watcher;
10860
+ try {
10861
+ watcher = nodeFs2.watch(watchedRoot, { recursive: true }, onChange);
10862
+ } catch (e) {
10863
+ jsonRes(res, 500, { error: String(e) });
10864
+ return;
10865
+ }
10866
+ res.writeHead(200, {
10867
+ "Content-Type": "text/event-stream; charset=utf-8",
10868
+ "Cache-Control": "no-store",
10869
+ Connection: "keep-alive"
10870
+ });
10871
+ let pending = /* @__PURE__ */ new Set();
10872
+ let debounceTimer = null;
10873
+ let liveCheck = null;
10874
+ let closed = false;
10875
+ function cleanup() {
10876
+ if (closed) return;
10877
+ closed = true;
10878
+ if (debounceTimer) clearTimeout(debounceTimer);
10879
+ if (liveCheck) clearInterval(liveCheck);
10880
+ watcher.close();
10881
+ }
10882
+ function flush() {
10883
+ if (closed || pending.size === 0) return;
10884
+ const paths = [...pending];
10885
+ pending = /* @__PURE__ */ new Set();
10886
+ res.write(`data: ${JSON.stringify({ paths })}
10887
+
10888
+ `);
10889
+ }
10890
+ function onChange(_event, filename) {
10891
+ const rel = filename ? filename.toString().split(path22.sep).join("/") : ".";
10892
+ if (rel !== "." && !shouldReportWatchPath(rel)) return;
10893
+ pending.add(rel);
10894
+ if (!debounceTimer) {
10895
+ debounceTimer = setTimeout(() => {
10896
+ debounceTimer = null;
10897
+ flush();
10898
+ }, WATCH_DEBOUNCE_MS);
10899
+ }
10900
+ }
10901
+ watcher.on("error", () => {
10902
+ if (closed) return;
10903
+ res.write(`data: ${JSON.stringify({ watcherDead: true })}
10904
+
10905
+ `);
10906
+ cleanup();
10907
+ res.end();
10908
+ });
10909
+ liveCheck = setInterval(() => {
10910
+ if (closed) return;
10911
+ if (getProjectRoot() !== watchedRoot) {
10912
+ res.write(`data: ${JSON.stringify({ watcherDead: true })}
10913
+
10914
+ `);
10915
+ cleanup();
10916
+ res.end();
10917
+ }
10918
+ }, WATCH_DEBOUNCE_MS);
10919
+ req.on("close", cleanup);
10920
+ }
10921
+
10922
+ // src/main/services/workbench-coi-server.ts
10835
10923
  var MAX_FS_WRITE_BYTES = 32 * 1024 * 1024;
10836
10924
  var MIME = {
10837
10925
  ".html": "text/html; charset=utf-8",
@@ -10855,8 +10943,8 @@ function setIsolationHeaders(res) {
10855
10943
  }
10856
10944
  function containedStaticPath(root, pathname) {
10857
10945
  const rel = pathname.replace(/^\/+/, "") || "index.html";
10858
- const resolved = path22.resolve(root, rel);
10859
- if (resolved !== root && !resolved.startsWith(root + path22.sep)) return null;
10946
+ const resolved = path23.resolve(root, rel);
10947
+ if (resolved !== root && !resolved.startsWith(root + path23.sep)) return null;
10860
10948
  return resolved;
10861
10949
  }
10862
10950
  function serveStaticFile(res, root, pathname) {
@@ -10867,23 +10955,23 @@ function serveStaticFile(res, root, pathname) {
10867
10955
  return;
10868
10956
  }
10869
10957
  Promise.all([fs11.realpath(root), fs11.realpath(candidate)]).then(([realRoot, realFile]) => {
10870
- if (realFile !== realRoot && !realFile.startsWith(realRoot + path22.sep)) {
10958
+ if (realFile !== realRoot && !realFile.startsWith(realRoot + path23.sep)) {
10871
10959
  res.writeHead(403);
10872
10960
  res.end("Forbidden");
10873
10961
  return;
10874
10962
  }
10875
- nodeFs2.stat(realFile, (err2, stat) => {
10963
+ nodeFs3.stat(realFile, (err2, stat) => {
10876
10964
  if (err2 || !stat.isFile()) {
10877
10965
  res.writeHead(404);
10878
10966
  res.end("Not Found");
10879
10967
  return;
10880
10968
  }
10881
10969
  res.writeHead(200, {
10882
- "Content-Type": MIME[path22.extname(realFile)] ?? "application/octet-stream",
10970
+ "Content-Type": MIME[path23.extname(realFile)] ?? "application/octet-stream",
10883
10971
  "Content-Length": stat.size,
10884
10972
  "Cache-Control": "no-store"
10885
10973
  });
10886
- nodeFs2.createReadStream(realFile).pipe(res);
10974
+ nodeFs3.createReadStream(realFile).pipe(res);
10887
10975
  });
10888
10976
  }).catch(() => {
10889
10977
  if (!res.headersSent) {
@@ -10892,11 +10980,6 @@ function serveStaticFile(res, root, pathname) {
10892
10980
  }
10893
10981
  });
10894
10982
  }
10895
- function jsonRes(res, code, obj) {
10896
- const body = Buffer.from(JSON.stringify(obj));
10897
- res.writeHead(code, { "Content-Type": "application/json; charset=utf-8", "Content-Length": body.length });
10898
- res.end(body);
10899
- }
10900
10983
  var BodyTooLargeError = class extends Error {
10901
10984
  constructor() {
10902
10985
  super("request body exceeds limit");
@@ -10952,6 +11035,7 @@ function fsErrorStatus(e) {
10952
11035
  if (code === "ENOACTIVE") return 409;
10953
11036
  if (code === "EACCES" || code === "EINVAL") return 403;
10954
11037
  if (code === "ENOENT") return 404;
11038
+ if (code === "EISDIR") return 404;
10955
11039
  return 500;
10956
11040
  }
10957
11041
  async function fsStat(ctx) {
@@ -10960,7 +11044,8 @@ async function fsStat(ctx) {
10960
11044
  }
10961
11045
  async function fsReaddir(ctx) {
10962
11046
  const entries = await readdirWithin(ctx.projectRoot, ctx.rel);
10963
- jsonRes(ctx.res, 200, entries.map((e) => [e.name, e.isDirectory() ? 2 : 1]));
11047
+ const visible = entries.filter((e) => !(e.isDirectory() && SKIP_DIRS.has(e.name)));
11048
+ jsonRes(ctx.res, 200, visible.map((e) => [e.name, e.isDirectory() ? 2 : 1]));
10964
11049
  }
10965
11050
  async function fsRead(ctx) {
10966
11051
  const buf = await readFileBufferWithin(ctx.projectRoot, ctx.rel);
@@ -11032,7 +11117,7 @@ async function listFilesRecursive(dir, base = "") {
11032
11117
  for (const e of entries) {
11033
11118
  const rel = base ? `${base}/${e.name}` : e.name;
11034
11119
  if (e.isDirectory()) {
11035
- out.push(...await listFilesRecursive(path22.join(dir, e.name), rel));
11120
+ out.push(...await listFilesRecursive(path23.join(dir, e.name), rel));
11036
11121
  } else if (e.isFile()) {
11037
11122
  out.push(rel);
11038
11123
  }
@@ -11049,9 +11134,9 @@ async function readContributedExtensions(extensionsDir) {
11049
11134
  }
11050
11135
  for (const e of entries) {
11051
11136
  if (!e.isDirectory()) continue;
11052
- const extDir = path22.join(extensionsDir, e.name);
11137
+ const extDir = path23.join(extensionsDir, e.name);
11053
11138
  try {
11054
- const pkgRaw = await fs11.readFile(path22.join(extDir, "package.json"), "utf8");
11139
+ const pkgRaw = await fs11.readFile(path23.join(extDir, "package.json"), "utf8");
11055
11140
  const files = await listFilesRecursive(extDir);
11056
11141
  out.push({ dir: e.name, packageJson: JSON.parse(pkgRaw), files });
11057
11142
  } catch {
@@ -11060,11 +11145,15 @@ async function readContributedExtensions(extensionsDir) {
11060
11145
  return out;
11061
11146
  }
11062
11147
  async function startWorkbenchCoiServer(options) {
11063
- const root = path22.resolve(options.rootDir);
11064
- const contribRoot = options.extensionsDir ? path22.resolve(options.extensionsDir) : null;
11148
+ const root = path23.resolve(options.rootDir);
11149
+ const contribRoot = options.extensionsDir ? path23.resolve(options.extensionsDir) : null;
11065
11150
  const server = http2.createServer((req, res) => {
11066
11151
  setIsolationHeaders(res);
11067
11152
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
11153
+ if (url.pathname === "/__fs/watch") {
11154
+ handleFsWatchRequest(req, res, options.getProjectRoot);
11155
+ return;
11156
+ }
11068
11157
  if (url.pathname.startsWith("/__fs/")) {
11069
11158
  handleFsRequest(req, res, options.getProjectRoot(), url).catch((e) => {
11070
11159
  if (!res.headersSent) jsonRes(res, 500, { error: String(e) });
@@ -11279,7 +11368,7 @@ async function disposeContext(ctx) {
11279
11368
  function createConfiguredMainWindow(config, rendererDir2) {
11280
11369
  const mainWindow = createMainWindow({
11281
11370
  title: config.appName ?? "Dimina DevTools",
11282
- indexHtml: path23.join(rendererDir2, "entries/main/index.html"),
11371
+ indexHtml: path24.join(rendererDir2, "entries/main/index.html"),
11283
11372
  width: config.window?.width,
11284
11373
  height: config.window?.height,
11285
11374
  minWidth: config.window?.minWidth,
@@ -11558,8 +11647,8 @@ async function createDevtoolsRuntime(config = {}) {
11558
11647
  bridge: context.bridge
11559
11648
  }));
11560
11649
  }
11561
- const bundleDir = config.editorViewConfig?.bundleDir ?? path23.join(devtoolsPackageRoot, "dist/vscode-workbench");
11562
- if (!fs12.existsSync(path23.join(bundleDir, "index.html"))) {
11650
+ const bundleDir = config.editorViewConfig?.bundleDir ?? path24.join(devtoolsPackageRoot, "dist/vscode-workbench");
11651
+ if (!fs12.existsSync(path24.join(bundleDir, "index.html"))) {
11563
11652
  console.warn(
11564
11653
  `[workbench] editor bundle not found at ${bundleDir} \u2014 skipping embedded editor assembly`
11565
11654
  );
@@ -108,7 +108,7 @@ export interface BridgeRouterHandle {
108
108
  */
109
109
  census?(): BridgeResourceCensus;
110
110
  /**
111
- * The flag-gated debugTap (§7) over the bridge message stream. Exposed so a
111
+ * The flag-gated debugTap (see foundation.md) over the bridge message stream. Exposed so a
112
112
  * hidden devtools panel / automation can read `.entries()` when
113
113
  * `DIMINA_DEBUG_TAP=1`; a no-op snapshot otherwise. Optional so the many
114
114
  * partial `BridgeRouterHandle` test mocks don't each have to stub it.
@@ -63,10 +63,11 @@ const STACK_ID = 'stack_0';
63
63
  * constant instead of a magic number.
64
64
  */
65
65
  export const LAUNCH_TIMEOUT_MS = 20_000;
66
- /** Hard ceiling on the pre-warm pool, mirroring ServiceHostPool/doc §3.3. */
66
+ /** Hard ceiling on the pre-warm pool, mirroring ServiceHostPool/prewarm-webview.md. */
67
67
  const PREWARM_MAX_POOL_SIZE = 4;
68
68
  /**
69
- * Resolve the pre-warm pool size from env (doc §6). Returns 0 (OFF) unless
69
+ * Resolve the pre-warm pool size from env (see prewarm-webview.md's config
70
+ * section). Returns 0 (OFF) unless
70
71
  * `DIMINA_PREWARM_POOL_SIZE` is a positive integer and `DIMINA_PREWARM_DISABLE`
71
72
  * is not set. Default OFF — pooling is opt-in so the spawn path is unchanged
72
73
  * unless explicitly enabled.
@@ -82,7 +83,7 @@ function resolvePrewarmPoolSize() {
82
83
  return 0;
83
84
  return Math.min(n, PREWARM_MAX_POOL_SIZE);
84
85
  }
85
- /** debugTap (§7) is opt-in via `DIMINA_DEBUG_TAP=1` — off everywhere else. */
86
+ /** debugTap (see foundation.md) is opt-in via `DIMINA_DEBUG_TAP=1` — off everywhere else. */
86
87
  function resolveDebugTapEnabled() {
87
88
  return process.env.DIMINA_DEBUG_TAP === '1';
88
89
  }
@@ -181,8 +182,8 @@ export function installBridgeRouter(ctx) {
181
182
  if (prewarmPoolSize > 0) {
182
183
  const pool = new ServiceHostPool();
183
184
  state.pool = pool;
184
- // Defer warm-up off the cold-start critical path (doc §3.3: app.ready +
185
- // idle). Warming eagerly here races main-window startup — it creates an
185
+ // Defer warm-up off the cold-start critical path (see prewarm-webview.md:
186
+ // app.ready + idle). Warming eagerly here races main-window startup — it creates an
186
187
  // extra hidden BrowserWindow before the workbench renderer has settled,
187
188
  // which made the e2e's first-window/preload-readiness flaky. The timer is
188
189
  // cancelled on teardown so it can't warm a disposed pool.
@@ -451,7 +452,7 @@ export function installBridgeRouter(ctx) {
451
452
  };
452
453
  ipcMain.on(C.DISPOSE, onDispose);
453
454
  ctx.registry.add(() => { ipcMain.removeListener(C.DISPOSE, onDispose); });
454
- // debugTap (§7) ingress recorder — near-free no-op unless DIMINA_DEBUG_TAP=1.
455
+ // debugTap (see foundation.md) ingress recorder — near-free no-op unless DIMINA_DEBUG_TAP=1.
455
456
  // Hung on the bridge dispatch chokepoint so the cross-wc message flow is
456
457
  // inspectable (the first real consumer of the workbench debugTap primitive).
457
458
  const tapIn = (channel, sender, payload) => {
@@ -753,7 +754,8 @@ async function handleSpawn(state, ctx, event, opts) {
753
754
  appSession.pages.set(bridgeId, rootPage);
754
755
  bindWc(state.serviceWcIdToAppSessionId, serviceWindow.webContents, appSessionId);
755
756
  bindSimulatorWc(state.simulatorWcIdToAppSessionIds, simulatorWc, appSessionId);
756
- // Track the service-host webContents as a Connection (foundation.md §4.3) and
757
+ // Track the service-host webContents as a Connection (the soft-reuse
758
+ // teardown path documented in foundation.md) and
757
759
  // own this session's serviceWc→appSessionId binding on its CURRENT lifetime
758
760
  // segment. On a pooled-window REUSE, `disposeAppSession` calls
759
761
  // `connections.reset(serviceWc.id)` → this owned cleanup fires + a fresh
@@ -761,7 +763,8 @@ async function handleSpawn(state, ctx, event, opts) {
761
763
  // real destroy the connection closes and fires it too. The cleanup is guarded
762
764
  // by `appSessionId` so it only clears a binding that is still THIS session's.
763
765
  // (The simulatorWc binding stays manual in `disposeAppSession`: the simulator
764
- // wc outlives individual app sessions — cross-connection state, §4.4 — so it
766
+ // wc outlives individual app sessions — cross-connection state documented in
767
+ // foundation.md — so it
765
768
  // cannot hang off any single session's segment.)
766
769
  state.connections.acquire(serviceWindow.webContents).own(() => {
767
770
  if (state.serviceWcIdToAppSessionId.get(appSession.serviceWc.id) === appSessionId) {
@@ -1732,8 +1735,8 @@ function ensureRenderBound(state, sender, bridgeId) {
1732
1735
  page.renderWc = sender;
1733
1736
  state.wcIdToBridgeId.set(sender.id, bridgeId);
1734
1737
  // Consolidate the render-guest bookkeeping teardown onto the connection
1735
- // layer (foundation.md §4 / P2): the render guest is its own webContents =
1736
- // its own connection (§4.4); own() ties this cleanup to its lifetime so it
1738
+ // layer (see foundation.md): the render guest is its own webContents =
1739
+ // its own connection; own() ties this cleanup to its lifetime so it
1737
1740
  // fires on destroy, replacing the bespoke `once('destroyed')`. acquire() is
1738
1741
  // idempotent — re-binding the same sender re-uses its connection.
1739
1742
  state.connections.acquire(sender).own(() => {
@@ -1874,7 +1877,7 @@ function closeSessionPages(state, ap) {
1874
1877
  // the unpooled path.
1875
1878
  async function releaseServiceWindow(state, ap, opts) {
1876
1879
  if (ap.poolEntryId !== null && state.pool && !opts.serviceAlreadyClosed) {
1877
- // Soft reuse (foundation.md §4.3): the pooled service-host webContents keeps
1880
+ // Soft reuse (see foundation.md): the pooled service-host webContents keeps
1878
1881
  // its wc.id but is about to run a NEW app session. Reset its Connection
1879
1882
  // BEFORE returning it to the pool — dispose this session's owned segment
1880
1883
  // (the serviceWc binding cleanup) and swap a fresh one — so an old in-flight
@@ -1,5 +1,9 @@
1
1
  import type { WorkbenchContext } from '../services/workbench-context.js';
2
2
  import type { Disposable } from '@dimina-kit/electron-deck/main';
3
+ /** Dependency/VCS/build directories invisible to every project-tree consumer:
4
+ * IPC `listFiles`, the `/__fs/readdir` bridge (so the workbench mirror and the
5
+ * WAL ledger seed never walk into them), and the `/__fs/watch` change stream. */
6
+ export declare const SKIP_DIRS: ReadonlySet<string>;
3
7
  /**
4
8
  * Synchronous, string-level guard: reject empty root (ENOACTIVE), NUL bytes
5
9
  * and empty paths (EINVAL), and lexical `..` escapes (EACCES) by resolving
@@ -49,7 +49,10 @@ import { validate } from '../utils/ipc-schema.js';
49
49
  import { ProjectFsChannel } from '../../shared/ipc-channels.js';
50
50
  /** Cap for `listFiles` — huge monorepos would otherwise stall the renderer. */
51
51
  const LIST_FILE_LIMIT = 5000;
52
- const SKIP_DIRS = new Set([
52
+ /** Dependency/VCS/build directories invisible to every project-tree consumer:
53
+ * IPC `listFiles`, the `/__fs/readdir` bridge (so the workbench mirror and the
54
+ * WAL ledger seed never walk into them), and the `/__fs/watch` change stream. */
55
+ export const SKIP_DIRS = new Set([
53
56
  'node_modules', '.git', '.svn', '.hg', 'dist', 'build',
54
57
  '.next', '.cache', '.turbo', '.parcel-cache', '.nuxt', '.output', '.vite',
55
58
  ]);
@@ -1,6 +1,6 @@
1
1
  /**
2
- * MiniappRuntime — the stable, host-facing contract surface (foundation.md §3
3
- * "MiniappRuntime 契约" layer).
2
+ * MiniappRuntime — the stable, host-facing contract surface documented in
3
+ * host-migration.md's public-contract section.
4
4
  *
5
5
  * HAND-WRITTEN, not derived from `WorkbenchContext`. A `Pick`-style projection
6
6
  * would drag every nested internal service type (ViewManager,
@@ -18,7 +18,8 @@
18
18
  * - `workspace.openProject` stays writable (no `readonly`) for backward
19
19
  * compat: hosts MAY still gate permissions by reassigning it. The preferred
20
20
  * path is now the declarative `WorkbenchAppConfig.onBeforeOpenProject` hook
21
- * (runs before any side effect, throw to veto) — see host-migration.md §7.
21
+ * (runs before any side effect, throw to veto) — see host-migration.md's
22
+ * `onBeforeOpenProject` hook section.
22
23
  *
23
24
  * `asMiniappRuntime(ctx)` is an identity return — the contract is a typed
24
25
  * VIEW onto the live context, not a snapshot/projection. That is what makes
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `GET /__fs/watch`: SSE stream of the active project root's disk changes, for
3
+ * the editor's memfs↔disk sync engine (devtools-fs-core-feasibility.md §7).
4
+ * Read-only — it adds no write surface, which is why workbench-coi-server.ts
5
+ * dispatches it before (and outside) the mutating-action guard of the generic
6
+ * `/__fs/*` handler. Split into its own module purely to keep each file within
7
+ * the repo size gate; the COI server is its only consumer.
8
+ *
9
+ * One `fs.watch(root, {recursive:true})` per connection (darwin supports
10
+ * `recursive`), changes batched over an 80ms window and pushed as
11
+ * `data: {"paths":["rel", ...]}` with POSIX-separated root-relative paths.
12
+ * `getProjectRoot` (not a resolved string) is threaded through so a project
13
+ * close/switch mid-stream is detected by re-polling the SAME source
14
+ * `handleFsRequest` uses — no separate close event is invented. Watcher errors
15
+ * (e.g. EMFILE) push `data: {"watcherDead":true}` then end the stream; no
16
+ * active project at connect time is a 409, matching the ENOACTIVE semantics of
17
+ * every other `/__fs` action.
18
+ */
19
+ import type http from 'node:http';
20
+ /** `false` for build/vcs/dependency churn, the empty string, and any path that
21
+ * (defensively — `fs.watch`'s filename is always root-relative) escapes the root.
22
+ * Matches SKIP_DIRS as a path SEGMENT at any depth — the same set `/__fs/readdir`
23
+ * omits, so the watcher never reports paths the editor mirror cannot see (a
24
+ * prefix match would let `packages/x/node_modules/...` through). The one
25
+ * accepted edge: a plain FILE named exactly like a skip dir (e.g. `dist`)
26
+ * is also unreported, though readdir would list it. */
27
+ declare function shouldReportWatchPath(rel: string): boolean;
28
+ export declare const __testing: {
29
+ shouldReportWatchPath: typeof shouldReportWatchPath;
30
+ };
31
+ export declare function handleFsWatchRequest(req: http.IncomingMessage, res: http.ServerResponse, getProjectRoot: () => string): void;
32
+ export {};
33
+ //# sourceMappingURL=fs-watch-sse.d.ts.map
@@ -0,0 +1,99 @@
1
+ import nodeFs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { SKIP_DIRS } from '../ipc/project-fs.js';
4
+ import { jsonRes } from './http-json.js';
5
+ /** Debounce window for batching change notifications (ms). */
6
+ const WATCH_DEBOUNCE_MS = 80;
7
+ /** `false` for build/vcs/dependency churn, the empty string, and any path that
8
+ * (defensively — `fs.watch`'s filename is always root-relative) escapes the root.
9
+ * Matches SKIP_DIRS as a path SEGMENT at any depth — the same set `/__fs/readdir`
10
+ * omits, so the watcher never reports paths the editor mirror cannot see (a
11
+ * prefix match would let `packages/x/node_modules/...` through). The one
12
+ * accepted edge: a plain FILE named exactly like a skip dir (e.g. `dist`)
13
+ * is also unreported, though readdir would list it. */
14
+ function shouldReportWatchPath(rel) {
15
+ if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel))
16
+ return false;
17
+ return !rel.split('/').some((segment) => SKIP_DIRS.has(segment));
18
+ }
19
+ export const __testing = { shouldReportWatchPath };
20
+ export function handleFsWatchRequest(req, res, getProjectRoot) {
21
+ const watchedRoot = getProjectRoot();
22
+ if (!watchedRoot) {
23
+ jsonRes(res, 409, { error: 'No active project', code: 'ENOACTIVE' });
24
+ return;
25
+ }
26
+ let watcher;
27
+ try {
28
+ watcher = nodeFs.watch(watchedRoot, { recursive: true }, onChange);
29
+ }
30
+ catch (e) {
31
+ jsonRes(res, 500, { error: String(e) });
32
+ return;
33
+ }
34
+ res.writeHead(200, {
35
+ 'Content-Type': 'text/event-stream; charset=utf-8',
36
+ 'Cache-Control': 'no-store',
37
+ Connection: 'keep-alive',
38
+ });
39
+ let pending = new Set();
40
+ let debounceTimer = null;
41
+ let liveCheck = null;
42
+ let closed = false;
43
+ function cleanup() {
44
+ if (closed)
45
+ return;
46
+ closed = true;
47
+ if (debounceTimer)
48
+ clearTimeout(debounceTimer);
49
+ if (liveCheck)
50
+ clearInterval(liveCheck);
51
+ watcher.close();
52
+ }
53
+ function flush() {
54
+ if (closed || pending.size === 0)
55
+ return;
56
+ const paths = [...pending];
57
+ pending = new Set();
58
+ res.write(`data: ${JSON.stringify({ paths })}\n\n`);
59
+ }
60
+ function onChange(_event, filename) {
61
+ // FSEvents overflow delivers a null filename ("something changed,
62
+ // rescan"). Dropping it loses data; report '.' and let the client's
63
+ // watch-batch expansion (wal-audit.ts) reconcile the whole tree.
64
+ const rel = filename ? filename.toString().split(path.sep).join('/') : '.';
65
+ if (rel !== '.' && !shouldReportWatchPath(rel))
66
+ return;
67
+ pending.add(rel);
68
+ if (!debounceTimer) {
69
+ debounceTimer = setTimeout(() => {
70
+ debounceTimer = null;
71
+ flush();
72
+ }, WATCH_DEBOUNCE_MS);
73
+ }
74
+ }
75
+ watcher.on('error', () => {
76
+ if (closed)
77
+ return;
78
+ res.write(`data: ${JSON.stringify({ watcherDead: true })}\n\n`);
79
+ cleanup();
80
+ res.end();
81
+ });
82
+ // The active project root has no dedicated close/switch event to subscribe
83
+ // to; poll the same getter `handleFsRequest` uses, on the debounce cadence,
84
+ // so a closed or switched project tears this stream down promptly. Push
85
+ // `watcherDead` BEFORE ending so the client gets a deterministic
86
+ // end-of-channel signal instead of depending on how its EventSource
87
+ // classifies the reconnect-then-409 that would follow a bare close.
88
+ liveCheck = setInterval(() => {
89
+ if (closed)
90
+ return;
91
+ if (getProjectRoot() !== watchedRoot) {
92
+ res.write(`data: ${JSON.stringify({ watcherDead: true })}\n\n`);
93
+ cleanup();
94
+ res.end();
95
+ }
96
+ }, WATCH_DEBOUNCE_MS);
97
+ req.on('close', cleanup);
98
+ }
99
+ //# sourceMappingURL=fs-watch-sse.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Minimal JSON responder shared by the workbench COI server and its
3
+ * `/__fs/watch` SSE module (fs-watch-sse.ts). Lives in its own module because
4
+ * workbench-coi-server.ts imports fs-watch-sse.ts — either file owning it
5
+ * would force an import cycle for the other.
6
+ */
7
+ import type http from 'node:http';
8
+ export declare function jsonRes(res: http.ServerResponse, code: number, obj: unknown): void;
9
+ //# sourceMappingURL=http-json.d.ts.map
@@ -0,0 +1,6 @@
1
+ export function jsonRes(res, code, obj) {
2
+ const body = Buffer.from(JSON.stringify(obj));
3
+ res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': body.length });
4
+ res.end(body);
5
+ }
6
+ //# sourceMappingURL=http-json.js.map
@@ -38,7 +38,7 @@ export interface RenderInspectorOptions {
38
38
  /** Override the injected IIFE source (default: read the built bundle). Test seam. */
39
39
  loadSource?: () => string;
40
40
  /**
41
- * Connection registry (foundation.md §4 / P2). When provided, the per-wc
41
+ * Connection registry (see foundation.md's teardown-paths section). When provided, the per-wc
42
42
  * `injected` bookkeeping is torn down via the wc's connection (deterministic
43
43
  * with the rest of that webContents's resources) instead of a bespoke
44
44
  * `once('destroyed')`. Optional so focused unit tests can omit it (they fall
@@ -71,7 +71,7 @@ export function createRenderInspector(options = {}) {
71
71
  }
72
72
  injected.add(wc.id);
73
73
  // Consolidate the per-wc bookkeeping teardown onto the connection layer
74
- // (foundation.md §4 / P2) when a registry is available; fall back to the
74
+ // (see foundation.md) when a registry is available; fall back to the
75
75
  // bespoke `once('destroyed')` only when omitted (focused unit tests).
76
76
  const forget = () => {
77
77
  injected.delete(wc.id);