Skip to content

Commit 66c26cf

Browse files
author
DavidQ
committed
Expose session IDs and enable copy/use actions for library operations - PR 11.241
1 parent 2c08b15 commit 66c26cf

4 files changed

Lines changed: 206 additions & 3 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# PR_11_241 — Session ID Usability (Recent Sessions + Library)
2+
3+
## Files Changed
4+
- `tools/workspace-v2/index.html`
5+
- `tools/workspace-v2/index.js`
6+
- `tests/runtime/V2SessionIdUsability.test.mjs`
7+
8+
## Implementation Summary
9+
- Updated Session Library label:
10+
- from `Session Name`
11+
- to `Session ID (for Save / Load / Delete)`
12+
- Added helper text below Session ID input:
13+
- `Use a session ID from Recent Sessions or saved library.`
14+
- Recent Sessions entries now expose full session ID via inline `code` display:
15+
- `Session ID: <full hostContextId>`
16+
- Added per-entry actions in Recent Sessions:
17+
- `Copy ID` (copies exact `hostContextId` to clipboard)
18+
- `Use in Library` (fills Session ID input with exact `hostContextId`)
19+
- Delete behavior remains library-only:
20+
- still deletes only from Session Library
21+
- does not delete Recent Sessions/history
22+
23+
## Scope Confirmation
24+
- No schema/sample/game changes.
25+
- No storage contract changes.
26+
- No auto-save/implicit naming changes.
27+
- No merge/diff computation changes.
28+
29+
## Validation Commands
30+
- `node --check tools/workspace-v2/index.js`
31+
- `node --check tests/runtime/V2SessionIdUsability.test.mjs`
32+
- `node tests/runtime/V2SessionIdUsability.test.mjs`
33+
- `node tests/runtime/V2DiffMergeButtonState.test.mjs`
34+
35+
## Validation Results
36+
- `node --check tools/workspace-v2/index.js` → PASS
37+
- `node --check tests/runtime/V2SessionIdUsability.test.mjs` → PASS
38+
- `node tests/runtime/V2SessionIdUsability.test.mjs` → PASS
39+
- `node tests/runtime/V2DiffMergeButtonState.test.mjs` → PASS
40+
41+
Runtime artifacts:
42+
- `tmp/v2-session-id-usability-results.json`
43+
- `tmp/v2-diff-merge-button-state-results.json`
44+
45+
## Required Behavior Verification
46+
- Copy ID copies exact session ID value (hostContextId): PASS
47+
- Use in Library populates Session ID input exactly: PASS
48+
- Delete remains library-only (no Recent Sessions mutation): PASS
49+
- Merge/Diff behavior remains unchanged in existing state-wiring test: PASS
50+
51+
## Full Smoke Decision
52+
- Full samples smoke not run.
53+
- Reason: scoped Workspace V2 UI usability update with targeted executable validation.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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-session-id-usability-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+
export function run() {
31+
const failures = [];
32+
const htmlExists = fs.existsSync(htmlPath);
33+
const jsExists = fs.existsSync(jsPath);
34+
const html = htmlExists ? readText(htmlPath) : "";
35+
const js = jsExists ? readText(jsPath) : "";
36+
const syntax = checkSyntax(jsPath);
37+
38+
const hasSessionIdLabel = html.includes("Session ID (for Save / Load / Delete)");
39+
const hasHelperText = html.includes("Use a session ID from Recent Sessions or saved library.");
40+
const hasCopyButtonText = js.includes("copyIdButton.textContent = \"Copy ID\";");
41+
const hasUseButtonText = js.includes("useInLibraryButton.textContent = \"Use in Library\";");
42+
const hasCopyMethod = js.includes("async copySessionIdToClipboard(hostContextId)");
43+
const hasClipboardWrite = js.includes("await navigator.clipboard.writeText(hostContextId.trim());");
44+
const hasUseMethod = js.includes("useSessionIdInLibraryInput(hostContextId)");
45+
const hasUsePopulate = js.includes("this.sessionNameNode.value = hostContextId.trim();");
46+
47+
const deleteMethodStart = js.indexOf("deleteNamedSession() {");
48+
const createSessionStart = js.indexOf("createSessionAndLaunch() {", deleteMethodStart);
49+
const deleteMethod = deleteMethodStart >= 0 && createSessionStart > deleteMethodStart
50+
? js.slice(deleteMethodStart, createSessionStart)
51+
: "";
52+
const deleteTouchesLibrary = deleteMethod.includes("const library = this.readSessionLibrary();") &&
53+
deleteMethod.includes("delete library[sessionName];") &&
54+
deleteMethod.includes("this.writeSessionLibrary(library);");
55+
const deleteTouchesHistory = deleteMethod.includes("readSessionHistory(") ||
56+
deleteMethod.includes("writeSessionHistory(");
57+
58+
if (!htmlExists) failures.push("Missing tools/workspace-v2/index.html.");
59+
if (!jsExists) failures.push("Missing tools/workspace-v2/index.js.");
60+
if (!syntax.ok) failures.push("tools/workspace-v2/index.js failed syntax check.");
61+
if (!hasSessionIdLabel) failures.push("Session Name label was not updated to Session ID label.");
62+
if (!hasHelperText) failures.push("Session ID helper text is missing.");
63+
if (!hasCopyButtonText) failures.push("Copy ID button is missing from Recent Sessions.");
64+
if (!hasUseButtonText) failures.push("Use in Library button is missing from Recent Sessions.");
65+
if (!hasCopyMethod || !hasClipboardWrite) failures.push("Copy ID behavior does not write exact hostContextId to clipboard.");
66+
if (!hasUseMethod || !hasUsePopulate) failures.push("Use in Library behavior does not populate Session ID input.");
67+
if (!deleteTouchesLibrary) failures.push("Delete behavior is not operating on Session Library as expected.");
68+
if (deleteTouchesHistory) failures.push("Delete behavior should not affect Recent Sessions history.");
69+
70+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
71+
fs.writeFileSync(resultsPath, `${JSON.stringify({
72+
generatedAt: new Date().toISOString(),
73+
failures,
74+
checks: {
75+
htmlExists,
76+
jsExists,
77+
syntax,
78+
hasSessionIdLabel,
79+
hasHelperText,
80+
hasCopyButtonText,
81+
hasUseButtonText,
82+
hasCopyMethod,
83+
hasClipboardWrite,
84+
hasUseMethod,
85+
hasUsePopulate,
86+
deleteTouchesLibrary,
87+
deleteTouchesHistory
88+
}
89+
}, null, 2)}\n`, "utf8");
90+
91+
console.log(`v2 session-id usability results: ${resultsPath}`);
92+
assert.equal(failures.length, 0, `V2 session-id usability failures: ${failures.join(" | ")}`);
93+
return { failures };
94+
}
95+
96+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
97+
try {
98+
const summary = run();
99+
console.log(JSON.stringify(summary, null, 2));
100+
} catch (error) {
101+
console.error(error);
102+
process.exitCode = 1;
103+
}
104+
}

