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,412 @@
|
|
|
1
|
+
"""Sessions screen with list and search."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from textual.app import ComposeResult
|
|
11
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
12
|
+
from textual.reactive import reactive
|
|
13
|
+
from textual.widget import Widget
|
|
14
|
+
from textual.widgets import (
|
|
15
|
+
Button,
|
|
16
|
+
DataTable,
|
|
17
|
+
Input,
|
|
18
|
+
LoadingIndicator,
|
|
19
|
+
Select,
|
|
20
|
+
Static,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from gobby.tui.api_client import GobbyAPIClient
|
|
24
|
+
from gobby.tui.ws_client import GobbyWebSocketClient
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SessionListPanel(Widget):
|
|
30
|
+
"""Panel displaying session list."""
|
|
31
|
+
|
|
32
|
+
DEFAULT_CSS = """
|
|
33
|
+
SessionListPanel {
|
|
34
|
+
width: 1fr;
|
|
35
|
+
height: 1fr;
|
|
36
|
+
border-right: solid #45475a;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
SessionListPanel .panel-header {
|
|
40
|
+
height: auto;
|
|
41
|
+
padding: 1;
|
|
42
|
+
background: #313244;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
SessionListPanel .search-row {
|
|
46
|
+
layout: horizontal;
|
|
47
|
+
height: 3;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
SessionListPanel #session-search {
|
|
51
|
+
width: 1fr;
|
|
52
|
+
margin-right: 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
SessionListPanel #status-filter {
|
|
56
|
+
width: 20;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
SessionListPanel #sessions-table {
|
|
60
|
+
height: 1fr;
|
|
61
|
+
}
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def compose(self) -> ComposeResult:
|
|
65
|
+
with Vertical(classes="panel-header"):
|
|
66
|
+
yield Static("📂 Sessions", classes="panel-title")
|
|
67
|
+
with Horizontal(classes="search-row"):
|
|
68
|
+
yield Input(placeholder="Search sessions...", id="session-search")
|
|
69
|
+
yield Select(
|
|
70
|
+
[
|
|
71
|
+
(label, value)
|
|
72
|
+
for label, value in [
|
|
73
|
+
("All", "all"),
|
|
74
|
+
("Active", "active"),
|
|
75
|
+
("Paused", "paused"),
|
|
76
|
+
("Handoff Ready", "handoff_ready"),
|
|
77
|
+
]
|
|
78
|
+
],
|
|
79
|
+
value="all",
|
|
80
|
+
id="status-filter",
|
|
81
|
+
)
|
|
82
|
+
yield DataTable(id="sessions-table")
|
|
83
|
+
|
|
84
|
+
def on_mount(self) -> None:
|
|
85
|
+
"""Set up the data table."""
|
|
86
|
+
table = self.query_one("#sessions-table", DataTable)
|
|
87
|
+
table.add_columns("ID", "Source", "Status", "Branch", "Age")
|
|
88
|
+
table.cursor_type = "row"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class SessionDetailPanel(Widget):
|
|
92
|
+
"""Panel displaying session details."""
|
|
93
|
+
|
|
94
|
+
DEFAULT_CSS = """
|
|
95
|
+
SessionDetailPanel {
|
|
96
|
+
width: 1fr;
|
|
97
|
+
height: 1fr;
|
|
98
|
+
padding: 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
SessionDetailPanel .detail-header {
|
|
102
|
+
height: auto;
|
|
103
|
+
padding-bottom: 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
SessionDetailPanel .detail-title {
|
|
107
|
+
text-style: bold;
|
|
108
|
+
color: #a78bfa;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
SessionDetailPanel .detail-section {
|
|
112
|
+
padding: 1 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
SessionDetailPanel .detail-row {
|
|
116
|
+
layout: horizontal;
|
|
117
|
+
height: 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
SessionDetailPanel .detail-label {
|
|
121
|
+
color: #a6adc8;
|
|
122
|
+
width: 14;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
SessionDetailPanel .detail-value {
|
|
126
|
+
width: 1fr;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
SessionDetailPanel .context-section {
|
|
130
|
+
padding: 1;
|
|
131
|
+
border: round #45475a;
|
|
132
|
+
height: auto;
|
|
133
|
+
max-height: 15;
|
|
134
|
+
overflow-y: auto;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
SessionDetailPanel .action-buttons {
|
|
138
|
+
layout: horizontal;
|
|
139
|
+
height: 3;
|
|
140
|
+
padding-top: 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
SessionDetailPanel .action-buttons Button {
|
|
144
|
+
margin-right: 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
SessionDetailPanel .empty-state {
|
|
148
|
+
content-align: center middle;
|
|
149
|
+
height: 1fr;
|
|
150
|
+
color: #a6adc8;
|
|
151
|
+
}
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
session: reactive[dict[str, Any] | None] = reactive(None)
|
|
155
|
+
|
|
156
|
+
def compose(self) -> ComposeResult:
|
|
157
|
+
if self.session is None:
|
|
158
|
+
yield Static("Select a session to view details", classes="empty-state")
|
|
159
|
+
else:
|
|
160
|
+
with Vertical(classes="detail-header"):
|
|
161
|
+
yield Static(
|
|
162
|
+
self.session.get("title", "Untitled Session"),
|
|
163
|
+
classes="detail-title",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
with Vertical(classes="detail-section"):
|
|
167
|
+
details = [
|
|
168
|
+
("ID", self.session.get("id", "")[:12] + "..."),
|
|
169
|
+
("Source", self.session.get("source", "Unknown")),
|
|
170
|
+
("Status", self.session.get("status", "unknown")),
|
|
171
|
+
("Branch", self.session.get("git_branch", "N/A")),
|
|
172
|
+
(
|
|
173
|
+
"Project",
|
|
174
|
+
self.session.get("project_id", "N/A")[:12]
|
|
175
|
+
if self.session.get("project_id")
|
|
176
|
+
else "N/A",
|
|
177
|
+
),
|
|
178
|
+
(
|
|
179
|
+
"Machine",
|
|
180
|
+
self.session.get("machine_id", "N/A")[:12]
|
|
181
|
+
if self.session.get("machine_id")
|
|
182
|
+
else "N/A",
|
|
183
|
+
),
|
|
184
|
+
]
|
|
185
|
+
for label, value in details:
|
|
186
|
+
with Horizontal(classes="detail-row"):
|
|
187
|
+
yield Static(f"{label}:", classes="detail-label")
|
|
188
|
+
yield Static(str(value), classes="detail-value")
|
|
189
|
+
|
|
190
|
+
# Show compact context if available
|
|
191
|
+
context = self.session.get("compact_markdown", "")
|
|
192
|
+
if context:
|
|
193
|
+
yield Static("Context:", classes="detail-label")
|
|
194
|
+
yield Static(
|
|
195
|
+
context[:500] + "..." if len(context) > 500 else context,
|
|
196
|
+
classes="context-section",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
with Horizontal(classes="action-buttons"):
|
|
200
|
+
yield Button("Pickup", variant="primary", id="btn-pickup")
|
|
201
|
+
yield Button("View Handoff", id="btn-handoff")
|
|
202
|
+
|
|
203
|
+
def watch_session(self, session: dict[str, Any] | None) -> None:
|
|
204
|
+
"""Recompose when session changes."""
|
|
205
|
+
|
|
206
|
+
def _handle_recompose_error(task: asyncio.Task[None]) -> None:
|
|
207
|
+
if not task.cancelled() and task.exception():
|
|
208
|
+
logger.error(f"Recompose failed: {task.exception()}", exc_info=task.exception())
|
|
209
|
+
|
|
210
|
+
task = asyncio.create_task(self.recompose())
|
|
211
|
+
task.add_done_callback(_handle_recompose_error)
|
|
212
|
+
|
|
213
|
+
def update_session(self, session: dict[str, Any] | None) -> None:
|
|
214
|
+
"""Update the displayed session."""
|
|
215
|
+
self.session = session
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class SessionsScreen(Widget):
|
|
219
|
+
"""Sessions screen with list and detail view."""
|
|
220
|
+
|
|
221
|
+
DEFAULT_CSS = """
|
|
222
|
+
SessionsScreen {
|
|
223
|
+
width: 1fr;
|
|
224
|
+
height: 1fr;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
SessionsScreen #sessions-container {
|
|
228
|
+
layout: horizontal;
|
|
229
|
+
height: 1fr;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
SessionsScreen #list-panel {
|
|
233
|
+
width: 55%;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
SessionsScreen #detail-panel {
|
|
237
|
+
width: 45%;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
SessionsScreen .loading-container {
|
|
241
|
+
width: 1fr;
|
|
242
|
+
height: 1fr;
|
|
243
|
+
content-align: center middle;
|
|
244
|
+
}
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
loading = reactive(True)
|
|
248
|
+
sessions: reactive[list[dict[str, Any]]] = reactive(list)
|
|
249
|
+
selected_session_id: reactive[str | None] = reactive(None)
|
|
250
|
+
current_filter = "all"
|
|
251
|
+
search_query = ""
|
|
252
|
+
|
|
253
|
+
def __init__(
|
|
254
|
+
self,
|
|
255
|
+
api_client: GobbyAPIClient,
|
|
256
|
+
ws_client: GobbyWebSocketClient,
|
|
257
|
+
**kwargs: Any,
|
|
258
|
+
) -> None:
|
|
259
|
+
super().__init__(**kwargs)
|
|
260
|
+
self.api_client = api_client
|
|
261
|
+
self.ws_client = ws_client
|
|
262
|
+
self._session_map: dict[str, dict[str, Any]] = {}
|
|
263
|
+
|
|
264
|
+
def compose(self) -> ComposeResult:
|
|
265
|
+
if self.loading:
|
|
266
|
+
with Container(classes="loading-container"):
|
|
267
|
+
yield LoadingIndicator()
|
|
268
|
+
else:
|
|
269
|
+
with Horizontal(id="sessions-container"):
|
|
270
|
+
yield SessionListPanel(id="list-panel")
|
|
271
|
+
yield SessionDetailPanel(id="detail-panel")
|
|
272
|
+
|
|
273
|
+
async def on_mount(self) -> None:
|
|
274
|
+
"""Load data when mounted."""
|
|
275
|
+
await self.refresh_data()
|
|
276
|
+
|
|
277
|
+
async def refresh_data(self) -> None:
|
|
278
|
+
"""Refresh session list."""
|
|
279
|
+
self.loading = True
|
|
280
|
+
await self.recompose()
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
status = None if self.current_filter == "all" else self.current_filter
|
|
284
|
+
sessions = await self.api_client.list_sessions(status=status, limit=100)
|
|
285
|
+
self.sessions = sessions
|
|
286
|
+
self._session_map = {s.get("id", ""): s for s in sessions}
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
self.notify(f"Failed to load sessions: {e}", severity="error")
|
|
290
|
+
finally:
|
|
291
|
+
self.loading = False
|
|
292
|
+
await self.recompose()
|
|
293
|
+
self._populate_table()
|
|
294
|
+
|
|
295
|
+
def _populate_table(self) -> None:
|
|
296
|
+
"""Populate the sessions table."""
|
|
297
|
+
try:
|
|
298
|
+
table = self.query_one("#sessions-table", DataTable)
|
|
299
|
+
table.clear()
|
|
300
|
+
|
|
301
|
+
# Filter by search query
|
|
302
|
+
filtered = self.sessions
|
|
303
|
+
if self.search_query:
|
|
304
|
+
query = self.search_query.lower()
|
|
305
|
+
filtered = [
|
|
306
|
+
s
|
|
307
|
+
for s in self.sessions
|
|
308
|
+
if query in s.get("id", "").lower()
|
|
309
|
+
or query in s.get("source", "").lower()
|
|
310
|
+
or query in s.get("title", "").lower()
|
|
311
|
+
or query in s.get("git_branch", "").lower()
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
for session in filtered:
|
|
315
|
+
session_id = session.get("id", "")[:12]
|
|
316
|
+
source = session.get("source", "Unknown")[:12]
|
|
317
|
+
status = session.get("status", "unknown")
|
|
318
|
+
branch = session.get("git_branch", "N/A")[:15]
|
|
319
|
+
|
|
320
|
+
# Calculate age
|
|
321
|
+
created = session.get("created_at", "")
|
|
322
|
+
if created:
|
|
323
|
+
try:
|
|
324
|
+
created_dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
|
|
325
|
+
age = datetime.now(created_dt.tzinfo) - created_dt
|
|
326
|
+
if age.days > 0:
|
|
327
|
+
age_str = f"{age.days}d"
|
|
328
|
+
elif age.seconds > 3600:
|
|
329
|
+
age_str = f"{age.seconds // 3600}h"
|
|
330
|
+
else:
|
|
331
|
+
age_str = f"{age.seconds // 60}m"
|
|
332
|
+
except Exception:
|
|
333
|
+
age_str = "?"
|
|
334
|
+
else:
|
|
335
|
+
age_str = "?"
|
|
336
|
+
|
|
337
|
+
table.add_row(session_id, source, status, branch, age_str, key=session.get("id"))
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.debug(f"Widget query failed (may not be mounted): {e}")
|
|
341
|
+
|
|
342
|
+
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
343
|
+
"""Handle session selection."""
|
|
344
|
+
session_id = str(event.row_key.value) if event.row_key else None
|
|
345
|
+
if session_id and session_id in self._session_map:
|
|
346
|
+
self.selected_session_id = session_id
|
|
347
|
+
session = self._session_map[session_id]
|
|
348
|
+
try:
|
|
349
|
+
detail_panel = self.query_one("#detail-panel", SessionDetailPanel)
|
|
350
|
+
detail_panel.update_session(session)
|
|
351
|
+
except Exception as e:
|
|
352
|
+
logger.debug(f"Detail panel query failed (may not be mounted): {e}")
|
|
353
|
+
|
|
354
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
355
|
+
"""Handle search input changes."""
|
|
356
|
+
if event.input.id == "session-search":
|
|
357
|
+
self.search_query = event.value
|
|
358
|
+
self._populate_table()
|
|
359
|
+
|
|
360
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
|
361
|
+
"""Handle filter changes."""
|
|
362
|
+
|
|
363
|
+
def _handle_refresh_error(task: asyncio.Task[None]) -> None:
|
|
364
|
+
if not task.cancelled() and task.exception():
|
|
365
|
+
logger.error(f"Refresh failed: {task.exception()}", exc_info=task.exception())
|
|
366
|
+
|
|
367
|
+
if event.select.id == "status-filter":
|
|
368
|
+
self.current_filter = str(event.value)
|
|
369
|
+
task = asyncio.create_task(self.refresh_data())
|
|
370
|
+
task.add_done_callback(_handle_refresh_error)
|
|
371
|
+
|
|
372
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
373
|
+
"""Handle action button presses."""
|
|
374
|
+
if not self.selected_session_id:
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
button_id = event.button.id
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
if button_id == "btn-pickup":
|
|
381
|
+
await self.api_client.call_tool(
|
|
382
|
+
"gobby-sessions",
|
|
383
|
+
"pickup",
|
|
384
|
+
{"session_id": self.selected_session_id},
|
|
385
|
+
)
|
|
386
|
+
self.notify(f"Session picked up: {self.selected_session_id[:12]}")
|
|
387
|
+
|
|
388
|
+
elif button_id == "btn-handoff":
|
|
389
|
+
result = await self.api_client.call_tool(
|
|
390
|
+
"gobby-sessions",
|
|
391
|
+
"get_handoff_context",
|
|
392
|
+
{"session_id": self.selected_session_id},
|
|
393
|
+
)
|
|
394
|
+
context = result.get("context", "No context available")
|
|
395
|
+
self.notify(f"Handoff context: {len(context)} chars")
|
|
396
|
+
|
|
397
|
+
except Exception as e:
|
|
398
|
+
self.notify(f"Action failed: {e}", severity="error")
|
|
399
|
+
|
|
400
|
+
def on_ws_event(self, event_type: str, data: dict[str, Any]) -> None:
|
|
401
|
+
"""Handle WebSocket events."""
|
|
402
|
+
if event_type == "session_message" or event_type == "hook_event":
|
|
403
|
+
# Refresh on session events
|
|
404
|
+
asyncio.create_task(self.refresh_data())
|
|
405
|
+
|
|
406
|
+
def activate_search(self) -> None:
|
|
407
|
+
"""Focus the search input."""
|
|
408
|
+
try:
|
|
409
|
+
search = self.query_one("#session-search", Input)
|
|
410
|
+
search.focus()
|
|
411
|
+
except Exception:
|
|
412
|
+
pass # nosec B110 - widget may not be mounted yet
|