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
components/file_popup.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""
|
|
2
|
+
文件选择弹窗组件 - 用于选择项目文件
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import fnmatch
|
|
7
|
+
from typing import List, Dict, Any, Set
|
|
8
|
+
from PySide6.QtWidgets import (
|
|
9
|
+
QFrame, QVBoxLayout, QScrollArea, QWidget, QGridLayout, QPushButton, QLabel
|
|
10
|
+
)
|
|
11
|
+
from PySide6.QtCore import Qt, Signal, QPoint
|
|
12
|
+
from PySide6.QtGui import QKeyEvent, QFont
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FilePopup(QFrame):
|
|
16
|
+
"""文件选择弹窗组件"""
|
|
17
|
+
|
|
18
|
+
# 信号定义
|
|
19
|
+
file_selected = Signal(str) # 选中文件路径
|
|
20
|
+
popup_closed = Signal() # 弹窗关闭
|
|
21
|
+
|
|
22
|
+
# 默认排除的目录
|
|
23
|
+
DEFAULT_EXCLUDED = {'.git', 'node_modules', '__pycache__', '.venv', 'venv',
|
|
24
|
+
'dist', 'build', '.idea', '.vscode', '.workspace'}
|
|
25
|
+
|
|
26
|
+
def __init__(self, parent=None):
|
|
27
|
+
super().__init__(parent)
|
|
28
|
+
self.files = [] # 存储文件数据
|
|
29
|
+
self.filtered_files = [] # 过滤后的文件
|
|
30
|
+
self.filter_text = "" # 过滤文本
|
|
31
|
+
self.project_dir = "" # 项目目录
|
|
32
|
+
|
|
33
|
+
# 导航相关属性
|
|
34
|
+
self.current_index = -1
|
|
35
|
+
self.file_buttons = []
|
|
36
|
+
|
|
37
|
+
# 分页相关
|
|
38
|
+
self.page_size = 50
|
|
39
|
+
self.current_page = 0
|
|
40
|
+
|
|
41
|
+
self._setup_ui()
|
|
42
|
+
self._setup_style()
|
|
43
|
+
|
|
44
|
+
# 设置窗口属性
|
|
45
|
+
self.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
|
46
|
+
self.setAttribute(Qt.WA_ShowWithoutActivating)
|
|
47
|
+
|
|
48
|
+
def _setup_ui(self):
|
|
49
|
+
"""设置UI"""
|
|
50
|
+
layout = QVBoxLayout(self)
|
|
51
|
+
layout.setContentsMargins(2, 2, 2, 2)
|
|
52
|
+
layout.setSpacing(0)
|
|
53
|
+
|
|
54
|
+
# 标题
|
|
55
|
+
self.title_label = QLabel("📁 选择文件")
|
|
56
|
+
self.title_label.setAlignment(Qt.AlignCenter)
|
|
57
|
+
font = QFont()
|
|
58
|
+
font.setPointSize(10)
|
|
59
|
+
font.setBold(True)
|
|
60
|
+
self.title_label.setFont(font)
|
|
61
|
+
layout.addWidget(self.title_label)
|
|
62
|
+
|
|
63
|
+
# 文件列表容器
|
|
64
|
+
self.scroll_area = QScrollArea()
|
|
65
|
+
self.scroll_area.setMaximumHeight(500)
|
|
66
|
+
self.scroll_area.setMinimumHeight(100)
|
|
67
|
+
self.scroll_area.setMinimumWidth(450)
|
|
68
|
+
self.scroll_area.setWidgetResizable(True)
|
|
69
|
+
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
70
|
+
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
71
|
+
|
|
72
|
+
# 网格容器
|
|
73
|
+
self.grid_widget = QWidget()
|
|
74
|
+
self.grid_layout = QGridLayout(self.grid_widget)
|
|
75
|
+
self.grid_layout.setSpacing(4)
|
|
76
|
+
self.grid_layout.setContentsMargins(4, 4, 4, 4)
|
|
77
|
+
|
|
78
|
+
self.scroll_area.setWidget(self.grid_widget)
|
|
79
|
+
layout.addWidget(self.scroll_area)
|
|
80
|
+
|
|
81
|
+
# 提示标签
|
|
82
|
+
self.hint_label = QLabel("↑↓ 方向键选择 | Enter 确认 | Esc 取消")
|
|
83
|
+
self.hint_label.setAlignment(Qt.AlignCenter)
|
|
84
|
+
hint_font = QFont()
|
|
85
|
+
hint_font.setPointSize(8)
|
|
86
|
+
self.hint_label.setFont(hint_font)
|
|
87
|
+
layout.addWidget(self.hint_label)
|
|
88
|
+
|
|
89
|
+
def _setup_style(self):
|
|
90
|
+
"""设置样式"""
|
|
91
|
+
self.setStyleSheet("""
|
|
92
|
+
FilePopup {
|
|
93
|
+
background-color: #2b2b2b;
|
|
94
|
+
border: 1px solid #555555;
|
|
95
|
+
border-radius: 4px;
|
|
96
|
+
}
|
|
97
|
+
QLabel {
|
|
98
|
+
color: #ffffff;
|
|
99
|
+
padding: 4px;
|
|
100
|
+
}
|
|
101
|
+
QScrollArea {
|
|
102
|
+
background-color: #2b2b2b;
|
|
103
|
+
border: none;
|
|
104
|
+
}
|
|
105
|
+
QPushButton {
|
|
106
|
+
background-color: #2b2b2b;
|
|
107
|
+
border: 1px solid #3a3a3a;
|
|
108
|
+
border-radius: 4px;
|
|
109
|
+
color: #cccccc;
|
|
110
|
+
padding: 4px 8px;
|
|
111
|
+
text-align: left;
|
|
112
|
+
min-height: 16px;
|
|
113
|
+
max-height: 24px;
|
|
114
|
+
font-size: 11px;
|
|
115
|
+
}
|
|
116
|
+
QPushButton:hover {
|
|
117
|
+
background-color: #353535;
|
|
118
|
+
border-color: #4a4a4a;
|
|
119
|
+
}
|
|
120
|
+
QPushButton:focus {
|
|
121
|
+
background-color: #0078d4;
|
|
122
|
+
border-color: #0078d4;
|
|
123
|
+
color: white;
|
|
124
|
+
}
|
|
125
|
+
""")
|
|
126
|
+
|
|
127
|
+
def set_project_dir(self, project_dir: str):
|
|
128
|
+
"""设置项目目录并扫描文件"""
|
|
129
|
+
self.project_dir = project_dir
|
|
130
|
+
self._load_gitignore()
|
|
131
|
+
self._scan_files()
|
|
132
|
+
|
|
133
|
+
def _load_gitignore(self):
|
|
134
|
+
"""加载 .gitignore 规则"""
|
|
135
|
+
self.gitignore_patterns = []
|
|
136
|
+
if not self.project_dir:
|
|
137
|
+
return
|
|
138
|
+
gitignore_path = os.path.join(self.project_dir, '.gitignore')
|
|
139
|
+
if os.path.exists(gitignore_path):
|
|
140
|
+
try:
|
|
141
|
+
with open(gitignore_path, 'r', encoding='utf-8') as f:
|
|
142
|
+
for line in f:
|
|
143
|
+
line = line.strip()
|
|
144
|
+
if line and not line.startswith('#'):
|
|
145
|
+
self.gitignore_patterns.append(line)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
def _is_ignored(self, rel_path: str, is_dir: bool) -> bool:
|
|
150
|
+
"""检查路径是否被 gitignore 忽略"""
|
|
151
|
+
# 检查默认排除
|
|
152
|
+
parts = rel_path.split(os.sep)
|
|
153
|
+
for part in parts:
|
|
154
|
+
if part in self.DEFAULT_EXCLUDED:
|
|
155
|
+
return True
|
|
156
|
+
# 检查 gitignore 规则
|
|
157
|
+
for pattern in self.gitignore_patterns:
|
|
158
|
+
# 处理目录模式
|
|
159
|
+
if pattern.endswith('/'):
|
|
160
|
+
if is_dir and fnmatch.fnmatch(rel_path, pattern[:-1]):
|
|
161
|
+
return True
|
|
162
|
+
if fnmatch.fnmatch(rel_path + '/', '*' + pattern):
|
|
163
|
+
return True
|
|
164
|
+
# 通用匹配
|
|
165
|
+
if fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(rel_path, '*/' + pattern):
|
|
166
|
+
return True
|
|
167
|
+
if fnmatch.fnmatch(os.path.basename(rel_path), pattern):
|
|
168
|
+
return True
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
def _scan_files(self):
|
|
172
|
+
"""扫描项目目录中的文件"""
|
|
173
|
+
self.files = []
|
|
174
|
+
if not self.project_dir or not os.path.exists(self.project_dir):
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
for root, dirs, files in os.walk(self.project_dir):
|
|
178
|
+
rel_root = os.path.relpath(root, self.project_dir)
|
|
179
|
+
if rel_root == '.':
|
|
180
|
+
rel_root = ''
|
|
181
|
+
|
|
182
|
+
# 排除目录
|
|
183
|
+
dirs[:] = [d for d in dirs if not self._is_ignored(
|
|
184
|
+
os.path.join(rel_root, d) if rel_root else d, True)]
|
|
185
|
+
|
|
186
|
+
# 添加目录
|
|
187
|
+
for dir_name in dirs:
|
|
188
|
+
rel_path = os.path.join(rel_root, dir_name) if rel_root else dir_name
|
|
189
|
+
self.files.append({
|
|
190
|
+
"path": os.path.join(root, dir_name),
|
|
191
|
+
"name": dir_name,
|
|
192
|
+
"rel_path": rel_path,
|
|
193
|
+
"is_dir": True
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
# 添加文件
|
|
197
|
+
for file_name in files:
|
|
198
|
+
rel_path = os.path.join(rel_root, file_name) if rel_root else file_name
|
|
199
|
+
if not self._is_ignored(rel_path, False):
|
|
200
|
+
self.files.append({
|
|
201
|
+
"path": os.path.join(root, file_name),
|
|
202
|
+
"name": file_name,
|
|
203
|
+
"rel_path": rel_path,
|
|
204
|
+
"is_dir": False
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
# 按路径排序
|
|
208
|
+
self.files.sort(key=lambda x: x["rel_path"])
|
|
209
|
+
self._update_filtered_files()
|
|
210
|
+
|
|
211
|
+
def set_filter(self, filter_text: str):
|
|
212
|
+
"""设置过滤文本"""
|
|
213
|
+
self.filter_text = filter_text.lower()
|
|
214
|
+
self.current_page = 0
|
|
215
|
+
self._update_filtered_files()
|
|
216
|
+
|
|
217
|
+
def _calc_match_score(self, file_info: Dict[str, Any], keyword: str) -> int:
|
|
218
|
+
"""计算匹配度分数(分数越小优先级越高)"""
|
|
219
|
+
name = file_info["name"].lower()
|
|
220
|
+
rel_path = file_info["rel_path"].lower()
|
|
221
|
+
is_dir = file_info["is_dir"]
|
|
222
|
+
|
|
223
|
+
# 基础分数(文件夹优先)
|
|
224
|
+
base = 0 if is_dir else 1000
|
|
225
|
+
|
|
226
|
+
# 名称完全匹配
|
|
227
|
+
if name == keyword or name.rstrip("/\\") == keyword:
|
|
228
|
+
return base + 0
|
|
229
|
+
# 名称开头匹配
|
|
230
|
+
if name.startswith(keyword):
|
|
231
|
+
return base + 100
|
|
232
|
+
# 名称包含匹配
|
|
233
|
+
if keyword in name:
|
|
234
|
+
return base + 200
|
|
235
|
+
# 路径包含匹配(按位置,越靠左优先级越高)
|
|
236
|
+
pos = rel_path.find(keyword)
|
|
237
|
+
if pos >= 0:
|
|
238
|
+
return base + 300 + pos
|
|
239
|
+
return base + 10000
|
|
240
|
+
|
|
241
|
+
def _update_filtered_files(self):
|
|
242
|
+
"""更新过滤后的文件列表"""
|
|
243
|
+
if self.filter_text:
|
|
244
|
+
# 过滤匹配的文件
|
|
245
|
+
matched = [
|
|
246
|
+
f for f in self.files
|
|
247
|
+
if self.filter_text in f["name"].lower() or
|
|
248
|
+
self.filter_text in f["rel_path"].lower()
|
|
249
|
+
]
|
|
250
|
+
# 按匹配度排序
|
|
251
|
+
self.filtered_files = sorted(
|
|
252
|
+
matched,
|
|
253
|
+
key=lambda f: (self._calc_match_score(f, self.filter_text), len(f["rel_path"]), f["rel_path"])
|
|
254
|
+
)
|
|
255
|
+
else:
|
|
256
|
+
self.filtered_files = self.files.copy()
|
|
257
|
+
|
|
258
|
+
self._update_list_widget()
|
|
259
|
+
|
|
260
|
+
def _update_list_widget(self):
|
|
261
|
+
"""更新文件列表显示"""
|
|
262
|
+
# 清空现有按钮
|
|
263
|
+
for button in self.file_buttons:
|
|
264
|
+
button.deleteLater()
|
|
265
|
+
self.file_buttons.clear()
|
|
266
|
+
|
|
267
|
+
# 清空布局
|
|
268
|
+
while self.grid_layout.count():
|
|
269
|
+
child = self.grid_layout.takeAt(0)
|
|
270
|
+
if child.widget():
|
|
271
|
+
child.widget().deleteLater()
|
|
272
|
+
|
|
273
|
+
if not self.filtered_files:
|
|
274
|
+
empty_label = QLabel("😔 没有找到匹配的文件")
|
|
275
|
+
empty_label.setAlignment(Qt.AlignCenter)
|
|
276
|
+
empty_label.setStyleSheet("color: #888888; padding: 20px;")
|
|
277
|
+
self.grid_layout.addWidget(empty_label, 0, 0, 1, 2)
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
# 分页显示
|
|
281
|
+
start_idx = self.current_page * self.page_size
|
|
282
|
+
end_idx = min(start_idx + self.page_size, len(self.filtered_files))
|
|
283
|
+
page_files = self.filtered_files[start_idx:end_idx]
|
|
284
|
+
|
|
285
|
+
# 显示文件
|
|
286
|
+
for i, file_info in enumerate(page_files):
|
|
287
|
+
icon = "📁" if file_info["is_dir"] else "📄"
|
|
288
|
+
display_text = f"{i + 1}. {icon} {file_info['rel_path']}"
|
|
289
|
+
|
|
290
|
+
button = QPushButton(display_text)
|
|
291
|
+
button.setToolTip(file_info["path"])
|
|
292
|
+
button.clicked.connect(lambda checked, f=file_info: self._on_file_clicked(f))
|
|
293
|
+
|
|
294
|
+
self.grid_layout.addWidget(button, i, 0, 1, 2)
|
|
295
|
+
self.file_buttons.append(button)
|
|
296
|
+
|
|
297
|
+
# 更新标题显示分页信息
|
|
298
|
+
total_pages = (len(self.filtered_files) + self.page_size - 1) // self.page_size
|
|
299
|
+
if total_pages > 1:
|
|
300
|
+
self.title_label.setText(
|
|
301
|
+
f"📁 选择文件 (第 {self.current_page + 1}/{total_pages} 页)"
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
self.title_label.setText("📁 选择文件")
|
|
305
|
+
|
|
306
|
+
def _on_file_clicked(self, file_info: Dict[str, Any]):
|
|
307
|
+
"""处理文件点击"""
|
|
308
|
+
self.file_selected.emit(file_info["path"])
|
|
309
|
+
self.close()
|
|
310
|
+
|
|
311
|
+
def keyPressEvent(self, event: QKeyEvent):
|
|
312
|
+
"""处理键盘事件"""
|
|
313
|
+
if event.key() == Qt.Key_Escape:
|
|
314
|
+
self.popup_closed.emit()
|
|
315
|
+
self.close()
|
|
316
|
+
|
|
317
|
+
elif event.key() in (Qt.Key_Up, Qt.Key_Down):
|
|
318
|
+
self._handle_arrow_navigation(event.key())
|
|
319
|
+
event.accept()
|
|
320
|
+
|
|
321
|
+
elif event.key() in (Qt.Key_Return, Qt.Key_Enter):
|
|
322
|
+
self._confirm_selection()
|
|
323
|
+
event.accept()
|
|
324
|
+
|
|
325
|
+
elif event.text().isdigit():
|
|
326
|
+
num = int(event.text())
|
|
327
|
+
if num > 0 and num <= len(self.file_buttons):
|
|
328
|
+
start_idx = self.current_page * self.page_size
|
|
329
|
+
file_info = self.filtered_files[start_idx + num - 1]
|
|
330
|
+
self._on_file_clicked(file_info)
|
|
331
|
+
event.accept()
|
|
332
|
+
|
|
333
|
+
else:
|
|
334
|
+
super().keyPressEvent(event)
|
|
335
|
+
|
|
336
|
+
def _handle_arrow_navigation(self, key):
|
|
337
|
+
"""处理方向键导航"""
|
|
338
|
+
if not self.file_buttons:
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
if self.current_index == -1:
|
|
342
|
+
self.current_index = 0
|
|
343
|
+
else:
|
|
344
|
+
if key == Qt.Key_Up and self.current_index > 0:
|
|
345
|
+
self.current_index -= 1
|
|
346
|
+
elif key == Qt.Key_Down and self.current_index < len(self.file_buttons) - 1:
|
|
347
|
+
self.current_index += 1
|
|
348
|
+
|
|
349
|
+
self._update_button_focus()
|
|
350
|
+
|
|
351
|
+
def _update_button_focus(self):
|
|
352
|
+
"""更新按钮焦点状态"""
|
|
353
|
+
for i, button in enumerate(self.file_buttons):
|
|
354
|
+
if i == self.current_index:
|
|
355
|
+
button.setFocus()
|
|
356
|
+
button.setStyleSheet("""
|
|
357
|
+
QPushButton {
|
|
358
|
+
background-color: #0078d4;
|
|
359
|
+
border: 1px solid #0078d4;
|
|
360
|
+
color: white;
|
|
361
|
+
padding: 4px 8px;
|
|
362
|
+
text-align: left;
|
|
363
|
+
min-height: 16px;
|
|
364
|
+
max-height: 24px;
|
|
365
|
+
font-size: 11px;
|
|
366
|
+
}
|
|
367
|
+
""")
|
|
368
|
+
self.scroll_area.ensureWidgetVisible(button)
|
|
369
|
+
else:
|
|
370
|
+
button.setStyleSheet("""
|
|
371
|
+
QPushButton {
|
|
372
|
+
background-color: #2b2b2b;
|
|
373
|
+
border: 1px solid #3a3a3a;
|
|
374
|
+
color: #cccccc;
|
|
375
|
+
padding: 4px 8px;
|
|
376
|
+
text-align: left;
|
|
377
|
+
min-height: 16px;
|
|
378
|
+
max-height: 24px;
|
|
379
|
+
font-size: 11px;
|
|
380
|
+
}
|
|
381
|
+
QPushButton:hover {
|
|
382
|
+
background-color: #353535;
|
|
383
|
+
border-color: #4a4a4a;
|
|
384
|
+
}
|
|
385
|
+
""")
|
|
386
|
+
|
|
387
|
+
def _confirm_selection(self):
|
|
388
|
+
"""确认当前选择"""
|
|
389
|
+
if self.current_index >= 0 and self.current_index < len(self.file_buttons):
|
|
390
|
+
start_idx = self.current_page * self.page_size
|
|
391
|
+
file_info = self.filtered_files[start_idx + self.current_index]
|
|
392
|
+
self._on_file_clicked(file_info)
|
|
393
|
+
|
|
394
|
+
def show_at_position(self, position: QPoint):
|
|
395
|
+
"""在指定位置显示弹窗"""
|
|
396
|
+
from PySide6.QtWidgets import QApplication
|
|
397
|
+
|
|
398
|
+
screen = QApplication.primaryScreen()
|
|
399
|
+
screen_geometry = screen.availableGeometry()
|
|
400
|
+
|
|
401
|
+
popup_size = self.sizeHint()
|
|
402
|
+
|
|
403
|
+
# 调整X坐标
|
|
404
|
+
if position.x() + popup_size.width() > screen_geometry.right():
|
|
405
|
+
position.setX(screen_geometry.right() - popup_size.width())
|
|
406
|
+
if position.x() < screen_geometry.left():
|
|
407
|
+
position.setX(screen_geometry.left())
|
|
408
|
+
|
|
409
|
+
# 调整Y坐标 - 默认在上方显示
|
|
410
|
+
position.setY(position.y() - popup_size.height() - 10)
|
|
411
|
+
|
|
412
|
+
# 如果上方空间不够,则显示在下方
|
|
413
|
+
if position.y() < screen_geometry.top():
|
|
414
|
+
position.setY(position.y() + popup_size.height() + 35)
|
|
415
|
+
|
|
416
|
+
self.move(position)
|
|
417
|
+
self.show()
|