Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
160 changes: 74 additions & 86 deletions apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -953,106 +953,94 @@ export default function GitActionsControl({
normalizedCreateBranchName !== normalizedCurrentBranchName &&
branchNames.has(normalizedCreateBranchName);

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

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

setIsCreateBranchDialogOpen(false);
setCreateBranchName("");

if (trimmedName.toLowerCase() === normalizedCurrentBranchName) {
if (activeThreadId) {
void api.orchestration
.dispatchCommand({
type: "thread.meta.update",
commandId: newCommandId(),
threadId: activeThreadId,
createBranchFlowCompleted: true,
})
.catch(() => {
setThreadWorkspaceAction(activeThreadId, {
createBranchFlowCompleted: false,
});
});
setThreadWorkspaceAction(activeThreadId, {
const createAndCheckoutBranch = async (branchName: string) => {
const api = readNativeApi();
if (!api || !gitCwd) return;

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

setIsCreateBranchDialogOpen(false);
setCreateBranchName("");

if (trimmedName.toLowerCase() === normalizedCurrentBranchName) {
if (activeThreadId) {
void api.orchestration
.dispatchCommand({
type: "thread.meta.update",
commandId: newCommandId(),
threadId: activeThreadId,
createBranchFlowCompleted: true,
})
.catch(() => {
setThreadWorkspaceAction(activeThreadId, {
createBranchFlowCompleted: false,
});
});
}
toastManager.add({
type: "success",
title: `Keeping ${trimmedName}`,
description: "Branch name confirmed.",
data: threadToastData,
setThreadWorkspaceAction(activeThreadId, {
createBranchFlowCompleted: true,
});
return;
}

const toastId = toastManager.add({
type: "loading",
title: "Creating branch...",
timeout: 0,
toastManager.add({
type: "success",
title: `Keeping ${trimmedName}`,
description: "Branch name confirmed.",
data: threadToastData,
});
return;
}

try {
await api.git.createBranch({ cwd: gitCwd, branch: trimmedName, publish: hasOriginRemote });
await api.git.checkout({ cwd: gitCwd, branch: trimmedName });
if (activeThreadId) {
void api.orchestration
.dispatchCommand({
type: "thread.meta.update",
commandId: newCommandId(),
threadId: activeThreadId,
branch: trimmedName,
worktreePath: activeThread?.worktreePath ?? null,
associatedWorktreeBranch: trimmedName,
associatedWorktreeRef: trimmedName,
createBranchFlowCompleted: true,
})
.catch(() => {
setThreadWorkspaceAction(activeThreadId, {
createBranchFlowCompleted: false,
});
});
setThreadWorkspaceAction(activeThreadId, {
const toastId = toastManager.add({
type: "loading",
title: "Creating branch...",
timeout: 0,
data: threadToastData,
});

try {
await api.git.createBranch({ cwd: gitCwd, branch: trimmedName, publish: hasOriginRemote });
await api.git.checkout({ cwd: gitCwd, branch: trimmedName });
if (activeThreadId) {
void api.orchestration
.dispatchCommand({
type: "thread.meta.update",
commandId: newCommandId(),
threadId: activeThreadId,
branch: trimmedName,
worktreePath: activeThread?.worktreePath ?? null,
associatedWorktreeBranch: trimmedName,
associatedWorktreeRef: trimmedName,
createBranchFlowCompleted: true,
})
.catch(() => {
setThreadWorkspaceAction(activeThreadId, {
createBranchFlowCompleted: false,
});
});
}
await invalidateGitQueries(queryClient);

toastManager.update(toastId, {
type: "success",
title: `Switched to ${trimmedName}`,
description: "Branch created and checked out.",
data: threadToastData,
});
} catch (error) {
toastManager.update(toastId, {
type: "error",
title: "Failed to create branch",
description: error instanceof Error ? error.message : "An error occurred.",
data: threadToastData,
setThreadWorkspaceAction(activeThreadId, {
branch: trimmedName,
associatedWorktreeBranch: trimmedName,
associatedWorktreeRef: trimmedName,
createBranchFlowCompleted: true,
});
}
},
[
activeThread?.worktreePath,
activeThreadId,
gitCwd,
hasOriginRemote,
normalizedCurrentBranchName,
queryClient,
setThreadWorkspaceAction,
threadToastData,
],
);
await invalidateGitQueries(queryClient);

toastManager.update(toastId, {
type: "success",
title: `Switched to ${trimmedName}`,
description: "Branch created and checked out.",
data: threadToastData,
});
} catch (error) {
toastManager.update(toastId, {
type: "error",
title: "Failed to create branch",
description: error instanceof Error ? error.message : "An error occurred.",
data: threadToastData,
});
}
};

const openDialogForMenuItem = useCallback(
(item: GitActionMenuItem) => {
Expand Down
4 changes: 0 additions & 4 deletions apps/web/src/components/ProjectScriptsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,6 @@ export default function ProjectScriptsControl({
// "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;
lastOpenAddActionNonceRef.current = openAddActionNonce;
openAddDialog();
Expand Down
75 changes: 43 additions & 32 deletions apps/web/src/components/ProjectSidebarIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,51 @@ 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
key={faviconSrc}
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 +159,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 +176,6 @@ export function ProjectSidebarIcon({
}}
/>
) : null}
</>
</span>
);
}
Loading
Loading