From b6e144a611c894b046eb403d099c0b76f254152b Mon Sep 17 00:00:00 2001 From: scotttong Date: Sun, 22 Mar 2026 02:37:39 -0700 Subject: [PATCH] experiment: board chat split-pane fraction, markdown wrap, tooltips ResizeObserver-driven chat/agent pane split; wrapped markdown bubbles with horizontal scroll for pre/table; new thread/history affordances. Made-with: Cursor --- ui/src/pages/BoardChat.tsx | 159 +++++++++++++++++++++++++++++-------- 1 file changed, 124 insertions(+), 35 deletions(-) diff --git a/ui/src/pages/BoardChat.tsx b/ui/src/pages/BoardChat.tsx index 679a40dc..c7ccda9b 100644 --- a/ui/src/pages/BoardChat.tsx +++ b/ui/src/pages/BoardChat.tsx @@ -1,4 +1,11 @@ -import { useEffect, useState, useRef, useCallback, useMemo } from "react"; +import { + useEffect, + useLayoutEffect, + useState, + useRef, + useCallback, + useMemo, +} from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; @@ -14,7 +21,12 @@ import { DropdownMenuRadioItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { ListFilter, Send } from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { History, ListFilter, MessageSquarePlus, Send } from "lucide-react"; import { cn } from "../lib/utils"; /** @@ -25,6 +37,8 @@ import { cn } from "../lib/utils"; /** Hit zone to the right of the 1px line (line sits on chat pane’s right edge). */ const SPLIT_DIVIDER_PX = 12; const SPLIT_MIN_PANE_PX = 280; +/** Chat pane share of width below the divider (agent feed gets the rest). */ +const DEFAULT_CHAT_FRACTION = 2 / 3; const AGENT_FEED_FILTER_OPTIONS = [ { value: "all", label: "All" }, @@ -35,11 +49,12 @@ const AGENT_FEED_FILTER_OPTIONS = [ type AgentFeedFilterValue = (typeof AGENT_FEED_FILTER_OPTIONS)[number]["value"]; -/** One row of content; bubble scrolls horizontally so nothing wraps to a second line. */ -const BOARD_CHAT_MARKDOWN_SINGLE_LINE = - "!flex w-max max-w-none flex-nowrap items-center gap-x-2 [&>*]:!my-0 [&>*]:shrink-0 [&>*]:whitespace-nowrap [&>ul]:inline-flex [&>ul]:flex-nowrap [&>ul]:gap-x-2 [&>ul]:!m-0 [&>ul]:!py-0 [&>ul]:!pl-0 [&>ol]:inline-flex [&>ol]:flex-nowrap [&>ol]:gap-x-2 [&>ol]:!m-0 [&>ol]:!py-0 [&>ol]:!pl-0 [&_li]:whitespace-nowrap"; +/** Wrapped markdown in bubbles; pre/table scroll horizontally when needed. */ +const BOARD_CHAT_MARKDOWN_CLASS = + "max-w-full overflow-visible [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_pre]:max-w-full [&_pre]:overflow-x-auto [&_table]:block [&_table]:max-w-full [&_table]:overflow-x-auto"; -const boardChatBubbleShell = "min-w-0 max-w-[85%] overflow-x-auto overflow-y-hidden px-3 py-2 text-sm"; +const boardChatBubbleShell = + "min-w-0 max-w-[85%] break-words px-3 py-2 text-sm overflow-x-auto overflow-y-visible"; export function BoardChat() { const { selectedCompanyId, selectedCompany } = useCompany(); @@ -51,10 +66,42 @@ export function BoardChat() { }, [setBreadcrumbs]); const splitContainerRef = useRef(null); - const [leftPaneWidth, setLeftPaneWidth] = useState(480); + const [containerWidth, setContainerWidth] = useState(0); + const [chatPaneFraction, setChatPaneFraction] = useState(DEFAULT_CHAT_FRACTION); const splitDragging = useRef(false); const [agentFeedFilter, setAgentFeedFilter] = useState("all"); + useLayoutEffect(() => { + const el = splitContainerRef.current; + if (!el) return; + const ro = new ResizeObserver(() => { + setContainerWidth(el.clientWidth); + }); + ro.observe(el); + setContainerWidth(el.clientWidth); + return () => ro.disconnect(); + }, []); + + const innerWidth = Math.max(0, containerWidth - SPLIT_DIVIDER_PX); + const splitLowerPx = SPLIT_MIN_PANE_PX; + const splitUpperPx = innerWidth - SPLIT_MIN_PANE_PX; + const minChatFraction = + innerWidth > 0 ? Math.min(1, SPLIT_MIN_PANE_PX / innerWidth) : 0; + const maxChatFraction = + innerWidth > 0 ? Math.max(0, 1 - SPLIT_MIN_PANE_PX / innerWidth) : 1; + const leftPaneWidth = + innerWidth > 0 + ? splitUpperPx < splitLowerPx + ? Math.max(0, Math.round(innerWidth / 2)) + : Math.round( + innerWidth * + Math.min( + maxChatFraction, + Math.max(minChatFraction, chatPaneFraction), + ), + ) + : 0; + const handleSplitDragStart = useCallback( (e: React.MouseEvent) => { e.preventDefault(); @@ -64,15 +111,17 @@ export function BoardChat() { const onMouseMove = (ev: MouseEvent) => { if (!splitDragging.current) return; - const containerW = splitContainerRef.current?.clientWidth ?? startWidth + 400; + const containerW = splitContainerRef.current?.clientWidth ?? containerWidth; const inner = containerW - SPLIT_DIVIDER_PX; const lower = SPLIT_MIN_PANE_PX; const upper = inner - SPLIT_MIN_PANE_PX; const next = startWidth + ev.clientX - startX; + if (inner <= 0) return; if (upper < lower) { - setLeftPaneWidth(Math.max(0, Math.round(inner / 2))); + setChatPaneFraction(0.5); } else { - setLeftPaneWidth(Math.min(upper, Math.max(lower, next))); + const clamped = Math.min(upper, Math.max(lower, next)); + setChatPaneFraction(clamped / inner); } }; @@ -85,7 +134,7 @@ export function BoardChat() { document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }, - [leftPaneWidth], + [containerWidth, leftPaneWidth], ); const [input, setInput] = useState(""); @@ -315,22 +364,57 @@ export function BoardChat() { ref={splitContainerRef} className="flex min-h-0 min-w-0 flex-1 flex-row" > - {/* Left: chat (self-contained pane) */} + {/* Left: chat (self-contained pane) — 2/3 default until container is measured for drag math */}
0 ? { width: leftPaneWidth } : undefined} > -
+
-

- {ceoAgent?.name ?? "Board Room"} -

-

- {selectedCompany?.name ?? "Your company"} -

+
+

+ {ceoAgent?.name ?? "Board Room"} +

+

+ {selectedCompany?.name ?? "Your company"} +

+
+
+ + + + + chat history + + + + + + new chat + +
{/* Messages — scroll viewport flush right so the scrollbar sits on the pane/divider edge */}
@@ -374,7 +458,7 @@ export function BoardChat() { : "bg-muted text-foreground [border-radius:12px_12px_12px_0px]", )} > - + {comment.body ?? ""}
@@ -388,7 +472,7 @@ export function BoardChat() {
{optimisticMessage} @@ -405,7 +489,7 @@ export function BoardChat() { "bg-muted text-foreground [border-radius:12px_12px_12px_0px]", )} > - {streamingText} + {streamingText}
)} @@ -483,17 +567,22 @@ export function BoardChat() {

- - - + + + + + + + filter by +