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.

@@ -0,0 +1,158 @@
1
+ /*
2
+ * Session List UI Styles
3
+ * Theme: Modern Dark / macOS Notification Center Style
4
+ *
5
+ * REQUIRED: Ensure the following ObjectNames are set in your Python code:
6
+ * - Main Container: "mainContainer"
7
+ * - Title Bar: "titleBar"
8
+ * - Title Label: "titleLabel"
9
+ * - Collapse Button: "collapseButton"
10
+ * - Scroll Area: "scrollArea"
11
+ * - Session Item Widget: "sessionCard"
12
+ * - Project Label: "projectLabel"
13
+ * - Workspace Label: "workspaceLabel"
14
+ * - Stage Label: "stageLabel"
15
+ * - Conversation Label: "conversationLabel"
16
+ * - Progress Bar: "progressBar"
17
+ * - Time Label: "timeLabel"
18
+ */
19
+
20
+ /* Main Container */
21
+ QWidget#mainContainer {
22
+ background-color: rgba(35, 35, 35, 240); /* Slightly transparent dark background */
23
+ border: 1px solid rgba(255, 255, 255, 30);
24
+ border-radius: 16px;
25
+ }
26
+
27
+ /* Title Bar */
28
+ QWidget#titleBar {
29
+ background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(70, 70, 70, 255), stop:1 rgba(50, 50, 50, 255));
30
+ border-top-left-radius: 15px;
31
+ border-top-right-radius: 15px;
32
+ border-bottom: 1px solid rgba(0, 0, 0, 50);
33
+ }
34
+
35
+ QLabel#titleLabel {
36
+ color: #FFFFFF;
37
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
38
+ font-size: 13px;
39
+ font-weight: bold;
40
+ background-color: transparent;
41
+ padding-left: 5px;
42
+ }
43
+
44
+ QPushButton#collapseButton {
45
+ background-color: rgba(255, 255, 255, 0);
46
+ color: rgba(255, 255, 255, 180);
47
+ border: none;
48
+ border-radius: 12px;
49
+ font-size: 12px;
50
+ font-weight: bold;
51
+ }
52
+
53
+ QPushButton#collapseButton:hover {
54
+ background-color: rgba(255, 255, 255, 30);
55
+ color: #FFFFFF;
56
+ }
57
+
58
+ /* Scroll Area */
59
+ QScrollArea#scrollArea {
60
+ background-color: transparent;
61
+ border: none;
62
+ }
63
+
64
+ QWidget#sessionContainer {
65
+ background-color: transparent;
66
+ }
67
+
68
+ /* Scroll Bar */
69
+ QScrollBar:vertical {
70
+ border: none;
71
+ background: transparent;
72
+ width: 8px;
73
+ margin: 2px;
74
+ }
75
+
76
+ QScrollBar::handle:vertical {
77
+ background: rgba(255, 255, 255, 40);
78
+ min-height: 20px;
79
+ border-radius: 4px;
80
+ }
81
+
82
+ QScrollBar::handle:vertical:hover {
83
+ background: rgba(255, 255, 255, 60);
84
+ }
85
+
86
+ QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
87
+ border: none;
88
+ background: none;
89
+ height: 0px;
90
+ }
91
+
92
+ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
93
+ background: none;
94
+ }
95
+
96
+ /* Session Card */
97
+ QWidget#sessionCard {
98
+ background-color: rgba(60, 60, 60, 200);
99
+ border: 1px solid rgba(255, 255, 255, 10);
100
+ border-radius: 10px;
101
+ }
102
+
103
+ QWidget#sessionCard:hover {
104
+ background-color: rgba(75, 75, 75, 230);
105
+ border: 1px solid rgba(255, 255, 255, 25);
106
+ /* Note: QSS does not support box-shadow or transform on hover in the same way CSS does */
107
+ }
108
+
109
+ /* Labels */
110
+ QLabel#projectLabel {
111
+ color: #FF9800; /* Orange */
112
+ font-size: 11px;
113
+ font-weight: 600;
114
+ background-color: transparent;
115
+ }
116
+
117
+ QLabel#workspaceLabel {
118
+ color: #4CAF50; /* Green */
119
+ font-size: 11px;
120
+ font-weight: 600;
121
+ background-color: transparent;
122
+ }
123
+
124
+ QLabel#stageLabel {
125
+ color: #64B5F6; /* Blue */
126
+ font-size: 11px;
127
+ font-weight: 600;
128
+ background-color: transparent;
129
+ }
130
+
131
+ QLabel#conversationLabel {
132
+ color: #EEEEEE; /* White-ish */
133
+ font-size: 12px;
134
+ background-color: transparent;
135
+ margin-top: 2px;
136
+ margin-bottom: 4px;
137
+ }
138
+
139
+ /* Progress Bar */
140
+ QProgressBar#progressBar {
141
+ background-color: rgba(0, 0, 0, 60);
142
+ border: none;
143
+ border-radius: 2px;
144
+ text-align: center;
145
+ }
146
+
147
+ QProgressBar#progressBar::chunk {
148
+ background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4CAF50, stop:1 #81C784);
149
+ border-radius: 2px;
150
+ }
151
+
152
+ /* Timer */
153
+ QLabel#timeLabel {
154
+ color: #AAAAAA;
155
+ font-size: 10px;
156
+ font-family: monospace;
157
+ background-color: transparent;
158
+ }
@@ -0,0 +1,197 @@
1
+ """
2
+ 窗口位置管理器
3
+ 用于管理多个 Feedback UI 窗口的位置,避免重叠
4
+ """
5
+ import os
6
+ import json
7
+ import time
8
+ from typing import Tuple, List, Dict, Optional
9
+ from PySide6.QtGui import QGuiApplication
10
+
11
+
12
+ class WindowPositionManager:
13
+ """管理多个窗口的位置,实现智能错位显示"""
14
+
15
+ # 窗口位置记录文件
16
+ POSITION_FILE = os.path.join(os.path.dirname(__file__), '.window_positions.json')
17
+
18
+ # 窗口错位的偏移量
19
+ OFFSET_X = 30 # 水平偏移
20
+ OFFSET_Y = 30 # 垂直偏移
21
+ MAX_CASCADE_COUNT = 10 # 最大级联数量
22
+
23
+ # 窗口存活时间(秒),超过这个时间的记录会被清理
24
+ WINDOW_TTL = 300 # 5分钟
25
+
26
+ @classmethod
27
+ def get_next_position(cls, window_type: str = 'main', default_x: int = None, default_y: int = None) -> Tuple[int, int]:
28
+ """
29
+ 获取下一个窗口的位置
30
+
31
+ Args:
32
+ window_type: 窗口类型 ('main' 或 'compact')
33
+ default_x: 默认 X 坐标
34
+ default_y: 默认 Y 坐标
35
+
36
+ Returns:
37
+ (x, y) 坐标元组
38
+ """
39
+ # 清理过期的窗口记录
40
+ cls._cleanup_old_positions()
41
+
42
+ # 读取现有位置记录
43
+ positions = cls._load_positions()
44
+
45
+ # 获取当前窗口类型的记录
46
+ type_positions = positions.get(window_type, [])
47
+
48
+ # 获取屏幕信息
49
+ screen = QGuiApplication.primaryScreen()
50
+ if screen:
51
+ screen_geometry = screen.availableGeometry()
52
+ screen_width = screen_geometry.width()
53
+ screen_height = screen_geometry.height()
54
+ screen_x = screen_geometry.x()
55
+ screen_y = screen_geometry.y()
56
+ else:
57
+ # 默认屏幕尺寸
58
+ screen_width = 1920
59
+ screen_height = 1080
60
+ screen_x = 0
61
+ screen_y = 0
62
+
63
+ # 计算基础位置
64
+ if default_x is None or default_y is None:
65
+ if window_type == 'compact':
66
+ # 精简版默认在顶部居中
67
+ base_x = screen_x + (screen_width - 250) // 2
68
+ base_y = screen_y + 20
69
+ else:
70
+ # 主窗口默认在屏幕中央
71
+ base_x = screen_x + (screen_width - 500) // 2
72
+ base_y = screen_y + (screen_height - 900) // 2
73
+ else:
74
+ base_x = default_x
75
+ base_y = default_y
76
+
77
+ # 计算级联位置
78
+ cascade_index = len(type_positions) % cls.MAX_CASCADE_COUNT
79
+
80
+ # 计算最终位置
81
+ final_x = base_x + (cascade_index * cls.OFFSET_X)
82
+ final_y = base_y + (cascade_index * cls.OFFSET_Y)
83
+
84
+ # 确保窗口不会超出屏幕边界
85
+ window_width = 250 if window_type == 'compact' else 500
86
+ window_height = 40 if window_type == 'compact' else 900
87
+
88
+ # 调整 X 坐标
89
+ if final_x + window_width > screen_x + screen_width:
90
+ final_x = screen_x + screen_width - window_width - 20
91
+ if final_x < screen_x:
92
+ final_x = screen_x + 20
93
+
94
+ # 调整 Y 坐标
95
+ if final_y + window_height > screen_y + screen_height:
96
+ final_y = screen_y + screen_height - window_height - 20
97
+ if final_y < screen_y:
98
+ final_y = screen_y + 20
99
+
100
+ # 记录新位置
101
+ cls._add_position(window_type, final_x, final_y)
102
+
103
+ return (final_x, final_y)
104
+
105
+ @classmethod
106
+ def remove_position(cls, window_type: str, x: int, y: int):
107
+ """
108
+ 移除窗口位置记录(窗口关闭时调用)
109
+
110
+ Args:
111
+ window_type: 窗口类型
112
+ x: X 坐标
113
+ y: Y 坐标
114
+ """
115
+ positions = cls._load_positions()
116
+
117
+ if window_type in positions:
118
+ # 移除匹配的位置记录
119
+ positions[window_type] = [
120
+ pos for pos in positions[window_type]
121
+ if not (abs(pos['x'] - x) < 5 and abs(pos['y'] - y) < 5)
122
+ ]
123
+
124
+ # 如果该类型没有窗口了,删除键
125
+ if not positions[window_type]:
126
+ del positions[window_type]
127
+
128
+ cls._save_positions(positions)
129
+
130
+ @classmethod
131
+ def _load_positions(cls) -> Dict[str, List[Dict]]:
132
+ """加载位置记录"""
133
+ if os.path.exists(cls.POSITION_FILE):
134
+ try:
135
+ with open(cls.POSITION_FILE, 'r') as f:
136
+ return json.load(f)
137
+ except (json.JSONDecodeError, IOError):
138
+ pass
139
+ return {}
140
+
141
+ @classmethod
142
+ def _save_positions(cls, positions: Dict[str, List[Dict]]):
143
+ """保存位置记录"""
144
+ try:
145
+ with open(cls.POSITION_FILE, 'w') as f:
146
+ json.dump(positions, f)
147
+ except IOError:
148
+ pass
149
+
150
+ @classmethod
151
+ def _add_position(cls, window_type: str, x: int, y: int):
152
+ """添加位置记录"""
153
+ positions = cls._load_positions()
154
+
155
+ if window_type not in positions:
156
+ positions[window_type] = []
157
+
158
+ positions[window_type].append({
159
+ 'x': x,
160
+ 'y': y,
161
+ 'timestamp': time.time()
162
+ })
163
+
164
+ cls._save_positions(positions)
165
+
166
+ @classmethod
167
+ def _cleanup_old_positions(cls):
168
+ """清理过期的位置记录"""
169
+ positions = cls._load_positions()
170
+ current_time = time.time()
171
+ updated = False
172
+
173
+ for window_type in list(positions.keys()):
174
+ # 过滤掉过期的记录
175
+ active_positions = [
176
+ pos for pos in positions[window_type]
177
+ if current_time - pos.get('timestamp', 0) < cls.WINDOW_TTL
178
+ ]
179
+
180
+ if len(active_positions) != len(positions[window_type]):
181
+ updated = True
182
+ if active_positions:
183
+ positions[window_type] = active_positions
184
+ else:
185
+ del positions[window_type]
186
+
187
+ if updated:
188
+ cls._save_positions(positions)
189
+
190
+ @classmethod
191
+ def clear_all_positions(cls):
192
+ """清除所有位置记录(用于调试)"""
193
+ if os.path.exists(cls.POSITION_FILE):
194
+ try:
195
+ os.remove(cls.POSITION_FILE)
196
+ except IOError:
197
+ pass
workspace_manager.py ADDED
@@ -0,0 +1,253 @@
1
+ """
2
+ 工作空间管理器 - 处理工作空间和阶段信息
3
+ """
4
+ import os
5
+ import json
6
+ import yaml
7
+ from typing import Optional, Dict, Any, Tuple
8
+ from pathlib import Path
9
+
10
+
11
+ class WorkspaceManager:
12
+ """工作空间管理器"""
13
+
14
+ def __init__(self, project_path: str = None):
15
+ """初始化工作空间管理器
16
+
17
+ Args:
18
+ project_path: 项目路径,如果不提供则使用当前工作目录
19
+ """
20
+ if project_path:
21
+ self.project_path = Path(project_path)
22
+ else:
23
+ # 尝试找到项目根目录
24
+ current_path = Path.cwd()
25
+ while current_path != current_path.parent:
26
+ if (current_path / '.workspace').exists():
27
+ self.project_path = current_path
28
+ break
29
+ current_path = current_path.parent
30
+ else:
31
+ self.project_path = Path.cwd()
32
+
33
+ self.workspace_dir = self.project_path / '.workspace'
34
+ self.workflows_dir = self.project_path / '.claude' / 'workflows'
35
+
36
+ def get_workspace_id_by_session(self, session_id: str) -> Optional[str]:
37
+ """根据session_id获取workspace_id
38
+
39
+ Args:
40
+ session_id: 会话ID
41
+
42
+ Returns:
43
+ workspace_id 或 None
44
+ """
45
+ session_map_path = self.workspace_dir / 'session_map.json'
46
+ if not session_map_path.exists():
47
+ return None
48
+
49
+ try:
50
+ with open(session_map_path, 'r', encoding='utf-8') as f:
51
+ session_map = json.load(f)
52
+
53
+ # 查找workspace_mappings
54
+ workspace_mappings = session_map.get('workspace_mappings', {})
55
+ return workspace_mappings.get(session_id)
56
+ except Exception as e:
57
+ print(f"Error reading session_map.json: {e}")
58
+ return None
59
+
60
+ def load_workspace_config(self, workspace_id: str) -> Optional[Dict[str, Any]]:
61
+ """加载工作空间配置
62
+
63
+ Args:
64
+ workspace_id: 工作空间ID
65
+
66
+ Returns:
67
+ 工作空间配置字典 或 None
68
+ """
69
+ workspace_path = self.workspace_dir / workspace_id / 'workspace.yml'
70
+ if not workspace_path.exists():
71
+ return None
72
+
73
+ try:
74
+ with open(workspace_path, 'r', encoding='utf-8') as f:
75
+ return yaml.safe_load(f)
76
+ except Exception as e:
77
+ print(f"Error reading workspace.yml: {e}")
78
+ return None
79
+
80
+ def load_stage_template(self, template_id: str) -> Optional[Dict[str, Any]]:
81
+ """加载阶段模板
82
+
83
+ Args:
84
+ template_id: 模板ID或完整文件路径
85
+
86
+ Returns:
87
+ 阶段模板配置 或 None
88
+ """
89
+ # 判断是否为完整路径
90
+ if '/' in template_id or template_id.endswith('.yml'):
91
+ # 直接使用路径
92
+ template_path = Path(template_id)
93
+ else:
94
+ # 兼容旧逻辑:拼接路径
95
+ template_path = self.workflows_dir / f'{template_id}.yml'
96
+
97
+ if not template_path.exists():
98
+ return None
99
+
100
+ try:
101
+ with open(template_path, 'r', encoding='utf-8') as f:
102
+ return yaml.safe_load(f)
103
+ except Exception as e:
104
+ print(f"Error reading stage template: {e}")
105
+ return None
106
+
107
+ def get_stage_info(self, session_id: str = None, workspace_id: str = None) -> Optional[Dict[str, Any]]:
108
+ """获取阶段信息
109
+
110
+ Args:
111
+ session_id: 会话ID(如果提供了workspace_id则可选)
112
+ workspace_id: 工作空间ID(优先使用,如果不提供则通过session_id查找)
113
+
114
+ Returns:
115
+ 包含当前阶段、上一阶段、下一阶段信息的字典
116
+ """
117
+ # 1. 获取workspace_id
118
+ if not workspace_id:
119
+ if not session_id:
120
+ return None
121
+ workspace_id = self.get_workspace_id_by_session(session_id)
122
+ if not workspace_id:
123
+ return None
124
+
125
+ # 2. 加载workspace配置
126
+ workspace_config = self.load_workspace_config(workspace_id)
127
+ if not workspace_config:
128
+ return None
129
+
130
+ # 3. 获取阶段模板和当前阶段
131
+ stage_template_id = workspace_config.get('stage_template_id')
132
+ current_stage_id = workspace_config.get('current_stage_id')
133
+
134
+ if not stage_template_id or not current_stage_id:
135
+ return None
136
+
137
+ # 4. 加载阶段模板
138
+ template_config = self.load_stage_template(stage_template_id)
139
+ if not template_config:
140
+ return None
141
+
142
+ # 5. 解析阶段信息
143
+ workflow = template_config.get('workflow', {})
144
+ steps = workflow.get('steps', [])
145
+
146
+ if not steps:
147
+ return None
148
+
149
+ # 找到当前阶段索引
150
+ current_index = -1
151
+ for i, step in enumerate(steps):
152
+ if step.get('id') == current_stage_id:
153
+ current_index = i
154
+ break
155
+
156
+ if current_index == -1:
157
+ return None
158
+
159
+ # 构建阶段信息
160
+ result = {
161
+ 'workspace_id': workspace_id,
162
+ 'current_stage': {
163
+ 'id': steps[current_index].get('id'),
164
+ 'title': steps[current_index].get('title'),
165
+ 'description': steps[current_index].get('des')
166
+ }
167
+ }
168
+
169
+ # 上一阶段
170
+ if current_index > 0:
171
+ result['prev_stage'] = {
172
+ 'id': steps[current_index - 1].get('id'),
173
+ 'title': steps[current_index - 1].get('title'),
174
+ 'description': steps[current_index - 1].get('des')
175
+ }
176
+ else:
177
+ result['prev_stage'] = None
178
+
179
+ # 下一阶段
180
+ if current_index < len(steps) - 1:
181
+ result['next_stage'] = {
182
+ 'id': steps[current_index + 1].get('id'),
183
+ 'title': steps[current_index + 1].get('title'),
184
+ 'description': steps[current_index + 1].get('des')
185
+ }
186
+ else:
187
+ result['next_stage'] = None
188
+
189
+ return result
190
+
191
+
192
+ def get_stage_info_for_session(session_id: str, project_path: str = None) -> Optional[Dict[str, Any]]:
193
+ """便捷函数:获取指定会话的阶段信息
194
+
195
+ Args:
196
+ session_id: 会话ID
197
+ project_path: 项目路径
198
+
199
+ Returns:
200
+ 阶段信息字典
201
+ """
202
+ manager = WorkspaceManager(project_path)
203
+ return manager.get_stage_info(session_id)
204
+
205
+
206
+ def get_workspace_goal_for_session(session_id: str, project_path: str = None) -> Optional[str]:
207
+ """便捷函数:获取指定会话的工作空间goal
208
+
209
+ Args:
210
+ session_id: 会话ID
211
+ project_path: 项目路径
212
+
213
+ Returns:
214
+ 工作空间goal或None
215
+ """
216
+ manager = WorkspaceManager(project_path)
217
+ workspace_id = manager.get_workspace_id_by_session(session_id)
218
+ if not workspace_id:
219
+ return None
220
+
221
+ workspace_config = manager.load_workspace_config(workspace_id)
222
+ if not workspace_config:
223
+ return None
224
+
225
+ return workspace_config.get('goal')
226
+
227
+
228
+ def get_session_title_for_session(session_id: str, project_path: str = None) -> Optional[str]:
229
+ """便捷函数:获取指定会话的对话标题
230
+
231
+ Args:
232
+ session_id: 会话ID
233
+ project_path: 项目路径
234
+
235
+ Returns:
236
+ 对话标题或None
237
+ """
238
+ manager = WorkspaceManager(project_path)
239
+ workspace_id = manager.get_workspace_id_by_session(session_id)
240
+ if not workspace_id:
241
+ return None
242
+
243
+ workspace_config = manager.load_workspace_config(workspace_id)
244
+ if not workspace_config:
245
+ return None
246
+
247
+ # 从sessions列表中查找匹配的session
248
+ sessions = workspace_config.get('sessions', [])
249
+ for session in sessions:
250
+ if session.get('id') == session_id:
251
+ return session.get('title')
252
+
253
+ return None