fix: zoom-aware getBoundingClientRect positioning for all dropdowns, menus, pickers, and overlays
app.js: overflow plus-menu positionMenu() — divide r.left/r.top by zoom cookbookRunning.js: hamburger dropdown — use window.innerWidth - (rect.right/z) cookbookServe.js: 5 fixes — toolbar dropdown, launch-more, split-arrow, GPU popup, saved-menu skills.js: kebab tool menu — divide rect bounds by zoom colorPicker.js: picker position() — divide rect coords by zoom emojiPicker.js: picker open — divide rect coords by zoom theme.js: zone highlights — divide all rect values by zoom
This commit is contained in:
parent
5f13f7b3c9
commit
c93c14ece9
13 changed files with 331 additions and 83 deletions
221
.env.save
Executable file
221
.env.save
Executable file
|
|
@ -0,0 +1,221 @@
|
|||
# Odysseus UI — Environment Configuration
|
||||
# Copy this file to .env and fill in your values.
|
||||
|
||||
COMPOSE_FILE=docker-compose.yml:docker/gpu.nvidia.yml
|
||||
|
||||
# ============================================================
|
||||
# LLM Configuration
|
||||
# ============================================================
|
||||
|
||||
# Primary LLM host (default: localhost)
|
||||
LLM_HOST=localhost
|
||||
|
||||
# Additional LLM hosts, comma-separated (for model discovery)
|
||||
# Use hostnames/IPs only; Odysseus scans common serve ports, including Ollama's 11434.
|
||||
# LLM_HOSTS=llm-host.local,backup-llm.local
|
||||
|
||||
# Optional Ollama base URL. In Docker, host Ollama is usually reachable here
|
||||
# when started with OLLAMA_HOST=0.0.0.0:11434.
|
||||
# OLLAMA_BASE_URL=http://host.docker.internal:11434/v1
|
||||
|
||||
# Optional LM Studio URL. In Docker, host LM Studio is reachable here
|
||||
# when LM Studio is set to serve on all interfaces (0.0.0.0).
|
||||
# LM_STUDIO_URL=http://host.docker.internal:1234
|
||||
|
||||
# OpenAI API key (only needed if using OpenAI models).
|
||||
# Do not commit real keys. Keep this commented until needed.
|
||||
# OPENAI_API_KEY=your_openai_api_key_here
|
||||
|
||||
# Research service LLM endpoint
|
||||
# RESEARCH_LLM_ENDPOINT=http://localhost:8000/v1/chat/completions
|
||||
|
||||
# Extra CA bundle for LLM providers whose TLS chain isn't in the default
|
||||
# trust store. Layered ON TOP of the system / certifi bundle — verification
|
||||
# stays on for every host, the trust set just gets larger. Useful for:
|
||||
# - GigaChat / Sber (Russian Trusted Root CA): without this the endpoint
|
||||
# shows offline with CERTIFICATE_VERIFY_FAILED — self-signed certificate
|
||||
# in certificate chain.
|
||||
# - On-premise / corporate LLM gateways with an internal CA.
|
||||
# Point at a PEM file containing the missing root(s).
|
||||
# LLM_CA_BUNDLE=/etc/odysseus/ca/extra-roots.pem
|
||||
|
||||
# ============================================================
|
||||
# Search & Web
|
||||
# ============================================================
|
||||
|
||||
# SearXNG instance URL (self-hosted, for web search).
|
||||
# Docker Compose overrides this to http://searxng:8080 for in-network access.
|
||||
SEARXNG_INSTANCE=http://localhost:8080
|
||||
|
||||
# Optional SearXNG cookie/CSRF secret. If blank, Docker generates one on first boot
|
||||
# and stores it in the searxng-data volume.
|
||||
# SEARXNG_SECRET=
|
||||
|
||||
# ============================================================
|
||||
# Database
|
||||
# ============================================================
|
||||
|
||||
# SQLite database path (default: sqlite:///./data/app.db)
|
||||
# DATABASE_URL=sqlite:///./data/app.db
|
||||
|
||||
# ============================================================
|
||||
# Data directory
|
||||
# ============================================================
|
||||
# Move everything that lives under data/ - settings, sessions, database, auth,
|
||||
# cache, uploads, etc. - to another path:
|
||||
# ODYSSEUS_DATA_DIR=C:\path\to\dir
|
||||
|
||||
# ============================================================
|
||||
# Auth & Security
|
||||
# ============================================================
|
||||
|
||||
# Enable authentication (default: true)
|
||||
# AUTH_ENABLED=true
|
||||
|
||||
# Host bind address and port for the Odysseus web UI in Docker Compose.
|
||||
# Keep APP_BIND on loopback unless you intentionally want LAN/reverse-proxy access.
|
||||
APP_BIND=0.0.0.0
|
||||
# Change this if another local service already uses 7000 (macOS AirPlay often does).
|
||||
# APP_PORT=7000
|
||||
|
||||
# Development-only auth bypass for loopback requests.
|
||||
# Keep false for Docker, LAN, reverse proxy, and any shared deployment.
|
||||
# LOCALHOST_BYPASS=false
|
||||
|
||||
# Mark session cookies Secure. Set true when Odysseus is served through HTTPS
|
||||
# by a trusted reverse proxy or private access gateway.
|
||||
# SECURE_COOKIES=true
|
||||
|
||||
# Optional: pre-seed the first admin password during setup.
|
||||
# Do not commit a real password.
|
||||
# ODYSSEUS_ADMIN_PASSWORD=change_me_before_first_boot
|
||||
|
||||
# CORS allowed origins (default: localhost-only; restrict to your public origin in production)
|
||||
# ALLOWED_ORIGINS=http://localhost:7000,http://localhost:8000
|
||||
|
||||
# ============================================================
|
||||
# ChromaDB (vector store)
|
||||
# ============================================================
|
||||
|
||||
# ChromaDB service host.
|
||||
# Manual host run: localhost:8100 when using `docker run -p 8100:8000 chromadb/chroma`.
|
||||
# Docker Compose overrides these to chromadb:8000 for in-network access.
|
||||
# CHROMADB_HOST=localhost
|
||||
# CHROMADB_PORT=8100
|
||||
|
||||
# Docker Compose host-port bind addresses for bundled services.
|
||||
# Defaults are loopback-only for safety. To expose ntfy only on Tailscale,
|
||||
# set NTFY_BIND to your host's Tailscale IP and update NTFY_BASE_URL.
|
||||
# CHROMADB_BIND=127.0.0.1
|
||||
# NTFY_BIND=127.0.0.1
|
||||
# NTFY_BASE_URL=http://localhost:8091
|
||||
# Example:
|
||||
# NTFY_BIND=100.x.y.z
|
||||
# NTFY_BASE_URL=http://100.x.y.z:8091
|
||||
|
||||
# ============================================================
|
||||
# RAG / Embeddings
|
||||
# ============================================================
|
||||
|
||||
# Embedding API endpoint (OpenAI-compatible /v1/embeddings)
|
||||
# Default: http://{LLM_HOST}:11434/v1/embeddings (ollama)
|
||||
# EMBEDDING_URL=http://localhost:11434/v1/embeddings
|
||||
|
||||
# Embedding API key (if there's one)
|
||||
# EMBEDDING_API_KEY=embedding_api_key_here
|
||||
|
||||
# Embedding model name (must be available at the endpoint above)
|
||||
# EMBEDDING_MODEL=all-minilm:l6-v2
|
||||
|
||||
# Local fallback embedding model (used when no HTTP embedding API is available)
|
||||
# Uses fastembed (ONNX) — downloads model on first run (~50MB)
|
||||
# FASTEMBED_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
||||
# FASTEMBED_CACHE_PATH= # defaults to ~/.cache/fastembed
|
||||
|
||||
# ============================================================
|
||||
# Misc
|
||||
# ============================================================
|
||||
|
||||
# Cleanup interval in hours (default: 24)
|
||||
# CLEANUP_INTERVAL_HOURS=24
|
||||
|
||||
# In-process email pollers (default: on). Set to 0 if you're driving
|
||||
# polling from cron / systemd via `scripts/odysseus-mail poll-scheduled`
|
||||
# and `scripts/odysseus-mail poll-summary`, otherwise both schedulers
|
||||
# race on the same SQLite.
|
||||
# ODYSSEUS_INPROCESS_POLLERS=1
|
||||
|
||||
# In-process scheduled-task runner (default: on). Set to 0 to let an
|
||||
# external driver fire scheduled tasks. Calendar reminders are
|
||||
# frontend-driven (polling /api/notes from the browser) so no gate is
|
||||
# needed there.
|
||||
# ODYSSEUS_INPROCESS_TASKS=1
|
||||
|
||||
# Host used by the built-in "run_script" scheduled-task action.
|
||||
# Empty/local/localhost runs scripts on the app host. Set to an SSH host alias
|
||||
# if you intentionally want scheduled scripts to run remotely.
|
||||
# ODYSSEUS_SCRIPT_HOST=localhost
|
||||
|
||||
# Chat / agent attachment size cap in bytes (default: 10 MB).
|
||||
# Raise this for local installs that need larger PDFs or text documents.
|
||||
# Example: 52428800 = 50 MB.
|
||||
# ODYSSEUS_CHAT_UPLOAD_MAX_BYTES=10485760
|
||||
|
||||
# Other per-feature upload size caps in bytes. All are validated and optional;
|
||||
# defaults shown. An invalid value (non-integer or < 1) fails fast at startup.
|
||||
# ODYSSEUS_GALLERY_UPLOAD_MAX_BYTES=104857600 # gallery image upload (100 MB)
|
||||
# ODYSSEUS_GALLERY_TRANSFORM_UPLOAD_MAX_BYTES=26214400 # gallery transform input (25 MB)
|
||||
# ODYSSEUS_MEMORY_IMPORT_MAX_BYTES=10485760 # memory import file (10 MB)
|
||||
# ODYSSEUS_PERSONAL_UPLOAD_MAX_BYTES=26214400 # personal document upload (25 MB)
|
||||
# ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES=26214400 # email compose attachment (25 MB)
|
||||
# ODYSSEUS_STT_MAX_AUDIO_BYTES=26214400 # speech-to-text audio (25 MB)
|
||||
# ODYSSEUS_ICS_MAX_BYTES=10485760 # calendar .ics import (10 MB)
|
||||
|
||||
# ============================================================
|
||||
# Host Docker access (explicit opt-in)
|
||||
# ============================================================
|
||||
# Default Docker Compose does not mount /var/run/docker.sock. Existing
|
||||
# Ollama, vLLM, and other OpenAI-compatible endpoints remain usable without it.
|
||||
#
|
||||
# Enable this only for intentional Cookbook/local Docker-daemon management.
|
||||
# Raw socket access is high-trust and can grant broad control over the host
|
||||
# Docker daemon. Set DOCKER_GID to the host docker group's numeric GID.
|
||||
# Put these values in .env, or export them before running docker compose.
|
||||
# COMPOSE_FILE=docker-compose.yml:docker/host-docker.yml
|
||||
# DOCKER_GID=963
|
||||
# docker/host-docker.yml sets this inside the container. Keep it paired
|
||||
# with the socket overlay; setting it alone is not sufficient.
|
||||
# ODYSSEUS_ENABLE_HOST_DOCKER=true
|
||||
#
|
||||
# Host Docker access can be combined with one GPU overlay:
|
||||
# COMPOSE_FILE=docker-compose.yml:docker/gpu.nvidia.yml:docker/host-docker.yml
|
||||
# COMPOSE_FILE=docker-compose.yml:docker/gpu.amd.yml:docker/host-docker.yml
|
||||
|
||||
# ============================================================
|
||||
# GPU support (Docker Compose)
|
||||
# ============================================================
|
||||
# Pass the host GPU into the odysseus container. Default (unset) = CPU.
|
||||
# COMPOSE_FILE is a native `docker compose` feature: a colon-separated
|
||||
# list of files merged left-to-right. Pick ONE GPU line below, or leave
|
||||
# all commented for CPU.
|
||||
#
|
||||
# NVIDIA (requires nvidia-container-toolkit + `nvidia-ctk runtime
|
||||
# configure --runtime=docker` on the host):
|
||||
# COMPOSE_FILE=docker-compose.yml:docker/gpu.nvidia.yml
|
||||
# COMPOSE_FILE=docker-compose.yml;docker/gpu.nvidia.yml #(Windows)
|
||||
#
|
||||
# AMD ROCm (requires ROCm drivers on the host and the GID of the render group):
|
||||
# COMPOSE_FILE=docker-compose.yml:docker/gpu.amd.yml
|
||||
# Find the render GID with: getent group render | cut -d: -f3
|
||||
# RENDER_GID=989
|
||||
#
|
||||
# These overlays only expose the GPU devices. The slim Odysseus image
|
||||
# still needs CUDA/ROCm userspace via Cookbook -> Dependencies (vLLM,
|
||||
# llama-cpp-python, etc.) before models can actually serve on GPU.
|
||||
|
||||
# ============================================================
|
||||
# Storage Paths (Docker Compose)
|
||||
# ============================================================
|
||||
|
||||
# APP_DATA_DIR=./data
|
||||
# APP_LOGS_DIR=./logs
|
||||
BIN
cuda-keyring_1.1-1_all.deb
Normal file
BIN
cuda-keyring_1.1-1_all.deb
Normal file
Binary file not shown.
|
|
@ -85,8 +85,8 @@ services:
|
|||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
depends_on:
|
||||
searxng:
|
||||
condition: service_healthy
|
||||
# searxng:
|
||||
# condition: service_healthy
|
||||
chromadb:
|
||||
condition: service_started
|
||||
restart: unless-stopped
|
||||
|
|
@ -109,55 +109,55 @@ services:
|
|||
- ANONYMIZED_TELEMETRY=FALSE
|
||||
restart: unless-stopped
|
||||
|
||||
searxng:
|
||||
# Pinned, not :latest — odysseus waits on searxng's healthcheck
|
||||
# (depends_on: condition: service_healthy), so a broken upstream `latest`
|
||||
# tag blocks the whole app from starting. 2026.6.2 crashes on boot with
|
||||
# `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414).
|
||||
# Bump this deliberately after verifying a newer tag boots clean.
|
||||
image: docker.io/searxng/searxng:2026.5.31-7159b8aed
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
if [ ! -s /etc/searxng/settings.yml ] || grep -q 'odysseus-local-searxng-json-2026-05-30\|__SEARXNG_SECRET__' /etc/searxng/settings.yml; then
|
||||
secret="$${SEARXNG_SECRET:-}"
|
||||
if [ -z "$$secret" ]; then
|
||||
secret="$$(python -c 'import secrets; print(secrets.token_urlsafe(48))')"
|
||||
fi
|
||||
sed "s|__SEARXNG_SECRET__|$$secret|g" /tmp/searxng-settings.yml.template > /etc/searxng/settings.yml
|
||||
fi
|
||||
exec /usr/local/searxng/entrypoint.sh
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
volumes:
|
||||
- searxng-data:/etc/searxng
|
||||
- ./config/searxng/settings.yml:/tmp/searxng-settings.yml.template:ro,z
|
||||
environment:
|
||||
- SEARXNG_BASE_URL=http://localhost:8080/
|
||||
- SEARXNG_SECRET=${SEARXNG_SECRET:-}
|
||||
# The official searxng image runs as the non-root `searxng` user, but its
|
||||
# entrypoint still needs to chown /etc/searxng on first boot, drop privs via
|
||||
# su-exec, and (with our wrapper above) write settings.yml into the named
|
||||
# volume. Without these capabilities the wrapper aborts at the redirection
|
||||
# with EACCES and the container fails its healthcheck with permission
|
||||
# errors during setup. Mirrors the cap set recommended by the upstream
|
||||
# searxng-docker compose file. See issue #721.
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- CHOWN
|
||||
- SETGID
|
||||
- SETUID
|
||||
- DAC_OVERRIDE
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/', timeout=5).read(1)\""]
|
||||
interval: 5s
|
||||
timeout: 6s
|
||||
retries: 20
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
# searxng:
|
||||
# Pinned, not :latest — odysseus waits on searxng's healthcheck
|
||||
# (depends_on: condition: service_healthy), so a broken upstream `latest`
|
||||
# tag blocks the whole app from starting. 2026.6.2 crashes on boot with
|
||||
# `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414).
|
||||
# Bump this deliberately after verifying a newer tag boots clean.
|
||||
# image: docker.io/searxng/searxng:2026.5.31-7159b8aed
|
||||
# entrypoint:
|
||||
# - /bin/sh
|
||||
# - -c
|
||||
# - |
|
||||
# set -eu
|
||||
# if [ ! -s /etc/searxng/settings.yml ] || grep -q 'odysseus-local-searxng-json-2026-05-30\\|__SEARXNG_SECRET__' /etc/searxng/settings.yml; then
|
||||
# secret=\"$${SEARXNG_SECRET:-}\"
|
||||
# if [ -z \"$$secret\" ]; then
|
||||
# secret=\"$$(python -c 'import secrets; print(secrets.token_urlsafe(48))')\"
|
||||
# fi
|
||||
# sed \"s|__SEARXNG_SECRET__|$$secret|g\" /tmp/searxng-settings.yml.template > /etc/searxng/settings.yml
|
||||
# fi
|
||||
# exec /usr/local/searxng/entrypoint.sh
|
||||
# ports:
|
||||
# - \"127.0.0.1:8080:8080\"
|
||||
# volumes:
|
||||
# - searxng-data:/etc/searxng
|
||||
# - ./config/searxng/settings.yml:/tmp/searxng-settings.yml.template:ro,z
|
||||
# environment:
|
||||
# - SEARXNG_BASE_URL=http://localhost:8080/
|
||||
# - SEARXNG_SECRET=${SEARXNG_SECRET:-}
|
||||
# The official searxng image runs as the non-root `searxng` user, but its
|
||||
# entrypoint still needs to chown /etc/searxng on first boot, drop privs via
|
||||
# su-exec, and (with our wrapper above) write settings.yml into the named
|
||||
# volume. Without these capabilities the wrapper aborts at the redirection
|
||||
# with EACCES and the container fails its healthcheck with permission
|
||||
# errors during setup. Mirrors the cap set recommended by the upstream
|
||||
# searxng-docker compose file. See issue #721.
|
||||
# cap_drop:
|
||||
# - ALL
|
||||
# cap_add:
|
||||
# - CHOWN
|
||||
# - SETGID
|
||||
# - SETUID
|
||||
# - DAC_OVERRIDE
|
||||
# healthcheck:
|
||||
# test: [\"CMD-SHELL\", \"python -c \\\"import urllib.request; urllib.request.urlopen('http://localhost:8080/', timeout=5).read(1)\\\"\"]\"
|
||||
# interval: 5s
|
||||
# timeout: 6s
|
||||
# retries: 20
|
||||
# start_period: 10s
|
||||
# restart: unless-stopped
|
||||
|
||||
ntfy:
|
||||
image: docker.io/binwiederhier/ntfy
|
||||
|
|
@ -171,6 +171,6 @@ services:
|
|||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
searxng-data:
|
||||
# searxng-data:
|
||||
chromadb-data:
|
||||
ntfy-cache:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ services:
|
|||
# Ollama at http://host.docker.internal:11434.
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
- LD_LIBRARY_PATH=/app/.local/bin
|
||||
- LLM_HOST=${LLM_HOST:-localhost}
|
||||
- LLM_HOSTS=${LLM_HOSTS:-}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||
|
|
@ -76,6 +77,9 @@ services:
|
|||
chromadb:
|
||||
condition: service_started
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
- searxng-net
|
||||
|
||||
chromadb:
|
||||
image: docker.io/chromadb/chroma:latest
|
||||
|
|
@ -152,3 +156,8 @@ volumes:
|
|||
# searxng-data:
|
||||
chromadb-data:
|
||||
ntfy-cache:
|
||||
|
||||
networks:
|
||||
searxng-net:
|
||||
external: true
|
||||
name: searxng_default
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@ Last updated: 2026-07-07
|
|||
|
||||
## Priority: HIGH
|
||||
|
||||
- [ ] **AUTO_START_LOCAL: persist & auto-restore last running model on restart**
|
||||
New `AUTO_START_LOCAL` env var (bool, default false). On graceful shutdown/restart, persist the currently running model + all launch parameters (engine, quantization, context length, GPU layers, etc.) to a file in `/app/data/`. On startup, if the file exists and AUTO_START_LOCAL is true, automate the full launch pipeline: resolve the saved preset, spin up the engine, and register the endpoint — skipping the manual cookbook → Launch → pick model → verify params → launch → wait → refresh cycle the user currently endures on every restart.
|
||||
|
||||
- [ ] **Floating windows overflow viewport after zoom/font changes — unreachable controls**
|
||||
`:root.ui-scale-125 { zoom: 1.25 }` renders all content 25% larger, but `window.innerHeight` doesn't change with `zoom` (CSS rendering only, not DOM). The `windowResize.js` saved-size restore at line 224 clamps to `window.innerHeight`, which after zoom makes windows 1.25× the visible viewport — pushing headers, close buttons, and top resize handles off-screen. The user literally can't close or move them. Fix: (a) divide restored height by zoom factor in the restore path; (b) add a general safety net — any `.modal-content` on open should clamp to `calc(100dvh / var(--ui-zoom, 1) - 40px)`. Also `windowResize.js` line 134 `if (top + height > vh)` needs zoom-aware `vh`. Current zoom compensation rules (line 181-196) only cover `.modal-content`, `.cal-modal-content`, `.settings-modal-content`, `#theme-popup`, `#cookbook-modal`, and `.pdf-export-overlay` — many windows lack the divisor.
|
||||
|
||||
- [ ] **Fix manage_documents tool schema — missing "read" action**
|
||||
The `manage_documents` function-calling schema in `tool_schemas.py` has `"enum": ["list", "delete", "tidy"]` but the handler in `document_tools.py` supports `"read", "view", "open", "get"`. The model can't read documents through function calling. Fix: add read/view/open/get to enum, add `document_id`/`offset`/`limit` params, update description to mention reading.
|
||||
- [x] **Fix manage_documents tool schema — missing "read" action**
|
||||
The `manage_documents` function-calling schema in `tool_schemas.py` had `"enum": ["list", "delete", "tidy"]` but the handler in `document_tools.py` supports `"read", "view", "open", "get"`. The model couldn't read documents through function calling. Fixed: added read/view/open/get to enum, added `document_id`/`offset`/`limit` params with read-specific descriptions, updated description with "DOCUMENT PANEL (not disk)" prefix. Also added disambiguation prefix to `create_document` and `edit_document` schemas.
|
||||
|
||||
- [ ] **Add "Send to Chat" button to document editor**
|
||||
Button left of the Save button in `#doc-actions-footer`. Sends a document reference (title + doc_id + snippet) to the active chat, similar to how Cursor injects file references. Must NOT paste full content — just a reference the agent can resolve. Backend needs an endpoint or the existing `open_panel: documents` UI action may be enough. Requires: `POST /api/document/<id>/reference` or use existing `ui_control` with a new action. Frontend: insert button in `document.js` `_buildPaneHTML()` at line ~4598, wire JS handler.
|
||||
- [x] **Add "Send to Chat" button to document editor**
|
||||
Button left of the Save button in `#doc-actions-footer`. Sends a document reference (title + doc_id + snippet) to the active chat, similar to how Cursor injects file references. Must NOT paste full content — just a reference the agent can resolve. Done: "Chat" button injects `[Title](#document-<id>) — <120-char snippet>` into the #message textarea.
|
||||
|
||||
## Priority: MEDIUM
|
||||
|
||||
|
|
@ -27,11 +30,14 @@ Last updated: 2026-07-07
|
|||
- Document references in chat that are clickable and auto-expand
|
||||
- The "Send to Chat" button (HIGH priority) is step 1; step 2 is making the agent proactively aware of library contents
|
||||
|
||||
- [ ] **Improve `create_document` schema description**
|
||||
Current description is verbose and overlaps with `edit_file`/`write_file` disambiguation. Models sometimes use `create_document` for disk files. Add a clear leading signal: "DOCUMENT PANEL (not disk)" prefix like the existing "FILESYSTEM TOOL (not memory)" pattern.
|
||||
- [x] **Improve `create_document` schema description**
|
||||
Current description is verbose and overlaps with `edit_file`/`write_file` disambiguation. Models sometimes use `create_document` for disk files. Fixed: added "DOCUMENT PANEL (not disk)" prefix, explicit "For files on disk use write_file" at end. Also applied to `edit_document`.
|
||||
|
||||
## Priority: LOW
|
||||
|
||||
- [ ] **Cookbook sliders: active highlight misaligned with zoom**
|
||||
The rounded highlight on CPU/GPU/RAM etc. toggle sliders doesn't scale with `ui-scale-125`. The active indicator (e.g. "GPU") is partially outside the highlight pill. Low priority — cosmetic, obvious which option is selected.
|
||||
|
||||
- [ ] **Document library: batch operations**
|
||||
Multi-select, bulk delete, bulk export. Currently only single-document operations.
|
||||
|
||||
|
|
|
|||
|
|
@ -2696,6 +2696,9 @@ def setup_cookbook_routes() -> APIRouter:
|
|||
elif disk_env.get("servers") and not inc_env.get("servers"):
|
||||
inc_env["servers"] = disk_env["servers"]
|
||||
logger.warning("cookbook state POST: incoming env.servers empty; preserved on-disk servers (anti-wipe guard)")
|
||||
if disk_env.get("hfToken") and not inc_env.get("hfToken"):
|
||||
inc_env["hfToken"] = disk_env["hfToken"]
|
||||
logger.warning("cookbook state POST: incoming env.hfToken empty; preserved on-disk hfToken (anti-wipe guard)")
|
||||
|
||||
disk_tasks = on_disk.get("tasks") or [] if isinstance(on_disk, dict) else []
|
||||
incoming_tasks = data.get("tasks") if isinstance(data.get("tasks"), list) else []
|
||||
|
|
|
|||
|
|
@ -1909,19 +1909,20 @@ function initializeEventListeners() {
|
|||
// genuinely taller than the room above the button.
|
||||
function positionMenu() {
|
||||
const r = plusBtn.getBoundingClientRect();
|
||||
menu.style.left = r.left + 'px';
|
||||
const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
menu.style.left = (r.left / z) + 'px';
|
||||
menu.style.right = 'auto';
|
||||
menu.style.bottom = 'auto';
|
||||
menu.style.maxHeight = ''; // reset so we can measure the natural height
|
||||
menu.style.overflowY = '';
|
||||
const avail = r.top - 16; // room above the chevron
|
||||
const avail = (r.top / z) - 16; // room above the chevron (viewport-relative)
|
||||
const natural = menu.scrollHeight;
|
||||
const h = Math.min(natural, avail);
|
||||
if (natural > avail) { // only cap + scroll when it doesn't fit
|
||||
menu.style.maxHeight = avail + 'px';
|
||||
menu.style.overflowY = 'auto';
|
||||
}
|
||||
menu.style.top = (r.top - 8 - h) + 'px';
|
||||
menu.style.top = ((r.top / z) - 8 - h) + 'px';
|
||||
}
|
||||
// Tapping the chevron must NOT steal focus from the message box, or the
|
||||
// mobile keyboard collapses. preventDefault on pointerdown keeps the
|
||||
|
|
|
|||
|
|
@ -293,10 +293,11 @@ function commitCurrent() {
|
|||
function position(p, anchor) {
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const pRect = p.getBoundingClientRect();
|
||||
let left = rect.left;
|
||||
let top = rect.bottom + 6;
|
||||
const zc = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
let left = rect.left / zc;
|
||||
let top = (rect.bottom / zc) + 6;
|
||||
if (left + pRect.width > window.innerWidth - 8) left = window.innerWidth - pRect.width - 8;
|
||||
if (top + pRect.height > window.innerHeight - 8) top = rect.top - pRect.height - 6;
|
||||
if (top + pRect.height > window.innerHeight - 8) top = (rect.top / zc) - pRect.height - 6;
|
||||
if (left < 8) left = 8;
|
||||
if (top < 8) top = 8;
|
||||
p.style.left = left + 'px';
|
||||
|
|
|
|||
|
|
@ -2530,8 +2530,8 @@ export function _renderRunningTab() {
|
|||
const rect = menuBtn.getBoundingClientRect();
|
||||
const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
dropdown.style.position = 'fixed';
|
||||
dropdown.style.top = rect.bottom + 2 + 'px';
|
||||
dropdown.style.right = ((window.innerWidth / z) - rect.right) + 'px';
|
||||
dropdown.style.top = (rect.bottom / z) + 2 + 'px';
|
||||
dropdown.style.right = (window.innerWidth - (rect.right / z)) + 'px';
|
||||
document.body.appendChild(dropdown);
|
||||
// Clamp into the *visible* area. On mobile (esp. Firefox) window.innerHeight
|
||||
// includes the strip hidden under the dynamic toolbar, so a menu that "fits"
|
||||
|
|
|
|||
|
|
@ -1078,7 +1078,7 @@ function _rerenderCachedModels() {
|
|||
dropdown.appendChild(cancelDiv);
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
dropdown.style.cssText = `position:fixed;z-index:${topPortalZ()};visibility:hidden;top:0;right:${(window.innerWidth / z) - rect.right}px;background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,0.3);font-size:12px;`;
|
||||
dropdown.style.cssText = `position:fixed;z-index:${topPortalZ()};visibility:hidden;top:0;right:${window.innerWidth - (rect.right / z)}px;background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,0.3);font-size:12px;`;
|
||||
document.body.appendChild(dropdown);
|
||||
// Clamp into the VISIBLE area (visualViewport, not innerHeight — they differ
|
||||
// on mobile under the dynamic toolbar). Flip above the button if there's no
|
||||
|
|
@ -2328,10 +2328,11 @@ function _rerenderCachedModels() {
|
|||
// Clamp into the viewport using the menu's real size (both axes); flip
|
||||
// above the toggle if there isn't room below. Right-align to the anchor.
|
||||
const w = dropdown.offsetWidth, h = dropdown.offsetHeight;
|
||||
let left = Math.min(rect.right - w, window.innerWidth - w - 8);
|
||||
const za = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
let left = Math.min((rect.right / za) - w, window.innerWidth - w - 8);
|
||||
left = Math.max(8, left);
|
||||
let top = rect.bottom + 6;
|
||||
if (top + h > window.innerHeight - 8) top = Math.max(8, rect.top - 6 - h);
|
||||
let top = (rect.bottom / za) + 6;
|
||||
if (top + h > window.innerHeight - 8) top = Math.max(8, (rect.top / za) - 6 - h);
|
||||
dropdown.style.left = `${left}px`;
|
||||
dropdown.style.top = `${top}px`;
|
||||
dropdown.style.visibility = '';
|
||||
|
|
@ -2433,8 +2434,9 @@ function _rerenderCachedModels() {
|
|||
menu.appendChild(mk('Clear Server', 'cookbook-dropdown-danger', () => _clearBtn?.click()));
|
||||
menu.appendChild(mk('Cancel', 'dropdown-cancel-mobile', () => {}));
|
||||
const r = _launchMoreBtn.getBoundingClientRect();
|
||||
const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
menu.style.position = 'fixed';
|
||||
menu.style.right = (window.innerWidth - r.right) + 'px';
|
||||
menu.style.right = (window.innerWidth - (r.right / z)) + 'px';
|
||||
document.body.appendChild(menu);
|
||||
{
|
||||
const vv = window.visualViewport;
|
||||
|
|
@ -2479,8 +2481,9 @@ function _rerenderCachedModels() {
|
|||
menu.appendChild(mk('Probe GPUs', '', () => _probeBtn?.click()));
|
||||
menu.appendChild(mk('Cancel', 'dropdown-cancel-mobile', () => {}));
|
||||
const r = _splitArrow.getBoundingClientRect();
|
||||
const z2 = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
menu.style.position = 'fixed';
|
||||
menu.style.right = (window.innerWidth - r.right) + 'px';
|
||||
menu.style.right = (window.innerWidth - (r.right / z2)) + 'px';
|
||||
document.body.appendChild(menu);
|
||||
// Default open BELOW, but if there's no room (esp. on mobile where
|
||||
// the arrow sits near the bottom of the modal) flip ABOVE so the
|
||||
|
|
@ -2587,12 +2590,13 @@ function _rerenderCachedModels() {
|
|||
// popup stays fully visible — GPU buttons near the right edge
|
||||
// of the modal previously anchored the popup mostly off-screen.
|
||||
const r = anchorBtn.getBoundingClientRect();
|
||||
const z3 = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
const vw = window.innerWidth || document.documentElement.clientWidth;
|
||||
const vh = window.innerHeight || document.documentElement.clientHeight;
|
||||
const pw = popup.offsetWidth || 320;
|
||||
const ph = popup.offsetHeight || 200;
|
||||
let left = r.left;
|
||||
let top = r.bottom + 4;
|
||||
let left = r.left / z3;
|
||||
let top = (r.bottom / z3) + 4;
|
||||
// Push left so the popup doesn't overflow the right edge.
|
||||
if (left + pw > vw - 8) left = Math.max(8, vw - pw - 8);
|
||||
// If there isn't room below, render above the button instead.
|
||||
|
|
|
|||
|
|
@ -157,9 +157,10 @@ function togglePicker(anchor, target) {
|
|||
document.body.appendChild(_pickerEl);
|
||||
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const ze = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
_pickerEl.style.position = 'fixed';
|
||||
_pickerEl.style.top = (rect.bottom + 4) + 'px';
|
||||
_pickerEl.style.left = rect.left + 'px';
|
||||
_pickerEl.style.top = ((rect.bottom / ze) + 4) + 'px';
|
||||
_pickerEl.style.left = (rect.left / ze) + 'px';
|
||||
_pickerEl.style.zIndex = String(topPortalZ());
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
|
|
|
|||
|
|
@ -443,13 +443,14 @@ function _openSkillMenu(btn, card, sk, name, isPublished) {
|
|||
// bring-to-front counter climbs past the static value (#4720).
|
||||
menu.style.zIndex = String(topPortalZ());
|
||||
const r = btn.getBoundingClientRect();
|
||||
menu.style.top = (r.bottom + 4) + 'px';
|
||||
menu.style.right = Math.max(6, window.innerWidth - r.right) + 'px';
|
||||
const zs = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
menu.style.top = ((r.bottom / zs) + 4) + 'px';
|
||||
menu.style.right = Math.max(6, window.innerWidth - (r.right / zs)) + 'px';
|
||||
// Keep it on-screen (mobile): flip above the button if it would overflow the
|
||||
// bottom, clamp the left edge, and cap the height as a last resort.
|
||||
const mr = menu.getBoundingClientRect();
|
||||
if (mr.bottom > window.innerHeight - 6) {
|
||||
menu.style.top = Math.max(6, r.top - mr.height - 4) + 'px';
|
||||
menu.style.top = Math.max(6, (r.top / zs) - mr.height - 4) + 'px';
|
||||
}
|
||||
if (mr.left < 6) {
|
||||
menu.style.right = Math.max(6, window.innerWidth - 6 - mr.width) + 'px';
|
||||
|
|
|
|||
|
|
@ -1397,12 +1397,13 @@ function _showThemeZoneHighlight(selector) {
|
|||
if (el.closest && el.closest('#theme-modal')) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.width < 2 || r.height < 2) return;
|
||||
const zt = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'theme-zone-highlight';
|
||||
overlay.style.top = (r.top - 2) + 'px';
|
||||
overlay.style.left = (r.left - 2) + 'px';
|
||||
overlay.style.width = (r.width + 4) + 'px';
|
||||
overlay.style.height = (r.height + 4) + 'px';
|
||||
overlay.style.top = ((r.top / zt) - 2) + 'px';
|
||||
overlay.style.left = ((r.left / zt) - 2) + 'px';
|
||||
overlay.style.width = ((r.width / zt) + 4) + 'px';
|
||||
overlay.style.height = ((r.height / zt) + 4) + 'px';
|
||||
document.body.appendChild(overlay);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue