Add workspace title display preference
This commit is contained in:
@@ -52,7 +52,7 @@ interface WorkspaceStatusGroupEvent {
|
||||
}
|
||||
|
||||
async function switchSidebarToStatusGrouping(page: import("@playwright/test").Page) {
|
||||
await page.getByTestId("sidebar-grouping-selector").click();
|
||||
await page.getByTestId("sidebar-display-preferences-menu").click();
|
||||
await page.getByTestId("sidebar-grouping-status").click();
|
||||
await expect(page.getByTestId("sidebar-status-group-done")).toBeVisible({ timeout: 30_000 });
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ test.describe("Model B sidebar shape", () => {
|
||||
await expect(workspaceRow(page, idleProject.workspaceId)).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// Switch to status grouping.
|
||||
await page.getByTestId("sidebar-grouping-selector").click();
|
||||
await page.getByTestId("sidebar-display-preferences-menu").click();
|
||||
await page.getByTestId("sidebar-grouping-status").click();
|
||||
|
||||
const sidebar = page.getByTestId("sidebar-sessions").filter({ visible: true }).first();
|
||||
|
||||
@@ -72,7 +72,7 @@ async function fetchAgentStatus(seeded: SeededWorkspace, agentId: string): Promi
|
||||
}
|
||||
|
||||
async function switchSidebarToStatusGrouping(page: import("@playwright/test").Page) {
|
||||
await page.getByTestId("sidebar-grouping-selector").click();
|
||||
await page.getByTestId("sidebar-display-preferences-menu").click();
|
||||
await page.getByTestId("sidebar-grouping-status").click();
|
||||
await expect(page.locator('[data-testid^="sidebar-status-group-"]').first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
|
||||
@@ -390,7 +390,7 @@ async function expectWorkspaceRowInStatusBucket(
|
||||
page: Page,
|
||||
input: { serverId: string; workspaceId: string; bucket: string },
|
||||
) {
|
||||
await page.getByTestId("sidebar-grouping-selector").click();
|
||||
await page.getByTestId("sidebar-display-preferences-menu").click();
|
||||
await page.getByTestId("sidebar-grouping-status").click();
|
||||
await expect(
|
||||
page
|
||||
|
||||
@@ -35,7 +35,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
||||
import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row";
|
||||
import { SidebarGroupingSelector } from "@/components/sidebar/sidebar-grouping-selector";
|
||||
import { SidebarDisplayPreferencesMenu } from "@/components/sidebar/sidebar-display-preferences-menu";
|
||||
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
|
||||
import { Shortcut } from "@/components/ui/shortcut";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
@@ -1096,7 +1096,7 @@ function WorkspacesSectionHeader({ serverId }: { serverId: string | null }) {
|
||||
<Tooltip delayDuration={300}>
|
||||
<TooltipTrigger asChild>
|
||||
<View>
|
||||
<SidebarGroupingSelector serverId={serverId} />
|
||||
<SidebarDisplayPreferencesMenu serverId={serverId} />
|
||||
</View>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="center" offset={8}>
|
||||
|
||||
+52
-10
@@ -8,9 +8,11 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useSidebarViewStore, type SidebarGroupMode } from "@/stores/sidebar-view-store";
|
||||
import { isWeb as platformIsWeb } from "@/constants/platform";
|
||||
import { useAppSettings, type WorkspaceTitleSource } from "@/hooks/use-settings";
|
||||
|
||||
const ThemedSettings2 = withUnistyles(Settings2);
|
||||
const filterColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||
@@ -20,11 +22,25 @@ const GROUP_MODE_ITEMS: Array<{ value: SidebarGroupMode; label: string }> = [
|
||||
{ value: "status", label: "Status" },
|
||||
];
|
||||
|
||||
export function SidebarGroupingSelector({ serverId }: { serverId: string | null }) {
|
||||
const WORKSPACE_TITLE_SOURCE_ITEMS: Array<{ value: WorkspaceTitleSource; label: string }> = [
|
||||
{ value: "title", label: "Title" },
|
||||
{ value: "branch", label: "Branch name" },
|
||||
];
|
||||
|
||||
interface DisplayPreferenceOption<Value extends string> {
|
||||
value: Value;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function SidebarDisplayPreferencesMenu({ serverId }: { serverId: string | null }) {
|
||||
const groupMode = useSidebarViewStore((state) =>
|
||||
serverId ? state.getGroupMode(serverId) : "project",
|
||||
);
|
||||
const setGroupMode = useSidebarViewStore((state) => state.setGroupMode);
|
||||
const {
|
||||
settings: { workspaceTitleSource },
|
||||
updateSettings,
|
||||
} = useAppSettings();
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(mode: SidebarGroupMode) => {
|
||||
@@ -34,6 +50,13 @@ export function SidebarGroupingSelector({ serverId }: { serverId: string | null
|
||||
[serverId, setGroupMode],
|
||||
);
|
||||
|
||||
const handleWorkspaceTitleSourceSelect = useCallback(
|
||||
(source: WorkspaceTitleSource) => {
|
||||
void updateSettings({ workspaceTitleSource: source });
|
||||
},
|
||||
[updateSettings],
|
||||
);
|
||||
|
||||
const triggerStyle = useCallback(
|
||||
({ hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
styles.trigger,
|
||||
@@ -47,45 +70,61 @@ export function SidebarGroupingSelector({ serverId }: { serverId: string | null
|
||||
<DropdownMenuTrigger
|
||||
style={triggerStyle}
|
||||
accessibilityRole={platformIsWeb ? undefined : "button"}
|
||||
accessibilityLabel="Sidebar grouping"
|
||||
testID="sidebar-grouping-selector"
|
||||
accessibilityLabel="Display preferences"
|
||||
testID="sidebar-display-preferences-menu"
|
||||
>
|
||||
<ThemedSettings2 size={14} uniProps={filterColorMapping} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" width={180} testID="sidebar-grouping-menu">
|
||||
<DropdownMenuContent align="end" width={180} testID="sidebar-display-preferences-content">
|
||||
<View style={styles.menuHeader}>
|
||||
<Text style={styles.menuHeaderLabel}>Group by</Text>
|
||||
</View>
|
||||
{GROUP_MODE_ITEMS.map((item) => (
|
||||
<GroupModeMenuItem
|
||||
<DisplayPreferenceMenuItem
|
||||
key={item.value}
|
||||
item={item}
|
||||
isSelected={groupMode === item.value}
|
||||
testIDPrefix="sidebar-grouping"
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<View style={styles.menuHeader}>
|
||||
<Text style={styles.menuHeaderLabel}>Workspace title</Text>
|
||||
</View>
|
||||
{WORKSPACE_TITLE_SOURCE_ITEMS.map((item) => (
|
||||
<DisplayPreferenceMenuItem
|
||||
key={item.value}
|
||||
item={item}
|
||||
isSelected={workspaceTitleSource === item.value}
|
||||
testIDPrefix="sidebar-workspace-title-source"
|
||||
onSelect={handleWorkspaceTitleSourceSelect}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupModeMenuItem({
|
||||
function DisplayPreferenceMenuItem<Value extends string>({
|
||||
item,
|
||||
isSelected,
|
||||
testIDPrefix,
|
||||
onSelect,
|
||||
}: {
|
||||
item: { value: SidebarGroupMode; label: string };
|
||||
item: DisplayPreferenceOption<Value>;
|
||||
isSelected: boolean;
|
||||
onSelect: (mode: SidebarGroupMode) => void;
|
||||
testIDPrefix: string;
|
||||
onSelect: (value: Value) => void;
|
||||
}) {
|
||||
const handleSelect = useCallback(() => onSelect(item.value), [item.value, onSelect]);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
testID={`sidebar-grouping-${item.value}`}
|
||||
testID={`${testIDPrefix}-${item.value}`}
|
||||
selected={isSelected}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{item.label}
|
||||
<Text style={styles.optionLabel}>{item.label}</Text>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
@@ -110,4 +149,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
optionLabel: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
}));
|
||||
@@ -22,12 +22,14 @@ import { GitHubIcon } from "@/components/icons/github-icon";
|
||||
import { WorkspaceHoverCard } from "@/components/workspace-hover-card";
|
||||
import { SyncedLoader } from "@/components/synced-loader";
|
||||
import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
|
||||
import { useAppSettings } from "@/hooks/use-settings";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import type { PrHint } from "@/git/use-pr-status-query";
|
||||
import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
||||
import { isEmphasizedStatusDotBucket } from "@/utils/status-dot-color";
|
||||
import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader";
|
||||
import { openExternalUrl } from "@/utils/open-external-url";
|
||||
import { resolveSidebarWorkspacePrimaryLabel } from "@/components/sidebar/sidebar-workspace-title";
|
||||
|
||||
const DEFAULT_STATUS_DOT_SIZE = 7;
|
||||
const EMPHASIZED_STATUS_DOT_SIZE = 9;
|
||||
@@ -110,6 +112,10 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
|
||||
showShortcutBadge?: boolean;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const {
|
||||
settings: { workspaceTitleSource },
|
||||
} = useAppSettings();
|
||||
const workspaceLabel = resolveSidebarWorkspacePrimaryLabel({ workspace, workspaceTitleSource });
|
||||
const workspaceBranchTextStyle = useMemo(
|
||||
() => [
|
||||
styles.workspaceBranchText,
|
||||
@@ -132,7 +138,7 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
|
||||
<View style={styles.workspaceTitleRow}>
|
||||
<View style={styles.workspaceTitleLeft}>
|
||||
<Text style={workspaceBranchTextStyle} numberOfLines={1}>
|
||||
{workspace.name}
|
||||
{workspaceLabel}
|
||||
</Text>
|
||||
{scriptIconKind ? <WorkspaceScriptIcon kind={scriptIconKind} /> : null}
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSidebarWorkspacePrimaryLabel } from "@/components/sidebar/sidebar-workspace-title";
|
||||
|
||||
describe("resolveSidebarWorkspacePrimaryLabel", () => {
|
||||
it("uses the workspace name in title mode", () => {
|
||||
const label = resolveSidebarWorkspacePrimaryLabel({
|
||||
workspace: { name: "Investigate search", currentBranch: "fix/search" },
|
||||
workspaceTitleSource: "title",
|
||||
});
|
||||
|
||||
expect(label).toBe("Investigate search");
|
||||
});
|
||||
|
||||
it("uses the branch name in branch mode", () => {
|
||||
const label = resolveSidebarWorkspacePrimaryLabel({
|
||||
workspace: { name: "Investigate search", currentBranch: "fix/search" },
|
||||
workspaceTitleSource: "branch",
|
||||
});
|
||||
|
||||
expect(label).toBe("fix/search");
|
||||
});
|
||||
|
||||
it("falls back to the workspace name in branch mode without a branch", () => {
|
||||
const label = resolveSidebarWorkspacePrimaryLabel({
|
||||
workspace: { name: "Local folder", currentBranch: null },
|
||||
workspaceTitleSource: "branch",
|
||||
});
|
||||
|
||||
expect(label).toBe("Local folder");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
|
||||
import type { WorkspaceTitleSource } from "@/hooks/use-settings";
|
||||
|
||||
export function resolveSidebarWorkspacePrimaryLabel(input: {
|
||||
workspace: Pick<SidebarWorkspaceEntry, "name" | "currentBranch">;
|
||||
workspaceTitleSource: WorkspaceTitleSource;
|
||||
}): string {
|
||||
if (input.workspaceTitleSource === "branch") {
|
||||
return input.workspace.currentBranch ?? input.workspace.name;
|
||||
}
|
||||
return input.workspace.name;
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
type ServiceUrlBehavior,
|
||||
type Settings,
|
||||
type SettingsDeps,
|
||||
type WorkspaceTitleSource,
|
||||
} from "./storage";
|
||||
|
||||
export {
|
||||
@@ -67,6 +68,7 @@ export type {
|
||||
ServiceUrlBehavior,
|
||||
Settings,
|
||||
SettingsDeps,
|
||||
WorkspaceTitleSource,
|
||||
};
|
||||
|
||||
const productionDeps: SettingsDeps = {
|
||||
@@ -172,6 +174,9 @@ export function useSettings(): UseSettingsReturn {
|
||||
if (updates.syntaxTheme !== undefined) {
|
||||
appUpdates.syntaxTheme = updates.syntaxTheme;
|
||||
}
|
||||
if (updates.workspaceTitleSource !== undefined) {
|
||||
appUpdates.workspaceTitleSource = updates.workspaceTitleSource;
|
||||
}
|
||||
const promises: Promise<void>[] = [];
|
||||
if (Object.keys(appUpdates).length > 0) {
|
||||
promises.push(appSettings.updateSettings(appUpdates));
|
||||
|
||||
@@ -61,6 +61,14 @@ describe("loadAppSettingsFromStorage", () => {
|
||||
expect(result.language).toBe("system");
|
||||
});
|
||||
|
||||
it("defaults workspace title source to title when storage is empty", async () => {
|
||||
const deps = makeDeps();
|
||||
|
||||
const result = await loadAppSettingsFromStorage(deps);
|
||||
|
||||
expect(result.workspaceTitleSource).toBe("title");
|
||||
});
|
||||
|
||||
it("loads configured terminal scrollback lines from app settings", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
@@ -73,6 +81,30 @@ describe("loadAppSettingsFromStorage", () => {
|
||||
expect(result.terminalScrollbackLines).toBe(42_000);
|
||||
});
|
||||
|
||||
it("loads configured workspace title source from app settings", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
[APP_SETTINGS_KEY]: JSON.stringify({ workspaceTitleSource: "branch" }),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await loadAppSettingsFromStorage(deps);
|
||||
|
||||
expect(result.workspaceTitleSource).toBe("branch");
|
||||
});
|
||||
|
||||
it("drops an unknown workspace title source back to title", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
[APP_SETTINGS_KEY]: JSON.stringify({ workspaceTitleSource: "directory" }),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await loadAppSettingsFromStorage(deps);
|
||||
|
||||
expect(result.workspaceTitleSource).toBe("title");
|
||||
});
|
||||
|
||||
it("normalizes terminal scrollback lines from storage", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
|
||||
@@ -11,9 +11,11 @@ const LEGACY_SETTINGS_KEY = "@paseo:settings";
|
||||
export type SendBehavior = "interrupt" | "queue";
|
||||
export type ReleaseChannel = "stable" | "beta";
|
||||
export type ServiceUrlBehavior = "ask" | "in-app" | "external";
|
||||
export type WorkspaceTitleSource = "title" | "branch";
|
||||
|
||||
const VALID_THEMES = new Set<string>([...Object.keys(THEME_TO_UNISTYLES), "auto"]);
|
||||
const VALID_SERVICE_URL_BEHAVIORS = new Set<ServiceUrlBehavior>(["ask", "in-app", "external"]);
|
||||
const VALID_WORKSPACE_TITLE_SOURCES = new Set<WorkspaceTitleSource>(["title", "branch"]);
|
||||
export const DEFAULT_TERMINAL_SCROLLBACK_LINES = 10_000;
|
||||
export const MIN_TERMINAL_SCROLLBACK_LINES = 0;
|
||||
export const MAX_TERMINAL_SCROLLBACK_LINES = 1_000_000;
|
||||
@@ -36,6 +38,7 @@ export interface AppSettings {
|
||||
uiFontSize: number; // clamped px, default 16
|
||||
codeFontSize: number; // clamped px, default 12
|
||||
syntaxTheme: SyntaxThemeId; // default "one"
|
||||
workspaceTitleSource: WorkspaceTitleSource;
|
||||
}
|
||||
|
||||
export interface Settings extends AppSettings {
|
||||
@@ -54,6 +57,7 @@ export const DEFAULT_CLIENT_SETTINGS: AppSettings = {
|
||||
uiFontSize: DEFAULT_UI_FONT_SIZE,
|
||||
codeFontSize: DEFAULT_CODE_FONT_SIZE,
|
||||
syntaxTheme: "one",
|
||||
workspaceTitleSource: "title",
|
||||
};
|
||||
|
||||
export const DEFAULT_APP_SETTINGS: Settings = {
|
||||
@@ -194,6 +198,12 @@ function pickAppSettings(stored: Partial<AppSettings>): Partial<AppSettings> {
|
||||
if (typeof stored.syntaxTheme === "string" && isSyntaxThemeId(stored.syntaxTheme)) {
|
||||
result.syntaxTheme = stored.syntaxTheme;
|
||||
}
|
||||
if (
|
||||
typeof stored.workspaceTitleSource === "string" &&
|
||||
VALID_WORKSPACE_TITLE_SOURCES.has(stored.workspaceTitleSource)
|
||||
) {
|
||||
result.workspaceTitleSource = stored.workspaceTitleSource;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user