Skip to content
6 changes: 4 additions & 2 deletions src/gaia/apps/webui/src/components/FileBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ export function FileBrowser() {
});
if (!hasSupported) {
const firstFile = files[0];
const ext = '.' + firstFile.split('.').pop()?.toLowerCase();
const dotIdx = firstFile.lastIndexOf('.');
const ext = dotIdx > 0 ? firstFile.slice(dotIdx).toLowerCase() : '';
const category = getUnsupportedCategory(ext);
setIndexError({
filename: files.length === 1
Expand Down Expand Up @@ -337,7 +338,8 @@ export function FileBrowser() {
const result = await indexAndAttachFiles(files, entries, currentSessionId, updateSessionInList);

if (result.supported.length === 0) {
const ext = '.' + files[0].split('.').pop()?.toLowerCase();
const dotIdx = files[0].lastIndexOf('.');
const ext = dotIdx > 0 ? files[0].slice(dotIdx).toLowerCase() : '';
const category = getUnsupportedCategory(ext);
setIndexError({
filename: files.length === 1 ? files[0].split(/[\\/]/).pop() || files[0] : `${files.length} files`,
Expand Down
2 changes: 1 addition & 1 deletion src/gaia/apps/webui/src/components/MessageBubble.css
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
word-wrap: break-word;
overflow-wrap: break-word;
padding-left: 32px;
overflow: hidden;
overflow-y: hidden;
min-width: 0;
}

Expand Down
2 changes: 0 additions & 2 deletions src/gaia/llm/lemonade_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,6 @@ def launch_server(self, log_level="info", background="none", ctx_size=None):
stderr=self._log_file,
text=True,
bufsize=1,
shell=True,
)
except Exception:
self._log_file.close()
Expand All @@ -723,7 +722,6 @@ def launch_server(self, log_level="info", background="none", ctx_size=None):
stderr=subprocess.PIPE,
text=True,
bufsize=1,
shell=True,
)

# Print stdout and stderr in real-time only for foreground mode
Expand Down
5 changes: 4 additions & 1 deletion src/gaia/mcp/mcp_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,10 @@ def handle_jsonrpc(self, data):
400,
{
"jsonrpc": "2.0",
"error": {"code": -32600, "message": "Invalid Request: expected JSON object"},
"error": {
"code": -32600,
"message": "Invalid Request: expected JSON object",
},
"id": None,
},
)
Expand Down
1 change: 1 addition & 0 deletions src/gaia/ui/document_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ async def _check_documents(self) -> None:
filepath,
doc_id,
)
self._db.update_document_status(doc_id, "missing")
continue

current_mtime, current_size = file_info
Expand Down
11 changes: 9 additions & 2 deletions src/gaia/ui/routers/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@

logger = logging.getLogger(__name__)

# Hold references to background tasks to prevent GC
_background_tasks: set[asyncio.Task] = set()

router = APIRouter(tags=["system"])

# Default model required for GAIA Chat agent
Expand Down Expand Up @@ -458,9 +461,11 @@ async def load_model_endpoint(body: LoadModelRequest):

ctx_size = body.ctx_size if body.ctx_size is not None else _MIN_CONTEXT_SIZE
payload = {"model_name": model_name, "ctx_size": ctx_size}
asyncio.create_task(
task = asyncio.create_task(
_lemonade_post("load", payload, timeout=300.0, log_context=f"Load {model_name}")
)
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
return {"status": "loading", "model": model_name, "ctx_size": ctx_size}


Expand All @@ -485,9 +490,11 @@ async def download_model_endpoint(body: DownloadModelRequest):
payload: dict = {"model_name": model_name}
if body.force:
payload["force"] = True
asyncio.create_task(
task = asyncio.create_task(
_lemonade_post(
"pull", payload, timeout=7200.0, log_context=f"Download {model_name}"
)
)
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
return {"status": "downloading", "model": model_name}
19 changes: 11 additions & 8 deletions src/gaia/ui/sse_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def __init__(self):
self._in_thinking = False # True while inside a <think>...</think> block
self._json_filtered = False # True after a JSON block was suppressed; used to eat trailing } artifacts
# Tool confirmation state (blocking until frontend responds)
self._confirm_lock = threading.Lock()
self._confirm_event: Optional[threading.Event] = None
self._confirm_result: bool = False
self._confirm_id: Optional[str] = None
Expand Down Expand Up @@ -691,8 +692,9 @@ def confirm_tool_execution(
``False`` otherwise.
"""
confirm_id = str(uuid.uuid4())
self._confirm_event = threading.Event()
self._confirm_result = False
with self._confirm_lock:
self._confirm_event = threading.Event()
self._confirm_result = False
self._confirm_id = confirm_id

self._emit(
Expand Down Expand Up @@ -739,12 +741,13 @@ def resolve_tool_confirmation(self, approved: bool) -> bool:
Called by the ``POST /api/chat/confirm-tool`` HTTP endpoint. Returns
``False`` if there is no pending confirmation request.
"""
if self._confirm_event is None:
# No pending confirmation — initialise state anyway so callers can
# inspect _confirm_result and _confirm_event after the call.
self._confirm_event = threading.Event()
self._confirm_result = approved
self._confirm_event.set()
with self._confirm_lock:
if self._confirm_event is None:
# No pending confirmation — initialise state anyway so callers can
# inspect _confirm_result and _confirm_event after the call.
self._confirm_event = threading.Event()
self._confirm_result = approved
self._confirm_event.set()
return True

def signal_done(self):
Expand Down
5 changes: 5 additions & 0 deletions src/gaia/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

def kill_process_on_port(port):
"""Kill any process running on the specified port."""
# Validate port is an integer to prevent shell injection
try:
port = int(port)
except (ValueError, TypeError):
raise ValueError(f"Invalid port number: {port!r}")
try:
if sys.platform.startswith("win"):
# Windows: use netstat + taskkill
Expand Down
Loading