[codex] Add local tool trace toggle (#806)
* test: harden tool approval browser contract * test: cover tool trace display edge cases * test: cover resumed tool trace edge cases * feat: hide tool traces by default * Add local tool trace toggle --------- Co-authored-by: Zhicheng Han <zhicheng.han@mathematik.uni-goettingen.de>
This commit is contained in:
@@ -120,9 +120,11 @@ describe('MessageItem tool details', () => {
|
||||
|
||||
const expected = JSON.stringify(message, null, 2)
|
||||
const code = wrapper.find('.tool-details code.hljs')
|
||||
const displayed = JSON.parse(code.text())
|
||||
expect(wrapper.find('.tool-details .code-lang').text()).toBe('json')
|
||||
expect(wrapper.html()).toContain('chat.truncated')
|
||||
expect(code.findAll('span')).toHaveLength(0)
|
||||
expect(displayed.content).toContain('chat.truncated')
|
||||
expect(code.findAll('span').length).toBeGreaterThan(0)
|
||||
|
||||
await wrapper.find('.tool-details [data-copy-code="true"]').trigger('click')
|
||||
expect(writeText).toHaveBeenCalledWith(expected)
|
||||
@@ -150,14 +152,45 @@ describe('MessageItem tool details', () => {
|
||||
|
||||
await wrapper.find('.tool-line').trigger('click')
|
||||
|
||||
const code = wrapper.find('.tool-details code.hljs')
|
||||
const displayed = JSON.parse(code.text())
|
||||
expect(wrapper.find('.tool-details .code-lang').text()).toBe('json')
|
||||
expect(wrapper.html()).toContain('chat.truncated')
|
||||
expect(wrapper.find('.tool-details code.hljs').findAll('span')).toHaveLength(0)
|
||||
expect(displayed.content).toContain('chat.truncated')
|
||||
expect(code.findAll('span').length).toBeGreaterThan(0)
|
||||
|
||||
await wrapper.find('.tool-details [data-copy-code="true"]').trigger('click')
|
||||
expect(writeText).toHaveBeenCalledWith(JSON.stringify(fullResult, null, 2))
|
||||
})
|
||||
|
||||
it('truncates large JSON arrays at item boundaries so display remains parseable JSON', async () => {
|
||||
const fullResult = Array.from({ length: 100 }, (_, index) => ({
|
||||
index,
|
||||
value: `item-${index}-${'x'.repeat(80)}`,
|
||||
}))
|
||||
const wrapper = mount(MessageItem, {
|
||||
props: {
|
||||
message: {
|
||||
id: 'tool-array',
|
||||
role: 'tool',
|
||||
content: '',
|
||||
timestamp: Date.now(),
|
||||
toolName: 'browser_snapshot',
|
||||
toolResult: JSON.stringify(fullResult),
|
||||
toolStatus: 'done',
|
||||
} satisfies Message,
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.find('.tool-line').trigger('click')
|
||||
|
||||
const code = wrapper.find('.tool-details code.hljs')
|
||||
const displayed = JSON.parse(code.text())
|
||||
expect(Array.isArray(displayed)).toBe(true)
|
||||
expect(displayed.at(-1)).toContain('chat.truncated')
|
||||
expect(code.text().length).toBeLessThanOrEqual(1000)
|
||||
})
|
||||
|
||||
it('copies the full large raw tool result even when the display is truncated', async () => {
|
||||
const writeText = vi.mocked(navigator.clipboard.writeText)
|
||||
const fullResult = 'line\n'.repeat(1200)
|
||||
@@ -177,9 +210,11 @@ describe('MessageItem tool details', () => {
|
||||
|
||||
await wrapper.find('.tool-line').trigger('click')
|
||||
|
||||
const displayedResult = fullResult.slice(0, 1000) + '\nchat.truncated'
|
||||
const code = wrapper.find('.tool-details code.hljs')
|
||||
expect(wrapper.find('.tool-details .code-lang').text()).toBe('text')
|
||||
expect(wrapper.html()).toContain('chat.truncated')
|
||||
expect(wrapper.find('.tool-details code.hljs').findAll('span')).toHaveLength(0)
|
||||
expect(code.text()).toBe(displayedResult)
|
||||
expect(code.findAll('span')).toHaveLength(0)
|
||||
|
||||
await wrapper.find('.tool-details [data-copy-code="true"]').trigger('click')
|
||||
expect(writeText).toHaveBeenCalledWith(fullResult)
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useTheme', () => ({
|
||||
useTheme: () => ({ isDark: false }),
|
||||
}))
|
||||
|
||||
import MessageList from '@/components/hermes/chat/MessageList.vue'
|
||||
import HistoryMessageList from '@/components/hermes/chat/HistoryMessageList.vue'
|
||||
import { useChatStore, type Message, type Session } from '@/stores/hermes/chat'
|
||||
import { useToolTraceVisibility } from '@/composables/useToolTraceVisibility'
|
||||
|
||||
const MessageItemStub = defineComponent({
|
||||
name: 'MessageItem',
|
||||
props: {
|
||||
message: { type: Object, required: true },
|
||||
highlight: { type: Boolean, default: false },
|
||||
},
|
||||
template: '<div class="stub-message" :data-role="message.role" :data-id="message.id">{{ message.toolName || message.content }}</div>',
|
||||
})
|
||||
|
||||
function makeSession(messages: Message[]): Session {
|
||||
return {
|
||||
id: 'session-1',
|
||||
title: 'Tool trace visibility',
|
||||
messages,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
const sampleMessages: Message[] = [
|
||||
{ id: 'user-1', role: 'user', content: 'inspect repo', timestamp: 1 },
|
||||
{ id: 'tool-named', role: 'tool', content: '', timestamp: 2, toolName: 'read_file', toolResult: 'ok', toolStatus: 'done' },
|
||||
{ id: 'tool-internal', role: 'tool', content: '', timestamp: 3, toolResult: 'internal', toolStatus: 'done' },
|
||||
{ id: 'assistant-1', role: 'assistant', content: 'done', timestamp: 4 },
|
||||
]
|
||||
|
||||
describe('tool trace visibility', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
localStorage.removeItem('hermes_show_tool_calls')
|
||||
useToolTraceVisibility().setToolTraceVisible(true)
|
||||
})
|
||||
|
||||
function mountLiveList() {
|
||||
const chatStore = useChatStore()
|
||||
chatStore.activeSessionId = 'session-1'
|
||||
chatStore.activeSession = makeSession(sampleMessages)
|
||||
chatStore.abortState = { aborting: true, synced: false }
|
||||
|
||||
return mount(MessageList, {
|
||||
global: {
|
||||
stubs: {
|
||||
MessageItem: MessageItemStub,
|
||||
Transition: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
it('shows named transcript and live tool traces by default while keeping unnamed internal tools hidden', () => {
|
||||
const wrapper = mountLiveList()
|
||||
|
||||
expect(wrapper.findAll('.stub-message').map(node => node.attributes('data-id'))).toEqual([
|
||||
'user-1',
|
||||
'tool-named',
|
||||
'assistant-1',
|
||||
])
|
||||
expect(wrapper.findAll('.tool-call-name').map(node => node.text())).toContain('read_file')
|
||||
})
|
||||
|
||||
it('applies the same default-visible rule to history sessions', () => {
|
||||
const wrapper = mount(HistoryMessageList, {
|
||||
props: { session: makeSession(sampleMessages) },
|
||||
global: {
|
||||
stubs: { MessageItem: MessageItemStub },
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('.stub-message').map(node => node.attributes('data-id'))).toEqual([
|
||||
'user-1',
|
||||
'tool-named',
|
||||
'assistant-1',
|
||||
])
|
||||
})
|
||||
|
||||
it('hides named live and history tool traces when the localStorage toggle is off', () => {
|
||||
useToolTraceVisibility().setToolTraceVisible(false)
|
||||
|
||||
const liveWrapper = mountLiveList()
|
||||
expect(liveWrapper.findAll('.stub-message').map(node => node.attributes('data-id'))).toEqual([
|
||||
'user-1',
|
||||
'assistant-1',
|
||||
])
|
||||
expect(liveWrapper.findAll('.tool-call-name').map(node => node.text())).not.toContain('read_file')
|
||||
|
||||
const historyWrapper = mount(HistoryMessageList, {
|
||||
props: { session: makeSession(sampleMessages) },
|
||||
global: {
|
||||
stubs: { MessageItem: MessageItemStub },
|
||||
},
|
||||
})
|
||||
expect(historyWrapper.findAll('.stub-message').map(node => node.attributes('data-id'))).toEqual([
|
||||
'user-1',
|
||||
'assistant-1',
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user