mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-07 14:12:40 +08:00
fix: address code review findings
- validate chat image attachments server-side: only files inside the ~/.cloudcli/assets upload store may reach provider file reads - harden asset serving with nosniff and attachment disposition for SVGs to prevent stored XSS - show the timestamp on image-only user messages - serialize git stage/unstage calls and defer the status re-sync so rapid toggles can't interleave or flicker - use a literal hex fallback for commit ref badges (alpha suffix on a var() string produced invalid CSS) - stop binding a URL session to a guening project instead - add the missing attentionRequiredIndicator key to all sidebar locales - clean up dangling conjunctions left the de/ru/tr/ja/zh-CN/zh-TW READMEs
This commit is contained in:
@@ -94,7 +94,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, s
|
||||
projectId={selectedProject?.projectId}
|
||||
/>
|
||||
)}
|
||||
{(userCopyContent.trim().length > 0 || !message.images?.length) && (
|
||||
{userCopyContent.trim().length > 0 || !message.images?.length ? (
|
||||
<div className="group max-w-full rounded-2xl rounded-br-md bg-blue-600 px-3 py-2 text-white shadow-sm sm:px-4">
|
||||
<div dir="auto" className="whitespace-pre-wrap break-words font-serif text-sm">
|
||||
{message.content}
|
||||
@@ -106,6 +106,11 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, s
|
||||
<span>{formattedTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Image-only turn: no text bubble, but the timestamp still shows */
|
||||
<div className="flex items-center justify-end gap-1 text-xs text-muted-foreground">
|
||||
<span>{formattedTime}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isGrouped && (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GitBranch, GitCommit, RefreshCw } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { ConfirmationRequest, FileStatusCode, GitDiffMap, GitStatusResponse } from '../../types/types';
|
||||
import { getAllChangedFiles, hasChangedFiles } from '../../utils/gitPanelUtils';
|
||||
import CommitComposer from './CommitComposer';
|
||||
@@ -49,20 +49,40 @@ export default function ChangesView({
|
||||
}: ChangesViewProps) {
|
||||
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
|
||||
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
|
||||
// Stage/unstage calls in flight or queued. While > 0, status refreshes must
|
||||
// not overwrite the optimistic selection with a snapshot that predates the
|
||||
// later clicks.
|
||||
const [pendingStageOps, setPendingStageOps] = useState(0);
|
||||
// Serializes stage/unstage requests so rapid toggles cannot interleave on
|
||||
// the server or resolve out of order.
|
||||
const stageOpQueueRef = useRef<Promise<unknown>>(Promise.resolve());
|
||||
|
||||
const changedFiles = useMemo(() => getAllChangedFiles(gitStatus), [gitStatus]);
|
||||
const hasExpandedFiles = expandedFiles.size > 0;
|
||||
|
||||
const enqueueStageOp = useCallback((operation: () => Promise<unknown>) => {
|
||||
setPendingStageOps((count) => count + 1);
|
||||
stageOpQueueRef.current = stageOpQueueRef.current
|
||||
.catch(() => {}) // a failed op must not block the queue
|
||||
.then(operation)
|
||||
.finally(() => setPendingStageOps((count) => count - 1));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!gitStatus || gitStatus.error) {
|
||||
setSelectedFiles(new Set());
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingStageOps > 0) {
|
||||
return; // keep the optimistic state until the queued ops settle
|
||||
}
|
||||
|
||||
// The Staged section mirrors the real git index reported by /status, so
|
||||
// files staged outside the app (VSCode, terminal) show up here too.
|
||||
// files staged outside the app (VSCode, terminal) show up here too. Also
|
||||
// re-runs when the queue drains, syncing to the final refreshed status.
|
||||
setSelectedFiles(new Set(gitStatus.staged ?? []));
|
||||
}, [gitStatus]);
|
||||
}, [gitStatus, pendingStageOps]);
|
||||
|
||||
useEffect(() => {
|
||||
onExpandedFilesChange(hasExpandedFiles);
|
||||
@@ -87,8 +107,8 @@ export default function ChangesView({
|
||||
}, []);
|
||||
|
||||
// Staging is real: every toggle runs git add / git reset through the API.
|
||||
// The set is flipped optimistically; the status refresh triggered by the
|
||||
// API call re-syncs it from the actual index afterwards.
|
||||
// The set is flipped optimistically; the queued API call keeps the git
|
||||
// index in sync and the final status refresh re-syncs once the queue drains.
|
||||
const toggleFileSelected = useCallback(
|
||||
(filePath: string) => {
|
||||
const isStaged = selectedFiles.has(filePath);
|
||||
@@ -101,9 +121,9 @@ export default function ChangesView({
|
||||
}
|
||||
return next;
|
||||
});
|
||||
void (isStaged ? onUnstageFiles([filePath]) : onStageFiles([filePath]));
|
||||
enqueueStageOp(() => (isStaged ? onUnstageFiles([filePath]) : onStageFiles([filePath])));
|
||||
},
|
||||
[onStageFiles, onUnstageFiles, selectedFiles],
|
||||
[enqueueStageOp, onStageFiles, onUnstageFiles, selectedFiles],
|
||||
);
|
||||
|
||||
const requestFileAction = useCallback(
|
||||
@@ -209,7 +229,7 @@ export default function ChangesView({
|
||||
onClick={() => {
|
||||
const filesToUnstage = Array.from(selectedFiles);
|
||||
setSelectedFiles(new Set());
|
||||
void onUnstageFiles(filesToUnstage);
|
||||
enqueueStageOp(() => onUnstageFiles(filesToUnstage));
|
||||
}}
|
||||
className="text-xs text-primary transition-colors hover:text-primary/80"
|
||||
>
|
||||
@@ -246,7 +266,7 @@ export default function ChangesView({
|
||||
onClick={() => {
|
||||
const filesToStage = Array.from(unstagedFiles);
|
||||
setSelectedFiles(new Set(changedFiles));
|
||||
void onStageFiles(filesToStage);
|
||||
enqueueStageOp(() => onStageFiles(filesToStage));
|
||||
}}
|
||||
className="text-xs text-primary transition-colors hover:text-primary/80"
|
||||
>
|
||||
|
||||
@@ -62,7 +62,9 @@ export default function CommitHistoryItem({
|
||||
return parseCommitFiles(diff);
|
||||
}, [diff]);
|
||||
|
||||
const badgeColor = graphRow ? laneColor(graphRow.nodeLane) : 'var(--color-primary, #0ea5e9)';
|
||||
// Must stay a literal hex value: RefBadge derives its HEAD tint by
|
||||
// appending an alpha byte (`${color}22`), which breaks for var() strings.
|
||||
const badgeColor = graphRow ? laneColor(graphRow.nodeLane) : '#0ea5e9';
|
||||
|
||||
return (
|
||||
<div className="flex border-b border-border last:border-0">
|
||||
|
||||
Reference in New Issue
Block a user