Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "context-room",
"version": "0.6.15",
"version": "0.6.16",
"description": "Local-first documentation control room for AI-assisted projects.",
"type": "module",
"homepage": "https://www.npmjs.com/package/context-room",
Expand Down
40 changes: 34 additions & 6 deletions src/context_room.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32209,6 +32209,18 @@ async function openContextHubProject(projectId, options = {}, requestedGeneratio
if (review?.startupContext?.skillName) target.searchParams.set("startupSkill", review.startupContext.skillName);
target.searchParams.set("explorer", (isExplorerDrawerViewport() || isExplorerCollapsed()) ? "collapsed" : "expanded");
if (options.filePath && IS_GLOBAL_CONTEXT_ROOM && state.workspaceIdentityReady) {
if (targetProjectId === state.activeProjectLocationId) {
if (state.page === "file" && state.selected === options.filePath) return;
await selectFile(options.filePath, {
reviewMode: true,
preserveViewer: state.page === "file" && Boolean(state.selected),
});
if (state.page === "file" && state.selected === options.filePath) {
syncWorkspaceUrl({ push: true });
recordWorkspaceDiagnostic("ready", "project-file-in-place");
}
return;
}
window.history.pushState(window.history.state, "", target);
state.workspaceSyncedUrl = "";
await applyWorkspaceUrlState({ reason: "project-file", force: true });
Expand Down Expand Up @@ -36066,7 +36078,10 @@ function acceptContextRoomRoot(nextRoot) {
if (IS_GLOBAL_CONTEXT_ROOM) {
state.root = nextRoot;
state.lastAgentCommandId = readLastAgentCommandId();
recordWorkspaceDiagnostic("refreshing", "selected-project-root-changed");
recordWorkspaceDiagnostic(
document.body.classList.contains("app-booting") ? "refreshing" : "ready",
"selected-project-root-changed",
);
return;
}
handleContextRoomProjectChange({ reason: "server-root-changed" });
Expand Down Expand Up @@ -36683,6 +36698,7 @@ async function selectFile(path, options = {}) {
const profilingBoot = document.body.classList.contains("app-booting");
if (profilingBoot) state.bootMilestones.fileOpenStarted = Date.now() - state.bootStartedAt;
const previousSelected = state.selected;
const preserveViewer = Boolean(options.preserveViewer && state.page === "file" && previousSelected && previousSelected !== path && el("viewer")?.children.length);
state.selected = path;
state.selectedReadOnly = Boolean(state.files.find((item) => item.path === path)?.readOnly);
state.openingFilePath = path;
Expand Down Expand Up @@ -36721,18 +36737,23 @@ async function selectFile(path, options = {}) {
el("viewer").hidden = false;
el("editor").hidden = true;
el("editor").value = "";
updateHeader();
updateHistoryButtons();
updateActionBanner();
updatePreview();
if (preserveViewer) {
el("viewer").inert = true;
el("viewer").setAttribute("aria-busy", "true");
} else {
updateHeader();
updateHistoryButtons();
updateActionBanner();
updatePreview();
}
if (IS_GLOBAL_CONTEXT_ROOM) {
renderGlobalProjectExplorer();
void refreshExplorerRelatedForCurrentFile().catch((error) => setStatus(error.message));
}
if (options.revealInExplorer || !document.querySelector('[data-file-path="' + cssEscape(path) + '"]')) renderFiles();
else updateExplorerSelectedFile(previousSelected, path);
if (options.revealInExplorer && !isExplorerCollapsed()) scrollExplorerToPath(path);
renderViewer();
if (!preserveViewer) renderViewer();
setStatus("opening...");

const fileRequest = readFileForOpen(path, { force: options.forceReload });
Expand All @@ -36754,6 +36775,11 @@ async function selectFile(path, options = {}) {
state.fileLoadError = null;
el("editor").value = data.content || "";
state.fileContentReadyPath = path;
if (preserveViewer) {
updateHeader();
updateHistoryButtons();
updatePreview();
}
renderViewer();
restorePersistedViewState(options.restoreViewState);
setStatus("open · loading Git diff...");
Expand Down Expand Up @@ -42935,6 +42961,8 @@ function applySelectedTemplateToEditor(templateId) {
}

function renderViewer() {
el("viewer").inert = false;
el("viewer").removeAttribute("aria-busy");
const text = el("editor").value;
const diff = state.selectedDiff || { changed: false, additions: 0, deletions: 0, patch: "", available: false };
const isStartupFile = Boolean(state.selectedStartupContext);
Expand Down
8 changes: 6 additions & 2 deletions test/context_room.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7199,6 +7199,7 @@ test("browser refresh restores the last Context Room page", () => {
assert.match(html, /const pathFilters = rawPathFilters\.filter\(\(filter\) => state\.files\.some\(\(file\) => pathMatchesFilter\(file\.path, filter\)\)\);/);
assert.match(html, /el\("search"\)\.value = persisted\.searchText \|\| folderFilterSearchQuery\(state\.pathFilters\);/);
assert.match(html, /function acceptContextRoomRoot\(nextRoot\)[\s\S]*if \(!state\.root\) \{[\s\S]*state\.root = nextRoot;[\s\S]*if \(state\.root === nextRoot\) return;[\s\S]*if \(IS_GLOBAL_CONTEXT_ROOM\) \{[\s\S]*state\.root = nextRoot;[\s\S]*handleContextRoomProjectChange\(\{ reason: "server-root-changed" \}\);/);
assert.match(html, /function acceptContextRoomRoot\(nextRoot\)[\s\S]*document\.body\.classList\.contains\("app-booting"\) \? "refreshing" : "ready",[\s\S]*"selected-project-root-changed"/);
assert.match(html, /acceptContextRoomRoot\(data\.root\);/);
assert.match(html, /const hasDirectContextHubTarget = Boolean\(requestedReviewFile \|\| requestedHubCard \|\| requestedStartupOrder \|\| state\.sharedContext\?\.mode === "review"\);/);
assert.match(html, /if \(options\.initial && IS_GLOBAL_CONTEXT_ROOM\) \{[\s\S]*await state\.contextHubReadyPromise;[\s\S]*renderGlobalProjectExplorer\(\);/);
Expand Down Expand Up @@ -7323,6 +7324,9 @@ test("file opening renders loading and retry states instead of a blank document"
assert.match(html, /kind: "hosted-review",[\s\S]*renderViewer\(\);/);
assert.match(html, /state\.fileLoadError = \{ path, message: error\.message \|\| "Failed to open file\." \};/);
assert.match(html, /updateExplorerSelectedFile\(previousSelected, path\)/);
assert.match(html, /targetProjectId === state\.activeProjectLocationId[\s\S]*preserveViewer: state\.page === "file"[\s\S]*syncWorkspaceUrl\(\{ push: true \}\)[\s\S]*project-file-in-place/);
assert.match(html, /const preserveViewer = Boolean\(options\.preserveViewer[\s\S]*el\("viewer"\)\.inert = true;[\s\S]*if \(!preserveViewer\) renderViewer\(\);/);
assert.match(html, /function renderViewer\(\) \{\s*el\("viewer"\)\.inert = false;\s*el\("viewer"\)\.removeAttribute\("aria-busy"\);/);
assert.match(html, /function reconcileMissingSelectedFile\(\)/);
assert.match(html, /function clearMissingSelectedFile\(stalePath = state\.selected\)/);
assert.match(html, /function canReviewMissingFile\(path\)/);
Expand Down Expand Up @@ -7613,9 +7617,9 @@ test("file opening shows content before secondary dependencies and keeps actions
assert.match(selectFileFn, /const annotationsRequest = settleUiRequest\(loadAnnotationsForPath\(path\)\);/);
assert.match(selectFileFn, /const diffRequest = settleUiRequest\(readDiffForOpen\(path, \{ force: options\.forceReload \}\)\);/);
assert.match(selectFileFn, /const reviewBaseRequest = options\.reviewMode[\s\S]*settleUiRequest\(readSelectedReviewBase\(path\)\)/);
assert.match(selectFileFn, /const data = await fileRequest;[\s\S]*state\.fileContentReadyPath = path;\s*renderViewer\(\);[\s\S]*void annotationsRequest\.then/);
assert.match(selectFileFn, /const data = await fileRequest;[\s\S]*state\.fileContentReadyPath = path;[\s\S]*renderViewer\(\);[\s\S]*void annotationsRequest\.then/);
assert.doesNotMatch(selectFileFn, /await annotationsRequest/);
assert.match(selectFileFn, /state\.fileContentReadyPath = path;\s*renderViewer\(\);\s*restorePersistedViewState\(options\.restoreViewState\);/);
assert.match(selectFileFn, /state\.fileContentReadyPath = path;[\s\S]*renderViewer\(\);\s*restorePersistedViewState\(options\.restoreViewState\);/);
assert.match(selectFileFn, /setStatus\("open · loading Git diff\.\.\."\);/);
assert.match(selectFileFn, /const \[diffResult, reviewBaseResult\] = await Promise\.all\(\[diffRequest, reviewBaseRequest\]\);/);
assert.match(selectFileFn, /const \[diffResult, reviewBaseResult\] = await Promise\.all\(\[diffRequest, reviewBaseRequest\]\);[\s\S]*?finishOpen\(diffResult, reviewBaseResult\);/);
Expand Down
51 changes: 51 additions & 0 deletions test/e2e/layout-contract.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,57 @@ test("@layout rapid document switches settle Git review reads", async ({ page, b
}
});

test("@layout Explorer file navigation keeps the workbench shell mounted", async ({ page }) => {
const data = fixture();
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto(`${data.origin}/?hub=1&project=${encodeURIComponent(data.projects.atlas.id)}&view=hub&explorer=expanded`);
await waitForBoot(page);
await ensureExplorerOpen(page);

const docsFolder = page.locator('[data-global-project-folder="docs"]').first();
if (await docsFolder.getAttribute("aria-expanded") !== "true") await docsFolder.click();
await page.locator('[data-global-project-file="docs/README.md"]').first().click();
await waitForOpenedFile(page, "docs/README.md");

const before = await page.evaluate(() => {
window.__persistentExplorerTree = document.querySelector(".global-project-folder-tree");
window.__persistentWorkspaceDock = document.querySelector(".workspace-dock");
window.__workspaceDiagnosticReasons = [];
window.__workspaceDiagnosticObserver = new MutationObserver(() => {
const reason = JSON.parse(document.body.dataset.workspaceDiagnostics || "{}").lastNavigationReason || "";
if (reason) window.__workspaceDiagnosticReasons.push(reason);
});
window.__workspaceDiagnosticObserver.observe(document.body, {
attributes: true,
attributeFilter: ["data-workspace-diagnostics"],
});
return {
navigationGeneration: state.workspaceNavigationGeneration,
bootCount: JSON.parse(document.body.dataset.workspaceDiagnostics || "{}").bootCount || 0,
};
});

await page.locator('[data-global-project-file="docs/operations.md"]').first().click();
await waitForOpenedFile(page, "docs/operations.md");

await expect.poll(() => page.evaluate((expected) => ({
explorerPreserved: window.__persistentExplorerTree === document.querySelector(".global-project-folder-tree"),
dockPreserved: window.__persistentWorkspaceDock === document.querySelector(".workspace-dock"),
navigationGeneration: state.workspaceNavigationGeneration,
bootCount: JSON.parse(document.body.dataset.workspaceDiagnostics || "{}").bootCount || 0,
inPlaceDiagnosticObserved: window.__workspaceDiagnosticReasons.includes("project-file-in-place"),
viewerBusy: document.querySelector("#viewer")?.getAttribute("aria-busy") || "",
}), before)).toEqual({
explorerPreserved: true,
dockPreserved: true,
navigationGeneration: before.navigationGeneration,
bootCount: before.bootCount,
inPlaceDiagnosticObserved: true,
viewerBusy: "",
});
await page.evaluate(() => window.__workspaceDiagnosticObserver?.disconnect());
});

test("@layout a fresh runtime subscription ignores replay already reflected by boot", async ({ page, browserName }) => {
test.skip(browserName !== "webkit", "WebKit's event dispatch timing deterministically exercises the fresh-subscription replay boundary.");
const data = fixture();
Expand Down
Loading