@huyooo/ai-chat-frontend-vue 0.1.6 → 0.1.7

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 (159) hide show
  1. package/README.md +367 -0
  2. package/dist/adapter.d.ts +7 -7
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/components/ChatPanel.vue.d.ts +120 -9
  5. package/dist/components/ChatPanel.vue.d.ts.map +1 -1
  6. package/dist/components/common/ConfirmDialog.vue.d.ts +30 -0
  7. package/dist/components/common/ConfirmDialog.vue.d.ts.map +1 -0
  8. package/dist/components/common/CopyButton.vue.d.ts +18 -0
  9. package/dist/components/common/CopyButton.vue.d.ts.map +1 -0
  10. package/dist/components/common/IndexingSettings.vue.d.ts +3 -0
  11. package/dist/components/common/IndexingSettings.vue.d.ts.map +1 -0
  12. package/dist/components/common/SettingsPanel.vue.d.ts +16 -0
  13. package/dist/components/common/SettingsPanel.vue.d.ts.map +1 -0
  14. package/dist/components/common/Toast.vue.d.ts +18 -0
  15. package/dist/components/common/Toast.vue.d.ts.map +1 -0
  16. package/dist/components/common/ToggleSwitch.vue.d.ts +10 -0
  17. package/dist/components/common/ToggleSwitch.vue.d.ts.map +1 -0
  18. package/dist/components/{chat/ui → header}/ChatHeader.vue.d.ts +5 -3
  19. package/dist/components/header/ChatHeader.vue.d.ts.map +1 -0
  20. package/dist/components/input/AtFilePicker.vue.d.ts +21 -0
  21. package/dist/components/input/AtFilePicker.vue.d.ts.map +1 -0
  22. package/dist/components/{ChatInput.vue.d.ts → input/ChatInput.vue.d.ts} +16 -14
  23. package/dist/components/input/ChatInput.vue.d.ts.map +1 -0
  24. package/dist/components/input/DropdownSelector.vue.d.ts +42 -0
  25. package/dist/components/input/DropdownSelector.vue.d.ts.map +1 -0
  26. package/dist/components/input/ImagePreviewModal.vue.d.ts +17 -0
  27. package/dist/components/input/ImagePreviewModal.vue.d.ts.map +1 -0
  28. package/dist/components/input/at-views/AtBranchView.vue.d.ts +18 -0
  29. package/dist/components/input/at-views/AtBranchView.vue.d.ts.map +1 -0
  30. package/dist/components/input/at-views/AtBrowserView.vue.d.ts +18 -0
  31. package/dist/components/input/at-views/AtBrowserView.vue.d.ts.map +1 -0
  32. package/dist/components/input/at-views/AtChatsView.vue.d.ts +18 -0
  33. package/dist/components/input/at-views/AtChatsView.vue.d.ts.map +1 -0
  34. package/dist/components/input/at-views/AtDocsView.vue.d.ts +18 -0
  35. package/dist/components/input/at-views/AtDocsView.vue.d.ts.map +1 -0
  36. package/dist/components/input/at-views/AtFilesView.vue.d.ts +23 -0
  37. package/dist/components/input/at-views/AtFilesView.vue.d.ts.map +1 -0
  38. package/dist/components/input/at-views/AtTerminalsView.vue.d.ts +18 -0
  39. package/dist/components/input/at-views/AtTerminalsView.vue.d.ts.map +1 -0
  40. package/dist/components/message/MessageBubble.vue.d.ts +45 -0
  41. package/dist/components/message/MessageBubble.vue.d.ts.map +1 -0
  42. package/dist/components/message/PartsRenderer.vue.d.ts +15 -0
  43. package/dist/components/message/PartsRenderer.vue.d.ts.map +1 -0
  44. package/dist/components/message/WelcomeMessage.vue.d.ts +14 -0
  45. package/dist/components/message/WelcomeMessage.vue.d.ts.map +1 -0
  46. package/dist/components/message/blocks/CodeBlock.vue.d.ts +11 -0
  47. package/dist/components/message/blocks/CodeBlock.vue.d.ts.map +1 -0
  48. package/dist/components/{chat/SearchResultBlock.vue.d.ts → message/blocks/TextBlock.vue.d.ts} +3 -4
  49. package/dist/components/message/blocks/TextBlock.vue.d.ts.map +1 -0
  50. package/dist/components/message/blocks/index.d.ts +6 -0
  51. package/dist/components/message/blocks/index.d.ts.map +1 -0
  52. package/dist/components/message/parts/CollapsibleCard.vue.d.ts +45 -0
  53. package/dist/components/message/parts/CollapsibleCard.vue.d.ts.map +1 -0
  54. package/dist/components/{chat/ToolCallBlock.vue.d.ts → message/parts/ErrorPart.vue.d.ts} +4 -5
  55. package/dist/components/message/parts/ErrorPart.vue.d.ts.map +1 -0
  56. package/dist/components/{chat/ThinkingBlock.vue.d.ts → message/parts/ImagePart.vue.d.ts} +3 -3
  57. package/dist/components/message/parts/ImagePart.vue.d.ts.map +1 -0
  58. package/dist/components/message/parts/SearchPart.vue.d.ts +12 -0
  59. package/dist/components/message/parts/SearchPart.vue.d.ts.map +1 -0
  60. package/dist/components/{chat/messages/ExecutionSteps.vue.d.ts → message/parts/TextPart.vue.d.ts} +2 -9
  61. package/dist/components/message/parts/TextPart.vue.d.ts.map +1 -0
  62. package/dist/components/message/parts/ThinkingPart.vue.d.ts +12 -0
  63. package/dist/components/message/parts/ThinkingPart.vue.d.ts.map +1 -0
  64. package/dist/components/message/parts/ToolCallPart.vue.d.ts +19 -0
  65. package/dist/components/message/parts/ToolCallPart.vue.d.ts.map +1 -0
  66. package/dist/components/message/parts/ToolResultPart.vue.d.ts +14 -0
  67. package/dist/components/message/parts/ToolResultPart.vue.d.ts.map +1 -0
  68. package/dist/components/message/parts/index.d.ts +12 -0
  69. package/dist/components/message/parts/index.d.ts.map +1 -0
  70. package/dist/components/message/tool-results/DefaultToolResult.vue.d.ts +4 -0
  71. package/dist/components/message/tool-results/DefaultToolResult.vue.d.ts.map +1 -0
  72. package/dist/components/message/tool-results/SearchResults.vue.d.ts +4 -0
  73. package/dist/components/message/tool-results/SearchResults.vue.d.ts.map +1 -0
  74. package/dist/components/message/tool-results/WeatherCard.vue.d.ts +4 -0
  75. package/dist/components/message/tool-results/WeatherCard.vue.d.ts.map +1 -0
  76. package/dist/components/message/tool-results/index.d.ts +7 -0
  77. package/dist/components/message/tool-results/index.d.ts.map +1 -0
  78. package/dist/components/message/welcome-types.d.ts +28 -0
  79. package/dist/components/message/welcome-types.d.ts.map +1 -0
  80. package/dist/composables/useChat.d.ts +99 -44
  81. package/dist/composables/useChat.d.ts.map +1 -1
  82. package/dist/composables/useImageUpload.d.ts +55 -0
  83. package/dist/composables/useImageUpload.d.ts.map +1 -0
  84. package/dist/index.d.ts +25 -26
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +55871 -1252
  87. package/dist/style.css +1 -1
  88. package/dist/types/index.d.ts +113 -53
  89. package/dist/types/index.d.ts.map +1 -1
  90. package/dist/utils/fileIcon.d.ts +13 -0
  91. package/dist/utils/fileIcon.d.ts.map +1 -0
  92. package/package.json +12 -6
  93. package/src/adapter.ts +12 -70
  94. package/src/components/ChatPanel.vue +329 -110
  95. package/src/components/common/ConfirmDialog.vue +208 -0
  96. package/src/components/common/CopyButton.vue +71 -0
  97. package/src/components/common/IndexingSettings.vue +580 -0
  98. package/src/components/common/SettingsPanel.vue +293 -0
  99. package/src/components/common/Toast.vue +90 -0
  100. package/src/components/common/ToggleSwitch.vue +75 -0
  101. package/src/components/{chat/ui → header}/ChatHeader.vue +170 -93
  102. package/src/components/input/AtFilePicker.vue +657 -0
  103. package/src/components/input/ChatInput.vue +653 -0
  104. package/src/components/input/DropdownSelector.vue +322 -0
  105. package/src/components/input/ImagePreviewModal.vue +238 -0
  106. package/src/components/input/at-views/AtBranchView.vue +63 -0
  107. package/src/components/input/at-views/AtBrowserView.vue +63 -0
  108. package/src/components/input/at-views/AtChatsView.vue +63 -0
  109. package/src/components/input/at-views/AtDocsView.vue +63 -0
  110. package/src/components/input/at-views/AtFilesView.vue +255 -0
  111. package/src/components/input/at-views/AtTerminalsView.vue +63 -0
  112. package/src/components/message/ContentRenderer.vue +61 -0
  113. package/src/components/message/MessageBubble.vue +411 -0
  114. package/src/components/message/PartsRenderer.vue +101 -0
  115. package/src/components/message/ToolResultRenderer.vue +27 -0
  116. package/src/components/message/WelcomeMessage.vue +308 -0
  117. package/src/components/message/blocks/CodeBlock.vue +113 -0
  118. package/src/components/message/blocks/TextBlock.vue +21 -0
  119. package/src/components/message/blocks/index.ts +6 -0
  120. package/src/components/message/parts/CollapsibleCard.vue +135 -0
  121. package/src/components/message/parts/ErrorPart.vue +51 -0
  122. package/src/components/message/parts/ImagePart.vue +98 -0
  123. package/src/components/message/parts/SearchPart.vue +101 -0
  124. package/src/components/message/parts/TextPart.vue +28 -0
  125. package/src/components/message/parts/ThinkingPart.vue +54 -0
  126. package/src/components/message/parts/ToolCallPart.vue +460 -0
  127. package/src/components/message/parts/ToolResultPart.vue +78 -0
  128. package/src/components/message/parts/index.ts +13 -0
  129. package/src/components/message/tool-results/DefaultToolResult.vue +43 -0
  130. package/src/components/message/tool-results/SearchResults.vue +133 -0
  131. package/src/components/message/tool-results/WeatherCard.vue +139 -0
  132. package/src/components/message/tool-results/index.ts +7 -0
  133. package/src/components/message/welcome-types.ts +47 -0
  134. package/src/composables/useChat.ts +807 -155
  135. package/src/composables/useImageUpload.ts +228 -0
  136. package/src/index.ts +93 -46
  137. package/src/styles.css +47 -0
  138. package/src/types/index.ts +146 -98
  139. package/src/utils/fileIcon.ts +49 -0
  140. package/dist/components/ChatInput.vue.d.ts.map +0 -1
  141. package/dist/components/chat/SearchResultBlock.vue.d.ts.map +0 -1
  142. package/dist/components/chat/ThinkingBlock.vue.d.ts.map +0 -1
  143. package/dist/components/chat/ToolCallBlock.vue.d.ts.map +0 -1
  144. package/dist/components/chat/messages/ExecutionSteps.vue.d.ts.map +0 -1
  145. package/dist/components/chat/messages/MessageBubble.vue.d.ts +0 -28
  146. package/dist/components/chat/messages/MessageBubble.vue.d.ts.map +0 -1
  147. package/dist/components/chat/ui/ChatHeader.vue.d.ts.map +0 -1
  148. package/dist/components/chat/ui/WelcomeMessage.vue.d.ts +0 -7
  149. package/dist/components/chat/ui/WelcomeMessage.vue.d.ts.map +0 -1
  150. package/dist/preload/preload.d.ts +0 -6
  151. package/dist/preload/preload.d.ts.map +0 -1
  152. package/src/components/ChatInput.vue +0 -649
  153. package/src/components/chat/SearchResultBlock.vue +0 -155
  154. package/src/components/chat/ThinkingBlock.vue +0 -109
  155. package/src/components/chat/ToolCallBlock.vue +0 -213
  156. package/src/components/chat/messages/ExecutionSteps.vue +0 -281
  157. package/src/components/chat/messages/MessageBubble.vue +0 -272
  158. package/src/components/chat/ui/WelcomeMessage.vue +0 -135
  159. package/src/preload/preload.ts +0 -79
