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
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
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;
}
Comment on lines +12 to +19
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theme mode read from localStorage is used without validation. If the stored value is anything other than dark/light/system, this will set data-theme/data-theme-mode to unexpected values and the UI can end up in a broken/unspecified theme state. Consider normalizing/whitelisting the mode and falling back to system (or dark) when the stored value is invalid.

Copilot uses AI. Check for mistakes.

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