gobby 0.2.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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""Tasks screen with tree view and detail panel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
10
|
+
from textual.message import Message
|
|
11
|
+
from textual.reactive import reactive
|
|
12
|
+
from textual.widget import Widget
|
|
13
|
+
from textual.widgets import (
|
|
14
|
+
Button,
|
|
15
|
+
LoadingIndicator,
|
|
16
|
+
Select,
|
|
17
|
+
Static,
|
|
18
|
+
Tree,
|
|
19
|
+
)
|
|
20
|
+
from textual.widgets.tree import TreeNode
|
|
21
|
+
|
|
22
|
+
from gobby.tui.api_client import GobbyAPIClient
|
|
23
|
+
from gobby.tui.ws_client import GobbyWebSocketClient
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TaskTreePanel(Widget):
|
|
27
|
+
"""Panel displaying task tree with filtering."""
|
|
28
|
+
|
|
29
|
+
DEFAULT_CSS = """
|
|
30
|
+
TaskTreePanel {
|
|
31
|
+
width: 1fr;
|
|
32
|
+
height: 1fr;
|
|
33
|
+
border-right: solid #45475a;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
TaskTreePanel .panel-header {
|
|
37
|
+
height: auto;
|
|
38
|
+
padding: 1;
|
|
39
|
+
background: #313244;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
TaskTreePanel .filter-row {
|
|
43
|
+
layout: horizontal;
|
|
44
|
+
height: 3;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
TaskTreePanel .filter-select {
|
|
48
|
+
width: 1fr;
|
|
49
|
+
margin-right: 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
TaskTreePanel #task-tree {
|
|
53
|
+
height: 1fr;
|
|
54
|
+
}
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def compose(self) -> ComposeResult:
|
|
58
|
+
with Vertical(classes="panel-header"):
|
|
59
|
+
yield Static("📋 Tasks", classes="panel-title")
|
|
60
|
+
with Horizontal(classes="filter-row"):
|
|
61
|
+
yield Select(
|
|
62
|
+
[
|
|
63
|
+
("All Status", "all"),
|
|
64
|
+
("Open", "open"),
|
|
65
|
+
("In Progress", "in_progress"),
|
|
66
|
+
("Review", "review"),
|
|
67
|
+
("Closed", "closed"),
|
|
68
|
+
],
|
|
69
|
+
value="all",
|
|
70
|
+
id="status-filter",
|
|
71
|
+
classes="filter-select",
|
|
72
|
+
)
|
|
73
|
+
yield Select(
|
|
74
|
+
[
|
|
75
|
+
("All Types", "all"),
|
|
76
|
+
("Task", "task"),
|
|
77
|
+
("Bug", "bug"),
|
|
78
|
+
("Feature", "feature"),
|
|
79
|
+
("Epic", "epic"),
|
|
80
|
+
],
|
|
81
|
+
value="all",
|
|
82
|
+
id="type-filter",
|
|
83
|
+
classes="filter-select",
|
|
84
|
+
)
|
|
85
|
+
yield Tree("Tasks", id="task-tree")
|
|
86
|
+
|
|
87
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
|
88
|
+
"""Handle filter changes."""
|
|
89
|
+
status_val = self.query_one("#status-filter", Select).value
|
|
90
|
+
type_val = self.query_one("#type-filter", Select).value
|
|
91
|
+
self.post_message(
|
|
92
|
+
TasksScreen.FilterChanged(
|
|
93
|
+
status=str(status_val) if status_val is not Select.BLANK else "all",
|
|
94
|
+
task_type=str(type_val) if type_val is not Select.BLANK else "all",
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TaskDetailPanel(Widget):
|
|
100
|
+
"""Panel displaying task details."""
|
|
101
|
+
|
|
102
|
+
DEFAULT_CSS = """
|
|
103
|
+
TaskDetailPanel {
|
|
104
|
+
width: 1fr;
|
|
105
|
+
height: 1fr;
|
|
106
|
+
padding: 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
TaskDetailPanel .detail-header {
|
|
110
|
+
height: auto;
|
|
111
|
+
padding-bottom: 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
TaskDetailPanel .detail-title {
|
|
115
|
+
text-style: bold;
|
|
116
|
+
color: #a78bfa;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
TaskDetailPanel .detail-ref {
|
|
120
|
+
color: #06b6d4;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
TaskDetailPanel .detail-section {
|
|
124
|
+
padding: 1 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
TaskDetailPanel .detail-label {
|
|
128
|
+
color: #a6adc8;
|
|
129
|
+
width: 12;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
TaskDetailPanel .detail-value {
|
|
133
|
+
width: 1fr;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
TaskDetailPanel .detail-description {
|
|
137
|
+
padding: 1;
|
|
138
|
+
border: round #45475a;
|
|
139
|
+
height: auto;
|
|
140
|
+
max-height: 10;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
TaskDetailPanel .action-buttons {
|
|
144
|
+
layout: horizontal;
|
|
145
|
+
height: 3;
|
|
146
|
+
padding-top: 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
TaskDetailPanel .action-buttons Button {
|
|
150
|
+
margin-right: 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
TaskDetailPanel .empty-state {
|
|
154
|
+
content-align: center middle;
|
|
155
|
+
height: 1fr;
|
|
156
|
+
color: #a6adc8;
|
|
157
|
+
}
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
task_data: reactive[dict[str, Any] | None] = reactive(None)
|
|
161
|
+
|
|
162
|
+
def compose(self) -> ComposeResult:
|
|
163
|
+
if self.task_data is None:
|
|
164
|
+
yield Static("Select a task to view details", classes="empty-state")
|
|
165
|
+
else:
|
|
166
|
+
with Vertical(classes="detail-header"):
|
|
167
|
+
yield Static(self.task_data.get("title", "Untitled"), classes="detail-title")
|
|
168
|
+
yield Static(self.task_data.get("ref", ""), classes="detail-ref")
|
|
169
|
+
|
|
170
|
+
with Vertical(classes="detail-section"):
|
|
171
|
+
with Horizontal():
|
|
172
|
+
yield Static("Status:", classes="detail-label")
|
|
173
|
+
yield Static(
|
|
174
|
+
self.task_data.get("status", "unknown"),
|
|
175
|
+
classes="detail-value",
|
|
176
|
+
id="detail-status",
|
|
177
|
+
)
|
|
178
|
+
with Horizontal():
|
|
179
|
+
yield Static("Type:", classes="detail-label")
|
|
180
|
+
yield Static(self.task_data.get("task_type", "task"), classes="detail-value")
|
|
181
|
+
with Horizontal():
|
|
182
|
+
yield Static("Priority:", classes="detail-label")
|
|
183
|
+
yield Static(str(self.task_data.get("priority", 3)), classes="detail-value")
|
|
184
|
+
with Horizontal():
|
|
185
|
+
yield Static("Assignee:", classes="detail-label")
|
|
186
|
+
yield Static(
|
|
187
|
+
self.task_data.get("assignee", "Unassigned"), classes="detail-value"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if self.task_data.get("description"):
|
|
191
|
+
yield Static("Description:", classes="detail-label")
|
|
192
|
+
yield Static(self.task_data.get("description", ""), classes="detail-description")
|
|
193
|
+
|
|
194
|
+
with Horizontal(classes="action-buttons"):
|
|
195
|
+
status = self.task_data.get("status", "")
|
|
196
|
+
if status == "open":
|
|
197
|
+
yield Button("Start", variant="primary", id="btn-start")
|
|
198
|
+
yield Button("Expand", id="btn-expand")
|
|
199
|
+
elif status == "in_progress":
|
|
200
|
+
yield Button("Complete", variant="success", id="btn-complete")
|
|
201
|
+
elif status == "review":
|
|
202
|
+
yield Button("Approve", variant="success", id="btn-approve")
|
|
203
|
+
yield Button("Reopen", variant="error", id="btn-reopen")
|
|
204
|
+
|
|
205
|
+
def watch_task_data(self, task_data: dict[str, Any] | None) -> None:
|
|
206
|
+
"""Recompose when task_data changes."""
|
|
207
|
+
self.call_after_refresh(self.recompose)
|
|
208
|
+
|
|
209
|
+
def update_task(self, task: dict[str, Any] | None) -> None:
|
|
210
|
+
"""Update the displayed task."""
|
|
211
|
+
self.task_data = task
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class TasksScreen(Widget):
|
|
215
|
+
"""Tasks screen with tree view and detail panel."""
|
|
216
|
+
|
|
217
|
+
DEFAULT_CSS = """
|
|
218
|
+
TasksScreen {
|
|
219
|
+
width: 1fr;
|
|
220
|
+
height: 1fr;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
TasksScreen #tasks-container {
|
|
224
|
+
layout: horizontal;
|
|
225
|
+
height: 1fr;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
TasksScreen #tree-panel {
|
|
229
|
+
width: 50%;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
TasksScreen #detail-panel {
|
|
233
|
+
width: 50%;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
TasksScreen .loading-container {
|
|
237
|
+
width: 1fr;
|
|
238
|
+
height: 1fr;
|
|
239
|
+
content-align: center middle;
|
|
240
|
+
}
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
@dataclass
|
|
244
|
+
class FilterChanged(Message):
|
|
245
|
+
"""Message sent when filters change."""
|
|
246
|
+
|
|
247
|
+
status: str
|
|
248
|
+
task_type: str
|
|
249
|
+
|
|
250
|
+
@dataclass
|
|
251
|
+
class TaskSelected(Message):
|
|
252
|
+
"""Message sent when a task is selected."""
|
|
253
|
+
|
|
254
|
+
task_id: str
|
|
255
|
+
|
|
256
|
+
loading = reactive(True)
|
|
257
|
+
tasks: reactive[list[dict[str, Any]]] = reactive(list)
|
|
258
|
+
selected_task_id: reactive[str | None] = reactive(None)
|
|
259
|
+
current_filter_status = "all"
|
|
260
|
+
current_filter_type = "all"
|
|
261
|
+
|
|
262
|
+
def __init__(
|
|
263
|
+
self,
|
|
264
|
+
api_client: GobbyAPIClient,
|
|
265
|
+
ws_client: GobbyWebSocketClient,
|
|
266
|
+
**kwargs: Any,
|
|
267
|
+
) -> None:
|
|
268
|
+
super().__init__(**kwargs)
|
|
269
|
+
self.api_client = api_client
|
|
270
|
+
self.ws_client = ws_client
|
|
271
|
+
self._task_map: dict[str, dict[str, Any]] = {}
|
|
272
|
+
|
|
273
|
+
def compose(self) -> ComposeResult:
|
|
274
|
+
if self.loading:
|
|
275
|
+
with Container(classes="loading-container"):
|
|
276
|
+
yield LoadingIndicator()
|
|
277
|
+
else:
|
|
278
|
+
with Horizontal(id="tasks-container"):
|
|
279
|
+
yield TaskTreePanel(id="tree-panel")
|
|
280
|
+
yield TaskDetailPanel(id="detail-panel")
|
|
281
|
+
|
|
282
|
+
async def on_mount(self) -> None:
|
|
283
|
+
"""Load data when mounted."""
|
|
284
|
+
await self.refresh_data()
|
|
285
|
+
|
|
286
|
+
async def refresh_data(self) -> None:
|
|
287
|
+
"""Refresh task list."""
|
|
288
|
+
self.loading = True
|
|
289
|
+
await self.recompose()
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
async with GobbyAPIClient(self.api_client.base_url) as client:
|
|
293
|
+
# Build filter args
|
|
294
|
+
args: dict[str, Any] = {}
|
|
295
|
+
if self.current_filter_status != "all":
|
|
296
|
+
args["status"] = self.current_filter_status
|
|
297
|
+
if self.current_filter_type != "all":
|
|
298
|
+
args["task_type"] = self.current_filter_type
|
|
299
|
+
|
|
300
|
+
tasks = await client.list_tasks(**args)
|
|
301
|
+
self.task_datas = tasks
|
|
302
|
+
self._task_map = {t.get("id", ""): t for t in tasks}
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
self.notify(f"Failed to load tasks: {e}", severity="error")
|
|
306
|
+
finally:
|
|
307
|
+
self.loading = False
|
|
308
|
+
await self.recompose()
|
|
309
|
+
self._populate_tree()
|
|
310
|
+
|
|
311
|
+
def _populate_tree(self) -> None:
|
|
312
|
+
"""Populate the task tree with loaded tasks."""
|
|
313
|
+
try:
|
|
314
|
+
tree = self.query_one("#task-tree", Tree)
|
|
315
|
+
tree.clear()
|
|
316
|
+
|
|
317
|
+
# Build parent -> children mapping
|
|
318
|
+
children_map: dict[str | None, list[dict[str, Any]]] = {}
|
|
319
|
+
for task in self.task_datas:
|
|
320
|
+
parent_id = task.get("parent_id")
|
|
321
|
+
if parent_id not in children_map:
|
|
322
|
+
children_map[parent_id] = []
|
|
323
|
+
children_map[parent_id].append(task)
|
|
324
|
+
|
|
325
|
+
# Add root level tasks
|
|
326
|
+
root_tasks = children_map.get(None, [])
|
|
327
|
+
for task in sorted(root_tasks, key=lambda t: t.get("priority", 3)):
|
|
328
|
+
self._add_task_to_tree(tree.root, task, children_map)
|
|
329
|
+
|
|
330
|
+
tree.root.expand()
|
|
331
|
+
|
|
332
|
+
except Exception:
|
|
333
|
+
pass # nosec B110 - TUI update failure is non-critical
|
|
334
|
+
|
|
335
|
+
def _add_task_to_tree(
|
|
336
|
+
self,
|
|
337
|
+
parent: TreeNode[str],
|
|
338
|
+
task: dict[str, Any],
|
|
339
|
+
children_map: dict[str | None, list[dict[str, Any]]],
|
|
340
|
+
) -> None:
|
|
341
|
+
"""Recursively add a task and its children to the tree."""
|
|
342
|
+
status = task.get("status", "open")
|
|
343
|
+
status_icon = {
|
|
344
|
+
"open": "○",
|
|
345
|
+
"in_progress": "◐",
|
|
346
|
+
"review": "◑",
|
|
347
|
+
"closed": "●",
|
|
348
|
+
}.get(status, "○")
|
|
349
|
+
|
|
350
|
+
ref = task.get("ref", "")
|
|
351
|
+
title = task.get("title", "Untitled")
|
|
352
|
+
label = f"{status_icon} {ref} {title}"
|
|
353
|
+
|
|
354
|
+
node = parent.add(label, data=task.get("id"))
|
|
355
|
+
|
|
356
|
+
# Add children
|
|
357
|
+
task_id = task.get("id")
|
|
358
|
+
children = children_map.get(task_id, [])
|
|
359
|
+
for child in sorted(children, key=lambda t: t.get("priority", 3)):
|
|
360
|
+
self._add_task_to_tree(node, child, children_map)
|
|
361
|
+
|
|
362
|
+
def on_tree_node_selected(self, event: Tree.NodeSelected[str]) -> None:
|
|
363
|
+
"""Handle task selection in tree."""
|
|
364
|
+
task_id = event.node.data
|
|
365
|
+
if task_id and task_id in self._task_map:
|
|
366
|
+
self.selected_task_id = task_id
|
|
367
|
+
task = self._task_map[task_id]
|
|
368
|
+
try:
|
|
369
|
+
detail_panel = self.query_one("#detail-panel", TaskDetailPanel)
|
|
370
|
+
detail_panel.update_task(task)
|
|
371
|
+
except Exception:
|
|
372
|
+
pass # nosec B110 - widget may not be mounted yet
|
|
373
|
+
|
|
374
|
+
async def on_filter_changed(self, event: FilterChanged) -> None:
|
|
375
|
+
"""Handle filter changes."""
|
|
376
|
+
self.current_filter_status = event.status if event.status != "all" else "all"
|
|
377
|
+
self.current_filter_type = event.task_type if event.task_type != "all" else "all"
|
|
378
|
+
await self.refresh_data()
|
|
379
|
+
|
|
380
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
381
|
+
"""Handle action button presses."""
|
|
382
|
+
if not self.selected_task_id:
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
button_id = event.button.id
|
|
386
|
+
task_id = self.selected_task_id
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
async with GobbyAPIClient(self.api_client.base_url) as client:
|
|
390
|
+
if button_id == "btn-start":
|
|
391
|
+
await client.update_task(task_id, status="in_progress")
|
|
392
|
+
self.notify(f"Task started: {task_id}")
|
|
393
|
+
|
|
394
|
+
elif button_id == "btn-expand":
|
|
395
|
+
await client.call_tool(
|
|
396
|
+
"gobby-tasks",
|
|
397
|
+
"expand_task",
|
|
398
|
+
{"task_id": task_id},
|
|
399
|
+
)
|
|
400
|
+
self.notify(f"Task expanded: {task_id}")
|
|
401
|
+
|
|
402
|
+
elif button_id == "btn-complete":
|
|
403
|
+
# Note: In real usage, this would need a commit SHA
|
|
404
|
+
await client.close_task(
|
|
405
|
+
task_id,
|
|
406
|
+
no_commit_needed=True,
|
|
407
|
+
override_justification="TUI completion - manual user action",
|
|
408
|
+
)
|
|
409
|
+
self.notify(f"Task completed: {task_id}")
|
|
410
|
+
|
|
411
|
+
elif button_id == "btn-approve":
|
|
412
|
+
await client.close_task(
|
|
413
|
+
task_id,
|
|
414
|
+
no_commit_needed=True,
|
|
415
|
+
override_justification="TUI approval - manual user review",
|
|
416
|
+
)
|
|
417
|
+
self.notify(f"Task approved: {task_id}")
|
|
418
|
+
|
|
419
|
+
elif button_id == "btn-reopen":
|
|
420
|
+
await client.call_tool(
|
|
421
|
+
"gobby-tasks",
|
|
422
|
+
"reopen_task",
|
|
423
|
+
{"task_id": task_id},
|
|
424
|
+
)
|
|
425
|
+
self.notify(f"Task reopened: {task_id}")
|
|
426
|
+
|
|
427
|
+
# Refresh after action
|
|
428
|
+
await self.refresh_data()
|
|
429
|
+
|
|
430
|
+
except Exception as e:
|
|
431
|
+
self.notify(f"Action failed: {e}", severity="error")
|
|
432
|
+
|
|
433
|
+
def on_ws_event(self, event_type: str, data: dict[str, Any]) -> None:
|
|
434
|
+
"""Handle WebSocket events."""
|
|
435
|
+
if event_type == "hook_event":
|
|
436
|
+
hook_type = data.get("event_type", "")
|
|
437
|
+
if "task" in hook_type.lower():
|
|
438
|
+
self.run_worker(self.refresh_data(), name="refresh_data", exclusive=True)
|
|
439
|
+
|
|
440
|
+
def activate_search(self) -> None:
|
|
441
|
+
"""Activate search mode."""
|
|
442
|
+
self.notify("Search not yet implemented", severity="information")
|