portacode 0.3.19.dev7__tar.gz → 0.3.19.dev9__tar.gz

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 (45) hide show
  1. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/PKG-INFO +1 -1
  2. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/_version.py +2 -2
  3. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/handlers/project_state_handlers.py +168 -54
  4. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode.egg-info/PKG-INFO +1 -1
  5. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/.claude/agents/communication-manager.md +0 -0
  6. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/.claude/settings.local.json +0 -0
  7. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/.gitignore +0 -0
  8. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/.gitmodules +0 -0
  9. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/LICENSE +0 -0
  10. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/MANIFEST.in +0 -0
  11. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/Makefile +0 -0
  12. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/README.md +0 -0
  13. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/backup.sh +0 -0
  14. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/docker-compose.yaml +0 -0
  15. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/README.md +0 -0
  16. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/__init__.py +0 -0
  17. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/__main__.py +0 -0
  18. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/cli.py +0 -0
  19. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/README.md +0 -0
  20. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/__init__.py +0 -0
  21. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/client.py +0 -0
  22. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/handlers/README.md +0 -0
  23. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -0
  24. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/handlers/__init__.py +0 -0
  25. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/handlers/base.py +0 -0
  26. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/handlers/file_handlers.py +0 -0
  27. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/handlers/registry.py +0 -0
  28. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/handlers/session.py +0 -0
  29. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/handlers/system_handlers.py +0 -0
  30. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/handlers/tab_factory.py +0 -0
  31. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/handlers/terminal_handlers.py +0 -0
  32. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/multiplex.py +0 -0
  33. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/connection/terminal.py +0 -0
  34. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/data.py +0 -0
  35. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/keypair.py +0 -0
  36. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode/service.py +0 -0
  37. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode.egg-info/SOURCES.txt +0 -0
  38. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode.egg-info/dependency_links.txt +0 -0
  39. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode.egg-info/entry_points.txt +0 -0
  40. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode.egg-info/requires.txt +0 -0
  41. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/portacode.egg-info/top_level.txt +0 -0
  42. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/pyproject.toml +0 -0
  43. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/restore.sh +0 -0
  44. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/setup.cfg +0 -0
  45. {portacode-0.3.19.dev7 → portacode-0.3.19.dev9}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.19.dev7
3
+ Version: 0.3.19.dev9
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.3.19.dev7'
21
- __version_tuple__ = version_tuple = (0, 3, 19, 'dev7')
20
+ __version__ = version = '0.3.19.dev9'
21
+ __version_tuple__ = version_tuple = (0, 3, 19, 'dev9')
@@ -300,9 +300,18 @@ class GitManager:
300
300
  logger.debug("diff-match-patch not available, skipping diff details computation")
301
301
  return None
302
302
 
303
+ # Add performance safeguards to prevent blocking
304
+ max_content_size = 50000 # 50KB max per file for diff details
305
+ if len(original_content) > max_content_size or len(modified_content) > max_content_size:
306
+ logger.debug("File too large for diff details computation")
307
+ return None
308
+
303
309
  try:
304
310
  dmp = diff_match_patch()
305
311
 
312
+ # Set timeout for diff computation
313
+ dmp.Diff_Timeout = 1.0 # 1 second timeout
314
+
306
315
  # Compute the diff
307
316
  diffs = dmp.diff_main(original_content, modified_content)
308
317
 
@@ -344,7 +353,8 @@ class GitManager:
344
353
 
345
354
  try:
346
355
  # Use Pygments' built-in filename detection
347
- return get_lexer_for_filename(file_path)
356
+ lexer = get_lexer_for_filename(file_path)
357
+ return lexer
348
358
  except ClassNotFound:
349
359
  # If no lexer found, return None (will fall back to plain text)
350
360
  logger.debug("No Pygments lexer found for file: %s", file_path)
@@ -359,14 +369,38 @@ class GitManager:
359
369
  logger.debug("Pygments not available for HTML diff generation")
360
370
  return None
361
371
 
