diff --git a/docs/chat-chain-changes/2026-10-22-thinking-tool-width.md b/docs/chat-chain-changes/2026-10-22-thinking-tool-width.md new file mode 100644 index 0000000..33b842a --- /dev/null +++ b/docs/chat-chain-changes/2026-10-22-thinking-tool-width.md @@ -0,0 +1,16 @@ +--- +date: 2026-10-22 +feature: Thinking tool call row width +impact: Use the available chat width instead of a fixed 520px row +--- + +PR/commit: pending. + +MessageList tool-call rows now use 100% of their panel width. The existing +flex shrinking, ellipsis and max-width rules keep long arguments within the +viewport. This also applies to the compression and abort status rows sharing +the same class. Assistant reply sizing and chat events are unchanged. + +Validation: Playwright reproduces the fixed-width failure and passes after the +change at 1920px, 900px and 390px, including long tool arguments and overflow +checks. Production build and harness checks pass. diff --git a/packages/client/src/components/hermes/chat/MessageList.vue b/packages/client/src/components/hermes/chat/MessageList.vue index 7f10d99..299d9f8 100644 --- a/packages/client/src/components/hermes/chat/MessageList.vue +++ b/packages/client/src/components/hermes/chat/MessageList.vue @@ -1502,7 +1502,7 @@ defineExpose({ display: flex; align-items: center; gap: 6px; - width: 520px; + width: 100%; max-width: 100%; min-width: 0; box-sizing: border-box; diff --git a/tests/e2e/thinking-panel-width.spec.ts b/tests/e2e/thinking-panel-width.spec.ts new file mode 100644 index 0000000..7f5cb30 --- /dev/null +++ b/tests/e2e/thinking-panel-width.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test' +import { authenticate, mockChatSocket, mockHermesApi, TEST_ACCESS_KEY } from './fixtures' + +test('thinking tool rows fill the chat width and shrink without overflowing', async ({ page }) => { + await page.setViewportSize({ width: 1920, height: 1080 }) + await authenticate(page, TEST_ACCESS_KEY, 'research') + await mockHermesApi(page) + await mockChatSocket(page) + await page.goto('/#/hermes/chat') + await page.locator('textarea.input-textarea').fill('Inspect the working directory') + await page.getByRole('button', { name: 'Send', exact: true }).click() + await page.waitForFunction(() => (window as any).__PW_CHAT_SOCKET__?.emitted.some((event: any) => event.event === 'run')) + await page.evaluate(() => { + const state = (window as any).__PW_CHAT_SOCKET__ + const sid = state.emitted.find((event: any) => event.event === 'run').payload.session_id + const common = { session_id: sid, run_id: 'width-run' } + state.latest.__trigger('run.started', { ...common, event: 'run.started' }) + state.latest.__trigger('tool.started', { + ...common, event: 'tool.started', tool_call_id: 'width-tool', tool: 'terminal', + preview: 'Read a long project path and inspect the current repository state '.repeat(30), + arguments: JSON.stringify({ command: 'git status --short' }), + }) + }) + const panel = page.locator('.tool-calls-panel') + const row = panel.locator('.tool-call-item').first() + await expect(row).toBeVisible() + for (const width of [1920, 900, 390]) { + await page.setViewportSize({ width, height: 1080 }) + await expect.poll(async () => { + const panelBox = await panel.boundingBox() + const rowBox = await row.boundingBox() + return panelBox && rowBox ? Math.abs(rowBox.width - panelBox.width) : Infinity + }).toBeLessThan(2) + const fits = await panel.evaluate(element => element.scrollWidth <= element.clientWidth + 1) + expect(fits).toBe(true) + const box = await row.boundingBox() + expect(box!.x + box!.width).toBeLessThanOrEqual(width) + } +})