Convert to PySide6, add ecosystem installer and Docker testing

PySide6 Migration:
- Convert all Qt imports from PyQt6 to PySide6 (LGPL license)
- Replace pyqtSignal with Signal throughout codebase
- Unifies Qt library across ecosystem (ramble, artifact-editor)

New Features:
- Add Re-align Goals button to dashboard (launches Ramble for interview)
- ReAlignGoalsDialog now directly launches Ramble with questions as fields

Ecosystem Tools:
- Add bin/install-ecosystem script for unified installation
- Add Dockerfile for testing ecosystem installation
- Add docker-compose.yml for easy testing
- Add ramble and cmdforge as tracked dependencies

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
rob 2026-01-09 10:54:53 -04:00
parent 45f059647a
commit 3ecadb5e16
18 changed files with 590 additions and 120 deletions

94
Dockerfile Normal file
View File

@ -0,0 +1,94 @@
# Development Hub Ecosystem - Docker Build for Testing
#
# Tests that all ecosystem projects can be installed together.
# GUI functionality is not tested (no X11), only imports and CLI tools.
#
# Build: docker build -t development-hub .
# Test: docker run --rm development-hub
FROM python:3.12-slim
# Install system dependencies for Qt (even though we won't run GUI, imports need libs)
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
libgl1 \
libegl1 \
libxkbcommon0 \
libdbus-1-3 \
libxcb-cursor0 \
libxcb-icccm4 \
libxcb-keysyms1 \
libxcb-shape0 \
libxcb-xfixes0 \
libxcb-xinerama0 \
libxcb-render0 \
libxcb-render-util0 \
libxcb-image0 \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*
# Set up virtual display for Qt imports (no actual display, just for import)
ENV QT_QPA_PLATFORM=offscreen
WORKDIR /workspace
# Clone all repos
ARG GITEA_URL=https://gitea.brrd.tech/rob
RUN git clone ${GITEA_URL}/CmdForge.git CmdForge && \
git clone ${GITEA_URL}/ramble.git ramble && \
git clone ${GITEA_URL}/artifact-editor.git artifact-editor && \
git clone ${GITEA_URL}/orchestrated-discussions.git orchestrated-discussions && \
git clone ${GITEA_URL}/development-hub.git development-hub
# Apply patches for PySide6 unification (until changes are pushed to Gitea)
# Patch pyproject.toml files
RUN sed -i 's|PyQt6|PySide6|g' /workspace/development-hub/pyproject.toml && \
sed -i 's|file:///home/rob/PycharmProjects|file:///workspace|g' /workspace/development-hub/pyproject.toml && \
sed -i 's|PyQt6|PySide6|g' /workspace/artifact-editor/pyproject.toml
# Patch source files: PyQt6 -> PySide6
RUN find /workspace/development-hub/src -name "*.py" -exec sed -i \
-e 's/from PyQt6/from PySide6/g' \
-e 's/import PyQt6/import PySide6/g' \
-e 's/pyqtSignal/Signal/g' \
-e 's/pyqtSlot/Slot/g' {} \;
RUN find /workspace/artifact-editor/src -name "*.py" -exec sed -i \
-e 's/from PyQt6/from PySide6/g' \
-e 's/import PyQt6/import PySide6/g' \
-e 's/pyqtSignal/Signal/g' \
-e 's/pyqtSlot/Slot/g' {} \;
# Create venv and install
RUN python -m venv /venv
ENV PATH="/venv/bin:$PATH"
RUN pip install --upgrade pip wheel setuptools
# Install in dependency order (all patches applied above)
RUN pip install -e /workspace/CmdForge
RUN pip install -e /workspace/ramble
RUN pip install -e /workspace/artifact-editor
RUN pip install -e "/workspace/orchestrated-discussions[gui]"
RUN pip install -e /workspace/development-hub
# Verification script
RUN echo '#!/bin/bash\n\
set -e\n\
echo "Testing imports..."\n\
python -c "import cmdforge; print(\" cmdforge: OK\")"\n\
python -c "import ramble; print(\" ramble: OK\")"\n\
python -c "import artifact_editor; print(\" artifact_editor: OK\")"\n\
python -c "import discussions; print(\" discussions: OK\")"\n\
python -c "import development_hub; print(\" development_hub: OK\")"\n\
echo ""\n\
echo "Testing CLI tools..."\n\
cmdforge --help > /dev/null && echo " cmdforge CLI: OK"\n\
ramble --help > /dev/null && echo " ramble CLI: OK"\n\
artifact-editor --help > /dev/null && echo " artifact-editor CLI: OK"\n\
discussions --help > /dev/null && echo " discussions CLI: OK"\n\
echo ""\n\
echo "All tests passed!"\n\
' > /test.sh && chmod +x /test.sh
CMD ["/test.sh"]

132
bin/install-ecosystem Executable file
View File

