@vibe-forge/client 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/dist/assets/{arc-C1rWFTer.js → arc-CSepokz3.js} +1 -1
  2. package/dist/assets/{blockDiagram-c4efeb88-DlZ9x70F.js → blockDiagram-c4efeb88-D0ARcoNf.js} +1 -1
  3. package/dist/assets/{c4Diagram-c83219d4-BKKxi__y.js → c4Diagram-c83219d4-BysYF9kP.js} +1 -1
  4. package/dist/assets/channel-CeKPk6Nd.js +1 -0
  5. package/dist/assets/{classDiagram-beda092f-CVGPySZq.js → classDiagram-beda092f-BG1GhIOL.js} +1 -1
  6. package/dist/assets/{classDiagram-v2-2358418a-7kp8GVVj.js → classDiagram-v2-2358418a-Dd08uGSH.js} +1 -1
  7. package/dist/assets/clone-CrkD2PuD.js +1 -0
  8. package/dist/assets/{createText-1719965b-Dykv8kT9.js → createText-1719965b-CigPEIEn.js} +1 -1
  9. package/dist/assets/{cssMode-B59COYVW.js → cssMode-MjflyEfm.js} +1 -1
  10. package/dist/assets/{edges-96097737-CkZ1ZBro.js → edges-96097737-DuTBJJRv.js} +1 -1
  11. package/dist/assets/{erDiagram-0228fc6a-281ADcRp.js → erDiagram-0228fc6a-Cp1bL7Y7.js} +1 -1
  12. package/dist/assets/{flowDb-c6c81e3f-BQjX_flP.js → flowDb-c6c81e3f-BfKbhiq5.js} +1 -1
  13. package/dist/assets/{flowDiagram-50d868cf-DMHZTjES.js → flowDiagram-50d868cf-m7gGc3PK.js} +1 -1
  14. package/dist/assets/flowDiagram-v2-4f6560a1-4ZU4bdp1.js +1 -0
  15. package/dist/assets/{flowchart-elk-definition-6af322e1-CI3yz4z8.js → flowchart-elk-definition-6af322e1-EVeTDRRK.js} +1 -1
  16. package/dist/assets/{freemarker2-DWnWjibn.js → freemarker2-Bb3-QAIN.js} +1 -1
  17. package/dist/assets/{ganttDiagram-a2739b55-B3IING9L.js → ganttDiagram-a2739b55-DslB2U0R.js} +1 -1
  18. package/dist/assets/{gitGraphDiagram-82fe8481-CnArIr_T.js → gitGraphDiagram-82fe8481-C-KFWMXL.js} +1 -1
  19. package/dist/assets/{graph-BZ1F0Yve.js → graph-CukaUc0o.js} +1 -1
  20. package/dist/assets/{handlebars-C1QH9qTz.js → handlebars-C4le-2Y6.js} +1 -1
  21. package/dist/assets/{html-D1NkqHjC.js → html-CjNiRs5S.js} +1 -1
  22. package/dist/assets/{htmlMode-DAZCE_rA.js → htmlMode-B73_3-We.js} +1 -1
  23. package/dist/assets/{index-5325376f-Da9zSHjA.js → index-5325376f-CVISZFPw.js} +1 -1
  24. package/dist/assets/{index-C0vjF3D0.js → index-BZosmb5_.js} +336 -336
  25. package/dist/assets/index-C1oh0w9H.css +32 -0
  26. package/dist/assets/{infoDiagram-8eee0895-DYbFvRM7.js → infoDiagram-8eee0895-DoirLE1K.js} +1 -1
  27. package/dist/assets/{javascript-CoMjGRHa.js → javascript-BDjnqJFP.js} +1 -1
  28. package/dist/assets/{journeyDiagram-c64418c1-Boebox0b.js → journeyDiagram-c64418c1-Ckn-p2CM.js} +1 -1
  29. package/dist/assets/{jsonMode-D__gAvuz.js → jsonMode-C-ftOc5j.js} +1 -1
  30. package/dist/assets/{layout-CTcHNbHp.js → layout-Z7yUG7hB.js} +1 -1
  31. package/dist/assets/{line-4AwinCz2.js → line-DPG_cfAy.js} +1 -1
  32. package/dist/assets/{linear-CeSMLzJW.js → linear--GSeVfMi.js} +1 -1
  33. package/dist/assets/{liquid-DZF6egdE.js → liquid-COiLZ9py.js} +1 -1
  34. package/dist/assets/{lspLanguageFeatures-6K4lv5S2.js → lspLanguageFeatures-DGmhryFq.js} +1 -1
  35. package/dist/assets/{mdx-Cnt4ka6w.js → mdx-BpL87Gej.js} +1 -1
  36. package/dist/assets/{mermaid.core-B0yG5s4D.js → mermaid.core-Cg1CCDo6.js} +4 -4
  37. package/dist/assets/{mindmap-definition-8da855dc-KJEvXMKj.js → mindmap-definition-8da855dc-CKDof1lD.js} +1 -1
  38. package/dist/assets/{pieDiagram-a8764435-17nFAXPJ.js → pieDiagram-a8764435-DwvCaZVE.js} +1 -1
  39. package/dist/assets/{python-DA3TtjDv.js → python-63dBmWV_.js} +1 -1
  40. package/dist/assets/{quadrantDiagram-1e28029f-Dt4vubi-.js → quadrantDiagram-1e28029f-CkzYBQpy.js} +1 -1
  41. package/dist/assets/{razor-CWDJgvX_.js → razor-C50tBqEZ.js} +1 -1
  42. package/dist/assets/{requirementDiagram-08caed73-H6aDyDK-.js → requirementDiagram-08caed73-Brgdjqf4.js} +1 -1
  43. package/dist/assets/{sankeyDiagram-a04cb91d-DxsVtbjI.js → sankeyDiagram-a04cb91d-CGkYexrs.js} +1 -1
  44. package/dist/assets/{sequenceDiagram-c5b8d532-BHa148XJ.js → sequenceDiagram-c5b8d532-D0wE-_J8.js} +1 -1
  45. package/dist/assets/{stateDiagram-1ecb1508-DgwBm8LO.js → stateDiagram-1ecb1508-BYb3NCXZ.js} +1 -1
  46. package/dist/assets/{stateDiagram-v2-c2b004d7-BK7IQLVc.js → stateDiagram-v2-c2b004d7-DrPqi4Pt.js} +1 -1
  47. package/dist/assets/{styles-b4e223ce-DzW27Bc-.js → styles-b4e223ce-DD66TIO4.js} +1 -1
  48. package/dist/assets/{styles-ca3715f6-Dex2GiLT.js → styles-ca3715f6-iy02LHIV.js} +1 -1
  49. package/dist/assets/{styles-d45a18b0-B6fGtDKS.js → styles-d45a18b0-BgqAgJyW.js} +1 -1
  50. package/dist/assets/{svgDrawCommon-b86b1483-B4HYgfV5.js → svgDrawCommon-b86b1483-CDq7ugnw.js} +1 -1
  51. package/dist/assets/{timeline-definition-faaaa080--QSbWb25.js → timeline-definition-faaaa080-DzcLLjK0.js} +1 -1
  52. package/dist/assets/{tsMode-ZM7ocZCH.js → tsMode-BFRFI4ct.js} +1 -1
  53. package/dist/assets/{typescript-CKWDmBCc.js → typescript-CBZQRAPv.js} +1 -1
  54. package/dist/assets/{xml-DuEUAzPi.js → xml-BpWm6upt.js} +1 -1
  55. package/dist/assets/{xychartDiagram-f5964ef8-D09Zkv2K.js → xychartDiagram-f5964ef8-zBN8FmLQ.js} +1 -1
  56. package/dist/assets/{yaml-DL7QPRYk.js → yaml-CqbJPiIP.js} +1 -1
  57. package/dist/index.html +2 -2
  58. package/package.json +10 -10
  59. package/src/api/git.ts +78 -0
  60. package/src/api.ts +24 -0
  61. package/src/components/chat/ChatHeader.tsx +4 -0
  62. package/src/components/chat/ChatHistoryView.tsx +22 -13
  63. package/src/components/chat/git-controls/BranchSwitcherDropdown.tsx +157 -0
  64. package/src/components/chat/git-controls/ChatGitControls.scss +616 -0
  65. package/src/components/chat/git-controls/ChatGitControls.tsx +151 -0
  66. package/src/components/chat/git-controls/GitCommitModal.tsx +199 -0
  67. package/src/components/chat/git-controls/GitCommitModalParts.tsx +151 -0
  68. package/src/components/chat/git-controls/GitOperationsDropdown.tsx +123 -0
  69. package/src/components/chat/git-controls/GitPushModal.tsx +106 -0
  70. package/src/components/chat/git-controls/GitWorktreeDropdown.tsx +68 -0
  71. package/src/components/chat/git-controls/git-branch-utils.ts +88 -0
  72. package/src/components/chat/git-controls/git-commit-utils.ts +79 -0
  73. package/src/components/chat/git-controls/git-mutation-utils.ts +69 -0
  74. package/src/components/chat/git-controls/git-operation-utils.ts +98 -0
  75. package/src/components/chat/git-controls/git-worktree-utils.ts +49 -0
  76. package/src/components/chat/git-controls/use-chat-git-commit.ts +185 -0
  77. package/src/components/chat/git-controls/use-chat-git-controls.ts +200 -0
  78. package/src/components/chat/git-controls/use-chat-git-push-state.ts +19 -0
  79. package/src/components/chat/git-controls/use-chat-git-worktrees.ts +39 -0
  80. package/src/components/chat/messages/MessageStatusNotice.scss +163 -0
  81. package/src/components/chat/messages/MessageStatusNotice.tsx +48 -0
  82. package/src/components/chat/messages/build-chat-history-status-notices.ts +138 -0
  83. package/src/components/chat/sender/@components/sender-body/SenderBody.tsx +0 -24
  84. package/src/components/chat/sender/@core/build-sender-controller-result.ts +0 -6
  85. package/src/components/chat/sender/@hooks/use-sender-controller.ts +0 -2
  86. package/src/components/chat/sender/@types/sender-props.ts +0 -3
  87. package/src/components/chat/sender/Sender.scss +0 -58
  88. package/src/components/chat/sender/Sender.tsx +0 -2
  89. package/src/components/chat/tools/DefaultTool.tsx +84 -208
  90. package/src/components/chat/tools/adapter-claude/ClaudeEditDiff.tsx +30 -0
  91. package/src/components/chat/tools/adapter-claude/GenericClaudeTool.scss +128 -0
  92. package/src/components/chat/tools/adapter-claude/GenericClaudeTool.tsx +119 -0
  93. package/src/components/chat/tools/adapter-claude/claude-tool-edit-builders.ts +109 -0
  94. package/src/components/chat/tools/adapter-claude/claude-tool-field-sections.tsx +83 -0
  95. package/src/components/chat/tools/adapter-claude/claude-tool-operation-builders.ts +135 -0
  96. package/src/components/chat/tools/adapter-claude/claude-tool-presentation.ts +61 -0
  97. package/src/components/chat/tools/adapter-claude/claude-tool-shared.ts +185 -0
  98. package/src/components/chat/tools/adapter-claude/claude-tool-summary.ts +76 -0
  99. package/src/components/chat/tools/adapter-claude/claude-tool-system-builders.ts +125 -0
  100. package/src/components/chat/tools/adapter-claude/claude-tool-task-builders.ts +148 -0
  101. package/src/components/chat/tools/adapter-claude/index.ts +24 -15
  102. package/src/components/chat/tools/core/ToolCallBox.scss +362 -36
  103. package/src/components/chat/tools/core/ToolCallBox.tsx +35 -13
  104. package/src/components/chat/tools/core/ToolDiffViewer.scss +138 -0
  105. package/src/components/chat/tools/core/ToolDiffViewer.tsx +180 -0
  106. package/src/components/chat/tools/core/ToolGroup.scss +52 -74
  107. package/src/components/chat/tools/core/ToolGroup.tsx +25 -40
  108. package/src/components/chat/tools/core/ToolRenderer.tsx +3 -3
  109. package/src/components/chat/tools/core/ToolResultContent.tsx +66 -0
  110. package/src/components/chat/tools/core/ToolSummaryHeader.tsx +67 -0
  111. package/src/components/chat/tools/core/generic-tool-presentation.ts +661 -0
  112. package/src/components/chat/tools/core/tool-content-presence.ts +57 -0
  113. package/src/components/chat/tools/core/tool-display.ts +203 -0
  114. package/src/components/chat/tools/core/tool-field-sections.tsx +132 -0
  115. package/src/components/chat/tools/core/tool-result-content-utils.ts +171 -0
  116. package/src/components/chat/tools/core/tool-summary.ts +206 -0
  117. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +59 -53
  118. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +26 -9
  119. package/src/components/chat/tools/task/ListTasksTool.tsx +22 -9
  120. package/src/components/chat/tools/task/StartTasksTool.tsx +22 -9
  121. package/src/hooks/chat/interaction-state.ts +29 -9
  122. package/src/hooks/chat/session-view-cache.ts +80 -0
  123. package/src/hooks/chat/use-chat-scroll.ts +2 -2
  124. package/src/hooks/chat/use-chat-session-messages.ts +139 -39
  125. package/src/hooks/chat/use-chat-session.ts +2 -2
  126. package/src/resources/locales/en.json +149 -0
  127. package/src/resources/locales/zh.json +149 -0
  128. package/src/routes/ChatRoute.tsx +24 -27
  129. package/src/utils/strip-ansi.ts +26 -0
  130. package/dist/assets/channel-F1aqMANO.js +0 -1
  131. package/dist/assets/clone-B-GCuXNo.js +0 -1
  132. package/dist/assets/flowDiagram-v2-4f6560a1-C5FzdVl1.js +0 -1
  133. package/dist/assets/index-vzEbM21t.css +0 -32
