flock-core 0.4.0b43__py3-none-any.whl → 0.4.0b45__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 (44) hide show
  1. flock/cli/manage_agents.py +19 -4
  2. flock/core/api/__init__.py +1 -2
  3. flock/core/api/endpoints.py +150 -218
  4. flock/core/api/main.py +134 -653
  5. flock/core/api/service.py +214 -0
  6. flock/core/flock.py +192 -134
  7. flock/core/flock_agent.py +31 -0
  8. flock/webapp/app/api/agent_management.py +135 -164
  9. flock/webapp/app/api/execution.py +76 -85
  10. flock/webapp/app/api/flock_management.py +60 -33
  11. flock/webapp/app/chat.py +233 -0
  12. flock/webapp/app/config.py +6 -3
  13. flock/webapp/app/dependencies.py +95 -0
  14. flock/webapp/app/main.py +320 -906
  15. flock/webapp/app/services/flock_service.py +183 -161
  16. flock/webapp/run.py +176 -100
  17. flock/webapp/static/css/chat.css +227 -0
  18. flock/webapp/static/css/components.css +167 -0
  19. flock/webapp/static/css/header.css +39 -0
  20. flock/webapp/static/css/layout.css +46 -0
  21. flock/webapp/static/css/sidebar.css +127 -0
  22. flock/webapp/templates/base.html +6 -1
  23. flock/webapp/templates/chat.html +60 -0
  24. flock/webapp/templates/chat_settings.html +20 -0
  25. flock/webapp/templates/flock_editor.html +1 -1
  26. flock/webapp/templates/partials/_agent_detail_form.html +8 -7
  27. flock/webapp/templates/partials/_agent_list.html +3 -3
  28. flock/webapp/templates/partials/_agent_manager_view.html +3 -4
  29. flock/webapp/templates/partials/_chat_container.html +9 -0
  30. flock/webapp/templates/partials/_chat_messages.html +13 -0
  31. flock/webapp/templates/partials/_chat_settings_form.html +65 -0
  32. flock/webapp/templates/partials/_execution_form.html +2 -2
  33. flock/webapp/templates/partials/_execution_view_container.html +1 -1
  34. flock/webapp/templates/partials/_flock_properties_form.html +2 -2
  35. flock/webapp/templates/partials/_registry_viewer_content.html +3 -3
  36. flock/webapp/templates/partials/_sidebar.html +17 -1
  37. flock/webapp/templates/registry_viewer.html +3 -3
  38. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/METADATA +1 -1
  39. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/RECORD +42 -31
  40. flock/webapp/static/css/custom.css +0 -612
  41. flock/webapp/templates/partials/_agent_manager_view_old.html +0 -19
  42. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/WHEEL +0 -0
  43. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/entry_points.txt +0 -0
  44. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/licenses/LICENSE +0 -0
flock/webapp/app/main.py CHANGED
@@ -1,9 +1,8 @@
1
- # ... (keep existing imports and app setup) ...
1
+ # src/flock/webapp/app/main.py
2
2
  import json
3
- import os # Needed for environment variable helpers
4
3
  import shutil
5
- import sys # For path
6
4
  import urllib.parse
5
+ from contextlib import asynccontextmanager
7
6
  from pathlib import Path
8
7
 
9
8
  from fastapi import FastAPI, File, Form, Query, Request, UploadFile
@@ -11,1064 +10,479 @@ from fastapi.responses import HTMLResponse, RedirectResponse
11
10
  from fastapi.staticfiles import StaticFiles
12
11
  from fastapi.templating import Jinja2Templates
13
12
 
13
+ from flock.core.api.endpoints import create_api_router
14
+ from flock.core.api.run_store import RunStore
15
+
16
+ # Import core Flock components and API related modules
17
+ from flock.core.flock import Flock # For type hinting
18
+ from flock.core.logging.logging import get_logger # For logging
19
+
20
+ # Import UI-specific routers
14
21
  from flock.webapp.app.api import (
15
22
  agent_management,
16
23
  execution,
17
24
  flock_management,
18
25
  registry_viewer,
19
26
  )
20
-
21
- # Import config functions
22
27
  from flock.webapp.app.config import (
23
- DEFAULT_THEME_NAME, # Import default for fallback
28
+ DEFAULT_THEME_NAME,
24
29
  FLOCK_FILES_DIR,
25
- THEMES_DIR, # Import THEMES_DIR from config
30
+ THEMES_DIR,
26
31
  get_current_theme_name,
27
- # set_current_theme_name, # Not directly used in main.py, but available
28
32
  )
