Add weekly progress report and fix terminal Tab key
- Add Reports menu with Weekly Progress Report dialog (Ctrl+R) - Report aggregates daily standups into summary with completed items, in-progress work, blockers, and focus areas - Support 7/14/30 day ranges with copy-to-clipboard - Fix Tab key in terminal widget by intercepting in event() before Qt's focus navigation consumes it - Add Shift+Tab (backtab) support for reverse completion - Improve AI output parsing in dashboard to handle extended thinking Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a6937463d0
commit
ce608957e5
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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"<h2>Progress Report</h2>"
|
||||
f"<p style='color: #888;'>{week_ago.strftime('%B %d')} - {today.strftime('%B %d, %Y')}</p>"
|
||||
)
|
||||
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!"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 "</think>" in json_str:
|
||||
json_str = json_str.split("</think>")[-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
|
||||
|
|
|
|||
Loading…
Reference in New Issue