feat: Sprint 8 — AI triggers, 130 tests, production polish - #5
Conversation
AI Feature UI Triggers: - Add Brand Audit, Copy/Tone, Persona Sim, A11y Spec buttons to QuickActions - Wire 4 action handlers in App.tsx calling backend API endpoints - Persist lastScreenshot ref so AI features work after initial analysis - Add isActionLoading state with spinner feedback during async calls Test Suite (130 tests, all passing): - Lint modules: checkLayoutSizing, checkConstraints, checkTypography, checkComponentProps (55 tests with mock Figma nodes) - Core design-lint: runDesignLint, DEFAULT_LINT_SETTINGS (18 tests) - Backend: confidence-filter (15), three-layer prompts (8), route validation (8) — no Anthropic API calls - Install vitest in both plugin and backend, add test scripts - Remove *.test.ts from .gitignore Production Polish: - ErrorBoundary component wrapping entire app (catches render crashes) - Defensive null guards in ScoreCard, IssuesList, AiReviewCard - Loading spinner overlay in QuickActions during async operations - Pulse animation on active analysis phase indicator Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughДобавлены новые AI‑анализы и их UI‑потоки (brand, copy, persona, a11y) с извлечением текста из скриншотов; усилена серверная валидация и rate‑limiting; введён sanitiser для prompt’ов; много новых UI‑компонентов, ErrorBoundary и крупный набор тестов. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as "UI (App)"
participant Chat as "ChatContainer"
participant Backend as "Backend API"
participant AI as "AI Service"
User->>UI: инициирует действие (Brand/Copy/Persona/A11y)
UI->>UI: валидация (backendAvailable, screenshot, textContent, lint)
UI->>Chat: set isActionLoading = true
Chat->>Chat: показать индикатор загрузки
UI->>Backend: POST /api/... (payload + textContent + lint)
Backend->>AI: формирование prompt (sanitized)
AI-->>Backend: возвращает результаты
Backend-->>UI: отдает JSON-результат
UI->>UI: обновляет pending/lastScreenshot и результаты
UI->>Chat: set isActionLoading = false
Chat->>Chat: скрыть индикатор
UI->>User: отобразить результаты
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan Review Summary by QodoSprint 8 — AI triggers, 130 tests, production polish
WalkthroughsDescription• Add 4 new AI feature QuickAction buttons (Brand Audit, Copy/Tone, Persona Sim, A11y Spec) with backend API integration • Implement 130 passing tests across plugin lint modules (73 tests) and backend services (31 tests) using Vitest • Add ErrorBoundary component wrapping entire app to gracefully catch and handle render crashes • Add defensive null guards in 3 card components (ScoreCard, IssuesList, AiReviewCard) to prevent crashes on missing data • Add loading spinner overlay and pulse animation on active analysis phase indicator for better UX feedback Diagramflowchart LR
A["App.tsx<br/>AI Action Handlers"] -->|calls| B["API Functions<br/>analyzeBrand, copyTone, etc."]
B -->|sends| C["Backend Routes<br/>analyze endpoint"]
C -->|returns| D["Chat Messages<br/>brand-consistency, copy-tone, etc."]
E["QuickActions<br/>4 new buttons"] -->|triggers| A
E -->|shows| F["Loading Spinner<br/>isActionLoading state"]
G["ErrorBoundary<br/>wraps App"] -->|catches| H["Render Errors<br/>graceful fallback"]
I["Test Files<br/>130 tests"] -->|validate| J["Lint Modules<br/>Layout, Constraints, Typography, Props"]
I -->|validate| K["Backend Services<br/>Confidence, Routes, Prompts"]
File Changes1. src/lint/__tests__/lint-modules.test.ts
|
Code Review by Qodo
1.
|
Docker Build: - Add python3/make/g++ to Alpine builder stages for better-sqlite3 native compilation (node-gyp needs Python) - 3-stage Dockerfile: builder → deps → production (no build tools in final image) Brand Audit (review comment #1): - Backend brand-consistency route now defaults personality to ['professional', 'clear', 'consistent'] when empty/omitted instead of rejecting with 400 Copy/Tone (review comment #2): - Plugin screenshot-result event now includes textContent[] extracted via extractTextContent(node) - App.tsx stores textContent in lastScreenshot ref - Copy-tone action uses real extracted text instead of empty array - Shows "No text content found" message if selection has no text layers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (5)
backend/Dockerfile (1)
31-32: Добавьте--start-periodвHEALTHCHECK.Это снизит риск ложного
unhealthyстатуса во время холодного старта контейнера.Предлагаемая правка
-HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD wget -qO- http://localhost:3000/api/health || exit 1🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/Dockerfile` around lines 31 - 32, HEALTHCHECK currently has no --start-period which can cause false unhealthy states during cold starts; update the HEALTHCHECK instruction (the line starting with HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD wget -qO- http://localhost:3000/api/health || exit 1) to include a suitable --start-period (for example --start-period=30s or --start-period=60s) so the container has a grace period before health probes begin; keep the rest of the flags (--interval, --timeout, --retries) unchanged.src/lint/__tests__/lint-modules.test.ts (1)
849-940: Сведите повторяющиеся проверки структурыLintIssueв общий helper.Сейчас один и тот же набор assert-ов продублирован в нескольких тестах; это удорожает поддержку при изменении контракта.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/__tests__/lint-modules.test.ts` around lines 849 - 940, Extract the repeated assertions about LintIssue into a single helper (e.g., expectLintIssueShape or assertLintIssues) and call it from each test; move the block of expect(...).toHaveProperty(...) and common type checks into that helper and accept either a single issue or an array (issues) so tests using checkLayoutSizing, checkConstraints, checkTypography, and checkComponentProps simply invoke the helper with result.issues (or map over issues) instead of repeating the assertions inline; keep the helper near the tests so it’s easy to reuse and update the LintIssue contract in one place.backend/src/__tests__/routes.test.ts (1)
31-33: Сделайте проверку версии менее хрупкой.Жёсткое сравнение с
1.0.0будет падать при любом релизном bump, даже если endpoint корректен.Предлагаемая правка
- expect(body.version).toBe('1.0.0'); + expect(typeof body.version).toBe('string'); + expect(body.version as string).toMatch(/^\d+\.\d+\.\d+(?:[-+].+)?$/);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/__tests__/routes.test.ts` around lines 31 - 33, Текест ожидания версии слишком жёсткий — вместо expect(body.version).toBe('1.0.0') измените проверку на менее хрупкую: либо импортируйте package.json и сравнивайте с текущей версии пакета (например expect(body.version).toBe(pkg.version)), либо используйте semver-валидатор (semver.valid(body.version) не должен быть null) или простую регулярку (expect(body.version).toMatch(/^\d+\.\d+\.\d+(-.+)?$/)); оставьте проверку статуса как есть (body.status).src/lint/__tests__/design-lint.test.ts (1)
348-362: Название теста сейчас шире фактической проверки.В текущем кейсе реально проверяется только
GROUP;SLICEиCOMPONENT_SETне покрыты. Лучше либо сузить название, либо добавить параметризованный тест по всем трём типам.Предложение (параметризованный кейс)
- it('skips GROUP, SLICE, and COMPONENT_SET nodes in lintNode', () => { - // GROUP nodes should be traversed for children but not lint-checked themselves - const group: any = { - id: nextNodeId(), - name: 'Group', - type: 'GROUP', - visible: true, - locked: false, - children: [], - }; - - const result = runDesignLint([group]); - expect(result.summary.totalNodes).toBe(1); - expect(result.errors).toHaveLength(0); - }); + it.each(['GROUP', 'SLICE', 'COMPONENT_SET'] as const)( + 'skips %s nodes in lintNode', + (nodeType) => { + const node: any = { + id: nextNodeId(), + name: nodeType, + type: nodeType, + visible: true, + locked: false, + children: [], + }; + + const result = runDesignLint([node]); + expect(result.summary.totalNodes).toBe(1); + expect(result.errors).toHaveLength(0); + }, + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/__tests__/design-lint.test.ts` around lines 348 - 362, Rename the test or expand it to actually cover all three node types: either change the test title to only mention GROUP (e.g., "skips GROUP nodes in lintNode") or make it parameterized to iterate over ['GROUP','SLICE','COMPONENT_SET'] and assert the same expectations for each; locate the test that calls runDesignLint (the current "skips GROUP, SLICE, and COMPONENT_SET nodes in lintNode" test) and update it to create and run a node for each type (or adjust the title) while keeping assertions against runDesignLint's result.summary.totalNodes and result.errors unchanged.ui/src/components/shared/ErrorBoundary.tsx (1)
27-29: Реализация корректна, рассмотрите защиту от бесконечных retry-циклов.Функция
handleRetryкорректно сбрасывает состояние ошибки. Однако, если ошибка воспроизводится при каждом рендере, пользователь может попасть в цикл "ошибка → retry → ошибка". Для улучшения UX можно добавить счётчик попыток и после N неудачных retry показывать предложение перезагрузить плагин.♻️ Опциональное улучшение с ограничением retry
interface ErrorBoundaryState { hasError: boolean; error: Error | null; + retryCount: number; } export default class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> { + static MAX_RETRIES = 3; + constructor(props: ErrorBoundaryProps) { super(props); - this.state = { hasError: false, error: null }; + this.state = { hasError: false, error: null, retryCount: 0 }; } static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> { - return { hasError: true, error }; + return { hasError: true, error }; } handleRetry = (): void => { - this.setState({ hasError: false, error: null }); + this.setState((prev) => ({ + hasError: false, + error: null, + retryCount: prev.retryCount + 1, + })); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/shared/ErrorBoundary.tsx` around lines 27 - 29, handleRetry currently resets hasError and error which can cause infinite retry loops if the error persists; add a retry counter to the ErrorBoundary state (e.g., retryCount) and a MAX_RETRIES constant, increment retryCount inside handleRetry and only reset hasError/error if retryCount < MAX_RETRIES, otherwise set a flag like showReload (or disable the retry button) to prompt the user to reload/uninstall the plugin; update render logic to show retry button until max attempts and then show the reload suggestion using the new retryCount/MAX_RETRIES and showReload flags.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/src/routes/brand-consistency.ts`:
- Around line 105-108: Normalize bg.personality instead of only checking for
emptiness: when handling brandGuide.personality (bg.personality) first ensure
it's an array, then create a filtered array that keeps only non-empty strings
(trimmed), assign that filtered array back to bg.personality, and only if the
filtered array is empty set the default ['professional','clear','consistent'];
update the logic around the existing Array.isArray(bg.personality) check to
perform the filtering and defaulting.
In `@src/lint/__tests__/lint-modules.test.ts`:
- Around line 13-15: The test currently mutates globalThis.figma without
restoring it; capture the original value (const _origFigma = (globalThis as
any).figma) before assigning the mock, set (globalThis as any).figma = { mixed:
MIXED_SENTINEL }, and ensure you restore the original in an afterEach/afterAll
hook ((globalThis as any).figma = _origFigma) so other tests are unaffected;
update the setup around the code that assigns globalThis.figma in
lint-modules.test.ts accordingly.
In `@ui/src/App.tsx`:
- Around line 771-781: The brand-audit currently sends a placeholder brand guide
to analyzeBrandConsistency (called with brandSs.screenshot), which leaves
colors, typography, and personality empty; update the action to read the saved
teamConfig/brand settings (e.g., pull from teamConfig or the component/state
that stores brand guide) and pass those real values into analyzeBrandConsistency
instead of the hardcoded empty object, and if no valid brand guide exists then
disable/guard the action (prevent calling analyzeBrandConsistency or show/throw
an error) until the guide is configured.
- Line 30: The lastScreenshot ref type is missing the incoming ssData metadata
(hasAutoLayout and childCount), causing generateA11ySpec to receive defaults;
update the useRef type for lastScreenshot to include hasAutoLayout: boolean and
childCount: number (e.g., extend the existing shape) and ensure any assignments
to lastScreenshot.current (where ssData is stored) preserve these fields so
generateA11ySpec receives the real values.
- Around line 764-766: The checks only verify that lastScreenshot exists but not
that it matches the current UI state; update all AI-request branches (where
lastScreenshot.current is used, e.g., the blocks around the shown diff and the
other ranges noted) to validate screenshot freshness by comparing a stable
identifier on the screenshot (e.g., screenshot.scanId, screenshot.version, or
timestamp stored on lastScreenshot.current) against the current scan/scanVersion
produced by the latest fix-all/rescan action (or a
currentScanId/currentScanVersion value); if they differ, reject the cached
screenshot (call chat.addMessage with the "Run an analysis first…" message or
trigger a fresh capture) so Brand/Copy/Persona/A11y analyses always run against
a screenshot that matches the current state. Ensure this same freshness check is
added wherever lastScreenshot.current is referenced for AI requests.
In `@ui/src/components/shared/QuickActions.tsx`:
- Line 25: The container currently uses the CSS class pointer-events-none when
isLoading is true which only blocks mouse clicks but leaves buttons
keyboard-focusable; in the QuickActions component replace that approach by
removing pointer-events-none and applying semantic disabling: either wrap the
action buttons render (see the button elements around the QuickActions render,
referenced near the existing button block at line ~96) in a <fieldset
disabled={isLoading}> or add the disabled prop to each button based on
isLoading, and keep the visual opacity logic (isLoading ? 'opacity-50' : '') so
the UI remains visually dimmed while controls are truly disabled for both mouse
and keyboard.
---
Nitpick comments:
In `@backend/Dockerfile`:
- Around line 31-32: HEALTHCHECK currently has no --start-period which can cause
false unhealthy states during cold starts; update the HEALTHCHECK instruction
(the line starting with HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD
wget -qO- http://localhost:3000/api/health || exit 1) to include a suitable
--start-period (for example --start-period=30s or --start-period=60s) so the
container has a grace period before health probes begin; keep the rest of the
flags (--interval, --timeout, --retries) unchanged.
In `@backend/src/__tests__/routes.test.ts`:
- Around line 31-33: Текест ожидания версии слишком жёсткий — вместо
expect(body.version).toBe('1.0.0') измените проверку на менее хрупкую: либо
импортируйте package.json и сравнивайте с текущей версии пакета (например
expect(body.version).toBe(pkg.version)), либо используйте semver-валидатор
(semver.valid(body.version) не должен быть null) или простую регулярку
(expect(body.version).toMatch(/^\d+\.\d+\.\d+(-.+)?$/)); оставьте проверку
статуса как есть (body.status).
In `@src/lint/__tests__/design-lint.test.ts`:
- Around line 348-362: Rename the test or expand it to actually cover all three
node types: either change the test title to only mention GROUP (e.g., "skips
GROUP nodes in lintNode") or make it parameterized to iterate over
['GROUP','SLICE','COMPONENT_SET'] and assert the same expectations for each;
locate the test that calls runDesignLint (the current "skips GROUP, SLICE, and
COMPONENT_SET nodes in lintNode" test) and update it to create and run a node
for each type (or adjust the title) while keeping assertions against
runDesignLint's result.summary.totalNodes and result.errors unchanged.
In `@src/lint/__tests__/lint-modules.test.ts`:
- Around line 849-940: Extract the repeated assertions about LintIssue into a
single helper (e.g., expectLintIssueShape or assertLintIssues) and call it from
each test; move the block of expect(...).toHaveProperty(...) and common type
checks into that helper and accept either a single issue or an array (issues) so
tests using checkLayoutSizing, checkConstraints, checkTypography, and
checkComponentProps simply invoke the helper with result.issues (or map over
issues) instead of repeating the assertions inline; keep the helper near the
tests so it’s easy to reuse and update the LintIssue contract in one place.
In `@ui/src/components/shared/ErrorBoundary.tsx`:
- Around line 27-29: handleRetry currently resets hasError and error which can
cause infinite retry loops if the error persists; add a retry counter to the
ErrorBoundary state (e.g., retryCount) and a MAX_RETRIES constant, increment
retryCount inside handleRetry and only reset hasError/error if retryCount <
MAX_RETRIES, otherwise set a flag like showReload (or disable the retry button)
to prompt the user to reload/uninstall the plugin; update render logic to show
retry button until max attempts and then show the reload suggestion using the
new retryCount/MAX_RETRIES and showReload flags.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 98875681-328c-44ef-a76c-dc297db2cc0b
⛔ Files ignored due to path filters (4)
backend/package-lock.jsonis excluded by!**/package-lock.jsondist/code.jsis excluded by!**/dist/**dist/ui.htmlis excluded by!**/dist/**package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (21)
.gitignorebackend/Dockerfilebackend/package.jsonbackend/src/__tests__/confidence-filter.test.tsbackend/src/__tests__/routes.test.tsbackend/src/__tests__/three-layer.test.tsbackend/src/routes/brand-consistency.tspackage.jsonsrc/lint/__tests__/design-lint.test.tssrc/lint/__tests__/lint-modules.test.tssrc/ui/message-handler.tsui/src/App.tsxui/src/components/chat/ChatContainer.tsxui/src/components/chat/MessageList.tsxui/src/components/messages/AiReviewCard.tsxui/src/components/messages/IssuesList.tsxui/src/components/messages/ScoreCard.tsxui/src/components/shared/ErrorBoundary.tsxui/src/components/shared/QuickActions.tsxui/src/lib/messages.tsui/src/main.tsx
| analyzeBrandConsistency({ | ||
| screenshot: brandSs.screenshot, | ||
| brandGuide: { | ||
| colors: {}, | ||
| typography: { | ||
| heading: { family: '', weights: [] }, | ||
| body: { family: '', weights: [] }, | ||
| }, | ||
| spacing: { base: 8, scale: [4, 8, 12, 16, 24, 32, 48, 64] }, | ||
| personality: [], | ||
| }, |
There was a problem hiding this comment.
brand-audit сейчас уходит с заглушкой вместо реального brand guide.
Здесь colors пустой, у typography пустые family/weights, а personality всегда []. В итоге бэкенду буквально не с чем сравнивать цвета и типографику, так что аудит легко станет слишком общим или просто неверным. Подставьте сохранённый teamConfig/настройки бренда или блокируйте action, пока guide не настроен.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/src/App.tsx` around lines 771 - 781, The brand-audit currently sends a
placeholder brand guide to analyzeBrandConsistency (called with
brandSs.screenshot), which leaves colors, typography, and personality empty;
update the action to read the saved teamConfig/brand settings (e.g., pull from
teamConfig or the component/state that stores brand guide) and pass those real
values into analyzeBrandConsistency instead of the hardcoded empty object, and
if no valid brand guide exists then disable/guard the action (prevent calling
analyzeBrandConsistency or show/throw an error) until the guide is configured.
Actionable: - Normalize brandGuide.personality (filter non-string/empty values) - Restore globalThis.figma in afterAll (lint-modules.test.ts) - Add hasAutoLayout/childCount to lastScreenshot ref type + pass real values to generateA11ySpec - Add selectionStale freshness check to all AI actions (brand/copy/persona/a11y) - Use teamConfig brand guide instead of empty placeholder in brand-audit - Replace pointer-events-none with <fieldset disabled> in QuickActions (keyboard a11y) Nitpicks: - Add --start-period=20s to Dockerfile HEALTHCHECK - Use semver regex instead of hardcoded '1.0.0' in routes.test.ts - Parametrize GROUP/SLICE/COMPONENT_SET skip test (design-lint.test.ts) - Extract expectLintIssueShape helper (lint-modules.test.ts) - Add retry limit (MAX_RETRIES=3) to ErrorBoundary Verified: tsc clean (root + backend), 132 tests pass, builds OK, Snyk SAST 0 issues. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
ui/src/App.tsx (1)
768-770:⚠️ Potential issue | 🟠 MajorПроверка свежести скриншота всё ещё недостаточна для AI-веток.
На Line 768 / 810 / 844 / 874 вы полагаетесь на
selectionStale, но он не покрывает рескан того жеnodeId. В этом случае можно отправить устаревшийlastScreenshotвместе с новыми lint-данными.💡 Вариант исправления через версионирование анализа
+ const analysisEpochRef = useRef(0); - const lastScreenshot = useRef<{ screenshot: string; nodeId: string; nodeName: string; width: number; height: number; hasAutoLayout?: boolean; childCount?: number; textContent?: string[] } | null>(null); + const lastScreenshot = useRef<{ + screenshot: string; + nodeId: string; + nodeName: string; + width: number; + height: number; + hasAutoLayout?: boolean; + childCount?: number; + textContent?: string[]; + analysisEpoch: number; + } | null>(null); const handleAnalyze = useCallback(() => { + analysisEpochRef.current += 1; ... }, [chat, post]); case 'screenshot-result': { const ssData = event.data as { ... }; - lastScreenshot.current = ssData; + lastScreenshot.current = { ...ssData, analysisEpoch: analysisEpochRef.current }; ... } + const hasFreshScreenshot = () => + !!lastScreenshot.current && + lastScreenshot.current.analysisEpoch === analysisEpochRef.current; case 'brand-audit': { - if (!lastScreenshot.current) { + if (!hasFreshScreenshot()) { chat.addMessage({ kind: 'ai-text', content: 'Run an analysis first to capture a screenshot.' }); break; }Also applies to: 810-812, 844-846, 874-876
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/App.tsx` around lines 768 - 770, The selectionStale check is insufficient because it doesn't detect when the same nodeId was re-scanned (so lastScreenshot may be stale relative to new lintResults); update the logic around selectionStale (used before chat.addMessage in the AI branches) to also compare lastScreenshot.nodeId === current.nodeId and a screenshot version/timestamp (e.g., lastScreenshot.updatedAt or screenshotVersion) against the lintResults.analysisAt (or include a lintResults.version), and if they mismatch treat as stale (prompt a re-scan) or refresh lastScreenshot before proceeding; ensure references to selectionStale, lastScreenshot, nodeId, lintResults, and the chat.addMessage branch are updated accordingly.
🧹 Nitpick comments (5)
ui/src/components/shared/ErrorBoundary.tsx (1)
69-73: Рекомендация: скрыть детали ошибки за раскрывающимся блоком.Отображение
error.messageнапрямую полезно для отладки, но может раскрывать детали реализации конечным пользователям. Для плагина Figma это допустимо, однако можно улучшить UX, сделав детали скрываемыми по умолчанию.♻️ Опциональное улучшение
{this.state.error && ( - <p className="text-11 text-fg-tertiary font-mono break-all"> - {this.state.error.message} - </p> + <details className="text-left"> + <summary className="text-11 text-fg-tertiary cursor-pointer hover:text-fg-secondary"> + Показать детали + </summary> + <p className="text-11 text-fg-tertiary font-mono break-all mt-1"> + {this.state.error.message} + </p> + </details> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/shared/ErrorBoundary.tsx` around lines 69 - 73, Replace the direct rendering of this.state.error.message with a collapsible/details pattern so error details are hidden by default: render a short, non-sensitive error label (e.g., "An error occurred") visible to users and put the full this.state.error.message inside a <details> with a <summary> (or a small toggle button) that expands to reveal the message; update the ErrorBoundary render branch that currently checks this.state.error to use that pattern and ensure the summary/toggle is keyboard-accessible and uses aria attributes for accessibility.backend/Dockerfile (2)
16-16: Используйте современный флаг--omit=devвместо устаревшего--production.На строке 16 флаг
--productionпомечен как deprecated в npm v10+ официальной документацией. Рекомендуется использоватьnpm ci --omit=devдля обеспечения совместимости с будущими версиями npm.Предлагаемый diff
-RUN npm ci --production +RUN npm ci --omit=dev🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/Dockerfile` at line 16, Replace the deprecated npm flag used in the Dockerfile RUN command: change the RUN instruction that currently reads "npm ci --production" to use the modern flag "npm ci --omit=dev" to ensure compatibility with npm v10+, and update any other occurrences of "npm ... --production" in the Dockerfile to the new form.
19-23: ДобавьтеENV NODE_ENV=productionв финальный runtime-образ.Переменная окружения NODE_ENV не задана в Dockerfile и docker-compose.yml. Код приложения использует проверку
process.env.NODE_ENV === 'development'в middleware аутентификации (backend/src/middleware/auth.ts), что подразумевает, что production должен быть установлен явно. Без этого зависимости могут работать в нежелательном режиме.Предлагаемый diff
FROM node:22.15-alpine3.21 WORKDIR /app +ENV NODE_ENV=production COPY --from=deps /app/node_modules/ ./node_modules/ COPY --from=builder /app/dist/ ./dist/ COPY package*.json ./🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/Dockerfile` around lines 19 - 23, Add an explicit production NODE_ENV to the final runtime image by setting ENV NODE_ENV=production in the Dockerfile's final stage (the image starting FROM node:22.15-alpine3.21 where WORKDIR /app and COPY --from=builder /app/dist/ ./dist/ are used); also ensure docker-compose.yml defines NODE_ENV: "production" for the service so runtime checks like process.env.NODE_ENV === 'development' in backend/src/middleware/auth.ts behave correctly in production.src/lint/__tests__/design-lint.test.ts (1)
348-364: Рассмотрите добавление теста для узлов с вложенными children в GROUP.Параметризованный тест проверяет, что GROUP, SLICE и COMPONENT_SET не генерируют ошибки, но узел создаётся с пустым
children: []. Было бы полезно добавить тест, проверяющий, что дети GROUP-узла всё равно обходятся (если это ожидаемое поведение design-lint).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/__tests__/design-lint.test.ts` around lines 348 - 364, Add a new parameterized case (or a separate test) in design-lint.test.ts that creates a GROUP node with non-empty children (use nextNodeId() to generate ids and the same node shape) and passes it to runDesignLint to assert children are traversed: call runDesignLint([groupNode]) and assert summary.totalNodes accounts for the group plus its children and that errors behave as expected (e.g., errors length is 0 if GROUP children should be skipped). Locate the existing test using runDesignLint and nextNodeId and extend or duplicate it to cover nested children under the GROUP node.ui/src/components/shared/QuickActions.tsx (1)
17-23: Сделайте индикатор загрузки озвучиваемым для screen reader.На Line 19 индикатор рендерится как обычный
div, поэтому динамическое состояние может не объявляться ассистивными технологиями. Лучше пометить его как live status.💡 Предлагаемое улучшение
- {isLoading && ( - <div className="flex items-center gap-2 px-3 py-1 text-11 text-fg-secondary"> + {isLoading && ( + <div + role="status" + aria-live="polite" + className="flex items-center gap-2 px-3 py-1 text-11 text-fg-secondary" + > <div className="w-3 h-3 border-[1.5px] border-bg-brand border-t-transparent rounded-full animate-spin shrink-0" /> <span>Running...</span> </div> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/shared/QuickActions.tsx` around lines 17 - 23, The loading indicator in the QuickActions component (rendered when isLoading is true) is a plain div and isn’t announced to screen readers; update the container that wraps the spinner and "Running..." text to be an accessible live region (e.g., add role="status" and aria-live="polite" and aria-atomic="true") so assistive tech receives the dynamic state change, and mark the decorative spinner as non‑informative (e.g., aria-hidden) while keeping the visible "Running..." text announceable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@ui/src/App.tsx`:
- Around line 768-770: The selectionStale check is insufficient because it
doesn't detect when the same nodeId was re-scanned (so lastScreenshot may be
stale relative to new lintResults); update the logic around selectionStale (used
before chat.addMessage in the AI branches) to also compare lastScreenshot.nodeId
=== current.nodeId and a screenshot version/timestamp (e.g.,
lastScreenshot.updatedAt or screenshotVersion) against the
lintResults.analysisAt (or include a lintResults.version), and if they mismatch
treat as stale (prompt a re-scan) or refresh lastScreenshot before proceeding;
ensure references to selectionStale, lastScreenshot, nodeId, lintResults, and
the chat.addMessage branch are updated accordingly.
---
Nitpick comments:
In `@backend/Dockerfile`:
- Line 16: Replace the deprecated npm flag used in the Dockerfile RUN command:
change the RUN instruction that currently reads "npm ci --production" to use the
modern flag "npm ci --omit=dev" to ensure compatibility with npm v10+, and
update any other occurrences of "npm ... --production" in the Dockerfile to the
new form.
- Around line 19-23: Add an explicit production NODE_ENV to the final runtime
image by setting ENV NODE_ENV=production in the Dockerfile's final stage (the
image starting FROM node:22.15-alpine3.21 where WORKDIR /app and COPY
--from=builder /app/dist/ ./dist/ are used); also ensure docker-compose.yml
defines NODE_ENV: "production" for the service so runtime checks like
process.env.NODE_ENV === 'development' in backend/src/middleware/auth.ts behave
correctly in production.
In `@src/lint/__tests__/design-lint.test.ts`:
- Around line 348-364: Add a new parameterized case (or a separate test) in
design-lint.test.ts that creates a GROUP node with non-empty children (use
nextNodeId() to generate ids and the same node shape) and passes it to
runDesignLint to assert children are traversed: call runDesignLint([groupNode])
and assert summary.totalNodes accounts for the group plus its children and that
errors behave as expected (e.g., errors length is 0 if GROUP children should be
skipped). Locate the existing test using runDesignLint and nextNodeId and extend
or duplicate it to cover nested children under the GROUP node.
In `@ui/src/components/shared/ErrorBoundary.tsx`:
- Around line 69-73: Replace the direct rendering of this.state.error.message
with a collapsible/details pattern so error details are hidden by default:
render a short, non-sensitive error label (e.g., "An error occurred") visible to
users and put the full this.state.error.message inside a <details> with a
<summary> (or a small toggle button) that expands to reveal the message; update
the ErrorBoundary render branch that currently checks this.state.error to use
that pattern and ensure the summary/toggle is keyboard-accessible and uses aria
attributes for accessibility.
In `@ui/src/components/shared/QuickActions.tsx`:
- Around line 17-23: The loading indicator in the QuickActions component
(rendered when isLoading is true) is a plain div and isn’t announced to screen
readers; update the container that wraps the spinner and "Running..." text to be
an accessible live region (e.g., add role="status" and aria-live="polite" and
aria-atomic="true") so assistive tech receives the dynamic state change, and
mark the decorative spinner as non‑informative (e.g., aria-hidden) while keeping
the visible "Running..." text announceable.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9c19382d-8878-459b-8edd-d1dab6f8d355
⛔ Files ignored due to path filters (1)
dist/ui.htmlis excluded by!**/dist/**
📒 Files selected for processing (8)
backend/Dockerfilebackend/src/__tests__/routes.test.tsbackend/src/routes/brand-consistency.tssrc/lint/__tests__/design-lint.test.tssrc/lint/__tests__/lint-modules.test.tsui/src/App.tsxui/src/components/shared/ErrorBoundary.tsxui/src/components/shared/QuickActions.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- backend/src/tests/routes.test.ts
- backend/src/routes/brand-consistency.ts
- src/lint/tests/lint-modules.test.ts
…, rate limiting HIGH: - Add 90s timeout (fetchWithTimeout) to all 8 API calls in api.ts; UI no longer hangs forever - Add prompt injection defense: sanitize.ts strips tags + enforces length limits on all user text interpolated into AI prompts (brand, copy-tone, persona, cognitive, chat-followup) MEDIUM: - flow.ts: validate frame structure (id/name/width/height), edge structure, max 50 frames - page-sweep.ts: validate frame structure (id/name/screenshot), max 50 frames - dark-mode.ts: validate modeData substructure (collection, modes, variableDiffs, missingValues) - session.ts: wrap all JSON.parse in try/catch to handle corrupted stored data - Per-route AI rate limit (15 req/min) separate from general limit (60 req/min) - Remove 4 `as any` casts in tryBackendAnalysis — proper type for screenshot param LOW: - Persona-sim taskDescription now uses componentName context instead of generic string - Add 10MB screenshot size validation before API calls - Log Refero background errors instead of silent swallow - Constant-time auth token comparison (crypto.timingSafeEqual) Verified: tsc clean (root + backend), 132 tests pass, builds OK, Snyk SAST 0 issues. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rebrand: - "FigmaLint" → "Bezier" in manifest, package.json (root/ui/backend), code.ts, message-handler (user-visible strings only), chat-followup prompt, MessageList empty state, index.html title - Storage keys (setSharedPluginData 'figmalint') preserved for backward compat shadcn/ui + prompt-kit foundation: - Path aliases (@/) in tsconfig.json + vite.config.ts - shadcn components.json, cn() utility in lib/utils.ts - CSS variable bridge: shadcn tokens → Figma runtime tokens (auto dark mode) - Tailwind config extended with shadcn semantic colors + border-radius prompt-kit components installed (12 files): - ChatContainer, Message, Markdown, CodeBlock, Loader, TextShimmer - PromptInput, Textarea, ScrollButton, Button, Avatar, Tooltip Verified: tsc clean (all 3 projects), 132 tests pass, builds OK, Snyk 0 issues. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/manifest.json (1)
1-13:⚠️ Potential issue | 🟡 MinorОбнаружен дублирующий manifest.json с отличающейся конфигурацией.
В репозитории присутствуют два файла манифеста:
manifest.json(корень) иsrc/manifest.json. Они имеют существенные различия:
Поле manifest.jsonsrc/manifest.jsonid 1521241390290871981ai-design-copilotallowedDomains 5 доменов только Anthropic Рекомендуется удалить дубликат или объединить конфигурации, чтобы избежать путаницы при сборке/деплое.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/manifest.json` around lines 1 - 13, There are two differing manifest files (root manifest.json and src/manifest.json); remove the duplicate or merge them into a single canonical manifest by reconciling key fields (ensure the desired id value is kept, and combine networkAccess.allowedDomains so the final manifest includes all required domains including Anthropic), and make sure api, main, ui, editorType, permissions and documentAccess are consistent across the project; update whatever build/deploy references expect the manifest so only the canonical manifest is used.backend/src/prompts/brand-consistency.ts (1)
27-46:⚠️ Potential issue | 🟠 MajorСанитизация тут всё ещё неполная.
В prompt по-прежнему попадают сырые
color-keys,typography.heading.family,typography.body.familyиrules[].id. Раз уж этот builder закрывает prompt-injection через санитизацию, эти поля тоже нужно прогонять черезsanitizeText, иначе обход остаётся через другие строки brand guide.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/prompts/brand-consistency.ts` around lines 27 - 46, The prompt builder still emits unsanitized values (color keys, typography.heading.family, typography.body.family, and rules[].id); update serialization to run those through sanitizeText (or sanitizeTextArray where appropriate) before interpolating. Specifically: when building the colors block, pass the color key/name and c.hex/c.usage through sanitizeText; when serializing typography use sanitizeText(brandGuide.typography.heading.family) and sanitizeText(brandGuide.typography.body.family) and sanitizeText on each weight string if weights can be arbitrary; and when emitting custom rules ensure r.id and r.severity (and r.description is already sanitized) are sanitized via sanitizeText before formatting in brandGuide.rules.map. Ensure you reuse existing sanitizeText/sanitizeTextArray helpers and preserve the same output structure.backend/src/middleware/rate-limit.ts (1)
23-25:⚠️ Potential issue | 🟠 MajorУстановите trusted proxy для валидации IP-адресов forwarding-заголовков.
Код использует
x-forwarded-forиx-real-ipбез проверки, что они приходят от доверенного прокси. Attacker может подменить эти заголовки и обойти ограничения, используя разные "IP-адреса" для каждого запроса. Это особенно критично дляaiRateLimit()с низким лимитом и дорогими запросами к AI.Кроме того,
parseInt()на строках 54-55 и 64-65 не валидирует результат. Если переменная окружения содержит невалидное значение,parseIntвернётNaN, и сравнениеentry.count > NaNвсегда будетfalse, отключив лимитер.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/middleware/rate-limit.ts` around lines 23 - 25, The IP extraction trusts unverified forwarding headers and must only accept x-forwarded-for/x-real-ip when the request came through a trusted proxy: update the ip calculation (the ip variable) to check the peer address (e.g., c.req.socket.remoteAddress or c.req.ip) against a configured TRUSTED_PROXIES list and only parse the forwarding headers when the peer is in that allowlist, otherwise use the remote peer address; also update aiRateLimit() usage to rely on this validated ip. For the parseInt usages that read env vars (the parseInt calls used to set limits/thresholds), validate the parsed value with Number.isFinite/Number.isInteger (or Number.isNaN check) and provide a safe default or throw a clear error when the env value is invalid so NaN cannot silently disable the limiter.
🧹 Nitpick comments (10)
ui/vite.config.ts (1)
5-6: Избыточный импорт и несогласованное использованиеpath.Строка 5 уже импортирует
resolveиз'path', а строка 6 добавляет импорт модуляpathцеликом. В строке 27 используетсяpath.resolve(), тогда как в остальном файле (строки 13-15) используетсяresolve(). Это создаёт несогласованность.♻️ Предлагаемое исправление: использовать существующий импорт
import { defineConfig, type Plugin } from 'vite'; import react from '@vitejs/plugin-react'; import { viteSingleFile } from 'vite-plugin-singlefile'; import { renameSync, existsSync } from 'fs'; import { resolve } from 'path'; -import path from 'path';resolve: { alias: { - "@": path.resolve(__dirname, "./src"), + "@": resolve(__dirname, "./src"), }, },Also applies to: 27-27
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/vite.config.ts` around lines 5 - 6, В файле есть дублирующий импорт: и named import resolve из 'path' и default import path; используйте существующий named import — удалите "import path from 'path'" и заменить все вызовы path.resolve() (в частности текущий вызов path.resolve) на resolve(), чтобы сделать импорт и использование консистентными (смотрите символы resolve, path и path.resolve в текущем диффе).ui/src/components/ui/prompt-input.tsx (1)
29-37:React.createRef()в default context создаёт новый ref при каждом вызове.При использовании
usePromptInput()внеPromptInputContext.Provider, каждый рендер создаст новый ref-объект. Хотя компоненты должны использоваться внутри Provider, безопаснее использовать{ current: null }.♻️ Предлагаемое исправление
const PromptInputContext = createContext<PromptInputContextType>({ isLoading: false, value: "", setValue: () => {}, maxHeight: 240, onSubmit: undefined, disabled: false, - textareaRef: React.createRef<HTMLTextAreaElement>(), + textareaRef: { current: null }, })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/ui/prompt-input.tsx` around lines 29 - 37, The default context value for PromptInputContext uses React.createRef(), which creates a new ref on each render; update the textareaRef default to a stable ref object like { current: null } instead to avoid recreating refs when usePromptInput() is used outside a Provider—change the textareaRef in the PromptInputContext default to { current: null } and keep the rest of the PromptInputContextType fields unchanged so components reading textareaRef (e.g., via usePromptInput) receive a stable ref.ui/src/App.tsx (1)
901-907: Небезопасное приведение типа(e as any).value.Если
LintErrorне содержит свойствоvalue, лучше добавить его в тип или использовать type guard вместоas any.♻️ Предлагаемое исправление
lintResult: { summary: a11yLint.summary, errors: a11yLint.errors.map((e) => ({ nodeId: e.nodeId, nodeName: e.nodeName, errorType: e.errorType, message: e.message, - value: (e as any).value ?? '', + value: 'value' in e ? String(e.value) : '', })), },Или расширьте тип
LintErrorвmessages.ts, добавив опциональное свойствоvalue?: string.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/App.tsx` around lines 901 - 907, The mapping uses an unsafe cast (e as any). Fix by making LintError include an optional value?: string in the messages.ts type definition or add a type guard that checks for 'value' on the item before reading it; then update the mapping in App.tsx (the a11yLint.errors map) to read e.value (or use the guarded value) without using "as any" so TypeScript knows value is optional and safe to access.ui/src/lib/api.ts (1)
38-44: Проверка размера base64 не учитывает кодирование.
screenshot.length— это длина base64-строки, а не размер исходных байтов. Base64 увеличивает размер примерно в 1.37 раза. Текущая проверка пропустит скриншот ~13.3MB (который закодирован как ~10MB base64) или отклонит валидный ~7.3MB файл.♻️ Предлагаемое исправление
const MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024; // 10MB max screenshot +// Base64 encoding increases size by ~4/3, so adjust threshold accordingly +const MAX_SCREENSHOT_BASE64_LENGTH = Math.floor(MAX_SCREENSHOT_BYTES * 4 / 3); function validateScreenshot(screenshot: string): void { - if (screenshot.length > MAX_SCREENSHOT_BYTES) { + // Remove data URI prefix if present before checking length + const base64Data = screenshot.replace(/^data:image\/\w+;base64,/, ''); + if (base64Data.length > MAX_SCREENSHOT_BASE64_LENGTH) { throw new Error('Screenshot too large (max 10MB). Try selecting a smaller frame.'); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/lib/api.ts` around lines 38 - 44, The current validateScreenshot uses screenshot.length (base64 characters) against MAX_SCREENSHOT_BYTES, which is wrong because base64 expands size ~4/3; update validateScreenshot to first strip any data URL prefix (data:*;base64,) then compute the actual byte size from the base64 string by accounting for padding and conversion (bytes = (base64Length * 3) / 4 - padding) and compare that byte count to MAX_SCREENSHOT_BYTES; keep the thrown Error message and the MAX_SCREENSHOT_BYTES constant unchanged and only adjust the size calculation logic in validateScreenshot.ui/src/components/ui/text-shimmer.tsx (1)
24-31: Дублирование длительности анимации.Класс
animate-[shimmer_4s_infinite_linear]задаёт4s, ноanimationDurationв inline-стиле переопределяет это значение. Работает корректно (inline-стиль приоритетнее), но класс вводит в заблуждение.♻️ Предлагаемое улучшение
className={cn( "bg-size-[200%_auto] bg-clip-text font-medium text-transparent", - "animate-[shimmer_4s_infinite_linear]", + "animate-[shimmer_infinite_linear]", className )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/ui/text-shimmer.tsx` around lines 24 - 31, The class string contains a hardcoded duration token "animate-[shimmer_4s_infinite_linear]" which conflicts with and misleads relative to the inline style animationDuration (`duration` prop); remove the hardcoded "4s" from that class token (e.g., change/remove "animate-[shimmer_4s_infinite_linear]" so it no longer encodes a duration) and keep the inline style animationDuration (`animationDuration: \`${duration}s\``) as the single source of truth; update the class-building expression that includes className so it no longer emits the "4s" variant.backend/src/index.ts (1)
71-79: Можно упростить поддержку списка AI-роутов (DRY).Сейчас 9 почти одинаковых вызовов
app.use. Удобнее вынести пути в массив и пройтись циклом — меньше шанс пропустить новый AI-эндпоинт при следующих изменениях.♻️ Вариант рефакторинга
-// Stricter rate limiting for AI-heavy routes -app.use('/api/analyze', aiRateLimit()); -app.use('/api/analyze-flow', aiRateLimit()); -app.use('/api/analyze-page', aiRateLimit()); -app.use('/api/brand-consistency', aiRateLimit()); -app.use('/api/copy-tone', aiRateLimit()); -app.use('/api/persona-research', aiRateLimit()); -app.use('/api/generate-a11y-spec', aiRateLimit()); -app.use('/api/validate-dark-mode', aiRateLimit()); -app.use('/api/cognitive-walkthrough', aiRateLimit()); +// Stricter rate limiting for AI-heavy routes +const aiHeavyRoutes = [ + '/api/analyze', + '/api/analyze-flow', + '/api/analyze-page', + '/api/brand-consistency', + '/api/copy-tone', + '/api/persona-research', + '/api/generate-a11y-spec', + '/api/validate-dark-mode', + '/api/cognitive-walkthrough', +]; +for (const route of aiHeavyRoutes) app.use(route, aiRateLimit());🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/index.ts` around lines 71 - 79, Replace the nine repeated app.use calls with a single iterable setup: gather the AI route paths (e.g., '/api/analyze', '/api/analyze-flow', '/api/analyze-page', '/api/brand-consistency', '/api/copy-tone', '/api/persona-research', '/api/generate-a11y-spec', '/api/validate-dark-mode', '/api/cognitive-walkthrough') into an array and iterate over it to call app.use(path, aiRateLimit()) for each; this keeps the behavior identical but centralizes the list so future endpoints only need to be added to the array (look for the repeated app.use and aiRateLimit references to locate the code).ui/src/components/ui/message.tsx (2)
108-117: TooltipProvider создаётся для каждого действия.Оборачивание каждого
MessageActionв отдельныйTooltipProviderможет привести к избыточным перерендерам при использовании нескольких действий вместе. Рассмотрите выносTooltipProviderна уровеньMessageActionsили выше.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/ui/message.tsx` around lines 108 - 117, Компонент TooltipProvider сейчас создаётся внутри MessageAction и будет рендериться для каждого действия — вынесите провайдер на уровень выше (например в контейнер MessageActions или родительский компонент), удалите обёртку TooltipProvider из MessageAction (оставьте только Tooltip, TooltipTrigger и TooltipContent в MessageAction), и убедитесь, что верхний компонент (MessageActions) оборачивает всех дочерних MessageAction в единый TooltipProvider чтобы снизить лишние перерендера.
47-53: Пересечение типов слишком широкое.Тип
MessageContentPropsобъединяетReact.ComponentProps<typeof Markdown>иReact.HTMLProps<HTMLDivElement>, но эти пропсы используются взаимоисключающе. Это позволяет передавать некорректные пропсы (например,onChangeдляMarkdown).♻️ Предлагаемое улучшение типов
-export type MessageContentProps = { - children: React.ReactNode - markdown?: boolean - className?: string -} & React.ComponentProps<typeof Markdown> & - React.HTMLProps<HTMLDivElement> +export type MessageContentProps = + | ({ + children: string + markdown: true + className?: string + } & Omit<React.ComponentProps<typeof Markdown>, 'children'>) + | ({ + children: React.ReactNode + markdown?: false + className?: string + } & React.HTMLProps<HTMLDivElement>)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/ui/message.tsx` around lines 47 - 53, MessageContentProps currently intersects React.ComponentProps<typeof Markdown> and React.HTMLProps<HTMLDivElement> allowing invalid combinations; change it to a discriminated union so props are mutually exclusive: one branch where markdown: true (or markdown === true) extends React.ComponentProps<typeof Markdown> and another branch where markdown is absent or false extends React.HTMLProps<HTMLDivElement>, and remove the broad intersection; update usages that rely on the union to narrow by the markdown flag (e.g., MessageContentProps with markdown key).ui/src/components/ui/loader.tsx (1)
68-93: Сложные inline-стили можно упростить.Вычисления
marginLeftиtransformOriginчерез цепочки тернарных операторов снижают читаемость. Рассмотрите вынос в объект по аналогии сbarSizes.♻️ Пример рефакторинга
+ const originSizes = { + sm: { marginLeft: "-0.75px", originX: "0.75px", originY: "10px" }, + md: { marginLeft: "-1px", originX: "1px", originY: "12px" }, + lg: { marginLeft: "-1.25px", originX: "1.25px", originY: "14px" }, + } + // In style: - marginLeft: size === "sm" ? "-0.75px" : size === "lg" ? "-1.25px" : "-1px", - transformOrigin: `${size === "sm" ? "0.75px" : ...} ${...}`, + marginLeft: originSizes[size].marginLeft, + transformOrigin: `${originSizes[size].originX} ${originSizes[size].originY}`,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/ui/loader.tsx` around lines 68 - 93, The inline ternary chains computing marginLeft and transformOrigin reduce readability; create a small lookup object (e.g., offsets or sizeOffsets) keyed by size (same keys as barSizes/sizeClasses) that contains marginLeft and transformOrigin values, then replace the ternary expressions in the style block inside the map with offsets[size].marginLeft and offsets[size].transformOrigin; keep barSizes and sizeClasses as-is and ensure the new object is defined near them or at top of the component for clarity.ui/src/components/ui/markdown.tsx (1)
77-79: Неполное сравнение пропсов в мемоизации.Функция
propsAreEqualсравнивает толькоcontent, игнорируяcomponents. Если родительский компонент передаст другой объектcomponents, компонент не перерендерится.В текущем использовании это безопасно (используется статический
INITIAL_COMPONENTS), но может привести к багам при динамической смене компонентов.♻️ Предлагаемое улучшение
function propsAreEqual(prevProps, nextProps) { - return prevProps.content === nextProps.content + return ( + prevProps.content === nextProps.content && + prevProps.components === nextProps.components + ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/ui/markdown.tsx` around lines 77 - 79, The memoization comparator propsAreEqual currently only compares prevProps.content and will skip re-renders when the components prop changes; update propsAreEqual to also compare the components prop (e.g., return prevProps.content === nextProps.content && prevProps.components === nextProps.components) or, if you need structural equality, use a deep/shallow equality helper (like isEqual or shallowEqual) to compare prevProps.components and nextProps.components; refer to the propsAreEqual function and the INITIAL_COMPONENTS constant when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/src/middleware/auth.ts`:
- Around line 5-8: The current timingSafeEqual function leaks length information
by returning early when a.length !== b.length; instead, remove the early length
check and compute fixed-length SHA-256 digests of both inputs, then call
cryptoTimingSafeEqual on the two digest Buffers so comparisons always operate on
equal-length buffers; update the timingSafeEqual implementation to accept
strings, produce sha256 buffers for both (using the existing crypto utilities),
and use cryptoTimingSafeEqual(digestA, digestB) to determine equality.
In `@backend/src/middleware/rate-limit.ts`:
- Around line 53-66: В функциях rateLimit и aiRateLimit нормализуйте и
валидируйте значения из ENV перед передачей в createRateLimiter: вместо прямого
parseInt проверяйте результат через Number.isFinite / !isNaN и что число > 0, и
при неверных значениях подставляйте безопасные fallback-значения (например 60 и
60000 для rateLimit, 15 и 60000 для aiRateLimit); также убедитесь, что windowMs
и max приводятся к целым числам и не равны 0, чтобы избежать передачи NaN/0 в
createRateLimiter и предотвращения утечек/неправильных заголовков.
In `@backend/src/routes/flow.ts`:
- Around line 29-35: The route currently only validates edges when body.edges is
an array, allowing non-array values to pass; update the validation around
body.edges so that if body.edges is present but not an array you immediately
return c.json({ error: 'edges must be an array' }, 400), and when iterating over
body.edges (the existing loop that checks each edge) also enforce that
edge.sourceFrameId and edge.destinationFrameId are strings (e.g., typeof ===
'string' and non-empty) rather than just truthy, keeping the existing error
response format used by c.json.
- Around line 26-27: Validate body.screenshots as a map of frameId->base64
before calling analyzeFlow: ensure body.screenshots is a non-null object and not
an Array (use !Array.isArray), then iterate Object.entries(body.screenshots) and
return 400 if any key is not a non-empty string or any value is not a non-empty
base64 string (e.g. check type === 'string' and a simple base64 pattern or
Buffer/base64 decode attempt). Replace the current loose check in the handler
(the body.screenshots branch) with this stricter validation and return c.json({
error: 'Invalid screenshots' }, 400) when it fails so analyzeFlow receives only
corrected input.
- Around line 21-23: The frame validation loop in backend/src/routes/flow.ts is
too permissive: change the checks inside the for (const frame of body.frames)
loop to require typeof frame.id === 'string' and typeof frame.name === 'string'
and use Number.isFinite(frame.width) and Number.isFinite(frame.height) instead
of truthiness/typeof 'number'; keep the same error response shape, and ensure
buildGraphDescription() continues to receive only validated frames (id/name
strings and finite numeric width/height).
In `@backend/src/routes/page-sweep.ts`:
- Around line 21-24: Проверка фрейма принимает нестроковые id/name и пустой
screenshot; исправьте условие цикла по body.frames так, чтобы вываливать 400
если frame не объект или typeof frame.id !== 'string' или typeof frame.name !==
'string' или typeof frame.screenshot !== 'string' или frame.screenshot.trim()
=== ''. Обновите проверку в том месте, где сейчас используется !frame.id ||
!frame.name || typeof frame.screenshot !== 'string', и используйте явную
проверку типов для frame.id/frame.name и отклоняйте пустые строки для
frame.screenshot (ссылки: body.frames, frame.id, frame.name, frame.screenshot,
тип FrameInput и последующая обработка, ожидающая строки).
In `@backend/src/routes/session.ts`:
- Line 24: The current parsing sets conversationLength from
JSON.parse(session.conversation || '[]') but if the JSON is valid non-array it
becomes undefined; update the try/catch around JSON.parse to assign a numeric
conversationLength: parse the string using JSON.parse(session.conversation ||
'[]'), then if Array.isArray(parsed) set conversationLength = parsed.length else
set conversationLength = 0, and keep the catch block to also set
conversationLength = 0 on parse errors; reference the session.conversation
variable and the conversationLength binding when making this change.
In `@backend/src/utils/sanitize.ts`:
- Around line 15-18: The sanitizeText function doesn't trim whitespace as
described; update sanitizeText to call trim() after stripTags and before slicing
so leading/trailing spaces are removed (e.g., use
stripTags(text).trim().slice(0, maxLen)); keep the existing non-string guard and
MAX_TEXT_LENGTH default, and ensure references to sanitizeText (or any
documentation/AI-summary that claims "trims and sanitizes") are consistent with
the implementation.
- Around line 29-32: wrapUserContent currently interpolates the label into
XML-like tag names without validation, which permits tag-injection; update
wrapUserContent to validate or sanitize the label parameter (e.g., restrict to a
safe regex like /^[A-Za-z0-9_-]+$/ or escape/replace any non-allowed characters)
before constructing `<user_${label}>` and `</user_${label}>`, and fail fast or
substitute a safe default when validation fails; ensure sanitizeText is still
applied only to content.
- Around line 21-27: sanitizeTextArray currently slices before filtering which
can drop valid string items if non-strings appear early; change the operation
order in sanitizeTextArray (function name) to first filter non-string items (use
the existing type guard (item): item is string => typeof item === 'string'),
then slice to maxItems, then map each remaining string through
sanitizeText(item, maxLen) so you keep up to maxItems valid strings rather than
truncating before filtering.
In `@package.json`:
- Line 36: Update the Vitest dependency entry in package.json: replace the
non-existent version specifier "^4.1.0" for the "vitest" dependency with the
published version "^4.0.18" so npm installs succeed; locate the "vitest" key in
the dependencies/devDependencies block and change its version string
accordingly.
In `@ui/src/components/ui/code-block.tsx`:
- Around line 41-52: Асинхронная функция highlight внутри useEffect вызывает
codeToHtml без обработки ошибок и без защиты от гонок: оберните вызов await
codeToHtml(...) в try/catch и логируйте/обрабатывайте ошибку (например,
установить пустой/fallback HTML через setHighlightedHtml), и добавьте механизм
отмены/версионирования результата (например, useRef counter или abort token
захваченный в highlight и проверяемый перед вызовом setHighlightedHtml) чтобы
игнорировать устаревшие асинхронные результаты при быстрых сменах
code/language/theme; обратите внимание на функции/символы highlight, useEffect,
codeToHtml и setHighlightedHtml при внесении правок.
- Around line 25-31: The prop type CodeBlockCodeProps currently extends
React.HTMLProps<HTMLDivElement>, which allows callers to pass children and
dangerouslySetInnerHTML and thus override the component's internal
dangerouslySetInnerHTML via the props spread; change the type to exclude those
dangerous props by using Omit on React.HTMLProps<HTMLDivElement> to remove
'children' and 'dangerouslySetInnerHTML' (i.e., define CodeBlockCodeProps as
your existing fields & Omit<React.HTMLProps<HTMLDivElement>, 'children' |
'dangerouslySetInnerHTML'>) so external callers cannot supply or override those
properties when spreading ...props in the component that uses
CodeBlockCodeProps.
In `@ui/src/components/ui/markdown.tsx`:
- Around line 49-53: The code unsafely casts children to string when rendering
CodeBlockCode; instead, update markdown rendering to safely extract text from
React children (e.g. use React.Children.toArray(children), filter for strings
and join them, or otherwise convert non-string nodes to text) before passing to
CodeBlockCode, so CodeBlockCode receives a real string rather than a blind cast;
adjust any related prop types (CodeBlockCode) if needed to accept the resulting
string and preserve the existing language prop in markdown.tsx.
In `@ui/src/components/ui/scroll-button.tsx`:
- Around line 22-35: The Button is an icon-only control (rendering ChevronDown)
and currently has no accessible label; update the ScrollButton component so
Button receives an aria-label (e.g., "Scroll to bottom") by default and allow
consumers to override it via props (accept a prop like ariaLabel or pass-through
aria-label from props), ensuring the onClick (scrollToBottom), variant, size,
className and other {...props} remain intact so screen readers get the label
while preserving existing behavior.
- Around line 22-33: The Button remains keyboard-focusable and its click handler
can be overridden; update the Button usage so that when isAtBottom is true it is
removed from keyboard/tab order by using the disabled attribute or
aria-hidden="true" (e.g., set disabled={isAtBottom} or aria-hidden={isAtBottom})
and include the visual classes accordingly, and fix the handler override by
moving {...props} before the explicit onClick or by invoking props.onClick
conditionally inside the onClick wrapper so scrollToBottom cannot be silently
replaced; update the Button instance (the Button JSX with props, onClick,
className, isAtBottom, scrollToBottom) to implement these changes.
In `@ui/tailwind.config.js`:
- Around line 135-206: The keyframe blocks (text-blink, bounce-dots, thin-pulse,
pulse-dot, shimmer-text, wave-bars, spinner-fade and shimmer) were defined as
siblings instead of inside the Tailwind `keyframes` object and thus won't be
recognized; move each named keyframe (text-blink, bounce-dots, thin-pulse,
pulse-dot, shimmer-text, wave-bars, spinner-fade, and shimmer) into the existing
`keyframes` property in the Tailwind config, remove the duplicated `shimmer`
definition so only one remains, and keep the exact keyframe names (e.g.,
text-blink, bounce-dots, shimmer) when relocating so animation references
continue to work.
---
Outside diff comments:
In `@backend/src/middleware/rate-limit.ts`:
- Around line 23-25: The IP extraction trusts unverified forwarding headers and
must only accept x-forwarded-for/x-real-ip when the request came through a
trusted proxy: update the ip calculation (the ip variable) to check the peer
address (e.g., c.req.socket.remoteAddress or c.req.ip) against a configured
TRUSTED_PROXIES list and only parse the forwarding headers when the peer is in
that allowlist, otherwise use the remote peer address; also update aiRateLimit()
usage to rely on this validated ip. For the parseInt usages that read env vars
(the parseInt calls used to set limits/thresholds), validate the parsed value
with Number.isFinite/Number.isInteger (or Number.isNaN check) and provide a safe
default or throw a clear error when the env value is invalid so NaN cannot
silently disable the limiter.
In `@backend/src/prompts/brand-consistency.ts`:
- Around line 27-46: The prompt builder still emits unsanitized values (color
keys, typography.heading.family, typography.body.family, and rules[].id); update
serialization to run those through sanitizeText (or sanitizeTextArray where
appropriate) before interpolating. Specifically: when building the colors block,
pass the color key/name and c.hex/c.usage through sanitizeText; when serializing
typography use sanitizeText(brandGuide.typography.heading.family) and
sanitizeText(brandGuide.typography.body.family) and sanitizeText on each weight
string if weights can be arbitrary; and when emitting custom rules ensure r.id
and r.severity (and r.description is already sanitized) are sanitized via
sanitizeText before formatting in brandGuide.rules.map. Ensure you reuse
existing sanitizeText/sanitizeTextArray helpers and preserve the same output
structure.
In `@src/manifest.json`:
- Around line 1-13: There are two differing manifest files (root manifest.json
and src/manifest.json); remove the duplicate or merge them into a single
canonical manifest by reconciling key fields (ensure the desired id value is
kept, and combine networkAccess.allowedDomains so the final manifest includes
all required domains including Anthropic), and make sure api, main, ui,
editorType, permissions and documentAccess are consistent across the project;
update whatever build/deploy references expect the manifest so only the
canonical manifest is used.
---
Nitpick comments:
In `@backend/src/index.ts`:
- Around line 71-79: Replace the nine repeated app.use calls with a single
iterable setup: gather the AI route paths (e.g., '/api/analyze',
'/api/analyze-flow', '/api/analyze-page', '/api/brand-consistency',
'/api/copy-tone', '/api/persona-research', '/api/generate-a11y-spec',
'/api/validate-dark-mode', '/api/cognitive-walkthrough') into an array and
iterate over it to call app.use(path, aiRateLimit()) for each; this keeps the
behavior identical but centralizes the list so future endpoints only need to be
added to the array (look for the repeated app.use and aiRateLimit references to
locate the code).
In `@ui/src/App.tsx`:
- Around line 901-907: The mapping uses an unsafe cast (e as any). Fix by making
LintError include an optional value?: string in the messages.ts type definition
or add a type guard that checks for 'value' on the item before reading it; then
update the mapping in App.tsx (the a11yLint.errors map) to read e.value (or use
the guarded value) without using "as any" so TypeScript knows value is optional
and safe to access.
In `@ui/src/components/ui/loader.tsx`:
- Around line 68-93: The inline ternary chains computing marginLeft and
transformOrigin reduce readability; create a small lookup object (e.g., offsets
or sizeOffsets) keyed by size (same keys as barSizes/sizeClasses) that contains
marginLeft and transformOrigin values, then replace the ternary expressions in
the style block inside the map with offsets[size].marginLeft and
offsets[size].transformOrigin; keep barSizes and sizeClasses as-is and ensure
the new object is defined near them or at top of the component for clarity.
In `@ui/src/components/ui/markdown.tsx`:
- Around line 77-79: The memoization comparator propsAreEqual currently only
compares prevProps.content and will skip re-renders when the components prop
changes; update propsAreEqual to also compare the components prop (e.g., return
prevProps.content === nextProps.content && prevProps.components ===
nextProps.components) or, if you need structural equality, use a deep/shallow
equality helper (like isEqual or shallowEqual) to compare prevProps.components
and nextProps.components; refer to the propsAreEqual function and the
INITIAL_COMPONENTS constant when making this change.
In `@ui/src/components/ui/message.tsx`:
- Around line 108-117: Компонент TooltipProvider сейчас создаётся внутри
MessageAction и будет рендериться для каждого действия — вынесите провайдер на
уровень выше (например в контейнер MessageActions или родительский компонент),
удалите обёртку TooltipProvider из MessageAction (оставьте только Tooltip,
TooltipTrigger и TooltipContent в MessageAction), и убедитесь, что верхний
компонент (MessageActions) оборачивает всех дочерних MessageAction в единый
TooltipProvider чтобы снизить лишние перерендера.
- Around line 47-53: MessageContentProps currently intersects
React.ComponentProps<typeof Markdown> and React.HTMLProps<HTMLDivElement>
allowing invalid combinations; change it to a discriminated union so props are
mutually exclusive: one branch where markdown: true (or markdown === true)
extends React.ComponentProps<typeof Markdown> and another branch where markdown
is absent or false extends React.HTMLProps<HTMLDivElement>, and remove the broad
intersection; update usages that rely on the union to narrow by the markdown
flag (e.g., MessageContentProps with markdown key).
In `@ui/src/components/ui/prompt-input.tsx`:
- Around line 29-37: The default context value for PromptInputContext uses
React.createRef(), which creates a new ref on each render; update the
textareaRef default to a stable ref object like { current: null } instead to
avoid recreating refs when usePromptInput() is used outside a Provider—change
the textareaRef in the PromptInputContext default to { current: null } and keep
the rest of the PromptInputContextType fields unchanged so components reading
textareaRef (e.g., via usePromptInput) receive a stable ref.
In `@ui/src/components/ui/text-shimmer.tsx`:
- Around line 24-31: The class string contains a hardcoded duration token
"animate-[shimmer_4s_infinite_linear]" which conflicts with and misleads
relative to the inline style animationDuration (`duration` prop); remove the
hardcoded "4s" from that class token (e.g., change/remove
"animate-[shimmer_4s_infinite_linear]" so it no longer encodes a duration) and
keep the inline style animationDuration (`animationDuration: \`${duration}s\``)
as the single source of truth; update the class-building expression that
includes className so it no longer emits the "4s" variant.
In `@ui/src/lib/api.ts`:
- Around line 38-44: The current validateScreenshot uses screenshot.length
(base64 characters) against MAX_SCREENSHOT_BYTES, which is wrong because base64
expands size ~4/3; update validateScreenshot to first strip any data URL prefix
(data:*;base64,) then compute the actual byte size from the base64 string by
accounting for padding and conversion (bytes = (base64Length * 3) / 4 - padding)
and compare that byte count to MAX_SCREENSHOT_BYTES; keep the thrown Error
message and the MAX_SCREENSHOT_BYTES constant unchanged and only adjust the size
calculation logic in validateScreenshot.
In `@ui/vite.config.ts`:
- Around line 5-6: В файле есть дублирующий импорт: и named import resolve из
'path' и default import path; используйте существующий named import — удалите
"import path from 'path'" и заменить все вызовы path.resolve() (в частности
текущий вызов path.resolve) на resolve(), чтобы сделать импорт и использование
консистентными (смотрите символы resolve, path и path.resolve в текущем диффе).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 40964206-8ea6-4103-bb6d-de21eb5f0898
⛔ Files ignored due to path filters (4)
dist/code.jsis excluded by!**/dist/**dist/manifest.jsonis excluded by!**/dist/**dist/ui.htmlis excluded by!**/dist/**ui/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (45)
backend/package.jsonbackend/src/index.tsbackend/src/middleware/auth.tsbackend/src/middleware/rate-limit.tsbackend/src/prompts/brand-consistency.tsbackend/src/prompts/chat-followup.tsbackend/src/prompts/cognitive-walkthrough.tsbackend/src/prompts/copy-tone.tsbackend/src/prompts/persona-research.tsbackend/src/routes/dark-mode.tsbackend/src/routes/flow.tsbackend/src/routes/page-sweep.tsbackend/src/routes/session.tsbackend/src/services/analyzer.tsbackend/src/utils/sanitize.tsmanifest.jsonpackage.jsonsrc/api/providers/openai.tssrc/code.tssrc/fix/naming-fixer.tssrc/manifest.jsonsrc/ui/message-handler.tsui/components.jsonui/index.htmlui/package.jsonui/src/App.tsxui/src/components/chat/MessageList.tsxui/src/components/ui/avatar.tsxui/src/components/ui/button.tsxui/src/components/ui/chat-container.tsxui/src/components/ui/code-block.tsxui/src/components/ui/loader.tsxui/src/components/ui/markdown.tsxui/src/components/ui/message.tsxui/src/components/ui/prompt-input.tsxui/src/components/ui/scroll-button.tsxui/src/components/ui/text-shimmer.tsxui/src/components/ui/textarea.tsxui/src/components/ui/tooltip.tsxui/src/lib/api.tsui/src/lib/utils.tsui/src/styles/globals.cssui/tailwind.config.jsui/tsconfig.jsonui/vite.config.ts
✅ Files skipped from review due to trivial changes (2)
- src/api/providers/openai.ts
- src/fix/naming-fixer.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- ui/src/components/chat/MessageList.tsx
- src/ui/message-handler.ts
- backend/package.json
| if (!body.screenshots || typeof body.screenshots !== 'object' || Object.keys(body.screenshots).length === 0) { | ||
| return c.json({ error: 'Missing screenshots' }, 400); |
There was a problem hiding this comment.
screenshots сейчас не проверяется как map frameId -> base64 string.
Проверка на typeof === 'object' и непустые ключи пропускает массивы и объекты с любыми значениями вроде { foo: 123 }. Ошибка тогда всплывёт уже внутри analyzeFlow, хотя её можно детерминированно вернуть как 400 здесь.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/routes/flow.ts` around lines 26 - 27, Validate body.screenshots
as a map of frameId->base64 before calling analyzeFlow: ensure body.screenshots
is a non-null object and not an Array (use !Array.isArray), then iterate
Object.entries(body.screenshots) and return 400 if any key is not a non-empty
string or any value is not a non-empty base64 string (e.g. check type ===
'string' and a simple base64 pattern or Buffer/base64 decode attempt). Replace
the current loose check in the handler (the body.screenshots branch) with this
stricter validation and return c.json({ error: 'Invalid screenshots' }, 400)
when it fails so analyzeFlow receives only corrected input.
| useEffect(() => { | ||
| async function highlight() { | ||
| if (!code) { | ||
| setHighlightedHtml("<pre><code></code></pre>") | ||
| return | ||
| } | ||
|
|
||
| const html = await codeToHtml(code, { lang: language, theme }) | ||
| setHighlightedHtml(html) | ||
| } | ||
| highlight() | ||
| }, [code, language, theme]) |
There was a problem hiding this comment.
Добавьте защиту от ошибок и гонки в async-подсветке.
На Line 48 codeToHtml(...) может бросить исключение, сейчас это приведёт к unhandled rejection. Плюс при быстрых изменениях code/language/theme более старый async-результат может перезаписать новый.
💡 Предлагаемый фикс
useEffect(() => {
+ let cancelled = false
async function highlight() {
+ setHighlightedHtml(null)
if (!code) {
- setHighlightedHtml("<pre><code></code></pre>")
return
}
- const html = await codeToHtml(code, { lang: language, theme })
- setHighlightedHtml(html)
+ try {
+ const html = await codeToHtml(code, { lang: language, theme })
+ if (!cancelled) {
+ setHighlightedHtml(html)
+ }
+ } catch {
+ if (!cancelled) {
+ setHighlightedHtml(null)
+ }
+ }
}
highlight()
+ return () => {
+ cancelled = true
+ }
}, [code, language, theme])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/src/components/ui/code-block.tsx` around lines 41 - 52, Асинхронная
функция highlight внутри useEffect вызывает codeToHtml без обработки ошибок и
без защиты от гонок: оберните вызов await codeToHtml(...) в try/catch и
логируйте/обрабатывайте ошибку (например, установить пустой/fallback HTML через
setHighlightedHtml), и добавьте механизм отмены/версионирования результата
(например, useRef counter или abort token захваченный в highlight и проверяемый
перед вызовом setHighlightedHtml) чтобы игнорировать устаревшие асинхронные
результаты при быстрых сменах code/language/theme; обратите внимание на
функции/символы highlight, useEffect, codeToHtml и setHighlightedHtml при
внесении правок.
| return ( | ||
| <CodeBlock className={className}> | ||
| <CodeBlockCode code={children as string} language={language} /> | ||
| </CodeBlock> | ||
| ) |
There was a problem hiding this comment.
Небезопасное приведение типа children as string.
children в ReactMarkdown может быть массивом React-узлов, а не строкой. Приведение children as string может передать некорректные данные в CodeBlockCode.
🔧 Предлагаемое исправление
const language = extractLanguage(className)
+ const codeString = Array.isArray(children)
+ ? children.join('')
+ : String(children ?? '')
return (
<CodeBlock className={className}>
- <CodeBlockCode code={children as string} language={language} />
+ <CodeBlockCode code={codeString} language={language} />
</CodeBlock>
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/src/components/ui/markdown.tsx` around lines 49 - 53, The code unsafely
casts children to string when rendering CodeBlockCode; instead, update markdown
rendering to safely extract text from React children (e.g. use
React.Children.toArray(children), filter for strings and join them, or otherwise
convert non-string nodes to text) before passing to CodeBlockCode, so
CodeBlockCode receives a real string rather than a blind cast; adjust any
related prop types (CodeBlockCode) if needed to accept the resulting string and
preserve the existing language prop in markdown.tsx.
| <Button | ||
| variant={variant} | ||
| size={size} | ||
| className={cn( | ||
| "h-10 w-10 rounded-full transition-all duration-150 ease-out", | ||
| !isAtBottom | ||
| ? "translate-y-0 scale-100 opacity-100" | ||
| : "pointer-events-none translate-y-4 scale-95 opacity-0", | ||
| className | ||
| )} | ||
| onClick={() => scrollToBottom()} | ||
| {...props} | ||
| > | ||
| <ChevronDown className="h-5 w-5" /> |
There was a problem hiding this comment.
Иконке-кнопке нужен доступный label.
Сейчас внутри только ChevronDown, поэтому скринридер получит безымянную кнопку. Для icon-only control лучше выставить дефолтный aria-label и дать возможность переопределить его через props.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/src/components/ui/scroll-button.tsx` around lines 22 - 35, The Button is
an icon-only control (rendering ChevronDown) and currently has no accessible
label; update the ScrollButton component so Button receives an aria-label (e.g.,
"Scroll to bottom") by default and allow consumers to override it via props
(accept a prop like ariaLabel or pass-through aria-label from props), ensuring
the onClick (scrollToBottom), variant, size, className and other {...props}
remain intact so screen readers get the label while preserving existing
behavior.
| <Button | ||
| variant={variant} | ||
| size={size} | ||
| className={cn( | ||
| "h-10 w-10 rounded-full transition-all duration-150 ease-out", | ||
| !isAtBottom | ||
| ? "translate-y-0 scale-100 opacity-100" | ||
| : "pointer-events-none translate-y-4 scale-95 opacity-0", | ||
| className | ||
| )} | ||
| onClick={() => scrollToBottom()} | ||
| {...props} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "scroll-button.tsx" | head -20Repository: lemone112/figmalint
Length of output: 103
🏁 Script executed:
cat -n ./ui/src/components/ui/scroll-button.tsxRepository: lemone112/figmalint
Length of output: 1473
🏁 Script executed:
cat -n ./ui/src/components/ui/button.tsx | head -50Repository: lemone112/figmalint
Length of output: 2236
🏁 Script executed:
rg "ScrollButton" --type tsx --type ts -B 2 -A 2Repository: lemone112/figmalint
Length of output: 90
🏁 Script executed:
rg "ScrollButton" -B 2 -A 2Repository: lemone112/figmalint
Length of output: 1203
🏁 Script executed:
rg "from.*scroll-button|import.*ScrollButton" -B 1 -A 1Repository: lemone112/figmalint
Length of output: 45
Устраните проблемы доступности и переопределения обработчика события.
При скрытом состоянии кнопка остаётся доступна с клавиатуры: pointer-events-none блокирует только события мыши, но не удаляет элемент из tab order. Используйте disabled атрибут или aria-hidden="true" для скрытия от клавиатуры.
Кроме того, порядок props некорректен: onClick идёт раньше {...props}, поэтому если родитель передаст свой onClick, он перепишет scrollToBottom. Переместите {...props} перед явным onClick или добавьте проверку перед вызовом.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/src/components/ui/scroll-button.tsx` around lines 22 - 33, The Button
remains keyboard-focusable and its click handler can be overridden; update the
Button usage so that when isAtBottom is true it is removed from keyboard/tab
order by using the disabled attribute or aria-hidden="true" (e.g., set
disabled={isAtBottom} or aria-hidden={isAtBottom}) and include the visual
classes accordingly, and fix the handler override by moving {...props} before
the explicit onClick or by invoking props.onClick conditionally inside the
onClick wrapper so scrollToBottom cannot be silently replaced; update the Button
instance (the Button JSX with props, onClick, className, isAtBottom,
scrollToBottom) to implement these changes.
Migration (prompt-kit components):
- AiMessage: parseBold → MessageContent+Markdown (full GFM rendering)
- InputBar: raw input → PromptInput with auto-resize textarea + Lucide Send icon
- MessageList: manual scrollIntoView → ChatContainerRoot+Content+ScrollAnchor
- User bubble: plain div → Message+MessageContent with proper styling
- StickyHeader: 4 inline SVGs → Lucide icons (Settings, ChevronUp, ChevronDown)
- AnalysisPhaseIndicator: CSS spinner → Loader variant="typing"
- ChatContainer: analyzing spinner → Loader component
- Batch/baseline inline SVGs → Lucide CheckCircle/Save
Bundle optimization:
- Remove Shiki (was adding ~9MB!) — replace with lightweight <pre><code> + copy button
- Remove marked dep — replace lexer with simple split
- Bundle: 10MB → 620KB raw, 176KB gzip
Accessibility (WCAG 2.1 AA):
- aria-live="polite" + aria-relevant="additions" on ChatContainerRoot
- role="status" on analyzing state, phase indicator, QuickActions loading
- aria-label on fieldsets ("Quick actions", "Advanced actions")
- role="alert" + <h2> on ErrorBoundary error state
- aria-label on severity dot in StickyHeader
- sr-only status text on analysis phases
- prefers-reduced-motion global CSS rule
Bug fixes:
- Pass streaming prop to AiMessage (was missing → no typing indicator)
- !min-h-[32px] on textarea (override default 44px)
- text-fg-secondary instead of text-muted-foreground in ChatContainer
Verified: tsc clean, 132 tests pass, builds OK, Snyk 0 issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (6)
ui/src/components/ui/markdown.tsx (1)
75-77: ФункцияpropsAreEqualигнорирует измененияcomponents.Текущая реализация сравнивает только
content, игнорируяcomponents. Если propcomponentsизменится (например, при динамической смене рендереров), блок не перерисуется.♻️ Предлагаемое исправление
function propsAreEqual(prevProps, nextProps) { - return prevProps.content === nextProps.content + return ( + prevProps.content === nextProps.content && + prevProps.components === nextProps.components + ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/ui/markdown.tsx` around lines 75 - 77, The memo equality function propsAreEqual currently only compares prevProps.content and ignores changes to the components prop; update propsAreEqual to also compare prevProps.components and nextProps.components (shallow reference equality) so the component re-renders when renderers change—e.g., return prevProps.content === nextProps.content && prevProps.components === nextProps.components; if you need deep comparison for component objects, replace the shallow check with an appropriate deep-equality utility.ui/src/components/messages/AiMessage.tsx (1)
18-25: Рассмотрите обработку пустого контента без streaming.Если
streaming=falseиcontent="", компонент отрендерит пустой bubble. Возможно, стоит добавить проверку:♻️ Опциональная обработка пустого контента
export default function AiMessage({ content, streaming }: AiMessageProps) { if (streaming && !content) { return ( <div className="bg-bg-secondary rounded-xl rounded-bl-sm px-3 py-2 max-w-[90%]"> <Loader variant="typing" size="sm" /> </div> ); } + if (!content) { + return null; + } + return (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/AiMessage.tsx` around lines 18 - 25, The component currently renders an empty bubble when streaming is false and content is an empty string; update the AiMessage component to guard rendering by checking the props (e.g., streaming and content) before returning MessageContent: if streaming === false and content is empty/whitespace, return null or a small placeholder instead of rendering the MessageContent bubble; locate the AiMessage function and modify the early-return logic around MessageContent (the MessageContent JSX) to perform this check and avoid rendering an empty bubble.ui/src/components/chat/InputBar.tsx (1)
22-29: useCallback: зависимость отvalueсоздаёт новую функцию при каждом вводе.
handleSubmitзависит отvalue, поэтому создаётся новая функция при каждом изменении текста. Это может вызывать ненужные ре-рендеры дочерних компонентов, получающихhandleSubmitкак prop.Альтернативный подход — использовать ref для хранения текущего значения:
♻️ Опциональный рефакторинг с useRef
+import { useState, useCallback, useRef } from 'react'; -import { useState, useCallback } from 'react'; ... export default function InputBar({ onSend, placeholder = 'Ask about this component...', disabled = false, }: InputBarProps) { const [value, setValue] = useState(''); + const valueRef = useRef(value); + valueRef.current = value; - const handleSubmit = useCallback(() => { - const trimmed = value.trim(); + const handleSubmit = useCallback(() => { + const trimmed = valueRef.current.trim(); if (!trimmed) return; onSend(trimmed); setValue(''); - }, [value, onSend]); + }, [onSend]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/chat/InputBar.tsx` around lines 22 - 29, handleSubmit is recreated on every keystroke because it depends on the state variable value; switch to using a ref to hold the current input so handleSubmit no longer depends on value. Add a useRef (e.g., valueRef) and update valueRef.current inside the input onChange alongside setValue, then redefine handleSubmit to depend only on onSend (or have no deps) and read/trim valueRef.current, call onSend(trimmed) and clear both setValue('') and valueRef.current = ''. Update references to value, setValue, handleSubmit, onSend, and the input onChange to use the ref accordingly.ui/src/components/chat/MessageList.tsx (1)
124-138: Множественные type assertionsas anyснижают типобезопасность.Несколько компонентов получают
data={m.data as any}, что обходит проверку типов TypeScript. Это может привести к runtime-ошибкам, если структура данных не соответствует ожиданиям компонента.Рекомендуется определить корректные типы для каждого
kindв union-типеChatMessage:♻️ Предлагаемый подход к типизации
// В lib/messages.ts определить discriminated union: type ChatMessageContent = | { kind: 'design-debt'; data: DesignDebtData } | { kind: 'dark-mode'; data: DarkModeData } | { kind: 'a11y-spec'; data: A11ySpecData } // ... остальные типы // Тогда TypeScript автоматически сузит тип data: case 'design-debt': return <DesignDebtCard key={msg.id} data={m.data} />; // без as any🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/chat/MessageList.tsx` around lines 124 - 138, The switch in MessageList.tsx is bypassing TypeScript checks by passing m.data as any to components (e.g., DesignDebtCard, DarkModeCard, A11ySpecCard, TokenComplianceCard, BrandConsistencyCard, CopyToneCard, PersonaResearchCard, AttentionHeatmapCard); define a discriminated union ChatMessage (or ChatMessageContent) in lib/messages.ts where each variant has a specific kind string and a typed data payload (e.g., { kind: 'design-debt'; data: DesignDebtData }, etc.), update the component prop types to accept the corresponding Data types, and then remove the "as any" casts in MessageList.tsx so each case returns e.g. <DesignDebtCard key={msg.id} data={m.data} /> relying on TypeScript narrowing to ensure type safety.research/figma-api-capabilities-2025.md (1)
602-625: Рассмотрите добавление отметки о дате последней проверки источников.Документ содержит 22 ссылки на официальную документацию Figma. Для долгосрочного поддержания актуальности исследования полезно фиксировать дату последней верификации источников (например, в формате "Last verified: March 2026"), особенно если список источников планируется регулярно обновляться. Это поможет читателям оценить свежесть информации и предотвратит использование устаревших ссылок.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@research/figma-api-capabilities-2025.md` around lines 602 - 625, Add a "Last verified: <Month YYYY>" line under the "## Sources" header to indicate when the links were last checked; update the Sources block by inserting a single-line timestamp (e.g., "Last verified: March 2026") immediately beneath "## Sources" so maintainers can see freshness at a glance, and ensure future updates change that timestamp when the list of links (the bulleted items under "## Sources") is modified.COMPETITIVE_LANDSCAPE_2025_2026.md (1)
5-6: Добавьте маркировку доверия к источникам для ключевых метрик.На Line 5-6 и в блоке источников на Line 476-483 смешаны первичные и вторичные источники для рыночных цифр. Для управленческих выводов лучше пометить каждую ключевую цифру как
High/Medium/Low confidenceи указать первичный источник рядом с утверждением.Also applies to: 476-483
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@COMPETITIVE_LANDSCAPE_2025_2026.md` around lines 5 - 6, Update the key metric statements (e.g., the line "Market Context: Figma $1.05B revenue (2025), 40.65% UI/UX market share, 13M+ MAU") to append a confidence label (High/Medium/Low) for each metric and annotate the primary source immediately after each metric in parentheses; also edit the sources block (the sources listed around lines 476-483) to mark each source as Primary or Secondary and add a mapping that indicates which metric(s) each source supports so readers can quickly verify provenance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@COMPETITIVE_LANDSCAPE_2025_2026.md`:
- Around line 1-2: Замените все упоминания бренда "FigmaLint" в документе на
текущий нейминг "Bezier" (включая заголовок "FigmaLint Competitive Landscape
Analysis" и все упоминания на строках, указанных в ревью: 14-15, 342-390), либо
в переходный период используйте форму "Bezier (formerly FigmaLint)"; убедитесь,
что вы обновили заголовок документа и все внутренние вхождения точным
совпадением с выбранной формой, включая метаданные и поиск/замену по всему
файлу.
In `@research/designer-pain-points-2026.md`:
- Around line 13-20: В файле отсутствуют пустые строки перед и/или после блоков
таблиц, что вызывает предупреждение markdownlint MD058; исправьте это, добавив
ровно одну пустую строку выше и одну пустую строку ниже каждого
Markdown-табличного блока (например таблицы, начинающейся с строки "| Finding |
Source | Data Point |" и всех аналогичных таблиц в документе), приведите
форматирование единообразно по всему файлу и прогоните markdownlint/CI, чтобы
убедиться, что предупреждение MD058 исчезло.
In `@research/figma-api-capabilities-2025.md`:
- Line 178: В таблице найдите строку содержащую текст "Apr 2025 | Update 108~ |
Annotation categories" и исправьте опечатку, удалив лишний символ "~" — заменить
"Update 108~" на "Update 108" чтобы привести формат к единообразию.
- Around line 244-247: В таблице для строки "File Metadata" добавьте корректный
REST путь вместо общего "GET endpoint": замените текст на "GET
/v1/files/:key/meta" и сохраните существующую область доступа
`file_metadata:read`; обновите соответствующий cell в строке таблицы, где
упомянуты "File Metadata" и "GET endpoint" (ищите по фразам "File Metadata" или
`file_metadata:read`).
In `@research/UX-RESEARCH-SYNTHESIS.md`:
- Around line 158-170: The fenced diagram code blocks in
UX-RESEARCH-SYNTHESIS.md (the blocks beginning with the Phase 1: DESIGN ...
Phase 6: DEV QA diagram and the later block starting “Phase 1: DESIGN + AMBIENT
LINT”) are missing a language tag; update both opening fences to use a language
such as `text` (or `md`) so they become ```text (instead of ```), ensuring
markdownlint MD040 compliance.
- Around line 234-242: Tables in the document lack the required blank line(s)
before and after them (MD058); locate each table by its header rows (e.g., the
row starting "| Touchpoint | Current Pain | FigmaLint Intervention | Time Saved
| Evidence |" and the other table header rows referenced) and ensure there is
exactly one empty line above the table start and one empty line after the table
end; apply this normalization consistently for the tables noted (the ones
beginning at the shown header and the other occurrences) so markdownlint MD058
warnings are resolved.
- Around line 460-461: Условие проверки публикации некорректно: выражение
"getPublishStatusAsync() === 'CHANGED' || 'UNPUBLISHED'" всегда истинно;
замените его на корректную проверку — либо вызовите getPublishStatusAsync() один
раз в переменную (например const status = await getPublishStatusAsync()) и
сравните status === 'CHANGED' || status === 'UNPUBLISHED', либо явно повторите
вызов по обеим сторонам оператора (getPublishStatusAsync() === 'CHANGED' ||
getPublishStatusAsync() === 'UNPUBLISHED'); убедитесь, что логика вокруг
component.description === '' использует эту исправленную проверку.
In `@ui/src/components/ui/markdown.tsx`:
- Around line 15-17: parseMarkdownIntoBlocks currently splits on two or more
newlines which will break fenced code blocks that contain blank lines; update
parseMarkdownIntoBlocks to iterate through the markdown line-by-line, maintain a
state variable (e.g., fenceDelimiter or inFence) that toggles when encountering
fence open/close markers like ``` or ~~~, and only treat two-or-more consecutive
blank lines as a block boundary when not inside a fenced code block; keep the
function name parseMarkdownIntoBlocks and return the same string[] of blocks but
build them by accumulating lines into the current block and pushing when a valid
boundary is found.
In `@ui/src/styles/globals.css`:
- Around line 29-49: The CSS token for destructive foreground is missing which
breaks uses of the class text-destructive-foreground in button.tsx; add a new
CSS variable --destructive-foreground to globals.css (set to
var(--figma-color-text-ondanger or a sensible fallback like `#fff/`#fff) matching
the pattern used for other tokens) and then update tailwind.config.js to include
the destructive token's foreground mapping so the utility
text-destructive-foreground resolves to the new --destructive-foreground
variable.
---
Nitpick comments:
In `@COMPETITIVE_LANDSCAPE_2025_2026.md`:
- Around line 5-6: Update the key metric statements (e.g., the line "Market
Context: Figma $1.05B revenue (2025), 40.65% UI/UX market share, 13M+ MAU") to
append a confidence label (High/Medium/Low) for each metric and annotate the
primary source immediately after each metric in parentheses; also edit the
sources block (the sources listed around lines 476-483) to mark each source as
Primary or Secondary and add a mapping that indicates which metric(s) each
source supports so readers can quickly verify provenance.
In `@research/figma-api-capabilities-2025.md`:
- Around line 602-625: Add a "Last verified: <Month YYYY>" line under the "##
Sources" header to indicate when the links were last checked; update the Sources
block by inserting a single-line timestamp (e.g., "Last verified: March 2026")
immediately beneath "## Sources" so maintainers can see freshness at a glance,
and ensure future updates change that timestamp when the list of links (the
bulleted items under "## Sources") is modified.
In `@ui/src/components/chat/InputBar.tsx`:
- Around line 22-29: handleSubmit is recreated on every keystroke because it
depends on the state variable value; switch to using a ref to hold the current
input so handleSubmit no longer depends on value. Add a useRef (e.g., valueRef)
and update valueRef.current inside the input onChange alongside setValue, then
redefine handleSubmit to depend only on onSend (or have no deps) and read/trim
valueRef.current, call onSend(trimmed) and clear both setValue('') and
valueRef.current = ''. Update references to value, setValue, handleSubmit,
onSend, and the input onChange to use the ref accordingly.
In `@ui/src/components/chat/MessageList.tsx`:
- Around line 124-138: The switch in MessageList.tsx is bypassing TypeScript
checks by passing m.data as any to components (e.g., DesignDebtCard,
DarkModeCard, A11ySpecCard, TokenComplianceCard, BrandConsistencyCard,
CopyToneCard, PersonaResearchCard, AttentionHeatmapCard); define a discriminated
union ChatMessage (or ChatMessageContent) in lib/messages.ts where each variant
has a specific kind string and a typed data payload (e.g., { kind:
'design-debt'; data: DesignDebtData }, etc.), update the component prop types to
accept the corresponding Data types, and then remove the "as any" casts in
MessageList.tsx so each case returns e.g. <DesignDebtCard key={msg.id}
data={m.data} /> relying on TypeScript narrowing to ensure type safety.
In `@ui/src/components/messages/AiMessage.tsx`:
- Around line 18-25: The component currently renders an empty bubble when
streaming is false and content is an empty string; update the AiMessage
component to guard rendering by checking the props (e.g., streaming and content)
before returning MessageContent: if streaming === false and content is
empty/whitespace, return null or a small placeholder instead of rendering the
MessageContent bubble; locate the AiMessage function and modify the early-return
logic around MessageContent (the MessageContent JSX) to perform this check and
avoid rendering an empty bubble.
In `@ui/src/components/ui/markdown.tsx`:
- Around line 75-77: The memo equality function propsAreEqual currently only
compares prevProps.content and ignores changes to the components prop; update
propsAreEqual to also compare prevProps.components and nextProps.components
(shallow reference equality) so the component re-renders when renderers
change—e.g., return prevProps.content === nextProps.content &&
prevProps.components === nextProps.components; if you need deep comparison for
component objects, replace the shallow check with an appropriate deep-equality
utility.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8a834c9b-06e1-4cb7-a8dd-024ffe15b252
⛔ Files ignored due to path filters (2)
dist/ui.htmlis excluded by!**/dist/**ui/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (16)
COMPETITIVE_LANDSCAPE_2025_2026.mdresearch/UX-RESEARCH-SYNTHESIS.mdresearch/designer-pain-points-2026.mdresearch/figma-api-capabilities-2025.mdui/package.jsonui/src/components/chat/ChatContainer.tsxui/src/components/chat/InputBar.tsxui/src/components/chat/MessageList.tsxui/src/components/chat/StickyHeader.tsxui/src/components/messages/AiMessage.tsxui/src/components/shared/ErrorBoundary.tsxui/src/components/shared/QuickActions.tsxui/src/components/ui/chat-container.tsxui/src/components/ui/code-block.tsxui/src/components/ui/markdown.tsxui/src/styles/globals.css
🚧 Files skipped from review as they are similar to previous changes (4)
- ui/src/components/shared/QuickActions.tsx
- ui/src/components/ui/code-block.tsx
- ui/src/components/shared/ErrorBoundary.tsx
- ui/src/components/ui/chat-container.tsx
| # FigmaLint Competitive Landscape Analysis | ||
| ## Design Linting & AI-Powered Design Quality Tools (2025-2026) |
There was a problem hiding this comment.
Унифицируйте бренд в документе с текущим неймингом продукта.
На Line 1 и дальше по тексту используется FigmaLint, при том что в PR идёт активный переход на Bezier. Для strategy/go-to-market документа это создаёт терминологический шум и риск двойной коммуникации. Рекомендую привести к единому виду (Bezier или Bezier (formerly FigmaLint) в переходный период).
Also applies to: 14-15, 342-390
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@COMPETITIVE_LANDSCAPE_2025_2026.md` around lines 1 - 2, Замените все
упоминания бренда "FigmaLint" в документе на текущий нейминг "Bezier" (включая
заголовок "FigmaLint Competitive Landscape Analysis" и все упоминания на
строках, указанных в ревью: 14-15, 342-390), либо в переходный период
используйте форму "Bezier (formerly FigmaLint)"; убедитесь, что вы обновили
заголовок документа и все внутренние вхождения точным совпадением с выбранной
формой, включая метаданные и поиск/замену по всему файлу.
| | Finding | Source | Data Point | | ||
| |---------|--------|------------| | ||
| | Design-implementation inconsistencies | UXTools 2024 Survey | **46.3%** of teams report "significant inconsistencies" between design system specs and coded implementations | | ||
| | AI output trust gap | Figma 2025 AI Report | Only **32%** of designers say they can rely on AI output quality; **40%** don't trust AI-generated outputs enough for production | | ||
| | AI speeds but doesn't fix quality | State of AI in Design 2025 | **78%** say AI speeds workflows, but only **58%** say it improves quality | | ||
| | Handoff time waste | Zeplin / Industry Data | Design-dev handoff challenges eat **4-8 hours per employee per week** | | ||
| | Automated accessibility coverage | Industry Consensus | Automated tools catch only **30-40%** of WCAG issues; full compliance needs human review | | ||
|
|
There was a problem hiding this comment.
Исправьте формат таблиц для прохождения markdownlint (MD058).
В нескольких таблицах нет пустых строк до/после блока таблицы (например, начиная с Line 13, 39, 114 и далее). Это лучше поправить единообразно по всему файлу, чтобы убрать предупреждения линтера и избежать нестабильности CI для docs.
Also applies to: 39-51, 114-124, 151-159, 206-216, 221-227, 240-248, 251-261, 286-304
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 18-18: Tables should be surrounded by blank lines
(MD058, blanks-around-tables)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@research/designer-pain-points-2026.md` around lines 13 - 20, В файле
отсутствуют пустые строки перед и/или после блоков таблиц, что вызывает
предупреждение markdownlint MD058; исправьте это, добавив ровно одну пустую
строку выше и одну пустую строку ниже каждого Markdown-табличного блока
(например таблицы, начинающейся с строки "| Finding | Source | Data Point |" и
всех аналогичных таблиц в документе), приведите форматирование единообразно по
всему файлу и прогоните markdownlint/CI, чтобы убедиться, что предупреждение
MD058 исчезло.
| | Apr 2024 | Update 91 | Various improvements | | ||
| | Nov 2024 | Update 102 | Variable binding for component properties | | ||
| | Dec 2024 | Update 104 | Annotations API, rich-text descriptions, prototyping reactions | | ||
| | Apr 2025 | Update 108~ | Annotation categories | |
There was a problem hiding this comment.
Исправьте артефакт в версии обновления.
На Line 178 значение Update 108~ выглядит как опечатка (лишний ~), лучше привести к единообразному формату.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@research/figma-api-capabilities-2025.md` at line 178, В таблице найдите
строку содержащую текст "Apr 2025 | Update 108~ | Annotation categories" и
исправьте опечатку, удалив лишний символ "~" — заменить "Update 108~" на "Update
108" чтобы привести формат к единообразию.
| ``` | ||
| Phase 1: DESIGN Phase 2: SELF-REVIEW Phase 3: PEER REVIEW | ||
| [Create/iterate] [Manual QA checklist] [Share link + wait] | ||
| Time: 30-60 min Time: 1-3 days | ||
| Pain: Tedious, inconsistent Pain: Inconsistent feedback | ||
| Emotion: Dread, boredom Emotion: Anxiety | ||
|
|
||
| Phase 4: REVISION Phase 5: HANDOFF Phase 6: DEV QA | ||
| [Fix feedback items] [Mark "Ready for Dev"] [Developer discovers issues] | ||
| Time: 1-4 hours Time: 15-30 min Time: 2-8 hours | ||
| Pain: Rework feeling Pain: "Did I miss anything?" Pain: Design-code mismatch | ||
| Emotion: Frustration Emotion: Uncertainty Emotion: Blame, rework | ||
| ``` |
There was a problem hiding this comment.
Добавьте язык у fenced code blocks (MD040).
Блоки диаграмм на Line 158 и Line 178 оформлены как fenced code без языка. Укажите text (или md) для соответствия markdownlint.
Предлагаемое исправление
-```
+```text
Phase 1: DESIGN Phase 2: SELF-REVIEW Phase 3: PEER REVIEW
...
-```
+```
-```
+```text
Phase 1: DESIGN + AMBIENT LINT
...
-```
+```Also applies to: 178-230
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@research/UX-RESEARCH-SYNTHESIS.md` around lines 158 - 170, The fenced diagram
code blocks in UX-RESEARCH-SYNTHESIS.md (the blocks beginning with the Phase 1:
DESIGN ... Phase 6: DEV QA diagram and the later block starting “Phase 1: DESIGN
+ AMBIENT LINT”) are missing a language tag; update both opening fences to use a
language such as `text` (or `md`) so they become ```text (instead of ```),
ensuring markdownlint MD040 compliance.
| | Touchpoint | Current Pain | FigmaLint Intervention | Time Saved | Evidence | | ||
| |---|---|---|---|---| | ||
| | During design | Issues accumulate invisibly | Ambient score badge on selection change | 10 min/session | Audit UX-6 | | ||
| | Self-review | 30-60 min manual checklist | Automated scan + batch fix | 20-50 min/review | Pain points Section 4.3 | | ||
| | Peer review | Reviewer catches mechanical errors | Mechanical issues pre-resolved | 30 min/review | Pipeline Evaluation value map | | ||
| | Revision | Unguided fixing in flat list | Guided navigator with auto-rescan | 15 min/revision cycle | Pipeline Evaluation P2 #17 | | ||
| | Handoff | Ambiguous specs, missing docs | Structured export + readiness score | 30 min/handoff | Pain points Section 1.2.4 | | ||
| | Dev QA | 46.3% inconsistency rate | Pre-validated token compliance | 60 min/week per dev | UXTools 2024 data | | ||
|
|
There was a problem hiding this comment.
Нормализуйте отступы вокруг таблиц (MD058).
В таблицах (например, начиная с Line 234, 251, 313, 488) не везде соблюдены пустые строки вокруг таблиц. Это стоит унифицировать по всему файлу, чтобы убрать предупреждения markdownlint и держать docs-пайплайн чистым.
Also applies to: 251-268, 313-319, 488-500
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@research/UX-RESEARCH-SYNTHESIS.md` around lines 234 - 242, Tables in the
document lack the required blank line(s) before and after them (MD058); locate
each table by its header rows (e.g., the row starting "| Touchpoint | Current
Pain | FigmaLint Intervention | Time Saved | Evidence |" and the other table
header rows referenced) and ensure there is exactly one empty line above the
table start and one empty line after the table end; apply this normalization
consistently for the tables noted (the ones beginning at the shown header and
the other occurrences) so markdownlint MD058 warnings are resolved.
| - Missing description: `component.description === ''` with `getPublishStatusAsync() === 'CHANGED' || 'UNPUBLISHED'` | ||
| - Instance swap without `preferredValues`: warning ("Consumers won't know which components to swap in") |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /tmp && find . -type f -name "UX-RESEARCH-SYNTHESIS.md" 2>/dev/null | head -5Repository: lemone112/figmalint
Length of output: 45
🏁 Script executed:
fd "UX-RESEARCH-SYNTHESIS.md"Repository: lemone112/figmalint
Length of output: 96
🏁 Script executed:
git ls-files | grep -i "ux-research"Repository: lemone112/figmalint
Length of output: 96
🏁 Script executed:
wc -l research/UX-RESEARCH-SYNTHESIS.mdRepository: lemone112/figmalint
Length of output: 100
🏁 Script executed:
sed -n '450,470p' research/UX-RESEARCH-SYNTHESIS.mdRepository: lemone112/figmalint
Length of output: 1783
🏁 Script executed:
sed -n '455,465p' research/UX-RESEARCH-SYNTHESIS.md | cat -nRepository: lemone112/figmalint
Length of output: 1042
🏁 Script executed:
cat -n research/UX-RESEARCH-SYNTHESIS.md | sed -n '455,465p'Repository: lemone112/figmalint
Length of output: 1042
Исправьте логическое условие для проверки статуса публикации на строке 460.
Выражение getPublishStatusAsync() === 'CHANGED' || 'UNPUBLISHED' в JavaScript/TypeScript фактически всегда истинно, так как строковый литерал 'UNPUBLISHED' справа от оператора || — это truthy значение. Это опасно для документации: читатели могут скопировать условие как есть.
Предлагаемое исправление
-- Missing description: `component.description === ''` with `getPublishStatusAsync() === 'CHANGED' || 'UNPUBLISHED'`
+- Missing description: `component.description === ''` with `(status === 'CHANGED' || status === 'UNPUBLISHED')`, where `status = await component.getPublishStatusAsync()`📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - Missing description: `component.description === ''` with `getPublishStatusAsync() === 'CHANGED' || 'UNPUBLISHED'` | |
| - Instance swap without `preferredValues`: warning ("Consumers won't know which components to swap in") | |
| - Missing description: `component.description === ''` with `(status === 'CHANGED' || status === 'UNPUBLISHED')`, where `status = await component.getPublishStatusAsync()` | |
| - Instance swap without `preferredValues`: warning ("Consumers won't know which components to swap in") |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@research/UX-RESEARCH-SYNTHESIS.md` around lines 460 - 461, Условие проверки
публикации некорректно: выражение "getPublishStatusAsync() === 'CHANGED' ||
'UNPUBLISHED'" всегда истинно; замените его на корректную проверку — либо
вызовите getPublishStatusAsync() один раз в переменную (например const status =
await getPublishStatusAsync()) и сравните status === 'CHANGED' || status ===
'UNPUBLISHED', либо явно повторите вызов по обеим сторонам оператора
(getPublishStatusAsync() === 'CHANGED' || getPublishStatusAsync() ===
'UNPUBLISHED'); убедитесь, что логика вокруг component.description === ''
использует эту исправленную проверку.
| function parseMarkdownIntoBlocks(markdown: string): string[] { | ||
| return markdown.split(/\n{2,}/).filter(Boolean) | ||
| } |
There was a problem hiding this comment.
Разбиение по \n{2,} может разбить блоки кода с пустыми строками.
Функция parseMarkdownIntoBlocks разделяет markdown по двум или более переносам строк. Это может некорректно разбить блоки кода (````code```), содержащие пустые строки внутри, на отдельные части, нарушив их рендеринг.
🔍 Пример проблемы
Входной markdown:
```js
function foo() {
return 42
}
```Будет разбит на два блока вместо одного, что сломает подсветку синтаксиса.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/src/components/ui/markdown.tsx` around lines 15 - 17,
parseMarkdownIntoBlocks currently splits on two or more newlines which will
break fenced code blocks that contain blank lines; update
parseMarkdownIntoBlocks to iterate through the markdown line-by-line, maintain a
state variable (e.g., fenceDelimiter or inFence) that toggles when encountering
fence open/close markers like ``` or ~~~, and only treat two-or-more consecutive
blank lines as a block boundary when not inside a fenced code block; keep the
function name parseMarkdownIntoBlocks and return the same string[] of blocks but
build them by accumulating lines into the current block and pushing when a valid
boundary is found.
| /* ── shadcn variables → Figma aliases ── */ | ||
| --background: var(--figma-color-bg, #ffffff); | ||
| --foreground: var(--figma-color-text, #333333); | ||
| --card: var(--figma-color-bg, #ffffff); | ||
| --card-foreground: var(--figma-color-text, #333333); | ||
| --popover: var(--figma-color-bg, #ffffff); | ||
| --popover-foreground: var(--figma-color-text, #333333); | ||
| --primary: var(--figma-color-bg-brand, #0d99ff); | ||
| --primary-foreground: var(--figma-color-text-onbrand, #ffffff); | ||
| --secondary: var(--figma-color-bg-secondary, #f5f5f5); | ||
| --secondary-foreground: var(--figma-color-text, #333333); | ||
| --muted: var(--figma-color-bg-tertiary, #e6e6e6); | ||
| --muted-foreground: var(--figma-color-text-secondary, #666666); | ||
| --accent: var(--figma-color-bg-hover, rgba(0,0,0,0.06)); | ||
| --accent-foreground: var(--figma-color-text, #333333); | ||
| --destructive: var(--figma-color-text-danger, #f24822); | ||
| --border: var(--figma-color-border, #e6e6e6); | ||
| --input: var(--figma-color-border, #e6e6e6); | ||
| --ring: var(--figma-color-bg-brand, #0d99ff); | ||
| --radius: 0.5rem; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Проверка определения токенов в Tailwind-конфиге =="
fd 'tailwind.config.*' -x sh -c 'echo "--- $1 ---"; sed -n "1,260p" "$1"' sh {}
echo "== Поиск использования destructive токенов в UI =="
rg -nP --type=ts --type=tsx --type=css -C2 '\b(bg|text|border)-destructive(?:-foreground)?\b|var\(--destructive-foreground\)'Repository: lemone112/figmalint
Length of output: 4646
🏁 Script executed:
# Поиск компонентов, использующих destructive-классы
rg -n '(bg|text|border)-destructive|destructive-foreground' --type-list | head -20Repository: lemone112/figmalint
Length of output: 452
🏁 Script executed:
# Более тщательный поиск по расширениям файлов
fd '\.(tsx?|jsx?|css)$' -x rg -l 'destructive' {} \; 2>/dev/nullRepository: lemone112/figmalint
Length of output: 192
🏁 Script executed:
# Проверить, использует ли какой-то компонент текст поверх bg-destructive
rg -n 'bg-destructive' -A 3 -B 1Repository: lemone112/figmalint
Length of output: 50376
Добавьте --destructive-foreground в CSS алиасы Tailwind-токенов.
Компонент button.tsx использует класс text-destructive-foreground (строка 14), но переменная --destructive-foreground не определена в globals.css. Это вызовет ошибку при применении деструктивного стиля к кнопкам и прочим компонентам с семантикой удаления/опасности.
Рекомендуемое исправление
--destructive: var(--figma-color-text-danger, `#f24822`);
+ --destructive-foreground: var(--figma-color-text-onbrand, `#ffffff`);Также обновите tailwind.config.js, чтобы добавить foreground в деструктивный токен:
destructive: {
- DEFAULT: 'var(--destructive)'
+ DEFAULT: 'var(--destructive)',
+ foreground: 'var(--destructive-foreground)'
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/src/styles/globals.css` around lines 29 - 49, The CSS token for
destructive foreground is missing which breaks uses of the class
text-destructive-foreground in button.tsx; add a new CSS variable
--destructive-foreground to globals.css (set to var(--figma-color-text-ondanger
or a sensible fallback like `#fff/`#fff) matching the pattern used for other
tokens) and then update tailwind.config.js to include the destructive token's
foreground mapping so the utility text-destructive-foreground resolves to the
new --destructive-foreground variable.
Security: - auth.ts: hash inputs with SHA-256 before timingSafeEqual (no length leak) - rate-limit.ts: validate parseInt results with Number.isFinite (prevent NaN disabling limiter) - sanitize.ts: validate wrapUserContent label against /^[A-Za-z0-9_-]+$/ (prevent tag injection) - sanitize.ts: add .trim() to sanitizeText after stripTags - brand-consistency.ts: sanitize color keys, font families, rule IDs + severity Validation: - flow.ts: strict typeof checks on frame/edge fields, reject non-array edges - session.ts: Array.isArray check before .length on parsed conversation - api.ts: fix base64 size validation (compute decoded byte size, not string length) UI/Config: - tailwind.config.js: move 7 keyframes into keyframes object (were siblings), remove duplicate shimmer - src/manifest.json: sync id + allowedDomains with root manifest - scroll-button.tsx: add aria-label="Scroll to bottom" - vite.config.ts: remove duplicate path import - research/figma-api-capabilities-2025.md: fix File Metadata API path Verified: tsc clean, 194 tests pass, builds OK, Snyk 0 issues. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
ui/src/lib/api.ts (2)
246-283:⚠️ Potential issue | 🟡 MinorОтсутствует валидация скриншотов в
analyzePageSweep.Функция принимает массив фреймов, каждый из которых содержит
screenshot, ноvalidateScreenshotне вызывается для них. Это создаёт несоответствие с другими эндпоинтами (analyzeComponent,analyzeBrandConsistency, и т.д.), где валидация выполняется.Злоумышленник или ошибка в клиентском коде могут отправить слишком большие скриншоты, что приведёт к проблемам с памятью или превышению лимитов на стороне сервера.
🛡️ Предлагаемое исправление
}): Promise<{ // ... return type }> { + for (const frame of data.frames) { + validateScreenshot(frame.screenshot); + } const resp = await fetchWithTimeout(`${backendUrl}/api/analyze-page`, {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/lib/api.ts` around lines 246 - 283, analyzePageSweep currently posts frames without validating each frame.screenshot; call the existing validateScreenshot function for every frame (e.g., iterate data.frames and run validateScreenshot(frame.screenshot)) and reject/throw or return a clear error when validation fails before calling fetchWithTimeout; ensure you reference validateScreenshot and analyzePageSweep so the check happens early (and preferably trim or reject oversized screenshots) to match behavior of analyzeComponent/analyzeBrandConsistency.
296-325:⚠️ Potential issue | 🟡 MinorОтсутствует валидация скриншотов в
analyzeFlow.Аналогично
analyzePageSweep, эта функция принимаетscreenshots: Record<string, string>, но не валидирует их размер перед отправкой. Рекомендуется добавить валидацию для консистентности с остальными эндпоинтами.🛡️ Предлагаемое исправление
}): Promise<{ // ... return type }> { + for (const screenshot of Object.values(data.screenshots)) { + validateScreenshot(screenshot); + } const resp = await fetchWithTimeout(`${backendUrl}/api/analyze-flow`, {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/lib/api.ts` around lines 296 - 325, The analyzeFlow function sends screenshots without validating their size/format like analyzePageSweep does; add a validation step (reuse or implement validateScreenshots) that iterates over the screenshots record passed into analyzeFlow and ensures each value is a base64 string and below the agreed size limit (e.g., ~3MB / character length threshold), and either strip/omit oversized entries or throw a clear error; call this validation before JSON.stringify(data) in analyzeFlow (near fetchWithTimeout/backendUrl) so only validated screenshots are sent to the /api/analyze-flow endpoint.
♻️ Duplicate comments (2)
backend/src/routes/flow.ts (1)
26-27:⚠️ Potential issue | 🟠 Major
screenshotsпо-прежнему не валидируется как словарьframeId -> base64 string.Проверка
typeof body.screenshots !== 'object'пропускает массивы (typeof [] === 'object'), а значения не проверяются на типstring. Это позволяет передать{ foo: 123 }или[...], что приведёт к ошибкам внутриanalyzeFlow, хотя их можно детерминированно отклонить здесь с кодом 400.,
🔧 Предлагаемое исправление
- if (!body.screenshots || typeof body.screenshots !== 'object' || Object.keys(body.screenshots).length === 0) { + if ( + !body.screenshots || + typeof body.screenshots !== 'object' || + Array.isArray(body.screenshots) || + Object.keys(body.screenshots).length === 0 + ) { + return c.json({ error: 'Missing screenshots' }, 400); + } + for (const [frameId, value] of Object.entries(body.screenshots)) { + if (typeof frameId !== 'string' || !frameId || typeof value !== 'string' || !value) { + return c.json({ error: 'Each screenshot must be a non-empty base64 string keyed by frameId' }, 400); + } + } - return c.json({ error: 'Missing screenshots' }, 400); - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/routes/flow.ts` around lines 26 - 27, Валидация поля body.screenshots в обработчике маршрута (flow.ts) неверна: поправьте проверку так, чтобы она отклоняла массивы и проверяла, что screenshots — плоский словарь frameId->base64 string; например, сначала отвергнуть Array.isArray(body.screenshots), затем пройти по Object.entries(body.screenshots) и вернуть 400 если любой ключ не строка или любое значение не непустая строка (опционально проверить соответствие base64 паттерну). Обновите сообщение об ошибке и убедитесь, что analyzeFlow будет получать только корректную структуру frameId->base64.research/figma-api-capabilities-2025.md (1)
178-178:⚠️ Potential issue | 🟡 MinorИсправьте артефакт в номере версии обновления.
Значение
Update 108~содержит лишний символ~. Все остальные строки таблицы используют форматUpdate NNNбез символа тильды. Необходимо привести к единообразному формату.🤖 Предлагаемое исправление
-| Apr 2025 | Update 108~ | Annotation categories | +| Apr 2025 | Update 108 | Annotation categories |🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@research/figma-api-capabilities-2025.md` at line 178, В строке таблицы, где указано "Update 108~" (строка с датой "Apr 2025"), удалите лишний символ тильды и измените значение на "Update 108" чтобы соответствовать формату остальных записей; отредактируйте соответствующую ячейку таблицы (значение в колонке версии — "Update 108~") заменив на "Update 108".
🧹 Nitpick comments (7)
research/figma-api-capabilities-2025.md (4)
18-41: Рассмотрите добавление пустых строк вокруг таблиц.Markdownlint рекомендует окружать таблицы пустыми строками для улучшения читаемости и соответствия стандартным практикам форматирования Markdown. Это относится к таблицам на строках 18, 26, 34, 41 и аналогичным таблицам в остальной части документа.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@research/figma-api-capabilities-2025.md` around lines 18 - 41, The Markdown tables (e.g., the tables listing methods like getVariableByIdAsync/getLocalVariablesAsync, the Creation table with createVariable/createVariableCollection/createVariableAlias, the Binding Helpers table with setBoundVariableForPaint/setBoundVariableForEffect, and the "Library / Extended Collections" table) need a blank line before and after each table; update the document by inserting an empty line above the table header and one below the table end for each occurrence to satisfy markdownlint and improve readability.
572-585: Добавьте спецификатор языка для блока кода.Блок кода с REST API эндпоинтами не имеет указания языка. Для единообразия с другими блоками кода в документе рекомендуется добавить спецификатор (например,
httpилиtext).♻️ Предлагаемое исправление
-``` +```http GET /v1/files/:key // Full file data🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@research/figma-api-capabilities-2025.md` around lines 572 - 585, The fenced code block containing the REST endpoints (starting with "GET /v1/files/:key" and ending with "POST /v2/webhooks") needs a language specifier for consistency; edit that code fence to include a language token such as "http" (e.g., change ``` to ```http) so the block is highlighted and matches other code blocks in the document.
79-102: Добавьте спецификатор языка для блока кода.Блок кода, перечисляющий значения
VariableScope, не имеет указания языка. Рекомендуется добавить спецификатор (например,typescriptилиtext) для улучшения читаемости и соответствия лучшим практикам Markdown.♻️ Предлагаемое исправление
-``` +```typescript ALL_SCOPES // Special: shown everywhere TEXT_CONTENT // Text node content🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@research/figma-api-capabilities-2025.md` around lines 79 - 102, The code fence showing VariableScope constants (e.g., ALL_SCOPES, TEXT_CONTENT, CORNER_RADIUS, etc.) lacks a language specifier; update the opening fence to include a language like "typescript" (or "text") so the block begins with ```typescript and leave the contents unchanged to improve Markdown rendering and syntax highlighting.
392-396: Добавьте спецификатор языка для блока кода.Блок кода с REST API эндпоинтами не имеет указания языка. Рекомендуется добавить спецификатор (например,
httpилиtext) для улучшения читаемости.♻️ Предлагаемое исправление
-``` +```http GET /v1/files/:file_key — Full file data🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@research/figma-api-capabilities-2025.md` around lines 392 - 396, В блоке с REST-эндпоинтами (строки содержащие "GET /v1/files/:file_key", "GET /v1/files/:file_key/nodes?ids=x,y,z" и "GET /v1/teams/:team_id/components") добавьте спецификатор языка для fenced code block (например ```http или ```text) перед началом блока и оставьте закрывающий ``` после него, чтобы код выделялся корректно и улучшалась читаемость.ui/src/lib/api.ts (3)
221-229:fetchReferoDataиcheckHealthне используютfetchWithTimeout.Эти функции выполняют сетевые запросы без таймаута. Для health-check это особенно важно — запрос без таймаута может блокировать UI при сетевых проблемах.
♻️ Предлагаемое исправление
export async function fetchReferoData(sessionId: string): Promise<{ ready: boolean; data?: any }> { try { - const resp = await fetch(`${backendUrl}/api/session/${sessionId}/refero`, { method: 'GET' }); + const resp = await fetchWithTimeout(`${backendUrl}/api/session/${sessionId}/refero`, { method: 'GET' }, 10_000); if (!resp.ok) return { ready: false }; return resp.json(); } catch { return { ready: false }; } } export async function checkHealth(): Promise<boolean> { try { - const resp = await fetch(`${backendUrl}/api/health`, { method: 'GET' }); + const resp = await fetchWithTimeout(`${backendUrl}/api/health`, { method: 'GET' }, 5_000); return resp.ok; } catch { return false; } }Also applies to: 234-241
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/lib/api.ts` around lines 221 - 229, The fetchReferoData and checkHealth functions currently call fetch without a timeout, which can hang the UI; update both (fetchReferoData and checkHealth) to call the shared fetchWithTimeout helper instead of fetch, passing an appropriate timeout value (e.g., a few seconds) and propagating/handling the timeout error similarly to existing error handling so the functions still return { ready: false } on failure; ensure you replace the raw fetch(...) calls with await fetchWithTimeout(...) and keep the existing response.ok checks and JSON return behavior.
20-36: Потенциальная утечка памяти: слушательabortне удаляется при успешном завершении запроса.На строке 29 добавляется слушатель события
abortк переданномуexistingSignal. Хотя используется{ once: true }, если сигнал никогда не срабатывает (что является нормальным сценарием при успешном fetch), слушатель остаётся прикреплённым кexistingSignalи удерживает ссылку на локальныйcontroller. ЕслиexistingSignalдолгоживущий (например, глобальный контроллер приложения), это приводит к накоплению "мёртвых" контроллеров.♻️ Предлагаемое исправление
function fetchWithTimeout(url: string, options: RequestInit, timeoutMs = API_TIMEOUT_MS): Promise<Response> { const controller = new AbortController(); const existingSignal = options.signal; + let abortHandler: (() => void) | undefined; if (existingSignal) { - // If already aborted, abort immediately if (existingSignal.aborted) { controller.abort(existingSignal.reason); } else { - existingSignal.addEventListener('abort', () => controller.abort(existingSignal.reason), { once: true }); + abortHandler = () => controller.abort(existingSignal.reason); + existingSignal.addEventListener('abort', abortHandler, { once: true }); } } const timeout = setTimeout(() => controller.abort(new DOMException('Request timed out', 'TimeoutError')), timeoutMs); - return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(timeout)); + return fetch(url, { ...options, signal: controller.signal }).finally(() => { + clearTimeout(timeout); + if (abortHandler && existingSignal) { + existingSignal.removeEventListener('abort', abortHandler); + } + }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/lib/api.ts` around lines 20 - 36, fetchWithTimeout attaches an abort listener to existingSignal that can retain references to the local controller if the signal never fires; fix by registering a named handler (e.g. const onAbort = () => controller.abort(existingSignal.reason)) when adding existingSignal.addEventListener('abort', onAbort) and then remove that listener in the cleanup path (the finally block after fetch) via existingSignal.removeEventListener('abort', onAbort); keep the immediate-aborted branch unchanged and only install/remove the handler when existingSignal is present to avoid the memory leak.
137-151:streamChatне использует таймаут — уточните, является ли это намеренным.В отличие от остальных API-вызовов,
streamChatиспользует прямойfetchбезfetchWithTimeout. Для streaming-соединений это может быть намеренным, так как передача данных может занимать длительное время. Однако отсутствие таймаута на установление соединения (connection timeout) может привести к зависанию при сетевых проблемах.Рассмотрите добавление таймаута хотя бы на фазу установления соединения, или добавьте комментарий, поясняющий намеренный пропуск таймаута для streaming.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/lib/api.ts` around lines 137 - 151, The streamChat function currently uses a plain fetch to `${backendUrl}/api/stream/${sessionId}` which omits any connection timeout; either wrap the initial request in the existing fetchWithTimeout helper or implement an AbortController-based short timeout for the connection phase inside streamChat so the request fails quickly on network hangs while preserving streaming of response chunks; update streamChat to call fetchWithTimeout (or create a temporary controller that aborts after X seconds but does not abort while reading the stream) and/or add a clear comment in streamChat explaining the intentional lack of full request timeout if you decide to keep the current behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/src/middleware/rate-limit.ts`:
- Line 58: The current rateLimit()/aiRateLimit() calls call
createRateLimiter(...) each time which spins up a new store and setInterval per
invocation; cache and reuse a single middleware instance per limiter
configuration/type instead. Change rateLimit and aiRateLimit to look up a shared
Map keyed by the limiter type or config (e.g., "ai" or a stringified
{max,windowMs}), create the middleware via createRateLimiter(...) only if
absent, store it, and return the cached middleware for subsequent calls so
store/setInterval are not duplicated (refer to the rateLimit, aiRateLimit
functions and createRateLimiter).
In `@ui/tailwind.config.js`:
- Around line 91-198: The keyframes block is defined at theme.keyframes which
overrides Tailwind's defaults (removing spin, pulse, ping, bounce); move the
entire keyframes object into theme.extend.keyframes so your custom keyframes
merge with built-ins. Update the config by relocating the current keyframes
declaration under the existing extend object (reference: theme.extend.keyframes)
so utilities like animate-spin and animate-pulse (and other default animations)
continue to work alongside your custom animations.
---
Outside diff comments:
In `@ui/src/lib/api.ts`:
- Around line 246-283: analyzePageSweep currently posts frames without
validating each frame.screenshot; call the existing validateScreenshot function
for every frame (e.g., iterate data.frames and run
validateScreenshot(frame.screenshot)) and reject/throw or return a clear error
when validation fails before calling fetchWithTimeout; ensure you reference
validateScreenshot and analyzePageSweep so the check happens early (and
preferably trim or reject oversized screenshots) to match behavior of
analyzeComponent/analyzeBrandConsistency.
- Around line 296-325: The analyzeFlow function sends screenshots without
validating their size/format like analyzePageSweep does; add a validation step
(reuse or implement validateScreenshots) that iterates over the screenshots
record passed into analyzeFlow and ensures each value is a base64 string and
below the agreed size limit (e.g., ~3MB / character length threshold), and
either strip/omit oversized entries or throw a clear error; call this validation
before JSON.stringify(data) in analyzeFlow (near fetchWithTimeout/backendUrl) so
only validated screenshots are sent to the /api/analyze-flow endpoint.
---
Duplicate comments:
In `@backend/src/routes/flow.ts`:
- Around line 26-27: Валидация поля body.screenshots в обработчике маршрута
(flow.ts) неверна: поправьте проверку так, чтобы она отклоняла массивы и
проверяла, что screenshots — плоский словарь frameId->base64 string; например,
сначала отвергнуть Array.isArray(body.screenshots), затем пройти по
Object.entries(body.screenshots) и вернуть 400 если любой ключ не строка или
любое значение не непустая строка (опционально проверить соответствие base64
паттерну). Обновите сообщение об ошибке и убедитесь, что analyzeFlow будет
получать только корректную структуру frameId->base64.
In `@research/figma-api-capabilities-2025.md`:
- Line 178: В строке таблицы, где указано "Update 108~" (строка с датой "Apr
2025"), удалите лишний символ тильды и измените значение на "Update 108" чтобы
соответствовать формату остальных записей; отредактируйте соответствующую ячейку
таблицы (значение в колонке версии — "Update 108~") заменив на "Update 108".
---
Nitpick comments:
In `@research/figma-api-capabilities-2025.md`:
- Around line 18-41: The Markdown tables (e.g., the tables listing methods like
getVariableByIdAsync/getLocalVariablesAsync, the Creation table with
createVariable/createVariableCollection/createVariableAlias, the Binding Helpers
table with setBoundVariableForPaint/setBoundVariableForEffect, and the "Library
/ Extended Collections" table) need a blank line before and after each table;
update the document by inserting an empty line above the table header and one
below the table end for each occurrence to satisfy markdownlint and improve
readability.
- Around line 572-585: The fenced code block containing the REST endpoints
(starting with "GET /v1/files/:key" and ending with "POST /v2/webhooks") needs a
language specifier for consistency; edit that code fence to include a language
token such as "http" (e.g., change ``` to ```http) so the block is highlighted
and matches other code blocks in the document.
- Around line 79-102: The code fence showing VariableScope constants (e.g.,
ALL_SCOPES, TEXT_CONTENT, CORNER_RADIUS, etc.) lacks a language specifier;
update the opening fence to include a language like "typescript" (or "text") so
the block begins with ```typescript and leave the contents unchanged to improve
Markdown rendering and syntax highlighting.
- Around line 392-396: В блоке с REST-эндпоинтами (строки содержащие "GET
/v1/files/:file_key", "GET /v1/files/:file_key/nodes?ids=x,y,z" и "GET
/v1/teams/:team_id/components") добавьте спецификатор языка для fenced code
block (например ```http или ```text) перед началом блока и оставьте закрывающий
``` после него, чтобы код выделялся корректно и улучшалась читаемость.
In `@ui/src/lib/api.ts`:
- Around line 221-229: The fetchReferoData and checkHealth functions currently
call fetch without a timeout, which can hang the UI; update both
(fetchReferoData and checkHealth) to call the shared fetchWithTimeout helper
instead of fetch, passing an appropriate timeout value (e.g., a few seconds) and
propagating/handling the timeout error similarly to existing error handling so
the functions still return { ready: false } on failure; ensure you replace the
raw fetch(...) calls with await fetchWithTimeout(...) and keep the existing
response.ok checks and JSON return behavior.
- Around line 20-36: fetchWithTimeout attaches an abort listener to
existingSignal that can retain references to the local controller if the signal
never fires; fix by registering a named handler (e.g. const onAbort = () =>
controller.abort(existingSignal.reason)) when adding
existingSignal.addEventListener('abort', onAbort) and then remove that listener
in the cleanup path (the finally block after fetch) via
existingSignal.removeEventListener('abort', onAbort); keep the immediate-aborted
branch unchanged and only install/remove the handler when existingSignal is
present to avoid the memory leak.
- Around line 137-151: The streamChat function currently uses a plain fetch to
`${backendUrl}/api/stream/${sessionId}` which omits any connection timeout;
either wrap the initial request in the existing fetchWithTimeout helper or
implement an AbortController-based short timeout for the connection phase inside
streamChat so the request fails quickly on network hangs while preserving
streaming of response chunks; update streamChat to call fetchWithTimeout (or
create a temporary controller that aborts after X seconds but does not abort
while reading the stream) and/or add a clear comment in streamChat explaining
the intentional lack of full request timeout if you decide to keep the current
behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 31018c1c-c24d-4e9d-a673-eab311b1f8d7
⛔ Files ignored due to path filters (1)
dist/ui.htmlis excluded by!**/dist/**
📒 Files selected for processing (12)
backend/src/middleware/auth.tsbackend/src/middleware/rate-limit.tsbackend/src/prompts/brand-consistency.tsbackend/src/routes/flow.tsbackend/src/routes/session.tsbackend/src/utils/sanitize.tsresearch/figma-api-capabilities-2025.mdsrc/manifest.jsonui/src/components/ui/scroll-button.tsxui/src/lib/api.tsui/tailwind.config.jsui/vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- backend/src/middleware/auth.ts
- backend/src/routes/session.ts
- backend/src/prompts/brand-consistency.ts
- src/manifest.json
- ui/src/components/ui/scroll-button.tsx
- ui/vite.config.ts
- backend/src/utils/sanitize.ts
| const rawWindow = parseInt(process.env.RATE_LIMIT_WINDOW_MS || '60000', 10); | ||
| const max = Number.isFinite(rawMax) && rawMax > 0 ? rawMax : 60; | ||
| const windowMs = Number.isFinite(rawWindow) && rawWindow > 0 ? rawWindow : 60000; | ||
| return createRateLimiter(max, windowMs); |
There was a problem hiding this comment.
Не создавайте новый лимитер на каждый вызов rateLimit()/aiRateLimit().
Сейчас каждый return createRateLimiter(...) создаёт отдельные store и setInterval. Если aiRateLimit() подключён к нескольким AI-роутам, квота становится независимой для каждого роута, поэтому более строгий лимит можно обойти, переключаясь между endpoint'ами; заодно дублируются таймеры cleanup. Лучше кэшировать по одному экземпляру middleware на тип лимитера.
💡 Вариант исправления
+function readPositiveInt(value: string | undefined, fallback: number): number {
+ const parsed = parseInt(value ?? String(fallback), 10);
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
+}
+
+let defaultRateLimitMiddleware: MiddlewareHandler | undefined;
+let defaultAiRateLimitMiddleware: MiddlewareHandler | undefined;
+
export function rateLimit(): MiddlewareHandler {
- const rawMax = parseInt(process.env.RATE_LIMIT_MAX || '60', 10);
- const rawWindow = parseInt(process.env.RATE_LIMIT_WINDOW_MS || '60000', 10);
- const max = Number.isFinite(rawMax) && rawMax > 0 ? rawMax : 60;
- const windowMs = Number.isFinite(rawWindow) && rawWindow > 0 ? rawWindow : 60000;
- return createRateLimiter(max, windowMs);
+ if (!defaultRateLimitMiddleware) {
+ defaultRateLimitMiddleware = createRateLimiter(
+ readPositiveInt(process.env.RATE_LIMIT_MAX, 60),
+ readPositiveInt(process.env.RATE_LIMIT_WINDOW_MS, 60000),
+ );
+ }
+ return defaultRateLimitMiddleware;
}
export function aiRateLimit(): MiddlewareHandler {
- const rawMax = parseInt(process.env.AI_RATE_LIMIT_MAX || '15', 10);
- const rawWindow = parseInt(process.env.AI_RATE_LIMIT_WINDOW_MS || '60000', 10);
- const max = Number.isFinite(rawMax) && rawMax > 0 ? rawMax : 15;
- const windowMs = Number.isFinite(rawWindow) && rawWindow > 0 ? rawWindow : 60000;
- return createRateLimiter(max, windowMs);
+ if (!defaultAiRateLimitMiddleware) {
+ defaultAiRateLimitMiddleware = createRateLimiter(
+ readPositiveInt(process.env.AI_RATE_LIMIT_MAX, 15),
+ readPositiveInt(process.env.AI_RATE_LIMIT_WINDOW_MS, 60000),
+ );
+ }
+ return defaultAiRateLimitMiddleware;
}Also applies to: 70-70
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/middleware/rate-limit.ts` at line 58, The current
rateLimit()/aiRateLimit() calls call createRateLimiter(...) each time which
spins up a new store and setInterval per invocation; cache and reuse a single
middleware instance per limiter configuration/type instead. Change rateLimit and
aiRateLimit to look up a shared Map keyed by the limiter type or config (e.g.,
"ai" or a stringified {max,windowMs}), create the middleware via
createRateLimiter(...) only if absent, store it, and return the cached
middleware for subsequent calls so store/setInterval are not duplicated (refer
to the rateLimit, aiRateLimit functions and createRateLimiter).
| keyframes: { | ||
| typing: { | ||
| '0%, 100%': { | ||
| transform: 'translateY(0)', | ||
| opacity: '0.5' | ||
| }, | ||
| '50%': { | ||
| transform: 'translateY(-2px)', | ||
| opacity: '1' | ||
| } | ||
| }, | ||
| 'loading-dots': { | ||
| '0%, 100%': { | ||
| opacity: '0' | ||
| }, | ||
| '50%': { | ||
| opacity: '1' | ||
| } | ||
| }, | ||
| wave: { | ||
| '0%, 100%': { | ||
| transform: 'scaleY(1)' | ||
| }, | ||
| '50%': { | ||
| transform: 'scaleY(0.6)' | ||
| } | ||
| }, | ||
| blink: { | ||
| '0%, 100%': { | ||
| opacity: '1' | ||
| }, | ||
| '50%': { | ||
| opacity: '0' | ||
| } | ||
| }, | ||
| shimmer: { | ||
| '0%': { | ||
| backgroundPosition: '200% 50%' | ||
| }, | ||
| '100%': { | ||
| backgroundPosition: '-200% 50%' | ||
| } | ||
| }, | ||
| 'text-blink': { | ||
| '0%, 100%': { | ||
| color: 'var(--primary)' | ||
| }, | ||
| '50%': { | ||
| color: 'var(--muted-foreground)' | ||
| } | ||
| }, | ||
| 'bounce-dots': { | ||
| '0%, 100%': { | ||
| transform: 'scale(0.8)', | ||
| opacity: '0.5' | ||
| }, | ||
| '50%': { | ||
| transform: 'scale(1.2)', | ||
| opacity: '1' | ||
| } | ||
| }, | ||
| 'thin-pulse': { | ||
| '0%, 100%': { | ||
| transform: 'scale(0.95)', | ||
| opacity: '0.8' | ||
| }, | ||
| '50%': { | ||
| transform: 'scale(1.05)', | ||
| opacity: '0.4' | ||
| } | ||
| }, | ||
| 'pulse-dot': { | ||
| '0%, 100%': { | ||
| transform: 'scale(1)', | ||
| opacity: '0.8' | ||
| }, | ||
| '50%': { | ||
| transform: 'scale(1.5)', | ||
| opacity: '1' | ||
| } | ||
| }, | ||
| 'shimmer-text': { | ||
| '0%': { | ||
| backgroundPosition: '150% center' | ||
| }, | ||
| '100%': { | ||
| backgroundPosition: '-150% center' | ||
| } | ||
| }, | ||
| 'wave-bars': { | ||
| '0%, 100%': { | ||
| transform: 'scaleY(1)', | ||
| opacity: '0.5' | ||
| }, | ||
| '50%': { | ||
| transform: 'scaleY(0.6)', | ||
| opacity: '1' | ||
| } | ||
| }, | ||
| 'spinner-fade': { | ||
| '0%': { | ||
| opacity: '0' | ||
| }, | ||
| '100%': { | ||
| opacity: '1' | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) Проверка расположения keyframes в конфиге:"
rg -n '^\s*keyframes\s*:' ui/tailwind.config.js -n -C2
echo
echo "2) Проверка использования дефолтных animate-* классов в UI:"
rg -nP --glob '**/*.{ts,tsx,js,jsx,html,css}' '\banimate-(spin|pulse|ping|bounce)\b' ui || true
echo
echo "Ожидание:"
echo "- если есть usage animate-spin/pulse/ping/bounce И keyframes задан в theme.keyframes,"
echo " то перенос в theme.extend.keyframes обязателен."Repository: lemone112/figmalint
Length of output: 875
🏁 Script executed:
head -n 110 ui/tailwind.config.js | tail -n 50Repository: lemone112/figmalint
Length of output: 829
🏁 Script executed:
sed -n '1,110p' ui/tailwind.config.js | head -n 100Repository: lemone112/figmalint
Length of output: 2758
🏁 Script executed:
tail -n 20 ui/tailwind.config.jsRepository: lemone112/figmalint
Length of output: 337
🏁 Script executed:
rg -n 'animation\s*:' ui/tailwind.config.jsRepository: lemone112/figmalint
Length of output: 45
🌐 Web query:
Tailwind CSS theme.keyframes vs theme.extend.keyframes override defaults
💡 Result:
Tailwind v3 (tailwind.config.js)
theme.keyframes(top-level) replaces the entirekeyframesnamespace from the default theme (same behavior as any other top-levelthemekey). If you do this, you’ll lose Tailwind’s built-in keyframes unless you re-add them yourself. [1]theme.extend.keyframesmerges your keyframes into the existing ones (keeps defaults, adds yours). [1]- If you define a keyframe with the same name under
extend, that specific entry overrides the default entry (but doesn’t remove the rest). This follows from “extend merges” + normal object key replacement. [1]
Example:
// Replaces all default keyframes:
theme: { keyframes: { wiggle: { /*...*/ } } }
// Keeps defaults and adds wiggle:
theme: { extend: { keyframes: { wiggle: { /*...*/ } } } }Tailwind v4 note (CSS-first theming)
Tailwind v4’s recommended approach is defining animations via @theme and --animate-*, with @keyframes inside @theme when you want them included with generated animation utilities. [2]
Sources:
[1] Tailwind CSS v3 docs — Theme Configuration (Extending vs Overriding; keyframes is a theme key)
[2] Tailwind CSS v4 docs — Theme variables (Defining animation keyframes)
🏁 Script executed:
rg -n 'animation' ui/tailwind.config.js -B2 -A2Repository: lemone112/figmalint
Length of output: 45
🏁 Script executed:
wc -l ui/tailwind.config.js && sed -n '85,105p' ui/tailwind.config.jsRepository: lemone112/figmalint
Length of output: 485
Перенесите keyframes в theme.extend.keyframes: текущая позиция на уровне theme.keyframes полностью переопределяет стандартные keyframes Tailwind.
При текущей структуре конфига (строка 91: keyframes: { как сосед extend, а не внутри него) теряются встроенные keyframes: spin, pulse, ping, bounce. Несмотря на то, что в коде используются animate-spin и animate-pulse, эти утилиты останутся без соответствующих @keyframes и анимации не будут работать.
Переместите всё содержимое keyframes в theme.extend.keyframes, чтобы объединить пользовательские keyframes с дефолтными.
Исправление
theme: {
extend: {
colors: {
...
},
fontSize: {
...
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
- }
- },
- keyframes: {
+ },
+ keyframes: {
typing: {
...
},
...
'spinner-fade': {
...
}
- }
+ }
+ }
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/tailwind.config.js` around lines 91 - 198, The keyframes block is defined
at theme.keyframes which overrides Tailwind's defaults (removing spin, pulse,
ping, bounce); move the entire keyframes object into theme.extend.keyframes so
your custom keyframes merge with built-ins. Update the config by relocating the
current keyframes declaration under the existing extend object (reference:
theme.extend.keyframes) so utilities like animate-spin and animate-pulse (and
other default animations) continue to work alongside your custom animations.
Adds staggered mock messages (score card, markdown AI text, issues list, user bubble, streaming indicator, action buttons) when running in dev mode. Dynamic import ensures zero impact on production bundle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Changes
Verification
npx tsc --noEmit— all 3 projects cleannpm run bundle— plugin builds (281.8kb)npm run build— UI builds (342.76kb)npx vitest run— 99 plugin tests passcd backend && npx vitest run— 31 backend tests passTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores