diff --git a/AGENTS.md b/AGENTS.md
index 5b34b807a..ce9df3784 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -5,6 +5,7 @@
- Before starting a feature, skim an existing page or form with similar behavior and mirror the conventions—this codebase is intentionally conventional. Look for similar pages in `app/pages` and forms in `app/forms` to use as templates.
- `@oxide/api` is at `app/api` and `@oxide/api-mocks` is at `mock-api/index.ts`.
- The language server often has out of date errors. tsgo is extremely fast, so confirm errors that come from the language server by running `npm run tsc`
+- Format with `npm run fmt` (oxfmt). Never use prettier directly—it is not the project formatter.
- Use Node.js 22+, then install deps and start the mock-backed dev server (skip if `npm run dev` is already running in another terminal):
```sh
diff --git a/app/forms/anti-affinity-group-create.tsx b/app/forms/anti-affinity-group-create.tsx
index dfaaaa037..38eb34a6f 100644
--- a/app/forms/anti-affinity-group-create.tsx
+++ b/app/forms/anti-affinity-group-create.tsx
@@ -79,7 +79,11 @@ export default function CreateAntiAffinityGroupForm() {
{ value: 'fail', label: 'Fail' },
]}
/>
-
+
)
}
diff --git a/app/forms/anti-affinity-group-edit.tsx b/app/forms/anti-affinity-group-edit.tsx
index d238827f8..1c1c43b25 100644
--- a/app/forms/anti-affinity-group-edit.tsx
+++ b/app/forms/anti-affinity-group-edit.tsx
@@ -83,7 +83,11 @@ export default function EditAntiAffintyGroupForm() {
>
-
+
)
}
diff --git a/app/forms/disk-create.tsx b/app/forms/disk-create.tsx
index 53d42fb8f..23d3835a2 100644
--- a/app/forms/disk-create.tsx
+++ b/app/forms/disk-create.tsx
@@ -230,7 +230,7 @@ export function CreateDiskSideModalForm({
images={images}
areImagesLoading={areImagesLoading}
/>
-
+
)
}
diff --git a/app/forms/external-subnet-create.tsx b/app/forms/external-subnet-create.tsx
index 790adf93f..9ffa57b75 100644
--- a/app/forms/external-subnet-create.tsx
+++ b/app/forms/external-subnet-create.tsx
@@ -153,7 +153,11 @@ export default function CreateExternalSubnetSideModalForm() {
description="The subnet to reserve, e.g., 10.128.1.0/24"
/>
)}
-
+
)
}
diff --git a/app/forms/external-subnet-edit.tsx b/app/forms/external-subnet-edit.tsx
index 0efa2622c..6b2c635fb 100644
--- a/app/forms/external-subnet-edit.tsx
+++ b/app/forms/external-subnet-edit.tsx
@@ -114,7 +114,11 @@ export default function EditExternalSubnetSideModalForm() {
-
+
)
}
diff --git a/app/forms/fleet-access.tsx b/app/forms/fleet-access.tsx
index 018097f55..4df4413ed 100644
--- a/app/forms/fleet-access.tsx
+++ b/app/forms/fleet-access.tsx
@@ -77,7 +77,11 @@ export function FleetAccessAddUserSideModal({
control={form.control}
/>
-
+
)
}
@@ -122,7 +126,11 @@ export function FleetAccessEditUserSideModal({
}}
>
-
+
)
}
diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx
index b36b218fe..18fb35d6d 100644
--- a/app/forms/floating-ip-create.tsx
+++ b/app/forms/floating-ip-create.tsx
@@ -108,7 +108,11 @@ export default function CreateFloatingIpSideModalForm() {
placeholder="Select a pool"
noItemsPlaceholder="No pools available"
/>
-
+
)
}
diff --git a/app/forms/floating-ip-edit.tsx b/app/forms/floating-ip-edit.tsx
index 960f2e71e..678f81975 100644
--- a/app/forms/floating-ip-edit.tsx
+++ b/app/forms/floating-ip-edit.tsx
@@ -115,7 +115,11 @@ export default function EditFloatingIpSideModalForm() {
-
+
)
}
diff --git a/app/forms/idp/edit.tsx b/app/forms/idp/edit.tsx
index 2f02e2723..65995ade6 100644
--- a/app/forms/idp/edit.tsx
+++ b/app/forms/idp/edit.tsx
@@ -117,7 +117,10 @@ export default function EditIdpSideModalForm() {
control={form.control}
disabled
/>
-
+
)
}
diff --git a/app/forms/image-edit.tsx b/app/forms/image-edit.tsx
index 2db4ca3f3..f69504cdb 100644
--- a/app/forms/image-edit.tsx
+++ b/app/forms/image-edit.tsx
@@ -60,7 +60,7 @@ export function EditImageSideModalForm({
-
+
)
}
diff --git a/app/forms/image-from-snapshot.tsx b/app/forms/image-from-snapshot.tsx
index 7c6f49acb..1be33e62c 100644
--- a/app/forms/image-from-snapshot.tsx
+++ b/app/forms/image-from-snapshot.tsx
@@ -102,7 +102,11 @@ export default function CreateImageFromSnapshotSideModalForm() {
-
+
)
}
diff --git a/app/forms/image-upload.tsx b/app/forms/image-upload.tsx
index facc6b28e..ffa2dff9f 100644
--- a/app/forms/image-upload.tsx
+++ b/app/forms/image-upload.tsx
@@ -681,7 +681,11 @@ export default function ImageCreate() {
/>
)}
-
+
)
}
diff --git a/app/forms/ip-pool-create.tsx b/app/forms/ip-pool-create.tsx
index 67bd8e9d3..500d852d9 100644
--- a/app/forms/ip-pool-create.tsx
+++ b/app/forms/ip-pool-create.tsx
@@ -89,7 +89,11 @@ export default function CreateIpPoolSideModalForm() {
]}
/>
*/}
-
+
)
}
diff --git a/app/forms/ip-pool-edit.tsx b/app/forms/ip-pool-edit.tsx
index c46c46521..35deeffd7 100644
--- a/app/forms/ip-pool-edit.tsx
+++ b/app/forms/ip-pool-edit.tsx
@@ -75,7 +75,11 @@ export default function EditIpPoolSideModalForm() {
-
+
)
}
diff --git a/app/forms/ip-pool-range-add.tsx b/app/forms/ip-pool-range-add.tsx
index aa6a75097..91f3faaaf 100644
--- a/app/forms/ip-pool-range-add.tsx
+++ b/app/forms/ip-pool-range-add.tsx
@@ -138,7 +138,11 @@ export default function IpPoolAddRange() {
control={form.control}
required
/>
-
+
)
}
diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx
index 0694e5f29..e8aa4703c 100644
--- a/app/forms/network-interface-create.tsx
+++ b/app/forms/network-interface-create.tsx
@@ -180,7 +180,11 @@ export function CreateNetworkInterfaceForm({
placeholder="Leave blank for auto-assignment"
/>
)}
-
+
)
}
diff --git a/app/forms/network-interface-edit.tsx b/app/forms/network-interface-edit.tsx
index 8bd206ccd..f64af5983 100644
--- a/app/forms/network-interface-edit.tsx
+++ b/app/forms/network-interface-edit.tsx
@@ -178,7 +178,11 @@ export function EditNetworkInterfaceForm({
variant="info"
content={`This network interface supports ${supportedVersions} transit IPs.`}
/>
-
+
)
}
diff --git a/app/forms/project-access.tsx b/app/forms/project-access.tsx
index 15566bc56..7e1ea5033 100644
--- a/app/forms/project-access.tsx
+++ b/app/forms/project-access.tsx
@@ -76,7 +76,11 @@ export function ProjectAccessAddUserSideModal({ onDismiss, policy }: AddRoleModa
control={form.control}
/>
-
+
)
}
@@ -123,7 +127,11 @@ export function ProjectAccessEditUserSideModal({
onDismiss={onDismiss}
>
-
+
)
}
diff --git a/app/forms/project-create.tsx b/app/forms/project-create.tsx
index 46c0fec48..7a33c4182 100644
--- a/app/forms/project-create.tsx
+++ b/app/forms/project-create.tsx
@@ -61,7 +61,11 @@ export default function ProjectCreateSideModalForm() {
>
-
+
)
}
diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx
index 43144736a..634834b83 100644
--- a/app/forms/project-edit.tsx
+++ b/app/forms/project-edit.tsx
@@ -70,7 +70,11 @@ export default function EditProjectSideModalForm() {
>
-
+
)
}
diff --git a/app/forms/silo-access.tsx b/app/forms/silo-access.tsx
index 6bc711230..c375f3f93 100644
--- a/app/forms/silo-access.tsx
+++ b/app/forms/silo-access.tsx
@@ -73,7 +73,11 @@ export function SiloAccessAddUserSideModal({ onDismiss, policy }: AddRoleModalPr
control={form.control}
/>
-
+
)
}
@@ -118,7 +122,11 @@ export function SiloAccessEditUserSideModal({
}}
>
-
+
)
}
diff --git a/app/forms/silo-create.tsx b/app/forms/silo-create.tsx
index de7e01223..05c694f9c 100644
--- a/app/forms/silo-create.tsx
+++ b/app/forms/silo-create.tsx
@@ -183,7 +183,11 @@ export default function CreateSiloSideModalForm() {
-
+
)
}
diff --git a/app/forms/snapshot-create.tsx b/app/forms/snapshot-create.tsx
index 8079c59f3..ef7634ff0 100644
--- a/app/forms/snapshot-create.tsx
+++ b/app/forms/snapshot-create.tsx
@@ -90,7 +90,11 @@ export default function SnapshotCreate() {
required
control={form.control}
/>
-
+
)
}
diff --git a/app/forms/ssh-key-create.tsx b/app/forms/ssh-key-create.tsx
index 2c3d27626..8437035a9 100644
--- a/app/forms/ssh-key-create.tsx
+++ b/app/forms/ssh-key-create.tsx
@@ -68,7 +68,11 @@ export function SSHKeyCreate({ onDismiss, message }: Props) {
control={form.control}
/>
{message}
-
+
)
}
diff --git a/app/forms/ssh-key-edit.tsx b/app/forms/ssh-key-edit.tsx
index 77408c1ee..c9d280433 100644
--- a/app/forms/ssh-key-edit.tsx
+++ b/app/forms/ssh-key-edit.tsx
@@ -74,7 +74,11 @@ export default function EditSSHKeySideModalForm() {
disabled
/>
-
+
)
}
diff --git a/app/forms/subnet-create.tsx b/app/forms/subnet-create.tsx
index ddfe14501..de55d8b68 100644
--- a/app/forms/subnet-create.tsx
+++ b/app/forms/subnet-create.tsx
@@ -96,7 +96,11 @@ export default function CreateSubnetForm() {
control={form.control}
required
/>
-
+
)
}
diff --git a/app/forms/subnet-edit.tsx b/app/forms/subnet-edit.tsx
index 8b5bcb61d..2f5035f5c 100644
--- a/app/forms/subnet-edit.tsx
+++ b/app/forms/subnet-edit.tsx
@@ -107,7 +107,11 @@ export default function EditSubnetForm() {
control={form.control}
required
/>
-
+
)
}
diff --git a/app/forms/vpc-create.tsx b/app/forms/vpc-create.tsx
index 8a22867ba..1692a2e22 100644
--- a/app/forms/vpc-create.tsx
+++ b/app/forms/vpc-create.tsx
@@ -65,7 +65,7 @@ export default function CreateVpcSideModalForm() {
-
+
)
}
diff --git a/app/forms/vpc-edit.tsx b/app/forms/vpc-edit.tsx
index a2e5c60e1..991539937 100644
--- a/app/forms/vpc-edit.tsx
+++ b/app/forms/vpc-edit.tsx
@@ -78,7 +78,7 @@ export default function EditVpcSideModalForm() {
-
+
)
}
diff --git a/app/forms/vpc-router-create.tsx b/app/forms/vpc-router-create.tsx
index 2578df6b0..cbcdfb6b9 100644
--- a/app/forms/vpc-router-create.tsx
+++ b/app/forms/vpc-router-create.tsx
@@ -57,7 +57,11 @@ export default function RouterCreate() {
>
-
+
)
}
diff --git a/app/forms/vpc-router-edit.tsx b/app/forms/vpc-router-edit.tsx
index 4feccb194..a8ac48eb3 100644
--- a/app/forms/vpc-router-edit.tsx
+++ b/app/forms/vpc-router-edit.tsx
@@ -80,7 +80,11 @@ export default function EditRouterSideModalForm() {
>
-
+
)
}
diff --git a/app/forms/vpc-router-route-common.tsx b/app/forms/vpc-router-route-common.tsx
index 4043f5f49..85375c2fa 100644
--- a/app/forms/vpc-router-route-common.tsx
+++ b/app/forms/vpc-router-route-common.tsx
@@ -222,6 +222,10 @@ export const RouteFormFields = ({ form, disabled }: RouteFormFieldsProps) => {
)
}
-export const RouteFormDocs = () => (
-
+export const RouteFormDocs = ({ apiOp, cliCmd }: { apiOp: string; cliCmd: string }) => (
+
)
diff --git a/app/forms/vpc-router-route-create.tsx b/app/forms/vpc-router-route-create.tsx
index 764497144..a78414f51 100644
--- a/app/forms/vpc-router-route-create.tsx
+++ b/app/forms/vpc-router-route-create.tsx
@@ -83,7 +83,7 @@ export default function CreateRouterRouteSideModalForm() {
submitError={createRouterRoute.error}
>
-
+
)
}
diff --git a/app/forms/vpc-router-route-edit.tsx b/app/forms/vpc-router-route-edit.tsx
index f0d2dbd24..6fcd1ca2e 100644
--- a/app/forms/vpc-router-route-edit.tsx
+++ b/app/forms/vpc-router-route-edit.tsx
@@ -100,7 +100,7 @@ export default function EditRouterRouteSideModalForm() {
submitDisabled={disabled ? routeFormMessage.vpcSubnetNotModifiable : undefined}
>
-
+
)
}
diff --git a/app/pages/project/disks/DiskDetailSideModal.tsx b/app/pages/project/disks/DiskDetailSideModal.tsx
index 990695f98..587b3248d 100644
--- a/app/pages/project/disks/DiskDetailSideModal.tsx
+++ b/app/pages/project/disks/DiskDetailSideModal.tsx
@@ -94,7 +94,7 @@ export function DiskDetailSideModal({
-
+
)
}
diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx
index 5d5ac415d..c35baadeb 100644
--- a/app/pages/project/vpcs/internet-gateway-edit.tsx
+++ b/app/pages/project/vpcs/internet-gateway-edit.tsx
@@ -201,7 +201,11 @@ export default function EditInternetGatewayForm() {
-
+
)
}
diff --git a/app/ui/lib/ModalLinks.tsx b/app/ui/lib/ModalLinks.tsx
index d75794347..44e9e5867 100644
--- a/app/ui/lib/ModalLinks.tsx
+++ b/app/ui/lib/ModalLinks.tsx
@@ -11,6 +11,8 @@ import { OpenLink12Icon } from '@oxide/design-system/icons/react'
import { FormDivider } from '~/ui/lib/Divider'
+const DOC_BASE = 'https://docs.oxide.computer'
+
export const ModalLinks = ({
heading,
children,
@@ -41,13 +43,59 @@ export const ModalLink = ({ to, label }: { to: string; label: string }) => (
type DocLink = { href: string; linkText: string }
-export const SideModalFormDocs = ({ docs }: { docs: DocLink[] }) => (
+/**
+ * `apiOp` is a snake_case operation ID, e.g., "project_create".
+ * `cliCmd` is a slash-delimited CLI path, e.g., "project/create".
+ */
+export const SideModalFormDocs = ({
+ docs,
+ apiOp,
+ cliCmd,
+}: {
+ docs: DocLink[]
+ apiOp?: string
+ cliCmd?: string
+}) => (
<>
{docs.map(({ href, linkText }) => (
))}
+ {apiOp && (
+
+
+
+
+ API:{' '}
+ {apiOp}
+
+
+
+ )}
+ {cliCmd && (
+
+
+
+
+ CLI:{' '}
+
+ oxide {cliCmd.replaceAll('/', ' ')}
+
+
+
+
+ )}
>
)