@ -0,0 +1,132 @@
#!/bin/bash
# Development Ecosystem Installer
#
# Installs all projects in the development ecosystem in the correct order.
# Projects are cloned from Gitea and installed as editable packages.
#
# Usage:
# ./install-ecosystem # Fresh install to ~/PycharmProjects
# ./install-ecosystem --update # Update existing repos and reinstall
# ./install-ecosystem --help # Show this help
#
# Projects installed (in order):
# 1. CmdForge - AI-powered CLI tools
# 2. ramble - Voice note transcription
# 3. artifact-editor - Visual artifact editor
# 4. orchestrated-discussions - Multi-agent AI discussions
# 5. development-hub - Central orchestration GUI
set -e
GITEA_URL="https://gitea.brrd.tech/rob"
PROJECTS_DIR="${PROJECTS_DIR:-$HOME/PycharmProjects}"
VENV_DIR="${VENV_DIR:-$PROJECTS_DIR/development-hub/.venv}"
# Project definitions: name, repo_name, extras
PROJECTS=(
"CmdForge:CmdForge:"
"ramble:ramble:"
"artifact-editor:artifact-editor:"
"orchestrated-discussions:orchestrated-discussions:gui"
"development-hub:development-hub:"
)
show_help() {
head -20 "$0" | tail -n +2 | sed 's/^# //' | sed 's/^#//'
}
info() {
echo -e "\033[1;34m==>\033[0m \033[1m$1\033[0m"
}
success() {
echo -e "\033[1;32m==>\033[0m \033[1m$1\033[0m"
}
error() {
echo -e "\033[1;31m==>\033[0m \033[1m$1\033[0m" >&2
}
# Parse arguments
UPDATE_MODE=false
while [[ $# -gt 0 ]]; do
case $1 in
--update|-u)
UPDATE_MODE=true
shift
;;
--help|-h)
show_help
exit 0
;;
*)
error "Unknown option: $1"
exit 1
;;
esac
done
# Ensure projects directory exists
mkdir -p "$PROJECTS_DIR"
cd "$PROJECTS_DIR"
info "Installing development ecosystem to $PROJECTS_DIR"
# Clone or update repositories
for project_spec in "${PROJECTS[@]}"; do
IFS=':' read -r name repo extras <<< "$project_spec"
project_dir="$PROJECTS_DIR/$name"
if [[ -d "$project_dir" ]]; then
if [[ "$UPDATE_MODE" == "true" ]]; then
info "Updating $name..."
cd "$project_dir"
git fetch origin
git pull --ff-only origin main 2>/dev/null || git pull --ff-only origin master 2>/dev/null || true
cd "$PROJECTS_DIR"
else
info "$name already exists, skipping clone"
fi
else
info "Cloning $name..."
git clone "$GITEA_URL/$repo.git" "$name"
fi
done
# Create or activate virtual environment
if [[ ! -d "$VENV_DIR" ]]; then
info "Creating virtual environment at $VENV_DIR..."
python3 -m venv "$VENV_DIR"
fi
info "Activating virtual environment..."
source "$VENV_DIR/bin/activate"
# Upgrade pip
pip install --upgrade pip wheel setuptools -q
# Install projects in order
for project_spec in "${PROJECTS[@]}"; do
IFS=':' read -r name repo extras <<< "$project_spec"
project_dir="$PROJECTS_DIR/$name"
if [[ -n "$extras" ]]; then
info "Installing $name[$extras]..."
pip install -e "$project_dir[$extras]" -q
else
info "Installing $name..."
pip install -e "$project_dir" -q
fi
done
success "Installation complete!"
echo ""
echo "To use the ecosystem:"
echo " source $VENV_DIR/bin/activate"
echo " development-hub # Launch the GUI"
echo ""
echo "Individual tools:"
echo " cmdforge # AI-powered CLI tools"
echo " ramble # Voice note transcription"
echo " artifact-editor # Visual artifact editor"
echo " discussions # Multi-agent AI discussions"

23
docker-compose.yml Normal file
View File

@ -0,0 +1,23 @@
# Development Ecosystem Docker Compose
#
# Builds and tests the complete ecosystem installation.
#
# Usage:
# docker compose build # Build the test image
# docker compose up # Run installation tests
# docker compose down # Cleanup
services:
ecosystem-test:
build: .
container_name: development-hub-test
environment:
- QT_QPA_PLATFORM=offscreen
- DISPLAY=:99
# For local development testing, mount the local repos:
# volumes:
# - ./:/workspace/development-hub:ro
# - ../CmdForge:/workspace/CmdForge:ro
# - ../ramble:/workspace/ramble:ro
# - ../artifact-editor:/workspace/artifact-editor:ro
# - ../orchestrated-discussions:/workspace/orchestrated-discussions:ro

View File

@ -10,9 +10,11 @@ readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
dependencies = [
"PyQt6>=6.5.0",
"PySide6>=6.4.0",
"pyte>=0.8.0",
"orchestrated-discussions[gui] @ file:///home/rob/PycharmProjects/orchestrated-discussions",
"ramble @ file:///home/rob/PycharmProjects/ramble",
"cmdforge @ file:///home/rob/PycharmProjects/CmdForge",
]
[project.optional-dependencies]

