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.13",
"version": "0.6.14",
"description": "Local-first documentation control room for AI-assisted projects.",
"type": "module",
"homepage": "https://www.npmjs.com/package/context-room",
Expand Down
12 changes: 7 additions & 5 deletions src/context_room.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27273,7 +27273,7 @@ export function renderAppHtml({ codexPromptMutationNonce = "", ownerMutationNonc
<main>
<div class="workspace-chrome">
<div class="workspace-dock" role="toolbar" aria-label="Workspace navigation">
<button id="proposalDockAccept" class="dock-button primary" type="button" hidden>Put on main</button>
<button id="proposalDockAccept" class="dock-button primary" type="button" hidden>Accept proposal</button>
<button id="brandHome" class="context-room-brand" type="button" title="Home" aria-label="Home"><span class="context-room-brand-mark" aria-hidden="true"></span><strong>Context Room</strong></button>
<button id="settingsButton" class="dock-button workspace-switch" type="button" title="Open settings">Settings</button>
<button id="proposalDockBack" class="dock-button proposal-dock-back" type="button" hidden>← Proposal</button>
Expand Down Expand Up @@ -32318,13 +32318,15 @@ function renderProposalDockControls() {
const actionable = shared?.mode === "review" && !preview && !terminal;
const preparing = Boolean(preview && ["opening", "loading", "ready"].includes(String(state.proposalOpenState?.phase || "opening")));
const noAcceptedChanges = actionable && shared?.acceptedChangesRemain === false;
acceptButton.hidden = !actionable || queueCount > 0 || Boolean(state.proposalAuthorityStatus);
acceptButton.disabled = Boolean(state.proposalActionBusy || queueCount > 0 || noAcceptedChanges);
acceptButton.hidden = (!actionable && !preparing) || Boolean(state.proposalAuthorityStatus);
acceptButton.disabled = Boolean(state.proposalActionBusy || !actionable || queueCount > 0 || noAcceptedChanges);
if (!acceptButton.hidden) setExplorerEdgePeek(false);
acceptButton.title = delivered
acceptButton.title = preparing
? "Available after Context Room verifies the exact proposal revision"
: delivered
? "This exact proposal revision is already in " + (delivered.defaultBranch || review.defaultBranch || "main")
: queueCount
? queueCount + " file(s) still need a human decision"
? queueCount + " file" + (queueCount === 1 ? " still needs" : "s still need") + " a human decision before this proposal can be accepted"
: noAcceptedChanges
? "No accepted changes remain. Reject the proposal to close it."
: "Put the exact reviewed result on " + (review.defaultBranch || "main");
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/hosted-profiles.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1232,9 +1232,9 @@ test.describe("hosted runtime profiles", () => {
));
};
await assertCanonicalReviewUrl();
await expect(page.getByRole("button", { name: "Put on main", exact: true })).toBeEnabled();
await expect(page.getByRole("button", { name: "Accept proposal", exact: true })).toBeEnabled();

await page.getByRole("button", { name: "Put on main", exact: true }).click();
await page.getByRole("button", { name: "Accept proposal", exact: true }).click();
let confirmation = page.getByRole("dialog", { name: /Put this proposal on main/ });
await expect(confirmation).toBeVisible();
await confirmation.getByRole("checkbox").check();
Expand Down
8 changes: 4 additions & 4 deletions test/e2e/layout-contract.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,7 @@ test("@layout themes, zoom, files, graph, proposals, and dialogs preserve geomet
await makeProposalTerminalReady(page);
const terminalAction = page.locator("#proposalDockAccept");
await expect(terminalAction).toBeVisible();
await expect(terminalAction).toHaveAccessibleName("Put on main");
await expect(terminalAction).toHaveAccessibleName("Accept proposal");
await expect(terminalAction).toBeInViewport({ ratio: 1 });
await audit(page, testInfo, `proposal-terminal-${width}`);

Expand All @@ -1060,12 +1060,12 @@ test("@layout themes, zoom, files, graph, proposals, and dialogs preserve geomet
await expect(terminalControl).toBeVisible();
if (browserZoom) {
const terminalLabel = () => terminalControl.evaluate((node) => node.getAttribute("aria-label") || node.textContent?.trim() || "");
expect(await terminalLabel()).toBe("Put on main");
expect(await terminalLabel()).toBe("Accept proposal");
await terminalControl.evaluate((node) => node.focus());
expect(await page.evaluate(() => document.activeElement?.id)).toBe("proposalDockAccept");
expect(await terminalLabel()).toBe("Put on main");
expect(await terminalLabel()).toBe("Accept proposal");
} else {
await expect(terminalControl).toHaveAccessibleName("Put on main");
await expect(terminalControl).toHaveAccessibleName("Accept proposal");
await terminalControl.focus();
await expect(terminalControl).toBeFocused();
}
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/proposal-acceptance-real-server.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ test("@smoke verified real-server acceptance removes the proposal from active Hu
response.request().method() === "POST"
&& new URL(response.url()).pathname.endsWith("/api/shared-context/accept")
));
await page.getByRole("button", { name: "Put on main", exact: true }).click();
await page.getByRole("button", { name: "Accept proposal", exact: true }).click();
const dialog = page.getByRole("dialog", { name: /Put this proposal on main\?/ });
await expect(dialog).toBeVisible();
await dialog.getByRole("checkbox").check();
Expand Down
56 changes: 40 additions & 16 deletions test/e2e/proposal-review-selection.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1002,7 +1002,11 @@ test("@layout proposal verification keeps the final action and header geometry s
setStatus("verifying exact proposal revision...");
});

const accept = page.getByRole("button", { name: "Accept proposal" });
const reject = page.getByRole("button", { name: "Reject proposal" });
await expect(accept).toBeVisible();
await expect(accept).toBeDisabled();
await expect(accept).toHaveAttribute("title", "Available after Context Room verifies the exact proposal revision");
await expect(reject).toBeVisible();
await expect(reject).toBeDisabled();
await expect(page.locator("#proposalReviewProgress")).toContainText("Verifying");
Expand All @@ -1013,6 +1017,7 @@ test("@layout proposal verification keeps the final action and header geometry s
return box ? { x: box.x, y: box.y, width: box.width, height: box.height } : null;
};
return {
accept: rect("proposalDockAccept"),
brand: rect("brandHome"),
reject: rect("proposalDockReject"),
title: rect("proposalReviewTitle"),
Expand Down Expand Up @@ -1053,6 +1058,9 @@ test("@layout proposal verification keeps the final action and header geometry s
setStatus("proposal ready");
});

await expect(accept).toBeVisible();
await expect(accept).toBeDisabled();
await expect(accept).toHaveAttribute("title", "2 files still need a human decision before this proposal can be accepted");
await expect(reject).toBeVisible();
await expect(reject).toBeEnabled();
const readyGeometry = await page.evaluate(() => {
Expand All @@ -1061,6 +1069,7 @@ test("@layout proposal verification keeps the final action and header geometry s
return box ? { x: box.x, y: box.y, width: box.width, height: box.height } : null;
};
return {
accept: rect("proposalDockAccept"),
brand: rect("brandHome"),
reject: rect("proposalDockReject"),
title: rect("proposalReviewTitle"),
Expand Down Expand Up @@ -1750,6 +1759,18 @@ test("@smoke the last individual file decision never auto-accepts the proposal",
showProposalReview();
});

const acceptProposal = page.getByRole("button", { name: "Accept proposal", exact: true });
await expect(acceptProposal).toBeVisible();
await expect(acceptProposal).toBeDisabled();
await expect(acceptProposal).toHaveAttribute(
"title",
"1 file still needs a human decision before this proposal can be accepted",
);
const acceptProposalRect = await acceptProposal.evaluate((button) => {
const rect = button.getBoundingClientRect();
return { left: Math.round(rect.left), width: Math.round(rect.width) };
});

const fileRow = page.getByRole("button", { name: /Open README\.md/ });
if (testInfo.project.name === "chromium-mobile") {
const pointer = await fileRow.evaluate((row) => {
Expand Down Expand Up @@ -1788,9 +1809,12 @@ test("@smoke the last individual file decision never auto-accepts the proposal",
decision: "accept",
files: ["README.md"],
});
const putOnMain = page.getByRole("button", { name: "Put on main", exact: true });
await expect(putOnMain).toBeVisible();
await expect(putOnMain).toBeEnabled();
await expect(acceptProposal).toBeVisible();
await expect(acceptProposal).toBeEnabled();
expect(await acceptProposal.evaluate((button) => {
const rect = button.getBoundingClientRect();
return { left: Math.round(rect.left), width: Math.round(rect.width) };
})).toEqual(acceptProposalRect);

await page.evaluate(() => {
state.lastReportRefreshAt = 0;
Expand All @@ -1809,8 +1833,8 @@ test("@smoke the last individual file decision never auto-accepts the proposal",
await expect(page.locator("#proposalReviewPage")).toBeVisible();
await expect(page.getByRole("heading", { name: "Last individual review" })).toBeVisible();
await expect(page.getByRole("button", { name: /Open README\.md/ })).toContainText("Reviewed");
await expect(putOnMain).toBeVisible();
await expect(putOnMain).toBeEnabled();
await expect(acceptProposal).toBeVisible();
await expect(acceptProposal).toBeEnabled();
expect(await page.evaluate(() => ({
page: state.page,
mode: state.sharedContext?.mode,
Expand Down Expand Up @@ -1860,7 +1884,7 @@ test("@smoke terminal proposal acceptance obtains a one-shot challenge before co
});

await showTerminalProposal(page);
const acceptButton = page.getByRole("button", { name: "Put on main", exact: true });
const acceptButton = page.getByRole("button", { name: "Accept proposal", exact: true });
await expect(acceptButton).toBeVisible();
await acceptButton.click();

Expand Down Expand Up @@ -1941,7 +1965,7 @@ test("@smoke verified terminal acceptance stays pending, reports the commit, the
});

await showTerminalProposal(page, { projectId });
const putOnMain = page.getByRole("button", { name: "Put on main", exact: true });
const putOnMain = page.getByRole("button", { name: "Accept proposal", exact: true });
if (!mobileProject) {
await expect.poll(async () => page.evaluate(() => {
const accept = document.querySelector("#proposalDockAccept")?.getBoundingClientRect();
Expand Down Expand Up @@ -2041,7 +2065,7 @@ test("@smoke verified terminal acceptance with a pending Hub refresh keeps succe
await showTerminalProposal(page, { projectId });
await page.evaluate(() => setExplorerEdgePeek(true));
await expect(page.locator(".app")).not.toHaveClass(/explorer-edge-peek/);
await page.getByRole("button", { name: "Put on main", exact: true }).click();
await page.getByRole("button", { name: "Accept proposal", exact: true }).click();
await confirmTerminalAcceptance(page);

await expect(page).toHaveURL(hubUrl);
Expand Down Expand Up @@ -2116,7 +2140,7 @@ test("@smoke terminal acceptance without a valid returnTo falls back to the root
await showTerminalProposal(page, { projectId });
const explorerClose = page.getByRole("button", { name: "Close explorer" });
if (await explorerClose.isVisible()) await explorerClose.click();
await page.getByRole("button", { name: "Put on main", exact: true }).click();
await page.getByRole("button", { name: "Accept proposal", exact: true }).click();
await confirmTerminalAcceptance(page);
await page.waitForURL((url) => url.searchParams.get("view") === "hub");

Expand Down Expand Up @@ -2203,7 +2227,7 @@ test("@smoke verified acceptance carries its one-shot success toast across Hub p
});

await showTerminalProposal(page, { projectId, proposal: "proposal/demo/cross-port-toast" });
await page.getByRole("button", { name: "Put on main", exact: true }).click();
await page.getByRole("button", { name: "Accept proposal", exact: true }).click();
await confirmTerminalAcceptance(page);
await page.waitForURL((url) => url.origin === hubProxy.origin && url.searchParams.get("view") === "hub");
await waitForBoot(page);
Expand Down Expand Up @@ -2401,7 +2425,7 @@ test("@smoke two different valid delivery SHAs stay on the proposal and offer re
});

await showTerminalProposal(page);
await page.getByRole("button", { name: "Put on main", exact: true }).click();
await page.getByRole("button", { name: "Accept proposal", exact: true }).click();
const dialog = await confirmTerminalAcceptance(page);

await expect.poll(() => acceptCalls).toBe(1);
Expand Down Expand Up @@ -2475,7 +2499,7 @@ test("@smoke a cross-origin accepted HTTP 200 without a flash token stays on the
});

await showTerminalProposal(page, { projectId });
await page.getByRole("button", { name: "Put on main", exact: true }).click();
await page.getByRole("button", { name: "Accept proposal", exact: true }).click();
const dialog = await confirmTerminalAcceptance(page);

await expect.poll(() => acceptCalls).toBe(1);
Expand Down Expand Up @@ -2560,7 +2584,7 @@ for (const mismatch of [
});

await showTerminalProposal(page);
await page.getByRole("button", { name: "Put on main", exact: true }).click();
await page.getByRole("button", { name: "Accept proposal", exact: true }).click();
const dialog = await confirmTerminalAcceptance(page);

await expect.poll(() => acceptCalls).toBe(1);
Expand Down Expand Up @@ -2618,7 +2642,7 @@ test("@smoke failed terminal acceptance stays put, stays silent, and retries wit
});

await showTerminalProposal(page);
await page.getByRole("button", { name: "Put on main", exact: true }).click();
await page.getByRole("button", { name: "Accept proposal", exact: true }).click();
await expect(page.getByRole("dialog", { name: /Put this proposal on main\?/ })).toBeVisible();
const dialog = await confirmTerminalAcceptance(page);
await expect(dialog.getByRole("button", { name: "Putting on main…", exact: true })).toBeDisabled();
Expand Down Expand Up @@ -2685,7 +2709,7 @@ test("@smoke a terminal acceptance network failure stays on the proposal with a
});

await showTerminalProposal(page);
await page.getByRole("button", { name: "Put on main", exact: true }).click();
await page.getByRole("button", { name: "Accept proposal", exact: true }).click();
const dialog = await confirmTerminalAcceptance(page);

await expect.poll(() => acceptCalls).toBe(1);
Expand Down Expand Up @@ -2740,7 +2764,7 @@ test("@smoke terminal action is in the viewport and keyboard-operable on mobile

await showTerminalProposal(page);
await page.evaluate(() => { document.documentElement.style.zoom = "2"; });
const acceptButton = page.getByRole("button", { name: "Put on main", exact: true });
const acceptButton = page.getByRole("button", { name: "Accept proposal", exact: true });
await expect(acceptButton).toBeVisible();
await expect(acceptButton).toBeInViewport();
expect(await page.evaluate(() => {
Expand Down
8 changes: 4 additions & 4 deletions test/e2e/real-proposal-workflows.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -589,12 +589,12 @@ test.describe.serial("real proposal workflows", () => {
};
page.on("response", recordTerminalReturn);
page.on("request", recordFlashConsume);
await expect(page.getByRole("button", { name: "Put on main", exact: true })).toBeEnabled();
await expect(page.getByRole("button", { name: "Accept proposal", exact: true })).toBeEnabled();
const challengePromise = page.waitForResponse((response) => (
response.request().method() === "POST"
&& new URL(response.url()).pathname.endsWith("/api/shared-context/accept-challenge")
));
await page.getByRole("button", { name: "Put on main", exact: true }).click();
await page.getByRole("button", { name: "Accept proposal", exact: true }).click();
const challengeResponse = await challengePromise;
expect(challengeResponse.status(), await challengeResponse.text()).toBe(201);
const dialog = page.getByRole("dialog", { name: /Put this proposal on main/ });
Expand Down Expand Up @@ -957,13 +957,13 @@ test.describe.serial("real proposal workflows", () => {
await page.getByRole("button", { name: "Accept file", exact: true }).click();
const secondDecision = await secondDecisionResponse;
expect(secondDecision.status(), await secondDecision.text()).toBe(200);
await expect(page.getByRole("button", { name: "Put on main", exact: true })).toBeEnabled();
await expect(page.getByRole("button", { name: "Accept proposal", exact: true })).toBeEnabled();

const unavailableAcceptance = page.waitForResponse((response) => (
response.request().method() === "POST"
&& new URL(response.url()).pathname.endsWith("/api/shared-context/accept-challenge")
));
await page.getByRole("button", { name: "Put on main", exact: true }).click();
await page.getByRole("button", { name: "Accept proposal", exact: true }).click();
const unavailableResponse = await unavailableAcceptance;
const unavailablePayload = await unavailableResponse.json();
expect(unavailableResponse.status(), JSON.stringify(unavailablePayload)).toBe(503);
Expand Down
7 changes: 4 additions & 3 deletions test/shared_context.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,12 @@ test("shared proposal review keeps navigation and explicit completion in the pro
assert.match(html, /id="proposalDockBack"/);
assert.match(html, /id="proposalDockAccept"/);
assert.match(html, /id="proposalDockReject"/);
assert.match(html, /Put on main/);
assert.match(html, /Accept proposal/);
assert.match(html, /Reject proposal/);
assert.match(html, /Open review/);
assert.match(html, /const wasOpen = state\.sharedProposalWorkspaceOpen/);
assert.doesNotMatch(html, /Accept proposal/);
assert.match(html, /acceptButton\.hidden = \(!actionable && !preparing\) \|\| Boolean\(state\.proposalAuthorityStatus\)/);
assert.match(html, /acceptButton\.disabled = Boolean\(state\.proposalActionBusy \|\| !actionable \|\| queueCount > 0 \|\| noAcceptedChanges\)/);
assert.doesNotMatch(html, /Prepare pull request|Open pull request|Accepted branch ready/);
assert.match(html, /if \(shared\?\.mode === "review" \|\| proposalPreview\) \{\s*controls\.hidden = true;/);
assert.match(html, /backButton\.hidden = !inProposalContext \|\| onProposalPage/);
Expand Down Expand Up @@ -311,7 +312,7 @@ test("recoverable proposal authority warnings stay inspectable without enabling
assert.match(html, /function contextRoomProposalBlockedState\(item\)/);
assert.match(html, /contextRoomProposalBlockedState\(item\)\?\.phase === "recovery_required"\) return \{ key: "critical", label: "Recovery required" \}/);
assert.match(html, /state\.proposalAuthorityStatus/);
assert.match(html, /acceptButton\.hidden = !actionable \|\| queueCount > 0 \|\| Boolean\(state\.proposalAuthorityStatus\)/);
assert.match(html, /acceptButton\.hidden = \(!actionable && !preparing\) \|\| Boolean\(state\.proposalAuthorityStatus\)/);
assert.match(html, /state\.proposalAuthorityMessage/);
});

Expand Down
Loading