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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useEffect, useState } from 'react';
import { useSession } from './useSession';
import { apiService } from '../services/apiService';

/**
* Hook to check if the current session's workspace differs from the server's current workspace
* Returns warning state when there's a mismatch, indicating the session is in preview-only mode
*/
export function useWorkspaceEnvironmentCheck() {
const { activeSessionId, sessions } = useSession();
const [isWorkspaceMismatch, setIsWorkspaceMismatch] = useState(false);
const [sessionWorkspace, setSessionWorkspace] = useState<string>('');
const [serverWorkspace, setServerWorkspace] = useState<string>('');
const [loading, setLoading] = useState(false);

useEffect(() => {
if (!activeSessionId) {
setIsWorkspaceMismatch(false);
setSessionWorkspace('');
setServerWorkspace('');
return;
}

const checkWorkspaceEnvironment = async () => {
try {
setLoading(true);

// Get current session's workspace
const currentSession = sessions.find(session => session.id === activeSessionId);
if (!currentSession) {
setIsWorkspaceMismatch(false);
return;
}

const sessionWorkspacePath = currentSession.workspace;

// Get server's current workspace
const serverWorkspaceInfo = await apiService.getWorkspaceInfo();
const serverWorkspacePath = serverWorkspaceInfo.path;

setSessionWorkspace(sessionWorkspacePath);
setServerWorkspace(serverWorkspacePath);

// Check if workspaces match
const mismatch = sessionWorkspacePath !== serverWorkspacePath;
setIsWorkspaceMismatch(mismatch);

if (mismatch) {
console.warn('Workspace environment mismatch detected:', {
sessionWorkspace: sessionWorkspacePath,
serverWorkspace: serverWorkspacePath,
});
}
} catch (error) {
console.error('Failed to check workspace environment:', error);
// On error, assume no mismatch to avoid false positives
setIsWorkspaceMismatch(false);
} finally {
setLoading(false);
}
};

checkWorkspaceEnvironment();
}, [activeSessionId, sessions]);

return {
isWorkspaceMismatch,
sessionWorkspace,
serverWorkspace,
loading,
};
}
13 changes: 12 additions & 1 deletion multimodal/tarko/agent-ui/src/standalone/chat/ChatPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useRef } from 'react';
import { useParams } from 'react-router-dom';
import { useSession } from '@/common/hooks/useSession';
import { useWorkspaceEnvironmentCheck } from '@/common/hooks/useWorkspaceEnvironmentCheck';
import { MessageGroup } from './Message/components/MessageGroup';
import { ChatInput } from './MessageInput';
import { useAtomValue } from 'jotai';
Expand All @@ -12,13 +13,15 @@ import { ScrollToBottomButton } from './components/ScrollToBottomButton';
import { EmptyState } from './components/EmptyState';
import { OfflineBanner } from './components/OfflineBanner';
import { SessionCreatingState } from './components/SessionCreatingState';
import { WorkspaceEnvironmentWarning } from './components/WorkspaceEnvironmentWarning';

import './ChatPanel.css';

