feat: refine mobile master agent sync and chat rendering

This commit is contained in:
kris
2026-04-18 04:51:50 +08:00
parent e0c0ea1814
commit 449f84fcbc
61 changed files with 7051 additions and 1075 deletions

View File

@@ -5,12 +5,14 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".BossApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:forceDarkAllowed="false">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"

File diff suppressed because it is too large Load Diff

View File

@@ -147,6 +147,22 @@ public class BossApiClient {
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
}
public ApiResponse updateMasterAgentModeModels(
@Nullable String fastModelOverride,
@Nullable String deepModelOverride,
@Nullable String modelOverride,
@Nullable String reasoningEffortOverride
) throws IOException, JSONException {
JSONObject payload = new JSONObject();
payload.put("fastModelOverride", fastModelOverride == null ? JSONObject.NULL : fastModelOverride);
payload.put("deepModelOverride", deepModelOverride == null ? JSONObject.NULL : deepModelOverride);
if (modelOverride != null || reasoningEffortOverride != null) {
payload.put("modelOverride", modelOverride == null ? JSONObject.NULL : modelOverride);
payload.put("reasoningEffortOverride", reasoningEffortOverride == null ? JSONObject.NULL : reasoningEffortOverride);
}
return requestWithRestore("POST", "/api/v1/projects/master-agent/agent-controls", payload);
}
public ApiResponse updateProjectTakeoverSettings(
String projectId,
@Nullable Boolean takeoverEnabled,
@@ -490,6 +506,10 @@ public class BossApiClient {
return requestWithRestore("POST", "/api/v1/accounts/" + encode(accountId) + "/validate", new JSONObject());
}
public ApiResponse validateDraftAccount(JSONObject payload) throws IOException, JSONException {
return requestWithRestore("POST", "/api/v1/accounts/validate-draft", payload);
}
public ApiResponse onboardOpenAiApiAccount(JSONObject payload) throws IOException, JSONException {
return onboardAccount("/api/v1/accounts/onboard/openai-api", payload);
}

View File

@@ -0,0 +1,13 @@
package com.hyzq.boss;
import android.app.Application;
import androidx.appcompat.app.AppCompatDelegate;
public final class BossApplication extends Application {
@Override
public void onCreate() {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
super.onCreate();
}
}

View File

@@ -27,6 +27,7 @@ public final class BossMarkdown {
private static final Pattern HEADING_PATTERN = Pattern.compile("^(#{1,3})\\s+(.+)$");
private static final Pattern BULLET_PATTERN = Pattern.compile("^[-*]\\s+(.+)$");
private static final Pattern ORDERED_PATTERN = Pattern.compile("^(\\d+)\\.\\s+(.+)$");
private static final Pattern LABEL_SECTION_PATTERN = Pattern.compile("^([^:\\n]{1,24})[:]\\s*(.+)$");
private static final Pattern INLINE_TOKEN_PATTERN = Pattern.compile("(\\*\\*([^*]+)\\*\\*)|(`([^`]+)`)");;
private static final LruCache<String, CharSequence> RENDER_CACHE = new LruCache<>(180);
@@ -86,6 +87,12 @@ public final class BossMarkdown {
continue;
}
Matcher labelMatcher = LABEL_SECTION_PATTERN.matcher(trimmed);
if (labelMatcher.matches()) {
appendLabelSection(builder, labelMatcher.group(1), labelMatcher.group(2), palette);
continue;
}
if (trimmed.startsWith(">")) {
appendQuote(builder, trimmed.substring(1).trim(), palette);
continue;
@@ -153,6 +160,22 @@ public final class BossMarkdown {
builder.append('\n');
}
private static void appendLabelSection(
SpannableStringBuilder builder,
String label,
String content,
Palette palette
) {
ensureBlockSeparation(builder, true);
int labelStart = builder.length();
builder.append(label.trim());
builder.setSpan(new StyleSpan(Typeface.BOLD), labelStart, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new RelativeSizeSpan(1.03f), labelStart, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.append('\n');
appendInlineStyled(builder, content.trim(), palette);
builder.append('\n');
}
private static void appendCodeBlock(SpannableStringBuilder builder, String text, Palette palette) {
if (TextUtils.isEmpty(text)) {
return;

View File

@@ -13,6 +13,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -90,6 +91,11 @@ final class BossRealtimeClient {
if (!running) {
return;
}
if (shouldReconnectImmediately(error)) {
Log.i(TAG, "Realtime stream timed out while idle; reconnecting immediately");
backoffMs = INITIAL_BACKOFF_MS;
continue;
}
if (shouldAttemptSessionRestore(error)) {
try {
BossApiClient.ApiResponse restored = apiClient.restoreSession();
@@ -174,6 +180,10 @@ final class BossRealtimeClient {
&& apiClient.hasRestoreToken();
}
static boolean shouldReconnectImmediately(@Nullable Exception error) {
return error instanceof SocketTimeoutException;
}
private void dispatchEventBlock(String rawBlock) {
BossRealtimeEvent event = parseEventBlock(rawBlock);
if (event == null || event.eventName.isEmpty()) {

View File

@@ -15,6 +15,8 @@ import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
@@ -475,6 +477,10 @@ public final class BossUi {
cell.addView(helperView);
}
ViewParent currentParent = field.getParent();
if (currentParent instanceof ViewGroup) {
((ViewGroup) currentParent).removeView(field);
}
field.setPadding(field.getPaddingLeft(), field.getPaddingTop(), field.getPaddingRight(), field.getPaddingBottom());
cell.addView(field);
return cell;

View File

@@ -1,5 +1,6 @@
package com.hyzq.boss;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
@@ -11,6 +12,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
@@ -141,7 +143,17 @@ public class MainActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
apiClient = new BossApiClient(this);
realtimeClient = new BossRealtimeClient(apiClient, this::handleRealtimeEvent);
realtimeClient = new BossRealtimeClient(apiClient, new BossRealtimeClient.Listener() {
@Override
public void onRealtimeEvent(BossRealtimeEvent event) {
handleRealtimeEvent(event);
}
@Override
public void onRealtimeConnectionChanged(boolean connected) {
runOnUiThread(() -> handleRealtimeConnectionChanged(connected));
}
});
bindViews();
bindActions();
applyInitialTab(getIntent());
@@ -619,11 +631,8 @@ public class MainActivity extends AppCompatActivity {
}
private void scheduleRealtimeRefresh() {
if (realtimeRefreshScheduled) {
return;
}
realtimeRefreshScheduled = true;
uiHandler.postDelayed(realtimeRefreshRunnable, REALTIME_REFRESH_DEBOUNCE_MS);
realtimeRefreshScheduled = false;
refreshCurrentTab();
}
private void cancelRealtimeRefreshSchedule() {
@@ -653,7 +662,9 @@ public class MainActivity extends AppCompatActivity {
private boolean shouldRefreshConversationsTab(BossRealtimeEvent event) {
if ("conversation.context_indicator.updated".equals(event.eventName)) {
return false;
return hasProjectId(event)
|| hasDeviceId(event)
|| event.payload.optJSONArray("conversations") != null;
}
if ("conversation.updated".equals(event.eventName)) {
return hasProjectId(event) || hasDeviceId(event);
@@ -1166,6 +1177,16 @@ public class MainActivity extends AppCompatActivity {
showMessage("缺少 folderKey");
return;
}
if (conversationSearchMode) {
String matchedProjectId = item.optString("searchMatchProjectId", "").trim();
String matchedProjectLabel = item.optString("searchMatchLabel", "").trim();
if (!matchedProjectId.isEmpty() && !matchedProjectLabel.isEmpty()) {
exitConversationSearchMode(true);
openProject(matchedProjectId, matchedProjectLabel);
return;
}
exitConversationSearchMode(true);
}
openConversationFolder(
folderKey,
resolveConversationFolderName(item, row),
@@ -1180,6 +1201,9 @@ public class MainActivity extends AppCompatActivity {
return;
}
String projectName = finalDisplayRow.threadTitle.isEmpty() ? "未命名会话" : finalDisplayRow.threadTitle;
if (conversationSearchMode) {
exitConversationSearchMode(true);
}
openProject(projectId, projectName);
})
));
@@ -1292,10 +1316,7 @@ public class MainActivity extends AppCompatActivity {
hideConversationQuickActions(false);
conversationSearchMode = true;
syncTopActionVisualState(screenRefresh.isRefreshing());
topSearchInput.post(() -> {
topSearchInput.requestFocus();
topSearchInput.setSelection(topSearchInput.getText().length());
});
showConversationSearchKeyboard();
}
private void exitConversationSearchMode(boolean clearQuery) {
@@ -1308,12 +1329,40 @@ public class MainActivity extends AppCompatActivity {
conversationSearchQuery = "";
topSearchInput.setText("");
}
hideConversationSearchKeyboard();
syncTopActionVisualState(screenRefresh != null && screenRefresh.isRefreshing());
if (queryChanged && "conversations".equals(activeTab) && contentPanel.getVisibility() == View.VISIBLE) {
renderConversationsRoot();
}
}
private void showConversationSearchKeyboard() {
if (topSearchInput == null) {
return;
}
topSearchInput.post(() -> {
topSearchInput.requestFocus();
topSearchInput.setSelection(topSearchInput.getText().length());
InputMethodManager inputMethodManager =
(InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null) {
inputMethodManager.showSoftInput(topSearchInput, InputMethodManager.SHOW_IMPLICIT);
}
});
}
private void hideConversationSearchKeyboard() {
if (topSearchInput == null) {
return;
}
InputMethodManager inputMethodManager =
(InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null) {
inputMethodManager.hideSoftInputFromWindow(topSearchInput.getWindowToken(), 0);
}
topSearchInput.clearFocus();
}
private void toggleConversationSelection(String projectId) {
if (selectedConversationProjectIds.contains(projectId)) {
selectedConversationProjectIds.remove(projectId);
@@ -1807,6 +1856,17 @@ public class MainActivity extends AppCompatActivity {
}
}
void handleRealtimeConnectionChanged(boolean connected) {
if (!connected
&& shouldMaintainConversationAutoRefresh()
&& !rootTabRefreshInFlight
&& screenRefresh != null
&& !screenRefresh.isRefreshing()) {
refreshCurrentTab();
}
updateConversationAutoRefresh();
}
private void openMeEntry(String key) {
Intent intent;
switch (key) {

View File

@@ -0,0 +1,121 @@
package com.hyzq.boss;
import android.text.TextUtils;
import androidx.annotation.Nullable;
final class MasterAgentModePresets {
static final class ModePreset {
final String key;
final String label;
@Nullable final String modelOverride;
@Nullable final String reasoningEffortOverride;
ModePreset(
String key,
String label,
@Nullable String modelOverride,
@Nullable String reasoningEffortOverride
) {
this.key = key;
this.label = label;
this.modelOverride = modelOverride;
this.reasoningEffortOverride = reasoningEffortOverride;
}
}
static final ModePreset DEFAULT = new ModePreset("default", "沿用默认", null, null);
private static final String DEFAULT_FAST_MODEL = "gpt-5.4-mini";
private static final String DEFAULT_DEEP_MODEL = "gpt-5.4";
private MasterAgentModePresets() {}
static ModePreset[] primaryChoices(@Nullable String fastModelOverride, @Nullable String deepModelOverride) {
return new ModePreset[]{
DEFAULT,
new ModePreset("fast", "快速反应", resolveFastModel(fastModelOverride), "low"),
new ModePreset("deep", "深度思考", resolveDeepModel(deepModelOverride), "high")
};
}
static String[] primaryChoiceLabels(@Nullable String fastModelOverride, @Nullable String deepModelOverride) {
return new String[]{
"沿用默认",
"快速反应(" + resolveFastModel(fastModelOverride) + "",
"深度思考(" + resolveDeepModel(deepModelOverride) + "",
"更多模型..."
};
}
static int findPrimaryChoiceIndex(
@Nullable String modelOverride,
@Nullable String reasoningEffortOverride,
@Nullable String fastModelOverride,
@Nullable String deepModelOverride
) {
ModePreset preset = matchPreset(modelOverride, reasoningEffortOverride, fastModelOverride, deepModelOverride);
if (preset == null) {
return primaryChoiceLabels(fastModelOverride, deepModelOverride).length - 1;
}
ModePreset[] choices = primaryChoices(fastModelOverride, deepModelOverride);
for (int index = 0; index < choices.length; index += 1) {
if (choices[index].key.equals(preset.key)) {
return index;
}
}
return 0;
}
@Nullable
static ModePreset matchPreset(
@Nullable String modelOverride,
@Nullable String reasoningEffortOverride,
@Nullable String fastModelOverride,
@Nullable String deepModelOverride
) {
String model = normalize(modelOverride);
String reasoning = normalize(reasoningEffortOverride);
if (TextUtils.isEmpty(model) && TextUtils.isEmpty(reasoning)) {
return DEFAULT;
}
for (ModePreset preset : primaryChoices(fastModelOverride, deepModelOverride)) {
if (TextUtils.equals(normalize(preset.modelOverride), model)
&& TextUtils.equals(normalize(preset.reasoningEffortOverride), reasoning)) {
return preset;
}
}
return null;
}
static String describeCurrentMode(
@Nullable String modelOverride,
@Nullable String reasoningEffortOverride,
@Nullable String fastModelOverride,
@Nullable String deepModelOverride
) {
ModePreset preset = matchPreset(modelOverride, reasoningEffortOverride, fastModelOverride, deepModelOverride);
return preset == null ? "自定义" : preset.label;
}
static String resolveFastModel(@Nullable String fastModelOverride) {
String resolved = normalize(fastModelOverride);
return TextUtils.isEmpty(resolved) ? DEFAULT_FAST_MODEL : resolved;
}
static String resolveDeepModel(@Nullable String deepModelOverride) {
String resolved = normalize(deepModelOverride);
return TextUtils.isEmpty(resolved) ? DEFAULT_DEEP_MODEL : resolved;
}
@Nullable
private static String normalize(@Nullable String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
if (trimmed.isEmpty() || "null".equalsIgnoreCase(trimmed)) {
return null;
}
return trimmed;
}
}

View File

@@ -39,6 +39,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
public class ProjectDetailActivity extends BossScreenActivity {
public static final String EXTRA_PROJECT_ID = "project_id";
@@ -55,6 +56,8 @@ public class ProjectDetailActivity extends BossScreenActivity {
private String projectFolderName;
private @Nullable String currentAgentModelOverride;
private @Nullable String currentReasoningEffortOverride;
private @Nullable String currentFastModelOverride;
private @Nullable String currentDeepModelOverride;
private LinearLayout quickActionsLayout;
private LinearLayout composerRow;
private LinearLayout multiSelectActionsLayout;
@@ -65,6 +68,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
private ScrollView chatScrollView;
private View pendingOutgoingBubble;
private boolean composerSending;
private @Nullable String pendingReplyPresenter;
private boolean renderNearBottom;
private boolean renderForcedScrollToBottom;
private boolean conversationInfoReady;
@@ -102,6 +106,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
private boolean reloadInFlight;
private boolean pendingReload;
private boolean pendingReloadForcedScrollToBottom;
private volatile boolean activityDestroyed;
private final Runnable conversationAutoRefreshRunnable = new Runnable() {
@Override
public void run() {
@@ -124,6 +129,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
triggerRealtimeReload(requireFullSnapshot);
}
};
private final Runnable composerViewportSyncRunnable = new Runnable() {
@Override
public void run() {
syncChatViewportForComposer();
}
};
static final class ChromeBindings {
final boolean multiSelecting;
@@ -263,10 +274,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
@Override
public void onRealtimeConnectionChanged(boolean connected) {
runOnUiThread(() -> {
lastKnownRealtimeConnected = connected;
syncRealtimeStatusIndicator();
});
runOnUiThread(() -> handleRealtimeConnectionChanged(connected));
}
});
@@ -297,6 +305,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
@Override
public void afterTextChanged(Editable s) {}
});
bindComposerViewportSync();
updateComposerSendButtonState();
updateSelectionUi();
if (shouldLoadOnCreate()) {
@@ -306,9 +315,11 @@ public class ProjectDetailActivity extends BossScreenActivity {
@Override
protected void onDestroy() {
activityDestroyed = true;
cancelConversationAutoRefresh();
cancelRealtimeReloadSchedule();
stopRealtimeUpdates();
uiHandler.removeCallbacksAndMessages(null);
replyWaitExecutor.shutdownNow();
super.onDestroy();
}
@@ -360,7 +371,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (tryApplyRealtimeMessagesPatch(event)) {
return;
}
runOnUiThread(() -> scheduleRealtimeReload(!"project.messages.updated".equals(event.eventName)));
runOnUiThread(() -> scheduleRealtimeReload(true));
}
private boolean tryApplyRealtimeMessagesPatch(BossRealtimeEvent event) {
@@ -523,14 +534,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
private void scheduleRealtimeReload(boolean requireFullSnapshot) {
if (requireFullSnapshot) {
realtimeReloadRequiresFullSnapshot = true;
}
if (realtimeReloadScheduled) {
return;
}
realtimeReloadScheduled = true;
uiHandler.postDelayed(realtimeReloadRunnable, REALTIME_REFRESH_DEBOUNCE_MS);
realtimeReloadRequiresFullSnapshot = false;
realtimeReloadScheduled = false;
triggerRealtimeReload(requireFullSnapshot);
}
private void cancelRealtimeReloadSchedule() {
@@ -547,6 +553,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
private void reloadSnapshot(boolean forcedScrollToBottom, boolean messagesOnly) {
if (shouldSkipAsyncUiWork()) {
return;
}
if (projectId == null || projectId.isEmpty()) {
showMessage("缺少 projectId");
finish();
@@ -561,27 +570,45 @@ public class ProjectDetailActivity extends BossScreenActivity {
renderForcedScrollToBottom = forcedScrollToBottom;
reloadInFlight = true;
setRefreshing(true);
executor.execute(() -> {
try {
ProjectSnapshot snapshot = messagesOnly
? loadProjectMessagesSnapshotForRefresh()
: loadProjectSnapshotForRefresh();
runOnUiThread(() -> {
renderLoadedProjectSnapshot(snapshot);
finishReloadCycle();
});
} catch (Exception error) {
runOnUiThread(() -> {
if (messagesOnly) {
reloadInFlight = false;
setRefreshing(false);
reload(forcedScrollToBottom);
return;
}
handleProjectReloadFailure(error);
finishReloadCycle();
});
try {
executor.execute(() -> {
try {
ProjectSnapshot snapshot = messagesOnly
? loadProjectMessagesSnapshotForRefresh()
: loadProjectSnapshotForRefresh();
runOnUiThreadIfActive(() -> {
renderLoadedProjectSnapshot(snapshot);
finishReloadCycle();
});
} catch (Exception error) {
runOnUiThreadIfActive(() -> {
if (messagesOnly) {
reloadInFlight = false;
setRefreshing(false);
reload(forcedScrollToBottom);
return;
}
handleProjectReloadFailure(error);
finishReloadCycle();
});
}
});
} catch (RejectedExecutionException ignored) {
reloadInFlight = false;
setRefreshing(false);
}
}
private boolean shouldSkipAsyncUiWork() {
return activityDestroyed || isFinishing() || isDestroyed();
}
private void runOnUiThreadIfActive(Runnable action) {
runOnUiThread(() -> {
if (shouldSkipAsyncUiWork()) {
return;
}
action.run();
});
}
@@ -654,6 +681,8 @@ 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));
currentDeepModelOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("deepModelOverride", null));
if (dispatchPlans != null) {
currentPendingDispatchPlan = ProjectChatUiState.latestPendingDispatchPlan(dispatchPlans);
currentRejectedDispatchPlan = currentPendingDispatchPlan == null
@@ -752,6 +781,19 @@ public class ProjectDetailActivity extends BossScreenActivity {
syncRealtimeStatusIndicator();
}
void handleRealtimeConnectionChanged(boolean connected) {
lastKnownRealtimeConnected = connected;
syncRealtimeStatusIndicator();
if (!connected
&& shouldMaintainConversationAutoRefresh()
&& !reloadInFlight
&& refreshLayout != null
&& !refreshLayout.isRefreshing()) {
reload();
}
updateConversationAutoRefresh();
}
private boolean shouldMaintainConversationAutoRefresh() {
return conversationAutoRefreshEnabled
&& apiClient != null
@@ -838,6 +880,47 @@ public class ProjectDetailActivity extends BossScreenActivity {
sendProjectMessage("text", body);
}
private void bindComposerViewportSync() {
if (composerInput != null) {
composerInput.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
scheduleComposerViewportSync();
}
});
composerInput.setOnClickListener(v -> scheduleComposerViewportSync());
}
if (composerRow != null) {
composerRow.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if ((bottom - top) == (oldBottom - oldTop) && bottom == oldBottom) {
return;
}
if (shouldKeepLatestMessageVisibleForInput()) {
scheduleComposerViewportSync();
}
});
}
}
private void scheduleComposerViewportSync() {
uiHandler.removeCallbacks(composerViewportSyncRunnable);
uiHandler.post(composerViewportSyncRunnable);
uiHandler.postDelayed(composerViewportSyncRunnable, 96L);
}
private void syncChatViewportForComposer() {
if (!shouldKeepLatestMessageVisibleForInput()) {
return;
}
scrollChatToBottom();
}
private boolean shouldKeepLatestMessageVisibleForInput() {
return composerInput != null
&& composerInput.isFocused()
&& composerRow != null
&& composerRow.getVisibility() == View.VISIBLE;
}
private void showAttachmentEntrySheet() {
if (isComposerBusy()) {
return;
@@ -958,6 +1041,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
JSONObject dispatchPlan = response.json.optJSONObject("dispatchPlan");
JSONObject collaborationGate = response.json.optJSONObject("collaborationGate");
String replyPresenter = response.json.optString("replyPresenter", "").trim();
ProjectChatUiState.ReplyWaitSpec waitSpec =
ProjectChatUiState.resolveReplyWaitAfterSend(response.json);
runOnUiThread(() -> {
@@ -970,8 +1054,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
currentPendingDispatchPlan = dispatchPlan;
currentRejectedDispatchPlan = null;
pendingReplyPresenter = replyPresenter.isEmpty() ? null : replyPresenter;
if (dispatchPlan != null) {
composerSending = false;
pendingReplyPresenter = null;
updateComposerSendButtonState();
showMessage(
"approval_required".equals(projectCollaborationMode)
@@ -982,21 +1068,22 @@ public class ProjectDetailActivity extends BossScreenActivity {
return;
}
if (waitSpec.shouldWait) {
if (isMasterAgentConversation()) {
startMasterAgentReplyWait(waitSpec, false, "消息已发送,主 Agent 思考中");
} else {
startReplyWait(waitSpec, false, "消息已发送,正在等待回复…");
}
startReplyWait(waitSpec, false, buildReplyWaitWaitingMessage());
return;
}
composerSending = false;
pendingReplyPresenter = null;
updateComposerSendButtonState();
if (tryApplyCompletedSendResponse(response.json)) {
return;
}
showMessage("消息已发送");
reload(true);
});
} catch (Exception error) {
runOnUiThread(() -> {
composerSending = false;
pendingReplyPresenter = null;
setRefreshing(false);
removePendingOutgoingBubble();
showMessage("发送失败:" + error.getMessage());
@@ -1006,6 +1093,51 @@ public class ProjectDetailActivity extends BossScreenActivity {
});
}
private boolean tryApplyCompletedSendResponse(@Nullable JSONObject response) {
if (response == null || currentRenderedProjectPayload == null) {
return false;
}
JSONObject sentMessage = response.optJSONObject("message");
JSONObject replyMessage = response.optJSONObject("replyMessage");
if (sentMessage == null || replyMessage == null) {
return false;
}
JSONObject payload = copyJson(currentRenderedProjectPayload);
JSONObject project = payload.optJSONObject("project");
if (project == null || !TextUtils.equals(project.optString("id", "").trim(), projectId)) {
return false;
}
JSONArray messages = project.optJSONArray("messages");
if (messages == null) {
return false;
}
appendMessageIfMissing(messages, sentMessage);
appendMessageIfMissing(messages, replyMessage);
removePendingOutgoingBubble();
clearMasterAgentReplyState();
renderNearBottom = true;
renderForcedScrollToBottom = true;
renderLoadedProjectSnapshot(new ProjectSnapshot(payload, null, currentParticipantsPayload));
return true;
}
private void appendMessageIfMissing(JSONArray messages, JSONObject message) {
if (messages == null || message == null) {
return;
}
String messageId = message.optString("id", "").trim();
if (messageId.isEmpty()) {
return;
}
for (int index = 0; index < messages.length(); index += 1) {
JSONObject existing = messages.optJSONObject(index);
if (existing != null && TextUtils.equals(existing.optString("id", "").trim(), messageId)) {
return;
}
}
messages.put(copyJson(message));
}
private void showThreadExecutionConflictDialog(JSONObject executionConflict, String body, String kind) {
String preferredMode = "gui".equals(executionConflict.optString("preferredExecutionMode", "cli"))
? "GUI"
@@ -1251,10 +1383,45 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (!isMasterAgentConversation()) {
return;
}
final MasterAgentModePresets.ModePreset[] presets = MasterAgentModePresets.primaryChoices(
currentFastModelOverride,
currentDeepModelOverride
);
final String[] options = MasterAgentModePresets.primaryChoiceLabels(
currentFastModelOverride,
currentDeepModelOverride
);
int checkedIndex = MasterAgentModePresets.findPrimaryChoiceIndex(
currentAgentModelOverride,
currentReasoningEffortOverride,
currentFastModelOverride,
currentDeepModelOverride
);
new AlertDialog.Builder(this)
.setTitle("模型")
.setSingleChoiceItems(options, checkedIndex, (dialog, which) -> {
if (which == options.length - 1) {
dialog.dismiss();
showAdvancedMasterAgentModelPicker();
return;
}
MasterAgentModePresets.ModePreset preset = presets[which];
dialog.dismiss();
updateMasterAgentControls(
preset.modelOverride,
preset.reasoningEffortOverride,
"主Agent模式已切换为 " + preset.label
);
})
.setNegativeButton("取消", null)
.show();
}
private void showAdvancedMasterAgentModelPicker() {
final String[] options = buildMasterAgentModelOptions();
int checkedIndex = findCheckedIndex(options, currentAgentModelOverride);
new AlertDialog.Builder(this)
.setTitle("模型")
.setTitle("更多模型")
.setSingleChoiceItems(options, checkedIndex, (dialog, which) -> {
if (which == 0) {
dialog.dismiss();
@@ -1275,7 +1442,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
private void showCustomMasterAgentModelDialog() {
final EditText input = BossUi.buildInput(this, "模型,例如 gpt-5.4", false);
input.setText(TextUtils.isEmpty(currentAgentModelOverride) ? "gpt-5.4" : currentAgentModelOverride);
input.setText(TextUtils.isEmpty(currentAgentModelOverride) ? "gpt-5.4-mini" : currentAgentModelOverride);
new AlertDialog.Builder(this)
.setTitle("自定义模型")
.setView(input)
@@ -1351,14 +1518,11 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (!TextUtils.isEmpty(currentAgentModelOverride)) {
options.add(currentAgentModelOverride);
}
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-4.1")) {
options.add("gpt-4.1");
String[] preferredModels = new String[]{"gpt-5.4-mini", "gpt-5.4", "gpt-5.1", "gpt-4.1"};
for (String model : preferredModels) {
if (!options.contains(model)) {
options.add(model);
}
}
options.add("自定义...");
return options.toArray(new String[0]);
@@ -2134,9 +2298,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
masterAgentReplyWaiting = false;
masterAgentReplyTimedOut = false;
masterAgentReplyBaselineMessageId = null;
pendingReplyPresenter = null;
}
private void scrollChatToBottom() {
void scrollChatToBottom() {
if (chatScrollView == null) {
return;
}
@@ -2555,7 +2720,45 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
protected void enqueueReplyWaitPoll(@Nullable String baselineMessageId, boolean includeDispatchPlans) {
replyWaitExecutor.execute(() -> pollUntilReply(baselineMessageId, includeDispatchPlans));
if (shouldSkipAsyncUiWork() || replyWaitExecutor.isShutdown() || replyWaitExecutor.isTerminated()) {
return;
}
try {
replyWaitExecutor.execute(() -> pollUntilReply(baselineMessageId, includeDispatchPlans));
} catch (RejectedExecutionException ignored) {
// Activity is already finishing; ignore late polling requests.
}
}
private String buildReplyWaitWaitingMessage() {
if ("master".equals(pendingReplyPresenter) && !isMasterAgentConversation()) {
return "消息已发送,主 Agent 正在转述";
}
return isMasterAgentConversation()
? "消息已发送,主 Agent 思考中"
: "消息已发送,线程正在处理中";
}
private String buildReplyWaitTimeoutMessage() {
if ("master".equals(pendingReplyPresenter) && !isMasterAgentConversation()) {
return "主 Agent 转述暂未回流,可继续等待或稍后刷新查看。";
}
return isMasterAgentConversation()
? "主 Agent 回复超时,可重试等待最新回复。"
: "当前线程暂未回流,继续后台等待或稍后刷新查看。";
}
private String buildReplyWaitFailureMessage(Exception error) {
String detail = error.getMessage();
if (detail == null || detail.trim().isEmpty()) {
detail = "未知错误";
}
if ("master".equals(pendingReplyPresenter) && !isMasterAgentConversation()) {
return "等待主 Agent 转述失败:" + detail;
}
return isMasterAgentConversation()
? "等待主 Agent 回复失败:" + detail
: "等待线程回复失败:" + detail;
}
private void pollUntilReply(
@@ -2565,13 +2768,15 @@ public class ProjectDetailActivity extends BossScreenActivity {
long deadlineAt = System.currentTimeMillis() + REPLY_WAIT_TIMEOUT_MS;
boolean renderedInitialSnapshot = false;
try {
while (!Thread.currentThread().isInterrupted() && System.currentTimeMillis() < deadlineAt) {
while (!shouldSkipAsyncUiWork()
&& !Thread.currentThread().isInterrupted()
&& System.currentTimeMillis() < deadlineAt) {
ProjectSnapshot snapshot = fetchProjectSnapshot(includeDispatchPlans);
JSONObject project = snapshot.payload.optJSONObject("project");
boolean hasReply = ProjectChatUiState.hasReplyBeyondBaseline(project, baselineMessageId);
if (!renderedInitialSnapshot || hasReply) {
runOnUiThread(() -> {
runOnUiThreadIfActive(() -> {
renderProject(snapshot.payload, snapshot.dispatchPlans, snapshot.participantsPayload);
if (!hasReply && !isMasterAgentConversation()) {
composerSending = true;
@@ -2583,7 +2788,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
if (hasReply) {
runOnUiThread(() -> {
runOnUiThreadIfActive(() -> {
clearMasterAgentReplyState();
composerSending = false;
updateComposerSendButtonState();
@@ -2596,7 +2801,11 @@ public class ProjectDetailActivity extends BossScreenActivity {
Thread.sleep(REPLY_WAIT_POLL_INTERVAL_MS);
}
runOnUiThread(() -> {
if (shouldSkipAsyncUiWork() || Thread.currentThread().isInterrupted()) {
return;
}
runOnUiThreadIfActive(() -> {
if (isMasterAgentConversation()) {
masterAgentReplyWaiting = false;
masterAgentReplyTimedOut = true;
@@ -2604,11 +2813,16 @@ public class ProjectDetailActivity extends BossScreenActivity {
composerSending = false;
updateComposerSendButtonState();
setRefreshing(false);
showMessage("主 Agent 回复超时,可重试等待最新回复。");
showMessage(buildReplyWaitTimeoutMessage());
reload(false);
});
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
} catch (Exception error) {
runOnUiThread(() -> {
if (shouldSkipAsyncUiWork() || Thread.currentThread().isInterrupted()) {
return;
}
runOnUiThreadIfActive(() -> {
if (isMasterAgentConversation()) {
masterAgentReplyWaiting = false;
masterAgentReplyTimedOut = true;
@@ -2616,7 +2830,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
composerSending = false;
updateComposerSendButtonState();
setRefreshing(false);
showMessage("等待回复失败:" + error.getMessage());
showMessage(buildReplyWaitFailureMessage(error));
reload(false);
});
}

View File

@@ -168,6 +168,25 @@ public class ProjectGoalsActivity extends BossScreenActivity {
""
));
JSONObject understanding = project.optJSONObject("projectUnderstanding");
if (understanding != null) {
String projectGoal = understanding.optString("projectGoal").trim();
String currentProgress = understanding.optString("currentProgress").trim();
String recommendedNextStep = understanding.optString("recommendedNextStep").trim();
if (!projectGoal.isEmpty() || !currentProgress.isEmpty() || !recommendedNextStep.isEmpty()) {
StringBuilder summary = new StringBuilder();
appendSummaryLine(summary, "项目目标", projectGoal);
appendSummaryLine(summary, "当前进度", currentProgress);
appendSummaryLine(summary, "建议下一步", recommendedNextStep);
appendContent(BossUi.buildCard(
this,
"同步项目摘要",
summary.toString().trim(),
understanding.optString("updatedAt", "")
));
}
}
if (goals == null || goals.length() == 0) {
appendContent(BossUi.buildEmptyCard(this, "当前项目还没有目标。点击右上角新增即可。"));
} else {
@@ -187,6 +206,16 @@ public class ProjectGoalsActivity extends BossScreenActivity {
setRefreshing(false);
}
private void appendSummaryLine(StringBuilder builder, String label, String value) {
if (value == null || value.trim().isEmpty()) {
return;
}
if (builder.length() > 0) {
builder.append('\n');
}
builder.append(label).append("").append(value.trim());
}
private LinearLayout buildGoalChecklistCard(JSONObject goal) {
LinearLayout card = BossUi.buildCard(this, "", "", "");
card.removeAllViews();

View File

@@ -13,7 +13,7 @@ import java.util.Map;
public class ProjectVersionsActivity extends BossScreenActivity {
public static final String EXTRA_PROJECT_ID = "project_id";
public static final String EXTRA_PROJECT_NAME = "project_name";
private static final String GOAL_REFRESH_NOTE = "project_goals.updated";
private static final String VERSION_REFRESH_NOTE = "project_versions.updated";
private static final long REALTIME_RELOAD_THROTTLE_MS = 900L;
private String projectId;
@@ -24,7 +24,7 @@ public class ProjectVersionsActivity extends BossScreenActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
configureScreen("版本迭代记录", getIntent().getStringExtra(EXTRA_PROJECT_NAME));
configureScreen("版本记录", getIntent().getStringExtra(EXTRA_PROJECT_NAME));
setHeaderAction("只读", v -> showMessage("版本记录只读"));
realtimeClient = new BossRealtimeClient(apiClient, this::handleRealtimeEvent);
reload();
@@ -111,7 +111,7 @@ public class ProjectVersionsActivity extends BossScreenActivity {
return false;
}
String payloadNote = event.payload.optString("note", "").trim();
return payloadProjectId.equals(projectId) && GOAL_REFRESH_NOTE.equals(payloadNote);
return payloadProjectId.equals(projectId) && VERSION_REFRESH_NOTE.equals(payloadNote);
}
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {

View File

@@ -85,37 +85,48 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ScrollView
android:id="@+id/project_chat_scroll"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:overScrollMode="ifContentScrolls">
android:orientation="vertical">
<LinearLayout
android:id="@+id/project_chat_quick_actions_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/boss_bg_app"
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingTop="10dp"
android:paddingRight="12dp"
android:paddingBottom="20dp">
android:paddingBottom="12dp">
<LinearLayout
android:id="@+id/project_chat_quick_actions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:orientation="horizontal" />
</LinearLayout>
<ScrollView
android:id="@+id/project_chat_scroll"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true"
android:overScrollMode="ifContentScrolls">
<LinearLayout
android:id="@+id/screen_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</ScrollView>
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingTop="0dp"
android:paddingRight="12dp"
android:paddingBottom="20dp" />
</ScrollView>
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<LinearLayout

View File

@@ -37,8 +37,10 @@
<TextView
android:id="@+id/screen_title"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="标题"
android:textColor="@color/boss_text_primary"
android:textSize="22sp"
@@ -46,9 +48,11 @@
<TextView
android:id="@+id/screen_subtitle"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:ellipsize="end"
android:maxLines="1"
android:text="副标题"
android:textColor="@color/boss_text_muted"
android:textSize="12sp" />
@@ -95,6 +99,8 @@
android:layout_height="wrap_content"
android:background="@color/boss_panel"
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="8dp"
android:paddingBottom="24dp" />
</ScrollView>

View File

@@ -9,10 +9,11 @@
<item name="android:windowBackground">@color/boss_bg_app</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.Light.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowBackground">@color/boss_bg_app</item>
<item name="android:forceDarkAllowed">false</item>
</style>