Files
kandev/apps/web/components/settings/workflow-pipeline-editor-helpers.tsx
T
2026-09-10 15:01:37 +01:00

297 lines
13 KiB
TypeScript

"use client";
import { IconInfoCircle } from "@tabler/icons-react";
import { useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@kandev/ui/drawer";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kandev/ui/tooltip";
import type { WorkflowStep } from "@/lib/types/http";
import type { ScriptPlaceholder } from "@/components/settings/profile-edit/script-editor-completions";
import { useTouchDrawer } from "@/hooks/use-compact-task-chrome";
import { cn } from "@/lib/utils";
/**
* Monaco completion entries for the step-prompt editor. `key` is the
* substitution token the backend expands (`{{task_prompt}}`) and is never
* translated; only the description and example are copy, so the list is built
* at render rather than frozen at module scope where `t()` cannot run.
*/
export function stepPromptPlaceholders(t: (key: string) => string): ScriptPlaceholder[] {
return [
{
key: "task_prompt",
description: t("workflows:stepPromptPlaceholderTaskPrompt"),
example: t("workflows:stepPromptPlaceholderExample"),
executor_types: [],
},
];
}
export function HelpTip({
text,
testId,
ariaLabel,
mobileTouchTarget = false,
}: {
text: ReactNode;
testId?: string;
ariaLabel?: string;
mobileTouchTarget?: boolean;
}) {
const { t } = useTranslation();
const label = ariaLabel ?? t("workflows:moreInformation");
const touchDrawer = useTouchDrawer();
const usesTouchDrawer = mobileTouchTarget && touchDrawer;
const [drawerOpen, setDrawerOpen] = useState(false);
const button = (
<button
type="button"
className={cn(
"inline-flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground/50 hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
mobileTouchTarget && "h-11 w-11 md:h-5 md:w-5",
)}
aria-label={label}
aria-haspopup={usesTouchDrawer ? "dialog" : undefined}
aria-expanded={usesTouchDrawer ? drawerOpen : undefined}
data-testid={testId}
>
<IconInfoCircle className="h-3.5 w-3.5" aria-hidden="true" />
</button>
);
if (usesTouchDrawer) {
return (
<Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
<DrawerTrigger asChild>{button}</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>{label}</DrawerTitle>
<DrawerDescription>{text}</DrawerDescription>
</DrawerHeader>
</DrawerContent>
</Drawer>
);
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent className="max-w-xs">{text}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
// `value` is the Tailwind class persisted as `WorkflowStep.color`; only
// `labelKey` is copy, and it resolves at render (see `StepConfigHeader`).
export const STEP_COLORS = [
{ value: "bg-slate-500", labelKey: "workflows:colorGray" },
{ value: "bg-red-500", labelKey: "workflows:colorRed" },
{ value: "bg-orange-500", labelKey: "workflows:colorOrange" },
{ value: "bg-yellow-500", labelKey: "workflows:colorYellow" },
{ value: "bg-green-500", labelKey: "workflows:colorGreen" },
{ value: "bg-cyan-500", labelKey: "workflows:colorCyan" },
{ value: "bg-blue-500", labelKey: "workflows:colorBlue" },
{ value: "bg-indigo-500", labelKey: "workflows:colorIndigo" },
{ value: "bg-purple-500", labelKey: "workflows:colorPurple" },
];
// The `prompt` bodies are deliberately NOT translated: clicking a template
// writes the text into `WorkflowStep.prompt`, which is persisted and sent to
// the agent verbatim. Only the button `labelKey` is copy.
// i18n-exempt: seeded step prompts, persisted and sent verbatim to the agent.
export const PROMPT_TEMPLATES = [
{
labelKey: "workflows:templatePlan",
prompt: `Analyze the task and create a detailed implementation plan.
{{task_prompt}}
INSTRUCTIONS:
1. Break the task into clear, ordered steps
2. For each step, describe what needs to be done and which files are affected
3. Identify potential risks or blockers
4. Estimate relative complexity for each step (low/medium/high)
Output the plan as a numbered list. Be specific about file paths, function names, and the approach for each step. Do NOT implement anything yet - only plan.`,
},
{
labelKey: "workflows:templateCodeReview",
prompt: `Please review the changed files in the current git worktree.
STEP 1: Determine what to review
- First, check if there are any uncommitted changes (dirty working directory)
- If there are uncommitted/staged changes: review those files
- If the working directory is clean: review ONLY the commits from this branch
- Run: git remote show origin | grep 'HEAD branch' to find the default branch name
- Set BASE_REF to origin/<default-branch> using the reported branch
- Use: git log --oneline $(git merge-base "$BASE_REF" HEAD)..HEAD to list the branch commits
- Use: git diff $(git merge-base "$BASE_REF" HEAD) to see the cumulative changes
- Do NOT diff directly against BASE_REF or origin/main/master - that would include unrelated changes if the branch is outdated
- Read each changed file in full - understand surrounding code, not just the diff
- Navigate callers, interfaces, and tests to understand changes end-to-end
- Check git blame on modified sections to understand why code was written a certain way
- Only REPORT issues on code modified in this changeset, but USE the full codebase for context
If a code review skill is available (e.g. /code-review, /review), invoke it instead of using the fallback below.
STEP 2: Review the changes across these layers (skip layers that don't apply):
ARCHITECTURAL FIT (highest priority):
- Changes belong in the correct layer/module and follow the dependency direction used by the codebase
- Business/domain logic is not placed in controllers, transport handlers, repositories, data sources, or infrastructure code
- Controllers handle protocol concerns, use cases orchestrate workflows, repositories define persistence needs, and data sources handle external systems
- Domain/application code does not depend on frameworks, transport models, database models, or vendor-specific types
- Changes do not bypass existing boundaries, duplicate responsibilities, or introduce unnecessary coupling between modules or domains
- New interfaces and abstractions have clear ownership and represent a meaningful boundary, rather than wrapping a single implementation
- Compare with neighbouring features and established patterns, but flag deviations only when they create a real architectural or maintainability problem
- Treat fundamental architectural misplacement or broken dependency direction as a blocker
DATA & STATE MODELLING:
- Domain entities, value objects, DTOs, persistence models, and external API models remain separate where their responsibilities differ
- State transitions and invariants are explicit and cannot create invalid or partially updated state
- There is a single clear source of truth; state or business rules are not duplicated across layers
- Nullability, optional fields, defaults, and invalid combinations are modelled deliberately
- Persistence schemas or transport types are not leaking implementation details into domain/application contracts
- Concurrency, retries, partial failures, and duplicate requests cannot corrupt state or apply transitions more than once
- Backward compatibility, migrations, and mixed-version behaviour are considered when contracts or persisted data change
SECURITY (blockers if found):
- No secrets, tokens, or credentials in code
- Input validation at system boundaries (user input, API handlers, external data)
- No SQL injection, XSS, command injection, or path traversal
- Auth and authorization checks on new endpoints
- No insecure crypto (MD5/SHA1 for passwords, weak random)
LOGIC & CORRECTNESS:
- Edge cases handled (empty input, nil/null, zero, max values)
- Error paths covered and not silently swallowed
- Race conditions or concurrency issues (unprotected shared state, missing synchronization, goroutine leaks)
- Broken invariants - state that can become invalid
PERFORMANCE:
- No N+1 queries (loop with individual DB calls)
- No memory leaks (unclosed connections, streams, listeners)
- Algorithm complexity appropriate for data scale (O(n^2) where O(n) is possible)
- Unnecessary allocations in loops, regex compilation in hot paths, unbounded resource growth
- Prefer structured concurrency (errgroup, conc) over raw primitives
CODE QUALITY:
- No dead code, unused imports, or commented-out code
- Check for orphaned code: if the change refactored or removed callers, grep for functions/types/exports that lost their last consumer
- No speculative code (unused flags, one-off abstractions with single call site)
- No duplicated logic - extract shared helpers or constants
- Deep nesting (>3 levels) - use early returns
AI SLOP DETECTION:
- Comments that restate code or narrate obvious steps
- Unnecessary try/catch that swallow errors or return silent defaults
- as any / as unknown casts to dodge type errors instead of fixing types
- Redundant validation where inputs are already parsed/typed
- Defensive checks abnormal for the area - compare with surrounding code patterns
STEP 3: Output your findings.
Every finding needs: file:line, what's wrong, why it matters, and how to fix it.
Only report findings you're >=80% confident about.
Use these sections (omit empty ones):
## BLOCKER
Must fix before merge - architectural violations, security holes, data loss risk, broken logic, crashes.
- file:line - Description. Why it matters. How to fix.
## SUGGESTION
Recommended but doesn't block - architecture, performance, maintainability, or specific missing tests.
- file:line - Description. Why it matters. How to fix.
End with a verdict: Ready to merge / Ready with suggestions / Blocked - fix blockers first
NOT A FINDING (skip these):
- Issues on lines or files the change didn't modify - even if they are real bugs
- Pre-existing code patterns that this change didn't introduce
- Things linters, typecheckers, or CI catch (imports, types, formatting)
- Intentional functionality changes directly related to the task
- Issues explicitly suppressed in code (lint-ignore, nolint comments)
- Pedantic nitpicks a senior engineer wouldn't flag
- General "add more tests" without specifying what logic is untested
Now review the changes.`,
},
{
labelKey: "workflows:templateSecurityAudit",
prompt: `Perform a security audit on the changed files in the current git worktree.
{{task_prompt}}
Review all changes and check for the following categories:
1. **Injection Vulnerabilities**: SQL injection, command injection, XSS, template injection, path traversal
2. **Authentication & Authorization**: Missing auth checks, broken access control, privilege escalation, insecure session handling
3. **Data Exposure**: Hardcoded secrets, credentials in logs, sensitive data in error messages, missing encryption
4. **Input Validation**: Missing or insufficient validation at system boundaries, unsafe deserialization, unrestricted file uploads
5. **Dependency Risks**: Known vulnerable dependencies, unsafe use of third-party libraries
6. **Concurrency Issues**: Race conditions on shared state, TOCTOU bugs, unsafe concurrent access to resources
For each finding, output:
- **Severity**: CRITICAL / HIGH / MEDIUM / LOW
- **Location**: filename:line_number
- **Issue**: What the vulnerability is
- **Impact**: What an attacker could do
- **Fix**: Specific remediation steps
Only report real, actionable findings. Do not flag theoretical issues without evidence in the code.`,
},
];
export function hasOnEnterAction(step: WorkflowStep, type: string): boolean {
return step.events?.on_enter?.some((a) => a.type === type) ?? false;
}
export function getTransitionType(step: WorkflowStep): string {
const action = step.events?.on_turn_complete?.find((a) =>
["move_to_next", "move_to_previous", "move_to_step"].includes(a.type),
);
return action?.type ?? "none";
}
export function getOnTurnStartTransitionType(step: WorkflowStep): string {
const action = step.events?.on_turn_start?.find((a) =>
["move_to_next", "move_to_previous", "move_to_step"].includes(a.type),
);
return action?.type ?? "none";
}
export function getChildrenCompletedTransitionType(step: WorkflowStep): string {
const action = step.events?.on_children_completed?.find((a) =>
["move_to_next", "move_to_previous", "move_to_step"].includes(a.type),
);
return action?.type ?? "none";
}
export function hasDisablePlanMode(step: WorkflowStep): boolean {
return step.events?.on_turn_complete?.some((a) => a.type === "disable_plan_mode") ?? false;
}
export function hasOnExitAction(step: WorkflowStep, type: string): boolean {
return step.events?.on_exit?.some((a) => a.type === type) ?? false;
}