Sketch out approval UX

This commit is contained in:
Will Chen
2025-04-18 10:58:59 -07:00
parent aa36986771
commit 7aec3ef455
7 changed files with 531 additions and 18 deletions

291
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "dyad",
"version": "0.1.4",
"version": "0.1.5-beta.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dyad",
"version": "0.1.4",
"version": "0.1.5-beta.3",
"license": "MIT",
"dependencies": {
"@ai-sdk/anthropic": "^1.2.8",
@@ -18,9 +18,11 @@
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-dialog": "^1.1.7",
"@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-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.2.0",
"@radix-ui/react-toggle": "^1.1.3",
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",
@@ -4804,6 +4806,85 @@
}
}
},
"node_modules/@radix-ui/react-label": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.4.tgz",
"integrity": "sha512-wy3dqizZnZVV4ja0FNnUhIWNwWdoldXrneEyUcVtLYDAt8ovGS4ridtMAOGgXBBIfggL4BOveVWsjXDORdGEQg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz",
"integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.7.tgz",
@@ -5847,6 +5928,164 @@
}
}
},
"node_modules/@radix-ui/react-switch": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.0.tgz",
"integrity": "sha512-gbuQn/7CjA0uiLfZ+9WoSCfVeBkXXEhySJcL4EbhZZfDT9MztZ/DN/t9ciHflj0fxJlX5JU9UaYmm4/xnvtsMw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-use-controllable-state": "1.2.0",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz",
"integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.0.tgz",
"integrity": "sha512-w6W3VoOHxvYbmgI/IEs2rh6LSz1/OG6LJwDRcOXUICuiOXIqkmDY6bpJDE0IQr+tfuxfHreqXCerFqnAeEiGZQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.0",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-size": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.3.tgz",
@@ -6173,6 +6412,39 @@
}
}
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.0.tgz",
"integrity": "sha512-hPHasHVYa4dyHRSq9bZNQ97a36UH95EOBdbfasnTer2iqhx7GkwC0AHCiF3gG+Xe931op4dW3erhXfBhJuwuqg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
@@ -6206,6 +6478,21 @@
}
}
},
"node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-rect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",

View File

@@ -72,9 +72,11 @@
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-dialog": "^1.1.7",
"@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-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.2.0",
"@radix-ui/react-toggle": "^1.1.3",
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",

View File

