mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
refactor: remove proven dead code across runtime surfaces (#100823)
* refactor: remove unused internal helpers * refactor(ui): remove unused compatibility helpers * refactor(android): remove unused talk cleanup shim * refactor(android): remove orphaned legacy UI * refactor: remove unused private exports * refactor(docs-i18n): remove dead matcher and satisfy lint
This commit is contained in:
parent
704c584686
commit
17777b3a9f
35 changed files with 22 additions and 1087 deletions
|
|
@ -147,7 +147,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-state-text",
|
||||
"line": 193,
|
||||
"line": 192,
|
||||
"path": "apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt",
|
||||
"source": "Mic off",
|
||||
"surface": "android",
|
||||
|
|
@ -155,7 +155,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-state-text",
|
||||
"line": 203,
|
||||
"line": 202,
|
||||
"path": "apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt",
|
||||
"source": "Off",
|
||||
"surface": "android",
|
||||
|
|
@ -195,7 +195,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-state-text",
|
||||
"line": 578,
|
||||
"line": 577,
|
||||
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
|
||||
"source": "Offline",
|
||||
"surface": "android",
|
||||
|
|
@ -203,7 +203,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 2457,
|
||||
"line": 2447,
|
||||
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
|
||||
"source": "Failed: this host requires wss:// or Tailscale Serve. No TLS endpoint detected.",
|
||||
"surface": "android",
|
||||
|
|
@ -211,7 +211,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 2459,
|
||||
"line": 2449,
|
||||
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
|
||||
"source": "Failed: secure endpoint reached, but TLS fingerprint verification timed out. Check Tailscale Serve or gateway TLS and retry.",
|
||||
"surface": "android",
|
||||
|
|
@ -219,7 +219,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 2461,
|
||||
"line": 2451,
|
||||
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
|
||||
"source": "Failed: couldn't reach the secure gateway endpoint for this host.",
|
||||
"surface": "android",
|
||||
|
|
@ -2345,14 +2345,6 @@
|
|||
"surface": "android",
|
||||
"id": "native.android.cf8344d3562e28e3"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 271,
|
||||
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/PostOnboardingTabs.kt",
|
||||
"source": "OpenClaw",
|
||||
"surface": "android",
|
||||
"id": "native.android.561d07951dc36959"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 86,
|
||||
|
|
@ -5657,14 +5649,6 @@
|
|||
"surface": "android",
|
||||
"id": "native.android.994ac738d819f2e6"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 96,
|
||||
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawSurfaces.kt",
|
||||
"source": "Needs attention",
|
||||
"surface": "android",
|
||||
"id": "native.android.75e509075f59deaa"
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 221,
|
||||
|
|
|
|||
|
|
@ -167,7 +167,6 @@ class MainViewModel(
|
|||
val mainSessionKey: StateFlow<String> = runtimeState(initial = "main") { it.mainSessionKey }
|
||||
|
||||
val cameraHud: StateFlow<CameraHudState?> = runtimeState(initial = null) { it.cameraHud }
|
||||
val cameraFlashToken: StateFlow<Long> = runtimeState(initial = 0L) { it.cameraFlashToken }
|
||||
|
||||
val instanceId: StateFlow<String> = prefs.instanceId
|
||||
val displayName: StateFlow<String> = prefs.displayName
|
||||
|
|
|
|||
|
|
@ -405,7 +405,6 @@ class NodeRuntime private constructor(
|
|||
camera = camera,
|
||||
externalAudioCaptureActive = externalAudioCaptureActive,
|
||||
showCameraHud = ::showCameraHud,
|
||||
triggerCameraFlash = ::triggerCameraFlash,
|
||||
invokeErrorFromThrowable = { invokeErrorFromThrowable(it) },
|
||||
)
|
||||
|
||||
|
|
@ -599,9 +598,6 @@ class NodeRuntime private constructor(
|
|||
private val _cameraHud = MutableStateFlow<CameraHudState?>(null)
|
||||
val cameraHud: StateFlow<CameraHudState?> = _cameraHud.asStateFlow()
|
||||
|
||||
private val _cameraFlashToken = MutableStateFlow(0L)
|
||||
val cameraFlashToken: StateFlow<Long> = _cameraFlashToken.asStateFlow()
|
||||
|
||||
private val _canvasA2uiHydrated = MutableStateFlow(false)
|
||||
val canvasA2uiHydrated: StateFlow<Boolean> = _canvasA2uiHydrated.asStateFlow()
|
||||
private val _canvasRehydratePending = MutableStateFlow(false)
|
||||
|
|
@ -1944,12 +1940,6 @@ class NodeRuntime private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun finishTalkCaptureIfIdle(ownershipEpoch: Long) {
|
||||
synchronized(voiceCaptureOwnershipLock) {
|
||||
finishTalkCaptureIfIdleUnderOwnershipLock(ownershipEpoch)
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishTalkCaptureIfIdleUnderOwnershipLock(ownershipEpoch: Long) {
|
||||
if (ownershipEpoch == 0L || voiceCaptureOwnershipEpoch.get() != ownershipEpoch) return
|
||||
if (!talkMode.isEnabled.value && !talkMode.isListening.value && !talkMode.isSpeaking.value) {
|
||||
|
|
@ -3856,11 +3846,6 @@ class NodeRuntime private constructor(
|
|||
return trimmed.ifEmpty { null }
|
||||
}
|
||||
|
||||
private fun triggerCameraFlash() {
|
||||
// Token is used as a pulse trigger; value doesn't matter as long as it changes.
|
||||
_cameraFlashToken.value = SystemClock.elapsedRealtimeNanos()
|
||||
}
|
||||
|
||||
private fun showCameraHud(
|
||||
message: String,
|
||||
kind: CameraHudKind,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ class CameraHandler(
|
|||
private val camera: CameraCaptureManager,
|
||||
private val externalAudioCaptureActive: MutableStateFlow<Boolean>,
|
||||
private val showCameraHud: (message: String, kind: CameraHudKind, autoHideMs: Long?) -> Unit,
|
||||
private val triggerCameraFlash: () -> Unit,
|
||||
private val invokeErrorFromThrowable: (err: Throwable) -> Pair<String, String>,
|
||||
) {
|
||||
/** Handles camera.list by exposing CameraX devices through gateway metadata. */
|
||||
|
|
@ -76,8 +75,6 @@ class CameraHandler(
|
|||
camLog("starting, params=$paramsJson")
|
||||
camLog("calling showCameraHud")
|
||||
showCameraHud("Taking photo…", CameraHudKind.Photo, null)
|
||||
camLog("calling triggerCameraFlash")
|
||||
triggerCameraFlash()
|
||||
val res =
|
||||
try {
|
||||
camLog("calling camera.snap()")
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
package ai.openclaw.app.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/** Full-screen white flash keyed by camera capture tokens. */
|
||||
@Composable
|
||||
fun CameraFlashOverlay(
|
||||
token: Long,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
CameraFlash(token = token)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CameraFlash(token: Long) {
|
||||
var alpha by remember { mutableFloatStateOf(0f) }
|
||||
LaunchedEffect(token) {
|
||||
if (token == 0L) return@LaunchedEffect
|
||||
// Token changes replay the animation even when consecutive captures use
|
||||
// the same HUD message.
|
||||
alpha = 0.85f
|
||||
delay(110)
|
||||
alpha = 0f
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(alpha)
|
||||
.background(Color.White),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,364 +0,0 @@
|
|||
package ai.openclaw.app.ui
|
||||
|
||||
import ai.openclaw.app.HomeDestination
|
||||
import ai.openclaw.app.MainViewModel
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ScreenShare
|
||||
import androidx.compose.material.icons.filled.ChatBubble
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.RecordVoiceOver
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
|
||||
private enum class HomeTab(
|
||||
val label: String,
|
||||
val icon: ImageVector,
|
||||
) {
|
||||
Connect(label = "Connect", icon = Icons.Default.CheckCircle),
|
||||
Chat(label = "Chat", icon = Icons.Default.ChatBubble),
|
||||
Voice(label = "Voice", icon = Icons.Default.RecordVoiceOver),
|
||||
Screen(label = "Screen", icon = Icons.AutoMirrored.Filled.ScreenShare),
|
||||
Settings(label = "Settings", icon = Icons.Default.Settings),
|
||||
}
|
||||
|
||||
private enum class StatusVisual {
|
||||
Connected,
|
||||
Connecting,
|
||||
Warning,
|
||||
Error,
|
||||
Offline,
|
||||
}
|
||||
|
||||
/** Legacy tab scaffold used by the mobile post-onboarding experience. */
|
||||
@Composable
|
||||
fun PostOnboardingTabs(
|
||||
viewModel: MainViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var activeTab by rememberSaveable { mutableStateOf(HomeTab.Connect) }
|
||||
var chatTabStarted by rememberSaveable { mutableStateOf(false) }
|
||||
var screenTabStarted by rememberSaveable { mutableStateOf(false) }
|
||||
val requestedHomeDestination by viewModel.requestedHomeDestination.collectAsState()
|
||||
|
||||
LaunchedEffect(requestedHomeDestination) {
|
||||
val destination = requestedHomeDestination ?: return@LaunchedEffect
|
||||
activeTab =
|
||||
when (destination) {
|
||||
HomeDestination.Connect -> HomeTab.Connect
|
||||
HomeDestination.Chat -> HomeTab.Chat
|
||||
HomeDestination.Voice -> HomeTab.Voice
|
||||
HomeDestination.Screen -> HomeTab.Screen
|
||||
HomeDestination.Settings -> HomeTab.Settings
|
||||
}
|
||||
viewModel.clearRequestedHomeDestination()
|
||||
}
|
||||
|
||||
// Stop TTS when user navigates away from voice tab, and lazily keep the Chat/Screen tabs
|
||||
// alive after the first visit so repeated tab switches do not rebuild their UI trees.
|
||||
LaunchedEffect(activeTab) {
|
||||
viewModel.setVoiceScreenActive(activeTab == HomeTab.Voice)
|
||||
if (activeTab == HomeTab.Chat) {
|
||||
chatTabStarted = true
|
||||
}
|
||||
if (activeTab == HomeTab.Screen) {
|
||||
screenTabStarted = true
|
||||
}
|
||||
}
|
||||
|
||||
val gatewayConnectionDisplay by viewModel.gatewayConnectionDisplay.collectAsState()
|
||||
|
||||
val statusVisual =
|
||||
remember(gatewayConnectionDisplay) {
|
||||
val lower = gatewayConnectionDisplay.statusText.lowercase()
|
||||
when {
|
||||
gatewayConnectionDisplay.isConnected -> StatusVisual.Connected
|
||||
lower.contains("connecting") || lower.contains("reconnecting") -> StatusVisual.Connecting
|
||||
lower.contains("pairing") || lower.contains("approval") || lower.contains("auth") -> StatusVisual.Warning
|
||||
lower.contains("error") || lower.contains("failed") -> StatusVisual.Error
|
||||
else -> StatusVisual.Offline
|
||||
}
|
||||
}
|
||||
|
||||
val density = LocalDensity.current
|
||||
val imeVisible = WindowInsets.ime.getBottom(density) > 0
|
||||
val hideBottomTabBar = activeTab == HomeTab.Chat && imeVisible
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = Color.Transparent,
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||
topBar = {
|
||||
TopStatusBar(
|
||||
statusText = gatewayConnectionDisplay.statusText,
|
||||
statusVisual = statusVisual,
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
if (!hideBottomTabBar) {
|
||||
BottomTabBar(
|
||||
activeTab = activeTab,
|
||||
onSelect = { activeTab = it },
|
||||
)
|
||||
}
|
||||
},
|
||||
) { innerPadding ->
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.consumeWindowInsets(innerPadding)
|
||||
.background(mobileBackgroundGradient),
|
||||
) {
|
||||
if (chatTabStarted) {
|
||||
// Keep chat mounted after first use so session state and scroll position
|
||||
// survive tab switches.
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.alpha(if (activeTab == HomeTab.Chat) 1f else 0f)
|
||||
.zIndex(if (activeTab == HomeTab.Chat) 1f else 0f),
|
||||
) {
|
||||
ChatSheet(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
if (screenTabStarted) {
|
||||
// Canvas can be expensive to initialize; keep it mounted once visited
|
||||
// and hide it by alpha/z-order instead of destroying the view tree.
|
||||
ScreenTabScreen(
|
||||
viewModel = viewModel,
|
||||
visible = activeTab == HomeTab.Screen,
|
||||
modifier =
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.alpha(if (activeTab == HomeTab.Screen) 1f else 0f)
|
||||
.zIndex(if (activeTab == HomeTab.Screen) 1f else 0f),
|
||||
)
|
||||
}
|
||||
|
||||
when (activeTab) {
|
||||
HomeTab.Connect -> ConnectTabScreen(viewModel = viewModel)
|
||||
HomeTab.Chat -> if (!chatTabStarted) ChatSheet(viewModel = viewModel)
|
||||
HomeTab.Voice -> VoiceTabScreen(viewModel = viewModel)
|
||||
HomeTab.Screen -> Unit
|
||||
HomeTab.Settings -> SettingsSheet(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Screen tab wrapper that refreshes canvas data once per gateway connection. */
|
||||
@Composable
|
||||
private fun ScreenTabScreen(
|
||||
viewModel: MainViewModel,
|
||||
visible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isConnected by viewModel.isConnected.collectAsState()
|
||||
var refreshedForCurrentConnection by rememberSaveable(isConnected) { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(isConnected, visible, refreshedForCurrentConnection) {
|
||||
if (visible && isConnected && !refreshedForCurrentConnection) {
|
||||
viewModel.refreshHomeCanvasOverviewIfConnected()
|
||||
refreshedForCurrentConnection = true
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
CanvasScreen(viewModel = viewModel, visible = visible, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
||||
/** Top status chip derived from gateway connection text. */
|
||||
@Composable
|
||||
private fun TopStatusBar(
|
||||
statusText: String,
|
||||
statusVisual: StatusVisual,
|
||||
) {
|
||||
val safeInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
|
||||
val (chipBg, chipDot, chipText, chipBorder) =
|
||||
when (statusVisual) {
|
||||
StatusVisual.Connected ->
|
||||
listOf(
|
||||
mobileSuccessSoft,
|
||||
mobileSuccess,
|
||||
mobileSuccess,
|
||||
LocalMobileColors.current.chipBorderConnected,
|
||||
)
|
||||
StatusVisual.Connecting ->
|
||||
listOf(
|
||||
mobileAccentSoft,
|
||||
mobileAccent,
|
||||
mobileAccent,
|
||||
LocalMobileColors.current.chipBorderConnecting,
|
||||
)
|
||||
StatusVisual.Warning ->
|
||||
listOf(
|
||||
mobileWarningSoft,
|
||||
mobileWarning,
|
||||
mobileWarning,
|
||||
LocalMobileColors.current.chipBorderWarning,
|
||||
)
|
||||
StatusVisual.Error ->
|
||||
listOf(
|
||||
mobileDangerSoft,
|
||||
mobileDanger,
|
||||
mobileDanger,
|
||||
LocalMobileColors.current.chipBorderError,
|
||||
)
|
||||
StatusVisual.Offline ->
|
||||
listOf(
|
||||
mobileSurface,
|
||||
mobileTextTertiary,
|
||||
mobileTextSecondary,
|
||||
mobileBorder,
|
||||
)
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth().windowInsetsPadding(safeInsets),
|
||||
color = Color.Transparent,
|
||||
shadowElevation = 0.dp,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 18.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = "OpenClaw",
|
||||
style = mobileTitle2,
|
||||
color = mobileText,
|
||||
)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = chipBg,
|
||||
border = androidx.compose.foundation.BorderStroke(1.dp, chipBorder),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.padding(top = 1.dp),
|
||||
color = chipDot,
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
) {
|
||||
Box(modifier = Modifier.padding(4.dp))
|
||||
}
|
||||
Text(
|
||||
text = statusText.trim().ifEmpty { "Offline" },
|
||||
style = mobileCaption1,
|
||||
color = chipText,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Bottom navigation for the legacy tab scaffold. */
|
||||
@Composable
|
||||
private fun BottomTabBar(
|
||||
activeTab: HomeTab,
|
||||
onSelect: (HomeTab) -> Unit,
|
||||
) {
|
||||
val safeInsets = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = mobileCardSurface.copy(alpha = 0.97f),
|
||||
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
|
||||
border = BorderStroke(1.dp, mobileBorder),
|
||||
shadowElevation = 6.dp,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.windowInsetsPadding(safeInsets)
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
HomeTab.entries.forEach { tab ->
|
||||
val active = tab == activeTab
|
||||
Surface(
|
||||
onClick = { onSelect(tab) },
|
||||
modifier = Modifier.weight(1f).heightIn(min = 58.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = if (active) mobileAccentSoft else Color.Transparent,
|
||||
border = if (active) BorderStroke(1.dp, LocalMobileColors.current.chipBorderConnecting) else null,
|
||||
shadowElevation = 0.dp,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp, vertical = 7.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = tab.icon,
|
||||
contentDescription = tab.label,
|
||||
tint = if (active) mobileAccent else mobileTextTertiary,
|
||||
)
|
||||
Text(
|
||||
text = tab.label,
|
||||
color = if (active) mobileAccent else mobileTextSecondary,
|
||||
style = mobileCaption2.copy(fontWeight = if (active) FontWeight.Bold else FontWeight.Medium),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
package ai.openclaw.app.ui
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Full-screen talk-mode presence indicator with pulsing rings and status fallback text.
|
||||
*/
|
||||
@Composable
|
||||
fun TalkOrbOverlay(
|
||||
seamColor: Color,
|
||||
statusText: String,
|
||||
isListening: Boolean,
|
||||
isSpeaking: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val transition = rememberInfiniteTransition(label = "talk-orb")
|
||||
val t by
|
||||
transition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec =
|
||||
infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1500, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart,
|
||||
),
|
||||
label = "pulse",
|
||||
)
|
||||
|
||||
val trimmed = statusText.trim()
|
||||
val showStatus = trimmed.isNotEmpty() && trimmed != "Off"
|
||||
val phase =
|
||||
when {
|
||||
isSpeaking -> "Speaking"
|
||||
isListening -> "Listening"
|
||||
else -> "Thinking"
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Canvas(modifier = Modifier.size(360.dp)) {
|
||||
val center = this.center
|
||||
val baseRadius = size.minDimension * 0.30f
|
||||
|
||||
val ring1 = 1.05f + (t * 0.25f)
|
||||
val ring2 = 1.20f + (t * 0.55f)
|
||||
val ringAlpha1 = (1f - t) * 0.34f
|
||||
val ringAlpha2 = (1f - t) * 0.22f
|
||||
|
||||
drawCircle(
|
||||
color = seamColor.copy(alpha = ringAlpha1),
|
||||
radius = baseRadius * ring1,
|
||||
center = center,
|
||||
style = Stroke(width = 3.dp.toPx()),
|
||||
)
|
||||
drawCircle(
|
||||
color = seamColor.copy(alpha = ringAlpha2),
|
||||
radius = baseRadius * ring2,
|
||||
center = center,
|
||||
style = Stroke(width = 3.dp.toPx()),
|
||||
)
|
||||
|
||||
drawCircle(
|
||||
brush =
|
||||
Brush.radialGradient(
|
||||
colors =
|
||||
listOf(
|
||||
seamColor.copy(alpha = 0.92f),
|
||||
seamColor.copy(alpha = 0.40f),
|
||||
Color.Black.copy(alpha = 0.56f),
|
||||
),
|
||||
center = center,
|
||||
radius = baseRadius * 1.35f,
|
||||
),
|
||||
radius = baseRadius,
|
||||
center = center,
|
||||
)
|
||||
|
||||
drawCircle(
|
||||
color = seamColor.copy(alpha = 0.34f),
|
||||
radius = baseRadius,
|
||||
center = center,
|
||||
style = Stroke(width = 1.dp.toPx()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showStatus) {
|
||||
Surface(
|
||||
color = Color.Black.copy(alpha = 0.40f),
|
||||
shape = CircleShape,
|
||||
) {
|
||||
Text(
|
||||
text = trimmed,
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp),
|
||||
color = Color.White.copy(alpha = 0.92f),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = phase,
|
||||
color = Color.White.copy(alpha = 0.80f),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -80,23 +80,3 @@ internal fun ClawLoadingState(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared recoverable error block with the app's attention styling.
|
||||
*/
|
||||
@Composable
|
||||
internal fun ClawErrorState(
|
||||
title: String,
|
||||
body: String,
|
||||
modifier: Modifier = Modifier,
|
||||
action: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
ClawPanel(modifier = modifier) {
|
||||
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
ClawStatusPill(text = "Needs attention", status = ClawStatus.Danger)
|
||||
Text(text = title, style = ClawTheme.type.section, color = ClawTheme.colors.text)
|
||||
Text(text = body, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
action?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -984,7 +984,7 @@ class GatewayBootstrapAuthTest {
|
|||
ownershipEpoch.set(41L)
|
||||
|
||||
runtime.setMicEnabled(true)
|
||||
val cleanup = runtime.javaClass.getDeclaredMethod("finishTalkCaptureIfIdle", Long::class.javaPrimitiveType)
|
||||
val cleanup = runtime.javaClass.getDeclaredMethod("cleanupFailedTalkCapture", Long::class.javaPrimitiveType)
|
||||
cleanup.isAccessible = true
|
||||
cleanup.invoke(runtime, 41L)
|
||||
|
||||
|
|
|
|||
|
|
@ -346,7 +346,6 @@ class InvokeDispatcherTest {
|
|||
camera = CameraCaptureManager(appContext),
|
||||
externalAudioCaptureActive = MutableStateFlow(false),
|
||||
showCameraHud = { _, _, _ -> },
|
||||
triggerCameraFlash = {},
|
||||
invokeErrorFromThrowable = { err -> "UNAVAILABLE" to (err.message ?: "camera failed") },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,5 +88,3 @@ export function createPollCommentFolder(options?: { windowMs?: number }) {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type PollCommentFolder = ReturnType<typeof createPollCommentFolder>;
|
||||
|
|
|
|||
|
|
@ -327,17 +327,13 @@ export type QaScorecardEvidenceMode = z.infer<typeof qaScorecardEvidenceModeSche
|
|||
export type QaScorecardChannelDriver = z.infer<typeof qaScorecardChannelDriverSchema>;
|
||||
export type QaMaturityScoreKey = (typeof QA_MATURITY_SCORE_KEYS)[number];
|
||||
export type QaMaturityScoreObject = z.infer<typeof qaMaturityScoreObjectSchema>;
|
||||
export type QaMaturityScoreBundle = z.infer<typeof qaMaturityScoreBundleSchema>;
|
||||
export type QaMaturityScoreLastRun = z.infer<typeof qaMaturityScoreLastRunSchema>;
|
||||
export type QaMaturityScoreSurfaceLts = z.infer<typeof qaMaturityScoreSurfaceLtsSchema>;
|
||||
export type QaMaturityScoreCategory = z.infer<typeof qaMaturityScoreCategorySchema>;
|
||||
export type QaMaturityScoreSurface = z.infer<typeof qaMaturityScoreSurfaceSchema>;
|
||||
export type QaMaturityScores = z.infer<typeof qaMaturityScoresSchema>;
|
||||
export type QaMaturityTaxonomyLevel = z.infer<typeof qaMaturityLevelSchema>;
|
||||
export type QaMaturityTaxonomyFeature = z.infer<typeof qaMaturityFeatureSchema>;
|
||||
export type QaMaturityTaxonomyCategory = z.infer<typeof qaMaturityCategorySchema>;
|
||||
export type QaMaturityTaxonomySurface = z.infer<typeof qaMaturitySurfaceSchema>;
|
||||
export type QaMaturityTaxonomyProfile = z.infer<typeof qaScorecardProfileSchema>;
|
||||
export type QaMaturityTaxonomy = z.infer<typeof qaMaturityTaxonomySchema>;
|
||||
type QaCoverageEvidenceRole = z.infer<typeof qaCoverageEvidenceRoleSchema>;
|
||||
|
||||
|
|
@ -603,23 +599,6 @@ export function qaMaturityTaxonomyLevelMap(taxonomy: QaMaturityTaxonomy) {
|
|||
return new Map(taxonomy.levels.map((level) => [level.id, level]));
|
||||
}
|
||||
|
||||
export function qaMaturityCategoryProfiles(taxonomy: QaMaturityTaxonomy): Map<string, string[]> {
|
||||
const profilesByCategory = new Map<string, string[]>();
|
||||
for (const profile of taxonomy.profiles) {
|
||||
const categoryIds = profile.includeAllCategories
|
||||
? activeQaMaturityTaxonomySurfaces(taxonomy).flatMap((surface) =>
|
||||
surface.categories.map((category) => `${surface.id}.${category.id}`),
|
||||
)
|
||||
: profile.categoryIds;
|
||||
for (const categoryId of categoryIds) {
|
||||
const profiles = profilesByCategory.get(categoryId) ?? [];
|
||||
profiles.push(profile.id);
|
||||
profilesByCategory.set(categoryId, profiles);
|
||||
}
|
||||
}
|
||||
return profilesByCategory;
|
||||
}
|
||||
|
||||
export function qaMaturityFamilyOrder(surfaces: readonly QaMaturityTaxonomySurface[]): string[] {
|
||||
const seen: string[] = [];
|
||||
for (const surface of surfaces) {
|
||||
|
|
|
|||
|
|
@ -237,12 +237,6 @@ export function isTelegramSpooledUpdateClaimOwnedByOtherLiveProcess(
|
|||
);
|
||||
}
|
||||
|
||||
export function isTelegramSpooledUpdateClaimOwnedByCurrentProcess(
|
||||
claim: ClaimedTelegramSpooledUpdate,
|
||||
): boolean {
|
||||
return claim.claim?.processId === TELEGRAM_SPOOLED_UPDATE_PROCESS_ID;
|
||||
}
|
||||
|
||||
export async function writeTelegramSpooledUpdate(params: {
|
||||
spoolDir: string;
|
||||
update: unknown;
|
||||
|
|
|
|||
|
|
@ -58,20 +58,6 @@ function resolveAcpAgentResumeHintLine(params: {
|
|||
return resolver ? resolver({ agentSessionId }) : undefined;
|
||||
}
|
||||
|
||||
/** Renders status-safe ACP session identifier lines from persisted session metadata. */
|
||||
export function resolveAcpSessionIdentifierLines(params: {
|
||||
sessionKey: string;
|
||||
meta?: SessionAcpMeta;
|
||||
}): string[] {
|
||||
const backend = normalizeText(params.meta?.backend) ?? "backend";
|
||||
const identity = resolveSessionIdentityFromMeta(params.meta);
|
||||
return resolveAcpSessionIdentifierLinesFromIdentity({
|
||||
backend,
|
||||
identity,
|
||||
mode: "status",
|
||||
});
|
||||
}
|
||||
|
||||
/** Renders resolved ACP backend/agent ids, hiding pending ids from thread intros. */
|
||||
export function resolveAcpSessionIdentifierLinesFromIdentity(params: {
|
||||
backend: string;
|
||||
|
|
|
|||
|
|
@ -14,10 +14,7 @@ import (
|
|||
const defaultDocChunkMaxBytes = 12000
|
||||
const defaultDocChunkPromptBudget = 15000
|
||||
|
||||
var (
|
||||
docsFenceRE = regexp.MustCompile(`^\s*(` + "```" + `|~~~)`)
|
||||
docsComponentTagRE = regexp.MustCompile(`<(/?)([A-Z][A-Za-z0-9]*)\b[^>]*?/?>`)
|
||||
)
|
||||
var docsComponentTagRE = regexp.MustCompile(`<(/?)([A-Z][A-Za-z0-9]*)\b[^>]*?/?>`)
|
||||
|
||||
var docsProtocolTokens = []string{
|
||||
frontmatterTagStart,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ type docsRedirect struct {
|
|||
var (
|
||||
localeDirRe = regexp.MustCompile(`^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})?$`)
|
||||
fencedBacktickCodeBlock = regexp.MustCompile("(?ms)(^|\\n)[ \\t]*```[^\\n]*\\n.*?\\n[ \\t]*```[ \\t]*(?:\\n|$)")
|
||||
fencedTildeCodeBlock = regexp.MustCompile("(?ms)(^|\\n)[ \\t]*~~~[^\\n]*\\n.*?\\n[ \\t]*~~~[ \\t]*(?:\\n|$)")
|
||||
fencedTildeCodeBlock = regexp.MustCompile(`(?ms)(^|\n)[ \t]*~~~[^\n]*\n.*?\n[ \t]*~~~[ \t]*(?:\n|$)`)
|
||||
markdownLinkTargetRe = regexp.MustCompile(`!?\[[^\]]*\]\(([^)]+)\)`)
|
||||
hrefDoubleQuotedValueRe = regexp.MustCompile(`\bhref\s*=\s*"([^"]*)"`)
|
||||
hrefSingleQuotedValueRe = regexp.MustCompile(`\bhref\s*=\s*'([^']*)'`)
|
||||
|
|
|
|||
|
|
@ -114,9 +114,7 @@ func splitFrontMatter(content string) (string, string) {
|
|||
}
|
||||
front := strings.Join(lines[1:endIndex], "\n")
|
||||
body := strings.Join(lines[endIndex+1:], "\n")
|
||||
if strings.HasPrefix(body, "\n") {
|
||||
body = body[1:]
|
||||
}
|
||||
body = strings.TrimPrefix(body, "\n")
|
||||
return front, body
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ func LoadTranslationMemory(path string) (*TranslationMemory, error) {
|
|||
}
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
for {
|
||||
|
|
|
|||
|
|
@ -207,7 +207,9 @@ func runCodexExecPrompt(ctx context.Context, req codexPromptRequest) (string, er
|
|||
}
|
||||
outputPath := outputFile.Name()
|
||||
_ = outputFile.Close()
|
||||
defer os.Remove(outputPath)
|
||||
defer func() {
|
||||
_ = os.Remove(outputPath)
|
||||
}()
|
||||
|
||||
codexHomeBase, err := isolatedCodexHomeBase()
|
||||
if err != nil {
|
||||
|
|
@ -217,7 +219,9 @@ func runCodexExecPrompt(ctx context.Context, req codexPromptRequest) (string, er
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.RemoveAll(codexHome)
|
||||
defer func() {
|
||||
_ = os.RemoveAll(codexHome)
|
||||
}()
|
||||
if err := writeCodexAuthFile(codexHome); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,12 +74,12 @@ export function sanitizeCompactionMessages(messages: AgentMessage[]): AgentMessa
|
|||
}
|
||||
|
||||
/** Estimates one message using the same sanitization path as multi-message planning. */
|
||||
export function estimateCompactionMessageTokens(message: AgentMessage): number {
|
||||
function estimateCompactionMessageTokens(message: AgentMessage): number {
|
||||
return estimateMessagesTokens([message]);
|
||||
}
|
||||
|
||||
/** Clamps requested split parts to a usable count for the available messages. */
|
||||
export function normalizeCompactionParts(parts: number, messageCount: number): number {
|
||||
function normalizeCompactionParts(parts: number, messageCount: number): number {
|
||||
if (!Number.isFinite(parts) || parts <= 1) {
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import {
|
|||
import { SessionManager } from "../sessions/index.js";
|
||||
import { log } from "./logger.js";
|
||||
import {
|
||||
persistTranscriptStateMutation,
|
||||
readTranscriptFileState,
|
||||
type TranscriptFileState,
|
||||
type TranscriptPersistedEntry,
|
||||
|
|
@ -491,71 +490,3 @@ export async function rewriteTranscriptEntriesInRuntimeTranscript(params: {
|
|||
await sessionLock?.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites a named transcript file artifact. Runtime callers should prefer
|
||||
* rewriteTranscriptEntriesInRuntimeTranscript with agent/session scope.
|
||||
*/
|
||||
export async function rewriteTranscriptEntriesInSessionFile(params: {
|
||||
sessionFile: string;
|
||||
sessionId?: string;
|
||||
sessionKey?: string;
|
||||
agentId?: string;
|
||||
request: TranscriptRewriteRequest;
|
||||
config?: SessionWriteLockAcquireTimeoutConfig;
|
||||
}): Promise<TranscriptRewriteResult> {
|
||||
let sessionLock: Awaited<ReturnType<typeof acquireSessionWriteLock>> | undefined;
|
||||
try {
|
||||
sessionLock = await acquireSessionWriteLock({
|
||||
sessionFile: params.sessionFile,
|
||||
...resolveSessionWriteLockOptions(params.config),
|
||||
});
|
||||
const state = await readTranscriptFileState(params.sessionFile);
|
||||
const result = rewriteTranscriptEntriesInState({
|
||||
state,
|
||||
replacements: params.request.replacements,
|
||||
...(params.request.allowedRewriteSuffixEntryIds
|
||||
? { allowedRewriteSuffixEntryIds: params.request.allowedRewriteSuffixEntryIds }
|
||||
: {}),
|
||||
});
|
||||
if (result.changed) {
|
||||
await persistTranscriptStateMutation({
|
||||
sessionFile: params.sessionFile,
|
||||
state,
|
||||
appendedEntries: result.appendedEntries,
|
||||
});
|
||||
emitSessionTranscriptUpdate({
|
||||
sessionFile: params.sessionFile,
|
||||
sessionKey: params.sessionKey,
|
||||
...(params.agentId ? { agentId: params.agentId } : {}),
|
||||
...(params.sessionId && params.sessionKey && params.agentId
|
||||
? {
|
||||
target: {
|
||||
agentId: params.agentId,
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
log.info(
|
||||
`[transcript-rewrite] rewrote ${result.rewrittenEntries} entr` +
|
||||
`${result.rewrittenEntries === 1 ? "y" : "ies"} ` +
|
||||
`bytesFreed=${result.bytesFreed} ` +
|
||||
`sessionKey=${params.sessionKey ?? params.sessionId ?? "unknown"}`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
const reason = formatErrorMessage(err);
|
||||
log.warn(`[transcript-rewrite] failed: ${reason}`);
|
||||
return {
|
||||
changed: false,
|
||||
bytesFreed: 0,
|
||||
rewrittenEntries: 0,
|
||||
reason,
|
||||
};
|
||||
} finally {
|
||||
await sessionLock?.release();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,10 +138,6 @@ export function abortSessionRunTargetWithOutcome(params: { key?: string; session
|
|||
return { active, aborted };
|
||||
}
|
||||
|
||||
export function abortSessionRunTarget(params: { key?: string; sessionId?: string }): boolean {
|
||||
return abortSessionRunTargetWithOutcome(params).aborted;
|
||||
}
|
||||
|
||||
export function formatAbortReplyText(
|
||||
stoppedSubagents?: number,
|
||||
rejectionReason?: "finalizing",
|
||||
|
|
|
|||
|
|
@ -190,8 +190,3 @@ export async function detectInferenceBackends(
|
|||
);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/** Format a candidate for prompts/logs, e.g. "Codex — openai/gpt-5.5 (logged in)". */
|
||||
export function formatInferenceBackendCandidate(candidate: InferenceBackendCandidate): string {
|
||||
return `${candidate.label} — ${candidate.modelRef} (${candidate.detail})`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,8 +207,3 @@ export function abortQueuedChatTurns(
|
|||
}
|
||||
return runIds;
|
||||
}
|
||||
|
||||
/** Test helper: clear all queued turns. */
|
||||
export function clearAllQueuedChatTurns(chatQueuedTurns: QueuedChatTurnMap): void {
|
||||
chatQueuedTurns.clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,15 +315,6 @@ export function hasOnlySessionLifecycleMutationKindActive(
|
|||
);
|
||||
}
|
||||
|
||||
export async function waitForSessionLifecycleMutationIdle(
|
||||
scope: string,
|
||||
identities: Iterable<string | undefined>,
|
||||
): Promise<void> {
|
||||
await waitForNormalizedSessionLifecycleMutationIdle(
|
||||
normalizeSessionIdentities(scope, identities),
|
||||
);
|
||||
}
|
||||
|
||||
export function isSessionWorkAdmissionActive(
|
||||
scope: string,
|
||||
identities: Iterable<string | undefined>,
|
||||
|
|
|
|||
|
|
@ -293,10 +293,3 @@ export function extractDurableInstructionProposals(params: {
|
|||
maxProposals: params.maxProposals,
|
||||
});
|
||||
}
|
||||
|
||||
/** Extracts a single candidate durable instruction; kept for callers that want one proposal. */
|
||||
export function extractDurableInstructionProposal(params: {
|
||||
messages: unknown[];
|
||||
}): DurableInstruction | undefined {
|
||||
return extractDurableInstructionProposals({ ...params, maxProposals: 1 }).at(-1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,5 @@
|
|||
// Runs Codex native subagent tasks and maps their lifecycle into task registry state.
|
||||
import {
|
||||
isChildlessNativeSubagentTask,
|
||||
resolveChildlessNativeSubagentTaskDefinition,
|
||||
} from "./native-subagent-task.js";
|
||||
import type { TaskRecord } from "./task-registry.types.js";
|
||||
|
||||
/** Runtime label used for Codex-native subagent task records. */
|
||||
export const CODEX_NATIVE_SUBAGENT_RUNTIME = "subagent";
|
||||
export const CODEX_NATIVE_SUBAGENT_TASK_KIND = "codex-native";
|
||||
export const CODEX_NATIVE_SUBAGENT_RUN_ID_PREFIX = "codex-thread:";
|
||||
export const CODEX_NATIVE_SUBAGENT_STALE_ERROR = "Codex native subagent stopped reporting progress";
|
||||
|
||||
/** Detects native Codex subagent tasks that have no child session to recover from. */
|
||||
export function isChildlessCodexNativeSubagentTask(task: TaskRecord): boolean {
|
||||
return (
|
||||
isChildlessNativeSubagentTask(task) &&
|
||||
resolveChildlessNativeSubagentTaskDefinition(task)?.taskKind === CODEX_NATIVE_SUBAGENT_TASK_KIND
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import type { TaskRecord } from "./task-registry.types.js";
|
|||
|
||||
export const COPILOT_NATIVE_SUBAGENT_TASK_KIND = "copilot-native";
|
||||
export const COPILOT_NATIVE_SUBAGENT_RUN_ID_PREFIX = "copilot-agent:";
|
||||
export const COPILOT_NATIVE_SUBAGENT_STALE_ERROR =
|
||||
"Copilot native subagent stopped reporting progress";
|
||||
|
||||
const CHILDLESS_NATIVE_SUBAGENT_DEFINITIONS = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -80,15 +80,6 @@ export async function startApplicationRouter(
|
|||
await router.start(history, basePath, context);
|
||||
}
|
||||
|
||||
export function startAppRouter(
|
||||
router: ApplicationRouter,
|
||||
history: RouterHistory,
|
||||
basePath: string,
|
||||
context: ApplicationContext<RouteId>,
|
||||
): Promise<void> {
|
||||
return startApplicationRouter(router, history, basePath, context);
|
||||
}
|
||||
|
||||
export {
|
||||
APP_ROUTE_DEFINITIONS,
|
||||
APP_ROUTE_IDS,
|
||||
|
|
|
|||
|
|
@ -69,30 +69,6 @@ export function loadLocalAssistantIdentity(opts?: {
|
|||
}
|
||||
}
|
||||
|
||||
export function saveLocalAssistantIdentity(next: LocalAssistantIdentity) {
|
||||
const agentId = normalizeOptionalString(next.agentId);
|
||||
if (!agentId) {
|
||||
return;
|
||||
}
|
||||
const storage = getSafeLocalStorage();
|
||||
try {
|
||||
const raw = storage?.getItem(LOCAL_ASSISTANT_IDENTITY_KEY);
|
||||
const avatars = raw
|
||||
? parseLocalAssistantAvatarMap(raw).avatars
|
||||
: (Object.create(null) as Record<string, string>);
|
||||
const avatar = normalizeOptionalString(next.avatar);
|
||||
if (avatar) {
|
||||
avatars[agentId] = avatar;
|
||||
} else {
|
||||
delete avatars[agentId];
|
||||
}
|
||||
persistLocalAssistantAvatarMap(storage, avatars);
|
||||
} catch {
|
||||
// best-effort — quota exceeded or security restrictions should not
|
||||
// prevent in-memory identity updates from being applied
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAssistantIdentity(
|
||||
client: GatewayBrowserClient,
|
||||
sessionKey?: string,
|
||||
|
|
|
|||
|
|
@ -271,7 +271,6 @@ export class AppSidebar extends LitElement {
|
|||
agentsList: context.agents.state.agentsList,
|
||||
chatAgentSessionRowsByAgent: this.sessionRowsByAgent,
|
||||
sessionsResult: this.sessionsResult,
|
||||
sessionsResultAgentId: this.sessionsAgentId,
|
||||
sessionKey: routeSessionKey,
|
||||
},
|
||||
nextAgentId,
|
||||
|
|
@ -768,7 +767,6 @@ export class AppSidebar extends LitElement {
|
|||
const options = resolveSessionAgentFilterOptions({
|
||||
agentsList: this.context?.agents.state.agentsList,
|
||||
sessionsResult: this.sessionsResult,
|
||||
sessionsResultAgentId: this.sessionsAgentId,
|
||||
sessionKey,
|
||||
});
|
||||
if (options.length <= 1) {
|
||||
|
|
|
|||
|
|
@ -82,10 +82,6 @@ function getPaletteItemsInternal(): PaletteItem[] {
|
|||
return getPaletteBaseItems();
|
||||
}
|
||||
|
||||
export function getPaletteItems(): readonly PaletteItem[] {
|
||||
return getPaletteItemsInternal();
|
||||
}
|
||||
|
||||
type CommandPaletteProps = {
|
||||
open: boolean;
|
||||
query: string;
|
||||
|
|
@ -112,10 +108,6 @@ function filteredItems(query: string, includeSlashCommands = true): PaletteItem[
|
|||
);
|
||||
}
|
||||
|
||||
export function getFilteredPaletteItems(query: string): readonly PaletteItem[] {
|
||||
return filteredItems(query);
|
||||
}
|
||||
|
||||
function groupItems(items: PaletteItem[]): Array<[string, PaletteItem[]]> {
|
||||
const map = new Map<string, PaletteItem[]>();
|
||||
for (const item of items) {
|
||||
|
|
|
|||
|
|
@ -4,14 +4,6 @@ import type { ModelAuthStatusProvider, ModelAuthStatusResult } from "../api/type
|
|||
|
||||
const EMPTY_AUTH_STATUS: ModelAuthStatusResult = { ts: 0, providers: [] };
|
||||
|
||||
export type ModelAuthStatusState = {
|
||||
client: GatewayBrowserClient | null;
|
||||
connected: boolean;
|
||||
modelAuthStatusLoading: boolean;
|
||||
modelAuthStatusResult: ModelAuthStatusResult | null;
|
||||
modelAuthStatusError: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* True when a provider's auth should be actively monitored on the dashboard.
|
||||
*
|
||||
|
|
@ -46,34 +38,3 @@ export async function loadModelAuthStatus(
|
|||
(await client.request<ModelAuthStatusResult>("models.authStatus", params)) ?? EMPTY_AUTH_STATUS
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadModelAuthStatusState(
|
||||
state: ModelAuthStatusState,
|
||||
opts?: { refresh?: boolean },
|
||||
): Promise<void> {
|
||||
const client = state.client;
|
||||
if (!client || !state.connected) {
|
||||
state.modelAuthStatusLoading = false;
|
||||
return;
|
||||
}
|
||||
if (state.modelAuthStatusLoading) {
|
||||
return;
|
||||
}
|
||||
state.modelAuthStatusLoading = true;
|
||||
state.modelAuthStatusError = null;
|
||||
try {
|
||||
const result = await loadModelAuthStatus(client, opts);
|
||||
if (state.client !== client || !state.connected) {
|
||||
return;
|
||||
}
|
||||
state.modelAuthStatusResult = result;
|
||||
} catch (err) {
|
||||
if (state.client !== client || !state.connected) {
|
||||
return;
|
||||
}
|
||||
state.modelAuthStatusError = err instanceof Error ? err.message : String(err);
|
||||
state.modelAuthStatusResult = EMPTY_AUTH_STATUS;
|
||||
} finally {
|
||||
state.modelAuthStatusLoading = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import type { SessionsListResult } from "../../api/types.ts";
|
||||
import { isCronSessionKey, resolveSessionDisplayName } from "../session-display.ts";
|
||||
import { isCronSessionKey } from "../session-display.ts";
|
||||
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../string-coerce.ts";
|
||||
import { getVisibleSessionRows } from "./index.ts";
|
||||
import {
|
||||
buildAgentMainSessionKey,
|
||||
isSessionKeyTiedToAgent,
|
||||
|
|
@ -20,23 +19,10 @@ type SessionAgentOptionsState = {
|
|||
}> | null;
|
||||
} | null;
|
||||
chatAgentSessionRowsByAgent?: Record<string, SessionsListResult["sessions"]>;
|
||||
sessionsHideCron?: boolean;
|
||||
sessionsResult?: SessionsListResult | null;
|
||||
sessionsResultAgentId?: string | null;
|
||||
sessionKey: string;
|
||||
};
|
||||
|
||||
export type SessionOptionGroup = {
|
||||
id: string;
|
||||
label: string;
|
||||
options: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
scopeLabel: string;
|
||||
title: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type SessionAgentFilterOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
|
|
@ -64,35 +50,6 @@ function resolvePreferredSessionCandidateAgentId(
|
|||
return normalizeAgentId(parsed?.agentId ?? defaultAgentId);
|
||||
}
|
||||
|
||||
export function rememberSessionAgentRows(
|
||||
state: SessionAgentOptionsState,
|
||||
sessions: SessionsListResult | null,
|
||||
): void {
|
||||
if (!sessions) {
|
||||
return;
|
||||
}
|
||||
const refreshedAgentId = normalizeOptionalString(state.sessionsResultAgentId);
|
||||
const defaultAgentId = normalizeAgentId(state.agentsList?.defaultId ?? "main");
|
||||
const grouped = new Map<string, SessionsListResult["sessions"]>();
|
||||
for (const row of sessions.sessions) {
|
||||
const agentId = resolvePreferredSessionCandidateAgentId(row, defaultAgentId);
|
||||
if (!agentId) {
|
||||
continue;
|
||||
}
|
||||
grouped.set(agentId, [...(grouped.get(agentId) ?? []), row]);
|
||||
}
|
||||
if (grouped.size === 0 && !refreshedAgentId) {
|
||||
return;
|
||||
}
|
||||
state.chatAgentSessionRowsByAgent ??= {};
|
||||
if (refreshedAgentId) {
|
||||
state.chatAgentSessionRowsByAgent[refreshedAgentId] = grouped.get(refreshedAgentId) ?? [];
|
||||
}
|
||||
for (const [agentId, agentRows] of grouped) {
|
||||
state.chatAgentSessionRowsByAgent[agentId] = agentRows;
|
||||
}
|
||||
}
|
||||
|
||||
function rowsForPreferredAgentSession(
|
||||
state: SessionAgentOptionsState,
|
||||
normalizedAgentId: string,
|
||||
|
|
@ -165,153 +122,6 @@ export function resolveSessionAgentFilterOptions(
|
|||
return options;
|
||||
}
|
||||
|
||||
export function resolveSessionOptionGroups(
|
||||
state: SessionAgentOptionsState,
|
||||
sessionKey: string,
|
||||
sessions: SessionsListResult | null,
|
||||
): SessionOptionGroup[] {
|
||||
const rows = sessions?.sessions ?? [];
|
||||
const hideCron = state.sessionsHideCron ?? true;
|
||||
const activeAgentId = resolveSessionAgentFilterId(state, sessionKey);
|
||||
const defaultAgentId = normalizeAgentId(state.agentsList?.defaultId ?? "main");
|
||||
const byKey = new Map<string, SessionsListResult["sessions"][number]>();
|
||||
for (const row of rows) {
|
||||
byKey.set(row.key, row);
|
||||
}
|
||||
|
||||
const seenKeys = new Set<string>();
|
||||
const groups = new Map<string, SessionOptionGroup>();
|
||||
const ensureGroup = (groupId: string, label: string): SessionOptionGroup => {
|
||||
const existing = groups.get(groupId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const created: SessionOptionGroup = {
|
||||
id: groupId,
|
||||
label,
|
||||
options: [],
|
||||
};
|
||||
groups.set(groupId, created);
|
||||
return created;
|
||||
};
|
||||
|
||||
const addOption = (key: string) => {
|
||||
if (!key || seenKeys.has(key)) {
|
||||
return;
|
||||
}
|
||||
seenKeys.add(key);
|
||||
const row = byKey.get(key);
|
||||
const parsed = parseAgentSessionKey(key);
|
||||
const group = parsed
|
||||
? ensureGroup(
|
||||
`agent:${normalizeLowercaseStringOrEmpty(parsed.agentId)}`,
|
||||
resolveAgentGroupLabel(state, parsed.agentId),
|
||||
)
|
||||
: ensureGroup("other", "Other Sessions");
|
||||
const scopeLabel = normalizeOptionalString(parsed?.rest) ?? key;
|
||||
group.options.push({
|
||||
key,
|
||||
label: resolveSessionScopedOptionLabel(key, row, parsed?.rest),
|
||||
scopeLabel,
|
||||
title: key,
|
||||
});
|
||||
};
|
||||
|
||||
for (const row of getVisibleSessionRows(sessions, {
|
||||
currentSessionKey: sessionKey,
|
||||
agentId: activeAgentId,
|
||||
defaultAgentId,
|
||||
filterByAgent: true,
|
||||
hideCron,
|
||||
})) {
|
||||
addOption(row.key);
|
||||
}
|
||||
if (byKey.has(sessionKey)) {
|
||||
addOption(sessionKey);
|
||||
} else if (sessionKey) {
|
||||
addOption(sessionKey);
|
||||
}
|
||||
|
||||
disambiguateSessionOptionLabels(groups);
|
||||
return Array.from(groups.values());
|
||||
}
|
||||
|
||||
function disambiguateSessionOptionLabels(groups: Map<string, SessionOptionGroup>) {
|
||||
for (const group of groups.values()) {
|
||||
const counts = new Map<string, number>();
|
||||
for (const option of group.options) {
|
||||
counts.set(option.label, (counts.get(option.label) ?? 0) + 1);
|
||||
}
|
||||
for (const option of group.options) {
|
||||
if ((counts.get(option.label) ?? 0) > 1 && option.scopeLabel !== option.label) {
|
||||
option.label = `${option.label} · ${option.scopeLabel}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allOptions = Array.from(groups.values()).flatMap((group) =>
|
||||
group.options.map((option) => ({ groupLabel: group.label, option })),
|
||||
);
|
||||
const labels = new Map(allOptions.map(({ option }) => [option, option.label]));
|
||||
const countAssignedLabels = () => {
|
||||
const counts = new Map<string, number>();
|
||||
for (const { option } of allOptions) {
|
||||
const label = labels.get(option) ?? option.label;
|
||||
counts.set(label, (counts.get(label) ?? 0) + 1);
|
||||
}
|
||||
return counts;
|
||||
};
|
||||
const labelIncludesScopeLabel = (label: string, scopeLabel: string) => {
|
||||
const trimmedScope = scopeLabel.trim();
|
||||
if (!trimmedScope) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
label === trimmedScope ||
|
||||
label.endsWith(` · ${trimmedScope}`) ||
|
||||
label.endsWith(` / ${trimmedScope}`)
|
||||
);
|
||||
};
|
||||
|
||||
const globalCounts = countAssignedLabels();
|
||||
for (const { groupLabel, option } of allOptions) {
|
||||
const currentLabel = labels.get(option) ?? option.label;
|
||||
if ((globalCounts.get(currentLabel) ?? 0) <= 1) {
|
||||
continue;
|
||||
}
|
||||
const scopedPrefix = `${groupLabel} / `;
|
||||
if (currentLabel.startsWith(scopedPrefix)) {
|
||||
continue;
|
||||
}
|
||||
labels.set(option, `${groupLabel} / ${currentLabel}`);
|
||||
}
|
||||
|
||||
const scopedCounts = countAssignedLabels();
|
||||
for (const { option } of allOptions) {
|
||||
const currentLabel = labels.get(option) ?? option.label;
|
||||
if ((scopedCounts.get(currentLabel) ?? 0) <= 1) {
|
||||
continue;
|
||||
}
|
||||
if (labelIncludesScopeLabel(currentLabel, option.scopeLabel)) {
|
||||
continue;
|
||||
}
|
||||
labels.set(option, `${currentLabel} · ${option.scopeLabel}`);
|
||||
}
|
||||
|
||||
const finalCounts = countAssignedLabels();
|
||||
for (const { option } of allOptions) {
|
||||
const currentLabel = labels.get(option) ?? option.label;
|
||||
if ((finalCounts.get(currentLabel) ?? 0) <= 1) {
|
||||
continue;
|
||||
}
|
||||
labels.set(option, `${currentLabel} · ${option.key}`);
|
||||
}
|
||||
|
||||
for (const { option } of allOptions) {
|
||||
option.label = labels.get(option) ?? option.label;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAgentGroupLabel(state: SessionAgentOptionsState, agentIdRaw: string): string {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(agentIdRaw);
|
||||
const agent = (state.agentsList?.agents ?? []).find(
|
||||
|
|
@ -321,22 +131,3 @@ function resolveAgentGroupLabel(state: SessionAgentOptionsState, agentIdRaw: str
|
|||
normalizeOptionalString(agent?.identity?.name) ?? normalizeOptionalString(agent?.name) ?? "";
|
||||
return name && name !== agentIdRaw ? `${name} (${agentIdRaw})` : agentIdRaw;
|
||||
}
|
||||
|
||||
function resolveSessionScopedOptionLabel(
|
||||
key: string,
|
||||
row?: SessionsListResult["sessions"][number],
|
||||
rest?: string,
|
||||
) {
|
||||
const base = normalizeOptionalString(rest) ?? key;
|
||||
if (!row) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const label = normalizeOptionalString(row.label) ?? "";
|
||||
const displayName = normalizeOptionalString(row.displayName) ?? "";
|
||||
if ((label && label !== key) || (displayName && displayName !== key)) {
|
||||
return resolveSessionDisplayName(key, row);
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,15 +222,3 @@ export function markQueuedChatSendsWaitingForReconnect(host: ChatQueueStoreHost)
|
|||
host.chatQueueBySession = queueBySession;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasReconnectableQueuedChatSends(host: ChatQueueStoreHost): boolean {
|
||||
const matches = (item: ChatQueueItem) =>
|
||||
Boolean(item.sendRunId) &&
|
||||
item.sendState === "waiting-reconnect" &&
|
||||
!item.pendingRunId &&
|
||||
!item.localCommandName;
|
||||
return (
|
||||
host.chatQueue.some(matches) ||
|
||||
Object.values(host.chatQueueBySession ?? {}).some((queue) => queue.some(matches))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue