[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:
ekko
2026-05-17 09:01:59 +08:00
committed by GitHub
parent 569ddc28da
commit 0c2bafc619
19 changed files with 975 additions and 29 deletions
+39 -4
View File
@@ -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)
+118
View File
@@ -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',
])
})
})