@pennyfarthing/core 11.1.0 → 11.2.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 +8 -8
- package/package.json +16 -14
- package/packages/core/dist/cli/utils/constants.d.ts +1 -1
- package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/constants.js +2 -1
- package/packages/core/dist/cli/utils/constants.js.map +1 -1
- package/packages/core/dist/consultation/dialogue-manager.d.ts +75 -0
- package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.js +334 -0
- package/packages/core/dist/consultation/dialogue-manager.js.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts +19 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js +444 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -0
- package/packages/core/dist/server/api/git.d.ts +13 -1
- package/packages/core/dist/server/api/git.d.ts.map +1 -1
- package/packages/core/dist/server/api/git.js +53 -34
- package/packages/core/dist/server/api/git.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.d.ts +16 -11
- package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
- package/packages/core/dist/server/otlp-receiver.js +185 -24
- package/packages/core/dist/server/otlp-receiver.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.test.d.ts +21 -0
- package/packages/core/dist/server/otlp-receiver.test.d.ts.map +1 -0
- package/packages/core/dist/server/otlp-receiver.test.js +446 -0
- package/packages/core/dist/server/otlp-receiver.test.js.map +1 -0
- package/packages/core/dist/shared/portrait-resolver.d.ts +9 -0
- package/packages/core/dist/shared/portrait-resolver.d.ts.map +1 -1
- package/packages/core/dist/shared/portrait-resolver.js +27 -0
- package/packages/core/dist/shared/portrait-resolver.js.map +1 -1
- package/packages/core/dist/shared/portrait-resolver.test.js +47 -1
- package/packages/core/dist/shared/portrait-resolver.test.js.map +1 -1
- package/packages/core/dist/shared/skill-search.test.js +2 -2
- package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts +13 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts.map +1 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.js +126 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.js.map +1 -0
- package/pennyfarthing-dist/agents/dev.md +1 -1
- package/pennyfarthing-dist/agents/reviewer.md +1 -1
- package/pennyfarthing-dist/agents/sm-setup.md +1 -1
- package/pennyfarthing-dist/agents/sm.md +2 -2
- package/pennyfarthing-dist/agents/tea.md +1 -1
- package/pennyfarthing-dist/agents/testing-runner.md +2 -1
- package/pennyfarthing-dist/commands/pf-chore.md +2 -2
- package/pennyfarthing-dist/commands/pf-standalone.md +7 -2
- package/pennyfarthing-dist/guides/agent-behavior.md +1 -1
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +1 -1
- package/pennyfarthing-dist/guides/bikerack.md +3 -3
- package/pennyfarthing-dist/guides/hooks.md +1 -1
- package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
- package/pennyfarthing-dist/guides/xml-tags.md +2 -2
- package/pennyfarthing-dist/scripts/README.md +1 -1
- package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
- package/pennyfarthing-dist/scripts/core/check-context.sh +1 -1
- package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +322 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
- package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
- package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
- package/pennyfarthing-dist/scripts/git/README.md +24 -14
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +5 -266
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +5 -151
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +6 -144
- package/pennyfarthing-dist/scripts/git/release.sh +0 -0
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +5 -496
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/README.md +1 -1
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +9 -11
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
- package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
- package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
- package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
- package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +5 -13
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +4 -123
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +4 -33
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +4 -156
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +4 -131
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +4 -249
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +4 -160
- package/pennyfarthing-dist/skills/pf-bc/usage.md +1 -1
- package/pennyfarthing-dist/skills/pf-jira/examples.md +5 -2
- package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
- package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -16
- package/pennyfarthing-dist/skills/pf-workflow/skill.md +9 -12
- package/pennyfarthing-dist/skills/pf-workflow/usage.md +33 -8
- package/pennyfarthing-dist/workflows/bdd-tandem.yaml +18 -6
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
- package/pennyfarthing-dist/workflows/review-tandem.yaml +65 -0
- package/pennyfarthing-dist/workflows/tdd-tandem.yaml +16 -8
- package/pennyfarthing_scripts/CLAUDE.md +26 -4
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/cli.py +3 -5
- package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/portrait.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/background_panel.py +86 -5
- package/pennyfarthing_scripts/bikerack/base_panel.py +62 -0
- package/pennyfarthing_scripts/bikerack/changed_panel.py +32 -28
- package/pennyfarthing_scripts/bikerack/cli.py +10 -11
- package/pennyfarthing_scripts/bikerack/debug_panel.py +31 -1
- package/pennyfarthing_scripts/bikerack/diffs_panel.py +74 -17
- package/pennyfarthing_scripts/bikerack/git_panel.py +103 -33
- package/pennyfarthing_scripts/bikerack/launcher.py +15 -15
- package/pennyfarthing_scripts/bikerack/progress_panel.py +315 -0
- package/pennyfarthing_scripts/bikerack/sprint_panel.py +158 -26
- package/pennyfarthing_scripts/bikerack/tui.py +336 -30
- package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
- package/pennyfarthing_scripts/cli.py +37 -65
- package/pennyfarthing_scripts/consultation/__init__.py +1 -0
- package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/cli.py +149 -0
- package/pennyfarthing_scripts/consultation/dialogue_manager.py +417 -0
- package/pennyfarthing_scripts/context.py +3 -3
- package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__init__.py +12 -1
- package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/hooks_installer.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/repos.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/worktree.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/create_branches.py +3 -4
- package/pennyfarthing_scripts/git/hooks_installer.py +152 -0
- package/pennyfarthing_scripts/git/repos.py +196 -0
- package/pennyfarthing_scripts/git/status_all.py +27 -11
- package/pennyfarthing_scripts/git/worktree.py +302 -0
- package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/cli.py +143 -40
- package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/complete_phase.py +12 -0
- package/pennyfarthing_scripts/handoff/resolve_gate.py +5 -14
- package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing_scripts/hooks.py +3 -17
- package/pennyfarthing_scripts/pretooluse_hook.py +1 -1
- package/pennyfarthing_scripts/prime/__pycache__/heatmap.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/heatmap.py +655 -0
- package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/session/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/session_start_hook.py +1 -1
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/loader.py +15 -1
- package/pennyfarthing_scripts/sprint/story_finish.py +14 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_bikerack.py +51 -51
- package/pennyfarthing_scripts/tests/test_dialogue_manager.py +811 -0
- package/pennyfarthing_scripts/tests/test_handoff_cli.py +16 -11
- package/pennyfarthing_scripts/tests/test_workflow_check.py +2 -3
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/tandem_awareness.py +254 -0
- package/pennyfarthing_scripts/validate/cli.py +17 -5
- package/pennyfarthing_scripts/workflow/__init__.py +40 -0
- package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/cli.py +1099 -0
- package/pennyfarthing_scripts/workflow/helpers.py +241 -0
- package/pennyfarthing_scripts/{workflow.py → workflow/scale.py} +0 -104
- package/pennyfarthing_scripts/workflow/state.py +112 -0
- package/scripts/README.md +41 -0
- package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -91
- package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -163
- package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -138
- package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -273
- package/pennyfarthing-dist/skills/pf-workflow/scripts/workflow-status.sh +0 -167
|
@@ -8,11 +8,61 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
from rich.
|
|
11
|
+
from rich.text import Text
|
|
12
12
|
|
|
13
13
|
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def _file_breakdown(dirty_files: list[dict]) -> Text:
|
|
17
|
+
"""Break down dirty files into +staged ~modified ?untracked counts."""
|
|
18
|
+
staged = 0
|
|
19
|
+
modified = 0
|
|
20
|
+
untracked = 0
|
|
21
|
+
for f in dirty_files:
|
|
22
|
+
if not isinstance(f, dict):
|
|
23
|
+
continue
|
|
24
|
+
status = f.get("status", " ")
|
|
25
|
+
idx = status[0] if len(status) >= 1 else " "
|
|
26
|
+
wt = status[1] if len(status) >= 2 else " "
|
|
27
|
+
if idx == "?" and wt == "?":
|
|
28
|
+
untracked += 1
|
|
29
|
+
elif idx not in (" ", "?"):
|
|
30
|
+
staged += 1
|
|
31
|
+
elif wt not in (" ", "?"):
|
|
32
|
+
modified += 1
|
|
33
|
+
|
|
34
|
+
parts = Text()
|
|
35
|
+
parts.append(f"+{staged}", style="green")
|
|
36
|
+
parts.append(" ")
|
|
37
|
+
parts.append(f"~{modified}", style="yellow")
|
|
38
|
+
parts.append(" ")
|
|
39
|
+
parts.append(f"?{untracked}", style="dim")
|
|
40
|
+
return parts
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_FILE_STATUS_MAP: dict[str, tuple[str, str, str]] = {
|
|
44
|
+
"M": ("~", "Modified", "yellow"),
|
|
45
|
+
"A": ("+", "Added", "green"),
|
|
46
|
+
"D": ("-", "Deleted", "red"),
|
|
47
|
+
"?": ("?", "Untracked", "dim"),
|
|
48
|
+
"R": ("→", "Renamed", "cyan"),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _parse_file_status(status: str) -> tuple[str, str, str]:
|
|
53
|
+
"""Parse git status code into (icon, label, style)."""
|
|
54
|
+
if len(status) < 2:
|
|
55
|
+
return _FILE_STATUS_MAP.get(status[:1], ("·", "Changed", "yellow"))
|
|
56
|
+
idx, wt = status[0], status[1]
|
|
57
|
+
if idx == "?" and wt == "?":
|
|
58
|
+
return _FILE_STATUS_MAP["?"]
|
|
59
|
+
if idx not in (" ", "?"):
|
|
60
|
+
return _FILE_STATUS_MAP.get(idx, ("·", "Changed", "yellow"))
|
|
61
|
+
if wt not in (" ", "?"):
|
|
62
|
+
return _FILE_STATUS_MAP.get(wt, ("·", "Changed", "yellow"))
|
|
63
|
+
return ("·", "Changed", "yellow")
|
|
64
|
+
|
|
65
|
+
|
|
16
66
|
class GitPanel(BasePanel):
|
|
17
67
|
"""Multi-repo git status panel.
|
|
18
68
|
|
|
@@ -26,44 +76,64 @@ class GitPanel(BasePanel):
|
|
|
26
76
|
icon: str = PANEL_ICONS["git"][0]
|
|
27
77
|
|
|
28
78
|
def render_panel(self, payload: dict[str, Any]) -> Any:
|
|
29
|
-
"""Render git status as Rich
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for repo in
|
|
79
|
+
"""Render git status as Rich Tree with expandable file lists."""
|
|
80
|
+
from rich.console import Group as RichGroup
|
|
81
|
+
|
|
82
|
+
repos = payload.get("repos", [])
|
|
83
|
+
if not repos:
|
|
84
|
+
return Text("No repository data", style="dim italic")
|
|
85
|
+
|
|
86
|
+
parts: list[Any] = []
|
|
87
|
+
for repo in repos:
|
|
38
88
|
branch = repo.get("branch", "")
|
|
39
89
|
ahead = repo.get("ahead", 0)
|
|
40
90
|
behind = repo.get("behind", 0)
|
|
41
91
|
clean = repo.get("clean", True)
|
|
42
92
|
dirty_files = repo.get("dirtyFiles", [])
|
|
93
|
+
name = repo.get("name", "")
|
|
43
94
|
|
|
44
|
-
#
|
|
45
|
-
|
|
95
|
+
# Build repo header line
|
|
96
|
+
header = Text()
|
|
97
|
+
arrow = "▼" if not clean and dirty_files else "▶"
|
|
98
|
+
header.append(f"{arrow} ", style="bold")
|
|
99
|
+
header.append(name, style="bold cyan")
|
|
100
|
+
header.append(f" \ue0a0 {branch}", style="dim")
|
|
46
101
|
|
|
47
|
-
# Commits
|
|
48
|
-
|
|
102
|
+
# Commits
|
|
103
|
+
commit_parts = []
|
|
49
104
|
if ahead:
|
|
50
|
-
|
|
105
|
+
commit_parts.append(f"↑{ahead}")
|
|
51
106
|
if behind:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
107
|
+
commit_parts.append(f"↓{behind}")
|
|
108
|
+
header.append(f" {' '.join(commit_parts) if commit_parts else '—'}", style="dim")
|
|
109
|
+
|
|
110
|
+
# File breakdown
|
|
111
|
+
header.append(" ")
|
|
112
|
+
header.append_text(_file_breakdown(dirty_files))
|
|
113
|
+
|
|
114
|
+
# Status
|
|
115
|
+
header.append(" ")
|
|
116
|
+
if clean:
|
|
117
|
+
header.append("✓ clean", style="green")
|
|
118
|
+
else:
|
|
119
|
+
header.append("✗ dirty", style="red")
|
|
120
|
+
|
|
121
|
+
parts.append(header)
|
|
122
|
+
|
|
123
|
+
# Expanded file list for dirty repos
|
|
124
|
+
if not clean and dirty_files:
|
|
125
|
+
for f in dirty_files:
|
|
126
|
+
if not isinstance(f, dict):
|
|
127
|
+
continue
|
|
128
|
+
status_code = f.get("status", " ")
|
|
129
|
+
path = f.get("path", "")
|
|
130
|
+
icon, label, style = _parse_file_status(status_code)
|
|
131
|
+
file_line = Text()
|
|
132
|
+
file_line.append(" ")
|
|
133
|
+
file_line.append(icon, style=f"bold {style}")
|
|
134
|
+
file_line.append(f" {path}", style=style)
|
|
135
|
+
parts.append(file_line)
|
|
136
|
+
|
|
137
|
+
parts.append(Text("")) # spacer
|
|
138
|
+
|
|
139
|
+
return RichGroup(*parts)
|
|
@@ -24,8 +24,8 @@ def is_process_alive(pid: int) -> bool:
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def cleanup_files(project_dir: Path) -> None:
|
|
27
|
-
"""Clean up .
|
|
28
|
-
for name in (".
|
|
27
|
+
"""Clean up .wheelhub-port, .wheelhub-pid, and .wheelhub-gui-pid files."""
|
|
28
|
+
for name in (".wheelhub-port", ".wheelhub-pid", ".wheelhub-gui-pid"):
|
|
29
29
|
try:
|
|
30
30
|
(project_dir / name).unlink()
|
|
31
31
|
except FileNotFoundError:
|
|
@@ -33,24 +33,24 @@ def cleanup_files(project_dir: Path) -> None:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def read_port_file(project_dir: Path) -> int | None:
|
|
36
|
-
"""Read port from .
|
|
36
|
+
"""Read port from .wheelhub-port file. Returns None if not found."""
|
|
37
37
|
try:
|
|
38
|
-
return int((project_dir / ".
|
|
38
|
+
return int((project_dir / ".wheelhub-port").read_text().strip())
|
|
39
39
|
except (FileNotFoundError, ValueError):
|
|
40
40
|
return None
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def read_pid_file(project_dir: Path) -> int | None:
|
|
44
|
-
"""Read PID from .
|
|
44
|
+
"""Read PID from .wheelhub-pid file. Returns None if not found."""
|
|
45
45
|
try:
|
|
46
|
-
return int((project_dir / ".
|
|
46
|
+
return int((project_dir / ".wheelhub-pid").read_text().strip())
|
|
47
47
|
except (FileNotFoundError, ValueError):
|
|
48
48
|
return None
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def write_pid_file(project_dir: Path, pid: int) -> None:
|
|
52
|
-
"""Write .
|
|
53
|
-
(project_dir / ".
|
|
52
|
+
"""Write .wheelhub-pid file."""
|
|
53
|
+
(project_dir / ".wheelhub-pid").write_text(str(pid))
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def build_otel_env(port: int) -> dict[str, str]:
|
|
@@ -104,8 +104,8 @@ def start_wheelhub(project_dir: Path) -> subprocess.Popen:
|
|
|
104
104
|
def poll_for_port_file(
|
|
105
105
|
project_dir: Path, timeout: float = 5.0, interval: float = 0.1
|
|
106
106
|
) -> int:
|
|
107
|
-
"""Poll for .
|
|
108
|
-
port_file = project_dir / ".
|
|
107
|
+
"""Poll for .wheelhub-port file, return port number."""
|
|
108
|
+
port_file = project_dir / ".wheelhub-port"
|
|
109
109
|
deadline = time.monotonic() + timeout
|
|
110
110
|
|
|
111
111
|
while True:
|
|
@@ -209,23 +209,23 @@ def get_status(project_dir: Path) -> dict:
|
|
|
209
209
|
|
|
210
210
|
|
|
211
211
|
def read_tui_pid_file(project_dir: Path) -> int | None:
|
|
212
|
-
"""Read TUI PID from .
|
|
212
|
+
"""Read TUI PID from .wheelhub-gui-pid file. Returns None if not found."""
|
|
213
213
|
try:
|
|
214
|
-
return int((project_dir / ".
|
|
214
|
+
return int((project_dir / ".wheelhub-gui-pid").read_text().strip())
|
|
215
215
|
except (FileNotFoundError, ValueError):
|
|
216
216
|
return None
|
|
217
217
|
|
|
218
218
|
|
|
219
219
|
def write_tui_pid_file(project_dir: Path, pid: int) -> None:
|
|
220
|
-
"""Write .
|
|
221
|
-
(project_dir / ".
|
|
220
|
+
"""Write .wheelhub-gui-pid file."""
|
|
221
|
+
(project_dir / ".wheelhub-gui-pid").write_text(str(pid))
|
|
222
222
|
|
|
223
223
|
|
|
224
224
|
def start_tui(project_dir: Path, port: int) -> subprocess.Popen:
|
|
225
225
|
"""Start TUI as independent subprocess.
|
|
226
226
|
|
|
227
227
|
Uses start_new_session=True so TUI survives parent exit.
|
|
228
|
-
Writes .
|
|
228
|
+
Writes .wheelhub-gui-pid for lifecycle tracking.
|
|
229
229
|
"""
|
|
230
230
|
import sys
|
|
231
231
|
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""ProgressPanel — Unified story progress view for BikeRack TUI.
|
|
2
|
+
|
|
3
|
+
Combines story context, workflow phase, acceptance criteria, todos, and
|
|
4
|
+
git status into a single at-a-glance panel. Subscribes to 4 WS channels:
|
|
5
|
+
/ws/story, /ws/todos, /ws/git, /ws/sprint.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from rich.console import Group
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from pennyfarthing_scripts.bikerack.base_panel import (
|
|
16
|
+
PANEL_ICONS,
|
|
17
|
+
BasePanel,
|
|
18
|
+
render_progress_bar,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProgressPanel(BasePanel):
|
|
23
|
+
"""Unified story progress panel.
|
|
24
|
+
|
|
25
|
+
Subscribes to ``story``, ``todos``, ``git``, and ``sprint`` channels.
|
|
26
|
+
Renders a compact overview combining story header, workflow phase,
|
|
27
|
+
AC progress, todo progress, and git summary.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
channel: str = "story" # primary channel
|
|
31
|
+
panel_name: str = "Progress"
|
|
32
|
+
icon: str = PANEL_ICONS.get("progress", ("\uf200", "P"))[0]
|
|
33
|
+
|
|
34
|
+
def __init__(self, client: Any = None, **kwargs: Any) -> None:
|
|
35
|
+
super().__init__(client=client, **kwargs)
|
|
36
|
+
self._story_data: dict[str, Any] | None = None
|
|
37
|
+
self._todos_data: dict[str, Any] | None = None
|
|
38
|
+
self._git_data: dict[str, Any] | None = None
|
|
39
|
+
self._sprint_data: dict[str, Any] | None = None
|
|
40
|
+
|
|
41
|
+
def on_mount(self) -> None:
|
|
42
|
+
"""Subscribe to all 4 channels."""
|
|
43
|
+
self._mounted = True
|
|
44
|
+
if self._client is not None:
|
|
45
|
+
self._client.subscribe("story", self._handle_story)
|
|
46
|
+
self._client.subscribe("todos", self._handle_todos)
|
|
47
|
+
self._client.subscribe("git", self._handle_git)
|
|
48
|
+
self._client.subscribe("sprint", self._handle_sprint)
|
|
49
|
+
|
|
50
|
+
def _handle_story(self, message: dict[str, Any] | None) -> None:
|
|
51
|
+
if message is None:
|
|
52
|
+
return
|
|
53
|
+
self._story_data = message
|
|
54
|
+
self._rerender()
|
|
55
|
+
|
|
56
|
+
def _handle_todos(self, message: dict[str, Any] | None) -> None:
|
|
57
|
+
if message is None:
|
|
58
|
+
return
|
|
59
|
+
self._todos_data = message
|
|
60
|
+
self._rerender()
|
|
61
|
+
|
|
62
|
+
def _handle_git(self, message: dict[str, Any] | None) -> None:
|
|
63
|
+
if message is None:
|
|
64
|
+
return
|
|
65
|
+
self._git_data = message
|
|
66
|
+
self._rerender()
|
|
67
|
+
|
|
68
|
+
def _handle_sprint(self, message: dict[str, Any] | None) -> None:
|
|
69
|
+
if message is None:
|
|
70
|
+
return
|
|
71
|
+
self._sprint_data = message
|
|
72
|
+
self._rerender()
|
|
73
|
+
|
|
74
|
+
def _rerender(self) -> None:
|
|
75
|
+
"""Re-render with latest data from all channels."""
|
|
76
|
+
rendered = self.render_panel({})
|
|
77
|
+
try:
|
|
78
|
+
self.update(rendered)
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def render_panel(self, payload: dict[str, Any]) -> Any:
|
|
83
|
+
"""Render unified progress view."""
|
|
84
|
+
parts: list[Any] = []
|
|
85
|
+
|
|
86
|
+
# --- Story Header ---
|
|
87
|
+
story_header = self._render_story_header()
|
|
88
|
+
if story_header is None:
|
|
89
|
+
return Text(
|
|
90
|
+
"No active story \u2014 start with /sprint work",
|
|
91
|
+
style="dim italic",
|
|
92
|
+
)
|
|
93
|
+
parts.append(story_header)
|
|
94
|
+
parts.append(Text("\u2500" * 35, style="dim"))
|
|
95
|
+
|
|
96
|
+
# --- Workflow Phase ---
|
|
97
|
+
workflow = self._render_workflow()
|
|
98
|
+
if workflow is not None:
|
|
99
|
+
parts.append(workflow)
|
|
100
|
+
parts.append(Text("\u2500" * 35, style="dim"))
|
|
101
|
+
|
|
102
|
+
# --- Acceptance Criteria ---
|
|
103
|
+
ac = self._render_ac()
|
|
104
|
+
if ac is not None:
|
|
105
|
+
parts.append(ac)
|
|
106
|
+
parts.append(Text("\u2500" * 35, style="dim"))
|
|
107
|
+
|
|
108
|
+
# --- Todos ---
|
|
109
|
+
todos = self._render_todos()
|
|
110
|
+
if todos is not None:
|
|
111
|
+
parts.append(todos)
|
|
112
|
+
parts.append(Text("\u2500" * 35, style="dim"))
|
|
113
|
+
|
|
114
|
+
# --- Git Summary ---
|
|
115
|
+
git = self._render_git()
|
|
116
|
+
if git is not None:
|
|
117
|
+
parts.append(git)
|
|
118
|
+
|
|
119
|
+
return Group(*parts)
|
|
120
|
+
|
|
121
|
+
def _render_story_header(self) -> Text | None:
|
|
122
|
+
"""Render story ID, title, points, epic, assignee."""
|
|
123
|
+
story = self._story_data or {}
|
|
124
|
+
sprint = self._sprint_data or {}
|
|
125
|
+
|
|
126
|
+
# Try sprint data for current story context
|
|
127
|
+
current = sprint.get("sprint", {}).get("currentStory")
|
|
128
|
+
if isinstance(current, str) and current:
|
|
129
|
+
story_id = current
|
|
130
|
+
else:
|
|
131
|
+
story_id = story.get("id", "")
|
|
132
|
+
|
|
133
|
+
title = story.get("title", "")
|
|
134
|
+
points = story.get("points", "")
|
|
135
|
+
epic = story.get("epic", "")
|
|
136
|
+
assignee = story.get("assignee", "")
|
|
137
|
+
|
|
138
|
+
# Also try to extract from sprint epics
|
|
139
|
+
if not title and sprint:
|
|
140
|
+
for ep in sprint.get("epics", []):
|
|
141
|
+
for s in ep.get("stories", []):
|
|
142
|
+
if s.get("id") == story_id:
|
|
143
|
+
title = s.get("title", "")
|
|
144
|
+
points = s.get("points", "")
|
|
145
|
+
epic = ep.get("id", "")
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
if not story_id and not title:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
header = Text()
|
|
152
|
+
if story_id:
|
|
153
|
+
header.append(story_id, style="bold cyan")
|
|
154
|
+
header.append(" ")
|
|
155
|
+
if title:
|
|
156
|
+
header.append(title, style="bold")
|
|
157
|
+
if points:
|
|
158
|
+
header.append(f" {points}pt", style="dim")
|
|
159
|
+
|
|
160
|
+
# Second line: epic + assignee
|
|
161
|
+
meta_parts: list[str] = []
|
|
162
|
+
if epic:
|
|
163
|
+
meta_parts.append(f"Epic {epic}")
|
|
164
|
+
if assignee:
|
|
165
|
+
meta_parts.append(assignee)
|
|
166
|
+
if meta_parts:
|
|
167
|
+
header.append("\n")
|
|
168
|
+
header.append(" \u00b7 ".join(meta_parts), style="dim")
|
|
169
|
+
|
|
170
|
+
return header
|
|
171
|
+
|
|
172
|
+
def _render_workflow(self) -> Text | None:
|
|
173
|
+
"""Render workflow type badge and phase dots."""
|
|
174
|
+
story = self._story_data or {}
|
|
175
|
+
workflow = story.get("workflow", "")
|
|
176
|
+
phases = story.get("workflowPhases", [])
|
|
177
|
+
current_phase = story.get("phase", "")
|
|
178
|
+
|
|
179
|
+
if not phases:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
line = Text()
|
|
183
|
+
|
|
184
|
+
# Workflow type badge
|
|
185
|
+
if workflow:
|
|
186
|
+
line.append(f"[{workflow}]", style="bold")
|
|
187
|
+
line.append(" ")
|
|
188
|
+
|
|
189
|
+
# Phase dots
|
|
190
|
+
for i, phase in enumerate(phases):
|
|
191
|
+
phase_name = phase if isinstance(phase, str) else phase.get("name", "")
|
|
192
|
+
phase_status = ""
|
|
193
|
+
if isinstance(phase, dict):
|
|
194
|
+
phase_status = phase.get("status", "")
|
|
195
|
+
|
|
196
|
+
# Determine phase state
|
|
197
|
+
if phase_status == "done" or (current_phase and phase_name != current_phase and _phase_before(phase_name, current_phase, phases)):
|
|
198
|
+
line.append("\u2713", style="green")
|
|
199
|
+
elif phase_name == current_phase:
|
|
200
|
+
line.append("\u25cf", style="bold yellow")
|
|
201
|
+
else:
|
|
202
|
+
line.append("\u25cb", style="dim")
|
|
203
|
+
|
|
204
|
+
line.append(f" {phase_name}", style="bold" if phase_name == current_phase else "dim")
|
|
205
|
+
|
|
206
|
+
if i < len(phases) - 1:
|
|
207
|
+
line.append(" \u2192 ", style="dim")
|
|
208
|
+
|
|
209
|
+
return line
|
|
210
|
+
|
|
211
|
+
def _render_ac(self) -> Text | None:
|
|
212
|
+
"""Render acceptance criteria progress bar."""
|
|
213
|
+
story = self._story_data or {}
|
|
214
|
+
criteria = story.get("criteria", [])
|
|
215
|
+
if not criteria:
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
total = len(criteria)
|
|
219
|
+
done = sum(1 for c in criteria if isinstance(c, dict) and c.get("met"))
|
|
220
|
+
|
|
221
|
+
if total == 0:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
pct = int(done / total * 100)
|
|
225
|
+
line = Text()
|
|
226
|
+
line.append("AC ", style="bold")
|
|
227
|
+
line.append_text(render_progress_bar(pct, width=10))
|
|
228
|
+
line.append(f" {done}/{total}")
|
|
229
|
+
return line
|
|
230
|
+
|
|
231
|
+
def _render_todos(self) -> Text | None:
|
|
232
|
+
"""Render todo progress bar with active task."""
|
|
233
|
+
data = self._todos_data or {}
|
|
234
|
+
todos = data.get("todos", [])
|
|
235
|
+
if not todos:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
total = len(todos)
|
|
239
|
+
done = sum(1 for t in todos if isinstance(t, dict) and t.get("status") == "done")
|
|
240
|
+
active = None
|
|
241
|
+
for t in todos:
|
|
242
|
+
if isinstance(t, dict) and t.get("status") in ("in-progress", "active", "running"):
|
|
243
|
+
active = t.get("description", t.get("title", ""))
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
if total == 0:
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
pct = int(done / total * 100)
|
|
250
|
+
line = Text()
|
|
251
|
+
line.append("Todo ", style="bold")
|
|
252
|
+
line.append_text(render_progress_bar(pct, width=10))
|
|
253
|
+
line.append(f" {done}/{total}")
|
|
254
|
+
if active:
|
|
255
|
+
line.append(f" \u25cf {active}", style="yellow")
|
|
256
|
+
return line
|
|
257
|
+
|
|
258
|
+
def _render_git(self) -> Text | None:
|
|
259
|
+
"""Render git summary: branch, dirty counts, ahead/behind."""
|
|
260
|
+
data = self._git_data or {}
|
|
261
|
+
repos = data.get("repos", [])
|
|
262
|
+
if not repos:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
line = Text()
|
|
266
|
+
line.append("Git ", style="bold")
|
|
267
|
+
|
|
268
|
+
repo_parts: list[str] = []
|
|
269
|
+
for repo in repos:
|
|
270
|
+
if not isinstance(repo, dict):
|
|
271
|
+
continue
|
|
272
|
+
branch = repo.get("branch", "")
|
|
273
|
+
dirty_files = repo.get("dirtyFiles", [])
|
|
274
|
+
ahead = repo.get("ahead", 0)
|
|
275
|
+
behind = repo.get("behind", 0)
|
|
276
|
+
|
|
277
|
+
# Count file types
|
|
278
|
+
modified = 0
|
|
279
|
+
untracked = 0
|
|
280
|
+
for f in dirty_files:
|
|
281
|
+
if not isinstance(f, dict):
|
|
282
|
+
continue
|
|
283
|
+
status = f.get("status", " ")
|
|
284
|
+
if status.startswith("?"):
|
|
285
|
+
untracked += 1
|
|
286
|
+
else:
|
|
287
|
+
modified += 1
|
|
288
|
+
|
|
289
|
+
part = Text()
|
|
290
|
+
if branch:
|
|
291
|
+
part.append(branch, style="cyan")
|
|
292
|
+
part.append(f" {modified}M", style="yellow" if modified else "dim")
|
|
293
|
+
part.append(f" {untracked}U", style="dim")
|
|
294
|
+
part.append(f" \u2191{ahead}", style="green" if ahead else "dim")
|
|
295
|
+
part.append(f" \u2193{behind}", style="red" if behind else "dim")
|
|
296
|
+
|
|
297
|
+
line.append_text(part)
|
|
298
|
+
|
|
299
|
+
# Only show first repo on main line, rest on separate lines
|
|
300
|
+
break
|
|
301
|
+
|
|
302
|
+
return line
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _phase_before(phase: str, current: str, phases: list) -> bool:
|
|
306
|
+
"""Check if phase comes before current in the phases list."""
|
|
307
|
+
phase_idx = -1
|
|
308
|
+
current_idx = -1
|
|
309
|
+
for i, p in enumerate(phases):
|
|
310
|
+
name = p if isinstance(p, str) else p.get("name", "")
|
|
311
|
+
if name == phase:
|
|
312
|
+
phase_idx = i
|
|
313
|
+
if name == current:
|
|
314
|
+
current_idx = i
|
|
315
|
+
return phase_idx >= 0 and current_idx >= 0 and phase_idx < current_idx
|