@vibe-forge/client 0.2.0-alpha.9 → 0.4.0

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 (166) hide show
  1. package/cli.cjs +1 -1
  2. package/dist/assets/{arc-CybT1Fs2.js → arc-DgIxeTMg.js} +1 -1
  3. package/dist/assets/{blockDiagram-c4efeb88-BY5Aoa-D.js → blockDiagram-c4efeb88-CEAob3X9.js} +1 -1
  4. package/dist/assets/{c4Diagram-c83219d4-F42hTbzS.js → c4Diagram-c83219d4-DwIxpDKd.js} +1 -1
  5. package/dist/assets/channel-DhtnrNJ6.js +1 -0
  6. package/dist/assets/{classDiagram-beda092f-D-tIPp-3.js → classDiagram-beda092f-Cz1q8u_0.js} +1 -1
  7. package/dist/assets/{classDiagram-v2-2358418a-J57aCe6u.js → classDiagram-v2-2358418a-CImgTuwd.js} +1 -1
  8. package/dist/assets/clone-7bHB6YkC.js +1 -0
  9. package/dist/assets/{createText-1719965b-ByfEqOF-.js → createText-1719965b-C1_HJcCc.js} +1 -1
  10. package/dist/assets/devicon-BWlTeAUU.woff +0 -0
  11. package/dist/assets/devicon-CirD-cQx.ttf +0 -0
  12. package/dist/assets/devicon-Dg8iWy0i.svg +1211 -0
  13. package/dist/assets/devicon-TqfHp33-.eot +0 -0
  14. package/dist/assets/{edges-96097737-CMEArkOa.js → edges-96097737-BU8qStzd.js} +1 -1
  15. package/dist/assets/{erDiagram-0228fc6a-Cf8mX2aj.js → erDiagram-0228fc6a-DNA1Fz2L.js} +1 -1
  16. package/dist/assets/{flowDb-c6c81e3f-DG6WKyo7.js → flowDb-c6c81e3f-DjiCStMN.js} +1 -1
  17. package/dist/assets/{flowDiagram-50d868cf-CstUxz-w.js → flowDiagram-50d868cf-CSDi0-RD.js} +1 -1
  18. package/dist/assets/flowDiagram-v2-4f6560a1-_13Sz5Wh.js +1 -0
  19. package/dist/assets/{flowchart-elk-definition-6af322e1--4CRoQ-H.js → flowchart-elk-definition-6af322e1-DrhIMas7.js} +1 -1
  20. package/dist/assets/{ganttDiagram-a2739b55-DYgHcKd-.js → ganttDiagram-a2739b55-CTZnUP5z.js} +1 -1
  21. package/dist/assets/{gitGraphDiagram-82fe8481-DDSVpfsd.js → gitGraphDiagram-82fe8481-COOW7jTi.js} +1 -1
  22. package/dist/assets/{graph-CRWF39gX.js → graph-CIkpD4Kx.js} +1 -1
  23. package/dist/assets/{index-5325376f-W1hft795.js → index-5325376f-aVVRRTIu.js} +1 -1
  24. package/dist/assets/index-D1giUI7r.css +1 -0
  25. package/dist/assets/index-DRSI_ZIL.js +514 -0
  26. package/dist/assets/{infoDiagram-8eee0895-D4SHcix6.js → infoDiagram-8eee0895-DQpZ1LVD.js} +1 -1
  27. package/dist/assets/{journeyDiagram-c64418c1-MWgCkVoE.js → journeyDiagram-c64418c1-DoKguIuk.js} +1 -1
  28. package/dist/assets/{layout-C88ObkCf.js → layout-Tnmha8Nh.js} +1 -1
  29. package/dist/assets/{line-C7WAYMt5.js → line-BQR2SOyl.js} +1 -1
  30. package/dist/assets/{linear-C4msxfcU.js → linear-DlG0eemV.js} +1 -1
  31. package/dist/assets/{mermaid.core-Cabag9SZ.js → mermaid.core-BnwYO0He.js} +6 -6
  32. package/dist/assets/{mindmap-definition-8da855dc-CeS8ETXx.js → mindmap-definition-8da855dc-BllYwDID.js} +1 -1
  33. package/dist/assets/{pieDiagram-a8764435-BvjyKnq5.js → pieDiagram-a8764435-DwCkhPVc.js} +1 -1
  34. package/dist/assets/{quadrantDiagram-1e28029f-DzYvpbNM.js → quadrantDiagram-1e28029f-c40GKTU0.js} +1 -1
  35. package/dist/assets/{requirementDiagram-08caed73-DHIoDbyo.js → requirementDiagram-08caed73-DnQp2Tk6.js} +1 -1
  36. package/dist/assets/{sankeyDiagram-a04cb91d-BFSGnQGs.js → sankeyDiagram-a04cb91d-CnJrs13b.js} +1 -1
  37. package/dist/assets/{sequenceDiagram-c5b8d532-_LM3BJ5-.js → sequenceDiagram-c5b8d532-1YBwnpKu.js} +1 -1
  38. package/dist/assets/{stateDiagram-1ecb1508-DwORjOzl.js → stateDiagram-1ecb1508-BFBxQ6Fh.js} +1 -1
  39. package/dist/assets/{stateDiagram-v2-c2b004d7-B4cAWWz1.js → stateDiagram-v2-c2b004d7-Dmechvv2.js} +1 -1
  40. package/dist/assets/{styles-b4e223ce-D_rmV3B_.js → styles-b4e223ce-DWWfWX8O.js} +1 -1
  41. package/dist/assets/{styles-ca3715f6-BFx4VuFc.js → styles-ca3715f6-CKKvZxaU.js} +1 -1
  42. package/dist/assets/{styles-d45a18b0-BE3106vL.js → styles-d45a18b0-dKMOUh9p.js} +1 -1
  43. package/dist/assets/{svgDrawCommon-b86b1483-DwDTO1op.js → svgDrawCommon-b86b1483-CBgjChPM.js} +1 -1
  44. package/dist/assets/{timeline-definition-faaaa080-C4b8qUQZ.js → timeline-definition-faaaa080-NCt-HHmb.js} +1 -1
  45. package/dist/assets/{xychartDiagram-f5964ef8-BRJ9Z4u-.js → xychartDiagram-f5964ef8-BJhXS4dG.js} +1 -1
  46. package/dist/index.html +2 -7
  47. package/index.html +0 -5
  48. package/package.json +11 -6
  49. package/src/App.tsx +2 -0
  50. package/src/api/README.md +26 -0
  51. package/src/api/automation.ts +88 -0
  52. package/src/api/base.ts +54 -0
  53. package/src/api/benchmark.ts +45 -0
  54. package/src/api/config.ts +24 -0
  55. package/src/api/knowledge.ts +72 -0
  56. package/src/api/projects.ts +15 -0
  57. package/src/api/sessions.ts +82 -0
  58. package/src/api/types.ts +20 -0
  59. package/src/api.ts +44 -241
  60. package/src/components/AutomationView/AutomationView.scss +5 -1
  61. package/src/components/AutomationView/RuleFormPanel.tsx +3 -2
  62. package/src/components/AutomationView/TaskList.scss +4 -6
  63. package/src/components/AutomationView/TaskList.tsx +2 -1
  64. package/src/components/AutomationView/TriggerList.scss +4 -1
  65. package/src/components/BenchmarkView/BenchmarkCasePanel.scss +267 -0
  66. package/src/components/BenchmarkView/BenchmarkCasePanel.tsx +309 -0
  67. package/src/components/BenchmarkView/BenchmarkSidebar.scss +182 -0
  68. package/src/components/BenchmarkView/BenchmarkSidebar.tsx +262 -0
  69. package/src/components/BenchmarkView/BenchmarkView.scss +78 -0
  70. package/src/components/BenchmarkView/index.tsx +197 -0
  71. package/src/components/BenchmarkView/types.ts +10 -0
  72. package/src/components/BenchmarkView/utils.ts +21 -0
  73. package/src/components/Chat.tsx +37 -29
  74. package/src/components/{chat/CodeBlock.tsx → CodeBlock.tsx} +3 -1
  75. package/src/components/ConfigView.tsx +13 -1
  76. package/src/components/{chat/MarkdownContent.tsx → MarkdownContent.tsx} +1 -1
  77. package/src/components/NavRail.tsx +7 -0
  78. package/src/components/chat/ChatHeader.scss +37 -19
  79. package/src/components/chat/ChatHeader.tsx +6 -9
  80. package/src/components/chat/ChatHistoryView.tsx +89 -45
  81. package/src/components/chat/CurrentTodoList.tsx +10 -9
  82. package/src/components/chat/{MessageItem.scss → Messages/MessageItem.scss} +14 -0
  83. package/src/components/chat/{MessageItem.tsx → Messages/MessageItem.tsx} +30 -8
  84. package/src/components/chat/{messageUtils.ts → Messages/message-utils.ts} +1 -1
  85. package/src/components/chat/NewSessionGuide.scss +35 -13
  86. package/src/components/chat/NewSessionGuide.tsx +20 -10
  87. package/src/components/chat/{Sender.scss → Sender/Sender.scss} +80 -0
  88. package/src/components/chat/{Sender.tsx → Sender/Sender.tsx} +161 -5
  89. package/src/components/chat/tools/DefaultTool.tsx +184 -21
  90. package/src/components/chat/tools/adapter-claude/BashTool.scss +67 -51
  91. package/src/components/chat/tools/adapter-claude/BashTool.tsx +83 -49
  92. package/src/components/chat/tools/adapter-claude/GlobTool.scss +0 -79
  93. package/src/components/chat/tools/adapter-claude/GlobTool.tsx +16 -36
  94. package/src/components/chat/tools/adapter-claude/GrepTool.scss +0 -87
  95. package/src/components/chat/tools/adapter-claude/GrepTool.tsx +22 -41
  96. package/src/components/chat/tools/adapter-claude/LSTool.scss +0 -79
  97. package/src/components/chat/tools/adapter-claude/LSTool.tsx +15 -15
  98. package/src/components/chat/tools/adapter-claude/ReadTool.scss +0 -55
  99. package/src/components/chat/tools/adapter-claude/ReadTool.tsx +20 -42
  100. package/src/components/chat/tools/adapter-claude/TodoTool.scss +8 -23
  101. package/src/components/chat/tools/adapter-claude/TodoTool.tsx +24 -11
  102. package/src/components/chat/tools/adapter-claude/WriteTool.scss +21 -69
  103. package/src/components/chat/tools/adapter-claude/WriteTool.tsx +22 -58
  104. package/src/components/chat/tools/adapter-claude/index.ts +4 -10
  105. package/src/components/chat/tools/adapter-claude/utils.ts +54 -0
  106. package/src/components/chat/tools/core/ToolCallBox.scss +356 -0
  107. package/src/components/chat/{ToolGroup.tsx → tools/core/ToolGroup.tsx} +26 -7
  108. package/src/components/chat/{ToolRenderer.tsx → tools/core/ToolRenderer.tsx} +6 -4
  109. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.scss +11 -0
  110. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +75 -0
  111. package/src/components/chat/tools/plugin-chrome-devtools/index.ts +45 -0
  112. package/src/components/chat/tools/task/GetTaskInfoTool.scss +2 -27
  113. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +48 -38
  114. package/src/components/chat/tools/task/ListTasksTool.scss +3 -28
  115. package/src/components/chat/tools/task/ListTasksTool.tsx +11 -8
  116. package/src/components/chat/tools/task/StartTasksTool.scss +3 -28
  117. package/src/components/chat/tools/task/StartTasksTool.tsx +14 -17
  118. package/src/components/chat/tools/task/components/TaskRow.scss +105 -0
  119. package/src/components/chat/tools/task/components/TaskRow.tsx +163 -0
  120. package/src/components/chat/tools/task/components/TaskToolCard.scss +15 -15
  121. package/src/components/chat/tools/task/components/TaskToolCard.tsx +8 -6
  122. package/src/components/config/AppSettingsPanel.tsx +33 -0
  123. package/src/components/config/ConfigSectionForm.tsx +12 -1
  124. package/src/components/config/channelDefinitions.ts +6 -0
  125. package/src/components/config/configSchema.ts +10 -1
  126. package/src/components/config/recordEditors/ChannelRecordEditor.scss +1 -0
  127. package/src/components/config/recordEditors/ChannelRecordEditor.tsx +397 -0
  128. package/src/components/config/recordEditors/index.tsx +1 -0
  129. package/src/components/knowledge-base/KnowledgeBaseView.tsx +51 -3
  130. package/src/components/knowledge-base/components/RuleItem.tsx +79 -0
  131. package/src/components/knowledge-base/components/RuleList.scss +5 -0
  132. package/src/components/knowledge-base/components/RuleList.tsx +70 -0
  133. package/src/components/knowledge-base/components/RulesTab.tsx +32 -7
  134. package/src/components/knowledge-base/components/SpecItem.tsx +1 -1
  135. package/src/hooks/chat/use-chat-interaction.ts +26 -0
  136. package/src/{components/chat/useChatModels.tsx → hooks/chat/use-chat-models.tsx} +65 -16
  137. package/src/hooks/chat/use-chat-permission-mode.ts +47 -0
  138. package/src/hooks/chat/use-chat-scroll.ts +51 -0
  139. package/src/hooks/chat/use-chat-session-actions.ts +147 -0
  140. package/src/hooks/chat/use-chat-session-messages.ts +250 -0
  141. package/src/hooks/chat/use-chat-session.ts +57 -0
  142. package/src/hooks/chat/use-chat-view.ts +39 -0
  143. package/src/main.tsx +10 -13
  144. package/src/resources/locales/en.json +73 -0
  145. package/src/resources/locales/zh.json +73 -0
  146. package/src/runtime-config.ts +52 -0
  147. package/src/store/index.ts +2 -0
  148. package/src/vite-env.d.ts +11 -0
  149. package/src/ws.ts +5 -3
  150. package/vite.config.ts +12 -4
  151. package/dist/assets/channel-DrWdSpqV.js +0 -1
  152. package/dist/assets/clone-D0cC8LLB.js +0 -1
  153. package/dist/assets/flowDiagram-v2-4f6560a1-Bf_DH7dp.js +0 -1
  154. package/dist/assets/index-CNMzWvKV.js +0 -497
  155. package/dist/assets/index-PEmISxiy.css +0 -1
  156. package/src/components/chat/ToolCallBox.scss +0 -137
  157. package/src/components/chat/useChatSession.ts +0 -370
  158. /package/src/components/{chat/CodeBlock.scss → CodeBlock.scss} +0 -0
  159. /package/src/components/chat/{MessageFooter.tsx → Messages/MessageFooter.tsx} +0 -0
  160. /package/src/components/chat/{CompletionMenu.scss → Sender/CompletionMenu.scss} +0 -0
  161. /package/src/components/chat/{CompletionMenu.tsx → Sender/CompletionMenu.tsx} +0 -0
  162. /package/src/components/chat/{ThinkingStatus.scss → Sender/ThinkingStatus.scss} +0 -0
  163. /package/src/components/chat/{ThinkingStatus.tsx → Sender/ThinkingStatus.tsx} +0 -0
  164. /package/src/components/chat/{ToolCallBox.tsx → tools/core/ToolCallBox.tsx} +0 -0
  165. /package/src/components/chat/{ToolGroup.scss → tools/core/ToolGroup.scss} +0 -0
  166. /package/src/{components/chat/safeSerialize.ts → utils/safe-serialize.ts} +0 -0
@@ -1,12 +1,15 @@
1
- import React, { useMemo } from 'react'
1
+ import React, { useEffect, useMemo, useRef } from 'react'
2
2
 
3
- import type { AskUserQuestionParams, ChatMessage, Session, SessionInfo } from '@vibe-forge/core'
3
+ import type { AskUserQuestionParams, ChatMessage, ChatMessageContent, Session, SessionInfo } from '@vibe-forge/core'
4
4
  import { CurrentTodoList } from './CurrentTodoList'
5
- import { MessageItem } from './MessageItem'
5
+ import { MessageItem } from './Messages/MessageItem'
6
6
  import { NewSessionGuide } from './NewSessionGuide'
7
- import { Sender } from './Sender'
8
- import { ToolGroup } from './ToolGroup'
9
- import { processMessages } from './messageUtils'
7
+ import { Sender } from './Sender/Sender'
8
+ import { ToolGroup } from './tools/core/ToolGroup'
9
+ import { processMessages } from './Messages/message-utils'
10
+ import type { PermissionMode } from '#~/hooks/chat/use-chat-permission-mode'
11
+ import { useChatScroll } from '#~/hooks/chat/use-chat-scroll'
12
+ import { useChatSessionActions } from '#~/hooks/chat/use-chat-session-actions'
10
13
 
11
14
  interface ModelSelectOption {
12
15
  value: string
@@ -24,68 +27,105 @@ export function ChatHistoryView({
24
27
  messages,
25
28
  session,
26
29
  sessionInfo,
27
- isCreating,
28
- showScrollBottom,
29
- messagesContainerRef,
30
- messagesEndRef,
31
- scrollToBottom,
32
30
  interactionRequest,
33
31
  onInteractionResponse,
32
+ onClearMessages,
34
33
  onSend,
35
- onInterrupt,
36
- onClear,
34
+ onSendContent,
37
35
  placeholder,
38
36
  modelOptions,
39
37
  selectedModel,
38
+ modelForQuery,
40
39
  onModelChange,
41
- modelUnavailable
40
+ permissionMode,
41
+ permissionModeOptions,
42
+ onPermissionModeChange,
43
+ modelUnavailable,
44
+ hasAvailableModels
42
45
  }: {
43
46
  isReady: boolean
44
47
  messages: ChatMessage[]
45
48
  session?: Session
46
49
  sessionInfo: SessionInfo | null
47
- isCreating: boolean
48
- showScrollBottom: boolean
49
- messagesContainerRef: React.RefObject<HTMLDivElement>
50
- messagesEndRef: React.RefObject<HTMLDivElement>
51
- scrollToBottom: (behavior?: ScrollBehavior) => void
52
50
  interactionRequest: { id: string; payload: AskUserQuestionParams } | null
53
51
  onInteractionResponse: (id: string, data: string | string[]) => void
52
+ onClearMessages: () => void
54
53
  onSend: (text: string) => void
55
- onInterrupt: () => void
56
- onClear: () => void
54
+ onSendContent: (content: ChatMessageContent[]) => void
57
55
  placeholder?: string
58
56
  modelOptions: ModelSelectGroup[]
59
57
  selectedModel?: string
58
+ modelForQuery?: string
60
59
  onModelChange: (model: string) => void
60
+ permissionMode: PermissionMode
61
+ permissionModeOptions: Array<{ value: PermissionMode; label: React.ReactNode }>
62
+ onPermissionModeChange: (mode: PermissionMode) => void
61
63
  modelUnavailable: boolean
64
+ hasAvailableModels: boolean
62
65
  }) {
66
+ const { messagesEndRef, messagesContainerRef, messagesContentRef, showScrollBottom, scrollToBottom } = useChatScroll({
67
+ messagesLength: messages.length
68
+ })
69
+ const { isCreating, send, sendContent, interrupt, clearMessages } = useChatSessionActions({
70
+ session,
71
+ modelForQuery,
72
+ hasAvailableModels,
73
+ permissionMode,
74
+ onClearMessages
75
+ })
76
+ const initialScrollDoneRef = useRef(false)
77
+ const handleSend = async (text: string) => {
78
+ await send(text)
79
+ if (session?.id) {
80
+ onSend(text)
81
+ }
82
+ }
83
+ const handleSendContent = async (content: ChatMessageContent[]) => {
84
+ await sendContent(content)
85
+ onSendContent(content)
86
+ }
87
+ useEffect(() => {
88
+ initialScrollDoneRef.current = false
89
+ }, [session?.id])
90
+ useEffect(() => {
91
+ if (!initialScrollDoneRef.current && isReady) {
92
+ scrollToBottom('auto')
93
+ initialScrollDoneRef.current = true
94
+ }
95
+ }, [isReady, messages.length, scrollToBottom])
96
+ useEffect(() => {
97
+ if (!showScrollBottom) {
98
+ scrollToBottom('auto')
99
+ }
100
+ }, [messages.length, scrollToBottom, showScrollBottom])
63
101
  const renderItems = useMemo(() => processMessages(messages), [messages])
64
102
 
65
103
  return (
66
104
  <>
67
105
  <div className={`chat-messages ${isReady ? 'ready' : ''}`} ref={messagesContainerRef}>
68
- {renderItems.map((item, index) => {
69
- if (item.type === 'message') {
70
- return (
71
- <MessageItem
72
- key={item.message.id || index}
73
- msg={item.message}
74
- isFirstInGroup={item.isFirstInGroup}
75
- />
76
- )
77
- } else if (item.type === 'tool-group') {
78
- return (
79
- <ToolGroup
80
- key={item.id || `group-${index}`}
81
- items={item.items}
82
- footer={item.footer}
83
- />
84
- )
85
- }
86
- return null
87
- })}
88
- <div ref={messagesEndRef} />
106
+ <div className='chat-messages-content' ref={messagesContentRef}>
107
+ {renderItems.map((item, index) => {
108
+ if (item.type === 'message') {
109
+ return (
110
+ <MessageItem
111
+ key={item.message.id || index}
112
+ msg={item.message}
113
+ isFirstInGroup={item.isFirstInGroup}
114
+ />
115
+ )
116
+ } else if (item.type === 'tool-group') {
117
+ return (
118
+ <ToolGroup
119
+ key={item.id || `group-${index}`}
120
+ items={item.items}
121
+ footer={item.footer}
122
+ />
123
+ )
124
+ }
125
+ return null
126
+ })}
127
+ <div ref={messagesEndRef} />
128
+ </div>
89
129
 
90
130
  {showScrollBottom && (
91
131
  <div className='scroll-bottom-btn' onClick={() => scrollToBottom()}>
@@ -103,10 +143,11 @@ export function ChatHistoryView({
103
143
  <CurrentTodoList messages={messages} />
104
144
  <div className='sender-container'>
105
145
  <Sender
106
- onSend={onSend}
146
+ onSend={handleSend}
147
+ onSendContent={handleSendContent}
107
148
  sessionStatus={isCreating ? 'running' : session?.status}
108
- onInterrupt={onInterrupt}
109
- onClear={onClear}
149
+ onInterrupt={interrupt}
150
+ onClear={clearMessages}
110
151
  sessionInfo={sessionInfo}
111
152
  interactionRequest={interactionRequest}
112
153
  onInteractionResponse={onInteractionResponse}
@@ -114,6 +155,9 @@ export function ChatHistoryView({
114
155
  modelOptions={modelOptions}
115
156
  selectedModel={selectedModel}
116
157
  onModelChange={onModelChange}
158
+ permissionMode={permissionMode}
159
+ permissionModeOptions={permissionModeOptions}
160
+ onPermissionModeChange={onPermissionModeChange}
117
161
  modelUnavailable={modelUnavailable}
118
162
  />
119
163
  </div>
@@ -1,14 +1,10 @@
1
1
  import './CurrentTodoList.scss'
2
- import type { ChatMessage } from '@vibe-forge/core'
3
2
  import React, { useState } from 'react'
4
3
  import { useTranslation } from 'react-i18next'
4
+ import type { ChatMessage } from '@vibe-forge/core'
5
+ import type { ToolInputs } from '@vibe-forge/core'
5
6
 
6
- interface TodoItem {
7
- id: string
8
- content: string
9
- status: 'pending' | 'in_progress' | 'completed'
10
- priority: string
11
- }
7
+ type TodoItem = ToolInputs['adapter:claude-code:TodoWrite']['todos'][number]
12
8
 
13
9
  export function CurrentTodoList({ messages }: { messages: ChatMessage[] }) {
14
10
  const { t } = useTranslation()
@@ -21,12 +17,17 @@ export function CurrentTodoList({ messages }: { messages: ChatMessage[] }) {
21
17
  const msg = messages[i]
22
18
  if (msg.role === 'assistant' && Array.isArray(msg.content)) {
23
19
  const todoUse = msg.content.find(c =>
24
- c != null && c.type === 'tool_use' && (c.name === 'TodoWrite' || c.name === 'todo_write')
20
+ c != null && c.type === 'tool_use' && (
21
+ c.name === 'TodoWrite' ||
22
+ c.name === 'todo_write' ||
23
+ c.name === 'adapter:claude-code:TodoWrite' ||
24
+ c.name === 'adapter:claude-code:todo_write'
25
+ )
25
26
  )
26
27
  if (
27
28
  todoUse != null && todoUse.type === 'tool_use' && todoUse.input != null && typeof todoUse.input === 'object'
28
29
  ) {
29
- const input = todoUse.input as { todos?: TodoItem[] }
30
+ const input = todoUse.input as Partial<ToolInputs['adapter:claude-code:TodoWrite']>
30
31
  if (Array.isArray(input.todos)) {
31
32
  latestTodos = input.todos
32
33
  break
@@ -122,6 +122,20 @@ html.dark {
122
122
  width: 100%;
123
123
  }
124
124
 
125
+ .message-image {
126
+ display: inline-block;
127
+ max-width: 360px;
128
+ border-radius: 10px;
129
+ overflow: hidden;
130
+ border: 1px solid var(--border-color);
131
+ }
132
+
133
+ .message-image img {
134
+ display: block;
135
+ max-width: 100%;
136
+ height: auto;
137
+ }
138
+
125
139
  .markdown-body {
126
140
  --border-color: #e5e7eb;
127
141
  --code-block-bg: #ffffff;
@@ -2,16 +2,18 @@ import './MessageItem.scss'
2
2
  import type { ChatMessage } from '@vibe-forge/core'
3
3
  import React from 'react'
4
4
  import { MessageFooter } from './MessageFooter'
5
- import { MarkdownContent } from './MarkdownContent'
6
- import { ToolRenderer } from './ToolRenderer'
5
+ import { MarkdownContent } from '#~/components/MarkdownContent'
6
+ import { ToolRenderer } from '../tools/core/ToolRenderer'
7
7
 
8
- export function MessageItem({
9
- msg,
10
- isFirstInGroup
11
- }: {
8
+ type MessageItemProps = {
12
9
  msg: ChatMessage
13
10
  isFirstInGroup: boolean
14
- }) {
11
+ }
12
+
13
+ function MessageItemComponent({
14
+ msg,
15
+ isFirstInGroup
16
+ }: MessageItemProps) {
15
17
  const isUser = msg.role === 'user'
16
18
 
17
19
  const renderContent = () => {
@@ -25,7 +27,7 @@ export function MessageItem({
25
27
 
26
28
  if (!Array.isArray(msg.content)) return null
27
29
 
28
- const hasContent = msg.content.some(c => c.type === 'text') || msg.toolCall != null
30
+ const hasContent = msg.content.some(c => c.type === 'text' || c.type === 'image') || msg.toolCall != null
29
31
  if (!hasContent) return null
30
32
 
31
33
  return (
@@ -36,6 +38,13 @@ export function MessageItem({
36
38
  <MarkdownContent key={i} content={item.text} />
37
39
  )
38
40
  }
41
+ if (item.type === 'image') {
42
+ return (
43
+ <a key={i} className='message-image' href={item.url} target='_blank' rel='noreferrer'>
44
+ <img src={item.url} alt={item.name ?? 'image'} />
45
+ </a>
46
+ )
47
+ }
39
48
  return null
40
49
  })}
41
50
  {msg.toolCall != null && (
@@ -76,3 +85,16 @@ export function MessageItem({
76
85
  </div>
77
86
  )
78
87
  }
88
+
89
+ const areMessageItemPropsEqual = (prev: MessageItemProps, next: MessageItemProps) => {
90
+ return prev.isFirstInGroup === next.isFirstInGroup
91
+ && prev.msg.id === next.msg.id
92
+ && prev.msg.role === next.msg.role
93
+ && prev.msg.createdAt === next.msg.createdAt
94
+ && prev.msg.model === next.msg.model
95
+ && prev.msg.content === next.msg.content
96
+ && prev.msg.toolCall === next.msg.toolCall
97
+ && prev.msg.usage === next.msg.usage
98
+ }
99
+
100
+ export const MessageItem = React.memo(MessageItemComponent, areMessageItemPropsEqual)
@@ -114,7 +114,7 @@ export function processMessages(messages: ChatMessage[]): ChatRenderItem[] {
114
114
  }
115
115
 
116
116
  for (const item of content) {
117
- if (item.type === 'text') {
117
+ if (item.type === 'text' || item.type === 'image') {
118
118
  flushTools()
119
119
  textParts.push(item)
120
120
  } else if (item.type === 'tool_use') {
@@ -15,19 +15,47 @@
15
15
  }
16
16
 
17
17
  .new-session-guide__announcements-header {
18
- display: inline-flex;
18
+ display: flex;
19
19
  align-items: center;
20
- gap: 8px;
20
+ justify-content: space-between;
21
+ gap: 12px;
21
22
  font-size: 13px;
22
23
  font-weight: 600;
23
24
  color: var(--text-color);
24
25
  }
25
26
 
27
+ .new-session-guide__announcements-title {
28
+ display: inline-flex;
29
+ align-items: center;
30
+ gap: 8px;
31
+ min-width: 0;
32
+ }
33
+
26
34
  .new-session-guide__announcements-icon {
27
35
  font-size: 18px;
28
36
  color: var(--sub-text-color);
29
37
  }
30
38
 
39
+ .new-session-guide__announcements-close {
40
+ border: none;
41
+ background: transparent;
42
+ padding: 0;
43
+ color: var(--sub-text-color);
44
+ cursor: pointer;
45
+ line-height: 1;
46
+ display: inline-flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ }
50
+
51
+ .new-session-guide__announcements-close:hover {
52
+ color: var(--text-color);
53
+ }
54
+
55
+ .new-session-guide__announcements-close .material-symbols-rounded {
56
+ font-size: 18px;
57
+ }
58
+
31
59
  .new-session-guide__announcements-list {
32
60
  display: flex;
33
61
  flex-direction: column;
@@ -53,10 +81,6 @@
53
81
  display: flex;
54
82
  flex-direction: column;
55
83
  gap: 12px;
56
- border: 1px solid var(--border-color);
57
- border-radius: 12px;
58
- padding: 16px;
59
- background: var(--bg-color);
60
84
  }
61
85
 
62
86
  .new-session-guide__header {
@@ -92,7 +116,8 @@
92
116
  display: flex;
93
117
  flex-direction: column;
94
118
  gap: 12px;
95
- min-height: 160px;
119
+ height: 160px;
120
+ overflow: scroll;
96
121
  }
97
122
 
98
123
  .new-session-guide__list {
@@ -105,16 +130,13 @@
105
130
  display: flex;
106
131
  flex-direction: column;
107
132
  gap: 4px;
108
- padding: 10px 12px;
109
- border-radius: 10px;
110
- background: var(--tag-bg);
111
133
  }
112
134
 
113
135
  .new-session-guide__item-title {
114
136
  display: inline-flex;
115
137
  align-items: center;
116
138
  gap: 6px;
117
- font-size: 13px;
139
+ font-size: 12px;
118
140
  font-weight: 600;
119
141
  color: var(--text-color);
120
142
  }
@@ -126,13 +148,13 @@
126
148
 
127
149
  .new-session-guide__item-desc {
128
150
  font-size: 12px;
129
- color: var(--sub-text-color);
151
+ color: var(--placeholder-color);
130
152
  line-height: 1.4;
131
153
  }
132
154
 
133
155
  .new-session-guide__meta {
134
156
  font-size: 11px;
135
- color: var(--sub-text-color);
157
+ color: var(--placeholder-color);
136
158
  }
137
159
 
138
160
  .new-session-guide__empty {
@@ -1,15 +1,17 @@
1
1
  import './NewSessionGuide.scss'
2
2
 
3
3
  import { App, Button } from 'antd'
4
- import React from 'react'
4
+ import { useAtom } from 'jotai'
5
5
  import { useTranslation } from 'react-i18next'
6
6
  import useSWR from 'swr'
7
7
 
8
8
  import type { EntitySummary, SpecSummary } from '#~/api.js'
9
+ import { showAnnouncementsAtom } from '#~/store/index.js'
9
10
 
10
11
  export function NewSessionGuide() {
11
12
  const { t } = useTranslation()
12
13
  const { message } = App.useApp()
14
+ const [showAnnouncements, setShowAnnouncements] = useAtom(showAnnouncementsAtom)
13
15
 
14
16
  const { data: specsRes } = useSWR<{ specs: SpecSummary[] }>('/api/ai/specs')
15
17
  const { data: entitiesRes } = useSWR<{ entities: EntitySummary[] }>('/api/ai/entities')
@@ -26,6 +28,7 @@ export function NewSessionGuide() {
26
28
  )
27
29
 
28
30
  const specs = specsRes?.specs ?? []
31
+ const alwaysSpecs = specs.filter(spec => spec.always)
29
32
  const entities = entitiesRes?.entities ?? []
30
33
  const isSpecsReady = specsRes != null
31
34
  const isEntitiesReady = entitiesRes != null
@@ -47,11 +50,20 @@ export function NewSessionGuide() {
47
50
 
48
51
  return (
49
52
  <div className='new-session-guide'>
50
- {announcements.length > 0 && (
53
+ {showAnnouncements && announcements.length > 0 && (
51
54
  <div className='new-session-guide__announcements'>
52
55
  <div className='new-session-guide__announcements-header'>
53
- <span className='material-symbols-rounded new-session-guide__announcements-icon'>campaign</span>
54
- <span>{t('chat.newSessionGuide.announcements.title')}</span>
56
+ <div className='new-session-guide__announcements-title'>
57
+ <span className='material-symbols-rounded new-session-guide__announcements-icon'>campaign</span>
58
+ <span>{t('chat.newSessionGuide.announcements.title')}</span>
59
+ </div>
60
+ <button
61
+ type='button'
62
+ className='new-session-guide__announcements-close'
63
+ onClick={() => setShowAnnouncements(false)}
64
+ >
65
+ <span className='material-symbols-rounded'>close</span>
66
+ </button>
55
67
  </div>
56
68
  <div className='new-session-guide__announcements-list'>
57
69
  {announcements.map((item, index) => (
@@ -69,18 +81,17 @@ export function NewSessionGuide() {
69
81
  <span className='material-symbols-rounded new-session-guide__title-icon'>account_tree</span>
70
82
  <span>{t('chat.newSessionGuide.specs.title')}</span>
71
83
  </div>
72
- <div className='new-session-guide__count'>{specs.length}</div>
84
+ <div className='new-session-guide__count'>{alwaysSpecs.length}</div>
73
85
  </div>
74
86
  <div className='new-session-guide__body'>
75
87
  {!isSpecsReady && (
76
88
  <div className='new-session-guide__loading'>{t('chat.newSessionGuide.loading')}</div>
77
89
  )}
78
- {isSpecsReady && specs.length > 0 && (
90
+ {isSpecsReady && alwaysSpecs.length > 0 && (
79
91
  <div className='new-session-guide__list'>
80
- {specs.map((spec) => (
92
+ {alwaysSpecs.map((spec) => (
81
93
  <div key={spec.id} className='new-session-guide__item'>
82
94
  <div className='new-session-guide__item-title'>
83
- <span className='material-symbols-rounded new-session-guide__item-icon'>route</span>
84
95
  <span>{spec.name}</span>
85
96
  </div>
86
97
  <div className='new-session-guide__item-desc'>{spec.description}</div>
@@ -93,7 +104,7 @@ export function NewSessionGuide() {
93
104
  ))}
94
105
  </div>
95
106
  )}
96
- {isSpecsReady && specs.length === 0 && (
107
+ {isSpecsReady && alwaysSpecs.length === 0 && (
97
108
  <div className='new-session-guide__empty'>
98
109
  <div className='new-session-guide__empty-desc'>{t('chat.newSessionGuide.specs.empty')}</div>
99
110
  <Button type='primary' size='small' onClick={handleCreateSpec}>
@@ -121,7 +132,6 @@ export function NewSessionGuide() {
121
132
  {entities.map((entity) => (
122
133
  <div key={entity.id} className='new-session-guide__item'>
123
134
  <div className='new-session-guide__item-title'>
124
- <span className='material-symbols-rounded new-session-guide__item-icon'>person</span>
125
135
  <span>{entity.name}</span>
126
136
  </div>
127
137
  <div className='new-session-guide__item-desc'>{entity.description}</div>
@@ -39,6 +39,54 @@
39
39
  }
40
40
  }
41
41
 
42
+ .file-input-hidden {
43
+ display: none;
44
+ }
45
+
46
+ .pending-images {
47
+ display: flex;
48
+ gap: 8px;
49
+ padding: 6px 0;
50
+ flex-wrap: wrap;
51
+ }
52
+
53
+ .pending-image {
54
+ position: relative;
55
+ width: 56px;
56
+ height: 56px;
57
+ border-radius: 10px;
58
+ overflow: hidden;
59
+ border: 1px solid var(--border-color);
60
+ background-color: var(--bg-color);
61
+
62
+ img {
63
+ width: 100%;
64
+ height: 100%;
65
+ object-fit: cover;
66
+ display: block;
67
+ }
68
+ }
69
+
70
+ .pending-image-remove {
71
+ position: absolute;
72
+ top: 4px;
73
+ right: 4px;
74
+ width: 18px;
75
+ height: 18px;
76
+ border-radius: 9px;
77
+ background-color: rgba(0, 0, 0, .55);
78
+ color: #fff;
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ cursor: pointer;
83
+
84
+ .material-symbols-rounded {
85
+ font-size: 14px;
86
+ line-height: 1;
87
+ }
88
+ }
89
+
42
90
  .chat-input-toolbar {
43
91
  display: flex;
44
92
  justify-content: space-between;
@@ -245,6 +293,38 @@
245
293
  }
246
294
  }
247
295
 
296
+ .permission-mode-select {
297
+ min-width: 120px;
298
+
299
+ .ant-select-selector {
300
+ height: 28px !important;
301
+ border-radius: 8px !important;
302
+ border: 1px solid var(--border-color) !important;
303
+ background-color: var(--tag-hover-bg, #f3f4f6) !important;
304
+ padding: 0 8px !important;
305
+ display: flex;
306
+ align-items: center;
307
+ }
308
+
309
+ .ant-select-selection-item,
310
+ .ant-select-selection-placeholder {
311
+ font-size: 12px;
312
+ color: var(--text-color);
313
+ line-height: 1;
314
+ }
315
+
316
+ &.ant-select-disabled .ant-select-selector {
317
+ background-color: var(--tag-bg, #f9fafb) !important;
318
+ color: var(--sub-text-color, #9ca3af);
319
+ }
320
+ }
321
+
322
+ .permission-mode-select-popup {
323
+ .ant-select-item-option-content {
324
+ font-size: 12px;
325
+ }
326
+ }
327
+
248
328
  .model-select-popup {
249
329
  .ant-select-item-group {
250
330
  padding: 8px 8px 4px;