export const ChatPanel: React.FC = () => {
const { sessionId: urlSessionId } = useParams<{ sessionId: string }>();
const { activeSessionId, isProcessing, connectionStatus, checkServerStatus, sendMessage } =
useSession();
const { isWorkspaceMismatch, sessionWorkspace, serverWorkspace } = useWorkspaceEnvironmentCheck();

const currentSessionId = urlSessionId || activeSessionId;
const groupedMessages = useAtomValue(groupedMessagesAtom);
Expand Down Expand Up @@ -60,6 +63,12 @@ export const ChatPanel: React.FC = () => {
onReconnect={checkServerStatus}
/>

<WorkspaceEnvironmentWarning
isVisible={isWorkspaceMismatch && !isReplayMode}
sessionWorkspace={sessionWorkspace}
serverWorkspace={serverWorkspace}
/>

{showEmptyState ? (
<EmptyState replayState={replayState} isReplayMode={isReplayMode} />
) : (
Expand Down Expand Up @@ -90,7 +99,8 @@ export const ChatPanel: React.FC = () => {
currentSessionId === 'creating' ||
isProcessing ||
!connectionStatus.connected ||
isReplayMode
isReplayMode ||
isWorkspaceMismatch
}
isProcessing={isProcessing}
connectionStatus={connectionStatus}
Expand All @@ -100,6 +110,7 @@ export const ChatPanel: React.FC = () => {
showContextualSelector={true}
autoFocus={false}
showHelpText={true}
isWorkspaceMismatch={isWorkspaceMismatch}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface ChatInputProps {
autoFocus?: boolean;
showHelpText?: boolean;
variant?: 'default' | 'home';
isWorkspaceMismatch?: boolean;
}

export const ChatInput: React.FC<ChatInputProps> = ({
Expand All @@ -58,6 +59,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
autoFocus = true,
showHelpText = true,
variant = 'default',
isWorkspaceMismatch = false,
}) => {
const [uploadedImages, setUploadedImages] = useState<ChatCompletionContentPart[]>([]);
const [isAborting, setIsAborting] = useState(false);
Expand Down Expand Up @@ -379,11 +381,13 @@ export const ChatInput: React.FC<ChatInputProps> = ({
const defaultPlaceholder =
connectionStatus && !connectionStatus.connected
? 'Server disconnected...'
: isProcessing
? `${getAgentTitle()} is running...`
: contextualSelectorEnabled
? `Ask ${getAgentTitle()} something... (Use @ to reference files/folders, Ctrl+Enter to send)`
: `Ask ${getAgentTitle()} something... (Ctrl+Enter to send)`;
: isWorkspaceMismatch
? 'Preview mode - workspace environment mismatch'
: isProcessing
? `${getAgentTitle()} is running...`
: contextualSelectorEnabled
? `Ask ${getAgentTitle()} something... (Use @ to reference files/folders, Ctrl+Enter to send)`
: `Ask ${getAgentTitle()} something... (Ctrl+Enter to send)`;

return (
<div className={`relative ${className}`}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from 'react';
import { FiAlertTriangle, FiEye } from 'react-icons/fi';
import { motion, AnimatePresence } from 'framer-motion';

interface WorkspaceEnvironmentWarningProps {
isVisible: boolean;
sessionWorkspace: string;
serverWorkspace: string;
}

/**
* Warning banner component for workspace environment mismatch
* Displays when session workspace differs from server workspace
*/
export const WorkspaceEnvironmentWarning: React.FC<WorkspaceEnvironmentWarningProps> = ({
isVisible,
sessionWorkspace,
serverWorkspace,
}) => {
if (!isVisible) return null;

const getWorkspaceName = (path: string) => {
if (!path) return 'Unknown';
return path.split('/').pop() || path;
};

return (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
className="mx-4 mb-4 overflow-hidden"
>
<div className="bg-gradient-to-r from-amber-50 via-orange-50 to-amber-50 dark:from-amber-900/20 dark:via-orange-900/20 dark:to-amber-900/20 border border-amber-200 dark:border-amber-700/50 rounded-2xl p-4 shadow-sm">
<div className="flex items-start gap-3">
{/* Warning icon */}
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-amber-100 dark:bg-amber-800/30 flex items-center justify-center mt-0.5">
<FiAlertTriangle size={16} className="text-amber-600 dark:text-amber-400" />
</div>

{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<FiEye size={16} className="text-amber-600 dark:text-amber-400 flex-shrink-0" />
<h3 className="text-sm font-semibold text-amber-800 dark:text-amber-200">
Preview Mode - Environment Mismatch
</h3>
</div>

<p className="text-sm text-amber-700 dark:text-amber-300 leading-relaxed mb-3">
This session was created in a different workspace environment.
You can view the conversation history, but cannot send new messages.
</p>

{/* Workspace comparison */}
<div className="space-y-2 text-xs">
<div className="flex items-center gap-2">
<span className="text-amber-600 dark:text-amber-400 font-medium">Session:</span>
<code className="px-2 py-1 bg-amber-100 dark:bg-amber-800/40 text-amber-800 dark:text-amber-200 rounded font-mono">
{getWorkspaceName(sessionWorkspace)}
</code>
</div>
<div className="flex items-center gap-2">
<span className="text-amber-600 dark:text-amber-400 font-medium">Current:</span>
<code className="px-2 py-1 bg-amber-100 dark:bg-amber-800/40 text-amber-800 dark:text-amber-200 rounded font-mono">
{getWorkspaceName(serverWorkspace)}
</code>
</div>
</div>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
};
Loading