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
1 change: 1 addition & 0 deletions apps/web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
@import "./chat/styles.css";
@import "./bench/styles.css";
@import "./settings/styles.css";
@import "./button-roles.css";

/* react-ui's `.corbits-toast` ships a hardcoded horizontal-center transform
(`left: 50%; transform: translateX(-50%) !important`) regardless of the
Expand Down
22 changes: 22 additions & 0 deletions apps/web/src/button-roles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* Button roles from DESIGN.md "Color roles". Primary is charcoal via the
`--primary` token; its icon carries the orange. Orange never fills. */
[data-slot="button"].bg-primary svg {
color: var(--action);
}

/* Danger-ghost: text-only red for everyday refusals (Deny, Remove). */
[data-slot="button"].btn-danger-ghost {
color: var(--danger);
}
[data-slot="button"].btn-danger-ghost:hover {
background: var(--danger-wash);
color: var(--danger);
}
[data-slot="button"].bg-destructive:hover {
background: var(--danger-hover);
opacity: 1;
}
[data-slot="button"].bg-destructive:focus-visible,
[data-slot="button"].btn-danger-ghost:focus-visible {
outline-color: var(--danger);
}
1 change: 1 addition & 0 deletions apps/web/src/chat/approval-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function ApprovalRow({
<Button
variant="ghost"
size="sm"
className="btn-danger-ghost"
disabled={pending !== null}
onClick={() => resolveMutation.mutate("deny")}
>
Expand Down
10 changes: 8 additions & 2 deletions apps/web/src/chat/blocks/approve-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,13 @@ function ApproveButtons({
<Button type="button" variant="primary" disabled={busy} onClick={onApprove}>
{deciding === "approve" ? CHAT_STRINGS.blockApproveApproving : actionLabel}
</Button>
<Button type="button" variant="outline" disabled={busy} onClick={onDeny}>
<Button
type="button"
variant="ghost"
className="btn-danger-ghost"
disabled={busy}
onClick={onDeny}
>
{deciding === "reject"
? CHAT_STRINGS.blockApproveRejecting
: CHAT_STRINGS.blockDenyAction}
Expand Down Expand Up @@ -227,7 +233,7 @@ export function ApproveBlockView({
<Button type="button" variant="primary" disabled>
{CHAT_STRINGS.blockApproveAction}
</Button>
<Button type="button" variant="outline" disabled>
<Button type="button" variant="ghost" className="btn-danger-ghost" disabled>
{CHAT_STRINGS.blockDenyAction}
</Button>
</div>
Expand Down
60 changes: 60 additions & 0 deletions apps/web/src/components/confirm-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useRef, useState } from "react";
import type { ReactNode } from "react";
import { Button, type ButtonProps } from "@corbits/react-ui";

export type ConfirmButtonProps = Omit<ButtonProps, "onClick" | "children" | "variant"> & {
/** Fires on the second, confirming click only. */
readonly onConfirm: () => void;
readonly children: ReactNode;
/** Armed label, e.g. "Delete X permanently". */
readonly confirmLabel: ReactNode;
};

/**
* Two-step destructive action: the first click arms it (solid red plus a
* Cancel), the second commits. Escape, Cancel, or focus leaving disarms.
*/
export function ConfirmButton({ onConfirm, children, confirmLabel, ...props }: ConfirmButtonProps) {
const [armed, setArmed] = useState(false);
const rootRef = useRef<HTMLSpanElement>(null);

return (
<span
ref={rootRef}
className="inline-flex items-center gap-1"
onKeyDown={(e) => {
if (e.key === "Escape") setArmed(false);
}}
onBlur={(e) => {
if (!rootRef.current?.contains(e.relatedTarget as Node | null)) setArmed(false);
}}
>
{armed ? (
<Button
type="button"
variant="ghost"
size={props.size ?? "sm"}
onClick={() => setArmed(false)}
>
Cancel
</Button>
) : null}
<Button
type="button"
{...props}
variant={armed ? "destructive" : "ghost"}
className={armed ? props.className : `btn-danger-ghost ${props.className ?? ""}`}
onClick={() => {
if (!armed) {
setArmed(true);
return;
}
setArmed(false);
onConfirm();
}}
>
<span aria-live="polite">{armed ? confirmLabel : children}</span>
</Button>
</span>
);
}
9 changes: 6 additions & 3 deletions apps/web/src/pages/tools-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
Card,
CardDescription,
CardTitle,
ConfirmButton,
Input,
PageShell,
RichEmptyState,
Expand Down Expand Up @@ -36,6 +35,7 @@ import {
import { useDeployedToolPackages } from "../tools/deployed-tool-packages";
import { useBench } from "../bench-context";
import { StageTopBar } from "../shell/stage-top-bar";
import { ConfirmButton } from "../components/confirm-button";

const AUTH_LABEL: Record<McpServer["auth"], string> = {
none: "No sign-in",
Expand Down Expand Up @@ -84,7 +84,6 @@ function McpServersTable({
</TableCell>
<TableCell className="text-right">
<ConfirmButton
variant="ghost"
size="sm"
disabled={removing}
confirmLabel="Remove?"
Expand Down Expand Up @@ -288,7 +287,11 @@ export function ToolsPage({ tenantId }: { readonly tenantId: string | null }) {
serversQuery.data.some((server) => server.handle === entry.handle)
}
onAdd={() => {
addServer({ url: entry.url, name: entry.name, handle: entry.handle });
addServer({
url: entry.url,
name: entry.name,
handle: entry.handle,
});
}}
/>
))}
Expand Down
13 changes: 8 additions & 5 deletions apps/web/src/pages/workbench-schedules-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type CronSchedule,
} from "../routines-api";
import { tenantKeys } from "../query-client";
import { ConfirmButton } from "../components/confirm-button";

type FormState = {
readonly definitionName: string;
Expand Down Expand Up @@ -64,7 +65,9 @@ export function WorkbenchSchedulesPanel({
});

const invalidate = () =>
queryClient.invalidateQueries({ queryKey: tenantKeys.schedules(workbenchTenantId) });
queryClient.invalidateQueries({
queryKey: tenantKeys.schedules(workbenchTenantId),
});

const create = useMutation({
mutationFn: (input: FormState) =>
Expand Down Expand Up @@ -164,14 +167,14 @@ export function WorkbenchSchedulesPanel({
Schedule again
</Button>
)}
<Button
variant="ghost"
<ConfirmButton
size="sm"
onClick={() => remove.mutate(schedule.id)}
confirmLabel="Delete permanently"
onConfirm={() => remove.mutate(schedule.id)}
disabled={remove.isPending}
>
Delete
</Button>
</ConfirmButton>
</div>
</li>
))}
Expand Down
31 changes: 24 additions & 7 deletions apps/web/src/settings/credentials-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import {
Button,
ConfirmButton,
Dialog,
DialogBody,
DialogContent,
Expand Down Expand Up @@ -56,6 +55,7 @@ import {
type Provider,
} from "./credentials-api";
import { SETTINGS_STRINGS } from "./strings";
import { ConfirmButton } from "../components/confirm-button";

type CatalogProvider = typeof ModelProviderResponse.infer;
type CatalogOffering = typeof ModelOfferingResponse.infer;
Expand Down Expand Up @@ -125,7 +125,13 @@ export function CredentialsSection({ tenantId }: { readonly tenantId: string | n
listOwnOfferings(tenantId),
listOwnModels(tenantId),
]);
return { credentials, providers, catalogProviders, catalogOfferings, catalogModels };
return {
credentials,
providers,
catalogProviders,
catalogOfferings,
catalogModels,
};
},
enabled: tenantId !== null,
});
Expand All @@ -137,11 +143,15 @@ export function CredentialsSection({ tenantId }: { readonly tenantId: string | n

function reload() {
if (tenantId === null) return;
void queryClient.invalidateQueries({ queryKey: tenantKeys.credentials(tenantId) });
void queryClient.invalidateQueries({
queryKey: tenantKeys.credentials(tenantId),
});
// The catalog rows this dialog can now rewrite are read by the
// Inference settings section and by chat's model resolution — both
// key off the resolved catalog, so a credential edit must bust it too.
void queryClient.invalidateQueries({ queryKey: ["tenant", tenantId, "settings-models"] });
void queryClient.invalidateQueries({
queryKey: ["tenant", tenantId, "settings-models"],
});
}

const create = useMutation({
Expand All @@ -157,7 +167,12 @@ export function CredentialsSection({ tenantId }: { readonly tenantId: string | n
if (tenantId === null) throw new Error("no workbench selected");
const existing = providers.find((provider) => provider.name === name);
const provider = existing !== undefined ? existing : await createProvider(tenantId, name);
return createCredential(tenantId, { providerId: provider.id, name, type, secret });
return createCredential(tenantId, {
providerId: provider.id,
name,
type,
secret,
});
},
onSuccess: () => {
setCreateOpen(false);
Expand Down Expand Up @@ -279,7 +294,10 @@ export function CredentialsSection({ tenantId }: { readonly tenantId: string | n
const editingLinkage: EditCredentialLinkage | null =
editingProvider === null
? null
: { baseURL: editingProvider.baseURL, model: editingModel?.canonicalName ?? "" };
: {
baseURL: editingProvider.baseURL,
model: editingModel?.canonicalName ?? "",
};

if (tenantId === null) {
return (
Expand Down Expand Up @@ -396,7 +414,6 @@ function CredentialsTable({
{SETTINGS_STRINGS.credentialsEditAction}
</Button>
<ConfirmButton
variant="destructive"
size="sm"
confirmLabel={SETTINGS_STRINGS.credentialsDeleteConfirm}
onConfirm={() => onDelete(credential)}
Expand Down
7 changes: 4 additions & 3 deletions apps/web/src/settings/grants-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import {
Badge,
Button,
ConfirmButton,
Dialog,
DialogBody,
DialogContent,
Expand Down Expand Up @@ -51,6 +50,7 @@ import {
type Principal,
type Role,
} from "./tenancy-api";
import { ConfirmButton } from "../components/confirm-button";

const EFFECT_TONE: Record<GrantEffect, "success" | "danger" | "info"> = {
allow: "success",
Expand Down Expand Up @@ -110,7 +110,9 @@ export function GrantsSection({ tenantId }: { readonly tenantId: string | null }

function reload() {
if (tenantId === null) return;
void queryClient.invalidateQueries({ queryKey: tenantKeys.grants(tenantId) });
void queryClient.invalidateQueries({
queryKey: tenantKeys.grants(tenantId),
});
}

if (tenantId === null) {
Expand Down Expand Up @@ -312,7 +314,6 @@ export function GrantsTable({
<TableCell>{grant.expiresAt ?? SETTINGS_STRINGS.grantsNoExpiry}</TableCell>
<TableCell>
<ConfirmButton
variant="destructive"
size="sm"
confirmLabel={SETTINGS_STRINGS.grantsRevokeConfirm}
onConfirm={() => onRevoke(grant)}
Expand Down
17 changes: 12 additions & 5 deletions apps/web/src/settings/people-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import {
Badge,
Button,
ConfirmButton,
Dialog,
DialogBody,
DialogContent,
Expand Down Expand Up @@ -42,6 +41,7 @@ import {
type Principal,
type Role,
} from "./tenancy-api";
import { ConfirmButton } from "../components/confirm-button";

const STATUS_TONE: Record<Principal["status"], "success" | "info" | "neutral"> = {
active: "success",
Expand Down Expand Up @@ -86,7 +86,9 @@ export function PeopleSection({ tenantId }: { readonly tenantId: string | null }

function reload() {
if (tenantId === null) return;
void queryClient.invalidateQueries({ queryKey: tenantKeys.principals(tenantId) });
void queryClient.invalidateQueries({
queryKey: tenantKeys.principals(tenantId),
});
}

const inviteMutation = useMutation({
Expand All @@ -99,7 +101,10 @@ export function PeopleSection({ tenantId }: { readonly tenantId: string | null }
reload();
},
onError: (cause: unknown) => {
reportError(cause, { operation: "settings.people.invite", tenantId: tenantId ?? "none" });
reportError(cause, {
operation: "settings.people.invite",
tenantId: tenantId ?? "none",
});
},
});

Expand Down Expand Up @@ -204,7 +209,10 @@ export function PeopleSection({ tenantId }: { readonly tenantId: string | null }
submitting={inviteMutation.isPending}
error={inviteMutation.isError ? SETTINGS_STRINGS.peopleInviteError : null}
onInvite={(email, roleId) =>
inviteMutation.mutate({ email, ...(roleId !== undefined ? { roleId } : {}) })
inviteMutation.mutate({
email,
...(roleId !== undefined ? { roleId } : {}),
})
}
/>
</SettingsPanel>
Expand Down Expand Up @@ -416,7 +424,6 @@ export function PeopleTable({
</Button>
)}
<ConfirmButton
variant="destructive"
size="sm"
confirmLabel={SETTINGS_STRINGS.peopleRemoveConfirm}
onConfirm={() => onRemove(person)}
Expand Down
Loading
Loading