vibesurf 0.1.10__py3-none-any.whl → 0.1.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of vibesurf might be problematic. Click here for more details.

Files changed (51) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/browser_use_agent.py +68 -45
  3. vibe_surf/agents/prompts/report_writer_prompt.py +73 -0
  4. vibe_surf/agents/prompts/vibe_surf_prompt.py +85 -172
  5. vibe_surf/agents/report_writer_agent.py +380 -226
  6. vibe_surf/agents/vibe_surf_agent.py +879 -825
  7. vibe_surf/agents/views.py +130 -0
  8. vibe_surf/backend/api/activity.py +3 -1
  9. vibe_surf/backend/api/browser.py +9 -5
  10. vibe_surf/backend/api/config.py +8 -5
  11. vibe_surf/backend/api/files.py +59 -50
  12. vibe_surf/backend/api/models.py +2 -2
  13. vibe_surf/backend/api/task.py +45 -12
  14. vibe_surf/backend/database/manager.py +24 -18
  15. vibe_surf/backend/database/queries.py +199 -192
  16. vibe_surf/backend/database/schemas.py +1 -1
  17. vibe_surf/backend/main.py +4 -2
  18. vibe_surf/backend/shared_state.py +28 -35
  19. vibe_surf/backend/utils/encryption.py +3 -1
  20. vibe_surf/backend/utils/llm_factory.py +41 -36
  21. vibe_surf/browser/agent_browser_session.py +0 -4
  22. vibe_surf/browser/browser_manager.py +14 -8
  23. vibe_surf/browser/utils.py +5 -3
  24. vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
  25. vibe_surf/chrome_extension/background.js +4 -0
  26. vibe_surf/chrome_extension/scripts/api-client.js +13 -0
  27. vibe_surf/chrome_extension/scripts/file-manager.js +27 -71
  28. vibe_surf/chrome_extension/scripts/session-manager.js +21 -3
  29. vibe_surf/chrome_extension/scripts/ui-manager.js +831 -48
  30. vibe_surf/chrome_extension/sidepanel.html +21 -4
  31. vibe_surf/chrome_extension/styles/activity.css +365 -5
  32. vibe_surf/chrome_extension/styles/input.css +139 -0
  33. vibe_surf/cli.py +4 -22
  34. vibe_surf/common.py +35 -0
  35. vibe_surf/llm/openai_compatible.py +148 -93
  36. vibe_surf/logger.py +99 -0
  37. vibe_surf/{controller/vibesurf_tools.py → tools/browser_use_tools.py} +233 -219
  38. vibe_surf/tools/file_system.py +415 -0
  39. vibe_surf/{controller → tools}/mcp_client.py +4 -3
  40. vibe_surf/tools/report_writer_tools.py +21 -0
  41. vibe_surf/tools/vibesurf_tools.py +657 -0
  42. vibe_surf/tools/views.py +120 -0
  43. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/METADATA +6 -2
  44. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/RECORD +49 -43
  45. vibe_surf/controller/file_system.py +0 -53
  46. vibe_surf/controller/views.py +0 -37
  47. /vibe_surf/{controller → tools}/__init__.py +0 -0
  48. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/WHEEL +0 -0
  49. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/entry_points.txt +0 -0
  50. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/licenses/LICENSE +0 -0
  51. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,130 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import pickle
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional, Union
11
+ from uuid_extensions import uuid7str
12
+ from json_repair import repair_json
13
+
14
+ from browser_use.browser.session import BrowserSession
15
+ from browser_use.llm.base import BaseChatModel
16
+ from browser_use.llm.messages import UserMessage, SystemMessage, BaseMessage, AssistantMessage, ContentPartTextParam, \
17
+ ContentPartImageParam, ImageURL
18
+ from browser_use.browser.views import TabInfo, BrowserStateSummary
19
+ from browser_use.filesystem.file_system import FileSystem
20
+ from browser_use.agent.views import AgentSettings
21
+ from pydantic import BaseModel, Field, ConfigDict, create_model
22
+ from browser_use.agent.views import AgentSettings, DEFAULT_INCLUDE_ATTRIBUTES
23
+ from browser_use.tools.registry.views import ActionModel
24
+
25
+
26
+ class VibeSurfAgentOutput(BaseModel):
27
+ """Agent output model following browser_use patterns"""
28
+ model_config = ConfigDict(arbitrary_types_allowed=True, extra='forbid')
29
+
30
+ thinking: str | None = None
31
+ action: List[Any] = Field(
32
+ ...,
33
+ description='List of actions to execute',
34
+ json_schema_extra={'min_items': 1},
35
+ )
36
+
37
+ @classmethod
38
+ def model_json_schema(cls, **kwargs):
39
+ schema = super().model_json_schema(**kwargs)
40
+ schema['required'] = ['thinking', 'action']
41
+ return schema
42
+
43
+ @staticmethod
44
+ def type_with_custom_actions(custom_actions: type) -> type:
45
+ """Extend actions with custom actions"""
46
+ model_ = create_model(
47
+ 'VibeSurfAgentOutput',
48
+ __base__=VibeSurfAgentOutput,
49
+ action=(
50
+ list[custom_actions], # type: ignore
51
+ Field(..., description='List of actions to execute', json_schema_extra={'min_items': 1}),
52
+ ),
53
+ __module__=VibeSurfAgentOutput.__module__,
54
+ )
55
+ model_.__doc__ = 'VibeSurfAgentOutput model with custom actions'
56
+ return model_
57
+
58
+
59
+ class VibeSurfAgentSettings(BaseModel):
60
+ use_vision: bool = True
61
+ max_failures: int = 3
62
+ override_system_message: str | None = None
63
+ extend_system_message: str | None = None
64
+ include_attributes: list[str] | None = DEFAULT_INCLUDE_ATTRIBUTES
65
+ max_actions_per_step: int = 4
66
+ max_history_items: int | None = None
67
+ include_token_cost: bool = False
68
+
69
+ calculate_cost: bool = False
70
+ include_tool_call_examples: bool = False
71
+ llm_timeout: int = 60 # Timeout in seconds for LLM calls
72
+ step_timeout: int = 180 # Timeout in seconds for each step
73
+
74
+
75
+ class CustomAgentOutput(BaseModel):
76
+ model_config = ConfigDict(arbitrary_types_allowed=True, extra='forbid')
77
+
78
+ thinking: str | None = None
79
+ action: list[ActionModel] = Field(
80
+ ...,
81
+ description='List of actions to execute',
82
+ json_schema_extra={'min_items': 1}, # Ensure at least one action is provided
83
+ )
84
+
85
+ @classmethod
86
+ def model_json_schema(cls, **kwargs):
87
+ schema = super().model_json_schema(**kwargs)
88
+ schema['required'] = ['action']
89
+ return schema
90
+
91
+ @staticmethod
92
+ def type_with_custom_actions(custom_actions: type[ActionModel]) -> type['CustomAgentOutput']:
93
+ """Extend actions with custom actions"""
94
+
95
+ model_ = create_model(
96
+ 'AgentOutput',
97
+ __base__=CustomAgentOutput,
98
+ action=(
99
+ list[custom_actions], # type: ignore
100
+ Field(..., description='List of actions to execute', json_schema_extra={'min_items': 1}),
101
+ ),
102
+ __module__=CustomAgentOutput.__module__,
103
+ )
104
+ model_.__doc__ = 'AgentOutput model with custom actions'
105
+ return model_
106
+
107
+ @staticmethod
108
+ def type_with_custom_actions_no_thinking(custom_actions: type[ActionModel]) -> type['CustomAgentOutput']:
109
+ """Extend actions with custom actions and exclude thinking field"""
110
+
111
+ class AgentOutputNoThinking(CustomAgentOutput):
112
+ @classmethod
113
+ def model_json_schema(cls, **kwargs):
114
+ schema = super().model_json_schema(**kwargs)
115
+ del schema['properties']['thinking']
116
+ schema['required'] = ['action']
117
+ return schema
118
+
119
+ model = create_model(
120
+ 'AgentOutput',
121
+ __base__=AgentOutputNoThinking,
122
+ action=(
123
+ list[custom_actions], # type: ignore
124
+ Field(..., description='List of actions to execute', json_schema_extra={'min_items': 1}),
125
+ ),
126
+ __module__=AgentOutputNoThinking.__module__,
127
+ )
128
+
129
+ model.__doc__ = 'AgentOutput model with custom actions'
130
+ return model
@@ -14,7 +14,9 @@ from ..database import get_db_session
14
14
  from ..database.queries import TaskQueries
