diff --git a/src/main/ai/branch-name.ts b/src/main/ai/branch-name.ts index e18ea49..fbb4dcd 100644 --- a/src/main/ai/branch-name.ts +++ b/src/main/ai/branch-name.ts @@ -67,7 +67,12 @@ export function slugFromModelOutput(text: string, taken: Iterable = []): // segment ending in '.lock'. Empty segments (from stripped junk) drop out. slug = slug .split('/') - .map((s) => s.replace(/(\.lock)+$/, '').replace(/^[-.]+/, '').replace(/[-.]+$/, '')) + .map((s) => + s + .replace(/(\.lock)+$/, '') + .replace(/^[-.]+/, '') + .replace(/[-.]+$/, '') + ) .filter(Boolean) .join('/') @@ -123,10 +128,7 @@ export interface BranchNameContext { export async function gatherBranchNameContext(repoPath: string): Promise { // Both reads are lock-free and independent — overlap them. - const [snapshot, branches] = await Promise.all([ - getRepoSnapshot(repoPath), - getBranches(repoPath) - ]) + const [snapshot, branches] = await Promise.all([getRepoSnapshot(repoPath), getBranches(repoPath)]) const pieces: string[] = [] let budget = BRANCH_NAME_CAPS.totalBytes diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index dfb9e80..f97cbb3 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -17,7 +17,7 @@ import type { SyncStatus, UndoSnapshot } from '@shared/types' -import { type CSSProperties, useCallback, useEffect, useRef, useState } from 'react' +import { type CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { AboutDialog } from './components/app/AboutDialog' import { AppModals, type Modal } from './components/app/AppModals' import { CloneDialog } from './components/app/CloneDialog' @@ -392,6 +392,14 @@ export function App() { resetRange } = useBranchRange({ getRepoPath, fail, loadRangeDiff, clearDiff }) + // The Graph's lit-up container, identified by (name, tip) — a tip hash + // alone is ambiguous: empty branches share it with their anchor's chain. + // Memoized so the canvas' redraw trigger only fires when it truly changes. + const selectedGraphBranch = useMemo( + () => (branchRange ? { name: branchRange.name, tipHash: branchRange.head } : null), + [branchRange] + ) + /** Select a commit, dismissing any open branch-changes view. */ const selectCommitOnly = useCallback( (commit: Commit) => { @@ -1609,7 +1617,7 @@ export function App() { clearDiff() } }} - selectedBranchTip={branchRange?.head ?? null} + selectedBranch={selectedGraphBranch} onSelectBranch={(row) => { resetDetail() clearDiff() diff --git a/src/renderer/src/components/app/CreateBranchDialog.tsx b/src/renderer/src/components/app/CreateBranchDialog.tsx index ccb3b8e..3f70b50 100644 --- a/src/renderer/src/components/app/CreateBranchDialog.tsx +++ b/src/renderer/src/components/app/CreateBranchDialog.tsx @@ -101,11 +101,11 @@ export function CreateBranchDialog({ // field greets the user with a name already forming. Only when a backend is // connected, there are changes to name from, and no caller-provided name. const autoRan = useRef(false) + // biome-ignore lint/correctness/useExhaustiveDependencies: fire once when the status resolves useEffect(() => { if (autoRan.current || !aiStatus || !canSuggest || initialName) return autoRan.current = true suggest() - // biome-ignore lint/correctness/useExhaustiveDependencies: fire once when the status resolves }, [aiStatus]) // The ghost shows while the field is empty: the streaming raw text as it diff --git a/src/renderer/src/components/app/ErrorDialog.tsx b/src/renderer/src/components/app/ErrorDialog.tsx index b91b66c..9872589 100644 --- a/src/renderer/src/components/app/ErrorDialog.tsx +++ b/src/renderer/src/components/app/ErrorDialog.tsx @@ -47,9 +47,7 @@ export function ErrorDialog({ message, info, ai, onClose }: Props) { const [teaserOpen, setTeaserOpen] = useState(false) const [noteOpen, setNoteOpen] = useState(false) const explainRef = useRef(null) - const explanation = useAiGeneration((e) => - setFailure(e instanceof Error ? e.message : String(e)) - ) + const explanation = useAiGeneration((e) => setFailure(e instanceof Error ? e.message : String(e))) const copy = () => { window.gitgrove.clipboardWrite(message) diff --git a/src/renderer/src/components/common/AiExplainCommit.tsx b/src/renderer/src/components/common/AiExplainCommit.tsx index 62981d1..93bceac 100644 --- a/src/renderer/src/components/common/AiExplainCommit.tsx +++ b/src/renderer/src/components/common/AiExplainCommit.tsx @@ -34,9 +34,7 @@ export function useAiExplainCommit(args: AiExplainArgs | null): { const [failure, setFailure] = useState(null) const [teaserOpen, setTeaserOpen] = useState(false) const chipRef = useRef(null) - const explanation = useAiGeneration((e) => - setFailure(e instanceof Error ? e.message : String(e)) - ) + const explanation = useAiGeneration((e) => setFailure(e instanceof Error ? e.message : String(e))) const hash = args?.hash // Some hosts remount per commit (keyed by hash), some don't (the Graph's diff --git a/src/renderer/src/components/common/AiTeaserPopover.tsx b/src/renderer/src/components/common/AiTeaserPopover.tsx index 0abf6a5..1d86f35 100644 --- a/src/renderer/src/components/common/AiTeaserPopover.tsx +++ b/src/renderer/src/components/common/AiTeaserPopover.tsx @@ -26,8 +26,8 @@ export function AiTeaserPopover({ anchor, open, onClose, title, body, onSetup }: {title}

- {body} Connect OpenAI, Anthropic, Gemini, a local Ollama or any compatible endpoint — - your key, sent only to your provider. + {body} Connect OpenAI, Anthropic, Gemini, a local Ollama or any compatible endpoint — your + key, sent only to your provider.