ayushm98 commited on
Commit
85d7785
·
1 Parent(s): 0887cf4

Add clean UI with agent progress, copyable code, and results table

Browse files

New v3.4.0 features:
- Live agent progress table (Explorer → Planner → Coder → Reviewer)
- Status icons: ✅ done, ⏳ working, ⬜ waiting
- Copyable code blocks with syntax highlighting
- Review results table showing pass/fail for each step
- Cost summary displayed throughout

Files changed (2) hide show
  1. Dockerfile +1 -1
  2. chainlit_app.py +320 -385
Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
  # HuggingFace Spaces Dockerfile for CodePilot
2
- # BUILD_VERSION: 16 (v3.3.7 validate-all - validate all messages for tool_use/result pairs)
3
  FROM python:3.11-slim
4
 
5
  # Set working directory
 
1
  # HuggingFace Spaces Dockerfile for CodePilot
2
+ # BUILD_VERSION: 17 (v3.4.0 clean-ui - clean progress display, copyable code, results table)
3
  FROM python:3.11-slim
4
 
5
  # Set working directory
chainlit_app.py CHANGED
@@ -2,17 +2,19 @@
2
  Chainlit UI for CodePilot Multi-Agent System
3
 
4
  This provides a chat interface showing detailed agent workflow:
 
5
  - Planner creates implementation plans
6
- - Coder writes code, uploads to sandbox, runs tests
7
  - Reviewer checks and approves code
8
 
9
- User can see every step in real-time.
10
  """
11
 
12
  import chainlit as cl
13
  import os
14
  import sys
15
  import io
 
16
  from contextlib import redirect_stdout, redirect_stderr
17
  import asyncio
18
  from concurrent.futures import ThreadPoolExecutor
@@ -20,8 +22,8 @@ from concurrent.futures import ThreadPoolExecutor
20
  # ============================================================
21
  # STARTUP VERSION CHECK - Change this to detect if rebuild worked
22
  # ============================================================
23
- APP_VERSION = "3.3.7-validate-all"
24
- BUILD_ID = "2024-12-19-v15"
25
  print("=" * 60)
26
  print(f"[STARTUP] CodePilot Chainlit App")
27
  print(f"[STARTUP] APP_VERSION: {APP_VERSION}")
@@ -47,31 +49,186 @@ from codepilot.tools.github_tools import (
47
  )
48
 
49
 
50
- # Authentication disabled for now - uncomment to enable password protection
51
- # @cl.password_auth_callback
52
- # def auth_callback(username: str, password: str):
53
- # """
54
- # Simple password authentication for CodePilot.
55
- #
56
- # For production, use environment variables and proper password hashing.
57
- # """
58
- # # Get password from environment variable (more secure)
59
- # required_password = os.getenv('CHAINLIT_PASSWORD', 'codepilot2024')
60
- #
61
- # # In production, you should hash passwords and use a proper auth system
62
- # if password == required_password:
63
- # return cl.User(
64
- # identifier=username,
65
- # metadata={"role": "user", "provider": "credentials"}
66
- # )
67
- # return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
 
70
  @cl.on_chat_start
71
  async def start():
72
  """Initialize the agent system when chat starts."""
73
 
74
- print("[CHAINLIT] on_chat_start triggered") # Debug log
75
 
76
  await cl.Message(
77
  content=f"# CodePilot - Autonomous AI Coding Agent\n\n"
@@ -80,20 +237,17 @@ async def start():
80
  "**How to use:**\n"
81
  "1. Paste a **public GitHub URL** and I'll clone and analyze it\n"
82
  "2. Tell me what you want to build or fix\n"
83
- "3. Watch my agents (Planner > Coder > Reviewer) work!\n\n"
84
  "**Example:**\n"
85
- "```\nAnalyze https://github.com/user/repo and add error handling to the API endpoints\n```\n\n"
86
- "**Ready!** Paste a GitHub URL or describe your task."
87
  ).send()
88
 
89
- print("[CHAINLIT] Welcome message sent") # Debug log
90
 
91
  # Initialize session variables
92
  cl.user_session.set("repo_path", None)
93
  cl.user_session.set("repo_info", None)
94
-
95
- # Skip self-indexing - agents will only work with cloned GitHub repos
96
- # Create orchestrator and mark as ready
97
  cl.user_session.set("orchestrator", Orchestrator(max_iterations=10))
98
  cl.user_session.set("ready", True)
99
  print("[CHAINLIT] Orchestrator created, ready for GitHub repos")
@@ -102,115 +256,138 @@ async def start():
102
  @cl.on_chat_end
103
  async def end():
104
  """Cleanup when chat ends."""
105
- # Clean up any cloned repositories
106
  repo_path = cl.user_session.get("repo_path")
107
  if repo_path:
108
  print(f"[CHAINLIT] Cleaning up repo: {repo_path}")
109
  cleanup_repository(repo_path)
110
 
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  @cl.on_message
113
  async def main(message: cl.Message):
114
  """Handle user messages and run the agent workflow."""
115
 
116
- # Check if ready
117
  if not cl.user_session.get("ready"):
118
  await cl.Message(content="System is still initializing, please wait...").send()
119
  return
120
 
