feat: clarify project agent onboarding
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
## الحالة الحالية — 2026-10-04
|
## الحالة الحالية — 2026-10-04
|
||||||
|
|
||||||
- 2026-10-04 — تسجيل المشاريع في الواجهة والـAPI: عند اختيار مجلد من Flutter يُسجّل API المحلي المسار في SQLite تحت هوية الحساب (`user_workspace_roots`) ثم يسرد الملفات المدعومة؛ عند إزالة المشروع يُلغى التسجيل ولا تُحذف الملفات. أضيف POST/DELETE لـ`/v1/agent/projects` ويُسمح بهما عبر loopback فقط. اختبارات API تثبت التسجيل والقراءة لصاحب المشروع، رفض الحساب الآخر، منع تسجيل مجلد متداخل بين حسابين أو مع جذر خصصه مسؤول لحساب آخر (409)، وإلغاء الوصول بعد الحذف أو بعد حذف المجلد الأصلي. اختبار حي على مجلد خارج جذر المنتج قرأ `README.md`، وسأل Gemma عبر الوكيل فأجاب من المحتوى باسم `Cedar`، ثم تحقق رفض الوصول بعد الإلغاء. اختبارات Python ذات الصلة 10/10 وFlutter 17/17، و`flutter analyze` بلا ملاحظات و`compileall` ناجح. أُعيد تشغيل API وفحص `/health` (`ok`, Gemma 4)، وتأكد وجود مساري POST/DELETE في OpenAPI، وبُني وشُغّل Windows Debug الحالي (PID 1328). تشغيل أوامر البناء/الاختبار من الوكيل لم يُدمج بعد؛ عزل نظام التشغيل لهذا التشغيل ما زال مفتوحًا.
|
- 2026-10-04 — تسجيل المشاريع في الواجهة والـAPI: عند اختيار مجلد من Flutter يُسجّل API المحلي المسار في SQLite تحت هوية الحساب (`user_workspace_roots`) ثم يسرد الملفات المدعومة؛ عند إزالة المشروع يُلغى التسجيل ولا تُحذف الملفات. أضيف POST/DELETE لـ`/v1/agent/projects` ويُسمح بهما عبر loopback فقط. اختبارات API تثبت التسجيل والقراءة لصاحب المشروع، رفض الحساب الآخر، منع تسجيل مجلد متداخل بين حسابين أو مع جذر خصصه مسؤول لحساب آخر (409)، وإلغاء الوصول بعد الحذف أو بعد حذف المجلد الأصلي. اختبار حي على مجلد خارج جذر المنتج قرأ `README.md`، وسأل Gemma عبر الوكيل فأجاب من المحتوى باسم `Cedar`، ثم تحقق رفض الوصول بعد الإلغاء. اختبارات Python ذات الصلة 10/10 وFlutter 17/17، و`flutter analyze` بلا ملاحظات و`compileall` ناجح. أُعيد تشغيل API وفحص `/health` (`ok`, Gemma 4)، وتأكد وجود مساري POST/DELETE في OpenAPI، وبُني وشُغّل Windows Debug الحالي (PID 1328). تشغيل أوامر البناء/الاختبار من الوكيل لم يُدمج بعد؛ عزل نظام التشغيل لهذا التشغيل ما زال مفتوحًا.
|
||||||
|
- 2026-10-04 — إرشاد تسجيل المشروع من شاشة البداية: عند عدم فتح مساحة عمل تعرض الواجهة بطاقة تشرح أن المطلوب اختيار مجلد موجود وتوفّر زرًا مباشرًا؛ بعد فتحه تعرض اسم المشروع وقدرات الوكيل وحدوده، ومنها أن تشغيل أوامر البناء والاختبار غير متاح بعد. تسمية زر الشريط العلوي صارت «تسجيل مجلد مشروع موجود». اختبارات `widget_test.dart` نجحت 7/7 و`flutter analyze --no-pub` بلا ملاحظات. لم يُبنَ أو يُستبدل Windows Debug بعد هذا التعديل، ولذلك يلزم تشغيل النسخة الجديدة لرؤية البطاقة في التطبيق.
|
||||||
|
|
||||||
- اختبار سقف التخزين: نجح `scripts/verify_vhd_disk_quota.ps1` بقرص VHDX مؤقت 64MiB؛ كتب 53,477,376 بايت حتى `ERROR_DISK_FULL` (112)، بقي 151,552 بايت حرًا، وحُذف قرص الاختبار بعد التحقق. سكربت القرص الدائم 1GiB اجتاز تحليل PowerShell بعد إصلاحه، لكن `Status` أكد عدم وجود القرص؛ محاولات UAC عبر Windows PowerShell وPowerShell 7 و`cmd.exe` انتهت بخطأ بدء `0xc0000142` قبل أي تعديل. لذلك لا يوجد حتى الآن VHDX دائم، ولا يُسمح للوكيل بتنفيذ أوامر.
|
- اختبار سقف التخزين: نجح `scripts/verify_vhd_disk_quota.ps1` بقرص VHDX مؤقت 64MiB؛ كتب 53,477,376 بايت حتى `ERROR_DISK_FULL` (112)، بقي 151,552 بايت حرًا، وحُذف قرص الاختبار بعد التحقق. سكربت القرص الدائم 1GiB اجتاز تحليل PowerShell بعد إصلاحه، لكن `Status` أكد عدم وجود القرص؛ محاولات UAC عبر Windows PowerShell وPowerShell 7 و`cmd.exe` انتهت بخطأ بدء `0xc0000142` قبل أي تعديل. لذلك لا يوجد حتى الآن VHDX دائم، ولا يُسمح للوكيل بتنفيذ أوامر.
|
||||||
|
|
||||||
|
|||||||
+146
-58
@@ -1411,7 +1411,7 @@ class _ChatPageState extends State<ChatPage> {
|
|||||||
FilledButton.tonalIcon(
|
FilledButton.tonalIcon(
|
||||||
onPressed: chat.isSending ? null : _addWorkspaceProject,
|
onPressed: chat.isSending ? null : _addWorkspaceProject,
|
||||||
icon: const Icon(Icons.create_new_folder_outlined),
|
icon: const Icon(Icons.create_new_folder_outlined),
|
||||||
label: const Text('إضافة مجلد مشروع'),
|
label: const Text('تسجيل مجلد مشروع موجود'),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
IconButton.filledTonal(
|
IconButton.filledTonal(
|
||||||
@@ -1762,70 +1762,158 @@ class _ChatPageState extends State<ChatPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _welcome() => Center(
|
Widget _welcome() {
|
||||||
child: SingleChildScrollView(
|
final chat = context.watch<ChatCubit>().state;
|
||||||
padding: const EdgeInsets.all(24),
|
final selectedProject =
|
||||||
child: ConstrainedBox(
|
chat.selectedWorkspacePath == null
|
||||||
constraints: const BoxConstraints(maxWidth: 680),
|
? null
|
||||||
child: Column(
|
: _workspaceName(chat.selectedWorkspacePath!);
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
return Center(
|
||||||
children: [
|
child: SingleChildScrollView(
|
||||||
Align(
|
padding: const EdgeInsets.all(24),
|
||||||
alignment: Alignment.centerRight,
|
child: ConstrainedBox(
|
||||||
child: Container(
|
constraints: const BoxConstraints(maxWidth: 680),
|
||||||
width: 62,
|
child: Column(
|
||||||
height: 62,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
alignment: Alignment.center,
|
children: [
|
||||||
decoration: BoxDecoration(
|
Align(
|
||||||
color: const Color(0xFFDCEAE3),
|
alignment: Alignment.centerRight,
|
||||||
borderRadius: BorderRadius.circular(20),
|
child: Container(
|
||||||
),
|
width: 62,
|
||||||
child: const Icon(
|
height: 62,
|
||||||
Icons.waving_hand_rounded,
|
alignment: Alignment.center,
|
||||||
color: _green,
|
decoration: BoxDecoration(
|
||||||
size: 30,
|
color: const Color(0xFFDCEAE3),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.waving_hand_rounded,
|
||||||
|
color: _green,
|
||||||
|
size: 30,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 22),
|
||||||
const SizedBox(height: 22),
|
const Text(
|
||||||
const Text(
|
'أهلًا، خلّينا نجرّب',
|
||||||
'أهلًا، خلّينا نجرّب',
|
style: TextStyle(
|
||||||
style: TextStyle(
|
color: _ink,
|
||||||
color: _ink,
|
fontSize: 30,
|
||||||
fontSize: 30,
|
fontWeight: FontWeight.w700,
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
context.watch<ChatCubit>().state.isWorkspaceMode
|
|
||||||
? 'الوكيل المحلي: قد يستخدم حتى ثلاث خطوات متتابعة، مثل البحث في الملفات والحساب؛ واقتراح تعديل الملف يعرض معاينة تنتظر موافقتك.'
|
|
||||||
: context.watch<ChatCubit>().state.isWebSearchMode
|
|
||||||
? 'بحث عميق: يجلب مصادر ويب متعددة ويلخصها مع الروابط.'
|
|
||||||
: 'اكتب سؤالك، وسأرسله إلى النموذج المحلي على جهازك.',
|
|
||||||
style: const TextStyle(color: Color(0xFF718078), fontSize: 14),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 28),
|
|
||||||
Wrap(
|
|
||||||
spacing: 10,
|
|
||||||
runSpacing: 10,
|
|
||||||
children: [
|
|
||||||
_suggestion('اكتب مثال Python يجمع رقمين', Icons.code_rounded),
|
|
||||||
_suggestion(
|
|
||||||
'اشرح لي ما هو الـ API',
|
|
||||||
Icons.lightbulb_outline_rounded,
|
|
||||||
),
|
),
|
||||||
_suggestion(
|
),
|
||||||
'اقترح خطوات لبناء وكيل بسيط',
|
const SizedBox(height: 8),
|
||||||
Icons.account_tree_outlined,
|
Text(
|
||||||
|
chat.isWorkspaceMode
|
||||||
|
? 'الوكيل المحلي: قد يستخدم حتى ثلاث خطوات متتابعة، مثل البحث في الملفات والحساب؛ واقتراح تعديل الملف يعرض معاينة تنتظر موافقتك.'
|
||||||
|
: chat.isWebSearchMode
|
||||||
|
? 'بحث عميق: يجلب مصادر ويب متعددة ويلخصها مع الروابط.'
|
||||||
|
: 'اكتب سؤالك، وسأرسله إلى النموذج المحلي على جهازك.',
|
||||||
|
style: const TextStyle(color: Color(0xFF718078), fontSize: 14),
|
||||||
|
),
|
||||||
|
if (!chat.isWorkspaceMode) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: const Color(0xFFF0F5F1),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: const BorderSide(color: Color(0xFFDCE7DE)),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'جرّب الوكيل على مشروعك',
|
||||||
|
style: TextStyle(
|
||||||
|
color: _ink,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
chat.savedWorkspacePaths.isEmpty
|
||||||
|
? 'سجّل مجلد مشروع موجود على جهازك؛ بعدها يقرأ الوكيل الملفات التي تختارها ويجيب عنها.'
|
||||||
|
: 'اختر مجلد مشروع مسجلًا من «مشاريعي»، أو سجّل مجلدًا موجودًا جديدًا ليقرأ الوكيل ملفاته.',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF617168),
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed:
|
||||||
|
chat.isSending ? null : _addWorkspaceProject,
|
||||||
|
icon: const Icon(Icons.folder_open_outlined),
|
||||||
|
label: const Text('اختيار مجلد مشروع موجود'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else if (selectedProject != null) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: const Color(0xFFF0F5F1),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: const BorderSide(color: Color(0xFFDCE7DE)),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'الوكيل يعمل على: $selectedProject',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: _ink,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
'يمكنه البحث والقراءة واقتراح تعديل مع معاينة وموافقتك. تشغيل أوامر البناء والاختبار داخل المشروع لم يُفعّل بعد.',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF617168),
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
const SizedBox(height: 28),
|
||||||
],
|
Wrap(
|
||||||
|
spacing: 10,
|
||||||
|
runSpacing: 10,
|
||||||
|
children: [
|
||||||
|
_suggestion(
|
||||||
|
'اكتب مثال Python يجمع رقمين',
|
||||||
|
Icons.code_rounded,
|
||||||
|
),
|
||||||
|
_suggestion(
|
||||||
|
'اشرح لي ما هو الـ API',
|
||||||
|
Icons.lightbulb_outline_rounded,
|
||||||
|
),
|
||||||
|
_suggestion(
|
||||||
|
'اقترح خطوات لبناء وكيل بسيط',
|
||||||
|
Icons.account_tree_outlined,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
Widget _suggestion(String text, IconData icon) => ActionChip(
|
Widget _suggestion(String text, IconData icon) => ActionChip(
|
||||||
onPressed: () => _send(text),
|
onPressed: () => _send(text),
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ void main() {
|
|||||||
|
|
||||||
expect(find.text('مساعدك المحلي'), findsOneWidget);
|
expect(find.text('مساعدك المحلي'), findsOneWidget);
|
||||||
expect(find.text('مشاريعي'), findsOneWidget);
|
expect(find.text('مشاريعي'), findsOneWidget);
|
||||||
|
expect(find.text('جرّب الوكيل على مشروعك'), findsOneWidget);
|
||||||
|
expect(find.text('اختيار مجلد مشروع موجود'), findsOneWidget);
|
||||||
expect(find.byTooltip('إضافة مشروع موجود إلى القائمة'), findsOneWidget);
|
expect(find.byTooltip('إضافة مشروع موجود إلى القائمة'), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
find.byTooltip('تسجيل صوت؛ يُرسل إلى Groq السحابي لتحويله إلى نص'),
|
find.byTooltip('تسجيل صوت؛ يُرسل إلى Groq السحابي لتحويله إلى نص'),
|
||||||
@@ -246,7 +248,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('إضافة مجلد مشروع'), findsOneWidget);
|
expect(find.text('تسجيل مجلد مشروع موجود'), findsOneWidget);
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
await chatCubit.close();
|
await chatCubit.close();
|
||||||
});
|
});
|
||||||
@@ -279,6 +281,8 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await chatCubit.selectWorkspace('C:/sample-project');
|
await chatCubit.selectWorkspace('C:/sample-project');
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('الوكيل يعمل على: sample-project'), findsOneWidget);
|
||||||
|
expect(find.textContaining('تشغيل أوامر البناء والاختبار'), findsOneWidget);
|
||||||
await tester.tap(find.byTooltip('اختيار مهارة للوكيل'));
|
await tester.tap(find.byTooltip('اختيار مهارة للوكيل'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('خطة اختبارات'), findsOneWidget);
|
expect(find.text('خطة اختبارات'), findsOneWidget);
|
||||||
|
|||||||
Reference in New Issue
Block a user