fix(chat): reading-width container + refresh placeholder on agent switch

- Wrap ChatMessageList and ChatInput in mx-auto max-w-4xl px-5 so wide
  chat windows don't sprawl — matches the issue-detail / project-detail
  width convention
- draftKey now includes the selected agent id in the new-chat state.
  Tiptap's Placeholder only applies at mount, so key-driven remount is
  the simplest way to refresh it when the user switches agents before
  sending the first message. Side benefit: per-agent new-chat drafts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing
2026-04-14 18:42:55 +08:00
parent d779cbd183
commit 34e452776b
2 changed files with 29 additions and 19 deletions

View File

@@ -30,7 +30,15 @@ export function ChatInput({
}: ChatInputProps) { }: ChatInputProps) {
const editorRef = useRef<ContentEditorRef>(null); const editorRef = useRef<ContentEditorRef>(null);
const activeSessionId = useChatStore((s) => s.activeSessionId); const activeSessionId = useChatStore((s) => s.activeSessionId);
const draftKey = activeSessionId ?? DRAFT_NEW_SESSION; const selectedAgentId = useChatStore((s) => s.selectedAgentId);
// Scope the new-chat draft by agent:
// 1. Switching agents while composing a brand-new chat gives each
// agent its own draft (no cross-agent leakage).
// 2. Tiptap's Placeholder extension is only applied at mount; this
// key changes on agent switch so the editor remounts and the
// `Tell {agent} what to do…` placeholder refreshes.
const draftKey =
activeSessionId ?? `${DRAFT_NEW_SESSION}:${selectedAgentId ?? ""}`;
// Select a primitive — empty-string fallback keeps referential stability. // Select a primitive — empty-string fallback keeps referential stability.
const inputDraft = useChatStore((s) => s.inputDrafts[draftKey] ?? ""); const inputDraft = useChatStore((s) => s.inputDrafts[draftKey] ?? "");
const setInputDraft = useChatStore((s) => s.setInputDraft); const setInputDraft = useChatStore((s) => s.setInputDraft);
@@ -65,8 +73,8 @@ export function ChatInput({
: "Tell me what to do…"; : "Tell me what to do…";
return ( return (
<div className="p-2 pt-0"> <div className="px-5 pb-3 pt-0">
<div className="relative flex min-h-16 max-h-40 flex-col rounded-lg bg-card pb-9 border-1 border-border transition-colors focus-within:border-brand"> <div className="relative mx-auto flex min-h-16 max-h-40 w-full max-w-4xl flex-col rounded-lg bg-card pb-9 border-1 border-border transition-colors focus-within:border-brand">
<div className="flex-1 min-h-0 overflow-y-auto px-3 py-2"> <div className="flex-1 min-h-0 overflow-y-auto px-3 py-2">
<ContentEditor <ContentEditor
// Remount the editor when the active session changes so its // Remount the editor when the active session changes so its

View File

@@ -53,22 +53,24 @@ export function ChatMessageList({
const hasLive = showLiveTimeline && liveTimeline.length > 0; const hasLive = showLiveTimeline && liveTimeline.length > 0;
return ( return (
<div <div ref={scrollRef} style={fadeStyle} className="flex-1 overflow-y-auto">
ref={scrollRef} {/* Inner container matches issue / project detail width convention
style={fadeStyle} * (max-w-4xl + mx-auto) so switching between chat and content
className="flex-1 overflow-y-auto px-4 py-3 space-y-4" * views doesn't jolt the reading width. px-5 is a touch tighter
> * than issue-detail's px-8 because the chat window can be narrow. */}
{messages.map((msg) => ( <div className="mx-auto w-full max-w-4xl px-5 py-4 space-y-4">
<MessageBubble key={msg.id} message={msg} /> {messages.map((msg) => (
))} <MessageBubble key={msg.id} message={msg} />
{hasLive && ( ))}
<div className="w-full space-y-1.5"> {hasLive && (
<TimelineView items={liveTimeline} /> <div className="w-full space-y-1.5">
</div> <TimelineView items={liveTimeline} />
)} </div>
{isWaiting && !hasLive && !pendingAlreadyPersisted && ( )}
<Loader2 className="size-4 animate-spin text-muted-foreground" /> {isWaiting && !hasLive && !pendingAlreadyPersisted && (
)} <Loader2 className="size-4 animate-spin text-muted-foreground" />
)}
</div>
</div> </div>
); );
} }