121
- # Get orchestrator
122
  orchestrator: Orchestrator = cl.user_session.get("orchestrator")
123
 
124
- # Check if we're waiting for clarification answers
125
- # If NOT waiting for clarification, this is a NEW task - reset orchestrator
126
- if not cl.user_session.get("waiting_for_clarification"):
127
- # Create fresh orchestrator for new tasks
128
- orchestrator = Orchestrator(max_iterations=10)
129
- cl.user_session.set("orchestrator", orchestrator)
130
- print("[CHAINLIT] Created fresh orchestrator for new task")
131
  if cl.user_session.get("waiting_for_clarification"):
132
  cl.user_session.set("waiting_for_clarification", False)
133
  user_answers = message.content
134
 
135
- await cl.Message(content="Got it! Let me create the plan with your clarifications...").send()
136
-
137
- # Resume the orchestrator with user answers
138
- log_msg = cl.Message(content="")
139
- await log_msg.send()
140
-
141
- try:
142
- captured_output = io.StringIO()
143
-
144
- def resume_orchestrator():
145
- with redirect_stdout(captured_output), redirect_stderr(captured_output):
146
- return orchestrator.resume_after_clarification(user_answers)
147
-
148
- loop = asyncio.get_event_loop()
149
- executor = ThreadPoolExecutor(max_workers=1)
150
- future = loop.run_in_executor(executor, resume_orchestrator)
151
-
152
- # Track tokens
153
- total_prompt_tokens = 0
154
- total_completion_tokens = 0
155
- total_tokens = 0
156
- seen_token_lines = set()
157
-
158
- # Stream logs
159
- accumulated_logs = ""
160
- while not future.done():
161
- await asyncio.sleep(0.5)
162
- current_output = captured_output.getvalue()
163
- if current_output != accumulated_logs:
164
- accumulated_logs = current_output
165
- filtered_lines = []
166
- for line in accumulated_logs.split('\n'):
167
- if 'Tokens:' in line and line not in seen_token_lines:
168
- seen_token_lines.add(line)
169
- try:
170
- parts = line.split('Tokens:')[1].strip()
171
- prompt = int(parts.split('prompt')[0].strip())
172
- completion = int(parts.split('+')[1].split('completion')[0].strip())
173
- total_prompt_tokens += prompt
174
- total_completion_tokens += completion
175
- total_tokens += (prompt + completion)
176
- except:
177
- pass
178
- if any(skip in line for skip in ['Tokens:', 'Batches:', '|##', 'it/s]']):
179
- continue
180
- if any(keep in line for keep in [
181
- '[CLASSIFIER]', '[ORCHESTRATOR]', '[PLANNER]', '[CODER]', '[REVIEWER]',
182
- '[EXPLORER]', 'Calling tool:', 'Transitioning', 'APPROVED', 'REJECTED'
183
- ]):
184
- filtered_lines.append(line)
185
- filtered_output = '\n'.join(filtered_lines)
186
- input_cost = (total_prompt_tokens / 1000000) * 3.0
187
- output_cost = (total_completion_tokens / 1000000) * 15.0
188
- total_cost = input_cost + output_cost
189
- usage_summary = f"\n\nCREDITS: ${total_cost:.4f}"
190
- log_msg.content = f"```\n{filtered_output}{usage_summary}\n```"
191
- await log_msg.update()
192
-
193
- result = await future
194
- # Continue to show results (handled by falling through to normal result handling below)
195
- # For now, show summary directly
196
- summary = f"## Result\n**Status:** {result.get('status')}\n"
197
  if result.get('code_changes'):
198
- summary += f"**Files created:** {len(result['code_changes'])}\n"
199
- summary += f"**Cost:** ${total_cost:.4f}"
200
- await cl.Message(content=summary).send()
201
- return
202
 
203
- except Exception as e:
204
- await cl.Message(content=f"Error resuming: {str(e)}").send()
205
- return
 
206
 
207
- # Check for GitHub URL in message
208
  github_url = extract_github_url(message.content)
209
  task_context = ""
210
 
211
  if github_url:
212
- # Clone the repository
213
- clone_msg = await cl.Message(content=f"Cloning repository: `{github_url}`...").send()
214
 
215
  success, result, repo_name = clone_repository(github_url)
216
 
@@ -218,24 +395,16 @@ async def main(message: cl.Message):
218
  repo_path = result
219
  repo_info = get_repo_info(repo_path)
220
 
221
- # Store in session
222
  cl.user_session.set("repo_path", repo_path)
223
  cl.user_session.set("repo_info", repo_info)
224
 
225
- # Index the repository for search (full BM25 + embeddings)
226
  try:
227
- index_result = index_codebase(repo_path)
228
- print(f"[CHAINLIT] Repository indexed: {index_result}")
229
  except Exception as e:
230
- print(f"[CHAINLIT] Indexing failed (non-critical): {e}")
231
 
232
- # Create context for the task (limited to avoid token overflow)
233
  languages = ", ".join(repo_info["languages"][:5]) if repo_info["languages"] else "Unknown"
234
- # Only include first 20 files to keep context small
235
- sample_files = repo_info["files"][:20] if repo_info["files"] else []
236
- files_preview = "\n".join(f" - {f}" for f in sample_files)
237
- if len(repo_info["files"]) > 20:
238
- files_preview += f"\n ... and {len(repo_info['files']) - 20} more files"
239
 
