Skip to content

feat(linux): Linux audio support, plugin SPI, and dictation engine#1

Merged
tstapler merged 12 commits into
mainfrom
feature/linux-dictation-plugin
May 11, 2026
Merged

feat(linux): Linux audio support, plugin SPI, and dictation engine#1
tstapler merged 12 commits into
mainfrom
feature/linux-dictation-plugin

Conversation

@tstapler
Copy link
Copy Markdown
Owner

@tstapler tstapler commented May 9, 2026

Summary

  • Linux parity: SystemAudioBackend interface abstracts macOS ScreenCaptureKit and Linux PipeWire behind a single contract. RecordingSessionManager is refactored to accept the backend via constructor injection — macOS code is completely unchanged.
  • Plugin SPI: SpeechOutputPlugin interface + PluginLoader using java.util.ServiceLoader with child-first URLClassLoader isolation. Drop a JAR in ~/.config/agrapha/plugins/ and it appears in Settings.
  • Dictation plugin: Built-in DictationPlugin implementing push-to-talk (in-window focus, Wayland portal follow-up in ADR-003), file transcription, and live captions via a StateFlow<List<String>>.

What's in this PR

New interfaces / abstractions

File What
platform/PlatformInfo.kt Testable OS detection (isLinux, isMac, isWayland, isPipeWireAvailable, avx2Supported)
audio/SystemAudioBackend.kt Platform-neutral audio capture interface + NoOpSystemAudioBackend
audio/ScreenCaptureBackend.kt Thin wrapper delegating to existing ScreenCaptureJniBridge (macOS)
audio/PipeWireCaptureBackend.kt Linux backend loading libpipewire-jni.so from classpath
audio/SystemAudioBackendFactory.kt Selects backend at runtime
plugin/SpeechOutputPlugin.kt (commonMain) SPI interface
plugin/DictationMode.kt (commonMain) PUSH_TO_TALK / FILE_TRANSCRIPTION / LIVE_CAPTIONS
plugin/PluginLoader.kt ServiceLoader + URLClassLoader with explicit close-on-unload
dictation/TextInjector.kt Abstraction for ydotool / xdotool text injection
dictation/YdotoolTextInjector.kt ydotool with 3-state health check, injectable ProcessBuilderFactory
dictation/XdotoolTextInjector.kt xdotool with Wayland guard
dictation/AutoDetectTextInjector.kt Lazy auto-detection: ydotool → xdotool → exception
dictation/plugin/DictationPlugin.kt Full SpeechOutputPlugin implementation

Modified files

  • audio/RecordingSessionManager.kt — constructor injection of SystemAudioBackend (default = factory); ScreenCaptureJniBridge direct calls replaced
  • transcription/WhisperService.kt — AVX2 guard before loading native library on Linux
  • domain/model/AppSettings.ktenabledPlugins: Map<String, Boolean> (default-safe)
  • ui/settings/SettingsScreen.kt — Plugins section

Native

  • native/PipeWireCaptureBridge/ — compilable C stub with pthread ring buffer and all four JNI function signatures. Full pw_stream implementation documented via TODO. nativeIsAvailable() returns JNI_FALSE until implementation is complete, so Kotlin falls back to NoOpSystemAudioBackend gracefully.
  • composeApp/build.gradle.ktsbuildPipeWireCaptureBridge Exec task (Linux-only)

Test plan

  • ./gradlew :composeApp:desktopTest184 tests, 0 failures
  • macOS regression check — ScreenCaptureJniBridge untouched; factory routes macOS to ScreenCaptureBackend identically to prior behaviour
  • Manual: Ubuntu 22.04 + PipeWire — run app, verify mic recording + Whisper transcription (system audio falls back to silence until PipeWire C implementation is complete)
  • Manual: Linux + ydotool — push-to-talk dictation button injects transcribed text into focused window
  • Manual: Plugin JAR drop-in — copy JAR to ~/.config/agrapha/plugins/, verify it appears in Settings

