Skip to content

Commit 1647765

Browse files
feat(desktop): Add sidebar empty states and align content (#162)
* feat(desktop): add sidebar empty states * fix(desktop): align sidebar rail content * Avoid false no-results hints for pinned matches Co-authored-by: oratorio-integration[bot] <281657135+oratorio-integration[bot]@users.noreply.github.com> --------- Co-authored-by: oratorio-integration[bot] <281657135+oratorio-integration[bot]@users.noreply.github.com>
1 parent ab872a9 commit 1647765

10 files changed

Lines changed: 126 additions & 13 deletions

File tree

desktop/src/renderer/components/sidebar/ThreadList.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import { addToast } from '../../stores/toastStore'
4444
import { PinIcon, ThreadEntry } from './ThreadEntry'
4545
import { WorkspaceOptionsMenu } from './WorkspaceHeader'
4646
import { useAddProjectFlow } from '../projects/AddProject'
47-
import { SIDEBAR_ROW_MIN_HEIGHT } from './sidebarNavRowStyles'
47+
import { SIDEBAR_RAIL_CONTENT_INSET, SIDEBAR_ROW_MIN_HEIGHT } from './sidebarNavRowStyles'
4848
import {
4949
isRemoteProjectKey,
5050
normalizeWorkspaceProjectKey,
@@ -265,8 +265,12 @@ export function ThreadList({
265265
<ProjectThreadSkeletonList />
266266
) : (
267267
<>
268-
{projectThreads.length === 0 && project.loaded && searchQuery && (
269-
<ProjectHint label={t('threadList.noSearchResults')} />
268+
{project.loaded && rawProjectThreads.length === 0 && (
269+
<ProjectHint
270+
label={searchQuery
271+
? t('threadList.noSearchResults')
272+
: t('projectsRail.noChats')}
273+
/>
270274
)}
271275
{projectThreads.map((thread) => (
272276
isForeground ? (
@@ -314,6 +318,9 @@ export function ThreadList({
314318
)}
315319
{showProjects && (
316320
<CollapsibleThreads collapsed={projectsSectionCollapsed} marginTop={0}>
321+
{projectsForRender.length === 0 && (
322+
<ProjectHint label={t('projectsRail.noProjects')} alignment="section" />
323+
)}
317324
{ordinaryProjects.map(renderProjectBlock)}
318325
</CollapsibleThreads>
319326
)}
@@ -1694,10 +1701,11 @@ function ProjectHint({
16941701
<div
16951702
style={{
16961703
padding: alignment === 'section'
1697-
? `4px ${SIDEBAR_SECTION_INSET} 8px`
1704+
? `4px ${SIDEBAR_RAIL_CONTENT_INSET} 8px`
16981705
: '4px 16px 8px 32px',
16991706
color: 'var(--text-dimmed)',
17001707
fontSize: 'var(--type-secondary-size)',
1708+
fontWeight: 400,
17011709
lineHeight: 'var(--type-secondary-line-height)'
17021710
}}
17031711
>
@@ -2109,14 +2117,12 @@ function projectFolderPaths(project: WorkspaceProjectSummary): string[] {
21092117
})
21102118
}
21112119

2112-
const SIDEBAR_SECTION_INSET = '8px'
2113-
21142120
const sidebarSectionHeaderStyle: CSSProperties = {
21152121
display: 'flex',
21162122
alignItems: 'center',
21172123
gap: '4px',
21182124
minHeight: '28px',
2119-
padding: `8px ${SIDEBAR_SECTION_INSET} 2px`,
2125+
padding: `8px ${SIDEBAR_RAIL_CONTENT_INSET} 2px`,
21202126
cursor: 'pointer',
21212127
userSelect: 'none'
21222128
}

desktop/src/renderer/components/sidebar/sidebarNavRowStyles.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import type { CSSProperties } from 'react'
77
*/
88
export const SIDEBAR_ROW_MIN_HEIGHT = '30px'
99

10+
/**
11+
* Shared horizontal origin for expanded-sidebar section labels, project icon
12+
* slots, and section-level empty states. Full-width rows reach this origin via
13+
* their 4px outer inset plus 12px inner padding.
14+
*/
15+
export const SIDEBAR_RAIL_CONTENT_INSET = '16px'
16+
1017
/** Shared geometry for New chat, Search, Automations, Skills, Settings, and project header rows. */
1118
export const SIDEBAR_NAV_ROW_OUTER: CSSProperties = {
1219
width: 'calc(100% - 8px)',

desktop/src/renderer/tests/ThreadListProjects.test.tsx

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ describe('ThreadList project-first layout', () => {
381381

382382
it('keeps cold projects collapsed and starts them only on double click', () => {
383383
useWorkspaceProjectsStore.getState().setPayload({
384-
foregroundWorkspacePath: '/workspace/a',
384+
foregroundWorkspacePath: '',
385385
secondaryLimit: 8,
386386
projects: [
387387
{
@@ -413,7 +413,7 @@ describe('ThreadList project-first layout', () => {
413413
expect(workspaceSwitch).toHaveBeenCalledWith('/workspace/cold')
414414
})
415415

416-
it('does not repeat empty chat copy for loaded empty projects', () => {
416+
it('shows a thread-aligned empty state for each loaded empty project', () => {
417417
useWorkspaceProjectsStore.getState().setPayload({
418418
foregroundWorkspacePath: '/workspace/a',
419419
secondaryLimit: 8,
@@ -445,6 +445,35 @@ describe('ThreadList project-first layout', () => {
445445

446446
expect(screen.getByRole('button', { name: 'a' })).toHaveAttribute('aria-expanded', 'true')
447447
expect(screen.getByRole('button', { name: 'b' })).toHaveAttribute('aria-expanded', 'true')
448+
const projectEmptyStates = screen.getAllByText('No chats')
449+
expect(projectEmptyStates).toHaveLength(2)
450+
for (const emptyState of projectEmptyStates) {
451+
expect(emptyState).toHaveStyle({ padding: '4px 16px 8px 32px' })
452+
}
453+
})
454+
455+
it('shows search feedback instead of the ordinary project empty state', () => {
456+
useThreadStore.getState().setSearchQuery('missing')
457+
useWorkspaceProjectsStore.getState().setPayload({
458+
foregroundWorkspacePath: '/workspace/a',
459+
secondaryLimit: 8,
460+
projects: [
461+
{
462+
path: '/workspace/a',
463+
name: 'a',
464+
state: 'foreground',
465+
running: true,
466+
loaded: true,
467+
threadCount: 1,
468+
threads: [makeThread('thread-a', 'Existing thread')],
469+
pinnedThreadIds: []
470+
}
471+
]
472+
})
473+
474+
renderList()
475+
476+
expect(screen.getByText('No threads match your search.')).toBeInTheDocument()
448477
expect(screen.queryByText('No chats')).not.toBeInTheDocument()
449478
})
450479

@@ -955,6 +984,66 @@ describe('ThreadList project-first layout', () => {
955984
})
956985
})
957986

987+
it('shows section-aligned empty states when there are no configured projects or recent chats', () => {
988+
useWorkspaceProjectsStore.getState().setPayload({
989+
foregroundWorkspacePath: '/chats',
990+
foregroundProjectId: '/chats',
991+
secondaryLimit: 8,
992+
projects: [],
993+
chat: {
994+
projectId: '/chats',
995+
kind: 'chat',
996+
path: '/chats',
997+
name: '/chats',
998+
state: 'foreground',
999+
running: true,
1000+
loaded: true,
1001+
threadCount: 0,
1002+
threads: [],
1003+
pinnedThreadIds: []
1004+
}
1005+
})
1006+
1007+
renderList({ workspacePath: '/chats' })
1008+
1009+
expect(screen.getByText('Projects')).toBeInTheDocument()
1010+
expect(screen.getByRole('button', { name: 'Toggle Projects section' }))
1011+
.toHaveStyle({ padding: '8px 16px 2px' })
1012+
expect(screen.getByText('No projects')).toHaveStyle({ padding: '4px 16px 8px' })
1013+
expect(screen.getByText('Recents')).toBeInTheDocument()
1014+
expect(screen.getByRole('button', { name: 'Toggle Recents section' }))
1015+
.toHaveStyle({ padding: '8px 16px 2px' })
1016+
expect(screen.getByText('No chats')).toHaveStyle({ padding: '4px 16px 8px' })
1017+
})
1018+
1019+
it('does not report No projects when every configured project is pinned', () => {
1020+
useWorkspaceProjectsStore.getState().setPayload({
1021+
foregroundWorkspacePath: '/workspace/a',
1022+
foregroundProjectId: '/workspace/a',
1023+
secondaryLimit: 8,
1024+
projects: [
1025+
{
1026+
path: '/workspace/a',
1027+
name: 'Pinned project',
1028+
state: 'foreground',
1029+
running: true,
1030+
loaded: true,
1031+
pinned: true,
1032+
threadCount: 0,
1033+
threads: [],
1034+
pinnedThreadIds: []
1035+
}
1036+
]
1037+
})
1038+
1039+
renderList()
1040+
1041+
expect(screen.getByText('Pinned')).toBeInTheDocument()
1042+
expect(screen.getByRole('button', { name: 'Pinned project' })).toBeInTheDocument()
1043+
expect(screen.getByText('Projects')).toBeInTheDocument()
1044+
expect(screen.queryByText('No projects')).not.toBeInTheDocument()
1045+
})
1046+
9581047
it('renders a Recents group with default chat workspace threads after Projects', () => {
9591048
useWorkspaceProjectsStore.getState().setPayload({
9601049
foregroundWorkspacePath: '/workspace/a',
@@ -991,8 +1080,8 @@ describe('ThreadList project-first layout', () => {
9911080

9921081
const projectsHeading = screen.getByText('Projects')
9931082
const recentsHeading = screen.getByText('Recents')
994-
expect(projectsHeading.parentElement).toHaveStyle({ padding: '8px 8px 2px' })
995-
expect(recentsHeading.parentElement).toHaveStyle({ padding: '8px 8px 2px' })
1083+
expect(projectsHeading.parentElement).toHaveStyle({ padding: '8px 16px 2px' })
1084+
expect(recentsHeading.parentElement).toHaveStyle({ padding: '8px 16px 2px' })
9961085
// Recents renders as its own group, after Projects.
9971086
expect(projectsHeading.compareDocumentPosition(recentsHeading) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
9981087
expect(screen.getByText('General chat thread')).toBeInTheDocument()
@@ -1033,8 +1122,12 @@ describe('ThreadList project-first layout', () => {
10331122

10341123
renderList()
10351124

1036-
expect(screen.getByText('Recents')).toBeInTheDocument()
1037-
expect(screen.getByText('No chats')).toHaveStyle({ padding: '4px 8px 8px' })
1125+
const recentsHeading = screen.getByText('Recents')
1126+
const recentsGroup = recentsHeading.parentElement?.parentElement
1127+
expect(recentsGroup).not.toBeNull()
1128+
expect(within(recentsGroup as HTMLElement).getByText('No chats')).toHaveStyle({
1129+
padding: '4px 16px 8px'
1130+
})
10381131
})
10391132

10401133
it('shows mutually exclusive waiting and running counts in project details', async () => {

desktop/src/shared/locales/messages/de.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src/shared/locales/messages/en.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src/shared/locales/messages/es.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src/shared/locales/messages/fr.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src/shared/locales/messages/ja.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src/shared/locales/messages/ko.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src/shared/locales/messages/zh-Hans.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)