Refine Foundry UI layout and styling (#235)

* feat: modernize chat UI and rename handoff to task

- Remove agent message bubbles, keep user bubbles (right-aligned)
- Rename "Handoffs" to "Tasks" with ListChecks icon in sidebar
- Move model picker inside composer, add renderFooter to ChatComposer SDK
- Make project sections collapsible with hover-only chevrons
- Remove divider between chat and composer
- Update model picker chevron to flip on open/close
- Replace all user-visible "handoff" strings with "task" across frontend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: real org mock data, model picker styling, project icons, task minutes indicator

- Replace fake acme/* mock data with real rivet-dev GitHub org repos and PRs
- Fix model picker popover: dark gray surface with backdrop blur instead of pure black
- Add colored letter icons to project section headers (swap to chevron on hover)
- Add "847 min used" indicator in transcript header
- Rename browser tab title from OpenHandoff to Foundry
- Reduce transcript header title font weight to 500

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: refine Foundry UI — single-line task cards, dark user bubbles, curved panel corners, send icon

- Collapse task sidebar cards to single-line layout (title, number, diffs, timestamp)
- Dark-themed user message bubbles matching site theme
- Curved top-left corner on center chat panel with border line
- Subtle focus border on composer input
- Replace ArrowUpFromLine with SendHorizonal icon
- Tab strip gaps, padding, and divider alignment fixes
- Plus button with visible background
- Right sidebar header color matching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nicholas Kissel 2026-03-11 01:50:36 -07:00 committed by GitHub
parent 20082512a3
commit e792a720a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 201 additions and 123 deletions

View file

@ -4,15 +4,15 @@ export const appTheme: Theme = createDarkTheme({
colors: { colors: {
primary: "#e4e4e7", // zinc-200 primary: "#e4e4e7", // zinc-200
accent: "#ff4f00", // orange accent (inspector) accent: "#ff4f00", // orange accent (inspector)
backgroundPrimary: "#000000", // pure black (inspector --bg) backgroundPrimary: "#09090b", // darkest — chat center panel
backgroundSecondary: "#0a0a0b", // near-black panels (inspector --bg-panel) backgroundSecondary: "#0f0f11", // slightly lighter — sidebars
backgroundTertiary: "#0a0a0b", // same as panel (border provides separation) backgroundTertiary: "#0c0c0e", // center + right panel headers
backgroundInversePrimary: "#fafafa", backgroundInversePrimary: "#fafafa",
contentPrimary: "#ffffff", // white (inspector --text) contentPrimary: "#ffffff", // white (inspector --text)
contentSecondary: "#a1a1aa", // zinc-400 (inspector --muted) contentSecondary: "#a1a1aa", // zinc-400 (inspector --muted)
contentTertiary: "#71717a", // zinc-500 contentTertiary: "#71717a", // zinc-500
contentInversePrimary: "#000000", contentInversePrimary: "#000000",
borderOpaque: "rgba(255, 255, 255, 0.18)", // inspector --border borderOpaque: "rgba(255, 255, 255, 0.10)", // inspector --border
borderTransparent: "rgba(255, 255, 255, 0.14)", // inspector --border-2 borderTransparent: "rgba(255, 255, 255, 0.07)", // inspector --border-2
}, },
}); });

View file

@ -455,6 +455,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
} }
}} }}
/> />
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", backgroundColor: "#09090b", borderTopLeftRadius: "12px", borderLeft: "1px solid rgba(255, 255, 255, 0.10)", borderTop: "1px solid rgba(255, 255, 255, 0.10)", overflow: "hidden" }}>
<TabStrip <TabStrip
handoff={handoff} handoff={handoff}
activeTabId={activeTabId} activeTabId={activeTabId}
@ -510,8 +511,8 @@ const TranscriptPanel = memo(function TranscriptPanel({
border: 0, border: 0,
borderRadius: "999px", borderRadius: "999px",
padding: "10px 18px", padding: "10px 18px",
background: "#ff4f00", background: "rgba(255, 255, 255, 0.12)",
color: "#fff", color: "#e4e4e7",
cursor: "pointer", cursor: "pointer",
fontWeight: 600, fontWeight: 600,
}} }}
@ -554,6 +555,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
onSetDefaultModel={setDefaultModel} onSetDefaultModel={setDefaultModel}
/> />
) : null} ) : null}
</div>
</SPanel> </SPanel>
); );
}); });
@ -723,7 +725,23 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
handoffWorkbenchClient.getSnapshot.bind(handoffWorkbenchClient), handoffWorkbenchClient.getSnapshot.bind(handoffWorkbenchClient),
); );
const handoffs = viewModel.handoffs ?? []; const handoffs = viewModel.handoffs ?? [];
const projects = viewModel.projects ?? []; const rawProjects = viewModel.projects ?? [];
const [projectOrder, setProjectOrder] = useState<string[] | null>(null);
const projects = useMemo(() => {
if (!projectOrder) return rawProjects;
const byId = new Map(rawProjects.map((p) => [p.id, p]));
const ordered = projectOrder.map((id) => byId.get(id)).filter(Boolean) as typeof rawProjects;
for (const p of rawProjects) {
if (!projectOrder.includes(p.id)) ordered.push(p);
}
return ordered;
}, [rawProjects, projectOrder]);
const reorderProjects = useCallback((fromIndex: number, toIndex: number) => {
const ids = projects.map((p) => p.id);
const [moved] = ids.splice(fromIndex, 1);
ids.splice(toIndex, 0, moved!);
setProjectOrder(ids);
}, [projects]);
const [activeTabIdByHandoff, setActiveTabIdByHandoff] = useState<Record<string, string | null>>({}); const [activeTabIdByHandoff, setActiveTabIdByHandoff] = useState<Record<string, string | null>>({});
const [lastAgentTabIdByHandoff, setLastAgentTabIdByHandoff] = useState<Record<string, string | null>>({}); const [lastAgentTabIdByHandoff, setLastAgentTabIdByHandoff] = useState<Record<string, string | null>>({});
const [openDiffsByHandoff, setOpenDiffsByHandoff] = useState<Record<string, string[]>>({}); const [openDiffsByHandoff, setOpenDiffsByHandoff] = useState<Record<string, string[]>>({});
@ -1027,34 +1045,35 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
> >
<div <div
style={{ style={{
width: "min(520px, 100%)", width: "min(440px, 100%)",
border: "1px solid rgba(255, 255, 255, 0.14)", border: "1px solid rgba(255, 255, 255, 0.10)",
borderRadius: "18px", borderRadius: "12px",
background: "#111113", background: "rgba(24, 24, 27, 0.98)",
boxShadow: "0 32px 80px rgba(0, 0, 0, 0.45)", backdropFilter: "blur(16px)",
padding: "24px", boxShadow: "0 24px 64px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04)",
padding: "28px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "16px", gap: "20px",
}} }}
> >
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}> <div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
<div style={{ fontSize: "12px", letterSpacing: "0.08em", textTransform: "uppercase", color: "rgba(255, 255, 255, 0.5)" }}>Onboarding</div> <div style={{ fontSize: "11px", letterSpacing: "0.06em", textTransform: "uppercase", fontWeight: 600, color: "rgba(255, 255, 255, 0.4)" }}>Welcome to Foundry</div>
<h2 style={{ margin: 0, fontSize: "24px", lineHeight: 1.1 }}>Give us support for sandbox agent</h2> <h2 style={{ margin: 0, fontSize: "18px", fontWeight: 500, lineHeight: 1.3 }}>Support Sandbox Agent</h2>
<p style={{ margin: 0, color: "rgba(255, 255, 255, 0.72)", lineHeight: 1.5 }}> <p style={{ margin: 0, color: "rgba(255, 255, 255, 0.55)", fontSize: "13px", lineHeight: 1.6 }}>
Before you keep going, give us support for sandbox agent and star the repo right here in the app. Star the repo to help us grow and stay up to date with new releases.
</p> </p>
</div> </div>
{starRepoError ? ( {starRepoError ? (
<div <div
style={{ style={{
borderRadius: "12px", borderRadius: "8px",
border: "1px solid rgba(255, 110, 110, 0.32)", border: "1px solid rgba(255, 110, 110, 0.24)",
background: "rgba(255, 110, 110, 0.08)", background: "rgba(255, 110, 110, 0.06)",
padding: "12px 14px", padding: "10px 12px",
color: "#ffb4b4", color: "#ff9b9b",
fontSize: "13px", fontSize: "12px",
}} }}
data-testid="onboarding-star-repo-error" data-testid="onboarding-star-repo-error"
> >
@ -1062,18 +1081,20 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
</div> </div>
) : null} ) : null}
<div style={{ display: "flex", justifyContent: "flex-end", gap: "10px" }}> <div style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}>
<button <button
type="button" type="button"
onClick={dismissStarRepoPrompt} onClick={dismissStarRepoPrompt}
style={{ style={{
border: "1px solid rgba(255, 255, 255, 0.14)", border: "1px solid rgba(255, 255, 255, 0.10)",
borderRadius: "999px", borderRadius: "6px",
padding: "10px 16px", padding: "8px 14px",
background: "transparent", background: "rgba(255, 255, 255, 0.05)",
color: "#e4e4e7", color: "rgba(255, 255, 255, 0.7)",
cursor: "pointer", cursor: "pointer",
fontWeight: 600, fontSize: "12px",
fontWeight: 500,
transition: "all 160ms ease",
}} }}
> >
Maybe later Maybe later
@ -1084,16 +1105,18 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
disabled={starRepoPending} disabled={starRepoPending}
style={{ style={{
border: 0, border: 0,
borderRadius: "999px", borderRadius: "6px",
padding: "10px 16px", padding: "8px 14px",
background: starRepoPending ? "#7f5539" : "#ff4f00", background: starRepoPending ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.12)",
color: "#fff", color: "#e4e4e7",
cursor: starRepoPending ? "progress" : "pointer", cursor: starRepoPending ? "progress" : "pointer",
fontWeight: 700, fontSize: "12px",
fontWeight: 600,
transition: "all 160ms ease",
}} }}
data-testid="onboarding-star-repo-submit" data-testid="onboarding-star-repo-submit"
> >
{starRepoPending ? "Starring..." : "Star the sandbox agent repo"} {starRepoPending ? "Starring..." : "Star the repo"}
</button> </button>
</div> </div>
</div> </div>
@ -1112,8 +1135,9 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
onMarkUnread={markHandoffUnread} onMarkUnread={markHandoffUnread}
onRenameHandoff={renameHandoff} onRenameHandoff={renameHandoff}
onRenameBranch={renameBranch} onRenameBranch={renameBranch}
onReorderProjects={reorderProjects}
/> />
<SPanel> <SPanel $style={{ backgroundColor: "#09090b" }}>
<ScrollBody> <ScrollBody>
<div <div
style={{ style={{
@ -1148,8 +1172,8 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
border: 0, border: 0,
borderRadius: "999px", borderRadius: "999px",
padding: "10px 18px", padding: "10px 18px",
background: viewModel.repos.length > 0 ? "#ff4f00" : "#444", background: viewModel.repos.length > 0 ? "rgba(255, 255, 255, 0.12)" : "#444",
color: "#fff", color: "#e4e4e7",
cursor: viewModel.repos.length > 0 ? "pointer" : "not-allowed", cursor: viewModel.repos.length > 0 ? "pointer" : "not-allowed",
fontWeight: 600, fontWeight: 600,
}} }}
@ -1178,6 +1202,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
onMarkUnread={markHandoffUnread} onMarkUnread={markHandoffUnread}
onRenameHandoff={renameHandoff} onRenameHandoff={renameHandoff}
onRenameBranch={renameBranch} onRenameBranch={renameBranch}
onReorderProjects={reorderProjects}
/> />
<TranscriptPanel <TranscriptPanel
handoff={activeHandoff} handoff={activeHandoff}

View file

@ -49,8 +49,8 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
borderTopRightRadius: "16px", borderTopRightRadius: "16px",
...(isUser ...(isUser
? { ? {
backgroundColor: "#ffffff", backgroundColor: "rgba(255, 255, 255, 0.10)",
color: "#000000", color: "#e4e4e7",
borderBottomLeftRadius: "16px", borderBottomLeftRadius: "16px",
borderBottomRightRadius: "4px", borderBottomRightRadius: "4px",
} }
@ -107,7 +107,7 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
})} })}
> >
<Copy size={11} /> <Copy size={11} />
{isCopied ? "Copied" : "Copy"} {isCopied ? "Copied" : null}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,7 +1,7 @@
import { memo, type Ref } from "react"; import { memo, type Ref } from "react";
import { useStyletron } from "baseui"; import { useStyletron } from "baseui";
import { ChatComposer, type ChatComposerClassNames } from "@sandbox-agent/react"; import { ChatComposer, type ChatComposerClassNames } from "@sandbox-agent/react";
import { ArrowUpFromLine, FileCode, Square, X } from "lucide-react"; import { FileCode, SendHorizonal, Square, X } from "lucide-react";
import { ModelPicker } from "./model-picker"; import { ModelPicker } from "./model-picker";
import { PROMPT_TEXTAREA_MAX_HEIGHT, PROMPT_TEXTAREA_MIN_HEIGHT } from "./ui"; import { PROMPT_TEXTAREA_MAX_HEIGHT, PROMPT_TEXTAREA_MIN_HEIGHT } from "./ui";
@ -45,7 +45,7 @@ export const PromptComposer = memo(function PromptComposer({
borderRadius: "16px", borderRadius: "16px",
minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 36}px`, minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 36}px`,
transition: "border-color 200ms ease", transition: "border-color 200ms ease",
":focus-within": { borderColor: "rgba(255, 255, 255, 0.3)" }, ":focus-within": { borderColor: "rgba(255, 255, 255, 0.15)" },
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
}), }),
@ -82,9 +82,9 @@ export const PromptComposer = memo(function PromptComposer({
justifyContent: "center", justifyContent: "center",
color: theme.colors.contentPrimary, color: theme.colors.contentPrimary,
transition: "background 200ms ease", transition: "background 200ms ease",
backgroundColor: isRunning ? "rgba(255, 255, 255, 0.06)" : "#ff4f00", backgroundColor: isRunning ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.12)",
":hover": { ":hover": {
backgroundColor: isRunning ? "rgba(255, 255, 255, 0.12)" : "#ff6a00", backgroundColor: isRunning ? "rgba(255, 255, 255, 0.12)" : "rgba(255, 255, 255, 0.20)",
}, },
":disabled": { ":disabled": {
cursor: "not-allowed", cursor: "not-allowed",
@ -157,7 +157,7 @@ export const PromptComposer = memo(function PromptComposer({
allowEmptySubmit={isRunning} allowEmptySubmit={isRunning}
submitLabel={isRunning ? "Stop" : "Send"} submitLabel={isRunning ? "Stop" : "Send"}
classNames={composerClassNames} classNames={composerClassNames}
renderSubmitContent={() => (isRunning ? <Square size={16} /> : <ArrowUpFromLine size={16} />)} renderSubmitContent={() => (isRunning ? <Square size={16} /> : <SendHorizonal size={16} />)}
renderFooter={() => ( renderFooter={() => (
<div className={css({ padding: "0 10px 8px" })}> <div className={css({ padding: "0 10px 8px" })}>
<ModelPicker <ModelPicker

View file

@ -135,8 +135,8 @@ export const RightSidebar = memo(function RightSidebar({
); );
return ( return (
<SPanel> <SPanel $style={{ backgroundColor: "#09090b" }}>
<PanelHeaderBar> <PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none" }}>
<div className={css({ flex: 1 })} /> <div className={css({ flex: 1 })} />
{!isTerminal ? ( {!isTerminal ? (
<div className={css({ display: "flex", alignItems: "center", gap: "4px" })}> <div className={css({ display: "flex", alignItems: "center", gap: "4px" })}>
@ -208,12 +208,14 @@ export const RightSidebar = memo(function RightSidebar({
) : null} ) : null}
</PanelHeaderBar> </PanelHeaderBar>
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", borderTop: `1px solid rgba(255, 255, 255, 0.10)` }}>
<div <div
className={css({ className={css({
display: "flex", display: "flex",
alignItems: "stretch", alignItems: "stretch",
gap: "4px",
borderBottom: `1px solid ${theme.colors.borderOpaque}`, borderBottom: `1px solid ${theme.colors.borderOpaque}`,
backgroundColor: theme.colors.backgroundSecondary, backgroundColor: "#09090b",
height: "41px", height: "41px",
minHeight: "41px", minHeight: "41px",
flexShrink: 0, flexShrink: 0,
@ -226,19 +228,21 @@ export const RightSidebar = memo(function RightSidebar({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "6px", gap: "6px",
height: "100%", padding: "4px 12px",
padding: "0 16px", marginTop: "6px",
marginBottom: "6px",
marginLeft: "6px",
borderRadius: "8px",
cursor: "pointer", cursor: "pointer",
fontSize: "12px", fontSize: "12px",
fontWeight: 600, fontWeight: 500,
whiteSpace: "nowrap", whiteSpace: "nowrap",
color: rightTab === "changes" ? theme.colors.contentPrimary : theme.colors.contentSecondary, color: rightTab === "changes" ? theme.colors.contentPrimary : theme.colors.contentSecondary,
borderBottom: `2px solid ${rightTab === "changes" ? "#ff4f00" : "transparent"}`, backgroundColor: rightTab === "changes" ? "rgba(255, 255, 255, 0.06)" : "transparent",
marginBottom: "-1px", transitionProperty: "color, background-color",
transitionProperty: "color, border-color",
transitionDuration: "200ms", transitionDuration: "200ms",
transitionTimingFunction: "ease", transitionTimingFunction: "ease",
":hover": { color: "#e4e4e7" }, ":hover": { color: "#e4e4e7", backgroundColor: rightTab === "changes" ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
})} })}
> >
Changes Changes
@ -268,19 +272,20 @@ export const RightSidebar = memo(function RightSidebar({
all: "unset", all: "unset",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
height: "100%", padding: "4px 12px",
padding: "0 16px", marginTop: "6px",
marginBottom: "6px",
borderRadius: "8px",
cursor: "pointer", cursor: "pointer",
fontSize: "12px", fontSize: "12px",
fontWeight: 600, fontWeight: 500,
whiteSpace: "nowrap", whiteSpace: "nowrap",
color: rightTab === "files" ? theme.colors.contentPrimary : theme.colors.contentSecondary, color: rightTab === "files" ? theme.colors.contentPrimary : theme.colors.contentSecondary,
borderBottom: `2px solid ${rightTab === "files" ? "#ff4f00" : "transparent"}`, backgroundColor: rightTab === "files" ? "rgba(255, 255, 255, 0.06)" : "transparent",
marginBottom: "-1px", transitionProperty: "color, background-color",
transitionProperty: "color, border-color",
transitionDuration: "200ms", transitionDuration: "200ms",
transitionTimingFunction: "ease", transitionTimingFunction: "ease",
":hover": { color: "#e4e4e7" }, ":hover": { color: "#e4e4e7", backgroundColor: rightTab === "files" ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
})} })}
> >
All Files All Files
@ -360,6 +365,7 @@ export const RightSidebar = memo(function RightSidebar({
</div> </div>
)} )}
</ScrollBody> </ScrollBody>
</div>
{contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null} {contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null}
</SPanel> </SPanel>
); );

View file

@ -1,4 +1,4 @@
import { memo, useState } from "react"; import { memo, useRef, useState } from "react";
import { useStyletron } from "baseui"; import { useStyletron } from "baseui";
import { LabelSmall, LabelXSmall } from "baseui/typography"; import { LabelSmall, LabelXSmall } from "baseui/typography";
import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, Plus } from "lucide-react"; import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, Plus } from "lucide-react";
@ -30,6 +30,7 @@ export const Sidebar = memo(function Sidebar({
onMarkUnread, onMarkUnread,
onRenameHandoff, onRenameHandoff,
onRenameBranch, onRenameBranch,
onReorderProjects,
}: { }: {
projects: ProjectSection[]; projects: ProjectSection[];
activeId: string; activeId: string;
@ -38,10 +39,13 @@ export const Sidebar = memo(function Sidebar({
onMarkUnread: (id: string) => void; onMarkUnread: (id: string) => void;
onRenameHandoff: (id: string) => void; onRenameHandoff: (id: string) => void;
onRenameBranch: (id: string) => void; onRenameBranch: (id: string) => void;
onReorderProjects: (fromIndex: number, toIndex: number) => void;
}) { }) {
const [css, theme] = useStyletron(); const [css, theme] = useStyletron();
const contextMenu = useContextMenu(); const contextMenu = useContextMenu();
const [collapsedProjects, setCollapsedProjects] = useState<Record<string, boolean>>({}); const [collapsedProjects, setCollapsedProjects] = useState<Record<string, boolean>>({});
const dragIndexRef = useRef<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
return ( return (
<SPanel> <SPanel>
@ -53,10 +57,10 @@ export const Sidebar = memo(function Sidebar({
display: none !important; display: none !important;
} }
`}</style> `}</style>
<PanelHeaderBar> <PanelHeaderBar $style={{ backgroundColor: "transparent", borderBottom: "none" }}>
<LabelSmall <LabelSmall
color={theme.colors.contentPrimary} color={theme.colors.contentPrimary}
$style={{ fontWeight: 600, flex: 1, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px" }} $style={{ fontWeight: 500, flex: 1, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px" }}
> >
<ListChecks size={14} /> <ListChecks size={14} />
Tasks Tasks
@ -65,17 +69,17 @@ export const Sidebar = memo(function Sidebar({
onClick={onCreate} onClick={onCreate}
className={css({ className={css({
all: "unset", all: "unset",
width: "24px", width: "26px",
height: "24px", height: "26px",
borderRadius: "4px", borderRadius: "8px",
backgroundColor: "#ff4f00", backgroundColor: "rgba(255, 255, 255, 0.12)",
color: "#ffffff", color: "#e4e4e7",
cursor: "pointer", cursor: "pointer",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
transition: "background 200ms ease", transition: "background 200ms ease",
":hover": { backgroundColor: "#ff6a00" }, ":hover": { backgroundColor: "rgba(255, 255, 255, 0.20)" },
})} })}
> >
<Plus size={14} /> <Plus size={14} />
@ -83,11 +87,48 @@ export const Sidebar = memo(function Sidebar({
</PanelHeaderBar> </PanelHeaderBar>
<ScrollBody> <ScrollBody>
<div className={css({ padding: "8px", display: "flex", flexDirection: "column", gap: "4px" })}> <div className={css({ padding: "8px", display: "flex", flexDirection: "column", gap: "4px" })}>
{projects.map((project) => { {projects.map((project, projectIndex) => {
const isCollapsed = collapsedProjects[project.id] === true; const isCollapsed = collapsedProjects[project.id] === true;
const isDragOver = dragOverIndex === projectIndex && dragIndexRef.current !== projectIndex;
return ( return (
<div key={project.id} className={css({ display: "flex", flexDirection: "column", gap: "4px" })}> <div
key={project.id}
draggable
onDragStart={(event) => {
dragIndexRef.current = projectIndex;
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", String(projectIndex));
}}
onDragOver={(event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
setDragOverIndex(projectIndex);
}}
onDragLeave={() => {
setDragOverIndex((current) => (current === projectIndex ? null : current));
}}
onDrop={(event) => {
event.preventDefault();
const fromIndex = dragIndexRef.current;
if (fromIndex != null && fromIndex !== projectIndex) {
onReorderProjects(fromIndex, projectIndex);
}
dragIndexRef.current = null;
setDragOverIndex(null);
}}
onDragEnd={() => {
dragIndexRef.current = null;
setDragOverIndex(null);
}}
className={css({
display: "flex",
flexDirection: "column",
gap: "4px",
borderTop: isDragOver ? "2px solid #ff4f00" : "2px solid transparent",
transition: "border-color 150ms ease",
})}
>
<div <div
onClick={() => onClick={() =>
setCollapsedProjects((current) => ({ setCollapsedProjects((current) => ({
@ -102,7 +143,7 @@ export const Sidebar = memo(function Sidebar({
justifyContent: "space-between", justifyContent: "space-between",
padding: "10px 8px 4px", padding: "10px 8px 4px",
gap: "8px", gap: "8px",
cursor: "pointer", cursor: "grab",
userSelect: "none", userSelect: "none",
":hover": { opacity: 0.8 }, ":hover": { opacity: 0.8 },
})} })}
@ -150,9 +191,11 @@ export const Sidebar = memo(function Sidebar({
{project.label} {project.label}
</LabelSmall> </LabelSmall>
</div> </div>
{isCollapsed ? (
<LabelXSmall color={theme.colors.contentTertiary}> <LabelXSmall color={theme.colors.contentTertiary}>
{formatRelativeAge(project.updatedAtMs)} {formatRelativeAge(project.updatedAtMs)}
</LabelXSmall> </LabelXSmall>
) : null}
</div> </div>
{!isCollapsed && project.handoffs.map((handoff) => { {!isCollapsed && project.handoffs.map((handoff) => {
@ -177,7 +220,7 @@ export const Sidebar = memo(function Sidebar({
]) ])
} }
className={css({ className={css({
padding: "12px", padding: "8px 12px",
borderRadius: "8px", borderRadius: "8px",
border: "1px solid transparent", border: "1px solid transparent",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent", backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
@ -204,35 +247,17 @@ export const Sidebar = memo(function Sidebar({
</div> </div>
<LabelSmall <LabelSmall
$style={{ $style={{
fontWeight: 600, fontWeight: hasUnread ? 600 : 400,
flex: 1,
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap", whiteSpace: "nowrap",
minWidth: 0,
flexShrink: 1,
}} }}
color={isDim ? theme.colors.contentSecondary : theme.colors.contentPrimary} color={hasUnread ? "#ffffff" : theme.colors.contentSecondary}
> >
{handoff.title} {handoff.title}
</LabelSmall> </LabelSmall>
{hasDiffs ? (
<div className={css({ display: "flex", gap: "4px", flexShrink: 0 })}>
<span className={css({ fontSize: "11px", color: "#7ee787" })}>+{totalAdded}</span>
<span className={css({ fontSize: "11px", color: "#ffa198" })}>-{totalRemoved}</span>
</div>
) : null}
</div>
<div className={css({ display: "flex", alignItems: "center", marginTop: "4px", gap: "6px" })}>
<LabelXSmall
color={theme.colors.contentTertiary}
$style={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flexShrink: 1,
}}
>
{handoff.repoName}
</LabelXSmall>
{handoff.pullRequest != null ? ( {handoff.pullRequest != null ? (
<span className={css({ display: "inline-flex", alignItems: "center", gap: "4px", flexShrink: 0 })}> <span className={css({ display: "inline-flex", alignItems: "center", gap: "4px", flexShrink: 0 })}>
<LabelXSmall color={theme.colors.contentSecondary} $style={{ fontWeight: 600 }}> <LabelXSmall color={theme.colors.contentSecondary} $style={{ fontWeight: 600 }}>
@ -243,7 +268,13 @@ export const Sidebar = memo(function Sidebar({
) : ( ) : (
<GitPullRequestDraft size={11} color={theme.colors.contentTertiary} /> <GitPullRequestDraft size={11} color={theme.colors.contentTertiary} />
)} )}
<LabelXSmall color={theme.colors.contentTertiary} $style={{ marginLeft: "auto", flexShrink: 0 }}> {hasDiffs ? (
<div className={css({ display: "flex", gap: "4px", flexShrink: 0, marginLeft: "auto" })}>
<span className={css({ fontSize: "11px", color: "#7ee787" })}>+{totalAdded}</span>
<span className={css({ fontSize: "11px", color: "#ffa198" })}>-{totalRemoved}</span>
</div>
) : null}
<LabelXSmall color={theme.colors.contentTertiary} $style={{ flexShrink: 0, marginLeft: hasDiffs ? undefined : "auto" }}>
{formatRelativeAge(handoff.updatedAtMs)} {formatRelativeAge(handoff.updatedAtMs)}
</LabelXSmall> </LabelXSmall>
</div> </div>

View file

@ -42,12 +42,18 @@ export const TabStrip = memo(function TabStrip({
return ( return (
<> <>
<style>{`
[data-tab]:hover [data-tab-close] { opacity: 0.5 !important; }
[data-tab]:hover [data-tab-close]:hover { opacity: 1 !important; }
`}</style>
<div <div
className={css({ className={css({
display: "flex", display: "flex",
alignItems: "stretch", alignItems: "stretch",
borderBottom: `1px solid ${theme.colors.borderOpaque}`, borderBottom: `1px solid ${theme.colors.borderOpaque}`,
backgroundColor: theme.colors.backgroundSecondary, gap: "4px",
backgroundColor: "#09090b",
paddingLeft: "6px",
height: "41px", height: "41px",
minHeight: "41px", minHeight: "41px",
overflowX: "auto", overflowX: "auto",
@ -79,16 +85,20 @@ export const TabStrip = memo(function TabStrip({
...(handoff.tabs.length > 1 ? [{ label: "Close tab", onClick: () => onCloseTab(tab.id) }] : []), ...(handoff.tabs.length > 1 ? [{ label: "Close tab", onClick: () => onCloseTab(tab.id) }] : []),
]) ])
} }
data-tab
className={css({ className={css({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "6px", gap: "6px",
padding: "0 14px", padding: "4px 12px",
borderBottom: isActive ? "2px solid #ff4f00" : "2px solid transparent", marginTop: "6px",
marginBottom: "6px",
borderRadius: "8px",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
cursor: "pointer", cursor: "pointer",
transition: "color 200ms ease, border-color 200ms ease", transition: "color 200ms ease, background-color 200ms ease",
flexShrink: 0, flexShrink: 0,
":hover": { color: "#e4e4e7" }, ":hover": { color: "#e4e4e7", backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
})} })}
> >
<div <div
@ -130,7 +140,7 @@ export const TabStrip = memo(function TabStrip({
})} })}
/> />
) : ( ) : (
<LabelXSmall color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary} $style={{ fontWeight: 600 }}> <LabelXSmall color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary} $style={{ fontWeight: 500 }}>
{tab.sessionName} {tab.sessionName}
</LabelXSmall> </LabelXSmall>
)} )}
@ -138,7 +148,8 @@ export const TabStrip = memo(function TabStrip({
<X <X
size={11} size={11}
color={theme.colors.contentTertiary} color={theme.colors.contentTertiary}
className={css({ cursor: "pointer", opacity: 0.5, ":hover": { opacity: 1 } })} data-tab-close
className={css({ cursor: "pointer", opacity: 0 })}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
onCloseTab(tab.id); onCloseTab(tab.id);
@ -161,29 +172,34 @@ export const TabStrip = memo(function TabStrip({
onCloseDiffTab(path); onCloseDiffTab(path);
} }
}} }}
data-tab
className={css({ className={css({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "6px", gap: "6px",
padding: "0 14px", padding: "4px 12px",
borderBottom: "2px solid transparent", marginTop: "6px",
marginBottom: "6px",
borderRadius: "8px",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
cursor: "pointer", cursor: "pointer",
transition: "color 200ms ease, border-color 200ms ease", transition: "color 200ms ease, background-color 200ms ease",
flexShrink: 0, flexShrink: 0,
":hover": { color: "#e4e4e7" }, ":hover": { color: "#e4e4e7", backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
})} })}
> >
<FileCode size={12} color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary} /> <FileCode size={12} color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary} />
<LabelXSmall <LabelXSmall
color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary} color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary}
$style={{ fontWeight: 600, fontFamily: '"IBM Plex Mono", monospace' }} $style={{ fontWeight: 500, fontFamily: '"IBM Plex Mono", monospace' }}
> >
{fileName(path)} {fileName(path)}
</LabelXSmall> </LabelXSmall>
<X <X
size={11} size={11}
color={theme.colors.contentTertiary} color={theme.colors.contentTertiary}
className={css({ cursor: "pointer", opacity: 0.5, ":hover": { opacity: 1 } })} data-tab-close
className={css({ cursor: "pointer", opacity: 0 })}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
onCloseDiffTab(path); onCloseDiffTab(path);

View file

@ -30,7 +30,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({
const [css, theme] = useStyletron(); const [css, theme] = useStyletron();
return ( return (
<PanelHeaderBar> <PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none" }}>
{editingField === "title" ? ( {editingField === "title" ? (
<input <input
autoFocus autoFocus
@ -58,7 +58,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({
<LabelSmall <LabelSmall
title="Rename" title="Rename"
color={theme.colors.contentPrimary} color={theme.colors.contentPrimary}
$style={{ fontWeight: 500, whiteSpace: "nowrap", cursor: "pointer", ":hover": { textDecoration: "underline" } }} $style={{ fontWeight: 400, whiteSpace: "nowrap", cursor: "pointer", ":hover": { textDecoration: "underline" } }}
onClick={() => onStartEditingField("title", handoff.title)} onClick={() => onStartEditingField("title", handoff.title)}
> >
{handoff.title} {handoff.title}

View file

@ -178,8 +178,8 @@ export const Shell = styled("div", ({ $theme }) => ({
display: "grid", display: "grid",
gap: "1px", gap: "1px",
height: "100dvh", height: "100dvh",
backgroundColor: $theme.colors.borderOpaque, backgroundColor: $theme.colors.backgroundSecondary,
gridTemplateColumns: "280px minmax(0, 1fr) 380px", gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1.5fr) 380px",
overflow: "hidden", overflow: "hidden",
})); }));