@@ -1,4 +1,14 @@
import { SendIcon, StopCircleIcon, X } from "lucide-react";
import {
SendIcon,
StopCircleIcon,
X,
ChevronDown,
ChevronUp,
AlertTriangle,
AlertOctagon,
FileText,
Check,
} from "lucide-react";
import type React from "react";
import { useEffect, useRef, useState } from "react";
import { ModelPicker } from "@/components/ModelPicker";
@@ -10,6 +20,9 @@ import { useStreamChat } from "@/hooks/useStreamChat";
import { useChats } from "@/hooks/useChats";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useLoadApp } from "@/hooks/useLoadApp";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
interface ChatInputProps {
chatId?: number;
@@ -94,6 +107,7 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
)}
<div className="p-4">
<div className="flex flex-col space-y-2 border border-border rounded-lg bg-(--background-lighter) shadow-sm">
<ChatInputActions />
<div className="flex items-start space-x-2 ">
<textarea
ref={textareaRef}
@@ -136,3 +150,155 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
</>
);
}
function ChatInputActions() {
const [autoApprove, setAutoApprove] = useState(false);
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
const handleApprove = () => {
console.log("Approve clicked");
// Add approve logic here
};
const handleReject = () => {
console.log("Reject clicked");
// Add reject logic here
};
// Placeholder data
const securityRisks = [
{
type: "warning",
title: "Potential XSS Vulnerability",
description: "User input is directly rendered without sanitization.",
},
{
type: "danger",
title: "Hardcoded API Key",
description: "API key found in plain text in configuration file.",
},
];
const filesChanged = [
{
name: "ChatInput.tsx",
path: "src/components/chat/ChatInput.tsx",
summary: "Added review actions and details section.",
},
{
name: "api.ts",
path: "src/lib/api.ts",
summary: "Refactored API call structure.",
},
];
return (
<div className="border-b border-border">
<div className="p-2">
{/* Row 1: Title, Expand Icon, and Security Chip */}
<div className="flex items-center gap-2 mb-1">
<button
className="flex items-center text-left text-sm font-medium hover:bg-muted p-1 rounded justify-start"
onClick={() => setIsDetailsVisible(!isDetailsVisible)}
>
{isDetailsVisible ? (
<ChevronUp size={16} className="mr-1" />
) : (
<ChevronDown size={16} className="mr-1" />
)}
Review: foo bar changes
</button>
{securityRisks.length > 0 && (
<span className="bg-red-100 text-red-700 text-xs font-medium px-2 py-0.5 rounded-full">
Security risks found
</span>
)}
</div>
{/* Row 2: Buttons and Toggle */}
<div className="flex items-center justify-start space-x-2">
<Button
className="px-8"
size="sm"
variant="outline"
onClick={handleApprove}
>
<Check size={16} className="mr-1" />
Approve
</Button>
<Button
className="px-8"
size="sm"
variant="outline"
onClick={handleReject}
>
<X size={16} className="mr-1" />
Reject
</Button>
<div className="flex items-center space-x-1 ml-auto">
{/* Basic HTML checkbox styled to look like a toggle */}
<input
type="checkbox"
id="auto-approve"
checked={autoApprove}
onChange={(e) => setAutoApprove(e.target.checked)}
className="relative peer shrink-0 appearance-none w-8 h-4 border border-input rounded-full bg-input checked:bg-primary cursor-pointer after:absolute after:w-3 after:h-3 after:top-[1px] after:left-[2px] after:bg-background after:rounded-full after:transition-all checked:after:translate-x-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
/>
<label htmlFor="auto-approve" className="text-xs cursor-pointer">
Auto-approve
</label>
</div>
</div>
</div>
{isDetailsVisible && (
<div className="p-3 border-t border-border bg-muted/50 text-sm">
<div className="mb-3">
<h4 className="font-semibold mb-1">Security Risks</h4>
<ul className="space-y-1">
{securityRisks.map((risk, index) => (
<li key={index} className="flex items-start space-x-2">
{risk.type === "warning" ? (
<AlertTriangle
size={16}
className="text-yellow-500 mt-0.5 flex-shrink-0"
/>
) : (
<AlertOctagon
size={16}
className="text-red-500 mt-0.5 flex-shrink-0"
/>
)}
<div>
<span className="font-medium">{risk.title}:</span>{" "}
<span>{risk.description}</span>
</div>
</li>
))}
</ul>
</div>
<div>
<h4 className="font-semibold mb-1">Files Changed</h4>
<ul className="space-y-1">
{filesChanged.map((file, index) => (
<li key={index} className="flex items-center space-x-2">
<FileText
size={16}
className="text-muted-foreground flex-shrink-0"
/>
<span title={file.path} className="truncate cursor-default">
{file.name}
</span>
<span className="text-muted-foreground text-xs truncate">
- {file.summary}
</span>
</li>
))}
</ul>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@@ -0,0 +1,29 @@
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View File

@@ -266,6 +266,7 @@ export function registerChatStreamHandlers() {
.set({ content: fullResponse })
.where(eq(messages.id, assistantMessage.id));
if (readSettings().autoApproveChanges) {
const status = await processFullResponseActions(
fullResponse,
req.chatId,
@@ -285,6 +286,12 @@ export function registerChatStreamHandlers() {
updatedFiles: status.updatedFiles ?? false,
} satisfies ChatResponseEnd);
}
} else {
event.sender.send("chat:response:end", {
chatId: req.chatId,
updatedFiles: false,
} satisfies ChatResponseEnd);
}
// Return the chat ID for backwards compatibility
return req.chatId;

View File

@@ -88,7 +88,7 @@ export const UserSettingsSchema = z.object({
providerSettings: z.record(z.string(), ProviderSettingSchema),
githubUser: GithubUserSchema.optional(),
githubAccessToken: SecretSchema.optional(),
autoApproveChanges: z.boolean().optional(),
// DEPRECATED.
runtimeMode: RuntimeModeSchema.optional(),
});