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