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,112 @@
|
|
|
1
|
+
"""Widget for displaying live subprocess output in the TUI."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Vertical
|
|
5
|
+
from textual.widgets import RichLog, Static
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CommandOutputPanel(Static):
|
|
9
|
+
"""Bottom panel showing live subprocess output.
|
|
10
|
+
|
|
11
|
+
Displays streaming output from a subprocess with status indicator
|
|
12
|
+
and dismiss hint after completion.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
DEFAULT_CSS = """
|
|
16
|
+
CommandOutputPanel {
|
|
17
|
+
height: auto;
|
|
18
|
+
max-height: 15;
|
|
19
|
+
background: $surface-darken-1;
|
|
20
|
+
border-top: solid $primary;
|
|
21
|
+
padding: 0 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
CommandOutputPanel #output-header {
|
|
25
|
+
height: 1;
|
|
26
|
+
color: $primary;
|
|
27
|
+
text-style: bold;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
CommandOutputPanel #output-log {
|
|
31
|
+
height: auto;
|
|
32
|
+
max-height: 10;
|
|
33
|
+
scrollbar-gutter: stable;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
CommandOutputPanel #output-status {
|
|
37
|
+
height: 1;
|
|
38
|
+
color: $text-muted;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
CommandOutputPanel #output-status.success {
|
|
42
|
+
color: #238636;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
CommandOutputPanel #output-status.failure {
|
|
46
|
+
color: #da3633;
|
|
47
|
+
}
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, title: str) -> None:
|
|
51
|
+
"""Initialize output panel.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
title: Title to show at the top of the panel
|
|
55
|
+
"""
|
|
56
|
+
super().__init__()
|
|
57
|
+
self._title = title
|
|
58
|
+
self._completed = False
|
|
59
|
+
self._success = False
|
|
60
|
+
self._lines: list[str] = []
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_completed(self) -> bool:
|
|
64
|
+
"""Check if the command has completed."""
|
|
65
|
+
return self._completed
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def succeeded(self) -> bool:
|
|
69
|
+
"""Check if the command succeeded."""
|
|
70
|
+
return self._success
|
|
71
|
+
|
|
72
|
+
def compose(self) -> ComposeResult:
|
|
73
|
+
"""Create panel content."""
|
|
74
|
+
with Vertical():
|
|
75
|
+
yield Static(f"[bold]{self._title}[/bold]", id="output-header")
|
|
76
|
+
yield RichLog(id="output-log", highlight=True, markup=True)
|
|
77
|
+
yield Static("Running...", id="output-status")
|
|
78
|
+
|
|
79
|
+
def append_line(self, line: str, is_stderr: bool = False) -> None:
|
|
80
|
+
"""Add output line to the log.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
line: The line of output to append
|
|
84
|
+
is_stderr: If True, style as error output (red)
|
|
85
|
+
"""
|
|
86
|
+
self._lines.append(line)
|
|
87
|
+
log = self.query_one("#output-log", RichLog)
|
|
88
|
+
if is_stderr:
|
|
89
|
+
log.write(f"[red]{line}[/red]")
|
|
90
|
+
else:
|
|
91
|
+
log.write(line)
|
|
92
|
+
|
|
93
|
+
def get_output_text(self) -> str:
|
|
94
|
+
"""Return all output lines joined with newlines."""
|
|
95
|
+
return "\n".join(self._lines)
|
|
96
|
+
|
|
97
|
+
def set_completed(self, success: bool) -> None:
|
|
98
|
+
"""Mark command as complete and show dismiss hint.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
success: Whether the command succeeded
|
|
102
|
+
"""
|
|
103
|
+
self._completed = True
|
|
104
|
+
self._success = success
|
|
105
|
+
|
|
106
|
+
status = self.query_one("#output-status", Static)
|
|
107
|
+
if success:
|
|
108
|
+
status.update("✓ Complete - Press Esc to close, y to copy logs")
|
|
109
|
+
status.add_class("success")
|
|
110
|
+
else:
|
|
111
|
+
status.update("✗ Failed - Press Esc to close, y to copy logs")
|
|
112
|
+
status.add_class("failure")
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Plan table widget for TUI dashboard."""
|
|
2
|
+
|
|
3
|
+
from rich.text import Text
|
|
4
|
+
from textual.events import Click
|
|
5
|
+
from textual.message import Message
|
|
6
|
+
from textual.widgets import DataTable
|
|
7
|
+
|
|
8
|
+
from erk.tui.data.types import PlanFilters, PlanRowData
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlanDataTable(DataTable):
|
|
12
|
+
"""DataTable subclass for displaying plans.
|
|
13
|
+
|
|
14
|
+
Manages column configuration and row population from PlanRowData.
|
|
15
|
+
Uses row selection mode (not cell selection) for simpler navigation.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
class LocalWtClicked(Message):
|
|
19
|
+
"""Posted when user clicks local-wt column on a row with existing worktree."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, row_index: int) -> None:
|
|
22
|
+
"""Initialize the message.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
row_index: Index of the clicked row
|
|
26
|
+
"""
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.row_index = row_index
|
|
29
|
+
|
|
30
|
+
class RunIdClicked(Message):
|
|
31
|
+
"""Posted when user clicks run-id column on a row with a run URL."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, row_index: int) -> None:
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.row_index = row_index
|
|
36
|
+
|
|
37
|
+
class PrClicked(Message):
|
|
38
|
+
"""Posted when user clicks pr column on a row with a PR URL."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, row_index: int) -> None:
|
|
41
|
+
super().__init__()
|
|
42
|
+
self.row_index = row_index
|
|
43
|
+
|
|
44
|
+
class PlanClicked(Message):
|
|
45
|
+
"""Posted when user clicks plan column on a row with an issue URL."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, row_index: int) -> None:
|
|
48
|
+
super().__init__()
|
|
49
|
+
self.row_index = row_index
|
|
50
|
+
|
|
51
|
+
def __init__(self, plan_filters: PlanFilters) -> None:
|
|
52
|
+
"""Initialize table with column configuration based on filters.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
plan_filters: Filter options that determine which columns to show
|
|
56
|
+
"""
|
|
57
|
+
super().__init__(cursor_type="row")
|
|
58
|
+
self._plan_filters = plan_filters
|
|
59
|
+
self._rows: list[PlanRowData] = []
|
|
60
|
+
self._plan_column_index: int = 0 # Always first column
|
|
61
|
+
self._pr_column_index: int | None = None
|
|
62
|
+
self._local_wt_column_index: int | None = None
|
|
63
|
+
self._run_id_column_index: int | None = None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def local_wt_column_index(self) -> int | None:
|
|
67
|
+
"""Get the column index for the local-wt column.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Column index (0-based), or None if columns not yet set up.
|
|
71
|
+
The index varies based on show_prs flag:
|
|
72
|
+
- Without PRs: index 2 (plan, title, local-wt)
|
|
73
|
+
- With PRs: index 4 (plan, title, pr, chks, local-wt)
|
|
74
|
+
"""
|
|
75
|
+
return self._local_wt_column_index
|
|
76
|
+
|
|
77
|
+
def action_cursor_left(self) -> None:
|
|
78
|
+
"""Disable left arrow navigation (row mode only)."""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
def action_cursor_right(self) -> None:
|
|
82
|
+
"""Disable right arrow navigation (row mode only)."""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def on_mount(self) -> None:
|
|
86
|
+
"""Configure columns when widget is mounted."""
|
|
87
|
+
self._setup_columns()
|
|
88
|
+
|
|
89
|
+
def _setup_columns(self) -> None:
|
|
90
|
+
"""Add columns based on current filter settings.
|
|
91
|
+
|
|
92
|
+
Tracks the column index for local-wt to enable click detection.
|
|
93
|
+
"""
|
|
94
|
+
col_index = 0
|
|
95
|
+
self.add_column("plan", key="plan")
|
|
96
|
+
col_index += 1
|
|
97
|
+
self.add_column("title", key="title")
|
|
98
|
+
col_index += 1
|
|
99
|
+
if self._plan_filters.show_prs:
|
|
100
|
+
self.add_column("pr", key="pr")
|
|
101
|
+
self._pr_column_index = col_index
|
|
102
|
+
col_index += 1
|
|
103
|
+
self.add_column("chks", key="chks")
|
|
104
|
+
col_index += 1
|
|
105
|
+
self._local_wt_column_index = col_index
|
|
106
|
+
self.add_column("local-wt", key="local_wt")
|
|
107
|
+
col_index += 1
|
|
108
|
+
self.add_column("local-impl", key="local_impl")
|
|
109
|
+
col_index += 1
|
|
110
|
+
if self._plan_filters.show_runs:
|
|
111
|
+
self.add_column("remote-impl", key="remote_impl")
|
|
112
|
+
col_index += 1
|
|
113
|
+
self.add_column("run-id", key="run_id")
|
|
114
|
+
self._run_id_column_index = col_index
|
|
115
|
+
col_index += 1
|
|
116
|
+
self.add_column("run-state", key="run_state")
|
|
117
|
+
|
|
118
|
+
def populate(self, rows: list[PlanRowData]) -> None:
|
|
119
|
+
"""Populate table with plan data, preserving cursor position.
|
|
120
|
+
|
|
121
|
+
If the selected plan still exists, cursor stays on it.
|
|
122
|
+
If the selected plan disappeared, cursor stays at the same row index.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
rows: List of PlanRowData to display
|
|
126
|
+
"""
|
|
127
|
+
# Save current selection by issue number (row key)
|
|
128
|
+
selected_key: str | None = None
|
|
129
|
+
if self._rows and self.cursor_row is not None and 0 <= self.cursor_row < len(self._rows):
|
|
130
|
+
selected_key = str(self._rows[self.cursor_row].issue_number)
|
|
131
|
+
|
|
132
|
+
# Save cursor row index for fallback (move up if plan disappears)
|
|
133
|
+
saved_cursor_row = self.cursor_row
|
|
134
|
+
|
|
135
|
+
self._rows = rows
|
|
136
|
+
self.clear()
|
|
137
|
+
|
|
138
|
+
for row in rows:
|
|
139
|
+
values = self._row_to_values(row)
|
|
140
|
+
self.add_row(*values, key=str(row.issue_number))
|
|
141
|
+
|
|
142
|
+
# Restore cursor position
|
|
143
|
+
if rows:
|
|
144
|
+
# Try to restore by key (issue number) first
|
|
145
|
+
if selected_key is not None:
|
|
146
|
+
for idx, row in enumerate(rows):
|
|
147
|
+
if str(row.issue_number) == selected_key:
|
|
148
|
+
self.move_cursor(row=idx)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
# Plan disappeared - stay at same row index, clamped to valid range
|
|
152
|
+
if saved_cursor_row is not None and saved_cursor_row >= 0:
|
|
153
|
+
target_row = min(saved_cursor_row, len(rows) - 1)
|
|
154
|
+
self.move_cursor(row=target_row)
|
|
155
|
+
|
|
156
|
+
def _row_to_values(self, row: PlanRowData) -> tuple[str | Text, ...]:
|
|
157
|
+
"""Convert PlanRowData to table cell values.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
row: Plan row data
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Tuple of cell values matching column order
|
|
164
|
+
"""
|
|
165
|
+
# Format issue number - colorize if clickable
|
|
166
|
+
plan_cell: str | Text = f"#{row.issue_number}"
|
|
167
|
+
if row.issue_url:
|
|
168
|
+
plan_cell = Text(plan_cell, style="cyan underline")
|
|
169
|
+
|
|
170
|
+
# Format worktree
|
|
171
|
+
if row.exists_locally:
|
|
172
|
+
wt_cell = row.worktree_name
|
|
173
|
+
else:
|
|
174
|
+
wt_cell = "-"
|
|
175
|
+
|
|
176
|
+
# Build values list based on columns
|
|
177
|
+
values: list[str | Text] = [plan_cell, row.title]
|
|
178
|
+
if self._plan_filters.show_prs:
|
|
179
|
+
# Strip Rich markup and colorize if clickable
|
|
180
|
+
pr_display = _strip_rich_markup(row.pr_display)
|
|
181
|
+
if row.pr_url:
|
|
182
|
+
pr_display = Text(pr_display, style="cyan underline")
|
|
183
|
+
checks_display = _strip_rich_markup(row.checks_display)
|
|
184
|
+
values.extend([pr_display, checks_display])
|
|
185
|
+
values.extend([wt_cell, row.local_impl_display])
|
|
186
|
+
if self._plan_filters.show_runs:
|
|
187
|
+
remote_impl = _strip_rich_markup(row.remote_impl_display)
|
|
188
|
+
run_id = _strip_rich_markup(row.run_id_display)
|
|
189
|
+
if row.run_url:
|
|
190
|
+
run_id = Text(run_id, style="cyan underline")
|
|
191
|
+
run_state = _strip_rich_markup(row.run_state_display)
|
|
192
|
+
values.extend([remote_impl, run_id, run_state])
|
|
193
|
+
|
|
194
|
+
return tuple(values)
|
|
195
|
+
|
|
196
|
+
def get_selected_row_data(self) -> PlanRowData | None:
|
|
197
|
+
"""Get the PlanRowData for the currently selected row.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
PlanRowData for selected row, or None if no selection
|
|
201
|
+
"""
|
|
202
|
+
cursor_row = self.cursor_row
|
|
203
|
+
if cursor_row is None or cursor_row < 0 or cursor_row >= len(self._rows):
|
|
204
|
+
return None
|
|
205
|
+
return self._rows[cursor_row]
|
|
206
|
+
|
|
207
|
+
def on_click(self, event: Click) -> None:
|
|
208
|
+
"""Detect clicks on specific columns and post appropriate messages.
|
|
209
|
+
|
|
210
|
+
Posts LocalWtClicked event if:
|
|
211
|
+
- Click is on the local-wt column
|
|
212
|
+
- The row has an existing local worktree (not '-')
|
|
213
|
+
|
|
214
|
+
Posts RunIdClicked event if:
|
|
215
|
+
- Click is on the run-id column
|
|
216
|
+
- The row has a run URL
|
|
217
|
+
|
|
218
|
+
Stops event propagation to prevent default row selection behavior when
|
|
219
|
+
a column-specific click is detected.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
event: Click event from Textual
|
|
223
|
+
"""
|
|
224
|
+
coord = self.hover_coordinate
|
|
225
|
+
if coord is None:
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
row_index = coord.row
|
|
229
|
+
col_index = coord.column
|
|
230
|
+
|
|
231
|
+
# Check plan column (issue number)
|
|
232
|
+
if col_index == self._plan_column_index:
|
|
233
|
+
if row_index < len(self._rows) and self._rows[row_index].issue_url:
|
|
234
|
+
self.post_message(self.PlanClicked(row_index))
|
|
235
|
+
event.prevent_default()
|
|
236
|
+
event.stop()
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
# Check PR column
|
|
240
|
+
if self._pr_column_index is not None and col_index == self._pr_column_index:
|
|
241
|
+
if row_index < len(self._rows) and self._rows[row_index].pr_url:
|
|
242
|
+
self.post_message(self.PrClicked(row_index))
|
|
243
|
+
event.prevent_default()
|
|
244
|
+
event.stop()
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
# Check local-wt column - post event if worktree exists
|
|
248
|
+
if self._local_wt_column_index is not None and col_index == self._local_wt_column_index:
|
|
249
|
+
if row_index < len(self._rows) and self._rows[row_index].exists_locally:
|
|
250
|
+
self.post_message(self.LocalWtClicked(row_index))
|
|
251
|
+
event.prevent_default()
|
|
252
|
+
event.stop()
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
# Check run-id column - post event if run URL exists
|
|
256
|
+
if self._run_id_column_index is not None and col_index == self._run_id_column_index:
|
|
257
|
+
if row_index < len(self._rows) and self._rows[row_index].run_url:
|
|
258
|
+
self.post_message(self.RunIdClicked(row_index))
|
|
259
|
+
event.prevent_default()
|
|
260
|
+
event.stop()
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _strip_rich_markup(text: str) -> str:
|
|
265
|
+
"""Remove Rich markup tags from text.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
text: Text potentially containing Rich markup like [link=...]...[/link]
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Plain text with markup removed
|
|
272
|
+
"""
|
|
273
|
+
import re
|
|
274
|
+
|
|
275
|
+
# Remove [tag=value] and [/tag] patterns
|
|
276
|
+
return re.sub(r"\[/?[^\]]+\]", "", text)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Status bar widget for TUI dashboard."""
|
|
2
|
+
|
|
3
|
+
from textual.widgets import Static
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StatusBar(Static):
|
|
7
|
+
"""Footer status bar showing plan count, refresh status, and messages.
|
|
8
|
+
|
|
9
|
+
Displays:
|
|
10
|
+
- Plan count
|
|
11
|
+
- Last update time
|
|
12
|
+
- Time until next refresh
|
|
13
|
+
- Action messages (e.g., command to copy)
|
|
14
|
+
- Key bindings hint
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
DEFAULT_CSS = """
|
|
18
|
+
StatusBar {
|
|
19
|
+
dock: bottom;
|
|
20
|
+
height: 1;
|
|
21
|
+
background: $surface;
|
|
22
|
+
color: $text-muted;
|
|
23
|
+
padding: 0 1;
|
|
24
|
+
}
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
"""Initialize status bar."""
|
|
29
|
+
super().__init__()
|
|
30
|
+
self._plan_count = 0
|
|
31
|
+
self._seconds_remaining = 0
|
|
32
|
+
self._last_update: str | None = None
|
|
33
|
+
self._fetch_duration: float | None = None
|
|
34
|
+
self._message: str | None = None
|
|
35
|
+
self._sort_mode: str | None = None
|
|
36
|
+
|
|
37
|
+
def set_plan_count(self, count: int) -> None:
|
|
38
|
+
"""Update the plan count display.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
count: Number of plans currently displayed
|
|
42
|
+
"""
|
|
43
|
+
self._plan_count = count
|
|
44
|
+
self._update_display()
|
|
45
|
+
|
|
46
|
+
def set_refresh_countdown(self, seconds: int) -> None:
|
|
47
|
+
"""Update the refresh countdown.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
seconds: Seconds until next refresh
|
|
51
|
+
"""
|
|
52
|
+
self._seconds_remaining = seconds
|
|
53
|
+
self._update_display()
|
|
54
|
+
|
|
55
|
+
def set_message(self, message: str | None) -> None:
|
|
56
|
+
"""Set or clear a status message.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
message: Message to display, or None to clear
|
|
60
|
+
"""
|
|
61
|
+
self._message = message
|
|
62
|
+
self._update_display()
|
|
63
|
+
|
|
64
|
+
def set_last_update(self, time_str: str, duration_secs: float | None = None) -> None:
|
|
65
|
+
"""Set the last update time.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
time_str: Formatted time string (e.g., "14:30:45")
|
|
69
|
+
duration_secs: Duration of the fetch in seconds, or None
|
|
70
|
+
"""
|
|
71
|
+
self._last_update = time_str
|
|
72
|
+
self._fetch_duration = duration_secs
|
|
73
|
+
self._update_display()
|
|
74
|
+
|
|
75
|
+
def set_sort_mode(self, mode: str) -> None:
|
|
76
|
+
"""Set the current sort mode display.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
mode: Sort mode label (e.g., "by issue#", "by recent activity")
|
|
80
|
+
"""
|
|
81
|
+
self._sort_mode = mode
|
|
82
|
+
self._update_display()
|
|
83
|
+
|
|
84
|
+
def _update_display(self) -> None:
|
|
85
|
+
"""Render the status bar content."""
|
|
86
|
+
parts: list[str] = []
|
|
87
|
+
|
|
88
|
+
# Plan count
|
|
89
|
+
if self._plan_count == 1:
|
|
90
|
+
parts.append("1 plan")
|
|
91
|
+
else:
|
|
92
|
+
parts.append(f"{self._plan_count} plans")
|
|
93
|
+
|
|
94
|
+
# Sort mode
|
|
95
|
+
if self._sort_mode:
|
|
96
|
+
parts.append(f"sorted {self._sort_mode}")
|
|
97
|
+
|
|
98
|
+
# Last update time with optional duration
|
|
99
|
+
if self._last_update:
|
|
100
|
+
update_str = f"updated: {self._last_update}"
|
|
101
|
+
if self._fetch_duration is not None:
|
|
102
|
+
update_str += f" ({self._fetch_duration:.1f}s)"
|
|
103
|
+
parts.append(update_str)
|
|
104
|
+
|
|
105
|
+
# Refresh countdown
|
|
106
|
+
if self._seconds_remaining > 0:
|
|
107
|
+
parts.append(f"next: {self._seconds_remaining}s")
|
|
108
|
+
|
|
109
|
+
# Message
|
|
110
|
+
if self._message:
|
|
111
|
+
parts.append(self._message)
|
|
112
|
+
|
|
113
|
+
# Key hints
|
|
114
|
+
parts.append("Enter:open p:PR /:filter s:sort r:refresh q:quit ?:help")
|
|
115
|
+
|
|
116
|
+
self.update(" │ ".join(parts))
|