@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,57 @@
1
+ import { toSerializable } from '#~/utils/safe-serialize'
2
+
3
+ import { getStringList, getStructuredBlocks } from './tool-result-content-utils'
4
+
5
+ const parseStructuredInput = (value: unknown) => {
6
+ if (typeof value !== 'string') {
7
+ return value
8
+ }
9
+
10
+ const trimmed = value.trim()
11
+ if (
12
+ (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
13
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))
14
+ ) {
15
+ try {
16
+ return JSON.parse(trimmed)
17
+ } catch {
18
+ return value
19
+ }
20
+ }
21
+
22
+ return value
23
+ }
24
+
25
+ export const hasMeaningfulToolValue = (value: unknown): boolean => {
26
+ const parsed = parseStructuredInput(toSerializable(value))
27
+
28
+ if (parsed == null) {
29
+ return false
30
+ }
31
+
32
+ if (typeof parsed === 'string') {
33
+ return parsed.trim() !== ''
34
+ }
35
+
36
+ if (typeof parsed === 'number' || typeof parsed === 'boolean') {
37
+ return true
38
+ }
39
+
40
+ if (Array.isArray(parsed)) {
41
+ return parsed.some(item => hasMeaningfulToolValue(item))
42
+ }
43
+
44
+ if (typeof parsed !== 'object') {
45
+ return false
46
+ }
47
+
48
+ if (getStructuredBlocks(parsed) != null) {
49
+ return true
50
+ }
51
+
52
+ if (getStringList(parsed) != null) {
53
+ return true
54
+ }
55
+
56
+ return Object.values(parsed as Record<string, unknown>).some(item => hasMeaningfulToolValue(item))
57
+ }
@@ -0,0 +1,203 @@
1
+ /* eslint-disable max-lines */
2
+
3
+ import { safeJsonStringify } from '#~/utils/safe-serialize'
4
+
5
+ export const TOOL_TOOLTIP_PROPS = {
6
+ arrow: false,
7
+ mouseEnterDelay: .2,
8
+ mouseLeaveDelay: .06
9
+ } as const
10
+
11
+ const PATH_TOOLTIP_MARKERS = ['apps', 'packages', 'src', 'scripts', '.ai', 'docs', 'tests', 'test', 'changelog']
12
+ const WINDOWS_PATH_RE = /^[a-z]:[\\/]+/i
13
+ const URL_RE = /^https?:\/\//i
14
+
15
+ const TOOL_FIELD_ICON_MAP: Record<string, string> = {
16
+ activeForm: 'description',
17
+ addBlockedBy: 'link_off',
18
+ addBlocks: 'block',
19
+ allowedDomains: 'travel_explore',
20
+ allowedPrompts: 'chat',
21
+ answers: 'fact_check',
22
+ args: 'segment',
23
+ blockedDomains: 'domain_disabled',
24
+ cellId: 'tag',
25
+ cellType: 'grid_view',
26
+ command: 'terminal',
27
+ content: 'notes',
28
+ contextLines: 'format_line_spacing',
29
+ cwd: 'folder_open',
30
+ changes: 'difference',
31
+ description: 'subject',
32
+ disableSandbox: 'shield',
33
+ details: 'info',
34
+ endLine: 'keyboard_double_arrow_down',
35
+ editMode: 'edit',
36
+ glob: 'data_object',
37
+ ignore: 'visibility_off',
38
+ ignoreCase: 'text_fields',
39
+ limit: 'filter_alt',
40
+ maxTurns: 'repeat',
41
+ maxOutputTokens: 'expand_content',
42
+ metadata: 'badge',
43
+ model: 'neurology',
44
+ mode: 'tune',
45
+ newSource: 'note_stack',
46
+ newString: 'edit_note',
47
+ oldString: 'history',
48
+ owner: 'person',
49
+ offset: 'format_indent_increase',
50
+ path: 'folder',
51
+ prompt: 'chat',
52
+ pushToRemote: 'upload',
53
+ questions: 'quiz',
54
+ remoteSession: 'devices',
55
+ remoteSessionUrl: 'link',
56
+ replaceAll: 'select_all',
57
+ resume: 'play_circle',
58
+ runInBackground: 'background_dot_large',
59
+ startLine: 'keyboard_double_arrow_up',
60
+ status: 'flag',
61
+ subagentType: 'hub',
62
+ subject: 'title',
63
+ timeout: 'timer',
64
+ todos: 'checklist',
65
+ workdir: 'folder_code',
66
+ yieldTimeMs: 'schedule'
67
+ }
68
+
69
+ const TOOL_FORMAT_ICON_MAP: Record<string, string> = {
70
+ code: 'code',
71
+ inline: 'label',
72
+ json: 'data_object',
73
+ list: 'format_list_bulleted',
74
+ questions: 'quiz',
75
+ text: 'article'
76
+ }
77
+
78
+ export const getToolSectionIcon = (section: 'call' | 'details' | 'result') => {
79
+ if (section === 'call') return 'tune'
80
+ if (section === 'result') return 'assignment_turned_in'
81
+ return 'info'
82
+ }
83
+
84
+ export const getToolFieldIcon = (labelKey: string, format: string) => {
85
+ const key = labelKey.split('.').pop() ?? labelKey
86
+ return TOOL_FIELD_ICON_MAP[key] ?? TOOL_FORMAT_ICON_MAP[format] ?? 'label'
87
+ }
88
+
89
+ export const getToolValueText = (value: unknown) => {
90
+ if (typeof value === 'string') {
91
+ return value
92
+ }
93
+
94
+ if (typeof value === 'number' || typeof value === 'boolean') {
95
+ return String(value)
96
+ }
97
+
98
+ return safeJsonStringify(value, 2)
99
+ }
100
+
101
+ export const getToolInlineValueText = (value: unknown) => (
102
+ getToolValueText(value).replace(/\s+/g, ' ').trim()
103
+ )
104
+
105
+ export const shouldUseMonospaceTarget = (value: string | undefined) => {
106
+ if (value == null || value === '') {
107
+ return false
108
+ }
109
+
110
+ return (
111
+ value.startsWith('/') ||
112
+ value.startsWith('./') ||
113
+ value.startsWith('../') ||
114
+ value.startsWith('http://') ||
115
+ value.startsWith('https://') ||
116
+ value.startsWith('#') ||
117
+ value.includes('/')
118
+ )
119
+ }
120
+
121
+ const normalizeToolPath = (value: string) => value.replace(/\\/g, '/')
122
+
123
+ const looksLikeToolPath = (value: string) => {
124
+ if (URL_RE.test(value)) {
125
+ return false
126
+ }
127
+
128
+ const normalized = normalizeToolPath(value)
129
+ if (normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) {
130
+ return true
131
+ }
132
+
133
+ if (WINDOWS_PATH_RE.test(value)) {
134
+ return true
135
+ }
136
+
137
+ if (!normalized.includes('/')) {
138
+ return false
139
+ }
140
+
141
+ return !normalized.includes(' ')
142
+ }
143
+
144
+ const getPathLabel = (value: string) => {
145
+ const normalized = normalizeToolPath(value)
146
+ const segments = normalized.split('/').filter(Boolean)
147
+ return segments.at(-1) ?? normalized
148
+ }
149
+
150
+ const getPathTooltip = (value: string) => {
151
+ const normalized = normalizeToolPath(value)
152
+ const segments = normalized.split('/').filter(Boolean)
153
+ const markerIndex = segments.findIndex(segment => PATH_TOOLTIP_MARKERS.includes(segment))
154
+
155
+ if (markerIndex >= 0) {
156
+ return segments.slice(markerIndex).join('/')
157
+ }
158
+
159
+ return normalized
160
+ }
161
+
162
+ export const getToolTargetPresentation = (value: string | undefined) => {
163
+ if (value == null || value.trim() === '') {
164
+ return {
165
+ text: undefined,
166
+ title: undefined,
167
+ monospace: false
168
+ }
169
+ }
170
+
171
+ const normalized = value.trim()
172
+
173
+ if (looksLikeToolPath(normalized)) {
174
+ return {
175
+ text: getPathLabel(normalized),
176
+ title: getPathTooltip(normalized),
177
+ monospace: true
178
+ }
179
+ }
180
+
181
+ if (URL_RE.test(normalized)) {
182
+ try {
183
+ const url = new URL(normalized)
184
+ return {
185
+ text: `${url.hostname}${url.pathname}${url.search}${url.hash}`,
186
+ title: normalized,
187
+ monospace: false
188
+ }
189
+ } catch {
190
+ return {
191
+ text: normalized,
192
+ title: normalized,
193
+ monospace: false
194
+ }
195
+ }
196
+ }
197
+
198
+ return {
199
+ text: normalized,
200
+ title: undefined,
201
+ monospace: shouldUseMonospaceTarget(normalized)
202
+ }
203
+ }
@@ -0,0 +1,132 @@
1
+ import { Tooltip } from 'antd'
2
+ import React from 'react'
3
+
4
+ import { CodeBlock } from '#~/components/CodeBlock'
5
+ import { safeJsonStringify } from '#~/utils/safe-serialize'
6
+
7
+ import { TOOL_TOOLTIP_PROPS, getToolFieldIcon, getToolInlineValueText, getToolValueText } from './tool-display'
8
+
9
+ type Translate = (key: string, options?: Record<string, unknown>) => string
10
+
11
+ export type ToolFieldFormat = 'inline' | 'text' | 'code' | 'list' | 'json' | 'questions'
12
+
13
+ export interface ToolFieldView {
14
+ labelKey: string
15
+ fallbackLabel: string
16
+ format: ToolFieldFormat
17
+ value: unknown
18
+ lang?: string
19
+ }
20
+
21
+ const getFieldKey = (field: ToolFieldView, index: number) => `${field.labelKey}-${index}`
22
+
23
+ const getSectionHeader = (icon: string, label: string) => (
24
+ <div className='tool-detail-section__header'>
25
+ <Tooltip title={label} {...TOOL_TOOLTIP_PROPS}>
26
+ <span className='tool-detail-section__icon material-symbols-rounded'>{icon}</span>
27
+ </Tooltip>
28
+ </div>
29
+ )
30
+
31
+ export function ToolInlineFields({
32
+ fields,
33
+ t
34
+ }: {
35
+ fields: ToolFieldView[]
36
+ t: Translate
37
+ }) {
38
+ if (fields.length === 0) {
39
+ return null
40
+ }
41
+
42
+ return (
43
+ <div
44
+ className='tool-inline-token-list tool-inline-token-list--standalone'
45
+ aria-label={t('chat.tools.fields.details')}
46
+ >
47
+ {fields.map((field, index) => {
48
+ const label = t(field.labelKey, { defaultValue: field.fallbackLabel })
49
+ const valueText = getToolInlineValueText(field.value)
50
+ return (
51
+ <Tooltip
52
+ key={getFieldKey(field, index)}
53
+ title={
54
+ <div className='tool-tooltip-content'>
55
+ <div className='tool-tooltip-content__title'>{label}</div>
56
+ <div className='tool-tooltip-content__value'>{getToolValueText(field.value)}</div>
57
+ </div>
58
+ }
59
+ {...TOOL_TOOLTIP_PROPS}
60
+ >
61
+ <div className='tool-inline-token'>
62
+ <span className='tool-inline-token__icon material-symbols-rounded'>
63
+ {getToolFieldIcon(field.labelKey, field.format)}
64
+ </span>
65
+ <span className='tool-inline-token__value'>{valueText}</span>
66
+ </div>
67
+ </Tooltip>
68
+ )
69
+ })}
70
+ </div>
71
+ )
72
+ }
73
+
74
+ export function renderToolBlockField(
75
+ field: ToolFieldView,
76
+ index: number,
77
+ t: Translate,
78
+ options: {
79
+ sectionClassName?: string
80
+ } = {}
81
+ ) {
82
+ const label = t(field.labelKey, { defaultValue: field.fallbackLabel })
83
+ const sectionHeader = getSectionHeader(getToolFieldIcon(field.labelKey, field.format), label)
84
+ const sectionClassName = options.sectionClassName ?? 'tool-detail-section'
85
+
86
+ if (field.format === 'text') {
87
+ return (
88
+ <div className={sectionClassName} key={getFieldKey(field, index)}>
89
+ {sectionHeader}
90
+ <div className='tool-detail-section__text'>{String(field.value)}</div>
91
+ </div>
92
+ )
93
+ }
94
+
95
+ if (field.format === 'code') {
96
+ return (
97
+ <div className={sectionClassName} key={getFieldKey(field, index)}>
98
+ {sectionHeader}
99
+ <CodeBlock
100
+ code={String(field.value)}
101
+ lang={field.lang ?? 'text'}
102
+ hideHeader={true}
103
+ />
104
+ </div>
105
+ )
106
+ }
107
+
108
+ if (field.format === 'list') {
109
+ const items = Array.isArray(field.value) ? field.value.map(item => String(item)) : []
110
+ return (
111
+ <div className={sectionClassName} key={getFieldKey(field, index)}>
112
+ {sectionHeader}
113
+ <div className='tool-detail-list'>
114
+ {items.map(listItem => (
115
+ <div className='tool-detail-list-item' key={listItem}>{listItem}</div>
116
+ ))}
117
+ </div>
118
+ </div>
119
+ )
120
+ }
121
+
122
+ return (
123
+ <div className={sectionClassName} key={getFieldKey(field, index)}>
124
+ {sectionHeader}
125
+ <CodeBlock
126
+ code={safeJsonStringify(field.value, 2)}
127
+ lang='json'
128
+ hideHeader={true}
129
+ />
130
+ </div>
131
+ )
132
+ }
@@ -0,0 +1,171 @@
1
+ import { toSerializable } from '#~/utils/safe-serialize'
2
+
3
+ interface StructuredTextBlock {
4
+ type: 'text'
5
+ text: string
6
+ format: 'text' | 'markdown'
7
+ }
8
+
9
+ interface StructuredImageBlock {
10
+ type: 'image'
11
+ src: string
12
+ alt?: string
13
+ title?: string
14
+ width?: number
15
+ height?: number
16
+ }
17
+
18
+ export type StructuredBlock = StructuredTextBlock | StructuredImageBlock
19
+
20
+ const parseStructuredInput = (value: unknown) => {
21
+ if (typeof value !== 'string') {
22
+ return value
23
+ }
24
+
25
+ const trimmed = value.trim()
26
+ if (
27
+ (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
28
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))
29
+ ) {
30
+ try {
31
+ return JSON.parse(trimmed)
32
+ } catch {
33
+ return value
34
+ }
35
+ }
36
+
37
+ return value
38
+ }
39
+
40
+ const resolveImageSource = (value: Record<string, unknown>) => {
41
+ const directUrl = typeof value.url === 'string'
42
+ ? value.url
43
+ : typeof value.src === 'string'
44
+ ? value.src
45
+ : typeof value.image_url === 'string'
46
+ ? value.image_url
47
+ : typeof value.imageUrl === 'string'
48
+ ? value.imageUrl
49
+ : typeof value.dataUrl === 'string'
50
+ ? value.dataUrl
51
+ : null
52
+ if (directUrl != null) {
53
+ return directUrl
54
+ }
55
+
56
+ const source = value.source != null && typeof value.source === 'object'
57
+ ? value.source as Record<string, unknown>
58
+ : null
59
+ const data = typeof value.data === 'string'
60
+ ? value.data
61
+ : typeof value.base64 === 'string'
62
+ ? value.base64
63
+ : source != null && typeof source.data === 'string'
64
+ ? source.data
65
+ : null
66
+ if (data == null || data === '') {
67
+ return null
68
+ }
69
+
70
+ const mimeType = typeof value.mimeType === 'string'
71
+ ? value.mimeType
72
+ : typeof value.mime_type === 'string'
73
+ ? value.mime_type
74
+ : source != null && typeof source.media_type === 'string'
75
+ ? source.media_type
76
+ : source != null && typeof source.mimeType === 'string'
77
+ ? source.mimeType
78
+ : source != null && typeof source.mime_type === 'string'
79
+ ? source.mime_type
80
+ : 'image/png'
81
+
82
+ return `data:${mimeType};base64,${data}`
83
+ }
84
+
85
+ const parseBlock = (value: unknown): StructuredBlock | null => {
86
+ if (value == null || typeof value !== 'object') {
87
+ return null
88
+ }
89
+
90
+ const obj = value as Record<string, unknown>
91
+ const rawType = typeof obj.type === 'string' ? obj.type.toLowerCase() : ''
92
+ if (rawType === 'text' || rawType === 'markdown' || rawType === 'md') {
93
+ const text = typeof obj.text === 'string'
94
+ ? obj.text
95
+ : typeof obj.content === 'string'
96
+ ? obj.content
97
+ : null
98
+ if (text == null) {
99
+ return null
100
+ }
101
+
102
+ const rawFormat = typeof obj.format === 'string' ? obj.format.toLowerCase() : 'markdown'
103
+ return {
104
+ type: 'text',
105
+ text,
106
+ format: rawType === 'text' && (rawFormat === 'text' || rawFormat === 'plain') ? 'text' : 'markdown'
107
+ }
108
+ }
109
+
110
+ if (rawType !== 'image') {
111
+ return null
112
+ }
113
+
114
+ const src = resolveImageSource(obj)
115
+ if (src == null) {
116
+ return null
117
+ }
118
+
119
+ return {
120
+ type: 'image',
121
+ src,
122
+ alt: typeof obj.alt === 'string' ? obj.alt : undefined,
123
+ title: typeof obj.title === 'string' ? obj.title : undefined,
124
+ width: typeof obj.width === 'number' ? obj.width : undefined,
125
+ height: typeof obj.height === 'number' ? obj.height : undefined
126
+ }
127
+ }
128
+
129
+ export const getStructuredBlocks = (value: unknown): StructuredBlock[] | null => {
130
+ const parsed = parseStructuredInput(toSerializable(value))
131
+ if (Array.isArray(parsed)) {
132
+ const blocks = parsed.map(parseBlock)
133
+ return blocks.every(Boolean) ? blocks as StructuredBlock[] : null
134
+ }
135
+
136
+ if (parsed != null && typeof parsed === 'object') {
137
+ const container = parsed as Record<string, unknown>
138
+ const content = container.content ?? container.items ?? container.blocks
139
+ if (Array.isArray(content)) {
140
+ const blocks = content.map(parseBlock)
141
+ return blocks.every(Boolean) ? blocks as StructuredBlock[] : null
142
+ }
143
+ }
144
+
145
+ const single = parseBlock(parsed)
146
+ return single != null ? [single] : null
147
+ }
148
+
149
+ export const getStringList = (value: unknown): string[] | null => {
150
+ const parsed = parseStructuredInput(toSerializable(value))
151
+ if (!Array.isArray(parsed) || !parsed.every(item => ['string', 'number', 'boolean'].includes(typeof item))) {
152
+ return null
153
+ }
154
+
155
+ return parsed.map(item => String(item))
156
+ }
157
+
158
+ export const looksLikeMarkdown = (value: string) => {
159
+ const trimmed = value.trim()
160
+ if (trimmed === '') {
161
+ return false
162
+ }
163
+
164
+ return (
165
+ trimmed.startsWith('```') ||
166
+ /^(?:#{1,6}\s|[-*+]\s|>\s|\d+\.\s)/m.test(trimmed) ||
167
+ /\[[^\]]+\]\([^)]+\)/.test(trimmed) ||
168
+ /!\[[^\]]*\]\([^)]+\)/.test(trimmed) ||
169
+ /\|.+\|/.test(trimmed)
170
+ )
171
+ }