View File

@ -3,8 +3,8 @@
import signal
import sys
from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QApplication
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication
from development_hub.main_window import MainWindow
from development_hub.styles import DARK_THEME

View File

@ -5,8 +5,8 @@ import subprocess
import shutil
from pathlib import Path
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QProcess
from PyQt6.QtWidgets import (
from PySide6.QtCore import Qt, QThread, Signal, QProcess, QTimer
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QDialog,
@ -36,8 +36,8 @@ from development_hub.settings import Settings
class RambleThread(QThread):
"""Thread to run Ramble subprocess."""
finished = pyqtSignal(dict) # Emits parsed result
error = pyqtSignal(str) # Emits error message
finished = Signal(dict) # Emits parsed result
error = Signal(str) # Emits error message
def __init__(self, prompt: str, fields: list[str], criteria: dict[str, str] = None):
super().__init__()
@ -97,8 +97,8 @@ class RambleThread(QThread):
class NewProjectThread(QThread):
"""Thread to run new-project script."""
output = pyqtSignal(str)
finished = pyqtSignal(bool, str) # success, message
output = Signal(str)
finished = Signal(bool, str) # success, message
def __init__(self, name: str, title: str, tagline: str, deploy: bool):
super().__init__()
@ -152,8 +152,8 @@ class NewProjectThread(QThread):
class DeployDocsThread(QThread):
"""Thread to run deploy docs script asynchronously."""
output = pyqtSignal(str)
finished = pyqtSignal(bool, str) # success, message
output = Signal(str)
finished = Signal(bool, str) # success, message
def __init__(self, project_key: str, project_title: str, docs_url: str):
super().__init__()
@ -198,7 +198,7 @@ class DeployDocsThread(QThread):
class UpdateDocsThread(QThread):
"""Thread to run update-docs CmdForge tool."""
finished = pyqtSignal(bool, str, str) # success, message, stderr
finished = Signal(bool, str, str) # success, message, stderr
def __init__(self, project_key: str):
super().__init__()
@ -237,8 +237,8 @@ class UpdateDocsThread(QThread):
class RebuildMainDocsThread(QThread):
"""Thread to refresh the main documentation site."""
output = pyqtSignal(str)
finished = pyqtSignal(bool, str) # success, message
output = Signal(str)
finished = Signal(bool, str) # success, message
def __init__(self):
super().__init__()
@ -301,7 +301,7 @@ class RebuildMainDocsThread(QThread):
class NewProjectDialog(QDialog):
"""Dialog for creating a new project."""
project_created = pyqtSignal(str) # Emits project name
project_created = Signal(str) # Emits project name
def __init__(self, parent=None):
super().__init__(parent)
@ -621,7 +621,7 @@ class SettingsDialog(QDialog):
class StandupDialog(QDialog):
"""Dialog for capturing daily standup progress."""
progress_saved = pyqtSignal(Path) # Emits path to saved file
progress_saved = Signal(Path) # Emits path to saved file
def __init__(self, parent=None):
super().__init__(parent)
@ -1182,3 +1182,204 @@ class DocsPreviewDialog(QDialog):
_, new_widget, _ = self._text_widgets[filename]
result[filename] = new_widget.toPlainText()
return result
class RealignGoalsThread(QThread):
"""Thread to run realign-goals CmdForge tool."""
finished = Signal(str) # Emits generated goals content
error = Signal(str) # Emits error message
def __init__(self, answers: dict, project_name: str = "global"):
super().__init__()
self.answers = answers
self.project_name = project_name
def run(self):
"""Run realign-goals tool."""
cmdforge_path = Path.home() / "PycharmProjects" / "CmdForge" / ".venv" / "bin" / "cmdforge"
if not cmdforge_path.exists():
cmdforge_path = shutil.which("cmdforge")
if not cmdforge_path:
self.error.emit("CmdForge not found")
return
# Format answers as input
input_text = json.dumps(self.answers, indent=2)
try:
result = subprocess.run(
[str(cmdforge_path), "run", "realign-goals", "--project", self.project_name],
input=input_text,
capture_output=True,
text=True,
timeout=120,
)
if result.returncode != 0:
self.error.emit(f"Error: {result.stderr or result.stdout}")
return
self.finished.emit(result.stdout)
except subprocess.TimeoutExpired:
self.error.emit("Tool timed out after 2 minutes")
except Exception as e:
self.error.emit(f"Error: {e}")
class ReAlignGoalsDialog(QDialog):
"""Dialog for re-aligning goals through Ramble interview.
Launches Ramble directly with interview questions as fields,
then generates new goals.md content and shows diff for approval.
"""
# Interview questions: (field_name, question_text, criteria_hint)
QUESTIONS = [
("motivation", "What drives you to build software?",
"solving problems, tools, automation, learning, etc."),
("common_thread", "What's the common thread in your projects?",
"problems or frustrations that led to building them"),
("finished", "How do you feel about 'finished' software?",
"complete and move on vs ongoing refinement"),
("audience", "Who are you building for?",
"yourself, developers, non-technical users, community"),
("polished", "What does 'polished' mean to you?",
"qualities that make software feel polished"),
("scope", "How do you feel about scope and focus?",
"build many things vs focus deeply"),
("priorities", "How do you decide what to work on next?",
"frustrations, closest to done, most impactful"),
("shipping", "What's your relationship with shipping?",
"polish forever vs push things out early"),
("ai_vision", "What's your vision for AI-assisted development?",
"tool, scope enabler, or something more"),
("done", "What does 'done' look like for a project?",
"is there a completion point"),
("principles", "What principles guide how you write code?",
"philosophies, patterns, practices"),
("success", "What does success look like in 2-3 years?",
"the picture if things go well"),
("avoid", "What do you want to avoid?",
"traps or patterns to stay away from"),
]
def __init__(self, goals_path: Path, project_name: str = "global", parent=None):
super().__init__(parent)
self.goals_path = goals_path
self.project_name = project_name
self._ramble_thread = None
self._realign_thread = None
self.setWindowTitle("Re-align Goals")
self.setFixedSize(400, 150)
self._setup_ui()
# Start Ramble immediately
QTimer.singleShot(100, self._start_ramble)
def _setup_ui(self):
"""Set up minimal progress UI."""
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)
self.status_label = QLabel("Launching Ramble...")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setStyleSheet("font-size: 14px; color: #e0e0e0;")
layout.addWidget(self.status_label)
self.progress = QProgressBar()
self.progress.setRange(0, 0) # Indeterminate
self.progress.setStyleSheet("""
QProgressBar {
border: 1px solid #3d3d3d;
border-radius: 4px;
text-align: center;
background-color: #2d2d2d;
}
QProgressBar::chunk {
background-color: #4a9eff;
}
""")
layout.addWidget(self.progress)
layout.addStretch()
cancel_btn = QPushButton("Cancel")
cancel_btn.clicked.connect(self.reject)
layout.addWidget(cancel_btn, alignment=Qt.AlignmentFlag.AlignCenter)
def _start_ramble(self):
"""Launch Ramble with interview questions as fields."""
self.status_label.setText("Ramble is open - answer the questions...")
# Build field names and criteria from questions
fields = [q[0] for q in self.QUESTIONS]
criteria = {q[0]: q[2] for q in self.QUESTIONS}
# Build hints from the question text
hints = [q[1] for q in self.QUESTIONS]
self._ramble_thread = RambleThread(
prompt=(
"Answer these questions about your development philosophy and goals. "
"Type or use voice input, then click Generate to fill in the fields."
),
fields=fields,
criteria=criteria,
)
self._ramble_thread.finished.connect(self._on_ramble_finished)
self._ramble_thread.error.connect(self._on_ramble_error)
self._ramble_thread.start()
def _on_ramble_finished(self, result: dict):
"""Handle Ramble completion - now generate goals."""
if not result or "fields" not in result:
self.reject()
return
answers = result["fields"]
if not any(v.strip() for v in answers.values()):
QMessageBox.warning(self, "No Answers", "Please answer at least some questions.")
self.reject()
return
self.status_label.setText("Generating goals from your answers...")
self._realign_thread = RealignGoalsThread(answers, self.project_name)
self._realign_thread.finished.connect(self._on_generate_finished)
self._realign_thread.error.connect(self._on_generate_error)
self._realign_thread.start()
def _on_ramble_error(self, error: str):
"""Handle Ramble error."""
QMessageBox.warning(self, "Ramble Error", error)
self.reject()
def _on_generate_finished(self, new_content: str):
"""Handle successful generation - show diff dialog."""
self.progress.setRange(0, 1)
self.progress.setValue(1)
# Read existing content
old_content = ""
if self.goals_path.exists():
old_content = self.goals_path.read_text()
# Show diff dialog
changes = {"goals.md": (old_content, new_content)}
preview = DocsPreviewDialog(changes, self)
if preview.exec() == QDialog.DialogCode.Accepted and preview.was_accepted():
selected = preview.get_selected_changes()
if "goals.md" in selected:
self.goals_path.write_text(selected["goals.md"])
QMessageBox.information(self, "Goals Updated", "Your goals have been updated.")
self.accept()
return
self.reject()
def _on_generate_error(self, error: str):
"""Handle generation error."""
QMessageBox.critical(self, "Generation Error", error)
self.reject()