@@ -0,0 +1,206 @@
1
+ /* eslint-disable max-lines */
2
+
3
+ import type { ChatMessageContent } from '@vibe-forge/core'
4
+
5
+ import {
6
+ buildClaudeToolPresentation,
7
+ getClaudeToolBaseName,
8
+ isClaudeToolName
9
+ } from '../adapter-claude/claude-tool-presentation'
10
+ import { getClaudeToolSummaryText } from '../adapter-claude/claude-tool-summary'
11
+ import { buildGenericToolPresentation } from './generic-tool-presentation'
12
+
13
+ export type ToolUseItem = Extract<ChatMessageContent, { type: 'tool_use' }>
14
+ type Translate = (key: string, options?: Record<string, unknown>) => string
15
+
16
+ const HUMANIZED_SEGMENT_SEPARATOR = /[_:-]+/g
17
+ const GENERIC_TOOL_NAMESPACE_PREFIXES = new Set(['adapter', 'agent', 'mcp', 'plugin', 'tool'])
18
+
19
+ const humanizeToolSegment = (value: string) =>
20
+ value
21
+ .replace(HUMANIZED_SEGMENT_SEPARATOR, ' ')
22
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
23
+ .trim()
24
+
25
+ const getToolNameSegments = (name: string) => (
26
+ name.includes('__') ? name.split('__').filter(Boolean) : name.split(':').filter(Boolean)
27
+ )
28
+
29
+ export const formatToolName = (name: string) => {
30
+ if (name.startsWith('mcp__ChromeDevtools__')) {
31
+ return name.replace('mcp__ChromeDevtools__', '')
32
+ }
33
+
34
+ const namespaceSegments = name.includes('__') ? name.split('__').filter(Boolean) : []
35
+ const lastSegment = namespaceSegments.length > 0
36
+ ? namespaceSegments[namespaceSegments.length - 1]
37
+ : name.split(':').pop() ?? name
38
+ return humanizeToolSegment(lastSegment)
39
+ }
40
+
41
+ export const getToolInputPreview = (input: unknown) => {
42
+ if (input == null || typeof input !== 'object' || Array.isArray(input)) {
43
+ return undefined
44
+ }
45
+
46
+ const record = input as Record<string, unknown>
47
+ const preferredKeys = [
48
+ 'file_path',
49
+ 'path',
50
+ 'url',
51
+ 'query',
52
+ 'pattern',
53
+ 'command',
54
+ 'selector',
55
+ 'taskId',
56
+ 'subject',
57
+ 'skill',
58
+ 'title'
59
+ ]
60
+
61
+ for (const key of preferredKeys) {
62
+ const value = record[key]
63
+ if (typeof value === 'string' && value.trim() !== '') {
64
+ return value.trim().split('\n')[0]
65
+ }
66
+ }
67
+
68
+ for (const value of Object.values(record)) {
69
+ if (typeof value === 'string' && value.trim() !== '') {
70
+ return value.trim().split('\n')[0]
71
+ }
72
+ }
73
+
74
+ return undefined
75
+ }
76
+
77
+ export function getToolSummaryText(item: ToolUseItem, t: Translate) {
78
+ if (isClaudeToolName(item.name)) {
79
+ return getClaudeToolSummaryText(item.name, item.input, t)
80
+ }
81
+
82
+ const presentation = buildGenericToolPresentation(item.name, item.input)
83
+ const displayName = presentation.titleKey != null
84
+ ? t(presentation.titleKey, { defaultValue: presentation.fallbackTitle })
85
+ : presentation.fallbackTitle
86
+ const preview = presentation.primary ?? getToolInputPreview(item.input)
87
+ return preview != null && preview !== '' ? `${displayName} ${preview}` : displayName
88
+ }
89
+
90
+ export function getToolTitleText(item: ToolUseItem, t: Translate) {
91
+ if (isClaudeToolName(item.name)) {
92
+ const presentation = buildClaudeToolPresentation(item.name, item.input)
93
+ return t(presentation.titleKey, { defaultValue: presentation.fallbackTitle })
94
+ }
95
+
96
+ const presentation = buildGenericToolPresentation(item.name, item.input)
97
+ return presentation.titleKey != null
98
+ ? t(presentation.titleKey, { defaultValue: presentation.fallbackTitle })
99
+ : presentation.fallbackTitle
100
+ }
101
+
102
+ export function getToolPrimaryText(item: ToolUseItem) {
103
+ if (isClaudeToolName(item.name)) {
104
+ return buildClaudeToolPresentation(item.name, item.input).primary
105
+ }
106
+
107
+ return buildGenericToolPresentation(item.name, item.input).primary ?? getToolInputPreview(item.input)
108
+ }
109
+
110
+ const getToolNamespaceLabel = (name: string) => {
111
+ const namespaceSegments = getToolNameSegments(name)
112
+ .slice(0, -1)
113
+ .filter((segment, index) => !(index === 0 && GENERIC_TOOL_NAMESPACE_PREFIXES.has(segment.toLowerCase())))
114
+ .map(humanizeToolSegment)
115
+ .filter(Boolean)
116
+
117
+ return namespaceSegments.length > 0 ? namespaceSegments.join(' ') : undefined
118
+ }
119
+
120
+ const getToolQualifiedLabel = (name: string, fallbackLabel: string) => {
121
+ const namespaceLabel = getToolNamespaceLabel(name)
122
+ return namespaceLabel != null && namespaceLabel !== ''
123
+ ? `${namespaceLabel} ${fallbackLabel}`
124
+ : fallbackLabel
125
+ }
126
+
127
+ interface ToolGroupDescriptor {
128
+ key: string
129
+ label: string
130
+ qualifiedLabel: string
131
+ }
132
+
133
+ function getToolGroupDescriptor(item: ToolUseItem, t: Translate): ToolGroupDescriptor {
134
+ if (isClaudeToolName(item.name)) {
135
+ const baseName = getClaudeToolBaseName(item.name)
136
+ const presentation = buildClaudeToolPresentation(item.name, item.input)
137
+ const label = t(presentation.titleKey, { defaultValue: presentation.fallbackTitle })
138
+ return {
139
+ key: `claude:${baseName}`,
140
+ label,
141
+ qualifiedLabel: label
142
+ }
143
+ }
144
+
145
+ const presentation = buildGenericToolPresentation(item.name, item.input)
146
+ const label = presentation.titleKey != null
147
+ ? t(presentation.titleKey, { defaultValue: presentation.fallbackTitle })
148
+ : presentation.fallbackTitle
149
+ return {
150
+ key: item.name,
151
+ label,
152
+ qualifiedLabel: getToolQualifiedLabel(item.name, label)
153
+ }
154
+ }
155
+
156
+ export function getToolGroupSummaryText(
157
+ items: Array<{
158
+ item: ToolUseItem
159
+ }>,
160
+ t: Translate
161
+ ) {
162
+ const groupedTools = new Map<string, { label: string; qualifiedLabel: string; count: number }>()
163
+
164
+ for (const { item } of items) {
165
+ const descriptor = getToolGroupDescriptor(item, t)
166
+ const label = descriptor.label
167
+ if (label === '') continue
168
+
169
+ const current = groupedTools.get(descriptor.key)
170
+ if (current != null) {
171
+ current.count += 1
172
+ continue
173
+ }
174
+
175
+ groupedTools.set(descriptor.key, {
176
+ label,
177
+ qualifiedLabel: descriptor.qualifiedLabel,
178
+ count: 1
179
+ })
180
+ }
181
+
182
+ const groups = Array.from(groupedTools.values())
183
+ const labelCounts = groups.reduce((map, group) => {
184
+ map.set(group.label, (map.get(group.label) ?? 0) + 1)
185
+ return map
186
+ }, new Map<string, number>())
187
+
188
+ if (groups.length === 0) {
189
+ return t('chat.usedTools', { count: items.length })
190
+ }
191
+
192
+ const summaries = groups.map((group) => {
193
+ const name = (labelCounts.get(group.label) ?? 0) > 1 ? group.qualifiedLabel : group.label
194
+ return {
195
+ count: group.count,
196
+ text: t('chat.tools.groupSummaryCount', { name, count: group.count })
197
+ }
198
+ })
199
+
200
+ const visible = summaries.slice(0, 2).map(summary => summary.text)
201
+ const hiddenCallCount = summaries.slice(2).reduce((count, summary) => count + summary.count, 0)
202
+
203
+ return hiddenCallCount > 0
204
+ ? [...visible, t('chat.tools.groupSummaryMoreCount', { count: hiddenCallCount })].join(' · ')
205
+ : visible.join(' · ')
206
+ }
@@ -1,75 +1,81 @@
1
1
  import './ChromeDevtoolsTool.scss'
