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.

session_manager.py ADDED
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 会话状态管理器
4
+ 用于管理stop hook和feedback UI之间的会话状态,避免死循环
5
+ """
6
+ import json
7
+ import os
8
+ import sys
9
+ from datetime import datetime, timedelta
10
+ from pathlib import Path
11
+ from typing import Optional, Dict, Any
12
+
13
+ class SessionManager:
14
+ """管理会话状态,避免stop hook死循环
15
+
16
+ 状态保存在 .workspace/chat_history/{session_id}.json 中
17
+ """
18
+
19
+ def __init__(self, session_id: Optional[str] = None, project_path: Optional[str] = None):
20
+ """初始化会话管理器
21
+
22
+ Args:
23
+ session_id: 会话ID
24
+ project_path: 项目路径,用于定位.workspace目录
25
+ """
26
+ self.session_id = session_id
27
+ self.project_path = project_path or os.getcwd()
28
+
29
+ # 不再加载所有sessions,改为按需加载单个session
30
+ # self.sessions = self._load_sessions()
31
+ # self._cleanup_old_sessions()
32
+
33
+ def _get_chat_history_path(self, session_id: str) -> Path:
34
+ """获取指定session的chat_history文件路径
35
+
36
+ Args:
37
+ session_id: 会话ID
38
+
39
+ Returns:
40
+ Path: chat_history文件路径
41
+ """
42
+ workspace_dir = Path(self.project_path) / '.workspace' / 'chat_history'
43
+ workspace_dir.mkdir(parents=True, exist_ok=True)
44
+ return workspace_dir / f"{session_id}.json"
45
+
46
+ def _load_session_data(self, session_id: str) -> list:
47
+ """加载指定session的chat_history数据(兼容旧格式)
48
+
49
+ Args:
50
+ session_id: 会话ID
51
+
52
+ Returns:
53
+ list: chat_history数据数组(内部格式,用于兼容现有代码)
54
+ """
55
+ chat_file = self._get_chat_history_path(session_id)
56
+ if not chat_file.exists():
57
+ return []
58
+
59
+ try:
60
+ with open(chat_file, 'r', encoding='utf-8') as f:
61
+ data = json.load(f)
62
+
63
+ # 新格式:{'dialogues': [...], 'control': {...}}
64
+ if isinstance(data, dict) and 'dialogues' in data:
65
+ # 转换为内部数组格式
66
+ result = []
67
+
68
+ # 添加对话记录(自动添加type字段以兼容现有代码)
69
+ for dialogue in data.get('dialogues', []):
70
+ dialogue_with_type = dialogue.copy()
71
+ dialogue_with_type['type'] = 'dialogue'
72
+ result.append(dialogue_with_type)
73
+
74
+ # 添加control记录
75
+ if 'control' in data and data['control']:
76
+ result.append(data['control'])
77
+
78
+ return result
79
+
80
+ # 旧格式:直接返回数组
81
+ return data if isinstance(data, list) else []
82
+ except (json.JSONDecodeError, IOError):
83
+ return []
84
+
85
+ def _save_session_data(self, session_id: str, data: list):
86
+ """保存指定session的chat_history数据(新格式)
87
+
88
+ Args:
89
+ session_id: 会话ID
90
+ data: chat_history数据数组
91
+ """
92
+ chat_file = self._get_chat_history_path(session_id)
93
+ try:
94
+ # 转换为新格式:dialogues数组
95
+ new_format_data = {
96
+ 'dialogues': []
97
+ }
98
+
99
+ # 过滤出对话项
100
+ for item in data:
101
+ if isinstance(item, dict):
102
+ # 保留 agent 记录(直接添加)
103
+ if item.get('role') == 'agent':
104
+ new_format_data['dialogues'].append(item)
105
+ continue
106
+
107
+ item_type = item.get('type')
108
+ if item_type == 'dialogue':
109
+ # 创建新格式的对话项(移除type字段)
110
+ dialogue = {
111
+ 'timestamp': item.get('timestamp', ''),
112
+ 'time_display': item.get('time_display', ''),
113
+ 'messages': item.get('messages', [])
114
+ }
115
+ new_format_data['dialogues'].append(dialogue)
116
+ elif item_type == 'stop_hook_status':
117
+ # 保留control字段(如果存在)
118
+ if 'control' not in new_format_data:
119
+ new_format_data['control'] = item
120
+
121
+ with open(chat_file, 'w', encoding='utf-8') as f:
122
+ json.dump(new_format_data, f, indent=2, ensure_ascii=False)
123
+ except IOError as e:
124
+ print(f"Error saving session data: {e}", file=sys.stderr)
125
+
126
+ def _get_stop_hook_status(self, session_id: str) -> Optional[Dict[str, Any]]:
127
+ """获取session的stop_hook状态
128
+
129
+ Args:
130
+ session_id: 会话ID
131
+
132
+ Returns:
133
+ Optional[Dict]: stop_hook状态数据,如果不存在则返回None
134
+ """
135
+ data = self._load_session_data(session_id)
136
+ # 在数组末尾查找stop_hook_status元素
137
+ for item in reversed(data):
138
+ if isinstance(item, dict) and item.get('type') == 'stop_hook_status':
139
+ return item
140
+ return None
141
+
142
+ def _set_stop_hook_status(self, session_id: str, status: str, action: Optional[str] = None):
143
+ """设置session的stop_hook状态
144
+
145
+ Args:
146
+ session_id: 会话ID
147
+ status: 状态值
148
+ action: 动作描述
149
+ """
150
+ data = self._load_session_data(session_id)
151
+
152
+ # 移除旧的stop_hook_status元素
153
+ data = [item for item in data if not (isinstance(item, dict) and item.get('type') == 'stop_hook_status')]
154
+
155
+ # 添加新的stop_hook_status元素
156
+ data.append({
157
+ 'type': 'stop_hook_status',
158
+ 'status': status,
159
+ 'timestamp': datetime.now().isoformat(),
160
+ 'last_action': action or status
161
+ })
162
+
163
+ self._save_session_data(session_id, data)
164
+
165
+ def _clear_stop_hook_status(self, session_id: str):
166
+ """清除session的stop_hook状态
167
+
168
+ Args:
169
+ session_id: 会话ID
170
+ """
171
+ data = self._load_session_data(session_id)
172
+ # 移除stop_hook_status元素
173
+ data = [item for item in data if not (isinstance(item, dict) and item.get('type') == 'stop_hook_status')]
174
+ self._save_session_data(session_id, data)
175
+
176
+ def get_session_status(self, session_id: str) -> Optional[str]:
177
+ """获取会话状态
178
+
179
+ Args:
180
+ session_id: 会话ID
181
+
182
+ Returns:
183
+ 会话状态: "user_closed_by_button", "timeout_closed", "active" 或 None
184
+ """
185
+ status_data = self._get_stop_hook_status(session_id)
186
+ if status_data:
187
+ return status_data.get("status")
188
+ return None
189
+
190
+ def set_session_status(self, session_id: str, status: str, action: Optional[str] = None):
191
+ """设置会话状态
192
+
193
+ Args:
194
+ session_id: 会话ID
195
+ status: 状态 ("user_closed_by_button", "timeout_closed", "active")
196
+ action: 最后的动作描述
197
+ """
198
+ self._set_stop_hook_status(session_id, status, action)
199
+
200
+ def is_feedback_closed(self, session_id: str) -> bool:
201
+ """检查会话是否因用户关闭feedback而结束
202
+
203
+ Args:
204
+ session_id: 会话ID
205
+
206
+ Returns:
207
+ True如果用户主动关闭了feedback窗口
208
+ """
209
+ status = self.get_session_status(session_id)
210
+ return status == "user_closed"
211
+
212
+ def mark_feedback_closed(self, session_id: str):
213
+ """标记feedback被用户关闭(通用方法,保留向后兼容)
214
+
215
+ Args:
216
+ session_id: 会话ID
217
+ """
218
+ self.set_session_status(session_id, "user_closed", "feedback_window_closed_by_user")
219
+
220
+ def mark_user_closed_by_button(self, session_id: str):
221
+ """标记用户点击关闭按钮
222
+
223
+ Args:
224
+ session_id: 会话ID
225
+ """
226
+ self.set_session_status(session_id, "user_closed_by_button", "user_clicked_close_button")
227
+
228
+ def mark_timeout_closed(self, session_id: str):
229
+ """标记超时自动关闭
230
+
231
+ Args:
232
+ session_id: 会话ID
233
+ """
234
+ self.set_session_status(session_id, "timeout_closed", "feedback_timeout_auto_closed")
235
+
236
+ def is_user_closed_by_button(self, session_id: str) -> bool:
237
+ """检查是否用户点击关闭按钮
238
+
239
+ Args:
240
+ session_id: 会话ID
241
+
242
+ Returns:
243
+ True如果用户点击关闭按钮
244
+ """
245
+ status = self.get_session_status(session_id)
246
+ return status == "user_closed_by_button"
247
+
248
+ def is_timeout_closed(self, session_id: str) -> bool:
249
+ """检查是否超时关闭
250
+
251
+ Args:
252
+ session_id: 会话ID
253
+
254
+ Returns:
255
+ True如果超时自动关闭
256
+ """
257
+ status = self.get_session_status(session_id)
258
+ return status == "timeout_closed"
259
+
260
+ def reset_on_feedback_show(self, session_id: str):
261
+ """feedback展示时重置状态
262
+
263
+ Args:
264
+ session_id: 会话ID
265
+ """
266
+ # 清除所有状态
267
+ self.clear_session(session_id)
268
+
269
+ def get_block_count(self, session_id: str) -> int:
270
+ """获取会话的阻止次数
271
+
272
+ Args:
273
+ session_id: 会话ID
274
+
275
+ Returns:
276
+ 阻止次数
277
+ """
278
+ status_data = self._get_stop_hook_status(session_id)
279
+ if status_data:
280
+ return status_data.get("block_count", 0)
281
+ return 0
282
+
283
+ def increment_block_count(self, session_id: str) -> int:
284
+ """增加会话的阻止次数
285
+
286
+ Args:
287
+ session_id: 会话ID
288
+
289
+ Returns:
290
+ 更新后的阻止次数
291
+ """
292
+ status_data = self._get_stop_hook_status(session_id)
293
+ if status_data:
294
+ new_count = status_data.get("block_count", 0) + 1
295
+ status_data["block_count"] = new_count
296
+ status_data["timestamp"] = datetime.now().isoformat()
297
+
298
+ # 更新到chat_history
299
+ data = self._load_session_data(session_id)
300
+ # 移除旧的stop_hook_status
301
+ data = [item for item in data if not (isinstance(item, dict) and item.get('type') == 'stop_hook_status')]
302
+ # 添加更新后的status
303
+ data.append(status_data)
304
+ self._save_session_data(session_id, data)
305
+ return new_count
306
+ else:
307
+ # 首次创建
308
+ self._set_stop_hook_status(session_id, "active", "block_count_increment")
309
+ data = self._load_session_data(session_id)
310
+ # 找到刚创建的status并设置block_count
311
+ for item in reversed(data):
312
+ if isinstance(item, dict) and item.get('type') == 'stop_hook_status':
313
+ item["block_count"] = 1
314
+ break
315
+ self._save_session_data(session_id, data)
316
+ return 1
317
+
318
+ def clear_session(self, session_id: str):
319
+ """清除会话状态
320
+
321
+ Args:
322
+ session_id: 会话ID
323
+ """
324
+ self._clear_stop_hook_status(session_id)
325
+
326
+
327
+ def main():
328
+ """命令行接口"""
329
+ import argparse
330
+
331
+ parser = argparse.ArgumentParser(description='管理会话状态')
332
+ parser.add_argument('action', choices=['get', 'set', 'check', 'mark_closed', 'clear'],
333
+ help='要执行的操作')
334
+ parser.add_argument('session_id', help='会话ID')
335
+ parser.add_argument('--status', help='设置的状态 (用于set操作)')
336
+ parser.add_argument('--state-file', help='状态文件路径')
337
+
338
+ args = parser.parse_args()
339
+
340
+ manager = SessionManager(args.state_file)
341
+
342
+ if args.action == 'get':
343
+ status = manager.get_session_status(args.session_id)
344
+ print(status if status else "none")
345
+
346
+ elif args.action == 'set':
347
+ if not args.status:
348
+ print("Error: --status required for set action", file=sys.stderr)
349
+ sys.exit(1)
350
+ manager.set_session_status(args.session_id, args.status)
351
+ print(f"Session {args.session_id} status set to {args.status}")
352
+
353
+ elif args.action == 'check':
354
+ is_closed = manager.is_feedback_closed(args.session_id)
355
+ print("closed" if is_closed else "active")
356
+ sys.exit(0 if not is_closed else 1)
357
+
358
+ elif args.action == 'mark_closed':
359
+ manager.mark_feedback_closed(args.session_id)
360
+ print(f"Session {args.session_id} marked as closed by user")
361
+
362
+ elif args.action == 'clear':
363
+ manager.clear_session(args.session_id)
364
+ print(f"Session {args.session_id} cleared")
365
+
366
+
367
+ if __name__ == "__main__":
368
+ main()
stop_hook.py ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Stop Hook处理脚本
4
+ 智能处理stop事件,避免死循环
5
+ """
6
+ import sys
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+
11
+ # 添加当前目录到Python路径
12
+ current_dir = Path(__file__).parent
13
+ sys.path.insert(0, str(current_dir))
14
+
15
+ from session_manager import SessionManager
16
+ from context_formatter import format_for_stop_hook
17
+
18
+
19
+ def main():
20
+ """主函数"""
21
+ try:
22
+ # 从stdin读取JSON输入
23
+ input_data = json.load(sys.stdin)
24
+
25
+ # 提取关键信息
26
+ session_id = input_data.get('session_id', '')
27
+ stop_hook_active = input_data.get('stop_hook_active', False)
28
+
29
+ # 获取项目路径
30
+ project_path = os.getcwd()
31
+
32
+ # 创建会话管理器(传入project_path)
33
+ manager = SessionManager(session_id=session_id, project_path=project_path)
34
+
35
+ # 决策逻辑
36
+ if session_id:
37
+ # 1. 检查用户是否点击关闭按钮
38
+ if manager.is_user_closed_by_button(session_id):
39
+ # 用户主动点击关闭,完全不提示(静默允许停止)
40
+ # 🔧 立即清除状态,避免死循环
41
+ manager.clear_session(session_id)
42
+ return 0
43
+
44
+ # 2. 检查是否超时关闭
45
+ if manager.is_timeout_closed(session_id):
46
+ # 超时关闭场景,最多提示2次
47
+ current_block_count = manager.get_block_count(session_id)
48
+ MAX_BLOCK_COUNT = 2
49
+
50
+ if current_block_count >= MAX_BLOCK_COUNT:
51
+ # 超过最大阻止次数,允许停止以避免死循环
52
+ manager.clear_session(session_id)
53
+ return 0
54
+
55
+ # 未超过2次,继续提示并增加计数
56
+ manager.increment_block_count(session_id)
57
+ else:
58
+ # 3. 正常场景(非关闭状态),重置计数
59
+ # 这样每次正常的stop hook都会重新开始计数
60
+ if manager.get_block_count(session_id) > 0:
61
+ manager.clear_session(session_id)
62
+
63
+ # 4. 默认行为:阻止停止并提示使用feedback工具
64
+ # 使用新的格式化上下文信息
65
+ if session_id:
66
+ reason_text = format_for_stop_hook(session_id, project_path)
67
+ else:
68
+ reason_text = "请你调用 feedback mcp tool 向用户反馈/请示。示例:使用 mcp__feedback__feedback 工具向用户汇报当前工作进度、完成状态或请求下一步指示。"
69
+
70
+ result = {
71
+ "decision": "block",
72
+ "reason": reason_text
73
+ }
74
+ print(json.dumps(result, ensure_ascii=False))
75
+ return 0
76
+
77
+ except Exception as e:
78
+ # 发生错误时,默认允许停止(避免卡死)
79
+ error_result = {
80
+ "decision": "approve",
81
+ "reason": f"Hook处理出错: {str(e)}"
82
+ }
83
+ print(json.dumps(error_result, ensure_ascii=False), file=sys.stderr)
84
+ return 1
85
+
86
+ if __name__ == "__main__":
87
+ sys.exit(main())
tabs/__init__.py ADDED
@@ -0,0 +1,87 @@
1
+ """
2
+ 标签页模块 - 导出所有标签页组件
3
+ """
4
+
5
+ # 基础标签页
6
+ try:
7
+ from .base_tab import BaseTab
8
+ except ImportError:
9
+ BaseTab = None
10
+
11
+ # 简单标签页
12
+ try:
13
+ from .workflow_tab import WorkflowTab
14
+ except ImportError:
15
+ WorkflowTab = None
16
+
17
+ try:
18
+ from .taskflow_tab import TaskflowTab
19
+ except ImportError:
20
+ TaskflowTab = None
21
+
22
+ try:
23
+ from .new_work_tab import NewWorkTab
24
+ except ImportError:
25
+ NewWorkTab = None
26
+
27
+ # 复杂标签页 - 分别导入,避免一个失败影响其他
28
+ try:
29
+ from .chat_tab import ChatTab
30
+ except ImportError:
31
+ ChatTab = None
32
+
33
+ try:
34
+ from .stats_tab import StatsTab
35
+ except ImportError:
36
+ StatsTab = None
37
+
38
+ try:
39
+ from .memory_tab import MemoryTab
40
+ except ImportError:
41
+ MemoryTab = None
42
+
43
+ try:
44
+ from .rules_tab import RulesTab
45
+ except ImportError:
46
+ RulesTab = None
47
+
48
+ try:
49
+ from .todos_tab import TodosTab
50
+ except ImportError:
51
+ TodosTab = None
52
+
53
+ try:
54
+ from .checkpoints_tab import CheckpointsTab
55
+ except ImportError:
56
+ CheckpointsTab = None
57
+
58
+ try:
59
+ from .new_project_tab import NewProjectTab
60
+ except ImportError:
61
+ NewProjectTab = None
62
+
63
+ try:
64
+ from .workspace_tab import WorkspaceTab
65
+ except ImportError:
66
+ WorkspaceTab = None
67
+
68
+ try:
69
+ from .chat_history_tab import ChatHistoryTab
70
+ except ImportError:
71
+ ChatHistoryTab = None
72
+
73
+ __all__ = [
74
+ 'BaseTab',
75
+ 'WorkflowTab',
76
+ 'TaskflowTab',
77
+ 'NewWorkTab',
78
+ 'ChatTab',
79
+ 'ChatHistoryTab',
80
+ 'StatsTab',
81
+ 'MemoryTab',
82
+ 'RulesTab',
83
+ 'TodosTab',
84
+ 'CheckpointsTab',
85
+ 'NewProjectTab',
86
+ 'WorkspaceTab'
87
+ ]
tabs/base_tab.py ADDED
@@ -0,0 +1,34 @@
1
+ """
2
+ 选项卡基础类
3
+
4
+ 定义所有选项卡的通用接口和行为。
5
+ """
6
+
7
+ from abc import ABC, abstractmethod, ABCMeta
8
+ from PySide6.QtWidgets import QWidget
9
+ from PySide6.QtCore import QObject
10
+
11
+
12
+ class BaseTabMeta(type(QWidget), ABCMeta):
13
+ """解决QWidget和ABC的元类冲突"""
14
+ pass
15
+
16
+
17
+ class BaseTab(QWidget, ABC, metaclass=BaseTabMeta):
18
+ """选项卡基础类
19
+
20
+ 所有选项卡都应该继承此类,并实现_setup_ui方法。
21
+ """
22
+
23
+ def __init__(self, parent=None):
24
+ super().__init__(parent)
25
+ self._setup_ui()
26
+
27
+ @abstractmethod
28
+ def _setup_ui(self):
29
+ """子类必须实现的UI创建方法"""
30
+ pass
31
+
32
+ def refresh_data(self):
33
+ """刷新数据的通用方法,子类可以重写"""
34
+ pass
@@ -0,0 +1,66 @@
1
+ /* Chat History Tab Modern Styles */
2
+
3
+ /* Global Scroll Area */
4
+ QScrollArea {
5
+ border: none;
6
+ background-color: #1e1e1e;
7
+ }
8
+
9
+ QWidget#messagesContainer {
10
+ background-color: #1e1e1e;
11
+ }
12
+
13
+ /* -------------------------------------------------------------------------
14
+ Bubbles
15
+ ------------------------------------------------------------------------- */
16
+ QFrame {
17
+ border: none;
18
+ }
19
+
20
+ QFrame#userBubble {
21
+ background-color: #0e639c; /* Blue */
22
+ border-radius: 12px;
23
+ border-top-right-radius: 2px;
24
+ }
25
+
26
+ QFrame#aiBubble {
27
+ background-color: #252526; /* Dark Gray */
28
+ border: 1px solid #454545;
29
+ border-radius: 12px;
30
+ border-top-left-radius: 2px;
31
+ }
32
+
33
+ QFrame#agentBubble {
34
+ background-color: #2d2b38; /* Slight Purple/Dark */
35
+ border: 1px solid #453555;
36
+ border-radius: 12px;
37
+ border-top-left-radius: 2px;
38
+ }
39
+
40
+ /* -------------------------------------------------------------------------
41
+ Avatars
42
+ ------------------------------------------------------------------------- */
43
+ QLabel#avatarLabel {
44
+ font-size: 18px;
45
+ background-color: transparent;
46
+ padding: 4px;
47
+ }
48
+
49
+ /* -------------------------------------------------------------------------
50
+ Role Labels
51
+ ------------------------------------------------------------------------- */
52
+ QLabel#roleLabel {
53
+ font-size: 11px;
54
+ font-weight: bold;
55
+ color: #cccccc;
56
+ margin-bottom: 2px;
57
+ }
58
+
59
+ /* -------------------------------------------------------------------------
60
+ Empty State
61
+ ------------------------------------------------------------------------- */
62
+ QLabel#emptyStateLabel {
63
+ color: #6e6e6e;
64
+ font-size: 14px;
65
+ padding: 40px;
66
+ }