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
14 changes: 14 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,17 @@ de71ccccd55f21e909cb597347bcc6e1f81103e3:packages/daemon/test/media-native-trans
8fbb9e106d1b0e85f07a3d05abaea73b088b5382:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1251
8fbb9e106d1b0e85f07a3d05abaea73b088b5382:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1252
8fbb9e106d1b0e85f07a3d05abaea73b088b5382:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1253

# Exact source-file SHA-256 values in the reviewed release manifest.
# Verified against each referenced file at commit 32834956; not credentials.
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1367
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1420
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1425
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1428
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1435
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1436
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1438
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1442
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1484
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:861
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:972
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ release versions will follow [Semantic Versioning](https://semver.org/spec/v2.0.

### Changed

- Normal conversations now combine model selection and reasoning strength in one
compact popover, with three approval shortcuts beside the input. Sandbox and
independent authorization defaults remain in Settings.
- Simplified the public README and added source setup, troubleshooting, and platform
development-package instructions with a fresh workspace screenshot.

Expand Down
57 changes: 21 additions & 36 deletions apps/web/e2e/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1997,50 +1997,35 @@ test("conversation home model picker uses the model workbench default without ch
await navigation.getByRole("button", { name: "New conversation" }).click();
const home = page.getByLabel("Conversation home");
await expect(home.getByRole("button", { name: "Selected model gemini-2.5-pro" })).toBeVisible();
await expect(home.getByRole("button", { name: "Selected thinking mode Off" })).toBeVisible();
await home.getByRole("button", { name: "Selected thinking mode Off" }).click();
await expect(page.getByRole("menu", { name: "Thinking mode" }).getByRole("menuitemradio")).toHaveText(["Off", "Minimal", "Low", "Medium", "High", "XHigh"]);
await page.keyboard.press("Escape");

await home.getByRole("button", { name: "Selected model gemini-2.5-pro" }).click();
const modelMenu = page.locator(".conversation-model-menu");
await expect(modelMenu).toBeVisible();
await expect(modelMenu).toHaveAttribute("role", "menu");
const modelTrigger = home.getByRole("button", { name: "Selected model gemini-2.5-pro" });
await expect(modelTrigger).toContainText("Off");
await modelTrigger.click();
const reasoning = page.getByRole("dialog", { name: "Thinking mode" });
await expect(reasoning.getByRole("slider")).toHaveAttribute("max", "5");
await reasoning.getByRole("button", { name: "Model options" }).click();
const modelMenu = page.getByRole("dialog", { name: "Model options" });
await expect(modelMenu.locator(".conversation-model-option")).toHaveCount(4);
await expect(modelMenu.locator(".conversation-model-option").first()).toContainText("gemini-2.5-pro");
await expect(modelMenu.locator(".conversation-model-option").first()).toContainText("Default");
await expect(modelMenu.locator(".conversation-model-default-option")).toContainText("Default");
await expect(modelMenu).not.toContainText("gpt-image-2");
await expect(modelMenu).not.toContainText("gemini/");
await expect(modelMenu).not.toContainText("anthropic/");

const modelSearch = modelMenu.getByRole("textbox", { name: "Search models" });
await modelSearch.focus();
await expect.poll(() => modelMenu.locator(".conversation-model-search").evaluate((searchField) => {
const fieldRect = searchField.getBoundingClientRect();
const inputRect = searchField.querySelector("input")?.getBoundingClientRect();
if (!inputRect) {
return false;
}
return inputRect.left >= fieldRect.left &&
inputRect.right <= fieldRect.right &&
inputRect.top >= fieldRect.top &&
inputRect.bottom <= fieldRect.bottom;
})).toBe(true);
await expect(modelSearch).toBeFocused();
await modelSearch.fill("gpt-4o");
await expect(modelMenu.locator(".conversation-model-option")).toHaveCount(1);
await modelMenu.getByRole("menuitemradio", { name: /cliproxy-gpt-4o/ }).click();
await modelMenu.getByRole("button", { name: /cliproxy-gpt-4o/ }).click();
await expect(home.getByRole("button", { name: "Selected model cliproxy-gpt-4o" })).toBeVisible();
await expect(home.getByRole("button", { name: /Selected thinking mode/ })).toHaveCount(0);

await home.getByRole("button", { name: "Selected model cliproxy-gpt-4o" }).click();
await expect(modelMenu).toBeVisible();
await expect(modelMenu.getByRole("slider")).toHaveCount(0);
await modelSearch.fill("sonnet");
await expect(modelMenu.locator(".conversation-model-option")).toHaveCount(1);
await expect(modelMenu.locator(".conversation-model-option")).toContainText("claude-sonnet-4");
await modelMenu.getByRole("menuitemradio", { name: /claude-sonnet-4/ }).click();
await expect(home.getByRole("button", { name: "Selected model claude-sonnet-4" })).toBeVisible();
await home.getByRole("button", { name: "Selected thinking mode Off" }).click();
await page.getByRole("menu", { name: "Thinking mode" }).getByRole("menuitemradio", { name: "High", exact: true }).click();
await expect(home.getByRole("button", { name: "Selected thinking mode High" })).toBeVisible();
await modelMenu.getByRole("button", { name: /claude-sonnet-4/ }).click();
const sonnet = home.getByRole("button", { name: "Selected model claude-sonnet-4" });
await sonnet.click();
await reasoning.getByRole("slider").focus();
await reasoning.getByRole("slider").press("End");
await reasoning.getByRole("slider").press("ArrowLeft");
await expect(sonnet).toContainText("High");
await page.keyboard.press("Escape");

await home.getByRole("textbox", { name: "Conversation message" }).fill("Use Sonnet for this conversation");
await home.getByRole("button", { name: "Send" }).click();
Expand All @@ -2054,7 +2039,7 @@ test("conversation home model picker uses the model workbench default without ch

await navigation.locator(".sidebar-recent-row").filter({ hasText: "Use Sonnet for this conversation" }).click();
await expect(conversationPage.getByRole("button", { name: "Selected model claude-sonnet-4" })).toBeVisible();
await expect(conversationPage.getByRole("button", { name: "Selected thinking mode High" })).toBeVisible();
await expect(conversationPage.getByRole("button", { name: "Selected model claude-sonnet-4" })).toContainText("High");
});

test("settings keeps configuration tools embedded while Work Board stays hidden from global navigation", async ({ page }) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/ui2-conversation-composer-ownership.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ test("T13-O.31-E Dark 1280 Normal keeps slash, permission, reduced motion, long
const permissionTrigger = composer.getByRole("button", { name: /Selected permission preset/u });
await permissionTrigger.click();
const permissionOverlay = page.locator('[data-ling-overlay="menu"]').filter({
has: page.getByRole("menuitemradio", { name: /Read only/u }),
has: page.getByRole("menuitemradio", { name: "Request approval", exact: true }),
});
await expect(permissionOverlay).toBeVisible();
await expectRegionWithinViewport(page, permissionOverlay);
Expand Down
93 changes: 88 additions & 5 deletions apps/web/e2e/ui2-full-access-confirmation-parity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ test("normal conversation requires explicit confirmation and preserves the prese
await prepareSurface(page, "dark", 1280, 800, true);
const runtimeErrors = collectRuntimeErrors(page);
const state = createFullAccessState();
state.workspaceSettings.defaultWorkspaceAccessMode = "read_only";
await mockDaemon(page, state);
await page.goto("/");
await page.getByRole("complementary", { name: /Sidebar display|侧栏显示/u }).getByRole("button", { name: "New conversation" }).click();
Expand All @@ -95,7 +96,8 @@ test("normal conversation requires explicit confirmation and preserves the prese
expect(state.runs).toHaveLength(0);
await selectFullAccess(page, permissionTrigger);
await confirmFullAccessSelection(page);
await expect(permissionTrigger).toContainText("Full access · Confirmed for this conversation");
await expect(permissionTrigger).toHaveText("Full access");
await expect(permissionTrigger).toHaveAccessibleName(/Confirmed for this conversation/);
await captureP6FeedbackEvidence(
page,
"shell/full-access-confirmation-parity",
Expand All @@ -116,7 +118,7 @@ test("normal conversation requires explicit confirmation and preserves the prese

const activeConversation = page.getByLabel("Conversation page");
const activePermissionTrigger = activeConversation.getByRole("button", { name: /Selected permission preset/ });
await expect(activePermissionTrigger).toContainText("Full access · Confirmed for this conversation");
await expect(activePermissionTrigger).toHaveText("Full access");
await activeConversation.getByRole("textbox", { name: "Conversation message" }).fill("Continue in the same trusted conversation");
await activeConversation.getByRole("button", { name: "Send" }).click();
await expect(fullAccessDialog(page)).toHaveCount(0);
Expand All @@ -132,7 +134,7 @@ test("normal conversation requires explicit confirmation and preserves the prese
const nextPermissionTrigger = nextHome.getByRole("button", { name: /Selected permission preset/ });
await selectFullAccess(page, nextPermissionTrigger);
await confirmFullAccessSelection(page);
await expect(nextPermissionTrigger).toContainText("Full access · Confirmed for this conversation");
await expect(nextPermissionTrigger).toHaveText("Full access");
await nextHome.getByRole("textbox", { name: "Conversation message" }).fill("Start another trusted conversation");
await nextHome.getByRole("button", { name: "Send" }).click();
await expect(fullAccessDialog(page)).toHaveCount(0);
Expand All @@ -145,14 +147,14 @@ test("normal conversation requires explicit confirmation and preserves the prese

await expectSubmissionAcknowledged(page, [cwd, "normal", "", ""]);
await expect(page.getByLabel("Conversation page").getByRole("button", { name: /Selected permission preset/ }))
.toContainText("Full access · Confirmed for this conversation");
.toHaveText("Full access");
await page.reload();
await page.getByRole("complementary", { name: /Sidebar display|侧栏显示/u }).getByRole("button", { name: /Open recent conversation/ }).first().click();
const restoredConversation = page.getByLabel("Conversation page");
const restoredPermissionTrigger = restoredConversation.getByRole("button", { name: /Selected permission preset/ });
await selectFullAccess(page, restoredPermissionTrigger);
await expect(fullAccessDialog(page)).toHaveCount(0);
await expect(restoredPermissionTrigger).toContainText("Full access · Confirmed for this conversation");
await expect(restoredPermissionTrigger).toHaveText("Full access");
await restoredConversation.getByRole("textbox", { name: "Conversation message" }).fill("Continue after restarting the app");
await restoredConversation.getByRole("button", { name: "Send" }).click();
await expect(fullAccessDialog(page)).toHaveCount(0);
Expand All @@ -162,6 +164,12 @@ test("normal conversation requires explicit confirmation and preserves the prese
mode: "full_access",
reason: "user_confirmed_full_access",
});
await restoredPermissionTrigger.click();
await page.getByRole("menuitemradio", { name: "Help me approve", exact: true }).click();
await restoredConversation.getByRole("textbox", { name: "Conversation message" }).fill("Restore the workspace sandbox after reopening");
const restoredRequest = page.waitForRequest((request) => request.method() === "POST" && new URL(request.url()).pathname.endsWith("/conversations/stream"));
await restoredConversation.getByRole("button", { name: "Send", exact: true }).click();
expect((await restoredRequest).postDataJSON()).toMatchObject({ authorizationMode: "smart", workspaceAccessMode: "read_only" });
await expectNoDocumentOverflow(page);
expect(runtimeErrors).toEqual([]);
});
Expand Down Expand Up @@ -203,6 +211,81 @@ test("normal conversation confirms inherited full access on the first run", asyn
expect(runtimeErrors).toEqual([]);
});

for (const fixture of [
{ access: "read_only" as const, theme: "light" as const },
{ access: "workspace_write" as const, theme: "dark" as const },
]) {
test(`normal conversation approval shortcuts preserve ${fixture.access} and restore it after full access`, async ({ page }) => {
await prepareSurface(page, fixture.theme, 1280, 800, true);
const runtimeErrors = collectRuntimeErrors(page);
const state = createFullAccessState();
state.workspaceSettings = {
...state.workspaceSettings,
defaultAuthorizationMode: "ask",
defaultWorkspaceAccessMode: fixture.access,
};
await mockDaemon(page, state);
await page.goto("/");
await page.getByRole("complementary", { name: /Sidebar display|侧栏显示/u }).getByRole("button", { name: "New conversation" }).click();

const composer = page.getByLabel("New conversation composer");
const trigger = composer.getByRole("button", { name: /Selected permission preset/ });
await expect(trigger).toHaveText("Request approval");
await selectFullAccess(page, trigger);
await fullAccessDialog(page).getByRole("button", { name: "Cancel" }).last().click();
await expect(trigger).toHaveText("Request approval");
expect(await sendPrompt("After cancelling full access")).toMatchObject({ authorizationMode: "ask", workspaceAccessMode: fixture.access });

await trigger.click();
const menu = page.getByRole("menu").filter({ has: page.getByRole("menuitemradio", { name: "Request approval", exact: true }) });
await expect(menu.getByRole("menuitemradio")).toHaveCount(3);
await captureP6FeedbackEvidence(page, "shell/full-access-confirmation-parity", `1280x800-ling-${fixture.theme}-normal-approval-shortcuts.png`);
await menu.getByRole("menuitemradio", { name: "Help me approve", exact: true }).click();
expect(await sendPrompt("With approval review")).toMatchObject({ authorizationMode: "smart", workspaceAccessMode: fixture.access });

await selectFullAccess(page, trigger);
await confirmFullAccessSelection(page);
await expect(trigger).toHaveText("Full access");
expect(await sendPrompt("With confirmed full access")).toMatchObject({ authorizationMode: "always", workspaceAccessMode: "full_access" });

await trigger.click();
await menu.getByRole("menuitemradio", { name: "Help me approve", exact: true }).click();
expect(await sendPrompt("Back to approval review")).toMatchObject({ authorizationMode: "smart", workspaceAccessMode: fixture.access });
await expectNoDocumentOverflow(page);
expect(runtimeErrors).toEqual([]);

async function sendPrompt(message: string): Promise<Record<string, unknown>> {
await composer.getByRole("textbox", { name: "Conversation message" }).fill(message);
const request = page.waitForRequest((candidate) => candidate.method() === "POST" && new URL(candidate.url()).pathname.endsWith("/conversations/stream"));
await composer.getByRole("button", { name: "Send", exact: true }).click();
const body = (await request).postDataJSON() as Record<string, unknown>;
await expect(page.getByLabel("Conversation page")).toBeVisible();
await expect(trigger).toBeEnabled();
return body;
}
});
}

test("normal conversation distinguishes inherited always authorization from full access", async ({ page }) => {
await prepareSurface(page, "light", 1280, 800, true);
const state = createFullAccessState();
state.workspaceSettings = { ...state.workspaceSettings, defaultAuthorizationMode: "always", defaultWorkspaceAccessMode: "read_only" };
await mockDaemon(page, state);
await page.goto("/");
await page.getByRole("complementary", { name: /Sidebar display|侧栏显示/u }).getByRole("button", { name: "New conversation" }).click();
const composer = page.getByLabel("New conversation composer");
const trigger = composer.getByRole("button", { name: /Selected permission preset/ });
await expect(trigger).toHaveText("Always authorize");
await trigger.click();
const menu = page.getByRole("menu").filter({ has: page.getByRole("menuitemradio", { name: "Request approval", exact: true }) });
await expect(menu.getByRole("menuitemradio", { name: "Full access", exact: true })).not.toBeChecked();
await menu.getByRole("menuitemradio", { name: "Request approval", exact: true }).click();
await composer.getByRole("textbox", { name: "Conversation message" }).fill("Keep the configured read-only sandbox");
const request = page.waitForRequest((candidate) => candidate.method() === "POST" && new URL(candidate.url()).pathname.endsWith("/conversations/stream"));
await composer.getByRole("button", { name: "Send", exact: true }).click();
expect((await request).postDataJSON()).toMatchObject({ authorizationMode: "ask", workspaceAccessMode: "read_only" });
});

test("teammate private chat confirms explicit selection per conversation", async ({ page }) => {
await prepareSurface(page, "light", 1440, 960);
const runtimeErrors = collectRuntimeErrors(page);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ test("1280x800 Ling Dark keeps Normal and Teammate command menus and focus conta

const normalPermission = normalComposer.getByRole("button", { name: /Selected permission preset/u });
await normalPermission.click();
const normalMenu = permissionMenu(page);
const normalMenu = page.getByRole("menu").filter({ has: page.getByRole("menuitemradio", { name: "Request approval", exact: true }) });
await expect(normalMenu).toBeVisible();
await expectOverlayContained(page, normalMenu);
await page.keyboard.press("Escape");
Expand Down
Loading
Loading