mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-01-25 02:47:31 +00:00
Compare commits
3 Commits
main
...
fix/sessio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cd1b5811a | ||
|
|
ddb26c7652 | ||
|
|
b3c6e95971 |
@@ -603,7 +603,8 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|||||||
const transformedMessage = transformMessage(message);
|
const transformedMessage = transformMessage(message);
|
||||||
ws.send({
|
ws.send({
|
||||||
type: 'claude-response',
|
type: 'claude-response',
|
||||||
data: transformedMessage
|
data: transformedMessage,
|
||||||
|
sessionId: capturedSessionId || sessionId || null
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract and send token budget updates from result messages
|
// Extract and send token budget updates from result messages
|
||||||
@@ -613,7 +614,8 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|||||||
console.log('Token budget from modelUsage:', tokenBudget);
|
console.log('Token budget from modelUsage:', tokenBudget);
|
||||||
ws.send({
|
ws.send({
|
||||||
type: 'token-budget',
|
type: 'token-budget',
|
||||||
data: tokenBudget
|
data: tokenBudget,
|
||||||
|
sessionId: capturedSessionId || sessionId || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -651,7 +653,8 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|||||||
// Send error to WebSocket
|
// Send error to WebSocket
|
||||||
ws.send({
|
ws.send({
|
||||||
type: 'claude-error',
|
type: 'claude-error',
|
||||||
error: error.message
|
error: error.message,
|
||||||
|
sessionId: capturedSessionId || sessionId || null
|
||||||
});
|
});
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -114,7 +114,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
// Send system info to frontend
|
// Send system info to frontend
|
||||||
ws.send({
|
ws.send({
|
||||||
type: 'cursor-system',
|
type: 'cursor-system',
|
||||||
data: response
|
data: response,
|
||||||
|
sessionId: capturedSessionId || sessionId || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -123,7 +124,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
// Forward user message
|
// Forward user message
|
||||||
ws.send({
|
ws.send({
|
||||||
type: 'cursor-user',
|
type: 'cursor-user',
|
||||||
data: response
|
data: response,
|
||||||
|
sessionId: capturedSessionId || sessionId || null
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -142,7 +144,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
type: 'text_delta',
|
type: 'text_delta',
|
||||||
text: textContent
|
text: textContent
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
sessionId: capturedSessionId || sessionId || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -157,7 +160,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
type: 'claude-response',
|
type: 'claude-response',
|
||||||
data: {
|
data: {
|
||||||
type: 'content_block_stop'
|
type: 'content_block_stop'
|
||||||
}
|
},
|
||||||
|
sessionId: capturedSessionId || sessionId || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +178,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
// Forward any other message types
|
// Forward any other message types
|
||||||
ws.send({
|
ws.send({
|
||||||
type: 'cursor-response',
|
type: 'cursor-response',
|
||||||
data: response
|
data: response,
|
||||||
|
sessionId: capturedSessionId || sessionId || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
@@ -182,7 +187,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
// If not JSON, send as raw text
|
// If not JSON, send as raw text
|
||||||
ws.send({
|
ws.send({
|
||||||
type: 'cursor-output',
|
type: 'cursor-output',
|
||||||
data: line
|
data: line,
|
||||||
|
sessionId: capturedSessionId || sessionId || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +199,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
console.error('Cursor CLI stderr:', data.toString());
|
console.error('Cursor CLI stderr:', data.toString());
|
||||||
ws.send({
|
ws.send({
|
||||||
type: 'cursor-error',
|
type: 'cursor-error',
|
||||||
error: data.toString()
|
error: data.toString(),
|
||||||
|
sessionId: capturedSessionId || sessionId || null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -229,7 +236,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
|
|
||||||
ws.send({
|
ws.send({
|
||||||
type: 'cursor-error',
|
type: 'cursor-error',
|
||||||
error: error.message
|
error: error.message,
|
||||||
|
sessionId: capturedSessionId || sessionId || null
|
||||||
});
|
});
|
||||||
|
|
||||||
reject(error);
|
reject(error);
|
||||||
|
|||||||
@@ -272,7 +272,8 @@ export async function queryCodex(command, options = {}, ws) {
|
|||||||
data: {
|
data: {
|
||||||
used: totalTokens,
|
used: totalTokens,
|
||||||
total: 200000 // Default context window for Codex models
|
total: 200000 // Default context window for Codex models
|
||||||
}
|
},
|
||||||
|
sessionId: currentSessionId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1894,6 +1894,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
// Streaming throttle buffers
|
// Streaming throttle buffers
|
||||||
const streamBufferRef = useRef('');
|
const streamBufferRef = useRef('');
|
||||||
const streamTimerRef = useRef(null);
|
const streamTimerRef = useRef(null);
|
||||||
|
// Track the session that this view expects when starting a brand‑new chat
|
||||||
|
// (prevents background sessions from streaming into a different view).
|
||||||
|
const pendingViewSessionRef = useRef(null);
|
||||||
const commandQueryTimerRef = useRef(null);
|
const commandQueryTimerRef = useRef(null);
|
||||||
const [debouncedInput, setDebouncedInput] = useState('');
|
const [debouncedInput, setDebouncedInput] = useState('');
|
||||||
const [showFileDropdown, setShowFileDropdown] = useState(false);
|
const [showFileDropdown, setShowFileDropdown] = useState(false);
|
||||||
@@ -1930,6 +1933,14 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
// Track provider transitions so we only clear approvals when provider truly changes.
|
// Track provider transitions so we only clear approvals when provider truly changes.
|
||||||
// This does not sync with the backend; it just prevents UI prompts from disappearing.
|
// This does not sync with the backend; it just prevents UI prompts from disappearing.
|
||||||
const lastProviderRef = useRef(provider);
|
const lastProviderRef = useRef(provider);
|
||||||
|
|
||||||
|
const resetStreamingState = useCallback(() => {
|
||||||
|
if (streamTimerRef.current) {
|
||||||
|
clearTimeout(streamTimerRef.current);
|
||||||
|
streamTimerRef.current = null;
|
||||||
|
}
|
||||||
|
streamBufferRef.current = '';
|
||||||
|
}, []);
|
||||||
// Load permission mode for the current session
|
// Load permission mode for the current session
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedSession?.id) {
|
if (selectedSession?.id) {
|
||||||
@@ -3004,6 +3015,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
const sessionChanged = currentSessionId !== null && currentSessionId !== selectedSession.id;
|
const sessionChanged = currentSessionId !== null && currentSessionId !== selectedSession.id;
|
||||||
|
|
||||||
if (sessionChanged) {
|
if (sessionChanged) {
|
||||||
|
if (!isSystemSessionChange) {
|
||||||
|
// Clear any streaming leftovers from the previous session
|
||||||
|
resetStreamingState();
|
||||||
|
pendingViewSessionRef.current = null;
|
||||||
|
setChatMessages([]);
|
||||||
|
setSessionMessages([]);
|
||||||
|
setClaudeStatus(null);
|
||||||
|
setCanAbortSession(false);
|
||||||
|
}
|
||||||
// Reset pagination state when switching sessions
|
// Reset pagination state when switching sessions
|
||||||
setMessagesOffset(0);
|
setMessagesOffset(0);
|
||||||
setHasMoreMessages(false);
|
setHasMoreMessages(false);
|
||||||
@@ -3073,17 +3093,22 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Only clear messages if this is NOT a system-initiated session change AND we're not loading
|
// New session view (no selected session) - always reset UI state
|
||||||
// During system session changes or while loading, preserve the chat messages
|
if (!isSystemSessionChange) {
|
||||||
if (!isSystemSessionChange && !isLoading) {
|
resetStreamingState();
|
||||||
|
pendingViewSessionRef.current = null;
|
||||||
setChatMessages([]);
|
setChatMessages([]);
|
||||||
setSessionMessages([]);
|
setSessionMessages([]);
|
||||||
|
setClaudeStatus(null);
|
||||||
|
setCanAbortSession(false);
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
setCurrentSessionId(null);
|
setCurrentSessionId(null);
|
||||||
sessionStorage.removeItem('cursorSessionId');
|
sessionStorage.removeItem('cursorSessionId');
|
||||||
setMessagesOffset(0);
|
setMessagesOffset(0);
|
||||||
setHasMoreMessages(false);
|
setHasMoreMessages(false);
|
||||||
setTotalMessages(0);
|
setTotalMessages(0);
|
||||||
|
setTokenBudget(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark loading as complete after messages are set
|
// Mark loading as complete after messages are set
|
||||||
@@ -3094,7 +3119,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadMessages();
|
loadMessages();
|
||||||
}, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange]);
|
}, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange, resetStreamingState]);
|
||||||
|
|
||||||
// External Message Update Handler: Reload messages when external CLI modifies current session
|
// External Message Update Handler: Reload messages when external CLI modifies current session
|
||||||
// This triggers when App.jsx detects a JSONL file change for the currently-viewed session
|
// This triggers when App.jsx detects a JSONL file change for the currently-viewed session
|
||||||
@@ -3133,6 +3158,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
}
|
}
|
||||||
}, [externalMessageUpdate, selectedSession, selectedProject, loadCursorSessionMessages, loadSessionMessages, isNearBottom, autoScrollToBottom, scrollToBottom]);
|
}, [externalMessageUpdate, selectedSession, selectedProject, loadCursorSessionMessages, loadSessionMessages, isNearBottom, autoScrollToBottom, scrollToBottom]);
|
||||||
|
|
||||||
|
// When the user navigates to a specific session, clear any pending "new session" marker.
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedSession?.id) {
|
||||||
|
pendingViewSessionRef.current = null;
|
||||||
|
}
|
||||||
|
}, [selectedSession?.id]);
|
||||||
|
|
||||||
// Update chatMessages when convertedMessages changes
|
// Update chatMessages when convertedMessages changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessionMessages.length > 0) {
|
if (sessionMessages.length > 0) {
|
||||||
@@ -3197,17 +3229,77 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
// Handle WebSocket messages
|
// Handle WebSocket messages
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
const latestMessage = messages[messages.length - 1];
|
const latestMessage = messages[messages.length - 1];
|
||||||
|
const messageData = latestMessage.data?.message || latestMessage.data;
|
||||||
|
|
||||||
// Filter messages by session ID to prevent cross-session interference
|
// Filter messages by session ID to prevent cross-session interference
|
||||||
// Skip filtering for global messages that apply to all sessions
|
// Skip filtering for global messages that apply to all sessions
|
||||||
const globalMessageTypes = ['projects_updated', 'taskmaster-project-updated', 'session-created', 'claude-complete', 'codex-complete'];
|
const globalMessageTypes = ['projects_updated', 'taskmaster-project-updated', 'session-created'];
|
||||||
const isGlobalMessage = globalMessageTypes.includes(latestMessage.type);
|
const isGlobalMessage = globalMessageTypes.includes(latestMessage.type);
|
||||||
|
const lifecycleMessageTypes = new Set([
|
||||||
|
'claude-complete',
|
||||||
|
'codex-complete',
|
||||||
|
'cursor-result',
|
||||||
|
'session-aborted',
|
||||||
|
'claude-error',
|
||||||
|
'cursor-error',
|
||||||
|
'codex-error'
|
||||||
|
]);
|
||||||
|
|
||||||
// For new sessions (currentSessionId is null), allow messages through
|
const isClaudeSystemInit = latestMessage.type === 'claude-response' &&
|
||||||
if (!isGlobalMessage && latestMessage.sessionId && currentSessionId && latestMessage.sessionId !== currentSessionId) {
|
messageData &&
|
||||||
// Message is for a different session, ignore it
|
messageData.type === 'system' &&
|
||||||
console.log('⏭️ Skipping message for different session:', latestMessage.sessionId, 'current:', currentSessionId);
|
messageData.subtype === 'init';
|
||||||
return;
|
const isCursorSystemInit = latestMessage.type === 'cursor-system' &&
|
||||||
|
latestMessage.data &&
|
||||||
|
latestMessage.data.type === 'system' &&
|
||||||
|
latestMessage.data.subtype === 'init';
|
||||||
|
|
||||||
|
const systemInitSessionId = isClaudeSystemInit
|
||||||
|
? messageData?.session_id
|
||||||
|
: isCursorSystemInit
|
||||||
|
? latestMessage.data?.session_id
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const activeViewSessionId = selectedSession?.id || currentSessionId || pendingViewSessionRef.current?.sessionId || null;
|
||||||
|
const isSystemInitForView = systemInitSessionId && (!activeViewSessionId || systemInitSessionId === activeViewSessionId);
|
||||||
|
const shouldBypassSessionFilter = isGlobalMessage || isSystemInitForView;
|
||||||
|
const isUnscopedError = !latestMessage.sessionId &&
|
||||||
|
pendingViewSessionRef.current &&
|
||||||
|
!pendingViewSessionRef.current.sessionId &&
|
||||||
|
(latestMessage.type === 'claude-error' || latestMessage.type === 'cursor-error' || latestMessage.type === 'codex-error');
|
||||||
|
|
||||||
|
const handleBackgroundLifecycle = (sessionId) => {
|
||||||
|
if (!sessionId) return;
|
||||||
|
if (onSessionInactive) {
|
||||||
|
onSessionInactive(sessionId);
|
||||||
|
}
|
||||||
|
if (onSessionNotProcessing) {
|
||||||
|
onSessionNotProcessing(sessionId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!shouldBypassSessionFilter) {
|
||||||
|
if (!activeViewSessionId) {
|
||||||
|
// No session in view; ignore session-scoped traffic.
|
||||||
|
if (latestMessage.sessionId && lifecycleMessageTypes.has(latestMessage.type)) {
|
||||||
|
handleBackgroundLifecycle(latestMessage.sessionId);
|
||||||
|
}
|
||||||
|
if (!isUnscopedError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!latestMessage.sessionId && !isUnscopedError) {
|
||||||
|
// Drop unscoped messages to prevent cross-session bleed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (latestMessage.sessionId !== activeViewSessionId) {
|
||||||
|
if (latestMessage.sessionId && lifecycleMessageTypes.has(latestMessage.type)) {
|
||||||
|
handleBackgroundLifecycle(latestMessage.sessionId);
|
||||||
|
}
|
||||||
|
// Message is for a different session, ignore it
|
||||||
|
console.log('??-?,? Skipping message for different session:', latestMessage.sessionId, 'current:', activeViewSessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (latestMessage.type) {
|
switch (latestMessage.type) {
|
||||||
@@ -3216,6 +3308,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
// Store it temporarily until conversation completes (prevents premature session association)
|
// Store it temporarily until conversation completes (prevents premature session association)
|
||||||
if (latestMessage.sessionId && !currentSessionId) {
|
if (latestMessage.sessionId && !currentSessionId) {
|
||||||
sessionStorage.setItem('pendingSessionId', latestMessage.sessionId);
|
sessionStorage.setItem('pendingSessionId', latestMessage.sessionId);
|
||||||
|
if (pendingViewSessionRef.current && !pendingViewSessionRef.current.sessionId) {
|
||||||
|
pendingViewSessionRef.current.sessionId = latestMessage.sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
// Mark as system change to prevent clearing messages when session ID updates
|
// Mark as system change to prevent clearing messages when session ID updates
|
||||||
setIsSystemSessionChange(true);
|
setIsSystemSessionChange(true);
|
||||||
@@ -3244,7 +3339,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'claude-response':
|
case 'claude-response':
|
||||||
const messageData = latestMessage.data.message || latestMessage.data;
|
|
||||||
|
|
||||||
// Handle Cursor streaming format (content_block_delta / content_block_stop)
|
// Handle Cursor streaming format (content_block_delta / content_block_stop)
|
||||||
if (messageData && typeof messageData === 'object' && messageData.type) {
|
if (messageData && typeof messageData === 'object' && messageData.type) {
|
||||||
@@ -3313,7 +3407,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
latestMessage.data.subtype === 'init' &&
|
latestMessage.data.subtype === 'init' &&
|
||||||
latestMessage.data.session_id &&
|
latestMessage.data.session_id &&
|
||||||
currentSessionId &&
|
currentSessionId &&
|
||||||
latestMessage.data.session_id !== currentSessionId) {
|
latestMessage.data.session_id !== currentSessionId &&
|
||||||
|
isSystemInitForView) {
|
||||||
|
|
||||||
console.log('🔄 Claude CLI session duplication detected:', {
|
console.log('🔄 Claude CLI session duplication detected:', {
|
||||||
originalSession: currentSessionId,
|
originalSession: currentSessionId,
|
||||||
@@ -3336,7 +3431,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
if (latestMessage.data.type === 'system' &&
|
if (latestMessage.data.type === 'system' &&
|
||||||
latestMessage.data.subtype === 'init' &&
|
latestMessage.data.subtype === 'init' &&
|
||||||
latestMessage.data.session_id &&
|
latestMessage.data.session_id &&
|
||||||
!currentSessionId) {
|
!currentSessionId &&
|
||||||
|
isSystemInitForView) {
|
||||||
|
|
||||||
console.log('🔄 New session init detected:', {
|
console.log('🔄 New session init detected:', {
|
||||||
newSession: latestMessage.data.session_id
|
newSession: latestMessage.data.session_id
|
||||||
@@ -3357,7 +3453,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
latestMessage.data.subtype === 'init' &&
|
latestMessage.data.subtype === 'init' &&
|
||||||
latestMessage.data.session_id &&
|
latestMessage.data.session_id &&
|
||||||
currentSessionId &&
|
currentSessionId &&
|
||||||
latestMessage.data.session_id === currentSessionId) {
|
latestMessage.data.session_id === currentSessionId &&
|
||||||
|
isSystemInitForView) {
|
||||||
console.log('🔄 System init message for current session, ignoring');
|
console.log('🔄 System init message for current session, ignoring');
|
||||||
return; // Don't process the message further
|
return; // Don't process the message further
|
||||||
}
|
}
|
||||||
@@ -3525,6 +3622,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
try {
|
try {
|
||||||
const cdata = latestMessage.data;
|
const cdata = latestMessage.data;
|
||||||
if (cdata && cdata.type === 'system' && cdata.subtype === 'init' && cdata.session_id) {
|
if (cdata && cdata.type === 'system' && cdata.subtype === 'init' && cdata.session_id) {
|
||||||
|
if (!isSystemInitForView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// If we already have a session and this differs, switch (duplication/redirect)
|
// If we already have a session and this differs, switch (duplication/redirect)
|
||||||
if (currentSessionId && cdata.session_id !== currentSessionId) {
|
if (currentSessionId && cdata.session_id !== currentSessionId) {
|
||||||
console.log('🔄 Cursor session switch detected:', { originalSession: currentSessionId, newSession: cdata.session_id });
|
console.log('🔄 Cursor session switch detected:', { originalSession: currentSessionId, newSession: cdata.session_id });
|
||||||
@@ -4316,6 +4416,11 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
// Session Protection: Mark session as active to prevent automatic project updates during conversation
|
// Session Protection: Mark session as active to prevent automatic project updates during conversation
|
||||||
// Use existing session if available; otherwise a temporary placeholder until backend provides real ID
|
// Use existing session if available; otherwise a temporary placeholder until backend provides real ID
|
||||||
const sessionToActivate = effectiveSessionId || `new-session-${Date.now()}`;
|
const sessionToActivate = effectiveSessionId || `new-session-${Date.now()}`;
|
||||||
|
if (!effectiveSessionId && !selectedSession?.id) {
|
||||||
|
// We are starting a brand-new session in this view. Track it so we only
|
||||||
|
// accept streaming updates for this run.
|
||||||
|
pendingViewSessionRef.current = { sessionId: null, startedAt: Date.now() };
|
||||||
|
}
|
||||||
if (onSessionActive) {
|
if (onSessionActive) {
|
||||||
onSessionActive(sessionToActivate);
|
onSessionActive(sessionToActivate);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user