@@ -1,5 +1,27 @@
1
1
  <template>
2
2
  <div class="chat-panel">
3
+ <!-- 确认弹窗 -->
4
+ <ConfirmDialog
5
+ v-model:visible="confirmDialog.visible"
6
+ :title="confirmDialog.title"
7
+ :message="confirmDialog.message"
8
+ :type="confirmDialog.type"
9
+ :confirm-text="confirmDialog.confirmText"
10
+ @confirm="confirmDialog.onConfirm"
11
+ />
12
+
13
+ <!-- Toast 消息 -->
14
+ <Toast v-model:visible="toast.visible" :message="toast.message" :type="toast.type" />
15
+
16
+ <!-- 工具批准现在内嵌在 ToolCallPart 中,不再需要全局对话框 -->
17
+
18
+ <!-- 设置面板 -->
19
+ <SettingsPanel
20
+ v-model:visible="settingsPanelVisible"
21
+ v-model:config="autoRunConfig"
22
+ @save="handleSaveSettings"
23
+ />
24
+
3
25
  <!-- 顶部标题栏 -->
4
26
  <ChatHeader
5
27
  v-if="!hideHeader"
@@ -9,6 +31,7 @@
9
31
  @new-session="createNewSession"
10
32
  @switch-session="switchSession"
11
33
  @delete-session="deleteSession"
34
+ @hide-session="hideSession"
12
35
  @close="handleClose"
13
36
  @clear-all="handleClearAll"
14
37
  @close-others="handleCloseOthers"
@@ -18,36 +41,49 @@
18
41
  @settings="handleSettings"
19
42
  />
20
43
 
21
- <!-- 消息列表 -->
22
- <div ref="messagesRef" class="messages-container">
23
- <WelcomeMessage
24
- v-if="messages.length === 0"
25
- @quick-action="handleQuickAction"
26
- />
27
- <template v-else>
28
- <MessageBubble
29
- v-for="(msg, index) in messages"
30
- :key="msg.id"
31
- :role="msg.role"
32
- :content="msg.content"
33
- :images="msg.images"
34
- :thinking="msg.thinking"
35
- :thinking-complete="msg.thinkingComplete"
36
- :search-results="msg.searchResults"
37
- :searching="msg.searching"
38
- :tool-calls="msg.toolCalls"
39
- :copied="msg.copied"
40
- :loading="msg.loading"
41
- @copy="copyMessage(msg.id)"
42
- @regenerate="regenerateMessage(index)"
43
- @send="(text) => handleResend(index, text)"
44
- />
45
- </template>
44
+ <!-- 消息列表容器 -->
45
+ <div class="messages-wrapper">
46
+ <div ref="messagesRef" class="messages-container chat-scrollbar" @scroll="handleScroll">
47
+ <WelcomeMessage v-if="messages.length === 0" :config="props.welcomeConfig" @quick-action="handleQuickAction" />
48
+ <template v-else>
49
+ <MessageBubble
50
+ v-for="(msg, index) in messages"
51
+ :key="msg.id"
52
+ :role="msg.role"
53
+ :parts="msg.parts"
54
+ :model="msg.model"
55
+ :mode="msg.mode"
56
+ :images="msg.images"
57
+ :copied="msg.copied"
58
+ :loading="msg.loading"
59
+ :timestamp="msg.timestamp"
60
+ :steps-expanded-type="stepsExpandedType"
61
+ :adapter="props.adapter"
62
+ :auto-run-config="autoRunConfig"
63
+ :on-save-config="saveAutoRunConfig"
64
+ @copy="copyMessage(msg.id)"
65
+ @regenerate="regenerateMessage(index)"
66
+ @send="(text) => handleResend(index, text)"
67
+ />
68
+ </template>
69
+ </div>
70
+
71
+ <!-- 滚动到底部按钮 -->
72
+ <Transition name="fade">
73
+ <button
74
+ v-if="!shouldAutoScroll && messages.length > 0"
75
+ class="scroll-to-bottom-btn"
76
+ @click="scrollToBottom(true)"
77
+ title="滚动到底部"
78
+ >
79
+ <Icon icon="lucide:arrow-down" width="16" />
80
+ </button>
81
+ </Transition>
46
82
  </div>
47
83
 
48
84
  <!-- 输入区域 -->
49
85
  <ChatInput
50
- :selected-images="selectedImages"
86
+ ref="chatInputRef"
51
87
  :is-loading="isLoading"
52
88
  :mode="mode"
53
89
  :model="model"
@@ -56,8 +92,6 @@
56
92
  :thinking-enabled="thinking"
57
93
  @send="handleSend"
58
94
  @cancel="cancelRequest"
59
- @remove-image="handleRemoveImage"
60
- @upload-image="handleUploadImage"
61
95
  @at-context="handleAtContext"
62
96
  @update:mode="setMode"
63
97
  @update:model="setModel"
@@ -68,48 +102,67 @@
68
102
  </template>
69
103
 
70
104
  <script setup lang="ts">
71
- import { ref, watch, nextTick, onMounted, provide, toRef } from 'vue';
105
+ import { ref, computed, watch, nextTick, onMounted, provide, toRef, type Component } from 'vue';
72
106
  import { useChat } from '../composables/useChat';
73
107
  import type { ChatAdapter } from '../adapter';
74
- import type { ModelConfig, ChatMode } from '../types';
75
- import { DEFAULT_MODELS } from '../types';
76
- import ChatHeader from './chat/ui/ChatHeader.vue';
77
- import WelcomeMessage from './chat/ui/WelcomeMessage.vue';
78
- import MessageBubble from './chat/messages/MessageBubble.vue';
79
- import ChatInput from './ChatInput.vue';
108
+ import type { ModelOption, ChatMode } from '../types';
109
+ import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer';
110
+ import { Icon } from '@iconify/vue';
111
+ import ChatHeader from './header/ChatHeader.vue';
112
+ import WelcomeMessage from './message/WelcomeMessage.vue';
113
+ import MessageBubble from './message/MessageBubble.vue';
114
+ import ChatInput from './input/ChatInput.vue';
115
+ import ConfirmDialog from './common/ConfirmDialog.vue';
116
+ import Toast from './common/Toast.vue';
117
+ // ToolApprovalDialog 已移除,工具批准现在内嵌在 ToolCallPart 中
118
+ import SettingsPanel from './common/SettingsPanel.vue';
119
+ import type { ToolCompleteEvent } from '../composables/useChat';
120
+ import type { WelcomeConfig } from './message/welcome-types';
80
121
 
81
122
  interface Props {
82
123
  /** Adapter 实例 */
83
- adapter?: ChatAdapter;
84
- /** 工作目录 */
85
- workingDir?: string;
124
+ adapter: ChatAdapter;
86
125
  /** 默认模型 */
87
126
  defaultModel?: string;
88
127
  /** 默认模式 */
89
128
  defaultMode?: ChatMode;
90
129
  /** 可用模型列表 */
91
- models?: ModelConfig[];
130
+ models?: ModelOption[];
92
131
  /** 隐藏标题栏 */
93
132
  hideHeader?: boolean;
94
133
  /** 关闭回调(有此属性时显示关闭按钮) */
95
134
  onClose?: () => void;
96
135
  /** 自定义类名 */
97
136
  className?: string;
137
+ /** 欢迎页配置 */
138
+ welcomeConfig?: Partial<WelcomeConfig>;
139
+ /** 自定义工具结果渲染器 - 根据工具名称选择渲染组件 */
140
+ toolRenderers?: Record<string, Component>;
141
+ /**
142
+ * 执行步骤折叠模式
143
+ * - 'open': 始终展开
144
+ * - 'close': 始终折叠
145
+ * - 'auto': 执行时展开,完成后折叠
146
+ */
147
+ stepsExpandedType?: 'open' | 'close' | 'auto';
98
148
  }
99
149
 
100
150
  const props = withDefaults(defineProps<Props>(), {
101
- adapter: undefined,
102
- workingDir: undefined,
103
151
  defaultModel: 'anthropic/claude-opus-4.5',
104
152
  defaultMode: 'agent',
105
- models: () => DEFAULT_MODELS,
153
+ models: () => [],
106
154
  hideHeader: false,
107
155
  onClose: undefined,
108
156
  className: '',
157
+ welcomeConfig: undefined,
158
+ toolRenderers: () => ({}),
159
+ stepsExpandedType: 'auto',
109
160
  });
110
161
 
111
162
  const emit = defineEmits<{
112
163
  close: [];
164
+ /** 工具执行完成事件 */
165
+ 'tool-complete': [event: ToolCompleteEvent];
113
166
  }>();
114
167
 
115
168
  // 使用 useChat composable
@@ -117,6 +170,7 @@ const chat = useChat({
117
170
  adapter: props.adapter,
118
171
  defaultModel: props.defaultModel,
119
172
  defaultMode: props.defaultMode,
173
+ onToolComplete: (event) => emit('tool-complete', event),
120
174
  });
121
175
 
122
176
  // 解构状态
@@ -133,62 +187,160 @@ const {
133
187
  switchSession,
134
188
  createNewSession,
135
189
  deleteSession,
190
+ hideSession,
191
+ clearAllSessions,
192
+ hideOtherSessions,
193
+ exportCurrentSession,
136
194
  sendMessage,
137
195
  cancelRequest,
138
196
  copyMessage,
139
197
  regenerateMessage,
198
+ resendFromIndex,
140
199
  setMode,
141
200
  setModel,
142
201
  setWebSearch,
143
202
  setThinking,
144
203
  setWorkingDirectory,
204
+ autoRunConfig,
205
+ saveAutoRunConfig,
145
206
  } = chat;