15
15
  from .models import ActivityQueryRequest, SessionActivityQueryRequest
16
16
 
17
- logger = logging.getLogger(__name__)
17
+ from vibe_surf.logger import get_logger
18
+
19
+ logger = get_logger(__name__)
18
20
 
19
21
  router = APIRouter(prefix="/activity", tags=["activity"])
20
22
 
@@ -8,16 +8,16 @@ from fastapi import APIRouter, HTTPException
8
8
  from typing import Dict, Any, Optional
9
9
  import logging
10
10
 
11
- # Import global variables from shared_state
12
- from ..shared_state import browser_manager
11
+ from vibe_surf.logger import get_logger
13
12
 
14
- logger = logging.getLogger(__name__)
13
+ logger = get_logger(__name__)
15
14
 
16
15
  router = APIRouter(prefix="/browser", tags=["browser"])
17
16
 
18
17
 
19
18
  @router.get("/active-tab")
20
19
  async def get_active_tab() -> Dict[str, Dict[str, str]]:
20
+ from ..shared_state import browser_manager
21
21
  """Get the current active tab information"""
22
22
  if not browser_manager:
23
23
  raise HTTPException(status_code=503, detail="Browser manager not initialized")
@@ -27,11 +27,13 @@ async def get_active_tab() -> Dict[str, Dict[str, str]]:
27
27
  active_tab_info = await browser_manager.get_activate_tab()