tools/workspace-v2/index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ <h2>Share Session Link</h2>
5757

5858
<section class="hub-panel">
5959
<h2>Session Library</h2>
60-
<label for="workspaceV2SessionName">Session Name</label>
61-
<input id="workspaceV2SessionName" type="text" placeholder="My Session Name" />
60+
<label for="workspaceV2SessionName">Session ID (for Save / Load / Delete)</label>
61+
<input id="workspaceV2SessionName" type="text" placeholder="session-id" />
62+
<p>Use a session ID from Recent Sessions or saved library.</p>
6263
<div>
6364
<button id="workspaceV2SaveSessionButton" type="button">Save Session</button>
6465
<button id="workspaceV2OverwriteSessionButton" type="button">Overwrite Session</button>

tools/workspace-v2/index.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,22 +438,67 @@ class WorkspaceV2SessionProducer {
438438
history.forEach((entry) => {
439439
const item = document.createElement("li");
440440
const title = document.createElement("strong");
441+
const idLine = document.createElement("div");
442+
const idLabel = document.createElement("span");
443+
const idCode = document.createElement("code");
441444
const meta = document.createElement("div");
442445
const reopenButton = document.createElement("button");
446+
const copyIdButton = document.createElement("button");
447+
const useInLibraryButton = document.createElement("button");
443448
title.textContent = `${entry.tool} (${entry.hostContextId})`;
449+
idLabel.textContent = "Session ID: ";
450+
idCode.textContent = entry.hostContextId;
451+
idCode.title = entry.hostContextId;
452+
idLine.append(idLabel, idCode);
444453
meta.textContent = entry.timestamp;
445454
reopenButton.type = "button";
446455
reopenButton.textContent = "Reopen";
447456
reopenButton.addEventListener("click", () => {
448457
this.reopenSessionHistoryEntry(entry.hostContextId);
449458
});
450-
item.append(title, meta, reopenButton);
459+
copyIdButton.type = "button";
460+
copyIdButton.textContent = "Copy ID";
461+
copyIdButton.addEventListener("click", () => {
462+
this.copySessionIdToClipboard(entry.hostContextId);
463+
});
464+
useInLibraryButton.type = "button";
465+
useInLibraryButton.textContent = "Use in Library";
466+
useInLibraryButton.addEventListener("click", () => {
467+
this.useSessionIdInLibraryInput(entry.hostContextId);
468+
});
469+
item.append(title, idLine, meta, reopenButton, copyIdButton, useInLibraryButton);
451470
this.sessionHistoryListNode.appendChild(item);
452471
});
453472
this.renderSessionDiffInputs();
454473
this.renderSessionMergeInputs();
455474
}
456475

476+
async copySessionIdToClipboard(hostContextId) {
477+
if (typeof hostContextId !== "string" || !hostContextId.trim()) {
478+
this.statusNode.textContent = "Copy ID failed: session ID is missing.";
479+
return;
480+
}
481+
try {
482+
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
483+
this.statusNode.textContent = "Copy ID is unavailable in this browser context.";
484+
return;
485+
}
486+
await navigator.clipboard.writeText(hostContextId.trim());
487+
this.statusNode.textContent = `Session ID copied: ${hostContextId.trim()}`;
488+
} catch (error) {
489+
this.statusNode.textContent = `Copy ID failed: ${error instanceof Error ? error.message : "unknown error"}`;
490+
}
491+
}
492+
493+
useSessionIdInLibraryInput(hostContextId) {
494+
if (typeof hostContextId !== "string" || !hostContextId.trim()) {
495+
this.statusNode.textContent = "Use in Library failed: session ID is missing.";
496+
return;
497+
}
498+
this.sessionNameNode.value = hostContextId.trim();
499+
this.statusNode.textContent = `Session ID ready for Library actions: ${hostContextId.trim()}`;
500+
}
501+
457502
resolveSessionPayloadFromContextId(contextId, fallbackPayload) {
458503
if (typeof contextId === "string" && contextId.trim()) {
459504
const raw = sessionStorage.getItem(contextId.trim());

0 commit comments

Comments
 (0)