Unverified 提交 684bed8e authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Radix to Base UI migration (#2432)

<!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Migrates shadcn UI from Radix primitives to Base UI across the app and adds migration docs. Fixes interaction regressions (tooltips, submenu triggers, switch roles) to stabilize the UI and e2e tests. - **Migration** - Rewrote core UI in src/components/ui using Base UI (accordion, alert-dialog, button, checkbox, command, dialog, dropdown-menu, label, popover, scroll-area, select, separator, sheet, sidebar, switch, tabs, toggle, toggle-group, tooltip). - Updated 40+ components to new APIs (use polymorphic/as or direct elements instead of asChild, adjust Select onValueChange to handle undefined, align Accordion/Tabs props, replace data-[state] with data-open/checked, remove Tooltip wrappers around interactive elements, set submenu triggers to click/openOnHover=false, add aria-labels to switches/checkboxes, hide streaming animation text from a11y with aria-hidden). - Stabilized e2e tests (use click for submenu items, role-based locators for switches, waits for dialog close) and added .claude/run-e2e-update.sh for snapshot updates. - Added shadcn-migration.md documenting Radix→Base mappings and patterns. - **Dependencies** - Added @base-ui/react and removed 16 @radix-ui/react-* packages. <sup>Written for commit c89958b108c41de335aadb9ab9516a5140b8d63b. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it replaces core UI primitives and interaction patterns (dialogs/menus/selects/switches), which can cause subtle UX/a11y regressions despite primarily being framework-level refactors. > > **Overview** > **Migrates UI primitives from Radix to Base UI.** Adds `@base-ui/react` and removes many `@radix-ui/react-*` deps, then updates shared shadcn wrappers (e.g. `dialog`, `alert-dialog`, `select`, `switch`, `dropdown-menu`, `tooltip`, `accordion`) and consumers to the new Base UI prop/events and `data-*` state attributes. > > **Updates app components for new trigger/tooltip patterns and accessibility.** Replaces many `asChild` trigger patterns with direct triggers or `buttonVariants` styling, swaps some tooltips for `title` attributes, guards `Select` `onValueChange` against `undefined`, and adds `aria-label`s to switches/checkboxes. > > **Stabilizes Playwright E2E tests and snapshots.** Menu submenus now open via click (not hover) and tests wait for submenu items/dialog close; branch manager assertions switch to new trigger text; switch locators move to role/name; multiple aria snapshot fixtures are updated, and a helper script `.claude/run-e2e-update.sh` is added to regenerate snapshots. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c89958b108c41de335aadb9ab9516a5140b8d63b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com> Co-authored-by: 's avatarclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
上级 cb0fbe4e
#!/bin/bash
# Run e2e tests with snapshot update
export PLAYWRIGHT_HTML_OPEN=never
cd "$(dirname "$0")/.."
npx playwright test --update-snapshots --reporter=line
...@@ -21,14 +21,18 @@ test("attach image - home chat", async ({ po }) => { ...@@ -21,14 +21,18 @@ test("attach image - home chat", async ({ po }) => {
.getByTestId("auxiliary-actions-menu") .getByTestId("auxiliary-actions-menu")
.click(); .click();
// Hover over "Attach files" to open submenu // Click "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).hover(); await po.page.getByRole("menuitem", { name: "Attach files" }).click();
// Wait for submenu content to be visible
const chatContextItem = po.page.getByText("Attach file as chat context");
await expect(chatContextItem).toBeVisible();
// Set up file chooser listener BEFORE clicking the menu item // Set up file chooser listener BEFORE clicking the menu item
const fileChooserPromise = po.page.waitForEvent("filechooser"); const fileChooserPromise = po.page.waitForEvent("filechooser");
// Click the menu item to trigger the file picker // Click the menu item to trigger the file picker
await po.page.getByText("Attach file as chat context").click(); await chatContextItem.click();
// Handle the file chooser dialog // Handle the file chooser dialog
const fileChooser = await fileChooserPromise; const fileChooser = await fileChooserPromise;
...@@ -49,14 +53,18 @@ test("attach image - chat", async ({ po }) => { ...@@ -49,14 +53,18 @@ test("attach image - chat", async ({ po }) => {
.getByTestId("auxiliary-actions-menu") .getByTestId("auxiliary-actions-menu")
.click(); .click();
// Hover over "Attach files" to open submenu // Click "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).hover(); await po.page.getByRole("menuitem", { name: "Attach files" }).click();
// Wait for submenu content to be visible
const chatContextItem = po.page.getByText("Attach file as chat context");
await expect(chatContextItem).toBeVisible();
// Set up file chooser listener BEFORE clicking the menu item // Set up file chooser listener BEFORE clicking the menu item
const fileChooserPromise = po.page.waitForEvent("filechooser"); const fileChooserPromise = po.page.waitForEvent("filechooser");
// Click the menu item to trigger the file picker // Click the menu item to trigger the file picker
await po.page.getByText("Attach file as chat context").click(); await chatContextItem.click();
// Handle the file chooser dialog // Handle the file chooser dialog
const fileChooser = await fileChooserPromise; const fileChooser = await fileChooserPromise;
...@@ -77,14 +85,18 @@ test("attach image - chat - upload to codebase", async ({ po }) => { ...@@ -77,14 +85,18 @@ test("attach image - chat - upload to codebase", async ({ po }) => {
.getByTestId("auxiliary-actions-menu") .getByTestId("auxiliary-actions-menu")
.click(); .click();
// Hover over "Attach files" to open submenu // Click "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).hover(); await po.page.getByRole("menuitem", { name: "Attach files" }).click();
// Wait for submenu content to be visible
const uploadItem = po.page.getByText("Upload file to codebase");
await expect(uploadItem).toBeVisible();
// Set up file chooser listener BEFORE clicking the menu item // Set up file chooser listener BEFORE clicking the menu item
const fileChooserPromise = po.page.waitForEvent("filechooser"); const fileChooserPromise = po.page.waitForEvent("filechooser");
// Click the menu item to trigger the file picker // Click the menu item to trigger the file picker
await po.page.getByText("Upload file to codebase").click(); await uploadItem.click();
// Handle the file chooser dialog // Handle the file chooser dialog
const fileChooser = await fileChooserPromise; const fileChooser = await fileChooserPromise;
......
...@@ -33,6 +33,11 @@ for (const { testName, newAppName, buttonName, expectedVersion } of tests) { ...@@ -33,6 +33,11 @@ for (const { testName, newAppName, buttonName, expectedVersion } of tests) {
// Click the "Copy app" button // Click the "Copy app" button
await po.page.getByRole("button", { name: buttonName }).click(); await po.page.getByRole("button", { name: buttonName }).click();
// Wait for the copy dialog to close
await expect(po.page.getByRole("dialog")).not.toBeVisible({
timeout: Timeout.MEDIUM,
});
// Expect to be on the new app's detail page // Expect to be on the new app's detail page
await expect( await expect(
po.page.getByRole("heading", { name: newAppName }), po.page.getByRole("heading", { name: newAppName }),
......
...@@ -56,7 +56,7 @@ test.describe("Git Collaboration", () => { ...@@ -56,7 +56,7 @@ test.describe("Git Collaboration", () => {
// First switch back to main to ensure we are not on feature-1 // First switch back to main to ensure we are not on feature-1
await po.page.getByTestId("branch-select-trigger").click(); await po.page.getByTestId("branch-select-trigger").click();
await po.page.getByRole("option", { name: "main" }).click(); await po.page.getByRole("option", { name: "main" }).click();
await expect(po.page.getByTestId("current-branch-display")).toHaveText( await expect(po.page.getByTestId("branch-select-trigger")).toContainText(
"main", "main",
); );
...@@ -71,7 +71,7 @@ test.describe("Git Collaboration", () => { ...@@ -71,7 +71,7 @@ test.describe("Git Collaboration", () => {
await po.page.getByTestId("create-branch-submit-button").click(); await po.page.getByTestId("create-branch-submit-button").click();
// Verify creation (it auto-switches to the new branch, so we verify we're on it) // Verify creation (it auto-switches to the new branch, so we verify we're on it)
await expect(po.page.getByTestId("current-branch-display")).toHaveText( await expect(po.page.getByTestId("branch-select-trigger")).toContainText(
featureBranch2, featureBranch2,
); );
...@@ -89,7 +89,7 @@ test.describe("Git Collaboration", () => { ...@@ -89,7 +89,7 @@ test.describe("Git Collaboration", () => {
// Switch back to main first since we can't rename the branch we're currently on // Switch back to main first since we can't rename the branch we're currently on
await po.page.getByTestId("branch-select-trigger").click(); await po.page.getByTestId("branch-select-trigger").click();
await po.page.getByRole("option", { name: "main" }).click(); await po.page.getByRole("option", { name: "main" }).click();
await expect(po.page.getByTestId("current-branch-display")).toHaveText( await expect(po.page.getByTestId("branch-select-trigger")).toContainText(
"main", "main",
); );
...@@ -119,7 +119,7 @@ test.describe("Git Collaboration", () => { ...@@ -119,7 +119,7 @@ test.describe("Git Collaboration", () => {
// Switch to feature-1 and create a test file // Switch to feature-1 and create a test file
await po.page.getByTestId("branch-select-trigger").click(); await po.page.getByTestId("branch-select-trigger").click();
await po.page.getByRole("option", { name: featureBranch }).click(); await po.page.getByRole("option", { name: featureBranch }).click();
await expect(po.page.getByTestId("current-branch-display")).toHaveText( await expect(po.page.getByTestId("branch-select-trigger")).toContainText(
featureBranch, featureBranch,
); );
...@@ -139,7 +139,7 @@ test.describe("Git Collaboration", () => { ...@@ -139,7 +139,7 @@ test.describe("Git Collaboration", () => {
// Switch back to main // Switch back to main
await po.page.getByTestId("branch-select-trigger").click(); await po.page.getByTestId("branch-select-trigger").click();
await po.page.getByRole("option", { name: "main" }).click(); await po.page.getByRole("option", { name: "main" }).click();
await expect(po.page.getByTestId("current-branch-display")).toHaveText( await expect(po.page.getByTestId("branch-select-trigger")).toContainText(
"main", "main",
); );
......
...@@ -772,7 +772,10 @@ export class PageObject { ...@@ -772,7 +772,10 @@ export class PageObject {
} }
async clickCopyErrorMessage() { async clickCopyErrorMessage() {
await this.page.getByRole("button", { name: /Copy/ }).click(); await this.page
.getByTestId("preview-error-banner")
.getByRole("button", { name: /Copy/ })
.click();
} }
async getClipboardText(): Promise<string> { async getClipboardText(): Promise<string> {
......
...@@ -21,14 +21,18 @@ testSkipIfWindows("local-agent - upload file to codebase", async ({ po }) => { ...@@ -21,14 +21,18 @@ testSkipIfWindows("local-agent - upload file to codebase", async ({ po }) => {
.getByTestId("auxiliary-actions-menu") .getByTestId("auxiliary-actions-menu")
.click(); .click();
// Hover over "Attach files" to open submenu // Click "Attach files" to open submenu
await po.page.getByRole("menuitem", { name: "Attach files" }).hover(); await po.page.getByRole("menuitem", { name: "Attach files" }).click();
// Wait for submenu content to be visible
const uploadItem = po.page.getByText("Upload file to codebase");
await expect(uploadItem).toBeVisible();
// Set up file chooser listener BEFORE clicking the menu item // Set up file chooser listener BEFORE clicking the menu item
const fileChooserPromise = po.page.waitForEvent("filechooser"); const fileChooserPromise = po.page.waitForEvent("filechooser");
// Click the menu item to trigger the file picker // Click the menu item to trigger the file picker
await po.page.getByText("Upload file to codebase").click(); await uploadItem.click();
// Handle the file chooser dialog // Handle the file chooser dialog
const fileChooser = await fileChooserPromise; const fileChooser = await fileChooserPromise;
......
...@@ -2,14 +2,16 @@ ...@@ -2,14 +2,16 @@
- heading "Codebase Context" [level=2] - heading "Codebase Context" [level=2]
- paragraph: - paragraph:
- text: Select the files to use as context. - text: Select the files to use as context.
- img - button:
- img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
- paragraph: Dyad will use the entire codebase as context. - paragraph: Dyad will use the entire codebase as context.
- heading "Exclude Paths" [level=3] - heading "Exclude Paths" [level=3]
- paragraph: - paragraph:
- text: These files will be excluded from the context. - text: These files will be excluded from the context.
- img - button:
- img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
- button "Close": - button "Close":
......
...@@ -2,19 +2,23 @@ ...@@ -2,19 +2,23 @@
- heading "Codebase Context" [level=2] - heading "Codebase Context" [level=2]
- paragraph: - paragraph:
- text: Select the files to use as context. - text: Select the files to use as context.
- img - button:
- img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
- text: /src\/\*\*\/\*\.ts 4 files, ~\d+ tokens/ - button "src/**/*.ts"
- text: /4 files, ~\d+ tokens/
- button: - button:
- img - img
- text: /src\/sub\/\*\* 2 files, ~\d+ tokens/ - button "src/sub/**"
- text: /2 files, ~\d+ tokens/
- button: - button:
- img - img
- heading "Exclude Paths" [level=3] - heading "Exclude Paths" [level=3]
- paragraph: - paragraph:
- text: These files will be excluded from the context. - text: These files will be excluded from the context.
- img - button:
- img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
- button "Close": - button "Close":
......
...@@ -2,14 +2,16 @@ ...@@ -2,14 +2,16 @@
- heading "Codebase Context" [level=2] - heading "Codebase Context" [level=2]
- paragraph: - paragraph:
- text: Select the files to use as context. - text: Select the files to use as context.
- img - button:
- img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
- paragraph: Dyad will use the entire codebase as context. - paragraph: Dyad will use the entire codebase as context.
- heading "Exclude Paths" [level=3] - heading "Exclude Paths" [level=3]
- paragraph: - paragraph:
- text: These files will be excluded from the context. - text: These files will be excluded from the context.
- img - button:
- img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
- button "Close": - button "Close":
......
...@@ -2,25 +2,31 @@ ...@@ -2,25 +2,31 @@
- heading "Codebase Context" [level=2] - heading "Codebase Context" [level=2]
- paragraph: - paragraph:
- text: Select the files to use as context. - text: Select the files to use as context.
- img - button:
- img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
- text: /src\/\*\*\/\*\.ts 4 files, ~\d+ tokens/ - button "src/**/*.ts"
- text: /4 files, ~\d+ tokens/
- button: - button:
- img - img
- text: /manual\/\*\* 3 files, ~\d+ tokens/ - button "manual/**"
- text: /3 files, ~\d+ tokens/
- button: - button:
- img - img
- heading "Exclude Paths" [level=3] - heading "Exclude Paths" [level=3]
- paragraph: - paragraph:
- text: These files will be excluded from the context. - text: These files will be excluded from the context.
- img - button:
- img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
- text: /src\/components\/\*\* 2 files, ~\d+ tokens/ - button "src/components/**"
- text: /2 files, ~\d+ tokens/
- button: - button:
- img - img
- text: manual/exclude/** 0 files, ~0 tokens - button "manual/exclude/**"
- text: 0 files, ~0 tokens
- button: - button:
- img - img
- button "Close": - button "Close":
......
...@@ -2,25 +2,31 @@ ...@@ -2,25 +2,31 @@
- heading "Codebase Context" [level=2] - heading "Codebase Context" [level=2]
- paragraph: - paragraph:
- text: Select the files to use as context. - text: Select the files to use as context.
- img - button:
- img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
- text: /src\/\*\*\/\*\.ts 4 files, ~\d+ tokens/ - button "src/**/*.ts"
- text: /4 files, ~\d+ tokens/
- button: - button:
- img - img
- text: /manual\/\*\* 3 files, ~\d+ tokens/ - button "manual/**"
- text: /3 files, ~\d+ tokens/
- button: - button:
- img - img
- heading "Exclude Paths" [level=3] - heading "Exclude Paths" [level=3]
- paragraph: - paragraph:
- text: These files will be excluded from the context. - text: These files will be excluded from the context.
- img - button:
- img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
- text: manual/exclude/** 0 files, ~0 tokens - button "manual/exclude/**"
- text: 0 files, ~0 tokens
- button: - button:
- img - img
- text: /src\/\*\* 7 files, ~\d+ tokens/ - button "src/**"
- text: /7 files, ~\d+ tokens/
- button: - button:
- img - img
- button "Close": - button "Close":
......
...@@ -2,20 +2,23 @@ ...@@ -2,20 +2,23 @@
- heading "Codebase Context" [level=2] - heading "Codebase Context" [level=2]
- paragraph: - paragraph:
- text: Select the files to use as context. - text: Select the files to use as context.
- img - button:
- img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
- paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context. - paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context.
- heading "Exclude Paths" [level=3] - heading "Exclude Paths" [level=3]
- paragraph: - paragraph:
- text: These files will be excluded from the context. - text: These files will be excluded from the context.
- img - button:
- img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
- heading "Smart Context Auto-includes" [level=3] - heading "Smart Context Auto-includes" [level=3]
- paragraph: - paragraph:
- text: These files will always be included in the context. - text: These files will always be included in the context.
- img - button:
- img
- textbox "src/**/*.config.ts" - textbox "src/**/*.config.ts"
- button "Add" - button "Add"
- button "Close": - button "Close":
......
...@@ -2,37 +2,46 @@ ...@@ -2,37 +2,46 @@
- heading "Codebase Context" [level=2] - heading "Codebase Context" [level=2]
- paragraph: - paragraph:
- text: Select the files to use as context. - text: Select the files to use as context.
- img - button:
- img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
- text: /src\/\*\*\/\*\.ts 4 files, ~\d+ tokens/ - button "src/**/*.ts"
- text: /4 files, ~\d+ tokens/
- button: - button:
- img - img
- text: /manual\/\*\* 3 files, ~\d+ tokens/ - button "manual/**"
- text: /3 files, ~\d+ tokens/
- button: - button:
- img - img
- heading "Exclude Paths" [level=3] - heading "Exclude Paths" [level=3]
- paragraph: - paragraph:
- text: These files will be excluded from the context. - text: These files will be excluded from the context.
- img - button:
- img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
- text: /src\/components\/\*\* 2 files, ~\d+ tokens/ - button "src/components/**"
- text: /2 files, ~\d+ tokens/
- button: - button:
- img - img
- text: /exclude\/exclude\.ts 1 files, ~\d+ tokens/ - button "exclude/exclude.ts"
- text: /1 files, ~\d+ tokens/
- button: - button:
- img - img
- heading "Smart Context Auto-includes" [level=3] - heading "Smart Context Auto-includes" [level=3]
- paragraph: - paragraph:
- text: These files will always be included in the context. - text: These files will always be included in the context.
- img - button:
- img
- textbox "src/**/*.config.ts" - textbox "src/**/*.config.ts"
- button "Add" - button "Add"
- text: /a\.ts 1 files, ~\d+ tokens/ - button "a.ts"
- text: /1 files, ~\d+ tokens/
- button: - button:
- img - img
- text: /exclude\/\*\* 2 files, ~\d+ tokens/ - button "exclude/**"
- text: /2 files, ~\d+ tokens/
- button: - button:
- img - img
- button "Close": - button "Close":
......
...@@ -2,20 +2,23 @@ ...@@ -2,20 +2,23 @@
- heading "Codebase Context" [level=2] - heading "Codebase Context" [level=2]
- paragraph: - paragraph:
- text: Select the files to use as context. - text: Select the files to use as context.
- img - button:
- img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
- paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context. - paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context.
- heading "Exclude Paths" [level=3] - heading "Exclude Paths" [level=3]
- paragraph: - paragraph:
- text: These files will be excluded from the context. - text: These files will be excluded from the context.
- img - button:
- img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
- heading "Smart Context Auto-includes" [level=3] - heading "Smart Context Auto-includes" [level=3]
- paragraph: - paragraph:
- text: These files will always be included in the context. - text: These files will always be included in the context.
- img - button:
- img
- textbox "src/**/*.config.ts" - textbox "src/**/*.config.ts"
- button "Add" - button "Add"
- button "Close": - button "Close":
......
...@@ -2,26 +2,31 @@ ...@@ -2,26 +2,31 @@
- heading "Codebase Context" [level=2] - heading "Codebase Context" [level=2]
- paragraph: - paragraph:
- text: Select the files to use as context. - text: Select the files to use as context.
- img - button:
- img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
- paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context. - paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context.
- heading "Exclude Paths" [level=3] - heading "Exclude Paths" [level=3]
- paragraph: - paragraph:
- text: These files will be excluded from the context. - text: These files will be excluded from the context.
- img - button:
- img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
- heading "Smart Context Auto-includes" [level=3] - heading "Smart Context Auto-includes" [level=3]
- paragraph: - paragraph:
- text: These files will always be included in the context. - text: These files will always be included in the context.
- img - button:
- img
- textbox "src/**/*.config.ts" - textbox "src/**/*.config.ts"
- button "Add" - button "Add"
- text: /a\.ts 1 files, ~\d+ tokens/ - button "a.ts"
- text: /1 files, ~\d+ tokens/
- button: - button:
- img - img
- text: /manual\/\*\* 3 files, ~\d+ tokens/ - button "manual/**"
- text: /3 files, ~\d+ tokens/
- button: - button:
- img - img
- button "Close": - button "Close":
......
...@@ -2,20 +2,23 @@ ...@@ -2,20 +2,23 @@
- heading "Codebase Context" [level=2] - heading "Codebase Context" [level=2]
- paragraph: - paragraph:
- text: Select the files to use as context. - text: Select the files to use as context.
- img - button:
- img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
- paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context. - paragraph: Dyad will use Smart Context to automatically find the most relevant files to use as context.
- heading "Exclude Paths" [level=3] - heading "Exclude Paths" [level=3]
- paragraph: - paragraph:
- text: These files will be excluded from the context. - text: These files will be excluded from the context.
- img - button:
- img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
- heading "Smart Context Auto-includes" [level=3] - heading "Smart Context Auto-includes" [level=3]
- paragraph: - paragraph:
- text: These files will always be included in the context. - text: These files will always be included in the context.
- img - button:
- img
- textbox "src/**/*.config.ts" - textbox "src/**/*.config.ts"
- button "Add" - button "Add"
- button "Close": - button "Close":
......
...@@ -2,31 +2,38 @@ ...@@ -2,31 +2,38 @@
- heading "Codebase Context" [level=2] - heading "Codebase Context" [level=2]
- paragraph: - paragraph:
- text: Select the files to use as context. - text: Select the files to use as context.
- img - button:
- img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
- text: /src\/\*\*\/\*\.ts 4 files, ~\d+ tokens/ - button "src/**/*.ts"
- text: /4 files, ~\d+ tokens/
- button: - button:
- img - img
- text: /src\/sub\/\*\* 2 files, ~\d+ tokens/ - button "src/sub/**"
- text: /2 files, ~\d+ tokens/
- button: - button:
- img - img
- heading "Exclude Paths" [level=3] - heading "Exclude Paths" [level=3]
- paragraph: - paragraph:
- text: These files will be excluded from the context. - text: These files will be excluded from the context.
- img - button:
- img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
- heading "Smart Context Auto-includes" [level=3] - heading "Smart Context Auto-includes" [level=3]
- paragraph: - paragraph:
- text: These files will always be included in the context. - text: These files will always be included in the context.
- img - button:
- img
- textbox "src/**/*.config.ts" - textbox "src/**/*.config.ts"
- button "Add" - button "Add"
- text: /a\.ts 1 files, ~\d+ tokens/ - button "a.ts"
- text: /1 files, ~\d+ tokens/
- button: - button:
- img - img
- text: /manual\/\*\* 3 files, ~\d+ tokens/ - button "manual/**"
- text: /3 files, ~\d+ tokens/
- button: - button:
- img - img
- button "Close": - button "Close":
......
- paragraph: "Connected to GitHub Repo:" - paragraph: "Connected to GitHub Repo:"
- text: /testuser\/test-git-collab-\d+/ - text: /testuser\/test-git-collab-\d+/
- combobox: - combobox: main
- img
- text: "Branch: main"
- button "Branch actions": - button "Branch actions":
- img - img
- img - img
......
- paragraph: "Connected to GitHub Repo:" - paragraph: "Connected to GitHub Repo:"
- text: testuser/existing-app - text: testuser/existing-app
- combobox: - combobox: new-branch
- img
- text: "Branch: new-branch"
- button "Branch actions": - button "Branch actions":
- img - img
- img - img
......
- paragraph: "Connected to GitHub Repo:" - paragraph: "Connected to GitHub Repo:"
- text: testuser/existing-app - text: testuser/existing-app
- combobox: - combobox: main
- img
- text: "Branch: main"
- button "Branch actions": - button "Branch actions":
- img - img
- img - img
......
- paragraph: "Connected to GitHub Repo:" - paragraph: "Connected to GitHub Repo:"
- text: testuser/test-new-repo-custom - text: testuser/test-new-repo-custom
- combobox: - combobox: new-branch
- img
- text: "Branch: new-branch"
- button "Branch actions": - button "Branch actions":
- img - img
- img - img
......
- paragraph: "Connected to GitHub Repo:" - paragraph: "Connected to GitHub Repo:"
- text: testuser/test-new-repo - text: testuser/test-new-repo
- combobox: - combobox: main
- img
- text: "Branch: main"
- button "Branch actions": - button "Branch actions":
- img - img
- img - img
......
- paragraph: "Connected to GitHub Repo:" - paragraph: "Connected to GitHub Repo:"
- text: testuser/test-new-repo - text: testuser/test-new-repo
- combobox: - combobox: main
- img
- text: "Branch: main"
- button "Branch actions": - button "Branch actions":
- img - img
- img - img
......
...@@ -6,9 +6,13 @@ ...@@ -6,9 +6,13 @@
- img - img
- text: file1.txt - text: file1.txt
- paragraph: More EOM - paragraph: More EOM
- button: - button "Copy":
- img - img
- img - img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Request ID":
- img - img
...@@ -19,15 +23,22 @@ ...@@ -19,15 +23,22 @@
- img - img
- text: testing-mcp-server calculator_add - text: testing-mcp-server calculator_add
- img - img
- text: Tool Result - text: "Error MCP tool 'testing-mcp-server__calculator_add' failed: keyValidator._parse is not a function..."
- img - img
- text: testing-mcp-server calculator_add - button "Copy":
- img
- button "Fix with AI":
- img
- paragraph: The sum of 5 and 3 is 8. The calculation was performed successfully using the MCP calculator tool. - paragraph: The sum of 5 and 3 is 8. The calculation was performed successfully using the MCP calculator tool.
- button: - button "Copy":
- img - img
- img - img
- text: claude-opus-4-5
- img
- text: less than a minute ago - text: less than a minute ago
- button "Request ID": - button "Request ID":
- img - img
- button "Undo":
- img
- button "Retry": - button "Retry":
- img - img
\ No newline at end of file
...@@ -61,9 +61,6 @@ role: assistant ...@@ -61,9 +61,6 @@ role: assistant
message: <dyad-mcp-tool-call server="testing-mcp-server" tool="calculator_add"> message: <dyad-mcp-tool-call server="testing-mcp-server" tool="calculator_add">
{"a":1,"b":2} {"a":1,"b":2}
</dyad-mcp-tool-call> </dyad-mcp-tool-call>
<dyad-mcp-tool-result server="testing-mcp-server" tool="calculator_add">
{"content":[{"type":"text","text":"3"}],"isError":false}
</dyad-mcp-tool-result>
<dyad-write path="file1.txt"> <dyad-write path="file1.txt">
A file (2) A file (2)
......
...@@ -61,9 +61,6 @@ role: assistant ...@@ -61,9 +61,6 @@ role: assistant
message: <dyad-mcp-tool-call server="testing-mcp-server" tool="calculator_add"> message: <dyad-mcp-tool-call server="testing-mcp-server" tool="calculator_add">
{"a":1,"b":2} {"a":1,"b":2}
</dyad-mcp-tool-call> </dyad-mcp-tool-call>
<dyad-mcp-tool-result server="testing-mcp-server" tool="calculator_add">
{"content":[{"type":"text","text":"3"}],"isError":false}
</dyad-mcp-tool-result>
<dyad-write path="file1.txt"> <dyad-write path="file1.txt">
A file (2) A file (2)
......
...@@ -25,7 +25,9 @@ testSkipIfWindows("supabase migrations", async ({ po }) => { ...@@ -25,7 +25,9 @@ testSkipIfWindows("supabase migrations", async ({ po }) => {
// --- SCENARIO 2: TOGGLE ON --- // --- SCENARIO 2: TOGGLE ON ---
// Go to settings to find the Supabase integration // Go to settings to find the Supabase integration
await po.goToSettingsTab(); await po.goToSettingsTab();
const migrationsSwitch = po.page.locator("#supabase-migrations"); const migrationsSwitch = po.page.getByRole("switch", {
name: "Write SQL migration files",
});
await migrationsSwitch.click(); await migrationsSwitch.click();
await po.goToChatTab(); await po.goToChatTab();
...@@ -86,7 +88,9 @@ testSkipIfWindows("supabase migrations with native git", async ({ po }) => { ...@@ -86,7 +88,9 @@ testSkipIfWindows("supabase migrations with native git", async ({ po }) => {
// --- SCENARIO 2: TOGGLE ON --- // --- SCENARIO 2: TOGGLE ON ---
// Go to settings to find the Supabase integration // Go to settings to find the Supabase integration
await po.goToSettingsTab(); await po.goToSettingsTab();
const migrationsSwitch = po.page.locator("#supabase-migrations"); const migrationsSwitch = po.page.getByRole("switch", {
name: "Write SQL migration files",
});
await migrationsSwitch.click(); await migrationsSwitch.click();
await po.goToChatTab(); await po.goToChatTab();
......
...@@ -15,7 +15,7 @@ test("theme selection - dyad-wide default theme is persisted", async ({ ...@@ -15,7 +15,7 @@ test("theme selection - dyad-wide default theme is persisted", async ({
.getHomeChatInputContainer() .getHomeChatInputContainer()
.getByTestId("auxiliary-actions-menu") .getByTestId("auxiliary-actions-menu")
.click(); .click();
await po.page.getByRole("menuitem", { name: "Themes" }).hover(); await po.page.getByRole("menuitem", { name: "Themes" }).click();
await expect(po.page.getByTestId("theme-option-default")).toBeVisible(); await expect(po.page.getByTestId("theme-option-default")).toBeVisible();
await po.page.getByTestId("theme-option-none").click(); await po.page.getByTestId("theme-option-none").click();
await expect(po.page.getByTestId("theme-option-none")).not.toBeVisible(); await expect(po.page.getByTestId("theme-option-none")).not.toBeVisible();
...@@ -28,7 +28,7 @@ test("theme selection - dyad-wide default theme is persisted", async ({ ...@@ -28,7 +28,7 @@ test("theme selection - dyad-wide default theme is persisted", async ({
.getHomeChatInputContainer() .getHomeChatInputContainer()
.getByTestId("auxiliary-actions-menu") .getByTestId("auxiliary-actions-menu")
.click(); .click();
await po.page.getByRole("menuitem", { name: "Themes" }).hover(); await po.page.getByRole("menuitem", { name: "Themes" }).click();
await expect(po.page.getByTestId("theme-option-none")).toHaveClass( await expect(po.page.getByTestId("theme-option-none")).toHaveClass(
/bg-primary/, /bg-primary/,
); );
...@@ -48,7 +48,7 @@ test("theme selection - app-specific theme is persisted", async ({ po }) => { ...@@ -48,7 +48,7 @@ test("theme selection - app-specific theme is persisted", async ({ po }) => {
.getChatInputContainer() .getChatInputContainer()
.getByTestId("auxiliary-actions-menu") .getByTestId("auxiliary-actions-menu")
.click(); .click();
await po.page.getByRole("menuitem", { name: "Themes" }).hover(); await po.page.getByRole("menuitem", { name: "Themes" }).click();
await expect(po.page.getByTestId("theme-option-none")).toBeVisible(); await expect(po.page.getByTestId("theme-option-none")).toBeVisible();
await po.page.getByTestId("theme-option-default").click(); await po.page.getByTestId("theme-option-default").click();
await expect(po.page.getByTestId("theme-option-default")).not.toBeVisible(); await expect(po.page.getByTestId("theme-option-default")).not.toBeVisible();
...@@ -58,7 +58,7 @@ test("theme selection - app-specific theme is persisted", async ({ po }) => { ...@@ -58,7 +58,7 @@ test("theme selection - app-specific theme is persisted", async ({ po }) => {
.getChatInputContainer() .getChatInputContainer()
.getByTestId("auxiliary-actions-menu") .getByTestId("auxiliary-actions-menu")
.click(); .click();
await po.page.getByRole("menuitem", { name: "Themes" }).hover(); await po.page.getByRole("menuitem", { name: "Themes" }).click();
await expect(po.page.getByTestId("theme-option-default")).toHaveClass( await expect(po.page.getByTestId("theme-option-default")).toHaveClass(
/bg-primary/, /bg-primary/,
); );
...@@ -70,7 +70,7 @@ test("theme selection - app-specific theme is persisted", async ({ po }) => { ...@@ -70,7 +70,7 @@ test("theme selection - app-specific theme is persisted", async ({ po }) => {
.getChatInputContainer() .getChatInputContainer()
.getByTestId("auxiliary-actions-menu") .getByTestId("auxiliary-actions-menu")
.click(); .click();
await po.page.getByRole("menuitem", { name: "Themes" }).hover(); await po.page.getByRole("menuitem", { name: "Themes" }).click();
await expect(po.page.getByTestId("theme-option-none")).toHaveClass( await expect(po.page.getByTestId("theme-option-none")).toHaveClass(
/bg-primary/, /bg-primary/,
); );
......
...@@ -108,7 +108,7 @@ test("themes management - create theme from chat input", async ({ po }) => { ...@@ -108,7 +108,7 @@ test("themes management - create theme from chat input", async ({ po }) => {
.click(); .click();
// Hover over Themes submenu // Hover over Themes submenu
await po.page.getByRole("menuitem", { name: "Themes" }).hover(); await po.page.getByRole("menuitem", { name: "Themes" }).click();
// Click "New Theme" option // Click "New Theme" option
await po.page.getByRole("menuitem", { name: "New Theme" }).click(); await po.page.getByRole("menuitem", { name: "New Theme" }).click();
...@@ -142,7 +142,7 @@ test("themes management - create theme from chat input", async ({ po }) => { ...@@ -142,7 +142,7 @@ test("themes management - create theme from chat input", async ({ po }) => {
.getHomeChatInputContainer() .getHomeChatInputContainer()
.getByTestId("auxiliary-actions-menu") .getByTestId("auxiliary-actions-menu")
.click(); .click();
await po.page.getByRole("menuitem", { name: "Themes" }).hover(); await po.page.getByRole("menuitem", { name: "Themes" }).click();
// The custom theme should be visible and selected (has bg-primary class) // The custom theme should be visible and selected (has bg-primary class)
await expect(po.page.getByTestId("theme-option-custom:1")).toHaveClass( await expect(po.page.getByTestId("theme-option-custom:1")).toHaveClass(
...@@ -168,7 +168,7 @@ test("themes management - AI generator image upload limit", async ({ po }) => { ...@@ -168,7 +168,7 @@ test("themes management - AI generator image upload limit", async ({ po }) => {
// Verify AI-Powered Generator tab is active by default // Verify AI-Powered Generator tab is active by default
const aiTab = po.page.getByRole("tab", { name: "AI-Powered Generator" }); const aiTab = po.page.getByRole("tab", { name: "AI-Powered Generator" });
await expect(aiTab).toHaveAttribute("data-state", "active"); await expect(aiTab).toHaveAttribute("data-active", "");
// Verify upload area is visible // Verify upload area is visible
const uploadArea = po.page.getByText("Click to upload images"); const uploadArea = po.page.getByText("Click to upload images");
...@@ -223,7 +223,7 @@ test("themes management - AI generator flow", async ({ po }) => { ...@@ -223,7 +223,7 @@ test("themes management - AI generator flow", async ({ po }) => {
// Verify AI-Powered Generator tab is active by default // Verify AI-Powered Generator tab is active by default
const aiTab = po.page.getByRole("tab", { name: "AI-Powered Generator" }); const aiTab = po.page.getByRole("tab", { name: "AI-Powered Generator" });
await expect(aiTab).toHaveAttribute("data-state", "active"); await expect(aiTab).toHaveAttribute("data-active", "");
// Verify upload area is visible // Verify upload area is visible
const uploadArea = po.page.getByText("Click to upload images"); const uploadArea = po.page.getByText("Click to upload images");
...@@ -290,7 +290,7 @@ test("themes management - AI generator from website URL", async ({ po }) => { ...@@ -290,7 +290,7 @@ test("themes management - AI generator from website URL", async ({ po }) => {
// Verify AI-Powered Generator tab is active by default // Verify AI-Powered Generator tab is active by default
const aiTab = po.page.getByRole("tab", { name: "AI-Powered Generator" }); const aiTab = po.page.getByRole("tab", { name: "AI-Powered Generator" });
await expect(aiTab).toHaveAttribute("data-state", "active"); await expect(aiTab).toHaveAttribute("data-active", "");
// Switch to Website URL input source // Switch to Website URL input source
await po.page.getByRole("button", { name: "Website URL" }).click(); await po.page.getByRole("button", { name: "Website URL" }).click();
......
差异被折叠。
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
"@ai-sdk/provider-utils": "^4.0.13", "@ai-sdk/provider-utils": "^4.0.13",
"@ai-sdk/xai": "^3.0.46", "@ai-sdk/xai": "^3.0.46",
"@babel/parser": "^7.28.5", "@babel/parser": "^7.28.5",
"@base-ui/react": "^1.1.0",
"@biomejs/biome": "^1.9.4", "@biomejs/biome": "^1.9.4",
"@dyad-sh/supabase-management-js": "v1.0.1", "@dyad-sh/supabase-management-js": "v1.0.1",
"@lexical/react": "^0.33.1", "@lexical/react": "^0.33.1",
...@@ -62,22 +63,6 @@ ...@@ -62,22 +63,6 @@
"@monaco-editor/react": "^4.7.0-rc.0", "@monaco-editor/react": "^4.7.0-rc.0",
"@neondatabase/api-client": "^2.1.0", "@neondatabase/api-client": "^2.1.0",
"@neondatabase/serverless": "^1.0.1", "@neondatabase/serverless": "^1.0.1",
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-alert-dialog": "^1.1.13",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.0",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.1.3",
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",
"@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-commonjs": "^28.0.3",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.3", "@tailwindcss/vite": "^4.1.3",
......
...@@ -13,6 +13,7 @@ export function AutoApproveSwitch({ ...@@ -13,6 +13,7 @@ export function AutoApproveSwitch({
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch <Switch
id="auto-approve" id="auto-approve"
aria-label="Auto-approve"
checked={settings?.autoApproveChanges} checked={settings?.autoApproveChanges}
onCheckedChange={() => { onCheckedChange={() => {
updateSettings({ autoApproveChanges: !settings?.autoApproveChanges }); updateSettings({ autoApproveChanges: !settings?.autoApproveChanges });
......
...@@ -10,6 +10,7 @@ export function AutoExpandPreviewSwitch() { ...@@ -10,6 +10,7 @@ export function AutoExpandPreviewSwitch() {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch <Switch
id="auto-expand-preview" id="auto-expand-preview"
aria-label="Auto-expand preview panel"
checked={isEnabled} checked={isEnabled}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
updateSettings({ updateSettings({
......
...@@ -14,6 +14,7 @@ export function AutoFixProblemsSwitch({ ...@@ -14,6 +14,7 @@ export function AutoFixProblemsSwitch({
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch <Switch
id="auto-fix-problems" id="auto-fix-problems"
aria-label="Auto-fix problems"
checked={settings?.enableAutoFixProblems} checked={settings?.enableAutoFixProblems}
onCheckedChange={() => { onCheckedChange={() => {
updateSettings({ updateSettings({
......
...@@ -15,6 +15,7 @@ export function AutoUpdateSwitch() { ...@@ -15,6 +15,7 @@ export function AutoUpdateSwitch() {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch <Switch
id="enable-auto-update" id="enable-auto-update"
aria-label="Auto-update"
checked={settings.enableAutoUpdate} checked={settings.enableAutoUpdate}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
updateSettings({ enableAutoUpdate: checked }); updateSettings({ enableAutoUpdate: checked });
......
import { ipc } from "@/ipc/types"; import { ipc } from "@/ipc/types";
import { Dialog, DialogTitle } from "@radix-ui/react-dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog";
import { DialogContent, DialogHeader } from "./ui/dialog";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { BugIcon, Camera } from "lucide-react"; import { BugIcon, Camera } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
......
...@@ -19,7 +19,7 @@ import { ...@@ -19,7 +19,7 @@ import {
SidebarMenu, SidebarMenu,
SidebarMenuItem, SidebarMenuItem,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
...@@ -241,15 +241,15 @@ export function ChatList({ show }: { show?: boolean }) { ...@@ -241,15 +241,15 @@ export function ChatList({ show }: { show?: boolean }) {
modal={false} modal={false}
onOpenChange={(open) => setIsDropdownOpen(open)} onOpenChange={(open) => setIsDropdownOpen(open)}
> >
<DropdownMenuTrigger asChild> <DropdownMenuTrigger
<Button className={buttonVariants({
variant="ghost" variant: "ghost",
size="icon" size: "icon",
className="ml-1 w-4" className: "ml-1",
onClick={(e) => e.stopPropagation()} })}
> onClick={(e) => e.stopPropagation()}
<MoreVertical className="h-4 w-4" /> >
</Button> <MoreVertical className="h-4 w-4" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
......
...@@ -5,11 +5,6 @@ import { ...@@ -5,11 +5,6 @@ import {
SelectItem, SelectItem,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { useFreeAgentQuota } from "@/hooks/useFreeAgentQuota"; import { useFreeAgentQuota } from "@/hooks/useFreeAgentQuota";
import type { ChatMode } from "@/lib/schemas"; import type { ChatMode } from "@/lib/schemas";
...@@ -92,32 +87,24 @@ export function ChatModeSelector() { ...@@ -92,32 +87,24 @@ export function ChatModeSelector() {
const isMac = detectIsMac(); const isMac = detectIsMac();
return ( return (
<Select value={selectedMode} onValueChange={handleModeChange}> <Select
<Tooltip> value={selectedMode}
<TooltipTrigger asChild> onValueChange={(v) => v && handleModeChange(v)}
<MiniSelectTrigger >
data-testid="chat-mode-selector" <MiniSelectTrigger
className={cn( data-testid="chat-mode-selector"
"h-6 w-fit px-1.5 py-0 text-xs-sm font-medium shadow-none gap-0.5", title={`Open mode menu (${isMac ? "⌘ + ." : "Ctrl + ."} to toggle)`}
selectedMode === "build" || selectedMode === "local-agent" className={cn(
? "bg-background hover:bg-muted/50 focus:bg-muted/50" "h-6 w-fit px-1.5 py-0 text-xs-sm font-medium shadow-none gap-0.5",
: "bg-primary/10 hover:bg-primary/20 focus:bg-primary/20 text-primary border-primary/20 dark:bg-primary/20 dark:hover:bg-primary/30 dark:focus:bg-primary/30", selectedMode === "build" || selectedMode === "local-agent"
)} ? "bg-background hover:bg-muted/50 focus:bg-muted/50"
size="sm" : "bg-primary/10 hover:bg-primary/20 focus:bg-primary/20 text-primary border-primary/20 dark:bg-primary/20 dark:hover:bg-primary/30 dark:focus:bg-primary/30",
> )}
<SelectValue>{getModeDisplayName(selectedMode)}</SelectValue> size="sm"
</MiniSelectTrigger> >
</TooltipTrigger> <SelectValue>{getModeDisplayName(selectedMode)}</SelectValue>
<TooltipContent> </MiniSelectTrigger>
<div className="flex flex-col"> <SelectContent align="start">
<span>Open mode menu</span>
<span className="text-xs text-gray-200 dark:text-gray-500">
{isMac ? "⌘ + ." : "Ctrl + ."} to toggle
</span>
</div>
</TooltipContent>
</Tooltip>
<SelectContent align="start" onCloseAutoFocus={(e) => e.preventDefault()}>
{isProEnabled && ( {isProEnabled && (
<SelectItem value="local-agent"> <SelectItem value="local-agent">
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
......
...@@ -120,14 +120,12 @@ export function ContextFilesPicker() { ...@@ -120,14 +120,12 @@ export function ContextFilesPicker() {
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger
<div className="flex items-center py-2 px-3 hover:bg-accent hover:text-accent-foreground rounded-sm cursor-pointer text-sm"
className="flex items-center py-2 px-3 hover:bg-accent hover:text-accent-foreground rounded-sm cursor-pointer text-sm" data-testid="codebase-context-trigger"
data-testid="codebase-context-trigger" >
> <Settings2 className="size-4 mr-2" />
<Settings2 className="size-4 mr-2" /> Codebase context
Codebase context
</div>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-md max-h-[80vh] overflow-y-auto"> <DialogContent className="max-w-md max-h-[80vh] overflow-y-auto">
...@@ -138,8 +136,8 @@ export function ContextFilesPicker() { ...@@ -138,8 +136,8 @@ export function ContextFilesPicker() {
Select the files to use as context.{" "} Select the files to use as context.{" "}
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger className="cursor-help">
<InfoIcon className="size-4 cursor-help" /> <InfoIcon className="size-4" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="max-w-[300px]"> <TooltipContent className="max-w-[300px]">
{isSmartContextEnabled ? ( {isSmartContextEnabled ? (
...@@ -189,10 +187,8 @@ export function ContextFilesPicker() { ...@@ -189,10 +187,8 @@ export function ContextFilesPicker() {
> >
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger className="truncate font-mono text-sm text-left">
<span className="truncate font-mono text-sm"> {p.globPath}
{p.globPath}
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{p.globPath}</p> <p>{p.globPath}</p>
...@@ -234,8 +230,8 @@ export function ContextFilesPicker() { ...@@ -234,8 +230,8 @@ export function ContextFilesPicker() {
These files will be excluded from the context.{" "} These files will be excluded from the context.{" "}
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger className="cursor-help">
<InfoIcon className="size-4 cursor-help" /> <InfoIcon className="size-4" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="max-w-[300px]"> <TooltipContent className="max-w-[300px]">
<p> <p>
...@@ -281,10 +277,8 @@ export function ContextFilesPicker() { ...@@ -281,10 +277,8 @@ export function ContextFilesPicker() {
> >
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger className="truncate font-mono text-sm text-red-600 text-left">
<span className="truncate font-mono text-sm text-red-600"> {p.globPath}
{p.globPath}
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{p.globPath}</p> <p>{p.globPath}</p>
...@@ -320,8 +314,8 @@ export function ContextFilesPicker() { ...@@ -320,8 +314,8 @@ export function ContextFilesPicker() {
These files will always be included in the context.{" "} These files will always be included in the context.{" "}
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger className="cursor-help">
<InfoIcon className="size-4 cursor-help" /> <InfoIcon className="size-4" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="max-w-[300px]"> <TooltipContent className="max-w-[300px]">
<p> <p>
...@@ -368,10 +362,8 @@ export function ContextFilesPicker() { ...@@ -368,10 +362,8 @@ export function ContextFilesPicker() {
> >
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger className="truncate font-mono text-sm text-left">
<span className="truncate font-mono text-sm"> {p.globPath}
{p.globPath}
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{p.globPath}</p> <p>{p.globPath}</p>
......
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { import {
...@@ -11,11 +11,6 @@ import { ...@@ -11,11 +11,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Plus, Save, Edit2 } from "lucide-react"; import { Plus, Save, Edit2 } from "lucide-react";
interface CreateOrEditPromptDialogProps { interface CreateOrEditPromptDialogProps {
...@@ -166,30 +161,19 @@ export function CreateOrEditPromptDialog({ ...@@ -166,30 +161,19 @@ export function CreateOrEditPromptDialog({
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
{trigger ? ( {trigger ? (
<DialogTrigger asChild>{trigger}</DialogTrigger> <DialogTrigger>{trigger}</DialogTrigger>
) : mode === "create" ? ( ) : mode === "create" ? (
<DialogTrigger asChild> <DialogTrigger className={buttonVariants()}>
<Button> <Plus className="mr-2 h-4 w-4" /> New Prompt
<Plus className="mr-2 h-4 w-4" /> New Prompt
</Button>
</DialogTrigger> </DialogTrigger>
) : ( ) : (
<Tooltip> <DialogTrigger
<TooltipTrigger asChild> className={buttonVariants({ variant: "ghost", size: "icon" })}
<DialogTrigger asChild> data-testid="edit-prompt-button"
<Button title="Edit prompt"
size="icon" >
variant="ghost" <Edit2 className="h-4 w-4" />
data-testid="edit-prompt-button" </DialogTrigger>
>
<Edit2 className="h-4 w-4" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Edit prompt</p>
</TooltipContent>
</Tooltip>
)} )}
<DialogContent className="sm:max-w-[500px]"> <DialogContent className="sm:max-w-[500px]">
<DialogHeader> <DialogHeader>
......
...@@ -58,7 +58,7 @@ export function DefaultChatModeSelector() { ...@@ -58,7 +58,7 @@ export function DefaultChatModeSelector() {
</label> </label>
<Select <Select
value={effectiveDefault} value={effectiveDefault}
onValueChange={handleDefaultChatModeChange} onValueChange={(v) => v && handleDefaultChatModeChange(v)}
> >
<SelectTrigger className="w-40" id="default-chat-mode"> <SelectTrigger className="w-40" id="default-chat-mode">
<SelectValue>{getModeDisplayName(effectiveDefault)}</SelectValue> <SelectValue>{getModeDisplayName(effectiveDefault)}</SelectValue>
......
import React from "react"; import React from "react";
import { Button } from "@/components/ui/button";
import { Trash2, Loader2 } from "lucide-react"; import { Trash2, Loader2 } from "lucide-react";
import { import {
AlertDialog, AlertDialog,
...@@ -12,11 +11,7 @@ import { ...@@ -12,11 +11,7 @@ import {
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { import { buttonVariants } from "@/components/ui/button";
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface DeleteConfirmationDialogProps { interface DeleteConfirmationDialogProps {
itemName: string; itemName: string;
...@@ -36,25 +31,16 @@ export function DeleteConfirmationDialog({ ...@@ -36,25 +31,16 @@ export function DeleteConfirmationDialog({
return ( return (
<AlertDialog> <AlertDialog>
{trigger ? ( {trigger ? (
<AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger> <AlertDialogTrigger>{trigger}</AlertDialogTrigger>
) : ( ) : (
<Tooltip> <AlertDialogTrigger
<TooltipTrigger asChild> className={buttonVariants({ variant: "ghost", size: "icon" })}
<AlertDialogTrigger asChild> data-testid="delete-prompt-button"
<Button disabled={isDeleting}
size="icon" title={`Delete ${itemType.toLowerCase()}`}
variant="ghost" >
data-testid="delete-prompt-button" <Trash2 className="h-4 w-4" />
disabled={isDeleting} </AlertDialogTrigger>
>
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Delete {itemType.toLowerCase()}</p>
</TooltipContent>
</Tooltip>
)} )}
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
......
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { import {
...@@ -11,11 +12,6 @@ import { ...@@ -11,11 +12,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Save, Edit2, Loader2 } from "lucide-react"; import { Save, Edit2, Loader2 } from "lucide-react";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { toast } from "sonner"; import { toast } from "sonner";
...@@ -120,24 +116,15 @@ export function EditThemeDialog({ ...@@ -120,24 +116,15 @@ export function EditThemeDialog({
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
{trigger ? ( {trigger ? (
<DialogTrigger asChild>{trigger}</DialogTrigger> <span onClick={() => setOpen(true)}>{trigger}</span>
) : ( ) : (
<Tooltip> <DialogTrigger
<TooltipTrigger asChild> className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
<DialogTrigger asChild> data-testid="edit-theme-button"
<Button title="Edit theme"
size="icon" >
variant="ghost" <Edit2 className="h-4 w-4" />
data-testid="edit-theme-button" </DialogTrigger>
>
<Edit2 className="h-4 w-4" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Edit theme</p>
</TooltipContent>
</Tooltip>
)} )}
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto"> <DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
......
...@@ -39,8 +39,8 @@ export function ForceCloseDialog({ ...@@ -39,8 +39,8 @@ export function ForceCloseDialog({
<AlertTriangle className="h-5 w-5 text-yellow-500" /> <AlertTriangle className="h-5 w-5 text-yellow-500" />
<AlertDialogTitle>Force Close Detected</AlertDialogTitle> <AlertDialogTitle>Force Close Detected</AlertDialogTitle>
</div> </div>
<AlertDialogDescription asChild> <AlertDialogDescription render={<div />}>
<div className="space-y-4 pt-2"> <div className="space-y-4 pt-2 text-muted-foreground">
<div className="text-base"> <div className="text-base">
The app was not closed properly the last time it was running. The app was not closed properly the last time it was running.
This could indicate a crash or unexpected termination. This could indicate a crash or unexpected termination.
......
...@@ -997,7 +997,7 @@ export function UnconnectedGitHubConnector({ ...@@ -997,7 +997,7 @@ export function UnconnectedGitHubConnector({
</Label> </Label>
<Select <Select
value={selectedRepo} value={selectedRepo}
onValueChange={setSelectedRepo} onValueChange={(v) => setSelectedRepo(v ?? "")}
disabled={isLoadingRepos} disabled={isLoadingRepos}
> >
<SelectTrigger <SelectTrigger
...@@ -1037,7 +1037,7 @@ export function UnconnectedGitHubConnector({ ...@@ -1037,7 +1037,7 @@ export function UnconnectedGitHubConnector({
if (value === "custom") { if (value === "custom") {
setBranchInputMode("custom"); setBranchInputMode("custom");
setCustomBranchName(""); setCustomBranchName("");
} else { } else if (value) {
setBranchInputMode("select"); setBranchInputMode("select");
setSelectedBranch(value); setSelectedBranch(value);
} }
......
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
Select, Select,
...@@ -54,12 +55,6 @@ import { ...@@ -54,12 +55,6 @@ import {
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { showSuccess, showError, showInfo } from "@/lib/toast"; import { showSuccess, showError, showInfo } from "@/lib/toast";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { import {
Card, Card,
...@@ -388,7 +383,7 @@ export function GithubBranchManager({ ...@@ -388,7 +383,7 @@ export function GithubBranchManager({
<div className="flex gap-2"> <div className="flex gap-2">
<Select <Select
value={currentBranch || ""} value={currentBranch || ""}
onValueChange={handleSwitchBranch} onValueChange={(v) => v && handleSwitchBranch(v)}
disabled={ disabled={
isSwitching || isSwitching ||
isDeleting || isDeleting ||
...@@ -419,23 +414,14 @@ export function GithubBranchManager({ ...@@ -419,23 +414,14 @@ export function GithubBranchManager({
</Select> </Select>
<DropdownMenu> <DropdownMenu>
<TooltipProvider> <DropdownMenuTrigger
<Tooltip> className={cn(buttonVariants({ variant: "outline", size: "icon" }))}
<TooltipTrigger asChild> title="Branch actions"
<DropdownMenuTrigger asChild> aria-label="Branch actions"
<Button data-testid="branch-actions-menu-trigger"
variant="outline" >
size="icon" <EllipsisVertical className="h-4 w-4" />
title="Branch actions" </DropdownMenuTrigger>
data-testid="branch-actions-menu-trigger"
>
<EllipsisVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Branch actions</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem <DropdownMenuItem
onClick={() => setShowCreateDialog(true)} onClick={() => setShowCreateDialog(true)}
...@@ -487,7 +473,10 @@ export function GithubBranchManager({ ...@@ -487,7 +473,10 @@ export function GithubBranchManager({
</div> </div>
<div> <div>
<Label htmlFor="source-branch">Source Branch</Label> <Label htmlFor="source-branch">Source Branch</Label>
<Select value={sourceBranch} onValueChange={setSourceBranch}> <Select
value={sourceBranch}
onValueChange={(v) => setSourceBranch(v ?? "")}
>
<SelectTrigger <SelectTrigger
className="mt-2" className="mt-2"
data-testid="source-branch-select-trigger" data-testid="source-branch-select-trigger"
...@@ -790,15 +779,11 @@ export function GithubBranchManager({ ...@@ -790,15 +779,11 @@ export function GithubBranchManager({
if (open) setIsExpanded(true); if (open) setIsExpanded(true);
}} }}
> >
<DropdownMenuTrigger asChild> <DropdownMenuTrigger
<Button className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-6 w-6"
variant="ghost" data-testid={`branch-actions-${branch}`}
size="icon" >
className="h-6 w-6" <MoreHorizontal className="h-4 w-4" />
data-testid={`branch-actions-${branch}`}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem <DropdownMenuItem
......
...@@ -16,16 +16,11 @@ import { Input } from "@/components/ui/input"; ...@@ -16,16 +16,11 @@ import { Input } from "@/components/ui/input";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@radix-ui/react-label"; import { Label } from "@/components/ui/label";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { useStreamChat } from "@/hooks/useStreamChat"; import { useStreamChat } from "@/hooks/useStreamChat";
import type { GithubRepository } from "@/ipc/types"; import type { GithubRepository } from "@/ipc/types";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { useLoadApps } from "@/hooks/useLoadApps"; import { useLoadApps } from "@/hooks/useLoadApps";
...@@ -402,6 +397,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -402,6 +397,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
id="copy-to-dyad-apps" id="copy-to-dyad-apps"
aria-label="Copy to the dyad-apps folder"
checked={copyToDyadApps} checked={copyToDyadApps}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setCopyToDyadApps(checked === true) setCopyToDyadApps(checked === true)
...@@ -446,7 +442,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -446,7 +442,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</div> </div>
</div> </div>
<Accordion type="single" collapsible> <Accordion>
<AccordionItem value="advanced-options"> <AccordionItem value="advanced-options">
<AccordionTrigger className="text-xs sm:text-sm hover:no-underline"> <AccordionTrigger className="text-xs sm:text-sm hover:no-underline">
Advanced options Advanced options
...@@ -489,19 +485,12 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -489,19 +485,12 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
{hasAiRules === false && ( {hasAiRules === false && (
<Alert className="border-yellow-500/20 text-yellow-500 flex items-start gap-2"> <Alert className="border-yellow-500/20 text-yellow-500 flex items-start gap-2">
<TooltipProvider> <span
<Tooltip> title="AI_RULES.md lets Dyad know which tech stack to use for editing the app"
<TooltipTrigger asChild> className="flex-shrink-0 mt-1"
<Info className="h-4 w-4 flex-shrink-0 mt-1" /> >
</TooltipTrigger> <Info className="h-4 w-4" />
<TooltipContent> </span>
<p className="text-xs">
AI_RULES.md lets Dyad know which tech stack to
use for editing the app
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<AlertDescription className="text-xs sm:text-sm"> <AlertDescription className="text-xs sm:text-sm">
No AI_RULES.md found. Dyad will automatically generate No AI_RULES.md found. Dyad will automatically generate
one after importing. one after importing.
...@@ -622,7 +611,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -622,7 +611,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
{repos.length > 0 && ( {repos.length > 0 && (
<> <>
<Accordion type="single" collapsible> <Accordion>
<AccordionItem value="advanced-options"> <AccordionItem value="advanced-options">
<AccordionTrigger className="text-xs sm:text-sm hover:no-underline"> <AccordionTrigger className="text-xs sm:text-sm hover:no-underline">
Advanced options Advanced options
...@@ -705,7 +694,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -705,7 +694,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
)} )}
</div> </div>
<Accordion type="single" collapsible> <Accordion>
<AccordionItem value="advanced-options"> <AccordionItem value="advanced-options">
<AccordionTrigger className="text-xs sm:text-sm hover:no-underline"> <AccordionTrigger className="text-xs sm:text-sm hover:no-underline">
Advanced options Advanced options
......
...@@ -76,7 +76,10 @@ export const MaxChatTurnsSelector: React.FC = () => { ...@@ -76,7 +76,10 @@ export const MaxChatTurnsSelector: React.FC = () => {
> >
Maximum number of chat turns used in context Maximum number of chat turns used in context
</label> </label>
<Select value={currentValue} onValueChange={handleValueChange}> <Select
value={currentValue}
onValueChange={(v) => v && handleValueChange(v)}
>
<SelectTrigger className="w-[180px]" id="max-chat-turns"> <SelectTrigger className="w-[180px]" id="max-chat-turns">
<SelectValue placeholder="Select turns" /> <SelectValue placeholder="Select turns" />
</SelectTrigger> </SelectTrigger>
......
import React, { useState } from "react"; import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Wrench } from "lucide-react"; import { Wrench } from "lucide-react";
import { useMcp } from "@/hooks/useMcp"; import { useMcp } from "@/hooks/useMcp";
...@@ -31,23 +24,13 @@ export function McpToolsPicker() { ...@@ -31,23 +24,13 @@ export function McpToolsPicker() {
return ( return (
<Popover open={isOpen} onOpenChange={setIsOpen}> <Popover open={isOpen} onOpenChange={setIsOpen}>
<TooltipProvider> <PopoverTrigger
<Tooltip> className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-8 px-2"
<TooltipTrigger asChild> data-testid="mcp-tools-button"
<PopoverTrigger asChild> title="Tools"
<Button >
variant="outline" <Wrench className="size-4" />
className="has-[>svg]:px-2" </PopoverTrigger>
size="sm"
data-testid="mcp-tools-button"
>
<Wrench className="size-4" />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Tools</TooltipContent>
</Tooltip>
</TooltipProvider>
<PopoverContent <PopoverContent
className="w-120 max-h-[80vh] overflow-y-auto" className="w-120 max-h-[80vh] overflow-y-auto"
align="start" align="start"
......
...@@ -36,14 +36,10 @@ export function NeonConnector() { ...@@ -36,14 +36,10 @@ export function NeonConnector() {
onClick={() => { onClick={() => {
ipc.system.openExternalUrl("https://console.neon.tech/"); ipc.system.openExternalUrl("https://console.neon.tech/");
}} }}
className="ml-2 px-2 py-1 h-8 mb-2" className="ml-2 px-2 py-1 h-8 mb-2 inline-flex items-center gap-1"
style={{ display: "inline-flex", alignItems: "center" }}
asChild
> >
<div className="flex items-center gap-1"> Neon
Neon <ExternalLink className="h-3 w-3" />
<ExternalLink className="h-3 w-3" />
</div>
</Button> </Button>
</div> </div>
<p className="text-sm text-gray-500 dark:text-gray-400 pb-3"> <p className="text-sm text-gray-500 dark:text-gray-400 pb-3">
......
...@@ -27,11 +27,6 @@ import { ...@@ -27,11 +27,6 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { CreateCustomProviderDialog } from "./CreateCustomProviderDialog"; import { CreateCustomProviderDialog } from "./CreateCustomProviderDialog";
...@@ -128,34 +123,26 @@ export function ProviderSettingsGrid() { ...@@ -128,34 +123,26 @@ export function ProviderSettingsGrid() {
className="flex items-center justify-end" className="flex items-center justify-end"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<Tooltip> <Button
<TooltipTrigger asChild> data-testid="edit-custom-provider"
<Button variant="ghost"
data-testid="edit-custom-provider" size="sm"
variant="ghost" className="h-8 w-8 p-0 hover:bg-muted rounded-md"
size="sm" title="Edit Provider"
className="h-8 w-8 p-0 hover:bg-muted rounded-md" onClick={() => handleEditProvider(provider)}
onClick={() => handleEditProvider(provider)} >
> <Edit className="h-4 w-4" />
<Edit className="h-4 w-4" /> </Button>
</Button> <Button
</TooltipTrigger> data-testid="delete-custom-provider"
<TooltipContent>Edit Provider</TooltipContent> variant="ghost"
</Tooltip> size="sm"
<Tooltip> className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10 rounded-md"
<TooltipTrigger asChild> title="Delete Provider"
<Button onClick={() => setProviderToDelete(provider.id)}
data-testid="delete-custom-provider" >
variant="ghost" <Trash2 className="h-4 w-4" />
size="sm" </Button>
className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10 rounded-md"
onClick={() => setProviderToDelete(provider.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete Provider</TooltipContent>
</Tooltip>
</div> </div>
)} )}
<CardTitle className="text-lg font-medium mb-2"> <CardTitle className="text-lg font-medium mb-2">
......
...@@ -56,7 +56,7 @@ export function ReleaseChannelSelector() { ...@@ -56,7 +56,7 @@ export function ReleaseChannelSelector() {
</label> </label>
<Select <Select
value={settings.releaseChannel} value={settings.releaseChannel}
onValueChange={handleReleaseChannelChange} onValueChange={(v) => v && handleReleaseChannelChange(v)}
> >
<SelectTrigger className="w-32" id="release-channel"> <SelectTrigger className="w-32" id="release-channel">
<SelectValue /> <SelectValue />
......
...@@ -36,7 +36,7 @@ export function RuntimeModeSelector() { ...@@ -36,7 +36,7 @@ export function RuntimeModeSelector() {
</Label> </Label>
<Select <Select
value={settings.runtimeMode2 ?? "host"} value={settings.runtimeMode2 ?? "host"}
onValueChange={handleRuntimeModeChange} onValueChange={(v) => v && handleRuntimeModeChange(v)}
> >
<SelectTrigger className="w-48" id="runtime-mode"> <SelectTrigger className="w-48" id="runtime-mode">
<SelectValue /> <SelectValue />
......
import { DialogTitle } from "@radix-ui/react-dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog";
import { Dialog, DialogContent, DialogHeader } from "./ui/dialog";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { BugIcon } from "lucide-react"; import { BugIcon } from "lucide-react";
......
...@@ -187,11 +187,7 @@ export function SetupBanner() { ...@@ -187,11 +187,7 @@ export function SetupBanner() {
setIsVisible={setIsOnboardingVisible} setIsVisible={setIsOnboardingVisible}
/> />
<div className={bannerClasses}> <div className={bannerClasses}>
<Accordion <Accordion multiple className="w-full" defaultValue={itemsNeedAction}>
type="multiple"
className="w-full"
defaultValue={itemsNeedAction}
>
<AccordionItem <AccordionItem
value="node-setup" value="node-setup"
className={cn( className={cn(
......
...@@ -176,18 +176,14 @@ export function SupabaseConnector({ appId }: { appId: number }) { ...@@ -176,18 +176,14 @@ export function SupabaseConnector({ appId }: { appId: number }) {
`https://supabase.com/dashboard/project/${app.supabaseProjectId}`, `https://supabase.com/dashboard/project/${app.supabaseProjectId}`,
); );
}} }}
className="ml-2 px-2 py-1" className="ml-2 px-2 py-1 inline-flex items-center gap-2"
style={{ display: "inline-flex", alignItems: "center" }}
asChild
> >
<div className="flex items-center gap-2"> <img
<img src={isDarkMode ? supabaseLogoDark : supabaseLogoLight}
src={isDarkMode ? supabaseLogoDark : supabaseLogoLight} alt="Supabase Logo"
alt="Supabase Logo" style={{ height: 20, width: "auto", marginRight: 4 }}
style={{ height: 20, width: "auto", marginRight: 4 }} />
/> <ExternalLink className="h-4 w-4" />
<ExternalLink className="h-4 w-4" />
</div>
</Button> </Button>
</CardTitle> </CardTitle>
<CardDescription className="flex flex-col gap-1.5 text-sm"> <CardDescription className="flex flex-col gap-1.5 text-sm">
...@@ -363,7 +359,7 @@ export function SupabaseConnector({ appId }: { appId: number }) { ...@@ -363,7 +359,7 @@ export function SupabaseConnector({ appId }: { appId: number }) {
<Label htmlFor="project-select">Project</Label> <Label htmlFor="project-select">Project</Label>
<Select <Select
value={currentProjectValue} value={currentProjectValue}
onValueChange={handleProjectSelect} onValueChange={(v) => v && handleProjectSelect(v)}
> >
<SelectTrigger id="project-select"> <SelectTrigger id="project-select">
<SelectValue placeholder="Select a project" /> <SelectValue placeholder="Select a project" />
......
...@@ -138,6 +138,7 @@ export function SupabaseIntegration() { ...@@ -138,6 +138,7 @@ export function SupabaseIntegration() {
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<Switch <Switch
id="supabase-migrations" id="supabase-migrations"
aria-label="Write SQL migration files"
checked={!!settings?.enableSupabaseWriteSqlMigration} checked={!!settings?.enableSupabaseWriteSqlMigration}
onCheckedChange={handleMigrationSettingChange} onCheckedChange={handleMigrationSettingChange}
/> />
...@@ -162,6 +163,7 @@ export function SupabaseIntegration() { ...@@ -162,6 +163,7 @@ export function SupabaseIntegration() {
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<Switch <Switch
id="skip-prune-edge-functions" id="skip-prune-edge-functions"
aria-label="Keep extra Supabase edge functions"
checked={!!settings?.skipPruneEdgeFunctions} checked={!!settings?.skipPruneEdgeFunctions}
onCheckedChange={handleSkipPruneSettingChange} onCheckedChange={handleSkipPruneSettingChange}
/> />
......
...@@ -8,6 +8,7 @@ export function TelemetrySwitch() { ...@@ -8,6 +8,7 @@ export function TelemetrySwitch() {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch <Switch
id="telemetry-switch" id="telemetry-switch"
aria-label="Telemetry"
checked={settings?.telemetryConsent === "opted_in"} checked={settings?.telemetryConsent === "opted_in"}
onCheckedChange={() => { onCheckedChange={() => {
updateSettings({ updateSettings({
......
...@@ -59,7 +59,10 @@ export const ThinkingBudgetSelector: React.FC = () => { ...@@ -59,7 +59,10 @@ export const ThinkingBudgetSelector: React.FC = () => {
> >
Thinking Budget Thinking Budget
</label> </label>
<Select value={currentValue} onValueChange={handleValueChange}> <Select
value={currentValue}
onValueChange={(v) => v && handleValueChange(v)}
>
<SelectTrigger className="w-[180px]" id="thinking-budget"> <SelectTrigger className="w-[180px]" id="thinking-budget">
<SelectValue placeholder="Select budget" /> <SelectValue placeholder="Select budget" />
</SelectTrigger> </SelectTrigger>
......
...@@ -578,7 +578,7 @@ function UnconnectedVercelConnector({ ...@@ -578,7 +578,7 @@ function UnconnectedVercelConnector({
</Label> </Label>
<Select <Select
value={selectedProject} value={selectedProject}
onValueChange={setSelectedProject} onValueChange={(v) => setSelectedProject(v ?? "")}
disabled={isLoadingProjects} disabled={isLoadingProjects}
> >
<SelectTrigger <SelectTrigger
......
...@@ -201,32 +201,28 @@ function AppIcons({ ...@@ -201,32 +201,28 @@ function AppIcons({
return ( return (
<SidebarMenuItem key={item.title}> <SidebarMenuItem key={item.title}>
<SidebarMenuButton <SidebarMenuButton
asChild as={Link}
to={item.to}
size="sm" size="sm"
className="font-medium w-14" className={`font-medium w-14 flex flex-col items-center gap-1 h-14 mb-2 rounded-2xl ${
isActive ? "bg-sidebar-accent" : ""
}`}
onMouseEnter={() => {
if (item.title === "Apps") {
onHoverChange("start-hover:app");
} else if (item.title === "Chat") {
onHoverChange("start-hover:chat");
} else if (item.title === "Settings") {
onHoverChange("start-hover:settings");
} else if (item.title === "Library") {
onHoverChange("start-hover:library");
}
}}
> >
<Link <div className="flex flex-col items-center gap-1">
to={item.to} <item.icon className="h-5 w-5" />
className={`flex flex-col items-center gap-1 h-14 mb-2 rounded-2xl ${ <span className={"text-xs"}>{item.title}</span>
isActive ? "bg-sidebar-accent" : "" </div>
}`}
onMouseEnter={() => {
if (item.title === "Apps") {
onHoverChange("start-hover:app");
} else if (item.title === "Chat") {
onHoverChange("start-hover:chat");
} else if (item.title === "Settings") {
onHoverChange("start-hover:settings");
} else if (item.title === "Library") {
onHoverChange("start-hover:library");
}
}}
>
<div className="flex flex-col items-center gap-1">
<item.icon className="h-5 w-5" />
<span className={"text-xs"}>{item.title}</span>
</div>
</Link>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
); );
......
...@@ -3,12 +3,6 @@ import { Star } from "lucide-react"; ...@@ -3,12 +3,6 @@ import { Star } from "lucide-react";
import { SidebarMenuItem } from "@/components/ui/sidebar"; import { SidebarMenuItem } from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import type { ListedApp } from "@/ipc/types/app"; import type { ListedApp } from "@/ipc/types/app";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type AppItemProps = { type AppItemProps = {
app: ListedApp; app: ListedApp;
...@@ -27,56 +21,47 @@ export function AppItem({ ...@@ -27,56 +21,47 @@ export function AppItem({
}: AppItemProps) { }: AppItemProps) {
return ( return (
<SidebarMenuItem className="mb-1 relative "> <SidebarMenuItem className="mb-1 relative ">
<TooltipProvider> <div className="flex w-[190px] items-center" title={app.name}>
<Tooltip> <Button
<TooltipTrigger asChild> variant="ghost"
<div className="flex w-[190px] items-center"> onClick={() => handleAppClick(app.id)}
<Button className={`justify-start w-full text-left py-3 hover:bg-sidebar-accent/80 ${
variant="ghost" selectedAppId === app.id
onClick={() => handleAppClick(app.id)} ? "bg-sidebar-accent text-sidebar-accent-foreground"
className={`justify-start w-full text-left py-3 hover:bg-sidebar-accent/80 ${ : ""
selectedAppId === app.id }`}
? "bg-sidebar-accent text-sidebar-accent-foreground" data-testid={`app-list-item-${app.name}`}
: "" >
}`} <div className="flex flex-col w-4/5">
data-testid={`app-list-item-${app.name}`} <span className="truncate">{app.name}</span>
> <span className="text-xs text-gray-500">
<div className="flex flex-col w-4/5"> {formatDistanceToNow(new Date(app.createdAt), {
<span className="truncate">{app.name}</span> addSuffix: true,
<span className="text-xs text-gray-500"> })}
{formatDistanceToNow(new Date(app.createdAt), { </span>
addSuffix: true, </div>
})} </Button>
</span> <Button
</div> variant="ghost"
</Button> size="sm"
<Button onClick={(e) => handleToggleFavorite(app.id, e)}
variant="ghost" disabled={isFavoriteLoading}
size="sm" className="absolute top-1 right-1 p-1 mx-1 h-6 w-6 z-10"
onClick={(e) => handleToggleFavorite(app.id, e)} key={app.id}
disabled={isFavoriteLoading} data-testid="favorite-button"
className="absolute top-1 right-1 p-1 mx-1 h-6 w-6 z-10" >
key={app.id} <Star
data-testid="favorite-button" size={12}
> className={
<Star app.isFavorite
size={12} ? "fill-[#6c55dc] text-[#6c55dc]"
className={ : selectedAppId === app.id
app.isFavorite ? "hover:fill-black hover:text-black"
? "fill-[#6c55dc] text-[#6c55dc]" : "hover:fill-[#6c55dc] hover:stroke-[#6c55dc] hover:text-[#6c55dc]"
: selectedAppId === app.id }
? "hover:fill-black hover:text-black" />
: "hover:fill-[#6c55dc] hover:stroke-[#6c55dc] hover:text-[#6c55dc]" </Button>
} </div>
/>
</Button>
</div>
</TooltipTrigger>
<TooltipContent side="right">
<p>{app.name}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</SidebarMenuItem> </SidebarMenuItem>
); );
} }
...@@ -72,8 +72,8 @@ export function AgentConsentBanner({ ...@@ -72,8 +72,8 @@ export function AgentConsentBanner({
{toolDescription && ( {toolDescription && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger className="cursor-help">
<Info className="w-3.5 h-3.5 text-muted-foreground cursor-help" /> <Info className="w-3.5 h-3.5 text-muted-foreground" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" className="max-w-xs"> <TooltipContent side="top" className="max-w-xs">
<p className="text-xs">{toolDescription}</p> <p className="text-xs">{toolDescription}</p>
......
...@@ -26,12 +26,6 @@ import { ...@@ -26,12 +26,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { ContextFilesPicker } from "@/components/ContextFilesPicker"; import { ContextFilesPicker } from "@/components/ContextFilesPicker";
import { FileAttachmentDropdown } from "./FileAttachmentDropdown"; import { FileAttachmentDropdown } from "./FileAttachmentDropdown";
import { CustomThemeDialog } from "@/components/CustomThemeDialog"; import { CustomThemeDialog } from "@/components/CustomThemeDialog";
...@@ -137,18 +131,14 @@ export function AuxiliaryActionsMenu({ ...@@ -137,18 +131,14 @@ export function AuxiliaryActionsMenu({
return ( return (
<> <>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}> <DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger
<Button className="inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-muted bg-primary/10 text-primary cursor-pointer h-8 px-2"
variant="ghost" data-testid="auxiliary-actions-menu"
size="sm" >
className="has-[>svg]:px-2 hover:bg-muted bg-primary/10 text-primary cursor-pointer rounded-xl" <Plus
data-testid="auxiliary-actions-menu" size={20}
> className={`transition-transform duration-200 ${isOpen ? "rotate-45" : "rotate-0"}`}
<Plus />
size={20}
className={`transition-transform duration-200 ${isOpen ? "rotate-45" : "rotate-0"}`}
/>
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
{/* Codebase Context */} {/* Codebase Context */}
...@@ -193,31 +183,26 @@ export function AuxiliaryActionsMenu({ ...@@ -193,31 +183,26 @@ export function AuxiliaryActionsMenu({
{themes?.map((theme) => { {themes?.map((theme) => {
const isSelected = currentThemeId === theme.id; const isSelected = currentThemeId === theme.id;
return ( return (
<Tooltip key={theme.id}> <DropdownMenuItem
<TooltipTrigger asChild> key={theme.id}
<DropdownMenuItem onClick={() => handleThemeSelect(theme.id)}
onClick={() => handleThemeSelect(theme.id)} className={`py-2 px-3 ${isSelected ? "bg-primary/10" : ""}`}
className={`py-2 px-3 ${isSelected ? "bg-primary/10" : ""}`} data-testid={`theme-option-${theme.id}`}
data-testid={`theme-option-${theme.id}`} title={theme.description}
> >
<div className="flex items-center w-full"> <div className="flex items-center w-full">
{theme.icon === "palette" && ( {theme.icon === "palette" && (
<Palette <Palette
size={16} size={16}
className="mr-2 text-muted-foreground" className="mr-2 text-muted-foreground"
/> />
)} )}
<span className="flex-1">{theme.name}</span> <span className="flex-1">{theme.name}</span>
{isSelected && ( {isSelected && (
<Check size={16} className="text-primary ml-2" /> <Check size={16} className="text-primary ml-2" />
)} )}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{theme.description}
</TooltipContent>
</Tooltip>
); );
})} })}
...@@ -229,32 +214,24 @@ export function AuxiliaryActionsMenu({ ...@@ -229,32 +214,24 @@ export function AuxiliaryActionsMenu({
const themeId = `custom:${theme.id}`; const themeId = `custom:${theme.id}`;
const isSelected = currentThemeId === themeId; const isSelected = currentThemeId === themeId;
return ( return (
<Tooltip key={themeId}> <DropdownMenuItem
<TooltipTrigger asChild> key={themeId}
<DropdownMenuItem onClick={() => handleThemeSelect(themeId)}
onClick={() => handleThemeSelect(themeId)} className={`py-2 px-3 ${isSelected ? "bg-primary/10" : ""}`}
className={`py-2 px-3 ${isSelected ? "bg-primary/10" : ""}`} data-testid={`theme-option-${themeId}`}
data-testid={`theme-option-${themeId}`} title={theme.description || "Custom theme"}
> >
<div className="flex items-center w-full"> <div className="flex items-center w-full">
<Brush <Brush
size={16} size={16}
className="mr-2 text-muted-foreground" className="mr-2 text-muted-foreground"
/> />
<span className="flex-1">{theme.name}</span> <span className="flex-1">{theme.name}</span>
{isSelected && ( {isSelected && (
<Check <Check size={16} className="text-primary ml-2" />
size={16} )}
className="text-primary ml-2" </div>
/> </DropdownMenuItem>
)}
</div>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{theme.description || "Custom theme"}
</TooltipContent>
</Tooltip>
); );
})} })}
</> </>
......
...@@ -6,11 +6,6 @@ import { ...@@ -6,11 +6,6 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { ChatSummary } from "@/lib/schemas"; import type { ChatSummary } from "@/lib/schemas";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { import {
...@@ -32,24 +27,18 @@ export function ChatActivityButton() { ...@@ -32,24 +27,18 @@ export function ChatActivityButton() {
}, [isStreamingById]); }, [isStreamingById]);
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<Tooltip> <PopoverTrigger
<TooltipTrigger asChild> className="no-app-region-drag relative flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
<PopoverTrigger asChild> data-testid="chat-activity-button"
<button title="Recent chat activity"
className="no-app-region-drag relative flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors" >
data-testid="chat-activity-button" {isAnyStreaming && (
> <span className="pointer-events-none absolute inset-0 flex items-center justify-center">
{isAnyStreaming && ( <span className="block size-7 rounded-full border-3 border-blue-500/60 border-t-transparent animate-spin" />
<span className="pointer-events-none absolute inset-0 flex items-center justify-center"> </span>
<span className="block size-7 rounded-full border-3 border-blue-500/60 border-t-transparent animate-spin" /> )}
</span> <Bell size={16} />
)} </PopoverTrigger>
<Bell size={16} />
</button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Recent chat activity</TooltipContent>
</Tooltip>
<PopoverContent <PopoverContent
align="end" align="end"
className="w-80 p-0 max-h-[50vh] overflow-y-auto" className="w-80 p-0 max-h-[50vh] overflow-y-auto"
......
...@@ -119,7 +119,7 @@ export function ChatHeader({ ...@@ -119,7 +119,7 @@ export function ChatHeader({
<> <>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
{isAnyCheckoutVersionInProgress ? ( {isAnyCheckoutVersionInProgress ? (
<> <>
......
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论