From 6e74964853c683853fe95727d6fa787309bf20c7 Mon Sep 17 00:00:00 2001
From: DCCONSTRUCTIONS
Date: Sat, 23 May 2026 10:32:04 +0300
Subject: [PATCH] fix: temporarily lock tasker dark theme
---
plane-src/apps/web/app/root.tsx | 25 ++++++++--
.../components/preferences/theme-switcher.tsx | 16 +++---
.../components/appearance/theme-switcher.tsx | 15 +++---
.../components/core/theme/theme-switch.tsx | 50 +++++++++----------
.../onboarding/switch-account-modal.tsx | 4 +-
.../power-k/config/preferences-commands.ts | 6 ++-
.../ui/pages/preferences/themes-menu.tsx | 4 +-
.../settings/codex-agent-api-settings.tsx | 25 ++++++++--
.../web/core/lib/wrappers/store-wrapper.tsx | 37 ++++++++++----
plane-src/packages/constants/src/themes.ts | 10 +++-
10 files changed, 126 insertions(+), 66 deletions(-)
diff --git a/plane-src/apps/web/app/root.tsx b/plane-src/apps/web/app/root.tsx
index 7e6ac34..7705c4e 100644
--- a/plane-src/apps/web/app/root.tsx
+++ b/plane-src/apps/web/app/root.tsx
@@ -10,7 +10,7 @@ import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router";
import { ThemeProvider } from "next-themes";
// plane imports
-import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants";
+import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED, SITE_DESCRIPTION, SITE_NAME, THEMES } from "@plane/constants";
import { cn } from "@plane/utils";
// types
// assets
@@ -24,9 +24,12 @@ import { NodeDCSessionSync } from "@/components/auth-screens/nodedc-session-sync
import { CustomErrorComponent } from "./error";
import { AppProvider } from "./provider";
// fonts
+// oxlint-disable-next-line import/no-unassigned-import
import "@fontsource-variable/inter";
import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url";
+// oxlint-disable-next-line import/no-unassigned-import
import "@fontsource/material-symbols-rounded";
+// oxlint-disable-next-line import/no-unassigned-import
import "@fontsource/ibm-plex-mono";
const APP_TITLE = "NODE.DC | Self-hosted task management workspace.";
@@ -110,15 +113,20 @@ export function Layout({ children }: { children: ReactNode }) {
const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0");
return (
-
+
-
+
{/* Meta info for PWA */}
-
+
@@ -128,7 +136,14 @@ export function Layout({ children }: { children: ReactNode }) {
-
+
{children}
diff --git a/plane-src/apps/web/ce/components/preferences/theme-switcher.tsx b/plane-src/apps/web/ce/components/preferences/theme-switcher.tsx
index b335b99..ad61016 100644
--- a/plane-src/apps/web/ce/components/preferences/theme-switcher.tsx
+++ b/plane-src/apps/web/ce/components/preferences/theme-switcher.tsx
@@ -9,7 +9,7 @@ import { observer } from "mobx-react";
import { useTheme } from "next-themes";
// plane imports
import type { I_THEME_OPTION } from "@plane/constants";
-import { THEME_OPTIONS } from "@plane/constants";
+import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED, THEME_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { setPromiseToast } from "@plane/propel/toast";
import { applyCustomTheme } from "@plane/utils";
@@ -35,18 +35,20 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
const { t } = useTranslation();
// derived values
const currentTheme = useMemo(() => {
- const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile?.theme?.theme);
- return userThemeOption || null;
+ const themeValue = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : userProfile?.theme?.theme;
+ return THEME_OPTIONS.find((themeOption) => themeOption.value === themeValue) || null;
}, [userProfile?.theme?.theme]);
const handleThemeChange = useCallback(
async (themeOption: I_THEME_OPTION) => {
try {
- setTheme(themeOption.value);
+ const nextTheme = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : themeOption.value;
+ setTheme(nextTheme);
// If switching to custom theme and user has saved custom colors, apply them immediately
if (
- themeOption.value === "custom" &&
+ !NODEDC_THEME_LOCK_ENABLED &&
+ nextTheme === "custom" &&
userProfile?.theme?.primary &&
userProfile?.theme?.background &&
userProfile?.theme?.darkPalette !== undefined
@@ -58,7 +60,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
);
}
- const updatePromise = updateUserTheme({ theme: themeOption.value });
+ const updatePromise = updateUserTheme({ theme: nextTheme });
setPromiseToast(updatePromise, {
loading: "Updating theme...",
success: {
@@ -96,7 +98,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
/>
}
/>
- {userProfile.theme?.theme === "custom" && }
+ {!NODEDC_THEME_LOCK_ENABLED && userProfile.theme?.theme === "custom" && }
>
);
});
diff --git a/plane-src/apps/web/core/components/appearance/theme-switcher.tsx b/plane-src/apps/web/core/components/appearance/theme-switcher.tsx
index 4ec2eba..1b47e2e 100644
--- a/plane-src/apps/web/core/components/appearance/theme-switcher.tsx
+++ b/plane-src/apps/web/core/components/appearance/theme-switcher.tsx
@@ -9,7 +9,7 @@ import { observer } from "mobx-react";
import { useTheme } from "next-themes";
// plane imports
import type { I_THEME_OPTION } from "@plane/constants";
-import { THEME_OPTIONS } from "@plane/constants";
+import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED, THEME_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { setPromiseToast } from "@plane/propel/toast";
// components
@@ -34,15 +34,16 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
const { t } = useTranslation();
// derived values
const currentTheme = useMemo(() => {
- const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile?.theme?.theme);
- return userThemeOption || null;
+ const themeValue = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : userProfile?.theme?.theme;
+ return THEME_OPTIONS.find((themeOption) => themeOption.value === themeValue) || null;
}, [userProfile?.theme?.theme]);
const handleThemeChange = useCallback(
(themeOption: I_THEME_OPTION) => {
try {
- setTheme(themeOption.value);
- const updatePromise = updateUserTheme({ theme: themeOption.value });
+ const nextTheme = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : themeOption.value;
+ setTheme(nextTheme);
+ const updatePromise = updateUserTheme({ theme: nextTheme });
setPromiseToast(updatePromise, {
loading: "Updating theme...",
success: {
@@ -58,7 +59,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
console.error("Error updating theme:", error);
}
},
- [updateUserTheme]
+ [setTheme, updateUserTheme]
);
if (!userProfile) return null;
@@ -70,7 +71,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
description={t(props.option.description)}
control={}
/>
- {userProfile.theme?.theme === "custom" && }
+ {!NODEDC_THEME_LOCK_ENABLED && userProfile.theme?.theme === "custom" && }
>
);
});
diff --git a/plane-src/apps/web/core/components/core/theme/theme-switch.tsx b/plane-src/apps/web/core/components/core/theme/theme-switch.tsx
index fc34351..94b1a78 100644
--- a/plane-src/apps/web/core/components/core/theme/theme-switch.tsx
+++ b/plane-src/apps/web/core/components/core/theme/theme-switch.tsx
@@ -6,7 +6,7 @@
// plane imports
import type { I_THEME_OPTION } from "@plane/constants";
-import { THEME_OPTIONS } from "@plane/constants";
+import { NODEDC_ACTIVE_THEME_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// constants
import { SelectionDropdown } from "@/components/common/selection-dropdown";
@@ -17,34 +17,34 @@ type Props = {
onChange: (value: I_THEME_OPTION) => void;
};
+const renderThemeSwatch = (themeOption: I_THEME_OPTION) => (
+
+);
+
export function ThemeSwitch(props: Props) {
const { value, onChange } = props;
// translation
const { t } = useTranslation();
- const renderThemeSwatch = (themeOption: I_THEME_OPTION) => (
-
- );
-
return (
({
+ options={NODEDC_ACTIVE_THEME_OPTIONS.map((themeOption) => ({
key: themeOption.value,
isChecked: value?.value === themeOption.value,
onClick: () => onChange(themeOption),
diff --git a/plane-src/apps/web/core/components/onboarding/switch-account-modal.tsx b/plane-src/apps/web/core/components/onboarding/switch-account-modal.tsx
index aa80f8e..3a0942e 100644
--- a/plane-src/apps/web/core/components/onboarding/switch-account-modal.tsx
+++ b/plane-src/apps/web/core/components/onboarding/switch-account-modal.tsx
@@ -9,6 +9,7 @@ import React, { useState } from "react";
import { useTheme } from "next-themes";
import { ArrowRightLeft } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
+import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED } from "@plane/constants";
// ui
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
@@ -42,9 +43,10 @@ export function SwitchAccountModal(props: Props) {
await signOut()
.then(() => {
- setTheme("system");
+ setTheme(NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : "system");
router.push("/");
handleClose();
+ return undefined;
})
.catch(() =>
setToast({
diff --git a/plane-src/apps/web/core/components/power-k/config/preferences-commands.ts b/plane-src/apps/web/core/components/power-k/config/preferences-commands.ts
index b1a9e97..e98a5e3 100644
--- a/plane-src/apps/web/core/components/power-k/config/preferences-commands.ts
+++ b/plane-src/apps/web/core/components/power-k/config/preferences-commands.ts
@@ -8,6 +8,7 @@ import { useCallback } from "react";
import { useTheme } from "next-themes";
import { Calendar, Earth, Languages, Palette } from "lucide-react";
// plane imports
+import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { setToast, TOAST_TYPE } from "@plane/propel/toast";
import type { EStartOfTheWeek, TUserProfile } from "@plane/types";
@@ -29,8 +30,9 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
const handleUpdateTheme = useCallback(
async (newTheme: string) => {
- setTheme(newTheme);
- return updateUserTheme({ theme: newTheme })
+ const nextTheme = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : newTheme;
+ setTheme(nextTheme);
+ return updateUserTheme({ theme: nextTheme })
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
diff --git a/plane-src/apps/web/core/components/power-k/ui/pages/preferences/themes-menu.tsx b/plane-src/apps/web/core/components/power-k/ui/pages/preferences/themes-menu.tsx
index 6018fe2..95d6a35 100644
--- a/plane-src/apps/web/core/components/power-k/ui/pages/preferences/themes-menu.tsx
+++ b/plane-src/apps/web/core/components/power-k/ui/pages/preferences/themes-menu.tsx
@@ -8,7 +8,7 @@ import React, { useEffect, useState } from "react";
import { Command } from "cmdk";
import { observer } from "mobx-react";
// plane imports
-import { THEME_OPTIONS } from "@plane/constants";
+import { NODEDC_ACTIVE_THEME_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// local imports
import { PowerKModalCommandItem } from "../../modal/command-item";
@@ -33,7 +33,7 @@ export const PowerKPreferencesThemesMenu = observer(function PowerKPreferencesTh
return (
- {THEME_OPTIONS.map((theme) => (
+ {NODEDC_ACTIVE_THEME_OPTIONS.map((theme) => (
onSelect(theme.value)} label={t(theme.i18n_label)} />
))}
diff --git a/plane-src/apps/web/core/components/workspace/settings/codex-agent-api-settings.tsx b/plane-src/apps/web/core/components/workspace/settings/codex-agent-api-settings.tsx
index 7a191c2..69f2e45 100644
--- a/plane-src/apps/web/core/components/workspace/settings/codex-agent-api-settings.tsx
+++ b/plane-src/apps/web/core/components/workspace/settings/codex-agent-api-settings.tsx
@@ -137,7 +137,10 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
})
)
);
- const workspaceProjectGroups = projectAccessPayload?.workspaces ?? [];
+ const workspaceProjectGroups = useMemo(
+ () => projectAccessPayload?.workspaces ?? [],
+ [projectAccessPayload?.workspaces]
+ );
const projectOptions = useMemo(() => flattenProjectAccessOptions(workspaceProjectGroups), [workspaceProjectGroups]);
const effectiveSelectedProjectKey = selectedProjectKey || projectOptions[0]?.key || "";
const setupCards = useMemo(
@@ -735,6 +738,10 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
В строке Authorization замените только ВАШ_УНИКАЛЬНЫЙ_ТОКЕН на значение из{" "}
Agent token. Bearer и ndcag_ оставьте как есть.
+
+ Не используйте bearer_token_env_var, если переменная окружения не добавлена в macOS launchd:
+ Codex Desktop не увидит обычный export из shell-профиля.
+
@@ -743,6 +750,10 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
Сохраните
config.toml и перезапустите локальный Codex. После старта попросите Codex проверить
доступные проекты через
tasker_list_projects.
+
+ Если токен или endpoint временно недоступны, чат Codex все равно откроется, но MCP tools появятся только
+ после исправления подключения.
+
Правила работы и grants Codex получит сам через MCP по токену.
@@ -775,13 +786,13 @@ function buildCodexConfigSnippet(endpoint: string): string {
return `[mcp_servers.${CODEX_MCP_SERVER_NAME}]
url = "${endpoint}"
enabled = true
-required = true
+required = false
startup_timeout_sec = 20
tool_timeout_sec = 60
[mcp_servers.${CODEX_MCP_SERVER_NAME}.headers]
Authorization = "Bearer ndcag_ВАШ_УНИКАЛЬНЫЙ_ТОКЕН"
-Accept = "application/json, text/event-stream"
+Accept = "application/json"
"MCP-Protocol-Version" = "2025-06-18"`;
}
@@ -995,6 +1006,7 @@ function mergeTokens(primaryTokens: TCodexAgentToken[], secondaryTokens: TCodexA
const tokensById = new Map();
for (const token of primaryTokens) tokensById.set(token.id, token);
for (const token of secondaryTokens) tokensById.set(token.id, tokensById.get(token.id) ?? token);
+ // oxlint-disable-next-line unicorn/no-array-sort
return Array.from(tokensById.values()).sort(
(leftToken, rightToken) => new Date(rightToken.created_at).getTime() - new Date(leftToken.created_at).getTime()
);
@@ -1072,13 +1084,16 @@ function buildProjectGrantPayload(grantKeys: string[]): { workspace_slug: string
}
function getGrantedProjectKeys(grants: TCodexAgentGrant[]): string[] {
- return [
+ const grantedProjectKeys = [
...new Set(
grants
.filter((grant) => grant.project_id)
.map((grant) => buildProjectGrantKey(grant.workspace_slug, String(grant.project_id)))
),
- ].sort();
+ ];
+
+ // oxlint-disable-next-line unicorn/no-array-sort
+ return grantedProjectKeys.sort();
}
type TMergeAgentGrantsOptions = {
diff --git a/plane-src/apps/web/core/lib/wrappers/store-wrapper.tsx b/plane-src/apps/web/core/lib/wrappers/store-wrapper.tsx
index 1b848c4..dbee0d3 100644
--- a/plane-src/apps/web/core/lib/wrappers/store-wrapper.tsx
+++ b/plane-src/apps/web/core/lib/wrappers/store-wrapper.tsx
@@ -9,6 +9,7 @@ import { useEffect, useRef } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { useTheme } from "next-themes";
+import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED } from "@plane/constants";
import type { TLanguage } from "@plane/i18n";
import { DEFAULT_LANGUAGE, useTranslation } from "@plane/i18n";
// helpers
@@ -57,16 +58,13 @@ function StoreWrapper(props: TStoreWrapper) {
*/
useEffect(() => {
const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed");
- const localBoolValue = localValue ? (localValue === "true" ? true : false) : false;
+ const localBoolValue = localValue === "true";
if (localValue && sidebarCollapsed === undefined) toggleSidebar(localBoolValue);
}, [sidebarCollapsed, setTheme, toggleSidebar]);
/**
- * Effect 1: Initial theme sync from server (one-time only)
- *
- * This effect runs ONCE per user session to load theme from server.
- * After initial load, all theme changes are localStorage-driven (next-themes).
- * This prevents a feedback loop where server updates trigger UI updates in a cycle.
+ * Temporary NODE.DC theme lock: keep the operational dark palette active
+ * until the light theme gets a full pass.
*/
useEffect(() => {
const userId = userProfile?.id;
@@ -79,6 +77,16 @@ function StoreWrapper(props: TStoreWrapper) {
currentUserIdRef.current = userId;
}
+ if (NODEDC_THEME_LOCK_ENABLED) {
+ if (!userProfile?.id || hasInitializedThemeRef.current) {
+ return;
+ }
+
+ setTheme(NODEDC_FORCED_THEME);
+ hasInitializedThemeRef.current = true;
+ return;
+ }
+
// Only initialize theme from server on FIRST load for this user
if (!userProfile?.theme?.theme || hasInitializedThemeRef.current) {
return; // Skip if already initialized or no profile data
@@ -89,7 +97,7 @@ function StoreWrapper(props: TStoreWrapper) {
// Mark as initialized - prevents future syncs from server
hasInitializedThemeRef.current = true;
- }, [userProfile?.theme?.theme, setTheme]);
+ }, [userProfile?.id, userProfile?.theme?.theme, setTheme]);
/**
* Effect 2: Custom theme CSS application (runs on every change)
@@ -98,12 +106,21 @@ function StoreWrapper(props: TStoreWrapper) {
* the theme changes. It runs independently of the initial sync effect.
*/
useEffect(() => {
- if (!userProfile?.theme?.theme) return;
+ if (NODEDC_THEME_LOCK_ENABLED) {
+ clearCustomTheme();
+ return;
+ }
+
+ if (!userProfile?.theme?.theme) {
+ return;
+ }
const currentTheme = userProfile?.theme?.theme;
const previousTheme = previousThemeRef.current;
const themeData = userProfile?.theme;
+ if (!currentTheme) return;
+
// Apply custom theme if current theme is custom
if (currentTheme === "custom" && themeData.primary && themeData.background && themeData.darkPalette !== undefined) {
applyCustomTheme(themeData.primary, themeData.background, themeData.darkPalette ? "dark" : "light");
@@ -116,7 +133,7 @@ function StoreWrapper(props: TStoreWrapper) {
// Update previous theme for next comparison
previousThemeRef.current = currentTheme;
- }, [userProfile?.theme]);
+ }, [userProfile?.id, userProfile?.theme]);
useEffect(() => {
if (!userProfile?.id) return;
@@ -138,7 +155,7 @@ function StoreWrapper(props: TStoreWrapper) {
useEffect(() => {
if (!userProfile?.id) return;
changeLanguage((userProfile?.language as TLanguage) || DEFAULT_LANGUAGE);
- }, [userProfile?.language, changeLanguage]);
+ }, [userProfile?.id, userProfile?.language, changeLanguage]);
useEffect(() => {
if (!params) return;
diff --git a/plane-src/packages/constants/src/themes.ts b/plane-src/packages/constants/src/themes.ts
index 4c2cf21..028bb0a 100644
--- a/plane-src/packages/constants/src/themes.ts
+++ b/plane-src/packages/constants/src/themes.ts
@@ -4,7 +4,9 @@
* See the LICENSE file for details.
*/
-export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"];
+export const NODEDC_FORCED_THEME = "dark";
+export const NODEDC_THEME_LOCK_ENABLED = true;
+export const THEMES = ["light", NODEDC_FORCED_THEME, "light-contrast", "dark-contrast", "custom"];
export interface I_THEME_OPTION {
key: string;
@@ -43,7 +45,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
},
{
key: "dark",
- value: "dark",
+ value: NODEDC_FORCED_THEME,
i18n_label: "Dark",
type: "dark",
icon: {
@@ -86,3 +88,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
},
},
];
+
+export const NODEDC_ACTIVE_THEME_OPTIONS = NODEDC_THEME_LOCK_ENABLED
+ ? THEME_OPTIONS.filter((themeOption) => themeOption.value === NODEDC_FORCED_THEME)
+ : THEME_OPTIONS;