mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-14 20:57:32 +00:00
Replace the legacy monolithic ChatInterface.jsx implementation with a modular TypeScript architecture centered around a small orchestration component (ChatInterface.tsx). Core architecture changes: - Remove src/components/ChatInterface.jsx and add src/components/ChatInterface.tsx as a thin coordinator that wires provider state, session state, realtime WebSocket handlers, and composer behavior via dedicated hooks. - Update src/components/MainContent.tsx to use typed ChatInterface directly (remove AnyChatInterface cast). State ownership and hook extraction: - Add src/hooks/chat/useChatProviderState.ts to centralize provider/model/permission-mode state, provider/session synchronization, cursor model bootstrap from backend config, and pending permission request scoping. - Add src/hooks/chat/useChatSessionState.ts to own chat/session lifecycle state: session loading, cursor/claude/codex history loading, pagination, scroll restoration, visible-window slicing, token budget loading, persisted chat hydration, and processing-state restoration. - Add src/hooks/chat/useChatRealtimeHandlers.ts to isolate WebSocket event processing for Claude/Cursor/Codex, including session filtering, streaming chunk buffering, session-created/pending-session transitions, permission request queueing/cancellation, completion/error handling, and session status updates. - Add src/hooks/chat/useChatComposerState.ts to own composer-local state and interactions: input/draft persistence, textarea sizing and keyboard behavior, slash command execution, file mentions, image attachment/drop/paste workflow, submit/abort flows, permission decision responses, and transcript insertion. UI modularization under src/components/chat: - Add view/ChatMessagesPane.tsx for message list rendering, loading/empty states, pagination affordances, and thinking indicator. - Add view/ChatComposer.tsx for composer shell layout and input area composition. - Add view/ChatInputControls.tsx for mode toggles, token display, command launcher, clear-input, and scroll-to-bottom controls. - Add view/PermissionRequestsBanner.tsx for explicit tool-permission review actions (allow once / allow & remember / deny). - Add view/ProviderSelectionEmptyState.tsx for provider and model selection UX plus task starter integration. - Add messages/MessageComponent.tsx and markdown/Markdown.tsx to isolate message rendering concerns, markdown/code rendering, and rich tool-output presentation. - Add input/ImageAttachment.tsx for attachment previews/removal/progress/error overlay rendering. Shared chat typing and utilities: - Add src/components/chat/types.ts with shared types for providers, permission mode, message/tool payloads, pending permission requests, and ChatInterfaceProps. - Add src/components/chat/utils/chatFormatting.ts for html decoding, code fence normalization, regex escaping, math-safe unescaping, and usage-limit text formatting. - Add src/components/chat/utils/chatPermissions.ts for permission rule derivation, suggestion generation, and grant flow. - Add src/components/chat/utils/chatStorage.ts for resilient localStorage access, quota handling, and normalized Claude settings retrieval. - Add src/components/chat/utils/messageTransforms.ts for session message normalization (Claude/Codex/Cursor) and cached diff computation utilities. Command/file input ergonomics: - Add src/hooks/chat/useSlashCommands.ts for slash command fetching, usage-based ranking, fuzzy filtering, keyboard navigation, and command history persistence. - Add src/hooks/chat/useFileMentions.tsx for project file flattening, @mention suggestions, mention highlighting, and keyboard/file insertion behavior. TypeScript support additions: - Add src/types/react-syntax-highlighter.d.ts module declarations to type-check markdown code highlighting imports. Behavioral intent: - Preserve existing chat behavior and provider flows while improving readability, separation of concerns, and future refactorability. - Move state closer to the components/hooks that own it, reducing cross-cutting concerns in the top-level chat component.
110 lines
5.0 KiB
TypeScript
110 lines
5.0 KiB
TypeScript
import React from 'react';
|
|
import type { PendingPermissionRequest } from '../types';
|
|
import { buildClaudeToolPermissionEntry, formatToolInputForDisplay } from '../utils/chatPermissions';
|
|
import { getClaudeSettings } from '../utils/chatStorage';
|
|
|
|
interface PermissionRequestsBannerProps {
|
|
pendingPermissionRequests: PendingPermissionRequest[];
|
|
handlePermissionDecision: (
|
|
requestIds: string | string[],
|
|
decision: { allow?: boolean; message?: string; rememberEntry?: string | null; updatedInput?: unknown },
|
|
) => void;
|
|
handleGrantToolPermission: (suggestion: { entry: string; toolName: string }) => { success: boolean };
|
|
}
|
|
|
|
export default function PermissionRequestsBanner({
|
|
pendingPermissionRequests,
|
|
handlePermissionDecision,
|
|
handleGrantToolPermission,
|
|
}: PermissionRequestsBannerProps) {
|
|
if (!pendingPermissionRequests.length) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="mb-3 space-y-2">
|
|
{pendingPermissionRequests.map((request) => {
|
|
const rawInput = formatToolInputForDisplay(request.input);
|
|
const permissionEntry = buildClaudeToolPermissionEntry(request.toolName, rawInput);
|
|
const settings = getClaudeSettings();
|
|
const alreadyAllowed = permissionEntry ? settings.allowedTools.includes(permissionEntry) : false;
|
|
const rememberLabel = alreadyAllowed ? 'Allow (saved)' : 'Allow & remember';
|
|
const matchingRequestIds = permissionEntry
|
|
? pendingPermissionRequests
|
|
.filter(
|
|
(item) =>
|
|
buildClaudeToolPermissionEntry(item.toolName, formatToolInputForDisplay(item.input)) === permissionEntry,
|
|
)
|
|
.map((item) => item.requestId)
|
|
: [request.requestId];
|
|
|
|
return (
|
|
<div
|
|
key={request.requestId}
|
|
className="rounded-lg border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20 p-3 shadow-sm"
|
|
>
|
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
<div>
|
|
<div className="text-sm font-semibold text-amber-900 dark:text-amber-100">Permission required</div>
|
|
<div className="text-xs text-amber-800 dark:text-amber-200">
|
|
Tool: <span className="font-mono">{request.toolName}</span>
|
|
</div>
|
|
</div>
|
|
{permissionEntry && (
|
|
<div className="text-xs text-amber-700 dark:text-amber-300">
|
|
Allow rule: <span className="font-mono">{permissionEntry}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{rawInput && (
|
|
<details className="mt-2">
|
|
<summary className="cursor-pointer text-xs text-amber-800 dark:text-amber-200 hover:text-amber-900 dark:hover:text-amber-100">
|
|
View tool input
|
|
</summary>
|
|
<pre className="mt-2 max-h-40 overflow-auto rounded-md bg-white/80 dark:bg-gray-900/60 border border-amber-200/60 dark:border-amber-800/60 p-2 text-xs text-amber-900 dark:text-amber-100 whitespace-pre-wrap">
|
|
{rawInput}
|
|
</pre>
|
|
</details>
|
|
)}
|
|
|
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => handlePermissionDecision(request.requestId, { allow: true })}
|
|
className="inline-flex items-center gap-2 rounded-md bg-amber-600 text-white text-xs font-medium px-3 py-1.5 hover:bg-amber-700 transition-colors"
|
|
>
|
|
Allow once
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (permissionEntry && !alreadyAllowed) {
|
|
handleGrantToolPermission({ entry: permissionEntry, toolName: request.toolName });
|
|
}
|
|
handlePermissionDecision(matchingRequestIds, { allow: true, rememberEntry: permissionEntry });
|
|
}}
|
|
className={`inline-flex items-center gap-2 rounded-md text-xs font-medium px-3 py-1.5 border transition-colors ${
|
|
permissionEntry
|
|
? 'border-amber-300 text-amber-800 hover:bg-amber-100 dark:border-amber-700 dark:text-amber-100 dark:hover:bg-amber-900/30'
|
|
: 'border-gray-300 text-gray-400 cursor-not-allowed'
|
|
}`}
|
|
disabled={!permissionEntry}
|
|
>
|
|
{rememberLabel}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => handlePermissionDecision(request.requestId, { allow: false, message: 'User denied tool use' })}
|
|
className="inline-flex items-center gap-2 rounded-md text-xs font-medium px-3 py-1.5 border border-red-300 text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-200 dark:hover:bg-red-900/30 transition-colors"
|
|
>
|
|
Deny
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|