Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions docs/SIDEBAR_ITEM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Sidebar Item

Sidebar branch items are split into small components under
`src/lib/components/templates/sidebar`:

- `sidebar-branch-item.tsx` renders the clickable row, branch name, status pill,
last commit age, and worktree removal action.
- `sidebar-item-icon.tsx` renders the branch or pull request icon.
- `sidebar-item-status.ts` classifies each branch item and stores the sidebar
label, icon color classes, and pill color classes in one place.
- `sidebar-project-item.tsx` groups branch items by project and decides which
stale items are visible.

## Classification

`getSidebarItemStatus` classifies a `BranchInfo` using this priority order:

1. `stale-worktree`: `branch.hasWorktree === true` and
`branch.isStale === true`.
- Label: `Stale Worktree`
- Color: danger/red
- Reason: this branch has a local worktree, but the backing branch is stale,
so it needs the strongest warning.
2. `worktree`: `branch.hasWorktree === true`.
- Label: `Worktree`
- Color: success/green
- Reason: this branch has a local runnable worktree.
3. `stale`: `branch.isStale === true`.
- Label: `Stale`
- Color: warning/yellow
- Reason: this branch is stale but does not have a local worktree.
4. `pull-request`: `branch.source === "pull-request"`.
- Label: `PR`
- Color: blue
- Reason: this item came from an open pull request.
5. `branch`: fallback for every other branch.
- Label: `Branch`
- Color: muted
- Reason: this is a regular branch without a worktree, stale marker, or pull
request source.

The priority order matters. For example, a pull request that is stale is shown
as `Stale`, and a stale branch with a worktree is shown as `Stale Worktree`.

## Color Source

Sidebar item colors should be changed only in `sidebar-item-status.ts`.

Both `SidebarBranchItem` and `SidebarItemIcon` consume the same status config:

- `pillClassName` controls the status pill color.
- `iconClassName` controls the icon background and icon color.

This keeps the icon and status pill visually aligned for combined states such as
`Stale Worktree`. Sidebar pills use the `custom` `StatusPill` tone so the
sidebar status config fully owns the background, border, and text colors.
44 changes: 44 additions & 0 deletions src/lib/components/atoms/busy-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { ComponentPropsWithoutRef } from "react";

import { cn } from "@/lib/utils/cn";

type BusyIconProps = ComponentPropsWithoutRef<"span"> & {
size?: "sm" | "md";
};

const busyIconSizeClassName = {
md: {
container: "h-5 w-5",
dot: "h-2.5 w-2.5",
},
sm: {
container: "h-4 w-4",
dot: "h-2 w-2",
},
};

