Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions src/routes/v2/pages/RunView/RunViewV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -199,16 +205,18 @@ export function RunViewV2() {
return (
<div className="h-full w-full flex flex-col bg-slate-100 select-none">
<SharedStoreProvider>
<ReactFlowProvider>
<ContextPanelProvider /** TODO: remove ContextPanelProvider */>
<ExecutionDataProvider
pipelineRunId={id}
subgraphExecutionId={subgraphExecutionId}
>
<RunViewContent />
</ExecutionDataProvider>
</ContextPanelProvider>
</ReactFlowProvider>
<AiChatStoreProvider>
<ReactFlowProvider>
<ContextPanelProvider /** TODO: remove ContextPanelProvider */>
<ExecutionDataProvider
pipelineRunId={id}
subgraphExecutionId={subgraphExecutionId}
>
<RunViewContent />
</ExecutionDataProvider>
</ContextPanelProvider>
</ReactFlowProvider>
</AiChatStoreProvider>
</SharedStoreProvider>
</div>
);
Expand Down
32 changes: 32 additions & 0 deletions src/routes/v2/pages/RunView/hooks/useAiChatWindow.tsx
Original file line number Diff line number Diff line change
@@ -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(
<AiChatContent createBridge={createRunViewToolBridge} />,
{
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]);
}
124 changes: 124 additions & 0 deletions src/routes/v2/pages/RunView/toolBridge/runViewToolBridge.ts
Original file line number Diff line number Diff line change
@@ -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<ValidationResult> {
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),
};
}
Loading