diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx
index 0ca57b98e..f43ac7624 100644
--- a/packages/app/src/components/sidebar-workspace-list.tsx
+++ b/packages/app/src/components/sidebar-workspace-list.tsx
@@ -101,7 +101,10 @@ import { SidebarStatusWorkspaceList } from "@/components/sidebar/sidebar-status-
import {
SidebarWorkspaceRowFrame,
SidebarWorkspaceRowContent,
- sidebarWorkspaceRowStyles,
+ SidebarWorkspaceShortcutBadge,
+ SidebarWorkspaceTrailingActionBase,
+ SidebarWorkspaceTrailingActionOverlay,
+ SidebarWorkspaceTrailingActionSlot,
} from "@/components/sidebar/sidebar-workspace-row-content";
import {
useHydratedWorkspaceEntries,
@@ -652,7 +655,10 @@ function WorkspaceRowRightGroup({
onCopyPath?: () => void;
onRename?: () => void;
}) {
+ const showShortcut = showShortcutBadge && shortcutNumber !== null;
const showKebab = Boolean(onArchive && (isHovered || isTouchPlatform));
+ const showKebabInSlot = showKebab && !showShortcut;
+ const shouldRenderActionSlot = Boolean(onArchive || workspace.diffStat);
return (
<>
{showScriptsIcon ? (
@@ -665,30 +671,35 @@ function WorkspaceRowRightGroup({
) : null}
{isCreating ? Creating... : null}
- {showKebab && onArchive ? (
-
- ) : null}
- {!showKebab && workspace.diffStat ? (
-
- ) : null}
- {showShortcutBadge && shortcutNumber !== null ? (
-
- {shortcutNumber}
-
+ {shouldRenderActionSlot ? (
+
+
+ {workspace.diffStat ? (
+
+ ) : null}
+
+
+ {onArchive ? (
+
+ ) : null}
+
+
) : null}
>
);
@@ -1275,8 +1286,8 @@ function ProjectHeaderRow({
removeProjectStatus={removeProjectStatus}
/>
{showShortcutBadge && shortcutNumber !== null ? (
-
- {shortcutNumber}
+
+
) : null}
>
@@ -1412,6 +1423,8 @@ function WorkspaceRowInner({
isHovered={isHovered}
isLoading={isArchiving || isCreating}
isCreating={isCreating}
+ shortcutNumber={shortcutNumber}
+ showShortcutBadge={showShortcutBadge}
>
({
textAlign: "center",
},
projectRow: {
+ position: "relative",
minHeight: 36,
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[2],
@@ -2925,6 +2939,11 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm,
},
projectActionTooltipShortcut: {},
+ projectShortcutBadgeOverlay: {
+ position: "absolute",
+ top: theme.spacing[2] + 1,
+ right: theme.spacing[2],
+ },
workspaceRow: {
minHeight: 36,
marginBottom: theme.spacing[1],
@@ -3045,24 +3064,6 @@ const styles = StyleSheet.create((theme) => ({
kebabButtonHovered: {
backgroundColor: theme.colors.surface2,
},
- shortcutBadge: {
- minWidth: 18,
- height: 18,
- paddingHorizontal: theme.spacing[1],
- alignItems: "center",
- justifyContent: "center",
- borderRadius: theme.borderRadius.sm,
- borderWidth: 1,
- borderColor: theme.colors.surface2,
- backgroundColor: theme.colors.surface0,
- flexShrink: 0,
- },
- shortcutBadgeText: {
- color: theme.colors.foregroundMuted,
- fontSize: theme.fontSize.xs,
- fontWeight: theme.fontWeight.medium,
- lineHeight: 14,
- },
statusDotNeedsInput: {
backgroundColor: theme.colors.palette.amber[500],
borderColor: theme.colors.surface0,
diff --git a/packages/app/src/components/sidebar/sidebar-status-list.tsx b/packages/app/src/components/sidebar/sidebar-status-list.tsx
index 8fabec4bd..831734130 100644
--- a/packages/app/src/components/sidebar/sidebar-status-list.tsx
+++ b/packages/app/src/components/sidebar/sidebar-status-list.tsx
@@ -59,7 +59,9 @@ import { useClearWorkspaceAttention } from "@/hooks/use-clear-workspace-attentio
import {
SidebarWorkspaceRowFrame,
SidebarWorkspaceRowContent,
- sidebarWorkspaceRowStyles,
+ SidebarWorkspaceTrailingActionBase,
+ SidebarWorkspaceTrailingActionOverlay,
+ SidebarWorkspaceTrailingActionSlot,
} from "@/components/sidebar/sidebar-workspace-row-content";
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
@@ -611,7 +613,10 @@ function StatusWorkspaceRowInner({
return (
{({ isHovered, hoverHandlers }) => {
+ const showShortcut = showShortcutBadge && shortcutNumber !== null;
const showKebab = Boolean(onArchive && (isHovered || isTouchPlatform));
+ const showKebabInSlot = showKebab && !showShortcut;
+ const shouldRenderActionSlot = Boolean(onArchive || workspace.diffStat);
const workspaceRowStyle = getStatusWorkspaceRowStyle({ selected, isHovered });
return (
@@ -628,6 +633,8 @@ function StatusWorkspaceRowInner({
subtitle={projectName}
isHovered={isHovered}
isLoading={isArchiving}
+ shortcutNumber={shortcutNumber}
+ showShortcutBadge={showShortcutBadge}
>
<>
{showScriptsIcon ? (
@@ -639,9 +646,11 @@ function StatusWorkspaceRowInner({
)}
) : null}
- {showKebab && onArchive ? (
-
) : null}
- {!showKebab && workspace.diffStat ? (
-
- ) : null}
- {showShortcutBadge && shortcutNumber !== null ? (
-
-
- {shortcutNumber}
-
-
- ) : null}
>
@@ -676,6 +672,63 @@ function StatusWorkspaceRowInner({
);
}
+function StatusWorkspaceActionSlot({
+ workspace,
+ showBase,
+ showOverlay,
+ onCopyPath,
+ onCopyBranchName,
+ onRename,
+ onMarkAsRead,
+ onArchive,
+ archiveLabel,
+ archiveStatus,
+ archivePendingLabel,
+ archiveShortcutKeys,
+}: {
+ workspace: SidebarWorkspaceEntry;
+ showBase: boolean;
+ showOverlay: boolean;
+ onCopyPath?: () => void;
+ onCopyBranchName?: () => void;
+ onRename?: () => void;
+ onMarkAsRead?: () => void;
+ onArchive?: () => void;
+ archiveLabel?: string;
+ archiveStatus?: "idle" | "pending" | "success";
+ archivePendingLabel?: string;
+ archiveShortcutKeys?: ShortcutKey[][] | null;
+}) {
+ return (
+
+
+ {workspace.diffStat ? (
+
+ ) : null}
+
+
+ {onArchive ? (
+
+ ) : null}
+
+
+ );
+}
+
function StatusKebabMenu({
workspaceKey,
onCopyPath,
diff --git a/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx b/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx
index 7a8112ae7..19fee6816 100644
--- a/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx
+++ b/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx
@@ -89,6 +89,8 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
isHovered,
isLoading,
isCreating = false,
+ shortcutNumber = null,
+ showShortcutBadge = false,
children,
}: {
workspace: SidebarWorkspaceEntry;
@@ -96,6 +98,8 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
isHovered: boolean;
isLoading: boolean;
isCreating?: boolean;
+ shortcutNumber?: number | null;
+ showShortcutBadge?: boolean;
children?: ReactNode;
}) {
const workspaceBranchTextStyle = useMemo(
@@ -108,7 +112,7 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
);
return (
- <>
+
{children}
+ {showShortcutBadge && shortcutNumber !== null ? (
+
+
+
+ ) : null}
{subtitle ? (
{subtitle}
@@ -133,7 +142,7 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
) : null}
- >
+
);
});
@@ -346,7 +355,7 @@ const checksBadgeStyles = StyleSheet.create((theme) => ({
export const sidebarWorkspaceRowStyles = StyleSheet.create((theme) => ({
rowRight: {
flexDirection: "row",
- alignItems: "center",
+ alignItems: "flex-start",
gap: theme.spacing[2],
flexShrink: 0,
},
@@ -368,12 +377,63 @@ export const sidebarWorkspaceRowStyles = StyleSheet.create((theme) => ({
fontWeight: theme.fontWeight.medium,
lineHeight: 14,
},
+ hidden: { opacity: 0 },
+ trailingActionSlot: {
+ position: "relative",
+ minWidth: 18,
+ minHeight: 20,
+ flexShrink: 0,
+ alignItems: "flex-end",
+ justifyContent: "flex-start",
+ },
+ trailingActionOverlay: {
+ position: "absolute",
+ top: 0,
+ right: 0,
+ },
}));
+export function SidebarWorkspaceShortcutBadge({ number }: { number: number }) {
+ return (
+
+ {number}
+
+ );
+}
+
+export function SidebarWorkspaceTrailingActionSlot({ children }: { children: ReactNode }) {
+ return {children};
+}
+
+export function SidebarWorkspaceTrailingActionBase({
+ visible,
+ children,
+}: {
+ visible: boolean;
+ children: ReactNode;
+}) {
+ if (!children) return null;
+ return {children};
+}
+
+export function SidebarWorkspaceTrailingActionOverlay({
+ visible,
+ children,
+}: {
+ visible: boolean;
+ children: ReactNode;
+}) {
+ if (!visible || !children) return null;
+ return {children};
+}
+
const styles = StyleSheet.create((theme) => ({
+ workspaceRowContent: {
+ position: "relative",
+ },
workspaceRowMain: {
flexDirection: "row",
- alignItems: "center",
+ alignItems: "flex-start",
justifyContent: "space-between",
gap: theme.spacing[2],
width: "100%",
@@ -386,6 +446,11 @@ const styles = StyleSheet.create((theme) => ({
minWidth: 0,
},
workspaceRowRight: sidebarWorkspaceRowStyles.rowRight,
+ shortcutBadgeOverlay: {
+ position: "absolute",
+ top: 1,
+ right: 0,
+ },
workspaceStatusDot: {
position: "relative",
width: WORKSPACE_STATUS_DOT_WIDTH,
diff --git a/packages/app/src/components/workspace-shortcut-targets-subscriber.tsx b/packages/app/src/components/workspace-shortcut-targets-subscriber.tsx
index f90e7940f..f5762cc42 100644
--- a/packages/app/src/components/workspace-shortcut-targets-subscriber.tsx
+++ b/packages/app/src/components/workspace-shortcut-targets-subscriber.tsx
@@ -28,6 +28,9 @@ export function WorkspaceShortcutTargetsSubscriber({
const collapsedProjectKeys = useSidebarCollapsedSectionsStore(
(state) => state.collapsedProjectKeys,
);
+ const collapsedStatusGroupKeys = useSidebarCollapsedSectionsStore(
+ (state) => state.collapsedStatusGroupKeys,
+ );
const setSidebarShortcutWorkspaceTargets = useKeyboardShortcutsStore(
(state) => state.setSidebarShortcutWorkspaceTargets,
);
@@ -37,6 +40,7 @@ export function WorkspaceShortcutTargetsSubscriber({
return buildStatusSidebarShortcutModel({
workspaces: statusWorkspaces,
projectNamesByKey,
+ collapsedStatusGroupKeys,
});
}
@@ -44,7 +48,14 @@ export function WorkspaceShortcutTargetsSubscriber({
projects,
collapsedProjectKeys,
});
- }, [collapsedProjectKeys, groupMode, projectNamesByKey, projects, statusWorkspaces]);
+ }, [
+ collapsedProjectKeys,
+ collapsedStatusGroupKeys,
+ groupMode,
+ projectNamesByKey,
+ projects,
+ statusWorkspaces,
+ ]);
useEffect(() => {
if (!enabled || !serverId) {
diff --git a/packages/app/src/utils/sidebar-shortcuts.test.ts b/packages/app/src/utils/sidebar-shortcuts.test.ts
index f191471a1..cfd6b2b22 100644
--- a/packages/app/src/utils/sidebar-shortcuts.test.ts
+++ b/packages/app/src/utils/sidebar-shortcuts.test.ts
@@ -199,6 +199,37 @@ describe("buildStatusSidebarShortcutModel", () => {
expect(model.shortcutIndexByWorkspaceKey.get("s1:running-old")).toBe(3);
expect(model.shortcutIndexByWorkspaceKey.get("s1:done-old")).toBe(4);
});
+
+ it("excludes collapsed status groups from shortcut targets", () => {
+ const workspaces = [
+ workspace({
+ serverId: "s1",
+ workspaceId: "needs-input",
+ workspaceDirectory: "/repo/needs-input",
+ name: "needs input",
+ projectKey: "p1",
+ statusBucket: "needs_input",
+ }),
+ workspace({
+ serverId: "s1",
+ workspaceId: "running",
+ workspaceDirectory: "/repo/running",
+ name: "running",
+ projectKey: "p1",
+ statusBucket: "running",
+ }),
+ ];
+
+ const model = buildStatusSidebarShortcutModel({
+ workspaces,
+ projectNamesByKey: new Map([["p1", "Project 1"]]),
+ collapsedStatusGroupKeys: new Set(["needs_input"]),
+ });
+
+ expect(model.shortcutTargets).toEqual([{ serverId: "s1", workspaceId: "running" }]);
+ expect(model.shortcutIndexByWorkspaceKey.get("s1:needs-input")).toBeUndefined();
+ expect(model.shortcutIndexByWorkspaceKey.get("s1:running")).toBe(1);
+ });
});
describe("getRelativeSidebarShortcutTarget", () => {
diff --git a/packages/app/src/utils/sidebar-shortcuts.ts b/packages/app/src/utils/sidebar-shortcuts.ts
index eff5f9cdf..2c75c8905 100644
--- a/packages/app/src/utils/sidebar-shortcuts.ts
+++ b/packages/app/src/utils/sidebar-shortcuts.ts
@@ -53,6 +53,7 @@ export function buildSidebarShortcutModel(input: {
export function buildStatusSidebarShortcutModel(input: {
workspaces: SidebarWorkspaceEntry[];
projectNamesByKey: Map;
+ collapsedStatusGroupKeys?: ReadonlySet;
shortcutLimit?: number;
}): SidebarShortcutModel {
const maxShortcuts = Math.max(0, Math.floor(input.shortcutLimit ?? 9));
@@ -61,6 +62,10 @@ export function buildStatusSidebarShortcutModel(input: {
const shortcutIndexByWorkspaceKey = new Map();
for (const group of groups) {
+ if (input.collapsedStatusGroupKeys?.has(group.bucket)) {
+ continue;
+ }
+
for (const workspace of group.rows) {
if (shortcutTargets.length >= maxShortcuts) {
break;