Merge pull request 'chore: bring upstream v1.20.0 into custom' (#2) from release/v1.20.0 into custom
This commit is contained in:
@@ -34,6 +34,9 @@ import {
|
||||
type ChatDraftSnapshot,
|
||||
} from '@/lib/chatDraftPersistence';
|
||||
import { ReviewFlowDialog, type ReviewFlowExecution } from '@/components/session/ReviewFlowDialog';
|
||||
import { BtwPanel } from './btw/BtwPanel';
|
||||
import { useBtwPanelState } from './btw/useBtwPanelState';
|
||||
import { destroyBtwSession, startBtwSession, type BtwSessionRef } from '@/lib/btw';
|
||||
import { AttachedFilesList, AttachedVSCodeFileChips, ActiveEditorFileSuggestion } from './FileAttachment';
|
||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||
import type { ToolPopupContent } from './message/types';
|
||||
@@ -51,7 +54,7 @@ import { PendingChangesBar } from './PendingChangesBar';
|
||||
import { useChatSurfaceMode } from './useChatSurfaceMode';
|
||||
import { MobileAgentButton } from './MobileAgentButton';
|
||||
import { MobileModelButton } from './MobileModelButton';
|
||||
import { useCurrentSessionActivity } from '@/hooks/useSessionActivity';
|
||||
import { useCurrentSessionActivity, useSessionActivity } from '@/hooks/useSessionActivity';
|
||||
import { toast } from '@/components/ui';
|
||||
// useMessageStore removed — messages now come from sync system
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
@@ -229,6 +232,7 @@ interface ChatInputProps {
|
||||
onOpenSettings?: () => void;
|
||||
scrollToBottom?: () => void;
|
||||
active?: boolean;
|
||||
draftPresentationExiting?: boolean;
|
||||
}
|
||||
|
||||
const resolveChatDraftIdentity = (sessionId: string | null): ChatDraftIdentity | null => {
|
||||
@@ -242,7 +246,12 @@ const resolveChatDraftIdentity = (sessionId: string | null): ChatDraftIdentity |
|
||||
return createChatDraftIdentity(getRuntimeKey(), directory, sessionId);
|
||||
};
|
||||
|
||||
const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBottom, active = true }) => {
|
||||
const ChatInputComponent: React.FC<ChatInputProps> = ({
|
||||
onOpenSettings,
|
||||
scrollToBottom,
|
||||
active = true,
|
||||
draftPresentationExiting = false,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
// Track if we restored a draft on mount (for text selection)
|
||||
const initialDraftRef = React.useRef<string | null>(null);
|
||||
@@ -314,6 +323,20 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const currentSessionDirectoryForSync = useSessionUIStore(
|
||||
React.useCallback((s) => currentSessionId ? s.getDirectoryForSession(currentSessionId) : null, [currentSessionId]),
|
||||
);
|
||||
// btw mode: the CURRENT session's metadata links an active btw fork and
|
||||
// the panel is expanded, so this composer's sends route to the fork
|
||||
// instead of the main session. Collapsed keeps the fork alive (chip stays
|
||||
// visible) while the composer talks to the main session again.
|
||||
const btwPanel = useBtwPanelState(currentSessionId, currentSessionDirectoryForSync ?? currentDirectory ?? undefined);
|
||||
const btwSessionId = btwPanel.btwSessionId;
|
||||
const btwDirectory = btwPanel.btwDirectory;
|
||||
const btwSessionRef = React.useMemo<BtwSessionRef | null>(
|
||||
() => (currentSessionId && btwSessionId && btwDirectory
|
||||
? { parentSessionId: currentSessionId, btwSessionId, directory: btwDirectory }
|
||||
: null),
|
||||
[btwDirectory, btwSessionId, currentSessionId],
|
||||
);
|
||||
const isBtwActive = Boolean(btwSessionRef) && !btwPanel.collapsed;
|
||||
const activeRuntimeKey = getRuntimeKey();
|
||||
const chatDraftIdentity = React.useMemo(
|
||||
() => createChatDraftIdentity(
|
||||
@@ -331,6 +354,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const setNewSessionDraftTarget = useSessionUIStore((s) => s.setNewSessionDraftTarget);
|
||||
const setDraftPermissionAutoAcceptEnabled = useSessionUIStore((s) => s.setDraftPermissionAutoAcceptEnabled);
|
||||
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
||||
const prepareChatDraftDirectory = useSessionUIStore((s) => s.prepareChatDraftDirectory);
|
||||
const abortPromptSessionId = useSessionUIStore((s) => s.abortPromptSessionId);
|
||||
const clearAbortPrompt = useSessionUIStore((s) => s.clearAbortPrompt);
|
||||
const attachedFiles = useInputStore((s) => s.attachedFiles);
|
||||
@@ -341,6 +365,11 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const pendingPresetSubmit = useInputStore((s) => s.pendingPresetSubmit);
|
||||
const setPendingInputText = useInputStore((s) => s.setPendingInputText);
|
||||
const pendingInputText = useInputStore((s) => s.pendingInputText);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!newSessionDraftOpen || newSessionDraft.target !== 'chat' || message.trim().length === 0) return;
|
||||
void prepareChatDraftDirectory();
|
||||
}, [message, newSessionDraft.target, newSessionDraftOpen, prepareChatDraftDirectory]);
|
||||
const consumePendingSyntheticParts = useInputStore((s) => s.consumePendingSyntheticParts);
|
||||
const acknowledgeSessionAbort = useSessionUIStore((s) => s.acknowledgeSessionAbort);
|
||||
const abortCurrentOperation = React.useCallback(
|
||||
@@ -557,7 +586,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const availableSkills = useSkillsStore((s) => s.skills);
|
||||
const knownSlashNames = React.useMemo(() => {
|
||||
const names = new Set<string>([
|
||||
'init', 'review', 'undo', 'redo', 'timeline', 'compact', 'summary', 'workspace-review', 'plan-feature', 'craft-goal', 'schedule-task', 'catch-up', 'debug', 'weigh', 'explore',
|
||||
'init', 'review', 'undo', 'redo', 'timeline', 'compact', 'btw', 'summary', 'workspace-review', 'plan-feature', 'craft-goal', 'schedule-task', 'catch-up', 'debug', 'weigh', 'explore',
|
||||
]);
|
||||
if (!isMobile && !isVSCodeRuntime()) names.add('handoff-review');
|
||||
for (const command of availableCommands) names.add(command.name.toLowerCase());
|
||||
@@ -832,8 +861,13 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
prevNewSessionDraftOpenRef.current = newSessionDraftOpen;
|
||||
}, [newSessionDraftOpen, isMobile]);
|
||||
|
||||
// Session activity for queue availability and controls
|
||||
const { phase: sessionPhase } = useCurrentSessionActivity();
|
||||
// Session activity for queue availability and controls. In btw mode the
|
||||
// composer controls the temporary fork, so the stop button and send-button
|
||||
// state follow the FORK's activity; the queue affordance stays tied to the
|
||||
// main session (queued messages always belong to the main chat).
|
||||
const { phase: currentSessionPhase } = useCurrentSessionActivity();
|
||||
const { phase: btwSessionPhase } = useSessionActivity(btwSessionId, btwDirectory ?? undefined);
|
||||
const sessionPhase = isBtwActive ? btwSessionPhase : currentSessionPhase;
|
||||
const autoReviewRunning = useAutoReviewStore(React.useCallback((state) => {
|
||||
if (!currentSessionId) return false;
|
||||
const run = state.runsByOriginalSessionID[currentSessionId];
|
||||
@@ -1043,12 +1077,15 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
// queued-message auto-send hook delivers it as the next turn once the
|
||||
// rejected turn winds down and the session returns to idle. This avoids
|
||||
// aborting the turn (which would surface an "aborted" notice).
|
||||
if (currentSessionId && !queuedOnly && autoReviewRunning) {
|
||||
if (currentSessionId && !queuedOnly && autoReviewRunning && !isBtwActive) {
|
||||
handleQueueMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSessionId && !queuedOnly) {
|
||||
// btw mode: the child fork's blocking prompts are answered inside the
|
||||
// panel; the composer send goes straight to the fork (routeMessage
|
||||
// queues if the fork's own turn is busy).
|
||||
if (currentSessionId && !queuedOnly && !isBtwActive) {
|
||||
// Sending is authoritative for blocking prompts: deny pending
|
||||
// permissions and dismiss open questions for the session subtree,
|
||||
// then queue the message once if either was open. The deny/clear
|
||||
@@ -1068,17 +1105,24 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessageOptions: {
|
||||
let sendMessageOptions: {
|
||||
target?: NonNullable<typeof capturedTarget>;
|
||||
sessionId?: string;
|
||||
directory?: string;
|
||||
draftSnapshot?: NonNullable<typeof capturedDraftSnapshot>;
|
||||
delivery?: 'steer';
|
||||
} | undefined = (capturedTarget || capturedDraftSnapshot || delivery)
|
||||
? {
|
||||
...(capturedTarget ? { target: capturedTarget } : {}),
|
||||
...(capturedDraftSnapshot ? { draftSnapshot: capturedDraftSnapshot } : {}),
|
||||
...(delivery ? { delivery } : {}),
|
||||
}
|
||||
: undefined;
|
||||
} | undefined;
|
||||
if (isBtwActive && btwSessionId && btwDirectory) {
|
||||
sendMessageOptions = {
|
||||
sessionId: btwSessionId,
|
||||
directory: btwDirectory,
|
||||
};
|
||||
} else if (capturedTarget || capturedDraftSnapshot || delivery) {
|
||||
sendMessageOptions = {};
|
||||
if (capturedTarget) sendMessageOptions.target = capturedTarget;
|
||||
if (capturedDraftSnapshot) sendMessageOptions.draftSnapshot = capturedDraftSnapshot;
|
||||
}
|
||||
if (delivery && sendMessageOptions) sendMessageOptions.delivery = delivery;
|
||||
|
||||
const preparedDocumentMentions = new Map<string, AttachedFile[]>();
|
||||
const reservedFilenames = new Set([
|
||||
@@ -1219,6 +1263,40 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (commandName === 'btw' && currentSessionId) {
|
||||
const question = argument.trim();
|
||||
if (!question) {
|
||||
toast.error(t('chat.btw.toast.emptyArgument'));
|
||||
return;
|
||||
}
|
||||
const targetDirectory = useSessionUIStore.getState().getDirectoryForSession(currentSessionId)
|
||||
|| currentDirectory
|
||||
|| null;
|
||||
if (!targetDirectory) {
|
||||
toast.error(t('chat.btw.toast.createFailed'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// A new btw replaces this session's current one: destroy
|
||||
// the previous fork first so forks never accumulate.
|
||||
if (btwSessionRef) {
|
||||
await destroyBtwSession(btwSessionRef);
|
||||
}
|
||||
await startBtwSession({
|
||||
parentSessionId: currentSessionId,
|
||||
question,
|
||||
directory: targetDirectory,
|
||||
providerID: providerIdToSend,
|
||||
modelID: modelIdToSend,
|
||||
agent: agentNameToSend,
|
||||
variant: variantToSend,
|
||||
});
|
||||
scrollToBottom?.();
|
||||
} catch (error) {
|
||||
toast.error(getSubmitErrorMessage(error, t('chat.btw.toast.createFailed')));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// The rest render a visible prompt plus synthetic instructions and
|
||||
// send them as one message.
|
||||
@@ -1254,7 +1332,9 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
}
|
||||
|
||||
const currentSessionDirectory = capturedTarget?.directory ?? currentDirectory;
|
||||
const shouldAddResponseStyle = newSessionDraftOpen || (currentSessionId ? !hasUserMessages(currentSessionId, currentSessionDirectory) : false);
|
||||
// btw mode: the fork already carries the question plus full history,
|
||||
// so the response-style instruction never applies there.
|
||||
const shouldAddResponseStyle = !isBtwActive && (newSessionDraftOpen || (currentSessionId ? !hasUserMessages(currentSessionId, currentSessionDirectory) : false));
|
||||
if (shouldAddResponseStyle) {
|
||||
const responseStyleInstruction = await fetchResponseStyleInstruction().catch(() => null);
|
||||
if (responseStyleInstruction) {
|
||||
@@ -1424,7 +1504,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
// Primary action for send/queue button — respects selected follow-up behavior
|
||||
const handlePrimaryAction = React.useCallback(() => {
|
||||
const inputSnapshot = getCurrentInputSnapshot();
|
||||
const canQueue = inputMode === 'normal' && inputSnapshot.hasContent && currentSessionId && (sessionPhase !== 'idle' || autoReviewRunning);
|
||||
const canQueue = !isBtwActive && inputMode === 'normal' && inputSnapshot.hasContent && currentSessionId && (currentSessionPhase !== 'idle' || autoReviewRunning);
|
||||
if (followUpBehavior === 'queue' && canQueue) {
|
||||
handleQueueMessage();
|
||||
} else if (followUpBehavior === 'steer' && canQueue) {
|
||||
@@ -1432,7 +1512,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
} else {
|
||||
void handleSubmitRef.current();
|
||||
}
|
||||
}, [inputMode, getCurrentInputSnapshot, currentSessionId, sessionPhase, autoReviewRunning, followUpBehavior, handleQueueMessage]);
|
||||
}, [inputMode, getCurrentInputSnapshot, currentSessionId, currentSessionPhase, autoReviewRunning, followUpBehavior, handleQueueMessage, isBtwActive]);
|
||||
|
||||
// Draft welcome presets: submit immediately.
|
||||
const submitPresetPrompt = React.useCallback((text: string, type: 'command' | 'skill') => {
|
||||
@@ -1636,15 +1716,19 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Enter/Ctrl+Enter based on selected follow-up behavior.
|
||||
if (e.key === 'Enter' && !e.shiftKey && (!isMobile || e.ctrlKey || e.metaKey)) {
|
||||
// Handle Enter/Ctrl+Enter based on selected follow-up behavior. On
|
||||
// mobile, and in desktop focus mode, plain Enter writes a newline and
|
||||
// only Cmd/Ctrl+Enter sends: both are surfaces for composing long
|
||||
// prompts, where an accidental send costs more than an extra keypress.
|
||||
const requiresModifierToSend = isMobile || isDesktopExpanded;
|
||||
if (e.key === 'Enter' && !e.shiftKey && (!requiresModifierToSend || e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
|
||||
const isCtrlEnter = e.ctrlKey || e.metaKey;
|
||||
|
||||
// Queueing / steering only works when there's an existing busy
|
||||
// session (or an active auto-review run).
|
||||
const canQueue = inputMode === 'normal' && hasContent && currentSessionId && (sessionPhase !== 'idle' || autoReviewRunning);
|
||||
const canQueue = !isBtwActive && inputMode === 'normal' && hasContent && currentSessionId && (currentSessionPhase !== 'idle' || autoReviewRunning);
|
||||
|
||||
if (followUpBehavior === 'queue') {
|
||||
if (isCtrlEnter || !canQueue) {
|
||||
@@ -1694,8 +1778,11 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
clearAbortPrompt();
|
||||
startAbortIndicator();
|
||||
|
||||
void abortCurrentOperation(currentSessionId || undefined);
|
||||
}, [abortCurrentOperation, clearAbortPrompt, currentSessionId, startAbortIndicator]);
|
||||
// btw mode: the stop button stops the fork's turn, not the main
|
||||
// session's.
|
||||
const abortTarget = isBtwActive && btwSessionId ? btwSessionId : currentSessionId;
|
||||
void abortCurrentOperation(abortTarget || undefined);
|
||||
}, [abortCurrentOperation, btwSessionId, clearAbortPrompt, currentSessionId, isBtwActive, startAbortIndicator]);
|
||||
|
||||
const handleCycleAgent = React.useCallback((direction: 1 | -1 = 1) => {
|
||||
const nextAgentName = getCycledPrimaryAgentName(agents, currentAgentName, direction);
|
||||
@@ -2392,6 +2479,15 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
|
||||
const chatSurfaceMode = useChatSurfaceMode();
|
||||
const isMiniChatSurface = chatSurfaceMode === 'mini-chat';
|
||||
const showDesktopDraftPresentation = (newSessionDraftOpen || draftPresentationExiting)
|
||||
&& !isDesktopExpanded
|
||||
&& !isMobile
|
||||
&& !isVSCode
|
||||
&& !isMiniChatSurface;
|
||||
const draftPresentationClassName = cn(
|
||||
'transition-opacity duration-[120ms] ease-out motion-reduce:transition-none',
|
||||
draftPresentationExiting && 'pointer-events-none opacity-0',
|
||||
);
|
||||
|
||||
const hasPendingChanges = React.useMemo(() => {
|
||||
if (isMiniChatSurface) {
|
||||
@@ -2405,7 +2501,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!showDraftTargetSelectors || !selectedDraftProject || !selectedDraftDirectory) {
|
||||
if (!showDraftTargetSelectors || !selectedDraftProject || selectedDraftProject.kind === 'chat' || !selectedDraftDirectory) {
|
||||
return;
|
||||
}
|
||||
if (newSessionDraft?.pendingWorktreeRequestId || newSessionDraft?.bootstrapPendingDirectory || newSessionDraft?.preserveDirectoryOverride) {
|
||||
@@ -2569,8 +2665,8 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
)}
|
||||
style={isMobile && inputBarOffset > 0 ? { marginBottom: `${inputBarOffset}px` } : undefined}
|
||||
>
|
||||
{newSessionDraftOpen && !isDesktopExpanded && !isMobile && !isVSCode && !isMiniChatSurface ? (
|
||||
<div className="chat-input-column mb-7 text-center">
|
||||
{showDesktopDraftPresentation ? (
|
||||
<div className={cn('chat-input-column mb-7 text-center', draftPresentationClassName)}>
|
||||
<h1 className="text-balance text-2xl font-normal tracking-tight text-foreground md:text-3xl">
|
||||
{renderDraftTitle(
|
||||
draftProjectLabel
|
||||
@@ -2645,21 +2741,23 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
? null
|
||||
: <PendingChangesBar />}
|
||||
/>
|
||||
{!isMobile && showDraftTargetSelectors && selectedDraftProject ? (
|
||||
<DraftTargetSelectors
|
||||
projects={draftProjects}
|
||||
selectedProject={selectedDraftProject}
|
||||
selectedDirectory={selectedDraftDirectory}
|
||||
selectedBranchLabel={selectedDraftBranchLabel}
|
||||
selectedBranchIsKnown={selectedDraftBranchIsKnown}
|
||||
projectRootBranchOption={projectRootBranchOption}
|
||||
worktreeBranchOptions={worktreeBranchOptions}
|
||||
branchItems={draftBranchItems}
|
||||
showBranchSelector={shouldShowDraftBranchSelector}
|
||||
onProjectChange={handleDraftProjectChange}
|
||||
onDirectoryChange={handleDraftDirectoryChange}
|
||||
theme={currentTheme}
|
||||
/>
|
||||
{!isMobile && (showDraftTargetSelectors || draftPresentationExiting) && selectedDraftProject ? (
|
||||
<div className={draftPresentationClassName}>
|
||||
<DraftTargetSelectors
|
||||
projects={draftProjects}
|
||||
selectedProject={selectedDraftProject}
|
||||
selectedDirectory={selectedDraftDirectory}
|
||||
selectedBranchLabel={selectedDraftBranchLabel}
|
||||
selectedBranchIsKnown={selectedDraftBranchIsKnown}
|
||||
projectRootBranchOption={projectRootBranchOption}
|
||||
worktreeBranchOptions={worktreeBranchOptions}
|
||||
branchItems={draftBranchItems}
|
||||
showBranchSelector={shouldShowDraftBranchSelector}
|
||||
onProjectChange={handleDraftProjectChange}
|
||||
onDirectoryChange={handleDraftDirectoryChange}
|
||||
theme={currentTheme}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{isMobile && showDraftTargetSelectors && selectedDraftProject ? (
|
||||
<MobileDraftTargetTriggers
|
||||
@@ -2826,11 +2924,13 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
}}
|
||||
onFocus={mobileShell.onEditorFocus}
|
||||
onBlur={mobileShell.onEditorBlur}
|
||||
placeholder={currentSessionId || newSessionDraftOpen
|
||||
? inputMode === 'shell'
|
||||
? t('chat.chatInput.placeholder.shell')
|
||||
: t(useCompactChatPlaceholder ? 'chat.chatInput.placeholder.chatCompact' : 'chat.chatInput.placeholder.chat')
|
||||
: t('chat.chatInput.placeholder.selectSession')}
|
||||
placeholder={isBtwActive
|
||||
? t('chat.btw.mainComposerPlaceholder')
|
||||
: currentSessionId || newSessionDraftOpen
|
||||
? inputMode === 'shell'
|
||||
? t('chat.chatInput.placeholder.shell')
|
||||
: t(useCompactChatPlaceholder ? 'chat.chatInput.placeholder.chatCompact' : 'chat.chatInput.placeholder.chat')
|
||||
: t('chat.chatInput.placeholder.selectSession')}
|
||||
editable={Boolean(currentSessionId || newSessionDraftOpen)}
|
||||
autoCorrect={isMobile}
|
||||
autoCapitalize={isMobile ? 'sentences' : 'none'}
|
||||
@@ -2923,12 +3023,13 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{newSessionDraftOpen && !isDesktopExpanded && !isMobile && !isVSCode && !isMiniChatSurface ? (
|
||||
{showDesktopDraftPresentation ? (
|
||||
<DraftPresetChips
|
||||
onSubmit={(starter) => submitPresetPrompt(starter.submitText, starter.ref.type)}
|
||||
className="chat-input-column mt-4"
|
||||
className={cn('chat-input-column mt-4', draftPresentationClassName)}
|
||||
/>
|
||||
) : null}
|
||||
{currentSessionId ? <BtwPanel parentSessionId={currentSessionId} panel={btwPanel} /> : null}
|
||||
</form>
|
||||
|
||||
{/* Issue Picker Dialog */}
|
||||
|
||||
Reference in New Issue
Block a user