Skip to content

Commit ff33a7e

Browse files
author
IM.codes
committed
fix: show tool timestamps without copying them
1 parent 487b7df commit ff33a7e

3 files changed

Lines changed: 67 additions & 2 deletions

File tree

web/src/components/ChatView.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ function buildViewItems(events: TimelineEvent[]): ViewItem[] {
332332
|| summarizeToolInput((next.payload.detail as any)?.input, next.payload.detail);
333333
const input = inputText ? ` ${inputText}` : '';
334334
const status = next.payload.error ? `✗ ${String(next.payload.error)}` : '✓';
335-
const output = !next.payload.error && next.payload.output ? String(next.payload.output) : undefined;
335+
const output = !next.payload.error ? formatToolPayloadValue(next.payload.output) : undefined;
336336
consolidated.push({
337337
...ev,
338338
type: 'tool.call',
@@ -1396,7 +1396,7 @@ function ToolCallGroup({
13961396
</button>
13971397
)
13981398
)}
1399-
{last && <ChatEvent event={last} onPathClick={onPathClick} onDownload={onDownload} serverId={serverId} />}
1399+
{last && <ChatEvent event={last} onPathClick={onPathClick} onDownload={onDownload} serverId={serverId} showTime />}
14001400
{expanded && middle.length > 0 && (
14011401
<button class="chat-tool-fold-btn" onClick={() => setExpanded(false)}>
14021402
{t('chat.tool_group_collapse')}
@@ -1483,6 +1483,7 @@ const ChatEvent = memo(function ChatEvent({
14831483
onDownload,
14841484
serverId,
14851485
onResendFailed,
1486+
showTime,
14861487
}: {
14871488
event: TimelineEvent;
14881489
nextTs?: number;
@@ -1491,6 +1492,7 @@ const ChatEvent = memo(function ChatEvent({
14911492
onDownload?: (path: string) => void;
14921493
serverId?: string;
14931494
onResendFailed?: (commandId: string, text: string) => void;
1495+
showTime?: boolean;
14941496
}) {
14951497
const { t } = useTranslation();
14961498
switch (event.type) {
@@ -1550,6 +1552,7 @@ const ChatEvent = memo(function ChatEvent({
15501552
case 'tool.call': {
15511553
const callDetail = event.payload._callDetail ?? event.payload.detail;
15521554
const resultDetail = event.payload._resultDetail;
1555+
const shouldShowTime = showTime || event.payload._merged === true;
15531556
// Fall back to result detail for input — transport SDK tool.call may arrive without input
15541557
const toolInput = summarizeToolInput(event.payload.input, callDetail)
15551558
|| summarizeToolInput((resultDetail as any)?.input, resultDetail);
@@ -1560,6 +1563,7 @@ const ChatEvent = memo(function ChatEvent({
15601563
<span class="chat-tool-icon">{'>'}</span>
15611564
<span class="chat-tool-name">{String(event.payload.tool ?? 'tool')}</span>
15621565
{toolInput && <span class="chat-tool-input">{' '}{splitPathsAndUrls(toolInput, onPathClick, undefined, onDownload)}</span>}
1566+
{shouldShowTime && <span class="chat-bubble-time" style={{ display: 'inline', margin: 0 }}>{new Date(event.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>}
15631567
</div>
15641568
{toolOutput && (
15651569
<div class="chat-event chat-tool chat-tool-result-preview">
@@ -1595,6 +1599,7 @@ const ChatEvent = memo(function ChatEvent({
15951599
) : (
15961600
<span class="chat-tool-output">done</span>
15971601
)}
1602+
{showTime && <span class="chat-bubble-time" style={{ display: 'inline', margin: 0 }}>{new Date(event.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>}
15981603
</div>
15991604
{detail && (
16001605
<details class="chat-tool-detail">

web/test/chat-view-tool-format.test.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,27 @@ describe('ChatView tool payload formatting', () => {
151151
expect(screen.getByText('output')).toBeDefined();
152152
});
153153

154+
it('shows a single timestamp on the final merged tool row', () => {
155+
const events = [
156+
makeEvent({
157+
eventId: 'tool-group-call',
158+
type: 'tool.call',
159+
ts: 1_000,
160+
payload: { tool: 'Read', input: { file_path: 'README.md' } },
161+
}),
162+
makeEvent({
163+
eventId: 'tool-group-result',
164+
type: 'tool.result',
165+
ts: 2_000,
166+
payload: { output: { path: '/tmp/README.md' } },
167+
}),
168+
];
169+
170+
const { container } = render(<ChatView events={events} loading={false} />);
171+
172+
expect(container.querySelectorAll('.chat-tool .chat-bubble-time')).toHaveLength(1);
173+
});
174+
154175
it('renders tool-call summary from detail.input when live payload.input is missing', () => {
155176
const events = [
156177
makeEvent({

web/test/components/ChatView.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,4 +886,43 @@ describe('ChatView', () => {
886886
if (hadTouchStart) (window as Window & { ontouchstart?: unknown }).ontouchstart = originalTouchStart;
887887
}
888888
});
889+
890+
it('copies the last tool event without the trailing timestamp from the context menu', async () => {
891+
const hadTouchStart = 'ontouchstart' in window;
892+
const originalTouchStart = (window as Window & { ontouchstart?: unknown }).ontouchstart;
893+
if (hadTouchStart) delete (window as Window & { ontouchstart?: unknown }).ontouchstart;
894+
try {
895+
const { container, getByText } = render(
896+
<ChatView
897+
events={[
898+
{
899+
eventId: 'evt-tool-copy-call',
900+
type: 'tool.call',
901+
ts: new Date('2026-04-17T12:34:00Z').getTime(),
902+
payload: { tool: 'Read', input: { file_path: 'README.md' } },
903+
},
904+
{
905+
eventId: 'evt-tool-copy-result',
906+
type: 'tool.result',
907+
ts: new Date('2026-04-17T12:35:00Z').getTime(),
908+
payload: { output: { path: '/tmp/README.md' } },
909+
},
910+
] as any}
911+
loading={false}
912+
sessionId="deck_main_brain"
913+
/>,
914+
);
915+
916+
const toolEvents = container.querySelectorAll('.chat-event.chat-tool');
917+
const lastToolEvent = toolEvents[toolEvents.length - 1] as HTMLElement;
918+
fireEvent.contextMenu(lastToolEvent, { clientX: 40, clientY: 40 });
919+
fireEvent.click(getByText('common.copy'));
920+
921+
await waitFor(() => {
922+
expect(clipboardWriteText).toHaveBeenCalledWith('/tmp/README.md');
923+
});
924+
} finally {
925+
if (hadTouchStart) (window as Window & { ontouchstart?: unknown }).ontouchstart = originalTouchStart;
926+
}
927+
});
889928
});

0 commit comments

Comments
 (0)