From d54a46209b99f7dab298e3adb607efc4e63116c7 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 14 Jan 2026 05:14:50 -0800 Subject: [PATCH] sidebar ui changes, loading state fixes (#2039) * tons of sidebar ui cleanup, loading states etc * lint fix * fix archive store * i18n --------- Co-authored-by: Louis Knight-Webb --- .../components/ui-new/ConversationList.tsx | 48 ++--- .../src/components/ui-new/actions/index.ts | 6 +- .../containers/SessionChatBoxContainer.tsx | 40 +++- .../ui-new/containers/WorkspacesLayout.tsx | 26 +++ .../ui-new/primitives/WorkspaceSummary.tsx | 98 ++++++++-- .../ui-new/views/WorkspacesSidebar.tsx | 184 +++++++++++------- frontend/src/i18n/locales/en/common.json | 7 + frontend/src/i18n/locales/es/common.json | 7 + frontend/src/i18n/locales/ja/common.json | 7 + frontend/src/i18n/locales/ko/common.json | 7 + frontend/src/i18n/locales/zh-Hans/common.json | 7 + frontend/src/i18n/locales/zh-Hant/common.json | 7 + 12 files changed, 328 insertions(+), 116 deletions(-) diff --git a/frontend/src/components/ui-new/ConversationList.tsx b/frontend/src/components/ui-new/ConversationList.tsx index e84e5cef..d699dd88 100644 --- a/frontend/src/components/ui-new/ConversationList.tsx +++ b/frontend/src/components/ui-new/ConversationList.tsx @@ -7,9 +7,8 @@ import { VirtuosoMessageListProps, } from '@virtuoso.dev/message-list'; import { useEffect, useMemo, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { SpinnerGapIcon } from '@phosphor-icons/react'; +import { cn } from '@/lib/utils'; import NewDisplayConversationEntry from './NewDisplayConversationEntry'; import { ApprovalFormProvider } from '@/contexts/ApprovalFormContext'; import { useEntries } from '@/contexts/EntriesContext'; @@ -86,7 +85,6 @@ const computeItemKey: VirtuosoMessageListProps< >['computeItemKey'] = ({ data }) => `conv-${data.patchKey}`; export function ConversationList({ attempt, task }: ConversationListProps) { - const { t } = useTranslation('common'); const [channelData, setChannelData] = useState | null>(null); const [loading, setLoading] = useState(true); @@ -156,29 +154,33 @@ export function ConversationList({ attempt, task }: ConversationListProps) { [attempt, task] ); + // Determine if content is ready to show (has data or finished loading) + const hasContent = !loading || (channelData?.data?.length ?? 0) > 0; + return ( - - - ref={messageListRef} - className="h-full scrollbar-none" - data={channelData} - initialLocation={INITIAL_TOP_ITEM} - context={messageListContext} - computeItemKey={computeItemKey} - ItemContent={ItemContent} - Header={() =>
} - Footer={() =>
} - /> - - {loading && !channelData?.data?.length && ( -
- -

{t('states.loadingHistory')}

-
- )} + + + ref={messageListRef} + className="h-full scrollbar-none" + data={channelData} + initialLocation={INITIAL_TOP_ITEM} + context={messageListContext} + computeItemKey={computeItemKey} + ItemContent={ItemContent} + Header={() =>
} + Footer={() =>
} + /> + +
); } diff --git a/frontend/src/components/ui-new/actions/index.ts b/frontend/src/components/ui-new/actions/index.ts index 9e23f58e..1dfb8acd 100644 --- a/frontend/src/components/ui-new/actions/index.ts +++ b/frontend/src/components/ui-new/actions/index.ts @@ -944,11 +944,7 @@ export type NavbarItem = ActionDefinition | typeof NavbarDivider; // Navbar action groups define which actions appear in each section export const NavbarActionGroups = { - left: [ - Actions.OpenInOldUI, - NavbarDivider, - Actions.ArchiveWorkspace, - ] as ActionDefinition[], + left: [Actions.OpenInOldUI] as ActionDefinition[], right: [ Actions.ToggleDiffViewMode, Actions.ToggleAllDiffs, diff --git a/frontend/src/components/ui-new/containers/SessionChatBoxContainer.tsx b/frontend/src/components/ui-new/containers/SessionChatBoxContainer.tsx index bc72ff32..2f7da5e8 100644 --- a/frontend/src/components/ui-new/containers/SessionChatBoxContainer.tsx +++ b/frontend/src/components/ui-new/containers/SessionChatBoxContainer.tsx @@ -545,9 +545,43 @@ export function SessionChatBoxContainer({ localMessage, ]); - // Don't render if no session and not in new session mode - if (!session && !isNewSessionMode) { - return null; + // Render placeholder state if no session and not in new session mode + // This maintains the visual structure during workspace transitions + const isPlaceholderMode = !session && !isNewSessionMode; + + // In placeholder mode, render a disabled version to maintain visual structure + if (isPlaceholderMode) { + return ( + {}, + }} + actions={{ + onSend: () => {}, + onQueue: () => {}, + onCancelQueue: () => {}, + onStop: () => {}, + onPasteFiles: () => {}, + }} + session={{ + sessions: [], + selectedSessionId: undefined, + onSelectSession: () => {}, + isNewSessionMode: false, + onNewSession: undefined, + }} + stats={{ + filesChanged: 0, + linesAdded: 0, + linesRemoved: 0, + onViewCode: undefined, + }} + /> + ); } return ( diff --git a/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx b/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx index 802f1c0f..1da83e7a 100644 --- a/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx +++ b/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx @@ -40,6 +40,7 @@ import { PERSIST_KEYS, useExpandedAll, usePaneSize, + usePersistedExpanded, } from '@/stores/useUiPreferencesStore'; import { useLayoutStore, @@ -304,6 +305,10 @@ export function WorkspacesLayout() { 50 ); const isRightMainPanelVisible = useIsRightMainPanelVisible(); + const [showArchive, setShowArchive] = usePersistedExpanded( + PERSIST_KEYS.workspacesSidebarArchived, + false + ); const defaultLayout = (): Layout => { let layout = { 'left-main': 50, 'right-main': 50 }; @@ -710,6 +715,23 @@ export function WorkspacesLayout() { ); }; + // Action handlers for sidebar workspace actions + const { executeAction } = useActions(); + + const handleArchiveWorkspace = useCallback( + (workspaceId: string) => { + executeAction(Actions.ArchiveWorkspace, workspaceId); + }, + [executeAction] + ); + + const handlePinWorkspace = useCallback( + (workspaceId: string) => { + executeAction(Actions.PinWorkspace, workspaceId); + }, + [executeAction] + ); + // Render sidebar with persisted draft title const renderSidebar = () => ( ); diff --git a/frontend/src/components/ui-new/primitives/WorkspaceSummary.tsx b/frontend/src/components/ui-new/primitives/WorkspaceSummary.tsx index f8ba8b51..94c66066 100644 --- a/frontend/src/components/ui-new/primitives/WorkspaceSummary.tsx +++ b/frontend/src/components/ui-new/primitives/WorkspaceSummary.tsx @@ -1,12 +1,13 @@ import { PushPinIcon, - DotsThreeIcon, HandIcon, TriangleIcon, PlayIcon, FileIcon, CircleIcon, GitPullRequestIcon, + ArchiveIcon, + ListIcon, } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import { cn } from '@/lib/utils'; @@ -30,6 +31,8 @@ interface WorkspaceSummaryProps { latestProcessStatus?: 'running' | 'completed' | 'failed' | 'killed'; prStatus?: 'open' | 'merged' | 'closed' | 'unknown'; onClick?: () => void; + onArchive?: () => void; + onPin?: () => void; className?: string; summary?: boolean; /** Whether this is a draft workspace (shows "Draft" instead of elapsed time) */ @@ -52,6 +55,8 @@ export function WorkspaceSummary({ latestProcessStatus, prStatus, onClick, + onArchive, + onPin, className, summary = false, isDraft = false, @@ -69,21 +74,41 @@ export function WorkspaceSummary({ }); }; + const handleArchive = (e: React.MouseEvent) => { + e.stopPropagation(); + onArchive?.(); + }; + + const handlePin = (e: React.MouseEvent) => { + e.stopPropagation(); + onPin?.(); + }; + return ( -
+
+ {/* Selection indicator - thin colored tab on the left */} +
+ {/* Right-side hover zone for action overlay */} {workspaceId && ( -
- + {/* Gradient fade from transparent to pill background */} +
+ {/* Action pill */} +
+ + + +
+
)}
diff --git a/frontend/src/components/ui-new/views/WorkspacesSidebar.tsx b/frontend/src/components/ui-new/views/WorkspacesSidebar.tsx index 714c8cc8..57f79eab 100644 --- a/frontend/src/components/ui-new/views/WorkspacesSidebar.tsx +++ b/frontend/src/components/ui-new/views/WorkspacesSidebar.tsx @@ -1,11 +1,9 @@ -import { PlusIcon } from '@phosphor-icons/react'; +import { PlusIcon, ArrowLeftIcon, ArchiveIcon } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import type { Workspace } from '@/components/ui-new/hooks/useWorkspaces'; -import { CollapsibleSection } from '@/components/ui-new/primitives/CollapsibleSection'; import { InputField } from '@/components/ui-new/primitives/InputField'; import { WorkspaceSummary } from '@/components/ui-new/primitives/WorkspaceSummary'; import { SectionHeader } from '../primitives/SectionHeader'; -import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore'; interface WorkspacesSidebarProps { workspaces: Workspace[]; @@ -13,6 +11,8 @@ interface WorkspacesSidebarProps { selectedWorkspaceId: string | null; onSelectWorkspace: (id: string) => void; onAddWorkspace?: () => void; + onArchiveWorkspace?: (id: string) => void; + onPinWorkspace?: (id: string) => void; searchQuery: string; onSearchChange: (value: string) => void; /** Whether we're in create mode */ @@ -21,6 +21,10 @@ interface WorkspacesSidebarProps { draftTitle?: string; /** Handler to navigate back to create mode */ onSelectCreate?: () => void; + /** Whether to show archived workspaces */ + showArchive?: boolean; + /** Handler for toggling archive view */ + onShowArchiveChange?: (show: boolean) => void; } export function WorkspacesSidebar({ @@ -29,11 +33,15 @@ export function WorkspacesSidebar({ selectedWorkspaceId, onSelectWorkspace, onAddWorkspace, + onArchiveWorkspace, + onPinWorkspace, searchQuery, onSearchChange, isCreateMode = false, draftTitle, onSelectCreate, + showArchive = false, + onShowArchiveChange, }: WorkspacesSidebarProps) { const { t } = useTranslation(['tasks', 'common']); const searchLower = searchQuery.toLowerCase(); @@ -48,8 +56,11 @@ export function WorkspacesSidebar({ .filter((workspace) => workspace.name.toLowerCase().includes(searchLower)) .slice(0, isSearching ? undefined : DISPLAY_LIMIT); + const hasArchivedWorkspaces = archivedWorkspaces.length > 0; + return (
+ {/* Header + Search */}
-
- - {draftTitle && ( - - )} - {filteredWorkspaces.map((workspace) => ( - onSelectWorkspace(workspace.id)} - /> - ))} - - - {filteredArchivedWorkspaces.map((workspace) => ( - onSelectWorkspace(workspace.id)} - /> - ))} - + + {/* Scrollable workspace list */} +
+ {showArchive ? ( + /* Archived workspaces view */ +
+ + {t('common:workspaces.archived')} + + {filteredArchivedWorkspaces.length === 0 ? ( + + {t('common:workspaces.noArchived')} + + ) : ( + filteredArchivedWorkspaces.map((workspace) => ( + onSelectWorkspace(workspace.id)} + onArchive={() => onArchiveWorkspace?.(workspace.id)} + onPin={() => onPinWorkspace?.(workspace.id)} + /> + )) + )} +
+ ) : ( + /* Active workspaces view */ +
+ + {t('common:workspaces.active')} + + {draftTitle && ( + + )} + {filteredWorkspaces.map((workspace) => ( + onSelectWorkspace(workspace.id)} + onArchive={() => onArchiveWorkspace?.(workspace.id)} + onPin={() => onPinWorkspace?.(workspace.id)} + /> + ))} +
+ )}
+ + {/* Fixed footer toggle - only show if there are archived workspaces */} + {hasArchivedWorkspaces && ( +
+ +
+ )}
); } diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index fbd18ad8..8714eae3 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -131,6 +131,13 @@ "loading": "Loading...", "selectToStart": "Select a workspace to get started", "draft": "Draft", + "viewArchive": "View Archive", + "backToActive": "Back to Active", + "noArchived": "No archived workspaces", + "pin": "Pin", + "unpin": "Unpin", + "archive": "Archive", + "more": "More actions", "rename": { "title": "Rename Workspace", "description": "Enter a new name for this workspace.", diff --git a/frontend/src/i18n/locales/es/common.json b/frontend/src/i18n/locales/es/common.json index 1c4abdba..cd9c8212 100644 --- a/frontend/src/i18n/locales/es/common.json +++ b/frontend/src/i18n/locales/es/common.json @@ -131,6 +131,13 @@ "loading": "Cargando...", "selectToStart": "Selecciona un espacio de trabajo para comenzar", "draft": "Borrador", + "viewArchive": "Ver archivo", + "backToActive": "Volver a activos", + "noArchived": "No hay espacios de trabajo archivados", + "pin": "Fijar", + "unpin": "Desfijar", + "archive": "Archivar", + "more": "Más acciones", "rename": { "title": "Renombrar espacio de trabajo", "description": "Ingresa un nuevo nombre para este espacio de trabajo.", diff --git a/frontend/src/i18n/locales/ja/common.json b/frontend/src/i18n/locales/ja/common.json index ae2e3208..a0352e6a 100644 --- a/frontend/src/i18n/locales/ja/common.json +++ b/frontend/src/i18n/locales/ja/common.json @@ -131,6 +131,13 @@ "loading": "読み込み中...", "selectToStart": "ワークスペースを選択して開始", "draft": "下書き", + "viewArchive": "アーカイブを表示", + "backToActive": "アクティブに戻る", + "noArchived": "アーカイブされたワークスペースはありません", + "pin": "ピン留め", + "unpin": "ピン留め解除", + "archive": "アーカイブ", + "more": "その他の操作", "rename": { "title": "ワークスペースの名前を変更", "description": "このワークスペースの新しい名前を入力してください。", diff --git a/frontend/src/i18n/locales/ko/common.json b/frontend/src/i18n/locales/ko/common.json index b065c90c..abb7a352 100644 --- a/frontend/src/i18n/locales/ko/common.json +++ b/frontend/src/i18n/locales/ko/common.json @@ -131,6 +131,13 @@ "loading": "로딩 중...", "selectToStart": "워크스페이스를 선택하여 시작", "draft": "초안", + "viewArchive": "보관함 보기", + "backToActive": "활성으로 돌아가기", + "noArchived": "보관된 워크스페이스 없음", + "pin": "고정", + "unpin": "고정 해제", + "archive": "보관", + "more": "더 많은 작업", "rename": { "title": "워크스페이스 이름 변경", "description": "이 워크스페이스의 새 이름을 입력하세요.", diff --git a/frontend/src/i18n/locales/zh-Hans/common.json b/frontend/src/i18n/locales/zh-Hans/common.json index 27685967..205039cd 100644 --- a/frontend/src/i18n/locales/zh-Hans/common.json +++ b/frontend/src/i18n/locales/zh-Hans/common.json @@ -131,6 +131,13 @@ "loading": "加载中...", "selectToStart": "选择一个工作区开始", "draft": "草稿", + "viewArchive": "查看归档", + "backToActive": "返回活跃", + "noArchived": "没有已归档的工作区", + "pin": "置顶", + "unpin": "取消置顶", + "archive": "归档", + "more": "更多操作", "rename": { "title": "重命名工作区", "description": "输入此工作区的新名称。", diff --git a/frontend/src/i18n/locales/zh-Hant/common.json b/frontend/src/i18n/locales/zh-Hant/common.json index 4d112638..e866d5a1 100644 --- a/frontend/src/i18n/locales/zh-Hant/common.json +++ b/frontend/src/i18n/locales/zh-Hant/common.json @@ -131,6 +131,13 @@ "loading": "載入中...", "selectToStart": "選擇一個工作區開始", "draft": "草稿", + "viewArchive": "檢視封存", + "backToActive": "返回活躍", + "noArchived": "沒有已封存的工作區", + "pin": "釘選", + "unpin": "取消釘選", + "archive": "封存", + "more": "更多操作", "rename": { "title": "重新命名工作區", "description": "輸入此工作區的新名稱。",