feat(ai-chat reference): explicit Preload button + Runs link in debug panel + sendAction bridge
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:<chatId> 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.
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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<ChatSettings | null>(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,
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 border-t border-gray-200 px-3 py-2.5 space-y-2">
|
||||
<label className="flex items-center gap-2 text-xs text-gray-500 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={preloadEnabled}
|
||||
onChange={(e) => onPreloadChange(e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
Preload new chats
|
||||
</label>
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<span className="shrink-0">Idle timeout</span>
|
||||
<input
|
||||
|
||||
@@ -98,6 +98,7 @@ export function ChatView({
|
||||
isNewChat={isNewChat}
|
||||
session={activeSession}
|
||||
dashboardUrl={process.env.NEXT_PUBLIC_TRIGGER_DASHBOARD_URL}
|
||||
projectDashboardPath={process.env.NEXT_PUBLIC_TRIGGER_PROJECT_DASHBOARD_PATH}
|
||||
onFirstMessage={handleFirstMessage}
|
||||
onMessagesChange={handleMessagesChange}
|
||||
/>
|
||||
|
||||
@@ -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({
|
||||
<Row label="Model" value={model} />
|
||||
<Row label="Status" value={status} />
|
||||
<Row label="Messages" value={String(messageCount)} />
|
||||
{runsUrl && <Row label="Runs" value="View in dashboard" link={runsUrl} />}
|
||||
{session ? (
|
||||
<>
|
||||
<Row label="Last Event ID" value={session.lastEventId ?? "—"} mono />
|
||||
@@ -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
|
||||
</button>
|
||||
{/* 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 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void transport.preload(chatId);
|
||||
}}
|
||||
className="rounded-lg bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-700 disabled:opacity-50"
|
||||
>
|
||||
Preload
|
||||
</button>
|
||||
)}
|
||||
{status === "streaming" && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user