dtSpark 1.1.0a3__py3-none-any.whl → 1.1.0a7__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.
Files changed (54) hide show
  1. dtSpark/_version.txt +1 -1
  2. dtSpark/aws/authentication.py +1 -1
  3. dtSpark/aws/bedrock.py +238 -239
  4. dtSpark/aws/costs.py +9 -5
  5. dtSpark/aws/pricing.py +25 -21
  6. dtSpark/cli_interface.py +77 -68
  7. dtSpark/conversation_manager.py +54 -47
  8. dtSpark/core/application.py +114 -91
  9. dtSpark/core/context_compaction.py +241 -226
  10. dtSpark/daemon/__init__.py +36 -22
  11. dtSpark/daemon/action_monitor.py +46 -17
  12. dtSpark/daemon/daemon_app.py +126 -104
  13. dtSpark/daemon/daemon_manager.py +59 -23
  14. dtSpark/daemon/pid_file.py +3 -2
  15. dtSpark/database/autonomous_actions.py +3 -0
  16. dtSpark/database/credential_prompt.py +52 -54
  17. dtSpark/files/manager.py +6 -12
  18. dtSpark/limits/__init__.py +1 -1
  19. dtSpark/limits/tokens.py +2 -2
  20. dtSpark/llm/anthropic_direct.py +246 -141
  21. dtSpark/llm/ollama.py +3 -1
  22. dtSpark/mcp_integration/manager.py +4 -4
  23. dtSpark/mcp_integration/tool_selector.py +83 -77
  24. dtSpark/resources/config.yaml.template +11 -0
  25. dtSpark/safety/patterns.py +45 -46
  26. dtSpark/safety/prompt_inspector.py +8 -1
  27. dtSpark/scheduler/creation_tools.py +273 -181
  28. dtSpark/scheduler/executor.py +503 -221
  29. dtSpark/tools/builtin.py +70 -53
  30. dtSpark/web/endpoints/autonomous_actions.py +12 -9
  31. dtSpark/web/endpoints/chat.py +8 -6
  32. dtSpark/web/endpoints/conversations.py +18 -9
  33. dtSpark/web/endpoints/main_menu.py +132 -105
  34. dtSpark/web/endpoints/streaming.py +2 -2
  35. dtSpark/web/server.py +70 -5
  36. dtSpark/web/ssl_utils.py +3 -3
  37. dtSpark/web/static/css/dark-theme.css +8 -29
  38. dtSpark/web/static/js/chat.js +6 -8
  39. dtSpark/web/static/js/main.js +8 -8
  40. dtSpark/web/static/js/sse-client.js +130 -122
  41. dtSpark/web/templates/actions.html +5 -5
  42. dtSpark/web/templates/base.html +15 -0
  43. dtSpark/web/templates/chat.html +10 -10
  44. dtSpark/web/templates/conversations.html +6 -2
  45. dtSpark/web/templates/goodbye.html +2 -2
  46. dtSpark/web/templates/main_menu.html +19 -17
  47. dtSpark/web/web_interface.py +2 -2
  48. {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a7.dist-info}/METADATA +9 -2
  49. dtspark-1.1.0a7.dist-info/RECORD +96 -0
  50. dtspark-1.1.0a3.dist-info/RECORD +0 -96
  51. {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a7.dist-info}/WHEEL +0 -0
  52. {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a7.dist-info}/entry_points.txt +0 -0
  53. {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a7.dist-info}/licenses/LICENSE +0 -0
  54. {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a7.dist-info}/top_level.txt +0 -0