33
+
34
+ # Import dependency management and config
35
+ from flock.webapp.app.dependencies import (
36
+ get_pending_custom_endpoints_and_clear,
37
+ set_global_flock_services,
38
+ )
39
+
40
+ # Import service functions (which now expect app_state)
29
41
  from flock.webapp.app.services.flock_service import (
30
- clear_current_flock,
42
+ clear_current_flock_service,
31
43
  create_new_flock_service,
32
44
  get_available_flock_files,
33
- get_current_flock_filename,
34
- get_current_flock_instance,
35
45
  get_flock_preview_service,
36
46
  load_flock_from_file_service,
47
+ # Note: get_current_flock_instance/filename are removed from service,
48
+ # as main.py will use request.app.state for this.
37
49
  )
38
50
  from flock.webapp.app.theme_mapper import alacritty_to_pico
39
51
 
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"
52
+ logger = get_logger("webapp.main")
49
53
 
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
54
 
58
55
  try:
59
56
  from flock.core.logging.formatters.themed_formatter import (
60
57
  load_theme_from_file,
61
58
  )
62
-
63
59
  THEME_LOADER_AVAILABLE = True
64
- # themes_dir is now imported from config
65
60
  except ImportError:
66
- print(
67
- "Warning: Could not import flock.core theme loading utilities.",
68
- file=sys.stderr,
69
- )
61
+ logger.warning("Could not import flock.core theme loading utilities.")
70
62
  THEME_LOADER_AVAILABLE = False
71
- # THEMES_DIR will be None if not imported, or its value from config
72
63
 
73
- # --- Lightweight .env helpers (self-contained, no external deps) ---
74
- ENV_FILE = ".env"
64
+ # --- .env helpers (copied from original main.py for self-containment) ---
65
+ ENV_FILE_PATH = Path(".env") #Path(os.getenv("FLOCK_WEB_ENV_FILE", Path.home() / ".flock" / ".env"))
66
+ #ENV_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
75
67
  SHOW_SECRETS_KEY = "SHOW_SECRETS"
76
68
 
77
- def load_env_file() -> dict[str, str]:
69
+ def load_env_file_web() -> dict[str, str]:
78
70
  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()
71
+ if not ENV_FILE_PATH.exists(): return env_vars
72
+ with open(ENV_FILE_PATH) as f: lines = f.readlines()
83
73
  for line in lines:
84
74
  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] = ""
75
+ if not line: env_vars[""] = ""; continue
76
+ if line.startswith("#"): env_vars[line] = ""; continue
77
+ if "=" in line: k, v = line.split("=", 1); env_vars[k] = v
78
+ else: env_vars[line] = ""
96
79
  return env_vars
97
80
 
98
- def save_env_file(env_vars: dict[str, str]):
81
+ def save_env_file_web(env_vars: dict[str, str]):
99
82
  try:
100
- with open(ENV_FILE, "w") as f:
83
+ with open(ENV_FILE_PATH, "w") as f:
101
84
  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()
85
+ if k.startswith("#"): f.write(f"{k}\n")
86
+ elif not k: f.write("\n")
87
+ else: f.write(f"{k}={v}\n")
88
+ except Exception as e: logger.error(f"[Settings] Failed to save .env: {e}")
89
+
90
+ def is_sensitive_web(key: str) -> bool:
91
+ patterns = ["key", "token", "secret", "password", "api", "pat"]; low = key.lower()
114
92
  return any(p in low for p in patterns)
115
93
 
116
- def mask_sensitive_value(value: str) -> str:
117
- if not value:
118
- return value
119
- if len(value) <= 4:
120
- return "••••"
94
+ def mask_sensitive_value_web(value: str) -> str:
95
+ if not value: return value
96
+ if len(value) <= 4: return "••••"
121
97
  return value[:2] + "•" * (len(value) - 4) + value[-2:]
122
98
 
123
- def get_show_secrets_setting(env_vars: dict[str, str]) -> bool:
99
+ def get_show_secrets_setting_web(env_vars: dict[str, str]) -> bool:
124
100
  return env_vars.get(SHOW_SECRETS_KEY, "false").lower() == "true"
125
101
 
126
- def set_show_secrets_setting(show: bool):
127
- env_vars = load_env_file()
102
+ def set_show_secrets_setting_web(show: bool):
103
+ env_vars = load_env_file_web()
128
104
  env_vars[SHOW_SECRETS_KEY] = str(show)
