Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7cefff5
feat(webapp): draggable/resizable window primitive
kathiekiwi Aug 26, 2026
c46f8b9
fix(webapp): SSR-safe viewport read and viewport-aware resize clamping
kathiekiwi Aug 26, 2026
8d8b039
feat(webapp): dashboard agent as a floating bottom-right window
kathiekiwi Aug 26, 2026
4b93738
fix(webapp): floating chat window review follow-ups
kathiekiwi Aug 26, 2026
9e36628
fix(webapp): keep the Ask Trigger button visible while chat is open
kathiekiwi Aug 26, 2026
b9e9e14
fix(webapp): eliminate the drag/resize gesture-baseline race
kathiekiwi Aug 26, 2026
a07500a
test(webapp): jsdom regression test for the pan-gesture ordering fix
kathiekiwi Aug 26, 2026
c2044a1
fix(webapp): Ask Trigger button closes the chat when clicked while open
kathiekiwi Aug 26, 2026
6e80fbe
fix(webapp): agent-ui story demos the floating window shell
kathiekiwi Aug 26, 2026
e1dab30
fix(webapp): close the storybook floating-chat demo on route change
kathiekiwi Aug 26, 2026
90b4002
feat(webapp): dot-matrix variants of the Investigate/Watch icons for …
kathiekiwi Aug 26, 2026
3e7007a
fix(webapp): rounder dot-matrix icons in the ai-agent Shape library
kathiekiwi Aug 26, 2026
2825e45
fix(webapp): dot-matrix icons drawn on the exact Shape library grid
kathiekiwi Aug 26, 2026
308c7d4
feat(webapp): five more Investigate dot-matrix icon candidates
kathiekiwi Aug 26, 2026
597879d
fix(webapp): replace dot-matrix icon candidates with an interactive e…
kathiekiwi Aug 26, 2026
cb9f478
feat(webapp): render the owner's 10 drawn dot-matrix icon bitmaps
kathiekiwi Aug 26, 2026
1b644e7
fix(webapp): Investigate/Watch surfaces use the Ask Trigger glyph; dr…
kathiekiwi Aug 26, 2026
aed73d5
fix(webapp): live watch result block uses the chat spinner, not a sta…
kathiekiwi Aug 26, 2026
73d0dde
fix(webapp): revert Investigate/Watch icons to original inside the ch…
kathiekiwi Aug 26, 2026
4474a85
docs(webapp): add server-changes note for the floating chat window
kathiekiwi Aug 26, 2026
1447674
fix(webapp): trim verbose comments; drag/resize gate fixes; kill dupl…
kathiekiwi Aug 26, 2026
8ce45c3
fix(webapp): drag filter opts out by marker, not tag, and lives in th…
kathiekiwi Aug 26, 2026
c59cf0e
fix(webapp): classify a drag gesture on whichever pan event lands first
kathiekiwi Aug 26, 2026
e705833
fix(webapp): resume a chat's in-flight turn on reopen; final comment …
kathiekiwi Aug 26, 2026
8e411e1
fix(webapp): resume prose-only turns too; behavioral resume test; res…
kathiekiwi Aug 26, 2026
8dea45a
fix(webapp): fullscreen toggle remounted the chat; settle-merge misse…
kathiekiwi Aug 26, 2026
00051e9
fix(webapp): raise dashboard agent chat z-index to 30
kathiekiwi Aug 28, 2026
91dbd9b
feat(webapp): add chat display mode icons
kathiekiwi Aug 28, 2026
23b958f
feat(webapp): chat display mode switcher (floating, right panel, full…
kathiekiwi Aug 28, 2026
78cca26
feat(webapp): move chat context and new-chat button under input; 360p…
kathiekiwi Aug 28, 2026
581c25b
fix(webapp): keep chat mounted across display modes; update mode guar…
kathiekiwi Aug 28, 2026
727f592
fix(webapp): floating chat default height 600px
kathiekiwi Aug 28, 2026
984eac5
fix(webapp): add right margin to new chat button
kathiekiwi Aug 28, 2026
a8c28b1
fix(webapp): restore fullscreen chat centering
kathiekiwi Aug 28, 2026
971c108
feat(webapp): compact animated chat mode toggle
kathiekiwi Aug 28, 2026
2d91412
fix(webapp): collapse degenerate resizable tracks so fullscreen fits …
kathiekiwi Aug 28, 2026
07a8480
fix(webapp): fullscreen chat column fills takeover height
kathiekiwi Aug 28, 2026
e091137
fix(webapp): clear floating geometry when chat docks
kathiekiwi Aug 28, 2026
5ff2924
docs(webapp): mention chat display modes in release note
kathiekiwi Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/floating-chat-window.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

Ask Trigger now opens as a floating window at the bottom of the page that you can drag anywhere and resize. The Ask Trigger button toggles the chat open and closed. You can also switch to a right-side panel or fullscreen view using a toggle in the chat header.
23 changes: 23 additions & 0 deletions apps/webapp/app/assets/icons/ChatFloatingPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function ChatFloatingPanel({ className }: { className?: string }) {
return (
<svg
className={className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 5H5C3.89543 5 3 5.89543 3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V7C21 5.89543 20.1046 5 19 5Z"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
/>
<path
d="M17 10H15C14.4477 10 14 10.4477 14 11V15C14 15.5523 14.4477 16 15 16H17C17.5523 16 18 15.5523 18 15V11C18 10.4477 17.5523 10 17 10Z"
fill="currentColor"
/>
</svg>
);
}
20 changes: 20 additions & 0 deletions apps/webapp/app/assets/icons/ChatFullScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function ChatFullScreen({ className }: { className?: string }) {
return (
<svg
className={className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 5H5C3.89543 5 3 5.89543 3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V7C21 5.89543 20.1046 5 19 5Z"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
/>
<rect x="5" y="7" width="14" height="10" rx="0.5" fill="currentColor" />
</svg>
);
}
20 changes: 20 additions & 0 deletions apps/webapp/app/assets/icons/ChatRightPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function ChatRightPanel({ className }: { className?: string }) {
return (
<svg
className={className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 5H5C3.89543 5 3 5.89543 3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V7C21 5.89543 20.1046 5 19 5Z"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
/>
<rect x="15" y="7" width="4" height="10" rx="0.5" fill="currentColor" />
</svg>
);
}
94 changes: 57 additions & 37 deletions apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import { DashboardAgentProvider, TOGGLE_PANEL_SHORTCUT } from "./dashboardAgentL
import { useDashboardAgentOpenRequests } from "./dashboardAgentOpenRequest";
import {
agentHiddenContentClassName,
agentTakeoverClassName,
readAgentFullscreen,
writeAgentFullscreen,
FloatingAgentWindow,
readAgentMode,
writeAgentMode,
type DashboardAgentMode,
} from "./panel-layout";
import { nextPendingTurnChatId } from "./pending-turn";
import { nextVisibleChat } from "./unread-counts";
Expand Down Expand Up @@ -114,14 +115,13 @@ export function DashboardAgent({
setUnreadWakes(initialUnreadWakes);
setUnreadWork(initialUnreadWork);
}, [environment.id, initialUnreadWakes, initialUnreadWork]);
// Read lazily so SSR always renders the side panel.
const [fullscreen, setFullscreen] = useState(readAgentFullscreen);
// Read lazily: SSR has no localStorage, so the server always renders the floating default.
const [mode, setMode] = useState<DashboardAgentMode>(readAgentMode);
const fullscreen = mode === "fullscreen";

const toggleFullscreen = useCallback(() => {
setFullscreen((current) => {
writeAgentFullscreen(!current);
return !current;
});
const changeMode = useCallback((next: DashboardAgentMode) => {
writeAgentMode(next);
setMode(next);
}, []);

// Pathname only: filter and search-param changes must keep fullscreen.
Expand All @@ -130,9 +130,10 @@ export function DashboardAgent({
useEffect(() => {
if (previousPathname.current === pathname) return;
previousPathname.current = pathname;
setFullscreen((current) => {
if (current) writeAgentFullscreen(false);
return false;
setMode((current) => {
if (current !== "fullscreen") return current;
writeAgentMode("floating");
return "floating";
});
}, [pathname]);
const [newChatSeq, setNewChatSeq] = useState(0);
Expand Down Expand Up @@ -165,8 +166,11 @@ export function DashboardAgent({
setOpen(false);
// Pending requests must be dropped or a stale one re-applies on the next open.
visibleChat.current = null;
setFullscreen(false);
writeAgentFullscreen(false);
setMode((current) => {
if (current !== "fullscreen") return current;
writeAgentMode("floating");
return "floating";
});
setRequestedMessage(undefined);
setOpenChatRequest(undefined);
setWatchRequest(undefined);
Expand Down Expand Up @@ -346,7 +350,10 @@ export function DashboardAgent({
return (
<DashboardAgentProvider value={context}>
{open ? (
// `relative` is the takeover's containing block.
// `relative` is the fullscreen takeover's containing block. The ResizablePanelGroup
// stays mounted across all three modes — only its sizing/handle degenerate outside
// rightPanel — so `FloatingAgentWindow` (and the chat panel inside it) sits at the
// same tree position in every mode and a mode switch never remounts it.
<div className="relative h-full min-h-0">
<ResizablePanelGroup
orientation="horizontal"
Expand All @@ -356,27 +363,40 @@ export function DashboardAgent({
<ResizablePanel id="dashboard-content" min="320px">
<div className={agentHiddenContentClassName(fullscreen)}>{children}</div>
</ResizablePanel>
<ResizableHandle
id="dashboard-agent-handle"
className={fullscreen ? "invisible" : undefined}
/>
<ResizablePanel id="dashboard-agent-panel" default="380px" min="320px" max="720px">
<div className={agentTakeoverClassName(fullscreen)}>
<DashboardAgentPanel
onClose={() => setPanelOpen(false)}
requestedMessage={requestedMessage}
openChatRequest={openChatRequest}
watchRequest={watchRequest}
newChatSeq={newChatSeq}
promotedPrompt={promotedPrompt}
onChatRead={markChatRead}
// The panel's own count, off the chat list it has already marked read.
onUnreadWorkChange={setUnreadWork}
onTurnActivityChange={handleTurnActivityChange}
isFullscreen={fullscreen}
onToggleFullscreen={toggleFullscreen}
/>
</div>
{mode === "rightPanel" && <ResizableHandle id="dashboard-agent-handle" />}
<ResizablePanel
id="dashboard-agent-panel"
default="380px"
min="320px"
max="720px"
collapsible
collapsed={mode !== "rightPanel"}
collapsedSize="0px"
// Non-rightPanel modes render through position:fixed/absolute, which must
// escape this panel's own clipping box to avoid being cut to its 0px width.
// Tailwind v4's important modifier is a trailing `!`, not a leading one.
className={mode === "rightPanel" ? undefined : "overflow-visible!"}
>
<FloatingAgentWindow mode={mode}>
{({ dragHandleProps, dragHandleClassName }) => (
<DashboardAgentPanel
onClose={() => setPanelOpen(false)}
requestedMessage={requestedMessage}
openChatRequest={openChatRequest}
watchRequest={watchRequest}
newChatSeq={newChatSeq}
promotedPrompt={promotedPrompt}
onChatRead={markChatRead}
// The panel's own count, off the chat list it has already marked read.
onUnreadWorkChange={setUnreadWork}
onTurnActivityChange={handleTurnActivityChange}
mode={mode}
onModeChange={changeMode}
dragHandleProps={dragHandleProps}
dragHandleClassName={dragHandleClassName}
/>
)}
</FloatingAgentWindow>
</ResizablePanel>
</ResizablePanelGroup>
</div>
Expand Down
53 changes: 43 additions & 10 deletions apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ import {
import { useLocation, useNavigate } from "@remix-run/react";
import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
import { useCallback, useEffect, useRef, useState } from "react";
import { PlusIcon } from "~/assets/icons/PlusIcon";
import { Button } from "~/components/primitives/Buttons";
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
import { useToast } from "~/components/primitives/Toast";
import { AgentQuotaNotice, AgentUpgradeBlock } from "./AgentUpgradeGate";
import { DashboardAgentComposer } from "./DashboardAgentComposer";
import { DashboardAgentContextBanner } from "./DashboardAgentContextBanner";
import { DashboardAgentHero } from "./DashboardAgentHero";
import { NEW_CHAT_SHORTCUT } from "./DashboardAgentHeader";
import { DashboardAgentMessages, type TurnActivity } from "./DashboardAgentMessages";
import { MESSAGE_TOO_LARGE_ERROR } from "./message-limits";
import {
Expand Down Expand Up @@ -46,6 +50,23 @@ export type DashboardAgentSession = {
lastEventId?: string;
};

/** The transport's `sessions` option for one chat. Extracted so the resume wiring is testable. */
export function chatSessionsOption(
chatId: string,
session: DashboardAgentSession | null,
streaming: boolean | undefined
) {
if (!session) return undefined;
return {
[chatId]: {
publicAccessToken: session.publicAccessToken,
lastEventId: session.lastEventId,
// Mid-turn chats must be marked streaming or the transport won't resume `session.out`.
isStreaming: streaming ?? false,
},
};
}

// Matches the agent's clientDataSchema input.
export type DashboardAgentClientData = {
userId: string;
Expand Down Expand Up @@ -80,6 +101,8 @@ export function DashboardAgentChat({
onTurnSettled,
onActivityChange,
onQuotaChange,
onNewChat,
showNewChat,
}: {
chatId: string;
initialMessages: UIMessage[];
Expand Down Expand Up @@ -109,6 +132,8 @@ export function DashboardAgentChat({
onActivityChange?: (chatId: string, activity: TurnActivity | null) => void;
/** The poll lives here, so this is where the panel learns the cap has lifted. */
onQuotaChange?: (quota: MessageQuota) => void;
onNewChat: () => void;
showNewChat: boolean;
}) {
const [input, setInput] = useState("");
// Set when the server refuses a send over the cap, so the block shows at once rather than
Expand Down Expand Up @@ -158,16 +183,7 @@ export function DashboardAgentChat({
return res;
},
clientData,
sessions: session
? {
[chatId]: {
publicAccessToken: session.publicAccessToken,
lastEventId: session.lastEventId,
// Mid-turn chats must be marked streaming or the transport won't resume `session.out`.
isStreaming: streaming ?? false,
},
}
: undefined,
sessions: chatSessionsOption(chatId, session, streaming),
startSession: async ({ chatId }) => {
const body = new FormData();
body.set("intent", "start");
Expand Down Expand Up @@ -489,6 +505,23 @@ export function DashboardAgentChat({
currentPage={currentPage}
/>
}
trailingAction={
showNewChat && (
<Button
variant="minimal/small"
className="aspect-square h-6 shrink-0 p-1 mr-[6px]"
aria-label="New chat"
tooltip={
<span className="flex items-center">
New chat
<ShortcutKey shortcut={NEW_CHAT_SHORTCUT} variant="medium" />
</span>
}
onClick={onNewChat}
LeadingIcon={<PlusIcon className="size-4 text-text-dimmed" />}
/>
)
}
/>
{quota.kind === "within" && (
<AgentQuotaNotice remaining={quota.remaining} limit={quota.limit} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function DashboardAgentComposer({
isStreaming,
focusKey,
context,
trailingAction,
layout = "docked",
autoFocus = true,
placeholderSuggestion,
Expand All @@ -32,6 +33,8 @@ export function DashboardAgentComposer({
// Bump to move focus back to the textarea.
focusKey?: string | number;
context?: React.ReactNode;
// Rendered right-aligned next to `context`, below the input.
trailingAction?: React.ReactNode;
layout?: DashboardAgentComposerLayout;
autoFocus?: boolean;
// Shown as the placeholder while the field is empty. Tab accepts it as editable
Expand Down Expand Up @@ -81,7 +84,6 @@ export function DashboardAgentComposer({
isHero ? "w-full" : "bg-background-bright px-3 pb-3 pt-1"
)}
>
{isHero ? null : context}
<div
className={cn(
"border border-border-bright bg-background-bright transition focus-within:border-border-brighter",
Expand Down Expand Up @@ -152,6 +154,12 @@ export function DashboardAgentComposer({
)}
</div>
</div>
{isHero ? null : (
<div className="flex min-w-0 items-center justify-between gap-2">
{context ?? <span />}
{trailingAction}
</div>
)}
{/* Mounted from the start, empty until there is something to say: a region that appears
with its first message goes unannounced in several screen readers. */}
<p className="sr-only" aria-live="polite">
Expand Down
Loading