Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion PROJECT_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ The application is standalone. It is not an Open WebUI fork, wrapper, or plugin.
| FR-23 | Tool catalog | Tools default off. When enabled, active schemas and permission metadata come from the runtime registry. |
| FR-24 | Agent execution | Model-selected tools run through schema validation, per-tool deadlines, an error budget, an eight-step default ceiling, approval boundaries, and a complete partial transcript. Tool results add an honest `termination` classification without changing existing status values. |
| FR-25 | Exact-call approval | An approval-required call is stored in process with canonical arguments, SHA-256 digest, transcript revision, single-use nonce, and a 300-second expiry. Resume accepts only the matching run, call, digest, and unmodified transcript; approval executes those exact arguments without replaying the paused model step, and denial appends an operator-denied tool result before continuing. |
| FR-25 | Responsive access | Chat, History, and Runtime navigation remains usable at mobile widths; motion respects `prefers-reduced-motion`. |
| FR-26 | Responsive access | Chat, History, and Runtime navigation remains usable at mobile widths; motion respects `prefers-reduced-motion`. |

## 5. Context and prompt contract

Expand Down
13 changes: 4 additions & 9 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,6 @@
).expanduser()
FFMPEG_BIN = os.getenv("FFMPEG_BIN") or shutil.which("ffmpeg") or "/opt/homebrew/bin/ffmpeg"

DEFAULT_MODEL_ID = (
"/Users/daverobertson/Desktop/Dave-LLM/models/qwen-vl-7b/"
"Qwen2.5-VL-7B-Instruct-Q4_K_M.gguf"
)

# Simple model catalog for routing decisions
MODEL_CATALOG = {
str(Path("/Users/daverobertson/Desktop/Dave-LLM/models/qwen-vl-7b/Qwen2.5-VL-7B-Instruct-Q4_K_M.gguf").resolve()): {
Expand Down Expand Up @@ -312,19 +307,16 @@ def record_error(event: str, detail: str):
"title": "New Conversation",
"system_prompt": SYSTEM_PROMPT,
"session_override": "",
"preferred_model": DEFAULT_MODEL_ID,
},
"code_review": {
"title": "Code Review Session",
"system_prompt": SYSTEM_PROMPT + "\n\nFocus on code quality, bugs, and optimization.",
"session_override": "Focus on code quality, bugs, and optimization.",
"preferred_model": "./models/llama3.2-3b-instruct-q4_k_m.gguf",
},
"brainstorm": {
"title": "Brainstorm",
"system_prompt": SYSTEM_PROMPT + "\n\nBe exploratory and propose multiple options.",
"session_override": "Be exploratory and propose multiple options.",
"preferred_model": "/Users/daverobertson/Desktop/Dave-LLM/models/qwen-vl-7b/Qwen2.5-VL-7B-Instruct-Q4_K_M.gguf",
},
}

Expand Down Expand Up @@ -3123,7 +3115,10 @@ def create_from_template(req: TemplateConversationRequest, user_id: str = Depend
provisional_conversation,
project_cfg,
)
preferred_model = template.get("preferred_model") or project_cfg.get("preferred_model")
# Templates carry no model preference. The only preferred model is the
# project's own setting, and the actual send still uses the visible
# inventory-backed selection.
preferred_model = project_cfg.get("preferred_model")
cid = f"convo_{uuid.uuid4().hex}"
CONVERSATIONS[cid] = {
"title": template.get("title", DEFAULT_CONVO_TITLE),
Expand Down
33 changes: 12 additions & 21 deletions static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ const state = {
// ---------------------------------------------
const ROUTER_BASE = (typeof window !== "undefined" && window.__API_BASE__)
|| (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8000");
const LOCAL_STORAGE_KEY = "dave_convos";
// Conversation and feedback content live only on the router. These legacy
// browser keys once mirrored them and are purged so no prompt or response
// text remains in persistent browser storage.
const LEGACY_CONTENT_STORAGE_KEYS = ["dave_convos", "dave_feedback"];
const LAST_SESSION_KEY = "dave_last_session";
const API_KEY_SESSION_KEY = "dave_api_key_session";
const THEME_STORAGE_KEY = "dave_theme";
Expand Down Expand Up @@ -190,9 +193,6 @@ function sendFeedback(score, content, modelId) {
complexity: complexityScoreClient(content || "")
})
}).catch(() => {});
const fb = JSON.parse(localStorage.getItem("dave_feedback") || "[]");
fb.push({ score, modelId: fbModel, content: content.slice(0, 200), ts: Date.now() });
localStorage.setItem("dave_feedback", JSON.stringify(fb));
} catch (e) {}
}

Expand Down Expand Up @@ -931,15 +931,10 @@ async function createProjectFlow() {
}
}

