mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 16:02:42 +00:00
Foundry UI polish: real org data, project icons, model picker (#233)
* 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> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
34a0587cbc
commit
32008797da
13 changed files with 895 additions and 607 deletions
|
|
@ -1,11 +1,27 @@
|
|||
import { memo, useState } from "react";
|
||||
import { useStyletron } from "baseui";
|
||||
import { LabelSmall, LabelXSmall } from "baseui/typography";
|
||||
import { CloudUpload, GitPullRequestDraft, Plus } from "lucide-react";
|
||||
import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, Plus } from "lucide-react";
|
||||
|
||||
import { formatRelativeAge, type Handoff, type ProjectSection } from "./view-model";
|
||||
import { ContextMenuOverlay, HandoffIndicator, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
|
||||
|
||||
const PROJECT_COLORS = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316"];
|
||||
|
||||
function projectInitial(label: string): string {
|
||||
const parts = label.split("/");
|
||||
const name = parts[parts.length - 1] ?? label;
|
||||
return name.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
function projectIconColor(label: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < label.length; i++) {
|
||||
hash = (hash * 31 + label.charCodeAt(i)) | 0;
|
||||
}
|
||||
return PROJECT_COLORS[Math.abs(hash) % PROJECT_COLORS.length]!;
|
||||
}
|
||||
|
||||
export const Sidebar = memo(function Sidebar({
|
||||
projects,
|
||||
activeId,
|
||||
|
|
@ -25,13 +41,25 @@ export const Sidebar = memo(function Sidebar({
|
|||
}) {
|
||||
const [css, theme] = useStyletron();
|
||||
const contextMenu = useContextMenu();
|
||||
const [expandedProjects, setExpandedProjects] = useState<Record<string, boolean>>({});
|
||||
const [collapsedProjects, setCollapsedProjects] = useState<Record<string, boolean>>({});
|
||||
|
||||
return (
|
||||
<SPanel>
|
||||
<style>{`
|
||||
[data-project-header]:hover [data-chevron] {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
[data-project-header]:hover [data-project-icon] {
|
||||
display: none !important;
|
||||
}
|
||||
`}</style>
|
||||
<PanelHeaderBar>
|
||||
<LabelSmall color={theme.colors.contentPrimary} $style={{ fontWeight: 600, flex: 1, fontSize: "13px" }}>
|
||||
Handoffs
|
||||
<LabelSmall
|
||||
color={theme.colors.contentPrimary}
|
||||
$style={{ fontWeight: 600, flex: 1, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px" }}
|
||||
>
|
||||
<ListChecks size={14} />
|
||||
Tasks
|
||||
</LabelSmall>
|
||||
<button
|
||||
onClick={onCreate}
|
||||
|
|
@ -56,38 +84,78 @@ export const Sidebar = memo(function Sidebar({
|
|||
<ScrollBody>
|
||||
<div className={css({ padding: "8px", display: "flex", flexDirection: "column", gap: "4px" })}>
|
||||
{projects.map((project) => {
|
||||
const visibleCount = expandedProjects[project.id] ? project.handoffs.length : Math.min(project.handoffs.length, 5);
|
||||
const hiddenCount = Math.max(0, project.handoffs.length - visibleCount);
|
||||
const isCollapsed = collapsedProjects[project.id] === true;
|
||||
|
||||
return (
|
||||
<div key={project.id} className={css({ display: "flex", flexDirection: "column", gap: "4px" })}>
|
||||
<div
|
||||
onClick={() =>
|
||||
setCollapsedProjects((current) => ({
|
||||
...current,
|
||||
[project.id]: !current[project.id],
|
||||
}))
|
||||
}
|
||||
data-project-header
|
||||
className={css({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "10px 8px 4px",
|
||||
gap: "8px",
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
":hover": { opacity: 0.8 },
|
||||
})}
|
||||
>
|
||||
<LabelSmall
|
||||
color={theme.colors.contentSecondary}
|
||||
$style={{
|
||||
fontSize: "11px",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.05em",
|
||||
textTransform: "uppercase",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{project.label}
|
||||
</LabelSmall>
|
||||
<LabelXSmall color={theme.colors.contentTertiary}>{formatRelativeAge(project.updatedAtMs)}</LabelXSmall>
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "4px", overflow: "hidden" })}>
|
||||
<div className={css({ position: "relative", width: "14px", height: "14px", flexShrink: 0 })}>
|
||||
<span
|
||||
className={css({
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "3px",
|
||||
fontSize: "9px",
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
color: "#fff",
|
||||
backgroundColor: projectIconColor(project.label),
|
||||
})}
|
||||
data-project-icon
|
||||
>
|
||||
{projectInitial(project.label)}
|
||||
</span>
|
||||
<span className={css({ position: "absolute", inset: 0, display: "none", alignItems: "center", justifyContent: "center" })} data-chevron>
|
||||
{isCollapsed ? (
|
||||
<ChevronDown size={12} color={theme.colors.contentTertiary} />
|
||||
) : (
|
||||
<ChevronUp size={12} color={theme.colors.contentTertiary} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<LabelSmall
|
||||
color={theme.colors.contentSecondary}
|
||||
$style={{
|
||||
fontSize: "11px",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.05em",
|
||||
textTransform: "uppercase",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{project.label}
|
||||
</LabelSmall>
|
||||
</div>
|
||||
<LabelXSmall color={theme.colors.contentTertiary}>
|
||||
{formatRelativeAge(project.updatedAtMs)}
|
||||
</LabelXSmall>
|
||||
</div>
|
||||
|
||||
{project.handoffs.slice(0, visibleCount).map((handoff) => {
|
||||
{!isCollapsed && project.handoffs.map((handoff) => {
|
||||
const isActive = handoff.id === activeId;
|
||||
const isDim = handoff.status === "archived";
|
||||
const isRunning = handoff.tabs.some((tab) => tab.status === "running");
|
||||
|
|
@ -103,7 +171,7 @@ export const Sidebar = memo(function Sidebar({
|
|||
onClick={() => onSelect(handoff.id)}
|
||||
onContextMenu={(event) =>
|
||||
contextMenu.open(event, [
|
||||
{ label: "Rename handoff", onClick: () => onRenameHandoff(handoff.id) },
|
||||
{ label: "Rename task", onClick: () => onRenameHandoff(handoff.id) },
|
||||
{ label: "Rename branch", onClick: () => onRenameBranch(handoff.id) },
|
||||
{ label: "Mark as unread", onClick: () => onMarkUnread(handoff.id) },
|
||||
])
|
||||
|
|
@ -111,13 +179,12 @@ export const Sidebar = memo(function Sidebar({
|
|||
className={css({
|
||||
padding: "12px",
|
||||
borderRadius: "8px",
|
||||
border: isActive ? "1px solid rgba(255, 255, 255, 0.2)" : "1px solid transparent",
|
||||
border: "1px solid transparent",
|
||||
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
borderColor: theme.colors.borderOpaque,
|
||||
},
|
||||
})}
|
||||
>
|
||||
|
|
@ -184,27 +251,6 @@ export const Sidebar = memo(function Sidebar({
|
|||
);
|
||||
})}
|
||||
|
||||
{hiddenCount > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setExpandedProjects((current) => ({
|
||||
...current,
|
||||
[project.id]: true,
|
||||
}))
|
||||
}
|
||||
className={css({
|
||||
all: "unset",
|
||||
padding: "8px 12px 10px 34px",
|
||||
color: theme.colors.contentSecondary,
|
||||
fontSize: "12px",
|
||||
cursor: "pointer",
|
||||
":hover": { color: theme.colors.contentPrimary },
|
||||
})}
|
||||
>
|
||||
Show {hiddenCount} more
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue