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
4 changes: 4 additions & 0 deletions crates/ironclaw_gateway/static/js/core/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ const GENERATED_IMAGE_THREAD_CACHE_CAP = 20;
const GENERATED_IMAGES_PER_THREAD_CAP = 8;
let engineV2Enabled = false;
let engineModeApplied = false;
// True when the user has at least one v1 routine in the database. Set
// from /api/routines/summary so the Routines tab stays visible after
// an engine v1 → v2 upgrade for users with pre-existing routines (#2982).
let userHasLegacyRoutines = false;
let currentMissionData = null;
let currentEngineThreadDetail = null;
let currentMissionList = [];
Expand Down
46 changes: 37 additions & 9 deletions crates/ironclaw_gateway/static/js/core/gateway-tee.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ function startGatewayStatusPolling() {
gatewayStatusInterval = setInterval(fetchGatewayStatus, 30000);
}

// Sets userHasLegacyRoutines from /api/routines/summary. Resolves
// regardless of fetch outcome so the caller's chained UI work always
// runs. A failure leaves the global at its current value (default false),
// which matches the pre-fix behaviour for v2 deployments.
function refreshLegacyRoutinesPresence() {
return apiFetch('/api/routines/summary').then(function(s) {
userHasLegacyRoutines = !!(s && (s.total || 0) > 0);
}).catch(function() {});
}

function formatTokenCount(n) {
if (n == null || n === 0) return '0';
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
Expand All @@ -30,21 +40,39 @@ function shortModelName(model) {

function fetchGatewayStatus() {
apiFetch('/api/gateway/status').then(function(data) {
activeWorkStore.setEngineV2Enabled(!!data.engine_v2);
applyEngineModeUi();
// Single canonical wire field: `engine_v2_enabled`. Reading two
// different field names from the same response was a divergence
// hazard called out in .claude/rules/types.md and triggered the
// ordering bug behind #2982.
var enabled = !!data.engine_v2_enabled;

// Apply engine v2 / v1 tab visibility once. Set the global before
// any UI helper reads it. The flag flips synchronously so that a
// second status poll firing while the first refresh is still in
// flight does not kick off a duplicate /api/routines/summary
// request. refreshLegacyRoutinesPresence swallows fetch errors, so
// the trailing .then() still runs on failure with
// userHasLegacyRoutines = false (the safe default).
if (!engineModeApplied) {
engineModeApplied = true;
engineV2Enabled = enabled;
// Refresh legacy-routine count once on first status so v1 users
// upgrading to v2 keep the Routines tab affordance (#2982).
refreshLegacyRoutinesPresence().then(function() {
applyEngineModeToTabs();
applyEngineModeUi();
});
} else {
applyEngineModeUi();
}

activeWorkStore.setEngineV2Enabled(enabled);
refreshPersistentActivityBar();

// Update restart button visibility
restartEnabled = data.restart_enabled || false;
updateRestartButtonVisibility();

// Apply engine v2 / v1 tab visibility once.
if (!engineModeApplied) {
engineV2Enabled = !!data.engine_v2_enabled;
applyEngineModeToTabs();
engineModeApplied = true;
}

var popover = document.getElementById('gateway-popover');
var html = '';

Expand Down
19 changes: 14 additions & 5 deletions crates/ironclaw_gateway/static/js/core/routing.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,16 @@ function parseHash() {
};
}

function shouldHideRoutinesTab() {
// The Routines tab belongs to engine v1. When v2 is on, hide it — UNLESS
// the user has existing v1 routines from a pre-v2 install. Without this
// affordance, an upgrade silently strips access to data the API still
// serves (#2982).
return engineV2Enabled && !userHasLegacyRoutines;
}

function normalizeTabForEngineMode(tab) {
if (engineV2Enabled && tab === 'routines') {
if (shouldHideRoutinesTab() && tab === 'routines') {
return 'missions';
}
return tab;
Expand All @@ -61,13 +69,14 @@ function normalizeTabForEngineMode(tab) {
function applyEngineModeUi() {
var routinesTab = document.querySelector('.tab-bar [data-tab-role="routines"]');
var routinesPanel = document.getElementById('tab-routines');
var hideRoutines = shouldHideRoutinesTab();
if (routinesTab) {
routinesTab.style.display = engineV2Enabled ? 'none' : '';
routinesTab.style.display = hideRoutines ? 'none' : '';
}
if (routinesPanel && engineV2Enabled && currentTab !== 'routines') {
if (routinesPanel && hideRoutines && currentTab !== 'routines') {
routinesPanel.classList.remove('active');
}
if (engineV2Enabled && currentTab === 'routines') {
if (hideRoutines && currentTab === 'routines') {
switchTab('missions');
}
}
Expand Down Expand Up @@ -104,7 +113,7 @@ function restoreFromHash() {
openJobDetail(state.detail);
break;
case 'routines':
if (engineV2Enabled) {
if (shouldHideRoutinesTab()) {
switchTab('missions');
} else {
openRoutineDetail(state.detail);
Expand Down
9 changes: 8 additions & 1 deletion crates/ironclaw_gateway/static/js/surfaces/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ function applyEngineModeToTabs() {
document.querySelectorAll('.tab-bar [data-v2-only]').forEach(function(el) {
el.style.display = engineV2Enabled ? '' : 'none';
});
// The Routines tab is the only v1-only tab today and stays visible
// when the user still has legacy routines (#2982). Other v1-only
// markers, if added later, follow the engine flag. shouldHideRoutinesTab
// is the single source of truth for the routines-visibility rule —
// duplicating its logic here was how #2574 / #2665 originally drifted.
document.querySelectorAll('.tab-bar [data-v1-only]').forEach(function(el) {
el.style.display = engineV2Enabled ? 'none' : '';
var isRoutinesTab = el.getAttribute('data-tab-role') === 'routines';
var hide = isRoutinesTab ? shouldHideRoutinesTab() : engineV2Enabled;
el.style.display = hide ? 'none' : '';
});
var activeBtn = document.querySelector('.tab-bar button[data-tab].active');
if (activeBtn && activeBtn.style.display === 'none') switchTab('chat');
Expand Down
7 changes: 7 additions & 0 deletions crates/ironclaw_gateway/static/js/surfaces/routines.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ function deleteRoutine(id, name) {
apiFetch('/api/routines/' + id, { method: 'DELETE' })
.then(() => {
showToast(I18n.t('routines.deleted'), 'success');
// Re-check legacy routine count so the v2 user who just deleted
// their last v1 routine sees the tab fall back to hidden without
// a page reload (#2982).
refreshLegacyRoutinesPresence().then(function() {
applyEngineModeToTabs();
applyEngineModeUi();
});
if (currentRoutineId === id) closeRoutineDetail();
else loadRoutines();
})
Expand Down
2 changes: 0 additions & 2 deletions src/channels/web/features/status/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ pub(crate) struct GatewayStatusResponse {
ws_connections: u64,
total_connections: u64,
uptime_secs: u64,
engine_v2: bool,
restart_enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
daily_cost: Option<String>,
Expand Down Expand Up @@ -105,7 +104,6 @@ pub(crate) async fn gateway_status_handler(
ws_connections,
total_connections: sse_connections + ws_connections,
uptime_secs,
engine_v2: crate::bridge::is_engine_v2_enabled(),
restart_enabled,
daily_cost,
actions_this_hour,
Expand Down
Loading
Loading