Prepare Multi-Casual public analytics release

This commit is contained in:
Xisheng-Zhao
2026-06-03 02:15:20 +08:00
parent 392b961fb6
commit 600aa41b1f
10 changed files with 390 additions and 24 deletions
+27 -5
View File
@@ -4,6 +4,18 @@ plugins {
id "org.jetbrains.kotlin.plugin.compose" id "org.jetbrains.kotlin.plugin.compose"
} }
def umengPublicAppKey = providers.gradleProperty("multicaUmengPublicAppKey")
.orElse("")
.get()
def postHogPublicApiKey = providers.gradleProperty("multicaPostHogPublicApiKey")
.orElse("")
.get()
def postHogPublicHost = providers.gradleProperty("multicaPostHogPublicHost")
.orElse("https://us.i.posthog.com")
.get()
def analyticsRoute = providers.gradleProperty("multicaAnalyticsRoute")
.orElse("auto")
.get()
android { android {
namespace "ai.multica.app" namespace "ai.multica.app"
@@ -11,7 +23,7 @@ android {
buildToolsVersion "36.1.0" buildToolsVersion "36.1.0"
defaultConfig { defaultConfig {
applicationId "ai.multica.app" applicationId "ai.multicasual.app"
minSdk 24 minSdk 24
targetSdk 36 targetSdk 36
versionCode 1 versionCode 1
@@ -37,14 +49,21 @@ android {
productFlavors { productFlavors {
create("public") { create("public") {
dimension "distribution" dimension "distribution"
applicationId "ai.multica.app" applicationId "ai.multicasual.app"
resValue "string", "app_name", "Multica" resValue "string", "app_name", "Multi-Casual"
manifestPlaceholders = [usesCleartextTraffic: "false", deepLinkScheme: "multica"] manifestPlaceholders = [usesCleartextTraffic: "false", deepLinkScheme: "multi-casual"]
buildConfigField "String", "MULTICA_ENVIRONMENT", "\"public\"" buildConfigField "String", "MULTICA_ENVIRONMENT", "\"public\""
buildConfigField "String", "MULTICA_API_BASE_URL", "\"https://api.multica.ai\"" buildConfigField "String", "MULTICA_API_BASE_URL", "\"https://api.multica.ai\""
buildConfigField "String", "MULTICA_WEB_BASE_URL", "\"https://app.multica.ai\"" buildConfigField "String", "MULTICA_WEB_BASE_URL", "\"https://app.multica.ai\""
buildConfigField "String", "MULTICA_WS_BASE_URL", "\"wss://api.multica.ai\"" buildConfigField "String", "MULTICA_WS_BASE_URL", "\"wss://api.multica.ai\""
buildConfigField "String", "MULTICA_AUTH_PREFS_NAME", "\"multica_auth\"" buildConfigField "String", "MULTICA_AUTH_PREFS_NAME", "\"multi_casual_auth\""
buildConfigField "String", "ANALYTICS_ROUTE", "\"${analyticsRoute}\""
buildConfigField "boolean", "UMENG_ENABLED", "true"
buildConfigField "String", "UMENG_APP_KEY", "\"${umengPublicAppKey}\""
buildConfigField "String", "UMENG_CHANNEL", "\"public\""
buildConfigField "boolean", "POSTHOG_ENABLED", "true"
buildConfigField "String", "POSTHOG_API_KEY", "\"${postHogPublicApiKey}\""
buildConfigField "String", "POSTHOG_HOST", "\"${postHogPublicHost}\""
} }
} }
@@ -56,6 +75,9 @@ android {
} }
dependencies { dependencies {
publicImplementation "com.umeng.umsdk:common:9.9.2"
publicImplementation "com.umeng.umsdk:asms:1.8.7.2"
publicImplementation "com.posthog:posthog-android:3.47.0"
implementation platform("androidx.compose:compose-bom:2024.10.01") implementation platform("androidx.compose:compose-bom:2024.10.01")
implementation "androidx.activity:activity-compose:1.9.3" implementation "androidx.activity:activity-compose:1.9.3"
implementation "androidx.compose.foundation:foundation" implementation "androidx.compose.foundation:foundation"
+2
View File
@@ -0,0 +1,2 @@
-keep class com.umeng.** { *; }
-keep class com.posthog.** { *; }
@@ -0,0 +1,40 @@
package ai.multica.app;
import android.content.Context;
import android.content.SharedPreferences;
final class AnalyticsConsentStore {
private static final String PREFS_SUFFIX = "_analytics";
private static final String CONSENT_DECIDED = "analytics_consent_decided";
private static final String CONSENT_GRANTED = "analytics_consent_granted";
private final SharedPreferences prefs;
AnalyticsConsentStore(Context context) {
prefs = context.getApplicationContext()
.getSharedPreferences(BuildConfig.MULTICA_AUTH_PREFS_NAME + PREFS_SUFFIX, Context.MODE_PRIVATE);
}
boolean hasDecision() {
return prefs.getBoolean(CONSENT_DECIDED, false);
}
boolean isGranted() {
return prefs.getBoolean(CONSENT_DECIDED, false)
&& prefs.getBoolean(CONSENT_GRANTED, false);
}
void grant() {
prefs.edit()
.putBoolean(CONSENT_DECIDED, true)
.putBoolean(CONSENT_GRANTED, true)
.apply();
}
void deny() {
prefs.edit()
.putBoolean(CONSENT_DECIDED, true)
.putBoolean(CONSENT_GRANTED, false)
.apply();
}
}
@@ -0,0 +1,277 @@
package ai.multica.app;
import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
public final class AppAnalytics {
private static final String TAG = "MultiCasualAnalytics";
static final String EVENT_APP_OPEN = "app_open";
private static volatile boolean initialized;
private static volatile boolean appOpenTracked;
private static AnalyticsProvider provider = AnalyticsProvider.NO_OP;
private AppAnalytics() {}
public static boolean shouldPromptForConsent(Context context) {
return !new AnalyticsConsentStore(context).hasDecision();
}
public static void grantConsentAndInitialize(Context context) {
new AnalyticsConsentStore(context).grant();
initializeIfAllowed(context);
}
public static void denyConsent(Context context) {
new AnalyticsConsentStore(context).deny();
provider.optOut();
}
public static void initializeIfAllowed(Context context) {
Context appContext = context.getApplicationContext();
if (!new AnalyticsConsentStore(appContext).isGranted()) {
Log.i(TAG, "Analytics disabled until user grants consent.");
return;
}
if (initialized) {
trackAppOpenOnce(appContext);
return;
}
synchronized (AppAnalytics.class) {
if (initialized) {
trackAppOpenOnce(appContext);
return;
}
provider = createProvider(appContext);
initialized = true;
}
trackAppOpenOnce(appContext);
}
static String selectedRouteForTesting(Context context) {
return selectRoute(context);
}
private static AnalyticsProvider createProvider(Context context) {
String route = selectRoute(context);
Log.i(TAG, "Analytics route=" + route + " environment=" + BuildConfig.MULTICA_ENVIRONMENT);
if ("cn".equals(route)) {
return UmengAnalyticsProvider.create(context);
}
if ("global".equals(route)) {
return PostHogAnalyticsProvider.create(context);
}
return AnalyticsProvider.NO_OP;
}
private static String selectRoute(Context context) {
String forced = safe(BuildConfig.ANALYTICS_ROUTE).toLowerCase(Locale.US);
if ("cn".equals(forced) || "global".equals(forced) || "none".equals(forced)) {
return forced;
}
if (isLikelyMainlandChina(context)) {
return "cn";
}
return "global";
}
private static boolean isLikelyMainlandChina(Context context) {
Locale locale = Build.VERSION.SDK_INT >= 24
? context.getResources().getConfiguration().getLocales().get(0)
: context.getResources().getConfiguration().locale;
String country = locale == null ? "" : safe(locale.getCountry()).toUpperCase(Locale.US);
if ("CN".equals(country)) {
return true;
}
String language = locale == null ? "" : safe(locale.getLanguage()).toLowerCase(Locale.US);
String timeZone = TimeZone.getDefault().getID();
return "zh".equals(language)
&& ("Asia/Shanghai".equals(timeZone)
|| "Asia/Chongqing".equals(timeZone)
|| "Asia/Harbin".equals(timeZone)
|| "Asia/Urumqi".equals(timeZone));
}
private static void trackAppOpenOnce(Context context) {
if (appOpenTracked) {
return;
}
synchronized (AppAnalytics.class) {
if (appOpenTracked) {
return;
}
appOpenTracked = true;
}
provider.trackAppOpen(appOpenProperties(context));
}
private static Map<String, Object> appOpenProperties(Context context) {
Map<String, Object> params = new LinkedHashMap<>();
params.put("app_version", BuildConfig.VERSION_NAME);
params.put("build_flavor", BuildConfig.MULTICA_ENVIRONMENT);
params.put("distribution_channel", "public");
params.put("analytics_route", selectRoute(context));
return params;
}
private static String safe(String value) {
return value == null ? "" : value.trim();
}
interface AnalyticsProvider {
AnalyticsProvider NO_OP = new AnalyticsProvider() {
@Override
public void trackAppOpen(Map<String, Object> properties) {}
@Override
public void optOut() {}
};
void trackAppOpen(Map<String, Object> properties);
void optOut();
}
private static final class UmengAnalyticsProvider implements AnalyticsProvider {
private final Context context;
private final Method onEventObjectMethod;
private UmengAnalyticsProvider(Context context, Method onEventObjectMethod) {
this.context = context.getApplicationContext();
this.onEventObjectMethod = onEventObjectMethod;
}
static AnalyticsProvider create(Context context) {
if (!BuildConfig.UMENG_ENABLED || TextUtils.isEmpty(safe(BuildConfig.UMENG_APP_KEY))) {
Log.w(TAG, "Umeng selected but UMENG_APP_KEY is empty; analytics not started.");
return NO_OP;
}
Context appContext = context.getApplicationContext();
try {
Class<?> configureClass = Class.forName("com.umeng.commonsdk.UMConfigure");
Class<?> mobclickClass = Class.forName("com.umeng.analytics.MobclickAgent");
Class<?> pageModeClass = Class.forName("com.umeng.analytics.MobclickAgent$PageMode");
Object manualMode = Enum.valueOf((Class<Enum>) pageModeClass.asSubclass(Enum.class), "MANUAL");
mobclickClass.getMethod("setPageCollectionMode", pageModeClass).invoke(null, manualMode);
mobclickClass.getMethod("setCatchUncaughtExceptions", boolean.class).invoke(null, false);
configureClass.getMethod("setLogEnabled", boolean.class).invoke(null, BuildConfig.DEBUG);
configureClass.getMethod("setProcessEvent", boolean.class).invoke(null, true);
configureClass.getMethod("init", Context.class, String.class, String.class, int.class, String.class)
.invoke(null, appContext, BuildConfig.UMENG_APP_KEY, BuildConfig.UMENG_CHANNEL,
configureClass.getField("DEVICE_TYPE_PHONE").getInt(null), null);
Method onEventObject = mobclickClass.getMethod("onEventObject", Context.class, String.class, Map.class);
Log.i(TAG, "Umeng analytics initialized.");
return new UmengAnalyticsProvider(appContext, onEventObject);
} catch (Throwable error) {
Log.w(TAG, "Umeng initialization failed; app launch continues.", error);
return NO_OP;
}
}
@Override
public void trackAppOpen(Map<String, Object> properties) {
try {
onEventObjectMethod.invoke(null, context, EVENT_APP_OPEN, properties);
Log.i(TAG, "Umeng app_open tracked.");
} catch (Throwable error) {
Log.w(TAG, "Umeng app_open failed; app continues.", error);
}
}
@Override
public void optOut() {}
}
private static final class PostHogAnalyticsProvider implements AnalyticsProvider {
private final Object postHog;
private final Method captureMethod;
private final Method optOutMethod;
private PostHogAnalyticsProvider(Object postHog, Method captureMethod, Method optOutMethod) {
this.postHog = postHog;
this.captureMethod = captureMethod;
this.optOutMethod = optOutMethod;
}
static AnalyticsProvider create(Context context) {
if (!BuildConfig.POSTHOG_ENABLED || TextUtils.isEmpty(safe(BuildConfig.POSTHOG_API_KEY))) {
Log.w(TAG, "PostHog selected but POSTHOG_API_KEY is empty; analytics not started.");
return NO_OP;
}
try {
Class<?> configClass = Class.forName("com.posthog.android.PostHogAndroidConfig");
Object config = configClass.getConstructor(String.class, String.class)
.newInstance(BuildConfig.POSTHOG_API_KEY, BuildConfig.POSTHOG_HOST);
invokeSetter(configClass, config, "setCaptureApplicationLifecycleEvents", false);
invokeSetter(configClass, config, "setCaptureScreenViews", false);
invokeSetter(configClass, config, "setCaptureDeepLinks", false);
invokeSetter(configClass, config, "setSessionReplay", false);
invokeSetter(configClass, config, "setPreloadFeatureFlags", false);
invokeSetter(configClass, config, "setRemoteConfig", false);
invokeSetter(configClass, config, "setSurveys", false);
invokeSetter(configClass, config, "setSetDefaultPersonProperties", false);
invokeSetter(configClass, config, "setSendFeatureFlagEvent", false);
invokeSetter(configClass, config, "setDebug", BuildConfig.DEBUG);
Class<?> postHogAndroidClass = Class.forName("com.posthog.android.PostHogAndroid");
Field companionField = postHogAndroidClass.getField("Companion");
Object companion = companionField.get(null);
Object shared = companion.getClass()
.getMethod("with", Context.class, configClass)
.invoke(companion, context.getApplicationContext(), config);
Class<?> interfaceClass = Class.forName("com.posthog.PostHogInterface");
Method capture = interfaceClass.getMethod(
"capture",
String.class,
String.class,
Map.class,
Map.class,
Map.class,
Map.class,
Date.class
);
Method optOut = interfaceClass.getMethod("optOut");
Log.i(TAG, "PostHog analytics initialized.");
return new PostHogAnalyticsProvider(shared, capture, optOut);
} catch (Throwable error) {
Log.w(TAG, "PostHog initialization failed; app launch continues.", error);
return NO_OP;
}
}
@Override
public void trackAppOpen(Map<String, Object> properties) {
try {
captureMethod.invoke(postHog, EVENT_APP_OPEN, null, properties, null, null, null, null);
Log.i(TAG, "PostHog app_open tracked.");
} catch (Throwable error) {
Log.w(TAG, "PostHog app_open failed; app continues.", error);
}
}
@Override
public void optOut() {
try {
optOutMethod.invoke(postHog);
} catch (Throwable error) {
Log.w(TAG, "PostHog optOut failed; app continues.", error);
}
}
private static void invokeSetter(Class<?> configClass, Object config, String methodName, boolean value) {
try {
configClass.getMethod(methodName, boolean.class).invoke(config, value);
} catch (Throwable ignored) {
}
}
}
}
@@ -548,8 +548,11 @@ private fun ComposePilotApp(
initialIssueId: String? = null, initialIssueId: String? = null,
initialTabName: String? = null, initialTabName: String? = null,
) { ) {
val context = LocalContext.current
var appState by remember { mutableStateOf<PilotState>(PilotState.Loading) } var appState by remember { mutableStateOf<PilotState>(PilotState.Loading) }
var refresh by remember { mutableIntStateOf(0) } var refresh by remember { mutableIntStateOf(0) }
var showAnalyticsConsent by remember { mutableStateOf(AppAnalytics.shouldPromptForConsent(context)) }
val zh = authStore.isChinese()
LaunchedEffect(refresh) { LaunchedEffect(refresh) {
appState = PilotState.Loading appState = PilotState.Loading
@@ -612,6 +615,27 @@ private fun ComposePilotApp(
onSessionUpdated = { appState = PilotState.Ready(it) }, onSessionUpdated = { appState = PilotState.Ready(it) },
) )
} }
MulticaCupertinoAlertDialog(
visible = showAnalyticsConsent,
title = if (zh) "允许基础使用统计?" else "Allow basic usage analytics?",
message = if (zh) {
"Multi-Casual 只会上报 app_open 启动事件,用于统计用户量和日活。不会采集页面浏览、聊天内容、文件内容、邮箱、Token 或精确位置。中国大陆用户走友盟+,海外用户走 PostHog。你可以选择不同意,应用仍可继续使用。"
} else {
"Multi-Casual only reports the app_open event to measure user count and daily active users. It does not collect page views, chat content, file content, email, tokens, or precise location. Mainland China routes to Umeng+, while global users route to PostHog. You can decline and still use the app."
},
cancelText = if (zh) "不同意" else "Decline",
confirmText = if (zh) "同意并继续" else "Agree",
contentDescription = "Analytics Consent",
onDismissRequest = {
AppAnalytics.denyConsent(context)
showAnalyticsConsent = false
},
onConfirm = {
AppAnalytics.grantConsentAndInitialize(context)
showAnalyticsConsent = false
},
)
} }
@Composable @Composable
@@ -710,7 +734,7 @@ private fun PilotLoginScreen(
) )
} }
Text( Text(
text = if (zh) "登录 Multica" else "Sign in to Multica", text = if (zh) "登录 Multi-Casual" else "Sign in to Multi-Casual",
style = MaterialTheme.typography.titleLarge.copy(fontSize = 30.sp, lineHeight = 36.sp, fontWeight = FontWeight.Bold), style = MaterialTheme.typography.titleLarge.copy(fontSize = 30.sp, lineHeight = 36.sp, fontWeight = FontWeight.Bold),
color = MulticaColors.Text, color = MulticaColors.Text,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@@ -983,7 +1007,7 @@ private fun PilotNoWorkspaceOnboarding(
verticalArrangement = Arrangement.spacedBy(7.dp), verticalArrangement = Arrangement.spacedBy(7.dp),
) { ) {
Text( Text(
text = "Multica", text = "Multi-Casual",
style = MaterialTheme.typography.titleLarge.copy(fontSize = 28.sp, lineHeight = 33.sp), style = MaterialTheme.typography.titleLarge.copy(fontSize = 28.sp, lineHeight = 33.sp),
color = MulticaColors.Text, color = MulticaColors.Text,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -2023,7 +2047,7 @@ private fun PilotSearchPage(
} }
PilotListPage( PilotListPage(
title = if (zh) "搜索 Multica" else "Search Multica", title = if (zh) "搜索 Multi-Casual" else "Search Multi-Casual",
leading = { PilotSecondaryBackButton(zh = zh, onBack = onBack) }, leading = { PilotSecondaryBackButton(zh = zh, onBack = onBack) },
) { ) {
item { item {
@@ -3846,7 +3870,7 @@ private fun ChatWindowWebEmptyState(
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
MulticaEmptyState( MulticaEmptyState(
title = if (zh) "欢迎使用 Multica" else "Welcome to Multica", title = if (zh) "欢迎使用 Multi-Casual" else "Welcome to Multi-Casual",
description = if (zh) "试试问" else "Try asking", description = if (zh) "试试问" else "Try asking",
) )
Column( Column(
@@ -13444,11 +13468,11 @@ private fun PilotInlineResultBannersPreview() {
.put("id", "comment-preview") .put("id", "comment-preview")
.put("content", "Inbox opened this issue at the exact comment.") .put("content", "Inbox opened this issue at the exact comment.")
.put("author_type", "member") .put("author_type", "member")
.put("author_name", "Multica") .put("author_name", "Multi-Casual")
.put("created_at", "2026-05-14T08:00:00Z") .put("created_at", "2026-05-14T08:00:00Z")
) )
}, },
authorName = "Multica", authorName = "Multi-Casual",
expandedActions = false, expandedActions = false,
contentExpanded = true, contentExpanded = true,
highlighted = true, highlighted = true,
@@ -24654,7 +24678,7 @@ private fun feedbackTypeOptions(zh: Boolean): List<FeedbackTypeOption> = listOf(
FeedbackTypeOption( FeedbackTypeOption(
key = "feature", key = "feature",
label = if (zh) "功能建议" else "Feature request", label = if (zh) "功能建议" else "Feature request",
detail = if (zh) "一个希望 Multica 支持的新能力。" else "A capability you want Multica to support.", detail = if (zh) "一个希望 Multi-Casual 支持的新能力。" else "A capability you want Multi-Casual to support.",
icon = Icons.Outlined.Bolt, icon = Icons.Outlined.Bolt,
), ),
FeedbackTypeOption( FeedbackTypeOption(
@@ -26673,9 +26697,9 @@ private fun AgentsWebEmptyState(
MulticaEmptyState( MulticaEmptyState(
title = if (zh) "还没有智能体" else "No agents yet", title = if (zh) "还没有智能体" else "No agents yet",
description = if (zh) { description = if (zh) {
"创建一个智能体,像分配给同事那样把 issue 交给它。本地智能体在你的机器上运行,云智能体在 Multica 运行时上运行。" "创建一个智能体,像分配给同事那样把 issue 交给它。本地智能体在你的机器上运行,云智能体在 Multi-Casual 运行时上运行。"
} else { } else {
"Create an agent and assign it issues, like any teammate. Local agents run on your machine; cloud agents run on Multica's runtime." "Create an agent and assign it issues, like any teammate. Local agents run on your machine; cloud agents run on Multi-Casual's runtime."
}, },
) )
} }
@@ -247,7 +247,7 @@ public final class MainActivity extends Activity {
header.setGravity(Gravity.CENTER_VERTICAL); header.setGravity(Gravity.CENTER_VERTICAL);
header.setPadding(dp(18), dp(12), dp(14), dp(10)); header.setPadding(dp(18), dp(12), dp(14), dp(10));
header.setBackgroundColor(0xFFFFFFFF); header.setBackgroundColor(0xFFFFFFFF);
TextView title = label("Multica", 22, TEXT, true); TextView title = label("Multi-Casual", 22, TEXT, true);
header.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); header.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
if (!workspaces.isEmpty()) { if (!workspaces.isEmpty()) {
@@ -2190,7 +2190,7 @@ public final class MainActivity extends Activity {
switch (key) { switch (key) {
case "loading": return "加载中..."; case "loading": return "加载中...";
case "sessionExpired": return "登录已过期,请重新登录"; case "sessionExpired": return "登录已过期,请重新登录";
case "signIn": return "登录 Multica"; case "signIn": return "登录 Multi-Casual";
case "signInHint": return "输入邮箱获取登录验证码"; case "signInHint": return "输入邮箱获取登录验证码";
case "continue": return "继续"; case "continue": return "继续";
case "google": return "使用 Google 继续"; case "google": return "使用 Google 继续";
@@ -2303,7 +2303,7 @@ public final class MainActivity extends Activity {
switch (key) { switch (key) {
case "loading": return "Loading..."; case "loading": return "Loading...";
case "sessionExpired": return "Session expired. Please sign in again."; case "sessionExpired": return "Session expired. Please sign in again.";
case "signIn": return "Sign in to Multica"; case "signIn": return "Sign in to Multi-Casual";
case "signInHint": return "Enter your email to get a login code"; case "signInHint": return "Enter your email to get a login code";
case "continue": return "Continue"; case "continue": return "Continue";
case "google": return "Continue with Google"; case "google": return "Continue with Google";
@@ -6,5 +6,6 @@ public final class MulticaApplication extends Application {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
AppAnalytics.initializeIfAllowed(this);
} }
} }
@@ -19,11 +19,11 @@ final class OnboardingPayload {
JSONObject payload = new JSONObject(); JSONObject payload = new JSONObject();
payload.put("workspace_id", workspaceId); payload.put("workspace_id", workspaceId);
payload.put("project", new JSONObject() payload.put("project", new JSONObject()
.put("title", "Getting Started with Multica") .put("title", "Getting Started with Multi-Casual")
.put("description", "A lightweight starter project for learning Multica on mobile.") .put("description", "A lightweight starter project for learning Multi-Casual on mobile.")
.put("icon", "sparkles")); .put("icon", "sparkles"));
payload.put("welcome_issue_template", new JSONObject() payload.put("welcome_issue_template", new JSONObject()
.put("title", "Welcome to Multica") .put("title", "Welcome to Multi-Casual")
.put("description", "Use this issue to test comments, Markdown, status changes, and agent collaboration.") .put("description", "Use this issue to test comments, Markdown, status changes, and agent collaboration.")
.put("priority", "high")); .put("priority", "high"));
payload.put("agent_guided_sub_issues", new JSONArray() payload.put("agent_guided_sub_issues", new JSONArray()
@@ -222,7 +222,7 @@ private fun MulticaGlobalActionsBar(
) )
} }
Text( Text(
text = workspaceName.ifBlank { "Multica" }, text = workspaceName.ifBlank { "Multi-Casual" },
style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.SemiBold),
color = MulticaColors.Text, color = MulticaColors.Text,
maxLines = 1, maxLines = 1,
@@ -909,14 +909,14 @@ fun MulticaBottomNav(
Modifier.hazeEffect( Modifier.hazeEffect(
hazeState, hazeState,
style = HazeDefaults.style( style = HazeDefaults.style(
backgroundColor = MulticaColors.SurfaceElevated.copy(alpha = 0.72f), backgroundColor = MulticaColors.SurfaceElevated,
blurRadius = 28.dp, blurRadius = 28.dp,
noiseFactor = 0.07f, noiseFactor = 0.07f,
), ),
) )
} }
) )
.background(MulticaColors.SurfaceElevated.copy(alpha = 0.72f)) .background(MulticaColors.SurfaceElevated)
.border(0.5.dp, MulticaColors.Border.copy(alpha = 0.62f), RoundedCornerShape(28.dp)) .border(0.5.dp, MulticaColors.Border.copy(alpha = 0.62f), RoundedCornerShape(28.dp))
.padding(horizontal = 6.dp, vertical = 6.dp), .padding(horizontal = 6.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(3.dp), horizontalArrangement = Arrangement.spacedBy(3.dp),
+1 -1
View File
@@ -1,3 +1,3 @@
<resources> <resources>
<string name="app_name">Multica</string> <string name="app_name">Multi-Casual</string>
</resources> </resources>