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,6 +2,7 @@ ...@@ -2,6 +2,7 @@
- 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.
- button:
- img - img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
...@@ -9,6 +10,7 @@ ...@@ -9,6 +10,7 @@
- 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.
- button:
- img - img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
......
...@@ -2,18 +2,22 @@ ...@@ -2,18 +2,22 @@
- 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.
- button:
- img - 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.
- button:
- img - img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
- 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.
- button:
- img - img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
...@@ -9,6 +10,7 @@ ...@@ -9,6 +10,7 @@
- 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.
- button:
- img - img
- textbox "node_modules/**/*" - textbox "node_modules/**/*"
- button "Add" - button "Add"
......
...@@ -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.
- button:
- img - 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.
- button:
- img - 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.
- button:
- img - 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.
- button:
- img - 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,6 +2,7 @@ ...@@ -2,6 +2,7 @@
- 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.
- button:
- img - img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
...@@ -9,12 +10,14 @@ ...@@ -9,12 +10,14 @@
- 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.
- button:
- img - 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.
- button:
- img - img
- textbox "src/**/*.config.ts" - textbox "src/**/*.config.ts"
- button "Add" - button "Add"
......
...@@ -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.
- button:
- img - 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.
- button:
- img - 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.
- button:
- img - 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,6 +2,7 @@ ...@@ -2,6 +2,7 @@
- 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.
- button:
- img - img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
...@@ -9,12 +10,14 @@ ...@@ -9,12 +10,14 @@
- 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.
- button:
- img - 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.
- button:
- img - img
- textbox "src/**/*.config.ts" - textbox "src/**/*.config.ts"
- button "Add" - button "Add"
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
- 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.
- button:
- img - img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
...@@ -9,19 +10,23 @@ ...@@ -9,19 +10,23 @@
- 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.
- button:
- img - 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.
- button:
- img - 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,6 +2,7 @@ ...@@ -2,6 +2,7 @@
- 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.
- button:
- img - img
- textbox "src/**/*.tsx" - textbox "src/**/*.tsx"
- button "Add" - button "Add"
...@@ -9,12 +10,14 @@ ...@@ -9,12 +10,14 @@
- 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.
- button:
- img - 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.
- button:
- img - img
- textbox "src/**/*.config.ts" - textbox "src/**/*.config.ts"
- button "Add" - button "Add"
......
...@@ -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.
- button:
- img - 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.
- button:
- img - 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.
- button:
- img - 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" /> <MoreVertical className="h-4 w-4" />
</Button>
</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,11 +87,13 @@ export function ChatModeSelector() { ...@@ -92,11 +87,13 @@ 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 <MiniSelectTrigger
data-testid="chat-mode-selector" data-testid="chat-mode-selector"
title={`Open mode menu (${isMac ? "⌘ + ." : "Ctrl + ."} to toggle)`}
className={cn( className={cn(
"h-6 w-fit px-1.5 py-0 text-xs-sm font-medium shadow-none gap-0.5", "h-6 w-fit px-1.5 py-0 text-xs-sm font-medium shadow-none gap-0.5",
selectedMode === "build" || selectedMode === "local-agent" selectedMode === "build" || selectedMode === "local-agent"
...@@ -107,17 +104,7 @@ export function ChatModeSelector() { ...@@ -107,17 +104,7 @@ export function ChatModeSelector() {
> >
<SelectValue>{getModeDisplayName(selectedMode)}</SelectValue> <SelectValue>{getModeDisplayName(selectedMode)}</SelectValue>
</MiniSelectTrigger> </MiniSelectTrigger>
</TooltipTrigger> <SelectContent align="start">
<TooltipContent>
<div className="flex flex-col">
<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>
<Button
size="icon"
variant="ghost"
data-testid="edit-prompt-button" data-testid="edit-prompt-button"
title="Edit prompt"
> >
<Edit2 className="h-4 w-4" /> <Edit2 className="h-4 w-4" />
</Button>
</DialogTrigger> </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>
<Button
size="icon"
variant="ghost"
data-testid="delete-prompt-button" data-testid="delete-prompt-button"
disabled={isDeleting} disabled={isDeleting}
title={`Delete ${itemType.toLowerCase()}`}
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger> </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>
<Button
size="icon"
variant="ghost"
data-testid="edit-theme-button" data-testid="edit-theme-button"
title="Edit theme"
> >
<Edit2 className="h-4 w-4" /> <Edit2 className="h-4 w-4" />
</Button>
</DialogTrigger> </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>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
title="Branch actions" title="Branch actions"
aria-label="Branch actions"
data-testid="branch-actions-menu-trigger" data-testid="branch-actions-menu-trigger"
> >
<EllipsisVertical className="h-4 w-4" /> <EllipsisVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger> </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"
size="icon"
className="h-6 w-6"
data-testid={`branch-actions-${branch}`} data-testid={`branch-actions-${branch}`}
> >
<MoreHorizontal className="h-4 w-4" /> <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>
<PopoverTrigger asChild>
<Button
variant="outline"
className="has-[>svg]:px-2"
size="sm"
data-testid="mcp-tools-button" data-testid="mcp-tools-button"
title="Tools"
> >
<Wrench className="size-4" /> <Wrench className="size-4" />
</Button>
</PopoverTrigger> </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"
......
import { isDyadProEnabled, type LargeLanguageModel } from "@/lib/schemas"; import { isDyadProEnabled, type LargeLanguageModel } from "@/lib/schemas";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
...@@ -170,34 +165,20 @@ export function ModelPicker() { ...@@ -170,34 +165,20 @@ export function ModelPicker() {
return ( return (
<DropdownMenu open={open} onOpenChange={setOpen}> <DropdownMenu open={open} onOpenChange={setOpen}>
<Tooltip> <DropdownMenuTrigger
<TooltipTrigger asChild> 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 max-w-[130px] px-1.5 text-xs-sm gap-2"
<DropdownMenuTrigger asChild> title={modelDisplayName}
<Button
variant="outline"
size="sm"
className="flex items-center gap-2 h-8 max-w-[130px] px-1.5 text-xs-sm"
> >
<span className="truncate"> <span className="truncate">
{modelDisplayName === "Auto" && ( {modelDisplayName === "Auto" && (
<> <>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">Model:</span>{" "}
Model:
</span>{" "}
</> </>
)} )}
{modelDisplayName} {modelDisplayName}
</span> </span>
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
</TooltipTrigger> <DropdownMenuContent className="w-64" align="start">
<TooltipContent>{modelDisplayName}</TooltipContent>
</Tooltip>
<DropdownMenuContent
className="w-64"
align="start"
onCloseAutoFocus={(e) => e.preventDefault()}
>
<DropdownMenuLabel>Cloud Models</DropdownMenuLabel> <DropdownMenuLabel>Cloud Models</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
...@@ -259,9 +240,9 @@ export function ModelPicker() { ...@@ -259,9 +240,9 @@ export function ModelPicker() {
{autoModels.length > 0 && ( {autoModels.length > 0 && (
<> <>
{autoModels.map((model) => ( {autoModels.map((model) => (
<Tooltip key={`auto-${model.apiName}`}>
<TooltipTrigger asChild>
<DropdownMenuItem <DropdownMenuItem
key={`auto-${model.apiName}`}
title={model.description}
className={ className={
selectedModel.provider === "auto" && selectedModel.provider === "auto" &&
selectedModel.name === model.apiName selectedModel.name === model.apiName
...@@ -294,11 +275,6 @@ export function ModelPicker() { ...@@ -294,11 +275,6 @@ export function ModelPicker() {
</div> </div>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{model.description}
</TooltipContent>
</Tooltip>
))} ))}
{Object.keys(modelsByProviders).length > 1 && ( {Object.keys(modelsByProviders).length > 1 && (
<DropdownMenuSeparator /> <DropdownMenuSeparator />
...@@ -355,9 +331,9 @@ export function ModelPicker() { ...@@ -355,9 +331,9 @@ export function ModelPicker() {
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{models.map((model) => ( {models.map((model) => (
<Tooltip key={`${providerId}-${model.apiName}`}>
<TooltipTrigger asChild>
<DropdownMenuItem <DropdownMenuItem
key={`${providerId}-${model.apiName}`}
title={model.description}
className={ className={
selectedModel.provider === providerId && selectedModel.provider === providerId &&
selectedModel.name === model.apiName selectedModel.name === model.apiName
...@@ -366,9 +342,7 @@ export function ModelPicker() { ...@@ -366,9 +342,7 @@ export function ModelPicker() {
} }
onClick={() => { onClick={() => {
const customModelId = const customModelId =
model.type === "custom" model.type === "custom" ? model.id : undefined;
? model.id
: undefined;
onModelSelect({ onModelSelect({
name: model.apiName, name: model.apiName,
provider: providerId, provider: providerId,
...@@ -387,11 +361,6 @@ export function ModelPicker() { ...@@ -387,11 +361,6 @@ export function ModelPicker() {
)} )}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{model.description}
</TooltipContent>
</Tooltip>
))} ))}
</DropdownMenuSubContent> </DropdownMenuSubContent>
</DropdownMenuSub> </DropdownMenuSub>
...@@ -439,9 +408,9 @@ export function ModelPicker() { ...@@ -439,9 +408,9 @@ export function ModelPicker() {
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{models.map((model) => ( {models.map((model) => (
<Tooltip key={`${providerId}-${model.apiName}`}>
<TooltipTrigger asChild>
<DropdownMenuItem <DropdownMenuItem
key={`${providerId}-${model.apiName}`}
title={model.description}
className={ className={
selectedModel.provider === providerId && selectedModel.provider === providerId &&
selectedModel.name === model.apiName selectedModel.name === model.apiName
...@@ -470,11 +439,6 @@ export function ModelPicker() { ...@@ -470,11 +439,6 @@ export function ModelPicker() {
)} )}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{model.description}
</TooltipContent>
</Tooltip>
))} ))}
</DropdownMenuSubContent> </DropdownMenuSubContent>
</DropdownMenuSub> </DropdownMenuSub>
......
...@@ -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">
......
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
...@@ -62,21 +57,13 @@ export function ProModeSelector() { ...@@ -62,21 +57,13 @@ export function ProModeSelector() {
return ( return (
<Popover> <Popover>
<Tooltip> <PopoverTrigger
<TooltipTrigger asChild> 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-primary/50 bg-background shadow-sm hover:bg-primary/10 h-8 px-1.5 gap-1.5 shadow-primary/10 hover:shadow-md hover:shadow-primary/15"
<PopoverTrigger asChild> title="Configure Dyad Pro settings"
<Button
variant="outline"
size="sm"
className="has-[>svg]:px-1.5 flex items-center gap-1.5 h-8 border-primary/50 hover:bg-primary/10 font-medium shadow-sm shadow-primary/10 transition-all hover:shadow-md hover:shadow-primary/15"
> >
<Sparkles className="h-4 w-4 text-primary" /> <Sparkles className="h-4 w-4 text-primary" />
<span className="text-primary font-medium text-xs-sm">Pro</span> <span className="text-primary font-medium text-xs-sm">Pro</span>
</Button>
</PopoverTrigger> </PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Configure Dyad Pro settings</TooltipContent>
</Tooltip>
<PopoverContent className="w-80 border-primary/20"> <PopoverContent className="w-80 border-primary/20">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
...@@ -88,21 +75,15 @@ export function ProModeSelector() { ...@@ -88,21 +75,15 @@ export function ProModeSelector() {
</div> </div>
{!hasProKey && ( {!hasProKey && (
<div className="text-sm text-center text-muted-foreground"> <div className="text-sm text-center text-muted-foreground">
<Tooltip>
<TooltipTrigger asChild>
<a <a
className="inline-flex items-center justify-center gap-2 rounded-md border border-primary/30 bg-primary/10 px-3 py-2 text-sm font-medium text-primary shadow-sm transition-colors hover:bg-primary/20 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer" className="inline-flex items-center justify-center gap-2 rounded-md border border-primary/30 bg-primary/10 px-3 py-2 text-sm font-medium text-primary shadow-sm transition-colors hover:bg-primary/20 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
onClick={() => { onClick={() => {
ipc.system.openExternalUrl("https://dyad.sh/pro#ai"); ipc.system.openExternalUrl("https://dyad.sh/pro#ai");
}} }}
title="Visit dyad.sh/pro to unlock Pro features"
> >
Unlock Pro modes Unlock Pro modes
</a> </a>
</TooltipTrigger>
<TooltipContent>
Visit dyad.sh/pro to unlock Pro features
</TooltipContent>
</Tooltip>
</div> </div>
)} )}
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
...@@ -164,19 +145,15 @@ function SelectorRow({ ...@@ -164,19 +145,15 @@ function SelectorRow({
> >
{label} {label}
</Label> </Label>
<Tooltip> <span title={tooltip}>
<TooltipTrigger asChild>
<Info <Info
className={`h-4 w-4 cursor-help ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`} className={`h-4 w-4 cursor-help ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`}
/> />
</TooltipTrigger> </span>
<TooltipContent side="right" className="max-w-72">
{tooltip}
</TooltipContent>
</Tooltip>
</div> </div>
<Switch <Switch
id={id} id={id}
aria-label={label}
checked={isTogglable ? settingEnabled : false} checked={isTogglable ? settingEnabled : false}
onCheckedChange={toggle} onCheckedChange={toggle}
disabled={!isTogglable} disabled={!isTogglable}
...@@ -218,76 +195,46 @@ function TurboEditsSelector({ ...@@ -218,76 +195,46 @@ function TurboEditsSelector({
<Label className={!isTogglable ? "text-muted-foreground/50" : ""}> <Label className={!isTogglable ? "text-muted-foreground/50" : ""}>
Turbo Edits Turbo Edits
</Label> </Label>
<Tooltip> <span title="Edits files efficiently without full rewrites. Classic: Uses a smaller model to complete edits. Search & replace: Find and replaces specific text blocks.">
<TooltipTrigger asChild>
<Info <Info
className={`h-4 w-4 cursor-help ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`} className={`h-4 w-4 cursor-help ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`}
/> />
</TooltipTrigger> </span>
<TooltipContent side="right" className="max-w-72">
Edits files efficiently without full rewrites.
<br />
<ul className="list-disc ml-4">
<li>
<b>Classic:</b> Uses a smaller model to complete edits.
</li>
<li>
<b>Search & replace:</b> Find and replaces specific text blocks.
</li>
</ul>
</TooltipContent>
</Tooltip>
</div> </div>
<div <div
className="inline-flex rounded-md border border-input" className="inline-flex rounded-md border border-input"
data-testid="turbo-edits-selector" data-testid="turbo-edits-selector"
> >
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
variant={currentValue === "off" ? "default" : "ghost"} variant={currentValue === "off" ? "default" : "ghost"}
size="sm" size="sm"
onClick={() => onValueChange("off")} onClick={() => onValueChange("off")}
disabled={!isTogglable} disabled={!isTogglable}
className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0" className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
title="Disable Turbo Edits"
> >
Off Off
</Button> </Button>
</TooltipTrigger>
<TooltipContent>Disable Turbo Edits</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
variant={currentValue === "v1" ? "default" : "ghost"} variant={currentValue === "v1" ? "default" : "ghost"}
size="sm" size="sm"
onClick={() => onValueChange("v1")} onClick={() => onValueChange("v1")}
disabled={!isTogglable} disabled={!isTogglable}
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0" className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
title="Uses a smaller model to complete edits"
> >
Classic Classic
</Button> </Button>
</TooltipTrigger>
<TooltipContent>
Uses a smaller model to complete edits
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
variant={currentValue === "v2" ? "default" : "ghost"} variant={currentValue === "v2" ? "default" : "ghost"}
size="sm" size="sm"
onClick={() => onValueChange("v2")} onClick={() => onValueChange("v2")}
disabled={!isTogglable} disabled={!isTogglable}
className="rounded-l-none h-8 px-3 text-xs flex-shrink-0" className="rounded-l-none h-8 px-3 text-xs flex-shrink-0"
title="Find and replaces specific text blocks"
> >
Search & replace Search & replace
</Button> </Button>
</TooltipTrigger>
<TooltipContent>
Find and replaces specific text blocks
</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
); );
...@@ -325,69 +272,46 @@ function SmartContextSelector({ ...@@ -325,69 +272,46 @@ function SmartContextSelector({
<Label className={!isTogglable ? "text-muted-foreground/50" : ""}> <Label className={!isTogglable ? "text-muted-foreground/50" : ""}>
Smart Context Smart Context
</Label> </Label>
<Tooltip> <span title="Selects the most relevant files as context to save credits working on large codebases.">
<TooltipTrigger asChild>
<Info <Info
className={`h-4 w-4 cursor-help ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`} className={`h-4 w-4 cursor-help ${!isTogglable ? "text-muted-foreground/50" : "text-muted-foreground"}`}
/> />
</TooltipTrigger> </span>
<TooltipContent side="right" className="max-w-72">
Selects the most relevant files as context to save credits working
on large codebases.
</TooltipContent>
</Tooltip>
</div> </div>
<div <div
className="inline-flex rounded-md border border-input" className="inline-flex rounded-md border border-input"
data-testid="smart-context-selector" data-testid="smart-context-selector"
> >
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
variant={currentValue === "off" ? "default" : "ghost"} variant={currentValue === "off" ? "default" : "ghost"}
size="sm" size="sm"
onClick={() => onValueChange("off")} onClick={() => onValueChange("off")}
disabled={!isTogglable} disabled={!isTogglable}
className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0" className="rounded-r-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
title="Disable Smart Context"
> >
Off Off
</Button> </Button>
</TooltipTrigger>
<TooltipContent>Disable Smart Context</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
variant={currentValue === "balanced" ? "default" : "ghost"} variant={currentValue === "balanced" ? "default" : "ghost"}
size="sm" size="sm"
onClick={() => onValueChange("balanced")} onClick={() => onValueChange("balanced")}
disabled={!isTogglable} disabled={!isTogglable}
className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0" className="rounded-none border-r border-input h-8 px-3 text-xs flex-shrink-0"
title="Selects most relevant files with balanced context size"
> >
Balanced Balanced
</Button> </Button>
</TooltipTrigger>
<TooltipContent>
Selects most relevant files with balanced context size
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
variant={currentValue === "deep" ? "default" : "ghost"} variant={currentValue === "deep" ? "default" : "ghost"}
size="sm" size="sm"
onClick={() => onValueChange("deep")} onClick={() => onValueChange("deep")}
disabled={!isTogglable} disabled={!isTogglable}
className="rounded-l-none h-8 px-3 text-xs flex-shrink-0" className="rounded-l-none h-8 px-3 text-xs flex-shrink-0"
title="Experimental: Keeps full conversation history for maximum context and cache-optimized to control costs"
> >
Deep Deep
</Button> </Button>
</TooltipTrigger>
<TooltipContent>
<b>Experimental:</b> Keeps full conversation history for maximum
context and cache-optimized to control costs
</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
); );
......
...@@ -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>
<TooltipTrigger asChild>
<Button <Button
data-testid="edit-custom-provider" data-testid="edit-custom-provider"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-8 w-8 p-0 hover:bg-muted rounded-md" className="h-8 w-8 p-0 hover:bg-muted rounded-md"
title="Edit Provider"
onClick={() => handleEditProvider(provider)} onClick={() => handleEditProvider(provider)}
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent>Edit Provider</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
data-testid="delete-custom-provider" data-testid="delete-custom-provider"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10 rounded-md" className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10 rounded-md"
title="Delete Provider"
onClick={() => setProviderToDelete(provider.id)} onClick={() => setProviderToDelete(provider.id)}
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </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,13 +201,10 @@ function AppIcons({ ...@@ -201,13 +201,10 @@ function AppIcons({
return ( return (
<SidebarMenuItem key={item.title}> <SidebarMenuItem key={item.title}>
<SidebarMenuButton <SidebarMenuButton
asChild as={Link}
size="sm"
className="font-medium w-14"
>
<Link
to={item.to} to={item.to}
className={`flex flex-col items-center gap-1 h-14 mb-2 rounded-2xl ${ size="sm"
className={`font-medium w-14 flex flex-col items-center gap-1 h-14 mb-2 rounded-2xl ${
isActive ? "bg-sidebar-accent" : "" isActive ? "bg-sidebar-accent" : ""
}`} }`}
onMouseEnter={() => { onMouseEnter={() => {
...@@ -226,7 +223,6 @@ function AppIcons({ ...@@ -226,7 +223,6 @@ function AppIcons({
<item.icon className="h-5 w-5" /> <item.icon className="h-5 w-5" />
<span className={"text-xs"}>{item.title}</span> <span className={"text-xs"}>{item.title}</span>
</div> </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,10 +21,7 @@ export function AppItem({ ...@@ -27,10 +21,7 @@ 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>
<TooltipTrigger asChild>
<div className="flex w-[190px] items-center">
<Button <Button
variant="ghost" variant="ghost"
onClick={() => handleAppClick(app.id)} onClick={() => handleAppClick(app.id)}
...@@ -71,12 +62,6 @@ export function AppItem({ ...@@ -71,12 +62,6 @@ export function AppItem({
/> />
</Button> </Button>
</div> </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"
size="sm"
className="has-[>svg]:px-2 hover:bg-muted bg-primary/10 text-primary cursor-pointer rounded-xl"
data-testid="auxiliary-actions-menu" data-testid="auxiliary-actions-menu"
> >
<Plus <Plus
size={20} size={20}
className={`transition-transform duration-200 ${isOpen ? "rotate-45" : "rotate-0"}`} className={`transition-transform duration-200 ${isOpen ? "rotate-45" : "rotate-0"}`}
/> />
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
{/* Codebase Context */} {/* Codebase Context */}
...@@ -193,12 +183,12 @@ export function AuxiliaryActionsMenu({ ...@@ -193,12 +183,12 @@ 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}>
<TooltipTrigger asChild>
<DropdownMenuItem <DropdownMenuItem
key={theme.id}
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" && (
...@@ -213,11 +203,6 @@ export function AuxiliaryActionsMenu({ ...@@ -213,11 +203,6 @@ export function AuxiliaryActionsMenu({
)} )}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{theme.description}
</TooltipContent>
</Tooltip>
); );
})} })}
...@@ -229,12 +214,12 @@ export function AuxiliaryActionsMenu({ ...@@ -229,12 +214,12 @@ 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}>
<TooltipTrigger asChild>
<DropdownMenuItem <DropdownMenuItem
key={themeId}
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
...@@ -243,18 +228,10 @@ export function AuxiliaryActionsMenu({ ...@@ -243,18 +228,10 @@ export function AuxiliaryActionsMenu({
/> />
<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> </div>
</DropdownMenuItem> </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,12 +27,10 @@ export function ChatActivityButton() { ...@@ -32,12 +27,10 @@ export function ChatActivityButton() {
}, [isStreamingById]); }, [isStreamingById]);
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<Tooltip> <PopoverTrigger
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<button
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" 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" data-testid="chat-activity-button"
title="Recent chat activity"
> >
{isAnyStreaming && ( {isAnyStreaming && (
<span className="pointer-events-none absolute inset-0 flex items-center justify-center"> <span className="pointer-events-none absolute inset-0 flex items-center justify-center">
...@@ -45,11 +38,7 @@ export function ChatActivityButton() { ...@@ -45,11 +38,7 @@ export function ChatActivityButton() {
</span> </span>
)} )}
<Bell size={16} /> <Bell size={16} />
</button>
</PopoverTrigger> </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 ? (
<> <>
......
...@@ -48,12 +48,6 @@ import { AutoApproveSwitch } from "../AutoApproveSwitch"; ...@@ -48,12 +48,6 @@ import { AutoApproveSwitch } from "../AutoApproveSwitch";
import { usePostHog } from "posthog-js/react"; import { usePostHog } from "posthog-js/react";
import { CodeHighlight } from "./CodeHighlight"; import { CodeHighlight } from "./CodeHighlight";
import { TokenBar } from "./TokenBar"; import { TokenBar } from "./TokenBar";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import { useVersions } from "@/hooks/useVersions"; import { useVersions } from "@/hooks/useVersions";
import { useAttachments } from "@/hooks/useAttachments"; import { useAttachments } from "@/hooks/useAttachments";
...@@ -410,25 +404,16 @@ export function ChatInput({ chatId }: { chatId?: number }) { ...@@ -410,25 +404,16 @@ export function ChatInput({ chatId }: { chatId?: number }) {
) : ( ) : (
selectedComponents.length > 0 && ( selectedComponents.length > 0 && (
<div className="border-b border-border p-3 bg-muted/30"> <div className="border-b border-border p-3 bg-muted/30">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button <button
onClick={() => { onClick={() => {
ipc.system.openExternalUrl("https://dyad.sh/pro"); ipc.system.openExternalUrl("https://dyad.sh/pro");
}} }}
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-primary transition-colors cursor-pointer" className="flex items-center gap-2 text-sm text-muted-foreground hover:text-primary transition-colors cursor-pointer"
title="Visual editing lets you make UI changes without AI and is a Pro-only feature"
> >
<Lock size={16} /> <Lock size={16} />
<span className="font-medium">Visual editor (Pro)</span> <span className="font-medium">Visual editor (Pro)</span>
</button> </button>
</TooltipTrigger>
<TooltipContent>
Visual editing lets you make UI changes without AI and is
a Pro-only feature
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
) )
)} )}
...@@ -508,21 +493,15 @@ function SuggestionButton({ ...@@ -508,21 +493,15 @@ function SuggestionButton({
}) { }) {
const { isStreaming } = useStreamChat(); const { isStreaming } = useStreamChat();
return ( return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
disabled={isStreaming} disabled={isStreaming}
variant="outline" variant="outline"
size="sm" size="sm"
onClick={onClick} onClick={onClick}
title={tooltipText}
> >
{children} {children}
</Button> </Button>
</TooltipTrigger>
<TooltipContent>{tooltipText}</TooltipContent>
</Tooltip>
</TooltipProvider>
); );
} }
......
...@@ -21,12 +21,6 @@ import { useAtomValue } from "jotai"; ...@@ -21,12 +21,6 @@ import { useAtomValue } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { useCopyToClipboard } from "@/hooks/useCopyToClipboard";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
interface ChatMessageProps { interface ChatMessageProps {
message: Message; message: Message;
...@@ -128,12 +122,10 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => { ...@@ -128,12 +122,10 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
{message.role === "assistant" && {message.role === "assistant" &&
message.content && message.content &&
!isStreaming && ( !isStreaming && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button <button
data-testid="copy-message-button" data-testid="copy-message-button"
onClick={handleCopyFormatted} onClick={handleCopyFormatted}
title={copied ? "Copied!" : "Copy"}
className="flex items-center space-x-1 px-2 py-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors duration-200 cursor-pointer" className="flex items-center space-x-1 px-2 py-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors duration-200 cursor-pointer"
> >
{copied ? ( {copied ? (
...@@ -143,12 +135,6 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => { ...@@ -143,12 +135,6 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
)} )}
<span className="hidden sm:inline"></span> <span className="hidden sm:inline"></span>
</button> </button>
</TooltipTrigger>
<TooltipContent>
{copied ? "Copied!" : "Copy"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)} )}
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{message.approvalState && ( {message.approvalState && (
...@@ -187,25 +173,20 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => { ...@@ -187,25 +173,20 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<GitCommit className="h-3 w-3" /> <GitCommit className="h-3 w-3" />
{messageVersion && messageVersion.message && ( {messageVersion && messageVersion.message && (
<Tooltip> <span
<TooltipTrigger asChild> className="max-w-50 truncate font-medium"
<span className="max-w-50 truncate font-medium"> title={messageVersion.message}
>
{ {
messageVersion.message messageVersion.message
.replace(/^\[dyad\]\s*/i, "") .replace(/^\[dyad\]\s*/i, "")
.split("\n")[0] .split("\n")[0]
} }
</span> </span>
</TooltipTrigger>
<TooltipContent>{messageVersion.message}</TooltipContent>
</Tooltip>
)} )}
</div> </div>
)} )}
{message.requestId && ( {message.requestId && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button <button
onClick={() => { onClick={() => {
if (!message.requestId) return; if (!message.requestId) return;
...@@ -225,6 +206,11 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => { ...@@ -225,6 +206,11 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
// noop // noop
}); });
}} }}
title={
copiedRequestId
? "Copied!"
: `Copy Request ID: ${message.requestId.slice(0, 8)}...`
}
className="flex items-center space-x-1 px-1 py-0.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors duration-200 cursor-pointer" className="flex items-center space-x-1 px-1 py-0.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors duration-200 cursor-pointer"
> >
{copiedRequestId ? ( {copiedRequestId ? (
...@@ -236,28 +222,14 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => { ...@@ -236,28 +222,14 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
{copiedRequestId ? "Copied" : "Request ID"} {copiedRequestId ? "Copied" : "Request ID"}
</span> </span>
</button> </button>
</TooltipTrigger>
<TooltipContent>
{copiedRequestId
? "Copied!"
: `Copy Request ID: ${message.requestId.slice(0, 8)}...`}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)} )}
{isLastMessage && message.totalTokens && ( {isLastMessage && message.totalTokens && (
<TooltipProvider> <div
<Tooltip> className="flex items-center space-x-1 px-1 py-0.5"
<TooltipTrigger asChild> title={`Max tokens used: ${message.totalTokens.toLocaleString()}`}
<div className="flex items-center space-x-1 px-1 py-0.5"> >
<Info className="h-3 w-3" /> <Info className="h-3 w-3" />
</div> </div>
</TooltipTrigger>
<TooltipContent>
Max tokens used: {message.totalTokens.toLocaleString()}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)} )}
</div> </div>
)} )}
......
import { AlertTriangle, ArrowRight } from "lucide-react"; import { AlertTriangle, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useSummarizeInNewChat } from "./SummarizeInNewChatButton"; import { useSummarizeInNewChat } from "./SummarizeInNewChatButton";
const CONTEXT_LIMIT_THRESHOLD = 40_000; const CONTEXT_LIMIT_THRESHOLD = 40_000;
...@@ -44,33 +39,14 @@ export function ContextLimitBanner({ ...@@ -44,33 +39,14 @@ export function ContextLimitBanner({
data-testid="context-limit-banner" data-testid="context-limit-banner"
> >
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400"> <div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-5 w-5 p-0 hover:bg-transparent text-amber-600 dark:text-amber-400 cursor-help" className="h-5 w-5 p-0 hover:bg-transparent text-amber-600 dark:text-amber-400 cursor-help"
title={`Used: ${formatTokenCount(totalTokens)} / Limit: ${formatTokenCount(contextWindow)}`}
> >
<AlertTriangle className="h-4 w-4 shrink-0" /> <AlertTriangle className="h-4 w-4 shrink-0" />
</Button> </Button>
</TooltipTrigger>
<TooltipContent className="w-auto p-2 text-xs" side="top">
<div className="grid gap-1">
<div className="flex justify-between gap-4">
<span>Used:</span>
<span className="font-medium">
{formatTokenCount(totalTokens)}
</span>
</div>
<div className="flex justify-between gap-4">
<span>Limit:</span>
<span className="font-medium">
{formatTokenCount(contextWindow)}
</span>
</div>
</div>
</TooltipContent>
</Tooltip>
<p className="text-sm font-medium"> <p className="text-sm font-medium">
You're close to the context limit for this chat. You're close to the context limit for this chat.
</p> </p>
......
...@@ -16,7 +16,7 @@ export const DyadTokenSavings: React.FC<DyadTokenSavingsProps> = ({ ...@@ -16,7 +16,7 @@ export const DyadTokenSavings: React.FC<DyadTokenSavingsProps> = ({
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger>
<div className="bg-green-50 dark:bg-green-950 hover:bg-green-100 dark:hover:bg-green-900 rounded-lg px-4 py-2 border border-green-200 dark:border-green-800 my-2 cursor-pointer"> <div className="bg-green-50 dark:bg-green-950 hover:bg-green-100 dark:hover:bg-green-900 rounded-lg px-4 py-2 border border-green-200 dark:border-green-800 my-2 cursor-pointer">
<div className="flex items-center gap-2 text-green-700 dark:text-green-300"> <div className="flex items-center gap-2 text-green-700 dark:text-green-300">
<Zap size={16} className="text-green-600 dark:text-green-400" /> <Zap size={16} className="text-green-600 dark:text-green-400" />
......
import { MessageSquare, Upload } from "lucide-react"; import { MessageSquare, Upload } from "lucide-react";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useRef } from "react"; import { useRef } from "react";
interface FileAttachmentDropdownProps { interface FileAttachmentDropdownProps {
...@@ -46,49 +40,33 @@ export function FileAttachmentDropdown({ ...@@ -46,49 +40,33 @@ export function FileAttachmentDropdown({
const menuItems = ( const menuItems = (
<> <>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuItem <DropdownMenuItem
onSelect={(e) => { onClick={(e: React.MouseEvent) => {
// Prevent default so menu doesn't close in order to keep the hidden inputs in the DOM // Prevent default so menu doesn't close in order to keep the hidden inputs in the DOM
// Manually close menu after file selection // Manually close menu after file selection
e.preventDefault(); e.preventDefault();
handleChatContextClick(); handleChatContextClick();
}} }}
className="py-3 px-4" className="py-3 px-4"
title="Example use case: screenshot of the app to point out a UI issue"
> >
<MessageSquare size={16} className="mr-2" /> <MessageSquare size={16} className="mr-2" />
Attach file as chat context Attach file as chat context
</DropdownMenuItem> </DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
Example use case: screenshot of the app to point out a UI issue
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuItem <DropdownMenuItem
onSelect={(e) => { onClick={(e: React.MouseEvent) => {
// Prevent default so menu doesn't close in order to keep the hidden inputs in the DOM // Prevent default so menu doesn't close in order to keep the hidden inputs in the DOM
// Manually close menu after file selection // Manually close menu after file selection
e.preventDefault(); e.preventDefault();
handleUploadToCodebaseClick(); handleUploadToCodebaseClick();
}} }}
className="py-3 px-4" className="py-3 px-4"
title="Example use case: add an image to use for your app"
> >
<Upload size={16} className="mr-2" /> <Upload size={16} className="mr-2" />
Upload file to codebase Upload file to codebase
</DropdownMenuItem> </DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
Example use case: add an image to use for your app
</TooltipContent>
</Tooltip>
</TooltipProvider>
</> </>
); );
......
...@@ -133,7 +133,10 @@ function useScrambleText(text: string) { ...@@ -133,7 +133,10 @@ function useScrambleText(text: string) {
function ScrambleVerb({ verb }: { verb: string }) { function ScrambleVerb({ verb }: { verb: string }) {
const display = useScrambleText(verb); const display = useScrambleText(verb);
return ( return (
<span className="inline-block text-sm text-muted-foreground"> <span
className="inline-block text-sm text-muted-foreground"
aria-hidden="true"
>
{display} {display}
</span> </span>
); );
......
...@@ -54,7 +54,7 @@ export function TokenBar({ chatId }: TokenBarProps) { ...@@ -54,7 +54,7 @@ export function TokenBar({ chatId }: TokenBarProps) {
<div className="px-4 pb-2 text-xs" data-testid="token-bar"> <div className="px-4 pb-2 text-xs" data-testid="token-bar">
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger>
<div className="w-full"> <div className="w-full">
<div className="flex justify-between mb-1 text-xs text-muted-foreground"> <div className="flex justify-between mb-1 text-xs text-muted-foreground">
<span>Tokens: {totalTokens.toLocaleString()}</span> <span>Tokens: {totalTokens.toLocaleString()}</span>
......
...@@ -154,7 +154,7 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) { ...@@ -154,7 +154,7 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
Date.now() - timestampMs > 24 * 60 * 60 * 1000; Date.now() - timestampMs > 24 * 60 * 60 * 1000;
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger>
<div <div
className={cn( className={cn(
"inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded-md", "inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded-md",
...@@ -221,8 +221,6 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) { ...@@ -221,8 +221,6 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{/* Restore button */} {/* Restore button */}
<Tooltip>
<TooltipTrigger asChild>
<button <button
onClick={async (e) => { onClick={async (e) => {
e.stopPropagation(); e.stopPropagation();
...@@ -241,10 +239,14 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) { ...@@ -241,10 +239,14 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
className={cn( className={cn(
"invisible mt-1 flex items-center gap-1 px-2 py-0.5 text-sm font-medium bg-(--primary) text-(--primary-foreground) hover:bg-background-lightest rounded-md transition-colors", "invisible mt-1 flex items-center gap-1 px-2 py-0.5 text-sm font-medium bg-(--primary) text-(--primary-foreground) hover:bg-background-lightest rounded-md transition-colors",
selectedVersionId === version.oid && "visible", selectedVersionId === version.oid && "visible",
isRevertingVersion && isRevertingVersion && "opacity-50 cursor-not-allowed",
"opacity-50 cursor-not-allowed",
)} )}
aria-label="Restore to this version" aria-label="Restore to this version"
title={
isRevertingVersion
? "Restoring to this version..."
: "Restore to this version"
}
> >
{isRevertingVersion ? ( {isRevertingVersion ? (
<Loader2 size={12} className="animate-spin" /> <Loader2 size={12} className="animate-spin" />
...@@ -255,13 +257,6 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) { ...@@ -255,13 +257,6 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
{isRevertingVersion ? "Restoring..." : "Restore"} {isRevertingVersion ? "Restoring..." : "Restore"}
</span> </span>
</button> </button>
</TooltipTrigger>
<TooltipContent>
{isRevertingVersion
? "Restoring to this version..."
: "Restore to this version"}
</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -24,12 +24,6 @@ import { ...@@ -24,12 +24,6 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { showError, showSuccess } from "@/lib/toast"; import { showError, showSuccess } from "@/lib/toast";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { useCheckProblems } from "@/hooks/useCheckProblems"; import { useCheckProblems } from "@/hooks/useCheckProblems";
...@@ -175,12 +169,13 @@ export const ActionHeader = () => { ...@@ -175,12 +169,13 @@ export const ActionHeader = () => {
testId: string, testId: string,
badge?: React.ReactNode, badge?: React.ReactNode,
) => { ) => {
const buttonContent = ( return (
<button <button
data-testid={testId} data-testid={testId}
ref={ref} ref={ref}
className="no-app-region-drag cursor-pointer relative flex items-center gap-0.5 px-2 py-0.5 rounded-md text-xs font-medium z-10 hover:bg-[var(--background)] flex-col" className="no-app-region-drag cursor-pointer relative flex items-center gap-0.5 px-2 py-0.5 rounded-md text-xs font-medium z-10 hover:bg-[var(--background)] flex-col"
onClick={() => selectPanel(mode)} onClick={() => selectPanel(mode)}
title={isCompact ? text : undefined}
> >
{icon} {icon}
<span> <span>
...@@ -189,24 +184,10 @@ export const ActionHeader = () => { ...@@ -189,24 +184,10 @@ export const ActionHeader = () => {
</span> </span>
</button> </button>
); );
if (isCompact) {
return (
<Tooltip>
<TooltipTrigger asChild>{buttonContent}</TooltipTrigger>
<TooltipContent>
<p>{text}</p>
</TooltipContent>
</Tooltip>
);
}
return buttonContent;
}; };
const iconSize = 15; const iconSize = 15;
return ( return (
<TooltipProvider>
<div className="flex items-center justify-between px-1 py-2 mt-1 border-b border-border"> <div className="flex items-center justify-between px-1 py-2 mt-1 border-b border-border">
<div className="relative flex rounded-md p-0.5 gap-0.5"> <div className="relative flex rounded-md p-0.5 gap-0.5">
<motion.div <motion.div
...@@ -274,14 +255,12 @@ export const ActionHeader = () => { ...@@ -274,14 +255,12 @@ export const ActionHeader = () => {
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<ChatActivityButton /> <ChatActivityButton />
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger
<button
data-testid="preview-more-options-button" data-testid="preview-more-options-button"
className="no-app-region-drag flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors" className="no-app-region-drag flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
title="More options" title="More options"
> >
<MoreVertical size={16} /> <MoreVertical size={16} />
</button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-60"> <DropdownMenuContent align="end" className="w-60">
<DropdownMenuItem onClick={onCleanRestart}> <DropdownMenuItem onClick={onCleanRestart}>
...@@ -306,6 +285,5 @@ export const ActionHeader = () => { ...@@ -306,6 +285,5 @@ export const ActionHeader = () => {
</DropdownMenu> </DropdownMenu>
</div> </div>
</div> </div>
</TooltipProvider>
); );
}; };
...@@ -9,12 +9,6 @@ import { ...@@ -9,12 +9,6 @@ import {
X, X,
} from "lucide-react"; } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ToolbarColorPicker } from "./ToolbarColorPicker"; import { ToolbarColorPicker } from "./ToolbarColorPicker";
interface AnnotatorToolbarProps { interface AnnotatorToolbarProps {
...@@ -50,14 +44,12 @@ export const AnnotatorToolbar = ({ ...@@ -50,14 +44,12 @@ export const AnnotatorToolbar = ({
}: AnnotatorToolbarProps) => { }: AnnotatorToolbarProps) => {
return ( return (
<div className="flex items-center justify-center p-2 border-b space-x-2"> <div className="flex items-center justify-center p-2 border-b space-x-2">
<TooltipProvider>
{/* Tool Selection Buttons */} {/* Tool Selection Buttons */}
<div className="flex space-x-1"> <div className="flex space-x-1">
<Tooltip>
<TooltipTrigger asChild>
<button <button
onClick={() => onToolChange("select")} onClick={() => onToolChange("select")}
aria-label="Select" aria-label="Select"
title="Select"
className={cn( className={cn(
"p-1 rounded transition-colors duration-200", "p-1 rounded transition-colors duration-200",
tool === "select" tool === "select"
...@@ -67,17 +59,11 @@ export const AnnotatorToolbar = ({ ...@@ -67,17 +59,11 @@ export const AnnotatorToolbar = ({
> >
<MousePointer2 size={16} /> <MousePointer2 size={16} />
</button> </button>
</TooltipTrigger>
<TooltipContent>
<p>Select</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button <button
onClick={() => onToolChange("draw")} onClick={() => onToolChange("draw")}
aria-label="Draw" aria-label="Draw"
title="Draw"
className={cn( className={cn(
"p-1 rounded transition-colors duration-200", "p-1 rounded transition-colors duration-200",
tool === "draw" tool === "draw"
...@@ -87,17 +73,11 @@ export const AnnotatorToolbar = ({ ...@@ -87,17 +73,11 @@ export const AnnotatorToolbar = ({
> >
<Pencil size={16} /> <Pencil size={16} />
</button> </button>
</TooltipTrigger>
<TooltipContent>
<p>Draw</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button <button
onClick={() => onToolChange("text")} onClick={() => onToolChange("text")}
aria-label="Text" aria-label="Text"
title="Text"
className={cn( className={cn(
"p-1 rounded transition-colors duration-200", "p-1 rounded transition-colors duration-200",
tool === "text" tool === "text"
...@@ -107,108 +87,68 @@ export const AnnotatorToolbar = ({ ...@@ -107,108 +87,68 @@ export const AnnotatorToolbar = ({
> >
<Type size={16} /> <Type size={16} />
</button> </button>
</TooltipTrigger>
<TooltipContent>
<p>Text</p>
</TooltipContent>
</Tooltip>
<Tooltip> <div
<TooltipTrigger asChild> className="p-1 rounded transition-colors duration-200 hover:bg-purple-200 dark:hover:bg-purple-900"
<div className="p-1 rounded transition-colors duration-200 hover:bg-purple-200 dark:hover:bg-purple-900"> title="Color"
>
<ToolbarColorPicker color={color} onChange={onColorChange} /> <ToolbarColorPicker color={color} onChange={onColorChange} />
</div> </div>
</TooltipTrigger>
<TooltipContent>
<p>Color</p>
</TooltipContent>
</Tooltip>
<div className="w-px bg-gray-200 dark:bg-gray-700 h-4" /> <div className="w-px bg-gray-200 dark:bg-gray-700 h-4" />
<Tooltip>
<TooltipTrigger asChild>
<button <button
onClick={onDelete} onClick={onDelete}
aria-label="Delete" aria-label="Delete"
title="Delete Selected"
className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900 disabled:opacity-50 disabled:cursor-not-allowed" className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!selectedId} disabled={!selectedId}
> >
<Trash2 size={16} /> <Trash2 size={16} />
</button> </button>
</TooltipTrigger>
<TooltipContent>
<p>Delete Selected</p>
</TooltipContent>
</Tooltip>
<div className="w-px bg-gray-200 dark:bg-gray-700 h-4" /> <div className="w-px bg-gray-200 dark:bg-gray-700 h-4" />
<Tooltip>
<TooltipTrigger asChild>
<button <button
onClick={onUndo} onClick={onUndo}
aria-label="Undo" aria-label="Undo"
title="Undo"
className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900 disabled:opacity-50 disabled:cursor-not-allowed" className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={historyStep === 0} disabled={historyStep === 0}
> >
<Undo size={16} /> <Undo size={16} />
</button> </button>
</TooltipTrigger>
<TooltipContent>
<p>Undo</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button <button
onClick={onRedo} onClick={onRedo}
aria-label="Redo" aria-label="Redo"
title="Redo"
className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900 disabled:opacity-50 disabled:cursor-not-allowed" className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={historyStep === historyLength - 1} disabled={historyStep === historyLength - 1}
> >
<Redo size={16} /> <Redo size={16} />
</button> </button>
</TooltipTrigger>
<TooltipContent>
<p>Redo</p>
</TooltipContent>
</Tooltip>
<div className="w-px bg-gray-200 dark:bg-gray-700 h-4" /> <div className="w-px bg-gray-200 dark:bg-gray-700 h-4" />
<Tooltip>
<TooltipTrigger asChild>
<button <button
onClick={onSubmit} onClick={onSubmit}
aria-label="Add to Chat" aria-label="Add to Chat"
title="Add to Chat"
className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900 disabled:opacity-50 disabled:cursor-not-allowed" className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!hasSubmitHandler} disabled={!hasSubmitHandler}
> >
<Check size={16} /> <Check size={16} />
</button> </button>
</TooltipTrigger>
<TooltipContent>
<p>Add to Chat</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button <button
onClick={onDeactivate} onClick={onDeactivate}
aria-label="Close Annotator" aria-label="Close Annotator"
title="Close Annotator"
className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900" className="p-1 rounded transition-colors duration-200 text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900"
> >
<X size={16} /> <X size={16} />
</button> </button>
</TooltipTrigger>
<TooltipContent>
<p>Close Annotator</p>
</TooltipContent>
</Tooltip>
</div> </div>
</TooltipProvider>
</div> </div>
); );
}; };
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论