Buckets:
| // Guards the exec_shell_command title while the call is still being | |
| // generated: the command must stream into the terminal title from the | |
| // first character (args arrive as partial JSON), with a blinking cursor | |
| // while the args are incomplete, and the conversation cwd standing in | |
| // for the x-tool-cwd value that only arrives with the result message. | |
| import ChatMessageToolCallBlockExecShellCommand from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockExecShellCommand.svelte'; | |
| import ChatMessageAgenticContent from '$lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte'; | |
| import { SETTINGS_KEYS } from '$lib/constants'; | |
| import { AgenticSectionType, BuiltInTool, MessageRole, MessageType } from '$lib/enums'; | |
| import { conversationsStore, settingsStore } from '$lib/stores'; | |
| import type { AgenticSection, DatabaseMessage } from '$lib/types'; | |
| import { tick } from 'svelte'; | |
| import { afterEach, beforeEach, describe, expect, it } from 'vitest'; | |
| import { render } from 'vitest-browser-svelte'; | |
| const CONV_ID = 'conv-1'; | |
| const TOOL_CALL_ID = 'call-1'; | |
| const TOOL_NAME = BuiltInTool.SERVER_EXEC_SHELL_COMMAND; | |
| const CWD = '/home/user/project'; | |
| function messageWithArgs(args: string): DatabaseMessage { | |
| return { | |
| children: [], | |
| content: '', | |
| convId: CONV_ID, | |
| id: 'assistant-1', | |
| parent: null, | |
| role: MessageRole.ASSISTANT, | |
| timestamp: 0, | |
| toolCalls: JSON.stringify([ | |
| { function: { arguments: args, name: TOOL_NAME }, id: TOOL_CALL_ID } | |
| ]), | |
| type: MessageType.TEXT | |
| }; | |
| } | |
| function toolResultMessage(content: string, toolCwd?: string): DatabaseMessage { | |
| return { | |
| children: [], | |
| content, | |
| convId: CONV_ID, | |
| id: 'tool-1', | |
| parent: null, | |
| role: MessageRole.TOOL, | |
| timestamp: 0, | |
| toolCallId: TOOL_CALL_ID, | |
| toolCalls: '', | |
| toolCwd, | |
| type: MessageType.TEXT | |
| }; | |
| } | |
| function triggerText(container: Element): string { | |
| return container.querySelector('[data-slot="collapsible-trigger"]')?.textContent ?? ''; | |
| } | |
| function hasCursor(container: Element): boolean { | |
| return container.querySelector('.exec-cursor') !== null; | |
| } | |
| describe('exec_shell_command streaming title', () => { | |
| beforeEach(() => { | |
| settingsStore.updateMultipleConfig({ | |
| [SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT]: false, | |
| [SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS]: true | |
| }); | |
| conversationsStore.activeConversation = { | |
| currNode: null, | |
| cwd: CWD, | |
| id: CONV_ID, | |
| lastModified: 0, | |
| name: 'test' | |
| }; | |
| }); | |
| afterEach(() => { | |
| conversationsStore.activeConversation = null; | |
| conversationsStore.activeMessages = []; | |
| }); | |
| it('streams the partial command into the title with a cursor', async () => { | |
| const { container } = render(ChatMessageAgenticContent, { | |
| isStreaming: true, | |
| message: messageWithArgs('{"command":"git sta'), | |
| toolMessages: [] | |
| }); | |
| await tick(); | |
| const title = triggerText(container); | |
| expect(title).toContain('git sta'); | |
| expect(title).toContain('$'); | |
| expect(title).toContain(CWD); | |
| expect(hasCursor(container)).toBe(true); | |
| }); | |
| it('drops the cursor once the args JSON is complete', async () => { | |
| const screen = render(ChatMessageAgenticContent, { | |
| isStreaming: true, | |
| message: messageWithArgs('{"command":"git sta'), | |
| toolMessages: [] | |
| }); | |
| await tick(); | |
| expect(hasCursor(screen.container)).toBe(true); | |
| screen.rerender({ | |
| isStreaming: true, | |
| message: messageWithArgs('{"command":"git status"}'), | |
| toolMessages: [] | |
| }); | |
| await tick(); | |
| expect(triggerText(screen.container)).toContain('git status'); | |
| expect(hasCursor(screen.container)).toBe(false); | |
| }); | |
| it('switches the prompt from the conversation cwd to the persisted one', async () => { | |
| const screen = render(ChatMessageAgenticContent, { | |
| isStreaming: true, | |
| message: messageWithArgs('{"command":"git status"}'), | |
| toolMessages: [] | |
| }); | |
| await tick(); | |
| expect(triggerText(screen.container)).toContain(CWD); | |
| // Result message lands: its x-tool-cwd takes over the prompt. | |
| screen.rerender({ | |
| isStreaming: true, | |
| message: messageWithArgs('{"command":"git status"}'), | |
| toolMessages: [toolResultMessage('', '/other/dir')] | |
| }); | |
| await tick(); | |
| expect(triggerText(screen.container)).toContain('/other/dir'); | |
| expect(triggerText(screen.container)).not.toContain(CWD); | |
| }); | |
| it('shows the executing label only after the command finishes streaming', async () => { | |
| const screen = render(ChatMessageAgenticContent, { | |
| isStreaming: true, | |
| message: messageWithArgs('{"command":"git sta'), | |
| toolMessages: [] | |
| }); | |
| await tick(); | |
| // While the args stream, the title cursor alone signals activity. | |
| expect(screen.container.textContent).not.toContain('Executing command...'); | |
| screen.rerender({ | |
| isStreaming: true, | |
| message: messageWithArgs('{"command":"git status"}'), | |
| toolMessages: [] | |
| }); | |
| await tick(); | |
| expect(screen.container.textContent).toContain('Executing command...'); | |
| }); | |
| it('shows Executing command... while executing before the first output chunk', async () => { | |
| // The result message is created empty right before execution, so | |
| // the block must not go blank while the process produces nothing. | |
| const section: AgenticSection = { | |
| content: '', | |
| toolArgs: '{"command":"sleep 1"}', | |
| toolCallId: TOOL_CALL_ID, | |
| toolName: TOOL_NAME, | |
| toolResult: '', | |
| type: AgenticSectionType.TOOL_CALL | |
| }; | |
| const { container } = render(ChatMessageToolCallBlockExecShellCommand, { | |
| isExecuting: true, | |
| isStreaming: true, | |
| open: true, | |
| section | |
| }); | |
| await tick(); | |
| expect(container.textContent).toContain('Executing command...'); | |
| }); | |
| it('updates the title when the store mutates toolCalls in place, as the stream does', async () => { | |
| // The live flow never swaps the message object: onToolCallsStreaming | |
| // writes growing toolCalls strings into the same store row per delta. | |
| conversationsStore.activeMessages = [messageWithArgs('')]; | |
| const screen = render(ChatMessageAgenticContent, { | |
| isStreaming: true, | |
| message: conversationsStore.activeMessages[0]!, | |
| toolMessages: [] | |
| }); | |
| await tick(); | |
| const idx = conversationsStore.findMessageIndex('assistant-1'); | |
| for (const args of ['{"command', '{"command":"git', '{"command":"git sta']) { | |
| conversationsStore.updateMessageAtIndex(idx, { | |
| toolCalls: JSON.stringify([ | |
| { function: { arguments: args, name: TOOL_NAME }, id: TOOL_CALL_ID } | |
| ]) | |
| }); | |
| await tick(); | |
| expect(triggerText(screen.container)).toContain(args.slice('{"command":"'.length)); | |
| expect(hasCursor(screen.container)).toBe(true); | |
| } | |
| }); | |
| }); | |
Xet Storage Details
- Size:
- 6.56 kB
- Xet hash:
- b9f9dc5f7eb01534538c16c5ce2915e77e07f4a1703eef48690531005ce5e042
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.