erk 0.4.5__py3-none-any.whl
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.
- erk/__init__.py +12 -0
- erk/__main__.py +6 -0
- erk/agent_docs/__init__.py +5 -0
- erk/agent_docs/models.py +123 -0
- erk/agent_docs/operations.py +666 -0
- erk/artifacts/__init__.py +5 -0
- erk/artifacts/artifact_health.py +623 -0
- erk/artifacts/detection.py +16 -0
- erk/artifacts/discovery.py +343 -0
- erk/artifacts/models.py +63 -0
- erk/artifacts/staleness.py +56 -0
- erk/artifacts/state.py +100 -0
- erk/artifacts/sync.py +624 -0
- erk/cli/__init__.py +0 -0
- erk/cli/activation.py +132 -0
- erk/cli/alias.py +53 -0
- erk/cli/cli.py +221 -0
- erk/cli/commands/__init__.py +0 -0
- erk/cli/commands/admin.py +153 -0
- erk/cli/commands/artifact/__init__.py +1 -0
- erk/cli/commands/artifact/check.py +260 -0
- erk/cli/commands/artifact/group.py +31 -0
- erk/cli/commands/artifact/list_cmd.py +89 -0
- erk/cli/commands/artifact/show.py +62 -0
- erk/cli/commands/artifact/sync_cmd.py +39 -0
- erk/cli/commands/branch/__init__.py +26 -0
- erk/cli/commands/branch/assign_cmd.py +152 -0
- erk/cli/commands/branch/checkout_cmd.py +357 -0
- erk/cli/commands/branch/create_cmd.py +161 -0
- erk/cli/commands/branch/list_cmd.py +82 -0
- erk/cli/commands/branch/unassign_cmd.py +197 -0
- erk/cli/commands/cc/__init__.py +15 -0
- erk/cli/commands/cc/jsonl_cmd.py +20 -0
- erk/cli/commands/cc/session/AGENTS.md +30 -0
- erk/cli/commands/cc/session/CLAUDE.md +1 -0
- erk/cli/commands/cc/session/__init__.py +15 -0
- erk/cli/commands/cc/session/list_cmd.py +167 -0
- erk/cli/commands/cc/session/show_cmd.py +175 -0
- erk/cli/commands/completion.py +89 -0
- erk/cli/commands/completions.py +165 -0
- erk/cli/commands/config.py +327 -0
- erk/cli/commands/docs/__init__.py +1 -0
- erk/cli/commands/docs/group.py +16 -0
- erk/cli/commands/docs/sync.py +121 -0
- erk/cli/commands/docs/validate.py +102 -0
- erk/cli/commands/doctor.py +243 -0
- erk/cli/commands/down.py +171 -0
- erk/cli/commands/exec/__init__.py +1 -0
- erk/cli/commands/exec/group.py +164 -0
- erk/cli/commands/exec/scripts/AGENTS.md +79 -0
- erk/cli/commands/exec/scripts/CLAUDE.md +1 -0
- erk/cli/commands/exec/scripts/__init__.py +5 -0
- erk/cli/commands/exec/scripts/add_reaction_to_comment.py +69 -0
- erk/cli/commands/exec/scripts/add_remote_execution_note.py +68 -0
- erk/cli/commands/exec/scripts/check_impl.py +152 -0
- erk/cli/commands/exec/scripts/ci_update_pr_body.py +294 -0
- erk/cli/commands/exec/scripts/create_extraction_branch.py +138 -0
- erk/cli/commands/exec/scripts/create_extraction_plan.py +242 -0
- erk/cli/commands/exec/scripts/create_issue_from_session.py +103 -0
- erk/cli/commands/exec/scripts/create_plan_from_context.py +103 -0
- erk/cli/commands/exec/scripts/create_worker_impl_from_issue.py +93 -0
- erk/cli/commands/exec/scripts/detect_trunk_branch.py +121 -0
- erk/cli/commands/exec/scripts/exit_plan_mode_hook.py +777 -0
- erk/cli/commands/exec/scripts/extract_latest_plan.py +49 -0
- erk/cli/commands/exec/scripts/extract_session_from_issue.py +150 -0
- erk/cli/commands/exec/scripts/find_project_dir.py +214 -0
- erk/cli/commands/exec/scripts/generate_pr_summary.py +112 -0
- erk/cli/commands/exec/scripts/get_closing_text.py +98 -0
- erk/cli/commands/exec/scripts/get_embedded_prompt.py +62 -0
- erk/cli/commands/exec/scripts/get_plan_metadata.py +95 -0
- erk/cli/commands/exec/scripts/get_pr_body_footer.py +70 -0
- erk/cli/commands/exec/scripts/get_pr_discussion_comments.py +149 -0
- erk/cli/commands/exec/scripts/get_pr_review_comments.py +155 -0
- erk/cli/commands/exec/scripts/impl_init.py +158 -0
- erk/cli/commands/exec/scripts/impl_signal.py +375 -0
- erk/cli/commands/exec/scripts/impl_verify.py +49 -0
- erk/cli/commands/exec/scripts/issue_title_to_filename.py +34 -0
- erk/cli/commands/exec/scripts/list_sessions.py +296 -0
- erk/cli/commands/exec/scripts/mark_impl_ended.py +188 -0
- erk/cli/commands/exec/scripts/mark_impl_started.py +188 -0
- erk/cli/commands/exec/scripts/marker.py +163 -0
- erk/cli/commands/exec/scripts/objective_save_to_issue.py +109 -0
- erk/cli/commands/exec/scripts/plan_save_to_issue.py +269 -0
- erk/cli/commands/exec/scripts/plan_update_issue.py +147 -0
- erk/cli/commands/exec/scripts/post_extraction_comment.py +237 -0
- erk/cli/commands/exec/scripts/post_or_update_pr_summary.py +133 -0
- erk/cli/commands/exec/scripts/post_pr_inline_comment.py +143 -0
- erk/cli/commands/exec/scripts/post_workflow_started_comment.py +168 -0
- erk/cli/commands/exec/scripts/preprocess_session.py +777 -0
- erk/cli/commands/exec/scripts/quick_submit.py +32 -0
- erk/cli/commands/exec/scripts/rebase_with_conflict_resolution.py +260 -0
- erk/cli/commands/exec/scripts/reply_to_discussion_comment.py +173 -0
- erk/cli/commands/exec/scripts/resolve_review_thread.py +170 -0
- erk/cli/commands/exec/scripts/session_id_injector_hook.py +52 -0
- erk/cli/commands/exec/scripts/setup_impl_from_issue.py +159 -0
- erk/cli/commands/exec/scripts/slot_objective.py +102 -0
- erk/cli/commands/exec/scripts/tripwires_reminder_hook.py +20 -0
- erk/cli/commands/exec/scripts/update_dispatch_info.py +116 -0
- erk/cli/commands/exec/scripts/user_prompt_hook.py +113 -0
- erk/cli/commands/exec/scripts/validate_plan_content.py +98 -0
- erk/cli/commands/exec/scripts/wrap_plan_in_metadata_block.py +34 -0
- erk/cli/commands/implement.py +695 -0
- erk/cli/commands/implement_shared.py +649 -0
- erk/cli/commands/info/__init__.py +14 -0
- erk/cli/commands/info/release_notes_cmd.py +128 -0
- erk/cli/commands/init.py +801 -0
- erk/cli/commands/land_cmd.py +690 -0
- erk/cli/commands/log_cmd.py +137 -0
- erk/cli/commands/md/__init__.py +5 -0
- erk/cli/commands/md/check.py +118 -0
- erk/cli/commands/md/group.py +14 -0
- erk/cli/commands/navigation_helpers.py +430 -0
- erk/cli/commands/objective/__init__.py +16 -0
- erk/cli/commands/objective/list_cmd.py +47 -0
- erk/cli/commands/objective_helpers.py +132 -0
- erk/cli/commands/plan/__init__.py +32 -0
- erk/cli/commands/plan/check_cmd.py +174 -0
- erk/cli/commands/plan/close_cmd.py +69 -0
- erk/cli/commands/plan/create_cmd.py +120 -0
- erk/cli/commands/plan/docs/__init__.py +18 -0
- erk/cli/commands/plan/docs/extract_cmd.py +53 -0
- erk/cli/commands/plan/docs/unextract_cmd.py +38 -0
- erk/cli/commands/plan/docs/unextracted_cmd.py +72 -0
- erk/cli/commands/plan/extraction/__init__.py +16 -0
- erk/cli/commands/plan/extraction/complete_cmd.py +101 -0
- erk/cli/commands/plan/extraction/create_raw_cmd.py +63 -0
- erk/cli/commands/plan/get.py +71 -0
- erk/cli/commands/plan/list_cmd.py +754 -0
- erk/cli/commands/plan/log_cmd.py +440 -0
- erk/cli/commands/plan/start_cmd.py +459 -0
- erk/cli/commands/planner/__init__.py +40 -0
- erk/cli/commands/planner/configure_cmd.py +73 -0
- erk/cli/commands/planner/connect_cmd.py +96 -0
- erk/cli/commands/planner/create_cmd.py +148 -0
- erk/cli/commands/planner/list_cmd.py +51 -0
- erk/cli/commands/planner/register_cmd.py +105 -0
- erk/cli/commands/planner/set_default_cmd.py +23 -0
- erk/cli/commands/planner/unregister_cmd.py +43 -0
- erk/cli/commands/pr/__init__.py +23 -0
- erk/cli/commands/pr/check_cmd.py +112 -0
- erk/cli/commands/pr/checkout_cmd.py +165 -0
- erk/cli/commands/pr/fix_conflicts_cmd.py +82 -0
- erk/cli/commands/pr/parse_pr_reference.py +10 -0
- erk/cli/commands/pr/submit_cmd.py +360 -0
- erk/cli/commands/pr/sync_cmd.py +181 -0
- erk/cli/commands/prepare_cwd_recovery.py +60 -0
- erk/cli/commands/project/__init__.py +16 -0
- erk/cli/commands/project/init_cmd.py +91 -0
- erk/cli/commands/run/__init__.py +17 -0
- erk/cli/commands/run/list_cmd.py +189 -0
- erk/cli/commands/run/logs_cmd.py +54 -0
- erk/cli/commands/run/shared.py +19 -0
- erk/cli/commands/shell_integration.py +29 -0
- erk/cli/commands/slot/__init__.py +23 -0
- erk/cli/commands/slot/check_cmd.py +277 -0
- erk/cli/commands/slot/common.py +314 -0
- erk/cli/commands/slot/init_pool_cmd.py +157 -0
- erk/cli/commands/slot/list_cmd.py +228 -0
- erk/cli/commands/slot/repair_cmd.py +190 -0
- erk/cli/commands/stack/__init__.py +23 -0
- erk/cli/commands/stack/consolidate_cmd.py +470 -0
- erk/cli/commands/stack/list_cmd.py +79 -0
- erk/cli/commands/stack/move_cmd.py +309 -0
- erk/cli/commands/stack/split_old/README.md +64 -0
- erk/cli/commands/stack/split_old/__init__.py +5 -0
- erk/cli/commands/stack/split_old/command.py +233 -0
- erk/cli/commands/stack/split_old/display.py +116 -0
- erk/cli/commands/stack/split_old/plan.py +216 -0
- erk/cli/commands/status.py +58 -0
- erk/cli/commands/submit.py +768 -0
- erk/cli/commands/up.py +154 -0
- erk/cli/commands/upgrade.py +82 -0
- erk/cli/commands/wt/__init__.py +29 -0
- erk/cli/commands/wt/checkout_cmd.py +110 -0
- erk/cli/commands/wt/create_cmd.py +998 -0
- erk/cli/commands/wt/current_cmd.py +35 -0
- erk/cli/commands/wt/delete_cmd.py +573 -0
- erk/cli/commands/wt/list_cmd.py +332 -0
- erk/cli/commands/wt/rename_cmd.py +66 -0
- erk/cli/config.py +242 -0
- erk/cli/constants.py +29 -0
- erk/cli/core.py +65 -0
- erk/cli/debug.py +9 -0
- erk/cli/ensure-conversion-tasks.md +288 -0
- erk/cli/ensure.py +628 -0
- erk/cli/github_parsing.py +96 -0
- erk/cli/graphite.py +81 -0
- erk/cli/graphite_command.py +80 -0
- erk/cli/help_formatter.py +345 -0
- erk/cli/output.py +361 -0
- erk/cli/presets/dagster.toml +12 -0
- erk/cli/presets/generic.toml +12 -0
- erk/cli/prompt_hooks_templates/README.md +68 -0
- erk/cli/script_output.py +32 -0
- erk/cli/shell_integration/bash_wrapper.sh +32 -0
- erk/cli/shell_integration/fish_wrapper.fish +39 -0
- erk/cli/shell_integration/handler.py +338 -0
- erk/cli/shell_integration/zsh_wrapper.sh +32 -0
- erk/cli/shell_utils.py +171 -0
- erk/cli/subprocess_utils.py +92 -0
- erk/cli/uvx_detection.py +59 -0
- erk/core/__init__.py +0 -0
- erk/core/claude_executor.py +511 -0
- erk/core/claude_settings.py +317 -0
- erk/core/command_log.py +406 -0
- erk/core/commit_message_generator.py +234 -0
- erk/core/completion.py +10 -0
- erk/core/consolidation_utils.py +177 -0
- erk/core/context.py +570 -0
- erk/core/display/__init__.py +4 -0
- erk/core/display/abc.py +24 -0
- erk/core/display/real.py +30 -0
- erk/core/display_utils.py +526 -0
- erk/core/file_utils.py +87 -0
- erk/core/health_checks.py +1315 -0
- erk/core/health_checks_dogfooder/__init__.py +85 -0
- erk/core/health_checks_dogfooder/deprecated_dot_agent_config.py +64 -0
- erk/core/health_checks_dogfooder/legacy_claude_docs.py +69 -0
- erk/core/health_checks_dogfooder/legacy_config_locations.py +122 -0
- erk/core/health_checks_dogfooder/legacy_erk_docs_agent.py +61 -0
- erk/core/health_checks_dogfooder/legacy_erk_kits_folder.py +60 -0
- erk/core/health_checks_dogfooder/legacy_hook_settings.py +104 -0
- erk/core/health_checks_dogfooder/legacy_kit_yaml.py +78 -0
- erk/core/health_checks_dogfooder/legacy_kits_toml.py +43 -0
- erk/core/health_checks_dogfooder/outdated_erk_skill.py +43 -0
- erk/core/implementation_queue/__init__.py +1 -0
- erk/core/implementation_queue/github/__init__.py +8 -0
- erk/core/implementation_queue/github/abc.py +7 -0
- erk/core/implementation_queue/github/noop.py +38 -0
- erk/core/implementation_queue/github/printing.py +43 -0
- erk/core/implementation_queue/github/real.py +119 -0
- erk/core/init_utils.py +227 -0
- erk/core/output_filter.py +338 -0
- erk/core/plan_store/__init__.py +6 -0
- erk/core/planner/__init__.py +1 -0
- erk/core/planner/registry_abc.py +8 -0
- erk/core/planner/registry_fake.py +129 -0
- erk/core/planner/registry_real.py +195 -0
- erk/core/planner/types.py +7 -0
- erk/core/pr_utils.py +30 -0
- erk/core/release_notes.py +263 -0
- erk/core/repo_discovery.py +126 -0
- erk/core/script_writer.py +41 -0
- erk/core/services/__init__.py +1 -0
- erk/core/services/plan_list_service.py +94 -0
- erk/core/shell.py +51 -0
- erk/core/user_feedback.py +11 -0
- erk/core/version_check.py +55 -0
- erk/core/workflow_display.py +75 -0
- erk/core/worktree_pool.py +190 -0
- erk/core/worktree_utils.py +300 -0
- erk/data/CHANGELOG.md +438 -0
- erk/data/__init__.py +1 -0
- erk/data/claude/agents/devrun.md +180 -0
- erk/data/claude/commands/erk/__init__.py +0 -0
- erk/data/claude/commands/erk/create-extraction-plan.md +360 -0
- erk/data/claude/commands/erk/fix-conflicts.md +25 -0
- erk/data/claude/commands/erk/git-pr-push.md +345 -0
- erk/data/claude/commands/erk/implement-stacked-plan.md +96 -0
- erk/data/claude/commands/erk/land.md +193 -0
- erk/data/claude/commands/erk/objective-create.md +370 -0
- erk/data/claude/commands/erk/objective-list.md +34 -0
- erk/data/claude/commands/erk/objective-next-plan.md +220 -0
- erk/data/claude/commands/erk/objective-update-with-landed-pr.md +216 -0
- erk/data/claude/commands/erk/plan-implement.md +202 -0
- erk/data/claude/commands/erk/plan-save.md +45 -0
- erk/data/claude/commands/erk/plan-submit.md +39 -0
- erk/data/claude/commands/erk/pr-address.md +367 -0
- erk/data/claude/commands/erk/pr-submit.md +58 -0
- erk/data/claude/skills/dignified-python/SKILL.md +48 -0
- erk/data/claude/skills/dignified-python/cli-patterns.md +155 -0
- erk/data/claude/skills/dignified-python/dignified-python-core.md +1190 -0
- erk/data/claude/skills/dignified-python/subprocess.md +99 -0
- erk/data/claude/skills/dignified-python/versions/python-3.10.md +517 -0
- erk/data/claude/skills/dignified-python/versions/python-3.11.md +536 -0
- erk/data/claude/skills/dignified-python/versions/python-3.12.md +662 -0
- erk/data/claude/skills/dignified-python/versions/python-3.13.md +653 -0
- erk/data/claude/skills/erk-diff-analysis/SKILL.md +27 -0
- erk/data/claude/skills/erk-diff-analysis/references/commit-message-prompt.md +78 -0
- erk/data/claude/skills/learned-docs/SKILL.md +362 -0
- erk/data/github/actions/setup-claude-erk/action.yml +11 -0
- erk/data/github/prompts/dignified-python-review.md +125 -0
- erk/data/github/workflows/dignified-python-review.yml +61 -0
- erk/data/github/workflows/erk-impl.yml +251 -0
- erk/hooks/__init__.py +1 -0
- erk/hooks/decorators.py +319 -0
- erk/status/__init__.py +8 -0
- erk/status/collectors/__init__.py +9 -0
- erk/status/collectors/base.py +52 -0
- erk/status/collectors/git.py +76 -0
- erk/status/collectors/github.py +81 -0
- erk/status/collectors/graphite.py +80 -0
- erk/status/collectors/impl.py +145 -0
- erk/status/models/__init__.py +4 -0
- erk/status/models/status_data.py +404 -0
- erk/status/orchestrator.py +169 -0
- erk/status/renderers/__init__.py +5 -0
- erk/status/renderers/simple.py +322 -0
- erk/tui/AGENTS.md +193 -0
- erk/tui/CLAUDE.md +1 -0
- erk/tui/__init__.py +1 -0
- erk/tui/app.py +1404 -0
- erk/tui/commands/__init__.py +1 -0
- erk/tui/commands/executor.py +66 -0
- erk/tui/commands/provider.py +165 -0
- erk/tui/commands/real_executor.py +63 -0
- erk/tui/commands/registry.py +121 -0
- erk/tui/commands/types.py +36 -0
- erk/tui/data/__init__.py +1 -0
- erk/tui/data/provider.py +492 -0
- erk/tui/data/types.py +104 -0
- erk/tui/filtering/__init__.py +1 -0
- erk/tui/filtering/logic.py +43 -0
- erk/tui/filtering/types.py +55 -0
- erk/tui/jsonl_viewer/__init__.py +1 -0
- erk/tui/jsonl_viewer/app.py +61 -0
- erk/tui/jsonl_viewer/models.py +208 -0
- erk/tui/jsonl_viewer/widgets.py +204 -0
- erk/tui/sorting/__init__.py +6 -0
- erk/tui/sorting/logic.py +55 -0
- erk/tui/sorting/types.py +68 -0
- erk/tui/styles/dash.tcss +95 -0
- erk/tui/widgets/__init__.py +1 -0
- erk/tui/widgets/command_output.py +112 -0
- erk/tui/widgets/plan_table.py +276 -0
- erk/tui/widgets/status_bar.py +116 -0
- erk-0.4.5.dist-info/METADATA +376 -0
- erk-0.4.5.dist-info/RECORD +331 -0
- erk-0.4.5.dist-info/WHEEL +4 -0
- erk-0.4.5.dist-info/entry_points.txt +2 -0
- erk-0.4.5.dist-info/licenses/LICENSE.md +3 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
"""Command to list plans with filtering."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import ParamSpec, TypeVar
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console, Group
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from erk.cli.core import discover_repo_context
|
|
13
|
+
from erk.core.context import ErkContext
|
|
14
|
+
from erk.core.display import LiveDisplay
|
|
15
|
+
from erk.core.display_utils import (
|
|
16
|
+
format_relative_time,
|
|
17
|
+
format_workflow_outcome,
|
|
18
|
+
format_workflow_run_id,
|
|
19
|
+
get_workflow_run_state,
|
|
20
|
+
)
|
|
21
|
+
from erk.core.pr_utils import select_display_pr
|
|
22
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
23
|
+
from erk.tui.app import ErkDashApp
|
|
24
|
+
from erk.tui.data.provider import RealPlanDataProvider
|
|
25
|
+
from erk.tui.data.types import PlanFilters
|
|
26
|
+
from erk.tui.sorting.types import SortKey, SortState
|
|
27
|
+
from erk_shared.gateway.browser.real import RealBrowserLauncher
|
|
28
|
+
from erk_shared.gateway.clipboard.real import RealClipboard
|
|
29
|
+
from erk_shared.github.emoji import format_checks_cell, get_pr_status_emoji
|
|
30
|
+
from erk_shared.github.issues import IssueInfo
|
|
31
|
+
from erk_shared.github.metadata.plan_header import (
|
|
32
|
+
extract_plan_header_local_impl_at,
|
|
33
|
+
extract_plan_header_local_impl_event,
|
|
34
|
+
extract_plan_header_remote_impl_at,
|
|
35
|
+
extract_plan_header_source_repo,
|
|
36
|
+
extract_plan_header_worktree_name,
|
|
37
|
+
)
|
|
38
|
+
from erk_shared.github.types import GitHubRepoId, GitHubRepoLocation, PullRequestInfo
|
|
39
|
+
from erk_shared.impl_folder import read_issue_reference
|
|
40
|
+
from erk_shared.output.output import user_output
|
|
41
|
+
from erk_shared.plan_store.types import Plan, PlanState
|
|
42
|
+
|
|
43
|
+
P = ParamSpec("P")
|
|
44
|
+
T = TypeVar("T")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _issue_to_plan(issue: IssueInfo) -> Plan:
|
|
48
|
+
"""Convert IssueInfo to Plan format.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
issue: IssueInfo from GraphQL query
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Plan object with equivalent data
|
|
55
|
+
"""
|
|
56
|
+
# Map issue state to PlanState
|
|
57
|
+
state = PlanState.OPEN if issue.state == "OPEN" else PlanState.CLOSED
|
|
58
|
+
|
|
59
|
+
return Plan(
|
|
60
|
+
plan_identifier=str(issue.number),
|
|
61
|
+
title=issue.title,
|
|
62
|
+
body=issue.body,
|
|
63
|
+
state=state,
|
|
64
|
+
url=issue.url,
|
|
65
|
+
labels=issue.labels,
|
|
66
|
+
assignees=issue.assignees,
|
|
67
|
+
created_at=issue.created_at,
|
|
68
|
+
updated_at=issue.updated_at,
|
|
69
|
+
metadata={"number": issue.number},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def format_pr_cell(pr: PullRequestInfo, *, use_graphite: bool, graphite_url: str | None) -> str:
|
|
74
|
+
"""Format PR cell with clickable link and emoji: #123 👀 or #123 👀🔗
|
|
75
|
+
|
|
76
|
+
The 🔗 emoji is appended for PRs that will auto-close the linked issue when merged.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
pr: PR information
|
|
80
|
+
use_graphite: If True, use Graphite URL; if False, use GitHub URL
|
|
81
|
+
graphite_url: Graphite URL for the PR (None if unavailable)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Formatted string for table cell with OSC 8 hyperlink
|
|
85
|
+
"""
|
|
86
|
+
emoji = get_pr_status_emoji(pr)
|
|
87
|
+
pr_text = f"#{pr.number}"
|
|
88
|
+
|
|
89
|
+
# Append 🔗 for PRs that will close the issue when merged
|
|
90
|
+
if pr.will_close_target:
|
|
91
|
+
emoji += "🔗"
|
|
92
|
+
|
|
93
|
+
# Determine which URL to use
|
|
94
|
+
url = graphite_url if use_graphite else pr.url
|
|
95
|
+
|
|
96
|
+
# Make PR number clickable if URL is available
|
|
97
|
+
# Rich supports OSC 8 via [link=...] markup
|
|
98
|
+
if url:
|
|
99
|
+
return f"[link={url}]{pr_text}[/link] {emoji}"
|
|
100
|
+
else:
|
|
101
|
+
return f"{pr_text} {emoji}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def format_worktree_name_cell(worktree_name: str, exists_locally: bool) -> str:
|
|
105
|
+
"""Format worktree name with existence styling.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
worktree_name: Name of the worktree
|
|
109
|
+
exists_locally: Whether the worktree exists on the local machine
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Formatted string with Rich markup:
|
|
113
|
+
- Exists locally: "[yellow]name[/yellow]"
|
|
114
|
+
- Doesn't exist: "-"
|
|
115
|
+
"""
|
|
116
|
+
if not exists_locally:
|
|
117
|
+
return "-"
|
|
118
|
+
return f"[yellow]{worktree_name}[/yellow]"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def format_local_run_cell(
|
|
122
|
+
last_local_impl_at: str | None,
|
|
123
|
+
last_local_impl_event: str | None,
|
|
124
|
+
) -> str:
|
|
125
|
+
"""Format last local implementation event as relative time with indicator.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
last_local_impl_at: ISO timestamp of last local implementation, or None
|
|
129
|
+
last_local_impl_event: Event type ("started" or "ended"), or None
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Relative time string with event indicator (e.g., "⟳ 2h" or "✓ 2h") or "-" if no timestamp
|
|
133
|
+
"""
|
|
134
|
+
relative_time = format_relative_time(last_local_impl_at)
|
|
135
|
+
if not relative_time:
|
|
136
|
+
return "-"
|
|
137
|
+
|
|
138
|
+
# Add event indicator
|
|
139
|
+
if last_local_impl_event == "started":
|
|
140
|
+
return f"⟳ {relative_time}"
|
|
141
|
+
if last_local_impl_event == "ended":
|
|
142
|
+
return f"✓ {relative_time}"
|
|
143
|
+
|
|
144
|
+
# Fallback for missing event (backward compatibility)
|
|
145
|
+
return relative_time
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def format_remote_run_cell(last_remote_impl_at: str | None) -> str:
|
|
149
|
+
"""Format last remote implementation timestamp as relative time.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
last_remote_impl_at: ISO timestamp of last remote (GitHub Actions) implementation, or None
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Relative time string (e.g., "2h ago") or "-" if no timestamp
|
|
156
|
+
"""
|
|
157
|
+
relative_time = format_relative_time(last_remote_impl_at)
|
|
158
|
+
return relative_time if relative_time else "-"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def plan_filter_options(f: Callable[P, T]) -> Callable[P, T]:
|
|
162
|
+
"""Shared filter options for plan list commands."""
|
|
163
|
+
f = click.option(
|
|
164
|
+
"--label",
|
|
165
|
+
multiple=True,
|
|
166
|
+
help="Filter by label (can be specified multiple times for AND logic)",
|
|
167
|
+
)(f)
|
|
168
|
+
f = click.option(
|
|
169
|
+
"--state",
|
|
170
|
+
type=click.Choice(["open", "closed"], case_sensitive=False),
|
|
171
|
+
help="Filter by state",
|
|
172
|
+
)(f)
|
|
173
|
+
f = click.option(
|
|
174
|
+
"--run-state",
|
|
175
|
+
type=click.Choice(
|
|
176
|
+
["queued", "in_progress", "success", "failure", "cancelled"], case_sensitive=False
|
|
177
|
+
),
|
|
178
|
+
help="Filter by workflow run state",
|
|
179
|
+
)(f)
|
|
180
|
+
f = click.option(
|
|
181
|
+
"--limit",
|
|
182
|
+
type=int,
|
|
183
|
+
help="Maximum number of results to return",
|
|
184
|
+
)(f)
|
|
185
|
+
f = click.option(
|
|
186
|
+
"--all-users",
|
|
187
|
+
"-A",
|
|
188
|
+
is_flag=True,
|
|
189
|
+
default=False,
|
|
190
|
+
help="Show plans from all users (default: show only your plans)",
|
|
191
|
+
)(f)
|
|
192
|
+
f = click.option(
|
|
193
|
+
"--sort",
|
|
194
|
+
type=click.Choice(["issue", "activity"], case_sensitive=False),
|
|
195
|
+
default="issue",
|
|
196
|
+
help="Sort order: by issue number (default) or recent branch activity",
|
|
197
|
+
)(f)
|
|
198
|
+
return f
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def dash_options(f: Callable[P, T]) -> Callable[P, T]:
|
|
202
|
+
"""TUI-specific options for dash command."""
|
|
203
|
+
f = click.option(
|
|
204
|
+
"--interval",
|
|
205
|
+
type=float,
|
|
206
|
+
default=15.0,
|
|
207
|
+
help="Refresh interval in seconds (default: 15.0)",
|
|
208
|
+
)(f)
|
|
209
|
+
return f
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _build_plans_table(
|
|
213
|
+
ctx: ErkContext,
|
|
214
|
+
label: tuple[str, ...],
|
|
215
|
+
state: str | None,
|
|
216
|
+
run_state: str | None,
|
|
217
|
+
runs: bool,
|
|
218
|
+
limit: int | None,
|
|
219
|
+
all_users: bool,
|
|
220
|
+
sort: str,
|
|
221
|
+
) -> tuple[Table | None, int]:
|
|
222
|
+
"""Build plan dashboard table.
|
|
223
|
+
|
|
224
|
+
Uses PlanListService to batch all API calls into 2 total:
|
|
225
|
+
1. Single unified GraphQL query for issues + PR linkages
|
|
226
|
+
2. REST API calls for workflow runs (one per issue with run_id)
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Tuple of (table, plan_count). Table is None if no plans found.
|
|
230
|
+
"""
|
|
231
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
232
|
+
ensure_erk_metadata_dir(repo) # Ensure erk metadata directories exist
|
|
233
|
+
repo_root = repo.root # Use git repository root for GitHub operations
|
|
234
|
+
|
|
235
|
+
# Build labels list - default to ["erk-plan"] if no labels specified
|
|
236
|
+
labels_list = list(label) if label else ["erk-plan"]
|
|
237
|
+
|
|
238
|
+
# Determine if we need workflow runs (for display or filtering)
|
|
239
|
+
needs_workflow_runs = runs or run_state is not None
|
|
240
|
+
|
|
241
|
+
# Get owner/repo from RepoContext (already populated via git remote URL parsing)
|
|
242
|
+
if repo.github is None:
|
|
243
|
+
user_output(click.style("Error: ", fg="red") + "Could not determine repository owner/name")
|
|
244
|
+
raise SystemExit(1)
|
|
245
|
+
owner = repo.github.owner
|
|
246
|
+
repo_name = repo.github.repo
|
|
247
|
+
|
|
248
|
+
# Determine creator filter: None for all users, authenticated username otherwise
|
|
249
|
+
creator: str | None = None
|
|
250
|
+
if not all_users:
|
|
251
|
+
is_authenticated, username, _ = ctx.github.check_auth_status()
|
|
252
|
+
if is_authenticated and username:
|
|
253
|
+
creator = username
|
|
254
|
+
|
|
255
|
+
# Use PlanListService for batched API calls
|
|
256
|
+
# Skip workflow runs when not needed for better performance
|
|
257
|
+
# PR linkages are always fetched via unified GraphQL query (no performance penalty)
|
|
258
|
+
try:
|
|
259
|
+
location = GitHubRepoLocation(root=repo_root, repo_id=GitHubRepoId(owner, repo_name))
|
|
260
|
+
plan_data = ctx.plan_list_service.get_plan_list_data(
|
|
261
|
+
location=location,
|
|
262
|
+
labels=labels_list,
|
|
263
|
+
state=state,
|
|
264
|
+
limit=limit,
|
|
265
|
+
skip_workflow_runs=not needs_workflow_runs,
|
|
266
|
+
creator=creator,
|
|
267
|
+
)
|
|
268
|
+
except RuntimeError as e:
|
|
269
|
+
user_output(click.style("Error: ", fg="red") + str(e))
|
|
270
|
+
raise SystemExit(1) from e
|
|
271
|
+
|
|
272
|
+
# Convert IssueInfo to Plan objects
|
|
273
|
+
plans = [_issue_to_plan(issue) for issue in plan_data.issues]
|
|
274
|
+
|
|
275
|
+
if not plans:
|
|
276
|
+
return None, 0
|
|
277
|
+
|
|
278
|
+
# Use pre-fetched data from PlanListService
|
|
279
|
+
pr_linkages = plan_data.pr_linkages
|
|
280
|
+
workflow_runs = plan_data.workflow_runs
|
|
281
|
+
|
|
282
|
+
# Build local worktree mapping from .impl/issue.json files
|
|
283
|
+
worktree_by_issue: dict[int, str] = {}
|
|
284
|
+
worktrees = ctx.git.list_worktrees(repo_root)
|
|
285
|
+
for worktree in worktrees:
|
|
286
|
+
impl_folder = worktree.path / ".impl"
|
|
287
|
+
if impl_folder.exists() and impl_folder.is_dir():
|
|
288
|
+
issue_ref = read_issue_reference(impl_folder)
|
|
289
|
+
if issue_ref is not None:
|
|
290
|
+
# If multiple worktrees have same issue, keep first found
|
|
291
|
+
if issue_ref.issue_number not in worktree_by_issue:
|
|
292
|
+
worktree_by_issue[issue_ref.issue_number] = worktree.path.name
|
|
293
|
+
|
|
294
|
+
# Apply run state filter if specified
|
|
295
|
+
if run_state:
|
|
296
|
+
filtered_plans: list[Plan] = []
|
|
297
|
+
for plan in plans:
|
|
298
|
+
# Get workflow run (keyed by issue number)
|
|
299
|
+
plan_issue_number = plan.metadata.get("number")
|
|
300
|
+
workflow_run = None
|
|
301
|
+
if isinstance(plan_issue_number, int):
|
|
302
|
+
workflow_run = workflow_runs.get(plan_issue_number)
|
|
303
|
+
if workflow_run is None:
|
|
304
|
+
# No workflow run - skip this plan when filtering
|
|
305
|
+
continue
|
|
306
|
+
plan_run_state = get_workflow_run_state(workflow_run)
|
|
307
|
+
if plan_run_state == run_state:
|
|
308
|
+
filtered_plans.append(plan)
|
|
309
|
+
plans = filtered_plans
|
|
310
|
+
|
|
311
|
+
# Check if filtering resulted in no plans
|
|
312
|
+
if not plans:
|
|
313
|
+
return None, 0
|
|
314
|
+
|
|
315
|
+
# Build activity timestamps for display column (always computed)
|
|
316
|
+
trunk = ctx.git.detect_trunk_branch(repo_root)
|
|
317
|
+
|
|
318
|
+
# Build issue -> branch mapping from worktrees
|
|
319
|
+
issue_to_branch: dict[int, str] = {}
|
|
320
|
+
for wt in worktrees:
|
|
321
|
+
impl_folder = wt.path / ".impl"
|
|
322
|
+
if impl_folder.exists() and impl_folder.is_dir():
|
|
323
|
+
issue_ref = read_issue_reference(impl_folder)
|
|
324
|
+
if issue_ref is not None and wt.branch is not None:
|
|
325
|
+
issue_to_branch[issue_ref.issue_number] = wt.branch
|
|
326
|
+
|
|
327
|
+
# Build activity timestamps for display and sorting
|
|
328
|
+
activity_by_issue: dict[int, str] = {}
|
|
329
|
+
for issue_num, branch in issue_to_branch.items():
|
|
330
|
+
timestamp = ctx.git.get_branch_last_commit_time(repo_root, branch, trunk)
|
|
331
|
+
if timestamp is not None:
|
|
332
|
+
activity_by_issue[issue_num] = timestamp
|
|
333
|
+
|
|
334
|
+
# Apply activity-based sorting if requested
|
|
335
|
+
if sort == "activity":
|
|
336
|
+
|
|
337
|
+
def get_last_commit_time(plan: Plan) -> tuple[bool, datetime]:
|
|
338
|
+
"""Return sort key: (has_local_activity, commit_time).
|
|
339
|
+
|
|
340
|
+
Plans with local activity sort first (by recency), then others by issue#.
|
|
341
|
+
"""
|
|
342
|
+
issue_number = plan.metadata.get("number")
|
|
343
|
+
if not isinstance(issue_number, int) or issue_number not in activity_by_issue:
|
|
344
|
+
return (False, datetime.min)
|
|
345
|
+
return (True, datetime.fromisoformat(activity_by_issue[issue_number]))
|
|
346
|
+
|
|
347
|
+
# Sort: plans with local activity first (by recency), then others by issue#
|
|
348
|
+
plans = sorted(plans, key=get_last_commit_time, reverse=True)
|
|
349
|
+
|
|
350
|
+
# Determine use_graphite for URL selection
|
|
351
|
+
use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
|
|
352
|
+
|
|
353
|
+
# Check if any plan has source_repo (for cross-repo plans column)
|
|
354
|
+
has_cross_repo_plans = any(
|
|
355
|
+
plan.body and extract_plan_header_source_repo(plan.body) for plan in plans
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Create Rich table with columns
|
|
359
|
+
table = Table(show_header=True, header_style="bold")
|
|
360
|
+
table.add_column("plan", style="cyan", no_wrap=True)
|
|
361
|
+
table.add_column("title", no_wrap=True)
|
|
362
|
+
if has_cross_repo_plans:
|
|
363
|
+
table.add_column("impl-repo", no_wrap=True)
|
|
364
|
+
table.add_column("pr", no_wrap=True)
|
|
365
|
+
table.add_column("chks", no_wrap=True)
|
|
366
|
+
table.add_column("lcl-wt", no_wrap=True)
|
|
367
|
+
table.add_column("lcl-actvty", no_wrap=True)
|
|
368
|
+
table.add_column("lcl-impl", no_wrap=True)
|
|
369
|
+
if runs:
|
|
370
|
+
table.add_column("remote-impl", no_wrap=True)
|
|
371
|
+
table.add_column("run-id", no_wrap=True)
|
|
372
|
+
table.add_column("run-state", no_wrap=True, width=12)
|
|
373
|
+
|
|
374
|
+
# Populate table rows
|
|
375
|
+
for plan in plans:
|
|
376
|
+
# Format issue number with clickable OSC 8 hyperlink
|
|
377
|
+
id_text = f"#{plan.plan_identifier}"
|
|
378
|
+
colored_id = f"[cyan]{id_text}[/cyan]"
|
|
379
|
+
|
|
380
|
+
# Make ID clickable using OSC 8 if URL is available
|
|
381
|
+
if plan.url:
|
|
382
|
+
# Rich library supports OSC 8 via markup syntax
|
|
383
|
+
issue_id = f"[link={plan.url}]{colored_id}[/link]"
|
|
384
|
+
else:
|
|
385
|
+
issue_id = colored_id
|
|
386
|
+
|
|
387
|
+
# Truncate title to 50 characters with ellipsis
|
|
388
|
+
title = plan.title
|
|
389
|
+
if len(title) > 50:
|
|
390
|
+
title = title[:47] + "..."
|
|
391
|
+
|
|
392
|
+
# Query worktree status - check local .impl/issue.json first, then issue body
|
|
393
|
+
issue_number = plan.metadata.get("number")
|
|
394
|
+
worktree_name = ""
|
|
395
|
+
exists_locally = False
|
|
396
|
+
last_local_impl_at: str | None = None
|
|
397
|
+
last_local_impl_event: str | None = None
|
|
398
|
+
last_remote_impl_at: str | None = None
|
|
399
|
+
|
|
400
|
+
# Check local mapping first (worktree exists locally)
|
|
401
|
+
if isinstance(issue_number, int) and issue_number in worktree_by_issue:
|
|
402
|
+
worktree_name = worktree_by_issue[issue_number]
|
|
403
|
+
exists_locally = True
|
|
404
|
+
|
|
405
|
+
# Extract from issue body - worktree may or may not exist locally
|
|
406
|
+
source_repo: str | None = None
|
|
407
|
+
if plan.body:
|
|
408
|
+
extracted = extract_plan_header_worktree_name(plan.body)
|
|
409
|
+
if extracted:
|
|
410
|
+
# If we don't have a local name yet, use the one from issue body
|
|
411
|
+
if not worktree_name:
|
|
412
|
+
worktree_name = extracted
|
|
413
|
+
# Extract implementation timestamps and event
|
|
414
|
+
last_local_impl_at = extract_plan_header_local_impl_at(plan.body)
|
|
415
|
+
last_local_impl_event = extract_plan_header_local_impl_event(plan.body)
|
|
416
|
+
last_remote_impl_at = extract_plan_header_remote_impl_at(plan.body)
|
|
417
|
+
# Extract source_repo for cross-repo plans
|
|
418
|
+
source_repo = extract_plan_header_source_repo(plan.body)
|
|
419
|
+
|
|
420
|
+
# Format the worktree cells
|
|
421
|
+
worktree_name_cell = format_worktree_name_cell(worktree_name, exists_locally)
|
|
422
|
+
local_run_cell = format_local_run_cell(last_local_impl_at, last_local_impl_event)
|
|
423
|
+
remote_run_cell = format_remote_run_cell(last_remote_impl_at)
|
|
424
|
+
|
|
425
|
+
# Get PR info for this issue
|
|
426
|
+
pr_cell = "-"
|
|
427
|
+
checks_cell = "-"
|
|
428
|
+
if isinstance(issue_number, int) and issue_number in pr_linkages:
|
|
429
|
+
issue_prs = pr_linkages[issue_number]
|
|
430
|
+
selected_pr = select_display_pr(issue_prs)
|
|
431
|
+
if selected_pr is not None:
|
|
432
|
+
graphite_url = ctx.graphite.get_graphite_url(
|
|
433
|
+
GitHubRepoId(selected_pr.owner, selected_pr.repo), selected_pr.number
|
|
434
|
+
)
|
|
435
|
+
pr_cell = format_pr_cell(
|
|
436
|
+
selected_pr, use_graphite=use_graphite, graphite_url=graphite_url
|
|
437
|
+
)
|
|
438
|
+
checks_cell = format_checks_cell(selected_pr)
|
|
439
|
+
|
|
440
|
+
# Get workflow run for this plan (keyed by issue number)
|
|
441
|
+
run_id_cell = "-"
|
|
442
|
+
workflow_run = None
|
|
443
|
+
if isinstance(issue_number, int):
|
|
444
|
+
workflow_run = workflow_runs.get(issue_number)
|
|
445
|
+
if workflow_run is not None:
|
|
446
|
+
# Build workflow URL from plan.url attribute
|
|
447
|
+
workflow_url = None
|
|
448
|
+
if plan.url:
|
|
449
|
+
# Parse owner/repo from URL like https://github.com/owner/repo/issues/123
|
|
450
|
+
parts = plan.url.split("/")
|
|
451
|
+
if len(parts) >= 5:
|
|
452
|
+
owner = parts[-4]
|
|
453
|
+
repo_name = parts[-3]
|
|
454
|
+
workflow_url = (
|
|
455
|
+
f"https://github.com/{owner}/{repo_name}/actions/runs/{workflow_run.run_id}"
|
|
456
|
+
)
|
|
457
|
+
# Format the run ID with linkification
|
|
458
|
+
run_id_cell = format_workflow_run_id(workflow_run, workflow_url)
|
|
459
|
+
|
|
460
|
+
# Format workflow run outcome
|
|
461
|
+
run_outcome_cell = format_workflow_outcome(workflow_run)
|
|
462
|
+
|
|
463
|
+
# Format activity cell (last commit time on local branch)
|
|
464
|
+
activity_cell = "-"
|
|
465
|
+
if isinstance(issue_number, int) and issue_number in activity_by_issue:
|
|
466
|
+
activity_cell = format_relative_time(activity_by_issue[issue_number]) or "-"
|
|
467
|
+
|
|
468
|
+
# Build row based on which columns are enabled
|
|
469
|
+
row: list[str] = [
|
|
470
|
+
issue_id,
|
|
471
|
+
title,
|
|
472
|
+
]
|
|
473
|
+
if has_cross_repo_plans:
|
|
474
|
+
# Show just repo name (owner/repo -> repo) for brevity
|
|
475
|
+
impl_repo_cell = source_repo.split("/")[-1] if source_repo else "-"
|
|
476
|
+
row.append(impl_repo_cell)
|
|
477
|
+
row.extend(
|
|
478
|
+
[
|
|
479
|
+
pr_cell,
|
|
480
|
+
checks_cell,
|
|
481
|
+
worktree_name_cell,
|
|
482
|
+
activity_cell,
|
|
483
|
+
local_run_cell,
|
|
484
|
+
]
|
|
485
|
+
)
|
|
486
|
+
if runs:
|
|
487
|
+
row.extend([remote_run_cell, run_id_cell, run_outcome_cell])
|
|
488
|
+
table.add_row(*row)
|
|
489
|
+
|
|
490
|
+
return table, len(plans)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _list_plans_impl(
|
|
494
|
+
ctx: ErkContext,
|
|
495
|
+
label: tuple[str, ...],
|
|
496
|
+
state: str | None,
|
|
497
|
+
run_state: str | None,
|
|
498
|
+
runs: bool,
|
|
499
|
+
limit: int | None,
|
|
500
|
+
all_users: bool,
|
|
501
|
+
sort: str,
|
|
502
|
+
) -> None:
|
|
503
|
+
"""Implementation logic for listing plans with optional filters."""
|
|
504
|
+
table, plan_count = _build_plans_table(
|
|
505
|
+
ctx, label, state, run_state, runs, limit, all_users, sort
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if table is None:
|
|
509
|
+
user_output("No plans found matching the criteria.")
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
# Display results header
|
|
513
|
+
user_output(f"\nFound {plan_count} plan(s):\n")
|
|
514
|
+
|
|
515
|
+
# Output table to stderr (consistent with user_output convention)
|
|
516
|
+
# Use width=200 to ensure proper display without truncation
|
|
517
|
+
# force_terminal=True ensures hyperlinks render even when Rich doesn't detect a TTY
|
|
518
|
+
console = Console(stderr=True, width=200, force_terminal=True)
|
|
519
|
+
console.print(table)
|
|
520
|
+
console.print() # Add blank line after table
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _build_watch_content(
|
|
524
|
+
table: Table | None,
|
|
525
|
+
count: int,
|
|
526
|
+
last_update: str,
|
|
527
|
+
seconds_remaining: int,
|
|
528
|
+
fetch_duration_secs: float | None = None,
|
|
529
|
+
) -> Group | Panel:
|
|
530
|
+
"""Build display content for watch mode.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
table: The plans table, or None if no plans
|
|
534
|
+
count: Number of plans found
|
|
535
|
+
last_update: Formatted time of last data refresh
|
|
536
|
+
seconds_remaining: Seconds until next refresh
|
|
537
|
+
fetch_duration_secs: Duration of last data fetch in seconds, or None
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
Rich renderable content for the display
|
|
541
|
+
"""
|
|
542
|
+
# Build duration suffix
|
|
543
|
+
duration_suffix = f" ({fetch_duration_secs:.1f}s)" if fetch_duration_secs is not None else ""
|
|
544
|
+
|
|
545
|
+
footer = (
|
|
546
|
+
f"Found {count} plan(s) | Updated: {last_update}{duration_suffix} | "
|
|
547
|
+
f"Next refresh: {seconds_remaining}s | Ctrl+C to exit"
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if table is None:
|
|
551
|
+
return Panel(f"No plans found.\n\n{footer}", title="erk dash --watch")
|
|
552
|
+
else:
|
|
553
|
+
return Group(table, Panel(footer, style="dim"))
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _run_watch_loop(
|
|
557
|
+
ctx: ErkContext,
|
|
558
|
+
live_display: LiveDisplay,
|
|
559
|
+
build_table_fn: Callable[[], tuple[Table | None, int]],
|
|
560
|
+
interval: float,
|
|
561
|
+
) -> None:
|
|
562
|
+
"""Run watch loop until KeyboardInterrupt.
|
|
563
|
+
|
|
564
|
+
Updates display every second with countdown timer. Fetches fresh data
|
|
565
|
+
when countdown reaches zero.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
ctx: ErkContext with time abstraction
|
|
569
|
+
live_display: Display renderer for live updates
|
|
570
|
+
build_table_fn: Function that returns (table, count)
|
|
571
|
+
interval: Seconds between data refreshes
|
|
572
|
+
"""
|
|
573
|
+
live_display.start()
|
|
574
|
+
try:
|
|
575
|
+
# Initial data fetch - with timing
|
|
576
|
+
start = ctx.time.now()
|
|
577
|
+
table, count = build_table_fn()
|
|
578
|
+
fetch_duration_secs = (ctx.time.now() - start).total_seconds()
|
|
579
|
+
last_update = ctx.time.now().strftime("%H:%M:%S")
|
|
580
|
+
seconds_remaining = int(interval)
|
|
581
|
+
|
|
582
|
+
while True:
|
|
583
|
+
# Update display with current countdown
|
|
584
|
+
content = _build_watch_content(
|
|
585
|
+
table, count, last_update, seconds_remaining, fetch_duration_secs
|
|
586
|
+
)
|
|
587
|
+
live_display.update(content)
|
|
588
|
+
|
|
589
|
+
# Sleep for 1 second
|
|
590
|
+
ctx.time.sleep(1.0)
|
|
591
|
+
seconds_remaining -= 1
|
|
592
|
+
|
|
593
|
+
# Refresh data when countdown reaches zero
|
|
594
|
+
if seconds_remaining <= 0:
|
|
595
|
+
start = ctx.time.now()
|
|
596
|
+
table, count = build_table_fn()
|
|
597
|
+
fetch_duration_secs = (ctx.time.now() - start).total_seconds()
|
|
598
|
+
last_update = ctx.time.now().strftime("%H:%M:%S")
|
|
599
|
+
seconds_remaining = int(interval)
|
|
600
|
+
except KeyboardInterrupt:
|
|
601
|
+
pass
|
|
602
|
+
finally:
|
|
603
|
+
live_display.stop()
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _run_interactive_mode(
|
|
607
|
+
ctx: ErkContext,
|
|
608
|
+
label: tuple[str, ...],
|
|
609
|
+
state: str | None,
|
|
610
|
+
run_state: str | None,
|
|
611
|
+
runs: bool,
|
|
612
|
+
prs: bool,
|
|
613
|
+
limit: int | None,
|
|
614
|
+
interval: float,
|
|
615
|
+
all_users: bool,
|
|
616
|
+
sort: str,
|
|
617
|
+
) -> None:
|
|
618
|
+
"""Run interactive TUI mode.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
ctx: ErkContext with all dependencies
|
|
622
|
+
label: Labels to filter by
|
|
623
|
+
state: State filter ("open", "closed", or None)
|
|
624
|
+
run_state: Workflow run state filter
|
|
625
|
+
runs: Whether to show run columns
|
|
626
|
+
prs: Whether to show PR columns
|
|
627
|
+
limit: Maximum number of results
|
|
628
|
+
interval: Refresh interval in seconds
|
|
629
|
+
all_users: If True, show plans from all users; if False, filter to authenticated user
|
|
630
|
+
sort: Sort order ("issue" or "activity")
|
|
631
|
+
"""
|
|
632
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
633
|
+
ensure_erk_metadata_dir(repo)
|
|
634
|
+
repo_root = repo.root
|
|
635
|
+
|
|
636
|
+
# Get owner/repo from RepoContext (already populated via git remote URL parsing)
|
|
637
|
+
if repo.github is None:
|
|
638
|
+
user_output(click.style("Error: ", fg="red") + "Could not determine repository owner/name")
|
|
639
|
+
raise SystemExit(1)
|
|
640
|
+
owner = repo.github.owner
|
|
641
|
+
repo_name = repo.github.repo
|
|
642
|
+
|
|
643
|
+
# Determine creator filter: None for all users, authenticated username otherwise
|
|
644
|
+
creator: str | None = None
|
|
645
|
+
if not all_users:
|
|
646
|
+
is_authenticated, username, _ = ctx.github.check_auth_status()
|
|
647
|
+
if is_authenticated and username:
|
|
648
|
+
creator = username
|
|
649
|
+
|
|
650
|
+
# Build labels - default to ["erk-plan"]
|
|
651
|
+
labels = label if label else ("erk-plan",)
|
|
652
|
+
|
|
653
|
+
# Create data provider and filters
|
|
654
|
+
location = GitHubRepoLocation(root=repo_root, repo_id=GitHubRepoId(owner, repo_name))
|
|
655
|
+
clipboard = RealClipboard()
|
|
656
|
+
browser = RealBrowserLauncher()
|
|
657
|
+
provider = RealPlanDataProvider(ctx, location, clipboard, browser)
|
|
658
|
+
filters = PlanFilters(
|
|
659
|
+
labels=labels,
|
|
660
|
+
state=state,
|
|
661
|
+
run_state=run_state,
|
|
662
|
+
limit=limit,
|
|
663
|
+
show_prs=prs,
|
|
664
|
+
show_runs=runs,
|
|
665
|
+
creator=creator,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
# Convert sort string to SortState
|
|
669
|
+
initial_sort = SortState(
|
|
670
|
+
key=SortKey.BRANCH_ACTIVITY if sort == "activity" else SortKey.ISSUE_NUMBER
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
# Run the TUI app
|
|
674
|
+
app = ErkDashApp(provider, filters, refresh_interval=interval, initial_sort=initial_sort)
|
|
675
|
+
app.run()
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
@click.command("list")
|
|
679
|
+
@plan_filter_options
|
|
680
|
+
@click.option(
|
|
681
|
+
"--runs",
|
|
682
|
+
"-r",
|
|
683
|
+
is_flag=True,
|
|
684
|
+
default=False,
|
|
685
|
+
help="Show workflow run columns (run-id, run-state)",
|
|
686
|
+
)
|
|
687
|
+
@click.pass_obj
|
|
688
|
+
def list_plans(
|
|
689
|
+
ctx: ErkContext,
|
|
690
|
+
label: tuple[str, ...],
|
|
691
|
+
state: str | None,
|
|
692
|
+
run_state: str | None,
|
|
693
|
+
limit: int | None,
|
|
694
|
+
all_users: bool,
|
|
695
|
+
sort: str,
|
|
696
|
+
runs: bool,
|
|
697
|
+
) -> None:
|
|
698
|
+
"""List plans as a static table.
|
|
699
|
+
|
|
700
|
+
By default, shows only plans created by you. Use --all-users (-A)
|
|
701
|
+
to show plans from all users.
|
|
702
|
+
|
|
703
|
+
Examples:
|
|
704
|
+
erk plan list # Your plans only
|
|
705
|
+
erk plan list --all-users # All users' plans
|
|
706
|
+
erk plan list -A # All users' plans (short form)
|
|
707
|
+
erk plan list --state open
|
|
708
|
+
erk plan list --label erk-plan --label bug
|
|
709
|
+
erk plan list --limit 10
|
|
710
|
+
erk plan list --run-state in_progress
|
|
711
|
+
erk plan list --runs
|
|
712
|
+
erk plan list --sort activity # Sort by recent branch activity
|
|
713
|
+
"""
|
|
714
|
+
_list_plans_impl(ctx, label, state, run_state, runs, limit, all_users, sort)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
@click.command("dash")
|
|
718
|
+
@plan_filter_options
|
|
719
|
+
@dash_options
|
|
720
|
+
@click.pass_obj
|
|
721
|
+
def dash(
|
|
722
|
+
ctx: ErkContext,
|
|
723
|
+
label: tuple[str, ...],
|
|
724
|
+
state: str | None,
|
|
725
|
+
run_state: str | None,
|
|
726
|
+
limit: int | None,
|
|
727
|
+
all_users: bool,
|
|
728
|
+
sort: str, # noqa: ARG001 # Accepted from shared options but not used by TUI
|
|
729
|
+
interval: float,
|
|
730
|
+
) -> None:
|
|
731
|
+
"""Interactive plan dashboard (TUI).
|
|
732
|
+
|
|
733
|
+
By default, shows only plans created by you. Use --all-users (-A)
|
|
734
|
+
to show plans from all users.
|
|
735
|
+
|
|
736
|
+
Launches an interactive terminal UI for viewing and managing plans.
|
|
737
|
+
Shows all columns (runs) by default. For a static table output, use
|
|
738
|
+
'erk plan list' instead.
|
|
739
|
+
|
|
740
|
+
Examples:
|
|
741
|
+
erk dash # Your plans only
|
|
742
|
+
erk dash --all-users # All users' plans
|
|
743
|
+
erk dash -A # All users' plans (short form)
|
|
744
|
+
erk dash --interval 10
|
|
745
|
+
erk dash --label erk-plan --state open
|
|
746
|
+
erk dash --limit 10
|
|
747
|
+
erk dash --run-state in_progress
|
|
748
|
+
erk dash --sort activity # Sort by recent branch activity
|
|
749
|
+
"""
|
|
750
|
+
# Default to showing all columns (runs=True)
|
|
751
|
+
prs = True # Always show PRs
|
|
752
|
+
runs = True # Default to showing runs
|
|
753
|
+
|
|
754
|
+
_run_interactive_mode(ctx, label, state, run_state, runs, prs, limit, interval, all_users, sort)
|