@pennyfarthing/core 11.3.8 → 11.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +4 -1
- package/packages/core/dist/public/css/react.css +1 -1
- package/packages/core/dist/public/js/react/react.js +24 -24
- package/packages/core/src/public/App.tsx +356 -0
- package/packages/core/src/public/components/AgentLoadDialog.tsx +202 -0
- package/packages/core/src/public/components/AgentPopup.tsx +308 -0
- package/packages/core/src/public/components/ApprovalModal/ApprovalModal.css +35 -0
- package/packages/core/src/public/components/ApprovalModal/index.tsx +632 -0
- package/packages/core/src/public/components/BikeRackIndex.tsx +53 -0
- package/packages/core/src/public/components/BikeRackWorkspace.tsx +217 -0
- package/packages/core/src/public/components/CommandPalette.tsx +554 -0
- package/packages/core/src/public/components/ConfirmDialog.tsx +168 -0
- package/packages/core/src/public/components/ContextIndicator/ContextIndicator.css +85 -0
- package/packages/core/src/public/components/ContextIndicator/index.tsx +330 -0
- package/packages/core/src/public/components/ContextSparkline.tsx +56 -0
- package/packages/core/src/public/components/ControlBar.tsx +636 -0
- package/packages/core/src/public/components/DeadCodeDialog.tsx +169 -0
- package/packages/core/src/public/components/DiffViewer.tsx +585 -0
- package/packages/core/src/public/components/DockviewWorkspace.tsx +749 -0
- package/packages/core/src/public/components/Editor.tsx +630 -0
- package/packages/core/src/public/components/ErrorBoundary.tsx +67 -0
- package/packages/core/src/public/components/FileTree.tsx +379 -0
- package/packages/core/src/public/components/FontPicker/FontPicker.css +276 -0
- package/packages/core/src/public/components/FontPicker/index.tsx +430 -0
- package/packages/core/src/public/components/FullFileTree.tsx +237 -0
- package/packages/core/src/public/components/HealthGauge.tsx +181 -0
- package/packages/core/src/public/components/Message.tsx +225 -0
- package/packages/core/src/public/components/MessageList.tsx +98 -0
- package/packages/core/src/public/components/MessageView.tsx +400 -0
- package/packages/core/src/public/components/ModeSwitch/ModeSwitch.css +165 -0
- package/packages/core/src/public/components/ModeSwitch/index.tsx +372 -0
- package/packages/core/src/public/components/PersonaHeader.tsx +242 -0
- package/packages/core/src/public/components/ProjectInfoBar.tsx +45 -0
- package/packages/core/src/public/components/QuickActions.tsx +267 -0
- package/packages/core/src/public/components/SpanTimeline.tsx +352 -0
- package/packages/core/src/public/components/StandalonePanel.tsx +82 -0
- package/packages/core/src/public/components/StatsStrip.tsx +162 -0
- package/packages/core/src/public/components/StreamingContent.tsx +77 -0
- package/packages/core/src/public/components/SubagentSpan.tsx +180 -0
- package/packages/core/src/public/components/TandemPortrait.tsx +72 -0
- package/packages/core/src/public/components/ThemePalette/ThemePalette.css +179 -0
- package/packages/core/src/public/components/ThemePalette/index.tsx +326 -0
- package/packages/core/src/public/components/ToolCallBlock.tsx +252 -0
- package/packages/core/src/public/components/ToolStack.tsx +209 -0
- package/packages/core/src/public/components/ToolStatus.tsx +57 -0
- package/packages/core/src/public/components/dialogs/CodeMarkersDialog.tsx +169 -0
- package/packages/core/src/public/components/dialogs/ComplexityDialog.tsx +163 -0
- package/packages/core/src/public/components/dialogs/DependenciesDialog.tsx +120 -0
- package/packages/core/src/public/components/dialogs/HotspotsDialog.tsx +451 -0
- package/packages/core/src/public/components/dialogs/ToolDialog.tsx +43 -0
- package/packages/core/src/public/components/panel-registry.ts +13 -0
- package/packages/core/src/public/components/panels/ACPanel.tsx +93 -0
- package/packages/core/src/public/components/panels/AcceptanceCriteriaPanel.tsx +104 -0
- package/packages/core/src/public/components/panels/AuditLogPanel.tsx +489 -0
- package/packages/core/src/public/components/panels/BackgroundPanel.tsx +115 -0
- package/packages/core/src/public/components/panels/BikeLanePanel.tsx +214 -0
- package/packages/core/src/public/components/panels/DebugPanel.tsx +344 -0
- package/packages/core/src/public/components/panels/DiffView.tsx +109 -0
- package/packages/core/src/public/components/panels/DiffsPanel.tsx +56 -0
- package/packages/core/src/public/components/panels/GitPanel.tsx +260 -0
- package/packages/core/src/public/components/panels/HotspotsPanel.tsx +365 -0
- package/packages/core/src/public/components/panels/MessageFeed.tsx +39 -0
- package/packages/core/src/public/components/panels/MessagePanel.tsx +497 -0
- package/packages/core/src/public/components/panels/ProgressPanel.tsx +189 -0
- package/packages/core/src/public/components/panels/SettingsPanel.tsx +361 -0
- package/packages/core/src/public/components/panels/SprintPanel.tsx +723 -0
- package/packages/core/src/public/components/panels/TandemPanel.tsx +104 -0
- package/packages/core/src/public/components/panels/TaskTracker.tsx +48 -0
- package/packages/core/src/public/components/panels/TeamPanel.tsx +64 -0
- package/packages/core/src/public/components/panels/TeamRoster.tsx +67 -0
- package/packages/core/src/public/components/panels/TodoPanel.tsx +142 -0
- package/packages/core/src/public/components/panels/WorkflowPanel.tsx +224 -0
- package/packages/core/src/public/components/panels/index.ts +24 -0
- package/packages/core/src/public/components/ui/alert-dialog.tsx +139 -0
- package/packages/core/src/public/components/ui/badge.tsx +36 -0
- package/packages/core/src/public/components/ui/button.tsx +57 -0
- package/packages/core/src/public/components/ui/checkbox.tsx +28 -0
- package/packages/core/src/public/components/ui/collapsible.tsx +9 -0
- package/packages/core/src/public/components/ui/command.tsx +151 -0
- package/packages/core/src/public/components/ui/dialog.tsx +120 -0
- package/packages/core/src/public/components/ui/popover.tsx +31 -0
- package/packages/core/src/public/components/ui/progress.tsx +28 -0
- package/packages/core/src/public/components/ui/scroll-area.tsx +46 -0
- package/packages/core/src/public/components/ui/select.tsx +157 -0
- package/packages/core/src/public/components/ui/separator.tsx +29 -0
- package/packages/core/src/public/components/ui/skeleton.tsx +15 -0
- package/packages/core/src/public/components/ui/switch.tsx +27 -0
- package/packages/core/src/public/components/ui/toggle-group.tsx +59 -0
- package/packages/core/src/public/components/ui/toggle.tsx +43 -0
- package/packages/core/src/public/components/ui/tooltip.tsx +30 -0
- package/packages/core/src/public/contexts/ClaudeContext.tsx +311 -0
- package/packages/core/src/public/contexts/MessageQueueContext.tsx +143 -0
- package/packages/core/src/public/css/theme-browser.css +550 -0
- package/packages/core/src/public/css/theme-system.css +630 -0
- package/packages/core/src/public/hooks/index.ts +49 -0
- package/packages/core/src/public/hooks/useAgentLoad.ts +105 -0
- package/packages/core/src/public/hooks/useBackgroundTasks.ts +131 -0
- package/packages/core/src/public/hooks/useClaude.ts +234 -0
- package/packages/core/src/public/hooks/useCodeMarkers.ts +101 -0
- package/packages/core/src/public/hooks/useColorScheme.ts +42 -0
- package/packages/core/src/public/hooks/useCommandHistory.ts +99 -0
- package/packages/core/src/public/hooks/useComplexity.ts +80 -0
- package/packages/core/src/public/hooks/useDeadCode.ts +99 -0
- package/packages/core/src/public/hooks/useDependencies.ts +82 -0
- package/packages/core/src/public/hooks/useDiffs.ts +143 -0
- package/packages/core/src/public/hooks/useFileBrowser.ts +73 -0
- package/packages/core/src/public/hooks/useFocusPanel.ts +137 -0
- package/packages/core/src/public/hooks/useGitStatus.ts +233 -0
- package/packages/core/src/public/hooks/useHealthScore.ts +71 -0
- package/packages/core/src/public/hooks/useHotspots.ts +123 -0
- package/packages/core/src/public/hooks/useLayoutPersistence.ts +141 -0
- package/packages/core/src/public/hooks/useMarkdownParser.ts +36 -0
- package/packages/core/src/public/hooks/useMarkerActions.ts +234 -0
- package/packages/core/src/public/hooks/useMessageQueue.ts +380 -0
- package/packages/core/src/public/hooks/useMessageStream.ts +131 -0
- package/packages/core/src/public/hooks/usePersona.ts +112 -0
- package/packages/core/src/public/hooks/usePlanModeExit.ts +105 -0
- package/packages/core/src/public/hooks/useResponsiveLayout.ts +173 -0
- package/packages/core/src/public/hooks/useSprint.ts +157 -0
- package/packages/core/src/public/hooks/useStatsStrip.ts +204 -0
- package/packages/core/src/public/hooks/useStory.ts +135 -0
- package/packages/core/src/public/hooks/useSubagentHelper.ts +64 -0
- package/packages/core/src/public/hooks/useSyntaxHighlighter.ts +52 -0
- package/packages/core/src/public/hooks/useTabCompletion.ts +124 -0
- package/packages/core/src/public/hooks/useTandemObservations.ts +165 -0
- package/packages/core/src/public/hooks/useTeamMembers.ts +273 -0
- package/packages/core/src/public/hooks/useTodos.ts +93 -0
- package/packages/core/src/public/hooks/useUserAvatar.ts +54 -0
- package/packages/core/src/public/images/cyclist-dark.png +0 -0
- package/packages/core/src/public/images/cyclist-light.png +0 -0
- package/packages/core/src/public/index.html +14 -0
- package/packages/core/src/public/index.tsx +10 -0
- package/packages/core/src/public/lib/utils.ts +6 -0
- package/packages/core/src/public/styles/dockview-theme.css +376 -0
- package/packages/core/src/public/styles/tailwind.css +4454 -0
- package/packages/core/src/public/types/message.ts +51 -0
- package/packages/core/src/public/utils/avatar-service.ts +73 -0
- package/packages/core/src/public/utils/color-presets.ts +940 -0
- package/packages/core/src/public/utils/font-presets.ts +362 -0
- package/packages/core/src/public/utils/formatDuration.ts +14 -0
- package/packages/core/src/public/utils/markdown.ts +249 -0
- package/packages/core/src/public/utils/messageFilters.ts +128 -0
- package/packages/core/src/public/utils/slash-commands.ts +341 -0
- package/packages/core/src/public/utils/subagent-display.ts +146 -0
- package/packages/core/src/public/utils/syntax.ts +219 -0
- package/packages/core/src/public/utils/toolIntentSummarizer.ts +199 -0
- package/packages/core/src/public/utils/toolStackGrouper.ts +106 -0
- package/packages/core/src/public/utils/toolTypeColors.ts +45 -0
- package/pennyfarthing-dist/pf/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/__pycache__/context.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bc/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bc/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bc/__pycache__/focus.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bc/__pycache__/split.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bc/cli.py +0 -1
- package/pennyfarthing-dist/pf/bc/focus.py +0 -1
- package/pennyfarthing-dist/pf/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bikerack/__pycache__/story_detail_data.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bikerack/__pycache__/story_detail_screen.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bikerack/base_panel.py +0 -1
- package/pennyfarthing-dist/pf/bikerack/events.py +1 -7
- package/pennyfarthing-dist/pf/bikerack/git_panel.py +273 -10
- package/pennyfarthing-dist/pf/bikerack/portrait_resolver.py +21 -0
- package/pennyfarthing-dist/pf/bikerack/sprint_panel.py +58 -1
- package/pennyfarthing-dist/pf/bikerack/tui.py +5 -20
- package/pennyfarthing-dist/pf/bmad/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bmad/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bmad/__pycache__/parser.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/bmad/parser.py +15 -9
- package/pennyfarthing-dist/pf/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/common/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/common/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/common/__pycache__/pr_config.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/common/__pycache__/themes.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/common/pr_config.py +27 -2
- package/pennyfarthing-dist/pf/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/complexity/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/consultation/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/deadcode/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/dependencies/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/epic/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/epic/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/git_group/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/handoff/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/handoff/__pycache__/marker.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/handoff/__pycache__/phase_check.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/healthscore/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/hotspots/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/jira/__pycache__/claim.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/jira/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/jira/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/jira/__pycache__/epic.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/jira/__pycache__/operations.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/jira/__pycache__/story.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/jira/__pycache__/sync.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/launch/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/launch/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/prime/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/prime/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/prime/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/prime/__pycache__/persona.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/prime/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/prime/__pycache__/tiers.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/session/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/session/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/settings/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/settings/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/settings/__pycache__/settings.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/settings/settings.py +44 -8
- package/pennyfarthing-dist/pf/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/archive.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/status.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/sprint/story_finish.py +14 -2
- package/pennyfarthing-dist/pf/sprint/validator.py +7 -7
- package/pennyfarthing-dist/pf/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing-dist/pf/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing-dist/pf/tests/test_sprint_validator.py +44 -0
- package/pennyfarthing-dist/pf/theme/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/theme/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/validate/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/workflow/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/workflow/__pycache__/scale.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/pf/workflow/__pycache__/state.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/scripts/lib/find-root.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -1
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +13 -13
- package/pennyfarthing-dist/scripts/workflow/check.py +4 -6
- package/pennyfarthing-dist/scripts/workflow/complete-step.py +2 -2
- package/pennyfarthing-dist/skills/skill-registry.yaml +19 -0
- package/pennyfarthing-dist/workflows/tdd-tandem.yaml +15 -2
- package/packages/core/dist/workflow/__test_context_watch__/.session/.tandem-turn-counter +0 -1
- package/packages/core/dist/workflow/__test_context_watch__/.session/95-6-session.md +0 -3
- package/packages/core/dist/workflow/__test_context_watch__/.session/95-6-tandem-architect.md +0 -6
- package/packages/core/dist/workflow/__test_file_watch__/.session/95-4-tandem-architect.md +0 -6
- package/packages/core/dist/workflow/__test_file_watch__/workdir/trigger.ts +0 -1
- package/packages/core/dist/workflow/__test_tool_watch__/.session/95-5-tandem-architect.md +0 -6
- package/packages/core/dist/workflow/__test_tool_watch__/.session/95-5-tandem-toolcalls.jsonl +0 -1
- package/pennyfarthing-dist/pf/bikerack/changed_panel.py +0 -201
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
"""GitPanel — Multi-repo git status panel for BikeRack TUI.
|
|
1
|
+
"""GitPanel — Multi-repo git status panel with diff drill-through for BikeRack TUI.
|
|
2
2
|
|
|
3
3
|
Story 103-10: Subscribes to /ws/git, renders multi-repo git status
|
|
4
4
|
as Rich table with Nerd Font glyphs for branch and status indicators.
|
|
5
|
+
|
|
6
|
+
Merge: Changed panel consolidated into Git panel — clickable file list
|
|
7
|
+
with Enter to view diff inline, Escape to return to repo overview.
|
|
5
8
|
"""
|
|
6
9
|
|
|
7
10
|
from __future__ import annotations
|
|
8
11
|
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
9
14
|
from typing import Any
|
|
10
15
|
|
|
16
|
+
from rich.console import Group as RichGroup
|
|
17
|
+
from rich.syntax import Syntax
|
|
11
18
|
from rich.text import Text
|
|
19
|
+
from textual.binding import Binding
|
|
12
20
|
|
|
13
21
|
from pf.bikerack.base_panel import PANEL_ICONS, BasePanel
|
|
14
22
|
|
|
@@ -63,27 +71,265 @@ def _parse_file_status(status: str) -> tuple[str, str, str]:
|
|
|
63
71
|
return ("·", "Changed", "yellow")
|
|
64
72
|
|
|
65
73
|
|
|
74
|
+
# ---- Diff rendering helpers (from diffs_panel.py patterns) ----
|
|
75
|
+
|
|
76
|
+
_LANG_MAP: dict[str, str] = {
|
|
77
|
+
".py": "python", ".ts": "typescript", ".tsx": "tsx",
|
|
78
|
+
".js": "javascript", ".jsx": "jsx", ".go": "go",
|
|
79
|
+
".rs": "rust", ".rb": "ruby", ".java": "java",
|
|
80
|
+
".css": "css", ".html": "html", ".json": "json",
|
|
81
|
+
".yaml": "yaml", ".yml": "yaml", ".md": "markdown",
|
|
82
|
+
".sh": "bash", ".zsh": "bash", ".toml": "toml",
|
|
83
|
+
".xml": "xml", ".sql": "sql", ".c": "c", ".cpp": "cpp",
|
|
84
|
+
".h": "c", ".hpp": "cpp",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _detect_language(path: str) -> str:
|
|
89
|
+
"""Detect programming language from file path for syntax highlighting."""
|
|
90
|
+
ext = os.path.splitext(path)[1].lower()
|
|
91
|
+
return _LANG_MAP.get(ext, "text")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _highlight_code(code: str, language: str) -> Text:
|
|
95
|
+
"""Apply syntax highlighting to a single line."""
|
|
96
|
+
if not code or not code.strip():
|
|
97
|
+
return Text(code)
|
|
98
|
+
try:
|
|
99
|
+
syntax = Syntax(code, language, theme="monokai", background_color="default")
|
|
100
|
+
highlighted = syntax.highlight(code)
|
|
101
|
+
highlighted.rstrip()
|
|
102
|
+
return highlighted
|
|
103
|
+
except Exception:
|
|
104
|
+
return Text(code)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _render_inline_diff(diff_entry: dict[str, Any]) -> list[Any]:
|
|
108
|
+
"""Render a single file diff inline for drill-through view."""
|
|
109
|
+
path = diff_entry.get("path", "unknown")
|
|
110
|
+
status = diff_entry.get("status", "")
|
|
111
|
+
additions = diff_entry.get("additions")
|
|
112
|
+
deletions = diff_entry.get("deletions")
|
|
113
|
+
raw_diff = diff_entry.get("diff", "")
|
|
114
|
+
|
|
115
|
+
header = Text()
|
|
116
|
+
header.append(path, style="bold cyan")
|
|
117
|
+
if status:
|
|
118
|
+
header.append(f" {status}", style="dim")
|
|
119
|
+
if additions is not None and deletions is not None:
|
|
120
|
+
header.append(f" +{additions} -{deletions}", style="dim")
|
|
121
|
+
|
|
122
|
+
language = _detect_language(path)
|
|
123
|
+
parts: list[Any] = [header]
|
|
124
|
+
|
|
125
|
+
if not raw_diff or not raw_diff.strip():
|
|
126
|
+
parts.append(Text("(no diff content)", style="dim italic"))
|
|
127
|
+
return parts
|
|
128
|
+
|
|
129
|
+
line_num = 0
|
|
130
|
+
for line in raw_diff.split("\n"):
|
|
131
|
+
if line.startswith("diff --git") or line.startswith("index "):
|
|
132
|
+
continue
|
|
133
|
+
if line.startswith("---") or line.startswith("+++"):
|
|
134
|
+
continue
|
|
135
|
+
if line.startswith("new file") or line.startswith("deleted file"):
|
|
136
|
+
continue
|
|
137
|
+
if line.startswith("rename "):
|
|
138
|
+
continue
|
|
139
|
+
if line == "":
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
if line.startswith("Binary files"):
|
|
143
|
+
parts.append(Text(line, style="dim italic"))
|
|
144
|
+
elif line.startswith("@@"):
|
|
145
|
+
match = re.match(r"@@ -\d+(?:,\d+)? \+(\d+)", line)
|
|
146
|
+
if match:
|
|
147
|
+
line_num = int(match.group(1)) - 1
|
|
148
|
+
parts.append(Text(line, style="cyan"))
|
|
149
|
+
elif line.startswith("+"):
|
|
150
|
+
line_num += 1
|
|
151
|
+
t = Text()
|
|
152
|
+
t.append(f"{line_num:4d} ", style="dim green")
|
|
153
|
+
t.append_text(_highlight_code(line[1:], language))
|
|
154
|
+
parts.append(t)
|
|
155
|
+
elif line.startswith("-"):
|
|
156
|
+
t = Text()
|
|
157
|
+
t.append(" - ", style="red")
|
|
158
|
+
t.append_text(_highlight_code(line[1:], language))
|
|
159
|
+
parts.append(t)
|
|
160
|
+
elif line.startswith(" "):
|
|
161
|
+
line_num += 1
|
|
162
|
+
t = Text()
|
|
163
|
+
t.append(f"{line_num:4d} ", style="dim")
|
|
164
|
+
t.append_text(_highlight_code(line[1:], language))
|
|
165
|
+
parts.append(t)
|
|
166
|
+
else:
|
|
167
|
+
parts.append(Text(line, style="dim"))
|
|
168
|
+
|
|
169
|
+
return parts
|
|
170
|
+
|
|
171
|
+
|
|
66
172
|
class GitPanel(BasePanel):
|
|
67
|
-
"""Multi-repo git status panel.
|
|
173
|
+
"""Multi-repo git status panel with diff drill-through.
|
|
68
174
|
|
|
69
|
-
Subscribes to the ``git``
|
|
70
|
-
git status for all configured repos
|
|
71
|
-
|
|
175
|
+
Subscribes to the ``git`` and ``diffs`` WebSocket channels.
|
|
176
|
+
Renders git status for all configured repos. Clicking a file
|
|
177
|
+
(Enter) shows its diff inline; Escape returns to the overview.
|
|
72
178
|
"""
|
|
73
179
|
|
|
74
180
|
channel: str = "git"
|
|
75
181
|
panel_name: str = "Git"
|
|
76
182
|
icon: str = PANEL_ICONS["git"][0]
|
|
183
|
+
can_focus = True
|
|
184
|
+
|
|
185
|
+
BINDINGS = [
|
|
186
|
+
Binding("up", "select_prev_key", "Up"),
|
|
187
|
+
Binding("down", "select_next_key", "Down"),
|
|
188
|
+
Binding("enter", "drill_into_file", "View diff"),
|
|
189
|
+
Binding("escape", "back_to_overview", "Back"),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
def __init__(self, client=None, **kwargs):
|
|
193
|
+
super().__init__(client=client, **kwargs)
|
|
194
|
+
self._selected_index: int = 0
|
|
195
|
+
self._file_paths: list[str] = []
|
|
196
|
+
self._viewing_diff: bool = False
|
|
197
|
+
self._diff_file_path: str | None = None
|
|
198
|
+
self._diffs_payload: dict[str, Any] | None = None
|
|
199
|
+
|
|
200
|
+
# Subscribe to diffs channel for diff data
|
|
201
|
+
if client is not None:
|
|
202
|
+
client.subscribe("diffs", self._handle_diffs_message)
|
|
203
|
+
|
|
204
|
+
def _handle_diffs_message(self, message: dict[str, Any] | None) -> None:
|
|
205
|
+
"""Store diffs payload for drill-through rendering."""
|
|
206
|
+
if message is not None:
|
|
207
|
+
self._diffs_payload = message
|
|
208
|
+
# If viewing a diff, re-render with updated diff data
|
|
209
|
+
if self._viewing_diff:
|
|
210
|
+
self._rerender()
|
|
211
|
+
|
|
212
|
+
def handle_message(self, message: dict[str, Any] | None) -> None:
|
|
213
|
+
"""Handle incoming git message — build file path index then render."""
|
|
214
|
+
if message is not None:
|
|
215
|
+
self._build_file_paths(message)
|
|
216
|
+
super().handle_message(message)
|
|
217
|
+
|
|
218
|
+
def _build_file_paths(self, payload: dict[str, Any]) -> None:
|
|
219
|
+
"""Extract flat list of file paths from repos payload."""
|
|
220
|
+
paths: list[str] = []
|
|
221
|
+
repos = payload.get("repos", [])
|
|
222
|
+
if isinstance(repos, list):
|
|
223
|
+
for repo in repos:
|
|
224
|
+
if not isinstance(repo, dict):
|
|
225
|
+
continue
|
|
226
|
+
dirty_files = repo.get("dirtyFiles", [])
|
|
227
|
+
if not isinstance(dirty_files, list):
|
|
228
|
+
continue
|
|
229
|
+
for f in dirty_files:
|
|
230
|
+
if isinstance(f, dict):
|
|
231
|
+
path = f.get("path", "")
|
|
232
|
+
if path:
|
|
233
|
+
paths.append(path)
|
|
234
|
+
self._file_paths = paths
|
|
235
|
+
if self._selected_index >= len(paths):
|
|
236
|
+
self._selected_index = max(0, len(paths) - 1)
|
|
237
|
+
|
|
238
|
+
def select_next(self) -> None:
|
|
239
|
+
"""Move selection to the next file."""
|
|
240
|
+
if self._file_paths and self._selected_index < len(self._file_paths) - 1:
|
|
241
|
+
self._selected_index += 1
|
|
242
|
+
self._rerender()
|
|
243
|
+
|
|
244
|
+
def select_prev(self) -> None:
|
|
245
|
+
"""Move selection to the previous file."""
|
|
246
|
+
if self._selected_index > 0:
|
|
247
|
+
self._selected_index -= 1
|
|
248
|
+
self._rerender()
|
|
249
|
+
|
|
250
|
+
def action_select_next_key(self) -> None:
|
|
251
|
+
"""Binding action: move selection down."""
|
|
252
|
+
if not self._viewing_diff:
|
|
253
|
+
self.select_next()
|
|
254
|
+
|
|
255
|
+
def action_select_prev_key(self) -> None:
|
|
256
|
+
"""Binding action: move selection up."""
|
|
257
|
+
if not self._viewing_diff:
|
|
258
|
+
self.select_prev()
|
|
259
|
+
|
|
260
|
+
def action_drill_into_file(self) -> None:
|
|
261
|
+
"""Enter diff drill-through for the selected file."""
|
|
262
|
+
if self._viewing_diff:
|
|
263
|
+
return
|
|
264
|
+
if not self._file_paths:
|
|
265
|
+
return
|
|
266
|
+
if self._selected_index >= len(self._file_paths):
|
|
267
|
+
return
|
|
268
|
+
self._diff_file_path = self._file_paths[self._selected_index]
|
|
269
|
+
self._viewing_diff = True
|
|
270
|
+
self._rerender()
|
|
271
|
+
|
|
272
|
+
def action_back_to_overview(self) -> None:
|
|
273
|
+
"""Return to the repo overview from diff view."""
|
|
274
|
+
if self._viewing_diff:
|
|
275
|
+
self._viewing_diff = False
|
|
276
|
+
self._diff_file_path = None
|
|
277
|
+
self._rerender()
|
|
278
|
+
|
|
279
|
+
def _rerender(self) -> None:
|
|
280
|
+
"""Re-render panel with current payload."""
|
|
281
|
+
if self._last_payload:
|
|
282
|
+
try:
|
|
283
|
+
self.update(self.render_panel(self._last_payload))
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
def _find_diff_for_path(self, path: str) -> dict[str, Any] | None:
|
|
288
|
+
"""Find a diff entry matching the given file path."""
|
|
289
|
+
if self._diffs_payload is None:
|
|
290
|
+
return None
|
|
291
|
+
diffs = self._diffs_payload.get("diffs", [])
|
|
292
|
+
for d in diffs:
|
|
293
|
+
d_path = d.get("path", "")
|
|
294
|
+
if d_path == path or d_path.endswith(path) or path.endswith(d_path):
|
|
295
|
+
return d
|
|
296
|
+
return None
|
|
77
297
|
|
|
78
298
|
def render_panel(self, payload: dict[str, Any]) -> Any:
|
|
79
|
-
"""Render git status
|
|
80
|
-
|
|
299
|
+
"""Render git status or diff drill-through view."""
|
|
300
|
+
if self._viewing_diff and self._diff_file_path:
|
|
301
|
+
return self._render_diff_view()
|
|
302
|
+
return self._render_repo_overview(payload)
|
|
303
|
+
|
|
304
|
+
def _render_diff_view(self) -> Any:
|
|
305
|
+
"""Render inline diff for the selected file."""
|
|
306
|
+
parts: list[Any] = []
|
|
81
307
|
|
|
308
|
+
# Back bar
|
|
309
|
+
back = Text()
|
|
310
|
+
back.append("← ", style="bold")
|
|
311
|
+
back.append("Esc", style="bold yellow")
|
|
312
|
+
back.append(" back ", style="dim")
|
|
313
|
+
back.append(self._diff_file_path or "", style="bold cyan")
|
|
314
|
+
parts.append(back)
|
|
315
|
+
parts.append(Text(""))
|
|
316
|
+
|
|
317
|
+
diff_entry = self._find_diff_for_path(self._diff_file_path or "")
|
|
318
|
+
if diff_entry:
|
|
319
|
+
parts.extend(_render_inline_diff(diff_entry))
|
|
320
|
+
else:
|
|
321
|
+
parts.append(Text(f"No diff available for {self._diff_file_path}", style="dim italic"))
|
|
322
|
+
|
|
323
|
+
return RichGroup(*parts)
|
|
324
|
+
|
|
325
|
+
def _render_repo_overview(self, payload: dict[str, Any]) -> Any:
|
|
326
|
+
"""Render git status as repo list with selectable file lists."""
|
|
82
327
|
repos = payload.get("repos", [])
|
|
83
328
|
if not repos:
|
|
84
329
|
return Text("No repository data", style="dim italic")
|
|
85
330
|
|
|
86
331
|
parts: list[Any] = []
|
|
332
|
+
flat_idx = 0
|
|
87
333
|
for repo in repos:
|
|
88
334
|
branch = repo.get("branch", "")
|
|
89
335
|
ahead = repo.get("ahead", 0)
|
|
@@ -120,7 +366,7 @@ class GitPanel(BasePanel):
|
|
|
120
366
|
|
|
121
367
|
parts.append(header)
|
|
122
368
|
|
|
123
|
-
# Expanded file list for dirty repos
|
|
369
|
+
# Expanded file list for dirty repos — with selection highlight
|
|
124
370
|
if not clean and dirty_files:
|
|
125
371
|
for f in dirty_files:
|
|
126
372
|
if not isinstance(f, dict):
|
|
@@ -128,12 +374,29 @@ class GitPanel(BasePanel):
|
|
|
128
374
|
status_code = f.get("status", " ")
|
|
129
375
|
path = f.get("path", "")
|
|
130
376
|
icon, label, style = _parse_file_status(status_code)
|
|
377
|
+
is_selected = flat_idx == self._selected_index
|
|
131
378
|
file_line = Text()
|
|
132
|
-
|
|
379
|
+
if is_selected:
|
|
380
|
+
file_line.append(" › ", style="bold reverse")
|
|
381
|
+
else:
|
|
382
|
+
file_line.append(" ")
|
|
133
383
|
file_line.append(icon, style=f"bold {style}")
|
|
134
|
-
file_line.append(
|
|
384
|
+
file_line.append(
|
|
385
|
+
f" {path}",
|
|
386
|
+
style="bold cyan reverse" if is_selected else style,
|
|
387
|
+
)
|
|
135
388
|
parts.append(file_line)
|
|
389
|
+
flat_idx += 1
|
|
136
390
|
|
|
137
391
|
parts.append(Text("")) # spacer
|
|
138
392
|
|
|
393
|
+
# Hint line
|
|
394
|
+
if self._file_paths:
|
|
395
|
+
hint = Text()
|
|
396
|
+
hint.append("↑↓", style="bold yellow")
|
|
397
|
+
hint.append(" select ", style="dim")
|
|
398
|
+
hint.append("Enter", style="bold yellow")
|
|
399
|
+
hint.append(" view diff", style="dim")
|
|
400
|
+
parts.append(hint)
|
|
401
|
+
|
|
139
402
|
return RichGroup(*parts)
|
|
@@ -101,6 +101,27 @@ def resolve_portrait_path(
|
|
|
101
101
|
if result:
|
|
102
102
|
return result
|
|
103
103
|
|
|
104
|
+
# Fallback: search Cyclist package portrait directories
|
|
105
|
+
# Portraits are bundled in @pennyfarthing/cyclist, not alongside theme YAMLs
|
|
106
|
+
root = project_root or Path.cwd()
|
|
107
|
+
cyclist_portrait_dirs = [
|
|
108
|
+
root / "packages" / "cyclist" / "portraits" / theme, # monorepo dev
|
|
109
|
+
root / "node_modules" / "@pennyfarthing" / "cyclist" / "portraits" / theme, # npm
|
|
110
|
+
]
|
|
111
|
+
# pnpm: resolve through .pennyfarthing symlink chain
|
|
112
|
+
pnpm_cyclist = root / "node_modules" / ".pnpm"
|
|
113
|
+
if pnpm_cyclist.is_dir():
|
|
114
|
+
for entry in pnpm_cyclist.iterdir():
|
|
115
|
+
if entry.name.startswith("@pennyfarthing+cyclist@"):
|
|
116
|
+
candidate = entry / "node_modules" / "@pennyfarthing" / "cyclist" / "portraits" / theme
|
|
117
|
+
cyclist_portrait_dirs.append(candidate)
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
for portraits_dir in cyclist_portrait_dirs:
|
|
121
|
+
result = _find_portrait(portraits_dir, slug)
|
|
122
|
+
if result:
|
|
123
|
+
return result
|
|
124
|
+
|
|
104
125
|
return None
|
|
105
126
|
|
|
106
127
|
|
|
@@ -289,7 +289,7 @@ class SprintPanel(Widget):
|
|
|
289
289
|
saved_expanded: dict[str, bool] = {}
|
|
290
290
|
for node in tree.root.children:
|
|
291
291
|
data = node.data
|
|
292
|
-
if data and data.get("type")
|
|
292
|
+
if data and data.get("type") in ("epic", "future_initiative"):
|
|
293
293
|
saved_expanded[data["id"]] = node.is_expanded
|
|
294
294
|
|
|
295
295
|
# Save cursor position by node identity
|
|
@@ -356,6 +356,63 @@ class SprintPanel(Widget):
|
|
|
356
356
|
if cursor_node_key == f"epic:{epic_id}":
|
|
357
357
|
cursor_target = epic_node
|
|
358
358
|
|
|
359
|
+
# Future Initiatives section (Story 118-1)
|
|
360
|
+
future_epics = payload.get("futureEpics", [])
|
|
361
|
+
if future_epics:
|
|
362
|
+
# Section separator
|
|
363
|
+
separator_label = Text("── Future Initiatives ──", style="bold dim")
|
|
364
|
+
tree.root.add_leaf(separator_label, data={"type": "separator"})
|
|
365
|
+
|
|
366
|
+
for initiative in future_epics:
|
|
367
|
+
init_id = initiative.get("id", "")
|
|
368
|
+
init_title = initiative.get("title", "")
|
|
369
|
+
init_pts = initiative.get("estimatedPoints", 0)
|
|
370
|
+
init_status = initiative.get("status", "planning")
|
|
371
|
+
children = initiative.get("children", [])
|
|
372
|
+
|
|
373
|
+
# Build initiative label
|
|
374
|
+
init_label = Text(no_wrap=True, overflow="ellipsis")
|
|
375
|
+
status_style = "green" if init_status == "ready" else "dim yellow"
|
|
376
|
+
init_label.append(f"[{init_status}]", style=status_style)
|
|
377
|
+
init_label.append(f" {init_title}", style="bold")
|
|
378
|
+
init_label.append(f" {init_pts} pts", style="dim")
|
|
379
|
+
if children:
|
|
380
|
+
init_label.append(f" ({len(children)} epics)", style="dim")
|
|
381
|
+
|
|
382
|
+
init_data: dict[str, Any] = {
|
|
383
|
+
"type": "future_initiative",
|
|
384
|
+
"id": init_id,
|
|
385
|
+
"title": init_title,
|
|
386
|
+
}
|
|
387
|
+
init_node = tree.root.add(init_label, data=init_data)
|
|
388
|
+
|
|
389
|
+
# Add child epics as leaves
|
|
390
|
+
for child in children:
|
|
391
|
+
child_label = Text(no_wrap=True, overflow="ellipsis")
|
|
392
|
+
child_status = child.get("status", "planning")
|
|
393
|
+
child_style = "green" if child_status == "ready" else "dim yellow"
|
|
394
|
+
child_label.append(f" [{child_status}]", style=child_style)
|
|
395
|
+
child_label.append(f" {child.get('title', '')}")
|
|
396
|
+
child_label.append(f" {child.get('estimatedPoints', 0)} pts", style="dim")
|
|
397
|
+
child_label.append(f" {child.get('storyCount', 0)} stories", style="dim")
|
|
398
|
+
child_data: dict[str, Any] = {
|
|
399
|
+
"type": "future_epic_child",
|
|
400
|
+
"id": child.get("id", ""),
|
|
401
|
+
}
|
|
402
|
+
init_node.add_leaf(child_label, data=child_data)
|
|
403
|
+
|
|
404
|
+
# Restore expand state or collapse by default
|
|
405
|
+
if init_id in saved_expanded:
|
|
406
|
+
if saved_expanded[init_id]:
|
|
407
|
+
init_node.expand()
|
|
408
|
+
else:
|
|
409
|
+
init_node.collapse()
|
|
410
|
+
else:
|
|
411
|
+
init_node.collapse()
|
|
412
|
+
|
|
413
|
+
if cursor_node_key == f"epic:{init_id}":
|
|
414
|
+
cursor_target = init_node
|
|
415
|
+
|
|
359
416
|
# Restore cursor position
|
|
360
417
|
if cursor_target is not None:
|
|
361
418
|
try:
|
|
@@ -27,11 +27,9 @@ from pf.bc.focus import get_last_panel, save_last_panel
|
|
|
27
27
|
from pf.bikerack.audit_log_panel import AuditLogPanel
|
|
28
28
|
from pf.bikerack.background_panel import BackgroundPanel
|
|
29
29
|
from pf.bikerack.base_panel import get_panel_icon
|
|
30
|
-
from pf.bikerack.changed_panel import ChangedPanel
|
|
31
30
|
from pf.bikerack.context_meter_footer import ContextMeterFooter
|
|
32
31
|
from pf.bikerack.debug_panel import DebugPanel
|
|
33
32
|
from pf.bikerack.diffs_panel import DiffsPanel
|
|
34
|
-
from pf.bikerack.events import NavigateToFile
|
|
35
33
|
from pf.bikerack.git_panel import GitPanel
|
|
36
34
|
from pf.bikerack.progress_panel import ProgressPanel
|
|
37
35
|
from pf.bikerack.sprint_panel import SprintPanel
|
|
@@ -79,7 +77,6 @@ PANEL_REGISTRY: list[tuple[str, str]] = [
|
|
|
79
77
|
("sprint", "Sprint"),
|
|
80
78
|
("git", "Git"),
|
|
81
79
|
("diffs", "Diffs"),
|
|
82
|
-
("changed", "Changed"),
|
|
83
80
|
("background", "Background"),
|
|
84
81
|
("audit-log", "Audit Log"),
|
|
85
82
|
("debug", "Debug"),
|
|
@@ -95,7 +92,6 @@ PANEL_DISPLAY_NAMES: dict[str, str] = {
|
|
|
95
92
|
"workflow": "Workflow",
|
|
96
93
|
"background": "Background",
|
|
97
94
|
"audit-log": "Audit Log",
|
|
98
|
-
"changed": "Changed",
|
|
99
95
|
"ac": "Acceptance Criteria",
|
|
100
96
|
"debug": "Debug",
|
|
101
97
|
"progress": "Progress",
|
|
@@ -110,7 +106,7 @@ _PANEL_KEYS = [key for key, _ in PANEL_REGISTRY]
|
|
|
110
106
|
# Story 110-4: Named presets for common side-by-side views.
|
|
111
107
|
SPLIT_PRESETS: dict[str, tuple[str, str]] = {
|
|
112
108
|
"sprint+diffs": ("sprint", "diffs"),
|
|
113
|
-
"
|
|
109
|
+
"git+diffs": ("git", "diffs"),
|
|
114
110
|
"progress+debug": ("progress", "debug"),
|
|
115
111
|
}
|
|
116
112
|
|
|
@@ -442,11 +438,10 @@ class BikeRackApp(App):
|
|
|
442
438
|
Binding("1", "switch_panel('sprint')", "Sprint", show=False),
|
|
443
439
|
Binding("2", "switch_panel('git')", "Git", show=False),
|
|
444
440
|
Binding("3", "switch_panel('diffs')", "Diffs", show=False),
|
|
445
|
-
Binding("4", "switch_panel('
|
|
446
|
-
Binding("5", "switch_panel('
|
|
447
|
-
Binding("6", "switch_panel('
|
|
448
|
-
Binding("7", "switch_panel('
|
|
449
|
-
Binding("8", "switch_panel('progress')", "Progress", show=False),
|
|
441
|
+
Binding("4", "switch_panel('background')", "Background", show=False),
|
|
442
|
+
Binding("5", "switch_panel('audit-log')", "Audit Log", show=False),
|
|
443
|
+
Binding("6", "switch_panel('debug')", "Debug", show=False),
|
|
444
|
+
Binding("7", "switch_panel('progress')", "Progress", show=False),
|
|
450
445
|
Binding("bracketright", "next_panel", "]Next"),
|
|
451
446
|
Binding("bracketleft", "prev_panel", "[Prev"),
|
|
452
447
|
Binding("tab", "next_panel", show=False, priority=True),
|
|
@@ -491,7 +486,6 @@ class BikeRackApp(App):
|
|
|
491
486
|
yield SprintPanel(client=self._client, id="panel-sprint")
|
|
492
487
|
yield GitPanel(client=self._client, id="panel-git")
|
|
493
488
|
yield DiffsPanel(client=self._client, id="panel-diffs")
|
|
494
|
-
yield ChangedPanel(client=self._client, id="panel-changed")
|
|
495
489
|
yield BackgroundPanel(client=self._client, id="panel-background")
|
|
496
490
|
yield AuditLogPanel(client=self._client, id="panel-audit-log")
|
|
497
491
|
yield DebugPanel(client=self._client, id="panel-debug")
|
|
@@ -537,15 +531,6 @@ class BikeRackApp(App):
|
|
|
537
531
|
self._client.subscribe("persona", self._handle_persona_message)
|
|
538
532
|
self.run_worker(self._client.connect(), exclusive=True, name="ws-client")
|
|
539
533
|
|
|
540
|
-
def on_navigate_to_file(self, event: NavigateToFile) -> None:
|
|
541
|
-
"""Handle NavigateToFile — switch to diffs and navigate to file."""
|
|
542
|
-
self.action_switch_panel("diffs")
|
|
543
|
-
try:
|
|
544
|
-
diffs = self.query_one("#panel-diffs", DiffsPanel)
|
|
545
|
-
diffs.navigate_to_file(event.path)
|
|
546
|
-
except Exception:
|
|
547
|
-
pass
|
|
548
|
-
|
|
549
534
|
def action_switch_panel(self, key: str) -> None:
|
|
550
535
|
"""Switch to a panel by key."""
|
|
551
536
|
if key not in _PANEL_KEYS:
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -61,7 +61,7 @@ _HEADER_PATTERNS: dict[str, re.Pattern[str]] = {
|
|
|
61
61
|
"date": re.compile(r"^Date:\s*(.+)$", re.MULTILINE),
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
_TITLE_RE = re.compile(r"^#\s+Story\s+\d+\.\d
|
|
64
|
+
_TITLE_RE = re.compile(r"^#\s+Story\s+\d+\.\d+(?:\.\d+)?:\s*(.+)$", re.MULTILINE)
|
|
65
65
|
|
|
66
66
|
# AC block: everything between ## Acceptance Criteria and the next ## heading
|
|
67
67
|
_AC_RE = re.compile(
|
|
@@ -88,10 +88,17 @@ def parse_bmad_story(path: Path) -> dict[str, Any]:
|
|
|
88
88
|
fields[name] = match.group(1).strip()
|
|
89
89
|
|
|
90
90
|
story_key = fields.get("story_key", "")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
# Consume all leading numeric segments as the ID
|
|
92
|
+
# e.g. "1-5-testing-framework" → "1-5", "2-8-1-claroty-plugin" → "2-8-1"
|
|
93
|
+
key_parts = story_key.split("-")
|
|
94
|
+
id_segments: list[str] = []
|
|
95
|
+
for seg in key_parts:
|
|
96
|
+
if seg.isdigit():
|
|
97
|
+
id_segments.append(seg)
|
|
98
|
+
else:
|
|
99
|
+
break
|
|
100
|
+
pf_id = "-".join(id_segments) if len(id_segments) >= 2 else "0-0"
|
|
101
|
+
epic_num = id_segments[0] if id_segments else "0"
|
|
95
102
|
|
|
96
103
|
# Title from # heading
|
|
97
104
|
title_match = _TITLE_RE.search(content)
|
|
@@ -194,10 +201,9 @@ def discover_bmad_stories(
|
|
|
194
201
|
story = parse_bmad_story(md_file)
|
|
195
202
|
stories.append(story)
|
|
196
203
|
|
|
197
|
-
# Sort by epic number, then story number
|
|
198
|
-
def sort_key(s: dict) -> tuple[int,
|
|
199
|
-
|
|
200
|
-
return (int(parts[0]), int(parts[1]))
|
|
204
|
+
# Sort by epic number, then story number (supports variable-length IDs like 2-8-1)
|
|
205
|
+
def sort_key(s: dict) -> tuple[int, ...]:
|
|
206
|
+
return tuple(int(p) for p in s["id"].split("-"))
|
|
201
207
|
|
|
202
208
|
stories.sort(key=sort_key)
|
|
203
209
|
return stories
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""PR mode configuration reader.
|
|
2
2
|
|
|
3
|
-
Reads the pr_mode
|
|
3
|
+
Reads the pr_mode and pr_merge preferences from .pennyfarthing/config.local.yaml.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
pr_mode values: draft | ready | none (default: draft)
|
|
6
|
+
pr_merge values: auto | human (default: auto)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
from __future__ import annotations
|
|
@@ -12,6 +13,9 @@ from pf.common.config import load_pennyfarthing_config
|
|
|
12
13
|
VALID_PR_MODES = {"draft", "ready", "none"}
|
|
13
14
|
DEFAULT_PR_MODE = "draft"
|
|
14
15
|
|
|
16
|
+
VALID_PR_MERGE_MODES = {"auto", "human"}
|
|
17
|
+
DEFAULT_PR_MERGE_MODE = "auto"
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
def get_pr_mode() -> str:
|
|
17
21
|
"""Read pr_mode from pennyfarthing config.
|
|
@@ -34,5 +38,26 @@ def get_pr_mode() -> str:
|
|
|
34
38
|
return mode
|
|
35
39
|
|
|
36
40
|
|
|
41
|
+
def get_pr_merge_mode() -> str:
|
|
42
|
+
"""Read pr_merge from pennyfarthing config.
|
|
43
|
+
|
|
44
|
+
Looks for workflow.pr_merge in .pennyfarthing/config.local.yaml.
|
|
45
|
+
Falls back to 'auto' if not set or invalid.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
One of: 'auto', 'human'
|
|
49
|
+
"""
|
|
50
|
+
config = load_pennyfarthing_config()
|
|
51
|
+
workflow = config.get("workflow", {})
|
|
52
|
+
if not isinstance(workflow, dict):
|
|
53
|
+
return DEFAULT_PR_MERGE_MODE
|
|
54
|
+
|
|
55
|
+
mode = workflow.get("pr_merge", DEFAULT_PR_MERGE_MODE)
|
|
56
|
+
if mode not in VALID_PR_MERGE_MODES:
|
|
57
|
+
return DEFAULT_PR_MERGE_MODE
|
|
58
|
+
|
|
59
|
+
return mode
|
|
60
|
+
|
|
61
|
+
|
|
37
62
|
if __name__ == "__main__":
|
|
38
63
|
print(get_pr_mode())
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|