diff --git a/selfdrive/controls/tests/test_conditional_reentry.py b/selfdrive/controls/tests/test_conditional_reentry.py new file mode 100644 index 0000000000..54a5af9577 --- /dev/null +++ b/selfdrive/controls/tests/test_conditional_reentry.py @@ -0,0 +1,117 @@ +"""Exact class and planner branch, fake clocks/scene/Params; no native runtime.""" +import ast +from pathlib import Path +from types import SimpleNamespace as NS +import pytest + +ROOT = Path(__file__).resolve().parents[3] + +def make_modes(): + now = [100.0] + statuses = {'OFF': 0, 'LEAD': 1, 'SPEED': 2, 'USER_EXPERIMENTAL': 99, 'USER_OVERRIDDEN': 99} + memory = {} + params = NS(get_bool=lambda _: False) + planner = NS(params=params, params_memory=NS(put_int=lambda k,v: memory.update({k:v}))) + ns = {'time': NS(monotonic=lambda: now[0]), 'CV': NS(MPH_TO_MS=0.44704), 'CCStatus': statuses, 'CEStatus': statuses, + 'restore_persisted_cc_state': lambda *_: memory.get('manual', 0), 'restore_persisted_ce_state': lambda *_: memory.get('manual', 0), + 'is_manual_cc_status': lambda s: s == 99, 'is_manual_ce_status': lambda s: s == 99, + 'FirstOrderFilter': lambda *args: NS(x=0), 'DT_MDL': .05} + for file, name in [('conditional_chill_mode.py','ConditionalChillMode'), ('conditional_experimental_mode.py','ConditionalExperimentalMode')]: + path = ROOT/'starpilot/controls/lib'/file + cls = next(n for n in ast.parse(path.read_text()).body if isinstance(n, ast.ClassDef) and n.name == name) + exec(compile(ast.Module(body=[cls], type_ignores=[]), str(path), 'exec'), ns) + cem = ns['ConditionalExperimentalMode'](planner) + ccm = ns['ConditionalChillMode'](planner, cem) + planner.starpilot_cem, planner.starpilot_ccm = cem, ccm + ccm._refresh_detector = lambda *_: None + ccm._get_chill_status = lambda *_: (1, False) + ccm._has_hard_veto = lambda *a, **k: False + cem.update_conditions = lambda *_: None + cem.check_conditions = lambda *_: False + cem.stop_sign_and_light = lambda *_: None + return planner, now, memory + +def branch(planner, mode): + path = ROOT/'starpilot/controls/starpilot_planner.py' + node = next(n for n in ast.walk(ast.parse(path.read_text())) if isinstance(n, ast.If) and ast.unparse(n.test).startswith('conditional_tracking_active and')) + exec(compile(ast.Module(body=[node],type_ignores=[]), str(path), 'exec'), + {'self': planner, 'conditional_tracking_active': True, 'starpilot_toggles': NS(conditional_experimental_mode=mode=='cem', conditional_chill_mode=mode=='ccm'), 'v_ego':20, 'v_cruise':30, 'sm':{}, 'PLANNER_TIME':10}) + +@pytest.mark.parametrize('absence', [.1, 100]) +@pytest.mark.parametrize('other', ['fixed', 'cem']) +def test_ccm_reentry_requires_new_confirmation(absence, other): + p, now, _ = make_modes() + p.starpilot_ccm.update(20,30,{},NS()) + original_update = p.starpilot_cem.update + p.starpilot_cem.update = lambda *_: None + branch(p, other) + p.starpilot_cem.update = original_update + now[0] += absence + p.starpilot_ccm.update(20,30,{},NS()) + assert p.starpilot_ccm.experimental_mode + assert p.starpilot_ccm._candidate_since == now[0] + now[0] += 1.01 + p.starpilot_ccm.update(20,30,{},NS()) + assert not p.starpilot_ccm.experimental_mode + +@pytest.mark.parametrize('other', ['fixed', 'ccm']) +def test_cem_reentry_does_not_inherit_mode_hold(other): + p, now, _ = make_modes() + cem = p.starpilot_cem + cem.prev_experimental_mode = True + cem.mode_hold_until = now[0] + .5 + cem.slow_lead_mode_hold_until = now[0] + 1.5 + original_update = p.starpilot_ccm.update + p.starpilot_ccm.update = lambda *_: None + branch(p, other) + p.starpilot_ccm.update = original_update + now[0] += .1 + cem.update(20, {'carState': NS(standstill=False)}, NS(conditional_lead=False,conditional_open_road=False)) + assert not cem.experimental_mode + +@pytest.mark.parametrize('other', ['fixed', 'cem']) +def test_ccm_manual_override_survives_deactivation(other): + p, _, memory = make_modes() + memory['manual'] = 99 + p.starpilot_cem.update = lambda *_: None + branch(p, other) + p.starpilot_ccm.update(20,30,{},NS()) + assert p.starpilot_ccm.experimental_mode + assert memory['manual'] == 99 + +def test_cem_deactivation_retains_shared_hazard_detector_state(): + p, _, _ = make_modes() + cem = p.starpilot_cem + cem.stop_light_detected = True + cem.stop_light_filter.x = .9 + cem.standstill_stop_reason = 'sign' + branch(p, 'fixed') + assert cem.stop_light_detected and cem.stop_light_filter.x == .9 + assert cem.standstill_stop_reason == 'sign' + + +@pytest.mark.parametrize('condition', ['none', 'veto', 'safe']) +def test_ccm_reentry_still_honors_current_scene_and_safety(condition): + p, now, _ = make_modes() + ccm = p.starpilot_ccm + ccm.update(20,30,{},NS()) + branch(p, 'fixed') + now[0] += 100 + if condition == 'none': + ccm._get_chill_status = lambda *_: (0, False) + elif condition == 'veto': + ccm._has_hard_veto = lambda *a, **k: True + else: + p.params.get_bool = lambda key: key == 'SafeMode' + ccm.update(20,30,{},NS()) + assert ccm.experimental_mode == (condition != 'safe') + assert ccm._candidate_since == 0 + + +def test_cem_manual_override_survives_inactive_interval(): + p, _, memory = make_modes() + memory['manual'] = 99 + branch(p, 'fixed') + p.starpilot_cem.update(20, {'carState': NS(standstill=False)}, NS()) + assert p.starpilot_cem.experimental_mode + assert memory['manual'] == 99 diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 21df28cd91..aea14c1a93 100644 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -35,6 +35,7 @@ from openpilot.system.hardware import HARDWARE from openpilot.starpilot.common.starpilot_utilities import contains_event_type from openpilot.starpilot.common.starpilot_variables import get_starpilot_toggles from openpilot.starpilot.common.lateral_only_experimental import experimental_mode_available +from openpilot.starpilot.common.longitudinal_mode import request_mode_refresh from openpilot.starpilot.common.vision_bsm import get_fresh_vasm_state from openpilot.starpilot.system.wheel_controls import ( CONTROLLER_ACTION_COUNTERS, @@ -845,10 +846,10 @@ class SelfdriveD: self.starpilot_events.add_from_msg(self.sm['starpilotPlan'].starpilotEvents) - if self.starpilot_toggles.conditional_experimental_mode or getattr(self.starpilot_toggles, "conditional_chill_mode", False): - self.experimental_mode = self.sm['starpilotPlan'].experimentalMode - else: - self.experimental_mode |= self.sm['starpilotPlan'].experimentalMode + self.experimental_mode = (not self.safe_mode and experimental_mode_available(self.CP) and ( + self.sm['starpilotPlan'].experimentalMode if not REPLAY or self.starpilot_toggles.conditional_experimental_mode + or getattr(self.starpilot_toggles, "conditional_chill_mode", False) + else self.experimental_mode or self.sm['starpilotPlan'].experimentalMode)) def data_sample(self): _car_state = messaging.recv_one(self.car_state_sock) @@ -995,10 +996,13 @@ class SelfdriveD: self.is_metric = self.params.get_bool("IsMetric") self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled") self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator") - if self.safe_mode: - self.experimental_mode = False - elif not self.starpilot_toggles.conditional_experimental_mode: - self.experimental_mode = self.params.get_bool("ExperimentalMode") and experimental_mode_available(self.CP) + if REPLAY: + if self.safe_mode: + self.experimental_mode = False + elif not self.starpilot_toggles.conditional_experimental_mode: + self.experimental_mode = self.params.get_bool("ExperimentalMode") and experimental_mode_available(self.CP) + else: + request_mode_refresh(self.params, self.params_memory, self.starpilot_toggles) self.personality = log.LongitudinalPersonality.relaxed if self.safe_mode else self.params.get("LongitudinalPersonality", return_default=True) time.sleep(0.1) diff --git a/starpilot/common/longitudinal_mode.py b/starpilot/common/longitudinal_mode.py new file mode 100644 index 0000000000..7951ec8e3b --- /dev/null +++ b/starpilot/common/longitudinal_mode.py @@ -0,0 +1,39 @@ +"""Coherent mode Params reads for participating Python writers/readers. + +The sidecar lock is outside the Params key directory (no registry addition). +Never unlink/replace it while processes are running. All locks are nonblocking: +background refreshers retry and keep the last complete toggle object on failure. +Legacy Dom writers are deliberately not excluded; their settled values remain +visible, but advisory locking cannot make their multi-key writes atomic. +""" +from contextlib import contextmanager +import fcntl +import os +from pathlib import Path + +MODE_KEYS = ("ExperimentalMode", "ConditionalChill", "ConditionalExperimental") + + +@contextmanager +def mode_lock(params, *, exclusive=False): + directory = Path(params.get_param_path()).parent + fd = os.open(directory / ".longitudinal_mode.lock", os.O_CREAT | os.O_RDWR | os.O_CLOEXEC, 0o660) + try: + fcntl.flock(fd, (fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH) | fcntl.LOCK_NB) + yield + finally: + os.close(fd) + + +def read_mode_values(params): + with mode_lock(params): + return {key: params.get_bool(key) for key in MODE_KEYS} + + +def request_mode_refresh(params, params_memory, toggles): + try: + values = read_mode_values(params) + except OSError: + return + if values != getattr(toggles, "longitudinal_mode_values", None): + params_memory.put_bool("StarPilotTogglesUpdated", True) diff --git a/starpilot/common/starpilot_variables.py b/starpilot/common/starpilot_variables.py index 1962fe9865..e8dcc75f53 100644 --- a/starpilot/common/starpilot_variables.py +++ b/starpilot/common/starpilot_variables.py @@ -31,6 +31,7 @@ from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.starpilot.common.model_versions import is_tinygrad_model_version from openpilot.starpilot.common.lateral_delay import full_lateral_delay from openpilot.starpilot.common.lateral_only_experimental import lateral_only_experimental_available +from openpilot.starpilot.common.longitudinal_mode import read_mode_values from openpilot.starpilot.common.accel_profile import ( ACCELERATION_PROFILES, A_CRUISE_MAX_BP_CUSTOM, @@ -623,6 +624,14 @@ class StarPilotVariables: def update(self, holiday_theme="stock", started=False, clear_update_flag=True): toggle = self.starpilot_toggles + try: + mode_values = read_mode_values(self.params) + except OSError: + self.params_memory.put_bool("StarPilotTogglesUpdated", True) + if hasattr(toggle, "longitudinal_mode_values"): + return + mode_values = {"ExperimentalMode": False, "ConditionalChill": False, "ConditionalExperimental": False} + clear_update_flag = False # CarParams uses this value to select the matching Panda safety configuration. toggle.tesla_cooperative_steering = self.params.get_bool("TeslaCoopSteering") toggle.rivian_angle_control = self.params.get_bool("RivianAngleControl") @@ -873,8 +882,10 @@ class StarPilotVariables: self.migrate_prius_cluster_offset(str(toggle.car_model)) toggle.cluster_offset = self.get_value("ClusterOffset", cast=float, condition=toggle.car_make == "toyota") - toggle.conditional_experimental_mode = toggle.openpilot_longitudinal and self.get_value("ConditionalExperimental") - toggle.conditional_chill_mode = toggle.openpilot_longitudinal and not toggle.conditional_experimental_mode and self.get_value("ConditionalChill") + toggle.longitudinal_mode_values = mode_values + toggle.experimental_mode = toggle.experimental_mode_available and not toggle.safe_mode and mode_values["ExperimentalMode"] + toggle.conditional_experimental_mode = toggle.openpilot_longitudinal and not toggle.safe_mode and mode_values["ConditionalExperimental"] + toggle.conditional_chill_mode = toggle.openpilot_longitudinal and not toggle.safe_mode and not toggle.conditional_experimental_mode and mode_values["ConditionalChill"] toggle.conditional_curves = self.get_value("CECurves", condition=toggle.conditional_experimental_mode) toggle.conditional_curves_lead = self.get_value("CECurvesLead", condition=toggle.conditional_curves) toggle.conditional_lead = self.get_value("CELead", condition=toggle.conditional_experimental_mode) diff --git a/starpilot/controls/lib/conditional_chill_mode.py b/starpilot/controls/lib/conditional_chill_mode.py index d33ee312a5..148395a458 100644 --- a/starpilot/controls/lib/conditional_chill_mode.py +++ b/starpilot/controls/lib/conditional_chill_mode.py @@ -138,6 +138,11 @@ class ConditionalChillMode: self._write_status(self.status_value if not self.experimental_mode else CCStatus["OFF"]) + def deactivate(self): + self._reset_timers() + self.experimental_mode = True + self._prev_cc_status = None + def _reset_timers(self): self._active_auto_status = CCStatus["OFF"] self._candidate_since = 0.0 diff --git a/starpilot/controls/lib/conditional_experimental_mode.py b/starpilot/controls/lib/conditional_experimental_mode.py index 2be9e9d953..477cb4d5ed 100644 --- a/starpilot/controls/lib/conditional_experimental_mode.py +++ b/starpilot/controls/lib/conditional_experimental_mode.py @@ -127,6 +127,17 @@ class ConditionalExperimentalMode: self.prev_open_road_triggered = False self.open_road_lead_hold_until = 0.0 + def deactivate(self): + self.experimental_mode = False + self.prev_experimental_mode = False + self.mode_hold_until = 0.0 + self.mode_false_since = 0.0 + self.slow_lead_mode_hold_until = 0.0 + self.open_road_triggered = False + self.prev_open_road_triggered = False + self.open_road_lead_hold_until = 0.0 + self._prev_ce_status = None + def update(self, v_ego, sm, starpilot_toggles, v_cruise=None): now = time.monotonic() standstill = bool(sm["carState"].standstill) diff --git a/starpilot/controls/starpilot_planner.py b/starpilot/controls/starpilot_planner.py index bf40fa3ff6..17e60fea06 100644 --- a/starpilot/controls/starpilot_planner.py +++ b/starpilot/controls/starpilot_planner.py @@ -227,13 +227,13 @@ class StarPilotPlanner: if conditional_tracking_active and bool(getattr(starpilot_toggles, "conditional_experimental_mode", False)): # Keep CEM's filters warm in AOL so engagement can inherit the current scene. self.starpilot_cem.update(v_ego, sm, starpilot_toggles, v_cruise) - self.starpilot_ccm.experimental_mode = True + self.starpilot_ccm.deactivate() elif conditional_tracking_active and bool(getattr(starpilot_toggles, "conditional_chill_mode", False)): self.starpilot_ccm.update(v_ego, v_cruise, sm, starpilot_toggles) - self.starpilot_cem.experimental_mode = False + self.starpilot_cem.deactivate() else: - self.starpilot_ccm.experimental_mode = True - self.starpilot_cem.experimental_mode = False + self.starpilot_ccm.deactivate() + self.starpilot_cem.deactivate() self.starpilot_cem.curve_detected = False self.starpilot_cem.stop_sign_and_light(v_ego, sm, PLANNER_TIME - 2) @@ -341,7 +341,7 @@ class StarPilotPlanner: starpilotPlan.pulseGlideCoasting = self.starpilot_acceleration.pulse_glide_coasting starpilotPlan.trackingLead = self.tracking_lead - conditional_experimental_mode = False + conditional_experimental_mode = bool(getattr(starpilot_toggles, "experimental_mode", False)) if starpilot_toggles.conditional_experimental_mode: conditional_experimental_mode = self.starpilot_cem.experimental_mode elif starpilot_toggles.conditional_chill_mode: diff --git a/starpilot/starpilot_process.py b/starpilot/starpilot_process.py index eabc68bdf9..462fe6ad72 100644 --- a/starpilot/starpilot_process.py +++ b/starpilot/starpilot_process.py @@ -256,6 +256,7 @@ def update_toggles_in_background(result, starpilot_variables, started, theme_man result["update"] = (updated_variables, updated_toggles) except Exception: result["failed"] = True + starpilot_variables.params_memory.put_bool("StarPilotTogglesUpdated", True) raise diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js index 3e5ca0ce7e..502dfaa165 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js @@ -8,6 +8,8 @@ import { } from "/assets/components/tools/personality_profiles.mjs" import { formatNumericParamValue, resolveVehicleUnitParam, vehicleSpeedUnit } from "/assets/mobile/js/params.js" +import { LONGITUDINAL_MODE_KEY, longitudinalModeLayout, validLongitudinalSnapshot } from "/assets/components/tools/longitudinal_mode.mjs" + const endpointOptionsCache = {} const endpointOptionsInflight = {} const COLOR_UI_DEFAULTS = { @@ -125,6 +127,9 @@ const FLM_ADVANCED_LATERAL_KEYS = new Set([ // Module-level state (persists across route changes) const state = reactive({ + longitudinalMode: null, + longitudinalModeUpdating: false, + longitudinalModeRequestId: 0, layout: [], allKeys: [], paramMetaByKey: {}, @@ -192,6 +197,7 @@ function matchesSettingValueCondition(param) { } function isSettingVisible(section, param) { + if (param.longitudinal_mode && param.longitudinal_mode !== state.longitudinalMode?.mode) return false if (PROFILE_HIDDEN_LAYOUT_KEYS.has(param.key) || HIDDEN_SETTING_KEYS.has(param.key) || !isVehicleSettingVisible(section, param) || !matchesSettingValueCondition(param)) return false if (param.requires_capability && !state.values[param.requires_capability]) return false @@ -239,6 +245,7 @@ function isParamEnabledForChildren(paramOrKey) { if (isGroupParam(param)) return true const key = isKey ? paramOrKey : (param && param.key) + if (key === LONGITUDINAL_MODE_KEY) return ["conditional_experimental", "conditional_chill"].includes(state.longitudinalMode?.mode) return !!(key && state.values[key]) } @@ -439,6 +446,62 @@ function syncInputs() { } } +function applyLongitudinalMode(data) { + if (!validLongitudinalSnapshot(data)) throw new Error("Longitudinal mode state is unavailable. Refresh to retry.") + if (JSON.stringify(state.longitudinalMode) === JSON.stringify(data) && state.values[LONGITUDINAL_MODE_KEY] === data.mode && + Object.entries(data.values).every(([key, value]) => state.values[key] === value)) return + state.longitudinalMode = data + state.values = { ...state.values, ...data.values, [LONGITUDINAL_MODE_KEY]: data.mode } + scheduleSyncInputs() +} + +async function fetchLongitudinalMode(force = false) { + if (state.longitudinalModeUpdating && !force) return + const requestId = ++state.longitudinalModeRequestId + try { + const response = await fetch("/api/longitudinal_mode", { cache: "no-store" }) + if (!response.ok) throw new Error("Longitudinal mode unavailable") + const data = await response.json() + if (requestId === state.longitudinalModeRequestId && (!state.longitudinalModeUpdating || force)) applyLongitudinalMode(data) + } catch (_error) { + if (requestId !== state.longitudinalModeRequestId || (state.longitudinalModeUpdating && !force)) return + state.longitudinalMode = null + state.values = { ...state.values, [LONGITUDINAL_MODE_KEY]: "" } + scheduleSyncInputs() + } +} + +async function updateLongitudinalMode(targetOverride = null) { + const el = document.getElementById(`ds-${LONGITUDINAL_MODE_KEY}`) + if ((!el && !targetOverride) || getSettingLockReason({ key: LONGITUDINAL_MODE_KEY })) { + scheduleSyncInputs() + return + } + const target = targetOverride || el.value + const current = state.longitudinalMode + if (target === current.mode) return + const acknowledged = target === "experimental" + // Invalidate pre-write reads, even if they finish after forced reconciliation. + ++state.longitudinalModeRequestId + state.longitudinalModeUpdating = true + try { + const response = await fetch("/api/longitudinal_mode", { + method: "PUT", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ mode: target, expected: current.values, acknowledged }), + }) + const data = await response.json() + if (!response.ok) throw new Error(data.error || "Longitudinal mode update failed") + applyLongitudinalMode(data) + showParamSnackbar("Longitudinal control mode updated.") + } catch (error) { + showParamSnackbar(error.message || "Longitudinal mode update failed", "error") + } finally { + await fetchLongitudinalMode(true) + state.longitudinalModeUpdating = false + scheduleSyncInputs() + } +} + async function fetchDefaultValues() { try { const defaultsRes = await fetch("/api/params/defaults") @@ -549,13 +612,14 @@ async function fetchLayoutAndParams() { const layoutRes = await fetch("/assets/components/tools/device_settings_layout.json?v=settings-tier-1", { cache: "no-store" }) const rawLayoutData = await layoutRes.json() - const layoutData = rawLayoutData + let layoutData = rawLayoutData .map(section => ({ ...section, params: (section.params || []).filter(param => param.key !== "Model"), })) .filter(section => section.params.length > 0) + layoutData = longitudinalModeLayout(layoutData) state.layout = layoutData const keys = [] @@ -589,6 +653,7 @@ async function fetchLayoutAndParams() { state.defaultValues = {} } + await fetchLongitudinalMode() await fetchFavoriteSlots() state.loadingValues = false @@ -876,7 +941,8 @@ async function refreshUiContextValues() { state.values = nextValues scheduleSyncInputs() } - }).catch(() => {}).finally(() => { + }).catch(() => {}).finally(async () => { + await fetchLongitudinalMode() uiContextPollInflight = null }) @@ -957,6 +1023,18 @@ function updateFavoriteFilter(index, event) { } async function updateFavoriteValue(key, checked, sourceEl = null) { + if (["ExperimentalMode", "ConditionalExperimental", "ConditionalChill"].includes(key)) { + await fetchLongitudinalMode() + if (state.longitudinalMode) { + const candidate = { ...state.longitudinalMode.values, [key]: checked } + if (checked && key !== "ExperimentalMode") candidate[key === "ConditionalExperimental" ? "ConditionalChill" : "ConditionalExperimental"] = false + const target = candidate.ConditionalExperimental ? "conditional_experimental" : candidate.ConditionalChill ? "conditional_chill" : candidate.ExperimentalMode ? "experimental" : "chill" + await updateLongitudinalMode(target) + } + if (sourceEl) sourceEl.checked = !!state.values[key] + scheduleSyncInputs() + return + } if (!confirmPandaFirmwareToggle(key, checked)) { if (sourceEl) sourceEl.checked = !!state.values[key] scheduleSyncInputs() @@ -1326,6 +1404,10 @@ async function runSettingAction(param) { } async function updateParam(key, elType) { + if (key === LONGITUDINAL_MODE_KEY) { + await updateLongitudinalMode() + return + } if (String(key).toLowerCase() === "starpilotfavoriteslots") { await saveFavoriteSlots(state.favoriteSlots) return @@ -1492,6 +1574,11 @@ function getSettingLockReason(param) { if (param?.key === "CustomPersonalities" && state.personalityMigrationRequired) { return "This profile data requires a verified migration before it can be edited." } + if (param?.key === LONGITUDINAL_MODE_KEY) { + if (state.longitudinalModeUpdating) return "Updating longitudinal control mode…" + if (!state.longitudinalMode) return "Longitudinal mode state unavailable. Refresh to retry." + return state.longitudinalMode.locked ? state.longitudinalMode.reason : "" + } if (param?.requires_offroad && state.values.IsOnroad) { return "This setting can only be changed while parked." } @@ -2596,7 +2683,16 @@ function renderSettingRow(p) { ` : ""} - ${() => p.is_parent_toggle && (p.key === "CustomPersonalities" || isParamEnabledForChildren(p)) ? html` + ${() => p.key === LONGITUDINAL_MODE_KEY && isParamEnabledForChildren(p) ? html` + + ` : ""} + ${() => p.key !== LONGITUDINAL_MODE_KEY && p.is_parent_toggle && (p.key === "CustomPersonalities" || isParamEnabledForChildren(p)) ? html` +
+ +
+ + `, +} diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/SettingTree.js b/starpilot/system/the_galaxy/assets/mobile/js/components/SettingTree.js index 0d04da9118..e74a3ced6c 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/components/SettingTree.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/components/SettingTree.js @@ -32,7 +32,7 @@ export const SettingTree = {