@dimina-kit/devtools 0.3.2-dev.20260524104239 → 0.3.2-dev.20260601115343

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 (174) hide show
  1. package/README.md +20 -7
  2. package/dist/main/api.d.ts +1 -1
  3. package/dist/main/app/app.js +30 -2
  4. package/dist/main/app/bootstrap.d.ts +9 -0
  5. package/dist/main/app/bootstrap.js +28 -1
  6. package/dist/main/index.bundle.js +1468 -496
  7. package/dist/main/ipc/project-fs.d.ts +57 -0
  8. package/dist/main/ipc/project-fs.js +448 -0
  9. package/dist/main/ipc/simulator-module.js +2 -0
  10. package/dist/main/ipc/views.d.ts +19 -0
  11. package/dist/main/ipc/views.js +27 -0
  12. package/dist/main/services/layout/index.d.ts +1 -1
  13. package/dist/main/services/layout/index.js +3 -3
  14. package/dist/main/services/simulator-temp-files/disk.d.ts +55 -0
  15. package/dist/main/services/simulator-temp-files/disk.js +215 -0
  16. package/dist/main/services/simulator-temp-files/fs-channels.d.ts +69 -0
  17. package/dist/main/services/simulator-temp-files/fs-channels.js +108 -0
  18. package/dist/main/services/simulator-temp-files/index.d.ts +33 -0
  19. package/dist/main/services/simulator-temp-files/index.js +160 -0
  20. package/dist/main/services/simulator-temp-files/request-handler.d.ts +30 -0
  21. package/dist/main/services/simulator-temp-files/request-handler.js +178 -0
  22. package/dist/main/services/simulator-temp-files/resolver.d.ts +22 -0
  23. package/dist/main/services/simulator-temp-files/resolver.js +22 -0
  24. package/dist/main/services/simulator-temp-files/store.d.ts +14 -0
  25. package/dist/main/services/simulator-temp-files/store.js +24 -0
  26. package/dist/main/services/toolbar/toolbar-store.d.ts +3 -3
  27. package/dist/main/services/toolbar/toolbar-store.js +18 -1
  28. package/dist/main/services/update/update-manager.d.ts +10 -0
  29. package/dist/main/services/update/update-manager.js +16 -3
  30. package/dist/main/services/views/view-manager.d.ts +17 -4
  31. package/dist/main/services/views/view-manager.js +71 -14
  32. package/dist/main/services/workbench-context.d.ts +6 -1
  33. package/dist/main/services/workspace/workspace-service.d.ts +8 -0
  34. package/dist/main/services/workspace/workspace-service.js +22 -4
  35. package/dist/main/utils/ipc-registry.d.ts +10 -1
  36. package/dist/main/utils/ipc-registry.js +43 -0
  37. package/dist/preload/runtime/temp-files.d.ts +8 -0
  38. package/dist/preload/runtime/temp-files.js +34 -0
  39. package/dist/preload/shared/expose.js +2 -2
  40. package/dist/preload/windows/main.cjs +4 -0
  41. package/dist/preload/windows/main.cjs.map +2 -2
  42. package/dist/preload/windows/simulator.js +40 -8
  43. package/dist/renderer/assets/abap-DiwvWnMr.js +2 -0
  44. package/dist/renderer/assets/apex-CmtZjKlf.js +2 -0
  45. package/dist/renderer/assets/azcli-DL2My_i-.js +2 -0
  46. package/dist/renderer/assets/bat-B-nC98wG.js +2 -0
  47. package/dist/renderer/assets/bicep-Ju5MwOgh.js +3 -0
  48. package/dist/renderer/assets/cameligo-8Eu1TyBr.js +2 -0
  49. package/dist/renderer/assets/chunk-DPI0Y_ll.js +1 -0
  50. package/dist/renderer/assets/clojure-u-RpMkH3.js +2 -0
  51. package/dist/renderer/assets/codicon-ngg6Pgfi.ttf +0 -0
  52. package/dist/renderer/assets/coffee-CdA7bbTe.js +2 -0
  53. package/dist/renderer/assets/constants-CLAQX6VZ.js +2 -0
  54. package/dist/renderer/assets/cpp-CzNFP8ks.js +2 -0
  55. package/dist/renderer/assets/csharp-j1LThmcE.js +2 -0
  56. package/dist/renderer/assets/csp-CLRC61y6.js +2 -0
  57. package/dist/renderer/assets/css-r6rC_7P2.js +4 -0
  58. package/dist/renderer/assets/css.worker-DyDu5ynT.js +90 -0
  59. package/dist/renderer/assets/cssMode-BCBgxrnA.js +2 -0
  60. package/dist/renderer/assets/cypher-CW08XVUh.js +2 -0
  61. package/dist/renderer/assets/dart-Cs9aL5T_.js +2 -0
  62. package/dist/renderer/assets/dockerfile-BWM0M184.js +2 -0
  63. package/dist/renderer/assets/ecl-MJJuer5P.js +2 -0
  64. package/dist/renderer/assets/editor-CWI9TnMM.css +1 -0
  65. package/dist/renderer/assets/editor.api2-BTaYmRq7.js +873 -0
  66. package/dist/renderer/assets/editor.worker-Dpt_xPrs.js +27 -0
  67. package/dist/renderer/assets/elixir-D2AIuXqn.js +2 -0
  68. package/dist/renderer/assets/flow9-B2H24giC.js +2 -0
  69. package/dist/renderer/assets/freemarker2-DGDGzimW.js +4 -0
  70. package/dist/renderer/assets/fsharp-CMk2OIJN.js +2 -0
  71. package/dist/renderer/assets/go-BrMkuJg0.js +2 -0
  72. package/dist/renderer/assets/graphql-PSR1UKGv.js +2 -0
  73. package/dist/renderer/assets/handlebars-bOHhw3EG.js +2 -0
  74. package/dist/renderer/assets/hcl-DAQrbDOW.js +2 -0
  75. package/dist/renderer/assets/html-BMyuX0_c.js +2 -0
  76. package/dist/renderer/assets/html.worker-IWNXhnIC.js +503 -0
  77. package/dist/renderer/assets/htmlMode-DR4kXJ7l.js +2 -0
  78. package/dist/renderer/assets/index-BDMxXUfn.js +50 -0
  79. package/dist/renderer/assets/ini-0TG5BxW0.js +2 -0
  80. package/dist/renderer/assets/input-CCYS8Ave.js +2 -0
  81. package/dist/renderer/assets/{ipc-transport-BzPzxPwf.js → ipc-transport-Bn4KXvhu.js} +5 -5
  82. package/dist/renderer/assets/ipc-transport-Cu7tubyg.css +1 -0
  83. package/dist/renderer/assets/java-rgorz17v.js +2 -0
  84. package/dist/renderer/assets/javascript-Bu-tl1P7.js +2 -0
  85. package/dist/renderer/assets/json.worker-c4h5s80L.js +59 -0
  86. package/dist/renderer/assets/jsonMode-MQV_ffLR.js +8 -0
  87. package/dist/renderer/assets/julia-C8VMdHm8.js +2 -0
  88. package/dist/renderer/assets/kotlin-AhhC4Qhm.js +2 -0
  89. package/dist/renderer/assets/less-Cgca25AP.js +3 -0
  90. package/dist/renderer/assets/lexon-D0GHdBaw.js +2 -0
  91. package/dist/renderer/assets/liquid-ClkfOvhB.js +2 -0
  92. package/dist/renderer/assets/lspLanguageFeatures-Bke_4Ie9.js +5 -0
  93. package/dist/renderer/assets/lua-DmRsNG-P.js +2 -0
  94. package/dist/renderer/assets/m3-BgL5dNKT.js +2 -0
  95. package/dist/renderer/assets/markdown-BuJfycGS.js +2 -0
  96. package/dist/renderer/assets/mdx-RZdUskPu.js +2 -0
  97. package/dist/renderer/assets/mips-C9m_93PR.js +2 -0
  98. package/dist/renderer/assets/monaco.contribution-B7rUMaEv.js +3 -0
  99. package/dist/renderer/assets/msdax-CpFHC9OI.js +2 -0
  100. package/dist/renderer/assets/mysql-qFvltsqN.js +2 -0
  101. package/dist/renderer/assets/objective-c-Bnmr858J.js +2 -0
  102. package/dist/renderer/assets/pascal-WP0_D5AO.js +2 -0
  103. package/dist/renderer/assets/pascaligo-Blom4Rij.js +2 -0
  104. package/dist/renderer/assets/perl-B-vk8g64.js +2 -0
  105. package/dist/renderer/assets/pgsql-Cgvz6v67.js +2 -0
  106. package/dist/renderer/assets/php-8a3Lrw9m.js +2 -0
  107. package/dist/renderer/assets/pla-DuFqEZ8V.js +2 -0
  108. package/dist/renderer/assets/popover-CH4rwpdt.js +2 -0
  109. package/dist/renderer/assets/postiats-DkLtSgkp.js +2 -0
  110. package/dist/renderer/assets/powerquery-BJ1aNepW.js +2 -0
  111. package/dist/renderer/assets/powershell-rE98k687.js +2 -0
  112. package/dist/renderer/assets/protobuf-CUheFacr.js +3 -0
  113. package/dist/renderer/assets/pug-LDcAMD8w.js +2 -0
  114. package/dist/renderer/assets/python-Cobrm_-8.js +2 -0
  115. package/dist/renderer/assets/qsharp-DUKSQoR1.js +2 -0
  116. package/dist/renderer/assets/r-D-QApv87.js +2 -0
  117. package/dist/renderer/assets/razor-BEUIQFNJ.js +2 -0
  118. package/dist/renderer/assets/redis-SXdDyWR9.js +2 -0
  119. package/dist/renderer/assets/redshift-Y6lsCryn.js +2 -0
  120. package/dist/renderer/assets/restructuredtext-edObr9a8.js +2 -0
  121. package/dist/renderer/assets/ruby-CNnUfF-8.js +2 -0
  122. package/dist/renderer/assets/rust-IHUZWzBr.js +2 -0
  123. package/dist/renderer/assets/sb-DrUvY44N.js +2 -0
  124. package/dist/renderer/assets/scala-B4hbXGLM.js +2 -0
  125. package/dist/renderer/assets/scheme-BGrd12j3.js +2 -0
  126. package/dist/renderer/assets/scss-x5G1ES4U.js +4 -0
  127. package/dist/renderer/assets/select-Ct4-sijm.js +2 -0
  128. package/dist/renderer/assets/settings-CMTB-t0x.js +2 -0
  129. package/dist/renderer/assets/{settings-api-BXCw9vOZ.js → settings-api-Csdngf9Z.js} +2 -2
  130. package/dist/renderer/assets/shell-DOehe2Y8.js +2 -0
  131. package/dist/renderer/assets/solidity-BeRvcwWV.js +2 -0
  132. package/dist/renderer/assets/sophia-DZbkUNjy.js +2 -0
  133. package/dist/renderer/assets/sparql-B7_oi5-h.js +2 -0
  134. package/dist/renderer/assets/sql-CTlsFWVE.js +2 -0
  135. package/dist/renderer/assets/st-DJVEJdPE.js +2 -0
  136. package/dist/renderer/assets/swift-CwhT3fYa.js +2 -0
  137. package/dist/renderer/assets/systemverilog-BQN63pkN.js +2 -0
  138. package/dist/renderer/assets/tcl-DqwfpskA.js +2 -0
  139. package/dist/renderer/assets/ts.worker-BwM5Ha3L.js +67720 -0
  140. package/dist/renderer/assets/tsMode-BHa7dl9v.js +12 -0
  141. package/dist/renderer/assets/twig-BiyenUgc.js +2 -0
  142. package/dist/renderer/assets/typescript-15aT0Yj8.js +2 -0
  143. package/dist/renderer/assets/typespec-CWOJribt.js +2 -0
  144. package/dist/renderer/assets/vb-Cq5F87m3.js +2 -0
  145. package/dist/renderer/assets/wgsl-BAvW2lVr.js +299 -0
  146. package/dist/renderer/assets/workbenchSettings-CODFCzOg.js +8 -0
  147. package/dist/renderer/assets/workers-CRBS0W-v.js +2 -0
  148. package/dist/renderer/assets/xml-bR2Zh-HV.js +2 -0
  149. package/dist/renderer/assets/yaml-Bdk54Qrb.js +2 -0
  150. package/dist/renderer/entries/main/index.html +29 -6
  151. package/dist/renderer/entries/popover/index.html +7 -6
  152. package/dist/renderer/entries/settings/index.html +6 -5
  153. package/dist/renderer/entries/workbench-settings/index.html +6 -5
  154. package/dist/shared/constants.d.ts +0 -4
  155. package/dist/shared/constants.js +0 -4
  156. package/dist/shared/ipc-channels.d.ts +24 -0
  157. package/dist/shared/ipc-channels.js +37 -0
  158. package/dist/shared/ipc-schemas.d.ts +6 -0
  159. package/dist/shared/ipc-schemas.js +15 -0
  160. package/dist/shared/types.d.ts +37 -0
  161. package/dist/shared/vpath.d.ts +42 -0
  162. package/dist/shared/vpath.js +105 -0
  163. package/dist/simulator/assets/simulator-B4e3c6_O.js +9 -0
  164. package/dist/simulator/simulator.html +1 -1
  165. package/package.json +5 -4
  166. package/dist/renderer/assets/constants-DI382mTP.js +0 -2
  167. package/dist/renderer/assets/index-4lkyF644.js +0 -46
  168. package/dist/renderer/assets/input-D4byFOLS.js +0 -2
  169. package/dist/renderer/assets/ipc-transport-DqbRdZNO.css +0 -1
  170. package/dist/renderer/assets/popover-cxKrg_os.js +0 -2
  171. package/dist/renderer/assets/select-BgSkkdHW.js +0 -2
  172. package/dist/renderer/assets/settings-oD2K1jdT.js +0 -2
  173. package/dist/renderer/assets/workbenchSettings-CTNBpTft.js +0 -8
  174. package/dist/simulator/assets/simulator-DDRBJ6Wa.js +0 -9
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Pure dispatcher for `difile://` URL requests.
3
+ *
4
+ * Spec: `packages/devtools/docs/file-system.md` §4.3.
5
+ *
6
+ * The shipping implementation in `index.ts` registers a thin
7
+ * `simSession.protocol.handle('difile')` wrapper that delegates here; the race
8
+ * waiter on `_tmp/*` lives in `index.ts` because it owns the IPC lifecycle.
9
+ * For unit tests we assume the bytes are already in the store.
10
+ *
11
+ * Response shape:
12
+ * - 200: full body, with Content-Type, Cache-Control (immutable), ETag
13
+ * - 206: range slice, plus Content-Range
14
+ * - 304: empty body when `If-None-Match` matches the on-disk ETag. Per RFC
15
+ * 9110 §13.1.2 If-None-Match wins over Range.
16
+ * - 404: anything `resolveVPath` rejects, plus disk-side ENOENT and any
17
+ * other I/O error. The protocol handler in `index.ts` translates this
18
+ * into the renderer's network failure surface; we deliberately do not
19
+ * leak errno strings.
20
+ */
21
+ import { resolveVPath } from '../../../shared/vpath.js';
22
+ import { readDiskFile } from './disk.js';
23
+ const CACHE_CONTROL = 'public, max-age=31536000, immutable';
24
+ function getHeader(headers, name) {
25
+ if (!headers)
26
+ return undefined;
27
+ const lc = name.toLowerCase();
28
+ for (const k of Object.keys(headers)) {
29
+ if (k.toLowerCase() === lc)
30
+ return headers[k];
31
+ }
32
+ return undefined;
33
+ }
34
+ /**
35
+ * Weak ETag comparison per RFC 9110 §8.8.3.2: strip the optional `W/` prefix
36
+ * from each side before comparing. Both our generated ETags and conditional
37
+ * headers may or may not include the weak prefix; the comparison must accept
38
+ * `W/"abc"` and `"abc"` as equivalent.
39
+ */
40
+ function etagsMatch(a, b) {
41
+ if (!a)
42
+ return false;
43
+ const stripped = (s) => (s.startsWith('W/') ? s.slice(2) : s);
44
+ return stripped(a.trim()) === stripped(b);
45
+ }
46
+ /**
47
+ * Parse a `Range: bytes=<start>-<end>` header into an inclusive `{start,end}`.
48
+ * Returns `null` if absent or malformed (callers should treat that as "no
49
+ * range" — a full 200 response, not a 416). `totalSize` clamps the end.
50
+ */
51
+ function parseRange(header, totalSize) {
52
+ if (!header)
53
+ return null;
54
+ const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
55
+ if (!m)
56
+ return null;
57
+ const startStr = m[1];
58
+ const endStr = m[2];
59
+ if (startStr === '' && endStr === '')
60
+ return null;
61
+ if (startStr === '') {
62
+ // Suffix range: last N bytes.
63
+ const n = Number(endStr);
64
+ if (!Number.isFinite(n) || n <= 0)
65
+ return null;
66
+ const start = Math.max(0, totalSize - n);
67
+ return { start, end: totalSize - 1 };
68
+ }
69
+ const start = Number(startStr);
70
+ if (!Number.isFinite(start) || start < 0)
71
+ return null;
72
+ const end = endStr === '' ? totalSize - 1 : Number(endStr);
73
+ if (!Number.isFinite(end))
74
+ return null;
75
+ return { start, end };
76
+ }
77
+ function notFound() {
78
+ return new Response(null, { status: 404 });
79
+ }
80
+ function notModified(etag) {
81
+ return new Response(null, {
82
+ status: 304,
83
+ headers: {
84
+ ETag: etag,
85
+ 'Cache-Control': CACHE_CONTROL,
86
+ 'Access-Control-Allow-Origin': '*',
87
+ },
88
+ });
89
+ }
90
+ function bufferToBody(bytes) {
91
+ // Copy into a fresh ArrayBuffer so the result is definitely typed as
92
+ // `ArrayBuffer` (not `ArrayBufferLike` / `SharedArrayBuffer`) and is
93
+ // safe to hand to Response without aliasing the Node Buffer pool.
94
+ const out = new ArrayBuffer(bytes.byteLength);
95
+ new Uint8Array(out).set(bytes);
96
+ return out;
97
+ }
98
+ function tempBody(bytes, mime) {
99
+ return new Response(bufferToBody(bytes), {
100
+ status: 200,
101
+ headers: {
102
+ 'Content-Type': mime,
103
+ 'Cache-Control': CACHE_CONTROL,
104
+ 'Access-Control-Allow-Origin': '*',
105
+ },
106
+ });
107
+ }
108
+ export async function handleDifileRequest(ctx, req) {
109
+ const v = resolveVPath(req.url);
110
+ if (!v)
111
+ return notFound();
112
+ if (v.kind === 'tmp') {
113
+ const record = ctx.tempStore.get(req.url);
114
+ if (!record)
115
+ return notFound();
116
+ return tempBody(record.bytes, record.mime || 'application/octet-stream');
117
+ }
118
+ if (!v.realPath)
119
+ return notFound();
120
+ // Disk-backed: _store or usr.
121
+ try {
122
+ // Probe first to learn the ETag/size so If-None-Match can short-circuit
123
+ // without reading the body. The probe also doubles as our existence /
124
+ // permission check, so anything that throws here (ENOENT, EACCES, ...)
125
+ // surfaces as a 404.
126
+ const probe = await readDiskFile(v.realPath);
127
+ const etag = probe.etag;
128
+ const totalSize = probe.totalSize;
129
+ const mime = probe.mime;
130
+ const ifNoneMatch = getHeader(req.headers, 'If-None-Match');
131
+ if (etagsMatch(ifNoneMatch, etag)) {
132
+ return notModified(etag);
133
+ }
134
+ const rangeHeader = getHeader(req.headers, 'Range');
135
+ const range = parseRange(rangeHeader, totalSize);
136
+ if (range) {
137
+ // Range out of bounds → 416 (Range Not Satisfiable) per RFC 9110 §15.4.
138
+ // Distinct from 404 so callers can tell "wrong file" from "wrong slice".
139
+ if (range.start < 0 || range.start >= totalSize || range.end < range.start) {
140
+ return new Response(null, {
141
+ status: 416,
142
+ headers: {
143
+ 'Content-Range': `bytes */${totalSize}`,
144
+ 'Access-Control-Allow-Origin': '*',
145
+ },
146
+ });
147
+ }
148
+ const clampedEnd = Math.min(range.end, totalSize - 1);
149
+ const sliced = await readDiskFile(v.realPath, { range: { start: range.start, end: clampedEnd } });
150
+ const { bytes } = sliced;
151
+ const lastByte = range.start + bytes.length - 1;
152
+ return new Response(bufferToBody(bytes), {
153
+ status: 206,
154
+ headers: {
155
+ 'Content-Type': mime,
156
+ 'Content-Length': String(bytes.length),
157
+ 'Content-Range': `bytes ${range.start}-${lastByte}/${totalSize}`,
158
+ 'Cache-Control': CACHE_CONTROL,
159
+ ETag: etag,
160
+ 'Access-Control-Allow-Origin': '*',
161
+ },
162
+ });
163
+ }
164
+ return new Response(bufferToBody(probe.bytes), {
165
+ status: 200,
166
+ headers: {
167
+ 'Content-Type': mime,
168
+ 'Cache-Control': CACHE_CONTROL,
169
+ ETag: etag,
170
+ 'Access-Control-Allow-Origin': '*',
171
+ },
172
+ });
173
+ }
174
+ catch {
175
+ return notFound();
176
+ }
177
+ }
178
+ //# sourceMappingURL=request-handler.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pure lookup helper backing the main-process `difile://_tmp/*` protocol
3
+ * handler. Receives the shared {@link TempFileStore} that the
4
+ * `simulator:temp-file:*` IPC channels populate from the renderer.
5
+ *
6
+ * The full URL (including scheme + host) is used as the Map key — the resolver
7
+ * never re-parses or normalises it, so any URL with a wrong scheme or host
8
+ * misses regardless of suffix similarity.
9
+ */
10
+ export interface TempFileRecord {
11
+ bytes: Buffer;
12
+ mime: string;
13
+ }
14
+ export type TempFileStore = Map<string, TempFileRecord>;
15
+ export declare function resolveTempFile(store: TempFileStore, url: string): {
16
+ status: 200;
17
+ bytes: Buffer;
18
+ mime: string;
19
+ } | {
20
+ status: 404;
21
+ };
22
+ //# sourceMappingURL=resolver.d.ts.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pure lookup helper backing the main-process `difile://_tmp/*` protocol
3
+ * handler. Receives the shared {@link TempFileStore} that the
4
+ * `simulator:temp-file:*` IPC channels populate from the renderer.
5
+ *
6
+ * The full URL (including scheme + host) is used as the Map key — the resolver
7
+ * never re-parses or normalises it, so any URL with a wrong scheme or host
8
+ * misses regardless of suffix similarity.
9
+ */
10
+ export function resolveTempFile(store, url) {
11
+ if (!url.startsWith('difile://_tmp/'))
12
+ return { status: 404 };
13
+ const record = store.get(url);
14
+ if (!record)
15
+ return { status: 404 };
16
+ return {
17
+ status: 200,
18
+ bytes: record.bytes,
19
+ mime: record.mime || 'application/octet-stream',
20
+ };
21
+ }
22
+ //# sourceMappingURL=resolver.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Mutators for the shared {@link TempFileStore}. The renderer-side bridge
3
+ * forwards every `createTempFilePath` / `registerTempFilePath` /
4
+ * `revokeTempFilePath` / `revokeAllTempFilePaths` call over IPC, and these
5
+ * helpers translate the payloads into Map writes.
6
+ *
7
+ * ArrayBuffer inputs are normalised to Buffer at the boundary so the
8
+ * resolver hot path can return the bytes directly without re-wrapping.
9
+ */
10
+ import type { TempFileStore } from './resolver.js';
11
+ export declare function registerTempFile(store: TempFileStore, path: string, mime: string, bytes: ArrayBuffer | Buffer): void;
12
+ export declare function revokeTempFile(store: TempFileStore, path: string): void;
13
+ export declare function revokeAllTempFiles(store: TempFileStore): void;
14
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Mutators for the shared {@link TempFileStore}. The renderer-side bridge
3
+ * forwards every `createTempFilePath` / `registerTempFilePath` /
4
+ * `revokeTempFilePath` / `revokeAllTempFilePaths` call over IPC, and these
5
+ * helpers translate the payloads into Map writes.
6
+ *
7
+ * ArrayBuffer inputs are normalised to Buffer at the boundary so the
8
+ * resolver hot path can return the bytes directly without re-wrapping.
9
+ */
10
+ export function registerTempFile(store, path, mime, bytes) {
11
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(new Uint8Array(bytes));
12
+ // delete-then-set so an existing entry's insertion order is refreshed.
13
+ // Without this, a FIFO cap eviction over `store.keys()` could drop a
14
+ // recently re-written path.
15
+ store.delete(path);
16
+ store.set(path, { bytes: buf, mime });
17
+ }
18
+ export function revokeTempFile(store, path) {
19
+ store.delete(path);
20
+ }
21
+ export function revokeAllTempFiles(store) {
22
+ store.clear();
23
+ }
24
+ //# sourceMappingURL=store.js.map
@@ -4,8 +4,8 @@ import type { ToolbarAction, ToolbarActionInput } from '../../../shared/types.js
4
4
  *
