File size: 5,964 Bytes
7c5ed63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import { memo, useCallback } from 'react';

import type { WireEntry } from '../../types';
import { formatDuration, formatWallClock } from '../../util/time';
import { TypeBadge } from './TypeBadge';
import { renderHeadline } from './WireHeadline';
import { WireRowDetail } from './WireRowDetail';

/** Pairing hint for a `tool.call` ↔ `tool.result` row. Computed by the
 *  parent (WireTab) from the full record list and threaded down here so
 *  the row can render an inline cross-reference and participate in the
 *  hover-highlight protocol. */
export interface PairHint {
  toolCallId: string;
  kind: 'call' | 'result';
  callLineNo: number | null;
  resultLineNo: number | null;
  /** result.time − call.time, when both records carry a timestamp. */
  durationMs: number | null;
}

interface WireRowProps {
  entry: WireEntry;
  expanded: boolean;
  onToggle: () => void;
  /** Scroll to a line and expand it — wired by the Wire tab via the virtualizer. */
  onJumpTo?: (lineNo: number) => void;
  /** Set when this entry is a tool.call/tool.result; carries the matching counterpart's line. */
  pair?: PairHint;
  /** True when another row from this pair is currently hovered. */
  highlighted: boolean;
  /** Notify the parent that this row's pair group is being hovered. */
  onHoverPair?: (toolCallId: string | null) => void;
}

export const WireRow = memo(function WireRow({
  entry,
  expanded,
  onToggle,
  onJumpTo,
  pair,
  highlighted,
  onHoverPair,
}: WireRowProps) {
  const record = entry.data;
  const h = renderHeadline(record);
  const timeTitle = formatTimeTitle(record.time);

  const handleEnter = useCallback(() => {
    if (pair !== undefined && onHoverPair !== undefined) {
      onHoverPair(pair.toolCallId);
    }
  }, [pair, onHoverPair]);
  const handleLeave = useCallback(() => {
    if (pair !== undefined && onHoverPair !== undefined) {
      onHoverPair(null);
    }
  }, [pair, onHoverPair]);

  return (
    <div
      onMouseEnter={handleEnter}
      onMouseLeave={handleLeave}
      className={[
        'flex items-stretch border-b border-border',
        highlighted
          ? 'bg-[color-mix(in_oklab,var(--color-cat-tools)_18%,transparent)]'
          : expanded
            ? 'bg-surface-1'
            : 'bg-surface-0 hover:bg-surface-1',
      ].join(' ')}
    >
      <div className="min-w-0 flex-1">
        <button
          onClick={onToggle}
          className="flex w-full items-center gap-3 px-2 py-[5px] text-left min-h-[28px]"
        >
          <span className="font-mono text-[11px] text-fg-3 tabular w-[52px] shrink-0 text-right">
            {entry.lineNo}
          </span>
          <span
            className="font-mono text-[11px] text-fg-3 tabular w-[68px] shrink-0"
            title={timeTitle}
          >
            {record.time !== undefined ? formatWallClock(record.time) : '--:--:--'}
          </span>
          <span className="shrink-0">
            <TypeBadge type={record.type} />
          </span>
          <span className="flex-1 min-w-0 flex items-center gap-2">{h.main}</span>
          <span className="flex items-center gap-2 shrink-0">
            {h.right}
            {pair !== undefined ? <PairIndicator pair={pair} onJumpTo={onJumpTo} /> : null}
            <Chevron open={expanded} />
          </span>
        </button>
        {expanded ? (
          <div className="border-t border-border bg-surface-1 px-2 pb-2 pt-1">
            <WireRowDetail entry={entry} onJumpTo={onJumpTo} />
          </div>
        ) : null}
      </div>
    </div>
  );
});

function PairIndicator({
  pair,
  onJumpTo,
}: {
  pair: PairHint;
  onJumpTo?: (lineNo: number) => void;
}) {
  const isCall = pair.kind === 'call';
  const target = isCall ? pair.resultLineNo : pair.callLineNo;
  const arrow = isCall ? '→' : '←';
  const orphan = target === null;
  const label = orphan ? `${arrow} ?` : `${arrow} #${target}`;
  const title = orphan
    ? isCall
      ? 'no matching tool.result yet'
      : 'no preceding tool.call seen'
    : isCall
      ? `jump to tool.result on line ${target}`
      : `jump to tool.call on line ${target}`;

  const className = `font-mono text-[10px] tabular ${
    orphan ? 'text-[var(--color-sev-error)]' : 'text-[var(--color-cat-tools)] hover:text-fg-0'
  }`;

  // Show the call→result elapsed time on whichever row has its partner.
  const duration =
    pair.durationMs !== null ? (
      <span className="font-mono text-[10px] text-fg-3 tabular" title="tool.call → tool.result elapsed">
        {formatDuration(pair.durationMs)}
      </span>
    ) : null;

  if (orphan || target === null || onJumpTo === undefined) {
    return (
      <span className="flex items-center gap-1.5">
        {duration}
        <span className={className} title={title}>
          {label}
        </span>
      </span>
    );
  }
  return (
    <span className="flex items-center gap-1.5">
      {duration}
      <span
        role="link"
        tabIndex={0}
        className={`${className} cursor-pointer`}
        title={title}
        onClick={(e) => {
          e.stopPropagation();
          onJumpTo(target);
        }}
        onKeyDown={(e) => {
          if (e.key === 'Enter') {
            e.stopPropagation();
            onJumpTo(target);
          }
        }}
      >
        {label}
      </span>
    </span>
  );
}

function formatTimeTitle(epochMs: number | undefined): string {
  if (epochMs === undefined || !Number.isFinite(epochMs)) return 'missing time';
  const date = new Date(epochMs);
  if (!Number.isFinite(date.getTime())) return 'invalid time';
  return date.toISOString();
}

function Chevron({ open }: { open: boolean }) {
  return (
    <svg
      width="10"
      height="10"
      viewBox="0 0 10 10"
      className={`text-fg-3 transition-transform ${open ? 'rotate-90' : ''}`}
      aria-hidden="true"
    >
      <path d="M3 2 L7 5 L3 8" stroke="currentColor" strokeWidth="1" fill="none" />
    </svg>
  );
}