x-ipe 1.0.16__py3-none-any.whl → 1.0.17__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 +80 -1236
- x_ipe/app.py.bak +1333 -0
- x_ipe/handlers/__init__.py +15 -0
- x_ipe/handlers/terminal_handlers.py +121 -0
- x_ipe/handlers/voice_handlers.py +179 -0
- x_ipe/resources/copilot-instructions.md +4 -3
- x_ipe/resources/skills/task-execution-guideline/SKILL.md +8 -5
- x_ipe/resources/skills/task-type-code-refactor-v2/SKILL.md +33 -0
- x_ipe/resources/skills/task-type-feature-breakdown/SKILL.md +29 -11
- x_ipe/resources/skills/task-type-feature-refinement/SKILL.md +165 -18
- x_ipe/resources/skills/task-type-idea-mockup/SKILL.md +9 -0
- x_ipe/resources/skills/task-type-refactoring-analysis/SKILL.md +57 -0
- x_ipe/resources/skills/task-type-requirement-gathering/SKILL.md +3 -3
- x_ipe/resources/skills/task-type-technical-design/SKILL.md +56 -9
- x_ipe/routes/__init__.py +21 -0
- x_ipe/routes/ideas_routes.py +297 -0
- x_ipe/routes/main_routes.py +186 -0
- x_ipe/routes/project_routes.py +189 -0
- x_ipe/routes/proxy_routes.py +66 -0
- x_ipe/routes/settings_routes.py +137 -0
- x_ipe/routes/tools_routes.py +192 -0
- x_ipe/routes/uiux_feedback_routes.py +57 -0
- x_ipe/services/__init__.py +6 -0
- x_ipe/services/proxy_service.py +347 -0
- x_ipe/services/uiux_feedback_service.py +137 -0
- x_ipe/static/3rdparty/css/bootstrap-icons.css +2078 -0
- x_ipe/static/3rdparty/css/bootstrap.min.css +6 -0
- x_ipe/static/3rdparty/css/codemirror-dialog.min.css +8 -0
- x_ipe/static/3rdparty/css/codemirror.min.css +8 -0
- x_ipe/static/3rdparty/css/easymde.min.css +7 -0
- x_ipe/static/3rdparty/css/highlight-github.min.css +10 -0
- x_ipe/static/3rdparty/css/xterm.css +209 -0
- x_ipe/static/3rdparty/fonts/bootstrap-icons.woff +0 -0
- x_ipe/static/3rdparty/fonts/bootstrap-icons.woff2 +0 -0
- x_ipe/static/3rdparty/js/bootstrap.bundle.min.js +7 -0
- x_ipe/static/3rdparty/js/codemirror-dialog.min.js +8 -0
- x_ipe/static/3rdparty/js/codemirror-jump-to-line.min.js +8 -0
- x_ipe/static/3rdparty/js/codemirror-search.min.js +8 -0
- x_ipe/static/3rdparty/js/codemirror-searchcursor.min.js +8 -0
- x_ipe/static/3rdparty/js/codemirror.min.js +8 -0
- x_ipe/static/3rdparty/js/easymde.min.js +7 -0
- x_ipe/static/3rdparty/js/highlight.min.js +1213 -0
- x_ipe/static/3rdparty/js/infographic.min.js +589 -0
- x_ipe/static/3rdparty/js/marked.min.js +6 -0
- x_ipe/static/3rdparty/js/mermaid.min.js +1646 -0
- x_ipe/static/3rdparty/js/socket.io.min.js +7 -0
- x_ipe/static/3rdparty/js/xterm-addon-fit.js +2 -0
- x_ipe/static/3rdparty/js/xterm-addon-unicode11.js +2 -0
- x_ipe/static/3rdparty/js/xterm.min.js +8 -0
- x_ipe/static/css/sidebar.css +45 -0
- x_ipe/static/css/uiux-feedback.css +933 -0
- x_ipe/static/js/features/sidebar.js +78 -12
- x_ipe/static/js/features/voice-input.js +1 -0
- x_ipe/static/js/features/workplace.js +41 -0
- x_ipe/static/js/uiux-feedback.js +1377 -0
- x_ipe/templates/base.html +30 -39
- x_ipe/templates/settings.html +5 -6
- x_ipe/templates/uiux-feedbacks.html +87 -0
- x_ipe/templates/workplace.html +114 -0
- {x_ipe-1.0.16.dist-info → x_ipe-1.0.17.dist-info}/METADATA +2 -1
- {x_ipe-1.0.16.dist-info → x_ipe-1.0.17.dist-info}/RECORD +64 -25
- x_ipe/resources/skills/task-type-code-refactor/SKILL.md +0 -602
- x_ipe/resources/skills/task-type-code-refactor/references/examples.md +0 -206
- x_ipe/resources/skills/task-type-code-refactor/references/refactoring-principles.md +0 -200
- {x_ipe-1.0.16.dist-info → x_ipe-1.0.17.dist-info}/WHEEL +0 -0
- {x_ipe-1.0.16.dist-info → x_ipe-1.0.17.dist-info}/entry_points.txt +0 -0
- {x_ipe-1.0.16.dist-info → x_ipe-1.0.17.dist-info}/licenses/LICENSE +0 -0
x_ipe/app.py
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Flask Application for
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
FEATURE-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
2
|
+
Flask Application Factory for X-IPE
|
|
3
|
+
|
|
4
|
+
This module provides the application factory pattern for creating Flask apps.
|
|
5
|
+
Route handling is delegated to Blueprint modules in x_ipe.routes.
|
|
6
|
+
WebSocket handling is delegated to handler modules in x_ipe.handlers.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- FEATURE-001: Project Navigation (via routes/main_routes.py)
|
|
10
|
+
- FEATURE-003: Content Editor (via routes/main_routes.py)
|
|
11
|
+
- FEATURE-005: Interactive Console (via handlers/terminal_handlers.py)
|
|
12
|
+
- FEATURE-006: Settings & Projects (via routes/settings_routes.py, project_routes.py)
|
|
13
|
+
- FEATURE-008: Workplace (via routes/ideas_routes.py)
|
|
14
|
+
- FEATURE-010: Project Config (via routes/settings_routes.py)
|
|
15
|
+
- FEATURE-011: Stage Toolbox (via routes/tools_routes.py)
|
|
16
|
+
- FEATURE-012: Design Themes (via routes/tools_routes.py)
|
|
17
|
+
- FEATURE-021: Voice Input (via handlers/voice_handlers.py)
|
|
13
18
|
"""
|
|
14
19
|
import os
|
|
15
|
-
import sys
|
|
16
|
-
import json
|
|
17
20
|
from pathlib import Path
|
|
18
|
-
from flask import Flask
|
|
19
|
-
from flask_socketio import SocketIO
|
|
21
|
+
from flask import Flask
|
|
22
|
+
from flask_socketio import SocketIO
|
|
23
|
+
|
|
24
|
+
from x_ipe.services import SettingsService, ProjectFoldersService, ConfigService
|
|
25
|
+
from x_ipe.services import session_manager
|
|
26
|
+
from x_ipe.config import config_by_name
|
|
27
|
+
|
|
20
28
|
|
|
21
|
-
# Load .env from config folder if it exists
|
|
22
29
|
def load_env_file():
|
|
23
30
|
"""Load environment variables from x-ipe-docs/config/.env file."""
|
|
24
31
|
env_path = Path(__file__).parent.parent.parent / 'x-ipe-docs' / 'config' / '.env'
|
|
@@ -30,49 +37,27 @@ def load_env_file():
|
|
|
30
37
|
key, value = line.split('=', 1)
|
|
31
38
|
key = key.strip()
|
|
32
39
|
value = value.strip()
|
|
33
|
-
if value and key not in os.environ:
|
|
40
|
+
if value and key not in os.environ:
|
|
34
41
|
os.environ[key] = value
|
|
35
42
|
|
|
36
|
-
load_env_file()
|
|
37
|
-
|
|
38
|
-
from x_ipe.services import ProjectService, ContentService, SettingsService, ProjectFoldersService, IdeasService, ConfigService, SkillsService, ToolsConfigService, ThemesService
|
|
39
|
-
from x_ipe.services import session_manager
|
|
40
|
-
from x_ipe.config import config_by_name
|
|
41
|
-
|
|
42
|
-
# Global settings service instance
|
|
43
|
-
settings_service = None
|
|
44
|
-
|
|
45
|
-
# Global ideas service instance (FEATURE-008)
|
|
46
|
-
ideas_service = None
|
|
47
43
|
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# Global config service instance (FEATURE-010)
|
|
52
|
-
config_service = None
|
|
53
|
-
|
|
54
|
-
# Global tools config service instance (FEATURE-011)
|
|
55
|
-
tools_config_service = None
|
|
44
|
+
# Load environment variables on module import
|
|
45
|
+
load_env_file()
|
|
56
46
|
|
|
57
47
|
# Socket.IO instance with ping/pong for keep-alive
|
|
58
|
-
# Increased timeouts for stability - session stays open for 1 hour regardless of focus
|
|
59
48
|
socketio = SocketIO(
|
|
60
49
|
cors_allowed_origins="*",
|
|
61
50
|
async_mode='threading',
|
|
62
|
-
ping_timeout=300,
|
|
63
|
-
ping_interval=60,
|
|
64
|
-
max_http_buffer_size=1e8,
|
|
65
|
-
always_connect=True,
|
|
51
|
+
ping_timeout=300,
|
|
52
|
+
ping_interval=60,
|
|
53
|
+
max_http_buffer_size=1e8,
|
|
54
|
+
always_connect=True,
|
|
66
55
|
logger=False,
|
|
67
56
|
engineio_logger=False,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
manage_session=True, # Let Socket.IO manage sessions
|
|
57
|
+
http_compression=True,
|
|
58
|
+
manage_session=True,
|
|
71
59
|
)
|
|
72
60
|
|
|
73
|
-
# Socket SID to Session ID mapping
|
|
74
|
-
socket_to_session = {}
|
|
75
|
-
|
|
76
61
|
|
|
77
62
|
def create_app(config=None):
|
|
78
63
|
"""
|
|
@@ -80,6 +65,9 @@ def create_app(config=None):
|
|
|
80
65
|
|
|
81
66
|
Args:
|
|
82
67
|
config: Configuration dict or config class name
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Flask application instance
|
|
83
71
|
"""
|
|
84
72
|
app = Flask(__name__,
|
|
85
73
|
static_folder='static',
|
|
@@ -94,1220 +82,76 @@ def create_app(config=None):
|
|
|
94
82
|
else:
|
|
95
83
|
app.config.from_object(config)
|
|
96
84
|
|
|
97
|
-
# Initialize
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
# Initialize services
|
|
86
|
+
_init_services(app)
|
|
87
|
+
|
|
88
|
+
# Register Blueprints
|
|
89
|
+
_register_blueprints(app)
|
|
90
|
+
|
|
91
|
+
# Initialize Socket.IO
|
|
92
|
+
socketio.init_app(app)
|
|
93
|
+
|
|
94
|
+
# Register WebSocket handlers
|
|
95
|
+
_register_handlers()
|
|
96
|
+
|
|
97
|
+
return app
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _init_services(app):
|
|
101
|
+
"""Initialize application services and store in app.config."""
|
|
102
|
+
db_path = app.config.get(
|
|
103
|
+
'SETTINGS_DB_PATH',
|
|
104
|
+
app.config.get('SETTINGS_DB', os.path.join(app.instance_path, 'settings.db'))
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Create service instances
|
|
100
108
|
settings_service = SettingsService(db_path)
|
|
101
109
|
project_folders_service = ProjectFoldersService(db_path)
|
|
102
110
|
|
|
111
|
+
# Store services in app.config for Blueprint access
|
|
112
|
+
app.config['SETTINGS_SERVICE'] = settings_service
|
|
113
|
+
app.config['PROJECT_FOLDERS_SERVICE'] = project_folders_service
|
|
114
|
+
|
|
103
115
|
# Initialize config service and load .x-ipe.yaml (FEATURE-010)
|
|
104
116
|
if not app.config.get('TESTING'):
|
|
105
117
|
config_service = ConfigService()
|
|
106
118
|
config_data = config_service.load()
|
|
107
119
|
if config_data:
|
|
108
120
|
app.config['X_IPE_CONFIG'] = config_data
|
|
109
|
-
# Always use config's project_root when .x-ipe.yaml is detected
|
|
110
121
|
app.config['PROJECT_ROOT'] = config_data.get_file_tree_path()
|
|
111
122
|
|
|
112
123
|
# Apply project_root from settings only if no .x-ipe.yaml detected
|
|
113
124
|
if not app.config.get('X_IPE_CONFIG'):
|
|
114
125
|
saved_root = settings_service.get('project_root')
|
|
115
126
|
if saved_root and saved_root != '.' and not app.config.get('TESTING'):
|
|
116
|
-
# Only apply if it's a valid path
|
|
117
127
|
if os.path.exists(saved_root) and os.path.isdir(saved_root):
|
|
118
128
|
app.config['PROJECT_ROOT'] = saved_root
|
|
119
|
-
|
|
120
|
-
# Register routes
|
|
121
|
-
register_routes(app)
|
|
122
|
-
register_settings_routes(app)
|
|
123
|
-
register_project_routes(app)
|
|
124
|
-
register_ideas_routes(app) # FEATURE-008
|
|
125
|
-
register_tools_config_routes(app) # FEATURE-011
|
|
126
|
-
|
|
127
|
-
# Initialize Socket.IO with the app
|
|
128
|
-
socketio.init_app(app)
|
|
129
|
-
|
|
130
|
-
# Register WebSocket handlers for terminal
|
|
131
|
-
register_terminal_handlers()
|
|
132
|
-
|
|
133
|
-
# Register WebSocket handlers for voice input (FEATURE-021)
|
|
134
|
-
register_voice_handlers()
|
|
135
|
-
|
|
136
|
-
return app
|
|
137
129
|
|
|
138
130
|
|
|
139
|
-
def
|
|
140
|
-
"""Register all
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def index():
|
|
144
|
-
"""Serve main page with sidebar navigation"""
|
|
145
|
-
return render_template('index.html')
|
|
146
|
-
|
|
147
|
-
@app.route('/api/project/structure')
|
|
148
|
-
def get_project_structure():
|
|
149
|
-
"""
|
|
150
|
-
GET /api/project/structure
|
|
151
|
-
|
|
152
|
-
Returns the project folder structure for sidebar navigation.
|
|
153
|
-
"""
|
|
154
|
-
project_root = app.config.get('PROJECT_ROOT')
|
|
155
|
-
|
|
156
|
-
if not project_root or not os.path.exists(project_root):
|
|
157
|
-
return jsonify({
|
|
158
|
-
'error': 'Project root not configured or does not exist',
|
|
159
|
-
'project_root': project_root
|
|
160
|
-
}), 400
|
|
161
|
-
|
|
162
|
-
service = ProjectService(project_root)
|
|
163
|
-
structure = service.get_structure()
|
|
164
|
-
|
|
165
|
-
return jsonify(structure)
|
|
131
|
+
def _register_blueprints(app):
|
|
132
|
+
"""Register all Flask Blueprints."""
|
|
133
|
+
from x_ipe.routes import main_bp, settings_bp, project_bp, ideas_bp, tools_bp, proxy_bp
|
|
134
|
+
from x_ipe.routes.uiux_feedback_routes import uiux_feedback_bp
|
|
166
135
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
"""
|
|
175
|
-
from pathlib import Path
|
|
176
|
-
|
|
177
|
-
file_path = request.args.get('path')
|
|
178
|
-
raw = request.args.get('raw', 'false').lower() == 'true'
|
|
179
|
-
|
|
180
|
-
if not file_path:
|
|
181
|
-
return jsonify({'error': 'Path parameter required'}), 400
|
|
182
|
-
|
|
183
|
-
project_root = app.config.get('PROJECT_ROOT')
|
|
184
|
-
|
|
185
|
-
# Binary file extensions that should always be served raw
|
|
186
|
-
binary_extensions = {
|
|
187
|
-
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp',
|
|
188
|
-
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
189
|
-
'.zip', '.rar', '.tar', '.gz', '.7z',
|
|
190
|
-
'.mp3', '.mp4', '.wav', '.avi', '.mov',
|
|
191
|
-
'.exe', '.dll', '.so', '.dylib',
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
try:
|
|
195
|
-
# Resolve path
|
|
196
|
-
full_path = (Path(project_root) / file_path).resolve()
|
|
197
|
-
|
|
198
|
-
# Security check: ensure path is within project root
|
|
199
|
-
if not str(full_path).startswith(str(Path(project_root).resolve())):
|
|
200
|
-
return jsonify({'error': 'Access denied'}), 403
|
|
201
|
-
|
|
202
|
-
if not full_path.exists():
|
|
203
|
-
return jsonify({'error': 'File not found'}), 404
|
|
204
|
-
|
|
205
|
-
ext = full_path.suffix.lower()
|
|
206
|
-
|
|
207
|
-
# Auto-detect binary files or use raw parameter
|
|
208
|
-
if raw or ext in binary_extensions:
|
|
209
|
-
# Determine MIME type
|
|
210
|
-
mime_types = {
|
|
211
|
-
'.png': 'image/png',
|
|
212
|
-
'.jpg': 'image/jpeg',
|
|
213
|
-
'.jpeg': 'image/jpeg',
|
|
214
|
-
'.gif': 'image/gif',
|
|
215
|
-
'.bmp': 'image/bmp',
|
|
216
|
-
'.ico': 'image/x-icon',
|
|
217
|
-
'.svg': 'image/svg+xml',
|
|
218
|
-
'.webp': 'image/webp',
|
|
219
|
-
'.pdf': 'application/pdf',
|
|
220
|
-
'.doc': 'application/msword',
|
|
221
|
-
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
222
|
-
'.xls': 'application/vnd.ms-excel',
|
|
223
|
-
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
224
|
-
'.ppt': 'application/vnd.ms-powerpoint',
|
|
225
|
-
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
226
|
-
'.zip': 'application/zip',
|
|
227
|
-
'.rar': 'application/vnd.rar',
|
|
228
|
-
}
|
|
229
|
-
mime_type = mime_types.get(ext, 'application/octet-stream')
|
|
230
|
-
|
|
231
|
-
return send_file(full_path, mimetype=mime_type)
|
|
232
|
-
|
|
233
|
-
service = ContentService(project_root)
|
|
234
|
-
result = service.get_content(file_path)
|
|
235
|
-
return jsonify(result)
|
|
236
|
-
except FileNotFoundError:
|
|
237
|
-
return jsonify({'error': 'File not found'}), 404
|
|
238
|
-
except PermissionError:
|
|
239
|
-
return jsonify({'error': 'Access denied'}), 403
|
|
240
|
-
except Exception as e:
|
|
241
|
-
return jsonify({'error': str(e)}), 500
|
|
242
|
-
|
|
243
|
-
@app.route('/api/file/save', methods=['POST'])
|
|
244
|
-
def save_file():
|
|
245
|
-
"""
|
|
246
|
-
POST /api/file/save
|
|
247
|
-
|
|
248
|
-
Save content to a file. Request body: {path: string, content: string}
|
|
249
|
-
|
|
250
|
-
FEATURE-003: Content Editor
|
|
251
|
-
"""
|
|
252
|
-
# Check for JSON body
|
|
253
|
-
if not request.is_json:
|
|
254
|
-
return jsonify({'success': False, 'error': 'JSON body required'}), 400
|
|
255
|
-
|
|
256
|
-
data = request.get_json()
|
|
257
|
-
|
|
258
|
-
# Validate required fields
|
|
259
|
-
if not data:
|
|
260
|
-
return jsonify({'success': False, 'error': 'Request body required'}), 400
|
|
261
|
-
|
|
262
|
-
if 'path' not in data:
|
|
263
|
-
return jsonify({'success': False, 'error': 'Path is required'}), 400
|
|
264
|
-
|
|
265
|
-
if 'content' not in data:
|
|
266
|
-
return jsonify({'success': False, 'error': 'Content is required'}), 400
|
|
267
|
-
|
|
268
|
-
project_root = app.config.get('PROJECT_ROOT')
|
|
269
|
-
|
|
270
|
-
if not project_root or not os.path.exists(project_root):
|
|
271
|
-
return jsonify({'success': False, 'error': 'Project root not configured'}), 400
|
|
272
|
-
|
|
273
|
-
service = ContentService(project_root)
|
|
274
|
-
result = service.save_content(data['path'], data['content'])
|
|
275
|
-
|
|
276
|
-
if result['success']:
|
|
277
|
-
return jsonify(result), 200
|
|
278
|
-
else:
|
|
279
|
-
return jsonify(result), 400
|
|
136
|
+
app.register_blueprint(main_bp)
|
|
137
|
+
app.register_blueprint(settings_bp)
|
|
138
|
+
app.register_blueprint(project_bp)
|
|
139
|
+
app.register_blueprint(ideas_bp)
|
|
140
|
+
app.register_blueprint(tools_bp)
|
|
141
|
+
app.register_blueprint(proxy_bp)
|
|
142
|
+
app.register_blueprint(uiux_feedback_bp)
|
|
280
143
|
|
|
281
144
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def register_terminal_handlers():
|
|
287
|
-
"""Register WebSocket event handlers for terminal."""
|
|
288
|
-
|
|
289
|
-
@socketio.on('connect')
|
|
290
|
-
def handle_connect():
|
|
291
|
-
"""Handle new WebSocket connection."""
|
|
292
|
-
sid = request.sid
|
|
293
|
-
print(f"[Terminal] Client connected: {sid}")
|
|
294
|
-
|
|
295
|
-
@socketio.on('attach')
|
|
296
|
-
def handle_attach(data):
|
|
297
|
-
"""
|
|
298
|
-
Handle session attachment.
|
|
299
|
-
Creates new session or reconnects to existing one.
|
|
300
|
-
"""
|
|
301
|
-
try:
|
|
302
|
-
sid = request.sid
|
|
303
|
-
requested_session_id = data.get('session_id') if data else None
|
|
304
|
-
# Ensure rows/cols are valid integers with defaults
|
|
305
|
-
rows = data.get('rows') if data else None
|
|
306
|
-
cols = data.get('cols') if data else None
|
|
307
|
-
rows = int(rows) if rows is not None else 24
|
|
308
|
-
cols = int(cols) if cols is not None else 80
|
|
309
|
-
|
|
310
|
-
def emit_output(output_data):
|
|
311
|
-
socketio.emit('output', output_data, room=sid)
|
|
312
|
-
|
|
313
|
-
# Try to reconnect to existing session
|
|
314
|
-
if requested_session_id and session_manager.has_session(requested_session_id):
|
|
315
|
-
session = session_manager.get_session(requested_session_id)
|
|
316
|
-
|
|
317
|
-
if session.is_expired():
|
|
318
|
-
session_manager.remove_session(requested_session_id)
|
|
319
|
-
else:
|
|
320
|
-
# Reconnect to existing session
|
|
321
|
-
session.attach(sid, emit_output)
|
|
322
|
-
socket_to_session[sid] = requested_session_id
|
|
323
|
-
|
|
324
|
-
# Replay buffered output
|
|
325
|
-
buffer = session.get_buffer()
|
|
326
|
-
if buffer:
|
|
327
|
-
socketio.emit('output', buffer, room=sid)
|
|
328
|
-
|
|
329
|
-
socketio.emit('reconnected', {'session_id': requested_session_id}, room=sid)
|
|
330
|
-
return
|
|
331
|
-
|
|
332
|
-
# Create new session
|
|
333
|
-
session_id = session_manager.create_session(emit_output, rows, cols)
|
|
334
|
-
session = session_manager.get_session(session_id)
|
|
335
|
-
session.attach(sid, emit_output)
|
|
336
|
-
socket_to_session[sid] = session_id
|
|
337
|
-
|
|
338
|
-
socketio.emit('session_id', session_id, room=sid)
|
|
339
|
-
socketio.emit('new_session', {'session_id': session_id}, room=sid)
|
|
340
|
-
except Exception as e:
|
|
341
|
-
print(f"[Terminal] Error in attach handler: {e}")
|
|
342
|
-
socketio.emit('error', {'message': 'Failed to attach terminal session'}, room=sid)
|
|
343
|
-
|
|
344
|
-
@socketio.on('disconnect')
|
|
345
|
-
def handle_disconnect():
|
|
346
|
-
"""Handle WebSocket disconnection - keep session alive."""
|
|
347
|
-
try:
|
|
348
|
-
sid = request.sid
|
|
349
|
-
session_id = socket_to_session.pop(sid, None)
|
|
350
|
-
|
|
351
|
-
if session_id:
|
|
352
|
-
session = session_manager.get_session(session_id)
|
|
353
|
-
if session:
|
|
354
|
-
session.detach() # Keep PTY alive for reconnection
|
|
355
|
-
|
|
356
|
-
print(f"[Terminal] Client disconnected: {sid}")
|
|
357
|
-
except Exception as e:
|
|
358
|
-
print(f"[Terminal] Error in disconnect handler: {e}")
|
|
359
|
-
|
|
360
|
-
@socketio.on('input')
|
|
361
|
-
def handle_input(data):
|
|
362
|
-
"""Forward input to PTY."""
|
|
363
|
-
try:
|
|
364
|
-
sid = request.sid
|
|
365
|
-
session_id = socket_to_session.get(sid)
|
|
366
|
-
|
|
367
|
-
if session_id:
|
|
368
|
-
session = session_manager.get_session(session_id)
|
|
369
|
-
if session:
|
|
370
|
-
session.write(data)
|
|
371
|
-
except Exception as e:
|
|
372
|
-
print(f"[Terminal] Error in input handler: {e}")
|
|
145
|
+
def _register_handlers():
|
|
146
|
+
"""Register all WebSocket handlers."""
|
|
147
|
+
from x_ipe.handlers import register_terminal_handlers, register_voice_handlers
|
|
373
148
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
"""Handle terminal resize."""
|
|
377
|
-
try:
|
|
378
|
-
sid = request.sid
|
|
379
|
-
session_id = socket_to_session.get(sid)
|
|
380
|
-
|
|
381
|
-
if session_id:
|
|
382
|
-
session = session_manager.get_session(session_id)
|
|
383
|
-
if session:
|
|
384
|
-
rows = data.get('rows', 24)
|
|
385
|
-
cols = data.get('cols', 80)
|
|
386
|
-
session.resize(rows, cols)
|
|
387
|
-
except Exception as e:
|
|
388
|
-
print(f"[Terminal] Error in resize handler: {e}")
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
# =============================================================================
|
|
392
|
-
# FEATURE-021: Console Voice Input - Socket.IO Handlers
|
|
393
|
-
# =============================================================================
|
|
394
|
-
|
|
395
|
-
# Global voice service instance
|
|
396
|
-
voice_service = None
|
|
397
|
-
|
|
398
|
-
# Mapping: socket_sid -> voice_session_id
|
|
399
|
-
socket_to_voice_session = {}
|
|
400
|
-
|
|
401
|
-
def register_voice_handlers():
|
|
402
|
-
"""Register WebSocket event handlers for voice input."""
|
|
403
|
-
from x_ipe.services.voice_input_service_v2 import VoiceInputService, is_voice_command
|
|
404
|
-
|
|
405
|
-
global voice_service
|
|
406
|
-
|
|
407
|
-
# Initialize voice service with API key from environment
|
|
408
|
-
api_key = os.environ.get('ALIBABA_SPEECH_API_KEY', '')
|
|
409
|
-
if api_key:
|
|
410
|
-
voice_service = VoiceInputService(api_key=api_key)
|
|
411
|
-
print("[Voice] Voice service initialized with API key")
|
|
412
|
-
else:
|
|
413
|
-
print("[Voice] No ALIBABA_SPEECH_API_KEY found, voice input disabled")
|
|
414
|
-
|
|
415
|
-
@socketio.on('voice_start')
|
|
416
|
-
def handle_voice_start(data=None):
|
|
417
|
-
"""Handle voice recording start request."""
|
|
418
|
-
global voice_service
|
|
419
|
-
sid = request.sid
|
|
420
|
-
|
|
421
|
-
print(f"[Voice] 🎬 voice_start received from {sid}")
|
|
422
|
-
print(f"[Voice] data: {data}")
|
|
423
|
-
|
|
424
|
-
if not voice_service:
|
|
425
|
-
print(f"[Voice] ❌ Voice service not configured!")
|
|
426
|
-
emit('voice_error', {'message': 'Voice service not configured. Set ALIBABA_SPEECH_API_KEY in x-ipe-docs/config/.env'})
|
|
427
|
-
return
|
|
428
|
-
|
|
429
|
-
try:
|
|
430
|
-
# Create voice session with callbacks for partial results
|
|
431
|
-
def on_partial(text):
|
|
432
|
-
print(f"[Voice] 📤 Emitting voice_partial: '{text}'")
|
|
433
|
-
socketio.emit('voice_partial', {'text': text}, room=sid)
|
|
434
|
-
|
|
435
|
-
session_id = voice_service.create_session(
|
|
436
|
-
socket_sid=sid,
|
|
437
|
-
on_partial=on_partial,
|
|
438
|
-
)
|
|
439
|
-
socket_to_voice_session[sid] = session_id
|
|
440
|
-
|
|
441
|
-
# Start recognition
|
|
442
|
-
print(f"[Voice] Starting recognition...")
|
|
443
|
-
if voice_service.start_recognition(session_id):
|
|
444
|
-
print(f"[Voice] ✅ Emitting voice_ready for session {session_id}")
|
|
445
|
-
emit('voice_ready', {'session_id': session_id})
|
|
446
|
-
else:
|
|
447
|
-
print(f"[Voice] ❌ Failed to start recognition")
|
|
448
|
-
emit('voice_error', {'message': 'Failed to start recognition'})
|
|
449
|
-
voice_service.remove_session(session_id)
|
|
450
|
-
del socket_to_voice_session[sid]
|
|
451
|
-
except Exception as e:
|
|
452
|
-
print(f"[Voice] ❌ Exception in voice_start: {e}")
|
|
453
|
-
import traceback
|
|
454
|
-
traceback.print_exc()
|
|
455
|
-
emit('voice_error', {'message': str(e)})
|
|
456
|
-
|
|
457
|
-
@socketio.on('voice_audio')
|
|
458
|
-
def handle_voice_audio(data):
|
|
459
|
-
"""Handle incoming audio chunk from client."""
|
|
460
|
-
global voice_service
|
|
461
|
-
sid = request.sid
|
|
462
|
-
|
|
463
|
-
if not voice_service:
|
|
464
|
-
return
|
|
465
|
-
|
|
466
|
-
session_id = socket_to_voice_session.get(sid)
|
|
467
|
-
if not session_id:
|
|
468
|
-
print(f"[Voice] ⚠️ voice_audio received but no session for {sid}")
|
|
469
|
-
return
|
|
470
|
-
|
|
471
|
-
try:
|
|
472
|
-
# Get audio data from message
|
|
473
|
-
audio_chunk = data.get('audio', b'') if isinstance(data, dict) else data
|
|
474
|
-
if isinstance(audio_chunk, list):
|
|
475
|
-
audio_chunk = bytes(audio_chunk)
|
|
476
|
-
|
|
477
|
-
# Forward to voice service (sync now with dashscope SDK)
|
|
478
|
-
voice_service.send_audio(session_id, audio_chunk)
|
|
479
|
-
except Exception as e:
|
|
480
|
-
print(f"[Voice] ❌ Exception in voice_audio: {e}")
|
|
481
|
-
emit('voice_error', {'message': f'Audio error: {e}'})
|
|
482
|
-
|
|
483
|
-
@socketio.on('voice_stop')
|
|
484
|
-
def handle_voice_stop(data=None):
|
|
485
|
-
"""Handle voice recording stop request - finalize and get transcription."""
|
|
486
|
-
global voice_service
|
|
487
|
-
sid = request.sid
|
|
488
|
-
|
|
489
|
-
print(f"[Voice] 🛑 voice_stop received from {sid}")
|
|
490
|
-
|
|
491
|
-
if not voice_service:
|
|
492
|
-
print(f"[Voice] ❌ Voice service not available")
|
|
493
|
-
emit('voice_error', {'message': 'Voice service not available'})
|
|
494
|
-
return
|
|
495
|
-
|
|
496
|
-
session_id = socket_to_voice_session.get(sid)
|
|
497
|
-
if not session_id:
|
|
498
|
-
print(f"[Voice] ❌ No active voice session for {sid}")
|
|
499
|
-
emit('voice_error', {'message': 'No active voice session'})
|
|
500
|
-
return
|
|
501
|
-
|
|
502
|
-
try:
|
|
503
|
-
print(f"[Voice] Stopping recognition for session {session_id}...")
|
|
504
|
-
# Stop recognition and get result (sync with dashscope SDK)
|
|
505
|
-
result = voice_service.stop_recognition(session_id)
|
|
506
|
-
|
|
507
|
-
print(f"[Voice] Result: '{result}'")
|
|
508
|
-
|
|
509
|
-
# Check if result is a voice command
|
|
510
|
-
command = is_voice_command(result)
|
|
511
|
-
|
|
512
|
-
if command:
|
|
513
|
-
print(f"[Voice] 📤 Emitting voice_command: {command}")
|
|
514
|
-
emit('voice_command', {'command': command, 'text': result})
|
|
515
|
-
else:
|
|
516
|
-
print(f"[Voice] 📤 Emitting voice_result: '{result}'")
|
|
517
|
-
emit('voice_result', {'text': result})
|
|
518
|
-
|
|
519
|
-
# Cleanup session
|
|
520
|
-
voice_service.remove_session(session_id)
|
|
521
|
-
if sid in socket_to_voice_session:
|
|
522
|
-
del socket_to_voice_session[sid]
|
|
523
|
-
|
|
524
|
-
print(f"[Voice] ✅ Session completed: {session_id}")
|
|
525
|
-
except Exception as e:
|
|
526
|
-
print(f"[Voice] ❌ Exception in voice_stop: {e}")
|
|
527
|
-
import traceback
|
|
528
|
-
traceback.print_exc()
|
|
529
|
-
emit('voice_error', {'message': str(e)})
|
|
530
|
-
|
|
531
|
-
@socketio.on('voice_cancel')
|
|
532
|
-
def handle_voice_cancel(data=None):
|
|
533
|
-
"""Handle voice recording cancel request - abort without transcription."""
|
|
534
|
-
global voice_service
|
|
535
|
-
sid = request.sid
|
|
536
|
-
|
|
537
|
-
print(f"[Voice] ⚠️ voice_cancel received from {sid}")
|
|
538
|
-
|
|
539
|
-
session_id = socket_to_voice_session.get(sid)
|
|
540
|
-
if not session_id:
|
|
541
|
-
print(f"[Voice] No session to cancel")
|
|
542
|
-
return
|
|
543
|
-
|
|
544
|
-
try:
|
|
545
|
-
# Cancel recognition (sync with dashscope SDK)
|
|
546
|
-
if voice_service:
|
|
547
|
-
voice_service.cancel_recognition(session_id)
|
|
548
|
-
voice_service.remove_session(session_id)
|
|
549
|
-
|
|
550
|
-
if sid in socket_to_voice_session:
|
|
551
|
-
del socket_to_voice_session[sid]
|
|
552
|
-
|
|
553
|
-
emit('voice_cancelled', {})
|
|
554
|
-
print(f"[Voice] ✅ Session cancelled: {session_id}")
|
|
555
|
-
except Exception as e:
|
|
556
|
-
print(f"[Voice] ❌ Exception in voice_cancel: {e}")
|
|
557
|
-
emit('voice_error', {'message': f'Cancel error: {e}'})
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
# =============================================================================
|
|
561
|
-
# FEATURE-006: Settings & Configuration - Routes
|
|
562
|
-
# =============================================================================
|
|
563
|
-
|
|
564
|
-
def register_settings_routes(app):
|
|
565
|
-
"""Register settings API and page routes."""
|
|
566
|
-
|
|
567
|
-
@app.route('/settings')
|
|
568
|
-
def settings_page():
|
|
569
|
-
"""
|
|
570
|
-
GET /settings
|
|
571
|
-
|
|
572
|
-
Render the settings page.
|
|
573
|
-
"""
|
|
574
|
-
global settings_service
|
|
575
|
-
current_settings = settings_service.get_all() if settings_service else {'project_root': '.'}
|
|
576
|
-
return render_template('settings.html', settings=current_settings)
|
|
577
|
-
|
|
578
|
-
@app.route('/api/settings', methods=['GET'])
|
|
579
|
-
def get_settings():
|
|
580
|
-
"""
|
|
581
|
-
GET /api/settings
|
|
582
|
-
|
|
583
|
-
Get all current settings.
|
|
584
|
-
|
|
585
|
-
Response:
|
|
586
|
-
- project_root: string - Current project root path
|
|
587
|
-
"""
|
|
588
|
-
global settings_service
|
|
589
|
-
if not settings_service:
|
|
590
|
-
return jsonify({'project_root': app.config.get('PROJECT_ROOT', '.')}), 200
|
|
591
|
-
|
|
592
|
-
return jsonify(settings_service.get_all())
|
|
593
|
-
|
|
594
|
-
@app.route('/api/settings', methods=['POST'])
|
|
595
|
-
def save_settings():
|
|
596
|
-
"""
|
|
597
|
-
POST /api/settings
|
|
598
|
-
|
|
599
|
-
Save settings.
|
|
600
|
-
|
|
601
|
-
Request body:
|
|
602
|
-
- project_root: string (optional) - New project root path
|
|
603
|
-
|
|
604
|
-
Response (success):
|
|
605
|
-
- success: true
|
|
606
|
-
- message: string
|
|
607
|
-
|
|
608
|
-
Response (error):
|
|
609
|
-
- success: false
|
|
610
|
-
- errors: object with field-specific error messages
|
|
611
|
-
"""
|
|
612
|
-
global settings_service, file_watcher
|
|
613
|
-
|
|
614
|
-
data = request.get_json() or {}
|
|
615
|
-
errors = {}
|
|
616
|
-
|
|
617
|
-
# Validate project_root if provided
|
|
618
|
-
if 'project_root' in data:
|
|
619
|
-
path = data['project_root']
|
|
620
|
-
path_errors = settings_service.validate_project_root(path)
|
|
621
|
-
errors.update(path_errors)
|
|
622
|
-
|
|
623
|
-
# Return errors if any
|
|
624
|
-
if errors:
|
|
625
|
-
return jsonify({'success': False, 'errors': errors}), 400
|
|
626
|
-
|
|
627
|
-
# Save settings
|
|
628
|
-
for key, value in data.items():
|
|
629
|
-
if key in ['project_root']: # Allowed settings
|
|
630
|
-
settings_service.set(key, value)
|
|
631
|
-
|
|
632
|
-
# Apply project_root change
|
|
633
|
-
if 'project_root' in data:
|
|
634
|
-
new_path = data['project_root']
|
|
635
|
-
app.config['PROJECT_ROOT'] = new_path
|
|
636
|
-
|
|
637
|
-
return jsonify({'success': True, 'message': 'Settings saved successfully'})
|
|
638
|
-
|
|
639
|
-
@app.route('/api/config', methods=['GET'])
|
|
640
|
-
def get_config():
|
|
641
|
-
"""
|
|
642
|
-
GET /api/config
|
|
643
|
-
|
|
644
|
-
Get current project configuration from .x-ipe.yaml.
|
|
645
|
-
|
|
646
|
-
FEATURE-010: Project Root Configuration
|
|
647
|
-
|
|
648
|
-
Response (config detected):
|
|
649
|
-
- detected: true
|
|
650
|
-
- config_file: string - Path to .x-ipe.yaml
|
|
651
|
-
- version: int
|
|
652
|
-
- project_root: string
|
|
653
|
-
- x_ipe_app: string
|
|
654
|
-
- file_tree_scope: string
|
|
655
|
-
- terminal_cwd: string
|
|
656
|
-
|
|
657
|
-
Response (no config):
|
|
658
|
-
- detected: false
|
|
659
|
-
- config_file: null
|
|
660
|
-
- using_defaults: true
|
|
661
|
-
- project_root: string - Current project root
|
|
662
|
-
- message: string
|
|
663
|
-
"""
|
|
664
|
-
config_data = app.config.get('X_IPE_CONFIG')
|
|
665
|
-
|
|
666
|
-
if config_data:
|
|
667
|
-
return jsonify({
|
|
668
|
-
'detected': True,
|
|
669
|
-
**config_data.to_dict()
|
|
670
|
-
})
|
|
671
|
-
else:
|
|
672
|
-
return jsonify({
|
|
673
|
-
'detected': False,
|
|
674
|
-
'config_file': None,
|
|
675
|
-
'using_defaults': True,
|
|
676
|
-
'project_root': app.config.get('PROJECT_ROOT', os.getcwd()),
|
|
677
|
-
'message': 'No .x-ipe.yaml found. Using default paths.'
|
|
678
|
-
})
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
def register_project_routes(app):
|
|
682
|
-
"""
|
|
683
|
-
Register project folder management routes.
|
|
684
|
-
|
|
685
|
-
FEATURE-006 v2.0: Multi-Project Folder Support
|
|
686
|
-
"""
|
|
687
|
-
|
|
688
|
-
@app.route('/api/projects', methods=['GET'])
|
|
689
|
-
def get_projects():
|
|
690
|
-
"""
|
|
691
|
-
GET /api/projects
|
|
692
|
-
|
|
693
|
-
Get all project folders and active project ID.
|
|
694
|
-
|
|
695
|
-
Response:
|
|
696
|
-
- projects: array of {id, name, path}
|
|
697
|
-
- active_project_id: number
|
|
698
|
-
"""
|
|
699
|
-
global project_folders_service
|
|
700
|
-
if not project_folders_service:
|
|
701
|
-
return jsonify({'projects': [], 'active_project_id': 1}), 200
|
|
702
|
-
|
|
703
|
-
return jsonify({
|
|
704
|
-
'projects': project_folders_service.get_all(),
|
|
705
|
-
'active_project_id': project_folders_service.get_active_id()
|
|
706
|
-
})
|
|
707
|
-
|
|
708
|
-
@app.route('/api/projects', methods=['POST'])
|
|
709
|
-
def add_project():
|
|
710
|
-
"""
|
|
711
|
-
POST /api/projects
|
|
712
|
-
|
|
713
|
-
Add a new project folder.
|
|
714
|
-
|
|
715
|
-
Request body:
|
|
716
|
-
- name: string - Project name
|
|
717
|
-
- path: string - Project path
|
|
718
|
-
|
|
719
|
-
Response (success):
|
|
720
|
-
- success: true
|
|
721
|
-
- project: {id, name, path}
|
|
722
|
-
|
|
723
|
-
Response (error):
|
|
724
|
-
- success: false
|
|
725
|
-
- errors: object with field-specific error messages
|
|
726
|
-
"""
|
|
727
|
-
global project_folders_service
|
|
728
|
-
|
|
729
|
-
if not request.is_json:
|
|
730
|
-
return jsonify({'success': False, 'error': 'JSON required'}), 400
|
|
731
|
-
|
|
732
|
-
data = request.get_json()
|
|
733
|
-
name = data.get('name', '').strip()
|
|
734
|
-
path = data.get('path', '').strip()
|
|
735
|
-
|
|
736
|
-
result = project_folders_service.add(name, path)
|
|
737
|
-
|
|
738
|
-
if result['success']:
|
|
739
|
-
return jsonify(result), 201
|
|
740
|
-
return jsonify(result), 400
|
|
741
|
-
|
|
742
|
-
@app.route('/api/projects/<int:project_id>', methods=['PUT'])
|
|
743
|
-
def update_project(project_id):
|
|
744
|
-
"""
|
|
745
|
-
PUT /api/projects/<id>
|
|
746
|
-
|
|
747
|
-
Update an existing project folder.
|
|
748
|
-
|
|
749
|
-
Request body:
|
|
750
|
-
- name: string (optional) - New project name
|
|
751
|
-
- path: string (optional) - New project path
|
|
752
|
-
|
|
753
|
-
Response (success):
|
|
754
|
-
- success: true
|
|
755
|
-
- project: {id, name, path}
|
|
756
|
-
|
|
757
|
-
Response (error):
|
|
758
|
-
- success: false
|
|
759
|
-
- errors: object with field-specific error messages
|
|
760
|
-
"""
|
|
761
|
-
global project_folders_service
|
|
762
|
-
|
|
763
|
-
if not request.is_json:
|
|
764
|
-
return jsonify({'success': False, 'error': 'JSON required'}), 400
|
|
765
|
-
|
|
766
|
-
data = request.get_json()
|
|
767
|
-
name = data.get('name')
|
|
768
|
-
path = data.get('path')
|
|
769
|
-
|
|
770
|
-
result = project_folders_service.update(project_id, name=name, path=path)
|
|
771
|
-
|
|
772
|
-
if result['success']:
|
|
773
|
-
return jsonify(result)
|
|
774
|
-
return jsonify(result), 400
|
|
775
|
-
|
|
776
|
-
@app.route('/api/projects/<int:project_id>', methods=['DELETE'])
|
|
777
|
-
def delete_project(project_id):
|
|
778
|
-
"""
|
|
779
|
-
DELETE /api/projects/<id>
|
|
780
|
-
|
|
781
|
-
Delete a project folder.
|
|
782
|
-
|
|
783
|
-
Response (success):
|
|
784
|
-
- success: true
|
|
785
|
-
|
|
786
|
-
Response (error):
|
|
787
|
-
- success: false
|
|
788
|
-
- error: string error message
|
|
789
|
-
"""
|
|
790
|
-
global project_folders_service
|
|
791
|
-
|
|
792
|
-
active_id = project_folders_service.get_active_id()
|
|
793
|
-
result = project_folders_service.delete(project_id, active_project_id=active_id)
|
|
794
|
-
|
|
795
|
-
if result['success']:
|
|
796
|
-
return jsonify(result)
|
|
797
|
-
return jsonify(result), 400
|
|
798
|
-
|
|
799
|
-
@app.route('/api/projects/switch', methods=['POST'])
|
|
800
|
-
def switch_project():
|
|
801
|
-
"""
|
|
802
|
-
POST /api/projects/switch
|
|
803
|
-
|
|
804
|
-
Switch the active project.
|
|
805
|
-
|
|
806
|
-
Request body:
|
|
807
|
-
- project_id: number - Project ID to switch to
|
|
808
|
-
|
|
809
|
-
Response (success):
|
|
810
|
-
- success: true
|
|
811
|
-
- active_project_id: number
|
|
812
|
-
- project: {id, name, path}
|
|
813
|
-
|
|
814
|
-
Response (error):
|
|
815
|
-
- success: false
|
|
816
|
-
- error: string error message
|
|
817
|
-
"""
|
|
818
|
-
global project_folders_service
|
|
819
|
-
|
|
820
|
-
if not request.is_json:
|
|
821
|
-
return jsonify({'success': False, 'error': 'JSON required'}), 400
|
|
822
|
-
|
|
823
|
-
data = request.get_json()
|
|
824
|
-
project_id = data.get('project_id')
|
|
825
|
-
|
|
826
|
-
if not project_id:
|
|
827
|
-
return jsonify({'success': False, 'error': 'project_id required'}), 400
|
|
828
|
-
|
|
829
|
-
result = project_folders_service.set_active(project_id)
|
|
830
|
-
|
|
831
|
-
if result['success']:
|
|
832
|
-
# Update app config with new project root
|
|
833
|
-
project = result['project']
|
|
834
|
-
project_path = project['path']
|
|
835
|
-
|
|
836
|
-
# If config is detected, project folders are relative to config's project_root
|
|
837
|
-
config_data = app.config.get('X_IPE_CONFIG')
|
|
838
|
-
if config_data:
|
|
839
|
-
# Config always takes precedence - use its project_root
|
|
840
|
-
app.config['PROJECT_ROOT'] = config_data.project_root
|
|
841
|
-
elif project_path == '.':
|
|
842
|
-
# No config, default project folder - use cwd where x-ipe was run
|
|
843
|
-
app.config['PROJECT_ROOT'] = os.environ.get('X_IPE_PROJECT_ROOT', os.getcwd())
|
|
844
|
-
else:
|
|
845
|
-
# Absolute path from project folder
|
|
846
|
-
app.config['PROJECT_ROOT'] = project_path
|
|
847
|
-
|
|
848
|
-
return jsonify(result)
|
|
849
|
-
return jsonify(result), 400
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
def register_ideas_routes(app):
|
|
853
|
-
"""
|
|
854
|
-
Register idea management routes.
|
|
855
|
-
|
|
856
|
-
FEATURE-008: Workplace (Idea Management)
|
|
857
|
-
"""
|
|
858
|
-
|
|
859
|
-
@app.route('/api/ideas/tree', methods=['GET'])
|
|
860
|
-
def get_ideas_tree():
|
|
861
|
-
"""
|
|
862
|
-
GET /api/ideas/tree
|
|
863
|
-
|
|
864
|
-
Get tree structure of x-ipe-docs/ideas/ directory.
|
|
865
|
-
|
|
866
|
-
Response:
|
|
867
|
-
- success: true
|
|
868
|
-
- tree: array of folder/file objects
|
|
869
|
-
"""
|
|
870
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
871
|
-
service = IdeasService(project_root)
|
|
872
|
-
|
|
873
|
-
try:
|
|
874
|
-
tree = service.get_tree()
|
|
875
|
-
return jsonify({
|
|
876
|
-
'success': True,
|
|
877
|
-
'tree': tree
|
|
878
|
-
})
|
|
879
|
-
except Exception as e:
|
|
880
|
-
return jsonify({
|
|
881
|
-
'success': False,
|
|
882
|
-
'error': str(e)
|
|
883
|
-
}), 500
|
|
884
|
-
|
|
885
|
-
@app.route('/api/ideas/upload', methods=['POST'])
|
|
886
|
-
def upload_ideas():
|
|
887
|
-
"""
|
|
888
|
-
POST /api/ideas/upload
|
|
889
|
-
|
|
890
|
-
Upload files to a new or existing idea folder.
|
|
891
|
-
|
|
892
|
-
Request: multipart/form-data with 'files' field
|
|
893
|
-
Optional: 'target_folder' field to upload to existing folder (CR-002)
|
|
894
|
-
|
|
895
|
-
Response:
|
|
896
|
-
- success: true/false
|
|
897
|
-
- folder_name: string
|
|
898
|
-
- folder_path: string
|
|
899
|
-
- files_uploaded: array of filenames
|
|
900
|
-
"""
|
|
901
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
902
|
-
service = IdeasService(project_root)
|
|
903
|
-
|
|
904
|
-
if 'files' not in request.files:
|
|
905
|
-
return jsonify({
|
|
906
|
-
'success': False,
|
|
907
|
-
'error': 'No files provided'
|
|
908
|
-
}), 400
|
|
909
|
-
|
|
910
|
-
uploaded_files = request.files.getlist('files')
|
|
911
|
-
if not uploaded_files or all(f.filename == '' for f in uploaded_files):
|
|
912
|
-
return jsonify({
|
|
913
|
-
'success': False,
|
|
914
|
-
'error': 'No files provided'
|
|
915
|
-
}), 400
|
|
916
|
-
|
|
917
|
-
# CR-002: Get optional target_folder from form data
|
|
918
|
-
target_folder = request.form.get('target_folder', None)
|
|
919
|
-
|
|
920
|
-
# Convert to (filename, content) tuples
|
|
921
|
-
files = [(f.filename, f.read()) for f in uploaded_files if f.filename]
|
|
922
|
-
|
|
923
|
-
result = service.upload(files, target_folder=target_folder)
|
|
924
|
-
|
|
925
|
-
if result['success']:
|
|
926
|
-
return jsonify(result)
|
|
927
|
-
return jsonify(result), 400
|
|
928
|
-
|
|
929
|
-
@app.route('/api/ideas/rename', methods=['POST'])
|
|
930
|
-
def rename_idea_folder():
|
|
931
|
-
"""
|
|
932
|
-
POST /api/ideas/rename
|
|
933
|
-
|
|
934
|
-
Rename an idea folder.
|
|
935
|
-
|
|
936
|
-
Request body:
|
|
937
|
-
- old_name: string - Current folder name
|
|
938
|
-
- new_name: string - New folder name
|
|
939
|
-
|
|
940
|
-
Response:
|
|
941
|
-
- success: true/false
|
|
942
|
-
- old_name: string
|
|
943
|
-
- new_name: string
|
|
944
|
-
- new_path: string
|
|
945
|
-
"""
|
|
946
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
947
|
-
service = IdeasService(project_root)
|
|
948
|
-
|
|
949
|
-
if not request.is_json:
|
|
950
|
-
return jsonify({
|
|
951
|
-
'success': False,
|
|
952
|
-
'error': 'JSON required'
|
|
953
|
-
}), 400
|
|
954
|
-
|
|
955
|
-
data = request.get_json()
|
|
956
|
-
old_name = data.get('old_name')
|
|
957
|
-
new_name = data.get('new_name')
|
|
958
|
-
|
|
959
|
-
if not old_name or not new_name:
|
|
960
|
-
return jsonify({
|
|
961
|
-
'success': False,
|
|
962
|
-
'error': 'old_name and new_name are required'
|
|
963
|
-
}), 400
|
|
964
|
-
|
|
965
|
-
result = service.rename_folder(old_name, new_name)
|
|
966
|
-
|
|
967
|
-
if result['success']:
|
|
968
|
-
return jsonify(result)
|
|
969
|
-
return jsonify(result), 400
|
|
970
|
-
|
|
971
|
-
@app.route('/api/ideas/rename-file', methods=['POST'])
|
|
972
|
-
def rename_idea_file():
|
|
973
|
-
"""
|
|
974
|
-
POST /api/ideas/rename-file
|
|
975
|
-
|
|
976
|
-
Rename a file within x-ipe-docs/ideas/.
|
|
977
|
-
|
|
978
|
-
Request body:
|
|
979
|
-
- path: string - Current file path (relative to project root)
|
|
980
|
-
- new_name: string - New file name (with extension)
|
|
981
|
-
|
|
982
|
-
Response:
|
|
983
|
-
- success: true/false
|
|
984
|
-
- old_path: string
|
|
985
|
-
- new_path: string
|
|
986
|
-
- new_name: string
|
|
987
|
-
"""
|
|
988
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
989
|
-
service = IdeasService(project_root)
|
|
990
|
-
|
|
991
|
-
if not request.is_json:
|
|
992
|
-
return jsonify({
|
|
993
|
-
'success': False,
|
|
994
|
-
'error': 'JSON required'
|
|
995
|
-
}), 400
|
|
996
|
-
|
|
997
|
-
data = request.get_json()
|
|
998
|
-
path = data.get('path')
|
|
999
|
-
new_name = data.get('new_name')
|
|
1000
|
-
|
|
1001
|
-
if not path or not new_name:
|
|
1002
|
-
return jsonify({
|
|
1003
|
-
'success': False,
|
|
1004
|
-
'error': 'path and new_name are required'
|
|
1005
|
-
}), 400
|
|
1006
|
-
|
|
1007
|
-
result = service.rename_file(path, new_name)
|
|
1008
|
-
|
|
1009
|
-
if result['success']:
|
|
1010
|
-
return jsonify(result)
|
|
1011
|
-
return jsonify(result), 400
|
|
1012
|
-
|
|
1013
|
-
@app.route('/api/ideas/delete', methods=['POST'])
|
|
1014
|
-
def delete_idea_item():
|
|
1015
|
-
"""
|
|
1016
|
-
POST /api/ideas/delete
|
|
1017
|
-
|
|
1018
|
-
Delete an idea file or folder.
|
|
1019
|
-
|
|
1020
|
-
Request body:
|
|
1021
|
-
- path: string - Relative path to file/folder within x-ipe-docs/ideas/
|
|
1022
|
-
|
|
1023
|
-
Response:
|
|
1024
|
-
- success: true/false
|
|
1025
|
-
- path: string
|
|
1026
|
-
- type: 'file' | 'folder'
|
|
1027
|
-
"""
|
|
1028
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1029
|
-
service = IdeasService(project_root)
|
|
1030
|
-
|
|
1031
|
-
if not request.is_json:
|
|
1032
|
-
return jsonify({
|
|
1033
|
-
'success': False,
|
|
1034
|
-
'error': 'JSON required'
|
|
1035
|
-
}), 400
|
|
1036
|
-
|
|
1037
|
-
data = request.get_json()
|
|
1038
|
-
path = data.get('path')
|
|
1039
|
-
|
|
1040
|
-
if not path:
|
|
1041
|
-
return jsonify({
|
|
1042
|
-
'success': False,
|
|
1043
|
-
'error': 'path is required'
|
|
1044
|
-
}), 400
|
|
1045
|
-
|
|
1046
|
-
result = service.delete_item(path)
|
|
1047
|
-
|
|
1048
|
-
if result['success']:
|
|
1049
|
-
return jsonify(result)
|
|
1050
|
-
return jsonify(result), 400
|
|
1051
|
-
|
|
1052
|
-
@app.route('/api/ideas/toolbox', methods=['GET'])
|
|
1053
|
-
def get_ideas_toolbox():
|
|
1054
|
-
"""
|
|
1055
|
-
GET /api/ideas/toolbox
|
|
1056
|
-
|
|
1057
|
-
Get ideation toolbox configuration.
|
|
1058
|
-
|
|
1059
|
-
Response:
|
|
1060
|
-
- version: string
|
|
1061
|
-
- ideation: {antv-infographic: bool, mermaid: bool}
|
|
1062
|
-
- mockup: {frontend-design: bool}
|
|
1063
|
-
- sharing: {}
|
|
1064
|
-
"""
|
|
1065
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1066
|
-
service = IdeasService(project_root)
|
|
1067
|
-
|
|
1068
|
-
config = service.get_toolbox()
|
|
1069
|
-
return jsonify(config)
|
|
1070
|
-
|
|
1071
|
-
@app.route('/api/ideas/toolbox', methods=['POST'])
|
|
1072
|
-
def save_ideas_toolbox():
|
|
1073
|
-
"""
|
|
1074
|
-
POST /api/ideas/toolbox
|
|
1075
|
-
|
|
1076
|
-
Save ideation toolbox configuration.
|
|
1077
|
-
|
|
1078
|
-
Request body:
|
|
1079
|
-
- version: string
|
|
1080
|
-
- ideation: {antv-infographic: bool, mermaid: bool}
|
|
1081
|
-
- mockup: {frontend-design: bool}
|
|
1082
|
-
- sharing: {}
|
|
1083
|
-
|
|
1084
|
-
Response:
|
|
1085
|
-
- success: true/false
|
|
1086
|
-
"""
|
|
1087
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1088
|
-
service = IdeasService(project_root)
|
|
1089
|
-
|
|
1090
|
-
config = request.get_json()
|
|
1091
|
-
result = service.save_toolbox(config)
|
|
1092
|
-
|
|
1093
|
-
if result['success']:
|
|
1094
|
-
return jsonify(result)
|
|
1095
|
-
return jsonify(result), 400
|
|
1096
|
-
|
|
1097
|
-
"""
|
|
1098
|
-
==========================================================================
|
|
1099
|
-
SKILLS API
|
|
1100
|
-
|
|
1101
|
-
Read-only API for skills defined in .github/skills/
|
|
1102
|
-
==========================================================================
|
|
1103
|
-
"""
|
|
1104
|
-
|
|
1105
|
-
@app.route('/api/skills', methods=['GET'])
|
|
1106
|
-
def get_skills():
|
|
1107
|
-
"""
|
|
1108
|
-
GET /api/skills
|
|
1109
|
-
|
|
1110
|
-
Get list of all skills with name and description.
|
|
1111
|
-
|
|
1112
|
-
Response:
|
|
1113
|
-
- success: true
|
|
1114
|
-
- skills: array of {name, description}
|
|
1115
|
-
"""
|
|
1116
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1117
|
-
service = SkillsService(project_root)
|
|
1118
|
-
|
|
1119
|
-
try:
|
|
1120
|
-
skills = service.get_all()
|
|
1121
|
-
return jsonify({
|
|
1122
|
-
'success': True,
|
|
1123
|
-
'skills': skills
|
|
1124
|
-
})
|
|
1125
|
-
except Exception as e:
|
|
1126
|
-
return jsonify({
|
|
1127
|
-
'success': False,
|
|
1128
|
-
'error': str(e)
|
|
1129
|
-
}), 500
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
# Entry point for running with `python -m src.app`
|
|
1133
|
-
def register_tools_config_routes(app):
|
|
1134
|
-
"""
|
|
1135
|
-
Register tools configuration routes.
|
|
1136
|
-
|
|
1137
|
-
FEATURE-011: Stage Toolbox
|
|
1138
|
-
"""
|
|
1139
|
-
|
|
1140
|
-
def _get_tools_service():
|
|
1141
|
-
"""Get ToolsConfigService instance for current project root."""
|
|
1142
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1143
|
-
return ToolsConfigService(project_root)
|
|
1144
|
-
|
|
1145
|
-
@app.route('/api/config/tools', methods=['GET'])
|
|
1146
|
-
def get_tools_config():
|
|
1147
|
-
"""
|
|
1148
|
-
GET /api/config/tools
|
|
1149
|
-
|
|
1150
|
-
Get current tools configuration.
|
|
1151
|
-
|
|
1152
|
-
Response:
|
|
1153
|
-
- success: true
|
|
1154
|
-
- config: tools configuration object
|
|
1155
|
-
"""
|
|
1156
|
-
try:
|
|
1157
|
-
service = _get_tools_service()
|
|
1158
|
-
config = service.load()
|
|
1159
|
-
return jsonify({
|
|
1160
|
-
'success': True,
|
|
1161
|
-
'config': config
|
|
1162
|
-
})
|
|
1163
|
-
except Exception as e:
|
|
1164
|
-
return jsonify({
|
|
1165
|
-
'success': False,
|
|
1166
|
-
'error': str(e)
|
|
1167
|
-
}), 500
|
|
1168
|
-
|
|
1169
|
-
@app.route('/api/config/tools', methods=['POST'])
|
|
1170
|
-
def save_tools_config():
|
|
1171
|
-
"""
|
|
1172
|
-
POST /api/config/tools
|
|
1173
|
-
|
|
1174
|
-
Save tools configuration.
|
|
1175
|
-
|
|
1176
|
-
Request Body: JSON with 'stages' key
|
|
1177
|
-
|
|
1178
|
-
Response:
|
|
1179
|
-
- success: true/false
|
|
1180
|
-
- error: string (on failure)
|
|
1181
|
-
"""
|
|
1182
|
-
try:
|
|
1183
|
-
config = request.get_json(force=True, silent=True)
|
|
1184
|
-
if config is None:
|
|
1185
|
-
return jsonify({
|
|
1186
|
-
'success': False,
|
|
1187
|
-
'error': 'Invalid JSON or empty body'
|
|
1188
|
-
}), 400
|
|
1189
|
-
|
|
1190
|
-
if 'stages' not in config:
|
|
1191
|
-
return jsonify({
|
|
1192
|
-
'success': False,
|
|
1193
|
-
'error': 'Invalid config format: missing stages key'
|
|
1194
|
-
}), 400
|
|
1195
|
-
|
|
1196
|
-
service = _get_tools_service()
|
|
1197
|
-
service.save(config)
|
|
1198
|
-
return jsonify({'success': True})
|
|
1199
|
-
except Exception as e:
|
|
1200
|
-
return jsonify({
|
|
1201
|
-
'success': False,
|
|
1202
|
-
'error': str(e)
|
|
1203
|
-
}), 500
|
|
1204
|
-
|
|
1205
|
-
@app.route('/api/config/copilot-prompt', methods=['GET'])
|
|
1206
|
-
def get_copilot_prompt_config():
|
|
1207
|
-
"""
|
|
1208
|
-
GET /api/config/copilot-prompt
|
|
1209
|
-
|
|
1210
|
-
Get Copilot prompt configuration for the Copilot button dropdown.
|
|
1211
|
-
|
|
1212
|
-
Response:
|
|
1213
|
-
- prompts: List of prompt objects with id, label, icon, command
|
|
1214
|
-
- placeholder: Dictionary of placeholder descriptions
|
|
1215
|
-
"""
|
|
1216
|
-
try:
|
|
1217
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1218
|
-
config_path = os.path.join(project_root, 'x-ipe-docs', 'config', 'copilot-prompt.json')
|
|
1219
|
-
|
|
1220
|
-
if os.path.exists(config_path):
|
|
1221
|
-
with open(config_path, 'r', encoding='utf-8') as f:
|
|
1222
|
-
config = json.load(f)
|
|
1223
|
-
return jsonify(config)
|
|
1224
|
-
else:
|
|
1225
|
-
# Return empty prompts if config doesn't exist
|
|
1226
|
-
return jsonify({
|
|
1227
|
-
'version': '1.0',
|
|
1228
|
-
'prompts': [],
|
|
1229
|
-
'placeholder': {}
|
|
1230
|
-
})
|
|
1231
|
-
except Exception as e:
|
|
1232
|
-
return jsonify({
|
|
1233
|
-
'prompts': [],
|
|
1234
|
-
'error': str(e)
|
|
1235
|
-
}), 500
|
|
1236
|
-
|
|
1237
|
-
# ============================================================
|
|
1238
|
-
# FEATURE-012: Design Themes API
|
|
1239
|
-
# ============================================================
|
|
1240
|
-
|
|
1241
|
-
def _get_themes_service():
|
|
1242
|
-
"""Get ThemesService instance for current project root."""
|
|
1243
|
-
project_root = app.config.get('PROJECT_ROOT', os.getcwd())
|
|
1244
|
-
return ThemesService(project_root)
|
|
1245
|
-
|
|
1246
|
-
@app.route('/api/themes', methods=['GET'])
|
|
1247
|
-
def list_themes():
|
|
1248
|
-
"""
|
|
1249
|
-
GET /api/themes
|
|
1250
|
-
|
|
1251
|
-
List all themes with metadata.
|
|
1252
|
-
|
|
1253
|
-
Response:
|
|
1254
|
-
- themes: List of theme objects with name, description, colors, files, path
|
|
1255
|
-
- selected: Currently selected theme name from config (null if none)
|
|
1256
|
-
"""
|
|
1257
|
-
try:
|
|
1258
|
-
themes_service = _get_themes_service()
|
|
1259
|
-
themes = themes_service.list_themes()
|
|
1260
|
-
|
|
1261
|
-
# Get selected theme from tools config (new format)
|
|
1262
|
-
tools_service = _get_tools_service()
|
|
1263
|
-
config = tools_service.load()
|
|
1264
|
-
selected_theme_config = config.get('selected-theme', {})
|
|
1265
|
-
selected = selected_theme_config.get('theme-name') if selected_theme_config else None
|
|
1266
|
-
|
|
1267
|
-
return jsonify({
|
|
1268
|
-
'themes': themes,
|
|
1269
|
-
'selected': selected
|
|
1270
|
-
})
|
|
1271
|
-
except Exception as e:
|
|
1272
|
-
return jsonify({
|
|
1273
|
-
'themes': [],
|
|
1274
|
-
'selected': 'theme-default',
|
|
1275
|
-
'error': str(e)
|
|
1276
|
-
}), 500
|
|
1277
|
-
|
|
1278
|
-
@app.route('/api/themes/<name>', methods=['GET'])
|
|
1279
|
-
def get_theme_detail(name):
|
|
1280
|
-
"""
|
|
1281
|
-
GET /api/themes/<name>
|
|
1282
|
-
|
|
1283
|
-
Get detailed information about a specific theme.
|
|
1284
|
-
|
|
1285
|
-
Args:
|
|
1286
|
-
name: Theme folder name (e.g., "theme-default")
|
|
1287
|
-
|
|
1288
|
-
Response:
|
|
1289
|
-
- Theme detail object with design_system content
|
|
1290
|
-
- 404 if theme not found
|
|
1291
|
-
"""
|
|
1292
|
-
try:
|
|
1293
|
-
themes_service = _get_themes_service()
|
|
1294
|
-
theme = themes_service.get_theme(name)
|
|
1295
|
-
|
|
1296
|
-
if theme is None:
|
|
1297
|
-
return jsonify({
|
|
1298
|
-
'error': f'Theme not found: {name}'
|
|
1299
|
-
}), 404
|
|
1300
|
-
|
|
1301
|
-
return jsonify(theme)
|
|
1302
|
-
except Exception as e:
|
|
1303
|
-
return jsonify({
|
|
1304
|
-
'error': str(e)
|
|
1305
|
-
}), 500
|
|
149
|
+
register_terminal_handlers(socketio)
|
|
150
|
+
register_voice_handlers(socketio)
|
|
1306
151
|
|
|
1307
152
|
|
|
153
|
+
# Entry point for running directly
|
|
1308
154
|
if __name__ == '__main__':
|
|
1309
155
|
app = create_app()
|
|
1310
|
-
# Start session cleanup task
|
|
1311
156
|
session_manager.start_cleanup_task()
|
|
1312
|
-
|
|
1313
|
-
socketio.run(app, debug=True, host='0.0.0.0', port=5858)
|
|
157
|
+
socketio.run(app, debug=True, host='0.0.0.0', port=5858)
|