Skip to content

Commit 08df526

Browse files
author
DavidQ
committed
Normalize Workspace Manager tool session keys and split Session Inspector V2 JSON and Schema views - PR_26128_018-session-inspector-v2-normalized-tool-keys
1 parent 3c1ac3a commit 08df526

13 files changed

Lines changed: 321 additions & 116 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Playwright Session Inspector V2 Normalized Tool Keys
2+
3+
## Command
4+
`npm run test:workspace-v2`
5+
6+
## Result
7+
- Passed: 15/15
8+
- Runtime: about 1.3 minutes
9+
10+
## Targeted Coverage
11+
- Workspace Manager V2 hydrates one session key per tool:
12+
- `workspace.tools.templates-v2`
13+
- `workspace.tools.asset-manager-v2`
14+
- `workspace.tools.palette-manager-v2`
15+
- `workspace.tools.preview-generator-v2`
16+
- `workspace.tools.session-inspector-v2`
17+
- Workspace Manager V2 does not create old split `.schema` or `.state` per-tool keys.
18+
- Session Inspector V2 shows normalized tool session values as one tile per tool.
19+
- JSON section shows the full combined object.
20+
- Schema section shows only schema data.
21+
- Schema section shows a visible empty-state message when the selected entry has no schema.
22+
- Per-tile Delete removes the normalized key.
23+
- Delete All still clears displayed entries.
24+
- Preview Generator V2 reads repo/game context from the normalized session key and remains able to generate a preview.
25+
26+
## Skipped
27+
- Full samples smoke test was skipped by request. The changed surface is covered by targeted Workspace Manager V2, Session Inspector V2, and Preview Generator V2 workspace-launch validation in `tests/playwright/tools/WorkspaceManagerV2.spec.mjs`.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Session Inspector V2 Normalized Tool Keys
2+
3+
## Scope
4+
- Normalized Workspace Manager V2 tool hydration from split `workspace.tools.<tool-id>.schema` and `workspace.tools.<tool-id>.state` keys into one `workspace.tools.<tool-id>` key per tool.
5+
- Preserved `workspace.repo.reference`.
6+
- Updated Preview Generator V2 to read its nested `state` from `workspace.tools.preview-generator-v2` so workspace launch repo/game context still resolves through session storage.
7+
- Renamed the Session Inspector V2 Details section/control to JSON.
8+
- Added a Schema section/control that renders only the selected entry's top-level `schema` data.
9+
10+
## Implementation Notes
11+
- Each Workspace Manager V2 tool session value is now a combined object with:
12+
- `schema`
13+
- `state`
14+
- Session Inspector V2 now renders normalized tool hydration as one tile per tool because the writer no longer creates split schema/state keys.
15+
- JSON renders the full stored value for the selected tile.
16+
- Schema renders only `value.schema` when present.
17+
- Schema shows an actionable empty state when the selected value has no top-level `schema` section.
18+
- Per-tile Delete still removes the selected storage key. For normalized tool tiles, that removes the single `workspace.tools.<tool-id>` key.
19+
- Delete All still clears all currently displayed storage entries.
20+
21+
## Guardrails
22+
- No cross-tool direct communication was added.
23+
- No sample JSON was modified.
24+
- No roadmap content was modified.
25+
- Game manifest SSoT semantics were not changed.
26+
27+
## Validation
28+
- Passed `npm run test:workspace-v2` with 15/15 tests.
29+
- Verified Workspace Manager V2 creates normalized per-tool keys only.
30+
- Verified old `.schema` and `.state` per-tool keys are not created by Workspace Manager V2 hydration.
31+
- Verified Session Inspector V2 JSON and Schema sections.
32+
- Verified Schema empty state for a selected entry without `schema`.
33+
- Verified per-tile Delete and Delete All behavior.
34+
- Verified Preview Generator V2 still resolves repo/game context and can generate a preview from workspace launch.
35+
36+
## Skipped
37+
- Full samples smoke test was skipped because this PR is limited to Workspace Manager V2 session hydration, Session Inspector V2 storage display, and the Preview Generator V2 workspace session read path. The requested targeted Workspace V2 Playwright validation covers the changed behavior.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 142 additions & 41 deletions
Large diffs are not rendered by default.

