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
erk/tui/data/provider.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""Data provider for TUI plan table."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from erk.core.context import ErkContext
|
|
9
|
+
from erk.core.display_utils import (
|
|
10
|
+
format_relative_time,
|
|
11
|
+
format_workflow_outcome,
|
|
12
|
+
format_workflow_run_id,
|
|
13
|
+
get_workflow_run_state,
|
|
14
|
+
)
|
|
15
|
+
from erk.core.pr_utils import select_display_pr
|
|
16
|
+
from erk.core.repo_discovery import NoRepoSentinel, RepoContext, ensure_erk_metadata_dir
|
|
17
|
+
from erk.tui.data.types import PlanFilters, PlanRowData
|
|
18
|
+
from erk.tui.sorting.types import BranchActivity
|
|
19
|
+
from erk_shared.gateway.browser.abc import BrowserLauncher
|
|
20
|
+
from erk_shared.gateway.clipboard.abc import Clipboard
|
|
21
|
+
from erk_shared.github.emoji import format_checks_cell, get_pr_status_emoji
|
|
22
|
+
from erk_shared.github.issues import IssueInfo
|
|
23
|
+
from erk_shared.github.metadata.plan_header import (
|
|
24
|
+
extract_plan_header_local_impl_at,
|
|
25
|
+
extract_plan_header_remote_impl_at,
|
|
26
|
+
extract_plan_header_worktree_name,
|
|
27
|
+
)
|
|
28
|
+
from erk_shared.github.parsing import github_repo_location_from_url
|
|
29
|
+
from erk_shared.github.types import GitHubRepoId, GitHubRepoLocation, PullRequestInfo, WorkflowRun
|
|
30
|
+
from erk_shared.naming import extract_leading_issue_number
|
|
31
|
+
from erk_shared.plan_store.types import Plan, PlanState
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PlanDataProvider(ABC):
|
|
35
|
+
"""Abstract base class for plan data providers.
|
|
36
|
+
|
|
37
|
+
Defines the interface for fetching plan data for TUI display.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def repo_root(self) -> Path:
|
|
43
|
+
"""Get the repository root path.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Path to the repository root directory
|
|
47
|
+
"""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def clipboard(self) -> Clipboard:
|
|
53
|
+
"""Get the clipboard interface for copy operations.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Clipboard interface for copying to system clipboard
|
|
57
|
+
"""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def browser(self) -> BrowserLauncher:
|
|
63
|
+
"""Get the browser launcher interface for opening URLs.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
BrowserLauncher interface for opening URLs in browser
|
|
67
|
+
"""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def fetch_plans(self, filters: PlanFilters) -> list[PlanRowData]:
|
|
72
|
+
"""Fetch plans matching the given filters.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
filters: Filter options for the query
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of PlanRowData objects for display
|
|
79
|
+
"""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def close_plan(self, issue_number: int, issue_url: str) -> list[int]:
|
|
84
|
+
"""Close a plan and its linked PRs.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
issue_number: The issue number to close
|
|
88
|
+
issue_url: The issue URL for PR linkage lookup
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of PR numbers that were also closed
|
|
92
|
+
"""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def submit_to_queue(self, issue_number: int, issue_url: str) -> None:
|
|
97
|
+
"""Submit a plan to the implementation queue.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
issue_number: The issue number to submit
|
|
101
|
+
issue_url: The issue URL for repository context
|
|
102
|
+
"""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def fetch_branch_activity(self, rows: list[PlanRowData]) -> dict[int, BranchActivity]:
|
|
107
|
+
"""Fetch branch activity for plans that exist locally.
|
|
108
|
+
|
|
109
|
+
Examines commits on each local branch (not in trunk) to determine
|
|
110
|
+
the most recent activity.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
rows: List of plan rows to fetch activity for
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Mapping of issue_number to BranchActivity for plans with local worktrees.
|
|
117
|
+
Plans without local worktrees are not included in the result.
|
|
118
|
+
"""
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class RealPlanDataProvider(PlanDataProvider):
|
|
123
|
+
"""Production implementation that wraps PlanListService.
|
|
124
|
+
|
|
125
|
+
Transforms PlanListData into PlanRowData for TUI display.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
ctx: ErkContext,
|
|
131
|
+
location: GitHubRepoLocation,
|
|
132
|
+
clipboard: Clipboard,
|
|
133
|
+
browser: BrowserLauncher,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Initialize with context and repository info.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
ctx: ErkContext with all dependencies
|
|
139
|
+
location: GitHub repository location (local root + repo identity)
|
|
140
|
+
clipboard: Clipboard interface for copy operations
|
|
141
|
+
browser: BrowserLauncher interface for opening URLs
|
|
142
|
+
"""
|
|
143
|
+
self._ctx = ctx
|
|
144
|
+
self._location = location
|
|
145
|
+
self._clipboard = clipboard
|
|
146
|
+
self._browser = browser
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def repo_root(self) -> Path:
|
|
150
|
+
"""Get the repository root path."""
|
|
151
|
+
return self._location.root
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def clipboard(self) -> Clipboard:
|
|
155
|
+
"""Get the clipboard interface for copy operations."""
|
|
156
|
+
return self._clipboard
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def browser(self) -> BrowserLauncher:
|
|
160
|
+
"""Get the browser launcher interface for opening URLs."""
|
|
161
|
+
return self._browser
|
|
162
|
+
|
|
163
|
+
def fetch_plans(self, filters: PlanFilters) -> list[PlanRowData]:
|
|
164
|
+
"""Fetch plans and transform to TUI row format.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
filters: Filter options for the query
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
List of PlanRowData objects for display
|
|
171
|
+
"""
|
|
172
|
+
# Determine if we need workflow runs
|
|
173
|
+
needs_workflow_runs = filters.show_runs or filters.run_state is not None
|
|
174
|
+
|
|
175
|
+
# Fetch data via PlanListService
|
|
176
|
+
# Note: PR linkages are always fetched via unified GraphQL query (no performance penalty)
|
|
177
|
+
plan_data = self._ctx.plan_list_service.get_plan_list_data(
|
|
178
|
+
location=self._location,
|
|
179
|
+
labels=list(filters.labels),
|
|
180
|
+
state=filters.state,
|
|
181
|
+
limit=filters.limit,
|
|
182
|
+
skip_workflow_runs=not needs_workflow_runs,
|
|
183
|
+
creator=filters.creator,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Build local worktree mapping
|
|
187
|
+
worktree_by_issue = self._build_worktree_mapping()
|
|
188
|
+
|
|
189
|
+
# Transform to PlanRowData
|
|
190
|
+
rows: list[PlanRowData] = []
|
|
191
|
+
use_graphite = self._ctx.global_config.use_graphite if self._ctx.global_config else False
|
|
192
|
+
|
|
193
|
+
for issue in plan_data.issues:
|
|
194
|
+
plan = _issue_to_plan(issue)
|
|
195
|
+
|
|
196
|
+
# Get workflow run for filtering
|
|
197
|
+
workflow_run = plan_data.workflow_runs.get(issue.number)
|
|
198
|
+
|
|
199
|
+
# Apply run_state filter
|
|
200
|
+
if filters.run_state is not None:
|
|
201
|
+
if workflow_run is None:
|
|
202
|
+
continue
|
|
203
|
+
if get_workflow_run_state(workflow_run) != filters.run_state:
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
# Build row data
|
|
207
|
+
row = self._build_row_data(
|
|
208
|
+
plan=plan,
|
|
209
|
+
issue_number=issue.number,
|
|
210
|
+
pr_linkages=plan_data.pr_linkages,
|
|
211
|
+
workflow_run=workflow_run,
|
|
212
|
+
worktree_by_issue=worktree_by_issue,
|
|
213
|
+
use_graphite=use_graphite,
|
|
214
|
+
)
|
|
215
|
+
rows.append(row)
|
|
216
|
+
|
|
217
|
+
return rows
|
|
218
|
+
|
|
219
|
+
def close_plan(self, issue_number: int, issue_url: str) -> list[int]:
|
|
220
|
+
"""Close a plan and its linked PRs.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
issue_number: The issue number to close
|
|
224
|
+
issue_url: The issue URL for PR linkage lookup
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
List of PR numbers that were also closed
|
|
228
|
+
"""
|
|
229
|
+
# Close linked PRs first
|
|
230
|
+
closed_prs = self._close_linked_prs(issue_number, issue_url)
|
|
231
|
+
|
|
232
|
+
# Close the plan (issue)
|
|
233
|
+
self._ctx.plan_store.close_plan(self._location.root, str(issue_number))
|
|
234
|
+
|
|
235
|
+
return closed_prs
|
|
236
|
+
|
|
237
|
+
def _close_linked_prs(self, issue_number: int, issue_url: str) -> list[int]:
|
|
238
|
+
"""Close all OPEN PRs linked to an issue.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
issue_number: The issue number
|
|
242
|
+
issue_url: The issue URL for location lookup
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
List of PR numbers that were closed
|
|
246
|
+
"""
|
|
247
|
+
location = github_repo_location_from_url(self._location.root, issue_url)
|
|
248
|
+
if location is None:
|
|
249
|
+
return []
|
|
250
|
+
pr_linkages = self._ctx.github.get_prs_linked_to_issues(location, [issue_number])
|
|
251
|
+
linked_prs = pr_linkages.get(issue_number, [])
|
|
252
|
+
|
|
253
|
+
closed_prs: list[int] = []
|
|
254
|
+
for pr in linked_prs:
|
|
255
|
+
if pr.state == "OPEN":
|
|
256
|
+
self._ctx.github.close_pr(self._location.root, pr.number)
|
|
257
|
+
closed_prs.append(pr.number)
|
|
258
|
+
|
|
259
|
+
return closed_prs
|
|
260
|
+
|
|
261
|
+
def submit_to_queue(self, issue_number: int, issue_url: str) -> None:
|
|
262
|
+
"""Submit a plan to the implementation queue.
|
|
263
|
+
|
|
264
|
+
Runs 'erk plan submit' as a subprocess to handle the complex workflow
|
|
265
|
+
of creating branches, PRs, and triggering GitHub Actions.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
issue_number: The issue number to submit
|
|
269
|
+
issue_url: The issue URL (unused, kept for interface consistency)
|
|
270
|
+
"""
|
|
271
|
+
# Run erk plan submit command from the repository root
|
|
272
|
+
subprocess.run(
|
|
273
|
+
["erk", "plan", "submit", str(issue_number)],
|
|
274
|
+
cwd=self._location.root,
|
|
275
|
+
check=True,
|
|
276
|
+
capture_output=True,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def fetch_branch_activity(self, rows: list[PlanRowData]) -> dict[int, BranchActivity]:
|
|
280
|
+
"""Fetch branch activity for plans that exist locally.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
rows: List of plan rows to fetch activity for
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Mapping of issue_number to BranchActivity for plans with local worktrees.
|
|
287
|
+
"""
|
|
288
|
+
result: dict[int, BranchActivity] = {}
|
|
289
|
+
|
|
290
|
+
# Get trunk branch
|
|
291
|
+
trunk = self._ctx.git.detect_trunk_branch(self._location.root)
|
|
292
|
+
|
|
293
|
+
for row in rows:
|
|
294
|
+
# Only fetch for plans with local branches
|
|
295
|
+
if not row.exists_locally or row.worktree_branch is None:
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
# Get commits on branch not in trunk
|
|
299
|
+
commits = self._ctx.git.get_branch_commits_with_authors(
|
|
300
|
+
self._location.root,
|
|
301
|
+
row.worktree_branch,
|
|
302
|
+
trunk,
|
|
303
|
+
limit=1, # Only need most recent
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if commits:
|
|
307
|
+
# Parse ISO timestamp from git
|
|
308
|
+
timestamp_str = commits[0]["timestamp"]
|
|
309
|
+
commit_at = datetime.fromisoformat(timestamp_str)
|
|
310
|
+
result[row.issue_number] = BranchActivity(
|
|
311
|
+
last_commit_at=commit_at,
|
|
312
|
+
last_commit_author=commits[0]["author"],
|
|
313
|
+
)
|
|
314
|
+
else:
|
|
315
|
+
result[row.issue_number] = BranchActivity.empty()
|
|
316
|
+
|
|
317
|
+
return result
|
|
318
|
+
|
|
319
|
+
def _build_worktree_mapping(self) -> dict[int, tuple[str, str | None]]:
|
|
320
|
+
"""Build mapping of issue number to (worktree name, branch).
|
|
321
|
+
|
|
322
|
+
Uses PXXXX prefix matching on worktree names to associate worktrees
|
|
323
|
+
with issues. Branch names follow pattern: P{issue_number}-{slug}-{timestamp}
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Mapping of issue number to tuple of (worktree_name, branch_name)
|
|
327
|
+
"""
|
|
328
|
+
_ensure_erk_metadata_dir_from_context(self._ctx.repo)
|
|
329
|
+
worktree_by_issue: dict[int, tuple[str, str | None]] = {}
|
|
330
|
+
worktrees = self._ctx.git.list_worktrees(self._location.root)
|
|
331
|
+
for worktree in worktrees:
|
|
332
|
+
issue_number = extract_leading_issue_number(worktree.path.name)
|
|
333
|
+
if issue_number is not None:
|
|
334
|
+
if issue_number not in worktree_by_issue:
|
|
335
|
+
worktree_by_issue[issue_number] = (
|
|
336
|
+
worktree.path.name,
|
|
337
|
+
worktree.branch,
|
|
338
|
+
)
|
|
339
|
+
return worktree_by_issue
|
|
340
|
+
|
|
341
|
+
def _build_row_data(
|
|
342
|
+
self,
|
|
343
|
+
*,
|
|
344
|
+
plan: Plan,
|
|
345
|
+
issue_number: int,
|
|
346
|
+
pr_linkages: dict[int, list[PullRequestInfo]],
|
|
347
|
+
workflow_run: WorkflowRun | None,
|
|
348
|
+
worktree_by_issue: dict[int, tuple[str, str | None]],
|
|
349
|
+
use_graphite: bool,
|
|
350
|
+
) -> PlanRowData:
|
|
351
|
+
"""Build a single PlanRowData from plan and related data."""
|
|
352
|
+
# Truncate title for display
|
|
353
|
+
title = plan.title
|
|
354
|
+
if len(title) > 50:
|
|
355
|
+
title = title[:47] + "..."
|
|
356
|
+
|
|
357
|
+
# Store full title
|
|
358
|
+
full_title = plan.title
|
|
359
|
+
|
|
360
|
+
# Worktree info
|
|
361
|
+
worktree_name = ""
|
|
362
|
+
worktree_branch: str | None = None
|
|
363
|
+
exists_locally = False
|
|
364
|
+
|
|
365
|
+
if issue_number in worktree_by_issue:
|
|
366
|
+
worktree_name, worktree_branch = worktree_by_issue[issue_number]
|
|
367
|
+
exists_locally = True
|
|
368
|
+
|
|
369
|
+
# Extract from issue body
|
|
370
|
+
local_impl_str: str | None = None
|
|
371
|
+
remote_impl_str: str | None = None
|
|
372
|
+
if plan.body:
|
|
373
|
+
extracted = extract_plan_header_worktree_name(plan.body)
|
|
374
|
+
if extracted and not worktree_name:
|
|
375
|
+
worktree_name = extracted
|
|
376
|
+
local_impl_str = extract_plan_header_local_impl_at(plan.body)
|
|
377
|
+
remote_impl_str = extract_plan_header_remote_impl_at(plan.body)
|
|
378
|
+
|
|
379
|
+
# Parse ISO 8601 timestamps for storage
|
|
380
|
+
last_local_impl_at: datetime | None = None
|
|
381
|
+
last_remote_impl_at: datetime | None = None
|
|
382
|
+
if local_impl_str:
|
|
383
|
+
last_local_impl_at = datetime.fromisoformat(local_impl_str.replace("Z", "+00:00"))
|
|
384
|
+
if remote_impl_str:
|
|
385
|
+
last_remote_impl_at = datetime.fromisoformat(remote_impl_str.replace("Z", "+00:00"))
|
|
386
|
+
|
|
387
|
+
# Format time displays
|
|
388
|
+
local_impl = format_relative_time(local_impl_str)
|
|
389
|
+
local_impl_display = local_impl if local_impl else "-"
|
|
390
|
+
remote_impl = format_relative_time(remote_impl_str)
|
|
391
|
+
remote_impl_display = remote_impl if remote_impl else "-"
|
|
392
|
+
|
|
393
|
+
# PR info
|
|
394
|
+
pr_number: int | None = None
|
|
395
|
+
pr_url: str | None = None
|
|
396
|
+
pr_title: str | None = None
|
|
397
|
+
pr_state: str | None = None
|
|
398
|
+
pr_display = "-"
|
|
399
|
+
checks_display = "-"
|
|
400
|
+
|
|
401
|
+
if issue_number in pr_linkages:
|
|
402
|
+
issue_prs = pr_linkages[issue_number]
|
|
403
|
+
selected_pr = select_display_pr(issue_prs)
|
|
404
|
+
if selected_pr is not None:
|
|
405
|
+
pr_number = selected_pr.number
|
|
406
|
+
pr_title = selected_pr.title
|
|
407
|
+
pr_state = selected_pr.state
|
|
408
|
+
graphite_url = self._ctx.graphite.get_graphite_url(
|
|
409
|
+
GitHubRepoId(selected_pr.owner, selected_pr.repo), selected_pr.number
|
|
410
|
+
)
|
|
411
|
+
pr_url = graphite_url if use_graphite and graphite_url else selected_pr.url
|
|
412
|
+
emoji = get_pr_status_emoji(selected_pr)
|
|
413
|
+
if selected_pr.will_close_target:
|
|
414
|
+
emoji += "🔗"
|
|
415
|
+
pr_display = f"#{selected_pr.number} {emoji}"
|
|
416
|
+
checks_display = format_checks_cell(selected_pr)
|
|
417
|
+
|
|
418
|
+
# Workflow run info
|
|
419
|
+
run_id: str | None = None
|
|
420
|
+
run_status: str | None = None
|
|
421
|
+
run_conclusion: str | None = None
|
|
422
|
+
run_id_display = "-"
|
|
423
|
+
run_state_display = "-"
|
|
424
|
+
run_url: str | None = None
|
|
425
|
+
|
|
426
|
+
if workflow_run is not None:
|
|
427
|
+
run_id = str(workflow_run.run_id)
|
|
428
|
+
run_status = workflow_run.status
|
|
429
|
+
run_conclusion = workflow_run.conclusion
|
|
430
|
+
if plan.url:
|
|
431
|
+
parts = plan.url.split("/")
|
|
432
|
+
if len(parts) >= 5:
|
|
433
|
+
owner = parts[-4]
|
|
434
|
+
repo_name = parts[-3]
|
|
435
|
+
run_url = (
|
|
436
|
+
f"https://github.com/{owner}/{repo_name}/actions/runs/{workflow_run.run_id}"
|
|
437
|
+
)
|
|
438
|
+
run_id_display = format_workflow_run_id(workflow_run, run_url)
|
|
439
|
+
run_state_display = format_workflow_outcome(workflow_run)
|
|
440
|
+
|
|
441
|
+
# Log entries (empty for now - will be fetched on demand in the modal)
|
|
442
|
+
log_entries: tuple[tuple[str, str, str], ...] = ()
|
|
443
|
+
|
|
444
|
+
return PlanRowData(
|
|
445
|
+
issue_number=issue_number,
|
|
446
|
+
issue_url=plan.url,
|
|
447
|
+
title=title,
|
|
448
|
+
pr_number=pr_number,
|
|
449
|
+
pr_url=pr_url,
|
|
450
|
+
pr_display=pr_display,
|
|
451
|
+
checks_display=checks_display,
|
|
452
|
+
worktree_name=worktree_name,
|
|
453
|
+
exists_locally=exists_locally,
|
|
454
|
+
local_impl_display=local_impl_display,
|
|
455
|
+
remote_impl_display=remote_impl_display,
|
|
456
|
+
run_id_display=run_id_display,
|
|
457
|
+
run_state_display=run_state_display,
|
|
458
|
+
run_url=run_url,
|
|
459
|
+
full_title=full_title,
|
|
460
|
+
pr_title=pr_title,
|
|
461
|
+
pr_state=pr_state,
|
|
462
|
+
worktree_branch=worktree_branch,
|
|
463
|
+
last_local_impl_at=last_local_impl_at,
|
|
464
|
+
last_remote_impl_at=last_remote_impl_at,
|
|
465
|
+
run_id=run_id,
|
|
466
|
+
run_status=run_status,
|
|
467
|
+
run_conclusion=run_conclusion,
|
|
468
|
+
log_entries=log_entries,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _issue_to_plan(issue: IssueInfo) -> Plan:
|
|
473
|
+
"""Convert IssueInfo to Plan format."""
|
|
474
|
+
state = PlanState.OPEN if issue.state == "OPEN" else PlanState.CLOSED
|
|
475
|
+
return Plan(
|
|
476
|
+
plan_identifier=str(issue.number),
|
|
477
|
+
title=issue.title,
|
|
478
|
+
body=issue.body,
|
|
479
|
+
state=state,
|
|
480
|
+
url=issue.url,
|
|
481
|
+
labels=issue.labels,
|
|
482
|
+
assignees=issue.assignees,
|
|
483
|
+
created_at=issue.created_at,
|
|
484
|
+
updated_at=issue.updated_at,
|
|
485
|
+
metadata={"number": issue.number},
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _ensure_erk_metadata_dir_from_context(repo: RepoContext | NoRepoSentinel) -> None:
|
|
490
|
+
"""Ensure erk metadata directory exists, handling sentinel case."""
|
|
491
|
+
if isinstance(repo, RepoContext):
|
|
492
|
+
ensure_erk_metadata_dir(repo)
|
erk/tui/data/types.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Data types for TUI components."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class PlanRowData:
|
|
11
|
+
"""Row data for displaying a plan in the TUI table.
|
|
12
|
+
|
|
13
|
+
Contains pre-formatted display strings and raw data needed for actions.
|
|
14
|
+
Immutable to ensure table state consistency.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
issue_number: GitHub issue number (e.g., 123)
|
|
18
|
+
issue_url: Full URL to the GitHub issue
|
|
19
|
+
title: Plan title (truncated for display)
|
|
20
|
+
pr_number: PR number if linked, None otherwise
|
|
21
|
+
pr_url: URL to PR (GitHub or Graphite), None if no PR
|
|
22
|
+
pr_display: Formatted PR cell content (e.g., "#123 👀")
|
|
23
|
+
checks_display: Formatted checks cell (e.g., "✓" or "✗")
|
|
24
|
+
worktree_name: Name of local worktree, empty string if none
|
|
25
|
+
exists_locally: Whether worktree exists on local machine
|
|
26
|
+
local_impl_display: Relative time since last local impl (e.g., "2h ago")
|
|
27
|
+
remote_impl_display: Relative time since last remote impl
|
|
28
|
+
run_id_display: Formatted workflow run ID
|
|
29
|
+
run_state_display: Formatted workflow run state
|
|
30
|
+
run_url: URL to the GitHub Actions run page
|
|
31
|
+
full_title: Complete untruncated plan title
|
|
32
|
+
pr_title: PR title if linked
|
|
33
|
+
pr_state: PR state (e.g., "OPEN", "MERGED", "CLOSED")
|
|
34
|
+
worktree_branch: Branch name in the worktree (if exists locally)
|
|
35
|
+
last_local_impl_at: Raw timestamp for local impl
|
|
36
|
+
last_remote_impl_at: Raw timestamp for remote impl
|
|
37
|
+
run_id: Raw workflow run ID (for display and URL construction)
|
|
38
|
+
run_status: Workflow run status (e.g., "completed", "in_progress")
|
|
39
|
+
run_conclusion: Workflow run conclusion (e.g., "success", "failure", "cancelled")
|
|
40
|
+
log_entries: List of (event_name, timestamp, comment_url) for plan log
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
issue_number: int
|
|
44
|
+
issue_url: str | None
|
|
45
|
+
title: str
|
|
46
|
+
pr_number: int | None
|
|
47
|
+
pr_url: str | None
|
|
48
|
+
pr_display: str
|
|
49
|
+
checks_display: str
|
|
50
|
+
worktree_name: str
|
|
51
|
+
exists_locally: bool
|
|
52
|
+
local_impl_display: str
|
|
53
|
+
remote_impl_display: str
|
|
54
|
+
run_id_display: str
|
|
55
|
+
run_state_display: str
|
|
56
|
+
run_url: str | None
|
|
57
|
+
full_title: str
|
|
58
|
+
pr_title: str | None
|
|
59
|
+
pr_state: str | None
|
|
60
|
+
worktree_branch: str | None
|
|
61
|
+
last_local_impl_at: datetime | None
|
|
62
|
+
last_remote_impl_at: datetime | None
|
|
63
|
+
run_id: str | None
|
|
64
|
+
run_status: str | None
|
|
65
|
+
run_conclusion: str | None
|
|
66
|
+
log_entries: tuple[tuple[str, str, str], ...]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class PlanFilters:
|
|
71
|
+
"""Filter options for plan list queries.
|
|
72
|
+
|
|
73
|
+
Matches options from the existing CLI command for consistency.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
labels: Labels to filter by (default: ["erk-plan"])
|
|
77
|
+
state: Filter by state ("open", "closed", or None for all)
|
|
78
|
+
run_state: Filter by workflow run state (e.g., "in_progress")
|
|
79
|
+
limit: Maximum number of results (None for no limit)
|
|
80
|
+
show_prs: Whether to include PR data
|
|
81
|
+
show_runs: Whether to include workflow run data
|
|
82
|
+
creator: Filter by creator username (None for all users)
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
labels: tuple[str, ...]
|
|
86
|
+
state: str | None
|
|
87
|
+
run_state: str | None
|
|
88
|
+
limit: int | None
|
|
89
|
+
show_prs: bool
|
|
90
|
+
show_runs: bool
|
|
91
|
+
creator: str | None = None
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def default() -> PlanFilters:
|
|
95
|
+
"""Create default filters (open erk-plan issues)."""
|
|
96
|
+
return PlanFilters(
|
|
97
|
+
labels=("erk-plan",),
|
|
98
|
+
state=None,
|
|
99
|
+
run_state=None,
|
|
100
|
+
limit=None,
|
|
101
|
+
show_prs=False,
|
|
102
|
+
show_runs=False,
|
|
103
|
+
creator=None,
|
|
104
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Filtering module for TUI dashboard."""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Pure filtering logic for TUI dashboard."""
|
|
2
|
+
|
|
3
|
+
from erk.tui.data.types import PlanRowData
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def filter_plans(plans: list[PlanRowData], query: str) -> list[PlanRowData]:
|
|
7
|
+
"""Filter plans by query matching title, issue number, or PR number.
|
|
8
|
+
|
|
9
|
+
Case-insensitive substring matching against:
|
|
10
|
+
- Plan title
|
|
11
|
+
- Issue number (as string)
|
|
12
|
+
- PR number (as string, if present)
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
plans: List of plans to filter
|
|
16
|
+
query: Search query string
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Filtered list of plans matching the query.
|
|
20
|
+
Returns all plans if query is empty.
|
|
21
|
+
"""
|
|
22
|
+
if not query:
|
|
23
|
+
return plans
|
|
24
|
+
|
|
25
|
+
query_lower = query.lower()
|
|
26
|
+
result: list[PlanRowData] = []
|
|
27
|
+
|
|
28
|
+
for plan in plans:
|
|
29
|
+
# Check title (case-insensitive)
|
|
30
|
+
if query_lower in plan.title.lower():
|
|
31
|
+
result.append(plan)
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
# Check issue number
|
|
35
|
+
if query_lower in str(plan.issue_number):
|
|
36
|
+
result.append(plan)
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
# Check PR number if present
|
|
40
|
+
if plan.pr_number is not None and query_lower in str(plan.pr_number):
|
|
41
|
+
result.append(plan)
|
|
42
|
+
|
|
43
|
+
return result
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Filter state types for TUI dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FilterMode(Enum):
|
|
10
|
+
"""Filter mode state."""
|
|
11
|
+
|
|
12
|
+
INACTIVE = auto()
|
|
13
|
+
ACTIVE = auto()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class FilterState:
|
|
18
|
+
"""State for filter mode with progressive escape behavior.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
mode: Current filter mode (INACTIVE or ACTIVE)
|
|
22
|
+
query: Current filter query text
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
mode: FilterMode
|
|
26
|
+
query: str = ""
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def initial() -> FilterState:
|
|
30
|
+
"""Create initial inactive state."""
|
|
31
|
+
return FilterState(mode=FilterMode.INACTIVE, query="")
|
|
32
|
+
|
|
33
|
+
def activate(self) -> FilterState:
|
|
34
|
+
"""Activate filter mode."""
|
|
35
|
+
return FilterState(mode=FilterMode.ACTIVE, query=self.query)
|
|
36
|
+
|
|
37
|
+
def with_query(self, query: str) -> FilterState:
|
|
38
|
+
"""Update query text."""
|
|
39
|
+
return FilterState(mode=self.mode, query=query)
|
|
40
|
+
|
|
41
|
+
def handle_escape(self) -> FilterState:
|
|
42
|
+
"""Handle escape key with progressive behavior.
|
|
43
|
+
|
|
44
|
+
Progressive escape:
|
|
45
|
+
- If text exists, clear it first (stay in active mode)
|
|
46
|
+
- If text is empty, deactivate filter mode
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
New state after escape handling
|
|
50
|
+
"""
|
|
51
|
+
if self.query:
|
|
52
|
+
# Clear text first, stay in active mode
|
|
53
|
+
return FilterState(mode=FilterMode.ACTIVE, query="")
|
|
54
|
+
# Text already empty, deactivate
|
|
55
|
+
return FilterState(mode=FilterMode.INACTIVE, query="")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""JSONL viewer TUI for Claude Code session files."""
|