diff --git a/CLAUDE.md b/CLAUDE.md index f3e18c8..1a7f2a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,6 +55,7 @@ src/development_hub/ - **Drag & Drop**: Drop files/folders into terminal to inject paths - **Session Persistence**: Remembers pane layout and open terminals - **New Project Dialog**: Integrates with Ramble for voice input +- **Progress Reports**: Export weekly progress summaries from daily standups ### Keyboard Shortcuts @@ -71,6 +72,7 @@ src/development_hub/ | `Ctrl+B` | Toggle project panel | | `Ctrl+N` | New project dialog | | `Ctrl+D` | New discussion | +| `Ctrl+R` | Weekly progress report | | `Ctrl+Z` | Undo (in dashboard) | | `Ctrl+Shift+Z` | Redo (in dashboard) | | `F5` | Refresh project list | diff --git a/src/development_hub/dialogs.py b/src/development_hub/dialogs.py index 1fd37de..934cd7c 100644 --- a/src/development_hub/dialogs.py +++ b/src/development_hub/dialogs.py @@ -1611,3 +1611,241 @@ class ReAlignGoalsDialog(QDialog): """Handle generation error.""" QMessageBox.critical(self, "Generation Error", error) self.reject() + + +class WeeklyReportDialog(QDialog): + """Dialog for generating and exporting weekly progress reports.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Weekly Progress Report") + self.setMinimumWidth(700) + self.setMinimumHeight(500) + self._setup_ui() + self._generate_report() + + def _setup_ui(self): + """Set up the dialog UI.""" + layout = QVBoxLayout(self) + + # Header with date range + from datetime import date, timedelta + today = date.today() + week_ago = today - timedelta(days=6) + header = QLabel( + f"

Progress Report

" + f"

{week_ago.strftime('%B %d')} - {today.strftime('%B %d, %Y')}

