mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-14 12:47:33 +00:00
fix(chat): stabilize provider/message handling and complete chat i18n coverage
Unify provider typing, harden realtime message effects, normalize tool input
serialization, and finish i18n/a11y updates across chat UI components.
- tighten provider contracts from `Provider | string` to `SessionProvider` in:
- `useChatProviderState`
- `useChatComposerState`
- `useChatRealtimeHandlers`
- `ChatMessagesPane`
- `ProviderSelectionEmptyState`
- refactor `AssistantThinkingIndicator` to accept `selectedProvider` via props
instead of reading provider from local storage during render
- fix stale-closure risk in `useChatRealtimeHandlers` by:
- adding missing effect dependencies
- introducing `lastProcessedMessageRef` to prevent duplicate processing when
dependencies change without a new message object
- standardize `toolInput` shape in `messageTransforms`:
- add `normalizeToolInput(...)`
- ensure all conversion paths produce consistent string output
- remove mixed `null`/raw/stringified variants across cursor/session branches
- harden tool display fallback in `CollapsibleDisplay`:
- default border class now falls back safely for unknown categories
- improve chat i18n consistency:
- localize hardcoded strings in `MessageComponent`
(`permissions.*`, `interactive.*`, `thinking.emoji`, `json.response`,
`messageTypes.error`)
- localize button titles in `ChatInputControls`
(`input.clearInput`, `input.scrollToBottom`)
- localize provider-specific empty-project prompt in `ChatInterface`
(`projectSelection.startChatWithProvider`)
- localize repeated “Start the next task” prompt in
`ProviderSelectionEmptyState` (`tasks.nextTaskPrompt`)
- add missing translation keys in all supported chat locales:
- `src/i18n/locales/en/chat.json`
- `src/i18n/locales/ko/chat.json`
- `src/i18n/locales/zh-CN/chat.json`
- new keys:
- `input.clearInput`
- `input.scrollToBottom`
- `projectSelection.startChatWithProvider`
- `tasks.nextTaskPrompt`
- improve attachment remove-button accessibility in `ImageAttachment`:
- add `type="button"` and `aria-label`
- make control visible on touch/small screens and focusable states
- preserve hover behavior on larger screens
Validation:
- `npm run typecheck`
This commit is contained in:
@@ -20,11 +20,10 @@ import type {
|
||||
ChatMessage,
|
||||
PendingPermissionRequest,
|
||||
PermissionMode,
|
||||
Provider,
|
||||
} from '../types/types';
|
||||
import { useFileMentions } from './useFileMentions';
|
||||
import { type SlashCommand, useSlashCommands } from './useSlashCommands';
|
||||
import type { Project, ProjectSession } from '../../../types/app';
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
|
||||
import { escapeRegExp } from '../utils/chatFormatting';
|
||||
|
||||
type PendingViewSession = {
|
||||
@@ -36,7 +35,7 @@ interface UseChatComposerStateArgs {
|
||||
selectedProject: Project | null;
|
||||
selectedSession: ProjectSession | null;
|
||||
currentSessionId: string | null;
|
||||
provider: Provider | string;
|
||||
provider: SessionProvider;
|
||||
permissionMode: PermissionMode | string;
|
||||
cyclePermissionMode: () => void;
|
||||
cursorModel: string;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import { CLAUDE_MODELS, CODEX_MODELS, CURSOR_MODELS } from '../../../../shared/modelConstants';
|
||||
import type { PendingPermissionRequest, PermissionMode, Provider } from '../types/types';
|
||||
import type { ProjectSession } from '../../../types/app';
|
||||
import type { ProjectSession, SessionProvider } from '../../../types/app';
|
||||
|
||||
interface UseChatProviderStateArgs {
|
||||
selectedSession: ProjectSession | null;
|
||||
@@ -11,8 +11,8 @@ interface UseChatProviderStateArgs {
|
||||
export function useChatProviderState({ selectedSession }: UseChatProviderStateArgs) {
|
||||
const [permissionMode, setPermissionMode] = useState<PermissionMode>('default');
|
||||
const [pendingPermissionRequests, setPendingPermissionRequests] = useState<PendingPermissionRequest[]>([]);
|
||||
const [provider, setProvider] = useState<Provider>(() => {
|
||||
return (localStorage.getItem('selected-provider') as Provider) || 'claude';
|
||||
const [provider, setProvider] = useState<SessionProvider>(() => {
|
||||
return (localStorage.getItem('selected-provider') as SessionProvider) || 'claude';
|
||||
});
|
||||
const [cursorModel, setCursorModel] = useState<string>(() => {
|
||||
return localStorage.getItem('cursor-model') || CURSOR_MODELS.DEFAULT;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||
import { decodeHtmlEntities, formatUsageLimitText } from '../utils/chatFormatting';
|
||||
import { safeLocalStorage } from '../utils/chatStorage';
|
||||
import type { ChatMessage, PendingPermissionRequest, Provider } from '../types/types';
|
||||
import type { Project, ProjectSession } from '../../../types/app';
|
||||
import type { ChatMessage, PendingPermissionRequest } from '../types/types';
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
|
||||
|
||||
type PendingViewSession = {
|
||||
sessionId: string | null;
|
||||
@@ -28,7 +28,7 @@ type LatestChatMessage = {
|
||||
|
||||
interface UseChatRealtimeHandlersArgs {
|
||||
latestMessage: LatestChatMessage | null;
|
||||
provider: Provider | string;
|
||||
provider: SessionProvider;
|
||||
selectedProject: Project | null;
|
||||
selectedSession: ProjectSession | null;
|
||||
currentSessionId: string | null;
|
||||
@@ -114,11 +114,19 @@ export function useChatRealtimeHandlers({
|
||||
onReplaceTemporarySession,
|
||||
onNavigateToSession,
|
||||
}: UseChatRealtimeHandlersArgs) {
|
||||
const lastProcessedMessageRef = useRef<LatestChatMessage | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard against duplicate processing when dependency updates occur without a new message object.
|
||||
if (lastProcessedMessageRef.current === latestMessage) {
|
||||
return;
|
||||
}
|
||||
lastProcessedMessageRef.current = latestMessage;
|
||||
|
||||
const messageData = latestMessage.data?.message || latestMessage.data;
|
||||
const structuredMessageData =
|
||||
messageData && typeof messageData === 'object' ? (messageData as Record<string, any>) : null;
|
||||
@@ -925,5 +933,24 @@ export function useChatRealtimeHandlers({
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [latestMessage]);
|
||||
}, [
|
||||
latestMessage,
|
||||
provider,
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
currentSessionId,
|
||||
setCurrentSessionId,
|
||||
setChatMessages,
|
||||
setIsLoading,
|
||||
setCanAbortSession,
|
||||
setClaudeStatus,
|
||||
setTokenBudget,
|
||||
setIsSystemSessionChange,
|
||||
setPendingPermissionRequests,
|
||||
onSessionInactive,
|
||||
onSessionProcessing,
|
||||
onSessionNotProcessing,
|
||||
onReplaceTemporarySession,
|
||||
onNavigateToSession,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ export const CollapsibleDisplay: React.FC<CollapsibleDisplayProps> = ({
|
||||
className = '',
|
||||
toolCategory
|
||||
}) => {
|
||||
const borderColor = borderColorMap[toolCategory || 'default'];
|
||||
// Fall back to default styling for unknown/new categories so className never includes "undefined".
|
||||
const borderColor = borderColorMap[toolCategory || 'default'] || borderColorMap.default;
|
||||
|
||||
return (
|
||||
<div className={`border-l-2 ${borderColor} pl-3 py-0.5 my-1 ${className}`}>
|
||||
|
||||
@@ -18,6 +18,22 @@ type CursorBlob = {
|
||||
|
||||
const asArray = <T>(value: unknown): T[] => (Array.isArray(value) ? (value as T[]) : []);
|
||||
|
||||
const normalizeToolInput = (value: unknown): string => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const toAbsolutePath = (projectPath: string, filePath?: string) => {
|
||||
if (!filePath) {
|
||||
return filePath;
|
||||
@@ -149,7 +165,7 @@ export const convertCursorSessionMessages = (blobs: CursorBlob[], projectPath: s
|
||||
isToolUse: true,
|
||||
toolName,
|
||||
toolId: toolCallId,
|
||||
toolInput: null,
|
||||
toolInput: normalizeToolInput(null),
|
||||
toolResult: {
|
||||
content: result,
|
||||
isError: false,
|
||||
@@ -253,7 +269,7 @@ export const convertCursorSessionMessages = (blobs: CursorBlob[], projectPath: s
|
||||
isToolUse: true,
|
||||
toolName,
|
||||
toolId,
|
||||
toolInput: toolInput ? JSON.stringify(toolInput) : null,
|
||||
toolInput: normalizeToolInput(toolInput),
|
||||
toolResult: null,
|
||||
};
|
||||
converted.push(toolMessage);
|
||||
@@ -412,7 +428,7 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
|
||||
timestamp: message.timestamp || new Date().toISOString(),
|
||||
isToolUse: true,
|
||||
toolName: message.toolName,
|
||||
toolInput: message.toolInput || '',
|
||||
toolInput: normalizeToolInput(message.toolInput),
|
||||
toolCallId: message.toolCallId,
|
||||
});
|
||||
return;
|
||||
@@ -459,7 +475,7 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
|
||||
timestamp: message.timestamp || new Date().toISOString(),
|
||||
isToolUse: true,
|
||||
toolName: part.name,
|
||||
toolInput: JSON.stringify(part.input),
|
||||
toolInput: normalizeToolInput(part.input),
|
||||
toolResult: toolResult
|
||||
? {
|
||||
content:
|
||||
|
||||
@@ -245,10 +245,22 @@ function ChatInterface({
|
||||
}, [resetStreamingState]);
|
||||
|
||||
if (!selectedProject) {
|
||||
const selectedProviderLabel =
|
||||
provider === 'cursor'
|
||||
? t('messageTypes.cursor')
|
||||
: provider === 'codex'
|
||||
? t('messageTypes.codex')
|
||||
: t('messageTypes.claude');
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center text-gray-500 dark:text-gray-400">
|
||||
<p>Select a project to start chatting with Claude</p>
|
||||
<p>
|
||||
{t('projectSelection.startChatWithProvider', {
|
||||
provider: selectedProviderLabel,
|
||||
defaultValue: 'Select a project to start chatting with {{provider}}',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,33 +1,37 @@
|
||||
import SessionProviderLogo from "../../../SessionProviderLogo";
|
||||
import { Provider } from "../../types/types";
|
||||
import { SessionProvider } from '../../../../types/app';
|
||||
import SessionProviderLogo from '../../../SessionProviderLogo';
|
||||
import type { Provider } from '../../types/types';
|
||||
|
||||
export default function AssistantThinkingIndicator() {
|
||||
const selectedProvider = (localStorage.getItem('selected-provider') || 'claude') as Provider;
|
||||
type AssistantThinkingIndicatorProps = {
|
||||
selectedProvider: SessionProvider;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-message assistant">
|
||||
<div className="w-full">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0 p-1 bg-transparent">
|
||||
<SessionProviderLogo provider={selectedProvider} className="w-full h-full" />
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{selectedProvider === 'cursor' ? 'Cursor' : selectedProvider === 'codex' ? 'Codex' : 'Claude'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full text-sm text-gray-500 dark:text-gray-400 pl-3 sm:pl-0">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="animate-pulse">.</div>
|
||||
<div className="animate-pulse" style={{ animationDelay: '0.2s' }}>
|
||||
.
|
||||
</div>
|
||||
<div className="animate-pulse" style={{ animationDelay: '0.4s' }}>
|
||||
.
|
||||
</div>
|
||||
<span className="ml-2">Thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
export default function AssistantThinkingIndicator({ selectedProvider }: AssistantThinkingIndicatorProps) {
|
||||
return (
|
||||
<div className="chat-message assistant">
|
||||
<div className="w-full">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0 p-1 bg-transparent">
|
||||
<SessionProviderLogo provider={selectedProvider} className="w-full h-full" />
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{selectedProvider === 'cursor' ? 'Cursor' : selectedProvider === 'codex' ? 'Codex' : 'Claude'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className="w-full text-sm text-gray-500 dark:text-gray-400 pl-3 sm:pl-0">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="animate-pulse">.</div>
|
||||
<div className="animate-pulse" style={{ animationDelay: '0.2s' }}>
|
||||
.
|
||||
</div>
|
||||
<div className="animate-pulse" style={{ animationDelay: '0.4s' }}>
|
||||
.
|
||||
</div>
|
||||
<span className="ml-2">Thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ export default function ChatInputControls({
|
||||
type="button"
|
||||
onClick={onClearInput}
|
||||
className="w-8 h-8 bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-full flex items-center justify-center transition-all duration-200 group shadow-sm"
|
||||
title="Clear input"
|
||||
title={t('input.clearInput', { defaultValue: 'Clear input' })}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-600 dark:text-gray-300 group-hover:text-gray-800 dark:group-hover:text-gray-100 transition-colors"
|
||||
@@ -126,7 +126,7 @@ export default function ChatInputControls({
|
||||
<button
|
||||
onClick={onScrollToBottom}
|
||||
className="w-8 h-8 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800"
|
||||
title="Scroll to bottom"
|
||||
title={t('input.scrollToBottom', { defaultValue: 'Scroll to bottom' })}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { Dispatch, RefObject, SetStateAction } from 'react';
|
||||
|
||||
import MessageComponent from './MessageComponent';
|
||||
import ProviderSelectionEmptyState from './ProviderSelectionEmptyState';
|
||||
import type { ChatMessage, Provider } from '../../types/types';
|
||||
import type { Project, ProjectSession } from '../../../../types/app';
|
||||
import type { ChatMessage } from '../../types/types';
|
||||
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
|
||||
import AssistantThinkingIndicator from './AssistantThinkingIndicator';
|
||||
import { getIntrinsicMessageKey } from '../../utils/messageKeys';
|
||||
|
||||
@@ -17,8 +17,8 @@ interface ChatMessagesPaneProps {
|
||||
chatMessages: ChatMessage[];
|
||||
selectedSession: ProjectSession | null;
|
||||
currentSessionId: string | null;
|
||||
provider: Provider | string;
|
||||
setProvider: (provider: Provider | string) => void;
|
||||
provider: SessionProvider;
|
||||
setProvider: (provider: SessionProvider) => void;
|
||||
textareaRef: RefObject<HTMLTextAreaElement>;
|
||||
claudeModel: string;
|
||||
setClaudeModel: (model: string) => void;
|
||||
@@ -201,7 +201,7 @@ export default function ChatMessagesPane({
|
||||
</>
|
||||
)}
|
||||
|
||||
{isLoading && <AssistantThinkingIndicator />}
|
||||
{isLoading && <AssistantThinkingIndicator selectedProvider={provider} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,8 +32,10 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }: ImageAttachm
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 opacity-0 group-hover:opacity-100"
|
||||
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 focus:opacity-100 transition-opacity"
|
||||
aria-label="Remove image"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
|
||||
@@ -186,7 +186,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
<svg className="w-4 h-4 text-red-500 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<span className="text-xs font-medium text-red-700 dark:text-red-300">Error</span>
|
||||
<span className="text-xs font-medium text-red-700 dark:text-red-300">{t('messageTypes.error')}</span>
|
||||
</div>
|
||||
<div className="relative text-sm text-red-900 dark:text-red-100">
|
||||
<Markdown className="prose prose-sm max-w-none prose-red dark:prose-invert">
|
||||
@@ -214,8 +214,8 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
}`}
|
||||
>
|
||||
{permissionSuggestion.isAllowed || permissionGrantState === 'granted'
|
||||
? 'Permission added'
|
||||
: `Grant permission for ${permissionSuggestion.toolName}`}
|
||||
? t('permissions.added')
|
||||
: t('permissions.grant', { tool: permissionSuggestion.toolName })}
|
||||
</button>
|
||||
{onShowSettings && (
|
||||
<button
|
||||
@@ -223,21 +223,21 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
onClick={(e) => { e.stopPropagation(); onShowSettings(); }}
|
||||
className="text-xs text-red-700 dark:text-red-200 underline hover:text-red-800 dark:hover:text-red-100"
|
||||
>
|
||||
Open settings
|
||||
{t('permissions.openSettings')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-red-700/90 dark:text-red-200/80">
|
||||
Adds <span className="font-mono">{permissionSuggestion.entry}</span> to Allowed Tools.
|
||||
{t('permissions.addTo', { entry: permissionSuggestion.entry })}
|
||||
</div>
|
||||
{permissionGrantState === 'error' && (
|
||||
<div className="mt-2 text-xs text-red-700 dark:text-red-200">
|
||||
Unable to update permissions. Please try again.
|
||||
{t('permissions.error')}
|
||||
</div>
|
||||
)}
|
||||
{(permissionSuggestion.isAllowed || permissionGrantState === 'granted') && (
|
||||
<div className="mt-2 text-xs text-green-700 dark:text-green-200">
|
||||
Permission saved. Retry the request to use the tool.
|
||||
{t('permissions.retry')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -273,7 +273,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-amber-900 dark:text-amber-100 text-base mb-3">
|
||||
Interactive Prompt
|
||||
{t('interactive.title')}
|
||||
</h4>
|
||||
{(() => {
|
||||
const lines = (message.content || '').split('\n').filter((line) => line.trim());
|
||||
@@ -333,10 +333,10 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
|
||||
<div className="bg-amber-100 dark:bg-amber-800/30 rounded-lg p-3">
|
||||
<p className="text-amber-900 dark:text-amber-100 text-sm font-medium mb-1">
|
||||
⏳ Waiting for your response in the CLI
|
||||
{t('interactive.waiting')}
|
||||
</p>
|
||||
<p className="text-amber-800 dark:text-amber-200 text-xs">
|
||||
Please select an option in your terminal where Claude is running.
|
||||
{t('interactive.instruction')}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
@@ -353,7 +353,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
<svg className="w-3 h-3 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span>💭 Thinking...</span>
|
||||
<span>{t('thinking.emoji')}</span>
|
||||
</summary>
|
||||
<div className="mt-2 pl-4 border-l-2 border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 text-sm">
|
||||
<Markdown className="prose prose-sm max-w-none dark:prose-invert prose-gray">
|
||||
@@ -368,7 +368,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
{showThinking && message.reasoning && (
|
||||
<details className="mb-3">
|
||||
<summary className="cursor-pointer text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 font-medium">
|
||||
💭 Thinking...
|
||||
{t('thinking.emoji')}
|
||||
</summary>
|
||||
<div className="mt-2 pl-4 border-l-2 border-gray-300 dark:border-gray-600 italic text-gray-600 dark:text-gray-400 text-sm">
|
||||
<div className="whitespace-pre-wrap">
|
||||
@@ -395,7 +395,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="font-medium">JSON Response</span>
|
||||
<span className="font-medium">{t('json.response')}</span>
|
||||
</div>
|
||||
<div className="bg-gray-800 dark:bg-gray-900 border border-gray-600/30 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<pre className="p-4 overflow-x-auto">
|
||||
@@ -437,4 +437,5 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
);
|
||||
});
|
||||
|
||||
export default MessageComponent;
|
||||
export default MessageComponent;
|
||||
|
||||
|
||||
@@ -3,14 +3,13 @@ import { useTranslation } from 'react-i18next';
|
||||
import SessionProviderLogo from '../../../SessionProviderLogo';
|
||||
import NextTaskBanner from '../../../NextTaskBanner.jsx';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../../../../shared/modelConstants';
|
||||
import type { Provider } from '../../types/types';
|
||||
import type { ProjectSession } from '../../../../types/app';
|
||||
import type { ProjectSession, SessionProvider } from '../../../../types/app';
|
||||
|
||||
interface ProviderSelectionEmptyStateProps {
|
||||
selectedSession: ProjectSession | null;
|
||||
currentSessionId: string | null;
|
||||
provider: Provider | string;
|
||||
setProvider: (next: Provider | string) => void;
|
||||
provider: SessionProvider;
|
||||
setProvider: (next: SessionProvider) => void;
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
claudeModel: string;
|
||||
setClaudeModel: (model: string) => void;
|
||||
@@ -42,8 +41,10 @@ export default function ProviderSelectionEmptyState({
|
||||
setInput,
|
||||
}: ProviderSelectionEmptyStateProps) {
|
||||
const { t } = useTranslation('chat');
|
||||
// Reuse one translated prompt so task-start behavior stays consistent across empty and session states.
|
||||
const nextTaskPrompt = t('tasks.nextTaskPrompt', { defaultValue: 'Start the next task' });
|
||||
|
||||
const selectProvider = (nextProvider: Provider) => {
|
||||
const selectProvider = (nextProvider: SessionProvider) => {
|
||||
setProvider(nextProvider);
|
||||
localStorage.setItem('selected-provider', nextProvider);
|
||||
setTimeout(() => textareaRef.current?.focus(), 100);
|
||||
@@ -202,7 +203,7 @@ export default function ProviderSelectionEmptyState({
|
||||
|
||||
{provider && tasksEnabled && isTaskMasterInstalled && (
|
||||
<div className="mt-4 px-4 sm:px-0">
|
||||
<NextTaskBanner onStartTask={() => setInput('Start the next task')} onShowAllTasks={onShowAllTasks} />
|
||||
<NextTaskBanner onStartTask={() => setInput(nextTaskPrompt)} onShowAllTasks={onShowAllTasks} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -214,7 +215,7 @@ export default function ProviderSelectionEmptyState({
|
||||
|
||||
{tasksEnabled && isTaskMasterInstalled && (
|
||||
<div className="mt-4 px-4 sm:px-0">
|
||||
<NextTaskBanner onStartTask={() => setInput('Start the next task')} onShowAllTasks={onShowAllTasks} />
|
||||
<NextTaskBanner onStartTask={() => setInput(nextTaskPrompt)} onShowAllTasks={onShowAllTasks} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -106,7 +106,9 @@
|
||||
"enter": "Enter to send • Shift+Enter for new line • Tab to change modes • / for slash commands"
|
||||
},
|
||||
"clickToChangeMode": "Click to change permission mode (or press Tab in input)",
|
||||
"showAllCommands": "Show all commands"
|
||||
"showAllCommands": "Show all commands",
|
||||
"clearInput": "Clear input",
|
||||
"scrollToBottom": "Scroll to bottom"
|
||||
},
|
||||
"thinkingMode": {
|
||||
"selector": {
|
||||
@@ -201,5 +203,11 @@
|
||||
"runCommand": "Run {{command}} in {{projectName}}",
|
||||
"startCli": "Starting Claude CLI in {{projectName}}",
|
||||
"defaultCommand": "command"
|
||||
},
|
||||
"projectSelection": {
|
||||
"startChatWithProvider": "Select a project to start chatting with {{provider}}"
|
||||
},
|
||||
"tasks": {
|
||||
"nextTaskPrompt": "Start the next task"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,9 @@
|
||||
"enter": "Enter로 전송 • Shift+Enter로 줄바꿈 • Tab으로 모드 변경 • /로 슬래시 명령어"
|
||||
},
|
||||
"clickToChangeMode": "클릭하여 권한 모드 변경 (또는 입력창에서 Tab)",
|
||||
"showAllCommands": "모든 명령어 보기"
|
||||
"showAllCommands": "모든 명령어 보기",
|
||||
"clearInput": "입력 지우기",
|
||||
"scrollToBottom": "맨 아래로 스크롤"
|
||||
},
|
||||
"thinkingMode": {
|
||||
"selector": {
|
||||
@@ -201,5 +203,11 @@
|
||||
"runCommand": "{{projectName}}에서 {{command}} 실행",
|
||||
"startCli": "{{projectName}}에서 Claude CLI 시작",
|
||||
"defaultCommand": "명령어"
|
||||
},
|
||||
"projectSelection": {
|
||||
"startChatWithProvider": "{{provider}}와 채팅을 시작하려면 프로젝트를 선택하세요"
|
||||
},
|
||||
"tasks": {
|
||||
"nextTaskPrompt": "다음 작업 시작"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,9 @@
|
||||
"enter": "Enter 发送 • Shift+Enter 换行 • Tab 切换模式 • / 斜杠命令"
|
||||
},
|
||||
"clickToChangeMode": "点击更改权限模式(或在输入框中按 Tab)",
|
||||
"showAllCommands": "显示所有命令"
|
||||
"showAllCommands": "显示所有命令",
|
||||
"clearInput": "清空输入",
|
||||
"scrollToBottom": "滚动到底部"
|
||||
},
|
||||
"thinkingMode": {
|
||||
"selector": {
|
||||
@@ -201,5 +203,11 @@
|
||||
"runCommand": "在 {{projectName}} 中运行 {{command}}",
|
||||
"startCli": "在 {{projectName}} 中启动 Claude CLI",
|
||||
"defaultCommand": "命令"
|
||||
},
|
||||
"projectSelection": {
|
||||
"startChatWithProvider": "选择一个项目以开始与 {{provider}} 聊天"
|
||||
},
|
||||
"tasks": {
|
||||
"nextTaskPrompt": "开始下一个任务"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user