@@ -80,50 +80,17 @@ async def get_account_info(
80
80
  """
81
81
  try:
82
82
  app_instance = request.app.state.app_instance
83
-
84
- # Get LLM manager to determine active provider
85
83
  llm_manager = getattr(app_instance, 'llm_manager', None)
86
84
  user_guid = getattr(app_instance, 'user_guid', 'unknown')
87
85
 
88
86
  if llm_manager and llm_manager.active_provider:
89
- active_provider = llm_manager.active_provider.lower()
90
-
91
- # AWS Bedrock
92
- if 'bedrock' in active_provider or 'aws' in active_provider:
93
- auth = getattr(app_instance, 'authenticator', None)
94
- if auth:
95
- account_info = auth.get_account_info()
96
- if account_info:
97
- return AccountInfo(
98
- provider='aws',
99
- user_arn=account_info.get('user_arn'),
100
- account_id=account_info.get('account_id'),
101
- region=account_info.get('region'),
102
- user_guid=user_guid,
103
- auth_method=account_info.get('auth_method'),
104
- )
105
-
106
- # Anthropic
107
- elif 'anthropic' in active_provider:
108
- return AccountInfo(
109
- provider='anthropic',
110
- user_guid=user_guid,
111
- auth_method='api_key',
112
- )
113
-
114
- # Ollama
115
- elif 'ollama' in active_provider:
116
- return AccountInfo(
117
- provider='ollama',
118
- user_guid=user_guid,
119
- auth_method='local',
120
- )
87
+ result = _build_account_info_for_provider(
88
+ app_instance, llm_manager.active_provider.lower(), user_guid
89
+ )
90
+ if result:
91
+ return result
121
92
 
122
- # No provider configured - return basic info
123
- return AccountInfo(
124
- provider='none',
125
- user_guid=user_guid,
126
- )
93
+ return AccountInfo(provider='none', user_guid=user_guid)
127
94
 
128
95
  except HTTPException:
129
96
  raise
@@ -132,6 +99,40 @@ async def get_account_info(
132
99
  raise HTTPException(status_code=500, detail=str(e))
133
100
 
134
101
 
102
+ def _build_account_info_for_provider(
103
+ app_instance, active_provider: str, user_guid: str
104
+ ) -> Optional[AccountInfo]:
105
+ """Build AccountInfo for the given active provider, or return None if unavailable."""
106
+ if 'bedrock' in active_provider or 'aws' in active_provider:
107
+ return _build_aws_account_info(app_instance, user_guid)
108
+
109
+ if 'anthropic' in active_provider:
110
+ return AccountInfo(provider='anthropic', user_guid=user_guid, auth_method='api_key')
111
+
112
+ if 'ollama' in active_provider:
113
+ return AccountInfo(provider='ollama', user_guid=user_guid, auth_method='local')
114
+
115
+ return None
116
+
117
+
118
+ def _build_aws_account_info(app_instance, user_guid: str) -> Optional[AccountInfo]:
119
+ """Build AccountInfo from the AWS authenticator, or return None."""
120
+ auth = getattr(app_instance, 'authenticator', None)
121
+ if not auth:
122
+ return None
123
+ account_info = auth.get_account_info()
124
+ if not account_info:
125
+ return None
126
+ return AccountInfo(
127
+ provider='aws',
128
+ user_arn=account_info.get('user_arn'),
129
+ account_id=account_info.get('account_id'),
130
+ region=account_info.get('region'),
131
+ user_guid=user_guid,
132
+ auth_method=account_info.get('auth_method'),
133
+ )
134
+
135
+
135
136
  @router.get("/providers")
136
137
  async def get_providers(
137
138
  request: Request,
@@ -147,71 +148,11 @@ async def get_providers(
147
148
  app_instance = request.app.state.app_instance
148
149
  providers = []
149
150
 
150
- # Get LLM manager which has all registered providers
151
151
  llm_manager = getattr(app_instance, 'llm_manager', None)
152
-
153
152
  if llm_manager and hasattr(llm_manager, 'providers'):
154
153
  for provider_name, service in llm_manager.providers.items():
155
- models = []
156
- status = 'connected'
157
- provider_type = 'unknown'
158
- auth_method = None
159
- region = None
160
- base_url = None
161
-
162
- # Determine provider type
163
- provider_name_lower = provider_name.lower()
164
- if 'bedrock' in provider_name_lower or 'aws' in provider_name_lower:
165
- provider_type = 'aws'
166
- auth = getattr(app_instance, 'authenticator', None)
167
- if auth:
168
- account_info = auth.get_account_info()
169
- if account_info:
170
- auth_method = account_info.get('auth_method')
171
- region = account_info.get('region')
172
- elif 'anthropic' in provider_name_lower:
173
- provider_type = 'anthropic'
174
- auth_method = 'api_key'
175
- elif 'ollama' in provider_name_lower:
176
- provider_type = 'ollama'
177
- auth_method = 'local'
178
- base_url = getattr(service, 'base_url', 'http://localhost:11434')
179
-
180
- # Get available models
181
- try:
182
- if hasattr(service, 'list_available_models'):
183
- available_models = service.list_available_models()
184
- elif hasattr(service, 'list_models'):
185
- available_models = service.list_models()
186
- else:
187
- available_models = []
188
-
189
- for model in available_models:
190
- if isinstance(model, dict):
191
- model_id = model.get('id') or model.get('modelId') or model.get('name') or str(model)
192
- display_name = model.get('display_name') or model.get('modelName') or model_id
193
- else:
194
- model_id = str(model)
195
- display_name = model_id
196
-
197
- models.append(ProviderModelInfo(
198
- model_id=model_id,
199
- display_name=display_name,
200
- ))
201
- except Exception as e:
202
- logger.warning(f"Failed to list models from {provider_name}: {e}")
203
- status = 'error'
204
-
205
- providers.append(ProviderInfo(
206
- name=provider_name,
207
- type=provider_type,
208
- enabled=True,
209
- status=status,
210
- models=models,
211
- auth_method=auth_method,
212
- region=region,
213
- base_url=base_url,
214
- ))
154
+ provider_info = _build_provider_info(app_instance, provider_name, service)
155
+ providers.append(provider_info)
215
156
 
216
157
  return providers
217
158
 
@@ -220,6 +161,88 @@ async def get_providers(
220
161
  raise HTTPException(status_code=500, detail=str(e))
221
162
 
222
163
 
164
+ def _build_provider_info(app_instance, provider_name: str, service) -> ProviderInfo:
165
+ """Build a ProviderInfo for a single registered provider."""
166
+ provider_type, auth_method, region, base_url = _detect_provider_type(
167
+ app_instance, provider_name, service
168
+ )
169
+
170
+ models, status = _list_provider_models(provider_name, service)
171
+
172
+ return ProviderInfo(
173
+ name=provider_name,
174
+ type=provider_type,
175
+ enabled=True,
176
+ status=status,
177
+ models=models,
178
+ auth_method=auth_method,
179
+ region=region,
180
+ base_url=base_url,
181
+ )
182
+
183
+
184
+ def _detect_provider_type(app_instance, provider_name: str, service) -> tuple:
185
+ """Detect the provider type, auth method, region, and base URL from the provider name."""
186
+ provider_name_lower = provider_name.lower()
187
+ auth_method = None
188
+ region = None
189
+ base_url = None
190
+
191
+ if 'bedrock' in provider_name_lower or 'aws' in provider_name_lower:
192
+ provider_type = 'aws'
193
+ auth = getattr(app_instance, 'authenticator', None)
194
+ if auth:
195
+ account_info = auth.get_account_info()
196
+ if account_info:
197
+ auth_method = account_info.get('auth_method')
198
+ region = account_info.get('region')
199
+ elif 'anthropic' in provider_name_lower:
200
+ provider_type = 'anthropic'
201
+ auth_method = 'api_key'
202
+ elif 'ollama' in provider_name_lower:
203
+ provider_type = 'ollama'
204
+ auth_method = 'local'
205
+ base_url = getattr(service, 'base_url', 'http://localhost:11434')
206
+ else:
207
+ provider_type = 'unknown'
208
+
209
+ return provider_type, auth_method, region, base_url
210
+
211
+
212
+ def _list_provider_models(provider_name: str, service) -> tuple:
213
+ """List available models from a provider service. Returns (models, status)."""
214
+ models = []
215
+ status = 'connected'
216
+
217
+ try:
218
+ if hasattr(service, 'list_available_models'):
219
+ available_models = service.list_available_models()
220
+ elif hasattr(service, 'list_models'):
221
+ available_models = service.list_models()
222
+ else:
223
+ available_models = []
224
+
225
+ for model in available_models:
226
+ models.append(_parse_model_info(model))
227
+ except Exception as e:
228
+ logger.warning(f"Failed to list models from {provider_name}: {e}")
229
+ status = 'error'
230
+
231
+ return models, status
232
+
233
+
234
+ def _parse_model_info(model) -> ProviderModelInfo:
235
+ """Parse a model entry (dict or string) into a ProviderModelInfo."""
236
+ if isinstance(model, dict):
237
+ model_id = model.get('id') or model.get('modelId') or model.get('name') or str(model)
238
+ display_name = model.get('display_name') or model.get('modelName') or model_id
239
+ else:
240
+ model_id = str(model)
241
+ display_name = model_id
242
+
243
+ return ProviderModelInfo(model_id=model_id, display_name=display_name)
244
+
245
+
223
246
  @router.get("/costs/last-month")
224
247
  async def get_last_month_costs(
225
248
  request: Request,
@@ -607,14 +630,18 @@ async def get_daemon_status(request: Request):
607
630
  except Exception:
608
631
  pass
609
632
 
633
+ if not daemon_running and scheduled_count > 0:
634
+ warning_message = (
635
+ f"Daemon is not running - {scheduled_count} scheduled action(s) will not execute"
636
+ )
637
+ else:
638
+ warning_message = None
639
+
610
640
  return {
611
641
  'daemon_running': daemon_running,
612
642
  'daemon_pid': daemon_pid,
613
643
  'scheduled_actions_count': scheduled_count,
614
- 'warning': None if daemon_running else (
615
- f"Daemon is not running - {scheduled_count} scheduled action(s) will not execute"
616
- if scheduled_count > 0 else None
617
- )
644
+ 'warning': warning_message,
618
645
  }
619
646
 
620
647
  except Exception as e:
@@ -151,7 +151,7 @@ class StreamingManager:
151
151
  "content": result.get('content', ''),
152
152
  }),
153
153
  }
154
- except:
154
+ except ValueError:
155
155
  pass
156
156
 
157
157
  elif role == 'assistant' and content.strip().startswith('['):
@@ -179,7 +179,7 @@ class StreamingManager:
179
179
  "input": block.get('input', {}),
180
180
  }),
181
181
  }
182
- except:
182
+ except ValueError:
183
183
  pass
184
184
 
185
185
  last_message_count = len(current_messages)
dtSpark/web/server.py CHANGED
@@ -9,6 +9,7 @@ import os
9
9
  import sys
10
10
  import socket
11
11
  import logging
12
+ import time
12
13
  import webbrowser
13
14
  import signal
14
15
  import asyncio
@@ -252,6 +253,48 @@ def create_app(
252
253
  loop = asyncio.get_running_loop()
253
254
  loop.set_exception_handler(_suppress_connection_reset_errors)
254
255
 
256
+ # Set to hold references to background tasks (prevents garbage collection)
257
+ _background_tasks = set()
258
+
259
+ # Browser heartbeat state
260
+ app.state.last_heartbeat = 0.0
261
+
262
+ @app.post("/api/heartbeat")
263
+ async def heartbeat():
264
+ """Receive browser heartbeat ping."""
265
+ app.state.last_heartbeat = time.time()
266
+ return JSONResponse({"status": "ok"})
267
+
268
+ @app.on_event("startup")
269
+ async def start_heartbeat_monitor():
270
+ """Start background task to monitor browser heartbeat."""
271
+ if not heartbeat_enabled:
272
+ return
273
+
274
+ async def _monitor_heartbeat():
275
+ # Initial grace period - wait for browser to connect and send first heartbeat
276
+ grace_period = heartbeat_timeout * 2
277
+ logger.info(
278
+ f"Browser heartbeat monitor started (interval={heartbeat_interval}s, "
279
+ f"timeout={heartbeat_timeout}s, grace={grace_period}s)"
280
+ )
281
+ await asyncio.sleep(grace_period)
282
+
283
+ while True:
284
+ await asyncio.sleep(heartbeat_interval)
285
+ last = app.state.last_heartbeat
286
+ if last > 0 and (time.time() - last) > heartbeat_timeout:
287
+ logger.info(
288
+ f"No browser heartbeat for {heartbeat_timeout}s - "
289
+ f"shutting down (browser likely closed)"
290
+ )
291
+ os.kill(os.getpid(), signal.SIGTERM)
292
+ return
293
+
294
+ task = asyncio.create_task(_monitor_heartbeat())
295
+ _background_tasks.add(task)
296
+ task.add_done_callback(_background_tasks.discard)
297
+
255
298
  # Get template and static directories
256
299
  web_dir = Path(__file__).parent
257
300
  templates_dir = web_dir / "templates"
@@ -260,11 +303,26 @@ def create_app(
260
303
  # Setup templates
261
304
  templates = Jinja2Templates(directory=str(templates_dir))
262
305
 
306
+ # Determine feature flags from app instance
307
+ actions_enabled = getattr(app_instance, 'actions_enabled', False)
308
+ new_conversations_allowed = getattr(app_instance, 'new_conversations_allowed', True)
309
+
310
+ # Read heartbeat configuration
311
+ from dtPyAppFramework.settings import Settings as _Settings
312
+ _hb_settings = _Settings()
313
+ heartbeat_enabled = _hb_settings.get('interface.web.browser_heartbeat.enabled', True)
314
+ heartbeat_interval = _hb_settings.get('interface.web.browser_heartbeat.interval_seconds', 15)
315
+ heartbeat_timeout = _hb_settings.get('interface.web.browser_heartbeat.timeout_seconds', 60)
316
+
263
317
  # Add global template variables for app name and version
264
318
  templates.env.globals['app_name'] = full_name()
265
319
  templates.env.globals['app_version'] = version()
266
320
  templates.env.globals['app_description'] = description()
267
321
  templates.env.globals['agent_name'] = agent_name()
322
+ templates.env.globals['actions_enabled'] = actions_enabled
323
+ templates.env.globals['new_conversations_allowed'] = new_conversations_allowed
324
+ templates.env.globals['heartbeat_enabled'] = heartbeat_enabled
325
+ templates.env.globals['heartbeat_interval_ms'] = heartbeat_interval * 1000
268
326
 
269
327
  # Mount static files
270
328
  app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
@@ -276,6 +334,7 @@ def create_app(
276
334
  app.state.templates = templates
277
335
  app.state.dark_theme = dark_theme
278
336
  app.state.cost_tracking_enabled = cost_tracking_enabled
337
+ app.state.new_conversations_allowed = new_conversations_allowed
279
338
 
280
339
  # Session dependency
281
340
  async def get_session(session_id: Optional[str] = Cookie(default=None)) -> str:
@@ -428,13 +487,14 @@ def create_app(
428
487
  logger.info("Shutdown request received via API")
429
488
  # Send shutdown signal to the process
430
489
  # Use a background task to allow the response to be sent first
431
- import asyncio
432
490
  async def shutdown_server():
433
491
  await asyncio.sleep(0.5) # Give time for response to be sent
434
492
  logger.info("Shutting down web server...")
435
493
  os.kill(os.getpid(), signal.SIGTERM)
436
494
 
437
- asyncio.create_task(shutdown_server())
495
+ task = asyncio.create_task(shutdown_server())
496
+ _background_tasks.add(task)
497
+ task.add_done_callback(_background_tasks.discard)
438
498
  return JSONResponse({"status": "shutdown initiated"})
439
499
 
440
500
  @app.get("/menu", response_class=HTMLResponse)
@@ -456,13 +516,14 @@ def create_app(
456
516
  chat_router,
457
517
  streaming_router,
458
518
  )
459
- from .endpoints.autonomous_actions import router as autonomous_actions_router
460
-
461
519
  app.include_router(main_menu_router, prefix="/api", tags=["Main Menu"])
462
520
  app.include_router(conversations_router, prefix="/api", tags=["Conversations"])
463
521
  app.include_router(chat_router, prefix="/api", tags=["Chat"])
464
522
  app.include_router(streaming_router, prefix="/api", tags=["Streaming"])
465
- app.include_router(autonomous_actions_router, prefix="/api", tags=["Autonomous Actions"])
523
+
524
+ if actions_enabled:
525
+ from .endpoints.autonomous_actions import router as autonomous_actions_router
526
+ app.include_router(autonomous_actions_router, prefix="/api", tags=["Autonomous Actions"])
466
527
 
467
528
  # Add template routes for conversations and chat
468
529
  @app.get("/conversations", response_class=HTMLResponse)
@@ -480,6 +541,8 @@ def create_app(
480
541
  @app.get("/conversations/new", response_class=HTMLResponse)
481
542
  async def new_conversation_page(request: Request, session_id: str = Depends(get_session)):
482
543
  """Display new conversation creation page."""
544
+ if not new_conversations_allowed:
545
+ return RedirectResponse(url="/conversations", status_code=303)
483
546
  return templates.TemplateResponse(
484
547
  "new_conversation.html",
485
548
  {
@@ -515,6 +578,8 @@ def create_app(
515
578
  @app.get("/actions", response_class=HTMLResponse)
516
579
  async def actions_page(request: Request, session_id: str = Depends(get_session)):
517
580
  """Display autonomous actions management page."""
581
+ if not actions_enabled:
582
+ return RedirectResponse(url="/menu", status_code=303)
518
583
  return templates.TemplateResponse(
519
584
  "actions.html",
520
585
  {
dtSpark/web/ssl_utils.py CHANGED
@@ -10,7 +10,7 @@ import os.path
10
10
  import socket
11
11
  import ipaddress
12
12
  from pathlib import Path
13
- from datetime import datetime, timedelta
13
+ from datetime import datetime, timedelta, timezone
14
14
  from typing import Tuple, Optional
15
15
 
16
16
  from dtPyAppFramework.paths import ApplicationPaths
@@ -72,8 +72,8 @@ def generate_self_signed_certificate(
72
72
  .issuer_name(issuer)
73
73
  .public_key(private_key.public_key())
74
74
  .serial_number(x509.random_serial_number())
75
- .not_valid_before(datetime.utcnow())
76
- .not_valid_after(datetime.utcnow() + timedelta(days=validity_days))
75
+ .not_valid_before(datetime.now(timezone.utc))
76
+ .not_valid_after(datetime.now(timezone.utc) + timedelta(days=validity_days))
77
77
  .add_extension(
78
78
  x509.SubjectAlternativeName([
79
79
  x509.DNSName(hostname),
@@ -94,7 +94,8 @@ body {
94
94
  }
95
95
 
96
96
  .chat-message .message-content pre {
97
- background-color: rgba(0, 0, 0, 0.5);
97
+ background-color: #0d1117;
98
+ border: 1px solid #30363d;
98
99
  padding: 1rem;
99
100
  border-radius: 0.5rem;
100
101
  overflow-x: auto;
@@ -195,16 +196,19 @@ body {
195
196
 
196
197
  /* Code blocks in markdown */
197
198
  .markdown-content pre {
198
- background-color: rgba(0, 0, 0, 0.5);
199
+ background-color: #0d1117;
200
+ border: 1px solid #30363d;
199
201
  padding: 1rem;
200
202
  border-radius: 0.5rem;
201
203
  overflow-x: auto;
204
+ margin: 1rem 0;
202
205
  }
203
206
 
204
207
  .markdown-content code {
205
- background-color: rgba(0, 0, 0, 0.3);
208
+ background-color: rgba(110, 118, 129, 0.3);
206
209
  padding: 0.2rem 0.4rem;
207
210
  border-radius: 0.25rem;
211
+ font-size: 0.875em;
208
212
  }
209
213
 
210
214
  /* Alerts */
@@ -324,7 +328,7 @@ body {
324
328
  border: 1px solid #f44336;
325
329
  border-radius: 0.5rem;
326
330
  padding: 1rem;
327
- color: #ff8a80;
331
+ color: #ffd7d3;
328
332
  }
329
333
 
330
334
  .mermaid-error .text-danger {
@@ -339,14 +343,6 @@ body {
339
343
  padding: 0;
340
344
  }
341
345
 
342
- .chat-message .message-content pre {
343
- background-color: #0d1117;
344
- border: 1px solid #30363d;
345
- border-radius: 0.5rem;
346
- padding: 1rem;
347
- overflow-x: auto;
348
- }
349
-
350
346
  .chat-message .message-content code:not(.hljs) {
351
347
  background-color: rgba(110, 118, 129, 0.3);
352
348
  padding: 0.2rem 0.4rem;
@@ -354,29 +350,12 @@ body {
354
350
  font-size: 0.875em;
355
351
  }
356
352
 
357
- /* Markdown content styling */
358
- .markdown-content pre {
359
- background-color: #0d1117;
360
- border: 1px solid #30363d;
361
- border-radius: 0.5rem;
362
- padding: 1rem;
363
- overflow-x: auto;
364
- margin: 1rem 0;
365
- }
366
-
367
353
  .markdown-content pre code {
368
354
  background-color: transparent;
369
355
  padding: 0;
370
356
  border-radius: 0;
371
357
  }
372
358
 
373
- .markdown-content code {
374
- background-color: rgba(110, 118, 129, 0.3);
375
- padding: 0.2rem 0.4rem;
376
- border-radius: 0.25rem;
377
- font-size: 0.875em;
378
- }
379
-
380
359
  .markdown-content blockquote {
381
360
  border-left: 4px solid var(--spark-cyan);
382
361
  padding-left: 1rem;
@@ -50,8 +50,6 @@ async function loadChatHistory(conversationId) {
50
50
  * @param {string} timestamp - Message timestamp (optional)
51
51
  */
52
52
  function appendMessage(role, content, timestamp = null) {
53
- const messagesContainer = document.getElementById('chat-messages');
54
-
55
53
  // Check if content contains tool results
56
54
  if (content.startsWith('[TOOL_RESULTS]')) {
57
55
  appendToolResults(content, timestamp);
@@ -107,7 +105,7 @@ function appendRegularMessage(role, content, timestamp = null) {
107
105
  messageDiv.className = `chat-message ${role}`;
108
106
 
109
107
  // Generate unique ID for the copy button
110
- const copyBtnId = 'copy-btn-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
108
+ const copyBtnId = 'copy-btn-' + Date.now() + '-' + Math.random().toString(36).substring(2, 11);
111
109
 
112
110
  // Create message header with copy icon
113
111
  const header = document.createElement('div');
@@ -308,8 +306,8 @@ function removeStatus(idOrElement) {
308
306
  document.getElementById(idOrElement) :
309
307
  idOrElement;
310
308
 
311
- if (element && element.parentNode) {
312
- element.parentNode.removeChild(element);
309
+ if (element?.parentNode) {
310
+ element.remove();
313
311
  }
314
312
  }
315
313
 
@@ -360,7 +358,7 @@ function updateStreamingMessage(content, messageElement = null) {
360
358
  messageElement.className = 'chat-message assistant';
361
359
 
362
360
  // Generate unique ID for the copy button
363
- const copyBtnId = 'stream-copy-btn-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
361
+ const copyBtnId = 'stream-copy-btn-' + Date.now() + '-' + Math.random().toString(36).substring(2, 11);
364
362
 
365
363
  messageElement.innerHTML = `
366
364
  <div class="message-header d-flex justify-content-between align-items-center">
@@ -463,7 +461,7 @@ function showToast(message, type = 'info') {
463
461
  const toast = document.createElement('div');
464
462
  toast.id = toastId;
465
463
  toast.className = 'toast';
466
- toast.setAttribute('role', 'alert');
464
+ toast.role = 'alert';
467
465
 
468
466
  let bgClass = 'bg-primary';
469
467
  if (type === 'success') bgClass = 'bg-success';
@@ -550,7 +548,7 @@ async function showToolPermissionDialog(requestId, toolName, toolDescription) {
550
548
  const buttons = modalElement.querySelectorAll('.permission-btn');
551
549
  buttons.forEach(button => {
552
550
  button.addEventListener('click', async () => {
553
- const response = button.getAttribute('data-response');
551
+ const response = button.dataset.response;
554
552
 
555
553
  // Send response to server
556
554
  try {
@@ -74,7 +74,7 @@ if (typeof marked !== 'undefined') {
74
74
 
75
75
  // Handle mermaid diagrams
76
76
  if (lang === 'mermaid') {
77
- const id = 'mermaid-' + Math.random().toString(36).substr(2, 9);
77
+ const id = 'mermaid-' + Math.random().toString(36).substring(2, 11);
78
78
  return `<div class="mermaid-container"><pre class="mermaid" id="${id}">${escapeHtmlForMermaid(code)}</pre></div>`;
79
79
  }
80
80
 
@@ -157,7 +157,7 @@ async function copySvgToClipboard(svgElement, button) {
157
157
  bgRect.setAttribute('width', '100%');
158
158
  bgRect.setAttribute('height', '100%');
159
159
  bgRect.setAttribute('fill', '#1a1a1a');
160
- svgClone.insertBefore(bgRect, svgClone.firstChild);
160
+ svgClone.prepend(bgRect);
161
161
 
162
162
  // Serialise SVG to string
163
163
  const serializer = new XMLSerializer();
@@ -255,7 +255,7 @@ async function renderMermaidDiagrams(container) {
255
255
 
256
256
  for (const block of mermaidBlocks) {
257
257
  try {
258
- const id = block.id || 'mermaid-' + Math.random().toString(36).substr(2, 9);
258
+ const id = block.id || 'mermaid-' + Math.random().toString(36).substring(2, 11);
259
259
  const code = block.textContent;
260
260
 
261
261
  // Render the diagram
@@ -287,7 +287,7 @@ async function renderMermaidDiagrams(container) {
287
287
  }
288
288
  };
289
289
 
290
- block.parentNode.replaceChild(wrapper, block);
290
+ block.replaceWith(wrapper);
291
291
  } catch (e) {
292
292
  console.error('Mermaid rendering error:', e);
293
293
  // Show error message in the block
@@ -370,9 +370,9 @@ function showToast(message, type = 'info') {
370
370
  // Create toast
371
371
  const toast = document.createElement('div');
372
372
  toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
373
- toast.setAttribute('role', 'alert');
374
- toast.setAttribute('aria-live', 'assertive');
375
- toast.setAttribute('aria-atomic', 'true');
373
+ toast.role = 'alert';
374
+ toast.ariaLive = 'assertive';
375
+ toast.ariaAtomic = 'true';
376
376
 
377
377
  toast.innerHTML = `
378
378
  <div class="d-flex">
@@ -432,7 +432,7 @@ function downloadFile(content, filename, mimeType = 'text/plain') {
432
432
  link.download = filename;
433
433
  document.body.appendChild(link);
434
434
  link.click();
435
- document.body.removeChild(link);
435
+ link.remove();
436
436
  URL.revokeObjectURL(url);
437
437
  }
438
438