FEAT - TASKER: показ Codex Agent API по Launcher entitlement

This commit is contained in:
DCCONSTRUCTIONS 2026-05-14 20:49:52 +03:00
parent 2ae353c8d5
commit 97566faba3
13 changed files with 276 additions and 34 deletions

View File

@ -25,6 +25,7 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
"default_managed_by": "tasker",
"invite_approval": "tasker",
"default_invite_approval": "tasker",
"service_modules": {},
"workspaces": [],
"reason": "NODE.DC workspace policy is not configured.",
}
@ -45,6 +46,7 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
"default_managed_by": "tasker",
"invite_approval": "tasker",
"default_invite_approval": "tasker",
"service_modules": {},
"workspaces": [],
"reason": "NODE.DC identity is not linked." if enforce_unlinked else "Standalone user without NODE.DC identity.",
}
@ -75,11 +77,18 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
"default_managed_by": "tasker",
"invite_approval": "disabled",
"default_invite_approval": "tasker",
"service_modules": {},
"workspaces": [],
"reason": "NODE.DC workspace policy is unavailable.",
}
workspace_policy = payload.get("workspacePolicy") if isinstance(payload.get("workspacePolicy"), dict) else {}
service_modules = normalize_service_modules(
workspace_policy.get("serviceModules")
or workspace_policy.get("service_modules")
or payload.get("serviceModules")
or payload.get("service_modules")
)
access_allowed = bool(payload.get("allowed"))
if not workspace_policy:
return {
@ -90,6 +99,7 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
"default_managed_by": "tasker",
"invite_approval": "tasker",
"default_invite_approval": "tasker",
"service_modules": service_modules,
"workspaces": [],
"reason": payload.get("reason") or "NODE.DC access check does not expose workspace policy.",
}
@ -117,6 +127,7 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
"default_managed_by": normalize_managed_by(workspace_policy.get("defaultManagedBy") or workspace_policy.get("managedBy")),
"invite_approval": invite_approval,
"default_invite_approval": default_invite_approval,
"service_modules": service_modules,
"workspaces": workspaces,
"reason": workspace_policy.get("reason") or payload.get("reason") or "NODE.DC workspace policy decision.",
}
@ -159,6 +170,18 @@ def normalize_workspace_management_list(value):
return workspaces
def normalize_service_modules(value):
if not isinstance(value, dict):
return {}
service_modules = {}
for module_key in ("codex_agents",):
if value.get(module_key) is True:
service_modules[module_key] = True
return service_modules
def resolve_workspace_managed_by(workspace_slug, workspaces, fallback):
if isinstance(workspace_slug, str) and workspace_slug.strip():
normalized_slug = workspace_slug.strip()

View File

@ -0,0 +1,12 @@
import { redirect } from "react-router";
// local imports
import type { Route } from "./+types/page";
export function clientLoader({ params }: Route.ClientLoaderArgs) {
const { workspaceSlug } = params;
throw redirect(`/${workspaceSlug}/?workspaceSettings=codex-agent-api`);
}
export default function CodexAgentApiSettingsPage() {
return null;
}

View File

@ -297,6 +297,10 @@ export const coreRoutes: RouteConfigEntry[] = [
":workspaceSlug/settings/ai-voice-tasker",
"./(all)/[workspaceSlug]/(settings)/settings/(workspace)/ai-voice-tasker/page.tsx"
),
route(
":workspaceSlug/settings/codex-agent-api",
"./(all)/[workspaceSlug]/(settings)/settings/(workspace)/codex-agent-api/page.tsx"
),
]),
// --------------------------------------------------------------------

View File