5
5
  * The toolbar is modelled as "one table the host recomputes from its current
6
6
  * state": `set()` replaces the whole table atomically. The non-serialisable
7
- * `handler` is kept main-process side; `list()` projects to `{id,label}[]`
8
- * for the IPC boundary, and `getHandler()` resolves a handler by id.
7
+ * `handler` is kept main-process side; `list()` projects display metadata for
8
+ * the IPC boundary, and `getHandler()` resolves a handler by id.
9
9
  */
10
10
  export interface ToolbarStore {
11
11
  /**
@@ -14,7 +14,7 @@ export interface ToolbarStore {
14
14
  * a rejected batch leaves the previous table intact.
15
15
  */
16
16
  set(actions: ToolbarActionInput[]): void;
17
- /** Project the current table to `{id,label}[]` — no `handler` leaks out. */
17
+ /** Project the current table to serialisable display metadata — no `handler` leaks out. */
18
18
  list(): ToolbarAction[];
19
19
  /** Resolve the handler stored under `id`, or `undefined` if unknown. */
20
20
  getHandler(id: string): (() => void | Promise<void>) | undefined;
@@ -1,3 +1,20 @@
1
+ function toPublicAction(action) {
2
+ const projected = {
3
+ id: action.id,
4
+ label: action.label,
5
+ };
6
+ if (action.kind)
7
+ projected.kind = action.kind;
8
+ if (action.placement)
9
+ projected.placement = action.placement;
10
+ if (action.icon)
11
+ projected.icon = action.icon;
12
+ if (action.displayInitial)
13
+ projected.displayInitial = action.displayInitial;
14
+ if (action.avatarUrl)
15
+ projected.avatarUrl = action.avatarUrl;
16
+ return projected;
17
+ }
1
18
  export function createToolbarStore() {
2
19
  let actions = [];
3
20
  return {
@@ -13,7 +30,7 @@ export function createToolbarStore() {
13
30
  actions = next.slice();
14
31
  },
15
32
  list() {
16
- return actions.map(({ id, label }) => ({ id, label }));
33
+ return actions.map(toPublicAction);
17
34
  },
18
35
  getHandler(id) {
19
36
  return actions.find((a) => a.id === id)?.handler;
@@ -24,6 +24,16 @@ export declare class UpdateManager {
24
24
  private getCurrentVersion;
25
25
  private latestUpdate;
26
26
  private downloadedPath;
27
+ /**
28
+ * Whether this instance has already emitted `Available` once. The periodic
29
+ * check fires forever (interval default 1h), and downstream UI (qdmp shell
30
+ * renders a native toast / Notification) is not idempotent — every event
31
+ * stacks. We dedupe at the source: at most one Available per instance
32
+ * lifetime, regardless of how the upstream `info.version` shifts. The user
33
+ * already knows "an update exists"; tracking minor version bumps mid-
34
+ * session is not worth duplicating the toast.
35
+ */
36
+ private hasNotified;
27
37
  private timer;
28
38
  private initialTimer;
29
39
  private ipc;
@@ -7,6 +7,16 @@ export class UpdateManager {
7
7
  getCurrentVersion;
8
8
  latestUpdate = null;
9
9
  downloadedPath = null;
10
+ /**
11
+ * Whether this instance has already emitted `Available` once. The periodic
12
+ * check fires forever (interval default 1h), and downstream UI (qdmp shell
13
+ * renders a native toast / Notification) is not idempotent — every event
14
+ * stacks. We dedupe at the source: at most one Available per instance
15
+ * lifetime, regardless of how the upstream `info.version` shifts. The user
16
+ * already knows "an update exists"; tracking minor version bumps mid-
17
+ * session is not worth duplicating the toast.
18
+ */
19
+ hasNotified = false;
10
20
  timer = null;
11
21
  initialTimer = null;
12
22
  ipc;
@@ -85,9 +95,12 @@ export class UpdateManager {
85
95
  }
86
96
  async checkAndNotify() {
87
97
  const result = await this.check();
88
- if (result.hasUpdate && result.info) {
89
- this.mainWindow.webContents.send(UpdateChannel.Available, result.info);
90
- }
98
+ if (!result.hasUpdate || !result.info)
99
+ return;
100
+ if (this.hasNotified)
101
+ return;
102
+ this.hasNotified = true;
103
+ this.mainWindow.webContents.send(UpdateChannel.Available, result.info);
91
104
  }
92
105
  }
93
106
  //# sourceMappingURL=update-manager.js.map
@@ -20,13 +20,14 @@ export interface ViewManagerContext {
20
20
  * Unified lifecycle manager for Electron WebContentsView overlays.
21
21
  *
22
22
  * Owns creation / attachment / detachment / positioning / destruction of
23
- * every overlay view hung off the main window's contentView (simulator,
24
- * settings, popover). All `new WebContentsView`, `addChildView`,
23
+ * every overlay view hung off the main window's contentView (simulator
24
+ * DevTools, settings, popover). All `new WebContentsView`, `addChildView`,
25
25
  * `removeChildView`, `webContents.destroy()` and overlay `setBounds` calls
26
26
  * should live here — IPC handlers just call into the manager.
27
27
  *
28
- * The main window's own renderer (added in `windows/main-window/create.ts`)
29
- * is the main content root and is not an overlay, so it is not managed here.
28
+ * The code editor is NOT an overlay: it renders as an in-renderer
29
+ * `<MonacoEditor/>` React component inside the main window. The main
30
+ * window's own renderer is the content root and is not managed here.
30
31
  */
31
32
  export interface ViewManager {
32
33
  /**
@@ -78,6 +79,18 @@ export interface ViewManager {
78
79
  resize(simWidth: number): void;
79
80
  /** Show or hide the simulator overlay based on visibility flag. */
80
81
  setVisible(visible: boolean, simWidth: number): void;
82
+ /**
83
+ * Apply a renderer-measured rectangle to the simulator's Chromium
84
+ * DevTools overlay view. `{ width: 0, height: 0 }` is treated as "hide" —
85
+ * the view is removed from the contentView but its WebContents is kept
86
+ * alive so re-showing it doesn't re-pay the DevTools bootstrap.
87
+ */
88
+ setSimulatorDevtoolsBounds(bounds: {
89
+ x: number;
90
+ y: number;
91
+ width: number;
92
+ height: number;
93
+ }): void;
81
94
  }
82
95
  /**
83
96
  * Build a ViewManager bound to the given context. The returned object is the
@@ -23,6 +23,11 @@ export function createViewManager(ctx) {
23
23
  let popoverView = null;
24
24
  let lastSimWidth = 375;
25
25
  let simulatorWebContentsId = null;
26
+ // Renderer-driven overlay bounds for the simulator DevTools view. When
27
+ // non-null this takes precedence over the legacy layout computed from the
28
+ // window content size. A zero-area rectangle means "hide" — the overlay is
29
+ // removed from the contentView but its WebContents stays alive.
30
+ let simulatorBoundsOverride = null;
26
31
  // ── Internal helpers ────────────────────────────────────────────────────
27
32
  function destroyViewInternal(view) {
28
33
  if (!view)
@@ -43,9 +48,42 @@ export function createViewManager(ctx) {
43
48
  function applySimulatorBounds(simWidth) {
44
49
  if (!simulatorView || ctx.windows.mainWindow.isDestroyed())
45
50
  return;
51
+ if (simulatorBoundsOverride) {
52
+ simulatorView.setBounds(simulatorBoundsOverride);
53
+ return;
54
+ }
46
55
  const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
47
56
  simulatorView.setBounds(layout.computeSimulatorBounds(w, h, simWidth, headerHeight));
48
57
  }
58
+ // Renderer publishes width/height = 0 to mean "hide overlay" — the
59
+ // surrounding React panel is unmounted/collapsed. We keep the cached
60
+ // value (so future republishes win over legacy layout) but remove the
61
+ // child view from the contentView until a non-empty rect arrives.
62
+ function isHidden(b) {
63
+ return b.width <= 0 || b.height <= 0;
64
+ }
65
+ function setSimulatorDevtoolsBounds(bounds) {
66
+ simulatorBoundsOverride = bounds;
67
+ if (!simulatorView || simulatorView.webContents.isDestroyed())
68
+ return;
69
+ if (ctx.windows.mainWindow.isDestroyed())
70
+ return;
71
+ if (isHidden(bounds)) {
72
+ if (simulatorViewAdded) {
73
+ try {
74
+ ctx.windows.mainWindow.contentView.removeChildView(simulatorView);
75
+ }
76
+ catch { /* already removed */ }
77
+ simulatorViewAdded = false;
78
+ }
79
+ return;
80
+ }
81
+ if (!simulatorViewAdded) {
82
+ ctx.windows.mainWindow.contentView.addChildView(simulatorView);
83
+ simulatorViewAdded = true;
84
+ }
85
+ simulatorView.setBounds(bounds);
86
+ }
49
87
  function applySettingsBounds() {
50
88
  if (!settingsView || ctx.windows.mainWindow.isDestroyed())
51
89
  return;
@@ -82,9 +120,13 @@ export function createViewManager(ctx) {
82
120
  sim.setDevToolsWebContents(simulatorView.webContents);
83
121
  sim.openDevTools();
84
122
  // Default DevTools to Console panel (Chrome DevTools defaults to Elements).
85
- // The DevTools UI renders inside simulatorView.webContents; we click the
86
- // Console tab once its header is laid out, and persist the choice via
87
- // localStorage so subsequent reloads honor it.
123
+ // The DevTools UI lives inside closed shadow roots, so a light-DOM
124
+ // querySelector('[role="tab"]') cannot reach the tab bar to click it.
125
+ // Instead drive the front-end's own view manager: the bundled DevTools
126
+ // exposes `UI.ViewManager.instance().showView(id)` on `globalThis.UI`
127
+ // once the front-end has finished bootstrapping. We poll for it and
128
+ // request the `console` view, and also persist the choice via the
129
+ // `panel-selectedTab` localStorage key so subsequent reloads honor it.
88
130
  const devtoolsWc = simulatorView.webContents;
89
131
  devtoolsWc.once('dom-ready', () => {
90
132
  devtoolsWc.executeJavaScript(`
@@ -93,24 +135,35 @@ export function createViewManager(ctx) {
93
135
  let tries = 0
94
136
  const timer = setInterval(() => {
95
137
  tries++
96
- const tabs = document.querySelectorAll('[role="tab"], .tabbed-pane-header-tab')
97
- for (const tab of tabs) {
98
- const label = (tab.getAttribute('aria-label') || tab.textContent || '').trim().toLowerCase()
99
- if (label === 'console' || label.startsWith('console')) {
100
- tab.click()
138
+ try {
139
+ const UI = globalThis.UI
140
+ const vm = UI && UI.ViewManager && typeof UI.ViewManager.instance === 'function'
141
+ ? UI.ViewManager.instance()
142
+ : null
143
+ if (vm && typeof vm.showView === 'function') {
144
+ vm.showView('console')
101
145
  clearInterval(timer)
102
146
  return
103
147
  }
104
- }
105
- if (tries > 40) clearInterval(timer)
106
- }, 100)
148
+ } catch {}
149
+ if (tries > 80) clearInterval(timer)
150
+ }, 50)
107
151
  })()
108
152
  `).catch(() => { });
109
153
  });
110
154
  if (getDefaultTab(ctx) === 'simulator') {
111
- ctx.windows.mainWindow.contentView.addChildView(simulatorView);
112
- simulatorViewAdded = true;
113
- applySimulatorBounds(simWidth);
155
+ if (simulatorBoundsOverride) {
156
+ if (!isHidden(simulatorBoundsOverride)) {
157
+ ctx.windows.mainWindow.contentView.addChildView(simulatorView);
158
+ simulatorViewAdded = true;
159
+ simulatorView.setBounds(simulatorBoundsOverride);
160
+ }
161
+ }
162
+ else {
163
+ ctx.windows.mainWindow.contentView.addChildView(simulatorView);
164
+ simulatorViewAdded = true;
165
+ applySimulatorBounds(simWidth);
166
+ }
114
167
  }
115
168
  }
116
169
  function detachSimulator() {
@@ -123,6 +176,9 @@ export function createViewManager(ctx) {
123
176
  simulatorView = null;
124
177
  simulatorViewAdded = false;
125
178
  simulatorWebContentsId = null;
179
+ // Drop the renderer-published rect so a stale "hidden" override doesn't
180
+ // suppress the next view before its renderer republishes.
181
+ simulatorBoundsOverride = null;
126
182
  }
127
183
  function showSimulator(simWidth) {
128
184
  lastSimWidth = simWidth;
@@ -276,6 +332,7 @@ export function createViewManager(ctx) {
276
332
  },
277
333
  resize,
278
334
  setVisible,
335
+ setSimulatorDevtoolsBounds,
279
336
  };
280
337
  }
281
338
  //# sourceMappingURL=view-manager.js.map
@@ -10,7 +10,7 @@ import { type SimulatorApiRegistry } from './simulator/custom-apis.js';
10
10
  import { type ToolbarStore } from './toolbar/toolbar-store.js';
11
11
  import { sanitizeTemplates } from './projects/templates.js';
12
12
  import type { ProjectsProvider, ProjectTemplate } from './projects/types.js';
13
- import type { CustomCreateProjectDialogResult } from '../../shared/types.js';
13
+ import type { BeforeOpenProjectResult, CustomCreateProjectDialogResult } from '../../shared/types.js';
14
14
  /**
15
15
  * Shared mutable state for the workbench application.
16
16
  * Passed to each IPC module so they can read/write shared state without closures.
@@ -67,6 +67,11 @@ export interface WorkbenchContext {
67
67
  parentWindow: BrowserWindow;
68
68
  templates: ProjectTemplate[];
69
69
  }) => Promise<CustomCreateProjectDialogResult>;
70
+ /**
71
+ * Optional host gate for opening projects. Runs after basic directory
72
+ * validation and before the current project session is closed.
73
+ */
74
+ beforeOpenProject?: (projectPath: string) => BeforeOpenProjectResult | Promise<BeforeOpenProjectResult>;
70
75
  /**
71
76
  * Trust predicate consulted by `IpcRegistry` for every incoming IPC.
72
77
  * Resolves the currently-trusted senders (main renderer + overlays)
@@ -38,6 +38,14 @@ export interface WorkspaceService {
38
38
  appInfo: unknown;
39
39
  } | null;
40
40
  getProjectPath(): string;
41
+ /**
42
+ * The project path most recently torn down by `closeProject`, or '' if none
43
+ * since the last `openProject`. Lets the project-fs sandbox accept an
44
+ * in-flight write (debounced/teardown flush) that targets the just-closed
45
+ * project even though `getProjectPath()` has already been cleared. Reset to
46
+ * '' on the next `openProject` so it never accumulates stale roots.
47
+ */
48
+ getLastClosedProjectPath(): string;
41
49
  hasActiveSession(): boolean;
42
50
  captureThumbnail(projectPath: string): Promise<string | null>;
43
51
  getThumbnail(projectPath: string): Promise<string | null>;
@@ -6,6 +6,10 @@ import { loadWorkbenchSettings } from '../settings/index.js';
6
6
  export function createWorkspaceService(ctx) {
7
7
  let currentSession = null;
8
8
  let currentProjectPath = '';
9
+ // The last project path cleared by `closeProject`; consumed by project-fs to
10
+ // accept an in-flight write that targets the just-closed project. Reset at
11
+ // the start of every `openProject` so it never accumulates a stale root.
12
+ let lastClosedProjectPath = '';
9
13
  function sendStatus(status, message, hotReload) {
10
14
  ctx.notify.projectStatus(hotReload ? { status, message, hotReload: true } : { status, message });
11
15
  }
@@ -58,9 +62,6 @@ export function createWorkspaceService(ctx) {
58
62
  ? provider.validateProjectDir(dirPath)
59
63
  : null,
60
64
  async openProject(projectPath) {
61
- clearSimulatorServicewechatReferer();
62
- await disposeSession();
63
- currentProjectPath = '';
64
65
  // Reject obviously broken projects up front so we never spin up a
65
66
  // compile/server that the simulator will then fetch asset-by-asset.
66
67
  // Without this guard, deleted recent-list entries and typo'd CLI paths
@@ -75,6 +76,18 @@ export function createWorkspaceService(ctx) {
75
76
  sendStatus('error', dirError);
76
77
  return { success: false, error: dirError };
77
78
  }
79
+ const gate = ctx.beforeOpenProject ? await ctx.beforeOpenProject(projectPath) : undefined;
80
+ if (gate && !gate.allow) {
81
+ const message = gate.userMessage ?? gate.reason;
82
+ sendStatus('error', message);
83
+ return { success: false, error: message };
84
+ }
85
+ clearSimulatorServicewechatReferer();
86
+ await disposeSession();
87
+ currentProjectPath = '';
88
+ // A fresh open starts a clean sandbox window — drop any previously
89
+ // recorded last-closed root so it can't accept writes after this point.
90
+ lastClosedProjectPath = '';
78
91
  sendStatus('compiling', '正在编译...');
79
92
  const { compile } = loadWorkbenchSettings();
80
93
  let session;
@@ -98,7 +111,7 @@ export function createWorkspaceService(ctx) {
98
111
  if (provider.updateLastOpened)
99
112
  provider.updateLastOpened(projectPath);
100
113
  });
101
- bestEffort('sendStatus', () => sendStatus('ready', '编译完成'));
114
+ bestEffort('sendStatus', () => sendStatus('ready', gate?.statusMessage ?? '编译完成'));
102
115
  bestEffort('applyReferer', () => applyRefererFromSession(session));
103
116
  return {
104
117
  success: true,
@@ -109,11 +122,16 @@ export function createWorkspaceService(ctx) {
109
122
  async closeProject() {
110
123
  clearSimulatorServicewechatReferer();
111
124
  await disposeSession();
125
+ // Record the root being torn down BEFORE clearing it, so a teardown/
126
+ // debounced write already in flight can still be accepted against it.
127
+ if (currentProjectPath !== '')
128
+ lastClosedProjectPath = currentProjectPath;
112
129
  currentProjectPath = '';
113
130
  ctx.views.disposeAll();
114
131
  },
115
132
  getSession: () => currentSession,
116
133
  getProjectPath: () => currentProjectPath,
134
+ getLastClosedProjectPath: () => lastClosedProjectPath,
117
135
  hasActiveSession: () => currentSession !== null,
118
136
  async captureThumbnail(projectPath) {
119
137
  const wc = ctx.views.getSimulatorWebContents();
@@ -1,4 +1,4 @@
1
- import type { WebContents } from 'electron';
1
+ import type { IpcMainEvent, WebContents } from 'electron';
2
2
  import { ipcMain } from 'electron';
3
3
  import { type Disposable } from './disposable.js';
4
4
  type HandleFn = Parameters<typeof ipcMain.handle>[1];
@@ -32,6 +32,15 @@ export declare class IpcRegistry implements Disposable {
32
32
  private registry;
33
33
  constructor(policy?: SenderPolicy | undefined);
34
34
  handle(channel: string, fn: HandleFn): this;
35
+ /**
36
+ * Register a SYNCHRONOUS `ipcRenderer.sendSync` handler. Unlike {@link on}
37
+ * (fire-and-forget), the renderer is blocked until we set `event.returnValue`,
38
+ * so `fn` MUST be synchronous and return the value to hand back. The sender
39
+ * policy gates it like {@link handle}; a rejected sender or a thrown error
40
+ * yields a structured `{ ok: false, code, message }` returnValue instead of
41
+ * the silent drop `on` uses, so the blocked renderer always gets an answer.
42
+ */
43
+ handleSync(channel: string, fn: (event: IpcMainEvent, ...args: unknown[]) => unknown): this;
35
44
  on(channel: string, fn: ListenerFn): this;
36
45
  dispose(): Promise<void>;
37
46
  }