feat: integrate Tauri v2 with Android widget and voice assistant

- Add Tauri v2 shell (Cargo, tauri.conf.json, capabilities, plugins)
- Migrate all fetch() calls to apiFetch() for Tauri-aware HTTP
- Migrate WebSocket endpoints to resolveEndpoints() for dynamic URLs
- Add ServerConfigDialog for remote server URL configuration
- Add tauri.ts lib with isTauri detection, apiFetch wrapper, plugin helpers
- Add server-config Pinia store with persistence via plugin-store
- Conditional PWA (disabled in Tauri builds)
- Android: home screen transcript widget (last 5 messages, 30s refresh)
- Android: voice command / share activity (SpeechRecognizer + WebSocket)
- Android: signed release APK with auto-copy to installers/
- Remove stale frontend/src-tauri directory
This commit is contained in:
2026-02-23 15:33:43 -06:00
parent 6dc0c5ff6f
commit e1aa8b1bdb
108 changed files with 8155 additions and 151 deletions

6235
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

23
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "agent-ui"
version = "0.1.0"
description = "Agent UI - Desktop & Mobile App"
authors = ["you"]
edition = "2021"
[lib]
name = "agent_ui_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-http = "2"
tauri-plugin-store = "2"
tauri-plugin-notification = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-utils/schema.json",
"identifier": "default",
"description": "Default permissions for Agent UI",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "http:default",
"allow": [
{ "url": "http://**" },
{ "url": "https://**" }
]
},
"store:default",
"notification:default",
"clipboard-manager:default",
"dialog:default"
]
}

View File

@@ -0,0 +1,97 @@
import java.util.Properties
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rust")
}
val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
android {
signingConfigs {
create("release") {
storeFile = file("../keystore.jks")
storePassword = "agentui123"
keyAlias = "agentui"
keyPassword = "agentui123"
}
}
compileSdk = 36
namespace = "com.agentui.desktop"
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "com.agentui.desktop"
minSdk = 24
targetSdk = 36
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
proguardFiles(
*fileTree(".") { include("**/*.pro") }
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
.toList().toTypedArray()
)
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
}
rust {
rootDirRel = "../../../../"
}
dependencies {
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.work:work-runtime-ktx:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
apply(from = "tauri.build.gradle.kts")
// Copy APK to src-tauri/installers after build
android.applicationVariants.all {
val variant = this
variant.outputs.all {
val output = this
variant.assembleProvider.get().doLast {
val src = output.outputFile
if (src.exists()) {
val dest = file("../../../../installers/AgentUI-${variant.versionName}-${variant.name}.apk")
src.copyTo(dest, overwrite = true)
println(">> Copied APK to ${dest.absolutePath}")
}
}
}
}

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- AndroidTV support -->
<uses-feature android:name="android.software.leanback" android:required="false" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.agent_ui"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:label="@string/main_activity_title"
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- AndroidTV support -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<!-- Transcript Widget -->
<receiver
android:name=".TranscriptWidgetProvider"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/transcript_widget_info" />
</receiver>
<!-- Voice Command / Share Activity -->
<activity
android:name=".VoiceCommandActivity"
android:label="Agent UI Voice"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,40 @@
package com.agentui.desktop
import android.os.Bundle
import android.util.Log
import androidx.activity.enableEdgeToEdge
import org.json.JSONObject
import java.io.File
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
syncServerUrlToPrefs()
}
override fun onResume() {
super.onResume()
syncServerUrlToPrefs()
}
/**
* Reads the Tauri plugin-store settings.json and syncs serverUrl
* to SharedPreferences so native components (widget, voice) can use it.
*/
private fun syncServerUrlToPrefs() {
try {
val storeFile = File(filesDir, "app_tauri-plugin-store/settings.json")
if (!storeFile.exists()) return
val json = JSONObject(storeFile.readText())
val serverUrl = json.optString("serverUrl", "")
if (serverUrl.isNotEmpty()) {
ServerConfig.setServerUrl(this, serverUrl)
Log.d("AgentUI", "Synced serverUrl to prefs: $serverUrl")
}
} catch (e: Exception) {
Log.w("AgentUI", "Failed to sync server URL", e)
}
}
}

