Skip to content

Commit 245b6a4

Browse files
author
DavidQ
committed
Register merged session results in recent sessions for immediate reuse - PR 11.254
1 parent 70da382 commit 245b6a4

6 files changed

Lines changed: 573 additions & 7 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# PR_11_254 — Auto-Register Merged Result In Recent Sessions
2+
3+
## Summary
4+
Updated Workspace V2 merge post-apply flow so successful merged results are immediately registered as runtime sessions and added to Recent Sessions, without auto-saving to Session Library.
5+
6+
## Files Changed
7+
- `tools/workspace-v2/index.js`
8+
- `tests/runtime/V2MergedRecentSessionRegistration.test.mjs`
9+
10+
## Implementation Details
11+
1. Merged hostContextId generation
12+
- Added:
13+
- `createMergedHostContextId(toolId)`
14+
- Format:
15+
- `<toolId>-merged-<timestamp>-<shortId>`
16+
17+
2. Post-apply merged registration
18+
- In `applySelectedSessionMerge()` after successful apply verification:
19+
- create merged hostContextId using merged format
20+
- persist merged payload to `sessionStorage`
21+
- merged payload includes:
22+
- `version: "v2"`
23+
- `toolId`
24+
- `mergeResultMeta` with:
25+
- `isMergedResult: true`
26+
- source/target context ids
27+
- merge timestamp
28+
- register into Recent Sessions via `addRecentSessionEntry(...)`
29+
30+
3. Recent Sessions label clarity
31+
- Recent row title now shows:
32+
- `<toolId> (merged)` for merged-result entries
33+
- using `payload.mergeResultMeta.isMergedResult === true`
34+
35+
4. Behavior guarantees
36+
- No auto-save into Session Library.
37+
- Source sessions are not overwritten.
38+
- Existing Recent Session actions remain unchanged and available:
39+
- Reopen
40+
- Copy ID
41+
- Use in Library
42+
- Delete
43+
- Diff/Merge selectors refresh through existing recent/session inventory refresh flow.
44+
45+
## Validation Commands Run
46+
```powershell
47+
node --check tools/workspace-v2/index.js
48+
node --check tests/runtime/V2MergedRecentSessionRegistration.test.mjs
49+
node tests/runtime/V2MergedRecentSessionRegistration.test.mjs
50+
```
51+
52+
## Validation Results
53+
- `node --check tools/workspace-v2/index.js` -> PASS
54+
- `node --check tests/runtime/V2MergedRecentSessionRegistration.test.mjs` -> PASS
55+
- `node tests/runtime/V2MergedRecentSessionRegistration.test.mjs` -> PASS
56+
- output: `tmp/v2-merged-recent-session-registration-results.json`
57+
- failures: `[]`
58+
59+
## Verified
60+
- Apply Merge creates new merged recent entry -> PASS
61+
- merged session appears at top of Recent Sessions -> PASS
62+
- merged session payload stored in sessionStorage with merged metadata -> PASS
63+
- merged session is reopenable -> PASS
64+
- merged session is available for Diff/Merge selection inventory -> PASS
65+
- deleting merged recent session removes it correctly -> PASS
66+
- Session Library remains unchanged unless explicitly saved -> PASS
67+
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# PR_11_255 — Undo Last Merge (Recent Only)
2+
3+
## Summary
4+
Added `Undo Last Merge` for Workspace V2 merge runtime flow. It removes only the most recent merged runtime session from Recent Sessions and `sessionStorage`, without changing Session Library entries.
5+
6+
## Files Changed
7+
- `tools/workspace-v2/index.html`
8+
- `tools/workspace-v2/index.js`
9+
- `tests/runtime/V2UndoLastMerge.test.mjs`
10+
11+
## Implementation Details
12+
1. Last merged tracking
13+
- Added key:
14+
- `v2-last-merged` (sessionStorage)
15+
- Added state:
16+
- `lastMergedHostContextId`
17+
- On startup:
18+
- reads `lastMergedHostContextId` from `v2-last-merged`
19+
- After successful Apply Merge:
20+
- stores merged host context id via `writeLastMergedHostContextId(hostContextId)`
21+
22+
2. Merge panel UI
23+
- Added button under merge panel:
24+
- `Undo Last Merge` (`#workspaceV2UndoLastMergeButton`)
25+
26+
3. Enable-state
27+
- Added `updateUndoLastMergeState()`:
28+
- enabled only when `lastMergedHostContextId` exists and is present in Recent Sessions
29+
- disabled otherwise
30+
- Called during recent-session rendering and reset paths.
31+
32+
4. Undo behavior
33+
- Added `undoLastMerge()`:
34+
- if no tracked merge exists:
35+
- status: `No recent merge to undo.`
36+
- if tracked merge exists:
37+
- removes matching entry from recent history
38+
- removes its `sessionStorage` payload
39+
- clears `lastMergedHostContextId` + `v2-last-merged`
40+
- clears Diff/Merge selections if they referenced that session
41+
- refreshes recent list and selector state
42+
- status: `Last merged session removed.`
43+
44+
5. Scope guarantees
45+
- Does not remove Session Library entries.
46+
- Does not undo older merges; only tracked last merge.
47+
- Does not modify source sessions.
48+
49+
## Validation Commands Run
50+
```powershell
51+
node --check tools/workspace-v2/index.js
52+
node --check tests/runtime/V2UndoLastMerge.test.mjs
53+
node tests/runtime/V2UndoLastMerge.test.mjs
54+
```
55+
56+
## Validation Results
57+
- `node --check tools/workspace-v2/index.js` -> PASS
58+
- `node --check tests/runtime/V2UndoLastMerge.test.mjs` -> PASS
59+
- `node tests/runtime/V2UndoLastMerge.test.mjs` -> PASS
60+
- output: `tmp/v2-undo-last-merge-results.json`
61+
- failures: `[]`
62+
63+
## Verified
64+
- merge creates trackable recent merged session context -> PASS
65+
- Undo Last Merge removes that recent merged session -> PASS
66+
- Undo clears A/B selection references to that merged session -> PASS
67+
- Undo disables itself after execution -> PASS
68+
- Undo does not affect Session Library -> PASS
69+
- Undo shows `No recent merge to undo.` when nothing is available -> PASS
70+
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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 jsPath = path.join(repoRoot, "tools", "workspace-v2", "index.js");
11+
const resultsPath = path.join(repoRoot, "tmp", "v2-merged-recent-session-registration-results.json");
12+
13+
function checkSyntax(filePath) {
14+
try {
15+
execFileSync(process.execPath, ["--check", filePath], {
16+
cwd: repoRoot,
17+
stdio: ["ignore", "pipe", "pipe"]
18+
});
19+
return { ok: true, error: "" };
20+
} catch (error) {
21+
return { ok: false, error: (error?.stderr || error?.stdout || error?.message || "").toString().trim() };
22+
}
23+
}
24+
25+
function createMergedHostContextId(toolId, timestamp, shortId) {
26+
return `${toolId}-merged-${timestamp}-${shortId}`;
27+
}
28+
29+
function registerMergedResult(state, toolId, mergedPayload, timestamp, shortId) {
30+
const next = {
31+
...state,
32+
sessionStorage: { ...state.sessionStorage },
33+
recent: [...state.recent],
34+
library: { ...state.library }
35+
};
36+
const hostContextId = createMergedHostContextId(toolId, timestamp, shortId);
37+
const mergedResultPayload = {
38+
...mergedPayload,
39+
version: "v2",
40+
toolId,
41+
mergeResultMeta: {
42+
isMergedResult: true,
43+
sourceSessionContextId: "source",
44+
targetSessionContextId: "target",
45+
mergedAt: "2026-05-02T00:00:00.000Z"
46+
}
47+
};
48+
next.sessionStorage[hostContextId] = JSON.stringify(mergedResultPayload);
49+
next.recent = next.recent.filter((entry) => entry.hostContextId !== hostContextId);
50+
next.recent.unshift({
51+
hostContextId,
52+
tool: toolId,
53+
timestamp: "2026-05-02T00:00:00.000Z",
54+
payload: mergedResultPayload
55+
});
56+
return { next, hostContextId, mergedResultPayload };
57+
}
58+
59+
export function run() {
60+
const failures = [];
61+
const jsExists = fs.existsSync(jsPath);
62+
const js = jsExists ? fs.readFileSync(jsPath, "utf8") : "";
63+
const jsSyntax = checkSyntax(jsPath);
64+
const testSyntax = checkSyntax(path.join(repoRoot, "tests", "runtime", "V2MergedRecentSessionRegistration.test.mjs"));
65+
66+
if (!jsExists) failures.push("Missing tools/workspace-v2/index.js.");
67+
if (!jsSyntax.ok) failures.push("tools/workspace-v2/index.js failed syntax check.");
68+
if (!testSyntax.ok) failures.push("tests/runtime/V2MergedRecentSessionRegistration.test.mjs failed syntax check.");
69+
70+
const requiredTokens = [
71+
"createMergedHostContextId(toolId)",
72+
"-merged-",
73+
"mergeResultMeta",
74+
"isMergedResult: true",
75+
"this.addRecentSessionEntry(hostContextId, this.pendingMergePreview.selectedToolId, appliedPayload);",
76+
"const titleToolLabel = isMergedResult ? `${entry.tool} (merged)` : entry.tool;"
77+
];
78+
requiredTokens.forEach((token) => {
79+
if (!js.includes(token)) failures.push(`Missing merged recent-session token: ${token}`);
80+
});
81+
82+
const baseState = {
83+
sessionStorage: {},
84+
recent: [
85+
{ hostContextId: "asset-browser-v2-123", tool: "asset-browser-v2", timestamp: "2026-05-01T00:00:00.000Z", payload: { version: "v2", toolId: "asset-browser-v2" } }
86+
],
87+
library: {}
88+
};
89+
const mergedPayload = { payloadJson: { combined: true } };
90+
const { next, hostContextId, mergedResultPayload } = registerMergedResult(baseState, "asset-browser-v2", mergedPayload, 1777777777777, "abc123xy");
91+
92+
if (!hostContextId.startsWith("asset-browser-v2-merged-1777777777777-")) {
93+
failures.push("Merged hostContextId format is incorrect.");
94+
}
95+
if (!Object.prototype.hasOwnProperty.call(next.sessionStorage, hostContextId)) {
96+
failures.push("Merged result was not registered in sessionStorage.");
97+
}
98+
if (!next.recent.length || next.recent[0].hostContextId !== hostContextId) {
99+
failures.push("Merged result should appear at the top of Recent Sessions.");
100+
}
101+
if (!next.recent[0].payload.mergeResultMeta || next.recent[0].payload.mergeResultMeta.isMergedResult !== true) {
102+
failures.push("Merged recent payload is missing merged-result metadata.");
103+
}
104+
if (!next.recent[0].payload.version || next.recent[0].payload.version !== "v2") {
105+
failures.push("Merged recent payload must include version v2.");
106+
}
107+
if (!next.recent[0].payload.toolId || next.recent[0].payload.toolId !== "asset-browser-v2") {
108+
failures.push("Merged recent payload must include toolId.");
109+
}
110+
111+
const reopenedPayload = JSON.parse(next.sessionStorage[hostContextId]);
112+
if (JSON.stringify(reopenedPayload) !== JSON.stringify(mergedResultPayload)) {
113+
failures.push("Merged session reopen payload mismatch.");
114+
}
115+
116+
const diffMergeSelectable = next.recent.some((entry) => entry.hostContextId === hostContextId);
117+
if (!diffMergeSelectable) {
118+
failures.push("Merged recent session is not selectable for Diff/Merge.");
119+
}
120+
121+
const afterDelete = { ...next, recent: next.recent.filter((entry) => entry.hostContextId !== hostContextId), sessionStorage: { ...next.sessionStorage } };
122+
delete afterDelete.sessionStorage[hostContextId];
123+
if (afterDelete.recent.some((entry) => entry.hostContextId === hostContextId)) {
124+
failures.push("Deleting merged recent session should remove recent entry.");
125+
}
126+
if (Object.prototype.hasOwnProperty.call(afterDelete.sessionStorage, hostContextId)) {
127+
failures.push("Deleting merged recent session should remove sessionStorage payload.");
128+
}
129+
130+
if (Object.keys(next.library).length !== 0) {
131+
failures.push("Session Library should remain unchanged unless explicitly saved.");
132+
}
133+
134+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
135+
fs.writeFileSync(resultsPath, `${JSON.stringify({
136+
generatedAt: new Date().toISOString(),
137+
failures,
138+
checks: { jsExists, jsSyntax, testSyntax },
139+
scenarios: { hostContextId, recentTop: next.recent[0], librarySize: Object.keys(next.library).length }
140+
}, null, 2)}\n`, "utf8");
141+
142+
console.log(`v2 merged-recent-session-registration results: ${resultsPath}`);
143+
assert.equal(failures.length, 0, `V2 merged-recent-session-registration failures: ${failures.join(" | ")}`);
144+
return { failures };
145+
}
146+
147+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
148+
try {
149+
const summary = run();
150+
console.log(JSON.stringify(summary, null, 2));
151+
} catch (error) {
152+
console.error(error);
153+
process.exitCode = 1;
154+
}
155+
}
156+

0 commit comments

Comments
 (0)