129
- save_env_file(env_vars)
130
-
131
- # -------------------------------------------------------------------
105
+ save_env_file_web(env_vars)
106
+ # --- End .env helpers ---
107
+
108
+
109
+ @asynccontextmanager
110
+ async def lifespan(app: FastAPI):
111
+ logger.info("FastAPI application starting up...")
112
+ # Flock instance and RunStore are expected to be set on app.state
113
+ # by `start_unified_server` in `webapp/run.py` *before* uvicorn starts the app.
114
+ # The call to `set_global_flock_services` also happens there.
115
+
116
+ # Add custom routes if any were passed during server startup
117
+ # These are retrieved from the dependency module where `start_unified_server` stored them.
118
+ pending_endpoints = get_pending_custom_endpoints_and_clear()
119
+ if pending_endpoints:
120
+ flock_instance_from_state: Flock | None = getattr(app.state, "flock_instance", None)
121
+ if flock_instance_from_state:
122
+ from flock.core.api.main import (
123
+ FlockAPI, # Local import for this specific task
124
+ )
125
+ # Create a temporary FlockAPI service object just for adding routes
126
+ temp_flock_api_service = FlockAPI(
127
+ flock_instance_from_state,
128
+ custom_endpoints=pending_endpoints
129
+ )
130
+ temp_flock_api_service.add_custom_routes_to_app(app)
131
+ logger.info(f"Lifespan: Added {len(pending_endpoints)} custom API routes to main app.")
132
+ else:
133
+ logger.warning("Lifespan: Pending custom endpoints found, but no Flock instance in app.state. Cannot add custom routes.")
134
+ yield
135
+ logger.info("FastAPI application shutting down...")
132
136
 
133
- app = FastAPI(title="Flock UI")
137
+ app = FastAPI(title="Flock Web UI & API", lifespan=lifespan)
134
138
 
135
139
  BASE_DIR = Path(__file__).resolve().parent.parent
136
- app.mount(
137
- "/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static"
138
- )
140
+ app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
139
141
  templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
140
142
 
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
-
143
+ core_api_router = create_api_router()
144
+ app.include_router(core_api_router, prefix="/api", tags=["Flock API Core"])
145
+ app.include_router(flock_management.router, prefix="/ui/api/flock", tags=["UI Flock Management"])
146
+ app.include_router(agent_management.router, prefix="/ui/api/flock", tags=["UI Agent Management"])
147
+ app.include_router(execution.router, prefix="/ui/api/flock", tags=["UI Execution"])
148
+ app.include_router(registry_viewer.router, prefix="/ui/api/registry", tags=["UI Registry"])
155
149
 
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
150
+ def generate_theme_css_web(theme_name: str | None) -> str:
151
+ if not THEME_LOADER_AVAILABLE or THEMES_DIR is None: return ""
152
+ active_theme_name = theme_name or get_current_theme_name() or DEFAULT_THEME_NAME
162
153
  theme_filename = f"{active_theme_name}.toml"
163
- theme_path = THEMES_DIR / theme_filename # Use imported THEMES_DIR
164
-
154
+ theme_path = THEMES_DIR / theme_filename
165
155
  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
156
+ logger.warning(f"Theme file not found: {theme_path}. Using default: {DEFAULT_THEME_NAME}.toml")
157
+ theme_path = THEMES_DIR / f"{DEFAULT_THEME_NAME}.toml"
158
+ active_theme_name = DEFAULT_THEME_NAME
173
159
  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
160
+ logger.warning(f"Default theme file not found: {theme_path}. No theme CSS.")
161
+ return ""
162
+ try: theme_dict = load_theme_from_file(str(theme_path))
163
+ except Exception as e: logger.error(f"Error loading theme {theme_path}: {e}"); return ""
282
164
 
283
165
  pico_vars = alacritty_to_pico(theme_dict)
166
+ if not pico_vars: return ""
284
167
  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
168
  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
169
  return css_string
303
170
 
304
-
305
- def get_base_context(
306
- request: Request,
307
- error: str = None,
308
- success: str = None,
309
- ui_mode: str = "standalone",
171
+ def get_base_context_web(
172
+ request: Request, error: str = None, success: str = None, ui_mode: str = "standalone"
310
173
  ) -> dict:
311
- theme_name = get_current_theme_name() # Get theme from config
312
- theme_css = generate_theme_css(theme_name)
174
+ flock_instance_from_state: Flock | None = getattr(request.app.state, "flock_instance", None)
175
+ current_flock_filename_from_state: str | None = getattr(request.app.state, "flock_filename", None)
176
+ theme_name = get_current_theme_name()
177
+ theme_css = generate_theme_css_web(theme_name)
313
178
  return {
314
179
  "request": request,
315
- "current_flock": get_current_flock_instance(),
316
- "current_filename": get_current_flock_filename(),
180
+ "current_flock": flock_instance_from_state,
181
+ "current_filename": current_flock_filename_from_state,
317
182
  "error_message": error,
318
183
  "success_message": success,
319
184
  "ui_mode": ui_mode,
320
- "theme_css": theme_css, # Add generated CSS to context
321
- "active_theme_name": theme_name, # Added active theme name
185
+ "theme_css": theme_css,
186
+ "active_theme_name": theme_name,
187
+ "chat_enabled": getattr(request.app.state, "chat_enabled", False),
322
188
  }
323
189
 
324
-
325
- # --- Main Page Routes ---
326
- @app.get("/", response_class=HTMLResponse)
190
+ @app.get("/", response_class=HTMLResponse, tags=["UI Pages"])
327
191
  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
192
+ request: Request, error: str = None, success: str = None, ui_mode: str = Query(None)
334
193
  ):
