diff --git a/frontend/src/app/pages/AgentChat/ChatInput.tsx b/frontend/src/app/pages/AgentChat/ChatInput.tsx index 243a0c51..b10111eb 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput.tsx @@ -42,9 +42,12 @@ interface Props { thinkingLevel?: 'off' | 'low' | 'medium' | 'high' | 'auto'; onThinkingLevelChange?: (level: 'off' | 'low' | 'medium' | 'high' | 'auto') => void; onActivityLabelChange?: (label: string | null) => void; + // Seed the composer with this text (unsent), so a starter-prompt click opens + // the chat with the message already typed, ready for the user to hit send. + prefillPrompt?: string; } -const ChatInput = forwardRef(({ onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange, isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0, thinkingLevel = 'auto', onThinkingLevelChange, onActivityLabelChange }, ref) => { +const ChatInput = forwardRef(({ onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange, isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0, thinkingLevel = 'auto', onThinkingLevelChange, onActivityLabelChange, prefillPrompt }, ref) => { const c = useClaudeTokens(); const editorRef = useRef(null); const containerRef = useRef(null); @@ -59,6 +62,19 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, if (autoFocus) editorRef.current?.focus(); }, [autoFocus]); + // Drop the seeded prompt into the editor when it arrives (starter-prompt click). + const prefilledRef = useRef(null); + useEffect(() => { + if (!prefillPrompt || prefilledRef.current === prefillPrompt) return; + const editor = editorRef.current; + if (!editor) return; + if (editor.tagName === 'TEXTAREA') (editor as unknown as HTMLTextAreaElement).value = prefillPrompt; + else editor.textContent = prefillPrompt; + setHasContent(true); + prefilledRef.current = prefillPrompt; + editor.focus(); + }, [prefillPrompt]); + useDraftLoad(editorRef, ownerId); const [hasContent, setHasContent] = useState(() => !!loadDraft(ownerId)); diff --git a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx index 2ba753c9..965682cb 100644 --- a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx @@ -57,6 +57,8 @@ interface Props { dashboardId?: string; newAgentBounce?: boolean; onNewAgentBounceEnd?: () => void; + // Text to seed the composer with when it opens (starter-prompt click). + prefillPrompt?: string; } const TOOLBAR_OWNER_ID = '__toolbar__'; @@ -100,7 +102,7 @@ function formatRelativeTime(dateStr: string | null): string { } const DashboardToolbar = React.forwardRef( - ({ inputOpen, onNewAgent, onCancel, onSend, onAddView, onHistoryResume, onAddBrowser, onAddNote, dashboardId, newAgentBounce, onNewAgentBounceEnd }, ref) => { + ({ inputOpen, onNewAgent, onCancel, onSend, onAddView, onHistoryResume, onAddBrowser, onAddNote, dashboardId, newAgentBounce, onNewAgentBounceEnd, prefillPrompt }, ref) => { const c = useClaudeTokens(); const dispatch = useAppDispatch(); const elementSelection = useElementSelection(); @@ -434,6 +436,7 @@ const DashboardToolbar = React.forwardRef( sessionId={TOOLBAR_OWNER_ID} thinkingLevel={thinkingLevel} onThinkingLevelChange={handleThinkingLevelChange} + prefillPrompt={prefillPrompt} /> ) : historyOpen ? ( diff --git a/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx b/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx index 62fb717a..d89ae699 100644 --- a/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx +++ b/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx @@ -73,6 +73,8 @@ interface DashboardCanvasProps { onNewAgent: () => void; onToolbarCancel: () => void; onToolbarSend: (...args: any[]) => void; + onStarterPrefill: (prompt: string) => void; + toolbarPrefill?: string; onAddView: (outputId: string) => void; onHistoryResume: (sessionId: string) => void; onAddBrowser: () => void; @@ -130,6 +132,8 @@ const DashboardCanvas: React.FC = ({ onNewAgent, onToolbarCancel, onToolbarSend, + onStarterPrefill, + toolbarPrefill, onAddView, onHistoryResume, onAddBrowser, @@ -215,7 +219,7 @@ const DashboardCanvas: React.FC = ({ /> {sessionList.length === 0 && Object.keys(viewCards).length === 0 && Object.keys(browserCards).length === 0 ? ( - + ) : (
= ({ onFitToView={onFitToView} onTidy={onTidy} onSearchPaletteClose={onSearchPaletteClose} + toolbarPrefill={toolbarPrefill} /> diff --git a/frontend/src/app/pages/Dashboard/canvas/DashboardEmptyState.tsx b/frontend/src/app/pages/Dashboard/canvas/DashboardEmptyState.tsx index d45db91b..297db543 100644 --- a/frontend/src/app/pages/Dashboard/canvas/DashboardEmptyState.tsx +++ b/frontend/src/app/pages/Dashboard/canvas/DashboardEmptyState.tsx @@ -62,7 +62,9 @@ const STARTER_CATEGORIES: StarterCategory[] = [ const DashboardEmptyState: React.FC<{ c: ClaudeTokens; onLaunch?: (prompt: string, mode: string, model: string) => void; -}> = ({ c, onLaunch }) => { + // Open the composer with the prompt typed in (unsent) so the user hits send. + onPrefill?: (prompt: string) => void; +}> = ({ c, onLaunch, onPrefill }) => { // The host hides Dashboard with visibility:hidden (not display:none), which keeps // CSS animations ticking; gate on active so the shimmer only burns while watched. const active = useDashboardActive(); @@ -72,7 +74,6 @@ const DashboardEmptyState: React.FC<{ const navigate = useNavigate(); const [launching, setLaunching] = React.useState(false); const [expanded, setExpanded] = React.useState(null); - const [hoveredPrompt, setHoveredPrompt] = React.useState(null); const currentCategory = STARTER_CATEGORIES.find((cat) => cat.id === expanded); const currentPrompts = currentCategory?.prompts ?? []; @@ -87,6 +88,12 @@ const DashboardEmptyState: React.FC<{ navigate(`/apps/new?prompt=${encodeURIComponent(prompt)}`); return; } + // Open the composer with the prompt typed in, unsent: the user sees the chat + // open with their message ready and clicks send. Falls back to direct launch. + if (onPrefill) { + onPrefill(prompt); + return; + } if (!onLaunch) return; setLaunching(true); // empty state unmounts on first session, but guard a fast double-click onLaunch(prompt, mode, model); @@ -194,16 +201,12 @@ const DashboardEmptyState: React.FC<{ > back - setHoveredPrompt(null)} - sx={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 0.9, width: '100%', maxWidth: 480 }} - > + {currentPrompts.map((prompt) => ( launch(prompt)} - onMouseEnter={() => setHoveredPrompt(prompt)} disabled={launching} sx={{ textAlign: 'left', @@ -223,45 +226,6 @@ const DashboardEmptyState: React.FC<{ {prompt} ))} - - {/* Hover preview: a translucent ghost of the chat that would open, - so the user sees their message land before committing. Pure - divs (not the heavy AgentChat), so it costs nothing to render. */} - - {hoveredPrompt && ( - - - New chat - - - - - {hoveredPrompt} - - - - - )} - )} diff --git a/frontend/src/app/pages/Dashboard/canvas/DashboardOverlays.tsx b/frontend/src/app/pages/Dashboard/canvas/DashboardOverlays.tsx index 55cc38d8..c4b750bb 100644 --- a/frontend/src/app/pages/Dashboard/canvas/DashboardOverlays.tsx +++ b/frontend/src/app/pages/Dashboard/canvas/DashboardOverlays.tsx @@ -41,6 +41,7 @@ interface DashboardOverlaysProps { onFitToView: () => void; onTidy: () => void; onSearchPaletteClose: () => void; + toolbarPrefill?: string; } const DashboardOverlays: React.FC = ({ @@ -68,6 +69,7 @@ const DashboardOverlays: React.FC = ({ onFitToView, onTidy, onSearchPaletteClose, + toolbarPrefill, }) => { return ( <> @@ -86,6 +88,7 @@ const DashboardOverlays: React.FC = ({ dashboardId={dashboardId} newAgentBounce={newAgentBounce} onNewAgentBounceEnd={onNewAgentBounceEnd} + prefillPrompt={toolbarPrefill} /> diff --git a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts index d4562256..2a21c465 100644 --- a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts +++ b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { useElementSelection } from '@/app/components/editor/ElementSelectionContext'; import { useCanvasControls } from '../interaction/useCanvasControls'; @@ -159,6 +159,18 @@ export function useDashboardController(dashboardId: string, isActive: boolean) { setSearchPaletteOpen, }); + // Starter-prompt prefill: opens the composer with the prompt typed in (unsent), + // so the user reviews and hits send. Bump a nonce so re-clicking the same prompt + // (after editing/clearing) still re-seeds. Cleared when the composer closes. + const [toolbarPrefill, setToolbarPrefill] = useState(undefined); + const handleStarterPrefill = useCallback((prompt: string) => { + setToolbarPrefill(prompt); + setToolbarOpen(true); + }, [setToolbarOpen]); + useEffect(() => { + if (!toolbarOpen && toolbarPrefill) setToolbarPrefill(undefined); + }, [toolbarOpen, toolbarPrefill]); + useDashboardClipboard({ isActive, dashboardId, @@ -253,6 +265,8 @@ export function useDashboardController(dashboardId: string, isActive: boolean) { focusedCardId, pendingFocusNoteId, multiDragDelta, shakeDirection, neighborDirections, toolbarOpen, searchPaletteOpen, newAgentBounce, toolbarRef, spawnOriginsRef, revealSpawnedRef, measuredHeightsRef, getCanvasState, + toolbarPrefill, + onStarterPrefill: handleStarterPrefill, onViewportMouseDown: handleViewportMouseDown, onViewportMouseMove: handleViewportMouseMove, onViewportMouseUp: handleViewportMouseUp,