dlab-cli 0.1.2__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.
- dlab/__init__.py +6 -0
- dlab/cli.py +1075 -0
- dlab/config.py +190 -0
- dlab/create_dpack.py +1096 -0
- dlab/create_dpack_wizard.py +1471 -0
- dlab/create_parallel_agent_wizard.py +582 -0
- dlab/data/__init__.py +0 -0
- dlab/data/models.json +1793 -0
- dlab/docker.py +592 -0
- dlab/js/__init__.py +0 -0
- dlab/js/parallel-agents.ts +418 -0
- dlab/local.py +269 -0
- dlab/model_fallback.py +360 -0
- dlab/parallel_tool.py +18 -0
- dlab/session.py +389 -0
- dlab/timeline.py +684 -0
- dlab/tui/__init__.py +9 -0
- dlab/tui/app.py +664 -0
- dlab/tui/log_watcher.py +208 -0
- dlab/tui/models.py +438 -0
- dlab/tui/widgets/__init__.py +18 -0
- dlab/tui/widgets/agent_list.py +170 -0
- dlab/tui/widgets/artifacts_pane.py +618 -0
- dlab/tui/widgets/log_view.py +505 -0
- dlab/tui/widgets/search_popup.py +151 -0
- dlab/tui/widgets/status_bar.py +106 -0
- dlab_cli-0.1.2.dist-info/METADATA +237 -0
- dlab_cli-0.1.2.dist-info/RECORD +32 -0
- dlab_cli-0.1.2.dist-info/WHEEL +5 -0
- dlab_cli-0.1.2.dist-info/entry_points.txt +2 -0
- dlab_cli-0.1.2.dist-info/licenses/LICENSE +201 -0
- dlab_cli-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Textual TUI wizard for creating a parallel agent configuration.
|
|
3
|
+
|
|
4
|
+
Single-screen wizard that collects configuration and writes a YAML file
|
|
5
|
+
to opencode/parallel_agents/{name}.yaml.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from textual.app import App, ComposeResult
|
|
13
|
+
from textual.binding import Binding
|
|
14
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
15
|
+
from textual.screen import Screen
|
|
16
|
+
from textual.widgets import (
|
|
17
|
+
Button,
|
|
18
|
+
Checkbox,
|
|
19
|
+
Input,
|
|
20
|
+
Label,
|
|
21
|
+
OptionList,
|
|
22
|
+
Static,
|
|
23
|
+
TextArea,
|
|
24
|
+
)
|
|
25
|
+
from textual.widgets.option_list import Option
|
|
26
|
+
|
|
27
|
+
from textual import work
|
|
28
|
+
|
|
29
|
+
from dlab.config import load_dpack_config
|
|
30
|
+
from dlab.create_dpack import filter_models, get_model_list
|
|
31
|
+
from dlab.create_dpack_wizard import DpackCheckbox, FormScroll
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Default prompt templates
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
DEFAULT_SUFFIX_PROMPT: str = """\
|
|
39
|
+
When you complete your task, write a summary.md file with:
|
|
40
|
+
|
|
41
|
+
## Approach
|
|
42
|
+
Describe your approach and key decisions.
|
|
43
|
+
|
|
44
|
+
## Results
|
|
45
|
+
Present your findings and outputs.
|
|
46
|
+
|
|
47
|
+
## Recommendations
|
|
48
|
+
Provide actionable recommendations based on your analysis."""
|
|
49
|
+
|
|
50
|
+
DEFAULT_SUMMARIZER_PROMPT: str = """\
|
|
51
|
+
Read all summary.md files from the parallel instances.
|
|
52
|
+
Create a consolidated comparison highlighting:
|
|
53
|
+
- Key differences in approaches
|
|
54
|
+
- Agreement and disagreement across instances
|
|
55
|
+
- Overall recommendations based on all results"""
|
|
56
|
+
|
|
57
|
+
WORKER_AGENT_MD: str = """---
|
|
58
|
+
description: {description}
|
|
59
|
+
mode: subagent
|
|
60
|
+
tools:
|
|
61
|
+
read: true
|
|
62
|
+
edit: true
|
|
63
|
+
bash: true
|
|
64
|
+
parallel-agents: false
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
You are a worker agent. Complete the task described in the prompt.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
_NEW_AGENT_ID: str = "_new_agent"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Main screen
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
class ParallelAgentScreen(Screen):
|
|
78
|
+
"""Single-screen wizard for parallel agent configuration."""
|
|
79
|
+
|
|
80
|
+
CSS = """
|
|
81
|
+
ParallelAgentScreen {
|
|
82
|
+
align: left bottom;
|
|
83
|
+
}
|
|
84
|
+
#pa-container {
|
|
85
|
+
width: 66%;
|
|
86
|
+
height: auto;
|
|
87
|
+
max-height: 90%;
|
|
88
|
+
padding: 1 2;
|
|
89
|
+
}
|
|
90
|
+
.field-label {
|
|
91
|
+
margin-top: 1;
|
|
92
|
+
}
|
|
93
|
+
.field-hint {
|
|
94
|
+
color: $text-muted;
|
|
95
|
+
text-style: italic;
|
|
96
|
+
text-wrap: wrap;
|
|
97
|
+
width: 1fr;
|
|
98
|
+
}
|
|
99
|
+
.section-divider {
|
|
100
|
+
margin-top: 1;
|
|
101
|
+
color: $accent;
|
|
102
|
+
}
|
|
103
|
+
.error-label {
|
|
104
|
+
color: $error;
|
|
105
|
+
}
|
|
106
|
+
.success-label {
|
|
107
|
+
color: $success;
|
|
108
|
+
}
|
|
109
|
+
.nav-bar {
|
|
110
|
+
margin-top: 1;
|
|
111
|
+
height: 1;
|
|
112
|
+
align: right middle;
|
|
113
|
+
}
|
|
114
|
+
#suffix-textarea, #summarizer-textarea {
|
|
115
|
+
height: 8;
|
|
116
|
+
border-left: tall $accent;
|
|
117
|
+
}
|
|
118
|
+
#retries-input {
|
|
119
|
+
margin-left: 4;
|
|
120
|
+
}
|
|
121
|
+
#new-agent-name-input {
|
|
122
|
+
margin-left: 4;
|
|
123
|
+
}
|
|
124
|
+
#model-selection-group {
|
|
125
|
+
display: none;
|
|
126
|
+
}
|
|
127
|
+
#summarizer-model-results {
|
|
128
|
+
height: auto;
|
|
129
|
+
max-height: 12;
|
|
130
|
+
}
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self) -> None:
|
|
134
|
+
super().__init__()
|
|
135
|
+
self._existing_agents: list[str] = []
|
|
136
|
+
self._default_model: str = ""
|
|
137
|
+
self._models: list[str] = get_model_list()
|
|
138
|
+
self._programmatic_fill: bool = False
|
|
139
|
+
|
|
140
|
+
def compose(self) -> ComposeResult:
|
|
141
|
+
with FormScroll(id="pa-container"):
|
|
142
|
+
yield Label("[b]Create Parallel Agent[/b]", classes="field-label")
|
|
143
|
+
yield Label("Tab / Shift+Tab to navigate | Ctrl+Q to quit", classes="field-hint")
|
|
144
|
+
|
|
145
|
+
# Agent selection or creation
|
|
146
|
+
yield Label("[b]Agent[/b]", classes="section-divider")
|
|
147
|
+
yield Label(
|
|
148
|
+
"Select an existing agent to run in parallel, or create a new one. "
|
|
149
|
+
"The parallel config YAML will share the agent's name.",
|
|
150
|
+
classes="field-hint",
|
|
151
|
+
)
|
|
152
|
+
with Vertical(classes="selection-group"):
|
|
153
|
+
yield OptionList(id="agent-select")
|
|
154
|
+
yield Label("Tab to continue", classes="option-hint")
|
|
155
|
+
|
|
156
|
+
# New agent name (only shown when "New agent..." selected)
|
|
157
|
+
yield Label("New agent name", classes="field-label", id="new-agent-label")
|
|
158
|
+
yield Label("Alphanumeric, hyphens, underscores", classes="field-hint")
|
|
159
|
+
yield Input(id="pa-name-input", placeholder="my-worker")
|
|
160
|
+
yield Label("", id="pa-name-error", classes="error-label")
|
|
161
|
+
yield Label(
|
|
162
|
+
"A worker agent .md will be created in opencode/agents/",
|
|
163
|
+
classes="field-hint",
|
|
164
|
+
id="new-agent-hint",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Description
|
|
168
|
+
yield Label("[b]Description[/b]", classes="section-divider")
|
|
169
|
+
yield Label(
|
|
170
|
+
"Optional. Used in the YAML config for documentation purposes.",
|
|
171
|
+
classes="field-hint",
|
|
172
|
+
)
|
|
173
|
+
yield Input(id="pa-desc-input", placeholder="(default: Parallel agent: {name})")
|
|
174
|
+
|
|
175
|
+
# Timeout
|
|
176
|
+
yield Label("[b]Instance Timeout[/b]", classes="section-divider")
|
|
177
|
+
yield Label(
|
|
178
|
+
"Maximum time each parallel instance is allowed to run before being stopped.",
|
|
179
|
+
classes="field-hint",
|
|
180
|
+
)
|
|
181
|
+
yield Input(id="timeout-input", value="60", placeholder="60")
|
|
182
|
+
|
|
183
|
+
# Failure behavior
|
|
184
|
+
yield Label("[b]Instance Failure Behavior[/b]", classes="section-divider")
|
|
185
|
+
yield Label(
|
|
186
|
+
"What happens when one of the parallel instances fails or times out.",
|
|
187
|
+
classes="field-hint",
|
|
188
|
+
)
|
|
189
|
+
with Vertical(classes="cb-group"):
|
|
190
|
+
yield DpackCheckbox(
|
|
191
|
+
"Continue — other instances keep running",
|
|
192
|
+
value=True,
|
|
193
|
+
id="fail-continue",
|
|
194
|
+
)
|
|
195
|
+
yield DpackCheckbox(
|
|
196
|
+
"Fail fast — stop all instances on first failure",
|
|
197
|
+
value=False,
|
|
198
|
+
id="fail-fast",
|
|
199
|
+
)
|
|
200
|
+
yield DpackCheckbox(
|
|
201
|
+
"Retry — re-run failed instances",
|
|
202
|
+
value=False,
|
|
203
|
+
id="fail-retry",
|
|
204
|
+
)
|
|
205
|
+
yield Label("Max retries", classes="field-label", id="retries-label")
|
|
206
|
+
yield Input(id="retries-input", value="2", placeholder="2")
|
|
207
|
+
|
|
208
|
+
# Suffix prompt
|
|
209
|
+
yield Label("[b]Suffix Prompt[/b]", classes="section-divider")
|
|
210
|
+
yield Label(
|
|
211
|
+
"Appended to each worker's prompt. Typically instructs writing summary.md.",
|
|
212
|
+
classes="field-hint",
|
|
213
|
+
)
|
|
214
|
+
yield TextArea(DEFAULT_SUFFIX_PROMPT, id="suffix-textarea")
|
|
215
|
+
|
|
216
|
+
# Consolidator prompt
|
|
217
|
+
yield Label("[b]Consolidator Prompt[/b]", classes="section-divider")
|
|
218
|
+
yield Label(
|
|
219
|
+
"Given to the consolidator agent that reads all summary.md files and "
|
|
220
|
+
"produces a combined report. Only runs when 3 or more instances are spawned.",
|
|
221
|
+
classes="field-hint",
|
|
222
|
+
)
|
|
223
|
+
yield TextArea(DEFAULT_SUMMARIZER_PROMPT, id="summarizer-textarea")
|
|
224
|
+
|
|
225
|
+
# Consolidator model
|
|
226
|
+
yield Label("[b]Consolidator Model[/b]", classes="section-divider")
|
|
227
|
+
yield Label(
|
|
228
|
+
"Model used by the consolidator agent. Defaults to the decision-pack's default model.",
|
|
229
|
+
classes="field-hint",
|
|
230
|
+
)
|
|
231
|
+
yield Input(id="summarizer-model-input", placeholder="(uses decision-pack default)")
|
|
232
|
+
with Vertical(classes="selection-group", id="model-selection-group"):
|
|
233
|
+
yield OptionList(id="summarizer-model-results")
|
|
234
|
+
yield Label("Tab to continue", classes="option-hint")
|
|
235
|
+
|
|
236
|
+
# Errors + nav
|
|
237
|
+
yield Label("", id="pa-error", classes="error-label")
|
|
238
|
+
with Horizontal(classes="nav-bar"):
|
|
239
|
+
yield Button("Create", id="create-btn", variant="success")
|
|
240
|
+
|
|
241
|
+
def on_mount(self) -> None:
|
|
242
|
+
app: CreateParallelAgentApp = self.app # type: ignore[assignment]
|
|
243
|
+
self._default_model = app.dpack_config.get("default_model", "")
|
|
244
|
+
self._programmatic_fill = True
|
|
245
|
+
self.query_one("#summarizer-model-input", Input).value = self._default_model
|
|
246
|
+
self._refresh_models()
|
|
247
|
+
|
|
248
|
+
# Discover existing agents (exclude default agent)
|
|
249
|
+
config_dir: Path = Path(app.dpack_config["config_dir"])
|
|
250
|
+
agents_dir: Path = config_dir / "opencode" / "agents"
|
|
251
|
+
default_agent: str = ""
|
|
252
|
+
|
|
253
|
+
opencode_json: Path = config_dir / "opencode" / "opencode.json"
|
|
254
|
+
if opencode_json.exists():
|
|
255
|
+
try:
|
|
256
|
+
data: dict[str, Any] = json.loads(opencode_json.read_text())
|
|
257
|
+
default_agent = data.get("default_agent", "")
|
|
258
|
+
except (json.JSONDecodeError, OSError):
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
# Find which agents already have a parallel config
|
|
262
|
+
pa_dir: Path = config_dir / "opencode" / "parallel_agents"
|
|
263
|
+
existing_yamls: set[str] = set()
|
|
264
|
+
if pa_dir.exists():
|
|
265
|
+
for yaml_file in pa_dir.glob("*.yaml"):
|
|
266
|
+
existing_yamls.add(yaml_file.stem)
|
|
267
|
+
|
|
268
|
+
if agents_dir.exists():
|
|
269
|
+
for md_file in sorted(agents_dir.glob("*.md")):
|
|
270
|
+
agent_name: str = md_file.stem
|
|
271
|
+
if agent_name != default_agent:
|
|
272
|
+
self._existing_agents.append(agent_name)
|
|
273
|
+
|
|
274
|
+
# Build agent selection list
|
|
275
|
+
ol: OptionList = self.query_one("#agent-select", OptionList)
|
|
276
|
+
first_available: int = -1
|
|
277
|
+
if self._existing_agents:
|
|
278
|
+
for i, agent in enumerate(self._existing_agents):
|
|
279
|
+
already_configured: bool = agent in existing_yamls
|
|
280
|
+
label: str = f"{agent} [dim](already configured)[/dim]" if already_configured else agent
|
|
281
|
+
ol.add_option(Option(label, id=agent, disabled=already_configured))
|
|
282
|
+
if not already_configured and first_available < 0:
|
|
283
|
+
first_available = i
|
|
284
|
+
ol.add_option(Option("New agent...", id=_NEW_AGENT_ID))
|
|
285
|
+
|
|
286
|
+
# Default: first available existing agent, or "New agent..."
|
|
287
|
+
if first_available >= 0:
|
|
288
|
+
ol.highlighted = first_available
|
|
289
|
+
self._show_new_agent_fields(False)
|
|
290
|
+
else:
|
|
291
|
+
ol.highlighted = ol.option_count - 1
|
|
292
|
+
self._show_new_agent_fields(True)
|
|
293
|
+
|
|
294
|
+
# Hide retries by default (continue is selected)
|
|
295
|
+
self._update_retries_visibility()
|
|
296
|
+
|
|
297
|
+
self.query_one("#pa-name-input", Input).focus()
|
|
298
|
+
|
|
299
|
+
def _show_new_agent_fields(self, show: bool) -> None:
|
|
300
|
+
self.query_one("#new-agent-label", Label).display = show
|
|
301
|
+
self.query_one("#pa-name-input", Input).display = show
|
|
302
|
+
self.query_one("#new-agent-hint", Label).display = show
|
|
303
|
+
# pa-error stays visible always (used for all validation errors)
|
|
304
|
+
|
|
305
|
+
def _update_retries_visibility(self) -> None:
|
|
306
|
+
retry: bool = self.query_one("#fail-retry", Checkbox).value
|
|
307
|
+
self.query_one("#retries-input", Input).display = retry
|
|
308
|
+
self.query_one("#retries-label", Label).display = retry
|
|
309
|
+
|
|
310
|
+
@work(thread=True)
|
|
311
|
+
def _refresh_models(self) -> None:
|
|
312
|
+
"""Fetch models from API in background and refresh the list."""
|
|
313
|
+
from dlab.create_dpack import fetch_models_from_api, save_model_cache, _model_sort_key
|
|
314
|
+
try:
|
|
315
|
+
data: dict[str, Any] = fetch_models_from_api()
|
|
316
|
+
save_model_cache(data)
|
|
317
|
+
new_models: list[str] = sorted(
|
|
318
|
+
set(self._models) | set(data.get("models", [])),
|
|
319
|
+
key=_model_sort_key,
|
|
320
|
+
)
|
|
321
|
+
if new_models != self._models:
|
|
322
|
+
self._models = new_models
|
|
323
|
+
self.app.call_from_thread(self._rebuild_model_options, "")
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
def _rebuild_model_options(self, query: str) -> None:
|
|
328
|
+
"""Rebuild the model OptionList filtered by query."""
|
|
329
|
+
matches: list[str] = filter_models(query, self._models) if query else self._models
|
|
330
|
+
ol: OptionList = self.query_one("#summarizer-model-results", OptionList)
|
|
331
|
+
ol.clear_options()
|
|
332
|
+
for m in matches:
|
|
333
|
+
ol.add_option(Option(m, id=m))
|
|
334
|
+
|
|
335
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
336
|
+
if event.input.id != "summarizer-model-input":
|
|
337
|
+
return
|
|
338
|
+
if self._programmatic_fill:
|
|
339
|
+
self._programmatic_fill = False
|
|
340
|
+
return
|
|
341
|
+
group: Vertical = self.query_one("#model-selection-group", Vertical)
|
|
342
|
+
group.display = True
|
|
343
|
+
self._rebuild_model_options(event.value.strip())
|
|
344
|
+
|
|
345
|
+
def _get_selected_agent(self) -> str | None:
|
|
346
|
+
"""Return the currently highlighted agent ID, or None."""
|
|
347
|
+
ol: OptionList = self.query_one("#agent-select", OptionList)
|
|
348
|
+
idx: int | None = ol.highlighted
|
|
349
|
+
if idx is None:
|
|
350
|
+
return None
|
|
351
|
+
return str(ol.get_option_at_index(idx).id)
|
|
352
|
+
|
|
353
|
+
def on_option_list_option_highlighted(self, event: OptionList.OptionHighlighted) -> None:
|
|
354
|
+
if event.option_list.id != "agent-select":
|
|
355
|
+
return
|
|
356
|
+
is_new: bool = str(event.option.id) == _NEW_AGENT_ID
|
|
357
|
+
self._show_new_agent_fields(is_new)
|
|
358
|
+
|
|
359
|
+
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
360
|
+
if event.option_list.id != "summarizer-model-results":
|
|
361
|
+
return
|
|
362
|
+
self._programmatic_fill = True
|
|
363
|
+
self.query_one("#summarizer-model-input", Input).value = str(event.option.prompt)
|
|
364
|
+
self.query_one("#model-selection-group", Vertical).display = False
|
|
365
|
+
self.query_one("#create-btn", Button).focus()
|
|
366
|
+
|
|
367
|
+
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
|
368
|
+
# Radio group for failure behavior
|
|
369
|
+
radio_ids: list[str] = ["fail-continue", "fail-fast", "fail-retry"]
|
|
370
|
+
if event.checkbox.id in radio_ids and event.value:
|
|
371
|
+
for rid in radio_ids:
|
|
372
|
+
if rid != event.checkbox.id:
|
|
373
|
+
self.query_one(f"#{rid}", Checkbox).value = False
|
|
374
|
+
self._update_retries_visibility()
|
|
375
|
+
|
|
376
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
377
|
+
if event.button.id != "create-btn":
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
error_label: Label = self.query_one("#pa-error", Label)
|
|
381
|
+
name_error_label: Label = self.query_one("#pa-name-error", Label)
|
|
382
|
+
name_error_label.update("")
|
|
383
|
+
|
|
384
|
+
# Determine agent name
|
|
385
|
+
selected_agent: str | None = self._get_selected_agent()
|
|
386
|
+
create_new_agent: bool = selected_agent == _NEW_AGENT_ID
|
|
387
|
+
agent_name: str
|
|
388
|
+
|
|
389
|
+
if create_new_agent:
|
|
390
|
+
agent_name = self.query_one("#pa-name-input", Input).value.strip()
|
|
391
|
+
if not agent_name:
|
|
392
|
+
name_error_label.update("[red]Agent name is required[/red]")
|
|
393
|
+
return
|
|
394
|
+
if not agent_name.replace("_", "").replace("-", "").isalnum():
|
|
395
|
+
name_error_label.update("[red]Must be alphanumeric (with - or _)[/red]")
|
|
396
|
+
return
|
|
397
|
+
else:
|
|
398
|
+
agent_name = selected_agent or ""
|
|
399
|
+
if not agent_name:
|
|
400
|
+
error_label.update("[red]Select an agent[/red]")
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
error_label.update("")
|
|
404
|
+
|
|
405
|
+
# Validate timeout
|
|
406
|
+
timeout_str: str = self.query_one("#timeout-input", Input).value.strip()
|
|
407
|
+
try:
|
|
408
|
+
timeout: int = int(timeout_str) if timeout_str else 60
|
|
409
|
+
if timeout <= 0:
|
|
410
|
+
raise ValueError
|
|
411
|
+
except ValueError:
|
|
412
|
+
error_label.update("[red]Timeout must be a positive number[/red]")
|
|
413
|
+
return
|
|
414
|
+
|
|
415
|
+
# Gather values
|
|
416
|
+
description: str = self.query_one("#pa-desc-input", Input).value.strip()
|
|
417
|
+
if not description:
|
|
418
|
+
description = f"Parallel agent: {agent_name}"
|
|
419
|
+
|
|
420
|
+
failure_behavior: str = "continue"
|
|
421
|
+
if self.query_one("#fail-fast", Checkbox).value:
|
|
422
|
+
failure_behavior = "fail_fast"
|
|
423
|
+
elif self.query_one("#fail-retry", Checkbox).value:
|
|
424
|
+
failure_behavior = "retry"
|
|
425
|
+
|
|
426
|
+
max_retries: int = 2
|
|
427
|
+
if failure_behavior == "retry":
|
|
428
|
+
retries_str: str = self.query_one("#retries-input", Input).value.strip()
|
|
429
|
+
try:
|
|
430
|
+
max_retries = int(retries_str) if retries_str else 2
|
|
431
|
+
except ValueError:
|
|
432
|
+
max_retries = 2
|
|
433
|
+
|
|
434
|
+
suffix_prompt: str = self.query_one("#suffix-textarea", TextArea).text
|
|
435
|
+
summarizer_prompt: str = self.query_one("#summarizer-textarea", TextArea).text
|
|
436
|
+
summarizer_model: str = self.query_one("#summarizer-model-input", Input).value.strip()
|
|
437
|
+
if not summarizer_model:
|
|
438
|
+
summarizer_model = self._default_model
|
|
439
|
+
|
|
440
|
+
# Build YAML
|
|
441
|
+
yaml_lines: list[str] = [
|
|
442
|
+
f"name: {agent_name}",
|
|
443
|
+
f'description: "{description}"',
|
|
444
|
+
f"timeout_minutes: {timeout}",
|
|
445
|
+
f"failure_behavior: {failure_behavior}",
|
|
446
|
+
]
|
|
447
|
+
if failure_behavior == "retry":
|
|
448
|
+
yaml_lines.append(f"max_retries: {max_retries}")
|
|
449
|
+
|
|
450
|
+
yaml_lines.append("")
|
|
451
|
+
yaml_lines.append("subagent_suffix_prompt: |")
|
|
452
|
+
for line in suffix_prompt.split("\n"):
|
|
453
|
+
yaml_lines.append(f" {line}")
|
|
454
|
+
|
|
455
|
+
yaml_lines.append("")
|
|
456
|
+
yaml_lines.append("summarizer_prompt: |")
|
|
457
|
+
for line in summarizer_prompt.split("\n"):
|
|
458
|
+
yaml_lines.append(f" {line}")
|
|
459
|
+
|
|
460
|
+
yaml_lines.append("")
|
|
461
|
+
yaml_lines.append(f'summarizer_model: "{summarizer_model}"')
|
|
462
|
+
yaml_lines.append("")
|
|
463
|
+
|
|
464
|
+
yaml_content: str = "\n".join(yaml_lines)
|
|
465
|
+
|
|
466
|
+
# Write files
|
|
467
|
+
app: CreateParallelAgentApp = self.app # type: ignore[assignment]
|
|
468
|
+
config_dir: Path = Path(app.dpack_config["config_dir"])
|
|
469
|
+
pa_dir: Path = config_dir / "opencode" / "parallel_agents"
|
|
470
|
+
pa_dir.mkdir(parents=True, exist_ok=True)
|
|
471
|
+
|
|
472
|
+
yaml_file: Path = pa_dir / f"{agent_name}.yaml"
|
|
473
|
+
if yaml_file.exists():
|
|
474
|
+
error_label.update(f"[red]{agent_name}.yaml already exists[/red]")
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
yaml_file.write_text(yaml_content)
|
|
478
|
+
created_files: list[str] = [str(yaml_file.relative_to(config_dir))]
|
|
479
|
+
|
|
480
|
+
# Create agent .md if "New agent..." was selected
|
|
481
|
+
if create_new_agent:
|
|
482
|
+
agents_dir: Path = config_dir / "opencode" / "agents"
|
|
483
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
484
|
+
agent_file: Path = agents_dir / f"{agent_name}.md"
|
|
485
|
+
if not agent_file.exists():
|
|
486
|
+
agent_file.write_text(
|
|
487
|
+
WORKER_AGENT_MD.format(description=description)
|
|
488
|
+
)
|
|
489
|
+
created_files.append(str(agent_file.relative_to(config_dir)))
|
|
490
|
+
|
|
491
|
+
# Exit app with created files list
|
|
492
|
+
app.created_files = created_files
|
|
493
|
+
app.exit()
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# ---------------------------------------------------------------------------
|
|
497
|
+
# Main App
|
|
498
|
+
# ---------------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
class CreateParallelAgentApp(App):
|
|
501
|
+
"""TUI wizard for creating a parallel agent configuration."""
|
|
502
|
+
|
|
503
|
+
TITLE = "dlab create-parallel-agent"
|
|
504
|
+
BINDINGS = [
|
|
505
|
+
Binding("ctrl+q", "quit", "Quit", priority=True),
|
|
506
|
+
Binding("down", "focus_next", show=False),
|
|
507
|
+
Binding("up", "focus_previous", show=False),
|
|
508
|
+
]
|
|
509
|
+
theme = "monokai"
|
|
510
|
+
|
|
511
|
+
CSS = """
|
|
512
|
+
/* --- Flat widget overrides ------------------------------------------ */
|
|
513
|
+
Input {
|
|
514
|
+
border: none;
|
|
515
|
+
border-left: tall $accent;
|
|
516
|
+
height: 1;
|
|
517
|
+
padding: 0 1;
|
|
518
|
+
background: $surface;
|
|
519
|
+
&:focus {
|
|
520
|
+
border: none;
|
|
521
|
+
border-left: tall $accent;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
Button {
|
|
525
|
+
min-width: 10;
|
|
526
|
+
border: none;
|
|
527
|
+
background: $surface;
|
|
528
|
+
&:hover {
|
|
529
|
+
background: $primary;
|
|
530
|
+
}
|
|
531
|
+
&.-success {
|
|
532
|
+
background: $success-muted;
|
|
533
|
+
border: none;
|
|
534
|
+
&:hover { background: $success; border: none; }
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
OptionList {
|
|
538
|
+
border: none;
|
|
539
|
+
border-left: tall $accent;
|
|
540
|
+
background: $surface;
|
|
541
|
+
scrollbar-size: 1 1;
|
|
542
|
+
}
|
|
543
|
+
Checkbox {
|
|
544
|
+
border: none;
|
|
545
|
+
background: transparent;
|
|
546
|
+
height: 1;
|
|
547
|
+
padding: 0;
|
|
548
|
+
}
|
|
549
|
+
Checkbox > .toggle--button {
|
|
550
|
+
color: $text-muted;
|
|
551
|
+
}
|
|
552
|
+
Checkbox.-on > .toggle--button {
|
|
553
|
+
color: $success;
|
|
554
|
+
}
|
|
555
|
+
.cb-group {
|
|
556
|
+
border-left: tall $accent;
|
|
557
|
+
background: $surface;
|
|
558
|
+
padding: 0 1;
|
|
559
|
+
height: auto;
|
|
560
|
+
}
|
|
561
|
+
.selection-group {
|
|
562
|
+
height: auto;
|
|
563
|
+
}
|
|
564
|
+
.option-hint {
|
|
565
|
+
display: none;
|
|
566
|
+
color: $text-muted;
|
|
567
|
+
text-style: italic;
|
|
568
|
+
height: 1;
|
|
569
|
+
}
|
|
570
|
+
.selection-group:focus-within .option-hint {
|
|
571
|
+
display: block;
|
|
572
|
+
}
|
|
573
|
+
"""
|
|
574
|
+
|
|
575
|
+
def __init__(self, dpack: str = ".") -> None:
|
|
576
|
+
super().__init__()
|
|
577
|
+
self.dpack: str = dpack
|
|
578
|
+
self.dpack_config: dict[str, Any] = load_dpack_config(dpack)
|
|
579
|
+
self.created_files: list[str] = []
|
|
580
|
+
|
|
581
|
+
def on_mount(self) -> None:
|
|
582
|
+
self.push_screen(ParallelAgentScreen())
|
dlab/data/__init__.py
ADDED
|
File without changes
|