Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@
"sessionCompletionNotificationModeNone": "Never",
"sessionCompletionNotificationModeBackgroundOnly": "Only when app is in background",
"sessionCompletionNotificationModeAlways": "Also when app is in foreground",
"settingsThinking": "Auto-expand thinking",
"thinkingInProgress": "Thinking…",
"thoughtForDuration": "Thought for {seconds}s",
"@thoughtForDuration": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"mainTabProjects": "PROJECTS",
"mainTabSettings": "SETTINGS",
"serverConfigConnectServer": "Connect Server",
Expand Down
18 changes: 18 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,24 @@ abstract class AppLocalizations {
/// **'Also when app is in foreground'**
String get sessionCompletionNotificationModeAlways;

/// No description provided for @settingsThinking.
///
/// In en, this message translates to:
/// **'Auto-expand thinking'**
String get settingsThinking;

/// No description provided for @thinkingInProgress.
///
/// In en, this message translates to:
/// **'Thinking…'**
String get thinkingInProgress;

/// No description provided for @thoughtForDuration.
///
/// In en, this message translates to:
/// **'Thought for {seconds}s'**
String thoughtForDuration(int seconds);

/// No description provided for @mainTabProjects.
///
/// In en, this message translates to:
Expand Down
11 changes: 11 additions & 0 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ class AppLocalizationsEn extends AppLocalizations {
String get sessionCompletionNotificationModeAlways =>
'Also when app is in foreground';

@override
String get settingsThinking => 'Auto-expand thinking';

@override
String get thinkingInProgress => 'Thinking…';

@override
String thoughtForDuration(int seconds) {
return 'Thought for ${seconds}s';
}

@override
String get mainTabProjects => 'PROJECTS';

Expand Down
11 changes: 11 additions & 0 deletions lib/l10n/app_localizations_zh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get sessionCompletionNotificationModeAlways => '应用在前台时也发送通知';

@override
String get settingsThinking => '自动展开思考';

@override
String get thinkingInProgress => '思考中…';

@override
String thoughtForDuration(int seconds) {
return '思考 $seconds 秒';
}

@override
String get mainTabProjects => '项目';

Expand Down
10 changes: 10 additions & 0 deletions lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@
"sessionCompletionNotificationModeNone": "不发送通知",
"sessionCompletionNotificationModeBackgroundOnly": "应用在后台时发送通知",
"sessionCompletionNotificationModeAlways": "应用在前台时也发送通知",
"settingsThinking": "自动展开思考",
"thinkingInProgress": "思考中…",
"thoughtForDuration": "思考 {seconds} 秒",
"@thoughtForDuration": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"mainTabProjects": "项目",
"mainTabSettings": "设置",
"serverConfigConnectServer": "连接服务器",
Expand Down
65 changes: 65 additions & 0 deletions lib/pages/settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import '../l10n/l10n.dart';
import '../providers/app_language_provider.dart';
import '../providers/session_completion_notification_provider.dart';
import '../providers/server_config_provider.dart';
import '../providers/thinking_auto_expand_provider.dart';
import '../theme/app_tokens.dart';

class SettingsPage extends ConsumerWidget {
Expand All @@ -21,6 +22,7 @@ class SettingsPage extends ConsumerWidget {
final notificationMode = ref.watch(
sessionCompletionNotificationModeProvider,
);
final thinkingAutoExpand = ref.watch(thinkingAutoExpandProvider);
final serverUrl = _serverDisplayText(
asyncServerConfig.value?.baseUrl ?? 'http://127.0.0.1:4096',
);
Expand Down Expand Up @@ -73,6 +75,17 @@ class SettingsPage extends ConsumerWidget {
context.push('/settings/theme');
},
),
_SettingsSwitchRow(
icon: Icons.psychology_outlined,
title: l10n.settingsThinking,
iconColor: mutedColor,
value: thinkingAutoExpand,
onChanged: (value) {
ref
.read(thinkingAutoExpandProvider.notifier)
.setAutoExpand(value);
},
),
_SettingsRow(
icon: Icons.notifications_outlined,
title: l10n.settingsSessionCompletionNotification,
Expand Down Expand Up @@ -131,6 +144,58 @@ class SettingsPage extends ConsumerWidget {
}
}

class _SettingsSwitchRow extends StatelessWidget {
const _SettingsSwitchRow({
required this.icon,
required this.title,
required this.iconColor,
required this.value,
required this.onChanged,
});

final IconData icon;
final String title;
final Color iconColor;
final bool value;
final ValueChanged<bool> onChanged;

@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final tokens = context.tokens;
final titleStyle = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
);

return SizedBox(
height: 56,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => onChanged(!value),
borderRadius: BorderRadius.circular(tokens.radiusXs),
splashColor: tokens.accent,
highlightColor: tokens.accent,
hoverColor: tokens.accent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
Icon(icon, size: 20, color: iconColor),
const SizedBox(width: 12),
Expanded(child: Text(title, style: titleStyle)),
Switch(value: value, onChanged: onChanged),
],
),
),
),
),
);
}
}

