chenbhao commited on
Commit
3d0fee2
·
1 Parent(s): c376933

feat: better collapsing fetch result

Browse files
src/components/messages/CollapsedReadSearchContent.tsx CHANGED
@@ -159,6 +159,7 @@ export function CollapsedReadSearchContent({
159
  memoryWriteCount,
160
  messages: groupMessages
161
  } = message;
 
162
  const [theme] = useTheme();
163
  const toolUseIds = getToolUseIdsFromCollapsedGroup(message);
164
  const anyError = toolUseIds.some(id => lookups.erroredToolUseIDs.has(id));
@@ -173,21 +174,24 @@ export function CollapsedReadSearchContent({
173
  const maxListCountRef = useRef(0);
174
  const maxMcpCountRef = useRef(0);
175
  const maxBashCountRef = useRef(0);
 
176
  maxReadCountRef.current = Math.max(maxReadCountRef.current, rawReadCount);
177
  maxSearchCountRef.current = Math.max(maxSearchCountRef.current, rawSearchCount);
178
  maxListCountRef.current = Math.max(maxListCountRef.current, rawListCount);
179
  maxMcpCountRef.current = Math.max(maxMcpCountRef.current, message.mcpCallCount ?? 0);
180
  maxBashCountRef.current = Math.max(maxBashCountRef.current, message.bashCount ?? 0);
 
181
  const readCount = maxReadCountRef.current;
182
  const searchCount = maxSearchCountRef.current;
183
  const listCount = maxListCountRef.current;
184
  const mcpCallCount = maxMcpCountRef.current;
 
185
  // Subtract commands surfaced as "Committed …" / "Created PR …" so the
186
  // same command isn't counted twice. gitOpBashCount is read live (no max-ref
187
  // needed — it's 0 until results arrive, then only grows).
188
  const gitOpBashCount = message.gitOpBashCount ?? 0;
189
  const bashCount = isFullscreenEnvEnabled() ? Math.max(0, maxBashCountRef.current - gitOpBashCount) : 0;
190
- const hasNonMemoryOps = searchCount > 0 || readCount > 0 || listCount > 0 || replCount > 0 || mcpCallCount > 0 || bashCount > 0 || gitOpBashCount > 0;
191
  const readPaths = message.readFilePaths;
192
  const searchArgs = message.searchArgs;
193
  let incomingHint = message.latestDisplayHint;
@@ -411,37 +415,48 @@ export function CollapsedReadSearchContent({
411
  {bashCount === 1 ? 'command' : 'commands'}
412
  </Text>);
413
  }
 
 
 
 
 
 
 
 
 
 
 
414
 
415
  // Build memory parts (auto-memory) — rendered after nonMemParts
416
  const hasPrecedingNonMem = nonMemParts.length > 0;
417
  const memParts: React.ReactNode[] = [];
418
  if (memoryReadCount > 0) {
419
- const isFirst_5 = !hasPrecedingNonMem && memParts.length === 0;
420
- const verb_2 = isActiveGroup ? isFirst_5 ? 'Recalling' : 'recalling' : isFirst_5 ? 'Recalled' : 'recalled';
421
- if (!isFirst_5) {
422
  memParts.push(<Text key="comma-mr">, </Text>);
423
  }
424
  memParts.push(<Text key="mem-read">
425
- {verb_2} <Text bold>{memoryReadCount}</Text>{' '}
426
  {memoryReadCount === 1 ? 'memory' : 'memories'}
427
  </Text>);
428
  }
429
  if (memorySearchCount > 0) {
430
- const isFirst_6 = !hasPrecedingNonMem && memParts.length === 0;
431
- const verb_3 = isActiveGroup ? isFirst_6 ? 'Searching' : 'searching' : isFirst_6 ? 'Searched' : 'searched';
432
- if (!isFirst_6) {
433
  memParts.push(<Text key="comma-ms">, </Text>);
434
  }
435
- memParts.push(<Text key="mem-search">{`${verb_3} memories`}</Text>);
436
  }
437
  if (memoryWriteCount > 0) {
438
- const isFirst_7 = !hasPrecedingNonMem && memParts.length === 0;
439
- const verb_4 = isActiveGroup ? isFirst_7 ? 'Writing' : 'writing' : isFirst_7 ? 'Wrote' : 'wrote';
440
- if (!isFirst_7) {
441
  memParts.push(<Text key="comma-mw">, </Text>);
442
  }
443
  memParts.push(<Text key="mem-write">
444
- {verb_4} <Text bold>{memoryWriteCount}</Text>{' '}
445
  {memoryWriteCount === 1 ? 'memory' : 'memories'}
446
  </Text>);
447
  }
 
159
  memoryWriteCount,
160
  messages: groupMessages
161
  } = message;
162
+ const rawWebFetchCount = message.webFetchCount ?? 0;
163
  const [theme] = useTheme();
164
  const toolUseIds = getToolUseIdsFromCollapsedGroup(message);
165
  const anyError = toolUseIds.some(id => lookups.erroredToolUseIDs.has(id));
 
174
  const maxListCountRef = useRef(0);
175
  const maxMcpCountRef = useRef(0);
176
  const maxBashCountRef = useRef(0);
177
+ const maxWebFetchCountRef = useRef(0);
178
  maxReadCountRef.current = Math.max(maxReadCountRef.current, rawReadCount);
179
  maxSearchCountRef.current = Math.max(maxSearchCountRef.current, rawSearchCount);
180
  maxListCountRef.current = Math.max(maxListCountRef.current, rawListCount);
181
  maxMcpCountRef.current = Math.max(maxMcpCountRef.current, message.mcpCallCount ?? 0);
182
  maxBashCountRef.current = Math.max(maxBashCountRef.current, message.bashCount ?? 0);
183
+ maxWebFetchCountRef.current = Math.max(maxWebFetchCountRef.current, rawWebFetchCount);
184
  const readCount = maxReadCountRef.current;
185
  const searchCount = maxSearchCountRef.current;
186
  const listCount = maxListCountRef.current;
187
  const mcpCallCount = maxMcpCountRef.current;
188
+ const webFetchCount = maxWebFetchCountRef.current;
189
  // Subtract commands surfaced as "Committed …" / "Created PR …" so the
190
  // same command isn't counted twice. gitOpBashCount is read live (no max-ref
191
  // needed — it's 0 until results arrive, then only grows).
192
  const gitOpBashCount = message.gitOpBashCount ?? 0;
193
  const bashCount = isFullscreenEnvEnabled() ? Math.max(0, maxBashCountRef.current - gitOpBashCount) : 0;
194
+ const hasNonMemoryOps = searchCount > 0 || readCount > 0 || listCount > 0 || replCount > 0 || mcpCallCount > 0 || bashCount > 0 || gitOpBashCount > 0 || webFetchCount > 0;
195
  const readPaths = message.readFilePaths;
196
  const searchArgs = message.searchArgs;
197
  let incomingHint = message.latestDisplayHint;
 
415
  {bashCount === 1 ? 'command' : 'commands'}
416
  </Text>);
417
  }