372
+ # Add performance safeguards to prevent blocking
373
+ max_content_size = 100000 # 100KB max per file
374
+ max_lines = 1000 # Max 1000 lines per file
375
+
376
+ if len(original_content) > max_content_size or len(modified_content) > max_content_size:
377
+ logger.warning(f"File too large for diff generation: {file_path}")
378
+ return None
379
+
380
+ original_line_count = original_content.count('\n')
381
+ modified_line_count = modified_content.count('\n')
382
+
383
+ if original_line_count > max_lines or modified_line_count > max_lines:
384
+ logger.warning(f"Too many lines for diff generation: {file_path} ({max(original_line_count, modified_line_count)} lines)")
385
+ return None
386
+
362
387
  try:
363
388
  import difflib
389
+ import time
390
+
391
+ start_time = time.time()
392
+ timeout_seconds = 5 # 5 second timeout
364
393
 
365
394
  # Get line-based diff using Python's difflib (similar to git diff)
366
395
  original_lines = original_content.splitlines(keepends=True)
367
396
  modified_lines = modified_content.splitlines(keepends=True)
368
397
 
369
- # Generate both minimal (3 lines context) and full context diffs
398
+ # Check timeout
399
+ if time.time() - start_time > timeout_seconds:
400
+ logger.warning(f"Diff generation timeout for {file_path}")
401
+ return None
402
+
403
+ # Generate minimal diff only to improve performance
370
404
  minimal_diff_lines = list(difflib.unified_diff(
371
405
  original_lines,
372
406
  modified_lines,
@@ -376,34 +410,96 @@ class GitManager:
376
410
  n=3 # 3 lines of context (default)
377
411
  ))
378
412
 
379
- full_diff_lines = list(difflib.unified_diff(
380
- original_lines,
381
- modified_lines,
382
- fromfile='a/' + os.path.basename(file_path),
383
- tofile='b/' + os.path.basename(file_path),
384
- lineterm='',
385
- n=len(original_lines) + len(modified_lines) # Show all lines
386
- ))
413
+ # Check timeout again
414
+ if time.time() - start_time > timeout_seconds:
415
+ logger.warning(f"Diff generation timeout for {file_path}")
416
+ return None
417
+
418
+ # Parse diff (simplified - no intraline processing to improve performance)
419
+ minimal_parsed = self._parse_unified_diff_simple(minimal_diff_lines)
387
420
 
388
- # Parse both diffs
389
- minimal_parsed = self._parse_unified_diff_with_intraline(minimal_diff_lines, original_lines, modified_lines)
390
- full_parsed = self._parse_unified_diff_with_intraline(full_diff_lines, original_lines, modified_lines)
421
+ # Check timeout
422
+ if time.time() - start_time > timeout_seconds:
423
+ logger.warning(f"Diff generation timeout for {file_path}")
424
+ return None
391
425
 
392
- # Generate HTML for both versions
426
+ # Generate HTML for minimal version only
393
427
  minimal_html = self._generate_diff_html(minimal_parsed, file_path, 'minimal')
394
- full_html = self._generate_diff_html(full_parsed, file_path, 'full')
395
428
 
396
429
  return {
397
430
  'minimal': minimal_html,
398
- 'full': full_html
431
+ 'full': minimal_html # Use same for both to improve performance
399
432
  }
400
433
 
401
434
  except Exception as e:
402
435
  logger.error("Error generating HTML diff: %s", e)
403
436
  return None
404
437
 
