feedback-mcp 1.0.64__py3-none-any.whl
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.
Potentially problematic release.
This version of feedback-mcp might be problematic. Click here for more details.
- add_command_dialog.py +712 -0
- command.py +636 -0
- components/__init__.py +15 -0
- components/agent_popup.py +187 -0
- components/chat_history.py +281 -0
- components/command_popup.py +399 -0
- components/feedback_text_edit.py +1125 -0
- components/file_popup.py +417 -0
- components/history_popup.py +582 -0
- components/markdown_display.py +262 -0
- context_formatter.py +301 -0
- debug_logger.py +107 -0
- feedback_config.py +144 -0
- feedback_mcp-1.0.64.dist-info/METADATA +327 -0
- feedback_mcp-1.0.64.dist-info/RECORD +41 -0
- feedback_mcp-1.0.64.dist-info/WHEEL +5 -0
- feedback_mcp-1.0.64.dist-info/entry_points.txt +2 -0
- feedback_mcp-1.0.64.dist-info/top_level.txt +20 -0
- feedback_ui.py +1680 -0
- get_session_id.py +53 -0
- git_operations.py +579 -0
- ide_utils.py +313 -0
- path_config.py +89 -0
- post_task_hook.py +78 -0
- record.py +188 -0
- server.py +746 -0
- session_manager.py +368 -0
- stop_hook.py +87 -0
- tabs/__init__.py +87 -0
- tabs/base_tab.py +34 -0
- tabs/chat_history_style.qss +66 -0
- tabs/chat_history_tab.py +1000 -0
- tabs/chat_tab.py +1931 -0
- tabs/workspace_tab.py +502 -0
- ui/__init__.py +20 -0
- ui/__main__.py +16 -0
- ui/compact_feedback_ui.py +376 -0
- ui/session_list_ui.py +793 -0
- ui/styles/session_list.qss +158 -0
- window_position_manager.py +197 -0
- workspace_manager.py +253 -0
tabs/chat_history_tab.py
ADDED
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
"""
|
|
2
|
+
对话记录标签页 - 展示所有对话内容
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import weakref
|
|
8
|
+
from typing import List, Dict, Optional
|
|
9
|
+
from PySide6.QtWidgets import (
|
|
10
|
+
QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QFrame, QLabel, QSizePolicy, QPushButton
|
|
11
|
+
)
|
|
12
|
+
from PySide6.QtCore import Qt, QFile, QTextStream, QTimer
|
|
13
|
+
import pyperclip
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from .base_tab import BaseTab
|
|
17
|
+
except ImportError:
|
|
18
|
+
from base_tab import BaseTab
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from ..components.markdown_display import MarkdownDisplayWidget
|
|
22
|
+
except ImportError:
|
|
23
|
+
try:
|
|
24
|
+
from components.markdown_display import MarkdownDisplayWidget
|
|
25
|
+
except ImportError:
|
|
26
|
+
from PySide6.QtWidgets import QTextEdit
|
|
27
|
+
MarkdownDisplayWidget = QTextEdit
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ChatHistoryTab(BaseTab):
|
|
31
|
+
"""对话记录标签页 - 展示所有对话内容"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, project_path: Optional[str] = None, session_id: Optional[str] = None, workspace_id: Optional[str] = None, parent=None):
|
|
34
|
+
super().__init__(parent)
|
|
35
|
+
self.project_path = project_path
|
|
36
|
+
self.session_id = session_id
|
|
37
|
+
self.workspace_id = workspace_id
|
|
38
|
+
|
|
39
|
+
# 计算 workspace_path
|
|
40
|
+
if workspace_id and project_path:
|
|
41
|
+
self.workspace_path = os.path.join(project_path, '.workspace', workspace_id)
|
|
42
|
+
else:
|
|
43
|
+
self.workspace_path = None
|
|
44
|
+
|
|
45
|
+
# UI组件
|
|
46
|
+
self.scroll_area = None
|
|
47
|
+
self.messages_container = None
|
|
48
|
+
self.messages_layout = None
|
|
49
|
+
self.load_more_button = None
|
|
50
|
+
|
|
51
|
+
# 历史记录管理
|
|
52
|
+
self.all_history = []
|
|
53
|
+
self.displayed_count = 10
|
|
54
|
+
self.current_start_idx = -1 # 当前显示的起始索引,用于增量加载
|
|
55
|
+
self._loaded = False # 延迟加载标志
|
|
56
|
+
|
|
57
|
+
self.create_ui()
|
|
58
|
+
|
|
59
|
+
def create_ui(self):
|
|
60
|
+
"""创建对话记录Tab的UI"""
|
|
61
|
+
layout = QVBoxLayout(self)
|
|
62
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
63
|
+
layout.setSpacing(0)
|
|
64
|
+
|
|
65
|
+
# 创建滚动区域
|
|
66
|
+
self.scroll_area = QScrollArea()
|
|
67
|
+
self.scroll_area.setWidgetResizable(True)
|
|
68
|
+
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
69
|
+
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
70
|
+
|
|
71
|
+
# 容器
|
|
72
|
+
self.messages_container = QWidget()
|
|
73
|
+
self.messages_container.setObjectName("messagesContainer")
|
|
74
|
+
self.messages_layout = QVBoxLayout(self.messages_container)
|
|
75
|
+
self.messages_layout.setContentsMargins(15, 15, 15, 15)
|
|
76
|
+
self.messages_layout.setSpacing(5)
|
|
77
|
+
self.messages_layout.setAlignment(Qt.AlignTop)
|
|
78
|
+
|
|
79
|
+
self.scroll_area.setWidget(self.messages_container)
|
|
80
|
+
layout.addWidget(self.scroll_area)
|
|
81
|
+
|
|
82
|
+
# 加载样式表
|
|
83
|
+
self._load_stylesheet()
|
|
84
|
+
|
|
85
|
+
def _load_stylesheet(self):
|
|
86
|
+
"""加载QSS样式表"""
|
|
87
|
+
try:
|
|
88
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
89
|
+
qss_path = os.path.join(current_dir, "chat_history_style.qss")
|
|
90
|
+
qss_file = QFile(qss_path)
|
|
91
|
+
if qss_file.open(QFile.ReadOnly | QFile.Text):
|
|
92
|
+
stream = QTextStream(qss_file)
|
|
93
|
+
self.setStyleSheet(stream.readAll())
|
|
94
|
+
qss_file.close()
|
|
95
|
+
else:
|
|
96
|
+
print(f"无法加载样式表: {qss_path}", file=sys.stderr)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print(f"加载样式表出错: {e}", file=sys.stderr)
|
|
99
|
+
|
|
100
|
+
def load_history(self):
|
|
101
|
+
"""加载并显示对话历史记录"""
|
|
102
|
+
# 清空现有消息
|
|
103
|
+
self._clear_messages()
|
|
104
|
+
|
|
105
|
+
# 读取历史记录
|
|
106
|
+
self.all_history = self._load_history_from_file()
|
|
107
|
+
|
|
108
|
+
if not self.all_history:
|
|
109
|
+
# 如果没有历史记录,显示提示
|
|
110
|
+
self._show_empty_message()
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# 显示最后5条记录
|
|
114
|
+
self._display_records()
|
|
115
|
+
|
|
116
|
+
def _display_records(self):
|
|
117
|
+
"""显示记录(从最新的开始显示指定数量)"""
|
|
118
|
+
# 清空所有现有消息
|
|
119
|
+
self._clear_messages()
|
|
120
|
+
self.load_more_button = None
|
|
121
|
+
|
|
122
|
+
total = len(self.all_history)
|
|
123
|
+
# 计算要显示的记录范围
|
|
124
|
+
start_idx = max(0, total - self.displayed_count)
|
|
125
|
+
self.current_start_idx = start_idx
|
|
126
|
+
records_to_show = self.all_history[start_idx:]
|
|
127
|
+
|
|
128
|
+
# 如果还有更多记录,显示"加载更多"按钮
|
|
129
|
+
if start_idx > 0:
|
|
130
|
+
self._add_load_more_button()
|
|
131
|
+
|
|
132
|
+
# 显示记录
|
|
133
|
+
for record in records_to_show:
|
|
134
|
+
self._render_record(record)
|
|
135
|
+
|
|
136
|
+
def _render_record(self, record):
|
|
137
|
+
"""渲染单条记录"""
|
|
138
|
+
role = record.get('role')
|
|
139
|
+
if role == 'user':
|
|
140
|
+
self._add_user_message(record.get('content', ''))
|
|
141
|
+
elif role == 'assistant':
|
|
142
|
+
self._add_assistant_message(record.get('content', ''))
|
|
143
|
+
elif role == 'tool':
|
|
144
|
+
name = record.get('name', '')
|
|
145
|
+
# feedback 工具拆分为两条消息
|
|
146
|
+
if 'feedback' in name.lower():
|
|
147
|
+
self._add_feedback_messages(record)
|
|
148
|
+
else:
|
|
149
|
+
self._add_tool_message(
|
|
150
|
+
name,
|
|
151
|
+
record.get('input', {}),
|
|
152
|
+
record.get('output', ''),
|
|
153
|
+
record.get('timestamp', '')
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _add_load_more_button(self):
|
|
157
|
+
"""添加加载更多按钮"""
|
|
158
|
+
self.load_more_button = QPushButton("点击查看更多")
|
|
159
|
+
self.load_more_button.setObjectName("loadMoreButton")
|
|
160
|
+
self.load_more_button.clicked.connect(self._load_more)
|
|
161
|
+
self.load_more_button.setStyleSheet("""
|
|
162
|
+
QPushButton {
|
|
163
|
+
background-color: #3a3a3a;
|
|
164
|
+
color: #e0e0e0;
|
|
165
|
+
border: 1px solid #555;
|
|
166
|
+
border-radius: 4px;
|
|
167
|
+
padding: 8px 16px;
|
|
168
|
+
font-size: 13px;
|
|
169
|
+
}
|
|
170
|
+
QPushButton:hover {
|
|
171
|
+
background-color: #4a4a4a;
|
|
172
|
+
}
|
|
173
|
+
""")
|
|
174
|
+
self.messages_layout.insertWidget(0, self.load_more_button)
|
|
175
|
+
|
|
176
|
+
def _load_more(self):
|
|
177
|
+
"""增量加载更多记录,保持滚动位置"""
|
|
178
|
+
if self.current_start_idx <= 0:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# 记录当前滚动位置
|
|
182
|
+
scrollbar = self.scroll_area.verticalScrollBar()
|
|
183
|
+
old_scroll_value = scrollbar.value()
|
|
184
|
+
old_max = scrollbar.maximum()
|
|
185
|
+
|
|
186
|
+
# 计算新的起始索引
|
|
187
|
+
new_start_idx = max(0, self.current_start_idx - 10)
|
|
188
|
+
new_records = self.all_history[new_start_idx:self.current_start_idx]
|
|
189
|
+
self.current_start_idx = new_start_idx
|
|
190
|
+
self.displayed_count += len(new_records)
|
|
191
|
+
|
|
192
|
+
# 移除旧的"加载更多"按钮
|
|
193
|
+
if self.load_more_button:
|
|
194
|
+
self.messages_layout.removeWidget(self.load_more_button)
|
|
195
|
+
self.load_more_button.deleteLater()
|
|
196
|
+
self.load_more_button = None
|
|
197
|
+
|
|
198
|
+
# 如果还有更多记录,添加新的"加载更多"按钮
|
|
199
|
+
if new_start_idx > 0:
|
|
200
|
+
self._add_load_more_button()
|
|
201
|
+
|
|
202
|
+
# 记录插入位置(在"加载更多"按钮之后)
|
|
203
|
+
insert_pos = 1 if self.load_more_button else 0
|
|
204
|
+
|
|
205
|
+
# 在顶部插入新记录
|
|
206
|
+
for record in new_records:
|
|
207
|
+
count_before = self.messages_layout.count()
|
|
208
|
+
self._render_record_at_position(record, insert_pos)
|
|
209
|
+
widgets_added = self.messages_layout.count() - count_before
|
|
210
|
+
insert_pos += widgets_added
|
|
211
|
+
|
|
212
|
+
# 恢复滚动位置(使用两次延迟确保布局完全更新)
|
|
213
|
+
def restore_scroll():
|
|
214
|
+
# 强制处理布局更新
|
|
215
|
+
self.messages_container.updateGeometry()
|
|
216
|
+
# 再次延迟以确保滚动条范围已更新
|
|
217
|
+
def do_restore():
|
|
218
|
+
new_max = scrollbar.maximum()
|
|
219
|
+
height_diff = new_max - old_max
|
|
220
|
+
scrollbar.setValue(old_scroll_value + height_diff)
|
|
221
|
+
QTimer.singleShot(50, do_restore)
|
|
222
|
+
|
|
223
|
+
QTimer.singleShot(0, restore_scroll)
|
|
224
|
+
|
|
225
|
+
def _render_record_at_position(self, record, position):
|
|
226
|
+
"""在指定位置渲染记录(用于增量加载)
|
|
227
|
+
|
|
228
|
+
复用现有的 _render_record 方法,然后将新添加的 widget 移动到指定位置
|
|
229
|
+
"""
|
|
230
|
+
# 记录当前 widget 数量
|
|
231
|
+
count_before = self.messages_layout.count()
|
|
232
|
+
|
|
233
|
+
# 使用现有方法添加记录(会添加到末尾)
|
|
234
|
+
self._render_record(record)
|
|
235
|
+
|
|
236
|
+
# 计算新添加的 widget 数量
|
|
237
|
+
count_after = self.messages_layout.count()
|
|
238
|
+
widgets_added = count_after - count_before
|
|
239
|
+
|
|
240
|
+
# 从末尾取出新添加的 widgets,插入到指定位置
|
|
241
|
+
for i in range(widgets_added):
|
|
242
|
+
item = self.messages_layout.takeAt(count_before)
|
|
243
|
+
if item and item.widget():
|
|
244
|
+
self.messages_layout.insertWidget(position + i, item.widget())
|
|
245
|
+
|
|
246
|
+
def _clear_messages(self):
|
|
247
|
+
"""清空所有消息"""
|
|
248
|
+
while self.messages_layout.count():
|
|
249
|
+
child = self.messages_layout.takeAt(0)
|
|
250
|
+
if child.widget():
|
|
251
|
+
child.widget().deleteLater()
|
|
252
|
+
|
|
253
|
+
def _show_empty_message(self):
|
|
254
|
+
"""显示无历史记录提示"""
|
|
255
|
+
empty_label = QLabel("暂无对话记录")
|
|
256
|
+
empty_label.setObjectName("emptyStateLabel")
|
|
257
|
+
empty_label.setAlignment(Qt.AlignCenter)
|
|
258
|
+
self.messages_layout.addWidget(empty_label)
|
|
259
|
+
|
|
260
|
+
def _setup_content_display(self, content: str) -> MarkdownDisplayWidget:
|
|
261
|
+
"""创建并配置内容显示组件(使用MarkdownDisplayWidget)"""
|
|
262
|
+
content_display = MarkdownDisplayWidget()
|
|
263
|
+
content_display.setMarkdownText(content)
|
|
264
|
+
content_display.setStyleSheet('''
|
|
265
|
+
QTextEdit {
|
|
266
|
+
background-color: transparent;
|
|
267
|
+
border: none;
|
|
268
|
+
padding: 0px;
|
|
269
|
+
color: #e0e0e0;
|
|
270
|
+
}
|
|
271
|
+
''')
|
|
272
|
+
content_display.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
273
|
+
content_display.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
274
|
+
content_display.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
275
|
+
|
|
276
|
+
# 根据内容自适应高度
|
|
277
|
+
doc = content_display.document()
|
|
278
|
+
doc.setTextWidth(content_display.viewport().width() if content_display.viewport().width() > 0 else 400)
|
|
279
|
+
height = int(doc.size().height()) + 10
|
|
280
|
+
content_display.setFixedHeight(height)
|
|
281
|
+
|
|
282
|
+
return content_display
|
|
283
|
+
|
|
284
|
+
def _create_avatar(self, text: str) -> QLabel:
|
|
285
|
+
"""创建头像标签"""
|
|
286
|
+
label = QLabel(text)
|
|
287
|
+
label.setObjectName("avatarLabel")
|
|
288
|
+
label.setFixedSize(32, 32)
|
|
289
|
+
label.setAlignment(Qt.AlignCenter)
|
|
290
|
+
return label
|
|
291
|
+
|
|
292
|
+
def _safe_set_text_later(self, widget, text: str, delay: int = 1000):
|
|
293
|
+
"""安全地延迟设置文本,使用弱引用避免访问已销毁对象"""
|
|
294
|
+
weak_widget = weakref.ref(widget)
|
|
295
|
+
|
|
296
|
+
def restore():
|
|
297
|
+
w = weak_widget()
|
|
298
|
+
if w is not None:
|
|
299
|
+
try:
|
|
300
|
+
w.setText(text)
|
|
301
|
+
except RuntimeError:
|
|
302
|
+
pass # 对象已销毁
|
|
303
|
+
|
|
304
|
+
QTimer.singleShot(delay, restore)
|
|
305
|
+
|
|
306
|
+
def _copy_content(self, content: str, button: QPushButton):
|
|
307
|
+
"""复制内容到剪贴板"""
|
|
308
|
+
try:
|
|
309
|
+
pyperclip.copy(content)
|
|
310
|
+
button.setText("✓")
|
|
311
|
+
self._safe_set_text_later(button, "📋")
|
|
312
|
+
except Exception as e:
|
|
313
|
+
print(f"复制失败: {e}", file=sys.stderr)
|
|
314
|
+
|
|
315
|
+
def _quote_content(self, msg_type: str, content: str, button: QPushButton):
|
|
316
|
+
"""生成引用格式并复制到剪贴板"""
|
|
317
|
+
truncated = content[:100] + "..." if len(content) > 100 else content
|
|
318
|
+
truncated = truncated.replace('\n', '\n> ')
|
|
319
|
+
quote = f"----请回忆如下引用的历史对话内容----\n```quote\n[{msg_type}]\n{truncated}\n```\n---------"
|
|
320
|
+
pyperclip.copy(quote)
|
|
321
|
+
button.setText("✓")
|
|
322
|
+
self._safe_set_text_later(button, "📎")
|
|
323
|
+
|
|
324
|
+
def _save_content(self, content: str):
|
|
325
|
+
"""弹窗输入标题,保存为.md文件"""
|
|
326
|
+
from PySide6.QtWidgets import QInputDialog
|
|
327
|
+
title, ok = QInputDialog.getText(self, "保存文档", "请输入文档标题:")
|
|
328
|
+
if ok and title:
|
|
329
|
+
safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).strip()
|
|
330
|
+
if not safe_title:
|
|
331
|
+
safe_title = "untitled"
|
|
332
|
+
|
|
333
|
+
# 优先使用 workspace_path,否则回退到 project_path/.workspace/
|
|
334
|
+
if self.workspace_path and os.path.exists(self.workspace_path):
|
|
335
|
+
save_dir = self.workspace_path
|
|
336
|
+
else:
|
|
337
|
+
save_dir = os.path.join(self.project_path, '.workspace') if self.project_path else '.'
|
|
338
|
+
|
|
339
|
+
os.makedirs(save_dir, exist_ok=True)
|
|
340
|
+
file_path = os.path.join(save_dir, f"{safe_title}.md")
|
|
341
|
+
|
|
342
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
343
|
+
f.write(content)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _add_user_message(self, content: str):
|
|
348
|
+
"""添加用户消息(居左展示,与AI消息样式相同)"""
|
|
349
|
+
row_widget = QWidget()
|
|
350
|
+
row_layout = QHBoxLayout(row_widget)
|
|
351
|
+
row_layout.setContentsMargins(0, 5, 0, 5)
|
|
352
|
+
row_layout.setSpacing(10)
|
|
353
|
+
|
|
354
|
+
# 1. 头像
|
|
355
|
+
avatar = self._create_avatar("👤")
|
|
356
|
+
row_layout.addWidget(avatar, alignment=Qt.AlignTop)
|
|
357
|
+
|
|
358
|
+
# 2. 消息气泡容器
|
|
359
|
+
bubble_container = QWidget()
|
|
360
|
+
bubble_layout = QVBoxLayout(bubble_container)
|
|
361
|
+
bubble_layout.setContentsMargins(0, 0, 0, 0)
|
|
362
|
+
bubble_layout.setSpacing(2)
|
|
363
|
+
|
|
364
|
+
# 角色标签和按钮
|
|
365
|
+
header_widget = QWidget()
|
|
366
|
+
header_layout = QHBoxLayout(header_widget)
|
|
367
|
+
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
368
|
+
role_label = QLabel("User")
|
|
369
|
+
role_label.setObjectName("roleLabel")
|
|
370
|
+
header_layout.addWidget(role_label)
|
|
371
|
+
|
|
372
|
+
copy_btn = QPushButton("📋")
|
|
373
|
+
copy_btn.setFixedSize(24, 24)
|
|
374
|
+
copy_btn.setStyleSheet("""
|
|
375
|
+
QPushButton {
|
|
376
|
+
background-color: transparent;
|
|
377
|
+
border: none;
|
|
378
|
+
font-size: 14px;
|
|
379
|
+
}
|
|
380
|
+
QPushButton:hover {
|
|
381
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
382
|
+
border-radius: 4px;
|
|
383
|
+
}
|
|
384
|
+
""")
|
|
385
|
+
copy_btn.setCursor(Qt.PointingHandCursor)
|
|
386
|
+
copy_btn.clicked.connect(lambda: self._copy_content(content, copy_btn))
|
|
387
|
+
header_layout.addWidget(copy_btn)
|
|
388
|
+
|
|
389
|
+
quote_btn = QPushButton("📎")
|
|
390
|
+
quote_btn.setFixedSize(24, 24)
|
|
391
|
+
quote_btn.setStyleSheet("""
|
|
392
|
+
QPushButton {
|
|
393
|
+
background-color: transparent;
|
|
394
|
+
border: none;
|
|
395
|
+
font-size: 14px;
|
|
396
|
+
}
|
|
397
|
+
QPushButton:hover {
|
|
398
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
399
|
+
border-radius: 4px;
|
|
400
|
+
}
|
|
401
|
+
""")
|
|
402
|
+
quote_btn.setCursor(Qt.PointingHandCursor)
|
|
403
|
+
quote_btn.clicked.connect(lambda: self._quote_content("用户消息", content, quote_btn))
|
|
404
|
+
header_layout.addWidget(quote_btn)
|
|
405
|
+
header_layout.addStretch()
|
|
406
|
+
|
|
407
|
+
bubble_layout.addWidget(header_widget)
|
|
408
|
+
|
|
409
|
+
# 气泡
|
|
410
|
+
bubble = QFrame()
|
|
411
|
+
bubble.setObjectName("aiBubble") # 使用与AI相同的样式
|
|
412
|
+
bubble_content_layout = QVBoxLayout(bubble)
|
|
413
|
+
bubble_content_layout.setContentsMargins(12, 8, 12, 8)
|
|
414
|
+
|
|
415
|
+
if content:
|
|
416
|
+
content_display = self._setup_content_display(content)
|
|
417
|
+
bubble_content_layout.addWidget(content_display)
|
|
418
|
+
|
|
419
|
+
bubble_layout.addWidget(bubble)
|
|
420
|
+
|
|
421
|
+
row_layout.addWidget(bubble_container, stretch=1)
|
|
422
|
+
|
|
423
|
+
# 3. 右侧占位
|
|
424
|
+
row_layout.addStretch(0)
|
|
425
|
+
|
|
426
|
+
self.messages_layout.addWidget(row_widget)
|
|
427
|
+
|
|
428
|
+
def _add_assistant_message(self, content: str):
|
|
429
|
+
"""添加AI消息"""
|
|
430
|
+
row_widget = QWidget()
|
|
431
|
+
row_layout = QHBoxLayout(row_widget)
|
|
432
|
+
row_layout.setContentsMargins(0, 5, 0, 5)
|
|
433
|
+
row_layout.setSpacing(10)
|
|
434
|
+
|
|
435
|
+
# 1. 头像
|
|
436
|
+
avatar = self._create_avatar("🤖")
|
|
437
|
+
row_layout.addWidget(avatar, alignment=Qt.AlignTop)
|
|
438
|
+
|
|
439
|
+
# 2. 消息气泡容器
|
|
440
|
+
bubble_container = QWidget()
|
|
441
|
+
bubble_layout = QVBoxLayout(bubble_container)
|
|
442
|
+
bubble_layout.setContentsMargins(0, 0, 0, 0)
|
|
443
|
+
bubble_layout.setSpacing(2)
|
|
444
|
+
|
|
445
|
+
# 角色标签和复制按钮
|
|
446
|
+
header_widget = QWidget()
|
|
447
|
+
header_layout = QHBoxLayout(header_widget)
|
|
448
|
+
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
449
|
+
role_label = QLabel("AI Assistant")
|
|
450
|
+
role_label.setObjectName("roleLabel")
|
|
451
|
+
header_layout.addWidget(role_label)
|
|
452
|
+
|
|
453
|
+
copy_btn = QPushButton("📋")
|
|
454
|
+
copy_btn.setFixedSize(24, 24)
|
|
455
|
+
copy_btn.setStyleSheet("""
|
|
456
|
+
QPushButton {
|
|
457
|
+
background-color: transparent;
|
|
458
|
+
border: none;
|
|
459
|
+
font-size: 14px;
|
|
460
|
+
}
|
|
461
|
+
QPushButton:hover {
|
|
462
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
463
|
+
border-radius: 4px;
|
|
464
|
+
}
|
|
465
|
+
""")
|
|
466
|
+
copy_btn.setCursor(Qt.PointingHandCursor)
|
|
467
|
+
copy_btn.clicked.connect(lambda: self._copy_content(content, copy_btn))
|
|
468
|
+
header_layout.addWidget(copy_btn)
|
|
469
|
+
|
|
470
|
+
quote_btn = QPushButton("📎")
|
|
471
|
+
quote_btn.setFixedSize(24, 24)
|
|
472
|
+
quote_btn.setStyleSheet("""
|
|
473
|
+
QPushButton {
|
|
474
|
+
background-color: transparent;
|
|
475
|
+
border: none;
|
|
476
|
+
font-size: 14px;
|
|
477
|
+
}
|
|
478
|
+
QPushButton:hover {
|
|
479
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
480
|
+
border-radius: 4px;
|
|
481
|
+
}
|
|
482
|
+
""")
|
|
483
|
+
quote_btn.setCursor(Qt.PointingHandCursor)
|
|
484
|
+
quote_btn.clicked.connect(lambda: self._quote_content("AI回复", content, quote_btn))
|
|
485
|
+
header_layout.addWidget(quote_btn)
|
|
486
|
+
|
|
487
|
+
save_btn = QPushButton("💾")
|
|
488
|
+
save_btn.setFixedSize(24, 24)
|
|
489
|
+
save_btn.setStyleSheet("""
|
|
490
|
+
QPushButton {
|
|
491
|
+
background-color: transparent;
|
|
492
|
+
border: none;
|
|
493
|
+
font-size: 14px;
|
|
494
|
+
}
|
|
495
|
+
QPushButton:hover {
|
|
496
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
497
|
+
border-radius: 4px;
|
|
498
|
+
}
|
|
499
|
+
""")
|
|
500
|
+
save_btn.setCursor(Qt.PointingHandCursor)
|
|
501
|
+
save_btn.clicked.connect(lambda: self._save_content(content))
|
|
502
|
+
header_layout.addWidget(save_btn)
|
|
503
|
+
header_layout.addStretch()
|
|
504
|
+
|
|
505
|
+
bubble_layout.addWidget(header_widget)
|
|
506
|
+
|
|
507
|
+
# 气泡
|
|
508
|
+
bubble = QFrame()
|
|
509
|
+
bubble.setObjectName("aiBubble")
|
|
510
|
+
bubble_content_layout = QVBoxLayout(bubble)
|
|
511
|
+
bubble_content_layout.setContentsMargins(12, 8, 12, 8)
|
|
512
|
+
|
|
513
|
+
if content:
|
|
514
|
+
content_display = self._setup_content_display(content)
|
|
515
|
+
bubble_content_layout.addWidget(content_display)
|
|
516
|
+
|
|
517
|
+
bubble_layout.addWidget(bubble)
|
|
518
|
+
|
|
519
|
+
row_layout.addWidget(bubble_container, stretch=1)
|
|
520
|
+
|
|
521
|
+
# 3. 右侧占位
|
|
522
|
+
row_layout.addStretch(0)
|
|
523
|
+
|
|
524
|
+
self.messages_layout.addWidget(row_widget)
|
|
525
|
+
|
|
526
|
+
def _add_feedback_messages(self, record: Dict):
|
|
527
|
+
"""将 feedback 工具拆分为两条消息:AI反馈 + 用户回复"""
|
|
528
|
+
input_data = record.get('input', {})
|
|
529
|
+
output = record.get('output', '')
|
|
530
|
+
|
|
531
|
+
# 消息1: AI 反馈 (使用 assistant 样式)
|
|
532
|
+
work_title = input_data.get('work_title', '')
|
|
533
|
+
message = input_data.get('message', '')
|
|
534
|
+
options = input_data.get('predefined_options', [])
|
|
535
|
+
files = input_data.get('files', [])
|
|
536
|
+
|
|
537
|
+
parts = []
|
|
538
|
+
if work_title:
|
|
539
|
+
parts.append(f"📢 **{work_title}**")
|
|
540
|
+
if message:
|
|
541
|
+
parts.append(message)
|
|
542
|
+
if options:
|
|
543
|
+
parts.append(f"**选项**: {' | '.join(options)}")
|
|
544
|
+
if files:
|
|
545
|
+
file_list = ', '.join([f"`{f}`" for f in files])
|
|
546
|
+
parts.append(f"**相关文件**: {file_list}")
|
|
547
|
+
|
|
548
|
+
ai_content = '\n\n'.join(parts) if parts else ''
|
|
549
|
+
if ai_content:
|
|
550
|
+
self._add_assistant_message(ai_content)
|
|
551
|
+
|
|
552
|
+
# 消息2: 用户回复 (使用 user 样式)
|
|
553
|
+
user_content = self._extract_user_feedback(output)
|
|
554
|
+
if user_content:
|
|
555
|
+
self._add_user_message(user_content)
|
|
556
|
+
|
|
557
|
+
def _extract_user_feedback(self, output: str) -> str:
|
|
558
|
+
"""从 feedback output 中提取用户输入"""
|
|
559
|
+
if not output:
|
|
560
|
+
return ''
|
|
561
|
+
# 提取 💬 用户输入 或 🔘 用户选择 后面的内容
|
|
562
|
+
for marker in ['💬 用户输入:\n', '🔘 用户选择的选项:\n']:
|
|
563
|
+
if marker in output:
|
|
564
|
+
idx = output.find(marker)
|
|
565
|
+
content = output[idx + len(marker):]
|
|
566
|
+
# 截断到 💡 请注意 之前
|
|
567
|
+
if '💡 请注意' in content:
|
|
568
|
+
end_idx = content.find('💡 请注意')
|
|
569
|
+
content = content[:end_idx].strip()
|
|
570
|
+
return content
|
|
571
|
+
return ''
|
|
572
|
+
|
|
573
|
+
def _format_tool_input(self, name: str, input_data: Dict) -> str:
|
|
574
|
+
"""格式化工具输入为 markdown"""
|
|
575
|
+
if name == 'Task':
|
|
576
|
+
desc = input_data.get('description', '')
|
|
577
|
+
prompt = input_data.get('prompt', '')
|
|
578
|
+
agent_type = input_data.get('subagent_type', '')
|
|
579
|
+
parts = []
|
|
580
|
+
# 格式: Agent(agent名称):描述
|
|
581
|
+
if agent_type and desc:
|
|
582
|
+
parts.append(f"**Agent**({agent_type}):{desc}")
|
|
583
|
+
elif agent_type:
|
|
584
|
+
parts.append(f"**Agent**({agent_type})")
|
|
585
|
+
elif desc:
|
|
586
|
+
parts.append(f"**描述**: {desc}")
|
|
587
|
+
if prompt:
|
|
588
|
+
parts.append(f"**Prompt**:\n{prompt}")
|
|
589
|
+
return '\n\n'.join(parts) if parts else str(input_data)
|
|
590
|
+
elif name in ('Read', 'Glob', 'Grep'):
|
|
591
|
+
file_path = input_data.get('file_path', input_data.get('path', ''))
|
|
592
|
+
pattern = input_data.get('pattern', '')
|
|
593
|
+
parts = []
|
|
594
|
+
if file_path:
|
|
595
|
+
parts.append(f"**路径**: `{file_path}`")
|
|
596
|
+
if pattern:
|
|
597
|
+
parts.append(f"**模式**: `{pattern}`")
|
|
598
|
+
return '\n'.join(parts) if parts else str(input_data)
|
|
599
|
+
elif name in ('Edit', 'Write'):
|
|
600
|
+
file_path = input_data.get('file_path', '')
|
|
601
|
+
return f"**文件**: `{file_path}`" if file_path else str(input_data)
|
|
602
|
+
elif name == 'Hook':
|
|
603
|
+
cmd = input_data.get('command', '')
|
|
604
|
+
return f"**命令**: `{cmd}`" if cmd else str(input_data)
|
|
605
|
+
elif 'feedback' in name.lower():
|
|
606
|
+
# feedback 工具特殊处理
|
|
607
|
+
work_title = input_data.get('work_title', '')
|
|
608
|
+
message = input_data.get('message', '')
|
|
609
|
+
options = input_data.get('predefined_options', [])
|
|
610
|
+
files = input_data.get('files', [])
|
|
611
|
+
parts = []
|
|
612
|
+
if work_title:
|
|
613
|
+
parts.append(f"📢 **{work_title}**")
|
|
614
|
+
if message:
|
|
615
|
+
parts.append(message)
|
|
616
|
+
if options:
|
|
617
|
+
parts.append(f"**选项**: {' | '.join(options)}")
|
|
618
|
+
if files:
|
|
619
|
+
parts.append(f"**相关文件**: {', '.join(files)}")
|
|
620
|
+
return '\n\n'.join(parts) if parts else str(input_data)
|
|
621
|
+
else:
|
|
622
|
+
# 其他工具显示简化的 JSON
|
|
623
|
+
input_str = json.dumps(input_data, ensure_ascii=False, indent=2)
|
|
624
|
+
if len(input_str) > 300:
|
|
625
|
+
input_str = input_str[:300] + "..."
|
|
626
|
+
return f"```json\n{input_str}\n```"
|
|
627
|
+
|
|
628
|
+
def _format_feedback_output(self, output: str) -> str:
|
|
629
|
+
"""格式化 feedback 工具的输出,提取用户输入"""
|
|
630
|
+
if not output:
|
|
631
|
+
return ''
|
|
632
|
+
# 提取 💬 用户输入 或 🔘 用户选择 后面的内容
|
|
633
|
+
for marker in ['💬 用户输入:\n', '🔘 用户选择的选项:\n']:
|
|
634
|
+
if marker in output:
|
|
635
|
+
idx = output.find(marker)
|
|
636
|
+
content = output[idx + len(marker):]
|
|
637
|
+
# 截断到 💡 请注意 之前
|
|
638
|
+
if '💡 请注意' in content:
|
|
639
|
+
end_idx = content.find('💡 请注意')
|
|
640
|
+
content = content[:end_idx].strip()
|
|
641
|
+
return f"**用户反馈**: {content}" if content else ''
|
|
642
|
+
return ''
|
|
643
|
+
|
|
644
|
+
def _add_tool_message(self, name: str, input_data: Dict, output: str, timestamp: str):
|
|
645
|
+
"""添加工具调用消息(默认折叠,feedback 默认展开)"""
|
|
646
|
+
is_feedback = 'feedback' in name.lower()
|
|
647
|
+
|
|
648
|
+
row_widget = QWidget()
|
|
649
|
+
row_layout = QHBoxLayout(row_widget)
|
|
650
|
+
row_layout.setContentsMargins(0, 2, 0, 2)
|
|
651
|
+
row_layout.setSpacing(10)
|
|
652
|
+
|
|
653
|
+
# 1. 头像
|
|
654
|
+
avatar = self._create_avatar("💬" if is_feedback else "⚙️")
|
|
655
|
+
row_layout.addWidget(avatar, alignment=Qt.AlignTop)
|
|
656
|
+
|
|
657
|
+
# 2. 消息气泡容器
|
|
658
|
+
bubble_container = QWidget()
|
|
659
|
+
bubble_layout = QVBoxLayout(bubble_container)
|
|
660
|
+
bubble_layout.setContentsMargins(0, 0, 0, 0)
|
|
661
|
+
bubble_layout.setSpacing(2)
|
|
662
|
+
|
|
663
|
+
# 生成标题文本
|
|
664
|
+
# Task 工具特殊处理:显示 Agent(agent_type): description
|
|
665
|
+
if name == 'Task':
|
|
666
|
+
agent_type = input_data.get('subagent_type', '')
|
|
667
|
+
desc = input_data.get('description', '')
|
|
668
|
+
if agent_type and desc:
|
|
669
|
+
header_title = f"Agent({agent_type}): {desc}"
|
|
670
|
+
elif agent_type:
|
|
671
|
+
header_title = f"Agent({agent_type})"
|
|
672
|
+
else:
|
|
673
|
+
header_title = f"Tool: {name}"
|
|
674
|
+
else:
|
|
675
|
+
header_title = f"Tool: {name}"
|
|
676
|
+
|
|
677
|
+
# 可点击的标题(用于展开/折叠)+ 复制按钮
|
|
678
|
+
# feedback 默认展开,其他默认折叠
|
|
679
|
+
initial_expanded = is_feedback
|
|
680
|
+
|
|
681
|
+
# Header 容器(包含折叠按钮和复制按钮)
|
|
682
|
+
header_widget = QWidget()
|
|
683
|
+
header_layout = QHBoxLayout(header_widget)
|
|
684
|
+
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
685
|
+
header_layout.setSpacing(4)
|
|
686
|
+
|
|
687
|
+
header_btn = QPushButton(f"{'▼' if initial_expanded else '▶'} {header_title}")
|
|
688
|
+
header_btn.setObjectName("toolHeaderButton")
|
|
689
|
+
# Task/Agent 使用绿色,其他工具使用灰色
|
|
690
|
+
header_color = "#4CAF50" if name == 'Task' else "#888"
|
|
691
|
+
header_hover_color = "#66BB6A" if name == 'Task' else "#aaa"
|
|
692
|
+
header_btn.setStyleSheet(f"""
|
|
693
|
+
QPushButton {{
|
|
694
|
+
background-color: transparent;
|
|
695
|
+
color: {header_color};
|
|
696
|
+
border: none;
|
|
697
|
+
text-align: left;
|
|
698
|
+
padding: 2px 0;
|
|
699
|
+
font-size: 12px;
|
|
700
|
+
}}
|
|
701
|
+
QPushButton:hover {{
|
|
702
|
+
color: {header_hover_color};
|
|
703
|
+
cursor: pointer;
|
|
704
|
+
}}
|
|
705
|
+
""")
|
|
706
|
+
header_btn.setCursor(Qt.PointingHandCursor)
|
|
707
|
+
header_layout.addWidget(header_btn)
|
|
708
|
+
|
|
709
|
+
# 复制按钮
|
|
710
|
+
copy_btn = QPushButton("📋")
|
|
711
|
+
copy_btn.setFixedSize(24, 24)
|
|
712
|
+
copy_btn.setStyleSheet("""
|
|
713
|
+
QPushButton {
|
|
714
|
+
background-color: transparent;
|
|
715
|
+
border: none;
|
|
716
|
+
font-size: 14px;
|
|
717
|
+
}
|
|
718
|
+
QPushButton:hover {
|
|
719
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
720
|
+
border-radius: 4px;
|
|
721
|
+
}
|
|
722
|
+
""")
|
|
723
|
+
copy_btn.setCursor(Qt.PointingHandCursor)
|
|
724
|
+
header_layout.addWidget(copy_btn)
|
|
725
|
+
|
|
726
|
+
# 引用按钮
|
|
727
|
+
quote_btn = QPushButton("📎")
|
|
728
|
+
quote_btn.setFixedSize(24, 24)
|
|
729
|
+
quote_btn.setStyleSheet("""
|
|
730
|
+
QPushButton {
|
|
731
|
+
background-color: transparent;
|
|
732
|
+
border: none;
|
|
733
|
+
font-size: 14px;
|
|
734
|
+
}
|
|
735
|
+
QPushButton:hover {
|
|
736
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
737
|
+
border-radius: 4px;
|
|
738
|
+
}
|
|
739
|
+
""")
|
|
740
|
+
quote_btn.setCursor(Qt.PointingHandCursor)
|
|
741
|
+
header_layout.addWidget(quote_btn)
|
|
742
|
+
|
|
743
|
+
# 保存按钮
|
|
744
|
+
save_btn = QPushButton("💾")
|
|
745
|
+
save_btn.setFixedSize(24, 24)
|
|
746
|
+
save_btn.setStyleSheet("""
|
|
747
|
+
QPushButton {
|
|
748
|
+
background-color: transparent;
|
|
749
|
+
border: none;
|
|
750
|
+
font-size: 14px;
|
|
751
|
+
}
|
|
752
|
+
QPushButton:hover {
|
|
753
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
754
|
+
border-radius: 4px;
|
|
755
|
+
}
|
|
756
|
+
""")
|
|
757
|
+
save_btn.setCursor(Qt.PointingHandCursor)
|
|
758
|
+
header_layout.addWidget(save_btn)
|
|
759
|
+
header_layout.addStretch()
|
|
760
|
+
|
|
761
|
+
bubble_layout.addWidget(header_widget)
|
|
762
|
+
|
|
763
|
+
# 气泡(feedback 默认显示,其他默认隐藏)
|
|
764
|
+
bubble = QFrame()
|
|
765
|
+
bubble.setObjectName("aiBubble")
|
|
766
|
+
bubble.setVisible(initial_expanded)
|
|
767
|
+
bubble_content_layout = QVBoxLayout(bubble)
|
|
768
|
+
bubble_content_layout.setContentsMargins(12, 8, 12, 8)
|
|
769
|
+
|
|
770
|
+
# 格式化输入
|
|
771
|
+
input_str = self._format_tool_input(name, input_data)
|
|
772
|
+
|
|
773
|
+
# 输出内容
|
|
774
|
+
output_str = str(output) if output else ''
|
|
775
|
+
|
|
776
|
+
# 过滤掉 agentId 行(Agent 调用结果中的元信息)
|
|
777
|
+
if output_str:
|
|
778
|
+
lines = output_str.split('\n')
|
|
779
|
+
filtered_lines = [line for line in lines if not line.strip().startswith('agentId:')]
|
|
780
|
+
output_str = '\n'.join(filtered_lines).strip()
|
|
781
|
+
|
|
782
|
+
# 处理 base64 图片 (只有真正的 base64 数据才替换)
|
|
783
|
+
# 真正的 base64 图片数据通常包含 data:image 或者是纯 base64 编码的长字符串
|
|
784
|
+
if output_str and len(output_str) > 500:
|
|
785
|
+
# 检测是否为 base64 图片数据
|
|
786
|
+
is_base64_image = (
|
|
787
|
+
'data:image' in output_str.lower() or
|
|
788
|
+
(output_str.startswith('/9j/') or output_str.startswith('iVBOR')) # JPEG/PNG base64 头
|
|
789
|
+
)
|
|
790
|
+
if is_base64_image:
|
|
791
|
+
output_str = "[图片]"
|
|
792
|
+
|
|
793
|
+
# feedback 工具特殊处理输出
|
|
794
|
+
if is_feedback:
|
|
795
|
+
output_str = self._format_feedback_output(output_str)
|
|
796
|
+
|
|
797
|
+
# 构建内容(不再截断,展示完整内容)
|
|
798
|
+
content_parts = [f"**Input:**\n{input_str}"]
|
|
799
|
+
if output_str:
|
|
800
|
+
content_parts.append(f"**Output:**\n{output_str}")
|
|
801
|
+
else:
|
|
802
|
+
content_parts.append("**Output:** (无输出)")
|
|
803
|
+
content = '\n\n'.join(content_parts)
|
|
804
|
+
content_display = self._setup_content_display(content)
|
|
805
|
+
bubble_content_layout.addWidget(content_display)
|
|
806
|
+
|
|
807
|
+
bubble_layout.addWidget(bubble)
|
|
808
|
+
|
|
809
|
+
# 复制按钮点击事件(复制完整内容:Input + Output)
|
|
810
|
+
copy_btn.clicked.connect(lambda: self._copy_content(content, copy_btn))
|
|
811
|
+
|
|
812
|
+
# 引用按钮点击事件
|
|
813
|
+
quote_btn.clicked.connect(lambda: self._quote_content(f"工具调用: {name}", content, quote_btn))
|
|
814
|
+
|
|
815
|
+
# 保存按钮点击事件
|
|
816
|
+
save_btn.clicked.connect(lambda: self._save_content(content))
|
|
817
|
+
|
|
818
|
+
# 点击展开/折叠
|
|
819
|
+
def toggle_content():
|
|
820
|
+
is_visible = bubble.isVisible()
|
|
821
|
+
bubble.setVisible(not is_visible)
|
|
822
|
+
header_btn.setText(f"{'▼' if not is_visible else '▶'} {header_title}")
|
|
823
|
+
|
|
824
|
+
header_btn.clicked.connect(toggle_content)
|
|
825
|
+
|
|
826
|
+
row_layout.addWidget(bubble_container, stretch=1)
|
|
827
|
+
|
|
828
|
+
# 3. 右侧占位
|
|
829
|
+
row_layout.addStretch(0)
|
|
830
|
+
|
|
831
|
+
self.messages_layout.addWidget(row_widget)
|
|
832
|
+
|
|
833
|
+
def _load_history_from_file(self) -> List[Dict]:
|
|
834
|
+
"""从Claude Code的session .jsonl文件加载历史记录"""
|
|
835
|
+
try:
|
|
836
|
+
if not self.session_id or not self.project_path:
|
|
837
|
+
return []
|
|
838
|
+
|
|
839
|
+
# 编码项目路径 (Claude Code 将 / 和 _ 都替换为 -)
|
|
840
|
+
encoded_path = self.project_path.replace('/', '-').replace('_', '-')
|
|
841
|
+
|
|
842
|
+
# 构建 .jsonl 文件路径
|
|
843
|
+
home_dir = os.path.expanduser('~')
|
|
844
|
+
jsonl_file = os.path.join(home_dir, '.claude', 'projects', encoded_path, f'{self.session_id}.jsonl')
|
|
845
|
+
|
|
846
|
+
if not os.path.exists(jsonl_file):
|
|
847
|
+
return []
|
|
848
|
+
|
|
849
|
+
# 读取所有行
|
|
850
|
+
lines = []
|
|
851
|
+
with open(jsonl_file, 'r', encoding='utf-8') as f:
|
|
852
|
+
lines = f.readlines()
|
|
853
|
+
|
|
854
|
+
# 第一遍:收集所有 tool_results
|
|
855
|
+
tool_results = {}
|
|
856
|
+
for line in lines:
|
|
857
|
+
line = line.strip()
|
|
858
|
+
if not line:
|
|
859
|
+
continue
|
|
860
|
+
try:
|
|
861
|
+
entry = json.loads(line)
|
|
862
|
+
message = entry.get('message', {})
|
|
863
|
+
if message.get('role') != 'user':
|
|
864
|
+
continue
|
|
865
|
+
content = message.get('content', [])
|
|
866
|
+
if isinstance(content, list):
|
|
867
|
+
for item in content:
|
|
868
|
+
if isinstance(item, dict) and item.get('type') == 'tool_result':
|
|
869
|
+
tool_use_id = item.get('tool_use_id')
|
|
870
|
+
tool_content = item.get('content', '')
|
|
871
|
+
# 处理 content 为数组的情况
|
|
872
|
+
if isinstance(tool_content, list):
|
|
873
|
+
texts = []
|
|
874
|
+
for c in tool_content:
|
|
875
|
+
if isinstance(c, dict) and c.get('type') == 'text':
|
|
876
|
+
texts.append(c.get('text', ''))
|
|
877
|
+
tool_content = '\n'.join(texts)
|
|
878
|
+
if tool_use_id:
|
|
879
|
+
tool_results[tool_use_id] = tool_content
|
|
880
|
+
except json.JSONDecodeError:
|
|
881
|
+
continue
|
|
882
|
+
|
|
883
|
+
# 第二遍:构建消息列表
|
|
884
|
+
messages = []
|
|
885
|
+
for line in lines:
|
|
886
|
+
line = line.strip()
|
|
887
|
+
if not line:
|
|
888
|
+
continue
|
|
889
|
+
|
|
890
|
+
try:
|
|
891
|
+
entry = json.loads(line)
|
|
892
|
+
message = entry.get('message', {})
|
|
893
|
+
role = message.get('role')
|
|
894
|
+
|
|
895
|
+
# 处理 system 消息 (hook)
|
|
896
|
+
entry_type = entry.get('type')
|
|
897
|
+
if entry_type == 'system':
|
|
898
|
+
subtype = entry.get('subtype', '')
|
|
899
|
+
if subtype == 'stop_hook_summary':
|
|
900
|
+
hook_infos = entry.get('hookInfos', [])
|
|
901
|
+
hook_errors = entry.get('hookErrors', [])
|
|
902
|
+
hook_cmd = hook_infos[0].get('command', '') if hook_infos else ''
|
|
903
|
+
# hookErrors 实际上是 hook 的输出内容
|
|
904
|
+
hook_output = '\n'.join(hook_errors) if hook_errors else '执行完成'
|
|
905
|
+
messages.append({
|
|
906
|
+
'role': 'tool',
|
|
907
|
+
'name': 'Hook',
|
|
908
|
+
'input': {'command': hook_cmd},
|
|
909
|
+
'output': hook_output,
|
|
910
|
+
'timestamp': entry.get('timestamp', '')
|
|
911
|
+
})
|
|
912
|
+
continue
|
|
913
|
+
|
|
914
|
+
if role not in ['user', 'assistant']:
|
|
915
|
+
continue
|
|
916
|
+
|
|
917
|
+
timestamp = entry.get('timestamp', '')
|
|
918
|
+
content = message.get('content', [])
|
|
919
|
+
|
|
920
|
+
# 处理 user 消息
|
|
921
|
+
if role == 'user':
|
|
922
|
+
if isinstance(content, str):
|
|
923
|
+
# 过滤 hook 注入的内容(在 "Stop hook feedback:" 或 "hook feedback:" 后面)
|
|
924
|
+
user_content = content
|
|
925
|
+
for marker in ['Stop hook feedback:\n', 'hook feedback:\n']:
|
|
926
|
+
if marker in user_content:
|
|
927
|
+
# 只保留 marker 之前的内容 + marker 本身
|
|
928
|
+
idx = user_content.find(marker)
|
|
929
|
+
user_content = user_content[:idx + len(marker)].rstrip()
|
|
930
|
+
break
|
|
931
|
+
if user_content:
|
|
932
|
+
messages.append({'role': 'user', 'content': user_content, 'timestamp': timestamp})
|
|
933
|
+
# tool_result 不作为独立消息显示
|
|
934
|
+
|
|
935
|
+
# 处理 assistant 消息
|
|
936
|
+
elif role == 'assistant':
|
|
937
|
+
if isinstance(content, list):
|
|
938
|
+
for item in content:
|
|
939
|
+
if not isinstance(item, dict):
|
|
940
|
+
continue
|
|
941
|
+
|
|
942
|
+
item_type = item.get('type')
|
|
943
|
+
|
|
944
|
+
# 文本消息
|
|
945
|
+
if item_type == 'text':
|
|
946
|
+
text = item.get('text', '')
|
|
947
|
+
if text:
|
|
948
|
+
messages.append({'role': 'assistant', 'content': text, 'timestamp': timestamp})
|
|
949
|
+
|
|
950
|
+
# 工具调用
|
|
951
|
+
elif item_type == 'tool_use':
|
|
952
|
+
tool_id = item.get('id')
|
|
953
|
+
tool_name = item.get('name', '')
|
|
954
|
+
tool_input = item.get('input', {})
|
|
955
|
+
tool_output = tool_results.get(tool_id, '')
|
|
956
|
+
|
|
957
|
+
messages.append({
|
|
958
|
+
'role': 'tool',
|
|
959
|
+
'name': tool_name,
|
|
960
|
+
'input': tool_input,
|
|
961
|
+
'output': tool_output,
|
|
962
|
+
'timestamp': timestamp
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
except json.JSONDecodeError:
|
|
966
|
+
continue
|
|
967
|
+
|
|
968
|
+
return messages
|
|
969
|
+
|
|
970
|
+
except Exception as e:
|
|
971
|
+
print(f"加载历史记录失败: {e}", file=sys.stderr)
|
|
972
|
+
return []
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def refresh_history(self):
|
|
976
|
+
"""刷新历史记录"""
|
|
977
|
+
self.load_history()
|
|
978
|
+
self._scroll_to_bottom()
|
|
979
|
+
|
|
980
|
+
def _scroll_to_bottom(self):
|
|
981
|
+
"""滚动到底部"""
|
|
982
|
+
weak_scroll = weakref.ref(self.scroll_area)
|
|
983
|
+
|
|
984
|
+
def do_scroll():
|
|
985
|
+
scroll = weak_scroll()
|
|
986
|
+
if scroll is not None:
|
|
987
|
+
try:
|
|
988
|
+
scroll.verticalScrollBar().setValue(scroll.verticalScrollBar().maximum())
|
|
989
|
+
except RuntimeError:
|
|
990
|
+
pass # 对象已销毁
|
|
991
|
+
|
|
992
|
+
QTimer.singleShot(100, do_scroll)
|
|
993
|
+
|
|
994
|
+
def showEvent(self, event):
|
|
995
|
+
"""Tab显示时加载历史记录并滚动到底部"""
|
|
996
|
+
super().showEvent(event)
|
|
997
|
+
if not self._loaded:
|
|
998
|
+
self._loaded = True
|
|
999
|
+
self.load_history()
|
|
1000
|
+
self._scroll_to_bottom()
|