class _SectionTitle extends StatelessWidget {
const _SectionTitle({required this.title});

Expand Down
37 changes: 25 additions & 12 deletions lib/providers/session_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,31 @@ final class MessageListStateReducer {
final newParts = List<Object>.from(message.parts);
if (partIndex >= 0) {
final part = message.parts[partIndex];
if (part is! TextPart) return current;
newParts[partIndex] = TextPart(
id: part.id,
sessionID: part.sessionID,
messageID: part.messageID,
type: part.type,
text: '${part.text}$delta',
synthetic: part.synthetic,
ignored: part.ignored,
time: part.time,
metadata: part.metadata,
);
if (part is TextPart) {
newParts[partIndex] = TextPart(
id: part.id,
sessionID: part.sessionID,
messageID: part.messageID,
type: part.type,
text: '${part.text}$delta',
synthetic: part.synthetic,
ignored: part.ignored,
time: part.time,
metadata: part.metadata,
);
} else if (part is ReasoningPart) {
newParts[partIndex] = ReasoningPart(
id: part.id,
sessionID: part.sessionID,
messageID: part.messageID,
type: part.type,
text: '${part.text}$delta',
metadata: part.metadata,
time: part.time,
);
} else {
return current;
}
} else {
newParts.add(
TextPart(
Expand Down
48 changes: 48 additions & 0 deletions lib/providers/thinking_auto_expand_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'hydrated_state.dart';
import 'shared_preferences_provider.dart';

const _kThinkingAutoExpandKey = 'thinking_auto_expand_v1';

final thinkingAutoExpandProvider =
NotifierProvider<ThinkingAutoExpandNotifier, bool>(
ThinkingAutoExpandNotifier.new,
);

class ThinkingAutoExpandNotifier extends Notifier<bool> {
late final HydratedValueController<bool> _hydration =
HydratedValueController<bool>(
readState: () => state,
writeState: (value) => state = value,
load: () => readThinkingAutoExpandFromStorage(
() => ref.read(sharedPreferencesProvider.future),
),
persist: _persist,
isMounted: () => ref.mounted,
);

@override
bool build() {
_hydration.startRestore();
return false;
}

Future<void> setAutoExpand(bool value) async {
if (state == value && !_hydration.isHydrating) return;
await _hydration.setValue(value, waitForHydration: true);
}

Future<void> _persist(bool value) async {
final prefs = await ref.read(sharedPreferencesProvider.future);
await prefs.setBool(_kThinkingAutoExpandKey, value);
}
}

Future<bool> readThinkingAutoExpandFromStorage(
Future<SharedPreferences> Function() preferencesLoader,
) async {
final prefs = await preferencesLoader();
return prefs.getBool(_kThinkingAutoExpandKey) ?? false;
}
Loading