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,61 @@
|
|
|
1
|
+
"""Main Textual application for JSONL viewer."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from textual.app import App, ComposeResult
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.widgets import Footer, Header
|
|
8
|
+
|
|
9
|
+
from erk.tui.jsonl_viewer.models import parse_jsonl_file
|
|
10
|
+
from erk.tui.jsonl_viewer.widgets import CustomListView, JsonlEntryItem
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JsonlViewerApp(App):
|
|
14
|
+
"""Interactive TUI for viewing JSONL files."""
|
|
15
|
+
|
|
16
|
+
BINDINGS = [
|
|
17
|
+
Binding("q", "quit", "Quit"),
|
|
18
|
+
Binding("escape", "quit", "Quit"),
|
|
19
|
+
Binding("j", "cursor_down", "Down", show=False),
|
|
20
|
+
Binding("k", "cursor_up", "Up", show=False),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
DEFAULT_CSS = """
|
|
24
|
+
JsonlViewerApp {
|
|
25
|
+
background: $surface;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ListView {
|
|
29
|
+
height: 1fr;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ListView > ListItem.--highlight {
|
|
33
|
+
background: $primary-darken-2;
|
|
34
|
+
}
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, jsonl_path: Path) -> None:
|
|
38
|
+
"""Initialize with path to JSONL file.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
jsonl_path: Path to the JSONL file to view
|
|
42
|
+
"""
|
|
43
|
+
super().__init__()
|
|
44
|
+
self._jsonl_path = jsonl_path
|
|
45
|
+
self._entries = parse_jsonl_file(jsonl_path)
|
|
46
|
+
|
|
47
|
+
def compose(self) -> ComposeResult:
|
|
48
|
+
"""Create application layout."""
|
|
49
|
+
yield Header(show_clock=False)
|
|
50
|
+
yield CustomListView(*[JsonlEntryItem(entry) for entry in self._entries])
|
|
51
|
+
yield Footer()
|
|
52
|
+
|
|
53
|
+
def action_cursor_down(self) -> None:
|
|
54
|
+
"""Move cursor down (vim j key)."""
|
|
55
|
+
list_view = self.query_one(CustomListView)
|
|
56
|
+
list_view.action_cursor_down()
|
|
57
|
+
|
|
58
|
+
def action_cursor_up(self) -> None:
|
|
59
|
+
"""Move cursor up (vim k key)."""
|
|
60
|
+
list_view = self.query_one(CustomListView)
|
|
61
|
+
list_view.action_cursor_up()
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Data models for JSONL viewer."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class JsonlEntry:
|
|
10
|
+
"""Represents a single entry from a JSONL file."""
|
|
11
|
+
|
|
12
|
+
line_number: int
|
|
13
|
+
entry_type: str
|
|
14
|
+
role: str | None
|
|
15
|
+
tool_name: str | None
|
|
16
|
+
raw_json: str
|
|
17
|
+
parsed: dict
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def extract_tool_name(entry: dict) -> str | None:
|
|
21
|
+
"""Extract tool name from tool_use content blocks.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
entry: Parsed JSON entry
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Tool name if found, None otherwise
|
|
28
|
+
"""
|
|
29
|
+
message = entry.get("message")
|
|
30
|
+
if not isinstance(message, dict):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
content = message.get("content")
|
|
34
|
+
if not isinstance(content, list):
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
# Find first tool_use block
|
|
38
|
+
for block in content:
|
|
39
|
+
if isinstance(block, dict) and block.get("type") == "tool_use":
|
|
40
|
+
name = block.get("name")
|
|
41
|
+
if isinstance(name, str):
|
|
42
|
+
return name
|
|
43
|
+
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def format_summary(entry: JsonlEntry) -> str:
|
|
48
|
+
"""Format entry summary for display.
|
|
49
|
+
|
|
50
|
+
Format: [line#] type | tool_name?
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
entry: JSONL entry to format
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Formatted summary string
|
|
57
|
+
"""
|
|
58
|
+
line_str = f"[{entry.line_number:>4}]"
|
|
59
|
+
|
|
60
|
+
parts = [line_str, entry.entry_type]
|
|
61
|
+
if entry.tool_name:
|
|
62
|
+
parts.append(entry.tool_name)
|
|
63
|
+
|
|
64
|
+
return " | ".join(parts)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _interpret_escape_sequences(text: str) -> str:
|
|
68
|
+
"""Convert literal escape sequences to actual characters.
|
|
69
|
+
|
|
70
|
+
Converts \\n, \\t, \\r to their actual character equivalents.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
text: Text with literal escape sequences
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Text with actual newlines, tabs, carriage returns
|
|
77
|
+
"""
|
|
78
|
+
return text.replace("\\n", "\n").replace("\\t", "\t").replace("\\r", "\r")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _format_as_yaml_like(value: object, indent: int = 0) -> str:
|
|
82
|
+
"""Format a value in YAML-like style for readability.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
value: Value to format (dict, list, str, int, float, bool, None)
|
|
86
|
+
indent: Current indentation level
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Formatted string representation
|
|
90
|
+
"""
|
|
91
|
+
prefix = " " * indent
|
|
92
|
+
|
|
93
|
+
if value is None:
|
|
94
|
+
return "null"
|
|
95
|
+
|
|
96
|
+
if isinstance(value, bool):
|
|
97
|
+
return "true" if value else "false"
|
|
98
|
+
|
|
99
|
+
if isinstance(value, (int, float)):
|
|
100
|
+
return str(value)
|
|
101
|
+
|
|
102
|
+
if isinstance(value, str):
|
|
103
|
+
# Interpret escape sequences - no additional formatting for strings
|
|
104
|
+
return _interpret_escape_sequences(value)
|
|
105
|
+
|
|
106
|
+
if isinstance(value, list):
|
|
107
|
+
if not value:
|
|
108
|
+
return "[]"
|
|
109
|
+
lines = []
|
|
110
|
+
for item in value:
|
|
111
|
+
formatted_item = _format_as_yaml_like(item, indent + 1)
|
|
112
|
+
if "\n" in formatted_item:
|
|
113
|
+
# Multi-line item
|
|
114
|
+
first_line, *rest = formatted_item.split("\n")
|
|
115
|
+
lines.append(f"{prefix}- {first_line}")
|
|
116
|
+
lines.extend(rest)
|
|
117
|
+
else:
|
|
118
|
+
lines.append(f"{prefix}- {formatted_item}")
|
|
119
|
+
return "\n".join(lines)
|
|
120
|
+
|
|
121
|
+
if isinstance(value, dict):
|
|
122
|
+
if not value:
|
|
123
|
+
return "{}"
|
|
124
|
+
lines = []
|
|
125
|
+
for k, v in value.items():
|
|
126
|
+
formatted_v = _format_as_yaml_like(v, indent + 1)
|
|
127
|
+
if "\n" in formatted_v:
|
|
128
|
+
# Multi-line value
|
|
129
|
+
first_line, *rest = formatted_v.split("\n")
|
|
130
|
+
lines.append(f"{prefix}{k}: {first_line}")
|
|
131
|
+
lines.extend(rest)
|
|
132
|
+
else:
|
|
133
|
+
lines.append(f"{prefix}{k}: {formatted_v}")
|
|
134
|
+
return "\n".join(lines)
|
|
135
|
+
|
|
136
|
+
# Fallback for unknown types
|
|
137
|
+
return str(value)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def format_entry_detail(entry: JsonlEntry, formatted: bool = True) -> str:
|
|
141
|
+
"""Format entry detail for display.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
entry: JSONL entry to format
|
|
145
|
+
formatted: If True, use YAML-like formatting. If False, use raw JSON.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Formatted string representation
|
|
149
|
+
"""
|
|
150
|
+
if not formatted:
|
|
151
|
+
return entry.raw_json
|
|
152
|
+
|
|
153
|
+
return _format_as_yaml_like(entry.parsed)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def parse_jsonl_file(path: Path) -> list[JsonlEntry]:
|
|
157
|
+
"""Parse JSONL file into list of entries.
|
|
158
|
+
|
|
159
|
+
Skips empty lines and malformed JSON.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
path: Path to JSONL file
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
List of parsed entries
|
|
166
|
+
"""
|
|
167
|
+
entries: list[JsonlEntry] = []
|
|
168
|
+
content = path.read_text(encoding="utf-8")
|
|
169
|
+
|
|
170
|
+
for line_number, line in enumerate(content.splitlines(), start=1):
|
|
171
|
+
stripped = line.strip()
|
|
172
|
+
if not stripped:
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
parsed = json.loads(stripped)
|
|
177
|
+
except json.JSONDecodeError:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
if not isinstance(parsed, dict):
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
entry_type = parsed.get("type", "unknown")
|
|
184
|
+
if not isinstance(entry_type, str):
|
|
185
|
+
entry_type = "unknown"
|
|
186
|
+
|
|
187
|
+
# Extract role from message if present
|
|
188
|
+
role: str | None = None
|
|
189
|
+
message = parsed.get("message")
|
|
190
|
+
if isinstance(message, dict):
|
|
191
|
+
msg_role = message.get("role")
|
|
192
|
+
if isinstance(msg_role, str):
|
|
193
|
+
role = msg_role
|
|
194
|
+
|
|
195
|
+
tool_name = extract_tool_name(parsed)
|
|
196
|
+
|
|
197
|
+
entries.append(
|
|
198
|
+
JsonlEntry(
|
|
199
|
+
line_number=line_number,
|
|
200
|
+
entry_type=entry_type,
|
|
201
|
+
role=role,
|
|
202
|
+
tool_name=tool_name,
|
|
203
|
+
raw_json=stripped,
|
|
204
|
+
parsed=parsed,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return entries
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Widgets for JSONL viewer."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from rich.markup import escape as escape_markup
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.binding import Binding
|
|
8
|
+
from textual.containers import Vertical
|
|
9
|
+
from textual.widgets import Label, ListItem, ListView, Static
|
|
10
|
+
|
|
11
|
+
from erk.tui.jsonl_viewer.models import JsonlEntry, format_entry_detail, format_summary
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JsonlEntryItem(ListItem):
|
|
15
|
+
"""Expandable JSONL entry with summary and JSON detail."""
|
|
16
|
+
|
|
17
|
+
DEFAULT_CSS = """
|
|
18
|
+
JsonlEntryItem {
|
|
19
|
+
height: auto;
|
|
20
|
+
padding: 0 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
JsonlEntryItem .entry-summary {
|
|
24
|
+
height: 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
JsonlEntryItem .entry-summary-user {
|
|
28
|
+
color: #58a6ff;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
JsonlEntryItem .entry-summary-assistant {
|
|
32
|
+
color: #7ee787;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
JsonlEntryItem .entry-summary-tool-result {
|
|
36
|
+
color: #ffa657;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
JsonlEntryItem .entry-summary-other {
|
|
40
|
+
color: $text-muted;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
JsonlEntryItem .json-detail {
|
|
44
|
+
display: none;
|
|
45
|
+
padding: 1;
|
|
46
|
+
background: $surface-darken-1;
|
|
47
|
+
overflow-x: auto;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
JsonlEntryItem.expanded .json-detail {
|
|
51
|
+
display: block;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
JsonlEntryItem.selected {
|
|
55
|
+
background: $accent;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
JsonlEntryItem.selected .entry-summary {
|
|
59
|
+
color: $text;
|
|
60
|
+
}
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, entry: JsonlEntry) -> None:
|
|
64
|
+
"""Initialize with JSONL entry.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
entry: The JSONL entry to display
|
|
68
|
+
"""
|
|
69
|
+
super().__init__()
|
|
70
|
+
self._entry = entry
|
|
71
|
+
self._expanded = False
|
|
72
|
+
|
|
73
|
+
def compose(self) -> ComposeResult:
|
|
74
|
+
"""Create widget content."""
|
|
75
|
+
summary = format_summary(self._entry)
|
|
76
|
+
|
|
77
|
+
# Determine style class based on entry type
|
|
78
|
+
entry_type = self._entry.entry_type
|
|
79
|
+
if entry_type == "user":
|
|
80
|
+
style_class = "entry-summary entry-summary-user"
|
|
81
|
+
elif entry_type == "assistant":
|
|
82
|
+
style_class = "entry-summary entry-summary-assistant"
|
|
83
|
+
elif entry_type == "tool_result":
|
|
84
|
+
style_class = "entry-summary entry-summary-tool-result"
|
|
85
|
+
else:
|
|
86
|
+
style_class = "entry-summary entry-summary-other"
|
|
87
|
+
|
|
88
|
+
yield Label(escape_markup(summary), classes=style_class)
|
|
89
|
+
|
|
90
|
+
# Pretty-printed JSON detail (hidden by default)
|
|
91
|
+
# Use markup=False to avoid Rich interpreting brackets as markup tags
|
|
92
|
+
pretty_json = json.dumps(self._entry.parsed, indent=2)
|
|
93
|
+
with Vertical(classes="json-detail"):
|
|
94
|
+
yield Static(pretty_json, markup=False)
|
|
95
|
+
|
|
96
|
+
def toggle_expand(self) -> None:
|
|
97
|
+
"""Toggle expand/collapse state."""
|
|
98
|
+
self._expanded = not self._expanded
|
|
99
|
+
if self._expanded:
|
|
100
|
+
self.add_class("expanded")
|
|
101
|
+
else:
|
|
102
|
+
self.remove_class("expanded")
|
|
103
|
+
# Ensure the widget is updated
|
|
104
|
+
self.refresh()
|
|
105
|
+
|
|
106
|
+
def set_expanded(self, expanded: bool) -> None:
|
|
107
|
+
"""Set expand state explicitly.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
expanded: Whether to expand (True) or collapse (False)
|
|
111
|
+
"""
|
|
112
|
+
self._expanded = expanded
|
|
113
|
+
if expanded:
|
|
114
|
+
self.add_class("expanded")
|
|
115
|
+
else:
|
|
116
|
+
self.remove_class("expanded")
|
|
117
|
+
self.refresh()
|
|
118
|
+
|
|
119
|
+
def is_expanded(self) -> bool:
|
|
120
|
+
"""Return current expanded state."""
|
|
121
|
+
return self._expanded
|
|
122
|
+
|
|
123
|
+
def update_format(self, formatted: bool) -> None:
|
|
124
|
+
"""Update the JSON detail display format.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
formatted: If True, use YAML-like format. If False, use raw JSON.
|
|
128
|
+
"""
|
|
129
|
+
detail_container = self.query_one(".json-detail", Vertical)
|
|
130
|
+
static = detail_container.query_one(Static)
|
|
131
|
+
content = format_entry_detail(self._entry, formatted=formatted)
|
|
132
|
+
# No escape_markup needed - Static widget has markup=False
|
|
133
|
+
static.update(content)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class CustomListView(ListView):
|
|
137
|
+
"""Custom ListView with expand/collapse keybinding."""
|
|
138
|
+
|
|
139
|
+
BINDINGS = [
|
|
140
|
+
Binding("enter", "toggle_expand", "Expand/Collapse"),
|
|
141
|
+
Binding("f", "toggle_format", "Format"),
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
def __init__(self, *children: ListItem) -> None:
|
|
145
|
+
"""Initialize with format and expand mode state.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
children: List items to include in the view
|
|
149
|
+
"""
|
|
150
|
+
super().__init__(*children)
|
|
151
|
+
self._formatted_mode = True
|
|
152
|
+
self._expand_mode = False
|
|
153
|
+
self._expanded_item: JsonlEntryItem | None = None
|
|
154
|
+
|
|
155
|
+
def action_toggle_expand(self) -> None:
|
|
156
|
+
"""Toggle expand/collapse for selected entry."""
|
|
157
|
+
highlighted = self.highlighted_child
|
|
158
|
+
if isinstance(highlighted, JsonlEntryItem):
|
|
159
|
+
highlighted.toggle_expand()
|
|
160
|
+
# Track expand mode
|
|
161
|
+
self._expand_mode = highlighted.is_expanded()
|
|
162
|
+
self._expanded_item = highlighted if self._expand_mode else None
|
|
163
|
+
|
|
164
|
+
def action_toggle_format(self) -> None:
|
|
165
|
+
"""Toggle format mode between formatted and raw JSON."""
|
|
166
|
+
self._formatted_mode = not self._formatted_mode
|
|
167
|
+
# Update all items with new format
|
|
168
|
+
for child in self.children:
|
|
169
|
+
if isinstance(child, JsonlEntryItem):
|
|
170
|
+
child.update_format(self._formatted_mode)
|
|
171
|
+
|
|
172
|
+
def watch_index(self, old_index: int | None, new_index: int | None) -> None:
|
|
173
|
+
"""Handle index changes for sticky expand mode and selection styling.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
old_index: Previous highlighted index (None if no previous selection)
|
|
177
|
+
new_index: New highlighted index (None if no selection)
|
|
178
|
+
"""
|
|
179
|
+
# Update selection styling
|
|
180
|
+
if old_index is not None and old_index >= 0 and old_index < len(self.children):
|
|
181
|
+
old_child = self.children[old_index]
|
|
182
|
+
if isinstance(old_child, JsonlEntryItem):
|
|
183
|
+
old_child.remove_class("selected")
|
|
184
|
+
|
|
185
|
+
if new_index is not None and new_index >= 0 and new_index < len(self.children):
|
|
186
|
+
new_child = self.children[new_index]
|
|
187
|
+
if isinstance(new_child, JsonlEntryItem):
|
|
188
|
+
new_child.add_class("selected")
|
|
189
|
+
|
|
190
|
+
# Sticky expand mode: maintain expand state when navigating
|
|
191
|
+
if self._expand_mode and self._expanded_item is not None:
|
|
192
|
+
# Collapse previous expanded item
|
|
193
|
+
self._expanded_item.set_expanded(False)
|
|
194
|
+
# Expand new item
|
|
195
|
+
new_child.set_expanded(True)
|
|
196
|
+
self._expanded_item = new_child
|
|
197
|
+
|
|
198
|
+
def _on_list_item__child_clicked(self, event: ListItem._ChildClicked) -> None:
|
|
199
|
+
"""Disable mouse clicks for item selection.
|
|
200
|
+
|
|
201
|
+
We want keyboard-only navigation for this viewer.
|
|
202
|
+
"""
|
|
203
|
+
event.prevent_default()
|
|
204
|
+
event.stop()
|
erk/tui/sorting/logic.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Pure sorting logic for TUI dashboard."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from erk.tui.data.types import PlanRowData
|
|
6
|
+
from erk.tui.sorting.types import BranchActivity, SortKey
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def sort_plans(
|
|
10
|
+
plans: list[PlanRowData],
|
|
11
|
+
sort_key: SortKey,
|
|
12
|
+
activity_by_issue: dict[int, BranchActivity] | None = None,
|
|
13
|
+
) -> list[PlanRowData]:
|
|
14
|
+
"""Sort plans by the given key.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
plans: List of plans to sort
|
|
18
|
+
sort_key: Which field to sort by
|
|
19
|
+
activity_by_issue: Mapping of issue number to branch activity data.
|
|
20
|
+
Required when sort_key is BRANCH_ACTIVITY.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Sorted list of plans. Original list is not modified.
|
|
24
|
+
"""
|
|
25
|
+
if sort_key == SortKey.ISSUE_NUMBER:
|
|
26
|
+
# Sort by issue number descending (newest first)
|
|
27
|
+
return sorted(plans, key=lambda p: p.issue_number, reverse=True)
|
|
28
|
+
|
|
29
|
+
if sort_key == SortKey.BRANCH_ACTIVITY:
|
|
30
|
+
# Sort by most recent commit on branch
|
|
31
|
+
# Plans with recent activity first, no activity at end
|
|
32
|
+
activity_map = activity_by_issue or {}
|
|
33
|
+
|
|
34
|
+
def get_activity_key(plan: PlanRowData) -> tuple[bool, datetime]:
|
|
35
|
+
"""Return sort key tuple: (has_activity, timestamp).
|
|
36
|
+
|
|
37
|
+
Returns tuple where:
|
|
38
|
+
- has_activity: True if there's branch activity (so it sorts first)
|
|
39
|
+
- timestamp: The activity timestamp (or min datetime for no activity)
|
|
40
|
+
"""
|
|
41
|
+
activity = activity_map.get(plan.issue_number)
|
|
42
|
+
if activity is None or activity.last_commit_at is None:
|
|
43
|
+
# No activity - sort to end with very old date
|
|
44
|
+
return (False, datetime.min)
|
|
45
|
+
return (True, activity.last_commit_at)
|
|
46
|
+
|
|
47
|
+
# Sort: has_activity=True first, then by timestamp descending (newest first)
|
|
48
|
+
return sorted(
|
|
49
|
+
plans,
|
|
50
|
+
key=get_activity_key,
|
|
51
|
+
reverse=True,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Default fallback: return as-is
|
|
55
|
+
return list(plans)
|
erk/tui/sorting/types.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Sort state types for TUI dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum, auto
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SortKey(Enum):
|
|
11
|
+
"""Available sort keys for plan list."""
|
|
12
|
+
|
|
13
|
+
ISSUE_NUMBER = auto() # Default: sort by issue number (descending)
|
|
14
|
+
BRANCH_ACTIVITY = auto() # Sort by most recent commit on branch
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class BranchActivity:
|
|
19
|
+
"""Branch activity data for a plan.
|
|
20
|
+
|
|
21
|
+
Represents the most recent commit on the branch (not in trunk),
|
|
22
|
+
indicating how recently the branch was worked on.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
last_commit_at: Timestamp of most recent commit on branch, None if no commits
|
|
26
|
+
last_commit_author: Author of most recent commit, None if no commits
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
last_commit_at: datetime | None
|
|
30
|
+
last_commit_author: str | None
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def empty() -> BranchActivity:
|
|
34
|
+
"""Create empty activity (no commits on branch)."""
|
|
35
|
+
return BranchActivity(last_commit_at=None, last_commit_author=None)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class SortState:
|
|
40
|
+
"""State for sort mode.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
key: Current sort key
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
key: SortKey
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def initial() -> SortState:
|
|
50
|
+
"""Create initial state with default sort (by issue number)."""
|
|
51
|
+
return SortState(key=SortKey.ISSUE_NUMBER)
|
|
52
|
+
|
|
53
|
+
def toggle(self) -> SortState:
|
|
54
|
+
"""Toggle between sort keys.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
New state with next sort key
|
|
58
|
+
"""
|
|
59
|
+
if self.key == SortKey.ISSUE_NUMBER:
|
|
60
|
+
return SortState(key=SortKey.BRANCH_ACTIVITY)
|
|
61
|
+
return SortState(key=SortKey.ISSUE_NUMBER)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def display_label(self) -> str:
|
|
65
|
+
"""Get display label for current sort mode."""
|
|
66
|
+
if self.key == SortKey.ISSUE_NUMBER:
|
|
67
|
+
return "by issue#"
|
|
68
|
+
return "by recent activity"
|
erk/tui/styles/dash.tcss
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* Styles for erk dash interactive TUI */
|
|
2
|
+
|
|
3
|
+
Screen {
|
|
4
|
+
background: $surface;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/* Plan table styling */
|
|
8
|
+
PlanDataTable {
|
|
9
|
+
height: 1fr;
|
|
10
|
+
margin: 1 2;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
PlanDataTable > .datatable--header {
|
|
14
|
+
background: $primary;
|
|
15
|
+
color: $text;
|
|
16
|
+
text-style: bold;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
PlanDataTable > .datatable--cursor {
|
|
20
|
+
background: $accent;
|
|
21
|
+
color: $text;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
PlanDataTable > .datatable--hover {
|
|
25
|
+
background: $surface-lighten-1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Status bar styling */
|
|
29
|
+
StatusBar {
|
|
30
|
+
dock: bottom;
|
|
31
|
+
height: 1;
|
|
32
|
+
background: $primary-background;
|
|
33
|
+
color: $text-muted;
|
|
34
|
+
padding: 0 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Help overlay styling */
|
|
38
|
+
#help-container {
|
|
39
|
+
align: center middle;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#help-panel {
|
|
43
|
+
width: 60;
|
|
44
|
+
height: auto;
|
|
45
|
+
max-height: 80%;
|
|
46
|
+
background: $surface;
|
|
47
|
+
border: solid $primary;
|
|
48
|
+
padding: 1 2;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#help-title {
|
|
52
|
+
text-style: bold;
|
|
53
|
+
text-align: center;
|
|
54
|
+
margin-bottom: 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#help-content {
|
|
58
|
+
height: auto;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Loading indicator */
|
|
62
|
+
#loading {
|
|
63
|
+
align: center middle;
|
|
64
|
+
height: 100%;
|
|
65
|
+
width: 100%;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#loading-message {
|
|
69
|
+
text-align: center;
|
|
70
|
+
color: $text-muted;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Header styling */
|
|
74
|
+
#header {
|
|
75
|
+
dock: top;
|
|
76
|
+
height: 1;
|
|
77
|
+
background: $primary;
|
|
78
|
+
color: $text;
|
|
79
|
+
text-style: bold;
|
|
80
|
+
content-align: center middle;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Filter input styling - positioned above status bar */
|
|
84
|
+
#filter-input {
|
|
85
|
+
dock: bottom;
|
|
86
|
+
height: 3;
|
|
87
|
+
display: none;
|
|
88
|
+
background: $primary;
|
|
89
|
+
border: solid $accent;
|
|
90
|
+
padding: 0 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#filter-input.visible {
|
|
94
|
+
display: block;
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""TUI widgets for erk dashboard."""
|