Snap-O

Tweaks Guide (Alpha)

Expose values directly from your Compose UI and see changes immediately. Interact with them through Snap-O’s Mac App Inspector, an optional on-device panel, the REST API, or an agent.

Use Maven Central

Snap-O publishes its Android libraries to Maven Central. Most Android projects already include mavenCentral(); add it to your dependency sources if yours does not.

settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}

Add the Android dependencies

Add the real Tweaks implementation to debug builds and the matching no-op implementation to release builds. Both expose the same Compose functions. In release, each function returns observable state containing its default without shipping the live registry or server. The no-op artifacts remain the recommended release setup. The overlay dependencies are optional. Add both only if you want an on-device floating panel. Their matching public APIs let the same app-root code compile in debug and release.

gradle/libs.versions.toml
[versions]
snapo = "3.2.0"

[libraries]
snapo-tweaks = { module = "com.openai.snapo:tweaks", version.ref = "snapo" }
snapo-tweaks-noop = { module = "com.openai.snapo:tweaks-noop", version.ref = "snapo" }

# Optional: add both if you want the in-app overlay panel.
snapo-tweaks-overlay = { module = "com.openai.snapo:tweaks-overlay", version.ref = "snapo" }
snapo-tweaks-overlay-noop = { module = "com.openai.snapo:tweaks-overlay-noop", version.ref = "snapo" }
app/build.gradle.kts
dependencies {
    debugImplementation(libs.snapo.tweaks)
    releaseImplementation(libs.snapo.tweaks.noop)

    // Optional: add both if you want the in-app overlay panel.
    debugImplementation(libs.snapo.tweaks.overlay)
    releaseImplementation(libs.snapo.tweaks.overlay.noop)
}

Add the Tweaks dependencies to each Android module that calls the tweak(...) function. In a multi-module app, a shared Gradle convention plugin can apply the debug and release pair consistently. Only the module that installs the optional overlay needs its additional overlay dependencies.

If a real Tweaks artifact is included in a nondebuggable app, tweaks still return their defaults, the server does not start, and the floating overlay stays hidden unless you explicitly enable Tweaks for that app.

Enable Tweaks in release builds

Only when you intentionally include the real Tweaks artifacts in a release build, add this metadata directly to your application's <application> element. It enables the Tweaks server and, if installed and enabled, the on-device overlay.

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <meta-data
            android:name="snapo.tweaks.allow_release"
            android:value="true" />
    </application>
</manifest>

This setting applies only to Tweaks. snapo.network.allow_release controls Network Inspector separately; enabling either feature does not enable the other. Prefer no-op release artifacts unless you need live inspection in a release build.

Expose values from Compose

Replace a fixed UI value with a tweak at the place that consumes it. Snap-O registers the control while that composable is in composition and returns observable State<T> that updates as you edit its value.

Kotlin · typography
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.openai.snapo.tweaks.tweak

@Composable
fun TypographyPreview() {
    Text(
        text = tweak("Make it feel right.", name = "Typography/Preview text").value,
        fontSize = tweak(36, "Typography/Font size", 16..72).value.sp,
        fontWeight = FontWeight(tweak(600, "Typography/Font weight", 100..900, step = 100).value),
        color = tweak(Color(0xFF18212F), "Colors/Text").value,
    )
}

Read each tweak close to its consumer when practical. You do not need to hoist every value to the top of a screen.

Compose function reference

Import com.openai.snapo.tweaks.tweak and call the overload matching your default value from composition. Every overload returns State<T>. Delegate the state with by, or read tweak(...).value immediately. Numeric ranges and increments are optional. For strings, name the name argument to distinguish it from the default value.

com.openai.snapo.tweaks · Kotlin
@Composable
fun tweak(
    default: Int,
    name: String,
    range: IntRange? = null,
    step: Int? = null,
): State<Int>

@Composable
fun tweak(
    default: Float,
    name: String,
    range: ClosedFloatingPointRange<Float>? = null,
    step: Float? = null,
): State<Float>

@Composable
fun tweak(
    default: Color,
    name: String,
): State<Color>

@Composable
fun tweak(
    default: Boolean,
    name: String,
): State<Boolean>

@Composable
fun tweak(
    default: String,
    name: String,
): State<String>

Keep the returned state unread until the phase that needs it. For a value used only during drawing or layout, read the delegated value inside the draw or layout callback to avoid recomposing the caller.