335
- # Determine effective ui_mode
336
194
  effective_ui_mode = ui_mode
337
- flock_is_preloaded = get_current_flock_instance() is not None
195
+ flock_is_preloaded = hasattr(request.app.state, "flock_instance") and request.app.state.flock_instance is not None
338
196
 
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)
197
+ if effective_ui_mode is None:
198
+ effective_ui_mode = "scoped" if flock_is_preloaded else "standalone"
199
+ if effective_ui_mode == "scoped":
200
+ return RedirectResponse(url=f"/?ui_mode=scoped&initial_load=true", status_code=307)
201
+
202
+ if effective_ui_mode == "standalone" and flock_is_preloaded:
203
+ clear_current_flock_service(request.app.state) # Pass app.state
204
+ logger.info("Switched to standalone mode, cleared preloaded Flock instance from app.state.")
205
+
206
+ context = get_base_context_web(request, error, success, effective_ui_mode)
207
+ flock_in_state = hasattr(request.app.state, "flock_instance") and request.app.state.flock_instance is not None
361
208
 
362
209
  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
- "/ui/htmx/execution-view-container"
366
- )
367
- else:
368
- context["initial_content_url"] = "/ui/htmx/scoped-no-flock-view"
369
- else: # Standalone mode
210
+ context["initial_content_url"] = "/ui/htmx/execution-view-container" if flock_in_state else "/ui/htmx/scoped-no-flock-view"
211
+ else:
370
212
  context["initial_content_url"] = "/ui/htmx/load-flock-view"
371
-
372
213
  return templates.TemplateResponse("base.html", context)
373
214
 
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"),
215
+ @app.get("/ui/editor/{section:path}", response_class=HTMLResponse, tags=["UI Pages"])
216
+ async def page_editor_section(
217
+ request: Request, section: str, success: str = None, error: str = None, ui_mode: str = Query("standalone")
381
218
  ):
382
- # ... (same as before) ...
383
- flock = get_current_flock_instance()
384
- if not flock:
219
+ flock_instance_from_state: Flock | None = getattr(request.app.state, "flock_instance", None)
220
+ if not flock_instance_from_state:
385
221
  err_msg = "No flock loaded. Please load or create a flock first."
386
- # Preserve ui_mode on redirect if it was passed
387
222
  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"
223
+ if ui_mode == "scoped": redirect_url += "&ui_mode=scoped"
412
224
  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
225
 
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"
226
+ context = get_base_context_web(request, error, success, ui_mode)
227
+ content_map = {
228
+ "properties": "/ui/api/flock/htmx/flock-properties-form",
229
+ "agents": "/ui/htmx/agent-manager-view",
230
+ "execute": "/ui/htmx/execution-view-container"
231
+ }
232
+ context["initial_content_url"] = content_map.get(section, "/ui/htmx/load-flock-view")
233
+ if section not in content_map: context["error_message"] = "Invalid editor section."
437
234
  return templates.TemplateResponse("base.html", context)
