From f3ee8424b22ef0c720bba41203150e114f19d7df Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Wed, 29 Apr 2026 14:14:24 +0100 Subject: [PATCH] feat(ai-chat reference): explicit Preload button + Runs link in debug panel + sendAction bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UX cleanup discovered during the Sessions e2e sweep. Three changes, one commit because they all live in the chat input row / debug panel area: - Explicit "Preload" button next to "Send" that only renders when the chat has no messages and no session yet. Clicking calls transport.preload(chatId), which mints the session and triggers the first run with trigger:"preload". Self-hides once session is truthy. Replaces the inert "Preload new chats" sidebar checkbox (the visible `+ New Chat` button only navigated and never called transport.preload — preloadEnabled was wired through the context but read by nobody, since ChatApp.tsx is no longer the mounted chat sidebar). Drops the dead preloadEnabled state + checkbox from chat-settings-context, chat-sidebar, chat-sidebar-wrapper, and the chat-app.tsx legacy code path. - Debug panel "Runs → View in dashboard" row, gated on dashboardUrl + a new NEXT_PUBLIC_TRIGGER_PROJECT_DASHBOARD_PATH env var. Resolves to the runs-list page filtered by chat: tag — so opening the link drops you straight into the run list for the active chat. Threads the new prop through chat-view → chat → DebugPanel. - window.__chat.sendAction(action) bridge wrapper that delegates to transport.sendAction(chatId, action). Lets smoke tests drive aiChatHydrated's actionSchema (undo/rollback/remove/replace) without reaching into React internals. --- .../ai-chat/src/components/chat-app.tsx | 13 ++------- .../src/components/chat-settings-context.tsx | 5 ---- .../src/components/chat-sidebar-wrapper.tsx | 4 --- .../ai-chat/src/components/chat-sidebar.tsx | 13 --------- .../ai-chat/src/components/chat-view.tsx | 1 + references/ai-chat/src/components/chat.tsx | 27 +++++++++++++++++++ 6 files changed, 30 insertions(+), 33 deletions(-) diff --git a/references/ai-chat/src/components/chat-app.tsx b/references/ai-chat/src/components/chat-app.tsx index df0a248c4..2d77ffe94 100644 --- a/references/ai-chat/src/components/chat-app.tsx +++ b/references/ai-chat/src/components/chat-app.tsx @@ -55,7 +55,6 @@ export function ChatApp({ // Model for new chats (before first message is sent) const [newChatModel, setNewChatModel] = useState(DEFAULT_MODEL); - const [preloadEnabled, setPreloadEnabled] = useState(true); const [idleTimeoutInSeconds, setIdleTimeoutInSeconds] = useState(60); const handleSessionChange = useCallback((chatId: string, session: SessionInfo | null) => { @@ -107,14 +106,7 @@ export function ChatApp({ setActiveChatId(id); setMessages([]); setNewChatModel(DEFAULT_MODEL); - if (preloadEnabled) { - // Eagerly create the session + first run before the user types. - // Transport calls `startSession({ chatId: id, taskId: taskMode })`. - // `idleTimeoutInSeconds` lives on the agent definition's - // `idleTimeoutInSeconds`, not per-call. - void transport.preload(id); - void idleTimeoutInSeconds; - } + void idleTimeoutInSeconds; } function handleSelectChat(id: string) { @@ -173,8 +165,6 @@ export function ChatApp({ onNewChat={handleNewChat} onDeleteChat={handleDeleteChat} onWipeAll={handleWipeAll} - preloadEnabled={preloadEnabled} - onPreloadChange={setPreloadEnabled} idleTimeoutInSeconds={idleTimeoutInSeconds} onIdleTimeoutChange={setIdleTimeoutInSeconds} taskMode={taskMode} @@ -193,6 +183,7 @@ export function ChatApp({ onModelChange={isNewChat ? setNewChatModel : undefined} session={activeSession} dashboardUrl={process.env.NEXT_PUBLIC_TRIGGER_DASHBOARD_URL} + projectDashboardPath={process.env.NEXT_PUBLIC_TRIGGER_PROJECT_DASHBOARD_PATH} onFirstMessage={handleFirstMessage} onMessagesChange={handleMessagesChange} /> diff --git a/references/ai-chat/src/components/chat-settings-context.tsx b/references/ai-chat/src/components/chat-settings-context.tsx index 7c8f2087f..0d9e605b5 100644 --- a/references/ai-chat/src/components/chat-settings-context.tsx +++ b/references/ai-chat/src/components/chat-settings-context.tsx @@ -5,8 +5,6 @@ import { createContext, useContext, useState, type ReactNode } from "react"; type ChatSettings = { taskMode: string; setTaskMode: (mode: string) => void; - preloadEnabled: boolean; - setPreloadEnabled: (enabled: boolean) => void; idleTimeoutInSeconds: number; setIdleTimeoutInSeconds: (seconds: number) => void; }; @@ -15,14 +13,11 @@ const ChatSettingsContext = createContext(null); export function ChatSettingsProvider({ children }: { children: ReactNode }) { const [taskMode, setTaskMode] = useState("ai-chat"); - const [preloadEnabled, setPreloadEnabled] = useState(true); const [idleTimeoutInSeconds, setIdleTimeoutInSeconds] = useState(60); const value: ChatSettings = { taskMode, setTaskMode, - preloadEnabled, - setPreloadEnabled, idleTimeoutInSeconds, setIdleTimeoutInSeconds, }; diff --git a/references/ai-chat/src/components/chat-sidebar-wrapper.tsx b/references/ai-chat/src/components/chat-sidebar-wrapper.tsx index ecfcb10e1..2827e901d 100644 --- a/references/ai-chat/src/components/chat-sidebar-wrapper.tsx +++ b/references/ai-chat/src/components/chat-sidebar-wrapper.tsx @@ -26,8 +26,6 @@ export function ChatSidebarWrapper({ const { taskMode, setTaskMode, - preloadEnabled, - setPreloadEnabled, idleTimeoutInSeconds, setIdleTimeoutInSeconds, } = useChatSettings(); @@ -83,8 +81,6 @@ export function ChatSidebarWrapper({ onNewChat={handleNewChat} onDeleteChat={handleDeleteChat} onWipeAll={handleWipeAll} - preloadEnabled={preloadEnabled} - onPreloadChange={setPreloadEnabled} idleTimeoutInSeconds={idleTimeoutInSeconds} onIdleTimeoutChange={setIdleTimeoutInSeconds} taskMode={taskMode} diff --git a/references/ai-chat/src/components/chat-sidebar.tsx b/references/ai-chat/src/components/chat-sidebar.tsx index 588f6bae6..11dd05475 100644 --- a/references/ai-chat/src/components/chat-sidebar.tsx +++ b/references/ai-chat/src/components/chat-sidebar.tsx @@ -25,8 +25,6 @@ type ChatSidebarProps = { onNewChat: () => void; onDeleteChat: (id: string) => void; onWipeAll: () => void; - preloadEnabled: boolean; - onPreloadChange: (enabled: boolean) => void; idleTimeoutInSeconds: number; onIdleTimeoutChange: (seconds: number) => void; taskMode: string; @@ -40,8 +38,6 @@ export function ChatSidebar({ onNewChat, onDeleteChat, onWipeAll, - preloadEnabled, - onPreloadChange, idleTimeoutInSeconds, onIdleTimeoutChange, taskMode, @@ -93,15 +89,6 @@ export function ChatSidebar({
-
Idle timeout diff --git a/references/ai-chat/src/components/chat.tsx b/references/ai-chat/src/components/chat.tsx index 5179a3569..c438955a2 100644 --- a/references/ai-chat/src/components/chat.tsx +++ b/references/ai-chat/src/components/chat.tsx @@ -192,6 +192,7 @@ function DebugPanel({ status, session, dashboardUrl, + projectDashboardPath, messageCount, ttfbHistory, }: { @@ -200,9 +201,14 @@ function DebugPanel({ status: string; session?: { publicAccessToken: string; lastEventId?: string; isStreaming?: boolean }; dashboardUrl?: string; + projectDashboardPath?: string; messageCount: number; ttfbHistory: TtfbEntry[]; }) { + const runsUrl = + dashboardUrl && projectDashboardPath + ? `${dashboardUrl}${projectDashboardPath}/env/dev/runs?tags=${encodeURIComponent(`chat:${chatId}`)}` + : undefined; const [open, setOpen] = useState(false); const latestTtfb = ttfbHistory.length > 0 ? ttfbHistory[ttfbHistory.length - 1]! : undefined; @@ -243,6 +249,7 @@ function DebugPanel({ + {runsUrl && } {session ? ( <> @@ -313,6 +320,7 @@ type ChatProps = { onModelChange?: (model: string) => void; session?: { publicAccessToken: string; lastEventId?: string; isStreaming?: boolean }; dashboardUrl?: string; + projectDashboardPath?: string; onFirstMessage?: (chatId: string, text: string) => void; onMessagesChange?: (chatId: string, messages: ChatUiMessage[]) => void; }; @@ -327,6 +335,7 @@ export function Chat({ onModelChange, session, dashboardUrl, + projectDashboardPath, onFirstMessage, onMessagesChange, }: ChatProps) { @@ -550,6 +559,7 @@ export function Chat({ promote: (id: string) => actionsRef.current.promote(id), send: (text: string) => actionsRef.current.send(text), stop: () => actionsRef.current.stop(), + sendAction: (action: unknown) => transport.sendAction(chatId, action), // ── Waiters ─────────────────────────────────────────────────── waitForStatus: (target: string, timeoutMs = DEFAULT_TIMEOUT_MS) => @@ -844,6 +854,7 @@ export function Chat({ status={status} session={session} dashboardUrl={dashboardUrl} + projectDashboardPath={projectDashboardPath} messageCount={messages.length} ttfbHistory={ttfbHistory} /> @@ -882,6 +893,22 @@ export function Chat({ > Send + {/* Preload — only visible before the first message lands. After + the user sends, the transport creates the session lazily, so + session becomes truthy and this button hides itself. The + transport tracks an in-flight preload internally; double-clicks + are a no-op. */} + {messages.length === 0 && !session && ( + + )} {status === "streaming" && (