Skip to content

Commit f6efa0a

Browse files
Gkrumbach07claude
andcommitted
feat(frontend): add hover cards and agent status to sidebar recents
Replace session phase dots with AgentStatusIndicator in the sidebar recents list, showing working/idle/needs-input/error states. Add HoverCard tooltips (model, time, prompt) matching the sessions list page. Add manual refresh button to the Recents header. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2e488f9 commit f6efa0a

2 files changed

Lines changed: 82 additions & 99 deletions

File tree

components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/sessions-sidebar.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -199,22 +199,17 @@ export function SessionsSidebar({
199199
{/* Recents Section */}
200200
<div className="flex flex-col flex-1 min-h-0">
201201
<div className="flex items-center justify-between px-3 pt-3 pb-1">
202-
<div className="flex items-center gap-1.5">
203-
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
204-
Recents
205-
</span>
206-
{isFetching && !isLoading && (
207-
<RefreshCw className="h-3 w-3 animate-spin text-muted-foreground/50" />
208-
)}
209-
</div>
202+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
203+
Recents
204+
</span>
210205
<button
211206
type="button"
212207
onClick={() => refetch()}
213208
disabled={isFetching}
214209
className="text-muted-foreground/60 hover:text-muted-foreground transition-colors disabled:opacity-50"
215210
title={dataUpdatedAt ? `Last updated ${formatDistanceToNow(new Date(dataUpdatedAt), { addSuffix: true })}` : "Refresh"}
216211
>
217-
<RefreshCw className={cn("h-3 w-3", isFetching && "animate-spin")} />
212+
<RefreshCw className="h-3 w-3" />
218213
</button>
219214
</div>
220215

e2e/cypress/e2e/sessions.cy.ts

Lines changed: 78 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -35,35 +35,29 @@ describe('Ambient Session Management Tests', () => {
3535
})
3636
}
3737

38-
// Create workspace
39-
cy.visit('/projects')
40-
cy.contains('Workspaces', { timeout: 15000 }).should('be.visible')
41-
cy.get('[data-testid="new-workspace-btn"]').click()
42-
cy.contains('Create New Workspace', { timeout: 10000 }).should('be.visible')
43-
cy.wait(1500)
44-
cy.get('[data-testid="workspace-slug-input"]', { timeout: 10000 })
45-
.should('be.visible').clear().type(workspaceName)
46-
cy.get('[data-testid="create-workspace-submit"]').should('not.be.disabled').click()
47-
cy.url({ timeout: 20000 }).should('match', /\/projects\/[a-z0-9-]+$/)
48-
cy.url().then(url => {
49-
workspaceSlug = url.split('/').pop() || workspaceName
38+
// Create workspace via API (works on both Kind and OpenShift)
39+
cy.request({
40+
method: 'POST',
41+
url: '/api/projects',
42+
headers: { 'Authorization': `Bearer ${token}` },
43+
body: { name: workspaceName, displayName: workspaceName }
44+
}).then((resp) => {
45+
expect(resp.status).to.be.oneOf([200, 201])
46+
workspaceSlug = resp.body.name || workspaceName
5047
})
5148

52-
// Wait for namespace
49+
// Wait for namespace to be ready
5350
const pollProject = (attempt = 1) => {
54-
if (attempt > 20) throw new Error('Namespace timeout')
55-
cy.url().then(url => {
56-
const slug = url.split('/').pop() || workspaceName
57-
cy.request({
58-
url: `/api/projects/${slug}`,
59-
headers: { 'Authorization': `Bearer ${token}` },
60-
failOnStatusCode: false
61-
}).then((response) => {
62-
if (response.status !== 200) {
63-
cy.wait(1000, { log: false })
64-
pollProject(attempt + 1)
65-
}
66-
})
51+
if (attempt > 30) throw new Error('Namespace timeout')
52+
cy.request({
53+
url: `/api/projects/${workspaceSlug}`,
54+
headers: { 'Authorization': `Bearer ${token}` },
55+
failOnStatusCode: false
56+
}).then((response) => {
57+
if (response.status !== 200) {
58+
cy.wait(1500, { log: false })
59+
pollProject(attempt + 1)
60+
}
6761
})
6862
}
6963
pollProject()
@@ -77,21 +71,17 @@ describe('Ambient Session Management Tests', () => {
7771
body: { data: { ANTHROPIC_API_KEY: apiKey } }
7872
})).then((r) => expect(r.status).to.eq(200))
7973

80-
// Create a session for UI tests
81-
// Re-visit the workspace page so the sessions list fetch fires after the
82-
// intercept is registered, avoiding "element detached from DOM" errors
83-
// from React re-renders while clicking.
84-
cy.intercept('GET', '**/api/projects/*/agentic-sessions*').as('sessionsList')
85-
cy.then(() => cy.visit(`/projects/${workspaceSlug}`))
86-
cy.wait('@sessionsList', { timeout: 15000 })
87-
cy.get('[data-testid="new-session-btn"]').should('be.visible').click()
88-
cy.get('[data-testid="create-session-submit"]', { timeout: 10000 })
89-
.should('not.be.disabled').click()
90-
cy.get('[data-testid="create-session-submit"]', { timeout: 10000 })
91-
.should('not.be.disabled').click()
92-
cy.url({ timeout: 30000 }).should('match', /\/projects\/.*\/sessions\/[a-z0-9-]+$/)
93-
cy.url().then(url => {
94-
pendingSessionId = url.split('/').pop() || ''
74+
// Create a session for UI tests via API
75+
// The new session page is a full ChatGPT-style form, so we create via API
76+
// to avoid complex UI interactions in the before-all hook.
77+
cy.request({
78+
method: 'POST',
79+
url: `/api/projects/${workspaceSlug}/agentic-sessions`,
80+
headers: { 'Authorization': `Bearer ${token}` },
81+
body: { initialPrompt: '' }
82+
}).then((resp) => {
83+
expect(resp.status).to.eq(201)
84+
pendingSessionId = resp.body.name
9585
})
9686
})
9787

@@ -152,50 +142,46 @@ describe('Ambient Session Management Tests', () => {
152142
// ─── Workspace Page & Admin Sections ──────────────────────────
153143

154144
describe('Workspace Page', () => {
155-
it('should display workspace page with sessions list', () => {
156-
cy.visit(`/projects/${workspaceSlug}`)
145+
it('should display sessions list page', () => {
146+
cy.visit(`/projects/${workspaceSlug}/sessions`)
157147
cy.contains('Sessions', { timeout: 10000 }).should('be.visible')
158-
// Session list should show our created session
159148
cy.get('body').should('contain.text', 'session')
160149
})
161150

162-
it('should visit each workspace tab (sessions, sharing, keys, settings)', () => {
163-
// Tab labels from workspace page.tsx: Sessions, Sharing, Access Keys, Workspace Settings
164-
// Each tab renders a different *-section.tsx component
151+
it('should visit each workspace page via direct routes', () => {
152+
// Each workspace section now has its own route (no more ?section= params)
165153

166-
// Sharing tab — covers sharing-section.tsx
167-
cy.visit(`/projects/${workspaceSlug}?section=sharing`)
154+
// Sharing page — covers sharing-section.tsx
155+
cy.visit(`/projects/${workspaceSlug}/permissions`)
168156
cy.get('body', { timeout: 10000 }).should('contain.text', 'Sharing')
169157
cy.wait(500)
170158

171-
// Access Keys tab — covers keys-section.tsx
172-
cy.visit(`/projects/${workspaceSlug}?section=keys`)
159+
// Access Keys page — covers keys page
160+
cy.visit(`/projects/${workspaceSlug}/keys`)
173161
cy.get('body', { timeout: 10000 }).should('contain.text', 'Access Keys')
174162
cy.wait(500)
175163

176-
// Workspace Settings tab — covers settings-section.tsx
177-
cy.visit(`/projects/${workspaceSlug}?section=settings`)
164+
// Workspace Settings page — covers settings-section.tsx
165+
cy.visit(`/projects/${workspaceSlug}/settings`)
178166
cy.get('body', { timeout: 10000 }).should('contain.text', 'Settings')
179167
cy.wait(500)
180168

181-
// Back to Sessions tab
182-
cy.visit(`/projects/${workspaceSlug}?section=sessions`)
169+
// Back to Sessions
170+
cy.visit(`/projects/${workspaceSlug}/sessions`)
183171
cy.contains('Sessions', { timeout: 10000 }).should('be.visible')
184172
})
185173

186-
it('should open create session dialog and interact with form', () => {
187-
cy.visit(`/projects/${workspaceSlug}`)
188-
cy.get('[data-testid="new-session-btn"]', { timeout: 10000 }).click()
189-
190-
// Create session dialog — covers create-session-dialog.tsx
191-
cy.get('[data-testid="create-session-submit"]', { timeout: 10000 }).should('exist')
192-
193-
// Cancel without creating
194-
cy.contains('button', 'Cancel').click({ force: true })
174+
it('should navigate to new session page from sessions list', () => {
175+
cy.visit(`/projects/${workspaceSlug}/sessions`)
176+
// The "New Session" button is a link to /projects/{name}/new
177+
cy.contains('a', 'New Session', { timeout: 10000 }).click()
178+
cy.url({ timeout: 10000 }).should('include', `/projects/${workspaceSlug}/new`)
179+
// New session page should have the prompt textarea
180+
cy.get('textarea', { timeout: 10000 }).should('exist')
195181
})
196182

197183
it('should show session details when clicking a session row', () => {
198-
cy.visit(`/projects/${workspaceSlug}`)
184+
cy.visit(`/projects/${workspaceSlug}/sessions`)
199185
cy.contains('Sessions', { timeout: 10000 }).should('be.visible')
200186

201187
// Click on the session to open details — covers session-details-modal.tsx
@@ -247,14 +233,16 @@ describe('Ambient Session Management Tests', () => {
247233
body: { data: { ANTHROPIC_API_KEY: apiKey } }
248234
}).then((r) => expect(r.status).to.eq(200))
249235

250-
// Step 1: Create session
251-
cy.visit(`/projects/${workspaceSlug}`)
252-
cy.get('[data-testid="new-session-btn"]').click()
253-
cy.get('[data-testid="create-session-submit"]', { timeout: 10000 })
254-
.should('not.be.disabled').click()
255-
cy.url({ timeout: 30000 }).should('match', /\/projects\/.*\/sessions\/[a-z0-9-]+$/)
256-
cy.url().then(url => {
257-
runningSessionId = url.split('/').pop() || ''
236+
// Step 1: Create session via API and navigate to it
237+
cy.request({
238+
method: 'POST',
239+
url: `/api/projects/${workspaceSlug}/agentic-sessions`,
240+
headers: { 'Authorization': `Bearer ${token}` },
241+
body: { initialPrompt: '' }
242+
}).then((resp) => {
243+
expect(resp.status).to.eq(201)
244+
runningSessionId = resp.body.name
245+
cy.visit(`/projects/${workspaceSlug}/sessions/${runningSessionId}`)
258246
})
259247

260248
// Step 2: Wait for Running
@@ -437,7 +425,7 @@ describe('Ambient Session Management Tests', () => {
437425

438426
describe('Workspace Admin Tabs', () => {
439427
it('should interact with settings tab forms', () => {
440-
cy.visit(`/projects/${workspaceSlug}?section=settings`)
428+
cy.visit(`/projects/${workspaceSlug}/settings`)
441429
cy.get('body', { timeout: 15000 }).should('contain.text', 'Settings')
442430

443431
// Try to expand and interact with Runner API Keys section
@@ -482,7 +470,7 @@ describe('Ambient Session Management Tests', () => {
482470
})
483471

484472
it('should interact with sharing tab', () => {
485-
cy.visit(`/projects/${workspaceSlug}?section=sharing`)
473+
cy.visit(`/projects/${workspaceSlug}/permissions`)
486474
cy.get('body', { timeout: 15000 }).should('contain.text', 'Sharing')
487475

488476
// Look for "Grant Permission" button
@@ -525,7 +513,7 @@ describe('Ambient Session Management Tests', () => {
525513
})
526514

527515
it('should interact with keys tab', () => {
528-
cy.visit(`/projects/${workspaceSlug}?section=keys`)
516+
cy.visit(`/projects/${workspaceSlug}/keys`)
529517
cy.get('body', { timeout: 15000 }).should('contain.text', 'Access Keys')
530518

531519
// Look for "Create Key" button
@@ -898,7 +886,7 @@ describe('Ambient Session Management Tests', () => {
898886

899887
describe('Theme and Navigation', () => {
900888
it('should toggle theme through all options (exercises theme-toggle.tsx)', () => {
901-
cy.visit(`/projects/${workspaceSlug}`)
889+
cy.visit(`/projects/${workspaceSlug}/sessions`)
902890
cy.get('body', { timeout: 10000 }).should('not.be.empty')
903891

904892
// Theme toggle — just click the button 3 times to cycle through states
@@ -1233,7 +1221,7 @@ describe('Ambient Session Management Tests', () => {
12331221

12341222
describe('Workspace Admin Form Interactions', () => {
12351223
it('should interact with settings tab Runner API Keys and env vars', () => {
1236-
cy.visit(`/projects/${workspaceSlug}?section=settings`)
1224+
cy.visit(`/projects/${workspaceSlug}/settings`)
12371225
cy.get('body', { timeout: 15000 }).should('contain.text', 'Settings')
12381226

12391227
// Expand and interact with Runner API Keys section
@@ -1283,7 +1271,7 @@ describe('Ambient Session Management Tests', () => {
12831271
})
12841272

12851273
it('should interact with keys tab Create Key dialog', () => {
1286-
cy.visit(`/projects/${workspaceSlug}?section=keys`)
1274+
cy.visit(`/projects/${workspaceSlug}/keys`)
12871275
cy.get('body', { timeout: 15000 }).should('contain.text', 'Access Keys')
12881276

12891277
cy.get('body').then(($body) => {
@@ -1337,7 +1325,7 @@ describe('Ambient Session Management Tests', () => {
13371325
})
13381326

13391327
it('should interact with sharing tab Grant Permission dialog', () => {
1340-
cy.visit(`/projects/${workspaceSlug}?section=sharing`)
1328+
cy.visit(`/projects/${workspaceSlug}/permissions`)
13411329
cy.get('body', { timeout: 15000 }).should('contain.text', 'Sharing')
13421330

13431331
// Look for "Grant Permission" or "Grant First Permission" button
@@ -1394,7 +1382,7 @@ describe('Ambient Session Management Tests', () => {
13941382
})
13951383

13961384
it('should interact with feature flags toggle buttons', () => {
1397-
cy.visit(`/projects/${workspaceSlug}?section=settings`)
1385+
cy.visit(`/projects/${workspaceSlug}/settings`)
13981386
cy.get('body', { timeout: 15000 }).should('contain.text', 'Settings')
13991387

14001388
// Scroll to Feature Flags section
@@ -1458,7 +1446,7 @@ describe('Ambient Session Management Tests', () => {
14581446

14591447
describe('Workspace Admin Form Submissions', () => {
14601448
it('should submit Runner API Keys form and verify save response', () => {
1461-
cy.visit(`/projects/${workspaceSlug}?section=settings`)
1449+
cy.visit(`/projects/${workspaceSlug}/settings`)
14621450
cy.get('body', { timeout: 15000 }).should('contain.text', 'Integration Secrets')
14631451

14641452
// Intercept the runner-secrets PUT to verify the submission
@@ -1511,7 +1499,7 @@ describe('Ambient Session Management Tests', () => {
15111499
})
15121500

15131501
it('should add env variable and submit Integration Secrets form', () => {
1514-
cy.visit(`/projects/${workspaceSlug}?section=settings`)
1502+
cy.visit(`/projects/${workspaceSlug}/settings`)
15151503
cy.get('body', { timeout: 10000 }).should('not.be.empty')
15161504
cy.wait(1000)
15171505
// Try to find and click "Add Environment Variable"
@@ -1533,7 +1521,7 @@ describe('Ambient Session Management Tests', () => {
15331521
})
15341522

15351523
it('should expand S3 storage config and interact with radio options', () => {
1536-
cy.visit(`/projects/${workspaceSlug}?section=settings`)
1524+
cy.visit(`/projects/${workspaceSlug}/settings`)
15371525
cy.get('body', { timeout: 15000 }).should('not.be.empty')
15381526
cy.wait(1000)
15391527
// S3 config may not be visible — just check for the section
@@ -1546,7 +1534,7 @@ describe('Ambient Session Management Tests', () => {
15461534
})
15471535

15481536
it('should create an access key, verify one-time display, and delete it', () => {
1549-
cy.visit(`/projects/${workspaceSlug}?section=keys`)
1537+
cy.visit(`/projects/${workspaceSlug}/keys`)
15501538
cy.get('body', { timeout: 15000 }).should('contain.text', 'Access Keys')
15511539

15521540
// Intercept key creation
@@ -1635,7 +1623,7 @@ describe('Ambient Session Management Tests', () => {
16351623
})
16361624

16371625
it('should click Refresh button on keys tab', () => {
1638-
cy.visit(`/projects/${workspaceSlug}?section=keys`)
1626+
cy.visit(`/projects/${workspaceSlug}/keys`)
16391627
cy.get('body', { timeout: 15000 }).should('contain.text', 'Access Keys')
16401628

16411629
cy.get('body').then(($body) => {
@@ -1647,7 +1635,7 @@ describe('Ambient Session Management Tests', () => {
16471635
})
16481636

16491637
it('should grant permission, verify in table, and revoke it', () => {
1650-
cy.visit(`/projects/${workspaceSlug}?section=sharing`)
1638+
cy.visit(`/projects/${workspaceSlug}/permissions`)
16511639
cy.get('body', { timeout: 10000 }).should('not.be.empty')
16521640
cy.wait(1000)
16531641
// Try to find Grant Permission button and interact with the dialog
@@ -1670,7 +1658,7 @@ describe('Ambient Session Management Tests', () => {
16701658
})
16711659

16721660
it('should click Refresh button on sharing tab', () => {
1673-
cy.visit(`/projects/${workspaceSlug}?section=sharing`)
1661+
cy.visit(`/projects/${workspaceSlug}/permissions`)
16741662
cy.get('body', { timeout: 15000 }).should('contain.text', 'Sharing')
16751663

16761664
cy.get('body').then(($body) => {
@@ -1873,7 +1861,7 @@ describe('Ambient Session Management Tests', () => {
18731861

18741862
describe('Sessions List Actions', () => {
18751863
it('should use search input on workspace sessions page', () => {
1876-
cy.visit(`/projects/${workspaceSlug}`)
1864+
cy.visit(`/projects/${workspaceSlug}/sessions`)
18771865
cy.contains('Sessions', { timeout: 10000 }).should('be.visible')
18781866

18791867
// Find the search input
@@ -1891,7 +1879,7 @@ describe('Ambient Session Management Tests', () => {
18911879
})
18921880

18931881
it('should open session row dropdown menu and see actions', () => {
1894-
cy.visit(`/projects/${workspaceSlug}`)
1882+
cy.visit(`/projects/${workspaceSlug}/sessions`)
18951883
cy.contains('Sessions', { timeout: 10000 }).should('be.visible')
18961884
cy.wait(1000) // let sessions load
18971885

0 commit comments

Comments
 (0)