Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8139,6 +8139,7 @@ export default function ChatView({
</header>

<RenameThreadDialog
key={activeThread.id}
open={renameDialogOpen}
currentTitle={activeThread.title}
onOpenChange={setRenameDialogOpen}
Expand Down
26 changes: 7 additions & 19 deletions apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -953,15 +953,14 @@ export default function GitActionsControl({
normalizedCreateBranchName !== normalizedCurrentBranchName &&
branchNames.has(normalizedCreateBranchName);

const createAndCheckoutBranch = useCallback(
async (branchName: string) => {
const api = readNativeApi();
if (!api || !gitCwd) return;
const createAndCheckoutBranch = async (branchName: string) => {
const api = readNativeApi();
if (!api || !gitCwd) return;

const trimmedName = branchName.trim();
if (!trimmedName) return;
const trimmedName = branchName.trim();
if (!trimmedName) return;

setIsCreateBranchDialogOpen(false);
setIsCreateBranchDialogOpen(false);
setCreateBranchName("");

if (trimmedName.toLowerCase() === normalizedCurrentBranchName) {
Expand Down Expand Up @@ -1041,18 +1040,7 @@ export default function GitActionsControl({
data: threadToastData,
});
}
},
[
activeThread?.worktreePath,
activeThreadId,
gitCwd,
hasOriginRemote,
normalizedCurrentBranchName,
queryClient,
setThreadWorkspaceAction,
threadToastData,
],
);
};

const openDialogForMenuItem = useCallback(
(item: GitActionMenuItem) => {
Expand Down
15 changes: 6 additions & 9 deletions apps/web/src/components/ProjectScriptsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import React, {
type FormEvent,
type KeyboardEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
Expand Down Expand Up @@ -271,16 +270,14 @@ export default function ProjectScriptsControl({

// Allow parent surfaces like the compact header menu to open the shared
// "Add action" dialog without duplicating script form logic.
useEffect(() => {
if (openAddActionNonce === undefined) return;
if (lastOpenAddActionNonceRef.current === undefined) {
lastOpenAddActionNonceRef.current = openAddActionNonce;
return;
}
if (openAddActionNonce === lastOpenAddActionNonceRef.current) return;
if (
openAddActionNonce !== undefined &&
lastOpenAddActionNonceRef.current !== undefined &&
openAddActionNonce !== lastOpenAddActionNonceRef.current
) {
lastOpenAddActionNonceRef.current = openAddActionNonce;
openAddDialog();
}, [openAddActionNonce]);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const confirmDeleteScript = useCallback(() => {
if (!editingScriptId) return;
Expand Down
70 changes: 38 additions & 32 deletions apps/web/src/components/ProjectSidebarIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,46 @@ export function ProjectSidebarIcon({
className = "size-4",
}: ProjectSidebarIconProps) {
const faviconSrc = resolveProjectFaviconUrl(cwd);
const shouldUseFavicon = iconMetadata === null;
const FolderGlyph = expanded ? HiOutlineFolderOpen : FolderClosed;

if (iconMetadata) {
const artwork = PROJECT_ICON_ARTWORK[iconMetadata.iconId];
const Icon = artwork.icon;

return (
<span
aria-label={`${iconMetadata.label} project icon`}
className={`${className} inline-flex shrink-0 items-center justify-center`}
data-project-icon-id={iconMetadata.iconId}
style={{ color: artwork.color }}
title={iconMetadata.label}
>
<Icon aria-hidden="true" className="size-[94%]" focusable="false" />
</span>
);
}

return (
<ProjectFolderIcon className={className} faviconSrc={faviconSrc} FolderGlyph={FolderGlyph} />
);
}

function ProjectFolderIcon({
className,
faviconSrc,
FolderGlyph,
}: {
className: string;
faviconSrc: string;
FolderGlyph: typeof HiOutlineFolderOpen;
}) {
const [hasFavicon, setHasFavicon] = useState<boolean>(
() => shouldUseFavicon && projectFaviconPresence.get(faviconSrc) === true,
() => projectFaviconPresence.get(faviconSrc) === true,
);
const FolderGlyph = expanded ? HiOutlineFolderOpen : FolderClosed;

// Probe with Image() so Electron/file-origin behaves like the actual visible <img>.
useEffect(() => {
if (!shouldUseFavicon) {
setHasFavicon(false);
return;
}

const cached = projectFaviconPresence.get(faviconSrc);
if (cached !== undefined) {
setHasFavicon(cached);
if (projectFaviconPresence.has(faviconSrc)) {
return;
Comment on lines 127 to 134

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Sync hasFavicon when faviconSrc changes.

useState keeps the previous project's value. If this component is reused for a new cwd whose favicon presence is already cached, Lines 128-129 return before copying that cached value into state, so the badge can stay stale or disappear for the new project.

Suggested fix
   const [hasFavicon, setHasFavicon] = useState<boolean>(
     () => projectFaviconPresence.get(faviconSrc) === true,
   );

   // Probe with Image() so Electron/file-origin behaves like the actual visible <img>.
   useEffect(() => {
-    if (projectFaviconPresence.has(faviconSrc)) {
+    const cachedPresence = projectFaviconPresence.get(faviconSrc);
+    if (cachedPresence !== undefined) {
+      setHasFavicon(cachedPresence);
       return;
     }
+
+    setHasFavicon(false);

     let cancelled = false;
     const image = new Image();

Also applies to: 157-157

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/ProjectSidebarIcon.tsx` around lines 122 - 129, The
component's hasFavicon state isn't synced when faviconSrc changes because the
useEffect returns early if projectFaviconPresence.has(faviconSrc) without
updating state; update the effect inside ProjectSidebarIcon so that when
projectFaviconPresence.has(faviconSrc) you call
setHasFavicon(projectFaviconPresence.get(faviconSrc) === true) before returning,
ensuring state mirrors the cache for the new faviconSrc (apply the same change
to the other similar effect that references projectFaviconPresence and
hasFavicon).

}

Expand Down Expand Up @@ -130,28 +154,10 @@ export function ProjectSidebarIcon({
image.removeEventListener("load", handleLoad);
image.removeEventListener("error", handleError);
};
}, [faviconSrc, shouldUseFavicon]);

if (iconMetadata) {
const artwork = PROJECT_ICON_ARTWORK[iconMetadata.iconId];
const Icon = artwork.icon;

return (
<span
aria-label={`${iconMetadata.label} project icon`}
className={`${className} inline-flex shrink-0 items-center justify-center`}
data-project-icon-id={iconMetadata.iconId}
role="img"
style={{ color: artwork.color }}
title={iconMetadata.label}
>
<Icon aria-hidden="true" className="size-[94%]" focusable="false" />
</span>
);
}
}, [faviconSrc]);

return (
<>
<span className="relative inline-flex shrink-0 items-center justify-center">
<FolderGlyph aria-hidden="true" focusable="false" className={className} />
{hasFavicon ? (
<img
Expand All @@ -165,6 +171,6 @@ export function ProjectSidebarIcon({
}}
/>
) : null}
</>
</span>
);
}
77 changes: 31 additions & 46 deletions apps/web/src/components/PullRequestThreadDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { GitResolvePullRequestResult } from "@jcode/contracts";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useDebouncedValue } from "@tanstack/react-pacer";
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { useEffect, useId, useRef, useState } from "react";

import {
gitPreparePullRequestThreadMutationOptions,
Expand Down Expand Up @@ -53,13 +53,6 @@ export function PullRequestThreadDialog({
(debouncerState) => ({ isPending: debouncerState.isPending }),
);

useEffect(() => {
if (!open) return;
setReference(initialReference ?? "");
setReferenceDirty(false);
setPreparingMode(null);
}, [initialReference, open]);

useEffect(() => {
if (!open) return;
const frame = window.requestAnimationFrame(() => {
Expand All @@ -79,7 +72,7 @@ export function PullRequestThreadDialog({
reference: open ? parsedDebouncedReference : null,
}),
);
const cachedPullRequest = useMemo(() => {
const cachedPullRequest = (() => {
if (!cwd || !parsedReference) {
return null;
}
Expand All @@ -90,7 +83,7 @@ export function PullRequestThreadDialog({
parsedReference,
]);
return cached?.pullRequest ?? null;
}, [cwd, parsedReference, queryClient]);
})();
const preparePullRequestThreadMutation = useMutation(
gitPreparePullRequestThreadMutationOptions({ cwd, queryClient }),
);
Expand All @@ -108,7 +101,7 @@ export function PullRequestThreadDialog({
parsedReference !== parsedDebouncedReference ||
resolvePullRequestQuery.isPending ||
resolvePullRequestQuery.isFetching);
const statusTone = useMemo(() => {
const statusTone = (() => {
switch (resolvedPullRequest?.state) {
case "merged":
return "text-[var(--app-status-plan-fg)]";
Expand All @@ -119,42 +112,34 @@ export function PullRequestThreadDialog({
default:
return "text-muted-foreground";
}
}, [resolvedPullRequest?.state]);
})();

const handleConfirm = useCallback(
async (mode: "local" | "worktree") => {
if (!parsedReference) {
setReferenceDirty(true);
return;
}
if (!parsedReference || !resolvedPullRequest || !cwd) {
return;
}
setPreparingMode(mode);
try {
const result = await preparePullRequestThreadMutation.mutateAsync({
reference: parsedReference,
mode,
});
await onPrepared({
branch: result.branch,
worktreePath: result.worktreePath,
pullRequest: resolvedPullRequest,
});
onOpenChange(false);
} finally {
setPreparingMode(null);
}
},
[
cwd,
onOpenChange,
onPrepared,
parsedReference,
preparePullRequestThreadMutation,
resolvedPullRequest,
],
);
const handleConfirm = async (mode: "local" | "worktree") => {
if (!parsedReference) {
setReferenceDirty(true);
return;
}
if (!parsedReference || !resolvedPullRequest || !cwd) {
return;
}
setPreparingMode(mode);
try {
const result = await preparePullRequestThreadMutation.mutateAsync({
reference: parsedReference,
mode,
});
await onPrepared({
branch: result.branch,
worktreePath: result.worktreePath,
pullRequest: resolvedPullRequest,
});
onOpenChange(false);
setPreparingMode(null);
} catch (error) {
setPreparingMode(null);
throw error;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
};

const validationMessage = !referenceDirty
? null
Expand Down
32 changes: 19 additions & 13 deletions apps/web/src/components/RenameThreadDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,41 +24,47 @@ export function RenameThreadDialog({
onOpenChange,
onSave,
}: RenameThreadDialogProps) {
const [value, setValue] = useState(currentTitle);
const [value, setValue] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
if (!open) {
setIsSaving(false);
return;
}
setValue(currentTitle);
if (!open) return;
const frame = window.requestAnimationFrame(() => {
inputRef.current?.focus();
inputRef.current?.select();
});
return () => {
window.cancelAnimationFrame(frame);
};
}, [open, currentTitle]);
}, [open]);

const trimmed = value.trim();
const inputValue = value ?? currentTitle;
const trimmed = inputValue.trim();
const canSave = trimmed.length > 0 && !isSaving;

const closeDialog = () => {
setValue(null);
setIsSaving(false);
onOpenChange(false);
};

const handleSubmit = async () => {
if (!canSave) return;
setIsSaving(true);
try {
await onSave(trimmed);
onOpenChange(false);
closeDialog();
} catch {
setIsSaving(false);
}
};

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<Dialog
open={open}
onOpenChange={(nextOpen) => (nextOpen ? onOpenChange(true) : closeDialog())}
>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<DialogPopup className="max-w-md">
<DialogHeader>
<DialogTitle>Rename chat</DialogTitle>
Expand All @@ -74,20 +80,20 @@ export function RenameThreadDialog({
<Input
ref={inputRef}
size="lg"
value={value}
value={inputValue}
disabled={isSaving}
onChange={(event) => setValue(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Escape") {
event.preventDefault();
onOpenChange(false);
closeDialog();
}
}}
/>
</form>
</DialogPanel>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isSaving}>
<Button variant="outline" onClick={closeDialog} disabled={isSaving}>
Cancel
</Button>
<Button onClick={() => void handleSubmit()} disabled={!canSave}>
Expand Down
Loading
Loading