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:
Haileyesus
2026-07-06 21:08:22 +03:00
parent 4e423f5fa2
commit 138371525a
23 changed files with 197 additions and 70 deletions

View File

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

View File

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

View File

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