export function BusyIcon({ className, size = "md", ...props }: BusyIconProps) {
const sizeClassName = busyIconSizeClassName[size];

return (
<span
aria-label="Busy terminal"
className={cn(
`bg-success/15 text-success grid flex-none place-items-center
rounded-md`,
sizeClassName.container,
className,
)}
role="img"
title="Busy terminal"
{...props}
>
<span
className={cn(
"bg-success ring-success/25 rounded-full ring-2",
sizeClassName.dot,
)}
/>
</span>
);
}
2 changes: 2 additions & 0 deletions src/lib/components/atoms/status-pill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cn } from "@/lib/utils/cn";
type StatusPillTone =
| "branch"
| "busy"
| "custom"
| "error"
| "idle"
| "pull-request"
Expand All @@ -18,6 +19,7 @@ type StatusPillProps = ComponentPropsWithoutRef<"span"> & {
const statusPillClassName: Record<StatusPillTone, string> = {
branch: "border-border bg-muted/35 text-muted-foreground",
busy: "border-success/25 bg-success/12 text-success-foreground",
custom: "",
error: "border-danger/25 bg-danger/12 text-danger-foreground",
idle: "border-border bg-muted/25 text-muted-foreground",
"pull-request":
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Plus, X } from "lucide-react";
import type { MouseEvent } from "react";

import { BusyIcon } from "@/lib/components/atoms/busy-icon";
import { Button } from "@/lib/components/atoms/button";
import { TabShell } from "@/lib/components/atoms/tab-shell";
import { cn } from "@/lib/utils/cn";
Expand Down Expand Up @@ -62,10 +63,7 @@ export function TerminalTabBar({
onClick={() => onSelectTab(tab.id)}
>
{tab.busyState === "busy" ? (
<span
className="h-1.5 w-1.5 shrink-0
rounded-full bg-emerald-400"
/>
<BusyIcon size="sm" />
) : tab.status === "exited" ? (
<span
className="bg-danger h-1.5 w-1.5
Expand Down
75 changes: 52 additions & 23 deletions src/lib/components/templates/branch-scripts-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useOpenScriptMutation } from "@/lib/hooks/query/use-open-script-mutatio
import { useRunScriptMutation } from "@/lib/hooks/query/use-run-script-mutation";
import { useScriptsQuery } from "@/lib/hooks/query/use-scripts-query";
import { getErrorMessage } from "@/lib/utils/get-error-message";
import { cn } from "@/lib/utils/cn";
import type { ScriptInfo } from "@/types/pr-run";

type BranchScriptsSectionProps = {
Expand Down Expand Up @@ -127,6 +128,7 @@ export function BranchScriptsSection({
const isDeleting =
deleteScriptMutation.isPending &&
deleteScriptMutation.variables === script.id;
const isActionVisible = isDeleting;

return (
<div
Expand All @@ -138,44 +140,71 @@ export function BranchScriptsSection({
className="hover:bg-muted/30
disabled:text-muted-foreground h-8
max-w-64 cursor-pointer
justify-start gap-1.5 pr-[58px]
text-left transition
justify-start gap-1.5 text-left
transition
disabled:cursor-not-allowed
disabled:opacity-60"
disabled={
Boolean(script.loadError) ||
isPreparing
}
type="button"
onClick={() => void runScript(script)}
onClick={() => {
runScript(script);
}}
>
<Play className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">
{isPreparing
? "Preparing..."
: script.title}
<span
className="flex min-w-0 items-center
gap-1.5"
>
<Play
className="h-3.5 w-3.5 shrink-0"
/>
<span className="truncate">
{isPreparing
? "Preparing..."
: script.title}
</span>
</span>
<span
aria-hidden
className={cn(
`block h-full w-14 shrink-0
transition-opacity
duration-150
group-focus-within:opacity-0
group-hover:opacity-0`,
isActionVisible && "opacity-0",
)}
>
&nbsp;
</span>
</Chip>
<div
className="pointer-events-none absolute
top-1/2 right-1 z-10 flex
-translate-y-1/2 items-center gap-1
opacity-0 transition-opacity
duration-150
group-focus-within:pointer-events-auto
group-focus-within:opacity-100
group-hover:pointer-events-auto
group-hover:opacity-100"
className={cn(
`pointer-events-none absolute
top-1/2 right-1 z-10 flex
-translate-y-1/2 items-center
gap-1 opacity-0
transition-opacity duration-150
group-focus-within:pointer-events-auto
group-focus-within:opacity-100
group-hover:pointer-events-auto
group-hover:opacity-100`,
isActionVisible &&
`pointer-events-auto
opacity-100`,
)}
>
<Button
aria-label={`Edit ${script.title}`}
isIconOnly
size="icon-xs"
type="button"
variant="outline"
onPress={() =>
void editScript(script)
}
onPress={() => {
editScript(script);
}}
>
<Pencil className="h-3.5 w-3.5" />
</Button>
Expand Down Expand Up @@ -227,9 +256,9 @@ export function BranchScriptsSection({
isDisabled={deleteScriptMutation.isPending}
type="button"
variant="danger"
onPress={() =>
void deleteScript(scriptPendingDelete)
}
onPress={() => {
deleteScript(scriptPendingDelete);
}}
>
<Trash2 className="h-4 w-4" />
{deleteScriptMutation.isPending
Expand Down
Loading