agencode 0.1.0__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.
- agencli/__init__.py +5 -0
- agencli/__main__.py +9 -0
- agencli/agents/__init__.py +1 -0
- agencli/agents/editor.py +110 -0
- agencli/agents/factory.py +335 -0
- agencli/agents/management_tools.py +277 -0
- agencli/agents/prebuilt/__init__.py +1 -0
- agencli/agents/prebuilt/catalog.py +66 -0
- agencli/agents/registry.py +50 -0
- agencli/agents/runtime.py +266 -0
- agencli/agents/supervisor.py +67 -0
- agencli/cli.py +561 -0
- agencli/core/__init__.py +1 -0
- agencli/core/config.py +179 -0
- agencli/core/keystore.py +14 -0
- agencli/core/logger.py +17 -0
- agencli/core/paths.py +37 -0
- agencli/core/session.py +513 -0
- agencli/mcp/__init__.py +1 -0
- agencli/mcp/client.py +33 -0
- agencli/mcp/config.py +99 -0
- agencli/providers/__init__.py +1 -0
- agencli/providers/model.py +180 -0
- agencli/skills/__init__.py +37 -0
- agencli/skills/cli_backend.py +446 -0
- agencli/skills/loader.py +77 -0
- agencli/skills/manager.py +153 -0
- agencli/tools/__init__.py +1 -0
- agencli/tools/mcp.py +106 -0
- agencli/tui/__init__.py +1 -0
- agencli/tui/app.py +4274 -0
- agencli/tui/commands.py +86 -0
- agencli/tui/screens.py +939 -0
- agencli/tui/trace.py +334 -0
- agencli/tui/voice.py +77 -0
- agencode-0.1.0.dist-info/METADATA +44 -0
- agencode-0.1.0.dist-info/RECORD +39 -0
- agencode-0.1.0.dist-info/WHEEL +4 -0
- agencode-0.1.0.dist-info/entry_points.txt +3 -0
agencli/tui/screens.py
ADDED
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.containers import Horizontal, Vertical
|
|
8
|
+
from textual.screen import ModalScreen
|
|
9
|
+
from textual.widgets import Button, DataTable, Input, SelectionList, Static, TextArea
|
|
10
|
+
|
|
11
|
+
from agencli.agents.editor import (
|
|
12
|
+
build_agent_spec_from_editor,
|
|
13
|
+
copy_agent_spec_for_customization,
|
|
14
|
+
format_token_list,
|
|
15
|
+
partition_skill_tokens,
|
|
16
|
+
)
|
|
17
|
+
from agencli.agents.factory import AgentSpec
|
|
18
|
+
from agencli.agents.prebuilt.catalog import get_prebuilt_agents
|
|
19
|
+
from agencli.agents.registry import AgentRegistry
|
|
20
|
+
from agencli.core.config import AgenCLIConfig, load_config, set_openai_compatible_provider
|
|
21
|
+
from agencli.core.session import create_thread_id, list_chat_threads, load_chat_history
|
|
22
|
+
from agencli.mcp.config import bootstrap_default_mcp_servers, load_mcp_servers
|
|
23
|
+
from agencli.skills.manager import install_skill, list_installed_skills
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class AgentManagerResult:
|
|
28
|
+
selected_agent_name: str | None = None
|
|
29
|
+
did_change: bool = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AgentEditorScreen(ModalScreen[AgentSpec | None]):
|
|
33
|
+
CSS = """
|
|
34
|
+
AgentEditorScreen {
|
|
35
|
+
align: center middle;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#agent-editor-modal {
|
|
39
|
+
width: 94%;
|
|
40
|
+
height: 92%;
|
|
41
|
+
border: round $accent;
|
|
42
|
+
background: $surface;
|
|
43
|
+
padding: 1 2;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#agent-editor-body {
|
|
47
|
+
height: 1fr;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#agent-editor-form {
|
|
51
|
+
width: 3fr;
|
|
52
|
+
height: 1fr;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#agent-editor-side {
|
|
56
|
+
width: 2fr;
|
|
57
|
+
height: 1fr;
|
|
58
|
+
border: round $panel;
|
|
59
|
+
padding: 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#agent-system-prompt {
|
|
63
|
+
height: 12;
|
|
64
|
+
margin-top: 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#agent-editor-skills {
|
|
68
|
+
height: 1fr;
|
|
69
|
+
margin-top: 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.agent-editor-field {
|
|
73
|
+
margin-top: 1;
|
|
74
|
+
}
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
BINDINGS = [Binding("escape", "close", "Cancel")]
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
config: AgenCLIConfig,
|
|
82
|
+
spec: AgentSpec,
|
|
83
|
+
*,
|
|
84
|
+
source_label: str,
|
|
85
|
+
) -> None:
|
|
86
|
+
super().__init__()
|
|
87
|
+
self.config = config
|
|
88
|
+
self.spec = spec
|
|
89
|
+
self.source_label = source_label
|
|
90
|
+
self.installed_skills = list_installed_skills(config.skills_dir)
|
|
91
|
+
self._installed_skill_names = {skill.name for skill in self.installed_skills}
|
|
92
|
+
self._managed_skill_tokens, self._extra_skill_tokens = partition_skill_tokens(
|
|
93
|
+
spec.skills,
|
|
94
|
+
self._installed_skill_names,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def compose(self):
|
|
98
|
+
with Vertical(id="agent-editor-modal"):
|
|
99
|
+
yield Static(f"Agent Editor: {self.spec.name}")
|
|
100
|
+
yield Static(self._editor_note())
|
|
101
|
+
with Horizontal(id="agent-editor-body"):
|
|
102
|
+
with Vertical(id="agent-editor-form"):
|
|
103
|
+
yield Static("Name", classes="agent-editor-field")
|
|
104
|
+
yield Input(value=self.spec.name, id="agent-name")
|
|
105
|
+
yield Static("Model", classes="agent-editor-field")
|
|
106
|
+
yield Input(value=self.spec.model or self.config.default_model, id="agent-model")
|
|
107
|
+
yield Static("Description", classes="agent-editor-field")
|
|
108
|
+
yield Input(value=self.spec.description, id="agent-description")
|
|
109
|
+
yield Static("Workspace Override", classes="agent-editor-field")
|
|
110
|
+
yield Input(value=self.spec.workspace_dir or "", id="agent-workspace")
|
|
111
|
+
yield Static("MCP Servers (comma separated)", classes="agent-editor-field")
|
|
112
|
+
yield Input(value=format_token_list(self.spec.mcp_servers), id="agent-mcp")
|
|
113
|
+
yield Static("System Prompt", classes="agent-editor-field")
|
|
114
|
+
yield TextArea(
|
|
115
|
+
self.spec.system_prompt,
|
|
116
|
+
id="agent-system-prompt",
|
|
117
|
+
language="markdown",
|
|
118
|
+
show_line_numbers=False,
|
|
119
|
+
)
|
|
120
|
+
with Vertical(id="agent-editor-side"):
|
|
121
|
+
yield Static("Assigned Installed Skills")
|
|
122
|
+
yield SelectionList(
|
|
123
|
+
("All installed skills (`installed`)", "installed", "installed" in self._managed_skill_tokens),
|
|
124
|
+
*[
|
|
125
|
+
(
|
|
126
|
+
f"{skill.name} - {skill.description}",
|
|
127
|
+
skill.name,
|
|
128
|
+
skill.name in self._managed_skill_tokens,
|
|
129
|
+
)
|
|
130
|
+
for skill in self.installed_skills
|
|
131
|
+
],
|
|
132
|
+
id="agent-editor-skills",
|
|
133
|
+
)
|
|
134
|
+
yield Static("Extra Skill Tokens / Paths", classes="agent-editor-field")
|
|
135
|
+
yield Input(value=format_token_list(self._extra_skill_tokens), id="agent-extra-skills")
|
|
136
|
+
yield Static(self._sidebar_note(), id="agent-editor-details", classes="agent-editor-field")
|
|
137
|
+
yield Static("", id="agent-editor-status", classes="agent-editor-field")
|
|
138
|
+
with Horizontal(classes="agent-editor-field"):
|
|
139
|
+
yield Button("Save Agent", id="agent-editor-save")
|
|
140
|
+
yield Button("Cancel", id="agent-editor-close")
|
|
141
|
+
|
|
142
|
+
def action_close(self) -> None:
|
|
143
|
+
self.dismiss(None)
|
|
144
|
+
|
|
145
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
146
|
+
if event.button.id == "agent-editor-close":
|
|
147
|
+
self.dismiss(None)
|
|
148
|
+
return
|
|
149
|
+
if event.button.id != "agent-editor-save":
|
|
150
|
+
return
|
|
151
|
+
try:
|
|
152
|
+
spec = build_agent_spec_from_editor(
|
|
153
|
+
base_spec=self.spec,
|
|
154
|
+
name=self.query_one("#agent-name", Input).value,
|
|
155
|
+
model=self.query_one("#agent-model", Input).value,
|
|
156
|
+
description=self.query_one("#agent-description", Input).value,
|
|
157
|
+
system_prompt=self.query_one("#agent-system-prompt", TextArea).text,
|
|
158
|
+
workspace_dir=self.query_one("#agent-workspace", Input).value,
|
|
159
|
+
mcp_servers_raw=self.query_one("#agent-mcp", Input).value,
|
|
160
|
+
selected_skill_tokens=self.query_one("#agent-editor-skills", SelectionList).selected,
|
|
161
|
+
extra_skill_tokens_raw=self.query_one("#agent-extra-skills", Input).value,
|
|
162
|
+
)
|
|
163
|
+
except ValueError as exc:
|
|
164
|
+
self.query_one("#agent-editor-status", Static).update(str(exc))
|
|
165
|
+
return
|
|
166
|
+
self.dismiss(spec)
|
|
167
|
+
|
|
168
|
+
def _editor_note(self) -> str:
|
|
169
|
+
if self.source_label == "prebuilt":
|
|
170
|
+
return "Editing a prebuilt agent creates or updates a saved custom copy in the agent registry."
|
|
171
|
+
if self.source_label == "new":
|
|
172
|
+
return "Create a custom agent by adjusting the fields below, then save it into the local agent registry."
|
|
173
|
+
return "Edit the saved agent fields below. Subagents are preserved even though they are not directly editable here yet."
|
|
174
|
+
|
|
175
|
+
def _sidebar_note(self) -> str:
|
|
176
|
+
parts = [
|
|
177
|
+
f"Source: {self.source_label}",
|
|
178
|
+
f"Configured MCP servers: {', '.join(self.spec.mcp_servers) or '-'}",
|
|
179
|
+
f"Subagents preserved: {len(self.spec.subagents)}",
|
|
180
|
+
f"Installed skill library: {len(self.installed_skills)} available",
|
|
181
|
+
"",
|
|
182
|
+
"Tip: use installed skills here for per-agent assignment. Any non-installed tokens or skill paths can stay in the extra field.",
|
|
183
|
+
]
|
|
184
|
+
return "\n".join(parts)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class AgentManagerScreen(ModalScreen[AgentManagerResult | None]):
|
|
188
|
+
CSS = """
|
|
189
|
+
AgentManagerScreen {
|
|
190
|
+
align: center middle;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#agent-manager-modal {
|
|
194
|
+
width: 92%;
|
|
195
|
+
height: 88%;
|
|
196
|
+
border: round $accent;
|
|
197
|
+
background: $surface;
|
|
198
|
+
padding: 1 2;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#agent-manager-body {
|
|
202
|
+
height: 1fr;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#agent-manager-table {
|
|
206
|
+
width: 2fr;
|
|
207
|
+
height: 1fr;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#agent-manager-details {
|
|
211
|
+
width: 1fr;
|
|
212
|
+
height: 1fr;
|
|
213
|
+
border: round $panel;
|
|
214
|
+
padding: 1;
|
|
215
|
+
}
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
BINDINGS = [Binding("escape", "close", "Close")]
|
|
219
|
+
|
|
220
|
+
def __init__(self, config: AgenCLIConfig) -> None:
|
|
221
|
+
super().__init__()
|
|
222
|
+
self.config = config
|
|
223
|
+
self.registry = AgentRegistry(config.agents_dir)
|
|
224
|
+
self._rows: list[tuple[str, str, AgentSpec]] = []
|
|
225
|
+
self._did_change = False
|
|
226
|
+
|
|
227
|
+
def compose(self):
|
|
228
|
+
with Vertical(id="agent-manager-modal"):
|
|
229
|
+
yield Static("Agent Manager")
|
|
230
|
+
with Horizontal(id="agent-manager-body"):
|
|
231
|
+
yield DataTable(id="agent-manager-table")
|
|
232
|
+
yield Static("", id="agent-manager-details")
|
|
233
|
+
with Horizontal():
|
|
234
|
+
yield Button("Use Selected", id="agent-use")
|
|
235
|
+
yield Button("New Custom", id="agent-new")
|
|
236
|
+
yield Button("Edit Selected", id="agent-edit")
|
|
237
|
+
yield Button("Save Prebuilt", id="agent-save")
|
|
238
|
+
yield Button("Delete Saved", id="agent-delete")
|
|
239
|
+
yield Button("Close", id="agent-close")
|
|
240
|
+
|
|
241
|
+
def on_mount(self) -> None:
|
|
242
|
+
table = self.query_one("#agent-manager-table", DataTable)
|
|
243
|
+
table.cursor_type = "row"
|
|
244
|
+
self._populate()
|
|
245
|
+
|
|
246
|
+
def action_close(self) -> None:
|
|
247
|
+
self.dismiss(
|
|
248
|
+
AgentManagerResult(
|
|
249
|
+
selected_agent_name=self._selected_row_name() if self._did_change else None,
|
|
250
|
+
did_change=self._did_change,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
255
|
+
if event.button.id == "agent-close":
|
|
256
|
+
self.action_close()
|
|
257
|
+
return
|
|
258
|
+
row = self._selected_row()
|
|
259
|
+
if event.button.id == "agent-new":
|
|
260
|
+
seed_spec = copy_agent_spec_for_customization(
|
|
261
|
+
row[2] if row is not None else None,
|
|
262
|
+
existing_names=self._existing_agent_names(),
|
|
263
|
+
default_model=self.config.default_model,
|
|
264
|
+
)
|
|
265
|
+
self.app.push_screen(
|
|
266
|
+
AgentEditorScreen(self.config, seed_spec, source_label="new"),
|
|
267
|
+
self._handle_editor_result,
|
|
268
|
+
)
|
|
269
|
+
return
|
|
270
|
+
if row is None:
|
|
271
|
+
return
|
|
272
|
+
name, source, spec = row
|
|
273
|
+
if event.button.id == "agent-use":
|
|
274
|
+
self.dismiss(AgentManagerResult(selected_agent_name=name, did_change=self._did_change))
|
|
275
|
+
elif event.button.id == "agent-edit":
|
|
276
|
+
editor_spec = (
|
|
277
|
+
copy_agent_spec_for_customization(
|
|
278
|
+
spec,
|
|
279
|
+
existing_names=self._existing_agent_names(),
|
|
280
|
+
default_model=self.config.default_model,
|
|
281
|
+
)
|
|
282
|
+
if source == "prebuilt"
|
|
283
|
+
else AgentSpec.from_dict(spec.serializable_dict())
|
|
284
|
+
)
|
|
285
|
+
self.app.push_screen(
|
|
286
|
+
AgentEditorScreen(self.config, editor_spec, source_label=source),
|
|
287
|
+
self._handle_editor_result,
|
|
288
|
+
)
|
|
289
|
+
elif event.button.id == "agent-save":
|
|
290
|
+
if source == "prebuilt":
|
|
291
|
+
path = self.registry.save(spec)
|
|
292
|
+
self._did_change = True
|
|
293
|
+
self._populate(select_name=spec.name, note=f"Saved prebuilt agent to {path}.")
|
|
294
|
+
else:
|
|
295
|
+
self.query_one("#agent-manager-details", Static).update(
|
|
296
|
+
self._format_details(spec, "Saved agents are already persisted.")
|
|
297
|
+
)
|
|
298
|
+
elif event.button.id == "agent-delete":
|
|
299
|
+
if source != "saved":
|
|
300
|
+
self.query_one("#agent-manager-details", Static).update(
|
|
301
|
+
self._format_details(spec, "Only saved custom agents can be deleted.")
|
|
302
|
+
)
|
|
303
|
+
return
|
|
304
|
+
path = self.registry.delete(name)
|
|
305
|
+
self._did_change = True
|
|
306
|
+
self._populate(note=f"Deleted saved agent {path.name}.")
|
|
307
|
+
|
|
308
|
+
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
309
|
+
if event.data_table.id == "agent-manager-table":
|
|
310
|
+
self._update_details(event.cursor_row)
|
|
311
|
+
|
|
312
|
+
def _populate(self, select_name: str | None = None, note: str = "") -> None:
|
|
313
|
+
table = self.query_one("#agent-manager-table", DataTable)
|
|
314
|
+
table.clear(columns=True)
|
|
315
|
+
table.add_columns("Agent", "Source", "MCP", "Description")
|
|
316
|
+
|
|
317
|
+
prebuilt = get_prebuilt_agents(self.config.default_model)
|
|
318
|
+
self._rows = [(name, "prebuilt", spec) for name, spec in prebuilt.items()]
|
|
319
|
+
self._rows.extend((name, "saved", self.registry.load(name)) for name in self.registry.list_agents())
|
|
320
|
+
|
|
321
|
+
for _, source, spec in self._rows:
|
|
322
|
+
table.add_row(spec.name, source, ", ".join(spec.mcp_servers) or "-", spec.description or "-")
|
|
323
|
+
if not self._rows:
|
|
324
|
+
self.query_one("#agent-manager-details", Static).update(note or "No agents available.")
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
target_row = self._find_row_index(select_name, preferred_source="saved" if select_name else None)
|
|
328
|
+
table.move_cursor(row=target_row, column=0)
|
|
329
|
+
self._update_details(target_row, note=note)
|
|
330
|
+
|
|
331
|
+
def _update_details(self, row_index: int, note: str = "") -> None:
|
|
332
|
+
if row_index < 0 or row_index >= len(self._rows):
|
|
333
|
+
return
|
|
334
|
+
_, source, spec = self._rows[row_index]
|
|
335
|
+
self.query_one("#agent-manager-details", Static).update(self._format_details(spec, source=source, note=note))
|
|
336
|
+
|
|
337
|
+
def _format_details(self, spec: AgentSpec, *, source: str = "-", note: str = "") -> str:
|
|
338
|
+
parts = [
|
|
339
|
+
f"Name: {spec.name}",
|
|
340
|
+
f"Source: {source}",
|
|
341
|
+
f"Model: {spec.model}",
|
|
342
|
+
f"Workspace: {spec.workspace_dir or self.config.workspace_dir}",
|
|
343
|
+
f"MCP: {', '.join(spec.mcp_servers) or '-'}",
|
|
344
|
+
f"Skills: {', '.join(spec.skills) or '-'}",
|
|
345
|
+
f"Subagents: {len(spec.subagents)}",
|
|
346
|
+
"",
|
|
347
|
+
"System Prompt:",
|
|
348
|
+
spec.system_prompt,
|
|
349
|
+
]
|
|
350
|
+
if note:
|
|
351
|
+
parts.extend(["", note])
|
|
352
|
+
return "\n".join(parts)
|
|
353
|
+
|
|
354
|
+
def _selected_row(self) -> tuple[str, str, AgentSpec] | None:
|
|
355
|
+
if not self._rows:
|
|
356
|
+
return None
|
|
357
|
+
row_index = self.query_one("#agent-manager-table", DataTable).cursor_row
|
|
358
|
+
if row_index < 0 or row_index >= len(self._rows):
|
|
359
|
+
return None
|
|
360
|
+
return self._rows[row_index]
|
|
361
|
+
|
|
362
|
+
def _selected_row_name(self) -> str | None:
|
|
363
|
+
row = self._selected_row()
|
|
364
|
+
return row[0] if row is not None else None
|
|
365
|
+
|
|
366
|
+
def _existing_agent_names(self) -> set[str]:
|
|
367
|
+
return {name for name, _, _ in self._rows}
|
|
368
|
+
|
|
369
|
+
def _find_row_index(self, name: str | None, *, preferred_source: str | None = None) -> int:
|
|
370
|
+
if not name:
|
|
371
|
+
return 0
|
|
372
|
+
for index, (row_name, source, _) in enumerate(self._rows):
|
|
373
|
+
if row_name == name and (preferred_source is None or source == preferred_source):
|
|
374
|
+
return index
|
|
375
|
+
for index, (row_name, _, _) in enumerate(self._rows):
|
|
376
|
+
if row_name == name:
|
|
377
|
+
return index
|
|
378
|
+
return 0
|
|
379
|
+
|
|
380
|
+
def _handle_editor_result(self, spec: AgentSpec | None) -> None:
|
|
381
|
+
if spec is None:
|
|
382
|
+
return
|
|
383
|
+
path = self.registry.save(spec)
|
|
384
|
+
self._did_change = True
|
|
385
|
+
self._populate(select_name=spec.name, note=f"Saved custom agent to {path}.")
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class MCPBrowserScreen(ModalScreen[None]):
|
|
389
|
+
CSS = """
|
|
390
|
+
MCPBrowserScreen {
|
|
391
|
+
align: center middle;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#mcp-browser-modal {
|
|
395
|
+
width: 92%;
|
|
396
|
+
height: 88%;
|
|
397
|
+
border: round $accent;
|
|
398
|
+
background: $surface;
|
|
399
|
+
padding: 1 2;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
#mcp-browser-body {
|
|
403
|
+
height: 1fr;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#mcp-browser-table {
|
|
407
|
+
width: 2fr;
|
|
408
|
+
height: 1fr;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#mcp-browser-details {
|
|
412
|
+
width: 1fr;
|
|
413
|
+
height: 1fr;
|
|
414
|
+
border: round $panel;
|
|
415
|
+
padding: 1;
|
|
416
|
+
}
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
BINDINGS = [Binding("escape", "close", "Close")]
|
|
420
|
+
|
|
421
|
+
def __init__(self, config: AgenCLIConfig) -> None:
|
|
422
|
+
super().__init__()
|
|
423
|
+
self.config = config
|
|
424
|
+
self._rows: list[tuple[str, str, str]] = []
|
|
425
|
+
|
|
426
|
+
def compose(self):
|
|
427
|
+
with Vertical(id="mcp-browser-modal"):
|
|
428
|
+
yield Static("MCP Browser")
|
|
429
|
+
with Horizontal(id="mcp-browser-body"):
|
|
430
|
+
yield DataTable(id="mcp-browser-table")
|
|
431
|
+
yield Static("", id="mcp-browser-details")
|
|
432
|
+
with Horizontal():
|
|
433
|
+
yield Button("Bootstrap Remote Defaults", id="mcp-bootstrap")
|
|
434
|
+
yield Button("Refresh", id="mcp-refresh")
|
|
435
|
+
yield Button("Close", id="mcp-close")
|
|
436
|
+
|
|
437
|
+
def on_mount(self) -> None:
|
|
438
|
+
table = self.query_one("#mcp-browser-table", DataTable)
|
|
439
|
+
table.cursor_type = "row"
|
|
440
|
+
self._populate()
|
|
441
|
+
|
|
442
|
+
def action_close(self) -> None:
|
|
443
|
+
self.dismiss(None)
|
|
444
|
+
|
|
445
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
446
|
+
if event.button.id == "mcp-close":
|
|
447
|
+
self.dismiss(None)
|
|
448
|
+
return
|
|
449
|
+
if event.button.id == "mcp-bootstrap":
|
|
450
|
+
path = bootstrap_default_mcp_servers(self.config.mcp_config_path, self.config.workspace_dir)
|
|
451
|
+
self._populate(note=f"Bootstrapped remote defaults into {path}.")
|
|
452
|
+
elif event.button.id == "mcp-refresh":
|
|
453
|
+
self._populate(note="Refreshed MCP server list.")
|
|
454
|
+
|
|
455
|
+
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
456
|
+
if event.data_table.id == "mcp-browser-table":
|
|
457
|
+
self._update_details(event.cursor_row)
|
|
458
|
+
|
|
459
|
+
def _populate(self, note: str = "") -> None:
|
|
460
|
+
table = self.query_one("#mcp-browser-table", DataTable)
|
|
461
|
+
table.clear(columns=True)
|
|
462
|
+
table.add_columns("Server", "Transport", "Target")
|
|
463
|
+
self._rows = []
|
|
464
|
+
servers = load_mcp_servers(self.config.mcp_config_path)
|
|
465
|
+
for name, server in sorted(servers.items()):
|
|
466
|
+
target = server.url or " ".join([server.command or "", *server.args]).strip() or "-"
|
|
467
|
+
table.add_row(name, server.transport, target)
|
|
468
|
+
self._rows.append((name, server.transport, target))
|
|
469
|
+
if self._rows:
|
|
470
|
+
table.move_cursor(row=0, column=0)
|
|
471
|
+
self._update_details(0, note=note)
|
|
472
|
+
else:
|
|
473
|
+
self.query_one("#mcp-browser-details", Static).update(note or "No MCP servers configured.")
|
|
474
|
+
|
|
475
|
+
def _update_details(self, row_index: int, note: str = "") -> None:
|
|
476
|
+
if row_index < 0 or row_index >= len(self._rows):
|
|
477
|
+
return
|
|
478
|
+
name, transport, target = self._rows[row_index]
|
|
479
|
+
parts = [
|
|
480
|
+
f"Name: {name}",
|
|
481
|
+
f"Transport: {transport}",
|
|
482
|
+
f"Target: {target}",
|
|
483
|
+
]
|
|
484
|
+
if note:
|
|
485
|
+
parts.extend(["", note])
|
|
486
|
+
self.query_one("#mcp-browser-details", Static).update("\n".join(parts))
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class ProviderOnboardingScreen(ModalScreen[str | None]):
|
|
490
|
+
CSS = """
|
|
491
|
+
ProviderOnboardingScreen {
|
|
492
|
+
align: center middle;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
#provider-onboarding-modal {
|
|
496
|
+
width: 78;
|
|
497
|
+
max-width: 92%;
|
|
498
|
+
height: auto;
|
|
499
|
+
border: round $accent;
|
|
500
|
+
background: $surface;
|
|
501
|
+
padding: 1 2;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.provider-onboarding-field {
|
|
505
|
+
margin-top: 1;
|
|
506
|
+
}
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
def compose(self):
|
|
510
|
+
with Vertical(id="provider-onboarding-modal"):
|
|
511
|
+
yield Static("Choose A Provider")
|
|
512
|
+
yield Static(
|
|
513
|
+
"Set up your provider the first time you launch AgenCLI. "
|
|
514
|
+
"OpenAI-compatible lets you enter API key, base URL, and model directly.",
|
|
515
|
+
classes="provider-onboarding-field",
|
|
516
|
+
)
|
|
517
|
+
with Horizontal(classes="provider-onboarding-field"):
|
|
518
|
+
yield Button("OpenAI-Compatible", id="provider-openai")
|
|
519
|
+
yield Button("DeepSeek-Compatible", id="provider-deepseek")
|
|
520
|
+
yield Button("Later", id="provider-later")
|
|
521
|
+
|
|
522
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
523
|
+
if event.button.id == "provider-openai":
|
|
524
|
+
self.dismiss("openai-compatible")
|
|
525
|
+
elif event.button.id == "provider-deepseek":
|
|
526
|
+
self.dismiss("deepseek-compatible")
|
|
527
|
+
elif event.button.id == "provider-later":
|
|
528
|
+
self.dismiss(None)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class ModelConfigScreen(ModalScreen[AgenCLIConfig | None]):
|
|
532
|
+
CSS = """
|
|
533
|
+
ModelConfigScreen {
|
|
534
|
+
align: center middle;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
#model-config-modal {
|
|
538
|
+
width: 72;
|
|
539
|
+
max-width: 92%;
|
|
540
|
+
height: auto;
|
|
541
|
+
border: round $accent;
|
|
542
|
+
background: $surface;
|
|
543
|
+
padding: 1 2;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.model-field {
|
|
547
|
+
margin-top: 1;
|
|
548
|
+
}
|
|
549
|
+
"""
|
|
550
|
+
|
|
551
|
+
BINDINGS = [Binding("escape", "close", "Close")]
|
|
552
|
+
|
|
553
|
+
def __init__(
|
|
554
|
+
self,
|
|
555
|
+
config: AgenCLIConfig,
|
|
556
|
+
config_path: Path | None = None,
|
|
557
|
+
*,
|
|
558
|
+
title: str = "Model / Provider Settings",
|
|
559
|
+
intro: str = "Update provider settings. API keys can be stored in keyring.",
|
|
560
|
+
prefill: dict[str, str] | None = None,
|
|
561
|
+
show_close: bool = True,
|
|
562
|
+
) -> None:
|
|
563
|
+
super().__init__()
|
|
564
|
+
self.config = config
|
|
565
|
+
self.config_path = config_path
|
|
566
|
+
self.title = title
|
|
567
|
+
self.intro = intro
|
|
568
|
+
self.prefill = prefill or {}
|
|
569
|
+
self.show_close = show_close
|
|
570
|
+
provider = config.openai_compatible
|
|
571
|
+
self._provider_name = self.prefill.get("provider_name", provider.provider_name)
|
|
572
|
+
self._base_url = self.prefill.get("base_url", provider.base_url)
|
|
573
|
+
self._model = self.prefill.get("model", provider.model or self.config.default_model)
|
|
574
|
+
self._model_kind = self.prefill.get("model_kind", provider.model_kind)
|
|
575
|
+
self._api_key_env = self.prefill.get("api_key_env", provider.api_key_env)
|
|
576
|
+
|
|
577
|
+
def compose(self):
|
|
578
|
+
with Vertical(id="model-config-modal"):
|
|
579
|
+
yield Static(self.title)
|
|
580
|
+
yield Static(self.intro, classes="model-field")
|
|
581
|
+
yield Static("Provider Name", classes="model-field")
|
|
582
|
+
yield Input(value=self._provider_name, id="provider-name")
|
|
583
|
+
yield Static("Base URL", classes="model-field")
|
|
584
|
+
yield Input(value=self._base_url, id="base-url")
|
|
585
|
+
yield Static("Model", classes="model-field")
|
|
586
|
+
yield Input(value=self._model, id="model-name")
|
|
587
|
+
yield Static("Model Kind", classes="model-field")
|
|
588
|
+
yield Input(value=self._model_kind, id="model-kind")
|
|
589
|
+
yield Static("API Key Env", classes="model-field")
|
|
590
|
+
yield Input(value=self._api_key_env, id="api-key-env")
|
|
591
|
+
yield Static("API Key (stored in keyring when provided)", classes="model-field")
|
|
592
|
+
yield Input(password=True, id="api-key")
|
|
593
|
+
yield Static("", id="model-config-status", classes="model-field")
|
|
594
|
+
with Horizontal(classes="model-field"):
|
|
595
|
+
yield Button("Save", id="model-save")
|
|
596
|
+
if self.show_close:
|
|
597
|
+
yield Button("Close", id="model-close")
|
|
598
|
+
|
|
599
|
+
def action_close(self) -> None:
|
|
600
|
+
self.dismiss(None)
|
|
601
|
+
|
|
602
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
603
|
+
if event.button.id == "model-close":
|
|
604
|
+
self.dismiss(None)
|
|
605
|
+
return
|
|
606
|
+
if event.button.id != "model-save":
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
updated = set_openai_compatible_provider(
|
|
610
|
+
config_path=self.config_path,
|
|
611
|
+
provider_name=self.query_one("#provider-name", Input).value.strip() or None,
|
|
612
|
+
base_url=self.query_one("#base-url", Input).value.strip() or None,
|
|
613
|
+
model=self.query_one("#model-name", Input).value.strip() or None,
|
|
614
|
+
model_kind=self.query_one("#model-kind", Input).value.strip() or None,
|
|
615
|
+
api_key=self.query_one("#api-key", Input).value.strip() or None,
|
|
616
|
+
api_key_env=self.query_one("#api-key-env", Input).value.strip() or None,
|
|
617
|
+
set_as_default=True,
|
|
618
|
+
)
|
|
619
|
+
self.query_one("#model-config-status", Static).update("Saved provider settings.")
|
|
620
|
+
self.dismiss(load_config(self.config_path) if self.config_path is not None else updated)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class SkillBrowserScreen(ModalScreen[None]):
|
|
624
|
+
CSS = """
|
|
625
|
+
SkillBrowserScreen {
|
|
626
|
+
align: center middle;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
#skill-browser-modal {
|
|
630
|
+
width: 92%;
|
|
631
|
+
height: 88%;
|
|
632
|
+
border: round $accent;
|
|
633
|
+
background: $surface;
|
|
634
|
+
padding: 1 2;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
#skill-browser-body {
|
|
638
|
+
height: 1fr;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
#skill-browser-table {
|
|
642
|
+
width: 2fr;
|
|
643
|
+
height: 1fr;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
#skill-browser-details {
|
|
647
|
+
width: 1fr;
|
|
648
|
+
height: 1fr;
|
|
649
|
+
border: round $panel;
|
|
650
|
+
padding: 1;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
#skill-source-input {
|
|
654
|
+
margin-top: 1;
|
|
655
|
+
}
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
BINDINGS = [Binding("escape", "close", "Close")]
|
|
659
|
+
|
|
660
|
+
def __init__(self, config: AgenCLIConfig) -> None:
|
|
661
|
+
super().__init__()
|
|
662
|
+
self.config = config
|
|
663
|
+
self._skills = []
|
|
664
|
+
|
|
665
|
+
def compose(self):
|
|
666
|
+
with Vertical(id="skill-browser-modal"):
|
|
667
|
+
yield Static("Skills")
|
|
668
|
+
with Horizontal(id="skill-browser-body"):
|
|
669
|
+
yield DataTable(id="skill-browser-table")
|
|
670
|
+
yield Static("", id="skill-browser-details")
|
|
671
|
+
yield Static("Install from local directory or SKILL.md path")
|
|
672
|
+
yield Input(placeholder="D:\\path\\to\\skill or D:\\path\\to\\SKILL.md", id="skill-source-input")
|
|
673
|
+
with Horizontal():
|
|
674
|
+
yield Button("Install Local", id="skill-install")
|
|
675
|
+
yield Button("Refresh", id="skill-refresh")
|
|
676
|
+
yield Button("Close", id="skill-close")
|
|
677
|
+
|
|
678
|
+
def on_mount(self) -> None:
|
|
679
|
+
table = self.query_one("#skill-browser-table", DataTable)
|
|
680
|
+
table.cursor_type = "row"
|
|
681
|
+
self._populate()
|
|
682
|
+
|
|
683
|
+
def action_close(self) -> None:
|
|
684
|
+
self.dismiss(None)
|
|
685
|
+
|
|
686
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
687
|
+
if event.button.id == "skill-close":
|
|
688
|
+
self.dismiss(None)
|
|
689
|
+
return
|
|
690
|
+
if event.button.id == "skill-refresh":
|
|
691
|
+
self._populate(note="Refreshed installed skills.")
|
|
692
|
+
return
|
|
693
|
+
if event.button.id != "skill-install":
|
|
694
|
+
return
|
|
695
|
+
|
|
696
|
+
source = self.query_one("#skill-source-input", Input).value.strip()
|
|
697
|
+
if not source:
|
|
698
|
+
self.query_one("#skill-browser-details", Static).update("Enter a local skill path first.")
|
|
699
|
+
return
|
|
700
|
+
try:
|
|
701
|
+
installed = install_skill(source, self.config.skills_dir, overwrite=False)
|
|
702
|
+
except Exception as exc:
|
|
703
|
+
self.query_one("#skill-browser-details", Static).update(f"Install failed: {exc}")
|
|
704
|
+
return
|
|
705
|
+
|
|
706
|
+
self.query_one("#skill-source-input", Input).value = ""
|
|
707
|
+
self._populate(note=f"Installed `{installed.name}`.")
|
|
708
|
+
|
|
709
|
+
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
710
|
+
if event.data_table.id == "skill-browser-table":
|
|
711
|
+
self._update_details(event.cursor_row)
|
|
712
|
+
|
|
713
|
+
def _populate(self, note: str = "") -> None:
|
|
714
|
+
table = self.query_one("#skill-browser-table", DataTable)
|
|
715
|
+
table.clear(columns=True)
|
|
716
|
+
table.add_columns("Skill", "Tools", "Description")
|
|
717
|
+
self._skills = list_installed_skills(self.config.skills_dir)
|
|
718
|
+
for skill in self._skills:
|
|
719
|
+
table.add_row(skill.name, ", ".join(skill.allowed_tools) or "-", skill.description)
|
|
720
|
+
if self._skills:
|
|
721
|
+
table.move_cursor(row=0, column=0)
|
|
722
|
+
self._update_details(0, note=note)
|
|
723
|
+
else:
|
|
724
|
+
self.query_one("#skill-browser-details", Static).update(note or "No local skills installed.")
|
|
725
|
+
|
|
726
|
+
def _update_details(self, row_index: int, note: str = "") -> None:
|
|
727
|
+
if row_index < 0 or row_index >= len(self._skills):
|
|
728
|
+
return
|
|
729
|
+
skill = self._skills[row_index]
|
|
730
|
+
parts = [
|
|
731
|
+
f"Name: {skill.name}",
|
|
732
|
+
f"Path: {skill.path}",
|
|
733
|
+
f"Token: {skill.name}",
|
|
734
|
+
"All Installed Token: installed",
|
|
735
|
+
f"Allowed Tools: {', '.join(skill.allowed_tools) or '-'}",
|
|
736
|
+
f"License: {skill.license or '-'}",
|
|
737
|
+
f"Compatibility: {skill.compatibility or '-'}",
|
|
738
|
+
"",
|
|
739
|
+
skill.description,
|
|
740
|
+
]
|
|
741
|
+
if note:
|
|
742
|
+
parts.extend(["", note])
|
|
743
|
+
self.query_one("#skill-browser-details", Static).update("\n".join(parts))
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@dataclass(slots=True)
|
|
747
|
+
class ThreadSelection:
|
|
748
|
+
agent_name: str
|
|
749
|
+
thread_id: str
|
|
750
|
+
created_new: bool = False
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
class SessionBrowserScreen(ModalScreen[ThreadSelection | None]):
|
|
754
|
+
CSS = """
|
|
755
|
+
SessionBrowserScreen {
|
|
756
|
+
align: center middle;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
#session-browser-modal {
|
|
760
|
+
width: 92%;
|
|
761
|
+
height: 88%;
|
|
762
|
+
border: round $accent;
|
|
763
|
+
background: $surface;
|
|
764
|
+
padding: 1 2;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
#session-browser-body {
|
|
768
|
+
height: 1fr;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
#session-browser-table {
|
|
772
|
+
width: 2fr;
|
|
773
|
+
height: 1fr;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
#session-browser-details {
|
|
777
|
+
width: 1fr;
|
|
778
|
+
height: 1fr;
|
|
779
|
+
border: round $panel;
|
|
780
|
+
padding: 1;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
#session-filter-input {
|
|
784
|
+
margin-top: 1;
|
|
785
|
+
}
|
|
786
|
+
"""
|
|
787
|
+
|
|
788
|
+
BINDINGS = [Binding("escape", "close", "Close")]
|
|
789
|
+
|
|
790
|
+
def __init__(
|
|
791
|
+
self,
|
|
792
|
+
config: AgenCLIConfig,
|
|
793
|
+
*,
|
|
794
|
+
preferred_agent_name: str | None = None,
|
|
795
|
+
active_thread_id: str | None = None,
|
|
796
|
+
) -> None:
|
|
797
|
+
super().__init__()
|
|
798
|
+
self.config = config
|
|
799
|
+
self.preferred_agent_name = preferred_agent_name
|
|
800
|
+
self.active_thread_id = active_thread_id
|
|
801
|
+
self._threads = []
|
|
802
|
+
|
|
803
|
+
def compose(self):
|
|
804
|
+
with Vertical(id="session-browser-modal"):
|
|
805
|
+
yield Static("Session Browser")
|
|
806
|
+
yield Static("Search agent name, thread ID, or latest content")
|
|
807
|
+
yield Input(placeholder="Filter persisted threads", id="session-filter-input")
|
|
808
|
+
with Horizontal(id="session-browser-body"):
|
|
809
|
+
yield DataTable(id="session-browser-table")
|
|
810
|
+
yield Static("", id="session-browser-details")
|
|
811
|
+
with Horizontal():
|
|
812
|
+
yield Button("Use Selected Thread", id="session-jump")
|
|
813
|
+
yield Button("New Thread", id="session-new")
|
|
814
|
+
yield Button("Refresh", id="session-refresh")
|
|
815
|
+
yield Button("Close", id="session-close")
|
|
816
|
+
|
|
817
|
+
def on_mount(self) -> None:
|
|
818
|
+
table = self.query_one("#session-browser-table", DataTable)
|
|
819
|
+
table.cursor_type = "row"
|
|
820
|
+
self._populate()
|
|
821
|
+
|
|
822
|
+
def action_close(self) -> None:
|
|
823
|
+
self.dismiss(None)
|
|
824
|
+
|
|
825
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
826
|
+
if event.button.id == "session-close":
|
|
827
|
+
self.dismiss(None)
|
|
828
|
+
return
|
|
829
|
+
if event.button.id == "session-refresh":
|
|
830
|
+
self._populate(note="Refreshed persisted threads.")
|
|
831
|
+
return
|
|
832
|
+
if event.button.id == "session-new":
|
|
833
|
+
self._create_thread_selection()
|
|
834
|
+
return
|
|
835
|
+
if event.button.id != "session-jump":
|
|
836
|
+
return
|
|
837
|
+
row_index = self.query_one("#session-browser-table", DataTable).cursor_row
|
|
838
|
+
if row_index < 0 or row_index >= len(self._threads):
|
|
839
|
+
return
|
|
840
|
+
thread = self._threads[row_index]
|
|
841
|
+
if not thread.agent_name or not thread.thread_id:
|
|
842
|
+
self.query_one("#session-browser-details", Static).update(
|
|
843
|
+
"This persisted row is missing agent or thread metadata and cannot be reopened directly."
|
|
844
|
+
)
|
|
845
|
+
return
|
|
846
|
+
self.dismiss(ThreadSelection(agent_name=thread.agent_name, thread_id=thread.thread_id))
|
|
847
|
+
|
|
848
|
+
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
849
|
+
if event.data_table.id == "session-browser-table":
|
|
850
|
+
self._update_details(event.cursor_row)
|
|
851
|
+
|
|
852
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
853
|
+
if event.input.id == "session-filter-input":
|
|
854
|
+
self._populate()
|
|
855
|
+
|
|
856
|
+
def _populate(self, note: str = "") -> None:
|
|
857
|
+
table = self.query_one("#session-browser-table", DataTable)
|
|
858
|
+
table.clear(columns=True)
|
|
859
|
+
table.add_columns("Agent", "Thread", "Turns", "Last Role", "Last Updated")
|
|
860
|
+
query = self.query_one("#session-filter-input", Input).value.strip()
|
|
861
|
+
self._threads = list_chat_threads(self.config.sessions_dir, limit=100, query=query or None)
|
|
862
|
+
for thread in self._threads:
|
|
863
|
+
table.add_row(
|
|
864
|
+
thread.agent_name or "-",
|
|
865
|
+
thread.thread_id or "-",
|
|
866
|
+
str(thread.turn_count),
|
|
867
|
+
thread.last_role,
|
|
868
|
+
thread.last_created_at,
|
|
869
|
+
)
|
|
870
|
+
if self._threads:
|
|
871
|
+
target_row = self._preferred_row_index()
|
|
872
|
+
table.move_cursor(row=target_row, column=0)
|
|
873
|
+
self._update_details(target_row, note=note)
|
|
874
|
+
else:
|
|
875
|
+
message = note or "No persisted threads match this filter yet."
|
|
876
|
+
if self.preferred_agent_name:
|
|
877
|
+
message = f"{message}\n\nUse `New Thread` to start a fresh thread for {self.preferred_agent_name}."
|
|
878
|
+
self.query_one("#session-browser-details", Static).update(message)
|
|
879
|
+
|
|
880
|
+
def _update_details(self, row_index: int, note: str = "") -> None:
|
|
881
|
+
if row_index < 0 or row_index >= len(self._threads):
|
|
882
|
+
return
|
|
883
|
+
thread = self._threads[row_index]
|
|
884
|
+
turns = load_chat_history(
|
|
885
|
+
self.config.sessions_dir,
|
|
886
|
+
limit=8,
|
|
887
|
+
agent_name=thread.agent_name,
|
|
888
|
+
thread_id=thread.thread_id,
|
|
889
|
+
)
|
|
890
|
+
lines = [
|
|
891
|
+
f"Agent: {thread.agent_name or '-'}",
|
|
892
|
+
f"Thread: {thread.thread_id or '-'}",
|
|
893
|
+
f"Turns: {thread.turn_count}",
|
|
894
|
+
f"Last Updated: {thread.last_created_at}",
|
|
895
|
+
]
|
|
896
|
+
if thread.thread_id == self.active_thread_id:
|
|
897
|
+
lines.append("Current Selection: active")
|
|
898
|
+
lines.extend(["", "Recent Turns:"])
|
|
899
|
+
if not turns:
|
|
900
|
+
lines.append("No turns found.")
|
|
901
|
+
else:
|
|
902
|
+
for turn in turns:
|
|
903
|
+
preview = turn.content if len(turn.content) <= 120 else f"{turn.content[:117]}..."
|
|
904
|
+
lines.append(f"{turn.role}: {preview}")
|
|
905
|
+
if note:
|
|
906
|
+
lines.extend(["", note])
|
|
907
|
+
self.query_one("#session-browser-details", Static).update("\n".join(lines))
|
|
908
|
+
|
|
909
|
+
def _create_thread_selection(self) -> None:
|
|
910
|
+
agent_name = self._selected_agent_name() or self.preferred_agent_name
|
|
911
|
+
if not agent_name:
|
|
912
|
+
self.query_one("#session-browser-details", Static).update(
|
|
913
|
+
"Select a persisted thread first, or open the browser while an agent is selected."
|
|
914
|
+
)
|
|
915
|
+
return
|
|
916
|
+
self.dismiss(
|
|
917
|
+
ThreadSelection(
|
|
918
|
+
agent_name=agent_name,
|
|
919
|
+
thread_id=create_thread_id(agent_name),
|
|
920
|
+
created_new=True,
|
|
921
|
+
)
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
def _selected_agent_name(self) -> str | None:
|
|
925
|
+
row_index = self.query_one("#session-browser-table", DataTable).cursor_row
|
|
926
|
+
if row_index < 0 or row_index >= len(self._threads):
|
|
927
|
+
return None
|
|
928
|
+
return self._threads[row_index].agent_name
|
|
929
|
+
|
|
930
|
+
def _preferred_row_index(self) -> int:
|
|
931
|
+
if self.active_thread_id is not None:
|
|
932
|
+
for index, thread in enumerate(self._threads):
|
|
933
|
+
if thread.thread_id == self.active_thread_id:
|
|
934
|
+
return index
|
|
935
|
+
if self.preferred_agent_name is not None:
|
|
936
|
+
for index, thread in enumerate(self._threads):
|
|
937
|
+
if thread.agent_name == self.preferred_agent_name:
|
|
938
|
+
return index
|
|
939
|
+
return 0
|