feat: register local agent project workspaces
This commit is contained in:
@@ -478,6 +478,26 @@ class ApiRepository {
|
||||
return (data['files'] as List<dynamic>).cast<String>();
|
||||
}
|
||||
|
||||
Future<List<String>> registerWorkspaceProject(String workspacePath) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/v1/agent/projects'),
|
||||
headers: await _userHeaders(),
|
||||
body: jsonEncode({'workspace_path': workspacePath}),
|
||||
);
|
||||
_checkStatus(response);
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return (data['files'] as List<dynamic>).cast<String>();
|
||||
}
|
||||
|
||||
Future<void> unregisterWorkspaceProject(String workspacePath) async {
|
||||
final response = await http.delete(
|
||||
Uri.parse('$_baseUrl/v1/agent/projects'),
|
||||
headers: await _userHeaders(),
|
||||
body: jsonEncode({'workspace_path': workspacePath}),
|
||||
);
|
||||
_checkStatus(response);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> indexWorkspaceKnowledge(
|
||||
String workspacePath,
|
||||
List<String> files,
|
||||
|
||||
+20
-3
@@ -271,7 +271,7 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
),
|
||||
);
|
||||
try {
|
||||
final files = await _api.listWorkspaceFiles(path);
|
||||
final files = await _api.registerWorkspaceProject(path);
|
||||
if (isClosed || state.selectedWorkspacePath != path) return const [];
|
||||
final savedPaths = [
|
||||
...state.savedWorkspacePaths.where((savedPath) => savedPath != path),
|
||||
@@ -300,9 +300,26 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
}
|
||||
|
||||
Future<bool> removeSavedWorkspace(String path) async {
|
||||
try {
|
||||
await _api.unregisterWorkspaceProject(path);
|
||||
} catch (error) {
|
||||
if (!isClosed) emit(state.copyWith(error: _readableError(error)));
|
||||
return false;
|
||||
}
|
||||
final savedPaths =
|
||||
state.savedWorkspacePaths.where((savedPath) => savedPath != path).toList();
|
||||
emit(state.copyWith(savedWorkspacePaths: List.unmodifiable(savedPaths)));
|
||||
state.savedWorkspacePaths
|
||||
.where((savedPath) => savedPath != path)
|
||||
.toList();
|
||||
final isSelected = state.selectedWorkspacePath == path;
|
||||
emit(
|
||||
state.copyWith(
|
||||
savedWorkspacePaths: List.unmodifiable(savedPaths),
|
||||
isWorkspaceMode: isSelected ? false : null,
|
||||
clearSelectedWorkspacePath: isSelected,
|
||||
availableWorkspaceFiles: isSelected ? const [] : null,
|
||||
selectedWorkspaceFiles: isSelected ? const [] : null,
|
||||
),
|
||||
);
|
||||
return _persistSettings({'saved_workspace_paths': savedPaths});
|
||||
}
|
||||
|
||||
|
||||
+26
-16
@@ -180,7 +180,7 @@ class _ChatPageState extends State<ChatPage> {
|
||||
(dialogContext) => AlertDialog(
|
||||
title: const Text('إزالة المشروع من القائمة؟'),
|
||||
content: Text(
|
||||
'سيُزال ${_workspaceName(path)} من قائمة المشاريع المحفوظة فقط. لن تُحذف ملفاته.',
|
||||
'سيُزال ${_workspaceName(path)} من قائمة المشاريع، ويُلغى أي تسجيل وصول خاص به. لن تُحذف ملفاته من جهازك.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -698,8 +698,8 @@ class _ChatPageState extends State<ChatPage> {
|
||||
content: Text(
|
||||
api.accountSessionPersists
|
||||
? credentials.$1
|
||||
? 'تم إنشاء الحساب وتخزين الجلسة بأمان.'
|
||||
: 'تم تسجيل الدخول وتخزين الجلسة بأمان.'
|
||||
? 'تم إنشاء الحساب وتخزين الجلسة بأمان.'
|
||||
: 'تم تسجيل الدخول وتخزين الجلسة بأمان.'
|
||||
: credentials.$1
|
||||
? 'تم إنشاء الحساب. جلسة الويب مؤقتة وستنتهي عند إعادة تحميل الصفحة.'
|
||||
: 'تم تسجيل الدخول. جلسة الويب مؤقتة وستنتهي عند إعادة تحميل الصفحة.',
|
||||
@@ -743,10 +743,11 @@ class _ChatPageState extends State<ChatPage> {
|
||||
child: const Text('إلغاء'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(
|
||||
dialogContext,
|
||||
emailController.text.trim(),
|
||||
),
|
||||
onPressed:
|
||||
() => Navigator.pop(
|
||||
dialogContext,
|
||||
emailController.text.trim(),
|
||||
),
|
||||
child: const Text('إرسال الرمز'),
|
||||
),
|
||||
],
|
||||
@@ -818,7 +819,9 @@ class _ChatPageState extends State<ChatPage> {
|
||||
passwordController.text.length < 12) {
|
||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('أدخل رمزًا صالحًا وكلمة مرور من 12 محرفًا على الأقل.'),
|
||||
content: Text(
|
||||
'أدخل رمزًا صالحًا وكلمة مرور من 12 محرفًا على الأقل.',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -841,7 +844,9 @@ class _ChatPageState extends State<ChatPage> {
|
||||
await api.completePasswordReset(token: reset.$1, newPassword: reset.$2);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('تم تغيير كلمة المرور. سجّل الدخول بها الآن.')),
|
||||
const SnackBar(
|
||||
content: Text('تم تغيير كلمة المرور. سجّل الدخول بها الآن.'),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
@@ -1121,14 +1126,18 @@ class _ChatPageState extends State<ChatPage> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
child: Material(
|
||||
color: selected
|
||||
? const Color(0xFFDCEAE3)
|
||||
: Colors.transparent,
|
||||
color:
|
||||
selected
|
||||
? const Color(0xFFDCEAE3)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: const EdgeInsets.only(left: 3, right: 8),
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 3,
|
||||
right: 8,
|
||||
),
|
||||
leading: Icon(
|
||||
Icons.folder_open_outlined,
|
||||
size: 18,
|
||||
@@ -1146,9 +1155,10 @@ class _ChatPageState extends State<ChatPage> {
|
||||
icon: const Icon(Icons.close, size: 16),
|
||||
onPressed: () => _removeSavedWorkspace(path),
|
||||
),
|
||||
onTap: chat.isSending
|
||||
? null
|
||||
: () => _openSavedWorkspace(path),
|
||||
onTap:
|
||||
chat.isSending
|
||||
? null
|
||||
: () => _openSavedWorkspace(path),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -17,6 +17,8 @@ class _VersionTestApi extends ApiRepository {
|
||||
String? indexedWorkspace;
|
||||
List<String> indexedFiles = const [];
|
||||
List<String> deletedFiles = const [];
|
||||
String? registeredProject;
|
||||
String? unregisteredProject;
|
||||
|
||||
@override
|
||||
Future<String> getModelName() async => 'gemma4:e2b';
|
||||
@@ -62,6 +64,17 @@ class _VersionTestApi extends ApiRepository {
|
||||
'README.md',
|
||||
];
|
||||
|
||||
@override
|
||||
Future<List<String>> registerWorkspaceProject(String workspacePath) async {
|
||||
registeredProject = workspacePath;
|
||||
return listWorkspaceFiles(workspacePath);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> unregisterWorkspaceProject(String workspacePath) async {
|
||||
unregisteredProject = workspacePath;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> indexWorkspaceKnowledge(
|
||||
String workspacePath,
|
||||
@@ -180,30 +193,50 @@ void main() {
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('registered project folders persist and can be removed from the list', () async {
|
||||
final settings = _MemorySettingsStore();
|
||||
final firstApi = _VersionTestApi();
|
||||
final first = ChatCubit(firstApi, settingsStore: settings);
|
||||
test(
|
||||
'registered project folders persist and can be removed from the list',
|
||||
() async {
|
||||
final settings = _MemorySettingsStore();
|
||||
final firstApi = _VersionTestApi();
|
||||
final first = ChatCubit(firstApi, settingsStore: settings);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
await first.selectWorkspace(r'C:\Projects\sovereign-ai');
|
||||
expect(firstApi.registeredProject, r'C:\Projects\sovereign-ai');
|
||||
expect(first.state.savedWorkspacePaths, [r'C:\Projects\sovereign-ai']);
|
||||
expect(settings.values['saved_workspace_paths'], [
|
||||
r'C:\Projects\sovereign-ai',
|
||||
]);
|
||||
await first.close();
|
||||
|
||||
final restoredApi = _VersionTestApi();
|
||||
final restored = ChatCubit(restoredApi, settingsStore: settings);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(restored.state.savedWorkspacePaths, [r'C:\Projects\sovereign-ai']);
|
||||
|
||||
await restored.removeSavedWorkspace(r'C:\Projects\sovereign-ai');
|
||||
expect(restoredApi.unregisteredProject, r'C:\Projects\sovereign-ai');
|
||||
expect(restored.state.savedWorkspacePaths, isEmpty);
|
||||
expect(settings.values['saved_workspace_paths'], isEmpty);
|
||||
await restored.close();
|
||||
},
|
||||
);
|
||||
|
||||
test('removing the active project revokes its chat context', () async {
|
||||
final api = _VersionTestApi();
|
||||
final cubit = ChatCubit(api, settingsStore: LocalSettingsStore.inMemory());
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
const project = r'C:\Projects\active-project';
|
||||
|
||||
await first.selectWorkspace(r'C:\Projects\sovereign-ai');
|
||||
expect(first.state.savedWorkspacePaths, [r'C:\Projects\sovereign-ai']);
|
||||
expect(settings.values['saved_workspace_paths'], [
|
||||
r'C:\Projects\sovereign-ai',
|
||||
]);
|
||||
await first.close();
|
||||
await cubit.selectWorkspace(project);
|
||||
expect(cubit.state.isWorkspaceMode, isTrue);
|
||||
await cubit.removeSavedWorkspace(project);
|
||||
|
||||
final restored = ChatCubit(
|
||||
_VersionTestApi(),
|
||||
settingsStore: settings,
|
||||
);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(restored.state.savedWorkspacePaths, [r'C:\Projects\sovereign-ai']);
|
||||
|
||||
await restored.removeSavedWorkspace(r'C:\Projects\sovereign-ai');
|
||||
expect(restored.state.savedWorkspacePaths, isEmpty);
|
||||
expect(settings.values['saved_workspace_paths'], isEmpty);
|
||||
await restored.close();
|
||||
expect(api.unregisteredProject, project);
|
||||
expect(cubit.state.isWorkspaceMode, isFalse);
|
||||
expect(cubit.state.selectedWorkspacePath, isNull);
|
||||
expect(cubit.state.availableWorkspaceFiles, isEmpty);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('regeneration keeps prior answers and selection is persisted', () async {
|
||||
|
||||
@@ -50,6 +50,13 @@ class _TestApiRepository extends ApiRepository {
|
||||
'lib/main.dart',
|
||||
];
|
||||
|
||||
@override
|
||||
Future<List<String>> registerWorkspaceProject(String workspacePath) async =>
|
||||
listWorkspaceFiles(workspacePath);
|
||||
|
||||
@override
|
||||
Future<void> unregisterWorkspaceProject(String workspacePath) async {}
|
||||
|
||||
@override
|
||||
Future<List<SavedConversation>> listConversations() async => [];
|
||||
}
|
||||
@@ -97,6 +104,13 @@ class _ProposalWidgetApi extends _TestApiRepository {
|
||||
'src/hello.py',
|
||||
];
|
||||
|
||||
@override
|
||||
Future<List<String>> registerWorkspaceProject(String workspacePath) async =>
|
||||
listWorkspaceFiles(workspacePath);
|
||||
|
||||
@override
|
||||
Future<void> unregisterWorkspaceProject(String workspacePath) async {}
|
||||
|
||||
@override
|
||||
Future<AgentRunResult> runAgent(
|
||||
String task, {
|
||||
|
||||
Reference in New Issue
Block a user