240
  task_context = f"""
241
  [REPOSITORY CONTEXT]
@@ -243,30 +412,19 @@ Repository: {repo_name}
243
  Path: {repo_path}
244
  Total Files: {repo_info['total_files']}
245
  Languages: {languages}
246
- Sample Files:
247
- {files_preview}
248
 
249
  AVAILABLE TOOLS:
250
- - search_repository: Search this cloned repository using BM25 keyword matching (use this to find functions, classes, or code patterns in the Flask repo)
251
- - read_file: Read a specific file (use full path: {repo_path}/filename.py)
252
- - search_code: Grep for exact pattern matches in the repository
253
  """
254
- # Update clone message
255
- clone_msg.content = f"**Repository cloned successfully!**\n\n" \
256
- f"- **Name:** {repo_name}\n" \
257
- f"- **Files:** {repo_info['total_files']}\n" \
258
- f"- **Languages:** {languages}\n" \
259
- f"- **Path:** `{repo_path}`"
260
  await clone_msg.update()
261
-
262
  else:
263
- # Clone failed
264
- clone_msg.content = f"**Failed to clone repository**\n\n{result}\n\n" \
265
- f"Make sure the repository is public and the URL is correct."
266
  await clone_msg.update()
267
  return
268
 
269
- # Check if we have a repo from previous message
270
  elif cl.user_session.get("repo_path"):
271
  repo_path = cl.user_session.get("repo_path")
272
  repo_info = cl.user_session.get("repo_info")
@@ -280,266 +438,43 @@ Total Files: {repo_info['total_files']}
280
  Languages: {languages}
281
 
282
  AVAILABLE TOOLS:
283
- - search_repository: Search this cloned repository using BM25 keyword matching (use this to find functions, classes, or code patterns in the Flask repo)
284
- - read_file: Read a specific file (use full path: {repo_path}/filename.py)
285
- - search_code: Grep for exact pattern matches in the repository
286
  """
287
 
288
- # Prepare the full task with context
289
- # Remove the GitHub URL from the message to get just the user's query
290
  user_query = message.content
291
- print(f"[DEBUG] Original message.content: '{message.content}'")
292
- print(f"[DEBUG] GitHub URL found: '{github_url}'")
293
-
294
  if github_url:
295
- # Remove the URL from the message to get the actual task
296
- import re
297
  user_query = re.sub(r'https?://github\.com/[^\s]+', '', user_query).strip()
298
- print(f"[DEBUG] After URL removal: '{user_query}'")
299
 
300
  full_task = task_context + "\n\n" + user_query if task_context else user_query
301
 
302
- print(f"[DEBUG] task_context exists: {bool(task_context)}")
303
- print(f"[DEBUG] task_context length: {len(task_context) if task_context else 0}")
304
- print(f"[DEBUG] Final user_query: '{user_query}'")
305
- print(f"[DEBUG] Full task (first 500 chars): '{full_task[:500]}...'")
306
 
307
- # Create a message for streaming logs
308
- log_msg = cl.Message(content="")
309
- await log_msg.send()
310
-
311
- try:
312
- # Capture stdout/stderr to stream logs
313
- captured_output = io.StringIO()
314
-
315
- def run_orchestrator():
316
- """Run orchestrator in thread and capture output."""
317
- try:
318
- with redirect_stdout(captured_output), redirect_stderr(captured_output):
319
- return orchestrator.run(full_task)
320
- except Exception as e:
321
- # Capture any exceptions from orchestrator
322
- print(f"Error in orchestrator: {str(e)}")
323
- import traceback
324
- traceback.print_exc()
325
- raise
326
-
327
- # Run in thread pool to avoid blocking
328
- loop = asyncio.get_event_loop()
329
- executor = ThreadPoolExecutor(max_workers=1)
330
-
331
- # Start the orchestrator in background
332
- future = loop.run_in_executor(executor, run_orchestrator)
333
-
334
- # Track API usage
335
- total_prompt_tokens = 0
336
- total_completion_tokens = 0
337
- total_tokens = 0
338
- seen_token_lines = set() # Track which token lines we've already counted
339
-
340
- # Stream logs while orchestrator is running - FILTERED
341
- accumulated_logs = ""
342
- while not future.done():
343
- await asyncio.sleep(0.5) # Check every 500ms
344
-
345
- # Get new output
346
- current_output = captured_output.getvalue()
347
- if current_output != accumulated_logs:
348
- accumulated_logs = current_output
349
-
350
- # Filter logs to show only important lines
351
- filtered_lines = []
352
- for line in accumulated_logs.split('\n'):
353
- # Extract token usage before filtering (only count each line once!)
354
- if 'Tokens:' in line and line not in seen_token_lines:
355
- seen_token_lines.add(line) # Mark as counted
356
- try:
357
- # Parse: "Tokens: 505 prompt + 20 completion = 525 total"
358
- parts = line.split('Tokens:')[1].strip()
359
- prompt = int(parts.split('prompt')[0].strip())
360
- completion = int(parts.split('+')[1].split('completion')[0].strip())
361
- total_prompt_tokens += prompt
362
- total_completion_tokens += completion
363
- total_tokens += (prompt + completion)
364
- except:
365
- pass
366
-
367
- # Skip token counts, progress bars, and verbose details
368
- if any(skip in line for skip in ['Tokens:', 'Batches:', '|##', 'it/s]']):
369
- continue
370
- # Keep important lines
371
- if any(keep in line for keep in [
372
- '[CLASSIFIER]', '[ORCHESTRATOR]', '[PLANNER]', '[CODER]', '[REVIEWER]',
373
- '[EXPLORER]', 'Calling tool:', 'Tool', 'Transitioning', 'APPROVED', 'REJECTED',
374
- '[GITHUB]', 'Cloning', 'Repository'
375
- ]):
376
- filtered_lines.append(line)
377
-
378
- filtered_output = '\n'.join(filtered_lines)
379
-
380
- # Calculate cost (Claude Sonnet 4.5 pricing: $3/1M input, $15/1M output)
381
- input_cost = (total_prompt_tokens / 1000000) * 3.0
382
- output_cost = (total_completion_tokens / 1000000) * 15.0
383
- total_cost = input_cost + output_cost
384
-
385
- # Add usage summary to logs
386
- usage_summary = f"\n\nCREDITS USED:\n"
387
- usage_summary += f" Input: {total_prompt_tokens:,} tokens (${input_cost:.4f})\n"
388
- usage_summary += f" Output: {total_completion_tokens:,} tokens (${output_cost:.4f})\n"
389
- usage_summary += f" Total: {total_tokens:,} tokens (${total_cost:.4f})"
390
-
391
- # Update message with filtered logs + usage
392
- log_msg.content = f"```\n{filtered_output}\n{usage_summary}\n```"
393
- await log_msg.update()
394
-
395
- # Get final result
396
- result = await future
397
-
398
- # Get final logs
399
- final_logs = captured_output.getvalue()
400
-
401
- # Update with final logs
402
- log_msg.content = f"## Execution Log\n```\n{final_logs}\n```"
403
- await log_msg.update()
404
-
405
- # Check if we need clarification from user
406
- if result.get('status') == 'clarifying' and result.get('clarifying_questions'):
407
- questions = result['clarifying_questions']
408
- # Store that we're waiting for clarification
409
- cl.user_session.set("waiting_for_clarification", True)
410
-
411
- await cl.Message(
412
- content=f"## Before I proceed, I have some questions:\n\n{questions}\n\n"
413
- f"**Please answer the questions above so I can create a better plan.**"
414
- ).send()
415
- return # Wait for user to respond
416
-
417
- # Send results summary
418
- summary_lines = []
419
-
420
- if result.get('plan'):
421
- summary_lines.append("## Planner")
422
- summary_lines.append(f"Plan created ({len(result['plan'])} chars)\n")
423
-
424
- if result.get('code_changes'):
425
- summary_lines.append("## Coder")
426
- summary_lines.append(f"Created {len(result['code_changes'])} file(s):")
427
- for file_path in result['code_changes'].keys():
428
- summary_lines.append(f" - {file_path}")
429
- summary_lines.append("")
430
-
431
- if result.get('review_feedback'):
432
- summary_lines.append("## Reviewer")
433
- if result.get('success'):
434
- summary_lines.append("Code approved")
435
- else:
436
- summary_lines.append("Needs revision")
437
- summary_lines.append("")
438
-
439
- summary_lines.append("## Result")
440
- if result.get('success'):
441
- summary_lines.append(f"**Success** (Iterations: {result.get('iterations', 'N/A')})")
442
- else:
443
- summary_lines.append(f"**Incomplete** (Iterations: {result.get('iterations', 'N/A')})")
444
-
445
- # Add final cost summary (Claude Sonnet 4.5 pricing: $3/1M input, $15/1M output)
446
- summary_lines.append("\n## API Credits Used (Claude Sonnet 4.5)")
447
- summary_lines.append(f"**Total Tokens:** {total_tokens:,}")
448
- summary_lines.append(f"- Input: {total_prompt_tokens:,} tokens (${(total_prompt_tokens/1000000)*3.0:.4f})")
449
- summary_lines.append(f"- Output: {total_completion_tokens:,} tokens (${(total_completion_tokens/1000000)*15.0:.4f})")
450
- summary_lines.append(f"\n**Estimated Cost:** ${total_cost:.4f}")
451
-
452
- await cl.Message(content="\n".join(summary_lines)).send()
453
-
454
- except Exception as e:
455
- # Determine error type and provide specific guidance
456
- error_message = str(e)
457
- error_type = type(e).__name__
458
-
459
- if "rate_limit" in error_message.lower() or "429" in error_message:
460
- user_message = f"""## Rate Limit Reached
461
-
462
- Claude API rate limit exceeded. This happens when too many requests are made in a short time.
463
-
464
- **What to do:**
465
- - Wait a few minutes and try again
466
- - Reduce max_iterations (currently: {orchestrator.max_iterations})
467
- - Your request will work once the rate limit resets
468
-
469
- **Error details:**
470
- ```
471
- {error_message}
472
- ```
473
- """
474
- elif "insufficient_quota" in error_message.lower() or "credit" in error_message.lower():
475
- user_message = f"""## API Credits Exhausted
476
-
477
- Your Anthropic API credits have been exhausted.
478
-
479
- **What to do:**
480
- - Add credits to your Anthropic account at https://console.anthropic.com/settings/billing
481
- - Check your usage at https://console.anthropic.com/settings/usage
482
- - Current model: Claude Sonnet 4.5 (~$0.20 per task)
483
-
484
- **Error details:**
485
- ```
486
- {error_message}
487
- ```
488
- """
489
- elif "api_key" in error_message.lower() or "authentication" in error_message.lower():
490
- user_message = f"""## API Key Error
491
-
492
- There's an issue with your Anthropic API key.
493
-
494
- **What to do:**
495
- - Verify your ANTHROPIC_API_KEY in .env file
496
- - Check that the key is valid at https://console.anthropic.com/settings/keys
497
- - Restart the application after updating .env
498
-
499
- **Error details:**
500
- ```
501
- {error_message}
502
- ```
503
- """
504
- elif "timeout" in error_message.lower():
505
- user_message = f"""## Request Timeout
506
-
507
- The operation took too long and timed out.
508
-
509
- **What to do:**
510
- - Try again with a simpler task
511
- - The task may be too complex for one iteration
512
- - Consider breaking it into smaller steps
513
-
514
- **Error details:**
515
- ```
516
- {error_message}
517
- ```
518
- """
519
- else:
520
- # Generic error with helpful context
521
- user_message = f"""## Error Occurred
522
-
523
- An unexpected error occurred during execution.
524
-
525
- **Error type:** {error_type}
526
 
