diff --git a/packages/chat-debug-view/src/parts/ChatViewEvent/ChatViewEvent.ts b/packages/chat-debug-view/src/parts/ChatViewEvent/ChatViewEvent.ts index 9f2b2003..f2f18bd8 100644 --- a/packages/chat-debug-view/src/parts/ChatViewEvent/ChatViewEvent.ts +++ b/packages/chat-debug-view/src/parts/ChatViewEvent/ChatViewEvent.ts @@ -4,10 +4,12 @@ export interface ChatViewEvent { readonly ended?: number | string readonly endTime?: number | string readonly eventId: number + readonly method?: string readonly [key: string]: unknown readonly sessionId?: string readonly started?: number | string readonly startTime?: number | string + readonly time?: string readonly timestamp?: number | string readonly type: string } diff --git a/packages/chat-debug-view/src/parts/GetCellDurationDom/GetCellDurationDom.ts b/packages/chat-debug-view/src/parts/GetCellDurationDom/GetCellDurationDom.ts new file mode 100644 index 00000000..4cd56a65 --- /dev/null +++ b/packages/chat-debug-view/src/parts/GetCellDurationDom/GetCellDurationDom.ts @@ -0,0 +1,15 @@ +import { mergeClassNames, type VirtualDomNode, VirtualDomElements, text } from '@lvce-editor/virtual-dom-worker' +import type { ChatViewEvent } from '../ChatViewEvent/ChatViewEvent.ts' +import { ChatDebugViewCellDuration, TableCell } from '../ClassNames/ClassNames.ts' +import { getEventTableDurationText } from '../GetEventTableDurationText/GetEventTableDurationText.ts' + +export const getCellDurationDom = (event: ChatViewEvent): readonly VirtualDomNode[] => { + return [ + { + childCount: 1, + className: mergeClassNames(TableCell, ChatDebugViewCellDuration), + type: VirtualDomElements.Td, + }, + text(getEventTableDurationText(event)), + ] +} diff --git a/packages/chat-debug-view/src/parts/GetCellMethodDom/GetCellMethodDom.ts b/packages/chat-debug-view/src/parts/GetCellMethodDom/GetCellMethodDom.ts new file mode 100644 index 00000000..18e30318 --- /dev/null +++ b/packages/chat-debug-view/src/parts/GetCellMethodDom/GetCellMethodDom.ts @@ -0,0 +1,15 @@ +import { type VirtualDomNode, VirtualDomElements, text } from '@lvce-editor/virtual-dom-worker' +import type { ChatViewEvent } from '../ChatViewEvent/ChatViewEvent.ts' +import { TableCell } from '../ClassNames/ClassNames.ts' +import { getEventTableMethodLabel } from '../GetEventTableMethodLabel/GetEventTableMethodLabel.ts' + +export const getCellMethodDom = (event: ChatViewEvent): readonly VirtualDomNode[] => { + return [ + { + childCount: 1, + className: TableCell, + type: VirtualDomElements.Td, + }, + text(getEventTableMethodLabel(event)), + ] +} diff --git a/packages/chat-debug-view/src/parts/GetCellStatusDom/GetCellStatusDom.ts b/packages/chat-debug-view/src/parts/GetCellStatusDom/GetCellStatusDom.ts new file mode 100644 index 00000000..1fc84532 --- /dev/null +++ b/packages/chat-debug-view/src/parts/GetCellStatusDom/GetCellStatusDom.ts @@ -0,0 +1,15 @@ +import { mergeClassNames, type VirtualDomNode, VirtualDomElements, text } from '@lvce-editor/virtual-dom-worker' +import type { ChatViewEvent } from '../ChatViewEvent/ChatViewEvent.ts' +import { ChatDebugViewCellStatusError, TableCell } from '../ClassNames/ClassNames.ts' +import { getStatusText } from '../GetStatusText/GetStatusText.ts' + +export const getCellStatusDom = (event: ChatViewEvent, isErrorStatus: boolean): readonly VirtualDomNode[] => { + return [ + { + childCount: 1, + className: mergeClassNames(TableCell, isErrorStatus ? ChatDebugViewCellStatusError : ''), + type: VirtualDomElements.Td, + }, + text(getStatusText(event)), + ] +} diff --git a/packages/chat-debug-view/src/parts/GetCellTypeDom/GetCellTypeDom.ts b/packages/chat-debug-view/src/parts/GetCellTypeDom/GetCellTypeDom.ts new file mode 100644 index 00000000..824123bc --- /dev/null +++ b/packages/chat-debug-view/src/parts/GetCellTypeDom/GetCellTypeDom.ts @@ -0,0 +1,15 @@ +import { type VirtualDomNode, VirtualDomElements, text } from '@lvce-editor/virtual-dom-worker' +import type { ChatViewEvent } from '../ChatViewEvent/ChatViewEvent.ts' +import { TableCell } from '../ClassNames/ClassNames.ts' +import { getEventTableTypeLabel } from '../GetEventTableTypeLabel/GetEventTableTypeLabel.ts' + +export const getCellTypeDom = (event: ChatViewEvent): readonly VirtualDomNode[] => { + return [ + { + childCount: 1, + className: TableCell, + type: VirtualDomElements.Td, + }, + text(getEventTableTypeLabel(event)), + ] +} diff --git a/packages/chat-debug-view/src/parts/GetDurationText/GetDurationText.ts b/packages/chat-debug-view/src/parts/GetDurationText/GetDurationText.ts index e99ef45c..eabb3e6c 100644 --- a/packages/chat-debug-view/src/parts/GetDurationText/GetDurationText.ts +++ b/packages/chat-debug-view/src/parts/GetDurationText/GetDurationText.ts @@ -2,6 +2,9 @@ import type { ChatViewEvent } from '../ChatViewEvent/ChatViewEvent.ts' import { toTimeNumber } from '../ToTimeNumber/ToTimeNumber.ts' export const getDurationText = (event: ChatViewEvent): string => { + if (event.time) { + return event.time + } const explicitDuration = event.durationMs ?? event.duration if (typeof explicitDuration === 'number' && Number.isFinite(explicitDuration)) { return `${explicitDuration}ms` diff --git a/packages/chat-debug-view/src/parts/GetEventTableMethodLabel/GetEventTableMethodLabel.ts b/packages/chat-debug-view/src/parts/GetEventTableMethodLabel/GetEventTableMethodLabel.ts index 765a09f2..e016369d 100644 --- a/packages/chat-debug-view/src/parts/GetEventTableMethodLabel/GetEventTableMethodLabel.ts +++ b/packages/chat-debug-view/src/parts/GetEventTableMethodLabel/GetEventTableMethodLabel.ts @@ -6,6 +6,9 @@ const postMethods = new Set(['create_directory', 'create_file', 'mkdir', 'write_ const deleteMethods = new Set(['delete_directory', 'delete_file', 'delete_folder', 'remove_directory', 'remove_file', 'remove_folder']) export const getEventTableMethodLabel = (event: ChatViewEvent): string => { + if (event.method) { + return event.method + } const toolName = getToolName(event) if (!toolName) { return '' diff --git a/packages/chat-debug-view/src/parts/GetRowCellNodes/GetRowCellNodes.ts b/packages/chat-debug-view/src/parts/GetRowCellNodes/GetRowCellNodes.ts index cce06f0d..c2bfe141 100644 --- a/packages/chat-debug-view/src/parts/GetRowCellNodes/GetRowCellNodes.ts +++ b/packages/chat-debug-view/src/parts/GetRowCellNodes/GetRowCellNodes.ts @@ -1,58 +1,11 @@ -import { mergeClassNames, type VirtualDomNode, VirtualDomElements, text } from '@lvce-editor/virtual-dom-worker' +import type { VirtualDomNode } from '@lvce-editor/virtual-dom-worker' import type { ChatViewEvent } from '../ChatViewEvent/ChatViewEvent.ts' -import { ChatDebugViewCellDuration, ChatDebugViewCellStatusError, TableCell } from '../ClassNames/ClassNames.ts' -import { getEventTableDurationText } from '../GetEventTableDurationText/GetEventTableDurationText.ts' -import { getEventTableMethodLabel } from '../GetEventTableMethodLabel/GetEventTableMethodLabel.ts' -import { getEventTableTypeLabel } from '../GetEventTableTypeLabel/GetEventTableTypeLabel.ts' -import { getStatusText } from '../GetStatusText/GetStatusText.ts' +import * as GetTableCellDom from '../GetTableCellDom/GetTableCellDom.ts' import * as TableColumn from '../TableColumn/TableColumn.ts' -const getTableCellDom = (column: TableColumn.TableColumnName, event: ChatViewEvent, isErrorStatus: boolean): readonly VirtualDomNode[] => { - switch (column) { - case TableColumn.Duration: - return [ - { - childCount: 1, - className: mergeClassNames(TableCell, ChatDebugViewCellDuration), - type: VirtualDomElements.Td, - }, - text(getEventTableDurationText(event)), - ] - case TableColumn.Method: - return [ - { - childCount: 1, - className: TableCell, - type: VirtualDomElements.Td, - }, - text(getEventTableMethodLabel(event)), - ] - case TableColumn.Status: - return [ - { - childCount: 1, - className: mergeClassNames(TableCell, isErrorStatus ? ChatDebugViewCellStatusError : ''), - type: VirtualDomElements.Td, - }, - text(getStatusText(event)), - ] - case TableColumn.Type: - return [ - { - childCount: 1, - className: TableCell, - type: VirtualDomElements.Td, - }, - text(getEventTableTypeLabel(event)), - ] - default: - return [] - } -} - export const getRowCellNodes = (event: ChatViewEvent, isErrorStatus: boolean, visibleTableColumns: readonly string[]): readonly VirtualDomNode[] => { const orderedVisibleTableColumns = TableColumn.getOrderedVisibleTableColumns(visibleTableColumns) return orderedVisibleTableColumns.flatMap((column) => { - return getTableCellDom(column, event, isErrorStatus) + return GetTableCellDom.getTableCellDom(column, event, isErrorStatus) }) } diff --git a/packages/chat-debug-view/src/parts/GetTableCellDom/GetTableCellDom.ts b/packages/chat-debug-view/src/parts/GetTableCellDom/GetTableCellDom.ts new file mode 100644 index 00000000..6499f4e2 --- /dev/null +++ b/packages/chat-debug-view/src/parts/GetTableCellDom/GetTableCellDom.ts @@ -0,0 +1,22 @@ +import type { VirtualDomNode } from '@lvce-editor/virtual-dom-worker' +import type { ChatViewEvent } from '../ChatViewEvent/ChatViewEvent.ts' +import * as GetCellDurationDom from '../GetCellDurationDom/GetCellDurationDom.ts' +import * as GetCellMethodDom from '../GetCellMethodDom/GetCellMethodDom.ts' +import * as GetCellStatusDom from '../GetCellStatusDom/GetCellStatusDom.ts' +import * as GetCellTypeDom from '../GetCellTypeDom/GetCellTypeDom.ts' +import * as TableColumn from '../TableColumn/TableColumn.ts' + +export const getTableCellDom = (column: TableColumn.TableColumnName, event: ChatViewEvent, isErrorStatus: boolean): readonly VirtualDomNode[] => { + switch (column) { + case TableColumn.Duration: + return GetCellDurationDom.getCellDurationDom(event) + case TableColumn.Method: + return GetCellMethodDom.getCellMethodDom(event) + case TableColumn.Status: + return GetCellStatusDom.getCellStatusDom(event, isErrorStatus) + case TableColumn.Type: + return GetCellTypeDom.getCellTypeDom(event) + default: + return [] + } +} diff --git a/packages/chat-debug-view/src/parts/ToPrettyEvents/ToPrettyEvents.ts b/packages/chat-debug-view/src/parts/ToPrettyEvents/ToPrettyEvents.ts index 5cbb3068..9fd05a1d 100644 --- a/packages/chat-debug-view/src/parts/ToPrettyEvents/ToPrettyEvents.ts +++ b/packages/chat-debug-view/src/parts/ToPrettyEvents/ToPrettyEvents.ts @@ -2,6 +2,29 @@ import type { ChatViewEvent } from '../ChatViewEvent/ChatViewEvent.ts' import type { ListChatViewEventsResult } from '../ListChatViewEventsResult/ListChatViewEventsResult.ts' import * as GetResponseMap from '../GetResponseMap/GetResponseMap.ts' +const getMergedRequestResponseEvent = (item: ChatViewEvent, response: ChatViewEvent): ChatViewEvent => { + const parsedStart = new Date(item.timestamp || '') + const parsedEnd = new Date(response.timestamp || '') + const durationMs = parsedEnd.getTime() - parsedStart.getTime() + + if (Number.isFinite(durationMs) && durationMs >= 0) { + return { + durationMs, + eventEndId: response.eventId, + eventId: item.eventId, + method: 'POST', + type: 'ai-request-response', + } + } + + return { + eventEndId: response.eventId, + eventId: item.eventId, + method: 'POST', + type: 'ai-request-response', + } +} + export const toPrettyEvents = (rawEvents: ListChatViewEventsResult): readonly ChatViewEvent[] => { if (rawEvents.type === 'error') { return [] @@ -12,11 +35,7 @@ export const toPrettyEvents = (rawEvents: ListChatViewEventsResult): readonly Ch if (item.type === 'ai-request' && 'requestId' in item && typeof item.requestId === 'string') { const response = map[item.requestId] if (response) { - pretty.push({ - eventEndId: response.eventId, - eventId: item.eventId, - type: 'ai-request-response', - }) + pretty.push(getMergedRequestResponseEvent(item, response)) } else { pretty.push(item) } diff --git a/packages/chat-debug-view/test/GetCellDurationDom.test.ts b/packages/chat-debug-view/test/GetCellDurationDom.test.ts new file mode 100644 index 00000000..892c5ddc --- /dev/null +++ b/packages/chat-debug-view/test/GetCellDurationDom.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@jest/globals' +import { VirtualDomElements, text } from '@lvce-editor/virtual-dom-worker' +import { getCellDurationDom } from '../src/parts/GetCellDurationDom/GetCellDurationDom.ts' + +test('getCellDurationDom should render the duration cell', () => { + const event = { + ended: '2026-03-08T00:00:01.250Z', + eventId: 1, + sessionId: 'session-1', + started: '2026-03-08T00:00:01.000Z', + timestamp: '2026-03-08T00:00:01.000Z', + type: 'tool-execution', + } + + const result = getCellDurationDom(event) + + expect(result).toEqual([ + { + childCount: 1, + className: 'TableCell ChatDebugViewCellDuration', + type: VirtualDomElements.Td, + }, + text('250 ms'), + ]) +}) diff --git a/packages/chat-debug-view/test/GetCellMethodDom.test.ts b/packages/chat-debug-view/test/GetCellMethodDom.test.ts new file mode 100644 index 00000000..17075c77 --- /dev/null +++ b/packages/chat-debug-view/test/GetCellMethodDom.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@jest/globals' +import { VirtualDomElements, text } from '@lvce-editor/virtual-dom-worker' +import { getCellMethodDom } from '../src/parts/GetCellMethodDom/GetCellMethodDom.ts' + +test('getCellMethodDom should render the method cell', () => { + const event = { + eventId: 1, + name: 'read_file', + sessionId: 'session-1', + timestamp: '2026-04-02T07:26:35.172Z', + type: 'tool-execution', + } + + const result = getCellMethodDom(event) + + expect(result).toEqual([ + { + childCount: 1, + className: 'TableCell', + type: VirtualDomElements.Td, + }, + text('GET'), + ]) +}) diff --git a/packages/chat-debug-view/test/GetCellStatusDom.test.ts b/packages/chat-debug-view/test/GetCellStatusDom.test.ts new file mode 100644 index 00000000..a4e51240 --- /dev/null +++ b/packages/chat-debug-view/test/GetCellStatusDom.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@jest/globals' +import { VirtualDomElements, text } from '@lvce-editor/virtual-dom-worker' +import { getCellStatusDom } from '../src/parts/GetCellStatusDom/GetCellStatusDom.ts' + +test('getCellStatusDom should render the error status cell', () => { + const event = { + error: 'Invalid argument: uri must be an absolute URI.', + eventId: 1, + name: 'list_files', + sessionId: 'session-1', + timestamp: '2026-04-02T07:26:35.172Z', + type: 'tool-execution', + } + + const result = getCellStatusDom(event, true) + + expect(result).toEqual([ + { + childCount: 1, + className: 'TableCell ChatDebugViewCellStatusError', + type: VirtualDomElements.Td, + }, + text('400'), + ]) +}) diff --git a/packages/chat-debug-view/test/GetCellTypeDom.test.ts b/packages/chat-debug-view/test/GetCellTypeDom.test.ts new file mode 100644 index 00000000..0b2c802c --- /dev/null +++ b/packages/chat-debug-view/test/GetCellTypeDom.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@jest/globals' +import { VirtualDomElements, text } from '@lvce-editor/virtual-dom-worker' +import { getCellTypeDom } from '../src/parts/GetCellTypeDom/GetCellTypeDom.ts' + +test('getCellTypeDom should render the type cell', () => { + const event = { + eventId: 1, + name: 'list_files', + sessionId: 'session-1', + timestamp: '2026-04-02T07:26:35.172Z', + type: 'tool-execution', + } + + const result = getCellTypeDom(event) + + expect(result).toEqual([ + { + childCount: 1, + className: 'TableCell', + type: VirtualDomElements.Td, + }, + text('list_files'), + ]) +}) diff --git a/packages/chat-debug-view/test/GetTableCellDom.test.ts b/packages/chat-debug-view/test/GetTableCellDom.test.ts new file mode 100644 index 00000000..4e21b2e2 --- /dev/null +++ b/packages/chat-debug-view/test/GetTableCellDom.test.ts @@ -0,0 +1,38 @@ +import { expect, test } from '@jest/globals' +import { VirtualDomElements, text } from '@lvce-editor/virtual-dom-worker' +import { getTableCellDom } from '../src/parts/GetTableCellDom/GetTableCellDom.ts' +import * as TableColumn from '../src/parts/TableColumn/TableColumn.ts' + +test('getTableCellDom should render a method cell', () => { + const event = { + eventId: 1, + name: 'read_file', + sessionId: 'session-1', + timestamp: '2026-04-02T07:26:35.172Z', + type: 'tool-execution', + } + + const result = getTableCellDom(TableColumn.Method, event, false) + + expect(result).toEqual([ + { + childCount: 1, + className: 'TableCell', + type: VirtualDomElements.Td, + }, + text('GET'), + ]) +}) + +test('getTableCellDom should return an empty array for unknown columns', () => { + const event = { + eventId: 1, + sessionId: 'session-1', + timestamp: '2026-04-02T07:26:35.172Z', + type: 'tool-execution', + } + + const result = getTableCellDom('unknown-column' as TableColumn.TableColumnName, event, false) + + expect(result).toEqual([]) +}) diff --git a/packages/chat-debug-view/test/Refresh.test.ts b/packages/chat-debug-view/test/Refresh.test.ts index 8f2bea1b..efc2261e 100644 --- a/packages/chat-debug-view/test/Refresh.test.ts +++ b/packages/chat-debug-view/test/Refresh.test.ts @@ -62,7 +62,7 @@ test('refresh should return session-not-found state when latest events are empty }) test('refresh should update events with latest data from chat storage worker', async () => { - const events = [{ eventId: 1, time: 1, type: 'request' }] + const events = [{ eventId: 1, time: '1ms', type: 'request' }] const listChatViewEventsSpy = jest.spyOn(refreshDependencies, 'listChatViewEvents').mockResolvedValue({ events, type: 'success', diff --git a/packages/chat-debug-view/test/SetSessionId.test.ts b/packages/chat-debug-view/test/SetSessionId.test.ts index 4e897e40..31ceb772 100644 --- a/packages/chat-debug-view/test/SetSessionId.test.ts +++ b/packages/chat-debug-view/test/SetSessionId.test.ts @@ -9,7 +9,7 @@ afterEach(() => { }) test('setSessionId should load events for the given session id and clear selection state', async () => { - const events = [{ eventId: 1, time: 1, type: 'request' }] + const events = [{ eventId: 1, time: '1ms', type: 'request' }] const listChatViewEventsSpy = jest.spyOn(setSessionIdDependencies, 'listChatViewEvents').mockResolvedValue({ events, type: 'success', diff --git a/packages/chat-debug-view/test/ToPrettyEvents.test.ts b/packages/chat-debug-view/test/ToPrettyEvents.test.ts index 4e6643a8..172437dd 100644 --- a/packages/chat-debug-view/test/ToPrettyEvents.test.ts +++ b/packages/chat-debug-view/test/ToPrettyEvents.test.ts @@ -1,6 +1,36 @@ import { expect, test } from '@jest/globals' import { toPrettyEvents } from '../src/parts/ToPrettyEvents/ToPrettyEvents.ts' +test('toPrettyEvents should include duration for merged ai request and ai response events', () => { + const requestEvent = { + eventId: 1, + requestId: 'request-1', + timestamp: '2026-05-12T10:00:00.000Z', + type: 'ai-request', + } + const responseEvent = { + eventId: 2, + requestId: 'request-1', + timestamp: '2026-05-12T10:00:00.125Z', + type: 'ai-response', + } + + const result = toPrettyEvents({ + events: [requestEvent, responseEvent], + type: 'success', + }) + + expect(result).toEqual([ + { + durationMs: 125, + eventEndId: 2, + eventId: 1, + method: 'POST', + type: 'ai-request-response', + }, + ]) +}) + test('toPrettyEvents should merge matching ai request and ai response events', () => { const requestEvent = { eventId: 1, @@ -22,6 +52,7 @@ test('toPrettyEvents should merge matching ai request and ai response events', ( { eventEndId: 2, eventId: 1, + method: 'POST', type: 'ai-request-response', }, ])