fix(modal): autofocus description field on add feature modal open

- Add autoFocus prop to DescriptionImageDropZone component
- Enable autoFocus in board-view for add feature modal
- Ensure description textarea gets focus instead of prompt tab
- Also improve scrollbar visibility in git diff panels

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
This commit is contained in:
Cody Seibert
2025-12-10 12:33:19 -05:00
parent a8a63f4bed
commit adfa92fce2
8 changed files with 146 additions and 42 deletions

View File

@@ -1 +1,28 @@
[]
[
{
"id": "feature-1765387670653-bl83444lj",
"category": "Kanban",
"description": "In the output logs of the proc agent output in the file diffs Can you add a scroll bar so it actually scroll to see all these new styles right now it seems like I can't scroll",
"steps": [],
"status": "in_progress",
"startedAt": "2025-12-10T17:32:30.636Z",
"imagePaths": [],
"skipTests": true,
"summary": "Added always-visible scrollbar to file diffs in agent output modal. Created new 'scrollbar-visible' CSS utility class in globals.css with theme support for all themes (light, dark, retro, etc.). Applied scrollbar-visible class to: git-diff-panel.tsx (FileDiffSection expanded content), agent-output-modal.tsx (changes view container and logs/raw view container). Scrollbars now remain visible even on macOS where they normally auto-hide.",
"model": "opus",
"thinkingLevel": "none"
},
{
"id": "feature-1765387746902-na752mp1y",
"category": "Kanban",
"description": "When the add feature modal pops up, make sure that the description is always the main focus. When it first loads up. Do not focus the prompt tab, which is currently doing this.",
"steps": [],
"status": "waiting_approval",
"startedAt": "2025-12-10T17:29:13.854Z",
"imagePaths": [],
"skipTests": true,
"summary": "Added autoFocus prop to DescriptionImageDropZone component. Modified: description-image-dropzone.tsx (added autoFocus prop support), board-view.tsx (enabled autoFocus on add feature modal). Now the description textarea receives focus when the modal opens instead of the prompt tab.",
"model": "opus",
"thinkingLevel": "none"
}
]

View File

