@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,138 @@
1
+ import type { ChatErrorState } from '#~/hooks/chat/interaction-state'
2
+
3
+ type Translate = (key: string, options?: Record<string, unknown>) => string
4
+
5
+ export interface ChatHistoryStatusNotice {
6
+ action?: 'retry-connection'
7
+ detail?: string
8
+ icon: string
9
+ id: string
10
+ isMock?: boolean
11
+ message: string
12
+ meta?: string
13
+ tone: 'error' | 'warning'
14
+ title: string
15
+ }
16
+
17
+ const createModelUnavailableNotice = (t: Translate, isMock = false): ChatHistoryStatusNotice => ({
18
+ icon: 'settings_suggest',
19
+ id: isMock ? 'mock-model-unavailable' : 'model-unavailable',
20
+ isMock,
21
+ message: t('chat.modelConfigRequired'),
22
+ detail: t('chat.modelConfigRequiredHelp'),
23
+ tone: 'warning',
24
+ title: t('chat.modelConfigRequiredTitle')
25
+ })
26
+
27
+ const createConnectionNotice = (
28
+ t: Translate,
29
+ state: ChatErrorState,
30
+ isMock = false
31
+ ): ChatHistoryStatusNotice => {
32
+ const isClosed = state.reason === 'closed'
33
+
34
+ return {
35
+ action: isMock ? undefined : 'retry-connection',
36
+ detail: isClosed ? t('chat.connectionClosedHelp') : t('chat.connectionErrorHelp'),
37
+ icon: isClosed ? 'wifi_off' : 'cloud_off',
38
+ id: isMock
39
+ ? isClosed ? 'mock-connection-closed' : 'mock-connection-error'
40
+ : isClosed
41
+ ? 'connection-closed'
42
+ : 'connection-error',
43
+ isMock,
44
+ message: state.message,
45
+ tone: 'error',
46
+ title: isClosed ? t('chat.connectionClosedTitle') : t('chat.connectionErrorTitle')
47
+ }
48
+ }
49
+
50
+ const createSessionNotice = (
51
+ t: Translate,
52
+ state: ChatErrorState,
53
+ isMock = false
54
+ ): ChatHistoryStatusNotice => ({
55
+ detail: t('chat.sessionErrorHelp'),
56
+ icon: 'error',
57
+ id: isMock ? 'mock-session-error' : 'session-error',
58
+ isMock,
59
+ message: state.message,
60
+ meta: state.code != null && state.code !== ''
61
+ ? t('chat.sessionErrorCode', { code: state.code })
62
+ : undefined,
63
+ tone: 'error',
64
+ title: t('chat.sessionErrorTitle')
65
+ })
66
+
67
+ export const buildChatHistoryStatusNotices = ({
68
+ errorState,
69
+ isDebugMode,
70
+ modelUnavailable,
71
+ t
72
+ }: {
73
+ errorState?: ChatErrorState | null
74
+ isDebugMode: boolean
75
+ modelUnavailable: boolean
76
+ t: Translate
77
+ }) => {
78
+ const notices: ChatHistoryStatusNotice[] = []
79
+ const activeScenarios = new Set<string>()
80
+
81
+ if (modelUnavailable) {
82
+ notices.push(createModelUnavailableNotice(t))
83
+ activeScenarios.add('model-unavailable')
84
+ }
85
+
86
+ if (errorState != null && errorState.message.trim() !== '') {
87
+ if (errorState.kind === 'session') {
88
+ notices.push(createSessionNotice(t, errorState))
89
+ activeScenarios.add('session-error')
90
+ } else {
91
+ notices.push(createConnectionNotice(t, errorState))
92
+ activeScenarios.add(errorState.reason === 'closed' ? 'connection-closed' : 'connection-error')
93
+ }
94
+ }
95
+
96
+ if (!isDebugMode) {
97
+ return notices
98
+ }
99
+
100
+ const mockNotices: Array<{ notice: ChatHistoryStatusNotice; scenario: string }> = [
101
+ {
102
+ scenario: 'connection-error',
103
+ notice: createConnectionNotice(t, {
104
+ kind: 'connection',
105
+ message: t('chat.debugMockConnectionErrorMessage'),
106
+ reason: 'error'
107
+ }, true)
108
+ },
109
+ {
110
+ scenario: 'connection-closed',
111
+ notice: createConnectionNotice(t, {
112
+ kind: 'connection',
113
+ message: t('chat.debugMockConnectionClosedMessage'),
114
+ reason: 'closed'
115
+ }, true)
116
+ },
117
+ {
118
+ scenario: 'session-error',
119
+ notice: createSessionNotice(t, {
120
+ kind: 'session',
121
+ message: t('chat.debugMockSessionErrorMessage'),
122
+ code: 'session_timeout'
123
+ }, true)
124
+ },
125
+ {
126
+ scenario: 'model-unavailable',
127
+ notice: createModelUnavailableNotice(t, true)
128
+ }
129
+ ]
130
+
131
+ for (const mock of mockNotices) {
132
+ if (!activeScenarios.has(mock.scenario)) {
133
+ notices.push(mock.notice)
134
+ }
135
+ }
136
+
137
+ return notices
138
+ }
@@ -1,11 +1,9 @@
1
- import { Button } from 'antd'
2
1
  import type { MutableRefObject } from 'react'