527
- **What to do:**
528
- - Try rephrasing your request
529
- - Check if all required files/dependencies exist
530
- - Verify your .env file has all required API keys
 
 
 
531
 
532
- **Error details:**
533
- ```
534
- {error_message}
535
- ```
536
 
537
- If this persists, please report the issue with the error details above.
538
- """
 
539
 
540
- await cl.Message(content=user_message).send()
 
 
541
 
542
 
543
  if __name__ == "__main__":
544
- import sys
545
  sys.exit("Run with: chainlit run chainlit_app.py")
 
2
  Chainlit UI for CodePilot Multi-Agent System
3
 
4
  This provides a chat interface showing detailed agent workflow:
5
+ - Explorer searches the codebase
6
  - Planner creates implementation plans
7
+ - Coder writes code
8
  - Reviewer checks and approves code
9
 
10
+ User sees clean progress updates and copyable code output.
11
  """
12
 
13
  import chainlit as cl
14
  import os
15
  import sys
16
  import io
17
+ import re
18
  from contextlib import redirect_stdout, redirect_stderr
19
  import asyncio
20
  from concurrent.futures import ThreadPoolExecutor
 
22
  # ============================================================
23
  # STARTUP VERSION CHECK - Change this to detect if rebuild worked
24
  # ============================================================
25
+ APP_VERSION = "3.4.0-clean-ui"
26
+ BUILD_ID = "2024-12-20-v1"
27
  print("=" * 60)
28
  print(f"[STARTUP] CodePilot Chainlit App")
29
  print(f"[STARTUP] APP_VERSION: {APP_VERSION}")
 
49
  )
50
 
51
 
