Skip to content

Commit 1469bbc

Browse files
author
DavidQ
committed
Refresh undo last merge enable state immediately after apply - PR 11.257
1 parent bb55e6b commit 1469bbc

3 files changed

Lines changed: 217 additions & 1 deletion

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# PR_11_257 — Refresh Undo Last Merge Enable State Immediately After Apply
2+
3+
## Summary
4+
Fixed merge post-apply state refresh ordering so `Undo Last Merge` enables immediately after successful apply/registration, without requiring a second run or page reload.
5+
6+
## Files Changed
7+
- `tools/workspace-v2/index.js`
8+
- `tests/runtime/V2UndoEnableStateRefresh.test.mjs`
9+
10+
## Implementation Details
11+
1. Immediate undo-state refresh after apply
12+
- In `applySelectedSessionMerge()`:
13+
- persist last merged id first: `writeLastMergedHostContextId(hostContextId)`
14+
- register merged recent entry
15+
- validate registration exists in history
16+
- force immediate UI refresh:
17+
- recent sessions
18+
- diff/merge inventories
19+
- merge enable-state
20+
- undo enable-state
21+
22+
2. Registration failure path
23+
- If merged recent registration check fails:
24+
- clear last merged id
25+
- keep undo disabled
26+
- status:
27+
- `Merge apply failed to register merged session in Recent Sessions. Undo remains disabled.`
28+
29+
3. Undo post-state
30+
- Existing undo flow still:
31+
- removes recent merged entry
32+
- removes payload from sessionStorage
33+
- clears last merged id
34+
- refreshes lists/selectors
35+
- disables undo immediately
36+
37+
## Validation Commands Run
38+
```powershell
39+
node --check tools/workspace-v2/index.js
40+
node --check tests/runtime/V2UndoEnableStateRefresh.test.mjs
41+
node --check tests/runtime/V2UndoLastMerge.test.mjs
42+
node tests/runtime/V2UndoEnableStateRefresh.test.mjs
43+
node tests/runtime/V2UndoLastMerge.test.mjs
44+
```
45+
46+
## Validation Results
47+
- `node --check tools/workspace-v2/index.js` -> PASS
48+
- `node --check tests/runtime/V2UndoEnableStateRefresh.test.mjs` -> PASS
49+
- `node --check tests/runtime/V2UndoLastMerge.test.mjs` -> PASS
50+
- `node tests/runtime/V2UndoEnableStateRefresh.test.mjs` -> PASS
51+
- output: `tmp/v2-undo-enable-state-refresh-results.json`
52+
- failures: `[]`
53+
- `node tests/runtime/V2UndoLastMerge.test.mjs` -> PASS
54+
- output: `tmp/v2-undo-last-merge-results.json`
55+
- failures: `[]`
56+
57+
## Verified
58+
- Undo disabled initially -> PASS
59+
- successful Apply Merge enables Undo immediately -> PASS
60+
- no second run required -> PASS
61+
- Undo removes merged session -> PASS
62+
- Undo disables immediately after use -> PASS
63+
- recent + selector state refreshes after apply and undo -> PASS
64+
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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-undo-enable-state-refresh-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 undoEnabled(lastMergedHostContextId, recent) {
26+
return Boolean(lastMergedHostContextId && recent.some((entry) => entry.hostContextId === lastMergedHostContextId));
27+
}
28+
29+
function applyRegistration(state, mergedId, mergedPayload) {
30+
const next = {
31+
...state,
32+
recent: [...state.recent],
33+
sessionStorageMap: { ...state.sessionStorageMap }
34+
};
35+
next.lastMergedHostContextId = mergedId;
36+
next.sessionStorageMap[mergedId] = JSON.stringify(mergedPayload);
37+
next.recent = next.recent.filter((entry) => entry.hostContextId !== mergedId);
38+
next.recent.unshift({ hostContextId: mergedId, tool: mergedPayload.toolId, payload: mergedPayload, timestamp: "2026-05-02T00:00:00.000Z" });
39+
const mergedRecentRegistered = next.recent.some((entry) => entry.hostContextId === mergedId);
40+
if (!mergedRecentRegistered) {
41+
next.lastMergedHostContextId = "";
42+
return { next, status: "Merge apply failed to register merged session in Recent Sessions. Undo remains disabled." };
43+
}
44+
return { next, status: "Session merge applied with no conflicts." };
45+
}
46+
47+
function undo(state) {
48+
const next = {
49+
...state,
50+
recent: [...state.recent],
51+
sessionStorageMap: { ...state.sessionStorageMap }
52+
};
53+
const id = next.lastMergedHostContextId;
54+
next.recent = next.recent.filter((entry) => entry.hostContextId !== id);
55+
delete next.sessionStorageMap[id];
56+
if (next.diffLeft === `history:${id}`) next.diffLeft = "";
57+
if (next.diffRight === `history:${id}`) next.diffRight = "";
58+
if (next.mergeLeft === `history:${id}`) next.mergeLeft = "";
59+
if (next.mergeRight === `history:${id}`) next.mergeRight = "";
60+
next.lastMergedHostContextId = "";
61+
return next;
62+
}
63+
64+
export function run() {
65+
const failures = [];
66+
const jsExists = fs.existsSync(jsPath);
67+
const js = jsExists ? fs.readFileSync(jsPath, "utf8") : "";
68+
const jsSyntax = checkSyntax(jsPath);
69+
const testSyntax = checkSyntax(path.join(repoRoot, "tests", "runtime", "V2UndoEnableStateRefresh.test.mjs"));
70+
71+
if (!jsExists) failures.push("Missing tools/workspace-v2/index.js.");
72+
if (!jsSyntax.ok) failures.push("tools/workspace-v2/index.js failed syntax check.");
73+
if (!testSyntax.ok) failures.push("tests/runtime/V2UndoEnableStateRefresh.test.mjs failed syntax check.");
74+
75+
const requiredTokens = [
76+
"this.writeLastMergedHostContextId(hostContextId);",
77+
"this.addRecentSessionEntry(hostContextId, this.pendingMergePreview.selectedToolId, appliedPayload);",
78+
"const mergedRecentRegistered = this.readSessionHistory().some((entry) => entry.hostContextId === hostContextId);",
79+
"this.updateUndoLastMergeState();",
80+
"Merge apply failed to register merged session in Recent Sessions. Undo remains disabled."
81+
];
82+
requiredTokens.forEach((token) => {
83+
if (!js.includes(token)) failures.push(`Missing immediate undo-refresh token: ${token}`);
84+
});
85+
86+
const base = {
87+
lastMergedHostContextId: "",
88+
recent: [{ hostContextId: "asset-browser-v2-regular", tool: "asset-browser-v2", payload: { version: "v2", toolId: "asset-browser-v2" } }],
89+
sessionStorageMap: { "asset-browser-v2-regular": "{\"version\":\"v2\",\"toolId\":\"asset-browser-v2\"}" },
90+
diffLeft: "",
91+
diffRight: "",
92+
mergeLeft: "",
93+
mergeRight: ""
94+
};
95+
96+
const initiallyDisabled = !undoEnabled(base.lastMergedHostContextId, base.recent);
97+
if (!initiallyDisabled) failures.push("Undo should be disabled initially.");
98+
99+
const mergedId = "asset-browser-v2-merged-1777777777777-abc123xy";
100+
const mergedPayload = { version: "v2", toolId: "asset-browser-v2", mergeResultMeta: { isMergedResult: true }, payloadJson: { merged: true } };
101+
const applied = applyRegistration(base, mergedId, mergedPayload);
102+
const enabledImmediately = undoEnabled(applied.next.lastMergedHostContextId, applied.next.recent);
103+
if (!enabledImmediately) failures.push("Undo should enable immediately after successful apply registration.");
104+
if (!applied.next.recent.length || applied.next.recent[0].hostContextId !== mergedId) {
105+
failures.push("Recent Sessions should refresh with merged session at top after apply.");
106+
}
107+
108+
const undone = undo(applied.next);
109+
const disabledAfterUndo = !undoEnabled(undone.lastMergedHostContextId, undone.recent);
110+
if (!disabledAfterUndo) failures.push("Undo should disable immediately after undo.");
111+
if (undone.recent.some((entry) => entry.hostContextId === mergedId)) {
112+
failures.push("Undo should remove merged session from recent.");
113+
}
114+
if (Object.prototype.hasOwnProperty.call(undone.sessionStorageMap, mergedId)) {
115+
failures.push("Undo should remove merged session payload from sessionStorage.");
116+
}
117+
118+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
119+
fs.writeFileSync(resultsPath, `${JSON.stringify({
120+
generatedAt: new Date().toISOString(),
121+
failures,
122+
checks: { jsExists, jsSyntax, testSyntax },
123+
scenarios: { initiallyDisabled, appliedStatus: applied.status, enabledImmediately, disabledAfterUndo }
124+
}, null, 2)}\n`, "utf8");
125+
126+
console.log(`v2 undo-enable-state-refresh results: ${resultsPath}`);
127+
assert.equal(failures.length, 0, `V2 undo-enable-state-refresh failures: ${failures.join(" | ")}`);
128+
return { failures };
129+
}
130+
131+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
132+
try {
133+
const summary = run();
134+
console.log(JSON.stringify(summary, null, 2));
135+
} catch (error) {
136+
console.error(error);
137+
process.exitCode = 1;
138+
}
139+
}
140+

