flock-core 0.4.0b43__py3-none-any.whl → 0.4.0b44__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 (42) hide show
  1. flock/core/api/__init__.py +1 -2
  2. flock/core/api/endpoints.py +149 -217
  3. flock/core/api/main.py +134 -653
  4. flock/core/api/service.py +214 -0
  5. flock/core/flock.py +192 -134
  6. flock/webapp/app/api/agent_management.py +135 -164
  7. flock/webapp/app/api/execution.py +76 -85
  8. flock/webapp/app/api/flock_management.py +60 -33
  9. flock/webapp/app/chat.py +233 -0
  10. flock/webapp/app/config.py +6 -3
  11. flock/webapp/app/dependencies.py +95 -0
  12. flock/webapp/app/main.py +320 -906
  13. flock/webapp/app/services/flock_service.py +183 -161
  14. flock/webapp/run.py +176 -100
  15. flock/webapp/static/css/chat.css +227 -0
  16. flock/webapp/static/css/components.css +167 -0
  17. flock/webapp/static/css/header.css +39 -0
  18. flock/webapp/static/css/layout.css +46 -0
  19. flock/webapp/static/css/sidebar.css +127 -0
  20. flock/webapp/templates/base.html +6 -1
  21. flock/webapp/templates/chat.html +60 -0
  22. flock/webapp/templates/chat_settings.html +20 -0
  23. flock/webapp/templates/flock_editor.html +1 -1
  24. flock/webapp/templates/partials/_agent_detail_form.html +4 -4
  25. flock/webapp/templates/partials/_agent_list.html +2 -2
  26. flock/webapp/templates/partials/_agent_manager_view.html +3 -4
  27. flock/webapp/templates/partials/_chat_container.html +9 -0
  28. flock/webapp/templates/partials/_chat_messages.html +13 -0
  29. flock/webapp/templates/partials/_chat_settings_form.html +65 -0
  30. flock/webapp/templates/partials/_execution_form.html +2 -2
  31. flock/webapp/templates/partials/_execution_view_container.html +1 -1
  32. flock/webapp/templates/partials/_flock_properties_form.html +2 -2
  33. flock/webapp/templates/partials/_registry_viewer_content.html +3 -3
  34. flock/webapp/templates/partials/_sidebar.html +17 -1
  35. flock/webapp/templates/registry_viewer.html +3 -3
  36. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b44.dist-info}/METADATA +1 -1
  37. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b44.dist-info}/RECORD +40 -29
  38. flock/webapp/static/css/custom.css +0 -612
  39. flock/webapp/templates/partials/_agent_manager_view_old.html +0 -19
  40. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b44.dist-info}/WHEEL +0 -0
  41. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b44.dist-info}/entry_points.txt +0 -0
  42. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b44.dist-info}/licenses/LICENSE +0 -0
flock/webapp/run.py CHANGED
@@ -1,4 +1,4 @@
1
- import os # For environment variable
1
+ # src/flock/webapp/run.py
2
2
  import sys
3
3
  from collections.abc import Callable, Sequence
4
4
  from pathlib import Path
@@ -6,132 +6,208 @@ from typing import TYPE_CHECKING, Any
6
6
 
7
7
  import uvicorn
8
8
 
9
- from flock.core.api.custom_endpoint import FlockEndpoint
10
-
9
+ # Import core Flock components
11
10
  if TYPE_CHECKING:
12
- from flock.core import Flock
11
+ from flock.core.api.custom_endpoint import FlockEndpoint
12
+ from flock.core.flock import Flock
13
13
 
14
- # --- Integrated Server Function ---
14
+ # --- Ensure src is in path for imports ---
15
+ current_file_path = Path(__file__).resolve()
16
+ flock_webapp_dir = current_file_path.parent
17
+ flock_dir = flock_webapp_dir.parent
18
+ src_dir = flock_dir.parent # Assuming `flock` is a package within `src`
15
19
 
20
+ if str(src_dir) not in sys.path:
21
+ sys.path.insert(0, str(src_dir))
16
22
 
17
- def start_integrated_server(
23
+ # --- Main Server Startup Function ---
24
+ def start_unified_server(
18
25
  flock_instance: "Flock",
19
26
  host: str,
20
27
  port: int,
21
- server_name: str, # Currently unused as UI sets its own title
22
- theme_name: str | None = None,
23
- custom_endpoints: Sequence[FlockEndpoint] | dict[tuple[str, list[str] | None], Callable[..., Any]] | None = None,
28
+ server_title: str,
29
+ enable_ui_routes: bool,
30
+ enable_chat_routes: bool = False,
31
+ ui_theme: str | None = None,
32
+ custom_endpoints: Sequence["FlockEndpoint"] | dict[tuple[str, list[str] | None], Callable[..., Any]] | None = None,
24
33
  ):
25
- """Starts the webapp, preloads flock & theme, includes API routes (TODO)."""
26
- print(
27
- f"Starting integrated server for Flock '{flock_instance.name}' on {host}:{port}"
28
- )
34
+ """Starts the unified FastAPI server for Flock.
35
+ - Initializes the web application (imported from webapp.app.main).
36
+ - Sets the provided Flock instance and a RunStore for dependency injection
37
+ and makes them available via app.state.
38
+ - Configures the UI theme.
39
+ - Stores custom API endpoints for registration during app lifespan startup.
40
+ - Optionally registers chat routes.
41
+ - Runs Uvicorn.
42
+ """
43
+ print(f"Attempting to start unified server for Flock '{flock_instance.name}' on http://{host}:{port}")
44
+ print(f"UI Routes Enabled: {enable_ui_routes}, Theme: {ui_theme or 'Default'}")
45
+
29
46
  try:
30
- # Ensure src is in path (important if called from core)
31
- src_dir = Path(__file__).resolve().parent.parent
32
- if str(src_dir) not in sys.path:
33
- sys.path.insert(0, str(src_dir))
34
-
35
- # Import necessary webapp components *after* path setup
36
- from flock.core.api.endpoints import (
37
- create_api_router, # Need to adapt this later
38
- )
39
- from flock.core.api.main import FlockAPI
40
- from flock.core.api.run_store import (
41
- RunStore, # Needed for API routes later
42
- )
43
- from flock.webapp.app.config import (
47
+ # Import necessary webapp components HERE, after path setup.
48
+ from flock.core.api.run_store import RunStore
49
+ from flock.core.logging.logging import get_logger # For logging
50
+ from flock.webapp.app.config import ( # For logging resolved theme
44
51
  get_current_theme_name,
45
52
  set_current_theme_name,
46
53
  )
47
- from flock.webapp.app.main import app as webapp_fastapi_app
48
- from flock.webapp.app.services.flock_service import (
49
- set_current_flock_instance_programmatically,
54
+ from flock.webapp.app.dependencies import (
55
+ add_pending_custom_endpoints,
56
+ set_global_flock_services,
50
57
  )
51
-
52
- # 1. Set Theme (use provided or default)
53
- set_current_theme_name(
54
- theme_name
55
- ) # Uses default from config if theme_name is None
56
- print(f"Integrated server using theme: {get_current_theme_name()}")
57
-
58
- # 2. Set Flock Instance
59
- set_current_flock_instance_programmatically(
60
- flock_instance,
61
- f"{flock_instance.name.replace(' ', '_').lower()}_integrated.flock",
58
+ from flock.webapp.app.main import (
59
+ app as fastapi_app, # The single FastAPI app instance
62
60
  )
63
- print(f"Flock '{flock_instance.name}' preloaded.")
64
-
65
- flock_api = FlockAPI(flock_instance, custom_endpoints)
66
- webapp_fastapi_app.include_router(flock_api.app.router, prefix="/api")
67
- print("API routes included.")
68
61
 
69
- # 4. Run Uvicorn - STILL PASSING INSTANCE here because we need to modify it (add routes)
70
- # and set state BEFORE running. Reload won't work well here.
62
+ logger = get_logger("webapp.run") # Use a logger
63
+
64
+ # 1. Set UI Theme globally for the webapp
65
+ set_current_theme_name(ui_theme)
66
+ logger.info(f"Unified server configured to use theme: {get_current_theme_name()}")
67
+
68
+ # 2. Create RunStore & Set Global Services for Dependency Injection
69
+ run_store_instance = RunStore()
70
+ set_global_flock_services(flock_instance, run_store_instance)
71
+ logger.info("Global Flock instance and RunStore set for dependency injection.")
72
+
73
+ # 3. Make Flock instance and filename available on app.state
74
+ fastapi_app.state.flock_instance = flock_instance
75
+ source_file_attr = "_source_file_path" # Attribute where Flock might store its load path
76
+ fastapi_app.state.flock_filename = getattr(flock_instance, source_file_attr, None) or \
77
+ f"{flock_instance.name.replace(' ', '_').lower()}.flock.yaml"
78
+ fastapi_app.state.run_store = run_store_instance
79
+ fastapi_app.state.chat_enabled = enable_chat_routes
80
+
81
+ logger.info(f"Flock '{flock_instance.name}' (from '{fastapi_app.state.flock_filename}') made available via app.state.")
82
+
83
+ # 4. Store Custom Endpoints for registration by the lifespan manager in app.main
84
+ processed_custom_endpoints = []
85
+ if custom_endpoints:
86
+ from flock.core.api.custom_endpoint import (
87
+ FlockEndpoint, # Ensure it's imported
88
+ )
89
+ if isinstance(custom_endpoints, dict):
90
+ for (path_val, methods_val), cb_val in custom_endpoints.items():
91
+ processed_custom_endpoints.append(
92
+ FlockEndpoint(path=path_val, methods=list(methods_val) if methods_val else ["GET"], callback=cb_val)
93
+ )
94
+ else: # Assumed Sequence[FlockEndpoint]
95
+ processed_custom_endpoints.extend(list(custom_endpoints))
96
+
97
+ if processed_custom_endpoints:
98
+ add_pending_custom_endpoints(processed_custom_endpoints)
99
+ logger.info(f"{len(processed_custom_endpoints)} custom endpoints stored for registration by app lifespan.")
100
+
101
+ # 5. Update FastAPI app title (FastAPI app instance is now imported from main)
102
+ fastapi_app.title = server_title
103
+
104
+ # 5a. Optionally strip UI routes if UI is disabled
105
+ if not enable_ui_routes:
106
+ from fastapi.routing import APIRoute
107
+
108
+ allowed_tags = {"Flock API Core", "Flock API Custom Endpoints", "Chat"}
109
+
110
+ def _route_is_allowed(route: APIRoute) -> bool: # type: ignore
111
+ # Keep documentation and non-API utility routes (no tags)
112
+ if not hasattr(route, "tags") or not route.tags:
113
+ return True
114
+ # Keep if any tag is in the allowed list
115
+ return any(tag in allowed_tags for tag in route.tags) # type: ignore
116
+
117
+ original_count = len(fastapi_app.router.routes)
118
+ fastapi_app.router.routes = [r for r in fastapi_app.router.routes if _route_is_allowed(r)]
119
+
120
+ # Clear cached OpenAPI schema so FastAPI regenerates it with the reduced route set
121
+ if hasattr(fastapi_app, "openapi_schema"):
122
+ fastapi_app.openapi_schema = None # type: ignore
123
+
124
+ logger.info(
125
+ f"UI disabled: removed {original_count - len(fastapi_app.router.routes)} UI routes. Remaining routes: {len(fastapi_app.router.routes)}"
126
+ )
127
+
128
+ # 5b. Include Chat routes if requested
129
+ if enable_chat_routes:
130
+ try:
131
+ from flock.webapp.app.chat import (
132
+ router as chat_router, # type: ignore
133
+ )
134
+ fastapi_app.include_router(chat_router, tags=["Chat"])
135
+ logger.info("Chat routes enabled and registered.")
136
+ except Exception as e:
137
+ logger.error(f"Failed to include chat routes: {e}")
138
+
139
+ # 6. Run Uvicorn
140
+ logger.info(f"Running Uvicorn with application: flock.webapp.app.main:app")
71
141
  uvicorn.run(
72
- webapp_fastapi_app, host=host, port=port, reload=False
73
- ) # Ensure reload=False
142
+ "flock.webapp.app.main:app",
143
+ host=host,
144
+ port=port,
145
+ reload=False # Critical for programmatically set state like flock_instance
146
+ )
74
147
 
75
148
  except ImportError as e:
76
- print(
77
- f"Error importing components for integrated server: {e}",
78
- file=sys.stderr,
79
- )
80
- print(
81
- "Ensure all dependencies are installed and paths are correct.",
82
- file=sys.stderr,
83
- )
149
+ # More specific error logging
150
+ print(f"CRITICAL: Error importing components for unified server: {e}", file=sys.stderr)
151
+ print(f"Module not found: {e.name}", file=sys.stderr)
152
+ print("This usually means a problem with sys.path or missing dependencies.", file=sys.stderr)
153
+ print(f"Current sys.path: {sys.path}", file=sys.stderr)
84
154
  sys.exit(1)
85
155
  except Exception as e:
86
- print(f"Error starting integrated server: {e}", file=sys.stderr)
156
+ print(f"CRITICAL: Error starting unified server: {e}", file=sys.stderr)
157
+ # Consider logging the full traceback for easier debugging
158
+ import traceback
159
+ traceback.print_exc(file=sys.stderr)
87
160
  sys.exit(1)
88
161
 
89
162
 
90
- # --- Standalone Webapp Runner (for `flock --web`) ---
163
+ # --- Standalone Webapp Runner (for `flock --web` or direct execution `python -m flock.webapp.run`) ---
164
+ def main():
165
+ """Runs the Flock web application in standalone mode.
166
+ In this mode, no specific Flock is pre-loaded by the startup script;
167
+ the user will load or create one via the UI.
168
+ The FastAPI app (`webapp.app.main:app`) will initialize with DI services
169
+ set to None for Flock, and a new RunStore.
170
+ """
171
+ print("Starting Flock web application in standalone mode...")
172
+
173
+ from flock.core.api.run_store import RunStore
174
+ from flock.webapp.app.config import (
175
+ get_current_theme_name, # To log the theme being used
176
+ )
177
+ from flock.webapp.app.dependencies import set_global_flock_services
91
178
 
179
+ # No pre-loaded Flock instance; create a RunStore so API calls can still function
180
+ standalone_run_store = RunStore()
181
+ set_global_flock_services(None, standalone_run_store)
92
182
 
93
- def main():
94
- """Run the Flock web application standalone."""
95
- # Theme is now set via environment variable read by config.py on import
96
- # No need to explicitly set it here anymore.
97
- print(f"Starting standalone webapp...")
98
- # Ensure src is in path
99
- src_dir = Path(__file__).resolve().parent.parent
100
- if str(src_dir) not in sys.path:
101
- sys.path.insert(0, str(src_dir))
183
+ print(
184
+ f"Standalone mode: Initialized global services. Flock: None, RunStore: {type(standalone_run_store)}"
185
+ )
186
+ print(f"Standalone webapp using theme: {get_current_theme_name()}")
102
187
 
188
+ host = "127.0.0.1"
189
+ port = 8344
103
190
  try:
104
- # Determine host, port, and reload settings
105
- host = os.environ.get("FLOCK_WEB_HOST", "127.0.0.1")
106
- port = int(os.environ.get("FLOCK_WEB_PORT", "8344"))
107
- reload = os.environ.get("FLOCK_WEB_RELOAD", "true").lower() == "true"
108
- # Use import string for app path
109
- app_import_string = "flock.webapp.app.main:app"
110
-
111
- # No need to import app instance here anymore for standalone mode
112
- # from flock.webapp.app.main import app as webapp_fastapi_app
113
- # from flock.webapp.app.config import get_current_theme_name
114
- # print(f"Standalone webapp using theme: {get_current_theme_name()}") # Config now logs this on load
191
+ import os
115
192
 
116
- uvicorn.run(
117
- app_import_string, # Use import string for reload capability
118
- host=host,
119
- port=port,
120
- reload=reload,
121
- )
122
- except ImportError as e:
123
- # Catch potential import error during uvicorn startup if path is wrong
124
- print(f"Error loading webapp modules via Uvicorn: {e}", file=sys.stderr)
125
- print(
126
- "Make sure all required packages are installed and src path is correct.",
127
- file=sys.stderr,
128
- )
129
- sys.exit(1)
130
- except Exception as e:
131
- print(f"Error starting standalone webapp: {e}", file=sys.stderr)
132
- sys.exit(1)
193
+ host = os.environ.get("FLOCK_WEB_HOST", host)
194
+ port = int(os.environ.get("FLOCK_WEB_PORT", port))
195
+ webapp_reload = os.environ.get("FLOCK_WEB_RELOAD", "true").lower() == "true"
196
+ except Exception:
197
+ webapp_reload = True
198
+
199
+ app_import_string = "flock.webapp.app.main:app"
200
+ print(
201
+ f"Running Uvicorn: app='{app_import_string}', host='{host}', port={port}, reload={webapp_reload}"
202
+ )
203
+
204
+ uvicorn.run(
205
+ app_import_string,
206
+ host=host,
207
+ port=port,
208
+ reload=webapp_reload,
209
+ )
133
210
 
134
211
 
135
- # Note: The `if __name__ == "__main__":` block is removed as this module
136
- # is now primarily meant to be called via `main()` or `start_integrated_server()`
137
- # The CLI entry point will call `main()` after potentially calling `set_initial_theme()`.
212
+ if __name__ == "__main__":
213
+ main()
@@ -0,0 +1,227 @@
1
+ /* --- Enhanced Chat Styles --- */
2
+ body.chat-page {
3
+ display: block;
4
+ padding: 0;
5
+ margin: 0;
6
+ height: 100vh;
7
+ width: 100vw;
8
+ overflow: hidden;
9
+ background-color: var(--pico-background-color);
10
+ }
11
+
12
+ #chat-container {
13
+ /* Allow the chat container to grow to full width on small screens,
14
+ but cap it at 80% of the viewport on larger displays and
15
+ center it horizontally. */
16
+ width: 100%;
17
+ max-width: 80%;
18
+ margin: 0 auto;
19
+ display: flex;
20
+ flex-direction: column;
21
+ height: 100vh;
22
+ background-color: var(--pico-background-color);
23
+ overflow: hidden;
24
+ }
25
+
26
+ #chat-log {
27
+ flex: 1;
28
+ overflow-y: auto;
29
+ padding: 1rem 1.5rem;
30
+ display: flex;
31
+ flex-direction: column;
32
+ min-height: 60vh;
33
+ gap: 1rem;
34
+ background-color: rgba(0, 0, 0, 0.05);
35
+ background-image:
36
+ radial-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
37
+ radial-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px);
38
+ background-size: 20px 20px;
39
+ background-position: 0 0, 10px 10px;
40
+ }
41
+
42
+ .bubble {
43
+ position: relative;
44
+ padding: 0.75rem 1rem;
45
+ border-radius: 1.2rem;
46
+ max-width: 80%;
47
+ margin: 0.25rem 0;
48
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
49
+ animation: fadeIn 0.3s ease-out;
50
+ word-break: break-word;
51
+ line-height: 1.4;
52
+ }
53
+
54
+ @keyframes fadeIn {
55
+ from { opacity: 0; transform: translateY(10px); }
56
+ to { opacity: 1; transform: translateY(0); }
57
+ }
58
+
59
+ .bubble.user {
60
+ background: var(--pico-primary);
61
+ color: var(--pico-button-base-color);
62
+ margin-left: auto;
63
+ border-bottom-right-radius: 0.3rem;
64
+ text-align: right;
65
+ }
66
+
67
+ .bubble.user::after {
68
+ content: '';
69
+ position: absolute;
70
+ bottom: 0;
71
+ right: -0.5rem;
72
+ width: 1rem;
73
+ height: 1rem;
74
+ background: var(--pico-primary-hover, var(--pico-primary));
75
+ clip-path: polygon(0 0, 0% 100%, 100% 100%);
76
+ }
77
+
78
+ .bubble.bot {
79
+ background: var(--pico-code-background-color);
80
+ color: var(--pico-code-color);
81
+ margin-right: auto;
82
+ border-bottom-left-radius: 0.3rem;
83
+ }
84
+
85
+ .bubble.bot::after {
86
+ content: '';
87
+ position: absolute;
88
+ bottom: 0;
89
+ left: -0.5rem;
90
+ width: 1rem;
91
+ height: 1rem;
92
+ background: var(--pico-code-background-color);
93
+ clip-path: polygon(100% 0, 0% 100%, 100% 100%);
94
+ }
95
+
96
+ #chat-container form {
97
+ display: flex;
98
+ gap: 0.5rem;
99
+ margin: 0;
100
+ padding: 1rem;
101
+ background-color: var(--pico-card-background-color);
102
+ border-top: 1px solid var(--pico-muted-border-color);
103
+ }
104
+
105
+ #chat-container form input[type="text"] {
106
+ flex: 1;
107
+ border-radius: 2rem;
108
+ padding-left: 1.25rem;
109
+ background-color: var(--pico-background-color);
110
+ border: 1px solid var(--pico-muted-border-color);
111
+ transition: all 0.2s ease;
112
+ height: 3rem;
113
+ font-size: 1rem;
114
+ }
115
+
116
+ #chat-container form input[type="text"]:focus {
117
+ border-color: var(--pico-primary);
118
+ box-shadow: 0 0 0 2px rgba(var(--pico-primary-rgb, 0, 123, 255), 0.25);
119
+ }
120
+
121
+ #chat-form button {
122
+ flex: 0 0 auto;
123
+ min-width: auto;
124
+ width: 150px; /* Wider send button */
125
+ padding: 0 1rem;
126
+ border-radius: 2rem;
127
+ background: var(--pico-primary);
128
+ color: var(--pico-button-base-color);
129
+ font-weight: 600;
130
+ transition: all 0.2s ease;
131
+ margin-left: 0.5rem;
132
+ height: 3rem;
133
+ }
134
+
135
+ #chat-form button:hover {
136
+ background: var(--pico-primary-hover, var(--pico-primary));
137
+ transform: translateY(-2px);
138
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
139
+ }
140
+
141
+ .chat-header {
142
+ padding: 1rem 1.5rem;
143
+ background-color: var(--pico-card-background-color);
144
+ border-bottom: 1px solid var(--pico-muted-border-color);
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: space-between;
148
+ flex-shrink: 0;
149
+ }
150
+
151
+ .chat-header h2 {
152
+ margin: 0;
153
+ font-size: 1.5rem;
154
+ color: var(--pico-color);
155
+ }
156
+
157
+ .chat-header h3 {
158
+ margin: 0;
159
+ font-size: 0.9rem;
160
+ color: var(--pico-muted-color);
161
+ font-weight: normal;
162
+ }
163
+
164
+ .chat-timestamp {
165
+ display: block;
166
+ font-size: 0.7rem;
167
+ color: var(--pico-button-base-color);
168
+ margin-top: 0.25rem;
169
+ opacity: 0.8;
170
+ }
171
+
172
+ .chat-footer {
173
+ padding: 0.5rem 1rem;
174
+ text-align: center;
175
+ font-size: 0.8rem;
176
+ color: var(--pico-muted-color);
177
+ background-color: var(--pico-card-background-color);
178
+ border-top: 1px solid var(--pico-muted-border-color);
179
+ flex-shrink: 0;
180
+ }
181
+ /* --- End Enhanced Chat Styles --- */
182
+
183
+ /* -------------------------------------------------------------------------
184
+ Chat container tweaks when embedded in the main UI (non-standalone mode)
185
+ ------------------------------------------------------------------------- */
186
+ body:not(.chat-page) #chat-container,
187
+ body:not(.chat-page) .chat-container {
188
+ height: auto;
189
+ min-height: 60vh;
190
+ /* share same centering rules */
191
+ width: 100%;
192
+ max-width: 80%;
193
+ margin: 0 auto;
194
+ }
195
+
196
+ /* Settings form inside chat container should stack vertically */
197
+ .chat-settings-form {
198
+ display: flex;
199
+ flex-direction: column;
200
+ min-height: 60vh;
201
+ min-width: 100%;
202
+ }
203
+
204
+ .chat-settings-form label,
205
+ .chat-settings-form input,
206
+ .chat-settings-form select {
207
+ display: block;
208
+ width: 100%;
209
+ margin-bottom: 0.75rem;
210
+ }
211
+
212
+ .chat-settings-form .grid {
213
+ display: flex;
214
+ gap: 0.5rem;
215
+ }
216
+
217
+ /* Button sizing for Save Settings */
218
+ .chat-settings-form .grid button:first-child {
219
+ flex: 1 1 auto; /* expand in standalone */
220
+ width: 50%;
221
+ }
222
+
223
+ /* In integrated UI (non chat-page) keep natural size */
224
+ body:not(.chat-page) .chat-settings-form .grid button:first-child {
225
+ flex: 0 0 auto;
226
+ width: auto;
227
+ }