mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 01:19:34 +00:00
* feat: unified thread model for web gateway Every piece of activity (user chat, routine run, heartbeat alert, external channel message) now lives in its own thread, properly isolated, with meaningful titles and visual distinction. Key changes: - Add `channel` field to ConversationSummary and ThreadInfo so the gateway can distinguish thread origins (gateway, telegram, routine, heartbeat). - Add `list_conversations_all_channels` to Database trait (both postgres and libsql) so chat_threads_handler shows cross-channel threads. - Routine runs get a persistent conversation per routine via `get_or_create_routine_conversation`; notifications carry thread_id. - Heartbeat gets a persistent conversation via `get_or_create_heartbeat_conversation`; HeartbeatRunner accepts an optional Database store and binds notifications to the thread. - Fix broadcast() in web gateway to propagate response.thread_id instead of hardcoding empty string. - Fix isCurrentThread(null) returning true (the core notification leak bug) — now returns false so events without a thread_id don't leak into the active thread. - Rewrite frontend thread sidebar: meaningful titles with channel-specific fallbacks, relative timestamps instead of turn counts, channel badges for non-gateway threads, unread notification dots, read-only indicator for external channel threads. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR review — TOCTOU races, stale comment, debounce, broadcast warning - Fix TOCTOU race in get_or_create_routine_conversation (postgres): use INSERT ON CONFLICT on new uq_conv_routine unique index + SELECT-back. - Fix TOCTOU race in get_or_create_heartbeat_conversation (postgres): use INSERT ON CONFLICT on new uq_conv_heartbeat unique index + SELECT-back. - Fix TOCTOU race in get_or_create_routine_conversation (libsql): use BEGIN IMMEDIATE transaction to serialize concurrent writers. - Fix TOCTOU race in get_or_create_heartbeat_conversation (libsql): use BEGIN IMMEDIATE transaction to serialize concurrent writers. - Add V11 migration with partial unique indexes for postgres. - Add matching unique indexes to libsql schema. - Update stale comment on isCurrentThread (said "always shown" but logic now returns false for missing thread_id). - Debounce loadThreads() on off-thread SSE events to prevent request storms. - Log warning in broadcast() when thread_id is None (clients will drop it). Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: sort in-memory thread fallback by updated_at descending The in-memory thread list fallback (when no DB is available) used HashMap::values() which has no guaranteed ordering. Sort by updated_at descending to match the SQL query ordering. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: retry libsql connect() on transient "unable to open database file" The cron ticker's background task occasionally fails with "unable to open database file" when creating a new SQLite connection concurrently with the main thread. Add retry with exponential backoff (50ms, 100ms, 200ms) to handle transient VFS/locking issues in libsql's local mode. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: use ON CONFLICT with index expressions instead of named constraints PostgreSQL ON CONFLICT ON CONSTRAINT requires a named table constraint, but V11 migration creates unique indexes. Switch to the expression form (ON CONFLICT (columns) WHERE condition) which works with unique indexes. Also fix dead code in threadTitle() where thread.title was already checked on the previous line. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * style: fix rustfmt chain collapse in heartbeat.rs [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: skip broadcast when thread_id is None instead of sending empty Clients drop SSE events with empty thread_id anyway, so avoid the unnecessary network traffic by returning early. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * test: add libsql routine/heartbeat conversation idempotency tests Add tests proving get_or_create_routine_conversation returns the same conversation ID across multiple invocations with the same routine_id. Add debug logging to routine engine to track conversation resolution. Co-Authored-By: Claude Opus 4.6 <[email protected]> * feat: show "New chat" title for empty threads - threadTitle() returns "New chat" when turn_count is 0 - Assistant thread label updates dynamically from API data - Default HTML label changed from "Assistant" to "New chat" - New threads naturally sort to top via last_activity DESC [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: thread sorting, routine isolation, and UI polish - Fix libsql timestamp format mismatch causing broken thread sort order. SQLite defaults used `datetime('now')` (space-separated) while Rust code used RFC3339 (T-separated), breaking string-based ORDER BY. All INSERTs now use RFC3339, and queries use `datetime()` to normalize comparison. - Route manual routine triggers through RoutineEngine.fire_manual() instead of injecting as regular chat messages, so routines always run in their dedicated conversation thread. - Add RoutineEngineSlot to GatewayState for gateway<->engine communication. - Derive routine thread titles from conversation metadata (routine_name) instead of showing truncated UUID hashes. - Make chat_new_thread_handler persist to DB synchronously so loadThreads() sees newly created threads immediately. - Fix enableChatInput() no-op and wrong element ID in disableChatInputReadOnly(). - Fix handlers/chat.rs stale gateway-only query (use list_conversations_all_channels). - Sort in-memory threads by DateTime before converting to RFC3339 strings. - Trigger debouncedLoadThreads() on thinking/status SSE events for non-current threads so routine/heartbeat threads appear in sidebar promptly. - Remove "Threads" text from sidebar header. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: routine history display, orphaned tool_results, duplicate system messages Three independent fixes with regression tests: 1. Routine conversations now display in the web UI. build_turns_from_db_messages() handles standalone assistant messages (no preceding user message) by creating turns with empty user_input. Frontend skips empty user bubbles. 2. Worker select_tools and execute_plan paths now push an assistant_with_tool_calls message before tool execution, preventing sanitize_tool_messages from rewriting tool_results as orphaned user messages. 3. Reasoning::plan() and respond_with_tools() merge system messages from context into a single system prompt instead of creating [system, system, ...] sequences that strict LLM providers (Qwen) reject. Also: sidebar padding/spacing improvements, wider thread panel (240px). Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR #607 review — RwLock held across await, missing ownership check, heartbeat config - Clone Arc<RoutineEngine> out of RwLock before .await in trigger handler - Add user_id ownership check to fire_manual() with NotAuthorized error - Wire heartbeat notify_user/notify_channel from config to AgentHeartbeatConfig Co-Authored-By: Claude Opus 4.6 <[email protected]> * chore: gitignore trace_*.json files and remove stale traces Co-Authored-By: Claude Opus 4.6 <[email protected]> * chore: remove trace JSON files from repo Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: proper HTTP status codes for routine errors, read-only input guard, respond thread_id - Map RoutineError::NotFound → 404, NotAuthorized → 403, Disabled → 409 - Guard enableChatInput() against re-enabling on read-only threads - Skip respond() when thread_id is None (matches broadcast() behavior) [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
319 lines
13 KiB
HTML
319 lines
13 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||
<title>IronClaw</title>
|
||
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="/style.css">
|
||
<script
|
||
src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"
|
||
integrity="sha384-pN9zSKOnTZwXRtYZAu0PBPEgR2B7DOC1aeLxQ33oJ0oy5iN1we6gm57xldM2irDG"
|
||
crossorigin="anonymous"
|
||
></script>
|
||
</head>
|
||
<body>
|
||
<!-- Auth Screen -->
|
||
<div id="auth-screen">
|
||
<div class="auth-card-login">
|
||
<div class="auth-brand">
|
||
<h1>IronClaw</h1>
|
||
<p class="auth-tagline">Secure AI Assistant</p>
|
||
</div>
|
||
<div class="auth-form">
|
||
<label for="token-input">Gateway Token</label>
|
||
<input type="password" id="token-input" placeholder="Paste your auth token" autofocus>
|
||
<button onclick="authenticate()">Connect</button>
|
||
</div>
|
||
<div id="auth-error"></div>
|
||
<p class="auth-hint">Enter the GATEWAY_AUTH_TOKEN from your .env configuration.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Restart Confirmation Modal -->
|
||
<div id="restart-confirm-modal" class="restart-modal" style="display: none;">
|
||
<div class="restart-modal-overlay" onclick="cancelRestart()"></div>
|
||
<div class="restart-modal-content">
|
||
<div class="restart-modal-header">
|
||
<h2>Restart IronClaw Instance</h2>
|
||
<button class="restart-modal-close" onclick="cancelRestart()" title="Close">×</button>
|
||
</div>
|
||
<div class="restart-modal-body">
|
||
<p class="restart-modal-description">
|
||
Are you sure you want to restart the IronClaw instance? This will gracefully restart the process.
|
||
</p>
|
||
<div class="restart-modal-warning">
|
||
<span class="restart-modal-warning-icon">⚠️</span>
|
||
<p>Any in-progress jobs may be interrupted. The restart will complete within a few seconds.</p>
|
||
</div>
|
||
</div>
|
||
<div class="restart-modal-footer">
|
||
<button class="restart-modal-btn cancel" onclick="cancelRestart()">Cancel</button>
|
||
<button class="restart-modal-btn confirm" onclick="confirmRestart()">Confirm Restart</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Restart Progress Modal -->
|
||
<div id="restart-loader" class="restart-loader" style="display: none;">
|
||
<div class="restart-loader-overlay"></div>
|
||
<div class="restart-loader-content">
|
||
<div class="restart-spinner"></div>
|
||
<div class="restart-loader-text">
|
||
<p class="restart-title">Restarting IronClaw</p>
|
||
<p class="restart-subtitle">Please wait while the process restarts...</p>
|
||
</div>
|
||
<div class="restart-progress-bar">
|
||
<div class="restart-progress-fill"></div>
|
||
</div>
|
||
<p class="restart-modal-info">
|
||
Check the Logs tab for details after the restart completes.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main App (hidden until authenticated) -->
|
||
<div id="app">
|
||
<!-- Tab Bar -->
|
||
<div class="tab-bar">
|
||
<button class="active" data-tab="chat">Chat</button>
|
||
<button data-tab="memory">Memory</button>
|
||
<button data-tab="jobs">Jobs</button>
|
||
<button data-tab="routines">Routines</button>
|
||
<button data-tab="extensions">Extensions</button>
|
||
<button data-tab="skills">Skills</button>
|
||
<div class="spacer"></div>
|
||
<button class="status-logs-btn" data-tab="logs" title="Logs">Logs</button>
|
||
<div class="tee-shield" id="tee-shield" style="display:none" title="Running in a Trusted Execution Environment">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||
</svg>
|
||
<span id="tee-shield-label">TEE Verified</span>
|
||
<div class="tee-popover" id="tee-popover"></div>
|
||
</div>
|
||
<div class="status" id="gateway-status-trigger">
|
||
<div class="dot" id="sse-dot"></div>
|
||
<span id="sse-status">Connected</span>
|
||
<div class="gateway-popover" id="gateway-popover"></div>
|
||
</div>
|
||
<button class="restart-btn" id="restart-btn" onclick="triggerRestart()" title="Gracefully restart the process">
|
||
<svg id="restart-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M23 4v6h-6"></path>
|
||
<path d="M1 20v-6h6"></path>
|
||
<path d="M3.51 9a9 9 0 0114.85-3.36M20.49 15a9 9 0 01-14.85 3.36"></path>
|
||
</svg>
|
||
<span>Restart</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Chat Tab -->
|
||
<div class="tab-panel active" id="tab-chat">
|
||
<div class="thread-sidebar" id="thread-sidebar">
|
||
<div class="thread-sidebar-header">
|
||
<button class="thread-new-btn" onclick="createNewThread()" title="New thread (Ctrl/Cmd+N)">+</button>
|
||
<div class="spacer"></div>
|
||
<button class="thread-toggle-btn" id="thread-toggle-btn" onclick="toggleThreadSidebar()" title="Toggle sidebar">«</button>
|
||
</div>
|
||
<div class="assistant-item" id="assistant-thread" onclick="switchToAssistant()">
|
||
<span class="assistant-label" id="assistant-label">Assistant</span>
|
||
<span class="assistant-meta" id="assistant-meta"></span>
|
||
</div>
|
||
<div class="threads-section-header">
|
||
<span>Conversations</span>
|
||
</div>
|
||
<div class="thread-list" id="thread-list"></div>
|
||
</div>
|
||
<div class="chat-container">
|
||
<div class="chat-messages" id="chat-messages"></div>
|
||
<div id="slash-autocomplete" class="slash-autocomplete" style="display:none"></div>
|
||
<div class="chat-input">
|
||
<textarea id="chat-input" placeholder="Message or / for commands..." rows="1"></textarea>
|
||
<button id="send-btn" onclick="sendMessage()">Send</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Memory Tab -->
|
||
<div class="tab-panel" id="tab-memory">
|
||
<div class="memory-container">
|
||
<div class="memory-sidebar">
|
||
<div class="search-box">
|
||
<input type="text" id="memory-search" placeholder="Search memory...">
|
||
</div>
|
||
<div class="memory-tree" id="memory-tree"></div>
|
||
</div>
|
||
<div class="memory-content">
|
||
<div class="memory-breadcrumb" id="memory-breadcrumb">
|
||
<span id="memory-breadcrumb-path">workspace /</span>
|
||
<button class="memory-edit-btn" id="memory-edit-btn" style="display:none" onclick="startMemoryEdit()">Edit</button>
|
||
</div>
|
||
<div class="memory-viewer" id="memory-viewer">
|
||
<div class="empty">Select a file to view its contents</div>
|
||
</div>
|
||
<div class="memory-editor" id="memory-editor" style="display:none">
|
||
<textarea id="memory-edit-textarea"></textarea>
|
||
<div class="memory-editor-actions">
|
||
<button class="btn-save" onclick="saveMemoryEdit()">Save</button>
|
||
<button class="btn-cancel-edit" onclick="cancelMemoryEdit()">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Jobs Tab -->
|
||
<div class="tab-panel" id="tab-jobs">
|
||
<div class="jobs-container">
|
||
<div class="jobs-summary" id="jobs-summary"></div>
|
||
<table class="jobs-table" id="jobs-table">
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Title</th>
|
||
<th>Source</th>
|
||
<th>Status</th>
|
||
<th>Created</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="jobs-tbody"></tbody>
|
||
</table>
|
||
<div class="empty-state" id="jobs-empty" style="display:none">No jobs found</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Logs Tab -->
|
||
<div class="tab-panel" id="tab-logs">
|
||
<div class="logs-container">
|
||
<div class="logs-toolbar">
|
||
<select id="logs-server-level" onchange="setServerLogLevel(this.value)" title="Server-side log level (changes what the server emits)">
|
||
<option value="error">Server: ERROR</option>
|
||
<option value="warn">Server: WARN</option>
|
||
<option value="info" selected>Server: INFO</option>
|
||
<option value="debug">Server: DEBUG</option>
|
||
</select>
|
||
<select id="logs-level-filter">
|
||
<option value="all">All Levels</option>
|
||
<option value="ERROR">Error</option>
|
||
<option value="WARN">Warn</option>
|
||
<option value="INFO">Info</option>
|
||
<option value="DEBUG">Debug</option>
|
||
</select>
|
||
<input type="text" id="logs-target-filter" placeholder="Filter by target...">
|
||
<label class="logs-checkbox"><input type="checkbox" id="logs-autoscroll" checked> Auto-scroll</label>
|
||
<button id="logs-pause-btn" onclick="toggleLogsPause()">Pause</button>
|
||
<button onclick="clearLogs()">Clear</button>
|
||
</div>
|
||
<div class="logs-output" id="logs-output"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Routines Tab -->
|
||
<div class="tab-panel" id="tab-routines">
|
||
<div class="routines-container">
|
||
<div class="routines-summary" id="routines-summary"></div>
|
||
<table class="routines-table" id="routines-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Trigger</th>
|
||
<th>Action</th>
|
||
<th>Last Run</th>
|
||
<th>Next Run</th>
|
||
<th>Runs</th>
|
||
<th>Status</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="routines-tbody"></tbody>
|
||
</table>
|
||
<div class="empty-state" id="routines-empty" style="display:none">
|
||
No routines configured. Ask the assistant to create one.
|
||
</div>
|
||
<div class="routine-detail" id="routine-detail" style="display:none"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Extensions Tab -->
|
||
<div class="tab-panel" id="tab-extensions">
|
||
<div class="extensions-container">
|
||
<div class="extensions-section">
|
||
<h3>Installed Extensions</h3>
|
||
<div class="extensions-list" id="extensions-list">
|
||
<div class="empty-state">Loading extensions...</div>
|
||
</div>
|
||
</div>
|
||
<div class="extensions-section" id="available-wasm-section">
|
||
<h3>Available WASM Extensions</h3>
|
||
<div class="extensions-list" id="available-wasm-list">
|
||
<div class="empty-state">Loading...</div>
|
||
</div>
|
||
</div>
|
||
<div class="extensions-section">
|
||
<h3>Install WASM Extension</h3>
|
||
<div class="ext-install-form">
|
||
<input type="text" id="wasm-install-name" placeholder="Extension name">
|
||
<input type="text" id="wasm-install-url" placeholder="URL to .tar.gz bundle">
|
||
<button onclick="installWasmExtension()">Install</button>
|
||
</div>
|
||
</div>
|
||
<div class="extensions-section">
|
||
<h3>MCP Servers</h3>
|
||
<div class="extensions-list" id="mcp-servers-list">
|
||
<div class="empty-state">Loading...</div>
|
||
</div>
|
||
<h4>Add Custom MCP Server</h4>
|
||
<div class="ext-install-form">
|
||
<input type="text" id="mcp-install-name" placeholder="Server name">
|
||
<input type="text" id="mcp-install-url" placeholder="MCP server URL (https://...)">
|
||
<button onclick="addMcpServer()">Add</button>
|
||
</div>
|
||
</div>
|
||
<div class="extensions-section">
|
||
<h3>Registered Tools</h3>
|
||
<table class="tools-table" id="tools-table">
|
||
<thead><tr><th>Name</th><th>Description</th></tr></thead>
|
||
<tbody id="tools-tbody"></tbody>
|
||
</table>
|
||
<div class="empty-state" id="tools-empty" style="display:none">No tools registered</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Skills Tab -->
|
||
<div class="tab-panel" id="tab-skills">
|
||
<div class="extensions-container">
|
||
<div class="extensions-section">
|
||
<h3>Search ClawHub</h3>
|
||
<div class="skill-search-box">
|
||
<input type="text" id="skill-search-input" placeholder="Search for skills...">
|
||
<button onclick="searchClawHub()">Search</button>
|
||
</div>
|
||
<div class="extensions-list" id="skill-search-results"></div>
|
||
</div>
|
||
<div class="extensions-section">
|
||
<h3>Installed Skills</h3>
|
||
<div class="extensions-list" id="skills-list">
|
||
<div class="empty-state">Loading skills...</div>
|
||
</div>
|
||
</div>
|
||
<div class="extensions-section">
|
||
<h3>Install Skill by URL</h3>
|
||
<div class="ext-install-form">
|
||
<input type="text" id="skill-install-name" placeholder="Skill name or slug">
|
||
<input type="text" id="skill-install-url" placeholder="HTTPS URL to SKILL.md (optional)">
|
||
<button onclick="installSkillFromForm()">Install</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="toasts"></div>
|
||
<script src="/app.js"></script>
|
||
</body>
|
||
</html>
|