Skip to content
Open
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
4 changes: 4 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:vide_cli/theme/theme.dart';
import 'package:vide_core/vide_core.dart';
import 'package:vide_cli/modules/agent_network/state/agent_networks_state_notifier.dart';
import 'package:vide_cli/services/sentry_service.dart';
import 'package:vide_cli/services/vide_settings.dart';

/// Provider for sidebar focus state, shared across the app.
/// Pages can update this to give focus to the sidebar.
Expand Down Expand Up @@ -69,6 +70,9 @@ void main(List<String> args, {List<Override> overrides = const []}) async {
// before launching the actual binary. The version indicator shows "ready" when
// an update has been downloaded and will be applied on next launch.

// Load app settings
await VideSettingsManager.instance.load();

await container.read(agentNetworksStateNotifierProvider.notifier).init();

await runApp(
Expand Down
41 changes: 41 additions & 0 deletions lib/modules/agent_network/network_execution_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,20 @@ class _AgentChatState extends State<_AgentChat> {
}

void _sendMessage(Message message) {
// Clear any previous sommelier commentary
context.read(codeSommelierProvider.notifier).state = null;

// Generate creative loading words with Haiku in the background
_generateLoadingWords(message.text);

// Check for code and trigger sommelier if enabled (delayed to avoid race with loading words)
final textToCheck = message.text;
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
_generateSommelierCommentary(textToCheck);
}
});

// Send the actual message
component.client.sendMessage(message);
}
Expand All @@ -331,6 +342,24 @@ class _AgentChatState extends State<_AgentChat> {
);
}

/// Generate wine-tasting style commentary for pasted code
void _generateSommelierCommentary(String text) async {
await MessageEnhancementService.generateSommelierCommentary(
text,
(commentary) {
if (!mounted) return;
context.read(codeSommelierProvider.notifier).state = commentary;

// Auto-clear after 30 seconds
Future.delayed(const Duration(seconds: 30), () {
if (mounted) {
context.read(codeSommelierProvider.notifier).state = null;
}
});
},
);
}