52
+ def get_file_extension(file_path: str) -> str:
53
+ """Get language identifier for syntax highlighting."""
54
+ ext_map = {
55
+ '.py': 'python',
56
+ '.js': 'javascript',
57
+ '.ts': 'typescript',
58
+ '.jsx': 'jsx',
59
+ '.tsx': 'tsx',
60
+ '.html': 'html',
61
+ '.css': 'css',
62
+ '.json': 'json',
63
+ '.md': 'markdown',
64
+ '.yml': 'yaml',
65
+ '.yaml': 'yaml',
66
+ '.sql': 'sql',
67
+ '.sh': 'bash',
68
+ '.rs': 'rust',
69
+ '.go': 'go',
70
+ '.java': 'java',
71
+ '.rb': 'ruby',
72
+ '.php': 'php',
73
+ }
74
+ ext = os.path.splitext(file_path)[1].lower()
75
+ return ext_map.get(ext, '')
76
+
77
+
78
+ def format_code_output(code_changes: dict) -> str:
79
+ """Format code changes as copyable markdown code blocks."""
80
+ if not code_changes:
81
+ return "No code changes."
82
+
83
+ output = []
84
+ for file_path, content in code_changes.items():
85
+ # Get just the filename for display
86
+ filename = os.path.basename(file_path)
87
+ lang = get_file_extension(file_path)
88
+
89
+ output.append(f"### `{filename}`")
90
+ output.append(f"**Full path:** `{file_path}`")
91
+ output.append(f"```{lang}")
92
+ output.append(content)
93
+ output.append("```")
94
+ output.append("")
95
+
96
+ return "\n".join(output)
97
+
98
+
99
+ def parse_agent_status(logs: str) -> dict:
100
+ """Parse logs to extract agent status information."""
101
+ status = {
102
+ 'current_agent': None,
103
+ 'explorer_done': False,
104
+ 'planner_done': False,
105
+ 'coder_done': False,
106
+ 'reviewer_done': False,
107
+ 'approved': None,
108
+ 'tools_called': [],
109
+ }
110
+
111
+ for line in logs.split('\n'):
112
+ if '[EXPLORER]' in line:
113
+ status['current_agent'] = 'Explorer'
114
+ if 'Calling tool:' in line:
115
+ tool = line.split('Calling tool:')[1].strip()
116
+ status['tools_called'].append(f"Explorer: {tool}")
117
+ elif '[PLANNER]' in line:
118
+ status['current_agent'] = 'Planner'
119
+ if 'Plan created' in line:
120
+ status['planner_done'] = True
121
+ elif '[CODER]' in line:
122
+ status['current_agent'] = 'Coder'
123
+ if 'Calling tool:' in line:
124
+ tool = line.split('Calling tool:')[1].strip()
125
+ status['tools_called'].append(f"Coder: {tool}")
126
+ if 'Finished implementation' in line:
127
+ status['coder_done'] = True
128
+ elif '[REVIEWER]' in line:
129
+ status['current_agent'] = 'Reviewer'
130
+ if 'Calling tool:' in line:
131
+ tool = line.split('Calling tool:')[1].strip()
132
+ status['tools_called'].append(f"Reviewer: {tool}")
133
+ elif 'APPROVED' in line:
134
+ status['approved'] = True
135
+ status['reviewer_done'] = True
136
+ elif 'REJECTED' in line:
137
+ status['approved'] = False
138
+ status['reviewer_done'] = True
139
+ elif 'Transitioning to CLARIFYING' in line:
140
+ status['explorer_done'] = True
141
+ elif 'Transitioning to PLANNING' in line:
142
+ status['explorer_done'] = True
143
+ elif 'Transitioning to CODING' in line:
144
+ status['planner_done'] = True
145
+ elif 'Transitioning to REVIEWING' in line:
146
+ status['coder_done'] = True
147
+ elif 'Transitioning to COMPLETE' in line:
148
+ status['reviewer_done'] = True
149
+
150
+ return status
151
+
152
+
153
+ def format_progress_display(status: dict, total_cost: float) -> str:
154
+ """Format a clean progress display."""
155
+
156
+ def icon(done: bool, active: bool = False) -> str:
157
+ if done:
158
+ return "✅"
159
+ elif active:
160
+ return "⏳"
161
+ else:
162
+ return "⬜"
163
+
164
+ current = status['current_agent']
165
+
166
+ lines = ["## Agent Progress\n"]
167
+ lines.append("| Agent | Status |")
168
+ lines.append("|-------|--------|")
169
+ lines.append(f"| Explorer | {icon(status['explorer_done'], current == 'Explorer')} {'Searching codebase...' if current == 'Explorer' and not status['explorer_done'] else 'Done' if status['explorer_done'] else 'Waiting'} |")
170
+ lines.append(f"| Planner | {icon(status['planner_done'], current == 'Planner')} {'Creating plan...' if current == 'Planner' and not status['planner_done'] else 'Done' if status['planner_done'] else 'Waiting'} |")
171
+ lines.append(f"| Coder | {icon(status['coder_done'], current == 'Coder')} {'Writing code...' if current == 'Coder' and not status['coder_done'] else 'Done' if status['coder_done'] else 'Waiting'} |")
172
+
173
+ reviewer_status = 'Waiting'
174
+ if current == 'Reviewer' and not status['reviewer_done']:
175
+ reviewer_status = 'Reviewing...'
176
+ elif status['reviewer_done']:
177
+ reviewer_status = '**APPROVED**' if status['approved'] else '**REJECTED**'
178
+ lines.append(f"| Reviewer | {icon(status['reviewer_done'], current == 'Reviewer')} {reviewer_status} |")
179
+
180
+ lines.append(f"\n**Cost:** ${total_cost:.4f}")
181
+
182
+ return "\n".join(lines)
183
+
184
+
185
+ def format_final_result(result: dict, total_cost: float) -> str:
186
+ """Format the final result with test table and summary."""
187
+ lines = []
188
+
189
+ # Status header
190
+ success = result.get('success', False)
191
+ status_icon = "✅" if success else "❌"
192
+ lines.append(f"## Result: {status_icon} {'Success' if success else 'Failed'}\n")
193
+
194
+ # Review result table
195
+ lines.append("### Review Results\n")
196
+ lines.append("| Check | Result |")
197
+ lines.append("|-------|--------|")
198
+
199
+ if result.get('plan'):
200
+ lines.append("| Plan Created | ✅ Pass |")
201
+ else:
202
+ lines.append("| Plan Created | ❌ Fail |")
203
+
204
+ if result.get('code_changes'):
205
+ lines.append(f"| Code Written | ✅ Pass ({len(result['code_changes'])} files) |")
206
+ else:
207
+ lines.append("| Code Written | ❌ Fail |")
208
+
209
+ if result.get('review_feedback'):
210
+ if success:
211
+ lines.append("| Code Review | ✅ Approved |")
212
+ else:
213
+ lines.append("| Code Review | ❌ Rejected |")
214
+ else:
215
+ lines.append("| Code Review | ⬜ Not Run |")
216
+
217
+ lines.append("")
218
+
219
+ # Cost summary
220
+ lines.append("### Cost Summary\n")
221
+ lines.append(f"**Total Cost:** ${total_cost:.4f}")
222
+ lines.append(f"**Iterations:** {result.get('iterations', 'N/A')}")
223
+
224
+ return "\n".join(lines)
225
 
