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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ jobs:
- name: Check
run: vp check

- name: Check className overrides on components/ui do not grow
run: vp run lint:restyle-ceiling

- name: Typecheck
run: vpr typecheck

Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ Full glossary with file links: `docs/internals/glossary.md`
## Taste

- Complexity belongs at the adapter boundary. Orchestration stays pure, UI stays dumb.
- `apps/web/src/components/ui` exports own their look. Pick a `variant` or `size`; do not restyle one with `className`. If none fits, add a variant to the component, not classes at the call site. Layout classes (width, flex, margin, position) belong on the parent. `shadcn/no-restyle` reports violations and CI caps their count.
- Inferred types over annotations. `any` is the enemy.
- Comments describe how a thing is used, and move when the code moves. To be used mostly to describe functions, not to annotate every line of behavior.
- Our users drive agents all day and notice a dropped frame, a lying spinner, and a stale label. No continuously repainting animations; they peg the GPU on high-refresh displays.
Expand Down
42 changes: 21 additions & 21 deletions apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7155,27 +7155,27 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
const values = Map.prototype.values;
yield* Effect.acquireRelease(
Effect.sync(() =>
vi
.spyOn(Map.prototype, "values")
.mockImplementation(function (this: Map<unknown, unknown>) {
const iterator = values.call(this);
const next = iterator.next.bind(iterator);
iterator.next = () => {
const result = next();
const value: unknown = result.value;
if (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof value.id === "string" &&
value.id.startsWith("history-part-")
) {
visitedHistoryParts += 1;
}
return result;
};
return iterator;
}),
vi.spyOn(Map.prototype, "values").mockImplementation(function (
this: Map<unknown, unknown>,
) {
const iterator = values.call(this);
const next = iterator.next.bind(iterator);
iterator.next = () => {
const result = next();
const value: unknown = result.value;
if (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof value.id === "string" &&
value.id.startsWith("history-part-")
) {
visitedHistoryParts += 1;
}
return result;
};
return iterator;
}),
),
(spy) => Effect.sync(() => spy.mockRestore()),
);
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/browser/HostedBrowserWebview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,26 @@ export function HostedBrowserWebview(props: {
}
}, recovery.delayMs);
};
// A click inside the guest only reaches this document as a webview focus
// event, so open menus and popovers never see the outside press that
// would dismiss them. Replay it as a pointerdown on the webview itself.
const dismissHostPopups = () => {
webview.dispatchEvent(
new PointerEvent("pointerdown", { bubbles: true, pointerType: "mouse" }),
);
};
webview.addEventListener("did-attach", register);
webview.addEventListener("dom-ready", register);
webview.addEventListener("render-process-gone", recoverGuest);
webview.addEventListener("focus", dismissHostPopups);
register();
return () => {
disposed = true;
if (recoveryTimeout !== null) clearTimeout(recoveryTimeout);
webview.removeEventListener("did-attach", register);
webview.removeEventListener("dom-ready", register);
webview.removeEventListener("render-process-gone", recoverGuest);
webview.removeEventListener("focus", dismissHostPopups);
};
}, [clientSettingsHydrated, config, initialSrc, runtimeTabId, webviewGeneration]);

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/AppSidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
side="left"
collapsible="offcanvas"
data-app-sidebar=""
className="border-r border-sidebar-border bg-sidebar text-sidebar-foreground"
className="border-r border-sidebar-border"
resizable={{
maxWidth: sidebarMaximumWidth,
minWidth: THREAD_SIDEBAR_MIN_WIDTH,
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -861,7 +861,7 @@ export function BranchToolbarBranchSelector({
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<ComboboxEmpty>No refs found.</ComboboxEmpty>
<div className="relative min-h-0 w-full max-h-56 flex-1 overflow-hidden">
<ComboboxListVirtualized className="size-full min-w-0 p-0">
<ComboboxListVirtualized>
<LegendList<string>
ref={branchListRef}
data={filteredBranchPickerItems}
Expand Down Expand Up @@ -905,7 +905,7 @@ export function BranchToolbarBranchSelector({
className="flex cursor-pointer items-center justify-between gap-3 border-t border-border/60 px-3 py-2 text-xs"
>
<span className="flex min-w-0 items-center gap-1.5 font-medium text-muted-foreground">
<RefreshIcon aria-hidden="true" className="size-3 shrink-0 opacity-70" />
<RefreshIcon aria-hidden="true" size="xs" className="shrink-0" />
<span className="truncate">Start from origin</span>
</span>
<Switch
Expand Down
12 changes: 0 additions & 12 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,6 @@ import {
import { BranchToolbar, type BranchToolbarHandle } from "./BranchToolbar";
import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings";
import { isEditableFocused } from "../lib/editableFocus";
import { undoLatestThreadAction } from "../hooks/showUndoToast";
import ThreadTerminalDrawer from "./ThreadTerminalDrawer";
import {
AlarmClockIcon,
Expand Down Expand Up @@ -6731,17 +6730,6 @@ export default function ChatView(props: ChatViewProps) {
return;
}

if (command === "thread.undo") {
// Only claim the chord when there is an Undo to run; otherwise the
// page keeps its native behavior for the key.
if (event.repeat) return;
if (undoLatestThreadAction()) {
event.preventDefault();
event.stopPropagation();
}
return;
}

if (command === "thread.pin") {
event.preventDefault();
event.stopPropagation();
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ConfirmDialogHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function ConfirmDialogHost() {
if (!open) completeConfirmDialogClose();
}}
>
<AlertDialogPopup className="max-w-lg">
<AlertDialogPopup>
<AlertDialogHeader>
<AlertDialogTitle className="wrap-anywhere">{copy.title}</AlertDialogTitle>
{copy.description ? (
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ export default function DiffPanel({
/>
}
>
<RefreshIcon className="size-3.5" refreshing={isRefreshingDiff} />
<RefreshIcon size="sm" refreshing={isRefreshingDiff} />
</TooltipTrigger>
<TooltipPopup side="top">
{isRefreshingDiff ? "Refreshing diff…" : "Refresh diff"}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
aria-live="polite"
className="flex items-center gap-2 rounded-md border border-input bg-muted/40 px-3 py-2 text-xs text-muted-foreground dark:border-transparent dark:bg-white/[0.035]"
>
<Spinner className="size-3.5" aria-hidden />
<Spinner size="sm" aria-hidden />
Publishing repository to {publishProviderLabel}...
</div>
) : null}
Expand Down Expand Up @@ -938,7 +938,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
<Button disabled={!canSubmitPublishRepository} onClick={submitPublishRepository}>
{publishRepositoryAction.isPending ? (
<>
<Spinner className="size-3.5" aria-hidden />
<Spinner size="sm" aria-hidden />
Publishing...
</>
) : (
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/components/NoActiveThreadState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { WorkspacePageHeader } from "./WorkspacePageHeader";

export function NoActiveThreadState() {
return (
<SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">
<SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none">
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
<WorkspacePageHeader electron={isElectron} className="border-b border-border">
{isElectron ? (
Expand All @@ -22,8 +22,8 @@ export function NoActiveThreadState() {
<Empty className="flex-1">
<div className="w-full max-w-lg px-8 py-12">
<EmptyHeader className="max-w-none">
<EmptyTitle className="text-foreground text-xl">Pick a thread to continue</EmptyTitle>
<EmptyDescription className="mt-2 text-sm text-muted-foreground/78">
<EmptyTitle className="text-foreground">Pick a thread to continue</EmptyTitle>
<EmptyDescription className="mt-2 text-muted-foreground/78">
Select an existing thread or create a new one to get started.
</EmptyDescription>
</EmptyHeader>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/NoProjectsHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ export function NoProjectsHero() {
const openAddProject = useCallback(() => openCommandPalette({ open: "add-project" }), []);

return (
<SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">
<SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none">
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
<Empty className="flex-1">
<div className="w-full max-w-lg px-8 py-12">
<EmptyHeader className="max-w-none">
<EmptyTitle className="text-foreground text-2xl sm:text-3xl">
What should we work on?
</EmptyTitle>
<EmptyDescription className="mt-2 text-sm text-muted-foreground/78">
<EmptyDescription className="mt-2 text-muted-foreground/78">
Add a project to start your first thread.
</EmptyDescription>
<div className="mt-6 flex justify-center">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ProjectScriptsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export default function ProjectScriptsControl({
>
<ScriptIcon icon={fileScript.icon ?? "play"} className="size-4" />
<MenuItemLabel className="truncate">{fileScript.name}</MenuItemLabel>
<MenuShortcut className="ms-auto">
<MenuShortcut>
<DownloadIcon className="size-3.5" aria-label="Import" />
</MenuShortcut>
</MenuItem>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ProviderUpdateEnvironmentRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function EnvironmentUpdateRow({
let trailing: ReactNode;
switch (status.kind) {
case "loading":
trailing = <Spinner className="size-4 text-muted-foreground" />;
trailing = <Spinner size="md" tone="muted" />;
break;
case "success":
trailing = <CheckIcon aria-hidden="true" className="size-4 text-success" />;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/PullRequestThreadDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ export function PullRequestThreadDialog({

{isResolving ? (
<div className="flex items-center gap-2 text-muted-foreground text-xs">
<Spinner className="size-3.5" />
<Spinner size="sm" />
Resolving {terminology.singular}...
</div>
) : null}
Expand Down
44 changes: 10 additions & 34 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3077,9 +3077,7 @@ export default function Sidebar() {
settlingThreadKeysRef.current.add(threadKey);
try {
const navigateAfterSettle = planForwardNavigation(threadKey, opts.coSettlingKeys);
const result = await settleThread(threadRef, {
undoToast: opts.coSettlingKeys === undefined,
});
const result = await settleThread(threadRef);
if (result._tag === "Failure") {
// Never navigate away from a thread that did not settle.
if (!isAtomCommandInterrupted(result)) {
Expand Down Expand Up @@ -3731,9 +3729,7 @@ export default function Sidebar() {
// Snoozing the open thread moves you forward, same as settle —
// both park the thread you're done with for now.
const navigateAfterSnooze = planForwardNavigation(threadKey, opts.coSnoozingKeys);
const result = await snoozeThread(threadRef, preset.snoozedUntil, {
undoToast: opts.coSnoozingKeys === undefined,
});
const result = await snoozeThread(threadRef, preset.snoozedUntil);
if (result._tag === "Failure") {
// Never navigate away from a thread that did not snooze.
return isAtomCommandInterrupted(result)
Expand Down Expand Up @@ -3887,35 +3883,15 @@ export default function Sidebar() {
outcome.status === "failure" ? [outcome.error] : [],
);

if (snoozedThreadRefs.length > 0) {
const snoozedCount = snoozedThreadRefs.length;
const failedCount = failures.length;
toastManager.add(
stackedThreadToast({
type: failedCount > 0 ? "warning" : "success",
title:
failedCount > 0
? `Snoozed ${snoozedCount} of ${selectedThreads.length} threads`
: `Snoozed ${snoozedCount} thread${snoozedCount === 1 ? "" : "s"}`,
description:
failedCount > 0
? `${failedCount} thread${failedCount === 1 ? "" : "s"} couldn't be snoozed.`
: undefined,
timeout: 5_000,
actionProps: {
children: "Undo",
onClick: () => {
for (const threadRef of snoozedThreadRefs) attemptUnsnooze(threadRef);
},
},
}),
);
} else if (failures.length > 0) {
if (failures.length > 0) {
const firstError = failures[0];
toastManager.add(
stackedThreadToast({
type: "error",
title: "Failed to snooze threads",
title:
snoozedThreadRefs.length > 0
? `Failed to snooze ${failures.length} thread${failures.length === 1 ? "" : "s"}`
: "Failed to snooze threads",
description:
firstError instanceof Error ? firstError.message : "An error occurred.",
}),
Expand Down Expand Up @@ -4019,7 +3995,6 @@ export default function Sidebar() {
},
[
attemptSettle,
attemptSnooze,
attemptUnpin,
clearSelection,
confirmThreadDelete,
Expand All @@ -4028,7 +4003,6 @@ export default function Sidebar() {
performSnooze,
removeFromSelection,
serverConfigs,
attemptUnsnooze,
updateThreadMetadata,
timestampFormat,
],
Expand Down Expand Up @@ -4798,7 +4772,9 @@ export default function Sidebar() {
key={threadKey}
id={threadKey}
disabled={
!draggableThreadKeys.has(threadKey) || optimisticDrop !== null
renamingThreadKey === threadKey ||
!draggableThreadKeys.has(threadKey) ||
optimisticDrop !== null
}
>
{(bag) => renderThreadRowInner(thread, section, bag)}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ThreadRouteView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export function ThreadRouteView({ target }: { target: ThreadRouteTarget }) {
}

return (
<SidebarInset className="h-svh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground md:h-dvh">
<SidebarInset className="h-svh min-h-0 overflow-hidden overscroll-y-none md:h-dvh">
{view}
</SidebarInset>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/ComposerPrimaryActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
</span>
) : null}
{isConnecting || isSendBusy ? (
<Spinner className="size-3.5" aria-hidden="true" />
<Spinner size="sm" aria-hidden="true" />
) : (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
<path
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2567,7 +2567,7 @@ function BackgroundWorktreeSetupChip({ snapshot }: { snapshot: WorktreeSetupSnap
/>
}
>
<Spinner className="size-3 shrink-0" />
<Spinner size="xs" className="shrink-0" />
<span className="truncate">{scriptName}</span>
</PopoverTrigger>
<PopoverPopup
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/ModelPickerContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {

{/* Model list */}
<div className="relative min-h-0 flex-1 overflow-hidden pr-px">
<ComboboxListVirtualized className="size-full min-w-0 p-0 not-empty:p-0">
<ComboboxListVirtualized className="not-empty:p-0">
<LegendList<string>
ref={modelListRef}
data={filteredItemKeys}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/WorktreeSetupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function StageIcon({ status }: { status: WorktreeSetupStage["status"] }) {
case "done":
return <CheckIcon aria-hidden className={className} />;
case "running":
return <Spinner className={className} />;
return <Spinner size="md" className="shrink-0" />;
case "failed":
return <XIcon aria-hidden className={className} />;
case "warning":
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/clerk/ClerkUserProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function ClerkUserProfileRefreshButton({
disabled={disabled || isPending}
onClick={onClick}
>
<RefreshIcon aria-hidden="true" className="size-3.5" refreshing={isPending} />
<RefreshIcon aria-hidden="true" size="sm" refreshing={isPending} />
Refresh
</Button>
);
Expand Down
Loading
Loading