/// Gets cumulative output token count across the conversation.
int? _getOutputTokens() {
final total = _conversation.totalOutputTokens;
Expand Down Expand Up @@ -569,6 +598,7 @@ class _AgentChatState extends State<_AgentChat> {

// Get dynamic loading words from provider
final dynamicLoadingWords = context.watch(loadingWordsProvider);
final sommelierCommentary = context.watch(codeSommelierProvider);

return Focusable(
onKeyEvent: _handleKeyEvent,
Expand Down Expand Up @@ -637,6 +667,17 @@ class _AgentChatState extends State<_AgentChat> {
)
else
Text(' '), // Reserve 1 line when loading indicator is hidden

// Show code sommelier commentary when available
if (sommelierCommentary != null)
Container(
padding: EdgeInsets.symmetric(vertical: 1),
child: Text(
'🍷 $sommelierCommentary',
style: TextStyle(color: Colors.magenta.withOpacity(0.8), fontStyle: FontStyle.italic),
),
),

// Show quit warning if active
if (component.showQuitWarning)
Text(
Expand Down
11 changes: 3 additions & 8 deletions lib/modules/haiku/message_enhancement_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:vide_cli/modules/haiku/haiku_service.dart';
import 'package:vide_cli/modules/haiku/prompts/loading_words_prompt.dart';
import 'package:vide_cli/modules/haiku/prompts/code_sommelier_prompt.dart';
import 'package:vide_cli/utils/code_detector.dart';
import 'package:vide_cli/services/vide_settings.dart';

/// Centralized service for message enhancement features.
/// Handles loading words generation and code sommelier commentary.
Expand Down Expand Up @@ -33,21 +34,15 @@ class MessageEnhancementService {

/// Generate wine-tasting style commentary for code in a message.
///
/// NOTE: This feature requires VideSettingsManager which is not available in this branch.
/// The method is kept for API compatibility but currently does nothing.
/// Enable by adding vide_settings.dart and calling this method from integration points.
///
/// [userMessage] The user's message that may contain code.
/// [setCommentary] Callback to set the generated commentary in the provider.
static Future<void> generateSommelierCommentary(
String userMessage,
void Function(String) setCommentary,
) async {
// Sommelier feature disabled in this branch - settings service not available
// To enable: add vide_settings.dart and uncomment the implementation below
return;
// Check if sommelier is enabled in settings
if (!VideSettingsManager.instance.settings.codeSommelierEnabled) return;

// ignore: dead_code
if (!CodeDetector.containsCode(userMessage)) return;

final extractedCode = CodeDetector.extractCode(userMessage);
Expand Down
174 changes: 174 additions & 0 deletions lib/modules/settings/vide_settings_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import 'package:nocterm/nocterm.dart';
import 'package:vide_cli/services/vide_settings.dart';

/// Page to view and modify Vide settings
class VideSettingsPage extends StatefulComponent {
const VideSettingsPage({super.key});

static Future push(BuildContext context) async {
return Navigator.of(context).push(
PageRoute(builder: (context) => VideSettingsPage(), settings: RouteSettings()),
);
}

@override
State<VideSettingsPage> createState() => _VideSettingsPageState();
}

class _VideSettingsPageState extends State<VideSettingsPage> {
late VideSettings _settings;
int _selectedIndex = 0;

// Define settings as a list for easy navigation
List<_SettingItem> get _settingItems => [
_SettingItem(
key: 'codeSommelier',
label: 'Code Sommelier',
description: 'Wine-tasting style commentary on pasted code',
value: _settings.codeSommelierEnabled,
onToggle: () => _toggleSetting('codeSommelier'),
),
];

@override
void initState() {
super.initState();
_settings = VideSettingsManager.instance.settings;
}

Future<void> _toggleSetting(String key) async {
switch (key) {
case 'codeSommelier':
final newValue = !_settings.codeSommelierEnabled;
await VideSettingsManager.instance.setCodeSommelierEnabled(newValue);
setState(() {
_settings = VideSettingsManager.instance.settings;
});
break;
}
}

@override
Component build(BuildContext context) {
final items = _settingItems;

return Focusable(
focused: true,
onKeyEvent: (event) {
// Up/Down to navigate
if (event.logicalKey == LogicalKey.arrowUp && _selectedIndex > 0) {
setState(() => _selectedIndex--);
return true;
}
if (event.logicalKey == LogicalKey.arrowDown && _selectedIndex < items.length - 1) {
setState(() => _selectedIndex++);
return true;
}

// Enter/Space to toggle
if (event.logicalKey == LogicalKey.enter || event.logicalKey == LogicalKey.space) {
items[_selectedIndex].onToggle();
return true;
}

// Escape/Q to go back
if (event.logicalKey == LogicalKey.escape || event.logicalKey == LogicalKey.keyQ) {
Navigator.of(context).pop();
return true;
}

return false;
},
child: Container(
padding: EdgeInsets.all(2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Title
Text(
'Vide Settings',
style: TextStyle(fontWeight: FontWeight.bold, decoration: TextDecoration.underline),
),
SizedBox(height: 1),

// Help text
Text(
'↑↓ Navigate • Enter/Space Toggle • Q/Esc Back',
style: TextStyle(color: Colors.grey),
),
SizedBox(height: 2),

// Settings list
Expanded(
child: ListView(
children: [
for (var i = 0; i < items.length; i++)
_SettingRow(
item: items[i],
isSelected: i == _selectedIndex,
),
],
),
),
],
),
),
);
}
}

class _SettingItem {
final String key;
final String label;
final String description;
final bool value;
final VoidCallback onToggle;

_SettingItem({
required this.key,
required this.label,
required this.description,
required this.value,
required this.onToggle,
});
}

class _SettingRow extends StatelessComponent {
final _SettingItem item;
final bool isSelected;

const _SettingRow({required this.item, required this.isSelected});

@override
Component build(BuildContext context) {
final checkbox = item.value ? '[✓]' : '[ ]';
final bgColor = isSelected ? Colors.blue : null;
final textColor = isSelected ? Colors.white : Colors.white;

return Container(
color: bgColor,
padding: EdgeInsets.symmetric(horizontal: 1),
child: Row(
children: [
Text(
checkbox,
style: TextStyle(
color: item.value ? Colors.green : Colors.grey,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 1),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.label, style: TextStyle(color: textColor, fontWeight: FontWeight.bold)),
Text(item.description, style: TextStyle(color: isSelected ? Colors.white : Colors.grey)),
],
),
),
],
),
);
}
}
92 changes: 92 additions & 0 deletions lib/services/vide_settings.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:vide_cli/services/vide_config_manager.dart';

/// App-wide settings for Vide CLI (separate from Claude settings)
class VideSettings {
final bool codeSommelierEnabled;

const VideSettings({
this.codeSommelierEnabled = false,
});

VideSettings copyWith({bool? codeSommelierEnabled}) {
return VideSettings(
codeSommelierEnabled: codeSommelierEnabled ?? this.codeSommelierEnabled,
);
}

Map<String, dynamic> toJson() => {
'codeSommelierEnabled': codeSommelierEnabled,
};

factory VideSettings.fromJson(Map<String, dynamic> json) {
return VideSettings(
codeSommelierEnabled: json['codeSommelierEnabled'] as bool? ?? false,
);
}

static VideSettings defaults() => const VideSettings();
}

/// Singleton manager for Vide app settings
class VideSettingsManager {
static final VideSettingsManager instance = VideSettingsManager._();
VideSettingsManager._();

VideSettings _settings = VideSettings.defaults();
bool _loaded = false;

VideSettings get settings => _settings;

String get _settingsPath {
final configRoot = VideConfigManager().configRoot;
return path.join(configRoot, 'settings.json');
}

/// Load settings from disk (call once at startup)
Future<void> load() async {
if (_loaded) return;

try {
final file = File(_settingsPath);
if (file.existsSync()) {
final content = file.readAsStringSync();
final json = jsonDecode(content) as Map<String, dynamic>;
_settings = VideSettings.fromJson(json);
}
} catch (e) {
// Use defaults on error
}
_loaded = true;
}

/// Save current settings to disk
Future<void> save() async {
try {
final file = File(_settingsPath);
final dir = file.parent;
if (!dir.existsSync()) {
dir.createSync(recursive: true);
}
file.writeAsStringSync(
const JsonEncoder.withIndent(' ').convert(_settings.toJson()),
);
} catch (e) {
// Ignore save errors
}
}

/// Update settings
Future<void> update(VideSettings newSettings) async {
_settings = newSettings;
await save();
}

/// Toggle code sommelier
Future<void> setCodeSommelierEnabled(bool enabled) async {
_settings = _settings.copyWith(codeSommelierEnabled: enabled);
await save();
}
}