tools/preview-generator-v2/PreviewGeneratorV2App.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const ASSET_MANAGER_V2_TOOL_KEY = "asset-manager-v2";
99
const PALETTE_MANAGER_V2_TOOL_KEY = "palette-manager-v2";
1010
const PREVIEW_GENERATOR_V2_TOOL_KEY = "preview-generator-v2";
1111
const WORKSPACE_REPO_REFERENCE_SESSION_KEY = "workspace.repo.reference";
12-
const WORKSPACE_PREVIEW_GENERATOR_STATE_SESSION_KEY = `workspace.tools.${PREVIEW_GENERATOR_V2_TOOL_KEY}.state`;
12+
const WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY = `workspace.tools.${PREVIEW_GENERATOR_V2_TOOL_KEY}`;
1313
const BACKGROUND_ROLE = "background";
1414
const PREVIEW_ROLE = "preview";
1515

@@ -127,25 +127,28 @@ function readWorkspaceRepoReference() {
127127
}
128128

129129
function readWorkspacePreviewGeneratorState(manifest) {
130-
const result = readSessionJson(WORKSPACE_PREVIEW_GENERATOR_STATE_SESSION_KEY);
130+
const result = readSessionJson(WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY);
131131
if (!result.ok) {
132132
return result;
133133
}
134-
const state = result.value;
134+
if (!isPlainObject(result.value.state)) {
135+
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.state must contain a JSON object.` };
136+
}
137+
const state = result.value.state;
135138
if (state.source !== "workspace-manager-v2") {
136-
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_STATE_SESSION_KEY}.source must be workspace-manager-v2.` };
139+
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.state.source must be workspace-manager-v2.` };
137140
}
138141
if (state.toolId !== PREVIEW_GENERATOR_V2_TOOL_KEY) {
139-
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_STATE_SESSION_KEY}.toolId must be ${PREVIEW_GENERATOR_V2_TOOL_KEY}.` };
142+
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.state.toolId must be ${PREVIEW_GENERATOR_V2_TOOL_KEY}.` };
140143
}
141144
if (state.repoReferenceKey !== WORKSPACE_REPO_REFERENCE_SESSION_KEY) {
142-
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_STATE_SESSION_KEY}.repoReferenceKey must be ${WORKSPACE_REPO_REFERENCE_SESSION_KEY}.` };
145+
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.state.repoReferenceKey must be ${WORKSPACE_REPO_REFERENCE_SESSION_KEY}.` };
143146
}
144147
if (state.gameId !== manifest.gameId) {
145-
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_STATE_SESSION_KEY}.gameId must match manifest.gameId ${manifest.gameId}.` };
148+
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.state.gameId must match manifest.gameId ${manifest.gameId}.` };
146149
}
147150
if (state.gameRoot !== manifest.gameRoot || state.assetsPath !== manifest.assetsPath) {
148-
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_STATE_SESSION_KEY} gameRoot/assetsPath must match the workspace manifest.` };
151+
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.state gameRoot/assetsPath must match the workspace manifest.` };
149152
}
150153
return { ok: true, state };
151154
}
@@ -1115,7 +1118,7 @@ class PreviewGeneratorV2App {
11151118
ui.setRepoDestinationDisplayName(repoDisplayName);
11161119
workspaceLaunchHydrated = true;
11171120
logger.log(`OK Workspace repo session reference loaded from ${WORKSPACE_REPO_REFERENCE_SESSION_KEY} for ${repoDisplayName}.`);
1118-
logger.log(`OK Workspace tool session state loaded from ${WORKSPACE_PREVIEW_GENERATOR_STATE_SESSION_KEY}.`);
1121+
logger.log(`OK Workspace tool session state loaded from ${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.`);
11191122
logger.log("Workspace launch repo context resolved from session storage; independent repo selection is not required.");
11201123
this.syncGeneratePreviewButton();
11211124
}

tools/session-inspector-v2/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ Session Inspector V2 is a first-class tool for inspecting and clearing current-o
1212
- Open `tools/session-inspector-v2/index.html`.
1313
- Seed a storage key in the current origin.
1414
- Refresh the tool and verify the key appears in the Entries list.
15-
- Select an entry and confirm the Details panel shows raw value, parsed value, parse status, and byte size.
15+
- Select an entry and confirm the JSON panel shows the full stored value.
16+
- Select a normalized workspace tool entry and confirm the Schema panel shows only its schema section.
1617
- Use per-entry Delete or Delete All and verify the Entries list and status log update immediately.

tools/session-inspector-v2/index.html

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,27 @@ <h2 class="tools-platform-frame__eyebrow">Session storage visibility</h2>
9595
</section>
9696
</section>
9797

98-
<aside class="session-inspector-v2__panel session-inspector-v2__panel--right tool-shell-common__fullscreen-panel tool-shell-common__fullscreen-side-panel tool-shell-common__fullscreen-panel-right" aria-label="Session details">
98+
<aside class="session-inspector-v2__panel session-inspector-v2__panel--right tool-shell-common__fullscreen-panel tool-shell-common__fullscreen-side-panel tool-shell-common__fullscreen-panel-right" aria-label="Session JSON and schema">
9999
<section class="accordion-v2 session-inspector-v2__accordion session-inspector-v2__accordion--fill is-open" data-accordion-v2-open="true">
100-
<div class="accordion-v2__header session-inspector-v2__details-accordion-header" role="button" tabindex="0" aria-expanded="true" aria-controls="sessionInspectorV2DetailsContent">
101-
<span>Details</span>
102-
<div class="session-inspector-v2__details-header-actions">
100+
<div class="accordion-v2__header session-inspector-v2__json-accordion-header" role="button" tabindex="0" aria-expanded="true" aria-controls="sessionInspectorV2JsonContent">
101+
<span>JSON</span>
102+
<div class="session-inspector-v2__json-header-actions">
103103
<span class="accordion-v2__icon" aria-hidden="true">+</span>
104-
<button id="copySessionInspectorV2DetailsButton" type="button">Copy</button>
104+
<button id="copySessionInspectorV2JsonButton" type="button">Copy</button>
105105
</div>
106106
</div>
107-
<div id="sessionInspectorV2DetailsContent" class="accordion-v2__content">
108-
<pre id="sessionInspectorV2DetailsOutput" class="session-inspector-v2__output">{}</pre>
107+
<div id="sessionInspectorV2JsonContent" class="accordion-v2__content">
108+
<pre id="sessionInspectorV2JsonOutput" class="session-inspector-v2__output">{}</pre>
109+
</div>
110+
</section>
111+
112+
<section class="accordion-v2 session-inspector-v2__accordion session-inspector-v2__accordion--fill is-open" data-accordion-v2-open="true">
113+
<div class="accordion-v2__header session-inspector-v2__schema-accordion-header" role="button" tabindex="0" aria-expanded="true" aria-controls="sessionInspectorV2SchemaContent">
114+
<span>Schema</span>
115+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
116+
</div>
117+
<div id="sessionInspectorV2SchemaContent" class="accordion-v2__content">
118+
<pre id="sessionInspectorV2SchemaOutput" class="session-inspector-v2__output">Select a storage entry with a top-level schema section.</pre>
109119
</div>
110120
</section>
111121

tools/session-inspector-v2/js/SessionInspectorV2App.js

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
export class SessionInspectorV2App {
22
constructor({
33
accordions,
4-
copyDetailsButton,
4+
copyJsonButton,
55
deleteAllButton,
6-
details,
76
entryList,
87
filters,
8+
json,
99
refreshButton,
1010
returnToWorkspaceButton,
1111
runtimeContract,
12+
schema,
1213
statusLog,
1314
storageService,
1415
windowRef = window
1516
}) {
1617
this.accordions = accordions;
17-
this.copyDetailsButton = copyDetailsButton;
18+
this.copyJsonButton = copyJsonButton;
1819
this.deleteAllButton = deleteAllButton;
19-
this.details = details;
2020
this.entries = [];
2121
this.entryList = entryList;
2222
this.filters = filters;
23+
this.json = json;
2324
this.refreshButton = refreshButton;
2425
this.returnToWorkspaceButton = returnToWorkspaceButton;
2526
this.runtimeContract = runtimeContract || { storageAccess: "read-only" };
27+
this.schema = schema;
2628
this.statusLog = statusLog;
2729
this.storageService = storageService;
2830
this.selectedId = "";
@@ -44,8 +46,8 @@ export class SessionInspectorV2App {
4446
onSelected: (entryId) => this.selectEntry(entryId)
4547
});
4648
this.refreshButton.addEventListener("click", () => this.refresh());
47-
this.copyDetailsButton.addEventListener("click", () => {
48-
void this.copyDetails();
49+
this.copyJsonButton.addEventListener("click", () => {
50+
void this.copyJson();
4951
});
5052
this.deleteAllButton.addEventListener("click", () => this.deleteAllShownEntries());
5153
this.returnToWorkspaceButton.addEventListener("click", () => this.returnToWorkspace());
@@ -62,7 +64,9 @@ export class SessionInspectorV2App {
6264
this.selectedId = this.entries[0]?.id || "";
6365
}
6466
this.entryList.render(this.entries, this.selectedId);
65-
this.details.render(this.entries.find((entry) => entry.id === this.selectedId));
67+
const selectedEntry = this.entries.find((entry) => entry.id === this.selectedId);
68+
this.json.render(selectedEntry);
69+
this.schema.render(selectedEntry);
6670
this.filters.setSummary(this.summaryCounts());
6771
if (!silent) {
6872
this.statusLog.ok(`Loaded ${this.entries.length} matching storage entries.`);
@@ -80,7 +84,8 @@ export class SessionInspectorV2App {
8084
this.selectedId = entryId;
8185
this.entryList.render(this.entries, this.selectedId);
8286
const entry = this.entries.find((candidate) => candidate.id === entryId);
83-
this.details.render(entry);
87+
this.json.render(entry);
88+
this.schema.render(entry);
8489
if (entry) {
8590
this.statusLog.info(`Selected ${entry.storageType}:${entry.key}.`);
8691
}
@@ -122,19 +127,19 @@ export class SessionInspectorV2App {
122127
this.refresh({ silent: true });
123128
}
124129

125-
async copyDetails() {
126-
const detailsText = this.details.text().trim();
127-
if (!detailsText || detailsText === "{}") {
128-
this.statusLog.warn("Copy skipped: no Details content is shown.");
130+
async copyJson() {
131+
const jsonText = this.json.text().trim();
132+
if (!jsonText || jsonText === "{}") {
133+
this.statusLog.warn("Copy skipped: no JSON content is shown.");
129134
return;
130135
}
131136
if (typeof this.window.navigator?.clipboard?.writeText !== "function") {
132137
this.statusLog.fail("Copy failed: clipboard API is unavailable.");
133138
return;
134139
}
135140
try {
136-
await this.window.navigator.clipboard.writeText(detailsText);
137-
this.statusLog.ok("Copied Details content to clipboard.");
141+
await this.window.navigator.clipboard.writeText(jsonText);
142+
this.statusLog.ok("Copied JSON content to clipboard.");
138143
} catch (error) {
139144
this.statusLog.fail(`Copy failed: ${error.message}`);
140145
}

tools/session-inspector-v2/js/bootstrap.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { SessionInspectorV2App } from "./SessionInspectorV2App.js";
22
import { AccordionSection } from "./controls/AccordionSection.js";
3-
import { DetailsControl } from "./controls/DetailsControl.js";
43
import { EntryListControl } from "./controls/EntryListControl.js";
54
import { FilterControl } from "./controls/FilterControl.js";
5+
import { JsonControl } from "./controls/JsonControl.js";
6+
import { SchemaControl } from "./controls/SchemaControl.js";
67
import { StatusLogControl } from "./controls/StatusLogControl.js";
78
import { sessionInspectorV2RuntimeContract } from "./services/SessionInspectorV2RuntimeContract.js";
89
import { SessionInspectorV2StorageService } from "./services/SessionInspectorV2StorageService.js";
@@ -18,9 +19,9 @@ function requireElement(selector) {
1819
window.addEventListener("DOMContentLoaded", () => {
1920
const app = new SessionInspectorV2App({
2021
accordions: Array.from(document.querySelectorAll(".accordion-v2"), (section) => new AccordionSection(section)),
21-
copyDetailsButton: requireElement("#copySessionInspectorV2DetailsButton"),
22-
details: new DetailsControl({
23-
output: requireElement("#sessionInspectorV2DetailsOutput")
22+
copyJsonButton: requireElement("#copySessionInspectorV2JsonButton"),
23+
json: new JsonControl({
24+
output: requireElement("#sessionInspectorV2JsonOutput")
2425
}),
2526
deleteAllButton: requireElement("#deleteAllSessionInspectorV2Button"),
2627
entryList: new EntryListControl({
@@ -34,6 +35,9 @@ window.addEventListener("DOMContentLoaded", () => {
3435
refreshButton: requireElement("#refreshSessionInspectorV2Button"),
3536
returnToWorkspaceButton: requireElement("#returnToWorkspaceButton"),
3637
runtimeContract: sessionInspectorV2RuntimeContract(),
38+
schema: new SchemaControl({
39+
output: requireElement("#sessionInspectorV2SchemaOutput")
40+
}),
3741
statusLog: new StatusLogControl({
3842
clearButton: requireElement("#clearSessionInspectorV2StatusButton"),
3943
output: requireElement("#statusLog")

tools/session-inspector-v2/js/controls/DetailsControl.js

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export class JsonControl {
2+
constructor({ output }) {
3+
this.output = output;
4+
}
5+
6+
clear() {
7+
this.output.textContent = "{}";
8+
}
9+
10+
text() {
11+
return String(this.output.textContent || "");
12+
}
13+
14+
render(entry) {
15+
if (!entry) {
16+
this.clear();
17+
return;
18+
}
19+
const value = entry.parseOk ? entry.parsedValue : entry.rawValue;
20+
this.output.textContent = JSON.stringify(value, null, 2);
21+
}
22+
}

0 commit comments

Comments
 (0)