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
85 changes: 78 additions & 7 deletions packages/app/src/pages/layout/pawwork-worktree-badge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ beforeAll(async () => {
mock.module("@opencode-ai/ui/icon", () => ({
Icon: (props: any) => ({ type: "Icon", props: props ?? {}, children: [] }) as Node,
}))
mock.module("@opencode-ai/ui/tooltip", () => ({
Tooltip: (props: any) =>
({
type: "Tooltip",
props: props ?? {},
children: Array.isArray(props?.children) ? props.children : [props?.children].filter(Boolean),
}) as Node,
TooltipKeybind: (props: any) =>
({
type: "TooltipKeybind",
props: props ?? {},
children: Array.isArray(props?.children) ? props.children : [props?.children].filter(Boolean),
}) as Node,
}))
PawworkWorktreeBadge = (await import("./pawwork-worktree-badge")).PawworkWorktreeBadge
})

Expand Down Expand Up @@ -55,7 +69,7 @@ function find(node: Node | string, predicate: (n: Node) => boolean): Node | unde
}

describe("PawworkWorktreeBadge", () => {
test("shows worktree name and branch in the visible titlebar label", () => {
test("shows only the worktree name in the visible titlebar label", () => {
const onClick = () => undefined
const tree = PawworkWorktreeBadge({
name: "feature-c",
Expand All @@ -66,11 +80,68 @@ describe("PawworkWorktreeBadge", () => {
disabled: true,
}) as unknown as Node

const label = find(tree, (node) => node.type === "span")
expect(label?.children.join("")).toBe("feature-c (pawwork/feature-c)")
expect(tree.props.title).toBe("pawwork/feature-c · /repo/.worktrees/pawwork/feature-c")
expect(tree.props.onClick).toBe(onClick)
expect(tree.props["aria-label"]).toBe("Open worktrees")
expect(tree.props.disabled).toBe(true)
const button = find(tree, (node) => node.type === "button")
const label = find(tree, (node) => node.type === "span" && node.children.join("") === "feature-c")
expect(label?.children.join("")).toBe("feature-c")
expect(button?.props.title).toBeUndefined()
expect(button?.props.onClick).toBe(onClick)
expect(button?.props["aria-label"]).toBe("Open worktrees")
expect(button?.props.disabled).toBe(true)
})

test("keeps visible label compact and shows three ordered hover rows", () => {
const tree = PawworkWorktreeBadge({
name: "very-long-worktree-name-used-for-titlebar-regression",
branch: "pawwork/very-long-worktree-name-used-for-titlebar-regression",
directory: "/repo/.worktrees/pawwork/very-long-worktree-name-used-for-titlebar-regression",
ariaLabel: "Open worktrees",
onClick: () => undefined,
}) as unknown as Node

const tooltip = find(tree, (node) => node.type === "Tooltip")
const button = find(tree, (node) => node.type === "button")
const label = find(tree, (node) => node.type === "span" && node.children.join("").startsWith("very-long-worktree"))

expect(button?.props.class).toContain("max-w-[280px]")
expect(label?.children.join("")).toBe("very-long-worktree-name-used-for-titlebar-regression")
expect(tooltip?.props.placement).toBe("bottom")
expect(tooltip?.props.class).toBe("shrink min-w-0")
expect(tooltip?.props.value).toMatchObject({
type: "div",
props: { "data-component": "pawwork-worktree-tooltip" },
children: [
expect.objectContaining({
children: expect.arrayContaining([expect.objectContaining({ children: ["Worktree"] })]),
}),
expect.objectContaining({
children: expect.arrayContaining([expect.objectContaining({ children: ["Branch"] })]),
}),
expect.objectContaining({
children: expect.arrayContaining([expect.objectContaining({ children: ["Location"] })]),
}),
],
})
})

test("does not duplicate fallback text in the structured tooltip", () => {
const tree = PawworkWorktreeBadge({
name: "",
branch: "pawwork/fallback-branch",
directory: "/repo/.worktrees/pawwork/fallback-branch",
ariaLabel: "Open worktrees",
onClick: () => undefined,
}) as unknown as Node

const tooltip = find(tree, (node) => node.type === "Tooltip")
const rows = (tooltip?.props.value as Node).children as Node[]
const worktreeRow = rows[0]
const branchRow = rows[1]

expect(worktreeRow.children).toEqual(
expect.arrayContaining([expect.objectContaining({ children: ["Not available"] })]),
)
expect(branchRow.children).toEqual(
expect.arrayContaining([expect.objectContaining({ children: ["pawwork/fallback-branch"] })]),
)
})
})
61 changes: 45 additions & 16 deletions packages/app/src/pages/layout/pawwork-worktree-badge.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"

function WorktreeTooltipRow(props: { label: string; value?: string; emphasis?: boolean }) {
return (
<div class="grid min-w-0 grid-cols-[64px_minmax(0,1fr)] items-start gap-3">
<span class="text-12-regular [color:var(--text-invert-weaker)]">{props.label}</span>
<span
classList={{
"text-13-medium [color:var(--text-invert-strong)]": props.emphasis,
"text-13-regular [color:var(--text-invert-base)]": !props.emphasis,
}}
class="min-w-0 break-all leading-[1.45]"
>
{props.value || "Not available"}
</span>
</div>
)
}

export function PawworkWorktreeBadge(props: {
name: string
Expand All @@ -9,23 +27,34 @@ export function PawworkWorktreeBadge(props: {
ariaLabel: string
disabled?: boolean
}) {
const title = () => [props.branch, props.directory].filter(Boolean).join(" · ") || props.name
const label = () => (props.branch ? `${props.name} (${props.branch})` : props.name)
const label = () => props.name || props.branch || props.directory || "Worktree"
const tooltip = () => (
<div data-component="pawwork-worktree-tooltip" class="grid min-w-0 gap-1.5 py-1 text-left">
<WorktreeTooltipRow label="Worktree" value={props.name} emphasis />
<WorktreeTooltipRow label="Branch" value={props.branch} />
<WorktreeTooltipRow label="Location" value={props.directory} />
</div>
)

return (
<Button
type="button"
variant="ghost"
size="small"
class="group h-6 max-w-[180px] min-w-0 shrink items-center gap-1 rounded px-1 shadow-none text-13-regular text-text-weak hover:text-text-strong"
data-component="pawwork-worktree-badge"
title={title()}
onClick={props.onClick}
aria-label={props.ariaLabel}
disabled={props.disabled}
>
<Icon name="worktree" size="small" class="shrink-0 text-text-weak transition-colors group-hover:text-text-strong" />
<span class="min-w-0 truncate">{label()}</span>
</Button>
<Tooltip placement="bottom" value={tooltip()} contentClass="max-w-[420px] px-3 py-2" class="shrink min-w-0">
<Button
type="button"
variant="ghost"
size="small"
class="group h-6 max-w-[280px] min-w-0 shrink items-center gap-1 rounded px-1 shadow-none text-13-regular text-text-weak transition-colors hover:bg-surface-raised-base-hover hover:text-text-strong"
data-component="pawwork-worktree-badge"
onClick={props.onClick}
aria-label={props.ariaLabel}
disabled={props.disabled}
>
<Icon
name="worktree"
size="small"
class="shrink-0 text-text-weak transition-colors group-hover:text-text-strong"
/>
<span class="min-w-0 truncate">{label()}</span>
</Button>
</Tooltip>
)
}
Loading