diff --git a/src/routes/v2/pages/RunView/RunViewV2.tsx b/src/routes/v2/pages/RunView/RunViewV2.tsx index d642cd2f8..cbb8ae739 100644 --- a/src/routes/v2/pages/RunView/RunViewV2.tsx +++ b/src/routes/v2/pages/RunView/RunViewV2.tsx @@ -9,6 +9,7 @@ import { useEffect, useRef } from "react"; import { InfoBox } from "@/components/shared/InfoBox"; import { LoadingScreen } from "@/components/shared/LoadingScreen"; import { RemoteAuthErrorView } from "@/components/shared/RemoteAuthErrorView"; +import { useFlagValue } from "@/components/shared/Settings/useFlags"; import { BlockStack, InlineStack } from "@/components/ui/layout"; import { Paragraph } from "@/components/ui/typography"; import type { ComponentSpec } from "@/models/componentSpec"; @@ -23,6 +24,7 @@ import { ExecutionDataProvider, useExecutionData, } from "@/providers/ExecutionDataProvider"; +import { AiChatStoreProvider } from "@/routes/v2/shared/components/AiChat/AiChatStoreContext"; import { useDockAreaAccordion } from "@/routes/v2/shared/hooks/useDockAreaAccordion"; import { NodeRegistryProvider } from "@/routes/v2/shared/nodes/NodeRegistryContext"; import { SpecProvider } from "@/routes/v2/shared/providers/SpecContext"; @@ -40,6 +42,7 @@ import { RemoteAuthError } from "@/utils/fetchWithErrorHandling"; import { RunViewFlowCanvas } from "./components/RunViewFlowCanvas"; import { RunViewMenuBar } from "./components/RunViewMenuBar/RunViewMenuBar"; +import { useAiChatWindow } from "./hooks/useAiChatWindow"; import { useFocusTaskFromUrl } from "./hooks/useFocusTaskFromUrl"; import { useRunViewSelectionSync } from "./hooks/useRunViewSelectionSync"; import { useRunViewSpecLifecycle } from "./hooks/useRunViewSpecLifecycle"; @@ -152,6 +155,9 @@ const RunViewLayout = observer(function RunViewLayout({ useRunViewSelectionSync(); useFocusTaskFromUrl(spec); + const aiEnabled = useFlagValue("ai-assistant"); + useAiChatWindow(aiEnabled); + const { navigation } = useSharedStores(); const activeSpec = navigation.activeSpec; @@ -199,16 +205,18 @@ export function RunViewV2() { return (
- - - - - - - + + + + + + + + +
); diff --git a/src/routes/v2/pages/RunView/hooks/useAiChatWindow.tsx b/src/routes/v2/pages/RunView/hooks/useAiChatWindow.tsx new file mode 100644 index 000000000..1e305e034 --- /dev/null +++ b/src/routes/v2/pages/RunView/hooks/useAiChatWindow.tsx @@ -0,0 +1,32 @@ +import { useEffect } from "react"; + +import { createRunViewToolBridge } from "@/routes/v2/pages/RunView/toolBridge/runViewToolBridge"; +import { AiChatContent } from "@/routes/v2/shared/components/AiChat/AiChatContent"; +import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext"; + +const AI_CHAT_WINDOW_ID = "run-ai-assistant-chat"; + +export function useAiChatWindow(enabled: boolean) { + const { windows } = useSharedStores(); + + useEffect(() => { + if (!enabled) { + windows.closeWindow(AI_CHAT_WINDOW_ID); + return; + } + if (windows.getWindowById(AI_CHAT_WINDOW_ID)) return; + + windows.openWindow( + , + { + id: AI_CHAT_WINDOW_ID, + title: "AI Assistant", + position: { x: 100, y: 80 }, + size: { width: 380, height: 520 }, + disabledActions: ["close"], + startVisible: true, + persisted: true, + }, + ); + }, [enabled, windows]); +} diff --git a/src/routes/v2/pages/RunView/toolBridge/runViewToolBridge.ts b/src/routes/v2/pages/RunView/toolBridge/runViewToolBridge.ts new file mode 100644 index 000000000..1ab552033 --- /dev/null +++ b/src/routes/v2/pages/RunView/toolBridge/runViewToolBridge.ts @@ -0,0 +1,124 @@ +/** + * Read-only tool bridge for the RunView AI assistant. + * + * RunView inspects a completed pipeline run, so its bridge composes the + * shared run-lifecycle and debug handlers but exposes a read-only CSOM + * slice: `getPipelineState` / `validatePipeline` work against the live + * spec, while every spec-mutating method short-circuits with an error. + * This satisfies the full `ToolBridgeApi` contract without depending on + * the Editor's spec-mutation actions or an undo store. + */ +import type { ToolBridgeApi, ValidationResult } from "@/agent/toolBridgeApi"; +import { validateSpec } from "@/models/componentSpec/validation/validateSpec"; +import { serializeSpecForAi } from "@/routes/v2/shared/components/AiChat/serializeSpecForAi"; +import { createDebugBridgeHandlers } from "@/routes/v2/shared/components/AiChat/toolBridge/debugBridge"; +import { createRunBridgeHandlers } from "@/routes/v2/shared/components/AiChat/toolBridge/runBridge"; +import type { BridgeDeps } from "@/routes/v2/shared/components/AiChat/toolBridge/utils"; +import { requireSpec } from "@/routes/v2/shared/components/AiChat/toolBridge/utils"; + +const READ_ONLY_ERROR = + "This is a read-only run view — the pipeline spec cannot be edited here."; + +type ReadOnlyCsomHandlers = Pick< + ToolBridgeApi, + | "getPipelineState" + | "setPipelineName" + | "setPipelineDescription" + | "addTask" + | "deleteTask" + | "renameTask" + | "addInput" + | "deleteInput" + | "renameInput" + | "addOutput" + | "deleteOutput" + | "renameOutput" + | "connectNodes" + | "deleteEdge" + | "setTaskArgument" + | "createSubgraph" + | "unpackSubgraph" + | "validatePipeline" +>; + +function createReadOnlyCsomHandlers(deps: BridgeDeps): ReadOnlyCsomHandlers { + return { + async getPipelineState() { + return serializeSpecForAi(requireSpec(deps), { + activeSubgraphPath: deps.getActiveSubgraphPath(), + }); + }, + + async validatePipeline(): Promise { + const issues = validateSpec(requireSpec(deps)); + return { + valid: issues.length === 0, + issueCount: issues.length, + issues: issues.map((i) => ({ + type: i.type, + severity: i.severity, + message: i.message, + entityId: i.entityId, + issueCode: i.issueCode, + })), + }; + }, + + async setPipelineName() { + return { success: false }; + }, + async setPipelineDescription() { + return { success: false }; + }, + async addTask() { + return { success: false, error: READ_ONLY_ERROR }; + }, + async deleteTask() { + return { success: false }; + }, + async renameTask() { + return { success: false }; + }, + async addInput() { + return { success: false, inputId: "", name: "" }; + }, + async deleteInput() { + return { success: false }; + }, + async renameInput() { + return { success: false }; + }, + async addOutput() { + return { success: false, outputId: "", name: "" }; + }, + async deleteOutput() { + return { success: false }; + }, + async renameOutput() { + return { success: false }; + }, + async connectNodes() { + return { success: false, error: READ_ONLY_ERROR }; + }, + async deleteEdge() { + return { success: false }; + }, + async setTaskArgument() { + return { success: false, error: READ_ONLY_ERROR }; + }, + async createSubgraph() { + return { success: false, error: READ_ONLY_ERROR }; + }, + async unpackSubgraph() { + return { success: false }; + }, + }; +} + +export function createRunViewToolBridge(deps: BridgeDeps): ToolBridgeApi { + return { + ...createReadOnlyCsomHandlers(deps), + ...createRunBridgeHandlers(deps), + ...createDebugBridgeHandlers(deps), + }; +}