438
+ def _parse_unified_diff_simple(self, diff_lines):
439
+ """Simple unified diff parser without intra-line highlighting for better performance."""
440
+ parsed = []
441
+ old_line_num = 0
442
+ new_line_num = 0
443
+
444
+ for line in diff_lines:
445
+ if line.startswith('@@'):
446
+ # Parse hunk header to get line numbers
447
+ import re
448
+ match = re.match(r'@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@', line)
449
+ if match:
450
+ old_line_num = int(match.group(1)) - 1
451
+ new_line_num = int(match.group(2)) - 1
452
+
453
+ parsed.append({
454
+ 'type': 'header',
455
+ 'content': line,
456
+ 'old_line_num': '',
457
+ 'new_line_num': ''
458
+ })
459
+ elif line.startswith('---') or line.startswith('+++'):
460
+ # Skip diff file headers (--- a/file, +++ b/file)
461
+ parsed.append({
462
+ 'type': 'header',
463
+ 'content': line,
464
+ 'old_line_num': '',
465
+ 'new_line_num': ''
466
+ })
467
+ elif line.startswith('-'):
468
+ old_line_num += 1
469
+ parsed.append({
470
+ 'type': 'delete',
471
+ 'old_line_num': old_line_num,
472
+ 'new_line_num': '',
473
+ 'content': line
474
+ })
475
+ elif line.startswith('+'):
476
+ new_line_num += 1
477
+ parsed.append({
478
+ 'type': 'add',
479
+ 'old_line_num': '',
480
+ 'new_line_num': new_line_num,
481
+ 'content': line
482
+ })
483
+ elif line.startswith(' '):
484
+ old_line_num += 1
485
+ new_line_num += 1
486
+ parsed.append({
487
+ 'type': 'context',
488
+ 'old_line_num': old_line_num,
489
+ 'new_line_num': new_line_num,
490
+ 'content': line
491
+ })
492
+
493
+ return parsed
494
+
405
495
  def _generate_diff_html(self, parsed_diff: List[Dict], file_path: str, view_mode: str) -> str:
406
496
  """Generate HTML for a parsed diff."""
497
+ # Limit diff size to prevent performance issues
498
+ max_diff_lines = 200
499
+ if len(parsed_diff) > max_diff_lines:
500
+ logger.warning(f"Diff too large, truncating: {file_path} ({len(parsed_diff)} lines)")
501
+ parsed_diff = parsed_diff[:max_diff_lines]
502
+
407
503
  # Get Pygments lexer for syntax highlighting
408
504
  lexer = self._get_pygments_lexer(file_path)
409
505
 
@@ -443,44 +539,40 @@ class GitManager:
443
539
  old_line_num = line_info.get('old_line_num', '')
444
540
  new_line_num = line_info.get('new_line_num', '')
445
541
  content = line_info['content']
446
- intraline_html = line_info.get('intraline_html', '')
447
542
 
448
- # Use intra-line highlighted content if available
449
- if intraline_html:
450
- final_content = intraline_html
451
- else:
452
- # Apply syntax highlighting to the content
453
- if content and content[0] in '+-':
454
- # For added/deleted lines, highlight the content without the prefix
455
- prefix = content[0]
456
- clean_content = content[1:]
457
-
458
- if lexer and clean_content.strip():
459
- try:
460
- # Apply syntax highlighting
461
- highlighted = highlight(clean_content, lexer, HtmlFormatter(nowrap=True, noclasses=False))
462
- final_content = prefix + highlighted
463
- except Exception as e:
464
- logger.debug("Error applying syntax highlighting: %s", e)
465
- final_content = self._escape_html(content)
466
- else:
543
+ # Apply syntax highlighting to the content
544
+ if content and content[0] in '+-':
545
+ # For added/deleted lines, highlight the content without the prefix
546
+ prefix = content[0]
547
+ clean_content = content[1:]
548
+
549
+ if lexer and clean_content.strip():
550
+ try:
551
+ # Apply syntax highlighting using Monokai theme to match ACE editor
552
+ highlighted = highlight(clean_content, lexer, HtmlFormatter(nowrap=True, noclasses=False, style='monokai'))
553
+ final_content = prefix + highlighted
554
+ except Exception as e:
555
+ logger.debug("Error applying syntax highlighting: %s", e)
467
556
  final_content = self._escape_html(content)
