feat(ui): compact trajectory role labels
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
container: trajectory-table / inline-size;
|
||||
}
|
||||
|
||||
.table {
|
||||
@@ -279,6 +280,10 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.turnLabelCompact {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.turnLabelActive {
|
||||
color: color-mix(
|
||||
in srgb,
|
||||
@@ -325,11 +330,66 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.kindTagIcon {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.kindTagLabel {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.table .kindSlot .message {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@container trajectory-table (max-width: 620px) {
|
||||
.eventColumn {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.event {
|
||||
padding-right: 3px !important;
|
||||
padding-left: 28px !important;
|
||||
}
|
||||
|
||||
.requestBoundaryControl {
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.kindSlot {
|
||||
width: 19px;
|
||||
}
|
||||
|
||||
.kindTag,
|
||||
.table .kindSlot .message {
|
||||
justify-content: center;
|
||||
width: 19px;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.kindTagIcon {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.kindTagLabel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.turnLabelFull {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.turnLabelCompact {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
color: var(--dsw-alias-state-business-primary);
|
||||
background: var(--dsw-alias-state-business-tertiary);
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { CSSProperties, ReactNode } from 'react'
|
||||
import {
|
||||
extractMarkdownPlainText, IconChevronRightOutline14, JsonTree, MarkdownText,
|
||||
extractMarkdownPlainText,
|
||||
IconChevronRightOutline14,
|
||||
IconSettingsOutline16,
|
||||
IconSparkle16,
|
||||
IconUserOutline16,
|
||||
JsonTree,
|
||||
MarkdownText,
|
||||
Tooltip,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { structuredPatch } from 'diff'
|
||||
import type {
|
||||
@@ -26,6 +33,77 @@ const KIND_LABEL: Record<TrajectoryCellKind, string> = {
|
||||
subtool: 'SUBTOOL',
|
||||
}
|
||||
|
||||
function ToolWrenchIcon(): ReactNode {
|
||||
return (
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
data-role-icon="wrench"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M14 3.3a3.8 3.8 0 0 1-4.8 4.8l-5.1 5.1a1.6 1.6 0 1 1-2.3-2.3l5.1-5.1A3.8 3.8 0 0 1 11.7 1l-2.3 2.3 2.3 2.3L14 3.3Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function InformationIcon(): ReactNode {
|
||||
return (
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
data-role-icon="information"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="8" cy="8" r="5.6" />
|
||||
<circle cx="8" cy="5.5" r=".85" fill="currentColor" stroke="none" />
|
||||
<path d="M8 7.75v3.4" strokeWidth="1.8" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function CompactedIcon(): ReactNode {
|
||||
return (
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
data-role-icon="compacted"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m2.5 2.5 3.75 3.75M3 6.25h3.25V3" />
|
||||
<path d="m13.5 2.5-3.75 3.75M13 6.25H9.75V3" />
|
||||
<path d="m2.5 13.5 3.75-3.75M3 9.75h3.25V13" />
|
||||
<path d="m13.5 13.5-3.75-3.75M13 9.75H9.75V13" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const KIND_ICON: Record<TrajectoryCellKind, ReactNode> = {
|
||||
system: <IconSettingsOutline16 size={13} />,
|
||||
user: <IconUserOutline16 size={13} />,
|
||||
context: <InformationIcon />,
|
||||
compacted: <CompactedIcon />,
|
||||
message: <IconSparkle16 size={13} />,
|
||||
tool: <ToolWrenchIcon />,
|
||||
subtool: <ToolWrenchIcon />,
|
||||
}
|
||||
|
||||
interface TableRecord {
|
||||
turn: number
|
||||
group: string
|
||||
@@ -1726,8 +1804,14 @@ export function TrajectoryTable({
|
||||
className={activeTurn === record.turn
|
||||
? `${css.turnLabel} ${css.turnLabelActive}`
|
||||
: css.turnLabel}
|
||||
aria-label={`Turn ${record.turn}`}
|
||||
>
|
||||
Turn {record.turn}
|
||||
<span className={css.turnLabelFull} aria-hidden="true">
|
||||
Turn {record.turn}
|
||||
</span>
|
||||
<span className={css.turnLabelCompact} aria-hidden="true">
|
||||
#{record.turn}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
<div className={css.eventInner}>
|
||||
@@ -1735,24 +1819,33 @@ export function TrajectoryTable({
|
||||
<span
|
||||
className={css.kindSlot}
|
||||
>
|
||||
<span className={`${css.kindTag} ${
|
||||
record.cell.kind === 'system'
|
||||
? css.systemNeutral
|
||||
: record.cell.kind === 'context'
|
||||
? css.contextGreen
|
||||
: record.cell.kind === 'compacted'
|
||||
? css.compacted
|
||||
: record.cell.kind === 'tool'
|
||||
? css.toolAmber
|
||||
: record.cell.kind === 'message'
|
||||
? css.assistantVioletBright
|
||||
: record.cell.kind === 'subtool'
|
||||
? css.subtoolAmber
|
||||
: css[record.cell.kind]
|
||||
}`}
|
||||
>
|
||||
{KIND_LABEL[record.cell.kind]}
|
||||
</span>
|
||||
<Tooltip label={KIND_LABEL[record.cell.kind]} side="bottom">
|
||||
<span
|
||||
className={`${css.kindTag} ${
|
||||
record.cell.kind === 'system'
|
||||
? css.systemNeutral
|
||||
: record.cell.kind === 'context'
|
||||
? css.contextGreen
|
||||
: record.cell.kind === 'compacted'
|
||||
? css.compacted
|
||||
: record.cell.kind === 'tool'
|
||||
? css.toolAmber
|
||||
: record.cell.kind === 'message'
|
||||
? css.assistantVioletBright
|
||||
: record.cell.kind === 'subtool'
|
||||
? css.subtoolAmber
|
||||
: css[record.cell.kind]
|
||||
}`}
|
||||
data-role-kind={record.cell.kind}
|
||||
>
|
||||
<span className={css.kindTagIcon} aria-hidden="true">
|
||||
{KIND_ICON[record.cell.kind]}
|
||||
</span>
|
||||
<span className={css.kindTagLabel}>
|
||||
{KIND_LABEL[record.cell.kind]}
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -148,6 +148,49 @@ describe('TrajectoryTable', () => {
|
||||
expect(screen.getByText('ToolError: non_zero_exit')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders responsive role icons with a custom tooltip', () => {
|
||||
const view = render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
|
||||
const toolTag = view.container.querySelector<HTMLElement>('[data-role-kind="tool"]')
|
||||
|
||||
expect(toolTag).not.toBeNull()
|
||||
expect(toolTag?.getAttribute('title')).toBeNull()
|
||||
expect(toolTag?.querySelector('[data-role-icon="wrench"]')).toBeTruthy()
|
||||
|
||||
fireEvent.mouseEnter(toolTag as HTMLElement)
|
||||
expect(screen.getByRole('tooltip').textContent).toBe('TOOL')
|
||||
fireEvent.mouseLeave(toolTag as HTMLElement)
|
||||
expect(screen.queryByRole('tooltip')).toBeNull()
|
||||
})
|
||||
|
||||
it('uses information and compression glyphs for injected and compacted context', () => {
|
||||
const turns: readonly TrajectoryTurnModel[] = [{
|
||||
turn: 1,
|
||||
groups: [{
|
||||
title: 'Context',
|
||||
cells: [
|
||||
{ index: 1, kind: 'context', text: 'Workspace context', timeSeconds: 0 },
|
||||
{ index: 2, kind: 'compacted', text: 'Compacted history', timeSeconds: 0 },
|
||||
],
|
||||
}],
|
||||
}]
|
||||
const view = render(<TrajectoryTable turns={turns} {...FOLD_PROPS} />)
|
||||
|
||||
expect(view.container.querySelector(
|
||||
'[data-role-kind="context"] [data-role-icon="information"]',
|
||||
)).toBeTruthy()
|
||||
expect(view.container.querySelector(
|
||||
'[data-role-kind="compacted"] [data-role-icon="compacted"]',
|
||||
)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('keeps a compact turn label available for narrow layouts', () => {
|
||||
render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
|
||||
const turnLabel = screen.getByLabelText('Turn 1')
|
||||
|
||||
expect(turnLabel.textContent).toContain('Turn 1')
|
||||
expect(turnLabel.textContent).toContain('#1')
|
||||
})
|
||||
|
||||
it('renders a single-text JSON tool result as a JSON tree', () => {
|
||||
const turns: readonly TrajectoryTurnModel[] = [{
|
||||
turn: 1,
|
||||
|
||||
Reference in New Issue
Block a user