226
 
227
  @cl.on_chat_start
228
  async def start():
229
  """Initialize the agent system when chat starts."""
230
 
231
+ print("[CHAINLIT] on_chat_start triggered")
232
 
233
  await cl.Message(
234
  content=f"# CodePilot - Autonomous AI Coding Agent\n\n"
 
237
  "**How to use:**\n"
238
  "1. Paste a **public GitHub URL** and I'll clone and analyze it\n"
239
  "2. Tell me what you want to build or fix\n"
240
+ "3. Watch my agents (Explorer → Planner Coder Reviewer) work!\n\n"
241
  "**Example:**\n"
242
+ "```\nhttps://github.com/pallets/flask add a health check endpoint example\n```\n\n"
243
+ "**Ready!** Paste a GitHub URL with your task."
244
  ).send()
245
 
246
+ print("[CHAINLIT] Welcome message sent")
247
 
248
  # Initialize session variables
249
  cl.user_session.set("repo_path", None)
250
  cl.user_session.set("repo_info", None)
 
 
 
251
  cl.user_session.set("orchestrator", Orchestrator(max_iterations=10))
252
  cl.user_session.set("ready", True)
253
  print("[CHAINLIT] Orchestrator created, ready for GitHub repos")
 
256
  @cl.on_chat_end
257
  async def end():
258
  """Cleanup when chat ends."""
 
259
  repo_path = cl.user_session.get("repo_path")
260
  if repo_path:
261
  print(f"[CHAINLIT] Cleaning up repo: {repo_path}")
262
  cleanup_repository(repo_path)
263
 
264
 
265
+ async def run_workflow(orchestrator, task_or_answers, is_resume=False):
266
+ """Run the orchestrator workflow and display clean progress."""
267
+
268
+ # Create progress message
269
+ progress_msg = cl.Message(content="## Agent Progress\n\nStarting...")
270
+ await progress_msg.send()
271
+
272
+ try:
273
+ captured_output = io.StringIO()
274
+
275
+ def run_task():
276
+ with redirect_stdout(captured_output), redirect_stderr(captured_output):
277
+ if is_resume:
278
+ return orchestrator.resume_after_clarification(task_or_answers)
279
+ else:
280
+ return orchestrator.run(task_or_answers)
281
+
282
+ loop = asyncio.get_event_loop()
283
+ executor = ThreadPoolExecutor(max_workers=1)
284
+ future = loop.run_in_executor(executor, run_task)
285
+
286
+ # Track tokens for cost calculation
287
+ total_prompt_tokens = 0
288
+ total_completion_tokens = 0
289
+ seen_token_lines = set()
290
+
291
+ # Stream progress updates
292
+ accumulated_logs = ""
293
+ while not future.done():
294
+ await asyncio.sleep(0.3)
295
+
296
+ current_output = captured_output.getvalue()
297
+ if current_output != accumulated_logs:
298
+ accumulated_logs = current_output
299
+
300
+ # Extract token usage
301
+ for line in accumulated_logs.split('\n'):
302
+ if 'Tokens:' in line and line not in seen_token_lines:
303
+ seen_token_lines.add(line)
304
+ try:
305
+ parts = line.split('Tokens:')[1].strip()
306
+ prompt = int(parts.split('prompt')[0].strip())
307
+ completion = int(parts.split('+')[1].split('completion')[0].strip())
308
+ total_prompt_tokens += prompt
309
+ total_completion_tokens += completion
310
+ except:
311
+ pass
312
+
313
+ # Calculate cost
314
+ total_cost = (total_prompt_tokens / 1000000) * 3.0 + (total_completion_tokens / 1000000) * 15.0
315
+
316
+ # Parse and display progress
317
+ status = parse_agent_status(accumulated_logs)
318
+ progress_msg.content = format_progress_display(status, total_cost)
319
+ await progress_msg.update()
320
+
321
+ # Get final result
322
+ result = await future
323
+
324
+ # Final token count
325
+ final_logs = captured_output.getvalue()
326
+ for line in final_logs.split('\n'):
327
+ if 'Tokens:' in line and line not in seen_token_lines:
328
+ seen_token_lines.add(line)
329
+ try:
330
+ parts = line.split('Tokens:')[1].strip()
331
+ prompt = int(parts.split('prompt')[0].strip())
332
+ completion = int(parts.split('+')[1].split('completion')[0].strip())
333
+ total_prompt_tokens += prompt
334
+ total_completion_tokens += completion
335
+ except:
336
+ pass
337
+
338
+ total_cost = (total_prompt_tokens / 1000000) * 3.0 + (total_completion_tokens / 1000000) * 15.0
339
+
340
+ # Update progress with final status
341
+ status = parse_agent_status(final_logs)
342
+ progress_msg.content = format_progress_display(status, total_cost)
343
+ await progress_msg.update()
344
+
345
+ return result, total_cost
346
+
347
+ except Exception as e:
348
+ await cl.Message(content=f"## Error\n\n```\n{str(e)}\n```").send()
349
+ return None, 0
350
+
351
+
352
  @cl.on_message
