Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
933d004
feat(web): customize chat timeline backgrounds
maria-rcks Sep 16, 2026
9bc9ed1
fix(web): preserve worktree activity in wallpaper previews
maria-rcks Sep 16, 2026
5926b37
chore: integrate main into wallpaper customization
maria-rcks Sep 21, 2026
b170f3f
feat(web): support custom CSS in themes
maria-rcks Sep 21, 2026
bf3b0f6
fix(web): preserve imported CSS and wallpaper scroll geometry
maria-rcks Sep 21, 2026
6bd45e9
fix(web): preserve appearance customization across current main
maria-rcks Oct 6, 2026
b91efc4
Merge commit '17c0878941ab8ab19108fb639c5138d92a89e919' into HEAD
maria-rcks Oct 6, 2026
40109d3
test(web): isolate settings restore from browser workers
maria-rcks Oct 6, 2026
edb2682
fix(web): keep changed file header readable over wallpaper
maria-rcks Oct 6, 2026
7fd66c8
fix(web): keep theme previews and wallpaper readable
maria-rcks Oct 7, 2026
30eae09
fix(web): sync draft theme surfaces in html previews
maria-rcks Oct 7, 2026
0bebee4
fix(web): keep chat metadata and warnings readable over wallpaper
maria-rcks Oct 7, 2026
3fe6aa5
fix(web): keep chat glass and warnings readable over wallpaper
maria-rcks Oct 7, 2026
bb9d6f7
fix(web): strengthen blockquote text over chat wallpaper
maria-rcks Oct 7, 2026
c1750f9
fix(web): preserve markdown link contrast over wallpaper
maria-rcks Oct 7, 2026
99b77b3
fix(web): preserve custom theme message color pairing
maria-rcks Oct 7, 2026
42b30bb
fix(web): keep changed-file glass continuous at rest
maria-rcks Oct 7, 2026
68169dd
fix(web): refresh sticky glass backing with header inset
maria-rcks Oct 7, 2026
2be9c36
fix(web): measure sticky headers after scroll restoration
maria-rcks Oct 7, 2026
e722bb5
fix(web): recheck sticky placement after card layout
maria-rcks Oct 7, 2026
7fcb18d
Merge remote-tracking branch 'origin/main' into t3code/customize-time…
maria-rcks Oct 9, 2026
42c6a06
refactor(web): drop unused wallpaper header inset prop and textarea v…
maria-rcks Oct 9, 2026
c707092
chore: merge main into theme CSS and chat wallpaper
maria-rcks Nov 10, 2026
6dd6305
fix(web): wallpaper and theme CSS pass main's lint and compiler checks
maria-rcks Nov 10, 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
5 changes: 5 additions & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const clientSettings: ClientSettings = {
notificationMode: "notifications-and-sound",
inAppNotificationsEnabled: true,
appearanceContrast: 100,
timelineBackgroundImage: "https://example.com/wallpaper.png",
timelineBackgroundOpacity: 47,
timelineBackgroundBlur: 8,
timelineTextShadowOpacity: 72,
timelineTextShadowBlur: 4,
browserDefaultViewport: { _tag: "preset", width: 1024, height: 600, presetId: "nest-hub" },
browserDefaultZoomFactor: 1.25,
browserDefaultAppearance: "dark",
Expand Down
7 changes: 7 additions & 0 deletions apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@
RESERVED_THEME_IDS.includes(value.id) ||
!isThemeLabel(value.label) ||
!isThemeAppearance(value.appearance) ||
(value.css !== undefined && typeof value.css !== "string") ||
!isRecord(value.colors)
) {
return false;
Expand Down Expand Up @@ -372,6 +373,12 @@
}
}
setThemeColor(chromeColor);
if (effectiveCustomTheme?.css) {
const style = document.createElement("style");
style.dataset.themeCss = "";
style.textContent = effectiveCustomTheme.css;
document.head.append(style);
}
} catch {
let prefersDark = true;
try {
Expand Down
31 changes: 26 additions & 5 deletions apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ import { createMarkdownRenderer } from "./chat/markdownRenderer";
import { splitMarkdownBlocks } from "./chat/markdownStreamBlocks";
import { MarkdownMermaidCodeBlock } from "./chat/MarkdownMermaidCodeBlock";
import { MarkdownTable } from "./chat/MarkdownTable";
import { CHAT_BACKGROUND_GLASS_SURFACE_CLASSES } from "./chat/ChatTimelineBackground";
import { MarkdownDetails } from "./chat/MarkdownDetails";
import {
authoredImageSizeStyle,
Expand Down Expand Up @@ -227,6 +228,8 @@ const isAssetNotFoundError = Schema.is(AssetWorkspaceAssetNotFoundError);
interface ChatMarkdownProps {
/** Compact markdown transforms remove blocks before rendering inside a label. */
inline?: boolean;
/** Enable translucent containers only when rendering over a timeline wallpaper. */
glassSurfaces?: boolean;
text: string;
cwd: string | undefined;
threadRef?: ScopedThreadRef | undefined;
Expand Down Expand Up @@ -279,6 +282,7 @@ const ARTIFACT_TEMPLATE_ICON_BY_KIND = {
function CodexArtifactTemplateCard(props: {
readonly template: CodexArtifactTemplate;
readonly onUse?: ((template: CodexArtifactTemplate) => void) | undefined;
readonly glass?: boolean;
}) {
const Icon = ARTIFACT_TEMPLATE_ICON_BY_KIND[props.template.artifactKind];
const presentationLabel = codexArtifactTemplatePresentationLabel(props.template.artifactKind);
Expand All @@ -288,7 +292,10 @@ function CodexArtifactTemplateCard(props: {
role="group"
aria-label={`${props.template.displayName} template`}
data-chat-markdown-artifact-template
className="my-[0.65rem] flex w-full min-w-0 items-center gap-3 rounded-xl border border-border/70 bg-card/60 px-3 py-2.5 text-foreground shadow-xs"
className={cn(
"my-[0.65rem] flex w-full min-w-0 items-center gap-3 rounded-xl border border-border/70 px-3 py-2.5 text-foreground shadow-xs",
props.glass ? CHAT_BACKGROUND_GLASS_SURFACE_CLASSES : "bg-card/60",
)}
data-artifact-kind={props.template.artifactKind}
data-markdown-copy={`${props.template.displayName} (${presentationLabel})\n\n`}
data-skill-name={props.template.skillName}
Expand Down Expand Up @@ -497,6 +504,7 @@ function useChatMarkdownState({
onImageExpand,
renderContextReference,
headingLevelOffset = 0,
glassSurfaces = false,
sourceControlMedia = false,
sourceControlMediaRepository,
}: ChatMarkdownProps) {
Expand Down Expand Up @@ -899,6 +907,7 @@ function useChatMarkdownState({
sourceControlMediaRepository,
renderContextReference,
headingLevelOffset,
glassSurfaces,
imageBaseDir,
workspaceMutationId,
inlineCodeFileLinkMetaByText,
Expand Down Expand Up @@ -968,11 +977,15 @@ const CHAT_MARKDOWN_COMPONENTS = {
h5: markdownHeadingRenderer(5),
h6: markdownHeadingRenderer(6),
div: function MarkdownDiv({ node, children, ...props }) {
const { onUseArtifactTemplate } = use(ChatMarkdownRendererContext);
const { onUseArtifactTemplate, glassSurfaces } = use(ChatMarkdownRendererContext);
const artifactTemplate = artifactTemplateFromHastProperties(node?.properties);
if (artifactTemplate) {
return (
<CodexArtifactTemplateCard template={artifactTemplate} onUse={onUseArtifactTemplate} />
<CodexArtifactTemplateCard
template={artifactTemplate}
onUse={onUseArtifactTemplate}
glass={glassSurfaces}
/>
);
}
return <div {...props}>{children}</div>;
Expand Down Expand Up @@ -1564,10 +1577,16 @@ const CHAT_MARKDOWN_COMPONENTS = {
return <ChatMarkdownImageFallback alt={altText} copyMarkdown={copyMarkdown} kind={kind} />;
},
table: function MarkdownTableRenderer({ node: _node, ...props }) {
return <MarkdownTable {...props} />;
const { glassSurfaces } = use(ChatMarkdownRendererContext);
return <MarkdownTable {...props} glass={glassSurfaces} />;
},
details: function MarkdownDetailsRenderer({ node: _node, children, open: detailsOpen }) {
return <MarkdownDetails open={detailsOpen}>{children}</MarkdownDetails>;
const { glassSurfaces } = use(ChatMarkdownRendererContext);
return (
<MarkdownDetails open={detailsOpen} glass={glassSurfaces}>
{children}
</MarkdownDetails>
);
},
pre: function MarkdownPre({ node, children, ...props }) {
const {
Expand All @@ -1578,6 +1597,7 @@ const CHAT_MARKDOWN_COMPONENTS = {
onRunShellCommand,
inline,
mathEnabled,
glassSurfaces,
} = use(ChatMarkdownRendererContext);
const text = use(ChatMarkdownTextContext);
const codeBlock = extractCodeBlock(children);
Expand Down Expand Up @@ -1647,6 +1667,7 @@ const CHAT_MARKDOWN_COMPONENTS = {
: undefined
}
isStreaming={isStreaming}
glass={glassSurfaces}
>
{highlightedCode}
</MarkdownCodeBlock>
Expand Down
73 changes: 55 additions & 18 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { ChildThreadApprovals } from "./chat/ChildThreadApprovals";
import { CHAT_LIST_ANCHOR_OFFSET } from "@t3tools/shared/chatList";
import {
ChatTimelineBackground,
CHAT_BACKGROUND_TEXT_SHADOW_CLASSES,
useHasTimelineBackground,
useTimelineTextShadowStyle,
} from "./chat/ChatTimelineBackground";
import { ChatTopbarBlur } from "./chat/ChatTopbarBlur";
import { CHAT_TIMELINE_ANCHOR_OFFSET } from "./chat/timelineScrollAnchoring";
import { workspaceAreaProps } from "../lib/workspaceFocus";
import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
import { resolveNewThreadModelSelection } from "@t3tools/shared/usageLimits";
Expand Down Expand Up @@ -322,10 +331,7 @@ import {
latestProviderTurnTokenUsage,
shouldOfferResumeCompaction,
} from "@t3tools/client-runtime/state/context-window";
import {
MOBILE_COMPOSER_VIEW_TRANSITION_NAME,
MOBILE_DRAFT_HEADLINE_VIEW_TRANSITION_NAME,
} from "./chat/draftHeroTransition";
import { MOBILE_DRAFT_HEADLINE_VIEW_TRANSITION_NAME } from "./chat/draftHeroTransition";
import {
buildLocalDraftThread,
deriveCommittedServerUserMessageIds,
Expand Down Expand Up @@ -557,6 +563,22 @@ export default function ChatView(props: ChatViewProps) {
return ids;
})();
const settings = useEnvironmentSettings(environmentId);
const hasTimelineBackground = useHasTimelineBackground();
const timelineTextShadowStyle = useTimelineTextShadowStyle();
const [chatHeaderElement, setChatHeaderElement] = useState<HTMLElement | null>(null);
const [chatHeaderHeight, setChatHeaderHeight] = useState(0);
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- measures the header DOM element so wallpaper rows can scroll beneath it
useLayoutEffect(() => {
if (!chatHeaderElement || !hasTimelineBackground) return;
return observeResize(chatHeaderElement, () =>
setChatHeaderHeight(chatHeaderElement.getBoundingClientRect().height),
);
}, [chatHeaderElement, hasTimelineBackground]);
const timelineUnderHeader = hasTimelineBackground && !embedded;
const timelineHeaderInset = timelineUnderHeader ? chatHeaderHeight : 0;
const timelineAnchorOffset =
(hasTimelineBackground ? CHAT_TIMELINE_ANCHOR_OFFSET : CHAT_LIST_ANCHOR_OFFSET) +
timelineHeaderInset;
const clientSettingsHydrated = useClientSettingsHydrated();
const timestampFormat = settings.timestampFormat;
const navigate = useNavigate();
Expand Down Expand Up @@ -687,6 +709,10 @@ export default function ChatView(props: ChatViewProps) {
const isMobileViewport = useMediaQuery("max-sm");
const [workspaceLayoutElement, setWorkspaceLayoutElement] = useState<HTMLDivElement | null>(null);
const threadPanelPopoverAnchorRef = useRef<HTMLElement | null>(null);
const attachChatHeader = (element: HTMLElement | null) => {
threadPanelPopoverAnchorRef.current = element;
setChatHeaderElement(element);
};
const [terminalUiLaunchContext, setTerminalUiLaunchContext] =
useState<TerminalLaunchContext | null>(null);
// Overrides chosen before a server thread's first turn; they belong to that thread only.
Expand Down Expand Up @@ -2103,6 +2129,7 @@ export default function ChatView(props: ChatViewProps) {
timelineEntries,
timelineAnchorMessageId,
setTimelineAnchor,
anchorViewOffset: timelineAnchorOffset,
});

// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- schedules a frame to focus the composer DOM element when a thread opens with the terminal closed
Expand Down Expand Up @@ -3271,19 +3298,22 @@ export default function ChatView(props: ChatViewProps) {
{rightPanelControlsAtRoot && !embedded ? panelLayoutControls : null}
<div
className={cn(
"flex min-h-0 min-w-0 flex-col overflow-x-hidden",
"relative flex min-h-0 min-w-0 flex-col overflow-x-hidden",
rightPanelMaximized ? "w-0 flex-none" : "flex-1",
)}
data-chat-column-maximized-away={rightPanelMaximized ? "true" : "false"}
data-chat-column
inert={rightPanelMaximized}
style={timelineTextShadowStyle}
>
<ChatTimelineBackground className="z-0" />
Comment thread
maria-rcks marked this conversation as resolved.
Resolved
{timelineUnderHeader ? <ChatTopbarBlur /> : null}
{embedded ? null : (
<header
ref={threadPanelPopoverAnchorRef}
ref={attachChatHeader}
data-chat-header
className={cn(
"relative bg-background [[data-panel-animations=true]_&]:motion-safe:transition-[padding-left] [[data-panel-animations=true]_&]:motion-safe:duration-(--panel-animation-duration) [[data-panel-animations=true]_&]:motion-safe:ease-out",
"relative [[data-panel-animations=true]_&]:motion-safe:transition-[padding-left] [[data-panel-animations=true]_&]:motion-safe:duration-(--panel-animation-duration) [[data-panel-animations=true]_&]:motion-safe:ease-out",
isElectron
? cn(
"drag-region flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center px-3 sm:px-5",
Expand All @@ -3293,6 +3323,7 @@ export default function ChatView(props: ChatViewProps) {
)
: "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center pl-(--workspace-gutter-start) pr-(--workspace-gutter-end)",
COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
hasTimelineBackground ? "isolate z-10" : "bg-background",
)}
>
{isElectron && rightPanelControlsAtRoot ? (
Expand Down Expand Up @@ -3363,7 +3394,11 @@ export default function ChatView(props: ChatViewProps) {
/>
<div
{...conversationArea}
className="relative flex min-h-0 flex-1 flex-col bg-background"
className={cn(
"relative isolate flex min-h-0 flex-1 flex-col",
!hasTimelineBackground && "bg-background",
timelineUnderHeader && "-mt-[var(--workspace-topbar-height)]",
)}
>
<WorkspaceMutationIdContext value={workspaceMutationId}>
<ThreadTimeline
Expand Down Expand Up @@ -3460,6 +3495,8 @@ export default function ChatView(props: ChatViewProps) {
hideEmptyPlaceholder={isDraftHeroState || threadDetailLoading}
emptyPlaceholderInset={composerReservedHeight}
topFadeEnabled={!hasTimelineTopBanner}
topFadeMaskEnabled={!hasTimelineBackground}
headerInset={timelineHeaderInset}
{...(threadHistoryControls === undefined
? {}
: { historyControls: threadHistoryControls })}
Expand All @@ -3483,11 +3520,12 @@ export default function ChatView(props: ChatViewProps) {
}}
inert={isRevertingCheckpoint}
data-chat-composer-overlay="true"
className={
className={cn(
isDraftHeroState
? "pointer-events-none absolute inset-0 z-20 flex items-center [@media(max-height:34rem)]:items-end"
: "pointer-events-none absolute inset-x-0 bottom-0 z-20 pt-1.5 sm:pt-2"
}
? "pointer-events-none absolute inset-0 flex items-center [@media(max-height:34rem)]:items-end"
: "pointer-events-none absolute inset-x-0 bottom-0 pt-1.5 sm:pt-2",
hasTimelineBackground ? "z-[1]" : "z-20",
)}
>
<div ref={draftHeroTransitionGroupRef} className="chat-composer-lane w-full">
<div
Expand All @@ -3497,7 +3535,10 @@ export default function ChatView(props: ChatViewProps) {
{isDraftHeroState ? (
<div className="absolute inset-x-0 bottom-full">
<div
className="pb-4 group-has-data-[composer-shoulder-tab]/composer-stack:pb-0"
className={cn(
"pb-4 group-has-data-[composer-shoulder-tab]/composer-stack:pb-0",
hasTimelineBackground && CHAT_BACKGROUND_TEXT_SHADOW_CLASSES,
)}
style={
forceExpandedMobileComposer
? { viewTransitionName: MOBILE_DRAFT_HEADLINE_VIEW_TRANSITION_NAME }
Expand All @@ -3515,11 +3556,7 @@ export default function ChatView(props: ChatViewProps) {
<div
ref={draftHeroComposerAnchorRef}
className="relative z-10"
style={
forceExpandedMobileComposer
? { viewTransitionName: MOBILE_COMPOSER_VIEW_TRANSITION_NAME }
: undefined
}
data-mobile-composer-transition={forceExpandedMobileComposer || undefined}
>
<ChildThreadApprovals threadRef={activeThreadRef} />
<div className="relative">
Expand Down
Loading
Loading