Follow up area text scroll (vibe-kanban) (#1295)

* Done! The textarea now has a maximum height of 40% of the viewport and will scroll internally, keeping the Send and other action buttons always visible on screen.

* Done! I've restructured [TaskFollowUpSection.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/components/tasks/TaskFollowUpSection.tsx) using a CSS Grid layout:

**Changes:**
- Root container is now a grid with `grid-rows-[minmax(0,1fr)_auto]`
- **Scrollable content area** (row 1): Contains all content - images, review comments, conflicts, textarea, status row
- **Fixed action bar** (row 2): Contains the image button, variant selector, and all action buttons (Stop/Send/Queue)

**Result:**
The action buttons are now **always visible** at the bottom, regardless of how much content is above or screen size. The content area scrolls independently when it exceeds available space.

* Cleanup script changes for task attempt ccffbb1b-ff71-424d-9bea-44081e584026

* Done! I've fixed the parent containers in all three locations:

**Updated files:**
1. [frontend/src/pages/project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/pages/project-tasks.tsx#L969-L971)
2. [frontend/src/pages/full-attempt-logs.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/pages/full-attempt-logs.tsx#L48-L52) (first occurrence)
3. [frontend/src/pages/full-attempt-logs.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/pages/full-attempt-logs.tsx#L64-L68) (second occurrence)

**What changed:**
- Replaced `shrink-0` with `min-h-0 overflow-hidden` on the outer wrapper
- Added `h-full min-h-0` to the inner max-width wrapper

**Result:**
The follow-up section can now shrink within the flex layout, giving TaskFollowUpSection's grid a real height constraint. The scrollable content area will scroll when multiple sections expand, while the action buttons remain always visible at the bottom.

* Cleanup script changes for task attempt ccffbb1b-ff71-424d-9bea-44081e584026

* Done! I've implemented the 50/50 maximum split across all layouts:

**Updated files:**
1. [frontend/src/pages/project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/pages/project-tasks.tsx#L961) - 3-row grid: logs (1fr) + TodoPanel (auto) + follow-up (1fr)
2. [frontend/src/pages/full-attempt-logs.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/pages/full-attempt-logs.tsx#L43) - 2-row grid: logs (1fr) + follow-up (1fr)
3. [frontend/src/pages/full-attempt-logs.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/pages/full-attempt-logs.tsx#L61) - 2-row grid: logs (1fr) + follow-up (1fr)

**Result:**
- Logs always get at least 50% of available vertical space
- Follow-up area capped at maximum 50%
- Action buttons remain always visible within the follow-up area
- Clean grid layout that clearly expresses the 50/50 constraint

* Cleanup script changes for task attempt ccffbb1b-ff71-424d-9bea-44081e584026

* Done! I've converted all layouts from grid to flex:

**Updated files:**
1. [frontend/src/pages/project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/pages/project-tasks.tsx#L961) - flex layout with `max-h-[50%]` cap
2. [frontend/src/pages/full-attempt-logs.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/pages/full-attempt-logs.tsx#L44) - flex layout (first occurrence)
3. [frontend/src/pages/full-attempt-logs.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/pages/full-attempt-logs.tsx#L59) - flex layout (second occurrence)

**Result:**
- Logs use `flex-1` - fill remaining space
- TodoPanel uses `shrink-0` - natural size
- Follow-up uses `max-h-[50%]` - natural size up to 50% max
- Action buttons remain always visible
- Much clearer, more readable code that directly expresses the intent

* Cleanup script changes for task attempt ccffbb1b-ff71-424d-9bea-44081e584026

* Done! I've eliminated all nested scrolling:

**Changes made:**

1. [TaskFollowUpSection.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/components/tasks/TaskFollowUpSection.tsx#L497) - Removed `textareaClassName="max-h-[40vh] overflow-y-auto"` from textarea
2. [TaskFollowUpSection.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/components/tasks/TaskFollowUpSection.tsx#L456) - Removed `max-h-[40vh] overflow-y-auto` from review preview
3. [FollowUpEditorCard.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/components/tasks/follow-up/FollowUpEditorCard.tsx#L53) - Increased `maxRows` from 30 to 80

**Result:**
- Single scroll container - only the follow-up content area scrolls
- Textarea auto-grows naturally up to 80 rows
- Review preview expands fully
- Action buttons remain fixed at bottom
- Clean, intuitive UX with no nested scrollbars

* Fixed! The issue was that the icon was using `text-primary` color on a `secondary` variant button, which can have poor contrast.

**Change made:**
[TaskFollowUpSection.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/ccff-follow-up-area-t/frontend/src/components/tasks/TaskFollowUpSection.tsx#L526) - Changed button variant to `default` when active (images present or upload area shown) and `secondary` when inactive, removing the custom icon color class.

**Result:**
The icon is now always clearly visible - the button changes its entire variant style when toggled, providing better visual feedback and contrast.

* Cleanup script changes for task attempt ccffbb1b-ff71-424d-9bea-44081e584026
This commit is contained in:
Louis Knight-Webb
2025-11-15 12:19:06 +00:00
committed by GitHub
parent c49a008c80
commit 49840a05c3
4 changed files with 262 additions and 249 deletions

View File

@@ -413,260 +413,269 @@ export function TaskFollowUpSection({
selectedAttemptId && ( selectedAttemptId && (
<div <div
className={cn( className={cn(
'p-4 focus-within:ring ring-inset', 'grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto] overflow-hidden focus-within:ring ring-inset',
isRetryActive && 'opacity-50' isRetryActive && 'opacity-50'
)} )}
> >
<div className="space-y-2"> {/* Scrollable content area */}
{followUpError && ( <div className="overflow-y-auto min-h-0 p-4">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{followUpError}</AlertDescription>
</Alert>
)}
<div className="space-y-2"> <div className="space-y-2">
<div {followUpError && (
className={cn( <Alert variant="destructive">
'mb-2', <AlertCircle className="h-4 w-4" />
!showImageUpload && images.length === 0 && 'hidden' <AlertDescription>{followUpError}</AlertDescription>
</Alert>
)}
<div className="space-y-2">
<div
className={cn(
'mb-2',
!showImageUpload && images.length === 0 && 'hidden'
)}
>
<ImageUploadSection
ref={imageUploadRef}
images={images}
onImagesChange={setImages}
onUpload={(file) => imagesApi.uploadForTask(task.id, file)}
onDelete={imagesApi.delete}
onImageUploaded={(image) => {
handleImageUploaded(image);
setFollowUpMessage((prev) =>
appendImageMarkdown(prev, image)
);
}}
disabled={!isEditable}
collapsible={false}
defaultExpanded={true}
/>
</div>
{/* Review comments preview */}
{reviewMarkdown && (
<div className="mb-4">
<div className="text-sm whitespace-pre-wrap break-words rounded-md border bg-muted p-3">
{reviewMarkdown}
</div>
</div>
)} )}
>
<ImageUploadSection {/* Conflict notice and actions (optional UI) */}
ref={imageUploadRef} {branchStatus && (
images={images} <FollowUpConflictSection
onImagesChange={setImages} selectedAttemptId={selectedAttemptId}
onUpload={(file) => imagesApi.uploadForTask(task.id, file)} attemptBranch={attemptBranch}
onDelete={imagesApi.delete} branchStatus={branchStatus}
onImageUploaded={(image) => { isEditable={isEditable}
handleImageUploaded(image); onResolve={onSendFollowUp}
setFollowUpMessage((prev) => enableResolve={
appendImageMarkdown(prev, image) canSendFollowUp && !isAttemptRunning && isEditable
); }
}} enableAbort={canSendFollowUp && !isAttemptRunning}
conflictResolutionInstructions={
conflictResolutionInstructions
}
/>
)}
{/* Clicked elements notice and actions */}
<ClickedElementsBanner />
<div className="flex flex-col gap-2">
<FollowUpEditorCard
placeholder={
isQueued
? 'Type your follow-up… It will auto-send when ready.'
: reviewMarkdown || conflictResolutionInstructions
? '(Optional) Add additional instructions... Type @ to insert tags or search files.'
: 'Continue working on this task attempt... Type @ to insert tags or search files.'
}
value={followUpMessage}
onChange={(value) => {
setFollowUpMessage(value);
if (followUpError) setFollowUpError(null);
}}
disabled={!isEditable}
showLoadingOverlay={isUnqueuing || !isDraftLoaded}
onPasteFiles={handlePasteImages}
onFocusChange={setIsTextareaFocused}
/>
<FollowUpStatusRow
status={{
save: { state: saveStatus, isSaving },
draft: {
isLoaded: isDraftLoaded,
isSending: !!draft?.sending,
},
queue: {
isUnqueuing: isUnqueuing,
isQueued: displayQueued,
},
}}
/>
</div>
</div>
</div>
</div>
{/* Always-visible action bar */}
<div className="border-t bg-background p-4">
<div className="flex flex-row gap-2 items-center">
<div className="flex-1 flex gap-2">
{/* Image button */}
<Button
variant={
images.length > 0 || showImageUpload ? 'default' : 'secondary'
}
size="sm"
onClick={() => setShowImageUpload(!showImageUpload)}
disabled={!isEditable}
>
<ImageIcon className="h-4 w-4" />
</Button>
<VariantSelector
currentProfile={currentProfile}
selectedVariant={selectedVariant}
onChange={setSelectedVariant}
disabled={!isEditable} disabled={!isEditable}
collapsible={false}
defaultExpanded={true}
/> />
</div> </div>
{/* Review comments preview */} {isAttemptRunning ? (
{reviewMarkdown && ( <Button
<div className="mb-4"> onClick={stopExecution}
<div className="text-sm whitespace-pre-wrap break-words max-h-[40vh] overflow-y-auto rounded-md border bg-muted p-3"> disabled={isStopping}
{reviewMarkdown} size="sm"
</div> variant="destructive"
</div> >
)} {isStopping ? (
<Loader2 className="animate-spin h-4 w-4 mr-2" />
{/* Conflict notice and actions (optional UI) */} ) : (
{branchStatus && ( <>
<FollowUpConflictSection <StopCircle className="h-4 w-4 mr-2" />
selectedAttemptId={selectedAttemptId} {t('followUp.stop')}
attemptBranch={attemptBranch} </>
branchStatus={branchStatus} )}
isEditable={isEditable} </Button>
onResolve={onSendFollowUp} ) : (
enableResolve={ <div className="flex items-center gap-2">
canSendFollowUp && !isAttemptRunning && isEditable {comments.length > 0 && (
}
enableAbort={canSendFollowUp && !isAttemptRunning}
conflictResolutionInstructions={conflictResolutionInstructions}
/>
)}
{/* Clicked elements notice and actions */}
<ClickedElementsBanner />
<div className="flex flex-col gap-2">
<FollowUpEditorCard
placeholder={
isQueued
? 'Type your follow-up… It will auto-send when ready.'
: reviewMarkdown || conflictResolutionInstructions
? '(Optional) Add additional instructions... Type @ to insert tags or search files.'
: 'Continue working on this task attempt... Type @ to insert tags or search files.'
}
value={followUpMessage}
onChange={(value) => {
setFollowUpMessage(value);
if (followUpError) setFollowUpError(null);
}}
disabled={!isEditable}
showLoadingOverlay={isUnqueuing || !isDraftLoaded}
onPasteFiles={handlePasteImages}
onFocusChange={setIsTextareaFocused}
/>
<FollowUpStatusRow
status={{
save: { state: saveStatus, isSaving },
draft: {
isLoaded: isDraftLoaded,
isSending: !!draft?.sending,
},
queue: { isUnqueuing: isUnqueuing, isQueued: displayQueued },
}}
/>
<div className="flex flex-row gap-2 items-center">
<div className="flex-1 flex gap-2">
{/* Image button */}
<Button <Button
variant="secondary" onClick={clearComments}
size="sm"
onClick={() => setShowImageUpload(!showImageUpload)}
disabled={!isEditable}
>
<ImageIcon
className={cn(
'h-4 w-4',
(images.length > 0 || showImageUpload) && 'text-primary'
)}
/>
</Button>
<VariantSelector
currentProfile={currentProfile}
selectedVariant={selectedVariant}
onChange={setSelectedVariant}
disabled={!isEditable}
/>
</div>
{isAttemptRunning ? (
<Button
onClick={stopExecution}
disabled={isStopping}
size="sm" size="sm"
variant="destructive" variant="destructive"
disabled={!isEditable}
> >
{isStopping ? ( {t('followUp.clearReviewComments')}
<Loader2 className="animate-spin h-4 w-4 mr-2" /> </Button>
) : ( )}
<Button
onClick={onSendFollowUp}
disabled={
!canSendFollowUp ||
isDraftLocked ||
!isDraftLoaded ||
isSendingFollowUp ||
isRetryActive
}
size="sm"
>
{isSendingFollowUp ? (
<Loader2 className="animate-spin h-4 w-4 mr-2" />
) : (
<>
<Send className="h-4 w-4 mr-2" />
{conflictResolutionInstructions
? t('followUp.resolveConflicts')
: t('followUp.send')}
</>
)}
</Button>
{isQueued && (
<Button
variant="default"
size="sm"
className="min-w-[180px] transition-all"
onClick={async () => {
setIsUnqueuing(true);
try {
const ok = await onUnqueue();
if (ok) setQueuedOptimistic(false);
} finally {
setIsUnqueuing(false);
}
}}
disabled={isUnqueuing}
>
{isUnqueuing ? (
<> <>
<StopCircle className="h-4 w-4 mr-2" /> <Loader2 className="animate-spin h-4 w-4 mr-2" />
{t('followUp.stop')} {t('followUp.unqueuing')}
</> </>
) : (
t('followUp.edit')
)} )}
</Button> </Button>
) : (
<div className="flex items-center gap-2">
{comments.length > 0 && (
<Button
onClick={clearComments}
size="sm"
variant="destructive"
disabled={!isEditable}
>
{t('followUp.clearReviewComments')}
</Button>
)}
<Button
onClick={onSendFollowUp}
disabled={
!canSendFollowUp ||
isDraftLocked ||
!isDraftLoaded ||
isSendingFollowUp ||
isRetryActive
}
size="sm"
>
{isSendingFollowUp ? (
<Loader2 className="animate-spin h-4 w-4 mr-2" />
) : (
<>
<Send className="h-4 w-4 mr-2" />
{conflictResolutionInstructions
? t('followUp.resolveConflicts')
: t('followUp.send')}
</>
)}
</Button>
{isQueued && (
<Button
variant="default"
size="sm"
className="min-w-[180px] transition-all"
onClick={async () => {
setIsUnqueuing(true);
try {
const ok = await onUnqueue();
if (ok) setQueuedOptimistic(false);
} finally {
setIsUnqueuing(false);
}
}}
disabled={isUnqueuing}
>
{isUnqueuing ? (
<>
<Loader2 className="animate-spin h-4 w-4 mr-2" />
{t('followUp.unqueuing')}
</>
) : (
t('followUp.edit')
)}
</Button>
)}
</div>
)}
{isAttemptRunning && (
<div className="flex items-center gap-2">
<Button
onClick={async () => {
if (displayQueued) {
setIsUnqueuing(true);
try {
const ok = await onUnqueue();
if (ok) setQueuedOptimistic(false);
} finally {
setIsUnqueuing(false);
}
} else {
setIsQueuing(true);
try {
const ok = await onQueue();
if (ok) setQueuedOptimistic(true);
} finally {
setIsQueuing(false);
}
}
}}
disabled={
displayQueued
? isUnqueuing
: !canSendFollowUp ||
!isDraftLoaded ||
isQueuing ||
isUnqueuing ||
!!draft?.sending ||
isRetryActive
}
size="sm"
variant="default"
className="md:min-w-[180px] transition-all"
>
{displayQueued ? (
isUnqueuing ? (
<>
<Loader2 className="animate-spin h-4 w-4 mr-2" />
{t('followUp.unqueuing')}
</>
) : (
t('followUp.edit')
)
) : isQueuing ? (
<>
<Loader2 className="animate-spin h-4 w-4 mr-2" />
{t('followUp.queuing')}
</>
) : (
t('followUp.queueForNextTurn')
)}
</Button>
</div>
)} )}
</div> </div>
</div> )}
{isAttemptRunning && (
<div className="flex items-center gap-2">
<Button
onClick={async () => {
if (displayQueued) {
setIsUnqueuing(true);
try {
const ok = await onUnqueue();
if (ok) setQueuedOptimistic(false);
} finally {
setIsUnqueuing(false);
}
} else {
setIsQueuing(true);
try {
const ok = await onQueue();
if (ok) setQueuedOptimistic(true);
} finally {
setIsQueuing(false);
}
}
}}
disabled={
displayQueued
? isUnqueuing
: !canSendFollowUp ||
!isDraftLoaded ||
isQueuing ||
isUnqueuing ||
!!draft?.sending ||
isRetryActive
}
size="sm"
variant="default"
className="md:min-w-[180px] transition-all"
>
{displayQueued ? (
isUnqueuing ? (
<>
<Loader2 className="animate-spin h-4 w-4 mr-2" />
{t('followUp.unqueuing')}
</>
) : (
t('followUp.edit')
)
) : isQueuing ? (
<>
<Loader2 className="animate-spin h-4 w-4 mr-2" />
{t('followUp.queuing')}
</>
) : (
t('followUp.queueForNextTurn')
)}
</Button>
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -51,7 +51,7 @@ export function FollowUpEditorCard({
disabled={disabled} disabled={disabled}
projectId={projectId} projectId={projectId}
rows={1} rows={1}
maxRows={30} maxRows={80}
onPasteFiles={onPasteFiles} onPasteFiles={onPasteFiles}
/> />
{showLoadingOverlay && ( {showLoadingOverlay && (

View File

@@ -41,12 +41,12 @@ export function FullAttemptLogsPage() {
> >
<TaskAttemptPanel attempt={attempt} task={task}> <TaskAttemptPanel attempt={attempt} task={task}>
{({ logs, followUp }) => ( {({ logs, followUp }) => (
<div className="h-full flex flex-col"> <div className="h-full min-h-0 flex flex-col">
<div className="flex-1 min-h-0 flex flex-col"> <div className="flex-1 min-h-0 flex flex-col">
{logs} {logs}
</div> </div>
<div className="border-t shrink-0"> <div className="min-h-0 max-h-[50%] border-t overflow-hidden">
<div className="mx-auto w-full max-w-[50rem]"> <div className="mx-auto w-full max-w-[50rem] h-full min-h-0">
{followUp} {followUp}
</div> </div>
</div> </div>
@@ -59,10 +59,10 @@ export function FullAttemptLogsPage() {
) : ( ) : (
<TaskAttemptPanel attempt={attempt} task={task}> <TaskAttemptPanel attempt={attempt} task={task}>
{({ logs, followUp }) => ( {({ logs, followUp }) => (
<div className="h-full flex flex-col"> <div className="h-full min-h-0 flex flex-col">
<div className="flex-1 min-h-0 flex flex-col">{logs}</div> <div className="flex-1 min-h-0 flex flex-col">{logs}</div>
<div className="border-t shrink-0"> <div className="min-h-0 max-h-[50%] border-t overflow-hidden">
<div className="mx-auto w-full max-w-[50rem]"> <div className="mx-auto w-full max-w-[50rem] h-full min-h-0">
{followUp} {followUp}
</div> </div>
</div> </div>

View File

@@ -958,16 +958,20 @@ export function ProjectTasks() {
{({ logs, followUp }) => ( {({ logs, followUp }) => (
<> <>
<GitErrorBanner /> <GitErrorBanner />
<div className="flex-1 min-h-0 flex flex-col">{logs}</div> <div className="flex-1 min-h-0 flex flex-col">
<div className="flex-1 min-h-0 flex flex-col">{logs}</div>
<div className="shrink-0 border-t"> <div className="shrink-0 border-t">
<div className="mx-auto w-full max-w-[50rem]"> <div className="mx-auto w-full max-w-[50rem]">
<TodoPanel /> <TodoPanel />
</div>
</div> </div>
</div>
<div className="shrink-0 border-t"> <div className="min-h-0 max-h-[50%] border-t overflow-hidden">
<div className="mx-auto w-full max-w-[50rem]">{followUp}</div> <div className="mx-auto w-full max-w-[50rem] h-full min-h-0">
{followUp}
</div>
</div>
</div> </div>
</> </>
)} )}