diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/DeviceNames.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/DeviceNames.kt new file mode 100644 index 000000000..327a4e955 --- /dev/null +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/DeviceNames.kt @@ -0,0 +1,26 @@ +package com.steipete.clawdis.node + +import android.content.Context +import android.os.Build +import android.provider.Settings + +object DeviceNames { + fun bestDefaultNodeName(context: Context): String { + val deviceName = + runCatching { + Settings.Global.getString(context.contentResolver, "device_name") + } + .getOrNull() + ?.trim() + .orEmpty() + + if (deviceName.isNotEmpty()) return deviceName + + val model = + listOfNotNull(Build.MANUFACTURER?.takeIf { it.isNotBlank() }, Build.MODEL?.takeIf { it.isNotBlank() }) + .joinToString(" ") + .trim() + + return model.ifEmpty { "Android Node" } + } +} diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/SecurePrefs.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/SecurePrefs.kt index 94f1b2fa1..eb25fa508 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/SecurePrefs.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/SecurePrefs.kt @@ -14,6 +14,7 @@ import java.util.UUID class SecurePrefs(context: Context) { companion object { val defaultWakeWords: List = listOf("clawd", "claude") + private const val displayNameKey = "node.displayName" } private val json = Json { ignoreUnknownKeys = true } @@ -35,7 +36,8 @@ class SecurePrefs(context: Context) { private val _instanceId = MutableStateFlow(loadOrCreateInstanceId()) val instanceId: StateFlow = _instanceId - private val _displayName = MutableStateFlow(prefs.getString("node.displayName", "Android Node")!!) + private val _displayName = + MutableStateFlow(loadOrMigrateDisplayName(context = context)) val displayName: StateFlow = _displayName private val _cameraEnabled = MutableStateFlow(prefs.getBoolean("camera.enabled", true)) @@ -68,7 +70,7 @@ class SecurePrefs(context: Context) { fun setDisplayName(value: String) { val trimmed = value.trim() - prefs.edit().putString("node.displayName", trimmed).apply() + prefs.edit().putString(displayNameKey, trimmed).apply() _displayName.value = trimmed } @@ -116,6 +118,17 @@ class SecurePrefs(context: Context) { return fresh } + private fun loadOrMigrateDisplayName(context: Context): String { + val existing = prefs.getString(displayNameKey, null)?.trim().orEmpty() + if (existing.isNotEmpty() && existing != "Android Node") return existing + + val candidate = DeviceNames.bestDefaultNodeName(context).trim() + val resolved = candidate.ifEmpty { "Android Node" } + + prefs.edit().putString(displayNameKey, resolved).apply() + return resolved + } + fun setWakeWords(words: List) { val sanitized = WakeWords.sanitize(words, defaultWakeWords) val encoded = diff --git a/apps/ios/Sources/Bridge/BridgeConnectionController.swift b/apps/ios/Sources/Bridge/BridgeConnectionController.swift index 4843b0be0..915fa44ea 100644 --- a/apps/ios/Sources/Bridge/BridgeConnectionController.swift +++ b/apps/ios/Sources/Bridge/BridgeConnectionController.swift @@ -126,7 +126,7 @@ final class BridgeConnectionController { private func makeHello(token: String) -> BridgeHello { let defaults = UserDefaults.standard let nodeId = defaults.string(forKey: "node.instanceId") ?? "ios-node" - let displayName = defaults.string(forKey: "node.displayName") ?? "iOS Node" + let displayName = self.resolvedDisplayName(defaults: defaults) return BridgeHello( nodeId: nodeId, @@ -139,6 +139,21 @@ final class BridgeConnectionController { caps: self.currentCaps()) } + private func resolvedDisplayName(defaults: UserDefaults) -> String { + let key = "node.displayName" + let existing = defaults.string(forKey: key)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if !existing.isEmpty, existing != "iOS Node" { return existing } + + let deviceName = UIDevice.current.name.trimmingCharacters(in: .whitespacesAndNewlines) + let candidate = deviceName.isEmpty ? "iOS Node" : deviceName + + if existing.isEmpty || existing == "iOS Node" { + defaults.set(candidate, forKey: key) + } + + return candidate + } + private func currentCaps() -> [String] { var caps = ["canvas"] diff --git a/apps/macos/Package.swift b/apps/macos/Package.swift index e5c77bca9..f3e8b1a36 100644 --- a/apps/macos/Package.swift +++ b/apps/macos/Package.swift @@ -48,6 +48,10 @@ let package = Package( .product(name: "PeekabooBridge", package: "PeekabooCore"), .product(name: "PeekabooAutomationKit", package: "PeekabooAutomationKit"), ], + exclude: [ + // Legacy web-based WebChat assets (SwiftUI-only WebChat doesn't use these). + "Resources/WebChat", + ], resources: [ .copy("Resources/Clawdis.icns"), .copy("Resources/CanvasA2UI"), diff --git a/docs/device-models.md b/docs/device-models.md new file mode 100644 index 000000000..980dd56b8 --- /dev/null +++ b/docs/device-models.md @@ -0,0 +1,40 @@ +# Device model database (friendly names) + +The macOS companion app shows friendly Apple device model names in the **Instances** UI by mapping Apple model identifiers (e.g. `iPad16,6`, `Mac16,6`) to human-readable names. + +The mapping is vendored as JSON under: + +- `apps/macos/Sources/Clawdis/Resources/DeviceModels/` + +## Data source + +We currently vendor the mapping from the MIT-licensed repository: + +- `kyle-seongwoo-jun/apple-device-identifiers` + +To keep builds deterministic, the JSON files are pinned to specific upstream commits (recorded in `apps/macos/Sources/Clawdis/Resources/DeviceModels/NOTICE.md`). + +## Updating the database + +1. Pick the upstream commits you want to pin to (one for iOS, one for macOS). +2. Update the commit hashes in `apps/macos/Sources/Clawdis/Resources/DeviceModels/NOTICE.md`. +3. Re-download the JSON files, pinned to those commits: + +```bash +IOS_COMMIT="" +MAC_COMMIT="" + +curl -fsSL "https://raw.githubusercontent.com/kyle-seongwoo-jun/apple-device-identifiers/${IOS_COMMIT}/ios-device-identifiers.json" \ + -o apps/macos/Sources/Clawdis/Resources/DeviceModels/ios-device-identifiers.json + +curl -fsSL "https://raw.githubusercontent.com/kyle-seongwoo-jun/apple-device-identifiers/${MAC_COMMIT}/mac-device-identifiers.json" \ + -o apps/macos/Sources/Clawdis/Resources/DeviceModels/mac-device-identifiers.json +``` + +4. Ensure `apps/macos/Sources/Clawdis/Resources/DeviceModels/LICENSE.apple-device-identifiers.txt` still matches upstream (replace it if the upstream license changes). +5. Verify the macOS app builds cleanly (no warnings): + +```bash +swift build --package-path apps/macos +``` +