Merge branch 'main' into dev

This commit is contained in:
Mohamed Boudra
2026-04-10 14:44:57 +07:00
parent 4176bd8aa1
commit 0deaed3794
22 changed files with 923 additions and 160 deletions
@@ -88,6 +88,7 @@ type ControlledAgentStatusBarProps = {
features?: AgentFeature[];
onSetFeature?: (featureId: string, value: unknown) => void;
onDropdownClose?: () => void;
onModelSelectorOpen?: () => void;
};
export interface DraftAgentStatusBarProps {
@@ -110,6 +111,7 @@ export interface DraftAgentStatusBarProps {
features?: AgentFeature[];
onSetFeature?: (featureId: string, value: unknown) => void;
onDropdownClose?: () => void;
onModelSelectorOpen?: () => void;
disabled?: boolean;
}
@@ -217,6 +219,7 @@ function ControlledStatusBar({
features,
onSetFeature,
onDropdownClose,
onModelSelectorOpen,
}: ControlledAgentStatusBarProps) {
const { theme } = useUnistyles();
const isWeb = Platform.OS === "web";
@@ -411,6 +414,7 @@ function ControlledStatusBar({
onToggleFavorite={onToggleFavoriteModel}
isLoading={isModelLoading}
disabled={modelDisabled}
onOpen={onModelSelectorOpen}
onClose={onDropdownClose}
/>
</View>
@@ -662,6 +666,7 @@ function ControlledStatusBar({
onToggleFavorite={onToggleFavoriteModel}
isLoading={isModelLoading}
disabled={modelDisabled}
onOpen={onModelSelectorOpen}
onClose={onDropdownClose}
renderTrigger={({ selectedModelLabel }) => (
<View
@@ -875,6 +880,7 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
entries: snapshotEntries,
isLoading: snapshotIsLoading,
isFetching: snapshotIsFetching,
invalidate: invalidateSnapshot,
} = useProvidersSnapshot(serverId);
const snapshotModels = useMemo(() => {
@@ -1035,6 +1041,7 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
});
}}
isModelLoading={snapshotIsLoading || snapshotIsFetching}
onModelSelectorOpen={invalidateSnapshot}
onDropdownClose={onDropdownClose}
disabled={!client}
/>
@@ -1061,6 +1068,7 @@ export function DraftAgentStatusBar({
features,
onSetFeature,
onDropdownClose,
onModelSelectorOpen,
disabled = false,
}: DraftAgentStatusBarProps) {
const isWeb = Platform.OS === "web";
@@ -1105,6 +1113,7 @@ export function DraftAgentStatusBar({
}}
isLoading={isAllModelsLoading}
disabled={disabled}
onOpen={onModelSelectorOpen}
onClose={onDropdownClose}
/>
<ControlledStatusBar
@@ -1154,6 +1163,7 @@ export function DraftAgentStatusBar({
onSelectThinkingOption={onSelectThinkingOption}
features={features}
onSetFeature={onSetFeature}
onModelSelectorOpen={onModelSelectorOpen}
disabled={disabled}
/>
</>
@@ -0,0 +1,125 @@
import { useRef } from "react";
import { Pressable, Text, View } from "react-native";
import { ChevronDown, GitBranch } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
interface BranchSwitcherProps {
currentBranchName: string | null;
title: string;
branchOptions: ComboboxOption[];
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onBranchSelect: (branchId: string) => void;
}
export function BranchSwitcher({
currentBranchName,
title,
branchOptions,
isOpen,
onOpenChange,
onBranchSelect,
}: BranchSwitcherProps) {
const { theme } = useUnistyles();
const anchorRef = useRef<View>(null);
if (!currentBranchName) {
return (
<Text
testID="workspace-header-title"
style={styles.headerTitle}
numberOfLines={1}
>
{title}
</Text>
);
}
return (
<View ref={anchorRef} collapsable={false}>
<Pressable
testID="workspace-header-branch-switcher"
onPress={() => onOpenChange(true)}
style={({ hovered, pressed }) => [
styles.branchSwitcherTrigger,
(hovered || pressed) && styles.branchSwitcherTriggerHovered,
]}
accessibilityRole="button"
accessibilityLabel={`Current branch: ${currentBranchName}. Press to switch branch.`}
>
<GitBranch
size={14}
color={theme.colors.foregroundMuted}
/>
<Text
testID="workspace-header-title"
style={styles.headerTitle}
numberOfLines={1}
>
{title}
</Text>
<ChevronDown
size={12}
color={theme.colors.foregroundMuted}
/>
</Pressable>
<Combobox
options={branchOptions}
value={currentBranchName}
onSelect={onBranchSelect}
searchable
placeholder="Switch branch..."
searchPlaceholder="Filter branches..."
emptyText="No branches found."
title="Switch branch"
open={isOpen}
onOpenChange={onOpenChange}
anchorRef={anchorRef}
desktopPlacement="bottom-start"
desktopPreventInitialFlash
desktopMinWidth={280}
renderOption={({ option, selected, active, onPress }) => (
<ComboboxItem
key={option.id}
label={option.label}
selected={selected}
active={active}
onPress={onPress}
leadingSlot={
<GitBranch
size={14}
color={theme.colors.foregroundMuted}
/>
}
/>
)}
/>
</View>
);
}
const styles = StyleSheet.create((theme) => ({
headerTitle: {
fontSize: theme.fontSize.base,
fontWeight: {
xs: "400",
md: "300",
},
color: theme.colors.foreground,
flexShrink: 1,
},
branchSwitcherTrigger: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.md,
flexShrink: 1,
minWidth: 0,
},
branchSwitcherTriggerHovered: {
backgroundColor: theme.colors.surface1,
},
}));
@@ -59,6 +59,7 @@ interface CombinedModelSelectorProps {
disabled: boolean;
isOpen: boolean;
}) => React.ReactNode;
onOpen?: () => void;
onClose?: () => void;
disabled?: boolean;
}
@@ -517,6 +518,7 @@ export function CombinedModelSelector({
favoriteKeys = new Set<string>(),
onToggleFavorite,
renderTrigger,
onOpen,
onClose,
disabled = false,
}: CombinedModelSelectorProps) {
@@ -553,12 +555,14 @@ export function CombinedModelSelector({
(open: boolean) => {
setIsOpen(open);
setView(computeInitialView());
if (!open) {
if (open) {
onOpen?.();
} else {
setSearchQuery("");
onClose?.();
}
},
[onClose, computeInitialView],
[onOpen, onClose, computeInitialView],
);
const handleSelect = useCallback(
+1 -1
View File
@@ -109,6 +109,7 @@ export function ToastViewport({
}) {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const isMobile = useIsCompactFormFactor();
const opacity = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(-8)).current;
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -182,7 +183,6 @@ export function ToastViewport({
return null;
}
const isMobile = useIsCompactFormFactor();
const headerHeight = isMobile ? HEADER_INNER_HEIGHT_MOBILE : HEADER_INNER_HEIGHT;
const headerTopPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
const topOffset =
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
buildVisibleComboboxOptions,
filterAndRankComboboxOptions,
getComboboxFallbackIndex,
orderVisibleComboboxOptions,
} from "./combobox-options";
@@ -47,6 +48,48 @@ describe("buildVisibleComboboxOptions", () => {
});
});
describe("filterAndRankComboboxOptions", () => {
const options = [
{ id: "feat/login", label: "feat/login" },
{ id: "main", label: "main" },
{ id: "feat/main-nav", label: "feat/main-nav" },
{ id: "fix/logout", label: "fix/logout", description: "fixes main logout bug" },
];
it("returns all options when search is empty", () => {
expect(filterAndRankComboboxOptions(options, "")).toEqual(options);
});
it("filters by label substring", () => {
const result = filterAndRankComboboxOptions(options, "login");
expect(result.map((o) => o.id)).toEqual(["feat/login"]);
});
it("filters by id substring", () => {
const result = filterAndRankComboboxOptions(options, "fix/");
expect(result.map((o) => o.id)).toEqual(["fix/logout"]);
});
it("filters by description substring", () => {
const result = filterAndRankComboboxOptions(options, "logout bug");
expect(result.map((o) => o.id)).toEqual(["fix/logout"]);
});
it("ranks prefix matches above substring matches", () => {
const result = filterAndRankComboboxOptions(options, "main");
expect(result.map((o) => o.id)).toEqual(["main", "feat/main-nav", "fix/logout"]);
});
it("is case-insensitive", () => {
const items = [{ id: "Alpha", label: "Alpha" }];
expect(filterAndRankComboboxOptions(items, "alpha")).toHaveLength(1);
});
it("returns empty when nothing matches", () => {
expect(filterAndRankComboboxOptions(options, "zzz")).toEqual([]);
});
});
describe("combobox above-search ordering", () => {
const visible = [
{ id: "/tmp/new-project", label: "/tmp/new-project", kind: "directory" as const },
@@ -35,18 +35,33 @@ export function shouldShowCustomComboboxOption(input: {
);
}
export function filterAndRankComboboxOptions(
options: ComboboxOptionModel[],
search: string,
): ComboboxOptionModel[] {
if (!search) return options;
return options
.filter(
(opt) =>
opt.label.toLowerCase().includes(search) ||
opt.id.toLowerCase().includes(search) ||
opt.description?.toLowerCase().includes(search),
)
.sort((a, b) => {
const aPrefix =
a.label.toLowerCase().startsWith(search) || a.id.toLowerCase().startsWith(search);
const bPrefix =
b.label.toLowerCase().startsWith(search) || b.id.toLowerCase().startsWith(search);
if (aPrefix !== bPrefix) return aPrefix ? -1 : 1;
return 0;
});
}
export function buildVisibleComboboxOptions(
input: BuildVisibleComboboxOptionsInput,
): ComboboxOptionModel[] {
const normalizedSearch = input.searchable ? input.searchQuery.trim().toLowerCase() : "";
const filteredOptions = normalizedSearch
? input.options.filter(
(opt) =>
opt.label.toLowerCase().includes(normalizedSearch) ||
opt.id.toLowerCase().includes(normalizedSearch) ||
opt.description?.toLowerCase().includes(normalizedSearch),
)
: input.options;
const filteredOptions = filterAndRankComboboxOptions(input.options, normalizedSearch);
const sanitizedSearchValue = input.searchQuery.trim();
const showCustomOption = shouldShowCustomComboboxOption({
@@ -93,6 +93,7 @@ export type UseAgentFormStateResult = {
isModelLoading: boolean;
modelError: string | null;
refreshProviderModels: () => void;
invalidateProviderModels: () => void;
setProviderAndModelFromUser: (provider: AgentProvider, modelId: string) => void;
workingDirIsEmpty: boolean;
persistFormPreferences: () => Promise<void>;
@@ -373,6 +374,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
isFetching: snapshotIsFetching,
error: snapshotError,
refresh: refreshSnapshot,
invalidate: invalidateSnapshot,
} = useProvidersSnapshot(formState.serverId);
const allProviderEntries = useMemo(() => snapshotEntries ?? [], [snapshotEntries]);
@@ -646,6 +648,10 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
refreshSnapshot();
}, [refreshSnapshot]);
const invalidateProviderModels = useCallback(() => {
invalidateSnapshot();
}, [invalidateSnapshot]);
const persistFormPreferences = useCallback(async () => {
const resolvedModel = resolveEffectiveModel(availableModels, formState.model);
const modelId = resolvedModel?.id ?? formState.model;
@@ -712,6 +718,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
isModelLoading,
modelError,
refreshProviderModels,
invalidateProviderModels,
setProviderAndModelFromUser,
workingDirIsEmpty,
persistFormPreferences,
@@ -743,6 +750,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
isModelLoading,
modelError,
refreshProviderModels,
invalidateProviderModels,
setProviderAndModelFromUser,
workingDirIsEmpty,
persistFormPreferences,
@@ -159,6 +159,7 @@ function buildDraftStatusControls(input: {
features,
onSetFeature,
onDropdownClose,
onModelSelectorOpen: formState.invalidateProviderModels,
};
}
@@ -0,0 +1,164 @@
import { useState, useCallback, useMemo } from "react";
import { useQuery, type QueryClient } from "@tanstack/react-query";
import type { DaemonClient } from "@server/client/daemon-client";
import type { ComboboxOption } from "@/components/ui/combobox";
import type { ToastApi } from "@/components/toast-host";
import { checkoutStatusQueryKey } from "@/hooks/use-checkout-status-query";
import { confirmDialog } from "@/utils/confirm-dialog";
interface UseBranchSwitcherInput {
client: DaemonClient | null;
normalizedServerId: string;
normalizedWorkspaceId: string;
currentBranchName: string | null;
isGitCheckout: boolean;
isConnected: boolean;
toast: ToastApi;
queryClient: QueryClient;
}
interface UseBranchSwitcherResult {
branchOptions: ComboboxOption[];
isOpen: boolean;
setIsOpen: (open: boolean) => void;
handleBranchSelect: (branchId: string) => void;
invalidateStashAndCheckout: () => Promise<void>;
}
export function useBranchSwitcher({
client,
normalizedServerId,
normalizedWorkspaceId,
currentBranchName,
isGitCheckout,
isConnected,
toast,
queryClient,
}: UseBranchSwitcherInput): UseBranchSwitcherResult {
const [isOpen, setIsOpen] = useState(false);
const branchSuggestionsQuery = useQuery({
queryKey: ["branchSuggestions", normalizedServerId, normalizedWorkspaceId],
queryFn: async () => {
if (!client) {
throw new Error("Daemon client unavailable");
}
const payload = await client.getBranchSuggestions({
cwd: normalizedWorkspaceId,
limit: 200,
});
if (payload.error) {
throw new Error(payload.error);
}
return payload.branches ?? [];
},
enabled: isOpen && isGitCheckout && Boolean(client) && isConnected,
retry: false,
staleTime: 15_000,
});
const branchOptions = useMemo<ComboboxOption[]>(() => {
const branches = branchSuggestionsQuery.data ?? [];
return branches.map((name) => ({ id: name, label: name }));
}, [branchSuggestionsQuery.data]);
const stashListQueryKey = useMemo(
() => ["stashList", normalizedServerId, normalizedWorkspaceId] as const,
[normalizedServerId, normalizedWorkspaceId],
);
const invalidateStashAndCheckout = useCallback(async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: stashListQueryKey }),
queryClient.invalidateQueries({
queryKey: checkoutStatusQueryKey(normalizedServerId, normalizedWorkspaceId),
}),
]);
}, [queryClient, stashListQueryKey, normalizedServerId, normalizedWorkspaceId]);
const stashAndSwitch = useCallback(
async (branchId: string) => {
if (!client) return;
const shouldStash = await confirmDialog({
title: "Uncommitted changes",
message:
"You have uncommitted changes. Stash them before switching branches?",
confirmLabel: "Stash & Switch",
cancelLabel: "Cancel",
});
if (!shouldStash) return;
try {
const stashPayload = await client.stashSave(normalizedWorkspaceId, {
branch: currentBranchName ?? undefined,
});
if (stashPayload.error) {
toast.error(stashPayload.error.message);
return;
}
await invalidateStashAndCheckout();
const switchPayload = await client.checkoutSwitchBranch(normalizedWorkspaceId, branchId);
if (switchPayload.error) {
toast.error(switchPayload.error.message);
return;
}
await invalidateStashAndCheckout();
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to stash changes");
}
},
[client, currentBranchName, invalidateStashAndCheckout, normalizedWorkspaceId, toast],
);
const handleBranchSelect = useCallback(
(branchId: string) => {
if (branchId === currentBranchName) return;
void (async () => {
if (!client) return;
try {
const payload = await client.checkoutSwitchBranch(normalizedWorkspaceId, branchId);
if (payload.error) {
// If the error is about uncommitted changes, offer the stash dialog
if (payload.error.message.toLowerCase().includes("uncommitted")) {
await stashAndSwitch(branchId);
return;
}
toast.error(payload.error.message);
return;
}
// Success — refresh and check for stashes on the target branch
await invalidateStashAndCheckout();
try {
const stashPayload = await client.stashList(normalizedWorkspaceId, { paseoOnly: true });
const targetStash = stashPayload.entries.find((e) => e.branch === branchId);
if (targetStash) {
const shouldRestore = await confirmDialog({
title: "Restore stashed changes?",
message: "This branch has stashed changes from a previous session. Would you like to restore them?",
confirmLabel: "Restore",
cancelLabel: "Later",
});
if (shouldRestore) {
const popPayload = await client.stashPop(normalizedWorkspaceId, targetStash.index);
if (popPayload.error) {
toast.error(popPayload.error.message);
} else {
toast.show("Stashed changes restored");
}
await invalidateStashAndCheckout();
}
}
} catch {
// Non-critical — user can still restore on next branch switch
}
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to switch branch");
}
})();
},
[client, currentBranchName, invalidateStashAndCheckout, normalizedWorkspaceId, stashAndSwitch, toast],
);
return { branchOptions, isOpen, setIsOpen, handleBranchSelect, invalidateStashAndCheckout };
}
@@ -17,6 +17,7 @@ interface UseProvidersSnapshotResult {
error: string | null;
supportsSnapshot: boolean;
refresh: () => void;
invalidate: () => void;
}
export function useProvidersSnapshot(serverId: string | null): UseProvidersSnapshotResult {
@@ -66,6 +67,10 @@ export function useProvidersSnapshot(serverId: string | null): UseProvidersSnaps
void client.refreshProvidersSnapshot();
}, [client]);
const invalidate = useCallback(() => {
void queryClient.invalidateQueries({ queryKey });
}, [queryClient, queryKey]);
return {
entries: snapshotQuery.data?.entries ?? undefined,
isLoading: snapshotQuery.isLoading,
@@ -73,6 +78,7 @@ export function useProvidersSnapshot(serverId: string | null): UseProvidersSnaps
error: snapshotQuery.error instanceof Error ? snapshotQuery.error.message : null,
supportsSnapshot,
refresh,
invalidate,
};
}
@@ -38,6 +38,28 @@ const styles = StyleSheet.create((theme) => ({
justifyContent: "flex-start",
paddingTop: theme.spacing[16],
},
errorScreen: {
position: "relative",
flex: 1,
backgroundColor: theme.colors.surface0,
},
errorScrollView: {
flex: 1,
...(Platform.OS === "web"
? {
overflowX: "auto",
overflowY: "auto",
}
: null),
},
errorScrollContent: {
flexGrow: 1,
alignItems: "center",
justifyContent: "flex-start",
paddingHorizontal: theme.spacing[8],
paddingVertical: theme.spacing[8],
paddingTop: theme.spacing[16],
},
centeredContent: {
alignItems: "center",
justifyContent: "center",
@@ -255,67 +277,73 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps
}
return (
<View style={[styles.container, styles.containerError]}>
<View style={styles.errorScreen}>
<TitlebarDragRegion />
<View style={styles.errorContent}>
<View style={styles.errorHeader}>
<PaseoLogo size={64} />
<Text style={[styles.title, styles.titleError]}>Something went wrong</Text>
<ScrollView
style={styles.errorScrollView}
contentContainerStyle={styles.errorScrollContent}
showsVerticalScrollIndicator
>
<View style={styles.errorContent}>
<View style={styles.errorHeader}>
<PaseoLogo size={64} />
<Text style={[styles.title, styles.titleError]}>Something went wrong</Text>
</View>
<Text style={styles.errorDescription}>
The local server failed to start. If this keeps happening, please report the issue on GitHub and include the logs below.
</Text>
<Text style={styles.errorMessage}>
{bootstrapState.error}
</Text>
{daemonLogs?.logPath ? <Text style={styles.logsMeta}>{daemonLogs.logPath}</Text> : null}
<View style={styles.logsContainer}>
<ScrollView
style={styles.logsScroll}
contentContainerStyle={styles.logsContent}
showsVerticalScrollIndicator
>
<Text selectable style={styles.logsText}>
{logsText}
</Text>
</ScrollView>
</View>
<View style={styles.actionRow}>
<Button
variant="secondary"
leftIcon={<Copy size={16} color={theme.colors.foreground} />}
onPress={handleCopyLogs}
>
Copy logs
</Button>
<Button
variant="outline"
leftIcon={<TriangleAlert size={16} color={theme.colors.foreground} />}
onPress={() => void openExternalUrl(GITHUB_ISSUE_URL)}
>
Open GitHub issue
</Button>
<Button
variant="outline"
leftIcon={<BookOpen size={16} color={theme.colors.foreground} />}
onPress={() => void openExternalUrl(DOCS_URL)}
>
Docs
</Button>
<Button
variant="default"
leftIcon={<RotateCw size={16} color={theme.colors.palette.white} />}
onPress={bootstrapState.retry}
>
Retry
</Button>
</View>
</View>
<Text style={styles.errorDescription}>
The local server failed to start. If this keeps happening, please report the issue on GitHub and include the logs below.
</Text>
<Text style={styles.errorMessage}>
{bootstrapState.error}
</Text>
{daemonLogs?.logPath ? <Text style={styles.logsMeta}>{daemonLogs.logPath}</Text> : null}
<View style={styles.logsContainer}>
<ScrollView
style={styles.logsScroll}
contentContainerStyle={styles.logsContent}
showsVerticalScrollIndicator
>
<Text selectable style={styles.logsText}>
{logsText}
</Text>
</ScrollView>
</View>
<View style={styles.actionRow}>
<Button
variant="secondary"
leftIcon={<Copy size={16} color={theme.colors.foreground} />}
onPress={handleCopyLogs}
>
Copy logs
</Button>
<Button
variant="outline"
leftIcon={<TriangleAlert size={16} color={theme.colors.foreground} />}
onPress={() => void openExternalUrl(GITHUB_ISSUE_URL)}
>
Open GitHub issue
</Button>
<Button
variant="outline"
leftIcon={<BookOpen size={16} color={theme.colors.foreground} />}
onPress={() => void openExternalUrl(DOCS_URL)}
>
Docs
</Button>
<Button
variant="default"
leftIcon={<RotateCw size={16} color={theme.colors.palette.white} />}
onPress={bootstrapState.retry}
>
Retry
</Button>
</View>
</View>
</ScrollView>
</View>
);
}
@@ -36,6 +36,7 @@ import invariant from "tiny-invariant";
import { SidebarMenuToggle } from "@/components/headers/menu-header";
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
import { ScreenHeader } from "@/components/headers/screen-header";
import { BranchSwitcher } from "@/components/branch-switcher";
import { Combobox, type ComboboxOption } from "@/components/ui/combobox";
import { Shortcut } from "@/components/ui/shortcut";
import {
@@ -82,6 +83,7 @@ import type { ListTerminalsResponse } from "@server/shared/messages";
import { upsertTerminalListEntry } from "@/utils/terminal-list";
import { confirmDialog } from "@/utils/confirm-dialog";
import { useArchiveAgent } from "@/hooks/use-archive-agent";
import { useBranchSwitcher } from "@/hooks/use-branch-switcher";
import { useStableEvent } from "@/hooks/use-stable-event";
import { buildProviderCommand } from "@/utils/provider-command-templates";
import { generateDraftId } from "@/stores/draft-keys";
@@ -786,6 +788,24 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
checkoutQuery.data?.isGit && checkoutQuery.data.currentBranch !== "HEAD"
? trimNonEmpty(checkoutQuery.data.currentBranch)
: null;
const {
branchOptions,
isOpen: isBranchSwitcherOpen,
setIsOpen: setIsBranchSwitcherOpen,
handleBranchSelect,
invalidateStashAndCheckout,
} = useBranchSwitcher({
client,
normalizedServerId,
normalizedWorkspaceId,
currentBranchName,
isGitCheckout,
isConnected,
toast,
queryClient,
});
const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
@@ -2018,13 +2038,14 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
</>
) : (
<>
<Text
testID="workspace-header-title"
style={styles.headerTitle}
numberOfLines={1}
>
{workspaceHeader.title}
</Text>
<BranchSwitcher
currentBranchName={currentBranchName}
title={workspaceHeader.title}
branchOptions={branchOptions}
isOpen={isBranchSwitcherOpen}
onOpenChange={setIsBranchSwitcherOpen}
onBranchSelect={handleBranchSelect}
/>
<Text
testID="workspace-header-subtitle"
style={styles.headerProjectTitle}
@@ -35,7 +35,7 @@ describe("deriveSidebarStateBucket", () => {
).toBe("attention");
});
it("treats initializing agents as running", () => {
it("treats initializing agents as done", () => {
expect(
deriveSidebarStateBucket({
status: "initializing",
@@ -43,6 +43,6 @@ describe("deriveSidebarStateBucket", () => {
requiresAttention: false,
attentionReason: null,
}),
).toBe("running");
).toBe("done");
});
});
@@ -21,7 +21,7 @@ export function deriveSidebarStateBucket(input: {
if (input.status === "error" || input.attentionReason === "error") {
return "failed";
}
if (input.status === "running" || input.status === "initializing") {
if (input.status === "running") {
return "running";
}
if (input.requiresAttention) {