tools/workspace-v2/index.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1591,8 +1591,20 @@ class WorkspaceV2SessionProducer {
15911591
this.setCurrentSessionPayload(appliedPayload, "merge-apply");
15921592
this.importJsonNode.value = JSON.stringify(appliedPayload, null, 2);
15931593
this.setLastMergedSessionResult(appliedPayload, this.pendingMergePreview.selectedToolId);
1594-
this.addRecentSessionEntry(hostContextId, this.pendingMergePreview.selectedToolId, appliedPayload);
15951594
this.writeLastMergedHostContextId(hostContextId);
1595+
this.addRecentSessionEntry(hostContextId, this.pendingMergePreview.selectedToolId, appliedPayload);
1596+
const mergedRecentRegistered = this.readSessionHistory().some((entry) => entry.hostContextId === hostContextId);
1597+
if (!mergedRecentRegistered) {
1598+
this.writeLastMergedHostContextId("");
1599+
this.updateUndoLastMergeState();
1600+
this.statusNode.textContent = "Merge apply failed to register merged session in Recent Sessions. Undo remains disabled.";
1601+
return;
1602+
}
1603+
this.renderSessionHistory();
1604+
this.renderSessionDiffInputs();
1605+
this.renderSessionMergeInputs();
1606+
this.updateMergeSelectionFeedbackAndState();
1607+
this.updateUndoLastMergeState();
15961608
this.recordMergeAuditEntry(this.pendingMergePreview);
15971609
this.renderDiagnosticsPanel();
15981610
this.pendingMergePreview = null;

0 commit comments

Comments
 (0)