Kotlin · deferred draw read
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.openai.snapo.tweaks.tweak

@Composable
fun MotionTrack(modifier: Modifier = Modifier) {
    val thickness by tweak(
        default = 2f,
        name = "Motion/Track thickness",
        range = 1f..8f,
        step = 0.5f,
    )

    Box(
        modifier = modifier.drawBehind {
            drawLine(
                color = Color.Black,
                start = Offset(x = 0f, y = center.y),
                end = Offset(x = size.width, y = center.y),
                strokeWidth = thickness.dp.toPx(),
            )
        },
    )
}

Group tweaks by section (optional)

Tweaks work without sections. When grouping would make a screen easier to inspect, use a slash-separated name to place a control in a visible inspector section. For example, Typography/Font size appears under Typography, and Motion/Duration appears under Motion.

Kotlin · visibility and motion
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.openai.snapo.tweaks.tweak

@Composable
fun MotionSection(isExpanded: Boolean) {
    if (!tweak(true, "Motion/Show").value) return

    val useSpring by tweak(true, "Motion/Use spring")
    val animationSpec = if (useSpring) {
        spring<Float>(
            dampingRatio = tweak(0.7f, "Motion/Spring damping", 0.1f..1f, step = 0.05f).value,
            stiffness = tweak(280f, "Motion/Spring stiffness", 80f..800f, step = 20f).value,
        )
    } else {
        tween<Float>(
            durationMillis = tweak(400, "Motion/Duration", 100..1500, step = 50).value,
            easing = FastOutSlowInEasing,
        )
    }

    AnimatedVisibility(
        visible = isExpanded,
        enter = fadeIn(animationSpec),
        exit = fadeOut(animationSpec),
    ) {
        Box(modifier = Modifier.size(48.dp)) {
            // Animated content.
        }
    }
}

One name, one tweak. Reusing the exact same name in multiple composed consumers shares one control and current value. Use matching defaults and bounds for each use.

Controls appear only while their composables are in composition. In the example, turning off Motion/Show removes the motion controls; changing Motion/Use spring swaps the spring settings for the duration control. When the same UI returns during the app process, its controls register again with their last edited values and original ordering.

Interact with tweaks

Once the enabled app exposes a tweak, choose the interaction that fits your workflow: inspect the app on your Mac, use an optional panel on the device, call the REST API, or let an agent work with the same live values.

Mac App Inspector

Snap-O’s App Inspector discovers the running app with Tweaks enabled and shows the controls registered by its current Compose screen.

  1. Install and launch the debug build, or an explicitly enabled release build, on a connected, authorized Android device or emulator.
  2. Open Snap-O on macOS and select the device.
  3. Choose Tools → Show App Inspector, or press ⌘⌥I.
  4. Select your app and its Tweaks entry in the inspector picker.
  5. Adjust a slider, color, switch, or text field and watch the running Compose UI update.
  6. Reset an individual control or use Reset all tweaks to restore defaults.

Navigate through your Android app to change which controls are visible. Only tweaks belonging to the currently composed UI appear in the inspector.

On-device panel

For a developer-facing UI on the Android device, the optional SnapOTweakOverlay starts as a movable floating control and expands into an editable panel. Install it at the app root and expose Snap-O’s built-in overlay setting from your developer settings. The panel automatically observes the same currently composed tweaks as the Mac inspector. See Add an on-device floating panel for the module and root wrapper.

REST API

The Tweaks server exposes a small HTTP API for reading currently composed controls, updating their values, and streaming changes. Use it to build your own inspection tools, integrations, and custom panels.

See the Tweaks protocol reference for every endpoint, example requests and responses, live events, resets, and error handling.

Agents (e.g. Codex)

Install the official Snap-O Codex plugin to give an agent the dedicated Tweaks skill and shared command-line client. The plugin requires Python 3 and Android Platform Tools.

Terminal · install the Codex plugin
codex plugin marketplace add openai/snap-o --ref main
codex plugin add snap-o@snap-o
Migrate an existing sparse marketplace installation

If Snap-O was previously installed with sparse paths, remove and add its marketplace again so the shared CLI and both plugin skills are available:

Terminal · migrate the Codex plugin
codex plugin marketplace remove snap-o
codex plugin marketplace add openai/snap-o --ref main
codex plugin add snap-o@snap-o

Start a new Codex session after installation. Ask the agent to inspect the available controls, apply a requested design direction, or reset a value. For example: “Make this screen feel calmer; try the typography, color, and motion tweaks and tell me what changed.” The skill discovers the running app, reads typed descriptors, and changes or resets values only when requested.

Snap-O for macOS also bundles the same CLI. Use it directly for discovery, automation, live snapshots, and explicitly requested updates:

Terminal · inspect and update live tweaks
SNAPO_BIN="/Applications/Snap-O.app/Contents/MacOS/snapo"

"$SNAPO_BIN" tweaks apps --json
"$SNAPO_BIN" tweaks list -s emulator-5554 -n snapo_tweaks_12345 --json
"$SNAPO_BIN" tweaks set 'Typography/Font size' 42 -s emulator-5554 -n snapo_tweaks_12345 --json
"$SNAPO_BIN" tweaks reset 'Typography/Font size' -s emulator-5554 -n snapo_tweaks_12345 --json
"$SNAPO_BIN" tweaks watch -s emulator-5554 -n snapo_tweaks_12345 --once --json

The CLI manages Android socket discovery, forwarding, and cleanup. Device serials and socket names come from the discovery output and change when the app process restarts.

You can also ask an agent to create a small custom panel for a specific workflow, such as typography comparisons, animation tuning, or a curated set of design controls. The panel can read GET /tweaks, send PATCH /tweaks, and subscribe to GET /tweaks/events. No separate Snap-O agent API or automatic panel integration is required or implied.

Apply tweaks to your codebase

Once you are happy with the adjustments you made in your app, ask an AI agent, such as Codex, to apply them to your codebase:

“Apply all the tweak adjustments I made in the app to my codebase.”

The agent can read the latest tweaked values from your running app and update the corresponding values in your source code so your adjustments become part of the app.

Optional: add an on-device floating panel

The optional overlay provides SnapOTweakOverlay for apps that also want to edit live tweaks on the device. Wrap your root content once. Snap-O owns and persists the developer setting, and the overlay automatically discovers tweaks from the current composition. Its matching no-op artifact renders your app content unchanged in release, so this code belongs in your shared source set.

Kotlin · shared root content
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.sp
import com.openai.snapo.tweaks.overlay.SnapOTweakOverlay
import com.openai.snapo.tweaks.overlay.SnapOTweakOverlaySettings
import com.openai.snapo.tweaks.tweak

@Composable
fun AppRoot() {
    SnapOTweakOverlay {
        ProfileScreen()
    }
}

@Composable
fun OverlayDeveloperSetting() {
    Switch(
        checked = SnapOTweakOverlaySettings.isEnabled,
        onCheckedChange = { SnapOTweakOverlaySettings.isEnabled = it },
    )
}

@Composable
fun ProfileScreen() {
    val fontSize by tweak(
        default = 16,
        name = "Typography/Font size",
        range = 12..32,
    )

    Text(
        text = "Profile",
        fontSize = fontSize.sp,
    )
}

The panel starts as a collapsed floating button that can be moved anywhere on the screen. Tap it to expand the panel and edit integer, floating-point, boolean, color, or text tweaks. Reset an individual tweak or restore every tweak to its default. Changes update the running app immediately and are available to the Mac inspector through its normal updates. Snap-O saves the button's horizontal and vertical position, restoring it when the button returns or the app restarts.

Visibility follows the current Compose screen. The button appears only when the real overlay is installed, the Tweaks runtime is enabled, SnapOTweakOverlaySettings.isEnabled is true and at least one tweak is in composition. The setting defaults to off and survives app restarts. As screens and sections appear or disappear, the panel updates automatically. The no-op overlay never appears, and its setting remains disabled. No manually supplied values, update callback, or build-specific app wrapper is required.

Troubleshooting

  • Confirm the installed app contains the live :tweaks artifact, not :tweaks-noop.
  • For a nondebuggable app, set snapo.tweaks.allow_release to true in its <application> metadata.
  • If no Tweaks server appears, check device authorization, app startup, the live dependency, and whether the current build allows the runtime.
  • If the server appears but its tweak list is empty, open a screen that calls tweak(...); controls exist only while their composables are in composition.
  • Use the exact same name, default, and bounds when one control is read in multiple places.
  • Choose your app’s Tweaks entry inside App Inspector.
  • After the Android app restarts, select its current running app or process if prompted.
  • For the optional overlay, confirm its developer setting is enabled and the current Compose screen contains at least one tweak.