mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(panel): dense tooltip pass — work surfaces (270 tips)
Task lifecycle actions get a 25-action tip map, every dialog field and git panel control annotated; fixes two round-1 disabled-button tips that could never fire and a double data-state stamp on tab-progress.
This commit is contained in:
@@ -71,6 +71,9 @@ describe("CodeSnippet", () => {
|
||||
// The active line's content is "b"; its row carries the highlight class.
|
||||
const row = screen.getByText("b").parentElement;
|
||||
expect(row?.className).toContain("bg-blue-500/15");
|
||||
// The active line's number is now a real HelpTip trigger (Radix injects
|
||||
// data-state via asChild), not a bare span.
|
||||
expect(screen.getByText("2").getAttribute("data-state")).toBe("closed");
|
||||
});
|
||||
|
||||
it("renders the fail-open hint on an error", () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useGitFile } from "@/hooks/use-git";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
|
||||
interface CodeSnippetProps {
|
||||
branch: string | null | undefined;
|
||||
@@ -64,9 +65,11 @@ export function CodeSnippet({
|
||||
"bg-blue-500/15 ring-1 ring-inset ring-blue-500/30",
|
||||
)}
|
||||
>
|
||||
<span className="select-none w-8 shrink-0 text-right text-muted-foreground/60">
|
||||
{lineNo}
|
||||
</span>
|
||||
<HelpTip label={isActive ? "The line flagged in this finding" : ""}>
|
||||
<span className="select-none w-8 shrink-0 text-right text-muted-foreground/60">
|
||||
{lineNo}
|
||||
</span>
|
||||
</HelpTip>
|
||||
<span className="whitespace-pre">{text || " "}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -74,10 +77,12 @@ export function CodeSnippet({
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
{data.truncated && (
|
||||
<p className="border-t px-2 py-1 text-[10px] text-muted-foreground">
|
||||
showing lines {data.start_line}–{data.start_line + lines.length - 1}{" "}
|
||||
of {data.total_lines}
|
||||
</p>
|
||||
<HelpTip label="Only a window around the flagged line is loaded, not the whole file — widen `context` to see more.">
|
||||
<p className="border-t px-2 py-1 text-[10px] text-muted-foreground w-fit">
|
||||
showing lines {data.start_line}–
|
||||
{data.start_line + lines.length - 1} of {data.total_lines}
|
||||
</p>
|
||||
</HelpTip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -150,28 +150,51 @@ export function GitActionsPanel({
|
||||
open={showCommitDialog}
|
||||
onOpenChange={handleCommitDialogOpenChange}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
variant={hasStagedChanges ? "default" : "outline"}
|
||||
disabled={!hasStagedChanges}
|
||||
{/* HelpTip+span wraps the whole DialogTrigger, not the Button
|
||||
itself — the Button stays DialogTrigger's asChild target so
|
||||
Radix's Slot merge is undisturbed, and the outer span keeps the
|
||||
tip hoverable even while disabled (disabled sets
|
||||
pointer-events:none on the button, which would swallow hover on
|
||||
anything nested inside it). */}
|
||||
<HelpTip
|
||||
label={
|
||||
hasStagedChanges
|
||||
? "Records the staged files as a local commit — doesn't push to the remote."
|
||||
: "Nothing staged yet — stage files first before a commit can be made."
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="block w-full"
|
||||
tabIndex={!hasStagedChanges ? 0 : undefined}
|
||||
>
|
||||
<GitCommit className="h-4 w-4 mr-2" />
|
||||
Commit Changes
|
||||
{hasStagedChanges && (
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
{status?.staged_files.length} files
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
variant={hasStagedChanges ? "default" : "outline"}
|
||||
disabled={!hasStagedChanges}
|
||||
>
|
||||
<GitCommit className="h-4 w-4 mr-2" />
|
||||
Commit Changes
|
||||
{hasStagedChanges && (
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
{status?.staged_files.length} files
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</span>
|
||||
</HelpTip>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Commit Changes</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Commit Message</label>
|
||||
<HelpTip label="Becomes the commit's message — shows up in the log panel and the PR's commit history.">
|
||||
<label className="text-sm font-medium w-fit">
|
||||
Commit Message
|
||||
</label>
|
||||
</HelpTip>
|
||||
<Textarea
|
||||
placeholder="Describe your changes..."
|
||||
value={commitMessage}
|
||||
@@ -216,52 +239,82 @@ export function GitActionsPanel({
|
||||
</Dialog>
|
||||
|
||||
{/* Push Action */}
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
variant={canPush ? "default" : "outline"}
|
||||
disabled={!canPush || isPushing}
|
||||
onClick={() => onPush(false)}
|
||||
<HelpTip
|
||||
label={
|
||||
canPush
|
||||
? "git push -u origin <branch>. Uploads your local commits to GitHub — never force, safe to repeat."
|
||||
: "Nothing to push — no local commits sit ahead of the remote branch yet."
|
||||
}
|
||||
>
|
||||
{isPushing ? (
|
||||
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Push to Remote
|
||||
{status?.ahead !== undefined && status.ahead > 0 && (
|
||||
<HelpTip label="Commits not yet pushed to the remote repository">
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
<ArrowUp className="h-3 w-3 mr-1" />
|
||||
{status.ahead}
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
</Button>
|
||||
<span
|
||||
className="block w-full"
|
||||
tabIndex={!canPush ? 0 : undefined}
|
||||
>
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
variant={canPush ? "default" : "outline"}
|
||||
disabled={!canPush || isPushing}
|
||||
onClick={() => onPush(false)}
|
||||
>
|
||||
{isPushing ? (
|
||||
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Push to Remote
|
||||
{status?.ahead !== undefined && status.ahead > 0 && (
|
||||
<HelpTip label="Commits not yet pushed to the remote repository">
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
<ArrowUp className="h-3 w-3 mr-1" />
|
||||
{status.ahead}
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
|
||||
{/* Create PR Action */}
|
||||
<Dialog open={showPRDialog} onOpenChange={handlePRDialogOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
variant="outline"
|
||||
disabled={!canCreatePR}
|
||||
<HelpTip
|
||||
label={
|
||||
canCreatePR
|
||||
? "Opens GitHub's PR creation flow for this branch against main."
|
||||
: "Already on main with nothing ahead of it — there's no branch content to open a PR for."
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="block w-full"
|
||||
tabIndex={!canCreatePR ? 0 : undefined}
|
||||
>
|
||||
<GitPullRequest className="h-4 w-4 mr-2" />
|
||||
Create Pull Request
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
variant="outline"
|
||||
disabled={!canCreatePR}
|
||||
>
|
||||
<GitPullRequest className="h-4 w-4 mr-2" />
|
||||
Create Pull Request
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</span>
|
||||
</HelpTip>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Pull Request</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Badge variant="outline">{status?.current_branch}</Badge>
|
||||
<span>→</span>
|
||||
<Badge variant="outline">main</Badge>
|
||||
</div>
|
||||
<HelpTip label="Source branch on the left merges into the target branch on the right.">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground w-fit">
|
||||
<Badge variant="outline">{status?.current_branch}</Badge>
|
||||
<span>→</span>
|
||||
<Badge variant="outline">main</Badge>
|
||||
</div>
|
||||
</HelpTip>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Title</label>
|
||||
<HelpTip label="Becomes the pull request's title on GitHub.">
|
||||
<label className="text-sm font-medium w-fit">Title</label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
placeholder="PR title..."
|
||||
value={prTitle}
|
||||
@@ -269,7 +322,11 @@ export function GitActionsPanel({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Description</label>
|
||||
<HelpTip label="The PR's body on GitHub — markdown supported, shown to reviewers.">
|
||||
<label className="text-sm font-medium w-fit">
|
||||
Description
|
||||
</label>
|
||||
</HelpTip>
|
||||
<Textarea
|
||||
placeholder="Describe your changes..."
|
||||
value={prBody}
|
||||
@@ -300,23 +357,31 @@ export function GitActionsPanel({
|
||||
open={showMergeDialog}
|
||||
onOpenChange={handleMergeDialogOpenChange}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-full justify-start" variant="outline">
|
||||
{isMerging ? (
|
||||
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<GitMerge className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Merge PR
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<HelpTip label="Merges an open PR via GitHub's API — squash by default, falls back to merge/rebase if blocked by branch protection.">
|
||||
<span className="block w-full">
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-full justify-start" variant="outline">
|
||||
{isMerging ? (
|
||||
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<GitMerge className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Merge PR
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</span>
|
||||
</HelpTip>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Merge Pull Request</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">PR Number</label>
|
||||
<HelpTip label="The GitHub PR number to merge, e.g. the 42 in .../pull/42.">
|
||||
<label className="text-sm font-medium w-fit">
|
||||
PR Number
|
||||
</label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="e.g. 42"
|
||||
@@ -351,51 +416,63 @@ export function GitActionsPanel({
|
||||
</Dialog>
|
||||
|
||||
{/* Pull Action */}
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
variant="outline"
|
||||
disabled={isPulling}
|
||||
onClick={onPull}
|
||||
>
|
||||
{isPulling ? (
|
||||
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Pull from Remote
|
||||
</Button>
|
||||
|
||||
{/* Fetch Action */}
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
variant="outline"
|
||||
disabled={isFetching}
|
||||
onClick={onFetch}
|
||||
>
|
||||
{isFetching ? (
|
||||
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<RefreshCcw className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Fetch Remote
|
||||
</Button>
|
||||
|
||||
{/* Rebase Action — destructive, requires confirmation */}
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<HelpTip label="Fetches origin, then fast-forwards your branch onto it. Refuses (no merge commit) if history has diverged.">
|
||||
<span className="block w-full">
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
variant="outline"
|
||||
disabled={isRebasing}
|
||||
disabled={isPulling}
|
||||
onClick={onPull}
|
||||
>
|
||||
{isRebasing ? (
|
||||
{isPulling ? (
|
||||
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<GitGraph className="h-4 w-4 mr-2" />
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Rebase onto Remote
|
||||
Pull from Remote
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
</span>
|
||||
</HelpTip>
|
||||
|
||||
{/* Fetch Action */}
|
||||
<HelpTip label="Updates remote-tracking refs from origin only — never touches your working directory or local branch.">
|
||||
<span className="block w-full">
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
variant="outline"
|
||||
disabled={isFetching}
|
||||
onClick={onFetch}
|
||||
>
|
||||
{isFetching ? (
|
||||
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<RefreshCcw className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Fetch Remote
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
|
||||
{/* Rebase Action — destructive, requires confirmation */}
|
||||
<AlertDialog>
|
||||
<HelpTip label="Rewrites this branch's commit history on top of another branch. Requires a force-push afterward — use with care.">
|
||||
<span className="block w-full">
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
className="w-full justify-start"
|
||||
variant="outline"
|
||||
disabled={isRebasing}
|
||||
>
|
||||
{isRebasing ? (
|
||||
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<GitGraph className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Rebase onto Remote
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
</span>
|
||||
</HelpTip>
|
||||
<AlertDialogContent className="border-destructive bg-destructive/5">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Rebase onto target branch?</AlertDialogTitle>
|
||||
@@ -407,7 +484,11 @@ export function GitActionsPanel({
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="px-6 py-2 space-y-2">
|
||||
<label className="text-sm font-medium">Target branch</label>
|
||||
<HelpTip label="Branch/ref to replay commits onto, e.g. origin/main. On conflict, rebase auto-aborts — your tree stays untouched.">
|
||||
<label className="text-sm font-medium w-fit">
|
||||
Target branch
|
||||
</label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
placeholder="Remote ref (e.g. origin/HEAD)"
|
||||
value={rebaseTargetBranch}
|
||||
|
||||
@@ -92,19 +92,27 @@ export function GitBranchPanel({
|
||||
Branches
|
||||
</span>
|
||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
New
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<HelpTip label="Creates a new branch named {type}/{team}/{task-id}, branched from the task's parent branch (or the project default).">
|
||||
<span className="inline-block">
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
New
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</span>
|
||||
</HelpTip>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Task Branch</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Branch Type</label>
|
||||
<HelpTip label="Sets the branch-name prefix (feature/bug/chore/docs/hotfix) — doesn't otherwise change behavior.">
|
||||
<label className="text-sm font-medium w-fit">
|
||||
Branch Type
|
||||
</label>
|
||||
</HelpTip>
|
||||
<Select
|
||||
value={newBranchType}
|
||||
onValueChange={(v) => setNewBranchType(v as BranchType)}
|
||||
@@ -122,7 +130,11 @@ export function GitBranchPanel({
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Task ID</label>
|
||||
<HelpTip label="Identifies the task this branch is for — gets embedded in the branch name (shortened to 8 chars).">
|
||||
<label className="text-sm font-medium w-fit">
|
||||
Task ID
|
||||
</label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
placeholder="Enter task ID..."
|
||||
value={taskId}
|
||||
@@ -156,41 +168,56 @@ export function GitBranchPanel({
|
||||
<div className="space-y-3">
|
||||
{/* Local Branches */}
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1">
|
||||
Local ({localBranches.length})
|
||||
</h4>
|
||||
<HelpTip label="Branches that exist in your local clone's .git — checking one out is instant, no fetch needed.">
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1 w-fit">
|
||||
Local ({localBranches.length})
|
||||
</h4>
|
||||
</HelpTip>
|
||||
<div className="space-y-0.5">
|
||||
{localBranches.map((branch) => (
|
||||
<Button
|
||||
<HelpTip
|
||||
key={branch.name}
|
||||
onClick={() =>
|
||||
!branch.is_current && onCheckout(branch.name)
|
||||
}
|
||||
disabled={branch.is_current || isCheckingOut}
|
||||
variant="ghost"
|
||||
className={
|
||||
"w-full h-auto justify-between px-2 py-1.5 text-sm font-normal whitespace-normal " +
|
||||
(branch.is_current
|
||||
? "bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary"
|
||||
: "hover:bg-muted")
|
||||
label={
|
||||
branch.is_current
|
||||
? "This is the branch you're already on."
|
||||
: "Switches your workspace to this branch (git checkout). Non-conflicting uncommitted changes carry over."
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{branch.is_current && (
|
||||
<Check className="h-3 w-3 text-primary shrink-0" />
|
||||
)}
|
||||
<span className="truncate font-mono text-xs">
|
||||
{branch.name}
|
||||
</span>
|
||||
</div>
|
||||
{branch.last_commit && (
|
||||
<HelpTip label="Short commit hash on this branch">
|
||||
<span className="text-xs text-muted-foreground font-mono shrink-0">
|
||||
{branch.last_commit.slice(0, 7)}
|
||||
</span>
|
||||
</HelpTip>
|
||||
)}
|
||||
</Button>
|
||||
<span
|
||||
className="block"
|
||||
tabIndex={branch.is_current ? 0 : undefined}
|
||||
>
|
||||
<Button
|
||||
onClick={() =>
|
||||
!branch.is_current && onCheckout(branch.name)
|
||||
}
|
||||
disabled={branch.is_current || isCheckingOut}
|
||||
variant="ghost"
|
||||
className={
|
||||
"w-full h-auto justify-between px-2 py-1.5 text-sm font-normal whitespace-normal " +
|
||||
(branch.is_current
|
||||
? "bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary"
|
||||
: "hover:bg-muted")
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{branch.is_current && (
|
||||
<Check className="h-3 w-3 text-primary shrink-0" />
|
||||
)}
|
||||
<span className="truncate font-mono text-xs">
|
||||
{branch.name}
|
||||
</span>
|
||||
</div>
|
||||
{branch.last_commit && (
|
||||
<HelpTip label="Short commit hash on this branch">
|
||||
<span className="text-xs text-muted-foreground font-mono shrink-0">
|
||||
{branch.last_commit.slice(0, 7)}
|
||||
</span>
|
||||
</HelpTip>
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,23 +225,31 @@ export function GitBranchPanel({
|
||||
{/* Remote Branches */}
|
||||
{remoteBranches.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1 flex items-center gap-1">
|
||||
<Cloud className="h-3 w-3" />
|
||||
Remote ({remoteBranches.length})
|
||||
</h4>
|
||||
<HelpTip label="Branches on origin without a local counterpart yet — checking one out creates a local tracking branch.">
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1 flex items-center gap-1 w-fit">
|
||||
<Cloud className="h-3 w-3" />
|
||||
Remote ({remoteBranches.length})
|
||||
</h4>
|
||||
</HelpTip>
|
||||
<div className="space-y-0.5">
|
||||
{remoteBranches.map((branch) => (
|
||||
<Button
|
||||
<HelpTip
|
||||
key={branch.name}
|
||||
onClick={() => onCheckout(branch.name)}
|
||||
disabled={isCheckingOut}
|
||||
variant="ghost"
|
||||
className="w-full h-auto justify-start px-2 py-1.5 text-sm font-normal whitespace-normal hover:bg-muted"
|
||||
label="Creates a local branch tracking this remote ref and switches to it."
|
||||
>
|
||||
<span className="truncate font-mono text-xs text-muted-foreground">
|
||||
{branch.name}
|
||||
<span className="block">
|
||||
<Button
|
||||
onClick={() => onCheckout(branch.name)}
|
||||
disabled={isCheckingOut}
|
||||
variant="ghost"
|
||||
className="w-full h-auto justify-start px-2 py-1.5 text-sm font-normal whitespace-normal hover:bg-muted"
|
||||
>
|
||||
<span className="truncate font-mono text-xs text-muted-foreground">
|
||||
{branch.name}
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
</Button>
|
||||
</HelpTip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { GitDiffViewer } from "./git-diff-viewer";
|
||||
import { GitActionsPanel } from "./git-actions-panel";
|
||||
import { GitBranch, FolderGit2 } from "lucide-react";
|
||||
import { useGitBrowser } from "@/hooks/use-git-browser";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
|
||||
function GitBrowserContent() {
|
||||
const {
|
||||
@@ -80,25 +81,29 @@ function GitBrowserContent() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Project Selector */}
|
||||
<Select value={projectSlug} onValueChange={handleProjectChange}>
|
||||
<SelectTrigger className="w-full sm:w-64">
|
||||
<FolderGit2 className="h-4 w-4 mr-2" />
|
||||
<SelectValue placeholder="Select a project..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{loadingProjects ? (
|
||||
<div className="p-2">
|
||||
<Skeleton className="h-8 w-full" />
|
||||
</div>
|
||||
) : (
|
||||
projects?.map((p) => (
|
||||
<SelectItem key={p.id} value={p.slug}>
|
||||
{p.name}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<HelpTip label="Switches every panel below (status, branches, log, diff) to this project's clone on disk.">
|
||||
<span className="w-full sm:w-64">
|
||||
<Select value={projectSlug} onValueChange={handleProjectChange}>
|
||||
<SelectTrigger className="w-full sm:w-64">
|
||||
<FolderGit2 className="h-4 w-4 mr-2" />
|
||||
<SelectValue placeholder="Select a project..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{loadingProjects ? (
|
||||
<div className="p-2">
|
||||
<Skeleton className="h-8 w-full" />
|
||||
</div>
|
||||
) : (
|
||||
projects?.map((p) => (
|
||||
<SelectItem key={p.id} value={p.slug}>
|
||||
{p.name}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -104,21 +104,30 @@ export function GitDiffViewer({
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<CardTitle className="text-sm flex items-center gap-2">
|
||||
<FileCode className="h-4 w-4" />
|
||||
Changes
|
||||
</CardTitle>
|
||||
<Button
|
||||
variant={wrap ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs"
|
||||
aria-pressed={wrap}
|
||||
onClick={() => setWrap((w) => !w)}
|
||||
title="Toggle line wrap"
|
||||
<HelpTip label="Green = added lines, red = removed, blue = hunk header (@@ ... @@ shows the surrounding line range).">
|
||||
<CardTitle className="text-sm flex items-center gap-2 w-fit">
|
||||
<FileCode className="h-4 w-4" />
|
||||
Changes
|
||||
</CardTitle>
|
||||
</HelpTip>
|
||||
<HelpTip
|
||||
label={
|
||||
wrap
|
||||
? "Long lines wrap to fit the panel — click to scroll horizontally instead."
|
||||
: "Long lines overflow with horizontal scroll — click to wrap them instead."
|
||||
}
|
||||
>
|
||||
<WrapText className="h-3.5 w-3.5 mr-1" />
|
||||
Wrap
|
||||
</Button>
|
||||
<Button
|
||||
variant={wrap ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs"
|
||||
aria-pressed={wrap}
|
||||
onClick={() => setWrap((w) => !w)}
|
||||
>
|
||||
<WrapText className="h-3.5 w-3.5 mr-1" />
|
||||
Wrap
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
|
||||
@@ -62,17 +62,22 @@ export function GitLogPanel({
|
||||
<CardTitle className="text-sm flex items-center gap-2">
|
||||
<GitCommit className="h-4 w-4" />
|
||||
Commit History
|
||||
<Badge variant="secondary" className="ml-auto text-xs">
|
||||
{log.branch}
|
||||
</Badge>
|
||||
<HelpTip label="Commits reachable from this branch, newest first.">
|
||||
<Badge variant="secondary" className="ml-auto text-xs">
|
||||
{log.branch}
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<ScrollArea className="h-80">
|
||||
<div className="p-4 space-y-0">
|
||||
{log.commits.map((commit, index) => (
|
||||
<Button
|
||||
<HelpTip
|
||||
key={commit.hash}
|
||||
label={onSelectCommit ? "Select this commit" : ""}
|
||||
>
|
||||
<Button
|
||||
onClick={() => onSelectCommit?.(commit)}
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
@@ -120,16 +125,19 @@ export function GitLogPanel({
|
||||
<User className="h-3 w-3" />
|
||||
{commit.author}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{formatDistanceToNow(new Date(commit.date), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
<HelpTip label={new Date(commit.date).toLocaleString()}>
|
||||
<span className="flex items-center gap-1 w-fit">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{formatDistanceToNow(new Date(commit.date), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</HelpTip>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -70,20 +70,24 @@ export function GitStatusPanel({ status, isLoading }: GitStatusPanelProps) {
|
||||
Repository Status
|
||||
</span>
|
||||
{!hasChanges && (
|
||||
<Badge variant="outline" className="text-green-600">
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
Clean
|
||||
</Badge>
|
||||
<HelpTip label="No staged, modified, or untracked files — the working tree matches HEAD.">
|
||||
<Badge variant="outline" className="text-green-600">
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
Clean
|
||||
</Badge>
|
||||
</HelpTip>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Branch Info */}
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-medium">{status.current_branch}</span>
|
||||
</div>
|
||||
<HelpTip label="The branch currently checked out in this project's workspace.">
|
||||
<div className="flex items-center gap-2 w-fit">
|
||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-medium">{status.current_branch}</span>
|
||||
</div>
|
||||
</HelpTip>
|
||||
{(status.ahead > 0 || status.behind > 0) && (
|
||||
<div className="flex items-center gap-2">
|
||||
{status.ahead > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user