feat(ui): compact trajectory role labels
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
container: trajectory-table / inline-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
@@ -279,6 +280,10 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.turnLabelCompact {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.turnLabelActive {
|
.turnLabelActive {
|
||||||
color: color-mix(
|
color: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
@@ -325,11 +330,66 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kindTagIcon {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kindTagLabel {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
.table .kindSlot .message {
|
.table .kindSlot .message {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
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 {
|
.user {
|
||||||
color: var(--dsw-alias-state-business-primary);
|
color: var(--dsw-alias-state-business-primary);
|
||||||
background: var(--dsw-alias-state-business-tertiary);
|
background: var(--dsw-alias-state-business-tertiary);
|
||||||
|
|||||||
@@ -3,7 +3,14 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import type { CSSProperties, ReactNode } from 'react'
|
import type { CSSProperties, ReactNode } from 'react'
|
||||||
import {
|
import {
|
||||||
extractMarkdownPlainText, IconChevronRightOutline14, JsonTree, MarkdownText,
|
extractMarkdownPlainText,
|
||||||
|
IconChevronRightOutline14,
|
||||||
|
IconSettingsOutline16,
|
||||||
|
IconSparkle16,
|
||||||
|
IconUserOutline16,
|
||||||
|
JsonTree,
|
||||||
|
MarkdownText,
|
||||||
|
Tooltip,
|
||||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
import { structuredPatch } from 'diff'
|
import { structuredPatch } from 'diff'
|
||||||
import type {
|
import type {
|
||||||
@@ -26,6 +33,77 @@ const KIND_LABEL: Record<TrajectoryCellKind, string> = {
|
|||||||
subtool: 'SUBTOOL',
|
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 {
|
interface TableRecord {
|
||||||
turn: number
|
turn: number
|
||||||
group: string
|
group: string
|
||||||
@@ -1726,8 +1804,14 @@ export function TrajectoryTable({
|
|||||||
className={activeTurn === record.turn
|
className={activeTurn === record.turn
|
||||||
? `${css.turnLabel} ${css.turnLabelActive}`
|
? `${css.turnLabel} ${css.turnLabelActive}`
|
||||||
: css.turnLabel}
|
: 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>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className={css.eventInner}>
|
<div className={css.eventInner}>
|
||||||
@@ -1735,24 +1819,33 @@ export function TrajectoryTable({
|
|||||||
<span
|
<span
|
||||||
className={css.kindSlot}
|
className={css.kindSlot}
|
||||||
>
|
>
|
||||||
<span className={`${css.kindTag} ${
|
<Tooltip label={KIND_LABEL[record.cell.kind]} side="bottom">
|
||||||
record.cell.kind === 'system'
|
<span
|
||||||
? css.systemNeutral
|
className={`${css.kindTag} ${
|
||||||
: record.cell.kind === 'context'
|
record.cell.kind === 'system'
|
||||||
? css.contextGreen
|
? css.systemNeutral
|
||||||
: record.cell.kind === 'compacted'
|
: record.cell.kind === 'context'
|
||||||
? css.compacted
|
? css.contextGreen
|
||||||
: record.cell.kind === 'tool'
|
: record.cell.kind === 'compacted'
|
||||||
? css.toolAmber
|
? css.compacted
|
||||||
: record.cell.kind === 'message'
|
: record.cell.kind === 'tool'
|
||||||
? css.assistantVioletBright
|
? css.toolAmber
|
||||||
: record.cell.kind === 'subtool'
|
: record.cell.kind === 'message'
|
||||||
? css.subtoolAmber
|
? css.assistantVioletBright
|
||||||
: css[record.cell.kind]
|
: record.cell.kind === 'subtool'
|
||||||
}`}
|
? css.subtoolAmber
|
||||||
>
|
: css[record.cell.kind]
|
||||||
{KIND_LABEL[record.cell.kind]}
|
}`}
|
||||||
</span>
|
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>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -148,6 +148,49 @@ describe('TrajectoryTable', () => {
|
|||||||
expect(screen.getByText('ToolError: non_zero_exit')).toBeTruthy()
|
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', () => {
|
it('renders a single-text JSON tool result as a JSON tree', () => {
|
||||||
const turns: readonly TrajectoryTurnModel[] = [{
|
const turns: readonly TrajectoryTurnModel[] = [{
|
||||||
turn: 1,
|
turn: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user