Skip to content

Commit 4dae430

Browse files
author
DavidQ
committed
Add row-level saved session actions and clarify library labels - PR 11.247
1 parent 96e814c commit 4dae430

4 files changed

Lines changed: 301 additions & 7 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# PR_11_247 — Saved Session Row Actions + Clear Labels
2+
3+
## Summary
4+
Implemented row-level Session Library actions so saved sessions are usable without copy/paste. Added explicit saved-vs-recent helper text and direct row controls for Copy ID, Use in Library, Load, and Delete Saved.
5+
6+
## Files Changed
7+
- `tools/workspace-v2/index.html`
8+
- `tools/workspace-v2/index.js`
9+
- `tests/runtime/V2SavedSessionRowActions.test.mjs`
10+
11+
## Implemented UI/Behavior
12+
- Updated helper text to:
13+
- `Saved sessions are stored in Session Library. Recent sessions are temporary.`
14+
- Each saved library row now shows:
15+
- readable label (`toolId` when available)
16+
- full session ID
17+
- `Copy ID`
18+
- `Use in Library`
19+
- `Load`
20+
- `Delete Saved`
21+
- Row actions:
22+
- `Use in Library` populates Session ID textbox.
23+
- `Copy ID` copies exact saved session ID to clipboard.
24+
- `Load` loads saved session directly and refreshes Session Library display.
25+
- `Delete Saved` deletes only the saved library entry (no recent/sessionStorage deletion path).
26+
- Existing textbox actions are preserved:
27+
- `Save Session`
28+
- `Overwrite Session`
29+
- `Load Session`
30+
- `Delete Saved Session`
31+
- Existing library mutation path still recomputes Diff/Merge inventory via `writeSessionLibrary(...)`.
32+
33+
## Validation Commands Run
34+
```powershell
35+
node --check tools/workspace-v2/index.js
36+
node --check tests/runtime/V2SavedSessionRowActions.test.mjs
37+
node tests/runtime/V2SavedSessionRowActions.test.mjs
38+
```
39+
40+
## Validation Results
41+
- `node --check tools/workspace-v2/index.js` -> PASS
42+
- `node --check tests/runtime/V2SavedSessionRowActions.test.mjs` -> PASS
43+
- `node tests/runtime/V2SavedSessionRowActions.test.mjs` -> PASS
44+
- output: `tmp/v2-saved-session-row-actions-results.json`
45+
- failures: `[]`
46+
47+
## Coverage Confirmed
48+
- saved rows expose Copy ID / Use in Library / Load / Delete Saved -> PASS
49+
- row Copy ID copies exact saved ID -> PASS
50+
- row Use in Library populates textbox -> PASS
51+
- row Load executes saved-session load path -> PASS
52+
- row Delete Saved removes saved entry only -> PASS
53+
- Recent Sessions unaffected by row Delete Saved -> PASS
54+
- textbox action methods remain present and wired -> PASS
55+
56+
## Full Samples Smoke
57+
Skipped.
58+
59+
Reason: PR scope is limited to Workspace V2 Session Library UI/action surface and targeted runtime validation. No schemas/samples/games/shared sample infrastructure changed.
60+
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
import { execFileSync } from "node:child_process";
5+
import { fileURLToPath, pathToFileURL } from "node:url";
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = path.dirname(__filename);
9+
const repoRoot = path.resolve(__dirname, "..", "..");
10+
const htmlPath = path.join(repoRoot, "tools", "workspace-v2", "index.html");
11+
const jsPath = path.join(repoRoot, "tools", "workspace-v2", "index.js");
12+
const resultsPath = path.join(repoRoot, "tmp", "v2-saved-session-row-actions-results.json");
13+
14+
function readText(filePath) {
15+
return fs.readFileSync(filePath, "utf8");
16+
}
17+
18+
function checkSyntax(filePath) {
19+
try {
20+
execFileSync(process.execPath, ["--check", filePath], {
21+
cwd: repoRoot,
22+
stdio: ["ignore", "pipe", "pipe"]
23+
});
24+
return { ok: true, error: "" };
25+
} catch (error) {
26+
return { ok: false, error: (error?.stderr || error?.stdout || error?.message || "").toString().trim() };
27+
}
28+
}
29+
30+
function simulateDeleteSavedRow(sessionId, library, history, storage) {
31+
const nextLibrary = { ...library };
32+
const nextHistory = [...history];
33+
const nextStorage = { ...storage };
34+
if (Object.prototype.hasOwnProperty.call(nextLibrary, sessionId)) {
35+
delete nextLibrary[sessionId];
36+
return { message: "Saved session deleted.", nextLibrary, nextHistory, nextStorage };
37+
}
38+
return { message: "Saved session not found.", nextLibrary, nextHistory, nextStorage };
39+
}
40+
41+
export function run() {
42+
const failures = [];
43+
const htmlExists = fs.existsSync(htmlPath);
44+
const jsExists = fs.existsSync(jsPath);
45+
const html = htmlExists ? readText(htmlPath) : "";
46+
const js = jsExists ? readText(jsPath) : "";
47+
const jsSyntax = checkSyntax(jsPath);
48+
const testSyntax = checkSyntax(path.join(repoRoot, "tests", "runtime", "V2SavedSessionRowActions.test.mjs"));
49+
50+
if (!htmlExists) failures.push("Missing tools/workspace-v2/index.html.");
51+
if (!jsExists) failures.push("Missing tools/workspace-v2/index.js.");
52+
if (!jsSyntax.ok) failures.push("tools/workspace-v2/index.js failed syntax check.");
53+
if (!testSyntax.ok) failures.push("tests/runtime/V2SavedSessionRowActions.test.mjs failed syntax check.");
54+
55+
if (!html.includes("Saved sessions are stored in Session Library. Recent sessions are temporary.")) {
56+
failures.push("Helper text is not updated to saved-vs-recent clarification.");
57+
}
58+
59+
const requiredRowTokens = [
60+
"copyIdButton.textContent = \"Copy ID\";",
61+
"useInLibraryButton.textContent = \"Use in Library\";",
62+
"loadButton.textContent = \"Load\";",
63+
"deleteSavedButton.textContent = \"Delete Saved\";",
64+
"copySavedSessionIdToClipboard(sessionId)",
65+
"useSavedSessionIdInLibraryInput(sessionId)",
66+
"loadSavedSessionById(sessionId)",
67+
"deleteSavedSessionById(sessionId)"
68+
];
69+
requiredRowTokens.forEach((token) => {
70+
if (!js.includes(token)) {
71+
failures.push(`Missing required saved-row action token: ${token}`);
72+
}
73+
});
74+
75+
if (!js.includes("await navigator.clipboard.writeText(sessionId.trim());")) {
76+
failures.push("Row Copy ID does not copy exact saved session ID.");
77+
}
78+
if (!js.includes("this.sessionNameNode.value = sessionId.trim();")) {
79+
failures.push("Use in Library / row actions are not populating Session ID textbox.");
80+
}
81+
if (!js.includes("this.loadNamedSession();")) {
82+
failures.push("Row Load is not wired to library load action.");
83+
}
84+
if (!js.includes("this.deleteNamedSession();")) {
85+
failures.push("Row Delete Saved is not wired to library delete action.");
86+
}
87+
if (!js.includes("this.renderSessionLibrary();")) {
88+
failures.push("Session Library refresh after row actions is missing.");
89+
}
90+
if (!js.includes("this.renderSessionDiffInputs();") || !js.includes("this.renderSessionMergeInputs();")) {
91+
failures.push("Diff/Merge selector recompute hooks are missing.");
92+
}
93+
94+
const library = {
95+
"asset-browser-v2-1777676088718-3eff5h3y": { version: "v2", toolId: "asset-browser-v2", payloadJson: { a: 1 } },
96+
"palette-manager-v2-1777676088720-abcd1234": { version: "v2", toolId: "palette-manager-v2", payloadJson: { b: 2 } }
97+
};
98+
const history = [{ hostContextId: "asset-browser-v2-1777676088718-3eff5h3y", tool: "asset-browser-v2", timestamp: "2026-05-01T00:00:00.000Z", payload: { version: "v2", toolId: "asset-browser-v2" } }];
99+
const storage = { "asset-browser-v2-1777676088718-3eff5h3y": "{\"version\":\"v2\",\"toolId\":\"asset-browser-v2\"}" };
100+
101+
const deleted = simulateDeleteSavedRow("asset-browser-v2-1777676088718-3eff5h3y", library, history, storage);
102+
if (deleted.message !== "Saved session deleted.") {
103+
failures.push("Row Delete Saved should delete saved library entry.");
104+
}
105+
if (Object.prototype.hasOwnProperty.call(deleted.nextLibrary, "asset-browser-v2-1777676088718-3eff5h3y")) {
106+
failures.push("Row Delete Saved did not remove saved entry.");
107+
}
108+
if (deleted.nextHistory.length !== history.length) {
109+
failures.push("Row Delete Saved must not remove Recent Sessions.");
110+
}
111+
if (!Object.prototype.hasOwnProperty.call(deleted.nextStorage, "asset-browser-v2-1777676088718-3eff5h3y")) {
112+
failures.push("Row Delete Saved must not remove sessionStorage payload.");
113+
}
114+
115+
const textboxActionsStillPresent = [
116+
"saveNamedSession(overwriteExisting)",
117+
"loadNamedSession()",
118+
"deleteNamedSession()",
119+
"Saved session already exists. Use Overwrite Session.",
120+
"Saved session not found.",
121+
"Saved session loaded.",
122+
"Saved session deleted."
123+
];
124+
textboxActionsStillPresent.forEach((token) => {
125+
if (!js.includes(token)) {
126+
failures.push(`Textbox Session Library action token missing: ${token}`);
127+
}
128+
});
129+
130+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
131+
fs.writeFileSync(resultsPath, `${JSON.stringify({
132+
generatedAt: new Date().toISOString(),
133+
failures,
134+
checks: {
135+
htmlExists,
136+
jsExists,
137+
jsSyntax,
138+
testSyntax
139+
},
140+
scenarios: {
141+
deleted
142+
}
143+
}, null, 2)}\n`, "utf8");
144+
145+
console.log(`v2 saved-session-row-actions results: ${resultsPath}`);
146+
assert.equal(failures.length, 0, `V2 saved-session-row-actions failures: ${failures.join(" | ")}`);
147+
return { failures };
148+
}
149+
150+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
151+
try {
152+
const summary = run();
153+
console.log(JSON.stringify(summary, null, 2));
154+
} catch (error) {
155+
console.error(error);
156+
process.exitCode = 1;
157+
}
158+
}

tools/workspace-v2/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ <h2>Share Session Link</h2>
5959
<h2>Session Library</h2>
6060
<label for="workspaceV2SessionName">Session ID (for Save / Load / Delete)</label>
6161
<input id="workspaceV2SessionName" type="text" placeholder="session-id" />
62-
<p>Use a session ID from Recent Sessions or saved library.</p>
62+
<p>Saved sessions are stored in Session Library. Recent sessions are temporary.</p>
6363
<div>
6464
<button id="workspaceV2SaveSessionButton" type="button">Save Session</button>
6565
<button id="workspaceV2OverwriteSessionButton" type="button">Overwrite Session</button>

tools/workspace-v2/index.js

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -434,19 +434,95 @@ class WorkspaceV2SessionProducer {
434434
this.libraryEmptyState.textContent = "No saved sessions in library.";
435435
sessionNames.forEach((sessionName) => {
436436
const item = document.createElement("li");
437-
const button = document.createElement("button");
438-
button.type = "button";
439-
button.textContent = sessionName;
440-
button.addEventListener("click", () => {
441-
this.sessionNameNode.value = sessionName;
437+
const payload = library[sessionName];
438+
const label = document.createElement("strong");
439+
const idLine = document.createElement("div");
440+
const idLabel = document.createElement("span");
441+
const idCode = document.createElement("code");
442+
const copyIdButton = document.createElement("button");
443+
const useInLibraryButton = document.createElement("button");
444+
const loadButton = document.createElement("button");
445+
const deleteSavedButton = document.createElement("button");
446+
const readableLabel = payload && typeof payload.toolId === "string" && payload.toolId.trim()
447+
? payload.toolId.trim()
448+
: "saved-session";
449+
label.textContent = `${readableLabel} (${sessionName})`;
450+
idLabel.textContent = "Session ID: ";
451+
idCode.textContent = sessionName;
452+
idCode.title = sessionName;
453+
idLine.append(idLabel, idCode);
454+
copyIdButton.type = "button";
455+
copyIdButton.textContent = "Copy ID";
456+
copyIdButton.addEventListener("click", () => {
457+
this.copySavedSessionIdToClipboard(sessionName);
442458
});
443-
item.appendChild(button);
459+
useInLibraryButton.type = "button";
460+
useInLibraryButton.textContent = "Use in Library";
461+
useInLibraryButton.addEventListener("click", () => {
462+
this.useSavedSessionIdInLibraryInput(sessionName);
463+
});
464+
loadButton.type = "button";
465+
loadButton.textContent = "Load";
466+
loadButton.addEventListener("click", () => {
467+
this.loadSavedSessionById(sessionName);
468+
});
469+
deleteSavedButton.type = "button";
470+
deleteSavedButton.textContent = "Delete Saved";
471+
deleteSavedButton.addEventListener("click", () => {
472+
this.deleteSavedSessionById(sessionName);
473+
});
474+
item.append(label, idLine, copyIdButton, useInLibraryButton, loadButton, deleteSavedButton);
444475
this.sessionListNode.appendChild(item);
445476
});
446477
this.renderSessionDiffInputs();
447478
this.renderSessionMergeInputs();
448479
}
449480

481+
async copySavedSessionIdToClipboard(sessionId) {
482+
if (typeof sessionId !== "string" || !sessionId.trim()) {
483+
this.setLibraryStatus("Copy ID failed: saved session ID is missing.");
484+
return;
485+
}
486+
try {
487+
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
488+
this.setLibraryStatus("Copy ID is unavailable in this browser context.");
489+
return;
490+
}
491+
await navigator.clipboard.writeText(sessionId.trim());
492+
this.setLibraryStatus(`Saved session ID copied: ${sessionId.trim()}`);
493+
} catch (error) {
494+
this.setLibraryStatus(`Copy ID failed: ${error instanceof Error ? error.message : "unknown error"}`);
495+
}
496+
}
497+
498+
useSavedSessionIdInLibraryInput(sessionId) {
499+
if (typeof sessionId !== "string" || !sessionId.trim()) {
500+
this.setLibraryStatus("Use in Library failed: saved session ID is missing.");
501+
return;
502+
}
503+
this.sessionNameNode.value = sessionId.trim();
504+
this.setLibraryStatus(`Saved session ID ready for Library actions: ${sessionId.trim()}`);
505+
}
506+
507+
loadSavedSessionById(sessionId) {
508+
if (typeof sessionId !== "string" || !sessionId.trim()) {
509+
this.setLibraryStatus("Enter a saved session ID before loading.");
510+
return;
511+
}
512+
this.sessionNameNode.value = sessionId.trim();
513+
this.loadNamedSession();
514+
this.renderSessionLibrary();
515+
}
516+
517+
deleteSavedSessionById(sessionId) {
518+
if (typeof sessionId !== "string" || !sessionId.trim()) {
519+
this.setLibraryStatus("Enter a saved session ID before deleting.");
520+
return;
521+
}
522+
this.sessionNameNode.value = sessionId.trim();
523+
this.deleteNamedSession();
524+
}
525+
450526
isValidSessionHistoryEntry(entry) {
451527
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
452528
if (typeof entry.hostContextId !== "string" || !entry.hostContextId.trim()) return false;

0 commit comments

Comments
 (0)