diff --git a/.github/workflows/android-apk.yml b/.github/workflows/android-apk.yml index db654f2..0f12fd4 100644 --- a/.github/workflows/android-apk.yml +++ b/.github/workflows/android-apk.yml @@ -3,6 +3,8 @@ name: Android APK on: push: branches: [main] + pull_request: + branches: [main] workflow_dispatch: permissions: @@ -33,13 +35,15 @@ jobs: run: npm run android:sync env: NODE_OPTIONS: "" + - name: Restore stable beta signing key + run: base64 -d android/ci/claw-debug.keystore.b64 > android/ci/claw-debug.keystore - name: Build debug APK working-directory: android run: ./gradlew assembleDebug --no-daemon - name: Upload CLAW Bridge APK uses: actions/upload-artifact@v4 with: - name: claw-bridge-v0.3-android-beta + name: claw-bridge-v0.4.8-stable-signed path: android/app/build/outputs/apk/debug/app-debug.apk if-no-files-found: error - retention-days: 30 + retention-days: 30 \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 643577d..0b23523 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,3 +7,13 @@ This version has breaking changes — APIs, conventions, and file structure may This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + +# CLAW Bridge runtime identity + +You are working inside CLAW Bridge, an Android-first AI control plane. The Android phone is the control plane; the paired gateway normally runs in Termux and the writable local runtime may be Termux or Ubuntu/PRoot. Codex, Claude, Termux and SSH are executors, not separate projects. + +Shared working context belongs to CLAW Bridge, not to any model provider. When CLAW injects a `[CLAW SHARED CONTEXT]` block, treat it as continuity from the user's prior planning. Do not ask the user to repeat decisions, constraints or the current goal that are already present there. Verify dynamic facts such as branch, files and test state before changing code. + +When handing work to another executor, preserve the goal, decisions, constraints, relevant files, acceptance criteria and latest result. After execution, report changed files, tests/build status, blockers and commit/branch information so CLAW can write the result back into shared context. + +Protected external actions remain approval-gated. Never store secrets, API keys, pairing tokens, passwords or private keys in shared context or handoff memory. diff --git a/android/app/build.gradle b/android/app/build.gradle index 7dd9ec0..6897dbf 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -3,20 +3,29 @@ apply plugin: 'com.android.application' android { namespace = "ee.clawbridge.app" compileSdk = rootProject.ext.compileSdkVersion + signingConfigs { + betaDebug { + storeFile file('../ci/claw-debug.keystore') + storePassword 'clawbridge' + keyAlias 'clawdebug' + keyPassword 'clawbridge' + } + } defaultConfig { applicationId "ee.clawbridge.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1 - versionName "0.3.0-beta" + versionCode 8 + versionName "0.4.8-beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { - // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. - // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' } } buildTypes { + debug { + signingConfig signingConfigs.betaDebug + } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' @@ -51,4 +60,4 @@ try { } } catch(Exception e) { logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") -} +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6dcccc8..e396202 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ @@ -36,7 +38,5 @@ - - diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..6539300 --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,7 @@ + + + + + localhost + + diff --git a/android/ci/claw-debug.keystore.b64 b/android/ci/claw-debug.keystore.b64 new file mode 100644 index 0000000..ace369a --- /dev/null +++ b/android/ci/claw-debug.keystore.b64 @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/app/command/page.tsx b/app/command/page.tsx index f0255dc..492824d 100644 --- a/app/command/page.tsx +++ b/app/command/page.tsx @@ -3,11 +3,11 @@ import { AppShell } from "@/components/app-shell" import { ConnectionGate } from "@/components/connection-gate" import { CommandScreen } from "@/components/command/command-screen" -export const metadata: Metadata = { title: "Command" } +export const metadata: Metadata = { title: "Chat" } export default function CommandPage() { return ( - + diff --git a/app/globals.css b/app/globals.css index f80395e..057de54 100644 --- a/app/globals.css +++ b/app/globals.css @@ -162,6 +162,55 @@ } } +.claw-shell { + --claw-red: #ef3d45; + --claw-glow-rgb: 239 61 69; +} + +.claw-orbit { + pointer-events: none; + position: fixed; + z-index: 0; + top: -24rem; + right: -26rem; + width: 42rem; + height: 42rem; + border-radius: 9999px; + border: 2px solid color-mix(in srgb, var(--claw-red) 88%, transparent); + opacity: .82; + background: + radial-gradient(circle at 32% 68%, rgb(var(--claw-glow-rgb) / 18%) 0 1px, transparent 2px) 0 0 / 18px 18px, + radial-gradient(circle at 50% 50%, rgb(var(--claw-glow-rgb) / 14%), transparent 58%); + box-shadow: + 0 0 16px rgb(var(--claw-glow-rgb) / 58%), + 0 0 42px rgb(var(--claw-glow-rgb) / 30%), + inset 0 0 54px rgb(var(--claw-glow-rgb) / 14%); + filter: saturate(1.15); +} + +.claw-orbit::after { + content: ""; + position: absolute; + inset: -2px; + border-radius: inherit; + border: 1px solid rgb(255 255 255 / 5%); + mask: linear-gradient(135deg, #000 0 36%, transparent 67%); +} + +.light .claw-orbit, +:root:not(.dark) .claw-orbit { + opacity: .2; +} + +@media (max-width: 480px) { + .claw-orbit { + top: -18rem; + right: -21rem; + width: 34rem; + height: 34rem; + } +} + .claw-launch { position: fixed; inset: 0; @@ -203,3 +252,141 @@ .claw-launch__bridge { animation: none; stroke-dashoffset: 0; } .claw-launch__prompt { animation: none; opacity: 1; } } + +/* CLAW display profiles. These override the base shadcn tokens without changing component code. */ +:root[data-claw-theme="claw-red"] { + color-scheme: dark; + --background: #090b0d; + --foreground: #f5f7f9; + --card: #111418; + --card-foreground: #f5f7f9; + --popover: #111418; + --popover-foreground: #f5f7f9; + --primary: #ef3d45; + --primary-foreground: #ffffff; + --secondary: #1b2026; + --secondary-foreground: #f5f7f9; + --muted: #171b20; + --muted-foreground: #9ca6b2; + --accent: #242a31; + --accent-foreground: #ffffff; + --border: rgb(255 255 255 / 11%); + --input: rgb(255 255 255 / 15%); + --ring: #ef3d45; +} + +:root[data-claw-theme="night-ops"] { + color-scheme: dark; + --background: #030605; + --foreground: #b8ffc4; + --card: #07100b; + --card-foreground: #c8ffd1; + --popover: #07100b; + --popover-foreground: #c8ffd1; + --primary: #39ff72; + --primary-foreground: #021006; + --secondary: #0b1a10; + --secondary-foreground: #aaf8b8; + --muted: #09140d; + --muted-foreground: #6faa7a; + --accent: #0f2415; + --accent-foreground: #c8ffd1; + --border: rgb(57 255 114 / 20%); + --input: rgb(57 255 114 / 18%); + --ring: #39ff72; +} + +:root[data-claw-theme="night-ops"] .claw-shell { + --claw-red: #39ff72; + --claw-glow-rgb: 57 255 114; +} + +:root[data-claw-theme="amber-terminal"] { + color-scheme: dark; + --background: #070604; + --foreground: #ffd889; + --card: #100d07; + --card-foreground: #ffe1a3; + --popover: #100d07; + --popover-foreground: #ffe1a3; + --primary: #ffb000; + --primary-foreground: #160d00; + --secondary: #1e1608; + --secondary-foreground: #ffd889; + --muted: #171106; + --muted-foreground: #a88648; + --accent: #2a1e08; + --accent-foreground: #ffe1a3; + --border: rgb(255 176 0 / 22%); + --input: rgb(255 176 0 / 18%); + --ring: #ffb000; +} + +:root[data-claw-theme="amber-terminal"] .claw-shell { + --claw-red: #ffb000; + --claw-glow-rgb: 255 176 0; +} + +:root[data-claw-theme="daylight"] { + color-scheme: light; + --background: #f7f7f2; + --foreground: #101113; + --card: #ffffff; + --card-foreground: #101113; + --popover: #ffffff; + --popover-foreground: #101113; + --primary: #0057ff; + --primary-foreground: #ffffff; + --secondary: #e8eaed; + --secondary-foreground: #15171a; + --muted: #eceeea; + --muted-foreground: #4f5660; + --accent: #dde5ff; + --accent-foreground: #0a2d78; + --destructive: #c62828; + --border: #c8ccd2; + --input: #bfc4ca; + --ring: #0057ff; +} + +:root[data-claw-theme="daylight"] .claw-shell { + --claw-red: #0057ff; + --claw-glow-rgb: 0 87 255; +} + +:root[data-claw-theme="daylight"] .claw-orbit { + opacity: .16; + filter: none; + box-shadow: 0 0 10px rgb(0 87 255 / 22%); +} + +:root[data-claw-theme="daylight"] header, +:root[data-claw-theme="daylight"] nav { + backdrop-filter: none; +} + +:root[data-claw-theme="oled-black"] { + color-scheme: dark; + --background: #000000; + --foreground: #ededed; + --card: #050505; + --card-foreground: #ededed; + --popover: #050505; + --popover-foreground: #ededed; + --primary: #ef3d45; + --primary-foreground: #ffffff; + --secondary: #101010; + --secondary-foreground: #ededed; + --muted: #0b0b0b; + --muted-foreground: #929292; + --accent: #151515; + --accent-foreground: #ffffff; + --border: rgb(255 255 255 / 12%); + --input: rgb(255 255 255 / 14%); + --ring: #ef3d45; +} + +:root[data-claw-theme="oled-black"] .claw-orbit { + opacity: .22; + box-shadow: 0 0 12px rgb(239 61 69 / 20%); +} diff --git a/app/layout.tsx b/app/layout.tsx index b7b669f..e01517e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,9 @@ import { Analytics } from "@vercel/analytics/next" import type { Metadata, Viewport } from "next" import { Geist, Geist_Mono } from "next/font/google" import { BridgeProvider } from "@/components/providers/bridge-provider" +import { ThemeProfileProvider } from "@/components/providers/theme-profile-provider" +import { LanguageProvider } from "@/components/providers/language-provider" +import { ObserverProvider } from "@/components/providers/observer-provider" import { Toaster } from "@/components/ui/sonner" import { BiometricGate } from "@/components/biometric-gate" import { LaunchSplash } from "@/components/launch-splash" @@ -37,7 +40,7 @@ export const metadata: Metadata = { } export const viewport: Viewport = { - colorScheme: "dark", + colorScheme: "dark light", themeColor: "#0a0a0a", width: "device-width", initialScale: 1, @@ -52,13 +55,19 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - + - - {children} - - + + + + + {children} + + + + + {process.env.NODE_ENV === "production" && } diff --git a/app/manifest.ts b/app/manifest.ts index 333b5a5..55809b0 100644 --- a/app/manifest.ts +++ b/app/manifest.ts @@ -22,7 +22,7 @@ export default function manifest(): MetadataRoute.Manifest { { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" }, ], shortcuts: [ - { name: "New command", url: "/command" }, + { name: "Chat / plan", url: "/command" }, { name: "Terminal", url: "/terminal" }, { name: "Approvals", url: "/approvals" }, ], diff --git a/app/page.tsx b/app/page.tsx index 8cef36f..061c7fe 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,7 +4,7 @@ import { Dashboard } from "@/components/dashboard/dashboard" export default function Page() { return ( - + diff --git a/app/seekclaw/page.tsx b/app/seekclaw/page.tsx new file mode 100644 index 0000000..2151d05 --- /dev/null +++ b/app/seekclaw/page.tsx @@ -0,0 +1,13 @@ +import { AppShell } from "@/components/app-shell" +import { ConnectionGate } from "@/components/connection-gate" +import { SeekClawDashboard } from "@/components/seekclaw/seekclaw-dashboard" + +export default function SeekClawPage() { + return ( + + + + + + ) +} diff --git a/app/settings/page.tsx b/app/settings/page.tsx index be87bab..4184383 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -2,14 +2,24 @@ import type { Metadata } from "next" import { AppShell } from "@/components/app-shell" import { ConnectionGate } from "@/components/connection-gate" import { SettingsScreen } from "@/components/settings/settings-screen" +import { ThemeSettings } from "@/components/settings/theme-settings" +import { LanguageSettings } from "@/components/settings/language-settings" +import { ObserverSettings } from "@/components/settings/observer-settings" +import { BrainSettings } from "@/components/settings/brain-settings" export const metadata: Metadata = { title: "Settings" } export default function SettingsPage() { return ( - + - +
+ + + + + +
) diff --git a/components/app-shell.tsx b/components/app-shell.tsx index 23a76be..0d0a412 100644 --- a/components/app-shell.tsx +++ b/components/app-shell.tsx @@ -3,60 +3,109 @@ import * as React from "react" import Link from "next/link" import { usePathname } from "next/navigation" -import { LayoutGridIcon, SendIcon, TerminalSquareIcon, ShieldCheckIcon, SettingsIcon } from "lucide-react" +import { LayoutGridIcon, SendIcon, TerminalSquareIcon, ShieldCheckIcon, SettingsIcon, PaletteIcon } from "lucide-react" import { cn } from "@/lib/utils" import { useBridge } from "@/components/providers/bridge-provider" +import { useThemeProfile } from "@/components/providers/theme-profile-provider" +import { useLanguage } from "@/components/providers/language-provider" +import { ObserverBanner } from "@/components/observer/observer-banner" import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" import { StatusDot } from "@/components/status-dot" const NAV = [ - { href: "/", label: "Home", icon: LayoutGridIcon }, - { href: "/command", label: "Command", icon: SendIcon }, - { href: "/terminal", label: "Terminal", icon: TerminalSquareIcon }, - { href: "/approvals", label: "Approvals", icon: ShieldCheckIcon }, - { href: "/settings", label: "Settings", icon: SettingsIcon }, + { href: "/", labelKey: "nav.home", icon: LayoutGridIcon }, + { href: "/command", labelKey: "nav.chat", icon: SendIcon }, + { href: "/terminal", labelKey: "nav.terminal", icon: TerminalSquareIcon }, + { href: "/approvals", labelKey: "nav.approvals", icon: ShieldCheckIcon }, + { href: "/settings", labelKey: "nav.settings", icon: SettingsIcon }, ] export function AppShell({ title, + titleKey, action, children, padded = true, }: { - title: string + title?: string + titleKey?: string action?: React.ReactNode children: React.ReactNode padded?: boolean }) { const pathname = usePathname() const { connectionState, status } = useBridge() + const { profile, resolvedProfile, cycleProfile } = useThemeProfile() + const { t } = useLanguage() const pending = status?.pendingApprovals ?? 0 + const [keyboardOpen, setKeyboardOpen] = React.useState(false) + const renderedTitle = titleKey ? t(titleKey) : (title ?? "CLAW") + + React.useEffect(() => { + const viewport = window.visualViewport + if (!viewport) return + let largestHeight = viewport.height + const update = () => { + largestHeight = Math.max(largestHeight, viewport.height) + const open = largestHeight - viewport.height > 96 + document.documentElement.style.setProperty("--claw-visual-height", `${viewport.height}px`) + setKeyboardOpen(open) + } + update() + viewport.addEventListener("resize", update) + viewport.addEventListener("scroll", update) + return () => { + viewport.removeEventListener("resize", update) + viewport.removeEventListener("scroll", update) + document.documentElement.style.removeProperty("--claw-visual-height") + } + }, []) return ( -
-
+
+
+ +
CLAW -

{title}

+

{renderedTitle}

+ {action}
+ {!keyboardOpen && } +
{children}