chore(goose2): checkpoint sidebar and composer polish

Signed-off-by: tulsi <tulsi@block.xyz>
This commit is contained in:
tulsi
2026-04-29 16:07:51 -07:00
parent 0f8b4e2897
commit 0cd484897c
7 changed files with 41 additions and 274 deletions
+1 -1
View File
@@ -637,6 +637,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
style={{
width: sidebarCollapsed ? 0 : sidebarWidth + 12,
paddingTop: 12,
paddingBottom: 12,
paddingLeft: 12,
transition: isResizing ? "none" : "width 200ms ease-out",
}}
@@ -659,7 +660,6 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
onMoveToProject={handleMoveToProject}
onReorderProject={projectStore.reorderProjects}
onSelectSession={handleSelectSession}
onSelectSearchResult={handleSelectSearchResult}
activeView={activeView}
activeSessionId={activeSessionId}
projects={projectStore.projects}
+3 -3
View File
@@ -362,14 +362,14 @@ export function ChatInput({
return (
<TooltipProvider delayDuration={300}>
<div className={cn("px-4 pb-6 pt-2", className)}>
<div className={cn("px-4 pb-3 pt-2", className)}>
<div className="mx-auto max-w-3xl">
<Popover open={mentionOpen}>
{/* biome-ignore lint/a11y/noStaticElementInteractions: drop zone for file attachments */}
<div
ref={containerRef}
className={cn(
"relative bg-transparent px-4 pb-3 pt-4 transition-colors",
"relative bg-transparent px-4 pb-2 pt-4 transition-colors",
isAttachmentDragOver && "bg-muted/20",
)}
onDragEnter={handleDragEnter}
@@ -454,7 +454,7 @@ export function ChatInput({
}
disabled={disabled}
rows={1}
className="mb-3 min-h-[36px] max-h-[200px] w-full resize-none bg-transparent px-1 text-[14px] leading-relaxed text-foreground placeholder:font-light placeholder:text-muted-foreground/60 focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:opacity-60"
className="mb-2 min-h-[36px] max-h-[200px] w-full resize-none bg-transparent px-1 text-[14px] leading-relaxed text-foreground placeholder:font-light placeholder:text-muted-foreground/60 focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:opacity-60"
aria-label={t("input.ariaLabel")}
/>
</PopoverAnchor>
+28 -141
View File
@@ -1,8 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { BookOpen, Bot, History, Home, Search } from "lucide-react";
import { getDisplaySessionTitle } from "@/features/chat/lib/sessionTitle";
import { GooseIcon } from "@/shared/ui/icons/GooseIcon";
import { BookOpen, Bot, History, Home } from "lucide-react";
import { cn } from "@/shared/lib/cn";
import type { AppView } from "@/app/types";
import type { ProjectInfo } from "@/features/projects/api/projects";
@@ -12,11 +10,7 @@ import {
useChatSessionStore,
} from "@/features/chat/stores/chatSessionStore";
import { isSessionRunning } from "@/features/chat/lib/sessionActivity";
import { useAgentStore } from "@/features/agents/stores/agentStore";
import { useProjectStore } from "@/features/projects/stores/projectStore";
import { useSessionSearch } from "@/features/sessions/hooks/useSessionSearch";
import { SidebarProjectsSection } from "./SidebarProjectsSection";
import { SidebarSearchResults } from "./SidebarSearchResults";
import { useSidebarHighlight } from "./useSidebarHighlight";
interface SidebarProps {
@@ -34,11 +28,6 @@ interface SidebarProps {
onReorderProject?: (fromId: string, toId: string) => void;
onNavigate?: (view: AppView) => void;
onSelectSession?: (sessionId: string) => void;
onSelectSearchResult?: (
sessionId: string,
messageId?: string,
query?: string,
) => void;
activeView?: AppView;
activeSessionId?: string | null;
className?: string;
@@ -62,15 +51,13 @@ export function Sidebar({
onReorderProject,
onNavigate,
onSelectSession,
onSelectSearchResult,
activeView,
activeSessionId,
className,
projects,
}: SidebarProps) {
const { t, i18n } = useTranslation(["sidebar", "common"]);
const { t } = useTranslation("sidebar");
const [expanded, setExpanded] = useState(!collapsed);
const searchInputRef = useRef<HTMLInputElement>(null);
const prevCollapsed = useRef(collapsed);
const [expandedProjects, setExpandedProjects] = useState<
Record<string, boolean>
@@ -92,10 +79,6 @@ export function Sidebar({
sessions,
chatStore.messagesBySession,
);
const activeSessions = visibleSessions.filter(
(session) => !session.archivedAt,
);
useEffect(() => {
if (collapsed) {
setExpanded(false);
@@ -110,7 +93,6 @@ export function Sidebar({
const labelTransition = "transition-[opacity,width] duration-300 ease-out";
const labelVisible = expanded && !collapsed;
const defaultTitle = t("common:session.defaultTitle");
const navItems: readonly { id: AppView; label: string; icon: typeof Bot }[] =
[
{ id: "agents", label: t("navigation.agents"), icon: Bot },
@@ -171,24 +153,6 @@ export function Sidebar({
return { byProject, standalone: limitedStandalone };
})();
const agentStoreState = useAgentStore();
const projectStoreState = useProjectStore();
const sidebarResolvers = {
getPersonaName: (personaId: string) =>
agentStoreState.getPersonaById(personaId)?.displayName,
getProjectName: (projectId: string) =>
projectStoreState.projects.find((p: { id: string }) => p.id === projectId)
?.name,
};
const sidebarSearch = useSessionSearch({
sessions: activeSessions,
resolvers: sidebarResolvers,
locale: i18n.resolvedLanguage,
getDisplayTitle: (session) =>
getDisplaySessionTitle(session.title, defaultTitle),
});
useEffect(() => {
if (!activeSessionId) return;
const activeSession = visibleSessions.find((s) => s.id === activeSessionId);
@@ -227,17 +191,6 @@ export function Sidebar({
});
}, [projects]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "k" && e.metaKey) {
e.preventDefault();
searchInputRef.current?.focus();
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);
const toggleProject = (projectId: string) =>
setExpandedProjects((prev) => ({ ...prev, [projectId]: !prev[projectId] }));
@@ -298,13 +251,9 @@ export function Sidebar({
WebkitBackdropFilter: "blur(12px)",
}}
>
<div className="flex-shrink-0 px-3 pt-3 pb-1">
<GooseIcon className="text-foreground" />
</div>
<nav
ref={navRef}
className="relative flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-1.5 py-1 pt-1.5 scrollbar-none"
className="relative flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-1.5 pb-5 pt-5 scrollbar-none"
onMouseLeave={onNavMouseLeave}
>
{currentRect && (
@@ -322,32 +271,6 @@ export function Sidebar({
)}
<div className="relative z-10 space-y-0.5">
<div className="mb-4 flex items-center w-full rounded-md gap-2 border-b border-[var(--surface-button)] px-2.5 py-1.5 text-[var(--text-body-alex)] text-muted-foreground hover:text-foreground hover:bg-transparent">
<Search className="size-3.5 flex-shrink-0 text-placeholder" />
<input
ref={searchInputRef}
type="text"
enterKeyHint="search"
value={sidebarSearch.query}
onChange={(e) => sidebarSearch.setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
void sidebarSearch.search();
}
}}
placeholder={t("search.placeholder")}
className={cn(
"focus-override appearance-none bg-transparent border-none text-xs flex-1 min-w-0 placeholder:text-placeholder outline-none focus-visible:ring-0 focus-visible:ring-offset-0",
labelTransition,
labelVisible
? "opacity-100 w-auto"
: "opacity-0 w-0 overflow-hidden",
)}
onClick={(e) => e.stopPropagation()}
/>
</div>
<button
ref={homeRef}
type="button"
@@ -424,67 +347,31 @@ export function Sidebar({
})}
</div>
{sidebarSearch.submittedQuery ? (
<div className="relative z-10 space-y-2">
{sidebarSearch.error && (
<p className="px-1 text-xs text-danger">{t("search.error")}</p>
)}
{sidebarSearch.isSearching &&
sidebarSearch.results.length === 0 && (
<div className="rounded-lg border border-dashed border-border px-3 py-6 text-center text-xs text-muted-foreground">
{t("search.searching")}
</div>
)}
{(!sidebarSearch.isSearching ||
sidebarSearch.results.length > 0) && (
<SidebarSearchResults
results={sidebarSearch.results}
activeSessionId={activeSessionId}
onSelectResult={(sessionId, messageId) => {
if (messageId) {
onSelectSearchResult?.(
sessionId,
messageId,
sidebarSearch.submittedQuery,
);
return;
}
onSelectSession?.(sessionId);
}}
getPersonaName={sidebarResolvers.getPersonaName}
getProjectName={sidebarResolvers.getProjectName}
/>
)}
</div>
) : (
<SidebarProjectsSection
projects={projects}
projectSessions={projectSessions}
expandedProjects={expandedProjects}
toggleProject={toggleProject}
collapsed={collapsed}
labelTransition={labelTransition}
labelVisible={labelVisible}
activeSessionId={activeSessionId}
activeProjectId={activeProjectId}
onNavigate={onNavigate}
onSelectSession={onSelectSession}
onNewChatInProject={onNewChatInProject}
onNewChat={onNewChat}
onCreateProject={onCreateProject}
onEditProject={onEditProject}
onArchiveProject={onArchiveProject}
onArchiveChat={onArchiveChat}
onRenameChat={onRenameChat}
onMoveToProject={onMoveToProject}
onReorderProject={onReorderProject}
onItemMouseEnter={onItemMouseEnter}
activeSessionRefCallback={activeSessionRefCallback}
activeProjectRefCallback={activeProjectRefCallback}
/>
)}
<SidebarProjectsSection
projects={projects}
projectSessions={projectSessions}
expandedProjects={expandedProjects}
toggleProject={toggleProject}
collapsed={collapsed}
labelTransition={labelTransition}
labelVisible={labelVisible}
activeSessionId={activeSessionId}
activeProjectId={activeProjectId}
onNavigate={onNavigate}
onSelectSession={onSelectSession}
onNewChatInProject={onNewChatInProject}
onNewChat={onNewChat}
onCreateProject={onCreateProject}
onEditProject={onEditProject}
onArchiveProject={onArchiveProject}
onArchiveChat={onArchiveChat}
onRenameChat={onRenameChat}
onMoveToProject={onMoveToProject}
onReorderProject={onReorderProject}
onItemMouseEnter={onItemMouseEnter}
activeSessionRefCallback={activeSessionRefCallback}
activeProjectRefCallback={activeProjectRefCallback}
/>
</nav>
</div>
</div>
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import {
IconChevronDown,
IconChevronRight,
IconFolder,
IconLibraryPlusFilled,
IconMessage,
IconPlus,
@@ -154,15 +155,11 @@ function ProjectSection({
: PROJECT_ROW_TEXT_CLASS,
)}
>
<span className="relative flex h-3 w-3 flex-shrink-0 items-center justify-center">
<span
className="absolute inline-block h-2 w-2 rounded-full transition-opacity duration-150 group-hover:opacity-0"
style={{ backgroundColor: project.color }}
/>
<span className="flex h-3 w-3 flex-shrink-0 items-center justify-center text-muted-foreground transition-colors duration-150 group-hover:text-foreground">
{isExpanded ? (
<IconChevronDown className="absolute h-3 w-3 opacity-0 transition-opacity duration-150 group-hover:opacity-100" />
<IconChevronDown className="h-3 w-3" />
) : (
<IconChevronRight className="absolute h-3 w-3 opacity-0 transition-opacity duration-150 group-hover:opacity-100" />
<IconChevronRight className="h-3 w-3" />
)}
</span>
<span className="flex-1 min-w-0 truncate text-left">
@@ -330,11 +327,11 @@ export function SidebarProjectsSection({
: "opacity-0 max-h-0 overflow-hidden",
)}
>
<div className="mx-3 mb-1 h-px bg-[var(--color-gray-200)]" />
<div className="mx-3 mt-3 h-px bg-black/15" />
<div
className={cn(
"group flex items-center transition-all duration-300",
collapsed ? "px-0 pt-0 pb-1 justify-center" : "pt-4 pb-1",
collapsed ? "px-0 pt-0 pb-1 justify-center" : "pt-1 pb-1",
)}
>
<span
@@ -377,10 +374,7 @@ export function SidebarProjectsSection({
onClick={() => onNavigate?.("projects")}
className="rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent/50"
>
<span
className="inline-block size-2.5 rounded-full"
style={{ backgroundColor: project.color }}
/>
<IconFolder className="size-4" />
</Button>
))}
</div>
@@ -468,11 +462,11 @@ export function SidebarProjectsSection({
onDragLeave={handleRecentsDragLeave}
onDrop={handleRecentsDrop}
>
<div className="mx-3 mb-1 h-px bg-[var(--color-gray-200)]" />
<div className="mx-3 mt-3 h-px bg-black/15" />
<div
className={cn(
"relative group flex items-center transition-all duration-300",
collapsed ? "px-0 pt-0 pb-1 justify-center" : "pt-4 pb-1",
collapsed ? "px-0 pt-0 pb-1 justify-center" : "pt-1 pb-1",
)}
>
<span
@@ -1,104 +0,0 @@
import { useTranslation } from "react-i18next";
import { Bot, Folder } from "lucide-react";
import { getDisplaySessionTitle } from "@/features/chat/lib/sessionTitle";
import type { SessionSearchDisplayResult } from "@/features/sessions/lib/buildSessionSearchResults";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
interface SidebarSearchResultsProps {
results: SessionSearchDisplayResult[];
activeSessionId?: string | null;
onSelectResult?: (sessionId: string, messageId?: string) => void;
getPersonaName: (personaId: string) => string | undefined;
getProjectName: (projectId: string) => string | undefined;
}
export function SidebarSearchResults({
results,
activeSessionId,
onSelectResult,
getPersonaName,
getProjectName,
}: SidebarSearchResultsProps) {
const { t } = useTranslation(["sidebar", "sessions", "common"]);
if (results.length === 0) {
return (
<div className="rounded-lg border border-dashed border-border px-3 py-6 text-center text-xs text-muted-foreground">
<p className="font-medium text-foreground/80">
{t("sessions:history.emptyNoMatches")}
</p>
<p className="mt-1">{t("sessions:history.emptyNoMatchesHint")}</p>
</div>
);
}
return (
<div className="space-y-1.5">
{results.map((result) => {
const session = result.session;
const displayTitle = getDisplaySessionTitle(
session.title,
t("common:session.defaultTitle"),
);
const personaName = session.personaId
? getPersonaName(session.personaId)
: undefined;
const projectName = session.projectId
? getProjectName(session.projectId)
: undefined;
return (
<Button
key={session.id}
type="button"
variant="ghost"
onClick={() => onSelectResult?.(session.id, result.messageId)}
className={cn(
"h-auto w-full items-start justify-start rounded-lg border border-transparent px-3 py-2 text-left hover:bg-accent/40",
activeSessionId === session.id && "border-border bg-accent/40",
)}
>
<div className="min-w-0 flex-1 space-y-1">
<p className="truncate text-sm font-medium text-foreground">
{displayTitle}
</p>
{(personaName || projectName) && (
<div className="flex flex-wrap gap-3 text-[11px] text-muted-foreground">
{personaName && (
<span className="inline-flex items-center gap-1">
<Bot className="size-3" />
{personaName}
</span>
)}
{projectName && (
<span className="inline-flex items-center gap-1">
<Folder className="size-3" />
{projectName}
</span>
)}
</div>
)}
{result.snippet && (
<p className="line-clamp-2 text-[11px] text-muted-foreground">
{result.snippet}
</p>
)}
{typeof result.matchCount === "number" && (
<p className="text-[11px] font-medium text-foreground/80">
{t("sessions:search.messageMatches", {
count: result.matchCount,
displayCount: result.matchCount,
})}
</p>
)}
</div>
</Button>
);
})}
</div>
);
}
@@ -16,11 +16,6 @@
"sessionHistory": "Session History",
"skills": "Skills"
},
"search": {
"error": "Message search failed. Showing metadata matches only.",
"placeholder": "Search conversations",
"searching": "Searching chats..."
},
"sections": {
"projects": "Projects",
"recents": "Recents"
@@ -16,11 +16,6 @@
"sessionHistory": "Historial de sesiones",
"skills": "Habilidades"
},
"search": {
"error": "La búsqueda de mensajes falló. Mostrando solo coincidencias de metadatos.",
"placeholder": "Buscar conversaciones",
"searching": "Buscando chats..."
},
"sections": {
"projects": "Proyectos",
"recents": "Recientes"