146
207
 
147
- // 选中的图片
148
- const selectedImages = ref<string[]>([]);
149
-
150
208
  // 消息容器引用
151
209
  const messagesRef = ref<HTMLDivElement | null>(null);
152
210
 
153
- // 可用模型
154
- const models = toRef(props, 'models');
211
+ // 是否应该自动滚动(用户在底部附近时才自动滚动)
212
+ const shouldAutoScroll = ref(true);
213
+ // 距离底部多少像素内算"在底部"
214
+ const SCROLL_THRESHOLD = 100;
215
+
216
+ // ChatInput 引用
217
+ const chatInputRef = ref<InstanceType<typeof ChatInput> | null>(null);
218
+
219
+ // 设置面板状态
220
+ const settingsPanelVisible = ref(false);
221
+
222
+ // 确认弹窗状态
223
+ const confirmDialog = ref({
224
+ visible: false,
225
+ title: '确认',
226
+ message: '',
227
+ type: 'warning' as 'info' | 'warning' | 'danger',
228
+ confirmText: '确定',
229
+ onConfirm: () => {},
230
+ });
231
+
232
+ // Toast 消息状态
233
+ const toast = ref({
234
+ visible: false,
235
+ message: '',
236
+ type: 'info' as 'info' | 'success' | 'warning' | 'error',
237
+ });
238
+
239
+ /** 显示 Toast 消息 */
240
+ function showToast(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info') {
241
+ toast.value = { visible: true, message, type };
242
+ }
243
+
244
+ /** 显示确认弹窗 */
245
+ function showConfirm(options: {
246
+ title?: string;
247
+ message: string;
248
+ type?: 'info' | 'warning' | 'danger';
249
+ confirmText?: string;
250
+ onConfirm: () => void;
251
+ }) {
252
+ confirmDialog.value = {
253
+ visible: true,
254
+ title: options.title || '确认',
255
+ message: options.message,
256
+ type: options.type || 'warning',
257
+ confirmText: options.confirmText || '确定',
258
+ onConfirm: options.onConfirm,
259
+ };
260
+ }
261
+
262
+ // 可用模型(如果没有传入,从后端获取)
263
+ const models = ref<ModelOption[]>(props.models || []);
264
+
265
+ // 从后端获取模型列表
266
+ onMounted(async () => {
267
+ try {
268
+ models.value = await props.adapter.getModels();
269
+ } catch (err) {
270
+ console.warn('获取模型列表失败:', err);
271
+ }
272
+ });
155
273
 
156
- // Provide adapter 给子组件使用
157
- provide('chatAdapter', props.adapter);
274
+ // 注意:cwd 已解耦,不再通过 prop 传递
275
+ // FileBrowser 会直接通过 adapter.setCwd() 同步到 Agent
276
+ // getCwdTool 会自动从 Agent 的 context.cwd 读取最新值
277
+ // 如果需要 cwd 给子组件使用,可以通过 getCwdTool 获取
278
+
279
+ // Provide 全局 input 状态给子组件(让 message 中的 input 也能修改)
280
+ provide('chatInputState', {
281
+ mode,
282
+ model,
283
+ webSearch,
284
+ thinking,
285
+ models,
286
+ isLoading,
287
+ setMode,
288
+ setModel,
289
+ setWebSearch,
290
+ setThinking,
291
+ });
292
+
293
+ // Provide 自定义工具渲染器(用于 ToolResultPart 组件)
294
+ provide('toolRenderers', props.toolRenderers);
158
295
 
159
296
  // 初始化
160
297
  onMounted(() => {
161
298
  loadSessions();
162
299
  });
163
300
 
164
- // 工作目录变化时更新
165
- watch(
166
- () => props.workingDir,
167
- (dir) => {
168
- if (dir) {
169
- setWorkingDirectory(dir);
170
- }
171
- },
172
- { immediate: true }
173
- );
301
+ // 注意:cwd 已解耦,不再需要监听 prop 变化
302
+
303
+ // 检查是否在底部附近
304
+ function isNearBottom(): boolean {
305
+ if (!messagesRef.value) return true;
306
+ const { scrollTop, scrollHeight, clientHeight } = messagesRef.value;
307
+ return scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD;
308
+ }
309
+
310
+ // 处理滚动事件
311
+ function handleScroll() {
312
+ shouldAutoScroll.value = isNearBottom();
313
+ }
174
314
 
175
315
  // 滚动到底部
