[eric] scroll chat to bottom on send, persist chat input drafts across unmount/remount, add click-to-collapse for expanded agent cards on

dashboard
This commit is contained in:
ciregenz
2026-04-10 13:21:10 -07:00
parent 889a98c513
commit 9752e9ddfe
4 changed files with 54 additions and 5 deletions
@@ -342,6 +342,9 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
const handleSend = (prompt: string, images?: Array<{ data: string; media_type: string }>, contextPaths?: Array<{ path: string; type: 'file' | 'directory' }>, forcedTools?: string[], attachedSkills?: Array<{ id: string; name: string; content: string }>, selectedBrowserIds?: string[]) => {
if (!id) return;
// Sending a message is a clear intent signal: the user wants to see
// the response. Force-scroll to bottom regardless of isAtBottomRef.
scrollToBottom();
const msg: QueuedMessage = { prompt, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds };
if (agentBusy) {
messageQueueRef.current.push(msg);
+36 -2
View File
@@ -82,6 +82,11 @@ export interface ChatInputHandle {
setContent: (prompt: string, contextPaths?: ContextPath[], forcedTools?: ForcedToolGroup[]) => void;
}
// Module-level draft store — survives component unmount/remount. Keyed by
// sessionId (or a fallback owner id). Stores the raw innerHTML of the
// contentEditable div so skill pills, formatting, etc. are preserved.
const _draftStore = new Map<string, string>();
const ICON_MAP: Record<string, React.ReactNode> = {
smart_toy: <SmartToyOutlinedIcon sx={{ fontSize: 14 }} />,
question_answer: <QuestionAnswerOutlinedIcon sx={{ fontSize: 14 }} />,
@@ -147,7 +152,25 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
if (autoFocus) editorRef.current?.focus();
}, [autoFocus]);
const [hasContent, setHasContent] = useState(false);
// Restore draft from the module-level store on mount.
useEffect(() => {
const saved = _draftStore.get(ownerId);
const editor = editorRef.current;
if (saved && editor && !editor.textContent?.trim()) {
editor.innerHTML = saved;
// Move cursor to end
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
}
// Only on mount — ownerId is stable for the component's lifetime
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [hasContent, setHasContent] = useState(() => !!_draftStore.get(ownerId));
const [attachedSkills, setAttachedSkills] = useState<Record<string, AttachedSkill>>({});
const attachedSkillsRef = useRef(attachedSkills);
attachedSkillsRef.current = attachedSkills;
@@ -378,6 +401,7 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
browserIds.length > 0 ? browserIds : undefined,
);
editor.innerHTML = '';
_draftStore.delete(ownerId);
setImages([]);
setContextPaths([]);
setForcedTools([]);
@@ -399,7 +423,17 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
updateHasContent();
detectTrigger();
syncAttachedSkills();
}, [updateHasContent, detectTrigger, syncAttachedSkills]);
// Persist draft so it survives unmount/remount (card collapse, navigation)
const editor = editorRef.current;
if (editor) {
const html = editor.innerHTML;
if (html && html !== '<br>') {
_draftStore.set(ownerId, html);
} else {
_draftStore.delete(ownerId);
}
}
}, [updateHasContent, detectTrigger, syncAttachedSkills, ownerId]);
const handleEditorClick = useCallback(() => {
detectTrigger();
+14 -2
View File
@@ -332,12 +332,24 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
}
selection.selectCard(id, type, false);
dispatch(bringToFront({ id, type }));
const alreadyExpanded = type === 'agent' && expandedSessionIds.includes(id);
if (alreadyExpanded) {
// Delay single-click collapse so double-click can override.
// Double-click handler (handleCardDoubleClick) clears clickTimerRef.
clickTimerRef.current = setTimeout(() => {
clickTimerRef.current = null;
dispatch(collapseSession(id));
}, 250);
return;
}
// Expand (if not already) + center + zoom + bring to front
if (type === 'agent' && !expandedSessionIds.includes(id)) {
if (type === 'agent') {
dispatch(expandSession(id));
}
dispatch(bringToFront({ id, type }));
setFocusedCardId(id);
setTimeout(() => {
const rect = getCardRect(id, type);
File diff suppressed because one or more lines are too long