@mindexec/cli 0.2.458 → 0.2.459

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 (41) hide show
  1. package/desktop-workspace-state.cjs +120 -0
  2. package/electron/main.cjs +17 -6
  3. package/electron/source-smoke.mjs +21 -0
  4. package/electron/windows-package-smoke.mjs +8 -2
  5. package/package.json +6 -5
  6. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  7. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  8. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  9. package/scripts/desktop-workspace-state-smoke.mjs +81 -0
  10. package/server.js +36 -16
  11. package/wwwroot/_content/MindExecution.Shared/js/mind-map-animated-image-preview.js +270 -0
  12. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +57 -53
  13. package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +30 -26
  14. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +94 -124
  15. package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +43 -21
  16. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +45 -13
  17. package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +117 -1
  18. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +4 -1
  19. package/wwwroot/_framework/{MindExecution.Core.2ch68iyy8o.dll → MindExecution.Core.b973f5f64y.dll} +0 -0
  20. package/wwwroot/_framework/{MindExecution.Kernel.dh617xfv36.dll → MindExecution.Kernel.k4h9fvi9wb.dll} +0 -0
  21. package/wwwroot/_framework/{MindExecution.Plugins.Admin.0sm50hbae9.dll → MindExecution.Plugins.Admin.u2emae8cb5.dll} +0 -0
  22. package/wwwroot/_framework/{MindExecution.Plugins.Business.3wq01orrbu.dll → MindExecution.Plugins.Business.cysw4qtyke.dll} +0 -0
  23. package/wwwroot/_framework/{MindExecution.Plugins.Concept.z8yl28fa2a.dll → MindExecution.Plugins.Concept.8iehgvzjio.dll} +0 -0
  24. package/wwwroot/_framework/{MindExecution.Plugins.Directory.m7murp5oes.dll → MindExecution.Plugins.Directory.t621fsxq2i.dll} +0 -0
  25. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.yuyrbpf0vh.dll → MindExecution.Plugins.PlanMaster.ft631uc7ki.dll} +0 -0
  26. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.rwxvn00rm2.dll → MindExecution.Plugins.YouTube.arivgu92h1.dll} +0 -0
  27. package/wwwroot/_framework/{MindExecution.Shared.r9k48iyijb.dll → MindExecution.Shared.wm997st9eb.dll} +0 -0
  28. package/wwwroot/_framework/{MindExecution.Web.742aribkxm.dll → MindExecution.Web.osdbdrue4h.dll} +0 -0
  29. package/wwwroot/_framework/blazor.boot.json +21 -21
  30. package/wwwroot/app-icon-1024.png +0 -0
  31. package/wwwroot/apple-touch-icon.png +0 -0
  32. package/wwwroot/appsettings.json +81 -81
  33. package/wwwroot/favicon-32x32.png +0 -0
  34. package/wwwroot/favicon.ico +0 -0
  35. package/wwwroot/icon-192.png +0 -0
  36. package/wwwroot/icon-512.png +0 -0
  37. package/wwwroot/index.html +70 -46
  38. package/wwwroot/manifest.webmanifest +4 -4
  39. package/wwwroot/mindexec-favicon-v3.png +0 -0
  40. package/wwwroot/service-worker-assets.js +888 -880
  41. package/wwwroot/service-worker.js +1 -1
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const DESKTOP_WORKSPACE_STATE_VERSION = 1;
7
+
8
+ function normalizeWorkspacePath(value) {
9
+ const candidate = String(value || '').trim();
10
+ return candidate ? path.resolve(candidate) : '';
11
+ }
12
+
13
+ function isExistingDirectory(candidatePath) {
14
+ if (!candidatePath) {
15
+ return false;
16
+ }
17
+
18
+ try {
19
+ return fs.statSync(candidatePath).isDirectory();
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ function loadDesktopWorkspaceState(statePath) {
26
+ const resolvedStatePath = normalizeWorkspacePath(statePath);
27
+ if (!resolvedStatePath) {
28
+ return null;
29
+ }
30
+
31
+ try {
32
+ const payload = JSON.parse(fs.readFileSync(resolvedStatePath, 'utf8'));
33
+ const workspacePath = normalizeWorkspacePath(payload?.workspacePath);
34
+ if (Number(payload?.version) !== DESKTOP_WORKSPACE_STATE_VERSION || !workspacePath) {
35
+ return null;
36
+ }
37
+
38
+ return {
39
+ version: DESKTOP_WORKSPACE_STATE_VERSION,
40
+ workspacePath
41
+ };
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function resolveDesktopWorkspace(options = {}) {
48
+ const explicitWorkspace = normalizeWorkspacePath(options.explicitWorkspace);
49
+ if (explicitWorkspace) {
50
+ return {
51
+ workspacePath: explicitWorkspace,
52
+ source: 'explicit'
53
+ };
54
+ }
55
+
56
+ const savedState = loadDesktopWorkspaceState(options.statePath);
57
+ if (savedState && isExistingDirectory(savedState.workspacePath)) {
58
+ return {
59
+ workspacePath: savedState.workspacePath,
60
+ source: 'saved'
61
+ };
62
+ }
63
+
64
+ const fallbackWorkspace = normalizeWorkspacePath(options.fallbackWorkspace);
65
+ if (!fallbackWorkspace) {
66
+ throw new Error('A fallback desktop workspace is required.');
67
+ }
68
+
69
+ return {
70
+ workspacePath: fallbackWorkspace,
71
+ source: 'fallback'
72
+ };
73
+ }
74
+
75
+ function saveDesktopWorkspaceState(statePath, workspacePath) {
76
+ const resolvedStatePath = normalizeWorkspacePath(statePath);
77
+ if (!resolvedStatePath) {
78
+ return false;
79
+ }
80
+
81
+ const resolvedWorkspacePath = normalizeWorkspacePath(workspacePath);
82
+ if (!isExistingDirectory(resolvedWorkspacePath)) {
83
+ throw new Error(`Desktop workspace must be an existing directory: ${resolvedWorkspacePath || workspacePath}`);
84
+ }
85
+
86
+ fs.mkdirSync(path.dirname(resolvedStatePath), { recursive: true });
87
+ const payload = {
88
+ version: DESKTOP_WORKSPACE_STATE_VERSION,
89
+ workspacePath: resolvedWorkspacePath
90
+ };
91
+ const tempPath = `${resolvedStatePath}.${process.pid}.${Date.now()}.tmp`;
92
+
93
+ try {
94
+ fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, {
95
+ encoding: 'utf8',
96
+ mode: 0o600
97
+ });
98
+ try {
99
+ fs.renameSync(tempPath, resolvedStatePath);
100
+ } catch (error) {
101
+ if (error?.code !== 'EEXIST' && error?.code !== 'EPERM' && error?.code !== 'ENOTEMPTY') {
102
+ throw error;
103
+ }
104
+
105
+ fs.rmSync(resolvedStatePath, { force: true });
106
+ fs.renameSync(tempPath, resolvedStatePath);
107
+ }
108
+ } finally {
109
+ fs.rmSync(tempPath, { force: true });
110
+ }
111
+
112
+ return true;
113
+ }
114
+
115
+ module.exports = {
116
+ DESKTOP_WORKSPACE_STATE_VERSION,
117
+ loadDesktopWorkspaceState,
118
+ resolveDesktopWorkspace,
119
+ saveDesktopWorkspaceState
120
+ };
package/electron/main.cjs CHANGED
@@ -6,6 +6,9 @@ const fs = require('fs');
6
6
  const http = require('http');
7
7
  const net = require('net');
8
8
  const path = require('path');
9
+ const workspaceState = require('../desktop-workspace-state.cjs');
10
+
11
+ const { resolveDesktopWorkspace } = workspaceState;
9
12
 
10
13
  const DEFAULT_BRIDGE_PORT = 5147;
11
14
  const DEFAULT_REMOTE_HUB_PORT = 5198;
@@ -20,6 +23,7 @@ let requestedExitCode = 0;
20
23
  let activeBridgePort = DEFAULT_BRIDGE_PORT;
21
24
  let activeRemoteHubPort = DEFAULT_REMOTE_HUB_PORT;
22
25
  let activeWorkspace = '';
26
+ let desktopWorkspaceStatePath = '';
23
27
 
24
28
  function resolveDesktopIconPath() {
25
29
  const iconPath = path.resolve(__dirname, '..', 'wwwroot', 'icon-512.png');
@@ -121,6 +125,7 @@ function spawnBundledBridge(packageRoot) {
121
125
  ELECTRON_RUN_AS_NODE: '1',
122
126
  MINDEXEC_DESKTOP: '1',
123
127
  MINDEXEC_NO_OPEN: '1',
128
+ MINDEXEC_DESKTOP_WORKSPACE_STATE_PATH: desktopWorkspaceStatePath,
124
129
  WORKSPACE_PATH: activeWorkspace,
125
130
  BRIDGE_PORT: String(activeBridgePort),
126
131
  REMOTE_HUB_PORT: String(activeRemoteHubPort)
@@ -180,15 +185,21 @@ async function prepareBundledRuntime() {
180
185
  'RemoteHub',
181
186
  activeBridgePort
182
187
  );
183
- activeWorkspace = path.resolve(
184
- process.env.MINDEXEC_DESKTOP_WORKSPACE
185
- || process.env.WORKSPACE_PATH
186
- || path.join(app.getPath('documents'), 'MindExecution')
188
+ desktopWorkspaceStatePath = path.resolve(
189
+ process.env.MINDEXEC_DESKTOP_WORKSPACE_STATE_PATH
190
+ || path.join(app.getPath('userData'), 'workspace-state.json')
187
191
  );
192
+ const workspaceResolution = resolveDesktopWorkspace({
193
+ explicitWorkspace: process.env.MINDEXEC_DESKTOP_WORKSPACE || process.env.WORKSPACE_PATH,
194
+ statePath: desktopWorkspaceStatePath,
195
+ fallbackWorkspace: path.join(app.getPath('documents'), 'MindExecution')
196
+ });
197
+ activeWorkspace = workspaceResolution.workspacePath;
188
198
 
189
199
  fs.mkdirSync(activeWorkspace, { recursive: true });
190
200
  appendDesktopLog(
191
- `Starting bundled runtime: bridge=${activeBridgePort}, remoteHub=${activeRemoteHubPort}, workspace=${activeWorkspace}`
201
+ `Starting bundled runtime: bridge=${activeBridgePort}, remoteHub=${activeRemoteHubPort}, ` +
202
+ `workspace=${activeWorkspace}, workspaceSource=${workspaceResolution.source}`
192
203
  );
193
204
  spawnBundledBridge(packageRoot);
194
205
  return waitForBundledBridge();
@@ -203,7 +214,7 @@ function createMainWindow() {
203
214
  minHeight: 640,
204
215
  show: !hiddenSmokeMode,
205
216
  autoHideMenuBar: true,
206
- backgroundColor: '#0b1020',
217
+ backgroundColor: '#f8fafc',
207
218
  title: 'MindExec',
208
219
  icon: resolveDesktopIconPath(),
209
220
  webPreferences: {
@@ -8,12 +8,14 @@ const electronDirectory = path.dirname(fileURLToPath(import.meta.url));
8
8
  const packageRoot = path.resolve(electronDirectory, '..');
9
9
  const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
10
10
  const mainSource = fs.readFileSync(path.join(electronDirectory, 'main.cjs'), 'utf8');
11
+ const serverSource = fs.readFileSync(path.join(packageRoot, 'server.js'), 'utf8');
11
12
 
12
13
  assert.equal(packageJson.build?.appId, 'com.mindexec.desktop', 'Stable desktop appId is required.');
13
14
  assert.equal(packageJson.build?.productName, 'MindExec', 'Desktop product name changed unexpectedly.');
14
15
  assert.equal(packageJson.build?.extraMetadata?.main, 'electron/main.cjs', 'Packaged Electron entry point is missing.');
15
16
  assert.equal(packageJson.build?.asar, false, 'Bundled runtime executables require an unpacked app directory.');
16
17
  assert.ok(packageJson.files?.includes('electron/'), 'The npm package must include the Electron source contract.');
18
+ assert.ok(packageJson.files?.includes('desktop-workspace-state.cjs'), 'The npm package must include desktop workspace state.');
17
19
  assert.match(packageJson.scripts?.['desktop:build:win'] || '', /electron-builder/, 'Windows build script is missing.');
18
20
  assert.match(packageJson.scripts?.['test:desktop:win'] || '', /windows-package-smoke/, 'Packaged smoke test is missing.');
19
21
 
@@ -27,6 +29,21 @@ assert.ok(fs.existsSync(builderIconPath), `Electron builder icon is missing: ${b
27
29
  const builderIconMetadata = await sharp(builderIconPath).metadata();
28
30
  assert.equal(builderIconMetadata.width, 1024, 'Electron builder icon must be 1024px wide.');
29
31
  assert.equal(builderIconMetadata.height, 1024, 'Electron builder icon must be 1024px tall.');
32
+ assert.equal(builderIconMetadata.hasAlpha, true, 'Electron builder icon must preserve transparency.');
33
+ const { data: builderIconPixels, info: builderIconInfo } = await sharp(builderIconPath)
34
+ .ensureAlpha()
35
+ .raw()
36
+ .toBuffer({ resolveWithObject: true });
37
+ const builderIconCornerAlpha = [
38
+ builderIconPixels[3],
39
+ builderIconPixels[(builderIconInfo.width - 1) * 4 + 3],
40
+ builderIconPixels[(builderIconInfo.height - 1) * builderIconInfo.width * 4 + 3],
41
+ builderIconPixels[(builderIconInfo.width * builderIconInfo.height - 1) * 4 + 3]
42
+ ];
43
+ assert.ok(
44
+ builderIconCornerAlpha.every((alpha) => alpha === 0),
45
+ 'Electron builder icon must have fully transparent corners.'
46
+ );
30
47
 
31
48
  const targets = packageJson.build?.win?.target || [];
32
49
  assert.ok(targets.some((target) => target.target === 'nsis'), 'NSIS target is required.');
@@ -43,6 +60,10 @@ assert.doesNotMatch(mainSource, /adopted|probeBridge/, 'Electron must not adopt
43
60
  assert.match(mainSource, /MINDEXEC_DESKTOP_SMOKE_RESULT/, 'Packaged runtime smoke contract is missing.');
44
61
  assert.match(mainSource, /icon:\s*resolveDesktopIconPath\(\)/, 'Electron windows must use the packaged MindExec icon.');
45
62
  assert.match(mainSource, /app\.setAppUserModelId\(DESKTOP_APP_ID\)/, 'Windows taskbar identity must use the stable app id.');
63
+ assert.match(mainSource, /resolveDesktopWorkspace/, 'Electron startup must restore the last desktop workspace.');
64
+ assert.match(mainSource, /MINDEXEC_DESKTOP_WORKSPACE_STATE_PATH/, 'Electron must share its workspace state path with LocalBridge.');
65
+ assert.match(serverSource, /saveDesktopWorkspaceState\(DESKTOP_WORKSPACE_STATE_PATH, resolvedPath\)/,
66
+ 'A successful desktop workspace switch must be saved for the next launch.');
46
67
 
47
68
  console.log(JSON.stringify({
48
69
  ok: true,
@@ -5,6 +5,9 @@ import net from 'node:net';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import desktopWorkspaceState from '../desktop-workspace-state.cjs';
9
+
10
+ const { saveDesktopWorkspaceState } = desktopWorkspaceState;
8
11
 
9
12
  const electronDirectory = path.dirname(fileURLToPath(import.meta.url));
10
13
  const packageRoot = path.resolve(electronDirectory, '..');
@@ -85,9 +88,11 @@ function canBind(port) {
85
88
  const bridgePort = await reservePort();
86
89
  const remoteHubPort = await reservePort();
87
90
  const smokeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mindexec-electron-smoke-'));
88
- const workspace = path.join(smokeRoot, 'workspace');
91
+ const workspace = path.join(smokeRoot, 'saved-workspace');
89
92
  const resultPath = path.join(smokeRoot, 'result.json');
93
+ const workspaceStatePath = path.join(smokeRoot, 'workspace-state.json');
90
94
  fs.mkdirSync(workspace, { recursive: true });
95
+ saveDesktopWorkspaceState(workspaceStatePath, workspace);
91
96
 
92
97
  let child;
93
98
  try {
@@ -97,7 +102,7 @@ try {
97
102
  ...process.env,
98
103
  MINDEXEC_DESKTOP_SMOKE: '1',
99
104
  MINDEXEC_DESKTOP_SMOKE_RESULT: resultPath,
100
- MINDEXEC_DESKTOP_WORKSPACE: workspace,
105
+ MINDEXEC_DESKTOP_WORKSPACE_STATE_PATH: workspaceStatePath,
101
106
  MINDEXEC_NO_OPEN: '1',
102
107
  BRIDGE_PORT: String(bridgePort),
103
108
  REMOTE_HUB_PORT: String(remoteHubPort)
@@ -123,6 +128,7 @@ try {
123
128
  assert.equal(result.runtimeMode, 'bundled', 'Electron did not use its bundled runtime.');
124
129
  assert.equal(result.bridgePort, bridgePort, 'Packaged shell did not use the requested Bridge port.');
125
130
  assert.equal(result.remoteHubPort, remoteHubPort, 'Packaged shell did not use the requested RemoteHub port.');
131
+ assert.equal(path.resolve(result.workspace), path.resolve(workspace), 'Packaged Electron did not restore the saved workspace.');
126
132
 
127
133
  const exitCode = await waitForExit(child, 15_000);
128
134
  assert.equal(exitCode, 0, `Packaged Electron smoke exited with code ${exitCode}.`);
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.458",
3
+ "version": "0.2.459",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
7
7
  "files": [
8
8
  "start-bridge.bat",
9
9
  "start-bridge.sh",
10
- "launch-bridge.cjs",
11
- "server.js",
10
+ "launch-bridge.cjs",
11
+ "desktop-workspace-state.cjs",
12
+ "server.js",
12
13
  "remote-hub.js",
13
14
  "lzo1x.js",
14
15
  "mode4-atlas.js",
@@ -28,9 +29,9 @@
28
29
  "dev": "node launch-bridge.cjs --watch",
29
30
  "desktop:start": "electron electron/main.cjs",
30
31
  "desktop:build:win": "electron-builder --win --x64 --publish never",
31
- "test:desktop": "node electron/source-smoke.mjs",
32
+ "test:desktop": "node electron/source-smoke.mjs && node scripts/desktop-workspace-state-smoke.mjs",
32
33
  "test:desktop:win": "node electron/windows-package-smoke.mjs",
33
- "test:syntax": "node --check server.js && node --check remote-hub.js && node --check lzo1x.js && node --check mode4-atlas.js && node --check mode4-atlas-worker.js && node --check codex-runtime.js && node --check codex-model-catalog.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check electron/main.cjs && node --check electron/source-smoke.mjs && node --check electron/windows-package-smoke.mjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/npm-fresh-install-smoke.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/codex-model-catalog-smoke.mjs && node --check scripts/codex-sdk-runtime-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-input-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
34
+ "test:syntax": "node --check server.js && node --check remote-hub.js && node --check lzo1x.js && node --check mode4-atlas.js && node --check mode4-atlas-worker.js && node --check codex-runtime.js && node --check codex-model-catalog.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check desktop-workspace-state.cjs && node --check electron/main.cjs && node --check electron/source-smoke.mjs && node --check electron/windows-package-smoke.mjs && node --check scripts/desktop-workspace-state-smoke.mjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/npm-fresh-install-smoke.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/codex-model-catalog-smoke.mjs && node --check scripts/codex-sdk-runtime-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-input-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
34
35
  "test:codex-sdk": "node scripts/codex-sdk-runtime-smoke.mjs",
35
36
  "test:codex-sdk:live": "node scripts/codex-sdk-runtime-smoke.mjs --live",
36
37
  "test:codex-sdk:live-tools": "node scripts/codex-sdk-runtime-smoke.mjs --live-tools",
@@ -0,0 +1,81 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import desktopWorkspaceState from '../desktop-workspace-state.cjs';
6
+
7
+ const {
8
+ loadDesktopWorkspaceState,
9
+ resolveDesktopWorkspace,
10
+ saveDesktopWorkspaceState
11
+ } = desktopWorkspaceState;
12
+
13
+ const smokeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mindexec-workspace-state-'));
14
+ const savedWorkspace = path.join(smokeRoot, 'saved-workspace');
15
+ const explicitWorkspace = path.join(smokeRoot, 'explicit-workspace');
16
+ const fallbackWorkspace = path.join(smokeRoot, 'fallback-workspace');
17
+ const missingWorkspace = path.join(smokeRoot, 'missing-workspace');
18
+ const statePath = path.join(smokeRoot, 'profile', 'workspace-state.json');
19
+
20
+ fs.mkdirSync(savedWorkspace, { recursive: true });
21
+ fs.mkdirSync(explicitWorkspace, { recursive: true });
22
+ fs.mkdirSync(fallbackWorkspace, { recursive: true });
23
+
24
+ try {
25
+ assert.equal(saveDesktopWorkspaceState('', savedWorkspace), false, 'Non-desktop runtimes must remain a no-op.');
26
+ assert.equal(saveDesktopWorkspaceState(statePath, savedWorkspace), true);
27
+ assert.deepEqual(loadDesktopWorkspaceState(statePath), {
28
+ version: 1,
29
+ workspacePath: path.resolve(savedWorkspace)
30
+ });
31
+
32
+ assert.equal(saveDesktopWorkspaceState(statePath, explicitWorkspace), true);
33
+ assert.equal(loadDesktopWorkspaceState(statePath)?.workspacePath, path.resolve(explicitWorkspace),
34
+ 'Changing folders must replace the prior desktop workspace state.');
35
+ assert.equal(saveDesktopWorkspaceState(statePath, savedWorkspace), true);
36
+
37
+ assert.deepEqual(resolveDesktopWorkspace({
38
+ statePath,
39
+ fallbackWorkspace
40
+ }), {
41
+ workspacePath: path.resolve(savedWorkspace),
42
+ source: 'saved'
43
+ });
44
+
45
+ assert.deepEqual(resolveDesktopWorkspace({
46
+ explicitWorkspace,
47
+ statePath,
48
+ fallbackWorkspace
49
+ }), {
50
+ workspacePath: path.resolve(explicitWorkspace),
51
+ source: 'explicit'
52
+ });
53
+
54
+ fs.writeFileSync(statePath, `${JSON.stringify({ version: 1, workspacePath: missingWorkspace })}\n`, 'utf8');
55
+ assert.deepEqual(resolveDesktopWorkspace({
56
+ statePath,
57
+ fallbackWorkspace
58
+ }), {
59
+ workspacePath: path.resolve(fallbackWorkspace),
60
+ source: 'fallback'
61
+ });
62
+ assert.equal(fs.existsSync(missingWorkspace), false, 'A deleted saved workspace must not be recreated.');
63
+
64
+ fs.writeFileSync(statePath, '{broken json', 'utf8');
65
+ assert.deepEqual(resolveDesktopWorkspace({
66
+ statePath,
67
+ fallbackWorkspace
68
+ }), {
69
+ workspacePath: path.resolve(fallbackWorkspace),
70
+ source: 'fallback'
71
+ });
72
+
73
+ console.log(JSON.stringify({
74
+ ok: true,
75
+ restoredWorkspace: path.resolve(savedWorkspace),
76
+ invalidSavedWorkspaceFallsBack: true,
77
+ explicitWorkspaceWins: true
78
+ }));
79
+ } finally {
80
+ fs.rmSync(smokeRoot, { recursive: true, force: true });
81
+ }
package/server.js CHANGED
@@ -26,10 +26,12 @@ import { createCodexModelCatalog } from './codex-model-catalog.js';
26
26
  import { createRemoteHub } from './remote-hub.js';
27
27
  import { Mode4AtlasSession } from './mode4-atlas.js';
28
28
  import portGuard from './port-guard.cjs';
29
+ import desktopWorkspaceState from './desktop-workspace-state.cjs';
29
30
 
30
31
  const execAsync = promisify(exec);
31
32
  const execFileAsync = promisify(execFile);
32
33
  const { normalizePort, releaseBridgePort } = portGuard;
34
+ const { saveDesktopWorkspaceState } = desktopWorkspaceState;
33
35
 
34
36
  const app = express();
35
37
  const PORT = normalizePort(process.env.BRIDGE_PORT);
@@ -48,6 +50,7 @@ const VERBOSE_REMOTE_HTTP_LOGS = /^(1|true|yes|on)$/i.test(String(process.env.MI
48
50
  const VERBOSE_REMOTE_AGENT_LOGS = /^(1|true|yes|on)$/i.test(String(process.env.MINDEXEC_VERBOSE_REMOTE_AGENT || process.env.BRIDGE_VERBOSE_REMOTE_AGENT || ''));
49
51
  const COLOR_LOGS_ENABLED = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
50
52
  const DEFAULT_WEB_APP_ROOT = path.join(BRIDGE_ROOT, 'wwwroot');
53
+ const DESKTOP_WORKSPACE_STATE_PATH = String(process.env.MINDEXEC_DESKTOP_WORKSPACE_STATE_PATH || '').trim();
51
54
 
52
55
  const ANSI = {
53
56
  reset: '\x1b[0m',
@@ -13204,11 +13207,12 @@ app.post('/api/workspace/set', async (req, res) => {
13204
13207
  return res.status(400).json({ error: 'Path is required' });
13205
13208
  }
13206
13209
 
13207
- const resolvedPath = path.resolve(newPath);
13208
- await closeProjectSession();
13209
- await ensureWorkspaceDataLayout(resolvedPath);
13210
-
13211
- workspacePath = resolvedPath;
13210
+ const resolvedPath = path.resolve(newPath);
13211
+ await closeProjectSession();
13212
+ await ensureWorkspaceDataLayout(resolvedPath);
13213
+ saveDesktopWorkspaceState(DESKTOP_WORKSPACE_STATE_PATH, resolvedPath);
13214
+
13215
+ workspacePath = resolvedPath;
13212
13216
  console.log(`[Workspace] Set to: ${workspacePath}`);
13213
13217
 
13214
13218
  res.json({ success: true, workspace: workspacePath });
@@ -14062,7 +14066,12 @@ async function generateThumbnail(inputBuffer, outputPath) {
14062
14066
  await fs.mkdir(thumbsDir, { recursive: true });
14063
14067
 
14064
14068
  // Generate thumbnail with sharp
14065
- await sharp(inputBuffer)
14069
+ await sharp(inputBuffer, {
14070
+ animated: false,
14071
+ page: 0,
14072
+ pages: 1,
14073
+ limitInputPixels: 64 * 1024 * 1024
14074
+ })
14066
14075
  .resize(THUMBNAIL_SIZE, THUMBNAIL_SIZE, {
14067
14076
  fit: 'inside',
14068
14077
  withoutEnlargement: true
@@ -14193,16 +14202,27 @@ app.post('/api/assets/generate-thumbnail/:filename', async (req, res) => {
14193
14202
  }
14194
14203
  }
14195
14204
 
14196
- // Read original file
14197
- const buffer = await fs.readFile(originalPath);
14198
-
14199
- // Generate thumbnail
14200
- const thumbnailPath = getThumbnailPath(filename);
14201
- const success = await generateThumbnail(buffer, thumbnailPath);
14202
-
14203
- if (success) {
14204
- const thumbnailUrl = `http://127.0.0.1:${PORT}/assets/thumbs/${path.basename(thumbnailPath)}`;
14205
- res.json({
14205
+ const thumbnailPath = getThumbnailPath(filename);
14206
+ const thumbnailUrl = `http://127.0.0.1:${PORT}/assets/thumbs/${path.basename(thumbnailPath)}`;
14207
+ try {
14208
+ await fs.access(thumbnailPath);
14209
+ return res.json({
14210
+ success: true,
14211
+ thumbnailUrl,
14212
+ filename: path.basename(thumbnailPath),
14213
+ existing: true
14214
+ });
14215
+ } catch { }
14216
+
14217
+ // Read the original only when a bounded static thumbnail is missing.
14218
+ const buffer = await fs.readFile(originalPath);
14219
+
14220
+ // Generate a single-frame thumbnail. Animated WebP originals can contain
14221
+ // hundreds of frames and must never be fully decoded for resident LOD.
14222
+ const success = await generateThumbnail(buffer, thumbnailPath);
14223
+
14224
+ if (success) {
14225
+ res.json({
14206
14226
  success: true,
14207
14227
  thumbnailUrl,
14208
14228
  filename: path.basename(thumbnailPath)