codee-agent 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.
- codee/.gitignore +2 -0
- codee/__init__.py +0 -0
- codee/admin.py +1672 -0
- codee/admin_api.py +61 -0
- codee/admin_cli.py +86 -0
- codee/admin_service.py +1005 -0
- codee/executor.py +381 -0
- codee/init_cli.py +82 -0
- codee/lib/__init__.py +0 -0
- codee/lib/cron_describe.py +33 -0
- codee/lib/runs_db.py +195 -0
- codee/lib/test_runs_db.py +199 -0
- codee/lib/test_trigger_cron_skills.py +485 -0
- codee/lib/test_trigger_issue_skills.py +93 -0
- codee/lib/trigger_aws_sqs_skills.py +224 -0
- codee/lib/trigger_cron_skills.py +364 -0
- codee/lib/trigger_email_skills.py +225 -0
- codee/lib/trigger_issue_skills.py +107 -0
- codee/mail_server.py +45 -0
- codee/start_cli.py +98 -0
- codee/templates/AGENTS.md +42 -0
- codee/templates/CLAUDE.md +1 -0
- codee/templates/skills/aws-sqs-alarm-response/SKILL.md +23 -0
- codee/templates/skills/cron-research-5xx-errors/SKILL.md +17 -0
- codee/templates/skills/story-code-reviewer/SKILL.md +29 -0
- codee/templates/skills/story-developer/SKILL.md +26 -0
- codee/templates/skills/story-planner/SKILL.md +35 -0
- codee/templates/skills/story-planner/assets/readme-template.md +43 -0
- codee/templates/skills/story-qa/SKILL.md +28 -0
- codee/templates/skills/task-developer/SKILL.md +25 -0
- codee/templates/skills/task-qa/SKILL.md +26 -0
- codee/test_admin_api.py +64 -0
- codee/test_admin_cli.py +63 -0
- codee/test_admin_service.py +897 -0
- codee/test_executor.py +190 -0
- codee/test_init_cli.py +131 -0
- codee/test_memory_index.py +31 -0
- codee/test_start_cli.py +164 -0
- codee/workflow_graph.py +83 -0
- codee_admin/__init__.py +1 -0
- codee_admin/codee_admin.py +4 -0
- codee_agent-0.1.0.dist-info/METADATA +66 -0
- codee_agent-0.1.0.dist-info/RECORD +69 -0
- codee_agent-0.1.0.dist-info/WHEEL +4 -0
- codee_agent-0.1.0.dist-info/entry_points.txt +6 -0
- codee_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- codee_agent_abstract/__init__.py +0 -0
- codee_agent_abstract/provider.py +56 -0
- codee_agent_claude_code/__init__.py +0 -0
- codee_agent_claude_code/provider.py +90 -0
- codee_agent_github_copilot/__init__.py +0 -0
- codee_agent_github_copilot/provider.py +253 -0
- codee_agent_github_copilot/test.py +176 -0
- codee_database/__init__.py +0 -0
- codee_database/database.py +13 -0
- codee_database/oauth_tokens.py +148 -0
- codee_main_context/__init__.py +0 -0
- codee_main_context/context.py +127 -0
- codee_main_context/logging.py +111 -0
- codee_main_context/test_logging.py +90 -0
- codee_tasks_abstract/__init__.py +0 -0
- codee_tasks_abstract/provider.py +58 -0
- codee_tasks_azure_devops/__init__.py +0 -0
- codee_tasks_azure_devops/oauth.py +346 -0
- codee_tasks_azure_devops/provider.py +207 -0
- codee_tasks_azure_devops/test.py +462 -0
- codee_tasks_jira/__init__.py +0 -0
- codee_tasks_jira/provider.py +162 -0
- codee_tasks_jira/test.py +75 -0
codee/admin.py
ADDED
|
@@ -0,0 +1,1672 @@
|
|
|
1
|
+
"""Reflex UI for managing Codee."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import reflex as rx
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from codee.admin_api import api_app
|
|
11
|
+
from codee.admin_service import AGENTS_FILE, AdminService, ISSUE_TYPES, SKILL_TYPES
|
|
12
|
+
from codee.workflow_graph import workflow_graph
|
|
13
|
+
|
|
14
|
+
SERVICE = AdminService()
|
|
15
|
+
|
|
16
|
+
RUNS_PAGE_SIZE = 20
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _save_toast(persisted: bool, pushed: bool, message: str) -> Any:
|
|
20
|
+
"""Warn instead of erroring when the change landed on disk but not in Git."""
|
|
21
|
+
if not persisted:
|
|
22
|
+
return rx.toast.error(message)
|
|
23
|
+
return rx.toast.success(message) if pushed else rx.toast.warning(message)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SkillSummary(BaseModel):
|
|
27
|
+
slug: str
|
|
28
|
+
name: str
|
|
29
|
+
description: str
|
|
30
|
+
type: str
|
|
31
|
+
issue_status: str = ""
|
|
32
|
+
issue_type: str = ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ModelOption(BaseModel):
|
|
36
|
+
"""One entry in the skill editor's model picker: code plus friendly name."""
|
|
37
|
+
|
|
38
|
+
id: str
|
|
39
|
+
name: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MemoryEntry(BaseModel):
|
|
43
|
+
title: str
|
|
44
|
+
file: str
|
|
45
|
+
hook: str
|
|
46
|
+
lineno: int
|
|
47
|
+
raw: str
|
|
48
|
+
matched: bool
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ActiveJob(BaseModel):
|
|
52
|
+
message: str
|
|
53
|
+
elapsed_label: str
|
|
54
|
+
viewer_url: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RunRecord(BaseModel):
|
|
58
|
+
skill_name: str
|
|
59
|
+
trigger_type: str
|
|
60
|
+
status: str
|
|
61
|
+
error: str
|
|
62
|
+
started_at: str
|
|
63
|
+
message: str
|
|
64
|
+
preview: str
|
|
65
|
+
viewer_url: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AdminState(rx.State):
|
|
69
|
+
skills: list[SkillSummary] = []
|
|
70
|
+
skill_query: str = ""
|
|
71
|
+
skill_filter: str = "All"
|
|
72
|
+
new_skill_name: str = ""
|
|
73
|
+
selected_skill: str = ""
|
|
74
|
+
skill_name: str = ""
|
|
75
|
+
skill_description: str = ""
|
|
76
|
+
skill_model: str = ""
|
|
77
|
+
skill_type: str = "knowledge"
|
|
78
|
+
skill_cron: str = "0 0 * * *"
|
|
79
|
+
skill_email: str = ""
|
|
80
|
+
skill_sqs: str = ""
|
|
81
|
+
skill_issue_status: str = ""
|
|
82
|
+
skill_issue_type: str = "story"
|
|
83
|
+
skill_body: str = ""
|
|
84
|
+
skill_extra: str = ""
|
|
85
|
+
skill_extra_enabled: bool = False
|
|
86
|
+
|
|
87
|
+
agent_models: list[ModelOption] = []
|
|
88
|
+
model_query: str = ""
|
|
89
|
+
models_loading: bool = False
|
|
90
|
+
|
|
91
|
+
editing_agents: bool = False
|
|
92
|
+
agents_content: str = ""
|
|
93
|
+
|
|
94
|
+
memories: list[MemoryEntry] = []
|
|
95
|
+
selected_memory: str = ""
|
|
96
|
+
memory_content: str = ""
|
|
97
|
+
|
|
98
|
+
active_jobs: list[ActiveJob] = []
|
|
99
|
+
total_runs: int = 0
|
|
100
|
+
last_24h_runs: int = 0
|
|
101
|
+
hourly_runs: list[dict[str, Any]] = []
|
|
102
|
+
dashboard_polling: bool = False
|
|
103
|
+
|
|
104
|
+
runs: list[RunRecord] = []
|
|
105
|
+
runs_has_more: bool = False
|
|
106
|
+
runs_loading: bool = False
|
|
107
|
+
session_viewer: str = SERVICE.session_viewer
|
|
108
|
+
|
|
109
|
+
story_workflow_nodes: list[dict[str, Any]] = []
|
|
110
|
+
story_workflow_edges: list[dict[str, Any]] = []
|
|
111
|
+
story_workflow_warnings: list[str] = []
|
|
112
|
+
task_workflow_nodes: list[dict[str, Any]] = []
|
|
113
|
+
task_workflow_edges: list[dict[str, Any]] = []
|
|
114
|
+
task_workflow_warnings: list[str] = []
|
|
115
|
+
workflow_error: str = ""
|
|
116
|
+
workflow_loading: bool = False
|
|
117
|
+
edge_menu_skills: list[str] = []
|
|
118
|
+
edge_menu_left: str = "0px"
|
|
119
|
+
edge_menu_top: str = "0px"
|
|
120
|
+
|
|
121
|
+
tasks_provider: str = "jira"
|
|
122
|
+
coding_agent: str = "claude_code"
|
|
123
|
+
max_parallel_agents: str = "3"
|
|
124
|
+
jira_base_url: str = ""
|
|
125
|
+
jira_account_email: str = ""
|
|
126
|
+
jira_api_token: str = ""
|
|
127
|
+
jira_project: str = ""
|
|
128
|
+
azure_organization_url: str = ""
|
|
129
|
+
azure_project: str = ""
|
|
130
|
+
azure_tenant_id: str = ""
|
|
131
|
+
azure_client_id: str = ""
|
|
132
|
+
azure_client_secret: str = ""
|
|
133
|
+
azure_connected: bool = False
|
|
134
|
+
azure_account: str = ""
|
|
135
|
+
azure_expires_label: str = ""
|
|
136
|
+
azure_redirect_uri: str = ""
|
|
137
|
+
|
|
138
|
+
@rx.var
|
|
139
|
+
def filtered_skills(self) -> list[SkillSummary]:
|
|
140
|
+
query = self.skill_query.strip().lower()
|
|
141
|
+
return [
|
|
142
|
+
skill for skill in self.skills
|
|
143
|
+
if (not query or query in f"{skill.name} {skill.description}".lower())
|
|
144
|
+
and (self.skill_filter == "All" or skill.type == self.skill_filter)
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
@rx.var
|
|
148
|
+
def agents_card_visible(self) -> bool:
|
|
149
|
+
"""AGENTS.md is not a skill, so only an unfiltered search can hide it."""
|
|
150
|
+
return (self.skill_filter == "All"
|
|
151
|
+
and self.skill_query.strip().lower() in AGENTS_FILE.lower())
|
|
152
|
+
|
|
153
|
+
@rx.var
|
|
154
|
+
def cron_description(self) -> str:
|
|
155
|
+
return SERVICE.describe_cron(self.skill_cron)
|
|
156
|
+
|
|
157
|
+
@rx.var
|
|
158
|
+
def filtered_models(self) -> list[ModelOption]:
|
|
159
|
+
query = self.model_query.strip().lower()
|
|
160
|
+
return [
|
|
161
|
+
model for model in self.agent_models
|
|
162
|
+
if not query or query in f"{model.name} {model.id}".lower()
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
@rx.var
|
|
166
|
+
def skill_model_label(self) -> str:
|
|
167
|
+
"""Friendly name of the selected model, falling back to the raw code."""
|
|
168
|
+
if not self.skill_model:
|
|
169
|
+
return "Agent default"
|
|
170
|
+
for model in self.agent_models:
|
|
171
|
+
if model.id == self.skill_model:
|
|
172
|
+
return model.name
|
|
173
|
+
return self.skill_model
|
|
174
|
+
|
|
175
|
+
@rx.var
|
|
176
|
+
def custom_model_query(self) -> str:
|
|
177
|
+
"""The search text when it names no known model, so it can be used as-is."""
|
|
178
|
+
query = self.model_query.strip()
|
|
179
|
+
if not query or any(model.id == query for model in self.agent_models):
|
|
180
|
+
return ""
|
|
181
|
+
return query
|
|
182
|
+
|
|
183
|
+
@rx.var
|
|
184
|
+
def active_route(self) -> str:
|
|
185
|
+
return self.router.url.path.rstrip("/") or "/"
|
|
186
|
+
|
|
187
|
+
def set_skill_query(self, value: str) -> None:
|
|
188
|
+
self.skill_query = value
|
|
189
|
+
|
|
190
|
+
def set_skill_filter(self, value: str) -> None:
|
|
191
|
+
self.skill_filter = value
|
|
192
|
+
|
|
193
|
+
def set_new_skill_name(self, value: str) -> None:
|
|
194
|
+
self.new_skill_name = value
|
|
195
|
+
|
|
196
|
+
def set_skill_name(self, value: str) -> None:
|
|
197
|
+
self.skill_name = value
|
|
198
|
+
|
|
199
|
+
def set_skill_description(self, value: str) -> None:
|
|
200
|
+
self.skill_description = value
|
|
201
|
+
|
|
202
|
+
def set_skill_type(self, value: str) -> None:
|
|
203
|
+
self.skill_type = value
|
|
204
|
+
|
|
205
|
+
def set_model_query(self, value: str) -> None:
|
|
206
|
+
self.model_query = value
|
|
207
|
+
|
|
208
|
+
def choose_model(self, model_id: str) -> None:
|
|
209
|
+
"""Pick a model from the list, or use whatever the user typed."""
|
|
210
|
+
self.skill_model = model_id.strip()
|
|
211
|
+
self.model_query = ""
|
|
212
|
+
|
|
213
|
+
@rx.event(background=True)
|
|
214
|
+
async def load_agent_models(self) -> None:
|
|
215
|
+
"""Fetch the configured agent's catalog off the event loop.
|
|
216
|
+
|
|
217
|
+
Asking an agent can mean spawning its CLI, so this runs in the
|
|
218
|
+
background while the skill list renders; the picker still accepts a
|
|
219
|
+
hand-typed model code if the list never arrives.
|
|
220
|
+
"""
|
|
221
|
+
async with self:
|
|
222
|
+
if self.models_loading:
|
|
223
|
+
return
|
|
224
|
+
self.models_loading = True
|
|
225
|
+
try:
|
|
226
|
+
models = await asyncio.to_thread(SERVICE.list_agent_models)
|
|
227
|
+
except Exception:
|
|
228
|
+
models = []
|
|
229
|
+
async with self:
|
|
230
|
+
self.agent_models = [ModelOption(**model) for model in models]
|
|
231
|
+
self.models_loading = False
|
|
232
|
+
|
|
233
|
+
def set_skill_cron(self, value: str) -> None:
|
|
234
|
+
self.skill_cron = value
|
|
235
|
+
|
|
236
|
+
def set_skill_email(self, value: str) -> None:
|
|
237
|
+
self.skill_email = value
|
|
238
|
+
|
|
239
|
+
def set_skill_sqs(self, value: str) -> None:
|
|
240
|
+
self.skill_sqs = value
|
|
241
|
+
|
|
242
|
+
def set_skill_issue_status(self, value: str) -> None:
|
|
243
|
+
self.skill_issue_status = value
|
|
244
|
+
|
|
245
|
+
def set_skill_issue_type(self, value: str) -> None:
|
|
246
|
+
self.skill_issue_type = value
|
|
247
|
+
|
|
248
|
+
def set_skill_body(self, value: str) -> None:
|
|
249
|
+
self.skill_body = value
|
|
250
|
+
|
|
251
|
+
def set_skill_extra(self, value: str) -> None:
|
|
252
|
+
self.skill_extra = value
|
|
253
|
+
|
|
254
|
+
def set_skill_extra_enabled(self, value: bool) -> None:
|
|
255
|
+
self.skill_extra_enabled = value
|
|
256
|
+
|
|
257
|
+
def load_skills(self) -> None:
|
|
258
|
+
self.skills = [SkillSummary(**skill)
|
|
259
|
+
for skill in SERVICE.list_skills()]
|
|
260
|
+
|
|
261
|
+
def create_skill(self) -> Any:
|
|
262
|
+
saved, pushed, message, slug = SERVICE.create_skill(
|
|
263
|
+
self.new_skill_name)
|
|
264
|
+
if saved:
|
|
265
|
+
self.new_skill_name = ""
|
|
266
|
+
self.load_skills()
|
|
267
|
+
self.edit_skill(slug)
|
|
268
|
+
return _save_toast(saved, pushed, message)
|
|
269
|
+
|
|
270
|
+
def edit_skill(self, slug: str) -> None:
|
|
271
|
+
skill = SERVICE.load_skill(slug)
|
|
272
|
+
self.editing_agents = False
|
|
273
|
+
self.selected_skill = skill["slug"]
|
|
274
|
+
self.skill_name = skill["name"]
|
|
275
|
+
self.skill_description = skill["description"]
|
|
276
|
+
self.skill_model = skill["model"]
|
|
277
|
+
self.model_query = ""
|
|
278
|
+
self.skill_type = skill["type"]
|
|
279
|
+
self.skill_cron = skill["cron"]
|
|
280
|
+
self.skill_email = skill["email"]
|
|
281
|
+
self.skill_sqs = skill["sqs"]
|
|
282
|
+
self.skill_issue_status = skill["issue_status"]
|
|
283
|
+
self.skill_issue_type = skill["issue_type"] or "story"
|
|
284
|
+
self.skill_body = skill["body"]
|
|
285
|
+
self.skill_extra = skill["extra"]
|
|
286
|
+
self.skill_extra_enabled = bool(skill["extra"])
|
|
287
|
+
|
|
288
|
+
def close_skill(self) -> None:
|
|
289
|
+
self.selected_skill = ""
|
|
290
|
+
|
|
291
|
+
def save_skill(self) -> Any:
|
|
292
|
+
saved, pushed, message, slug = SERVICE.save_skill({
|
|
293
|
+
"slug": self.selected_skill,
|
|
294
|
+
"name": self.skill_name,
|
|
295
|
+
"description": self.skill_description,
|
|
296
|
+
"model": self.skill_model,
|
|
297
|
+
"type": self.skill_type,
|
|
298
|
+
"cron": self.skill_cron,
|
|
299
|
+
"email": self.skill_email,
|
|
300
|
+
"sqs": self.skill_sqs,
|
|
301
|
+
"issue_status": self.skill_issue_status,
|
|
302
|
+
"issue_type": self.skill_issue_type,
|
|
303
|
+
"body": self.skill_body,
|
|
304
|
+
# Unchecking the box drops the fields from the frontmatter, while
|
|
305
|
+
# the text stays around in case the box goes back on.
|
|
306
|
+
"extra": self.skill_extra if self.skill_extra_enabled else "",
|
|
307
|
+
})
|
|
308
|
+
if saved:
|
|
309
|
+
self.selected_skill = slug
|
|
310
|
+
self.load_skills()
|
|
311
|
+
return _save_toast(saved, pushed, message)
|
|
312
|
+
|
|
313
|
+
def delete_skill(self) -> Any:
|
|
314
|
+
deleted, pushed, message = SERVICE.delete_skill(self.selected_skill)
|
|
315
|
+
if deleted:
|
|
316
|
+
self.selected_skill = ""
|
|
317
|
+
self.load_skills()
|
|
318
|
+
return _save_toast(deleted, pushed, message)
|
|
319
|
+
|
|
320
|
+
def force_run_skill(self) -> Any:
|
|
321
|
+
SERVICE.force_run_skill(self.selected_skill)
|
|
322
|
+
return rx.toast.success("Queued to run on the next trigger tick")
|
|
323
|
+
|
|
324
|
+
def edit_agents(self) -> None:
|
|
325
|
+
self.selected_skill = ""
|
|
326
|
+
self.agents_content = SERVICE.load_agents()
|
|
327
|
+
self.editing_agents = True
|
|
328
|
+
|
|
329
|
+
def set_agents_content(self, value: str) -> None:
|
|
330
|
+
self.agents_content = value
|
|
331
|
+
|
|
332
|
+
def close_agents(self) -> None:
|
|
333
|
+
self.editing_agents = False
|
|
334
|
+
|
|
335
|
+
def save_agents(self) -> Any:
|
|
336
|
+
return _save_toast(*SERVICE.save_agents(self.agents_content))
|
|
337
|
+
|
|
338
|
+
def load_memories(self) -> None:
|
|
339
|
+
self.memories = [MemoryEntry(**entry)
|
|
340
|
+
for entry in SERVICE.list_memories()]
|
|
341
|
+
|
|
342
|
+
def edit_memory(self, filename: str) -> None:
|
|
343
|
+
self.selected_memory = filename
|
|
344
|
+
self.memory_content = SERVICE.load_memory(filename)
|
|
345
|
+
|
|
346
|
+
def set_memory_content(self, value: str) -> None:
|
|
347
|
+
self.memory_content = value
|
|
348
|
+
|
|
349
|
+
def close_memory(self) -> None:
|
|
350
|
+
self.selected_memory = ""
|
|
351
|
+
|
|
352
|
+
def save_memory(self) -> Any:
|
|
353
|
+
saved, pushed, message = SERVICE.save_memory(
|
|
354
|
+
self.selected_memory, self.memory_content)
|
|
355
|
+
if saved:
|
|
356
|
+
self.load_memories()
|
|
357
|
+
return _save_toast(saved, pushed, message)
|
|
358
|
+
|
|
359
|
+
def delete_memory(self, filename: str, raw: str) -> Any:
|
|
360
|
+
deleted, pushed, message = SERVICE.delete_memory(filename, raw)
|
|
361
|
+
if filename == self.selected_memory:
|
|
362
|
+
self.selected_memory = ""
|
|
363
|
+
self.load_memories()
|
|
364
|
+
return _save_toast(deleted, pushed, message)
|
|
365
|
+
|
|
366
|
+
def _refresh_dashboard(self) -> None:
|
|
367
|
+
dashboard = SERVICE.dashboard()
|
|
368
|
+
self.active_jobs = [
|
|
369
|
+
ActiveJob(
|
|
370
|
+
message=(job.get("message") or "(no prompt)")[:140],
|
|
371
|
+
elapsed_label=job["elapsed_label"],
|
|
372
|
+
viewer_url=(SERVICE.session_viewer.format(session_id=job["session_id"])
|
|
373
|
+
if SERVICE.session_viewer and job.get("session_id") else ""),
|
|
374
|
+
)
|
|
375
|
+
for job in dashboard["active"]
|
|
376
|
+
]
|
|
377
|
+
self.total_runs = dashboard["counts"]["total"]
|
|
378
|
+
self.last_24h_runs = dashboard["counts"]["last_24h"]
|
|
379
|
+
self.hourly_runs = dashboard["hourly"]
|
|
380
|
+
|
|
381
|
+
@rx.event(background=True)
|
|
382
|
+
async def poll_dashboard(self) -> None:
|
|
383
|
+
async with self:
|
|
384
|
+
if self.dashboard_polling:
|
|
385
|
+
return
|
|
386
|
+
self.dashboard_polling = True
|
|
387
|
+
while True:
|
|
388
|
+
async with self:
|
|
389
|
+
self._refresh_dashboard()
|
|
390
|
+
await asyncio.sleep(1)
|
|
391
|
+
|
|
392
|
+
def _fetch_runs_page(self, offset: int) -> list[RunRecord]:
|
|
393
|
+
"""One page of runs. Reads one row past the page to learn whether more exist."""
|
|
394
|
+
rows = SERVICE.recent_runs(RUNS_PAGE_SIZE + 1, offset)
|
|
395
|
+
self.runs_has_more = len(rows) > RUNS_PAGE_SIZE
|
|
396
|
+
records = []
|
|
397
|
+
for run in rows[:RUNS_PAGE_SIZE]:
|
|
398
|
+
message = (run.get("message") or "").strip()
|
|
399
|
+
preview = message.splitlines()[0] if message else "No message"
|
|
400
|
+
records.append(RunRecord(
|
|
401
|
+
skill_name=run["skill_name"],
|
|
402
|
+
trigger_type=run["trigger_type"],
|
|
403
|
+
status=run["status"],
|
|
404
|
+
error=run.get("error") or "",
|
|
405
|
+
started_at=run["started_at"],
|
|
406
|
+
message=message,
|
|
407
|
+
preview=preview[:120] + ("..." if len(preview) > 120 else ""),
|
|
408
|
+
viewer_url=(SERVICE.session_viewer.format(session_id=run["session_id"])
|
|
409
|
+
if SERVICE.session_viewer and run.get("session_id") else ""),
|
|
410
|
+
))
|
|
411
|
+
return records
|
|
412
|
+
|
|
413
|
+
def load_runs(self) -> None:
|
|
414
|
+
"""Load (or reload) the first page. Runs on every visit to /runs."""
|
|
415
|
+
self.runs_loading = False
|
|
416
|
+
self.runs = self._fetch_runs_page(0)
|
|
417
|
+
|
|
418
|
+
def load_more_runs(self) -> None:
|
|
419
|
+
if self.runs_loading or not self.runs_has_more:
|
|
420
|
+
return
|
|
421
|
+
self.runs_loading = True
|
|
422
|
+
try:
|
|
423
|
+
self.runs = self.runs + self._fetch_runs_page(len(self.runs))
|
|
424
|
+
finally:
|
|
425
|
+
self.runs_loading = False
|
|
426
|
+
|
|
427
|
+
@rx.event(background=True)
|
|
428
|
+
async def load_workflow(self, force: bool = False) -> None:
|
|
429
|
+
async with self:
|
|
430
|
+
if self.workflow_loading:
|
|
431
|
+
return
|
|
432
|
+
self.workflow_loading = True
|
|
433
|
+
self.workflow_error = ""
|
|
434
|
+
self.edge_menu_skills = []
|
|
435
|
+
try:
|
|
436
|
+
workflow = await asyncio.to_thread(
|
|
437
|
+
SERVICE.generate_workflow, force)
|
|
438
|
+
except Exception as error:
|
|
439
|
+
async with self:
|
|
440
|
+
self.workflow_error = str(error)
|
|
441
|
+
self.story_workflow_nodes = []
|
|
442
|
+
self.story_workflow_edges = []
|
|
443
|
+
self.story_workflow_warnings = []
|
|
444
|
+
self.task_workflow_nodes = []
|
|
445
|
+
self.task_workflow_edges = []
|
|
446
|
+
self.task_workflow_warnings = []
|
|
447
|
+
self.workflow_loading = False
|
|
448
|
+
return
|
|
449
|
+
async with self:
|
|
450
|
+
self.story_workflow_nodes = workflow["story"]["nodes"]
|
|
451
|
+
self.story_workflow_edges = workflow["story"]["edges"]
|
|
452
|
+
self.story_workflow_warnings = workflow["story"]["warnings"]
|
|
453
|
+
self.task_workflow_nodes = workflow["task"]["nodes"]
|
|
454
|
+
self.task_workflow_edges = workflow["task"]["edges"]
|
|
455
|
+
self.task_workflow_warnings = workflow["task"]["warnings"]
|
|
456
|
+
self.workflow_loading = False
|
|
457
|
+
|
|
458
|
+
def open_edge_menu(self, skills: list[str], x: float, y: float) -> None:
|
|
459
|
+
self.edge_menu_skills = skills
|
|
460
|
+
self.edge_menu_left = f"{round(x)}px"
|
|
461
|
+
self.edge_menu_top = f"{round(y)}px"
|
|
462
|
+
|
|
463
|
+
def close_edge_menu(self) -> None:
|
|
464
|
+
self.edge_menu_skills = []
|
|
465
|
+
|
|
466
|
+
def edit_workflow_skill(self, label: str) -> Any:
|
|
467
|
+
self.edge_menu_skills = []
|
|
468
|
+
slug = SERVICE.resolve_skill_slug(label)
|
|
469
|
+
if not slug:
|
|
470
|
+
return rx.toast.error(f"No skill found for transition '{label}'")
|
|
471
|
+
self.load_skills()
|
|
472
|
+
self.edit_skill(slug)
|
|
473
|
+
return rx.redirect("/skills")
|
|
474
|
+
|
|
475
|
+
def load_settings(self) -> Any:
|
|
476
|
+
settings = SERVICE.load_settings()
|
|
477
|
+
self.tasks_provider = settings.tasks_provider.value
|
|
478
|
+
self.coding_agent = settings.coding_agent.value
|
|
479
|
+
self.max_parallel_agents = str(settings.max_parallel_agents)
|
|
480
|
+
jira = settings.credentials.get("jira", {})
|
|
481
|
+
azure = settings.credentials.get("azure_devops", {})
|
|
482
|
+
self.jira_base_url = jira.get("base_url", "")
|
|
483
|
+
self.jira_account_email = jira.get("account_email", "")
|
|
484
|
+
self.jira_api_token = jira.get("api_token", "")
|
|
485
|
+
self.jira_project = jira.get("project", "")
|
|
486
|
+
self.azure_organization_url = azure.get("organization_url", "")
|
|
487
|
+
self.azure_project = azure.get("project", "")
|
|
488
|
+
self.azure_tenant_id = azure.get("tenant_id", "")
|
|
489
|
+
self.azure_client_id = azure.get("client_id", "")
|
|
490
|
+
self.azure_client_secret = azure.get("client_secret", "")
|
|
491
|
+
self.load_azure_connection()
|
|
492
|
+
return self._azure_callback_toast()
|
|
493
|
+
|
|
494
|
+
def load_azure_connection(self) -> None:
|
|
495
|
+
connection = SERVICE.azure_connection()
|
|
496
|
+
self.azure_connected = connection["connected"]
|
|
497
|
+
self.azure_account = connection["account"]
|
|
498
|
+
self.azure_expires_label = connection["expires_label"]
|
|
499
|
+
self.azure_redirect_uri = SERVICE.azure_redirect_uri()
|
|
500
|
+
|
|
501
|
+
def _azure_callback_toast(self) -> Any:
|
|
502
|
+
"""Surface the OAuth outcome the callback route passed back in the URL."""
|
|
503
|
+
params = self.router.url.query_parameters
|
|
504
|
+
outcome = params.get("azure", "")
|
|
505
|
+
if not outcome:
|
|
506
|
+
return None
|
|
507
|
+
message = params.get("message", "")
|
|
508
|
+
if outcome == "connected":
|
|
509
|
+
return rx.toast.success(message or "Connected to Azure DevOps")
|
|
510
|
+
return rx.toast.error(message or "Could not connect to Azure DevOps")
|
|
511
|
+
|
|
512
|
+
def set_tasks_provider(self, value: str) -> None:
|
|
513
|
+
self.tasks_provider = value
|
|
514
|
+
|
|
515
|
+
def set_coding_agent(self, value: str) -> None:
|
|
516
|
+
self.coding_agent = value
|
|
517
|
+
|
|
518
|
+
def set_max_parallel_agents(self, value: str) -> None:
|
|
519
|
+
self.max_parallel_agents = value
|
|
520
|
+
|
|
521
|
+
def set_jira_base_url(self, value: str) -> None:
|
|
522
|
+
self.jira_base_url = value
|
|
523
|
+
|
|
524
|
+
def set_jira_account_email(self, value: str) -> None:
|
|
525
|
+
self.jira_account_email = value
|
|
526
|
+
|
|
527
|
+
def set_jira_api_token(self, value: str) -> None:
|
|
528
|
+
self.jira_api_token = value
|
|
529
|
+
|
|
530
|
+
def set_jira_project(self, value: str) -> None:
|
|
531
|
+
self.jira_project = value
|
|
532
|
+
|
|
533
|
+
def set_azure_organization_url(self, value: str) -> None:
|
|
534
|
+
self.azure_organization_url = value
|
|
535
|
+
|
|
536
|
+
def set_azure_project(self, value: str) -> None:
|
|
537
|
+
self.azure_project = value
|
|
538
|
+
|
|
539
|
+
def set_azure_tenant_id(self, value: str) -> None:
|
|
540
|
+
self.azure_tenant_id = value
|
|
541
|
+
|
|
542
|
+
def set_azure_client_id(self, value: str) -> None:
|
|
543
|
+
self.azure_client_id = value
|
|
544
|
+
|
|
545
|
+
def set_azure_client_secret(self, value: str) -> None:
|
|
546
|
+
self.azure_client_secret = value
|
|
547
|
+
|
|
548
|
+
@rx.var
|
|
549
|
+
def azure_can_connect(self) -> bool:
|
|
550
|
+
"""Everything the authorization request and the later queries need."""
|
|
551
|
+
return all(value.strip() for value in (
|
|
552
|
+
self.azure_organization_url, self.azure_project,
|
|
553
|
+
self.azure_client_id, self.azure_client_secret))
|
|
554
|
+
|
|
555
|
+
def connect_azure_devops(self) -> Any:
|
|
556
|
+
"""Save the app registration, then hand the browser to Entra ID for consent.
|
|
557
|
+
|
|
558
|
+
Saving first is what makes the callback work: it arrives as its own HTTP
|
|
559
|
+
request and reads the client secret back off disk to exchange the code.
|
|
560
|
+
"""
|
|
561
|
+
if not self.azure_can_connect:
|
|
562
|
+
return rx.toast.error(
|
|
563
|
+
"Fill in organization URL, project, client ID and client secret first.")
|
|
564
|
+
error = self._persist_settings()
|
|
565
|
+
if error:
|
|
566
|
+
return rx.toast.error(error)
|
|
567
|
+
started, result = SERVICE.start_azure_authorization()
|
|
568
|
+
if not started:
|
|
569
|
+
return rx.toast.error(result)
|
|
570
|
+
# Same tab, so the callback lands back on /settings once consent is done.
|
|
571
|
+
return rx.redirect(result)
|
|
572
|
+
|
|
573
|
+
def disconnect_azure_devops(self) -> Any:
|
|
574
|
+
SERVICE.disconnect_azure()
|
|
575
|
+
self.load_azure_connection()
|
|
576
|
+
return rx.toast.success("Disconnected from Azure DevOps")
|
|
577
|
+
|
|
578
|
+
def _persist_settings(self) -> str:
|
|
579
|
+
"""Write the settings to disk. Returns an error message, or '' when saved."""
|
|
580
|
+
try:
|
|
581
|
+
parallel_agents = int(self.max_parallel_agents)
|
|
582
|
+
except ValueError:
|
|
583
|
+
return "Max parallel tasks must be a number"
|
|
584
|
+
credentials = (
|
|
585
|
+
{
|
|
586
|
+
"base_url": self.jira_base_url,
|
|
587
|
+
"account_email": self.jira_account_email,
|
|
588
|
+
"api_token": self.jira_api_token,
|
|
589
|
+
"project": self.jira_project,
|
|
590
|
+
}
|
|
591
|
+
if self.tasks_provider == "jira"
|
|
592
|
+
else {
|
|
593
|
+
"organization_url": self.azure_organization_url,
|
|
594
|
+
"project": self.azure_project,
|
|
595
|
+
"tenant_id": self.azure_tenant_id,
|
|
596
|
+
"client_id": self.azure_client_id,
|
|
597
|
+
"client_secret": self.azure_client_secret,
|
|
598
|
+
}
|
|
599
|
+
)
|
|
600
|
+
SERVICE.save_settings(
|
|
601
|
+
self.tasks_provider,
|
|
602
|
+
self.coding_agent,
|
|
603
|
+
parallel_agents,
|
|
604
|
+
credentials,
|
|
605
|
+
)
|
|
606
|
+
return ""
|
|
607
|
+
|
|
608
|
+
def save_settings(self) -> Any:
|
|
609
|
+
error = self._persist_settings()
|
|
610
|
+
return rx.toast.error(error) if error else rx.toast.success("Settings saved")
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
ACCENT = "var(--codee-accent)"
|
|
614
|
+
BORDER = "1px solid var(--codee-border)"
|
|
615
|
+
MUTED = "var(--codee-muted)"
|
|
616
|
+
SURFACE = "var(--codee-surface)"
|
|
617
|
+
PAGE_BACKGROUND = "var(--codee-page-background)"
|
|
618
|
+
NAV_BACKGROUND = "var(--codee-nav-background)"
|
|
619
|
+
TEXT = "var(--codee-text)"
|
|
620
|
+
HOVER = "var(--codee-hover)"
|
|
621
|
+
ACTIVE = "var(--codee-active)"
|
|
622
|
+
GRID = "var(--codee-grid)"
|
|
623
|
+
SUBTLE_ICON = "var(--codee-subtle-icon)"
|
|
624
|
+
RUNNING_BACKGROUND = "var(--codee-running-background)"
|
|
625
|
+
RUNNING_GLOW = "var(--codee-running-glow)"
|
|
626
|
+
MONO = "IBM Plex Mono, monospace"
|
|
627
|
+
LOGO = "👨🏻💻"
|
|
628
|
+
# Inline SVG carrying the logo emoji, so the favicon needs no binary asset.
|
|
629
|
+
FAVICON = (
|
|
630
|
+
"data:image/svg+xml;base64,"
|
|
631
|
+
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMD"
|
|
632
|
+
"AgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn5Go8J+Pu+KAjfCfkrs8L3Rl"
|
|
633
|
+
"eHQ+PC9zdmc+"
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def nav_link(label: str, icon: str, href: str) -> rx.Component:
|
|
638
|
+
is_active = AdminState.active_route == href
|
|
639
|
+
return rx.link(
|
|
640
|
+
rx.hstack(rx.icon(icon, size=17), rx.text(
|
|
641
|
+
label), spacing="3", align="center"),
|
|
642
|
+
href=href,
|
|
643
|
+
aria_current=rx.cond(is_active, "page", ""),
|
|
644
|
+
color=rx.cond(is_active, ACCENT, TEXT),
|
|
645
|
+
background=rx.cond(is_active, ACTIVE, "transparent"),
|
|
646
|
+
font_weight=rx.cond(is_active, "600", "500"),
|
|
647
|
+
box_shadow=rx.cond(
|
|
648
|
+
is_active, f"inset 3px 0 0 0 {ACCENT}", "inset 0 0 0 0 transparent"),
|
|
649
|
+
padding="0.6rem 0.75rem",
|
|
650
|
+
border_radius="6px",
|
|
651
|
+
_hover={"background": rx.cond(is_active, ACTIVE, HOVER),
|
|
652
|
+
"color": ACCENT},
|
|
653
|
+
text_decoration="none",
|
|
654
|
+
width=rx.breakpoints(initial="auto", lg="100%"),
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def shell(content: rx.Component) -> rx.Component:
|
|
659
|
+
return rx.box(
|
|
660
|
+
rx.grid(
|
|
661
|
+
rx.box(
|
|
662
|
+
rx.vstack(
|
|
663
|
+
rx.hstack(
|
|
664
|
+
rx.box(LOGO, color="white", background=ACCENT, width="2rem",
|
|
665
|
+
height="2rem", display="grid", place_items="center",
|
|
666
|
+
border_radius="6px", font_weight="700",
|
|
667
|
+
font_size="1.1rem", line_height="1"),
|
|
668
|
+
rx.text("Codee", font_size="1.1rem",
|
|
669
|
+
font_weight="700"),
|
|
670
|
+
rx.spacer(),
|
|
671
|
+
rx.color_mode.button(
|
|
672
|
+
position="static",
|
|
673
|
+
variant="soft",
|
|
674
|
+
aria_label="Toggle color mode",
|
|
675
|
+
),
|
|
676
|
+
spacing="3",
|
|
677
|
+
align="center",
|
|
678
|
+
width="100%",
|
|
679
|
+
),
|
|
680
|
+
rx.flex(
|
|
681
|
+
nav_link("Dashboard", "layout-dashboard", "/"),
|
|
682
|
+
nav_link("Skills", "blocks", "/skills"),
|
|
683
|
+
nav_link("Workflow", "git-branch", "/workflow"),
|
|
684
|
+
nav_link("Memory", "notebook-text", "/memory"),
|
|
685
|
+
nav_link("Runs", "history", "/runs"),
|
|
686
|
+
rx.cond(
|
|
687
|
+
AdminState.coding_agent == "claude_code",
|
|
688
|
+
nav_link("Sessions", "key-round", "/sessions"),
|
|
689
|
+
),
|
|
690
|
+
nav_link("Settings", "settings", "/settings"),
|
|
691
|
+
direction=rx.breakpoints(initial="row", lg="column"),
|
|
692
|
+
wrap="wrap",
|
|
693
|
+
gap="0.2rem",
|
|
694
|
+
width="100%",
|
|
695
|
+
),
|
|
696
|
+
spacing="6",
|
|
697
|
+
align="start",
|
|
698
|
+
width="100%",
|
|
699
|
+
),
|
|
700
|
+
padding=rx.breakpoints(initial="1rem", lg="1.5rem"),
|
|
701
|
+
border_right=rx.breakpoints(initial="none", lg=BORDER),
|
|
702
|
+
border_bottom=rx.breakpoints(initial=BORDER, lg="none"),
|
|
703
|
+
background=NAV_BACKGROUND,
|
|
704
|
+
min_height=rx.breakpoints(initial="auto", lg="100vh"),
|
|
705
|
+
),
|
|
706
|
+
rx.box(content, padding=rx.breakpoints(initial="1.25rem", md="2rem", xl="3rem"),
|
|
707
|
+
min_width="0", max_width="1440px", width="100%"),
|
|
708
|
+
columns=rx.breakpoints(initial="1fr", lg="230px minmax(0, 1fr)"),
|
|
709
|
+
min_height="100vh",
|
|
710
|
+
),
|
|
711
|
+
background=PAGE_BACKGROUND,
|
|
712
|
+
color=TEXT,
|
|
713
|
+
font_family="IBM Plex Sans, sans-serif",
|
|
714
|
+
style={
|
|
715
|
+
"--codee-accent": rx.color_mode_cond("#167d5a", "#4abd85"),
|
|
716
|
+
"--codee-border": rx.color_mode_cond("#dfe4df", "#354039"),
|
|
717
|
+
"--codee-muted": rx.color_mode_cond("#68716b", "#a1ada5"),
|
|
718
|
+
"--codee-surface": rx.color_mode_cond("#ffffff", "#18201c"),
|
|
719
|
+
"--codee-page-background": rx.color_mode_cond("#f3f5f3", "#101512"),
|
|
720
|
+
"--codee-nav-background": rx.color_mode_cond("#f7f9f7", "#141b17"),
|
|
721
|
+
"--codee-text": rx.color_mode_cond("#202721", "#edf3ef"),
|
|
722
|
+
"--codee-hover": rx.color_mode_cond("#edf5f0", "#213029"),
|
|
723
|
+
"--codee-active": rx.color_mode_cond("#e2efe9", "#23372e"),
|
|
724
|
+
"--codee-grid": rx.color_mode_cond("#e4e9e5", "#2e3932"),
|
|
725
|
+
"--codee-subtle-icon": rx.color_mode_cond("#91a097", "#718078"),
|
|
726
|
+
"--codee-warning-background": rx.color_mode_cond("#fffbeb", "#332a13"),
|
|
727
|
+
"--codee-warning-border": rx.color_mode_cond("#d97706", "#f59e0b"),
|
|
728
|
+
# Tint reserved for in-flight work, so a live run reads at a glance.
|
|
729
|
+
"--codee-running-background": rx.color_mode_cond("#effaf4", "#15271f"),
|
|
730
|
+
"--codee-running-glow": rx.color_mode_cond(
|
|
731
|
+
"rgba(22, 125, 90, 0.16)", "rgba(74, 189, 133, 0.18)"),
|
|
732
|
+
"color_scheme": "light dark",
|
|
733
|
+
},
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def page_header(title: str, description: str) -> rx.Component:
|
|
738
|
+
return rx.vstack(
|
|
739
|
+
rx.heading(title, size="7", letter_spacing="0"),
|
|
740
|
+
rx.text(description, color=MUTED, font_size="0.95rem"),
|
|
741
|
+
spacing="1",
|
|
742
|
+
align="start",
|
|
743
|
+
margin_bottom="2rem",
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def empty_state(icon: str, text: str) -> rx.Component:
|
|
748
|
+
return rx.center(
|
|
749
|
+
rx.vstack(rx.icon(icon, size=28, color=SUBTLE_ICON), rx.text(text, color=MUTED),
|
|
750
|
+
spacing="3", align="center"),
|
|
751
|
+
border="1px dashed var(--codee-border)",
|
|
752
|
+
min_height="10rem",
|
|
753
|
+
width="100%",
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def live_dot(size: str = "0.6rem") -> rx.Component:
|
|
758
|
+
"""Accent dot with an expanding halo: the "this is live right now" marker."""
|
|
759
|
+
return rx.box(
|
|
760
|
+
rx.box(position="absolute", inset="0", border_radius="50%", background=ACCENT,
|
|
761
|
+
animation="codee-ping 1.8s cubic-bezier(0, 0, 0.2, 1) infinite"),
|
|
762
|
+
rx.box(position="absolute", inset="0", border_radius="50%", background=ACCENT),
|
|
763
|
+
class_name="codee-live-dot",
|
|
764
|
+
position="relative",
|
|
765
|
+
width=size,
|
|
766
|
+
height=size,
|
|
767
|
+
flex_shrink="0",
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def elapsed_pill(label: rx.Var | str) -> rx.Component:
|
|
772
|
+
return rx.hstack(
|
|
773
|
+
rx.icon("timer", size=14, color=ACCENT),
|
|
774
|
+
rx.text(label, font_family=MONO, font_size="0.85rem", font_weight="500"),
|
|
775
|
+
spacing="2",
|
|
776
|
+
align="center",
|
|
777
|
+
flex_shrink="0",
|
|
778
|
+
padding="0.2rem 0.6rem",
|
|
779
|
+
background=SURFACE,
|
|
780
|
+
border=BORDER,
|
|
781
|
+
border_radius="999px",
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def active_job_row(job: ActiveJob) -> rx.Component:
|
|
786
|
+
return rx.flex(
|
|
787
|
+
live_dot(),
|
|
788
|
+
rx.text(job.message, font_weight="600", font_family=MONO, font_size="0.9rem",
|
|
789
|
+
flex="1", min_width="0", overflow="hidden", text_overflow="ellipsis",
|
|
790
|
+
white_space="nowrap", custom_attrs={"title": job.message}),
|
|
791
|
+
elapsed_pill(job.elapsed_label),
|
|
792
|
+
rx.cond(
|
|
793
|
+
job.viewer_url != "",
|
|
794
|
+
rx.link(rx.icon("external-link", size=16), href=job.viewer_url, is_external=True,
|
|
795
|
+
aria_label="View session", color=ACCENT, display="flex",
|
|
796
|
+
align_items="center"),
|
|
797
|
+
),
|
|
798
|
+
gap="0.85rem",
|
|
799
|
+
align="center",
|
|
800
|
+
padding="0.85rem 1rem",
|
|
801
|
+
background=RUNNING_BACKGROUND,
|
|
802
|
+
border=BORDER,
|
|
803
|
+
border_left=f"3px solid {ACCENT}",
|
|
804
|
+
border_radius="4px",
|
|
805
|
+
width="100%",
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def running_tile() -> rx.Component:
|
|
810
|
+
"""Stat tile that lights up while sessions are in flight."""
|
|
811
|
+
running = AdminState.active_jobs.length()
|
|
812
|
+
is_running = running > 0
|
|
813
|
+
return rx.box(
|
|
814
|
+
rx.hstack(rx.text("Running now", color=MUTED),
|
|
815
|
+
rx.cond(is_running, live_dot("0.5rem")),
|
|
816
|
+
spacing="2", align="center"),
|
|
817
|
+
rx.heading(running, size="8", color=rx.cond(is_running, ACCENT, TEXT)),
|
|
818
|
+
padding="1.25rem",
|
|
819
|
+
background=rx.cond(is_running, RUNNING_BACKGROUND, SURFACE),
|
|
820
|
+
border=rx.cond(is_running, f"1px solid {ACCENT}", BORDER),
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def running_panel() -> rx.Component:
|
|
825
|
+
"""Live sessions panel — accent-lit while anything is in flight, quiet when idle."""
|
|
826
|
+
running = AdminState.active_jobs.length()
|
|
827
|
+
is_running = running > 0
|
|
828
|
+
return rx.box(
|
|
829
|
+
rx.hstack(
|
|
830
|
+
rx.cond(is_running, live_dot("0.55rem")),
|
|
831
|
+
rx.heading("Currently running", size="4"),
|
|
832
|
+
rx.cond(
|
|
833
|
+
is_running,
|
|
834
|
+
rx.box(running, color=ACCENT, background=RUNNING_BACKGROUND,
|
|
835
|
+
border=f"1px solid {ACCENT}", border_radius="999px",
|
|
836
|
+
padding="0.05rem 0.55rem", font_size="0.8rem", font_weight="600",
|
|
837
|
+
font_family=MONO, class_name="codee-breathe",
|
|
838
|
+
animation="codee-breathe 2.4s ease-in-out infinite"),
|
|
839
|
+
),
|
|
840
|
+
spacing="3",
|
|
841
|
+
align="center",
|
|
842
|
+
width="100%",
|
|
843
|
+
margin_bottom="0.9rem",
|
|
844
|
+
),
|
|
845
|
+
rx.cond(
|
|
846
|
+
is_running,
|
|
847
|
+
rx.vstack(rx.foreach(AdminState.active_jobs, active_job_row),
|
|
848
|
+
spacing="2", width="100%"),
|
|
849
|
+
rx.hstack(rx.icon("moon", size=16, color=SUBTLE_ICON),
|
|
850
|
+
rx.text("No sessions running right now.", color=MUTED),
|
|
851
|
+
spacing="2", align="center"),
|
|
852
|
+
),
|
|
853
|
+
padding="1.25rem",
|
|
854
|
+
background=SURFACE,
|
|
855
|
+
border=rx.cond(is_running, f"1px solid {ACCENT}", BORDER),
|
|
856
|
+
box_shadow=rx.cond(is_running, f"0 0 0 4px {RUNNING_GLOW}", "none"),
|
|
857
|
+
width="100%",
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def dashboard_page() -> rx.Component:
|
|
862
|
+
return shell(rx.vstack(
|
|
863
|
+
page_header(
|
|
864
|
+
"Dashboard", "Run activity and live coding-agent sessions."),
|
|
865
|
+
rx.grid(
|
|
866
|
+
rx.box(rx.text("Total runs", color=MUTED), rx.heading(AdminState.total_runs, size="8"),
|
|
867
|
+
padding="1.25rem", background=SURFACE, border=BORDER),
|
|
868
|
+
rx.box(rx.text("Last 24 hours", color=MUTED), rx.heading(AdminState.last_24h_runs, size="8"),
|
|
869
|
+
padding="1.25rem", background=SURFACE, border=BORDER),
|
|
870
|
+
running_tile(),
|
|
871
|
+
columns=rx.breakpoints(initial="1", sm="2", lg="3"), gap="1rem", width="100%"),
|
|
872
|
+
running_panel(),
|
|
873
|
+
rx.box(
|
|
874
|
+
rx.heading("Last 24 hours by hour",
|
|
875
|
+
size="4", margin_bottom="1rem"),
|
|
876
|
+
rx.recharts.bar_chart(
|
|
877
|
+
rx.recharts.cartesian_grid(
|
|
878
|
+
stroke_dasharray="3 3", stroke=GRID),
|
|
879
|
+
rx.recharts.x_axis(data_key="hour", tick={"fontSize": 11}),
|
|
880
|
+
rx.recharts.y_axis(allow_decimals=False,
|
|
881
|
+
tick={"fontSize": 11}),
|
|
882
|
+
rx.recharts.tooltip(),
|
|
883
|
+
rx.recharts.bar(data_key="runs", fill=ACCENT,
|
|
884
|
+
radius=[3, 3, 0, 0]),
|
|
885
|
+
data=AdminState.hourly_runs,
|
|
886
|
+
width="100%", height=280,
|
|
887
|
+
),
|
|
888
|
+
padding="1.25rem", background=SURFACE, border=BORDER, width="100%"),
|
|
889
|
+
spacing="5", align="start", width="100%",
|
|
890
|
+
))
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def skill_card(skill: SkillSummary) -> rx.Component:
|
|
894
|
+
return rx.box(
|
|
895
|
+
rx.vstack(
|
|
896
|
+
rx.hstack(rx.heading(skill.name, size="4"), rx.spacer(),
|
|
897
|
+
rx.badge(skill.type, color_scheme="green", variant="soft"), width="100%"),
|
|
898
|
+
rx.text(skill.description, color=MUTED, font_size="0.9rem", min_height="2.7rem",
|
|
899
|
+
overflow="hidden"),
|
|
900
|
+
rx.cond(
|
|
901
|
+
skill.issue_status != "",
|
|
902
|
+
rx.hstack(
|
|
903
|
+
rx.badge(skill.issue_type, color_scheme="green",
|
|
904
|
+
variant="outline"),
|
|
905
|
+
rx.icon("circle-dot", size=14, color=SUBTLE_ICON),
|
|
906
|
+
rx.text(skill.issue_status, color=MUTED,
|
|
907
|
+
font_size="0.82rem"),
|
|
908
|
+
spacing="2",
|
|
909
|
+
align="center",
|
|
910
|
+
width="100%",
|
|
911
|
+
background=HOVER,
|
|
912
|
+
padding="0.55rem 0.65rem",
|
|
913
|
+
border_radius="4px",
|
|
914
|
+
),
|
|
915
|
+
),
|
|
916
|
+
rx.button(rx.icon("pencil", size=15), "Edit", variant="soft",
|
|
917
|
+
on_click=AdminState.edit_skill(skill.slug), width="100%"),
|
|
918
|
+
spacing="4", align="start", height="100%", width="100%"),
|
|
919
|
+
padding="1rem", background=SURFACE, border=BORDER, min_height="185px"),
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def agents_card() -> rx.Component:
|
|
923
|
+
return rx.box(
|
|
924
|
+
rx.vstack(
|
|
925
|
+
rx.hstack(rx.heading(AGENTS_FILE, size="4"), rx.spacer(),
|
|
926
|
+
rx.badge("always on", color_scheme="gray", variant="soft"), width="100%"),
|
|
927
|
+
rx.text("Plain-text instructions every coding-agent run loads. Cannot be deleted.",
|
|
928
|
+
color=MUTED, font_size="0.9rem", min_height="2.7rem", overflow="hidden"),
|
|
929
|
+
rx.button(rx.icon("pencil", size=15), "Edit", variant="soft",
|
|
930
|
+
on_click=AdminState.edit_agents, width="100%"),
|
|
931
|
+
spacing="4", align="start", height="100%", width="100%"),
|
|
932
|
+
padding="1rem", background=SURFACE, border=BORDER, min_height="185px")
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def agents_editor() -> rx.Component:
|
|
936
|
+
return rx.vstack(
|
|
937
|
+
rx.hstack(rx.button(rx.icon("arrow-left", size=16), "Back", variant="ghost",
|
|
938
|
+
on_click=AdminState.close_agents),
|
|
939
|
+
rx.heading(AGENTS_FILE, size="4"), rx.spacer(),
|
|
940
|
+
rx.button(rx.icon("save", size=16), f"Save {AGENTS_FILE}",
|
|
941
|
+
on_click=AdminState.save_agents),
|
|
942
|
+
spacing="3", align="center", width="100%"),
|
|
943
|
+
rx.text("Edited as plain text, without skill frontmatter.",
|
|
944
|
+
color=MUTED, font_size="0.85rem"),
|
|
945
|
+
rx.text_area(value=AdminState.agents_content, on_change=AdminState.set_agents_content,
|
|
946
|
+
width="100%", min_height="32rem", font_family="IBM Plex Mono, monospace"),
|
|
947
|
+
spacing="4", align="start", width="100%")
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def field(label: str, control: rx.Component, hint: rx.Component | None = None) -> rx.Component:
|
|
951
|
+
children = [rx.text(label, font_weight="600",
|
|
952
|
+
font_size="0.85rem"), control]
|
|
953
|
+
if hint is not None:
|
|
954
|
+
children.append(hint)
|
|
955
|
+
return rx.vstack(*children, spacing="2", align="start", width="100%")
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def model_menu_item(button: rx.Component) -> rx.Component:
|
|
959
|
+
"""Make a picker row dismiss the popover while staying clickable end to end.
|
|
960
|
+
|
|
961
|
+
``rx.popover.close`` wraps any child carrying an ``on_click`` in a Flex of
|
|
962
|
+
its own, and that wrapper hugs its content — so a full-width button inside
|
|
963
|
+
it is still only clickable across the text. The width has to be restated at
|
|
964
|
+
every level to give the row a full-width hit area.
|
|
965
|
+
"""
|
|
966
|
+
return rx.popover.close(rx.flex(button, width="100%"), width="100%")
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def model_option_row(option: ModelOption) -> rx.Component:
|
|
970
|
+
"""One row of the model picker: friendly name left, model code right."""
|
|
971
|
+
return model_menu_item(
|
|
972
|
+
rx.button(
|
|
973
|
+
# No spacer between the two: pinning the code to the right edge ran
|
|
974
|
+
# it under the scroll bar.
|
|
975
|
+
rx.hstack(rx.text(option.name, font_size="0.85rem"),
|
|
976
|
+
rx.code(option.id, font_size="0.72rem",
|
|
977
|
+
color_scheme="gray"),
|
|
978
|
+
align="center", spacing="2", width="100%"),
|
|
979
|
+
variant="ghost", color_scheme="gray", width="100%",
|
|
980
|
+
justify_content="start", padding="0.45rem 0.6rem",
|
|
981
|
+
on_click=AdminState.choose_model(option.id)))
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def model_picker() -> rx.Component:
|
|
985
|
+
"""Searchable model select that also accepts a model code typed by hand.
|
|
986
|
+
|
|
987
|
+
The agent's own catalog is only a convenience — anything typed here is saved
|
|
988
|
+
verbatim, so a model the agent gained after this list was built still works.
|
|
989
|
+
"""
|
|
990
|
+
return field(
|
|
991
|
+
"Model",
|
|
992
|
+
rx.popover.root(
|
|
993
|
+
rx.popover.trigger(
|
|
994
|
+
rx.button(
|
|
995
|
+
rx.hstack(rx.text(AdminState.skill_model_label), rx.spacer(),
|
|
996
|
+
rx.icon("chevrons-up-down", size=14),
|
|
997
|
+
align="center", width="100%"),
|
|
998
|
+
variant="surface", color_scheme="gray", width="100%",
|
|
999
|
+
type="button")),
|
|
1000
|
+
rx.popover.content(
|
|
1001
|
+
rx.vstack(
|
|
1002
|
+
rx.input(placeholder="Search models, or type a model code",
|
|
1003
|
+
value=AdminState.model_query,
|
|
1004
|
+
on_change=AdminState.set_model_query,
|
|
1005
|
+
auto_focus=True, width="100%"),
|
|
1006
|
+
rx.cond(
|
|
1007
|
+
AdminState.custom_model_query != "",
|
|
1008
|
+
model_menu_item(
|
|
1009
|
+
rx.button(
|
|
1010
|
+
rx.hstack(rx.icon("plus", size=14),
|
|
1011
|
+
rx.text("Use "),
|
|
1012
|
+
rx.code(
|
|
1013
|
+
AdminState.custom_model_query),
|
|
1014
|
+
align="center", spacing="2"),
|
|
1015
|
+
variant="soft", width="100%",
|
|
1016
|
+
justify_content="start",
|
|
1017
|
+
padding="0.45rem 0.6rem",
|
|
1018
|
+
on_click=AdminState.choose_model(
|
|
1019
|
+
AdminState.custom_model_query)))),
|
|
1020
|
+
rx.scroll_area(
|
|
1021
|
+
rx.vstack(
|
|
1022
|
+
model_menu_item(
|
|
1023
|
+
rx.button(
|
|
1024
|
+
"Agent default", variant="ghost",
|
|
1025
|
+
color_scheme="gray", width="100%",
|
|
1026
|
+
justify_content="start",
|
|
1027
|
+
padding="0.45rem 0.6rem",
|
|
1028
|
+
on_click=AdminState.choose_model(""))),
|
|
1029
|
+
rx.foreach(AdminState.filtered_models,
|
|
1030
|
+
model_option_row),
|
|
1031
|
+
rx.cond(
|
|
1032
|
+
AdminState.models_loading,
|
|
1033
|
+
rx.text("Loading models from the coding agent…",
|
|
1034
|
+
color=MUTED, font_size="0.8rem",
|
|
1035
|
+
padding="0.5rem")),
|
|
1036
|
+
spacing="1", width="100%"),
|
|
1037
|
+
type="auto", scrollbars="vertical",
|
|
1038
|
+
max_height="15rem", width="100%"),
|
|
1039
|
+
spacing="2", width="100%"),
|
|
1040
|
+
width="24rem"),
|
|
1041
|
+
),
|
|
1042
|
+
rx.text(
|
|
1043
|
+
rx.cond(AdminState.skill_model == "",
|
|
1044
|
+
"Runs on whatever the coding agent defaults to.",
|
|
1045
|
+
rx.fragment("Saved as ", rx.code(AdminState.skill_model),
|
|
1046
|
+
" in the skill frontmatter.")),
|
|
1047
|
+
color=MUTED, font_size="0.82rem"))
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
def delete_skill_dialog() -> rx.Component:
|
|
1051
|
+
return rx.alert_dialog.root(
|
|
1052
|
+
rx.alert_dialog.trigger(
|
|
1053
|
+
rx.button(rx.icon("trash-2", size=16), "Delete skill",
|
|
1054
|
+
variant="outline", color_scheme="red")),
|
|
1055
|
+
rx.alert_dialog.content(
|
|
1056
|
+
rx.alert_dialog.title("Delete skill"),
|
|
1057
|
+
rx.alert_dialog.description(
|
|
1058
|
+
"This permanently deletes ", rx.text.strong(
|
|
1059
|
+
AdminState.selected_skill),
|
|
1060
|
+
" and pushes the removal to Git. This cannot be undone."),
|
|
1061
|
+
rx.hstack(
|
|
1062
|
+
rx.alert_dialog.cancel(rx.button("Cancel", variant="soft",
|
|
1063
|
+
color_scheme="gray")),
|
|
1064
|
+
rx.alert_dialog.action(rx.button("Delete skill", color_scheme="red",
|
|
1065
|
+
on_click=AdminState.delete_skill)),
|
|
1066
|
+
spacing="3", justify="end", margin_top="1.25rem", width="100%"),
|
|
1067
|
+
max_width="27rem"),
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def skill_editor() -> rx.Component:
|
|
1072
|
+
return rx.vstack(
|
|
1073
|
+
rx.hstack(rx.button(rx.icon("arrow-left", size=16), "Back", variant="ghost",
|
|
1074
|
+
on_click=AdminState.close_skill), rx.spacer(),
|
|
1075
|
+
delete_skill_dialog(),
|
|
1076
|
+
rx.button(rx.icon("save", size=16), "Save skill",
|
|
1077
|
+
on_click=AdminState.save_skill),
|
|
1078
|
+
spacing="3",
|
|
1079
|
+
width="100%"),
|
|
1080
|
+
rx.grid(
|
|
1081
|
+
field("Name", rx.input(value=AdminState.skill_name,
|
|
1082
|
+
on_change=AdminState.set_skill_name, width="100%")),
|
|
1083
|
+
field("Skill type", rx.select(SKILL_TYPES, value=AdminState.skill_type,
|
|
1084
|
+
on_change=AdminState.set_skill_type, width="100%")),
|
|
1085
|
+
columns=rx.breakpoints(initial="1", md="2"), gap="1rem", width="100%"),
|
|
1086
|
+
field("Description", rx.text_area(value=AdminState.skill_description,
|
|
1087
|
+
on_change=AdminState.set_skill_description,
|
|
1088
|
+
width="100%", min_height="5rem")),
|
|
1089
|
+
model_picker(),
|
|
1090
|
+
rx.cond(AdminState.skill_type == "cron trigger",
|
|
1091
|
+
field("Cron expression", rx.input(value=AdminState.skill_cron,
|
|
1092
|
+
on_change=AdminState.set_skill_cron, width="100%"),
|
|
1093
|
+
rx.hstack(rx.icon("clock-3", size=14), rx.text(AdminState.cron_description),
|
|
1094
|
+
color=MUTED, font_size="0.82rem"))),
|
|
1095
|
+
rx.cond(AdminState.skill_type == "email trigger",
|
|
1096
|
+
field("Email address", rx.input(value=AdminState.skill_email,
|
|
1097
|
+
on_change=AdminState.set_skill_email, width="100%"))),
|
|
1098
|
+
rx.cond(AdminState.skill_type == "aws-sqs trigger",
|
|
1099
|
+
field("AWS SQS queue", rx.input(value=AdminState.skill_sqs,
|
|
1100
|
+
on_change=AdminState.set_skill_sqs, width="100%"))),
|
|
1101
|
+
rx.cond(AdminState.skill_type == "issue trigger",
|
|
1102
|
+
rx.grid(
|
|
1103
|
+
field("Issue type", rx.select(
|
|
1104
|
+
ISSUE_TYPES, value=AdminState.skill_issue_type,
|
|
1105
|
+
on_change=AdminState.set_skill_issue_type, width="100%")),
|
|
1106
|
+
field("Issue statuses", rx.input(value=AdminState.skill_issue_status,
|
|
1107
|
+
on_change=AdminState.set_skill_issue_status,
|
|
1108
|
+
placeholder="Ready, In progress", width="100%")),
|
|
1109
|
+
columns=rx.breakpoints(initial="1", md="2"), gap="1rem", width="100%")),
|
|
1110
|
+
rx.vstack(
|
|
1111
|
+
rx.checkbox("Use other fields in frontmatter (for instance allowed-tools)",
|
|
1112
|
+
checked=AdminState.skill_extra_enabled,
|
|
1113
|
+
on_change=AdminState.set_skill_extra_enabled,
|
|
1114
|
+
size="2"),
|
|
1115
|
+
rx.cond(
|
|
1116
|
+
AdminState.skill_extra_enabled,
|
|
1117
|
+
field("Other frontmatter fields",
|
|
1118
|
+
rx.text_area(value=AdminState.skill_extra,
|
|
1119
|
+
on_change=AdminState.set_skill_extra,
|
|
1120
|
+
placeholder="allowed-tools: Bash",
|
|
1121
|
+
width="100%", min_height="7rem",
|
|
1122
|
+
font_family="IBM Plex Mono, monospace"),
|
|
1123
|
+
rx.text("YAML lines written into the frontmatter as they are. "
|
|
1124
|
+
"Leave out the fields that already have their own control above.",
|
|
1125
|
+
color=MUTED, font_size="0.82rem"))),
|
|
1126
|
+
spacing="3", align="start", width="100%"),
|
|
1127
|
+
field("Skill body", rx.text_area(value=AdminState.skill_body, on_change=AdminState.set_skill_body,
|
|
1128
|
+
width="100%", min_height="25rem",
|
|
1129
|
+
font_family="IBM Plex Mono, monospace")),
|
|
1130
|
+
rx.cond(AdminState.skill_type == "cron trigger",
|
|
1131
|
+
rx.button(rx.icon("play", size=16), "Run on next tick", variant="outline",
|
|
1132
|
+
on_click=AdminState.force_run_skill)),
|
|
1133
|
+
spacing="5", align="start", width="100%",
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def skills_page() -> rx.Component:
|
|
1138
|
+
listing = rx.vstack(
|
|
1139
|
+
rx.flex(rx.input(placeholder="New skill name", value=AdminState.new_skill_name,
|
|
1140
|
+
on_change=AdminState.set_new_skill_name, flex="1"),
|
|
1141
|
+
rx.button(rx.icon("plus", size=16), "Create",
|
|
1142
|
+
on_click=AdminState.create_skill),
|
|
1143
|
+
gap="0.75rem", width="100%"),
|
|
1144
|
+
rx.grid(rx.input(placeholder="Search skills", value=AdminState.skill_query,
|
|
1145
|
+
on_change=AdminState.set_skill_query, width="100%"),
|
|
1146
|
+
rx.select(["All", *SKILL_TYPES], value=AdminState.skill_filter,
|
|
1147
|
+
on_change=AdminState.set_skill_filter, width="100%"),
|
|
1148
|
+
columns=rx.breakpoints(initial="1", md="3fr 1fr"), gap="0.75rem", width="100%"),
|
|
1149
|
+
rx.cond(AdminState.agents_card_visible | (AdminState.filtered_skills.length() > 0),
|
|
1150
|
+
rx.grid(rx.cond(AdminState.agents_card_visible, agents_card()),
|
|
1151
|
+
rx.foreach(AdminState.filtered_skills, skill_card),
|
|
1152
|
+
columns=rx.breakpoints(initial="1", md="2", xl="3"), gap="1rem", width="100%"),
|
|
1153
|
+
empty_state("search-x", "No skills match this view.")),
|
|
1154
|
+
spacing="5", align="start", width="100%")
|
|
1155
|
+
return shell(rx.vstack(page_header("Skills", "Create and configure agent capabilities."),
|
|
1156
|
+
rx.cond(AdminState.editing_agents, agents_editor(),
|
|
1157
|
+
rx.cond(AdminState.selected_skill ==
|
|
1158
|
+
"", listing, skill_editor())),
|
|
1159
|
+
align="start", width="100%"))
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def memory_row(entry: MemoryEntry) -> rx.Component:
|
|
1163
|
+
return rx.cond(
|
|
1164
|
+
entry.matched,
|
|
1165
|
+
rx.flex(
|
|
1166
|
+
rx.vstack(rx.text(entry.title, font_weight="600"), rx.text(entry.hook, color=MUTED,
|
|
1167
|
+
font_size="0.85rem"),
|
|
1168
|
+
spacing="1", align="start", flex="1"),
|
|
1169
|
+
rx.text(entry.file, color=MUTED,
|
|
1170
|
+
font_family="IBM Plex Mono, monospace", font_size="0.8rem"),
|
|
1171
|
+
rx.button(rx.icon("pencil", size=15), variant="ghost",
|
|
1172
|
+
on_click=AdminState.edit_memory(entry.file), aria_label="Edit memory"),
|
|
1173
|
+
rx.button(rx.icon("trash-2", size=15), variant="ghost", color_scheme="red",
|
|
1174
|
+
on_click=AdminState.delete_memory(entry.file, entry.raw), aria_label="Delete memory"),
|
|
1175
|
+
gap="0.75rem", align="center", padding="1rem", background=SURFACE,
|
|
1176
|
+
border=BORDER, width="100%"),
|
|
1177
|
+
rx.box(rx.text(entry.raw, font_family="IBM Plex Mono, monospace", font_size="0.85rem"),
|
|
1178
|
+
padding="1rem", border=BORDER, background=SURFACE),
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def memory_editor() -> rx.Component:
|
|
1183
|
+
return rx.vstack(
|
|
1184
|
+
rx.hstack(rx.button(rx.icon("arrow-left", size=16), "Back", variant="ghost",
|
|
1185
|
+
on_click=AdminState.close_memory),
|
|
1186
|
+
rx.heading(AdminState.selected_memory,
|
|
1187
|
+
size="4"), rx.spacer(),
|
|
1188
|
+
rx.button(rx.icon("save", size=16), "Save memory",
|
|
1189
|
+
on_click=AdminState.save_memory),
|
|
1190
|
+
width="100%"),
|
|
1191
|
+
rx.text_area(value=AdminState.memory_content, on_change=AdminState.set_memory_content,
|
|
1192
|
+
width="100%", min_height="32rem", font_family="IBM Plex Mono, monospace"),
|
|
1193
|
+
spacing="4", width="100%"),
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def memory_page() -> rx.Component:
|
|
1197
|
+
listing = rx.cond(AdminState.memories.length() > 0,
|
|
1198
|
+
rx.vstack(rx.foreach(AdminState.memories,
|
|
1199
|
+
memory_row), spacing="3", width="100%"),
|
|
1200
|
+
empty_state("notebook-text", "No memories yet."))
|
|
1201
|
+
return shell(rx.vstack(page_header("Memory", "Review and maintain durable project context."),
|
|
1202
|
+
rx.cond(AdminState.selected_memory ==
|
|
1203
|
+
"", listing, memory_editor()),
|
|
1204
|
+
align="start", width="100%"))
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def run_row(run: RunRecord) -> rx.Component:
|
|
1208
|
+
return rx.box(
|
|
1209
|
+
rx.flex(
|
|
1210
|
+
rx.vstack(rx.hstack(rx.text(run.skill_name, font_weight="600"),
|
|
1211
|
+
rx.badge(run.status, color_scheme=rx.cond(run.status == "succeeded", "green", "red"))),
|
|
1212
|
+
rx.text(run.started_at, color=MUTED, font_size="0.8rem",
|
|
1213
|
+
font_family="IBM Plex Mono, monospace"),
|
|
1214
|
+
rx.text(run.preview, color=MUTED),
|
|
1215
|
+
rx.cond(run.error != "", rx.text(
|
|
1216
|
+
run.error, color="#b42318", font_size="0.85rem")),
|
|
1217
|
+
spacing="2", align="start", flex="1"),
|
|
1218
|
+
rx.badge(run.trigger_type, variant="outline"),
|
|
1219
|
+
rx.cond(run.viewer_url != "", rx.link(rx.icon("external-link", size=16), href=run.viewer_url,
|
|
1220
|
+
is_external=True, aria_label="View session", color=ACCENT)),
|
|
1221
|
+
gap="1rem", align="start", width="100%"),
|
|
1222
|
+
rx.cond(run.message != "", rx.accordion.root(rx.accordion.item(
|
|
1223
|
+
header="Full message", content=rx.text(run.message, white_space="pre-wrap"), value=run.started_at),
|
|
1224
|
+
collapsible=True, width="100%")),
|
|
1225
|
+
padding="1rem", background=SURFACE, border=BORDER, width="100%")
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def runs_page() -> rx.Component:
|
|
1229
|
+
listing = rx.vstack(
|
|
1230
|
+
rx.foreach(AdminState.runs, run_row),
|
|
1231
|
+
rx.cond(AdminState.runs_has_more,
|
|
1232
|
+
rx.button(rx.cond(AdminState.runs_loading, "Loading...",
|
|
1233
|
+
f"Load {RUNS_PAGE_SIZE} more"),
|
|
1234
|
+
variant="soft", width="100%",
|
|
1235
|
+
disabled=AdminState.runs_loading,
|
|
1236
|
+
on_click=AdminState.load_more_runs)),
|
|
1237
|
+
spacing="3", width="100%")
|
|
1238
|
+
return shell(rx.vstack(page_header("Runs", "Recent trigger executions and outcomes."),
|
|
1239
|
+
rx.cond(AdminState.runs.length() > 0, listing,
|
|
1240
|
+
empty_state("history", "No runs recorded yet.")),
|
|
1241
|
+
align="start", width="100%"))
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def workflow_warning(message: rx.Var) -> rx.Component:
|
|
1245
|
+
return rx.callout(
|
|
1246
|
+
message,
|
|
1247
|
+
icon="triangle-alert",
|
|
1248
|
+
color_scheme="amber",
|
|
1249
|
+
width="100%",
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
def edge_menu_item(skill: rx.Var) -> rx.Component:
|
|
1254
|
+
return rx.button(
|
|
1255
|
+
rx.icon("pencil", size=15),
|
|
1256
|
+
rx.text("Edit skill", font_weight="500"),
|
|
1257
|
+
rx.text(
|
|
1258
|
+
skill,
|
|
1259
|
+
color=MUTED,
|
|
1260
|
+
font_family="IBM Plex Mono, monospace",
|
|
1261
|
+
font_size="0.8rem",
|
|
1262
|
+
),
|
|
1263
|
+
variant="ghost",
|
|
1264
|
+
justify="start",
|
|
1265
|
+
width="100%",
|
|
1266
|
+
on_click=AdminState.edit_workflow_skill(skill),
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
def workflow_edge_menu() -> rx.Component:
|
|
1271
|
+
"""Context menu anchored to the last clicked transition arrow."""
|
|
1272
|
+
return rx.cond(
|
|
1273
|
+
AdminState.edge_menu_skills.length() > 0,
|
|
1274
|
+
rx.fragment(
|
|
1275
|
+
rx.box(
|
|
1276
|
+
position="fixed",
|
|
1277
|
+
inset="0",
|
|
1278
|
+
z_index="40",
|
|
1279
|
+
on_click=AdminState.close_edge_menu,
|
|
1280
|
+
),
|
|
1281
|
+
rx.vstack(
|
|
1282
|
+
rx.foreach(AdminState.edge_menu_skills, edge_menu_item),
|
|
1283
|
+
position="fixed",
|
|
1284
|
+
left=AdminState.edge_menu_left,
|
|
1285
|
+
top=AdminState.edge_menu_top,
|
|
1286
|
+
z_index="41",
|
|
1287
|
+
spacing="1",
|
|
1288
|
+
padding="0.3rem",
|
|
1289
|
+
min_width="12rem",
|
|
1290
|
+
background=SURFACE,
|
|
1291
|
+
border=BORDER,
|
|
1292
|
+
border_radius="6px",
|
|
1293
|
+
box_shadow="0 8px 24px rgba(0, 0, 0, 0.28)",
|
|
1294
|
+
),
|
|
1295
|
+
),
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def workflow_section(
|
|
1300
|
+
title: str,
|
|
1301
|
+
nodes: rx.Var,
|
|
1302
|
+
edges: rx.Var,
|
|
1303
|
+
warnings: rx.Var,
|
|
1304
|
+
) -> rx.Component:
|
|
1305
|
+
return rx.vstack(
|
|
1306
|
+
rx.heading(title, size="5"),
|
|
1307
|
+
rx.cond(
|
|
1308
|
+
nodes.length() > 0,
|
|
1309
|
+
rx.vstack(
|
|
1310
|
+
rx.cond(
|
|
1311
|
+
warnings.length() > 0,
|
|
1312
|
+
rx.vstack(
|
|
1313
|
+
rx.foreach(warnings, workflow_warning),
|
|
1314
|
+
spacing="3",
|
|
1315
|
+
width="100%",
|
|
1316
|
+
),
|
|
1317
|
+
),
|
|
1318
|
+
workflow_graph(
|
|
1319
|
+
nodes,
|
|
1320
|
+
edges,
|
|
1321
|
+
on_edge_click=AdminState.open_edge_menu,
|
|
1322
|
+
on_pane_click=AdminState.close_edge_menu,
|
|
1323
|
+
),
|
|
1324
|
+
spacing="4",
|
|
1325
|
+
width="100%",
|
|
1326
|
+
),
|
|
1327
|
+
empty_state(
|
|
1328
|
+
"git-branch",
|
|
1329
|
+
f"No {title.lower()} issue-trigger skills found.",
|
|
1330
|
+
),
|
|
1331
|
+
),
|
|
1332
|
+
spacing="4",
|
|
1333
|
+
align="start",
|
|
1334
|
+
width="100%",
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def workflow_page() -> rx.Component:
|
|
1339
|
+
return shell(rx.vstack(
|
|
1340
|
+
rx.flex(
|
|
1341
|
+
page_header(
|
|
1342
|
+
"Workflow", "Story and task transitions inferred from issue-trigger skills."),
|
|
1343
|
+
rx.spacer(),
|
|
1344
|
+
rx.button(
|
|
1345
|
+
rx.icon("refresh-cw", size=16),
|
|
1346
|
+
"Regenerate",
|
|
1347
|
+
variant="outline",
|
|
1348
|
+
loading=AdminState.workflow_loading,
|
|
1349
|
+
on_click=AdminState.load_workflow(True),
|
|
1350
|
+
),
|
|
1351
|
+
align="start",
|
|
1352
|
+
width="100%",
|
|
1353
|
+
),
|
|
1354
|
+
rx.cond(
|
|
1355
|
+
AdminState.workflow_loading,
|
|
1356
|
+
rx.center(rx.spinner(size="3"), min_height="48rem", width="100%"),
|
|
1357
|
+
rx.cond(
|
|
1358
|
+
AdminState.workflow_error != "",
|
|
1359
|
+
rx.callout(
|
|
1360
|
+
AdminState.workflow_error,
|
|
1361
|
+
icon="triangle-alert",
|
|
1362
|
+
color_scheme="red",
|
|
1363
|
+
width="100%",
|
|
1364
|
+
),
|
|
1365
|
+
rx.vstack(
|
|
1366
|
+
workflow_section(
|
|
1367
|
+
"Story workflow",
|
|
1368
|
+
AdminState.story_workflow_nodes,
|
|
1369
|
+
AdminState.story_workflow_edges,
|
|
1370
|
+
AdminState.story_workflow_warnings,
|
|
1371
|
+
),
|
|
1372
|
+
workflow_section(
|
|
1373
|
+
"Task workflow",
|
|
1374
|
+
AdminState.task_workflow_nodes,
|
|
1375
|
+
AdminState.task_workflow_edges,
|
|
1376
|
+
AdminState.task_workflow_warnings,
|
|
1377
|
+
),
|
|
1378
|
+
spacing="8",
|
|
1379
|
+
width="100%",
|
|
1380
|
+
),
|
|
1381
|
+
),
|
|
1382
|
+
),
|
|
1383
|
+
workflow_edge_menu(),
|
|
1384
|
+
align="start",
|
|
1385
|
+
width="100%",
|
|
1386
|
+
))
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def sessions_page() -> rx.Component:
|
|
1390
|
+
return shell(rx.vstack(
|
|
1391
|
+
page_header(
|
|
1392
|
+
"Sessions", "Open the configured Claude Code session viewer."),
|
|
1393
|
+
rx.cond(AdminState.session_viewer != "",
|
|
1394
|
+
rx.link(rx.button(rx.icon("external-link", size=16), "Open session viewer"),
|
|
1395
|
+
href=AdminState.session_viewer, is_external=True),
|
|
1396
|
+
empty_state("key-round", "Set CODEE_SESSION_VIEWER_URL to enable the session viewer.")),
|
|
1397
|
+
align="start", width="100%"))
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
def azure_step(number: int, title: str, detail: rx.Component | str) -> rx.Component:
|
|
1401
|
+
return rx.hstack(
|
|
1402
|
+
rx.box(str(number), color="white", background=ACCENT, min_width="1.4rem",
|
|
1403
|
+
height="1.4rem", display="grid", place_items="center",
|
|
1404
|
+
border_radius="50%", font_size="0.75rem", font_weight="700"),
|
|
1405
|
+
rx.vstack(rx.text(title, font_weight="600", font_size="0.9rem"),
|
|
1406
|
+
rx.text(detail, color=MUTED, font_size="0.85rem")
|
|
1407
|
+
if isinstance(detail, str) else detail,
|
|
1408
|
+
spacing="1", align="start", width="100%"),
|
|
1409
|
+
spacing="3", align="start", width="100%")
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def azure_redirect_uri_box() -> rx.Component:
|
|
1413
|
+
"""The redirect URI to register, copyable — Entra ID matches it character for character."""
|
|
1414
|
+
return rx.hstack(
|
|
1415
|
+
rx.input(value=AdminState.azure_redirect_uri, read_only=True,
|
|
1416
|
+
font_family="IBM Plex Mono, monospace", font_size="0.8rem",
|
|
1417
|
+
width="100%"),
|
|
1418
|
+
rx.button(rx.icon("copy", size=15), variant="outline", type="button",
|
|
1419
|
+
on_click=rx.set_clipboard(AdminState.azure_redirect_uri)),
|
|
1420
|
+
spacing="2", width="100%")
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
def azure_instructions() -> rx.Component:
|
|
1424
|
+
"""Collapsed by default: needed once, when the Entra app is first created."""
|
|
1425
|
+
return rx.accordion.root(
|
|
1426
|
+
rx.accordion.item(
|
|
1427
|
+
header=rx.text("How to create the Azure app registration",
|
|
1428
|
+
font_size="0.9rem", font_weight="600"),
|
|
1429
|
+
content=rx.vstack(
|
|
1430
|
+
azure_step(1, "Register the app",
|
|
1431
|
+
"Azure portal → Microsoft Entra ID → App registrations → "
|
|
1432
|
+
"New registration. Name it Codee. Use the directory that "
|
|
1433
|
+
"backs your Azure DevOps organization."),
|
|
1434
|
+
azure_step(2, "Add a Web redirect URI",
|
|
1435
|
+
rx.vstack(
|
|
1436
|
+
rx.text("Platform Web — not SPA, because Codee exchanges the "
|
|
1437
|
+
"code on the server with the client secret.",
|
|
1438
|
+
color=MUTED, font_size="0.85rem"),
|
|
1439
|
+
azure_redirect_uri_box(),
|
|
1440
|
+
spacing="2", width="100%")),
|
|
1441
|
+
azure_step(3, "Grant the Azure DevOps permission",
|
|
1442
|
+
"API permissions → Add a permission → Azure DevOps → Delegated "
|
|
1443
|
+
"→ user_impersonation → Add. Entra publishes no read-only scope "
|
|
1444
|
+
"for Azure DevOps; Codee only ever issues read calls, and you can "
|
|
1445
|
+
"narrow it further by connecting with an account that has Readers "
|
|
1446
|
+
"access to the project."),
|
|
1447
|
+
azure_step(4, "Create a client secret",
|
|
1448
|
+
"Certificates & secrets → New client secret. Copy the Value "
|
|
1449
|
+
"column immediately — Azure hides it once you leave the page."),
|
|
1450
|
+
azure_step(5, "Copy the app identifiers",
|
|
1451
|
+
"From Overview: Application (client) ID and Directory (tenant) ID."),
|
|
1452
|
+
azure_step(6, "Fill the fields below and connect",
|
|
1453
|
+
"Connecting sends you to Microsoft to sign in. Codee stores the "
|
|
1454
|
+
"resulting access and refresh tokens and renews them on its own."),
|
|
1455
|
+
spacing="4", padding="1rem 0.25rem", width="100%"),
|
|
1456
|
+
),
|
|
1457
|
+
type="single", collapsible=True, variant="ghost", width="100%")
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
def azure_connection_status() -> rx.Component:
|
|
1461
|
+
return rx.cond(
|
|
1462
|
+
AdminState.azure_connected,
|
|
1463
|
+
rx.hstack(
|
|
1464
|
+
rx.icon("circle-check", size=17, color=ACCENT),
|
|
1465
|
+
rx.vstack(
|
|
1466
|
+
rx.text(rx.cond(AdminState.azure_account != "",
|
|
1467
|
+
f"Connected as {AdminState.azure_account}",
|
|
1468
|
+
"Connected to Azure DevOps"),
|
|
1469
|
+
font_weight="600", font_size="0.9rem"),
|
|
1470
|
+
rx.text(AdminState.azure_expires_label,
|
|
1471
|
+
color=MUTED, font_size="0.8rem"),
|
|
1472
|
+
spacing="1", align="start"),
|
|
1473
|
+
rx.spacer(),
|
|
1474
|
+
rx.button("Disconnect", variant="outline", color_scheme="red",
|
|
1475
|
+
type="button", on_click=AdminState.disconnect_azure_devops),
|
|
1476
|
+
align="center", width="100%"),
|
|
1477
|
+
rx.callout("Not connected. Fill in the app details, then connect.",
|
|
1478
|
+
icon="info", size="1", color_scheme="gray", width="100%"),
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def azure_fields() -> rx.Component:
|
|
1483
|
+
return rx.vstack(
|
|
1484
|
+
azure_instructions(),
|
|
1485
|
+
field("Organization URL", rx.input(value=AdminState.azure_organization_url,
|
|
1486
|
+
on_change=AdminState.set_azure_organization_url,
|
|
1487
|
+
placeholder="https://dev.azure.com/your-org",
|
|
1488
|
+
width="100%")),
|
|
1489
|
+
field("Project", rx.input(value=AdminState.azure_project,
|
|
1490
|
+
on_change=AdminState.set_azure_project, width="100%")),
|
|
1491
|
+
field("Application (client) ID", rx.input(value=AdminState.azure_client_id,
|
|
1492
|
+
on_change=AdminState.set_azure_client_id,
|
|
1493
|
+
width="100%")),
|
|
1494
|
+
field("Client secret", rx.input(value=AdminState.azure_client_secret,
|
|
1495
|
+
on_change=AdminState.set_azure_client_secret,
|
|
1496
|
+
type="password", width="100%")),
|
|
1497
|
+
field("Directory (tenant) ID", rx.input(value=AdminState.azure_tenant_id,
|
|
1498
|
+
on_change=AdminState.set_azure_tenant_id,
|
|
1499
|
+
width="100%"),
|
|
1500
|
+
hint=rx.text("Optional. Leave empty to sign in against any work or school "
|
|
1501
|
+
"directory you belong to.", color=MUTED, font_size="0.8rem")),
|
|
1502
|
+
azure_connection_status(),
|
|
1503
|
+
rx.button(rx.icon("plug", size=16),
|
|
1504
|
+
rx.cond(AdminState.azure_connected,
|
|
1505
|
+
"Reconnect to Azure DevOps", "Connect to Azure DevOps"),
|
|
1506
|
+
type="button", disabled=~AdminState.azure_can_connect,
|
|
1507
|
+
on_click=AdminState.connect_azure_devops),
|
|
1508
|
+
spacing="4", width="100%")
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
def settings_page() -> rx.Component:
|
|
1512
|
+
jira_fields = rx.vstack(
|
|
1513
|
+
field("Base URL", rx.input(value=AdminState.jira_base_url,
|
|
1514
|
+
on_change=AdminState.set_jira_base_url, width="100%")),
|
|
1515
|
+
field("Account email", rx.input(value=AdminState.jira_account_email,
|
|
1516
|
+
on_change=AdminState.set_jira_account_email, width="100%")),
|
|
1517
|
+
field("API token", rx.input(value=AdminState.jira_api_token,
|
|
1518
|
+
on_change=AdminState.set_jira_api_token, type="password", width="100%")),
|
|
1519
|
+
field("Project key", rx.input(value=AdminState.jira_project,
|
|
1520
|
+
on_change=AdminState.set_jira_project, width="100%")),
|
|
1521
|
+
spacing="4", width="100%")
|
|
1522
|
+
return shell(rx.vstack(
|
|
1523
|
+
page_header(
|
|
1524
|
+
"Settings", "Choose providers and control executor concurrency."),
|
|
1525
|
+
rx.box(
|
|
1526
|
+
rx.heading("Coding agent", size="4", margin_bottom="1rem"),
|
|
1527
|
+
rx.grid(
|
|
1528
|
+
field("Agent", rx.select(["claude_code", "github_copilot"], value=AdminState.coding_agent,
|
|
1529
|
+
on_change=AdminState.set_coding_agent, width="100%")),
|
|
1530
|
+
field("Max parallel tasks", rx.input(value=AdminState.max_parallel_agents,
|
|
1531
|
+
on_change=AdminState.set_max_parallel_agents,
|
|
1532
|
+
type="number", min=1, width="100%")),
|
|
1533
|
+
columns=rx.breakpoints(initial="1", md="2"), gap="1rem", width="100%"),
|
|
1534
|
+
padding="1.25rem", background=SURFACE, border=BORDER, width="100%"),
|
|
1535
|
+
rx.box(
|
|
1536
|
+
rx.heading("Tasks provider", size="4", margin_bottom="1rem"),
|
|
1537
|
+
field("Provider", rx.select(["jira", "azure_devops"], value=AdminState.tasks_provider,
|
|
1538
|
+
on_change=AdminState.set_tasks_provider, width="100%")),
|
|
1539
|
+
rx.box(rx.cond(AdminState.tasks_provider == "jira",
|
|
1540
|
+
jira_fields, azure_fields()), margin_top="1rem"),
|
|
1541
|
+
padding="1.25rem", background=SURFACE, border=BORDER, width="100%"),
|
|
1542
|
+
rx.button(rx.icon("save", size=16), "Save settings",
|
|
1543
|
+
on_click=AdminState.save_settings),
|
|
1544
|
+
spacing="5", align="start", width="100%"))
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
app = rx.App(
|
|
1548
|
+
style={
|
|
1549
|
+
"button:not(:disabled), [role='button']:not([aria-disabled='true'])": {
|
|
1550
|
+
"cursor": "pointer",
|
|
1551
|
+
},
|
|
1552
|
+
# Expanding halo behind the live dot on in-flight runs.
|
|
1553
|
+
"@keyframes codee-ping": {
|
|
1554
|
+
"0%": {"transform": "scale(1)", "opacity": "0.55"},
|
|
1555
|
+
"70%": {"transform": "scale(2.6)", "opacity": "0"},
|
|
1556
|
+
"100%": {"transform": "scale(2.6)", "opacity": "0"},
|
|
1557
|
+
},
|
|
1558
|
+
"@keyframes codee-breathe": {
|
|
1559
|
+
"0%, 100%": {"opacity": "1"},
|
|
1560
|
+
"50%": {"opacity": "0.55"},
|
|
1561
|
+
},
|
|
1562
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
1563
|
+
".codee-live-dot > *, .codee-breathe": {
|
|
1564
|
+
"animation": "none !important",
|
|
1565
|
+
},
|
|
1566
|
+
},
|
|
1567
|
+
"button:disabled, [role='button'][aria-disabled='true']": {
|
|
1568
|
+
"cursor": "not-allowed",
|
|
1569
|
+
},
|
|
1570
|
+
".rt-TextFieldRoot": {
|
|
1571
|
+
"background": "var(--codee-surface) !important",
|
|
1572
|
+
"color": "var(--codee-text) !important",
|
|
1573
|
+
"box_shadow": "inset 0 0 0 1px var(--codee-border)",
|
|
1574
|
+
},
|
|
1575
|
+
".rt-TextFieldInput": {"color": "var(--codee-text) !important"},
|
|
1576
|
+
".rt-TextAreaRoot": {
|
|
1577
|
+
"background": "var(--codee-surface) !important",
|
|
1578
|
+
"color": "var(--codee-text) !important",
|
|
1579
|
+
},
|
|
1580
|
+
".rt-SelectTrigger": {
|
|
1581
|
+
"background": "var(--codee-surface) !important",
|
|
1582
|
+
"color": "var(--codee-text) !important",
|
|
1583
|
+
"box_shadow": "inset 0 0 0 1px var(--codee-border)",
|
|
1584
|
+
},
|
|
1585
|
+
".workflow-node": {
|
|
1586
|
+
"background": "var(--codee-surface)",
|
|
1587
|
+
"border": "1px solid var(--codee-border)",
|
|
1588
|
+
"border_radius": "6px",
|
|
1589
|
+
"color": "var(--codee-text)",
|
|
1590
|
+
"font_family": "IBM Plex Sans, sans-serif",
|
|
1591
|
+
"font_weight": "600",
|
|
1592
|
+
"min_width": "220px",
|
|
1593
|
+
"padding": "0.85rem 1rem",
|
|
1594
|
+
},
|
|
1595
|
+
".workflow-node--disconnected": {
|
|
1596
|
+
"background": "var(--codee-warning-background)",
|
|
1597
|
+
"border": "2px solid var(--codee-warning-border)",
|
|
1598
|
+
},
|
|
1599
|
+
# No `position` here: React Flow places nodes with `position: absolute`
|
|
1600
|
+
# and a bare `transform`, so overriding it drops the node into normal
|
|
1601
|
+
# flow and shifts every sibling's static position.
|
|
1602
|
+
".workflow-node--unhandled": {
|
|
1603
|
+
"background": "var(--codee-warning-background)",
|
|
1604
|
+
"border": "2px dashed var(--codee-warning-border)",
|
|
1605
|
+
},
|
|
1606
|
+
".workflow-node--unhandled::after": {
|
|
1607
|
+
"content": "'No issue trigger skill for this status'",
|
|
1608
|
+
"position": "absolute",
|
|
1609
|
+
"bottom": "calc(100% + 8px)",
|
|
1610
|
+
"left": "50%",
|
|
1611
|
+
"transform": "translateX(-50%)",
|
|
1612
|
+
"background": "var(--codee-surface)",
|
|
1613
|
+
"border": "1px solid var(--codee-warning-border)",
|
|
1614
|
+
"border_radius": "4px",
|
|
1615
|
+
"box_shadow": "0 8px 24px rgba(0, 0, 0, 0.28)",
|
|
1616
|
+
"color": "var(--codee-text)",
|
|
1617
|
+
"font_family": "IBM Plex Sans, sans-serif",
|
|
1618
|
+
"font_size": "0.75rem",
|
|
1619
|
+
"font_weight": "500",
|
|
1620
|
+
"padding": "0.35rem 0.55rem",
|
|
1621
|
+
"white_space": "nowrap",
|
|
1622
|
+
"opacity": "0",
|
|
1623
|
+
"pointer_events": "none",
|
|
1624
|
+
"transition": "opacity 0.12s ease",
|
|
1625
|
+
"z_index": "5",
|
|
1626
|
+
},
|
|
1627
|
+
".workflow-node--unhandled:hover::after": {"opacity": "1"},
|
|
1628
|
+
".react-flow__edge.workflow-edge": {
|
|
1629
|
+
"cursor": "pointer",
|
|
1630
|
+
},
|
|
1631
|
+
".workflow-route-node .react-flow__handle": {
|
|
1632
|
+
"border": "0",
|
|
1633
|
+
"border_radius": "0",
|
|
1634
|
+
"height": "2px",
|
|
1635
|
+
"left": "50%",
|
|
1636
|
+
"min_height": "2px",
|
|
1637
|
+
"min_width": "6px",
|
|
1638
|
+
"right": "auto",
|
|
1639
|
+
"transform": "translate(-50%, -50%)",
|
|
1640
|
+
"width": "6px",
|
|
1641
|
+
},
|
|
1642
|
+
".workflow-route-node--forward .react-flow__handle": {
|
|
1643
|
+
"background": "#167d5a",
|
|
1644
|
+
},
|
|
1645
|
+
".workflow-route-node--return .react-flow__handle": {
|
|
1646
|
+
"background": "#d97706",
|
|
1647
|
+
},
|
|
1648
|
+
},
|
|
1649
|
+
stylesheets=[
|
|
1650
|
+
"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap"
|
|
1651
|
+
],
|
|
1652
|
+
head_components=[
|
|
1653
|
+
rx.el.link(rel="icon", type="image/svg+xml", href=FAVICON),
|
|
1654
|
+
],
|
|
1655
|
+
# Serves the OAuth callback route on the same origin as the UI; Reflex
|
|
1656
|
+
# mounts itself underneath, so every other path still reaches the pages.
|
|
1657
|
+
api_transformer=api_app,
|
|
1658
|
+
)
|
|
1659
|
+
app.add_page(dashboard_page, route="/", title="Dashboard | Codee",
|
|
1660
|
+
on_load=AdminState.poll_dashboard)
|
|
1661
|
+
app.add_page(skills_page, route="/skills", title="Skills | Codee",
|
|
1662
|
+
on_load=[AdminState.load_skills, AdminState.load_agent_models])
|
|
1663
|
+
app.add_page(workflow_page, route="/workflow", title="Workflow | Codee",
|
|
1664
|
+
on_load=AdminState.load_workflow)
|
|
1665
|
+
app.add_page(memory_page, route="/memory", title="Memory | Codee",
|
|
1666
|
+
on_load=AdminState.load_memories)
|
|
1667
|
+
app.add_page(runs_page, route="/runs", title="Runs | Codee",
|
|
1668
|
+
on_load=AdminState.load_runs)
|
|
1669
|
+
app.add_page(sessions_page, route="/sessions",
|
|
1670
|
+
title="Sessions | Codee", on_load=AdminState.load_settings)
|
|
1671
|
+
app.add_page(settings_page, route="/settings",
|
|
1672
|
+
title="Settings | Codee", on_load=AdminState.load_settings)
|