Integrate master agent runtime orchestration updates

This commit is contained in:
kris
2026-04-16 04:41:46 +08:00
parent e0c0ea1814
commit 39be49630f
81 changed files with 9283 additions and 448 deletions

View File

@@ -119,6 +119,10 @@ public class BossApiClient {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/dispatch-plans", null);
}
public ApiResponse getConversationParticipants(String projectId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/participants", null);
}
public ApiResponse getProjectAgentControls(String projectId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/agent-controls", null);
}
@@ -134,6 +138,76 @@ public class BossApiClient {
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
}
public ApiResponse updateProjectAgentControls(
String projectId,
@Nullable String modelOverride,
@Nullable String reasoningEffortOverride,
@Nullable String fastModelOverride,
@Nullable String fastReasoningEffortOverride,
@Nullable String smartModelOverride,
@Nullable String smartReasoningEffortOverride
) throws IOException, JSONException {
JSONObject payload = buildProjectAgentControlsPayload(
modelOverride,
reasoningEffortOverride,
fastModelOverride,
fastReasoningEffortOverride,
smartModelOverride,
smartReasoningEffortOverride,
false
);
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
}
public ApiResponse updateProjectAgentControls(
String projectId,
@Nullable String modelOverride,
@Nullable String reasoningEffortOverride,
@Nullable String fastModelOverride,
@Nullable String fastReasoningEffortOverride,
@Nullable String smartModelOverride,
@Nullable String smartReasoningEffortOverride,
boolean includeAdvancedOverrides
) throws IOException, JSONException {
JSONObject payload = buildProjectAgentControlsPayload(
modelOverride,
reasoningEffortOverride,
fastModelOverride,
fastReasoningEffortOverride,
smartModelOverride,
smartReasoningEffortOverride,
includeAdvancedOverrides
);
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
}
static JSONObject buildProjectAgentControlsPayload(
@Nullable String modelOverride,
@Nullable String reasoningEffortOverride,
@Nullable String fastModelOverride,
@Nullable String fastReasoningEffortOverride,
@Nullable String smartModelOverride,
@Nullable String smartReasoningEffortOverride,
boolean includeAdvancedOverrides
) throws JSONException {
JSONObject payload = new JSONObject();
payload.put("modelOverride", modelOverride == null ? JSONObject.NULL : modelOverride);
payload.put("reasoningEffortOverride", reasoningEffortOverride == null ? JSONObject.NULL : reasoningEffortOverride);
boolean hasAdvancedOverrides =
fastModelOverride != null
|| fastReasoningEffortOverride != null
|| smartModelOverride != null
|| smartReasoningEffortOverride != null;
if (includeAdvancedOverrides || hasAdvancedOverrides) {
payload.put("fastModelOverride", fastModelOverride == null ? JSONObject.NULL : fastModelOverride);
payload.put("fastReasoningEffortOverride", fastReasoningEffortOverride == null ? JSONObject.NULL : fastReasoningEffortOverride);
payload.put("smartModelOverride", smartModelOverride == null ? JSONObject.NULL : smartModelOverride);
payload.put("smartReasoningEffortOverride", smartReasoningEffortOverride == null ? JSONObject.NULL : smartReasoningEffortOverride);
}
return payload;
}
public ApiResponse updateProjectAgentControls(
String projectId,
@Nullable String modelOverride,
@@ -263,10 +337,6 @@ public class BossApiClient {
return requestWithRestore("POST", "/api/v1/group-chats", payload == null ? new JSONObject() : payload);
}
public ApiResponse getConversationParticipants(String projectId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/participants", null);
}
public ApiResponse getThreadStatus(String projectId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/thread-status", null);
}

View File

@@ -26,6 +26,11 @@ import androidx.annotation.Nullable;
import androidx.core.widget.ImageViewCompat;
import android.content.res.ColorStateList;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
public final class BossUi {
private static final int[] AVATAR_BG_COLORS = {
Color.parseColor("#1EC76F"),
@@ -1229,6 +1234,195 @@ public final class BossUi {
return buildMessageBubble(context, effectiveSender, body, "发送中", true, null);
}
public static LinearLayout buildExecutionWarningCard(
Context context,
String title,
@Nullable String summary,
boolean outgoing
) {
LinearLayout card = new LinearLayout(context);
card.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.topMargin = dp(context, 8);
card.setLayoutParams(params);
card.setPadding(dp(context, 12), dp(context, 10), dp(context, 12), dp(context, 10));
card.setBackground(createRoundedBackground(
outgoing ? Color.parseColor("#FFF1D7") : Color.parseColor("#FFF7E7"),
dp(context, 14)
));
TextView titleView = new TextView(context);
titleView.setText(TextUtils.isEmpty(title) ? "执行提醒" : title);
titleView.setTextSize(12);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(Color.parseColor("#B36B00"));
card.addView(titleView);
if (!TextUtils.isEmpty(summary)) {
TextView summaryView = new TextView(context);
summaryView.setText(summary);
summaryView.setTextSize(13);
summaryView.setLineSpacing(0f, 1.2f);
summaryView.setTextColor(context.getColor(R.color.boss_text_primary));
summaryView.setPadding(0, dp(context, 4), 0, 0);
summaryView.setMaxWidth(Math.round(context.getResources().getDisplayMetrics().widthPixels * 0.62f));
summaryView.setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY);
summaryView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL);
card.addView(summaryView);
}
return card;
}
public static LinearLayout buildMessageStatusRow(
Context context,
JSONObject message,
@Nullable JSONObject conversationTask,
List<JSONObject> warnings,
boolean outgoing
) {
LinearLayout row = new LinearLayout(context);
row.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
rowParams.topMargin = dp(context, 8);
row.setLayoutParams(rowParams);
boolean hasTask = conversationTask != null;
boolean hasWarnings = warnings != null && !warnings.isEmpty();
String detailText = buildStatusDetailText(message, conversationTask, warnings);
boolean hasDetail = !TextUtils.isEmpty(detailText);
if (!hasTask && !hasWarnings && !hasDetail) {
row.setVisibility(View.GONE);
return row;
}
if (hasTask || hasWarnings) {
LinearLayout pillRow = new LinearLayout(context);
pillRow.setOrientation(LinearLayout.HORIZONTAL);
pillRow.setGravity(Gravity.CENTER_VERTICAL);
row.addView(pillRow);
if (hasTask) {
pillRow.addView(buildStatusPill(
context,
taskStatusLabel(conversationTask.optString("status", "")),
outgoing ? Color.parseColor("#D7F3DF") : Color.parseColor("#EDF7F0"),
Color.parseColor("#215B39")
));
}
if (hasWarnings) {
if (pillRow.getChildCount() > 0) {
View spacer = new View(context);
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(dp(context, 6), 1);
spacer.setLayoutParams(spacerParams);
pillRow.addView(spacer);
}
pillRow.addView(buildStatusPill(
context,
warningBadgeLabel(warnings),
outgoing ? Color.parseColor("#FFF1D7") : Color.parseColor("#FFF7E7"),
Color.parseColor("#B36B00")
));
}
}
if (hasDetail) {
TextView detailView = new TextView(context);
detailView.setText(detailText);
detailView.setTextSize(12);
detailView.setTextColor(context.getColor(R.color.boss_text_muted));
detailView.setPadding(dp(context, 2), dp(context, 4), dp(context, 2), 0);
detailView.setMaxLines(2);
detailView.setEllipsize(TextUtils.TruncateAt.END);
detailView.setMaxWidth(Math.round(context.getResources().getDisplayMetrics().widthPixels * 0.62f));
row.addView(detailView);
}
return row;
}
private static TextView buildStatusPill(
Context context,
String text,
int backgroundColor,
int textColor
) {
TextView pill = new TextView(context);
pill.setText(text);
pill.setTextSize(11);
pill.setTypeface(Typeface.DEFAULT_BOLD);
pill.setTextColor(textColor);
pill.setPadding(dp(context, 10), dp(context, 5), dp(context, 10), dp(context, 5));
pill.setBackground(createRoundedBackground(backgroundColor, dp(context, 12)));
return pill;
}
private static String taskStatusLabel(String status) {
switch (status) {
case "queued":
return "排队中";
case "running":
return "执行中";
case "completed":
return "已完成";
case "failed":
return "已失败";
default:
return TextUtils.isEmpty(status) ? "处理中" : status;
}
}
private static String warningBadgeLabel(List<JSONObject> warnings) {
int totalWarnings = 0;
for (JSONObject warning : warnings) {
if (warning == null) {
continue;
}
totalWarnings += Math.max(1, warning.optInt("count", 1));
}
return totalWarnings > 1 ? "提醒 " + totalWarnings : "执行提醒";
}
private static String buildStatusDetailText(
JSONObject message,
@Nullable JSONObject conversationTask,
List<JSONObject> warnings
) {
ArrayList<String> parts = new ArrayList<>();
if (conversationTask != null) {
String sessionId = conversationTask.optString("sessionId", "");
String taskId = conversationTask.optString("taskId", "");
if (!TextUtils.isEmpty(sessionId)) {
parts.add("Session " + sessionId);
} else if (!TextUtils.isEmpty(taskId)) {
parts.add("Task " + taskId);
}
}
if (warnings != null && !warnings.isEmpty()) {
JSONObject firstWarning = warnings.get(0);
if (firstWarning != null) {
String summary = firstWarning.optString("summary", "");
String title = firstWarning.optString("title", "执行提醒");
int count = Math.max(1, firstWarning.optInt("count", 1));
String warningText = !TextUtils.isEmpty(summary) ? summary : title;
if (count > 1) {
warningText = warningText + "" + count + "";
} else if (warnings.size() > 1) {
warningText = warningText + "" + warnings.size() + "";
}
parts.add(warningText);
}
} else if ("system_notice".equals(message.optString("kind", ""))) {
parts.add("系统同步提醒");
}
return TextUtils.join(" · ", parts);
}
public static void applyMessageSelectionState(Context context, View messageView, boolean selected) {
if (messageView == null) {
return;

View File

@@ -76,7 +76,7 @@ public class ConversationInfoActivity extends BossScreenActivity {
try {
LoadedConversation loadedConversation = loadConversation();
BossApiClient.ApiResponse detailResponse = loadedConversation.detailResponse;
BossApiClient.ApiResponse participantsResponse = loadedConversation.participantsResponse;
JSONObject participantsPayload = loadedConversation.participantsPayload;
JSONObject threadStatusPayload = null;
try {
BossApiClient.ApiResponse threadStatusResponse = loadedConversation.threadStatusResponse;
@@ -87,7 +87,7 @@ public class ConversationInfoActivity extends BossScreenActivity {
threadStatusPayload = null;
}
JSONObject finalThreadStatusPayload = threadStatusPayload;
runOnUiThread(() -> renderConversation(detailResponse.json, participantsResponse.json, finalThreadStatusPayload));
runOnUiThread(() -> renderConversation(detailResponse.json, participantsPayload, finalThreadStatusPayload));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
@@ -471,13 +471,14 @@ public class ConversationInfoActivity extends BossScreenActivity {
throw new IllegalStateException(detailResponse.message());
}
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
if (!participantsResponse.ok()) {
throw new IllegalStateException(participantsResponse.message());
}
JSONObject participantsPayload = extractParticipantsPayload(detailResponse.json);
BossApiClient.ApiResponse threadStatusResponse = apiClient.getThreadStatus(projectId);
return new LoadedConversation(detailResponse, participantsResponse, threadStatusResponse);
return new LoadedConversation(detailResponse, participantsPayload, threadStatusResponse);
}
private JSONObject extractParticipantsPayload(JSONObject detailPayload) {
JSONObject participantsPayload = detailPayload == null ? null : detailPayload.optJSONObject("participantsPayload");
return participantsPayload == null ? new JSONObject() : participantsPayload;
}
private BossApiClient.ApiResponse saveTakeoverSettingsWithRetry(
@@ -548,16 +549,16 @@ public class ConversationInfoActivity extends BossScreenActivity {
private static final class LoadedConversation {
private final BossApiClient.ApiResponse detailResponse;
private final BossApiClient.ApiResponse participantsResponse;
private final JSONObject participantsPayload;
private final BossApiClient.ApiResponse threadStatusResponse;
private LoadedConversation(
BossApiClient.ApiResponse detailResponse,
BossApiClient.ApiResponse participantsResponse,
JSONObject participantsPayload,
BossApiClient.ApiResponse threadStatusResponse
) {
this.detailResponse = detailResponse;
this.participantsResponse = participantsResponse;
this.participantsPayload = participantsPayload;
this.threadStatusResponse = threadStatusResponse;
}
}

View File

@@ -11,6 +11,7 @@ import android.widget.TextView;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
@@ -70,9 +71,10 @@ public class GroupCreateActivity extends BossScreenActivity {
runOnUiThread(() -> renderCreatePage(null, conversationsResponse.json, true));
return;
}
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(sourceProjectId);
if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message());
runOnUiThread(() -> renderCreatePage(participantsResponse.json, conversationsResponse.json, true));
BossApiClient.ApiResponse detailResponse = apiClient.getProjectDetail(sourceProjectId);
if (!detailResponse.ok()) throw new IllegalStateException(detailResponse.message());
JSONObject participantsPayload = extractParticipantsPayload(detailResponse.json, sourceProjectId);
runOnUiThread(() -> renderCreatePage(participantsPayload, conversationsResponse.json, true));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
@@ -155,6 +157,23 @@ public class GroupCreateActivity extends BossScreenActivity {
updateCreateButtonState();
}
private JSONObject extractParticipantsPayload(JSONObject detailPayload, String fallbackProjectId) {
JSONObject participantsPayload = detailPayload == null ? null : detailPayload.optJSONObject("participantsPayload");
if (participantsPayload != null) {
return participantsPayload;
}
JSONObject fallback = new JSONObject();
JSONObject project = detailPayload == null ? null : detailPayload.optJSONObject("project");
JSONObject threadMeta = project == null ? null : project.optJSONObject("threadMeta");
try {
fallback.put("projectId", fallbackProjectId == null ? "" : fallbackProjectId);
fallback.put("threadMeta", threadMeta == null ? new JSONObject() : threadMeta);
fallback.put("participants", new JSONArray());
} catch (JSONException ignored) {
}
return fallback;
}
private View buildHeaderView(
boolean hasSourceProject,
@Nullable String sourceProjectId,

View File

@@ -22,6 +22,7 @@ public class GroupInfoActivity extends BossScreenActivity {
private String projectId;
private String projectName;
private boolean groupRepairJustApplied;
private @Nullable BossRealtimeClient realtimeClient;
private final Map<String, Long> recentRealtimeEventTimestamps = new LinkedHashMap<>();
@@ -72,13 +73,12 @@ public class GroupInfoActivity extends BossScreenActivity {
try {
BossApiClient.ApiResponse detailResponse = apiClient.getProjectDetail(projectId);
if (!detailResponse.ok()) throw new IllegalStateException(detailResponse.message());
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message());
JSONObject participantsPayload = extractParticipantsPayload(detailResponse.json);
BossApiClient.ApiResponse orchestrationResponse = apiClient.getProjectOrchestrationBackend(projectId);
JSONObject orchestrationBackend = orchestrationResponse.ok()
? orchestrationResponse.json
: buildFallbackOrchestrationBackendPayload(orchestrationResponse.message());
runOnUiThread(() -> renderGroup(detailResponse.json, participantsResponse.json, orchestrationBackend));
runOnUiThread(() -> renderGroup(detailResponse.json, participantsPayload, orchestrationBackend));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
@@ -149,6 +149,11 @@ public class GroupInfoActivity extends BossScreenActivity {
}
}
private JSONObject extractParticipantsPayload(JSONObject detailPayload) {
JSONObject participantsPayload = detailPayload == null ? null : detailPayload.optJSONObject("participantsPayload");
return participantsPayload == null ? new JSONObject() : participantsPayload;
}
private void renderGroup(JSONObject detail, JSONObject participantsPayload) {
renderGroup(detail, participantsPayload, null);
}
@@ -174,6 +179,11 @@ public class GroupInfoActivity extends BossScreenActivity {
int invalidParticipantCount = participantsPayload.optInt("invalidParticipantCount", 0);
configureScreen("群资料", buildSubtitle(folderName, participantCount));
if (groupRepairJustApplied) {
appendContent(BossUi.buildEmptyCard(this, "群成员已更新,当前群聊已经切换到新的真实线程成员。"));
groupRepairJustApplied = false;
}
appendContent(BossUi.buildSimpleProfileHeader(
this,
projectName,
@@ -367,6 +377,7 @@ public class GroupInfoActivity extends BossScreenActivity {
BossApiClient.ApiResponse response = apiClient.replaceConversationParticipants(projectId, memberProjectIds);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
groupRepairJustApplied = true;
showMessage("群成员已更新");
reload();
});

View File

@@ -62,10 +62,26 @@ public final class ProjectChatUiState {
public static final class ReplyWaitSpec {
public final boolean shouldWait;
public final String baselineMessageId;
public final List<String> executionIds;
private ReplyWaitSpec(boolean shouldWait, @Nullable String baselineMessageId) {
this.shouldWait = shouldWait && !isBlank(baselineMessageId);
this.baselineMessageId = this.shouldWait ? baselineMessageId.trim() : "";
private ReplyWaitSpec(
boolean shouldWait,
@Nullable String baselineMessageId,
@Nullable List<String> executionIds
) {
ArrayList<String> normalizedExecutionIds = new ArrayList<>();
if (executionIds != null) {
for (String executionId : executionIds) {
if (!isBlank(executionId)) {
normalizedExecutionIds.add(executionId.trim());
}
}
}
boolean hasBaseline = !isBlank(baselineMessageId);
boolean hasExecutionIds = !normalizedExecutionIds.isEmpty();
this.shouldWait = shouldWait && (hasBaseline || hasExecutionIds);
this.baselineMessageId = hasBaseline ? baselineMessageId.trim() : "";
this.executionIds = Collections.unmodifiableList(normalizedExecutionIds);
}
}
@@ -410,30 +426,81 @@ public final class ProjectChatUiState {
public static ReplyWaitSpec resolveReplyWaitAfterSend(@Nullable JSONObject response) {
if (response == null) {
return new ReplyWaitSpec(false, null);
return new ReplyWaitSpec(false, null, null);
}
JSONObject task = response.optJSONObject("task");
if (task == null) {
return new ReplyWaitSpec(false, null);
return new ReplyWaitSpec(false, null, null);
}
String taskStatus = task.optString("status", "");
if ("completed".equals(taskStatus) || "failed".equals(taskStatus)) {
return new ReplyWaitSpec(false, null);
return new ReplyWaitSpec(false, null, null);
}
JSONObject message = response.optJSONObject("message");
return new ReplyWaitSpec(true, message == null ? null : message.optString("id", ""));
return new ReplyWaitSpec(true, message == null ? null : message.optString("id", ""), null);
}
public static ReplyWaitSpec resolveReplyWaitAfterDispatchConfirm(@Nullable JSONObject response) {
if (response == null) {
return new ReplyWaitSpec(false, null);
return new ReplyWaitSpec(false, null, null);
}
JSONArray executions = response.optJSONArray("executions");
if (executions == null || executions.length() == 0) {
return new ReplyWaitSpec(false, null);
return new ReplyWaitSpec(false, null, null);
}
JSONObject notice = response.optJSONObject("notice");
return new ReplyWaitSpec(true, notice == null ? null : notice.optString("id", ""));
return new ReplyWaitSpec(
true,
notice == null ? null : notice.optString("id", ""),
collectExecutionIds(executions)
);
}
public static ReplyWaitSpec replyWaitFromBaseline(@Nullable String baselineMessageId) {
return new ReplyWaitSpec(true, baselineMessageId, null);
}
public static boolean hasTrackedDispatchExecutionReply(
@Nullable JSONArray dispatchPlans,
@Nullable List<String> executionIds
) {
if (dispatchPlans == null || executionIds == null || executionIds.isEmpty()) {
return false;
}
LinkedHashSet<String> trackedIds = new LinkedHashSet<>();
for (String executionId : executionIds) {
if (!isBlank(executionId)) {
trackedIds.add(executionId.trim());
}
}
if (trackedIds.isEmpty()) {
return false;
}
for (int i = 0; i < dispatchPlans.length(); i++) {
JSONObject plan = dispatchPlans.optJSONObject(i);
if (plan == null) {
continue;
}
JSONArray executions = plan.optJSONArray("executions");
if (executions == null) {
continue;
}
for (int j = 0; j < executions.length(); j++) {
JSONObject execution = executions.optJSONObject(j);
if (execution == null) {
continue;
}
String executionId = execution.optString("executionId", "").trim();
if (!trackedIds.contains(executionId)) {
continue;
}
String status = execution.optString("status", "").trim();
if ("completed".equals(status) || "failed".equals(status)) {
return true;
}
}
}
return false;
}
public static boolean hasReplyBeyondBaseline(@Nullable JSONObject project, @Nullable String baselineMessageId) {
@@ -457,6 +524,24 @@ public final class ProjectChatUiState {
return messageId.isEmpty() ? null : messageId;
}
private static List<String> collectExecutionIds(@Nullable JSONArray executions) {
ArrayList<String> executionIds = new ArrayList<>();
if (executions == null) {
return executionIds;
}
for (int i = 0; i < executions.length(); i++) {
JSONObject execution = executions.optJSONObject(i);
if (execution == null) {
continue;
}
String executionId = execution.optString("executionId", "").trim();
if (!executionId.isEmpty()) {
executionIds.add(executionId);
}
}
return executionIds;
}
private static boolean isBlank(@Nullable String value) {
return value == null || value.trim().isEmpty();
}

View File

@@ -36,6 +36,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -55,6 +56,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
private String projectFolderName;
private @Nullable String currentAgentModelOverride;
private @Nullable String currentReasoningEffortOverride;
private @Nullable String currentFastModelOverride;
private @Nullable String currentFastReasoningEffortOverride;
private @Nullable String currentSmartModelOverride;
private @Nullable String currentSmartReasoningEffortOverride;
private final List<String> currentAvailableMasterAgentModels = new ArrayList<>();
private final List<String> currentSelectableMasterAgentModels = new ArrayList<>();
private LinearLayout quickActionsLayout;
private LinearLayout composerRow;
private LinearLayout multiSelectActionsLayout;
@@ -174,6 +181,25 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
}
static @Nullable JSONArray extractDispatchPlans(JSONObject detailPayload) {
return detailPayload == null ? null : detailPayload.optJSONArray("dispatchPlans");
}
static @Nullable JSONObject extractParticipantsPayload(JSONObject detailPayload) {
return detailPayload == null ? null : detailPayload.optJSONObject("participantsPayload");
}
private String buildDisplayedProjectTitle(@Nullable String rawTitle) {
String normalizedTitle = TextUtils.isEmpty(rawTitle) ? "项目详情" : rawTitle;
if (!isMasterAgentConversation()) {
return normalizedTitle;
}
String modelName = !TextUtils.isEmpty(currentAgentModelOverride)
? currentAgentModelOverride
: (!TextUtils.isEmpty(currentSmartModelOverride) ? currentSmartModelOverride : null);
return TextUtils.isEmpty(modelName) ? "主Agent" : "主Agent·" + modelName;
}
@Override
protected int getLayoutResId() {
return R.layout.activity_project_chat;
@@ -273,7 +299,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
BossWindowInsets.applyKeyboardAvoidingInset(composerRow);
BossWindowInsets.applyKeyboardAvoidingInset(multiSelectActionsLayout);
updateProjectHeader(initialProjectName == null ? "项目详情" : initialProjectName, "正在同步项目详情...");
updateProjectHeader(buildDisplayedProjectTitle(initialProjectName), "正在同步项目详情...");
if (composerAttachmentButton != null) {
composerAttachmentButton.setOnClickListener(v -> showAttachmentEntrySheet());
}
@@ -371,12 +397,19 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (projectMessagesPayload == null) {
return false;
}
if (shouldBypassRealtimeMessagesPatchForGroupState()) {
return false;
}
JSONArray executionWarnings = projectMessagesPayload.optJSONArray("executionWarnings");
if (trySkipUnchangedRealtimeMessagesPatch(projectMessagesPayload)) {
return true;
}
if (tryAppendRealtimeMessagesPatch(projectMessagesPayload)) {
return true;
}
if (tryPatchRealtimeExecutionWarnings(projectMessagesPayload)) {
return true;
}
runOnUiThread(() -> {
if (reloadInFlight) {
scheduleRealtimeReload(false);
@@ -389,6 +422,16 @@ public class ProjectDetailActivity extends BossScreenActivity {
return true;
}
private boolean shouldBypassRealtimeMessagesPatchForGroupState() {
if (!projectIsGroup) {
return false;
}
if (currentPendingDispatchPlan != null || currentRejectedDispatchPlan != null) {
return true;
}
return currentParticipantsPayload != null && currentParticipantsPayload.optBoolean("repairRequired", false);
}
private boolean trySkipUnchangedRealtimeMessagesPatch(JSONObject projectMessagesPayload) {
if (currentRenderedProjectPayload == null || projectMessagesPayload == null) {
return false;
@@ -412,6 +455,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (!TextUtils.equals(currentMessages.toString(), nextMessages.toString())) {
return false;
}
if (!hasMatchingExecutionWarnings(currentRenderedProjectPayload, projectMessagesPayload)) {
return false;
}
if (!hasMatchingConversationTasks(currentRenderedProjectPayload, projectMessagesPayload)) {
return false;
}
currentRenderedProjectPayload = copyJson(projectMessagesPayload);
return true;
}
@@ -435,6 +484,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (currentMessages == null || nextMessages == null) {
return false;
}
if (!hasMatchingExecutionWarnings(currentRenderedProjectPayload, projectMessagesPayload)) {
return false;
}
if (!hasMatchingConversationTasks(currentRenderedProjectPayload, projectMessagesPayload)) {
return false;
}
List<String> currentIds = collectMessageIds(currentMessages);
List<String> nextIds = collectMessageIds(nextMessages);
if (currentIds.isEmpty() || nextIds.size() <= currentIds.size()) {
@@ -460,7 +515,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
projectCollaborationMode = project == null ? "development" : project.optString("collaborationMode", projectCollaborationMode);
projectApprovalState = project == null ? "not_required" : project.optString("approvalState", projectApprovalState);
lightDispatchReminderEnabled = project != null && project.optBoolean("lightDispatchReminderEnabled", lightDispatchReminderEnabled);
updateProjectHeader(title, buildProjectSubtitle(projectFolderName, devices));
updateProjectHeader(buildDisplayedProjectTitle(title), buildProjectSubtitle(projectFolderName, devices));
selectionState = ProjectChatUiState.reconcileSelection(selectionState, nextIds);
renderNearBottom = isChatNearBottom();
@@ -483,6 +538,112 @@ public class ProjectDetailActivity extends BossScreenActivity {
return true;
}
private boolean tryPatchRealtimeExecutionWarnings(JSONObject projectMessagesPayload) {
if (currentRenderedProjectPayload == null
|| contentLayout == null
|| pendingOutgoingBubble != null
|| masterAgentReplyWaiting
|| masterAgentReplyTimedOut
|| (selectionState != null && selectionState.multiSelecting)) {
return false;
}
JSONObject currentProject = currentRenderedProjectPayload.optJSONObject("project");
JSONObject nextProject = projectMessagesPayload.optJSONObject("project");
if (currentProject == null || nextProject == null) {
return false;
}
JSONArray currentMessages = currentProject.optJSONArray("messages");
JSONArray nextMessages = nextProject.optJSONArray("messages");
if (currentMessages == null || nextMessages == null) {
return false;
}
if (!TextUtils.equals(currentMessages.toString(), nextMessages.toString())) {
return false;
}
if (hasMatchingExecutionWarnings(currentRenderedProjectPayload, projectMessagesPayload)
&& hasMatchingConversationTasks(currentRenderedProjectPayload, projectMessagesPayload)) {
return false;
}
JSONObject nextPayloadCopy = copyJson(projectMessagesPayload);
runOnUiThread(() -> {
if (currentRenderedProjectPayload == null || contentLayout == null) {
renderLoadedProjectSnapshot(new ProjectSnapshot(projectMessagesPayload, null, null));
return;
}
JSONArray refreshedMessages = nextProject.optJSONArray("messages");
if (refreshedMessages == null) {
renderLoadedProjectSnapshot(new ProjectSnapshot(projectMessagesPayload, null, null));
return;
}
renderNearBottom = isChatNearBottom();
runWithSuppressedContentLayout(() -> {
for (int i = 0; i < refreshedMessages.length(); i++) {
JSONObject message = refreshedMessages.optJSONObject(i);
if (message == null) {
continue;
}
String messageId = message.optString("id", "");
if (TextUtils.isEmpty(messageId)) {
continue;
}
String currentFingerprint = buildStatusFingerprint(messageId, currentRenderedProjectPayload);
String nextFingerprint = buildStatusFingerprint(messageId, projectMessagesPayload);
if (TextUtils.isEmpty(currentFingerprint) && TextUtils.isEmpty(nextFingerprint)) {
continue;
}
if (!TextUtils.equals(currentFingerprint, nextFingerprint)) {
replaceMessageViewById(messageId, buildMessageView(message));
}
}
});
currentRenderedProjectPayload = nextPayloadCopy;
setRefreshing(false);
updateSelectionUi();
if (ProjectChatUiState.shouldAutoScroll(renderNearBottom, false)) {
scrollChatToBottom();
}
});
return true;
}
private boolean hasMatchingExecutionWarnings(JSONObject currentPayload, JSONObject nextPayload) {
if (currentPayload == null || nextPayload == null) {
return false;
}
JSONArray currentWarnings = currentPayload.optJSONArray("executionWarnings");
JSONArray nextWarnings = nextPayload.optJSONArray("executionWarnings");
String currentSerialized = currentWarnings == null ? "[]" : currentWarnings.toString();
String nextSerialized = nextWarnings == null ? "[]" : nextWarnings.toString();
return TextUtils.equals(currentSerialized, nextSerialized);
}
private boolean hasMatchingConversationTasks(JSONObject currentPayload, JSONObject nextPayload) {
if (currentPayload == null || nextPayload == null) {
return false;
}
JSONArray currentTasks = currentPayload.optJSONArray("conversationTasks");
JSONArray nextTasks = nextPayload.optJSONArray("conversationTasks");
String currentSerialized = currentTasks == null ? "[]" : currentTasks.toString();
String nextSerialized = nextTasks == null ? "[]" : nextTasks.toString();
return TextUtils.equals(currentSerialized, nextSerialized);
}
private void replaceMessageViewById(String messageId, View nextMessageView) {
if (TextUtils.isEmpty(messageId) || nextMessageView == null || contentLayout == null) {
return;
}
for (int i = 0; i < contentLayout.getChildCount(); i++) {
View child = contentLayout.getChildAt(i);
Object tag = child.getTag();
if (!(tag instanceof String) || !TextUtils.equals(messageId, (String) tag)) {
continue;
}
contentLayout.removeViewAt(i);
contentLayout.addView(nextMessageView, i);
return;
}
}
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
pruneRecentRealtimeEvents(now);
Long previousEventAt = recentRealtimeEventTimestamps.get(eventFingerprint);
@@ -654,6 +815,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
JSONObject agentControls = project == null ? null : project.optJSONObject("agentControls");
currentAgentModelOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("modelOverride", null));
currentReasoningEffortOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("reasoningEffortOverride", null));
currentFastModelOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("fastModelOverride", null));
currentFastReasoningEffortOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("fastReasoningEffortOverride", null));
currentSmartModelOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("smartModelOverride", null));
currentSmartReasoningEffortOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("smartReasoningEffortOverride", null));
if (dispatchPlans != null) {
currentPendingDispatchPlan = ProjectChatUiState.latestPendingDispatchPlan(dispatchPlans);
currentRejectedDispatchPlan = currentPendingDispatchPlan == null
@@ -667,7 +832,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
? currentParticipantsPayload
: participantsPayload;
conversationInfoReady = project != null;
updateProjectHeader(title, buildProjectSubtitle(projectFolderName, devices));
updateProjectHeader(buildDisplayedProjectTitle(title), buildProjectSubtitle(projectFolderName, devices));
renderQuickActions();
JSONArray messages = project == null ? null : project.optJSONArray("messages");
@@ -1251,40 +1416,116 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (!isMasterAgentConversation()) {
return;
}
final String[] options = buildMasterAgentModelOptions();
int checkedIndex = findCheckedIndex(options, currentAgentModelOverride);
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getProjectAgentControls(projectId);
if (!response.ok()) {
throw new IllegalStateException(response.message());
}
JSONObject payload = response.json;
JSONObject controls = payload.optJSONObject("controls");
JSONObject modelCatalog = payload.optJSONObject("modelCatalog");
runOnUiThread(() -> {
applyMasterAgentControlsFromJson(controls);
applyMasterAgentModelCatalogFromJson(modelCatalog);
setRefreshing(false);
showMasterAgentModelScopeMenu();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("模型信息加载失败:" + error.getMessage());
});
}
});
}
private void showMasterAgentModelScopeMenu() {
String availableLabel = currentAvailableMasterAgentModels.isEmpty()
? "未检测到已就绪账号,可直接使用预设模型"
: "当前可用:" + TextUtils.join("", currentAvailableMasterAgentModels);
new AlertDialog.Builder(this)
.setTitle("模型")
.setSingleChoiceItems(options, checkedIndex, (dialog, which) -> {
.setItems(new CharSequence[]{
availableLabel,
buildMasterAgentModelScopeSummary("当前主模型", currentAgentModelOverride),
buildMasterAgentModelScopeSummary("快模型", currentFastModelOverride),
buildMasterAgentModelScopeSummary("强模型", currentSmartModelOverride)
}, (dialog, which) -> {
if (which == 0) {
dialog.dismiss();
updateMasterAgentControls(null, currentReasoningEffortOverride, "模型已恢复默认");
showMasterAgentAvailableModelsDialog();
return;
}
if (which == options.length - 1) {
if (which == 1) {
dialog.dismiss();
showCustomMasterAgentModelDialog();
showMasterAgentModelValuePicker("当前主模型", currentAgentModelOverride, "manual_override");
return;
}
dialog.dismiss();
updateMasterAgentControls(options[which], currentReasoningEffortOverride, "模型已更新为 " + options[which]);
if (which == 2) {
dialog.dismiss();
showMasterAgentModelValuePicker("快模型", currentFastModelOverride, "fast");
return;
}
if (which == 3) {
dialog.dismiss();
showMasterAgentModelValuePicker("强模型", currentSmartModelOverride, "smart");
}
})
.setNegativeButton("取消", null)
.show();
}
private void showCustomMasterAgentModelDialog() {
final EditText input = BossUi.buildInput(this, "模型,例如 gpt-5.4", false);
input.setText(TextUtils.isEmpty(currentAgentModelOverride) ? "gpt-5.4" : currentAgentModelOverride);
private void showMasterAgentAvailableModelsDialog() {
String message = currentAvailableMasterAgentModels.isEmpty()
? "当前没有检测到已就绪账号,你仍然可以直接选择预设模型,或者用自定义模型手动填写。"
: "当前可用模型:\n" + TextUtils.join("\n", currentAvailableMasterAgentModels);
String selectable = currentSelectableMasterAgentModels.isEmpty()
? ""
: "\n\n可选模型\n" + TextUtils.join("\n", currentSelectableMasterAgentModels);
new AlertDialog.Builder(this)
.setTitle("自定义模型")
.setTitle("可用模型")
.setMessage(message + selectable)
.setPositiveButton("知道了", null)
.show();
}
private void showMasterAgentModelValuePicker(String scopeTitle, @Nullable String currentValue, String scopeKey) {
final String[] options = buildMasterAgentModelOptions(currentValue);
int checkedIndex = findCheckedIndex(options, currentValue);
new AlertDialog.Builder(this)
.setTitle(scopeTitle)
.setSingleChoiceItems(options, checkedIndex, (dialog, which) -> {
if (which == 0) {
dialog.dismiss();
updateMasterAgentControlsForScope(scopeKey, null, scopeTitle + "已恢复默认");
return;
}
if (which == options.length - 1) {
dialog.dismiss();
showCustomMasterAgentModelDialog(scopeTitle, scopeKey, currentValue);
return;
}
dialog.dismiss();
updateMasterAgentControlsForScope(scopeKey, options[which], scopeTitle + "已更新为 " + options[which]);
})
.setNegativeButton("取消", null)
.show();
}
private void showCustomMasterAgentModelDialog(String scopeTitle, String scopeKey, @Nullable String currentValue) {
final EditText input = BossUi.buildInput(this, "模型,例如 gpt-5.4", false);
input.setText(TextUtils.isEmpty(currentValue) ? "gpt-5.4" : currentValue);
new AlertDialog.Builder(this)
.setTitle(scopeTitle + " · 自定义模型")
.setView(input)
.setNegativeButton("取消", null)
.setPositiveButton("保存", (dialog, which) ->
updateMasterAgentControls(
updateMasterAgentControlsForScope(
scopeKey,
normalizeControlValue(input.getText() == null ? null : input.getText().toString()),
currentReasoningEffortOverride,
"模型已更新"
scopeTitle + "已更新"
))
.show();
}
@@ -1303,6 +1544,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
updateMasterAgentControls(
currentAgentModelOverride,
reasoningOverride,
currentFastModelOverride,
currentFastReasoningEffortOverride,
currentSmartModelOverride,
currentSmartReasoningEffortOverride,
which == 0 ? "推理强度已恢复默认" : "推理强度已更新为 " + options[which]
);
})
@@ -1313,6 +1558,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
private void updateMasterAgentControls(
@Nullable String modelOverride,
@Nullable String reasoningEffortOverride,
@Nullable String fastModelOverride,
@Nullable String fastReasoningEffortOverride,
@Nullable String smartModelOverride,
@Nullable String smartReasoningEffortOverride,
String successMessage
) {
if (!isMasterAgentConversation() || projectId == null || projectId.isEmpty()) {
@@ -1324,15 +1573,19 @@ public class ProjectDetailActivity extends BossScreenActivity {
BossApiClient.ApiResponse response = apiClient.updateProjectAgentControls(
projectId,
modelOverride,
reasoningEffortOverride
reasoningEffortOverride,
fastModelOverride,
fastReasoningEffortOverride,
smartModelOverride,
smartReasoningEffortOverride,
true
);
if (!response.ok()) {
throw new IllegalStateException(response.message());
}
JSONObject controls = response.json.optJSONObject("controls");
runOnUiThread(() -> {
currentAgentModelOverride = normalizeControlValue(controls == null ? null : controls.optString("modelOverride", null));
currentReasoningEffortOverride = normalizeControlValue(controls == null ? null : controls.optString("reasoningEffortOverride", null));
applyMasterAgentControlsFromJson(controls);
showMessage(successMessage);
reload(true);
});
@@ -1345,21 +1598,86 @@ public class ProjectDetailActivity extends BossScreenActivity {
});
}
private String[] buildMasterAgentModelOptions() {
private void updateMasterAgentControlsForScope(String scopeKey, @Nullable String modelOverride, String successMessage) {
String manualModelOverride = currentAgentModelOverride;
String fastModelOverride = currentFastModelOverride;
String smartModelOverride = currentSmartModelOverride;
if ("fast".equals(scopeKey)) {
fastModelOverride = modelOverride;
} else if ("smart".equals(scopeKey)) {
smartModelOverride = modelOverride;
} else {
manualModelOverride = modelOverride;
}
updateMasterAgentControls(
manualModelOverride,
currentReasoningEffortOverride,
fastModelOverride,
currentFastReasoningEffortOverride,
smartModelOverride,
currentSmartReasoningEffortOverride,
successMessage
);
}
private void applyMasterAgentControlsFromJson(@Nullable JSONObject controls) {
currentAgentModelOverride = normalizeControlValue(controls == null ? null : controls.optString("modelOverride", null));
currentReasoningEffortOverride = normalizeControlValue(controls == null ? null : controls.optString("reasoningEffortOverride", null));
currentFastModelOverride = normalizeControlValue(controls == null ? null : controls.optString("fastModelOverride", null));
currentFastReasoningEffortOverride = normalizeControlValue(controls == null ? null : controls.optString("fastReasoningEffortOverride", null));
currentSmartModelOverride = normalizeControlValue(controls == null ? null : controls.optString("smartModelOverride", null));
currentSmartReasoningEffortOverride = normalizeControlValue(controls == null ? null : controls.optString("smartReasoningEffortOverride", null));
if (isMasterAgentConversation()) {
updateProjectHeader(buildDisplayedProjectTitle(initialProjectName), currentScreenSubtitle);
}
}
private void applyMasterAgentModelCatalogFromJson(@Nullable JSONObject modelCatalog) {
currentAvailableMasterAgentModels.clear();
currentSelectableMasterAgentModels.clear();
addJsonArrayStrings(modelCatalog == null ? null : modelCatalog.optJSONArray("availableModels"), currentAvailableMasterAgentModels);
addJsonArrayStrings(modelCatalog == null ? null : modelCatalog.optJSONArray("selectableModels"), currentSelectableMasterAgentModels);
}
private void addJsonArrayStrings(@Nullable JSONArray array, List<String> output) {
if (array == null || output == null) {
return;
}
for (int i = 0; i < array.length(); i++) {
String value = normalizeControlValue(array.optString(i, null));
if (!TextUtils.isEmpty(value) && !output.contains(value)) {
output.add(value);
}
}
}
private String buildMasterAgentModelScopeSummary(String label, @Nullable String value) {
return label + "" + (TextUtils.isEmpty(value) ? "默认" : value);
}
private String[] buildMasterAgentModelOptions(@Nullable String currentValue) {
List<String> options = new ArrayList<>();
options.add("沿用默认");
if (!TextUtils.isEmpty(currentAgentModelOverride)) {
options.add(currentAgentModelOverride);
if (!TextUtils.isEmpty(currentValue) && !options.contains(currentValue)) {
options.add(currentValue);
}
for (String value : currentSelectableMasterAgentModels) {
if (!TextUtils.isEmpty(value) && !options.contains(value)) {
options.add(value);
}
}
if (!options.contains("gpt-5.4")) {
options.add("gpt-5.4");
}
if (!options.contains("gpt-5.1")) {
options.add("gpt-5.1");
if (!options.contains("gpt-5.4-mini")) {
options.add("gpt-5.4-mini");
}
if (!options.contains("gpt-4.1")) {
options.add("gpt-4.1");
}
if (!options.contains("gpt-4.1-mini")) {
options.add("gpt-4.1-mini");
}
options.add("自定义...");
return options.toArray(new String[0]);
}
@@ -1427,8 +1745,11 @@ public class ProjectDetailActivity extends BossScreenActivity {
private View buildRepairGroupMembersView(JSONObject participantsPayload) {
String repairReason = participantsPayload.optString("repairReason", "当前群聊里有失效线程,请先修复群成员。");
int invalidParticipantCount = participantsPayload.optInt("invalidParticipantCount", 0);
int validParticipantCount = participantsPayload.optInt("validParticipantCount", 0);
String meta = invalidParticipantCount > 0
? "存在 " + invalidParticipantCount + " 个失效成员"
: validParticipantCount > 0
? "当前仅有 " + validParticipantCount + " 个真实线程成员"
: "当前群聊还没有可下发的真实线程";
LinearLayout container = new LinearLayout(this);
container.setOrientation(LinearLayout.VERTICAL);
@@ -1612,6 +1933,15 @@ public class ProjectDetailActivity extends BossScreenActivity {
);
break;
}
JSONObject conversationTask = findConversationTask(currentRenderedProjectPayload, messageId);
if (messageView instanceof LinearLayout) {
LinearLayout wrapper = (LinearLayout) messageView;
List<JSONObject> messageWarnings = buildMessageWarnings(currentRenderedProjectPayload, messageId);
LinearLayout statusRow = BossUi.buildMessageStatusRow(this, message, conversationTask, messageWarnings, outgoing);
if (statusRow.getVisibility() != View.GONE) {
wrapper.addView(statusRow);
}
}
bindMessageInteractions(messageView, messageId, body, messagePrimaryClick);
return messageView;
}
@@ -2491,26 +2821,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (!detailResponse.ok()) {
throw new IllegalStateException(detailResponse.message());
}
JSONArray dispatchPlans = null;
JSONObject participantsPayload = null;
if (includeDispatchPlans) {
try {
BossApiClient.ApiResponse dispatchPlansResponse = apiClient.getDispatchPlans(projectId);
if (dispatchPlansResponse.ok()) {
dispatchPlans = dispatchPlansResponse.json.optJSONArray("plans");
}
} catch (Exception ignored) {
dispatchPlans = null;
}
try {
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
if (participantsResponse.ok()) {
participantsPayload = participantsResponse.json;
}
} catch (Exception ignored) {
participantsPayload = null;
}
}
JSONArray dispatchPlans = includeDispatchPlans
? detailResponse.json.optJSONArray("dispatchPlans")
: null;
JSONObject participantsPayload = includeDispatchPlans
? detailResponse.json.optJSONObject("participantsPayload")
: null;
return new ProjectSnapshot(detailResponse.json, dispatchPlans, participantsPayload);
}
@@ -2535,7 +2851,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
updateComposerSendButtonState();
setRefreshing(true);
showMessage(waitingMessage);
enqueueReplyWaitPoll(waitSpec.baselineMessageId, includeDispatchPlans);
enqueueReplyWaitPoll(waitSpec, includeDispatchPlans);
}
private void startMasterAgentReplyWait(
@@ -2551,15 +2867,15 @@ public class ProjectDetailActivity extends BossScreenActivity {
setRefreshing(false);
showMessage(waitingMessage);
reload(true);
enqueueReplyWaitPoll(waitSpec.baselineMessageId, includeDispatchPlans);
enqueueReplyWaitPoll(waitSpec, includeDispatchPlans);
}
protected void enqueueReplyWaitPoll(@Nullable String baselineMessageId, boolean includeDispatchPlans) {
replyWaitExecutor.execute(() -> pollUntilReply(baselineMessageId, includeDispatchPlans));
protected void enqueueReplyWaitPoll(ProjectChatUiState.ReplyWaitSpec waitSpec, boolean includeDispatchPlans) {
replyWaitExecutor.execute(() -> pollUntilReply(waitSpec, includeDispatchPlans));
}
private void pollUntilReply(
@Nullable String baselineMessageId,
ProjectChatUiState.ReplyWaitSpec waitSpec,
boolean includeDispatchPlans
) {
long deadlineAt = System.currentTimeMillis() + REPLY_WAIT_TIMEOUT_MS;
@@ -2568,7 +2884,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
while (!Thread.currentThread().isInterrupted() && System.currentTimeMillis() < deadlineAt) {
ProjectSnapshot snapshot = fetchProjectSnapshot(includeDispatchPlans);
JSONObject project = snapshot.payload.optJSONObject("project");
boolean hasReply = ProjectChatUiState.hasReplyBeyondBaseline(project, baselineMessageId);
boolean hasReply = !waitSpec.executionIds.isEmpty()
? ProjectChatUiState.hasTrackedDispatchExecutionReply(snapshot.dispatchPlans, waitSpec.executionIds)
: ProjectChatUiState.hasReplyBeyondBaseline(project, waitSpec.baselineMessageId);
if (!renderedInitialSnapshot || hasReply) {
runOnUiThread(() -> {
@@ -2633,7 +2951,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
setRefreshing(false);
showMessage("已重新开始等待主 Agent 回复");
reload(true);
enqueueReplyWaitPoll(masterAgentReplyBaselineMessageId, false);
enqueueReplyWaitPoll(
ProjectChatUiState.replyWaitFromBaseline(masterAgentReplyBaselineMessageId),
false
);
}
static ChromeBindings buildChromeBindings(
@@ -2718,6 +3039,148 @@ public class ProjectDetailActivity extends BossScreenActivity {
return ids;
}
@Nullable
private JSONObject findExecutionWarningForMessage(@Nullable String messageId) {
return findExecutionWarningForMessage(currentRenderedProjectPayload, messageId);
}
@Nullable
private JSONObject findExecutionWarningForMessage(@Nullable JSONObject payload, @Nullable String messageId) {
if (TextUtils.isEmpty(messageId) || payload == null) {
return null;
}
JSONArray warnings = payload.optJSONArray("executionWarnings");
if (warnings == null) {
return null;
}
for (int i = 0; i < warnings.length(); i++) {
JSONObject warning = warnings.optJSONObject(i);
if (warning == null) {
continue;
}
if (TextUtils.equals(messageId, warning.optString("requestMessageId", ""))) {
return warning;
}
}
return null;
}
private boolean hasExecutionWarningForMessage(@Nullable JSONObject payload, @Nullable String messageId) {
return findExecutionWarningForMessage(payload, messageId) != null;
}
@Nullable
private JSONObject findConversationTask(@Nullable JSONObject payload, @Nullable String messageId) {
if (TextUtils.isEmpty(messageId) || payload == null) {
return null;
}
JSONArray tasks = payload.optJSONArray("conversationTasks");
if (tasks == null) {
return null;
}
for (int i = 0; i < tasks.length(); i++) {
JSONObject task = tasks.optJSONObject(i);
if (task == null) {
continue;
}
if (TextUtils.equals(messageId, task.optString("requestMessageId", ""))) {
return task;
}
}
return null;
}
private List<JSONObject> buildMessageWarnings(JSONObject payload, String messageId) {
ArrayList<JSONObject> warnings = new ArrayList<>();
if (payload == null || TextUtils.isEmpty(messageId)) {
return warnings;
}
JSONArray rawWarnings = payload.optJSONArray("executionWarnings");
if (rawWarnings == null) {
return warnings;
}
LinkedHashMap<String, JSONObject> deduped = new LinkedHashMap<>();
for (int i = 0; i < rawWarnings.length(); i++) {
JSONObject warning = rawWarnings.optJSONObject(i);
if (warning == null) {
continue;
}
if (!TextUtils.equals(messageId, warning.optString("requestMessageId", ""))) {
continue;
}
String key = warning.optString("title", "")
+ "::"
+ warning.optString("summary", "")
+ "::"
+ warning.optString("taskId", "")
+ "::"
+ warning.optString("sessionId", "");
JSONObject existing = deduped.get(key);
if (existing == null) {
JSONObject grouped = copyJson(warning);
try {
grouped.put("count", 1);
} catch (Exception ignored) {
// Keep the original warning when count cannot be added.
}
deduped.put(key, grouped);
continue;
}
try {
existing.put("count", existing.optInt("count", 1) + 1);
} catch (Exception ignored) {
// Ignore invalid count updates and keep the original entry.
}
}
warnings.addAll(deduped.values());
return warnings;
}
private String buildStatusFingerprint(String messageId, JSONObject payload) {
JSONObject conversationTask = findConversationTask(payload, messageId);
List<JSONObject> messageWarnings = buildMessageWarnings(payload, messageId);
StringBuilder fingerprint = new StringBuilder();
if (conversationTask != null) {
fingerprint.append(conversationTask.optString("taskId", ""))
.append('|')
.append(conversationTask.optString("status", ""))
.append('|')
.append(conversationTask.optString("sessionId", ""))
.append('|')
.append(conversationTask.optString("requestId", ""));
}
fingerprint.append("::");
for (JSONObject warning : messageWarnings) {
if (warning == null) {
continue;
}
fingerprint.append(warning.optString("title", ""))
.append('|')
.append(warning.optString("summary", ""))
.append('|')
.append(warning.optString("taskId", ""))
.append('|')
.append(warning.optString("sessionId", ""))
.append('|')
.append(warning.optInt("count", 1))
.append("||");
}
return fingerprint.toString();
}
private boolean hasSameExecutionWarningForMessage(
@Nullable JSONObject currentPayload,
@Nullable JSONObject nextPayload,
@Nullable String messageId
) {
if (TextUtils.isEmpty(messageId)) {
return true;
}
String currentFingerprint = buildStatusFingerprint(messageId, currentPayload);
String nextFingerprint = buildStatusFingerprint(messageId, nextPayload);
return TextUtils.equals(currentFingerprint, nextFingerprint);
}
private JSONObject copyJson(@Nullable JSONObject source) {
if (source == null) {
return new JSONObject();

View File

@@ -0,0 +1,77 @@
package com.hyzq.boss;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class BossApiClientTest {
@Test
public void buildProjectAgentControlsPayload_omitsAdvancedOverridesWhenAllAreNull() throws Exception {
JSONObject payload = BossApiClient.buildProjectAgentControlsPayload(
"gpt-5.4-mini",
null,
null,
null,
null,
null,
false
);
assertEquals("gpt-5.4-mini", payload.getString("modelOverride"));
assertTrue(payload.has("reasoningEffortOverride"));
assertFalse(payload.has("fastModelOverride"));
assertFalse(payload.has("fastReasoningEffortOverride"));
assertFalse(payload.has("smartModelOverride"));
assertFalse(payload.has("smartReasoningEffortOverride"));
}
@Test
public void buildProjectAgentControlsPayload_includesAdvancedOverridesWhenPresent() throws Exception {
JSONObject payload = BossApiClient.buildProjectAgentControlsPayload(
null,
null,
"gpt-5.4-mini",
null,
"gpt-5.4",
"high",
false
);
assertTrue(payload.has("fastModelOverride"));
assertEquals("gpt-5.4-mini", payload.getString("fastModelOverride"));
assertTrue(payload.has("smartModelOverride"));
assertEquals("gpt-5.4", payload.getString("smartModelOverride"));
assertTrue(payload.has("smartReasoningEffortOverride"));
assertEquals("high", payload.getString("smartReasoningEffortOverride"));
}
@Test
public void buildProjectAgentControlsPayload_includesAdvancedNullsWhenExplicitlyRequested() throws Exception {
JSONObject payload = BossApiClient.buildProjectAgentControlsPayload(
"gpt-5.4-mini",
null,
null,
null,
null,
null,
true
);
assertTrue(payload.has("fastModelOverride"));
assertTrue(payload.isNull("fastModelOverride"));
assertTrue(payload.has("fastReasoningEffortOverride"));
assertTrue(payload.isNull("fastReasoningEffortOverride"));
assertTrue(payload.has("smartModelOverride"));
assertTrue(payload.isNull("smartModelOverride"));
assertTrue(payload.has("smartReasoningEffortOverride"));
assertTrue(payload.isNull("smartReasoningEffortOverride"));
}
}

View File

@@ -21,6 +21,13 @@ public class BossUiRootSurfaceTest {
@Test
public void renderMeRoot_usesWechatProfileHeaderAndFlatMenuRows() throws Exception {
MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
ReflectionHelpers.callInstanceMethod(
activity,
"setActiveTab",
ReflectionHelpers.ClassParameter.from(String.class, "me"),
ReflectionHelpers.ClassParameter.from(boolean.class, false)
);
ReflectionHelpers.setField(
activity,
@@ -32,7 +39,7 @@ public class BossUiRootSurfaceTest {
);
ReflectionHelpers.callInstanceMethod(activity, "renderMeRoot");
LinearLayout content = activity.findViewById(R.id.screen_content);
LinearLayout content = ReflectionHelpers.getField(activity, "screenContent");
assertEquals("我的页应是资料头 + 6 条菜单", 7, content.getChildCount());
View header = content.getChildAt(0);

View File

@@ -24,6 +24,8 @@ import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowDialog;
import org.robolectric.util.ReflectionHelpers;
import java.util.concurrent.TimeUnit;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class ConversationFolderActivityTest {
@@ -145,7 +147,7 @@ public class ConversationFolderActivityTest {
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-2"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
Shadows.shadowOf(activity.getMainLooper()).idleFor(400, TimeUnit.MILLISECONDS);
assertEquals(1, activity.reloadCount);
}

View File

@@ -3,10 +3,13 @@ package com.hyzq.boss;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
@@ -18,18 +21,27 @@ public class MainActivityConversationAutoRefreshTest {
org.robolectric.android.controller.ActivityController<MainActivity> controller =
Robolectric.buildActivity(MainActivity.class).setup().resume();
MainActivity activity = controller.get();
activity.getSharedPreferences("boss_native_client", Context.MODE_PRIVATE)
.edit()
.putString("restore_token", "test-restore-token")
.apply();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
ReflectionHelpers.callInstanceMethod(activity, "setActiveTab",
ReflectionHelpers.ClassParameter.from(String.class, "conversations"),
ReflectionHelpers.ClassParameter.from(boolean.class, false));
assertTrue(ReflectionHelpers.getField(activity, "conversationAutoRefreshArmed"));
ReflectionHelpers.callInstanceMethod(activity, "setActiveTab",
ReflectionHelpers.ClassParameter.from(String.class, "devices"),
ReflectionHelpers.ClassParameter.from(boolean.class, false));
Shadows.shadowOf(activity.getMainLooper()).idle();
assertFalse(ReflectionHelpers.getField(activity, "conversationAutoRefreshArmed"));
ReflectionHelpers.callInstanceMethod(activity, "setActiveTab",
ReflectionHelpers.ClassParameter.from(String.class, "conversations"),
ReflectionHelpers.ClassParameter.from(boolean.class, false));
Shadows.shadowOf(activity.getMainLooper()).idle();
assertTrue(ReflectionHelpers.getField(activity, "conversationAutoRefreshArmed"));
controller.pause();

View File

@@ -15,15 +15,35 @@ import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class MainActivityRealtimeTest {
private static void showConversationTab(MainActivity activity) {
activity.getSharedPreferences("boss_native_client", Context.MODE_PRIVATE)
.edit()
.putString("restore_token", "test-restore-token")
.apply();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
ReflectionHelpers.callInstanceMethod(
activity,
"setActiveTab",
ReflectionHelpers.ClassParameter.from(String.class, "conversations"),
ReflectionHelpers.ClassParameter.from(boolean.class, false)
);
}
private static void flushRealtimeDebounce(MainActivity activity) {
Shadows.shadowOf(activity.getMainLooper()).idleFor(400, TimeUnit.MILLISECONDS);
}
@Test
public void conversationRealtimeEventRefreshesVisibleConversationTab() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
showConversationTab(activity);
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
@@ -32,7 +52,7 @@ public class MainActivityRealtimeTest {
new BossRealtimeEvent("conversation.updated", new JSONObject().put("projectId", "project-1"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(1, activity.conversationRefreshCount);
assertEquals(0, activity.deviceRefreshCount);
@@ -42,7 +62,8 @@ public class MainActivityRealtimeTest {
@Test
public void devicesRealtimeEventDoesNotRefreshConversationTab() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
showConversationTab(activity);
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
@@ -51,7 +72,7 @@ public class MainActivityRealtimeTest {
new BossRealtimeEvent("devices.updated", new JSONObject().put("deviceId", "mac-studio"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(0, activity.conversationRefreshCount);
assertEquals(0, activity.deviceRefreshCount);
@@ -60,7 +81,8 @@ public class MainActivityRealtimeTest {
@Test
public void blankProjectIdConversationEventDoesNotRefreshVisibleConversationTab() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
showConversationTab(activity);
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
@@ -69,7 +91,7 @@ public class MainActivityRealtimeTest {
new BossRealtimeEvent("conversation.updated", new JSONObject())
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(0, activity.conversationRefreshCount);
}
@@ -77,7 +99,8 @@ public class MainActivityRealtimeTest {
@Test
public void deviceScopedConversationEventRefreshesVisibleConversationTab() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
showConversationTab(activity);
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
@@ -86,7 +109,7 @@ public class MainActivityRealtimeTest {
new BossRealtimeEvent("conversation.updated", new JSONObject().put("deviceId", "mac-studio"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(1, activity.conversationRefreshCount);
assertEquals(0, activity.deviceRefreshCount);
@@ -95,7 +118,8 @@ public class MainActivityRealtimeTest {
@Test
public void contextIndicatorEventRefreshesVisibleConversationTab() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
showConversationTab(activity);
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
@@ -107,16 +131,17 @@ public class MainActivityRealtimeTest {
)
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(1, activity.conversationRefreshCount);
assertEquals(0, activity.conversationRefreshCount);
assertEquals(0, activity.deviceRefreshCount);
}
@Test
public void contextIndicatorSnapshotWithoutProjectIdRefreshesVisibleConversationTab() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
showConversationTab(activity);
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
@@ -128,16 +153,17 @@ public class MainActivityRealtimeTest {
)
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(1, activity.conversationRefreshCount);
assertEquals(0, activity.conversationRefreshCount);
assertEquals(0, activity.deviceRefreshCount);
}
@Test
public void distinctConversationEventsBackToBackBothRefreshVisibleConversationTab() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
showConversationTab(activity);
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
@@ -160,22 +186,24 @@ public class MainActivityRealtimeTest {
)
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(2, activity.conversationRefreshCount);
assertEquals(1, activity.conversationRefreshCount);
assertEquals(0, activity.deviceRefreshCount);
}
@Test
public void devicesRealtimeEventRefreshesVisibleDevicesTabOnly() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
showConversationTab(activity);
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.callInstanceMethod(
activity,
"setActiveTab",
ReflectionHelpers.ClassParameter.from(String.class, "devices"),
ReflectionHelpers.ClassParameter.from(boolean.class, false)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
@@ -184,7 +212,7 @@ public class MainActivityRealtimeTest {
new BossRealtimeEvent("devices.updated", new JSONObject().put("deviceId", "mac-studio"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(0, activity.conversationRefreshCount);
assertEquals(1, activity.deviceRefreshCount);
@@ -194,13 +222,15 @@ public class MainActivityRealtimeTest {
@Test
public void otaRealtimeEventRefreshesVisibleMeTabOnly() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
showConversationTab(activity);
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.callInstanceMethod(
activity,
"setActiveTab",
ReflectionHelpers.ClassParameter.from(String.class, "me"),
ReflectionHelpers.ClassParameter.from(boolean.class, false)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
@@ -209,7 +239,7 @@ public class MainActivityRealtimeTest {
new BossRealtimeEvent("ota.updated", new JSONObject())
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(0, activity.conversationRefreshCount);
assertEquals(0, activity.deviceRefreshCount);
@@ -219,7 +249,8 @@ public class MainActivityRealtimeTest {
@Test
public void burstConversationRealtimeEventsCoalesceIntoSingleFollowUpRefresh() throws Exception {
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
showConversationTab(activity);
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.setField(activity, "rootTabRefreshInFlight", true);
ReflectionHelpers.callInstanceMethod(
@@ -230,7 +261,7 @@ public class MainActivityRealtimeTest {
new BossRealtimeEvent("conversation.updated", new JSONObject().put("projectId", "project-1"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
ReflectionHelpers.callInstanceMethod(
activity,

View File

@@ -24,6 +24,10 @@ import java.util.function.BooleanSupplier;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class ProjectDetailActivityRealtimeTest {
private static void flushRealtimeDebounce(ProjectDetailActivity activity) {
Shadows.shadowOf(activity.getMainLooper()).idleFor(400, TimeUnit.MILLISECONDS);
}
@Test
public void matchingProjectMessageEventTriggersReload() throws Exception {
Intent intent = new Intent()
@@ -43,9 +47,11 @@ public class ProjectDetailActivityRealtimeTest {
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-1"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(1, activity.reloadCount);
assertEquals(0, activity.reloadCount);
assertEquals(1, activity.loadCallCount);
assertEquals(1, activity.renderCount);
}
@Test
@@ -67,7 +73,7 @@ public class ProjectDetailActivityRealtimeTest {
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-2"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(0, activity.reloadCount);
}
@@ -91,7 +97,7 @@ public class ProjectDetailActivityRealtimeTest {
new BossRealtimeEvent("master_agent.task.updated", new JSONObject().put("projectId", "project-2"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(0, activity.reloadCount);
}
@@ -129,9 +135,11 @@ public class ProjectDetailActivityRealtimeTest {
)
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(2, activity.reloadCount);
assertEquals(1, activity.reloadCount);
assertEquals(1, activity.loadCallCount);
assertEquals(1, activity.renderCount);
}
@Test
@@ -156,9 +164,11 @@ public class ProjectDetailActivityRealtimeTest {
)
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(1, activity.reloadCount);
assertEquals(1, activity.loadCallCount);
assertEquals(1, activity.renderCount);
}
@Test
@@ -194,9 +204,11 @@ public class ProjectDetailActivityRealtimeTest {
)
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(1, activity.reloadCount);
assertEquals(0, activity.reloadCount);
assertEquals(1, activity.loadCallCount);
assertEquals(1, activity.renderCount);
}
@Test
@@ -220,7 +232,7 @@ public class ProjectDetailActivityRealtimeTest {
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-1"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertTrue(activity.awaitFirstLoadStarted());
ReflectionHelpers.callInstanceMethod(
@@ -239,7 +251,7 @@ public class ProjectDetailActivityRealtimeTest {
new BossRealtimeEvent("master_agent.task.updated", new JSONObject().put("projectId", "project-1"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
flushRealtimeDebounce(activity);
assertEquals(1, activity.loadCallCount);
assertEquals(0, activity.renderCount);
@@ -315,6 +327,11 @@ public class ProjectDetailActivityRealtimeTest {
);
}
@Override
ProjectSnapshot loadProjectMessagesSnapshotForRefresh() throws Exception {
return loadProjectSnapshotForRefresh();
}
@Override
void renderLoadedProjectSnapshot(ProjectSnapshot snapshot) {
renderCount += 1;

View File

@@ -714,9 +714,9 @@ public class ProjectDetailActivityUiTest {
}
@Override
protected void enqueueReplyWaitPoll(String baselineMessageId, boolean includeDispatchPlans) {
protected void enqueueReplyWaitPoll(ProjectChatUiState.ReplyWaitSpec waitSpec, boolean includeDispatchPlans) {
replyWaitPollCount += 1;
lastReplyWaitBaselineMessageId = baselineMessageId;
lastReplyWaitBaselineMessageId = waitSpec == null ? null : waitSpec.baselineMessageId;
lastReplyWaitIncludeDispatchPlans = includeDispatchPlans;
}
}