Skip to content

Commit 16b0b64

Browse files
author
DavidQ
committed
Restore Session Inspector as a first-class tool without reintroducing tool communication coupling - PR_26128_002-restore-session-inspector
1 parent c3225ce commit 16b0b64

16 files changed

Lines changed: 849 additions & 0 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# PR_26128_002 Session Inspector Restore
2+
3+
## Changes
4+
- Restored `tools/session-inspector/**` as a self-contained first-class tool.
5+
- Added read-only storage inspection for current-origin `sessionStorage` and `localStorage`.
6+
- Added first-class registry wiring in `tools/toolRegistry.js`.
7+
- Added tools index grouping for `session-inspector` as a Viewer in `tools/renderToolsIndex.js`.
8+
- Added shared navigation grouping for `session-inspector` as a Viewer in `tools/shared/platformShell.js`.
9+
10+
## Boundaries
11+
- No cross-tool communication was added.
12+
- No storage writes are performed by `tools/session-inspector/**`.
13+
- No repo-selection or File System Access API code was added.
14+
- No sample JSON was modified.
15+
- No roadmap content was modified.
16+
17+
## Validation
18+
- `npm run test:workspace-v2`: Pass, 10 passed.
19+
- Targeted Session Inspector launch test: Pass.
20+
- Targeted tools index Session Inspector card/link validation: Pass.
21+
- Targeted Workspace Manager V2 launch validation: Pass.
22+
- Targeted Preview Generator V2 launch validation: Pass.
23+
- Full samples smoke test: Skipped by BUILD instruction. This PR is scoped to restoring Session Inspector first-class launch and registry/nav wiring only.
24+
25+
## Changed Files
26+
- `tools/session-inspector/index.html`
27+
- `tools/session-inspector/README.md`
28+
- `tools/session-inspector/how_to_use.html`
29+
- `tools/session-inspector/styles/sessionInspector.css`
30+
- `tools/session-inspector/js/bootstrap.js`
31+
- `tools/session-inspector/js/SessionInspectorApp.js`
32+
- `tools/session-inspector/js/controls/AccordionSection.js`
33+
- `tools/session-inspector/js/controls/DetailsControl.js`
34+
- `tools/session-inspector/js/controls/EntryListControl.js`
35+
- `tools/session-inspector/js/controls/FilterControl.js`
36+
- `tools/session-inspector/js/controls/StatusLogControl.js`
37+
- `tools/session-inspector/js/services/SessionInspectorStorageService.js`
38+
- `tools/toolRegistry.js`
39+
- `tools/renderToolsIndex.js`
40+
- `tools/shared/platformShell.js`

tools/renderToolsIndex.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ function classifyToolGroup(toolId) {
7878
]);
7979
const viewerToolIds = new Set([
8080
"3d-asset-viewer",
81+
"session-inspector",
8182
"replay-visualizer",
8283
"performance-profiler"
8384
]);

tools/session-inspector/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Session Inspector
2+
3+
Session Inspector is a read-only first-class tool for inspecting current-origin browser storage during Workspace V2 and tool launch validation.
4+
5+
## Scope
6+
- Reads `sessionStorage` and `localStorage` values for display only.
7+
- Does not write storage values.
8+
- Does not pass data to other tools.
9+
- Does not add repo selection or File System Access API behavior.
10+
11+
## Validation
12+
- Open `tools/session-inspector/index.html`.
13+
- Seed a storage key in the current origin.
14+
- 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.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Session Inspector - How To Use</title>
7+
<link rel="stylesheet" href="../../src/engine/theme/main.css">
8+
<link rel="stylesheet" href="../../src/engine/ui/hubCommon.css">
9+
</head>
10+
<body class="tools-platform-tool-page tools-platform-surface hub-page-tools" data-tool-id="session-inspector">
11+
<main class="wrap">
12+
<section class="hero">
13+
<h1>Session Inspector</h1>
14+
<p class="subtitle">Inspect current-origin storage values without writing handoff data or changing tool state.</p>
15+
</section>
16+
<section class="card">
17+
<h2>Use</h2>
18+
<p>Open the tool, choose a storage scope, filter by key or value text, and select an entry to inspect its parsed JSON and raw value.</p>
19+
</section>
20+
<section class="card">
21+
<h2>Boundaries</h2>
22+
<p>The tool is read-only. It does not create repo handles, write storage, modify workspace manifests, or communicate state to another tool.</p>
23+
</section>
24+
</main>
25+
</body>
26+
</html>