@ -9,7 +9,12 @@ import { usePathname } from "next/navigation";
import { useParams } from "react-router";
import useSWR from "swr";
// plane imports
import { EUserPermissionsLevel, GROUPED_WORKSPACE_SETTINGS, WORKSPACE_SETTINGS, WORKSPACE_SETTINGS_CATEGORIES } from "@plane/constants";
import {
EUserPermissionsLevel,
GROUPED_WORKSPACE_SETTINGS,
WORKSPACE_SETTINGS,
WORKSPACE_SETTINGS_CATEGORIES,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import type { TWorkspaceSettingsTabs } from "@plane/types";
import { joinUrlPath } from "@plane/utils";
@ -19,12 +24,14 @@ import { SettingsSidebarItem } from "@/components/settings/sidebar/item";
import { useUserPermissions } from "@/hooks/store/user";
// services
import { WorkspaceAIService } from "@/services/workspace-ai.service";
import { WorkspaceService } from "@/services/workspace.service";
// local imports
import { WORKSPACE_SETTINGS_ICONS } from "./item-icon";
const HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set(["billing-and-plans"]);
const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["ai-voice-tasker"]);
const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["ai-voice-tasker", "codex-agent-api"]);
const workspaceAIService = new WorkspaceAIService();
const workspaceService = new WorkspaceService();
export const WorkspaceSettingsSidebarItemCategories = observer(function WorkspaceSettingsSidebarItemCategories() {
// params
@ -42,7 +49,12 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac
canLoadVoiceTaskerEntitlement ? `WORKSPACE_AI_SETTINGS_${workspaceSlug}` : null,
() => workspaceAIService.retrieveSettings(workspaceSlug as string)
);
const { data: nodedcWorkspacePolicy } = useSWR(
workspaceSlug ? `NODEDC_WORKSPACE_POLICY_${workspaceSlug}` : null,
() => workspaceService.getNodeDCWorkspacePolicy(workspaceSlug as string)
);
const isVoiceTaskerEntitled = aiSettings?.feature_entitlement_enabled === true;
const isCodexAgentEntitled = nodedcWorkspacePolicy?.service_modules?.codex_agents === true;
return (
<div className="mt-4 flex flex-col divide-y divide-white/6">
@ -51,7 +63,11 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac
const accessibleItems = categoryItems.filter(
(item) =>
!HIDDEN_WORKSPACE_SETTINGS_KEYS.has(item.key) &&
(!WORKSPACE_FEATURE_GATED_SETTINGS_KEYS.has(item.key) || isVoiceTaskerEntitled) &&
(!WORKSPACE_FEATURE_GATED_SETTINGS_KEYS.has(item.key) ||
isWorkspaceFeatureSettingsEntitled(item.key, {
isCodexAgentEntitled,
isVoiceTaskerEntitled,
})) &&
allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug)
);
@ -59,7 +75,7 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac
return (
<div key={category} className="shrink-0 py-3.5 first:pt-0 last:pb-0">
<div className="px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-tertiary">
<div className="px-3 py-1.5 text-[11px] font-semibold tracking-[0.18em] text-tertiary uppercase">
{t(category)}
</div>
<div className="flex flex-col">
@ -87,3 +103,16 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac
</div>
);
});
function isWorkspaceFeatureSettingsEntitled(
itemKey: TWorkspaceSettingsTabs,
entitlements: {
isCodexAgentEntitled: boolean;
isVoiceTaskerEntitled: boolean;
}
) {
if (itemKey === "ai-voice-tasker") return entitlements.isVoiceTaskerEntitled;
if (itemKey === "codex-agent-api") return entitlements.isCodexAgentEntitled;
return true;
}

View File

@ -5,7 +5,7 @@
*/
import type { LucideIcon } from "lucide-react";
import { ArrowUpToLine, Building, CreditCard, Database, Mic, Users, Webhook } from "lucide-react";
import { ArrowUpToLine, Bot, Building, CreditCard, Database, Mic, Users, Webhook } from "lucide-react";
// plane imports
import type { ISvgIcons } from "@plane/propel/icons";
import type { TWorkspaceSettingsTabs } from "@plane/types";
@ -18,4 +18,5 @@ export const WORKSPACE_SETTINGS_ICONS: Record<TWorkspaceSettingsTabs, LucideIcon
storage: Database,
webhooks: Webhook,
"ai-voice-tasker": Mic,
"codex-agent-api": Bot,
};

View File

