Browse Source

feat(ui): compact trajectory role labels

_Kerman 2 months ago
parent
commit
411bbdf2aa

+ 60 - 0
packages/client/ui-trajectory/src/client/TrajectoryTable.module.css

@@ -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);

+ 113 - 20
packages/client/ui-trajectory/src/client/TrajectoryTable.tsx

@@ -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>

+ 43 - 0
packages/client/ui-trajectory/tests/table.spec.tsx

@@ -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,