From 6e11d5a171706dc3d412d1965ded61f144c48213 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Jan 2026 14:32:32 +0000 Subject: [PATCH 1/9] feat(webapp): show beginning and end of task names in filter dropdown When task names are too long to fit in the dropdown, truncate in the middle (showing beginning...end) instead of just cutting off the end. This helps users identify tasks with colon-namespaced names where the unique part may be at the end. Show full name in tooltip on hover. --- .../components/primitives/MiddleTruncate.tsx | 148 ++++++++++++++++++ .../app/components/runs/v3/RunFilters.tsx | 3 +- 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 apps/webapp/app/components/primitives/MiddleTruncate.tsx diff --git a/apps/webapp/app/components/primitives/MiddleTruncate.tsx b/apps/webapp/app/components/primitives/MiddleTruncate.tsx new file mode 100644 index 0000000000..d0ecdd6dea --- /dev/null +++ b/apps/webapp/app/components/primitives/MiddleTruncate.tsx @@ -0,0 +1,148 @@ +import { useRef, useState, useLayoutEffect, useCallback } from "react"; +import { cn } from "~/utils/cn"; +import { SimpleTooltip } from "./Tooltip"; + +interface MiddleTruncateProps { + text: string; + className?: string; +} + +/** + * A component that truncates text in the middle, showing the beginning and end. + * Shows the full text in a tooltip on hover when truncated. + * + * Example: "namespace:category:subcategory:task-name" becomes "namespace:cat…task-name" + */ +export function MiddleTruncate({ text, className }: MiddleTruncateProps) { + const containerRef = useRef(null); + const measureRef = useRef(null); + const [displayText, setDisplayText] = useState(text); + const [isTruncated, setIsTruncated] = useState(false); + + const calculateTruncation = useCallback(() => { + const container = containerRef.current; + const measure = measureRef.current; + if (!container || !measure) return; + + const parent = container.parentElement; + if (!parent) return; + + // Get the available width from the parent container + const parentStyle = getComputedStyle(parent); + const availableWidth = + parent.clientWidth - + parseFloat(parentStyle.paddingLeft) - + parseFloat(parentStyle.paddingRight); + + // Measure full text width + measure.textContent = text; + const fullTextWidth = measure.offsetWidth; + + // If text fits, no truncation needed + if (fullTextWidth <= availableWidth) { + setDisplayText(text); + setIsTruncated(false); + return; + } + + // Text needs truncation - find optimal split + const ellipsis = "…"; + measure.textContent = ellipsis; + const ellipsisWidth = measure.offsetWidth; + + const targetWidth = availableWidth - ellipsisWidth - 4; // small buffer + + if (targetWidth <= 0) { + setDisplayText(ellipsis); + setIsTruncated(true); + return; + } + + // Binary search for optimal character counts + let startChars = 0; + let endChars = 0; + + // Alternate adding characters from start and end + while (startChars + endChars < text.length) { + // Try adding to start + const testStart = text.slice(0, startChars + 1); + const testEnd = endChars > 0 ? text.slice(-endChars) : ""; + measure.textContent = testStart + ellipsis + testEnd; + + if (measure.offsetWidth > targetWidth) break; + startChars++; + + if (startChars + endChars >= text.length) break; + + // Try adding to end + const newTestEnd = text.slice(-(endChars + 1)); + measure.textContent = text.slice(0, startChars) + ellipsis + newTestEnd; + + if (measure.offsetWidth > targetWidth) break; + endChars++; + } + + // Ensure minimum characters on each side for readability + const minChars = 4; + if (startChars < minChars && text.length > minChars * 2 + 1) { + startChars = minChars; + } + if (endChars < minChars && text.length > minChars * 2 + 1) { + endChars = minChars; + } + + // If combined chars would exceed text length, show full text + if (startChars + endChars >= text.length) { + setDisplayText(text); + setIsTruncated(false); + return; + } + + const result = text.slice(0, startChars) + ellipsis + text.slice(-endChars); + setDisplayText(result); + setIsTruncated(true); + }, [text]); + + useLayoutEffect(() => { + calculateTruncation(); + + // Recalculate on resize + const resizeObserver = new ResizeObserver(() => { + calculateTruncation(); + }); + + const container = containerRef.current; + if (container?.parentElement) { + resizeObserver.observe(container.parentElement); + } + + return () => { + resizeObserver.disconnect(); + }; + }, [calculateTruncation]); + + const content = ( + + {/* Hidden span for measuring text width */} + + ); + + if (isTruncated) { + return ( + {text}} + side="top" + asChild + /> + ); + } + + return content; +} diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 5695081816..d7c95ff21e 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -31,6 +31,7 @@ import { DateTime } from "~/components/primitives/DateTime"; import { FormError } from "~/components/primitives/FormError"; import { Input } from "~/components/primitives/Input"; import { Label } from "~/components/primitives/Label"; +import { MiddleTruncate } from "~/components/primitives/MiddleTruncate"; import { Paragraph } from "~/components/primitives/Paragraph"; import { ComboBox, @@ -654,7 +655,7 @@ function TasksDropdown({ } > - {item.slug} + ))} From 3370c968d84c5bfa7431806f4d0c16d8e832e0af Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Jan 2026 14:53:27 +0000 Subject: [PATCH 2/9] Widen task filter dropdown popup by 50% Increased the max-width of the TasksDropdown SelectPopover from 240px to 360px to provide more space for viewing task names, especially those with colon-namespaced naming conventions. --- apps/webapp/app/components/runs/v3/RunFilters.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index d7c95ff21e..b352f0855b 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -635,7 +635,7 @@ function TasksDropdown({ {trigger} { if (onClose) { onClose(); From e5d9927ccbf3dc60ee359d55f2788b661597e027 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Jan 2026 18:15:16 +0000 Subject: [PATCH 3/9] fix(webapp): set min-width for task filter dropdown when text is truncated Changed the SelectPopover min-width from 0 to 360px to ensure the dropdown always has adequate space for displaying task names with middle truncation. This prevents the dropdown from becoming too narrow when long task names are present. Slack thread: https://triggerdotdev.slack.com/archives/C032WA2S43F/p1769451219774789?thread_ts=1769436524.157219&cid=C032WA2S43F https://claude.ai/code/session_01KrFXrmK7jz9sWTcS8pMtXL --- apps/webapp/app/components/runs/v3/RunFilters.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index b352f0855b..d91874f80c 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -635,7 +635,7 @@ function TasksDropdown({ {trigger} { if (onClose) { onClose(); From 59c69b124bf0fb7f6cdd0aa13af29d4708f60bda Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Jan 2026 18:34:03 +0000 Subject: [PATCH 4/9] Revert "fix(webapp): set min-width for task filter dropdown when text is truncated" This reverts commit c769a57ac7404319db3ceb032ac7efd632866454. --- apps/webapp/app/components/runs/v3/RunFilters.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index d91874f80c..b352f0855b 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -635,7 +635,7 @@ function TasksDropdown({ {trigger} { if (onClose) { onClose(); From 22db328a6de445d77ecc81be3ba8c2bc75a6cf25 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Jan 2026 18:34:28 +0000 Subject: [PATCH 5/9] fix(webapp): set min-width on MiddleTruncate component when text is truncated When text is truncated, apply min-w-[360px] to the MiddleTruncate component to ensure the truncated text has adequate display space. This matches the max-width of the dropdown popup and prevents the component from becoming too narrow. Slack thread: https://triggerdotdev.slack.com/archives/C032WA2S43F/p1769451219774789?thread_ts=1769436524.157219&cid=C032WA2S43F https://claude.ai/code/session_01KrFXrmK7jz9sWTcS8pMtXL --- apps/webapp/app/components/primitives/MiddleTruncate.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/MiddleTruncate.tsx b/apps/webapp/app/components/primitives/MiddleTruncate.tsx index d0ecdd6dea..98d32a74cf 100644 --- a/apps/webapp/app/components/primitives/MiddleTruncate.tsx +++ b/apps/webapp/app/components/primitives/MiddleTruncate.tsx @@ -122,7 +122,10 @@ export function MiddleTruncate({ text, className }: MiddleTruncateProps) { }, [calculateTruncation]); const content = ( - + {/* Hidden span for measuring text width */} Date: Tue, 27 Jan 2026 17:22:50 +0000 Subject: [PATCH 6/9] fix(webapp): address review feedback for MiddleTruncate component - Changed interface to type alias to match repo TS style conventions - Added re-measurement after enforcing minChars to prevent overflow in narrow containers - Changed min-w-[360px] to min-w-full to prevent layout overflow when popover width is constrained on narrow viewports https://claude.ai/code/session_01KrFXrmK7jz9sWTcS8pMtXL --- .../components/primitives/MiddleTruncate.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/primitives/MiddleTruncate.tsx b/apps/webapp/app/components/primitives/MiddleTruncate.tsx index 98d32a74cf..d66f2f54ab 100644 --- a/apps/webapp/app/components/primitives/MiddleTruncate.tsx +++ b/apps/webapp/app/components/primitives/MiddleTruncate.tsx @@ -2,10 +2,10 @@ import { useRef, useState, useLayoutEffect, useCallback } from "react"; import { cn } from "~/utils/cn"; import { SimpleTooltip } from "./Tooltip"; -interface MiddleTruncateProps { +type MiddleTruncateProps = { text: string; className?: string; -} +}; /** * A component that truncates text in the middle, showing the beginning and end. @@ -84,6 +84,9 @@ export function MiddleTruncate({ text, className }: MiddleTruncateProps) { // Ensure minimum characters on each side for readability const minChars = 4; + const prevStartChars = startChars; + const prevEndChars = endChars; + if (startChars < minChars && text.length > minChars * 2 + 1) { startChars = minChars; } @@ -91,6 +94,16 @@ export function MiddleTruncate({ text, className }: MiddleTruncateProps) { endChars = minChars; } + // Re-measure after enforcing minChars to prevent overflow + if (startChars !== prevStartChars || endChars !== prevEndChars) { + measure.textContent = text.slice(0, startChars) + ellipsis + text.slice(-endChars); + if (measure.offsetWidth > targetWidth) { + // Revert to previous values if minChars enforcement causes overflow + startChars = prevStartChars; + endChars = prevEndChars; + } + } + // If combined chars would exceed text length, show full text if (startChars + endChars >= text.length) { setDisplayText(text); @@ -124,7 +137,7 @@ export function MiddleTruncate({ text, className }: MiddleTruncateProps) { const content = ( {/* Hidden span for measuring text width */} Date: Tue, 27 Jan 2026 17:37:42 +0000 Subject: [PATCH 7/9] fix(webapp): address CodeRabbit review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed misleading comment: "Binary search" → "Incrementally find" - Added ResizeObserver guard for jsdom/older browser compatibility https://claude.ai/code/session_01KrFXrmK7jz9sWTcS8pMtXL --- apps/webapp/app/components/primitives/MiddleTruncate.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/primitives/MiddleTruncate.tsx b/apps/webapp/app/components/primitives/MiddleTruncate.tsx index d66f2f54ab..e0b87cff07 100644 --- a/apps/webapp/app/components/primitives/MiddleTruncate.tsx +++ b/apps/webapp/app/components/primitives/MiddleTruncate.tsx @@ -58,7 +58,7 @@ export function MiddleTruncate({ text, className }: MiddleTruncateProps) { return; } - // Binary search for optimal character counts + // Incrementally find the optimal character counts let startChars = 0; let endChars = 0; @@ -119,7 +119,11 @@ export function MiddleTruncate({ text, className }: MiddleTruncateProps) { useLayoutEffect(() => { calculateTruncation(); - // Recalculate on resize + // Recalculate on resize (guard for jsdom/older browsers) + if (typeof ResizeObserver === "undefined") { + return; + } + const resizeObserver = new ResizeObserver(() => { calculateTruncation(); }); From 3df57679bc98c14fa3ccd857ad9f72c52ba8aca4 Mon Sep 17 00:00:00 2001 From: Oskar Otwinowski Date: Tue, 27 Jan 2026 19:09:40 +0100 Subject: [PATCH 8/9] fix(webapp): increase min width for run slug in filter dropdown Increase the minimum width for the run slug display inside the RunFilters select dropdown so long slugs are less likely to be truncated prematurely. Change MiddleTruncate to include a min-w-[360px] class on the SelectItem content to provide more room for slugs and improve readability in the runs filter UI. --- apps/webapp/app/components/runs/v3/RunFilters.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index b352f0855b..75f5306155 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -655,7 +655,7 @@ function TasksDropdown({ } > - + ))} From b011415d4277235c71a53b67aaacb784cd1c46c2 Mon Sep 17 00:00:00 2001 From: Oskar Otwinowski Date: Tue, 27 Jan 2026 19:16:25 +0100 Subject: [PATCH 9/9] feat(webapp): MiddleTruncate to take decent amount of space when possible min-w-[360px] class allows to keep reasonable spacing when there is only one item on the list --- apps/webapp/app/components/primitives/MiddleTruncate.tsx | 2 +- apps/webapp/app/components/runs/v3/RunFilters.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/primitives/MiddleTruncate.tsx b/apps/webapp/app/components/primitives/MiddleTruncate.tsx index e0b87cff07..c116205aed 100644 --- a/apps/webapp/app/components/primitives/MiddleTruncate.tsx +++ b/apps/webapp/app/components/primitives/MiddleTruncate.tsx @@ -141,7 +141,7 @@ export function MiddleTruncate({ text, className }: MiddleTruncateProps) { const content = ( {/* Hidden span for measuring text width */} } > - + ))}