View File

@@ -0,0 +1,40 @@
package com.agentui.desktop
import android.content.Context
object ServerConfig {
private const val PREFS_NAME = "agent_ui_config"
private const val KEY_SERVER_URL = "server_url"
fun getServerUrl(context: Context): String? {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getString(KEY_SERVER_URL, null)
}
fun setServerUrl(context: Context, url: String) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.putString(KEY_SERVER_URL, url.trimEnd('/'))
.apply()
}
/** e.g. "http://192.168.1.10:4103" */
fun terminalBaseUrl(context: Context): String? {
val base = getServerUrl(context) ?: return null
val uri = android.net.Uri.parse(base)
val host = uri.host ?: return null
return "${uri.scheme}://$host:4103"
}
/** e.g. "ws://192.168.1.10:4103" */
fun terminalWsUrl(context: Context): String? {
val base = terminalBaseUrl(context) ?: return null
return base.replace("http://", "ws://").replace("https://", "wss://")
}
/** e.g. "http://192.168.1.10:4100/api" */
fun apiBaseUrl(context: Context): String? {
val base = getServerUrl(context) ?: return null
return "${base.trimEnd('/')}/api"
}
}

View File

@@ -0,0 +1,42 @@
package com.agentui.desktop
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
class TranscriptWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
val views = RemoteViews(context.packageName, R.layout.widget_transcript)
// Tap widget → open main app
val intent = Intent(context, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
views.setOnClickPendingIntent(R.id.widget_root, pendingIntent)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
// Start periodic refresh worker
TranscriptWidgetWorker.enqueue(context)
}
override fun onEnabled(context: Context) {
TranscriptWidgetWorker.enqueue(context)
}
override fun onDisabled(context: Context) {
TranscriptWidgetWorker.cancel(context)
}
}

View File