418
+ if (webFetchCount > 0) {
419
+ const isFirst_5 = nonMemParts.length === 0;
420
+ const verb_2 = isActiveGroup ? isFirst_5 ? 'Fetching' : 'fetching' : isFirst_5 ? 'Fetched' : 'fetched';
421
+ if (!isFirst_5) {
422
+ nonMemParts.push(<Text key="comma-webfetch">, </Text>);
423
+ }
424
+ nonMemParts.push(<Text key="webfetch">
425
+ {verb_2} <Text bold>{webFetchCount}</Text> URL{' '}
426
+ {webFetchCount === 1 ? '' : 's'}
427
+ </Text>);
428
+ }
429
 
430
  // Build memory parts (auto-memory) — rendered after nonMemParts
431
  const hasPrecedingNonMem = nonMemParts.length > 0;
432
  const memParts: React.ReactNode[] = [];
433
  if (memoryReadCount > 0) {
434
+ const isFirst_6 = !hasPrecedingNonMem && memParts.length === 0;
435
+ const verb_3 = isActiveGroup ? isFirst_6 ? 'Recalling' : 'recalling' : isFirst_6 ? 'Recalled' : 'recalled';
436
+ if (!isFirst_6) {
437
  memParts.push(<Text key="comma-mr">, </Text>);
438
  }
439
  memParts.push(<Text key="mem-read">
440
+ {verb_3} <Text bold>{memoryReadCount}</Text>{' '}
441
  {memoryReadCount === 1 ? 'memory' : 'memories'}
442
  </Text>);
443
  }
444
  if (memorySearchCount > 0) {
445
+ const isFirst_7 = !hasPrecedingNonMem && memParts.length === 0;
446
+ const verb_4 = isActiveGroup ? isFirst_7 ? 'Searching' : 'searching' : isFirst_7 ? 'Searched' : 'searched';
447
+ if (!isFirst_7) {
448
  memParts.push(<Text key="comma-ms">, </Text>);
449
  }
450
+ memParts.push(<Text key="mem-search">{`${verb_4} memories`}</Text>);
451
  }
452
  if (memoryWriteCount > 0) {
453
+ const isFirst_8 = !hasPrecedingNonMem && memParts.length === 0;
454
+ const verb_5 = isActiveGroup ? isFirst_8 ? 'Writing' : 'writing' : isFirst_8 ? 'Wrote' : 'wrote';
455
+ if (!isFirst_8) {
456
  memParts.push(<Text key="comma-mw">, </Text>);
457
  }
458
  memParts.push(<Text key="mem-write">
459
+ {verb_5} <Text bold>{memoryWriteCount}</Text>{' '}
460
  {memoryWriteCount === 1 ? 'memory' : 'memories'}
461
  </Text>);
462
  }
src/types/message.ts ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { UUID } from 'crypto'
2
+ import type {
3
+ ContentBlockParam,
4
+ MessageParam,
5
+ } from '@anthropic-ai/sdk/resources/messages.mjs'
6
+
7
+ export type MessageOrigin = 'user' | 'assistant' | 'system'
8
+
9
+ export type Message = {
10
+ role: 'user' | 'assistant' | 'system'
11
+ content: ContentBlockParam[]
12
+ uuid: string
13
+ timestamp: number
14
+ origin?: MessageOrigin
15
+ }
16
+
17
+ export type UserMessage = {
18
+ role: 'user'
19
+ content: ContentBlockParam[]
20
+ uuid: string
21
+ timestamp: number
22
+ origin?: MessageOrigin
23
+ }
24
+
25
+ export type AssistantMessage = {
26
+ role: 'assistant'
27
+ content: ContentBlockParam[]
28
+ uuid: string
29
+ timestamp: number
30
+ origin?: MessageOrigin
31
+ model?: string
32
+ }
33
+
34
+ export type NormalizedMessage =
35
+ | NormalizedUserMessage
36
+ | NormalizedAssistantMessage
37
+
38
+ export type NormalizedUserMessage = {
39
+ type: 'user'
40
+ message: UserMessage
41
+ uuid: string
42
+ timestamp: number
43
+ origin?: MessageOrigin
44
+ toolUseResult?: { stdout?: string; stderr?: string }
45
+ }
46
+
47
+ export type NormalizedAssistantMessage = {
48
+ type: 'assistant'
49
+ message: AssistantMessage
50
+ uuid: string
51
+ timestamp: number
52
+ origin?: MessageOrigin
53
+ model?: string
54
+ }
55
+
56
+ export type SystemMessage = {
57
+ type: 'system'
58
+ subtype: string
59
+ uuid: string
60
+ timestamp: number
61
+ level?: string
62
+ text?: string
63
+ }
64
+
65
+ export type SystemInformationalMessage = SystemMessage & {
66
+ subtype: 'informational'
67
+ text: string
68
+ }
69
+
70
+ export type SystemAPIErrorMessage = SystemMessage & {
71
+ subtype: 'api_error'
72
+ error: string
73
+ }
74
+
75
+ export type StopHookInfo = {
76
+ command: string
77
+ durationMs?: number
78
+ hookName?: string
79
+ }
80
+
81
+ export type SystemStopHookSummaryMessage = SystemMessage & {
82
+ subtype: 'stop_hook_summary'
83
+ hookLabel: string
84
+ hookCount: number
85
+ totalDurationMs?: number
86
+ hookInfos: StopHookInfo[]
87
+ }
88
+
89
+ export type SystemAgentsKilledMessage = SystemMessage & {
90
+ subtype: 'agents_killed'
91
+ }
92
+
93
+ export type SystemApiMetricsMessage = SystemMessage & {
94
+ subtype: 'api_metrics'
95
+ }
96
+
97
+ export type SystemAwaySummaryMessage = SystemMessage & {
98
+ subtype: 'away_summary'
99
+ }
100
+
101
+ export type SystemBridgeStatusMessage = SystemMessage & {
102
+ subtype: 'bridge_status'
103
+ }
104
+
105
+ export type SystemCompactBoundaryMessage = SystemMessage & {
106
+ subtype: 'compact_boundary'
107
+ }
108
+
109
+ export type SystemLocalCommandMessage = SystemMessage & {
110
+ subtype: 'local_command'
111
+ }
112
+
113
+ export type SystemMemorySavedMessage = SystemMessage & {
114
+ subtype: 'memory_saved'
115
+ }
116
+
117
+ export type SystemMicrocompactBoundaryMessage = SystemMessage & {
118
+ subtype: 'microcompact_boundary'
119
+ }
120
+
121
+ export type SystemPermissionRetryMessage = SystemMessage & {
122
+ subtype: 'permission_retry'
123
+ }
124
+
125
+ export type SystemScheduledTaskFireMessage = SystemMessage & {
126
+ subtype: 'scheduled_task_fire'
127
+ }
128
+
129
+ export type SystemTurnDurationMessage = SystemMessage & {
130
+ subtype: 'turn_duration'
131
+ }
132
+
133
+ export type ProgressMessage = {
134
+ type: 'progress'
135
+ uuid: string
136
+ timestamp: number
137
+ data?: {
138
+ type: string
139
+ phase?: string
140
+ toolName?: string
141
+ toolInput?: unknown
142
+ elapsedTimeSeconds?: number
143
+ totalLines?: number
144
+ }
145
+ }
146
+
147
+ export type HookResultMessage = {
148
+ type: 'hook_result'
149
+ uuid: string
150
+ timestamp: number
151
+ hookName: string
152
+ }
153
+
154
+ export type Attachment = {
155
+ type: string
156
+ memories?: { path: string; content: string; mtimeMs: number }[]
157
+ }
158
+
159
+ export type AttachmentMessage = {
160
+ type: 'attachment'
161
+ attachment: Attachment
162
+ uuid: string
163
+ timestamp: number
164
+ }
165
+
166
+ export type GroupedToolUseMessage = {
167
+ type: 'grouped_tool_use'
168
+ toolName: string
169
+ messages: NormalizedAssistantMessage[]
170
+ uuid: string
171
+ timestamp: number
172
+ displayMessage: NormalizedAssistantMessage
173
+ }
174
+
175
+ export type CollapsibleMessage =
176
+ | NormalizedAssistantMessage
177
+ | GroupedToolUseMessage
178
+ | NormalizedUserMessage
179
+
180
+ export type RenderableMessage =
181
+ | NormalizedUserMessage
182
+ | NormalizedAssistantMessage
183
+ | SystemMessage
184
+ | AttachmentMessage
185
+ | GroupedToolUseMessage
186
+ | ProgressMessage
187
+
188
+ export type TombstoneMessage = SystemMessage & {
189
+ subtype: 'tombstone'
190
+ }
191
+
192
+ export type ToolUseSummaryMessage = {
193
+ type: 'tool_use_summary'
194
+ uuid: string
195
+ timestamp: number
196
+ }
197
+
198
+ export type CollapsedReadSearchGroup = {
199
+ type: 'collapsed_read_search'
200
+ searchCount: number
201
+ readCount: number
202
+ listCount: number
203
+ replCount: number
204
+ memorySearchCount: number
205
+ memoryReadCount: number
206
+ memoryWriteCount: number
207
+ readFilePaths: string[]
208
+ searchArgs: string[]
209
+ latestDisplayHint?: string
210
+ messages: CollapsibleMessage[]
211
+ displayMessage: CollapsibleMessage
212
+ uuid: string
213
+ timestamp: number
214
+ mcpCallCount?: number
215
+ mcpServerNames?: string[]
216
+ bashCount?: number
217
+ gitOpBashCount?: number
218
+ commits?: { sha: string; kind: string }[]
219
+ pushes?: { branch: string }[]
220
+ branches?: { ref: string; action: string }[]
221
+ prs?: { number: number; url?: string; action: string }[]
222
+ hookTotalMs?: number
223
+ hookCount?: number
224
+ hookInfos?: StopHookInfo[]
225
+ relevantMemories?: { path: string; content: string; mtimeMs: number }[]
226
+ teamMemorySearchCount?: number
227
+ teamMemoryReadCount?: number
228
+ teamMemoryWriteCount?: number
229
+ webFetchCount?: number
230
+ webFetchURLs?: string[]
231
+ }
232
+
233
+ export type PartialCompactDirection = 'forward' | 'backward'
234
+
235
+ export type RequestStartEvent = {
236
+ type: 'request_start'
237
+ timestamp: number
238
+ }
239
+
240
+ export type StreamEvent =
241
+ | { type: 'message_start'; message: MessageParam }
242
+ | { type: 'content_block_start'; content_block: ContentBlockParam }
243
+ | { type: 'content_block_delta'; delta: unknown }
244
+ | { type: 'content_block_stop' }
245
+ | { type: 'message_delta'; usage: unknown }
246
+ | { type: 'message_stop' }
src/utils/collapseReadSearch.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
  type PrAction,
15
  } from '../tools/shared/gitOperationTracking.js'
16
  import { TOOL_SEARCH_TOOL_NAME } from '../tools/ToolSearchTool/prompt.js'
 
17
  import type {
18
  CollapsedReadSearchGroup,
19
  CollapsibleMessage,
@@ -161,6 +162,21 @@ export function getToolSearchOrReadInfo(
161
  }
162
  }
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  // Memory file writes/edits are collapsible
165
  if (isMemoryWriteOrEdit(toolName, toolInput)) {
166
  return {
@@ -620,6 +636,9 @@ type GroupAccumulator = {
620
  // memories, not explicit Read calls). Paths mirrored into readFilePaths +
621
  // memoryReadFilePaths so the inline "recalled N memories" text is accurate.
622
  relevantMemories?: { path: string; content: string; mtimeMs: number }[]
 
 
 
623
  }
624
 
625
  function createEmptyGroup(): GroupAccumulator {
@@ -638,6 +657,8 @@ function createEmptyGroup(): GroupAccumulator {
638
  hookTotalMs: 0,
639
  hookCount: 0,
640
  hookInfos: [],
 
 
641
  }
642
  if (feature('TEAMMEM')) {
643
  group.teamMemorySearchCount = 0
@@ -748,6 +769,10 @@ function createCollapsedGroup(
748
  if (group.relevantMemories && group.relevantMemories.length > 0) {
749
  result.relevantMemories = group.relevantMemories
750
  }
 
 
 
 
751
  return result
752
  }
753
 
@@ -800,6 +825,16 @@ export function collapseReadSearchGroups(
800
  // Snip/ToolSearch absorbed silently — no count, no summary text.
801
  // Hidden from the default view but still shown in verbose mode
802
  // (Ctrl+O) via the groupMessages iteration in CollapsedReadSearchContent.
 
 
 
 
 
 
 
 
 
 
803
  } else if (toolInfo.mcpServerName) {
804
  // MCP search/read — counted separately so the summary says
805
  // "Queried slack N times" instead of "Read N files".
 
14
  type PrAction,
15
  } from '../tools/shared/gitOperationTracking.js'
16
  import { TOOL_SEARCH_TOOL_NAME } from '../tools/ToolSearchTool/prompt.js'
17
+ import { WEB_FETCH_TOOL_NAME } from '../tools/WebFetchTool/prompt.js'
18
  import type {
19
  CollapsedReadSearchGroup,
20
  CollapsibleMessage,
 
162
  }
163
  }
164
 
165
+ // WebFetch is absorbed silently — counted separately so display says
166
+ // "Fetched N URLs" instead of "Read N files". Remains visible in verbose
167
+ // mode via the groupMessages iteration.
168
+ if (toolName === WEB_FETCH_TOOL_NAME) {
169
+ return {
170
+ isCollapsible: true,
171
+ isSearch: false,
172
+ isRead: false,
173
+ isList: false,
174
+ isREPL: false,
175
+ isMemoryWrite: false,
176
+ isAbsorbedSilently: false,
177
+ }
178
+ }
179
+
180
  // Memory file writes/edits are collapsible
181
  if (isMemoryWriteOrEdit(toolName, toolInput)) {
182
  return {
 
636
  // memories, not explicit Read calls). Paths mirrored into readFilePaths +
637
  // memoryReadFilePaths so the inline "recalled N memories" text is accurate.
638
  relevantMemories?: { path: string; content: string; mtimeMs: number }[]
639
+ // WebFetch operations (tracked separately for "Fetched N URLs")
640
+ webFetchCount?: number
641
+ webFetchURLs?: string[]
642
  }
643
 
644
  function createEmptyGroup(): GroupAccumulator {
 
657
  hookTotalMs: 0,
658
  hookCount: 0,
659
  hookInfos: [],
660
+ webFetchCount: 0,
661
+ webFetchURLs: [],
662
  }
663
  if (feature('TEAMMEM')) {
664
  group.teamMemorySearchCount = 0
 
769
  if (group.relevantMemories && group.relevantMemories.length > 0) {
770
  result.relevantMemories = group.relevantMemories
771
  }
772
+ if ((group.webFetchCount ?? 0) > 0) {
773
+ result.webFetchCount = group.webFetchCount
774
+ result.webFetchURLs = group.webFetchURLs
775
+ }
776
  return result
777
  }
778
 
 
825
  // Snip/ToolSearch absorbed silently — no count, no summary text.
826
  // Hidden from the default view but still shown in verbose mode
827
  // (Ctrl+O) via the groupMessages iteration in CollapsedReadSearchContent.
828
+ } else if (toolInfo.name === WEB_FETCH_TOOL_NAME) {
829
+ // WebFetch — counted separately so the summary says
830
+ // "Fetched N URLs" instead of "Read N files".
831
+ const count = countToolUses(msg)
832
+ currentGroup.webFetchCount = (currentGroup.webFetchCount ?? 0) + count
833
+ const input = toolInfo.input as { url?: string } | undefined
834
+ if (input?.url) {
835
+ currentGroup.webFetchURLs?.push(input.url)
836
+ currentGroup.latestDisplayHint = input.url
837
+ }
838
  } else if (toolInfo.mcpServerName) {
839
  // MCP search/read — counted separately so the summary says
840
  // "Queried slack N times" instead of "Read N files".