Z User commited on
Commit
3fa1cf7
Β·
1 Parent(s): 817e3da

feat: Add HF Bucket storage support & cool new GUI

Browse files

- Add HF Storage Bucket integration for persistent cloud storage
- New create_bucket() method for creating cloud buckets
- New write_to_bucket() for direct cloud file writes
- New read_from_bucket(), list_bucket_files(), delete_from_bucket()
- New sync_to_bucket(), get_bucket_status() methods
- Support for 'bucket' mode in addition to local/hub/hybrid

- Update constants.py with bucket configuration
- HF_BUCKET_ENABLED, HF_BUCKET_REPO_ID settings
- HF_STORAGE_MODE options: local/hub/hybrid/bucket/auto
- Updated system prompt with cloud storage awareness
- Version bumped to 2.2.0

- Enhanced GUI with modern design
- Animated gradient background with particles
- Glassmorphism effects on panels
- Neon glow effects on interactive elements
- Smooth animations and transitions
- Modern typography hierarchy
- Professional dark theme with cyan/purple accents

Files changed (3) hide show
  1. code/config/constants.py +37 -5
  2. code/hub_storage.py +677 -132
  3. index.html +2248 -430
code/config/constants.py CHANGED
@@ -6,6 +6,7 @@ Enhanced version with:
6
  - Session management settings
7
  - Performance tuning parameters
8
  - Security configurations
 
9
  """
10
 
11
  from __future__ import annotations
@@ -15,7 +16,7 @@ import re
15
  # ─── App Identity ────────────────────────────────────────────────────────
16
 
17
  APP_TITLE = "SoniCoder"
18
- APP_VERSION = "2.1.0" # Enhanced version
19
  MODEL_URL = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct"
20
 
21
  # ─── Model Configs ───────────────────────────────────────────────────────
@@ -104,6 +105,30 @@ BLOCKED_PATTERNS_EXTRA = [
104
  r"subprocess\.Popen",
105
  ]
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  # ─── Regex Patterns ─────────────────────────────────────────────────────
108
 
109
  THINKING_BLOCK_RE = re.compile(
@@ -429,11 +454,11 @@ LANGUAGE_MAP: dict[str, list[str]] = {lang: frameworks for lang, frameworks in L
429
 
430
  # ─── System Prompt (Enhanced) ────────────────────────────────────────────
431
 
432
- SYSTEM_PROMPT = """You are SoniCoder v2.1 β€” an advanced autonomous coding agent that writes files to a sandboxed workspace using tools.
433
 
434
  CAPABILITIES:
435
  - Generate complete, runnable applications in any language/framework
436
- - Read, write, and edit files in the workspace via tools
437
  - Run shell commands (git, npm, pip, tests) via the bash tool
438
  - Track multi-step tasks with the todo system
439
  - Apply specialized skills (frontend-design, feature-dev, code-review, debugging, fullstack-scaffold, commit-workflow)
@@ -441,6 +466,13 @@ CAPABILITIES:
441
  - Use code templates when appropriate to accelerate development
442
  - Provide intelligent code suggestions and best practices
443
 
 
 
 
 
 
 
 
444
  CRITICAL RULES β€” READ CAREFULLY:
445
 
446
  1. NEVER paste code directly in your reply as a markdown code block.
@@ -448,7 +480,7 @@ CRITICAL RULES β€” READ CAREFULLY:
448
  - Do NOT use the @@FILE: format.
449
  - ALL code MUST be saved to files using the `write_file` tool.
450
 
451
- 2. For ANY file you want to create (Python, HTML, JS, config, README, etc.), call the `write_file` tool with the full path and content. The file will be written to the sandboxed workspace automatically.
452
 
453
  3. For multi-step tasks, ALWAYS create a todo list first with `todo_write`.
454
 
@@ -475,7 +507,7 @@ CODE QUALITY STANDARDS:
475
 
476
  WORKFLOW EXAMPLE:
477
  - User asks: "Build a Flask API for a book library"
478
- - You: call todo_write β†’ call write_file (app.py) β†’ call write_file (requirements.txt) β†’ call write_file (README.md) β†’ respond with a short summary listing the files created.
479
 
480
  FULLSTACK PROJECT RULES:
481
  - For React/Next.js/Vue/Express/NestJS: create package.json, vite.config.js or next.config.js as needed, plus all source files via `write_file`.
 
6
  - Session management settings
7
  - Performance tuning parameters
8
  - Security configurations
9
+ - **NEW: Hugging Face Hub Bucket storage support**
10
  """
11
 
12
  from __future__ import annotations
 
16
  # ─── App Identity ────────────────────────────────────────────────────────
17
 
18
  APP_TITLE = "SoniCoder"
19
+ APP_VERSION = "2.2.0" # Updated: Added HF Bucket support
20
  MODEL_URL = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct"
21
 
22
  # ─── Model Configs ───────────────────────────────────────────────────────
 
105
  r"subprocess\.Popen",
106
  ]
107
 
108
+ # ─── Hugging Face Hub Bucket Storage (NEW) ──────────────────────────────
109
+ # HF Bucket provides persistent cloud storage for AI-generated files
110
+
111
+ # Default bucket configuration
112
+ HF_BUCKET_ENABLED = True # Enable bucket storage feature
113
+ HF_BUCKET_REPO_ID = "" # Will be set from env or user config (e.g., "user/workspace-bucket")
114
+ HF_BUCKET_MOUNT_PATH = "/data/bucket" # Where bucket mounts in Spaces
115
+ HF_BUCKET_AUTO_CREATE = True # Auto-create bucket if it doesn't exist
116
+ HF_BUCKET_PUBLIC = False # Private by default for workspace data
117
+
118
+ # Storage mode options: "local", "hub", "hybrid", "bucket", "auto"
119
+ # - local: Files only on disk (ephemeral on Spaces)
120
+ # - hub: Files synced to HF repo
121
+ # - hybrid: Local + Hub sync
122
+ # - bucket: Files in HF Storage Bucket (persistent, recommended for Spaces)
123
+ # - auto: Auto-detect based on environment
124
+ HF_STORAGE_MODE = "auto"
125
+
126
+ # Environment variable names for HF configuration
127
+ HF_TOKEN_ENV_VARS = ["HF_TOKEN", "HF_AUTH_TOKEN", "HUGGING_FACE_TOKEN"]
128
+ HF_REPO_ID_ENV_VAR = "HF_REPO_ID"
129
+ HF_BUCKET_ID_ENV_VAR = "HF_BUCKET_REPO_ID"
130
+ HF_STORAGE_MODE_ENV_VAR = "HF_STORAGE_MODE"
131
+
132
  # ─── Regex Patterns ─────────────────────────────────────────────────────
133
 
134
  THINKING_BLOCK_RE = re.compile(
 
454
 
455
  # ─── System Prompt (Enhanced) ────────────────────────────────────────────
456
 
457
+ SYSTEM_PROMPT = """You are SoniCoder v2.2 β€” an advanced autonomous coding agent that writes files to cloud-enabled workspace using tools.
458
 
459
  CAPABILITIES:
460
  - Generate complete, runnable applications in any language/framework
461
+ - Read, write, and edit files in the workspace via tools (with cloud sync!)
462
  - Run shell commands (git, npm, pip, tests) via the bash tool
463
  - Track multi-step tasks with the todo system
464
  - Apply specialized skills (frontend-design, feature-dev, code-review, debugging, fullstack-scaffold, commit-workflow)
 
466
  - Use code templates when appropriate to accelerate development
467
  - Provide intelligent code suggestions and best practices
468
 
469
+ CLOUD STORAGE (NEW):
470
+ - Your files are automatically synced to Hugging Face Hub cloud storage!
471
+ - When you write files using `write_file`, they are persisted to the cloud
472
+ - Files survive Space restarts and are accessible from anywhere
473
+ - You can check storage status with the hub_status tool
474
+ - Use `sync_to_hub` or `sync_from_hub` for manual sync operations
475
+
476
  CRITICAL RULES β€” READ CAREFULLY:
477
 
478
  1. NEVER paste code directly in your reply as a markdown code block.
 
480
  - Do NOT use the @@FILE: format.
481
  - ALL code MUST be saved to files using the `write_file` tool.
482
 
483
+ 2. For ANY file you want to create (Python, HTML, JS, config, README, etc.), call the `write_file` tool with the full path and content. The file will be written to workspace AND synced to cloud automatically.
484
 
485
  3. For multi-step tasks, ALWAYS create a todo list first with `todo_write`.
486
 
 
507
 
508
  WORKFLOW EXAMPLE:
509
  - User asks: "Build a Flask API for a book library"
510
+ - You: call todo_write β†’ call write_file (app.py) β†’ call write_file (requirements.txt) β†’ call write_file (README.md) β†’ respond with a short summary listing the files created (all auto-synced to cloud!).
511
 
512
  FULLSTACK PROJECT RULES:
513
  - For React/Next.js/Vue/Express/NestJS: create package.json, vite.config.js or next.config.js as needed, plus all source files via `write_file`.
code/hub_storage.py CHANGED
@@ -5,23 +5,42 @@ This module provides seamless integration with Hugging Face Hub for:
5
  - Persistent workspace on HF Spaces
6
  - Direct code writing to repositories
7
  - Dataset-backed persistence
 
8
 
9
  Architecture:
10
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
11
  β”‚ AI Agent │────▢│ hub_storage.py │────▢│ HuggingFace Hub β”‚
12
  β”‚ β”‚ β”‚ (this module) β”‚ β”‚ (Cloud Storage) β”‚
13
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
14
- β”‚
15
- β–Ό
16
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
17
- β”‚ Local Fallback β”‚
18
- β”‚ (when offline) β”‚
19
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
20
 
21
  Usage Modes:
22
  1. LOCAL: Files stored in ./workspace/ (default)
23
  2. HUB: Files synced to HF Hub repository
24
  3. HYBRID: Local + Hub sync (recommended for Spaces)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  """
26
 
27
  from __future__ import annotations
@@ -51,7 +70,7 @@ class HubConfig:
51
  token: str = ""
52
 
53
  # Storage mode
54
- mode: str = "local" # "local", "hub", or "hybrid"
55
 
56
  # Workspace path (for local/hybrid)
57
  local_workspace: str = "./workspace"
@@ -63,13 +82,25 @@ class HubConfig:
63
  # Dataset repo for persistent storage (recommended for Spaces)
64
  dataset_repo_id: str = "" # e.g., "username/sonicoder-workspace"
65
 
 
 
 
 
66
  def is_hub_enabled(self) -> bool:
67
  """Check if Hub storage is enabled."""
68
- return self.mode in ("hub", "hybrid") and bool(self.repo_id) and bool(self.token)
69
 
70
  def is_dataset_storage_enabled(self) -> bool:
71
  """Check if dataset-based storage is enabled."""
72
  return bool(self.dataset_repo_id) and bool(self.token)
 
 
 
 
 
 
 
 
73
 
74
 
75
  @dataclass
@@ -82,6 +113,7 @@ class FileMetadata:
82
  content_hash: str = ""
83
  is_local: bool = True
84
  is_remote: bool = False
 
85
 
86
 
87
  class HubStorageError(Exception):
@@ -99,17 +131,17 @@ class HubStorage:
99
  Main class for Hugging Face Hub cloud storage integration.
100
 
101
  Provides transparent file operations that work with both
102
- local filesystem and HF Hub, with automatic sync.
103
 
104
  Example:
105
  >>> storage = HubStorage(
106
  ... repo_id="sonic-coder/sonicoder",
107
  ... token="hf_...",
108
- ... mode="hybrid",
109
- ... dataset_repo_id="sonic-coder/sonicoder-workspace"
110
  ... )
111
  >>> storage.initialize()
112
- >>> storage.write_file("app.py", "print('Hello!')")
113
  >>> content = storage.read_file("app.py")
114
  >>> files = storage.list_files()
115
  """
@@ -134,6 +166,10 @@ class HubStorage:
134
  self._on_sync: Optional[Callable] = None
135
  self._on_error: Optional[Callable] = None
136
 
 
 
 
 
137
  @property
138
  def api(self):
139
  """Lazy initialization of HfApi."""
@@ -154,7 +190,7 @@ class HubStorage:
154
  Initialize the Hub storage system.
155
 
156
  Creates necessary directories, validates connection,
157
- and sets up the storage backend.
158
 
159
  Returns:
160
  Dict with initialization status and info.
@@ -164,6 +200,7 @@ class HubStorage:
164
  "mode": self.config.mode,
165
  "hub_enabled": False,
166
  "dataset_enabled": False,
 
167
  "workspace_path": str(self._local_workspace),
168
  }
169
 
@@ -199,14 +236,26 @@ class HubStorage:
199
  logger.warning("Dataset setup failed: %s", e)
200
  result["dataset_error"] = str(e)
201
 
 
 
 
 
 
 
 
 
 
 
 
202
  self._initialized = True
203
  result["success"] = True
204
 
205
  logger.info(
206
- "HubStorage initialized: mode=%s, hub=%s, dataset=%s",
207
  self.config.mode,
208
  result["hub_enabled"],
209
- result["dataset_enabled"]
 
210
  )
211
 
212
  except Exception as e:
@@ -248,34 +297,558 @@ class HubStorage:
248
  private=True # Workspaces are usually private
249
  )
250
 
251
- # ─── File Operations ──────────────────────────────────────────────
 
 
252
 
253
- def write_file(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  self,
255
  path: str,
256
  content: str | bytes,
257
- commit_message: str | None = None,
258
- sync_to_hub: bool = True
259
  ) -> FileMetadata:
260
  """
261
- Write a file to storage.
262
 
263
- In hybrid/local mode, writes to local filesystem first.
264
- If sync_to_hub and hub is enabled, also uploads to HF Hub.
 
265
 
266
  Args:
267
- path: Relative file path (e.g., "src/app.py")
268
  content: File content (string or bytes)
269
- commit_message: Optional git commit message
270
- sync_to_hub: Whether to sync to Hub immediately
271
 
272
  Returns:
273
  FileMetadata with file information.
 
 
 
 
 
274
  """
275
  # Normalize path
276
  path = self._normalize_path(path)
277
 
278
- # Generate commit message if not provided
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  if not commit_message:
280
  commit_message = f"Update {path}"
281
 
@@ -311,18 +884,14 @@ class HubStorage:
311
  """
312
  Read a file from storage.
313
 
314
- Tries local first, then falls back to Hub if available.
315
-
316
- Args:
317
- path: Relative file path
318
- use_cache: Whether to use cached remote file list
319
-
320
- Returns:
321
- File content as string.
322
-
323
- Raises:
324
- HubStorageError if file not found anywhere.
325
  """
 
 
 
 
 
326
  path = self._normalize_path(path)
327
 
328
  # Try local first
@@ -351,8 +920,7 @@ class HubStorage:
351
  if local_path.exists():
352
  return local_path.read_bytes()
353
 
354
- if self.config.is_hub_enabled():
355
- # Download to temp and read
356
  with tempfile.NamedTemporaryFile(delete=False) as tmp:
357
  tmp_path = tmp.name
358
 
@@ -364,30 +932,23 @@ class HubStorage:
364
  local_dir=os.path.dirname(tmp_path),
365
  )
366
 
367
- # Find downloaded file
368
  downloaded = Path(tmp_path).parent / Path(path).name
369
  if downloaded.exists():
370
  return downloaded.read_bytes()
371
  finally:
372
- # Cleanup temp
373
  if os.path.exists(tmp_path):
374
  os.unlink(tmp_path)
375
 
376
  raise HubStorageError(f"Binary file not found: {path}", operation="read_binary")
377
 
378
  def delete_file(self, path: str, commit_message: str | None = None) -> bool:
379
- """
380
- Delete a file from storage.
381
-
382
- Args:
383
- path: Relative file path
384
- commit_message: Optional commit message
385
-
386
- Returns:
387
- True if deleted successfully.
388
- """
389
  path = self._normalize_path(path)
390
 
 
 
 
 
391
  if not commit_message:
392
  commit_message = f"Delete {path}"
393
 
@@ -397,7 +958,6 @@ class HubStorage:
397
  local_path = self._local_workspace / path
398
  if local_path.exists():
399
  local_path.unlink()
400
- # Remove empty parent directories
401
  self._cleanup_empty_dirs(local_path.parent)
402
 
403
  # Delete from Hub
@@ -419,12 +979,10 @@ class HubStorage:
419
  """Check if a file exists in storage."""
420
  path = self._normalize_path(path)
421
 
422
- # Check local
423
  if (self._local_workspace / path).exists():
424
  return True
425
 
426
- # Check Hub cache or remote
427
- if self.config.is_hub_enabled():
428
  try:
429
  files = self.list_files(use_cache=True)
430
  return any(f["path"] == path for f in files)
@@ -439,19 +997,11 @@ class HubStorage:
439
  recursive: bool = True,
440
  use_cache: bool = True
441
  ) -> list[dict[str, Any]]:
442
- """
443
- List files in storage.
444
-
445
- Combines local and remote file listings.
446
 
447
- Args:
448
- path: Directory path to list (default: root)
449
- recursive: Whether to list recursively
450
- use_cache: Whether to use cached file listing
451
-
452
- Returns:
453
- List of file info dicts with 'path', 'size', 'modified' keys.
454
- """
455
  files = []
456
  seen_paths = set()
457
 
@@ -483,12 +1033,13 @@ class HubStorage:
483
  except Exception as e:
484
  logger.warning("Failed to list remote files: %e", e)
485
 
486
- # Sort by path
487
  files.sort(key=lambda x: x["path"])
488
 
489
  return files
490
 
491
- # ─── Hub Operations ────────────────────────────────────────────────
 
 
492
 
493
  def _should_sync_to_hub(self) -> bool:
494
  """Check if we should sync to Hub based on config."""
@@ -497,9 +1048,9 @@ class HubStorage:
497
  self.config.is_dataset_storage_enabled()
498
  ) and self.config.auto_sync
499
 
 
500
  def _get_target_repo(self) -> str:
501
  """Get the target repository ID for operations."""
502
- # Prefer dataset repo for workspace files
503
  if self.config.is_dataset_storage_enabled():
504
  return self.config.dataset_repo_id
505
  return self.config.repo_id
@@ -518,8 +1069,7 @@ class HubStorage:
518
  ) -> FileMetadata:
519
  """Upload a file to HF Hub."""
520
 
521
- # For large content, use temp file
522
- if isinstance(content, str) and len(content) > 100000: # > 100KB
523
  with tempfile.NamedTemporaryFile(
524
  mode='w',
525
  suffix=Path(path).suffix,
@@ -540,7 +1090,6 @@ class HubStorage:
540
  finally:
541
  os.unlink(tmp_path)
542
  else:
543
- # Upload directly from content
544
  content_bytes = content.encode('utf-8') if isinstance(content, str) else content
545
 
546
  from io import BytesIO
@@ -563,8 +1112,6 @@ class HubStorage:
563
 
564
  def _download_from_hub(self, path: str) -> str:
565
  """Download a file from HF Hub."""
566
-
567
- # Use hf_hub_download for caching
568
  from huggingface_hub import hf_hub_download
569
 
570
  local_path = hf_hub_download(
@@ -574,7 +1121,6 @@ class HubStorage:
574
  token=self.config.token
575
  )
576
 
577
- # Read and return content
578
  with open(local_path, 'r', encoding='utf-8') as f:
579
  return f.read()
580
 
@@ -584,12 +1130,10 @@ class HubStorage:
584
  ) -> list[dict[str, Any]]:
585
  """List files in the Hub repository."""
586
 
587
- # Check cache
588
  if use_cache and self._remote_cache:
589
  if time.time() - self._cache_timestamp < self._cache_ttl:
590
  return list(self._remote_cache.values())
591
 
592
- # Fetch from API
593
  repo_info = self.api.repo_info(
594
  repo_id=self._get_target_repo(),
595
  repo_type=self._get_target_repo_type()
@@ -608,29 +1152,25 @@ class HubStorage:
608
  "is_remote": True,
609
  })
610
 
611
- # Update cache
612
  self._remote_cache = {f["path"]: f for f in files}
613
  self._cache_timestamp = time.time()
614
 
615
  return files
616
 
617
- # ─── Sync Operations ──────────────────────────────────────────────
 
 
618
 
619
  def sync_to_hub(
620
  self,
621
  path: str | None = None,
622
  commit_message: str = "Sync workspace"
623
  ) -> dict[str, Any]:
624
- """
625
- Sync local files to Hub.
 
 
626
 
627
- Args:
628
- path: Specific file or directory to sync (None = all)
629
- commit_message: Commit message for changes
630
-
631
- Returns:
632
- Dict with sync results.
633
- """
634
  result = {
635
  "success": False,
636
  "uploaded": [],
@@ -646,7 +1186,6 @@ class HubStorage:
646
  files_to_sync = []
647
 
648
  if path:
649
- # Sync specific file/directory
650
  full_path = self._local_workspace / path
651
  if full_path.is_file():
652
  files_to_sync.append(path)
@@ -655,13 +1194,11 @@ class HubStorage:
655
  if fp.is_file():
656
  files_to_sync.append(str(fp.relative_to(self._local_workspace)))
657
  else:
658
- # Sync all local files
659
  for fp in self._local_workspace.rglob("*"):
660
  if fp.is_file():
661
  rel = str(fp.relative_to(self._local_workspace))
662
  files_to_sync.append(rel)
663
 
664
- # Upload each file
665
  for file_path in files_to_sync:
666
  try:
667
  local_path = self._local_workspace / file_path
@@ -694,15 +1231,7 @@ class HubStorage:
694
  self,
695
  path: str | None = None
696
  ) -> dict[str, Any]:
