Skip to content
Open
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
6 changes: 6 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["import", "typescript", "unicorn", "react", "react-perf", "vitest", "oxc"],
"overrides": [
{
"files": ["apps/native/src/components/widget/drift/**/*.tsx"],
"rules": {
"unicorn/no-nested-ternary": "error"
}
},
{
"files": [
"**/*.spec.{js,mjs,ts,tsx}",
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import { Button } from "@/components/ui/button";
import { Check } from "lucide-react";

interface DriftDiscardConfirmationProps {
isManualDrift: boolean;
onCancel: () => void;
onConfirm: () => void;
total: number;
}

export function DriftDiscardConfirmation({
isManualDrift,
onCancel,
onConfirm,
total,
}: DriftDiscardConfirmationProps) {
return (
<div className="flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-3">
<p className="text-foreground text-sm">
Discard all {total} {isManualDrift ? "manual " : ""}
{total === 1 ? "change" : "changes"}? This reverts to the tracked state and cannot be undone.
</p>
<div className="flex shrink-0 items-center gap-2">
<Button variant="ghost" size="sm" onClick={onCancel} className="text-muted-foreground">
Cancel
</Button>
<Button variant="destructive" size="sm" onClick={onConfirm}>
<Check className="h-3.5 w-3.5" aria-hidden="true" />
Discard
</Button>
</div>
</div>
);
}
175 changes: 175 additions & 0 deletions apps/native/src/components/widget/drift/drift-review-actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"use client";

import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ConfirmButton } from "@/components/widget/controls/confirm-button";
import { cn } from "@/lib/utils";
import {
ArrowLeft,
ChevronDown,
CircleAlert,
GitCommitHorizontal,
Loader2,
Sparkles,
Trash2,
Wrench,
} from "lucide-react";

const busyButtonSheenClassName =
"relative overflow-hidden disabled:opacity-100 after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-[linear-gradient(110deg,transparent,rgba(255,255,255,0.3),transparent)] after:bg-size-[200%_100%] after:content-[''] motion-safe:after:animate-shimmer";

interface DriftReviewActionsProps {
buildChecking: boolean;
buildCheckFailed: boolean;
buildReady: boolean;
isApplyBusy: boolean;
isManualDrift: boolean;
onApply: () => void;
onBackToPrompt: () => void;
onRefineWithAi: () => void;
onRequestDiscard: () => void;
rebuildRunning: boolean;
}

export function DriftReviewActions({
buildChecking,
buildCheckFailed,
buildReady,
isApplyBusy,
isManualDrift,
onApply,
onBackToPrompt,
onRefineWithAi,
onRequestDiscard,
rebuildRunning,
}: DriftReviewActionsProps) {
const isBusy = buildChecking || isApplyBusy || rebuildRunning;
let statusMessage: string | null = null;
let buildButtonLabel = "Build & Test";

if (buildCheckFailed) {
statusMessage = "Build check failed";
buildButtonLabel = "Check failed";
}

if (isApplyBusy) {
statusMessage = "Applying configuration";
buildButtonLabel = "Applying…";
}

if (rebuildRunning) {
statusMessage = "Building and testing changes";
buildButtonLabel = "Building…";
}

if (buildChecking) {
statusMessage = "Checking changes before build";
buildButtonLabel = "Checking…";
}

return (
<footer className="flex items-center justify-between gap-3 border-border/60 border-t pt-3">
<Button
variant="ghost"
size="sm"
onClick={onRequestDiscard}
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
Discard
</Button>

<div className="flex items-center gap-2">
{statusMessage && (
<span
className={cn(
"flex items-center gap-1.5 text-xs",
buildCheckFailed && !isBusy ? "text-destructive" : "text-muted-foreground",
)}
role={buildCheckFailed && !isBusy ? "alert" : "status"}
>
{isBusy ? (
<Loader2 className="h-3 w-3 animate-spin text-teal-500" aria-hidden="true" />
) : (
<CircleAlert className="h-3 w-3" aria-hidden="true" />
)}
{statusMessage}
</span>
)}

{!isManualDrift && (
<Button
variant="ghost"
size="sm"
onClick={onBackToPrompt}
className="text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="h-3.5 w-3.5" aria-hidden="true" />
Back to Prompt
</Button>
)}

<div className="flex items-center">
<ConfirmButton
size="sm"
disabled={!buildReady}
aria-busy={isBusy}
title={statusMessage ?? undefined}
className={cn(isManualDrift && "rounded-r-none", isBusy && busyButtonSheenClassName)}
confirmPrefKey="confirmBuild"
onConfirm={onApply}
message="Rebuild with these configuration changes?"
color="teal"
>
<Wrench className="h-3.5 w-3.5" aria-hidden="true" />
{buildButtonLabel}
</ConfirmButton>

{isManualDrift && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="sm"
disabled={isBusy}
aria-busy={isBusy}
aria-label="More build options"
className={cn(
"rounded-l-none border-primary-foreground/20 border-l px-2",
isBusy && busyButtonSheenClassName,
)}
>
<ChevronDown className="h-4 w-4" aria-hidden="true" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="end" className="w-56">
<DropdownMenuItem onSelect={onRefineWithAi}>
<Sparkles />
<span>
Refine with AI first
<span className="block text-[10px] text-muted-foreground">
Adopt these changes into an AI session
</span>
</span>
</DropdownMenuItem>
<DropdownMenuItem disabled>
<GitCommitHorizontal />
<span>
Commit without building
<span className="block text-[10px] text-muted-foreground">
Track as-is, skip rebuild — coming soon
</span>
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
</footer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client";

import { ConfirmButton } from "@/components/widget/controls/confirm-button";
import { cn } from "@/lib/utils";
import { Wrench, CircleCheckBig } from "lucide-react";

const busyButtonSheenClassName =
"relative overflow-hidden disabled:opacity-100 after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-[linear-gradient(110deg,transparent,rgba(255,255,255,0.3),transparent)] after:bg-size-[200%_100%] after:content-[''] motion-safe:after:animate-shimmer";

interface DriftReviewBuildCardProps {
buildReady: boolean;
isApplyBusy: boolean;
rebuildRunning: boolean;
onApply: () => void;
}

export function DriftReviewBuildCard({
buildReady,
isApplyBusy,
rebuildRunning,
onApply,
}: DriftReviewBuildCardProps) {
const isBusy = isApplyBusy || rebuildRunning;
let buildButtonLabel = "Build & Test";
let buildButtonTitle: string | undefined;

if (isApplyBusy) {
buildButtonLabel = "Applying…";
buildButtonTitle = "Applying configuration";
}

if (rebuildRunning) {
buildButtonLabel = "Building…";
buildButtonTitle = "Building and testing changes";
}

return (
<div className="flex flex-col gap-5 rounded-xl border border-border/70 bg-card/50 p-5">
<div className="flex items-start gap-3">
<div className="mt-0.5 rounded-full bg-teal-500/10 p-2 text-teal-500">
<CircleCheckBig className="h-5 w-5" aria-hidden="true" />
</div>
<div className="space-y-1">
<h2 className="font-semibold text-foreground">New configuration updates are available</h2>
<p className="max-w-xl text-muted-foreground text-sm leading-relaxed">
This Mac isn’t using the latest configuration yet. Apply the updates to bring it up to
date.
</p>
</div>
</div>

<footer className="flex justify-end border-border/60 border-t pt-4">
<ConfirmButton
size="sm"
disabled={!buildReady}
aria-busy={isBusy}
title={buildButtonTitle}
className={cn(isBusy && busyButtonSheenClassName)}
confirmPrefKey="confirmBuild"
onConfirm={onApply}
message="Build and apply your saved configuration?"
color="teal"
>
<Wrench className="h-3.5 w-3.5" aria-hidden="true" />
{buildButtonLabel}
</ConfirmButton>
</footer>
</div>
);
}
31 changes: 31 additions & 0 deletions apps/native/src/components/widget/drift/drift-review-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import { DriftFileRow } from "./drift-file-row";
import { DriftSummaryView } from "./drift-summary-view";
import type { DriftFileRowData } from "./drift-utils";
import type { DriftView } from "./drift-review-types";

interface DriftReviewContentProps {
files: DriftFileRowData[];
view: DriftView;
}

export function DriftReviewContent({ files, view }: DriftReviewContentProps) {
return (
<div>
{view === "summary" ? (
<DriftSummaryView />
) : (
<ul className="divide-y divide-border/50">
{files.map((file, index) => (
<DriftFileRow
key={file.hash}
file={file}
defaultOpen={index === 0}
/>
))}
</ul>
)}
</div>
);
}
48 changes: 48 additions & 0 deletions apps/native/src/components/widget/drift/drift-review-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";

import { AnimatedTabsList, AnimatedTabsTrigger } from "@/components/ui/animated-tabs";
import { Badge } from "@/components/ui/badge";
import { Tabs } from "@/components/ui/tabs";
import { formatDriftCounts, type DriftSummaryCounts } from "./drift-utils";
import type { DriftView } from "./drift-review-types";
import { ListTree, MessageSquareText } from "lucide-react";

interface DriftReviewHeaderProps {
counts: DriftSummaryCounts;
isManualDrift: boolean;
onViewChange: (view: DriftView) => void;
view: DriftView;
}

export function DriftReviewHeader({
counts,
isManualDrift,
onViewChange,
view,
}: DriftReviewHeaderProps) {
return (
<header className="flex items-center justify-between gap-3 pb-2">
<div className="flex min-w-0 items-center gap-2">
<span className="font-semibold text-foreground text-sm">
{isManualDrift ? "Detected changes" : "Proposed changes"}
</span>
<Badge variant="secondary" className="font-mono text-muted-foreground">
{formatDriftCounts(counts)}
</Badge>
</div>

<Tabs value={view} onValueChange={(value) => onViewChange(value as DriftView)}>
<AnimatedTabsList value={view}>
<AnimatedTabsTrigger value="summary">
<MessageSquareText className="h-3.5 w-3.5" aria-hidden="true" />
Semantic
</AnimatedTabsTrigger>
<AnimatedTabsTrigger value="files">
<ListTree className="h-3.5 w-3.5" aria-hidden="true" />
Diff
</AnimatedTabsTrigger>
</AnimatedTabsList>
</Tabs>
</header>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type DriftView = "summary" | "files";
Loading
Loading