mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-31 00:55:42 +08:00
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`
115 lines
3.7 KiB
TypeScript
115 lines
3.7 KiB
TypeScript
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, SessionProvider } from '../../../types/app';
|
|
|
|
interface UseChatProviderStateArgs {
|
|
selectedSession: ProjectSession | null;
|
|
}
|
|
|
|
export function useChatProviderState({ selectedSession }: UseChatProviderStateArgs) {
|
|
const [permissionMode, setPermissionMode] = useState<PermissionMode>('default');
|
|
const [pendingPermissionRequests, setPendingPermissionRequests] = useState<PendingPermissionRequest[]>([]);
|
|
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;
|
|
});
|
|
const [claudeModel, setClaudeModel] = useState<string>(() => {
|
|
return localStorage.getItem('claude-model') || CLAUDE_MODELS.DEFAULT;
|
|
});
|
|
const [codexModel, setCodexModel] = useState<string>(() => {
|
|
return localStorage.getItem('codex-model') || CODEX_MODELS.DEFAULT;
|
|
});
|
|
|
|
const lastProviderRef = useRef(provider);
|
|
|
|
useEffect(() => {
|
|
if (!selectedSession?.id) {
|
|
return;
|
|
}
|
|
|
|
const savedMode = localStorage.getItem(`permissionMode-${selectedSession.id}`);
|
|
setPermissionMode((savedMode as PermissionMode) || 'default');
|
|
}, [selectedSession?.id]);
|
|
|
|
useEffect(() => {
|
|
if (!selectedSession?.__provider || selectedSession.__provider === provider) {
|
|
return;
|
|
}
|
|
|
|
setProvider(selectedSession.__provider);
|
|
localStorage.setItem('selected-provider', selectedSession.__provider);
|
|
}, [provider, selectedSession]);
|
|
|
|
useEffect(() => {
|
|
if (lastProviderRef.current === provider) {
|
|
return;
|
|
}
|
|
setPendingPermissionRequests([]);
|
|
lastProviderRef.current = provider;
|
|
}, [provider]);
|
|
|
|
useEffect(() => {
|
|
setPendingPermissionRequests((previous) =>
|
|
previous.filter((request) => !request.sessionId || request.sessionId === selectedSession?.id),
|
|
);
|
|
}, [selectedSession?.id]);
|
|
|
|
useEffect(() => {
|
|
if (provider !== 'cursor') {
|
|
return;
|
|
}
|
|
|
|
authenticatedFetch('/api/cursor/config')
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (!data.success || !data.config?.model?.modelId) {
|
|
return;
|
|
}
|
|
|
|
const modelId = data.config.model.modelId as string;
|
|
if (!localStorage.getItem('cursor-model')) {
|
|
setCursorModel(modelId);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error('Error loading Cursor config:', error);
|
|
});
|
|
}, [provider]);
|
|
|
|
const cyclePermissionMode = useCallback(() => {
|
|
const modes: PermissionMode[] =
|
|
provider === 'codex'
|
|
? ['default', 'acceptEdits', 'bypassPermissions']
|
|
: ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
|
|
|
|
const currentIndex = modes.indexOf(permissionMode);
|
|
const nextIndex = (currentIndex + 1) % modes.length;
|
|
const nextMode = modes[nextIndex];
|
|
setPermissionMode(nextMode);
|
|
|
|
if (selectedSession?.id) {
|
|
localStorage.setItem(`permissionMode-${selectedSession.id}`, nextMode);
|
|
}
|
|
}, [permissionMode, provider, selectedSession?.id]);
|
|
|
|
return {
|
|
provider,
|
|
setProvider,
|
|
cursorModel,
|
|
setCursorModel,
|
|
claudeModel,
|
|
setClaudeModel,
|
|
codexModel,
|
|
setCodexModel,
|
|
permissionMode,
|
|
setPermissionMode,
|
|
pendingPermissionRequests,
|
|
setPendingPermissionRequests,
|
|
cyclePermissionMode,
|
|
};
|
|
}
|