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,322 @@
|
|
|
1
|
+
"""Simple text-based status renderer."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.status.models.status_data import StatusData
|
|
6
|
+
from erk_shared.output.output import user_output
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SimpleRenderer:
|
|
10
|
+
"""Renders status information as simple formatted text."""
|
|
11
|
+
|
|
12
|
+
def render(self, status: StatusData) -> None:
|
|
13
|
+
"""Render status data to console.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
status: Status data to render
|
|
17
|
+
"""
|
|
18
|
+
self._render_header(status)
|
|
19
|
+
self._render_plan(status)
|
|
20
|
+
self._render_stack(status)
|
|
21
|
+
self._render_pr_status(status)
|
|
22
|
+
self._render_git_status(status)
|
|
23
|
+
self._render_related_worktrees(status)
|
|
24
|
+
|
|
25
|
+
def _render_file_list(self, files: list[str], *, max_files: int = 3) -> None:
|
|
26
|
+
"""Render a list of files with truncation.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
files: List of file paths
|
|
30
|
+
max_files: Maximum number of files to display
|
|
31
|
+
"""
|
|
32
|
+
for file in files[:max_files]:
|
|
33
|
+
user_output(f" {file}")
|
|
34
|
+
|
|
35
|
+
if len(files) > max_files:
|
|
36
|
+
remaining = len(files) - max_files
|
|
37
|
+
user_output(
|
|
38
|
+
click.style(
|
|
39
|
+
f" ... and {remaining} more",
|
|
40
|
+
fg="white",
|
|
41
|
+
dim=True,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def _render_header(self, status: StatusData) -> None:
|
|
46
|
+
"""Render worktree header section.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
status: Status data
|
|
50
|
+
"""
|
|
51
|
+
wt = status.worktree_info
|
|
52
|
+
|
|
53
|
+
# Title
|
|
54
|
+
name_color = "green" if wt.is_root else "cyan"
|
|
55
|
+
user_output(click.style(f"Worktree: {wt.name}", fg=name_color, bold=True))
|
|
56
|
+
|
|
57
|
+
# Location
|
|
58
|
+
user_output(click.style(f"Location: {wt.path}", fg="white", dim=True))
|
|
59
|
+
|
|
60
|
+
# Branch
|
|
61
|
+
if wt.branch:
|
|
62
|
+
user_output(click.style(f"Branch: {wt.branch}", fg="yellow"))
|
|
63
|
+
else:
|
|
64
|
+
user_output(click.style("Branch: (detached HEAD)", fg="red", dim=True))
|
|
65
|
+
|
|
66
|
+
user_output()
|
|
67
|
+
|
|
68
|
+
def _truncate_plan_filename(self, filename: str) -> str:
|
|
69
|
+
"""Truncate enriched plan filename to max 22 characters, stripping suffixes.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
filename: Full filename (e.g., "very-long-plan-name-plan.md")
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Base name truncated to 22 chars, with "-plan.md" suffix removed
|
|
76
|
+
Format: "first-14-chars...last-5-chars" (exactly 22 chars when truncated)
|
|
77
|
+
"""
|
|
78
|
+
max_length = 22
|
|
79
|
+
|
|
80
|
+
# Strip "-plan.md" suffix if present (9 chars)
|
|
81
|
+
suffix = "-plan.md"
|
|
82
|
+
if filename.endswith(suffix):
|
|
83
|
+
base_name = filename[: -len(suffix)]
|
|
84
|
+
else:
|
|
85
|
+
base_name = filename
|
|
86
|
+
|
|
87
|
+
# If short enough, return as-is
|
|
88
|
+
if len(base_name) <= max_length:
|
|
89
|
+
return base_name
|
|
90
|
+
|
|
91
|
+
# Truncate with ellipsis: first 14 chars + "..." + last 5 chars = 22 chars
|
|
92
|
+
return f"{base_name[:14]}...{base_name[-5:]}"
|
|
93
|
+
|
|
94
|
+
def _render_plan(self, status: StatusData) -> None:
|
|
95
|
+
"""Render plan folder section if available.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
status: Status data
|
|
99
|
+
"""
|
|
100
|
+
if status.plan is None:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Check if we have either .impl/ folder or enriched plan
|
|
104
|
+
has_plan_folder = status.plan.exists
|
|
105
|
+
has_enriched_plan = status.plan.enriched_plan_filename is not None
|
|
106
|
+
|
|
107
|
+
if not has_plan_folder and not has_enriched_plan:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Build plan header
|
|
111
|
+
plan_header = "Plan:"
|
|
112
|
+
|
|
113
|
+
# Add enriched plan indicator if exists
|
|
114
|
+
if has_enriched_plan and status.plan.enriched_plan_filename is not None:
|
|
115
|
+
# Strip suffixes and truncate to max 22 chars for display
|
|
116
|
+
display_filename = self._truncate_plan_filename(status.plan.enriched_plan_filename)
|
|
117
|
+
plan_header += f" 🆕 {display_filename}"
|
|
118
|
+
|
|
119
|
+
user_output(click.style(plan_header, fg="bright_magenta", bold=True))
|
|
120
|
+
|
|
121
|
+
# Only show plan content details if .impl/ folder exists
|
|
122
|
+
if has_plan_folder:
|
|
123
|
+
if status.plan.first_lines:
|
|
124
|
+
for line in status.plan.first_lines:
|
|
125
|
+
user_output(f" {line}")
|
|
126
|
+
|
|
127
|
+
user_output(
|
|
128
|
+
click.style(
|
|
129
|
+
f" ({status.plan.line_count} lines in plan.md)",
|
|
130
|
+
fg="white",
|
|
131
|
+
dim=True,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Show GitHub issue link if available (make clickable)
|
|
136
|
+
if status.plan.issue_number is not None and status.plan.issue_url:
|
|
137
|
+
id_text = f"#{status.plan.issue_number}"
|
|
138
|
+
colored_id = click.style(id_text, fg="cyan")
|
|
139
|
+
# Make ID clickable using OSC 8
|
|
140
|
+
clickable_id = f"\033]8;;{status.plan.issue_url}\033\\{colored_id}\033]8;;\033\\"
|
|
141
|
+
user_output(f" Issue: {clickable_id}")
|
|
142
|
+
user_output(
|
|
143
|
+
click.style(
|
|
144
|
+
f" {status.plan.issue_url}",
|
|
145
|
+
fg="white",
|
|
146
|
+
dim=True,
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
user_output()
|
|
151
|
+
|
|
152
|
+
def _render_stack(self, status: StatusData) -> None:
|
|
153
|
+
"""Render worktree stack section if available.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
status: Status data
|
|
157
|
+
"""
|
|
158
|
+
if status.stack_position is None:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
stack = status.stack_position
|
|
162
|
+
|
|
163
|
+
user_output(click.style("Stack Position:", fg="blue", bold=True))
|
|
164
|
+
|
|
165
|
+
# Show position in stack
|
|
166
|
+
if stack.is_trunk:
|
|
167
|
+
user_output(" This is a trunk branch")
|
|
168
|
+
else:
|
|
169
|
+
if stack.parent_branch:
|
|
170
|
+
parent = click.style(stack.parent_branch, fg="yellow")
|
|
171
|
+
user_output(f" Parent: {parent}")
|
|
172
|
+
|
|
173
|
+
if stack.children_branches:
|
|
174
|
+
children = ", ".join(click.style(c, fg="yellow") for c in stack.children_branches)
|
|
175
|
+
user_output(f" Children: {children}")
|
|
176
|
+
|
|
177
|
+
# Show stack visualization
|
|
178
|
+
if len(stack.stack) > 1:
|
|
179
|
+
user_output()
|
|
180
|
+
user_output(click.style(" Stack:", fg="white", dim=True))
|
|
181
|
+
for branch in reversed(stack.stack):
|
|
182
|
+
is_current = branch == stack.current_branch
|
|
183
|
+
|
|
184
|
+
if is_current:
|
|
185
|
+
marker = click.style("◉", fg="bright_green")
|
|
186
|
+
branch_text = click.style(branch, fg="bright_green", bold=True)
|
|
187
|
+
else:
|
|
188
|
+
marker = click.style("◯", fg="bright_black")
|
|
189
|
+
branch_text = branch
|
|
190
|
+
|
|
191
|
+
user_output(f" {marker} {branch_text}")
|
|
192
|
+
|
|
193
|
+
user_output()
|
|
194
|
+
|
|
195
|
+
def _render_pr_status(self, status: StatusData) -> None:
|
|
196
|
+
"""Render PR status section if available.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
status: Status data
|
|
200
|
+
"""
|
|
201
|
+
if status.pr_status is None:
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
pr = status.pr_status
|
|
205
|
+
|
|
206
|
+
user_output(click.style("Pull Request:", fg="blue", bold=True))
|
|
207
|
+
|
|
208
|
+
# PR number (clickable) and state
|
|
209
|
+
# Make PR number clickable using OSC 8
|
|
210
|
+
pr_number_text = f"#{pr.number}"
|
|
211
|
+
colored_pr_number = click.style(pr_number_text, fg="cyan")
|
|
212
|
+
clickable_pr = f"\033]8;;{pr.url}\033\\{colored_pr_number}\033]8;;\033\\"
|
|
213
|
+
|
|
214
|
+
state_color = (
|
|
215
|
+
"green" if pr.state == "OPEN" else "red" if pr.state == "CLOSED" else "magenta"
|
|
216
|
+
)
|
|
217
|
+
state_text = click.style(pr.state, fg=state_color)
|
|
218
|
+
user_output(f" {clickable_pr} {state_text}")
|
|
219
|
+
|
|
220
|
+
# Draft status
|
|
221
|
+
if pr.is_draft:
|
|
222
|
+
user_output(click.style(" Draft PR", fg="yellow"))
|
|
223
|
+
|
|
224
|
+
# Checks status
|
|
225
|
+
if pr.checks_passing is not None:
|
|
226
|
+
if pr.checks_passing:
|
|
227
|
+
user_output(click.style(" Checks: passing", fg="green"))
|
|
228
|
+
else:
|
|
229
|
+
user_output(click.style(" Checks: failing", fg="red"))
|
|
230
|
+
|
|
231
|
+
# Ready to merge
|
|
232
|
+
if pr.ready_to_merge:
|
|
233
|
+
user_output(click.style(" ✓ Ready to merge", fg="green", bold=True))
|
|
234
|
+
|
|
235
|
+
user_output()
|
|
236
|
+
|
|
237
|
+
def _render_git_status(self, status: StatusData) -> None:
|
|
238
|
+
"""Render git status section.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
status: Status data
|
|
242
|
+
"""
|
|
243
|
+
if status.git_status is None:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
git = status.git_status
|
|
247
|
+
|
|
248
|
+
user_output(click.style("Git Status:", fg="blue", bold=True))
|
|
249
|
+
|
|
250
|
+
# Clean/dirty status
|
|
251
|
+
if git.clean:
|
|
252
|
+
user_output(click.style(" Working tree clean", fg="green"))
|
|
253
|
+
else:
|
|
254
|
+
user_output(click.style(" Working tree has changes:", fg="yellow"))
|
|
255
|
+
|
|
256
|
+
if git.staged_files:
|
|
257
|
+
user_output(click.style(" Staged:", fg="green"))
|
|
258
|
+
self._render_file_list(git.staged_files, max_files=3)
|
|
259
|
+
|
|
260
|
+
if git.modified_files:
|
|
261
|
+
user_output(click.style(" Modified:", fg="yellow"))
|
|
262
|
+
self._render_file_list(git.modified_files, max_files=3)
|
|
263
|
+
|
|
264
|
+
if git.untracked_files:
|
|
265
|
+
user_output(click.style(" Untracked:", fg="red"))
|
|
266
|
+
self._render_file_list(git.untracked_files, max_files=3)
|
|
267
|
+
|
|
268
|
+
# Ahead/behind
|
|
269
|
+
if git.ahead > 0 or git.behind > 0:
|
|
270
|
+
parts = []
|
|
271
|
+
if git.ahead > 0:
|
|
272
|
+
parts.append(click.style(f"{git.ahead} ahead", fg="green"))
|
|
273
|
+
if git.behind > 0:
|
|
274
|
+
parts.append(click.style(f"{git.behind} behind", fg="red"))
|
|
275
|
+
|
|
276
|
+
user_output(f" Branch: {', '.join(parts)}")
|
|
277
|
+
|
|
278
|
+
# Recent commits
|
|
279
|
+
if git.recent_commits:
|
|
280
|
+
user_output()
|
|
281
|
+
user_output(click.style(" Recent commits:", fg="white", dim=True))
|
|
282
|
+
for commit in git.recent_commits[:3]:
|
|
283
|
+
sha = click.style(commit.sha, fg="yellow")
|
|
284
|
+
message = commit.message[:60]
|
|
285
|
+
if len(commit.message) > 60:
|
|
286
|
+
message += "..."
|
|
287
|
+
user_output(f" {sha} {message}")
|
|
288
|
+
|
|
289
|
+
user_output()
|
|
290
|
+
|
|
291
|
+
def _render_related_worktrees(self, status: StatusData) -> None:
|
|
292
|
+
"""Render related worktrees section.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
status: Status data
|
|
296
|
+
"""
|
|
297
|
+
if not status.related_worktrees:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
user_output(click.style("Related Worktrees:", fg="blue", bold=True))
|
|
301
|
+
|
|
302
|
+
for wt in status.related_worktrees[:5]:
|
|
303
|
+
name_color = "green" if wt.is_root else "cyan"
|
|
304
|
+
name_part = click.style(wt.name, fg=name_color)
|
|
305
|
+
|
|
306
|
+
if wt.branch:
|
|
307
|
+
branch_part = click.style(f"[{wt.branch}]", fg="yellow", dim=True)
|
|
308
|
+
user_output(f" {name_part} {branch_part}")
|
|
309
|
+
else:
|
|
310
|
+
user_output(f" {name_part}")
|
|
311
|
+
|
|
312
|
+
if len(status.related_worktrees) > 5:
|
|
313
|
+
remaining = len(status.related_worktrees) - 5
|
|
314
|
+
user_output(
|
|
315
|
+
click.style(
|
|
316
|
+
f" ... and {remaining} more",
|
|
317
|
+
fg="white",
|
|
318
|
+
dim=True,
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
user_output()
|
erk/tui/AGENTS.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Textual Dash - Interactive TUI for erk
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Textual Dash provides an interactive terminal UI for the `erk dash` command, enabling keyboard-driven navigation through plan lists with quick actions. It's an alternative to the static table output and watch mode, optimized for users managing many plans.
|
|
6
|
+
|
|
7
|
+
**Invocation**: `erk dash -i` or `erk dash --interactive`
|
|
8
|
+
|
|
9
|
+
## Architecture Overview
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/erk/tui/
|
|
13
|
+
├── AGENTS.md # This file
|
|
14
|
+
├── TEXTUAL_QUIRKS.md # API quirks and workarounds (READ THIS)
|
|
15
|
+
├── __init__.py
|
|
16
|
+
├── app.py # ErkDashApp - main Textual application
|
|
17
|
+
├── data/
|
|
18
|
+
│ ├── __init__.py
|
|
19
|
+
│ ├── provider.py # PlanDataProvider ABC + RealPlanDataProvider
|
|
20
|
+
│ └── types.py # PlanRowData, PlanFilters dataclasses
|
|
21
|
+
├── widgets/
|
|
22
|
+
│ ├── __init__.py
|
|
23
|
+
│ ├── plan_table.py # PlanDataTable - DataTable subclass
|
|
24
|
+
│ └── status_bar.py # StatusBar - footer with stats and hints
|
|
25
|
+
└── styles/
|
|
26
|
+
└── dash.tcss # Textual CSS styles
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Key Components
|
|
30
|
+
|
|
31
|
+
### ErkDashApp (`app.py`)
|
|
32
|
+
|
|
33
|
+
The main Textual `App` subclass. Responsibilities:
|
|
34
|
+
|
|
35
|
+
- Compose layout (Header, PlanDataTable, StatusBar)
|
|
36
|
+
- Handle keyboard bindings (q, r, Enter, o, p, i, ?, j/k)
|
|
37
|
+
- Manage auto-refresh timer and countdown
|
|
38
|
+
- Coordinate data loading via workers
|
|
39
|
+
|
|
40
|
+
**Key bindings**:
|
|
41
|
+
| Key | Action |
|
|
42
|
+
|-----|--------|
|
|
43
|
+
| `q` / `Esc` | Quit |
|
|
44
|
+
| `r` | Refresh data (resets countdown) |
|
|
45
|
+
| `Enter` / `o` | Open issue in browser |
|
|
46
|
+
| `p` | Open PR in browser |
|
|
47
|
+
| `i` | Show implement command |
|
|
48
|
+
| `?` | Show help overlay |
|
|
49
|
+
| `j` / `k` | Vim-style navigation |
|
|
50
|
+
|
|
51
|
+
### PlanDataProvider (`data/provider.py`)
|
|
52
|
+
|
|
53
|
+
ABC defining data fetching interface. Enables testing with fakes.
|
|
54
|
+
|
|
55
|
+
- `PlanDataProvider` - Abstract base class
|
|
56
|
+
- `RealPlanDataProvider` - Production implementation wrapping `PlanListService`
|
|
57
|
+
|
|
58
|
+
The provider transforms `PlanListData` from the service layer into `PlanRowData` tuples optimized for table display.
|
|
59
|
+
|
|
60
|
+
### PlanRowData (`data/types.py`)
|
|
61
|
+
|
|
62
|
+
Immutable dataclass containing:
|
|
63
|
+
|
|
64
|
+
- Raw data for actions (issue_number, issue_url, pr_number, pr_url)
|
|
65
|
+
- Pre-formatted display strings (title, pr_display, checks_display, etc.)
|
|
66
|
+
|
|
67
|
+
### PlanDataTable (`widgets/plan_table.py`)
|
|
68
|
+
|
|
69
|
+
DataTable subclass with:
|
|
70
|
+
|
|
71
|
+
- Row selection mode (not cell selection)
|
|
72
|
+
- Column setup based on filter flags
|
|
73
|
+
- Cursor position preservation on refresh
|
|
74
|
+
- Left/right arrow disabled (row mode only)
|
|
75
|
+
|
|
76
|
+
### StatusBar (`widgets/status_bar.py`)
|
|
77
|
+
|
|
78
|
+
Footer widget showing:
|
|
79
|
+
|
|
80
|
+
- Plan count
|
|
81
|
+
- Last update time with fetch duration
|
|
82
|
+
- Countdown to next refresh
|
|
83
|
+
- Key binding hints
|
|
84
|
+
|
|
85
|
+
## Data Flow
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
1. erk dash -i
|
|
89
|
+
└── _run_interactive_mode()
|
|
90
|
+
└── Creates RealPlanDataProvider with ErkContext
|
|
91
|
+
└── Creates ErkDashApp(provider, filters, interval)
|
|
92
|
+
└── app.run()
|
|
93
|
+
|
|
94
|
+
2. On mount:
|
|
95
|
+
└── run_worker(_load_data())
|
|
96
|
+
└── provider.fetch_plans(filters) # In executor thread
|
|
97
|
+
└── _update_table(rows, time, duration)
|
|
98
|
+
└── table.populate(rows)
|
|
99
|
+
└── status_bar.set_plan_count()
|
|
100
|
+
└── status_bar.set_last_update()
|
|
101
|
+
|
|
102
|
+
3. Auto-refresh (every N seconds):
|
|
103
|
+
└── _tick_countdown() decrements counter
|
|
104
|
+
└── When 0: action_refresh()
|
|
105
|
+
└── Resets countdown
|
|
106
|
+
└── run_worker(_load_data())
|
|
107
|
+
|
|
108
|
+
4. User actions:
|
|
109
|
+
└── Enter/o → action_open_issue() → click.launch(url)
|
|
110
|
+
└── p → action_open_pr() → click.launch(url)
|
|
111
|
+
└── r → action_refresh() → reload data
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Testing Strategy
|
|
115
|
+
|
|
116
|
+
Tests live in `tests/tui/`:
|
|
117
|
+
|
|
118
|
+
- `test_plan_table.py` - Unit tests for table widget and row conversion
|
|
119
|
+
- `test_app.py` - Textual pilot-based integration tests
|
|
120
|
+
|
|
121
|
+
**Fake infrastructure** in `tests/fakes/plan_data_provider.py`:
|
|
122
|
+
|
|
123
|
+
- `FakePlanDataProvider` - Returns canned data, tracks fetch count
|
|
124
|
+
- `make_plan_row()` - Helper to create test PlanRowData
|
|
125
|
+
|
|
126
|
+
**Testing pattern**:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
@pytest.mark.asyncio
|
|
130
|
+
async def test_something(self) -> None:
|
|
131
|
+
provider = FakePlanDataProvider([make_plan_row(123, "Test")])
|
|
132
|
+
app = ErkDashApp(provider, PlanFilters.default(), refresh_interval=0)
|
|
133
|
+
|
|
134
|
+
async with app.run_test() as pilot:
|
|
135
|
+
await pilot.pause() # Wait for async load
|
|
136
|
+
# assertions...
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Important: Read TEXTUAL_QUIRKS.md
|
|
140
|
+
|
|
141
|
+
Before modifying this code, read `TEXTUAL_QUIRKS.md` which documents:
|
|
142
|
+
|
|
143
|
+
- DataTable `cursor_type` initialization pattern
|
|
144
|
+
- Naming conflicts to avoid (`_filters`)
|
|
145
|
+
- Cursor position preservation on `clear()`
|
|
146
|
+
- Enter key handling via `RowSelected` event
|
|
147
|
+
- `action_quit` override issues
|
|
148
|
+
- Footer vs custom StatusBar conflicts
|
|
149
|
+
- Async data loading patterns
|
|
150
|
+
- pytest-asyncio configuration
|
|
151
|
+
|
|
152
|
+
## Design Decisions
|
|
153
|
+
|
|
154
|
+
### Why Row Selection Mode?
|
|
155
|
+
|
|
156
|
+
Cell selection adds complexity (left/right navigation) without benefit for this use case. Users care about selecting a plan, not a specific column.
|
|
157
|
+
|
|
158
|
+
### Why Separate PlanDataProvider?
|
|
159
|
+
|
|
160
|
+
1. **Testability**: FakePlanDataProvider enables fast tests without API calls
|
|
161
|
+
2. **Separation of concerns**: TUI code doesn't know about PlanListService internals
|
|
162
|
+
3. **Future flexibility**: Could add caching, filtering, or alternative data sources
|
|
163
|
+
|
|
164
|
+
### Why No Footer Widget?
|
|
165
|
+
|
|
166
|
+
Textual's built-in `Footer` shows BINDINGS but conflicts with custom status bars. Our `StatusBar` provides richer information (countdown, timing, messages) in a single line.
|
|
167
|
+
|
|
168
|
+
### Why `-i` Implies `-a`?
|
|
169
|
+
|
|
170
|
+
Interactive mode benefits from seeing all columns. Users navigating with keyboard want full context without remembering to add flags.
|
|
171
|
+
|
|
172
|
+
## Adding Features
|
|
173
|
+
|
|
174
|
+
### New Key Binding
|
|
175
|
+
|
|
176
|
+
1. Add to `BINDINGS` list in `ErkDashApp`
|
|
177
|
+
2. Implement `action_*` method
|
|
178
|
+
3. Update status bar hints in `StatusBar._update_display()`
|
|
179
|
+
4. Update help screen in `HelpScreen.compose()`
|
|
180
|
+
|
|
181
|
+
### New Column
|
|
182
|
+
|
|
183
|
+
1. Add field to `PlanRowData` in `types.py`
|
|
184
|
+
2. Update `RealPlanDataProvider._build_row_data()` to populate it
|
|
185
|
+
3. Add column in `PlanDataTable._setup_columns()` (check filter flags)
|
|
186
|
+
4. Add value in `PlanDataTable._row_to_values()`
|
|
187
|
+
5. Update `make_plan_row()` helper in test fakes
|
|
188
|
+
|
|
189
|
+
### New Status Bar Info
|
|
190
|
+
|
|
191
|
+
1. Add field and setter to `StatusBar`
|
|
192
|
+
2. Update `_update_display()` to include in output
|
|
193
|
+
3. Call setter from `ErkDashApp` at appropriate time
|
erk/tui/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
erk/tui/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Interactive TUI components for erk."""
|