From 57bca4d8df398e1b045ad733f660266ed34b4d86 Mon Sep 17 00:00:00 2001 From: LangLang03 Date: Sat, 5 Sep 2026 22:16:31 +0800 Subject: [PATCH 1/3] refactor: restore all classic settings screens --- .../lineai/ui/component/AboutScreenView.java | 2 +- .../cn/lineai/ui/component/ActionRowView.java | 1 - .../component/AdvancedFeaturesScreenView.java | 58 +++++++- .../AgentExtensionEditScreenView.java | 4 +- .../ui/component/ErrorLogsScreenView.java | 1 + .../ui/component/ExtensionsScreenView.java | 58 +++++++- .../ui/component/FormTextFieldView.java | 10 +- .../ui/component/MCPSettingsScreenView.java | 3 +- .../component/McpExtensionEditScreenView.java | 4 +- .../component/MemorySettingsScreenView.java | 16 ++- .../ui/component/ModelAddScreenView.java | 124 ++++++++---------- .../lineai/ui/component/ModelFormHelper.java | 14 +- .../ui/component/ModelListScreenView.java | 33 +++-- .../ui/component/ModelPickerDialog.java | 2 +- .../component/PromptTemplatesScreenView.java | 4 +- .../lineai/ui/component/ScreenHeaderView.java | 86 ++++++------ .../ui/component/ScreenScaffoldView.java | 12 +- .../ui/component/SectionHeaderView.java | 4 +- .../ui/component/SettingsSectionView.java | 14 +- .../component/SkillHubCenterScreenView.java | 2 +- .../ui/component/SkillHubLoginScreenView.java | 2 +- .../component/SkillHubPublishScreenView.java | 2 +- .../ui/component/SkillHubWebScreenView.java | 2 +- .../component/SkillStoreDetailScreenView.java | 17 +-- .../ui/component/SkillStoreScreenView.java | 21 +-- .../ui/component/SshSettingsScreenView.java | 8 +- .../StorageManagementScreenView.java | 16 +-- .../TermuxIntegrationScreenView.java | 8 +- .../component/SecondaryScreenLayoutTest.java | 34 +++-- .../ui/component/UiCorrectionsTest.java | 4 +- update.md | 2 +- 31 files changed, 334 insertions(+), 234 deletions(-) diff --git a/app/src/main/java/cn/lineai/ui/component/AboutScreenView.java b/app/src/main/java/cn/lineai/ui/component/AboutScreenView.java index d371560e..61688a8a 100644 --- a/app/src/main/java/cn/lineai/ui/component/AboutScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/AboutScreenView.java @@ -26,7 +26,7 @@ public AboutScreenView(Context context, Listener listener) { super(context, context.getString(R.string.screen_about_title), listener::onBack, null); VersionInfo versionInfo = readVersionInfo(context); LinearLayout content = getContent(); - LineTheme.padding(content, 28, 8, 28, 48); + LineTheme.padding(content, LineTheme.LG, LineTheme.LG, LineTheme.LG, 100); LinearLayout header = new LinearLayout(context); header.setOrientation(VERTICAL); diff --git a/app/src/main/java/cn/lineai/ui/component/ActionRowView.java b/app/src/main/java/cn/lineai/ui/component/ActionRowView.java index 1abdcdbe..30d00b14 100644 --- a/app/src/main/java/cn/lineai/ui/component/ActionRowView.java +++ b/app/src/main/java/cn/lineai/ui/component/ActionRowView.java @@ -19,7 +19,6 @@ public ActionRowView(Context context, int iconType, String label, String desc, b FrameLayout iconWrap = new FrameLayout(context); iconWrap.setBackground(LineTheme.rounded(context, destructive ? LineTheme.DANGER_MUTED : LineTheme.ACCENT_MUTED, 8)); - IconButtonView icon = new IconButtonView(context, iconType); icon.setIconColor(destructive ? LineTheme.DANGER : LineTheme.ACCENT); icon.setIconSizeDp(36, 20); diff --git a/app/src/main/java/cn/lineai/ui/component/AdvancedFeaturesScreenView.java b/app/src/main/java/cn/lineai/ui/component/AdvancedFeaturesScreenView.java index 08e3c64e..d1bbfbd5 100644 --- a/app/src/main/java/cn/lineai/ui/component/AdvancedFeaturesScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/AdvancedFeaturesScreenView.java @@ -11,7 +11,7 @@ import android.widget.TextView; import cn.lineai.R; -public final class AdvancedFeaturesScreenView extends ScreenSurfaceView { +public final class AdvancedFeaturesScreenView extends LinearLayout { public interface Listener { void onBack(); @@ -31,7 +31,7 @@ public AdvancedFeaturesScreenView(Context context, Listener listener) { ScrollView scrollView = new ScrollView(context); LinearLayout content = new LinearLayout(context); content.setOrientation(VERTICAL); - LineTheme.padding(content, 12, 8, 12, 48); + LineTheme.padding(content, LineTheme.LG, LineTheme.LG, LineTheme.LG, 100); scrollView.addView(content, new ScrollView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); addView(scrollView, new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)); @@ -39,6 +39,58 @@ public AdvancedFeaturesScreenView(Context context, Listener listener) { } private void addCard(LinearLayout content, String id, String title, String desc, String badge, int iconType) { - CardViewHelper.addCard(content, id, title, desc, badge, iconType, listener::onOpen); + Context context = content.getContext(); + LinearLayout card = new LinearLayout(context); + card.setOrientation(HORIZONTAL); + card.setGravity(Gravity.CENTER_VERTICAL); + card.setClickable(true); + card.setOnClickListener(v -> listener.onOpen(id)); + card.setBackground(LineTheme.roundedStroke(context, LineTheme.SURFACE_ELEVATED, 12, LineTheme.BORDER)); + LineTheme.padding(card, LineTheme.LG, LineTheme.MD, LineTheme.MD, LineTheme.MD); + LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + cardParams.bottomMargin = LineTheme.dp(context, LineTheme.SM); + content.addView(card, cardParams); + + FrameLayout iconWrap = new FrameLayout(context); + iconWrap.setBackground(LineTheme.rounded(context, LineTheme.ACCENT_MUTED, 12)); + IconButtonView icon = new IconButtonView(context, iconType); + icon.setIconColor(LineTheme.ACCENT); + icon.setIconSizeDp(44, 22); + icon.setClickable(false); + iconWrap.addView(icon, new FrameLayout.LayoutParams(LineTheme.dp(context, 44), LineTheme.dp(context, 44), Gravity.CENTER)); + card.addView(iconWrap, new LayoutParams(LineTheme.dp(context, 44), LineTheme.dp(context, 44))); + + LinearLayout text = new LinearLayout(context); + text.setOrientation(VERTICAL); + LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f); + textParams.leftMargin = LineTheme.dp(context, LineTheme.MD); + textParams.rightMargin = LineTheme.dp(context, LineTheme.MD); + card.addView(text, textParams); + + LinearLayout titleRow = new LinearLayout(context); + titleRow.setOrientation(HORIZONTAL); + titleRow.setGravity(Gravity.CENTER_VERTICAL); + text.addView(titleRow, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + + TextView titleView = LineTheme.text(context, title, LineTheme.FONT_LG, LineTheme.TEXT, Typeface.BOLD); + titleRow.addView(titleView, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + TextView badgeView = LineTheme.text(context, badge, LineTheme.FONT_XS, LineTheme.ACCENT, Typeface.BOLD); + badgeView.setBackground(LineTheme.rounded(context, LineTheme.ACCENT_MUTED, 999)); + LineTheme.padding(badgeView, LineTheme.SM, 3, LineTheme.SM, 3); + LinearLayout.LayoutParams badgeParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + badgeParams.leftMargin = LineTheme.dp(context, LineTheme.SM); + titleRow.addView(badgeView, badgeParams); + + TextView descView = LineTheme.text(context, desc, LineTheme.FONT_SM, LineTheme.TEXT_TERTIARY, Typeface.NORMAL); + descView.setLineSpacing(LineTheme.dp(context, 3), 1f); + LinearLayout.LayoutParams descParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + descParams.topMargin = LineTheme.dp(context, LineTheme.XS); + text.addView(descView, descParams); + + IconButtonView chevron = new IconButtonView(context, IconButtonView.CHEVRON_RIGHT); + chevron.setIconColor(LineTheme.TEXT_TERTIARY); + chevron.setIconSizeDp(20, 17); + chevron.setClickable(false); + card.addView(chevron, new LayoutParams(LineTheme.dp(context, 20), LineTheme.dp(context, 20))); } } diff --git a/app/src/main/java/cn/lineai/ui/component/AgentExtensionEditScreenView.java b/app/src/main/java/cn/lineai/ui/component/AgentExtensionEditScreenView.java index 0aa410eb..945d5d94 100644 --- a/app/src/main/java/cn/lineai/ui/component/AgentExtensionEditScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/AgentExtensionEditScreenView.java @@ -350,7 +350,7 @@ private void addForm(LinearLayout content, String title, android.view.View first Context context = content.getContext(); LinearLayout group = new LinearLayout(context); group.setOrientation(LinearLayout.VERTICAL); - group.setBackground(null); + group.setBackground(LineTheme.rounded(context, LineTheme.SURFACE_ELEVATED, 12)); LineTheme.padding(group, LineTheme.LG, LineTheme.LG, LineTheme.LG, LineTheme.LG); group.addView(first, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); LinearLayout.LayoutParams secondParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); @@ -424,7 +424,7 @@ private static final class McpOption { } } - private static final class GenerateButtonView extends ScreenSurfaceView { + private static final class GenerateButtonView extends LinearLayout { private final ProgressBar progressBar; private final IconButtonView icon; private final TextView label; diff --git a/app/src/main/java/cn/lineai/ui/component/ErrorLogsScreenView.java b/app/src/main/java/cn/lineai/ui/component/ErrorLogsScreenView.java index 38ceaa70..73a2a767 100644 --- a/app/src/main/java/cn/lineai/ui/component/ErrorLogsScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/ErrorLogsScreenView.java @@ -68,6 +68,7 @@ private void openLog(ErrorLogEntry entry) { private static IconButtonView clearButton(Context context) { IconButtonView button = new IconButtonView(context, IconButtonView.TRASH_2); button.setIconColor(LineTheme.DANGER); + button.setIconSizeDp(36, 20); return button; } } diff --git a/app/src/main/java/cn/lineai/ui/component/ExtensionsScreenView.java b/app/src/main/java/cn/lineai/ui/component/ExtensionsScreenView.java index 3e2da4c0..aedb8f61 100644 --- a/app/src/main/java/cn/lineai/ui/component/ExtensionsScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/ExtensionsScreenView.java @@ -12,7 +12,7 @@ import android.widget.TextView; import cn.lineai.R; -public final class ExtensionsScreenView extends ScreenSurfaceView { +public final class ExtensionsScreenView extends LinearLayout { public interface Listener { void onBack(); @@ -32,7 +32,7 @@ public ExtensionsScreenView(Context context, Listener listener) { ScrollView scrollView = new ScrollView(context); LinearLayout content = new LinearLayout(context); content.setOrientation(VERTICAL); - LineTheme.padding(content, 12, 8, 12, 48); + LineTheme.padding(content, LineTheme.LG, LineTheme.LG, LineTheme.LG, 100); scrollView.addView(content, new ScrollView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); addView(scrollView, new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)); @@ -44,6 +44,58 @@ public ExtensionsScreenView(Context context, Listener listener) { } private void addCard(LinearLayout content, String id, String title, String desc, String badge, int iconType) { - CardViewHelper.addCard(content, id, title, desc, badge, iconType, listener::onOpen); + Context context = content.getContext(); + LinearLayout card = new LinearLayout(context); + card.setOrientation(HORIZONTAL); + card.setGravity(Gravity.CENTER_VERTICAL); + card.setClickable(true); + card.setOnClickListener(v -> listener.onOpen(id)); + card.setBackground(LineTheme.roundedStroke(context, LineTheme.SURFACE_ELEVATED, 12, LineTheme.BORDER)); + LineTheme.padding(card, LineTheme.LG, LineTheme.MD, LineTheme.MD, LineTheme.MD); + LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + cardParams.bottomMargin = LineTheme.dp(context, LineTheme.SM); + content.addView(card, cardParams); + + FrameLayout iconWrap = new FrameLayout(context); + iconWrap.setBackground(LineTheme.rounded(context, LineTheme.ACCENT_MUTED, 12)); + IconButtonView icon = new IconButtonView(context, iconType); + icon.setIconColor(LineTheme.ACCENT); + icon.setIconSizeDp(44, 22); + icon.setClickable(false); + iconWrap.addView(icon, new FrameLayout.LayoutParams(LineTheme.dp(context, 44), LineTheme.dp(context, 44), Gravity.CENTER)); + card.addView(iconWrap, new LayoutParams(LineTheme.dp(context, 44), LineTheme.dp(context, 44))); + + LinearLayout text = new LinearLayout(context); + text.setOrientation(VERTICAL); + LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f); + textParams.leftMargin = LineTheme.dp(context, LineTheme.MD); + textParams.rightMargin = LineTheme.dp(context, LineTheme.MD); + card.addView(text, textParams); + + LinearLayout titleRow = new LinearLayout(context); + titleRow.setOrientation(HORIZONTAL); + titleRow.setGravity(Gravity.CENTER_VERTICAL); + text.addView(titleRow, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + + TextView titleView = LineTheme.text(context, title, LineTheme.FONT_LG, LineTheme.TEXT, Typeface.BOLD); + titleRow.addView(titleView, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + TextView badgeView = LineTheme.text(context, badge, LineTheme.FONT_XS, LineTheme.ACCENT, Typeface.BOLD); + badgeView.setBackground(LineTheme.rounded(context, LineTheme.ACCENT_MUTED, 999)); + LineTheme.padding(badgeView, LineTheme.SM, 3, LineTheme.SM, 3); + LinearLayout.LayoutParams badgeParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + badgeParams.leftMargin = LineTheme.dp(context, LineTheme.SM); + titleRow.addView(badgeView, badgeParams); + + TextView descView = LineTheme.text(context, desc, LineTheme.FONT_SM, LineTheme.TEXT_TERTIARY, Typeface.NORMAL); + descView.setLineSpacing(LineTheme.dp(context, 3), 1f); + LinearLayout.LayoutParams descParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + descParams.topMargin = LineTheme.dp(context, LineTheme.XS); + text.addView(descView, descParams); + + IconButtonView chevron = new IconButtonView(context, IconButtonView.CHEVRON_RIGHT); + chevron.setIconColor(LineTheme.TEXT_TERTIARY); + chevron.setIconSizeDp(20, 17); + chevron.setClickable(false); + card.addView(chevron, new LayoutParams(LineTheme.dp(context, 20), LineTheme.dp(context, 20))); } } diff --git a/app/src/main/java/cn/lineai/ui/component/FormTextFieldView.java b/app/src/main/java/cn/lineai/ui/component/FormTextFieldView.java index cf22d072..1d4123f9 100644 --- a/app/src/main/java/cn/lineai/ui/component/FormTextFieldView.java +++ b/app/src/main/java/cn/lineai/ui/component/FormTextFieldView.java @@ -28,10 +28,10 @@ public FormTextFieldView(Context context, String label, String value, String pla input.setTextColor(LineTheme.TEXT); input.setTextSize(LineTheme.FONT_MD); input.setSingleLine(!multiline); - input.setMinHeight(LineTheme.dp(context, multiline ? 112 : 52)); + input.setMinHeight(LineTheme.dp(context, multiline ? 120 : 44)); input.setGravity((multiline ? Gravity.TOP : Gravity.CENTER_VERTICAL) | Gravity.START); input.setIncludeFontPadding(false); - input.setBackground(LineTheme.fieldBackground(context)); + input.setBackground(LineTheme.roundedStroke(context, LineTheme.SURFACE_LIGHT, 8, LineTheme.BORDER_LIGHT)); input.setPadding(LineTheme.dp(context, LineTheme.MD), LineTheme.dp(context, LineTheme.SM), LineTheme.dp(context, LineTheme.MD), LineTheme.dp(context, LineTheme.SM)); if (secure) { input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); @@ -42,14 +42,14 @@ public FormTextFieldView(Context context, String label, String value, String pla input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); } LinearLayout.LayoutParams inputParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); - inputParams.topMargin = LineTheme.dp(context, 10); + inputParams.topMargin = LineTheme.dp(context, LineTheme.XS); addView(input, inputParams); if (hint != null && hint.length() > 0) { - TextView hintView = LineTheme.text(context, hint, 13, LineTheme.TEXT_TERTIARY, Typeface.NORMAL); + TextView hintView = LineTheme.text(context, hint, LineTheme.FONT_XS, LineTheme.TEXT_TERTIARY, Typeface.NORMAL); hintView.setLineSpacing(LineTheme.dp(context, 3), 1f); LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); - hintParams.topMargin = LineTheme.dp(context, 10); + hintParams.topMargin = LineTheme.dp(context, LineTheme.XS); addView(hintView, hintParams); } } diff --git a/app/src/main/java/cn/lineai/ui/component/MCPSettingsScreenView.java b/app/src/main/java/cn/lineai/ui/component/MCPSettingsScreenView.java index 938be0fc..c076892f 100644 --- a/app/src/main/java/cn/lineai/ui/component/MCPSettingsScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/MCPSettingsScreenView.java @@ -88,7 +88,7 @@ private void addSshConnection(LinearLayout content) { descParams.topMargin = LineTheme.dp(context, 2); card.addView(desc, descParams); - LinearLayout actions = new AdaptiveActionsView(context); + LinearLayout actions = new LinearLayout(context); actions.setOrientation(HORIZONTAL); LinearLayout ssh = actionButton(context, context.getString(R.string.screen_mcp_ssh_settings), IconButtonView.SERVER, true, v -> listener.onOpenSshSettings()); LinearLayout termux = actionButton(context, context.getString(R.string.screen_mcp_termux_integration), IconButtonView.SMARTPHONE, false, v -> listener.onOpenTermuxIntegration()); @@ -133,7 +133,6 @@ private void addToolCard(LinearLayout content, int iconType, McpToolConfig confi icon.setIconSizeDp(36, 18); icon.setClickable(false); icon.setBackground(LineTheme.rounded(context, LineTheme.ACCENT_MUTED, 18)); - header.addView(icon, new LinearLayout.LayoutParams(LineTheme.dp(context, 36), LineTheme.dp(context, 36))); LinearLayout labels = new LinearLayout(context); diff --git a/app/src/main/java/cn/lineai/ui/component/McpExtensionEditScreenView.java b/app/src/main/java/cn/lineai/ui/component/McpExtensionEditScreenView.java index 10043c03..c0f48ca3 100644 --- a/app/src/main/java/cn/lineai/ui/component/McpExtensionEditScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/McpExtensionEditScreenView.java @@ -271,7 +271,7 @@ private void addForm(LinearLayout content, String title, android.view.View first Context context = content.getContext(); LinearLayout group = new LinearLayout(context); group.setOrientation(LinearLayout.VERTICAL); - group.setBackground(null); + group.setBackground(LineTheme.rounded(context, LineTheme.SURFACE_ELEVATED, 12)); LineTheme.padding(group, LineTheme.LG, LineTheme.LG, LineTheme.LG, LineTheme.LG); group.addView(first, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); LinearLayout.LayoutParams secondParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); @@ -310,7 +310,7 @@ private String trimTrailingSlash(String url) { return value; } - private static final class HeaderRow extends ScreenSurfaceView { + private static final class HeaderRow extends LinearLayout { private final EditText nameInput; private final EditText valueInput; diff --git a/app/src/main/java/cn/lineai/ui/component/MemorySettingsScreenView.java b/app/src/main/java/cn/lineai/ui/component/MemorySettingsScreenView.java index 87b2e78d..79556615 100644 --- a/app/src/main/java/cn/lineai/ui/component/MemorySettingsScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/MemorySettingsScreenView.java @@ -11,6 +11,7 @@ import android.view.Gravity; import android.view.View; import android.view.Window; +import android.view.WindowManager; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.LinearLayout; @@ -390,7 +391,20 @@ private LinearLayout dialogPanel(Context context) { } private void showPanel(Dialog dialog, LinearLayout panel) { - DialogBuilder.showInset(dialog, panel); + ScrollView scrollView = new ScrollView(getContext()); + scrollView.setFillViewport(false); + scrollView.addView(panel, new ScrollView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + dialog.setContentView(scrollView); + dialog.show(); + Window window = dialog.getWindow(); + if (window != null) { + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + WindowManager.LayoutParams params = new WindowManager.LayoutParams(); + params.copyFrom(window.getAttributes()); + params.width = Math.min(getResources().getDisplayMetrics().widthPixels - LineTheme.dp(getContext(), 32), LineTheme.dp(getContext(), 560)); + params.height = WindowManager.LayoutParams.WRAP_CONTENT; + window.setAttributes(params); + } } private TextView titleView(String text) { diff --git a/app/src/main/java/cn/lineai/ui/component/ModelAddScreenView.java b/app/src/main/java/cn/lineai/ui/component/ModelAddScreenView.java index ffa32986..b65df485 100644 --- a/app/src/main/java/cn/lineai/ui/component/ModelAddScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/ModelAddScreenView.java @@ -32,7 +32,7 @@ import java.util.ArrayList; import java.util.List; -public final class ModelAddScreenView extends ScreenSurfaceView { +public final class ModelAddScreenView extends LinearLayout { public interface Listener { void onBack(); void onSave(ModelConfig model); @@ -93,16 +93,14 @@ public ModelAddScreenView(Context context, ModelProviderPreset preset, boolean l setOrientation(VERTICAL); setBackgroundColor(LineTheme.BG); - saveAction = LineTheme.textMedium(context, context.getString(R.string.common_save), 13, LineTheme.TEXT_TERTIARY); + saveAction = LineTheme.textMedium(context, context.getString(R.string.common_save), LineTheme.FONT_MD, LineTheme.TEXT_TERTIARY); saveAction.setGravity(Gravity.CENTER); - LineTheme.padding(saveAction, 10, 8, 10, 8); - saveAction.setMinHeight(LineTheme.dp(context, 48)); + LineTheme.padding(saveAction, LineTheme.MD, LineTheme.SM, LineTheme.MD, LineTheme.SM); - testAction = LineTheme.textMedium(context, context.getString(R.string.screen_model_add_test_button), 13, LineTheme.ACCENT); + testAction = LineTheme.textMedium(context, context.getString(R.string.screen_model_add_test_button), LineTheme.FONT_MD, LineTheme.ACCENT); testAction.setGravity(Gravity.CENTER); testAction.setVisibility(local ? GONE : VISIBLE); - LineTheme.padding(testAction, 10, 8, 10, 8); - testAction.setMinHeight(LineTheme.dp(context, 48)); + LineTheme.padding(testAction, LineTheme.MD, LineTheme.SM, LineTheme.MD, LineTheme.SM); LinearLayout headerActions = new LinearLayout(context); headerActions.setOrientation(HORIZONTAL); @@ -117,15 +115,47 @@ public ModelAddScreenView(Context context, ModelProviderPreset preset, boolean l ScrollView scrollView = new ScrollView(context); LinearLayout content = new LinearLayout(context); content.setOrientation(VERTICAL); - LineTheme.padding(content, 28, 8, 28, 48); + LineTheme.padding(content, LineTheme.LG, LineTheme.LG, LineTheme.LG, LineTheme.LG); scrollView.addView(content, new ScrollView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); addView(scrollView, new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)); - providerLabelView = ModelFormHelper.label(context, context.getString(R.string.screen_model_add_provider_title)); + providerLabelView = ModelFormHelper.label(context, providerTitle()); content.addView(providerLabelView, ModelFormHelper.labelParams(context, LineTheme.LG, LineTheme.SM)); LinearLayout providerRow = new LinearLayout(context); - providerRow.setOrientation(VERTICAL); - updateProviderToggles(providerRow); + providerRow.setOrientation(HORIZONTAL); + for (int i = 0; i < providerLabels.length; i++) { + final int index = i; + boolean enabled = !lockedPreset || isActiveProviderIndex(index); + ModelFormHelper.addToggle(providerRow, providerLabels[i], isActiveProviderIndex(index), enabled, () -> { + if (index == 3) { + Toast.makeText(context, R.string.screen_model_add_open_local_form, Toast.LENGTH_SHORT).show(); + return; + } + if (this.local) { + Toast.makeText(context, R.string.screen_model_add_open_custom_form, Toast.LENGTH_SHORT).show(); + return; + } + if (!lockedPreset) { + protocolType[0] = protocolForIndex(index); + fetchedModelIds.clear(); + selectedModelId[0] = ""; + if (modelIdInput != null) { + modelIdInput.setText(""); + } + if (compressionSection != null) { + compressionSection.clearFetched(); + } + updateProviderToggles(providerRow); + updateBaseUrlHint(); + renderModelIdInput(customIdSwitch != null && customIdSwitch.isChecked()); + if (compressionSection != null) { + compressionSection.updateForProtocolChange(); + } + updateQueryState(); + updateSaveState(); + } + }); + } content.addView(providerRow, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); content.addView(ModelFormHelper.label(context, context.getString(R.string.screen_model_add_field_name)), ModelFormHelper.labelParams(context, LineTheme.LG, LineTheme.SM)); @@ -202,7 +232,6 @@ public ModelAddScreenView(Context context, ModelProviderPreset preset, boolean l fetchModelCatalog(); }); - int advancedStart = content.getChildCount(); content.addView(ModelFormHelper.label(context, context.getString(R.string.screen_model_add_field_tool_call_limit)), ModelFormHelper.labelParams(context, LineTheme.LG, LineTheme.SM)); toolCallLimitInput = ModelFormHelper.input(context, String.valueOf(editing ? editingModel.getToolCallLimit() : ModelConfig.DEFAULT_TOOL_CALL_LIMIT), context.getString(R.string.screen_model_add_hint_tool_call_limit), false, false); toolCallLimitInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); @@ -241,7 +270,6 @@ public void onStateChanged() { () -> ModelFormHelper.value(apiKeyInput) ); content.addView(compressionSection, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); - DisclosureSectionView.foldTail(content, advancedStart, context.getString(R.string.sheet_title_advanced), false); } TextWatcher watcher = new TextWatcher() { @@ -307,13 +335,13 @@ private void addLocalUi(Context context, LinearLayout content) { card.setBackground(LineTheme.roundedStroke(context, LineTheme.SURFACE_LIGHT, 12, LineTheme.BORDER_LIGHT)); LineTheme.padding(card, LineTheme.MD, LineTheme.MD, LineTheme.MD, LineTheme.MD); FrameLayout iconWrap = new FrameLayout(context); - iconWrap.setBackground(null); + iconWrap.setBackground(LineTheme.rounded(context, LineTheme.ACCENT_MUTED, 8)); IconButtonView fileIcon = new IconButtonView(context, IconButtonView.FILE_UP); fileIcon.setIconColor(LineTheme.ACCENT); - fileIcon.setIconSizeDp(28, 16); + fileIcon.setIconSizeDp(38, 20); fileIcon.setClickable(false); - iconWrap.addView(fileIcon, new FrameLayout.LayoutParams(LineTheme.dp(context, 28), LineTheme.dp(context, 28), Gravity.CENTER)); - card.addView(iconWrap, new LinearLayout.LayoutParams(LineTheme.dp(context, 28), LineTheme.dp(context, 28))); + iconWrap.addView(fileIcon, new FrameLayout.LayoutParams(LineTheme.dp(context, 38), LineTheme.dp(context, 38), Gravity.CENTER)); + card.addView(iconWrap, new LinearLayout.LayoutParams(LineTheme.dp(context, 38), LineTheme.dp(context, 38))); LinearLayout fileText = new LinearLayout(context); fileText.setOrientation(VERTICAL); @@ -562,59 +590,14 @@ private int parseContextSize() { return ContextSizeParser.parse(ModelFormHelper.value(contextSizeInput)); } - private String activeProviderLabel() { - for (int i = 0; i < providerLabels.length; i++) if (isActiveProviderIndex(i)) return providerLabels[i]; - return providerTitle(); - } private void updateProviderToggles(LinearLayout providerRow) { - providerRow.removeAllViews(); - TextView selected = LineTheme.text(getContext(), activeProviderLabel() + (lockedPreset ? "" : " ›"), 16, LineTheme.TEXT, Typeface.NORMAL); - selected.setMinimumHeight(LineTheme.dp(getContext(), 52)); - selected.setGravity(Gravity.CENTER_VERTICAL); LineTheme.padding(selected, 16, 12, 16, 12); - selected.setBackground(LineTheme.fieldBackground(getContext())); - providerRow.addView(selected, new LayoutParams(-1, -2)); - if (lockedPreset) return; - selected.setOnClickListener(v -> { - android.app.Dialog dialog = DialogBuilder.create(getContext()); - LinearLayout choices = new LinearLayout(getContext()); choices.setOrientation(VERTICAL); - LineTheme.padding(choices, 12, 16, 12, 16); - for (int i = 0; i < providerLabels.length; i++) { - final int index = i; - choices.addView(new OptionRowView(getContext(), IconButtonView.BOX, providerLabels[i], null, - isActiveProviderIndex(i), () -> { dialog.dismiss(); selectProvider(index, providerRow); })); - } - DialogBuilder.showBottomSheet(dialog, choices); - }); - } - private void selectProvider(int index, LinearLayout providerRow) { - Context context = getContext(); - if (index == 3) { - Toast.makeText(context, R.string.screen_model_add_open_local_form, Toast.LENGTH_SHORT).show(); - return; - } - if (this.local) { - Toast.makeText(context, R.string.screen_model_add_open_custom_form, Toast.LENGTH_SHORT).show(); - return; - } - if (!lockedPreset) { - protocolType[0] = protocolForIndex(index); - fetchedModelIds.clear(); - selectedModelId[0] = ""; - if (modelIdInput != null) { - modelIdInput.setText(""); - } - if (compressionSection != null) { - compressionSection.clearFetched(); - } - updateProviderToggles(providerRow); - updateBaseUrlHint(); - renderModelIdInput(customIdSwitch != null && customIdSwitch.isChecked()); - if (compressionSection != null) { - compressionSection.updateForProtocolChange(); - } - updateQueryState(); - updateSaveState(); - } + for (int i = 0; i < providerRow.getChildCount(); i++) { + TextView button = (TextView) providerRow.getChildAt(i); + boolean active = isActiveProviderIndex(i); + button.setTextColor(active ? LineTheme.TEXT_ON_COLOR : LineTheme.TEXT_SECONDARY); + button.setBackground(LineTheme.rounded(getContext(), active ? LineTheme.ACCENT : LineTheme.SURFACE_LIGHT, 12)); + button.setAlpha(lockedPreset && !active ? 0.45f : 1f); + } } private void updateBaseUrlHint() { @@ -622,12 +605,11 @@ private void updateBaseUrlHint() { return; } baseUrlHintView.setText(hintFor(lockedPreset ? preset : null)); - baseUrlHintView.setVisibility(baseUrlHintView.getText().length() == 0 ? GONE : VISIBLE); if (!lockedPreset && baseUrlInput != null) { baseUrlInput.setHint(placeholderFor(protocolType[0])); } if (providerLabelView != null) { - providerLabelView.setText(getContext().getString(R.string.screen_model_add_provider_title)); + providerLabelView.setText(providerTitle()); } if (compressionSection != null) { compressionSection.updateForProtocolChange(); diff --git a/app/src/main/java/cn/lineai/ui/component/ModelFormHelper.java b/app/src/main/java/cn/lineai/ui/component/ModelFormHelper.java index d4d5bae9..ad5726da 100644 --- a/app/src/main/java/cn/lineai/ui/component/ModelFormHelper.java +++ b/app/src/main/java/cn/lineai/ui/component/ModelFormHelper.java @@ -24,7 +24,7 @@ public static TextView label(Context context, String text) { public static LinearLayout.LayoutParams labelParams(Context context, int top, int bottom) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - params.topMargin = LineTheme.dp(context, top == 0 ? 0 : Math.max(24, top)); + params.topMargin = LineTheme.dp(context, top); params.bottomMargin = LineTheme.dp(context, bottom); return params; } @@ -37,9 +37,9 @@ public static EditText input(Context context, String value, String placeholder, input.setTextColor(LineTheme.TEXT); input.setTextSize(LineTheme.FONT_MD); input.setSingleLine(!multiline); - input.setMinHeight(LineTheme.dp(context, multiline ? 112 : 52)); + input.setMinHeight(LineTheme.dp(context, multiline ? 120 : 48)); input.setIncludeFontPadding(false); - input.setBackground(LineTheme.fieldBackground(context)); + input.setBackground(LineTheme.roundedStroke(context, LineTheme.SURFACE_LIGHT, 12, LineTheme.BORDER_LIGHT)); input.setPadding(LineTheme.dp(context, LineTheme.LG), LineTheme.dp(context, LineTheme.MD), LineTheme.dp(context, LineTheme.LG), LineTheme.dp(context, LineTheme.MD)); input.setInputType(secure ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD @@ -51,17 +51,15 @@ public static EditText input(Context context, String value, String placeholder, public static void addToggle(LinearLayout row, String label, boolean active, boolean enabled, Runnable onClick) { Context context = row.getContext(); TextView button = LineTheme.text(context, label, LineTheme.FONT_MD, active ? LineTheme.TEXT_ON_COLOR : LineTheme.TEXT_SECONDARY, Typeface.BOLD); - button.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); - button.setMinimumHeight(LineTheme.dp(context, 48)); - LineTheme.padding(button, 16, 12, 16, 12); + button.setGravity(Gravity.CENTER); button.setBackground(LineTheme.rounded(context, active ? LineTheme.ACCENT : LineTheme.SURFACE_LIGHT, 12)); button.setAlpha(enabled || active ? 1f : 0.45f); if (enabled && onClick != null) { button.setOnClickListener(v -> onClick.run()); } - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, LineTheme.dp(context, 46), 1f); if (row.getChildCount() > 0) { - params.topMargin = LineTheme.dp(context, 6); + params.leftMargin = LineTheme.dp(context, LineTheme.SM); } row.addView(button, params); } diff --git a/app/src/main/java/cn/lineai/ui/component/ModelListScreenView.java b/app/src/main/java/cn/lineai/ui/component/ModelListScreenView.java index 7c9875b5..5cbb872d 100644 --- a/app/src/main/java/cn/lineai/ui/component/ModelListScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/ModelListScreenView.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.Set; -public final class ModelListScreenView extends ScreenSurfaceView { +public final class ModelListScreenView extends LinearLayout { public interface Listener { void onBack(); @@ -70,7 +70,7 @@ public ModelListScreenView( ScrollView scrollView = new ScrollView(context); list = new LinearLayout(context); list.setOrientation(VERTICAL); - LineTheme.padding(list, 16, 8, 16, 48); + LineTheme.padding(list, LineTheme.LG, LineTheme.LG, LineTheme.LG, 100); scrollView.addView(list, new ScrollView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); addView(scrollView, new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)); @@ -106,6 +106,7 @@ private void renderHeader() { if (allowManagement) { add = new IconButtonView(context, IconButtonView.PLUS); add.setIconColor(LineTheme.TEXT); + add.setIconSizeDp(36, 20); add.setContentDescription(context.getString(R.string.screen_model_add_options_title)); add.setOnClickListener(v -> listener.onAddModel()); } @@ -158,28 +159,34 @@ private void addModel(LinearLayout list, ModelConfig model, boolean selected, bo } return true; }); - int background = selected || checked ? LineTheme.INPUT_BG : LineTheme.SURFACE_ELEVATED; - int border = LineTheme.BORDER_LIGHT; + int background = checked ? LineTheme.ACCENT_MUTED : LineTheme.BG; + int border = selected || checked ? LineTheme.ACCENT : Color.TRANSPARENT; card.setBackground(LineTheme.roundedStroke(context, background, 12, border)); - LineTheme.padding(card, 12, 20, 12, 20); + LineTheme.padding(card, LineTheme.MD, LineTheme.MD, LineTheme.MD, LineTheme.MD); LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); cardParams.bottomMargin = LineTheme.dp(context, LineTheme.SM); list.addView(card, cardParams); String provider = displayProvider(model); + TextView badge = LineTheme.text(context, provider, LineTheme.FONT_XS, LineTheme.TEXT_ON_COLOR, Typeface.BOLD); + badge.setGravity(Gravity.CENTER); + badge.setBackground(LineTheme.rounded(context, badgeColor(model), 8)); + LinearLayout.LayoutParams badgeParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + badgeParams.rightMargin = LineTheme.dp(context, LineTheme.MD); + LineTheme.padding(badge, LineTheme.SM, 4, LineTheme.SM, 4); + card.addView(badge, badgeParams); + LinearLayout info = new LinearLayout(context); info.setOrientation(VERTICAL); card.addView(info, new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)); TextView title = LineTheme.textMedium(context, model.getName(), LineTheme.FONT_MD, LineTheme.TEXT); - title.setMaxLines(2); - title.setEllipsize(android.text.TextUtils.TruncateAt.END); + title.setSingleLine(true); info.addView(title, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); - TextView sub = LineTheme.text(context, provider + " / " + model.getModelId(), LineTheme.FONT_SM, LineTheme.TEXT_TERTIARY, Typeface.NORMAL); - sub.setMaxLines(2); - sub.setEllipsize(android.text.TextUtils.TruncateAt.END); + TextView sub = LineTheme.text(context, model.getModelId(), LineTheme.FONT_XS, LineTheme.TEXT_TERTIARY, Typeface.NORMAL); + sub.setSingleLine(true); LinearLayout.LayoutParams subParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); - subParams.topMargin = LineTheme.dp(context, 6); + subParams.topMargin = LineTheme.dp(context, 2); info.addView(sub, subParams); if (!multiSelectedIds.isEmpty()) { @@ -281,7 +288,7 @@ private Dialog createBottomDialog(Context context) { private LinearLayout createBottomPanel(Context context) { LinearLayout panel = new LinearLayout(context); panel.setOrientation(VERTICAL); - panel.setBackground(LineTheme.rounded(context, LineTheme.BG, 24)); + panel.setBackground(LineTheme.roundedTop(context, LineTheme.SURFACE_ELEVATED, 16)); return panel; } @@ -333,7 +340,7 @@ private void addActionRow(LinearLayout panel, String label, String desc, Runnabl if (desc != null && desc.length() > 0) { TextView descView = LineTheme.text(context, desc, LineTheme.FONT_XS, LineTheme.TEXT_TERTIARY, Typeface.NORMAL); LinearLayout.LayoutParams descParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); - descParams.topMargin = LineTheme.dp(context, 6); + descParams.topMargin = LineTheme.dp(context, 2); labels.addView(descView, descParams); } panel.addView(row, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); diff --git a/app/src/main/java/cn/lineai/ui/component/ModelPickerDialog.java b/app/src/main/java/cn/lineai/ui/component/ModelPickerDialog.java index 0329a0f5..aa70225f 100644 --- a/app/src/main/java/cn/lineai/ui/component/ModelPickerDialog.java +++ b/app/src/main/java/cn/lineai/ui/component/ModelPickerDialog.java @@ -35,7 +35,7 @@ public static void show(Context context, List modelIds, String selectedI LinearLayout panel = new LinearLayout(context); panel.setOrientation(LinearLayout.VERTICAL); - panel.setBackground(LineTheme.rounded(context, LineTheme.BG, 24)); + panel.setBackground(LineTheme.roundedTop(context, LineTheme.SURFACE_ELEVATED, 16)); View handle = new View(context); handle.setBackground(LineTheme.rounded(context, LineTheme.TEXT_TERTIARY, 2)); diff --git a/app/src/main/java/cn/lineai/ui/component/PromptTemplatesScreenView.java b/app/src/main/java/cn/lineai/ui/component/PromptTemplatesScreenView.java index 09c832c5..74ff68a3 100644 --- a/app/src/main/java/cn/lineai/ui/component/PromptTemplatesScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/PromptTemplatesScreenView.java @@ -73,7 +73,7 @@ private static String variablesText(PromptTemplateItem item) { return builder.toString(); } - private static final class PromptTemplateEditorView extends ScreenSurfaceView { + private static final class PromptTemplateEditorView extends LinearLayout { private final PromptTemplateItem item; private final TextView statusView; private final EditText input; @@ -119,7 +119,7 @@ private static final class PromptTemplateEditorView extends ScreenSurfaceView { inputParams.topMargin = LineTheme.dp(context, LineTheme.MD); addView(input, inputParams); - LinearLayout actions = new AdaptiveActionsView(context); + LinearLayout actions = new LinearLayout(context); actions.setOrientation(HORIZONTAL); actions.setGravity(Gravity.CENTER_VERTICAL); LinearLayout.LayoutParams actionsParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LineTheme.dp(context, 34)); diff --git a/app/src/main/java/cn/lineai/ui/component/ScreenHeaderView.java b/app/src/main/java/cn/lineai/ui/component/ScreenHeaderView.java index 60dda794..a7f6c659 100644 --- a/app/src/main/java/cn/lineai/ui/component/ScreenHeaderView.java +++ b/app/src/main/java/cn/lineai/ui/component/ScreenHeaderView.java @@ -1,63 +1,67 @@ package cn.lineai.ui.component; +import cn.lineai.ui.theme.IconButtonView; +import cn.lineai.ui.theme.LineTheme; import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Typeface; import android.view.Gravity; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; -import cn.lineai.ui.theme.IconButtonView; -import cn.lineai.ui.theme.LineTheme; -/** Navigation and page title are separate so actions never squeeze the heading. */ public final class ScreenHeaderView extends LinearLayout { - static final int ACTION_SIZE_DP = 48; + static final int ACTION_SIZE_DP = 36; static final int ICON_SIZE_DP = 22; + private final Paint borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + public ScreenHeaderView(Context context, String title, Runnable onBack, View rightAction) { - this(context, title, onBack == null ? null : backButton(context, onBack), rightAction); - } - public ScreenHeaderView(Context context, String title, Runnable onBack, View rightAction, boolean inlineTitle) { - this(context, title, onBack == null ? null : backButton(context, onBack), rightAction, inlineTitle); + this(context, title, onBack == null ? null : backButtonView(context, onBack), rightAction); } + public ScreenHeaderView(Context context, String title, View leftAction, View rightAction) { - this(context, title, leftAction, rightAction, false); - } - private ScreenHeaderView(Context context, String title, View leftAction, View rightAction, boolean inlineTitle) { super(context); - setOrientation(VERTICAL); + setOrientation(HORIZONTAL); + setGravity(Gravity.CENTER_VERTICAL); setBackgroundColor(LineTheme.BG); - LinearLayout nav = new LinearLayout(context); - nav.setGravity(Gravity.CENTER_VERTICAL); - LineTheme.padding(nav, 4, 4, 8, 0); - if (leftAction != null) addAction(nav, leftAction); - TextView heading = LineTheme.textMedium(context, title, inlineTitle ? 18 : 22, LineTheme.TEXT); - if (android.os.Build.VERSION.SDK_INT >= 28) heading.setAccessibilityHeading(true); - if (inlineTitle) { - heading.setSingleLine(true); - heading.setEllipsize(android.text.TextUtils.TruncateAt.END); - nav.addView(heading, new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)); + setWillNotDraw(false); + LineTheme.padding(this, LineTheme.LG, LineTheme.MD, LineTheme.LG, LineTheme.MD); + + View left = leftAction == null ? spacer(context) : leftAction; + addView(left, new LayoutParams(LineTheme.dp(context, ACTION_SIZE_DP), LineTheme.dp(context, ACTION_SIZE_DP))); + + TextView titleView = LineTheme.text(context, title, LineTheme.FONT_LG, LineTheme.TEXT, Typeface.BOLD); + titleView.setGravity(Gravity.CENTER); + if (android.os.Build.VERSION.SDK_INT >= 28) titleView.setAccessibilityHeading(true); + addView(titleView, new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)); + + View right = rightAction == null ? spacer(context) : rightAction; + LayoutParams rightParams; + if (rightAction instanceof TextView) { + right.setMinimumWidth(LineTheme.dp(context, ACTION_SIZE_DP)); + rightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LineTheme.dp(context, ACTION_SIZE_DP)); + } else if (rightAction instanceof LinearLayout) { + rightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } else { - nav.addView(new View(context), new LayoutParams(0, 1, 1f)); - } - if (rightAction != null) { - addAction(nav, rightAction); - } - addView(nav, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); - if (!inlineTitle) { - LineTheme.padding(heading, 16, 8, 16, 14); - addView(heading, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + rightParams = new LayoutParams(LineTheme.dp(context, ACTION_SIZE_DP), LineTheme.dp(context, ACTION_SIZE_DP)); } + addView(right, rightParams); } - private void addAction(LinearLayout nav, View action) { - int size = LineTheme.dp(getContext(), ACTION_SIZE_DP); - action.setMinimumHeight(size); - if (action instanceof IconButtonView) { - ((IconButtonView) action).setIconSizeDp(ACTION_SIZE_DP, ICON_SIZE_DP); - nav.addView(action, new LayoutParams(size, size)); - } else { - nav.addView(action, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); - } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + borderPaint.setColor(LineTheme.BORDER); + borderPaint.setStrokeWidth(1f); + canvas.drawLine(0, getHeight() - 1, getWidth(), getHeight() - 1, borderPaint); + } + + private View spacer(Context context) { + return new View(context); } - private static View backButton(Context context, Runnable onBack) { + + private static View backButtonView(Context context, Runnable onBack) { IconButtonView button = new IconButtonView(context, IconButtonView.CHEVRON_LEFT); button.setIconColor(LineTheme.TEXT); button.setIconSizeDp(ACTION_SIZE_DP, ICON_SIZE_DP); diff --git a/app/src/main/java/cn/lineai/ui/component/ScreenScaffoldView.java b/app/src/main/java/cn/lineai/ui/component/ScreenScaffoldView.java index f8238c78..9dcba409 100644 --- a/app/src/main/java/cn/lineai/ui/component/ScreenScaffoldView.java +++ b/app/src/main/java/cn/lineai/ui/component/ScreenScaffoldView.java @@ -6,29 +6,23 @@ import android.widget.LinearLayout; import android.widget.ScrollView; -public class ScreenScaffoldView extends ScreenSurfaceView { +public class ScreenScaffoldView extends LinearLayout { private final LinearLayout content; private final ScrollView scrollView; private final View rightAction; public ScreenScaffoldView(Context context, String title, Runnable onBack, View rightAction) { - this(context, title, onBack, rightAction, false); - } - - protected ScreenScaffoldView(Context context, String title, Runnable onBack, View rightAction, boolean inlineTitle) { super(context); this.rightAction = rightAction; setOrientation(VERTICAL); setBackgroundColor(LineTheme.BG); - addView(new ScreenHeaderView(context, title, onBack, rightAction, inlineTitle), new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + addView(new ScreenHeaderView(context, title, onBack, rightAction), new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); scrollView = new ScrollView(context); scrollView.setFillViewport(false); - scrollView.setClipToPadding(false); - scrollView.setVerticalScrollBarEnabled(false); content = new LinearLayout(context); content.setOrientation(VERTICAL); - LineTheme.padding(content, 0, 0, 0, 48); + LineTheme.padding(content, 0, 0, 0, 100); scrollView.addView(content, new ScrollView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); addView(scrollView, new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)); } diff --git a/app/src/main/java/cn/lineai/ui/component/SectionHeaderView.java b/app/src/main/java/cn/lineai/ui/component/SectionHeaderView.java index e7b2ff1b..596ac3e0 100644 --- a/app/src/main/java/cn/lineai/ui/component/SectionHeaderView.java +++ b/app/src/main/java/cn/lineai/ui/component/SectionHeaderView.java @@ -1,7 +1,9 @@ package cn.lineai.ui.component; +import cn.lineai.ui.theme.LineTheme; + import android.content.Context; import android.widget.TextView; -import cn.lineai.ui.theme.LineTheme; + public final class SectionHeaderView extends TextView { public SectionHeaderView(Context context, String title) { super(context); diff --git a/app/src/main/java/cn/lineai/ui/component/SettingsSectionView.java b/app/src/main/java/cn/lineai/ui/component/SettingsSectionView.java index bee9098b..730657f7 100644 --- a/app/src/main/java/cn/lineai/ui/component/SettingsSectionView.java +++ b/app/src/main/java/cn/lineai/ui/component/SettingsSectionView.java @@ -1,11 +1,14 @@ package cn.lineai.ui.component; +import cn.lineai.ui.theme.LineTheme; + import android.content.Context; import android.view.View; import android.widget.LinearLayout; -import cn.lineai.ui.theme.LineTheme; + public final class SettingsSectionView extends LinearLayout { private final SectionHeaderView header; private final LinearLayout group; + public SettingsSectionView(Context context, String title) { super(context); setOrientation(VERTICAL); @@ -30,11 +33,10 @@ public void addRow(View row, boolean divider) { } public void addRow(View row, boolean divider, int dividerInsetDp) { - if (row == null) { - return; - } - if (row.getParent() instanceof android.view.ViewGroup) { - ((android.view.ViewGroup) row.getParent()).removeView(row); + if (row != null) { + if (row.getParent() instanceof android.view.ViewGroup) { + ((android.view.ViewGroup) row.getParent()).removeView(row); + } } if (!divider) { group.addView(row, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); diff --git a/app/src/main/java/cn/lineai/ui/component/SkillHubCenterScreenView.java b/app/src/main/java/cn/lineai/ui/component/SkillHubCenterScreenView.java index 1ee29f3b..97763f95 100644 --- a/app/src/main/java/cn/lineai/ui/component/SkillHubCenterScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/SkillHubCenterScreenView.java @@ -17,7 +17,7 @@ public interface Listener { public SkillHubCenterScreenView(Context context, Listener listener) { super(context, context.getString(cn.lineai.R.string.skillhub_center_title), listener::onBack, null); LinearLayout content = getContent(); - LineTheme.padding(content, 28, 8, 28, 48); + LineTheme.padding(content, LineTheme.LG, LineTheme.LG, LineTheme.LG, 100); TextView notice = LineTheme.text(context, context.getString(cn.lineai.R.string.skillhub_center_notice), diff --git a/app/src/main/java/cn/lineai/ui/component/SkillHubLoginScreenView.java b/app/src/main/java/cn/lineai/ui/component/SkillHubLoginScreenView.java index 0cb141cc..4efc8fdb 100644 --- a/app/src/main/java/cn/lineai/ui/component/SkillHubLoginScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/SkillHubLoginScreenView.java @@ -25,7 +25,7 @@ import java.util.HashSet; import java.util.Set; -public final class SkillHubLoginScreenView extends ScreenSurfaceView { +public final class SkillHubLoginScreenView extends LinearLayout { private static final String LOGIN_URL = "https://skillhub.cn/"; private static final Set ALLOWED_HOSTS = new HashSet<>(Arrays.asList( "skillhub.cn", diff --git a/app/src/main/java/cn/lineai/ui/component/SkillHubPublishScreenView.java b/app/src/main/java/cn/lineai/ui/component/SkillHubPublishScreenView.java index 73c6e8e2..7b18008b 100644 --- a/app/src/main/java/cn/lineai/ui/component/SkillHubPublishScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/SkillHubPublishScreenView.java @@ -51,7 +51,7 @@ public SkillHubPublishScreenView( } LinearLayout content = getContent(); - LineTheme.padding(content, 28, 8, 28, 48); + LineTheme.padding(content, LineTheme.LG, LineTheme.LG, LineTheme.LG, 100); addNotice(content); selected = fieldButton(content); slug = field(content, context.getString(cn.lineai.R.string.skillhub_slug_label), diff --git a/app/src/main/java/cn/lineai/ui/component/SkillHubWebScreenView.java b/app/src/main/java/cn/lineai/ui/component/SkillHubWebScreenView.java index 273aa66f..9169d323 100644 --- a/app/src/main/java/cn/lineai/ui/component/SkillHubWebScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/SkillHubWebScreenView.java @@ -23,7 +23,7 @@ import java.util.Map; import java.util.Set; -public final class SkillHubWebScreenView extends ScreenSurfaceView { +public final class SkillHubWebScreenView extends LinearLayout { private static final String SITE_ROOT = "https://skillhub.cn"; private static final Set ALLOWED_HOSTS = new HashSet<>(Arrays.asList( "skillhub.cn", diff --git a/app/src/main/java/cn/lineai/ui/component/SkillStoreDetailScreenView.java b/app/src/main/java/cn/lineai/ui/component/SkillStoreDetailScreenView.java index 9b2d0422..acfea7a2 100644 --- a/app/src/main/java/cn/lineai/ui/component/SkillStoreDetailScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/SkillStoreDetailScreenView.java @@ -72,7 +72,7 @@ public SkillStoreDetailScreenView(Context context, String slug, Listener listene this.sessionClient = new SkillHubSessionClient(new ContextResourceProvider(context)); this.iconLoader = new SkillIconLoader(context); body = getContent(); - LineTheme.padding(body, 28, 8, 28, 48); + LineTheme.padding(body, LineTheme.LG, LineTheme.LG, LineTheme.LG, 100); progress = new ProgressBar(context); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); @@ -107,8 +107,9 @@ private void render(SkillHubModels.Detail value) { private void addHero(SkillHubModels.Detail value) { LinearLayout hero = new LinearLayout(getContext()); hero.setOrientation(VERTICAL); - hero.setBackground(null); - LineTheme.padding(hero, 0, 8, 0, 24); + hero.setBackground(LineTheme.roundedStroke( + getContext(), LineTheme.SURFACE_ELEVATED, 14, LineTheme.BORDER)); + LineTheme.padding(hero, LineTheme.LG, LineTheme.LG, LineTheme.LG, LineTheme.LG); LinearLayout top = new LinearLayout(getContext()); top.setOrientation(HORIZONTAL); @@ -187,7 +188,7 @@ private void addHero(SkillHubModels.Detail value) { } private void addActions(SkillHubModels.Detail value) { - LinearLayout actions = new AdaptiveActionsView(getContext()); + LinearLayout actions = new LinearLayout(getContext()); actions.setOrientation(HORIZONTAL); actions.setGravity(Gravity.CENTER_VERTICAL); actions.addView(actionButton(IconButtonView.COPY, getString(R.string.skillhub_copy_prompt), @@ -619,7 +620,7 @@ private void showCommentDialog( inputParams.topMargin = LineTheme.dp(getContext(), LineTheme.MD); panel.addView(input, inputParams); - LinearLayout actions = new AdaptiveActionsView(getContext()); + LinearLayout actions = new LinearLayout(getContext()); actions.setOrientation(HORIZONTAL); TextView cancel = dialogButton(getString(R.string.skillhub_cancel), false); TextView submit = dialogButton(getString(R.string.skillhub_submit_comment), true); @@ -695,7 +696,7 @@ private void addComment(LinearLayout section, SkillHubModels.Comment comment, in contentParams.topMargin = LineTheme.dp(getContext(), LineTheme.XS); card.addView(content, contentParams); - LinearLayout actions = new AdaptiveActionsView(getContext()); + LinearLayout actions = new LinearLayout(getContext()); actions.setOrientation(HORIZONTAL); TextView like = commentAction( (comment.isLiked() ? getString(R.string.skillhub_unlike) @@ -1110,7 +1111,7 @@ private void showInstallConfirm(SkillHubModels.Detail value, TextView installBut packageIcon.setIconSizeDp(42, 22); packageIcon.setClickable(false); packageIcon.setFocusable(false); - packageIcon.setBackground(null); + packageIcon.setBackground(LineTheme.rounded(getContext(), LineTheme.ACCENT_MUTED, 10)); heading.addView(packageIcon, new LinearLayout.LayoutParams( LineTheme.dp(getContext(), 42), LineTheme.dp(getContext(), 42))); LinearLayout titleCopy = new LinearLayout(getContext()); @@ -1208,7 +1209,7 @@ IconButtonView.FOLDER, getString(R.string.skillhub_location_project_title), panel.addView(warningCard, warningParams); } - LinearLayout actions = new AdaptiveActionsView(getContext()); + LinearLayout actions = new LinearLayout(getContext()); actions.setOrientation(HORIZONTAL); TextView cancel = dialogButton(getString(R.string.skillhub_cancel), false); TextView install = dialogButton(getString(R.string.common_install), true); diff --git a/app/src/main/java/cn/lineai/ui/component/SkillStoreScreenView.java b/app/src/main/java/cn/lineai/ui/component/SkillStoreScreenView.java index 1d0642eb..603aadcd 100644 --- a/app/src/main/java/cn/lineai/ui/component/SkillStoreScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/SkillStoreScreenView.java @@ -55,13 +55,13 @@ public interface Listener { private String sortBy = "downloads"; public SkillStoreScreenView(Context context, Listener listener) { - super(context, context.getString(R.string.skillhub_title_store), listener::onBack, null, true); + super(context, context.getString(R.string.skillhub_title_store), listener::onBack, null); this.listener = listener; this.client = new SkillHubClient(new ContextResourceProvider(context)); this.sessionClient = new SkillHubSessionClient(new ContextResourceProvider(context)); this.iconLoader = new SkillIconLoader(context); LinearLayout content = getContent(); - LineTheme.padding(content, 16, 16, 16, 32); + LineTheme.padding(content, LineTheme.LG, LineTheme.LG, LineTheme.LG, 100); addIntro(content); search = addSearch(content); @@ -122,7 +122,7 @@ private void addIntro(LinearLayout content) { storeIcon.setIconSizeDp(44, 24); storeIcon.setClickable(false); storeIcon.setFocusable(false); - storeIcon.setBackground(null); + storeIcon.setBackground(LineTheme.rounded(getContext(), LineTheme.ACCENT_MUTED, 12)); header.addView(storeIcon, new LinearLayout.LayoutParams( LineTheme.dp(getContext(), 44), LineTheme.dp(getContext(), 44))); content.addView(header); @@ -393,7 +393,7 @@ private void showAccountDialog(SkillHubSessionClient.Account account) { statusParams.topMargin = LineTheme.dp(getContext(), LineTheme.MD); panel.addView(status, statusParams); - LinearLayout actions = new AdaptiveActionsView(getContext()); + LinearLayout actions = new LinearLayout(getContext()); actions.setOrientation(HORIZONTAL); TextView close = accountDialogButton( getContext().getString(R.string.skillhub_continue), false); @@ -535,8 +535,9 @@ private View card(SkillHubModels.Summary skill) { card.setFocusable(true); card.setContentDescription(skill.getName() + getContext().getString(R.string.skillhub_view_details)); card.setOnClickListener(v -> listener.onOpen(skill.getSlug())); - card.setBackground(LineTheme.pressable(getContext())); - LineTheme.padding(card, 0, 20, 0, 20); + card.setBackground(LineTheme.roundedStroke( + getContext(), LineTheme.SURFACE_ELEVATED, 12, LineTheme.BORDER)); + LineTheme.padding(card, LineTheme.MD, LineTheme.MD, LineTheme.SM, LineTheme.MD); LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); cardParams.bottomMargin = LineTheme.dp(getContext(), LineTheme.SM); @@ -547,7 +548,7 @@ private View card(SkillHubModels.Summary skill) { icon.setIconSizeDp(48, 25); icon.setClickable(false); icon.setFocusable(false); - + icon.setBackground(LineTheme.rounded(getContext(), LineTheme.ACCENT_MUTED, 12)); card.addView(icon, new LinearLayout.LayoutParams( LineTheme.dp(getContext(), 48), LineTheme.dp(getContext(), 48))); iconLoader.load(skill.getIconUrl(), icon); @@ -565,7 +566,7 @@ private View card(SkillHubModels.Summary skill) { titleRow.setGravity(Gravity.CENTER_VERTICAL); TextView title = LineTheme.textMedium(getContext(), skill.getName(), LineTheme.FONT_MD, LineTheme.TEXT); - title.setMaxLines(2); + title.setMaxLines(1); titleRow.addView(title, new LinearLayout.LayoutParams( 0, LayoutParams.WRAP_CONTENT, 1f)); if (skill.isVerified()) { @@ -597,7 +598,7 @@ private View card(SkillHubModels.Summary skill) { apiParams.leftMargin = LineTheme.dp(getContext(), LineTheme.XS); tags.addView(apiKey, apiParams); } - + copy.addView(tags, tagsParams); } TextView description = LineTheme.text(getContext(), skill.getDescription(), @@ -615,7 +616,7 @@ private View card(SkillHubModels.Summary skill) { LinearLayout.LayoutParams statsParams = new LinearLayout.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); statsParams.topMargin = LineTheme.dp(getContext(), LineTheme.XS); - + copy.addView(stats, statsParams); IconButtonView chevron = new IconButtonView(getContext(), IconButtonView.CHEVRON_RIGHT); chevron.setIconColor(LineTheme.TEXT_TERTIARY); diff --git a/app/src/main/java/cn/lineai/ui/component/SshSettingsScreenView.java b/app/src/main/java/cn/lineai/ui/component/SshSettingsScreenView.java index 254fe1f0..f7f33c64 100644 --- a/app/src/main/java/cn/lineai/ui/component/SshSettingsScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/SshSettingsScreenView.java @@ -66,12 +66,10 @@ public SshSettingsScreenView(Context context, Listener listener) { form.addView(portField, formParams(context)); form.addView(usernameField, formParams(context)); form.addView(passwordField, formParams(context)); - DisclosureSectionView keys = new DisclosureSectionView(context, context.getString(R.string.screen_ssh_field_private_key), !config.getPrivateKey().isEmpty()); - keys.getBody().addView(privateKeyField, formParams(context)); - keys.getBody().addView(passphraseField, formParams(context)); - form.addView(keys, formParams(context)); + form.addView(privateKeyField, formParams(context)); + form.addView(passphraseField, formParams(context)); - LinearLayout actions = new AdaptiveActionsView(context); + LinearLayout actions = new LinearLayout(context); actions.setOrientation(HORIZONTAL); LinearLayout saveButton = button(context, context.getString(R.string.screen_ssh_save), IconButtonView.SAVE, false, v -> { listener.onSaveConfig(readConfig()); diff --git a/app/src/main/java/cn/lineai/ui/component/StorageManagementScreenView.java b/app/src/main/java/cn/lineai/ui/component/StorageManagementScreenView.java index 170c228a..0ad6f1c8 100644 --- a/app/src/main/java/cn/lineai/ui/component/StorageManagementScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/StorageManagementScreenView.java @@ -44,13 +44,12 @@ public StorageManagementScreenView(Context context, Listener listener) { this.refreshButton = (RefreshCwButtonView) getRightAction(); this.refreshButton.setOnClickListener(v -> loadStats()); LinearLayout content = getContent(); - LineTheme.padding(content, 16, 8, 16, 32); + LineTheme.padding(content, LineTheme.LG, LineTheme.LG, LineTheme.LG, 100); LinearLayout summary = new LinearLayout(context); summary.setOrientation(VERTICAL); - - summary.setBackground(LineTheme.roundedStroke(context, LineTheme.SURFACE_ELEVATED, 14, LineTheme.BORDER_LIGHT)); - LineTheme.padding(summary, 16, 16, 16, 20); + summary.setBackground(LineTheme.rounded(context, LineTheme.SURFACE_ELEVATED, 12)); + LineTheme.padding(summary, LineTheme.LG, LineTheme.LG, LineTheme.LG, LineTheme.LG); TextView label = LineTheme.textMedium(context, context.getString(R.string.screen_storage_counted), LineTheme.FONT_XS, LineTheme.TEXT_TERTIARY); summary.addView(label, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); totalSizeView = LineTheme.text(context, context.getString(R.string.screen_storage_calculating), LineTheme.FONT_XXL, LineTheme.TEXT, Typeface.BOLD); @@ -89,7 +88,7 @@ public StorageManagementScreenView(Context context, Listener listener) { } private static View createRefreshButton(Context context) { - View button = new RefreshCwButtonView(context, ScreenHeaderView.ICON_SIZE_DP); + View button = new RefreshCwButtonView(context, 18); button.setContentDescription(context.getString(R.string.common_refresh)); return button; } @@ -98,12 +97,11 @@ private LinearLayout createStorageRow(int iconType, String title, String desc) { LinearLayout row = new LinearLayout(context); row.setOrientation(HORIZONTAL); row.setGravity(Gravity.CENTER_VERTICAL); - - row.setBackground(LineTheme.roundedStroke(context, LineTheme.SURFACE_ELEVATED, 14, LineTheme.BORDER_LIGHT)); - LineTheme.padding(row, 12, 16, 12, 16); + row.setBackground(LineTheme.rounded(context, LineTheme.SURFACE_ELEVATED, 12)); + LineTheme.padding(row, LineTheme.MD, LineTheme.MD, LineTheme.MD, LineTheme.MD); FrameLayout iconWrap = new FrameLayout(context); - iconWrap.setBackground(null); + iconWrap.setBackground(LineTheme.rounded(context, LineTheme.ACCENT_MUTED, 19)); IconButtonView icon = new IconButtonView(context, iconType); icon.setIconColor(LineTheme.ACCENT); icon.setIconSizeDp(38, 19); diff --git a/app/src/main/java/cn/lineai/ui/component/TermuxIntegrationScreenView.java b/app/src/main/java/cn/lineai/ui/component/TermuxIntegrationScreenView.java index 744cb074..c74c8343 100644 --- a/app/src/main/java/cn/lineai/ui/component/TermuxIntegrationScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/TermuxIntegrationScreenView.java @@ -40,7 +40,7 @@ public TermuxIntegrationScreenView(Context context, Listener listener) { this.listener = listener; LinearLayout content = getContent(); - LineTheme.padding(content, 28, 8, 28, 48); + LineTheme.padding(content, LineTheme.LG, LineTheme.LG, LineTheme.LG, 100); LinearLayout intro = card(context); intro.addView(title(context, context.getString(R.string.screen_termux_section_use))); @@ -221,8 +221,8 @@ private LinearLayout button(Context context, String label, int iconType, boolean private LinearLayout card(Context context) { LinearLayout card = new LinearLayout(context); card.setOrientation(VERTICAL); - card.setBackground(null); - LineTheme.padding(card, 0, 16, 0, 24); + card.setBackground(LineTheme.rounded(context, LineTheme.SURFACE_ELEVATED, 12)); + LineTheme.padding(card, LineTheme.LG, LineTheme.LG, LineTheme.LG, LineTheme.LG); return card; } @@ -243,7 +243,7 @@ private void addCard(LinearLayout content, LinearLayout card) { content.addView(card, params); } - private static final class GridLikeActions extends ScreenSurfaceView { + private static final class GridLikeActions extends LinearLayout { GridLikeActions(Context context) { super(context); setOrientation(VERTICAL); diff --git a/app/src/test/java/cn/lineai/ui/component/SecondaryScreenLayoutTest.java b/app/src/test/java/cn/lineai/ui/component/SecondaryScreenLayoutTest.java index 469dab85..732002c8 100644 --- a/app/src/test/java/cn/lineai/ui/component/SecondaryScreenLayoutTest.java +++ b/app/src/test/java/cn/lineai/ui/component/SecondaryScreenLayoutTest.java @@ -93,22 +93,20 @@ private void layout(View view,int width,int height) { TextView models=text(view,activity.getString(cn.lineai.R.string.settings_row_models_title)); clickable(models).performClick();assertTrue(events.contains("onItem")); ScreenHeaderView header=find(view,ScreenHeaderView.class); - ((ViewGroup)header.getChildAt(0)).getChildAt(0).performClick();assertTrue(events.contains("onBack")); + header.getChildAt(0).performClick();assertTrue(events.contains("onBack")); } - @Test public void foldingFormRetainsDraftAndSaveCallback() throws Exception { + @Test public void classicSshFormRetainsDraftAndSaveCallback() throws Exception { SshSettingsScreenView view=new SshSettingsScreenView(activity,listener(SshSettingsScreenView.Listener.class)); - DisclosureSectionView section=find(view,DisclosureSectionView.class); - assertEquals(View.GONE,section.getBody().getVisibility()); - section.getChildAt(0).performClick(); - EditText key=find(section,EditText.class); key.setText("draft-private-key"); - section.getChildAt(0).performClick();section.getChildAt(0).performClick(); + assertNull(find(view,DisclosureSectionView.class)); + FormTextFieldView keyField=(FormTextFieldView)text(view,activity.getString(cn.lineai.R.string.screen_ssh_field_private_key)).getParent(); + EditText key=keyField.getInput(); key.setText("draft-private-key"); assertEquals("draft-private-key",key.getText().toString()); clickable(text(view,activity.getString(cn.lineai.R.string.screen_ssh_save))).performClick(); assertTrue(events.contains("onSaveConfig")); } @Test public void darkPagesAndTabletReadingWidth() throws Exception { LineTheme.apply(ThemePalette.forMode("dark")); - View view=screen("Extensions");layout(view,1100,800);assertEquals(154,view.getPaddingLeft()); + View view=screen("Extensions");layout(view,1100,800);assertEquals(0,view.getPaddingLeft()); layout(view,390,844);screenshot(view,"dark-390-Extensions"); view=screen("Settings");layout(view,390,844);screenshot(view,"dark-390-Settings"); } @@ -125,15 +123,12 @@ private void layout(View view,int width,int height) { panel.animate().cancel();panel.setTranslationY(0);sheet.getChildAt(0).animate().cancel();sheet.getChildAt(0).setAlpha(1); screenshot(sheet,"large-font-320-Sheet"); } - @Test public void protocolPickerSwitchesWithoutLosingNameDraft() throws Exception { + @Test public void classicProtocolTabsSwitchWithoutLosingNameDraft() throws Exception { ModelAddScreenView view=new ModelAddScreenView(activity,null,false,listener(ModelAddScreenView.Listener.class)); activity.setContentView(view);layout(view,390,844); EditText name=find(view,EditText.class);name.setText("My model"); - text(view,"OpenAI ›").performClick(); - android.app.Dialog dialog=org.robolectric.shadows.ShadowDialog.getLatestDialog(); - assertTrue(dialog.isShowing()); - clickable(text(dialog.getWindow().getDecorView(),"Anthropic")).performClick(); - assertFalse(dialog.isShowing());assertNotNull(text(view,"Anthropic ›"));assertEquals("My model",name.getText().toString()); + clickable(text(view,"Anthropic")).performClick(); + assertEquals("My model",name.getText().toString()); } @Test public void modelRowsDispatchSelectionAndKeepManagement() throws Exception { ModelConfig model=ModelConfig.builder("one","工作模型",ModelProtocolType.OPENAI_COMPATIBLE,"Custom","https://example.invalid/v1","","example-model").build(); @@ -218,10 +213,10 @@ private void layout(View view,int width,int height) { layout(view, 390, 844); ScreenHeaderView header = find(view, ScreenHeaderView.class); View refresh = find(view, RefreshCwButtonView.class); - View back = ((ViewGroup) header.getChildAt(0)).getChildAt(0); + View back = header.getChildAt(0); assertEquals(back.getWidth(), refresh.getWidth()); assertEquals(back.getHeight(), refresh.getHeight()); - assertEquals(48, refresh.getWidth()); + assertEquals(ScreenHeaderView.ACTION_SIZE_DP, refresh.getWidth()); assertTrue(header.getHeight() < 130); assertTrue(view.getScrollView().getHeight() > 600); assertTrue(text(view, activity.getString(cn.lineai.R.string.screen_storage_row_home)).getHeight() > 0); @@ -245,13 +240,14 @@ private void layout(View view,int width,int height) { screenshot(tools, "execution-without-ids"); } - @Test public void skillStoreTitleIsBesideBackAndContentUsesNarrowGutters() throws Exception { + @Test public void skillStoreUsesClassicCenteredHeaderAndGutters() throws Exception { SkillStoreScreenView view = (SkillStoreScreenView) screen("SkillStore"); layout(view, 390, 844); ScreenHeaderView header = find(view, ScreenHeaderView.class); - View back = ((ViewGroup) header.getChildAt(0)).getChildAt(0); + View back = header.getChildAt(0); TextView title = text(header, activity.getString(cn.lineai.R.string.skillhub_title_store)); - assertSame(back.getParent(), title.getParent()); + assertSame(header, back.getParent()); + assertSame(header, title.getParent()); assertEquals(back.getRight(), title.getLeft()); assertEquals(16, view.getContent().getPaddingLeft()); assertEquals(16, view.getContent().getPaddingRight()); diff --git a/app/src/test/java/cn/lineai/ui/component/UiCorrectionsTest.java b/app/src/test/java/cn/lineai/ui/component/UiCorrectionsTest.java index 7dae354a..aa777b18 100644 --- a/app/src/test/java/cn/lineai/ui/component/UiCorrectionsTest.java +++ b/app/src/test/java/cn/lineai/ui/component/UiCorrectionsTest.java @@ -105,12 +105,12 @@ private void screenshot(View view,String name) throws Exception { ModelListScreenView models=new ModelListScreenView(activity,Collections.emptyList(),"",listener(ModelListScreenView.Listener.class)); layout(models);assertMatchingHeaderActions(models,IconButtonView.PLUS);screenshot(models,"models"); ModelAddScreenView add=new ModelAddScreenView(activity,null,false,listener(ModelAddScreenView.Listener.class));layout(add); - assertEquals(13f,text(add,activity.getString(R.string.common_save)).getTextSize(),.01f); + assertEquals((float)LineTheme.FONT_MD,text(add,activity.getString(R.string.common_save)).getTextSize(),.01f); screenshot(add,"model-add"); } private void assertMatchingHeaderActions(View page,int rightType) { IconButtonView left=icon(page,IconButtonView.CHEVRON_LEFT),right=icon(page,rightType); - assertEquals(22,iconSize(left));assertEquals(iconSize(left),iconSize(right)); + assertEquals(22,iconSize(left)); assertEquals(left.getWidth(),right.getWidth());assertEquals(left.getHeight(),right.getHeight()); } @Test public void settingsRowsUseRestoredGroupsAndStandaloneCardsKeepGaps() throws Exception { diff --git a/update.md b/update.md index b7f72d65..c1d9a6a1 100644 --- a/update.md +++ b/update.md @@ -40,7 +40,7 @@ ### 设置、主题与抽屉细节 -- **设置结构恢复与配色更新** - 设置主页与共享设置子页恢复 UI 重构前的分区卡片、行内说明、彩色图标底板和分割线;继续使用新版 `LineTheme` 配色及内置主题色阶,保证卡片和应用背景清晰区分 +- **完整设置树结构恢复与配色更新** - 设置主页及模型、AI 行为、工具与执行、输入输出、主题、安全、数据、扩展、Skill Hub、存储、SSH、Termux、关于等全部子设置恢复 UI 重构前的页头、内容宽度、分区卡片、表单、行内说明、彩色图标底板和分割线;继续使用新版 `LineTheme` 配色及内置主题色阶 - **主题编辑器保留完整操作** - 自定义主题继续使用完整的颜色编辑布局,保留已有配置入口,并补齐保存按钮右侧留白;浅色与深色系统栏颜色随主题适配 - **模型管理布局** - 缩小本地模型入口图标,调整预设提供商间隔和页面边距;模型选择短列表按内容高度展开,长列表受最大高度限制,模型长按菜单去除多余底部留白 - **工作区与目录抽屉** - 工作区选中背景铺满抽屉内容宽度,保留工作区选择与长按删除;目录选择确认操作置于面板内,限制窄屏和横屏下的面板尺寸 From 27b9da988ec64a686a47defbde02551730db2826 Mon Sep 17 00:00:00 2001 From: LangLang03 Date: Sat, 5 Sep 2026 22:34:22 +0800 Subject: [PATCH 2/3] fix: keep tool reviews out of conversation context --- .../cn/lineai/mvp/ChatUiStateAssembler.java | 4 +- .../lineai/mvp/GenerationFlowController.java | 10 -- .../lineai/mvp/MainControllerInitializer.java | 5 - .../java/cn/lineai/mvp/MainCoordinator.java | 15 +- .../lineai/mvp/OutputSettingsController.java | 2 + .../mvp/SettingsManagementController.java | 12 ++ .../mvp/ToolConfirmationController.java | 11 -- .../cn/lineai/mvp/ToolReviewController.java | 148 +++++++++++++++--- .../ui/component/AssistantTurnView.java | 10 ++ .../ui/component/ChatMessageListView.java | 6 +- .../component/OutputSettingsScreenView.java | 10 ++ .../lineai/ui/component/ScreenFactories.java | 5 + .../lineai/ui/component/UserMessageView.java | 2 +- app/src/main/res/values-ru/strings.xml | 3 + app/src/main/res/values-zh/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + .../mvp/SettingsManagementControllerTest.java | 16 ++ .../mvp/ToolConfirmationControllerTest.java | 9 +- .../lineai/mvp/ToolReviewControllerTest.java | 85 ++++++++++ .../lineai/ui/component/ChatLayoutTest.java | 38 ++++- .../component/SecondaryScreenLayoutTest.java | 2 +- .../java/cn/lineai/model/ChatUiState.java | 25 ++- .../java/cn/lineai/model/OutputSettings.java | 11 ++ .../cn/lineai/data/repository/DiffRecord.java | 26 +++ .../data/repository/DiffRepository.java | 29 +++- .../cn/lineai/data/repository/DiffStore.java | 2 + .../repository/OutputSettingsRepository.java | 8 +- .../lineai/tool/ui/view/ToolCallReadView.java | 3 + update.md | 7 +- 29 files changed, 447 insertions(+), 63 deletions(-) create mode 100644 app/src/test/java/cn/lineai/mvp/ToolReviewControllerTest.java diff --git a/app/src/main/java/cn/lineai/mvp/ChatUiStateAssembler.java b/app/src/main/java/cn/lineai/mvp/ChatUiStateAssembler.java index 31ef3e98..ffa6a11a 100644 --- a/app/src/main/java/cn/lineai/mvp/ChatUiStateAssembler.java +++ b/app/src/main/java/cn/lineai/mvp/ChatUiStateAssembler.java @@ -73,6 +73,7 @@ public ChatUiState assemble( hasConfiguredModel, aiSettings.isThinkingScrollEnabled(), aiSettings.isThinkingAutoExpandEnabled(), + outputSettings.isProcessAutoExpandEnabled(), outputSettings.isCodeWrapEnabled(), outputSettings.getBrowserMode(), inputSettings.getEnterKeyBehavior(), @@ -80,7 +81,8 @@ public ChatUiState assemble( conversationId, messages, selectedModelId, - availableModels + availableModels, + null ); } diff --git a/app/src/main/java/cn/lineai/mvp/GenerationFlowController.java b/app/src/main/java/cn/lineai/mvp/GenerationFlowController.java index a2489f92..f6774de2 100644 --- a/app/src/main/java/cn/lineai/mvp/GenerationFlowController.java +++ b/app/src/main/java/cn/lineai/mvp/GenerationFlowController.java @@ -735,16 +735,6 @@ private void handleToolExecutionBatch( toolMessageController.addOrReplaceToolResults(batch.getCompletedResults()); int executedCount = usedToolCallCount + batch.getCompletedResults().size(); if (batch.getPendingCall() != null) { - ToolResult pendingResult = ToolResult.withReview( - batch.getPendingCall().getId(), - batch.getPendingCall().getName(), - "", - false, - "", - "pending", - "" - ); - addOrReplaceToolResult(pendingResult); toolConfirmationController.setPendingToolExecution(new PendingToolExecution( generationId, selectedModel, diff --git a/app/src/main/java/cn/lineai/mvp/MainControllerInitializer.java b/app/src/main/java/cn/lineai/mvp/MainControllerInitializer.java index 4d15d26d..c4b8e48e 100644 --- a/app/src/main/java/cn/lineai/mvp/MainControllerInitializer.java +++ b/app/src/main/java/cn/lineai/mvp/MainControllerInitializer.java @@ -324,11 +324,6 @@ public void refreshFileTreeAfterRevert(String filePath) { coordinator.refreshFileTreeAfterRevert(filePath); } - @Override - public void persistCurrentConversation() { - coordinator.persistCurrentConversation(); - } - @Override public void render() { coordinator.render(); diff --git a/app/src/main/java/cn/lineai/mvp/MainCoordinator.java b/app/src/main/java/cn/lineai/mvp/MainCoordinator.java index 856cb5af..0500edd1 100644 --- a/app/src/main/java/cn/lineai/mvp/MainCoordinator.java +++ b/app/src/main/java/cn/lineai/mvp/MainCoordinator.java @@ -48,6 +48,7 @@ import cn.lineai.security.UrlPolicy; import cn.lineai.tool.BaseTool; import cn.lineai.model.ChatMessage; +import cn.lineai.model.ChatUiState; import cn.lineai.model.FileTreeNode; import cn.lineai.model.KeepAliveSettings; import cn.lineai.model.StorageStatsUiModel; @@ -624,6 +625,11 @@ public void onCodeWrapChanged(boolean enabled) { settingsManagementController.setCodeWrapEnabled(enabled); } + @Override + public void onProcessAutoExpandChanged(boolean enabled) { + settingsManagementController.setProcessAutoExpandEnabled(enabled); + } + @Override public void onBrowserModeChanged(String mode) { settingsManagementController.setBrowserMode(mode); @@ -1039,7 +1045,7 @@ void render() { return; } String activeChatMode = syncModePermission(); - viewProxy.render(chatUiStateAssembler.assemble( + ChatUiState uiState = chatUiStateAssembler.assemble( projectState.label(), projectState.source(), projectState.path(), @@ -1047,7 +1053,12 @@ void render() { activeChatMode, chatSessionStore.isStreaming(), messages - ).withToolApproval(generationFlowController == null ? null : generationFlowController.pendingToolApproval())); + ); + if (toolReviewController != null) { + uiState = uiState.withDisplayMessages(toolReviewController.applyLocalReviews(messages)); + } + viewProxy.render(uiState.withToolApproval( + generationFlowController == null ? null : generationFlowController.pendingToolApproval())); } void resetTodoState() { diff --git a/app/src/main/java/cn/lineai/mvp/OutputSettingsController.java b/app/src/main/java/cn/lineai/mvp/OutputSettingsController.java index 4bf2fc19..78bab1fb 100644 --- a/app/src/main/java/cn/lineai/mvp/OutputSettingsController.java +++ b/app/src/main/java/cn/lineai/mvp/OutputSettingsController.java @@ -7,6 +7,8 @@ public interface OutputSettingsController { void onCodeWrapChanged(boolean enabled); + void onProcessAutoExpandChanged(boolean enabled); + void onBrowserModeChanged(String mode); void onBrowserJavaScriptChanged(boolean enabled); diff --git a/app/src/main/java/cn/lineai/mvp/SettingsManagementController.java b/app/src/main/java/cn/lineai/mvp/SettingsManagementController.java index 170d5fe9..0545d476 100644 --- a/app/src/main/java/cn/lineai/mvp/SettingsManagementController.java +++ b/app/src/main/java/cn/lineai/mvp/SettingsManagementController.java @@ -74,6 +74,8 @@ interface SettingsStore { void setCodeWrapEnabled(boolean enabled); + void setProcessAutoExpandEnabled(boolean enabled); + void setBrowserMode(String mode); void setBrowserJavaScriptEnabled(boolean enabled); @@ -232,6 +234,11 @@ public void setCodeWrapEnabled(boolean enabled) { outputSettingsRepository.setCodeWrapEnabled(enabled); } + @Override + public void setProcessAutoExpandEnabled(boolean enabled) { + outputSettingsRepository.setProcessAutoExpandEnabled(enabled); + } + @Override public void setBrowserMode(String mode) { outputSettingsRepository.setBrowserMode(mode); @@ -427,6 +434,11 @@ public void setCodeWrapEnabled(boolean enabled) { host.render(); } + public void setProcessAutoExpandEnabled(boolean enabled) { + settingsStore.setProcessAutoExpandEnabled(enabled); + host.render(); + } + public void setBrowserMode(String mode) { settingsStore.setBrowserMode(mode); host.render(); diff --git a/app/src/main/java/cn/lineai/mvp/ToolConfirmationController.java b/app/src/main/java/cn/lineai/mvp/ToolConfirmationController.java index 504a895c..e0dc851d 100644 --- a/app/src/main/java/cn/lineai/mvp/ToolConfirmationController.java +++ b/app/src/main/java/cn/lineai/mvp/ToolConfirmationController.java @@ -274,17 +274,6 @@ private void handlePendingToolReview(PendingToolExecution pending, String state) rememberSessionAutoConfirmation(pending.getToolCall()); } - ToolResult accepted = ToolResult.withReview( - pending.getToolCall().getId(), - pending.getToolCall().getName(), - "", - false, - "", - "accepted", - "" - ); - callback.addOrReplaceToolResult(accepted); - callback.persistCurrentConversation(); callback.render(); callback.executeAcceptedPendingTool(pending); } diff --git a/app/src/main/java/cn/lineai/mvp/ToolReviewController.java b/app/src/main/java/cn/lineai/mvp/ToolReviewController.java index 2ad688aa..5eca43fd 100644 --- a/app/src/main/java/cn/lineai/mvp/ToolReviewController.java +++ b/app/src/main/java/cn/lineai/mvp/ToolReviewController.java @@ -4,13 +4,18 @@ import cn.lineai.data.repository.DiffRepository; import cn.lineai.data.repository.DiffStore; import cn.lineai.data.service.FileRestorer; +import cn.lineai.model.ChatMessage; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.json.JSONArray; +import org.json.JSONObject; final class ToolReviewController { interface Host { void refreshFileTreeAfterRevert(String filePath); - void persistCurrentConversation(); - void render(); } @@ -19,6 +24,7 @@ interface Host { private final BackgroundTaskRunner backgroundTasks; private final MainThreadDispatcher mainThread; private final Host host; + private final Map localReviewCache = new HashMap<>(); ToolReviewController( DiffStore diffRepository, @@ -49,8 +55,10 @@ void review(String toolCallId, String state, String diffId) { return; } } - toolMessageController.updateToolReview(toolCallId, resolvedDiffId, normalizedState, ""); - host.persistCurrentConversation(); + if (resolvedDiffId.length() == 0) { + resolvedDiffId = toolMessageController.findToolMessageDiffId(toolCallId); + } + setLocalReview(resolvedDiffId, normalizedState, ""); host.render(); } @@ -61,8 +69,7 @@ private void rejectWithRevert(String toolCallId, String diffId) { DiffRepository.RevertResult result = diffRepository.revertDiff(diffId); if (!result.isSuccess()) { mainThread.post(() -> { - toolMessageController.updateToolReview(toolCallId, diffId, "", result.getMessage()); - host.persistCurrentConversation(); + setLocalReview(diffId, "", result.getMessage()); host.render(); }); return; @@ -72,13 +79,7 @@ private void rejectWithRevert(String toolCallId, String diffId) { FileRestorer.restoreOldContent(result.getDiffRecord()); } catch (Exception e) { mainThread.post(() -> { - toolMessageController.updateToolReview( - toolCallId, - diffId, - "", - "File restore failed: " + e.getMessage() - ); - host.persistCurrentConversation(); + setLocalReview(diffId, "", "File restore failed: " + e.getMessage()); host.render(); }); return; @@ -86,16 +87,123 @@ private void rejectWithRevert(String toolCallId, String diffId) { diffRepository.markReverted(diffId); } mainThread.post(() -> { - toolMessageController.updateToolReview( - toolCallId, - diffId, - "rejected", - "Reverted change to " + filePath - ); + setLocalReview(diffId, "rejected", "Reverted change to " + filePath); host.refreshFileTreeAfterRevert(filePath); - host.persistCurrentConversation(); host.render(); }); }); } + + List applyLocalReviews(List source) { + ArrayList display = new ArrayList<>(); + if (source == null) { + return display; + } + for (ChatMessage message : source) { + ChatMessage next = message; + if (message != null && message.getRole() == ChatMessage.Role.TOOL) { + DiffRecord direct = localReview(message.getDiffId()); + if (direct != null) { + next = next.withToolReview( + message.getDiffId(), + reviewState(direct), + direct.getReviewMessage() + ); + } + String content = applyNestedLocalReviews(next.getContent()); + if (!content.equals(next.getContent())) { + next = next.withContent(content, next.getReasoningContent(), next.isStreaming()); + } + } + display.add(next); + } + return display; + } + + private String applyNestedLocalReviews(String content) { + if (content == null || content.trim().length() == 0) { + return content == null ? "" : content; + } + try { + JSONObject root = new JSONObject(content); + return applyNestedLocalReviews(root) ? root.toString() : content; + } catch (Exception ignored) { + return content; + } + } + + private boolean applyNestedLocalReviews(JSONObject object) throws Exception { + if (object == null) { + return false; + } + boolean changed = applyLocalReviews(object.optJSONArray("tool_calls")); + JSONArray agents = object.optJSONArray("agents"); + if (agents != null) { + for (int i = 0; i < agents.length(); i++) { + JSONObject agent = agents.optJSONObject(i); + changed = applyNestedLocalReviews(agent) || changed; + } + } + return changed; + } + + private boolean applyLocalReviews(JSONArray calls) throws Exception { + if (calls == null) { + return false; + } + boolean changed = false; + for (int i = 0; i < calls.length(); i++) { + JSONObject item = calls.optJSONObject(i); + if (item == null) { + continue; + } + JSONObject result = item.optJSONObject("result"); + DiffRecord local = localReview(result == null ? "" : result.optString("diff_id")); + if (local != null) { + result.put("review_state", reviewState(local)); + result.put("review_message", local.getReviewMessage()); + changed = true; + } + changed = applyNestedLocalReviews(item) || changed; + } + return changed; + } + + private DiffRecord localReview(String diffId) { + if (diffId == null || diffId.length() == 0) { + return null; + } + DiffRecord record; + synchronized (localReviewCache) { + if (localReviewCache.containsKey(diffId)) { + record = localReviewCache.get(diffId); + } else { + record = diffRepository.getDiff(diffId); + localReviewCache.put(diffId, record); + } + } + if (record == null || reviewState(record).length() == 0 && record.getReviewMessage().length() == 0) { + return null; + } + return record; + } + + private void setLocalReview(String diffId, String state, String message) { + if (diffId == null || diffId.length() == 0) { + return; + } + diffRepository.setReview(diffId, state, message); + synchronized (localReviewCache) { + localReviewCache.remove(diffId); + } + } + + private String reviewState(DiffRecord record) { + if (record == null) { + return ""; + } + return record.getReviewState().length() > 0 + ? record.getReviewState() + : record.isReverted() ? "rejected" : ""; + } } diff --git a/app/src/main/java/cn/lineai/ui/component/AssistantTurnView.java b/app/src/main/java/cn/lineai/ui/component/AssistantTurnView.java index 23f38539..d1afd619 100644 --- a/app/src/main/java/cn/lineai/ui/component/AssistantTurnView.java +++ b/app/src/main/java/cn/lineai/ui/component/AssistantTurnView.java @@ -117,6 +117,12 @@ public AssistantTurnView(Context context) { public void bind(ConversationTimeline.Row row, Map disclosure, String projectPath, ToolReviewListener reviewer, MarkdownLinkHandler links, MessageActionListener actions, boolean codeWrap, boolean generating) { + bind(row, disclosure, projectPath, reviewer, links, actions, codeWrap, generating, false); + } + + public void bind(ConversationTimeline.Row row, Map disclosure, String projectPath, + ToolReviewListener reviewer, MarkdownLinkHandler links, MessageActionListener actions, + boolean codeWrap, boolean generating, boolean processAutoExpand) { String nextIdentity = row.first.getId(); if (!identity.equals(nextIdentity)) { process.removeAllViews(); blocks.clear(); files.removeAllViews(); fileViews.clear(); @@ -126,6 +132,10 @@ public void bind(ConversationTimeline.Row row, Map disclosure, this.reviewer = reviewer; this.links = links; this.codeWrap = codeWrap; this.generating = generating; hasTools = row.isTurn; + String processKey = identity + ":process"; + if (hasTools && processAutoExpand && !disclosure.containsKey(processKey)) { + disclosure.put(processKey, true); + } setProcessVisibility(hasTools); updateProcessLabel(); if (row.answer != null) { diff --git a/app/src/main/java/cn/lineai/ui/component/ChatMessageListView.java b/app/src/main/java/cn/lineai/ui/component/ChatMessageListView.java index d537f941..4d92c438 100644 --- a/app/src/main/java/cn/lineai/ui/component/ChatMessageListView.java +++ b/app/src/main/java/cn/lineai/ui/component/ChatMessageListView.java @@ -453,6 +453,7 @@ private static final class MessageAdapter extends BaseAdapter { private final Map disclosure = new HashMap<>(); private boolean thinkingAutoExpand; private boolean thinkingScroll; + private boolean processAutoExpand; private boolean codeWrapEnabled; private boolean multiSelectMode; private Set selectedMessageIds = java.util.Collections.emptySet(); @@ -497,6 +498,7 @@ boolean render(ChatUiState state) { boolean nextShowConfigureState = nextMessages.isEmpty() && state != null && !state.hasConfiguredModel(); boolean nextThinkingAutoExpand = state != null && state.isThinkingAutoExpandEnabled(); boolean nextThinkingScroll = state == null || state.isThinkingScrollEnabled(); + boolean nextProcessAutoExpand = state != null && state.isProcessAutoExpandEnabled(); boolean nextCodeWrapEnabled = state != null && state.isCodeWrapEnabled(); String nextConversationId = state == null ? "" : state.getConversationId(); String nextProjectPath = state == null ? "" : state.getProjectPath(); @@ -506,6 +508,7 @@ boolean render(ChatUiState state) { && showConfigureState == nextShowConfigureState && thinkingAutoExpand == nextThinkingAutoExpand && thinkingScroll == nextThinkingScroll + && processAutoExpand == nextProcessAutoExpand && codeWrapEnabled == nextCodeWrapEnabled && stringEquals(conversationId, nextConversationId) && stringEquals(projectPath, nextProjectPath) @@ -524,6 +527,7 @@ && sameMessages(nextMessages)) { showConfigureState = nextShowConfigureState; thinkingAutoExpand = nextThinkingAutoExpand; thinkingScroll = nextThinkingScroll; + processAutoExpand = nextProcessAutoExpand; codeWrapEnabled = nextCodeWrapEnabled; conversationId = nextConversationId; projectPath = nextProjectPath; @@ -598,7 +602,7 @@ public View getView(int position, View convertView, android.view.ViewGroup paren : obtain(AssistantTurnView.class, key, new AssistantTurnView(context)); view.bind(timeline.get(position), disclosure, projectPath, toolReviewListener, markdownLinkHandler, messageActionListener, codeWrapEnabled, - generating && position == timeline.size() - 1); + generating && position == timeline.size() - 1, processAutoExpand); return view; } if (message.isModelSwitchNotification()) { diff --git a/app/src/main/java/cn/lineai/ui/component/OutputSettingsScreenView.java b/app/src/main/java/cn/lineai/ui/component/OutputSettingsScreenView.java index 5ab45d8a..9ae35873 100644 --- a/app/src/main/java/cn/lineai/ui/component/OutputSettingsScreenView.java +++ b/app/src/main/java/cn/lineai/ui/component/OutputSettingsScreenView.java @@ -14,6 +14,8 @@ public interface Listener { void onCodeWrapChanged(boolean enabled); + void onProcessAutoExpandChanged(boolean enabled); + void onBrowserModeChanged(String mode); void onBrowserJavaScriptChanged(boolean enabled); @@ -41,6 +43,14 @@ public OutputSettingsScreenView(Context context, OutputSettings settings, Listen boolean browserJavaScriptEnabled = safeSettings.isBrowserJavaScriptEnabled(); LinearLayout content = getContent(); + SettingsSectionView conversation = new SettingsSectionView(context, context.getString(R.string.screen_output_section_conversation)); + conversation.addRow(new SwitchRowView(context, IconButtonView.EXPAND, + context.getString(R.string.screen_output_process_auto_expand_label), + context.getString(R.string.screen_output_process_auto_expand_desc), + safeSettings.isProcessAutoExpandEnabled(), + (buttonView, isChecked) -> listener.onProcessAutoExpandChanged(isChecked)), false); + content.addView(conversation, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + previewView = new MarkdownView(context); previewView.setCodeWrapEnabled(codeWrapEnabled); previewView.setLinkHandler(url -> {}); diff --git a/app/src/main/java/cn/lineai/ui/component/ScreenFactories.java b/app/src/main/java/cn/lineai/ui/component/ScreenFactories.java index cb9b8358..237f4eea 100644 --- a/app/src/main/java/cn/lineai/ui/component/ScreenFactories.java +++ b/app/src/main/java/cn/lineai/ui/component/ScreenFactories.java @@ -355,6 +355,11 @@ public void onCodeWrapChanged(boolean enabled) { controller.onCodeWrapChanged(enabled); } + @Override + public void onProcessAutoExpandChanged(boolean enabled) { + controller.onProcessAutoExpandChanged(enabled); + } + @Override public void onBrowserModeChanged(String mode) { controller.onBrowserModeChanged(mode); diff --git a/app/src/main/java/cn/lineai/ui/component/UserMessageView.java b/app/src/main/java/cn/lineai/ui/component/UserMessageView.java index 1cc1496e..222008bb 100644 --- a/app/src/main/java/cn/lineai/ui/component/UserMessageView.java +++ b/app/src/main/java/cn/lineai/ui/component/UserMessageView.java @@ -30,7 +30,7 @@ public UserMessageView(Context context) { super(context); setOrientation(VERTICAL); setGravity(Gravity.END); - LineTheme.padding(this, 28, 16, 28, 32); + LineTheme.padding(this, 16, 16, 16, 32); defaultPaddingLeft = getPaddingLeft(); defaultPaddingTop = getPaddingTop(); defaultPaddingRight = getPaddingRight(); diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 30752832..2f4e2c1d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -285,6 +285,9 @@ Вывод и браузер Отображение кода + Отображение диалога + Автоматически раскрывать обработку + Автоматически открывать каждый раздел обработки при первом появлении Автоперенос кода Способ открытия страниц Встроенный браузер diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index e63e9396..1af7d02a 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -285,6 +285,9 @@ 输出与浏览 代码显示 + 对话显示 + 自动展开处理 + 处理区域首次出现时自动展开 代码自动换行 网页打开方式 内置浏览器 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 71e3a2aa..fdd5eebb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -285,6 +285,9 @@ Output & browser Code display + Conversation display + Auto-expand processing + Open each processing section automatically when it first appears Code auto wrap Page opening method Built-in browser diff --git a/app/src/test/java/cn/lineai/mvp/SettingsManagementControllerTest.java b/app/src/test/java/cn/lineai/mvp/SettingsManagementControllerTest.java index 695405af..25c83297 100644 --- a/app/src/test/java/cn/lineai/mvp/SettingsManagementControllerTest.java +++ b/app/src/test/java/cn/lineai/mvp/SettingsManagementControllerTest.java @@ -26,6 +26,16 @@ public void thinkingScrollChangeRenders() { Assert.assertTrue(fixture.host.rendered); } + @Test + public void processAutoExpandChangePersistsAndRenders() { + Fixture fixture = new Fixture(); + + fixture.controller.setProcessAutoExpandEnabled(true); + + Assert.assertTrue(fixture.store.processAutoExpandEnabled); + Assert.assertTrue(fixture.host.rendered); + } + @Test public void toneChangeDoesNotForceRender() { Fixture fixture = new Fixture(); @@ -112,6 +122,7 @@ private static final class Fixture { private static final class FakeSettingsStore implements SettingsManagementController.SettingsStore { private String toneMode = AiBehaviorSettings.TONE_CODING; private boolean thinkingScrollEnabled = true; + private boolean processAutoExpandEnabled; private String themeMode = ThemePalette.MODE_DARK; private String executionMode = "local"; private String mcpGroupId = ""; @@ -211,6 +222,11 @@ public OutputSettings getOutputSettings() { public void setCodeWrapEnabled(boolean enabled) { } + @Override + public void setProcessAutoExpandEnabled(boolean enabled) { + processAutoExpandEnabled = enabled; + } + @Override public void setBrowserMode(String mode) { } diff --git a/app/src/test/java/cn/lineai/mvp/ToolConfirmationControllerTest.java b/app/src/test/java/cn/lineai/mvp/ToolConfirmationControllerTest.java index b8b85d75..cf64aa55 100644 --- a/app/src/test/java/cn/lineai/mvp/ToolConfirmationControllerTest.java +++ b/app/src/test/java/cn/lineai/mvp/ToolConfirmationControllerTest.java @@ -22,6 +22,7 @@ public class ToolConfirmationControllerTest { assertNotNull(controller.pendingToolApproval()); controller.handleToolReview("accepted"); assertEquals(1, host.executed); assertTrue(host.grants.isEmpty()); assertNull(controller.pendingToolApproval()); + assertEquals(0, host.addedResults); assertEquals(0, host.persisted); assertEquals(1, host.rendered); } @Test public void permanentGrantIsSavedBeforeExecution() { Host host = new Host(); ToolConfirmationController controller = new ToolConfirmationController(host); @@ -67,11 +68,11 @@ public class ToolConfirmationControllerTest { } private static final class Host implements ToolConfirmationController.Callback { String scope = "ssh:host:/project"; boolean active = true; boolean grantedAtExecution; - int executed, continued; ToolResult result; final Set grants = new HashSet<>(); + int executed, continued, addedResults, persisted, rendered; ToolResult result; final Set grants = new HashSet<>(); public boolean isActiveGeneration(int generation) { return active; } - public void addOrReplaceToolResult(ToolResult result) { this.result = result; } - public void persistCurrentConversation() {} - public void render() {} + public void addOrReplaceToolResult(ToolResult result) { this.result = result; addedResults++; } + public void persistCurrentConversation() { persisted++; } + public void render() { rendered++; } public void continueToolExecution(int id, ModelConfig model, List calls, int count, String path, ModelCancellationToken token) { continued++; } public void executeAcceptedPendingTool(PendingToolExecution pending) { executed++; grantedAtExecution = isPermanentlyAllowed(scope, pending.getToolCall()); } public String currentConversationId() { return "conversation"; } diff --git a/app/src/test/java/cn/lineai/mvp/ToolReviewControllerTest.java b/app/src/test/java/cn/lineai/mvp/ToolReviewControllerTest.java new file mode 100644 index 00000000..f78a720c --- /dev/null +++ b/app/src/test/java/cn/lineai/mvp/ToolReviewControllerTest.java @@ -0,0 +1,85 @@ +package cn.lineai.mvp; + +import cn.lineai.data.repository.DiffRecord; +import cn.lineai.data.repository.DiffRepository; +import cn.lineai.data.repository.DiffStore; +import cn.lineai.model.ChatMessage; +import cn.lineai.model.tool.ToolCall; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; + +public final class ToolReviewControllerTest { + @Test + public void acceptingAFileEditKeepsConversationMessagesImmutable() { + ArrayList messages = messagesWithDiff("diff-1"); + FakeDiffStore diffs = new FakeDiffStore(); + diffs.record = new DiffRecord("diff-1", "/workspace/a.txt", "a", "b", true, 1L, false); + FakeHost host = new FakeHost(); + ToolMessageController toolMessages = new ToolMessageController(messages, () -> "next"); + ToolReviewController controller = new ToolReviewController( + diffs, toolMessages, new BackgroundTaskRunner(), + new MainThreadDispatcher(null, true), host); + + controller.review("edit-1", "accepted", "diff-1"); + + assertEquals("", messages.get(1).getReviewState()); + assertEquals("accepted", diffs.state); + assertEquals(1, host.rendered); + List display = controller.applyLocalReviews(messages); + assertEquals("accepted", display.get(1).getReviewState()); + assertNotSame(messages.get(1), display.get(1)); + } + + @Test + public void nestedAgentReviewIsAppliedOnlyToDisplayCopy() { + String nested = "{\"tool_calls\":[{\"id\":\"edit-1\",\"result\":{\"diff_id\":\"diff-1\",\"review_state\":\"\"}}]}"; + ChatMessage outer = ChatMessage.toolResult("tool", nested, "agent-1", "agent", false, "", "", ""); + FakeDiffStore diffs = new FakeDiffStore(); + diffs.record = new DiffRecord("diff-1", "/workspace/a.txt", "a", "b", true, 1L, false, "accepted", ""); + ToolReviewController controller = new ToolReviewController( + diffs, new ToolMessageController(new ArrayList<>(), () -> "next"), + new BackgroundTaskRunner(), new MainThreadDispatcher(null, true), new FakeHost()); + + List display = controller.applyLocalReviews(Collections.singletonList(outer)); + + assertFalse(outer.getContent().contains("accepted")); + assertTrue(display.get(0).getContent().contains("accepted")); + } + + private static ArrayList messagesWithDiff(String diffId) { + ArrayList messages = new ArrayList<>(); + messages.add(new ChatMessage("assistant", ChatMessage.Role.ASSISTANT, "", false) + .withToolCalls(Collections.singletonList(new ToolCall("edit-1", "file_edit", "{}")), false)); + messages.add(ChatMessage.toolResult("tool", "done", "edit-1", "file_edit", false, diffId, "", "")); + return messages; + } + + private static final class FakeHost implements ToolReviewController.Host { + int rendered; + @Override public void refreshFileTreeAfterRevert(String filePath) {} + @Override public void render() { rendered++; } + } + + private static final class FakeDiffStore implements DiffStore { + DiffRecord record; + String state = ""; + String message = ""; + @Override public DiffRecord recordDiff(String filePath, String oldContent, String newContent, boolean oldExists) { return record; } + @Override public DiffRecord getDiff(String diffId) { + if (record == null || !record.getId().equals(diffId)) return null; + return state.length() == 0 && message.length() == 0 ? record : new DiffRecord( + record.getId(), record.getFilePath(), record.getOldContent(), record.getNewContent(), + record.isOldExists(), record.getTimestamp(), record.isReverted(), state, message); + } + @Override public List getDiffChain(String filePath) { return Collections.emptyList(); } + @Override public DiffRepository.RevertResult revertDiff(String diffId) { return null; } + @Override public void markReverted(String diffId) {} + @Override public void setReview(String diffId, String state, String message) { + this.state = state == null ? "" : state; + this.message = message == null ? "" : message; + } + } +} diff --git a/app/src/test/java/cn/lineai/ui/component/ChatLayoutTest.java b/app/src/test/java/cn/lineai/ui/component/ChatLayoutTest.java index f4d8dc25..ccfd6f0f 100644 --- a/app/src/test/java/cn/lineai/ui/component/ChatLayoutTest.java +++ b/app/src/test/java/cn/lineai/ui/component/ChatLayoutTest.java @@ -92,8 +92,8 @@ private int wrapHeight(View view, int width) { view.bind(new ChatMessage("user", ChatMessage.Role.USER, "SSH 断开后,帮我自动重连。", false)); wrapHeight(view, 390); TextView text = text(view, "SSH 断开后,帮我自动重连。"); - assertNotNull(text.getBackground()); assertEquals(362, text.getRight()); - assertTrue(text.getLeft() > 28); + assertNotNull(text.getBackground()); assertEquals(374, text.getRight()); + assertTrue(text.getLeft() > 16); } @Test public void streamUpdatesRespectManualProcessAndToolDisclosure() { Map expansion = new HashMap<>(); @@ -123,6 +123,40 @@ private void bindTurn(AssistantTurnView view, Map expansion, Ch view.bind(ConversationTimeline.build(Arrays.asList(work, answer)).get(0), expansion, "/workspace", null, null, null, false, true); wrapHeight(view, 390); } + @Test public void processingCanAutoExpandWithoutOverridingAManualCollapse() { + ToolCall call = new ToolCall("read", ToolNames.FILE_READ, "{\"file_path\":\"/workspace/a.txt\"}"); + ChatMessage work = new ChatMessage("work", ChatMessage.Role.ASSISTANT, "", false) + .withToolCalls(Collections.singletonList(call), false); + ChatMessage answer = new ChatMessage("answer", ChatMessage.Role.ASSISTANT, "done", false); + Map state = new HashMap<>(); + AssistantTurnView view = new AssistantTurnView(activity); + + view.bind(ConversationTimeline.build(Arrays.asList(work, answer)).get(0), state, + "/workspace", null, null, null, false, false, true); + assertTrue(state.get("work:process")); + + state.put("work:process", false); + view.bind(ConversationTimeline.build(Arrays.asList(work, answer)).get(0), state, + "/workspace", null, null, null, false, false, true); + assertFalse(state.get("work:process")); + } + @Test public void readRowsUseToolSpecificWebIcons() { + cn.lineai.tool.ui.ToolInfoResolverProvider.setDefault(new cn.lineai.tool.ui.ToolInfoResolver() { + public cn.lineai.tool.ToolDisplayCategory getDisplayCategory(String name) { return cn.lineai.tool.ToolDisplayCategory.READ; } + public String getDisplayLabel(android.content.Context context, String name, org.json.JSONObject input, String workspace) { return name; } + public String getActionName(android.content.Context context, String name) { return name; } + public int getActionIcon(String name) { return "web_search".equals(name) ? cn.lineai.ui.theme.IconButtonView.SEARCH : cn.lineai.ui.theme.IconButtonView.GLOBE; } + public cn.lineai.tool.ToolInfo getToolInfo(String name) { return null; } + }); + ToolCallReadView search = new ToolCallReadView(activity); + search.bind(new ToolCall("search", "web_search", "{}"), null); + assertEquals(cn.lineai.ui.theme.IconButtonView.SEARCH, + find(search, cn.lineai.ui.theme.IconButtonView.class).getIconType()); + ToolCallReadView fetch = new ToolCallReadView(activity); + fetch.bind(new ToolCall("fetch", "web_fetch", "{}"), null); + assertEquals(cn.lineai.ui.theme.IconButtonView.GLOBE, + find(fetch, cn.lineai.ui.theme.IconButtonView.class).getIconType()); + } @Test public void readRowsAreNotClickableAndNeverContainTheFileContents() { ToolCallReadView view = new ToolCallReadView(activity); ToolCall call = new ToolCall("read", ToolNames.FILE_READ, "{\"file_path\":\"/workspace/a.txt\"}"); diff --git a/app/src/test/java/cn/lineai/ui/component/SecondaryScreenLayoutTest.java b/app/src/test/java/cn/lineai/ui/component/SecondaryScreenLayoutTest.java index 732002c8..b8c4c24b 100644 --- a/app/src/test/java/cn/lineai/ui/component/SecondaryScreenLayoutTest.java +++ b/app/src/test/java/cn/lineai/ui/component/SecondaryScreenLayoutTest.java @@ -78,7 +78,7 @@ private void layout(View view,int width,int height) { try { View view=screen(name);layout(view,320,720); assertTrue(view.getMeasuredHeight()>0); - if(Arrays.asList("Settings","Extensions","ModelAdd","SshSettings","MCPSettings","ThemeSettings","SkillStore","TerminalProviderDetail","MemorySettings","Tutorial").contains(name)) { + if(Arrays.asList("Settings","Extensions","ModelAdd","SshSettings","MCPSettings","OutputSettings","ThemeSettings","SkillStore","TerminalProviderDetail","MemorySettings","Tutorial").contains(name)) { layout(view,390,844);screenshot(view,"light-390-"+name); } } catch(Throwable error) { diff --git a/core-model/src/main/java/cn/lineai/model/ChatUiState.java b/core-model/src/main/java/cn/lineai/model/ChatUiState.java index c09206ef..9ab80c9d 100644 --- a/core-model/src/main/java/cn/lineai/model/ChatUiState.java +++ b/core-model/src/main/java/cn/lineai/model/ChatUiState.java @@ -16,6 +16,7 @@ public final class ChatUiState { private final boolean hasConfiguredModel; private final boolean thinkingScrollEnabled; private final boolean thinkingAutoExpandEnabled; + private final boolean processAutoExpandEnabled; private final boolean codeWrapEnabled; private final String browserMode; private final String enterKeyBehavior; @@ -109,6 +110,19 @@ public ChatUiState( boolean codeWrapEnabled, String browserMode, String enterKeyBehavior, String chatMode, String conversationId, List messages, String selectedModelId, List availableModels, ToolApproval toolApproval + ) { + this(projectLabel, projectPath, modelLabel, contextLabel, contextPercent, streaming, hasConfiguredModel, + thinkingScrollEnabled, thinkingAutoExpandEnabled, false, codeWrapEnabled, browserMode, + enterKeyBehavior, chatMode, conversationId, messages, selectedModelId, availableModels, toolApproval); + } + + public ChatUiState( + String projectLabel, String projectPath, String modelLabel, String contextLabel, + int contextPercent, boolean streaming, boolean hasConfiguredModel, + boolean thinkingScrollEnabled, boolean thinkingAutoExpandEnabled, + boolean processAutoExpandEnabled, boolean codeWrapEnabled, String browserMode, String enterKeyBehavior, + String chatMode, String conversationId, List messages, + String selectedModelId, List availableModels, ToolApproval toolApproval ) { this.toolApproval = toolApproval; this.projectLabel = projectLabel; @@ -121,6 +135,7 @@ public ChatUiState( this.hasConfiguredModel = hasConfiguredModel; this.thinkingScrollEnabled = thinkingScrollEnabled; this.thinkingAutoExpandEnabled = thinkingAutoExpandEnabled; + this.processAutoExpandEnabled = processAutoExpandEnabled; this.codeWrapEnabled = codeWrapEnabled; this.browserMode = OutputSettings.normalizeBrowserMode(browserMode); this.enterKeyBehavior = InputSettings.normalizeEnterKeyBehavior(enterKeyBehavior); @@ -137,9 +152,16 @@ public ChatUiState( public ToolApproval getToolApproval() { return toolApproval; } public ChatUiState withToolApproval(ToolApproval approval) { return new ChatUiState(projectLabel, projectPath, modelLabel, contextLabel, contextPercent, streaming, - hasConfiguredModel, thinkingScrollEnabled, thinkingAutoExpandEnabled, codeWrapEnabled, browserMode, + hasConfiguredModel, thinkingScrollEnabled, thinkingAutoExpandEnabled, processAutoExpandEnabled, + codeWrapEnabled, browserMode, enterKeyBehavior, chatMode, conversationId, messages, selectedModelId, availableModels, approval); } + public ChatUiState withDisplayMessages(List displayMessages) { + return new ChatUiState(projectLabel, projectPath, modelLabel, contextLabel, contextPercent, streaming, + hasConfiguredModel, thinkingScrollEnabled, thinkingAutoExpandEnabled, processAutoExpandEnabled, + codeWrapEnabled, browserMode, enterKeyBehavior, chatMode, conversationId, displayMessages, + selectedModelId, availableModels, toolApproval); + } public String getProjectLabel() { return projectLabel; } public String getProjectPath() { return projectPath; } public String getModelLabel() { return modelLabel; } @@ -151,6 +173,7 @@ public ChatUiState withToolApproval(ToolApproval approval) { public boolean hasConfiguredModel() { return hasConfiguredModel; } public boolean isThinkingScrollEnabled() { return thinkingScrollEnabled; } public boolean isThinkingAutoExpandEnabled() { return thinkingAutoExpandEnabled; } + public boolean isProcessAutoExpandEnabled() { return processAutoExpandEnabled; } public boolean isCodeWrapEnabled() { return codeWrapEnabled; } public String getBrowserMode() { return browserMode; } public String getEnterKeyBehavior() { return enterKeyBehavior; } diff --git a/core-model/src/main/java/cn/lineai/model/OutputSettings.java b/core-model/src/main/java/cn/lineai/model/OutputSettings.java index 9356b3ba..0ea98cf6 100644 --- a/core-model/src/main/java/cn/lineai/model/OutputSettings.java +++ b/core-model/src/main/java/cn/lineai/model/OutputSettings.java @@ -9,6 +9,7 @@ public final class OutputSettings { private final boolean browserJavaScriptEnabled; private final boolean allowAnyHttp; private final boolean bypassPathProtection; + private final boolean processAutoExpandEnabled; public OutputSettings(boolean codeWrapEnabled, String browserMode) { this(codeWrapEnabled, browserMode, false, false, false); @@ -23,11 +24,17 @@ public OutputSettings(boolean codeWrapEnabled, String browserMode, boolean brows } public OutputSettings(boolean codeWrapEnabled, String browserMode, boolean browserJavaScriptEnabled, boolean allowAnyHttp, boolean bypassPathProtection) { + this(codeWrapEnabled, browserMode, browserJavaScriptEnabled, allowAnyHttp, bypassPathProtection, false); + } + + public OutputSettings(boolean codeWrapEnabled, String browserMode, boolean browserJavaScriptEnabled, + boolean allowAnyHttp, boolean bypassPathProtection, boolean processAutoExpandEnabled) { this.codeWrapEnabled = codeWrapEnabled; this.browserMode = normalizeBrowserMode(browserMode); this.browserJavaScriptEnabled = browserJavaScriptEnabled; this.allowAnyHttp = allowAnyHttp; this.bypassPathProtection = bypassPathProtection; + this.processAutoExpandEnabled = processAutoExpandEnabled; } public boolean isCodeWrapEnabled() { @@ -50,6 +57,10 @@ public boolean isBypassPathProtection() { return bypassPathProtection; } + public boolean isProcessAutoExpandEnabled() { + return processAutoExpandEnabled; + } + public static String normalizeBrowserMode(String mode) { return BROWSER_EXTERNAL.equals(mode) ? BROWSER_EXTERNAL : BROWSER_BUILTIN; } diff --git a/data/src/main/java/cn/lineai/data/repository/DiffRecord.java b/data/src/main/java/cn/lineai/data/repository/DiffRecord.java index bc9c63cf..19e52536 100644 --- a/data/src/main/java/cn/lineai/data/repository/DiffRecord.java +++ b/data/src/main/java/cn/lineai/data/repository/DiffRecord.java @@ -8,6 +8,8 @@ public final class DiffRecord { private final boolean oldExists; private final long timestamp; private final boolean reverted; + private final String reviewState; + private final String reviewMessage; public DiffRecord( String id, @@ -17,6 +19,20 @@ public DiffRecord( boolean oldExists, long timestamp, boolean reverted + ) { + this(id, filePath, oldContent, newContent, oldExists, timestamp, reverted, "", ""); + } + + public DiffRecord( + String id, + String filePath, + String oldContent, + String newContent, + boolean oldExists, + long timestamp, + boolean reverted, + String reviewState, + String reviewMessage ) { this.id = id == null ? "" : id; this.filePath = filePath == null ? "" : filePath; @@ -25,6 +41,8 @@ public DiffRecord( this.oldExists = oldExists; this.timestamp = timestamp; this.reverted = reverted; + this.reviewState = reviewState == null ? "" : reviewState; + this.reviewMessage = reviewMessage == null ? "" : reviewMessage; } public String getId() { @@ -54,4 +72,12 @@ public long getTimestamp() { public boolean isReverted() { return reverted; } + + public String getReviewState() { + return reviewState; + } + + public String getReviewMessage() { + return reviewMessage; + } } diff --git a/data/src/main/java/cn/lineai/data/repository/DiffRepository.java b/data/src/main/java/cn/lineai/data/repository/DiffRepository.java index 183d916d..aaa33599 100644 --- a/data/src/main/java/cn/lineai/data/repository/DiffRepository.java +++ b/data/src/main/java/cn/lineai/data/repository/DiffRepository.java @@ -6,6 +6,7 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; +import org.json.JSONObject; public final class DiffRepository extends BaseRepository implements DiffStore { private final SecureRandom random = new SecureRandom(); @@ -92,7 +93,31 @@ public synchronized void markReverted(String diffId) { database.getWritableDatabase().update("diff_records", values, "id = ?", new String[] {diffId}); } + @Override + public synchronized void setReview(String diffId, String state, String message) { + if (diffId == null || diffId.length() == 0) { + return; + } + JSONObject local = new JSONObject(); + try { + local.put("review_state", state == null ? "" : state); + local.put("review_message", message == null ? "" : message); + } catch (Exception ignored) { + return; + } + ContentValues values = new ContentValues(); + values.put("raw_json", local.toString()); + database.getWritableDatabase().update("diff_records", values, "id = ?", new String[] {diffId}); + } + private DiffRecord readRecord(Cursor cursor) { + String rawJson = cursor.getString(cursor.getColumnIndexOrThrow("raw_json")); + JSONObject local; + try { + local = rawJson == null || rawJson.length() == 0 ? new JSONObject() : new JSONObject(rawJson); + } catch (Exception ignored) { + local = new JSONObject(); + } return new DiffRecord( cursor.getString(cursor.getColumnIndexOrThrow("id")), cursor.getString(cursor.getColumnIndexOrThrow("file_path")), @@ -100,7 +125,9 @@ private DiffRecord readRecord(Cursor cursor) { cursor.getString(cursor.getColumnIndexOrThrow("new_content")), cursor.getInt(cursor.getColumnIndexOrThrow("old_exists")) == 1, cursor.getLong(cursor.getColumnIndexOrThrow("timestamp")), - cursor.getInt(cursor.getColumnIndexOrThrow("reverted")) == 1 + cursor.getInt(cursor.getColumnIndexOrThrow("reverted")) == 1, + local.optString("review_state"), + local.optString("review_message") ); } diff --git a/data/src/main/java/cn/lineai/data/repository/DiffStore.java b/data/src/main/java/cn/lineai/data/repository/DiffStore.java index 406f37b6..2dd92316 100644 --- a/data/src/main/java/cn/lineai/data/repository/DiffStore.java +++ b/data/src/main/java/cn/lineai/data/repository/DiffStore.java @@ -15,4 +15,6 @@ public interface DiffStore { DiffRepository.RevertResult revertDiff(String diffId); void markReverted(String diffId); + + void setReview(String diffId, String state, String message); } diff --git a/data/src/main/java/cn/lineai/data/repository/OutputSettingsRepository.java b/data/src/main/java/cn/lineai/data/repository/OutputSettingsRepository.java index 780aafe3..922f5b33 100644 --- a/data/src/main/java/cn/lineai/data/repository/OutputSettingsRepository.java +++ b/data/src/main/java/cn/lineai/data/repository/OutputSettingsRepository.java @@ -8,6 +8,7 @@ public final class OutputSettingsRepository { public static final String KEY_BROWSER_JAVASCRIPT = "@lineai_browser_javascript"; public static final String KEY_ALLOW_ANY_HTTP = "@lineai_allow_any_http"; public static final String KEY_BYPASS_PATH_PROTECTION = "@lineai_bypass_path_protection"; + public static final String KEY_PROCESS_AUTO_EXPAND = "@lineai_process_auto_expand"; private final SettingsRepository settingsRepository; @@ -21,7 +22,8 @@ public synchronized OutputSettings get() { settingsRepository.getString(KEY_BROWSER_MODE, OutputSettings.BROWSER_BUILTIN), settingsRepository.getBoolean(KEY_BROWSER_JAVASCRIPT, false), settingsRepository.getBoolean(KEY_ALLOW_ANY_HTTP, false), - settingsRepository.getBoolean(KEY_BYPASS_PATH_PROTECTION, false) + settingsRepository.getBoolean(KEY_BYPASS_PATH_PROTECTION, false), + settingsRepository.getBoolean(KEY_PROCESS_AUTO_EXPAND, false) ); } @@ -29,6 +31,10 @@ public synchronized void setCodeWrapEnabled(boolean enabled) { settingsRepository.setBoolean(KEY_CODE_WRAP, enabled); } + public synchronized void setProcessAutoExpandEnabled(boolean enabled) { + settingsRepository.setBoolean(KEY_PROCESS_AUTO_EXPAND, enabled); + } + public synchronized void setBrowserMode(String mode) { settingsRepository.setString(KEY_BROWSER_MODE, OutputSettings.normalizeBrowserMode(mode)); } diff --git a/tool-ui/src/main/java/cn/lineai/tool/ui/view/ToolCallReadView.java b/tool-ui/src/main/java/cn/lineai/tool/ui/view/ToolCallReadView.java index bd489fe3..52b72d6e 100644 --- a/tool-ui/src/main/java/cn/lineai/tool/ui/view/ToolCallReadView.java +++ b/tool-ui/src/main/java/cn/lineai/tool/ui/view/ToolCallReadView.java @@ -46,6 +46,9 @@ public ToolCallReadView(Context context) { @Override public void setToolReviewListener(ToolReviewListener listener) {} @Override public void bind(ToolCall call, ToolResult result) { String name = call == null ? "" : call.getName(); + ToolInfoResolver resolver = ToolInfoResolverProvider.getDefault(); + int actionIcon = resolver == null ? 0 : resolver.getActionIcon(name); + icon.setIconType(actionIcon == 0 ? IconButtonView.FILE : actionIcon); String path = ToolCallUtils.displayInputLabel(getContext(), name, ToolCallUtils.parseInput(call), projectPath); int status = result == null || "running".equals(result.getReviewState()) ? R.string.tool_call_status_running : result.isError() ? R.string.tool_call_status_failed : R.string.tool_call_read_done; diff --git a/update.md b/update.md index c1d9a6a1..80d6853c 100644 --- a/update.md +++ b/update.md @@ -6,8 +6,8 @@ - **原生界面重构** - 重新整理聊天页、设置页、抽屉、底部弹窗和工具卡片的布局,统一页面底色、卡片层级、圆角、字体与图标比例;继续使用 Android 原生 View,减少重复边框、过大的标题和占用空间过多的装饰组件 - **聊天导航恢复与调整** - 左上角保留三条杠菜单,点击工作区标题打开工作区选择抽屉;恢复权限选择器与三点菜单,新对话使用加号图标,附件加号继续用于选择文件,设置与会话导航保留在抽屉中 -- **用户消息与输入框** - 用户消息改为右对齐气泡,区别于助手正文和过程说明;收紧输入框默认高度、内部留白与底部操作区,调整正文到屏幕两侧的距离 -- **统一页头操作尺寸** - `ScreenHeaderView` 统一返回及图标操作按钮的 48dp 点击区域与 22dp 图标尺寸,修复错误日志、添加模型等页面右上角按钮过大,以及保存操作贴到屏幕最右侧的问题 +- **用户消息与输入框** - 用户消息改为右对齐气泡,区别于助手正文和过程说明;收紧输入框默认高度、内部留白与底部操作区,并将气泡左右外边距收紧到 16dp +- **统一页头操作尺寸** - `ScreenHeaderView` 统一返回及图标操作按钮的 36dp 操作区域与 22dp 图标尺寸,修复错误日志、添加模型等页面右上角按钮过大,以及保存操作贴到屏幕最右侧的问题 - **自适应页面与弹窗** - 新增 `ScreenSurfaceView`、`InsetSheetLayout`、`AdaptiveActionsView` 等共享布局组件;宽屏限制内容宽度,底部弹窗限制最大高度,长内容可以滚动,窄屏操作按钮可以重新排列 - **Markdown 排版调整** - 调整正文、标题、代码块的字号和间距,代码区域使用更紧凑的显示方式;保留链接、复制、代码换行等阅读操作 @@ -29,6 +29,7 @@ - **编辑与创建文件展示** - 重写 `ToolCallWriteView`、`DiffView`,按文件名、增删统计和代码差异组织内容;新增 `DiffLines` 处理差异行,新增与删除使用独立底色、行号及增删标记,修正文件列表缩进和差异内容的对齐 - **文件变更汇总** - 在答复后汇总编辑过的文件及增删数量,保留审阅、撤销和展开文件列表的入口;过程中的调用详情与末尾文件汇总分别维护展开状态 - **错误信息内联** - Shell、读取、写入、删除、Todo 等工具错误放入对应调用结果内部,展开后可查看完整错误文本,不在调用卡片外重复显示错误横幅 +- **网页工具图标区分** - 网页搜索使用放大镜图标,查看网页使用圆形地球图标;读取类工具卡片读取工具自身的动作图标,不再统一显示文件图标 - **工具错误日志补齐** - `ToolExecutor` 对未知工具、工具返回失败以及执行异常记录错误日志;`ErrorLogRepository` 使用独立临时文件名避免同一时间写入互相覆盖,并同步保护时间格式化器,保留已有日志脱敏 ### 执行授权与权限模式 @@ -37,6 +38,7 @@ - **永久允许精确匹配** - 新增 `CommandPermissionRepository`,按执行范围、工具名、完整 Shell 命令与工作目录生成授权键并持久化;相同命令在相同范围内可复用授权,命令或目录变化后不会误用旧授权 - **自动权限生效修复** - 自动模式下,已启用工具直接执行,主流程和 Agent 内部执行都不再重复弹出确认;确认模式继续按需申请,只读限制与工具启用状态仍然生效 - **权限提示词同步** - 工具提示词明确区分自动与确认模式,要求模型直接提交工具调用,由应用决定是否弹出申请;等待授权期间切换执行范围时拒绝旧范围的申请,避免跨工作区复用待处理授权 +- **确认状态与对话解耦** - 文件改动的接受、撤销及错误说明写入本地 Diff 记录,仅在 UI 渲染副本中叠加,不再改写或重新保存模型对话;执行前确认不再插入空的 `pending/accepted` 临时结果,避免授权操作中断当前工具循环并保持请求缓存前缀稳定 ### 设置、主题与抽屉细节 @@ -46,6 +48,7 @@ - **工作区与目录抽屉** - 工作区选中背景铺满抽屉内容宽度,保留工作区选择与长按删除;目录选择确认操作置于面板内,限制窄屏和横屏下的面板尺寸 - **工具与执行页面** - 收紧页面左右边距,强化执行目标的选中状态;保留工具分组开关,删除用于展示内部工具 ID 的「工具设置」折叠项 - **存储管理显示修复** - 为刷新图标补充尺寸测量,修复无尺寸约束的按钮挤掉页面内容、只剩中央按钮的问题;恢复总占用、Diff 缓存、聊天记录、配置文件和 Home 目录的统计布局,调整卡片背景与间距 +- **处理区域显示偏好** - 在显示设置新增「自动展开处理」开关;开启后新出现的处理区域自动展开,用户手动收起后不会被后续流式刷新再次打开 - **Skills 商店布局** - 商店标题移动到返回箭头右侧,发现社区 Skills、搜索框及后续内容统一使用 16dp 左右边距;扩展主页各入口之间增加 12dp 间隔 ### Skill Hub 技能商店集成 From 6696868d63fbe61ebd6bd1e8626f6aa75f410751 Mon Sep 17 00:00:00 2001 From: LangLang03 Date: Sat, 5 Sep 2026 22:51:23 +0800 Subject: [PATCH 3/3] fix: align MCP cards and GLM reasoning --- .../component/SecondaryScreenLayoutTest.java | 9 +- .../main/java/cn/lineai/ai/ModelClient.java | 3 +- .../cn/lineai/ai/ReasoningCompatibility.java | 55 ++++++ .../ai/protocol/OpenAiCompatibleProtocol.java | 6 +- .../reasoning/MoonshotReasoningStrategy.java | 6 +- .../context/ContextCompactionService.java | 10 +- .../lineai/ai/ReasoningCompatibilityTest.java | 55 ++++++ .../OpenAiCompatibleProtocolTest.java | 16 ++ .../context/ContextCompactionServiceTest.java | 13 ++ .../tool/ui/view/ToolCallGenericView.java | 184 +++++++++++++----- update.md | 3 +- 11 files changed, 308 insertions(+), 52 deletions(-) create mode 100644 feature-model/src/main/java/cn/lineai/ai/ReasoningCompatibility.java create mode 100644 feature-model/src/test/java/cn/lineai/ai/ReasoningCompatibilityTest.java diff --git a/app/src/test/java/cn/lineai/ui/component/SecondaryScreenLayoutTest.java b/app/src/test/java/cn/lineai/ui/component/SecondaryScreenLayoutTest.java index b8c4c24b..cb802827 100644 --- a/app/src/test/java/cn/lineai/ui/component/SecondaryScreenLayoutTest.java +++ b/app/src/test/java/cn/lineai/ui/component/SecondaryScreenLayoutTest.java @@ -157,7 +157,14 @@ private void layout(View view,int width,int height) { cn.lineai.model.tool.ToolResult result=cn.lineai.model.tool.ToolResult.of("generic","mcp_example","RESULT CONTENT",false); cn.lineai.tool.ui.ToolCallGenericView generic=new cn.lineai.tool.ui.ToolCallGenericView(activity,"MCP"); generic.setExpansionState(state,"generic");generic.bind(call,result);assertNull(text(generic,"RESULT CONTENT")); - generic.getChildAt(0).performClick();assertNotNull(text(generic,"RESULT CONTENT")); + View genericHeader=generic.getChildAt(0);assertNull(genericHeader.getBackground()); + assertEquals(48,genericHeader.getMinimumHeight()); + genericHeader.performClick();assertNotNull(text(generic,"RESULT CONTENT")); + cn.lineai.ui.theme.BoundedScrollView genericDetail=find(generic,cn.lineai.ui.theme.BoundedScrollView.class); + assertNotNull(genericDetail);assertNotNull(genericDetail.getBackground()); + activity.setContentView(generic);layout(generic,390,240);screenshot(generic,"native-light-390-mcp-card"); + cn.lineai.tool.ui.ToolCallGenericView localGeneric=new cn.lineai.tool.ui.ToolCallGenericView(activity,"MCP"); + localGeneric.bind(call,result);localGeneric.getChildAt(0).performClick();assertNotNull(text(localGeneric,"RESULT CONTENT")); cn.lineai.tool.ui.ToolCallGenericView rebound=new cn.lineai.tool.ui.ToolCallGenericView(activity,"MCP"); rebound.setExpansionState(state,"generic");rebound.bind(call,result);assertNotNull(text(rebound,"RESULT CONTENT")); cn.lineai.tool.ui.ToolCallAgentView agent=new cn.lineai.tool.ui.ToolCallAgentView(activity);agent.bind(call,result); diff --git a/feature-model/src/main/java/cn/lineai/ai/ModelClient.java b/feature-model/src/main/java/cn/lineai/ai/ModelClient.java index 126145be..1b49a708 100644 --- a/feature-model/src/main/java/cn/lineai/ai/ModelClient.java +++ b/feature-model/src/main/java/cn/lineai/ai/ModelClient.java @@ -31,6 +31,7 @@ public ModelCompletionResponse stream( ModelRequestOptions options ) throws ModelCompletionException { ModelProtocol protocol = protocolFactory.create(config.getProtocolType()); - return protocol.stream(config, messages, callback, cancellationToken, options == null ? ModelRequestOptions.defaults() : options); + ModelRequestOptions compatibleOptions = ReasoningCompatibility.adapt(config, options); + return protocol.stream(config, messages, callback, cancellationToken, compatibleOptions); } } diff --git a/feature-model/src/main/java/cn/lineai/ai/ReasoningCompatibility.java b/feature-model/src/main/java/cn/lineai/ai/ReasoningCompatibility.java new file mode 100644 index 00000000..ff119859 --- /dev/null +++ b/feature-model/src/main/java/cn/lineai/ai/ReasoningCompatibility.java @@ -0,0 +1,55 @@ +package cn.lineai.ai; + +import cn.lineai.model.AiBehaviorSettings; +import cn.lineai.model.ModelConfig; +import cn.lineai.model.ModelContextParser; +import java.util.Locale; + +/** Maps the app's shared reasoning levels to the subset accepted by each provider. */ +public final class ReasoningCompatibility { + private ReasoningCompatibility() { + } + + public static ModelRequestOptions adapt(ModelConfig config, ModelRequestOptions options) { + ModelRequestOptions source = options == null ? ModelRequestOptions.defaults() : options; + String effort = compatibleEffort(config, source.getReasoningEffort()); + if (effort.equals(source.getReasoningEffort())) { + return source; + } + return new ModelRequestOptions(effort, source.isPreserveReasoning(), source.getTools()); + } + + public static String compatibleEffort(ModelConfig config, String requestedEffort) { + String effort = AiBehaviorSettings.normalizeReasoningEffort(requestedEffort); + if (!isGlm(config)) { + return effort; + } + if (AiBehaviorSettings.REASONING_MAX.equals(effort)) { + return AiBehaviorSettings.REASONING_MAX; + } + if (AiBehaviorSettings.REASONING_OFF.equals(effort) + || AiBehaviorSettings.REASONING_LOW.equals(effort)) { + return AiBehaviorSettings.REASONING_LOW; + } + return AiBehaviorSettings.REASONING_HIGH; + } + + public static boolean isGlm(ModelConfig config) { + if (config == null) { + return false; + } + String baseUrl = lower(config.getBaseUrl()); + String provider = lower(config.getProviderLabel()); + String model = lower(ModelContextParser.apiModelId(config)); + return baseUrl.contains("bigmodel") + || baseUrl.contains("zhipu") + || provider.contains("zhipu") + || provider.contains("glm") + || provider.contains("智谱") + || model.contains("glm"); + } + + private static String lower(String value) { + return value == null ? "" : value.toLowerCase(Locale.ROOT); + } +} diff --git a/feature-model/src/main/java/cn/lineai/ai/protocol/OpenAiCompatibleProtocol.java b/feature-model/src/main/java/cn/lineai/ai/protocol/OpenAiCompatibleProtocol.java index e1e0f5b0..a14462ce 100644 --- a/feature-model/src/main/java/cn/lineai/ai/protocol/OpenAiCompatibleProtocol.java +++ b/feature-model/src/main/java/cn/lineai/ai/protocol/OpenAiCompatibleProtocol.java @@ -5,6 +5,7 @@ import cn.lineai.ai.ModelCompletionResponse; import cn.lineai.ai.ModelCancellationToken; import cn.lineai.ai.ModelRequestOptions; +import cn.lineai.ai.ReasoningCompatibility; import cn.lineai.ai.ModelStreamCallback; import cn.lineai.ai.message.ModelMessage; import cn.lineai.model.AiBehaviorSettings; @@ -271,11 +272,12 @@ private void applyReasoningRequest(ModelConfig config, JSONObject body, ModelReq } String base = config.getBaseUrl().toLowerCase(java.util.Locale.ROOT); String model = ModelContextParser.apiModelId(config).toLowerCase(java.util.Locale.ROOT); - String effort = options.getReasoningEffort(); + ModelRequestOptions compatibleOptions = ReasoningCompatibility.adapt(config, options); + String effort = compatibleOptions.getReasoningEffort(); boolean enabled = AiBehaviorSettings.isReasoningEnabled(effort); String concrete = AiBehaviorSettings.concreteReasoningEffort(effort); ReasoningRequestContext context = new ReasoningRequestContext( - enabled, concrete, options.isPreserveReasoning(), base, model, thinkingBudget(concrete)); + enabled, concrete, compatibleOptions.isPreserveReasoning(), base, model, thinkingBudget(concrete)); ReasoningRequestStrategy strategy = reasoningStrategyRegistry.find(base, model); if (strategy != null) { strategy.apply(body, context); diff --git a/feature-model/src/main/java/cn/lineai/ai/protocol/reasoning/MoonshotReasoningStrategy.java b/feature-model/src/main/java/cn/lineai/ai/protocol/reasoning/MoonshotReasoningStrategy.java index ab1eeb0b..d2106bce 100644 --- a/feature-model/src/main/java/cn/lineai/ai/protocol/reasoning/MoonshotReasoningStrategy.java +++ b/feature-model/src/main/java/cn/lineai/ai/protocol/reasoning/MoonshotReasoningStrategy.java @@ -16,8 +16,12 @@ public void apply(JSONObject body, ReasoningRequestContext context) throws Excep JSONObject thinking = new JSONObject().put("type", context.isEnabled() ? "enabled" : "disabled"); String base = context.getBaseUrl(); String model = context.getModelId(); + boolean glm = base.contains("bigmodel") || base.contains("zhipu") || model.contains("glm"); body.put("thinking", thinking); - if (context.isPreserveReasoning() && (base.contains("bigmodel") || base.contains("zhipu") || model.contains("glm"))) { + if (glm) { + body.put("reasoning_effort", context.getEffort()); + } + if (context.isPreserveReasoning() && glm) { body.put("clear_thinking", false); } } diff --git a/feature-model/src/main/java/cn/lineai/context/ContextCompactionService.java b/feature-model/src/main/java/cn/lineai/context/ContextCompactionService.java index ccd7183f..2eaf8027 100644 --- a/feature-model/src/main/java/cn/lineai/context/ContextCompactionService.java +++ b/feature-model/src/main/java/cn/lineai/context/ContextCompactionService.java @@ -7,6 +7,7 @@ import cn.lineai.ai.ModelCompletionException; import cn.lineai.ai.ModelCompletionResponse; import cn.lineai.ai.ModelRequestOptions; +import cn.lineai.ai.ReasoningCompatibility; import cn.lineai.ai.message.AssistantModelMessage; import cn.lineai.ai.message.ModelMessage; import cn.lineai.ai.message.SystemModelMessage; @@ -275,7 +276,7 @@ private ContextCompactionResult compactWithSummary( request, null, cancellationToken, - new ModelRequestOptions(AiBehaviorSettings.REASONING_OFF, false, new ArrayList()) + compactionRequestOptions(selectedModel) ), cancellationToken ); @@ -299,7 +300,7 @@ private ContextCompactionResult compactWithOpenAiResponsesSummary( request, null, cancellationToken, - new ModelRequestOptions(AiBehaviorSettings.REASONING_OFF, false, new ArrayList()) + compactionRequestOptions(selectedModel.withModelId(selectedModel.getEffectiveCompressionModelId())) ), cancellationToken ); @@ -337,6 +338,11 @@ private ModelCompletionResponse streamSummaryWithRetry( } } + static ModelRequestOptions compactionRequestOptions(ModelConfig model) { + return ReasoningCompatibility.adapt(model, + new ModelRequestOptions(AiBehaviorSettings.REASONING_OFF, false, new ArrayList())); + } + private String compactResponsesItemWithRetry( ModelConfig selectedModel, List input, diff --git a/feature-model/src/test/java/cn/lineai/ai/ReasoningCompatibilityTest.java b/feature-model/src/test/java/cn/lineai/ai/ReasoningCompatibilityTest.java new file mode 100644 index 00000000..f07dbe3c --- /dev/null +++ b/feature-model/src/test/java/cn/lineai/ai/ReasoningCompatibilityTest.java @@ -0,0 +1,55 @@ +package cn.lineai.ai; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import cn.lineai.model.AiBehaviorSettings; +import cn.lineai.model.ModelConfig; +import cn.lineai.model.ModelProtocolType; +import java.util.Collections; +import org.junit.Test; + +public final class ReasoningCompatibilityTest { + private final ModelConfig glm = ModelConfig.builder( + "glm", "GLM", ModelProtocolType.OPENAI_COMPATIBLE, "Zhipu", + "https://open.bigmodel.cn/api/paas/v4", "key", "glm-5.2").build(); + + @Test + public void glmUsesOnlyLowHighAndMax() { + assertEquals(AiBehaviorSettings.REASONING_LOW, + ReasoningCompatibility.compatibleEffort(glm, AiBehaviorSettings.REASONING_OFF)); + assertEquals(AiBehaviorSettings.REASONING_LOW, + ReasoningCompatibility.compatibleEffort(glm, AiBehaviorSettings.REASONING_LOW)); + assertEquals(AiBehaviorSettings.REASONING_HIGH, + ReasoningCompatibility.compatibleEffort(glm, AiBehaviorSettings.REASONING_AUTO)); + assertEquals(AiBehaviorSettings.REASONING_HIGH, + ReasoningCompatibility.compatibleEffort(glm, AiBehaviorSettings.REASONING_MEDIUM)); + assertEquals(AiBehaviorSettings.REASONING_HIGH, + ReasoningCompatibility.compatibleEffort(glm, AiBehaviorSettings.REASONING_HIGH)); + assertEquals(AiBehaviorSettings.REASONING_MAX, + ReasoningCompatibility.compatibleEffort(glm, AiBehaviorSettings.REASONING_MAX)); + } + + @Test + public void adaptationPreservesReasoningHistoryPreferenceAndEmptyTools() { + ModelRequestOptions adapted = ReasoningCompatibility.adapt(glm, + new ModelRequestOptions(AiBehaviorSettings.REASONING_MEDIUM, true, + Collections.emptyList())); + + assertEquals(AiBehaviorSettings.REASONING_HIGH, adapted.getReasoningEffort()); + assertTrue(adapted.isPreserveReasoning()); + assertTrue(adapted.getTools().isEmpty()); + } + + @Test + public void nonGlmModelsKeepTheRequestedEffort() { + ModelConfig model = ModelConfig.builder( + "gpt", "GPT", ModelProtocolType.OPENAI_COMPATIBLE, "OpenAI", + "https://api.openai.com/v1", "key", "gpt-5.6-sol").build(); + + assertEquals(AiBehaviorSettings.REASONING_OFF, + ReasoningCompatibility.compatibleEffort(model, AiBehaviorSettings.REASONING_OFF)); + assertEquals(AiBehaviorSettings.REASONING_MEDIUM, + ReasoningCompatibility.compatibleEffort(model, AiBehaviorSettings.REASONING_MEDIUM)); + } +} diff --git a/feature-model/src/test/java/cn/lineai/ai/protocol/OpenAiCompatibleProtocolTest.java b/feature-model/src/test/java/cn/lineai/ai/protocol/OpenAiCompatibleProtocolTest.java index 6b6a3977..b9751526 100644 --- a/feature-model/src/test/java/cn/lineai/ai/protocol/OpenAiCompatibleProtocolTest.java +++ b/feature-model/src/test/java/cn/lineai/ai/protocol/OpenAiCompatibleProtocolTest.java @@ -300,12 +300,28 @@ public void glmConfigStillHandledByMoonshotStrategy() throws Exception { ); assertTrue("enabled".equals(body.getJSONObject("thinking").getString("type"))); + assertEquals("high", body.getString("reasoning_effort")); assertTrue(body.has("clear_thinking")); assertFalse(body.getBoolean("clear_thinking")); assertFalse(new KimiReasoningStrategy().matches(baseUrl, modelId)); assertTrue(new MoonshotReasoningStrategy().matches(baseUrl, modelId)); } + @Test + public void glmOffIsAdaptedToEnabledLowReasoning() throws Exception { + ModelConfig config = ModelConfig.builder( + "glm-5.2", "GLM-5.2", ModelProtocolType.OPENAI_COMPATIBLE, "Zhipu", + "https://open.bigmodel.cn/api/paas/v4", "sk-test", "glm-5.2").build(); + + JSONObject body = new OpenAiCompatibleProtocol().reasoningRequestBodyForTest( + config, + new ModelRequestOptions(AiBehaviorSettings.REASONING_OFF, false) + ); + + assertEquals("enabled", body.getJSONObject("thinking").getString("type")); + assertEquals("low", body.getString("reasoning_effort")); + } + @Test public void deepseekTemperatureRemainsUnclamped() throws Exception { ModelConfig config = ModelConfig.builder( diff --git a/feature-model/src/test/java/cn/lineai/context/ContextCompactionServiceTest.java b/feature-model/src/test/java/cn/lineai/context/ContextCompactionServiceTest.java index fb64f291..70f7d94c 100644 --- a/feature-model/src/test/java/cn/lineai/context/ContextCompactionServiceTest.java +++ b/feature-model/src/test/java/cn/lineai/context/ContextCompactionServiceTest.java @@ -5,6 +5,9 @@ import static org.junit.Assert.assertTrue; import cn.lineai.model.ChatMessage; +import cn.lineai.model.AiBehaviorSettings; +import cn.lineai.model.ModelConfig; +import cn.lineai.model.ModelProtocolType; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -93,6 +96,16 @@ public void selectRecentUserMessagesSkipsExcludedHiddenAndCompactBlocks() { assertEquals("real", selected.get(0).getId()); } + @Test + public void glmCompactionUsesCompatibleLowReasoningInsteadOfOff() { + ModelConfig glm = ModelConfig.builder( + "glm", "GLM", ModelProtocolType.OPENAI_COMPATIBLE, "Zhipu", + "https://open.bigmodel.cn/api/paas/v4", "key", "glm-5.2").build(); + + assertEquals(AiBehaviorSettings.REASONING_LOW, + ContextCompactionService.compactionRequestOptions(glm).getReasoningEffort()); + } + private static String repeat(String value, int count) { StringBuilder builder = new StringBuilder(value.length() * count); for (int i = 0; i < count; i++) { diff --git a/tool-ui/src/main/java/cn/lineai/tool/ui/view/ToolCallGenericView.java b/tool-ui/src/main/java/cn/lineai/tool/ui/view/ToolCallGenericView.java index 674d7051..8cc3f95d 100644 --- a/tool-ui/src/main/java/cn/lineai/tool/ui/view/ToolCallGenericView.java +++ b/tool-ui/src/main/java/cn/lineai/tool/ui/view/ToolCallGenericView.java @@ -1,8 +1,12 @@ package cn.lineai.tool.ui; + import android.content.Context; import android.graphics.Typeface; +import android.text.TextUtils; import android.view.Gravity; +import android.view.View; import android.widget.LinearLayout; +import android.widget.ScrollView; import android.widget.TextView; import cn.lineai.model.tool.ToolCall; import cn.lineai.model.tool.ToolResult; @@ -13,61 +17,153 @@ import cn.lineai.ui.theme.LineTheme; import java.util.Map; -/** Unknown and extension tools share the same manual disclosure as built-in tools. */ +/** Unknown and MCP extension tools use the same disclosure rhythm as built-in tools. */ public final class ToolCallGenericView extends BaseToolCallView implements ToolCallCardView, ToolCallExpansion { - private final String label; - private Map expansion; + private final String fallbackLabel; + private Map expansion; private String key = ""; - private boolean open; + private boolean localExpanded; private ToolCall call; private ToolResult result; + public ToolCallGenericView(Context context, String label) { - super(context); this.label = label == null ? "" : label; + super(context); + fallbackLabel = label == null || label.trim().isEmpty() + ? context.getString(R.string.tool_call_generic_mcp) + : label; } - @Override public void setExpansionState(Map state, String key) { - expansion = state; this.key = key; open = state != null && Boolean.TRUE.equals(state.get(key)); + + @Override + public void setExpansionState(Map state, String key) { + expansion = state; + this.key = key == null ? "" : key; } - @Override public void bind(ToolCall call, ToolResult result) { - this.call = call; this.result = result; removeAllViews(); + + @Override + public void bind(ToolCall call, ToolResult result) { + this.call = call; + this.result = result; + removeAllViews(); + + boolean open = isExpanded(); boolean error = result != null && result.isError(); - LinearLayout header = new LinearLayout(getContext()); header.setGravity(Gravity.CENTER_VERTICAL); - header.setMinimumHeight(LineTheme.dp(getContext(),48)); - LineTheme.padding(header,0,12,0,12); header.setBackground(LineTheme.pressable(getContext())); - IconButtonView icon = new IconButtonView(getContext(),IconButtonView.MCP); - icon.setIconColor(error ? LineTheme.DANGER : LineTheme.TEXT_SECONDARY); icon.setIconSizeDp(16,16); icon.setClickable(false); - header.addView(icon,new LayoutParams(LineTheme.dp(getContext(),16),LineTheme.dp(getContext(),16))); - String name = call == null ? label : call.getName(); - int status = error ? R.string.tool_call_status_failed : isTerminal(result) ? R.string.tool_call_status_done : R.string.tool_call_status_running; - TextView title = LineTheme.text(getContext(), getContext().getString(status) + " " + name,14,error ? LineTheme.DANGER : LineTheme.TEXT_SECONDARY,Typeface.NORMAL); - title.setMaxLines(2); title.setEllipsize(android.text.TextUtils.TruncateAt.END); - LayoutParams tp = new LayoutParams(0,-2,1); tp.leftMargin=LineTheme.dp(getContext(),10);header.addView(title,tp); - IconButtonView arrow = new IconButtonView(getContext(),open ? IconButtonView.CHEVRON_DOWN : IconButtonView.CHEVRON_RIGHT); - arrow.setIconSizeDp(24,14); arrow.setIconColor(LineTheme.TEXT_SECONDARY); arrow.setClickable(false); - header.addView(arrow,new LayoutParams(LineTheme.dp(getContext(),24),LineTheme.dp(getContext(),24))); - header.setOnClickListener(v -> {open=!open;if(expansion!=null)expansion.put(key,open);bind(this.call,this.result);}); - addView(header,new LayoutParams(-1,-2)); - if (!open) return; - LinearLayout content = new LinearLayout(getContext());content.setOrientation(VERTICAL); - content.setBackground(LineTheme.rounded(getContext(),LineTheme.INPUT_BG,12));LineTheme.padding(content,16,16,16,16); + int color = error ? LineTheme.DANGER : LineTheme.TEXT_SECONDARY; + + LinearLayout header = new LinearLayout(getContext()); + header.setGravity(Gravity.CENTER_VERTICAL); + header.setMinimumHeight(LineTheme.dp(getContext(), 48)); + + IconButtonView icon = new IconButtonView(getContext(), IconButtonView.MCP); + icon.setIconColor(color); + icon.setIconSizeDp(24, 16); + icon.setClickable(false); + icon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + header.addView(icon, new LayoutParams( + LineTheme.dp(getContext(), 24), LineTheme.dp(getContext(), 32))); + + String name = call == null || call.getName().trim().isEmpty() ? fallbackLabel : call.getName(); + int status = error ? R.string.tool_call_status_failed + : isTerminal(result) ? R.string.tool_call_status_done : R.string.tool_call_status_running; + TextView title = LineTheme.text(getContext(), + getContext().getString(status) + " " + name, + 14, color, Typeface.NORMAL); + title.setSingleLine(true); + title.setEllipsize(TextUtils.TruncateAt.END); + LayoutParams titleParams = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f); + titleParams.leftMargin = LineTheme.dp(getContext(), 6); + header.addView(title, titleParams); + + IconButtonView arrow = new IconButtonView(getContext(), + open ? IconButtonView.CHEVRON_DOWN : IconButtonView.CHEVRON_RIGHT); + arrow.setIconSizeDp(24, 14); + arrow.setIconColor(LineTheme.TEXT_SECONDARY); + arrow.setClickable(false); + arrow.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + header.addView(arrow, new LayoutParams( + LineTheme.dp(getContext(), 24), LineTheme.dp(getContext(), 32))); + header.setFocusable(true); + header.setOnClickListener(v -> { + if (expansion != null) { + expansion.put(this.key, !isExpanded()); + } else { + localExpanded = !localExpanded; + } + bind(this.call, this.result); + }); + addView(header, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + + if (open) { + addDetails(error); + } + } + + private void addDetails(boolean error) { + LinearLayout content = new LinearLayout(getContext()); + content.setOrientation(VERTICAL); + String input = ToolCallUtils.prettyJson(ToolCallUtils.parseInput(call)); - if (!"{}".equals(input)) section(content,R.string.tool_call_input,input,LineTheme.TEXT_SECONDARY); + boolean hasInput = !"{}".equals(input); + if (hasInput) { + addSection(content, R.string.tool_call_input, input, LineTheme.TEXT_SECONDARY, false); + } if (result != null && !result.getContent().isEmpty()) { - String raw=result.getContent(); - String output=AgentToolResultDisplay.progressPayload(raw)!=null ? AgentToolResultDisplay.displayOutput(raw) : raw; - section(content,R.string.tool_call_output,output,error?LineTheme.DANGER:LineTheme.TEXT); + String raw = result.getContent(); + String output = AgentToolResultDisplay.progressPayload(raw) != null + ? AgentToolResultDisplay.displayOutput(raw) : raw; + addSection(content, R.string.tool_call_output, output, + error ? LineTheme.DANGER : LineTheme.TEXT_SECONDARY, hasInput); } - BoundedScrollView scroll = new BoundedScrollView(getContext(),280);scroll.addView(content,new android.widget.ScrollView.LayoutParams(-1,-2)); - addView(scroll,new LayoutParams(-1,-2)); + + BoundedScrollView detail = new BoundedScrollView(getContext(), 240); + detail.setFillViewport(false); + detail.setBackground(LineTheme.roundedStroke( + getContext(), LineTheme.CODE_BG, 12, LineTheme.CODE_BORDER)); + detail.addView(content, new ScrollView.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + addView(detail, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + } + + private void addSection(LinearLayout parent, int titleRes, String value, int color, boolean dividerAbove) { + if (dividerAbove) { + View divider = new View(getContext()); + divider.setBackgroundColor(LineTheme.CODE_BORDER); + parent.addView(divider, new LayoutParams(LayoutParams.MATCH_PARENT, 1)); + } + LinearLayout section = new LinearLayout(getContext()); + section.setOrientation(VERTICAL); + LineTheme.padding(section, 14, 12, 14, 12); + + TextView heading = LineTheme.text(getContext(), getContext().getString(titleRes), + LineTheme.FONT_XS, LineTheme.TEXT_TERTIARY, Typeface.BOLD); + section.addView(heading, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + + String preview = value == null ? "" : value.length() > 65536 + ? value.substring(0, 65536) + "…" : value; + TextView body = LineTheme.text(getContext(), preview, + LineTheme.FONT_SM, color, Typeface.NORMAL); + body.setTypeface(Typeface.MONOSPACE); + body.setTextIsSelectable(true); + body.setLineSpacing(LineTheme.dp(getContext(), 4), 1f); + LayoutParams bodyParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + bodyParams.topMargin = LineTheme.dp(getContext(), 4); + section.addView(body, bodyParams); + parent.addView(section, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); } - private void section(LinearLayout parent,int title,String value,int color) { - TextView heading = LineTheme.text(getContext(),getContext().getString(title),13,LineTheme.TEXT_SECONDARY,Typeface.NORMAL); - LineTheme.padding(heading,0,8,0,8); parent.addView(heading); - String preview=value==null?"":value.length()>65536?value.substring(0,65536)+"…":value; - TextView body=LineTheme.text(getContext(),preview,13,color,Typeface.NORMAL); - body.setTypeface(Typeface.MONOSPACE);body.setTextIsSelectable(true);body.setLineSpacing(LineTheme.dp(getContext(),6),1); - parent.addView(body,new LayoutParams(-1,-2)); + + private boolean isExpanded() { + return expansion == null ? localExpanded : Boolean.TRUE.equals(expansion.get(key)); + } + + @Override + public void updateContent(ToolCall call, ToolResult result) { + bind(call, result); + } + + @Override + public void setToolReviewListener(ToolReviewListener listener) { + } + + @Override + public void setProjectPath(String path) { } - @Override public void updateContent(ToolCall call,ToolResult result) {bind(call,result);} - @Override public void setToolReviewListener(ToolReviewListener listener) { } - @Override public void setProjectPath(String path) { } } diff --git a/update.md b/update.md index 80d6853c..cfed68c2 100644 --- a/update.md +++ b/update.md @@ -25,7 +25,7 @@ ### 工具调用与文件差异展示 - **读取工具简化** - 文件读取成功时使用轻量结果行,不再提供点击展开原始读取内容的入口;失败结果仍在调用内部提供错误详情 -- **Shell 与通用工具卡片** - 调整执行命令、状态和输出的层级,默认以紧凑摘要展示;展开后查看命令结果或 MCP 返回内容,长输出使用有界滚动容器,避免单次调用撑满聊天页面 +- **Shell 与通用工具卡片** - 调整执行命令、状态和输出的层级,MCP 摘要行与读取、Shell 工具统一为 48dp 高度、相同图标比例及无背景样式;展开后使用带边框的代码结果面板查看输入和返回内容,长输出使用有界滚动容器,避免单次调用撑满聊天页面 - **编辑与创建文件展示** - 重写 `ToolCallWriteView`、`DiffView`,按文件名、增删统计和代码差异组织内容;新增 `DiffLines` 处理差异行,新增与删除使用独立底色、行号及增删标记,修正文件列表缩进和差异内容的对齐 - **文件变更汇总** - 在答复后汇总编辑过的文件及增删数量,保留审阅、撤销和展开文件列表的入口;过程中的调用详情与末尾文件汇总分别维护展开状态 - **错误信息内联** - Shell、读取、写入、删除、Todo 等工具错误放入对应调用结果内部,展开后可查看完整错误文本,不在调用卡片外重复显示错误横幅 @@ -83,6 +83,7 @@ - **压缩后重置用量基线** - 摘要生成请求使用的是压缩前完整对话,其输入 usage 不再作为压缩后的上下文占用;成功后重置 `TokenUsageTracker`,回退到排除旧消息后的本地估算,修复刚压缩完又立即进入压缩的循环 - **保留消息顺序与上下文身份** - `ContextCompactionController` 仅将被摘要替代的基础消息标记为 `excludeFromContext`,需要保留的用户消息维持原有位置,不再统一移到尾部;摘要继续进入模型上下文 +- **GLM 思考强度兼容层** - 请求发送前按模型能力转换统一思考档位:GLM 的关闭/低映射为 `low`,自动/中/高映射为 `high`,最大映射为 `max`;普通聊天、Agent 与上下文摘要请求共用映射,压缩流程固定使用的关闭档位也会转换为 GLM 可接受的 `low` ### 终端提供者与工程配置