28
28
 
29
29
  if not active_tab_info:
30
+ logger.info("No active tab found!")
30
31
  return {}
31
32
 
33
+ logger.info(active_tab_info)
32
34
  # Return dict format: {tab_id: {url: , title: }}
33
35
  return {
34
- active_tab_info.target_id: {
36
+ active_tab_info.target_id[:-4]: {
35
37
  "url": active_tab_info.url,
36
38
  "title": active_tab_info.title
37
39
  }
@@ -45,6 +47,8 @@ async def get_active_tab() -> Dict[str, Dict[str, str]]:
45
47
  @router.get("/all-tabs")
46
48
  async def get_all_tabs() -> Dict[str, Dict[str, str]]:
47
49
  """Get all browser tabs information"""
50
+ from ..shared_state import browser_manager
51
+
48
52
  if not browser_manager:
49
53
  raise HTTPException(status_code=503, detail="Browser manager not initialized")
50
54
 
@@ -54,7 +58,7 @@ async def get_all_tabs() -> Dict[str, Dict[str, str]]:
54
58
  # Filter only page targets and build result dict
55
59
  result = {}
56
60
  for tab_info in all_tab_infos:
57
- result[tab_info.target_id] = {
61
+ result[tab_info.target_id[-4:]] = {
58
62
  "url": tab_info.url,
59
63
  "title": tab_info.title
60
64
  }
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Configuration API endpoints for VibeSurf Backend
3
3
 
4
- Handles LLM Profile and controller configuration management.
4
+ Handles LLM Profile and tools configuration management.
5
5
  """
6
6
 
7
7
  from fastapi import APIRouter, HTTPException, Depends
@@ -18,7 +18,10 @@ from .models import (
18
18
  )
19
19
 
20
20
  router = APIRouter(prefix="/config", tags=["config"])
21
- logger = logging.getLogger(__name__)
21
+
22
+ from vibe_surf.logger import get_logger
23
+
24
+ logger = get_logger(__name__)
22
25
 
23
26
  def _profile_to_response_dict(profile) -> dict:
24
27
  """Convert SQLAlchemy LLMProfile to dict for Pydantic validation - safe extraction"""
@@ -654,8 +657,8 @@ async def get_configuration_status(db: AsyncSession = Depends(get_db_session)):
654
657
  "default_profile": default_profile.profile_name if default_profile else None,
655
658
  "has_default": default_profile is not None
656
659
  },
657
- "controller": {
658
- "initialized": shared_state.controller is not None
660
+ "tools": {
661
+ "initialized": shared_state.vibesurf_tools is not None
659
662
  },
660
663
  "browser_manager": {
661
664
  "initialized": shared_state.browser_manager is not None
@@ -666,7 +669,7 @@ async def get_configuration_status(db: AsyncSession = Depends(get_db_session)):
666
669
  },
667
670
  "overall_status": "ready" if (
668
671
  default_profile and
669
- shared_state.controller and
672
+ shared_state.vibesurf_tools and
670
673
  shared_state.browser_manager and
671
674
  shared_state.vibesurf_agent
672
675
  ) else "partial"
@@ -21,39 +21,44 @@ from ..database import get_db_session
21
21
  from ..database.queries import UploadedFileQueries
22
22
  from .models import FileListQueryRequest, SessionFilesQueryRequest
23
23
 
24
- logger = logging.getLogger(__name__)
24
+ from vibe_surf.logger import get_logger
25
+
26
+ logger = get_logger(__name__)
25
27
 
26
28
  router = APIRouter(prefix="/files", tags=["files"])
27
29
 
30
+
28
31
  def get_upload_directory(session_id: Optional[str] = None) -> str:
29
32
  from ..shared_state import workspace_dir
30
33
  """Get the upload directory path for a session or global uploads"""
31
34
  if session_id:
32
- upload_dir = os.path.join(workspace_dir, session_id, "upload_files")
35
+ upload_dir = os.path.join(workspace_dir, "sessions", session_id, "upload_files")
33
36
  else:
34
- upload_dir = os.path.join(workspace_dir, "upload_files")
35
-
37
+ upload_dir = os.path.join(workspace_dir, "sessions", "upload_files")
38
+
36
39
  # Create directory if it doesn't exist
37
40
  os.makedirs(upload_dir, exist_ok=True)
38
41
  return upload_dir
39
42
 
43
+
40
44
  def is_safe_path(basedir: str, path: str) -> bool:
41
45
  """Check if the path is safe (within basedir)"""
42
46
  try:
43
47
  # Resolve both paths to absolute paths
44
48
  basedir = os.path.abspath(basedir)
45
49
  path = os.path.abspath(path)
46
-
50
+
47
51
  # Check if path starts with basedir
48
52
  return path.startswith(basedir)
49
53
  except:
50
54
  return False
51
55
 
56
+
52
57
  @router.post("/upload")
53
58
  async def upload_files(
54
- files: List[UploadFile] = File(...),
55
- session_id: Optional[str] = Form(None),
56
- db: AsyncSession = Depends(get_db_session)
59
+ files: List[UploadFile] = File(...),
60
+ session_id: Optional[str] = Form(None),
61
+ db: AsyncSession = Depends(get_db_session)
57
62
  ):
58
63
  """Upload files to workspace/upload_files folder or session-specific folder"""
59
64
  try:
@@ -61,18 +66,18 @@ async def upload_files(
61
66
 
62
67
  upload_dir = get_upload_directory(session_id)
63
68
  uploaded_file_info = []
64
-
69
+
65
70
  for file in files:
66
71
  if not file.filename:
67
72
  continue
68
-
73
+
69
74
  # Generate unique file ID
70
75
  file_id = uuid7str()
71
-
76
+
72
77
  # Create safe filename
73
78
  filename = file.filename
74
79
  file_path = os.path.join(upload_dir, filename)
75
-
80
+
76
81
  # Handle duplicate filenames by adding suffix
77
82
  counter = 1
78
83
  base_name, ext = os.path.splitext(filename)
@@ -81,21 +86,21 @@ async def upload_files(
81
86
  file_path = os.path.join(upload_dir, new_filename)
82
87
  filename = new_filename
83
88
  counter += 1
84
-
89
+
85
90
  # Ensure path is safe
86
91
  if not is_safe_path(upload_dir, file_path):
87
92
  raise HTTPException(status_code=400, detail=f"Invalid file path: {filename}")
88
-
93
+
89
94
  # Save file
90
95
  try:
91
96
  with open(file_path, "wb") as buffer:
92
97
  shutil.copyfileobj(file.file, buffer)
93
-
98
+
94
99
  # Get file info
95
100
  file_size = os.path.getsize(file_path)
96
101
  mime_type, _ = mimetypes.guess_type(file_path)
97
102
  relative_path = os.path.relpath(file_path, workspace_dir)
98
-
103
+
99
104
  # Store file metadata in database
100
105
  uploaded_file = await UploadedFileQueries.create_file_record(
101
106
  db=db,
@@ -108,7 +113,7 @@ async def upload_files(
108
113
  mime_type=mime_type or "application/octet-stream",
109
114
  relative_path=relative_path
110
115
  )
111
-
116
+
112
117
  # Create response metadata
113
118
  file_metadata = {
114
119
  "file_id": uploaded_file.file_id,
@@ -120,11 +125,11 @@ async def upload_files(
120
125
  "upload_time": uploaded_file.upload_time.isoformat(),
121
126
  "file_path": file_path
122
127
  }
123
-
128
+
124
129
  uploaded_file_info.append(file_metadata)
125
-
130
+
126
131
  logger.info(f"File uploaded: {filename} (ID: {file_id}) to {upload_dir}")
127
-
132
+
128
133
  except Exception as e:
129
134
  logger.error(f"Failed to save file {filename}: {e}")
130
135
  # If database record was created but file save failed, clean up
@@ -133,20 +138,21 @@ async def upload_files(
133
138
  except:
134
139
  pass
135
140
  raise HTTPException(status_code=500, detail=f"Failed to save file {filename}: {str(e)}")
136
-
141
+
137
142
  # Commit all database changes
138
143
  await db.commit()
139
-
144
+
140
145
  return {
141
146
  "message": f"Successfully uploaded {len(uploaded_file_info)} files",
142
147
  "files": uploaded_file_info,
143
148
  "upload_directory": upload_dir
144
149
  }
145
-
150
+
146
151
  except Exception as e:
147
152
  logger.error(f"File upload failed: {e}")
148
153
  raise HTTPException(status_code=500, detail=f"File upload failed: {str(e)}")
149
154
 
155
+
150
156
  @router.get("/{file_id}")
151
157
  async def download_file(file_id: str, db: AsyncSession = Depends(get_db_session)):
152
158
  """Download file by file ID"""
@@ -155,26 +161,27 @@ async def download_file(file_id: str, db: AsyncSession = Depends(get_db_session)
155
161
  uploaded_file = await UploadedFileQueries.get_file(db, file_id)
156
162
  if not uploaded_file:
157
163
  raise HTTPException(status_code=404, detail="File not found")
158
-
164
+
159
165
  file_path = uploaded_file.file_path
160
-
166
+
161
167
  if not os.path.exists(file_path):
162
168
  raise HTTPException(status_code=404, detail="File not found on disk")
163
-
169
+
164
170
  # Ensure path is safe
165
171
  if not is_safe_path(workspace_dir, file_path):
166
172
  raise HTTPException(status_code=403, detail="Access denied")
167
-
173
+
168
174
  return FileResponse(
169
175
  path=file_path,
170
176
  filename=uploaded_file.original_filename,
171
177
  media_type=uploaded_file.mime_type
172
178
  )
173
179
 
180
+
174
181
  @router.get("")
175
182
  async def list_uploaded_files(
176
- query: FileListQueryRequest = Depends(),
177
- db: AsyncSession = Depends(get_db_session)
183
+ query: FileListQueryRequest = Depends(),
184
+ db: AsyncSession = Depends(get_db_session)
178
185
  ):
179
186
  """List uploaded files, optionally filtered by session"""
180
187
  try:
@@ -186,14 +193,14 @@ async def list_uploaded_files(
186
193
  offset=query.offset,
187
194
  active_only=True
188
195
  )
189
-
196
+
190
197
  # Get total count
191
198
  total_count = await UploadedFileQueries.count_files(
192
199
  db=db,
193
200
  session_id=query.session_id,
194
201
  active_only=True
195
202
  )
196
-
203
+
197
204
  # Convert to response format (exclude file_path for security)
198
205
  files_response = []
199
206
  for file_record in uploaded_files:
@@ -207,7 +214,7 @@ async def list_uploaded_files(
207
214
  "upload_time": file_record.upload_time.isoformat(),
208
215
  "file_path": file_record.file_path
209
216
  })
210
-
217
+
211
218
  return {
212
219
  "files": files_response,
213
220
  "total_count": total_count,
@@ -216,11 +223,12 @@ async def list_uploaded_files(
216
223
  "has_more": query.limit != -1 and (query.offset + query.limit < total_count),
217
224
  "session_id": query.session_id
218
225
  }
219
-
226
+
220
227
  except Exception as e:
221
228
  logger.error(f"Failed to list files: {e}")
222
229
  raise HTTPException(status_code=500, detail=f"Failed to list files: {str(e)}")
223
230
 
231
+
224
232
  @router.delete("/{file_id}")
225
233
  async def delete_file(file_id: str, db: AsyncSession = Depends(get_db_session)):
226
234
  """Delete uploaded file by file ID"""
@@ -228,40 +236,41 @@ async def delete_file(file_id: str, db: AsyncSession = Depends(get_db_session)):
228
236
  uploaded_file = await UploadedFileQueries.get_file(db, file_id)
229
237
  if not uploaded_file:
230
238
  raise HTTPException(status_code=404, detail="File not found")
231
-
239
+
232
240
  try:
233
241
  # Remove file from disk
234
242
  if os.path.exists(uploaded_file.file_path):
235
243
  os.remove(uploaded_file.file_path)
236
-
244
+
237
245
  # Soft delete from database
238
246
  success = await UploadedFileQueries.delete_file(db, file_id)
239
247
  if not success:
240
248
  raise HTTPException(status_code=500, detail="Failed to delete file record")
241
-
249
+
242
250
  await db.commit()
243
-
251
+
244
252
  return {
245
253
  "message": f"File {uploaded_file.original_filename} deleted successfully",
246
254
  "file_id": file_id
247
255
  }
248
-
256
+
249
257
  except HTTPException:
250
258
  raise
251
259
  except Exception as e:
252
260
  logger.error(f"Failed to delete file {file_id}: {e}")
253
261
  raise HTTPException(status_code=500, detail=f"Failed to delete file: {str(e)}")
254
262
 
263
+
255
264
  @router.get("/session/{session_id}")
256
265
  async def list_session_files(
257
- session_id: str,
258
- query: SessionFilesQueryRequest = Depends()
266
+ session_id: str,
267
+ query: SessionFilesQueryRequest = Depends()
259
268
  ):
260
269
  """List all files in a session directory"""
261
270
  try:
262
271
  from ..shared_state import workspace_dir
263
272
  session_dir = os.path.join(workspace_dir, session_id)
264
-
273
+
265
274
  if not os.path.exists(session_dir):
266
275
  return {
267
276
  "session_id": session_id,
@@ -269,16 +278,16 @@ async def list_session_files(
269
278
  "directories": [],
270
279
  "message": "Session directory not found"
271
280
  }
272
-
281
+
273
282
  files = []
274
283
  directories = []
275
-
284
+
276
285
  for root, dirs, filenames in os.walk(session_dir):
277
286
  # Calculate relative path from session directory
278
287
  rel_root = os.path.relpath(root, session_dir)
279
288
  if rel_root == ".":
280
289
  rel_root = ""
281
-
290
+
282
291
  # Add directories if requested
283
292
  if query.include_directories:
284
293
  for dirname in dirs:
@@ -288,16 +297,16 @@ async def list_session_files(
288
297
  "path": dir_path,
289
298
  "type": "directory"
290
299
  })
291
-
300
+
292
301
  # Add files
293
302
  for filename in filenames:
294
303
  file_path = os.path.join(root, filename)
295
304
  rel_path = os.path.join(rel_root, filename) if rel_root else filename
296
-
305
+
297
306
  try:
298
307
  stat = os.stat(file_path)
299
308
  mime_type, _ = mimetypes.guess_type(file_path)
300
-
309
+
301
310
  files.append({
302
311
  "name": filename,
303
312
  "path": rel_path,
@@ -308,7 +317,7 @@ async def list_session_files(
308
317
  })
309
318
  except Exception as e:
310
319
  logger.warning(f"Could not get stats for file {file_path}: {e}")
311
-
320
+
312
321
  return {
313
322
  "session_id": session_id,
314
323
  "files": files,
@@ -316,7 +325,7 @@ async def list_session_files(
316
325
  "total_files": len(files),
317
326
  "total_directories": len(directories) if query.include_directories else 0
318
327
  }
319
-
328
+
320
329
  except Exception as e:
321
330
  logger.error(f"Failed to list session files for {session_id}: {e}")
322
- raise HTTPException(status_code=500, detail=f"Failed to list session files: {str(e)}")
331
+ raise HTTPException(status_code=500, detail=f"Failed to list session files: {str(e)}")
@@ -245,13 +245,13 @@ class LLMConfigResponse(BaseModel):
245
245
  available_providers: List[str] = []
246
246
 
247
247
  class ControllerConfigRequest(BaseModel):
248
- """Request model for updating controller configuration"""
248
+ """Request model for updating tools configuration"""
249
249
  exclude_actions: Optional[List[str]] = Field(default=None, description="Actions to exclude from execution")
250
250
  max_actions_per_task: Optional[int] = Field(default=None, gt=0, description="Maximum actions per task")
251
251
  display_files_in_done_text: Optional[bool] = Field(default=None, description="Whether to display files in done text")
252
252
 
253
253
  class ControllerConfigResponse(BaseModel):
254
- """Response model for controller configuration"""
254
+ """Response model for tools configuration"""
255
255
  exclude_actions: List[str] = []
256
256
  max_actions_per_task: int = 100
257
257
  display_files_in_done_text: bool = True
@@ -25,7 +25,9 @@ from ..shared_state import (
25
25
  browser_manager
26
26
  )
27
27
 
28
- logger = logging.getLogger(__name__)
28
+ from vibe_surf.logger import get_logger
29
+
30
+ logger = get_logger(__name__)
29
31
 
30
32
  router = APIRouter(prefix="/tasks", tags=["tasks"])
31
33
 
@@ -76,16 +78,16 @@ async def submit_task(
76
78
  task_id = uuid7str()
77
79
 
78
80
  # Get MCP server config for saving
79
- from ..shared_state import controller, active_mcp_server
81
+ from ..shared_state import vibesurf_tools, active_mcp_server
80
82
  mcp_server_config = task_request.mcp_server_config
81
- if not mcp_server_config and controller and hasattr(controller, 'mcp_server_config'):
82
- mcp_server_config = controller.mcp_server_config
83
-
83
+ if not mcp_server_config and vibesurf_tools and hasattr(vibesurf_tools, 'mcp_server_config'):
84
+ mcp_server_config = vibesurf_tools.mcp_server_config
85
+
84
86
  # Ensure we have a valid MCP server config (never None)
85
87
  if mcp_server_config is None:
86
88
  mcp_server_config = {"mcpServers": {}}
87
89
  logger.info("Using default empty MCP server configuration")
88
-
90
+
89
91
  # DEBUG: Log the type and content of mcp_server_config
90
92
  logger.info(f"mcp_server_config type: {type(mcp_server_config)}, value: {mcp_server_config}")
91
93
 
@@ -140,16 +142,17 @@ async def _ensure_llm_initialized(llm_profile):
140
142
  """Ensure LLM is initialized with the specified profile"""
141
143
  from ..utils.llm_factory import create_llm_from_profile
142
144
  from ..shared_state import vibesurf_agent
143
-
145
+
144
146
  if not vibesurf_agent:
145
147
  raise HTTPException(status_code=503, detail="VibeSurf agent not initialized")
146
-
148
+
147
149
  # Always create new LLM instance to ensure we're using the right profile
148
150
  new_llm = create_llm_from_profile(llm_profile)
149
-
150
- # Update vibesurf agent's LLM
151
- vibesurf_agent.llm = new_llm
152
- logger.info(f"LLM updated for profile: {llm_profile['profile_name']}")
151
+
152
+ # Update vibesurf agent's LLM and register with token cost service
153
+ if vibesurf_agent and vibesurf_agent.token_cost_service:
154
+ vibesurf_agent.llm = vibesurf_agent.token_cost_service.register_llm(new_llm)
155
+ logger.info(f"LLM updated and registered for token tracking for profile: {llm_profile['profile_name']}")
153
156
 
154
157
 
155
158
  @router.post("/pause")
@@ -260,6 +263,36 @@ async def stop_task(control_request: TaskControlRequest):
260
263
  raise HTTPException(status_code=500, detail=f"Failed to stop task: {str(e)}")
261
264
 
262
265
 
266
+ @router.post("/add-new-task")
267
+ async def add_new_task(control_request: TaskControlRequest):
268
+ """Add a new task or follow-up instruction during execution"""
269
+ from ..shared_state import vibesurf_agent
270
+
271
+ if not vibesurf_agent:
272
+ raise HTTPException(status_code=503, detail="VibeSurf agent not initialized")
273
+
274
+ if not is_task_running():
275
+ raise HTTPException(status_code=400, detail="No active task to add new instruction to")
276
+
277
+ try:
278
+ # Use the reason field as the new task content
279
+ new_task = control_request.reason or "No additional task provided"
280
+
281
+ # Add the new task to the running agent
282
+ await vibesurf_agent.add_new_task(new_task)
283
+
284
+ return {
285
+ "success": True,
286
+ "message": "New task added successfully",
287
+ "operation": "add_new_task",
288
+ "new_task": new_task
289
+ }
290
+
291
+ except Exception as e:
292
+ logger.error(f"Failed to add new task: {e}")
293
+ raise HTTPException(status_code=500, detail=f"Failed to add new task: {str(e)}")
294
+
295
+
263
296
  @router.get("/detailed-status")
264
297
  async def get_detailed_task_status():
265
298
  """Get detailed task execution status with vibesurf information"""