Files
odysseus/static/js/storage.js

126 lines
3.0 KiB
JavaScript
Raw Normal View History

2026-05-31 23:58:26 +09:00
// static/js/storage.js
// Centralized localStorage access with key constants and JSON parse safety
// ── Key constants ──
export const KEYS = {
THEME: 'odysseus-theme',
TOGGLES: 'odysseus-toggles',
SIDEBAR_COLLAPSED: 'sidebar-collapsed',
SIDEBAR_WIDTH: 'sidebar-width',
SIDEBAR_SIDE: 'sidebar-side',
CURRENT_SESSION: 'currentSessionId',
COMPARE_SAVE: 'compare-save-results',
COMPARE_CHAT: 'compare-continue-chat',
COMPARE_BLIND: 'compare-blind',
COMPARE_RANDOM: 'compare-randomize',
MODELS_EXPANDED: 'odysseus-model-expanded',
MODEL_ENDPOINTS: 'odysseus-model-endpoints',
MODEL_SELECTED: 'odysseus-selected-model',
SORT_ORDER: 'odysseus-sessions-sort',
CHAT_SEARCH_SCOPE: 'odysseus-search-scope',
INCOGNITO: 'odysseus-incognito',
RAG_ACTIVE: 'odysseus-rag-active',
MCP_ACTIVE: 'odysseus-mcp-active',
SECTION_ORDER: 'sidebar-section-order',
ADMIN_LAST_TAB: 'admin-last-tab',
feat: Add workspace: confine agent tools to a folder (#1103) * feat: Add workspace: confine agent tools to a folder Pick a server folder as the agent's workspace so its file/shell tools work there and don't touch files outside it. File tools are hard-confined; bash/ python run with cwd set to the folder. Includes a slash command: `/workspace` (alias `/ws`) — show / `set <path>` / `clear` / `pick` (open the directory browser). - routes/workspace_routes.py: GET /api/workspace/browse (admin-only). - src/tool_execution.py: hard path confinement for read_file/write_file; bash/python cwd. Threaded route → stream_agent_loop → execute_tool_block. - src/agent_loop.py: workspace note prepended to the system prompt. - static/: overflow menu item, input-bar pill, directory-browser modal, and the /workspace slash command. - tests/test_workspace_confine.py. * Wire workspace confinement into tools that landed after this PR edit_file (#1239) and grep/glob/ls (#1670) merged after workspace-confine was written, so they bypassed the workspace boundary. Thread the workspace through: - edit_file: _do_edit_file resolves via _resolve_tool_path_in_workspace - grep/glob/ls: _resolve_search_root confines to the workspace (root + paths) - bash/python/bg cwd: workspace or _AGENT_WORKDIR (keep the #2586 data-dir default when no workspace is set) Tests cover edit_file + grep/ls confinement (inside ok, outside rejected). * Workspace picker: editable path bar + modal style cohesion + cross-platform hardening - Make the current-folder strip an editable address bar: type/paste a full path and press Enter to navigate (also reaches other Windows drives and hidden dirs the up-only browser cannot). - Reuse shared modal CSS: drop bespoke .workspace-modal-content/.workspace-btn* in favour of base .modal-content/.modal-body and the .confirm-btn button family; separators/hover use var(--border). Net -31 CSS lines. - Fix the path field overflowing the modal right edge (flex stretch + margin vs an overflow:auto scrollbar-feedback loop): full-bleed, no h-margin. - Cross-platform confinement: normcase the workspace commonpath check so containment holds on case-insensitive filesystems (Windows/macOS). - Make tests OS-portable: sibling temp dirs instead of /etc, python os.getcwd() instead of pwd. 5 pass.
2026-06-05 00:06:37 +02:00
DENSITY: 'odysseus-density',
feat: Add plan mode to the chat agent (#638) * feat: Add plan mode to the chat agent Adds a plan mode: the agent investigates read-only, proposes a checklist, and waits for approval before changing anything. On approval it runs with full tools and checks items off as it goes. Enforcement reuses the existing disabled_tools gate. Includes a slash command: `/plan [on|off]` (and `/toggle plan`) to flip the plan toggle from the chat input. - src/tool_security.py, src/mcp_manager.py: read-only allowlist (tools + MCP). - src/agent_loop.py, routes/chat_routes.py: union the disabled set, prepend the plan directive, force agent mode. - static/: plan toggle pill, Approve & Run, dockable plan window, task-list checkboxes, and the /plan slash command. - tests/test_plan_mode.py. * Plan mode: persistent re-referenceable plan + agent write-back Three improvements so a long plan survives a weak model and stays in reach: 1. Re-reference the plan (out-of-context fix). On the execution turn the frontend sends the approved checklist back (`approved_plan`); the backend pins it as a top-of-context `## ACTIVE PLAN` system note (kept by the context trimmer), so the agent can always re-read the plan instead of losing the thread on a long run. New `build_active_plan_note()` (unit-tested). 2. Re-open / dock the plan anytime. The plan checklist is stored per-session (localStorage). When a plan exists, the plan-mode button opens a small menu ("Show plan" / "Plan mode: On/Off") that re-opens the side-dockable plan window — so it can stay docked while the agent works. The window live-refreshes as the plan changes. 3. Agent write-back: new `update_plan` tool. The agent calls it to tick steps `- [x]` after finishing them, or to revise steps when the user asks. Marker tool (no I/O) → `plan_update` SSE event → the stored plan + docked window update live. The ACTIVE PLAN note instructs the agent to use it. Backend: src/agent_loop.py (param + pin + note builder + emit + prompt blurb), src/tool_execution.py (update_plan handler), routes/chat_routes.py (parse `approved_plan`, relay `plan_update`), registration in tool_schemas / agent_tools / tool_index (always-available, not admin-gated). Frontend: static/js/chat.js (plan store, send `approved_plan`, handle `plan_update`, capture restated checklists), static/app.js (plan-button menu), static/js/planWindow.js (`isPlanWindowOpen`), static/js/storage.js (PLAN key). Tests: tests/test_plan_mode.py (plan-note), tests/test_update_plan_tool.py. * Plan mode: drop bash/python, rely on read-only discovery tools Shell can mutate (write files, hit the network) and can't be constrained to read-only at the tool layer, so plan mode no longer relies on a prompt to keep it well-behaved — bash/python are removed from the read-only allowlist and added to the fail-closed block set. Discovery is covered by the dedicated read-only tools (read_file, grep, glob, ls) instead. Rewrites the plan-mode directive to state shell is disabled and lists the available read-only tools positively. Addresses review feedback on #638. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Comment: note _MCP_READONLY_VERBS are prefixes not whole words Clarifies that entries like "summar" are intentional stems matched via startswith (covers summarise/summarize/summary), not typos. Addresses review feedback on #638. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Plan mode: clarify why gating inverts the allowlist into a denylist Rename _PLAN_MODE_FALLBACK_BLOCK -> _PLAN_MODE_KNOWN_MUTATORS and rewrite the comments. The tool gate is a denylist (disabled_tools); plan mode's policy is an allowlist, so it returns the inverse (all known tool names minus the allowlist). The static mutator set is a backstop for the schema-derived name list, which misses XML-only tools and can fail to import. Addresses review feedback on #638. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Plan mode: stop hardcoding the read-only tool list in the directive The model is already shown its available (read-only) tools by _assemble_prompt, which removes every disabled tool. Enumerating them again in the directive only duplicated that list and would drift as tools change. Point at the tools listed below instead. Addresses review feedback on #638.
2026-06-05 16:32:25 +02:00
WORKSPACE: 'odysseus-workspace',
PLAN: 'odysseus-plan'
2026-05-31 23:58:26 +09:00
};
/**
* Safely get and parse a JSON value from localStorage.
* Returns fallback on any error.
*/
export function getJSON(key, fallback) {
try {
const raw = localStorage.getItem(key);
if (raw === null) return fallback !== undefined ? fallback : null;
return JSON.parse(raw);
} catch (e) {
console.warn('[Storage] Failed to parse key "' + key + '":', e.message);
return fallback !== undefined ? fallback : null;
}
}
/**
* Set a JSON-serialized value in localStorage.
*/
export function setJSON(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.warn('[Storage] Failed to set key "' + key + '":', e.message);
}
}
/**
* Get a raw string value from localStorage.
*/
export function get(key, fallback) {
try {
const val = localStorage.getItem(key);
return val !== null ? val : (fallback !== undefined ? fallback : null);
} catch (e) {
return fallback !== undefined ? fallback : null;
}
}
/**
* Set a raw string value in localStorage.
*/
export function set(key, value) {
try {
localStorage.setItem(key, value);
} catch (e) {
console.warn('[Storage] Failed to set key "' + key + '":', e.message);
}
}
/**
* Remove a key from localStorage.
*/
export function remove(key) {
try {
localStorage.removeItem(key);
} catch (e) {
// Ignore removal errors
}
}
// ── Toggle state helpers ──
export function loadToggleState() {
return getJSON(KEYS.TOGGLES, {});
}
export function saveToggleState(state) {
setJSON(KEYS.TOGGLES, state);
}
export function getToggle(name, fallback) {
const state = loadToggleState();
return state[name] !== undefined ? state[name] : (fallback !== undefined ? fallback : false);
}
export function setToggle(name, value) {
const state = loadToggleState();
state[name] = value;
saveToggleState(state);
}
const Storage = {
KEYS,
getJSON,
setJSON,
get,
set,
remove,
loadToggleState,
saveToggleState,
getToggle,
setToggle
};
export default Storage;