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.
Files changed (198) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +96 -35
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/adapters/gemini.py +140 -38
  10. gobby/agents/definitions.py +11 -1
  11. gobby/agents/isolation.py +525 -0
  12. gobby/agents/registry.py +11 -0
  13. gobby/agents/sandbox.py +261 -0
  14. gobby/agents/session.py +1 -0
  15. gobby/agents/spawn.py +42 -287
  16. gobby/agents/spawn_executor.py +415 -0
  17. gobby/agents/spawners/__init__.py +24 -0
  18. gobby/agents/spawners/command_builder.py +189 -0
  19. gobby/agents/spawners/embedded.py +21 -2
  20. gobby/agents/spawners/headless.py +21 -2
  21. gobby/agents/spawners/macos.py +26 -1
  22. gobby/agents/spawners/prompt_manager.py +125 -0
  23. gobby/cli/__init__.py +0 -2
  24. gobby/cli/install.py +4 -4
  25. gobby/cli/installers/claude.py +6 -0
  26. gobby/cli/installers/gemini.py +6 -0
  27. gobby/cli/installers/shared.py +103 -4
  28. gobby/cli/memory.py +185 -0
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/utils.py +9 -2
  31. gobby/clones/git.py +177 -0
  32. gobby/config/__init__.py +12 -97
  33. gobby/config/app.py +10 -94
  34. gobby/config/extensions.py +2 -2
  35. gobby/config/features.py +7 -130
  36. gobby/config/skills.py +31 -0
  37. gobby/config/tasks.py +4 -28
  38. gobby/hooks/__init__.py +0 -13
  39. gobby/hooks/event_handlers.py +150 -8
  40. gobby/hooks/hook_manager.py +21 -3
  41. gobby/hooks/plugins.py +1 -1
  42. gobby/hooks/webhooks.py +1 -1
  43. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  44. gobby/llm/resolver.py +3 -2
  45. gobby/mcp_proxy/importer.py +62 -4
  46. gobby/mcp_proxy/instructions.py +4 -2
  47. gobby/mcp_proxy/registries.py +22 -8
  48. gobby/mcp_proxy/services/recommendation.py +43 -11
  49. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  50. gobby/mcp_proxy/tools/agents.py +76 -740
  51. gobby/mcp_proxy/tools/artifacts.py +43 -9
  52. gobby/mcp_proxy/tools/clones.py +0 -385
  53. gobby/mcp_proxy/tools/memory.py +2 -2
  54. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  55. gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
  56. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  57. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  58. gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
  59. gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
  60. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  61. gobby/mcp_proxy/tools/spawn_agent.py +455 -0
  62. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  63. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  64. gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
  65. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  66. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  67. gobby/mcp_proxy/tools/workflows.py +84 -34
  68. gobby/mcp_proxy/tools/worktrees.py +32 -350
  69. gobby/memory/extractor.py +15 -1
  70. gobby/memory/ingestion/__init__.py +5 -0
  71. gobby/memory/ingestion/multimodal.py +221 -0
  72. gobby/memory/manager.py +62 -283
  73. gobby/memory/search/__init__.py +10 -0
  74. gobby/memory/search/coordinator.py +248 -0
  75. gobby/memory/services/__init__.py +5 -0
  76. gobby/memory/services/crossref.py +142 -0
  77. gobby/prompts/loader.py +5 -2
  78. gobby/runner.py +13 -0
  79. gobby/servers/http.py +1 -4
  80. gobby/servers/routes/admin.py +14 -0
  81. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  82. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  83. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  84. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  85. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  86. gobby/servers/routes/mcp/hooks.py +51 -4
  87. gobby/servers/routes/mcp/tools.py +48 -1506
  88. gobby/servers/websocket.py +57 -1
  89. gobby/sessions/analyzer.py +2 -2
  90. gobby/sessions/lifecycle.py +1 -1
  91. gobby/sessions/manager.py +9 -0
  92. gobby/sessions/processor.py +10 -0
  93. gobby/sessions/transcripts/base.py +1 -0
  94. gobby/sessions/transcripts/claude.py +15 -5
  95. gobby/sessions/transcripts/gemini.py +100 -34
  96. gobby/skills/parser.py +30 -2
  97. gobby/storage/database.py +9 -2
  98. gobby/storage/memories.py +32 -21
  99. gobby/storage/migrations.py +174 -368
  100. gobby/storage/sessions.py +45 -7
  101. gobby/storage/skills.py +80 -7
  102. gobby/storage/tasks/_lifecycle.py +18 -3
  103. gobby/sync/memories.py +1 -1
  104. gobby/tasks/external_validator.py +1 -1
  105. gobby/tasks/validation.py +22 -20
  106. gobby/tools/summarizer.py +91 -10
  107. gobby/utils/project_context.py +2 -3
  108. gobby/utils/status.py +13 -0
  109. gobby/workflows/actions.py +221 -1217
  110. gobby/workflows/artifact_actions.py +31 -0
  111. gobby/workflows/autonomous_actions.py +11 -0
  112. gobby/workflows/context_actions.py +50 -1
  113. gobby/workflows/detection_helpers.py +38 -24
  114. gobby/workflows/enforcement/__init__.py +47 -0
  115. gobby/workflows/enforcement/blocking.py +281 -0
  116. gobby/workflows/enforcement/commit_policy.py +283 -0
  117. gobby/workflows/enforcement/handlers.py +269 -0
  118. gobby/workflows/enforcement/task_policy.py +542 -0
  119. gobby/workflows/engine.py +93 -0
  120. gobby/workflows/evaluator.py +110 -0
  121. gobby/workflows/git_utils.py +106 -0
  122. gobby/workflows/hooks.py +41 -0
  123. gobby/workflows/llm_actions.py +30 -0
  124. gobby/workflows/mcp_actions.py +20 -1
  125. gobby/workflows/memory_actions.py +91 -0
  126. gobby/workflows/safe_evaluator.py +191 -0
  127. gobby/workflows/session_actions.py +44 -0
  128. gobby/workflows/state_actions.py +60 -1
  129. gobby/workflows/stop_signal_actions.py +55 -0
  130. gobby/workflows/summary_actions.py +217 -51
  131. gobby/workflows/task_sync_actions.py +347 -0
  132. gobby/workflows/todo_actions.py +34 -1
  133. gobby/workflows/webhook_actions.py +185 -0
  134. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
  135. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
  136. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
  137. gobby/adapters/codex.py +0 -1332
  138. gobby/cli/tui.py +0 -34
  139. gobby/install/claude/commands/gobby/bug.md +0 -51
  140. gobby/install/claude/commands/gobby/chore.md +0 -51
  141. gobby/install/claude/commands/gobby/epic.md +0 -52
  142. gobby/install/claude/commands/gobby/eval.md +0 -235
  143. gobby/install/claude/commands/gobby/feat.md +0 -49
  144. gobby/install/claude/commands/gobby/nit.md +0 -52
  145. gobby/install/claude/commands/gobby/ref.md +0 -52
  146. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  147. gobby/prompts/defaults/expansion/system.md +0 -119
  148. gobby/prompts/defaults/expansion/user.md +0 -48
  149. gobby/prompts/defaults/external_validation/agent.md +0 -72
  150. gobby/prompts/defaults/external_validation/external.md +0 -63
  151. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  152. gobby/prompts/defaults/external_validation/system.md +0 -6
  153. gobby/prompts/defaults/features/import_mcp.md +0 -22
  154. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  155. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  156. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  157. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  158. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  159. gobby/prompts/defaults/features/server_description.md +0 -20
  160. gobby/prompts/defaults/features/server_description_system.md +0 -6
  161. gobby/prompts/defaults/features/task_description.md +0 -31
  162. gobby/prompts/defaults/features/task_description_system.md +0 -6
  163. gobby/prompts/defaults/features/tool_summary.md +0 -17
  164. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  165. gobby/prompts/defaults/handoff/compact.md +0 -63
  166. gobby/prompts/defaults/handoff/session_end.md +0 -57
  167. gobby/prompts/defaults/memory/extract.md +0 -61
  168. gobby/prompts/defaults/research/step.md +0 -58
  169. gobby/prompts/defaults/validation/criteria.md +0 -47
  170. gobby/prompts/defaults/validation/validate.md +0 -38
  171. gobby/storage/migrations_legacy.py +0 -1359
  172. gobby/tui/__init__.py +0 -5
  173. gobby/tui/api_client.py +0 -278
  174. gobby/tui/app.py +0 -329
  175. gobby/tui/screens/__init__.py +0 -25
  176. gobby/tui/screens/agents.py +0 -333
  177. gobby/tui/screens/chat.py +0 -450
  178. gobby/tui/screens/dashboard.py +0 -377
  179. gobby/tui/screens/memory.py +0 -305
  180. gobby/tui/screens/metrics.py +0 -231
  181. gobby/tui/screens/orchestrator.py +0 -903
  182. gobby/tui/screens/sessions.py +0 -412
  183. gobby/tui/screens/tasks.py +0 -440
  184. gobby/tui/screens/workflows.py +0 -289
  185. gobby/tui/screens/worktrees.py +0 -174
  186. gobby/tui/widgets/__init__.py +0 -21
  187. gobby/tui/widgets/chat.py +0 -210
  188. gobby/tui/widgets/conductor.py +0 -104
  189. gobby/tui/widgets/menu.py +0 -132
  190. gobby/tui/widgets/message_panel.py +0 -160
  191. gobby/tui/widgets/review_gate.py +0 -224
  192. gobby/tui/widgets/task_tree.py +0 -99
  193. gobby/tui/widgets/token_budget.py +0 -166
  194. gobby/tui/ws_client.py +0 -258
  195. gobby/workflows/task_enforcement_actions.py +0 -1343
  196. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
  197. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
  198. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
@@ -1,440 +0,0 @@
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
- reason="obsolete",
407
- )
408
- self.notify(f"Task completed: {task_id}")
409
-
410
- elif button_id == "btn-approve":
411
- await client.close_task(
412
- task_id,
413
- reason="obsolete",
414
- )
415
- self.notify(f"Task approved: {task_id}")
416
-
417
- elif button_id == "btn-reopen":
418
- await client.call_tool(
419
- "gobby-tasks",
420
- "reopen_task",
421
- {"task_id": task_id},
422
- )
423
- self.notify(f"Task reopened: {task_id}")
424
-
425
- # Refresh after action
426
- await self.refresh_data()
427
-
428
- except Exception as e:
429
- self.notify(f"Action failed: {e}", severity="error")
430
-
431
- def on_ws_event(self, event_type: str, data: dict[str, Any]) -> None:
432
- """Handle WebSocket events."""
433
- if event_type == "hook_event":
434
- hook_type = data.get("event_type", "")
435
- if "task" in hook_type.lower():
436
- self.run_worker(self.refresh_data(), name="refresh_data", exclusive=True)
437
-
438
- def activate_search(self) -> None:
439
- """Activate search mode."""
440
- self.notify("Search not yet implemented", severity="information")