function saveAllConversations() {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(state.conversations));
}

function loadAllConversations() {
const saved = localStorage.getItem(LOCAL_STORAGE_KEY);
if (saved) {
state.conversations = JSON.parse(saved);
}
function purgeLegacyContentStorage() {
try {
LEGACY_CONTENT_STORAGE_KEYS.forEach((key) => localStorage.removeItem(key));
} catch (e) {}
}

function toggleDictation() {
Expand Down Expand Up @@ -1127,7 +1122,8 @@ async function downloadModelFromHF() {

/**
* Load all conversations from backend server.
* Replaces localStorage as source of truth when available.
* The router is the only source of truth; a failure leaves the list empty
* and visible as an error instead of restoring a browser copy.
*/
async function loadConversationsFromBackend() {
try {
Expand Down Expand Up @@ -1156,8 +1152,7 @@ async function loadConversationsFromBackend() {

} catch (err) {
console.error("Failed to load conversations from backend:", err);
console.warn("⚠️ Falling back to localStorage");
loadAllConversations();
state.conversations = {};
return false;
}
}
Expand Down Expand Up @@ -1461,7 +1456,6 @@ async function deleteConversation(cid, element) {
try {
await deleteConversationFromBackend(cid);
delete state.conversations[cid];
saveAllConversations();

if (state.sessionId === cid) {
const remaining = Object.keys(state.conversations);
Expand Down Expand Up @@ -1492,7 +1486,6 @@ async function clearConversation(cid) {
await clearConversationOnBackend(cid);
const convo = state.conversations[cid];
convo.messages = [];
saveAllConversations();
renderMessages();
} catch (err) {
console.error("Failed to clear conversation:", err);
Expand Down Expand Up @@ -2672,9 +2665,6 @@ async function renameConversation(cid, element) {
} else {
console.log("ℹ️ New conversation, skipping backend sync (will sync on first message)");
}

saveAllConversations();

} catch (err) {
console.error("❌ Error during finalize:", err);
} finally {
Expand Down Expand Up @@ -3967,6 +3957,7 @@ async function init() {
restoredAnything = restoredAnything || preferredTemplate !== "general";
}

purgeLegacyContentStorage();
await loadConversationsFromBackend();

const lastSessionId = localStorage.getItem(LAST_SESSION_KEY);
Expand Down
16 changes: 16 additions & 0 deletions tests/test_api_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,22 @@ def test_template_body_and_authenticated_markdown_export(router_factory):
assert response.json()["system_prompt"] == expected_prompt
assert router.CONVERSATIONS[conversation_id]["system_prompt"] == expected_prompt
assert router.CONVERSATIONS[conversation_id]["messages"] == []
# Templates carry no source-controlled model path; a General chat has no
# preferred model and a project chat reports only the project's setting.
assert response.json()["preferred_model"] is None
assert all("preferred_model" not in template for template in router.TEMPLATES.values())
project = client.post(
"/projects",
headers=AUTH,
json={"name": "Preferred", "preferred_model": MODEL_ID},
).json()
project_chat = client.post(
"/conversations/from_template",
headers=AUTH,
json={"template_name": "brainstorm", "project_id": project["project_id"]},
)
assert project_chat.status_code == 200
assert project_chat.json()["preferred_model"] == MODEL_ID

assert client.get(f"/conversations/{conversation_id}/export").status_code == 401
exported = client.get(f"/conversations/{conversation_id}/export", headers=AUTH)
Expand Down
10 changes: 10 additions & 0 deletions tests/test_security_and_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,16 @@ def test_displayed_prompt_contract_and_renderer_security():
assert "innerHTML" not in app_source
assert "innerHTML" not in monitoring_source
assert "localStorage.getItem(\"dave_api_key\")" not in app_source + monitoring_source
# The router is the only conversation and feedback store. The renderer must
# not mirror prompt or response content into persistent browser storage or
# restore a browser copy when the router is unreachable.
assert "saveAllConversations" not in app_source
assert "loadAllConversations" not in app_source
assert "localStorage.setItem(LOCAL_STORAGE_KEY" not in app_source
assert 'localStorage.setItem("dave_convos"' not in app_source
assert 'localStorage.setItem("dave_feedback"' not in app_source
assert 'LEGACY_CONTENT_STORAGE_KEYS = ["dave_convos", "dave_feedback"]' in app_source
assert "purgeLegacyContentStorage();" in app_source
assert "DAVE_API_KEY" not in preload_source
assert 'details.requestHeaders["X-API-Key"] = apiKey' in main_source
assert main_source.index("await waitForBackend(child)") < main_source.index("createWindow();")
Expand Down
Loading