Skip to content

Commit 863b11d

Browse files
author
DavidQ
committed
refactor(workspace): enforce source-of-truth state by clearing toolboxaid launch state, removing adapter default-state generation, and requiring explicit tool state in workspace manifests
1 parent 1e9b46e commit 863b11d

3 files changed

Lines changed: 121 additions & 143 deletions

File tree

tools/shared/platformShell.js

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ const TOOLS_PLATFORM_BOOT_MS = Date.now();
3030
const GAME_ASSET_CATALOG_SCHEMA = "html-js-gaming.game-asset-catalog";
3131
const GAME_ASSET_CATALOG_VERSION = 1;
3232
const WORKSPACE_LAUNCH_SIGNATURE_STORAGE_KEY = "toolboxaid.toolsPlatform.launchSignature";
33+
const TOOL_STATE_STORAGE_KEY_PREFIX = "toolboxaid.";
34+
const PRESERVED_TOOL_STATE_KEYS = new Set([
35+
HEADER_EXPANDED_STORAGE_KEY,
36+
WORKSPACE_LAUNCH_SIGNATURE_STORAGE_KEY
37+
]);
38+
const TOOLS_REQUIRING_WORKSPACE_TOOL_STATE = new Set([
39+
"skin-editor",
40+
"sprite-editor",
41+
"tile-map-editor",
42+
"parallax-editor",
43+
"vector-map-editor",
44+
"vector-asset-studio"
45+
]);
3346

