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.
Files changed (67) hide show
  1. x_ipe/app.py +80 -1236
  2. x_ipe/app.py.bak +1333 -0
  3. x_ipe/handlers/__init__.py +15 -0
  4. x_ipe/handlers/terminal_handlers.py +121 -0
  5. x_ipe/handlers/voice_handlers.py +179 -0
  6. x_ipe/resources/copilot-instructions.md +4 -3
  7. x_ipe/resources/skills/task-execution-guideline/SKILL.md +8 -5
  8. x_ipe/resources/skills/task-type-code-refactor-v2/SKILL.md +33 -0
  9. x_ipe/resources/skills/task-type-feature-breakdown/SKILL.md +29 -11
  10. x_ipe/resources/skills/task-type-feature-refinement/SKILL.md +165 -18
  11. x_ipe/resources/skills/task-type-idea-mockup/SKILL.md +9 -0
  12. x_ipe/resources/skills/task-type-refactoring-analysis/SKILL.md +57 -0
  13. x_ipe/resources/skills/task-type-requirement-gathering/SKILL.md +3 -3
  14. x_ipe/resources/skills/task-type-technical-design/SKILL.md +56 -9
  15. x_ipe/routes/__init__.py +21 -0
  16. x_ipe/routes/ideas_routes.py +297 -0
  17. x_ipe/routes/main_routes.py +186 -0
  18. x_ipe/routes/project_routes.py +189 -0
  19. x_ipe/routes/proxy_routes.py +66 -0
  20. x_ipe/routes/settings_routes.py +137 -0
  21. x_ipe/routes/tools_routes.py +192 -0
  22. x_ipe/routes/uiux_feedback_routes.py +57 -0
  23. x_ipe/services/__init__.py +6 -0
  24. x_ipe/services/proxy_service.py +347 -0
  25. x_ipe/services/uiux_feedback_service.py +137 -0
  26. x_ipe/static/3rdparty/css/bootstrap-icons.css +2078 -0
  27. x_ipe/static/3rdparty/css/bootstrap.min.css +6 -0
  28. x_ipe/static/3rdparty/css/codemirror-dialog.min.css +8 -0
  29. x_ipe/static/3rdparty/css/codemirror.min.css +8 -0
  30. x_ipe/static/3rdparty/css/easymde.min.css +7 -0
  31. x_ipe/static/3rdparty/css/highlight-github.min.css +10 -0
  32. x_ipe/static/3rdparty/css/xterm.css +209 -0
  33. x_ipe/static/3rdparty/fonts/bootstrap-icons.woff +0 -0
  34. x_ipe/static/3rdparty/fonts/bootstrap-icons.woff2 +0 -0
  35. x_ipe/static/3rdparty/js/bootstrap.bundle.min.js +7 -0
  36. x_ipe/static/3rdparty/js/codemirror-dialog.min.js +8 -0
  37. x_ipe/static/3rdparty/js/codemirror-jump-to-line.min.js +8 -0
  38. x_ipe/static/3rdparty/js/codemirror-search.min.js +8 -0
  39. x_ipe/static/3rdparty/js/codemirror-searchcursor.min.js +8 -0
  40. x_ipe/static/3rdparty/js/codemirror.min.js +8 -0
  41. x_ipe/static/3rdparty/js/easymde.min.js +7 -0
  42. x_ipe/static/3rdparty/js/highlight.min.js +1213 -0
  43. x_ipe/static/3rdparty/js/infographic.min.js +589 -0
  44. x_ipe/static/3rdparty/js/marked.min.js +6 -0
  45. x_ipe/static/3rdparty/js/mermaid.min.js +1646 -0
  46. x_ipe/static/3rdparty/js/socket.io.min.js +7 -0
  47. x_ipe/static/3rdparty/js/xterm-addon-fit.js +2 -0
  48. x_ipe/static/3rdparty/js/xterm-addon-unicode11.js +2 -0
  49. x_ipe/static/3rdparty/js/xterm.min.js +8 -0
  50. x_ipe/static/css/sidebar.css +45 -0
  51. x_ipe/static/css/uiux-feedback.css +933 -0
  52. x_ipe/static/js/features/sidebar.js +78 -12
  53. x_ipe/static/js/features/voice-input.js +1 -0
  54. x_ipe/static/js/features/workplace.js +41 -0
  55. x_ipe/static/js/uiux-feedback.js +1377 -0
  56. x_ipe/templates/base.html +30 -39
  57. x_ipe/templates/settings.html +5 -6
  58. x_ipe/templates/uiux-feedbacks.html +87 -0
  59. x_ipe/templates/workplace.html +114 -0
  60. {x_ipe-1.0.16.dist-info → x_ipe-1.0.17.dist-info}/METADATA +2 -1
  61. {x_ipe-1.0.16.dist-info → x_ipe-1.0.17.dist-info}/RECORD +64 -25
  62. x_ipe/resources/skills/task-type-code-refactor/SKILL.md +0 -602
  63. x_ipe/resources/skills/task-type-code-refactor/references/examples.md +0 -206
  64. x_ipe/resources/skills/task-type-code-refactor/references/refactoring-principles.md +0 -200
  65. {x_ipe-1.0.16.dist-info → x_ipe-1.0.17.dist-info}/WHEEL +0 -0
  66. {x_ipe-1.0.16.dist-info → x_ipe-1.0.17.dist-info}/entry_points.txt +0 -0
  67. {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 Document Viewer
3
-
4
- FEATURE-001: Project Navigation
5
- - Provides API for project structure
6
- - HTTP polling for real-time updates (no WebSocket)
7
- - Serves frontend with sidebar navigation
8
-
9
- FEATURE-005: Interactive Console v4.0
10
- - WebSocket handlers for terminal I/O
11
- - Session persistence with automatic reconnection
12
- - xterm.js frontend integration
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, render_template, jsonify, request, current_app, send_file
19
- from flask_socketio import SocketIO, emit
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: # Don't override existing env vars
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
- # Global project folders service instance (FEATURE-006 v2.0)
49
- project_folders_service = None
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, # Wait 5 minutes for pong response (was 60s)
63
- ping_interval=60, # Send ping every 60s (was 25s)
64
- max_http_buffer_size=1e8, # 100MB max message size
65
- always_connect=True, # Always emit connect even on reconnect
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
- # Improved stability settings
69
- http_compression=True, # Compress HTTP responses
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 settings service
98
- global settings_service, project_folders_service, ideas_service, config_service, tools_config_service
99
- db_path = app.config.get('SETTINGS_DB_PATH', app.config.get('SETTINGS_DB', os.path.join(app.instance_path, 'settings.db')))
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 register_routes(app):
140
- """Register all application routes"""
141
-
142
- @app.route('/')
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
- @app.route('/api/file/content')
168
- def get_file_content():
169
- """
170
- GET /api/file/content?path=<relative_path>&raw=<true/false>
171
-
172
- Returns the content of a file with metadata for rendering.
173
- If raw=true or file is binary (images, etc.), serves the raw file content.
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
- # FEATURE-005: Interactive Console v4.0 - WebSocket Handlers
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
- @socketio.on('resize')
375
- def handle_resize(data):
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
- # Use socketio.run for WebSocket support
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)