fix(client): refine trajectory inspection behavior
This commit is contained in:
@@ -740,7 +740,10 @@
|
||||
}
|
||||
|
||||
.detailBodySummary > .overview {
|
||||
flex: none;
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.detailBodySummary > .compactedSummary {
|
||||
@@ -858,7 +861,7 @@
|
||||
.overviewSections {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-height: min-content;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-top: 0;
|
||||
@@ -866,17 +869,13 @@
|
||||
|
||||
.overviewSection {
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
flex: 1 1 0;
|
||||
max-height: max-content;
|
||||
min-height: 28px;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overviewSection:has(> .overviewPreview > .overview),
|
||||
.overviewSection:has(> .overviewPreview > .noPayload) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.overviewSection + .overviewSection {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
@@ -225,6 +225,8 @@ export interface TrajectoryTableProps {
|
||||
onSelectedIndexChange?: (index: number | null) => void
|
||||
/** Report a direct user selection from a ledger row. */
|
||||
onRecordSelect?: (index: number) => void
|
||||
/** Clear selection state owned by the ledger host. */
|
||||
onClearSelection?: () => void
|
||||
/** Turn ids whose rows after the first are folded into a summary. */
|
||||
collapsedTurns: ReadonlySet<number>
|
||||
/** Toggle one turn between folded and expanded. */
|
||||
@@ -1384,6 +1386,7 @@ function OverviewSection({
|
||||
|
||||
/**
|
||||
* Render trajectory events as a dense ledger with turn and step separators.
|
||||
* Clicking ledger whitespace clears the active record or request selection.
|
||||
* @param props - Grouped trajectory data and whole-ledger fold state.
|
||||
* @returns The ledger and an optional local record inspector.
|
||||
*/
|
||||
@@ -1394,6 +1397,7 @@ export function TrajectoryTable({
|
||||
searchMatchIndexes = null,
|
||||
onSelectedIndexChange,
|
||||
onRecordSelect,
|
||||
onClearSelection,
|
||||
collapsedTurns,
|
||||
onToggleTurn,
|
||||
collapsedAssistants,
|
||||
@@ -1517,6 +1521,16 @@ export function TrajectoryTable({
|
||||
setActiveTab(tab)
|
||||
}
|
||||
|
||||
const clearInspectorSelection = () => {
|
||||
setSelectedIndex(null)
|
||||
setSelectedRequest(null)
|
||||
}
|
||||
|
||||
const clearAllSelections = () => {
|
||||
clearInspectorSelection()
|
||||
onClearSelection?.()
|
||||
}
|
||||
|
||||
const selectRecord = (index: number) => {
|
||||
const record = allRecords.find(candidate => candidate.cell.index === index)
|
||||
onRecordSelect?.(index)
|
||||
@@ -1562,7 +1576,12 @@ export function TrajectoryTable({
|
||||
|
||||
return (
|
||||
<div className={css.split} style={splitStyle}>
|
||||
<div className={css.tablePane}>
|
||||
<div
|
||||
className={css.tablePane}
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) clearAllSelections()
|
||||
}}
|
||||
>
|
||||
<table className={css.table}>
|
||||
<colgroup>
|
||||
<col className={css.eventColumn} />
|
||||
@@ -1918,10 +1937,7 @@ export function TrajectoryTable({
|
||||
type="button"
|
||||
className={css.close}
|
||||
aria-label="Close details"
|
||||
onClick={() => {
|
||||
setSelectedIndex(null)
|
||||
setSelectedRequest(null)
|
||||
}}
|
||||
onClick={clearInspectorSelection}
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 10px 0 12px;
|
||||
padding: 0 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -503,6 +503,7 @@ export function TrajectoryView({ useSession, loadAllHistory }: ConvViewProps & T
|
||||
searchMatchIndexes={searchMatchIndexes}
|
||||
onSelectedIndexChange={setSelectedTimelineIndex}
|
||||
onRecordSelect={handleRecordSelect}
|
||||
onClearSelection={() => { setTimelineSelection(null) }}
|
||||
collapsedTurns={collapsedTurns}
|
||||
onToggleTurn={toggleTurn}
|
||||
collapsedAssistants={collapsedAssistants}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
/** Trajectory ledger selection, details, status, and fold behavior. */
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { TrajectoryTable } from '../src/client/TrajectoryTable.tsx'
|
||||
import type { TrajectoryTurnModel } from '../src/client/layout.ts'
|
||||
@@ -111,6 +111,30 @@ describe('TrajectoryTable', () => {
|
||||
)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('clears the selected row when ledger whitespace is clicked', () => {
|
||||
const onClearSelection = vi.fn()
|
||||
render(
|
||||
<TrajectoryTable
|
||||
turns={TURNS}
|
||||
{...FOLD_PROPS}
|
||||
onClearSelection={onClearSelection}
|
||||
/>,
|
||||
)
|
||||
const row = screen.getByRole('row', { name: /ASSISTANT/ })
|
||||
fireEvent.click(row)
|
||||
|
||||
expect(row.getAttribute('aria-selected')).toBe('true')
|
||||
expect(screen.getByRole('complementary', { name: 'Event details' })).toBeTruthy()
|
||||
|
||||
const tablePane = screen.getByRole('table').parentElement
|
||||
expect(tablePane).not.toBeNull()
|
||||
fireEvent.click(tablePane as HTMLElement)
|
||||
|
||||
expect(row.getAttribute('aria-selected')).toBe('false')
|
||||
expect(screen.queryByRole('complementary', { name: 'Event details' })).toBeNull()
|
||||
expect(onClearSelection).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('keeps running and failure semantics distinct from record roles', () => {
|
||||
const view = render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
|
||||
expect(view.container.querySelector('tr[data-kind="tool"][data-running="true"]')).toBeTruthy()
|
||||
|
||||
@@ -245,6 +245,18 @@ describe('tab switching in ConversationRoot', () => {
|
||||
fireEvent.pointerMove(plot, { clientX: 95, pointerId: 1 })
|
||||
fireEvent.pointerUp(plot, { clientX: 95, pointerId: 1 })
|
||||
|
||||
expect(screen.getByRole('row', { name: /USER/ }).getAttribute('data-timeline-focus'))
|
||||
.toBe('outside')
|
||||
|
||||
const tablePane = screen.getByRole('table').parentElement
|
||||
expect(tablePane).not.toBeNull()
|
||||
fireEvent.click(tablePane as HTMLElement)
|
||||
expect(screen.getByRole('row', { name: /USER/ }).getAttribute('data-timeline-focus'))
|
||||
.toBeNull()
|
||||
|
||||
fireEvent.pointerDown(plot, { button: 0, clientX: 55, pointerId: 2 })
|
||||
fireEvent.pointerMove(plot, { clientX: 95, pointerId: 2 })
|
||||
fireEvent.pointerUp(plot, { clientX: 95, pointerId: 2 })
|
||||
expect(screen.getByRole('row', { name: /USER/ }).getAttribute('data-timeline-focus'))
|
||||
.toBe('outside')
|
||||
fireEvent.contextMenu(plot)
|
||||
|
||||
Reference in New Issue
Block a user