plans/security-panel-chats.md
When a user clicks Fix Issue or Fix N Issues in the Security panel, Dyad should reopen the existing fix chat for that security finding instead of creating duplicate chats. If no fix chat exists yet, Dyad should create one, show it immediately, run the fix prompt, and record the association for future clicks.
When an existing fix chat is reopened, Dyad does not automatically resend the prompt; instead it shows a toast with a Re-run fix action so the user can explicitly send the fix prompt again into the same chat. This also covers the recovery case where the fix chat was created but the original prompt never ran (stream failure, app quit).
The Security panel should remain visible on the right. The chat pane should be forced open so the user can see the fix run or review the prior fix chat.
security_fix_chats SQLite table:
appId, reviewChatId, findingKey, fixChatId, createdAt.appId → apps.id, reviewChatId → chats.id, fixChatId → chats.id. Deleting the fix chat removes the mapping, so the next click creates a fresh fix chat (intended behavior).(appId, reviewChatId, findingKey).npm run db:generate; do not write migration SQL by hand.getOrCreateSecurityFixChat, taking { appId, reviewChatId, findings } and returning { chatId, created }.
findings with z.array(SecurityFindingSchema).min(1).insert ... onConflictDoNothing() against the unique index, then re-select — no check-then-insert, so a double-click cannot create two chats.createChat handler (extract a shared helper) so initialCommitHash is captured — do not do a bare db.insert(chats).Fix: <finding title> for a single finding, Fix <N> security issues for multi-select.findingKey in the main process from normalized finding data:
title|level|description (fields trimmed). Do not store raw JSON as the key — descriptions can be kilobytes and this is an indexed column.SecurityPanel:
getOrCreateSecurityFixChat from both single-finding and multi-select fix flows.isChatPanelHiddenAtom to false and select the returned chat immediately. Also set it to false in handleRunSecurityReview for consistency.streamMessage automatically when created === true.created === false:
fixingFindingKey / isFixingSelected) — there is no stream, so no onSettled will fire.toast.info("Opened existing fix chat", { action: { label: "Re-run fix", onClick } })) whose action streams the same fix prompt into the existing chat, with the normal fixing state and onSettled handling.Add a new IPC contract under securityContracts:
input: {
appId: number;
reviewChatId: number;
findings: SecurityFinding[]; // min 1
}
output: {
chatId: number;
created: boolean;
}
getLatestSecurityReview already returns chatId; use that value as reviewChatId.
e2e-tests/security_review.spec.ts:
Fix Issue; assert the fix prompt appears.Fix Issue again.Re-run fix action; assert a second fix prompt is sent into the same chat (no new chat).Fix Issue, and assert the existing or new fix chat is visible.Fix Issue again, assert a new fix chat is created.npm run build
PLAYWRIGHT_HTML_OPEN=never npm run e2e -- e2e-tests/security_review.spec.ts
Re-run fix button.