438
235
 
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)
236
+ @app.get("/ui/registry", response_class=HTMLResponse, tags=["UI Pages"])
237
+ async def page_registry(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
238
+ context = get_base_context_web(request, error, success, ui_mode)
449
239
  context["initial_content_url"] = "/ui/htmx/registry-viewer"
450
240
  return templates.TemplateResponse("base.html", context)
451
241
 
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
242
+ @app.get("/ui/create", response_class=HTMLResponse, tags=["UI Pages"])
243
+ async def page_create(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
244
+ clear_current_flock_service(request.app.state) # Pass app.state
245
+ context = get_base_context_web(request, error, success, "standalone")
466
246
  context["initial_content_url"] = "/ui/htmx/create-flock-form"
467
247
  return templates.TemplateResponse("base.html", context)
468
248
 
249
+ @app.get("/ui/htmx/sidebar", response_class=HTMLResponse, tags=["UI HTMX Partials"])
250
+ async def htmx_get_sidebar(request: Request, ui_mode: str = Query("standalone")):
251
+ return templates.TemplateResponse("partials/_sidebar.html", get_base_context_web(request, ui_mode=ui_mode))
469
252
 
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
-
253
+ @app.get("/ui/htmx/header-flock-status", response_class=HTMLResponse, tags=["UI HTMX Partials"])
254
+ async def htmx_get_header_flock_status(request: Request, ui_mode: str = Query("standalone")):
255
+ return templates.TemplateResponse("partials/_header_flock_status.html", get_base_context_web(request, ui_mode=ui_mode))
500
256
 
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
- )
257
+ @app.get("/ui/htmx/load-flock-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
258
+ async def htmx_get_load_flock_view(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
259
+ return templates.TemplateResponse("partials/_load_manager_view.html", get_base_context_web(request, error, success, ui_mode))
520
260
 
521
-
522
- @app.get("/ui/htmx/dashboard-flock-file-list", response_class=HTMLResponse)
261
+ @app.get("/ui/htmx/dashboard-flock-file-list", response_class=HTMLResponse, tags=["UI HTMX Partials"])
523
262
  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
- )
263
+ return templates.TemplateResponse("partials/_dashboard_flock_file_list.html", {"request": request, "flock_files": get_available_flock_files()})
529
264
 
530
-
531
- @app.get("/ui/htmx/dashboard-default-action-pane", response_class=HTMLResponse)
265
+ @app.get("/ui/htmx/dashboard-default-action-pane", response_class=HTMLResponse, tags=["UI HTMX Partials"])
532
266
  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
- )
267
+ return HTMLResponse("""<article style="text-align:center; margin-top: 2rem; border: none; background: transparent;"><p>Select a Flock from the list to view its details and load it into the editor.</p><hr><p>Or, create a new Flock or upload an existing one using the "Create New Flock" option in the sidebar.</p></article>""")
560
268
 
269
+ @app.get("/ui/htmx/dashboard-flock-properties-preview/{filename}", response_class=HTMLResponse, tags=["UI HTMX Partials"])
270
+ async def htmx_get_dashboard_flock_properties_preview(request: Request, filename: str):
271
+ preview_flock_data = get_flock_preview_service(filename)
272
+ return templates.TemplateResponse("partials/_dashboard_flock_properties_preview.html", {"request": request, "selected_filename": filename, "preview_flock": preview_flock_data})
561
273
 
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
-
274
+ @app.get("/ui/htmx/create-flock-form", response_class=HTMLResponse, tags=["UI HTMX Partials"])
275
+ async def htmx_get_create_flock_form(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
276
+ return templates.TemplateResponse("partials/_create_flock_form.html", get_base_context_web(request, error, success, ui_mode))
581
277
 
582
- @app.get("/ui/htmx/agent-manager-view", response_class=HTMLResponse)
278
+ @app.get("/ui/htmx/agent-manager-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
583
279
  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
- )
280
+ context = get_base_context_web(request) # This gets flock from app.state
281
+ if not context.get("current_flock"): # Check if flock exists in the context
282
+ return HTMLResponse("<article class='error'><p>No flock loaded. Cannot manage agents.</p></article>")
283
+ # Pass the 'current_flock' from the context to the template as 'flock'
590
284
  return templates.TemplateResponse(
591
285
  "partials/_agent_manager_view.html",
592
- {"request": request, "flock": flock},
286
+ {"request": request, "flock": context.get("current_flock")}
593
287
  )
594
288
 
595
-
596
- @app.get("/ui/htmx/registry-viewer", response_class=HTMLResponse)
289
+ @app.get("/ui/htmx/registry-viewer", response_class=HTMLResponse, tags=["UI HTMX Partials"])
597
290
  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
- )
291
+ return templates.TemplateResponse("partials/_registry_viewer_content.html", get_base_context_web(request))
602
292
 
603
-
604
- # --- NEW HTMX ROUTE FOR THE EXECUTION VIEW CONTAINER ---
605
- @app.get("/ui/htmx/execution-view-container", response_class=HTMLResponse)
293
+ @app.get("/ui/htmx/execution-view-container", response_class=HTMLResponse, tags=["UI HTMX Partials"])
606
294
  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
-
295
+ context = get_base_context_web(request)
296
+ if not context.get("current_flock"): return HTMLResponse("<article class='error'><p>No Flock loaded. Cannot execute.</p></article>")
297
+ return templates.TemplateResponse("partials/_execution_view_container.html", context)
616
298
 
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)
299
+ @app.get("/ui/htmx/scoped-no-flock-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
619
300
  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
-
301
+ return HTMLResponse("""<article style="text-align:center; margin-top: 2rem; border: none; background: transparent;"><hgroup><h2>Scoped Flock Mode</h2><h3>No Flock Loaded</h3></hgroup><p>This UI is in a scoped mode, expecting a Flock to be pre-loaded.</p><p>Please ensure the calling application provides a Flock instance.</p></article>""")
697
302
 
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)
303
+ # --- Action Routes (POST requests for UI interactions) ---
304
+ @app.post("/ui/load-flock-action/by-name", response_class=HTMLResponse, tags=["UI Actions"])
305
+ async def ui_load_flock_by_name_action(request: Request, selected_flock_filename: str = Form(...)):
306
+ loaded_flock = load_flock_from_file_service(selected_flock_filename, request.app.state)
707
307
  response_headers = {}