697
- """
698
- Sync files from Hub to local.
699
-
700
- Args:
701
- path: Specific file to download (None = all)
702
-
703
- Returns:
704
- Dict with sync results.
705
- """
706
  result = {
707
  "success": False,
708
  "downloaded": [],
@@ -711,14 +1240,12 @@ class HubStorage:
711
 
712
  try:
713
  if path:
714
- # Download specific file
715
  content = self._download_from_hub(path)
716
  local_path = self._local_workspace / path
717
  local_path.parent.mkdir(parents=True, exist_ok=True)
718
  local_path.write_text(content, encoding='utf-8')
719
  result["downloaded"].append(path)
720
  else:
721
- # Download all files
722
  remote_files = self._list_hub_files(use_cache=False)
723
 
724
  for rf in remote_files:
@@ -743,37 +1270,28 @@ class HubStorage:
743
  return result
744
 
745
  def full_sync(self) -> dict[str, Any]:
746
- """
747
- Perform bidirectional sync between local and Hub.
748
-
749
- Merges changes from both sides.
750
- """
751
  result = {
752
  "to_hub": {},
753
  "from_hub": {}
754
  }
755
 
756
- # Push local to hub
757
  result["to_hub"] = self.sync_to_hub(commit_message="Full sync push")
758
-
759
- # Pull new/updated from hub
760
  result["from_hub"] = self.sync_from_hub()
761
 
762
  return result
763
 
764
- # ─── Utility Methods ──────────────────────────────────────────────
 
 
765
 
766
  def _normalize_path(self, path: str) -> str:
767
  """Normalize a file path."""
768
- # Remove leading ./
769
  if path.startswith("./"):
770
  path = path[2:]
771
- # Convert backslashes
772
  path = path.replace("\\", "/")
773
- # Remove double slashes
774
  while "//" in path:
775
  path = path.replace("//", "/")
776
- # Security: prevent path traversal
777
  if ".." in path:
778
  raise HubStorageError(
779
  "Path traversal detected",
@@ -800,6 +1318,7 @@ class HubStorage:
800
  "mode": self.config.mode,
801
  "hub_enabled": self.config.is_hub_enabled(),
802
  "dataset_enabled": self.config.is_dataset_storage_enabled(),
 
803
  "local_files": 0,
804
  "local_size_mb": 0,
805
  "last_sync": self._cache_timestamp,
@@ -814,6 +1333,14 @@ class HubStorage:
814
 
815
  stats["local_size_mb"] = round(stats["local_size_mb"], 2)
816
 
 
 
 
 
 
 
 
 
817
  return stats
818
 
819
  def clear_cache(self) -> None:
@@ -832,16 +1359,7 @@ class HubStorage:
832
 
833
  @contextmanager
834
  def batch_operation(self, commit_message: str = "Batch update"):
835
- """
836
- Context manager for batch operations.
837
-
838
- All writes within the context are grouped into one commit.
839
-
840
- Usage:
841
- with storage.batch_operation("Create API"):
842
- storage.write_file("app.py", "...")
843
- storage.write_file("config.py", "...")
844
- """
845
  original_auto_sync = self.config.auto_sync
846
  self.config.auto_sync = False
847
 
@@ -850,14 +1368,18 @@ class HubStorage:
850
  try:
851
  yield files_written
852
 
853
- # Commit all at once
854
  if files_written:
855
- self.sync_to_hub(commit_message=commit_message)
 
 
 
856
  finally:
857
  self.config.auto_sync = original_auto_sync
858
 
859
 
860
- # ─── Global Instance Management ────────────────────────────────────────
 
 
861
 
862
  _global_storage: Optional[HubStorage] = None
863
 
@@ -874,6 +1396,7 @@ def init_hub_storage(
874
  token: str = "",
875
  repo_id: str = "",
876
  dataset_repo_id: str = "",
 
877
  mode: str = "auto",
878
  **kwargs
879
  ) -> HubStorage:
@@ -881,23 +1404,37 @@ def init_hub_storage(
881
  Initialize the global HubStorage instance.
882
 
883
  Auto-detects environment when running on HF Spaces.
 
884
 
885
  Args:
886
  token: HF API token
887
  repo_id: Main repository ID
888
  dataset_repo_id: Dataset repository for workspace storage
889
- mode: Storage mode ("local", "hub", "hybrid", "auto")
 
890
 
891
  Returns:
892
  Initialized HubStorage instance.
 
 
 
 
 
 
 
893
  """
894
  global _global_storage
895
 
896
  # Auto-detect mode
897
  if mode == "auto":
898
- # Check if running on HF Spaces
899
  if os.environ.get("SPACE_ID"):
900
- mode = "hybrid" if token else "local"
 
 
 
 
 
 
901
  else:
902
  mode = "local"
903
 
@@ -909,6 +1446,7 @@ def init_hub_storage(
909
  token=token,
910
  repo_id=repo_id,
911
  dataset_repo_id=dataset_repo_id,
 
912
  mode=mode,
913
  **kwargs
914
  )
@@ -926,6 +1464,13 @@ def is_hub_available() -> bool:
926
  return _global_storage.config.is_hub_enabled()
927
 
928
 
 
 
 
 
 
 
 
929
  def get_storage_mode() -> str:
930
  """Get current storage mode."""
931
  if _global_storage is None:
 
5
  - Persistent workspace on HF Spaces
6
  - Direct code writing to repositories
7
  - Dataset-backed persistence
8
+ - **NEW: HF Storage Buckets support** (persistent cloud storage)
9
 
10
  Architecture:
11
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
12
  β”‚ AI Agent │────▢│ hub_storage.py │────▢│ HuggingFace Hub β”‚
13
  β”‚ β”‚ β”‚ (this module) β”‚ β”‚ (Cloud Storage) β”‚
14
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
15
+ β”‚ β”‚
16
+ β–Ό β–Ό
17
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
18
+ β”‚ Local Fallback β”‚ β”‚ HF Bucket β”‚
19
+ β”‚ (when offline) β”‚ β”‚ (Persistent) β”‚
20
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
21
 
22
  Usage Modes:
23
  1. LOCAL: Files stored in ./workspace/ (default)
24
  2. HUB: Files synced to HF Hub repository
25
  3. HYBRID: Local + Hub sync (recommended for Spaces)
26
+ 4. BUCKET: Files stored in HF Storage Bucket (NEW - best for Spaces)
27
+
28
+ HF Bucket API Reference:
29
+ - create_bucket: Create a new storage bucket
30
+ - upload_file: Upload files to bucket
31
+ - list_bucket_tree: List bucket contents
32
+ - Buckets provide S3-like persistent storage on HF Hub
33
+
34
+ Example usage with buckets:
35
+ >>> from code.hub_storage import init_hub_storage
36
+ >>> storage = init_hub_storage(
37
+ ... token="hf_...",
38
+ ... repo_id="sonic-coder/sonicoder",
39
+ ... bucket_repo_id="sonic-coder/sonicoder-workspace-bucket",
40
+ ... mode="bucket"
41
+ ... )
42
+ >>> # Now AI can write directly to cloud storage!
43
+ >>> storage.write_file("project/app.py", "print('Hello Cloud!')")
44
  """
45
 
46
  from __future__ import annotations
 
70
  token: str = ""
71
 
72
  # Storage mode
73
+ mode: str = "local" # "local", "hub", "hybrid", or "bucket"
74
 
75
  # Workspace path (for local/hybrid)
76
  local_workspace: str = "./workspace"
 
82
  # Dataset repo for persistent storage (recommended for Spaces)
83
  dataset_repo_id: str = "" # e.g., "username/sonicoder-workspace"
84
 
85
+ # HF Storage Bucket (NEW - persistent cloud storage)
86
+ bucket_repo_id: str = "" # e.g., "username/sonicoder-bucket"
87
+ bucket_mount_path: str = "/data/bucket" # Where to mount in Spaces
88
+
89
  def is_hub_enabled(self) -> bool:
90
  """Check if Hub storage is enabled."""
91
+ return self.mode in ("hub", "hybrid", "bucket") and bool(self.repo_id) and bool(self.token)
92
 
93
  def is_dataset_storage_enabled(self) -> bool:
94
  """Check if dataset-based storage is enabled."""
95
  return bool(self.dataset_repo_id) and bool(self.token)
96
+
97
+ def is_bucket_enabled(self) -> bool:
98
+ """Check if HF Storage Bucket is enabled (NEW)."""
99
+ return (
100
+ self.mode == "bucket" and
101
+ bool(self.bucket_repo_id) and
102
+ bool(self.token)
103
+ )
104
 
105
 
106
  @dataclass
 
113
  content_hash: str = ""
114
  is_local: bool = True
115
  is_remote: bool = False
116
+ is_in_bucket: bool = False # NEW: Track if file is in bucket
117
 
118
 
119
  class HubStorageError(Exception):
 
131
  Main class for Hugging Face Hub cloud storage integration.
132
 
133
  Provides transparent file operations that work with both
134
+ local filesystem, HF Hub repos, AND HF Storage Buckets.
135
 
136
  Example:
137
  >>> storage = HubStorage(
138
  ... repo_id="sonic-coder/sonicoder",
139
  ... token="hf_...",
140
+ ... mode="bucket",
141
+ ... bucket_repo_id="sonic-coder/sonicoder-workspace"
142
  ... )
143
  >>> storage.initialize()
144
+ >>> storage.write_file("app.py", "print('Hello!')") # Writes to cloud!
145
  >>> content = storage.read_file("app.py")
146
  >>> files = storage.list_files()
147
  """
 
166
  self._on_sync: Optional[Callable] = None
167
  self._on_error: Optional[Callable] = None
168
 
169
+ # Bucket state
170
+ self._bucket_created: bool = False
171
+ self._bucket_mounted: bool = False
172
+
173
  @property
174
  def api(self):
175
  """Lazy initialization of HfApi."""
 
190
  Initialize the Hub storage system.
191
 
192
  Creates necessary directories, validates connection,
193
+ sets up the storage backend, and creates bucket if needed.
194
 
195
  Returns:
196
  Dict with initialization status and info.
 
200
  "mode": self.config.mode,
201
  "hub_enabled": False,
202
  "dataset_enabled": False,
203
+ "bucket_enabled": False,
204
  "workspace_path": str(self._local_workspace),
205
  }
206
 
 
236
  logger.warning("Dataset setup failed: %s", e)
237
  result["dataset_error"] = str(e)
238
 
239
+ # NEW: Initialize Bucket storage if enabled
240
+ if self.config.is_bucket_enabled():
241
+ try:
242
+ bucket_result = self._initialize_bucket()
243
+ result["bucket_enabled"] = bucket_result["success"]
244
+ result.update(bucket_result)
245
+ logger.info("Bucket storage ready: %s", self.config.bucket_repo_id)
246
+ except Exception as e:
247
+ logger.warning("Bucket setup failed: %s", e)
248
+ result["bucket_error"] = str(e)
249
+
250
  self._initialized = True
251
  result["success"] = True
252
 
253
  logger.info(
254
+ "HubStorage initialized: mode=%s, hub=%s, dataset=%s, bucket=%s",
255
  self.config.mode,
256
  result["hub_enabled"],
257
+ result.get("dataset_enabled", False),
258
+ result.get("bucket_enabled", False)
259
  )
260
 
261
  except Exception as e:
 
297
  private=True # Workspaces are usually private
298
  )
299
 
300
+ # ═══════════════════════════════════════════════════════════════════
301
+ # HF STORAGE BUCKET METHODS (NEW)
302
+ # ═══════════════════════════════════════════════════════════════════
303
 
304
+ def _initialize_bucket(self) -> dict[str, Any]:
305
+ """
306
+ Initialize HF Storage Bucket.
307
+
308
+ Creates the bucket if it doesn't exist and prepares it for use.
309
+ Uses the new HuggingFace Hub Storage Bucket API.
310
+
311
+ Returns:
312
+ Dict with bucket initialization status.
313
+ """
314
+ result = {
315
+ "success": False,
316
+ "bucket_repo_id": self.config.bucket_repo_id,
317
+ "created": False,
318
+ "mounted": False,
319
+ }
320
+
321
+ try:
322
+ # Try to get bucket info (check if exists)
323
+ try:
324
+ bucket_info = self.api.get_bucket_info(
325
+ repo_id=self.config.bucket_repo_id
326
+ )
327
+ result["exists"] = True
328
+ logger.info("Bucket exists: %s", self.config.bucket_repo_id)
329
+ except Exception:
330
+ # Bucket doesn't exist, create it
331
+ logger.info("Creating new bucket: %s", self.config.bucket_repo_id)
332
+
333
+ # Use create_bucket API
334
+ try:
335
+ create_result = self.api.create_bucket(
336
+ repo_id=self.config.bucket_repo_id,
337
+ description="SoniCoder AI Workspace - Persistent Cloud Storage",
338
+ public=False, # Private workspace
339
+ )
340
+ result["created"] = True
341
+ self._bucket_created = True
342
+ logger.info("Bucket created successfully")
343
+ except AttributeError:
344
+ # Fallback: create as dataset repo (older API)
345
+ logger.warning("create_bucket not available, using dataset fallback")
346
+ self.api.create_repo(
347
+ repo_id=self.config.bucket_repo_id,
348
+ repo_type="dataset",
349
+ exist_ok=True,
350
+ private=True
351
+ )
352
+ result["created"] = True
353
+ self._bucket_created = True
354
+
355
+ # Check if running in Spaces with mount
356
+ if os.environ.get("SPACE_ID"):
357
+ mount_path = self.config.bucket_mount_path
358
+ if os.path.exists(mount_path):
359
+ result["mounted"] = True
360
+ self._bucket_mounted = True
361
+ logger.info("Bucket mounted at: %s", mount_path)
362
+
363
+ result["success"] = True
364
+
365
+ except Exception as e:
366
+ logger.exception("Failed to initialize bucket")
367
+ result["error"] = str(e)
368
+
369
+ return result
370
+
371
+ def create_bucket(
372
+ self,
373
+ bucket_repo_id: str = "",
374
+ description: str = "SoniCoder Workspace Bucket",
375
+ public: bool = False
376
+ ) -> dict[str, Any]:
377
+ """
378
+ Create a new HF Storage Bucket explicitly.
379
+
380
+ This is the main method to create a bucket for cloud storage.
381
+ The bucket provides persistent S3-like storage on HF Hub.
382
+
383
+ Args:
384
+ bucket_repo_id: Bucket ID (e.g., "username/my-bucket")
385
+ Uses config value if not provided
386
+ description: Bucket description
387
+ public: Whether bucket is publicly readable
388
+
389
+ Returns:
390
+ Dict with creation result.
391
+
392
+ Example:
393
+ >>> result = storage.create_bucket(
394
+ ... bucket_repo_id="sonic-coder/my-workspace",
395
+ ... description="AI Code Workspace"
396
+ ... )
397
+ >>> print(result) # {"success": True, "bucket_url": "..."}
398
+ """
399
+ if not bucket_repo_id:
400
+ bucket_repo_id = self.config.bucket_repo_id
401
+
402
+ if not bucket_repo_id:
403
+ return {
404
+ "success": False,
405
+ "error": "No bucket_repo_id provided"
406
+ }
407
+
408
+ result = {
409
+ "success": False,
410
+ "bucket_repo_id": bucket_repo_id,
411
+ }
412
+
413
+ try:
414
+ # Try new bucket API first
415
+ try:
416
+ create_result = self.api.create_bucket(
417
+ repo_id=bucket_repo_id,
418
+ description=description,
419
+ public=public,
420
+ )
421
+ result["success"] = True
422
+ result["created"] = True
423
+ result["bucket_url"] = f"https://huggingface.co/{bucket_repo_id}"
424
+
425
+ # Update config
426
+ self.config.bucket_repo_id = bucket_repo_id
427
+ self._bucket_created = True
428
+
429
+ logger.info("Bucket created: %s", bucket_repo_id)
430
+
431
+ except AttributeError:
432
+ # Fallback: Use dataset as bucket-like storage
433
+ logger.info("Using dataset as bucket storage")
434
+ self.api.create_repo(
435
+ repo_id=bucket_repo_id,
436
+ repo_type="dataset",
437
+ exist_ok=True,
438
+ private=not public
439
+ )
440
+ result["success"] = True
441
+ result["created"] = True
442
+ result["fallback_mode"] = "dataset"
443
+ result["bucket_url"] = f"https://huggingface.co/{bucket_repo_id}"
444
+
445
+ self.config.bucket_repo_id = bucket_repo_id
446
+ self._bucket_created = True
447
+
448
+ except Exception as e:
449
+ logger.exception("Failed to create bucket")
450
+ result["error"] = str(e)
451
+
452
+ return result
453
+
454
+ def write_to_bucket(
455
  self,
456
  path: str,
457
  content: str | bytes,
458
+ commit_message: str | None = None
 
459
  ) -> FileMetadata:
460
  """
461
+ Write a file directly to HF Storage Bucket.
462
 
463
+ This is the MAIN method for writing files to cloud storage.
464
+ When using bucket mode, this writes directly to persistent
465
+ cloud storage that survives Space restarts.
466
 
467
  Args:
468
+ path: Relative file path (e.g., "project/main.py")
469
  content: File content (string or bytes)
470
+ commit_message: Optional commit message
 
471
 
472
  Returns:
473
  FileMetadata with file information.
474
+
475
+ Example:
476
+ >>> # Write code directly to cloud!
477
+ >>> meta = storage.write_to_bucket("hello.py", "print('Hello Cloud!')")
478
+ >>> print(f"File saved to cloud: {meta.path}")
479
  """
480
  # Normalize path
481
  path = self._normalize_path(path)
482
 
483
+ if not commit_message:
484
+ commit_message = f"AI generated: {path}"
485
+
486
+ # Determine target repo (bucket or fallback)
487
+ target_repo = self.config.bucket_repo_id or self.config.dataset_repo_id or self.config.repo_id
488
+ target_repo_type = "dataset" if self.config.bucket_repo_id or self.config.dataset_repo_id else self.config.repo_type
489
+
490
+ # Write to local cache first
491
+ local_path = self._local_workspace / path
492
+ local_path.parent.mkdir(parents=True, exist_ok=True)
493
+
494
+ if isinstance(content, bytes):
495
+ local_path.write_bytes(content)
496
+ content_size = len(content)
497
+ else:
498
+ local_path.write_text(content, encoding='utf-8')
499
+ content_size = len(content.encode('utf-8'))
500
+
501
+ # Upload to bucket/cloud
502
+ metadata = FileMetadata(
503
+ path=path,
504
+ size=content_size,
505
+ is_local=True,
506
+ is_in_bucket=False
507
+ )
508
+
509
+ try:
510
+ # Prepare content for upload
511
+ if isinstance(content, str):
512
+ content_bytes = content.encode('utf-8')
513
+ else:
514
+ content_bytes = content
515
+
516
+ # For large files, use temp file
517
+ if len(content_bytes) > 100000: # > 100KB
518
+ with tempfile.NamedTemporaryFile(
519
+ mode='wb',
520
+ suffix=Path(path).suffix,
521
+ delete=False
522
+ ) as tmp:
523
+ tmp.write(content_bytes)
524
+ tmp_path = tmp.name
525
+
526
+ try:
527
+ self.api.upload_file(
528
+ path_or_fileobj=tmp_path,
529
+ path_in_repo=path,
530
+ repo_id=target_repo,
531
+ repo_type=target_repo_type,
532
+ commit_message=commit_message
533
+ )
534
+ finally:
535
+ if os.path.exists(tmp_path):
536
+ os.unlink(tmp_path)
537
+ else:
538
+ # Upload from memory
539
+ from io import BytesIO
540
+ self.api.upload_file(
541
+ path_or_fileobj=BytesIO(content_bytes),
542
+ path_in_repo=path,
543
+ repo_id=target_repo,
544
+ repo_type=target_repo_type,
545
+ commit_message=commit_message
546
+ )
547
+
548
+ # Update metadata
549
+ metadata.is_remote = True
550
+ metadata.is_in_bucket = bool(self.config.bucket_repo_id)
551
+
552
+ logger.info("Written to bucket: %s (%d bytes)", path, content_size)
553
+
554
+ except Exception as e:
555
+ logger.error("Failed to write to bucket: %s", e)
556
+ if self._on_error:
557
+ self._on_error("write_to_bucket", path, e)
558
+ raise HubStorageError(
559
+ f"Failed to write to bucket: {e}",
560
+ operation="write_to_bucket"
561
+ )
562
+
563
+ return metadata
564
+
565
+ def read_from_bucket(self, path: str) -> str:
566
+ """
567
+ Read a file from HF Storage Bucket.
568
+
569
+ Args:
570
+ path: Relative file path in bucket
571
+
572
+ Returns:
573
+ File content as string.
574
+
575
+ Raises:
576
+ HubStorageError if file not found.
577
+ """
578
+ path = self._normalize_path(path)
579
+
580
+ # Try local cache first
581
+ local_path = self._local_workspace / path
582
+ if local_path.exists():
583
+ return local_path.read_text(encoding='utf-8')
584
+
585
+ # Download from bucket
586
+ target_repo = self.config.bucket_repo_id or self.config.dataset_repo_id or self.config.repo_id
587
+ target_repo_type = "dataset" if self.config.bucket_repo_id or self.config.dataset_repo_id else self.config.repo_type
588
+
589
+ try:
590
+ from huggingface_hub import hf_hub_download
591
+
592
+ local_path = hf_hub_download(
593
+ repo_id=target_repo,
594
+ filename=path,
595
+ repo_type=target_repo_type,
596
+ token=self.config.token
597
+ )
598
+
599
+ with open(local_path, 'r', encoding='utf-8') as f:
600
+ return f.read()
601
+
602
+ except Exception as e:
603
+ raise HubStorageError(
604
+ f"File not found in bucket: {path} - {e}",
605
+ operation="read_from_bucket",
606
+ recoverable=False
607
+ )
608
+
609
+ def list_bucket_files(
610
+ self,
611
+ path: str = ".",
612
+ recursive: bool = True
613
+ ) -> list[dict[str, Any]]:
614
+ """
615
+ List files in the HF Storage Bucket.
616
+
617
+ Args:
618
+ path: Directory path to list
619
+ recursive: Whether to list recursively
620
+
621
+ Returns:
622
+ List of file info dicts.
623
+ """
624
+ target_repo = self.config.bucket_repo_id or self.config.dataset_repo_id or self.config.repo_id
625
+ target_repo_type = "dataset" if self.config.bucket_repo_id or self.config.dataset_repo_id else self.config.repo_type
626
+
627
+ files = []
628
+
629
+ try:
630
+ # Get repo info with file listing
631
+ repo_info = self.api.repo_info(
632
+ repo_id=target_repo,
633
+ repo_type=target_repo_type
634
+ )
635
+
636
+ siblings = getattr(repo_info, 'siblings', [])
637
+
638
+ for sibling in siblings:
639
+ if hasattr(sibling, 'rfilename'):
640
+ file_path = sibling.rfilename
641
+
642
+ # Filter by path prefix if specified
643
+ if path and path != ".":
644
+ if not file_path.startswith(path):
645
+ continue
646
+ if not recursive and '/' in file_path[len(path):]:
647
+ continue
648
+
649
+ files.append({
650
+ "path": file_path,
651
+ "size": getattr(sibling, 'size', 0),
652
+ "blob_id": getattr(sibling, 'blob_id', ''),
653
+ "is_local": False,
654
+ "is_remote": True,
655
+ "is_in_bucket": True,
656
+ })
657
+
658
+ logger.info("Listed %d files from bucket", len(files))
659
+
660
+ except Exception as e:
661
+ logger.error("Failed to list bucket files: %s", e)
662
+
663
+ return files
664
+
665
+ def delete_from_bucket(
666
+ self,
667
+ path: str,
668
+ commit_message: str | None = None
669
+ ) -> bool:
670
+ """
671
+ Delete a file from HF Storage Bucket.
672
+
673
+ Args:
674
+ path: File path to delete
675
+ commit_message: Optional commit message
676
+
677
+ Returns:
678
+ True if deleted successfully.
679
+ """
680
+ path = self._normalize_path(path)
681
+
682
+ if not commit_message:
683
+ commit_message = f"Delete: {path}"
684
+
685
+ target_repo = self.config.bucket_repo_id or self.config.dataset_repo_id or self.config.repo_id
686
+ target_repo_type = "dataset" if self.config.bucket_repo_id or self.config.dataset_repo_id else self.config.repo_type
687
+
688
+ try:
689
+ self.api.delete_file(
690
+ path_in_repo=path,
691
+ repo_id=target_repo,
692
+ repo_type=target_repo_type,
693
+ commit_message=commit_message
694
+ )
695
+
696
+ # Delete from local cache too
697
+ local_path = self._local_workspace / path
698
+ if local_path.exists():
699
+ local_path.unlink()
700
+
701
+ logger.info("Deleted from bucket: %s", path)
702
+ return True
703
+
704
+ except Exception as e:
705
+ logger.error("Failed to delete from bucket: %s", e)
706
+ return False
707
+
708
+ def sync_to_bucket(
709
+ self,
710
+ path: str | None = None,
711
+ commit_message: str = "Sync to bucket"
712
+ ) -> dict[str, Any]:
713
+ """
714
+ Sync local files to HF Bucket.
715
+
716
+ Args:
717
+ path: Specific file/directory to sync (None = all)
718
+ commit_message: Commit message
719
+
720
+ Returns:
721
+ Sync result dict.
722
+ """
723
+ result = {
724
+ "success": False,
725
+ "uploaded": [],
726
+ "skipped": [],
727
+ "errors": []
728
+ }
729
+
730
+ if not self.config.bucket_repo_id and not self.config.dataset_repo_id:
731
+ result["errors"].append("No bucket configured")
732
+ return result
733
+
734
+ try:
735
+ files_to_sync = []
736
+
737
+ if path:
738
+ full_path = self._local_workspace / path
739
+ if full_path.is_file():
740
+ files_to_sync.append(path)
741
+ elif full_path.is_dir():
742
+ for fp in full_path.rglob("*"):
743
+ if fp.is_file():
744
+ files_to_sync.append(str(fp.relative_to(self._local_workspace)))
745
+ else:
746
+ for fp in self._local_workspace.rglob("*"):
747
+ if fp.is_file():
748
+ rel = str(fp.relative_to(self._local_workspace))
749
+ files_to_sync.append(rel)
750
+
751
+ for file_path in files_to_sync:
752
+ try:
753
+ local_path = self._local_workspace / file_path
754
+ content = local_path.read_text(encoding='utf-8')
755
+
756
+ self.write_to_bucket(
757
+ file_path,
758
+ content,
759
+ f"{commit_message}: {file_path}"
760
+ )
761
+
762
+ result["uploaded"].append(file_path)
763
+
764
+ except Exception as e:
765
+ logger.error("Failed to sync %s: %s", file_path, e)
766
+ result["errors"].append({"file": file_path, "error": str(e)})
767
+
768
+ result["success"] = len(result["errors"]) == 0
769
+
770
+ except Exception as e:
771
+ result["errors"].append({"file": "*", "error": str(e)})
772
+
773
+ return result
774
+
775
+ def get_bucket_status(self) -> dict[str, Any]:
776
+ """
777
+ Get current bucket status and info.
778
+
779
+ Returns:
780
+ Dict with bucket status information.
781
+ """
782
+ status = {
783
+ "available": False,
784
+ "configured": False,
785
+ "repo_id": self.config.bucket_repo_id,
786
+ "mode": self.config.mode,
787
+ }
788
+
789
+ if not self.config.bucket_repo_id:
790
+ return status
791
+
792
+ status["configured"] = True
793
+
794
+ try:
795
+ # Try to get bucket info
796
+ try:
797
+ bucket_info = self.api.get_bucket_info(
798
+ repo_id=self.config.bucket_repo_id
799
+ )
800
+ status["available"] = True
801
+ status["exists"] = True
802
+ status.update({
803
+ k: v for k, v in bucket_info.items()
804
+ if isinstance(v, (str, int, float, bool))
805
+ })
806
+ except AttributeError:
807
+ # Fallback: check as dataset
808
+ repo_info = self.api.repo_info(
809
+ repo_id=self.config.bucket_repo_id,
810
+ repo_type="dataset"
811
+ )
812
+ status["available"] = True
813
+ status["exists"] = True
814
+ status["fallback_mode"] = "dataset"
815
+
816
+ # Count files
817
+ files = self.list_bucket_files()
818
+ status["file_count"] = len(files)
819
+ status["total_size_mb"] = round(
820
+ sum(f.get("size", 0) for f in files) / (1024 * 1024), 2
821
+ )
822
+
823
+ except Exception as e:
824
+ status["error"] = str(e)
825
+
826
+ return status
827
+
828
+ # ═══════════════════════════════════════════════════════════════════
829
+ # LEGACY FILE OPERATIONS (with bucket support)
830
+ # ═══════════════════════════════════════════════════════════════════
831
+
832
+ def write_file(
833
+ self,
834
+ path: str,
835
+ content: str | bytes,
836
+ commit_message: str | None = None,
837
+ sync_to_hub: bool = True
838
+ ) -> FileMetadata:
839
+ """
840
+ Write a file to storage.
841
+
842
+ If bucket mode is enabled, writes directly to cloud bucket.
843
+ Otherwise uses legacy behavior (local + optional hub sync).
844
+ """
845
+ # If bucket mode, use bucket method
846
+ if self.config.is_bucket_enabled():
847
+ return self.write_to_bucket(path, content, commit_message)
848
+
849
+ # Legacy behavior
850
+ path = self._normalize_path(path)
851
+
852
  if not commit_message:
853
  commit_message = f"Update {path}"
854
 
 
884
  """
885
  Read a file from storage.
886
 
887
+ If bucket mode is enabled, reads from bucket.
888
+ Otherwise tries local then Hub.
 
 
 
 
 
 
 
 
 
889
  """
890
+ # If bucket mode, use bucket method
891
+ if self.config.is_bucket_enabled():
892
+ return self.read_from_bucket(path)
893
+
894
+ # Legacy behavior
895
  path = self._normalize_path(path)
896
 
897
  # Try local first
 
920
  if local_path.exists():
921
  return local_path.read_bytes()
922
 
923
+ if self.config.is_hub_enabled() or self.config.is_bucket_enabled():
 
924
  with tempfile.NamedTemporaryFile(delete=False) as tmp:
925
  tmp_path = tmp.name
926
 
 
932
  local_dir=os.path.dirname(tmp_path),
933
  )
934
 
 
935
  downloaded = Path(tmp_path).parent / Path(path).name
936
  if downloaded.exists():
937
  return downloaded.read_bytes()
938
  finally:
 
939
  if os.path.exists(tmp_path):
940
  os.unlink(tmp_path)
941
 
942
  raise HubStorageError(f"Binary file not found: {path}", operation="read_binary")
943
 
944
  def delete_file(self, path: str, commit_message: str | None = None) -> bool:
945
+ """Delete a file from storage."""
 
 
 
 
 
 
 
 
 
946
  path = self._normalize_path(path)
947
 
948
+ # If bucket mode, use bucket delete
949
+ if self.config.is_bucket_enabled():
950
+ return self.delete_from_bucket(path, commit_message)
951
+
952
  if not commit_message:
953
  commit_message = f"Delete {path}"
954
 
 
958
  local_path = self._local_workspace / path
959
  if local_path.exists():
960
  local_path.unlink()
 
961
  self._cleanup_empty_dirs(local_path.parent)
962
 
963
  # Delete from Hub
 
979
  """Check if a file exists in storage."""
980
  path = self._normalize_path(path)
981
 
 
982
  if (self._local_workspace / path).exists():
983
  return True
984
 
985
+ if self.config.is_hub_enabled() or self.config.is_bucket_enabled():
 
986
  try:
987
  files = self.list_files(use_cache=True)
988
  return any(f["path"] == path for f in files)
 
997
  recursive: bool = True,
998
  use_cache: bool = True
999
  ) -> list[dict[str, Any]]:
1000
+ """List files in storage."""
1001
+ # If bucket mode, use bucket listing
1002
+ if self.config.is_bucket_enabled():
1003
+ return self.list_bucket_files(path, recursive)
1004
 
 
 
 
 
 
 
 
 
1005
  files = []
1006
  seen_paths = set()
1007
 
 
1033
  except Exception as e:
1034
  logger.warning("Failed to list remote files: %e", e)
1035
 
 
1036
  files.sort(key=lambda x: x["path"])
1037
 
1038
  return files
1039
 
1040
+ # ═══════════════════════════════════════════════════════════════════
1041
+ # LEGACY HUB OPERATIONS
1042
+ # ═══════════════════════════════════════════════════════════════════
1043
 
1044
  def _should_sync_to_hub(self) -> bool:
1045
  """Check if we should sync to Hub based on config."""
 
1048
  self.config.is_dataset_storage_enabled()
1049
  ) and self.config.auto_sync
1050
 
1051
+
1052
  def _get_target_repo(self) -> str:
1053
  """Get the target repository ID for operations."""
 
1054
  if self.config.is_dataset_storage_enabled():
1055
  return self.config.dataset_repo_id
1056
  return self.config.repo_id
 
1069
  ) -> FileMetadata:
1070
  """Upload a file to HF Hub."""
1071
 
1072
+ if isinstance(content, str) and len(content) > 100000:
 
1073
  with tempfile.NamedTemporaryFile(
1074
  mode='w',
1075
  suffix=Path(path).suffix,
 
1090
  finally:
1091
  os.unlink(tmp_path)
1092
  else:
 
1093
  content_bytes = content.encode('utf-8') if isinstance(content, str) else content
1094
 
1095
  from io import BytesIO
 
1112
 
1113
  def _download_from_hub(self, path: str) -> str:
1114
  """Download a file from HF Hub."""
 
 
1115
  from huggingface_hub import hf_hub_download
1116
 
1117
  local_path = hf_hub_download(
 
1121
  token=self.config.token
1122
  )
1123
 
 
1124
  with open(local_path, 'r', encoding='utf-8') as f:
1125
  return f.read()
1126
 
 
1130
  ) -> list[dict[str, Any]]:
1131
  """List files in the Hub repository."""
1132
 
 
1133
  if use_cache and self._remote_cache:
1134
  if time.time() - self._cache_timestamp < self._cache_ttl:
1135
  return list(self._remote_cache.values())
1136
 
 
1137
  repo_info = self.api.repo_info(
1138
  repo_id=self._get_target_repo(),
1139
  repo_type=self._get_target_repo_type()
 
1152
  "is_remote": True,
1153
  })
1154
 
 
1155
  self._remote_cache = {f["path"]: f for f in files}
1156
  self._cache_timestamp = time.time()
1157
 
1158
  return files
1159
 
1160
+ # ═══════════════════════════════════════════════════════════════════
1161
+ # SYNC OPERATIONS
1162
+ # ═══════════════════════════════════════════════════════════════════
1163
 
1164
  def sync_to_hub(
1165
  self,
1166
  path: str | None = None,
1167
  commit_message: str = "Sync workspace"
1168
  ) -> dict[str, Any]:
1169
+ """Sync local files to Hub/Bucket."""
1170
+ # If bucket mode, use bucket sync
1171
+ if self.config.is_bucket_enabled():
1172
+ return self.sync_to_bucket(path, commit_message)
1173
 
 
 
 
 
 
 
 
1174
  result = {
1175
  "success": False,
1176
  "uploaded": [],
 
1186
  files_to_sync = []
1187
 
1188
  if path:
 
1189
  full_path = self._local_workspace / path
1190
  if full_path.is_file():
1191
  files_to_sync.append(path)
 
1194
  if fp.is_file():
1195
  files_to_sync.append(str(fp.relative_to(self._local_workspace)))
1196
  else:
 
1197
  for fp in self._local_workspace.rglob("*"):
1198
  if fp.is_file():
1199
  rel = str(fp.relative_to(self._local_workspace))
1200
  files_to_sync.append(rel)
1201
 
 
1202
  for file_path in files_to_sync:
1203
  try:
1204
  local_path = self._local_workspace / file_path
 
1231
  self,
1232
  path: str | None = None
1233
  ) -> dict[str, Any]:
1234
+ """Sync files from Hub to local."""
 
 
 
 
 
 
 
 
1235
  result = {
1236
  "success": False,
1237
  "downloaded": [],
 
1240
 
1241
  try:
1242
  if path:
 
1243
  content = self._download_from_hub(path)
1244
  local_path = self._local_workspace / path
1245
  local_path.parent.mkdir(parents=True, exist_ok=True)
1246
  local_path.write_text(content, encoding='utf-8')
1247
  result["downloaded"].append(path)
1248
  else:
 
1249
  remote_files = self._list_hub_files(use_cache=False)
1250
 
1251
  for rf in remote_files:
 
1270
  return result
1271
 
1272
  def full_sync(self) -> dict[str, Any]:
1273
+ """Perform bidirectional sync between local and Hub."""
 
 
 
 
1274
  result = {
1275
  "to_hub": {},
1276
  "from_hub": {}
1277
  }
1278
 
 
1279
  result["to_hub"] = self.sync_to_hub(commit_message="Full sync push")
 
 
1280
  result["from_hub"] = self.sync_from_hub()
1281
 
1282
  return result
1283
 
1284
+ # ═══════════════════════════════════════════════════════════════════
1285
+ # UTILITY METHODS
1286
+ # ═══════════════════════════════════════════════════════════════════
1287
 
1288
  def _normalize_path(self, path: str) -> str:
1289
  """Normalize a file path."""
 
1290
  if path.startswith("./"):
1291
  path = path[2:]
 
1292
  path = path.replace("\\", "/")
 
1293
  while "//" in path:
1294
  path = path.replace("//", "/")
 
1295
  if ".." in path:
1296
  raise HubStorageError(
1297
  "Path traversal detected",
 
1318
  "mode": self.config.mode,
1319
  "hub_enabled": self.config.is_hub_enabled(),
1320
  "dataset_enabled": self.config.is_dataset_storage_enabled(),
1321
+ "bucket_enabled": self.config.is_bucket_enabled(),
1322
  "local_files": 0,
1323
  "local_size_mb": 0,
1324
  "last_sync": self._cache_timestamp,
 
1333
 
1334
  stats["local_size_mb"] = round(stats["local_size_mb"], 2)
1335
 
1336
+ # Add bucket stats if available
1337
+ if self.config.is_bucket_enabled():
1338
+ try:
1339
+ bucket_status = self.get_bucket_status()
1340
+ stats["bucket_stats"] = bucket_status
1341
+ except Exception:
1342
+ pass
1343
+
1344
  return stats
1345
 
1346
  def clear_cache(self) -> None:
 
1359
 
1360
  @contextmanager
1361
  def batch_operation(self, commit_message: str = "Batch update"):
1362
+ """Context manager for batch operations."""
 
 
 
 
 
 
 
 
 
1363
  original_auto_sync = self.config.auto_sync
1364
  self.config.auto_sync = False
1365
 
 
1368
  try:
1369
  yield files_written
1370
 
 
1371
  if files_written:
1372
+ if self.config.is_bucket_enabled():
1373
+ self.sync_to_bucket(commit_message=commit_message)
1374
+ else:
1375
+ self.sync_to_hub(commit_message=commit_message)
1376
  finally:
1377
  self.config.auto_sync = original_auto_sync
1378
 
1379
 
1380
+ # ════════════════════════════════════════════════════════════════════════
1381
+ # GLOBAL INSTANCE MANAGEMENT
1382
+ # ════════════════════════════════════════════════════════════════════════
1383
 
1384
  _global_storage: Optional[HubStorage] = None
1385
 
 
1396
  token: str = "",
1397
  repo_id: str = "",
1398
  dataset_repo_id: str = "",
1399
+ bucket_repo_id: str = "", # NEW: Bucket support
1400
  mode: str = "auto",
1401
  **kwargs
1402
  ) -> HubStorage:
 
1404
  Initialize the global HubStorage instance.
1405
 
1406
  Auto-detects environment when running on HF Spaces.
1407
+ Now supports HF Storage Buckets for persistent cloud storage!
1408
 
1409
  Args:
1410
  token: HF API token
1411
  repo_id: Main repository ID
1412
  dataset_repo_id: Dataset repository for workspace storage
1413
+ bucket_repo_id: HF Storage Bucket ID (NEW - recommended for Spaces)
1414
+ mode: Storage mode ("local", "hub", "hybrid", "bucket", "auto")
1415
 
1416
  Returns:
1417
  Initialized HubStorage instance.
1418
+
1419
+ Example with bucket:
1420
+ >>> storage = init_hub_storage(
1421
+ ... token="hf_...",
1422
+ ... bucket_repo_id="sonic-coder/my-workspace-bucket",
1423
+ ... mode="bucket"
1424
+ ... )
1425
  """
1426
  global _global_storage
1427
 
1428
  # Auto-detect mode
1429
  if mode == "auto":
 
1430
  if os.environ.get("SPACE_ID"):
1431
+ # On Spaces, prefer bucket if available
1432
+ if bucket_repo_id:
1433
+ mode = "bucket"
1434
+ elif token:
1435
+ mode = "hybrid"
1436
+ else:
1437
+ mode = "local"
1438
  else:
1439
  mode = "local"
1440
 
 
1446
  token=token,
1447
  repo_id=repo_id,
1448
  dataset_repo_id=dataset_repo_id,
1449
+ bucket_repo_id=bucket_repo_id,
1450
  mode=mode,
1451
  **kwargs
1452
  )
 
1464
  return _global_storage.config.is_hub_enabled()
1465
 
1466
 
1467
+ def is_bucket_available() -> bool:
1468
+ """Check if HF Bucket storage is available (NEW)."""
1469
+ if _global_storage is None:
1470
+ return False
1471
+ return _global_storage.config.is_bucket_enabled()
1472
+
1473
+
1474
  def get_storage_mode() -> str:
1475
  """Get current storage mode."""
1476
  if _global_storage is None:
index.html CHANGED
@@ -7,496 +7,2285 @@
7
  <meta name="description" content="AI-powered code agent with MCP support, multiple 1B models, and Gemini CLI patterns">
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
  <style>
12
  /* ═══════════════════════════════════════════════════════
13
- RESET & BASE
 
14
  ═══════════════════════════════════════════════════════ */
15
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
16
- :root{
17
- --bg-deep:#0a0e14;--bg-panel:#0d1117;--bg-code:#161b22;
18
- --border:#1e2a3a;--border-focus:#2d4a6a;
19
- --green:#39ff14;--green-dim:#1a7a0a;--cyan:#00d4ff;--amber:#ffb300;
20
- --purple:#a855f7;--gray-light:#e0e0e0;--gray-mid:#8b949e;--gray-dim:#484f58;
21
- --red:#ff5555;--success:#50fa7b;--code-text:#f8f8f2;
22
- --glow-green:0 0 8px rgba(57,255,20,0.3);--glow-cyan:0 0 8px rgba(0,212,255,0.3);
23
- --glow-amber:0 0 8px rgba(255,179,0,0.2);--glow-purple:0 0 8px rgba(168,85,247,0.3);
24
- --font-mono:'JetBrains Mono','Fira Code','Cascadia Code',monospace;
25
- --radius:4px;--transition:0.2s ease;
26
- }
27
- html,body{height:100%;background:var(--bg-deep);color:var(--gray-light);font-family:var(--font-mono);font-size:13px;line-height:1.6;overflow:hidden}
28
- body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.03) 2px,rgba(0,0,0,0.03) 4px)}
29
- ::selection{background:rgba(57,255,20,0.25);color:#fff}
30
- ::-webkit-scrollbar{width:6px;height:6px}
31
- ::-webkit-scrollbar-track{background:transparent}
32
- ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
33
- ::-webkit-scrollbar-thumb:hover{background:var(--gray-dim)}
34
- a{color:var(--cyan);text-decoration:none}
35
- a:hover{text-decoration:underline;text-shadow:var(--glow-cyan)}
36
-
37
- /* ═══ APP SHELL ═══ */
38
- #app{display:flex;flex-direction:column;height:100vh;max-height:100vh}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  /* ═══ HEADER ═══ */
41
- #header{display:flex;align-items:center;justify-content:space-between;padding:10px 20px;background:var(--bg-panel);border-bottom:1px solid var(--border);flex-shrink:0;gap:16px}
42
- .header-title{display:flex;flex-direction:column;gap:2px}
43
- .header-ascii{color:var(--green);font-size:11px;line-height:1.3;text-shadow:var(--glow-green);white-space:pre;letter-spacing:.5px}
44
- .header-subtitle{color:var(--gray-mid);font-size:11px;padding-left:3px}
45
- .header-actions{display:flex;align-items:center;gap:10px;flex-shrink:0}
46
- .pill{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border:1px solid var(--border);border-radius:12px;font-size:11px;color:var(--gray-mid);text-decoration:none;transition:all var(--transition);cursor:default}
47
- .pill .dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
48
- .pill .dot.ready{background:var(--success);box-shadow:0 0 6px var(--success)}
49
- .pill .dot.loading{background:var(--amber);box-shadow:0 0 6px var(--amber);animation:pulse 1.5s ease infinite}
50
- .pill .dot.error{background:var(--red);box-shadow:0 0 6px var(--red)}
51
- .pill .dot.unknown{background:var(--gray-dim)}
52
- @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
53
-
54
- #model-select{background:var(--bg-deep);border:1px solid var(--border);color:var(--cyan);font-family:var(--font-mono);font-size:11px;padding:5px 8px;border-radius:var(--radius);outline:none;cursor:pointer;transition:border-color var(--transition)}
55
- #model-select:focus{border-color:var(--border-focus)}
56
- #model-select option{background:var(--bg-deep);color:var(--gray-light)}
57
-
58
- .mcp-pill .dot.connected{background:var(--success);box-shadow:0 0 6px var(--success)}
59
- .mcp-pill .dot.disconnected{background:var(--red);box-shadow:0 0 6px var(--red)}
60
-
61
- .header-btn{background:transparent;border:1px solid var(--border);color:var(--amber);font-family:var(--font-mono);font-size:11px;padding:5px 12px;border-radius:var(--radius);cursor:pointer;transition:all var(--transition);letter-spacing:1px;white-space:nowrap}
62
- .header-btn:hover{border-color:var(--amber);background:rgba(255,179,0,.08);text-shadow:var(--glow-amber)}
63
-
64
- /* ═══ BANNER ═══ */
65
- #banner{background:linear-gradient(90deg,rgba(168,85,247,.08),rgba(57,255,20,.05));border-bottom:1px solid var(--border);color:var(--gray-mid);font-size:12px;padding:7px 18px;text-align:center;flex-shrink:0}
66
- #banner strong{color:var(--gray-light);font-weight:600}
67
- #banner a{color:var(--purple);font-weight:600}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- /* ═══ MAIN LAYOUT ═══ */
70
- #main{display:flex;flex:1;min-height:0;overflow:hidden}
71
-
72
- /* ═══ TERMINAL PANEL ═══ */
73
- #terminal-panel{display:flex;flex-direction:column;flex:1;min-width:0;border-right:1px solid var(--border)}
74
- .panel-label{padding:6px 16px;font-size:10px;letter-spacing:2px;color:var(--gray-dim);border-bottom:1px solid var(--border);background:rgba(13,17,23,.6);text-transform:uppercase;flex-shrink:0}
75
- #chat-messages{flex:1;overflow-y:auto;padding:16px;scroll-behavior:smooth}
76
-
77
- /* Messages */
78
- .msg{margin-bottom:14px;line-height:1.65;animation:msgIn .25s ease}
79
- @keyframes msgIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
80
- .msg-prefix{font-weight:600;margin-right:4px}
81
- .msg-user .msg-prefix{color:var(--green);text-shadow:var(--glow-green)}
82
- .msg-user .msg-content{color:var(--green);text-shadow:var(--glow-green)}
83
- .msg-assistant .msg-prefix{color:var(--cyan);text-shadow:var(--glow-cyan);float:left}
84
- .msg-assistant .msg-body{overflow:hidden}
85
- .msg-assistant .msg-content{color:var(--gray-light)}
86
- .msg-system .msg-prefix{color:var(--amber);text-shadow:var(--glow-amber)}
87
- .msg-system .msg-content{color:var(--amber);opacity:.85}
88
-
89
- .msg-content strong{color:#fff;font-weight:600}
90
- .msg-content em{font-style:italic;color:var(--gray-mid)}
91
- .msg-content code:not(pre code){background:var(--bg-code);color:var(--code-text);padding:1px 5px;border-radius:3px;font-size:12px;border:1px solid var(--border)}
92
- .msg-content a{color:var(--cyan)}
93
- .msg-content ul,.msg-content ol{margin:6px 0 6px 20px}
94
- .msg-content li{margin-bottom:3px}
95
- .msg-content h1,.msg-content h2,.msg-content h3{color:var(--cyan);margin:10px 0 6px;font-size:14px;text-shadow:var(--glow-cyan)}
96
- .msg-content p{margin:4px 0}
97
-
98
- /* Code blocks */
99
- .code-block-wrap{margin:8px 0;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;background:var(--bg-code)}
100
- .code-block-header{display:flex;align-items:center;justify-content:space-between;padding:4px 10px;background:rgba(30,42,58,.5);border-bottom:1px solid var(--border);font-size:11px}
101
- .code-lang{color:var(--amber);text-transform:uppercase;letter-spacing:1px}
102
- .btn-copy{background:transparent;border:1px solid var(--border);color:var(--gray-mid);font-family:var(--font-mono);font-size:10px;padding:2px 8px;border-radius:3px;cursor:pointer;transition:all var(--transition)}
103
- .btn-copy:hover{border-color:var(--green);color:var(--green)}
104
- .btn-copy.copied{border-color:var(--success);color:var(--success)}
105
- .code-block-wrap pre{margin:0;padding:10px 12px;overflow-x:auto;font-size:12px;line-height:1.5;color:var(--code-text)}
106
- .code-block-wrap pre code{font-family:var(--font-mono);background:none;border:none;padding:0}
107
-
108
- /* Tool calls */
109
- .tool-block{margin:8px 0;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-code);overflow:hidden}
110
- .tool-header{display:flex;align-items:center;gap:8px;padding:6px 10px;background:rgba(30,42,58,.5);border-bottom:1px solid var(--border);font-size:11px;cursor:pointer}
111
- .tool-icon{font-size:13px}
112
- .tool-name{color:var(--purple);font-weight:600;flex:1}
113
- .tool-badge{padding:1px 8px;border-radius:10px;font-size:10px;font-weight:600;letter-spacing:.5px}
114
- .tool-badge.running{background:rgba(255,179,0,.15);color:var(--amber);border:1px solid rgba(255,179,0,.3)}
115
- .tool-badge.success{background:rgba(80,250,123,.1);color:var(--success);border:1px solid rgba(80,250,123,.3)}
116
- .tool-badge.error{background:rgba(255,85,85,.1);color:var(--red);border:1px solid rgba(255,85,85,.3)}
117
- .tool-args{padding:6px 10px;font-size:11px;color:var(--gray-dim);border-bottom:1px solid rgba(30,42,58,.3);display:none}
118
- .tool-block.open .tool-args{display:block}
119
- .tool-result{padding:8px 10px;font-size:11px;color:var(--gray-mid);max-height:200px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;display:none}
120
- .tool-block.open .tool-result{display:block}
121
- .tool-result.error-output{color:var(--red)}
122
-
123
- /* Streaming cursor */
124
- .streaming-cursor::after{content:'\2588';animation:blink .8s step-end infinite;color:var(--green);margin-left:2px}
125
- @keyframes blink{50%{opacity:0}}
126
-
127
- /* Iteration badge */
128
- .iteration-badge{display:inline-flex;align-items:center;gap:4px;margin:4px 0 2px;padding:2px 8px;border-radius:10px;background:rgba(168,85,247,.1);border:1px solid rgba(168,85,247,.2);font-size:10px;color:var(--purple);letter-spacing:.5px}
129
 
130
  /* ═══ INPUT AREA ═══ */
131
- #input-area{flex-shrink:0;border-top:1px solid var(--border);background:var(--bg-panel);padding:10px 16px 8px}
132
- #input-row{display:flex;gap:8px;align-items:flex-end}
133
- .input-prompt{color:var(--green);font-weight:700;font-size:14px;line-height:36px;text-shadow:var(--glow-green);flex-shrink:0}
134
- #chat-input{flex:1;background:var(--bg-deep);border:1px solid var(--border);border-radius:var(--radius);color:var(--green);font-family:var(--font-mono);font-size:13px;padding:8px 12px;resize:none;outline:none;min-height:36px;max-height:120px;line-height:1.5;transition:border-color var(--transition);caret-color:var(--green);text-shadow:var(--glow-green)}
135
- #chat-input::placeholder{color:var(--gray-dim);text-shadow:none}
136
- #chat-input:focus{border-color:var(--border-focus)}
137
- #btn-send,#btn-stop{font-family:var(--font-mono);font-size:12px;padding:8px 14px;border-radius:var(--radius);cursor:pointer;transition:all var(--transition);letter-spacing:1px;flex-shrink:0;height:36px;display:flex;align-items:center;gap:4px}
138
- #btn-send{background:transparent;border:1px solid var(--green);color:var(--green)}
139
- #btn-send:hover:not(:disabled){background:var(--green);color:var(--bg-deep);box-shadow:0 0 12px rgba(57,255,20,.3)}
140
- #btn-send:disabled{opacity:.3;cursor:not-allowed}
141
- #btn-stop{background:transparent;border:1px solid var(--red);color:var(--red);display:none}
142
- #btn-stop:hover{background:var(--red);color:var(--bg-deep);box-shadow:0 0 12px rgba(255,85,85,.3)}
143
- #examples-row{display:flex;align-items:center;gap:8px;margin-top:8px;flex-wrap:wrap}
144
- .examples-label{font-size:10px;color:var(--gray-dim);letter-spacing:1px;text-transform:uppercase;flex-shrink:0}
145
- .example-chip{background:rgba(30,42,58,.4);border:1px solid var(--border);border-radius:12px;padding:3px 10px;font-family:var(--font-mono);font-size:11px;color:var(--gray-mid);cursor:pointer;transition:all var(--transition);white-space:nowrap}
146
- .example-chip:hover{border-color:var(--purple);color:var(--purple);background:rgba(168,85,247,.05);text-shadow:var(--glow-purple)}
147
-
148
- /* ═══ OPTIONS ROW ═══ */
149
- #options-row{display:flex;align-items:center;gap:6px;margin-top:6px;flex-wrap:wrap}
150
- .opt-group{display:inline-flex;align-items:center;gap:4px;background:rgba(30,42,58,.3);border:1px solid var(--border);border-radius:var(--radius);padding:2px 4px 2px 6px;transition:border-color var(--transition)}
151
- .opt-group:hover{border-color:var(--border-focus)}
152
- .opt-group label{font-size:9px;color:var(--gray-dim);letter-spacing:1px;text-transform:uppercase;flex-shrink:0}
153
- .opt-group select{background:transparent;border:none;color:var(--cyan);font-family:var(--font-mono);font-size:11px;padding:3px 4px;outline:none;cursor:pointer;max-width:120px}
154
- .opt-group select option{background:var(--bg-deep);color:var(--gray-light)}
155
- .opt-toggle{display:inline-flex;align-items:center;gap:5px;background:rgba(30,42,58,.3);border:1px solid var(--border);border-radius:var(--radius);padding:3px 8px;font-size:10px;color:var(--gray-mid);cursor:pointer;transition:all var(--transition);user-select:none;letter-spacing:.5px}
156
- .opt-toggle:hover{border-color:var(--border-focus);color:var(--gray-light)}
157
- .opt-toggle input{display:none}
158
- .opt-toggle .toggle-dot{width:8px;height:8px;border-radius:50%;background:var(--gray-dim);transition:all var(--transition)}
159
- .opt-toggle.active{color:var(--amber);border-color:rgba(255,179,0,.4);background:rgba(255,179,0,.08);text-shadow:var(--glow-amber)}
160
- .opt-toggle.active .toggle-dot{background:var(--amber);box-shadow:0 0 6px var(--amber)}
161
- .opt-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;background:rgba(30,42,58,.3);border:1px solid var(--border);border-radius:var(--radius);color:var(--gray-mid);cursor:pointer;transition:all var(--transition);font-size:13px;flex-shrink:0}
162
- .opt-icon-btn:hover{border-color:var(--cyan);color:var(--cyan);text-shadow:var(--glow-cyan)}
163
- .opt-icon-btn input{display:none}
164
- .img-preview{display:inline-flex;align-items:center;gap:4px;background:rgba(0,212,255,.08);border:1px solid rgba(0,212,255,.3);border-radius:var(--radius);padding:2px 6px;font-size:10px;color:var(--cyan);cursor:pointer}
165
- .img-preview img{width:18px;height:18px;border-radius:2px;object-fit:cover}
166
- .img-preview .remove{cursor:pointer;color:var(--red);margin-left:2px}
167
-
168
- /* ═══ OUTPUT PANEL ═══ */
169
- #output-panel{display:flex;flex-direction:column;width:45%;min-width:340px;max-width:55%;min-height:0;background:var(--bg-panel)}
170
- #output-tabs{display:flex;border-bottom:1px solid var(--border);background:rgba(13,17,23,.6);flex-shrink:0}
171
- .output-tab{flex:1;background:transparent;border:none;border-bottom:2px solid transparent;color:var(--gray-dim);font-family:var(--font-mono);font-size:11px;padding:8px 6px;cursor:pointer;transition:all var(--transition);letter-spacing:.5px;text-transform:uppercase}
172
- .output-tab:hover{color:var(--gray-mid)}
173
- .output-tab.active{color:var(--cyan);border-bottom-color:var(--cyan);text-shadow:var(--glow-cyan)}
174
- #output-content{flex:1;min-height:0;overflow:hidden;position:relative}
175
- .tab-pane{display:none;height:100%;min-height:0}
176
- .tab-pane.active{display:flex;flex-direction:column}
177
-
178
- /* Preview */
179
- #pane-preview{align-items:stretch;justify-content:stretch;position:relative;min-height:0;overflow:hidden}
180
- .preview-placeholder{align-self:center;margin:auto;text-align:center;color:var(--gray-dim);padding:40px 20px}
181
- .preview-placeholder .ascii-art{font-size:11px;line-height:1.3;margin-bottom:16px;color:var(--border-focus)}
182
- .preview-placeholder .placeholder-text{font-size:12px;letter-spacing:.5px}
183
- #preview-iframe{display:none;position:absolute;inset:0;width:100%;height:100%;min-height:0;border:none;background:#fff}
184
- #btn-fullscreen{display:none;position:absolute;top:8px;right:8px;background:rgba(13,17,23,.8);border:1px solid var(--border);color:var(--gray-mid);font-family:var(--font-mono);font-size:11px;padding:4px 10px;border-radius:var(--radius);cursor:pointer;z-index:5;transition:all var(--transition)}
185
- #btn-fullscreen:hover{border-color:var(--cyan);color:var(--cyan)}
186
-
187
- /* Console */
188
- #pane-console{padding:12px 16px;gap:12px;overflow-y:auto}
189
- .console-section{margin-bottom:8px}
190
- .console-label{font-size:10px;letter-spacing:2px;color:var(--gray-dim);margin-bottom:4px;text-transform:uppercase}
191
- .console-output{background:var(--bg-deep);border:1px solid var(--border);border-radius:var(--radius);padding:10px 12px;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;min-height:40px;max-height:280px;overflow-y:auto}
192
- #console-stdout{color:var(--success)}
193
- #console-stderr{color:var(--red)}
194
-
195
- /* Code */
196
- #pane-code{padding:0}
197
- .code-tab-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--border);background:rgba(30,42,58,.3);flex-shrink:0}
198
- .code-tab-lang{font-size:11px;color:var(--amber);letter-spacing:1px;text-transform:uppercase}
199
- .code-tab-actions{display:flex;gap:8px}
200
- .code-tab-btn{background:transparent;border:1px solid var(--border);color:var(--gray-mid);font-family:var(--font-mono);font-size:10px;padding:3px 8px;border-radius:3px;cursor:pointer;transition:all var(--transition)}
201
- .code-tab-btn:hover{border-color:var(--cyan);color:var(--cyan)}
202
- #code-display{flex:1;overflow:auto;padding:12px;background:var(--bg-code)}
203
- #code-display pre{margin:0;font-size:12px;line-height:1.5;color:var(--code-text)}
204
- .code-placeholder{display:flex;align-items:center;justify-content:center;height:100%;color:var(--gray-dim);font-size:12px}
205
-
206
- /* Files */
207
- #pane-files{padding:0}
208
- .files-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--border);background:rgba(30,42,58,.3);flex-shrink:0}
209
- .files-header span{font-size:11px;color:var(--amber);letter-spacing:1px;text-transform:uppercase}
210
- .files-breadcrumb{padding:6px 12px;font-size:11px;color:var(--gray-dim);border-bottom:1px solid var(--border);background:rgba(30,42,58,.2)}
211
- .files-breadcrumb span{color:var(--cyan);cursor:pointer}
212
- .files-breadcrumb span:hover{text-shadow:var(--glow-cyan)}
213
- #files-tree{flex:1;overflow-y:auto;padding:8px 12px;font-size:12px;line-height:1.8;color:var(--gray-mid)}
214
- #files-tree .ws-empty{color:var(--gray-dim);font-style:italic;padding:8px 0}
215
- #files-tree .ws-dir{margin:2px 0}
216
- #files-tree .ws-dir-name{color:var(--cyan);font-weight:500;cursor:default}
217
- #files-tree .ws-children{margin-left:14px;border-left:1px dashed var(--border);padding-left:6px}
218
- #files-tree .ws-file{cursor:pointer;padding:1px 4px;border-radius:3px;transition:background .15s}
219
- #files-tree .ws-file:hover{background:var(--bg-code);color:var(--green)}
220
- #files-tree .ws-file span{color:var(--gray-light)}
221
- #file-viewer{flex:1;overflow:auto;padding:12px;background:var(--bg-code);display:none}
222
- #file-viewer pre{margin:0;font-size:11px;line-height:1.5;color:var(--code-text);white-space:pre-wrap;word-break:break-word}
223
- .file-viewer-header{display:flex;align-items:center;gap:10px;padding-bottom:8px;margin-bottom:8px;border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--bg-code)}
224
- .file-viewer-header button{background:var(--border);color:var(--gray-light);border:1px solid var(--border-focus);padding:4px 10px;border-radius:3px;cursor:pointer;font-family:var(--font-mono);font-size:11px}
225
- .file-viewer-header button:hover{background:var(--border-focus)}
226
- .file-viewer-header span{color:var(--cyan);font-size:12px}
227
- .file-line-num{color:var(--gray-dim);display:inline-block;width:50px;text-align:right;margin-right:12px;user-select:none;border-right:1px solid var(--border);padding-right:8px}
228
-
229
- /* MCP */
230
- #pane-mcp{padding:16px;gap:12px;overflow-y:auto}
231
- .mcp-server{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin-bottom:8px}
232
- .mcp-server-header{display:flex;align-items:center;gap:8px;padding:8px 12px;background:rgba(30,42,58,.5);border-bottom:1px solid var(--border);font-size:12px}
233
- .mcp-server-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
234
- .mcp-server-dot.connected{background:var(--success);box-shadow:0 0 6px var(--success)}
235
- .mcp-server-dot.disconnected{background:var(--red);box-shadow:0 0 6px var(--red)}
236
- .mcp-server-name{font-weight:600;color:var(--gray-light);flex:1}
237
- .mcp-tools{padding:8px 12px;display:flex;flex-wrap:wrap;gap:4px}
238
- .mcp-tool-tag{padding:2px 8px;background:rgba(0,212,255,.08);border:1px solid rgba(0,212,255,.2);border-radius:10px;font-size:10px;color:var(--cyan)}
239
- .mcp-empty{color:var(--gray-dim);font-size:12px;text-align:center;padding:40px 20px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
  /* ═══ STATUS BAR ═══ */
242
- #status-bar{display:flex;align-items:center;gap:8px;padding:5px 16px;border-top:1px solid var(--border);background:var(--bg-panel);font-size:11px;flex-shrink:0}
243
- .status-indicator{display:inline-flex;align-items:center;gap:6px}
244
- .status-dot{font-size:10px;line-height:1}
245
- #status-text{letter-spacing:1px;text-transform:uppercase}
246
- .status-idle{color:var(--gray-dim)}
247
- .status-working{color:var(--amber);text-shadow:var(--glow-amber)}
248
- .status-success{color:var(--success);text-shadow:0 0 8px rgba(80,250,123,.3)}
249
- .status-error{color:var(--red);text-shadow:0 0 8px rgba(255,85,85,.3)}
250
- .status-info{color:var(--cyan);text-shadow:var(--glow-cyan)}
251
- @keyframes spin{to{transform:rotate(360deg)}}
252
- .status-working .status-dot{display:inline-block;animation:spin 1s linear infinite}
253
-
254
- /* ═══ FULLSCREEN ═══ */
255
- #fullscreen-overlay{display:none;position:fixed;inset:0;z-index:1000;background:var(--bg-deep);flex-direction:column}
256
- #fullscreen-overlay.active{display:flex}
257
- #fullscreen-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;border-bottom:1px solid var(--border);background:var(--bg-panel)}
258
- #fullscreen-bar span{color:var(--cyan);font-size:12px;letter-spacing:1px}
259
- #btn-exit-fullscreen{background:transparent;border:1px solid var(--border);color:var(--gray-mid);font-family:var(--font-mono);font-size:11px;padding:4px 12px;border-radius:var(--radius);cursor:pointer;transition:all var(--transition)}
260
- #btn-exit-fullscreen:hover{border-color:var(--red);color:var(--red)}
261
- #fullscreen-iframe{flex:1;border:none;background:#fff}
262
-
263
- /* ═══ RESPONSIVE ═══ */
264
- @media(max-width:900px){
265
- #main{flex-direction:column}
266
- #terminal-panel{border-right:none;border-bottom:1px solid var(--border);max-height:55vh}
267
- #output-panel{width:100%;max-width:100%;min-width:0;flex:1}
268
- .header-ascii{font-size:10px}
269
- #chat-input{font-size:12px}
270
- }
271
- @media(max-width:600px){
272
- #header{padding:8px 12px;gap:8px}
273
- .header-ascii{display:none}
274
- .header-subtitle{display:none}
275
- .pill{font-size:10px;padding:3px 8px}
276
- #chat-messages{padding:10px}
277
- #input-area{padding:8px 10px 6px}
278
- #examples-row{display:none}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  }
280
  </style>
281
  </head>
282
  <body>
283
- <div id="app">
284
- <header id="header">
285
- <div class="header-title">
286
- <div class="header-ascii">&#9556;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552; SONICODER v2 &#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9562;</div>
287
- <div class="header-subtitle">AI Code Agent | MCP Support | <span id="header-model-name">Loading...</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  </div>
289
- <div class="header-actions">
290
- <div class="pill" id="model-pill">
291
- <span class="dot loading" id="model-dot"></span>
292
- <span id="model-pill-text">Loading...</span>
 
 
293
  </div>
294
- <select id="model-select" title="Switch AI model"></select>
295
- <div class="pill mcp-pill" id="mcp-pill">
296
- <span class="dot disconnected" id="mcp-dot"></span>
297
  <span id="mcp-pill-text">MCP</span>
298
  </div>
299
- <button class="header-btn" onclick="newChat()" title="Start a new chat session">[NEW]</button>
 
 
 
 
300
  </div>
301
  </header>
302
 
303
- <div id="banner">
304
- Powered by <a id="banner-model-link" href="https://huggingface.co/openbmb/MiniCPM5-1B" target="_blank" rel="noopener"><strong id="banner-model-name">MiniCPM5-1B</strong></a> running locally &mdash; no external APIs. Supports MCP tools, multiple 1B models, and agentic code generation with Gemini CLI patterns.
305
- </div>
306
-
307
- <div id="main">
308
- <div id="terminal-panel">
309
- <div class="panel-label">Terminal</div>
310
- <div id="chat-messages"></div>
311
- <div id="input-area">
312
- <div id="input-row">
313
- <span class="input-prompt">&#10095;</span>
314
- <textarea id="chat-input" rows="1" placeholder="Describe what you want to build..." spellcheck="false"></textarea>
315
- <button id="btn-send" onclick="handleSend()" title="Send message">&#10148;</button>
316
- <button id="btn-stop" onclick="stopGeneration()" title="Stop generation">&#9632; STOP</button>
 
 
 
 
 
 
 
317
  </div>
318
- <div id="options-row">
319
- <div class="opt-group">
 
 
 
 
 
 
 
 
 
 
 
320
  <label>Lang</label>
321
- <select id="opt-language" onchange="updateFrameworkOptions()" title="Target language">
322
  <option value="">Auto</option>
323
  </select>
324
  </div>
325
- <div class="opt-group">
 
326
  <label>FW</label>
327
- <select id="opt-framework" title="Target framework">
328
  <option value="">Auto</option>
329
  </select>
330
  </div>
331
- <div class="opt-group">
 
332
  <label>Skill</label>
333
- <select id="opt-skill" title="Apply a skill (use /skill command for multiple)">
334
  <option value="">None</option>
335
  </select>
336
  </div>
337
- <div class="opt-group">
 
338
  <label>Agent</label>
339
- <select id="opt-agent" title="Activate a custom agent persona" onchange="setActiveAgent(this.value)">
340
  <option value="">Default</option>
341
  </select>
342
  </div>
343
- <label class="opt-toggle" id="opt-search-toggle" title="Search the web before generating">
 
344
  <input type="checkbox" id="opt-search">
345
- <span class="toggle-dot"></span>
346
- <span>Web</span>
347
  </label>
348
- <label class="opt-icon-btn" title="Attach image (for VLM models)">
 
 
349
  <input type="file" id="opt-image-input" accept="image/*" onchange="handleImageSelect(event)">
350
- <span>&#128247;</span>
351
  </label>
352
- <span id="image-preview-container"></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  </div>
354
- <div id="examples-row"></div>
355
  </div>
356
- </div>
357
 
358
- <div id="output-panel">
359
- <div id="output-tabs">
 
360
  <button class="output-tab active" data-tab="preview" onclick="switchTab('preview')">Preview</button>
361
- <button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button>
362
  <button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button>
363
- <button class="output-tab" data-tab="files" onclick="switchTab('files')">Files</button>
364
- <button class="output-tab" data-tab="mcp" onclick="switchTab('mcp')">MCP</button>
365
  </div>
366
- <div id="output-content">
367
- <div class="tab-pane active" id="pane-preview">
 
368
  <div class="preview-placeholder" id="preview-placeholder">
369
- <div class="ascii-art">
370
- &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
371
- &#9474; &#9585;&#9473;&#9473;&#9473;&#9586; &#9474;
372
- &#9474; &#9474; &#9654; &#9474; OUTPUT &#9474;
373
- &#9474; &#9589;&#9473;&#9473;&#9473;&#9588; &#9474;
374
- &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;</div>
375
- <div class="placeholder-text">Generated code preview will appear here</div>
 
 
 
 
 
 
 
 
376
  </div>
377
- <iframe id="preview-iframe" sandbox="allow-scripts"></iframe>
378
- <button id="btn-fullscreen" onclick="openFullscreen()">&#10570; FULLSCREEN</button>
379
  </div>
380
- <div class="tab-pane" id="pane-console">
 
 
 
381
  <div class="console-section">
382
- <div class="console-label">stdout:</div>
383
  <div class="console-output" id="console-stdout">No output yet.</div>
384
  </div>
385
  <div class="console-section">
386
- <div class="console-label">stderr:</div>
387
  <div class="console-output" id="console-stderr">No errors.</div>
388
  </div>
389
  </div>
390
- <div class="tab-pane" id="pane-code">
391
- <div class="code-tab-header">
392
- <span class="code-tab-lang" id="code-tab-lang">&mdash;</span>
393
- <div class="code-tab-actions">
394
- <button class="code-tab-btn" onclick="copyCode()">&#128203; Copy</button>
395
- </div>
396
- </div>
397
- <div id="code-display"><div class="code-placeholder">No code generated yet.</div></div>
398
- </div>
399
- <div class="tab-pane" id="pane-files">
400
- <div class="files-header">
401
- <span>Workspace</span>
402
- <button class="code-tab-btn" onclick="refreshWorkspace()">&#8635; Refresh</button>
403
- </div>
404
- <div id="files-tree"></div>
405
- <div id="file-viewer"><pre id="file-content"></pre></div>
406
- </div>
407
- <div class="tab-pane" id="pane-mcp">
408
- <div id="mcp-content"><div class="mcp-empty">Loading MCP status...</div></div>
409
- </div>
410
  </div>
411
- </div>
412
- </div>
413
 
414
- <div id="status-bar">
415
- <div class="status-indicator status-idle" id="status-indicator">
416
- <span class="status-dot">&#9679;</span>
417
- <span id="status-text">LOADING MODEL...</span>
 
 
 
 
 
 
 
 
 
 
 
 
418
  </div>
419
- </div>
 
420
  </div>
421
 
422
- <div id="fullscreen-overlay">
423
- <div id="fullscreen-bar">
424
- <span>PREVIEW</span>
425
- <button id="btn-exit-fullscreen" onclick="closeFullscreen()">[&#10005; CLOSE]</button>
426
- </div>
427
- <iframe id="fullscreen-iframe" sandbox="allow-scripts"></iframe>
428
  </div>
429
 
430
  <script>
431
- // ═══ STATE ═══
 
 
 
 
432
  var state = {
433
  sessionId: '',
434
  isGenerating: false,
435
  modelReady: false,
436
- activeTab: 'preview',
437
  toolCalls: {},
438
  currentStreamDiv: null,
439
  lastCode: '',
440
- lastCodeLang: '',
441
  consoleStdout: '',
442
  consoleStderr: '',
443
- generatedFiles: [],
444
- attachedImage: null, // {dataUrl, file_url} or null
445
- config: null, // last loaded runtime config
446
  };
447
 
448
- // ═══ INIT ═══
 
449
  document.addEventListener('DOMContentLoaded', function() {
 
450
  loadConfig();
451
  pollModelStatus();
452
- refreshWorkspace();
453
  loadMcpStatus();
454
-
455
- var input = document.getElementById('chat-input');
456
- input.addEventListener('input', autoResize);
457
- input.addEventListener('keydown', function(e) {
458
- if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); handleSend(); }
459
- });
460
-
461
- // Wire up the Web search toggle
462
- var searchToggle = document.getElementById('opt-search-toggle');
463
- searchToggle.addEventListener('click', function(e) {
464
- e.preventDefault();
465
- var cb = document.getElementById('opt-search');
466
- cb.checked = !cb.checked;
467
- searchToggle.classList.toggle('active', cb.checked);
468
- });
469
  });
470
 
471
- function autoResize() {
472
- var el = document.getElementById('chat-input');
473
- el.style.height = 'auto';
474
- el.style.height = Math.min(el.scrollHeight, 120) + 'px';
475
- }
476
-
477
- // ═══ API HELPERS ═══
478
-
479
- function setStatus(cls, text) {
480
- var ind = document.getElementById('status-indicator');
481
- ind.className = 'status-indicator ' + cls;
482
- document.getElementById('status-text').textContent = text;
 
 
483
  }
484
 
485
  async function loadConfig() {
486
  try {
487
- var resp = await fetch('/api/get_config');
488
- var config = await resp.json();
489
- state.config = config;
490
- var sel = document.getElementById('model-select');
491
- sel.innerHTML = '';
492
- (config.models || []).forEach(function(m) {
493
- var opt = document.createElement('option');
494
- opt.value = m.key;
495
- opt.textContent = m.name + ' (' + m.size_gb + 'GB)';
496
- if (m.key === config.default_model) opt.selected = true;
497
- sel.appendChild(opt);
498
- });
499
- sel.addEventListener('change', function() { switchModel(sel.value); });
500
 
501
  // Populate language selector
502
  var langSel = document.getElementById('opt-language');
@@ -614,21 +2403,20 @@ async function pollModelStatus() {
614
 
615
  if (data.status === 'ready') {
616
  state.modelReady = true;
617
- dot.className = 'dot ready';
618
  pillText.textContent = data.model_name || 'Ready';
619
  document.getElementById('header-model-name').textContent = data.model_name || '';
620
- document.getElementById('banner-model-name').textContent = data.model_name || '';
621
  setStatus('status-success', 'MODEL READY');
622
  setTimeout(function() {
623
  if (!state.isGenerating) setStatus('status-idle', 'IDLE');
624
  }, 3000);
625
  return;
626
  } else if (data.status === 'loading') {
627
- dot.className = 'dot loading';
628
  pillText.textContent = 'Loading...';
629
  setStatus('status-working', 'LOADING MODEL...');
630
  } else {
631
- dot.className = 'dot error';
632
  pillText.textContent = 'Error';
633
  setStatus('status-error', 'MODEL ERROR');
634
  }
@@ -638,7 +2426,7 @@ async function pollModelStatus() {
638
 
639
  async function switchModel(key) {
640
  try {
641
- document.getElementById('model-dot').className = 'dot loading';
642
  document.getElementById('model-pill-text').textContent = 'Switching...';
643
  setStatus('status-working', 'SWITCHING MODEL...');
644
  await fetch('/api/switch_model', {
@@ -657,7 +2445,6 @@ async function refreshWorkspace() {
657
  tree.style.display = 'block';
658
  document.getElementById('file-viewer').style.display = 'none';
659
 
660
- // The backend returns {success: bool, tree: {name, type, children: [...]}}
661
  var rootNode = data.tree;
662
  if (!rootNode || typeof rootNode === 'string') {
663
  tree.innerHTML = '<div class="ws-empty">' + escHtml(rootNode || '(empty workspace)') + '</div>';
@@ -668,7 +2455,7 @@ async function refreshWorkspace() {
668
  if (!node) return '';
669
  var name = escHtml(node.name || '');
670
  if (node.type === 'dir') {
671
- var html = '<div class="ws-dir"><span class="ws-dir-name">&#128193; ' + name + '</span>';
672
  if (node.children && node.children.length > 0) {
673
  html += '<div class="ws-children">';
674
  for (var i = 0; i < node.children.length; i++) {
@@ -680,7 +2467,7 @@ async function refreshWorkspace() {
680
  return html;
681
  } else {
682
  var size = node.size ? ' (' + formatSize(node.size) + ')' : '';
683
- return '<div class="ws-file" data-path="' + name + '" onclick="openWorkspaceFile(\'' + name.replace(/'/g, "\\'") + '\")"><span>&#128196; ' + name + size + '</span></div>';
684
  }
685
  }
686
 
@@ -716,7 +2503,7 @@ async function openWorkspaceFile(path) {
716
  tree.style.display = 'none';
717
  var viewer = document.getElementById('file-viewer');
718
  viewer.style.display = 'block';
719
- viewer.innerHTML = '<div class="file-viewer-header"><button onclick="refreshWorkspace()">&#8634; Back</button><span>' + escHtml(path) + '</span></div><pre class="file-viewer-content">' + escHtml(data.content || '') + '</pre>';
720
  } catch(e) {
721
  alert('Error reading file: ' + e.message);
722
  }
@@ -765,7 +2552,6 @@ async function loadMcpStatus() {
765
  }
766
 
767
  function renderExamples(config) {
768
- // Use the examples from config; fall back to a curated list if missing.
769
  var examples = [];
770
  if (config && Array.isArray(config.examples) && config.examples.length) {
771
  examples = config.examples.map(function(e) {
@@ -782,9 +2568,9 @@ function renderExamples(config) {
782
  'Make a React calculator component',
783
  ];
784
  }
785
- // Show at most 6 examples so the row doesn't overflow on narrow screens.
786
  examples = examples.slice(0, 6);
787
  var row = document.getElementById('examples-row');
 
788
  row.innerHTML = '<span class="examples-label">Try:</span>';
789
  examples.forEach(function(ex) {
790
  var chip = document.createElement('span');
@@ -827,6 +2613,20 @@ document.addEventListener('keydown', function(e) {
827
  if (e.key === 'Escape') closeFullscreen();
828
  });
829
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
830
  // ═══ MESSAGES ═══
831
 
832
  function escHtml(t) { var d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
@@ -837,14 +2637,19 @@ function addMessage(role, content, extra) {
837
  var div = document.createElement('div');
838
  div.className = 'msg msg-' + role;
839
  div.id = extra.msgId || '';
 
 
 
 
840
  var prefix = '';
841
- if (role === 'user') prefix = '<span class="msg-prefix">&gt; </span>';
842
- else if (role === 'assistant') prefix = '<span class="msg-prefix">&#9670; </span>';
843
- else if (role === 'system') prefix = '<span class="msg-prefix">&#9888; </span>';
844
- var bodyClass = role === 'assistant' ? '<div class="msg-body">' : '';
845
- var bodyClose = role === 'assistant' ? '</div>' : '';
846
  var iterationHtml = extra.iteration ? '<div class="iteration-badge">Iteration ' + extra.iteration + '/' + extra.max_iterations + '</div>' : '';
847
- div.innerHTML = iterationHtml + prefix + '<span class="msg-content">' + renderMd(content) + '</span>' + bodyClose + bodyClose;
 
 
848
  container.appendChild(div);
849
  container.scrollTop = container.scrollHeight;
850
  return div;
@@ -856,7 +2661,7 @@ function renderMd(text) {
856
  // Code blocks
857
  text = text.replace(/```(\w*)\n([\s\S]*?)```/g, function(_, lang, code) {
858
  var id = 'cb_' + Date.now() + Math.random().toString(36).substr(2,4);
859
- return '<div class="code-block-wrap"><div class="code-block-header"><span class="code-lang">' + (lang||'code') + '</span><button class="btn-copy" onclick="copyBlock(\'' + id + '\')">&#128203; Copy</button></div><pre><code id="' + id + '">' + code.trim() + '</code></pre></div>';
860
  });
861
  text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
862
  text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
@@ -874,9 +2679,9 @@ function copyBlock(id) {
874
  if (!el) return;
875
  navigator.clipboard.writeText(el.textContent).then(function() {
876
  var btn = el.closest('.code-block-wrap').querySelector('.btn-copy');
877
- btn.textContent = 'Copied!';
878
  btn.classList.add('copied');
879
- setTimeout(function() { btn.innerHTML = '&#128203; Copy'; btn.classList.remove('copied'); }, 2000);
880
  });
881
  }
882
 
@@ -884,8 +2689,8 @@ function copyCode() {
884
  if (!state.lastCode) return;
885
  navigator.clipboard.writeText(state.lastCode).then(function() {
886
  var btn = document.querySelector('#pane-code .code-tab-btn');
887
- btn.textContent = 'Copied!';
888
- setTimeout(function() { btn.innerHTML = '&#128203; Copy'; }, 2000);
889
  });
890
  }
891
 
@@ -906,7 +2711,7 @@ function addToolCall(toolName, args) {
906
  }
907
  div.innerHTML =
908
  '<div class="tool-header">' +
909
- '<span class="tool-icon">&#9889;</span>' +
910
  '<span class="tool-name">' + escHtml(toolName) + '</span>' +
911
  '<span class="tool-badge running">RUNNING</span>' +
912
  '</div>' +
@@ -1056,8 +2861,6 @@ async function handleSend() {
1056
  break;
1057
  case 'complete':
1058
  finalizeStream(ev.content || 'Done.');
1059
- // Always refresh workspace on completion β€” files may have been
1060
- // written via write_file tool calls OR via the fallback extractor.
1061
  setTimeout(refreshWorkspace, 500);
1062
  setStatus('status-success', 'COMPLETE');
1063
  setTimeout(function() { if (!state.isGenerating) setStatus('status-idle', 'IDLE'); }, 3000);
@@ -1120,7 +2923,7 @@ function showPreview(html) {
1120
  var btn = document.getElementById('btn-fullscreen');
1121
  placeholder.style.display = 'none';
1122
  iframe.style.display = 'block';
1123
- btn.style.display = 'block';
1124
  iframe.srcdoc = html;
1125
  switchTab('preview');
1126
  }
@@ -1129,6 +2932,21 @@ function updateConsole() {
1129
  document.getElementById('console-stdout').textContent = state.consoleStdout || 'No output yet.';
1130
  document.getElementById('console-stderr').textContent = state.consoleStderr || 'No errors.';
1131
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1132
  </script>
1133
  </body>
1134
- </html>
 
7
  <meta name="description" content="AI-powered code agent with MCP support, multiple 1B models, and Gemini CLI patterns">
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
  <style>
12
  /* ═══════════════════════════════════════════════════════
13
+ SONICODER V2 - FUTURISTIC UI DESIGN SYSTEM
14
+ Premium AI Code Agent Interface
15
  ═══════════════════════════════════════════════════════ */
16
+
17
+ /* ═══ CSS VARIABLES & DESIGN TOKENS ═══ */
18
+ :root {
19
+ /* Core Colors */
20
+ --bg-deep: #050810;
21
+ --bg-primary: #0a0e17;
22
+ --bg-secondary: #0d1321;
23
+ --bg-tertiary: #111827;
24
+ --bg-elevated: #151d2e;
25
+
26
+ /* Glass Effect Colors */
27
+ --glass-bg: rgba(15, 23, 42, 0.65);
28
+ --glass-bg-light: rgba(20, 30, 50, 0.5);
29
+ --glass-border: rgba(100, 180, 255, 0.12);
30
+ --glass-border-hover: rgba(0, 212, 255, 0.25);
31
+ --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
32
+
33
+ /* Accent Colors - Neon */
34
+ --cyan: #00d4ff;
35
+ --cyan-bright: #5eeaff;
36
+ --cyan-dim: #007a94;
37
+ --cyan-glow: rgba(0, 212, 255, 0.4);
38
+ --cyan-glow-subtle: rgba(0, 212, 255, 0.15);
39
+
40
+ --purple: #a855f7;
41
+ --purple-bright: #c084fc;
42
+ --purple-dim: #7c3aed;
43
+ --purple-glow: rgba(168, 85, 247, 0.4);
44
+ --purple-glow-subtle: rgba(168, 85, 247, 0.15);
45
+
46
+ --green: #39ff14;
47
+ --green-dim: #22c55e;
48
+ --green-glow: rgba(57, 255, 20, 0.35);
49
+
50
+ --amber: #ffb300;
51
+ --amber-glow: rgba(255, 179, 0, 0.3);
52
+
53
+ --red: #ff5555;
54
+ --red-glow: rgba(255, 85, 85, 0.3);
55
+
56
+ /* Text Colors */
57
+ --text-primary: #f1f5f9;
58
+ --text-secondary: #94a3b8;
59
+ --text-muted: #64748b;
60
+ --text-accent: var(--cyan);
61
+
62
+ /* Border Colors */
63
+ --border-default: rgba(71, 85, 105, 0.3);
64
+ --border-focus: var(--cyan);
65
+ --border-glow: 0 0 0 1px var(--cyan), 0 0 20px var(--cyan-glow);
66
+
67
+ /* Typography */
68
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
69
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
70
+
71
+ /* Spacing & Sizing */
72
+ --radius-sm: 6px;
73
+ --radius-md: 10px;
74
+ --radius-lg: 16px;
75
+ --radius-xl: 24px;
76
+ --header-height: 56px;
77
+ --sidebar-width: 260px;
78
+
79
+ /* Transitions */
80
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
81
+ --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
82
+ --transition-slow: 400ms cubic-bezier(0.4, 0, 0.2, 1);
83
+ --transition-spring: 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
84
+
85
+ /* Z-Index Scale */
86
+ --z-base: 1;
87
+ --z-panel: 10;
88
+ --z-overlay: 100;
89
+ --z-modal: 1000;
90
+ --z-toast: 2000;
91
+ }
92
+
93
+ /* ═══ RESET & BASE ═══ */
94
+ *, *::before, *::after {
95
+ box-sizing: border-box;
96
+ margin: 0;
97
+ padding: 0;
98
+ }
99
+
100
+ html {
101
+ height: 100%;
102
+ font-size: 14px;
103
+ }
104
+
105
+ body {
106
+ height: 100%;
107
+ background: var(--bg-deep);
108
+ color: var(--text-primary);
109
+ font-family: var(--font-sans);
110
+ line-height: 1.6;
111
+ overflow: hidden;
112
+ position: relative;
113
+ }
114
+
115
+ ::selection {
116
+ background: var(--cyan-glow);
117
+ color: white;
118
+ }
119
+
120
+ ::-webkit-scrollbar {
121
+ width: 6px;
122
+ height: 6px;
123
+ }
124
+
125
+ ::-webkit-scrollbar-track {
126
+ background: transparent;
127
+ }
128
+
129
+ ::-webkit-scrollbar-thumb {
130
+ background: var(--border-default);
131
+ border-radius: 3px;
132
+ transition: background var(--transition-fast);
133
+ }
134
+
135
+ ::-webkit-scrollbar-thumb:hover {
136
+ background: var(--cyan-dim);
137
+ }
138
+
139
+ /* ═══ ANIMATED BACKGROUND SYSTEM ═══ */
140
+ .bg-canvas {
141
+ position: fixed;
142
+ inset: 0;
143
+ z-index: 0;
144
+ overflow: hidden;
145
+ pointer-events: none;
146
+ }
147
+
148
+ .bg-gradient-mesh {
149
+ position: absolute;
150
+ inset: 0;
151
+ background:
152
+ radial-gradient(ellipse 80% 60% at 20% 10%, rgba(0, 212, 255, 0.08) 0%, transparent 50%),
153
+ radial-gradient(ellipse 60% 80% at 80% 90%, rgba(168, 85, 247, 0.06) 0%, transparent 50%),
154
+ radial-gradient(ellipse 50% 50% at 50% 50%, rgba(57, 255, 20, 0.03) 0%, transparent 60%);
155
+ animation: meshFloat 20s ease-in-out infinite alternate;
156
+ }
157
+
158
+ @keyframes meshFloat {
159
+ 0% { transform: scale(1) rotate(0deg); opacity: 1; }
160
+ 50% { transform: scale(1.1) rotate(1deg); opacity: 0.9; }
161
+ 100% { transform: scale(1) rotate(-1deg); opacity: 1; }
162
+ }
163
+
164
+ .particles-container {
165
+ position: absolute;
166
+ inset: 0;
167
+ }
168
+
169
+ .particle {
170
+ position: absolute;
171
+ width: 2px;
172
+ height: 2px;
173
+ background: var(--cyan);
174
+ border-radius: 50%;
175
+ box-shadow: 0 0 6px var(--cyan-glow), 0 0 12px var(--cyan-glow-subtle);
176
+ opacity: 0;
177
+ animation: particleFloat linear infinite;
178
+ }
179
+
180
+ .particle:nth-child(odd) {
181
+ background: var(--purple);
182
+ box-shadow: 0 0 6px var(--purple-glow), 0 0 12px var(--purple-glow-subtle);
183
+ }
184
+
185
+ @keyframes particleFloat {
186
+ 0% {
187
+ opacity: 0;
188
+ transform: translateY(100vh) scale(0);
189
+ }
190
+ 10% {
191
+ opacity: 0.6;
192
+ transform: translateY(90vh) scale(1);
193
+ }
194
+ 90% {
195
+ opacity: 0.6;
196
+ transform: translateY(10vh) scale(1);
197
+ }
198
+ 100% {
199
+ opacity: 0;
200
+ transform: translateY(-10vh) scale(0);
201
+ }
202
+ }
203
+
204
+ .grid-overlay {
205
+ position: absolute;
206
+ inset: 0;
207
+ background-image:
208
+ linear-gradient(rgba(0, 212, 255, 0.03) 1px, transparent 1px),
209
+ linear-gradient(90deg, rgba(0, 212, 255, 0.03) 1px, transparent 1px);
210
+ background-size: 50px 50px;
211
+ mask-image: radial-gradient(ellipse at center, black 30%, transparent 70%);
212
+ -webkit-mask-image: radial-gradient(ellipse at center, black 30%, transparent 70%);
213
+ }
214
+
215
+ .scanline {
216
+ position: absolute;
217
+ top: 0;
218
+ left: 0;
219
+ right: 0;
220
+ height: 2px;
221
+ background: linear-gradient(90deg, transparent, var(--cyan-glow-subtle), transparent);
222
+ animation: scanlineMove 8s linear infinite;
223
+ opacity: 0.5;
224
+ }
225
+
226
+ @keyframes scanlineMove {
227
+ 0% { top: -2px; }
228
+ 100% { top: 100%; }
229
+ }
230
+
231
+ /* ═══ MAIN LAYOUT ═══ */
232
+ .app-container {
233
+ display: flex;
234
+ flex-direction: column;
235
+ height: 100vh;
236
+ position: relative;
237
+ z-index: var(--z-base);
238
+ }
239
 
240
  /* ═══ HEADER ═══ */
241
+ .header {
242
+ height: var(--header-height);
243
+ min-height: var(--header-height);
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: space-between;
247
+ padding: 0 16px;
248
+ background: var(--glass-bg);
249
+ backdrop-filter: blur(20px);
250
+ -webkit-backdrop-filter: blur(20px);
251
+ border-bottom: 1px solid var(--glass-border);
252
+ position: relative;
253
+ z-index: var(--z-panel);
254
+ }
255
+
256
+ .header::before {
257
+ content: '';
258
+ position: absolute;
259
+ bottom: 0;
260
+ left: 0;
261
+ right: 0;
262
+ height: 1px;
263
+ background: linear-gradient(90deg, transparent, var(--cyan-glow-subtle), var(--purple-glow-subtle), transparent);
264
+ }
265
+
266
+ .header-left {
267
+ display: flex;
268
+ align-items: center;
269
+ gap: 16px;
270
+ }
271
+
272
+ .logo {
273
+ display: flex;
274
+ align-items: center;
275
+ gap: 10px;
276
+ cursor: pointer;
277
+ transition: transform var(--transition-fast);
278
+ }
279
+
280
+ .logo:hover {
281
+ transform: scale(1.02);
282
+ }
283
+
284
+ .logo-icon {
285
+ width: 36px;
286
+ height: 36px;
287
+ background: linear-gradient(135deg, var(--cyan), var(--purple));
288
+ border-radius: var(--radius-md);
289
+ display: flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ font-size: 18px;
293
+ font-weight: 700;
294
+ color: white;
295
+ box-shadow: 0 4px 15px var(--cyan-glow), 0 0 30px var(--purple-glow-subtle);
296
+ animation: logoPulse 3s ease-in-out infinite;
297
+ position: relative;
298
+ overflow: hidden;
299
+ }
300
+
301
+ .logo-icon::after {
302
+ content: '';
303
+ position: absolute;
304
+ inset: -50%;
305
+ background: conic-gradient(from 0deg, transparent, rgba(255,255,255,0.3), transparent);
306
+ animation: logoShine 4s linear infinite;
307
+ }
308
+
309
+ @keyframes logoPulse {
310
+ 0%, 100% { box-shadow: 0 4px 15px var(--cyan-glow), 0 0 30px var(--purple-glow-subtle); }
311
+ 50% { box-shadow: 0 4px 25px var(--cyan-glow), 0 0 40px var(--purple-glow); }
312
+ }
313
+
314
+ @keyframes logoShine {
315
+ from { transform: rotate(0deg); }
316
+ to { transform: rotate(360deg); }
317
+ }
318
+
319
+ .logo-text {
320
+ display: flex;
321
+ flex-direction: column;
322
+ line-height: 1.2;
323
+ }
324
+
325
+ .logo-title {
326
+ font-size: 16px;
327
+ font-weight: 700;
328
+ letter-spacing: -0.5px;
329
+ background: linear-gradient(135deg, var(--text-primary), var(--cyan-bright));
330
+ -webkit-background-clip: text;
331
+ -webkit-text-fill-color: transparent;
332
+ background-clip: text;
333
+ }
334
+
335
+ .logo-version {
336
+ font-size: 10px;
337
+ color: var(--text-muted);
338
+ text-transform: uppercase;
339
+ letter-spacing: 1px;
340
+ font-weight: 500;
341
+ }
342
+
343
+ .header-center {
344
+ display: flex;
345
+ align-items: center;
346
+ gap: 12px;
347
+ }
348
+
349
+ .model-selector {
350
+ display: flex;
351
+ align-items: center;
352
+ gap: 8px;
353
+ padding: 6px 14px;
354
+ background: var(--glass-bg-light);
355
+ border: 1px solid var(--glass-border);
356
+ border-radius: var(--radius-xl);
357
+ cursor: pointer;
358
+ transition: all var(--transition-fast);
359
+ position: relative;
360
+ overflow: hidden;
361
+ }
362
+
363
+ .model-selector:hover {
364
+ border-color: var(--glass-border-hover);
365
+ box-shadow: 0 0 20px var(--cyan-glow-subtle);
366
+ }
367
+
368
+ .model-dot {
369
+ width: 8px;
370
+ height: 8px;
371
+ border-radius: 50%;
372
+ background: var(--text-muted);
373
+ transition: all var(--transition-base);
374
+ }
375
+
376
+ .model-dot.ready {
377
+ background: var(--green);
378
+ box-shadow: 0 0 8px var(--green-glow);
379
+ animation: dotPulse 2s ease-in-out infinite;
380
+ }
381
+
382
+ .model-dot.loading {
383
+ background: var(--amber);
384
+ animation: dotSpin 1s linear infinite;
385
+ }
386
+
387
+ .model-dot.error {
388
+ background: var(--red);
389
+ box-shadow: 0 0 8px var(--red-glow);
390
+ }
391
+
392
+ @keyframes dotPulse {
393
+ 0%, 100% { transform: scale(1); opacity: 1; }
394
+ 50% { transform: scale(1.2); opacity: 0.7; }
395
+ }
396
+
397
+ @keyframes dotSpin {
398
+ 0% { transform: rotate(0deg); }
399
+ 100% { transform: rotate(360deg); }
400
+ }
401
+
402
+ .model-text {
403
+ font-size: 12px;
404
+ font-weight: 500;
405
+ color: var(--text-secondary);
406
+ max-width: 120px;
407
+ overflow: hidden;
408
+ text-overflow: ellipsis;
409
+ white-space: nowrap;
410
+ }
411
+
412
+ .header-right {
413
+ display: flex;
414
+ align-items: center;
415
+ gap: 8px;
416
+ }
417
+
418
+ .status-indicators {
419
+ display: flex;
420
+ align-items: center;
421
+ gap: 6px;
422
+ }
423
+
424
+ .status-pill {
425
+ display: flex;
426
+ align-items: center;
427
+ gap: 6px;
428
+ padding: 5px 12px;
429
+ background: var(--glass-bg-light);
430
+ border: 1px solid var(--glass-border);
431
+ border-radius: var(--radius-xl);
432
+ font-size: 11px;
433
+ font-weight: 500;
434
+ transition: all var(--transition-fast);
435
+ }
436
+
437
+ .status-pill:hover {
438
+ border-color: var(--glass-border-hover);
439
+ }
440
+
441
+ .status-pill .dot {
442
+ width: 6px;
443
+ height: 6px;
444
+ border-radius: 50%;
445
+ background: var(--text-muted);
446
+ }
447
+
448
+ .status-pill .dot.connected,
449
+ .status-pill .dot.ready {
450
+ background: var(--green);
451
+ box-shadow: 0 0 6px var(--green-glow);
452
+ }
453
+
454
+ .status-pill .dot.disconnected {
455
+ background: var(--red);
456
+ }
457
+
458
+ .status-pill .dot.loading {
459
+ background: var(--amber);
460
+ animation: dotSpin 1s linear infinite;
461
+ }
462
+
463
+ .btn-icon {
464
+ width: 34px;
465
+ height: 34px;
466
+ display: flex;
467
+ align-items: center;
468
+ justify-content: center;
469
+ background: var(--glass-bg-light);
470
+ border: 1px solid var(--glass-border);
471
+ border-radius: var(--radius-md);
472
+ color: var(--text-secondary);
473
+ cursor: pointer;
474
+ transition: all var(--transition-fast);
475
+ font-size: 14px;
476
+ }
477
+
478
+ .btn-icon:hover {
479
+ background: var(--glass-bg);
480
+ border-color: var(--cyan-dim);
481
+ color: var(--cyan);
482
+ box-shadow: 0 0 15px var(--cyan-glow-subtle);
483
+ transform: translateY(-1px);
484
+ }
485
+
486
+ .btn-icon:active {
487
+ transform: translateY(0);
488
+ }
489
+
490
+ /* ═══ MAIN CONTENT AREA ═══ */
491
+ .main-content {
492
+ flex: 1;
493
+ display: flex;
494
+ overflow: hidden;
495
+ position: relative;
496
+ }
497
+
498
+ /* ═══ SIDEBAR ═══ */
499
+ .sidebar {
500
+ width: var(--sidebar-width);
501
+ min-width: var(--sidebar-width);
502
+ background: var(--glass-bg);
503
+ backdrop-filter: blur(20px);
504
+ -webkit-backdrop-filter: blur(20px);
505
+ border-right: 1px solid var(--glass-border);
506
+ display: flex;
507
+ flex-direction: column;
508
+ overflow: hidden;
509
+ position: relative;
510
+ z-index: var(--z-panel);
511
+ }
512
+
513
+ .sidebar::before {
514
+ content: '';
515
+ position: absolute;
516
+ top: 0;
517
+ right: 0;
518
+ bottom: 0;
519
+ width: 1px;
520
+ background: linear-gradient(180deg, var(--cyan-glow-subtle), transparent, var(--purple-glow-subtle));
521
+ opacity: 0.5;
522
+ }
523
+
524
+ .sidebar-header {
525
+ padding: 16px;
526
+ border-bottom: 1px solid var(--glass-border);
527
+ display: flex;
528
+ align-items: center;
529
+ justify-content: space-between;
530
+ }
531
+
532
+ .sidebar-title {
533
+ font-size: 11px;
534
+ font-weight: 600;
535
+ text-transform: uppercase;
536
+ letter-spacing: 1.5px;
537
+ color: var(--text-muted);
538
+ }
539
+
540
+ .sidebar-actions {
541
+ display: flex;
542
+ gap: 4px;
543
+ }
544
+
545
+ .btn-sidebar-action {
546
+ width: 24px;
547
+ height: 24px;
548
+ display: flex;
549
+ align-items: center;
550
+ justify-content: center;
551
+ background: transparent;
552
+ border: none;
553
+ border-radius: var(--radius-sm);
554
+ color: var(--text-muted);
555
+ cursor: pointer;
556
+ transition: all var(--transition-fast);
557
+ font-size: 12px;
558
+ }
559
+
560
+ .btn-sidebar-action:hover {
561
+ background: var(--glass-bg-light);
562
+ color: var(--cyan);
563
+ }
564
+
565
+ .sidebar-content {
566
+ flex: 1;
567
+ overflow-y: auto;
568
+ padding: 12px;
569
+ }
570
+
571
+ /* File Tree Styles */
572
+ .file-tree {
573
+ font-family: var(--font-mono);
574
+ font-size: 12px;
575
+ }
576
+
577
+ .ws-dir {
578
+ margin-bottom: 4px;
579
+ }
580
+
581
+ .ws-dir-name {
582
+ display: flex;
583
+ align-items: center;
584
+ gap: 6px;
585
+ padding: 6px 8px;
586
+ border-radius: var(--radius-sm);
587
+ cursor: pointer;
588
+ color: var(--text-secondary);
589
+ transition: all var(--transition-fast);
590
+ user-select: none;
591
+ }
592
+
593
+ .ws-dir-name:hover {
594
+ background: var(--glass-bg-light);
595
+ color: var(--cyan);
596
+ }
597
+
598
+ .ws-dir-name::before {
599
+ content: 'πŸ“';
600
+ font-size: 11px;
601
+ filter: grayscale(0.3);
602
+ }
603
+
604
+ .ws-children {
605
+ margin-left: 16px;
606
+ padding-left: 12px;
607
+ border-left: 1px solid var(--border-default);
608
+ }
609
+
610
+ .ws-file {
611
+ margin-bottom: 2px;
612
+ }
613
+
614
+ .ws-file > span {
615
+ display: flex;
616
+ align-items: center;
617
+ gap: 6px;
618
+ padding: 5px 8px;
619
+ border-radius: var(--radius-sm);
620
+ cursor: pointer;
621
+ color: var(--text-secondary);
622
+ transition: all var(--transition-fast);
623
+ font-size: 12px;
624
+ }
625
+
626
+ .ws-file > span:hover {
627
+ background: var(--glass-bg-light);
628
+ color: var(--cyan-bright);
629
+ padding-left: 12px;
630
+ }
631
+
632
+ .ws-file > span::before {
633
+ content: 'πŸ“„';
634
+ font-size: 11px;
635
+ }
636
+
637
+ .ws-empty {
638
+ padding: 24px 12px;
639
+ text-align: center;
640
+ color: var(--text-muted);
641
+ font-size: 12px;
642
+ }
643
+
644
+ /* File Viewer */
645
+ .file-viewer-header {
646
+ display: flex;
647
+ align-items: center;
648
+ gap: 12px;
649
+ padding: 12px;
650
+ background: var(--glass-bg-light);
651
+ border-bottom: 1px solid var(--glass-border);
652
+ }
653
+
654
+ .file-viewer-header button {
655
+ padding: 6px 12px;
656
+ background: var(--glass-bg);
657
+ border: 1px solid var(--glass-border);
658
+ border-radius: var(--radius-sm);
659
+ color: var(--text-secondary);
660
+ cursor: pointer;
661
+ font-family: var(--font-mono);
662
+ font-size: 11px;
663
+ transition: all var(--transition-fast);
664
+ }
665
+
666
+ .file-viewer-header button:hover {
667
+ border-color: var(--cyan-dim);
668
+ color: var(--cyan);
669
+ }
670
+
671
+ .file-viewer-header span {
672
+ font-size: 12px;
673
+ color: var(--text-muted);
674
+ overflow: hidden;
675
+ text-overflow: ellipsis;
676
+ white-space: nowrap;
677
+ }
678
+
679
+ .file-viewer-content {
680
+ padding: 16px;
681
+ font-family: var(--font-mono);
682
+ font-size: 12px;
683
+ line-height: 1.7;
684
+ color: var(--text-secondary);
685
+ overflow-x: auto;
686
+ white-space: pre-wrap;
687
+ word-break: break-all;
688
+ }
689
+
690
+ /* Sidebar Sections */
691
+ .sidebar-section {
692
+ margin-bottom: 16px;
693
+ }
694
+
695
+ .sidebar-section-title {
696
+ font-size: 10px;
697
+ font-weight: 600;
698
+ text-transform: uppercase;
699
+ letter-spacing: 1px;
700
+ color: var(--text-muted);
701
+ padding: 8px 12px 6px;
702
+ display: flex;
703
+ align-items: center;
704
+ gap: 6px;
705
+ }
706
+
707
+ /* MCP Section */
708
+ .mcp-server {
709
+ padding: 10px 12px;
710
+ background: var(--glass-bg-light);
711
+ border: 1px solid var(--glass-border);
712
+ border-radius: var(--radius-md);
713
+ margin-bottom: 8px;
714
+ transition: all var(--transition-fast);
715
+ }
716
+
717
+ .mcp-server:hover {
718
+ border-color: var(--glass-border-hover);
719
+ }
720
+
721
+ .mcp-server-header {
722
+ display: flex;
723
+ align-items: center;
724
+ gap: 8px;
725
+ margin-bottom: 6px;
726
+ }
727
+
728
+ .mcp-server-dot {
729
+ width: 6px;
730
+ height: 6px;
731
+ border-radius: 50%;
732
+ background: var(--text-muted);
733
+ }
734
+
735
+ .mcp-server-dot.connected {
736
+ background: var(--green);
737
+ box-shadow: 0 0 6px var(--green-glow);
738
+ }
739
+
740
+ .mcp-server-dot.disconnected {
741
+ background: var(--red);
742
+ }
743
+
744
+ .mcp-server-name {
745
+ font-size: 12px;
746
+ font-weight: 500;
747
+ color: var(--text-primary);
748
+ flex: 1;
749
+ }
750
+
751
+ .tool-badge {
752
+ font-size: 9px;
753
+ font-weight: 600;
754
+ padding: 2px 6px;
755
+ border-radius: var(--radius-sm);
756
+ text-transform: uppercase;
757
+ letter-spacing: 0.5px;
758
+ }
759
+
760
+ .tool-badge.success {
761
+ background: rgba(57, 255, 20, 0.15);
762
+ color: var(--green);
763
+ }
764
+
765
+ .tool-badge.error {
766
+ background: rgba(255, 85, 85, 0.15);
767
+ color: var(--red);
768
+ }
769
+
770
+ .tool-badge.running {
771
+ background: rgba(255, 179, 0, 0.15);
772
+ color: var(--amber);
773
+ animation: badgePulse 1.5s ease-in-out infinite;
774
+ }
775
+
776
+ @keyframes badgePulse {
777
+ 0%, 100% { opacity: 1; }
778
+ 50% { opacity: 0.6; }
779
+ }
780
+
781
+ .mcp-tools {
782
+ display: flex;
783
+ flex-wrap: wrap;
784
+ gap: 4px;
785
+ }
786
+
787
+ .mcp-tool-tag {
788
+ font-size: 10px;
789
+ padding: 2px 8px;
790
+ background: var(--bg-tertiary);
791
+ border-radius: var(--radius-sm);
792
+ color: var(--text-muted);
793
+ }
794
+
795
+ .mcp-empty {
796
+ padding: 20px 12px;
797
+ text-align: center;
798
+ color: var(--text-muted);
799
+ font-size: 12px;
800
+ line-height: 1.6;
801
+ }
802
+
803
+ /* ═══ CHAT AREA ═══ */
804
+ .chat-area {
805
+ flex: 1;
806
+ display: flex;
807
+ flex-direction: column;
808
+ overflow: hidden;
809
+ position: relative;
810
+ }
811
+
812
+ .chat-messages {
813
+ flex: 1;
814
+ overflow-y: auto;
815
+ padding: 20px;
816
+ scroll-behavior: smooth;
817
+ }
818
+
819
+ .chat-messages::-webkit-scrollbar {
820
+ width: 4px;
821
+ }
822
+
823
+ /* Welcome Banner */
824
+ .welcome-banner {
825
+ max-width: 600px;
826
+ margin: 40px auto;
827
+ text-align: center;
828
+ padding: 40px;
829
+ background: var(--glass-bg);
830
+ backdrop-filter: blur(20px);
831
+ -webkit-backdrop-filter: blur(20px);
832
+ border: 1px solid var(--glass-border);
833
+ border-radius: var(--radius-xl);
834
+ position: relative;
835
+ overflow: hidden;
836
+ }
837
+
838
+ .welcome-banner::before {
839
+ content: '';
840
+ position: absolute;
841
+ top: 0;
842
+ left: 0;
843
+ right: 0;
844
+ height: 1px;
845
+ background: linear-gradient(90deg, transparent, var(--cyan), var(--purple), transparent);
846
+ }
847
+
848
+ .welcome-banner::after {
849
+ content: '';
850
+ position: absolute;
851
+ inset: 0;
852
+ background: radial-gradient(ellipse at center top, var(--cyan-glow-subtle), transparent 70%);
853
+ pointer-events: none;
854
+ }
855
+
856
+ .banner-icon {
857
+ font-size: 48px;
858
+ margin-bottom: 16px;
859
+ display: block;
860
+ animation: bannerFloat 3s ease-in-out infinite;
861
+ }
862
+
863
+ @keyframes bannerFloat {
864
+ 0%, 100% { transform: translateY(0); }
865
+ 50% { transform: translateY(-8px); }
866
+ }
867
+
868
+ .banner-title {
869
+ font-size: 24px;
870
+ font-weight: 700;
871
+ margin-bottom: 8px;
872
+ background: linear-gradient(135deg, var(--text-primary), var(--cyan-bright), var(--purple-bright));
873
+ -webkit-background-clip: text;
874
+ -webkit-text-fill-color: transparent;
875
+ background-clip: text;
876
+ }
877
+
878
+ .banner-model {
879
+ font-size: 13px;
880
+ color: var(--cyan);
881
+ margin-bottom: 16px;
882
+ font-family: var(--font-mono);
883
+ }
884
+
885
+ .banner-desc {
886
+ color: var(--text-secondary);
887
+ font-size: 14px;
888
+ line-height: 1.7;
889
+ margin-bottom: 24px;
890
+ }
891
+
892
+ .examples-row {
893
+ display: flex;
894
+ flex-wrap: wrap;
895
+ gap: 8px;
896
+ justify-content: center;
897
+ }
898
+
899
+ .examples-label {
900
+ font-size: 12px;
901
+ color: var(--text-muted);
902
+ display: flex;
903
+ align-items: center;
904
+ margin-right: 4px;
905
+ }
906
+
907
+ .example-chip {
908
+ padding: 8px 14px;
909
+ background: var(--glass-bg-light);
910
+ border: 1px solid var(--glass-border);
911
+ border-radius: var(--radius-xl);
912
+ font-size: 12px;
913
+ color: var(--text-secondary);
914
+ cursor: pointer;
915
+ transition: all var(--transition-fast);
916
+ position: relative;
917
+ overflow: hidden;
918
+ }
919
+
920
+ .example-chip::before {
921
+ content: '';
922
+ position: absolute;
923
+ inset: 0;
924
+ background: linear-gradient(135deg, var(--cyan-glow-subtle), var(--purple-glow-subtle));
925
+ opacity: 0;
926
+ transition: opacity var(--transition-fast);
927
+ }
928
+
929
+ .example-chip:hover {
930
+ border-color: var(--cyan-dim);
931
+ color: var(--cyan);
932
+ transform: translateY(-2px);
933
+ box-shadow: 0 4px 20px var(--cyan-glow-subtle);
934
+ }
935
+
936
+ .example-chip:hover::before {
937
+ opacity: 1;
938
+ }
939
+
940
+ .example-chip span {
941
+ position: relative;
942
+ z-index: 1;
943
+ }
944
+
945
+ /* Message Styles */
946
+ .msg {
947
+ margin-bottom: 20px;
948
+ animation: msgSlideIn 0.3s ease-out;
949
+ position: relative;
950
+ }
951
+
952
+ @keyframes msgSlideIn {
953
+ from {
954
+ opacity: 0;
955
+ transform: translateY(10px);
956
+ }
957
+ to {
958
+ opacity: 1;
959
+ transform: translateY(0);
960
+ }
961
+ }
962
+
963
+ .msg-user {
964
+ display: flex;
965
+ justify-content: flex-end;
966
+ }
967
+
968
+ .msg-user .msg-content-wrapper {
969
+ max-width: 75%;
970
+ background: linear-gradient(135deg, rgba(0, 212, 255, 0.15), rgba(0, 212, 255, 0.05));
971
+ border: 1px solid rgba(0, 212, 255, 0.2);
972
+ border-radius: var(--radius-lg) var(--radius-lg) 4px var(--radius-lg);
973
+ padding: 14px 18px;
974
+ position: relative;
975
+ }
976
+
977
+ .msg-user .msg-content-wrapper::before {
978
+ content: '';
979
+ position: absolute;
980
+ top: 0;
981
+ left: 0;
982
+ right: 0;
983
+ height: 1px;
984
+ background: linear-gradient(90deg, transparent, var(--cyan-glow-subtle), transparent);
985
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
986
+ }
987
+
988
+ .msg-assistant {
989
+ display: flex;
990
+ justify-content: flex-start;
991
+ }
992
+
993
+ .msg-assistant .msg-content-wrapper {
994
+ max-width: 85%;
995
+ background: var(--glass-bg);
996
+ backdrop-filter: blur(10px);
997
+ -webkit-backdrop-filter: blur(10px);
998
+ border: 1px solid var(--glass-border);
999
+ border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) 4px;
1000
+ padding: 14px 18px;
1001
+ position: relative;
1002
+ }
1003
+
1004
+ .msg-system {
1005
+ display: flex;
1006
+ justify-content: center;
1007
+ }
1008
+
1009
+ .msg-system .msg-content-wrapper {
1010
+ max-width: 80%;
1011
+ background: var(--glass-bg-light);
1012
+ border: 1px dashed var(--glass-border);
1013
+ border-radius: var(--radius-md);
1014
+ padding: 12px 20px;
1015
+ text-align: center;
1016
+ }
1017
+
1018
+ .msg-prefix {
1019
+ font-weight: 600;
1020
+ margin-right: 6px;
1021
+ }
1022
+
1023
+ .msg-user .msg-prefix {
1024
+ color: var(--cyan);
1025
+ }
1026
+
1027
+ .msg-assistant .msg-prefix {
1028
+ color: var(--purple);
1029
+ }
1030
+
1031
+ .msg-system .msg-prefix {
1032
+ color: var(--amber);
1033
+ }
1034
+
1035
+ .msg-content {
1036
+ color: var(--text-primary);
1037
+ line-height: 1.7;
1038
+ word-wrap: break-word;
1039
+ }
1040
+
1041
+ .msg-body {
1042
+ margin-top: 8px;
1043
+ }
1044
+
1045
+ .iteration-badge {
1046
+ display: inline-flex;
1047
+ align-items: center;
1048
+ gap: 6px;
1049
+ padding: 4px 10px;
1050
+ background: var(--purple-glow-subtle);
1051
+ border: 1px solid var(--purple);
1052
+ border-radius: var(--radius-sm);
1053
+ font-size: 10px;
1054
+ font-weight: 600;
1055
+ color: var(--purple-bright);
1056
+ text-transform: uppercase;
1057
+ letter-spacing: 0.5px;
1058
+ margin-bottom: 10px;
1059
+ animation: iterationGlow 2s ease-in-out infinite;
1060
+ }
1061
+
1062
+ @keyframes iterationGlow {
1063
+ 0%, 100% { box-shadow: 0 0 10px var(--purple-glow-subtle); }
1064
+ 50% { box-shadow: 0 0 20px var(--purple-glow); }
1065
+ }
1066
+
1067
+ /* Code Block Styles */
1068
+ .code-block-wrap {
1069
+ margin: 12px 0;
1070
+ background: var(--bg-deep);
1071
+ border: 1px solid var(--border-default);
1072
+ border-radius: var(--radius-md);
1073
+ overflow: hidden;
1074
+ position: relative;
1075
+ }
1076
+
1077
+ .code-block-wrap::before {
1078
+ content: '';
1079
+ position: absolute;
1080
+ top: 0;
1081
+ left: 0;
1082
+ right: 0;
1083
+ height: 1px;
1084
+ background: linear-gradient(90deg, var(--cyan-glow-subtle), var(--purple-glow-subtle));
1085
+ }
1086
+
1087
+ .code-block-header {
1088
+ display: flex;
1089
+ align-items: center;
1090
+ justify-content: space-between;
1091
+ padding: 8px 12px;
1092
+ background: var(--bg-tertiary);
1093
+ border-bottom: 1px solid var(--border-default);
1094
+ }
1095
+
1096
+ .code-lang {
1097
+ font-size: 11px;
1098
+ font-weight: 500;
1099
+ color: var(--cyan);
1100
+ text-transform: uppercase;
1101
+ letter-spacing: 0.5px;
1102
+ font-family: var(--font-mono);
1103
+ }
1104
+
1105
+ .btn-copy {
1106
+ padding: 4px 10px;
1107
+ background: var(--glass-bg-light);
1108
+ border: 1px solid var(--glass-border);
1109
+ border-radius: var(--radius-sm);
1110
+ color: var(--text-muted);
1111
+ font-size: 11px;
1112
+ cursor: pointer;
1113
+ font-family: var(--font-mono);
1114
+ transition: all var(--transition-fast);
1115
+ }
1116
+
1117
+ .btn-copy:hover {
1118
+ border-color: var(--cyan-dim);
1119
+ color: var(--cyan);
1120
+ box-shadow: 0 0 10px var(--cyan-glow-subtle);
1121
+ }
1122
+
1123
+ .btn-copy.copied {
1124
+ background: rgba(57, 255, 20, 0.15);
1125
+ border-color: var(--green);
1126
+ color: var(--green);
1127
+ }
1128
+
1129
+ .code-block-wrap pre {
1130
+ margin: 0;
1131
+ padding: 14px;
1132
+ overflow-x: auto;
1133
+ }
1134
+
1135
+ .code-block-wrap code {
1136
+ font-family: var(--font-mono);
1137
+ font-size: 12px;
1138
+ line-height: 1.7;
1139
+ color: var(--code-text, #e0e0e0);
1140
+ --code-text: #e2e8f0;
1141
+ }
1142
+
1143
+ /* Inline code */
1144
+ .msg-content code:not([id]) {
1145
+ padding: 2px 6px;
1146
+ background: var(--bg-deep);
1147
+ border: 1px solid var(--border-default);
1148
+ border-radius: 4px;
1149
+ font-family: var(--font-mono);
1150
+ font-size: 12px;
1151
+ color: var(--cyan-bright);
1152
+ }
1153
+
1154
+ /* Headings in messages */
1155
+ .msg-content h1, .msg-content h2, .msg-content h3 {
1156
+ margin: 16px 0 8px;
1157
+ font-weight: 600;
1158
+ color: var(--text-primary);
1159
+ }
1160
+
1161
+ .msg-content h1 { font-size: 18px; color: var(--cyan-bright); }
1162
+ .msg-content h2 { font-size: 16px; color: var(--purple-bright); }
1163
+ .msg-content h3 { font-size: 14px; }
1164
+
1165
+ .msg-content strong {
1166
+ color: var(--text-primary);
1167
+ font-weight: 600;
1168
+ }
1169
+
1170
+ .msg-content em {
1171
+ color: var(--cyan);
1172
+ font-style: normal;
1173
+ }
1174
+
1175
+ .msg-content li {
1176
+ margin-left: 20px;
1177
+ margin-bottom: 4px;
1178
+ list-style: none;
1179
+ position: relative;
1180
+ }
1181
+
1182
+ .msg-content li::before {
1183
+ content: 'β–Ή';
1184
+ position: absolute;
1185
+ left: -16px;
1186
+ color: var(--cyan);
1187
+ }
1188
+
1189
+ /* Streaming Cursor */
1190
+ .streaming-cursor {
1191
+ display: inline-block;
1192
+ width: 8px;
1193
+ height: 16px;
1194
+ background: var(--cyan);
1195
+ margin-left: 2px;
1196
+ vertical-align: text-bottom;
1197
+ animation: cursorBlink 1s step-end infinite;
1198
+ border-radius: 1px;
1199
+ }
1200
+
1201
+ @keyframes cursorBlink {
1202
+ 0%, 100% { opacity: 1; }
1203
+ 50% { opacity: 0; }
1204
+ }
1205
+
1206
+ /* Tool Call Block */
1207
+ .tool-block {
1208
+ margin: 12px 0;
1209
+ background: var(--bg-deep);
1210
+ border: 1px solid var(--border-default);
1211
+ border-radius: var(--radius-md);
1212
+ overflow: hidden;
1213
+ animation: toolSlideIn 0.3s ease-out;
1214
+ }
1215
+
1216
+ @keyframes toolSlideIn {
1217
+ from {
1218
+ opacity: 0;
1219
+ transform: translateX(-10px);
1220
+ }
1221
+ to {
1222
+ opacity: 1;
1223
+ transform: translateX(0);
1224
+ }
1225
+ }
1226
+
1227
+ .tool-header {
1228
+ display: flex;
1229
+ align-items: center;
1230
+ gap: 10px;
1231
+ padding: 12px 14px;
1232
+ background: var(--bg-tertiary);
1233
+ border-bottom: 1px solid var(--border-default);
1234
+ cursor: pointer;
1235
+ transition: background var(--transition-fast);
1236
+ }
1237
+
1238
+ .tool-header:hover {
1239
+ background: var(--bg-elevated);
1240
+ }
1241
 
1242
+ .tool-icon {
1243
+ font-size: 16px;
1244
+ }
1245
+
1246
+ .tool-name {
1247
+ font-family: var(--font-mono);
1248
+ font-size: 12px;
1249
+ font-weight: 600;
1250
+ color: var(--text-primary);
1251
+ flex: 1;
1252
+ }
1253
+
1254
+ .tool-args {
1255
+ padding: 12px 14px;
1256
+ font-family: var(--font-mono);
1257
+ font-size: 11px;
1258
+ color: var(--text-muted);
1259
+ white-space: pre-wrap;
1260
+ word-break: break-all;
1261
+ border-bottom: 1px solid var(--border-default);
1262
+ background: rgba(0, 0, 0, 0.2);
1263
+ }
1264
+
1265
+ .tool-result {
1266
+ padding: 12px 14px;
1267
+ font-family: var(--font-mono);
1268
+ font-size: 11px;
1269
+ color: var(--text-secondary);
1270
+ white-space: pre-wrap;
1271
+ word-break: break-all;
1272
+ max-height: 200px;
1273
+ overflow-y: auto;
1274
+ }
1275
+
1276
+ .tool-result.error-output {
1277
+ color: var(--red);
1278
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1279
 
1280
  /* ═══ INPUT AREA ═══ */
1281
+ .input-area {
1282
+ padding: 16px 20px;
1283
+ background: var(--glass-bg);
1284
+ backdrop-filter: blur(20px);
1285
+ -webkit-backdrop-filter: blur(20px);
1286
+ border-top: 1px solid var(--glass-border);
1287
+ position: relative;
1288
+ }
1289
+
1290
+ .input-area::before {
1291
+ content: '';
1292
+ position: absolute;
1293
+ top: 0;
1294
+ left: 0;
1295
+ right: 0;
1296
+ height: 1px;
1297
+ background: linear-gradient(90deg, transparent, var(--cyan-glow-subtle), var(--purple-glow-subtle), transparent);
1298
+ }
1299
+
1300
+ .options-bar {
1301
+ display: flex;
1302
+ flex-wrap: wrap;
1303
+ gap: 8px;
1304
+ margin-bottom: 12px;
1305
+ align-items: center;
1306
+ }
1307
+
1308
+ .option-group {
1309
+ display: flex;
1310
+ align-items: center;
1311
+ gap: 6px;
1312
+ }
1313
+
1314
+ .option-group label {
1315
+ font-size: 10px;
1316
+ font-weight: 500;
1317
+ text-transform: uppercase;
1318
+ letter-spacing: 0.5px;
1319
+ color: var(--text-muted);
1320
+ }
1321
+
1322
+ .option-select {
1323
+ padding: 6px 10px;
1324
+ background: var(--glass-bg-light);
1325
+ border: 1px solid var(--glass-border);
1326
+ border-radius: var(--radius-sm);
1327
+ color: var(--text-secondary);
1328
+ font-family: var(--font-mono);
1329
+ font-size: 11px;
1330
+ cursor: pointer;
1331
+ transition: all var(--transition-fast);
1332
+ outline: none;
1333
+ }
1334
+
1335
+ .option-select:hover, .option-select:focus {
1336
+ border-color: var(--cyan-dim);
1337
+ box-shadow: 0 0 10px var(--cyan-glow-subtle);
1338
+ }
1339
+
1340
+ .option-checkbox {
1341
+ display: flex;
1342
+ align-items: center;
1343
+ gap: 6px;
1344
+ cursor: pointer;
1345
+ font-size: 11px;
1346
+ color: var(--text-muted);
1347
+ padding: 6px 10px;
1348
+ background: var(--glass-bg-light);
1349
+ border: 1px solid var(--glass-border);
1350
+ border-radius: var(--radius-sm);
1351
+ transition: all var(--transition-fast);
1352
+ }
1353
+
1354
+ .option-checkbox:hover {
1355
+ border-color: var(--glass-border-hover);
1356
+ }
1357
+
1358
+ .option-checkbox input[type="checkbox"] {
1359
+ appearance: none;
1360
+ -webkit-appearance: none;
1361
+ width: 14px;
1362
+ height: 14px;
1363
+ background: var(--bg-tertiary);
1364
+ border: 1px solid var(--border-default);
1365
+ border-radius: 3px;
1366
+ cursor: pointer;
1367
+ position: relative;
1368
+ transition: all var(--transition-fast);
1369
+ }
1370
+
1371
+ .option-checkbox input[type="checkbox"]:checked {
1372
+ background: var(--cyan);
1373
+ border-color: var(--cyan);
1374
+ box-shadow: 0 0 8px var(--cyan-glow);
1375
+ }
1376
+
1377
+ .option-checkbox input[type="checkbox"]:checked::after {
1378
+ content: 'βœ“';
1379
+ position: absolute;
1380
+ top: 50%;
1381
+ left: 50%;
1382
+ transform: translate(-50%, -50%);
1383
+ font-size: 10px;
1384
+ color: var(--bg-deep);
1385
+ font-weight: bold;
1386
+ }
1387
+
1388
+ .image-upload-btn {
1389
+ padding: 6px 12px;
1390
+ background: var(--glass-bg-light);
1391
+ border: 1px solid var(--glass-border);
1392
+ border-radius: var(--radius-sm);
1393
+ color: var(--text-muted);
1394
+ font-size: 11px;
1395
+ cursor: pointer;
1396
+ transition: all var(--transition-fast);
1397
+ display: flex;
1398
+ align-items: center;
1399
+ gap: 6px;
1400
+ }
1401
+
1402
+ .image-upload-btn:hover {
1403
+ border-color: var(--purple-dim);
1404
+ color: var(--purple);
1405
+ }
1406
+
1407
+ #opt-image-input {
1408
+ display: none;
1409
+ }
1410
+
1411
+ .image-preview-container {
1412
+ display: flex;
1413
+ gap: 8px;
1414
+ margin-bottom: 8px;
1415
+ flex-wrap: wrap;
1416
+ }
1417
+
1418
+ .img-preview {
1419
+ display: flex;
1420
+ align-items: center;
1421
+ gap: 8px;
1422
+ padding: 6px 10px;
1423
+ background: var(--glass-bg-light);
1424
+ border: 1px solid var(--purple-dim);
1425
+ border-radius: var(--radius-sm);
1426
+ animation: imgAppear 0.3s ease-out;
1427
+ }
1428
+
1429
+ @keyframes imgAppear {
1430
+ from { opacity: 0; transform: scale(0.95); }
1431
+ to { opacity: 1; transform: scale(1); }
1432
+ }
1433
+
1434
+ .img-preview img {
1435
+ width: 28px;
1436
+ height: 28px;
1437
+ object-fit: cover;
1438
+ border-radius: 4px;
1439
+ }
1440
+
1441
+ .img-preview span {
1442
+ font-size: 11px;
1443
+ color: var(--text-secondary);
1444
+ max-width: 100px;
1445
+ overflow: hidden;
1446
+ text-overflow: ellipsis;
1447
+ white-space: nowrap;
1448
+ }
1449
+
1450
+ .img-preview .remove {
1451
+ cursor: pointer;
1452
+ color: var(--text-muted);
1453
+ font-size: 16px;
1454
+ line-height: 1;
1455
+ transition: color var(--transition-fast);
1456
+ }
1457
+
1458
+ .img-preview .remove:hover {
1459
+ color: var(--red);
1460
+ }
1461
+
1462
+ .input-row {
1463
+ display: flex;
1464
+ gap: 10px;
1465
+ align-items: flex-end;
1466
+ }
1467
+
1468
+ .input-wrapper {
1469
+ flex: 1;
1470
+ position: relative;
1471
+ }
1472
+
1473
+ .chat-input {
1474
+ width: 100%;
1475
+ min-height: 48px;
1476
+ max-height: 150px;
1477
+ padding: 14px 18px;
1478
+ padding-right: 50px;
1479
+ background: var(--bg-deep);
1480
+ border: 1px solid var(--glass-border);
1481
+ border-radius: var(--radius-lg);
1482
+ color: var(--text-primary);
1483
+ font-family: var(--font-sans);
1484
+ font-size: 14px;
1485
+ line-height: 1.5;
1486
+ resize: none;
1487
+ outline: none;
1488
+ transition: all var(--transition-fast);
1489
+ }
1490
+
1491
+ .chat-input::placeholder {
1492
+ color: var(--text-muted);
1493
+ }
1494
+
1495
+ .chat-input:focus {
1496
+ border-color: var(--cyan);
1497
+ box-shadow: 0 0 0 3px var(--cyan-glow-subtle), 0 0 30px var(--cyan-glow-subtle);
1498
+ }
1499
+
1500
+ .chat-input:not(:placeholder-shown) {
1501
+ border-color: var(--glass-border-hover);
1502
+ }
1503
+
1504
+ .input-hint {
1505
+ position: absolute;
1506
+ right: 14px;
1507
+ bottom: 12px;
1508
+ font-size: 10px;
1509
+ color: var(--text-muted);
1510
+ font-family: var(--font-mono);
1511
+ }
1512
+
1513
+ .send-buttons {
1514
+ display: flex;
1515
+ gap: 8px;
1516
+ }
1517
+
1518
+ .btn-send, .btn-stop {
1519
+ width: 48px;
1520
+ height: 48px;
1521
+ display: flex;
1522
+ align-items: center;
1523
+ justify-content: center;
1524
+ border: none;
1525
+ border-radius: var(--radius-lg);
1526
+ cursor: pointer;
1527
+ font-size: 18px;
1528
+ transition: all var(--transition-base);
1529
+ position: relative;
1530
+ overflow: hidden;
1531
+ }
1532
+
1533
+ .btn-send {
1534
+ background: linear-gradient(135deg, var(--cyan), var(--cyan-dim));
1535
+ color: var(--bg-deep);
1536
+ box-shadow: 0 4px 20px var(--cyan-glow);
1537
+ }
1538
+
1539
+ .btn-send:hover {
1540
+ transform: translateY(-2px);
1541
+ box-shadow: 0 6px 30px var(--cyan-glow), 0 0 40px var(--cyan-glow-subtle);
1542
+ }
1543
+
1544
+ .btn-send:active {
1545
+ transform: translateY(0);
1546
+ }
1547
+
1548
+ .btn-send::after {
1549
+ content: '';
1550
+ position: absolute;
1551
+ inset: 0;
1552
+ background: linear-gradient(135deg, transparent, rgba(255,255,255,0.2), transparent);
1553
+ transform: translateX(-100%);
1554
+ transition: transform 0.5s;
1555
+ }
1556
+
1557
+ .btn-send:hover::after {
1558
+ transform: translateX(100%);
1559
+ }
1560
+
1561
+ .btn-stop {
1562
+ background: linear-gradient(135deg, var(--red), #cc4444);
1563
+ color: white;
1564
+ box-shadow: 0 4px 20px var(--red-glow);
1565
+ display: none;
1566
+ }
1567
+
1568
+ .btn-stop:hover {
1569
+ transform: translateY(-2px);
1570
+ box-shadow: 0 6px 30px var(--red-glow);
1571
+ }
1572
+
1573
+ /* ═══ OUTPUT PANEL (Right Side) ═══ */
1574
+ .output-panel {
1575
+ width: 380px;
1576
+ min-width: 380px;
1577
+ background: var(--glass-bg);
1578
+ backdrop-filter: blur(20px);
1579
+ -webkit-backdrop-filter: blur(20px);
1580
+ border-left: 1px solid var(--glass-border);
1581
+ display: flex;
1582
+ flex-direction: column;
1583
+ overflow: hidden;
1584
+ position: relative;
1585
+ z-index: var(--z-panel);
1586
+ }
1587
+
1588
+ .output-panel::before {
1589
+ content: '';
1590
+ position: absolute;
1591
+ top: 0;
1592
+ left: 0;
1593
+ bottom: 0;
1594
+ width: 1px;
1595
+ background: linear-gradient(180deg, var(--purple-glow-subtle), transparent, var(--cyan-glow-subtle));
1596
+ opacity: 0.5;
1597
+ }
1598
+
1599
+ .output-tabs {
1600
+ display: flex;
1601
+ padding: 8px;
1602
+ gap: 4px;
1603
+ border-bottom: 1px solid var(--glass-border);
1604
+ background: var(--glass-bg-light);
1605
+ }
1606
+
1607
+ .output-tab {
1608
+ flex: 1;
1609
+ padding: 8px 12px;
1610
+ background: transparent;
1611
+ border: none;
1612
+ border-radius: var(--radius-sm);
1613
+ color: var(--text-muted);
1614
+ font-family: var(--font-mono);
1615
+ font-size: 11px;
1616
+ font-weight: 500;
1617
+ cursor: pointer;
1618
+ transition: all var(--transition-fast);
1619
+ text-align: center;
1620
+ }
1621
+
1622
+ .output-tab:hover {
1623
+ color: var(--text-secondary);
1624
+ background: var(--glass-bg);
1625
+ }
1626
+
1627
+ .output-tab.active {
1628
+ background: var(--glass-bg);
1629
+ color: var(--cyan);
1630
+ box-shadow: 0 0 15px var(--cyan-glow-subtle);
1631
+ }
1632
+
1633
+ .tab-pane {
1634
+ display: none;
1635
+ flex: 1;
1636
+ overflow-y: auto;
1637
+ padding: 12px;
1638
+ }
1639
+
1640
+ .tab-pane.active {
1641
+ display: block;
1642
+ }
1643
+
1644
+ /* Preview Pane */
1645
+ .preview-container {
1646
+ height: 100%;
1647
+ display: flex;
1648
+ flex-direction: column;
1649
+ background: var(--bg-deep);
1650
+ border-radius: var(--radius-md);
1651
+ overflow: hidden;
1652
+ }
1653
+
1654
+ .preview-placeholder {
1655
+ flex: 1;
1656
+ display: flex;
1657
+ flex-direction: column;
1658
+ align-items: center;
1659
+ justify-content: center;
1660
+ color: var(--text-muted);
1661
+ text-align: center;
1662
+ padding: 24px;
1663
+ }
1664
+
1665
+ .preview-placeholder-icon {
1666
+ font-size: 36px;
1667
+ margin-bottom: 12px;
1668
+ opacity: 0.5;
1669
+ }
1670
+
1671
+ .preview-placeholder-text {
1672
+ font-size: 13px;
1673
+ }
1674
+
1675
+ .preview-iframe {
1676
+ flex: 1;
1677
+ width: 100%;
1678
+ border: none;
1679
+ background: white;
1680
+ display: none;
1681
+ }
1682
+
1683
+ .preview-toolbar {
1684
+ display: flex;
1685
+ justify-content: flex-end;
1686
+ padding: 8px;
1687
+ background: var(--bg-tertiary);
1688
+ border-top: 1px solid var(--border-default);
1689
+ }
1690
+
1691
+ .btn-fullscreen {
1692
+ padding: 6px 12px;
1693
+ background: var(--glass-bg);
1694
+ border: 1px solid var(--glass-border);
1695
+ border-radius: var(--radius-sm);
1696
+ color: var(--text-muted);
1697
+ font-size: 11px;
1698
+ cursor: pointer;
1699
+ transition: all var(--transition-fast);
1700
+ display: none;
1701
+ }
1702
+
1703
+ .btn-fullscreen:hover {
1704
+ border-color: var(--cyan-dim);
1705
+ color: var(--cyan);
1706
+ }
1707
+
1708
+ /* Code Pane */
1709
+ .code-container {
1710
+ height: 100%;
1711
+ display: flex;
1712
+ flex-direction: column;
1713
+ }
1714
+
1715
+ .code-toolbar {
1716
+ display: flex;
1717
+ align-items: center;
1718
+ justify-content: space-between;
1719
+ padding: 8px 12px;
1720
+ background: var(--bg-tertiary);
1721
+ border: 1px solid var(--border-default);
1722
+ border-radius: var(--radius-md) var(--radius-md) 0 0;
1723
+ }
1724
+
1725
+ .code-tab-btn {
1726
+ padding: 5px 10px;
1727
+ background: var(--glass-bg);
1728
+ border: 1px solid var(--glass-border);
1729
+ border-radius: var(--radius-sm);
1730
+ color: var(--text-muted);
1731
+ font-size: 10px;
1732
+ font-family: var(--font-mono);
1733
+ cursor: pointer;
1734
+ transition: all var(--transition-fast);
1735
+ }
1736
+
1737
+ .code-tab-btn:hover {
1738
+ border-color: var(--cyan-dim);
1739
+ color: var(--cyan);
1740
+ }
1741
+
1742
+ .code-content {
1743
+ flex: 1;
1744
+ background: var(--bg-deep);
1745
+ border: 1px solid var(--border-default);
1746
+ border-top: none;
1747
+ border-radius: 0 0 var(--radius-md) var(--radius-md);
1748
+ padding: 14px;
1749
+ overflow: auto;
1750
+ font-family: var(--font-mono);
1751
+ font-size: 12px;
1752
+ line-height: 1.7;
1753
+ color: var(--text-secondary);
1754
+ white-space: pre-wrap;
1755
+ word-break: break-all;
1756
+ }
1757
+
1758
+ /* Console Pane */
1759
+ .console-container {
1760
+ height: 100%;
1761
+ display: flex;
1762
+ flex-direction: column;
1763
+ gap: 8px;
1764
+ }
1765
+
1766
+ .console-section {
1767
+ flex: 1;
1768
+ display: flex;
1769
+ flex-direction: column;
1770
+ min-height: 0;
1771
+ }
1772
+
1773
+ .console-label {
1774
+ font-size: 10px;
1775
+ font-weight: 600;
1776
+ text-transform: uppercase;
1777
+ letter-spacing: 0.5px;
1778
+ color: var(--text-muted);
1779
+ padding: 6px 8px;
1780
+ display: flex;
1781
+ align-items: center;
1782
+ gap: 6px;
1783
+ }
1784
+
1785
+ .console-label.stdout { color: var(--green); }
1786
+ .console-label.stderr { color: var(--red); }
1787
+
1788
+ .console-output {
1789
+ flex: 1;
1790
+ background: var(--bg-deep);
1791
+ border: 1px solid var(--border-default);
1792
+ border-radius: var(--radius-sm);
1793
+ padding: 10px;
1794
+ overflow: auto;
1795
+ font-family: var(--font-mono);
1796
+ font-size: 11px;
1797
+ line-height: 1.6;
1798
+ color: var(--text-secondary);
1799
+ white-space: pre-wrap;
1800
+ word-break: break-all;
1801
+ }
1802
 
1803
  /* ═══ STATUS BAR ═══ */
1804
+ .status-bar {
1805
+ height: 28px;
1806
+ min-height: 28px;
1807
+ display: flex;
1808
+ align-items: center;
1809
+ justify-content: space-between;
1810
+ padding: 0 16px;
1811
+ background: var(--bg-primary);
1812
+ border-top: 1px solid var(--glass-border);
1813
+ font-size: 11px;
1814
+ font-family: var(--font-mono);
1815
+ }
1816
+
1817
+ .status-left, .status-right {
1818
+ display: flex;
1819
+ align-items: center;
1820
+ gap: 16px;
1821
+ }
1822
+
1823
+ .status-item {
1824
+ display: flex;
1825
+ align-items: center;
1826
+ gap: 6px;
1827
+ color: var(--text-muted);
1828
+ }
1829
+
1830
+ .status-item .indicator {
1831
+ width: 6px;
1832
+ height: 6px;
1833
+ border-radius: 50%;
1834
+ background: currentColor;
1835
+ }
1836
+
1837
+ .status-idle { color: var(--text-muted); }
1838
+ .status-working { color: var(--amber); }
1839
+ .status-success { color: var(--green); }
1840
+ .status-error { color: var(--red); }
1841
+ .status-info { color: var(--cyan); }
1842
+
1843
+ .status-working .indicator {
1844
+ animation: statusPulse 1s ease-in-out infinite;
1845
+ }
1846
+
1847
+ @keyframes statusPulse {
1848
+ 0%, 100% { opacity: 1; transform: scale(1); }
1849
+ 50% { opacity: 0.5; transform: scale(0.8); }
1850
+ }
1851
+
1852
+ /* ═══ FULLSCREEN OVERLAY ═══ */
1853
+ .fullscreen-overlay {
1854
+ position: fixed;
1855
+ inset: 0;
1856
+ background: rgba(5, 8, 16, 0.95);
1857
+ backdrop-filter: blur(10px);
1858
+ z-index: var(--z-modal);
1859
+ display: none;
1860
+ align-items: center;
1861
+ justify-content: center;
1862
+ flex-direction: column;
1863
+ }
1864
+
1865
+ .fullscreen-overlay.active {
1866
+ display: flex;
1867
+ animation: fadeIn 0.2s ease-out;
1868
+ }
1869
+
1870
+ @keyframes fadeIn {
1871
+ from { opacity: 0; }
1872
+ to { opacity: 1; }
1873
+ }
1874
+
1875
+ .fullscreen-close {
1876
+ position: absolute;
1877
+ top: 20px;
1878
+ right: 20px;
1879
+ width: 40px;
1880
+ height: 40px;
1881
+ display: flex;
1882
+ align-items: center;
1883
+ justify-content: center;
1884
+ background: var(--glass-bg);
1885
+ border: 1px solid var(--glass-border);
1886
+ border-radius: var(--radius-md);
1887
+ color: var(--text-secondary);
1888
+ font-size: 20px;
1889
+ cursor: pointer;
1890
+ transition: all var(--transition-fast);
1891
+ }
1892
+
1893
+ .fullscreen-close:hover {
1894
+ border-color: var(--red);
1895
+ color: var(--red);
1896
+ }
1897
+
1898
+ .fullscreen-iframe {
1899
+ width: 90vw;
1900
+ height: 90vh;
1901
+ border: 1px solid var(--glass-border);
1902
+ border-radius: var(--radius-lg);
1903
+ background: white;
1904
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
1905
+ }
1906
+
1907
+ /* ═══ RESPONSIVE ADJUSTMENTS ═══ */
1908
+ @media (max-width: 1200px) {
1909
+ .output-panel {
1910
+ width: 300px;
1911
+ min-width: 300px;
1912
+ }
1913
+
1914
+ .sidebar {
1915
+ width: 220px;
1916
+ min-width: 220px;
1917
+ }
1918
+ }
1919
+
1920
+ @media (max-width: 900px) {
1921
+ .sidebar {
1922
+ display: none;
1923
+ }
1924
+
1925
+ .output-panel {
1926
+ width: 250px;
1927
+ min-width: 250px;
1928
+ }
1929
+ }
1930
+
1931
+ @media (max-width: 600px) {
1932
+ .header-center {
1933
+ display: none;
1934
+ }
1935
+
1936
+ .status-indicators {
1937
+ display: none;
1938
+ }
1939
+
1940
+ .options-bar {
1941
+ display: none;
1942
+ }
1943
+
1944
+ .output-panel {
1945
+ position: fixed;
1946
+ right: 0;
1947
+ top: var(--header-height);
1948
+ bottom: 28px;
1949
+ width: 100%;
1950
+ min-width: 100%;
1951
+ transform: translateX(100%);
1952
+ transition: transform var(--transition-base);
1953
+ z-index: var(--z-overlay);
1954
+ }
1955
+
1956
+ .output-panel.show {
1957
+ transform: translateX(0);
1958
+ }
1959
+ }
1960
+
1961
+ /* ═══ LOADING/SPINNER ANIMATIONS ═══ */
1962
+ .spinner {
1963
+ width: 20px;
1964
+ height: 20px;
1965
+ border: 2px solid var(--glass-border);
1966
+ border-top-color: var(--cyan);
1967
+ border-radius: 50%;
1968
+ animation: spin 0.8s linear infinite;
1969
+ }
1970
+
1971
+ @keyframes spin {
1972
+ to { transform: rotate(360deg); }
1973
+ }
1974
+
1975
+ .typing-indicator {
1976
+ display: flex;
1977
+ align-items: center;
1978
+ gap: 4px;
1979
+ padding: 8px 12px;
1980
+ }
1981
+
1982
+ .typing-indicator span {
1983
+ width: 6px;
1984
+ height: 6px;
1985
+ background: var(--cyan);
1986
+ border-radius: 50%;
1987
+ animation: typingBounce 1.4s ease-in-out infinite;
1988
+ }
1989
+
1990
+ .typing-indicator span:nth-child(2) {
1991
+ animation-delay: 0.2s;
1992
+ }
1993
+
1994
+ .typing-indicator span:nth-child(3) {
1995
+ animation-delay: 0.4s;
1996
+ }
1997
+
1998
+ @keyframes typingBounce {
1999
+ 0%, 80%, 100% {
2000
+ transform: scale(0.6);
2001
+ opacity: 0.4;
2002
+ }
2003
+ 40% {
2004
+ transform: scale(1);
2005
+ opacity: 1;
2006
+ }
2007
+ }
2008
+
2009
+ /* ═══ UTILITY CLASSES ═══ */
2010
+ .hidden { display: none !important; }
2011
+ .visible { display: block !important; }
2012
+
2013
+ /* Glow effects on hover for interactive elements */
2014
+ .glow-hover {
2015
+ transition: box-shadow var(--transition-fast);
2016
+ }
2017
+
2018
+ .glow-hover:hover {
2019
+ box-shadow: 0 0 20px var(--cyan-glow-subtle);
2020
  }
2021
  </style>
2022
  </head>
2023
  <body>
2024
+
2025
+ <!-- Animated Background -->
2026
+ <div class="bg-canvas">
2027
+ <div class="bg-gradient-mesh"></div>
2028
+ <div class="particles-container" id="particles"></div>
2029
+ <div class="grid-overlay"></div>
2030
+ <div class="scanline"></div>
2031
+ </div>
2032
+
2033
+ <!-- Main App Container -->
2034
+ <div class="app-container">
2035
+
2036
+ <!-- Header -->
2037
+ <header class="header">
2038
+ <div class="header-left">
2039
+ <div class="logo" onclick="newChat()">
2040
+ <div class="logo-icon">⚑</div>
2041
+ <div class="logo-text">
2042
+ <span class="logo-title">SoniCoder</span>
2043
+ <span class="logo-version">v2.0 AI Agent</span>
2044
+ </div>
2045
+ </div>
2046
  </div>
2047
+
2048
+ <div class="header-center">
2049
+ <div class="model-selector" onclick="toggleModelMenu()">
2050
+ <span class="model-dot" id="model-dot"></span>
2051
+ <span class="model-text" id="model-pill-text">Loading...</span>
2052
+ <span style="font-size: 10px; color: var(--text-muted);">β–Ό</span>
2053
  </div>
2054
+
2055
+ <div class="status-pill">
2056
+ <span class="dot" id="mcp-dot"></span>
2057
  <span id="mcp-pill-text">MCP</span>
2058
  </div>
2059
+ </div>
2060
+
2061
+ <div class="header-right">
2062
+ <button class="btn-icon" onclick="refreshWorkspace()" title="Refresh Workspace">↻</button>
2063
+ <button class="btn-icon" onclick="newChat()" title="New Chat">+</button>
2064
  </div>
2065
  </header>
2066
 
2067
+ <!-- Main Content -->
2068
+ <main class="main-content">
2069
+
2070
+ <!-- Sidebar -->
2071
+ <aside class="sidebar">
2072
+ <div class="sidebar-header">
2073
+ <span class="sidebar-title">Workspace</span>
2074
+ <div class="sidebar-actions">
2075
+ <button class="btn-sidebar-action" onclick="refreshWorkspace()" title="Refresh">↻</button>
2076
+ </div>
2077
+ </div>
2078
+
2079
+ <div class="sidebar-content">
2080
+ <div class="file-tree" id="files-tree"></div>
2081
+ <div class="file-viewer" id="file-viewer"></div>
2082
+ </div>
2083
+
2084
+ <div class="sidebar-section">
2085
+ <div class="sidebar-section-title">πŸ”Œ MCP Servers</div>
2086
+ <div class="sidebar-content" style="padding: 0 12px;">
2087
+ <div id="mcp-content"></div>
2088
  </div>
2089
+ </div>
2090
+ </aside>
2091
+
2092
+ <!-- Chat Area -->
2093
+ <section class="chat-area">
2094
+ <div class="chat-messages" id="chat-messages">
2095
+ <!-- Messages will be inserted here -->
2096
+ </div>
2097
+
2098
+ <!-- Input Area -->
2099
+ <div class="input-area">
2100
+ <div class="options-bar">
2101
+ <div class="option-group">
2102
  <label>Lang</label>
2103
+ <select class="option-select" id="opt-language" onchange="updateFrameworkOptions()">
2104
  <option value="">Auto</option>
2105
  </select>
2106
  </div>
2107
+
2108
+ <div class="option-group">
2109
  <label>FW</label>
2110
+ <select class="option-select" id="opt-framework">
2111
  <option value="">Auto</option>
2112
  </select>
2113
  </div>
2114
+
2115
+ <div class="option-group">
2116
  <label>Skill</label>
2117
+ <select class="option-select" id="opt-skill">
2118
  <option value="">None</option>
2119
  </select>
2120
  </div>
2121
+
2122
+ <div class="option-group">
2123
  <label>Agent</label>
2124
+ <select class="option-select" id="opt-agent" onchange="setActiveAgent(this.value)">
2125
  <option value="">Default</option>
2126
  </select>
2127
  </div>
2128
+
2129
+ <label class="option-checkbox">
2130
  <input type="checkbox" id="opt-search">
2131
+ <span>Web Search</span>
 
2132
  </label>
2133
+
2134
+ <label class="image-upload-btn">
2135
+ πŸ–ΌοΈ Image
2136
  <input type="file" id="opt-image-input" accept="image/*" onchange="handleImageSelect(event)">
 
2137
  </label>
2138
+
2139
+ <div class="image-preview-container" id="image-preview-container"></div>
2140
+ </div>
2141
+
2142
+ <div class="input-row">
2143
+ <div class="input-wrapper">
2144
+ <textarea
2145
+ class="chat-input"
2146
+ id="chat-input"
2147
+ placeholder="Ask SoniCoder to build something amazing..."
2148
+ rows="1"
2149
+ onkeydown="handleKeyDown(event)"
2150
+ oninput="autoResize(this)"
2151
+ ></textarea>
2152
+ <span class="input-hint">Ctrl+Enter</span>
2153
+ </div>
2154
+
2155
+ <div class="send-buttons">
2156
+ <button class="btn-send" id="btn-send" onclick="handleSend()">➀</button>
2157
+ <button class="btn-stop" id="btn-stop" onclick="stopGeneration()">β– </button>
2158
+ </div>
2159
  </div>
 
2160
  </div>
2161
+ </section>
2162
 
2163
+ <!-- Output Panel -->
2164
+ <aside class="output-panel">
2165
+ <div class="output-tabs">
2166
  <button class="output-tab active" data-tab="preview" onclick="switchTab('preview')">Preview</button>
 
2167
  <button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button>
2168
+ <button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button>
 
2169
  </div>
2170
+
2171
+ <div class="tab-pane active" id="pane-preview">
2172
+ <div class="preview-container">
2173
  <div class="preview-placeholder" id="preview-placeholder">
2174
+ <div class="preview-placeholder-icon">πŸ–₯️</div>
2175
+ <div class="preview-placeholder-text">Preview will appear here<br><small>when HTML is generated</small></div>
2176
+ </div>
2177
+ <iframe class="preview-iframe" id="preview-iframe" sandbox="allow-scripts allow-same-origin"></iframe>
2178
+ <div class="preview-toolbar">
2179
+ <button class="btn-fullscreen" id="btn-fullscreen" onclick="openFullscreen()">β›Ά Fullscreen</button>
2180
+ </div>
2181
+ </div>
2182
+ </div>
2183
+
2184
+ <div class="tab-pane" id="pane-code">
2185
+ <div class="code-container">
2186
+ <div class="code-toolbar">
2187
+ <span style="font-size: 11px; color: var(--text-muted);">Generated Code</span>
2188
+ <button class="code-tab-btn" onclick="copyCode()">πŸ“‹ Copy</button>
2189
  </div>
2190
+ <pre class="code-content" id="generated-code">// Code will appear here...</pre>
 
2191
  </div>
2192
+ </div>
2193
+
2194
+ <div class="tab-pane" id="pane-console">
2195
+ <div class="console-container">
2196
  <div class="console-section">
2197
+ <div class="console-label stdout">● STDOUT</div>
2198
  <div class="console-output" id="console-stdout">No output yet.</div>
2199
  </div>
2200
  <div class="console-section">
2201
+ <div class="console-label stderr">● STDERR</div>
2202
  <div class="console-output" id="console-stderr">No errors.</div>
2203
  </div>
2204
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2205
  </div>
2206
+ </aside>
 
2207
 
2208
+ </main>
2209
+
2210
+ <!-- Status Bar -->
2211
+ <footer class="status-bar">
2212
+ <div class="status-left">
2213
+ <span class="status-item status-idle" id="status-display">
2214
+ <span class="indicator"></span>
2215
+ <span id="status-text">IDLE</span>
2216
+ </span>
2217
+ </div>
2218
+ <div class="status-right">
2219
+ <span class="status-item">
2220
+ <span id="header-model-name"></span>
2221
+ </span>
2222
+ <span class="status-item">Local Mode</span>
2223
+ <span class="status-item">SoniCoder v2</span>
2224
  </div>
2225
+ </footer>
2226
+
2227
  </div>
2228
 
2229
+ <!-- Fullscreen Overlay -->
2230
+ <div class="fullscreen-overlay" id="fullscreen-overlay">
2231
+ <button class="fullscreen-close" onclick="closeFullscreen()">βœ•</button>
2232
+ <iframe class="fullscreen-iframe" id="fullscreen-iframe" sandbox="allow-scripts allow-same-origin"></iframe>
 
 
2233
  </div>
2234
 
2235
  <script>
2236
+ // ═══════════════════════════════════════════════════════
2237
+ // SONICODER V2 - APPLICATION LOGIC
2238
+ // ═══════════════════════════════════════════════════════
2239
+
2240
+ // State Management
2241
  var state = {
2242
  sessionId: '',
2243
  isGenerating: false,
2244
  modelReady: false,
2245
+ config: null,
2246
  toolCalls: {},
2247
  currentStreamDiv: null,
2248
  lastCode: '',
 
2249
  consoleStdout: '',
2250
  consoleStderr: '',
2251
+ attachedImage: null,
2252
+ activeTab: 'preview'
 
2253
  };
2254
 
2255
+ // ═══ INITIALIZATION ═══
2256
+
2257
  document.addEventListener('DOMContentLoaded', function() {
2258
+ initParticles();
2259
  loadConfig();
2260
  pollModelStatus();
 
2261
  loadMcpStatus();
2262
+ refreshWorkspace();
2263
+
2264
+ // Add welcome message
2265
+ addMessage('system', 'Welcome to **SoniCoder v2** β€” Your AI Code Agent. What would you like to build today?');
 
 
 
 
 
 
 
 
 
 
 
2266
  });
2267
 
2268
+ function initParticles() {
2269
+ var container = document.getElementById('particles');
2270
+ var particleCount = 30;
2271
+
2272
+ for (var i = 0; i < particleCount; i++) {
2273
+ var particle = document.createElement('div');
2274
+ particle.className = 'particle';
2275
+ particle.style.left = Math.random() * 100 + '%';
2276
+ particle.style.width = (Math.random() * 3 + 1) + 'px';
2277
+ particle.style.height = particle.style.width;
2278
+ particle.style.animationDuration = (Math.random() * 20 + 15) + 's';
2279
+ particle.style.animationDelay = (Math.random() * 20) + 's';
2280
+ container.appendChild(particle);
2281
+ }
2282
  }
2283
 
2284
  async function loadConfig() {
2285
  try {
2286
+ var resp = await fetch('/api/config');
2287
+ state.config = await resp.json();
2288
+ var config = state.config;
 
 
 
 
 
 
 
 
 
 
2289
 
2290
  // Populate language selector
2291
  var langSel = document.getElementById('opt-language');
 
2403
 
2404
  if (data.status === 'ready') {
2405
  state.modelReady = true;
2406
+ dot.className = 'model-dot ready';
2407
  pillText.textContent = data.model_name || 'Ready';
2408
  document.getElementById('header-model-name').textContent = data.model_name || '';
 
2409
  setStatus('status-success', 'MODEL READY');
2410
  setTimeout(function() {
2411
  if (!state.isGenerating) setStatus('status-idle', 'IDLE');
2412
  }, 3000);
2413
  return;
2414
  } else if (data.status === 'loading') {
2415
+ dot.className = 'model-dot loading';
2416
  pillText.textContent = 'Loading...';
2417
  setStatus('status-working', 'LOADING MODEL...');
2418
  } else {
2419
+ dot.className = 'model-dot error';
2420
  pillText.textContent = 'Error';
2421
  setStatus('status-error', 'MODEL ERROR');
2422
  }
 
2426
 
2427
  async function switchModel(key) {
2428
  try {
2429
+ document.getElementById('model-dot').className = 'model-dot loading';
2430
  document.getElementById('model-pill-text').textContent = 'Switching...';
2431
  setStatus('status-working', 'SWITCHING MODEL...');
2432
  await fetch('/api/switch_model', {
 
2445
  tree.style.display = 'block';
2446
  document.getElementById('file-viewer').style.display = 'none';
2447
 
 
2448
  var rootNode = data.tree;
2449
  if (!rootNode || typeof rootNode === 'string') {
2450
  tree.innerHTML = '<div class="ws-empty">' + escHtml(rootNode || '(empty workspace)') + '</div>';
 
2455
  if (!node) return '';
2456
  var name = escHtml(node.name || '');
2457
  if (node.type === 'dir') {
2458
+ var html = '<div class="ws-dir"><span class="ws-dir-name">' + name + '</span>';
2459
  if (node.children && node.children.length > 0) {
2460
  html += '<div class="ws-children">';
2461
  for (var i = 0; i < node.children.length; i++) {
 
2467
  return html;
2468
  } else {
2469
  var size = node.size ? ' (' + formatSize(node.size) + ')' : '';
2470
+ return '<div class="ws-file" data-path="' + name + '" onclick="openWorkspaceFile(\'' + name.replace(/'/g, "\\'") + '\')"><span>' + name + size + '</span></div>';
2471
  }
2472
  }
2473
 
 
2503
  tree.style.display = 'none';
2504
  var viewer = document.getElementById('file-viewer');
2505
  viewer.style.display = 'block';
2506
+ viewer.innerHTML = '<div class="file-viewer-header"><button onclick="refreshWorkspace()">↻ Back</button><span>' + escHtml(path) + '</span></div><pre class="file-viewer-content">' + escHtml(data.content || '') + '</pre>';
2507
  } catch(e) {
2508
  alert('Error reading file: ' + e.message);
2509
  }
 
2552
  }
2553
 
2554
  function renderExamples(config) {
 
2555
  var examples = [];
2556
  if (config && Array.isArray(config.examples) && config.examples.length) {
2557
  examples = config.examples.map(function(e) {
 
2568
  'Make a React calculator component',
2569
  ];
2570
  }
 
2571
  examples = examples.slice(0, 6);
2572
  var row = document.getElementById('examples-row');
2573
+ if (!row) return;
2574
  row.innerHTML = '<span class="examples-label">Try:</span>';
2575
  examples.forEach(function(ex) {
2576
  var chip = document.createElement('span');
 
2613
  if (e.key === 'Escape') closeFullscreen();
2614
  });
2615
 
2616
+ // ═══ INPUT HANDLING ═══
2617
+
2618
+ function handleKeyDown(event) {
2619
+ if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
2620
+ event.preventDefault();
2621
+ handleSend();
2622
+ }
2623
+ }
2624
+
2625
+ function autoResize(textarea) {
2626
+ textarea.style.height = 'auto';
2627
+ textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';
2628
+ }
2629
+
2630
  // ═══ MESSAGES ═══
2631
 
2632
  function escHtml(t) { var d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
 
2637
  var div = document.createElement('div');
2638
  div.className = 'msg msg-' + role;
2639
  div.id = extra.msgId || '';
2640
+
2641
+ var wrapper = document.createElement('div');
2642
+ wrapper.className = 'msg-content-wrapper';
2643
+
2644
  var prefix = '';
2645
+ if (role === 'user') prefix = '<span class="msg-prefix">β–Έ </span>';
2646
+ else if (role === 'assistant') prefix = '<span class="msg-prefix">β—ˆ </span>';
2647
+ else if (role === 'system') prefix = '<span class="msg-prefix">β—‰ </span>';
2648
+
 
2649
  var iterationHtml = extra.iteration ? '<div class="iteration-badge">Iteration ' + extra.iteration + '/' + extra.max_iterations + '</div>' : '';
2650
+ wrapper.innerHTML = iterationHtml + prefix + '<span class="msg-content">' + renderMd(content) + '</span>';
2651
+
2652
+ div.appendChild(wrapper);
2653
  container.appendChild(div);
2654
  container.scrollTop = container.scrollHeight;
2655
  return div;
 
2661
  // Code blocks
2662
  text = text.replace(/```(\w*)\n([\s\S]*?)```/g, function(_, lang, code) {
2663
  var id = 'cb_' + Date.now() + Math.random().toString(36).substr(2,4);
2664
+ return '<div class="code-block-wrap"><div class="code-block-header"><span class="code-lang">' + (lang||'code') + '</span><button class="btn-copy" onclick="copyBlock(\'' + id + '\')">πŸ“‹ Copy</button></div><pre><code id="' + id + '">' + code.trim() + '</code></pre></div>';
2665
  });
2666
  text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
2667
  text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
 
2679
  if (!el) return;
2680
  navigator.clipboard.writeText(el.textContent).then(function() {
2681
  var btn = el.closest('.code-block-wrap').querySelector('.btn-copy');
2682
+ btn.textContent = 'βœ“ Copied!';
2683
  btn.classList.add('copied');
2684
+ setTimeout(function() { btn.innerHTML = 'πŸ“‹ Copy'; btn.classList.remove('copied'); }, 2000);
2685
  });
2686
  }
2687
 
 
2689
  if (!state.lastCode) return;
2690
  navigator.clipboard.writeText(state.lastCode).then(function() {
2691
  var btn = document.querySelector('#pane-code .code-tab-btn');
2692
+ btn.textContent = 'βœ“ Copied!';
2693
+ setTimeout(function() { btn.innerHTML = 'πŸ“‹ Copy'; }, 2000);
2694
  });
2695
  }
2696
 
 
2711
  }
2712
  div.innerHTML =
2713
  '<div class="tool-header">' +
2714
+ '<span class="tool-icon">⚑</span>' +
2715
  '<span class="tool-name">' + escHtml(toolName) + '</span>' +
2716
  '<span class="tool-badge running">RUNNING</span>' +
2717
  '</div>' +
 
2861
  break;
2862
  case 'complete':
2863
  finalizeStream(ev.content || 'Done.');
 
 
2864
  setTimeout(refreshWorkspace, 500);
2865
  setStatus('status-success', 'COMPLETE');
2866
  setTimeout(function() { if (!state.isGenerating) setStatus('status-idle', 'IDLE'); }, 3000);
 
2923
  var btn = document.getElementById('btn-fullscreen');
2924
  placeholder.style.display = 'none';
2925
  iframe.style.display = 'block';
2926
+ btn.style.display = 'inline-block';
2927
  iframe.srcdoc = html;
2928
  switchTab('preview');
2929
  }
 
2932
  document.getElementById('console-stdout').textContent = state.consoleStdout || 'No output yet.';
2933
  document.getElementById('console-stderr').textContent = state.consoleStderr || 'No errors.';
2934
  }
2935
+
2936
+ // ═══ STATUS HELPER ═══
2937
+
2938
+ function setStatus(className, text) {
2939
+ var el = document.getElementById('status-display');
2940
+ el.className = 'status-item ' + className;
2941
+ document.getElementById('status-text').textContent = text;
2942
+ }
2943
+
2944
+ // ═══ MODEL MENU TOGGLE (placeholder) ═══
2945
+
2946
+ function toggleModelMenu() {
2947
+ // Could implement dropdown menu here
2948
+ console.log('Model menu toggle clicked');
2949
+ }
2950
  </script>
2951
  </body>
2952
+ </html>