tools/session-inspector/index.html

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Session Inspector</title>
7+
<link rel="stylesheet" href="../../src/engine/theme/main.css">
8+
<link rel="stylesheet" href="../../src/engine/ui/hubCommon.css">
9+
<link rel="stylesheet" href="../../src/engine/theme/accordionV2/accordionV2.css">
10+
<link rel="stylesheet" href="./styles/sessionInspector.css">
11+
<script type="module" src="./js/bootstrap.js"></script>
12+
</head>
13+
<body class="tools-platform-tool-page tools-platform-surface hub-page-tools" data-tool-id="session-inspector">
14+
<details class="is-collapsible" open>
15+
<summary class="is-collapsible__summary" data-session-inspector-summary>Hide Header and Details</summary>
16+
<div class="is-collapsible__content">
17+
<div id="shared-theme-header"></div>
18+
<header class="session-inspector__header" data-session-inspector-header>
19+
<section class="tools-platform-frame session-inspector__local-shell-frame">
20+
<div class="tools-platform-frame__accordion-content">
21+
<div class="tools-platform-frame__accordion-summary">
22+
<div class="tools-platform-frame__summary-copy">
23+
<h1 class="tools-platform-frame__title" data-tool-id="session-inspector">Session Inspector</h1>
24+
<h2 class="tools-platform-frame__eyebrow">Read-only session visibility</h2>
25+
</div>
26+
<div class="tools-platform-frame__summary-meta">
27+
<div class="tools-platform-frame__meta">Inspect current-origin storage values without writing handoff data.</div>
28+
</div>
29+
</div>
30+
</div>
31+
</section>
32+
</header>
33+
</div>
34+
</details>
35+
36+
<nav class="session-inspector__menu" aria-label="Session Inspector actions">
37+
<button id="refreshSessionButton" type="button">Refresh</button>
38+
<button id="clearStatusButton" type="button">Clear Status</button>
39+
</nav>
40+
41+
<main class="session-inspector app-shell" data-tool-id="session-inspector">
42+
<aside class="session-inspector__panel session-inspector__panel--left" aria-label="Session filters">
43+
<section class="accordion-v2 session-inspector__accordion is-open" data-accordion-v2-open="true">
44+
<button class="accordion-v2__header" type="button" aria-expanded="true" aria-controls="sessionFiltersContent">
45+
<span>Filters</span>
46+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
47+
</button>
48+
<div id="sessionFiltersContent" class="accordion-v2__content">
49+
<label class="session-inspector__field" for="storageScopeSelect">
50+
<span>Storage</span>
51+
<select id="storageScopeSelect">
52+
<option value="sessionStorage">sessionStorage</option>
53+
<option value="localStorage">localStorage</option>
54+
<option value="all">All storage</option>
55+
</select>
56+
</label>
57+
<label class="session-inspector__field" for="sessionFilterInput">
58+
<span>Filter</span>
59+
<input id="sessionFilterInput" type="search" autocomplete="off" placeholder="Key or value text">
60+
</label>
61+
<p id="sessionSummary" class="session-inspector__hint">No storage entries loaded.</p>
62+
</div>
63+
</section>
64+
</aside>
65+
66+
<section class="session-inspector__panel session-inspector__panel--center" aria-label="Storage entries">
67+
<section class="accordion-v2 session-inspector__accordion session-inspector__accordion--fill is-open" data-accordion-v2-open="true">
68+
<button class="accordion-v2__header" type="button" aria-expanded="true" aria-controls="sessionEntriesContent">
69+
<span>Entries</span>
70+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
71+
</button>
72+
<div id="sessionEntriesContent" class="accordion-v2__content">
73+
<div id="sessionEntryList" class="session-inspector__entry-list" aria-label="Storage entries"></div>
74+
</div>
75+
</section>
76+
</section>
77+
78+
<aside class="session-inspector__panel session-inspector__panel--right" aria-label="Session details">
79+
<section class="accordion-v2 session-inspector__accordion session-inspector__accordion--fill is-open" data-accordion-v2-open="true">
80+
<button class="accordion-v2__header" type="button" aria-expanded="true" aria-controls="sessionDetailsContent">
81+
<span>Details</span>
82+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
83+
</button>
84+
<div id="sessionDetailsContent" class="accordion-v2__content">
85+
<pre id="sessionDetailsOutput" class="session-inspector__output">{}</pre>
86+
</div>
87+
</section>
88+
89+
<section class="accordion-v2 session-inspector__accordion is-open" data-accordion-v2-open="true">
90+
<div class="accordion-v2__header session-inspector__status-accordion-header" role="button" tabindex="0" aria-expanded="true" aria-controls="sessionStatusContent">
91+
<span>Status</span>
92+
<div class="session-inspector__status-header-actions">
93+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
94+
</div>
95+
</div>
96+
<div id="sessionStatusContent" class="accordion-v2__content">
97+
<textarea id="statusLog" readonly rows="10" aria-label="Session Inspector status log"></textarea>
98+
</div>
99+
</section>
100+
</aside>
101+
</main>
102+
<script type="module" src="../../src/engine/theme/mount-shared-header.js"></script>
103+
</body>
104+
</html>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
export class SessionInspectorApp {
2+
constructor({
3+
accordions,
4+
details,
5+
entryList,
6+
filters,
7+
refreshButton,
8+
statusLog,
9+
storageService
10+
}) {
11+
this.accordions = accordions;
12+
this.details = details;
13+
this.entries = [];
14+
this.entryList = entryList;
15+
this.filters = filters;
16+
this.refreshButton = refreshButton;
17+
this.statusLog = statusLog;
18+
this.storageService = storageService;
19+
this.selectedId = "";
20+
}
21+
22+
start() {
23+
this.accordions.forEach((accordion) => accordion.mount());
24+
this.statusLog.mount();
25+
this.filters.mount({
26+
onFilterChanged: () => this.refresh({ silent: true })
27+
});
28+
this.entryList.mount({
29+
onSelected: (entryId) => this.selectEntry(entryId)
30+
});
31+
this.refreshButton.addEventListener("click", () => this.refresh());
32+
this.refresh({ silent: true });
33+
this.statusLog.ok("Session Inspector ready. Storage is read-only.");
34+
}
35+
36+
refresh({ silent = false } = {}) {
37+
this.entries = this.storageService.readEntries({
38+
filterText: this.filters.filterText(),
39+
scope: this.filters.scope()
40+
});
41+
if (!this.entries.some((entry) => entry.id === this.selectedId)) {
42+
this.selectedId = this.entries[0]?.id || "";
43+
}
44+
this.entryList.render(this.entries, this.selectedId);
45+
this.details.render(this.entries.find((entry) => entry.id === this.selectedId));
46+
this.filters.setSummary(this.summaryText());
47+
if (!silent) {
48+
this.statusLog.ok(`Loaded ${this.entries.length} matching storage entries.`);
49+
}
50+
}
51+
52+
summaryText() {
53+
const sessionCount = this.entries.filter((entry) => entry.storageType === "sessionStorage").length;
54+
const localCount = this.entries.filter((entry) => entry.storageType === "localStorage").length;
55+
const totalCount = this.entries.length;
56+
return `${totalCount} entries shown. sessionStorage: ${sessionCount}. localStorage: ${localCount}.`;
57+
}
58+
59+
selectEntry(entryId) {
60+
this.selectedId = entryId;
61+
this.entryList.render(this.entries, this.selectedId);
62+
const entry = this.entries.find((candidate) => candidate.id === entryId);
63+
this.details.render(entry);
64+
if (entry) {
65+
this.statusLog.info(`Selected ${entry.storageType}:${entry.key}.`);
66+
}
67+
}
68+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { SessionInspectorApp } from "./SessionInspectorApp.js";
2+
import { AccordionSection } from "./controls/AccordionSection.js";
3+
import { DetailsControl } from "./controls/DetailsControl.js";
4+
import { EntryListControl } from "./controls/EntryListControl.js";
5+
import { FilterControl } from "./controls/FilterControl.js";
6+
import { StatusLogControl } from "./controls/StatusLogControl.js";
7+
import { SessionInspectorStorageService } from "./services/SessionInspectorStorageService.js";
8+
9+
function requireElement(selector) {
10+
const element = document.querySelector(selector);
11+
if (!element) {
12+
throw new Error(`Missing required Session Inspector element: ${selector}`);
13+
}
14+
return element;
15+
}
16+
17+
window.addEventListener("DOMContentLoaded", () => {
18+
const app = new SessionInspectorApp({
19+
accordions: Array.from(document.querySelectorAll(".accordion-v2"), (section) => new AccordionSection(section)),
20+
details: new DetailsControl({
21+
output: requireElement("#sessionDetailsOutput")
22+
}),
23+
entryList: new EntryListControl({
24+
container: requireElement("#sessionEntryList")
25+
}),
26+
filters: new FilterControl({
27+
filterInput: requireElement("#sessionFilterInput"),
28+
scopeSelect: requireElement("#storageScopeSelect"),
29+
summary: requireElement("#sessionSummary")
30+
}),
31+
refreshButton: requireElement("#refreshSessionButton"),
32+
statusLog: new StatusLogControl({
33+
clearButton: requireElement("#clearStatusButton"),
34+
output: requireElement("#statusLog")
35+
}),
36+
storageService: new SessionInspectorStorageService()
37+
});
38+
window.__sessionInspectorApp = app;
39+
app.start();
40+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export class AccordionSection {
2+
constructor(section) {
3+
this.section = section;
4+
this.header = section?.querySelector(".accordion-v2__header") || null;
5+
this.content = section?.querySelector(".accordion-v2__content") || null;
6+
this.icon = this.header?.querySelector(".accordion-v2__icon") || null;
7+
}
8+
9+
mount() {
10+
if (!this.section || !this.header || !this.content || this.header.dataset.accordionV2Bound === "true") {
11+
return;
12+
}
13+
14+
this.header.dataset.accordionV2Bound = "true";
15+
this.setOpen(this.section.dataset.accordionV2Open !== "false");
16+
this.header.addEventListener("click", (event) => {
17+
const clickedNestedButton = event.target instanceof Element
18+
&& event.target.closest("button")
19+
&& event.target !== this.header;
20+
if (clickedNestedButton) {
21+
return;
22+
}
23+
this.setOpen(!this.section.classList.contains("is-open"));
24+
});
25+
this.header.addEventListener("keydown", (event) => {
26+
if (event.key !== "Enter" && event.key !== " ") {
27+
return;
28+
}
29+
event.preventDefault();
30+
this.setOpen(!this.section.classList.contains("is-open"));
31+
});
32+
}
33+
34+
setOpen(isOpen) {
35+
this.section.classList.toggle("is-open", isOpen);
36+
this.section.dataset.accordionV2Open = String(isOpen);
37+
this.header.setAttribute("aria-expanded", String(isOpen));
38+
this.content.hidden = !isOpen;
39+
if (this.icon) {
40+
this.icon.dataset.accordionV2IconState = isOpen ? "open" : "closed";
41+
}
42+
}
43+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export class DetailsControl {
2+
constructor({ output }) {
3+
this.output = output;
4+
}
5+
6+
clear() {
7+
this.output.textContent = "{}";
8+
}
9+
10+
render(entry) {
11+
if (!entry) {
12+
this.clear();
13+
return;
14+
}
15+
this.output.textContent = JSON.stringify({
16+
storageType: entry.storageType,
17+
key: entry.key,
18+
valueType: entry.valueType,
19+
sizeBytes: entry.sizeBytes,
20+
parseOk: entry.parseOk,
21+
parsedValue: entry.parsedValue,
22+
rawValue: entry.rawValue
23+
}, null, 2);
24+
}
25+
}

0 commit comments

Comments
 (0)