308
+ ui_mode_query = request.query_params.get("ui_mode", "standalone")
708
309
  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
- )
310
+ success_message_text = f"Flock '{loaded_flock.name}' loaded from '{selected_flock_filename}'."
311
+ response_headers["HX-Push-Url"] = "/ui/editor/properties?ui_mode=" + request.query_params.get("ui_mode", "standalone")
312
+ response_headers["HX-Trigger"] = json.dumps({"flockLoaded": None, "notify": {"type": "success", "message": success_message_text}})
313
+ # Use get_base_context_web to ensure all necessary context vars are present for the partial
314
+ context = get_base_context_web(request, success=success_message_text, ui_mode=ui_mode_query)
315
+ return templates.TemplateResponse("partials/_flock_properties_form.html", context, headers=response_headers)
726
316
  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 = {}
317
+ error_message_text = f"Failed to load flock file '{selected_flock_filename}'."
318
+ response_headers["HX-Trigger"] = json.dumps({"notify": {"type": "error", "message": error_message_text}})
319
+ context = get_base_context_web(request, error=error_message_text, ui_mode=ui_mode_query)
320
+ context["error_message_inline"] = error_message_text # For direct display in partial
321
+ return templates.TemplateResponse("partials/_load_manager_view.html", context, headers=response_headers)
322
+
323
+ @app.post("/ui/load-flock-action/by-upload", response_class=HTMLResponse, tags=["UI Actions"])
324
+ async def ui_load_flock_by_upload_action(request: Request, flock_file_upload: UploadFile = File(...)):
325
+ error_message_text, filename_to_load, response_headers = None, None, {}
326
+ ui_mode_query = request.query_params.get("ui_mode", "standalone")
327
+
747
328
  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."
329
+ if not flock_file_upload.filename.endswith((".yaml", ".yml", ".flock")): error_message_text = "Invalid file type."
750
330
  else:
751
331
  upload_path = FLOCK_FILES_DIR / flock_file_upload.filename
752
332
  try:
753
- with upload_path.open("wb") as buffer:
754
- shutil.copyfileobj(flock_file_upload.file, buffer)
333
+ with upload_path.open("wb") as buffer: shutil.copyfileobj(flock_file_upload.file, buffer)
755
334
  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."
335
+ except Exception as e: error_message_text = f"Upload failed: {e}"
336
+ finally: await flock_file_upload.close()
337
+ else: error_message_text = "No file uploaded."
762
338
 
763
- if filename_to_load and not error_message:
764
- loaded_flock = load_flock_from_file_service(filename_to_load)
339
+ if filename_to_load and not error_message_text:
340
+ loaded_flock = load_flock_from_file_service(filename_to_load, request.app.state)
765
341
  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
- ):
342
+ success_message_text = f"Flock '{loaded_flock.name}' loaded from '{filename_to_load}'."
343
+ response_headers["HX-Push-Url"] = f"/ui/editor/properties?ui_mode={ui_mode_query}"
344
+ response_headers["HX-Trigger"] = json.dumps({"flockLoaded": None, "flockFileListChanged": None, "notify": {"type": "success", "message": success_message_text}})
345
+ # CORRECTED CALL:
346
+ context = get_base_context_web(request, success=success_message_text, ui_mode=ui_mode_query)
347
+ return templates.TemplateResponse("partials/_flock_properties_form.html", context, headers=response_headers)
348
+ else: error_message_text = f"Failed to process uploaded '{filename_to_load}'."
349
+
350
+ final_error_msg = error_message_text or "Upload failed."
351
+ response_headers["HX-Trigger"] = json.dumps({"notify": {"type": "error", "message": final_error_msg}})
352
+ # CORRECTED CALL:
353
+ context = get_base_context_web(request, error=final_error_msg, ui_mode=ui_mode_query)
354
+ return templates.TemplateResponse("partials/_create_flock_form.html", context, headers=response_headers)
355
+
356
+ @app.post("/ui/create-flock", response_class=HTMLResponse, tags=["UI Actions"])
357
+ async def ui_create_flock_action(request: Request, flock_name: str = Form(...), default_model: str = Form(None), description: str = Form(None)):
358
+ ui_mode_query = request.query_params.get("ui_mode", "standalone")
814
359
  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)