@@ -235,14 +235,16 @@ class AutoModeService {
// Update feature status based on result
// For skipTests features, go to waiting_approval on success instead of verified
// On failure, skipTests features should also go to waiting_approval for user review
// On failure, ALL features go to waiting_approval so user can review and decide next steps
// This prevents infinite retry loops when the same issue keeps failing
let newStatus;
if (result.passes) {
newStatus = feature.skipTests ? "waiting_approval" : "verified";
} else {
// For skipTests features, keep in waiting_approval so user can review
// For normal TDD features, move to backlog for retry
newStatus = feature.skipTests ? "waiting_approval" : "backlog";
// On failure, go to waiting_approval for user review
// Don't automatically move back to backlog to avoid infinite retry loops
// (especially when hitting rate limits or persistent errors)
newStatus = "waiting_approval";
}
await featureLoader.updateFeatureStatus(
feature.id,
@@ -507,11 +509,13 @@ class AutoModeService {
// Update feature status based on final result
// For skipTests features, go to waiting_approval on success instead of verified
// On failure, go to waiting_approval so user can review and decide next steps
let newStatus;
if (finalResult.passes) {
newStatus = feature.skipTests ? "waiting_approval" : "verified";
} else {
newStatus = "in_progress";
// On failure after all retry attempts, go to waiting_approval for user review
newStatus = "waiting_approval";
}
await featureLoader.updateFeatureStatus(
featureId,
@@ -691,14 +695,16 @@ class AutoModeService {
// Update feature status based on result
// For skipTests features, go to waiting_approval on success instead of verified
// On failure, skipTests features should also go to waiting_approval for user review
// On failure, ALL features go to waiting_approval so user can review and decide next steps
// This prevents infinite retry loops when the same issue keeps failing
let newStatus;
if (result.passes) {
newStatus = feature.skipTests ? "waiting_approval" : "verified";
} else {
// For skipTests features, keep in waiting_approval so user can review
// For normal TDD features, move to backlog for retry
newStatus = feature.skipTests ? "waiting_approval" : "backlog";
// On failure, go to waiting_approval for user review
// Don't automatically move back to backlog to avoid infinite retry loops
// (especially when hitting rate limits or persistent errors)
newStatus = "waiting_approval";
}
await featureLoader.updateFeatureStatus(
feature.id,
@@ -936,11 +942,12 @@ class AutoModeService {
);
// For skipTests features, go to waiting_approval on success instead of verified
// On failure, go to waiting_approval so user can review and decide next steps
const newStatus = result.passes
? feature.skipTests
? "waiting_approval"
: "verified"
: "in_progress";
: "waiting_approval";
await featureLoader.updateFeatureStatus(
feature.id,

View File

@@ -1039,6 +1039,62 @@
background: var(--background);
}
/* Always visible scrollbar for file diffs and code blocks */
.scrollbar-visible {
overflow-y: auto !important;
scrollbar-width: thin;
scrollbar-color: var(--muted-foreground) var(--muted);
}
.scrollbar-visible::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.scrollbar-visible::-webkit-scrollbar-track {
background: var(--muted);
border-radius: 4px;
}
.scrollbar-visible::-webkit-scrollbar-thumb {
background: var(--muted-foreground);
border-radius: 4px;
min-height: 40px;
}
.scrollbar-visible::-webkit-scrollbar-thumb:hover {
background: var(--foreground-secondary);
}
/* Force scrollbar to always be visible (not auto-hide) */
.scrollbar-visible::-webkit-scrollbar-thumb {
visibility: visible;
}
/* Light mode scrollbar-visible adjustments */
.light .scrollbar-visible::-webkit-scrollbar-track {
background: oklch(0.95 0 0);
}
.light .scrollbar-visible::-webkit-scrollbar-thumb {
background: oklch(0.7 0 0);
}
.light .scrollbar-visible::-webkit-scrollbar-thumb:hover {
background: oklch(0.6 0 0);
}
/* Retro mode scrollbar-visible adjustments */
.retro .scrollbar-visible::-webkit-scrollbar-thumb {
background: var(--primary);
border-radius: 0;
}
.retro .scrollbar-visible::-webkit-scrollbar-track {
background: var(--background);
border-radius: 0;
}
/* Glass morphism utilities */
@layer utilities {
.glass {

View File

@@ -30,6 +30,7 @@ interface DescriptionImageDropZoneProps {
// Optional: pass preview map from parent to persist across tab switches
previewMap?: ImagePreviewMap;
onPreviewMapChange?: (map: ImagePreviewMap) => void;
autoFocus?: boolean;
}
const ACCEPTED_IMAGE_TYPES = [
@@ -53,6 +54,7 @@ export function DescriptionImageDropZone({
maxFileSize = DEFAULT_MAX_FILE_SIZE,
previewMap,
onPreviewMapChange,
autoFocus = false,
}: DescriptionImageDropZoneProps) {
const [isDragOver, setIsDragOver] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
@@ -303,6 +305,7 @@ export function DescriptionImageDropZone({
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
autoFocus={autoFocus}
className={cn(
"min-h-[120px]",
isProcessing && "opacity-50 pointer-events-none"

View File

@@ -1,33 +1,33 @@
"use client"
"use client";
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
@@ -43,7 +43,7 @@ function DialogOverlay({
)}
{...props}
/>
)
);
}
function DialogContent({
@@ -53,9 +53,13 @@ function DialogContent({
compact = false,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
compact?: boolean
showCloseButton?: boolean;
compact?: boolean;
}) {
// Check if className contains a custom max-width
const hasCustomMaxWidth =
typeof className === "string" && className.includes("max-w-");
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
@@ -63,7 +67,11 @@ function DialogContent({
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 max-h-[calc(100vh-4rem)]",
compact ? "max-w-2xl p-4" : "sm:max-w-2xl p-6",
compact
? "max-w-4xl p-4"
: !hasCustomMaxWidth
? "sm:max-w-2xl p-6"
: "p-6",
className
)}
{...props}
@@ -83,7 +91,7 @@ function DialogContent({
)}
</DialogPrimitive.Content>
</DialogPortal>
)
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -93,7 +101,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -106,7 +114,7 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
)}
{...props}
/>
)
);
}
function DialogTitle({
@@ -119,7 +127,7 @@ function DialogTitle({
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
);
}
function DialogDescription({
@@ -132,7 +140,7 @@ function DialogDescription({
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
);
}
export {
@@ -146,4 +154,4 @@ export {
DialogPortal,
DialogTitle,
DialogTrigger,
}
};

View File

@@ -333,7 +333,7 @@ function FileDiffSection({
</div>
</button>
{isExpanded && (
<div className="bg-background border-t border-border max-h-[400px] overflow-y-auto">
<div className="bg-background border-t border-border max-h-[400px] overflow-y-auto scrollbar-visible">
{fileDiff.hunks.map((hunk, hunkIndex) => (
<div key={hunkIndex} className="border-b border-border-glass last:border-b-0">
{hunk.lines.map((line, lineIndex) => (
@@ -458,7 +458,7 @@ export function GitDiffPanel({
return (
<div
className={cn(
"rounded-xl border border-border bg-card backdrop-blur-sm overflow-hidden",
"rounded-xl border border-border bg-card backdrop-blur-sm overflow-hidden flex flex-col h-full",
className
)}
data-testid="git-diff-panel"
@@ -466,7 +466,7 @@ export function GitDiffPanel({
{/* Header */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full px-4 py-3 flex items-center justify-between bg-card hover:bg-accent/50 transition-colors text-left"
className="w-full px-4 py-3 flex items-center justify-between bg-card hover:bg-accent/50 transition-colors text-left flex-shrink-0"
data-testid="git-diff-panel-toggle"
>
<div className="flex items-center gap-2">
@@ -497,7 +497,7 @@ export function GitDiffPanel({
{/* Content */}
{isExpanded && (
<div className="border-t border-border">
<div className="border-t border-border flex-1 overflow-y-auto scrollbar-visible">
{isLoading ? (
<div className="flex items-center justify-center gap-2 py-8 text-muted-foreground">
<Loader2 className="w-5 h-5 animate-spin" />

View File

@@ -154,29 +154,31 @@ export function AgentOutputModal({
case "auto_mode_ultrathink_preparation":
// Format thinking level preparation information
let prepContent = `\n🧠 Ultrathink Preparation\n`;
if (event.warnings && event.warnings.length > 0) {
prepContent += `\n⚠ Warnings:\n`;
event.warnings.forEach((warning: string) => {
prepContent += `${warning}\n`;
});
}
if (event.recommendations && event.recommendations.length > 0) {
prepContent += `\n💡 Recommendations:\n`;
event.recommendations.forEach((rec: string) => {
prepContent += `${rec}\n`;
});
}
if (event.estimatedCost !== undefined) {
prepContent += `\n💰 Estimated Cost: ~$${event.estimatedCost.toFixed(2)} per execution\n`;
prepContent += `\n💰 Estimated Cost: ~$${event.estimatedCost.toFixed(
2
)} per execution\n`;
}
if (event.estimatedTime) {
prepContent += `\n⏱ Estimated Time: ${event.estimatedTime}\n`;
}
newContent = prepContent;
break;
case "auto_mode_feature_complete":
@@ -299,7 +301,7 @@ export function AgentOutputModal({
</DialogHeader>
{viewMode === "changes" ? (
<div className="flex-1 overflow-y-auto min-h-[400px] max-h-[60vh]">
<div className="flex-1 overflow-y-auto min-h-[400px] max-h-[60vh] scrollbar-visible">
{projectPath ? (
<GitDiffPanel
projectPath={projectPath}
@@ -320,7 +322,7 @@ export function AgentOutputModal({
<div
ref={scrollRef}
onScroll={handleScroll}
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs min-h-[400px] max-h-[60vh]"
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs min-h-[400px] max-h-[60vh] scrollbar-visible"
>
{isLoading && !output ? (
<div className="flex items-center justify-center h-full text-muted-foreground">

View File

@@ -1738,6 +1738,7 @@ export function BoardView() {
placeholder="Describe the feature..."
previewMap={newFeaturePreviewMap}
onPreviewMapChange={setNewFeaturePreviewMap}
autoFocus
/>
</div>
<div className="space-y-2">