diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 720694cfa..dea8369af 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -8139,6 +8139,7 @@ export default function ChatView({ { - 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) => { diff --git a/apps/web/src/components/ProjectScriptsControl.tsx b/apps/web/src/components/ProjectScriptsControl.tsx index 7f6194f3f..1679e2c3d 100644 --- a/apps/web/src/components/ProjectScriptsControl.tsx +++ b/apps/web/src/components/ProjectScriptsControl.tsx @@ -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(); diff --git a/apps/web/src/components/ProjectSidebarIcon.tsx b/apps/web/src/components/ProjectSidebarIcon.tsx index 61bfd4b09..9954d7e96 100644 --- a/apps/web/src/components/ProjectSidebarIcon.tsx +++ b/apps/web/src/components/ProjectSidebarIcon.tsx @@ -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 ( + + + ); + } + + return ( + + ); +} + +function ProjectFolderIcon({ + className, + faviconSrc, + FolderGlyph, +}: { + className: string; + faviconSrc: string; + FolderGlyph: typeof HiOutlineFolderOpen; +}) { const [hasFavicon, setHasFavicon] = useState( - () => 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 . useEffect(() => { - if (!shouldUseFavicon) { - setHasFavicon(false); - return; - } - - const cached = projectFaviconPresence.get(faviconSrc); - if (cached !== undefined) { - setHasFavicon(cached); + if (projectFaviconPresence.has(faviconSrc)) { return; } @@ -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 ( - - - ); - } + }, [faviconSrc]); return ( - <> + ); } diff --git a/apps/web/src/components/PullRequestThreadDialog.tsx b/apps/web/src/components/PullRequestThreadDialog.tsx index e61237a8a..762c75e6f 100644 --- a/apps/web/src/components/PullRequestThreadDialog.tsx +++ b/apps/web/src/components/PullRequestThreadDialog.tsx @@ -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, @@ -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(() => { @@ -79,7 +72,7 @@ export function PullRequestThreadDialog({ reference: open ? parsedDebouncedReference : null, }), ); - const cachedPullRequest = useMemo(() => { + const cachedPullRequest = (() => { if (!cwd || !parsedReference) { return null; } @@ -90,7 +83,7 @@ export function PullRequestThreadDialog({ parsedReference, ]); return cached?.pullRequest ?? null; - }, [cwd, parsedReference, queryClient]); + })(); const preparePullRequestThreadMutation = useMutation( gitPreparePullRequestThreadMutationOptions({ cwd, queryClient }), ); @@ -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)]"; @@ -119,42 +112,33 @@ 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 { + setPreparingMode(null); + } + }; const validationMessage = !referenceDirty ? null diff --git a/apps/web/src/components/RenameThreadDialog.tsx b/apps/web/src/components/RenameThreadDialog.tsx index b844a7755..f3bf0bf1a 100644 --- a/apps/web/src/components/RenameThreadDialog.tsx +++ b/apps/web/src/components/RenameThreadDialog.tsx @@ -24,16 +24,12 @@ export function RenameThreadDialog({ onOpenChange, onSave, }: RenameThreadDialogProps) { - const [value, setValue] = useState(currentTitle); + const [value, setValue] = useState(null); const [isSaving, setIsSaving] = useState(false); const inputRef = useRef(null); useEffect(() => { - if (!open) { - setIsSaving(false); - return; - } - setValue(currentTitle); + if (!open) return; const frame = window.requestAnimationFrame(() => { inputRef.current?.focus(); inputRef.current?.select(); @@ -41,24 +37,39 @@ export function RenameThreadDialog({ 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 requestCloseDialog = () => { + if (isSaving) return; + closeDialog(); + }; + const handleSubmit = async () => { if (!canSave) return; setIsSaving(true); try { await onSave(trimmed); - onOpenChange(false); + closeDialog(); } catch { setIsSaving(false); } }; return ( - + (nextOpen ? onOpenChange(true) : requestCloseDialog())} + > Rename chat @@ -74,20 +85,20 @@ export function RenameThreadDialog({ setValue(event.target.value)} onKeyDown={(event) => { if (event.key === "Escape") { event.preventDefault(); - onOpenChange(false); + requestCloseDialog(); } }} /> -