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
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ DATABASE_POOL_SIZE=10

# LLM Provider
# LLM_BACKEND=nearai # default
# Possible values: nearai, ollama, openai_compatible, openai, anthropic, tinfoil
# Possible values: nearai, ollama, openai_compatible, openai, anthropic, tinfoil, openai_codex
# LLM_REQUEST_TIMEOUT_SECS=120 # Increase for local LLMs (Ollama, vLLM, LM Studio)

# === Anthropic Direct ===
Expand Down Expand Up @@ -92,6 +92,13 @@ NEARAI_AUTH_URL=https://private.near.ai
# long = 1-hour TTL, 2.0× (200%) write surcharge
# ANTHROPIC_CACHE_RETENTION=short

# === OpenAI Codex (ChatGPT subscription, OAuth) ===
# LLM_BACKEND=openai_codex
# OPENAI_CODEX_MODEL=gpt-5.3-codex # default
# OPENAI_CODEX_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann # override (rare)
# OPENAI_CODEX_AUTH_URL=https://auth.openai.com # override (rare)
# OPENAI_CODEX_API_URL=https://chatgpt.com/backend-api/codex # override (rare)

# For full provider setup guide see docs/LLM_PROVIDERS.md

# Channel Configuration
Expand Down
3 changes: 3 additions & 0 deletions src/agent/job_monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ mod tests {
job_id: job_id.to_string(),
status: "completed".to_string(),
session_id: None,
fallback_deliverable: None,
},
))
.unwrap();
Expand Down Expand Up @@ -468,6 +469,7 @@ mod tests {
job_id: job_id.to_string(),
status: "failed".to_string(),
session_id: None,
fallback_deliverable: None,
},
))
.unwrap();
Expand Down Expand Up @@ -506,6 +508,7 @@ mod tests {
job_id: job_id.to_string(),
status: "completed".to_string(),
session_id: None,
fallback_deliverable: None,
},
))
.unwrap();
Expand Down
1 change: 1 addition & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,7 @@ impl AppBuilder {
// fail early with a clear error instead of a confusing runtime failure.
if self.config.llm.backend != "nearai"
&& self.config.llm.backend != "bedrock"
&& self.config.llm.backend != "openai_codex"
&& self.config.llm.provider.is_none()
{
let backend = &self.config.llm.backend;
Expand Down
11 changes: 11 additions & 0 deletions src/channels/wasm/wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3314,6 +3314,7 @@ mod tests {
use std::sync::Arc;

use crate::channels::Channel;
use crate::channels::OutgoingResponse;
use crate::channels::wasm::capabilities::ChannelCapabilities;
use crate::channels::wasm::runtime::{
PreparedChannelModule, WasmChannelRuntime, WasmChannelRuntimeConfig,
Expand Down Expand Up @@ -3401,6 +3402,16 @@ mod tests {
assert!(channel.health_check().await.is_err());
}

#[tokio::test]
async fn test_broadcast_delegates_to_call_on_broadcast() {
let channel = create_test_channel();
// With `component: None`, call_on_broadcast short-circuits to Ok(()).
let result = channel
.broadcast("146032821", OutgoingResponse::text("hello"))
.await;
assert!(result.is_ok());
}

#[tokio::test]
async fn test_execute_poll_no_wasm_returns_empty() {
// When there's no WASM module (None component), execute_poll
Expand Down
11 changes: 11 additions & 0 deletions src/channels/web/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ pub async fn start_server(
.route("/", get(index_handler))
.route("/style.css", get(css_handler))
.route("/app.js", get(js_handler))
.route("/theme-init.js", get(theme_init_handler))
.route("/favicon.ico", get(favicon_handler))
.route("/i18n/index.js", get(i18n_index_handler))
.route("/i18n/en.js", get(i18n_en_handler))
Expand Down Expand Up @@ -465,6 +466,16 @@ async fn js_handler() -> impl IntoResponse {
)
}

async fn theme_init_handler() -> impl IntoResponse {
(
[
(header::CONTENT_TYPE, "application/javascript"),
(header::CACHE_CONTROL, "no-cache"),
],
include_str!("static/theme-init.js"),
)
}

async fn favicon_handler() -> impl IntoResponse {
(
[
Expand Down
64 changes: 64 additions & 0 deletions src/channels/web/static/app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,69 @@
// IronClaw Web Gateway - Client

// --- Theme Management (dark / light / system) ---
// Icon switching is handled by pure CSS via data-theme-mode on <html>.

function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}

const VALID_THEME_MODES = { dark: true, light: true, system: true };

function getThemeMode() {
const stored = localStorage.getItem('ironclaw-theme');
return (stored && VALID_THEME_MODES[stored]) ? stored : 'system';
}

function resolveTheme(mode) {
return mode === 'system' ? getSystemTheme() : mode;
}

function applyTheme(mode) {
const resolved = resolveTheme(mode);
document.documentElement.setAttribute('data-theme', resolved);
document.documentElement.setAttribute('data-theme-mode', mode);
const titleKeys = { dark: 'theme.tooltipDark', light: 'theme.tooltipLight', system: 'theme.tooltipSystem' };
const btn = document.getElementById('theme-toggle');
if (btn) btn.title = (typeof I18n !== 'undefined' && titleKeys[mode]) ? I18n.t(titleKeys[mode]) : ('Theme: ' + mode);
const announce = document.getElementById('theme-announce');
if (announce) announce.textContent = (typeof I18n !== 'undefined') ? I18n.t('theme.announce', { mode: mode }) : ('Theme: ' + mode);
}

function toggleTheme() {
const cycle = { dark: 'light', light: 'system', system: 'dark' };
const current = getThemeMode();
const next = cycle[current] || 'dark';
localStorage.setItem('ironclaw-theme', next);
applyTheme(next);
}

// Apply theme immediately (FOUC prevention is done via inline script in <head>,
// but we call again here to ensure tooltip is set after DOM is ready).
applyTheme(getThemeMode());

// Delay enabling theme transition to avoid flash on initial load.
requestAnimationFrame(function() {
requestAnimationFrame(function() {
document.body.classList.add('theme-transition');
});
});

// Listen for OS theme changes — only re-apply when in 'system' mode.
const mql = window.matchMedia('(prefers-color-scheme: light)');
const onSchemeChange = function() {
if (getThemeMode() === 'system') {
applyTheme('system');
}
};
if (mql.addEventListener) {
mql.addEventListener('change', onSchemeChange);
} else if (mql.addListener) {
mql.addListener(onSchemeChange);
}

// Bind theme toggle button (CSP-compliant — no inline onclick).
document.getElementById('theme-toggle').addEventListener('click', toggleTheme);

let token = '';
let eventSource = null;
let logEventSource = null;
Expand Down
6 changes: 6 additions & 0 deletions src/channels/web/static/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ I18n.register('en', {
'restart.progressSubtitle': 'Please wait for the process to restart...',
'restart.checkLogs': 'Check the Logs tab for details after restart completes.',

// Theme
'theme.tooltipDark': 'Theme: Dark (click for Light)',
'theme.tooltipLight': 'Theme: Light (click for System)',
'theme.tooltipSystem': 'Theme: System (click for Dark)',
'theme.announce': 'Theme: {mode}',

// Tabs
'tab.chat': 'Chat',
'tab.memory': 'Memory',
Expand Down
6 changes: 6 additions & 0 deletions src/channels/web/static/i18n/zh-CN.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ I18n.register('zh-CN', {
'restart.progressSubtitle': '请等待进程重启...',
'restart.checkLogs': '重启完成后,请查看日志标签页了解详情。',

// 主题
'theme.tooltipDark': '主题:深色(点击切换浅色)',
'theme.tooltipLight': '主题:浅色(点击切换跟随系统)',
'theme.tooltipSystem': '主题:跟随系统(点击切换深色)',
'theme.announce': '主题:{mode}',

// 标签页
'tab.chat': '聊天',
'tab.memory': '记忆',
Expand Down
13 changes: 13 additions & 0 deletions src/channels/web/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
integrity="sha384-pN9zSKOnTZwXRtYZAu0PBPEgR2B7DOC1aeLxQ33oJ0oy5iN1we6gm57xldM2irDG"
crossorigin="anonymous"
></script>
<script src="/theme-init.js"></script>
</head>
<body>
<!-- Auth Screen -->
Expand Down Expand Up @@ -109,6 +110,18 @@ <h2 data-i18n="restart.title">Restart IronClaw Instance</h2>
</div>

<button class="status-logs-btn" data-tab="logs" data-i18n="tab.logs" title="Logs">Logs</button>
<button class="theme-toggle-btn" id="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg class="theme-icon icon-dark" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
<svg class="theme-icon icon-light" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
<svg class="theme-icon icon-system" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>
</svg>
</button>
<span id="theme-announce" class="sr-only" aria-live="polite"></span>
<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"/>
Expand Down
Loading
Loading