Skip to content

Commit 30a2b10

Browse files
author
cw
committed
feat: add 12-domain task system with rich UI cards, chat persistence, and voice toggle
- Add TaskDomain architecture: 12 domains (calculator, calendar, contacts, email, files, messages, music, notes, reminders, timer, translation, weather) with 34 task types via DomainRegistry + DomainPluginAdapter - Add domain pattern matching in Flutter for quick-path routing before AI fallback, with IntentRecognizer integration - Add 8 specialized domain card widgets (calculator, calendar, music, timer, reminders, weather, generic, domain_card_registry) with success-aware titles that show error states properly - Add chat message persistence via SharedPreferences (last 100 messages, filters image_base64 blobs) - Add ChatMessage toJson/fromJson serialization - Change voice input from long-press to tap-toggle with AnimatedContainer - Add domain E2E test suite (29/34 pass across 12 domains) - Add stress test report: 36 complex NL commands at 81% accuracy Tested on real iOS simulator (iPhone 16 Pro) with 36 commands including 13 very long story-like sentences (40-53 words each).
1 parent 265e01c commit 30a2b10

41 files changed

Lines changed: 5414 additions & 12 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

daemon/lib/core/daemon.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import 'package:opencli_daemon/telemetry/telemetry.dart';
1616
import 'package:opencli_daemon/plugins/mcp_manager.dart';
1717
import 'package:opencli_daemon/api/unified_api_server.dart';
1818
import 'package:opencli_daemon/api/message_handler.dart';
19+
import 'package:opencli_daemon/domains/domain_registry.dart';
20+
import 'package:opencli_daemon/domains/domain_plugin_adapter.dart';
1921

2022
class Daemon {
2123
static const String version = '0.2.0';
@@ -31,6 +33,7 @@ class Daemon {
3133
late final MobileTaskHandler _mobileTaskHandler;
3234
late final StatusServer _statusServer;
3335
late final TelemetryManager _telemetry;
36+
late final DomainRegistry _domainRegistry;
3437
WebUILauncher? _webUILauncher;
3538
PluginMarketplaceUI? _pluginMarketplaceUI;
3639
UnifiedApiServer? _unifiedApiServer;
@@ -146,6 +149,24 @@ class Daemon {
146149
// Continue without permission checks
147150
}
148151

152+
// Initialize domain registry (calendar, music, timer, weather, etc.)
153+
TerminalUI.printInitStep('Initializing task domain registry');
154+
_domainRegistry = createBuiltinDomainRegistry();
155+
await _domainRegistry.initializeAll();
156+
157+
// Register domain executors into mobile task handler
158+
_domainRegistry.registerIntoTaskHandler(_mobileTaskHandler);
159+
160+
// Register domain routes into request router (IPC + Unified API)
161+
_router.setDomainRegistry(_domainRegistry);
162+
163+
TerminalUI.success(
164+
'Domain registry: ${_domainRegistry.domains.length} domains, '
165+
'${_domainRegistry.allTaskTypes.length} task types '
166+
'(plugin + MCP + API)',
167+
prefix: ' ✓',
168+
);
169+
149170
// Start config watcher for hot-reload
150171
TerminalUI.printInitStep('Starting config watcher');
151172
_configWatcher = ConfigWatcher(
@@ -320,6 +341,9 @@ class Daemon {
320341
TerminalUI.printInitStep('Stopping MCP servers');
321342
await _mcpManager.stopAll();
322343

344+
TerminalUI.printInitStep('Disposing domain registry');
345+
await _domainRegistry.disposeAll();
346+
323347
TerminalUI.printInitStep('Disposing task handler');
324348
_mobileTaskHandler.dispose();
325349

@@ -354,9 +378,13 @@ class Daemon {
354378
'memory_mb': _healthMonitor.memoryUsageMb,
355379
'telemetry': _telemetry.getStats(),
356380
'taskHandler': _mobileTaskHandler.getStats(),
381+
'domains': _domainRegistry.getStats(),
357382
};
358383
}
359384

360385
/// Get mobile task handler for capability management
361386
MobileTaskHandler get taskHandler => _mobileTaskHandler;
387+
388+
/// Get domain registry for domain-based task access
389+
DomainRegistry get domainRegistry => _domainRegistry;
362390
}

daemon/lib/core/request_router.dart

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1+
import 'dart:convert';
12
import 'package:opencli_daemon/plugins/plugin_manager.dart';
23
import 'package:opencli_daemon/ipc/ipc_protocol.dart';
4+
import 'package:opencli_daemon/domains/domain_registry.dart';
5+
import 'package:opencli_daemon/domains/domain_plugin_adapter.dart';
36

47
class RequestRouter {
58
final PluginManager pluginManager;
9+
DomainRegistry? _domainRegistry;
610
int _totalRequests = 0;
711

812
RequestRouter(this.pluginManager);
913

1014
int get totalRequests => _totalRequests;
1115

16+
/// Set the domain registry for domain-based routing
17+
void setDomainRegistry(DomainRegistry registry) {
18+
_domainRegistry = registry;
19+
}
20+
1221
Future<String> route(IpcRequest request) async {
1322
_totalRequests++;
1423

@@ -23,6 +32,38 @@ class RequestRouter {
2332
return await _handleSystemCommand(parts.sublist(1), request.params);
2433
}
2534

35+
// Domain commands: domain.taskType (e.g., music.music_play, timer.timer_set)
36+
if (parts.length >= 2 && _domainRegistry != null) {
37+
final domainId = parts[0];
38+
final domain = _domainRegistry!.getDomain(domainId);
39+
if (domain != null) {
40+
final taskType = parts.sublist(1).join('_');
41+
final params = _extractParams(request.params);
42+
final result = await _domainRegistry!.executeTask(taskType, params);
43+
return jsonEncode(result);
44+
}
45+
}
46+
47+
// Domain shorthand: just taskType directly (e.g., music_play, timer_set)
48+
if (_domainRegistry != null && _domainRegistry!.handlesTaskType(parts[0])) {
49+
final params = _extractParams(request.params);
50+
final result = await _domainRegistry!.executeTask(parts[0], params);
51+
return jsonEncode(result);
52+
}
53+
54+
// MCP tool calls: mcp.toolName (e.g., mcp.opencli_music_play)
55+
if (parts[0] == 'mcp' && parts.length >= 2 && _domainRegistry != null) {
56+
final toolName = parts.sublist(1).join('_');
57+
final params = _extractParams(request.params);
58+
final result = await _domainRegistry!.executeMcpTool(toolName, params);
59+
return jsonEncode(result);
60+
}
61+
62+
// Domain discovery: domains.list / domains.stats / domains.tools
63+
if (parts[0] == 'domains') {
64+
return await _handleDomainsCommand(parts.sublist(1));
65+
}
66+
2667
// Plugin commands: plugin.action
2768
if (parts.length >= 2) {
2869
final pluginName = parts[0];
@@ -44,6 +85,15 @@ class RequestRouter {
4485
throw Exception('Unknown method: ${request.method}');
4586
}
4687

88+
/// Extract params map from IPC request params list
89+
Map<String, dynamic> _extractParams(List<dynamic> params) {
90+
if (params.isEmpty) return <String, dynamic>{};
91+
if (params.first is Map) {
92+
return Map<String, dynamic>.from(params.first as Map);
93+
}
94+
return <String, dynamic>{};
95+
}
96+
4797
Future<String> _handleSystemCommand(List<String> parts, List<dynamic> params) async {
4898
if (parts.isEmpty) {
4999
throw Exception('Missing system command');
@@ -53,14 +103,52 @@ class RequestRouter {
53103
case 'health':
54104
return 'OK';
55105
case 'plugins':
56-
return pluginManager.listPlugins().join(', ');
106+
final plugins = pluginManager.listPlugins();
107+
// Include domain plugins in listing
108+
if (_domainRegistry != null) {
109+
for (final domain in _domainRegistry!.domains) {
110+
plugins.add('@opencli/domain-${domain.id}');
111+
}
112+
}
113+
return plugins.join(', ');
57114
case 'version':
58-
return '0.1.0';
115+
return '0.2.0';
116+
case 'domains':
117+
if (_domainRegistry != null) {
118+
return jsonEncode(_domainRegistry!.getApiDiscovery());
119+
}
120+
return jsonEncode({'domains': [], 'total_domains': 0});
121+
case 'tools':
122+
if (_domainRegistry != null) {
123+
return jsonEncode(_domainRegistry!.generateMcpToolSchemas());
124+
}
125+
return jsonEncode([]);
59126
default:
60127
throw Exception('Unknown system command: ${parts[0]}');
61128
}
62129
}
63130

131+
/// Handle domains.* commands for discovery and management
132+
Future<String> _handleDomainsCommand(List<String> parts) async {
133+
if (_domainRegistry == null) {
134+
return jsonEncode({'error': 'Domain registry not initialized'});
135+
}
136+
137+
if (parts.isEmpty || parts[0] == 'list') {
138+
return jsonEncode(_domainRegistry!.getApiDiscovery());
139+
}
140+
141+
if (parts[0] == 'stats') {
142+
return jsonEncode(_domainRegistry!.getStats());
143+
}
144+
145+
if (parts[0] == 'tools') {
146+
return jsonEncode(_domainRegistry!.generateMcpToolSchemas());
147+
}
148+
149+
throw Exception('Unknown domains command: ${parts.join(".")}');
150+
}
151+
64152
Future<String> _handleChat(List<dynamic> params) async {
65153
if (params.isEmpty) {
66154
return 'Hello! How can I help you?';

0 commit comments

Comments
 (0)