Skip to content

Commit ca0df02

Browse files
Refactor
1 parent ab34551 commit ca0df02

5 files changed

Lines changed: 105 additions & 288 deletions

File tree

‎e2e/dashboard.spec.ts‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,22 @@ function buildDashboardInteractionSessionDetailResponse(sessionId?: string) {
307307
filesModified: [] as string[],
308308
checkpointId: null,
309309
toolUses: [] as Array<Record<string, unknown>>,
310+
transcriptEntries: [
311+
{
312+
entryId: 'stub-turn-01-e0',
313+
sessionId: 'sess-01',
314+
turnId: 'turn-01',
315+
order: 0,
316+
timestamp: '2025-02-14T10:12:00.000Z',
317+
actor: 'USER',
318+
variant: 'CHAT',
319+
source: 'TRANSCRIPT',
320+
text: FIRST_SESSION_TURN_ONE_PROMPT,
321+
toolUseId: null,
322+
toolKind: null,
323+
isError: false,
324+
},
325+
],
310326
},
311327
{
312328
turnId: 'turn-02',
@@ -323,6 +339,22 @@ function buildDashboardInteractionSessionDetailResponse(sessionId?: string) {
323339
filesModified: [] as string[],
324340
checkpointId: null,
325341
toolUses: [] as Array<Record<string, unknown>>,
342+
transcriptEntries: [
343+
{
344+
entryId: 'stub-turn-02-e0',
345+
sessionId: 'sess-01',
346+
turnId: 'turn-02',
347+
order: 0,
348+
timestamp: '2025-02-14T10:28:00.000Z',
349+
actor: 'USER',
350+
variant: 'CHAT',
351+
source: 'TRANSCRIPT',
352+
text: FIRST_SESSION_TURN_TWO_PROMPT,
353+
toolUseId: null,
354+
toolKind: null,
355+
isError: false,
356+
},
357+
],
326358
},
327359
]
328360
: ([] as Array<Record<string, unknown>>)
@@ -333,6 +365,7 @@ function buildDashboardInteractionSessionDetailResponse(sessionId?: string) {
333365
summary,
334366
turns,
335367
rawEvents: [] as Array<Record<string, unknown>>,
368+
sessionTranscriptEntries: [] as Array<Record<string, unknown>>,
336369
}
337370
: null,
338371
},
@@ -1051,7 +1084,7 @@ test.describe('Session detail', () => {
10511084
await expect(page.getByText(FIRST_SESSION_PROMPT).last()).toBeVisible()
10521085
})
10531086

1054-
test('sidebar turns tab falls back to turn prompts when transcript fragments are unavailable', async ({
1087+
test('sidebar turns tab renders canonical transcript entries per turn', async ({
10551088
page,
10561089
}) => {
10571090
await stubApiRoutes(page)

‎src/features/dashboard/components/session-detail-sidebar.tsx‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,17 @@ function SessionSummaryView({
112112
<Separator />
113113
<div>
114114
<h3 className='mb-2 text-sm font-semibold'>Token Usage</h3>
115-
{summary.token_usage ? (
115+
{/*
116+
Cursor does not expose reliable per-session token counts through its
117+
transcript or hooks, so any numbers we would otherwise show are
118+
incomplete. Treat Cursor sessions as having no token data regardless
119+
of what's stored.
120+
*/}
121+
{summary.agent_type === 'cursor' ? (
122+
<p className='text-sm text-muted-foreground'>
123+
No token information available.
124+
</p>
125+
) : summary.token_usage ? (
116126
<Suspense
117127
fallback={
118128
<div className='h-40 animate-pulse rounded-md bg-muted/30' />
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { DashboardTranscriptEntryDto } from '@/features/dashboard/api-types'
2+
import { describe, expect, it } from 'vitest'
3+
import { transcriptEntriesToMessages } from './transcript-entries'
4+
5+
function makeEntry(
6+
overrides: Partial<DashboardTranscriptEntryDto> & {
7+
entry_id: string
8+
order: number
9+
text: string
10+
},
11+
): DashboardTranscriptEntryDto {
12+
return {
13+
session_id: 's1',
14+
turn_id: null,
15+
timestamp: null,
16+
actor: 'USER',
17+
variant: 'CHAT',
18+
source: 'TRANSCRIPT',
19+
tool_use_id: null,
20+
tool_kind: null,
21+
is_error: false,
22+
...overrides,
23+
}
24+
}
25+
26+
describe('transcriptEntriesToMessages', () => {
27+
it('sorts by order then entry_id when the API returns rows out of sequence', () => {
28+
const messages = transcriptEntriesToMessages([
29+
makeEntry({ entry_id: 'b', order: 1, text: 'second' }),
30+
makeEntry({ entry_id: 'a', order: 0, text: 'first' }),
31+
makeEntry({ entry_id: 'c', order: 2, text: 'third' }),
32+
])
33+
expect(messages.map((m) => m.text)).toEqual(['first', 'second', 'third'])
34+
})
35+
36+
it('uses entry_id as a stable tie-breaker when order matches', () => {
37+
const messages = transcriptEntriesToMessages([
38+
makeEntry({ entry_id: 'z-same', order: 0, text: 'z' }),
39+
makeEntry({ entry_id: 'a-same', order: 0, text: 'a' }),
40+
])
41+
expect(messages.map((m) => m.text)).toEqual(['a', 'z'])
42+
})
43+
})

‎src/features/dashboard/utils/transcript-entries.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,21 @@ export function toTranscriptMessage(
5555
}
5656
}
5757

58+
function compareTranscriptEntriesByStreamOrder(
59+
a: DashboardTranscriptEntryDto,
60+
b: DashboardTranscriptEntryDto,
61+
): number {
62+
if (a.order !== b.order) {
63+
return a.order - b.order
64+
}
65+
return a.entry_id.localeCompare(b.entry_id)
66+
}
67+
5868
/**
59-
* Convert a list of canonical entries to legacy messages, preserving order.
69+
* Convert a list of canonical entries to legacy messages.
70+
*
71+
* Entries are sorted by ascending `order`, then `entry_id`, so rendering stays
72+
* correct if the API returns rows out of sequence.
6073
*
6174
* Returns an empty array for empty input so callers can use length-based
6275
* branching for dual-read:
@@ -70,7 +83,9 @@ export function toTranscriptMessage(
7083
export function transcriptEntriesToMessages(
7184
entries: ReadonlyArray<DashboardTranscriptEntryDto>,
7285
): TranscriptMessage[] {
73-
return entries.map(toTranscriptMessage)
86+
return [...entries]
87+
.sort(compareTranscriptEntriesByStreamOrder)
88+
.map(toTranscriptMessage)
7489
}
7590

7691
/**

0 commit comments

Comments
 (0)