3
2
  import { useTranslation } from 'react-i18next'
4
3
 
5
4
  import type { SessionInfo } from '@vibe-forge/types'
6
5
 
7
6
  import { ContextFilePicker } from '#~/components/workspace/ContextFilePicker'
8
- import type { ChatErrorBannerState } from '#~/hooks/chat/interaction-state'
9
7
 
10
8
  import type {
11
9
  SenderToolbarData,
@@ -25,8 +23,6 @@ export function SenderBody({
25
23
  isInlineEdit,
26
24
  isBusy,
27
25
  modelUnavailable,
28
- errorBanner,
29
- onRetryConnection,
30
26
  pendingImages,
31
27
  pendingFiles,
32
28
  onRemovePendingImage,
@@ -52,8 +48,6 @@ export function SenderBody({
52
48
  isInlineEdit: boolean
53
49
  isBusy: boolean
54
50
  modelUnavailable?: boolean
55
- errorBanner?: ChatErrorBannerState | null
56
- onRetryConnection?: () => void
57
51
  pendingImages: Parameters<typeof SenderAttachments>[0]['pendingImages']
58
52
  pendingFiles: Parameters<typeof SenderAttachments>[0]['pendingFiles']
59
53
  onRemovePendingImage: (id: string) => void
@@ -81,27 +75,9 @@ export function SenderBody({
81
75
  onConfirmContextPicker: (files: PendingContextFile[]) => void
82
76
  }) {
83
77
  const { t } = useTranslation()
84
- const errorTitle = errorBanner?.kind === 'session'
85
- ? t('chat.sessionErrorTitle')
86
- : t('chat.connectionErrorTitle')
87
78
 
88
79
  return (
89
80
  <div className={`chat-input-container ${isInlineEdit ? 'chat-input-container--inline-edit' : ''}`.trim()}>
90
- {!isInlineEdit && errorBanner != null && errorBanner.message.trim() !== '' && (
91
- <div className='connection-error-banner'>
92
- <div className='connection-error-content'>
93
- <span className='material-symbols-rounded'>error</span>
94
- <div className='connection-error-copy'>
95
- <div className='connection-error-title'>{errorTitle}</div>
96
- <div className='connection-error-message'>{errorBanner.message}</div>
97
- </div>
98
- </div>
99
- {errorBanner.kind === 'connection' && onRetryConnection != null && (
100
- <Button size='small' onClick={onRetryConnection}>{t('chat.retryConnection')}</Button>
101
- )}
102
- </div>
103
- )}
104
- {!isInlineEdit && modelUnavailable && <div className='model-unavailable'>{t('chat.modelConfigRequired')}</div>}
105
81
  <SenderAttachments
106
82
  pendingImages={pendingImages}
107
83
  pendingFiles={pendingFiles}
@@ -49,7 +49,6 @@ export const buildSenderControllerResult = ({
49
49
  attachments,
50
50
  completion,
51
51
  composer,
52
- errorBanner,
53
52
  focusRestore,
54
53
  handleKeyDown,
55
54
  hideSender,
@@ -59,7 +58,6 @@ export const buildSenderControllerResult = ({
59
58
  isInlineEdit,
60
59
  isThinking,
61
60
  modelUnavailable,
62
- onRetryConnection,
63
61
  permissionContext,
64
62
  placeholder,
65
63
  editorRef,
@@ -68,7 +66,6 @@ export const buildSenderControllerResult = ({
68
66
  attachments: SenderControllerAttachments
69
67
  completion: SenderControllerCompletion
70
68
  composer: SenderControllerComposer
71
- errorBanner?: SenderProps['errorBanner']
72
69
  focusRestore: { queueEditorFocusRestore: () => void }
73
70
  handleKeyDown: (event: KeyboardEvent) => void
74
71
  hideSender: boolean
@@ -78,7 +75,6 @@ export const buildSenderControllerResult = ({
78
75
  isInlineEdit: boolean
79
76
  isThinking: boolean
80
77
  modelUnavailable?: boolean
81
- onRetryConnection?: (() => void) | undefined
82
78
  permissionContext?: {
83
79
  deniedTools?: string[]
84
80
  reasons?: string[]
@@ -102,8 +98,6 @@ export const buildSenderControllerResult = ({
102
98
  handleKeyDown,
103
99
  interactionRequest,
104
100
  interactionResponse,
105
- errorBanner,
106
- onRetryConnection,
107
101
  modelUnavailable,
108
102
  placeholder,
109
103
  onInputChange: completion.handleInputChange,
@@ -174,7 +174,6 @@ export const useSenderController = (props: SenderProps) => {
174
174
  attachments,
175
175
  completion,
176
176
  composer,
177
- errorBanner: props.errorBanner,
178
177
  focusRestore,
179
178
  handleKeyDown,
180
179
  hideSender,
@@ -184,7 +183,6 @@ export const useSenderController = (props: SenderProps) => {
184
183
  isInlineEdit,
185
184
  isThinking,
186
185
  modelUnavailable: props.modelUnavailable,
187
- onRetryConnection: props.onRetryConnection,
188
186
  permissionContext,
189
187
  editorRef,
190
188
  placeholder: props.placeholder ?? props.interactionRequest?.payload.question ?? t('chat.inputPlaceholder'),
@@ -3,7 +3,6 @@ import type { ReactNode } from 'react'
3
3
  import type { AskUserQuestionParams, ChatMessageContent, SessionStatus } from '@vibe-forge/core'
4
4
  import type { SessionInfo } from '@vibe-forge/types'
5
5
 
6
- import type { ChatErrorBannerState } from '#~/hooks/chat/interaction-state'
7
6
  import type { ChatEffort } from '#~/hooks/chat/use-chat-effort'
8
7
  import type { ModelSelectMenuGroup, ModelSelectOption } from '#~/hooks/chat/use-chat-model-adapter-selection'
9
8
  import type { PermissionMode } from '#~/hooks/chat/use-chat-permission-mode'
@@ -19,8 +18,6 @@ export interface SenderProps {
19
18
  onInterrupt: () => void
20
19
  onClear?: () => void
21
20
  sessionInfo?: SessionInfo | null
22
- errorBanner?: ChatErrorBannerState | null
23
- onRetryConnection?: () => void
24
21
  interactionRequest?: { id: string; payload: AskUserQuestionParams } | null
25
22
  onInteractionResponse?: (id: string, data: string | string[]) => void
26
23
  placeholder?: string
@@ -27,64 +27,6 @@
27
27
  .chat-input-monaco {
28
28
  color: var(--text-color);
29
29
  }
30
-
31
- .model-unavailable {
32
- margin-bottom: 6px;
33
- padding: 6px 8px;
34
- border-radius: 8px;
35
- border: 1px dashed var(--border-color);
36
- background-color: var(--tag-bg, #f9fafb);
37
- color: var(--placeholder-color);
38
- font-size: 12px;
39
- }
40
-
41
- .connection-error-banner {
42
- display: flex;
43
- align-items: center;
44
- justify-content: space-between;
45
- gap: 12px;
46
- margin-bottom: 8px;
47
- padding: 10px 12px;
48
- border-radius: 10px;
49
- border: 1px solid rgba(220, 38, 38, .2);
50
- background: linear-gradient(
51
- 135deg,
52
- rgba(254, 242, 242, .95),
53
- rgba(255, 255, 255, .92)
54
- );
55
- }
56
-
57
- .connection-error-content {
58
- display: flex;
59
- align-items: flex-start;
60
- gap: 10px;
61
- min-width: 0;
62
-
63
- .material-symbols-rounded {
64
- color: #dc2626;
65
- font-size: 18px;
66
- line-height: 1;
67
- margin-top: 1px;
68
- }
69
- }
70
-
71
- .connection-error-copy {
72
- min-width: 0;
73
- }
74
-
75
- .connection-error-title {
76
- color: #991b1b;
77
- font-size: 12px;
78
- font-weight: 600;
79
- }
80
-
81
- .connection-error-message {
82
- margin-top: 2px;
83
- color: #7f1d1d;
84
- font-size: 12px;
85
- line-height: 1.4;
86
- word-break: break-word;
87
- }
88
30
  }
89
31
 
90
32
  .chat-input-container--inline-edit {
@@ -33,8 +33,6 @@ export function Sender(props: SenderProps) {
33
33
  isInlineEdit={controller.isInlineEdit}
34
34
  isBusy={controller.isBusy}
35
35
  modelUnavailable={controller.modelUnavailable}
36
- errorBanner={controller.errorBanner}
37
- onRetryConnection={controller.onRetryConnection}
38
36
  pendingImages={controller.composer.pendingImages}
39
37
  pendingFiles={controller.composer.pendingFiles}
40
38
  onRemovePendingImage={(id) =>
@@ -1,172 +1,17 @@
1
- import { CodeBlock } from '#~/components/CodeBlock'
2
- import { MarkdownContent } from '#~/components/MarkdownContent'
3
- import { safeJsonStringify, toSerializable } from '#~/utils/safe-serialize'
4
- import type { ChatMessageContent } from '@vibe-forge/core'
1
+ import { useMemo } from 'react'
5
2
  import { useTranslation } from 'react-i18next'
6
- import { ToolCallBox } from './core/ToolCallBox'
7
-
8
- interface StructuredTextBlock {
9
- type: 'text'
10
- text: string
11
- format: 'text' | 'markdown'
12
- }
13
-
14
- interface StructuredImageBlock {
15
- type: 'image'
16
- src: string
17
- alt?: string
18
- title?: string
19
- width?: number
20
- height?: number
21
- }
22
-
23
- type StructuredBlock = StructuredTextBlock | StructuredImageBlock
24
3
 
25
- function parseStructuredInput(value: unknown) {
26
- if (typeof value !== 'string') {
27
- return value
28
- }
29
- const trimmed = value.trim()
30
- if (
31
- (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
32
- (trimmed.startsWith('[') && trimmed.endsWith(']'))
33
- ) {
34
- try {
35
- return JSON.parse(trimmed)
36
- } catch {
37
- return value
38
- }
39
- }
40
- return value
41
- }
42
-
43
- function resolveImageSource(value: Record<string, unknown>) {
44
- const directUrl = typeof value.url === 'string'
45
- ? value.url
46
- : typeof value.src === 'string'
47
- ? value.src
48
- : typeof value.image_url === 'string'
49
- ? value.image_url
50
- : typeof value.imageUrl === 'string'
51
- ? value.imageUrl
52
- : typeof value.dataUrl === 'string'
53
- ? value.dataUrl
54
- : null
55
- if (directUrl) {
56
- return directUrl
57
- }
58
- const source = value.source != null && typeof value.source === 'object'
59
- ? (value.source as Record<string, unknown>)
60
- : null
61
- const data = typeof value.data === 'string'
62
- ? value.data
63
- : typeof value.base64 === 'string'
64
- ? value.base64
65
- : source != null && typeof source.data === 'string'
66
- ? source.data
67
- : null
68
- if (!data) {
69
- return null
70
- }
71
- const mimeType = typeof value.mimeType === 'string'
72
- ? value.mimeType
73
- : typeof value.mime_type === 'string'
74
- ? value.mime_type
75
- : source != null && typeof source.media_type === 'string'
76
- ? source.media_type
77
- : source != null && typeof source.mimeType === 'string'
78
- ? source.mimeType
79
- : source != null && typeof source.mime_type === 'string'
80
- ? source.mime_type
81
- : 'image/png'
82
- return `data:${mimeType};base64,${data}`
83
- }
84
-
85
- function parseBlock(value: unknown): StructuredBlock | null {
86
- if (value == null || typeof value !== 'object') {
87
- return null
88
- }
89
- const obj = value as Record<string, unknown>
90
- const rawType = typeof obj.type === 'string' ? obj.type.toLowerCase() : ''
91
- if (rawType === 'text' || rawType === 'markdown' || rawType === 'md') {
92
- const text = typeof obj.text === 'string'
93
- ? obj.text
94
- : typeof obj.content === 'string'
95
- ? obj.content
96
- : null
97
- if (text == null) {
98
- return null
99
- }
100
- const rawFormat = typeof obj.format === 'string' ? obj.format.toLowerCase() : 'markdown'
101
- const format = rawType === 'text'
102
- ? (rawFormat === 'text' || rawFormat === 'plain' ? 'text' : 'markdown')
103
- : 'markdown'
104
- return { type: 'text', text, format }
105
- }
106
- if (rawType === 'image') {
107
- const src = resolveImageSource(obj)
108
- if (!src) {
109
- return null
110
- }
111
- const alt = typeof obj.alt === 'string' ? obj.alt : undefined
112
- const title = typeof obj.title === 'string' ? obj.title : undefined
113
- const width = typeof obj.width === 'number' ? obj.width : undefined
114
- const height = typeof obj.height === 'number' ? obj.height : undefined
115
- return { type: 'image', src, alt, title, width, height }
116
- }
117
- return null
118
- }
119
-
120
- function getStructuredBlocks(value: unknown): StructuredBlock[] | null {
121
- const serializable = toSerializable(value)
122
- const parsed = parseStructuredInput(serializable)
123
- if (Array.isArray(parsed)) {
124
- const blocks = parsed.map(parseBlock)
125
- return blocks.every(Boolean) ? (blocks as StructuredBlock[]) : null
126
- }
127
- if (parsed != null && typeof parsed === 'object') {
128
- const container = parsed as Record<string, unknown>
129
- const content = container.content ?? container.items ?? container.blocks
130
- if (Array.isArray(content)) {
131
- const blocks = content.map(parseBlock)
132
- return blocks.every(Boolean) ? (blocks as StructuredBlock[]) : null
133
- }
134
- }
135
- const single = parseBlock(parsed)
136
- return single ? [single] : null
137
- }
4
+ import type { ChatMessageContent } from '@vibe-forge/core'
138
5
 
139
- function StructuredToolResult({ blocks }: { blocks: StructuredBlock[] }) {
140
- return (
141
- <div className='tool-result-structured'>
142
- {blocks.map((block, index) => {
143
- if (block.type === 'text') {
144
- return (
145
- <div className='tool-result-text' key={`text-${index}`}>
146
- {block.format === 'markdown'
147
- ? <MarkdownContent content={block.text} />
148
- : <div className='tool-result-text-content'>{block.text}</div>}
149
- </div>
150
- )
151
- }
152
- return (
153
- <div className='tool-result-image-wrapper' key={`image-${index}`}>
154
- <img
155
- className='tool-result-image'
156
- src={block.src}
157
- alt={block.alt ?? ''}
158
- width={block.width}
159
- height={block.height}
160
- />
161
- {block.title != null && block.title.length > 0 && (
162
- <div className='tool-result-image-caption'>{block.title}</div>
163
- )}
164
- </div>
165
- )
166
- })}
167
- </div>
168
- )
169
- }
6
+ import { ToolCallBox } from './core/ToolCallBox'
7
+ import { ToolDiffViewer } from './core/ToolDiffViewer'
8
+ import { ToolResultContent } from './core/ToolResultContent'
9
+ import { ToolSummaryHeader } from './core/ToolSummaryHeader'
10
+ import { buildGenericToolPresentation } from './core/generic-tool-presentation'
11
+ import { hasMeaningfulToolValue } from './core/tool-content-presence'
12
+ import { getToolTargetPresentation } from './core/tool-display'
13
+ import { ToolInlineFields, renderToolBlockField } from './core/tool-field-sections'
14
+ import { getToolPrimaryText, getToolTitleText } from './core/tool-summary'
170
15
 
171
16
  export function DefaultTool({
172
17
  item,
@@ -176,51 +21,82 @@ export function DefaultTool({
176
21
  resultItem?: Extract<ChatMessageContent, { type: 'tool_result' }>
177
22
  }) {
178
23
  const { t } = useTranslation()
179
- const structuredBlocks = resultItem != null ? getStructuredBlocks(resultItem.content) : null
24
+ const view = useMemo(() => buildGenericToolPresentation(item.name, item.input), [item.input, item.name])
25
+ const hasCallDetails = view.inlineFields.length > 0 || view.blockFields.length > 0 || view.diff != null
26
+ const hasResultDetails = resultItem != null && hasMeaningfulToolValue(resultItem.content)
27
+ const showResultDetails = hasResultDetails && !(view.suppressSuccessResult === true && resultItem?.is_error !== true)
28
+ const hasDetails = hasCallDetails || showResultDetails
29
+ const titleText = view.titleKey != null
30
+ ? t(view.titleKey, { defaultValue: view.fallbackTitle })
31
+ : getToolTitleText(item, t)
32
+ const targetPresentation = getToolTargetPresentation(view.primary ?? getToolPrimaryText(item))
33
+ const preferMarkdown = ['webfetch', 'websearch'].includes(
34
+ item.name.split(':').pop()?.replace(/[^a-z0-9]+/gi, '').toLowerCase() ?? ''
35
+ )
36
+ const errorMeta = resultItem?.is_error === true
37
+ ? (
38
+ <span className='tool-status tool-status--error'>
39
+ <span className='material-symbols-rounded'>error</span>
40
+ </span>
41
+ )
42
+ : undefined
43
+
180
44
  return (
181
- <div className='tool-group'>
45
+ <div className='tool-group tool-group--compact'>
182
46
  <ToolCallBox
183
- header={
184
- <div className='tool-header-content'>
185
- <span className='material-symbols-rounded tool-header-icon'>build</span>
186
- <span className='tool-header-title'>{item.name}</span>
187
- <span className='tool-header-hint'>{t('chat.tools.call')}</span>
188
- </div>
189
- }
190
- content={
191
- <div className='tool-content'>
192
- <CodeBlock
193
- code={safeJsonStringify(item.input != null ? item.input : {}, 2)}
194
- lang='json'
195
- />
196
- </div>
197
- }
198
- />
199
- {resultItem != null && (
200
- <ToolCallBox
201
- type='result'
202
- isError={resultItem.is_error}
203
- header={
204
- <div className='tool-header-content'>
205
- <span className='material-symbols-rounded tool-header-icon'>
206
- {resultItem.is_error === true ? 'error' : 'check_circle'}
207
- </span>
208
- <span className='tool-header-title'>{t('chat.result')}</span>
47
+ variant='inline'
48
+ defaultExpanded={false}
49
+ collapsible={hasDetails}
50
+ header={({ isExpanded, isCollapsible }) => (
51
+ <ToolSummaryHeader
52
+ icon={<span className='material-symbols-rounded'>{view.icon}</span>}
53
+ title={titleText}
54
+ target={targetPresentation.text}
55
+ targetTitle={targetPresentation.title}
56
+ targetMonospace={targetPresentation.monospace}
57
+ expanded={isExpanded}
58
+ collapsible={isCollapsible}
59
+ meta={errorMeta}
60
+ metaTitle={errorMeta == null ? undefined : t('chat.result')}
61
+ />
62
+ )}
63
+ content={hasDetails
64
+ ? (
65
+ <div className='tool-detail-sections'>
66
+ <ToolInlineFields fields={view.inlineFields} t={t} />
67
+ {view.diff != null && (
68
+ <div className='tool-detail-section'>
69
+ <ToolDiffViewer
70
+ original={view.diff.original}
71
+ modified={view.diff.modified}
72
+ language={view.diff.language}
73
+ metaItems={(view.diff.metaItems ?? []).map(item => ({
74
+ icon: item.icon,
75
+ label: t(item.labelKey, { defaultValue: item.fallbackLabel }),
76
+ value: item.value != null && item.value !== ''
77
+ ? (item.value === 'true'
78
+ ? t('chat.tools.booleanOn')
79
+ : item.value === 'false'
80
+ ? t('chat.tools.booleanOff')
81
+ : item.value)
82
+ : undefined,
83
+ tone: item.tone
84
+ }))}
85
+ splitLabel={t('chat.tools.diffSplit')}
86
+ inlineLabel={t('chat.tools.diffInline')}
87
+ />
88
+ </div>
89
+ )}
90
+ {view.blockFields.map((field, index) => renderToolBlockField(field, index, t))}
91
+ {showResultDetails && resultItem != null && (
92
+ <div className='tool-detail-section'>
93
+ <ToolResultContent content={resultItem.content} preferMarkdown={preferMarkdown} />
94
+ </div>
95
+ )}
209
96
  </div>
210
- }
211
- content={
212
- <div className='tool-content'>
213
- {structuredBlocks != null
214
- ? <StructuredToolResult blocks={structuredBlocks} />
215
- : (typeof resultItem.content === 'string'
216
- ? (resultItem.content.startsWith('```')
217
- ? <MarkdownContent content={resultItem.content} />
218
- : <CodeBlock code={resultItem.content} lang='text' />)
219
- : <CodeBlock code={safeJsonStringify(resultItem.content, 2)} lang='json' />)}
220
- </div>
221
- }
222
- />
223
- )}
97
+ )
98
+ : null}
99
+ />
224
100
  </div>
225
101
  )
226
102
  }
@@ -0,0 +1,30 @@
1
+ import React from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+
4
+ import { ToolDiffViewer } from '../core/ToolDiffViewer'
5
+ import type { ToolDiffMetaItem } from '../core/ToolDiffViewer'
6
+
7
+ export function ClaudeEditDiff({
8
+ oldValue,
9
+ newValue,
10
+ lang,
11
+ metaItems = []
12
+ }: {
13
+ oldValue?: string
14
+ newValue?: string
15
+ lang?: string
16
+ metaItems?: ToolDiffMetaItem[]
17
+ }) {
18
+ const { t } = useTranslation()
19
+
20
+ return (
21
+ <ToolDiffViewer
22
+ original={oldValue ?? ''}
23
+ modified={newValue ?? ''}
24
+ language={lang}
25
+ metaItems={metaItems}
26
+ splitLabel={t('chat.tools.diffSplit')}
27
+ inlineLabel={t('chat.tools.diffInline')}
28
+ />
29
+ )
30
+ }