468
- elif content and content[0] == ' ':
469
- # For context lines, highlight without the space prefix
470
- clean_content = content[1:]
471
-
472
- if lexer and clean_content.strip():
473
- try:
474
- # Apply syntax highlighting
475
- highlighted = highlight(clean_content, lexer, HtmlFormatter(nowrap=True, noclasses=False))
476
- final_content = ' ' + highlighted
477
- except Exception as e:
478
- logger.debug("Error applying syntax highlighting: %s", e)
479
- final_content = self._escape_html(content)
480
- else:
557
+ else:
558
+ final_content = self._escape_html(content)
559
+
560
+ elif content and content[0] == ' ':
561
+ # For context lines, highlight without the space prefix
562
+ clean_content = content[1:]
563
+
564
+ if lexer and clean_content.strip():
565
+ try:
566
+ # Apply syntax highlighting using Monokai theme to match ACE editor
567
+ highlighted = highlight(clean_content, lexer, HtmlFormatter(nowrap=True, noclasses=False, style='monokai'))
568
+ final_content = ' ' + highlighted
569
+ except Exception as e:
570
+ logger.debug("Error applying syntax highlighting: %s", e)
481
571
  final_content = self._escape_html(content)
482
572
  else:
483
573
  final_content = self._escape_html(content)
574
+ else:
575
+ final_content = self._escape_html(content)
484
576
 
485
577
  # CSS classes for different line types
486
578
  row_class = f'diff-line diff-{line_type}'
@@ -550,6 +642,14 @@ class GitManager:
550
642
  old_line_num = int(match.group(1)) - 1
551
643
  new_line_num = int(match.group(2)) - 1
552
644
 
645
+ parsed.append({
646
+ 'type': 'header',
647
+ 'content': line,
648
+ 'old_line_num': '',
649
+ 'new_line_num': ''
650
+ })
651
+ elif line.startswith('---') or line.startswith('+++'):
652
+ # Skip diff file headers (--- a/file, +++ b/file)
553
653
  parsed.append({
554
654
  'type': 'header',
555
655
  'content': line,
@@ -599,6 +699,9 @@ class GitManager:
599
699
 
600
700
  def _generate_intraline_diff(self, old_text: str, new_text: str) -> Tuple[str, str]:
601
701
  """Generate intra-line character-level diff highlighting."""
702
+ # Temporarily disable intraline highlighting to fix performance issues
703
+ return self._escape_html(old_text), self._escape_html(new_text)
704
+
602
705
  if not DIFF_MATCH_PATCH_AVAILABLE:
603
706
  return self._escape_html(old_text), self._escape_html(new_text)
604
707
 
@@ -1563,7 +1666,13 @@ class ProjectStateManager:
1563
1666
  diff_details = git_manager._compute_diff_details(original_content, modified_content)
1564
1667
 
1565
1668
  # Generate HTML diff with syntax highlighting (both minimal and full context)
1566
- html_diff_versions = git_manager._generate_html_diff(original_content, modified_content, file_path)
1669
+ # Wrap in try-catch to prevent blocking the WebSocket handler
1670
+ html_diff_versions = None
1671
+ try:
1672
+ html_diff_versions = git_manager._generate_html_diff(original_content, modified_content, file_path)
1673
+ except Exception as e:
1674
+ logger.error(f"Error generating HTML diff for {file_path}: {e}")
1675
+ # Continue without HTML diff - fallback to basic diff will be used
1567
1676
 
1568
1677
  # Create a descriptive title for the diff
1569
1678
  title_parts = []
@@ -1585,14 +1694,19 @@ class ProjectStateManager:
1585
1694
  )
1586
1695
 
1587
1696
  # Add metadata about the diff references
1588
- diff_tab.metadata.update({
1697
+ metadata_update = {
1589
1698
  'from_ref': from_ref,
1590
1699
  'to_ref': to_ref,
1591
1700
  'from_hash': from_hash,
1592
1701
  'to_hash': to_hash,
1593
- 'diff_timeline': True,
1594
- 'html_diff_versions': html_diff_versions
1595
- })
1702
+ 'diff_timeline': True
1703
+ }
1704
+
1705
+ # Only add HTML diff versions if they were successfully generated
1706
+ if html_diff_versions:
1707
+ metadata_update['html_diff_versions'] = html_diff_versions
1708
+
1709
+ diff_tab.metadata.update(metadata_update)
1596
1710
 
1597
1711
  project_state.open_tabs[tab_key] = diff_tab
1598
1712
  project_state.active_tab = diff_tab
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.19.dev7
3
+ Version: 0.3.19.dev9
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
File without changes