From f2cb9c0057833d6f24ddb5800d4fb103e6ea0544 Mon Sep 17 00:00:00 2001 From: rob Date: Sun, 1 Feb 2026 01:26:56 -0400 Subject: [PATCH] Add collapsible README viewer to tool details panel and owner override for publish - Add collapsible README section to tools page detail panel with lazy loading - README loads from disk only when user clicks the toggle bar - Section collapses to a single header bar, expands to show content - Add --owner flag to registry publish command for admin use - Simplify dependency gathering in publish to use unified code path Co-Authored-By: Claude Opus 4.5 --- src/cmdforge/cli/__init__.py | 1 + src/cmdforge/cli/registry_commands.py | 32 +++++------ src/cmdforge/gui/pages/tools_page.py | 77 +++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 16 deletions(-) diff --git a/src/cmdforge/cli/__init__.py b/src/cmdforge/cli/__init__.py index c608afd..8a50847 100644 --- a/src/cmdforge/cli/__init__.py +++ b/src/cmdforge/cli/__init__.py @@ -187,6 +187,7 @@ def main(): p_reg_publish.add_argument("path", nargs="?", help="Path to tool directory (default: current dir)") p_reg_publish.add_argument("--dry-run", action="store_true", help="Validate without publishing") p_reg_publish.add_argument("-f", "--force", action="store_true", help="Skip confirmation prompts") + p_reg_publish.add_argument("--owner", default="", help="Owner override (admin only, e.g. 'official')") p_reg_publish.set_defaults(func=cmd_registry) # registry my-tools diff --git a/src/cmdforge/cli/registry_commands.py b/src/cmdforge/cli/registry_commands.py index 499eaa9..63459a1 100644 --- a/src/cmdforge/cli/registry_commands.py +++ b/src/cmdforge/cli/registry_commands.py @@ -456,21 +456,20 @@ def _cmd_registry_publish(args): my_owner = None tool = load_tool(name) - if tool: - dep_result = gather_local_unpublished_deps([name], client, my_owner) - else: - # Tool isn't installed locally; derive deps from config.yaml - temp_tool = Tool.from_dict(data) - dep_names = [] - for dep in temp_tool.dependencies: - if '/' not in dep: - dep_names.append(dep) - for step in temp_tool.steps: - if isinstance(step, ToolStep) and '/' not in step.tool: - dep_names.append(step.tool) - dep_names = sorted(set(dep_names)) - if dep_names: - dep_result = gather_local_unpublished_deps(dep_names, client, my_owner) + if not tool: + # Tool isn't installed locally; derive from config.yaml + tool = Tool.from_dict(data) + + dep_names = [] + for dep in tool.dependencies: + if '/' not in dep: + dep_names.append(dep) + for step in tool.steps: + if isinstance(step, ToolStep) and '/' not in step.tool: + dep_names.append(step.tool) + dep_names = sorted(set(dep_names) - {name}) + if dep_names: + dep_result = gather_local_unpublished_deps(dep_names, client, my_owner) except Exception as e: print(f"Warning: Could not check dependencies: {e}", file=sys.stderr) dep_result = None @@ -560,7 +559,8 @@ def _cmd_registry_publish(args): try: client = get_client() - result = client.publish_tool(config_yaml, readme, defaults) + owner = getattr(args, "owner", "") + result = client.publish_tool(config_yaml, readme, defaults, owner=owner) pr_url = result.get("pr_url", "") status = result.get("status", "") diff --git a/src/cmdforge/gui/pages/tools_page.py b/src/cmdforge/gui/pages/tools_page.py index d7e2242..6c6c1f0 100644 --- a/src/cmdforge/gui/pages/tools_page.py +++ b/src/cmdforge/gui/pages/tools_page.py @@ -365,6 +365,7 @@ class ToolsPage(QWidget): self._issue_worker = None self._my_slug: Optional[str] = None # Cached current user slug self._my_slug_fetched = False + self._readme_loaded_for: Optional[str] = None # Track which tool's README is loaded self._setup_ui() self.refresh() @@ -452,6 +453,37 @@ class ToolsPage(QWidget): rating_bar_layout.addWidget(self.btn_report_issue) self.rating_bar.setVisible(False) + + # Collapsible README section + self.readme_container = QWidget() + readme_container_layout = QVBoxLayout(self.readme_container) + readme_container_layout.setContentsMargins(0, 8, 0, 0) + readme_container_layout.setSpacing(0) + + self.readme_toggle = QPushButton("▶ README") + self.readme_toggle.setStyleSheet( + "QPushButton { text-align: left; padding: 6px 10px; " + "background: #edf2f7; border: 1px solid #e2e8f0; border-radius: 4px; " + "color: #4a5568; font-weight: 600; font-size: 12px; }" + "QPushButton:hover { background: #e2e8f0; }" + ) + self.readme_toggle.setCursor(Qt.PointingHandCursor) + self.readme_toggle.clicked.connect(self._on_readme_toggled) + readme_container_layout.addWidget(self.readme_toggle) + + self.readme_text = QTextEdit() + self.readme_text.setReadOnly(True) + self.readme_text.setMinimumHeight(200) + self.readme_text.setStyleSheet( + "QTextEdit { font-family: monospace; font-size: 12px; " + "border: 1px solid #e2e8f0; border-top: none; border-radius: 0 0 4px 4px; }" + ) + self.readme_text.setVisible(False) + readme_container_layout.addWidget(self.readme_text) + + self.readme_container.setVisible(False) + info_layout.addWidget(self.readme_container) + info_layout.addWidget(self.rating_bar) right_layout.addWidget(info_box, 1) @@ -527,6 +559,7 @@ class ToolsPage(QWidget): self._current_tool = None self.info_text.clear() self.rating_bar.setVisible(False) + self.readme_container.setVisible(False) self._has_pending_tools = False # Reset, will be set during tree building tools = list_tools() @@ -724,6 +757,7 @@ class ToolsPage(QWidget): self._current_tool = None self.info_text.clear() self.rating_bar.setVisible(False) + self.readme_container.setVisible(False) self._update_buttons() return @@ -871,6 +905,49 @@ class ToolsPage(QWidget): # Update rating bar below the detail text self._update_rating_bar(qname) + # Reset README section (collapsed, visible, lazy-loaded on expand) + self._current_tool = tool + self._readme_loaded_for = None + self.readme_text.clear() + self.readme_text.setVisible(False) + self.readme_toggle.setText("▶ README") + self.readme_container.setVisible(tool.path is not None) + + def _on_readme_toggled(self): + """Toggle README section visibility and lazy-load content on first expand.""" + expanding = not self.readme_text.isVisible() + self.readme_text.setVisible(expanding) + self.readme_toggle.setText("▼ README" if expanding else "▶ README") + + if not expanding: + return + + tool = self._current_tool + if not tool or not tool.path: + return + + # Avoid reloading if already loaded for this tool + if self._readme_loaded_for == tool.name: + return + + readme_path = tool.path.parent / "README.md" + if readme_path.exists(): + try: + content = readme_path.read_text() + escaped = content.replace("&", "&").replace("<", "<").replace(">", ">") + self.readme_text.setHtml( + f"
{escaped}
" + ) + except Exception as e: + self.readme_text.setPlainText(f"Error reading README: {e}") + else: + self.readme_text.setHtml( + "

No README.md found for this tool.

" + ) + + self._readme_loaded_for = tool.name + def _update_rating_bar(self, qname: str): """Update the rating bar widget at the bottom of the detail panel.""" registry_info = get_tool_registry_info(qname, self._my_slug)