gobby 0.2.6__py3-none-any.whl → 0.2.8__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 +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +96 -35
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/adapters/gemini.py +140 -38
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +525 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +415 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/macos.py +26 -1
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +0 -2
- gobby/cli/install.py +4 -4
- gobby/cli/installers/claude.py +6 -0
- gobby/cli/installers/gemini.py +6 -0
- gobby/cli/installers/shared.py +103 -4
- gobby/cli/memory.py +185 -0
- gobby/cli/sessions.py +1 -1
- gobby/cli/utils.py +9 -2
- gobby/clones/git.py +177 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +10 -94
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/skills.py +31 -0
- gobby/config/tasks.py +4 -28
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +150 -8
- gobby/hooks/hook_manager.py +21 -3
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/webhooks.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/llm/resolver.py +3 -2
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +4 -2
- gobby/mcp_proxy/registries.py +22 -8
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +76 -740
- gobby/mcp_proxy/tools/artifacts.py +43 -9
- gobby/mcp_proxy/tools/clones.py +0 -385
- gobby/mcp_proxy/tools/memory.py +2 -2
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
- gobby/mcp_proxy/tools/skills/__init__.py +14 -29
- gobby/mcp_proxy/tools/spawn_agent.py +455 -0
- gobby/mcp_proxy/tools/tasks/_context.py +18 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows.py +84 -34
- gobby/mcp_proxy/tools/worktrees.py +32 -350
- gobby/memory/extractor.py +15 -1
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +62 -283
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +13 -0
- gobby/servers/http.py +1 -4
- gobby/servers/routes/admin.py +14 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +51 -4
- gobby/servers/routes/mcp/tools.py +48 -1506
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +2 -2
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/manager.py +9 -0
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +15 -5
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/skills/parser.py +30 -2
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +174 -368
- gobby/storage/sessions.py +45 -7
- gobby/storage/skills.py +80 -7
- gobby/storage/tasks/_lifecycle.py +18 -3
- gobby/sync/memories.py +1 -1
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +22 -20
- gobby/tools/summarizer.py +91 -10
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1217
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +50 -1
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +281 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/enforcement/task_policy.py +542 -0
- gobby/workflows/engine.py +93 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +91 -0
- gobby/workflows/safe_evaluator.py +191 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +217 -51
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1332
- gobby/cli/tui.py +0 -34
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/mcp_proxy/tools/session_messages.py +0 -1055
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/handoff/compact.md +0 -63
- gobby/prompts/defaults/handoff/session_end.md +0 -57
- gobby/prompts/defaults/memory/extract.md +0 -61
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tui/__init__.py +0 -5
- gobby/tui/api_client.py +0 -278
- gobby/tui/app.py +0 -329
- gobby/tui/screens/__init__.py +0 -25
- gobby/tui/screens/agents.py +0 -333
- gobby/tui/screens/chat.py +0 -450
- gobby/tui/screens/dashboard.py +0 -377
- gobby/tui/screens/memory.py +0 -305
- gobby/tui/screens/metrics.py +0 -231
- gobby/tui/screens/orchestrator.py +0 -903
- gobby/tui/screens/sessions.py +0 -412
- gobby/tui/screens/tasks.py +0 -440
- gobby/tui/screens/workflows.py +0 -289
- gobby/tui/screens/worktrees.py +0 -174
- gobby/tui/widgets/__init__.py +0 -21
- gobby/tui/widgets/chat.py +0 -210
- gobby/tui/widgets/conductor.py +0 -104
- gobby/tui/widgets/menu.py +0 -132
- gobby/tui/widgets/message_panel.py +0 -160
- gobby/tui/widgets/review_gate.py +0 -224
- gobby/tui/widgets/task_tree.py +0 -99
- gobby/tui/widgets/token_budget.py +0 -166
- gobby/tui/ws_client.py +0 -258
- gobby/workflows/task_enforcement_actions.py +0 -1343
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
gobby/tui/screens/sessions.py
DELETED
|
@@ -1,412 +0,0 @@
|
|
|
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
|