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:
Haileyesus
2026-02-12 22:39:55 +03:00
parent 0306f8d59b
commit a4e55984ea
15 changed files with 166 additions and 79 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
]);
}

View File

@@ -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}`}>

View File

@@ -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:

View File

@@ -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>
);

View File

@@ -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>
);
}

View File

@@ -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" />

View File

@@ -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>
);
}

View File

@@ -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" />

View File

@@ -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;

View File

@@ -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>

View File

@@ -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"
}
}

View File

@@ -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": "다음 작업 시작"
}
}

View File

@@ -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": "开始下一个任务"
}
}