2
+ import { Tooltip } from 'antd'
2
3
  import React from 'react'
3
4
  import { useTranslation } from 'react-i18next'
4
5
 
5
6
  import { CodeBlock } from '#~/components/CodeBlock'
6
7
  import { safeJsonStringify } from '#~/utils/safe-serialize'
7
8
  import { ToolCallBox } from '../core/ToolCallBox'
9
+ import { ToolResultContent } from '../core/ToolResultContent'
10
+ import { ToolSummaryHeader } from '../core/ToolSummaryHeader'
11
+ import { hasMeaningfulToolValue } from '../core/tool-content-presence'
12
+ import { TOOL_TOOLTIP_PROPS, getToolSectionIcon, getToolTargetPresentation } from '../core/tool-display'
13
+ import { getToolPrimaryText, getToolTitleText } from '../core/tool-summary'
8
14
  import { defineToolRender } from '../defineToolRender'
9
15
 
10
- const formatToolName = (name: string) => {
11
- if (name.startsWith('mcp__ChromeDevtools__')) {
12
- return name.replace('mcp__ChromeDevtools__', '')
13
- }
14
- return name
15
- }
16
-
17
16
  export const ChromeDevtoolsTool = defineToolRender(({ item, resultItem }) => {
18
17
  const { t } = useTranslation()
19
- const displayName = formatToolName(item.name)
20
18
  const input = item.input != null ? item.input : {}
19
+ const hasCallDetails = hasMeaningfulToolValue(input)
20
+ const hasResultDetails = resultItem != null && hasMeaningfulToolValue(resultItem.content)
21
+ const hasDetails = hasCallDetails || hasResultDetails
22
+ const titleText = getToolTitleText(item, t)
23
+ const targetPresentation = getToolTargetPresentation(getToolPrimaryText(item))
24
+ const errorMeta = resultItem?.is_error === true
25
+ ? (
26
+ <span className='tool-status tool-status--error'>
27
+ <span className='material-symbols-rounded'>error</span>
28
+ </span>
29
+ )
30
+ : undefined
21
31
 
22
32
  return (
23
- <div className='tool-group chrome-devtools-tool'>
33
+ <div className='tool-group tool-group--compact chrome-devtools-tool'>
24
34
  <ToolCallBox
25
- defaultExpanded={true}
26
- header={
27
- <div className='tool-header-content'>
28
- <i className='tool-header-icon chrome-devtools-tool__icon devicon-chrome-plain colored' />
29
- <span className='tool-header-title'>{displayName}</span>
30
- <span className='tool-header-hint'>{t('chat.tools.call')}</span>
31
- </div>
32
- }
33
- content={
34
- <div className='tool-content'>
35
- <CodeBlock
36
- code={safeJsonStringify(input, 2)}
37
- lang='json'
38
- />
39
- </div>
40
- }
41
- />
42
- {resultItem != null && (
43
- <ToolCallBox
44
- type='result'
45
- isError={resultItem.is_error}
46
- header={
47
- <div className='tool-header-content'>
48
- <span className='material-symbols-rounded tool-header-icon'>
49
- {resultItem.is_error === true ? 'error' : 'check_circle'}
50
- </span>
51
- <span className='tool-header-title'>{t('chat.result')}</span>
52
- </div>
53
- }
54
- content={
55
- <div className='tool-content'>
56
- {typeof resultItem.content === 'string'
57
- ? (
35
+ variant='inline'
36
+ defaultExpanded={false}
37
+ collapsible={hasDetails}
38
+ header={({ isExpanded, isCollapsible }) => (
39
+ <ToolSummaryHeader
40
+ icon={<i className='devicon-chrome-plain colored' />}
41
+ title={titleText}
42
+ target={targetPresentation.text}
43
+ targetTitle={targetPresentation.title}
44
+ targetMonospace={targetPresentation.monospace}
45
+ expanded={isExpanded}
46
+ collapsible={isCollapsible}
47
+ meta={errorMeta}
48
+ metaTitle={errorMeta == null ? undefined : t('chat.result')}
49
+ />
50
+ )}
51
+ content={hasDetails
52
+ ? (
53
+ <div className='tool-detail-sections'>
54
+ {hasCallDetails && (
55
+ <div className='tool-detail-section'>
56
+ <div className='tool-detail-section__header'>
57
+ <Tooltip title={t('chat.tools.call')} {...TOOL_TOOLTIP_PROPS}>
58
+ <span className='tool-detail-section__icon material-symbols-rounded'>
59
+ {getToolSectionIcon('call')}
60
+ </span>
61
+ </Tooltip>
62
+ </div>
58
63
  <CodeBlock
59
- code={resultItem.content}
60
- lang='text'
61
- />
62
- )
63
- : (
64
- <CodeBlock
65
- code={safeJsonStringify(resultItem.content, 2)}
64
+ code={safeJsonStringify(input, 2)}
66
65
  lang='json'
66
+ hideHeader={true}
67
67
  />
68
- )}
68
+ </div>
69
+ )}
70
+ {hasResultDetails && resultItem != null && (
71
+ <div className='tool-detail-section'>
72
+ <ToolResultContent content={resultItem.content} />
73
+ </div>
74
+ )}
69
75
  </div>
70
- }
71
- />
72
- )}
76
+ )
77
+ : null}
78
+ />
73
79
  </div>
74
80
  )
75
81
  })
