From d3d7566f0c0b2ac7cca90b319a513a6493d37e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Thu, 11 Jun 2026 00:41:04 +0800 Subject: [PATCH] refactor(trace): redesign trace UI with LangSmith-style two-pane layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI rebuild (desktop): - TraceSession: replace 3-column layout with two panes — turn-grouped timeline tree (draggable splitter, search/filter, keyboard nav) and a section-flow detail panel (Response / Messages / System Prompt / Tools / Parameters / Raw), collapse state persists across spans - Render LLM requests/responses semantically: messages as role-colored conversation with tool_use/tool_result pairing instead of raw JSON dumps; Raw fallback via CodeViewer for legacy truncated records - TraceList: row-style list with model chips, mono metrics, hover actions; content-visibility rows (no virtualization, WebKit-safe) - i18n synced across zh/en/jp/kr/zh-TW (+25/-55 keys) Data & capture (server): - Capture full bodies: preview cap 2048 -> 240k chars, stream cap 256KB -> 1MB; list API trims previews to keep polling light; new GET /api/sessions/:id/trace/calls/:callId returns the full record - Extract per-call token usage at read time (SSE + JSON + proxy wrapped); mtime-keyed read cache for the polling path - Fix sensitive-key regex redacting *_tokens count fields, which made token stats always report 0 Frontend data layer: - SSE stream reassembly (Anthropic + OpenAI chat) adapted from claude-tap (MIT, attribution in THIRD_PARTY_LICENSES.md), request/ response body parsers, shared formatters, on-demand call detail cache; traceViewModel gains tokenUsage/isLifecycleNoise, drops fullRaw Tested: - bun run check:server (1201 pass) - bun run check:desktop (lint + 1358 tests + build) - Chromium walkthrough against real local traces: list, session tree, LLM semantic detail (new format), legacy fallback, tool detail --- THIRD_PARTY_LICENSES.md | 33 + desktop/src/api/sessions.test.ts | 17 + desktop/src/api/sessions.ts | 6 +- desktop/src/components/trace/TraceBadges.tsx | 174 ++ desktop/src/components/trace/TraceDetail.tsx | 164 ++ .../src/components/trace/TraceSplitLayout.tsx | 95 ++ desktop/src/components/trace/TraceTree.tsx | 361 ++++ .../components/trace/detail/LlmCallDetail.tsx | 338 ++++ .../components/trace/detail/MessageBlocks.tsx | 196 +++ .../components/trace/detail/MessageDetail.tsx | 60 + .../src/components/trace/detail/Section.tsx | 72 + .../trace/detail/SessionOverview.tsx | 132 ++ .../components/trace/detail/ToolDetail.tsx | 99 ++ desktop/src/i18n/locales/en.ts | 80 +- desktop/src/i18n/locales/jp.ts | 80 +- desktop/src/i18n/locales/kr.ts | 80 +- desktop/src/i18n/locales/zh-TW.ts | 80 +- desktop/src/i18n/locales/zh.ts | 80 +- desktop/src/lib/trace/callCache.test.ts | 118 ++ desktop/src/lib/trace/callCache.ts | 29 + desktop/src/lib/trace/formatters.test.ts | 75 + desktop/src/lib/trace/formatters.ts | 31 + desktop/src/lib/trace/requestParse.test.ts | 204 +++ desktop/src/lib/trace/requestParse.ts | 213 +++ desktop/src/lib/trace/sse.test.ts | 180 ++ desktop/src/lib/trace/sse.ts | 418 +++++ desktop/src/lib/trace/types.ts | 18 + desktop/src/lib/traceViewModel.test.ts | 21 + desktop/src/lib/traceViewModel.ts | 28 +- desktop/src/pages/TraceList.test.tsx | 98 +- desktop/src/pages/TraceList.tsx | 279 +++- desktop/src/pages/TraceSession.test.tsx | 460 ++++-- desktop/src/pages/TraceSession.tsx | 1472 +++-------------- desktop/src/theme/globals.css | 8 + desktop/src/types/trace.ts | 8 + src/server/__tests__/trace-capture.test.ts | 482 +++++- src/server/api/sessions.ts | 22 +- src/server/proxy/handler.ts | 3 +- src/server/services/traceCaptureService.ts | 4 + src/services/api/traceCapture.ts | 244 ++- 40 files changed, 4775 insertions(+), 1787 deletions(-) create mode 100644 THIRD_PARTY_LICENSES.md create mode 100644 desktop/src/components/trace/TraceBadges.tsx create mode 100644 desktop/src/components/trace/TraceDetail.tsx create mode 100644 desktop/src/components/trace/TraceSplitLayout.tsx create mode 100644 desktop/src/components/trace/TraceTree.tsx create mode 100644 desktop/src/components/trace/detail/LlmCallDetail.tsx create mode 100644 desktop/src/components/trace/detail/MessageBlocks.tsx create mode 100644 desktop/src/components/trace/detail/MessageDetail.tsx create mode 100644 desktop/src/components/trace/detail/Section.tsx create mode 100644 desktop/src/components/trace/detail/SessionOverview.tsx create mode 100644 desktop/src/components/trace/detail/ToolDetail.tsx create mode 100644 desktop/src/lib/trace/callCache.test.ts create mode 100644 desktop/src/lib/trace/callCache.ts create mode 100644 desktop/src/lib/trace/formatters.test.ts create mode 100644 desktop/src/lib/trace/formatters.ts create mode 100644 desktop/src/lib/trace/requestParse.test.ts create mode 100644 desktop/src/lib/trace/requestParse.ts create mode 100644 desktop/src/lib/trace/sse.test.ts create mode 100644 desktop/src/lib/trace/sse.ts create mode 100644 desktop/src/lib/trace/types.ts diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md new file mode 100644 index 00000000..60cc1820 --- /dev/null +++ b/THIRD_PARTY_LICENSES.md @@ -0,0 +1,33 @@ +# Third-Party Licenses + +This project includes code adapted from the following open source projects. + +## claude-tap + +- Project: claude-tap (https://github.com/liaohch3/claude-tap) +- Adapted in: `desktop/src/lib/trace/sse.ts` (SSE stream reassembly, ported from Python to TypeScript) +- License: MIT + +``` +MIT License + +Copyright (c) 2025 liaohch3 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/desktop/src/api/sessions.test.ts b/desktop/src/api/sessions.test.ts index fb0bde89..5a6aee98 100644 --- a/desktop/src/api/sessions.test.ts +++ b/desktop/src/api/sessions.test.ts @@ -39,4 +39,21 @@ describe('sessionsApi', () => { }), }) }) + + it('fetches a single trace call from the call detail endpoint', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ + call: { id: 'call-1', sessionId: 'session-1' }, + }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + })) + + const result = await sessionsApi.getTraceCall('session-1', 'call-1') + + expect(result.call.id).toBe('call-1') + const [url, init] = fetchMock.mock.calls[0]! + expect(url).toBe('http://127.0.0.1:3456/api/sessions/session-1/trace/calls/call-1') + expect(init).toMatchObject({ method: 'GET' }) + }) }) diff --git a/desktop/src/api/sessions.ts b/desktop/src/api/sessions.ts index a19bb741..00f3d373 100644 --- a/desktop/src/api/sessions.ts +++ b/desktop/src/api/sessions.ts @@ -2,7 +2,7 @@ import { api } from './client' import type { AgentTaskNotification } from '../types/chat' import type { SessionListItem, MessageEntry } from '../types/session' import type { PermissionMode } from '../types/settings' -import type { TraceSession } from '../types/trace' +import type { TraceCallRecord, TraceSession } from '../types/trace' type SessionsResponse = { sessions: SessionListItem[]; total: number } type MessagesResponse = { @@ -325,6 +325,10 @@ export const sessionsApi = { return api.get(`/api/sessions/${sessionId}/trace`) }, + getTraceCall(sessionId: string, callId: string) { + return api.get<{ call: TraceCallRecord }>(`/api/sessions/${sessionId}/trace/calls/${callId}`) + }, + create(input?: string | CreateSessionRequest) { const body = typeof input === 'string' ? (input ? { workDir: input } : {}) diff --git a/desktop/src/components/trace/TraceBadges.tsx b/desktop/src/components/trace/TraceBadges.tsx new file mode 100644 index 00000000..9f144cbf --- /dev/null +++ b/desktop/src/components/trace/TraceBadges.tsx @@ -0,0 +1,174 @@ +import type { ReactNode } from 'react' +import { + AlertTriangle, + Bot, + CircleDot, + Clock3, + FileJson2, + GitBranch, + MessageSquareText, + RadioTower, + Sparkles, + Wrench, +} from 'lucide-react' +import { useTranslation } from '../../i18n' +import type { TraceSpan, TraceSpanStatus } from '../../lib/traceViewModel' + +type TraceTranslator = ReturnType + +export function TypeIcon({ span, size = 14 }: { span: TraceSpan; size?: number }) { + const { icon, className } = iconForSpan(span, size) + return ( + + ) +} + +function iconForSpan(span: TraceSpan, size: number): { icon: ReactNode; className: string } { + const tertiary = 'text-[var(--color-text-tertiary)]' + switch (span.kind) { + case 'llm': + return { icon: , className: 'text-[var(--color-brand)]' } + case 'tool': + return { icon: , className: 'text-[var(--color-warning)]' } + case 'tool_result': + return { icon: , className: tertiary } + case 'turn': + return { icon: , className: tertiary } + case 'session': + return { icon: , className: tertiary } + case 'event': + return span.status === 'error' + ? { icon: , className: 'text-[var(--color-error)]' } + : { icon: , className: tertiary } + case 'message': + if (span.message?.type === 'assistant') { + return { icon: , className: tertiary } + } + if (span.message?.type === 'system') { + return { icon: , className: tertiary } + } + return { icon: , className: tertiary } + default: + return { icon: , className: tertiary } + } +} + +export function StatusGlyph({ status }: { status: TraceSpanStatus }) { + if (status === 'error') { + return