diff --git a/plane-src/apps/api/plane/app/urls/codex_agents.py b/plane-src/apps/api/plane/app/urls/codex_agents.py
index a50ca66..e6d1edc 100644
--- a/plane-src/apps/api/plane/app/urls/codex_agents.py
+++ b/plane-src/apps/api/plane/app/urls/codex_agents.py
@@ -7,6 +7,7 @@ from django.urls import path
from plane.app.views import (
CodexAgentDetailEndpoint,
CodexAgentGrantListEndpoint,
+ CodexAgentGrantReplaceEndpoint,
CodexAgentListEndpoint,
CodexAgentRevokeEndpoint,
CodexAgentSetupEndpoint,
@@ -36,6 +37,11 @@ urlpatterns = [
CodexAgentGrantListEndpoint.as_view(),
name="codex-agent-api-agent-grants",
),
+ path(
+ "workspaces//codex-agent-api/agents//grants/replace/",
+ CodexAgentGrantReplaceEndpoint.as_view(),
+ name="codex-agent-api-agent-grants-replace",
+ ),
path(
"workspaces//codex-agent-api/agents//tokens/",
CodexAgentTokenListEndpoint.as_view(),
diff --git a/plane-src/apps/api/plane/app/views/__init__.py b/plane-src/apps/api/plane/app/views/__init__.py
index 781df5f..130a8ed 100644
--- a/plane-src/apps/api/plane/app/views/__init__.py
+++ b/plane-src/apps/api/plane/app/views/__init__.py
@@ -177,6 +177,7 @@ from .api import ApiTokenEndpoint
from .codex_agents import (
CodexAgentDetailEndpoint,
CodexAgentGrantListEndpoint,
+ CodexAgentGrantReplaceEndpoint,
CodexAgentListEndpoint,
CodexAgentRevokeEndpoint,
CodexAgentSetupEndpoint,
diff --git a/plane-src/apps/api/plane/app/views/codex_agents.py b/plane-src/apps/api/plane/app/views/codex_agents.py
index 8f66603..32be6b3 100644
--- a/plane-src/apps/api/plane/app/views/codex_agents.py
+++ b/plane-src/apps/api/plane/app/views/codex_agents.py
@@ -139,6 +139,41 @@ def validate_project_in_workspace(workspace, project_id, user):
return None
+def validate_projects_in_workspace(workspace, project_ids, user):
+ if not isinstance(project_ids, list) or len(project_ids) == 0:
+ return Response(
+ {
+ "ok": False,
+ "error": "project_ids_required",
+ "message": "Select at least one project for Codex Agent grants.",
+ },
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ normalized_project_ids = []
+ for project_id in project_ids:
+ project_id = str(project_id or "").strip()
+ if not project_id or project_id in normalized_project_ids:
+ continue
+
+ project_error = validate_project_in_workspace(workspace, project_id, user)
+ if project_error is not None:
+ return project_error
+ normalized_project_ids.append(project_id)
+
+ if not normalized_project_ids:
+ return Response(
+ {
+ "ok": False,
+ "error": "project_ids_required",
+ "message": "Select at least one project for Codex Agent grants.",
+ },
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ return normalized_project_ids
+
+
def gateway_request(method, path, payload=None):
config, error_response = require_gateway_config()
if error_response is not None:
@@ -284,6 +319,34 @@ class CodexAgentGrantListEndpoint(CodexAgentEntitledEndpoint):
return gateway_request("POST", f"/api/internal/v1/owners/{owner_path(request.user)}/agents/{agent_path(agent_id)}/grants", payload)
+class CodexAgentGrantReplaceEndpoint(CodexAgentEntitledEndpoint):
+ @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
+ def post(self, request, slug, agent_id):
+ entitlement_error = self.require_entitlement(request, slug)
+ if entitlement_error is not None:
+ return entitlement_error
+
+ workspace, workspace_error = require_workspace(slug)
+ if workspace_error is not None:
+ return workspace_error
+
+ project_ids_or_error = validate_projects_in_workspace(workspace, request.data.get("project_ids"), request.user)
+ if isinstance(project_ids_or_error, Response):
+ return project_ids_or_error
+
+ payload = {
+ "workspace_slug": slug,
+ "project_ids": project_ids_or_error,
+ "scopes": request.data.get("scopes") or [],
+ "mode": request.data.get("mode") or "voluntary",
+ }
+ return gateway_request(
+ "POST",
+ f"/api/internal/v1/owners/{owner_path(request.user)}/agents/{agent_path(agent_id)}/grants/replace",
+ payload,
+ )
+
+
class CodexAgentTokenListEndpoint(CodexAgentEntitledEndpoint):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug, agent_id):
diff --git a/plane-src/apps/web/app/assets/favicon/apple-touch-icon.png b/plane-src/apps/web/app/assets/favicon/apple-touch-icon.png
index a631267..5078c01 100644
Binary files a/plane-src/apps/web/app/assets/favicon/apple-touch-icon.png and b/plane-src/apps/web/app/assets/favicon/apple-touch-icon.png differ
diff --git a/plane-src/apps/web/app/assets/favicon/favicon-16x16.png b/plane-src/apps/web/app/assets/favicon/favicon-16x16.png
index af59ef0..970564a 100644
Binary files a/plane-src/apps/web/app/assets/favicon/favicon-16x16.png and b/plane-src/apps/web/app/assets/favicon/favicon-16x16.png differ
diff --git a/plane-src/apps/web/app/assets/favicon/favicon-32x32.png b/plane-src/apps/web/app/assets/favicon/favicon-32x32.png
index 16a1271..b2f01c5 100644
Binary files a/plane-src/apps/web/app/assets/favicon/favicon-32x32.png and b/plane-src/apps/web/app/assets/favicon/favicon-32x32.png differ
diff --git a/plane-src/apps/web/app/assets/favicon/favicon.ico b/plane-src/apps/web/app/assets/favicon/favicon.ico
index 613b1a3..973f3b2 100644
Binary files a/plane-src/apps/web/app/assets/favicon/favicon.ico and b/plane-src/apps/web/app/assets/favicon/favicon.ico differ
diff --git a/plane-src/apps/web/app/assets/icons/icon-180x180.png b/plane-src/apps/web/app/assets/icons/icon-180x180.png
index e7142bc..5078c01 100644
Binary files a/plane-src/apps/web/app/assets/icons/icon-180x180.png and b/plane-src/apps/web/app/assets/icons/icon-180x180.png differ
diff --git a/plane-src/apps/web/app/assets/icons/icon-512x512.png b/plane-src/apps/web/app/assets/icons/icon-512x512.png
index 4c070d0..a6f47b9 100644
Binary files a/plane-src/apps/web/app/assets/icons/icon-512x512.png and b/plane-src/apps/web/app/assets/icons/icon-512x512.png differ
diff --git a/plane-src/apps/web/app/layout.tsx b/plane-src/apps/web/app/layout.tsx
index 81d1109..7ce90d1 100644
--- a/plane-src/apps/web/app/layout.tsx
+++ b/plane-src/apps/web/app/layout.tsx
@@ -14,13 +14,6 @@ import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants";
// helpers
import { cn } from "@plane/utils";
-// assets
-import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url";
-import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url";
-import faviconIco from "@/app/assets/favicon/favicon.ico?url";
-import icon180 from "@/app/assets/icons/icon-180x180.png?url";
-import icon512 from "@/app/assets/icons/icon-512x512.png?url";
-
// local
import { AppProvider } from "./provider";
@@ -62,10 +55,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
-
-
+
-
{/* Meta info for PWA */}
@@ -73,9 +64,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
-
-
-
+
+
+
diff --git a/plane-src/apps/web/app/root.tsx b/plane-src/apps/web/app/root.tsx
index 8bff47e..7e6ac34 100644
--- a/plane-src/apps/web/app/root.tsx
+++ b/plane-src/apps/web/app/root.tsx
@@ -14,11 +14,6 @@ import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants";
import { cn } from "@plane/utils";
// types
// assets
-import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url";
-import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url";
-import faviconIco from "@/app/assets/favicon/favicon.ico?url";
-import icon180 from "@/app/assets/icons/icon-180x180.png?url";
-import icon512 from "@/app/assets/icons/icon-512x512.png?url";
import ogImage from "@/app/assets/og-image.png?url";
import globalStyles from "@/styles/globals.css?url";
import type { Route } from "./+types/root";
@@ -95,13 +90,11 @@ const designConfigStyle = {
} as CSSProperties;
export const links: LinksFunction = () => [
- { rel: "icon", type: "image/png", sizes: "32x32", href: favicon32 },
- { rel: "icon", type: "image/png", sizes: "16x16", href: favicon16 },
- { rel: "shortcut icon", href: faviconIco },
+ { rel: "icon", type: "image/svg+xml", sizes: "any", href: "/favicon/icon-adaptive.svg?v=nodedc-adaptive-20260516" },
{ rel: "manifest", href: "/site.webmanifest.json" },
- { rel: "apple-touch-icon", href: icon512 },
- { rel: "apple-touch-icon", sizes: "180x180", href: icon180 },
- { rel: "apple-touch-icon", sizes: "512x512", href: icon512 },
+ { rel: "apple-touch-icon", href: "/apple-touch-icon.png" },
+ { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
+ { rel: "apple-touch-icon", sizes: "512x512", href: "/icons/icon-512x512.png" },
{ rel: "manifest", href: "/manifest.json" },
{ rel: "stylesheet", href: globalStyles },
{
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 d56c463..67cd119 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
@@ -6,7 +6,7 @@
import { type ChangeEvent, useMemo, useRef, useState } from "react";
import { observer } from "mobx-react";
-import { Bot, Check, Copy, KeyRound, Route, ShieldCheck } from "lucide-react";
+import { Bot, Check, ChevronDown, Copy, FolderKanban, KeyRound, Route, ShieldCheck } from "lucide-react";
import useSWR from "swr";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
@@ -20,6 +20,7 @@ import { ProjectService } from "@/services/project/project.service";
import {
WorkspaceCodexAgentService,
type TCodexAgent,
+ type TCodexAgentGrant,
type TCodexAgentSetupPacket,
type TCodexAgentToken,
} from "@/services/workspace-codex-agent.service";
@@ -59,10 +60,17 @@ type TProps = {
type TAgentSetupCard = {
agent: TCodexAgent;
+ grants: TCodexAgentGrant[];
setup?: TCodexAgentSetupPacket;
tokens: TCodexAgentToken[];
};
+type TProjectAccessOption = {
+ id: string;
+ identifier?: string | null;
+ name: string;
+};
+
export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSettingsContent(props: TProps) {
const { showHeading = true, workspaceSlug } = props;
const createAvatarInputRef = useRef(null);
@@ -76,6 +84,9 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
const [revealedTokens, setRevealedTokens] = useState>({});
const [updatingAgentIds, setUpdatingAgentIds] = useState>({});
const [creatingTokenAgentIds, setCreatingTokenAgentIds] = useState>({});
+ const [openProjectAccessAgentId, setOpenProjectAccessAgentId] = useState(null);
+ const [projectGrantDrafts, setProjectGrantDrafts] = useState>({});
+ const [savingProjectGrantAgentIds, setSavingProjectGrantAgentIds] = useState>({});
const { currentWorkspace } = useWorkspace();
const { data: nodedcWorkspacePolicy, isLoading } = useSWR(
workspaceSlug ? `NODEDC_WORKSPACE_POLICY_${workspaceSlug}` : null,
@@ -109,13 +120,15 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
async () =>
Promise.all(
activeAgents.map(async (agent) => {
- const [tokensPayload, setupPayload] = await Promise.all([
+ const [tokensPayload, setupPayload, grantsPayload] = await Promise.all([
codexAgentService.listTokens(workspaceSlug, agent.id),
codexAgentService.getSetup(workspaceSlug, agent.id),
+ codexAgentService.listGrants(workspaceSlug, agent.id),
]);
return {
agent,
+ grants: grantsPayload.grants,
setup: setupPayload.setup,
tokens: tokensPayload.tokens.filter((token) => token.status === "active"),
};
@@ -200,7 +213,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
display_name: displayName,
avatar_url: newAgentAvatarUrl,
});
- await codexAgentService.upsertGrant(workspaceSlug, createResponse.agent.id, {
+ const grantResponse = await codexAgentService.upsertGrant(workspaceSlug, createResponse.agent.id, {
project_id: effectiveSelectedProjectId,
scopes: TASK_AUTHOR_SCOPES,
mode: "voluntary",
@@ -213,7 +226,9 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
[tokenResponse.token_record.id]: tokenResponse.token,
}));
setCreatedSetupCards((currentCards) =>
- upsertSetupCardToken(currentCards, createResponse.agent, tokenResponse.token_record, tokenResponse.setup)
+ upsertSetupCardToken(currentCards, createResponse.agent, tokenResponse.token_record, tokenResponse.setup, [
+ grantResponse.grant,
+ ])
);
await mutateCodexAgents();
await mutateSetupCards();
@@ -243,8 +258,9 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
...currentTokens,
[tokenResponse.token_record.id]: tokenResponse.token,
}));
+ const currentGrants = setupCards.find((card) => card.agent.id === agent.id)?.grants ?? [];
setCreatedSetupCards((currentCards) =>
- upsertSetupCardToken(currentCards, agent, tokenResponse.token_record, tokenResponse.setup)
+ upsertSetupCardToken(currentCards, agent, tokenResponse.token_record, tokenResponse.setup, currentGrants)
);
await mutateSetupCards();
setToast({
@@ -263,6 +279,70 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
}
};
+ const handleToggleProjectGrant = (agentId: string, currentProjectIds: string[], projectId: string) => {
+ setProjectGrantDrafts((currentDrafts) => {
+ const currentDraftProjectIds = currentDrafts[agentId] ?? currentProjectIds;
+ const nextProjectIds = currentDraftProjectIds.includes(projectId)
+ ? currentDraftProjectIds.filter((currentProjectId) => currentProjectId !== projectId)
+ : [...currentDraftProjectIds, projectId];
+
+ return {
+ ...currentDrafts,
+ [agentId]: nextProjectIds,
+ };
+ });
+ };
+
+ const handleSaveProjectAccess = async (agent: TCodexAgent, selectedProjectIds: string[]) => {
+ const projectIds = [...new Set(selectedProjectIds.filter(Boolean))];
+ if (projectIds.length === 0) {
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Выберите project",
+ message: "У агента должен быть доступ хотя бы к одному project в workspace.",
+ });
+ return;
+ }
+
+ setSavingProjectGrantAgentIds((current) => ({ ...current, [agent.id]: true }));
+ try {
+ const grantsResponse = await codexAgentService.replaceProjectGrants(workspaceSlug, agent.id, {
+ project_ids: projectIds,
+ scopes: TASK_AUTHOR_SCOPES,
+ mode: "voluntary",
+ });
+ setCreatedSetupCards((currentCards) =>
+ currentCards.map((card) =>
+ card.agent.id === agent.id
+ ? {
+ ...card,
+ grants: mergeAgentGrants(card.grants, grantsResponse.grants, workspaceSlug),
+ }
+ : card
+ )
+ );
+ setProjectGrantDrafts((currentDrafts) => {
+ const nextDrafts = { ...currentDrafts };
+ delete nextDrafts[agent.id];
+ return nextDrafts;
+ });
+ await mutateSetupCards();
+ setToast({
+ type: TOAST_TYPE.SUCCESS,
+ title: "Доступы Codex обновлены",
+ message: "Agent token теперь работает только с выбранными projects.",
+ });
+ } catch (error: any) {
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Не удалось обновить доступы",
+ message: error?.message ?? error?.error ?? "Проверьте project membership и Gateway.",
+ });
+ } finally {
+ setSavingProjectGrantAgentIds((current) => ({ ...current, [agent.id]: false }));
+ }
+ };
+
const handleSaveAgentName = async (agent: TCodexAgent) => {
const displayName = getAgentDraftName(agentDraftNames, agent).trim();
if (!displayName) return;
@@ -431,6 +511,11 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
const isAgentDirty = draftName.trim() !== agent.display_name;
const setupCard = setupCards.find((card) => card.agent.id === agent.id);
const agentTokens = setupCard?.tokens ?? [];
+ const agentGrants = setupCard?.grants ?? [];
+ const currentProjectIds = getGrantedProjectIds(agentGrants, workspaceSlug);
+ const draftProjectIds = projectGrantDrafts[agent.id] ?? currentProjectIds;
+ const isProjectAccessOpen = openProjectAccessAgentId === agent.id;
+ const isSavingProjectAccess = savingProjectGrantAgentIds[agent.id] === true;
return (
@@ -513,9 +598,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
{areSetupCardsLoading && agentTokens.length === 0 ? (
-
- Загрузка токена...
-
+ Загрузка токена...
) : agentTokens.length > 0 ? (
{agentTokens.map((token) => {
@@ -550,6 +633,29 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
Токен ещё не выпущен. Нажмите «Новый токен», чтобы получить доступ для локального Codex.
)}
+
+ void handleSaveProjectAccess(agent, draftProjectIds)}
+ onToggleOpen={() => {
+ setOpenProjectAccessAgentId(isProjectAccessOpen ? null : agent.id);
+ setProjectGrantDrafts((currentDrafts) =>
+ currentDrafts[agent.id]
+ ? currentDrafts
+ : {
+ ...currentDrafts,
+ [agent.id]: currentProjectIds,
+ }
+ );
+ }}
+ onToggleProject={(projectId) =>
+ handleToggleProjectGrant(agent.id, currentProjectIds, projectId)
+ }
+ />
);
})
@@ -600,11 +706,11 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
1. Найдите config.toml
Windows: откройте файл через проводник или VS Code.
-
+
C:\Users\имя-пользователя\.codex\config.toml
macOS / Linux:
- ~/.codex/config.toml
+ ~/.codex/config.toml
Если файла нет — создайте его.
@@ -614,7 +720,7 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
Создайте пользовательскую переменную окружения {CODEX_TOKEN_ENV_VAR}, заменив токен из примера
на уникальный токен конкретного агента.
-
+
{CODEX_TOKEN_ENV_VAR}=ndcag_...
@@ -776,6 +882,109 @@ function AgentAvatarButton(props: {
);
}
+type TAgentProjectAccessPanelProps = {
+ currentProjectIds: string[];
+ draftProjectIds: string[];
+ isOpen: boolean;
+ isSaving: boolean;
+ onSave: () => void;
+ onToggleOpen: () => void;
+ onToggleProject: (projectId: string) => void;
+ projects: TProjectAccessOption[];
+};
+
+function AgentProjectAccessPanel(props: TAgentProjectAccessPanelProps) {
+ const isDirty = !areProjectSelectionsEqual(props.currentProjectIds, props.draftProjectIds);
+ const selectedCount = props.draftProjectIds.length;
+ const summary =
+ selectedCount === 0
+ ? "Нет выбранных projects"
+ : selectedCount === 1
+ ? "1 project выбран"
+ : `${selectedCount} projects выбрано`;
+
+ return (
+
+
+
+
+
+
+
+
Доступы к проектам
+
+ Выберите projects, куда этот agent token может читать и писать карточки.
+
+
+
+
+ {summary}
+
+
+
+
+ {props.isOpen && (
+
+ {props.projects.length > 0 ? (
+
+ {props.projects.map((project) => {
+ const isChecked = props.draftProjectIds.includes(project.id);
+
+ return (
+ props.onToggleProject(project.id)}
+ >
+
+ {project.name}
+ {project.identifier && (
+ {project.identifier}
+ )}
+
+
+
+
+
+ );
+ })}
+
+ ) : (
+
В workspace нет доступных projects.
+ )}
+
+
+
+ Сохранение заменяет grants текущего workspace: снятые галочки сразу отзывают доступ.
+
+
+ Сохранить доступы
+
+
+
+ )}
+
+ );
+}
+
function mergeSetupCards(persistedCards: TAgentSetupCard[], createdCards: TAgentSetupCard[]): TAgentSetupCard[] {
const cardsByAgentId = new Map();
@@ -792,6 +1001,7 @@ function mergeSetupCards(persistedCards: TAgentSetupCard[], createdCards: TAgent
cardsByAgentId.set(card.agent.id, {
agent: persistedCard.agent,
+ grants: mergeAgentGrants(persistedCard.grants, card.grants),
setup: persistedCard.setup ?? card.setup,
tokens: mergeTokens(persistedCard.tokens, card.tokens),
});
@@ -813,17 +1023,19 @@ function upsertSetupCardToken(
cards: TAgentSetupCard[],
agent: TCodexAgent,
token: TCodexAgentToken,
- setup?: TCodexAgentSetupPacket
+ setup?: TCodexAgentSetupPacket,
+ grants: TCodexAgentGrant[] = []
): TAgentSetupCard[] {
const existingCard = cards.find((card) => card.agent.id === agent.id);
if (!existingCard) {
- return [{ agent, setup, tokens: [token] }, ...cards];
+ return [{ agent, grants, setup, tokens: [token] }, ...cards];
}
return cards.map((card) =>
card.agent.id === agent.id
? {
agent,
+ grants: mergeAgentGrants(card.grants, grants),
setup: setup ?? card.setup,
tokens: mergeTokens([token], card.tokens),
}
@@ -831,6 +1043,51 @@ function upsertSetupCardToken(
);
}
+function getGrantedProjectIds(grants: TCodexAgentGrant[], workspaceSlug: string): string[] {
+ return [
+ ...new Set(
+ grants
+ .filter((grant) => grant.workspace_slug === workspaceSlug && grant.project_id)
+ .map((grant) => String(grant.project_id))
+ ),
+ ].sort();
+}
+
+function mergeAgentGrants(
+ currentGrants: TCodexAgentGrant[],
+ nextGrants: TCodexAgentGrant[],
+ workspaceSlug?: string
+): TCodexAgentGrant[] {
+ const grantsByKey = new Map();
+
+ for (const grant of currentGrants) {
+ if (workspaceSlug && grant.workspace_slug === workspaceSlug) continue;
+ grantsByKey.set(buildGrantKey(grant), grant);
+ }
+
+ for (const grant of nextGrants) {
+ grantsByKey.set(buildGrantKey(grant), grant);
+ }
+
+ return Array.from(grantsByKey.values());
+}
+
+function buildGrantKey(grant: TCodexAgentGrant): string {
+ return `${grant.workspace_slug}:${grant.project_id ?? "*"}`;
+}
+
+function areProjectSelectionsEqual(leftProjectIds: string[], rightProjectIds: string[]): boolean {
+ const leftSet = new Set(leftProjectIds);
+ const rightSet = new Set(rightProjectIds);
+ if (leftSet.size !== rightSet.size) return false;
+
+ for (const projectId of leftSet) {
+ if (!rightSet.has(projectId)) return false;
+ }
+
+ return true;
+}
+
function readAvatarDataUrl(file: File): Promise {
if (!file.type.startsWith("image/")) {
return Promise.reject(new Error("Поддерживаются только изображения PNG, JPG, WEBP или GIF."));
diff --git a/plane-src/apps/web/core/services/workspace-codex-agent.service.ts b/plane-src/apps/web/core/services/workspace-codex-agent.service.ts
index e9a298d..ac346fb 100644
--- a/plane-src/apps/web/core/services/workspace-codex-agent.service.ts
+++ b/plane-src/apps/web/core/services/workspace-codex-agent.service.ts
@@ -72,6 +72,11 @@ export type TCodexAgentTokenListResponse = {
tokens: TCodexAgentToken[];
};
+export type TCodexAgentGrantListResponse = {
+ ok: boolean;
+ grants: TCodexAgentGrant[];
+};
+
export type TCodexAgentSetupResponse = {
ok: boolean;
setup?: TCodexAgentSetupPacket;
@@ -129,6 +134,30 @@ export class WorkspaceCodexAgentService extends APIService {
});
}
+ async listGrants(workspaceSlug: string, agentId: string): Promise {
+ return this.get(`/api/workspaces/${workspaceSlug}/codex-agent-api/agents/${agentId}/grants/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async replaceProjectGrants(
+ workspaceSlug: string,
+ agentId: string,
+ data: {
+ mode?: TCodexAgentGrantMode;
+ project_ids: string[];
+ scopes: string[];
+ }
+ ): Promise {
+ return this.post(`/api/workspaces/${workspaceSlug}/codex-agent-api/agents/${agentId}/grants/replace/`, data)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
async createToken(
workspaceSlug: string,
agentId: string,
diff --git a/plane-src/apps/web/manifest.json b/plane-src/apps/web/manifest.json
index d081c52..b6469af 100644
--- a/plane-src/apps/web/manifest.json
+++ b/plane-src/apps/web/manifest.json
@@ -1,7 +1,7 @@
{
- "theme_color": "#3579f6",
- "background_color": "#ffffff",
- "display": "standalone",
+ "theme_color": "#eeeff4",
+ "background_color": "#eeeff4",
+ "display": "browser",
"scope": "/",
"start_url": "/",
"name": "NODE.DC | Self-hosted task management workspace.",
@@ -9,22 +9,17 @@
"description": "NODE.DC streamlines task management, projects, and internal workflows.",
"icons": [
{
- "src": "/icon-192x192.png",
+ "src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
- "src": "/icon-256x256.png",
- "sizes": "256x256",
+ "src": "/icons/icon-348x348.png",
+ "sizes": "348x348",
"type": "image/png"
},
{
- "src": "/icon-384x384.png",
- "sizes": "384x384",
- "type": "image/png"
- },
- {
- "src": "/icon-512x512.png",
+ "src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
diff --git a/plane-src/apps/web/public/apple-touch-icon.png b/plane-src/apps/web/public/apple-touch-icon.png
new file mode 100644
index 0000000..5078c01
Binary files /dev/null and b/plane-src/apps/web/public/apple-touch-icon.png differ
diff --git a/plane-src/apps/web/public/favicon.ico b/plane-src/apps/web/public/favicon.ico
new file mode 100644
index 0000000..973f3b2
Binary files /dev/null and b/plane-src/apps/web/public/favicon.ico differ
diff --git a/plane-src/apps/web/public/favicon.svg b/plane-src/apps/web/public/favicon.svg
new file mode 100644
index 0000000..fee636f
--- /dev/null
+++ b/plane-src/apps/web/public/favicon.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/plane-src/apps/web/public/favicon/android-chrome-192x192.png b/plane-src/apps/web/public/favicon/android-chrome-192x192.png
index 4a005e5..d553a58 100644
Binary files a/plane-src/apps/web/public/favicon/android-chrome-192x192.png and b/plane-src/apps/web/public/favicon/android-chrome-192x192.png differ
diff --git a/plane-src/apps/web/public/favicon/android-chrome-512x512.png b/plane-src/apps/web/public/favicon/android-chrome-512x512.png
index 27fafe8..a6f47b9 100644
Binary files a/plane-src/apps/web/public/favicon/android-chrome-512x512.png and b/plane-src/apps/web/public/favicon/android-chrome-512x512.png differ
diff --git a/plane-src/apps/web/public/favicon/apple-touch-icon.png b/plane-src/apps/web/public/favicon/apple-touch-icon.png
new file mode 100644
index 0000000..5078c01
Binary files /dev/null and b/plane-src/apps/web/public/favicon/apple-touch-icon.png differ
diff --git a/plane-src/apps/web/public/favicon/favicon.ico b/plane-src/apps/web/public/favicon/favicon.ico
new file mode 100644
index 0000000..973f3b2
Binary files /dev/null and b/plane-src/apps/web/public/favicon/favicon.ico differ
diff --git a/plane-src/apps/web/public/favicon/icon-adaptive.svg b/plane-src/apps/web/public/favicon/icon-adaptive.svg
new file mode 100644
index 0000000..fee636f
--- /dev/null
+++ b/plane-src/apps/web/public/favicon/icon-adaptive.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/plane-src/apps/web/public/favicon/site.webmanifest b/plane-src/apps/web/public/favicon/site.webmanifest
index 1d41057..17ac181 100644
--- a/plane-src/apps/web/public/favicon/site.webmanifest
+++ b/plane-src/apps/web/public/favicon/site.webmanifest
@@ -1,11 +1,15 @@
{
- "name": "",
- "short_name": "",
+ "name": "NODE.DC",
+ "short_name": "NODE.DC",
+ "theme_color": "#eeeff4",
+ "background_color": "#eeeff4",
+ "display": "browser",
+ "scope": "/",
+ "start_url": "/",
"icons": [
+ { "src": "/favicon/icon-adaptive.svg", "sizes": "any", "type": "image/svg+xml" },
{ "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
- { "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
- ],
- "theme_color": "#ffffff",
- "background_color": "#ffffff",
- "display": "standalone"
+ { "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" },
+ { "src": "/favicon/apple-touch-icon.png", "sizes": "180x180", "type": "image/png" }
+ ]
}
diff --git a/plane-src/apps/web/public/icons/icon-192x192.png b/plane-src/apps/web/public/icons/icon-192x192.png
index 1654263..d553a58 100644
Binary files a/plane-src/apps/web/public/icons/icon-192x192.png and b/plane-src/apps/web/public/icons/icon-192x192.png differ
diff --git a/plane-src/apps/web/public/icons/icon-348x348.png b/plane-src/apps/web/public/icons/icon-348x348.png
index c457397..42c8695 100644
Binary files a/plane-src/apps/web/public/icons/icon-348x348.png and b/plane-src/apps/web/public/icons/icon-348x348.png differ
diff --git a/plane-src/apps/web/public/icons/icon-512x512.png b/plane-src/apps/web/public/icons/icon-512x512.png
index 4c070d0..a6f47b9 100644
Binary files a/plane-src/apps/web/public/icons/icon-512x512.png and b/plane-src/apps/web/public/icons/icon-512x512.png differ
diff --git a/plane-src/apps/web/public/manifest.json b/plane-src/apps/web/public/manifest.json
index dc76803..00ffc5a 100644
--- a/plane-src/apps/web/public/manifest.json
+++ b/plane-src/apps/web/public/manifest.json
@@ -19,8 +19,8 @@
"type": "image/png"
}
],
- "theme_color": "#FFFFFF",
- "background_color": "#FFFFFF",
+ "theme_color": "#eeeff4",
+ "background_color": "#eeeff4",
"start_url": "/",
"display": "standalone",
"orientation": "portrait"
diff --git a/plane-src/apps/web/public/site.webmanifest.json b/plane-src/apps/web/public/site.webmanifest.json
index e870fba..543c39e 100644
--- a/plane-src/apps/web/public/site.webmanifest.json
+++ b/plane-src/apps/web/public/site.webmanifest.json
@@ -2,12 +2,14 @@
"name": "NODE.DC",
"short_name": "NODE.DC",
"description": "NODE.DC helps you manage work items, projects, and operational workflows.",
- "start_url": ".",
- "display": "standalone",
- "background_color": "#f9fafb",
- "theme_color": "#3f76ff",
+ "start_url": "/",
+ "display": "browser",
+ "background_color": "#eeeff4",
+ "theme_color": "#eeeff4",
"icons": [
- { "src": "/plane-logos/plane-mobile-pwa.png", "sizes": "192x192", "type": "image/png" },
- { "src": "/plane-logos/plane-mobile-pwa.png", "sizes": "512x512", "type": "image/png" }
+ { "src": "/favicon/icon-adaptive.svg", "sizes": "any", "type": "image/svg+xml" },
+ { "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" },
+ { "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" },
+ { "src": "/favicon/apple-touch-icon.png", "sizes": "180x180", "type": "image/png" }
]
}
diff --git a/plane-src/apps/web/styles/globals.css b/plane-src/apps/web/styles/globals.css
index da8d5ba..3cd9239 100644
--- a/plane-src/apps/web/styles/globals.css
+++ b/plane-src/apps/web/styles/globals.css
@@ -2234,6 +2234,30 @@
rgba(255, 255, 255, 0.042) !important;
}
+ .nodedc-project-grants-surface {
+ background: rgba(0, 0, 0, 0.1) !important;
+ border: 0 !important;
+ outline: none !important;
+ box-shadow: none !important;
+ }
+
+ .nodedc-project-grants-row,
+ .nodedc-project-grants-row:hover,
+ .nodedc-project-grants-row:focus,
+ .nodedc-project-grants-row:focus-visible,
+ .nodedc-project-grants-row:active {
+ background: transparent !important;
+ border: 0 !important;
+ outline: none !important;
+ box-shadow: none !important;
+ }
+
+ .nodedc-project-grants-check {
+ border: 0 !important;
+ outline: none !important;
+ box-shadow: none !important;
+ }
+
.nodedc-settings-primary-button {
min-height: 2.75rem;
border: 0 !important;