x-ipe 1.0.23__py3-none-any.whl → 1.0.25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- x_ipe/app.py +32 -1
- x_ipe/handlers/terminal_handlers.py +6 -0
- x_ipe/handlers/voice_handlers.py +5 -0
- x_ipe/resources/copilot-instructions.md +19 -6
- x_ipe/resources/skills/lesson-learned/SKILL.md +208 -0
- x_ipe/resources/skills/lesson-learned/references/examples.md +238 -0
- x_ipe/resources/skills/project-quality-board-management/SKILL.md +135 -298
- x_ipe/resources/skills/project-quality-board-management/references/evaluation-principles.md +213 -0
- x_ipe/resources/skills/project-quality-board-management/references/evaluation-procedures.md +214 -0
- x_ipe/resources/skills/project-quality-board-management/templates/quality-report.md +70 -18
- x_ipe/resources/skills/task-execution-guideline/SKILL.md +2 -2
- x_ipe/resources/skills/task-execution-guideline/templates/task-record.yaml +1 -1
- x_ipe/resources/skills/task-type-code-implementation/SKILL.md +72 -270
- x_ipe/resources/skills/task-type-code-implementation/references/implementation-guidelines.md +432 -0
- x_ipe/resources/skills/task-type-code-refactor-v2/SKILL.md +127 -353
- x_ipe/resources/skills/task-type-code-refactor-v2/references/refactoring-techniques.md +373 -0
- x_ipe/resources/skills/task-type-feature-breakdown/SKILL.md +31 -243
- x_ipe/resources/skills/task-type-feature-breakdown/references/breakdown-guidelines.md +330 -0
- x_ipe/resources/skills/task-type-feature-refinement/SKILL.md +27 -180
- x_ipe/resources/skills/task-type-feature-refinement/references/specification-writing-guide.md +267 -0
- x_ipe/resources/skills/task-type-idea-mockup/SKILL.md +38 -276
- x_ipe/resources/skills/task-type-idea-mockup/references/mockup-guidelines.md +299 -0
- x_ipe/resources/skills/task-type-idea-to-architecture/SKILL.md +20 -218
- x_ipe/resources/skills/task-type-idea-to-architecture/references/architecture-patterns.md +342 -0
- x_ipe/resources/skills/task-type-ideation/SKILL.md +10 -266
- x_ipe/resources/skills/task-type-ideation/references/folder-naming-guide.md +55 -0
- x_ipe/resources/skills/task-type-ideation/references/tool-usage-guide.md +236 -0
- x_ipe/resources/skills/task-type-ideation-v2/SKILL.md +488 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/examples.md +377 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/folder-naming-guide.md +74 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/tool-usage-guide.md +145 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/visualization-guide.md +160 -0
- x_ipe/resources/skills/task-type-ideation-v2/templates/idea-summary.md +86 -0
- x_ipe/resources/skills/task-type-refactoring-analysis/SKILL.md +83 -145
- x_ipe/resources/skills/task-type-refactoring-analysis/references/output-schema.md +172 -0
- x_ipe/resources/skills/task-type-technical-design/SKILL.md +28 -214
- x_ipe/resources/skills/task-type-technical-design/references/design-templates.md +422 -0
- x_ipe/resources/skills/task-type-test-generation/SKILL.md +47 -332
- x_ipe/resources/skills/task-type-test-generation/references/test-patterns.md +368 -0
- x_ipe/resources/skills/tool-tracing-creator/SKILL.md +312 -0
- x_ipe/resources/skills/tool-tracing-creator/references/examples.md +324 -0
- x_ipe/resources/skills/tool-tracing-instrumentation/SKILL.md +373 -0
- x_ipe/resources/skills/tool-tracing-instrumentation/references/examples.md +264 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/SKILL.md +486 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/10. example-gate-conditions.md +73 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/11. reference-quality-standards.md +127 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/2. reference-section-order.md +127 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/3. example-step-based-code-review.md +84 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/4. example-step-based-feature-implementation.md +113 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/5. example-function-based-validation.md +73 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/6. example-function-based-analysis.md +94 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/7. example-task-io-code-implementation.md +36 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/8. example-structured-summary.md +43 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/9. example-dor-dod.md +77 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/examples.md +429 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/skill-general-guidelines-v2.md +611 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-meta.md +153 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-based.md +324 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-category.md +109 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-tool.md +205 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-meta.md +334 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-task-based.md +279 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-tool.md +175 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-workflow-orchestration.md +329 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/SKILL.md +487 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/examples.md +377 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/folder-naming-guide.md +74 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/tool-usage-guide.md +145 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/visualization-guide.md +160 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/templates/idea-summary.md +86 -0
- x_ipe/routes/__init__.py +2 -0
- x_ipe/routes/ideas_routes.py +289 -0
- x_ipe/routes/kb_routes.py +80 -0
- x_ipe/routes/main_routes.py +18 -0
- x_ipe/routes/project_routes.py +7 -0
- x_ipe/routes/proxy_routes.py +10 -2
- x_ipe/routes/quality_evaluation_routes.py +193 -0
- x_ipe/routes/settings_routes.py +6 -0
- x_ipe/routes/tools_routes.py +6 -0
- x_ipe/routes/tracing_routes.py +232 -0
- x_ipe/routes/uiux_feedback_routes.py +50 -0
- x_ipe/services/__init__.py +5 -0
- x_ipe/services/config_service.py +6 -0
- x_ipe/services/file_service.py +20 -0
- x_ipe/services/homepage_service.py +160 -0
- x_ipe/services/ideas_service.py +535 -2
- x_ipe/services/kb_service.py +378 -0
- x_ipe/services/proxy_service.py +37 -7
- x_ipe/services/settings_service.py +13 -0
- x_ipe/services/skills_service.py +4 -0
- x_ipe/services/terminal_service.py +24 -0
- x_ipe/services/themes_service.py +4 -0
- x_ipe/services/tools_config_service.py +4 -0
- x_ipe/services/tracing_service.py +333 -0
- x_ipe/services/uiux_feedback_service.py +148 -1
- x_ipe/services/voice_input_service_v2.py +11 -0
- x_ipe/static/css/base.css +7 -0
- x_ipe/static/css/homepage-infinity.css +330 -0
- x_ipe/static/css/kb-core.css +301 -0
- x_ipe/static/css/quality-evaluation.css +345 -0
- x_ipe/static/css/sidebar.css +14 -4
- x_ipe/static/css/terminal.css +23 -0
- x_ipe/static/css/tracing-dashboard.css +796 -0
- x_ipe/static/css/uiux-feedback.css +7 -1
- x_ipe/static/css/workplace.css +636 -0
- x_ipe/static/img/homepage-infinity-loop.png +0 -0
- x_ipe/static/js/features/confirm-dialog.js +169 -0
- x_ipe/static/js/features/folder-view.js +742 -0
- x_ipe/static/js/features/homepage-infinity.js +314 -0
- x_ipe/static/js/features/kb-core.js +371 -0
- x_ipe/static/js/features/quality-evaluation.js +387 -0
- x_ipe/static/js/features/sidebar.js +255 -12
- x_ipe/static/js/features/tracing-dashboard.js +855 -0
- x_ipe/static/js/features/tracing-graph.js +1031 -0
- x_ipe/static/js/features/tree-drag.js +227 -0
- x_ipe/static/js/features/tree-search.js +228 -0
- x_ipe/static/js/features/workplace.js +661 -33
- x_ipe/static/js/init.js +76 -0
- x_ipe/static/js/terminal-v2.js +45 -14
- x_ipe/static/js/terminal.js +50 -49
- x_ipe/static/js/uiux-feedback.js +75 -16
- x_ipe/templates/base.html +24 -0
- x_ipe/templates/index.html +10 -1
- x_ipe/templates/knowledge-base.html +110 -0
- x_ipe/templates/workplace.html +4 -0
- x_ipe/tracing/__init__.py +37 -0
- x_ipe/tracing/buffer.py +135 -0
- x_ipe/tracing/context.py +125 -0
- x_ipe/tracing/decorator.py +288 -0
- x_ipe/tracing/middleware.py +197 -0
- x_ipe/tracing/parser.py +235 -0
- x_ipe/tracing/redactor.py +111 -0
- x_ipe/tracing/writer.py +122 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/METADATA +2 -2
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/RECORD +138 -65
- x_ipe/app.py.bak +0 -1333
- x_ipe/resources/skills/x-ipe-skill-creator/SKILL.md +0 -329
- x_ipe/resources/skills/x-ipe-skill-creator/references/output-patterns.md +0 -169
- x_ipe/resources/skills/x-ipe-skill-creator/references/skill-structure.md +0 -162
- x_ipe/resources/skills/x-ipe-skill-creator/references/workflows.md +0 -110
- x_ipe/resources/skills/x-ipe-skill-creator/templates/references/examples.md +0 -113
- x_ipe/resources/skills/x-ipe-skill-creator/templates/skill-category-skill.md +0 -296
- x_ipe/resources/skills/x-ipe-skill-creator/templates/task-type-skill.md +0 -269
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/WHEEL +0 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/entry_points.txt +0 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/licenses/LICENSE +0 -0
x_ipe/services/ideas_service.py
CHANGED
|
@@ -11,6 +11,8 @@ from datetime import datetime
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Dict, List, Any
|
|
13
13
|
|
|
14
|
+
from x_ipe.tracing import x_ipe_tracing
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
class IdeasService:
|
|
16
18
|
"""
|
|
@@ -48,6 +50,7 @@ class IdeasService:
|
|
|
48
50
|
self.project_root = Path(project_root).resolve()
|
|
49
51
|
self.ideas_root = self.project_root / self.IDEAS_PATH
|
|
50
52
|
|
|
53
|
+
@x_ipe_tracing()
|
|
51
54
|
def get_tree(self) -> List[Dict]:
|
|
52
55
|
"""
|
|
53
56
|
Scan x-ipe-docs/ideas/ and return tree structure.
|
|
@@ -93,6 +96,7 @@ class IdeasService:
|
|
|
93
96
|
|
|
94
97
|
return items
|
|
95
98
|
|
|
99
|
+
@x_ipe_tracing()
|
|
96
100
|
def upload(self, files: List[tuple], date: str = None, target_folder: str = None) -> Dict[str, Any]:
|
|
97
101
|
"""
|
|
98
102
|
Upload files to a new or existing idea folder.
|
|
@@ -155,6 +159,73 @@ class IdeasService:
|
|
|
155
159
|
'files_uploaded': uploaded_files
|
|
156
160
|
}
|
|
157
161
|
|
|
162
|
+
@x_ipe_tracing()
|
|
163
|
+
def create_folder(self, folder_name: str, parent_folder: str = None) -> Dict[str, Any]:
|
|
164
|
+
"""
|
|
165
|
+
Create an empty folder in ideas directory.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
folder_name: Name for the new folder
|
|
169
|
+
parent_folder: Optional parent folder path (relative to ideas root)
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dict with success, folder_name, folder_path or error
|
|
173
|
+
"""
|
|
174
|
+
# Validate folder name
|
|
175
|
+
folder_name = folder_name.strip()
|
|
176
|
+
is_valid, error = self._validate_folder_name(folder_name)
|
|
177
|
+
if not is_valid:
|
|
178
|
+
return {
|
|
179
|
+
'success': False,
|
|
180
|
+
'error': error
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Determine base path
|
|
184
|
+
if parent_folder:
|
|
185
|
+
# Strip 'x-ipe-docs/ideas/' prefix if present
|
|
186
|
+
if parent_folder.startswith(self.IDEAS_PATH + '/'):
|
|
187
|
+
parent_folder = parent_folder[len(self.IDEAS_PATH) + 1:]
|
|
188
|
+
elif parent_folder.startswith(self.IDEAS_PATH):
|
|
189
|
+
parent_folder = parent_folder[len(self.IDEAS_PATH):]
|
|
190
|
+
|
|
191
|
+
base_path = self.ideas_root / parent_folder
|
|
192
|
+
if not base_path.exists():
|
|
193
|
+
return {
|
|
194
|
+
'success': False,
|
|
195
|
+
'error': f"Parent folder '{parent_folder}' does not exist"
|
|
196
|
+
}
|
|
197
|
+
else:
|
|
198
|
+
base_path = self.ideas_root
|
|
199
|
+
|
|
200
|
+
# Ensure ideas root exists
|
|
201
|
+
self.ideas_root.mkdir(parents=True, exist_ok=True)
|
|
202
|
+
|
|
203
|
+
# Generate unique name if folder exists
|
|
204
|
+
final_name = self._generate_unique_name(folder_name, base_path)
|
|
205
|
+
|
|
206
|
+
# Create the folder
|
|
207
|
+
folder_path = base_path / final_name
|
|
208
|
+
try:
|
|
209
|
+
folder_path.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
except OSError as e:
|
|
211
|
+
return {
|
|
212
|
+
'success': False,
|
|
213
|
+
'error': f'Failed to create folder: {str(e)}'
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Build relative path for response
|
|
217
|
+
if parent_folder:
|
|
218
|
+
relative_path = f'{self.IDEAS_PATH}/{parent_folder}/{final_name}'
|
|
219
|
+
else:
|
|
220
|
+
relative_path = f'{self.IDEAS_PATH}/{final_name}'
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
'success': True,
|
|
224
|
+
'folder_name': final_name,
|
|
225
|
+
'folder_path': relative_path
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@x_ipe_tracing()
|
|
158
229
|
def rename_folder(self, old_name: str, new_name: str) -> Dict[str, Any]:
|
|
159
230
|
"""
|
|
160
231
|
Rename an idea folder.
|
|
@@ -207,6 +278,7 @@ class IdeasService:
|
|
|
207
278
|
'new_path': f'{self.IDEAS_PATH}/{final_name}'
|
|
208
279
|
}
|
|
209
280
|
|
|
281
|
+
@x_ipe_tracing()
|
|
210
282
|
def rename_file(self, file_path: str, new_name: str) -> Dict[str, Any]:
|
|
211
283
|
"""
|
|
212
284
|
Rename a file within x-ipe-docs/ideas/.
|
|
@@ -312,20 +384,28 @@ class IdeasService:
|
|
|
312
384
|
|
|
313
385
|
return (True, None)
|
|
314
386
|
|
|
315
|
-
def _generate_unique_name(self, base_name: str) -> str:
|
|
387
|
+
def _generate_unique_name(self, base_name: str, base_path: Path = None) -> str:
|
|
316
388
|
"""
|
|
317
389
|
Generate unique folder name if base_name exists.
|
|
318
390
|
Appends (2), (3), etc. until unique.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
base_name: Base name for the folder
|
|
394
|
+
base_path: Optional base path to check existence (defaults to ideas_root)
|
|
319
395
|
"""
|
|
396
|
+
if base_path is None:
|
|
397
|
+
base_path = self.ideas_root
|
|
398
|
+
|
|
320
399
|
name = base_name
|
|
321
400
|
counter = 2
|
|
322
401
|
|
|
323
|
-
while (
|
|
402
|
+
while (base_path / name).exists():
|
|
324
403
|
name = f'{base_name} ({counter})'
|
|
325
404
|
counter += 1
|
|
326
405
|
|
|
327
406
|
return name
|
|
328
407
|
|
|
408
|
+
@x_ipe_tracing()
|
|
329
409
|
def delete_item(self, path: str) -> Dict[str, Any]:
|
|
330
410
|
"""
|
|
331
411
|
Delete a file or folder within x-ipe-docs/ideas/.
|
|
@@ -386,6 +466,7 @@ class IdeasService:
|
|
|
386
466
|
'error': f'Failed to delete: {str(e)}'
|
|
387
467
|
}
|
|
388
468
|
|
|
469
|
+
@x_ipe_tracing()
|
|
389
470
|
def get_next_version_number(self, folder_path: str, base_name: str = 'idea-summary') -> int:
|
|
390
471
|
"""
|
|
391
472
|
Get the next version number for a versioned file.
|
|
@@ -414,6 +495,7 @@ class IdeasService:
|
|
|
414
495
|
|
|
415
496
|
return max_version + 1
|
|
416
497
|
|
|
498
|
+
@x_ipe_tracing()
|
|
417
499
|
def create_versioned_summary(self, folder_path: str, content: str, base_name: str = 'idea-summary') -> Dict[str, Any]:
|
|
418
500
|
"""
|
|
419
501
|
Create a new versioned idea summary file.
|
|
@@ -472,6 +554,7 @@ class IdeasService:
|
|
|
472
554
|
'error': f'Failed to create file: {str(e)}'
|
|
473
555
|
}
|
|
474
556
|
|
|
557
|
+
@x_ipe_tracing()
|
|
475
558
|
def get_toolbox(self) -> Dict:
|
|
476
559
|
"""
|
|
477
560
|
Read toolbox configuration from JSON file.
|
|
@@ -489,6 +572,7 @@ class IdeasService:
|
|
|
489
572
|
return copy.deepcopy(self.DEFAULT_TOOLBOX)
|
|
490
573
|
return copy.deepcopy(self.DEFAULT_TOOLBOX)
|
|
491
574
|
|
|
575
|
+
@x_ipe_tracing()
|
|
492
576
|
def save_toolbox(self, config: Dict) -> Dict:
|
|
493
577
|
"""
|
|
494
578
|
Save toolbox configuration to JSON file.
|
|
@@ -510,3 +594,452 @@ class IdeasService:
|
|
|
510
594
|
return {'success': True}
|
|
511
595
|
except IOError as e:
|
|
512
596
|
return {'success': False, 'error': str(e)}
|
|
597
|
+
|
|
598
|
+
# =========================================================================
|
|
599
|
+
# CR-006: Folder Tree UX Enhancement
|
|
600
|
+
# =========================================================================
|
|
601
|
+
|
|
602
|
+
@x_ipe_tracing()
|
|
603
|
+
def move_item(self, source_path: str, target_folder: str) -> Dict[str, Any]:
|
|
604
|
+
"""
|
|
605
|
+
Move file or folder to target folder.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
source_path: Path (relative to project root or ideas root)
|
|
609
|
+
target_folder: Target folder path (relative to project root or ideas root)
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
{success: bool, new_path: str, error?: str}
|
|
613
|
+
"""
|
|
614
|
+
if not source_path:
|
|
615
|
+
return {'success': False, 'error': 'Source path is required'}
|
|
616
|
+
if target_folder is None:
|
|
617
|
+
return {'success': False, 'error': 'Target folder is required'}
|
|
618
|
+
|
|
619
|
+
# Normalize source path - handle both x-ipe-docs/ideas/... and relative paths
|
|
620
|
+
if source_path.startswith(self.IDEAS_PATH + '/'):
|
|
621
|
+
source_rel = source_path[len(self.IDEAS_PATH) + 1:]
|
|
622
|
+
elif source_path.startswith(self.IDEAS_PATH):
|
|
623
|
+
source_rel = source_path[len(self.IDEAS_PATH):]
|
|
624
|
+
else:
|
|
625
|
+
source_rel = source_path
|
|
626
|
+
source_full = self.ideas_root / source_rel
|
|
627
|
+
|
|
628
|
+
# Normalize target path
|
|
629
|
+
if target_folder == '' or target_folder == self.IDEAS_PATH:
|
|
630
|
+
target_full = self.ideas_root
|
|
631
|
+
target_rel = ''
|
|
632
|
+
elif target_folder.startswith(self.IDEAS_PATH + '/'):
|
|
633
|
+
target_rel = target_folder[len(self.IDEAS_PATH) + 1:]
|
|
634
|
+
target_full = self.ideas_root / target_rel
|
|
635
|
+
elif target_folder.startswith(self.IDEAS_PATH):
|
|
636
|
+
target_rel = target_folder[len(self.IDEAS_PATH):]
|
|
637
|
+
target_full = self.ideas_root / target_rel
|
|
638
|
+
else:
|
|
639
|
+
target_rel = target_folder
|
|
640
|
+
target_full = self.ideas_root / target_folder
|
|
641
|
+
|
|
642
|
+
# Validate source exists
|
|
643
|
+
if not source_full.exists():
|
|
644
|
+
return {'success': False, 'error': f'Source not found: {source_path}'}
|
|
645
|
+
|
|
646
|
+
# Validate target exists
|
|
647
|
+
if not target_full.exists():
|
|
648
|
+
return {'success': False, 'error': f'Target folder not found: {target_folder}'}
|
|
649
|
+
|
|
650
|
+
# Validate target is a folder
|
|
651
|
+
if not target_full.is_dir():
|
|
652
|
+
return {'success': False, 'error': 'Target is not a folder'}
|
|
653
|
+
|
|
654
|
+
# Validate not moving into self or child (use normalized paths)
|
|
655
|
+
if not self.is_valid_drop_target(source_rel, target_rel):
|
|
656
|
+
return {'success': False, 'error': 'Cannot move folder into itself or its children'}
|
|
657
|
+
|
|
658
|
+
# Determine destination path
|
|
659
|
+
dest_path = target_full / source_full.name
|
|
660
|
+
|
|
661
|
+
# Handle name collision
|
|
662
|
+
if dest_path.exists():
|
|
663
|
+
dest_path = self._get_unique_path(dest_path)
|
|
664
|
+
|
|
665
|
+
try:
|
|
666
|
+
shutil.move(str(source_full), str(dest_path))
|
|
667
|
+
# Return path relative to ideas root
|
|
668
|
+
new_path = str(dest_path.relative_to(self.ideas_root))
|
|
669
|
+
return {'success': True, 'new_path': new_path}
|
|
670
|
+
except OSError as e:
|
|
671
|
+
return {'success': False, 'error': f'Failed to move: {str(e)}'}
|
|
672
|
+
|
|
673
|
+
@x_ipe_tracing()
|
|
674
|
+
def duplicate_item(self, path: str) -> Dict[str, Any]:
|
|
675
|
+
"""
|
|
676
|
+
Duplicate file or folder with -copy suffix.
|
|
677
|
+
|
|
678
|
+
Creates: filename-copy.ext or foldername-copy/
|
|
679
|
+
If exists: filename-copy-2.ext, etc.
|
|
680
|
+
"""
|
|
681
|
+
if not path:
|
|
682
|
+
return {'success': False, 'error': 'Path is required'}
|
|
683
|
+
|
|
684
|
+
# Normalize path
|
|
685
|
+
if path.startswith(self.IDEAS_PATH + '/'):
|
|
686
|
+
path_rel = path[len(self.IDEAS_PATH) + 1:]
|
|
687
|
+
elif path.startswith(self.IDEAS_PATH):
|
|
688
|
+
path_rel = path[len(self.IDEAS_PATH):]
|
|
689
|
+
else:
|
|
690
|
+
path_rel = path
|
|
691
|
+
|
|
692
|
+
full_path = self.ideas_root / path_rel
|
|
693
|
+
|
|
694
|
+
if not full_path.exists():
|
|
695
|
+
return {'success': False, 'error': f'Path not found: {path}'}
|
|
696
|
+
|
|
697
|
+
# Validate path is within ideas directory
|
|
698
|
+
try:
|
|
699
|
+
resolved_path = full_path.resolve()
|
|
700
|
+
ideas_resolved = self.ideas_root.resolve()
|
|
701
|
+
|
|
702
|
+
if not str(resolved_path).startswith(str(ideas_resolved)):
|
|
703
|
+
return {'success': False, 'error': 'Path must be within x-ipe-docs/ideas/'}
|
|
704
|
+
except Exception:
|
|
705
|
+
return {'success': False, 'error': 'Invalid path'}
|
|
706
|
+
|
|
707
|
+
# Generate copy name
|
|
708
|
+
if full_path.is_file():
|
|
709
|
+
stem = full_path.stem
|
|
710
|
+
suffix = full_path.suffix
|
|
711
|
+
copy_name = f"{stem}-copy{suffix}"
|
|
712
|
+
copy_path = full_path.parent / copy_name
|
|
713
|
+
|
|
714
|
+
# Handle collisions
|
|
715
|
+
counter = 2
|
|
716
|
+
while copy_path.exists():
|
|
717
|
+
copy_name = f"{stem}-copy-{counter}{suffix}"
|
|
718
|
+
copy_path = full_path.parent / copy_name
|
|
719
|
+
counter += 1
|
|
720
|
+
|
|
721
|
+
try:
|
|
722
|
+
shutil.copy2(str(full_path), str(copy_path))
|
|
723
|
+
except OSError as e:
|
|
724
|
+
return {'success': False, 'error': f'Failed to duplicate: {str(e)}'}
|
|
725
|
+
else:
|
|
726
|
+
# Folder
|
|
727
|
+
copy_name = f"{full_path.name}-copy"
|
|
728
|
+
copy_path = full_path.parent / copy_name
|
|
729
|
+
|
|
730
|
+
# Handle collisions
|
|
731
|
+
counter = 2
|
|
732
|
+
while copy_path.exists():
|
|
733
|
+
copy_name = f"{full_path.name}-copy-{counter}"
|
|
734
|
+
copy_path = full_path.parent / copy_name
|
|
735
|
+
counter += 1
|
|
736
|
+
|
|
737
|
+
try:
|
|
738
|
+
shutil.copytree(str(full_path), str(copy_path))
|
|
739
|
+
except OSError as e:
|
|
740
|
+
return {'success': False, 'error': f'Failed to duplicate: {str(e)}'}
|
|
741
|
+
|
|
742
|
+
# Return path relative to ideas root
|
|
743
|
+
new_path = str(copy_path.relative_to(self.ideas_root))
|
|
744
|
+
return {'success': True, 'new_path': new_path}
|
|
745
|
+
|
|
746
|
+
@x_ipe_tracing()
|
|
747
|
+
def get_folder_contents(self, folder_path: str) -> Dict[str, Any]:
|
|
748
|
+
"""
|
|
749
|
+
Get contents of a specific folder for folder view panel.
|
|
750
|
+
|
|
751
|
+
Args:
|
|
752
|
+
folder_path: Path (relative to project root or ideas root)
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
{success: bool, items: [...], error?: str}
|
|
756
|
+
"""
|
|
757
|
+
# Normalize path
|
|
758
|
+
if not folder_path:
|
|
759
|
+
folder_path_rel = ''
|
|
760
|
+
elif folder_path.startswith(self.IDEAS_PATH + '/'):
|
|
761
|
+
folder_path_rel = folder_path[len(self.IDEAS_PATH) + 1:]
|
|
762
|
+
elif folder_path.startswith(self.IDEAS_PATH):
|
|
763
|
+
folder_path_rel = folder_path[len(self.IDEAS_PATH):]
|
|
764
|
+
else:
|
|
765
|
+
folder_path_rel = folder_path
|
|
766
|
+
|
|
767
|
+
if folder_path_rel:
|
|
768
|
+
full_path = self.ideas_root / folder_path_rel
|
|
769
|
+
else:
|
|
770
|
+
full_path = self.ideas_root
|
|
771
|
+
|
|
772
|
+
if not full_path.exists():
|
|
773
|
+
return {'success': False, 'error': f'Folder not found: {folder_path}'}
|
|
774
|
+
|
|
775
|
+
if not full_path.is_dir():
|
|
776
|
+
return {'success': False, 'error': 'Path is not a folder'}
|
|
777
|
+
|
|
778
|
+
# Validate path is within ideas directory
|
|
779
|
+
try:
|
|
780
|
+
resolved_path = full_path.resolve()
|
|
781
|
+
ideas_resolved = self.ideas_root.resolve()
|
|
782
|
+
|
|
783
|
+
if not str(resolved_path).startswith(str(ideas_resolved)):
|
|
784
|
+
return {'success': False, 'error': 'Path must be within x-ipe-docs/ideas/'}
|
|
785
|
+
except Exception:
|
|
786
|
+
return {'success': False, 'error': 'Invalid path'}
|
|
787
|
+
|
|
788
|
+
items = []
|
|
789
|
+
try:
|
|
790
|
+
for entry in sorted(full_path.iterdir()):
|
|
791
|
+
if entry.name.startswith('.'):
|
|
792
|
+
continue
|
|
793
|
+
|
|
794
|
+
# Return path relative to project root (consistent with get_tree)
|
|
795
|
+
relative_path = str(entry.relative_to(self.project_root))
|
|
796
|
+
item = {
|
|
797
|
+
'name': entry.name,
|
|
798
|
+
'type': 'folder' if entry.is_dir() else 'file',
|
|
799
|
+
'path': relative_path
|
|
800
|
+
}
|
|
801
|
+
if entry.is_dir():
|
|
802
|
+
item['children'] = [] # Lazy load
|
|
803
|
+
items.append(item)
|
|
804
|
+
except PermissionError:
|
|
805
|
+
return {'success': False, 'error': 'Permission denied'}
|
|
806
|
+
|
|
807
|
+
# Return folder_path relative to project root
|
|
808
|
+
folder_path_result = str(full_path.relative_to(self.project_root)) if full_path != self.ideas_root else self.IDEAS_PATH
|
|
809
|
+
return {'success': True, 'items': items, 'folder_path': folder_path_result}
|
|
810
|
+
|
|
811
|
+
@x_ipe_tracing()
|
|
812
|
+
def is_valid_drop_target(self, source_path: str, target_folder: str) -> bool:
|
|
813
|
+
"""
|
|
814
|
+
Validate that target is not source or child of source.
|
|
815
|
+
|
|
816
|
+
Args:
|
|
817
|
+
source_path: Path of item being dragged (relative to ideas root)
|
|
818
|
+
target_folder: Path of drop target folder (relative to ideas root)
|
|
819
|
+
"""
|
|
820
|
+
if not source_path:
|
|
821
|
+
return False
|
|
822
|
+
|
|
823
|
+
# Normalize paths (remove x-ipe-docs/ideas/ prefix if present)
|
|
824
|
+
if source_path.startswith(self.IDEAS_PATH + '/'):
|
|
825
|
+
source_norm = source_path[len(self.IDEAS_PATH) + 1:].rstrip('/')
|
|
826
|
+
elif source_path.startswith(self.IDEAS_PATH):
|
|
827
|
+
source_norm = source_path[len(self.IDEAS_PATH):].lstrip('/').rstrip('/')
|
|
828
|
+
else:
|
|
829
|
+
source_norm = source_path.rstrip('/')
|
|
830
|
+
|
|
831
|
+
if not target_folder:
|
|
832
|
+
return True # Root is always valid
|
|
833
|
+
|
|
834
|
+
if target_folder.startswith(self.IDEAS_PATH + '/'):
|
|
835
|
+
target_norm = target_folder[len(self.IDEAS_PATH) + 1:].rstrip('/')
|
|
836
|
+
elif target_folder.startswith(self.IDEAS_PATH):
|
|
837
|
+
target_norm = target_folder[len(self.IDEAS_PATH):].lstrip('/').rstrip('/')
|
|
838
|
+
else:
|
|
839
|
+
target_norm = target_folder.rstrip('/')
|
|
840
|
+
|
|
841
|
+
# Cannot drop onto self
|
|
842
|
+
if source_norm == target_norm:
|
|
843
|
+
return False
|
|
844
|
+
|
|
845
|
+
# Check if source is a folder
|
|
846
|
+
source_full = self.ideas_root / source_norm
|
|
847
|
+
if not source_full.exists() or not source_full.is_dir():
|
|
848
|
+
return True # Files can be dropped anywhere, non-existent is handled elsewhere
|
|
849
|
+
|
|
850
|
+
# Cannot drop folder into its own children
|
|
851
|
+
if target_norm.startswith(source_norm + '/'):
|
|
852
|
+
return False
|
|
853
|
+
|
|
854
|
+
return True
|
|
855
|
+
|
|
856
|
+
@x_ipe_tracing()
|
|
857
|
+
def filter_tree(self, query: str) -> List[Dict]:
|
|
858
|
+
"""
|
|
859
|
+
Filter tree by search query, returning matching items with parent context.
|
|
860
|
+
|
|
861
|
+
Args:
|
|
862
|
+
query: Search string to match against item names
|
|
863
|
+
|
|
864
|
+
Returns:
|
|
865
|
+
Flat list of matching items and their parent folders
|
|
866
|
+
"""
|
|
867
|
+
if not query or not query.strip():
|
|
868
|
+
return self.get_tree()
|
|
869
|
+
|
|
870
|
+
query_lower = query.lower().strip()
|
|
871
|
+
tree = self.get_tree()
|
|
872
|
+
|
|
873
|
+
# Collect all matching items plus their parents
|
|
874
|
+
results = []
|
|
875
|
+
self._collect_matches(tree, query_lower, results)
|
|
876
|
+
return results
|
|
877
|
+
|
|
878
|
+
def _collect_matches(self, items: List[Dict], query: str, results: List[Dict], include_all: bool = False) -> bool:
|
|
879
|
+
"""Recursively collect matching items and parents into flat results list.
|
|
880
|
+
|
|
881
|
+
Returns True if any child matches (to include parent in results).
|
|
882
|
+
"""
|
|
883
|
+
any_match = False
|
|
884
|
+
|
|
885
|
+
for item in items:
|
|
886
|
+
item_copy = copy.copy(item)
|
|
887
|
+
name_matches = query in item['name'].lower()
|
|
888
|
+
|
|
889
|
+
if item['type'] == 'folder' and 'children' in item:
|
|
890
|
+
# Check if any children match
|
|
891
|
+
child_results = []
|
|
892
|
+
has_child_match = self._collect_matches(item['children'], query, child_results, include_all=name_matches)
|
|
893
|
+
|
|
894
|
+
if name_matches or has_child_match:
|
|
895
|
+
item_copy['_matches'] = name_matches
|
|
896
|
+
item_copy['children'] = [] # Don't include nested children in flat result
|
|
897
|
+
results.append(item_copy)
|
|
898
|
+
results.extend(child_results)
|
|
899
|
+
any_match = True
|
|
900
|
+
else:
|
|
901
|
+
# File - include if name matches or parent matched
|
|
902
|
+
if name_matches or include_all:
|
|
903
|
+
item_copy['_matches'] = name_matches
|
|
904
|
+
results.append(item_copy)
|
|
905
|
+
if name_matches:
|
|
906
|
+
any_match = True
|
|
907
|
+
|
|
908
|
+
return any_match
|
|
909
|
+
|
|
910
|
+
@x_ipe_tracing()
|
|
911
|
+
def get_download_info(self, path: str) -> Dict[str, Any]:
|
|
912
|
+
"""
|
|
913
|
+
Get file content and mime type for download.
|
|
914
|
+
|
|
915
|
+
Args:
|
|
916
|
+
path: Path (relative to project root or ideas root)
|
|
917
|
+
|
|
918
|
+
Returns:
|
|
919
|
+
{success: bool, content: bytes, filename: str, mime_type: str}
|
|
920
|
+
"""
|
|
921
|
+
if not path:
|
|
922
|
+
return {'success': False, 'error': 'Path is required'}
|
|
923
|
+
|
|
924
|
+
# Normalize path
|
|
925
|
+
if path.startswith(self.IDEAS_PATH + '/'):
|
|
926
|
+
path_rel = path[len(self.IDEAS_PATH) + 1:]
|
|
927
|
+
elif path.startswith(self.IDEAS_PATH):
|
|
928
|
+
path_rel = path[len(self.IDEAS_PATH):]
|
|
929
|
+
else:
|
|
930
|
+
path_rel = path
|
|
931
|
+
|
|
932
|
+
full_path = self.ideas_root / path_rel
|
|
933
|
+
|
|
934
|
+
if not full_path.exists():
|
|
935
|
+
return {'success': False, 'error': f'File not found: {path}'}
|
|
936
|
+
|
|
937
|
+
if not full_path.is_file():
|
|
938
|
+
return {'success': False, 'error': 'Cannot download a folder'}
|
|
939
|
+
|
|
940
|
+
# Validate path is within ideas directory
|
|
941
|
+
try:
|
|
942
|
+
resolved_path = full_path.resolve()
|
|
943
|
+
ideas_resolved = self.ideas_root.resolve()
|
|
944
|
+
|
|
945
|
+
if not str(resolved_path).startswith(str(ideas_resolved)):
|
|
946
|
+
return {'success': False, 'error': 'Path must be within x-ipe-docs/ideas/'}
|
|
947
|
+
except Exception:
|
|
948
|
+
return {'success': False, 'error': 'Invalid path'}
|
|
949
|
+
|
|
950
|
+
# Determine mime type
|
|
951
|
+
suffix = full_path.suffix.lower()
|
|
952
|
+
mime_types = {
|
|
953
|
+
'.md': 'text/markdown',
|
|
954
|
+
'.txt': 'text/plain',
|
|
955
|
+
'.json': 'application/json',
|
|
956
|
+
'.html': 'text/html',
|
|
957
|
+
'.pdf': 'application/pdf',
|
|
958
|
+
'.png': 'image/png',
|
|
959
|
+
'.jpg': 'image/jpeg',
|
|
960
|
+
'.jpeg': 'image/jpeg',
|
|
961
|
+
'.gif': 'image/gif',
|
|
962
|
+
}
|
|
963
|
+
mime_type = mime_types.get(suffix, 'application/octet-stream')
|
|
964
|
+
|
|
965
|
+
try:
|
|
966
|
+
content = full_path.read_bytes()
|
|
967
|
+
# For text files, decode to string for easier testing
|
|
968
|
+
text_types = ['.md', '.txt', '.json', '.html', '.css', '.js']
|
|
969
|
+
if suffix in text_types:
|
|
970
|
+
try:
|
|
971
|
+
content = content.decode('utf-8')
|
|
972
|
+
except UnicodeDecodeError:
|
|
973
|
+
pass # Keep as bytes if decode fails
|
|
974
|
+
return {
|
|
975
|
+
'success': True,
|
|
976
|
+
'content': content,
|
|
977
|
+
'filename': full_path.name,
|
|
978
|
+
'mime_type': mime_type
|
|
979
|
+
}
|
|
980
|
+
except OSError as e:
|
|
981
|
+
return {'success': False, 'error': f'Failed to read file: {str(e)}'}
|
|
982
|
+
|
|
983
|
+
@x_ipe_tracing()
|
|
984
|
+
def get_delete_info(self, path: str) -> Dict[str, Any]:
|
|
985
|
+
"""
|
|
986
|
+
Get item info for delete confirmation dialog.
|
|
987
|
+
|
|
988
|
+
Returns item type and count of children for folders.
|
|
989
|
+
"""
|
|
990
|
+
if not path:
|
|
991
|
+
return {'success': False, 'error': 'Path is required'}
|
|
992
|
+
|
|
993
|
+
# Normalize path
|
|
994
|
+
if path.startswith(self.IDEAS_PATH + '/'):
|
|
995
|
+
path_rel = path[len(self.IDEAS_PATH) + 1:]
|
|
996
|
+
elif path.startswith(self.IDEAS_PATH):
|
|
997
|
+
path_rel = path[len(self.IDEAS_PATH):]
|
|
998
|
+
else:
|
|
999
|
+
path_rel = path
|
|
1000
|
+
|
|
1001
|
+
full_path = self.ideas_root / path_rel
|
|
1002
|
+
|
|
1003
|
+
if not full_path.exists():
|
|
1004
|
+
return {'success': False, 'error': f'Path not found: {path}'}
|
|
1005
|
+
|
|
1006
|
+
item_type = 'folder' if full_path.is_dir() else 'file'
|
|
1007
|
+
item_count = 1
|
|
1008
|
+
|
|
1009
|
+
if full_path.is_dir():
|
|
1010
|
+
# Count all items recursively
|
|
1011
|
+
item_count = sum(1 for _ in full_path.rglob('*') if not _.name.startswith('.'))
|
|
1012
|
+
|
|
1013
|
+
return {
|
|
1014
|
+
'success': True,
|
|
1015
|
+
'path': path,
|
|
1016
|
+
'name': full_path.name,
|
|
1017
|
+
'type': item_type,
|
|
1018
|
+
'item_count': item_count
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
def _get_unique_path(self, path: Path) -> Path:
|
|
1022
|
+
"""Generate unique path if target exists."""
|
|
1023
|
+
if not path.exists():
|
|
1024
|
+
return path
|
|
1025
|
+
|
|
1026
|
+
parent = path.parent
|
|
1027
|
+
if path.is_file() or not path.exists():
|
|
1028
|
+
stem = path.stem
|
|
1029
|
+
suffix = path.suffix
|
|
1030
|
+
counter = 2
|
|
1031
|
+
while True:
|
|
1032
|
+
new_name = f"{stem}-{counter}{suffix}"
|
|
1033
|
+
new_path = parent / new_name
|
|
1034
|
+
if not new_path.exists():
|
|
1035
|
+
return new_path
|
|
1036
|
+
counter += 1
|
|
1037
|
+
else:
|
|
1038
|
+
name = path.name
|
|
1039
|
+
counter = 2
|
|
1040
|
+
while True:
|
|
1041
|
+
new_name = f"{name}-{counter}"
|
|
1042
|
+
new_path = parent / new_name
|
|
1043
|
+
if not new_path.exists():
|
|
1044
|
+
return new_path
|
|
1045
|
+
counter += 1
|