diff --git a/apps/web/src/components/layout/app/shell/app-shell.tsx b/apps/web/src/components/layout/app/shell/app-shell.tsx
index 26c05cd..a4d97fd 100644
--- a/apps/web/src/components/layout/app/shell/app-shell.tsx
+++ b/apps/web/src/components/layout/app/shell/app-shell.tsx
@@ -1,6 +1,7 @@
import { Outlet, useRouterState } from "@tanstack/react-router"
import { AuthProvider } from "@tripwire/auth/components"
import { TopNav } from "#/components/layout/app/shell/top-nav"
+import { BetaBanner } from "#/components/layout/app/shell/beta-banner"
import { WorkspaceRedirect } from "#/components/layout/app/shell/workspace-redirect"
import { InstallGitHubPrompt } from "#/components/layout/app/shell/install-github-prompt"
import { AskSidePanel } from "#/components/layout/app/chat/ask-side-panel"
@@ -55,8 +56,11 @@ function AppShellInner() {
isChatRoute ? undefined : { boxShadow: "#00000008 0px 1px 4px" }
}
>
-
- {needsInstall ?
:
}
+
+
+
+ {needsInstall ? : }
+
diff --git a/apps/web/src/components/layout/app/shell/beta-banner.tsx b/apps/web/src/components/layout/app/shell/beta-banner.tsx
new file mode 100644
index 0000000..8fc9f7a
--- /dev/null
+++ b/apps/web/src/components/layout/app/shell/beta-banner.tsx
@@ -0,0 +1,78 @@
+import { useState, useSyncExternalStore } from "react"
+import { Button } from "@tripwire/ui/button"
+import { CloseIcon } from "@tripwire/ui/icons/close-icon"
+import { cn } from "@tripwire/ui/utils"
+
+const STORAGE_KEY = "tripwire:beta-banner:dismissed"
+const EVENT_NAME = "tripwire:beta-banner-dismissed"
+
+function subscribe(callback: () => void): () => void {
+ window.addEventListener(EVENT_NAME, callback)
+ return () => window.removeEventListener(EVENT_NAME, callback)
+}
+
+function getSnapshot(): boolean {
+ return window.localStorage.getItem(STORAGE_KEY) === "true"
+}
+
+function getServerSnapshot(): boolean {
+ return true
+}
+
+function dismiss(): void {
+ window.localStorage.setItem(STORAGE_KEY, "true")
+ window.dispatchEvent(new Event(EVENT_NAME))
+}
+
+export function BetaBanner() {
+ const dismissed = useSyncExternalStore(
+ subscribe,
+ getSnapshot,
+ getServerSnapshot
+ )
+ const [closing, setClosing] = useState(false)
+
+ if (dismissed) return null
+
+ return (
+ {
+ if (e.target === e.currentTarget) dismiss()
+ }}
+ >
+
+
+
+ Tripwire is in closed beta. Please raise any issues via DM on X{" "}
+
+ @tripwiredotsh
+
+
+
+
+
+
+ )
+}