mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
chore(goose2): checkpoint sidebar and composer polish
Signed-off-by: tulsi <tulsi@block.xyz>
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user