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.

tabs/chat_tab.py ADDED
@@ -0,0 +1,1931 @@
1
+ """
2
+ 聊天标签页 - 包含反馈输入、预定义选项、指令管理等功能
3
+ """
4
+ import sys
5
+ import os
6
+ import json
7
+ from datetime import datetime
8
+ from typing import Optional, List, Dict
9
+ from PySide6.QtWidgets import (
10
+ QWidget, QVBoxLayout, QHBoxLayout, QFrame, QGridLayout,
11
+ QCheckBox, QPushButton, QProgressBar, QSizePolicy, QFileDialog, QMessageBox, QLabel
12
+ )
13
+ from PySide6.QtCore import Qt, Signal, QTimer, QPoint
14
+ from PySide6.QtGui import QFont, QTextCursor, QCursor
15
+
16
+ try:
17
+ from .base_tab import BaseTab
18
+ except ImportError:
19
+ from base_tab import BaseTab
20
+
21
+ try:
22
+ from ..components.feedback_text_edit import FeedbackTextEdit
23
+ from ..components.markdown_display import MarkdownDisplayWidget
24
+ except ImportError:
25
+ try:
26
+ from components.feedback_text_edit import FeedbackTextEdit
27
+ from components.markdown_display import MarkdownDisplayWidget
28
+ except ImportError:
29
+ # 如果导入失败,使用原始组件
30
+ from PySide6.QtWidgets import QTextEdit
31
+ FeedbackTextEdit = QTextEdit
32
+ MarkdownDisplayWidget = QTextEdit
33
+
34
+ # 导入指令管理组件
35
+ try:
36
+ from ..components.command_tab import CommandTabWidget
37
+ except ImportError:
38
+ try:
39
+ from components.command_tab import CommandTabWidget
40
+ except ImportError:
41
+ CommandTabWidget = None
42
+
43
+
44
+
45
+ class ChatTab(BaseTab):
46
+ """聊天标签页 - 处理用户反馈输入和交互"""
47
+
48
+ # 信号定义
49
+ feedback_submitted = Signal(list, list) # 结构化内容数组, 图片列表
50
+ command_executed = Signal(str) # 指令内容
51
+ option_executed = Signal(int) # 选项索引
52
+ text_changed = Signal() # 文本变化
53
+
54
+ def __init__(self, prompt: str, predefined_options: Optional[List[str]] = None,
55
+ project_path: Optional[str] = None, work_title: Optional[str] = None,
56
+ timeout: int = 60, files: Optional[List[str]] = None, bugdetail: Optional[str] = None,
57
+ session_id: Optional[str] = None, workspace_id: Optional[str] = None, parent=None):
58
+ super().__init__(parent)
59
+ self.prompt = prompt
60
+ self.predefined_options = predefined_options or []
61
+ self.project_path = project_path
62
+ self.work_title = work_title or ""
63
+ self.timeout = timeout
64
+ self.elapsed_time = 0
65
+ self.files = files or [] # 保存文件列表
66
+ self.bugdetail = bugdetail # 保存bug详情
67
+ self.session_id = session_id # 保存会话ID
68
+ self.workspace_id = workspace_id # 保存工作空间ID
69
+
70
+ # 阶段信息
71
+ self.stage_info = None
72
+ self._load_stage_info()
73
+
74
+ # 工作空间信息
75
+ self.workspace_goal = None
76
+ self.dialog_title = None
77
+ self._load_workspace_context()
78
+
79
+ # 任务信息
80
+ self.current_task = None
81
+ self.next_task = None
82
+ self._load_task_info()
83
+
84
+ # 深度思考模式状态 - 从设置中恢复
85
+ self.deep_thinking_mode = self._load_deep_thinking_mode()
86
+
87
+ # UI组件
88
+ self.description_display = None
89
+ self.option_checkboxes = []
90
+ self.command_widget = None
91
+ self.feedback_text = None
92
+ self.submit_button = None
93
+ self.progress_bar = None
94
+ self.image_button = None # 图片选择按钮
95
+ self.deep_thinking_button = None # 深度思考按钮
96
+
97
+ # 指令标签相关属性
98
+ self.selected_command = None # 当前选中的指令信息
99
+ self.command_label_widget = None # 指令标签组件
100
+
101
+ # Agent 标签相关属性
102
+ self.agent_tags_container = None
103
+ self.agent_tags_layout = None
104
+
105
+ # 历史记录管理器
106
+ self.history_manager = None
107
+ self._init_history_manager()
108
+
109
+ self.create_ui()
110
+
111
+ # 初始化完成后更新深度思考按钮状态
112
+ if hasattr(self, 'deep_thinking_button') and self.deep_thinking_button:
113
+ self.deep_thinking_button.setChecked(self.deep_thinking_mode)
114
+
115
+ # 保存AI发送的消息(prompt)到历史记录
116
+ if prompt and prompt.strip():
117
+ self.save_response_to_history(prompt)
118
+
119
+ def _init_history_manager(self):
120
+ """初始化历史记录管理器"""
121
+ try:
122
+ from components.chat_history import ChatHistoryManager
123
+ self.history_manager = ChatHistoryManager(self.project_path, self.session_id)
124
+ except ImportError:
125
+ try:
126
+ import sys
127
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
128
+ if parent_dir not in sys.path:
129
+ sys.path.insert(0, parent_dir)
130
+ from components.chat_history import ChatHistoryManager
131
+ self.history_manager = ChatHistoryManager(self.project_path, self.session_id)
132
+ except Exception:
133
+ self.history_manager = None
134
+
135
+ def create_ui(self):
136
+ """创建聊天标签页UI"""
137
+ layout = QVBoxLayout(self)
138
+
139
+ # Agent 标签容器(垂直布局,每个标签一行)
140
+ self.agent_tags_container = QWidget()
141
+ self.agent_tags_layout = QVBoxLayout(self.agent_tags_container)
142
+ # 设置左右边距为 0,使标签宽度与下方 MarkdownDisplayWidget 对齐
143
+ # MarkdownDisplayWidget 本身 padding 为 0,HTML body padding 为 5px
144
+ # 标签按钮内部已有 padding: 6px 12px,所以外层左右边距设为 0
145
+ self.agent_tags_layout.setContentsMargins(0, 5, 0, 5)
146
+ self.agent_tags_layout.setSpacing(5)
147
+ # 暂时注释掉 agent 标签加载
148
+ # self._load_agent_tags()
149
+ self.agent_tags_container.hide() # 默认隐藏
150
+ layout.addWidget(self.agent_tags_container)
151
+
152
+ # 构建display_prompt
153
+ display_prompt = self.prompt
154
+
155
+ # 1. 如果有bugdetail,添加到前面
156
+ if self.bugdetail:
157
+ display_prompt = f"🐛 **当前正在修复bug:**\n{self.bugdetail}\n\n---\n\n{display_prompt}"
158
+
159
+ # 2. 如果有上下文信息,添加到最前面
160
+ context_info = self._format_context_info()
161
+ if context_info:
162
+ display_prompt = f"{context_info}{display_prompt}"
163
+
164
+ # 使用支持Markdown的显示组件 - 简化布局,去掉外围框架
165
+ self.description_display = MarkdownDisplayWidget()
166
+ self.description_display.setMarkdownText(display_prompt)
167
+ # 设置合适的高度,降低最小高度以便可选项少时获得更多空间
168
+ self.description_display.setMinimumHeight(150)
169
+ # 移除最大高度限制,让Markdown区域可以自动拉伸
170
+ # self.description_display.setMaximumHeight(400)
171
+ layout.addWidget(self.description_display, 1) # 设置拉伸因子为1,占用剩余空间
172
+
173
+ # 创建一个反馈布局容器(只包含其他元素,不包含markdown显示)
174
+ feedback_container = QWidget()
175
+ feedback_layout = QVBoxLayout(feedback_container)
176
+ feedback_layout.setContentsMargins(5, 5, 5, 5)
177
+
178
+ # 添加预定义选项
179
+ if self.predefined_options:
180
+ self._create_predefined_options(feedback_layout)
181
+
182
+ # 添加阶段切换按钮(如果有)
183
+ if self.stage_info:
184
+ self._create_stage_buttons(feedback_layout)
185
+
186
+ # 添加下一任务按钮(独立显示,不依赖stage_info)
187
+ if self.next_task:
188
+ self._create_next_task_button(feedback_layout)
189
+
190
+ # 添加文件列表显示
191
+ if self.files:
192
+ self._create_files_list(feedback_layout)
193
+
194
+ # 使用新的指令管理组件(隐藏固定显示区域)
195
+ if CommandTabWidget:
196
+ self.command_widget = CommandTabWidget(self.project_path, self)
197
+ self.command_widget.command_executed.connect(self._handle_command_execution)
198
+ # 隐藏固定显示的指令区域,用户通过 / // /// 弹窗使用指令
199
+ self.command_widget.hide()
200
+
201
+ # 自由文本反馈输入
202
+ self._create_feedback_input(feedback_layout)
203
+
204
+ # 提交按钮布局
205
+ self._create_submit_section(feedback_layout)
206
+
207
+ # 进度条布局
208
+ if self.timeout > 0:
209
+ self._create_progress_section(feedback_layout)
210
+
211
+ # 添加反馈容器到主布局(不拉伸)
212
+ layout.addWidget(feedback_container, 0) # 设置拉伸因子为0,不额外拉伸
213
+
214
+ # 恢复草稿内容
215
+ self._restore_draft()
216
+
217
+ def _format_context_info(self) -> str:
218
+ """格式化上下文信息为Markdown文本
219
+
220
+ Returns:
221
+ str: 格式化的Markdown文本,如果所有信息都为空则返回空字符串
222
+
223
+ Example:
224
+ "📦 工作空间: XXX\n📍 阶段: XXX\n💬 对话: XXX\n\n---\n\n"
225
+ """
226
+ parts = []
227
+
228
+ if self.workspace_goal:
229
+ parts.append(f"📦 工作空间: {self.workspace_goal}")
230
+
231
+ if self.stage_info and self.stage_info.get('current_stage'):
232
+ stage_name = self.stage_info['current_stage'].get('title', '')
233
+ parts.append(f"📍 阶段: {stage_name}")
234
+
235
+ if self.dialog_title:
236
+ parts.append(f"💬 对话: {self.dialog_title}")
237
+
238
+ if self.current_task:
239
+ task_title = self.current_task.get('title', '')
240
+ parts.append(f"📌 当前任务: {task_title}")
241
+
242
+ if not parts:
243
+ return ""
244
+
245
+ return "\n".join(parts) + "\n\n---\n\n"
246
+
247
+ def _create_files_list(self, layout):
248
+ """创建文件列表显示区域"""
249
+ import subprocess
250
+ import platform
251
+ from functools import partial
252
+
253
+ # 导入配置管理
254
+ try:
255
+ from feedback_config import FeedbackConfig
256
+ except ImportError:
257
+ FeedbackConfig = None
258
+
259
+ # 获取配置的IDE
260
+ def get_configured_ide():
261
+ """获取配置的IDE名称,优先级:配置文件 > 环境变量 > 默认值"""
262
+ ide_name = None
263
+
264
+ # 1. 尝试从配置文件读取
265
+ if FeedbackConfig and self.project_path:
266
+ try:
267
+ config_manager = FeedbackConfig(self.project_path)
268
+ ide_name = config_manager.get_ide()
269
+ except Exception:
270
+ pass
271
+
272
+ # 2. 如果配置文件没有,使用环境变量
273
+ if not ide_name:
274
+ ide_name = os.getenv('IDE')
275
+
276
+ # 3. 最后使用默认值
277
+ if not ide_name:
278
+ ide_name = 'cursor'
279
+
280
+ return ide_name
281
+
282
+ # 创建紧凑的文件列表容器(使用水平布局)
283
+ files_container = QWidget()
284
+ files_container.setMaximumHeight(40) # 限制高度,更紧凑
285
+ files_container_layout = QHBoxLayout(files_container)
286
+ files_container_layout.setContentsMargins(5, 5, 5, 5)
287
+ files_container_layout.setSpacing(10)
288
+
289
+ # 添加文件图标标题
290
+ title_label = QLabel("📝")
291
+ title_label.setToolTip("AI创建或修改的文件")
292
+ title_label.setStyleSheet("""
293
+ QLabel {
294
+ font-size: 14px;
295
+ color: #888;
296
+ background-color: transparent;
297
+ }
298
+ """)
299
+ files_container_layout.addWidget(title_label)
300
+
301
+ # 为每个文件创建紧凑的可点击标签
302
+ for file_path in self.files:
303
+ file_name = os.path.basename(file_path)
304
+ # 如果文件名太长,截断显示
305
+ display_name = file_name if len(file_name) <= 20 else file_name[:17] + "..."
306
+
307
+ file_btn = QPushButton(display_name)
308
+ # 获取IDE名称(使用配置)
309
+ ide_name = get_configured_ide()
310
+ # IDE显示名称映射
311
+ ide_display_names = {
312
+ 'cursor': 'Cursor',
313
+ 'kiro': 'Kiro',
314
+ 'vscode': 'VSCode',
315
+ 'code': 'VSCode'
316
+ }
317
+ display_ide = ide_display_names.get(ide_name.lower(), ide_name)
318
+ file_btn.setToolTip(f"点击在{display_ide}中打开: {file_path}")
319
+ file_btn.setCursor(Qt.PointingHandCursor) # 设置手形光标
320
+ file_btn.setStyleSheet("""
321
+ QPushButton {
322
+ background-color: rgba(76, 175, 80, 20);
323
+ color: #4CAF50;
324
+ border: 1px solid rgba(76, 175, 80, 40);
325
+ padding: 3px 8px;
326
+ border-radius: 3px;
327
+ font-size: 11px;
328
+ font-weight: 500;
329
+ }
330
+ QPushButton:hover {
331
+ background-color: rgba(76, 175, 80, 40);
332
+ border: 1px solid #4CAF50;
333
+ }
334
+ QPushButton:pressed {
335
+ background-color: rgba(76, 175, 80, 60);
336
+ }
337
+ """)
338
+
339
+ # 使用partial函数绑定参数,避免闭包问题
340
+ def open_with_ide(file_path):
341
+ try:
342
+ # 导入ide_utils模块
343
+ import sys
344
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
345
+ if parent_dir not in sys.path:
346
+ sys.path.insert(0, parent_dir)
347
+ from ide_utils import open_project_with_ide
348
+
349
+ # 获取IDE名称(使用配置)
350
+ ide_name = get_configured_ide()
351
+
352
+ # 使用通用的IDE打开函数
353
+ success = open_project_with_ide(file_path, ide_name)
354
+
355
+ if not success:
356
+ # 如果失败,使用系统默认编辑器打开
357
+ if platform.system() == "Darwin":
358
+ subprocess.run(["open", file_path], check=True)
359
+ elif platform.system() == "Windows":
360
+ os.startfile(file_path)
361
+ else:
362
+ subprocess.run(["xdg-open", file_path], check=True)
363
+
364
+ except Exception as e:
365
+ # 使用系统默认编辑器打开作为最终后备
366
+ try:
367
+ if platform.system() == "Darwin":
368
+ subprocess.run(["open", file_path], check=True)
369
+ elif platform.system() == "Windows":
370
+ os.startfile(file_path)
371
+ else:
372
+ subprocess.run(["xdg-open", file_path], check=True)
373
+ except Exception as e2:
374
+ QMessageBox.warning(self, "打开失败",
375
+ f"无法打开文件: {file_name}\n"
376
+ f"路径: {file_path}\n"
377
+ f"错误: {str(e2)}")
378
+
379
+ file_btn.clicked.connect(partial(open_with_ide, file_path))
380
+ files_container_layout.addWidget(file_btn)
381
+
382
+ # 添加弹簧使按钮靠左对齐
383
+ files_container_layout.addStretch()
384
+
385
+ layout.addWidget(files_container)
386
+
387
+ def _create_predefined_options(self, layout):
388
+ """创建预定义选项区域 - 与原始版本样式保持一致,高度自适应"""
389
+ options_frame = QFrame()
390
+
391
+ # 根据选项数量动态计算高度
392
+ total_options = len(self.predefined_options)
393
+ columns = 2 # 两列布局
394
+ rows = (total_options + columns - 1) // columns # 向上取整
395
+ item_height = 26 # 每行约26px(包含按钮高度+间距)
396
+ padding = 8 # 上下边距
397
+ calculated_height = max(rows * item_height + padding, 50) # 最小50px
398
+
399
+ options_frame.setMinimumHeight(calculated_height)
400
+ options_frame.setMaximumHeight(calculated_height) # 设置最大高度=最小高度,实现固定自适应高度
401
+
402
+ # 使用网格布局实现两列显示,与原版保持一致
403
+ options_layout = QGridLayout(options_frame)
404
+ options_layout.setContentsMargins(0, 2, 0, 2)
405
+ options_layout.setSpacing(0) # 设置间距
406
+
407
+ for i, option in enumerate(self.predefined_options):
408
+ # 计算当前项目在网格中的位置
409
+ row = i // columns
410
+ col = i % columns
411
+
412
+ # Create horizontal layout for each option (checkbox + button)
413
+ option_item_frame = QFrame()
414
+ option_item_layout = QHBoxLayout(option_item_frame)
415
+ option_item_layout.setContentsMargins(5, 0, 5, 0)
416
+
417
+ # Checkbox
418
+ checkbox = QCheckBox(option)
419
+ self.option_checkboxes.append(checkbox)
420
+ option_item_layout.addWidget(checkbox)
421
+
422
+ # Add stretch to push button to the right
423
+ option_item_layout.addStretch()
424
+
425
+ # Execute button for this option - 使用与原始版本相同的样式
426
+ execute_btn = QPushButton("立即执行")
427
+ execute_btn.setMaximumWidth(80)
428
+ execute_btn.setProperty('option_index', i)
429
+ execute_btn.clicked.connect(lambda checked, idx=i: self._execute_option_immediately(idx))
430
+ execute_btn.setStyleSheet("""
431
+ QPushButton {
432
+ background-color: #FF9800;
433
+ color: white;
434
+ border: none;
435
+ padding: 4px 8px;
436
+ border-radius: 3px;
437
+ font-size: 11px;
438
+ }
439
+ QPushButton:hover {
440
+ background-color: #F57C00;
441
+ }
442
+ QPushButton:pressed {
443
+ background-color: #E65100;
444
+ }
445
+ """)
446
+ option_item_layout.addWidget(execute_btn)
447
+
448
+ # Add frame to grid layout
449
+ options_layout.addWidget(option_item_frame, row, col)
450
+
451
+ layout.addWidget(options_frame)
452
+
453
+ def _create_feedback_input(self, layout):
454
+ """创建反馈输入区域"""
455
+ # 创建指令标签区域(默认隐藏)
456
+ self._create_command_label_section(layout)
457
+
458
+ self.feedback_text = FeedbackTextEdit()
459
+
460
+ # 设置项目路径,启用指令弹窗功能
461
+ if self.project_path:
462
+ self.feedback_text.set_project_path(self.project_path)
463
+
464
+ # 设置自定义指令选择处理器
465
+ self.feedback_text.set_command_handler(self._on_command_selected_new)
466
+
467
+ # 设置输入框的大小策略,让它能够随窗口拉伸自适应高度
468
+ self.feedback_text.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
469
+
470
+ font_metrics = self.feedback_text.fontMetrics()
471
+ row_height = font_metrics.height()
472
+ # Calculate height for 5 lines + some padding for margins
473
+ padding = self.feedback_text.contentsMargins().top() + self.feedback_text.contentsMargins().bottom() + 5
474
+ self.feedback_text.setMinimumHeight(5 * row_height + padding)
475
+
476
+ self.feedback_text.setPlaceholderText("请在此输入您的反馈内容 (Ctrl+Enter 或 Cmd+Enter,输入/打开项目指令; 输入//打开个人指令;输入///打开系统指令;输入指令对应的字母选中指令)")
477
+
478
+ # 监听文本变化,动态改变发送按钮颜色
479
+ self.feedback_text.textChanged.connect(self._on_text_changed)
480
+
481
+ layout.addWidget(self.feedback_text)
482
+
483
+ def _create_command_label_section(self, layout):
484
+ """创建紧凑型Element UI Tag风格的指令标签区域"""
485
+ self.command_label_widget = QFrame()
486
+ # 默认样式,会在显示时根据类型动态设置
487
+ self.command_label_widget.setStyleSheet("""
488
+ QFrame {
489
+ background: #409EFF;
490
+ border: 1px solid #409EFF;
491
+ border-radius: 4px;
492
+ margin: 2px 0px;
493
+ padding: 0px;
494
+ }
495
+ """)
496
+ self.command_label_widget.hide() # 默认隐藏
497
+
498
+ label_layout = QHBoxLayout(self.command_label_widget)
499
+ label_layout.setContentsMargins(6, 4, 6, 4)
500
+ label_layout.setSpacing(6)
501
+
502
+ # 关闭按钮 - 在容器内左侧
503
+ close_button = QPushButton("×")
504
+ close_button.setFixedSize(16, 16)
505
+ close_button.setToolTip("清除选中的指令 (或按ESC键)")
506
+ close_button.setStyleSheet("""
507
+ QPushButton {
508
+ background: transparent;
509
+ color: rgba(255, 255, 255, 0.8);
510
+ border: none;
511
+ border-radius: 8px;
512
+ font-size: 12px;
513
+ font-weight: bold;
514
+ }
515
+ QPushButton:hover {
516
+ background: rgba(255, 255, 255, 0.2);
517
+ color: white;
518
+ }
519
+ QPushButton:pressed {
520
+ background: rgba(255, 255, 255, 0.3);
521
+ }
522
+ """)
523
+ close_button.clicked.connect(self._clear_selected_command)
524
+ label_layout.addWidget(close_button)
525
+
526
+ # 指令标题标签
527
+ self.command_title_label = QLabel()
528
+ self.command_title_label.setStyleSheet("""
529
+ QLabel {
530
+ color: white;
531
+ font-weight: 500;
532
+ font-size: 12px;
533
+ background: transparent;
534
+ border: none;
535
+ padding: 0px;
536
+ }
537
+ """)
538
+ label_layout.addWidget(self.command_title_label)
539
+
540
+ # 编辑按钮 - 小图标
541
+ edit_button = QPushButton("✏️")
542
+ edit_button.setFixedSize(16, 16)
543
+ edit_button.setToolTip("在IDE中打开指令文件")
544
+ edit_button.setStyleSheet("""
545
+ QPushButton {
546
+ background: transparent;
547
+ color: rgba(255, 255, 255, 0.8);
548
+ border: none;
549
+ border-radius: 8px;
550
+ font-size: 10px;
551
+ font-weight: bold;
552
+ }
553
+ QPushButton:hover {
554
+ background: rgba(255, 255, 255, 0.2);
555
+ color: white;
556
+ }
557
+ QPushButton:pressed {
558
+ background: rgba(255, 255, 255, 0.3);
559
+ }
560
+ """)
561
+ edit_button.clicked.connect(self._edit_selected_command)
562
+ label_layout.addWidget(edit_button)
563
+
564
+ layout.addWidget(self.command_label_widget)
565
+
566
+ def _on_command_selected_new(self, command_content: str, command_data: dict = None):
567
+ """新的指令选择处理方法 - 显示标签而不是替换文本"""
568
+ # 使用直接传递的指令数据,避免通过弹窗获取可能不准确的数据
569
+ if command_data:
570
+ self.selected_command = {
571
+ 'title': command_data.get('title', '未知指令'),
572
+ 'content': command_content,
573
+ 'type': command_data.get('type', 'unknown'),
574
+ 'full_path': command_data.get('full_path', '') # 保存文件路径
575
+ }
576
+ self._show_command_label()
577
+
578
+ # 关闭弹窗但不修改输入框内容
579
+ self.feedback_text._close_command_popup()
580
+
581
+ def _show_command_label(self):
582
+ """显示紧凑型Element UI Tag风格的指令标签"""
583
+ if not self.selected_command:
584
+ return
585
+
586
+ # Element UI Tag的类型配色
587
+ type_config = {
588
+ 'project': {
589
+ 'bg_color': '#409EFF',
590
+ 'border_color': '#409EFF'
591
+ },
592
+ 'personal': {
593
+ 'bg_color': '#67C23A',
594
+ 'border_color': '#67C23A'
595
+ },
596
+ 'plugin': {
597
+ 'bg_color': '#409EFF', # 与项目指令使用相同的蓝色
598
+ 'border_color': '#409EFF'
599
+ },
600
+ 'system': {
601
+ 'bg_color': '#E6A23C',
602
+ 'border_color': '#E6A23C'
603
+ }
604
+ }
605
+
606
+ config = type_config.get(self.selected_command['type'], {
607
+ 'bg_color': '#909399',
608
+ 'border_color': '#909399'
609
+ })
610
+
611
+ # 更新整个容器的Element UI Tag样式
612
+ self.command_label_widget.setStyleSheet(f"""
613
+ QFrame {{
614
+ background: {config['bg_color']};
615
+ border: 1px solid {config['border_color']};
616
+ border-radius: 4px;
617
+ margin: 2px 0px;
618
+ padding: 0px;
619
+ }}
620
+ """)
621
+
622
+ # 设置标题
623
+ self.command_title_label.setText(self.selected_command['title'])
624
+
625
+ # 显示标签
626
+ self.command_label_widget.show()
627
+
628
+ def _clear_selected_command(self):
629
+ """清除选中的指令"""
630
+ self.selected_command = None
631
+ self.command_label_widget.hide()
632
+
633
+ def _select_image(self):
634
+ """选择图片文件"""
635
+ try:
636
+ file_dialog = QFileDialog(self)
637
+ file_dialog.setFileMode(QFileDialog.ExistingFiles) # 允许选择多个文件
638
+ file_dialog.setNameFilter("图片文件 (*.png *.jpg *.jpeg *.gif *.bmp *.webp);;所有文件 (*)")
639
+ file_dialog.setWindowTitle("选择图片文件")
640
+
641
+ if file_dialog.exec():
642
+ selected_files = file_dialog.selectedFiles()
643
+
644
+ for file_path in selected_files:
645
+ # 检查文件大小
646
+ try:
647
+ import os
648
+ file_size = os.path.getsize(file_path)
649
+ file_size_mb = file_size / (1024 * 1024)
650
+
651
+ if file_size_mb > 50: # 限制原始文件大小不超过50MB
652
+ QMessageBox.warning(
653
+ self,
654
+ "文件过大",
655
+ f"文件 {os.path.basename(file_path)} 大小为 {file_size_mb:.1f}MB,超过50MB限制。\n"
656
+ "请选择更小的图片文件。"
657
+ )
658
+ continue
659
+
660
+ # 添加图片到编辑器
661
+ self.feedback_text.add_image_file(file_path)
662
+
663
+ except Exception as e:
664
+ QMessageBox.warning(
665
+ self,
666
+ "添加图片失败",
667
+ f"无法添加图片 {file_path}: {str(e)}"
668
+ )
669
+
670
+ except Exception as e:
671
+ QMessageBox.critical(
672
+ self,
673
+ "选择图片失败",
674
+ f"选择图片时发生错误: {str(e)}"
675
+ )
676
+
677
+ def _create_submit_section(self, layout):
678
+ """创建提交按钮区域"""
679
+ submit_layout = QHBoxLayout()
680
+
681
+ # 深度思考按钮 - 放在最左边(已隐藏)
682
+ # self.deep_thinking_button = QPushButton("🧠")
683
+ # self.deep_thinking_button.setToolTip("深度思考模式")
684
+ # self.deep_thinking_button.setCheckable(True) # 可切换状态
685
+ # self.deep_thinking_button.setChecked(self.deep_thinking_mode)
686
+ # self.deep_thinking_button.clicked.connect(self._toggle_deep_thinking)
687
+ # self.deep_thinking_button.setMaximumWidth(30)
688
+ # self.deep_thinking_button.setObjectName("deep_thinking_btn")
689
+ # self.deep_thinking_button.setStyleSheet("""
690
+ # QPushButton#deep_thinking_btn {
691
+ # background-color: #404040;
692
+ # color: white;
693
+ # border: 1px solid #555;
694
+ # height: 30px;
695
+ # width: 30px;
696
+ # line-height: 30px;
697
+ # text-align: center;
698
+ # border-radius: 4px;
699
+ # font-size: 18px;
700
+ # font-weight: bold;
701
+ # }
702
+ # QPushButton#deep_thinking_btn:checked {
703
+ # background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
704
+ # stop:0 #667eea, stop:1 #764ba2);
705
+ # border: 2px solid #667eea;
706
+ # }
707
+ # QPushButton#deep_thinking_btn:hover {
708
+ # background-color: #505050;
709
+ # }
710
+ # QPushButton#deep_thinking_btn:checked:hover {
711
+ # background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
712
+ # stop:0 #7788ff, stop:1 #8755b2);
713
+ # }
714
+ # QPushButton#deep_thinking_btn:pressed {
715
+ # background-color: #303030;
716
+ # }
717
+ # """)
718
+ # submit_layout.addWidget(self.deep_thinking_button)
719
+ #
720
+ # # 添加一些间距
721
+ # submit_layout.addSpacing(5)
722
+
723
+ # 指令按钮 - 快速打开指令弹层
724
+ self.command_button = QPushButton("⚡")
725
+ self.command_button.setToolTip("打开指令列表 (相当于输入 / 触发)")
726
+ self.command_button.clicked.connect(self._show_command_popup)
727
+ self.command_button.setMaximumWidth(30)
728
+ self.command_button.setStyleSheet("""
729
+ QPushButton {
730
+ background-color: #666666;
731
+ color: white;
732
+ border: none;
733
+ height:30px;
734
+ width:30px;
735
+ line-height:30px;
736
+ text-align:center;
737
+ border-radius: 4px;
738
+ font-size: 18px;
739
+ font-weight: bold;
740
+ }
741
+ QPushButton:hover {
742
+ background-color: #777777;
743
+ }
744
+ QPushButton:pressed {
745
+ background-color: #555555;
746
+ }
747
+ """)
748
+ submit_layout.addWidget(self.command_button)
749
+
750
+ # 添加一些间距
751
+ submit_layout.addSpacing(5)
752
+
753
+ # 图片选择按钮 - 只保留图标,与发送按钮并排
754
+ self.image_button = QPushButton("📷")
755
+ self.image_button.setToolTip("选择图片文件 (支持 PNG、JPG、JPEG、GIF、BMP、WebP)")
756
+ self.image_button.clicked.connect(self._select_image)
757
+ # 设置最小宽度,让高度自动匹配发送按钮
758
+ self.image_button.setMaximumWidth(30)
759
+ self.image_button.setStyleSheet("""
760
+ QPushButton {
761
+ background-color: #666666;
762
+ color: white;
763
+ border: none;
764
+ height:30px;
765
+ width:30px;
766
+ line-height:30px;
767
+ text-align:center;
768
+ border-radius: 4px;
769
+ font-size: 18px;
770
+ font-weight: bold;
771
+ }
772
+ QPushButton:hover {
773
+ background-color: #777777;
774
+ }
775
+ QPushButton:pressed {
776
+ background-color: #555555;
777
+ }
778
+ """)
779
+ submit_layout.addWidget(self.image_button)
780
+
781
+ # 添加一些间距
782
+ submit_layout.addSpacing(5)
783
+
784
+ # Submit button
785
+ self.submit_button = QPushButton("发送反馈(Ctrl+Enter 或 Cmd+Enter 提交)")
786
+ self.submit_button.clicked.connect(self._submit_feedback)
787
+ self.submit_button.setStyleSheet("""
788
+ QPushButton {
789
+ background-color: #666666;
790
+ color: white;
791
+ border: none;
792
+ height:30px;
793
+ line-height:30px;
794
+ text-align:center;
795
+ border-radius: 4px;
796
+ font-size: 12px;
797
+ }
798
+ QPushButton:hover {
799
+ background-color: #777777;
800
+ }
801
+ QPushButton:pressed {
802
+ background-color: #555555;
803
+ }
804
+ """)
805
+ submit_layout.addWidget(self.submit_button)
806
+
807
+ layout.addLayout(submit_layout)
808
+
809
+ def _create_progress_section(self, layout):
810
+ """创建进度条区域"""
811
+ progress_layout = QHBoxLayout()
812
+
813
+ # Countdown progress bar section
814
+ self.progress_bar = QProgressBar()
815
+ self.progress_bar.setRange(0, self.timeout)
816
+ self.progress_bar.setValue(self.elapsed_time)
817
+ self.progress_bar.setFormat(self._format_time(self.elapsed_time))
818
+ self.progress_bar.setStyleSheet("""
819
+ QProgressBar {
820
+ border: 1px solid #444;
821
+ border-radius: 2px;
822
+ background-color: #2b2b2b;
823
+ height: 2px;
824
+ color: white;
825
+ font-size: 11px;
826
+ text-align: right;
827
+ }
828
+ QProgressBar::chunk {
829
+ background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
830
+ stop: 0 #4CAF50, stop: 0.5 #45a049, stop: 1 #4CAF50);
831
+ border-radius: 2px;
832
+ }
833
+ """)
834
+ progress_layout.addWidget(self.progress_bar)
835
+ layout.addLayout(progress_layout)
836
+
837
+ def _handle_command_execution(self, command_content: str):
838
+ """处理指令执行"""
839
+ if command_content:
840
+ self.command_executed.emit(command_content)
841
+
842
+ def _execute_option_immediately(self, option_index: int):
843
+ """立即执行选项"""
844
+ self.option_executed.emit(option_index)
845
+
846
+ def _show_command_popup(self):
847
+ """显示指令弹窗"""
848
+ try:
849
+ # 确保输入框有焦点
850
+ if self.feedback_text:
851
+ self.feedback_text.setFocus()
852
+
853
+ # 触发指令弹窗(默认显示项目指令)
854
+ if hasattr(self.feedback_text, '_show_command_popup'):
855
+ self.feedback_text._show_command_popup("", "project")
856
+ else:
857
+ QMessageBox.information(self, "提示", "指令功能暂不可用")
858
+
859
+ except Exception as e:
860
+ QMessageBox.critical(self, "错误", f"显示指令弹窗失败: {str(e)}")
861
+
862
+ def _on_text_changed(self):
863
+ """文本变化处理"""
864
+ if self.feedback_text and self.submit_button:
865
+ # 根据文本内容动态改变按钮颜色 - 与原版保持一致
866
+ has_text = bool(self.feedback_text.toPlainText().strip())
867
+ if has_text:
868
+ # 有内容时,按钮变为蓝色(与原版一致)
869
+ self.submit_button.setStyleSheet("""
870
+ QPushButton {
871
+ background-color: #2196F3;
872
+ color: white;
873
+ border: none;
874
+ height:30px;
875
+ line-height:30px;
876
+ text-align:center;
877
+ border-radius: 4px;
878
+ font-size: 12px;
879
+ }
880
+ QPushButton:hover {
881
+ background-color: #1976D2;
882
+ }
883
+ QPushButton:pressed {
884
+ background-color: #0D47A1;
885
+ }
886
+ """)
887
+ else:
888
+ # 无内容时,按钮为灰色(与原版一致)
889
+ self.submit_button.setStyleSheet("""
890
+ QPushButton {
891
+ background-color: #666666;
892
+ color: white;
893
+ border: none;
894
+ height:30px;
895
+ line-height:30px;
896
+ text-align:center;
897
+ border-radius: 4px;
898
+ font-size: 12px;
899
+ }
900
+ QPushButton:hover {
901
+ background-color: #777777;
902
+ }
903
+ QPushButton:pressed {
904
+ background-color: #555555;
905
+ }
906
+ """)
907
+
908
+ self.text_changed.emit()
909
+
910
+ def _get_text_with_image_placeholders(self):
911
+ """获取包含图片占位符的文本
912
+
913
+ 遍历文档内容,在图片位置插入占位符 [图片1]、[图片2] 等
914
+ """
915
+ if not self.feedback_text:
916
+ return ""
917
+
918
+ document = self.feedback_text.document()
919
+ cursor = QTextCursor(document)
920
+ cursor.movePosition(QTextCursor.Start)
921
+
922
+ result_text = ""
923
+ image_index = 1
924
+ block = document.begin()
925
+
926
+ # 遍历所有文本块
927
+ while block.isValid():
928
+ # 获取当前块的迭代器
929
+ it = block.begin()
930
+
931
+ # 遍历块中的所有片段
932
+ while not it.atEnd():
933
+ fragment = it.fragment()
934
+ if fragment.isValid():
935
+ char_format = fragment.charFormat()
936
+
937
+ # 检查是否是图片格式
938
+ if char_format.isImageFormat():
939
+ # 插入图片占位符
940
+ result_text += f"[图片{image_index}]"
941
+ image_index += 1
942
+ else:
943
+ # 添加普通文本
944
+ result_text += fragment.text()
945
+
946
+ it += 1
947
+
948
+ # 添加块之间的换行符(除了最后一个块)
949
+ block = block.next()
950
+ if block.isValid():
951
+ result_text += "\n"
952
+
953
+ return result_text.strip()
954
+
955
+ def _submit_feedback(self):
956
+ """提交反馈"""
957
+ if not self.feedback_text:
958
+ return
959
+
960
+ # 获取包含图片占位符的文本内容
961
+ text_content = self._get_text_with_image_placeholders()
962
+
963
+ # 在图片占位符文本基础上,解析大文本占位符
964
+ if hasattr(self.feedback_text, 'resolve_large_text_placeholders'):
965
+ text_content = self.feedback_text.resolve_large_text_placeholders(text_content)
966
+
967
+ images = self.feedback_text.get_pasted_images() if hasattr(self.feedback_text, 'get_pasted_images') else []
968
+
969
+ # 获取选中的预定义选项
970
+ selected_options = []
971
+ for i, checkbox in enumerate(self.option_checkboxes):
972
+ if checkbox.isChecked():
973
+ selected_options.append(self.predefined_options[i])
974
+
975
+ # 检查是否有内容可发送:文本、图片或选中的选项
976
+ if not text_content.strip() and not images and not selected_options:
977
+ return # 没有内容,不发送
978
+
979
+ # 检查已选中的指令(优先使用新的指令标签机制)
980
+ selected_command_content = ""
981
+ if self.selected_command:
982
+ # 使用新的指令标签机制
983
+ selected_command_content = self.selected_command['content']
984
+ elif hasattr(self, 'command_widget') and self.command_widget:
985
+ # 兼容原有的指令选择方式
986
+ for i in range(self.command_widget.count()):
987
+ tab = self.command_widget.widget(i)
988
+ # 检查是否有command_button_group(所有指令选项卡都有)
989
+ if hasattr(tab, 'command_button_group'):
990
+ checked_button = tab.command_button_group.checkedButton()
991
+ if checked_button:
992
+ command_index = checked_button.property('command_index')
993
+ # 检查是否有commands数组(所有指令选项卡都有)
994
+ if (command_index is not None and
995
+ hasattr(tab, 'commands') and
996
+ 0 <= command_index < len(tab.commands)):
997
+ selected_command_content = tab.commands[command_index]['content']
998
+ break # 找到就停止查找
999
+
1000
+ # 构建结构化内容数组
1001
+ content_parts = []
1002
+
1003
+ # 如果开启深度思考模式,在最前面添加提示
1004
+ if self.deep_thinking_mode:
1005
+ content_parts.append({
1006
+ "type": "text",
1007
+ "text": "**ultrathink**"
1008
+ })
1009
+
1010
+ # 添加选中的预定义选项
1011
+ if selected_options:
1012
+ content_parts.append({
1013
+ "type": "options",
1014
+ "text": "; ".join(selected_options)
1015
+ })
1016
+
1017
+ # 添加选中的指令内容
1018
+ if selected_command_content:
1019
+ content_parts.append({
1020
+ "type": "command",
1021
+ "text": selected_command_content
1022
+ })
1023
+
1024
+ # 处理大文本:如果超过10k字符,保存为文件
1025
+ if text_content and len(text_content) > 10000:
1026
+ try:
1027
+ import tempfile
1028
+ from datetime import datetime
1029
+
1030
+ # 使用与图片相同的目录
1031
+ if self.project_path:
1032
+ tmp_dir = os.path.join(self.project_path, ".workspace", "chat_history", "tmp")
1033
+ os.makedirs(tmp_dir, exist_ok=True)
1034
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1035
+ txt_path = os.path.join(tmp_dir, f"{timestamp}_text.txt")
1036
+ else:
1037
+ txt_path = tempfile.mktemp(suffix=".txt")
1038
+
1039
+ with open(txt_path, 'w', encoding='utf-8') as f:
1040
+ f.write(text_content)
1041
+
1042
+ # 使用文件路径替代原始文本
1043
+ content_parts.append({
1044
+ "type": "text",
1045
+ "text": f"[大文本已保存到文件: {txt_path}]"
1046
+ })
1047
+ except Exception:
1048
+ # 如果保存失败,仍然使用原始文本
1049
+ content_parts.append({
1050
+ "type": "text",
1051
+ "text": text_content
1052
+ })
1053
+ elif text_content:
1054
+ content_parts.append({
1055
+ "type": "text",
1056
+ "text": text_content
1057
+ })
1058
+
1059
+ # 始终发送信号,即使content_parts为空(允许发送空反馈)
1060
+ self.feedback_submitted.emit(content_parts, images)
1061
+
1062
+ # 提交后在后台清空草稿
1063
+ if self.history_manager:
1064
+ import threading
1065
+ threading.Thread(target=self.history_manager.clear_draft, daemon=True).start()
1066
+
1067
+ # 提交后清空输入框和选项,避免超时/关闭时重复保存
1068
+ self.clear_feedback()
1069
+
1070
+ def _format_time(self, seconds: int) -> str:
1071
+ """格式化时间显示"""
1072
+ if seconds < 60:
1073
+ return f"AI已等待: {seconds}秒"
1074
+ else:
1075
+ minutes = seconds // 60
1076
+ remaining_seconds = seconds % 60
1077
+ return f"AI已等待: {minutes}分{remaining_seconds}秒"
1078
+
1079
+ def update_progress(self, elapsed_time: int):
1080
+ """更新进度条"""
1081
+ self.elapsed_time = elapsed_time
1082
+ if self.progress_bar:
1083
+ self.progress_bar.setValue(elapsed_time)
1084
+ self.progress_bar.setFormat(self._format_time(elapsed_time))
1085
+
1086
+ def get_feedback_text(self) -> str:
1087
+ """获取反馈文本"""
1088
+ if self.feedback_text:
1089
+ return self.feedback_text.toPlainText().strip()
1090
+ return ""
1091
+
1092
+ def get_selected_options(self) -> List[str]:
1093
+ """获取选中的预定义选项"""
1094
+ selected = []
1095
+ for i, checkbox in enumerate(self.option_checkboxes):
1096
+ if checkbox.isChecked():
1097
+ selected.append(self.predefined_options[i])
1098
+ return selected
1099
+
1100
+ def _toggle_deep_thinking(self):
1101
+ """切换深度思考模式"""
1102
+ self.deep_thinking_mode = self.deep_thinking_button.isChecked()
1103
+
1104
+ # 保存状态到设置
1105
+ self._save_deep_thinking_mode(self.deep_thinking_mode)
1106
+
1107
+ # 更新工具提示
1108
+ if self.deep_thinking_button:
1109
+ if self.deep_thinking_mode:
1110
+ self.deep_thinking_button.setToolTip("深度思考模式已开启 (点击关闭)")
1111
+ else:
1112
+ self.deep_thinking_button.setToolTip("深度思考模式 (点击开启)")
1113
+
1114
+ def _load_stage_info(self):
1115
+ """加载工作空间阶段信息"""
1116
+ # 如果没有session_id和workspace_id,直接返回
1117
+ if not self.session_id and not self.workspace_id:
1118
+ return
1119
+
1120
+ try:
1121
+ # 导入工作空间管理器
1122
+ import sys
1123
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1124
+ if parent_dir not in sys.path:
1125
+ sys.path.insert(0, parent_dir)
1126
+ from workspace_manager import WorkspaceManager
1127
+
1128
+ # 创建管理器实例
1129
+ manager = WorkspaceManager(self.project_path)
1130
+
1131
+ # 优先使用workspace_id,如果没有则使用session_id
1132
+ self.stage_info = manager.get_stage_info(
1133
+ session_id=self.session_id,
1134
+ workspace_id=self.workspace_id
1135
+ )
1136
+ except Exception as e:
1137
+ # 静默处理加载失败,不影响主流程
1138
+ self.stage_info = None
1139
+
1140
+ def _load_workspace_context(self):
1141
+ """加载工作空间上下文信息(goal和对话标题)"""
1142
+ if not self.session_id:
1143
+ return
1144
+
1145
+ try:
1146
+ # 导入工作空间管理器函数
1147
+ import sys
1148
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1149
+ if parent_dir not in sys.path:
1150
+ sys.path.insert(0, parent_dir)
1151
+ from workspace_manager import get_workspace_goal_for_session, get_session_title_for_session
1152
+
1153
+ # 获取工作空间goal
1154
+ self.workspace_goal = get_workspace_goal_for_session(self.session_id, self.project_path)
1155
+
1156
+ # 获取对话标题(优先从workspace.yml的sessions获取,如果没有再使用work_title)
1157
+ session_title = get_session_title_for_session(self.session_id, self.project_path)
1158
+ if session_title:
1159
+ self.dialog_title = session_title
1160
+ else:
1161
+ self.dialog_title = self.work_title
1162
+
1163
+ except Exception as e:
1164
+ # 静默处理加载失败,不影响主流程
1165
+ pass
1166
+ self.workspace_goal = None
1167
+ self.dialog_title = self.work_title
1168
+
1169
+ def _create_stage_buttons(self, layout):
1170
+ """创建阶段切换按钮"""
1171
+ if not self.stage_info:
1172
+ return
1173
+
1174
+ # 创建按钮容器
1175
+ stage_buttons_container = QWidget()
1176
+ stage_buttons_layout = QHBoxLayout(stage_buttons_container)
1177
+ stage_buttons_layout.setContentsMargins(5, 5, 5, 5)
1178
+ stage_buttons_layout.setSpacing(10)
1179
+
1180
+ # 创建上一阶段按钮
1181
+ if self.stage_info.get('prev_stage'):
1182
+ prev_stage = self.stage_info['prev_stage']
1183
+ # 截断过长的标题
1184
+ title = prev_stage.get('title', '')
1185
+ if len(title) > 10:
1186
+ title = title[:10] + "..."
1187
+ prev_btn = QPushButton(f"上一阶段: {title}")
1188
+ prev_btn.setToolTip(prev_stage.get('description', ''))
1189
+ prev_btn.setCursor(Qt.PointingHandCursor)
1190
+ prev_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) # 水平扩展
1191
+ prev_btn.setStyleSheet("""
1192
+ QPushButton {
1193
+ background-color: rgba(200, 200, 200, 25);
1194
+ color: #AAA;
1195
+ border: 1px solid rgba(200, 200, 200, 45);
1196
+ padding: 6px 12px;
1197
+ border-radius: 4px;
1198
+ font-size: 13px;
1199
+ text-align: center;
1200
+ min-width: 0px;
1201
+ }
1202
+ QPushButton:hover {
1203
+ background-color: rgba(200, 200, 200, 40);
1204
+ border: 1px solid #BBB;
1205
+ color: #888;
1206
+ }
1207
+ QPushButton:pressed {
1208
+ background-color: rgba(200, 200, 200, 55);
1209
+ }
1210
+ """)
1211
+ prev_btn.clicked.connect(lambda: self._on_stage_button_clicked("请进入上一阶段"))
1212
+ stage_buttons_layout.addWidget(prev_btn, 1) # 权重1,占50%
1213
+ else:
1214
+ # 如果没有上一阶段,添加一个占位空间
1215
+ stage_buttons_layout.addStretch(1)
1216
+
1217
+ # 创建下一阶段按钮
1218
+ if self.stage_info.get('next_stage'):
1219
+ next_stage = self.stage_info['next_stage']
1220
+ # 截断过长的标题
1221
+ title = next_stage.get('title', '')
1222
+ if len(title) > 10:
1223
+ title = title[:10] + "..."
1224
+ next_btn = QPushButton(f"下一阶段: {title}")
1225
+ next_btn.setToolTip(next_stage.get('description', ''))
1226
+ next_btn.setCursor(Qt.PointingHandCursor)
1227
+ next_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) # 水平扩展
1228
+ next_btn.setStyleSheet("""
1229
+ QPushButton {
1230
+ background-color: rgba(76, 175, 80, 30);
1231
+ color: #4CAF50;
1232
+ border: 1px solid rgba(76, 175, 80, 50);
1233
+ padding: 6px 12px;
1234
+ border-radius: 4px;
1235
+ font-size: 13px;
1236
+ text-align: center;
1237
+ min-width: 0px;
1238
+ }
1239
+ QPushButton:hover {
1240
+ background-color: rgba(76, 175, 80, 50);
1241
+ border: 1px solid #4CAF50;
1242
+ }
1243
+ QPushButton:pressed {
1244
+ background-color: rgba(76, 175, 80, 70);
1245
+ }
1246
+ """)
1247
+ next_btn.clicked.connect(lambda: self._on_stage_button_clicked("请进入下一阶段"))
1248
+ stage_buttons_layout.addWidget(next_btn, 1) # 权重1,占50%
1249
+ else:
1250
+ # 如果没有下一阶段,添加一个占位空间
1251
+ stage_buttons_layout.addStretch(1)
1252
+
1253
+ layout.addWidget(stage_buttons_container)
1254
+
1255
+ def _create_next_task_button(self, layout):
1256
+ """创建下一任务按钮(独立方法)"""
1257
+ if not self.next_task:
1258
+ return
1259
+
1260
+ next_task_title = self.next_task.get('title', '')
1261
+ # 如果标题过长,截断
1262
+ if len(next_task_title) > 20:
1263
+ next_task_title = next_task_title[:20] + "..."
1264
+
1265
+ next_task_btn = QPushButton(f"下一任务: {next_task_title}")
1266
+ next_task_btn.setCursor(Qt.PointingHandCursor)
1267
+ next_task_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
1268
+ next_task_btn.setStyleSheet("""
1269
+ QPushButton {
1270
+ background-color: rgba(76, 175, 80, 30);
1271
+ color: #4CAF50;
1272
+ border: 1px solid rgba(76, 175, 80, 50);
1273
+ padding: 6px 12px;
1274
+ border-radius: 4px;
1275
+ font-size: 13px;
1276
+ text-align: center;
1277
+ }
1278
+ QPushButton:hover {
1279
+ background-color: rgba(76, 175, 80, 50);
1280
+ border: 1px solid #4CAF50;
1281
+ }
1282
+ QPushButton:pressed {
1283
+ background-color: rgba(76, 175, 80, 70);
1284
+ }
1285
+ """)
1286
+ next_task_btn.clicked.connect(self._on_next_task_clicked)
1287
+ layout.addWidget(next_task_btn)
1288
+
1289
+ def _on_stage_button_clicked(self, message):
1290
+ """处理阶段切换按钮点击"""
1291
+ # 作为文本内容提交
1292
+ content_parts = [{
1293
+ "type": "text",
1294
+ "text": message
1295
+ }]
1296
+ self.feedback_submitted.emit(content_parts, [])
1297
+ # 关闭窗口(如果有父窗口)
1298
+ if self.parent() and hasattr(self.parent(), 'close'):
1299
+ self.parent().close()
1300
+
1301
+ def _load_deep_thinking_mode(self) -> bool:
1302
+ """从设置中加载深度思考模式状态"""
1303
+ from PySide6.QtCore import QSettings
1304
+
1305
+ # 优先尝试加载项目级设置
1306
+ if self.project_path:
1307
+ project_settings_file = os.path.join(self.project_path, '.feedback_settings.json')
1308
+ if os.path.exists(project_settings_file):
1309
+ try:
1310
+ with open(project_settings_file, 'r') as f:
1311
+ settings = json.load(f)
1312
+ return settings.get('deep_thinking_mode', False)
1313
+ except Exception:
1314
+ pass # 如果读取失败,使用全局设置
1315
+
1316
+ # 使用全局QSettings
1317
+ settings = QSettings("FeedbackUI", "ChatTab")
1318
+ return settings.value("deep_thinking_mode", False, type=bool)
1319
+
1320
+ def _save_deep_thinking_mode(self, enabled: bool):
1321
+ """保存深度思考模式状态到设置"""
1322
+ from PySide6.QtCore import QSettings
1323
+
1324
+ # 保存到项目级设置(如果有项目路径)
1325
+ if self.project_path:
1326
+ project_settings_file = os.path.join(self.project_path, '.feedback_settings.json')
1327
+ settings = {}
1328
+
1329
+ # 读取现有设置
1330
+ if os.path.exists(project_settings_file):
1331
+ try:
1332
+ with open(project_settings_file, 'r') as f:
1333
+ settings = json.load(f)
1334
+ except Exception:
1335
+ settings = {}
1336
+
1337
+ # 更新深度思考模式设置
1338
+ settings['deep_thinking_mode'] = enabled
1339
+
1340
+ # 保存回文件
1341
+ try:
1342
+ with open(project_settings_file, 'w') as f:
1343
+ json.dump(settings, f, indent=2)
1344
+ except Exception:
1345
+ pass # 如果保存失败,至少保存到全局设置
1346
+
1347
+ # 同时保存到全局QSettings
1348
+ settings = QSettings("FeedbackUI", "ChatTab")
1349
+ settings.setValue("deep_thinking_mode", enabled)
1350
+
1351
+ def get_history_file_path(self) -> Optional[str]:
1352
+ """获取历史记录文件路径"""
1353
+ # 如果没有session_id,返回None
1354
+ if not self.session_id:
1355
+ return None
1356
+
1357
+ if self.project_path:
1358
+ return os.path.join(self.project_path, '.workspace', 'chat_history', f'{self.session_id}.json')
1359
+ else:
1360
+ # 如果没有项目路径,使用脚本目录
1361
+ script_dir = os.path.dirname(os.path.abspath(__file__))
1362
+ return os.path.join(script_dir, '..', '.workspace', 'chat_history', f'{self.session_id}.json')
1363
+
1364
+ def save_response_to_history(self, response: str) -> bool:
1365
+ """保存AI回复到当前对话历史(新格式)
1366
+
1367
+ Args:
1368
+ response: AI的回复内容
1369
+
1370
+ Returns:
1371
+ bool: 保存是否成功
1372
+ """
1373
+ if not response.strip():
1374
+ return False
1375
+
1376
+ try:
1377
+ print(f"[DEBUG save_response_to_history] project_path={self.project_path}", file=sys.stderr)
1378
+ print(f"[DEBUG save_response_to_history] session_id={self.session_id}", file=sys.stderr)
1379
+
1380
+ # 写入调试日志文件
1381
+ debug_log_path = "/Users/yang/workspace/interactive-feedback-mcp/.workspace/debug_save_response.log"
1382
+ with open(debug_log_path, 'a', encoding='utf-8') as debug_f:
1383
+ debug_f.write(f"\n=== {datetime.now().isoformat()} ===\n")
1384
+ debug_f.write(f"project_path={self.project_path}\n")
1385
+ debug_f.write(f"session_id={self.session_id}\n")
1386
+
1387
+ # 获取历史记录文件路径
1388
+ history_file = self.get_history_file_path()
1389
+ print(f"[DEBUG save_response_to_history] history_file={history_file}", file=sys.stderr)
1390
+
1391
+ # 追加写入历史文件路径
1392
+ with open(debug_log_path, 'a', encoding='utf-8') as debug_f:
1393
+ debug_f.write(f"history_file={history_file}\n")
1394
+
1395
+ # 如果没有session_id,静默跳过
1396
+ if not history_file:
1397
+ return False
1398
+
1399
+ # 读取现有数据
1400
+ existing_data = {}
1401
+ if os.path.exists(history_file):
1402
+ try:
1403
+ with open(history_file, 'r', encoding='utf-8') as f:
1404
+ existing_data = json.load(f)
1405
+ print(f"[DEBUG] existing_data type: {type(existing_data)}", file=sys.stderr)
1406
+ if isinstance(existing_data, dict):
1407
+ print(f"[DEBUG] dialogues count: {len(existing_data.get('dialogues', []))}", file=sys.stderr)
1408
+ agents = [d for d in existing_data.get('dialogues', []) if d.get('role') == 'agent']
1409
+ print(f"[DEBUG] agent records: {len(agents)}", file=sys.stderr)
1410
+
1411
+ # 写入调试日志
1412
+ with open(debug_log_path, 'a', encoding='utf-8') as debug_f:
1413
+ debug_f.write(f"existing_data type: {type(existing_data)}\n")
1414
+ if isinstance(existing_data, dict):
1415
+ debug_f.write(f"dialogues count: {len(existing_data.get('dialogues', []))}\n")
1416
+ agents = [d for d in existing_data.get('dialogues', []) if d.get('role') == 'agent']
1417
+ debug_f.write(f"agent records loaded: {len(agents)}\n")
1418
+ # 兼容旧格式:如果是数组,转换为新格式
1419
+ if isinstance(existing_data, list):
1420
+ dialogues = []
1421
+ for record in existing_data:
1422
+ if isinstance(record, dict):
1423
+ # 跳过 stop_hook_status 类型
1424
+ if record.get('type') == 'stop_hook_status':
1425
+ continue
1426
+
1427
+ # 保留 agent 记录(直接添加)
1428
+ if record.get('role') == 'agent':
1429
+ dialogues.append(record)
1430
+ continue
1431
+
1432
+ # 转换普通消息为对话格式
1433
+ dialogue = {
1434
+ 'timestamp': record.get('timestamp', ''),
1435
+ 'time_display': record.get('time_display', ''),
1436
+ 'messages': record.get('messages', []) if 'messages' in record else [{
1437
+ 'role': 'user',
1438
+ 'content': record.get('content', ''),
1439
+ 'time': record.get('time_display', '').split(' ')[-1] if 'time_display' in record else ''
1440
+ }]
1441
+ }
1442
+ dialogues.append(dialogue)
1443
+ existing_data = {'dialogues': dialogues}
1444
+ except (json.JSONDecodeError, IOError):
1445
+ existing_data = {}
1446
+
1447
+ # 确保有dialogues数组
1448
+ if 'dialogues' not in existing_data:
1449
+ existing_data['dialogues'] = []
1450
+
1451
+ dialogues = existing_data['dialogues']
1452
+
1453
+ # 查找最后一个有messages字段的对话记录(跳过agent记录)
1454
+ last_dialogue_index = -1
1455
+ for i in range(len(dialogues) - 1, -1, -1):
1456
+ if 'messages' in dialogues[i]:
1457
+ last_dialogue_index = i
1458
+ break
1459
+
1460
+ if last_dialogue_index >= 0:
1461
+ # 在最后一个对话记录中添加AI回复
1462
+ dialogues[last_dialogue_index]['messages'].append({
1463
+ 'role': 'assistant',
1464
+ 'content': response.strip(),
1465
+ 'time': datetime.now().strftime('%H:%M:%S')
1466
+ })
1467
+ else:
1468
+ # 没有找到对话记录,创建新的对话记录(仅包含AI回复)
1469
+ new_record = {
1470
+ 'timestamp': datetime.now().isoformat(),
1471
+ 'time_display': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
1472
+ 'messages': [{
1473
+ 'role': 'assistant',
1474
+ 'content': response.strip(),
1475
+ 'time': datetime.now().strftime('%H:%M:%S')
1476
+ }]
1477
+ }
1478
+ dialogues.append(new_record)
1479
+
1480
+ # 保存到文件
1481
+ agents_after = [d for d in existing_data.get('dialogues', []) if d.get('role') == 'agent']
1482
+ print(f"[DEBUG] agent records before save: {len(agents_after)}", file=sys.stderr)
1483
+
1484
+ # 写入调试日志
1485
+ with open(debug_log_path, 'a', encoding='utf-8') as debug_f:
1486
+ agents_after = [d for d in existing_data.get('dialogues', []) if d.get('role') == 'agent']
1487
+ debug_f.write(f"agent records before save: {len(agents_after)}\n")
1488
+
1489
+ os.makedirs(os.path.dirname(history_file), exist_ok=True)
1490
+ with open(history_file, 'w', encoding='utf-8') as f:
1491
+ json.dump(existing_data, f, ensure_ascii=False, indent=2)
1492
+
1493
+ return True
1494
+
1495
+ except Exception:
1496
+ # 静默处理保存失败,不影响主流程
1497
+ return False
1498
+
1499
+ def load_history_from_file(self) -> List[Dict]:
1500
+ """从文件加载历史记录(兼容旧格式)"""
1501
+ try:
1502
+ history_file = self.get_history_file_path()
1503
+
1504
+ # 如果没有session_id,静默跳过
1505
+ if not history_file:
1506
+ return []
1507
+
1508
+ if os.path.exists(history_file):
1509
+ with open(history_file, 'r', encoding='utf-8') as f:
1510
+ data = json.load(f)
1511
+
1512
+ # 新格式:{'dialogues': [...], 'control': {...}}
1513
+ if isinstance(data, dict) and 'dialogues' in data:
1514
+ # 直接返回dialogues数组(不含type字段)
1515
+ return data.get('dialogues', [])
1516
+
1517
+ # 旧格式数组处理
1518
+ if isinstance(data, list):
1519
+ # 兼容旧格式:将旧格式转换为新的对话格式
1520
+ converted_history = []
1521
+ for record in data:
1522
+ if isinstance(record, dict):
1523
+ # 跳过control类型的记录
1524
+ if record.get('type') == 'stop_hook_status':
1525
+ continue
1526
+
1527
+ # 保留 agent 记录(直接添加)
1528
+ if record.get('role') == 'agent':
1529
+ converted_history.append(record)
1530
+ continue
1531
+
1532
+ # 转换普通消息为对话格式
1533
+ if 'type' not in record or record.get('type') != 'dialogue':
1534
+ # 旧格式单条消息 - 转换为对话格式(不含type)
1535
+ converted_record = {
1536
+ 'timestamp': record.get('timestamp', ''),
1537
+ 'time_display': record.get('time_display', ''),
1538
+ 'messages': [{
1539
+ 'role': 'user',
1540
+ 'content': record.get('content', ''),
1541
+ 'time': record.get('time_display', '').split(' ')[-1] if 'time_display' in record else ''
1542
+ }]
1543
+ }
1544
+ converted_history.append(converted_record)
1545
+ else:
1546
+ # 已经是对话格式 - 移除type字段
1547
+ dialogue = {
1548
+ 'timestamp': record.get('timestamp', ''),
1549
+ 'time_display': record.get('time_display', ''),
1550
+ 'messages': record.get('messages', [])
1551
+ }
1552
+ converted_history.append(dialogue)
1553
+ return converted_history
1554
+
1555
+ return []
1556
+ return []
1557
+ except Exception:
1558
+ # 静默处理加载失败,不影响主流程
1559
+ return []
1560
+
1561
+ def get_recent_history(self, count: Optional[int] = None) -> List[Dict]:
1562
+ """获取最近的历史记录
1563
+
1564
+ Args:
1565
+ count: 获取记录数量,如果为None则返回所有记录
1566
+
1567
+ Returns:
1568
+ List[Dict]: 历史记录列表
1569
+ """
1570
+ history = self.load_history_from_file()
1571
+ if count is None:
1572
+ return history # 返回所有历史记录
1573
+ return history[-count:]
1574
+
1575
+ def save_input_to_history(self):
1576
+ """保存输入框内容到草稿(用于超时或关闭时自动保存)"""
1577
+ if not self.feedback_text or not self.history_manager:
1578
+ return
1579
+
1580
+ text_content = self.feedback_text.toPlainText().strip()
1581
+ if text_content:
1582
+ self.history_manager.save_draft(text_content)
1583
+
1584
+ def clear_feedback(self):
1585
+ """清空反馈内容"""
1586
+ if self.feedback_text:
1587
+ self.feedback_text.clear()
1588
+ if hasattr(self.feedback_text, 'clear_images'):
1589
+ self.feedback_text.clear_images()
1590
+
1591
+ # 清空选项
1592
+ for checkbox in self.option_checkboxes:
1593
+ checkbox.setChecked(False)
1594
+
1595
+ # 清空选中的指令
1596
+ self._clear_selected_command()
1597
+
1598
+ def _get_configured_ide_or_prompt(self):
1599
+ """获取配置的IDE,如果未配置则弹出设置对话框,返回IDE名称或None"""
1600
+ try:
1601
+ from feedback_config import FeedbackConfig
1602
+
1603
+ # 获取项目路径
1604
+ project_path = self.project_path if hasattr(self, 'project_path') else None
1605
+ if not project_path and hasattr(self, 'parent'):
1606
+ main_window = self.parent()
1607
+ while main_window and not hasattr(main_window, 'project_path'):
1608
+ main_window = main_window.parent()
1609
+ if main_window:
1610
+ project_path = main_window.project_path
1611
+
1612
+ if not project_path:
1613
+ return None
1614
+
1615
+ config_manager = FeedbackConfig(project_path)
1616
+ ide = config_manager.get_ide() or os.getenv('IDE')
1617
+
1618
+ if not ide:
1619
+ # 弹出设置IDE对话框
1620
+ reply = QMessageBox.question(
1621
+ self,
1622
+ "未配置IDE",
1623
+ "尚未配置默认IDE,是否现在设置?",
1624
+ QMessageBox.Yes | QMessageBox.No
1625
+ )
1626
+ if reply == QMessageBox.Yes:
1627
+ # 获取主窗口并调用设置对话框
1628
+ main_window = self.parent()
1629
+ while main_window and not hasattr(main_window, '_show_ide_settings_dialog'):
1630
+ main_window = main_window.parent()
1631
+ if main_window:
1632
+ main_window._show_ide_settings_dialog()
1633
+ # 重新获取配置
1634
+ ide = config_manager.get_ide()
1635
+
1636
+ return ide
1637
+ except Exception as e:
1638
+ print(f"获取IDE配置失败: {e}")
1639
+ return None
1640
+
1641
+ def _edit_selected_command(self):
1642
+ """在IDE中打开选中的指令文件"""
1643
+ if not self.selected_command:
1644
+ return
1645
+
1646
+ # 优先使用保存的文件路径
1647
+ file_path = self.selected_command.get('full_path', '')
1648
+
1649
+ # 如果没有保存路径,则尝试查找
1650
+ if not file_path:
1651
+ command_data = {
1652
+ 'title': self.selected_command['title'],
1653
+ 'content': self.selected_command['content'],
1654
+ 'type': self.selected_command['type'],
1655
+ }
1656
+ file_path = self._find_command_file_path(command_data)
1657
+
1658
+ if not file_path or not os.path.exists(file_path):
1659
+ QMessageBox.warning(self, "打开失败", f"无法找到指令文件\n标题: {self.selected_command.get('title')}\n路径: {file_path or '未找到'}")
1660
+ return
1661
+
1662
+ # 使用IDE打开文件
1663
+ try:
1664
+ # 导入ide_utils模块
1665
+ import sys
1666
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1667
+ if parent_dir not in sys.path:
1668
+ sys.path.insert(0, parent_dir)
1669
+ from ide_utils import open_project_with_ide
1670
+
1671
+ # 使用统一的IDE获取方法
1672
+ ide_name = self._get_configured_ide_or_prompt()
1673
+
1674
+ if not ide_name:
1675
+ # 用户取消了设置IDE或无法获取配置
1676
+ return
1677
+
1678
+ # 使用IDE打开文件
1679
+ success = open_project_with_ide(file_path, ide_name)
1680
+
1681
+ if not success:
1682
+ # 如果IDE打开失败,提示用户重新配置IDE
1683
+ reply = QMessageBox.question(
1684
+ self,
1685
+ "IDE打开失败",
1686
+ f"无法使用 '{ide_name}' 打开文件。\n\n可能的原因:\n1. IDE未正确安装\n2. IDE路径配置错误\n3. IDE不支持打开此类型文件\n\n是否重新设置IDE?",
1687
+ QMessageBox.Yes | QMessageBox.No,
1688
+ QMessageBox.Yes
1689
+ )
1690
+
1691
+ if reply == QMessageBox.Yes:
1692
+ # 获取主窗口并调用设置对话框
1693
+ main_window = self.parent()
1694
+ while main_window and not hasattr(main_window, '_show_ide_settings_dialog'):
1695
+ main_window = main_window.parent()
1696
+ if main_window:
1697
+ main_window._show_ide_settings_dialog()
1698
+
1699
+ except Exception as e:
1700
+ QMessageBox.critical(self, "打开失败", f"无法打开指令文件: {str(e)}")
1701
+
1702
+ def _find_command_file_path(self, command_data):
1703
+ """查找指令文件路径"""
1704
+ import os
1705
+
1706
+ # 获取项目路径
1707
+ project_path = self.project_path if hasattr(self, 'project_path') else None
1708
+ if not project_path:
1709
+ # 从父窗口获取
1710
+ main_window = self.parent()
1711
+ while main_window and not hasattr(main_window, 'project_path'):
1712
+ main_window = main_window.parent()
1713
+ if main_window:
1714
+ project_path = main_window.project_path
1715
+
1716
+ if not project_path:
1717
+ return None
1718
+
1719
+ title = command_data['title']
1720
+ if title.endswith('.md'):
1721
+ title = title[:-3]
1722
+
1723
+ # 根据指令类型确定搜索目录
1724
+ if command_data['type'] == 'project':
1725
+ search_dirs = [
1726
+ os.path.join(project_path, ".claude", "commands"),
1727
+ os.path.join(project_path, "_agent-local", "prompts"),
1728
+ os.path.join(project_path, ".cursor", "rules")
1729
+ ]
1730
+ elif command_data['type'] == 'personal':
1731
+ search_dirs = [
1732
+ os.path.join(project_path, "prompts"),
1733
+ os.path.expanduser("~/.claude/commands")
1734
+ ]
1735
+ else: # system
1736
+ search_dirs = [
1737
+ os.path.join(project_path, ".claude", "commands"),
1738
+ os.path.join(project_path, "src-min")
1739
+ ]
1740
+
1741
+ # 在各个目录中搜索文件
1742
+ for search_dir in search_dirs:
1743
+ if not os.path.exists(search_dir):
1744
+ continue
1745
+
1746
+ # 尝试不同的文件扩展名
1747
+ for ext in ['.md', '.mdc', '.txt']:
1748
+ file_path = os.path.join(search_dir, f"{title}{ext}")
1749
+ if os.path.exists(file_path):
1750
+ return file_path
1751
+
1752
+ # 递归搜索子目录
1753
+ try:
1754
+ for root, dirs, files in os.walk(search_dir):
1755
+ for file in files:
1756
+ if file.startswith(title) and file.endswith(('.md', '.mdc', '.txt')):
1757
+ return os.path.join(root, file)
1758
+ except Exception:
1759
+ pass
1760
+
1761
+ return None
1762
+
1763
+ def _load_task_info(self):
1764
+ """加载任务信息"""
1765
+ if not self.session_id:
1766
+ return
1767
+
1768
+ try:
1769
+ if not self.project_path:
1770
+ return
1771
+
1772
+ # 构建任务文件路径
1773
+ task_file = os.path.join(self.project_path, '.workspace', 'tasks', f'{self.session_id}.json')
1774
+ if not os.path.exists(task_file):
1775
+ return
1776
+
1777
+ # 读取任务文件
1778
+ with open(task_file, 'r', encoding='utf-8') as f:
1779
+ data = json.load(f)
1780
+ tasks = data.get('tasks', [])
1781
+
1782
+ # 查找当前任务(state == "in_progress")
1783
+ for task in tasks:
1784
+ if task.get('state') == 'in_progress':
1785
+ self.current_task = {
1786
+ 'id': task.get('id'),
1787
+ 'title': task.get('title', ''),
1788
+ 'state': task.get('state')
1789
+ }
1790
+ break
1791
+
1792
+ # 查找下一个任务(state == "pending")
1793
+ for task in tasks:
1794
+ if task.get('state') == 'pending':
1795
+ self.next_task = {
1796
+ 'id': task.get('id'),
1797
+ 'title': task.get('title', ''),
1798
+ 'state': task.get('state')
1799
+ }
1800
+ break
1801
+
1802
+ except Exception:
1803
+ # 静默处理加载失败,不影响主流程
1804
+ pass
1805
+
1806
+ def _create_current_task_label(self, layout):
1807
+ """创建当前任务显示标签"""
1808
+ if not self.current_task:
1809
+ return
1810
+
1811
+ task_title = self.current_task.get('title', '')
1812
+ task_label = QLabel(f"📌 当前任务: {task_title}")
1813
+ task_label.setWordWrap(True)
1814
+ task_label.setAlignment(Qt.AlignCenter)
1815
+ task_label.setStyleSheet("""
1816
+ QLabel {
1817
+ font-size: 13px;
1818
+ font-weight: bold;
1819
+ color: #FF8C00;
1820
+ padding: 6px;
1821
+ background-color: rgba(255, 140, 0, 10);
1822
+ border: 1px solid rgba(255, 140, 0, 30);
1823
+ border-radius: 4px;
1824
+ margin: 5px 0px;
1825
+ }
1826
+ """)
1827
+ layout.addWidget(task_label)
1828
+
1829
+ def _on_next_task_clicked(self):
1830
+ """处理下一任务按钮点击"""
1831
+ content_parts = [{
1832
+ "type": "text",
1833
+ "text": "请开始任务列表中的下一个任务"
1834
+ }]
1835
+ self.feedback_submitted.emit(content_parts, [])
1836
+ # 关闭窗口(如果有父窗口)
1837
+ if self.parent() and hasattr(self.parent(), 'close'):
1838
+ self.parent().close()
1839
+
1840
+ def _load_agent_tags(self):
1841
+ """加载并显示 agent 标签(垂直排列,每个标签一行)"""
1842
+ # 清空现有标签
1843
+ while self.agent_tags_layout.count():
1844
+ child = self.agent_tags_layout.takeAt(0)
1845
+ if child.widget():
1846
+ child.widget().deleteLater()
1847
+
1848
+ # 获取 agent 记录
1849
+ if not self.history_manager:
1850
+ self.agent_tags_container.hide()
1851
+ return
1852
+
1853
+ agent_records = self.history_manager.get_agent_records_after_last_user()
1854
+
1855
+ if not agent_records:
1856
+ self.agent_tags_container.hide()
1857
+ return
1858
+
1859
+ # 为每个 agent 创建标签
1860
+ for record in agent_records:
1861
+ tag = self._create_agent_tag(record)
1862
+ self.agent_tags_layout.addWidget(tag)
1863
+
1864
+ self.agent_tags_container.show()
1865
+
1866
+ def _create_agent_tag(self, record: Dict) -> QPushButton:
1867
+ """创建 agent 标签按钮(100%宽度,不截断文本)"""
1868
+ subagent_type = record.get('subagent_type', 'unknown')
1869
+ description = record.get('description', '')
1870
+ label = f"{subagent_type}:{description}" if description else subagent_type
1871
+
1872
+ # 完整显示标签,不截断
1873
+ tag = QPushButton(label)
1874
+ tag.setToolTip(f"点击查看详情: {label}")
1875
+ tag.setCursor(Qt.PointingHandCursor)
1876
+ # 设置宽度自动扩展到100%
1877
+ tag.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
1878
+ tag.setStyleSheet("""
1879
+ QPushButton {
1880
+ background-color: rgba(76, 175, 80, 20);
1881
+ color: #4CAF50;
1882
+ border: 1px solid rgba(76, 175, 80, 40);
1883
+ padding: 6px 12px;
1884
+ border-radius: 4px;
1885
+ font-size: 11px;
1886
+ font-weight: 500;
1887
+ text-align: left;
1888
+ }
1889
+ QPushButton:hover {
1890
+ background-color: rgba(76, 175, 80, 40);
1891
+ border: 1px solid #4CAF50;
1892
+ }
1893
+ QPushButton:pressed {
1894
+ background-color: rgba(76, 175, 80, 60);
1895
+ }
1896
+ """)
1897
+ tag.clicked.connect(lambda checked, r=record: self._show_agent_popup(r))
1898
+ return tag
1899
+
1900
+ def _show_agent_popup(self, record: Dict):
1901
+ """显示 agent 内容弹窗"""
1902
+ try:
1903
+ from components.agent_popup import AgentPopup
1904
+ except ImportError:
1905
+ try:
1906
+ import sys
1907
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1908
+ if parent_dir not in sys.path:
1909
+ sys.path.insert(0, parent_dir)
1910
+ from components.agent_popup import AgentPopup
1911
+ except Exception:
1912
+ return
1913
+
1914
+ popup = AgentPopup(self)
1915
+ popup.set_agent_data(record)
1916
+
1917
+ # 计算弹窗位置
1918
+ tag_pos = self.agent_tags_container.mapToGlobal(
1919
+ self.agent_tags_container.rect().bottomLeft()
1920
+ )
1921
+ popup.show_at_position(QPoint(tag_pos.x(), tag_pos.y() + 5))
1922
+
1923
+ def _restore_draft(self):
1924
+ """恢复草稿内容到输入框"""
1925
+ if not self.history_manager or not self.feedback_text:
1926
+ return
1927
+
1928
+ draft = self.history_manager.get_latest_draft()
1929
+ if draft and draft.get('text'):
1930
+ self.feedback_text.setPlainText(draft['text'])
1931
+ self.history_manager.clear_draft()