@ -0,0 +1,108 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import { Bot, Check, KeyRound, Route, ShieldCheck } from "lucide-react";
import useSWR from "swr";
// components
import { SettingsHeading } from "@/components/settings/heading";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
// services
import { WorkspaceService } from "@/services/workspace.service";
const workspaceService = new WorkspaceService();
type TProps = {
showHeading?: boolean;
workspaceSlug: string;
};
export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSettingsContent(props: TProps) {
const { showHeading = true, workspaceSlug } = props;
const { currentWorkspace } = useWorkspace();
const { data: nodedcWorkspacePolicy, isLoading } = useSWR(
workspaceSlug ? `NODEDC_WORKSPACE_POLICY_${workspaceSlug}` : null,
() => workspaceService.getNodeDCWorkspacePolicy(workspaceSlug)
);
const isCodexAgentEntitled = nodedcWorkspacePolicy?.service_modules?.codex_agents === true;
return (
<div className="flex w-full flex-col gap-7">
{showHeading && (
<SettingsHeading
title="Codex Agent API"
description="Workspace-level вход в отдельный NODE.DC Agent Gateway. Внешний Codex получает только ограниченные agent grants, а не Plane session cookies или прямой Tasker API."
/>
)}
{isLoading ? (
<div className="nodedc-settings-card text-sm px-5 py-5 text-secondary">Загрузка статуса модуля...</div>
) : (
<>
<section className="nodedc-settings-card overflow-hidden">
<div className="flex flex-col gap-4 px-5 py-5 md:flex-row md:items-start md:justify-between">
<div className="min-w-0">
<div className="flex items-center gap-2 text-16 font-semibold text-primary">
<Bot className="size-5 text-tertiary" />
<span>Agent Gateway для {currentWorkspace?.name ?? workspaceSlug}</span>
</div>
<p className="mt-2 max-w-3xl text-13 leading-5 text-secondary">
Доступ к модулю приходит из Launcher entitlement Operational Core Codex Agent API. Если entitlement
снят, этот раздел исчезает из настроек workspace и backend policy больше не возвращает активный модуль.
</p>
</div>
<div className="nodedc-external-readonly-value shrink-0">
<span className="grid size-5 place-items-center rounded-full bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))]">
<Check className="size-3.5" />
</span>
<span>{isCodexAgentEntitled ? "Доступ выдан" : "Доступ не выдан"}</span>
</div>
</div>
</section>
<section className="grid gap-4 md:grid-cols-3">
<CapabilityCard
icon={ShieldCheck}
title="Граница прав"
description="Агент работает только в workspace/project grants и не получает права на удаление карточек, проектов, участников или состояний."
/>
<CapabilityCard
icon={Route}
title="Маршрутизация"
description="Все write-действия идут через отдельный Gateway и Tasker internal adapter, с audit trail и idempotency key."
/>
<CapabilityCard
icon={KeyRound}
title="Локальный Codex"
description="Пользовательский Codex подключается по MCP endpoint с agent token; token хранится только на стороне Gateway."
/>
</section>
</>
)}
</div>
);
});
type TCapabilityCardProps = {
description: string;
icon: typeof ShieldCheck;
title: string;
};
function CapabilityCard(props: TCapabilityCardProps) {
const Icon = props.icon;
return (
<div className="nodedc-settings-card px-5 py-5">
<div className="flex items-center gap-2 text-14 font-semibold text-primary">
<Icon className="size-4 text-tertiary" />
<span>{props.title}</span>
</div>
<p className="mt-3 text-13 leading-5 text-secondary">{props.description}</p>
</div>
);
}

View File

@ -25,6 +25,7 @@ import { SettingsSidebarItem } from "@/components/settings/sidebar/item";
import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon";
import { WorkspaceSettingsSidebarHeader } from "@/components/settings/workspace/sidebar/header";
import { AIVoiceTaskerSettingsContent } from "@/components/workspace/settings/ai-voice-tasker-settings";
import { CodexAgentApiSettingsContent } from "@/components/workspace/settings/codex-agent-api-settings";
import { WorkspaceExportsSettingsContent } from "@/components/workspace/settings/exports-settings";
import { WorkspaceMembersSettingsContent } from "@/components/workspace/settings/members-settings";
import { StorageSettingsContent } from "@/components/workspace/settings/storage-settings";
@ -48,7 +49,7 @@ import {
const HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["billing-and-plans"]);
const LAUNCHER_MANAGED_HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["members"]);
const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["ai-voice-tasker"]);
const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["ai-voice-tasker", "codex-agent-api"]);
const MODAL_TABS = new Set<TWorkspaceSettingsTabs>([
"general",
"members",
@ -56,6 +57,7 @@ const MODAL_TABS = new Set<TWorkspaceSettingsTabs>([
"storage",
"webhooks",
"ai-voice-tasker",
"codex-agent-api",
]);
const workspaceAIService = new WorkspaceAIService();
const workspaceService = new WorkspaceService();
@ -99,6 +101,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
);
const isVoiceTaskerEntitled = aiSettings?.feature_entitlement_enabled === true;
const isLauncherManagedWorkspace = nodedcWorkspacePolicy?.managed_by === "launcher";
const isCodexAgentEntitled = nodedcWorkspacePolicy?.service_modules?.codex_agents === true;
useEffect(() => {
const syncFromLocation = () => {
@ -136,6 +139,11 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
if (!isVoiceTaskerEntitled) openWorkspaceSettingsModal("general", true);
}, [activeTab, isOpen, isVoiceTaskerEntitlementLoading, isVoiceTaskerEntitled]);
useEffect(() => {
if (!isOpen || activeTab !== "codex-agent-api" || !nodedcWorkspacePolicy) return;
if (!isCodexAgentEntitled) openWorkspaceSettingsModal("general", true);
}, [activeTab, isCodexAgentEntitled, isOpen, nodedcWorkspacePolicy]);
useEffect(() => {
if (!isOpen || activeTab !== "members" || !isLauncherManagedWorkspace) return;
openWorkspaceSettingsModal("general", true);
@ -162,6 +170,11 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
return <AIVoiceTaskerSettingsContent workspaceSlug={currentWorkspace.slug} />;
}
if (activeTab === "codex-agent-api" && currentWorkspace?.slug) {
if (!isCodexAgentEntitled) return <WorkspaceDetails />;
return <CodexAgentApiSettingsContent workspaceSlug={currentWorkspace.slug} />;
}
if (activeTab === "members" && currentWorkspace?.slug) {
return <WorkspaceMembersSettingsContent workspaceSlug={currentWorkspace.slug} />;
}
@ -204,6 +217,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
allowPermissions={allowPermissions}
isVoiceTaskerEntitled={isVoiceTaskerEntitled}
isLauncherManagedWorkspace={isLauncherManagedWorkspace}
isCodexAgentEntitled={isCodexAgentEntitled}
workspaceSlug={currentWorkspace?.slug}
/>
</div>
@ -238,6 +252,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
type TWorkspaceModalSidebarProps = {
activeTab: TWorkspaceSettingsModalTab;
allowPermissions: ReturnType<typeof useUserPermissions>["allowPermissions"];
isCodexAgentEntitled: boolean;
isLauncherManagedWorkspace: boolean;
isVoiceTaskerEntitled: boolean;
onSelectItem: (itemKey: TWorkspaceSettingsTabs, itemHref: string) => void;
@ -247,6 +262,7 @@ type TWorkspaceModalSidebarProps = {
function WorkspaceModalSidebar({
activeTab,
allowPermissions,
isCodexAgentEntitled,
isLauncherManagedWorkspace,
isVoiceTaskerEntitled,
onSelectItem,
@ -267,7 +283,11 @@ function WorkspaceModalSidebar({
(item) =>
!HIDDEN_WORKSPACE_SETTINGS_KEYS.has(item.key) &&
(!isLauncherManagedWorkspace || !LAUNCHER_MANAGED_HIDDEN_WORKSPACE_SETTINGS_KEYS.has(item.key)) &&
(!WORKSPACE_FEATURE_GATED_SETTINGS_KEYS.has(item.key) || isVoiceTaskerEntitled) &&
(!WORKSPACE_FEATURE_GATED_SETTINGS_KEYS.has(item.key) ||
isWorkspaceFeatureSettingsEntitled(item.key, {
isCodexAgentEntitled,
isVoiceTaskerEntitled,
})) &&
allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug)
);
@ -302,3 +322,16 @@ function WorkspaceModalSidebar({
</ScrollArea>
);
}
function isWorkspaceFeatureSettingsEntitled(
itemKey: TWorkspaceSettingsTabs,
entitlements: {
isCodexAgentEntitled: boolean;
isVoiceTaskerEntitled: boolean;
}
) {
if (itemKey === "ai-voice-tasker") return entitlements.isVoiceTaskerEntitled;
if (itemKey === "codex-agent-api") return entitlements.isCodexAgentEntitled;
return true;
}

View File

@ -3,7 +3,14 @@ export const WORKSPACE_SETTINGS_MODAL_EVENT = "nodedc:workspace-settings-modal";
export const WORKSPACE_SETTINGS_WEBHOOK_QUERY_KEY = "webhookId";
export type TWorkspaceSettingsModalTab = "general" | "members" | "export" | "storage" | "webhooks" | "ai-voice-tasker";
export type TWorkspaceSettingsModalTab =
| "general"
| "members"
| "export"
| "storage"
| "webhooks"
| "ai-voice-tasker"
| "codex-agent-api";
type TWorkspaceSettingsModalEventDetail = {
isOpen: boolean;
@ -23,7 +30,8 @@ export const getWorkspaceSettingsModalTabFromSearch = (search: string): TWorkspa
value === "export" ||
value === "storage" ||
value === "webhooks" ||
value === "ai-voice-tasker"
value === "ai-voice-tasker" ||
value === "codex-agent-api"
)
return value;

View File

@ -43,6 +43,9 @@ export interface NodeDCWorkspacePolicy {
default_managed_by: "launcher" | "tasker";
invite_approval: "tasker" | "nodedc" | "launcher" | "disabled";
default_invite_approval: "tasker" | "nodedc" | "launcher" | "disabled";
service_modules?: {
codex_agents?: boolean;
};
workspaces: Array<{
slug: string;
name: string | null;

View File

@ -70,6 +70,13 @@ export const WORKSPACE_SETTINGS: Record<TWorkspaceSettingsTabs, TWorkspaceSettin
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/ai-voice-tasker/`,
},
"codex-agent-api": {
key: "codex-agent-api",
i18n_label: "workspace_settings.settings.codex_agent_api.title",
href: `/settings/codex-agent-api`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/codex-agent-api/`,
},
};
export const WORKSPACE_SETTINGS_ACCESS = Object.fromEntries(
@ -84,6 +91,9 @@ export const GROUPED_WORKSPACE_SETTINGS: Record<WORKSPACE_SETTINGS_CATEGORY, TWo
WORKSPACE_SETTINGS["export"],
WORKSPACE_SETTINGS["storage"],
],
[WORKSPACE_SETTINGS_CATEGORY.FEATURES]: [WORKSPACE_SETTINGS["ai-voice-tasker"]],
[WORKSPACE_SETTINGS_CATEGORY.FEATURES]: [
WORKSPACE_SETTINGS["ai-voice-tasker"],
WORKSPACE_SETTINGS["codex-agent-api"],
],
[WORKSPACE_SETTINGS_CATEGORY.DEVELOPER]: [WORKSPACE_SETTINGS["webhooks"]],
};

View File

@ -325,11 +325,11 @@ export default {
incoming_description: "Requests routed into this contour from other projects will appear here.",
},
},
list: {
last_updated: "Last updated",
unassigned: "Unassigned",
unread_updates: "New updates available",
},
list: {
last_updated: "Last updated",
unassigned: "Unassigned",
unread_updates: "New updates available",
},
empty_state: {
title: "External contours module is ready for the next stage",
description:
@ -424,7 +424,8 @@ export default {
},
decline_modal: {
title: "Return the request for rework",
description: "Provide the reason for returning the request. This comment will be sent to the external contour and added to the target issue.",
description:
"Provide the reason for returning the request. This comment will be sent to the external contour and added to the target issue.",
placeholder: "Describe what needs to be revised or clarified",
submit: "Decline and return",
},
@ -1794,6 +1795,9 @@ export default {
ai_voice_tasker: {
title: "AI / Voice Tasker",
},
codex_agent_api: {
title: "Codex Agent API",
},
api_tokens: {
title: "Personal Access Tokens",
add_token: "Add personal access token",
@ -1991,7 +1995,8 @@ export default {
},
list_heading: "Estimate list",
archived_heading: "Archived estimates",
archived_description: "These are estimates from earlier project versions that are not currently in use. Read more",
archived_description:
"These are estimates from earlier project versions that are not currently in use. Read more",
no_estimate: "No estimate",
new: "New estimate system",
create: {
@ -2395,7 +2400,8 @@ export default {
project_page: {
delete_modal: {
title: "Delete page",
content: 'Are you sure you want to delete page "{value}"? The page will be permanently removed and this action cannot be undone.',
content:
'Are you sure you want to delete page "{value}"? The page will be permanently removed and this action cannot be undone.',
success_title: "Page deleted",
success_message: "Page deleted successfully.",
error_title: "Page delete failed",
@ -3108,24 +3114,24 @@ export default {
project_settings_label: "Project settings",
project_join_modal: {
title: "Join project?",
description: "Are you sure you want to join the project {project}? Click \"Join project\" to continue.",
description: 'Are you sure you want to join the project {project}? Click "Join project" to continue.',
submit: "Join project",
loading: "Joining...",
},
project_leave_modal: {
title: "Leave project",
description:
"Are you sure you want to leave the project \"{project}\"? All work items associated with you will become inaccessible.",
'Are you sure you want to leave the project "{project}"? All work items associated with you will become inaccessible.',
enter_project_name: "Enter the project name {project} to continue:",
project_name_placeholder: "Enter project name",
confirm_instruction: "To confirm, type {keyword} below:",
confirm_placeholder: "Enter \"leave project\"",
confirm_placeholder: 'Enter "leave project"',
confirm_keyword: "leave project",
loading: "Leaving...",
submit: "Leave project",
error_title: "Error!",
error_default: "Something went wrong. Please try again later.",
error_confirm: "Please confirm leaving the project by typing \"leave project\".",
error_confirm: 'Please confirm leaving the project by typing "leave project".',
error_name: "Please enter the project name exactly as shown in the description.",
error_fields: "Please fill all fields.",
},
@ -3136,7 +3142,7 @@ export default {
enter_project_name: "Enter the project name {project} to continue:",
project_name_placeholder: "Project name",
confirm_instruction: "To confirm, type {keyword} below:",
confirm_placeholder: "Enter \"delete my project\"",
confirm_placeholder: 'Enter "delete my project"',
confirm_keyword: "delete my project",
loading: "Deleting",
submit: "Delete project",
@ -3151,7 +3157,7 @@ export default {
type_workspace_name: "Type this workspace name to continue.",
final_confirmation: "For final confirmation, type {keyword} below.",
confirm_keyword: "delete my workspace",
input_placeholder: "Enter \"delete my workspace\"",
input_placeholder: 'Enter "delete my workspace"',
},
project_invitation_modal: {
success_title: "Success!",

View File

@ -482,11 +482,11 @@ export default {
incoming_description: "Здесь будут видны запросы, которые пришли в этот контур из других проектов.",
},
},
list: {
last_updated: "Последнее изменение",
unassigned: "Не назначено",
unread_updates: "Есть новые изменения",
},
list: {
last_updated: "Последнее изменение",
unassigned: "Не назначено",
unread_updates: "Есть новые изменения",
},
empty_state: {
title: "Модуль внешних контуров подготовлен",
description:
@ -552,7 +552,8 @@ export default {
},
traceability: {
title: "Маршрутизация",
description: "Здесь отображается, из какого контура ушёл запрос, куда он направлен и в каком состоянии находится работа по нему.",
description:
"Здесь отображается, из какого контура ушёл запрос, куда он направлен и в каком состоянии находится работа по нему.",
source_contour: "Исходный внутренний контур",
source_decision: "Решение источника",
source_decision_pending: "Ожидает решения",
@ -1956,6 +1957,9 @@ export default {
ai_voice_tasker: {
title: "AI / Voice Tasker",
},
codex_agent_api: {
title: "Codex Agent API",
},
api_tokens: {
title: "API-токены",
add_token: "Добавить токен",
@ -2552,7 +2556,8 @@ export default {
project_page: {
delete_modal: {
title: "Удалить страницу",
content: 'Вы уверены, что хотите удалить страницу "{value}"? Страница будет удалена без возможности восстановления.',
content:
'Вы уверены, что хотите удалить страницу "{value}"? Страница будет удалена без возможности восстановления.',
success_title: "Страница удалена",
success_message: "Страница успешно удалена.",
error_title: "Не удалось удалить страницу",
@ -3379,8 +3384,7 @@ export default {
},
cycles: {
title: "Двигайтесь циклами",
description:
"Циклы помогают команде двигаться быстрее и ближе всего соответствуют спринтам в agile-подходе.",
description: "Циклы помогают команде двигаться быстрее и ближе всего соответствуют спринтам в agile-подходе.",
},
modules: {
title: "Делите работу на модули",

View File

@ -17,7 +17,8 @@ export type TWorkspaceSettingsTabs =
| "export"
| "storage"
| "webhooks"
| "ai-voice-tasker";
| "ai-voice-tasker"
| "codex-agent-api";
export type TWorkspaceSettingsItem = {
key: TWorkspaceSettingsTabs;
i18n_label: string;