
Gesture-triggered debug overlay to inspect logs, grouped events, HTTP traffic, and live session state; supports custom log types/renderers, real-time search/filters, custom triggers, and zero-overhead release stubs.
A gesture-triggered in-app devtool for Kotlin Multiplatform apps.
Drop it in, swipe to open, and inspect logs, HTTP traffic, and session state — without touching production code.
| 📋 Log viewer | Filterable log list — levels, tags, full-text search |
| 🎨 Custom log types | Define your own Log subtypes with their own UI renderers |
| 🔍 Search & filter | Real-time full-text search across message, tag, and level |
| 🔗 Log grouping | Link related logs with a shared groupId
|
| 🌐 Network inspector | OkHttp + Ktor interceptors with headers, body, duration, status |
| 🗂️ Details panel | Live key/value sidebar for session info, flags, build metadata |
| ⏸️ Stepper | Pause log processing and replay events one-by-one |
| 🔬 UI inspector | Composable tree, bounds, recomposition counts, heatmap, quick controls, font and RTL overrides |
| 🖥️ Custom tabs | Add your own full screens to the Console navigation bar |
| 👆 Custom triggers | Swap the default swipe for any gesture — double-tap, shake, etc. |
| 🚫 Zero prod cost | Noop stubs with identical APIs — no UI, no overhead in release builds |
Adds the JetBrains Compose repository required to resolve Compose Multiplatform artifacts.
// settings.gradle.kts
plugins {
id("io.github.thernal.console") version "<version>"
}// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-bundle-ui:<version>")
releaseImplementation("io.github.thernal:console-ui-noop:<version>")
}console-bundle-ui brings in every console module and addon at once. Prefer
hand-picking? Depend on the individual artifacts instead (see
Modules):
dependencies {
debugImplementation("io.github.thernal:console-ui:<version>")
debugImplementation("io.github.thernal:console-logging-ui:<version>")
releaseImplementation("io.github.thernal:console-ui-noop:<version>")
}@Composable
fun App() {
ConsoleProvider {
YourAppContent()
}
}Swipe ↑ ↓ ← → anywhere on screen (default gesture).
// Fire-and-forget — thread-safe, non-blocking
Console.notify {
Log(message = "Payment initiated", tag = "Payments", level = LogLevel.Info)
}
// Suspending — preserves ordering when called sequentially inside a coroutine
Console.asyncNotify {
Log(message = "Token refreshed", tag = "Auth", level = LogLevel.Success)
}| Level | Typical use |
|---|---|
None |
No level indicator |
Verbose |
Trace-level detail |
Debug |
Development information |
Info |
General flow events |
Success |
Completed operations |
Warning |
Recoverable issues |
Error |
Failures that were handled |
Fatal |
Unrecoverable crashes |
Logs with the same groupId are visually linked — useful for correlating events like a network request and its response.
val id = Uuid.random().toString()
Console.notify {
Log(message = "→ POST /auth/login", tag = "Network", level = LogLevel.Debug, groupId = id)
}
// ... later ...
Console.notify {
Log(message = "← 200 OK (143ms)", tag = "Network", level = LogLevel.Success, groupId = id)
}Implement Log to carry structured data through the pipeline.
data class AnalyticsLog(
override val message: String,
override val level: LogLevel = LogLevel.Debug,
val eventName: String,
val params: Map<String, Any> = emptyMap(),
override val id: String = Uuid.random().toString(),
override val tag: String? = "Analytics",
override val groupId: String? = null,
override val timestamp: Instant = Clock.System.now(),
) : LogSend it like any other log:
Console.notify {
AnalyticsLog(
message = "screen_view",
eventName = "screen_view",
params = mapOf("screen_name" to "Checkout"),
)
}Implement LogRenderer so AnalyticsLog entries have their own item and detail screens, then register it for the type:
@file:OptIn(ConsoleInternalApi::class) // LogRendererRegistry.register is a first-party API
object AnalyticsLogRenderer : LogRenderer {
@Composable
override fun Item(log: Log, modifier: Modifier) {
if (log !is AnalyticsLog) return
AnalyticsLogItem(log, modifier)
}
@Composable
override fun Detail(log: Log) {
if (log !is AnalyticsLog) return
AnalyticsLogDetail(log)
}
}
object AnalyticsAddon : ConsoleAddon {
override fun onInstall() {
LogRendererRegistry.register<AnalyticsLog>(AnalyticsLogRenderer)
}
}
// Call once at startup
AnalyticsAddon.install()The log list filters in real time as you type. The query matches the log message; tags and levels are chip filters
above the list (an empty selection means all). See addons/logging-ui for the buffer
size and capture settings.
The built-in gesture is a swipe sequence: ↑ ↓ ← →
ConsoleProvider { YourAppContent() } // default trigger, no configuration neededConsoleProvider(
trigger = ConsoleTrigger.swipeSequence(Swipe.UP, Swipe.DOWN)
) {
YourAppContent()
}ConsoleTrigger is a fun interface — any Modifier extension that calls onDetected() qualifies:
// Double-tap anywhere on screen
val doubleTapTrigger = ConsoleTrigger { onDetected ->
pointerInput(Unit) {
detectTapGestures(onDoubleTap = { onDetected() })
}
}
ConsoleProvider(trigger = doubleTapTrigger) {
YourAppContent()
}// Long-press trigger
val longPressTrigger = ConsoleTrigger { onDetected ->
pointerInput(Unit) {
detectTapGestures(onLongPress = { onDetected() })
}
}Captures HTTP traffic and renders it in the log list with method, status code, URL, headers, body, and round-trip duration. Tap any entry to see the full request/response detail.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-network-core:<version>")
debugImplementation("io.github.thernal:console-network-okhttp:<version>")
debugImplementation("io.github.thernal:console-network-ui:<version>")
}val client = OkHttpClient.Builder()
.addInterceptor(ConsoleNetworkOkHttpInterceptor())
.build()// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-network-core:<version>")
debugImplementation("io.github.thernal:console-network-ktor:<version>")
debugImplementation("io.github.thernal:console-network-ui:<version>")
}val client = HttpClient {
install(ConsoleNetworkKtorPlugin)
}Authorization, Cookie, Set-Cookie, X-Api-Key, and Proxy-Authorization are masked with *** by default (SensitiveHeaders.DEFAULT).
// Custom names
ConsoleNetworkOkHttpInterceptor(
sensitiveHeaders = SensitiveHeaders(names = setOf("authorization", "x-session-token"))
)
// Disable masking entirely
ConsoleNetworkOkHttpInterceptor(sensitiveHeaders = SensitiveHeaders.NONE)
// Same API for Ktor
HttpClient {
install(ConsoleNetworkKtorPlugin) {
sensitiveHeaders = SensitiveHeaders.NONE
}
}The names can also be changed at runtime from the Network settings section. Masked values always read ***; a
custom mask is currently not applied. See addons/network-ui for the capture flow and
body handling.
A live key/value sidebar for session info, feature flags, user context, or any ambient state — visible at a glance without scrolling through logs.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-details-ui:<version>")
releaseImplementation("io.github.thernal:console-details-core-noop:<version>")
}// Upsert — updates in place if the key already exists
ConsoleDetails.put("User" to "alice@example.com")
ConsoleDetails.put("Environment" to "staging")
ConsoleDetails.put("Feature:NewCheckout" to "enabled")
// Remove
ConsoleDetails.remove("Environment")ConsoleKey declares reusable keys, and ConsoleDetails.clear() empties the panel; see
addons/details-ui.
Pauses log processing and lets you replay events one-by-one — useful for stepping through complex async flows that would otherwise scroll past instantly.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-stepper-ui:<version>")
}No code required. Once the module is on the classpath, the stepper control appears automatically in the floating dock. Tap Pause to freeze the pipeline, Step to advance one event at a time, and Resume to return to live mode. While the pipeline is paused the widget stays on screen even if floating widgets are hidden.
A held event holds the whole pipeline, including callers that wait for it (asyncNotify, blockingNotify, the network
integrations). See addons/stepper-ui for the pause filter and auto-resume.
Inspects the running Compose UI from inside the console: the composable tree, design-tool style picking, frame, padding and margin, a two-point ruler, and recomposition counts with flashes and a heatmap, plus font scale and RTL overrides for the app content.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-inspector-ui:<version>")
releaseImplementation("io.github.thernal:console-inspector-core-noop:<version>")
}No code required: the Inspector tab and the dock widget appear automatically. On JVM desktop call
enableInspectorSourceInformation() before creating the window. See
addons/inspector-ui for every feature, the settings and the architecture.
Streams every log to disk as it happens and captures uncaught exceptions, so after a crash you can relaunch the app and inspect the session locally — the fatal stack trace plus the logs that led up to it — without waiting for a remote crash-reporting round-trip.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-crash-report-ui:<version>")
}No code required. Past sessions appear in the Crashes tab, classified as Confirmed (trace captured), Probable (died in the foreground — native crash/OOM/ANR), or Safe (killed in the background; hidden by default, toggleable from the in-console settings screen). Opening a session shows its logs — network logs render with their full detail, the fatal stack trace arrives as the final Fatal-level entry — and each log opens its own detail screen. Sessions are deleted with a swipe, and persistence is tunable from the settings screen or in code:
CrashReports.updateConfig {
copy(bodyPolicy = CrashBodyPolicy.None, maxSessions = 5)
}See addons/crash-report-ui for the save-filter, redactor, and custom log codec details.
Add your own full-screen view to the Console navigation bar by implementing ConsoleAddon.
// 1. Define the tab
object MetricsTab : ConsoleTab {
override val title = "Metrics"
override val icon = Icons.Default.BarChart
override val order = 10 // lower = further left in the nav bar
@Composable
override fun Content() {
MetricsScreen()
}
}
// 2. Expose it via an addon
object MetricsAddon : ConsoleAddon {
override fun tab(): ConsoleTab = MetricsTab
}
// 3. Install once at app startup
MetricsAddon.install()Interactive floating controls live in one shared dock: a compact, opaque container that owns position and stacking, so addons never fight over screen corners. Resting, it is one round button that snaps to a screen edge. Tapping it opens a card with a tab per widget (sorted by title, so no addon looks more important than another); the selected widget's live summary and controls fill the card, and switching tabs slides between them. The card is the only place controls live, so each control is always under the addon it belongs to. The button shows a dot while a widget is running. When a widget needs the user (an inspect pick, a paused stepper) the border turns amber, and when the dock is folded it grows a pill with just that widget's quick actions. The dock never folds by itself, so you can toggle several controls in a row, and it cannot be hidden while a widget needs attention.
Add a widget from your addon:
object MetricsWidget : ConsoleDockWidget {
override val id = "metrics"
override val title = "Metrics"
override val icon = Icons.Default.BarChart
@Composable override fun status() = ConsoleDockStatus.Idle // Active shows a dot, Attention grows a pill, pins it
@Composable override fun summary(): String? = "3 tracked" // live line under the title
@Composable override fun QuickAction() { /* what to do right now, beside the folded dock; only shown while status is Attention */ }
@Composable override fun Panel() {
DsChipGroup(title = "Metrics") {
DsActionChip(label = "Chart", icon = Icons.Default.BarChart, onClick = { /* ... */ })
}
}
}
object MetricsAddon : ConsoleAddon {
override fun dockWidget(): ConsoleDockWidget = MetricsWidget
}Full-screen decoration goes through layered overlays instead, ordered and hidden together with the widget they name:
override fun overlays() = listOf(
ConsoleOverlay(layer = ConsoleOverlayLayer.Backdrop, widgetId = "metrics") { /* draws, no touches */ },
ConsoleOverlay(layer = ConsoleOverlayLayer.Capture, widgetId = "metrics") { /* may take over touches */ },
)Stacking, bottom to top: app, Backdrop, Capture, legacy overlay(), the dock, the console itself.
Build the panel from the design system's DsChipGroup and DsActionChip (a labelled wrap of icon-only buttons, tinted
when on) and the quick action from DsQuickAction.
Hiding: Settings → Floating widgets has a switch that hides every widget and overlay, and a list of widget ids
to hide individually. Widgets that need attention (a paused stepper, an active inspector pick) stay visible. The same
config is available in code: ConsoleDock.updateConfig { copy(isVisible = false) }.
Beyond tabs, ConsoleAddon also supports:
navGraph() — register a full navigation sub-graph behind your taboverlay() — inject a free-form composable over the app (prefer dockWidget() and overlays() above)One-dependency aggregators. Both exclude the design system (add
console-components yourself if you want the Ds* primitives) and the HTTP
interceptors (add the console-network-ktor / console-network-okhttp module
matching your client).
| Artifact | Description |
|---|---|
io.github.thernal:console-bundle-core:<version> |
Everything a plain JVM/domain module can depend on — console-core/io/runtime + every addon -core module (no Compose) |
io.github.thernal:console-bundle-ui:<version> |
The full console — console-bundle-core + the UI shell and every addon |
| Artifact | Description |
|---|---|
io.github.thernal:console-core:<version> |
Foundation — Log, LogLevel, LogObserver, LogProcessor, ConsoleScope (no UI, no pipeline) |
io.github.thernal:console-io:<version> |
Shared file-system infrastructure — ConsoleFileSystem, AppendSink (no UI, no pipeline) |
io.github.thernal:console-runtime:<version> |
Console singleton + log pipeline (depends on console-core) |
io.github.thernal:console-api:<version> |
Addon contracts — ConsoleAddon, ConsoleTab, LogRenderer, LogRendererRegistry (depends on console-core, not runtime) |
io.github.thernal:console-ui:<version> |
Compose UI shell — ConsoleProvider, navigation, overlay |
io.github.thernal:console-ui-noop:<version> |
No-op stub for production builds |
Each addon has its own README with setup, settings and architecture; start from the addon index.
| Artifact | Description |
|---|---|
io.github.thernal:console-logging-ui:<version> |
Log list, log detail screen, BasicLog renderer |
io.github.thernal:console-details-ui:<version> |
Live key/value Details panel |
io.github.thernal:console-details-core-noop:<version> |
No-op stub for production builds |
io.github.thernal:console-network-core:<version> |
Shared network log types |
io.github.thernal:console-network-okhttp:<version> |
OkHttp interceptor |
io.github.thernal:console-network-ktor:<version> |
Ktor plugin |
io.github.thernal:console-network-ui:<version> |
Network log UI renderer |
io.github.thernal:console-stepper-ui:<version> |
Pause-and-step log replay |
io.github.thernal:console-inspector-core:<version> |
UI inspector config and facade |
io.github.thernal:console-inspector-core-noop:<version> |
No-op stub for production builds |
io.github.thernal:console-inspector-ui:<version> |
UI inspector — tree, overlay, recomposition tracking, display overrides |
io.github.thernal:console-crash-report-core:<version> |
Crash session serialization + LogCodec registry |
io.github.thernal:console-crash-report-ui:<version> |
Crashes tab — streaming persistence, crash capture, session list/detail |
io.github.thernal:console-settings-api:<version> |
Settings registration contract — SettingsSection, entry DSL, SettingsRegistry
|
io.github.thernal:console-settings-api-noop:<version> |
No-op stub for production builds |
io.github.thernal:console-settings-ui:<version> |
Settings tab — persisted per-addon overrides, entry renderers, General section |
MIT — see LICENSE.
A gesture-triggered in-app devtool for Kotlin Multiplatform apps.
Drop it in, swipe to open, and inspect logs, HTTP traffic, and session state — without touching production code.
| 📋 Log viewer | Filterable log list — levels, tags, full-text search |
| 🎨 Custom log types | Define your own Log subtypes with their own UI renderers |
| 🔍 Search & filter | Real-time full-text search across message, tag, and level |
| 🔗 Log grouping | Link related logs with a shared groupId
|
| 🌐 Network inspector | OkHttp + Ktor interceptors with headers, body, duration, status |
| 🗂️ Details panel | Live key/value sidebar for session info, flags, build metadata |
| ⏸️ Stepper | Pause log processing and replay events one-by-one |
| 🔬 UI inspector | Composable tree, bounds, recomposition counts, heatmap, quick controls, font and RTL overrides |
| 🖥️ Custom tabs | Add your own full screens to the Console navigation bar |
| 👆 Custom triggers | Swap the default swipe for any gesture — double-tap, shake, etc. |
| 🚫 Zero prod cost | Noop stubs with identical APIs — no UI, no overhead in release builds |
Adds the JetBrains Compose repository required to resolve Compose Multiplatform artifacts.
// settings.gradle.kts
plugins {
id("io.github.thernal.console") version "<version>"
}// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-bundle-ui:<version>")
releaseImplementation("io.github.thernal:console-ui-noop:<version>")
}console-bundle-ui brings in every console module and addon at once. Prefer
hand-picking? Depend on the individual artifacts instead (see
Modules):
dependencies {
debugImplementation("io.github.thernal:console-ui:<version>")
debugImplementation("io.github.thernal:console-logging-ui:<version>")
releaseImplementation("io.github.thernal:console-ui-noop:<version>")
}@Composable
fun App() {
ConsoleProvider {
YourAppContent()
}
}Swipe ↑ ↓ ← → anywhere on screen (default gesture).
// Fire-and-forget — thread-safe, non-blocking
Console.notify {
Log(message = "Payment initiated", tag = "Payments", level = LogLevel.Info)
}
// Suspending — preserves ordering when called sequentially inside a coroutine
Console.asyncNotify {
Log(message = "Token refreshed", tag = "Auth", level = LogLevel.Success)
}| Level | Typical use |
|---|---|
None |
No level indicator |
Verbose |
Trace-level detail |
Debug |
Development information |
Info |
General flow events |
Success |
Completed operations |
Warning |
Recoverable issues |
Error |
Failures that were handled |
Fatal |
Unrecoverable crashes |
Logs with the same groupId are visually linked — useful for correlating events like a network request and its response.
val id = Uuid.random().toString()
Console.notify {
Log(message = "→ POST /auth/login", tag = "Network", level = LogLevel.Debug, groupId = id)
}
// ... later ...
Console.notify {
Log(message = "← 200 OK (143ms)", tag = "Network", level = LogLevel.Success, groupId = id)
}Implement Log to carry structured data through the pipeline.
data class AnalyticsLog(
override val message: String,
override val level: LogLevel = LogLevel.Debug,
val eventName: String,
val params: Map<String, Any> = emptyMap(),
override val id: String = Uuid.random().toString(),
override val tag: String? = "Analytics",
override val groupId: String? = null,
override val timestamp: Instant = Clock.System.now(),
) : LogSend it like any other log:
Console.notify {
AnalyticsLog(
message = "screen_view",
eventName = "screen_view",
params = mapOf("screen_name" to "Checkout"),
)
}Implement LogRenderer so AnalyticsLog entries have their own item and detail screens, then register it for the type:
@file:OptIn(ConsoleInternalApi::class) // LogRendererRegistry.register is a first-party API
object AnalyticsLogRenderer : LogRenderer {
@Composable
override fun Item(log: Log, modifier: Modifier) {
if (log !is AnalyticsLog) return
AnalyticsLogItem(log, modifier)
}
@Composable
override fun Detail(log: Log) {
if (log !is AnalyticsLog) return
AnalyticsLogDetail(log)
}
}
object AnalyticsAddon : ConsoleAddon {
override fun onInstall() {
LogRendererRegistry.register<AnalyticsLog>(AnalyticsLogRenderer)
}
}
// Call once at startup
AnalyticsAddon.install()The log list filters in real time as you type. The query matches the log message; tags and levels are chip filters
above the list (an empty selection means all). See addons/logging-ui for the buffer
size and capture settings.
The built-in gesture is a swipe sequence: ↑ ↓ ← →
ConsoleProvider { YourAppContent() } // default trigger, no configuration neededConsoleProvider(
trigger = ConsoleTrigger.swipeSequence(Swipe.UP, Swipe.DOWN)
) {
YourAppContent()
}ConsoleTrigger is a fun interface — any Modifier extension that calls onDetected() qualifies:
// Double-tap anywhere on screen
val doubleTapTrigger = ConsoleTrigger { onDetected ->
pointerInput(Unit) {
detectTapGestures(onDoubleTap = { onDetected() })
}
}
ConsoleProvider(trigger = doubleTapTrigger) {
YourAppContent()
}// Long-press trigger
val longPressTrigger = ConsoleTrigger { onDetected ->
pointerInput(Unit) {
detectTapGestures(onLongPress = { onDetected() })
}
}Captures HTTP traffic and renders it in the log list with method, status code, URL, headers, body, and round-trip duration. Tap any entry to see the full request/response detail.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-network-core:<version>")
debugImplementation("io.github.thernal:console-network-okhttp:<version>")
debugImplementation("io.github.thernal:console-network-ui:<version>")
}val client = OkHttpClient.Builder()
.addInterceptor(ConsoleNetworkOkHttpInterceptor())
.build()// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-network-core:<version>")
debugImplementation("io.github.thernal:console-network-ktor:<version>")
debugImplementation("io.github.thernal:console-network-ui:<version>")
}val client = HttpClient {
install(ConsoleNetworkKtorPlugin)
}Authorization, Cookie, Set-Cookie, X-Api-Key, and Proxy-Authorization are masked with *** by default (SensitiveHeaders.DEFAULT).
// Custom names
ConsoleNetworkOkHttpInterceptor(
sensitiveHeaders = SensitiveHeaders(names = setOf("authorization", "x-session-token"))
)
// Disable masking entirely
ConsoleNetworkOkHttpInterceptor(sensitiveHeaders = SensitiveHeaders.NONE)
// Same API for Ktor
HttpClient {
install(ConsoleNetworkKtorPlugin) {
sensitiveHeaders = SensitiveHeaders.NONE
}
}The names can also be changed at runtime from the Network settings section. Masked values always read ***; a
custom mask is currently not applied. See addons/network-ui for the capture flow and
body handling.
A live key/value sidebar for session info, feature flags, user context, or any ambient state — visible at a glance without scrolling through logs.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-details-ui:<version>")
releaseImplementation("io.github.thernal:console-details-core-noop:<version>")
}// Upsert — updates in place if the key already exists
ConsoleDetails.put("User" to "alice@example.com")
ConsoleDetails.put("Environment" to "staging")
ConsoleDetails.put("Feature:NewCheckout" to "enabled")
// Remove
ConsoleDetails.remove("Environment")ConsoleKey declares reusable keys, and ConsoleDetails.clear() empties the panel; see
addons/details-ui.
Pauses log processing and lets you replay events one-by-one — useful for stepping through complex async flows that would otherwise scroll past instantly.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-stepper-ui:<version>")
}No code required. Once the module is on the classpath, the stepper control appears automatically in the floating dock. Tap Pause to freeze the pipeline, Step to advance one event at a time, and Resume to return to live mode. While the pipeline is paused the widget stays on screen even if floating widgets are hidden.
A held event holds the whole pipeline, including callers that wait for it (asyncNotify, blockingNotify, the network
integrations). See addons/stepper-ui for the pause filter and auto-resume.
Inspects the running Compose UI from inside the console: the composable tree, design-tool style picking, frame, padding and margin, a two-point ruler, and recomposition counts with flashes and a heatmap, plus font scale and RTL overrides for the app content.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-inspector-ui:<version>")
releaseImplementation("io.github.thernal:console-inspector-core-noop:<version>")
}No code required: the Inspector tab and the dock widget appear automatically. On JVM desktop call
enableInspectorSourceInformation() before creating the window. See
addons/inspector-ui for every feature, the settings and the architecture.
Streams every log to disk as it happens and captures uncaught exceptions, so after a crash you can relaunch the app and inspect the session locally — the fatal stack trace plus the logs that led up to it — without waiting for a remote crash-reporting round-trip.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-crash-report-ui:<version>")
}No code required. Past sessions appear in the Crashes tab, classified as Confirmed (trace captured), Probable (died in the foreground — native crash/OOM/ANR), or Safe (killed in the background; hidden by default, toggleable from the in-console settings screen). Opening a session shows its logs — network logs render with their full detail, the fatal stack trace arrives as the final Fatal-level entry — and each log opens its own detail screen. Sessions are deleted with a swipe, and persistence is tunable from the settings screen or in code:
CrashReports.updateConfig {
copy(bodyPolicy = CrashBodyPolicy.None, maxSessions = 5)
}See addons/crash-report-ui for the save-filter, redactor, and custom log codec details.
Add your own full-screen view to the Console navigation bar by implementing ConsoleAddon.
// 1. Define the tab
object MetricsTab : ConsoleTab {
override val title = "Metrics"
override val icon = Icons.Default.BarChart
override val order = 10 // lower = further left in the nav bar
@Composable
override fun Content() {
MetricsScreen()
}
}
// 2. Expose it via an addon
object MetricsAddon : ConsoleAddon {
override fun tab(): ConsoleTab = MetricsTab
}
// 3. Install once at app startup
MetricsAddon.install()Interactive floating controls live in one shared dock: a compact, opaque container that owns position and stacking, so addons never fight over screen corners. Resting, it is one round button that snaps to a screen edge. Tapping it opens a card with a tab per widget (sorted by title, so no addon looks more important than another); the selected widget's live summary and controls fill the card, and switching tabs slides between them. The card is the only place controls live, so each control is always under the addon it belongs to. The button shows a dot while a widget is running. When a widget needs the user (an inspect pick, a paused stepper) the border turns amber, and when the dock is folded it grows a pill with just that widget's quick actions. The dock never folds by itself, so you can toggle several controls in a row, and it cannot be hidden while a widget needs attention.
Add a widget from your addon:
object MetricsWidget : ConsoleDockWidget {
override val id = "metrics"
override val title = "Metrics"
override val icon = Icons.Default.BarChart
@Composable override fun status() = ConsoleDockStatus.Idle // Active shows a dot, Attention grows a pill, pins it
@Composable override fun summary(): String? = "3 tracked" // live line under the title
@Composable override fun QuickAction() { /* what to do right now, beside the folded dock; only shown while status is Attention */ }
@Composable override fun Panel() {
DsChipGroup(title = "Metrics") {
DsActionChip(label = "Chart", icon = Icons.Default.BarChart, onClick = { /* ... */ })
}
}
}
object MetricsAddon : ConsoleAddon {
override fun dockWidget(): ConsoleDockWidget = MetricsWidget
}Full-screen decoration goes through layered overlays instead, ordered and hidden together with the widget they name:
override fun overlays() = listOf(
ConsoleOverlay(layer = ConsoleOverlayLayer.Backdrop, widgetId = "metrics") { /* draws, no touches */ },
ConsoleOverlay(layer = ConsoleOverlayLayer.Capture, widgetId = "metrics") { /* may take over touches */ },
)Stacking, bottom to top: app, Backdrop, Capture, legacy overlay(), the dock, the console itself.
Build the panel from the design system's DsChipGroup and DsActionChip (a labelled wrap of icon-only buttons, tinted
when on) and the quick action from DsQuickAction.
Hiding: Settings → Floating widgets has a switch that hides every widget and overlay, and a list of widget ids
to hide individually. Widgets that need attention (a paused stepper, an active inspector pick) stay visible. The same
config is available in code: ConsoleDock.updateConfig { copy(isVisible = false) }.
Beyond tabs, ConsoleAddon also supports:
navGraph() — register a full navigation sub-graph behind your taboverlay() — inject a free-form composable over the app (prefer dockWidget() and overlays() above)One-dependency aggregators. Both exclude the design system (add
console-components yourself if you want the Ds* primitives) and the HTTP
interceptors (add the console-network-ktor / console-network-okhttp module
matching your client).
| Artifact | Description |
|---|---|
io.github.thernal:console-bundle-core:<version> |
Everything a plain JVM/domain module can depend on — console-core/io/runtime + every addon -core module (no Compose) |
io.github.thernal:console-bundle-ui:<version> |
The full console — console-bundle-core + the UI shell and every addon |
| Artifact | Description |
|---|---|
io.github.thernal:console-core:<version> |
Foundation — Log, LogLevel, LogObserver, LogProcessor, ConsoleScope (no UI, no pipeline) |
io.github.thernal:console-io:<version> |
Shared file-system infrastructure — ConsoleFileSystem, AppendSink (no UI, no pipeline) |
io.github.thernal:console-runtime:<version> |
Console singleton + log pipeline (depends on console-core) |
io.github.thernal:console-api:<version> |
Addon contracts — ConsoleAddon, ConsoleTab, LogRenderer, LogRendererRegistry (depends on console-core, not runtime) |
io.github.thernal:console-ui:<version> |
Compose UI shell — ConsoleProvider, navigation, overlay |
io.github.thernal:console-ui-noop:<version> |
No-op stub for production builds |
Each addon has its own README with setup, settings and architecture; start from the addon index.
| Artifact | Description |
|---|---|
io.github.thernal:console-logging-ui:<version> |
Log list, log detail screen, BasicLog renderer |
io.github.thernal:console-details-ui:<version> |
Live key/value Details panel |
io.github.thernal:console-details-core-noop:<version> |
No-op stub for production builds |
io.github.thernal:console-network-core:<version> |
Shared network log types |
io.github.thernal:console-network-okhttp:<version> |
OkHttp interceptor |
io.github.thernal:console-network-ktor:<version> |
Ktor plugin |
io.github.thernal:console-network-ui:<version> |
Network log UI renderer |
io.github.thernal:console-stepper-ui:<version> |
Pause-and-step log replay |
io.github.thernal:console-inspector-core:<version> |
UI inspector config and facade |
io.github.thernal:console-inspector-core-noop:<version> |
No-op stub for production builds |
io.github.thernal:console-inspector-ui:<version> |
UI inspector — tree, overlay, recomposition tracking, display overrides |
io.github.thernal:console-crash-report-core:<version> |
Crash session serialization + LogCodec registry |
io.github.thernal:console-crash-report-ui:<version> |
Crashes tab — streaming persistence, crash capture, session list/detail |
io.github.thernal:console-settings-api:<version> |
Settings registration contract — SettingsSection, entry DSL, SettingsRegistry
|
io.github.thernal:console-settings-api-noop:<version> |
No-op stub for production builds |
io.github.thernal:console-settings-ui:<version> |
Settings tab — persisted per-addon overrides, entry renderers, General section |
MIT — see LICENSE.