353
  async def main(message: cl.Message):
354
  """Handle user messages and run the agent workflow."""
355
 
 
356
  if not cl.user_session.get("ready"):
357
  await cl.Message(content="System is still initializing, please wait...").send()
358
  return
359
 
 
360
  orchestrator: Orchestrator = cl.user_session.get("orchestrator")
361
 
362
+ # Handle clarification answers
 
 
 
 
 
 
363
  if cl.user_session.get("waiting_for_clarification"):
364
  cl.user_session.set("waiting_for_clarification", False)
365
  user_answers = message.content
366
 
367
+ await cl.Message(content="Thanks! Creating plan with your input...").send()
368
+
369
+ result, total_cost = await run_workflow(orchestrator, user_answers, is_resume=True)
370
+
371
+ if result:
372
+ # Show final result
373
+ await cl.Message(content=format_final_result(result, total_cost)).send()
374
+
375
+ # Show code if any
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  if result.get('code_changes'):
377
+ await cl.Message(content="## Generated Code\n\n" + format_code_output(result['code_changes'])).send()
378
+ return
 
 
379
 
380
+ # New task - reset orchestrator
381
+ orchestrator = Orchestrator(max_iterations=10)
382
+ cl.user_session.set("orchestrator", orchestrator)
383
+ print("[CHAINLIT] Created fresh orchestrator for new task")
384
 
385
+ # Check for GitHub URL
386
  github_url = extract_github_url(message.content)
387
  task_context = ""
388
 
389
  if github_url:
390
+ clone_msg = await cl.Message(content=f"📦 Cloning `{github_url}`...").send()
 
391
 
392
  success, result, repo_name = clone_repository(github_url)
393
 
 
395
  repo_path = result
396
  repo_info = get_repo_info(repo_path)
397
 
 
398
  cl.user_session.set("repo_path", repo_path)
399
  cl.user_session.set("repo_info", repo_info)
400
 
401
+ # Index repository
402
  try:
403
+ index_codebase(repo_path)
 
404
  except Exception as e:
405
+ print(f"[CHAINLIT] Indexing failed: {e}")
406
 
 
407
  languages = ", ".join(repo_info["languages"][:5]) if repo_info["languages"] else "Unknown"
 
 
 
 
 
408
 
409
  task_context = f"""
410
  [REPOSITORY CONTEXT]
 
412
  Path: {repo_path}
413
  Total Files: {repo_info['total_files']}
414
  Languages: {languages}
 
 
415
 
416
  AVAILABLE TOOLS:
417
+ - search_repository: Search using BM25
418
+ - read_file: Read file (use full path: {repo_path}/...)
419
+ - search_code: Grep for patterns
420
  """
421
+ clone_msg.content = f"✅ **Cloned:** {repo_name} ({repo_info['total_files']} files, {languages})"
 
 
 
 
 
422
  await clone_msg.update()
 
423
  else:
424
+ clone_msg.content = f"❌ **Failed to clone:** {result}"
 
 
425
  await clone_msg.update()
426
  return
427
 
 
428
  elif cl.user_session.get("repo_path"):
429
  repo_path = cl.user_session.get("repo_path")
430
  repo_info = cl.user_session.get("repo_info")
 
438
  Languages: {languages}
439
 
440
  AVAILABLE TOOLS:
441
+ - search_repository: Search using BM25
442
+ - read_file: Read file (use full path: {repo_path}/...)
443
+ - search_code: Grep for patterns
444
  """
445
 
446
+ # Extract user query (remove URL if present)
 
447
  user_query = message.content
 
 
 
448
  if github_url:
 
 
449
  user_query = re.sub(r'https?://github\.com/[^\s]+', '', user_query).strip()
 
450
 
451
  full_task = task_context + "\n\n" + user_query if task_context else user_query
452
 
453
+ # Run workflow
454
+ result, total_cost = await run_workflow(orchestrator, full_task, is_resume=False)
 
 
455
 
456
+ if not result:
457
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
 
459
+ # Check if clarification needed
460
+ if result.get('status') == 'clarifying' and result.get('clarifying_questions'):
461
+ cl.user_session.set("waiting_for_clarification", True)
462
+ await cl.Message(
463
+ content=f"## Before I proceed, I have some questions:\n\n{result['clarifying_questions']}\n\n**Please answer above to continue.**"
464
+ ).send()
465
+ return
466
 
467
+ # Show final result
468
+ await cl.Message(content=format_final_result(result, total_cost)).send()
 
 
469
 
470
+ # Show generated code
471
+ if result.get('code_changes'):
472
+ await cl.Message(content="## Generated Code\n\n" + format_code_output(result['code_changes'])).send()
473
 
474
+ # Show plan if available
475
+ if result.get('plan') and not result.get('code_changes'):
476
+ await cl.Message(content=f"## Plan\n\n{result['plan']}").send()
477
 
478
 
479
  if __name__ == "__main__":
 
480
  sys.exit("Run with: chainlit run chainlit_app.py")