360
+ # CORRECTED CALL:
361
+ context = get_base_context_web(request, error="Flock name cannot be empty.", ui_mode=ui_mode_query)
362
+ return templates.TemplateResponse("partials/_create_flock_form.html", context)
363
+
364
+ new_flock = create_new_flock_service(flock_name, default_model, description, request.app.state)
365
+ success_msg_text = f"New flock '{new_flock.name}' created. Configure properties and save."
366
+ response_headers = {"HX-Push-Url": f"/ui/editor/properties?ui_mode={ui_mode_query}", "HX-Trigger": json.dumps({"flockLoaded": None, "notify": {"type": "success", "message": success_msg_text}})}
367
+ # CORRECTED CALL:
368
+ context = get_base_context_web(request, success=success_msg_text, ui_mode=ui_mode_query)
369
+ return templates.TemplateResponse("partials/_flock_properties_form.html", context, headers=response_headers)
370
+
371
+ # --- Settings Page & Endpoints ---
372
+ @app.get("/ui/settings", response_class=HTMLResponse, tags=["UI Pages"])
373
+ async def page_settings(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
374
+ context = get_base_context_web(request, error, success, ui_mode)
856
375
  context["initial_content_url"] = "/ui/htmx/settings-view"
857
376
  return templates.TemplateResponse("base.html", context)
858
377
 
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)
378
+ def _prepare_env_vars_for_template_web():
379
+ env_vars_raw = load_env_file_web(); show_secrets = get_show_secrets_setting_web(env_vars_raw)
865
380
  env_vars_list = []
866
381
  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
- )
382
+ if name.startswith("#") or name == "": continue
383
+ display_value = value if (not is_sensitive_web(name) or show_secrets) else mask_sensitive_value_web(value)
875
384
  env_vars_list.append({"name": name, "value": display_value})
876
385
  return env_vars_list, show_secrets
877
386
 
