Add analytics tab view analytics

This commit is contained in:
Xisheng-Zhao
2026-05-18 17:46:23 +08:00
parent 82b556f0c0
commit 392b961fb6
5 changed files with 326 additions and 44 deletions
+7
View File
@@ -4,6 +4,7 @@ plugins {
id "org.jetbrains.kotlin.plugin.compose" id "org.jetbrains.kotlin.plugin.compose"
} }
android { android {
namespace "ai.multica.app" namespace "ai.multica.app"
compileSdk 36 compileSdk 36
@@ -46,6 +47,12 @@ android {
buildConfigField "String", "MULTICA_AUTH_PREFS_NAME", "\"multica_auth\"" buildConfigField "String", "MULTICA_AUTH_PREFS_NAME", "\"multica_auth\""
} }
} }
buildTypes {
release {
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
} }
dependencies { dependencies {
View File
+2
View File
@@ -1,11 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:name=".MulticaApplication"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="${usesCleartextTraffic}"> android:usesCleartextTraffic="${usesCleartextTraffic}">
@@ -359,8 +359,10 @@ private data class InboxIssueContext(
private data class PilotIssueDetailData( private data class PilotIssueDetailData(
val issue: Models.Issue, val issue: Models.Issue,
val parentIssue: Models.Issue?,
val comments: List<Models.Comment>, val comments: List<Models.Comment>,
val runs: List<Models.AgentTask>, val runs: List<Models.AgentTask>,
val projects: List<Models.Project>,
val members: List<Models.Member>, val members: List<Models.Member>,
val agents: List<Models.Agent>, val agents: List<Models.Agent>,
val issueLabels: List<Models.IssueLabel>, val issueLabels: List<Models.IssueLabel>,
@@ -1591,6 +1593,7 @@ private fun ComposePilotShell(
chatSession != null || chatSession != null ||
settingsPage != null settingsPage != null
BackHandler(enabled = secondaryScreenOpen) { BackHandler(enabled = secondaryScreenOpen) {
when { when {
issueFormOpen -> { issueFormOpen -> {
@@ -1971,6 +1974,7 @@ private fun PilotTabContent(
} }
} }
@Composable @Composable
private fun PilotSearchPage( private fun PilotSearchPage(
api: ApiClient, api: ApiClient,
@@ -7569,6 +7573,68 @@ private fun IssueDetailPriorityMenu(
} }
} }
@Composable
private fun IssueDetailParentIssueLink(
parentIssue: Models.Issue,
zh: Boolean,
onOpen: () -> Unit,
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
.clip(RoundedCornerShape(14.dp))
.clickable(onClick = onOpen)
.semantics { contentDescription = "Issue Detail Parent Issue Link" },
shape = RoundedCornerShape(14.dp),
color = MulticaColors.Surface.copy(alpha = 0.86f),
tonalElevation = 0.dp,
shadowElevation = 0.dp,
border = androidx.compose.foundation.BorderStroke(0.75.dp, MulticaColors.Border.copy(alpha = 0.5f)),
) {
Row(
modifier = Modifier.padding(horizontal = 13.dp, vertical = 11.dp),
horizontalArrangement = Arrangement.spacedBy(9.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = issueStatusIcon(parentIssue.status),
contentDescription = null,
tint = MulticaColors.Accent,
modifier = Modifier.size(17.dp),
)
Text(
text = if (zh) "子 Issue of" else "Sub-issue of",
style = MaterialTheme.typography.labelSmall.copy(
fontSize = MulticaTypeScale.Caption,
lineHeight = 15.sp,
fontWeight = FontWeight.SemiBold,
),
color = MulticaColors.Muted,
maxLines = 1,
)
Text(
text = listOf(parentIssue.identifier, parentIssue.title).filter { it.isNotBlank() }.joinToString(" "),
style = MaterialTheme.typography.bodyMedium.copy(
fontSize = MulticaTypeScale.Body,
lineHeight = 19.sp,
fontWeight = FontWeight.SemiBold,
),
color = MulticaColors.Text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
)
Icon(
imageVector = Icons.Outlined.KeyboardArrowRight,
contentDescription = null,
tint = MulticaColors.Muted,
modifier = Modifier.size(18.dp),
)
}
}
}
@Composable @Composable
private fun IssueDetailHeroCard( private fun IssueDetailHeroCard(
issue: Models.Issue, issue: Models.Issue,
@@ -8869,6 +8935,7 @@ private fun IssueCommentCard(
members = members, members = members,
agents = agents, agents = agents,
currentUser = currentUser, currentUser = currentUser,
attachments = comment.attachments,
onIssueIdClick = onIssueIdClick, onIssueIdClick = onIssueIdClick,
onIssueReferenceClick = onIssueReferenceClick, onIssueReferenceClick = onIssueReferenceClick,
) )
@@ -9270,7 +9337,9 @@ private fun PilotIssueDetail(
return api.agentRuns(requestedIssueId, requestedWorkspaceId) return api.agentRuns(requestedIssueId, requestedWorkspaceId)
} }
override fun projects(): List<Models.Project> = emptyList() override fun projects(): List<Models.Project> = runCatching {
api.projects(workspaceId, 100, 0).items
}.getOrElse { emptyList() }
override fun members(): List<Models.Member> = api.members(workspaceId) override fun members(): List<Models.Member> = api.members(workspaceId)
@@ -9283,11 +9352,13 @@ private fun PilotIssueDetail(
return@LaunchedEffect return@LaunchedEffect
} }
val initialData = PilotIssueDetailData( val initialData = PilotIssueDetailData(
core.issue, issue = core.issue,
core.comments, parentIssue = null,
core.runs, comments = core.comments,
core.members, runs = core.runs,
core.agents, projects = core.projects,
members = core.members,
agents = core.agents,
issueLabels = emptyList(), issueLabels = emptyList(),
allLabels = emptyList(), allLabels = emptyList(),
subscribers = emptyList(), subscribers = emptyList(),
@@ -9304,11 +9375,23 @@ private fun PilotIssueDetail(
val subscribers = runCatching { api.issueSubscribers(workspaceId, issueId) }.getOrElse { emptyList() } val subscribers = runCatching { api.issueSubscribers(workspaceId, issueId) }.getOrElse { emptyList() }
val attachments = runCatching { api.issueAttachments(workspaceId, issueId) }.getOrElse { emptyList() } val attachments = runCatching { api.issueAttachments(workspaceId, issueId) }.getOrElse { emptyList() }
val children = runCatching { api.childIssues(workspaceId, issueId) }.getOrElse { emptyList() } val children = runCatching { api.childIssues(workspaceId, issueId) }.getOrElse { emptyList() }
val parentIssue = core.issue.parentIssueId?.takeIf { it.isNotBlank() }?.let { parentIssueId ->
runCatching { api.issue(parentIssueId, workspaceId) }.getOrNull()
}
val projectId = core.issue.projectId?.takeIf { it.isNotBlank() }
val projects = if (projectId != null && core.projects.none { it.id == projectId }) {
val issueProject = runCatching { api.project(workspaceId, projectId) }.getOrNull()
if (issueProject != null) core.projects + issueProject else core.projects
} else {
core.projects
}
val usage = runCatching { api.issueUsage(workspaceId, issueId) }.getOrNull() val usage = runCatching { api.issueUsage(workspaceId, issueId) }.getOrNull()
val timelineAnchor = highlightCommentId ?: core.comments.lastOrNull()?.id val timelineAnchor = highlightCommentId ?: core.comments.lastOrNull()?.id
val timeline = runCatching { api.issueTimeline(workspaceId, issueId, 10, null, null, timelineAnchor) }.getOrNull() val timeline = runCatching { api.issueTimeline(workspaceId, issueId, 10, null, null, timelineAnchor) }.getOrNull()
val activeTasks = runCatching { api.activeIssueTasks(workspaceId, issueId) }.getOrElse { emptyList() } val activeTasks = runCatching { api.activeIssueTasks(workspaceId, issueId) }.getOrElse { emptyList() }
initialData.copy( initialData.copy(
parentIssue = parentIssue,
projects = projects,
issueLabels = issueLabels, issueLabels = issueLabels,
allLabels = allLabels, allLabels = allLabels,
subscribers = subscribers, subscribers = subscribers,
@@ -9761,6 +9844,10 @@ private fun PilotIssueDetail(
val WEB_SSOT_EXECUTION_LOG_SECTION = "/Users/park0er/coding/multica-web-source/packages/views/issues/components/execution-log-section.tsx" val WEB_SSOT_EXECUTION_LOG_SECTION = "/Users/park0er/coding/multica-web-source/packages/views/issues/components/execution-log-section.tsx"
val WEB_SSOT_PULL_REQUEST_LIST = "/Users/park0er/coding/multica-web-source/packages/views/issues/components/pull-request-list.tsx" val WEB_SSOT_PULL_REQUEST_LIST = "/Users/park0er/coding/multica-web-source/packages/views/issues/components/pull-request-list.tsx"
val WEB_SSOT_TERMINATE_TASK_CONFIRM_DIALOG = "/Users/park0er/coding/multica-web-source/packages/views/issues/components/terminate-task-confirm-dialog.tsx" val WEB_SSOT_TERMINATE_TASK_CONFIRM_DIALOG = "/Users/park0er/coding/multica-web-source/packages/views/issues/components/terminate-task-confirm-dialog.tsx"
val projectName = data.projects.firstOrNull { it.id == data.issue.projectId }?.name
?.takeIf { it.isNotBlank() }
?: data.issue.projectId?.takeIf { it.isNotBlank() }?.let { Models.shortId(it) }
?: if (zh) "无项目" else "No project"
PilotListPage( PilotListPage(
title = "${data.issue.identifier} · ${Models.statusLabel(data.issue.status, zh)}", title = "${data.issue.identifier} · ${Models.statusLabel(data.issue.status, zh)}",
@@ -9819,6 +9906,15 @@ private fun PilotIssueDetail(
} }
}, },
) { ) {
data.parentIssue?.let { parentIssue ->
item {
IssueDetailParentIssueLink(
parentIssue = parentIssue,
zh = zh,
onOpen = { onOpenIssue(parentIssue.id) },
)
}
}
item { item {
IssueDetailHeroCard( IssueDetailHeroCard(
issue = data.issue, issue = data.issue,
@@ -9833,7 +9929,7 @@ private fun PilotIssueDetail(
issue = data.issue, issue = data.issue,
issueLabels = data.issueLabels, issueLabels = data.issueLabels,
assignee = Models.issueAssigneeDisplayName(data.issue, data.members, data.agents, currentUser), assignee = Models.issueAssigneeDisplayName(data.issue, data.members, data.agents, currentUser),
project = data.issue.projectId?.let { Models.shortId(it) } ?: if (zh) "无项目" else "No project", project = projectName,
zh = zh, zh = zh,
) )
} }
@@ -9871,6 +9967,7 @@ private fun PilotIssueDetail(
members = data.members, members = data.members,
agents = data.agents, agents = data.agents,
currentUser = currentUser, currentUser = currentUser,
attachments = data.attachments,
onIssueIdClick = { onOpenIssue(it) }, onIssueIdClick = { onOpenIssue(it) },
onIssueReferenceClick = { openIssueReference(it) }, onIssueReferenceClick = { openIssueReference(it) },
) )
@@ -11430,6 +11527,7 @@ private fun MarkdownBlock(
members: List<Models.Member> = emptyList(), members: List<Models.Member> = emptyList(),
agents: List<Models.Agent> = emptyList(), agents: List<Models.Agent> = emptyList(),
currentUser: Models.User? = null, currentUser: Models.User? = null,
attachments: List<Models.Attachment> = emptyList(),
onIssueIdClick: ((String) -> Unit)? = null, onIssueIdClick: ((String) -> Unit)? = null,
onIssueReferenceClick: ((String) -> Unit)? = null, onIssueReferenceClick: ((String) -> Unit)? = null,
) { ) {
@@ -11462,32 +11560,177 @@ private fun MarkdownBlock(
} }
} }
} }
SelectionContainer( val segments = remember(markdown, attachments) { parseMarkdownImageSegments(markdown, attachments) }
val markdownContext = LocalContext.current
Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.semantics { contentDescription = "Selectable Markdown Block" }, .semantics { contentDescription = "Selectable Markdown Block" },
verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
AndroidView( segments.forEach { segment ->
modifier = Modifier.fillMaxWidth(), when (segment) {
factory = { context -> is MarkdownTextSegment -> {
LinearLayout(context).apply { orientation = LinearLayout.VERTICAL } SelectionContainer {
}, AndroidView(
update = { view -> modifier = Modifier.fillMaxWidth(),
view.removeAllViews() factory = { context ->
MarkdownRenderer.render( LinearLayout(context).apply { orientation = LinearLayout.VERTICAL }
view.context, },
view, update = { view ->
markdown, view.removeAllViews()
textColor, MarkdownRenderer.render(
mutedColor, view.context,
borderColor, view,
linkHandler, segment.text,
) textColor,
}, mutedColor,
borderColor,
linkHandler,
)
},
)
}
}
is MarkdownImageSegment -> {
MulticaMarkdownImage(
image = segment,
onOpen = { url -> openExternalUrl(markdownContext, url) },
)
}
}
}
}
}
private sealed interface MarkdownImageAwareSegment
private data class MarkdownTextSegment(val text: String) : MarkdownImageAwareSegment
private data class MarkdownImageSegment(
val alt: String,
val url: String,
) : MarkdownImageAwareSegment
private val MARKDOWN_IMAGE_LINE_REGEX = Regex("""^!\[([^\]]*)]\(([^)\s]+)(?:\s+"[^"]*")?\)$""")
private fun parseMarkdownImageSegments(
markdown: String,
attachments: List<Models.Attachment>,
): List<MarkdownImageAwareSegment> {
val segments = mutableListOf<MarkdownImageAwareSegment>()
val text = StringBuilder()
fun flushText() {
val value = text.toString().trim('\n')
if (value.isNotBlank()) segments += MarkdownTextSegment(value)
text.clear()
}
markdown.lines().forEach { line ->
val match = MARKDOWN_IMAGE_LINE_REGEX.matchEntire(line.trim())
if (match != null) {
flushText()
val alt = clean(match.groupValues[1]).ifBlank { "image" }
val target = clean(match.groupValues[2]).trim('<', '>')
segments += MarkdownImageSegment(
alt = alt,
url = resolveMarkdownAttachmentImageUrl(target, alt, attachments),
)
} else {
if (text.isNotEmpty()) text.append('\n')
text.append(line)
}
}
flushText()
return segments.ifEmpty { listOf(MarkdownTextSegment(markdown)) }
}
private fun resolveMarkdownAttachmentImageUrl(
target: String,
alt: String,
attachments: List<Models.Attachment>,
): String {
val normalizedTarget = clean(target)
val match = attachments.firstOrNull { attachment ->
val url = clean(attachment.url)
val downloadUrl = clean(attachment.downloadUrl)
val filename = clean(attachment.filename)
url == normalizedTarget ||
downloadUrl == normalizedTarget ||
filename.equals(alt, ignoreCase = true)
}
return clean(match?.downloadUrl).ifBlank {
clean(match?.url).ifBlank { normalizedTarget }
}
}
@Composable
private fun MulticaMarkdownImage(
image: MarkdownImageSegment,
onOpen: (String) -> Unit,
) {
val context = LocalContext.current
val imageUrl = remember(image.url) { absoluteMarkdownImageUrl(image.url) }
val authStore = remember(context) { AuthStore(context.applicationContext) }
val cookieHeader = remember(authStore) { authStore.cloudFrontCookieHeader() }
val token = remember(authStore) { authStore.token().orEmpty() }
val imageRequest = remember(context, imageUrl, cookieHeader, token) {
ImageRequest.Builder(context)
.data(imageUrl)
.crossfade(true)
.apply {
if (token.isNotBlank() && imageUrl.startsWith(BuildConfig.MULTICA_API_BASE_URL)) {
addHeader("Authorization", "Bearer $token")
}
if (cookieHeader.isNotBlank() && imageUrl.contains("static.multica.ai")) {
addHeader("Cookie", cookieHeader)
}
}
.build()
}
Surface(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(14.dp))
.clickable { if (imageUrl.isNotBlank()) onOpen(imageUrl) }
.semantics { contentDescription = "Markdown Image ${image.alt}" },
shape = RoundedCornerShape(14.dp),
color = MulticaColors.Surface.copy(alpha = 0.9f),
tonalElevation = 0.dp,
shadowElevation = 0.dp,
border = androidx.compose.foundation.BorderStroke(0.75.dp, MulticaColors.Border.copy(alpha = 0.48f)),
) {
AsyncImage(
model = imageRequest,
contentDescription = image.alt,
contentScale = ContentScale.Fit,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 140.dp, max = 420.dp)
.background(MulticaColors.SurfaceElevated.copy(alpha = 0.35f))
.padding(6.dp),
) )
} }
} }
private fun absoluteMarkdownImageUrl(url: String): String {
val value = clean(url)
return if (value.startsWith("/")) {
BuildConfig.MULTICA_API_BASE_URL.trimEnd('/') + value
} else {
value
}
}
private fun openExternalUrl(context: Context, url: String) {
val target = clean(url)
if (target.isBlank()) return
runCatching {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(target)))
}
}
@Composable @Composable
private fun SectionHeader(title: String) { private fun SectionHeader(title: String) {
Text( Text(
@@ -20191,24 +20434,33 @@ private fun PilotRuntimesSettings(api: ApiClient, workspaceId: String, zh: Boole
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.semantics { contentDescription = "Runtime Web Table Header" } .semantics { contentDescription = "Runtime Web Table Header Aligned Columns" }
.padding(horizontal = 12.dp, vertical = 6.dp), .padding(horizontal = RuntimeListHorizontalPadding, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(RuntimeListColumnGap),
) { ) {
Spacer(Modifier.width(RuntimeListProviderColumnWidth))
Text( Text(
if (zh) "Runtime" else "Runtime", if (zh) "Runtime" else "Runtime",
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f, fill = true),
style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp, fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp, fontWeight = FontWeight.SemiBold),
color = MulticaColors.TextTertiary, color = MulticaColors.TextTertiary,
) )
Text( Text(
if (zh) "健康" else "Health", if (zh) "健康" else "Health",
modifier = Modifier.width(88.dp), modifier = Modifier.width(RuntimeListHealthColumnWidth),
style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp, fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp, fontWeight = FontWeight.SemiBold),
color = MulticaColors.TextTertiary, color = MulticaColors.TextTertiary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
Spacer(Modifier.width(42.dp)) Text(
if (zh) "版本" else "Version",
modifier = Modifier.width(RuntimeListMetaColumnWidth),
style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp, fontWeight = FontWeight.SemiBold),
color = MulticaColors.TextTertiary,
textAlign = TextAlign.End,
)
Spacer(Modifier.width(RuntimeListActionColumnWidth))
} }
} }
} }
@@ -20358,6 +20610,13 @@ private fun RuntimesWebNoMatchesState(
} }
} }
private val RuntimeListProviderColumnWidth = 34.dp
private val RuntimeListHealthColumnWidth = 72.dp
private val RuntimeListMetaColumnWidth = 76.dp
private val RuntimeListActionColumnWidth = 36.dp
private val RuntimeListHorizontalPadding = 10.dp
private val RuntimeListColumnGap = 8.dp
@Composable @Composable
private fun RuntimeWebDenseRow( private fun RuntimeWebDenseRow(
runtime: Models.Runtime, runtime: Models.Runtime,
@@ -20393,11 +20652,15 @@ private fun RuntimeWebDenseRow(
.fillMaxWidth() .fillMaxWidth()
.border(1.dp, MulticaColors.Border.copy(alpha = 0.58f), RoundedCornerShape(8.dp)) .border(1.dp, MulticaColors.Border.copy(alpha = 0.58f), RoundedCornerShape(8.dp))
.clickable(onClick = onOpen) .clickable(onClick = onOpen)
.padding(horizontal = 12.dp, vertical = 9.dp), .padding(horizontal = RuntimeListHorizontalPadding, vertical = 9.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(RuntimeListColumnGap),
) { ) {
RuntimeProviderMark(provider = runtime.provider, runtimeMode = runtime.runtimeMode) RuntimeProviderMark(
provider = runtime.provider,
runtimeMode = runtime.runtimeMode,
modifier = Modifier.size(RuntimeListProviderColumnWidth),
)
RuntimeRowNameBlock( RuntimeRowNameBlock(
baseName = nameParts.first, baseName = nameParts.first,
hostname = nameParts.second, hostname = nameParts.second,
@@ -20406,11 +20669,11 @@ private fun RuntimeWebDenseRow(
deviceInfo = runtime.deviceInfo, deviceInfo = runtime.deviceInfo,
lastSeenAt = runtime.lastSeenAt, lastSeenAt = runtime.lastSeenAt,
zh = zh, zh = zh,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f, fill = true),
) )
Row( Row(
modifier = Modifier modifier = Modifier
.width(88.dp) .width(RuntimeListHealthColumnWidth)
.semantics { contentDescription = "Runtime Health ${runtimeHealthLabel(health, zh)}" }, .semantics { contentDescription = "Runtime Health ${runtimeHealthLabel(health, zh)}" },
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -20431,7 +20694,7 @@ private fun RuntimeWebDenseRow(
) )
} }
Column( Column(
modifier = Modifier.widthIn(min = 72.dp, max = 96.dp), modifier = Modifier.width(RuntimeListMetaColumnWidth),
verticalArrangement = Arrangement.spacedBy(5.dp), verticalArrangement = Arrangement.spacedBy(5.dp),
horizontalAlignment = Alignment.End, horizontalAlignment = Alignment.End,
) { ) {
@@ -20444,7 +20707,7 @@ private fun RuntimeWebDenseRow(
onClick = { onExpandedChange(true) }, onClick = { onExpandedChange(true) },
tone = if (expanded) MulticaButtonTone.Primary else MulticaButtonTone.Ghost, tone = if (expanded) MulticaButtonTone.Primary else MulticaButtonTone.Ghost,
modifier = Modifier modifier = Modifier
.size(36.dp) .size(RuntimeListActionColumnWidth)
.semantics { contentDescription = "Runtime Actions ${runtime.name}" }, .semantics { contentDescription = "Runtime Actions ${runtime.name}" },
) )
MulticaCupertinoActionSheet( MulticaCupertinoActionSheet(
@@ -20537,7 +20800,7 @@ private fun RuntimeRowNameBlock(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier.semantics { contentDescription = "Runtime Row Name Block ${baseName.ifBlank { hostname ?: "" }}" }, modifier = modifier.semantics { contentDescription = "Runtime Row Name Expanded Layout ${baseName.ifBlank { hostname ?: "" }}" },
verticalArrangement = Arrangement.spacedBy(3.dp), verticalArrangement = Arrangement.spacedBy(3.dp),
) { ) {
Text( Text(
@@ -20545,7 +20808,7 @@ private fun RuntimeRowNameBlock(
modifier = Modifier.semantics { contentDescription = "Runtime Row Base Name ${baseName.ifBlank { "untitled" }}" }, modifier = Modifier.semantics { contentDescription = "Runtime Row Base Name ${baseName.ifBlank { "untitled" }}" },
style = MaterialTheme.typography.titleSmall.copy(fontSize = 14.sp, lineHeight = 18.sp, fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.titleSmall.copy(fontSize = 14.sp, lineHeight = 18.sp, fontWeight = FontWeight.SemiBold),
color = MulticaColors.Text, color = MulticaColors.Text,
maxLines = 1, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
Text( Text(
@@ -21321,11 +21584,11 @@ private fun RuntimeDetailWebDenseRow(
fontSize = 13.sp, fontSize = 13.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
), ),
color = MulticaColors.Text, color = MulticaColors.Text,
maxLines = 1, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
if (!subtitle.isNullOrBlank()) { if (!subtitle.isNullOrBlank()) {
Text( Text(
text = subtitle, text = subtitle,
@@ -0,0 +1,10 @@
package ai.multica.app;
import android.app.Application;
public final class MulticaApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
}
}