diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeChatThread.test.tsx b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeChatThread.test.tsx
new file mode 100644
index 0000000000..0f545887a5
--- /dev/null
+++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeChatThread.test.tsx
@@ -0,0 +1,136 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { ROUTES } from '@studio/constants/routes';
+import { ClaudeCodeChatThread } from '@studio/routes/agents/ClaudeCodeChatRoute/ClaudeCodeChatThread';
+import type {
+ ClaudeCodeChatRuntime,
+ StudioNavigationRequest,
+} from '@studio/routes/agents/ClaudeCodeChatRoute/useClaudeCodeChatRuntime';
+import { TestProviders } from '@studio/tests/util/TestProviders';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import type { ReactNode } from 'react';
+import { createMemoryRouter, generatePath, RouterProvider } from 'react-router-dom';
+
+const mocks = vi.hoisted(() => ({
+ resolveStudioNavigationRequest: vi.fn(),
+}));
+
+vi.mock('@assistant-ui/react', () => ({
+ AssistantRuntimeProvider: ({ children }: { children: ReactNode }) => <>{children}>,
+}));
+
+vi.mock('@nemo/common/src/components/AssistantChat/AssistantChatThread', () => ({
+ AssistantChatThread: ({
+ composerOverride,
+ showRunningIndicator,
+ }: {
+ composerOverride?: ReactNode;
+ showRunningIndicator?: boolean;
+ }) => (
+
+ {composerOverride}
+
+ ),
+}));
+
+const WORKSPACE = 'default';
+const CHAT_PATH = generatePath(ROUTES.workspace.claudeCodeChat, { workspace: WORKSPACE });
+
+const makeStudioNavigationRequest = (
+ overrides?: Partial
+): StudioNavigationRequest => ({
+ id: 'guardrails:1',
+ prompt: 'Add guardrails to an agent',
+ suggestion: {
+ id: 'guardrails',
+ title: 'Open Guardrails',
+ description: 'Studio has a UI for managing NeMo Guardrails configurations.',
+ href: '/workspaces/default/guardrails',
+ },
+ ...overrides,
+});
+
+const makeChat = (studioNavigationRequest: StudioNavigationRequest | null) =>
+ ({
+ artifacts: { selections: [], files: [], links: [], tools: [] },
+ decisionChoices: [],
+ decisionRequest: null,
+ decisionStatus: 'pending',
+ handleReset: vi.fn(),
+ inputRequest: null,
+ inputStatus: 'pending',
+ isRunning: false,
+ loadSession: vi.fn(),
+ resolveDecisionRequest: vi.fn(),
+ resolveInputRequest: vi.fn(),
+ resolveStudioNavigationRequest: mocks.resolveStudioNavigationRequest,
+ runtime: {},
+ sessionId: null,
+ skipDecisionRequest: vi.fn(),
+ skipInputRequest: vi.fn(),
+ studioNavigationRequest,
+ studioNavigationStatus: 'pending',
+ submitPrompt: vi.fn(),
+ }) as unknown as ClaudeCodeChatRuntime;
+
+const renderThread = (studioNavigationRequest = makeStudioNavigationRequest()) => {
+ const router = createMemoryRouter(
+ [
+ {
+ path: ROUTES.workspace.claudeCodeChat,
+ element: ,
+ },
+ { path: ROUTES.workspace.guardrails, element: },
+ ],
+ { initialEntries: [CHAT_PATH] }
+ );
+
+ return render(
+
+
+
+ );
+};
+
+describe('ClaudeCodeChatThread Studio UI navigation', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('offers the matching Studio UI before continuing with Claude Code', () => {
+ renderThread();
+
+ expect(screen.getByText('Studio UI available')).toBeInTheDocument();
+ expect(screen.getByRole('option', { name: /1\.\s+Open Guardrails/i })).toBeInTheDocument();
+ expect(screen.getByRole('option', { name: /2\.\s+Continue in chat/i })).toBeInTheDocument();
+ expect(screen.getByTestId('assistant-chat-thread')).toHaveAttribute(
+ 'data-show-running-indicator',
+ 'false'
+ );
+ });
+
+ it('resolves with navigate and opens the Studio route', async () => {
+ const user = userEvent.setup();
+ renderThread();
+
+ await user.click(screen.getByRole('option', { name: /1\.\s+Open Guardrails/i }));
+
+ expect(mocks.resolveStudioNavigationRequest).toHaveBeenCalledWith('navigate');
+ expect(await screen.findByTestId('guardrails-route')).toBeInTheDocument();
+ });
+
+ it('resolves with continue when the user keeps chatting', async () => {
+ const user = userEvent.setup();
+ renderThread();
+
+ await user.click(screen.getByRole('option', { name: /2\.\s+Continue in chat/i }));
+
+ expect(mocks.resolveStudioNavigationRequest).toHaveBeenCalledWith('continue');
+ expect(screen.queryByTestId('guardrails-route')).not.toBeInTheDocument();
+ });
+});
diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeChatThread.tsx b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeChatThread.tsx
index c18553fd1d..6924dfd02e 100644
--- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeChatThread.tsx
+++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeChatThread.tsx
@@ -4,13 +4,17 @@
import { AssistantRuntimeProvider } from '@assistant-ui/react';
import { AssistantChatThread } from '@nemo/common/src/components/AssistantChat/AssistantChatThread';
import { type AgentBlockingInputSubmission } from '@studio/components/agents/AgentBlockingInput';
-import { AgentDecisionInput } from '@studio/components/agents/AgentDecisionInput';
+import {
+ AgentDecisionInput,
+ type AgentDecisionChoice,
+} from '@studio/components/agents/AgentDecisionInput';
import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath';
import { BlockingInputComposer } from '@studio/routes/agents/ClaudeCodeChatRoute/BlockingInputComposer';
import { ClaudeCodeStudioLink } from '@studio/routes/agents/ClaudeCodeChatRoute/ClaudeCodeStudioLink';
import { ClaudeCodeToolCallPart } from '@studio/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart';
import type { ClaudeCodeChatRuntime } from '@studio/routes/agents/ClaudeCodeChatRoute/useClaudeCodeChatRuntime';
-import { type FC, useCallback, useLayoutEffect, useRef } from 'react';
+import { type FC, useCallback, useLayoutEffect, useMemo, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
const CHAT_VIEWPORT_SCROLLBAR_CLASS = [
'[scrollbar-width:thin]',
@@ -37,6 +41,7 @@ export const ClaudeCodeChatThread: FC = ({
scrollToBottomSignal,
}) => {
const workspace = useWorkspaceFromPath();
+ const navigate = useNavigate();
const chatViewportRef = useRef(null);
const {
decisionChoices,
@@ -47,11 +52,33 @@ export const ClaudeCodeChatThread: FC = ({
inputStatus,
resolveInputRequest,
resolveDecisionRequest,
+ resolveStudioNavigationRequest,
runtime,
skipInputRequest,
skipDecisionRequest,
+ studioNavigationRequest,
+ studioNavigationStatus,
} = chat;
+ const studioNavigationChoices = useMemo(
+ () =>
+ studioNavigationRequest
+ ? [
+ {
+ id: 'open-ui',
+ label: studioNavigationRequest.suggestion.title,
+ description: 'Use the guided Studio UI for this workflow.',
+ },
+ {
+ id: 'continue-chat',
+ label: 'Continue in chat',
+ description: 'Keep working with Claude Code in this conversation.',
+ },
+ ]
+ : [],
+ [studioNavigationRequest]
+ );
+
const scrollViewportToBottom = useCallback(() => {
const viewport = chatViewportRef.current;
if (!viewport) return undefined;
@@ -85,10 +112,26 @@ export const ClaudeCodeChatThread: FC = ({
[resolveInputRequest]
);
+ const handleStudioNavigationSubmit = useCallback(
+ (choice: AgentDecisionChoice) => {
+ const request = studioNavigationRequest;
+ if (!request) return;
+
+ if (choice.id === 'open-ui') {
+ resolveStudioNavigationRequest('navigate');
+ navigate(request.suggestion.href);
+ return;
+ }
+
+ resolveStudioNavigationRequest('continue');
+ },
+ [navigate, resolveStudioNavigationRequest, studioNavigationRequest]
+ );
+
useLayoutEffect(() => {
- if (!decisionRequest && !inputRequest) return undefined;
+ if (!studioNavigationRequest && !decisionRequest && !inputRequest) return undefined;
return scrollViewportToBottom();
- }, [decisionRequest, inputRequest, scrollViewportToBottom]);
+ }, [decisionRequest, inputRequest, scrollViewportToBottom, studioNavigationRequest]);
useLayoutEffect(() => {
if (scrollToBottomSignal === undefined) return undefined;
@@ -114,14 +157,26 @@ export const ClaudeCodeChatThread: FC = ({
}}
placeholder="Ask Claude Code to work in this workspace"
onReset={handleChatReset}
- showRunningIndicator={!decisionRequest && !inputRequest}
+ showRunningIndicator={!studioNavigationRequest && !decisionRequest && !inputRequest}
messageContentProps={{ markdownLinkComponent: ClaudeCodeStudioLink }}
emptyState={{
slotHeading: 'Start a Claude Code session',
slotSubheading: 'Ask Claude Code to work in this workspace.',
}}
composerOverride={
- decisionRequest ? (
+ studioNavigationRequest ? (
+
+ ) : decisionRequest ? (
{
+ beforeEach(() => {
+ mockFeatureFlags({
+ agentsEnabled: true,
+ customizerEnabled: true,
+ dataDesignerEnabled: true,
+ datasetsEnabled: true,
+ deploymentsEnabled: true,
+ evaluatorEnabled: true,
+ guardrailsEnabled: true,
+ inferenceProviderEnabled: true,
+ intakeEnabled: true,
+ jobsEnabled: true,
+ modelCompareEnabled: true,
+ safeSynthesizerEnabled: true,
+ secretsEnabled: true,
+ settingsEnabled: true,
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('returns the matching Studio destination for a product workflow', () => {
+ expect(getStudioUiNavigationSuggestion('Add guardrails to an agent', workspace)).toMatchObject({
+ id: 'guardrails',
+ href: '/workspaces/default/guardrails',
+ title: 'Open Guardrails',
+ });
+
+ expect(
+ getStudioUiNavigationSuggestion('Generate a synthetic dataset for fine tuning', workspace)
+ ).toMatchObject({
+ id: 'safe-synthesizer-new',
+ href: '/workspaces/default/safe-synthesizer/new',
+ });
+ });
+
+ it('keeps navigation shortcuts when prompts include Studio product context', () => {
+ const cases = [
+ {
+ prompt: 'Review model sizing for an agent',
+ id: 'agent-optimizations',
+ },
+ {
+ prompt: 'Show agent token usage',
+ id: 'agent-monitor',
+ },
+ {
+ prompt: 'Manage workspace secrets',
+ id: 'secrets',
+ },
+ {
+ prompt: 'Open model playground',
+ id: 'model-playground',
+ },
+ {
+ prompt: 'Show workspace job history',
+ id: 'jobs',
+ },
+ {
+ prompt: 'Review intake annotations',
+ id: 'annotation',
+ },
+ {
+ prompt: 'Open workspace settings',
+ id: 'settings',
+ },
+ {
+ prompt: 'Use the guardrails-plugin skill to debug guardrail middleware',
+ id: 'guardrails',
+ },
+ {
+ prompt: 'Use the inference skill to configure inference in this workspace',
+ id: 'inference-providers',
+ },
+ {
+ prompt: 'Use the nemo-build-agent skill to build an agent',
+ id: 'agents',
+ },
+ {
+ prompt: 'Use the nemo-evaluator skill to review eval history',
+ id: 'evaluations',
+ },
+ {
+ prompt: 'Use the safe-synthesizer skill to generate safety data',
+ id: 'safe-synthesizer-new',
+ },
+ {
+ prompt: 'Create a data generation workflow',
+ id: 'data-designer-new',
+ },
+ ];
+
+ for (const { prompt, id } of cases) {
+ expect(getStudioUiNavigationSuggestion(prompt, workspace)).toMatchObject({ id });
+ }
+ });
+
+ it('prefers agent-specific evaluation routes over general model evaluations', () => {
+ expect(getStudioUiNavigationSuggestion('Evaluate an agent', workspace)).toMatchObject({
+ id: 'agent-evaluations',
+ href: '/workspaces/default/agents/evaluations',
+ });
+ });
+
+ it('returns undefined when the matching feature is disabled', () => {
+ mockFeatureFlags({ guardrailsEnabled: false });
+
+ expect(getStudioUiNavigationSuggestion('Create guardrails for this agent', workspace)).toBe(
+ undefined
+ );
+ });
+
+ it('does not interrupt ordinary coding-agent prompts', () => {
+ const prompts = [
+ 'Review the current working tree',
+ 'Fix the settings page component',
+ 'Update settings page component',
+ 'Analyze token usage in this parser',
+ 'Add annotation support to the chart',
+ 'Open the playground component',
+ 'Create token validation helpers',
+ 'Tune model sizing calculations in this file',
+ 'Review job history component',
+ 'Build an agent class for this test helper',
+ 'Configure inference in this TypeScript module',
+ 'Generate synthetic test data in fixtures',
+ 'Review evaluator plugin imports',
+ ];
+
+ for (const prompt of prompts) {
+ expect(getStudioUiNavigationSuggestion(prompt, workspace)).toBe(undefined);
+ }
+ });
+});
diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/studioUiNavigationSuggestions.ts b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/studioUiNavigationSuggestions.ts
new file mode 100644
index 0000000000..1d099f5939
--- /dev/null
+++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/studioUiNavigationSuggestions.ts
@@ -0,0 +1,354 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { featureFlags } from '@studio/constants/featureFlags';
+import type { FeatureFlags } from '@studio/constants/featureFlags/featureFlags';
+import {
+ getAgentEvaluationsListRoute,
+ getAgentMonitorRoute,
+ getAgentOptimizationsRoute,
+ getAgentsListRoute,
+ getDataDesignerJobListRoute,
+ getEvaluationResultsRoute,
+ getGuardrailsRoute,
+ getIntakeRoute,
+ getModelCompareRoute,
+ getNewDataDesignerJobRoute,
+ getNewFilesetRoute,
+ getNewSafeSynthesizerRoute,
+ getPromptTuningFormRoute,
+ getSecretsRoute,
+ getWorkspaceBaseModelsRoute,
+ getWorkspaceCustomizationJobListRoute,
+ getWorkspaceDeploymentsRoute,
+ getWorkspaceInferenceProvidersRoute,
+ getWorkspaceJobsRoute,
+ getWorkspaceMembersRoute,
+ getWorkspaceNewCustomizationJobRoute,
+ getWorkspaceSafeSynthesizerRoute,
+ getWorkspaceSettingsRoute,
+} from '@studio/routes/utils';
+
+type FeatureFlagName = keyof FeatureFlags;
+
+export interface StudioUiNavigationSuggestion {
+ id: string;
+ title: string;
+ description: string;
+ href: string;
+}
+
+interface StudioUiDestination {
+ id: string;
+ title: string;
+ description: string;
+ getHref: (workspace: string) => string;
+ patterns: readonly RegExp[];
+ requiredFeatureFlags?: readonly FeatureFlagName[];
+}
+
+const STUDIO_UI_DESTINATIONS: readonly StudioUiDestination[] = [
+ {
+ id: 'safe-synthesizer-new',
+ title: 'Open Safe Synthesizer',
+ description: 'Studio has a guided UI for generating safe synthetic datasets.',
+ getHref: getNewSafeSynthesizerRoute,
+ requiredFeatureFlags: ['safeSynthesizerEnabled'],
+ patterns: [
+ /\bsafe[-\s]?synthesizer\b/i,
+ /\bsynthetic (data|dataset|datasets)\b/i,
+ /\bsynthesi[sz]e (data|dataset|datasets)\b/i,
+ /\bgenerate (safety[-\s]?focused|safe|synthetic) (data|dataset|datasets)\b/i,
+ /\bsafety data\b/i,
+ ],
+ },
+ {
+ id: 'agent-evaluations',
+ title: 'Open Agent Evaluations',
+ description: 'Studio has a UI for submitting and reviewing agent evaluation jobs.',
+ getHref: getAgentEvaluationsListRoute,
+ requiredFeatureFlags: ['agentsEnabled'],
+ patterns: [
+ /\bagent (eval|evaluation|evaluations)\b/i,
+ /\bevaluat(e|ing|ion)s? (an? )?agent\b/i,
+ /\brun (an? )?(eval|evaluation) (for|on) (an? )?agent\b/i,
+ /\b(agent|agents).*\b(eval|evaluation|evaluations) jobs?\b/i,
+ ],
+ },
+ {
+ id: 'agent-optimizations',
+ title: 'Open Agent Suggestions',
+ description: 'Studio has a UI for generating optimization suggestions for deployed agents.',
+ getHref: getAgentOptimizationsRoute,
+ requiredFeatureFlags: ['agentsEnabled'],
+ patterns: [
+ /\boptimi[sz]e (an? )?agent\b/i,
+ /\b(agent|agents).*\b(cheaper|faster|smaller|right[-\s]?size)\b/i,
+ /\b(agent|agents).*\bmodel sizing\b/i,
+ /\bmodel sizing (for|on|of) (an? )?agent\b/i,
+ /\bsuggestions? for (an? )?agent\b/i,
+ ],
+ },
+ {
+ id: 'agent-monitor',
+ title: 'Open Agent Monitor',
+ description: 'Studio has a monitor UI for agent telemetry, logs, and token usage.',
+ getHref: getAgentMonitorRoute,
+ requiredFeatureFlags: ['agentsEnabled'],
+ patterns: [
+ /\bmonitor (an? )?agent\b/i,
+ /\bagent (monitor|telemetry|logs|traces|usage)\b/i,
+ /\b(agent|agents).*\btoken usage\b/i,
+ /\btoken usage (for|on|of) (an? )?agent\b/i,
+ ],
+ },
+ {
+ id: 'guardrails',
+ title: 'Open Guardrails',
+ description: 'Studio has a UI for managing NeMo Guardrails configurations.',
+ getHref: getGuardrailsRoute,
+ requiredFeatureFlags: ['guardrailsEnabled'],
+ patterns: [
+ /\bguardrails?\b/i,
+ /\bcontent safety\b/i,
+ /\bjailbreak\b/i,
+ /\bpii (redaction|guard|protection)\b/i,
+ /\bguardrail middleware\b/i,
+ ],
+ },
+ {
+ id: 'data-designer-new',
+ title: 'Open Data Designer',
+ description: 'Studio has a Data Designer UI for creating and transforming datasets.',
+ getHref: getNewDataDesignerJobRoute,
+ requiredFeatureFlags: ['dataDesignerEnabled'],
+ patterns: [
+ /\bdata designer\b/i,
+ /\bgenerate (synthetic )?(data|dataset|datasets)\b/i,
+ /\bcreate (a )?(dataset|datasets)\b/i,
+ /\btransform (a )?(dataset|datasets)\b/i,
+ /\bdata generation (workflow|pipeline)\b/i,
+ ],
+ },
+ {
+ id: 'fileset-new',
+ title: 'Open Fileset Upload',
+ description: 'Studio has a UI for creating filesets and uploading files.',
+ getHref: getNewFilesetRoute,
+ requiredFeatureFlags: ['datasetsEnabled'],
+ patterns: [
+ /\bcreate (a )?fileset\b/i,
+ /\bnew fileset\b/i,
+ /\bupload (a )?(file|files|dataset|datasets)\b/i,
+ /\bimport (a )?(file|files|dataset|datasets)\b/i,
+ ],
+ },
+ {
+ id: 'inference-providers',
+ title: 'Open Inference Providers',
+ description: 'Studio has a UI for adding and managing inference providers.',
+ getHref: getWorkspaceInferenceProvidersRoute,
+ requiredFeatureFlags: ['inferenceProviderEnabled'],
+ patterns: [
+ /\binference provider\b/i,
+ /\bmodel provider\b/i,
+ /\bconfigure inference (for|in) (this )?workspace\b/i,
+ /\bconfigure (a )?ne?mo inference\b/i,
+ /\b(add|create|configure|connect|manage|register) (an? )?(provider|inference endpoint)\b/i,
+ /\b(connect|configure) (openai|nvidia|nim|build)\b/i,
+ ],
+ },
+ {
+ id: 'secrets',
+ title: 'Open Secrets',
+ description: 'Studio has a UI for creating and managing workspace secrets.',
+ getHref: getSecretsRoute,
+ requiredFeatureFlags: ['secretsEnabled'],
+ patterns: [
+ /\b(add|create|manage|store|update) (an? )?(workspace )?(secret|secrets)\b/i,
+ /\b(add|create|manage|store|update) (an? )?(api key|credential|credentials|token) (secret|secrets|in (the )?workspace|for (this )?workspace)\b/i,
+ /\bworkspace (api key|credential|credentials|token|secret|secrets)\b/i,
+ ],
+ },
+ {
+ id: 'prompt-tuning',
+ title: 'Open Prompt Tuning',
+ description: 'Studio has a UI for creating prompt-tuned models.',
+ getHref: getPromptTuningFormRoute,
+ requiredFeatureFlags: ['customizerEnabled'],
+ patterns: [/\bprompt[-\s]?tun(e|ing)\b/i],
+ },
+ {
+ id: 'fine-tuning',
+ title: 'Open Fine-Tuning',
+ description: 'Studio has a UI for creating fine-tuned custom models.',
+ getHref: getWorkspaceNewCustomizationJobRoute,
+ requiredFeatureFlags: ['customizerEnabled'],
+ patterns: [
+ /\bfine[-\s]?tun(e|ing)\b/i,
+ /\bfinetun(e|ing)\b/i,
+ /\btrain (a )?(model|custom model)\b/i,
+ /\bcustomi[sz]e (a )?model\b/i,
+ ],
+ },
+ {
+ id: 'model-playground',
+ title: 'Open Playground',
+ description: 'Studio has a playground UI for chatting with and comparing models.',
+ getHref: getModelCompareRoute,
+ requiredFeatureFlags: ['modelCompareEnabled'],
+ patterns: [
+ /\bmodel playground\b/i,
+ /\bopen (the )?playground (for|with) (a )?model\b/i,
+ /\bcompare (models|model responses)\b/i,
+ /\bchat with (a )?model\b/i,
+ ],
+ },
+ {
+ id: 'model-deployments',
+ title: 'Open Deployments',
+ description: 'Studio has a UI for managing model deployments.',
+ getHref: getWorkspaceDeploymentsRoute,
+ requiredFeatureFlags: ['deploymentsEnabled'],
+ patterns: [
+ /\bmodel deployments?\b/i,
+ /\bdeploy (a )?model\b/i,
+ /\bserve (a )?model\b/i,
+ /\bmodel endpoint\b/i,
+ ],
+ },
+ {
+ id: 'base-models',
+ title: 'Open Base Models',
+ description: 'Studio has a UI for browsing base models and model details.',
+ getHref: getWorkspaceBaseModelsRoute,
+ requiredFeatureFlags: ['baseModelsEnabled'],
+ patterns: [
+ /\bbase models?\b/i,
+ /\bmodel catalog\b/i,
+ /\bbrowse models?\b/i,
+ /\blist models?\b/i,
+ ],
+ },
+ {
+ id: 'custom-models',
+ title: 'Open Custom Models',
+ description: 'Studio has a UI for managing custom models and customization jobs.',
+ getHref: getWorkspaceCustomizationJobListRoute,
+ requiredFeatureFlags: ['customizerEnabled'],
+ patterns: [/\bcustom models?\b/i, /\bcustomization jobs?\b/i],
+ },
+ {
+ id: 'agents',
+ title: 'Open Agents',
+ description: 'Studio has a UI for viewing agents and managing their deployments.',
+ getHref: getAgentsListRoute,
+ requiredFeatureFlags: ['agentsEnabled'],
+ patterns: [
+ /\bmanage agents?\b/i,
+ /\bview agents?\b/i,
+ /\b(build|create) (an? )?agent\b(?!\s+(class|component|helper|test|function|module))\b/i,
+ /\bcreate example agent\b/i,
+ /\bclone (an? )?agent\b/i,
+ /\bchat with (an? )?agent\b/i,
+ /\bdeploy (an? )?agent\b/i,
+ /\btry (a )?deployed agent\b/i,
+ ],
+ },
+ {
+ id: 'evaluations',
+ title: 'Open Evaluations',
+ description: 'Studio has a UI for reviewing model evaluation results.',
+ getHref: getEvaluationResultsRoute,
+ requiredFeatureFlags: ['evaluatorEnabled'],
+ patterns: [
+ /\bevaluat(e|ing|ion)s? (a )?model\b/i,
+ /\bmodel (eval|evaluation|evaluations)\b/i,
+ /\bevaluation results?\b/i,
+ /\b(eval|evaluation) history\b/i,
+ /\bnemo[-\s]?evaluator\b/i,
+ /\bevaluator (jobs?|sdk specs?)\b/i,
+ /\buse (the )?evaluator plugin\b/i,
+ ],
+ },
+ {
+ id: 'safe-synthesizer',
+ title: 'Open Safe Synthesizer',
+ description: 'Studio has a UI for monitoring safe synthetic data jobs.',
+ getHref: getWorkspaceSafeSynthesizerRoute,
+ requiredFeatureFlags: ['safeSynthesizerEnabled'],
+ patterns: [/\bsafe synth(esizer)? jobs?\b/i],
+ },
+ {
+ id: 'data-designer',
+ title: 'Open Data Designer',
+ description: 'Studio has a UI for managing Data Designer jobs.',
+ getHref: getDataDesignerJobListRoute,
+ requiredFeatureFlags: ['dataDesignerEnabled'],
+ patterns: [/\bdata designer jobs?\b/i],
+ },
+ {
+ id: 'jobs',
+ title: 'Open Jobs',
+ description: 'Studio has a UI for viewing workspace jobs.',
+ getHref: getWorkspaceJobsRoute,
+ requiredFeatureFlags: ['jobsEnabled'],
+ patterns: [/\bworkspace jobs?\b/i, /\bworkspace job history\b/i],
+ },
+ {
+ id: 'annotation',
+ title: 'Open Annotation',
+ description: 'Studio has a UI for inspecting intake traces and annotations.',
+ getHref: getIntakeRoute,
+ requiredFeatureFlags: ['intakeEnabled'],
+ patterns: [
+ /\bintake (annotation|annotations|traces?|trace review)\b/i,
+ /\b(annotation|annotations) (for|in|on) (intake|trace|traces)\b/i,
+ /\b(trace|traces).*\b(annotation|annotations|review)\b/i,
+ ],
+ },
+ {
+ id: 'members',
+ title: 'Open Members',
+ description: 'Studio has a UI for managing workspace members.',
+ getHref: getWorkspaceMembersRoute,
+ requiredFeatureFlags: ['membersEnabled'],
+ patterns: [/\bworkspace members?\b/i, /\badd (a )?member\b/i, /\buser access\b/i],
+ },
+ {
+ id: 'settings',
+ title: 'Open Settings',
+ description: 'Studio has a UI for workspace settings.',
+ getHref: getWorkspaceSettingsRoute,
+ requiredFeatureFlags: ['settingsEnabled'],
+ patterns: [
+ /\bworkspace settings?\b/i,
+ /\b(open|change|manage|update) (the )?settings (for|in|of) (this )?workspace\b/i,
+ ],
+ },
+];
+
+const isDestinationEnabled = (destination: StudioUiDestination): boolean =>
+ destination.requiredFeatureFlags?.every((flag) => featureFlags[flag] !== false) ?? true;
+
+export const getStudioUiNavigationSuggestion = (
+ prompt: string,
+ workspace: string
+): StudioUiNavigationSuggestion | undefined => {
+ const trimmedPrompt = prompt.trim();
+ if (!trimmedPrompt) return undefined;
+
+ for (const destination of STUDIO_UI_DESTINATIONS) {
+ if (!isDestinationEnabled(destination)) continue;
+ if (!destination.patterns.some((pattern) => pattern.test(trimmedPrompt))) continue;
+
+ return {
+ id: destination.id,
+ title: destination.title,
+ description: destination.description,
+ href: destination.getHref(workspace),
+ };
+ }
+
+ return undefined;
+};
diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useClaudeCodeChatRuntime.test.ts b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useClaudeCodeChatRuntime.test.ts
index 5e2e0ccae8..6ce536f5df 100644
--- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useClaudeCodeChatRuntime.test.ts
+++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useClaudeCodeChatRuntime.test.ts
@@ -2,12 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
import { useClaudeCodeChatRuntime } from '@studio/routes/agents/ClaudeCodeChatRoute/useClaudeCodeChatRuntime';
+import { mockFeatureFlags } from '@studio/tests/util/mockFeatureFlags';
import { act, renderHook, waitFor } from '@testing-library/react';
const mocks = vi.hoisted(() => ({
appendUserMessage: vi.fn(),
createClaudeCodeSession: vi.fn(),
invalidateQueries: vi.fn(),
+ prepareForUserInput: vi.fn(),
resolveClaudeCodeInput: vi.fn(),
resolveClaudeCodePermission: vi.fn(),
streamClaudeCodeMessage: vi.fn(),
@@ -24,8 +26,10 @@ vi.mock('@studio/routes/agents/ClaudeCodeChatRoute/api', () => ({
vi.mock('@studio/routes/agents/ClaudeCodeChatRoute/useCustomAssistantChatRuntime', () => ({
useCustomAssistantChatRuntime: ({
+ onBeforeRun,
onRun,
}: {
+ onBeforeRun?: (context: unknown) => Promise<'continue' | 'cancel' | void>;
onRun: (context: unknown) => Promise;
}) => ({
appendUserMessage: mocks.appendUserMessage,
@@ -35,14 +39,18 @@ vi.mock('@studio/routes/agents/ClaudeCodeChatRoute/useCustomAssistantChatRuntime
replaceMessages: vi.fn(),
runtime: {},
submitPrompt: async (prompt: string) => {
- await onRun({
+ const context = {
prompt,
signal: new AbortController().signal,
appendAssistantParts: vi.fn(),
appendAssistantText: vi.fn(),
- prepareForUserInput: vi.fn(),
+ prepareForUserInput: mocks.prepareForUserInput,
isCurrentRun: () => true,
- });
+ };
+ const beforeRunResult = await onBeforeRun?.(context);
+ if (beforeRunResult !== 'cancel') {
+ await onRun(context);
+ }
await mocks.submitPrompt(prompt);
},
}),
@@ -70,6 +78,10 @@ describe('useClaudeCodeChatRuntime', () => {
vi.clearAllMocks();
});
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
it('exposes the latest streamed coding-agent model without promoting it to selected model', async () => {
mocks.createClaudeCodeSession.mockResolvedValue('session-1');
mocks.streamClaudeCodeMessage.mockImplementation(
@@ -153,6 +165,66 @@ describe('useClaudeCodeChatRuntime', () => {
await waitFor(() => expect(result.current.artifacts.coding_agent_model).toBeUndefined());
});
+ it('pauses for a matching Studio UI suggestion before creating a Claude Code session', async () => {
+ let submitPromise!: Promise;
+ mockFeatureFlags({ guardrailsEnabled: true });
+ mocks.createClaudeCodeSession.mockResolvedValue('session-1');
+ mocks.streamClaudeCodeMessage.mockResolvedValue(undefined);
+
+ const { result } = renderUseClaudeCodeChatRuntime({ workspace: 'default' });
+
+ act(() => {
+ submitPromise = result.current.submitPrompt('Add guardrails to an agent');
+ });
+
+ await waitFor(() =>
+ expect(result.current.studioNavigationRequest).toEqual(
+ expect.objectContaining({
+ prompt: 'Add guardrails to an agent',
+ suggestion: expect.objectContaining({
+ id: 'guardrails',
+ href: '/workspaces/default/guardrails',
+ }),
+ })
+ )
+ );
+
+ expect(mocks.prepareForUserInput).toHaveBeenCalled();
+ expect(mocks.createClaudeCodeSession).not.toHaveBeenCalled();
+
+ await act(async () => {
+ result.current.resolveStudioNavigationRequest('continue');
+ await submitPromise;
+ });
+
+ expect(mocks.createClaudeCodeSession).toHaveBeenCalledTimes(1);
+ expect(result.current.studioNavigationRequest).toBeNull();
+ });
+
+ it('does not start Claude Code when the user chooses the Studio UI', async () => {
+ let submitPromise!: Promise;
+ mockFeatureFlags({ guardrailsEnabled: true });
+
+ const { result } = renderUseClaudeCodeChatRuntime({ workspace: 'default' });
+
+ act(() => {
+ submitPromise = result.current.submitPrompt('Add guardrails to an agent');
+ });
+
+ await waitFor(() =>
+ expect(result.current.studioNavigationRequest?.suggestion.id).toBe('guardrails')
+ );
+
+ await act(async () => {
+ result.current.resolveStudioNavigationRequest('navigate');
+ await submitPromise;
+ });
+
+ expect(mocks.createClaudeCodeSession).not.toHaveBeenCalled();
+ expect(mocks.streamClaudeCodeMessage).not.toHaveBeenCalled();
+ expect(result.current.studioNavigationRequest).toBeNull();
+ });
+
it('does not append denial text when permission resolution fails', async () => {
const onError = vi.fn();
let finishStream!: () => void;
@@ -332,7 +404,7 @@ describe('useClaudeCodeChatRuntime', () => {
const { result } = renderUseClaudeCodeChatRuntime();
act(() => {
- submitPromise = result.current.submitPrompt('Evaluate an agent');
+ submitPromise = result.current.submitPrompt('Pick an agent for this workflow');
});
await waitFor(() =>
expect(result.current.inputRequest).toEqual(
@@ -394,7 +466,7 @@ describe('useClaudeCodeChatRuntime', () => {
const { result } = renderUseClaudeCodeChatRuntime();
act(() => {
- submitPromise = result.current.submitPrompt('Evaluate an agent');
+ submitPromise = result.current.submitPrompt('Pick an agent for this workflow');
});
await waitFor(() =>
expect(result.current.inputRequest).toEqual(
diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useClaudeCodeChatRuntime.ts b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useClaudeCodeChatRuntime.ts
index 29f8d19501..3994023ef5 100644
--- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useClaudeCodeChatRuntime.ts
+++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useClaudeCodeChatRuntime.ts
@@ -25,6 +25,10 @@ import {
updateClaudeCodeChatArtifactsFromSelections,
} from '@studio/routes/agents/ClaudeCodeChatRoute/artifacts';
import { getAssistantPartsFromClaudeEvent } from '@studio/routes/agents/ClaudeCodeChatRoute/stream';
+import {
+ getStudioUiNavigationSuggestion,
+ type StudioUiNavigationSuggestion,
+} from '@studio/routes/agents/ClaudeCodeChatRoute/studioUiNavigationSuggestions';
import type {
ClaudeCodeChatArtifacts,
ClaudeCodeInputDecision,
@@ -78,6 +82,14 @@ interface AskUserQuestionDecisionState {
type ActiveDecisionState = PermissionDecisionState | AskUserQuestionDecisionState;
+export type StudioNavigationDecision = 'continue' | 'navigate' | 'cancel';
+
+export interface StudioNavigationRequest {
+ id: string;
+ prompt: string;
+ suggestion: StudioUiNavigationSuggestion;
+}
+
const isRecord = (value: unknown): value is Record =>
typeof value === 'object' && value !== null;
@@ -237,9 +249,16 @@ export const useClaudeCodeChatRuntime = (options?: UseClaudeCodeChatRuntimeOptio
const [decisionStatus, setDecisionStatus] = useState('pending');
const [inputRequest, setInputRequest] = useState(null);
const [inputStatus, setInputStatus] = useState('pending');
+ const [studioNavigationRequest, setStudioNavigationRequest] =
+ useState(null);
+ const [studioNavigationStatus, setStudioNavigationStatus] =
+ useState('pending');
const sessionIdRef = useRef(options?.initialSessionId ?? null);
const permissionRequestRef = useRef(null);
const inputRequestRef = useRef(null);
+ const studioNavigationResolverRef = useRef<((decision: StudioNavigationDecision) => void) | null>(
+ null
+ );
const activeDecisionRef = useRef(null);
const initialArtifactsRef = useRef(
options?.initialArtifacts
@@ -283,6 +302,20 @@ export const useClaudeCodeChatRuntime = (options?: UseClaudeCodeChatRuntimeOptio
setInputStatus('pending');
}, []);
+ const clearStudioNavigationRequest = useCallback(() => {
+ studioNavigationResolverRef.current = null;
+ setStudioNavigationRequest(null);
+ setStudioNavigationStatus('pending');
+ }, []);
+
+ const resolveStudioNavigationRequest = useCallback((decision: StudioNavigationDecision) => {
+ const resolve = studioNavigationResolverRef.current;
+ if (!resolve) return;
+
+ setStudioNavigationStatus('submitting');
+ resolve(decision);
+ }, []);
+
const setAskUserQuestionDecision = useCallback(
(request: ClaudeCodePermissionRequest, state: AskUserQuestionDecisionState) => {
const question = state.questions[state.questionIndex];
@@ -336,12 +369,63 @@ export const useClaudeCodeChatRuntime = (options?: UseClaudeCodeChatRuntimeOptio
const handleInputRequest = useCallback(
(request: ClaudeCodeInputRequest) => {
clearPermissionRequest();
+ clearStudioNavigationRequest();
inputRequestRef.current = request;
setInputStatus('pending');
setInputRequest(request);
},
- [clearPermissionRequest]
+ [clearPermissionRequest, clearStudioNavigationRequest]
+ );
+
+ const requestStudioNavigationDecision = useCallback(
+ async ({
+ prompt,
+ signal,
+ prepareForUserInput,
+ isCurrentRun,
+ }: {
+ prompt: string;
+ signal: AbortSignal;
+ prepareForUserInput: () => void;
+ isCurrentRun: () => boolean;
+ }): Promise<'continue' | 'cancel'> => {
+ if (!workspace) return 'continue';
+
+ const suggestion = getStudioUiNavigationSuggestion(prompt, workspace);
+ if (!suggestion) return 'continue';
+
+ clearPermissionRequest();
+ clearInputRequest();
+ prepareForUserInput();
+
+ let resolveDecision: (decision: StudioNavigationDecision) => void = () => undefined;
+ const decisionPromise = new Promise((resolve) => {
+ resolveDecision = resolve;
+ });
+ studioNavigationResolverRef.current = resolveDecision;
+ setStudioNavigationStatus('pending');
+ setStudioNavigationRequest({
+ id: `${suggestion.id}:${Date.now()}`,
+ prompt,
+ suggestion,
+ });
+
+ const handleAbort = () => resolveDecision('cancel');
+ signal.addEventListener('abort', handleAbort, { once: true });
+
+ try {
+ const decision = await decisionPromise;
+ if (!isCurrentRun()) return 'cancel';
+ return decision === 'continue' ? 'continue' : 'cancel';
+ } finally {
+ signal.removeEventListener('abort', handleAbort);
+ if (studioNavigationResolverRef.current === resolveDecision) {
+ clearStudioNavigationRequest();
+ }
+ }
+ },
+ [clearInputRequest, clearPermissionRequest, clearStudioNavigationRequest, workspace]
);
const {
@@ -353,10 +437,12 @@ export const useClaudeCodeChatRuntime = (options?: UseClaudeCodeChatRuntimeOptio
submitPrompt,
} = useCustomAssistantChatRuntime({
initialMessages: options?.initialMessages,
+ onBeforeRun: requestStudioNavigationDecision,
onError,
onRun: async ({ prompt, signal, appendAssistantParts, prepareForUserInput, isCurrentRun }) => {
clearPermissionRequest();
clearInputRequest();
+ clearStudioNavigationRequest();
const activeSessionId = await ensureSessionId();
let doneReceived = false;
@@ -559,8 +645,18 @@ export const useClaudeCodeChatRuntime = (options?: UseClaudeCodeChatRuntimeOptio
setArtifacts(createWorkspaceArtifacts(undefined, workspace));
clearPermissionRequest();
clearInputRequest();
+ resolveStudioNavigationRequest('cancel');
+ clearStudioNavigationRequest();
resetThread();
- }, [clearInputRequest, clearPermissionRequest, onSessionIdChange, resetThread, workspace]);
+ }, [
+ clearInputRequest,
+ clearPermissionRequest,
+ clearStudioNavigationRequest,
+ onSessionIdChange,
+ resetThread,
+ resolveStudioNavigationRequest,
+ workspace,
+ ]);
const loadSession = useCallback(
({
@@ -574,9 +670,19 @@ export const useClaudeCodeChatRuntime = (options?: UseClaudeCodeChatRuntimeOptio
setArtifacts(createWorkspaceArtifacts(nextArtifacts, workspace));
clearPermissionRequest();
clearInputRequest();
+ resolveStudioNavigationRequest('cancel');
+ clearStudioNavigationRequest();
replaceMessages(messages);
},
- [clearInputRequest, clearPermissionRequest, onSessionIdChange, replaceMessages, workspace]
+ [
+ clearInputRequest,
+ clearPermissionRequest,
+ clearStudioNavigationRequest,
+ onSessionIdChange,
+ replaceMessages,
+ resolveStudioNavigationRequest,
+ workspace,
+ ]
);
return {
@@ -591,10 +697,13 @@ export const useClaudeCodeChatRuntime = (options?: UseClaudeCodeChatRuntimeOptio
loadSession,
resolveInputRequest,
resolveDecisionRequest,
+ resolveStudioNavigationRequest,
runtime,
sessionId,
skipInputRequest,
skipDecisionRequest,
+ studioNavigationRequest,
+ studioNavigationStatus,
submitPrompt,
};
};
diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useCustomAssistantChatRuntime.test.ts b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useCustomAssistantChatRuntime.test.ts
index 5c412397c4..b73ff2e711 100644
--- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useCustomAssistantChatRuntime.test.ts
+++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useCustomAssistantChatRuntime.test.ts
@@ -8,6 +8,7 @@ import {
CLAUDE_CODE_SUBTLE_TOOL_GROUP_NAME,
} from '@studio/routes/agents/ClaudeCodeChatRoute/toolParts';
import {
+ type CustomAssistantBeforeRunContext,
type CustomAssistantRunContext,
useCustomAssistantChatRuntime,
} from '@studio/routes/agents/ClaudeCodeChatRoute/useCustomAssistantChatRuntime';
@@ -45,6 +46,65 @@ describe('useCustomAssistantChatRuntime', () => {
mocks.useExternalStoreRuntime.mockClear();
});
+ it('pauses before running and resumes when the before-run hook continues', async () => {
+ let continueRun!: () => void;
+ const onBeforeRun = vi.fn(async (context: CustomAssistantBeforeRunContext) => {
+ context.prepareForUserInput();
+ await new Promise((resolve) => {
+ continueRun = resolve;
+ });
+ return 'continue' as const;
+ });
+ const onRun = vi.fn(async (context: CustomAssistantRunContext) => {
+ context.appendAssistantText('Continuing in chat.');
+ });
+ const { result } = renderHook(() => useCustomAssistantChatRuntime({ onBeforeRun, onRun }));
+
+ act(() => {
+ void result.current.submitPrompt('Add guardrails');
+ });
+
+ await waitFor(() => {
+ expect(onBeforeRun).toHaveBeenCalledWith(
+ expect.objectContaining({ prompt: 'Add guardrails' })
+ );
+ expect(onRun).not.toHaveBeenCalled();
+ expect(getMockRuntime(result.current.runtime).messages.map(getMessageText)).toEqual([
+ 'Add guardrails',
+ ]);
+ });
+
+ await act(async () => {
+ continueRun();
+ });
+
+ await waitFor(() => {
+ expect(onRun).toHaveBeenCalled();
+ expect(getMockRuntime(result.current.runtime).messages.map(getMessageText)).toEqual([
+ 'Add guardrails',
+ 'Continuing in chat.',
+ ]);
+ });
+ });
+
+ it('does not run when the before-run hook cancels', async () => {
+ const onBeforeRun = vi.fn((context: CustomAssistantBeforeRunContext) => {
+ context.prepareForUserInput();
+ return 'cancel' as const;
+ });
+ const onRun = vi.fn();
+ const { result } = renderHook(() => useCustomAssistantChatRuntime({ onBeforeRun, onRun }));
+
+ await act(async () => {
+ await result.current.submitPrompt('Open guardrails');
+ });
+
+ expect(onRun).not.toHaveBeenCalled();
+ expect(getMockRuntime(result.current.runtime).messages.map(getMessageText)).toEqual([
+ 'Open guardrails',
+ ]);
+ });
+
it('shows user interventions between agent messages', async () => {
let runContext: CustomAssistantRunContext | undefined;
const onRun = vi.fn(async (context: CustomAssistantRunContext) => {
diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useCustomAssistantChatRuntime.ts b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useCustomAssistantChatRuntime.ts
index 01212c7952..8e2e3b3ff1 100644
--- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useCustomAssistantChatRuntime.ts
+++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/useCustomAssistantChatRuntime.ts
@@ -40,8 +40,20 @@ export interface CustomAssistantRunResult {
text?: string;
}
+export type CustomAssistantBeforeRunResult = 'continue' | 'cancel';
+
+export interface CustomAssistantBeforeRunContext {
+ prompt: string;
+ signal: AbortSignal;
+ prepareForUserInput: () => void;
+ isCurrentRun: () => boolean;
+}
+
interface UseCustomAssistantChatRuntimeOptions {
initialMessages?: readonly ThreadMessageLike[];
+ onBeforeRun?: (
+ context: CustomAssistantBeforeRunContext
+ ) => Promise | CustomAssistantBeforeRunResult | void;
onRun: (context: CustomAssistantRunContext) => Promise;
onError?: (error: Error) => void;
}
@@ -107,6 +119,7 @@ const completeClaudeCodeAssistantContent = (
export const useCustomAssistantChatRuntime = ({
initialMessages = [],
+ onBeforeRun,
onRun,
onError,
}: UseCustomAssistantChatRuntimeOptions) => {
@@ -242,6 +255,18 @@ export const useCustomAssistantChatRuntime = ({
};
try {
+ const beforeRunResult = await onBeforeRun?.({
+ prompt,
+ signal: runController.signal,
+ prepareForUserInput,
+ isCurrentRun,
+ });
+
+ if (beforeRunResult === 'cancel' || runController.signal.aborted || !isCurrentRun()) {
+ completeActiveAssistantMessage(CANCELLED_STATUS);
+ return;
+ }
+
const result = await onRun({
prompt,
signal: runController.signal,
@@ -287,6 +312,7 @@ export const useCustomAssistantChatRuntime = ({
},
[
completeAssistantMessageContent,
+ onBeforeRun,
onError,
onRun,
setThreadMessages,