feat(webapp): floating Ask Trigger chat window - #4794
Conversation
useDraggableResizable hook (framer-motion pan gestures, pure geometry helpers) for a floating window draggable and resizable from any edge, clamped to the viewport. Adds a storybook demo route.
getViewport() no longer reads window during SSR; the resize effect re-clamps once on mount. resizeRect now caps per-edge growth against the viewport before deriving x/y, so the far edge never drifts. Drops gratuitous useCallback in handler literals, renames the className helper to reflect it returns a string.
Replace the slide-in right column with a draggable, resizable floating chat window (variant-1 style), default and only mode now.
Drop dead exports for knip, register the storybook route, grab cursor on the drag handle, and a source guard against the right-column mode coming back.
Clicking it while open re-affirms the single floating window instead of doing nothing, matching what live testing expected.
framer-motion defers onPanStart/onPanEnd by a frame (its internal scheduler) but calls onPan synchronously, so a gesture-start rect snapshot captured in onPanStart could still be stale (or the mount-time initial rect) when a gesture's first onPan landed. Switch to framer's per-event `delta` folded onto the latest committed rect via functional setState, so there's no baseline left to race.
Pure-function tests can't fail on the deferred-onPanStart race by construction. This drives the hook's real handlers in framer's actual order (onPan, onPan, then a late onPanStart, then onPan) — fails against an offset+baseline shape, passes with the delta fold.
Toggles instead of re-affirming a no-op; tooltip says "Close chat" when open.
Chat UI gallery gets a live floating-window section (drag, resize, real header) and drops the "side panel" wording. Reuse draggableResizeHandleClassName from the primitive instead of a local duplicate. Move the floating-chat-window story next to Chat UI and drop the now-standalone draggable-resizable story.
Scoped to the storybook stories; the real dashboard chat still persists across navigation.
…comparison Storybook only, in the ai-agent story's new "Action icons" tab next to the current heroicons versions. Not wired into the app.
Investigate (magnifier + glasses), Watch (eye) and Alert (bell), all generated from circular/elliptical bands instead of hand-drawn corners, with a smaller edge dot to soften the curve. Moved into the Shape library section; dropped the separate Action icons tab.
Magnifier, glasses, eye, bell now sit on the same MATRIX x MATRIX grid nodes as DOT_SHAPES (exported dotMatrixGeometry from AgentDotMatrix), same pitch and dot radius, no off-grid or resized dots. Shown Face-options-style: faint full grid behind the lit silhouette.
Radar, fingerprint, crosshair, flashlight, sonar ping — same true MATRIX bitmap contract as the existing four. Grouped with the magnifier and glasses in the Shape library so all seven compare side by side.
…ditor Remove the nine candidate icon files and their story rows. In their place: a clickable MATRIXxMATRIX grid (dotMatrixGeometry, 1.5x the candidates' size) that toggles dots green and reads back as a MATRIX-line bitmap string. Keeps MATRIX/dotMatrixGeometry in AgentDotMatrix.tsx; drops the now-unconsumed static icon renderer.
…op editor's owner-bitmap row Every feature surface (buttons, suggested prompts, chat history status, watch card, watch result block) now renders AgentMonoLogo instead of the heroicons magnifier/eye, matching the Ask Trigger nav button. Unrelated magnifiers/eyes (search inputs, admin search, password-visibility toggles) are untouched. storybook.ai-agent: remove the owner-drawn candidates row and the editor's description text; the interactive editor stays.
…tic glyph WatchResultBlock's "watching" outcome (a watch just created and still live) now shows AgentSpinner — the same animated dot-matrix spinner the chat uses while thinking, and what WatchChips already shows for an active watch. The one-shot terminal outcomes keep their icons.
…at panel The Ask Trigger glyph swap stays only outside the chat window (InvestigateButton, WatchButton — page CTAs that open the chat). Surfaces rendered inside the panel tree revert to their original icons: suggested-prompt chips, chat history's investigating status, and the watch draft card header. The active-watch spinner in WatchResultBlock is untouched.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe dashboard agent now opens in a draggable and resizable floating window. Shared geometry utilities and a React hook enforce viewport, size, and position constraints. The panel supports fullscreen mode, drag exclusions, resize handles, and launcher toggling. Agent controls use updated icons and active-watch rendering. Reopened chats include streaming state. New DOM and math tests cover interaction behavior. Storybook includes a floating-window example and an accessible interactive dot-matrix editor. Legacy floating-window manifest entries were removed. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…icate floating-window demo Comment sweep: cap every survivor at two sentences, drop what-comments and a stale dotMatrixIcon.tsx reference, across panel-layout, DraggableResizable, draggableResizableMath, WatchResultBlock and the storybook routes. Gate fixes from cross-model review: - resizeRect: minSize now wins over a viewport-derived cap smaller than it, for all four resize directions (+4 tests). - Header drag no longer engages when a gesture starts on a button/ link/input inside it. - Fullscreen (no dragHandleProps.onPan) no longer applies drag cursor classes or local pan-state handlers. - Ask Trigger's "Close chat" tooltip drops the new-chat shortcut key. - Resize handles no longer get clipped by the window's overflow-hidden (moved to an inner content wrapper). - Drag handle gets touch-action:none so touch drags don't fight scrolling. - Dot-grid editor dots are keyboard-operable (role=button, Enter/ Space toggles). - storybook.dashboard-agent-floating: mount-gates the demo window (avoids an SSR/client position mismatch) and resets fullscreen on every close path. - New panel-layout.dom.test.ts: initialFloatingRect against a stubbed viewport, and a hook-level render proving the rect (and the minSize fix) survive the real wiring. Remove the "Floating window" section from storybook.agent-ui: the standalone storybook.dashboard-agent-floating route is the one floating-window demo going forward.
…e shell
closest("button, a, input, [role=button]") rejected pans starting on
the header title, since the whole title (including the truncated
text) sits inside a Popover trigger <button>. Replace with an opt-out
marker (data-agent-no-drag) on the header's action-button group and
on the popover trigger's chevron only, so the title text stays
draggable while Radix's click-to-open behavior is untouched.
Move the filter and cursor/touch-action state out of
DashboardAgentPanel and into FloatingAgentWindow itself, so every
consumer (the real panel, the standalone story) gets identical
behavior instead of reimplementing it. New tests in
panel-layout.dom.test.ts drive the filter directly: a pan starting on
ordinary content drags, one starting on a data-agent-no-drag element
doesn't.
onPan can arrive before onPanStart, so a gesture starting on a data-agent-no-drag element could leak its first delta(s) before ignoringGesture was set. Both handlers now classify the gesture once (gestureClassified) using whichever event's target shows up first. New test drives onPan before onPanStart from a no-drag target and asserts zero movement. Also drop a gratuitous useMemo over a 25-item array in storybook.ai-agent's DotGridEditor (CodeRabbit nitpick).
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/webapp/app/components/dashboard-agent/panel-layout.tsx (1)
106-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd crumb instrumentation for the gesture state changes.
Add
//@crumbs`` markers or an@crumbsregion around gesture classification and drag-state updates. This interaction path has ordering-dependent behavior.As per coding guidelines: “Add crumbs as you write code — not just when debugging.”
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: bfd69823-2072-4339-9347-8011283121bc
📒 Files selected for processing (3)
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.tsapps/webapp/app/components/dashboard-agent/panel-layout.tsxapps/webapp/app/routes/storybook.ai-agent/route.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/webapp/app/routes/storybook.ai-agent/route.tsx
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.
📜 Review details
⏰ Context from checks skipped due to timeout. (47)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (18, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (19, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (22, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (21, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (17, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (13, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (20, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (15, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (16, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (14, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (24, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (23, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 24)
- GitHub Check: sdk-compat / Node.js 20.20 (warp-ubuntu-latest-x64-4x)
- GitHub Check: sdk-compat / Deno Runtime
- GitHub Check: sdk-compat / Node.js 26.4 (warp-ubuntu-latest-x64-4x)
- GitHub Check: sdk-compat / Cloudflare Workers
- GitHub Check: packages / 🧪 Unit Tests: Packages (2, 3)
- GitHub Check: sdk-compat / Node.js 24.18 (warp-ubuntu-latest-x64-4x)
- GitHub Check: e2e / 🧪 CLI v3 tests (warp-ubuntu-latest-x64-4x - npm)
- GitHub Check: sdk-compat / Node.js 22.23 (warp-ubuntu-latest-x64-4x)
- GitHub Check: sdk-compat / Bun Runtime
- GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - npm)
- GitHub Check: runops-guard / runops-guard
- GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (1, 2)
- GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (2, 2)
- GitHub Check: fk-cascade-guard / fk-cascade-guard
- GitHub Check: e2e / 🧪 CLI v3 tests (warp-ubuntu-latest-x64-4x - pnpm)
- GitHub Check: internal / 🧪 Unit Tests: Internal
- GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - pnpm)
- GitHub Check: typecheck / typecheck
- GitHub Check: packages / 🧪 Unit Tests: Packages (1, 3)
- GitHub Check: packages / 🧪 Unit Tests: Packages (3, 3)
- GitHub Check: obsmap / 🧪 Unit Tests: Observability Map
- GitHub Check: code-quality / code-quality
- GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (13)
Never use `request.signal` to detect client disconnects. Use `getRequestAbortSignal()` from `app/services/httpAsyncStorage.server.ts`, which is wired to Express response close events.
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.ts
We use vitest exclusively. **Never mock anything** - use testcontainers instead.
📄 CodeRabbit inference engine (AGENTS.md)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.ts
Test files must not import `app/env.server.ts`; pass configuration as options instead.
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.ts
For dashboard changes, visually verify the running Remix app with Chrome DevTools MCP, using snapshots, screenshots, interaction, and console-message checks as appropriate.
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.tsapps/webapp/app/components/dashboard-agent/panel-layout.tsx
**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:
📄 CodeRabbit inference engine (AGENTS.md)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.tsapps/webapp/app/components/dashboard-agent/panel-layout.tsx
Add crumbs as you write code — not just when debugging. Mark lines with
📄 CodeRabbit inference engine (AGENTS.md)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.tsapps/webapp/app/components/dashboard-agent/panel-layout.tsx
Use zod for validation in packages/core and apps/webapp
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.tsapps/webapp/app/components/dashboard-agent/panel-layout.tsx
Do not import `env.server.ts` directly or indirectly into test files; instead pass environment-dependent values through options/parameters to make code testable
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.ts
Access environment variables through the `env` export of `env.server.ts` instead of directly accessing `process.env`
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.tsapps/webapp/app/components/dashboard-agent/panel-layout.tsx
Use vitest for all tests in the Trigger.dev repository
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.ts
Use function declarations instead of default exports
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.tsapps/webapp/app/components/dashboard-agent/panel-layout.tsx
Use types over interfaces for TypeScript
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.tsapps/webapp/app/components/dashboard-agent/panel-layout.tsx
When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)
Files:
apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.ts
…sweep Closing the panel already keeps the server turn alive (teardownCancelsTurn returns false for panel-closed and chat-switched). The missing piece was resume-on-reopen: opening a chat never marked it streaming, so the transport never resumed the live `.out` stream and a mid-turn reopen showed a stalled transcript. resolveOpenedChat now derives `streaming` from whether the fetched transcript still looks mid-turn (the same transcriptLooksUnfinished check already used to poll a just-settled turn), and DashboardAgentPanel already spreads it into ActiveChat, so useTriggerChatTransport's isStreaming resume engages on reopen. Chat-switch design decision: unchanged, matches the packet's preference — teardownCancelsTurn already keeps chat-switched turns alive, same as panel-closed, so switching away and back also resumes. A true SSE integration test is impractical in jsdom; resume-wiring.test.ts source-checks the streaming/isStreaming/stopGeneration wiring instead, alongside new resolveOpenedChat unit tests for the streaming derivation. Final comment sweep over the branch diff: every comment capped at two plain lines, non-critical/signpost ones removed.
…tore a comment clause transcriptLooksUnfinished only checked open investigations and in-flight tool parts, so a text-only assistant reply mid-stream derived streaming:false on reopen. OR in a new check: the last assistant message has a `text` part with state "streaming". resume-wiring.test.ts: extracted the transport's per-chat sessions option into chatSessionsOption (DashboardAgentChat.tsx) and test it behaviorally, feeding it a real resolveOpenedChat result instead of grepping for a source substring. Retitled the stopGeneration test to say what it actually checks (occurrence count, not call sites). panel-layout.tsx: restored the clause that FloatingAgentWindow owns the drag-vs-click filter, so the panel and the story stay identical.
…d streaming text Confirmed both, refuted neither: 1. FloatingAgentWindow (Devin BUG, panel-layout.tsx): fullscreen and floating returned different element trees, so children() sat at a different tree position in each — flipping fullscreen unmounted and remounted the chat panel, resetting its session and interrupting a live stream. Verified with a jsdom test (mount count 1 -> 2 on toggle) before fixing; now one tree shape, only classes/style and a conditional drag-handler object change. 2. mergeSettledMessages (CodeRabbit MAJOR, settled-transcript.ts): stillRunning only checked tool-* parts, so a settled text message never replaced its still-"streaming" in-memory copy — a resumed prose-only chat could stay stuck showing partial text after the server settled it. Widened stillRunning to also treat a `text` part with state "streaming" as running. Item 3 (crumbs markers) intentionally not touched.
| const classifyGesture = (event: PointerEvent) => { | ||
| if (gestureClassified.current) return; | ||
| gestureClassified.current = true; | ||
| ignoringGesture.current = !!(event.target as HTMLElement | null)?.closest(NO_DRAG_SELECTOR); | ||
| }; |
There was a problem hiding this comment.
🔍 Drag filter relies on event.target being the pressed element
The drag-vs-click filter reads (event.target).closest('[data-agent-no-drag]') to decide whether a gesture on a header control (e.g. Close) drags the window. This holds only if framer-motion's pan events report the DOM element under the pointer. If framer-motion pointer-captures the header motion.div, event.target becomes that div, closest returns null, and the window drags even from a no-drag control. The DOM tests inject the inner element as target, so they never exercise real framer-motion event delivery. Verify against framer-motion v10 pan behavior in a browser.
Was this helpful? React with 👍 or 👎 to provide feedback.
The Ask Trigger chat now opens as a floating window docked at the bottom right, replacing the right-hand column mode. The window can be dragged anywhere on the page by its header and resized from all edges and corners.