Known limitations / follow-ups

  • PipeWire system audio: the C bridge is a compilable stub. Full pw_stream capture implementation is the natural next PR — the architecture is in place, JNI signatures are correct, and the TODO comments in PipeWireCaptureBridgeJNI.c document exactly what to do.
  • Wayland global hotkey: push-to-talk is in-window-focus only. Full xdg-desktop-portal GlobalShortcuts integration (requires dbus-java) is tracked in ADR-003 and planned for a follow-up.
  • CI Linux runner: GitHub Actions YAML should add a Linux job for ./gradlew :composeApp:desktopTest (all tests pass without PipeWire hardware).

🤖 Generated with Claude Code

Delivers three parallel capabilities: Linux parity for meeting recording,
a ServiceLoader-based SpeechOutputPlugin API for extensible STT output
modes, and a built-in DictationPlugin implementing push-to-talk, file
transcription, and live captions.

**Linux audio backend**
- Extract SystemAudioBackend interface; ScreenCaptureBackend wraps the
  existing ScreenCaptureJniBridge (macOS unchanged)
- PipeWireCaptureBackend + compilable C stub (native/PipeWireCaptureBridge)
  with pthread ring buffer and correct JNI signatures; full pw_stream
  implementation documented via TODO comments
- SystemAudioBackendFactory selects backend at runtime via PlatformInfo
- RecordingSessionManager now accepts SystemAudioBackend via constructor
  injection (default = factory); all existing macOS behaviour preserved
- Gradle buildPipeWireCaptureBridge Exec task (Linux-only, wired to
  desktopProcessResources)

**Platform utilities**
- PlatformInfo / Platform: testable OS detection (isLinux, isMac,
  isWayland, isPipeWireAvailable, avx2Supported)
- AVX2 guard in WhisperService.loadLibraryOnce() for Linux CPU backend

**Plugin SPI**
- SpeechOutputPlugin interface + DictationMode enum in commonMain
- PluginLoader: child-first URLClassLoader per JAR, isolated error
  handling, explicit close() on unload
- AppSettings.enabledPlugins: Map<String, Boolean> (default-safe
  serialization for existing settings files)
- Settings UI Plugins section with enable/disable toggle

**TextInjector abstraction**
- TextInjector interface with NOT_INSTALLED / DAEMON_NOT_RUNNING / OK
  status enum
- YdotoolTextInjector: ydotoold health-check, ProcessBuilder injection
  (injectable for testing), text sanitization
- XdotoolTextInjector: DISPLAY/WAYLAND_DISPLAY env guard, same pattern
- AutoDetectTextInjector: lazy ydotool-first selection with xdotool fallback

**Dictation plugin**
- DictationPlugin implements SpeechOutputPlugin with all three modes
- PUSH_TO_TALK: in-window-focus MVP (Wayland portal follow-up in ADR-003)
- FILE_TRANSCRIPTION: inputPath → WhisperService → outputPath or stdout
- LIVE_CAPTIONS: 3s mic chunks → streaming StateFlow<List<String>>
- Registered via META-INF/services for zero-config ServiceLoader discovery

184 tests pass; 0 macOS regressions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 9, 2026 20:05
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Linux-oriented infrastructure for system-audio capture, introduces a ServiceLoader-based plugin SPI with Settings UI integration, and ships an initial built-in dictation plugin plus related platform/tooling utilities and tests.

Changes:

  • Introduces SystemAudioBackend abstraction with macOS adapter + Linux PipeWire backend scaffolding, wired into RecordingSessionManager.
  • Adds plugin SPI (SpeechOutputPlugin, DictationMode), a PluginLoader, Settings UI for plugin management, and ServiceLoader registration for the built-in DictationPlugin.
  • Adds Linux dictation text injection backends (ydotool/xdotool + auto-detect) and a Linux AVX2 preflight guard for whisper-jni loading, plus extensive tests and build tooling for PipeWire JNI.

Reviewed changes

Copilot reviewed 42 out of 43 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
project_plans/linux-dictation-plugin/research/stack.md Research notes for whisper-jni native coverage and PipeWire/JNI build details.
project_plans/linux-dictation-plugin/research/pitfalls.md Research notes on PipeWire permissions, ydotool pitfalls, Wayland hotkeys, and classloader leak risks.
project_plans/linux-dictation-plugin/research/features.md Research notes on injection tools, hotkeys, ServiceLoader usage, and Linux Compose behavior.
project_plans/linux-dictation-plugin/research/architecture.md Proposed architecture tying together platform detection, JNI patterns, and plugin loader structure.
project_plans/linux-dictation-plugin/requirements.md Requirements/spec for Linux parity + plugin SPI + dictation modes.
project_plans/linux-dictation-plugin/implementation/validation.md Test plan mapping requirements to unit/integration/acceptance tests.
project_plans/linux-dictation-plugin/implementation/plan.md Implementation plan/epics for Linux audio + plugins + dictation.
native/PipeWireCaptureBridge/Makefile Build script to produce and place libpipewire-jni.so into desktop resources.
native/PipeWireCaptureBridge/jni/PipeWireCaptureBridgeJNI.h JNI header declaring PipeWire bridge native functions.
native/PipeWireCaptureBridge/jni/PipeWireCaptureBridgeJNI.c Compilable PipeWire JNI stub with ring buffer and placeholder availability/capture APIs.
composeApp/src/desktopTest/kotlin/plugin/ServiceLoaderRegistrationTest.kt Tests that built-in dictation plugin is discoverable via ServiceLoader.
composeApp/src/desktopTest/kotlin/plugin/PluginLoaderTest.kt Tests for PluginLoader behavior (empty dir, unload no-op, classpath discovery).
composeApp/src/desktopTest/kotlin/plugin/dictation/DictationPluginTest.kt Unit tests for DictationPlugin identity and basic activation behavior.
composeApp/src/desktopTest/kotlin/platform/PlatformInfoTest.kt Unit tests for platform detection helpers and guards.
composeApp/src/desktopTest/kotlin/injection/XdotoolTextInjectorTest.kt Unit tests for xdotool injector availability/sanitization behavior.
composeApp/src/desktopTest/kotlin/injection/AutoDetectTextInjectorTest.kt Unit tests for injector auto-detection and caching logic.
composeApp/src/desktopTest/kotlin/audio/SystemAudioBackendFactoryTest.kt Unit tests for backend selection by platform.
composeApp/src/desktopTest/kotlin/audio/SilentAudioBackendTest.kt Unit tests for no-op system audio backend behavior.
composeApp/src/desktopTest/kotlin/audio/RecordingSessionManagerBackendTest.kt Tests verifying RecordingSessionManager uses injected backend and produces WAVs.
composeApp/src/desktopTest/kotlin/audio/PipeWireCaptureBackendTest.kt Tests verifying PipeWire backend reports unavailable gracefully in CI/macOS.
composeApp/src/desktopMain/resources/META-INF/services/com.meetingnotes.plugin.SpeechOutputPlugin Registers the built-in DictationPlugin for ServiceLoader discovery.
composeApp/src/desktopMain/kotlin/ui/settings/SettingsScreen.kt Adds Plugins section wiring to Settings screen.
composeApp/src/desktopMain/kotlin/ui/settings/PluginsSettingsSection.kt New composable rendering plugin successes/failures and enable toggles.
composeApp/src/desktopMain/kotlin/transcription/WhisperService.kt Adds Linux AVX2 preflight guard before loading whisper-jni CPU backend.
composeApp/src/desktopMain/kotlin/plugin/PluginLoader.kt Implements JAR directory scanning + ServiceLoader plugin loading with classloader isolation and unload.
composeApp/src/desktopMain/kotlin/platform/PlatformInfo.kt Adds testable platform detection utility (Linux/mac/Wayland/X11/PipeWire/AVX2).
composeApp/src/desktopMain/kotlin/dictation/YdotoolTextInjector.kt Implements ydotool-based text injection with status checks and injectable subprocess factory.
composeApp/src/desktopMain/kotlin/dictation/XdotoolTextInjector.kt Implements xdotool-based injection with Wayland guard and subprocess checks.
composeApp/src/desktopMain/kotlin/dictation/TextInjector.kt Defines TextInjector interface + availability/status model + exception type.
composeApp/src/desktopMain/kotlin/dictation/plugin/DictationPlugin.kt Built-in SpeechOutputPlugin implementing PUSH_TO_TALK (MVP), FILE_TRANSCRIPTION, LIVE_CAPTIONS.
composeApp/src/desktopMain/kotlin/dictation/AutoDetectTextInjector.kt Auto-selects the first available injector and caches selection.
composeApp/src/desktopMain/kotlin/audio/SystemAudioBackendFactory.kt Selects ScreenCapture vs PipeWire vs no-op system audio backend by platform.
composeApp/src/desktopMain/kotlin/audio/SystemAudioBackend.kt Defines SystemAudioBackend and a no-op fallback implementation.
composeApp/src/desktopMain/kotlin/audio/ScreenCaptureBackend.kt macOS adapter delegating to existing ScreenCaptureJniBridge.
composeApp/src/desktopMain/kotlin/audio/RecordingSessionManager.kt Refactors to use injected SystemAudioBackend instead of direct JNI bridge calls.
composeApp/src/desktopMain/kotlin/audio/PipeWireCaptureBackend.kt PipeWire JNI loader + backend wrapper (resource extraction + native calls).
composeApp/src/commonTest/kotlin/domain/plugin/DictationModeTest.kt Tests DictationMode JSON serialization round-trip.
composeApp/src/commonTest/kotlin/domain/AppSettingsTest.kt Tests AppSettings enabledPlugins serialization/migration defaults.
composeApp/src/commonMain/kotlin/plugin/SpeechOutputPlugin.kt Defines SPI interface and PluginException for plugins (commonMain).
composeApp/src/commonMain/kotlin/plugin/DictationMode.kt Defines serializable DictationMode enum (commonMain).
composeApp/src/commonMain/kotlin/domain/model/AppSettings.kt Adds enabledPlugins: Map<String, Boolean> to persisted settings.
composeApp/build.gradle.kts Adds Linux-only Gradle Exec tasks to build/clean PipeWire JNI library and wires into resources processing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +316 to +322
// On Linux, verify AVX2 support required by whisper-jni's CPU backend.
if (PlatformInfo.isLinux() && !PlatformInfo.avx2Supported()) {
throw UnsatisfiedLinkError(
"Whisper CPU backend requires AVX2 (Intel Haswell 2013+ or AMD Ryzen). " +
"Check /proc/cpuinfo for 'avx2' flag."
)
}
Comment on lines +25 to +32
fun avx2Supported(): Boolean {
if (!isLinux()) return true // assume capable on non-Linux
return try {
java.io.File("/proc/cpuinfo").readText().contains("avx2")
} catch (_: Exception) {
true
}
}
Comment on lines +12 to +17
* 3. [deactivate] — called when the session ends (may be called multiple times)
* 4. [close] — called once before the plugin's classloader is closed; do cleanup here
*
* Plugin authors must ensure that [activate] and [deactivate] are safe to call from
* a coroutine context, and that [close] does not block the calling thread for more
* than a few hundred milliseconds.
Comment on lines +41 to +44
* A single JAR may contain multiple plugins (each [SpeechOutputPlugin] entry in
* its META-INF/services file produces a separate result entry).
*/
fun loadAll(pluginDir: File): List<PluginLoadResult> {
Comment on lines +90 to +95

for (plugin in pluginsInJar) {
loadedPlugins[plugin.id] = Pair(plugin, loader)
results += PluginLoadResult.Success(plugin, jarPath)
}

Comment on lines +58 to +66
/* PipeWire socket check */
const char* xdg_runtime = getenv("XDG_RUNTIME_DIR");
if (!xdg_runtime) return JNI_FALSE;

char path[512];
snprintf(path, sizeof(path), "%s/pipewire-0", xdg_runtime);
FILE* sock = fopen(path, "r");
if (!sock) return JNI_FALSE;
fclose(sock);
Comment on lines 1 to 3
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.gradle.internal.os.OperatingSystem

Comment on lines +55 to +73
// Child-first URLClassLoader: plugin classes override host-app versions.
val loader = object : URLClassLoader(
arrayOf(jar.toURI().toURL()),
parentClassLoader
) {
override fun loadClass(name: String, resolve: Boolean): Class<*> {
// Try loading from this JAR first (child-first delegation).
synchronized(getClassLoadingLock(name)) {
var c = findLoadedClass(name)
if (c == null) {
c = try { findClass(name) } catch (_: ClassNotFoundException) { null }
}
if (c == null) {
c = parent.loadClass(name)
}
if (resolve) resolveClass(c)
return c
}
}
tstapler and others added 11 commits May 9, 2026 13:44
- Replace C PipeWire stub with full Rust crate (agrapha-native):
  pipewire 0.9 Box API (MainLoopBox/ContextBox/StreamBox), ring buffer,
  x11rb X11 grab, zbus Wayland portal GlobalShortcuts
- Add HotkeyService with injectable HotkeyBridge for test isolation;
  listen() polls in 1s windows so coroutine cancellation is prompt
- Wire PUSH_TO_TALK mode in DictationPlugin to the real hotkey listener
- Fix HotkeyServiceTest timing: runTest→runBlocking for IO-dispatcher tests
- Drop legacy native/PipeWireCaptureBridge C directory

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pture-kit)

The three-layer Swift → Obj-C → JNI stack is replaced by the single
agrapha-native Rust crate, which now covers both platforms:

  macOS  – mac_audio_capture.rs wraps ScreenCaptureKit via objc2 0.6 /
            objc2-screen-capture-kit 0.3; AudioDelegate implements
            SCStreamOutput + SCStreamDelegate using define_class!;
            async Obj-C completion handlers are synchronised with Condvar.
  Linux  – pipewire_capture.rs + global_shortcut.rs (unchanged).

All Linux modules are now cfg-gated so macOS builds never pull in PipeWire
or x11rb. Conversely, macOS deps are target-gated and never affect Linux.

Gradle build task now runs on macOS too, producing libagrapha_native.dylib.
ScreenCaptureJniBridge loads the single dylib instead of the old two-dylib
(@loader_path rpath) pair. native/AudioCaptureBridge/ is deleted entirely.

Compiler story before: swift build + clang + cargo build
Compiler story after:  cargo build (Xcode CLTools still needed for Apple SDK)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Captures the full implementation state of feature/linux-dictation-plugin:
21 of 22 stories complete (all Kotlin + Rust implementation done, all 194
tests passing). The single remaining story — the Linux CI job (Story 1.3)
— is the PR #1 merge gate; its YAML is specified in docs/tasks/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…S CI cleanup

- Add build-linux job to .github/workflows/build.yml running ubuntu-latest:
  installs Rust, PipeWire/X11 dev headers, and xvfb; runs desktopTest under
  xvfb-run so AWT-dependent tests pass headlessly
- Fix macOS build job: add Rust toolchain + Cargo cache (required now that
  buildAgraphaNative runs cargo on macOS); remove stale AudioCaptureBridge
  step whose directory was deleted in the Swift→Rust migration
- Add LiveCaptionsOverlay.kt: always-on-top undecorated frameless Compose
  Window that observes DictationPlugin.liveSegments and auto-shows/hides
  based on flow content; non-focusable so it never steals keyboard focus
- Wire DictationPlugin into Main.kt and AppRoot.kt so the overlay is live
  whenever LIVE_CAPTIONS mode populates liveSegments

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… done

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…add integration tests

WhisperService.isRepetitionLoop(): the verbatim-repeat branch checked
normalized.startsWith(half) where half = normalized.substring(0, len/2) —
trivially true for any string, so every sentence ≥ 20 chars was classified
as a repetition loop and filtered out. Fix to normalized.endsWith(half) so
only strings where the second half duplicates the first are rejected.

Exploratory testing on a 19-second recording revealed the bug: Whisper
transcribed both sentences correctly (verified via WhisperService logs) but
filteredRepeat=2 silenced them both.

Also fix three bugs found during Linux exploratory run:
- ScreenCaptureJniBridge.load(): always extracted libagrapha_native.dylib
  even on Linux where the classpath resource is libagrapha_native.so — now
  detects OS and picks the correct filename
- OnboardingScreen: nativeCheckPermission() UnsatisfiedLinkError (Linux) was
  caught but returned step=0, showing the macOS permission screen; now returns
  step=1 to skip that step on non-macOS platforms
- OnboardingScreen: nativeRequestPermission() button caught Exception but
  UnsatisfiedLinkError is an Error — changed catch to Throwable

Add WhisperServiceIntegrationTest: 4 tests that skip gracefully when no
model is present (Assume.assumeNotNull) and activate automatically on dev
machines. Budget scales with model size so tiny (~75 MB) gets a 15s ceiling
and distil-large-v3 (~1.5 GB) gets 180s, verified on ggml-distil-large-v3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…il-v3.5 model

Implements three parallel improvements to the transcription stack:

**Backend abstraction**
- New TranscriptionBackend interface (whisper / apple-speech / parakeet)
- WhisperTranscriptionBackend wraps existing WhisperService
- AppleSpeechBackend + Rust mac_speech_recognizer.rs for macOS on-device SFSpeechRecognizer
- TranscriptionBackendFactory selects backend from AppSettings.transcriptionBackend
- DictationPlugin now depends on TranscriptionBackend? instead of WhisperService?

**Parakeet-TDT-0.6B ONNX backend (experimental)**
- MelSpectrogramExtractor: pure-Kotlin Cooley-Tukey FFT + 128-band log-mel
- ParakeetOnnxBackend: ONNX Runtime 1.20.0, dynamic tensor name discovery, greedy CTC decode
- AppSettings.parakeetModelDir for user-configured model directory
- SettingsViewModel validates encoder.onnx presence when backend is selected
- SettingsScreen shows model dir field with inline error display

**Model catalog (Stream A)**
- WhisperModelSpec.sha256 is now nullable (skip integrity check when hash not published)
- WhisperModelSpec.sizeBytes=0 falls back to HTTP Content-Length for progress
- Adds ggml-distil-large-v3.5 (6x faster than large-v3, successor to distil-large-v3)

**Bug fix**
- SettingsViewModel.expandPath() expands ~ and $VAR in wiki path validation
- Tests: expandPath() coverage added to SettingsViewModelTest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Test

job.cancel() is non-blocking — the IO thread may still be inside waitOnce()
writing to waitCalls when the forEach runs. cancelAndJoin() ensures the
coroutine has fully completed before we inspect the list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bjc2 0.6

- Rename SCShareableContent method to getShareableContentExcludingDesktopWindows...
  (API changed in objc2-screen-capture-kit 0.3.2)
- Add #[thread_kind = AnyThread] to define_class! to satisfy NSObjectProtocol
  bounds on SCStreamOutput/SCStreamDelegate impls
- Add unsafe impl Send for CaptureState to allow use in static Mutex
- Fix NSArray::objectAtIndex now returns Retained<SCDisplay> directly (not Option)
- Replace DispatchQueue::global (removed in dispatch2 0.3) with None queue arg
- Replace CMSampleBuffer::dataBuffer() (removed) with CMSampleBufferGetDataBuffer C FFI
- Replace deprecated msg_send_id! with msg_send! in mac_speech_recognizer
- Replace Retained::as_ptr() (removed in objc2 0.6) with &* dereference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add unsafe impl NSObjectProtocol for AudioDelegate (no longer blanket-impl'd
  for define_class! types in objc2 0.6; must be explicit)
- Fix request_permission completion: *mut c_void -> *mut SCShareableContent
  to match getShareableContentExcluding... block signature
- Fix setSampleRate: takes NSInteger (isize) not f64 in sck 0.3.2
- Fix mac_speech_recognizer: use *mut AnyObject raw returns from msg_send!
  instead of Retained<AnyObject> (Retained<AnyObject> does not impl Encode)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add release-please-action v4 triggered on main branch merges
- release-please-config.json: simple release type, updates packageVersion
  in composeApp/build.gradle.kts via x-release-please-version marker
- .release-please-manifest.json: seed at 1.0.0
- Fix release.yml: remove dead AudioCaptureBridge step (replaced by the
  Rust native crate in this PR) and add Rust toolchain + Cargo cache so
  the release DMG build includes libagrapha_native.dylib

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tstapler tstapler merged commit fef6348 into main May 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants