feat: Plan review and the AI assistant in the dashboard#67
Conversation
Mutating actions run directly by default. When a deployment has plan review required, the same buttons transparently switch to a preview flow: the change is computed server side and shown in a review modal with summary counts, per-change reasons and expandable before/after diffs, with sensitive values masked behind an explicit reveal. Apply and discard act on the saved plan, and a stale preview surfaces as a warning instead of applying blindly. Deployment settings moved under the Configuration tab, which now uses a reusable attached sub-tab component, and were restyled as a single expandable list with a Require Plan Review switch next to Protected Mode. Start, stop, restart, rebuild and pull are split buttons that target the whole deployment or any single service, and each service row gains its own start, stop, restart and rebuild controls.
Every surface that shows logs now carries an AI action in the log toolbar: deployment logs, operation output in both operation modals, and any future use of the shared log viewer. The assistant opens in one global modal where the user can switch between diagnose, improve, secure and explain, and ask free-text follow-up questions against the same context. When AI is not configured, the entry points stay visible and lead to the settings page instead of silently hiding. Analyses can return suggested actions; each is shown with its command or action and reasoning, and runs only on explicit click through the same APIs a human would use, so permissions, protected mode and plan review apply unchanged, with output shown inline. Failed operations default to diagnosis and successful ones to explanation. The assistant is configured from a new settings tab: any OpenAI-compatible endpoint, model name and a write-only API key, applied to the running agent without a restart.
The AI icon used a glyph that only exists in PrimeIcons 7 while the app pinned version 6, so every assistant button rendered as an empty space; the icon set is upgraded, which is additive and keeps all existing icons. The assistant also kept telling users to enable AI after they had just configured it, because the enabled status was cached from before setup; the status is now re-checked when it reads disabled and refreshed immediately after saving AI settings.
All AI entry points (log toolbar action, assistant modal header and hints, the assist button, the AI settings tab) now render the lucide sparkles icon instead of a PrimeIcons glyph that only exists in a newer icon-font version than the app ships, which left every AI button as an empty space. The icon set version is reverted; new features should use lucide as the project moves away from PrimeIcons, and the settings tab bar can now render lucide icons alongside the legacy class-based ones.
The AI assistant is now a chat, not a one-shot answer. A persistent launcher sits top-center in the dashboard header whenever AI is enabled, opening a general session where you can ask about the instance, its deployments, networks, or anything going wrong, and keep asking follow-ups in the same thread. The chat shows the assistant's investigation inline: each read-only lookup it runs (or proposes) appears as an expandable step with its result, so you see how it reached an answer. A per-session toggle chooses whether those lookups run automatically or pause for your approval, and pending lookups are shown with allow or decline before anything runs. Suggested fixes still execute only on click through the guarded APIs. Every place that shows logs (deployment logs, operation output) and every operation modal opens this same chat, seeded with what you were looking at, so the assistant starts from your context and can dig further with its tools.
… transcript A proposed read-only lookup now shows with Allow and Decline buttons beside it, like a CLI tool prompt: a single pending call resolves on one click, and when several are proposed each gets its own decision (with allow-all and decline-all), showing each outcome as the batch runs. Seeded analyses (from a log view or an operation result) send the logs or output to the model as hidden context while the conversation shows only a short prompt, so the chat no longer echoes a wall of log lines. The header launcher is larger and now reads as running the instance, not just asking, and the empty prompt invites both questions and actions.
Code Review SummaryThis PR introduces a robust Plan/Apply flow and an AI assistant integrated into the dashboard. It successfully bridges the gap between raw operations and user-reviewed changes, particularly for destructive actions. 🚀 Key Improvements
💡 Minor Suggestions
|
| }; | ||
|
|
||
| const renderMarkdown = (content: string) => | ||
| DOMPurify.sanitize(marked.parse(content, { async: false }) as string); |
There was a problem hiding this comment.
The default marked.parse behavior can include IDs and other attributes that might bypass basic sanitization or cause CSS collisions. It is safer to explicitly configure the sanitizer to allow only a safe subset of tags, especially when dealing with AI-generated markdown which can be unpredictable.
| DOMPurify.sanitize(marked.parse(content, { async: false }) as string); | |
| DOMPurify.sanitize(marked.parse(content, { async: false }) as string, { USE_PROFILES: { html: true } }); |
| const parts = computed(() => diffLines(props.before ?? "", props.after ?? "")); | ||
|
|
||
| const partLines = (part: ChangeObject<string>) => { | ||
| const lines = part.value.split("\n"); |
There was a problem hiding this comment.
In the partLines function, using lines.pop() modifies the array if the last element is empty. While functional, it's slightly cleaner to filter or slice to handle the trailing newline often returned by diffing libraries.
| const lines = part.value.split("\n"); | |
| const lines = part.value.split("\n"); | |
| return lines.length > 1 && lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines; |
Deploying flatrun-ui with
|
| Latest commit: |
7d5ee52
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://01abb2d4.flatrun-ui.pages.dev |
| Branch Preview URL: | https://feat-ai-native.flatrun-ui.pages.dev |
|
|
||
| const parts = computed(() => diffLines(props.before ?? "", props.after ?? "")); |
There was a problem hiding this comment.
When both 'before' and 'after' are null, diffLines might return an empty array or unexpected result. While types allow null, the UI should ideally handle the empty state explicitly or prevent rendering if both are missing.
| const parts = computed(() => diffLines(props.before ?? "", props.after ?? "")); | |
| const parts = computed(() => { | |
| if (props.before === null && props.after === null) return []; | |
| return diffLines(props.before ?? "", props.after ?? ""); | |
| }); |
| const detail = args.command || args.path || args.service || args.deployment || ""; | ||
| return detail ? `${humanTool(call.name)}: ${detail}` : humanTool(call.name); | ||
| }; | ||
| const parseArgs = (raw: string): Record<string, string> => { |
There was a problem hiding this comment.
JSON.parse can fail if the AI returns malformed arguments. While there is a catch block, logging the error to console might help debug integration issues with different LLM providers.
| const parseArgs = (raw: string): Record<string, string> => { | |
| const parseArgs = (raw: string): Record<string, string> => { | |
| try { | |
| return JSON.parse(raw || "{}"); | |
| } catch (e) { | |
| console.error("AI Assistant: Failed to parse tool arguments", raw, e); | |
| return {}; | |
| } | |
| }; |
Front-end for the agent's plan/apply and AI features. Depends on the agent
feat/ai-nativePR; merge that first (builds against its new endpoints).Plan review
AI assistant
413 tests, lint and build green.