From f3981dbfefc09e6a888ada489badb7dafdf122cf Mon Sep 17 00:00:00 2001
From: Will Pfleger >([]);
@@ -80,11 +90,41 @@ class ComposeBar extends HookConsumerWidget {
final hasAttachments = attachments.value.isNotEmpty;
final hasPendingUploads = uploadingCount.value > 0;
final customEmoji = ref.watch(customEmojiListProvider);
+ final reducedMotion = MediaQuery.disableAnimationsOf(context);
+ final composerExpansionController = useAnimationController(
+ initialValue: 0,
+ upperBound: 1.05,
+ );
+ final composerExpansionValue = useAnimation(composerExpansionController);
+ final composerExpansionProgress = composerExpansionValue
+ .clamp(0.0, 1.0)
+ .toDouble();
final resolvedHint =
hintText ??
(channelName.isNotEmpty ? 'Message #$channelName' : 'Message\u2026');
+ useEffect(() {
+ final target = isComposerExpanded.value ? 1.0 : 0.0;
+ if (reducedMotion) {
+ composerExpansionController.value = target;
+ } else if ((composerExpansionController.value - target).abs() > 0.001) {
+ composerExpansionController.animateWith(
+ SpringSimulation(
+ SpringDescription.withDurationAndBounce(
+ duration: const Duration(milliseconds: 280),
+ bounce: 0.16,
+ ),
+ composerExpansionController.value,
+ target,
+ 0,
+ snapToEnd: true,
+ ),
+ );
+ }
+ return null;
+ }, [isComposerExpanded.value, reducedMotion]);
+
useEffect(() {
if (defaultTargetPlatform != TargetPlatform.iOS) return null;
@@ -288,12 +328,28 @@ class ComposeBar extends HookConsumerWidget {
// Insert `#` at the cursor to manually trigger channel mode.
void triggerChannel() => _insertTriggerAtCursor(controller, focusNode, '#');
+ // Insert a selected emoji at the cursor without replacing the draft.
+ void insertEmoji(String emoji) {
+ final text = controller.text;
+ final selection = controller.selection;
+ final cursor = selection.isValid
+ ? selection.baseOffset.clamp(0, text.length)
+ : text.length;
+ controller.value = TextEditingValue(
+ text: text.replaceRange(cursor, cursor, emoji),
+ selection: TextSelection.collapsed(offset: cursor + emoji.length),
+ );
+ focusNode.requestFocus();
+ }
+
void clearComposer() {
controller.clear();
attachments.value = [];
mentionMap.value.clear();
mentionQuery.value = null;
channelQuery.value = null;
+ showAttachments.value = false;
+ showCamera.value = false;
showFormatting.value = false;
uploadError.value = null;
focusNode.requestFocus();
@@ -490,21 +546,6 @@ class ComposeBar extends HookConsumerWidget {
);
}
- // Insert an emoji at the cursor.
- void insertEmoji(String emoji) {
- final text = controller.text;
- final cursor = controller.selection.isValid
- ? controller.selection.baseOffset
- : text.length;
- final before = text.substring(0, cursor);
- final after = text.substring(cursor);
- controller.text = '$before$emoji$after';
- controller.selection = TextSelection.collapsed(
- offset: cursor + emoji.length,
- );
- focusNode.requestFocus();
- }
-
// Wrap (or insert) markdown formatting around the current selection.
void applyFormat(String prefix, [String? suffix]) {
suffix ??= prefix;
@@ -539,61 +580,150 @@ class ComposeBar extends HookConsumerWidget {
// ----- Widget tree ----------------------------------------------------
- final hasSuggestions =
- suggestions.isNotEmpty || channelSuggestions.isNotEmpty;
+ void chooseAttachment(Future
>[],
+ }) async {},
+ ),
+ );
+
+ await _expandComposer(tester);
+ await tester.enterText(find.byType(TextField), 'hello world');
+ final textField = tester.widget
>[],
+ }) async {},
+ ),
+ );
+
+ await _openAttachmentMenu(tester);
+ await tester.tap(find.text('Photos'));
+ await tester.pump();
+
+ expect(
+ find.byKey(const ValueKey('compose-upload-progress')),
+ findsOneWidget,
+ );
+ expect(find.text('Uploading attachment…'), findsOneWidget);
+
+ pickedImage.complete(null);
+ await tester.pumpAndSettle();
+
+ expect(
+ find.byKey(const ValueKey('compose-upload-progress')),
+ findsNothing,
+ );
+ });
+
+ testWidgets('renders markdown formatting without visible delimiters', (
+ tester,
+ ) async {
+ await tester.pumpWidget(
+ _buildComposeBar(
+ uploadService: _testUploadService(nostr.Keys.generate().nsec),
+ onSend:
+ (
+ content,
+ mentionPubkeys, {
+ mediaTags = const
>[],
+ }) async {},
+ ),
+ );
+
+ await _expandComposer(tester);
+ await tester.enterText(
+ find.byType(TextField),
+ '**bold** _italic_ ~~strike~~ `code`',
+ );
+
+ final textField = tester.widget
>[],
+ }) async {},
+ ),
+ );
+
+ await _expandComposer(tester);
+ await tester.tap(find.byIcon(LucideIcons.aLargeSmall));
+ await tester.pumpAndSettle();
+
+ final boldFinder = find.byIcon(LucideIcons.bold);
+ final boldIcon = tester.widget
- Buzz detected the harnesses available on this machine. Install or sign - in to at least one to continue. + Buzz checks for command-line harnesses on this machine. Install the + CLI or sign in to at least one to continue.
@@ -629,8 +632,8 @@ function RuntimeProvidersSection({ className="max-w-[560px] rounded-2xl bg-white/70 px-6 py-6 text-sm text-muted-foreground" data-testid="onboarding-acp-empty" > - No supported agent harnesses were detected yet. Install Claude Code - or Codex, then check again. + No supported command-line harnesses were detected yet. Install a + supported CLI, then check again. )} diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx index 7a0186a7ba..894d88bd22 100644 --- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx +++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx @@ -53,6 +53,13 @@ const RUNTIME_SORT_PRIORITY: Record{runtime.installHint}
+ {runtime.installInstructionsUrl.trim().length > 0 ? ( + + ) : null} +) : null} - {installSuccess && runtime.availability !== "available" ? ( -
- {runtime.label} installed. Checking for sign-in options... -
- ) : null} {installError ? ( {
),
),
);
- expect(new Set(rowHeights).size).toBe(1);
+ expect(rowHeights[2]).toBeGreaterThan(rowHeights[0]);
const [gooseColors, codexColors] = await Promise.all(
["goose", "codex"].map((runtimeId) =>
page.getByTestId(`doctor-runtime-${runtimeId}`).evaluate((element) => {
@@ -194,11 +196,11 @@ test.describe("Doctor panel state screenshots", () => {
await expect(toggle.locator("span")).toHaveClass(/shadow-none/);
}
await expect(
- page.getByRole("menuitem", { name: "Instructions" }),
+ page.getByRole("menuitem", { name: "CLI setup guide" }),
).toHaveCount(0);
await page.getByTestId("doctor-runtime-menu-codex").click();
await expect(
- page.getByRole("menuitem", { name: "Instructions" }),
+ page.getByRole("menuitem", { name: "CLI setup guide" }),
).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
@@ -212,6 +214,17 @@ test.describe("Doctor panel state screenshots", () => {
await expect(page.getByTestId("doctor-runtime-codex")).not.toContainText(
"Not installed",
);
+ await expect(page.getByTestId("doctor-runtime-status-codex")).toHaveText(
+ "CLI needed",
+ );
+ await expect(
+ page.getByTestId("doctor-runtime-guidance-codex"),
+ ).toContainText("desktop app alone is not enough");
+ await expect(
+ page
+ .getByTestId("doctor-runtime-guidance-codex")
+ .getByRole("button", { name: "CLI setup guide" }),
+ ).toBeVisible();
await runtimeList.scrollIntoViewIfNeeded();
await waitForAnimations(page);
@@ -287,7 +300,7 @@ test.describe("Doctor panel state screenshots", () => {
);
await page.getByTestId("doctor-runtime-menu-codex").click();
await expect(
- page.getByRole("menuitem", { name: "Instructions" }),
+ page.getByRole("menuitem", { name: "CLI setup guide" }),
).toBeVisible();
await page.keyboard.press("Escape");
@@ -331,7 +344,7 @@ test.describe("Doctor panel state screenshots", () => {
);
await page.getByTestId("doctor-runtime-menu-claude").click();
await expect(
- page.getByRole("menuitem", { name: "Instructions" }),
+ page.getByRole("menuitem", { name: "CLI setup guide" }),
).toBeVisible();
await page.keyboard.press("Escape");
@@ -354,8 +367,9 @@ test.describe("Doctor panel state screenshots", () => {
availability: "adapter_missing",
underlying_cli_path: "/usr/local/bin/codex",
node_required: true,
- install_hint:
- "Install the Codex ACP adapter: npm install -g @zed-industries/codex-acp",
+ install_hint: "Install the Codex ACP adapter via npm.",
+ install_instructions_url:
+ "https://github.com/agentclientprotocol/codex-acp",
},
BUZZ_AGENT_AVAILABLE,
],
@@ -373,16 +387,20 @@ test.describe("Doctor panel state screenshots", () => {
"Adapter needed",
);
await expect(row).not.toContainText("Node.js is required");
- await expect(row).toHaveCSS(
- "height",
+ expect(
+ await row.evaluate((element) => element.getBoundingClientRect().height),
+ ).toBeGreaterThan(
await page
.getByTestId("doctor-runtime-goose")
- .evaluate((element) => getComputedStyle(element).height),
+ .evaluate((element) => element.getBoundingClientRect().height),
);
await page.getByTestId("doctor-runtime-menu-codex").click();
await expect(
page.getByRole("menuitem", { name: "Install Node.js" }),
).toBeVisible();
+ await expect(
+ page.getByRole("menuitem", { name: "Adapter install guide" }),
+ ).toBeVisible();
await page.keyboard.press("Escape");
await row.scrollIntoViewIfNeeded();
@@ -395,9 +413,10 @@ test.describe("Doctor panel state screenshots", () => {
*
* The mock is configured with a two-call sequence:
* call 1 → failure (E404)
- * call 2 → success
+ * call 2 → installer exit 0, but post-install discovery still cannot find
+ * the runtime
* This exercises the full retry path: fail state → toggle on again →
- * success banner.
+ * verified failure without a false installed state.
*/
test("05-retry-after-failure", async ({ page }) => {
await installMockBridge(page, {
@@ -427,16 +446,26 @@ test.describe("Doctor panel state screenshots", () => {
],
},
{
- success: true,
+ success: false,
steps: [
{
- step: "adapter",
- command: "npm install -g @zed-industries/codex-acp",
+ step: "cli",
+ command: "powershell.exe install codex",
success: true,
- stdout: "added 1 package",
+ stdout: "installed",
stderr: "",
exit_code: 0,
},
+ {
+ step: "verify",
+ command: "discover codex",
+ success: false,
+ stdout: "",
+ stderr:
+ "The installer finished, but Buzz still could not use codex (observed: NotInstalled).",
+ exit_code: null,
+ hint: "Buzz requires the vendor CLI executable, not only its desktop app. If the CLI was installed while Buzz was open, restart Buzz and check again.",
+ },
],
},
],
@@ -470,25 +499,71 @@ test.describe("Doctor panel state screenshots", () => {
await waitForAnimations(page);
await row.screenshot({ path: `${SHOTS}/05-retry-after-failure.png` });
- // Toggle on again — the mock returns success on the second call.
+ // Toggle on again — the install command exits 0, but verification fails.
await toggle.click();
await expect(loading).toBeVisible();
await expect(toggle).toHaveCount(0);
- // The error disappears, then the success banner and on state render.
+ // The runtime remains retryable and never renders a false success state.
await expect(loading).toHaveCount(0, { timeout: 5_000 });
- await expect(row).not.toContainText("failed", { timeout: 5_000 });
- await expect(
- row.getByText("Codex installed. Checking for sign-in options..."),
- ).toBeVisible({
- timeout: 10_000,
- });
- await expect(toggle).toBeChecked();
- await expect(toggle).toBeDisabled();
+ await expect(row).toContainText("desktop app", { timeout: 5_000 });
+ await expect(row).toContainText('Step "verify" failed');
+ await expect(row.getByText(/installed\. Checking/)).toHaveCount(0);
+ await expect(toggle).not.toBeChecked();
+ await expect(toggle).toBeEnabled();
await row.scrollIntoViewIfNeeded();
await waitForAnimations(page);
- await row.screenshot({ path: `${SHOTS}/05-retry-success.png` });
+ await row.screenshot({ path: `${SHOTS}/05-verification-failure.png` });
+ });
+
+ test("05b-verified-install-enables-runtime", async ({ page }) => {
+ await installMockBridge(page, {
+ acpRuntimesCatalog: [
+ GOOSE_AVAILABLE,
+ CLAUDE_AVAILABLE_LOGGED_IN,
+ CODEX_NOT_INSTALLED,
+ BUZZ_AGENT_AVAILABLE,
+ ],
+ acpRuntimesCatalogAfterInstall: [
+ GOOSE_AVAILABLE,
+ CLAUDE_AVAILABLE_LOGGED_IN,
+ {
+ ...CODEX_NOT_INSTALLED,
+ availability: "available",
+ command: "codex-acp",
+ binary_path: "/usr/local/bin/codex-acp",
+ underlying_cli_path: "/usr/local/bin/codex",
+ auth_status: { status: "logged_in" },
+ },
+ BUZZ_AGENT_AVAILABLE,
+ ],
+ installAcpRuntimeResult: {
+ success: true,
+ steps: [
+ {
+ step: "adapter",
+ command: "npm install -g @agentclientprotocol/codex-acp",
+ success: true,
+ stdout: "added 1 package",
+ stderr: "",
+ exit_code: 0,
+ },
+ ],
+ },
+ });
+
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ await openSettings(page, "agents");
+
+ const toggle = page.getByTestId("doctor-runtime-toggle-codex");
+ await expect(toggle).toBeEnabled();
+ await toggle.click();
+ await expect(toggle).toBeChecked({ timeout: 5_000 });
+ await expect(toggle).toBeDisabled();
+ await expect(page.getByTestId("doctor-runtime-guidance-codex")).toHaveCount(
+ 0,
+ );
});
/**
@@ -590,7 +665,7 @@ test.describe("Doctor panel state screenshots", () => {
);
await page.getByTestId("doctor-runtime-menu-claude").click();
await expect(
- page.getByRole("menuitem", { name: "Instructions" }),
+ page.getByRole("menuitem", { name: "CLI setup guide" }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: "Sign in with ChatGPT" }),
diff --git a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
index e1d8b11b6f..d007bd6118 100644
--- a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
+++ b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
@@ -84,6 +84,40 @@ test("setup shows only Claude Code and Codex as detected harnesses", async ({
await expect(page.getByRole("checkbox")).toHaveCount(0);
});
+test("setup distinguishes a missing CLI from an installed desktop app", async ({
+ page,
+}) => {
+ await installMockBridge(
+ page,
+ {
+ acpRuntimesCatalog: [
+ runtime(
+ "codex",
+ "not_installed",
+ { status: "unknown" },
+ {
+ install_hint:
+ "Buzz requires the Codex CLI; the desktop app alone is not enough.",
+ install_instructions_url:
+ "https://developers.openai.com/codex/cli/",
+ },
+ ),
+ ],
+ },
+ { skipCommunitySeed: true, skipOnboardingSeed: true },
+ );
+ await page.goto("/");
+ await navigateToSetupPage(page);
+
+ const card = page.getByTestId("onboarding-runtime-codex");
+ await expect(card).toContainText(
+ "CLI not detected; the desktop app alone isn’t enough.",
+ );
+ await expect(card.getByTestId("onboarding-runtime-install-codex")).toHaveText(
+ "INSTALL",
+ );
+});
+
test("ready state is detected and enables Next without persisting a default", async ({
page,
}) => {
From e527d74f069de5d706714d63d99a494389e824af Mon Sep 17 00:00:00 2001
From: joel ) — WKWebView bypasses WARP, so a
+ * proxy (like every other relay-hosted
) — WKWebView bypasses the VPN tunnel, so a
* direct relay URL gets a Cloudflare Access 403 and renders as a broken image.
*/
function EmojiGlyph({
diff --git a/desktop/src/features/profile/lib/selfProfileStorage.ts b/desktop/src/features/profile/lib/selfProfileStorage.ts
index d0a5a4e383..dbc4f88760 100644
--- a/desktop/src/features/profile/lib/selfProfileStorage.ts
+++ b/desktop/src/features/profile/lib/selfProfileStorage.ts
@@ -1,7 +1,7 @@
/**
* Cached self-profile store for relay-unreachable scenarios.
*
- * When the relay is unreachable (e.g. WARP VPN needs reauth), the app cannot
+ * When the relay is unreachable (e.g. corporate VPN needs reauth), the app cannot
* fetch the user's kind-0 profile. This module persists the last successfully
* fetched profile — including a base64 avatar snapshot — so the UI can render
* the user's identity even when offline.
diff --git a/desktop/src/features/user-status/ui/StatusEmoji.tsx b/desktop/src/features/user-status/ui/StatusEmoji.tsx
index 7632cfb267..950143f2b2 100644
--- a/desktop/src/features/user-status/ui/StatusEmoji.tsx
+++ b/desktop/src/features/user-status/ui/StatusEmoji.tsx
@@ -13,7 +13,7 @@ import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
* Every place that shows a status emoji renders this, so the shortcode→image
* resolution can't drift across the (five) display sites — the same reason the
* picker is unified. The relay URL is rewritten through the localhost media
- * proxy, matching reactions' `EmojiGlyph` (WKWebView bypasses WARP, so a direct
+ * proxy, matching reactions' `EmojiGlyph` (WKWebView bypasses the VPN tunnel, so a direct
* relay URL 403s and renders broken).
*/
type StatusEmojiProps = {
diff --git a/desktop/src/shared/api/relayClientShared.ts b/desktop/src/shared/api/relayClientShared.ts
index 76b7448c06..596bdde34e 100644
--- a/desktop/src/shared/api/relayClientShared.ts
+++ b/desktop/src/shared/api/relayClientShared.ts
@@ -8,7 +8,7 @@ import type { RelayEvent } from "@/shared/api/types";
* - `connected` — socket open and AUTH'd.
* - `reconnecting` — socket dropped, waiting for the backoff timer.
* - `stalled` — socket is *open* per the WS layer but no inbound frames
- * for a long time (half-open / Warp split-brain). We
+ * for a long time (half-open socket / VPN split-brain). We
* surface this so the UI can warn even though tungstenite
* hasn't reported anything wrong yet.
* - `disconnected` — final/terminal disconnect (auth rejected, community
diff --git a/desktop/src/shared/api/relayStallWatchdog.test.mjs b/desktop/src/shared/api/relayStallWatchdog.test.mjs
index b160a9435c..94097ca26d 100644
--- a/desktop/src/shared/api/relayStallWatchdog.test.mjs
+++ b/desktop/src/shared/api/relayStallWatchdog.test.mjs
@@ -44,7 +44,7 @@ test("does not send probes while watching for stalls", async () => {
await sleep(45);
wd.stop();
// The passive watchdog has no send callback by construction. This test is a
- // regression guard for the WARP bug: liveness checks must not write to a
+ // regression guard for the half-open-socket bug: liveness checks must not write to a
// socket already suspected of being half-open.
assert.equal(typeof wd.recordInbound, "function");
});
diff --git a/desktop/src/shared/api/tauriMedia.ts b/desktop/src/shared/api/tauriMedia.ts
index e56c611b6c..1dab9b7728 100644
--- a/desktop/src/shared/api/tauriMedia.ts
+++ b/desktop/src/shared/api/tauriMedia.ts
@@ -11,7 +11,7 @@ export async function pickAndUploadImage(): Promise
requests
+ * WKWebView's networking stack bypasses the VPN tunnel, so direct
requests
* to the relay get 403'd by Cloudflare Access. The localhost proxy routes
- * fetches through the Rust backend (via reqwest), which goes through WARP.
+ * fetches through the Rust backend (via reqwest), which goes through the VPN.
*
* For video, the proxy streams via axum — no buffering the entire file.
* Images and other media also benefit from this path.
diff --git a/desktop/src/shared/lib/relayError.ts b/desktop/src/shared/lib/relayError.ts
index c13b42c6b2..ca16aa41e2 100644
--- a/desktop/src/shared/lib/relayError.ts
+++ b/desktop/src/shared/lib/relayError.ts
@@ -3,7 +3,7 @@
*
* The Rust backend (`desktop/src-tauri/src/relay.rs`) prefixes every
* "relay unreachable" error message with this literal string so that the
- * frontend can distinguish a transient connectivity failure (e.g. WARP VPN
+ * frontend can distinguish a transient connectivity failure (e.g. corporate VPN
* needs reauth, Cloudflare Access 403) from an application-level error.
*
* Contract: the Rust layer MUST emit errors starting with exactly this prefix
@@ -18,7 +18,7 @@ export const RELAY_UNREACHABLE_MESSAGE =
/**
* Returns true when `error` carries the stable Rust-layer prefix indicating
- * the relay is unreachable (network failure, WARP VPN reauth needed, etc.).
+ * the relay is unreachable (network failure, VPN reauth needed, etc.).
*
* Accepts both `Error` instances and raw strings so callers can pass whatever
* the Tauri IPC or WebSocket layer hands them without pre-normalizing.
diff --git a/desktop/tests/e2e/custom-emoji.spec.ts b/desktop/tests/e2e/custom-emoji.spec.ts
index e78ac5737a..aa345570f9 100644
--- a/desktop/tests/e2e/custom-emoji.spec.ts
+++ b/desktop/tests/e2e/custom-emoji.spec.ts
@@ -167,7 +167,7 @@ test("native emoji-only messages leave space below the author metadata", async (
//
// The bug (shipped in the custom-emoji launch, PR #816): the reaction renderer
// put the relay emoji URL straight into
without going through
-// rewriteRelayUrl(). WKWebView bypasses WARP, so the direct relay URL gets a
+// rewriteRelayUrl(). WKWebView bypasses the VPN tunnel, so the direct relay URL gets a
// Cloudflare Access 403 and shows a broken image — even though the same emoji
// rendered fine inline in chat (that path rewrites). The chat path was covered
// by the tests above; the reaction path was not, which is why it slipped.
diff --git a/docs/admin/README.md b/docs/admin/README.md
index a66a53fc9e..e51566fb29 100644
--- a/docs/admin/README.md
+++ b/docs/admin/README.md
@@ -62,7 +62,7 @@ content retains attachment disposition. Successful reads produce a structured
trace containing feedback ID, community ID, and attachment hash, but no feedback
body or attachment URL.
-The human trust boundary remains the private admin ingress. WARP/source-IP
+The human trust boundary remains the private admin ingress. VPN/source-IP
admission is not per-operator identity. Anyone admitted to the dashboard can
read attachments for feedback records they can access. Per-person attribution
or revocation requires authenticated operator identity at ingress/application
From 60a171b19efd515d9213b535d52a2bcbec3ff2fe Mon Sep 17 00:00:00 2001
From: Alex Rosenzweig <64241648+shellz-n-stuff@users.noreply.github.com>
Date: Fri, 24 Jul 2026 19:24:47 -0700
Subject: [PATCH 011/393] fix(workflow): bypass system proxies for webhooks
(#2800)
## Summary
Disable automatic system-proxy discovery for workflow webhook requests.
## Why
Webhook destinations are resolved, validated, and pinned before the
request to prevent DNS-rebinding SSRF. If reqwest uses a system proxy,
the proxy can resolve the original hostname itself instead of connecting
to the validated address, bypassing that pinning guarantee.
Calling `no_proxy()` keeps these security-sensitive requests on the
directly validated connection path. Redirects remain disabled.
## Test plan
- `cargo fmt --all -- --check`
- `cargo test -p buzz-workflow --features reqwest` (149 passed)
- `cargo clippy -p buzz-workflow --all-targets --features reqwest -- -D
warnings`
- `git diff --check`
Co-authored-by: Amp