View File

@ -3,9 +3,9 @@
import subprocess
from pathlib import Path
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QAction, QKeySequence
from PyQt6.QtWidgets import (
from PySide6.QtCore import Qt
from PySide6.QtGui import QAction, QKeySequence
from PySide6.QtWidgets import (
QLabel,
QMainWindow,
QSplitter,
@ -227,7 +227,7 @@ class MainWindow(QMainWindow):
def _launch_discussion(self):
"""Launch orchestrated-discussions UI in the current project directory."""
from PyQt6.QtWidgets import QMessageBox
from PySide6.QtWidgets import QMessageBox
# Get current project context from active pane
pane = self.workspace.get_active_pane()
@ -260,7 +260,7 @@ class MainWindow(QMainWindow):
def _launch_global_discussion(self):
"""Launch orchestrated-discussions UI in the root projects directory with new dialog."""
from PyQt6.QtWidgets import QMessageBox
from PySide6.QtWidgets import QMessageBox
projects_root = Path.home() / "PycharmProjects"
@ -345,7 +345,7 @@ class MainWindow(QMainWindow):
def _show_about(self):
"""Show about dialog."""
from PyQt6.QtWidgets import QMessageBox
from PySide6.QtWidgets import QMessageBox
QMessageBox.about(
self,

View File

@ -4,9 +4,9 @@ import subprocess
import webbrowser
from pathlib import Path
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import (
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QAction
from PySide6.QtWidgets import (
QDialog,
QLabel,
QListWidget,
@ -26,9 +26,9 @@ from development_hub.project_discovery import Project, discover_projects
class ProjectListWidget(QWidget):
"""Widget displaying the list of projects with context menu actions."""
project_selected = pyqtSignal(Project)
open_terminal_requested = pyqtSignal(Project)
open_dashboard_requested = pyqtSignal(Project)
project_selected = Signal(Project)
open_terminal_requested = Signal(Project)
open_dashboard_requested = Signal(Project)
def __init__(self, parent=None):
super().__init__(parent)
@ -267,7 +267,7 @@ class ProjectListWidget(QWidget):
elif clicked == commit_btn:
# Show commit dialog
from PyQt6.QtWidgets import QInputDialog
from PySide6.QtWidgets import QInputDialog
message, ok = QInputDialog.getText(
self,
"Commit Message",
@ -415,8 +415,8 @@ class ProjectListWidget(QWidget):
def _update_docs(self, project: Project):
"""Update documentation using CmdForge update-docs tool with preview."""
from PyQt6.QtWidgets import QMessageBox, QProgressDialog
from PyQt6.QtCore import Qt, QTimer
from PySide6.QtWidgets import QMessageBox, QProgressDialog
from PySide6.QtCore import Qt, QTimer
from pathlib import Path
# Confirm before starting
@ -513,7 +513,7 @@ class ProjectListWidget(QWidget):
def _on_update_docs_finished(self, success: bool, message: str, detail: str):
"""Handle update docs thread completion."""
from PyQt6.QtWidgets import QMessageBox
from PySide6.QtWidgets import QMessageBox
from pathlib import Path
# Stop timer

View File

@ -10,8 +10,8 @@ import termios
from pathlib import Path
import pyte
from PyQt6.QtCore import QThread, pyqtSignal, Qt, QTimer
from PyQt6.QtGui import (
from PySide6.QtCore import QThread, Signal, Qt, QTimer
from PySide6.QtGui import (
QFont,
QFontMetrics,
QColor,
@ -22,14 +22,14 @@ from PyQt6.QtGui import (
QDragEnterEvent,
QDropEvent,
)
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QScrollBar, QAbstractScrollArea
from PySide6.QtWidgets import QWidget, QVBoxLayout, QScrollBar, QAbstractScrollArea
class PtyReaderThread(QThread):
"""Thread that reads from PTY master and emits output."""
output_ready = pyqtSignal(bytes)
finished_signal = pyqtSignal()
output_ready = Signal(bytes)
finished_signal = Signal()
def __init__(self, master_fd: int):
super().__init__()
@ -98,8 +98,8 @@ BG_COLORS = {
class TerminalDisplay(QWidget):
"""Terminal display widget that renders a pyte screen."""
key_pressed = pyqtSignal(bytes)
cursor_position_requested = pyqtSignal()
key_pressed = Signal(bytes)
cursor_position_requested = Signal()
def __init__(self, rows: int = 24, cols: int = 80, parent=None):
super().__init__(parent)
@ -450,7 +450,7 @@ class TerminalDisplay(QWidget):
def contextMenuEvent(self, event):
"""Show context menu with copy/paste."""
from PyQt6.QtWidgets import QMenu, QApplication
from PySide6.QtWidgets import QMenu, QApplication
menu = QMenu(self)
@ -490,7 +490,7 @@ class TerminalDisplay(QWidget):
def _copy_selection(self):
"""Copy selected text to clipboard."""
from PyQt6.QtWidgets import QApplication
from PySide6.QtWidgets import QApplication
start, end = self._get_selection_bounds()
if start is None:
@ -513,7 +513,7 @@ class TerminalDisplay(QWidget):
def _paste_clipboard(self):
"""Paste clipboard content as keyboard input."""
from PyQt6.QtWidgets import QApplication
from PySide6.QtWidgets import QApplication
text = QApplication.clipboard().text()
if text:
@ -589,7 +589,7 @@ class TerminalDisplay(QWidget):
class TerminalWidget(QWidget):
"""Full terminal widget with PTY support using pyte emulation."""
closed = pyqtSignal()
closed = Signal()
def __init__(self, cwd: Path | None = None, parent=None):
super().__init__(parent)

View File

@ -6,8 +6,8 @@ from pathlib import Path
from development_hub.parsers.base import atomic_write
from PyQt6.QtCore import Qt, pyqtSignal, QFileSystemWatcher, QTimer, QThread, QObject
from PyQt6.QtWidgets import (
from PySide6.QtCore import Qt, Signal, QFileSystemWatcher, QTimer, QThread, QObject
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
@ -27,13 +27,14 @@ from development_hub.project_discovery import Project
from development_hub.services.health_checker import HealthChecker
from development_hub.parsers.progress_parser import ProgressLogManager
from development_hub.widgets.health_card import HealthCardCompact
from development_hub.dialogs import ReAlignGoalsDialog
class AuditWorker(QObject):
"""Background worker for running goals audit."""
finished = pyqtSignal(str, bool) # output, success
error = pyqtSignal(str)
finished = Signal(str, bool) # output, success
error = Signal(str)
def __init__(self, goals_content: str, project_path: Path | None = None):
super().__init__()
@ -117,9 +118,9 @@ class ProjectDashboard(QWidget):
- Today's progress
"""
terminal_requested = pyqtSignal(object) # Emits Project
project_selected = pyqtSignal(str) # Emits project_key (global mode only)
standup_requested = pyqtSignal() # Global mode only
terminal_requested = Signal(object) # Emits Project
project_selected = Signal(str) # Emits project_key (global mode only)
standup_requested = Signal() # Global mode only
def __init__(self, project: Project | None = None, parent: QWidget | None = None):
"""Initialize project dashboard.
@ -312,7 +313,7 @@ class ProjectDashboard(QWidget):
goals_header_layout.addWidget(goals_label)
# Goals progress bar
from PyQt6.QtWidgets import QProgressBar
from PySide6.QtWidgets import QProgressBar
self.goals_progress = QProgressBar()
self.goals_progress.setMinimum(0)
self.goals_progress.setMaximum(100)
@ -343,6 +344,11 @@ class ProjectDashboard(QWidget):
audit_goals_btn.clicked.connect(self._audit_goals)
goals_header_layout.addWidget(audit_goals_btn)
realign_goals_btn = QPushButton("Re-align")
realign_goals_btn.setStyleSheet(self._button_style())
realign_goals_btn.clicked.connect(self._realign_goals)
goals_header_layout.addWidget(realign_goals_btn)
edit_goals_btn = QPushButton("Edit")
edit_goals_btn.setStyleSheet(self._button_style())
edit_goals_btn.clicked.connect(self._open_goals)
@ -1592,7 +1598,7 @@ class ProjectDashboard(QWidget):
def _on_todo_start_discussion(self, todo):
"""Handle starting a discussion for a todo item."""
from PyQt6.QtWidgets import QMessageBox
from PySide6.QtWidgets import QMessageBox
# Cannot start discussion in global mode (no project context)
if self.is_global:
@ -2413,7 +2419,7 @@ class ProjectDashboard(QWidget):
"""Handle audit completion."""
import json
from datetime import datetime
from PyQt6.QtWidgets import QTextEdit, QDialogButtonBox
from PySide6.QtWidgets import QTextEdit, QDialogButtonBox
self._cleanup_audit()
@ -2660,6 +2666,18 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
goals_path = self._docs_root / "projects" / self.project.key / "goals.md"
self._open_file_in_editor(goals_path)
def _realign_goals(self):
"""Open the Re-align Goals dialog to regenerate goals from interview."""
if self.is_global:
goals_path = self._docs_root / "goals" / "goals.md"
project_name = "global"
else:
goals_path = self._docs_root / "projects" / self.project.key / "goals.md"
project_name = self.project.key
dialog = ReAlignGoalsDialog(goals_path, project_name, parent=self)
dialog.exec()
def _open_milestones(self):
"""Open the milestones.md file."""
if self.is_global:

View File

@ -1,7 +1,7 @@
"""Global dashboard view - wrapper around ProjectDashboard in global mode."""
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QWidget, QVBoxLayout
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QWidget, QVBoxLayout
from development_hub.views.dashboard import ProjectDashboard
@ -12,8 +12,8 @@ class GlobalDashboard(QWidget):
This is a thin wrapper around ProjectDashboard in global mode.
"""
project_selected = pyqtSignal(str) # Emits project_key
standup_requested = pyqtSignal()
project_selected = Signal(str) # Emits project_key
standup_requested = Signal()
def __init__(self, parent: QWidget | None = None):
"""Initialize global dashboard."""

View File

@ -1,8 +1,8 @@
"""Action menu dropdown widget (hamburger menu)."""
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import QToolButton, QMenu, QWidget
from PySide6.QtCore import Signal
from PySide6.QtGui import QAction
from PySide6.QtWidgets import QToolButton, QMenu, QWidget
class ActionMenu(QToolButton):
@ -17,14 +17,14 @@ class ActionMenu(QToolButton):
settings_requested: Emitted when Settings action is triggered
"""
dashboard_requested = pyqtSignal()
global_dashboard_requested = pyqtSignal()
terminal_requested = pyqtSignal()
launch_discussion_requested = pyqtSignal()
standup_requested = pyqtSignal()
update_docs_requested = pyqtSignal()
settings_requested = pyqtSignal()
close_pane_requested = pyqtSignal()
dashboard_requested = Signal()
global_dashboard_requested = Signal()
terminal_requested = Signal()
launch_discussion_requested = Signal()
standup_requested = Signal()
update_docs_requested = Signal()
settings_requested = Signal()
close_pane_requested = Signal()
def __init__(self, parent: QWidget | None = None):
"""Initialize action menu.

View File

@ -1,7 +1,7 @@
"""Collapsible section widget for dashboards."""
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import (
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
@ -23,7 +23,7 @@ class CollapsibleSection(QFrame):
toggled: Emitted when section is expanded/collapsed (bool expanded)
"""
toggled = pyqtSignal(bool)
toggled = Signal(bool)
def __init__(
self,
@ -179,10 +179,10 @@ class CollapsibleSection(QFrame):
class TodoItemWidget(QWidget):
"""Widget for displaying a single todo item with checkbox."""
toggled = pyqtSignal(object, bool) # Emits (todo, completed)
deleted = pyqtSignal(object) # Emits todo
start_discussion = pyqtSignal(object) # Emits todo
edited = pyqtSignal(object, str, str) # Emits (todo, old_text, new_text)
toggled = Signal(object, bool) # Emits (todo, completed)
deleted = Signal(object) # Emits todo
start_discussion = Signal(object) # Emits todo
edited = Signal(object, str, str) # Emits (todo, old_text, new_text)
def __init__(self, todo, parent: QWidget | None = None, show_priority: bool = False):
"""Initialize todo item widget.
@ -371,7 +371,7 @@ class TodoItemWidget(QWidget):
def eventFilter(self, obj, event):
"""Handle Escape key to cancel editing, focus out to save."""
from PyQt6.QtCore import QEvent
from PySide6.QtCore import QEvent
if obj == self._edit_widget:
if event.type() == QEvent.Type.KeyPress:
@ -416,9 +416,9 @@ class TodoItemWidget(QWidget):
class GoalItemWidget(QWidget):
"""Widget for displaying a single goal item with checkbox."""
toggled = pyqtSignal(object, bool, bool, bool) # Emits (goal, new_completed, was_completed, was_partial)
deleted = pyqtSignal(object) # Emits goal
edited = pyqtSignal(object, str, str) # Emits (goal, old_text, new_text)
toggled = Signal(object, bool, bool, bool) # Emits (goal, new_completed, was_completed, was_partial)
deleted = Signal(object) # Emits goal
edited = Signal(object, str, str) # Emits (goal, old_text, new_text)
def __init__(self, goal, parent: QWidget | None = None, is_non_goal: bool = False):
"""Initialize goal item widget.
@ -624,7 +624,7 @@ class GoalItemWidget(QWidget):
def eventFilter(self, obj, event):
"""Handle Escape key to cancel editing, focus out to save."""
from PyQt6.QtCore import QEvent
from PySide6.QtCore import QEvent
if obj == self._edit_widget:
if event.type() == QEvent.Type.KeyPress:
@ -641,8 +641,8 @@ class GoalItemWidget(QWidget):
class DeliverableItemWidget(QWidget):
"""Widget for displaying a single deliverable item with checkbox."""
toggled = pyqtSignal(object, bool) # Emits (deliverable, is_done)
deleted = pyqtSignal(object) # Emits deliverable
toggled = Signal(object, bool) # Emits (deliverable, is_done)
deleted = Signal(object) # Emits deliverable
def __init__(self, deliverable, parent: QWidget | None = None):
"""Initialize deliverable item widget.
@ -753,14 +753,14 @@ class MilestoneWidget(QFrame):
- Linked todos (preferred, when todos parameter is provided)
"""
deliverable_toggled = pyqtSignal(object, object, bool) # (milestone, deliverable, is_done)
deliverable_added = pyqtSignal(object, str) # (milestone, text)
deliverable_deleted = pyqtSignal(object, object) # (milestone, deliverable)
todo_toggled = pyqtSignal(object, bool) # (todo, completed) - for linked todos mode
todo_deleted = pyqtSignal(object) # (todo) - for linked todos mode
todo_added = pyqtSignal(str, str, str) # (text, priority, milestone_id) - for adding new todos
todo_start_discussion = pyqtSignal(object) # (todo) - for starting discussion from todo
todo_edited = pyqtSignal(object, str, str) # (todo, old_text, new_text) - for inline editing
deliverable_toggled = Signal(object, object, bool) # (milestone, deliverable, is_done)
deliverable_added = Signal(object, str) # (milestone, text)
deliverable_deleted = Signal(object, object) # (milestone, deliverable)
todo_toggled = Signal(object, bool) # (todo, completed) - for linked todos mode
todo_deleted = Signal(object) # (todo) - for linked todos mode
todo_added = Signal(str, str, str) # (text, priority, milestone_id) - for adding new todos
todo_start_discussion = Signal(object) # (todo) - for starting discussion from todo
todo_edited = Signal(object, str, str) # (todo, old_text, new_text) - for inline editing
def __init__(
self,

View File

@ -1,7 +1,7 @@
"""Project health card widget."""
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import QFrame, QHBoxLayout, QVBoxLayout, QLabel, QProgressBar
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QFrame, QHBoxLayout, QVBoxLayout, QLabel, QProgressBar
class HealthCard(QFrame):
@ -14,7 +14,7 @@ class HealthCard(QFrame):
- Todo count
"""
clicked = pyqtSignal(str) # Emits project_key when clicked
clicked = Signal(str) # Emits project_key when clicked
def __init__(self, parent=None):
"""Initialize health card.
@ -125,7 +125,7 @@ class HealthCard(QFrame):
class HealthCardCompact(QFrame):
"""Compact single-line health card."""
clicked = pyqtSignal(str)
clicked = Signal(str)
def __init__(self, parent=None):
"""Initialize compact health card."""

View File

@ -1,7 +1,7 @@
"""Milestone progress bar widget."""
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QProgressBar
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QProgressBar
class MilestoneProgressBar(QWidget):

View File

@ -1,7 +1,7 @@
"""Statistic card widget."""
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QFrame, QVBoxLayout, QLabel
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QFrame, QVBoxLayout, QLabel
class StatCard(QFrame):
@ -88,7 +88,7 @@ class StatCardRow(QFrame):
def __init__(self, parent=None):
"""Initialize stat card row."""
super().__init__(parent)
from PyQt6.QtWidgets import QHBoxLayout
from PySide6.QtWidgets import QHBoxLayout
self._layout = QHBoxLayout(self)
self._layout.setContentsMargins(0, 0, 0, 0)

View File

@ -1,7 +1,7 @@
"""Toast notification widget with undo/redo support."""
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton
from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton
class Toast(QWidget):
@ -13,9 +13,9 @@ class Toast(QWidget):
dismissed: Emitted when toast is dismissed (timeout or manual)
"""
undo_clicked = pyqtSignal()
redo_clicked = pyqtSignal()
dismissed = pyqtSignal()
undo_clicked = Signal()
redo_clicked = Signal()
dismissed = Signal()
def __init__(self, parent: QWidget | None = None):
"""Initialize toast widget.

View File

@ -3,8 +3,8 @@
from pathlib import Path
from weakref import ref
from PyQt6.QtCore import Qt, pyqtSignal, QEvent, QTimer
from PyQt6.QtWidgets import (
from PySide6.QtCore import Qt, Signal, QEvent, QTimer
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QSplitter,
@ -40,14 +40,14 @@ PANE_UNFOCUSED_STYLE = """
class PaneWidget(QFrame):
"""A pane containing a tab widget with terminals."""
clicked = pyqtSignal(object) # Emits self when clicked
empty = pyqtSignal(object) # Emits self when last tab closed
terminal_requested = pyqtSignal() # Request new terminal
standup_requested = pyqtSignal() # Request daily standup dialog
launch_discussion_requested = pyqtSignal() # Request launch discussion action
update_docs_requested = pyqtSignal() # Request docs update
settings_requested = pyqtSignal() # Request settings dialog
close_requested = pyqtSignal(object) # Emits self when close pane requested
clicked = Signal(object) # Emits self when clicked
empty = Signal(object) # Emits self when last tab closed
terminal_requested = Signal() # Request new terminal
standup_requested = Signal() # Request daily standup dialog
launch_discussion_requested = Signal() # Request launch discussion action
update_docs_requested = Signal() # Request docs update
settings_requested = Signal() # Request settings dialog
close_requested = Signal(object) # Emits self when close pane requested
def __init__(self, parent=None):
super().__init__(parent)
@ -362,12 +362,12 @@ class PaneWidget(QFrame):
class WorkspaceManager(QWidget):
"""Manages splittable panes, each with their own tab bar."""
pane_count_changed = pyqtSignal(int)
terminal_requested = pyqtSignal() # Request new terminal (no project context)
standup_requested = pyqtSignal() # Request daily standup dialog
launch_discussion_requested = pyqtSignal() # Request launch discussion action
update_docs_requested = pyqtSignal(str) # Request docs update with project key
settings_requested = pyqtSignal() # Request settings dialog
pane_count_changed = Signal(int)
terminal_requested = Signal() # Request new terminal (no project context)
standup_requested = Signal() # Request daily standup dialog
launch_discussion_requested = Signal() # Request launch discussion action
update_docs_requested = Signal(str) # Request docs update with project key
settings_requested = Signal() # Request settings dialog
def __init__(self, parent=None):
super().__init__(parent)