Skip to content

Commit ec77fe9

Browse files
author
DavidQ
committed
Add direct recent session delete and clarify saved-session delete scope - PR 11.242
1 parent 66c26cf commit ec77fe9

4 files changed

Lines changed: 237 additions & 3 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# PR_11_242 — Recent Session Delete + Library Delete Scope Clarification
2+
3+
## Summary
4+
Implemented Workspace V2 session deletion UX updates so recent sessions can be deleted directly, library delete scope is explicit, and recent-only session IDs entered in the library delete field produce a clear actionable message.
5+
6+
## Files Changed
7+
- `tools/workspace-v2/index.html`
8+
- `tools/workspace-v2/index.js`
9+
- `tests/runtime/V2RecentSessionDelete.test.mjs`
10+
11+
## Implementation Notes
12+
- Added per-row `Delete` action in Recent Sessions UI.
13+
- Added `deleteRecentSessionEntry(hostContextId)` flow to:
14+
- remove the recent history entry,
15+
- remove matching `sessionStorage` payload by `hostContextId`,
16+
- clear active session state when deleting the active context,
17+
- refresh recent list and dependent selector state.
18+
- Clarified library delete scope in UI label:
19+
- `Delete Session` -> `Delete Saved Session`
20+
- Added explicit library-delete message for recent-only IDs:
21+
- `Session ID is not saved in Session Library. Use Delete on Recent Sessions to remove temporary sessions.`
22+
23+
## Validation Commands Run
24+
```powershell
25+
node --check tools/workspace-v2/index.js
26+
node --check tests/runtime/V2RecentSessionDelete.test.mjs
27+
node --check tests/runtime/V2DiffMergeButtonState.test.mjs
28+
node tests/runtime/V2RecentSessionDelete.test.mjs
29+
node tests/runtime/V2DiffMergeButtonState.test.mjs
30+
```
31+
32+
## Validation Results
33+
- `node --check tools/workspace-v2/index.js` -> PASS
34+
- `node --check tests/runtime/V2RecentSessionDelete.test.mjs` -> PASS
35+
- `node --check tests/runtime/V2DiffMergeButtonState.test.mjs` -> PASS
36+
- `node tests/runtime/V2RecentSessionDelete.test.mjs` -> PASS
37+
- Output: `tmp/v2-recent-session-delete-results.json`
38+
- Failures: `[]`
39+
- `node tests/runtime/V2DiffMergeButtonState.test.mjs` -> PASS
40+
- Output: `tmp/v2-diff-merge-button-state-results.json`
41+
- Failures: `[]`
42+
43+
## Required Behavior Coverage
44+
- Delete Recent removes recent entry -> PASS
45+
- Delete Recent removes matching `sessionStorage` payload -> PASS
46+
- Delete Saved Session does not delete recent-only sessions -> PASS
47+
- Recent-only delete attempt through library shows clear message -> PASS
48+
- Deleting selected session clears Diff/Merge selection restoration paths -> PASS
49+
- Deleting active session clears active state safely -> PASS
50+
51+
## Full Samples Smoke
52+
Skipped.
53+
54+
Reason: PR scope is limited to Workspace V2 Session Library + Recent Sessions UI and targeted runtime validation. No schemas/samples/games/shared sample infrastructure were modified.
55+
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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-recent-session-delete-results.json");
13+
14+
function readText(filePath) {
15+
return fs.readFileSync(filePath, "utf8");
16+
}
17+
18+
function checkSyntax(jsFilePath) {
19+
try {
20+
execFileSync(process.execPath, ["--check", jsFilePath], {
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 resolvePersistedSelectionIds(entries, persisted) {
31+
if (!Array.isArray(entries) || entries.length < 2) return { leftId: "", rightId: "" };
32+
if (!persisted.sessionA || !persisted.sessionB || persisted.sessionA === persisted.sessionB) return { leftId: "", rightId: "" };
33+
const left = entries.find((entry) => entry.contextId === persisted.sessionA);
34+
const right = entries.find((entry) => entry.contextId === persisted.sessionB);
35+
if (!left || !right || left.id === right.id) return { leftId: "", rightId: "" };
36+
return { leftId: left.id, rightId: right.id };
37+
}
38+
39+
export function run() {
40+
const failures = [];
41+
const htmlExists = fs.existsSync(htmlPath);
42+
const jsExists = fs.existsSync(jsPath);
43+
const html = htmlExists ? readText(htmlPath) : "";
44+
const js = jsExists ? readText(jsPath) : "";
45+
const syntax = checkSyntax(jsPath);
46+
47+
const hasDeleteSavedLabel = html.includes("Delete Saved Session");
48+
const hasRecentDeleteButton = js.includes("deleteRecentButton.textContent = \"Delete\";");
49+
const hasRecentDeleteMethod = js.includes("deleteRecentSessionEntry(hostContextId)");
50+
const hasRecentDeleteHistoryWrite = js.includes("const nextHistory = history.filter((entry) => entry.hostContextId !== sessionId);") && js.includes("this.writeSessionHistory(nextHistory);");
51+
const hasRecentDeleteStorageClear = js.includes("sessionStorage.removeItem(sessionId);");
52+
const hasRecentDeleteActiveClear = js.includes("if (this.currentHostContextId === sessionId)") && js.includes("this.currentHostContextId = \"\";");
53+
const hasLibraryScopeMessage = js.includes("Session ID is not saved in Session Library. Use Delete on Recent Sessions to remove temporary sessions.");
54+
55+
if (!htmlExists) failures.push("Missing tools/workspace-v2/index.html.");
56+
if (!jsExists) failures.push("Missing tools/workspace-v2/index.js.");
57+
if (!syntax.ok) failures.push("tools/workspace-v2/index.js failed syntax check.");
58+
if (!hasDeleteSavedLabel) failures.push("Delete Saved Session label is missing.");
59+
if (!hasRecentDeleteButton) failures.push("Recent Session row Delete button is missing.");
60+
if (!hasRecentDeleteMethod) failures.push("deleteRecentSessionEntry(hostContextId) is missing.");
61+
if (!hasRecentDeleteHistoryWrite) failures.push("Recent delete does not remove the history entry.");
62+
if (!hasRecentDeleteStorageClear) failures.push("Recent delete does not remove matching sessionStorage payload.");
63+
if (!hasRecentDeleteActiveClear) failures.push("Recent delete does not clear active hostContextId when deleted.");
64+
if (!hasLibraryScopeMessage) failures.push("Missing explicit message for recent-only delete attempts in library delete flow.");
65+
66+
const history = [
67+
{ hostContextId: "ctx-a", tool: "asset-browser-v2", timestamp: "2026-05-01T00:00:00.000Z", payload: { toolId: "asset-browser-v2" } },
68+
{ hostContextId: "ctx-b", tool: "asset-browser-v2", timestamp: "2026-05-01T00:01:00.000Z", payload: { toolId: "asset-browser-v2" } }
69+
];
70+
const sessionStorageMap = { "ctx-a": "{\"toolId\":\"asset-browser-v2\"}", "ctx-b": "{\"toolId\":\"asset-browser-v2\"}" };
71+
const library = { "saved-1": { toolId: "asset-browser-v2" } };
72+
let currentHostContextId = "ctx-a";
73+
74+
const nextHistory = history.filter((entry) => entry.hostContextId !== "ctx-a");
75+
delete sessionStorageMap["ctx-a"];
76+
if (currentHostContextId === "ctx-a") currentHostContextId = "";
77+
78+
if (nextHistory.some((entry) => entry.hostContextId === "ctx-a")) {
79+
failures.push("Delete Recent should remove the recent entry.");
80+
}
81+
if (Object.prototype.hasOwnProperty.call(sessionStorageMap, "ctx-a")) {
82+
failures.push("Delete Recent should remove matching sessionStorage payload.");
83+
}
84+
if (currentHostContextId !== "") {
85+
failures.push("Deleting active session should clear active state safely.");
86+
}
87+
88+
const libraryDeleteAttemptId = "ctx-b";
89+
const inLibrary = Object.prototype.hasOwnProperty.call(library, libraryDeleteAttemptId);
90+
const inRecent = nextHistory.some((entry) => entry.hostContextId === libraryDeleteAttemptId);
91+
if (inLibrary) {
92+
failures.push("Fixture error: recent-only id unexpectedly exists in library.");
93+
}
94+
if (!inRecent) {
95+
failures.push("Fixture error: recent-only id should exist in recent history.");
96+
}
97+
98+
const entries = nextHistory.map((entry) => ({
99+
id: `history:${entry.hostContextId}`,
100+
contextId: entry.hostContextId
101+
}));
102+
const persisted = { sessionA: "ctx-a", sessionB: "ctx-b" };
103+
const restored = resolvePersistedSelectionIds(entries, persisted);
104+
if (restored.leftId || restored.rightId) {
105+
failures.push("Deleting a selected session should clear selections referencing it.");
106+
}
107+
108+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
109+
fs.writeFileSync(resultsPath, `${JSON.stringify({
110+
generatedAt: new Date().toISOString(),
111+
failures,
112+
checks: {
113+
htmlExists,
114+
jsExists,
115+
syntax,
116+
hasDeleteSavedLabel,
117+
hasRecentDeleteButton,
118+
hasRecentDeleteMethod,
119+
hasRecentDeleteHistoryWrite,
120+
hasRecentDeleteStorageClear,
121+
hasRecentDeleteActiveClear,
122+
hasLibraryScopeMessage
123+
},
124+
scenarios: {
125+
nextHistory,
126+
sessionStorageMap,
127+
currentHostContextId,
128+
libraryDeleteAttemptId,
129+
restored
130+
}
131+
}, null, 2)}\n`, "utf8");
132+
133+
console.log(`v2 recent-session delete results: ${resultsPath}`);
134+
assert.equal(failures.length, 0, `V2 recent-session delete failures: ${failures.join(" | ")}`);
135+
return { failures };
136+
}
137+
138+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
139+
try {
140+
const summary = run();
141+
console.log(JSON.stringify(summary, null, 2));
142+
} catch (error) {
143+
console.error(error);
144+
process.exitCode = 1;
145+
}
146+
}

tools/workspace-v2/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ <h2>Session Library</h2>
6464
<button id="workspaceV2SaveSessionButton" type="button">Save Session</button>
6565
<button id="workspaceV2OverwriteSessionButton" type="button">Overwrite Session</button>
6666
<button id="workspaceV2LoadSessionButton" type="button">Load Session</button>
67-
<button id="workspaceV2DeleteSessionButton" type="button">Delete Session</button>
67+
<button id="workspaceV2DeleteSessionButton" type="button">Delete Saved Session</button>
6868
</div>
6969
<p id="workspaceV2LibraryEmptyState">No saved sessions in library.</p>
7070
<ul id="workspaceV2SessionList" aria-label="Workspace V2 saved sessions"></ul>

tools/workspace-v2/index.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ class WorkspaceV2SessionProducer {
445445
const reopenButton = document.createElement("button");
446446
const copyIdButton = document.createElement("button");
447447
const useInLibraryButton = document.createElement("button");
448+
const deleteRecentButton = document.createElement("button");
448449
title.textContent = `${entry.tool} (${entry.hostContextId})`;
449450
idLabel.textContent = "Session ID: ";
450451
idCode.textContent = entry.hostContextId;
@@ -466,7 +467,12 @@ class WorkspaceV2SessionProducer {
466467
useInLibraryButton.addEventListener("click", () => {
467468
this.useSessionIdInLibraryInput(entry.hostContextId);
468469
});
469-
item.append(title, idLine, meta, reopenButton, copyIdButton, useInLibraryButton);
470+
deleteRecentButton.type = "button";
471+
deleteRecentButton.textContent = "Delete";
472+
deleteRecentButton.addEventListener("click", () => {
473+
this.deleteRecentSessionEntry(entry.hostContextId);
474+
});
475+
item.append(title, idLine, meta, reopenButton, copyIdButton, useInLibraryButton, deleteRecentButton);
470476
this.sessionHistoryListNode.appendChild(item);
471477
});
472478
this.renderSessionDiffInputs();
@@ -499,6 +505,29 @@ class WorkspaceV2SessionProducer {
499505
this.statusNode.textContent = `Session ID ready for Library actions: ${hostContextId.trim()}`;
500506
}
501507

508+
deleteRecentSessionEntry(hostContextId) {
509+
if (typeof hostContextId !== "string" || !hostContextId.trim()) {
510+
this.statusNode.textContent = "Delete Recent failed: session ID is missing.";
511+
return;
512+
}
513+
const sessionId = hostContextId.trim();
514+
const history = this.readSessionHistory();
515+
const exists = history.some((entry) => entry.hostContextId === sessionId);
516+
if (!exists) {
517+
this.statusNode.textContent = `Recent session '${sessionId}' was not found.`;
518+
return;
519+
}
520+
const nextHistory = history.filter((entry) => entry.hostContextId !== sessionId);
521+
this.writeSessionHistory(nextHistory);
522+
sessionStorage.removeItem(sessionId);
523+
if (this.currentHostContextId === sessionId) {
524+
this.currentHostContextId = "";
525+
this.setCurrentSessionPayload(null, "");
526+
}
527+
this.renderSessionHistory();
528+
this.statusNode.textContent = `Recent session '${sessionId}' deleted.`;
529+
}
530+
502531
resolveSessionPayloadFromContextId(contextId, fallbackPayload) {
503532
if (typeof contextId === "string" && contextId.trim()) {
504533
const raw = sessionStorage.getItem(contextId.trim());
@@ -1713,7 +1742,11 @@ class WorkspaceV2SessionProducer {
17131742
return;
17141743
}
17151744
if (!Object.prototype.hasOwnProperty.call(library, sessionName)) {
1716-
this.statusNode.textContent = `Session '${sessionName}' was not found in library.`;
1745+
const history = this.readSessionHistory();
1746+
const recentMatch = history.some((entry) => entry.hostContextId === sessionName);
1747+
this.statusNode.textContent = recentMatch
1748+
? "Session ID is not saved in Session Library. Use Delete on Recent Sessions to remove temporary sessions."
1749+
: `Session '${sessionName}' was not found in library.`;
17171750
return;
17181751
}
17191752
delete library[sessionName];

0 commit comments

Comments
 (0)