mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
polish sidebar hierarchy and speed up sidebar reloads (#61)
* Polish sidebar hierarchy and speed up sidebar hydration * Avoid clobbering tab selection during startup * Refine active chat row and project spacing
This commit is contained in:
@@ -6,33 +6,47 @@ export function useAppStartup() {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const store = useAgentStore.getState();
|
||||
const loadPersonas = async () => {
|
||||
store.setPersonasLoading(true);
|
||||
try {
|
||||
const { listPersonas } = await import("@/shared/api/agents");
|
||||
const personas = await listPersonas();
|
||||
store.setPersonas(personas);
|
||||
} catch (err) {
|
||||
console.error("Failed to load personas on startup:", err);
|
||||
} finally {
|
||||
store.setPersonasLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
store.setPersonasLoading(true);
|
||||
try {
|
||||
const { listPersonas } = await import("@/shared/api/agents");
|
||||
const personas = await listPersonas();
|
||||
store.setPersonas(personas);
|
||||
} catch (err) {
|
||||
console.error("Failed to load personas on startup:", err);
|
||||
} finally {
|
||||
store.setPersonasLoading(false);
|
||||
}
|
||||
const loadProviders = async () => {
|
||||
store.setProvidersLoading(true);
|
||||
try {
|
||||
const { discoverAcpProviders } = await import("@/shared/api/acp");
|
||||
const providers = await discoverAcpProviders();
|
||||
store.setProviders(providers);
|
||||
} catch (err) {
|
||||
console.error("Failed to load ACP providers on startup:", err);
|
||||
} finally {
|
||||
store.setProvidersLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
store.setProvidersLoading(true);
|
||||
try {
|
||||
const { discoverAcpProviders } = await import("@/shared/api/acp");
|
||||
const providers = await discoverAcpProviders();
|
||||
store.setProviders(providers);
|
||||
} catch (err) {
|
||||
console.error("Failed to load ACP providers on startup:", err);
|
||||
} finally {
|
||||
store.setProvidersLoading(false);
|
||||
}
|
||||
const loadSessionState = async () => {
|
||||
const { loadSessions, loadTabState, setActiveTab } =
|
||||
useChatSessionStore.getState();
|
||||
await loadSessions();
|
||||
const hydratedTabState = await loadTabState();
|
||||
if (hydratedTabState) {
|
||||
setActiveTab(null);
|
||||
}
|
||||
};
|
||||
|
||||
const { loadSessions, loadTabState } = useChatSessionStore.getState();
|
||||
await loadSessions();
|
||||
await loadTabState();
|
||||
useChatSessionStore.getState().setActiveTab(null);
|
||||
await Promise.allSettled([
|
||||
loadPersonas(),
|
||||
loadProviders(),
|
||||
loadSessionState(),
|
||||
]);
|
||||
})();
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from "@/shared/api/chat";
|
||||
import type { Session } from "@/shared/types/chat";
|
||||
|
||||
const SESSION_CACHE_STORAGE_KEY = "goose:chat-sessions";
|
||||
|
||||
// Extended session metadata for tab management
|
||||
export interface ChatSession {
|
||||
id: string; // === sessionId
|
||||
@@ -54,7 +56,7 @@ interface ChatSessionStoreActions {
|
||||
|
||||
// Persistence (stubs for Phase 2b)
|
||||
persistTabState: () => void;
|
||||
loadTabState: () => Promise<void>;
|
||||
loadTabState: () => Promise<boolean>;
|
||||
|
||||
// Helpers
|
||||
getSession: (id: string) => ChatSession | undefined;
|
||||
@@ -63,6 +65,34 @@ interface ChatSessionStoreActions {
|
||||
|
||||
export type ChatSessionStore = ChatSessionStoreState & ChatSessionStoreActions;
|
||||
|
||||
function tabIdsMatch(a: string[], b: string[]): boolean {
|
||||
return a.length === b.length && a.every((id, index) => id === b[index]);
|
||||
}
|
||||
|
||||
function loadCachedSessions(): ChatSession[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const stored = window.localStorage.getItem(SESSION_CACHE_STORAGE_KEY);
|
||||
if (!stored) return [];
|
||||
const parsed = JSON.parse(stored);
|
||||
return Array.isArray(parsed) ? (parsed as ChatSession[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function persistSessions(sessions: ChatSession[]): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
SESSION_CACHE_STORAGE_KEY,
|
||||
JSON.stringify(sessions),
|
||||
);
|
||||
} catch {
|
||||
// localStorage may be unavailable
|
||||
}
|
||||
}
|
||||
|
||||
function sessionToChatSession(session: Session): ChatSession {
|
||||
return {
|
||||
id: session.id,
|
||||
@@ -81,7 +111,7 @@ function sessionToChatSession(session: Session): ChatSession {
|
||||
|
||||
export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
||||
// State
|
||||
sessions: [],
|
||||
sessions: loadCachedSessions(),
|
||||
openTabIds: [],
|
||||
activeTabId: null,
|
||||
isLoading: false,
|
||||
@@ -117,6 +147,7 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
||||
set((state) => ({
|
||||
sessions: [...state.sessions, chatSession],
|
||||
}));
|
||||
persistSessions([...get().sessions]);
|
||||
return chatSession;
|
||||
},
|
||||
|
||||
@@ -126,6 +157,7 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
||||
const backendSessions = await apiListSessions();
|
||||
const chatSessions = backendSessions.map(sessionToChatSession);
|
||||
set({ sessions: chatSessions });
|
||||
persistSessions(chatSessions);
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
@@ -139,6 +171,7 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
||||
: s,
|
||||
),
|
||||
}));
|
||||
persistSessions(get().sessions);
|
||||
const backendPatch: {
|
||||
title?: string;
|
||||
providerId?: string;
|
||||
@@ -181,11 +214,11 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
||||
try {
|
||||
await apiArchiveSession(id);
|
||||
const archivedAt = new Date().toISOString();
|
||||
set((state) => ({
|
||||
sessions: state.sessions
|
||||
.map((s) => (s.id === id ? { ...s, archivedAt } : s))
|
||||
.filter((s) => !s.archivedAt),
|
||||
}));
|
||||
const nextSessions = get()
|
||||
.sessions.map((s) => (s.id === id ? { ...s, archivedAt } : s))
|
||||
.filter((s) => !s.archivedAt);
|
||||
set({ sessions: nextSessions });
|
||||
persistSessions(nextSessions);
|
||||
} catch (err) {
|
||||
set({
|
||||
openTabIds,
|
||||
@@ -263,9 +296,24 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
||||
},
|
||||
|
||||
loadTabState: async () => {
|
||||
const baselineOpenTabIds = get().openTabIds;
|
||||
const baselineActiveTabId = get().activeTabId;
|
||||
try {
|
||||
const { openTabIds, activeTabId } = await apiLoadUiState();
|
||||
const { sessions } = get();
|
||||
const {
|
||||
sessions,
|
||||
openTabIds: currentOpenTabIds,
|
||||
activeTabId: currentActiveTabId,
|
||||
} = get();
|
||||
|
||||
const userChangedTabs =
|
||||
currentActiveTabId !== baselineActiveTabId ||
|
||||
!tabIdsMatch(currentOpenTabIds, baselineOpenTabIds);
|
||||
|
||||
if (userChangedTabs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sessionIds = new Set(sessions.map((s) => s.id));
|
||||
|
||||
// Filter to only tabs whose sessions still exist
|
||||
@@ -279,8 +327,10 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
||||
openTabIds: validTabIds,
|
||||
activeTabId: validActiveTabId,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("Failed to load tab state:", err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -7,6 +7,32 @@ import {
|
||||
type ProjectInfo,
|
||||
} from "../api/projects";
|
||||
|
||||
const PROJECT_CACHE_STORAGE_KEY = "goose:projects";
|
||||
|
||||
function loadCachedProjects(): ProjectInfo[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const stored = window.localStorage.getItem(PROJECT_CACHE_STORAGE_KEY);
|
||||
if (!stored) return [];
|
||||
const parsed = JSON.parse(stored);
|
||||
return Array.isArray(parsed) ? (parsed as ProjectInfo[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function persistProjects(projects: ProjectInfo[]): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
PROJECT_CACHE_STORAGE_KEY,
|
||||
JSON.stringify(projects),
|
||||
);
|
||||
} catch {
|
||||
// localStorage may be unavailable
|
||||
}
|
||||
}
|
||||
|
||||
interface ProjectState {
|
||||
projects: ProjectInfo[];
|
||||
loading: boolean;
|
||||
@@ -43,7 +69,7 @@ interface ProjectState {
|
||||
}
|
||||
|
||||
export const useProjectStore = create<ProjectState>((set, get) => ({
|
||||
projects: [],
|
||||
projects: loadCachedProjects(),
|
||||
loading: false,
|
||||
activeProjectId: null,
|
||||
|
||||
@@ -52,8 +78,9 @@ export const useProjectStore = create<ProjectState>((set, get) => ({
|
||||
try {
|
||||
const projects = await listProjects();
|
||||
set({ projects, loading: false });
|
||||
persistProjects(projects);
|
||||
} catch {
|
||||
set({ projects: [], loading: false });
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -80,6 +107,7 @@ export const useProjectStore = create<ProjectState>((set, get) => ({
|
||||
useWorktrees,
|
||||
);
|
||||
set((state) => ({ projects: [...state.projects, project] }));
|
||||
persistProjects(get().projects);
|
||||
return project;
|
||||
},
|
||||
|
||||
@@ -110,6 +138,7 @@ export const useProjectStore = create<ProjectState>((set, get) => ({
|
||||
set((state) => ({
|
||||
projects: state.projects.map((p) => (p.id === id ? project : p)),
|
||||
}));
|
||||
persistProjects(get().projects);
|
||||
return project;
|
||||
},
|
||||
|
||||
@@ -120,6 +149,7 @@ export const useProjectStore = create<ProjectState>((set, get) => ({
|
||||
activeProjectId:
|
||||
state.activeProjectId === id ? null : state.activeProjectId,
|
||||
}));
|
||||
persistProjects(get().projects);
|
||||
},
|
||||
|
||||
setActiveProject: (id) => set({ activeProjectId: id }),
|
||||
|
||||
@@ -42,6 +42,10 @@ const NAV_ITEMS: readonly { id: AppView; label: string; icon: typeof Bot }[] = [
|
||||
{ id: "skills", label: "Skills", icon: BookOpen },
|
||||
];
|
||||
|
||||
const SIDEBAR_NAV_TEXT_CLASS =
|
||||
"text-foreground-subtle hover:text-foreground hover:bg-accent";
|
||||
const EXPANDED_PROJECTS_STORAGE_KEY = "goose:sidebar:expanded-projects";
|
||||
|
||||
export function Sidebar({
|
||||
collapsed,
|
||||
width = 240,
|
||||
@@ -66,7 +70,17 @@ export function Sidebar({
|
||||
const prevCollapsed = useRef(collapsed);
|
||||
const [expandedProjects, setExpandedProjects] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
>(() => {
|
||||
if (typeof window === "undefined") return {};
|
||||
try {
|
||||
const stored = window.localStorage.getItem(EXPANDED_PROJECTS_STORAGE_KEY);
|
||||
if (!stored) return {};
|
||||
const parsed = JSON.parse(stored);
|
||||
return parsed && typeof parsed === "object" ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
// Read all sessions and open tab IDs from the store
|
||||
const { sessions, openTabIds } = useChatSessionStore();
|
||||
@@ -138,6 +152,32 @@ export function Sidebar({
|
||||
}
|
||||
}, [activeTabId, sessions]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
EXPANDED_PROJECTS_STORAGE_KEY,
|
||||
JSON.stringify(expandedProjects),
|
||||
);
|
||||
} catch {
|
||||
// localStorage may be unavailable
|
||||
}
|
||||
}, [expandedProjects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projects.length === 0) return;
|
||||
const validProjectIds = new Set(projects.map((project) => project.id));
|
||||
setExpandedProjects((prev) => {
|
||||
const next = Object.fromEntries(
|
||||
Object.entries(prev).filter(([projectId]) =>
|
||||
validProjectIds.has(projectId),
|
||||
),
|
||||
);
|
||||
return Object.keys(next).length === Object.keys(prev).length
|
||||
? prev
|
||||
: next;
|
||||
});
|
||||
}, [projects]);
|
||||
|
||||
const toggleProject = (projectId: string) => {
|
||||
setExpandedProjects((prev) => ({
|
||||
...prev,
|
||||
@@ -258,7 +298,8 @@ export function Sidebar({
|
||||
onClick={onNewChat}
|
||||
title={collapsed ? "New Chat" : undefined}
|
||||
className={cn(
|
||||
"flex items-center w-full rounded-md text-[13px] transition-all duration-200 text-foreground-secondary hover:text-foreground hover:bg-background-tertiary/50",
|
||||
"flex items-center w-full rounded-md text-[13px] transition-all duration-200",
|
||||
SIDEBAR_NAV_TEXT_CLASS,
|
||||
collapsed
|
||||
? "justify-center px-0 py-1.5"
|
||||
: "gap-2.5 px-3 py-1.5",
|
||||
@@ -278,7 +319,7 @@ export function Sidebar({
|
||||
</button>
|
||||
|
||||
{/* Nav items */}
|
||||
{NAV_ITEMS.map((item, index) => {
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeView === item.id;
|
||||
return (
|
||||
@@ -294,13 +335,9 @@ export function Sidebar({
|
||||
: "gap-2.5 px-3 py-1.5",
|
||||
isActive
|
||||
? "bg-background-secondary text-foreground"
|
||||
: "text-foreground-secondary hover:text-foreground hover:bg-background-tertiary/50",
|
||||
: SIDEBAR_NAV_TEXT_CLASS,
|
||||
)}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
style={{
|
||||
transitionDelay:
|
||||
!collapsed && expanded ? `${index * 30}ms` : "0ms",
|
||||
}}
|
||||
>
|
||||
<Icon className="w-4 h-4 flex-shrink-0" />
|
||||
<span
|
||||
@@ -310,11 +347,6 @@ export function Sidebar({
|
||||
? "opacity-100 w-auto"
|
||||
: "opacity-0 w-0 overflow-hidden",
|
||||
)}
|
||||
style={{
|
||||
transitionDelay: labelVisible
|
||||
? `${index * 30 + 60}ms`
|
||||
: "0ms",
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
|
||||
@@ -2,6 +2,10 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { MoreHorizontal, Pencil, Trash2 } from "lucide-react";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
|
||||
const OPEN_CHAT_ROW_CLASS = "text-foreground group-hover:text-foreground";
|
||||
const INACTIVE_CHAT_ROW_CLASS = "text-foreground group-hover:text-foreground";
|
||||
const ACTIVE_CHAT_ROW_CLASS = "bg-accent text-foreground";
|
||||
|
||||
interface SidebarChatRowProps {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -71,7 +75,7 @@ export function SidebarChatRow({
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<div className={cn("flex items-center group", className)}>
|
||||
<div className={cn("flex items-center group rounded-md", className)}>
|
||||
<div className="flex items-center flex-1 min-w-0 py-1.5 rounded-md text-[13px] px-2.5">
|
||||
<input
|
||||
ref={inputRef}
|
||||
@@ -101,7 +105,13 @@ export function SidebarChatRow({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center group", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center group rounded-md transition-colors duration-150",
|
||||
isActive ? "bg-accent" : "hover:bg-accent",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect?.(id)}
|
||||
@@ -112,13 +122,13 @@ export function SidebarChatRow({
|
||||
}}
|
||||
title="Double-click to rename"
|
||||
className={cn(
|
||||
"flex items-center gap-2 flex-1 min-w-0 py-1.5 rounded-md text-[13px]",
|
||||
"transition-colors duration-150 px-2.5 text-left",
|
||||
"flex items-center gap-2 flex-1 min-w-0 py-1.5 text-[13px]",
|
||||
"transition-colors duration-150 px-2.5 text-left bg-transparent",
|
||||
isActive
|
||||
? "bg-background-tertiary/70 text-foreground font-medium"
|
||||
? ACTIVE_CHAT_ROW_CLASS
|
||||
: isOpen
|
||||
? "text-foreground hover:bg-background-tertiary/50"
|
||||
: "text-foreground-secondary/70 hover:text-foreground hover:bg-background-tertiary/50",
|
||||
? OPEN_CHAT_ROW_CLASS
|
||||
: INACTIVE_CHAT_ROW_CLASS,
|
||||
)}
|
||||
>
|
||||
<span className="flex-1 min-w-0 truncate">{title}</span>
|
||||
@@ -136,7 +146,7 @@ export function SidebarChatRow({
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center w-6 h-6 rounded-md",
|
||||
"text-foreground-secondary/40 hover:text-foreground hover:bg-background-tertiary/50",
|
||||
"text-muted-foreground/60 hover:text-foreground",
|
||||
menuOpen
|
||||
? "visible opacity-100"
|
||||
: "invisible group-hover:visible opacity-0 group-hover:opacity-100",
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { ProjectInfo } from "@/features/projects/api/projects";
|
||||
import { SidebarChatRow } from "./SidebarChatRow";
|
||||
|
||||
const MAX_VISIBLE_CHATS = 3;
|
||||
const PROJECT_ROW_TEXT_CLASS =
|
||||
"text-foreground-subtle group-hover:text-foreground";
|
||||
|
||||
interface TabInfo {
|
||||
id: string;
|
||||
@@ -80,7 +82,7 @@ function ItemMenu({
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center w-6 h-6 rounded-md",
|
||||
"text-foreground-secondary/40 hover:text-foreground hover:bg-background-tertiary/50",
|
||||
"text-foreground-secondary/40 hover:text-foreground",
|
||||
open
|
||||
? "visible opacity-100"
|
||||
: "invisible group-hover:visible opacity-0 group-hover:opacity-100",
|
||||
@@ -161,25 +163,27 @@ function ProjectSection({
|
||||
return (
|
||||
<div>
|
||||
{/* Project row */}
|
||||
<div className="flex items-center group">
|
||||
<div className="flex items-center group rounded-md transition-colors duration-150 hover:bg-accent">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleProject(project.id)}
|
||||
className={cn(
|
||||
"flex items-center flex-1 min-w-0 gap-2 py-1.5 px-2.5 rounded-md text-[13px]",
|
||||
"flex items-center flex-1 min-w-0 gap-1.5 py-1.5 px-2.5 text-[13px]",
|
||||
"transition-colors duration-150",
|
||||
"text-foreground-secondary hover:text-foreground hover:bg-background-tertiary/50",
|
||||
PROJECT_ROW_TEXT_CLASS,
|
||||
)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-3 h-3 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="w-3 h-3 flex-shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: project.color }}
|
||||
/>
|
||||
<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 }}
|
||||
/>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="absolute h-3 w-3 opacity-0 transition-opacity duration-150 group-hover:opacity-100" />
|
||||
) : (
|
||||
<ChevronRight className="absolute h-3 w-3 opacity-0 transition-opacity duration-150 group-hover:opacity-100" />
|
||||
)}
|
||||
</span>
|
||||
<span className="flex-1 min-w-0 truncate text-left">
|
||||
{project.name}
|
||||
</span>
|
||||
@@ -198,7 +202,7 @@ function ProjectSection({
|
||||
title="New chat in project"
|
||||
className={cn(
|
||||
"flex items-center justify-center w-6 h-6 rounded-md mr-1 flex-shrink-0",
|
||||
"text-foreground-secondary/50 hover:text-foreground hover:bg-background-tertiary/50",
|
||||
"text-foreground-secondary/50 hover:text-foreground",
|
||||
"invisible group-hover:visible opacity-0 group-hover:opacity-100",
|
||||
)}
|
||||
>
|
||||
@@ -208,7 +212,7 @@ function ProjectSection({
|
||||
|
||||
{/* Nested chats */}
|
||||
{isExpanded && (
|
||||
<div className="space-y-0.5">
|
||||
<div className="mt-0.5 space-y-0.5">
|
||||
{visibleChats.map((tab) => {
|
||||
const isActive = activeTabId === tab.id;
|
||||
const isOpen = tab.isOpenTab ?? false;
|
||||
@@ -285,12 +289,6 @@ export function SidebarProjectsSection({
|
||||
onArchiveChat,
|
||||
onRenameChat,
|
||||
}: SidebarProjectsSectionProps) {
|
||||
const [showAllRecents, setShowAllRecents] = useState(false);
|
||||
const visibleRecents = showAllRecents
|
||||
? projectTabs.standalone
|
||||
: projectTabs.standalone.slice(0, MAX_VISIBLE_CHATS);
|
||||
const hasMoreRecents = projectTabs.standalone.length > MAX_VISIBLE_CHATS;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -404,7 +402,7 @@ export function SidebarProjectsSection({
|
||||
|
||||
{collapsed ? (
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
{visibleRecents.map((tab) => (
|
||||
{projectTabs.standalone.map((tab) => (
|
||||
<button
|
||||
type="button"
|
||||
key={tab.id}
|
||||
@@ -423,7 +421,7 @@ export function SidebarProjectsSection({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
{visibleRecents.map((tab) => {
|
||||
{projectTabs.standalone.map((tab) => {
|
||||
const isActive = activeTabId === tab.id;
|
||||
const isOpen = tab.isOpenTab ?? false;
|
||||
return (
|
||||
@@ -439,30 +437,6 @@ export function SidebarProjectsSection({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{hasMoreRecents && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllRecents((prev) => !prev)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 w-full px-2.5 py-1 text-[11px]",
|
||||
"text-foreground-secondary/60 hover:text-foreground-secondary transition-colors duration-150",
|
||||
)}
|
||||
>
|
||||
{showAllRecents ? (
|
||||
<>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
Show less
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
{projectTabs.standalone.length > 8
|
||||
? `View all ${projectTabs.standalone.length} chats`
|
||||
: `${projectTabs.standalone.length - MAX_VISIBLE_CHATS} more`}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
|
||||
/* Text */
|
||||
--color-text-primary: #0a0a0a;
|
||||
--color-text-subtle: #5f5f5f;
|
||||
--color-text-secondary: #878787;
|
||||
--color-text-tertiary: #a3a3a3;
|
||||
--color-text-inverse: #ffffff;
|
||||
@@ -138,6 +139,7 @@
|
||||
|
||||
/* Text */
|
||||
--color-text-primary: #fafafa;
|
||||
--color-text-subtle: #c2c2c2;
|
||||
--color-text-secondary: #a3a3a3;
|
||||
--color-text-tertiary: #666666;
|
||||
--color-text-inverse: #000000;
|
||||
|
||||
@@ -63,6 +63,7 @@ export default {
|
||||
foreground: {
|
||||
DEFAULT: "var(--color-text-primary)",
|
||||
primary: "var(--color-text-primary)",
|
||||
subtle: "var(--color-text-subtle)",
|
||||
secondary: "var(--color-text-secondary)",
|
||||
tertiary: "var(--color-text-tertiary)",
|
||||
inverse: "var(--color-text-inverse)",
|
||||
|
||||
Reference in New Issue
Block a user