878
-
879
- @app.get("/ui/htmx/settings-view", response_class=HTMLResponse)
387
+ @app.get("/ui/htmx/settings-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
880
388
  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()
389
+ env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
883
390
  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
- )
391
+ themes_available = [p.stem for p in THEMES_DIR.glob("*.toml")] if THEMES_DIR and THEMES_DIR.exists() else []
392
+ return templates.TemplateResponse("partials/_settings_view.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets, "themes": themes_available, "current_theme": theme_name})
897
393
 
898
-
899
- # --- Env Var Manager Endpoints ---
900
- @app.post("/ui/htmx/toggle-show-secrets", response_class=HTMLResponse)
394
+ @app.post("/ui/htmx/toggle-show-secrets", response_class=HTMLResponse, tags=["UI Actions"])
901
395
  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
- )
396
+ env_vars_raw = load_env_file_web(); current = get_show_secrets_setting_web(env_vars_raw)
397
+ set_show_secrets_setting_web(not current)
398
+ env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
399
+ return templates.TemplateResponse("partials/_env_vars_table.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
915
400
 
916
-
917
- @app.post("/ui/htmx/env-delete", response_class=HTMLResponse)
401
+ @app.post("/ui/htmx/env-delete", response_class=HTMLResponse, tags=["UI Actions"])
918
402
  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
-
403
+ env_vars_raw = load_env_file_web()
404
+ if var_name in env_vars_raw: del env_vars_raw[var_name]; save_env_file_web(env_vars_raw)
405
+ env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
406
+ return templates.TemplateResponse("partials/_env_vars_table.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
933
407
 
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
408
+ @app.post("/ui/htmx/env-edit", response_class=HTMLResponse, tags=["UI Actions"])
409
+ async def htmx_env_edit(request: Request, var_name: str = Form(...)):
940
410
  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)
411
+ env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
412
+ if new_value is not None:
413
+ env_vars_raw = load_env_file_web()
414
+ env_vars_raw[var_name] = new_value
415
+ save_env_file_web(env_vars_raw)
416
+ env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
417
+ return templates.TemplateResponse("partials/_env_vars_table.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
418
+
419
+ @app.get("/ui/htmx/env-add-form", response_class=HTMLResponse, tags=["UI HTMX Partials"])
967
420
  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
-
421
+ return HTMLResponse("""<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;'><input name='var_name' placeholder='NAME' required style='flex:2;'><input name='var_value' placeholder='VALUE' style='flex:3;'><button type='submit'>Add</button></form>""")
979
422
 
980
- @app.post("/ui/htmx/env-add", response_class=HTMLResponse)
423
+ @app.post("/ui/htmx/env-add", response_class=HTMLResponse, tags=["UI Actions"])
981
424
  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
-
425
+ env_vars_raw = load_env_file_web()
426
+ env_vars_raw[var_name] = var_value; save_env_file_web(env_vars_raw)
427
+ env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
428
+ return templates.TemplateResponse("partials/_env_vars_table.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
995
429
 
996
- # --- Theme Preview and Apply Endpoints ---
997
- @app.get("/ui/htmx/theme-preview", response_class=HTMLResponse)
430
+ @app.get("/ui/htmx/theme-preview", response_class=HTMLResponse, tags=["UI HTMX Partials"])
998
431
  async def htmx_theme_preview(request: Request, theme: str = Query(None)):
999
- theme_name = theme or get_current_theme_name()
1000
- # Load theme data
432
+ theme_name = theme or get_current_theme_name() or DEFAULT_THEME_NAME
1001
433
  try:
1002
434
  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
- )
435
+ if not (theme_path and theme_path.exists()): return HTMLResponse("<p>Theme not found.</p>")
1008
436
  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
-
437
+ except Exception as e: return HTMLResponse(f"<p>Error loading theme: {e}</p>")
1012
438
  css_vars = alacritty_to_pico(theme_data)
1013
439
  css_vars_str = ":root {\n" + "\n".join([f" {k}: {v};" for k, v in css_vars.items()]) + "\n}"
440
+ main_colors = [("Background", css_vars.get("--pico-background-color")), ("Text", css_vars.get("--pico-color")), ("Primary", css_vars.get("--pico-primary")), ("Secondary", css_vars.get("--pico-secondary")), ("Muted", css_vars.get("--pico-muted-color"))]
441
+ return templates.TemplateResponse("partials/_theme_preview.html", {"request": request, "theme_name": theme_name, "css_vars_str": css_vars_str, "main_colors": main_colors})
1014
442
 
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")
443
+ @app.post("/ui/apply-theme", tags=["UI Actions"])
1035
444
  async def apply_theme(request: Request, theme: str = Form(...)):
1036
445
  try:
1037
446
  from flock.webapp.app.config import set_current_theme_name
1038
-
1039
447
  set_current_theme_name(theme)
1040
- # Trigger full refresh via HTMX
1041
448
  headers = {"HX-Refresh": "true"}
1042
449
  return HTMLResponse("", headers=headers)
1043
- except Exception as e:
1044
- return HTMLResponse(f"Failed to apply theme: {e}", status_code=500)
450
+ except Exception as e: return HTMLResponse(f"Failed to apply theme: {e}", status_code=500)
1045
451
 
1046
-
1047
- # --- Settings Content Endpoints (for tab navigation) ---
1048
- @app.get("/ui/htmx/settings/env-vars", response_class=HTMLResponse)
452
+ @app.get("/ui/htmx/settings/env-vars", response_class=HTMLResponse, tags=["UI HTMX Partials"])
1049
453
  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
- )
454
+ env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
455
+ return templates.TemplateResponse("partials/_settings_env_content.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
1059
456
 
1060
-
1061
- @app.get("/ui/htmx/settings/theme", response_class=HTMLResponse)
457
+ @app.get("/ui/htmx/settings/theme", response_class=HTMLResponse, tags=["UI HTMX Partials"])
1062
458
  async def htmx_settings_theme(request: Request):
1063
459
  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
- )
460
+ themes_available = [p.stem for p in THEMES_DIR.glob("*.toml")] if THEMES_DIR and THEMES_DIR.exists() else []
461
+ return templates.TemplateResponse("partials/_settings_theme_content.html", {"request": request, "themes": themes_available, "current_theme": theme_name})
462
+
463
+ @app.get("/ui/chat", response_class=HTMLResponse, tags=["UI Pages"])
464
+ async def page_chat(request: Request, ui_mode: str = Query("standalone")):
465
+ context = get_base_context_web(request, ui_mode=ui_mode)
466
+ context["initial_content_url"] = "/ui/htmx/chat-view"
467
+ return templates.TemplateResponse("base.html", context)
468
+
469
+ @app.get("/ui/htmx/chat-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
470
+ async def htmx_get_chat_view(request: Request):
471
+ # Render container partial; session handled in chat router
472
+ return templates.TemplateResponse("partials/_chat_container.html", get_base_context_web(request))
473
+
474
+ if __name__ == "__main__":
475
+ import uvicorn
476
+ # Ensure the dependency injection system is initialized for standalone run
477
+ temp_run_store = RunStore()
478
+ # Create a default/dummy Flock instance for standalone UI testing
479
+ # This allows the UI to function without being started by `Flock.start_api()`
480
+ dev_flock_instance = Flock(name="DevStandaloneFlock", model="test/dummy", enable_logging=True, show_flock_banner=False)
481
+
482
+ set_global_flock_services(dev_flock_instance, temp_run_store)
483
+ app.state.flock_instance = dev_flock_instance
484
+ app.state.run_store = temp_run_store
485
+ app.state.flock_filename = "development_standalone.flock.yaml"
486
+
487
+ logger.info("Running webapp.app.main directly for development with a dummy Flock instance.")
488
+ uvicorn.run(app, host="127.0.0.1", port=8344, reload=True)