feat: massive chat reliability + UX pass (web/desktop/mobile/vscode) (#593)

## Added Features
- Add VS Code save-as-image flow for assistant messages via webview bridge + native save dialog.
- Add hourly desktop update checks after startup.
- Add new tool output display mode: `Changes` (auto-expand edit/write/patch only; keep activity expanded; mode guidance text).
- Add GitHub PR attachment flow in chat input with PR picker + attached PR chip/details.
- Add mobile overlay presentation for GitHub Issue and PR pickers (shared with desktop picker content).

## Fixes
- Save-gate project icon updates until explicit Save; allow icon removal with same save-gated behavior.
- Restore clickable chat action buttons in sticky header mode (desktop + Firefox hit-target issue).
- Clamp sticky user messages to bounded chat height and allow internal scrolling.
- Prevent drawer context crash during iPad/tablet orientation switching.
- Improve text-selection action menu placement on narrow screens.
- Move assistant message time into clock tooltip; keep duration display clean.
- Hide `Link GitHub Issue` row in VS Code chat input area (GitHub flow is not yet ready there).
- Remove laggy close animation in text-selection popover; keep open motion/positioning behavior.
- Fetch branches when picker opens and cache empty; show loading state instead of false “No branches found”.
- Fix share-image export metadata rendering (theme background resolution, timestamp rendering, footer alignment).
- Scope MCP services status/toggles to active directory to avoid cross-project leakage.
- Improve long user-message clamp behavior (40% cap variant, hidden scrollbar, scroll shadows, expansion detection).
- Fix desktop `Check for Updates` menu handler; prevent duplicate checks; show clear success/error toasts.
- Stabilize long user-message scrolling behavior (follow-up hardening).
- Avoid premature web update failure on slower servers.
- Restore user message image previews + fullscreen gallery navigation payload.
- Repair desktop chat drag-and-drop image attachments when native drop coords are missing.
- Move GitHub issue linking entry into Add attachment menu.
- Align header context usage percentage visuals with context panel.
- Align `@` file search with active project in all runtimes.
- Route `@` file discovery through OpenCode SDK `find.files`; remove legacy `/api/fs/search` reliance.
- Make chat `@` mention behavior consistent with files-style behavior.
- Keep status-row todos in stable order after status changes; add compact status icons; replace noisy priority labels.

## Refactors / UX Consistency
- Simplify chat attachment model and remove project file picker path.
- Keep composer focused on `@` mention file flow.
- Use direct `Attach files` action in VS Code instead of attachment dropdown path.
- Unify issue/PR picker behavior between desktop and mobile overlays.
This commit is contained in:
Bohdan Triapitsyn
2026-03-04 01:41:01 +02:00
committed by GitHub
parent ca18b8be0f
commit 79143bff4c
42 changed files with 2212 additions and 1477 deletions
@@ -8,6 +8,7 @@ import {
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { toast } from '@/components/ui';
import {
RiCheckboxBlankLine,
@@ -79,6 +80,7 @@ export function GitHubIssuePickerDialog({
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
const isMobile = useUIStore((state) => state.isMobile);
const activeProject = useProjectsStore((state) => state.getActiveProject());
const projectDirectory = activeProject?.path ?? null;
@@ -512,32 +514,24 @@ Do not implement changes until I confirm; end with: “Next actions: <1 sentence
}
}, [createInWorktree, github, mode, onOpenChange, onSelect, projectDirectory, resolveDefaultAgentName, resolveDefaultModelSelection, resolveDefaultVariant, startingIssueNumber]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[70vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<RiGithubLine className="h-5 w-5" />
{mode === 'select' ? 'Link GitHub Issue' : 'New Session From GitHub Issue'}
</DialogTitle>
<DialogDescription>
{mode === 'select'
? 'Select an issue to link to this session.'
: 'Seeds a new session with hidden issue context (title/body/labels/comments).'}
</DialogDescription>
</DialogHeader>
const title = mode === 'select' ? 'Link GitHub Issue' : 'New Session From GitHub Issue';
const description = mode === 'select'
? 'Select an issue to link to this session.'
: 'Seeds a new session with hidden issue context (title/body/labels/comments).';
<div className="relative mt-2">
<RiSearchLine className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by title or #123, or paste issue URL"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-9 w-full"
/>
</div>
const content = (
<>
<div className="relative mt-2">
<RiSearchLine className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by title or #123, or paste issue URL"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-9 w-full"
/>
</div>
<div className="flex-1 overflow-y-auto mt-2">
<div className={cn(isMobile ? 'min-h-0 mt-2' : 'flex-1 overflow-y-auto mt-2')}>
{!projectDirectory ? (
<div className="text-center text-muted-foreground py-8">No active project selected.</div>
) : null}
@@ -649,9 +643,9 @@ Do not implement changes until I confirm; end with: “Next actions: <1 sentence
</button>
</div>
) : null}
</div>
</div>
{mode !== 'select' && (
{mode !== 'select' && (
<div className="mt-4 p-3 bg-muted/30 rounded-lg">
<p className="typography-meta text-muted-foreground font-medium mb-2">Actions</p>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-2">
@@ -703,7 +697,45 @@ Do not implement changes until I confirm; end with: “Next actions: <1 sentence
</div>
</div>
</div>
)}
</>
);
if (isMobile) {
return (
<MobileOverlayPanel
open={open}
title={title}
onClose={() => onOpenChange(false)}
renderHeader={(closeButton) => (
<div className="flex flex-col gap-1.5 px-3 py-2 border-b border-border/40">
<div className="flex items-center justify-between">
<h2 className="typography-ui-label font-semibold text-foreground">{title}</h2>
{closeButton}
</div>
<p className="typography-small text-muted-foreground">{description}</p>
</div>
)}
>
{content}
</MobileOverlayPanel>
);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[70vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<RiGithubLine className="h-5 w-5" />
{title}
</DialogTitle>
<DialogDescription>
{description}
</DialogDescription>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
);
@@ -0,0 +1,489 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { toast } from '@/components/ui';
import {
RiGithubLine,
RiLoader4Line,
RiSearchLine,
RiExternalLinkLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useUIStore } from '@/stores/useUIStore';
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
import type { GitHubPullRequestContextResult, GitHubPullRequestSummary, GitHubPullRequestsListResult } from '@/lib/api/types';
const parsePrNumber = (value: string): number | null => {
const trimmed = value.trim();
if (!trimmed) return null;
const urlMatch = trimmed.match(/\/pull\/(\d+)(?:\b|\/|$)/i);
if (urlMatch) {
const parsed = Number(urlMatch[1]);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
const hashMatch = trimmed.match(/^#?(\d+)$/);
if (hashMatch) {
const parsed = Number(hashMatch[1]);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
return null;
};
const buildPullRequestContextText = (payload: GitHubPullRequestContextResult) => {
return `GitHub pull request context (JSON)\n${JSON.stringify(payload, null, 2)}`;
};
const PR_REVIEW_INSTRUCTIONS = `Before reporting issues:
- First identify the PR intent (what it's trying to achieve) from title/body/diff, then evaluate whether the implementation matches that intent; call out missing pieces, incorrect behavior vs intent, and scope creep.
- Gather any needed repository context (code, config, docs) to validate assumptions.
- No speculation: if something is unclear or cannot be verified, say what's missing and ask for it instead of guessing.
Output rules:
- Start with a 1-2 sentence summary.
- Provide a single concise PR review comment.
- No emojis. No code snippets. No fenced blocks.
- Short inline code identifiers allowed, but no snippets or fenced blocks.
- Reference evidence with file paths and line ranges (e.g., path/to/file.ts:120-138). If exact lines aren't available, cite the file and say "approx" + why.
- Keep the entire comment under ~300 words.
Report:
- Must-fix issues (blocking)-brief why and a one-line action each.
- Nice-to-have improvements (optional)-brief why and a one-line action each.
Quality & safety (general):
- Call out correctness risks, edge cases, performance regressions, security/privacy concerns, and backwards-compatibility risks.
- Call out missing tests/verification steps and suggest the minimal validation needed.
- Note readability/maintainability issues when they materially affect future changes.
Applicability (only if relevant):
- If changes affect multiple components/targets/environments (e.g., client/server, OSs, deployments), state what is affected vs not, and why.
Architecture:
- Call out breakages, missing implementations across modules/targets, boundary violations, and cross-cutting concerns (errors, logging/observability, accessibility).
Precedence:
- If local precedent conflicts with best practices, state it and suggest a follow-up task.
Do not implement changes until I confirm; end with a short "Next actions" sentence describing the recommended plan.
Format exactly:
Must-fix:
- <issue> - <brief why> - <file:line-range> - Action: <one-line action>
Nice-to-have:
- <issue> - <brief why> - <file:line-range> - Action: <one-line action>
If no issues, write:
Must-fix:
- None
Nice-to-have:
- None`;
export function GitHubPrPickerDialog({
open,
onOpenChange,
onSelect,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
onSelect?: (pr: {
number: number;
title: string;
url: string;
head: string;
base: string;
includeDiff: boolean;
instructionsText: string;
contextText: string;
author?: { login: string; avatarUrl?: string };
}) => void;
}) {
const { github } = useRuntimeAPIs();
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
const isMobile = useUIStore((state) => state.isMobile);
const activeProject = useProjectsStore((state) => state.getActiveProject());
const projectDirectory = activeProject?.path ?? null;
const [query, setQuery] = React.useState('');
const [includeDiff, setIncludeDiff] = React.useState(false);
const [result, setResult] = React.useState<GitHubPullRequestsListResult | null>(null);
const [prs, setPrs] = React.useState<GitHubPullRequestSummary[]>([]);
const [page, setPage] = React.useState(1);
const [hasMore, setHasMore] = React.useState(false);
const [loadingPrNumber, setLoadingPrNumber] = React.useState<number | null>(null);
const [isLoading, setIsLoading] = React.useState(false);
const [isLoadingMore, setIsLoadingMore] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const refresh = React.useCallback(async () => {
if (!projectDirectory) {
setResult(null);
setError('No active project');
return;
}
if (githubAuthChecked && githubAuthStatus?.connected === false) {
setResult({ connected: false });
setPrs([]);
setHasMore(false);
setPage(1);
setError(null);
return;
}
if (!github?.prsList) {
setResult(null);
setError('GitHub runtime API unavailable');
return;
}
setIsLoading(true);
setError(null);
try {
const next = await github.prsList(projectDirectory, { page: 1 });
setResult(next);
setPrs(next.prs ?? []);
setPage(next.page ?? 1);
setHasMore(Boolean(next.hasMore));
if (next.connected === false) {
setError(null);
}
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setIsLoading(false);
}
}, [github, githubAuthChecked, githubAuthStatus, projectDirectory]);
const loadMore = React.useCallback(async () => {
if (!projectDirectory) return;
if (!github?.prsList) return;
if (isLoadingMore || isLoading) return;
if (!hasMore) return;
setIsLoadingMore(true);
try {
const nextPage = page + 1;
const next = await github.prsList(projectDirectory, { page: nextPage });
setResult(next);
setPrs((prev) => [...prev, ...(next.prs ?? [])]);
setPage(next.page ?? nextPage);
setHasMore(Boolean(next.hasMore));
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error('Failed to load more pull requests', { description: message });
} finally {
setIsLoadingMore(false);
}
}, [github, hasMore, isLoading, isLoadingMore, page, projectDirectory]);
React.useEffect(() => {
if (!open) {
setQuery('');
setIncludeDiff(false);
setLoadingPrNumber(null);
setError(null);
setResult(null);
setPrs([]);
setPage(1);
setHasMore(false);
setIsLoading(false);
return;
}
void refresh();
}, [open, refresh]);
React.useEffect(() => {
if (!open) return;
if (githubAuthChecked && githubAuthStatus?.connected === false) {
setResult({ connected: false });
setPrs([]);
setHasMore(false);
setPage(1);
setError(null);
}
}, [githubAuthChecked, githubAuthStatus, open]);
const connected = githubAuthChecked ? result?.connected !== false : true;
const openGitHubSettings = React.useCallback(() => {
setSettingsPage('github');
setSettingsDialogOpen(true);
}, [setSettingsDialogOpen, setSettingsPage]);
const filtered = React.useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return prs;
return prs.filter((pr) => {
if (String(pr.number) === q.replace(/^#/, '')) return true;
return pr.title.toLowerCase().includes(q);
});
}, [prs, query]);
const directNumber = React.useMemo(() => parsePrNumber(query), [query]);
const attachPr = React.useCallback(async (prNumber: number) => {
if (!projectDirectory) {
toast.error('No active project');
return;
}
if (!github?.prContext) {
toast.error('GitHub runtime API unavailable');
return;
}
if (loadingPrNumber) return;
setLoadingPrNumber(prNumber);
try {
const context = await github.prContext(projectDirectory, prNumber, {
includeDiff,
includeCheckDetails: false,
});
if (context.connected === false) {
toast.error('GitHub not connected');
return;
}
if (!context.pr) {
toast.error('Pull request not found');
return;
}
if (!context.repo) {
toast.error('Repo not resolvable', {
description: 'origin remote must be a GitHub URL',
});
return;
}
if (onSelect) {
onSelect({
number: context.pr.number,
title: context.pr.title,
url: context.pr.url,
head: context.pr.head,
base: context.pr.base,
includeDiff,
instructionsText: PR_REVIEW_INSTRUCTIONS,
contextText: buildPullRequestContextText(context),
author: context.pr.author
? {
login: context.pr.author.login,
avatarUrl: context.pr.author.avatarUrl,
}
: undefined,
});
}
onOpenChange(false);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error('Failed to load pull request details', { description: message });
} finally {
setLoadingPrNumber(null);
}
}, [github, includeDiff, loadingPrNumber, onOpenChange, onSelect, projectDirectory]);
const title = 'Link GitHub Pull Request';
const description = 'Select a pull request to attach review context to this message.';
const content = (
<>
<div className="mt-2 flex items-center gap-3">
<div className="relative flex-1 min-w-0">
<RiSearchLine className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by title or #123, or paste pull request URL"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-9 w-full"
/>
</div>
<button
type="button"
onClick={() => setIncludeDiff((prev) => !prev)}
className="h-9 shrink-0 flex items-center gap-1 text-left"
aria-pressed={includeDiff}
aria-label="Include PR diff in attached context"
>
<Checkbox
checked={includeDiff}
onChange={(checked) => setIncludeDiff(checked)}
ariaLabel="Include PR diff in attached context"
className="size-6"
iconClassName="size-5"
/>
<span className="typography-small text-muted-foreground whitespace-nowrap">Include PR diff</span>
</button>
</div>
<div className={cn(isMobile ? 'min-h-0' : 'flex-1 overflow-y-auto')}>
{!projectDirectory ? (
<div className="text-center text-muted-foreground py-8">No active project selected.</div>
) : null}
{!github ? (
<div className="text-center text-muted-foreground py-8">GitHub runtime API unavailable.</div>
) : null}
{isLoading ? (
<div className="text-center text-muted-foreground py-8 flex items-center justify-center gap-2">
<RiLoader4Line className="h-4 w-4 animate-spin" />
Loading pull requests...
</div>
) : null}
{connected === false ? (
<div className="text-center text-muted-foreground py-8 space-y-3">
<div>GitHub not connected. Connect your GitHub account in settings.</div>
<div className="flex justify-center">
<Button variant="outline" size="sm" onClick={openGitHubSettings}>
Open settings
</Button>
</div>
</div>
) : null}
{error ? (
<div className="text-center text-muted-foreground py-8 break-words">{error}</div>
) : null}
{directNumber && projectDirectory && github && connected ? (
<div
className={cn(
'group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer',
loadingPrNumber === directNumber && 'bg-interactive-selection/30'
)}
onClick={() => void attachPr(directNumber)}
>
<span className="typography-meta text-muted-foreground w-5 text-right flex-shrink-0">#</span>
<p className="flex-1 min-w-0 typography-small text-foreground truncate ml-0.5">
Use pull request #{directNumber}
</p>
<div className="flex-shrink-0 h-5 flex items-center mr-2">
{loadingPrNumber === directNumber ? (
<RiLoader4Line className="h-4 w-4 animate-spin text-muted-foreground" />
) : null}
</div>
</div>
) : null}
{filtered.length === 0 && !isLoading && connected && github && projectDirectory ? (
<div className="text-center text-muted-foreground py-8">{query ? 'No pull requests found' : 'No open pull requests found'}</div>
) : null}
{filtered.map((pr) => (
<div
key={pr.number}
className={cn(
'group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer',
loadingPrNumber === pr.number && 'bg-interactive-selection/30'
)}
onClick={() => void attachPr(pr.number)}
>
<div className="flex-1 min-w-0 ml-0.5">
<p className="typography-small text-foreground truncate">
<span className="text-muted-foreground mr-1">#{pr.number}</span>
{pr.title}
</p>
<p className="typography-meta text-muted-foreground truncate">{pr.head} {pr.base}</p>
</div>
<div className="flex-shrink-0 h-5 flex items-center mr-2">
{loadingPrNumber === pr.number ? (
<RiLoader4Line className="h-4 w-4 animate-spin text-muted-foreground" />
) : (
<a
href={pr.url}
target="_blank"
rel="noopener noreferrer"
className="hidden group-hover:flex h-5 w-5 items-center justify-center text-muted-foreground hover:text-foreground transition-colors"
onClick={(e) => e.stopPropagation()}
aria-label="Open in GitHub"
>
<RiExternalLinkLine className="h-4 w-4" />
</a>
)}
</div>
</div>
))}
{hasMore && connected && projectDirectory && github ? (
<div className="py-2 flex justify-center">
<button
type="button"
onClick={() => void loadMore()}
disabled={isLoadingMore || Boolean(loadingPrNumber)}
className={cn(
'typography-meta text-muted-foreground hover:text-foreground transition-colors underline underline-offset-4',
(isLoadingMore || Boolean(loadingPrNumber)) && 'opacity-50 cursor-not-allowed hover:text-muted-foreground'
)}
>
{isLoadingMore ? (
<span className="inline-flex items-center gap-2">
<RiLoader4Line className="h-4 w-4 animate-spin" />
Loading...
</span>
) : (
'Load more'
)}
</button>
</div>
) : null}
</div>
</>
);
if (isMobile) {
return (
<MobileOverlayPanel
open={open}
title={title}
onClose={() => onOpenChange(false)}
renderHeader={(closeButton) => (
<div className="flex flex-col gap-1.5 px-3 py-2 border-b border-border/40">
<div className="flex items-center justify-between">
<h2 className="typography-ui-label font-semibold text-foreground">{title}</h2>
{closeButton}
</div>
<p className="typography-small text-muted-foreground">{description}</p>
</div>
)}
>
{content}
</MobileOverlayPanel>
);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[70vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<RiGithubLine className="h-5 w-5" />
{title}
</DialogTitle>
<DialogDescription>
{description}
</DialogDescription>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
);
}
@@ -45,7 +45,7 @@ import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
import { generateBranchSlug } from '@/lib/git/branchNameGenerator';
import { opencodeClient } from '@/lib/opencode/client';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useGitBranches } from '@/stores/useGitStore';
import { useGitBranches, useGitStore } from '@/stores/useGitStore';
import { GitHubIntegrationDialog } from './GitHubIntegrationDialog';
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
@@ -195,7 +195,7 @@ export function NewWorktreeDialog({
onOpenChange,
onWorktreeCreated,
}: NewWorktreeDialogProps) {
const { github } = useRuntimeAPIs();
const { github, git } = useRuntimeAPIs();
const isMobile = useUIStore((state) => state.isMobile);
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
@@ -230,6 +230,14 @@ export function NewWorktreeDialog({
// Use cached branches from Git store (instant if already fetched)
const branches = useGitBranches(projectDirectory);
const isLoadingBranches = useGitStore((state) => state.isLoadingBranches);
const fetchBranches = useGitStore((state) => state.fetchBranches);
React.useEffect(() => {
if (!open || !projectDirectory || !git) return;
if (branches?.all) return;
void fetchBranches(projectDirectory, git);
}, [open, projectDirectory, git, branches?.all, fetchBranches]);
// Compute local and remote branch lists (same pattern as GitView)
const localBranches = React.useMemo(() => {
@@ -1052,7 +1060,11 @@ Nice-to-have:
onClose={() => setExistingBranchPickerOpen(false)}
>
<div className="space-y-4">
{localBranches.length === 0 && remoteBranches.length === 0 ? (
{isLoadingBranches ? (
<div className="px-2 py-8 text-center typography-small text-muted-foreground">
Loading branches...
</div>
) : localBranches.length === 0 && remoteBranches.length === 0 ? (
<div className="px-2 py-8 text-center typography-small text-muted-foreground">
No branches found
</div>
@@ -1267,7 +1279,11 @@ Nice-to-have:
onClose={() => setSourceBranchPickerOpen(false)}
>
<div className="space-y-4">
{localBranches.length === 0 && remoteBranches.length === 0 ? (
{isLoadingBranches ? (
<div className="px-2 py-8 text-center typography-small text-muted-foreground">
Loading branches...
</div>
) : localBranches.length === 0 && remoteBranches.length === 0 ? (
<div className="px-2 py-8 text-center typography-small text-muted-foreground">
No branches found
</div>
@@ -1438,7 +1454,11 @@ Nice-to-have:
<SelectValue placeholder="Choose a branch..." />
</SelectTrigger>
<SelectContent className="max-h-[280px] max-w-[320px]">
{localBranches.length === 0 && remoteBranches.length === 0 ? (
{isLoadingBranches ? (
<div className="px-2 py-4 text-center typography-small text-muted-foreground">
Loading branches...
</div>
) : localBranches.length === 0 && remoteBranches.length === 0 ? (
<div className="px-2 py-4 text-center typography-small text-muted-foreground">
No branches found
</div>
@@ -1598,7 +1618,11 @@ Nice-to-have:
<SelectValue placeholder="Select source branch..." />
</SelectTrigger>
<SelectContent className="max-h-[280px] max-w-[320px]">
{localBranches.length === 0 && remoteBranches.length === 0 ? (
{isLoadingBranches ? (
<div className="px-2 py-4 text-center typography-small text-muted-foreground">
Loading branches...
</div>
) : localBranches.length === 0 && remoteBranches.length === 0 ? (
<div className="px-2 py-4 text-center typography-small text-muted-foreground">
No branches found
</div>