Show guarded apply blockers in IDE
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-16 20:11:23 +03:00
parent b63d18a173
commit 5b26c67947
@@ -70,6 +70,7 @@ type AuthoringChange = ProjectWorkspaceData["authoringChanges"][number];
type ProjectVersion = ProjectWorkspaceData["projectVersions"][number];
type VersionDetail = Awaited<ReturnType<typeof getLineageVersions>>[number];
type VersionDiff = Awaited<ReturnType<typeof getVersionDiff>>;
type GuardCheck = { name: string; status: string; message: string };
type WorkspaceMode = "overview" | "module" | "form" | "properties" | "events" | "versions" | "documentation" | "knowledge" | "learning" | "flowchart";
type OpenDocument = {
id: string;
@@ -1985,6 +1986,10 @@ function EditorPanel({
const isRealSourceLoaded = Boolean(data.editorSourceText && !data.editorSourceText.startsWith("// Код модуля не загружен."));
const hasEditorChanges = normalizeEditorText(proposedText) !== normalizeEditorText(sourceText);
const applyEnabled = semanticDiffPreview?.version_preview?.apply_available ?? false;
const activeGuardChecks = semanticDiffPreview?.checks ?? data.authoringPreview?.checks ?? [];
const blockedGuardChecks = getBlockingGuardChecks(activeGuardChecks);
const firstBlockedGuardCheck = blockedGuardChecks[0] ?? null;
const isGuardBlocked = blockedGuardChecks.length > 0;
const canApplyPayload = Boolean(data.editorSelectedObject && data.editorSourceText && proposedText && hasEditorChanges);
const navigationTarget = data.authoringPreview?.context.routine ?? data.authoringPreview?.context.object ?? selectedObject ?? null;
const initialSymbolQuery = data.authoringPreview?.context.routine?.name ?? selectedMetadataNode?.label ?? selectedObject?.name ?? "Проведение";
@@ -2153,6 +2158,11 @@ function EditorPanel({
async function handleApplyToSfera() {
const versionPreview = semanticDiffPreview?.version_preview;
if (isGuardBlocked) {
setApplyState("error");
setApplyMessage(firstBlockedGuardCheck?.message ?? t.blocked);
return;
}
if (!versionPreview?.apply_available || !canApplyPayload) {
setApplyState("error");
setApplyMessage(t.previewRequired);
@@ -2227,9 +2237,13 @@ function EditorPanel({
<EditorToolbarButton icon={GitCompareArrows} label={t.semanticDiff} onClick={openSemanticDiff} />
<EditorToolbarButton icon={Sparkles} label={t.quickFixes} onClick={openKnowledgePanel} />
<EditorToolbarButton icon={FileText} label={t.documentationMode} onClick={openDocumentationPanel} />
<EditorToolbarButton actionId="apply-to-sfera" disabled={!applyEnabled || isApplying || isRefreshingPreview || !canApplyPayload} icon={CheckCircle2} label={applyLabel} onClick={handleApplyToSfera} />
<EditorToolbarButton actionId="apply-to-sfera" disabled={!applyEnabled || isGuardBlocked || isApplying || isRefreshingPreview || !canApplyPayload} icon={CheckCircle2} label={applyLabel} onClick={handleApplyToSfera} />
<EditorToolbarButton icon={History} label={t.rollbackPlan} onClick={openSemanticDiff} />
{isGuardBlocked ? (
<Badge tone="danger" title={firstBlockedGuardCheck?.message}>{language === "ru" ? `Блокировки: ${blockedGuardChecks.length}` : `Blocked: ${blockedGuardChecks.length}`}</Badge>
) : (
<Badge tone={canApplyPayload ? "success" : "warning"}>{canApplyPayload ? t.workspaceApplyReady : t.previewRequired}</Badge>
)}
</div>
</div>
<div className="min-h-0 flex-1">
@@ -3961,6 +3975,73 @@ function formatGuardStatus(status: string, language: UiLanguage) {
return status;
}
function formatGuardName(name: string, language: UiLanguage) {
const labels: Record<string, { en: string; ru: string }> = {
"ai-token-budget": { en: "AI token budget", ru: "Бюджет AI-токенов" },
apply: { en: "Apply", ru: "Применение" },
impact: { en: "Impact", ru: "Влияние" },
preview: { en: "Preview", ru: "Предпросмотр" },
privacy: { en: "Privacy", ru: "Приватность" },
"production-1c": { en: "Production 1C", ru: "Продуктивная 1С" },
rbac: { en: "RBAC", ru: "Права доступа" },
review: { en: "Review", ru: "Ревью" },
"task-session": { en: "Task session", ru: "Задача и сессия" },
"workspace-history": { en: "Workspace history", ru: "История workspace" }
};
const label = labels[name];
if (label) {
return label[language];
}
return name.replaceAll("-", " ");
}
function getGuardTone(status: string): "neutral" | "success" | "warning" | "danger" | "info" {
if (status === "BLOCKED") {
return "danger";
}
if (status === "READY" || status === "OK") {
return "success";
}
if (status === "REQUIRED") {
return "warning";
}
return "info";
}
function getBlockingGuardChecks(checks: GuardCheck[]) {
return checks.filter((check) => check.status === "BLOCKED");
}
function GuardChecklist({
checks,
emptyRows,
language
}: Readonly<{
checks: GuardCheck[];
emptyRows: string[];
language: UiLanguage;
}>) {
return (
<div className="space-y-2" data-guard-summary>
{checks.length === 0 ? (
emptyRows.map((row) => (
<div className="truncate text-xs text-muted-foreground" key={row}>{row}</div>
))
) : (
checks.map((check) => (
<div className="grid gap-1 border-b border-border/60 pb-2 last:border-b-0 last:pb-0" data-guard-check data-guard-name={check.name} data-guard-status={check.status} key={`${check.name}:${check.status}:${check.message}`}>
<div className="flex min-w-0 items-center gap-2">
<Badge tone={getGuardTone(check.status)}>{formatGuardStatus(check.status, language)}</Badge>
<span className="truncate text-xs font-medium">{formatGuardName(check.name, language)}</span>
</div>
<div className="text-xs leading-5 text-muted-foreground">{check.message}</div>
</div>
))
)}
</div>
);
}
function sourceLocationFromNode(node?: SirNode | null) {
return {
source_path: node?.source_path ?? null,
@@ -4226,7 +4307,12 @@ function BottomWorkbenchPanel({
</div>
<div className="min-h-0 overflow-auto border-l border-border bg-muted/20">
<BottomList title={t.outputPanel} rows={outputRows} />
<BottomList title={t.guardedApply} rows={checks.length > 0 ? checks.map((check) => `${formatGuardStatus(check.status, language)}: ${check.message}`) : [t.previewRequired, t.impactBeforeApply, t.reviewBeforeApply]} />
<div className="border-b border-border p-3">
<div className="text-xs font-semibold uppercase text-muted-foreground">{t.guardedApply}</div>
<div className="mt-2">
<GuardChecklist checks={checks} emptyRows={[t.previewRequired, t.impactBeforeApply, t.reviewBeforeApply]} language={language} />
</div>
</div>
<BottomList title={t.aiShort} rows={aiRows} />
</div>
</div>
@@ -4287,6 +4373,8 @@ function DiffPanel({
data.authoringPreview?.semantic_diff ??
suggestedCode.map((text) => ({ kind: "ADD", text }));
const checks = semanticPreview?.checks ?? data.authoringPreview?.checks ?? [];
const blockedChecks = getBlockingGuardChecks(checks);
const isApplyBlocked = blockedChecks.length > 0;
return (
<Card className="min-h-0 rounded-none border-0 border-b border-border p-0 shadow-none">
@@ -4298,12 +4386,12 @@ function DiffPanel({
<div className="mt-2 grid gap-1 text-xs text-muted-foreground">
<span>{semanticPreview.version_preview.next_version_id}</span>
<span>{t.affectedNodes}: {semanticPreview.affected_nodes.length}</span>
<span>{semanticPreview.version_preview.apply_available ? t.guardedApply : t.workspaceApplyReady}</span>
<span>{isApplyBlocked ? `${t.blocked}: ${blockedChecks.length}` : semanticPreview.version_preview.apply_available ? t.guardedApply : t.workspaceApplyReady}</span>
<span>{t.productionApplyDisabled}</span>
</div>
<div className="mt-3 grid grid-cols-3 gap-2">
<ActionButton label={t.previewRequired} />
<ActionButton disabled={!semanticPreview.version_preview.apply_available} label={t.applyToSfera} />
<ActionButton disabled={!semanticPreview.version_preview.apply_available || isApplyBlocked} label={t.applyToSfera} />
<ActionButton label={t.rollbackPlan} />
</div>
</div>
@@ -4318,18 +4406,8 @@ function DiffPanel({
))}
<div className="rounded-lg border border-warning/30 bg-warning/10 p-3 text-sm">
<div className="font-medium text-warning">{t.guardedApply}</div>
<div className="mt-2 grid gap-2 text-xs text-muted-foreground">
{checks.length === 0 ? (
<>
<span>{t.previewRequired}</span>
<span>{t.impactBeforeApply}</span>
<span>{t.reviewBeforeApply}</span>
</>
) : (
checks.map((check) => (
<span key={check.name}>{formatGuardStatus(check.status, language)}: {check.message}</span>
))
)}
<div className="mt-2">
<GuardChecklist checks={checks} emptyRows={[t.previewRequired, t.impactBeforeApply, t.reviewBeforeApply]} language={language} />
</div>
</div>
</div>