176
- async function scrollToBottom() {
316
+ async function scrollToBottom(force = false) {
177
317
  await nextTick();
178
- if (messagesRef.value) {
318
+ if (messagesRef.value && (force || shouldAutoScroll.value)) {
179
319
  messagesRef.value.scrollTop = messagesRef.value.scrollHeight;
320
+ shouldAutoScroll.value = true;
180
321
  }
181
322
  }
182
323
 
183
- // 消息变化时滚动
184
- watch(messages, () => {
185
- scrollToBottom();
186
- }, { flush: 'post' });
324
+ // 消息变化时滚动(只在用户在底部时)
325
+ watch(
326
+ messages,
327
+ () => {
328
+ scrollToBottom();
329
+ },
330
+ { flush: 'post', deep: true }
331
+ );
332
+
333
+ // 发送新消息时强制滚动到底部
334
+ watch(isLoading, (loading, prevLoading) => {
335
+ // 开始加载时(发送消息时)强制滚动到底部
336
+ if (loading && !prevLoading) {
337
+ scrollToBottom(true);
338
+ }
339
+ });
187
340
 
188
341
  // 发送消息
189
342
  function handleSend(text: string) {
190
- sendMessage(text, selectedImages.value.length > 0 ? selectedImages.value : undefined);
191
- selectedImages.value = [];
343
+ sendMessage(text);
192
344
  }
193
345
 
194
346
  // 快捷操作
@@ -198,22 +350,7 @@ function handleQuickAction(text: string) {
198
350
 
199
351
  // 重新发送(编辑后)
200
352
  function handleResend(index: number, text: string) {
201
- // 删除当前消息及后续消息,然后重新发送
202
- messages.value = messages.value.slice(0, index);
203
- sendMessage(text);
204
- }
205
-
206
- // 移除图片
207
- function handleRemoveImage(index: number) {
208
- if (index >= 0 && index < selectedImages.value.length) {
209
- selectedImages.value.splice(index, 1);
210
- }
211
- }
212
-
213
- // 上传图片
214
- function handleUploadImage() {
215
- // TODO: 实现图片上传
216
- console.log('上传图片');
353
+ resendFromIndex(index, text);
217
354
  }
218
355
 
219
356
  // @ 上下文
@@ -232,27 +369,50 @@ function handleClose() {
232
369
 
233
370
  // 清空所有对话
234
371
  function handleClearAll() {
235
- // TODO: 实现清空所有对话
236
- console.log('清空所有对话');
372
+ showConfirm({
373
+ title: '清空所有对话',
374
+ message: '确定要清空所有对话吗?此操作不可恢复。',
375
+ type: 'danger',
376
+ confirmText: '清空',
377
+ onConfirm: () => clearAllSessions(),
378
+ });
237
379
  }
238
380
 
239
381
  // 关闭其他对话
240
- function handleCloseOthers() {
241
- // TODO: 实现关闭其他对话
242
- console.log('关闭其他对话');
382
+ async function handleCloseOthers() {
383
+ await hideOtherSessions()
243
384
  }
244
385
 
245
386
  // 导出对话
246
387
  function handleExport() {
247
- // TODO: 实现导出对话
248
- console.log('导出对话');
388
+ const data = exportCurrentSession()
389
+ if (!data) {
390
+ showToast('当前会话没有内容可导出', 'warning')
391
+ return
392
+ }
393
+
394
+ // 创建下载链接
395
+ const blob = new Blob([data], { type: 'application/json' })
396
+ const url = URL.createObjectURL(blob)
397
+ const a = document.createElement('a')
398
+ const session = sessions.value.find((s) => s.id === currentSessionId.value)
399
+ const filename = `chat-${session?.title || 'export'}-${new Date().toISOString().slice(0, 10)}.json`
400
+ a.href = url
401
+ a.download = filename
402
+ document.body.appendChild(a)
403
+ a.click()
404
+ document.body.removeChild(a)
405
+ URL.revokeObjectURL(url)
249
406
  }
250
407
 
251
- // 复制请求 ID
252
- function handleCopyId() {
253
- if (currentSessionId.value) {
254
- navigator.clipboard.writeText(currentSessionId.value);
255
- console.log('已复制请求 ID:', currentSessionId.value);
408
+ // 复制会话 ID
409
+ async function handleCopyId() {
410
+ if (!currentSessionId.value) return
411
+ try {
412
+ await navigator.clipboard.writeText(currentSessionId.value)
413
+ showToast('已复制会话 ID', 'success')
414
+ } catch (error) {
415
+ console.error('复制失败:', error)
256
416
  }
257
417
  }
258
418
 
@@ -264,23 +424,62 @@ function handleFeedback() {
264
424
 
265
425
  // 设置
266
426
  function handleSettings() {
267
- // TODO: 实现设置
268
- console.log('Agent 设置');
427
+ settingsPanelVisible.value = true;
269
428
  }
429
+
430
+ // 保存设置(静默保存,不显示提示)
431
+ async function handleSaveSettings(config: AutoRunConfig) {
432
+ try {
433
+ await saveAutoRunConfig(config);
434
+ } catch (error) {
435
+ console.error('保存设置失败:', error);
436
+ showToast('保存设置失败', 'error');
437
+ }
438
+ }
439
+
440
+ // 模式变更现在在 ToolCallPart 中处理,不再需要此函数
441
+
442
+ // 暴露给外部的方法
443
+ defineExpose({
444
+ /** 设置输入框内容 */
445
+ setInputText: (text: string) => {
446
+ chatInputRef.value?.setText(text);
447
+ },
448
+ /** 在光标位置插入文本(用于 @ 上下文) */
449
+ insertInputText: (text: string) => {
450
+ chatInputRef.value?.insertText?.(text);
451
+ },
452
+ /** 聚焦输入框 */
453
+ focusInput: () => {
454
+ chatInputRef.value?.focus();
455
+ },
456
+ /** 发送消息 */
457
+ sendMessage: (text: string) => {
458
+ sendMessage(text);
459
+ },
460
+ /** 当前工作目录 */
461
+ setCwd: setWorkingDirectory,
462
+ });
270
463
  </script>
271
464
 
272
465
  <style scoped>
273
466
  .chat-panel {
274
467
  display: flex;
275
468
  flex-direction: column;
276
- width: 420px;
469
+ width: 100%;
277
470
  height: 100%;
278
471
  background: var(--chat-bg, #1e1e1e);
279
- border-left: 1px solid var(--chat-border, #333);
280
- flex-shrink: 0;
281
472
  overflow: hidden;
282
473
  }
283
474
 
475
+ .messages-wrapper {
476
+ flex: 1;
477
+ min-height: 0;
478
+ position: relative;
479
+ display: flex;
480
+ flex-direction: column;
481
+ }
482
+
284
483
  .messages-container {
285
484
  flex: 1;
286
485
  min-height: 0;
@@ -289,21 +488,41 @@ function handleSettings() {
289
488
  scroll-behavior: smooth;
290
489
  }
291
490
 
292
- /* 滚动条样式 */
293
- .messages-container::-webkit-scrollbar {
294
- width: 6px;
491
+ /* 滚动到底部按钮 */
492
+ .scroll-to-bottom-btn {
493
+ position: absolute;
494
+ bottom: 16px;
495
+ right: 16px;
496
+ display: flex;
497
+ align-items: center;
498
+ justify-content: center;
499
+ width: 32px;
500
+ height: 32px;
501
+ border-radius: 50%;
502
+ background: var(--chat-bubble-bg, #2d2d2d);
503
+ border: 1px solid var(--chat-border, #3d3d3d);
504
+ color: var(--chat-text-secondary, #888);
505
+ cursor: pointer;
506
+ transition: all 0.2s ease;
507
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
508
+ z-index: 10;
509
+ pointer-events: auto;
295
510
  }
296
511
 
297
- .messages-container::-webkit-scrollbar-track {
298
- background: transparent;
512
+ .scroll-to-bottom-btn:hover {
513
+ background: var(--chat-bubble-bg-hover, #3a3a3a);
514
+ color: var(--chat-text, #e0e0e0);
515
+ transform: scale(1.1);
299
516
  }
300
517
 
301
- .messages-container::-webkit-scrollbar-thumb {
302
- background: var(--chat-scrollbar, rgba(255, 255, 255, 0.2));
303
- border-radius: 3px;
518
+ /* 淡入淡出动画 */
519
+ .fade-enter-active,
520
+ .fade-leave-active {
521
+ transition: opacity 0.2s ease;
304
522
  }
305
523
 
306
- .messages-container::-webkit-scrollbar-thumb:hover {
307
- background: var(--chat-scrollbar-hover, rgba(255, 255, 255, 0.3));
524
+ .fade-enter-from,
525
+ .fade-leave-to {
526
+ opacity: 0;
308
527
  }
309
528
  </style>