From c20747f10b543f47433f34cdb7613f02af676516 Mon Sep 17 00:00:00 2001 From: thetanav Date: Thu, 30 Apr 2026 11:57:07 +0530 Subject: [PATCH 1/3] collapsable sidebar --- apps/web/src/components/NoActiveThreadState.tsx | 7 +++++-- apps/web/src/components/ProjectScriptsControl.tsx | 2 +- apps/web/src/components/Sidebar.tsx | 2 +- apps/web/src/components/chat/ChatHeader.tsx | 9 ++++++--- apps/web/src/components/ui/sidebar.tsx | 13 +++++++------ apps/web/src/routes/settings.tsx | 6 ++++-- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/apps/web/src/components/NoActiveThreadState.tsx b/apps/web/src/components/NoActiveThreadState.tsx index cd1f76ed2c..4b9f58ab3d 100644 --- a/apps/web/src/components/NoActiveThreadState.tsx +++ b/apps/web/src/components/NoActiveThreadState.tsx @@ -1,9 +1,12 @@ import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "./ui/empty"; -import { SidebarInset, SidebarTrigger } from "./ui/sidebar"; +import { SidebarInset, SidebarTrigger, useSidebar } from "./ui/sidebar"; import { isElectron } from "../env"; import { cn } from "~/lib/utils"; export function NoActiveThreadState() { + const { isMobile, open, openMobile } = useSidebar(); + const sidebarOpen = isMobile ? openMobile : open; + return (
@@ -21,7 +24,7 @@ export function NoActiveThreadState() { ) : (
- + {!sidebarOpen ? : null} No active thread diff --git a/apps/web/src/components/ProjectScriptsControl.tsx b/apps/web/src/components/ProjectScriptsControl.tsx index 11b08cc2cf..a20c9e34db 100644 --- a/apps/web/src/components/ProjectScriptsControl.tsx +++ b/apps/web/src/components/ProjectScriptsControl.tsx @@ -271,7 +271,7 @@ export default function ProjectScriptsControl({ {primaryScript ? ( ); diff --git a/apps/web/src/routes/settings.tsx b/apps/web/src/routes/settings.tsx index 64b5e7bc68..5591be4a20 100644 --- a/apps/web/src/routes/settings.tsx +++ b/apps/web/src/routes/settings.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; import { useSettingsRestore } from "../components/settings/SettingsPanels"; import { Button } from "../components/ui/button"; -import { SidebarInset, SidebarTrigger } from "../components/ui/sidebar"; +import { SidebarInset, SidebarTrigger, useSidebar } from "../components/ui/sidebar"; import { isElectron } from "../env"; function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) { @@ -25,9 +25,11 @@ function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) { function SettingsContentLayout() { const location = useLocation(); + const { isMobile, open, openMobile } = useSidebar(); const [restoreSignal, setRestoreSignal] = useState(0); const showRestoreDefaults = location.pathname === "/settings/general"; const handleRestored = () => setRestoreSignal((value) => value + 1); + const sidebarOpen = isMobile ? openMobile : open; useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { @@ -50,7 +52,7 @@ function SettingsContentLayout() { {!isElectron && (
- + {!sidebarOpen ? : null} Settings {showRestoreDefaults ? (
From 0fdd23cfc404f2301c39d216f6aa846bdd5fac06 Mon Sep 17 00:00:00 2001 From: thetanav Date: Thu, 30 Apr 2026 12:18:32 +0530 Subject: [PATCH 2/3] better ui few tweaks --- .../src/components/chat/MessageCopyButton.tsx | 13 +- .../src/components/chat/MessagesTimeline.tsx | 268 ++++++++++++------ apps/web/src/timestampFormat.ts | 52 +++- 3 files changed, 235 insertions(+), 98 deletions(-) diff --git a/apps/web/src/components/chat/MessageCopyButton.tsx b/apps/web/src/components/chat/MessageCopyButton.tsx index ad5d56dd5a..756c7de7b6 100644 --- a/apps/web/src/components/chat/MessageCopyButton.tsx +++ b/apps/web/src/components/chat/MessageCopyButton.tsx @@ -22,7 +22,10 @@ const onCopy = (ref: React.RefObject) => { } }; -const onCopyError = (ref: React.RefObject, error: Error) => { +const onCopyError = ( + ref: React.RefObject, + error: Error, +) => { if (ref.current) { anchoredToastManager.add({ data: { @@ -41,7 +44,7 @@ const onCopyError = (ref: React.RefObject, error: Erro export const MessageCopyButton = memo(function MessageCopyButton({ text, size = "xs", - variant = "outline", + variant = "ghost", className, }: { text: string; @@ -72,7 +75,11 @@ export const MessageCopyButton = memo(function MessageCopyButton({ /> } > - {isCopied ? : } + {isCopied ? ( + + ) : ( + + )}

Copy to clipboard

diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index e4b683592e..e2ed3fdd33 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1,4 +1,8 @@ -import { type EnvironmentId, type MessageId, type TurnId } from "@t3tools/contracts"; +import { + type EnvironmentId, + type MessageId, + type TurnId, +} from "@t3tools/contracts"; import { createContext, memo, @@ -30,7 +34,10 @@ import { ZapIcon, } from "lucide-react"; import { Button } from "../ui/button"; -import { buildExpandedImagePreview, ExpandedImagePreview } from "./ExpandedImagePreview"; +import { + buildExpandedImagePreview, + ExpandedImagePreview, +} from "./ExpandedImagePreview"; import { ProposedPlanCard } from "./ProposedPlanCard"; import { ChangedFilesTree } from "./ChangedFilesTree"; import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel"; @@ -230,7 +237,10 @@ export const MessagesTimeline = memo(function MessagesTimeline({ // from TimelineRowCtx, which propagates through LegendList's memo. const renderItem = useCallback( ({ item }: { item: MessagesTimelineRow }) => ( -
+
), @@ -278,7 +288,10 @@ function keyExtractor(item: MessagesTimelineRow) { type TimelineEntry = ReturnType[number]; type TimelineMessage = Extract["message"]; -type TimelineWorkEntry = Extract["groupedEntries"][number]; +type TimelineWorkEntry = Extract< + MessagesTimelineRow, + { kind: "work" } +>["groupedEntries"][number]; type TimelineRow = MessagesTimelineRow; function TimelineRowContent({ row }: { row: TimelineRow }) { @@ -288,29 +301,39 @@ function TimelineRowContent({ row }: { row: TimelineRow }) {
- {row.kind === "work" && } + {row.kind === "work" && ( + + )} {row.kind === "message" && row.message.role === "user" && (() => { const userImages = row.message.attachments ?? []; - const displayedUserMessage = deriveDisplayedUserMessageState(row.message.text); + const displayedUserMessage = deriveDisplayedUserMessageState( + row.message.text, + ); const terminalContexts = displayedUserMessage.contexts; const canRevertAgentWork = typeof row.revertTurnCount === "number"; return ( -
+
{userImages.length > 0 && (
{userImages.map( - (image: NonNullable[number]) => ( + ( + image: NonNullable< + TimelineMessage["attachments"] + >[number], + ) => (
{ - const preview = buildExpandedImagePreview(userImages, image.id); + const preview = buildExpandedImagePreview( + userImages, + image.id, + ); if (!preview) return; ctx.onImageExpand(preview); }} @@ -349,28 +375,26 @@ function TimelineRowContent({ row }: { row: TimelineRow }) { terminalContexts={terminalContexts} /> )} -
-
- {displayedUserMessage.copyText && ( - - )} - {canRevertAgentWork && ( - - )} -
-

- {formatTimestamp(row.message.createdAt, ctx.timestampFormat)} -

-
+
+
+ {displayedUserMessage.copyText && ( + + )} + {canRevertAgentWork && ( + + )} +

+ {formatTimestamp(row.message.createdAt, ctx.timestampFormat)} +

); @@ -379,7 +403,9 @@ function TimelineRowContent({ row }: { row: TimelineRow }) { {row.kind === "message" && row.message.role === "assistant" && (() => { - const messageText = row.message.text || (row.message.streaming ? "" : "(empty response)"); + const messageText = + row.message.text || + (row.message.streaming ? "" : "(empty response)"); const assistantTurnStillInProgress = ctx.activeTurnInProgress && ctx.activeTurnId !== null && @@ -396,7 +422,9 @@ function TimelineRowContent({ row }: { row: TimelineRow }) {
- {ctx.completionSummary ? `Response • ${ctx.completionSummary}` : "Response"} + {ctx.completionSummary + ? `Response • ${ctx.completionSummary}` + : "Response"}
@@ -424,7 +452,10 @@ function TimelineRowContent({ row }: { row: TimelineRow }) { ) : ( formatMessageMeta( row.message.createdAt, - formatElapsed(row.durationStart, row.message.completedAt), + formatElapsed( + row.durationStart, + row.message.completedAt, + ), ctx.timestampFormat, ) )} @@ -434,8 +465,7 @@ function TimelineRowContent({ row }: { row: TimelineRow }) {
) : null} @@ -493,7 +523,9 @@ function WorkingTimer({ createdAt }: { createdAt: string }) { const id = setInterval(() => setNowMs(Date.now()), 1000); return () => clearInterval(id); }, [createdAt]); - return <>{formatWorkingTimer(createdAt, new Date(nowMs).toISOString()) ?? "0s"}; + return ( + <>{formatWorkingTimer(createdAt, new Date(nowMs).toISOString()) ?? "0s"} + ); } /** Live timestamp + elapsed duration for a streaming assistant message. */ @@ -527,7 +559,10 @@ function LiveMessageMeta({ const WorkGroupSection = memo(function WorkGroupSection({ groupedEntries, }: { - groupedEntries: Extract["groupedEntries"]; + groupedEntries: Extract< + MessagesTimelineRow, + { kind: "work" } + >["groupedEntries"]; }) { const { workspaceRoot } = use(TimelineRowCtx); const [isExpanded, setIsExpanded] = useState(false); @@ -537,7 +572,9 @@ const WorkGroupSection = memo(function WorkGroupSection({ ? groupedEntries.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES) : groupedEntries; const hiddenCount = groupedEntries.length - visibleEntries.length; - const onlyToolEntries = groupedEntries.every((entry) => entry.tone === "tool"); + const onlyToolEntries = groupedEntries.every( + (entry) => entry.tone === "tool", + ); const showHeader = hasOverflow || !onlyToolEntries; const groupLabel = onlyToolEntries ? "Tool calls" : "Work log"; @@ -574,31 +611,33 @@ const WorkGroupSection = memo(function WorkGroupSection({ /** Subscribes directly to the UI state store for expand/collapse state, * so toggling re-renders only this component — not the entire list. */ -const AssistantChangedFilesSection = memo(function AssistantChangedFilesSection({ - turnSummary, - routeThreadKey, - resolvedTheme, - onOpenTurnDiff, -}: { - turnSummary: TurnDiffSummary | undefined; - routeThreadKey: string; - resolvedTheme: "light" | "dark"; - onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void; -}) { - if (!turnSummary) return null; - const checkpointFiles = turnSummary.files; - if (checkpointFiles.length === 0) return null; +const AssistantChangedFilesSection = memo( + function AssistantChangedFilesSection({ + turnSummary, + routeThreadKey, + resolvedTheme, + onOpenTurnDiff, + }: { + turnSummary: TurnDiffSummary | undefined; + routeThreadKey: string; + resolvedTheme: "light" | "dark"; + onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void; + }) { + if (!turnSummary) return null; + const checkpointFiles = turnSummary.files; + if (checkpointFiles.length === 0) return null; - return ( - - ); -}); + return ( + + ); + }, +); /** Inner component that only mounts when there are actual changed files, * so the store subscription is unconditional (no hooks after early return). */ @@ -616,9 +655,14 @@ function AssistantChangedFilesSectionInner({ onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void; }) { const allDirectoriesExpanded = useUiStateStore( - (store) => store.threadChangedFilesExpandedById[routeThreadKey]?.[turnSummary.turnId] ?? true, + (store) => + store.threadChangedFilesExpandedById[routeThreadKey]?.[ + turnSummary.turnId + ] ?? true, + ); + const setExpanded = useUiStateStore( + (store) => store.setThreadChangedFilesExpanded, ); - const setExpanded = useUiStateStore((store) => store.setThreadChangedFilesExpanded); const summaryStat = summarizeTurnDiffStats(checkpointFiles); const changedFileCountLabel = String(checkpointFiles.length); @@ -630,7 +674,10 @@ function AssistantChangedFilesSectionInner({ {hasNonZeroStat(summaryStat) && ( <> - + )}

@@ -640,7 +687,13 @@ function AssistantChangedFilesSectionInner({ size="xs" variant="outline" data-scroll-anchor-ignore - onClick={() => setExpanded(routeThreadKey, turnSummary.turnId, !allDirectoriesExpanded)} + onClick={() => + setExpanded( + routeThreadKey, + turnSummary.turnId, + !allDirectoriesExpanded, + ) + } > {allDirectoriesExpanded ? "Collapse all" : "Expand all"} @@ -648,7 +701,9 @@ function AssistantChangedFilesSectionInner({ type="button" size="xs" variant="outline" - onClick={() => onOpenTurnDiff(turnSummary.turnId, checkpointFiles[0]?.path)} + onClick={() => + onOpenTurnDiff(turnSummary.turnId, checkpointFiles[0]?.path) + } > View diff @@ -671,13 +726,20 @@ function AssistantChangedFilesSectionInner({ // --------------------------------------------------------------------------- const UserMessageTerminalContextInlineLabel = memo( - function UserMessageTerminalContextInlineLabel(props: { context: ParsedTerminalContextEntry }) { + function UserMessageTerminalContextInlineLabel(props: { + context: ParsedTerminalContextEntry; + }) { const tooltipText = props.context.body.length > 0 ? `${props.context.header}\n${props.context.body}` : props.context.header; - return ; + return ( + + ); }, ); @@ -705,7 +767,9 @@ const UserMessageBody = memo(function UserMessageBody(props: { } if (matchIndex > cursor) { inlineNodes.push( - + {props.text.slice(cursor, matchIndex)} , ); @@ -744,14 +808,21 @@ const UserMessageBody = memo(function UserMessageBody(props: { />, ); inlineNodes.push( -