" + ) + layout.addWidget(header) + + # Days selector + days_layout = QHBoxLayout() + days_layout.addWidget(QLabel("Include last:")) + self.days_combo = QComboBox() + self.days_combo.addItems(["7 days", "14 days", "30 days"]) + self.days_combo.currentIndexChanged.connect(self._generate_report) + days_layout.addWidget(self.days_combo) + days_layout.addStretch() + layout.addLayout(days_layout) + + # Report content + self.report_text = QPlainTextEdit() + self.report_text.setReadOnly(True) + self.report_text.setStyleSheet(""" + QPlainTextEdit { + background-color: #1e1e1e; + color: #d4d4d4; + font-family: monospace; + font-size: 12px; + border: 1px solid #3d3d3d; + border-radius: 4px; + padding: 8px; + } + """) + layout.addWidget(self.report_text) + + # Button row + btn_layout = QHBoxLayout() + + copy_btn = QPushButton("Copy to Clipboard") + copy_btn.clicked.connect(self._copy_to_clipboard) + btn_layout.addWidget(copy_btn) + + btn_layout.addStretch() + + close_btn = QPushButton("Close") + close_btn.clicked.connect(self.accept) + btn_layout.addWidget(close_btn) + + layout.addLayout(btn_layout) + + def _get_days(self) -> int: + """Get selected number of days.""" + text = self.days_combo.currentText() + if "14" in text: + return 14 + elif "30" in text: + return 30 + return 7 + + def _generate_report(self): + """Generate the weekly report from progress logs.""" + from datetime import date, timedelta + from development_hub.parsers.progress_parser import ProgressLogManager + + days = self._get_days() + progress_dir = Path.home() / "PycharmProjects" / "project-docs" / "docs" / "progress" + + if not progress_dir.exists(): + self.report_text.setPlainText( + "No progress directory found.\n\n" + f"Expected: {progress_dir}\n\n" + "Use the Daily Standup feature to create progress entries." + ) + return + + manager = ProgressLogManager(progress_dir) + entries = manager.get_recent(days) + + if not entries: + self.report_text.setPlainText( + f"No progress entries found for the last {days} days.\n\n" + "Use the Daily Standup feature (Terminal menu > Standup) to log daily progress." + ) + return + + # Build report + report = self._build_report(entries, days) + self.report_text.setPlainText(report) + + def _build_report(self, entries: list, days: int) -> str: + """Build markdown report from progress entries.""" + from datetime import date, timedelta + from collections import defaultdict + + today = date.today() + start_date = today - timedelta(days=days - 1) + + lines = [ + f"# Progress Report", + f"**{start_date.strftime('%B %d')} - {today.strftime('%B %d, %Y')}**", + f"", + f"---", + f"", + ] + + # Aggregate data + all_projects = set() + all_completed = [] + all_in_progress = [] + all_blocked = [] + focus_areas = [] + + for entry in entries: + all_projects.update(entry.projects) + + for item in entry.completed: + all_completed.append((entry.date, item)) + + for item in entry.in_progress: + if item not in [i for _, i in all_completed]: # Skip if already completed + all_in_progress.append((entry.date, item)) + + for blocked in entry.blocked: + all_blocked.append((entry.date, blocked)) + + if entry.focus_primary: + focus_areas.append((entry.date, entry.focus_primary)) + + # Summary stats + lines.append("## Summary") + lines.append("") + lines.append(f"- **Days logged:** {len(entries)}") + lines.append(f"- **Projects touched:** {', '.join(sorted(all_projects)) if all_projects else 'None'}") + lines.append(f"- **Tasks completed:** {len(all_completed)}") + lines.append(f"- **Currently in progress:** {len(set(i for _, i in all_in_progress))}") + if all_blocked: + lines.append(f"- **Blockers:** {len(all_blocked)}") + lines.append("") + + # Focus areas + if focus_areas: + lines.append("## Focus Areas") + lines.append("") + seen = set() + for date_str, focus in focus_areas: + if focus not in seen: + lines.append(f"- {focus}") + seen.add(focus) + lines.append("") + + # Completed items + if all_completed: + lines.append("## Completed") + lines.append("") + + # Group by date + by_date = defaultdict(list) + for date_str, item in all_completed: + by_date[date_str].append(item) + + for date_str in sorted(by_date.keys(), reverse=True): + lines.append(f"### {date_str}") + for item in by_date[date_str]: + lines.append(f"- [x] {item}") + lines.append("") + + # In Progress + if all_in_progress: + lines.append("## In Progress") + lines.append("") + seen = set() + for _, item in all_in_progress: + if item not in seen: + lines.append(f"- [ ] {item}") + seen.add(item) + lines.append("") + + # Blockers + if all_blocked: + lines.append("## Blockers") + lines.append("") + for date_str, (issue, project, waiting) in all_blocked: + line = f"- **{issue}**" + if project: + line += f" ({project})" + if waiting: + line += f" - waiting on: {waiting}" + lines.append(line) + lines.append("") + + # Daily breakdown + lines.append("---") + lines.append("") + lines.append("## Daily Log") + lines.append("") + + for entry in sorted(entries, key=lambda e: e.date, reverse=True): + lines.append(f"### {entry.date}") + if entry.focus_primary: + lines.append(f"**Focus:** {entry.focus_primary}") + if entry.completed: + lines.append(f"**Completed:** {len(entry.completed)} items") + if entry.notes: + lines.append(f"**Notes:** {entry.notes[:100]}{'...' if len(entry.notes) > 100 else ''}") + lines.append("") + + return "\n".join(lines) + + def _copy_to_clipboard(self): + """Copy report to clipboard.""" + from PySide6.QtWidgets import QApplication + + QApplication.clipboard().setText(self.report_text.toPlainText()) + + # Show brief confirmation + QMessageBox.information( + self, + "Copied", + "Report copied to clipboard!" + ) diff --git a/src/development_hub/main_window.py b/src/development_hub/main_window.py index fa3f084..8d6cb74 100644 --- a/src/development_hub/main_window.py +++ b/src/development_hub/main_window.py @@ -17,7 +17,7 @@ from PySide6.QtWidgets import ( from development_hub.project_discovery import Project from development_hub.project_list import ProjectListWidget from development_hub.workspace import WorkspaceManager -from development_hub.dialogs import NewProjectDialog, SettingsDialog, StandupDialog +from development_hub.dialogs import NewProjectDialog, SettingsDialog, StandupDialog, WeeklyReportDialog from development_hub.settings import Settings @@ -166,6 +166,14 @@ class MainWindow(QMainWindow): prev_pane.triggered.connect(self.workspace.focus_previous_pane) view_menu.addAction(prev_pane) + # Reports menu + reports_menu = menubar.addMenu("&Reports") + + weekly_report = QAction("&Weekly Progress Report...", self) + weekly_report.setShortcut(QKeySequence("Ctrl+R")) + weekly_report.triggered.connect(self._show_weekly_report) + reports_menu.addAction(weekly_report) + # Terminal menu terminal_menu = menubar.addMenu("&Terminal") @@ -225,6 +233,11 @@ class MainWindow(QMainWindow): dialog = StandupDialog(self) dialog.exec() + def _show_weekly_report(self): + """Show weekly progress report dialog.""" + dialog = WeeklyReportDialog(self) + dialog.exec() + def _launch_discussion(self): """Launch orchestrated-discussions UI in the current project directory.""" from PySide6.QtWidgets import QMessageBox diff --git a/src/development_hub/terminal_widget.py b/src/development_hub/terminal_widget.py index 57448d8..c7d5a16 100644 --- a/src/development_hub/terminal_widget.py +++ b/src/development_hub/terminal_widget.py @@ -329,6 +329,26 @@ class TerminalDisplay(QWidget): return self._color_to_qcolor(char.bg, default_fg=False) + def event(self, event): + """Override event to intercept Tab before Qt's focus handling.""" + if event.type() == event.Type.KeyPress: + key = event.key() + modifiers = event.modifiers() + # Intercept Tab and Shift+Tab before Qt uses them for focus navigation + if key == Qt.Key.Key_Tab: + if modifiers == Qt.KeyboardModifier.ShiftModifier: + # Shift+Tab (backtab) + self.key_pressed.emit(b'\x1b[Z') + else: + # Regular Tab + self.key_pressed.emit(b'\t') + return True + elif key == Qt.Key.Key_Backtab: + # Backtab (some systems report this instead of Shift+Tab) + self.key_pressed.emit(b'\x1b[Z') + return True + return super().event(event) + def keyPressEvent(self, event: QKeyEvent): """Handle keyboard input and send to PTY.""" key = event.key() @@ -390,9 +410,7 @@ class TerminalDisplay(QWidget): elif key == Qt.Key.Key_Backspace: self.key_pressed.emit(b'\x7f') return - elif key == Qt.Key.Key_Tab: - self.key_pressed.emit(b'\t') - return + # Tab is handled in event() to intercept before Qt's focus navigation elif key == Qt.Key.Key_Return or key == Qt.Key.Key_Enter: self.key_pressed.emit(b'\r') return diff --git a/src/development_hub/views/dashboard.py b/src/development_hub/views/dashboard.py index ec32acf..563afd4 100644 --- a/src/development_hub/views/dashboard.py +++ b/src/development_hub/views/dashboard.py @@ -2445,14 +2445,26 @@ class ProjectDashboard(QWidget): # Try to parse as JSON try: - # Clean up output - remove any markdown code blocks + # Clean up output - remove thinking tags and markdown code blocks json_str = output.strip() + + # Remove thinking output (e.g., from extended thinking models) + if "" in json_str: + json_str = json_str.split("")[-1].strip() + + # Remove markdown code blocks if json_str.startswith("```"): json_str = json_str.split("```")[1] if json_str.startswith("json"): json_str = json_str[4:] json_str = json_str.strip() + # Find the JSON object by locating first { and last } + first_brace = json_str.find("{") + last_brace = json_str.rfind("}") + if first_brace != -1 and last_brace != -1 and last_brace > first_brace: + json_str = json_str[first_brace:last_brace + 1] + audit_data = json.loads(json_str) # Update the goals.md file with new checkbox states