@@ -5,6 +5,8 @@ import React, { useMemo } from 'react'
5
5
  import { useTranslation } from 'react-i18next'
6
6
 
7
7
  import { ToolCallBox } from '../core/ToolCallBox'
8
+ import { ToolSummaryHeader } from '../core/ToolSummaryHeader'
9
+ import { getToolTargetPresentation } from '../core/tool-display'
8
10
  import { defineToolRender } from '../defineToolRender'
9
11
  import { TaskRow } from './components/TaskRow'
10
12
 
@@ -53,18 +55,33 @@ export const GetTaskInfoTool = defineToolRender(({ item, resultItem }) => {
53
55
  })()
54
56
  : []
55
57
  const titleFallback = t('chat.tools.task')
58
+ const taskIdPresentation = getToolTargetPresentation(taskResult?.taskId ?? inputTaskId)
59
+ const errorMeta = resultItem?.is_error === true
60
+ ? (
61
+ <span className='tool-status tool-status--error'>
62
+ <span className='material-symbols-rounded'>error</span>
63
+ </span>
64
+ )
65
+ : undefined
56
66
 
57
67
  return (
58
- <div className='tool-group get-task-info-tool'>
68
+ <div className='tool-group tool-group--compact get-task-info-tool'>
59
69
  <ToolCallBox
60
- defaultExpanded={true}
61
- header={
62
- <div className='tool-header-content'>
63
- <span className='material-symbols-rounded tool-header-icon'>info</span>
64
- <span className='tool-header-title'>{t('chat.tools.getTaskInfo')}</span>
65
- <span className='tool-header-chip'>{taskResult ? 1 : 0}</span>
66
- </div>
67
- }
70
+ variant='inline'
71
+ defaultExpanded={false}
72
+ header={({ isExpanded, isCollapsible }) => (
73
+ <ToolSummaryHeader
74
+ icon={<span className='material-symbols-rounded'>info</span>}
75
+ title={t('chat.tools.getTaskInfo')}
76
+ target={taskIdPresentation.text}
77
+ targetTitle={taskIdPresentation.title}
78
+ targetMonospace={taskIdPresentation.monospace}
79
+ expanded={isExpanded}
80
+ collapsible={isCollapsible}
81
+ meta={errorMeta}
82
+ metaTitle={errorMeta == null ? undefined : t('chat.result')}
83
+ />
84
+ )}
68
85
  content={
69
86
  <div className='tool-content'>
70
87
  {taskResult
@@ -4,6 +4,7 @@ import React, { useMemo } from 'react'
4
4
  import { useTranslation } from 'react-i18next'
5
5
 
6
6
  import { ToolCallBox } from '../core/ToolCallBox'
7
+ import { ToolSummaryHeader } from '../core/ToolSummaryHeader'
7
8
  import { defineToolRender } from '../defineToolRender'
8
9
  import { TaskRow } from './components/TaskRow'
9
10
 
@@ -29,18 +30,30 @@ export const ListTasksTool = defineToolRender(({ resultItem }) => {
29
30
  const text = resultItem.content[0].text.trim()
30
31
  return JSON.parse(text) as TaskResult[]
31
32
  }, [resultItem?.content])
33
+ const errorMeta = resultItem?.is_error === true
34
+ ? (
35
+ <span className='tool-status tool-status--error'>
36
+ <span className='material-symbols-rounded'>error</span>
37
+ </span>
38
+ )
39
+ : undefined
32
40
 
33
41
  return (
34
- <div className='tool-group list-tasks-tool'>
42
+ <div className='tool-group tool-group--compact list-tasks-tool'>
35
43
  <ToolCallBox
36
- defaultExpanded={true}
37
- header={
38
- <div className='tool-header-content'>
39
- <span className='material-symbols-rounded tool-header-icon'>list_alt</span>
40
- <span className='tool-header-title'>{t('chat.tools.listTasks')}</span>
41
- <span className='tool-header-chip'>{taskResults.length}</span>
42
- </div>
43
- }
44
+ variant='inline'
45
+ defaultExpanded={false}
46
+ header={({ isExpanded, isCollapsible }) => (
47
+ <ToolSummaryHeader
48
+ icon={<span className='material-symbols-rounded'>list_alt</span>}
49
+ title={t('chat.tools.listTasks')}
50
+ target={t('chat.tools.taskCount', { count: taskResults.length })}
51
+ expanded={isExpanded}
52
+ collapsible={isCollapsible}
53
+ meta={errorMeta}
54
+ metaTitle={errorMeta == null ? undefined : t('chat.result')}
55
+ />
56
+ )}
44
57
  content={
45
58
  <div className='tool-content'>
46
59
  <div className='list-tasks-tool__list'>
@@ -5,6 +5,7 @@ import React, { useMemo } from 'react'
5
5
  import { useTranslation } from 'react-i18next'
6
6
 
7
7
  import { ToolCallBox } from '../core/ToolCallBox'
8
+ import { ToolSummaryHeader } from '../core/ToolSummaryHeader'
8
9
  import { defineToolRender } from '../defineToolRender'
9
10
  import { TaskRow } from './components/TaskRow'
10
11
 
@@ -39,18 +40,30 @@ export const StartTasksTool = defineToolRender(({ item, resultItem }) => {
39
40
  }, [resultItem?.content])
40
41
 
41
42
  const { taskResults } = parsedResult
43
+ const errorMeta = resultItem?.is_error === true
44
+ ? (
45
+ <span className='tool-status tool-status--error'>
46
+ <span className='material-symbols-rounded'>error</span>
47
+ </span>
48
+ )
49
+ : undefined
42
50
 
43
51
  return (
44
- <div className='tool-group start-tasks-tool'>
52
+ <div className='tool-group tool-group--compact start-tasks-tool'>
45
53
  <ToolCallBox
46
- defaultExpanded={true}
47
- header={
48
- <div className='tool-header-content'>
49
- <span className='material-symbols-rounded tool-header-icon'>playlist_add</span>
50
- <span className='tool-header-title'>{t('chat.tools.startTasks')}</span>
51
- <span className='tool-header-chip'>{tasks.length}</span>
52
- </div>
53
- }
54
+ variant='inline'
55
+ defaultExpanded={false}
56
+ header={({ isExpanded, isCollapsible }) => (
57
+ <ToolSummaryHeader
58
+ icon={<span className='material-symbols-rounded'>playlist_add</span>}
59
+ title={t('chat.tools.startTasks')}
60
+ target={t('chat.tools.taskCount', { count: tasks.length })}
61
+ expanded={isExpanded}
62
+ collapsible={isCollapsible}
63
+ meta={errorMeta}
64
+ metaTitle={errorMeta == null ? undefined : t('chat.result')}
65
+ />
66
+ )}
54
67
  content={
55
68
  <div className='tool-content'>
56
69
  <div className='start-tasks-tool__list'>
@@ -1,13 +1,17 @@
1
1
  import type { AskUserQuestionParams, Session, WSEvent } from '@vibe-forge/core'
2
2
 
3
+ import { stripAnsi } from '#~/utils/strip-ansi'
4
+
3
5
  export interface InteractionRequestState {
4
6
  id: string
5
7
  payload: AskUserQuestionParams
6
8
  }
7
9
 
8
- export interface ChatErrorBannerState {
10
+ export interface ChatErrorState {
9
11
  kind: 'connection' | 'session'
10
12
  message: string
13
+ code?: string
14
+ reason?: 'error' | 'closed'
11
15
  }
12
16
 
13
17
  export interface FatalSessionErrorState {
@@ -15,6 +19,8 @@ export interface FatalSessionErrorState {
15
19
  code?: string
16
20
  }
17
21
 
22
+ const normalizeErrorMessage = (value: string) => stripAnsi(value).trim()
23
+
18
24
  export const getFatalSessionError = (event: WSEvent): FatalSessionErrorState | null => {
19
25
  if (event?.type !== 'error') {
20
26
  return null
@@ -24,20 +30,34 @@ export const getFatalSessionError = (event: WSEvent): FatalSessionErrorState | n
24
30
  return null
25
31
  }
26
32
 
33
+ const code = event.data != null && typeof event.data === 'object' &&
34
+ 'code' in event.data &&
35
+ typeof event.data.code === 'string' &&
36
+ event.data.code.trim() !== ''
37
+ ? event.data.code
38
+ : undefined
39
+
27
40
  if (event.data != null && typeof event.data === 'object' && 'message' in event.data) {
28
41
  const message = event.data.message
29
- if (typeof message === 'string' && message.trim() !== '') {
30
- return {
31
- message,
32
- code: typeof event.data.code === 'string' && event.data.code.trim() !== ''
33
- ? event.data.code
34
- : undefined
42
+ if (typeof message === 'string') {
43
+ const normalizedMessage = normalizeErrorMessage(message)
44
+ if (normalizedMessage !== '') {
45
+ return {
46
+ message: normalizedMessage,
47
+ code
48
+ }
35
49
  }
36
50
  }
37
51
  }
38
52
 
39
- if (typeof event.message === 'string' && event.message.trim() !== '') {
40
- return { message: event.message }
53
+ if (typeof event.message === 'string') {
54
+ const normalizedMessage = normalizeErrorMessage(event.message)
55
+ if (normalizedMessage !== '') {
56
+ return {
57
+ message: normalizedMessage,
58
+ code
59
+ }
60
+ }
41
61
  }
42
62
 
43
63
  return null
@@ -0,0 +1,80 @@
1
+ import type { ChatMessage } from '@vibe-forge/core'
2
+ import type { SessionInfo } from '@vibe-forge/types'
3
+
4
+ import type { ChatErrorState, InteractionRequestState } from './interaction-state'
5
+
6
+ export interface ChatSessionViewSnapshot {
7
+ messages: ChatMessage[]
8
+ sessionInfo: SessionInfo | null
9
+ errorState: ChatErrorState | null
10
+ interactionRequest: InteractionRequestState | null
11
+ isHydrated: boolean
12
+ }
13
+
14
+ export const MAX_CHAT_SESSION_VIEW_SNAPSHOTS = 20
15
+
16
+ export const createChatSessionViewSnapshot = (
17
+ value?: Partial<ChatSessionViewSnapshot>
18
+ ): ChatSessionViewSnapshot => ({
19
+ messages: value?.messages ?? [],
20
+ sessionInfo: value?.sessionInfo ?? null,
21
+ errorState: value?.errorState ?? null,
22
+ interactionRequest: value?.interactionRequest ?? null,
23
+ isHydrated: value?.isHydrated ?? false
24
+ })
25
+
26
+ export const mergeChatSessionViewSnapshot = (
27
+ current: ChatSessionViewSnapshot | undefined,
28
+ patch: Partial<ChatSessionViewSnapshot>
29
+ ): ChatSessionViewSnapshot => {
30
+ return createChatSessionViewSnapshot({
31
+ ...createChatSessionViewSnapshot(current),
32
+ ...patch
33
+ })
34
+ }
35
+
36
+ export const restoreChatSessionViewSnapshot = (snapshot?: ChatSessionViewSnapshot) => {
37
+ const resolved = createChatSessionViewSnapshot(snapshot)
38
+ const restorable = resolved.isHydrated === true
39
+ ? resolved
40
+ : createChatSessionViewSnapshot()
41
+
42
+ return {
43
+ messages: restorable.messages,
44
+ sessionInfo: restorable.sessionInfo,
45
+ errorState: restorable.errorState,
46
+ interactionRequest: restorable.interactionRequest,
47
+ isReady: restorable.isHydrated
48
+ }
49
+ }
50
+
51
+ export const setChatSessionViewSnapshot = (
52
+ cache: Map<string, ChatSessionViewSnapshot>,
53
+ sessionId: string,
54
+ patch: Partial<ChatSessionViewSnapshot>
55
+ ) => {
56
+ const next = mergeChatSessionViewSnapshot(cache.get(sessionId), patch)
57
+
58
+ if (cache.has(sessionId)) {
59
+ cache.delete(sessionId)
60
+ }
61
+
62
+ cache.set(sessionId, next)
63
+
64
+ while (cache.size > MAX_CHAT_SESSION_VIEW_SNAPSHOTS) {
65
+ const oldestSessionId = cache.keys().next().value
66
+ if (oldestSessionId == null) {
67
+ break
68
+ }
69
+ cache.delete(oldestSessionId)
70
+ }
71
+
72
+ return next
73
+ }
74
+
75
+ export const deleteChatSessionViewSnapshot = (
76
+ cache: Map<string, ChatSessionViewSnapshot>,
77
+ sessionId: string
78
+ ) => {
79
+ cache.delete(sessionId)
80
+ }
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
2
2
 
3
3
  const SCROLL_THRESHOLD = 80
4
4
 
5
- export function useChatScroll({ messagesLength }: { messagesLength: number }) {
5
+ export function useChatScroll({ contentVersion }: { contentVersion: number }) {
6
6
  const messagesEndRef = useRef<HTMLDivElement>(null)
7
7
  const messagesContainerRef = useRef<HTMLDivElement>(null)
8
8
  const messagesContentRef = useRef<HTMLDivElement>(null)
@@ -39,7 +39,7 @@ export function useChatScroll({ messagesLength }: { messagesLength: number }) {
39
39
 
40
40
  useEffect(() => {
41
41
  updateScrollState()
42
- }, [updateScrollState, messagesLength])
42
+ }, [contentVersion, updateScrollState])
43
43
 
44
44
  return {
45
45
  messagesEndRef,