Add status polling and CLI status command for tool moderation

- Add QTimer polling every 30s when tools are pending/changes_requested
- Display changes_requested (⚠) and rejected (✗) states in tool tree
- Show moderator feedback in tool info panel
- Add `cmdforge registry status <tool>` CLI command with --sync flag
- Update _sync_tool to persist registry_feedback field

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-16 19:50:30 -04:00
co-authored by Claude Opus 4.5
parent cd4c0682e8
commit 64b9e8e70d
4 changed files with 255 additions and 6 deletions
+130 -2
View File
@@ -11,7 +11,7 @@ from PySide6.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QTextEdit, QLabel,
QPushButton, QGroupBox, QMessageBox, QFrame
)
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtCore import Qt, QThread, Signal, QTimer
from PySide6.QtGui import QFont, QColor, QBrush
from ...tool import (
@@ -68,9 +68,11 @@ class StatusSyncWorker(QThread):
status_data = client.get_my_tool_status(tool_name)
new_status = status_data.get("status", "pending")
new_hash = status_data.get("config_hash")
new_feedback = status_data.get("feedback")
old_status = config_data.get("registry_status", "pending")
old_hash = config_data.get("registry_hash")
old_feedback = config_data.get("registry_feedback")
changed = False
if old_status != new_status:
@@ -79,6 +81,12 @@ class StatusSyncWorker(QThread):
if new_hash and old_hash != new_hash:
config_data["registry_hash"] = new_hash
changed = True
if new_feedback != old_feedback:
if new_feedback:
config_data["registry_feedback"] = new_feedback
elif "registry_feedback" in config_data:
del config_data["registry_feedback"]
changed = True
if changed:
config_path.write_text(yaml.dump(config_data, default_flow_style=False, sort_keys=False))
@@ -93,6 +101,8 @@ def get_tool_publish_state(tool_name: str) -> Tuple[str, Optional[str]]:
Tuple of (state, registry_hash) where state is:
- "published" - approved in registry and current hash matches
- "pending" - submitted but awaiting moderation
- "changes_requested" - admin requested changes before approval
- "rejected" - rejected by admin
- "modified" - has registry_hash but current hash differs
- "local" - no registry_hash (never published/downloaded)
"""
@@ -123,8 +133,12 @@ def get_tool_publish_state(tool_name: str) -> Tuple[str, Optional[str]]:
# Hash matches - check moderation status
if registry_status == "approved":
return ("published", registry_hash)
elif registry_status == "changes_requested":
return ("changes_requested", registry_hash)
elif registry_status == "rejected":
return ("rejected", registry_hash)
else:
# pending, rejected, or unknown
# pending or unknown
return ("pending", registry_hash)
except Exception:
return ("local", None)
@@ -139,6 +153,8 @@ class ToolsPage(QWidget):
self._current_tool = None
self._sync_worker = None
self._syncing = False # Prevent re-sync during update
self._poll_timer = None # Timer for automatic status polling
self._has_pending_tools = False # Track if we need to poll
self._setup_ui()
self.refresh()
@@ -269,6 +285,7 @@ class ToolsPage(QWidget):
self.tool_tree.clear()
self._current_tool = None
self.info_text.clear()
self._has_pending_tools = False # Reset, will be set during tree building
tools = list_tools()
@@ -301,6 +318,10 @@ class ToolsPage(QWidget):
# Get publish state
state, registry_hash = get_tool_publish_state(name)
# Track if we have pending tools for polling
if state in ("pending", "changes_requested"):
self._has_pending_tools = True
# Show state indicator in display name
if state == "published":
display_name = f"{name} ✓"
@@ -310,6 +331,14 @@ class ToolsPage(QWidget):
display_name = f"{name} ◐"
tooltip = "Submitted to registry - pending review"
color = QColor(214, 158, 46) # Yellow/amber
elif state == "changes_requested":
display_name = f"{name} ⚠"
tooltip = "Changes requested - see feedback"
color = QColor(245, 158, 11) # Orange/amber
elif state == "rejected":
display_name = f"{name} ✗"
tooltip = "Rejected by moderator"
color = QColor(220, 38, 38) # Red
elif state == "modified":
display_name = f"{name} ●"
tooltip = "Published to registry - local modifications"
@@ -346,6 +375,9 @@ class ToolsPage(QWidget):
# Start background sync for published tools
self._start_background_sync(tools)
# Manage polling timer based on pending state
self._manage_poll_timer()
def _start_background_sync(self, tools: list):
"""Start background sync for tools that have been published."""
if self._syncing:
@@ -385,6 +417,51 @@ class ToolsPage(QWidget):
"""Handle background sync completion."""
self._syncing = False
def _manage_poll_timer(self):
"""Start or stop the polling timer based on pending tools."""
config = load_config()
should_poll = self._has_pending_tools and config.registry.token
if should_poll:
if not self._poll_timer:
# Create timer that polls every 30 seconds
self._poll_timer = QTimer(self)
self._poll_timer.timeout.connect(self._poll_status)
if not self._poll_timer.isActive():
self._poll_timer.start(30000) # 30 seconds
else:
# No pending tools, stop polling
if self._poll_timer and self._poll_timer.isActive():
self._poll_timer.stop()
def _poll_status(self):
"""Timer callback to poll status for pending tools."""
if self._syncing:
return # Already syncing
# Get list of tools with pending status
tools = list_tools()
pending_tools = []
for name in tools:
config_path = get_tools_dir() / name / "config.yaml"
if config_path.exists():
try:
config_data = yaml.safe_load(config_path.read_text()) or {}
status = config_data.get("registry_status")
if status in ("pending", "changes_requested") and config_data.get("registry_hash"):
pending_tools.append(name)
except Exception:
pass
if pending_tools:
self._start_background_sync(pending_tools)
else:
# No more pending tools, stop timer
if self._poll_timer and self._poll_timer.isActive():
self._poll_timer.stop()
self._has_pending_tools = False
def _on_tool_status_updated(self, tool_name: str):
"""Handle background sync updating a tool's status."""
# Refresh the display - _syncing flag prevents re-triggering sync
@@ -420,6 +497,17 @@ class ToolsPage(QWidget):
if tool_name:
self._edit_tool()
def _get_tool_feedback(self, tool_name: str) -> Optional[str]:
"""Get the feedback for a tool from its config."""
config_path = get_tools_dir() / tool_name / "config.yaml"
if config_path.exists():
try:
config_data = yaml.safe_load(config_path.read_text()) or {}
return config_data.get("registry_feedback")
except Exception:
pass
return None
def _show_tool_info(self, tool: Tool):
"""Display tool information."""
lines = []
@@ -443,6 +531,36 @@ class ToolsPage(QWidget):
"border-radius: 4px; margin-bottom: 12px; font-size: 12px;'>"
"◐ Submitted to registry - pending review</p>"
)
elif state == "changes_requested":
lines.append(
"<p style='background: #fef3c7; color: #92400e; padding: 6px 10px; "
"border-radius: 4px; margin-bottom: 8px; font-size: 12px;'>"
"⚠ Changes requested - please address feedback and republish</p>"
)
# Show feedback if available
feedback = self._get_tool_feedback(tool.name)
if feedback:
feedback_escaped = feedback.replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<br>")
lines.append(
f"<div style='background: #fff7ed; border-left: 3px solid #f59e0b; "
f"padding: 8px 12px; margin-bottom: 12px; font-size: 12px;'>"
f"<strong>Feedback:</strong><br>{feedback_escaped}</div>"
)
elif state == "rejected":
lines.append(
"<p style='background: #fee2e2; color: #991b1b; padding: 6px 10px; "
"border-radius: 4px; margin-bottom: 8px; font-size: 12px;'>"
"✗ Rejected by moderator</p>"
)
# Show feedback if available
feedback = self._get_tool_feedback(tool.name)
if feedback:
feedback_escaped = feedback.replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<br>")
lines.append(
f"<div style='background: #fef2f2; border-left: 3px solid #dc2626; "
f"padding: 8px 12px; margin-bottom: 12px; font-size: 12px;'>"
f"<strong>Reason:</strong><br>{feedback_escaped}</div>"
)
elif state == "modified":
lines.append(
"<p style='background: #feebc8; color: #c05621; padding: 6px 10px; "
@@ -592,11 +710,13 @@ class ToolsPage(QWidget):
status_data = client.get_my_tool_status(tool_name)
new_status = status_data.get("status", "pending")
new_hash = status_data.get("config_hash")
new_feedback = status_data.get("feedback")
# Update local config
config_data = yaml.safe_load(config_path.read_text()) or {}
old_status = config_data.get("registry_status", "pending")
old_hash = config_data.get("registry_hash")
old_feedback = config_data.get("registry_feedback")
changed = False
if old_status != new_status:
@@ -605,6 +725,12 @@ class ToolsPage(QWidget):
if new_hash and old_hash != new_hash:
config_data["registry_hash"] = new_hash
changed = True
if new_feedback != old_feedback:
if new_feedback:
config_data["registry_feedback"] = new_feedback
elif "registry_feedback" in config_data:
del config_data["registry_feedback"]
changed = True
if changed:
config_path.write_text(yaml.dump(config_data, default_flow_style=False, sort_keys=False))
@@ -613,6 +739,8 @@ class ToolsPage(QWidget):
self.main_window.show_status(f"Tool '{tool_name}' has been approved!")
elif new_status == "rejected":
self.main_window.show_status(f"Tool '{tool_name}' was rejected")
elif new_status == "changes_requested":
self.main_window.show_status(f"Changes requested for '{tool_name}' - see feedback")
else:
self.main_window.show_status(f"Status updated: {new_status}")
else: