From 828e7710d498ed56bc2ba42dececae1478adf040 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Wed, 11 Mar 2026 15:57:06 +0000 Subject: [PATCH 01/10] fix: pipeline AI chat scroll by adding min-h-0 to flex chain --- src/components/flow/ai-pipeline-dialog.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/flow/ai-pipeline-dialog.tsx b/src/components/flow/ai-pipeline-dialog.tsx index 3172715..f5a3d65 100644 --- a/src/components/flow/ai-pipeline-dialog.tsx +++ b/src/components/flow/ai-pipeline-dialog.tsx @@ -258,7 +258,7 @@ export function AiPipelineDialog({ return ( - + @@ -269,7 +269,7 @@ export function AiPipelineDialog({ - setMode(v as "generate" | "review")} className="flex flex-col flex-1 overflow-hidden"> + setMode(v as "generate" | "review")} className="flex flex-col flex-1 min-h-0 overflow-hidden"> Generate @@ -335,7 +335,7 @@ export function AiPipelineDialog({ {/* ---- Review tab (conversation thread) ---- */} - + {conversation.isLoading ? (
From e11b8855db697ffa96bd1f9f4e41ec3b6a18f9ec Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Wed, 11 Mar 2026 15:57:31 +0000 Subject: [PATCH 02/10] fix: use normalized whitespace matching for VRL suggestion status checks --- src/lib/ai/vrl-suggestion-types.ts | 71 +++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/src/lib/ai/vrl-suggestion-types.ts b/src/lib/ai/vrl-suggestion-types.ts index 1e00bf8..1893807 100644 --- a/src/lib/ai/vrl-suggestion-types.ts +++ b/src/lib/ai/vrl-suggestion-types.ts @@ -39,6 +39,36 @@ export function parseVrlChatResponse(raw: string): VrlChatResponse | null { } } +/** Collapse all whitespace runs to single space and trim. */ +function normalizeWhitespace(s: string): string { + return s.replace(/\s+/g, " ").trim(); +} + +/** + * Find a substring match using normalized whitespace comparison. + * Returns the original start/end indices in haystack, or null. + */ +function findNormalizedMatch( + haystack: string, + needle: string, +): { start: number; end: number } | null { + const normNeedle = normalizeWhitespace(needle); + if (!normNeedle) return null; + + // Slide start through haystack + for (let start = 0; start < haystack.length; start++) { + // Try windows of varying length around the expected needle length (±10 chars) + const minEnd = Math.max(start + 1, start + needle.length - 10); + const maxEnd = Math.min(haystack.length, start + needle.length + 10); + for (let end = minEnd; end <= maxEnd; end++) { + if (normalizeWhitespace(haystack.slice(start, end)) === normNeedle) { + return { start, end }; + } + } + } + return null; +} + /** * Compute the status of each VRL suggestion based on the current editor content. * @@ -64,8 +94,16 @@ export function computeVrlSuggestionStatuses( } // replace_code and remove_code: check if targetCode exists in current editor - if (s.targetCode && currentCode.includes(s.targetCode)) { - statuses.set(s.id, "actionable"); + if (s.targetCode) { + // Fast path: exact match + if (currentCode.includes(s.targetCode)) { + statuses.set(s.id, "actionable"); + // Fallback: normalized whitespace match + } else if (findNormalizedMatch(currentCode, s.targetCode)) { + statuses.set(s.id, "actionable"); + } else { + statuses.set(s.id, "outdated"); + } } else { statuses.set(s.id, "outdated"); } @@ -88,17 +126,30 @@ export function applyVrlSuggestion( ? `${currentCode}\n${suggestion.code}` : suggestion.code; - case "replace_code": - if (!suggestion.targetCode || !currentCode.includes(suggestion.targetCode)) { - return null; + case "replace_code": { + if (!suggestion.targetCode) return null; + // Fast path: exact match + if (currentCode.includes(suggestion.targetCode)) { + return currentCode.replaceAll(suggestion.targetCode, suggestion.code); } - return currentCode.replaceAll(suggestion.targetCode, suggestion.code); + // Fallback: normalized whitespace match + const match = findNormalizedMatch(currentCode, suggestion.targetCode); + if (!match) return null; + return currentCode.slice(0, match.start) + suggestion.code + currentCode.slice(match.end); + } - case "remove_code": - if (!suggestion.targetCode || !currentCode.includes(suggestion.targetCode)) { - return null; + case "remove_code": { + if (!suggestion.targetCode) return null; + // Fast path: exact match + if (currentCode.includes(suggestion.targetCode)) { + return currentCode.replaceAll(suggestion.targetCode, "").replace(/\n{3,}/g, "\n\n").trim(); } - return currentCode.replaceAll(suggestion.targetCode, "").replace(/\n{3,}/g, "\n\n").trim(); + // Fallback: normalized whitespace match + const match = findNormalizedMatch(currentCode, suggestion.targetCode); + if (!match) return null; + const removed = currentCode.slice(0, match.start) + currentCode.slice(match.end); + return removed.replace(/\n{3,}/g, "\n\n").trim(); + } default: return null; From db49892ec15ba6e309f850a0848adb34309aaa33 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Wed, 11 Mar 2026 16:00:21 +0000 Subject: [PATCH 03/10] =?UTF-8?q?fix:=20VRL=20editor=20modal=20layout=20?= =?UTF-8?q?=E2=80=94=20single=20right=20panel=20slot,=20editor=20stays=20f?= =?UTF-8?q?ull-width?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/vrl-editor/vrl-editor.tsx | 368 +++++++++++------------ 1 file changed, 181 insertions(+), 187 deletions(-) diff --git a/src/components/vrl-editor/vrl-editor.tsx b/src/components/vrl-editor/vrl-editor.tsx index 72608cb..35af342 100644 --- a/src/components/vrl-editor/vrl-editor.tsx +++ b/src/components/vrl-editor/vrl-editor.tsx @@ -28,7 +28,7 @@ import { VrlSnippetDrawer } from "@/components/flow/vrl-snippet-drawer"; import { VrlFieldsPanel } from "./vrl-fields-panel"; import { VrlAiPanel } from "./vrl-ai-panel"; import { useVrlAiConversation } from "@/hooks/use-vrl-ai-conversation"; -import { cn } from "@/lib/utils"; + import { useTeamStore } from "@/stores/team-store"; import { getMergedOutputSchemas, getSourceOutputSchema } from "@/lib/vector/source-output-schemas"; import type { Monaco, OnMount } from "@monaco-editor/react"; @@ -69,7 +69,8 @@ export function VrlEditor({ value, onChange, sourceTypes, pipelineId, componentK const [sampleInput, setSampleInput] = useState(""); const [testOutput, setTestOutput] = useState(null); const [testError, setTestError] = useState(null); - const [toolsPanel, setToolsPanel] = useState<"fields" | "snippets" | null>(null); + type RightPanel = "fields" | "snippets" | "ai" | null; + const [rightPanel, setRightPanel] = useState("fields"); const [expanded, setExpanded] = useState(false); const editorRef = useRef(null); const monacoRef = useRef(null); @@ -91,10 +92,12 @@ export function VrlEditor({ value, onChange, sourceTypes, pipelineId, componentK ); const aiEnabled = teamQuery.data?.aiEnabled ?? false; - const [aiPanelOpen, setAiPanelOpen] = useState(false); - const canUseAiChat = aiEnabled && !!pipelineId && !!componentKey; + const togglePanel = (panel: RightPanel) => { + setRightPanel((prev) => (prev === panel ? null : panel)); + }; + const isRawTextSource = useMemo(() => { if (!sourceTypes || sourceTypes.length === 0) return false; return sourceTypes.some((t) => { @@ -183,7 +186,7 @@ export function VrlEditor({ value, onChange, sourceTypes, pipelineId, componentK const schema = (sample.schema as Array<{ path: string; type: string; sample: string }>) ?? []; setLiveSchemaFields(schema); if (schema.length > 0) { - setToolsPanel("fields"); + setRightPanel("fields"); } } } else if (data.status === "ERROR" || data.status === "EXPIRED") { @@ -360,17 +363,73 @@ export function VrlEditor({ value, onChange, sourceTypes, pipelineId, componentK {/* Full-screen modal: editor (left) + tools (right) */} e.stopPropagation()} > VRL Editor + + {/* Toolbar — always visible */} +
+ {hasFields && ( + + )} + + {canUseAiChat && ( + + )} + {pipelineId && upstreamSourceKeys && upstreamSourceKeys.length > 0 && ( + <> + + + {sampleEvents.length > 0 && ( + + {sampleEvents.length} sample{sampleEvents.length !== 1 ? "s" : ""} + + )} + + )} +
+
{/* Left: Monaco editor at full height */}
@@ -393,190 +452,125 @@ export function VrlEditor({ value, onChange, sourceTypes, pipelineId, componentK />
- {/* Right: tools pane */} -
- {/* Action buttons */} -
- {hasFields && ( - - )} - - {canUseAiChat && ( - - )} - {pipelineId && upstreamSourceKeys && upstreamSourceKeys.length > 0 && ( - <> - - - {sampleEvents.length > 0 && ( - - {sampleEvents.length} sample{sampleEvents.length !== 1 ? "s" : ""} - + {/* Right panel: single slot for tools OR AI */} + {rightPanel && ( +
+ {/* Tools content (fields, snippets, test panel) */} + {(rightPanel === "fields" || rightPanel === "snippets") && ( +
+ {/* Raw text source hint */} + {isRawTextSource && rightPanel !== "snippets" && ( +
+

This source emits raw text in .message

+

+ Use a parsing function to extract fields — click Snippets → Parsing for examples. +

+
)} - - )} -
- {/* Raw text source hint */} - {isRawTextSource && toolsPanel !== "snippets" && ( -
-

This source emits raw text in .message

-

- Use a parsing function to extract fields — click Snippets → Parsing for examples. -

-
- )} - - {/* Fields panel */} - {toolsPanel === "fields" && ( - - )} - - {/* Snippet drawer */} - {toolsPanel === "snippets" && ( - - )} - - {/* Test panel (always visible) */} -
-
-
- - {sampleEvents.length > 1 && ( -
- - - {sampleIndex + 1}/{sampleEvents.length} - - -
+ {rightPanel === "fields" && ( + )} -
-