@@ -0,0 +1,146 @@
package com.agentui.desktop
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.widget.RemoteViews
import androidx.work.*
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONArray
import org.json.JSONObject
import java.util.concurrent.TimeUnit
class TranscriptWidgetWorker(
private val context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
companion object {
const val WORK_NAME = "transcript_widget_refresh"
private val MSG_IDS = intArrayOf(
R.id.msg1, R.id.msg2, R.id.msg3, R.id.msg4, R.id.msg5
)
private val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()
fun enqueue(context: Context) {
val request = OneTimeWorkRequestBuilder<TranscriptWidgetWorker>()
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(WORK_NAME, ExistingWorkPolicy.REPLACE, request)
}
fun cancel(context: Context) {
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME)
}
}
override suspend fun doWork(): Result {
val messages = fetchMessages()
updateWidget(messages)
scheduleNext()
return Result.success()
}
private fun fetchMessages(): List<String> {
val apiBase = ServerConfig.apiBaseUrl(context) ?: return listOf("No server configured")
try {
// Get most recent session for 'ejecutor' agent
val sessionsUrl = "$apiBase/transcript-debug/sessions?agent=ejecutor"
val sessionsReq = Request.Builder().url(sessionsUrl).build()
val sessionsResp = client.newCall(sessionsReq).execute()
if (!sessionsResp.isSuccessful) return listOf("Failed to fetch sessions")
val sessionsJson = JSONArray(sessionsResp.body?.string() ?: "[]")
if (sessionsJson.length() == 0) return listOf("No sessions found")
// Get the most recent session (sorted by mtime desc from server)
val latestSession = sessionsJson.getJSONObject(0)
val sessionId = latestSession.getString("id")
// Fetch raw JSONL
val rawUrl = "$apiBase/transcript-debug/$sessionId/raw?agent=ejecutor"
val rawReq = Request.Builder().url(rawUrl).build()
val rawResp = client.newCall(rawReq).execute()
if (!rawResp.isSuccessful) return listOf("Failed to fetch transcript")
val rawText = rawResp.body?.string() ?: return listOf("Empty transcript")
return parseJsonlMessages(rawText)
} catch (e: Exception) {
return listOf("Error: ${e.message?.take(40)}")
}
}
private fun parseJsonlMessages(jsonl: String): List<String> {
val messages = mutableListOf<String>()
val lines = jsonl.trim().split("\n").filter { it.isNotBlank() }
for (line in lines) {
try {
val obj = JSONObject(line)
val type = obj.optString("type", "")
when (type) {
"human" -> {
val content = obj.optString("message", "")
.ifEmpty { extractMessageContent(obj) }
if (content.isNotEmpty()) {
messages.add("> ${content.take(80)}")
}
}
"assistant" -> {
val content = extractMessageContent(obj)
if (content.isNotEmpty()) {
messages.add("< ${content.take(80)}")
}
}
}
} catch (_: Exception) { }
}
return messages.takeLast(5).ifEmpty { listOf("No messages yet") }
}
private fun extractMessageContent(obj: JSONObject): String {
// Try "message" field first (simple string)
val msg = obj.optString("message", "")
if (msg.isNotEmpty()) return msg
// Try "message" as array of content blocks
val msgArray = obj.optJSONArray("message")
if (msgArray != null) {
for (i in 0 until msgArray.length()) {
val block = msgArray.optJSONObject(i) ?: continue
if (block.optString("type") == "text") {
return block.optString("text", "")
}
}
}
return ""
}
private fun updateWidget(messages: List<String>) {
val views = RemoteViews(context.packageName, R.layout.widget_transcript)
for (i in MSG_IDS.indices) {
views.setTextViewText(MSG_IDS[i], messages.getOrElse(i) { "" })
}
val appWidgetManager = AppWidgetManager.getInstance(context)
val widgetComponent = ComponentName(context, TranscriptWidgetProvider::class.java)
appWidgetManager.updateAppWidget(widgetComponent, views)
}
private fun scheduleNext() {
val request = OneTimeWorkRequestBuilder<TranscriptWidgetWorker>()
.setInitialDelay(30, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(WORK_NAME, ExistingWorkPolicy.REPLACE, request)
}
}

View File

