flock-core 0.4.0b34__py3-none-any.whl → 0.4.0b36__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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (63) hide show
  1. flock/__init__.py +53 -8
  2. flock/cli/loaded_flock_cli.py +38 -18
  3. flock/core/api/main.py +138 -39
  4. flock/core/flock.py +48 -3
  5. flock/themes/alabaster.toml +43 -43
  6. flock/themes/guezwhoz.toml +43 -43
  7. flock/themes/wildcherry.toml +43 -43
  8. flock/themes/wombat.toml +43 -43
  9. flock/themes/zenburn.toml +43 -43
  10. flock/webapp/__init__.py +1 -0
  11. flock/webapp/app/__init__.py +0 -0
  12. flock/webapp/app/api/__init__.py +0 -0
  13. flock/webapp/app/api/agent_management.py +270 -0
  14. flock/webapp/app/api/execution.py +173 -0
  15. flock/webapp/app/api/flock_management.py +102 -0
  16. flock/webapp/app/api/registry_viewer.py +30 -0
  17. flock/webapp/app/config.py +87 -0
  18. flock/webapp/app/main.py +1074 -0
  19. flock/webapp/app/models_ui.py +7 -0
  20. flock/webapp/app/services/__init__.py +0 -0
  21. flock/webapp/app/services/flock_service.py +291 -0
  22. flock/webapp/app/templates/theme_mapper.html +326 -0
  23. flock/webapp/app/theme_mapper.py +812 -0
  24. flock/webapp/app/utils.py +85 -0
  25. flock/webapp/run.py +132 -0
  26. flock/webapp/static/css/custom.css +612 -0
  27. flock/webapp/templates/base.html +105 -0
  28. flock/webapp/templates/flock_editor.html +17 -0
  29. flock/webapp/templates/index.html +12 -0
  30. flock/webapp/templates/partials/_agent_detail_form.html +98 -0
  31. flock/webapp/templates/partials/_agent_list.html +19 -0
  32. flock/webapp/templates/partials/_agent_manager_view.html +53 -0
  33. flock/webapp/templates/partials/_agent_manager_view_old.html +19 -0
  34. flock/webapp/templates/partials/_agent_tools_checklist.html +14 -0
  35. flock/webapp/templates/partials/_create_flock_form.html +52 -0
  36. flock/webapp/templates/partials/_dashboard_flock_detail.html +18 -0
  37. flock/webapp/templates/partials/_dashboard_flock_file_list.html +17 -0
  38. flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +29 -0
  39. flock/webapp/templates/partials/_dashboard_upload_flock_form.html +17 -0
  40. flock/webapp/templates/partials/_dynamic_input_form_content.html +22 -0
  41. flock/webapp/templates/partials/_env_vars_table.html +25 -0
  42. flock/webapp/templates/partials/_execution_form.html +48 -0
  43. flock/webapp/templates/partials/_execution_view_container.html +20 -0
  44. flock/webapp/templates/partials/_flock_file_list.html +24 -0
  45. flock/webapp/templates/partials/_flock_properties_form.html +52 -0
  46. flock/webapp/templates/partials/_flock_upload_form.html +17 -0
  47. flock/webapp/templates/partials/_header_flock_status.html +5 -0
  48. flock/webapp/templates/partials/_load_manager_view.html +50 -0
  49. flock/webapp/templates/partials/_registry_table.html +25 -0
  50. flock/webapp/templates/partials/_registry_viewer_content.html +47 -0
  51. flock/webapp/templates/partials/_results_display.html +35 -0
  52. flock/webapp/templates/partials/_settings_env_content.html +10 -0
  53. flock/webapp/templates/partials/_settings_theme_content.html +15 -0
  54. flock/webapp/templates/partials/_settings_view.html +36 -0
  55. flock/webapp/templates/partials/_sidebar.html +70 -0
  56. flock/webapp/templates/partials/_structured_data_view.html +40 -0
  57. flock/webapp/templates/partials/_theme_preview.html +23 -0
  58. flock/webapp/templates/registry_viewer.html +84 -0
  59. {flock_core-0.4.0b34.dist-info → flock_core-0.4.0b36.dist-info}/METADATA +1 -1
  60. {flock_core-0.4.0b34.dist-info → flock_core-0.4.0b36.dist-info}/RECORD +63 -14
  61. {flock_core-0.4.0b34.dist-info → flock_core-0.4.0b36.dist-info}/WHEEL +0 -0
  62. {flock_core-0.4.0b34.dist-info → flock_core-0.4.0b36.dist-info}/entry_points.txt +0 -0
  63. {flock_core-0.4.0b34.dist-info → flock_core-0.4.0b36.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1074 @@
1
+ # ... (keep existing imports and app setup) ...
2
+ import json
3
+ import os # Needed for environment variable helpers
4
+ import shutil
5
+ import sys # For path
6
+ import urllib.parse
7
+ from pathlib import Path
8
+
9
+ from fastapi import FastAPI, File, Form, Query, Request, UploadFile
10
+ from fastapi.responses import HTMLResponse, RedirectResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+ from fastapi.templating import Jinja2Templates
13
+
14
+ from flock.webapp.app.api import (
15
+ agent_management,
16
+ execution,
17
+ flock_management,
18
+ registry_viewer,
19
+ )
20
+
21
+ # Import config functions
22
+ from flock.webapp.app.config import (
23
+ DEFAULT_THEME_NAME, # Import default for fallback
24
+ FLOCK_FILES_DIR,
25
+ THEMES_DIR, # Import THEMES_DIR from config
26
+ get_current_theme_name,
27
+ # set_current_theme_name, # Not directly used in main.py, but available
28
+ )
29
+ from flock.webapp.app.services.flock_service import (
30
+ clear_current_flock,
31
+ create_new_flock_service,
32
+ get_available_flock_files,
33
+ get_current_flock_filename,
34
+ get_current_flock_instance,
35
+ get_flock_preview_service,
36
+ load_flock_from_file_service,
37
+ )
38
+ from flock.webapp.app.theme_mapper import alacritty_to_pico
39
+
40
+ # Helper for theme loading
41
+
42
+ # Find the 'src/flock' directory - This can be removed if THEMES_DIR from config is sufficient
43
+ # flock_base_dir = (
44
+ # Path(__file__).resolve().parent.parent.parent
45
+ # ) # src/flock/webapp/app -> src/flock
46
+
47
+ # Calculate themes directory relative to the flock base dir - This can be removed
48
+ # themes_dir = flock_base_dir / "themes"
49
+
50
+ # Ensure the parent ('src') is in the path for core imports
51
+ # This path manipulation might still be needed if core imports are relative in a specific way
52
+ flock_webapp_dir = Path(__file__).resolve().parent.parent # src/flock/webapp/
53
+ flock_base_dir = flock_webapp_dir.parent # src/flock/
54
+ src_dir = flock_base_dir.parent # src/
55
+ if str(src_dir) not in sys.path:
56
+ sys.path.insert(0, str(src_dir))
57
+
58
+ try:
59
+ from flock.core.logging.formatters.themed_formatter import (
60
+ load_theme_from_file,
61
+ )
62
+
63
+ THEME_LOADER_AVAILABLE = True
64
+ # themes_dir is now imported from config
65
+ except ImportError:
66
+ print(
67
+ "Warning: Could not import flock.core theme loading utilities.",
68
+ file=sys.stderr,
69
+ )
70
+ THEME_LOADER_AVAILABLE = False
71
+ # THEMES_DIR will be None if not imported, or its value from config
72
+
73
+ # --- Lightweight .env helpers (self-contained, no external deps) ---
74
+ ENV_FILE = ".env"
75
+ SHOW_SECRETS_KEY = "SHOW_SECRETS"
76
+
77
+ def load_env_file() -> dict[str, str]:
78
+ env_vars: dict[str, str] = {}
79
+ if not os.path.exists(ENV_FILE):
80
+ return env_vars
81
+ with open(ENV_FILE) as f:
82
+ lines = f.readlines()
83
+ for line in lines:
84
+ line = line.strip()
85
+ if not line:
86
+ env_vars[""] = ""
87
+ continue
88
+ if line.startswith("#"):
89
+ env_vars[line] = ""
90
+ continue
91
+ if "=" in line:
92
+ k, v = line.split("=", 1)
93
+ env_vars[k] = v
94
+ else:
95
+ env_vars[line] = ""
96
+ return env_vars
97
+
98
+ def save_env_file(env_vars: dict[str, str]):
99
+ try:
100
+ with open(ENV_FILE, "w") as f:
101
+ for k, v in env_vars.items():
102
+ if k.startswith("#"):
103
+ f.write(f"{k}\n")
104
+ elif not k:
105
+ f.write("\n")
106
+ else:
107
+ f.write(f"{k}={v}\n")
108
+ except Exception as e:
109
+ print(f"[Settings] Failed to save .env: {e}")
110
+
111
+ def is_sensitive(key: str) -> bool:
112
+ patterns = ["key", "token", "secret", "password", "api", "pat"]
113
+ low = key.lower()
114
+ return any(p in low for p in patterns)
115
+
116
+ def mask_sensitive_value(value: str) -> str:
117
+ if not value:
118
+ return value
119
+ if len(value) <= 4:
120
+ return "••••"
121
+ return value[:2] + "•" * (len(value) - 4) + value[-2:]
122
+
123
+ def get_show_secrets_setting(env_vars: dict[str, str]) -> bool:
124
+ return env_vars.get(SHOW_SECRETS_KEY, "false").lower() == "true"
125
+
126
+ def set_show_secrets_setting(show: bool):
127
+ env_vars = load_env_file()
128
+ env_vars[SHOW_SECRETS_KEY] = str(show)
129
+ save_env_file(env_vars)
130
+
131
+ # -------------------------------------------------------------------
132
+
133
+ app = FastAPI(title="Flock UI")
134
+
135
+ BASE_DIR = Path(__file__).resolve().parent.parent
136
+ app.mount(
137
+ "/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static"
138
+ )
139
+ templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
140
+
141
+ app.include_router(
142
+ flock_management.router, prefix="/api/flocks", tags=["Flock Management API"]
143
+ )
144
+ app.include_router(
145
+ agent_management.router, prefix="/api/flocks", tags=["Agent Management API"]
146
+ )
147
+ # Ensure execution router is imported and included BEFORE it's referenced by the renamed route
148
+ app.include_router(
149
+ execution.router, prefix="/api/flocks", tags=["Execution API"]
150
+ )
151
+ app.include_router(
152
+ registry_viewer.router, prefix="/api/registry", tags=["Registry API"]
153
+ )
154
+
155
+
156
+ def generate_theme_css(theme_name: str | None) -> str:
157
+ """Loads a theme TOML and generates CSS variable overrides."""
158
+ if not THEME_LOADER_AVAILABLE or THEMES_DIR is None: # Use imported THEMES_DIR
159
+ return "" # Return empty if theme loading isn't possible
160
+
161
+ active_theme_name = theme_name or DEFAULT_THEME_NAME
162
+ theme_filename = f"{active_theme_name}.toml"
163
+ theme_path = THEMES_DIR / theme_filename # Use imported THEMES_DIR
164
+
165
+ if not theme_path.exists():
166
+ print(
167
+ f"Warning: Theme file not found: {theme_path}. Using default theme.",
168
+ file=sys.stderr,
169
+ )
170
+ # Optionally load the default theme file if the requested one isn't found
171
+ theme_filename = f"{DEFAULT_THEME_NAME}.toml"
172
+ theme_path = THEMES_DIR / theme_filename
173
+ if not theme_path.exists():
174
+ print(
175
+ f"Warning: Default theme file not found: {theme_path}. No theme CSS generated.",
176
+ file=sys.stderr,
177
+ )
178
+ return "" # Return empty if even default isn't found
179
+ active_theme_name = (
180
+ DEFAULT_THEME_NAME # Update active name if defaulted
181
+ )
182
+
183
+ try:
184
+ theme_dict = load_theme_from_file(str(theme_path))
185
+ except Exception as e:
186
+ print(f"Error loading theme file {theme_path}: {e}", file=sys.stderr)
187
+ return "" # Return empty on error
188
+
189
+ # --- Define TOML Color -> CSS Variable Mapping ---
190
+ # This mapping is crucial and may need adjustment based on theme intent & Pico usage
191
+ css_vars = {}
192
+ try:
193
+ # Basic Colors
194
+ # Base colors
195
+ css_vars["--pico-background-color"] = theme_dict["colors"]["primary"].get("background") # Main background
196
+ css_vars["--pico-color"] = theme_dict["colors"]["primary"].get("foreground") # Main text
197
+
198
+ # Headings
199
+ css_vars["--pico-h1-color"] = theme_dict["colors"]["selection"].get("text") # Primary heading
200
+ css_vars["--pico-h2-color"] = theme_dict["colors"]["selection"].get("text") # Secondary heading
201
+ css_vars["--pico-h3-color"] = theme_dict["colors"]["primary"].get("foreground") # Body heading
202
+ css_vars["--pico-muted-color"] = theme_dict["colors"]["selection"].get("text") # Muted/subtext
203
+ css_vars["--pico-primary-inverse"] = theme_dict["colors"]["cursor"].get("text") # Contrast on primary
204
+ css_vars["--pico-contrast"] = theme_dict["colors"]["primary"].get("background") # Contrast text on dark
205
+ css_vars["--pico-contrast-inverse"] = theme_dict["colors"]["primary"].get("foreground") # Contrast text on dark
206
+
207
+ # Primary interaction
208
+ css_vars["--pico-primary"] = theme_dict["colors"]["normal"].get("blue")
209
+ css_vars["--pico-primary-hover"] = theme_dict["colors"]["bright"].get("blue")
210
+ css_vars["--pico-primary-focus"] = f"rgba({theme_dict['colors']['bright'].get('blue')}, 0.25)"
211
+ css_vars["--pico-primary-active"] = theme_dict["colors"]["normal"].get("blue")
212
+
213
+
214
+
215
+ # Secondary interaction
216
+ css_vars["--pico-secondary"] = theme_dict["colors"]["normal"].get("magenta")
217
+ css_vars["--pico-secondary-hover"] = theme_dict["colors"]["bright"].get("magenta")
218
+ css_vars["--pico-secondary-focus"] = f"rgba({theme_dict['colors']['bright'].get('magenta')}, 0.25)"
219
+ css_vars["--pico-secondary-active"] = theme_dict["colors"]["normal"].get("magenta")
220
+
221
+ # Cards and containers
222
+ css_vars["--pico-card-background-color"] = theme_dict["colors"]["primary"].get("background")
223
+ css_vars["--pico-card-border-color"] = theme_dict["colors"]["bright"].get("black") # Mid-tone, visible on bright bg
224
+ css_vars["--pico-card-sectioning-background-color"] = theme_dict["colors"]["selection"].get("background") # Subtle contrast
225
+ css_vars["--pico-border-color"] = theme_dict["colors"]["bright"].get("black")
226
+ css_vars["--pico-muted-border-color"] = theme_dict["colors"]["normal"].get("black") # More subtle than main border
227
+
228
+ # Forms
229
+ css_vars["--pico-form-element-background-color"] = theme_dict["colors"]["primary"].get("background")
230
+ css_vars["--pico-form-element-border-color"] = theme_dict["colors"]["bright"].get("black")
231
+ css_vars["--pico-form-element-color"] = theme_dict["colors"]["primary"].get("foreground")
232
+ css_vars["--pico-form-element-focus-color"] = theme_dict["colors"]["bright"].get("blue")
233
+ css_vars["--pico-form-element-placeholder-color"] = theme_dict["colors"]["bright"].get("black")
234
+ css_vars["--pico-form-element-active-border-color"] = theme_dict["colors"]["bright"].get("blue")
235
+ css_vars["--pico-form-element-active-background-color"] = theme_dict["colors"]["selection"].get("background")
236
+ css_vars["--pico-form-element-disabled-background-color"] = theme_dict["colors"]["normal"].get("black")
237
+ css_vars["--pico-form-element-disabled-border-color"] = theme_dict["colors"]["bright"].get("black")
238
+ css_vars["--pico-form-element-invalid-border-color"] = theme_dict["colors"]["normal"].get("red")
239
+ css_vars["--pico-form-element-invalid-focus-color"] = theme_dict["colors"]["bright"].get("red")
240
+
241
+ # Buttons
242
+ css_vars["--pico-button-base-background-color"] = theme_dict["colors"]["primary"].get("background")
243
+ css_vars["--pico-button-base-color"] = theme_dict["colors"]["primary"].get("foreground")
244
+ css_vars["--pico-button-hover-background-color"] = theme_dict["colors"]["selection"].get("background")
245
+ css_vars["--pico-button-hover-color"] = theme_dict["colors"]["selection"].get("text")
246
+
247
+ # Code blocks
248
+ css_vars["--pico-code-background-color"] = theme_dict["colors"]["cursor"].get("text") # Background behind code
249
+ css_vars["--pico-code-color"] = theme_dict["colors"]["primary"].get("foreground") # Code text
250
+ css_vars["--pico-code-kbd-background-color"] = theme_dict["colors"]["selection"].get("background")
251
+ css_vars["--pico-code-kbd-color"] = theme_dict["colors"]["selection"].get("text")
252
+ css_vars["--pico-code-tag-color"] = theme_dict["colors"]["normal"].get("blue") # Tag elements
253
+ css_vars["--pico-code-property-color"] = theme_dict["colors"]["normal"].get("green") # CSS property names
254
+ css_vars["--pico-code-value-color"] = theme_dict["colors"]["normal"].get("red") # Values and literals
255
+ css_vars["--pico-code-comment-color"] = theme_dict["colors"]["bright"].get("black") # Dim comment color
256
+
257
+
258
+ # Semantic markup
259
+ css_vars["--pico-mark-background-color"] = theme_dict["colors"]["normal"].get("yellow") + "33"
260
+ css_vars["--pico-mark-color"] = theme_dict["colors"]["primary"].get("foreground")
261
+ css_vars["--pico-ins-color"] = theme_dict["colors"]["normal"].get("green")
262
+ css_vars["--pico-del-color"] = theme_dict["colors"]["normal"].get("red")
263
+ # Deleted content - red
264
+ # Custom flock vars (mapped previously, ensure they are kept)
265
+ css_vars["--flock-sidebar-background"] = theme_dict["colors"]["primary"].get("background")# css_vars["--pico-card-background-color"] # Example: Link sidebar to card background
266
+ css_vars["--flock-header-background"] = theme_dict["colors"]["selection"].get("background") # Example: Link header to card background
267
+ css_vars["--flock-error-color"] = theme_dict["colors"]["normal"].get("red", "#dc3545")
268
+ css_vars["--flock-success-color"] = theme_dict["colors"]["normal"].get("green", "#28a745")
269
+
270
+
271
+
272
+ #css_vars.update(pico_vars)
273
+
274
+ except KeyError as e:
275
+ print(
276
+ f"Warning: Missing expected key in theme '{active_theme_name}': {e}. CSS may be incomplete.",
277
+ file=sys.stderr,
278
+ )
279
+
280
+ if not css_vars:
281
+ return "" # Return empty if no variables were mapped
282
+
283
+ pico_vars = alacritty_to_pico(theme_dict)
284
+ css_rules = [f" {name}: {value};" for name, value in pico_vars.items()]
285
+
286
+ # Apply overrides within the currently active theme selector for better specificity
287
+ # We could get the theme name passed in, or maybe check the <html> tag attribute?
288
+ # For simplicity, let's assume we want to override Pico's dark theme vars when a theme is loaded.
289
+ # A better approach might involve removing data-theme="dark" and applying theme to :root
290
+ # or having specific data-theme selectors for each flock theme.
291
+ # Let's try applying to [data-theme="dark"] first.
292
+ selector = '[data-theme="dark"]'
293
+ css_string = ":root {\n" + "\n".join(css_rules) + "\n}"
294
+
295
+ print(
296
+ f"--- Generated CSS for theme '{active_theme_name}' ---"
297
+ ) # Debugging print
298
+ print(css_string) # Debugging print
299
+ print(
300
+ "----------------------------------------------------"
301
+ ) # Debugging print
302
+ return css_string
303
+
304
+
305
+ def get_base_context(
306
+ request: Request,
307
+ error: str = None,
308
+ success: str = None,
309
+ ui_mode: str = "standalone",
310
+ ) -> dict:
311
+ theme_name = get_current_theme_name() # Get theme from config
312
+ theme_css = generate_theme_css(theme_name)
313
+ return {
314
+ "request": request,
315
+ "current_flock": get_current_flock_instance(),
316
+ "current_filename": get_current_flock_filename(),
317
+ "error_message": error,
318
+ "success_message": success,
319
+ "ui_mode": ui_mode,
320
+ "theme_css": theme_css, # Add generated CSS to context
321
+ "active_theme_name": theme_name, # Added active theme name
322
+ }
323
+
324
+
325
+ # --- Main Page Routes ---
326
+ @app.get("/", response_class=HTMLResponse)
327
+ async def page_dashboard(
328
+ request: Request,
329
+ error: str = None,
330
+ success: str = None,
331
+ ui_mode: str = Query(
332
+ None
333
+ ), # Default to None to detect if it was explicitly passed
334
+ ):
335
+ # Determine effective ui_mode
336
+ effective_ui_mode = ui_mode
337
+ flock_is_preloaded = get_current_flock_instance() is not None
338
+
339
+ if effective_ui_mode is None: # ui_mode not in query parameters
340
+ if flock_is_preloaded:
341
+ # If a flock is preloaded (likely by API server) and no mode specified,
342
+ # default to scoped and redirect to make the URL explicit.
343
+ return RedirectResponse(url="/?ui_mode=scoped", status_code=307)
344
+ else:
345
+ effective_ui_mode = "standalone" # True standalone launch
346
+ elif effective_ui_mode == "scoped" and not flock_is_preloaded:
347
+ # If explicitly asked for scoped mode but no flock is loaded (e.g. user bookmarked URL after server restart)
348
+ # It will show the "scoped-no-flock-view". We could also redirect to standalone.
349
+ # For now, let it show the "no flock loaded in scoped mode" message.
350
+ pass
351
+
352
+ # Conditional flock clearing based on the *effective* ui_mode
353
+ if effective_ui_mode != "scoped":
354
+ # If we are about to enter standalone mode, and a flock might have been
355
+ # preloaded (e.g. user navigated from /?ui_mode=scoped to /?ui_mode=standalone),
356
+ # ensure it's cleared for a true standalone experience.
357
+ if flock_is_preloaded: # Clear only if one was there
358
+ clear_current_flock()
359
+
360
+ context = get_base_context(request, error, success, effective_ui_mode)
361
+
362
+ if effective_ui_mode == "scoped":
363
+ if get_current_flock_instance(): # Re-check, as clear_current_flock might have run if user switched modes
364
+ context["initial_content_url"] = (
365
+ "/api/flocks/htmx/flock-properties-form"
366
+ )
367
+ else:
368
+ context["initial_content_url"] = "/ui/htmx/scoped-no-flock-view"
369
+ else: # Standalone mode
370
+ context["initial_content_url"] = "/ui/htmx/load-flock-view"
371
+
372
+ return templates.TemplateResponse("base.html", context)
373
+
374
+
375
+ @app.get("/ui/editor/properties", response_class=HTMLResponse)
376
+ async def page_editor_properties(
377
+ request: Request,
378
+ success: str = None,
379
+ error: str = None,
380
+ ui_mode: str = Query("standalone"),
381
+ ):
382
+ # ... (same as before) ...
383
+ flock = get_current_flock_instance()
384
+ if not flock:
385
+ err_msg = "No flock loaded. Please load or create a flock first."
386
+ # Preserve ui_mode on redirect if it was passed
387
+ redirect_url = f"/?error={urllib.parse.quote(err_msg)}"
388
+ if ui_mode == "scoped":
389
+ redirect_url += "&ui_mode=scoped"
390
+ return RedirectResponse(url=redirect_url, status_code=303)
391
+ context = get_base_context(request, error, success, ui_mode)
392
+ context["initial_content_url"] = "/api/flocks/htmx/flock-properties-form"
393
+ return templates.TemplateResponse("base.html", context)
394
+
395
+
396
+ @app.get("/ui/editor/agents", response_class=HTMLResponse)
397
+ async def page_editor_agents(
398
+ request: Request,
399
+ success: str = None,
400
+ error: str = None,
401
+ ui_mode: str = Query("standalone"),
402
+ ):
403
+ # ... (same as before) ...
404
+ flock = get_current_flock_instance()
405
+ if not flock:
406
+ # Preserve ui_mode on redirect
407
+ redirect_url = (
408
+ f"/?error={urllib.parse.quote('No flock loaded for agent view.')}"
409
+ )
410
+ if ui_mode == "scoped":
411
+ redirect_url += "&ui_mode=scoped"
412
+ return RedirectResponse(url=redirect_url, status_code=303)
413
+ context = get_base_context(request, error, success, ui_mode)
414
+ context["initial_content_url"] = "/ui/htmx/agent-manager-view"
415
+ return templates.TemplateResponse("base.html", context)
416
+
417
+
418
+ @app.get("/ui/editor/execute", response_class=HTMLResponse)
419
+ async def page_editor_execute(
420
+ request: Request,
421
+ success: str = None,
422
+ error: str = None,
423
+ ui_mode: str = Query("standalone"),
424
+ ):
425
+ flock = get_current_flock_instance()
426
+ if not flock:
427
+ # Preserve ui_mode on redirect
428
+ redirect_url = (
429
+ f"/?error={urllib.parse.quote('No flock loaded to execute.')}"
430
+ )
431
+ if ui_mode == "scoped":
432
+ redirect_url += "&ui_mode=scoped"
433
+ return RedirectResponse(url=redirect_url, status_code=303)
434
+ context = get_base_context(request, error, success, ui_mode)
435
+ # UPDATED initial_content_url
436
+ context["initial_content_url"] = "/ui/htmx/execution-view-container"
437
+ return templates.TemplateResponse("base.html", context)
438
+
439
+
440
+ # ... (registry and create page routes remain the same) ...
441
+ @app.get("/ui/registry", response_class=HTMLResponse)
442
+ async def page_registry(
443
+ request: Request,
444
+ error: str = None,
445
+ success: str = None,
446
+ ui_mode: str = Query("standalone"),
447
+ ):
448
+ context = get_base_context(request, error, success, ui_mode)
449
+ context["initial_content_url"] = "/ui/htmx/registry-viewer"
450
+ return templates.TemplateResponse("base.html", context)
451
+
452
+
453
+ @app.get("/ui/create", response_class=HTMLResponse)
454
+ async def page_create(
455
+ request: Request,
456
+ error: str = None,
457
+ success: str = None,
458
+ ui_mode: str = Query("standalone"),
459
+ ):
460
+ clear_current_flock()
461
+ # Create page should arguably not be accessible in scoped mode directly via URL,
462
+ # as the sidebar link will be hidden. If accessed, treat as standalone.
463
+ context = get_base_context(
464
+ request, error, success, "standalone"
465
+ ) # Force standalone for direct access
466
+ context["initial_content_url"] = "/ui/htmx/create-flock-form"
467
+ return templates.TemplateResponse("base.html", context)
468
+
469
+
470
+ # --- HTMX Content Routes ---
471
+ @app.get("/ui/htmx/sidebar", response_class=HTMLResponse)
472
+ async def htmx_get_sidebar(
473
+ request: Request, ui_mode: str = Query("standalone")
474
+ ):
475
+ # ... (same as before) ...
476
+ return templates.TemplateResponse(
477
+ "partials/_sidebar.html",
478
+ {
479
+ "request": request,
480
+ "current_flock": get_current_flock_instance(),
481
+ "ui_mode": ui_mode,
482
+ },
483
+ )
484
+
485
+
486
+ @app.get("/ui/htmx/header-flock-status", response_class=HTMLResponse)
487
+ async def htmx_get_header_flock_status(
488
+ request: Request, ui_mode: str = Query("standalone")
489
+ ):
490
+ # ui_mode isn't strictly needed for this partial's content, but good to accept if passed by hx-get
491
+ return templates.TemplateResponse(
492
+ "partials/_header_flock_status.html",
493
+ {
494
+ "request": request,
495
+ "current_flock": get_current_flock_instance(),
496
+ "current_filename": get_current_flock_filename(),
497
+ },
498
+ )
499
+
500
+
501
+ @app.get("/ui/htmx/load-flock-view", response_class=HTMLResponse)
502
+ async def htmx_get_load_flock_view(
503
+ request: Request,
504
+ error: str = None,
505
+ success: str = None,
506
+ ui_mode: str = Query("standalone"),
507
+ ):
508
+ # ... (same as before) ...
509
+ # This view is part of the "standalone" functionality.
510
+ # If somehow accessed in scoped mode, it might be confusing, but let it render.
511
+ return templates.TemplateResponse(
512
+ "partials/_load_manager_view.html",
513
+ {
514
+ "request": request,
515
+ "error_message": error,
516
+ "success_message": success,
517
+ "ui_mode": ui_mode, # Pass for consistency, though not directly used in this partial
518
+ },
519
+ )
520
+
521
+
522
+ @app.get("/ui/htmx/dashboard-flock-file-list", response_class=HTMLResponse)
523
+ async def htmx_get_dashboard_flock_file_list_partial(request: Request):
524
+ # ... (same as before) ...
525
+ return templates.TemplateResponse(
526
+ "partials/_dashboard_flock_file_list.html",
527
+ {"request": request, "flock_files": get_available_flock_files()},
528
+ )
529
+
530
+
531
+ @app.get("/ui/htmx/dashboard-default-action-pane", response_class=HTMLResponse)
532
+ async def htmx_get_dashboard_default_action_pane(request: Request):
533
+ # ... (same as before) ...
534
+ return HTMLResponse("""
535
+ <article style="text-align:center; margin-top: 2rem; border: none; background: transparent;">
536
+ <p>Select a Flock from the list to view its details and load it into the editor.</p>
537
+ <hr>
538
+ <p>Or, create a new Flock or upload an existing one using the "Create New Flock" option in the sidebar.</p>
539
+ </article>
540
+ """)
541
+
542
+
543
+ @app.get(
544
+ "/ui/htmx/dashboard-flock-properties-preview/{filename}",
545
+ response_class=HTMLResponse,
546
+ )
547
+ async def htmx_get_dashboard_flock_properties_preview(
548
+ request: Request, filename: str
549
+ ):
550
+ # ... (same as before) ...
551
+ preview_flock_data = get_flock_preview_service(filename)
552
+ return templates.TemplateResponse(
553
+ "partials/_dashboard_flock_properties_preview.html",
554
+ {
555
+ "request": request,
556
+ "selected_filename": filename,
557
+ "preview_flock": preview_flock_data,
558
+ },
559
+ )
560
+
561
+
562
+ @app.get("/ui/htmx/create-flock-form", response_class=HTMLResponse)
563
+ async def htmx_get_create_flock_form(
564
+ request: Request,
565
+ error: str = None,
566
+ success: str = None,
567
+ ui_mode: str = Query("standalone"),
568
+ ):
569
+ # ... (same as before) ...
570
+ # This view is part of the "standalone" functionality.
571
+ return templates.TemplateResponse(
572
+ "partials/_create_flock_form.html",
573
+ {
574
+ "request": request,
575
+ "error_message": error,
576
+ "success_message": success,
577
+ "ui_mode": ui_mode, # Pass for consistency
578
+ },
579
+ )
580
+
581
+
582
+ @app.get("/ui/htmx/agent-manager-view", response_class=HTMLResponse)
583
+ async def htmx_get_agent_manager_view(request: Request):
584
+ # ... (same as before) ...
585
+ flock = get_current_flock_instance()
586
+ if not flock:
587
+ return HTMLResponse(
588
+ "<article class='error'><p>No flock loaded. Cannot manage agents.</p></article>"
589
+ )
590
+ return templates.TemplateResponse(
591
+ "partials/_agent_manager_view.html",
592
+ {"request": request, "flock": flock},
593
+ )
594
+
595
+
596
+ @app.get("/ui/htmx/registry-viewer", response_class=HTMLResponse)
597
+ async def htmx_get_registry_viewer(request: Request):
598
+ # ... (same as before) ...
599
+ return templates.TemplateResponse(
600
+ "partials/_registry_viewer_content.html", {"request": request}
601
+ )
602
+
603
+
604
+ # --- NEW HTMX ROUTE FOR THE EXECUTION VIEW CONTAINER ---
605
+ @app.get("/ui/htmx/execution-view-container", response_class=HTMLResponse)
606
+ async def htmx_get_execution_view_container(request: Request):
607
+ flock = get_current_flock_instance()
608
+ if not flock:
609
+ return HTMLResponse(
610
+ "<article class='error'><p>No Flock loaded. Cannot execute.</p></article>"
611
+ )
612
+ return templates.TemplateResponse(
613
+ "partials/_execution_view_container.html", {"request": request}
614
+ )
615
+
616
+
617
+ # A new HTMX route for scoped mode when no flock is initially loaded (should ideally not happen)
618
+ @app.get("/ui/htmx/scoped-no-flock-view", response_class=HTMLResponse)
619
+ async def htmx_scoped_no_flock_view(request: Request):
620
+ return HTMLResponse("""
621
+ <article style="text-align:center; margin-top: 2rem; border: none; background: transparent;">
622
+ <hgroup>
623
+ <h2>Scoped Flock Mode</h2>
624
+ <h3>No Flock Loaded</h3>
625
+ </hgroup>
626
+ <p>This UI is in a scoped mode, expecting a Flock to be pre-loaded.</p>
627
+ <p>Please ensure the calling application provides a Flock instance.</p>
628
+ </article>
629
+ """)
630
+
631
+
632
+ # Endpoint to launch the UI in scoped mode with a preloaded flock
633
+ @app.post("/ui/launch-scoped", response_class=RedirectResponse)
634
+ async def launch_scoped_ui(
635
+ request: Request,
636
+ flock_data: dict, # This would be the flock's JSON data
637
+ # Potentially also receive filename if it's from a saved file
638
+ ):
639
+ # Here, you would parse flock_data, create a Flock instance,
640
+ # and set it as the current flock using your flock_service methods.
641
+ # For now, let's assume flock_service has a method like:
642
+ # set_current_flock_from_data(data) -> bool (returns True if successful)
643
+
644
+ # This is a placeholder for actual flock loading logic
645
+ # from flock.core.entities.flock import Flock # Assuming Flock can be instantiated from dict
646
+ # from flock.webapp.app.services.flock_service import set_current_flock_instance, set_current_flock_filename
647
+
648
+ # try:
649
+ # # Assuming flock_data is a dict that can initialize a Flock object
650
+ # # You might need a more robust way to deserialize, e.g., using Pydantic models
651
+ # loaded_flock = Flock(**flock_data) # This is a simplistic example
652
+ # set_current_flock_instance(loaded_flock)
653
+ # # If the flock has a name or identifier, you might set it as well
654
+ # # set_current_flock_filename(flock_data.get("name", "scoped_flock")) # Example
655
+ #
656
+ # # Redirect to the agent editor or properties page in scoped mode
657
+ # # The page_dashboard will handle ui_mode=scoped and redirect/set initial content appropriately
658
+ # return RedirectResponse(url="/?ui_mode=scoped", status_code=303)
659
+ # except Exception as e:
660
+ # # Log error e
661
+ # # Redirect to an error page or the standalone dashboard with an error message
662
+ # error_msg = f"Failed to load flock for scoped view: {e}"
663
+ # return RedirectResponse(url=f"/?error={urllib.parse.quote(error_msg)}&ui_mode=standalone", status_code=303)
664
+
665
+ # For now, since we don't have the flock loading logic here,
666
+ # we'll just redirect. The calling service (`src/flock/core/api`)
667
+ # will need to ensure the flock is loaded into the webapp's session/state
668
+ # *before* redirecting to this UI.
669
+
670
+ # A more direct way if `load_flock_from_data_service` exists and sets it globally for the session:
671
+ # success = load_flock_from_data_service(flock_data, "scoped_runtime_flock") # example filename
672
+ # if success:
673
+ # return RedirectResponse(url="/ui/editor/agents?ui_mode=scoped", status_code=303) # or properties
674
+ # else:
675
+ # return RedirectResponse(url="/?error=Failed+to+load+scoped+flock&ui_mode=standalone", status_code=303)
676
+
677
+ # Given the current structure, the simplest way for an external service to "preload" a flock
678
+ # is to use the existing `load_flock_from_file_service` if the flock can be temporarily saved,
679
+ # or by enhancing `flock_service` to allow setting a Flock instance directly.
680
+ # Let's assume the flock is already loaded into the session by the calling API for now.
681
+ # The calling API will be responsible for calling a service function within the webapp's context.
682
+
683
+ # This endpoint's primary job is now to redirect to the UI in the correct mode.
684
+ # The actual loading of the flock should happen *before* this redirect,
685
+ # by the API server calling a service function within the webapp's context.
686
+
687
+ # For demonstration, let's imagine the calling API has already used a service
688
+ # to set the flock. We just redirect.
689
+ if get_current_flock_instance():
690
+ return RedirectResponse(
691
+ url="/ui/editor/agents?ui_mode=scoped", status_code=303
692
+ )
693
+ else:
694
+ # If no flock is loaded, go to the main page in scoped mode, which will show the "no flock" message.
695
+ return RedirectResponse(url="/?ui_mode=scoped", status_code=303)
696
+
697
+
698
+ # --- Action Routes ...
699
+ # The `load-flock-action/*` and `create-flock` POST routes should remain the same as they already
700
+ # correctly target `#main-content-area` and trigger `flockLoaded`.
701
+ # ... (rest of action routes: load-flock-action/by-name, by-upload, create-flock)
702
+ @app.post("/ui/load-flock-action/by-name", response_class=HTMLResponse)
703
+ async def ui_load_flock_by_name_action(
704
+ request: Request, selected_flock_filename: str = Form(...)
705
+ ):
706
+ loaded_flock = load_flock_from_file_service(selected_flock_filename)
707
+ response_headers = {}
708
+ if loaded_flock:
709
+ success_message = f"Flock '{loaded_flock.name}' loaded from '{selected_flock_filename}'."
710
+ response_headers["HX-Push-Url"] = "/ui/editor/properties"
711
+ response_headers["HX-Trigger"] = json.dumps(
712
+ {
713
+ "flockLoaded": None,
714
+ "notify": {"type": "success", "message": success_message},
715
+ }
716
+ )
717
+ return templates.TemplateResponse(
718
+ "partials/_flock_properties_form.html",
719
+ {
720
+ "request": request,
721
+ "flock": loaded_flock,
722
+ "current_filename": get_current_flock_filename(),
723
+ },
724
+ headers=response_headers,
725
+ )
726
+ else:
727
+ error_message = (
728
+ f"Failed to load flock file '{selected_flock_filename}'."
729
+ )
730
+ response_headers["HX-Trigger"] = json.dumps(
731
+ {"notify": {"type": "error", "message": error_message}}
732
+ )
733
+ return templates.TemplateResponse(
734
+ "partials/_load_manager_view.html",
735
+ {"request": request, "error_message_inline": error_message},
736
+ headers=response_headers,
737
+ )
738
+
739
+
740
+ @app.post("/ui/load-flock-action/by-upload", response_class=HTMLResponse)
741
+ async def ui_load_flock_by_upload_action(
742
+ request: Request, flock_file_upload: UploadFile = File(...)
743
+ ):
744
+ error_message = None
745
+ filename_to_load = None
746
+ response_headers = {}
747
+ if flock_file_upload and flock_file_upload.filename:
748
+ if not flock_file_upload.filename.endswith((".yaml", ".yml", ".flock")):
749
+ error_message = "Invalid file type."
750
+ else:
751
+ upload_path = FLOCK_FILES_DIR / flock_file_upload.filename
752
+ try:
753
+ with upload_path.open("wb") as buffer:
754
+ shutil.copyfileobj(flock_file_upload.file, buffer)
755
+ filename_to_load = flock_file_upload.filename
756
+ except Exception as e:
757
+ error_message = f"Upload failed: {e}"
758
+ finally:
759
+ await flock_file_upload.close()
760
+ else:
761
+ error_message = "No file uploaded."
762
+
763
+ if filename_to_load and not error_message:
764
+ loaded_flock = load_flock_from_file_service(filename_to_load)
765
+ if loaded_flock:
766
+ success_message = (
767
+ f"Flock '{loaded_flock.name}' loaded from '{filename_to_load}'."
768
+ )
769
+ response_headers["HX-Push-Url"] = "/ui/editor/properties"
770
+ response_headers["HX-Trigger"] = json.dumps(
771
+ {
772
+ "flockLoaded": None,
773
+ "flockFileListChanged": None,
774
+ "notify": {"type": "success", "message": success_message},
775
+ }
776
+ )
777
+ return templates.TemplateResponse(
778
+ "partials/_flock_properties_form.html",
779
+ {
780
+ "request": request,
781
+ "flock": loaded_flock,
782
+ "current_filename": get_current_flock_filename(),
783
+ },
784
+ headers=response_headers,
785
+ )
786
+ else:
787
+ error_message = f"Failed to process uploaded '{filename_to_load}'."
788
+
789
+ response_headers["HX-Trigger"] = json.dumps(
790
+ {
791
+ "notify": {
792
+ "type": "error",
793
+ "message": error_message or "Upload failed.",
794
+ }
795
+ }
796
+ )
797
+ return templates.TemplateResponse(
798
+ "partials/_create_flock_form.html",
799
+ { # Changed target to create form on upload error
800
+ "request": request,
801
+ "error_message": error_message or "Upload action failed.",
802
+ },
803
+ headers=response_headers,
804
+ )
805
+
806
+
807
+ @app.post("/ui/create-flock", response_class=HTMLResponse)
808
+ async def ui_create_flock_action(
809
+ request: Request,
810
+ flock_name: str = Form(...),
811
+ default_model: str = Form(None),
812
+ description: str = Form(None),
813
+ ):
814
+ if not flock_name.strip():
815
+ return templates.TemplateResponse(
816
+ "partials/_create_flock_form.html",
817
+ {
818
+ "request": request,
819
+ "error_message": "Flock name cannot be empty.",
820
+ },
821
+ )
822
+ new_flock = create_new_flock_service(flock_name, default_model, description)
823
+ success_msg = (
824
+ f"New flock '{new_flock.name}' created. Configure properties and save."
825
+ )
826
+ response_headers = {
827
+ "HX-Push-Url": "/ui/editor/properties",
828
+ "HX-Trigger": json.dumps(
829
+ {
830
+ "flockLoaded": None,
831
+ "notify": {"type": "success", "message": success_msg},
832
+ }
833
+ ),
834
+ }
835
+ return templates.TemplateResponse(
836
+ "partials/_flock_properties_form.html",
837
+ {
838
+ "request": request,
839
+ "flock": new_flock,
840
+ "current_filename": get_current_flock_filename(),
841
+ },
842
+ headers=response_headers,
843
+ )
844
+
845
+
846
+ # --- Settings Page ---
847
+ @app.get("/ui/settings", response_class=HTMLResponse)
848
+ async def page_settings(
849
+ request: Request,
850
+ error: str = None,
851
+ success: str = None,
852
+ ui_mode: str = Query("standalone"),
853
+ ):
854
+ """Render the Settings top-level page which in turn loads the HTMX settings view."""
855
+ context = get_base_context(request, error, success, ui_mode)
856
+ context["initial_content_url"] = "/ui/htmx/settings-view"
857
+ return templates.TemplateResponse("base.html", context)
858
+
859
+
860
+ # Helper to build env var list for templates
861
+
862
+ def _prepare_env_vars_for_template():
863
+ env_vars_raw = load_env_file()
864
+ show_secrets = get_show_secrets_setting(env_vars_raw)
865
+ env_vars_list = []
866
+ for name, value in env_vars_raw.items():
867
+ if name.startswith("#") or name == "":
868
+ # skip comments/blank for table
869
+ continue
870
+ display_value = (
871
+ value
872
+ if (not is_sensitive(name) or show_secrets)
873
+ else mask_sensitive_value(value)
874
+ )
875
+ env_vars_list.append({"name": name, "value": display_value})
876
+ return env_vars_list, show_secrets
877
+
878
+
879
+ @app.get("/ui/htmx/settings-view", response_class=HTMLResponse)
880
+ async def htmx_get_settings_view(request: Request):
881
+ """Return the settings composite view (env vars + theme switcher)."""
882
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
883
+ theme_name = get_current_theme_name()
884
+ themes_available = []
885
+ if THEMES_DIR and THEMES_DIR.exists():
886
+ themes_available = [p.stem for p in THEMES_DIR.glob("*.toml")]
887
+ return templates.TemplateResponse(
888
+ "partials/_settings_view.html",
889
+ {
890
+ "request": request,
891
+ "env_vars": env_vars_list,
892
+ "show_secrets": show_secrets,
893
+ "themes": themes_available,
894
+ "current_theme": theme_name,
895
+ },
896
+ )
897
+
898
+
899
+ # --- Env Var Manager Endpoints ---
900
+ @app.post("/ui/htmx/toggle-show-secrets", response_class=HTMLResponse)
901
+ async def htmx_toggle_show_secrets(request: Request):
902
+ # Toggle and return updated table
903
+ env_vars_raw = load_env_file()
904
+ current = get_show_secrets_setting(env_vars_raw)
905
+ set_show_secrets_setting(not current)
906
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
907
+ return templates.TemplateResponse(
908
+ "partials/_env_vars_table.html",
909
+ {
910
+ "request": request,
911
+ "env_vars": env_vars_list,
912
+ "show_secrets": show_secrets,
913
+ },
914
+ )
915
+
916
+
917
+ @app.post("/ui/htmx/env-delete", response_class=HTMLResponse)
918
+ async def htmx_env_delete(request: Request, var_name: str = Form(...)):
919
+ env_vars_raw = load_env_file()
920
+ if var_name in env_vars_raw:
921
+ del env_vars_raw[var_name]
922
+ save_env_file(env_vars_raw)
923
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
924
+ return templates.TemplateResponse(
925
+ "partials/_env_vars_table.html",
926
+ {
927
+ "request": request,
928
+ "env_vars": env_vars_list,
929
+ "show_secrets": show_secrets,
930
+ },
931
+ )
932
+
933
+
934
+ @app.post("/ui/htmx/env-edit", response_class=HTMLResponse)
935
+ async def htmx_env_edit(
936
+ request: Request,
937
+ var_name: str = Form(...),
938
+ ):
939
+ # New value is provided via HX-Prompt header
940
+ new_value = request.headers.get("HX-Prompt")
941
+ if new_value is None:
942
+ # Nothing entered; just return current table
943
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
944
+ return templates.TemplateResponse(
945
+ "partials/_env_vars_table.html",
946
+ {
947
+ "request": request,
948
+ "env_vars": env_vars_list,
949
+ "show_secrets": show_secrets,
950
+ },
951
+ )
952
+ env_vars_raw = load_env_file()
953
+ env_vars_raw[var_name] = new_value
954
+ save_env_file(env_vars_raw)
955
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
956
+ return templates.TemplateResponse(
957
+ "partials/_env_vars_table.html",
958
+ {
959
+ "request": request,
960
+ "env_vars": env_vars_list,
961
+ "show_secrets": show_secrets,
962
+ },
963
+ )
964
+
965
+
966
+ @app.get("/ui/htmx/env-add-form", response_class=HTMLResponse)
967
+ async def htmx_env_add_form(request: Request):
968
+ # Return simple form row at top of table
969
+ return HTMLResponse(
970
+ """
971
+ <form hx-post='/ui/htmx/env-add' hx-target='#env-vars-container' hx-swap='outerHTML' style='display:flex; gap:0.5rem; margin-bottom:0.5rem;'>
972
+ <input name='var_name' placeholder='NAME' required style='flex:2;'>
973
+ <input name='var_value' placeholder='VALUE' style='flex:3;'>
974
+ <button type='submit'>Add</button>
975
+ </form>
976
+ """
977
+ )
978
+
979
+
980
+ @app.post("/ui/htmx/env-add", response_class=HTMLResponse)
981
+ async def htmx_env_add(request: Request, var_name: str = Form(...), var_value: str = Form("")):
982
+ env_vars_raw = load_env_file()
983
+ env_vars_raw[var_name] = var_value
984
+ save_env_file(env_vars_raw)
985
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
986
+ return templates.TemplateResponse(
987
+ "partials/_env_vars_table.html",
988
+ {
989
+ "request": request,
990
+ "env_vars": env_vars_list,
991
+ "show_secrets": show_secrets,
992
+ },
993
+ )
994
+
995
+
996
+ # --- Theme Preview and Apply Endpoints ---
997
+ @app.get("/ui/htmx/theme-preview", response_class=HTMLResponse)
998
+ async def htmx_theme_preview(request: Request, theme: str = Query(None)):
999
+ theme_name = theme or get_current_theme_name()
1000
+ # Load theme data
1001
+ try:
1002
+ theme_path = THEMES_DIR / f"{theme_name}.toml" if THEMES_DIR else None
1003
+ if not (theme_path and theme_path.exists()):
1004
+ return HTMLResponse("<p>Theme not found.</p>")
1005
+ from flock.core.logging.formatters.themed_formatter import (
1006
+ load_theme_from_file,
1007
+ )
1008
+ theme_data = load_theme_from_file(str(theme_path))
1009
+ except Exception as e:
1010
+ return HTMLResponse(f"<p>Error loading theme: {e}</p>")
1011
+
1012
+ css_vars = alacritty_to_pico(theme_data)
1013
+ css_vars_str = ":root {\n" + "\n".join([f" {k}: {v};" for k, v in css_vars.items()]) + "\n}"
1014
+
1015
+ main_colors = [
1016
+ ("Background", css_vars["--pico-background-color"]),
1017
+ ("Text", css_vars["--pico-color"]),
1018
+ ("Primary", css_vars["--pico-primary"]),
1019
+ ("Secondary", css_vars["--pico-secondary"]),
1020
+ ("Muted", css_vars["--pico-muted-color"]),
1021
+ ]
1022
+
1023
+ return templates.TemplateResponse(
1024
+ "partials/_theme_preview.html",
1025
+ {
1026
+ "request": request,
1027
+ "theme_name": theme_name,
1028
+ "css_vars_str": css_vars_str,
1029
+ "main_colors": main_colors,
1030
+ },
1031
+ )
1032
+
1033
+
1034
+ @app.post("/ui/apply-theme")
1035
+ async def apply_theme(request: Request, theme: str = Form(...)):
1036
+ try:
1037
+ from flock.webapp.app.config import set_current_theme_name
1038
+
1039
+ set_current_theme_name(theme)
1040
+ # Trigger full refresh via HTMX
1041
+ headers = {"HX-Refresh": "true"}
1042
+ return HTMLResponse("", headers=headers)
1043
+ except Exception as e:
1044
+ return HTMLResponse(f"Failed to apply theme: {e}", status_code=500)
1045
+
1046
+
1047
+ # --- Settings Content Endpoints (for tab navigation) ---
1048
+ @app.get("/ui/htmx/settings/env-vars", response_class=HTMLResponse)
1049
+ async def htmx_settings_env_vars(request: Request):
1050
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
1051
+ return templates.TemplateResponse(
1052
+ "partials/_settings_env_content.html",
1053
+ {
1054
+ "request": request,
1055
+ "env_vars": env_vars_list,
1056
+ "show_secrets": show_secrets,
1057
+ },
1058
+ )
1059
+
1060
+
1061
+ @app.get("/ui/htmx/settings/theme", response_class=HTMLResponse)
1062
+ async def htmx_settings_theme(request: Request):
1063
+ theme_name = get_current_theme_name()
1064
+ themes_available = []
1065
+ if THEMES_DIR and THEMES_DIR.exists():
1066
+ themes_available = [p.stem for p in THEMES_DIR.glob("*.toml")]
1067
+ return templates.TemplateResponse(
1068
+ "partials/_settings_theme_content.html",
1069
+ {
1070
+ "request": request,
1071
+ "themes": themes_available,
1072
+ "current_theme": theme_name,
1073
+ },
1074
+ )