3447
function getPageMode() {
3548
return document.body.dataset.toolsPlatformPage || "tool";
@@ -220,12 +233,54 @@ function clearSharedBindingsForNewLaunch(signature) {
220233
if (previous && previous === signature) {
221234
return false;
222235
}
236+
clearToolStateStorageForWorkspaceLaunch();
223237
clearSharedAssetHandoff();
224238
clearSharedPaletteHandoff();
225239
writeStoredLaunchSignature(signature);
226240
return true;
227241
}
228242

243+
function clearToolStateStorageForWorkspaceLaunch() {
244+
if (typeof window === "undefined") {
245+
return;
246+
}
247+
248+
const clearStorageLike = (storageLike) => {
249+
if (!storageLike || typeof storageLike.length !== "number") {
250+
return;
251+
}
252+
const keysToRemove = [];
253+
for (let index = 0; index < storageLike.length; index += 1) {
254+
const key = normalizeTextValue(storageLike.key(index));
255+
if (!key || !key.startsWith(TOOL_STATE_STORAGE_KEY_PREFIX)) {
256+
continue;
257+
}
258+
if (PRESERVED_TOOL_STATE_KEYS.has(key)) {
259+
continue;
260+
}
261+
keysToRemove.push(key);
262+
}
263+
keysToRemove.forEach((key) => {
264+
try {
265+
storageLike.removeItem(key);
266+
} catch {
267+
// Ignore storage write failures and continue.
268+
}
269+
});
270+
};
271+
272+
try {
273+
clearStorageLike(window.localStorage);
274+
} catch {
275+
// Ignore storage read/write failures and continue.
276+
}
277+
try {
278+
clearStorageLike(window.sessionStorage);
279+
} catch {
280+
// Ignore storage read/write failures and continue.
281+
}
282+
}
283+
229284
function readGameLaunchContext() {
230285
if (typeof window === "undefined") {
231286
return null;
@@ -622,7 +677,14 @@ function resolveWorkspaceToolLockState() {
622677
const palette = readSharedPaletteHandoff();
623678
const workspaceReady = Boolean(manifest && manifest.workspace?.notes !== "closed");
624679
const paletteReady = Boolean(palette && typeof palette.displayName === "string" && palette.displayName.trim());
625-
return { workspaceReady, paletteReady };
680+
const hasToolState = (toolId) => {
681+
const normalizedToolId = normalizeTextValue(toolId);
682+
if (!normalizedToolId || !manifest || typeof manifest !== "object") {
683+
return false;
684+
}
685+
return Boolean(manifest.tools && manifest.tools[normalizedToolId]);
686+
};
687+
return { workspaceReady, paletteReady, hasToolState };
626688
}
627689

628690
function renderToolLinks(currentToolId) {
@@ -920,18 +982,30 @@ function applyDocumentMetadata(currentTool) {
920982
document.body.classList.add("tools-platform-surface");
921983
const workspaceContext = isWorkspaceManagerContext();
922984
const lockState = resolveWorkspaceToolLockState();
985+
const searchParams = typeof window !== "undefined"
986+
? new URLSearchParams(window.location.search)
987+
: null;
988+
const launchHasSourcePreset = searchParams?.has("samplePresetPath") === true;
923989
const isToolSurfacePage = getPageMode() !== "landing";
924990
const isPaletteBrowser = currentTool?.id === "palette-browser";
991+
const toolRequiresState = TOOLS_REQUIRING_WORKSPACE_TOOL_STATE.has(currentTool?.id || "");
992+
const toolStateMissingLock = workspaceContext
993+
&& isToolSurfacePage
994+
&& toolRequiresState
995+
&& !launchHasSourcePreset
996+
&& !lockState.hasToolState(currentTool?.id);
925997
const workspaceMissingLock = workspaceContext && isToolSurfacePage && !lockState.workspaceReady;
926998
const paletteMissingLock = workspaceContext
927999
&& isToolSurfacePage
9281000
&& lockState.workspaceReady
9291001
&& !lockState.paletteReady
9301002
&& !isPaletteBrowser;
931-
const shouldLockToolSurface = workspaceMissingLock || paletteMissingLock;
1003+
const shouldLockToolSurface = workspaceMissingLock || paletteMissingLock || toolStateMissingLock;
9321004
const lockMessage = workspaceMissingLock
9331005
? "Create or open a workspace to use this tool."
934-
: "Select a shared palette in Palette Browser to use this tool.";
1006+
: toolStateMissingLock
1007+
? "Workspace loaded with no source tool JSON for this tool. Load workspace JSON that includes this tool state."
1008+
: "Select a shared palette in Palette Browser to use this tool.";
9351009
document.body.classList.toggle("tools-platform-workspace-context", workspaceContext);
9361010
document.body.classList.toggle("tools-platform-workspace-tool-locked", shouldLockToolSurface);
9371011
if (lastLockedSurfaceElement) {
@@ -1208,6 +1282,7 @@ async function initPlatformShell() {
12081282
workspaceController = createWorkspaceSystemController({
12091283
toolId: currentToolId,
12101284
launchContext,
1285+
launchHasSourcePreset: launchedFromSamplePreset,
12111286
skipInitialToolStateApply: launchedFromSamplePreset,
12121287
onChange(payload) {
12131288
const manifest = payload?.manifest || {};
@@ -1237,7 +1312,13 @@ async function initPlatformShell() {
12371312
}
12381313
}
12391314
});
1240-
workspaceController.startWatching();
1315+
if (launchedFromSamplePreset) {
1316+
window.setTimeout(() => {
1317+
workspaceController?.startWatching();
1318+
}, 600);
1319+
} else {
1320+
workspaceController.startWatching();
1321+
}
12411322
}
12421323

12431324
const catalogContext = await readCatalogContextFromLaunchContext(launchContext);

tools/shared/projectSystem.js

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,13 @@ export function createWorkspaceSystemController(options = {}) {
7272
const toolEntry = getToolById(toolId);
7373
const isReadOnlyTool = toolEntry?.readOnly === true;
7474
const skipInitialToolStateApply = options.skipInitialToolStateApply === true;
75+
const launchHasSourcePreset = options.launchHasSourcePreset === true;
7576
const launchContext = options.launchContext && typeof options.launchContext === "object"
7677
? options.launchContext
7778
: {};
7879
const launchGameId = safeString(launchContext.gameId, "");
7980
const launchGameTitle = safeString(launchContext.gameTitle, launchGameId);
81+
const strictLaunchMode = Boolean(launchGameId);
8082
const onChange = typeof options.onChange === "function" ? options.onChange : () => {};
8183
const onStatus = typeof options.onStatus === "function" ? options.onStatus : () => {};
8284
const adapter = () => getProjectAdapter(toolId);
@@ -87,7 +89,8 @@ export function createWorkspaceSystemController(options = {}) {
8789
lastObservedHash: "",
8890
adapterReady: false,
8991
appliedInitialState: skipInitialToolStateApply,
90-
launchContextHydrated: false
92+
launchContextHydrated: false,
93+
toolStateSourceValidated: strictLaunchMode ? launchHasSourcePreset : true
9194
};
9295

9396
function isGenericWorkspaceName(name) {
@@ -110,7 +113,7 @@ export function createWorkspaceSystemController(options = {}) {
110113

111114
function computeObservedManifest() {
112115
const toolAdapter = adapter();
113-
const currentManifest = state.manifest
116+
let currentManifest = state.manifest
114117
? cloneValue(state.manifest)
115118
: createEmptyProjectManifest({
116119
name: toolAdapter.getProjectName?.() || getToolById(toolId)?.displayName || "Untitled Workspace",
@@ -121,6 +124,11 @@ export function createWorkspaceSystemController(options = {}) {
121124
if (!state.launchContextHydrated) {
122125
clearSharedAssetHandoff();
123126
clearSharedPaletteHandoff();
127+
currentManifest = createEmptyProjectManifest({
128+
name: launchGameTitle || launchGameId,
129+
toolId
130+
});
131+
state.manifest = cloneValue(currentManifest);
124132
}
125133
currentManifest.workspace = currentManifest.workspace && typeof currentManifest.workspace === "object"
126134
? currentManifest.workspace
@@ -142,7 +150,7 @@ export function createWorkspaceSystemController(options = {}) {
142150
? currentManifest.tools
143151
: {};
144152

145-
if (toolAdapter.ready) {
153+
if (toolAdapter.ready && (!strictLaunchMode || state.toolStateSourceValidated)) {
146154
currentManifest.tools[toolId] = normalizeToolStateForProjectManifest(
147155
toolId,
148156
toolAdapter.captureState()
@@ -156,6 +164,8 @@ export function createWorkspaceSystemController(options = {}) {
156164
if (adapterName && (!manifestName || manifestName === defaultManifestName)) {
157165
currentManifest.name = adapterName;
158166
}
167+
} else if (strictLaunchMode && !state.toolStateSourceValidated && currentManifest.tools?.[toolId]) {
168+
delete currentManifest.tools[toolId];
159169
}
160170

161171
currentManifest.toolIntegration = buildProjectToolIntegration(currentManifest.tools);
@@ -223,7 +233,10 @@ export function createWorkspaceSystemController(options = {}) {
223233
const toolState = manifest.tools?.[toolId];
224234
if (toolState) {
225235
toolAdapter.applyState(cloneValue(unwrapToolStateForAdapter(toolId, toolState)));
236+
state.toolStateSourceValidated = true;
226237
onStatus(buildStatusSummary(validateProjectManifest(manifest)));
238+
} else if (strictLaunchMode) {
239+
onStatus(`Workspace launched for ${launchGameTitle || launchGameId}. Waiting for source tool JSON for ${toolId}.`);
227240
}
228241
state.appliedInitialState = true;
229242
markSaved("initial-apply");
@@ -237,16 +250,13 @@ export function createWorkspaceSystemController(options = {}) {
237250
name: nextName,
238251
toolId
239252
});
240-
if (toolAdapter.ready) {
241-
const defaultState = normalizeToolStateForProjectManifest(toolId, toolAdapter.createDefaultState(nextName));
242-
nextManifest.tools[toolId] = defaultState;
243-
nextManifest.toolIntegration = buildProjectToolIntegration(nextManifest.tools);
244-
toolAdapter.applyState(cloneValue(unwrapToolStateForAdapter(toolId, defaultState)));
245-
}
253+
nextManifest.tools = {};
254+
nextManifest.toolIntegration = buildProjectToolIntegration(nextManifest.tools);
246255
clearSharedAssetHandoff();
247256
clearSharedPaletteHandoff();
248257
state.manifest = nextManifest;
249258
state.appliedInitialState = true;
259+
state.toolStateSourceValidated = !strictLaunchMode;
250260
markSaved("new-project");
251261
onStatus(`Started ${nextName}.`);
252262
}
@@ -270,12 +280,14 @@ export function createWorkspaceSystemController(options = {}) {
270280
state.manifest = nextManifest;
271281
const toolAdapter = adapter();
272282
if (toolAdapter.ready) {
273-
const nextToolState = nextManifest.tools?.[toolId]
274-
? normalizeToolStateForProjectManifest(toolId, nextManifest.tools[toolId])
275-
: normalizeToolStateForProjectManifest(toolId, toolAdapter.createDefaultState(nextManifest.name));
283+
if (!nextManifest.tools?.[toolId]) {
284+
throw new Error(`Workspace manifest is missing required state for ${toolId}.`);
285+
}
286+
const nextToolState = normalizeToolStateForProjectManifest(toolId, nextManifest.tools[toolId]);
276287
toolAdapter.applyState(cloneValue(unwrapToolStateForAdapter(toolId, nextToolState)));
277288
nextManifest.tools[toolId] = cloneValue(nextToolState);
278289
nextManifest.toolIntegration = buildProjectToolIntegration(nextManifest.tools);
290+
state.toolStateSourceValidated = true;
279291
}
280292
state.appliedInitialState = true;
281293
markSaved("open-project");
@@ -298,26 +310,28 @@ export function createWorkspaceSystemController(options = {}) {
298310
}
299311

300312
function handleCloseWorkspace() {
301-
const toolAdapter = adapter();
302313
const fallbackName = "No Active Workspace";
303314
const nextManifest = createEmptyProjectManifest({
304315
name: fallbackName,
305316
toolId
306317
});
307318
nextManifest.workspace.notes = "closed";
308-
if (toolAdapter.ready) {
309-
const defaultState = normalizeToolStateForProjectManifest(toolId, toolAdapter.createDefaultState(fallbackName));
310-
nextManifest.tools[toolId] = defaultState;
311-
nextManifest.toolIntegration = buildProjectToolIntegration(nextManifest.tools);
312-
toolAdapter.applyState(cloneValue(unwrapToolStateForAdapter(toolId, defaultState)));
313-
}
319+
nextManifest.tools = {};
320+
nextManifest.toolIntegration = buildProjectToolIntegration(nextManifest.tools);
314321
state.manifest = nextManifest;
315322
state.appliedInitialState = true;
323+
state.toolStateSourceValidated = !strictLaunchMode;
316324
state.baselineHash = "";
325+
state.lastObservedHash = "";
317326
clearStorage();
318327
clearSharedAssetHandoff();
319328
clearSharedPaletteHandoff();
320-
updateDirtyState("close-project");
329+
onChange({
330+
manifest: cloneValue(state.manifest),
331+
dirty: false,
332+
ready: adapter().ready,
333+
reason: "close-project"
334+
});
321335
onStatus("Workspace closed.");
322336
}
323337

@@ -371,6 +385,7 @@ export function createWorkspaceSystemController(options = {}) {
371385
}
372386

373387
state.appliedInitialState = true;
388+
state.toolStateSourceValidated = true;
374389
markSaved("external-preset");
375390
const label = safeString(payload.label, "external preset");
376391
if (applied) {

0 commit comments

Comments
 (0)