@@ -0,0 +1,160 @@
package com.agentui.desktop
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.speech.RecognizerIntent
import android.util.Log
import android.widget.Toast
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class VoiceCommandActivity : Activity() {
companion object {
private const val TAG = "AgentUI.Voice"
private const val SPEECH_REQUEST_CODE = 1001
}
private val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Check if launched via share intent
if (intent?.action == Intent.ACTION_SEND && intent.type == "text/plain") {
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
if (!sharedText.isNullOrBlank()) {
sendToServer(sharedText)
return
}
}
// Otherwise, start speech recognition
startSpeechRecognition()
}
private fun startSpeechRecognition() {
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
putExtra(RecognizerIntent.EXTRA_PROMPT, "Agent UI - Speak your command")
}
try {
startActivityForResult(intent, SPEECH_REQUEST_CODE)
} catch (e: Exception) {
Toast.makeText(this, "Speech recognition not available", Toast.LENGTH_SHORT).show()
finish()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == SPEECH_REQUEST_CODE) {
if (resultCode == RESULT_OK && data != null) {
val results = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
val text = results?.firstOrNull()
if (!text.isNullOrBlank()) {
sendToServer(text)
return
}
}
finish()
}
}
private fun sendToServer(text: String) {
Thread {
try {
val terminalBase = ServerConfig.terminalBaseUrl(this)
val terminalWs = ServerConfig.terminalWsUrl(this)
if (terminalBase == null || terminalWs == null) {
showToastAndFinish("No server configured")
return@Thread
}
// 1. Create a new terminal session
val createBody = JSONObject().apply {
put("agent", "ejecutor")
put("transcriptSessionId", "__new__")
put("label", text.take(60))
put("command", "ejecutor")
}.toString().toRequestBody("application/json".toMediaType())
val createReq = Request.Builder()
.url("$terminalBase/create-terminal")
.post(createBody)
.build()
val createResp = client.newCall(createReq).execute()
if (!createResp.isSuccessful) {
showToastAndFinish("Failed to create terminal")
return@Thread
}
val respJson = JSONObject(createResp.body?.string() ?: "{}")
val sessionId = respJson.optString("ephemeralSessionId", "")
if (sessionId.isEmpty()) {
showToastAndFinish("No session ID returned")
return@Thread
}
// 2. Connect via WebSocket and send the input
val wsUrl = "$terminalWs/ws/terminal?session=$sessionId"
val wsReq = Request.Builder().url(wsUrl).build()
val latch = CountDownLatch(1)
client.newWebSocket(wsReq, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
// Send the text as input
val inputMsg = JSONObject().apply {
put("type", "input")
put("data", text)
}.toString()
webSocket.send(inputMsg)
// Send Enter key after a short delay
Thread.sleep(80)
val enterMsg = JSONObject().apply {
put("type", "input")
put("data", "\r")
}.toString()
webSocket.send(enterMsg)
// Close after sending
Thread.sleep(200)
webSocket.close(1000, "done")
latch.countDown()
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e(TAG, "WebSocket failed", t)
latch.countDown()
}
})
latch.await(5, TimeUnit.SECONDS)
showToastAndFinish("Sent: ${text.take(40)}")
} catch (e: Exception) {
Log.e(TAG, "Failed to send command", e)
showToastAndFinish("Error: ${e.message?.take(40)}")
}
}.start()
}
private fun showToastAndFinish(message: String) {
runOnUiThread {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
finish()
}
}
}

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="12dp"
android:background="#DD1A1A2E">
<TextView
android:id="@+id/widget_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Agent UI Transcript"
android:textColor="#8888FF"
android:textSize="12sp"
android:fontFamily="monospace"
android:paddingBottom="4dp" />
<TextView
android:id="@+id/msg1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#CCCCCC"
android:textSize="11sp"
android:fontFamily="monospace"
android:maxLines="1"
android:ellipsize="end" />
<TextView
android:id="@+id/msg2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#CCCCCC"
android:textSize="11sp"
android:fontFamily="monospace"
android:maxLines="1"
android:ellipsize="end" />
<TextView
android:id="@+id/msg3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#CCCCCC"
android:textSize="11sp"
android:fontFamily="monospace"
android:maxLines="1"
android:ellipsize="end" />
<TextView
android:id="@+id/msg4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#CCCCCC"
android:textSize="11sp"
android:fontFamily="monospace"
android:maxLines="1"
android:ellipsize="end" />
<TextView
android:id="@+id/msg5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#CCCCCC"
android:textSize="11sp"
android:fontFamily="monospace"
android:maxLines="1"
android:ellipsize="end" />
</LinearLayout>

View File

@@ -0,0 +1,5 @@
<resources>
<string name="app_name">Agent UI</string>
<string name="main_activity_title">Agent UI</string>
<string name="widget_description">Shows recent transcript messages from Agent UI</string>
</resources>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="250dp"
android:minHeight="110dp"
android:updatePeriodMillis="1800000"
android:initialLayout="@layout/widget_transcript"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:description="@string/widget_description" />

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

11
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,11 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
agent_ui_lib::run()
}

39
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,39 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "Agent UI",
"version": "0.1.0",
"identifier": "com.agentui.desktop",
"build": {
"frontendDist": "../frontend/dist",
"devUrl": "http://localhost:4100",
"beforeDevCommand": "cd frontend && npm run dev",
"beforeBuildCommand": "cd frontend && npx vite build"
},
"app": {
"windows": [
{
"title": "Agent UI",
"width": 1280,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"decorations": true,
"resizable": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}