Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ export const BranchToolbar = memo(function BranchToolbar({
if (!hasActiveThread || !activeProject) return null;

return (
<div className="chat-composer-context-strip -mt-4 mx-auto flex w-[calc(100%-2.75rem)] max-w-[calc(48rem-2.75rem)] items-center gap-2 px-1 pt-5 pb-1">
<div className="chat-composer-context-strip -mt-4 mx-auto flex w-[calc(100%-2.75rem)] max-w-[calc(var(--chat-content-max-width,48rem)-2.75rem)] items-center gap-2 px-1 pt-5 pb-1">
{isMobile ? (
<MobileRunContextSelector
envLocked={envLocked}
Expand Down
5 changes: 1 addition & 4 deletions apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1543,10 +1543,7 @@ function ChatMarkdown({

return (
<div
className={cn(
"chat-markdown w-full min-w-0 text-sm leading-relaxed text-foreground/80",
className,
)}
className={cn("chat-markdown w-full min-w-0 leading-relaxed text-foreground/80", className)}
onCopy={handleCopy}
>
<ReactMarkdown
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5614,6 +5614,8 @@ function ChatViewContent(props: ChatViewProps) {
rightPanelMaximized ? "w-0 flex-none" : "flex-1",
)}
data-chat-column-maximized-away={rightPanelMaximized ? "true" : "false"}
data-transcript-text-size={settings.transcriptTextSize}
data-transcript-width={settings.transcriptWidth}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Details summary ignores text size

Medium Severity

ThreadErrorBanner still caps at hardcoded 48rem while living under the new data-transcript-width column. Composer banners now follow --chat-content-max-width, so Narrow/Wide leaves the thread error banner misaligned with the transcript and composer.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 160179d. Configure here.

>
{/* Top bar */}
<header
Expand Down Expand Up @@ -5707,6 +5709,7 @@ function ChatViewContent(props: ChatViewProps) {
onManualNavigation={cancelTimelineLiveFollowForUserNavigation}
hideEmptyPlaceholder={isDraftHeroState}
topFadeEnabled={!hasTimelineTopBanner}
transcriptWidth={settings.transcriptWidth}
/>

{/* scroll to end pill — shown when user has scrolled away from the live edge */}
Expand Down Expand Up @@ -5776,7 +5779,7 @@ function ChatViewContent(props: ChatViewProps) {
>
<div
className={cn(
"chat-composer-glass-shell relative mx-auto w-full max-w-3xl",
"chat-composer-glass-shell relative mx-auto w-full max-w-[var(--chat-content-max-width,48rem)]",
showComposerContextStrip && "chat-composer-glass-shell-with-context",
)}
>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2197,7 +2197,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
<form
ref={composerFormRef}
onSubmit={submitComposer}
className="mx-auto w-full min-w-0 max-w-3xl"
className="mx-auto w-full min-w-0 max-w-[var(--chat-content-max-width,48rem)]"
data-chat-composer-form="true"
>
<div
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/components/chat/ComposerBannerStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
};

return (
<div className={cn("group/banner-stack mx-auto mb-2 max-w-3xl", className)}>
<div
className={cn(
"group/banner-stack mx-auto mb-2 max-w-[var(--chat-content-max-width,48rem)]",
className,
)}
>
<div
className={cn(
"relative flex flex-col-reverse",
Expand Down
15 changes: 4 additions & 11 deletions apps/web/src/components/chat/MessagesTimeline.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export const MAX_VISIBLE_WORK_LOG_ENTRIES = 1;
export const TIMELINE_MINIMAP_ITEM_SPACING = 8;
export const TIMELINE_MINIMAP_MIN_ITEMS = 2;
export const TIMELINE_MINIMAP_MAX_HEIGHT_CSS = "calc(100vh - 18rem)";
export const TIMELINE_CONTENT_MAX_WIDTH = 768;
export const TIMELINE_MINIMAP_PERSISTENT_GUTTER = 48;

export interface TimelineEndState {
Expand Down Expand Up @@ -54,13 +53,10 @@ export function resolveTimelineMinimapIndexFromPointer(input: {
return Math.max(0, Math.min(input.itemCount - 1, Math.round(progress * (input.itemCount - 1))));
}

export function resolveTimelineMinimapHasPersistentGutter(viewportWidth: number): boolean {
if (!Number.isFinite(viewportWidth) || viewportWidth <= 0) {
export function resolveTimelineMinimapHasPersistentGutter(sideGutter: number): boolean {
if (!Number.isFinite(sideGutter) || sideGutter <= 0) {
return false;
}

const contentWidth = Math.min(viewportWidth, TIMELINE_CONTENT_MAX_WIDTH);
const sideGutter = Math.max(0, (viewportWidth - contentWidth) / 2);
return sideGutter >= TIMELINE_MINIMAP_PERSISTENT_GUTTER;
}

Expand All @@ -75,13 +71,10 @@ export const TIMELINE_MINIMAP_EXPANDED_HIT_STRIP_WIDTH = "22rem";
* text and swallow its pointer events. Cap the strip's width so it never
* extends past the gutter into the content column; 0 disables the strip.
*/
export function resolveTimelineMinimapHitStripWidth(viewportWidth: number): number {
if (!Number.isFinite(viewportWidth) || viewportWidth <= 0) {
export function resolveTimelineMinimapHitStripWidth(sideGutter: number): number {
if (!Number.isFinite(sideGutter) || sideGutter <= 0) {
return 0;
}

const contentWidth = Math.min(viewportWidth, TIMELINE_CONTENT_MAX_WIDTH);
const sideGutter = Math.max(0, (viewportWidth - contentWidth) / 2);
return Math.max(
0,
Math.min(
Expand Down
19 changes: 10 additions & 9 deletions apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ function buildProps() {
contentInsetEndAdjustment: 0,
onIsAtEndChange: () => {},
onManualNavigation: () => {},
transcriptWidth: "medium" as const,
};
}

Expand Down Expand Up @@ -330,21 +331,21 @@ describe("MessagesTimeline", () => {
pointerY: 999,
}),
).toBe(100);
expect(resolveTimelineMinimapHasPersistentGutter(832)).toBe(false);
expect(resolveTimelineMinimapHasPersistentGutter(863)).toBe(false);
expect(resolveTimelineMinimapHasPersistentGutter(864)).toBe(true);
expect(resolveTimelineMinimapHasPersistentGutter(32)).toBe(false);
expect(resolveTimelineMinimapHasPersistentGutter(47.5)).toBe(false);
expect(resolveTimelineMinimapHasPersistentGutter(48)).toBe(true);

// No usable gutter (zoomed in / narrow pane): the strip must go inert
// instead of overlaying the centered content column.
expect(resolveTimelineMinimapHitStripWidth(768)).toBe(0);
expect(resolveTimelineMinimapHitStripWidth(792)).toBe(0);
expect(resolveTimelineMinimapHitStripWidth(0)).toBe(0);
expect(resolveTimelineMinimapHitStripWidth(12)).toBe(0);
// Partial gutter: strip shrinks to what fits between the viewport edge
// and the content column.
expect(resolveTimelineMinimapHitStripWidth(820)).toBe(14);
expect(resolveTimelineMinimapHitStripWidth(26)).toBe(14);
expect(resolveTimelineMinimapHitStripWidth(20)).toBe(8);
// Full gutter: unchanged 40px-wide strip.
expect(resolveTimelineMinimapHitStripWidth(872)).toBe(40);
expect(resolveTimelineMinimapHitStripWidth(1400)).toBe(40);
expect(resolveTimelineMinimapHitStripWidth(0)).toBe(0);
expect(resolveTimelineMinimapHitStripWidth(52)).toBe(40);
expect(resolveTimelineMinimapHitStripWidth(316)).toBe(40);
expect(resolveTimelineMinimapHitStripWidth(Number.NaN)).toBe(0);

// The collapsed target stays narrow, but an open preview keeps its full
Expand Down
29 changes: 20 additions & 9 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ import {
} from "~/lib/previewAnnotation";
import { cn } from "~/lib/utils";
import { useUiStateStore } from "~/uiStateStore";
import { type TimestampFormat } from "@t3tools/contracts/settings";
import { type TimestampFormat, type TranscriptWidth } from "@t3tools/contracts/settings";
import { formatChatTimestampTooltip, formatShortTimestamp } from "../../timestampFormat";

import {
Expand Down Expand Up @@ -184,6 +184,7 @@ interface MessagesTimelineProps {
onManualNavigation: () => void;
hideEmptyPlaceholder?: boolean;
topFadeEnabled?: boolean;
transcriptWidth: TranscriptWidth;
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -219,6 +220,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
onManualNavigation,
hideEmptyPlaceholder = false,
topFadeEnabled = false,
transcriptWidth,
}: MessagesTimelineProps) {
const [expandedTurnIds, setExpandedTurnIds] = useState<ReadonlySet<TurnId>>(new Set());
const [expandedWorkGroupIds, setExpandedWorkGroupIds] = useState<ReadonlySet<string>>(new Set());
Expand Down Expand Up @@ -396,12 +398,18 @@ export const MessagesTimeline = memo(function MessagesTimeline({
}

const measure = () => {
const viewportWidth = timelineViewportElement.getBoundingClientRect().width;
const nextHasPersistentGutter = resolveTimelineMinimapHasPersistentGutter(viewportWidth);
const viewportLeft = timelineViewportElement.getBoundingClientRect().left;
const contentElement = timelineViewportElement.querySelector<HTMLElement>(
'[data-timeline-root="true"]',
);
const sideGutter = contentElement
? Math.max(0, contentElement.getBoundingClientRect().left - viewportLeft)
: 0;
const nextHasPersistentGutter = resolveTimelineMinimapHasPersistentGutter(sideGutter);
setMinimapHasPersistentGutter((current) =>
current === nextHasPersistentGutter ? current : nextHasPersistentGutter,
);
setMinimapHitStripWidth(resolveTimelineMinimapHitStripWidth(viewportWidth));
setMinimapHitStripWidth(resolveTimelineMinimapHitStripWidth(sideGutter));
};

const frame = requestAnimationFrame(measure);
Expand All @@ -413,7 +421,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
cancelAnimationFrame(frame);
observer.disconnect();
};
}, [timelineViewportElement, rows.length]);
}, [timelineViewportElement, rows.length, transcriptWidth]);

const sharedState = useMemo<TimelineRowSharedState>(
() => ({
Expand Down Expand Up @@ -460,7 +468,10 @@ export const MessagesTimeline = memo(function MessagesTimeline({
// from TimelineRowCtx, which propagates through LegendList's memo.
const renderItem = useCallback(
({ item }: { item: MessagesTimelineRow }) => (
<div className="mx-auto w-full min-w-0 max-w-3xl overflow-x-clip" data-timeline-root="true">
<div
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
className="mx-auto w-full min-w-0 max-w-[var(--chat-content-max-width,48rem)] overflow-x-clip"
data-timeline-root="true"
>
<TimelineRowContent row={item} />
</div>
),
Expand Down Expand Up @@ -1585,7 +1596,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {
}

return (
<div className="whitespace-pre-wrap wrap-break-word text-sm leading-relaxed text-foreground">
<div className="whitespace-pre-wrap wrap-break-word text-[length:var(--transcript-text-size,0.875rem)] leading-relaxed text-foreground">
{inlineNodes}
</div>
);
Expand Down Expand Up @@ -1623,7 +1634,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {
}

return (
<div className="whitespace-pre-wrap wrap-break-word text-sm leading-relaxed text-foreground">
<div className="whitespace-pre-wrap wrap-break-word text-[length:var(--transcript-text-size,0.875rem)] leading-relaxed text-foreground">
{inlineNodes}
</div>
);
Expand Down Expand Up @@ -1664,7 +1675,7 @@ function UserMessageReviewCommentCard({ comment }: { comment: ReviewCommentConte
</div>
</div>
{comment.text.length > 0 && (
<div className="whitespace-pre-wrap wrap-break-word text-sm">
<div className="whitespace-pre-wrap wrap-break-word text-[length:var(--transcript-text-size,0.875rem)]">
<SkillInlineText text={comment.text} skills={ctx.skills} />
</div>
)}
Expand Down
95 changes: 95 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { Button } from "../ui/button";
import { DraftInput } from "../ui/draft-input";
import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
import { Switch } from "../ui/switch";
import { Toggle, ToggleGroup } from "../ui/toggle-group";
import { stackedThreadToast, toastManager } from "../ui/toast";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog";
Expand Down Expand Up @@ -114,6 +115,18 @@ const THEME_OPTIONS = [
},
] as const;

const TRANSCRIPT_TEXT_SIZE_OPTIONS = [
{ value: "small", label: "Small" },
{ value: "medium", label: "Medium" },
{ value: "large", label: "Large" },
] as const;

const TRANSCRIPT_WIDTH_OPTIONS = [
{ value: "narrow", label: "Narrow" },
{ value: "medium", label: "Medium" },
{ value: "wide", label: "Wide" },
] as const;

const TIMESTAMP_FORMAT_LABELS = {
locale: "System default",
"12-hour": "12-hour",
Expand Down Expand Up @@ -404,6 +417,12 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat
? ["Time format"]
: []),
...(settings.transcriptTextSize !== DEFAULT_UNIFIED_SETTINGS.transcriptTextSize
? ["Transcript text size"]
: []),
...(settings.transcriptWidth !== DEFAULT_UNIFIED_SETTINGS.transcriptWidth
? ["Transcript width"]
: []),
...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount
? ["Visible threads"]
: []),
Expand Down Expand Up @@ -463,6 +482,8 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.sidebarProjectGroupingMode,
settings.sidebarThreadPreviewCount,
settings.timestampFormat,
settings.transcriptTextSize,
settings.transcriptWidth,
settings.wordWrap,
theme,
],
Expand All @@ -481,6 +502,8 @@ export function useSettingsRestore(onRestored?: () => void) {
setTheme("system");
updateSettings({
timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
transcriptTextSize: DEFAULT_UNIFIED_SETTINGS.transcriptTextSize,
transcriptWidth: DEFAULT_UNIFIED_SETTINGS.transcriptWidth,
wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,
Expand Down Expand Up @@ -588,6 +611,78 @@ export function GeneralSettingsPanel() {
}
/>

<SettingsRow
title="Transcript text size"
description="Size of the conversation transcript text."
resetAction={
settings.transcriptTextSize !== DEFAULT_UNIFIED_SETTINGS.transcriptTextSize ? (
<SettingResetButton
label="transcript text size"
onClick={() =>
updateSettings({
transcriptTextSize: DEFAULT_UNIFIED_SETTINGS.transcriptTextSize,
})
}
/>
) : null
}
control={
<ToggleGroup
variant="outline"
size="sm"
value={[settings.transcriptTextSize]}
onValueChange={(value) => {
const nextValue = value[0];
if (nextValue === "small" || nextValue === "medium" || nextValue === "large") {
updateSettings({ transcriptTextSize: nextValue });
}
}}
aria-label="Transcript text size"
>
{TRANSCRIPT_TEXT_SIZE_OPTIONS.map((option) => (
<Toggle key={option.value} value={option.value} className="min-w-16 px-2">
{option.label}
</Toggle>
))}
</ToggleGroup>
}
/>

<SettingsRow
title="Transcript width"
description="Maximum width of the transcript and composer columns."
resetAction={
settings.transcriptWidth !== DEFAULT_UNIFIED_SETTINGS.transcriptWidth ? (
<SettingResetButton
label="transcript width"
onClick={() =>
updateSettings({ transcriptWidth: DEFAULT_UNIFIED_SETTINGS.transcriptWidth })
}
/>
) : null
}
control={
<ToggleGroup
variant="outline"
size="sm"
value={[settings.transcriptWidth]}
onValueChange={(value) => {
const nextValue = value[0];
if (nextValue === "narrow" || nextValue === "medium" || nextValue === "wide") {
updateSettings({ transcriptWidth: nextValue });
}
}}
aria-label="Transcript width"
>
{TRANSCRIPT_WIDTH_OPTIONS.map((option) => (
<Toggle key={option.value} value={option.value} className="min-w-16 px-2">
{option.label}
</Toggle>
))}
</ToggleGroup>
}
/>

<SettingsRow
title="Glass opacity"
description="Control how transparent glass surfaces are. Higher values make menus, dialogs, and the composer more solid."
Expand Down
Loading
Loading