Add options to hide panes (#244)

* Add multiple pane views

* Fix rendering issues

* Fix copilot apply selection pane error and remove old debug logs

* Make top bar more compact

* Fix copilot generation from starting prompt

* Fix panel resizing issues

* Fix content preservation upon hiding panes

* Added changes on top of pane_layouts branch to remove example agent from the workflow editor

* Grey out options for changing pane layout during zero agents state

* Add zero-agent state for playground and publish buttons

* Fix pane sizes and bugs

---------

Co-authored-by: tusharmagar <tushmag@gmail.com>
This commit is contained in:
Akhilesh Sudhakar 2025-09-12 00:37:44 +04:00 committed by GitHub
parent 431f835ba1
commit ee02d61996
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 900 additions and 400 deletions

View file

@ -12,21 +12,8 @@ function buildTemplates(): { [key: string]: z.infer<typeof WorkflowTemplate> } {
templates['default'] = {
name: 'Blank Template',
description: 'A blank canvas to build your agents.',
startAgent: "Example Agent",
agents: [
{
name: "Example Agent",
type: "conversation",
description: "An example agent",
instructions: "## 🧑‍ Role:\nYou are an helpful customer support assistant\n\n---\n## ⚙️ Steps to Follow:\n1. Ask the user what they would like help with\n2. Ask the user for their email address and let them know someone will contact them soon.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Asking the user their issue\n- Getting their email\n\n❌ Out of Scope:\n- Questions unrelated to customer support\n- If a question is out of scope, politely inform the user and avoid providing an answer.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- ask user their issue\n\n❌ Don'ts:\n- don't ask user any other detail than email",
model: DEFAULT_MODEL,
toggleAble: true,
ragReturnType: "chunks",
ragK: 3,
controlType: "retain",
outputVisibility: "user_facing",
},
],
startAgent: "",
agents: [],
prompts: [],
tools: [
{

View file

@ -48,6 +48,8 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
isInitialState = false,
dataSources,
}, ref) {
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
const [discardContext, setDiscardContext] = useState(false);
const [isLastInteracted, setIsLastInteracted] = useState(isInitialState);
@ -95,17 +97,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
onMessagesChange?.(messages);
}, [messages, onMessagesChange]);
// Check for initial prompt in local storage and send it
useEffect(() => {
const prompt = localStorage.getItem(`project_prompt_${projectId}`);
if (prompt && messages.length === 0) {
localStorage.removeItem(`project_prompt_${projectId}`);
setMessages([{
role: 'user',
content: prompt
}]);
}
}, [projectId, messages.length]);
// Removed localStorage auto-start. Initial prompts are sent by parent via ref.
// Reset discardContext when chatContext changes
useEffect(() => {
@ -134,15 +126,19 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
const currentStart = startRef.current;
const currentCancel = cancelRef.current;
currentStart(messages, (finalResponse: string) => {
setMessages(prev => [
...prev,
{
role: 'assistant',
content: finalResponse
}
]);
});
if (currentStart) {
currentStart(messages, (finalResponse: string) => {
setMessages(prev => [
...prev,
{
role: 'assistant',
content: finalResponse
}
]);
});
} else {
// startRef not yet ready; no-op
}
return () => currentCancel();
}, [messages, responseError]);
@ -269,7 +265,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
toolQuery={toolQuery}
/>
</div>
<div className="shrink-0 px-0 pb-0">
<div className="shrink-0 px-0 pb-10">
{responseError && (
<div className="mb-4 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex gap-2 justify-between items-center text-sm">
<p className="text-red-600 dark:text-red-400">{responseError}</p>
@ -322,8 +318,8 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
dispatch: (action: WorkflowDispatch) => void;
isInitialState?: boolean;
dataSources?: z.infer<typeof DataSource>[];
activePanel: 'playground' | 'copilot';
onTogglePanel: () => void;
activePanel?: 'playground' | 'copilot';
onTogglePanel?: () => void;
}>(({
projectId,
workflow,
@ -334,6 +330,13 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
activePanel,
onTogglePanel,
}, ref) => {
console.log('🎪 Copilot wrapper component mounted:', {
projectId,
isInitialState,
activePanel,
chatContextType: chatContext?.type
});
const [copilotKey, setCopilotKey] = useState(0);
const [showCopySuccess, setShowCopySuccess] = useState(false);
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
@ -369,34 +372,7 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
<Panel
variant="copilot"
tourTarget="copilot"
title={
<div className="flex items-center">
<div className="flex items-center gap-2 rounded-lg p-1 bg-blue-50/70 dark:bg-blue-900/30">
<button
onClick={onTogglePanel}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
activePanel === 'copilot'
? 'bg-white dark:bg-zinc-700 text-indigo-700 dark:text-indigo-300 shadow-md border border-indigo-200 dark:border-indigo-700'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60'
}`}
>
<span className="text-base"></span>
Build
</button>
<button
onClick={onTogglePanel}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
activePanel === 'playground'
? 'bg-white dark:bg-zinc-700 text-indigo-700 dark:text-indigo-300 shadow-md border border-indigo-200 dark:border-indigo-700'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60'
}`}
>
<span className="text-base">💬</span>
Chat
</button>
</div>
</div>
}
title={<div className="flex items-center gap-2 text-zinc-800 dark:text-zinc-200 font-semibold"><Sparkles className="w-4 h-4" /> Skipper</div>}
subtitle="Build your assistant"
rightActions={
<div className="flex items-center gap-2">

View file

@ -67,14 +67,14 @@ export function Action({
switch (action.config_type) {
case 'agent':
dispatch({
type: 'update_agent',
type: 'update_agent_no_select',
name: action.name,
agent: changes
});
break;
case 'tool':
dispatch({
type: 'update_tool',
type: 'update_tool_no_select',
name: action.name,
tool: changes
});

View file

@ -222,7 +222,8 @@ function AssistantMessage({
agent: {
name: action.name,
...action.config_changes
}
},
fromCopilot: true
});
break;
}
@ -236,7 +237,8 @@ function AssistantMessage({
tool: {
name: action.name,
...action.config_changes
}
},
fromCopilot: true
});
break;
}
@ -246,7 +248,8 @@ function AssistantMessage({
prompt: {
name: action.name,
...action.config_changes
}
},
fromCopilot: true
});
break;
case 'pipeline':
@ -255,7 +258,8 @@ function AssistantMessage({
pipeline: {
name: action.name,
...action.config_changes
}
},
fromCopilot: true
});
break;
}
@ -263,14 +267,14 @@ function AssistantMessage({
switch (action.config_type) {
case 'agent':
dispatch({
type: 'update_agent',
type: 'update_agent_no_select',
name: action.name,
agent: action.config_changes
});
break;
case 'tool':
dispatch({
type: 'update_tool',
type: 'update_tool_no_select',
name: action.name,
tool: action.config_changes
});

View file

@ -38,6 +38,7 @@ export function useCopilot({ projectId, workflow, context, dataSources }: UseCop
const [billingError, setBillingError] = useState<string | null>(null);
const cancelRef = useRef<() => void>(() => { });
const responseRef = useRef('');
const inFlightRef = useRef(false);
function clearError() {
setError(null);
@ -51,7 +52,19 @@ export function useCopilot({ projectId, workflow, context, dataSources }: UseCop
messages: z.infer<typeof CopilotMessage>[],
onDone: (finalResponse: string) => void,
) => {
if (!messages.length || messages.at(-1)?.role !== 'user') return;
if (!messages.length || messages.at(-1)?.role !== 'user') {
return;
}
// Prevent duplicate/concurrent starts (e.g., StrictMode double effects or remounts)
if (inFlightRef.current) {
return;
}
inFlightRef.current = true;
setStreamingResponse('');
responseRef.current = '';
@ -61,16 +74,23 @@ export function useCopilot({ projectId, workflow, context, dataSources }: UseCop
setLoading(true);
try {
// Wait 2 rAF frames to let layout stabilize (avoids StrictMode/remount race on initial load)
await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
const res = await getCopilotResponseStream(projectId, messages, workflow, context || null, dataSources);
// Check for billing error
if ('billingError' in res) {
setLoading(false);
setError(res.billingError);
setBillingError(res.billingError);
inFlightRef.current = false;
return;
}
const eventSource = new EventSource(`/api/copilot-stream-response/${res.streamId}`);
eventSource.onmessage = (event) => {
@ -102,24 +122,29 @@ export function useCopilot({ projectId, workflow, context, dataSources }: UseCop
eventSource.close();
setLoading(false);
onDone(responseRef.current);
inFlightRef.current = false;
});
eventSource.onerror = () => {
eventSource.close();
setError('Streaming failed');
setLoading(false);
inFlightRef.current = false;
};
cancelRef.current = () => eventSource.close();
} catch (err) {
console.error('❌ Error in useCopilot.start:', err);
setError('Failed to initiate stream');
setLoading(false);
inFlightRef.current = false;
}
}, [projectId, workflow, context, dataSources]);
const cancel = useCallback(() => {
cancelRef.current?.();
setLoading(false);
inFlightRef.current = false;
}, []);
return {

View file

@ -28,8 +28,8 @@ export function App({
onPanelClick?: () => void;
triggerCopilotChat?: (message: string) => void;
isLiveWorkflow: boolean;
activePanel: 'playground' | 'copilot';
onTogglePanel: () => void;
activePanel?: 'playground' | 'copilot';
onTogglePanel?: () => void;
onMessageSent?: () => void;
}) {
const [counter, setCounter] = useState<number>(0);
@ -56,6 +56,8 @@ export function App({
}
}, []);
const hasAgents = (workflow?.agents?.length || 0) > 0;
return (
<>
<Panel
@ -63,35 +65,13 @@ export function App({
variant="playground"
tourTarget="playground"
title={
<div className="flex items-center">
<div className="flex items-center gap-2 rounded-lg p-1 bg-blue-50/70 dark:bg-blue-900/30">
<button
onClick={onTogglePanel}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
activePanel === 'copilot'
? 'bg-white dark:bg-zinc-700 text-indigo-700 dark:text-indigo-300 shadow-md border border-indigo-200 dark:border-indigo-700'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60'
}`}
>
<span className="text-base"></span>
Build
</button>
<button
onClick={onTogglePanel}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
activePanel === 'playground'
? 'bg-white dark:bg-zinc-700 text-indigo-700 dark:text-indigo-300 shadow-md border border-indigo-200 dark:border-indigo-700'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60'
}`}
>
<span className="text-base">💬</span>
Chat
</button>
</div>
<div className="flex items-center gap-2 text-zinc-800 dark:text-zinc-200 font-semibold">
<MessageCircle className="w-4 h-4" />
Chat
</div>
}
subtitle="Chat with your assistant"
rightActions={
subtitle={hasAgents ? "Chat with your assistant" : "Create an agent to start chatting"}
rightActions={hasAgents ? (
<div className="flex items-center gap-2">
<Button
variant="primary"
@ -131,21 +111,46 @@ export function App({
)}
</Button>
</div>
}
) : (
// Preserve header height when there are zero agents
<div className="h-8" />
)}
onClick={onPanelClick}
>
<div className="h-full overflow-auto px-4 py-4">
<Chat
key={`chat-${counter}`}
projectId={projectId}
workflow={workflow}
messageSubscriber={messageSubscriber}
onCopyClick={(fn) => { getCopyContentRef.current = fn; }}
showDebugMessages={showDebugMessages}
triggerCopilotChat={triggerCopilotChat}
isLiveWorkflow={isLiveWorkflow}
onMessageSent={onMessageSent}
/>
{hasAgents ? (
<Chat
key={`chat-${counter}`}
projectId={projectId}
workflow={workflow}
messageSubscriber={messageSubscriber}
onCopyClick={(fn) => { getCopyContentRef.current = fn; }}
showDebugMessages={showDebugMessages}
triggerCopilotChat={triggerCopilotChat}
isLiveWorkflow={isLiveWorkflow}
onMessageSent={onMessageSent}
/>
) : (
<div className="h-full flex items-center justify-center">
<div className="text-center max-w-md">
<div className="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300">
<MessageCircle className="w-6 h-6" />
</div>
<div className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Create an agent to start chatting</div>
<div className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">Skipper can build agents for you!</div>
<div className="mt-4 flex items-center justify-center gap-3">
<Button
variant="primary"
size="sm"
className="!bg-blue-700 hover:!bg-blue-800 !text-white dark:!bg-blue-600 dark:hover:!bg-blue-700 !border !border-blue-700 dark:!border-blue-600"
onClick={() => triggerCopilotChat?.("Help me create my first agent.")}
>
Ask Skipper
</Button>
</div>
</div>
</div>
)}
</div>
</Panel>
</>

View file

@ -1,6 +1,6 @@
"use client";
import React from "react";
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input } from "@heroui/react";
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input, ButtonGroup } from "@heroui/react";
import { Button as CustomButton } from "@/components/ui/button";
import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon, Clock, Plug, MessageCircleIcon, ShareIcon } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
@ -18,6 +18,7 @@ interface TopBarProps {
canUndo: boolean;
canRedo: boolean;
activePanel: 'playground' | 'copilot';
viewMode: "two_agents_chat" | "two_agents_skipper" | "two_chat_skipper" | "three_all";
hasAgentInstructionChanges: boolean;
hasPlaygroundTested: boolean;
hasPublished: boolean;
@ -29,6 +30,8 @@ interface TopBarProps {
onChangeMode: (mode: 'draft' | 'live') => void;
onRevertToLive: () => void;
onTogglePanel: () => void;
onSetViewMode: (mode: "two_agents_chat" | "two_agents_skipper" | "two_chat_skipper" | "three_all") => void;
hasAgents?: boolean;
onUseAssistantClick: () => void;
onStartNewChatAndFocus: () => void;
onStartBuildTour?: () => void;
@ -52,6 +55,7 @@ export function TopBar({
canUndo,
canRedo,
activePanel,
viewMode,
hasAgentInstructionChanges,
hasPlaygroundTested,
hasPublished,
@ -63,6 +67,8 @@ export function TopBar({
onChangeMode,
onRevertToLive,
onTogglePanel,
onSetViewMode,
hasAgents = true,
onUseAssistantClick,
onStartNewChatAndFocus,
onStartBuildTour,
@ -121,19 +127,24 @@ export function TopBar({
/>
</div>
{/* Show divider and CTA only in live view */}
{/* Show divider and mode indicator */}
{isLive && <div className="h-4 w-px bg-gray-300 dark:bg-gray-600"></div>}
{isLive ? (
<Button
variant="solid"
size="md"
size="sm"
onPress={() => onChangeMode('draft')}
className="gap-2 px-4 bg-gray-100 hover:bg-gray-200 text-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-300 font-medium text-sm border border-gray-200 dark:border-gray-600 shadow-sm"
startContent={<PenLine size={16} />}
className="gap-2 px-3 h-8 bg-gray-100 hover:bg-gray-200 text-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-300 font-medium text-sm border border-gray-200 dark:border-gray-600 shadow-sm"
startContent={<PenLine size={14} />}
>
Switch to draft
</Button>
) : null}
) : (
<div className="flex items-center gap-1.5 px-2.5 py-1 bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 font-medium text-xs rounded-full">
<PenLine size={12} />
<span>Draft</span>
</div>
)}
</div>
{/* Progress Bar - Center */}
@ -163,31 +174,106 @@ export function TopBar({
</div>}
{!isLive && <>
{!isLive && <div className="flex items-center gap-0.5">
<CustomButton
variant="primary"
size="sm"
onClick={onUndo}
disabled={!canUndo}
className="bg-gray-50 text-gray-700 hover:bg-gray-100 disabled:bg-gray-25 disabled:text-gray-400"
className="min-w-8 h-8 px-2 bg-gray-50 text-gray-700 hover:bg-gray-100 disabled:bg-gray-25 disabled:text-gray-400"
showHoverContent={true}
hoverContent="Undo"
>
<UndoIcon className="w-4 h-4" />
<UndoIcon className="w-3.5 h-3.5" />
</CustomButton>
<CustomButton
variant="primary"
size="sm"
onClick={onRedo}
disabled={!canRedo}
className="bg-gray-50 text-gray-700 hover:bg-gray-100 disabled:bg-gray-25 disabled:text-gray-400"
className="min-w-8 h-8 px-2 bg-gray-50 text-gray-700 hover:bg-gray-100 disabled:bg-gray-25 disabled:text-gray-400"
showHoverContent={true}
hoverContent="Redo"
>
<RedoIcon className="w-4 h-4" />
<RedoIcon className="w-3.5 h-3.5" />
</CustomButton>
</>}
</div>}
{/* View controls (hidden in live mode) */}
{!isLive && (<div className="flex items-center gap-2 mr-2">
{(() => {
// Current visibility booleans
const showAgents = viewMode !== "two_chat_skipper";
const showChat = viewMode !== "two_agents_skipper";
const showSkipper = viewMode !== "two_agents_chat";
// Determine selected radio option
type RadioKey = 'show-all' | 'hide-agents' | 'hide-chat' | 'hide-skipper';
let selectedKey: RadioKey = 'show-all';
if (!(showAgents && showChat && showSkipper)) {
if (!showAgents) selectedKey = 'hide-agents';
else if (!showChat) selectedKey = 'hide-chat';
else if (!showSkipper) selectedKey = 'hide-skipper';
}
// Map radio selection to viewMode
const setByKey = (key: RadioKey) => {
switch (key) {
case 'show-all':
onSetViewMode('three_all');
break;
case 'hide-agents':
onSetViewMode('two_chat_skipper');
break;
case 'hide-chat':
onSetViewMode('two_agents_skipper');
break;
case 'hide-skipper':
onSetViewMode('two_agents_chat');
break;
}
};
// Disable rules
// When there are zero agents, allow only Show All and Hide Chat
const zeroAgents = !hasAgents;
const disableShowAll = false; // always allow switching to 3-pane view
const disableHideAgents = zeroAgents; // cannot hide agents if none exist
const disableHideChat = false; // allow hide chat even with zero agents (default)
const disableHideSkipper = zeroAgents; // keep skipper visible when no agents
return (
<Dropdown>
<DropdownTrigger>
<Button variant="light" size="sm" aria-label="Layout options" className="h-8 min-w-0 bg-transparent text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/50 border border-transparent gap-1 px-2">
{/* Unified icon: 3-pane visual */}
<svg width="20" height="14" viewBox="0 0 22 16" aria-hidden="true">
<rect x="1" y="1" width="20" height="14" rx="2" fill="none" stroke="currentColor" opacity=".55" />
<rect x="2.3" y="2.5" width="5.5" height="11" rx="1.2" fill="currentColor" opacity=".8" />
<rect x="8.5" y="2.5" width="6" height="11" rx="1.2" fill="currentColor" opacity=".5" />
<rect x="15.5" y="2.5" width="5.5" height="11" rx="1.2" fill="currentColor" opacity=".4" />
</svg>
<ChevronDownIcon size={14} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Choose layout" selectionMode="single" selectedKeys={[selectedKey]} closeOnSelect={true} onSelectionChange={(keys) => {
const key = Array.from(keys as Set<string>)[0] as RadioKey;
const zeroAgents = !hasAgents;
// Allow only permitted options when zero agents
if (zeroAgents && key !== 'show-all' && key !== 'hide-chat') return;
if (key === 'hide-chat' && disableHideChat) return;
setByKey(key);
}}>
<DropdownItem key="show-all" isDisabled={disableShowAll} className={selectedKey==='show-all' ? 'bg-zinc-100 dark:bg-zinc-800' : ''} startContent={<input type="radio" readOnly checked={selectedKey==='show-all'} className="accent-zinc-600 dark:accent-zinc-300" />}>Show All</DropdownItem>
<DropdownItem key="hide-agents" isDisabled={disableHideAgents} className={selectedKey==='hide-agents' ? 'bg-zinc-100 dark:bg-zinc-800' : ''} startContent={<input type="radio" readOnly checked={selectedKey==='hide-agents'} className="accent-zinc-600 dark:accent-zinc-300" />}>Hide Agents</DropdownItem>
<DropdownItem key="hide-chat" isDisabled={disableHideChat} className={selectedKey==='hide-chat' ? 'bg-zinc-100 dark:bg-zinc-800' : ''} startContent={<input type="radio" readOnly checked={selectedKey==='hide-chat'} className="accent-zinc-600 dark:accent-zinc-300" />}>Hide Chat</DropdownItem>
<DropdownItem key="hide-skipper" isDisabled={disableHideSkipper} className={selectedKey==='hide-skipper' ? 'bg-zinc-100 dark:bg-zinc-800' : ''} startContent={<input type="radio" readOnly checked={selectedKey==='hide-skipper'} className="accent-zinc-600 dark:accent-zinc-300" />}>Hide Skipper</DropdownItem>
</DropdownMenu>
</Dropdown>
);
})()}
</div>)}
{/* Deploy CTA - always visible */}
<div className="flex items-center gap-3">
{isLive ? (
@ -196,13 +282,13 @@ export function TopBar({
<DropdownTrigger>
<Button
variant="solid"
size="md"
className="gap-2 px-4 bg-blue-50 hover:bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 dark:text-blue-400 font-semibold text-sm border border-blue-200 dark:border-blue-700 shadow-sm"
startContent={<Plug size={16} />}
size="sm"
className="gap-2 px-3 h-8 bg-blue-50 hover:bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 dark:text-blue-400 font-semibold text-sm border border-blue-200 dark:border-blue-700 shadow-sm"
startContent={<Plug size={14} />}
onPress={onUseAssistantClick}
>
Use Assistant
<ChevronDownIcon size={14} />
<ChevronDownIcon size={12} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Assistant access options">
@ -239,7 +325,6 @@ export function TopBar({
</DropdownMenu>
</Dropdown>
{/* Live workflow label moved here */}
<div className="flex items-center gap-2 ml-2">
{publishing && <Spinner size="sm" />}
<div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
@ -270,7 +355,7 @@ export function TopBar({
<Tooltip content="Download Assistant JSON">
<button
onClick={onDownloadJSON}
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors cursor-pointer"
aria-label="Download JSON"
type="button"
>
@ -282,47 +367,79 @@ export function TopBar({
) : (
<>
<div className="flex">
<Button
variant="solid"
size="md"
onPress={onPublishWorkflow}
className="gap-2 px-4 bg-green-100 hover:bg-green-200 text-green-800 font-semibold text-sm rounded-r-none border border-green-300 shadow-sm"
startContent={<RocketIcon size={16} />}
data-tour-target="deploy"
>
Publish
</Button>
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="md"
className="min-w-0 px-2 bg-green-100 hover:bg-green-200 text-green-800 rounded-l-none border border-l-0 border-green-300 shadow-sm"
>
<ChevronDownIcon size={14} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Deploy actions">
<DropdownItem
key="view-live"
startContent={<RadioIcon size={16} />}
onPress={() => onChangeMode('live')}
>
View live version
</DropdownItem>
<DropdownItem
key="reset-to-live"
startContent={<AlertTriangle size={16} />}
onPress={onRevertToLive}
className="text-red-600 dark:text-red-400"
>
Reset to live version
</DropdownItem>
</DropdownMenu>
</Dropdown>
{(!hasAgents) ? (
<Tooltip content="Create agents to publish your assistant">
<span className="inline-flex">
<Button
variant="solid"
size="sm"
onPress={onPublishWorkflow}
isDisabled
className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed`}
startContent={<RocketIcon size={14} />}
data-tour-target="deploy"
>
Publish
</Button>
</span>
</Tooltip>
) : (
<Button
variant="solid"
size="sm"
onPress={onPublishWorkflow}
className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-green-100 hover:bg-green-200 text-green-800 border-green-300`}
startContent={<RocketIcon size={14} />}
data-tour-target="deploy"
>
Publish
</Button>
)}
{hasAgents ? (
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="sm"
className={`min-w-0 px-2 h-8 rounded-l-none border border-l-0 shadow-sm bg-green-100 hover:bg-green-200 text-green-800 border-green-300`}
>
<ChevronDownIcon size={12} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Deploy actions">
<DropdownItem
key="view-live"
startContent={<RadioIcon size={16} />}
onPress={() => onChangeMode('live')}
>
View live version
</DropdownItem>
<DropdownItem
key="reset-to-live"
startContent={<AlertTriangle size={16} />}
onPress={onRevertToLive}
className="text-red-600 dark:text-red-400"
>
Reset to live version
</DropdownItem>
</DropdownMenu>
</Dropdown>
) : (
<Tooltip content="Create agents to publish your assistant">
<span className="inline-flex">
<Button
variant="solid"
size="sm"
isDisabled
className={`min-w-0 px-2 h-8 rounded-l-none border border-l-0 shadow-sm bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed`}
>
<ChevronDownIcon size={12} />
</Button>
</span>
</Tooltip>
)}
</div>
{/* Moved draft/live labels and download button here */}
<div className="flex items-center gap-2 ml-2">
{publishing && <Spinner size="sm" />}
{isLive && <div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
@ -357,7 +474,7 @@ export function TopBar({
<Tooltip content="Download Assistant JSON">
<button
onClick={onDownloadJSON}
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors cursor-pointer"
aria-label="Download JSON"
type="button"
>

View file

@ -41,12 +41,50 @@ import { TopBar } from "./components/TopBar";
enablePatches();
// View mode specific panel ratios
// To maintain same absolute width for entityList across modes, we need to calculate
// the percentage relative to visible panels only
const VIEW_MODE_RATIOS = {
three_all: {
// Three panel layout with equal distribution between chat and copilot
entityList: 25, // Agents panel takes 25% of total width
chatApp: 37.5, // Chat panel takes 37.5% of total width
copilot: 37.5 // Copilot panel takes 37.5% of total width
},
two_agents_chat: {
// Two panel layout showing agents and chat
// entityList maintains same absolute width as three panel layout (25/62.5 = 40%)
entityList: 40, // Agents panel takes 40% of visible width
chatApp: 60, // Chat panel takes remaining 60% width
copilot: 0 // Copilot panel is hidden
},
two_agents_skipper: {
// Two panel layout showing agents and copilot
// entityList maintains same absolute width as three panel layout (25/62.5 = 40%)
entityList: 40, // Agents panel takes 40% of visible width
chatApp: 0, // Chat panel is hidden
copilot: 60 // Copilot panel takes remaining 60% width
},
two_chat_skipper: {
// Two panel layout showing chat and copilot with equal split
entityList: 0, // Agents panel is hidden
chatApp: 50, // Chat panel takes 50% width
copilot: 50 // Copilot panel takes 50% width
}
} as const;
// Legacy PANEL_RATIOS for backward compatibility
const PANEL_RATIOS = {
entityList: 25, // Left panel
chatApp: 40, // Middle panel
copilot: 35 // Right panel
} as const;
// Helper function to get panel ratios for current view mode
const getPanelRatios = (viewMode: "two_agents_chat" | "two_agents_skipper" | "two_chat_skipper" | "three_all") => {
return VIEW_MODE_RATIOS[viewMode];
};
interface StateItem {
workflow: z.infer<typeof Workflow>;
publishing: boolean;
@ -89,19 +127,20 @@ export type Action = {
} | {
type: "add_agent";
agent: Partial<z.infer<typeof WorkflowAgent>>;
fromCopilot?: boolean;
} | {
type: "add_tool";
tool: Partial<z.infer<typeof WorkflowTool>>;
fromCopilot?: boolean;
} | {
type: "add_prompt";
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
} | {
type: "add_prompt_no_select";
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
fromCopilot?: boolean;
} | {
type: "add_pipeline";
pipeline: Partial<z.infer<typeof WorkflowPipeline>>;
defaultModel?: string;
fromCopilot?: boolean;
} | {
type: "select_agent";
name: string;
@ -128,10 +167,18 @@ export type Action = {
type: "update_agent";
name: string;
agent: Partial<z.infer<typeof WorkflowAgent>>;
} | {
type: "update_agent_no_select";
name: string;
agent: Partial<z.infer<typeof WorkflowAgent>>;
} | {
type: "update_tool";
name: string;
tool: Partial<z.infer<typeof WorkflowTool>>;
} | {
type: "update_tool_no_select";
name: string;
tool: Partial<z.infer<typeof WorkflowTool>>;
} | {
type: "set_saving";
saving: boolean;
@ -363,6 +410,9 @@ function reducer(state: State, action: Action): State {
newAgentName = `New agent ${draft.workflow.agents.filter((agent) =>
agent.name.startsWith("New agent")).length + 1}`;
}
const finalAgentName = action.agent.name || newAgentName;
draft.workflow?.agents.push({
name: newAgentName,
type: "conversation",
@ -379,10 +429,19 @@ function reducer(state: State, action: Action): State {
maxCallsPerParentAgent: 3,
...action.agent
});
draft.selection = {
type: "agent",
name: action.agent.name || newAgentName
};
// If this is the first agent or there's no start agent, set it as start agent
if (!draft.workflow?.startAgent || draft.workflow.agents.length === 1) {
draft.workflow.startAgent = finalAgentName;
}
// Only set selection if not from Copilot
if (!action.fromCopilot) {
draft.selection = {
type: "agent",
name: action.agent.name || newAgentName
};
}
draft.pendingChanges = true;
draft.chatKey++;
break;
@ -404,10 +463,13 @@ function reducer(state: State, action: Action): State {
mockTool: false,
...action.tool
});
draft.selection = {
type: "tool",
name: action.tool.name || newToolName
};
// Only set selection if not from Copilot
if (!action.fromCopilot) {
draft.selection = {
type: "tool",
name: action.tool.name || newToolName
};
}
draft.pendingChanges = true;
draft.chatKey++;
break;
@ -424,27 +486,13 @@ function reducer(state: State, action: Action): State {
prompt: "",
...action.prompt
});
draft.selection = {
type: "prompt",
name: action.prompt.name || newPromptName
};
draft.pendingChanges = true;
draft.chatKey++;
break;
}
case "add_prompt_no_select": {
let newPromptName = "New Variable";
if (draft.workflow?.prompts.some((prompt) => prompt.name === newPromptName)) {
newPromptName = `New Variable ${draft.workflow?.prompts.filter((prompt) =>
prompt.name.startsWith("New Variable")).length + 1}`;
// Only set selection if not from Copilot
if (!action.fromCopilot) {
draft.selection = {
type: "prompt",
name: action.prompt.name || newPromptName
};
}
draft.workflow?.prompts.push({
name: newPromptName,
type: "base_prompt",
prompt: "",
...action.prompt
});
// Don't set selection - this is the key difference
draft.pendingChanges = true;
draft.chatKey++;
break;
@ -516,8 +564,8 @@ function reducer(state: State, action: Action): State {
...action.pipeline
});
// 4. ✅ Select the first agent for configuration
if (pipelineAgents.length > 0) {
// 4. ✅ Select the first agent for configuration (only if not from Copilot)
if (pipelineAgents.length > 0 && !action.fromCopilot) {
draft.selection = {
type: "agent",
name: pipelineAgents[0]
@ -717,6 +765,18 @@ function reducer(state: State, action: Action): State {
draft.chatKey++;
break;
}
case "update_agent_no_select": {
// Same as update_agent but do not change selection
if (action.agent.instructions !== undefined) {
draft.agentInstructionsChanged = true;
}
draft.workflow.agents = draft.workflow.agents.map((agent) =>
agent.name === action.name ? { ...agent, ...action.agent } : agent
);
draft.pendingChanges = true;
draft.chatKey++;
break;
}
case "update_tool":
// update tool data
@ -759,6 +819,13 @@ function reducer(state: State, action: Action): State {
draft.pendingChanges = true;
draft.chatKey++;
break;
case "update_tool_no_select":
draft.workflow.tools = draft.workflow.tools.map((tool) =>
tool.name === action.name ? { ...tool, ...action.tool } : tool
);
draft.pendingChanges = true;
draft.chatKey++;
break;
case "update_prompt":
// update prompt data
@ -938,6 +1005,56 @@ export function WorkflowEditor({
}
});
// View mode state controls top-level layout visibility (not unmounting panes)
type ViewMode = "two_agents_chat" | "two_agents_skipper" | "two_chat_skipper" | "three_all";
const [viewMode, setViewMode] = useState<ViewMode>(() => {
if (typeof window === 'undefined') return "three_all";
const fromUrl = new URLSearchParams(window.location.search).get('view');
const valid: ViewMode[] = ["two_agents_chat", "two_agents_skipper", "two_chat_skipper", "three_all"];
if (fromUrl && (valid as string[]).includes(fromUrl)) {
localStorage.setItem('workflow_view_mode', fromUrl);
return fromUrl as ViewMode;
}
return (localStorage.getItem('workflow_view_mode') as ViewMode) || "three_all";
});
const updateViewMode = useCallback((mode: ViewMode) => {
setViewMode(mode);
// Clear selection when switching to hide agents mode to close configuration panels
if (mode === 'two_chat_skipper') {
// Clear any active selection to close configuration panels
// All unselect actions set selection to null, so we can use any of them
dispatch({ type: "unselect_agent" });
}
if (typeof window !== 'undefined') {
localStorage.setItem('workflow_view_mode', mode);
const url = new URL(window.location.href);
url.searchParams.set('view', mode);
window.history.replaceState({}, '', url.toString());
}
}, []);
// 1) Auto-layout: when no agents exist, prefer Agents + Skipper
const prevAgentCountRef = useRef<number>(state.present.workflow.agents.length);
useEffect(() => {
const count = state.present.workflow.agents.length;
// If live mode, another effect will pin Agents + Chat; skip here
if (!isLive) {
if (count === 0) {
// Only auto-pin to Agents+Skipper if user hasn't explicitly chosen 3-pane
if (viewMode !== 'two_agents_skipper' && viewMode !== 'three_all') {
updateViewMode('two_agents_skipper');
}
} else if (prevAgentCountRef.current === 0 && count > 0) {
// 2) As soon as first agent is created from zero, switch to default (three panes)
updateViewMode('three_all');
}
}
prevAgentCountRef.current = count;
}, [state.present.workflow.agents.length, isLive, updateViewMode, viewMode]);
const [chatMessages, setChatMessages] = useState<z.infer<typeof Message>[]>([]);
const updateChatMessages = useCallback((messages: z.infer<typeof Message>[]) => {
setChatMessages(messages);
@ -1068,10 +1185,15 @@ export function WorkflowEditor({
if (startNewChatRef.current) {
startNewChatRef.current();
}
// Switch to playground (chat) mode and collapse left panel
// Ensure chat is visible and collapse left panel
setActivePanel('playground');
setViewMode((prev: ViewMode) => prev);
updateViewMode(
viewMode === 'three_all' ? 'three_all' :
(viewMode === 'two_agents_skipper' ? 'two_agents_chat' : 'two_chat_skipper')
);
setIsLeftPanelCollapsed(true);
}, []);
}, [updateViewMode, viewMode]);
// Load agent order from localStorage on mount
// useEffect(() => {
@ -1097,33 +1219,53 @@ export function WorkflowEditor({
// Function to trigger copilot chat
const triggerCopilotChat = useCallback((message: string) => {
setActivePanel('copilot');
updateViewMode(
viewMode === 'three_all' ? 'three_all' :
(viewMode === 'two_agents_chat' ? 'two_agents_skipper' : 'two_chat_skipper')
);
// Small delay to ensure copilot is mounted
setTimeout(() => {
copilotRef.current?.handleUserMessage(message);
}, 100);
}, []);
}, [updateViewMode, viewMode]);
const handleOpenDataSourcesModal = useCallback(() => {
entityListRef.current?.openDataSourcesModal();
}, []);
console.log(`workflow editor chat key: ${state.present.chatKey}`);
// Auto-show copilot and increment key when prompt is present
// Auto-show copilot and send initial prompt exactly once when present
const hasSentInitPromptRef = useRef<boolean>(false);
useEffect(() => {
if (hasSentInitPromptRef.current) return;
const prompt = localStorage.getItem(`project_prompt_${projectId}`);
console.log('init project prompt', prompt);
if (prompt) {
setActivePanel('copilot');
}
}, [projectId]);
if (!prompt) return;
// Mark as handled and remove immediately to avoid any other readers
hasSentInitPromptRef.current = true;
localStorage.removeItem(`project_prompt_${projectId}`);
// Switch UI to show Copilot
setActivePanel('copilot');
updateViewMode(viewMode === 'three_all' ? 'three_all' : (viewMode.includes('agents') ? 'two_agents_skipper' : 'two_chat_skipper'));
// Allow layout to render Copilot, then send the prompt via ref
requestAnimationFrame(() => {
requestAnimationFrame(() => {
copilotRef.current?.handleUserMessage(prompt);
});
});
}, [projectId, updateViewMode, viewMode]);
// Switch to playground when switching to live mode
useEffect(() => {
if (isLive) {
setActivePanel('playground');
// 3) In live mode, pin view to Agents + Chat
updateViewMode('two_agents_chat');
}
}, [isLive]);
}, [isLive, updateViewMode, viewMode]);
// Reset initial state when user interacts with copilot or opens other menus
useEffect(() => {
@ -1307,7 +1449,7 @@ export function WorkflowEditor({
// Modal-specific handlers that don't auto-select
function handleAddPromptFromModal(prompt: Partial<z.infer<typeof WorkflowPrompt>>) {
dispatch({ type: "add_prompt_no_select", prompt });
dispatch({ type: "add_prompt", prompt, fromCopilot: true });
}
function handleUpdatePromptFromModal(name: string, prompt: Partial<z.infer<typeof WorkflowPrompt>>) {
@ -1521,7 +1663,7 @@ export function WorkflowEditor({
}, [isLive, state.present.publishing, onChangeMode]);
const WORKFLOW_MOD_ACTIONS = useRef(new Set([
'add_agent','add_tool','add_prompt','add_prompt_no_select','add_pipeline',
'add_agent','add_tool','add_prompt','add_pipeline',
'update_agent','update_tool','update_prompt','update_prompt_no_select','update_pipeline',
'delete_agent','delete_tool','delete_prompt','delete_pipeline',
'toggle_agent','set_main_agent','reorder_agents','reorder_pipelines'
@ -1595,14 +1737,21 @@ export function WorkflowEditor({
}, [isLive, state.present.isLive]);
function handleTogglePanel() {
if (isLive && activePanel === 'playground') {
if (isLive && (viewMode === 'two_agents_chat' || viewMode === 'two_chat_skipper' || viewMode === 'three_all')) {
// User is trying to switch to Build mode in live mode
handleModeTransition('draft', 'switch_draft');
setShowBuildModeBanner(true);
// Auto-hide banner after 5 seconds
setTimeout(() => setShowBuildModeBanner(false), 5000);
} else {
setActivePanel(activePanel === 'playground' ? 'copilot' : 'playground');
// Toggle between showing chat vs skipper within current context
if (viewMode === 'three_all') {
setActivePanel(activePanel === 'playground' ? 'copilot' : 'playground');
return;
}
if (viewMode === 'two_agents_chat') updateViewMode('two_agents_skipper');
else if (viewMode === 'two_agents_skipper') updateViewMode('two_agents_chat');
else if (viewMode === 'two_chat_skipper') updateViewMode('two_chat_skipper');
}
}
@ -1663,6 +1812,9 @@ export function WorkflowEditor({
}
};
const [isHydrated, setIsHydrated] = useState(false);
useEffect(() => { setIsHydrated(true); }, []);
return (
<EntitySelectionContext.Provider value={{
onSelectAgent: handleSelectAgent,
@ -1716,6 +1868,8 @@ export function WorkflowEditor({
canUndo={state.currentIndex > 0}
canRedo={state.currentIndex < state.patches.length}
activePanel={activePanel}
viewMode={viewMode}
hasAgents={state.present.workflow.agents.length > 0}
hasAgentInstructionChanges={hasAgentInstructionChanges}
hasPlaygroundTested={hasPlaygroundTested}
hasPublished={hasPublished}
@ -1730,26 +1884,138 @@ export function WorkflowEditor({
onChangeMode={onChangeMode}
onRevertToLive={handleRevertToLive}
onTogglePanel={handleTogglePanel}
onSetViewMode={updateViewMode}
onUseAssistantClick={markUseAssistantClicked}
onStartNewChatAndFocus={handleStartNewChatAndFocus}
onStartBuildTour={() => setShowBuildTour(true)}
onStartTestTour={() => setShowTestTour(true)}
onStartBuildTour={() => {
// Ensure 3-pane layout first, then start tour after layout renders
updateViewMode('three_all');
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setShowBuildTour(true);
});
});
}}
onStartTestTour={() => {
updateViewMode('three_all');
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setShowTestTour(true);
});
});
}}
onStartPublishTour={() => {
// Switch to 3-pane first to ensure elements are visible
updateViewMode('three_all');
if (isLive) {
handleModeTransition('draft', 'switch_draft');
}
setShowPublishTour(true);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setShowPublishTour(true);
});
});
}}
onStartUseTour={() => {
updateViewMode('three_all');
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setShowUseTour(true);
});
});
}}
onStartUseTour={() => setShowUseTour(true)}
/>
{/* Content Area */}
<ResizablePanelGroup direction="horizontal" className="flex-1 flex overflow-auto gap-1 rounded-xl bg-zinc-50 dark:bg-zinc-900">
{/* Content Area - hydration-safe layout */}
{!isHydrated ? (
<ResizablePanelGroup key={`hydration-${viewMode}`} direction="horizontal" className="flex-1 flex overflow-auto gap-1 rounded-xl bg-zinc-50 dark:bg-zinc-900">
{(viewMode !== 'two_chat_skipper') && (
<ResizablePanel
key={`entity-list-${state.present.selection ? '3-pane' : '2-pane'}`}
key={`entity-list-hydration`}
minSize={10}
defaultSize={PANEL_RATIOS.entityList}
className={isLeftPanelCollapsed ? 'hidden' : ''}
defaultSize={getPanelRatios(viewMode).entityList}
id="entities"
order={1}
className={`${isLeftPanelCollapsed ? 'hidden' : ''}`}
>
<div className="flex flex-col h-full">
<EntityList
ref={entityListRef}
agents={state.present.workflow.agents}
tools={state.present.workflow.tools}
prompts={state.present.workflow.prompts}
pipelines={state.present.workflow.pipelines || []}
dataSources={dataSources}
workflow={state.present.workflow}
selectedEntity={null}
startAgentName={state.present.workflow.startAgent}
onSelectAgent={handleSelectAgent}
onSelectTool={handleSelectTool}
onSelectPrompt={handleSelectPrompt}
onSelectPipeline={handleSelectPipeline}
onSelectDataSource={handleSelectDataSource}
onAddAgent={handleAddAgent}
onAddTool={handleAddTool}
onAddPrompt={handleAddPrompt}
onUpdatePrompt={handleUpdatePrompt}
onAddPromptFromModal={handleAddPromptFromModal}
onUpdatePromptFromModal={handleUpdatePromptFromModal}
onAddPipeline={handleAddPipeline}
onAddAgentToPipeline={handleAddAgentToPipeline}
onToggleAgent={handleToggleAgent}
onSetMainAgent={handleSetMainAgent}
onDeleteAgent={handleDeleteAgent}
onDeleteTool={handleDeleteTool}
onDeletePrompt={handleDeletePrompt}
onDeletePipeline={handleDeletePipeline}
onShowVisualise={handleShowVisualise}
projectId={projectId}
onProjectToolsUpdated={onProjectToolsUpdated}
onDataSourcesUpdated={onDataSourcesUpdated}
projectConfig={projectConfig}
onReorderAgents={handleReorderAgents}
onReorderPipelines={handleReorderPipelines}
useRagUploads={useRagUploads}
useRagS3Uploads={useRagS3Uploads}
useRagScraping={useRagScraping}
/>
</div>
</ResizablePanel>
)}
{(viewMode !== 'two_chat_skipper') && (
<ResizableHandle withHandle className={`w-[3px] bg-transparent ${(isLeftPanelCollapsed) ? 'hidden' : ''}`} />
)}
{(viewMode === 'two_agents_chat' || viewMode === 'three_all') && (
<ResizablePanel minSize={20} defaultSize={getPanelRatios(viewMode).chatApp} id="chat" order={2} className="overflow-hidden">
{/* Minimal mount of Chat during SSR hydration */}
<div className="h-full" />
</ResizablePanel>
)}
{(viewMode === 'three_all') && (<ResizableHandle withHandle className="w-[3px] bg-transparent" />)}
{(viewMode === 'two_agents_skipper' || viewMode === 'three_all') && (
<ResizablePanel minSize={20} defaultSize={getPanelRatios(viewMode).copilot} id="copilot" order={3} className="overflow-hidden">
<div className="h-full" />
</ResizablePanel>
)}
{(viewMode === 'two_chat_skipper') && (
<>
<ResizablePanel minSize={20} defaultSize={getPanelRatios(viewMode).chatApp} id="chat" order={1} className="overflow-hidden"><div className="h-full" /></ResizablePanel>
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
<ResizablePanel minSize={20} defaultSize={getPanelRatios(viewMode).copilot} id="copilot" order={2} className="overflow-hidden"><div className="h-full" /></ResizablePanel>
</>
)}
</ResizablePanelGroup>
) : (
<ResizablePanelGroup key={`main-${viewMode}`} direction="horizontal" className="flex-1 flex overflow-auto gap-1 rounded-xl bg-zinc-50 dark:bg-zinc-900">
{/* Agents (Entity List) column */}
{(viewMode !== 'two_chat_skipper') && (
<ResizablePanel
key={`entity-list-main`}
minSize={10}
defaultSize={getPanelRatios(viewMode).entityList}
id="entities"
order={1}
className={`${isLeftPanelCollapsed ? 'hidden' : ''}`}
>
<div className="flex flex-col h-full">
<EntityList
@ -1803,150 +2069,209 @@ export function WorkflowEditor({
/>
</div>
</ResizablePanel>
<ResizableHandle withHandle className={`w-[3px] bg-transparent ${(isLeftPanelCollapsed || !state.present.selection) ? 'hidden' : ''}`} />
)}
{(viewMode !== 'two_chat_skipper') && (
<ResizableHandle withHandle className={`w-[3px] bg-transparent ${(isLeftPanelCollapsed && !state.present.selection) ? 'hidden' : ''}`} />
)}
{/* Config Panel - always rendered, visibility controlled */}
<ResizablePanel
minSize={20}
defaultSize={45}
className={`overflow-auto ${!state.present.selection ? 'hidden' : ''}`}
>
{state.present.selection?.type === "agent" && <AgentConfig
key={`agent-${state.present.workflow.agents.findIndex(agent => agent.name === state.present.selection!.name)}-${configKey}`}
{/* Playground column - always mounted; hide via viewMode */}
<ResizablePanel minSize={20} defaultSize={getPanelRatios(viewMode).chatApp} id="chat" order={2} className={`overflow-hidden relative ${viewMode === 'two_agents_skipper' ? 'hidden' : ''}`}>
<ChatApp
key={'' + state.present.chatKey}
projectId={projectId}
workflow={state.present.workflow}
agent={state.present.workflow.agents.find((agent) => agent.name === state.present.selection!.name)!}
usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))}
usedPipelineNames={new Set((state.present.workflow.pipelines || []).map((pipeline) => pipeline.name))}
agents={state.present.workflow.agents}
tools={state.present.workflow.tools}
prompts={state.present.workflow.prompts}
dataSources={dataSources}
handleUpdate={(update) => { dispatchGuarded({ type: "update_agent", name: state.present.selection!.name, agent: update }); }}
handleClose={handleUnselectAgent}
useRag={useRag}
messageSubscriber={updateChatMessages}
onPanelClick={handlePlaygroundClick}
triggerCopilotChat={triggerCopilotChat}
eligibleModels={eligibleModels === "*" ? "*" : eligibleModels.agentModels}
onOpenDataSourcesModal={handleOpenDataSourcesModal}
/>}
{state.present.selection?.type === "tool" && (() => {
const selectedTool = state.present.workflow.tools.find(
(tool) => tool.name === state.present.selection!.name
);
return <ToolConfig
key={`${state.present.selection.name}-${configKey}`}
tool={selectedTool!}
usedToolNames={new Set([
...state.present.workflow.tools.filter((tool) => tool.name !== state.present.selection!.name).map((tool) => tool.name),
])}
handleUpdate={(update) => { dispatchGuarded({ type: "update_tool", name: state.present.selection!.name, tool: update }); }}
handleClose={handleUnselectTool}
/>;
})()}
{state.present.selection?.type === "prompt" && <PromptConfig
key={`${state.present.selection.name}-${configKey}`}
prompt={state.present.workflow.prompts.find((prompt) => prompt.name === state.present.selection!.name)!}
agents={state.present.workflow.agents}
tools={state.present.workflow.tools}
prompts={state.present.workflow.prompts}
usedPromptNames={new Set(state.present.workflow.prompts.filter((prompt) => prompt.name !== state.present.selection!.name).map((prompt) => prompt.name))}
handleUpdate={(update) => { dispatchGuarded({ type: "update_prompt", name: state.present.selection!.name, prompt: update }); }}
handleClose={handleUnselectPrompt}
/>}
{state.present.selection?.type === "datasource" && <DataSourceConfig
key={`${state.present.selection.name}-${configKey}`}
dataSourceId={state.present.selection.name}
handleClose={() => dispatch({ type: "unselect_datasource" })}
onDataSourceUpdate={onDataSourcesUpdated}
/>}
{state.present.selection?.type === "pipeline" && <PipelineConfig
key={`${state.present.selection.name}-${configKey}`}
projectId={projectId}
workflow={state.present.workflow}
pipeline={state.present.workflow.pipelines?.find((pipeline) => pipeline.name === state.present.selection!.name)!}
usedPipelineNames={new Set((state.present.workflow.pipelines || []).filter((pipeline) => pipeline.name !== state.present.selection!.name).map((pipeline) => pipeline.name))}
usedAgentNames={new Set(state.present.workflow.agents.map((agent) => agent.name))}
agents={state.present.workflow.agents}
pipelines={state.present.workflow.pipelines || []}
handleUpdate={handleUpdatePipeline.bind(null, state.present.selection.name)}
handleClose={() => dispatch({ type: "unselect_pipeline" })}
/>}
{state.present.selection?.type === "visualise" && (
<Panel
title={
<div className="flex items-center justify-between w-full">
<div className="text-base font-semibold text-gray-900 dark:text-gray-100">
Agent Graph Visualizer
</div>
<CustomButton
variant="secondary"
size="sm"
onClick={handleHideVisualise}
showHoverContent={true}
hoverContent="Close"
>
<XIcon className="w-4 h-4" />
</CustomButton>
</div>
}
>
<div className="h-full overflow-hidden">
<AgentGraphVisualizer workflow={state.present.workflow} />
isLiveWorkflow={isLive}
activePanel={activePanel}
onTogglePanel={handleTogglePanel}
onMessageSent={markPlaygroundTested}
/>
{/* Config overlay above Playground when selection open */}
{state.present.selection && viewMode !== 'two_agents_skipper' && (
<div className="absolute inset-0 z-10">
<div className="h-full overflow-auto">
{state.present.selection?.type === "agent" && <AgentConfig
key={`overlay-agent-${state.present.workflow.agents.findIndex(agent => agent.name === state.present.selection!.name)}-${configKey}`}
projectId={projectId}
workflow={state.present.workflow}
agent={state.present.workflow.agents.find((agent) => agent.name === state.present.selection!.name)!}
usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))}
usedPipelineNames={new Set((state.present.workflow.pipelines || []).map((pipeline) => pipeline.name))}
agents={state.present.workflow.agents}
tools={state.present.workflow.tools}
prompts={state.present.workflow.prompts}
dataSources={dataSources}
handleUpdate={(update) => { dispatchGuarded({ type: "update_agent", name: state.present.selection!.name, agent: update }); }}
handleClose={handleUnselectAgent}
useRag={useRag}
triggerCopilotChat={triggerCopilotChat}
eligibleModels={eligibleModels === "*" ? "*" : eligibleModels.agentModels}
onOpenDataSourcesModal={handleOpenDataSourcesModal}
/>}
{state.present.selection?.type === "tool" && (() => {
const selectedTool = state.present.workflow.tools.find(
(tool) => tool.name === state.present.selection!.name
);
return <ToolConfig
key={`overlay-${state.present.selection.name}-${configKey}`}
tool={selectedTool!}
usedToolNames={new Set([
...state.present.workflow.tools.filter((tool) => tool.name !== state.present.selection!.name).map((tool) => tool.name),
])}
handleUpdate={(update) => { dispatchGuarded({ type: "update_tool", name: state.present.selection!.name, tool: update }); }}
handleClose={handleUnselectTool}
/>;
})()}
{state.present.selection?.type === "prompt" && <PromptConfig
key={`overlay-${state.present.selection.name}-${configKey}`}
prompt={state.present.workflow.prompts.find((prompt) => prompt.name === state.present.selection!.name)!}
agents={state.present.workflow.agents}
tools={state.present.workflow.tools}
prompts={state.present.workflow.prompts}
usedPromptNames={new Set(state.present.workflow.prompts.filter((prompt) => prompt.name !== state.present.selection!.name).map((prompt) => prompt.name))}
handleUpdate={(update) => { dispatchGuarded({ type: "update_prompt", name: state.present.selection!.name, prompt: update }); }}
handleClose={handleUnselectPrompt}
/>}
{state.present.selection?.type === "datasource" && <DataSourceConfig
key={`overlay-${state.present.selection.name}-${configKey}`}
dataSourceId={state.present.selection.name}
handleClose={() => dispatch({ type: "unselect_datasource" })}
onDataSourceUpdate={onDataSourcesUpdated}
/>}
{state.present.selection?.type === "pipeline" && <PipelineConfig
key={`overlay-${state.present.selection.name}-${configKey}`}
projectId={projectId}
workflow={state.present.workflow}
pipeline={state.present.workflow.pipelines?.find((pipeline) => pipeline.name === state.present.selection!.name)!}
usedPipelineNames={new Set((state.present.workflow.pipelines || []).filter((pipeline) => pipeline.name !== state.present.selection!.name).map((pipeline) => pipeline.name))}
usedAgentNames={new Set(state.present.workflow.agents.map((agent) => agent.name))}
agents={state.present.workflow.agents}
pipelines={state.present.workflow.pipelines || []}
handleUpdate={handleUpdatePipeline.bind(null, state.present.selection.name)}
handleClose={() => dispatch({ type: "unselect_pipeline" })}
/>}
{state.present.selection?.type === "visualise" && (
<Panel title={<div className="flex items-center justify-between w-full"><div className="text-base font-semibold text-gray-900 dark:text-gray-100">Agent Graph Visualizer</div><CustomButton variant="secondary" size="sm" onClick={handleHideVisualise} showHoverContent={true} hoverContent="Close"><XIcon className="w-4 h-4" /></CustomButton></div>}>
<div className="h-full overflow-hidden">
<AgentGraphVisualizer workflow={state.present.workflow} />
</div>
</Panel>
)}
</div>
</Panel>
</div>
)}
</ResizablePanel>
{/* Second handle - between config and chat panels */}
<ResizableHandle withHandle className={`w-[3px] bg-transparent ${(isLeftPanelCollapsed && !state.present.selection) ? 'hidden' : ''}`} />
{/* ChatApp/Copilot Panel - always visible */}
<ResizablePanel
key={`chat-panel-${state.present.selection ? '3-pane' : '2-pane'}`}
minSize={20}
defaultSize={state.present.selection ? 30 : PANEL_RATIOS.chatApp + PANEL_RATIOS.copilot}
className="overflow-auto"
>
<div className={(activePanel === 'playground') ? 'block h-full' : 'hidden h-full'}>
<ChatApp
key={'' + state.present.chatKey}
projectId={projectId}
workflow={state.present.workflow}
messageSubscriber={updateChatMessages}
onPanelClick={handlePlaygroundClick}
triggerCopilotChat={triggerCopilotChat}
isLiveWorkflow={isLive}
activePanel={activePanel}
onTogglePanel={handleTogglePanel}
onMessageSent={markPlaygroundTested}
/>
</div>
<div className={(activePanel === 'copilot') ? 'block h-full' : 'hidden h-full'}>
<Copilot
ref={copilotRef}
projectId={projectId}
workflow={state.present.workflow}
dispatch={dispatch}
chatContext={
state.present.selection &&
(state.present.selection.type === "agent" ||
state.present.selection.type === "tool" ||
state.present.selection.type === "prompt")
? {
type: state.present.selection.type,
name: state.present.selection.name
}
: chatMessages.length > 0
? { type: 'chat', messages: chatMessages }
: undefined
}
isInitialState={isInitialState}
dataSources={dataSources}
activePanel={activePanel}
onTogglePanel={handleTogglePanel}
/>
</div>
{/* Divider between playground and copilot when both visible */}
{(viewMode === 'three_all' || viewMode === 'two_chat_skipper') && (
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
)}
{/* Copilot column - always mounted; hide via viewMode */}
<ResizablePanel minSize={20} defaultSize={getPanelRatios(viewMode).copilot} id="copilot" order={viewMode === 'three_all' ? 3 : 2} className={`overflow-hidden relative ${viewMode === 'two_agents_chat' ? 'hidden' : ''}`}>
<Copilot
ref={copilotRef}
projectId={projectId}
workflow={state.present.workflow}
dispatch={dispatch}
chatContext={
state.present.selection &&
(state.present.selection.type === "agent" ||
state.present.selection.type === "tool" ||
state.present.selection.type === "prompt")
? {
type: state.present.selection.type,
name: state.present.selection.name
}
: chatMessages.length > 0
? { type: 'chat', messages: chatMessages }
: undefined
}
isInitialState={isInitialState}
dataSources={dataSources}
activePanel={activePanel}
onTogglePanel={handleTogglePanel}
/>
{/* Config overlay above Copilot when agents + skipper layout is active */}
{state.present.selection && viewMode === 'two_agents_skipper' && (
<div className="absolute inset-0 z-10">
<div className="h-full overflow-auto">
{state.present.selection?.type === "agent" && <AgentConfig
key={`overlay2-agent-${state.present.workflow.agents.findIndex(agent => agent.name === state.present.selection!.name)}-${configKey}`}
projectId={projectId}
workflow={state.present.workflow}
agent={state.present.workflow.agents.find((agent) => agent.name === state.present.selection!.name)!}
usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))}
usedPipelineNames={new Set((state.present.workflow.pipelines || []).map((pipeline) => pipeline.name))}
agents={state.present.workflow.agents}
tools={state.present.workflow.tools}
prompts={state.present.workflow.prompts}
dataSources={dataSources}
handleUpdate={(update) => { dispatchGuarded({ type: "update_agent", name: state.present.selection!.name, agent: update }); }}
handleClose={handleUnselectAgent}
useRag={useRag}
triggerCopilotChat={triggerCopilotChat}
eligibleModels={eligibleModels === "*" ? "*" : eligibleModels.agentModels}
onOpenDataSourcesModal={handleOpenDataSourcesModal}
/>}
{state.present.selection?.type === "tool" && (() => {
const selectedTool = state.present.workflow.tools.find(
(tool) => tool.name === state.present.selection!.name
);
return <ToolConfig
key={`overlay2-${state.present.selection.name}-${configKey}`}
tool={selectedTool!}
usedToolNames={new Set([
...state.present.workflow.tools.filter((tool) => tool.name !== state.present.selection!.name).map((tool) => tool.name),
])}
handleUpdate={(update) => { dispatchGuarded({ type: "update_tool", name: state.present.selection!.name, tool: update }); }}
handleClose={handleUnselectTool}
/>;
})()}
{state.present.selection?.type === "prompt" && <PromptConfig
key={`overlay2-${state.present.selection.name}-${configKey}`}
prompt={state.present.workflow.prompts.find((prompt) => prompt.name === state.present.selection!.name)!}
agents={state.present.workflow.agents}
tools={state.present.workflow.tools}
prompts={state.present.workflow.prompts}
usedPromptNames={new Set(state.present.workflow.prompts.filter((prompt) => prompt.name !== state.present.selection!.name).map((prompt) => prompt.name))}
handleUpdate={(update) => { dispatchGuarded({ type: "update_prompt", name: state.present.selection!.name, prompt: update }); }}
handleClose={handleUnselectPrompt}
/>}
{state.present.selection?.type === "datasource" && <DataSourceConfig
key={`overlay2-${state.present.selection.name}-${configKey}`}
dataSourceId={state.present.selection.name}
handleClose={() => dispatch({ type: "unselect_datasource" })}
onDataSourceUpdate={onDataSourcesUpdated}
/>}
{state.present.selection?.type === "pipeline" && <PipelineConfig
key={`overlay2-${state.present.selection.name}-${configKey}`}
projectId={projectId}
workflow={state.present.workflow}
pipeline={state.present.workflow.pipelines?.find((pipeline) => pipeline.name === state.present.selection!.name)!}
usedPipelineNames={new Set((state.present.workflow.pipelines || []).filter((pipeline) => pipeline.name !== state.present.selection!.name).map((pipeline) => pipeline.name))}
usedAgentNames={new Set(state.present.workflow.agents.map((agent) => agent.name))}
agents={state.present.workflow.agents}
pipelines={state.present.workflow.pipelines || []}
handleUpdate={handleUpdatePipeline.bind(null, state.present.selection.name)}
handleClose={() => dispatch({ type: "unselect_pipeline" })}
/>}
{state.present.selection?.type === "visualise" && (
<Panel title={<div className="flex items-center justify-between w-full"><div className="text-base font-semibold text-gray-900 dark:text-gray-100">Agent Graph Visualizer</div><CustomButton variant="secondary" size="sm" onClick={handleHideVisualise} showHoverContent={true} hoverContent="Close"><XIcon className="w-4 h-4" /></CustomButton></div>}>
<div className="h-full overflow-hidden">
<AgentGraphVisualizer workflow={state.present.workflow} />
</div>
</Panel>
)}
</div>
</div>
)}
</ResizablePanel>
</ResizablePanelGroup>
)}
{USE_PRODUCT_TOUR && showTour && (
<ProductTour
projectId={projectId}
@ -1976,7 +2301,7 @@ export function WorkflowEditor({
forceStart
stepsOverride={[
{ target: 'playground', title: 'Step 1/2', content: 'Chat with your assistant to test it. Send messages, watch tool calls in action, and debug agent flows.' },
{ target: 'copilot', title: 'Step 2/2', content: 'Ask Copilot to improve your agents based on test results. Use “Fix” and “Explain” to iterate quickly.' },
{ target: 'copilot', title: 'Step 2/2', content: 'Ask Copilot to improve your agents based on test results. Use "Fix" and "Explain" to iterate quickly.' },
]}
onStepChange={(index) => {
if (index === 0) setActivePanel('playground');

View file

@ -132,7 +132,7 @@ export function ComposeBoxCopilot({
scale-100 hover:scale-105 active:scale-95
disabled:opacity-50 disabled:scale-95
hover:shadow-md dark:hover:shadow-indigo-950/10
mb-0.5
mb-1.5 mr-2
`}
>
{loading ? (

View file

@ -83,14 +83,13 @@ export function Panel({
>
<div
className={clsx(
// For copilot and playground, mimic TopBar appearance
(variant === 'copilot' || variant === 'playground')
? "shrink-0 relative rounded-xl bg-white/70 dark:bg-zinc-800/70 shadow-sm backdrop-blur-sm border border-zinc-200 dark:border-zinc-800 px-0 pt-0 pb-2 mx-0 mt-0 mb-2 flex items-center justify-between"
: "shrink-0 border-b relative",
(variant !== 'copilot' && variant !== 'playground') && "border-zinc-100 dark:border-zinc-800",
"shrink-0 border-b relative",
// Use the same header treatment as entity list/default for playground/copilot
"border-zinc-100 dark:border-zinc-800",
{
"flex flex-col gap-3 px-4 py-3": variant === 'projects',
"flex items-center justify-between h-[53px] p-3": isEntityList,
"flex items-center justify-between px-3 py-2": variant === 'copilot' || variant === 'playground',
"flex items-center justify-between px-6 py-3": !isEntityList && variant !== 'projects' && variant !== 'copilot' && variant !== 'playground'
}
)}

View file

@ -22,18 +22,17 @@ export function SectionCard({ icon, title, children, labelWidth = 'md:w-32', cla
React.useEffect(() => {
const btn = document.getElementById(`section-card-header-${title && typeof title === 'string' ? title : ''}`);
if (btn) {
console.log('SectionCard header button:', btn, btn.getBoundingClientRect(), window.getComputedStyle(btn));
const chevron = btn.querySelector('svg');
if (chevron) {
console.log('Chevron:', chevron, chevron.getBoundingClientRect(), window.getComputedStyle(chevron));
// Chevron positioning logic can go here if needed
}
const iconEl = btn.querySelector('.section-card-icon');
if (iconEl) {
console.log('Icon:', iconEl, iconEl.getBoundingClientRect(), window.getComputedStyle(iconEl));
// Icon positioning logic can go here if needed
}
const label = btn.querySelector('span');
if (label) {
console.log('Label:', label, label.getBoundingClientRect(), window.getComputedStyle(label));
// Label positioning logic can go here if needed
}
}
}, [title]);

View file

@ -28,28 +28,23 @@ export function ProgressBar({ steps, className, onStepClick }: ProgressBarProps)
};
return (
<nav aria-label="Workflow progress" className={cn("flex items-center gap-4", className)}>
{/* Progress Label */}
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 mr-2">
Progress:
</span>
<nav aria-label="Workflow progress" className={cn("flex items-center", className)}>
{/* Steps */}
<ol role="list" className="flex items-center gap-2">
<ol role="list" className="flex items-center gap-1.5">
{steps.map((step, index) => {
const isLast = index === steps.length - 1;
const tooltipText = (() => {
switch (step.id) {
case 1:
return 'Build your assistant - click for tour';
return 'Let skipper build your assistant for you';
case 2:
return 'Test your assistant - click for tour';
return 'Chat with your assistant to test it';
case 3:
return 'Make assistant live - click for tour';
return 'Make your assistant live';
case 4:
return 'Interact with your assistant - click for tour';
return 'Interact with your assistant';
default:
return 'Click for tour';
return '';
}
})();
@ -77,7 +72,7 @@ export function ProgressBar({ steps, className, onStepClick }: ProgressBarProps)
}
} : undefined}
className={cn(
"w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs font-semibold transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-indigo-400",
"w-5 h-5 rounded-full border-2 flex items-center justify-center text-xs font-semibold transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-indigo-400",
step.completed
? "bg-green-500 border-green-500 text-white"
: step.isCurrent
@ -88,7 +83,7 @@ export function ProgressBar({ steps, className, onStepClick }: ProgressBarProps)
{step.completed ? "✓" : step.isCurrent ? "⚡" : "○"}
</div>
</Tooltip>
<span className="hidden md:block mt-1 text-[11px] leading-none text-gray-700 dark:text-gray-300 font-medium">
<span className="hidden md:block mt-0.5 text-[10px] leading-none text-gray-700 dark:text-gray-300 font-medium">
{step.shortLabel ?? getShortLabel(step.label)}
</span>
</div>

View file

@ -379,6 +379,20 @@ function mapConfig(workflow: z.infer<typeof Workflow>): {
}
async function* emitGreetingTurn(logger: PrefixLogger, workflow: z.infer<typeof Workflow>): AsyncIterable<z.infer<typeof ZOutMessage>> {
// Check if there are no agents or no start agent
if (workflow.agents.length === 0 || !workflow.startAgent) {
logger.log(`no agents available, emitting no-agent message`);
// emit no-agent message
yield* emitEvent(logger, {
role: 'assistant',
content: 'Hi! To get started with chatting with your Assistant please create an agent first!',
agentName: 'System',
responseType: 'external',
});
return;
}
// find the greeting prompt
const prompt = workflow.prompts.find(p => p.type === 'greeting')?.prompt || 'How can I help you today?';
logger.log(`greeting turn: ${prompt}`);
@ -1274,6 +1288,20 @@ export async function* streamResponse(
logger.log(`start agent: ${workflow.startAgent}`);
logger.log(`=== END CONFIGURATION ===`);
// Check if there are no agents or no start agent
if (Object.keys(agentConfig).length === 0 || !workflow.startAgent) {
logger.log(`no agents available, emitting no-agent message`);
// emit no-agent message
yield* emitEvent(logger, {
role: 'assistant',
content: 'Hi! To get started with chatting with your Assistant please create an agent first!',
agentName: 'System',
responseType: 'external',
});
return;
}
const stack: string[] = [];
logger.log(`initialized stack: ${JSON.stringify(stack)}`);
@ -1296,6 +1324,20 @@ export async function* streamResponse(
const startOfTurnAgentName = getStartOfTurnAgentName(logger, messages, agentConfig, pipelineConfig, workflow);
logger.log(`🎯 START AGENT DECISION: ${startOfTurnAgentName}`);
// Additional safety check - if startOfTurnAgentName is empty or invalid, return no-agent message
if (!startOfTurnAgentName || !agentConfig[startOfTurnAgentName]) {
logger.log(`invalid start agent name: ${startOfTurnAgentName}, emitting no-agent message`);
// emit no-agent message
yield* emitEvent(logger, {
role: 'assistant',
content: 'Hi! To get started with chatting with your Assistant please create an agent first!',
agentName: 'System',
responseType: 'external',
});
return;
}
let agentName: string | null = startOfTurnAgentName;
// start the turn loop

View file

@ -39,7 +39,7 @@ When the user asks you to create agents for a multi-agent system, you should fol
1. Understand the users intent what they want the workflow to achieve. Plan accordingly to build an elegant and efficient system.
2. Identify required tools - if the user mentions specific tasks (e.g. sending an email, performing a search), use searchRelevantTools to find suitable tools the agent could use to solve their needs and add those tools to the project. Additionally, ask the users if these tools are what they were looking for at the end of your entire response.
3. Create a first draft of a new agent for each step in the plan. If there is an example agent, you must start off by editing this into the Hub agent. Attach all tools to the relevant agents.
3. Create a first draft of a new agent for each step in the plan. You must always ensure to set a start agent when creating a multi-agent system. Attach all tools to the relevant agents.
4. Describe your work briefly summarise what you've done at the end of your turn.
It is good practice to add tools first and then agents
@ -77,6 +77,8 @@ CRITICAL: Always include these required fields when creating agents:
- For task agents: "outputVisibility": "internal", "controlType": "relinquish_to_parent"
- For conversational agents: "outputVisibility": "user_facing", "controlType": "retain"
CRITICAL: When creating a multi-agent system, you MUST always set a start agent. Use the action "set_main_agent" or "edit" with "config_type": "start_agent" to set the start agent to the main conversational agent (usually the Hub agent).
However, there are some important things you need to instruct the individual agents when they call other agents (you need to customize the below to the specific agent and its):
- SEQUENTIAL TRANSFERS AND RESPONSES:

View file

@ -7,7 +7,7 @@ The design of the multi-agent system is represented by the following JSON schema
{workflow_schema}
\`\`\`
If the workflow has an 'Example Agent' as the main agent, it means the user is yet to create the main agent. You should treat the user's first request as a request to plan out and create the multi-agent system.
If the workflow has no agents or an empty startAgent, it means the user is yet to create their multi-agent system. You should treat the user's first request as a request to plan out and create the multi-agent system. When creating agents, you must always set a start agent.
---
`;

View file

@ -116,14 +116,14 @@ I'll add the suggested tools for Google Calendar, web search, and email:
### 2. Create Agents
#### a. Hub Agent (Meeting Assistant Hub)
I'll edit the Example Agent to become the hub agent:
I'll create the hub agent:
\`\`\`copilot_change
// action: edit
// action: create_new
// config_type: agent
// name: Example Agent
// name: Meeting Assistant Hub
{
"change_description": "Transformed Example Agent into the main hub agent orchestrating the meeting summary workflow.",
"change_description": "Created the main hub agent orchestrating the meeting summary workflow.",
"config_changes": {
"name": "Meeting Assistant Hub",
"type": "conversation",
@ -223,6 +223,20 @@ I'll edit the Example Agent to become the hub agent:
---
### 6. Set the Start Agent
\`\`\`copilot_change
// action: edit
// config_type: start_agent
// name: Meeting Assistant Hub
{
"change_description": "Set the Meeting Assistant Hub as the start agent for the workflow.",
"config_changes": {}
}
\`\`\`
---
Once you review and apply the changes, you can try out a basic chat first. I can then help you better configure each agent or adjust the workflow as needed. Would you like to customize any step or add more details?
---
@ -277,14 +291,14 @@ I'm adding the "Get document by id" tool to fetch the content of a Google Doc by
}
\`\`\`
I'm replacing the Example Agent with a user-facing agent that fetches a Google Doc by ID and answers questions based on its content:
I'm creating a user-facing agent that fetches a Google Doc by ID and answers questions based on its content:
\`\`\`copilot_change
// action: edit
// action: create_new
// config_type: agent
// name: Example Agent
// name: Google Doc QnA Assistant
{
"change_description": "Replaced Example Agent with a user-facing agent that fetches a Google Doc by ID and answers user questions based only on its content.",
"change_description": "Created a user-facing agent that fetches a Google Doc by ID and answers user questions based only on its content.",
"config_changes": {
"name": "Google Doc QnA Assistant",
"type": "conversation",
@ -297,6 +311,16 @@ I'm replacing the Example Agent with a user-facing agent that fetches a Google D
}
\`\`\`
\`\`\`copilot_change
// action: edit
// config_type: start_agent
// name: Google Doc QnA Assistant
{
"change_description": "Set the Google Doc QnA Assistant as the start agent for the workflow.",
"config_changes": {}
}
\`\`\`
Once you review and apply the changes, you can try out a basic chat by providing a Google Doc ID and a question. I can then help you further refine the assistant if needed.
---