desktop: Fix with AI on scan findings that need a code change (aiFix: true, AI_FIX_RULES). The host (src-tauri/src/ai_fix.rs) runs the owner's local Claude Code headless in the app folder with Read / Edit / Write / Grep / Glob only (Bash, web tools, sub-agents disallowed) and a prompt it builds from the finding's fields; it refuses while the folder has uncommitted files, streams progress, times out after 10 min with a heartbeat, kills the process group on Cancel or exit, and records the run's changed files so Undo restores exactly those; the window shows the diff with Keep / Undo and rescans. Without claude the button copies the prompt (Copy for AI). Only the claude binary, git and kill / taskkill are started there (tests/security.rs); tests/ai_fix.rs covers the prompt, dirty-tree refusal, undo scope, timeout and cancel
This commit is contained in:
parent
992f5bcc0e
commit
c95f2af8ff
24 changed files with 1023 additions and 10 deletions
|
|
@ -3,9 +3,12 @@
|
|||
import type { Project } from '../../api';
|
||||
import { t } from '../../i18n';
|
||||
import { ago } from '../../lib/format';
|
||||
import type { Finding, FixAction, ScanResult, ScanSeverity, ValidateResult } from '../../lib/model';
|
||||
import type { Finding, FixAction, ScanFinding, ScanResult, ScanSeverity, ValidateResult } from '../../lib/model';
|
||||
import { useData, useRead, useWrite } from '../../store';
|
||||
import { WriteButton } from '../../ui/WriteButton';
|
||||
import { AiFix, aiFinding } from '../../ui/AiFix';
|
||||
import { api } from '../../api';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Icon } from '../../ui/icons';
|
||||
import { Card, Chip, Empty, ErrorNotice, ReadControl, Skeleton } from '../../ui/kit';
|
||||
import { exportName, validateMd } from '../../lib/exports';
|
||||
|
|
@ -33,6 +36,20 @@ function ScanCard({ project }: { project: Project }) {
|
|||
const s = useRead<ScanResult>(project.id, 'scan', { network: false, title: t('checks.scan') });
|
||||
const w = useWrite(project.id);
|
||||
const findings = Array.isArray(s.data?.findings) ? s.data!.findings : [];
|
||||
// Fix with AI: is claude installed (else Copy for AI); one run at a time
|
||||
const [claude, setClaude] = useState<boolean | null>(null);
|
||||
const [aiBusy, setAiBusy] = useState(false);
|
||||
// the finding an AI fix works on stays in the list (its diff, Keep / Undo) even after the scan stops reporting it
|
||||
const [aiActive, setAiActive] = useState<ScanFinding | null>(null);
|
||||
const same = (a: ScanFinding, b: ScanFinding) => a.store === b.store && a.rule === b.rule && a.title === b.title;
|
||||
const rows = aiActive && !findings.some(x => same(x, aiActive)) ? [...findings, aiActive] : findings;
|
||||
const seen = new Map<string, number>();
|
||||
const keyOf = (f: ScanFinding) => { const k = `${f.store}|${f.rule}|${f.title}`; const n = (seen.get(k) ?? 0) + 1; seen.set(k, n); return `${k}|${n}`; };
|
||||
const firstAi = findings.find(f => f.aiFix && !f.accepted && !fixable(f.action));
|
||||
useEffect(() => {
|
||||
if (!firstAi || claude !== null) return;
|
||||
api.aiFixInfo(project.id, aiFinding(firstAi)).then(i => setClaude(i.claude), () => setClaude(false));
|
||||
}, [project.id, firstAi, claude]);
|
||||
const sdks = Array.isArray(s.data?.sdks) ? s.data!.sdks : [];
|
||||
const asks = (Array.isArray(s.data?.declarations?.items) ? s.data!.declarations.items : []).filter(x => x.status === 'ask').length;
|
||||
return (
|
||||
|
|
@ -40,19 +57,21 @@ function ScanCard({ project }: { project: Project }) {
|
|||
actions={<ReadControl r={s} label={t('checks.scanAgain')} />}>
|
||||
{s.error && <div className="card-body"><ErrorNotice error={s.error} keep={!!s.data} /></div>}
|
||||
{!s.data ? (s.loading && !s.error ? <div className="card-body"><Skeleton lines={4} /></div> : null)
|
||||
: !findings.length ? <Empty compact illo="shield" title={t('checks.scanClean')}>{t('checks.scanCleanText')}</Empty> : (
|
||||
: !rows.length ? <Empty compact illo="shield" title={t('checks.scanClean')}>{t('checks.scanCleanText')}</Empty> : (
|
||||
<div className="list">
|
||||
{findings.map((f, i) => {
|
||||
{rows.map(f => {
|
||||
const [tone, icon] = f.accepted ? ['ok', 'check'] as const : SEV[f.severity] ?? SEV.low;
|
||||
const ev = Array.isArray(f.evidence) ? f.evidence : [];
|
||||
return (
|
||||
<div className="li" key={`${f.rule}-${i}`}>
|
||||
<div className="li" key={keyOf(f)}>
|
||||
<span className={`li-icon tone-bg-${tone}`}><Icon name={icon} /></span>
|
||||
<div className="li-main">
|
||||
<span className="li-title">{f.guideline} · {f.title}</span>
|
||||
{ev.slice(0, 2).map((e, j) => <span className="li-sub selectable" key={j}><code>{e.file}:{e.line}</code>{e.text ? ` ${e.text}` : ''}</span>)}
|
||||
{f.accepted ? <span className="li-sub">{t('checks.accepted', { reason: f.accepted })}</span>
|
||||
: <span className="fix"><Icon name="wand" /><span className="selectable">{f.fix} <code>{f.skill}</code></span></span>}
|
||||
{!f.accepted && f.aiFix && !fixable(f.action) && <AiFix projectId={project.id} f={f} claude={claude} busy={aiBusy} setBusy={setAiBusy} rescan={() => { s.refresh(); }}
|
||||
stillThere={findings.some(x => same(x, f))} onActive={on => setAiActive(on ? f : null)} />}
|
||||
</div>
|
||||
{!f.accepted && fixable(f.action) && <FixButton action={f.action} what={f.title} w={w} />}
|
||||
<Chip tone={f.accepted ? 'ok' : tone}>{t(f.accepted ? 'checks.sev.accepted' : `checks.sev.${f.severity}` as Parameters<typeof t>[0])}</Chip>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue