Compare commits
2 commits
ea201ab4b0
...
185667954c
| Author | SHA1 | Date | |
|---|---|---|---|
| 185667954c | |||
| 23fe1769b7 |
4 changed files with 151 additions and 56 deletions
|
|
@ -71,8 +71,8 @@ services:
|
||||||
- PUID=${PUID:-1000}
|
- PUID=${PUID:-1000}
|
||||||
- PGID=${PGID:-1000}
|
- PGID=${PGID:-1000}
|
||||||
depends_on:
|
depends_on:
|
||||||
searxng:
|
# searxng:
|
||||||
condition: service_healthy
|
# condition: service_healthy
|
||||||
chromadb:
|
chromadb:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
@ -87,55 +87,55 @@ services:
|
||||||
- ANONYMIZED_TELEMETRY=FALSE
|
- ANONYMIZED_TELEMETRY=FALSE
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
searxng:
|
# searxng:
|
||||||
# Pinned, not :latest — odysseus waits on searxng's healthcheck
|
# Pinned, not :latest — odysseus waits on searxng's healthcheck
|
||||||
# (depends_on: condition: service_healthy), so a broken upstream `latest`
|
# (depends_on: condition: service_healthy), so a broken upstream `latest`
|
||||||
# tag blocks the whole app from starting. 2026.6.2 crashes on boot with
|
# tag blocks the whole app from starting. 2026.6.2 crashes on boot with
|
||||||
# `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414).
|
# `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414).
|
||||||
# Bump this deliberately after verifying a newer tag boots clean.
|
# Bump this deliberately after verifying a newer tag boots clean.
|
||||||
image: docker.io/searxng/searxng:2026.5.31-7159b8aed
|
# image: docker.io/searxng/searxng:2026.5.31-7159b8aed
|
||||||
entrypoint:
|
# entrypoint:
|
||||||
- /bin/sh
|
# - /bin/sh
|
||||||
- -c
|
# - -c
|
||||||
- |
|
# - |
|
||||||
set -eu
|
# set -eu
|
||||||
if [ ! -s /etc/searxng/settings.yml ] || grep -q 'odysseus-local-searxng-json-2026-05-30\|__SEARXNG_SECRET__' /etc/searxng/settings.yml; then
|
# 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:-}"
|
# secret="$${SEARXNG_SECRET:-}"
|
||||||
if [ -z "$$secret" ]; then
|
# if [ -z "$$secret" ]; then
|
||||||
secret="$$(python -c 'import secrets; print(secrets.token_urlsafe(48))')"
|
# secret="$$(python -c 'import secrets; print(secrets.token_urlsafe(48))')"
|
||||||
fi
|
# fi
|
||||||
sed "s|__SEARXNG_SECRET__|$$secret|g" /tmp/searxng-settings.yml.template > /etc/searxng/settings.yml
|
# sed "s|__SEARXNG_SECRET__|$$secret|g" /tmp/searxng-settings.yml.template > /etc/searxng/settings.yml
|
||||||
fi
|
# fi
|
||||||
exec /usr/local/searxng/entrypoint.sh
|
# exec /usr/local/searxng/entrypoint.sh
|
||||||
ports:
|
# ports:
|
||||||
- "127.0.0.1:8080:8080"
|
# - "127.0.0.1:8080:8080"
|
||||||
volumes:
|
# volumes:
|
||||||
- searxng-data:/etc/searxng
|
# - searxng-data:/etc/searxng
|
||||||
- ./config/searxng/settings.yml:/tmp/searxng-settings.yml.template:ro,z
|
# - ./config/searxng/settings.yml:/tmp/searxng-settings.yml.template:ro,z
|
||||||
environment:
|
# environment:
|
||||||
- SEARXNG_BASE_URL=http://localhost:8080/
|
# - SEARXNG_BASE_URL=http://localhost:8080/
|
||||||
- SEARXNG_SECRET=${SEARXNG_SECRET:-}
|
# restart: unless-stopped
|
||||||
# The official searxng image runs as the non-root `searxng` user, but its
|
# # 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
|
# # 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
|
# # su-exec, and (with our wrapper above) write settings.yml into the named
|
||||||
# volume. Without these capabilities the wrapper aborts at the redirection
|
# # volume. Without these capabilities the wrapper aborts at the redirection
|
||||||
# with EACCES and the container fails its healthcheck with permission
|
# # with EACCES and the container fails its healthcheck with permission
|
||||||
# errors during setup. Mirrors the cap set recommended by the upstream
|
# # errors during setup. Mirrors the cap set recommended by the upstream
|
||||||
# searxng-docker compose file. See issue #721.
|
# # searxng-docker compose file. See issue #721.
|
||||||
cap_drop:
|
# cap_drop:
|
||||||
- ALL
|
# - ALL
|
||||||
cap_add:
|
# cap_add:
|
||||||
- CHOWN
|
# - CHOWN
|
||||||
- SETGID
|
# - SETGID
|
||||||
- SETUID
|
# - SETUID
|
||||||
- DAC_OVERRIDE
|
# - DAC_OVERRIDE
|
||||||
healthcheck:
|
# healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/', timeout=5).read(1)\""]
|
# test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/', timeout=5).read(1)\""]
|
||||||
interval: 5s
|
# interval: 5s
|
||||||
timeout: 6s
|
# timeout: 6s
|
||||||
retries: 20
|
# retries: 20
|
||||||
start_period: 10s
|
# start_period: 10s
|
||||||
restart: unless-stopped
|
# restart: unless-stopped
|
||||||
|
|
||||||
ntfy:
|
ntfy:
|
||||||
image: docker.io/binwiederhier/ntfy
|
image: docker.io/binwiederhier/ntfy
|
||||||
|
|
@ -149,6 +149,6 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
searxng-data:
|
# searxng-data:
|
||||||
chromadb-data:
|
chromadb-data:
|
||||||
ntfy-cache:
|
ntfy-cache:
|
||||||
|
|
|
||||||
48
docs/improvement-tasks.md
Normal file
48
docs/improvement-tasks.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Ulysses Improvement Tasks
|
||||||
|
|
||||||
|
Last updated: 2026-07-07
|
||||||
|
|
||||||
|
## Priority: HIGH
|
||||||
|
|
||||||
|
- [ ] **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.
|
||||||
|
|
||||||
|
- [ ] **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.
|
||||||
|
|
||||||
|
## Priority: MEDIUM
|
||||||
|
|
||||||
|
- [ ] **Bolster Library/Documents CRUD via MCP or native tools**
|
||||||
|
Current document tools are solid but disjointed from agent-mode workflow. The agent has filesystem access (`read_file`, `write_file`, `edit_file`, `glob`, `ls`, `grep`, `bash`) but these tools operate on disk files, not Ulysses library documents. Need to bridge the gap: the agent should be able to list, read, grep, and edit library documents as naturally as files. Consider:
|
||||||
|
- A unified `read_document` tool distinct from `manage_documents` (which is an admin-style multi-action tool)
|
||||||
|
- A `grep_documents` or `search_documents` tool for semantic/fulltext search
|
||||||
|
- Auto-inject active document content into agent context when relevant
|
||||||
|
|
||||||
|
- [ ] **First-party document support in agent mode**
|
||||||
|
Documents feel "disjointed from the agent." The agent creates documents fine but can't easily discover or reference existing ones contextually. Ideas:
|
||||||
|
- "Agent workspace" awareness — automatically surface relevant documents based on chat context
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## Priority: LOW
|
||||||
|
|
||||||
|
- [ ] **Document library: batch operations**
|
||||||
|
Multi-select, bulk delete, bulk export. Currently only single-document operations.
|
||||||
|
|
||||||
|
- [ ] **Document version diff view**
|
||||||
|
The version history panel shows versions but doesn't offer a side-by-side diff view between versions.
|
||||||
|
|
||||||
|
- [ ] **Auto-language detection on document open**
|
||||||
|
The editor auto-detects language on typing but not on initial document open from the library. Opening a Python file saved as "markdown" should suggest the language switch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Completed
|
||||||
|
|
||||||
|
<!-- Move completed items here with completion date -->
|
||||||
|
|
@ -29,6 +29,14 @@ const MIN_H = 200;
|
||||||
// within EDGE px of the window border (close buttons, sliders, inputs, links).
|
// within EDGE px of the window border (close buttons, sliders, inputs, links).
|
||||||
const INTERACTIVE = 'button, input, select, textarea, a, [contenteditable=""], [contenteditable="true"]';
|
const INTERACTIVE = 'button, input, select, textarea, a, [contenteditable=""], [contenteditable="true"]';
|
||||||
|
|
||||||
|
// `zoom` (ui-scale-*) makes window.innerHeight misleading — CSS renders
|
||||||
|
// content larger than the viewport, so a window saved at 700px pre-zoom
|
||||||
|
// renders at 875px after 1.25x. Divide constraints by the zoom factor.
|
||||||
|
function _uiZoom() {
|
||||||
|
const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||||
|
return z > 0 ? z : 1;
|
||||||
|
}
|
||||||
|
|
||||||
export function makeWindowResizable(content, options = {}) {
|
export function makeWindowResizable(content, options = {}) {
|
||||||
if (!content) return;
|
if (!content) return;
|
||||||
const modal = options.modal || null;
|
const modal = options.modal || null;
|
||||||
|
|
@ -128,10 +136,13 @@ export function makeWindowResizable(content, options = {}) {
|
||||||
if (width < minW) { if (active.l) left = startRect.left + (startRect.width - minW); width = minW; }
|
if (width < minW) { if (active.l) left = startRect.left + (startRect.width - minW); width = minW; }
|
||||||
if (height < minH) { if (active.t) top = startRect.top + (startRect.height - minH); height = minH; }
|
if (height < minH) { if (active.t) top = startRect.top + (startRect.height - minH); height = minH; }
|
||||||
// Keep the window on-screen and never larger than the viewport.
|
// Keep the window on-screen and never larger than the viewport.
|
||||||
|
// Zoom-aware: divide by ui-zoom so clamped dimensions fit the visible area.
|
||||||
|
const z = _uiZoom();
|
||||||
|
const vhZ = vh / z, vwZ = vw / z;
|
||||||
if (active.l && left < 0) { width += left; left = 0; }
|
if (active.l && left < 0) { width += left; left = 0; }
|
||||||
if (active.t && top < 0) { height += top; top = 0; }
|
if (active.t && top < 0) { height += top; top = 0; }
|
||||||
if (left + width > vw) width = Math.max(minW, vw - left);
|
if (left + width > vwZ) width = Math.max(minW, vwZ - left);
|
||||||
if (top + height > vh) height = Math.max(minH, vh - top);
|
if (top + height > vhZ) height = Math.max(minH, vhZ - top);
|
||||||
content.style.left = left + 'px';
|
content.style.left = left + 'px';
|
||||||
content.style.top = top + 'px';
|
content.style.top = top + 'px';
|
||||||
content.style.width = width + 'px';
|
content.style.width = width + 'px';
|
||||||
|
|
@ -220,8 +231,10 @@ export function makeWindowResizable(content, options = {}) {
|
||||||
try {
|
try {
|
||||||
const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
|
const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
|
||||||
if (saved && saved.w && saved.h) {
|
if (saved && saved.w && saved.h) {
|
||||||
const w = Math.max(minW, Math.min(saved.w, window.innerWidth));
|
const z = _uiZoom();
|
||||||
const h = Math.max(minH, Math.min(saved.h, window.innerHeight));
|
const vwZ = window.innerWidth / z, vhZ = window.innerHeight / z;
|
||||||
|
const w = Math.max(minW, Math.min(saved.w, vwZ));
|
||||||
|
const h = Math.max(minH, Math.min(saved.h, vhZ));
|
||||||
content.style.width = w + 'px';
|
content.style.width = w + 'px';
|
||||||
content.style.height = h + 'px';
|
content.style.height = h + 'px';
|
||||||
content.style.maxWidth = 'none';
|
content.style.maxWidth = 'none';
|
||||||
|
|
|
||||||
|
|
@ -177,11 +177,43 @@ html {
|
||||||
(a catch-22 — you can't reach the control to turn the size back down). Divide
|
(a catch-22 — you can't reach the control to turn the size back down). Divide
|
||||||
each max-height by the same factor to keep the original on-screen footprint.
|
each max-height by the same factor to keep the original on-screen footprint.
|
||||||
Desktop only — the mobile `!important` full-sheet rules win on small screens
|
Desktop only — the mobile `!important` full-sheet rules win on small screens
|
||||||
and stay top-anchored, so their headers are already visible. */
|
and stay top-anchored, so their headers are already visible.
|
||||||
:root.ui-scale-125 .modal-content { max-height: calc(85dvh / 1.25); }
|
|
||||||
:root.ui-scale-125 .cal-modal-content { max-height: calc(88dvh / 1.25); }
|
Comprehensive coverage: every modal-content variant, standalone panes,
|
||||||
:root.ui-scale-125 .settings-modal-content { max-height: calc(85dvh / 1.25); }
|
and user-resized windows that carry inline height styles. Users who
|
||||||
:root.ui-scale-125 #theme-popup { max-height: min(calc(85dvh / 1.25), 480px); }
|
changed zoom/font settings saw headers and close buttons pushed
|
||||||
|
off-screen because only a subset of windows had the divisor. */
|
||||||
|
:root.ui-scale-125 .modal-content,
|
||||||
|
:root.ui-scale-125 .cal-modal-content,
|
||||||
|
:root.ui-scale-125 .settings-modal-content,
|
||||||
|
:root.ui-scale-125 .memory-modal-content,
|
||||||
|
:root.ui-scale-125 .tasks-modal-content,
|
||||||
|
:root.ui-scale-125 .preset-modal-content,
|
||||||
|
:root.ui-scale-125 .doclib-modal-content,
|
||||||
|
:root.ui-scale-125 .gallery-modal-content,
|
||||||
|
:root.ui-scale-125 .notes-pane,
|
||||||
|
:root.ui-scale-125 #research-overlay .modal-content,
|
||||||
|
:root.ui-scale-125 #research-overlay,
|
||||||
|
:root.ui-scale-125 #compare-model-overlay .modal-content,
|
||||||
|
:root.ui-scale-125 #compare-model-overlay,
|
||||||
|
:root.ui-scale-125 #email-lib-modal .modal-content,
|
||||||
|
:root.ui-scale-125 #doclib-modal .modal-content {
|
||||||
|
max-height: calc(85dvh / 1.25);
|
||||||
|
}
|
||||||
|
/* Any modal-content carrying an inline height style (user-resized) gets
|
||||||
|
viewport-clamped — this catches windows whose saved localStorage size
|
||||||
|
was set before a zoom change and would otherwise render off-screen. */
|
||||||
|
:root.ui-scale-125 .modal-content[style*="height"],
|
||||||
|
:root.ui-scale-125 .cal-modal-content[style*="height"],
|
||||||
|
:root.ui-scale-125 .settings-modal-content[style*="height"],
|
||||||
|
:root.ui-scale-125 .memory-modal-content[style*="height"],
|
||||||
|
:root.ui-scale-125 .tasks-modal-content[style*="height"],
|
||||||
|
:root.ui-scale-125 .preset-modal-content[style*="height"],
|
||||||
|
:root.ui-scale-125 .doclib-modal-content[style*="height"],
|
||||||
|
:root.ui-scale-125 .gallery-modal-content[style*="height"],
|
||||||
|
:root.ui-scale-125 .notes-pane[style*="height"] {
|
||||||
|
max-height: calc(100dvh / 1.25 - 16px) !important;
|
||||||
|
}
|
||||||
/* Cookbook is the one modal that set its height inline (94vh), which beat the
|
/* Cookbook is the one modal that set its height inline (94vh), which beat the
|
||||||
.modal-content compensation above and overflowed the viewport at 1.25x
|
.modal-content compensation above and overflowed the viewport at 1.25x
|
||||||
(header + close button pushed off-screen). Own its height here so the same
|
(header + close button pushed off-screen). Own its height here so the same
|
||||||
|
|
@ -194,6 +226,8 @@ html {
|
||||||
1.25x compensation applies. */
|
1.25x compensation applies. */
|
||||||
.pdf-export-overlay .modal-content { max-height: 86vh; }
|
.pdf-export-overlay .modal-content { max-height: 86vh; }
|
||||||
:root.ui-scale-125 .pdf-export-overlay .modal-content { max-height: calc(86dvh / 1.25); }
|
:root.ui-scale-125 .pdf-export-overlay .modal-content { max-height: calc(86dvh / 1.25); }
|
||||||
|
/* Theme popup: cap at a reasonable fixed height regardless of zoom */
|
||||||
|
:root.ui-scale-125 #theme-popup { max-height: min(calc(85dvh / 1.25), 480px); }
|
||||||
|
|
||||||
/* ── Background Patterns ── */
|
/* ── Background Patterns ── */
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue