fix: simplify agent thread layout to match reference chat UI (#1388)

Replace the floating absolute-positioned prompt bar (gradient overlay +
128px bottomInset) with a plain flex column: MessageView as flex-1 with a
shrink-0 prompt bar beneath it, matching open-swe-app's ChatView.

Also remove the redundant always-on 'Files Changed' panel — MessageView
already renders an inline TurnChangedFilesCard per agent turn, and the
duplicate was being clipped by the floating prompt bar. This eliminates
the large whitespace gap and the broken files-changed box.
This commit is contained in:
Johannes du Plessis 2026-06-03 08:37:16 -07:00 • committed by GitHub
parent c8a997c125
commit 9e3406bd29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,13 +1,8 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { ChevronDown } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { AgentPromptBar } from "@/components/agents/AgentPromptBar";
import { AgentsShell } from "@/components/agents/AgentsSidebar";
import {
MessageView,
summarizeChangedFiles,
type MessageViewScrollControl,
} from "@/components/agents/ported";
import { MessageView } from "@/components/agents/ported";
import type { SessionUser } from "@/lib/api";
import type { AgentThread, Message } from "@/lib/agents/types";
import { useSendAgentMessage } from "@/lib/agents/queries";
@ -24,13 +19,9 @@ interface AgentThreadViewProps {
thread: AgentThread;
}
const PROMPT_OVERLAY_INSET = 128;
export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
const sendMessage = useSendAgentMessage(thread.id);
useAgentThreadStream(thread.id, thread.status === "running");
const scrollControlRef = useRef<MessageViewScrollControl | null>(null);
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
const [pendingPrompts, setPendingPrompts] = useState<PendingPrompt[]>(() =>
getPendingPrompts(thread.id),
);
@ -92,12 +83,6 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
return result;
}, [thread.messages, pendingPrompts]);
const changedFiles = useMemo(() => {
const agentMessages = thread.messages.filter((m) => m.author === "agent");
const allChunks = agentMessages.flatMap((m) => m.chunks);
return summarizeChangedFiles(allChunks);
}, [thread.messages]);
const hasMessages = displayMessages.length > 0;
const isStreaming = thread.status === "running" || pendingPrompts.length > 0;
@ -106,53 +91,14 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
<div className="flex min-w-0 flex-1 flex-col">
<div className="flex min-h-0 flex-1 flex-col">
{hasMessages ? (
<div className="relative flex min-h-0 flex-1 flex-col">
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<MessageView
messages={displayMessages}
isStreaming={isStreaming}
contentWidthClass="max-w-3xl"
bottomInset={PROMPT_OVERLAY_INSET}
scrollButtonSlot="external"
scrollControlRef={scrollControlRef}
onShowScrollToBottomChange={setShowScrollToBottom}
/>
{changedFiles.length > 0 && (
<div className="mx-auto mt-4 w-full max-w-3xl shrink-0 px-6">
<div className="rounded-lg border border-[var(--ui-border)] bg-[var(--ui-panel)] p-3">
<div className="mb-2 text-xs font-medium text-[var(--ui-text-muted)]">
{changedFiles.length} Files Changed
</div>
<div className="space-y-1">
{changedFiles.map((file) => (
<div
key={file.filePath}
className="flex items-center justify-between rounded px-2 py-1 text-xs hover:bg-[var(--ui-panel-2)]"
>
<span className="font-mono text-[var(--ui-text)]">{file.filePath}</span>
<span className="text-[var(--ui-success)]">+{file.additions}</span>
</div>
))}
</div>
</div>
</div>
)}
</div>
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 px-6 pb-4">
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-[var(--ui-bg)] via-[var(--ui-bg)]/80 to-transparent" />
<div className="pointer-events-auto relative mx-auto max-w-3xl">
{showScrollToBottom && (
<button
type="button"
onClick={() => scrollControlRef.current?.scrollToBottom()}
aria-label="Scroll to bottom"
className="absolute bottom-full left-1/2 z-30 mb-2 inline-flex h-8 w-8 -translate-x-1/2 items-center justify-center rounded-full bg-[var(--ui-panel-2)] text-[color:var(--ui-text-muted)] shadow-md transition-colors hover:bg-[var(--ui-panel)] hover:text-[color:var(--ui-text)]"
>
<ChevronDown className="h-3.5 w-3.5" />
</button>
)}
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
<MessageView
messages={displayMessages}
isStreaming={isStreaming}
contentWidthClass="max-w-3xl"
/>
<div className="shrink-0 px-4 pb-4">
<div className="mx-auto w-full min-w-0 max-w-3xl">
<AgentPromptBar
placeholder="Add a follow up"
compact